mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-12 04:18:54 +00:00
chore: improve password error message (#6255)
chore: improve error message chore: add password minlength & maxlength i18n chore: check max length fix: i18n variables feat: add CredentialsRequirementType
This commit is contained in:
@@ -56,7 +56,7 @@ AFFiNE.port = 3010;
|
|||||||
// /* User Signup password limitation */
|
// /* User Signup password limitation */
|
||||||
// AFFiNE.auth.password = {
|
// AFFiNE.auth.password = {
|
||||||
// minLength: 8,
|
// minLength: 8,
|
||||||
// maxLength: 20,
|
// maxLength: 32,
|
||||||
// };
|
// };
|
||||||
//
|
//
|
||||||
// /* How long the login session would last by default */
|
// /* How long the login session would last by default */
|
||||||
|
|||||||
@@ -22,6 +22,20 @@ export function ADD_ENABLED_FEATURES(feature: ServerFeature) {
|
|||||||
ENABLED_FEATURES.add(feature);
|
ENABLED_FEATURES.add(feature);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ObjectType()
|
||||||
|
export class PasswordLimitsType {
|
||||||
|
@Field()
|
||||||
|
minLength!: number;
|
||||||
|
@Field()
|
||||||
|
maxLength!: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ObjectType()
|
||||||
|
export class CredentialsRequirementType {
|
||||||
|
@Field()
|
||||||
|
password!: PasswordLimitsType;
|
||||||
|
}
|
||||||
|
|
||||||
@ObjectType()
|
@ObjectType()
|
||||||
export class ServerConfigType {
|
export class ServerConfigType {
|
||||||
@Field({
|
@Field({
|
||||||
@@ -47,6 +61,11 @@ export class ServerConfigType {
|
|||||||
|
|
||||||
@Field(() => [ServerFeature], { description: 'enabled server features' })
|
@Field(() => [ServerFeature], { description: 'enabled server features' })
|
||||||
features!: ServerFeature[];
|
features!: ServerFeature[];
|
||||||
|
|
||||||
|
@Field(() => CredentialsRequirementType, {
|
||||||
|
description: 'credentials requirement',
|
||||||
|
})
|
||||||
|
credentialsRequirement!: CredentialsRequirementType;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ServerConfigResolver {
|
export class ServerConfigResolver {
|
||||||
@@ -65,6 +84,9 @@ export class ServerConfigResolver {
|
|||||||
// this field should be removed after frontend feature flags implemented
|
// this field should be removed after frontend feature flags implemented
|
||||||
flavor: AFFiNE.type,
|
flavor: AFFiNE.type,
|
||||||
features: Array.from(ENABLED_FEATURES),
|
features: Array.from(ENABLED_FEATURES),
|
||||||
|
credentialsRequirement: {
|
||||||
|
password: AFFiNE.auth.password,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -214,9 +214,14 @@ export interface AFFiNEConfig {
|
|||||||
* authentication config
|
* authentication config
|
||||||
*/
|
*/
|
||||||
auth: {
|
auth: {
|
||||||
|
/**
|
||||||
|
* The minimum and maximum length of the password when registering new users
|
||||||
|
*
|
||||||
|
* @default [8,32]
|
||||||
|
*/
|
||||||
password: {
|
password: {
|
||||||
/**
|
/**
|
||||||
* The minimum and maximum length of the password when registering new users
|
* The minimum length of the password
|
||||||
*
|
*
|
||||||
* @default 8
|
* @default 8
|
||||||
*/
|
*/
|
||||||
@@ -224,7 +229,7 @@ export interface AFFiNEConfig {
|
|||||||
/**
|
/**
|
||||||
* The maximum length of the password
|
* The maximum length of the password
|
||||||
*
|
*
|
||||||
* @default 20
|
* @default 32
|
||||||
*/
|
*/
|
||||||
maxLength: number;
|
maxLength: number;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -10,6 +10,10 @@ input CreateCheckoutSessionInput {
|
|||||||
successCallbackLink: String
|
successCallbackLink: String
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type CredentialsRequirementType {
|
||||||
|
password: PasswordLimitsType!
|
||||||
|
}
|
||||||
|
|
||||||
"""
|
"""
|
||||||
A date-time string at UTC, such as 2019-12-03T09:54:33Z, compliant with the date-time format.
|
A date-time string at UTC, such as 2019-12-03T09:54:33Z, compliant with the date-time format.
|
||||||
"""
|
"""
|
||||||
@@ -164,6 +168,11 @@ enum OAuthProviderType {
|
|||||||
Google
|
Google
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type PasswordLimitsType {
|
||||||
|
maxLength: Int!
|
||||||
|
minLength: Int!
|
||||||
|
}
|
||||||
|
|
||||||
"""User permission in workspace"""
|
"""User permission in workspace"""
|
||||||
enum Permission {
|
enum Permission {
|
||||||
Admin
|
Admin
|
||||||
@@ -236,6 +245,9 @@ type ServerConfigType {
|
|||||||
"""server base url"""
|
"""server base url"""
|
||||||
baseUrl: String!
|
baseUrl: String!
|
||||||
|
|
||||||
|
"""credentials requirement"""
|
||||||
|
credentialsRequirement: CredentialsRequirementType!
|
||||||
|
|
||||||
"""enabled server features"""
|
"""enabled server features"""
|
||||||
features: [ServerFeature!]!
|
features: [ServerFeature!]!
|
||||||
|
|
||||||
|
|||||||
@@ -70,7 +70,8 @@
|
|||||||
"react-virtuoso": "^4.7.0",
|
"react-virtuoso": "^4.7.0",
|
||||||
"rxjs": "^7.8.1",
|
"rxjs": "^7.8.1",
|
||||||
"swr": "^2.2.5",
|
"swr": "^2.2.5",
|
||||||
"uuid": "^9.0.1"
|
"uuid": "^9.0.1",
|
||||||
|
"zod": "^3.22.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@blocksuite/block-std": "0.14.0-canary-202403250855-4171ecd",
|
"@blocksuite/block-std": "0.14.0-canary-202403250855-4171ecd",
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type { PasswordLimitsFragment } from '@affine/graphql';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import { useSetAtom } from 'jotai';
|
import { useSetAtom } from 'jotai';
|
||||||
import type { FC } from 'react';
|
import type { FC } from 'react';
|
||||||
@@ -11,9 +12,15 @@ import type { User } from './type';
|
|||||||
|
|
||||||
export const ChangePasswordPage: FC<{
|
export const ChangePasswordPage: FC<{
|
||||||
user: User;
|
user: User;
|
||||||
|
passwordLimits: PasswordLimitsFragment;
|
||||||
onSetPassword: (password: string) => Promise<void>;
|
onSetPassword: (password: string) => Promise<void>;
|
||||||
onOpenAffine: () => void;
|
onOpenAffine: () => void;
|
||||||
}> = ({ user: { email }, onSetPassword: propsOnSetPassword, onOpenAffine }) => {
|
}> = ({
|
||||||
|
user: { email },
|
||||||
|
passwordLimits,
|
||||||
|
onSetPassword: propsOnSetPassword,
|
||||||
|
onOpenAffine,
|
||||||
|
}) => {
|
||||||
const t = useAFFiNEI18N();
|
const t = useAFFiNEI18N();
|
||||||
const [hasSetUp, setHasSetUp] = useState(false);
|
const [hasSetUp, setHasSetUp] = useState(false);
|
||||||
const pushNotification = useSetAtom(pushNotificationAtom);
|
const pushNotification = useSetAtom(pushNotificationAtom);
|
||||||
@@ -46,7 +53,10 @@ export const ChangePasswordPage: FC<{
|
|||||||
t['com.affine.auth.sent.reset.password.success.message']()
|
t['com.affine.auth.sent.reset.password.success.message']()
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{t['com.affine.auth.page.sent.email.subtitle']()}
|
{t['com.affine.auth.page.sent.email.subtitle']({
|
||||||
|
min: String(passwordLimits.minLength),
|
||||||
|
max: String(passwordLimits.maxLength),
|
||||||
|
})}
|
||||||
<a href={`mailto:${email}`}>{email}</a>
|
<a href={`mailto:${email}`}>{email}</a>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
@@ -57,7 +67,10 @@ export const ChangePasswordPage: FC<{
|
|||||||
{t['com.affine.auth.open.affine']()}
|
{t['com.affine.auth.open.affine']()}
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<SetPassword onSetPassword={onSetPassword} />
|
<SetPassword
|
||||||
|
passwordLimits={passwordLimits}
|
||||||
|
onSetPassword={onSetPassword}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</AuthPageContainer>
|
</AuthPageContainer>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,104 +1,199 @@
|
|||||||
|
import { type PasswordLimitsFragment } from '@affine/graphql';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import { passwordStrength } from 'check-password-strength';
|
import { type Options, passwordStrength } from 'check-password-strength';
|
||||||
import type { FC } from 'react';
|
import { type FC, useEffect, useMemo } from 'react';
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useState } from 'react';
|
||||||
|
import { z, type ZodCustomIssue, ZodIssueCode } from 'zod';
|
||||||
|
|
||||||
import type { InputProps } from '../../../ui/input';
|
import type { InputProps } from '../../../ui/input';
|
||||||
import { Input } from '../../../ui/input';
|
import { Input } from '../../../ui/input';
|
||||||
import * as styles from '../share.css';
|
import * as styles from '../share.css';
|
||||||
import { ErrorIcon } from './error';
|
import { ErrorIcon } from './error';
|
||||||
|
import { statusWrapper } from './style.css';
|
||||||
import { SuccessIcon } from './success';
|
import { SuccessIcon } from './success';
|
||||||
import { Tag } from './tag';
|
import { Tag } from './tag';
|
||||||
|
|
||||||
export type Status = 'weak' | 'medium' | 'strong' | 'maximum';
|
export type Status = 'weak' | 'medium' | 'strong' | 'minimum' | 'maximum';
|
||||||
|
|
||||||
|
const PASSWORD_STRENGTH_OPTIONS: Options<string> = [
|
||||||
|
{
|
||||||
|
id: 0,
|
||||||
|
value: 'weak',
|
||||||
|
minDiversity: 0,
|
||||||
|
minLength: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
value: 'medium',
|
||||||
|
minDiversity: 4,
|
||||||
|
minLength: 8,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
value: 'strong',
|
||||||
|
minDiversity: 4,
|
||||||
|
minLength: 10,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
export const PasswordInput: FC<
|
export const PasswordInput: FC<
|
||||||
InputProps & {
|
InputProps & {
|
||||||
|
passwordLimits: PasswordLimitsFragment;
|
||||||
onPass: (password: string) => void;
|
onPass: (password: string) => void;
|
||||||
onPrevent: () => void;
|
onPrevent: () => void;
|
||||||
}
|
}
|
||||||
> = ({ onPass, onPrevent, ...inputProps }) => {
|
> = ({ passwordLimits, onPass, onPrevent, ...inputProps }) => {
|
||||||
const t = useAFFiNEI18N();
|
const t = useAFFiNEI18N();
|
||||||
|
|
||||||
const [status, setStatus] = useState<Status | null>(null);
|
const [status, setStatus] = useState<Status | null>(null);
|
||||||
const [confirmStatus, setConfirmStatus] = useState<
|
const [confirmStatus, setConfirmStatus] = useState<
|
||||||
'success' | 'error' | null
|
'success' | 'error' | null
|
||||||
>(null);
|
>(null);
|
||||||
|
const [canSubmit, setCanSubmit] = useState(false);
|
||||||
|
|
||||||
const [password, setPassWord] = useState('');
|
const [password, setPassWord] = useState('');
|
||||||
const [confirmPassword, setConfirmPassword] = useState('');
|
const [confirmPassword, setConfirmPassword] = useState('');
|
||||||
|
const validationSchema = useMemo(() => {
|
||||||
|
const { minLength, maxLength } = passwordLimits;
|
||||||
|
return z.string().superRefine((val, ctx) => {
|
||||||
|
if (val.length < minLength) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: ZodIssueCode.custom,
|
||||||
|
params: {
|
||||||
|
status: 'minimum',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else if (val.length > maxLength) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: ZodIssueCode.custom,
|
||||||
|
params: {
|
||||||
|
status: 'maximum',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// https://github.com/deanilvincent/check-password-strength?tab=readme-ov-file#default-options
|
||||||
|
const { value: status } = passwordStrength(
|
||||||
|
val,
|
||||||
|
PASSWORD_STRENGTH_OPTIONS
|
||||||
|
);
|
||||||
|
|
||||||
const onPasswordChange = useCallback((value: string) => {
|
ctx.addIssue({
|
||||||
setPassWord(value);
|
code: ZodIssueCode.custom,
|
||||||
if (!value) {
|
message: 'password strength',
|
||||||
return setStatus(null);
|
path: ['strength'],
|
||||||
}
|
params: {
|
||||||
if (value.length > 20) {
|
status,
|
||||||
return setStatus('maximum');
|
},
|
||||||
}
|
});
|
||||||
switch (passwordStrength(value).id) {
|
});
|
||||||
case 0:
|
}, [passwordLimits]);
|
||||||
case 1:
|
|
||||||
setStatus('weak');
|
|
||||||
break;
|
|
||||||
case 2:
|
|
||||||
setStatus('medium');
|
|
||||||
break;
|
|
||||||
case 3:
|
|
||||||
setStatus('strong');
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const onConfirmPasswordChange = useCallback((value: string) => {
|
const validatePasswords = useCallback(
|
||||||
setConfirmPassword(value);
|
(password: string, confirmPassword: string) => {
|
||||||
}, []);
|
const result = validationSchema.safeParse(password);
|
||||||
|
let canSubmit = false;
|
||||||
|
if (!result.success) {
|
||||||
|
const issues = result.error.issues as ZodCustomIssue[];
|
||||||
|
const firstIssue = issues[0];
|
||||||
|
setStatus(firstIssue.params?.status || null);
|
||||||
|
// ignore strength error
|
||||||
|
if (firstIssue.path.includes('strength')) {
|
||||||
|
canSubmit = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (confirmPassword) {
|
||||||
|
const isEqual = password === confirmPassword;
|
||||||
|
if (isEqual) {
|
||||||
|
setConfirmStatus('success');
|
||||||
|
} else {
|
||||||
|
setConfirmStatus('error');
|
||||||
|
}
|
||||||
|
canSubmit &&= isEqual;
|
||||||
|
} else {
|
||||||
|
canSubmit &&= false;
|
||||||
|
setConfirmStatus(null);
|
||||||
|
}
|
||||||
|
setCanSubmit(canSubmit);
|
||||||
|
},
|
||||||
|
[validationSchema]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onPasswordChange = useCallback(
|
||||||
|
(value: string) => {
|
||||||
|
const password = value.trim();
|
||||||
|
setPassWord(password);
|
||||||
|
validatePasswords(password, confirmPassword);
|
||||||
|
},
|
||||||
|
[validatePasswords, confirmPassword]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onConfirmPasswordChange = useCallback(
|
||||||
|
(value: string) => {
|
||||||
|
const confirmPassword = value.trim();
|
||||||
|
setConfirmPassword(confirmPassword);
|
||||||
|
validatePasswords(password, confirmPassword);
|
||||||
|
},
|
||||||
|
[validatePasswords, password]
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!password || !confirmPassword) {
|
if (canSubmit) {
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (password === confirmPassword) {
|
|
||||||
setConfirmStatus('success');
|
|
||||||
} else {
|
|
||||||
setConfirmStatus('error');
|
|
||||||
}
|
|
||||||
}, [confirmPassword, password]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (confirmStatus === 'success' && password.length > 7) {
|
|
||||||
onPass(password);
|
onPass(password);
|
||||||
} else {
|
} else {
|
||||||
onPrevent();
|
onPrevent();
|
||||||
}
|
}
|
||||||
}, [confirmStatus, onPass, onPrevent, password]);
|
}, [canSubmit, password, onPass, onPrevent]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Input
|
<Input
|
||||||
|
name="password"
|
||||||
className={styles.input}
|
className={styles.input}
|
||||||
type="password"
|
type="password"
|
||||||
size="extraLarge"
|
size="extraLarge"
|
||||||
|
minLength={passwordLimits.minLength}
|
||||||
|
maxLength={passwordLimits.maxLength}
|
||||||
style={{ marginBottom: 20 }}
|
style={{ marginBottom: 20 }}
|
||||||
placeholder={t['com.affine.auth.set.password.placeholder']()}
|
placeholder={t['com.affine.auth.set.password.placeholder']({
|
||||||
|
min: String(passwordLimits.minLength),
|
||||||
|
})}
|
||||||
onChange={onPasswordChange}
|
onChange={onPasswordChange}
|
||||||
endFix={status ? <Tag status={status} /> : null}
|
endFix={
|
||||||
|
<div className={statusWrapper}>
|
||||||
|
{status ? (
|
||||||
|
<Tag
|
||||||
|
status={status}
|
||||||
|
minimum={t['com.affine.auth.set.password.message.minlength']({
|
||||||
|
min: String(passwordLimits.minLength),
|
||||||
|
})}
|
||||||
|
maximum={t['com.affine.auth.set.password.message.maxlength']({
|
||||||
|
max: String(passwordLimits.maxLength),
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
{...inputProps}
|
{...inputProps}
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
|
name="confirmPassword"
|
||||||
className={styles.input}
|
className={styles.input}
|
||||||
type="password"
|
type="password"
|
||||||
size="extraLarge"
|
size="extraLarge"
|
||||||
|
minLength={passwordLimits.minLength}
|
||||||
|
maxLength={passwordLimits.maxLength}
|
||||||
placeholder={t['com.affine.auth.set.password.placeholder.confirm']()}
|
placeholder={t['com.affine.auth.set.password.placeholder.confirm']()}
|
||||||
onChange={onConfirmPasswordChange}
|
onChange={onConfirmPasswordChange}
|
||||||
endFix={
|
endFix={
|
||||||
confirmStatus ? (
|
<div className={statusWrapper}>
|
||||||
confirmStatus === 'success' ? (
|
{confirmStatus ? (
|
||||||
<SuccessIcon />
|
confirmStatus === 'success' ? (
|
||||||
) : (
|
<SuccessIcon />
|
||||||
<ErrorIcon />
|
) : (
|
||||||
)
|
<ErrorIcon />
|
||||||
) : null
|
)
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
{...inputProps}
|
{...inputProps}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
import { cssVar } from '@toeverything/theme';
|
import { cssVar } from '@toeverything/theme';
|
||||||
import { style } from '@vanilla-extract/css';
|
import { style } from '@vanilla-extract/css';
|
||||||
|
export const statusWrapper = style({
|
||||||
|
marginLeft: 8,
|
||||||
|
marginRight: 10,
|
||||||
|
});
|
||||||
export const tag = style({
|
export const tag = style({
|
||||||
padding: '0 15px',
|
padding: '2px 15px',
|
||||||
height: 20,
|
height: 20,
|
||||||
lineHeight: '20px',
|
lineHeight: '20px',
|
||||||
borderRadius: 10,
|
borderRadius: 10,
|
||||||
@@ -19,7 +23,7 @@ export const tag = style({
|
|||||||
backgroundColor: cssVar('tagGreen'),
|
backgroundColor: cssVar('tagGreen'),
|
||||||
color: cssVar('successColor'),
|
color: cssVar('successColor'),
|
||||||
},
|
},
|
||||||
'&.maximum': {
|
'&.minimum, &.maximum': {
|
||||||
backgroundColor: cssVar('tagRed'),
|
backgroundColor: cssVar('tagRed'),
|
||||||
color: cssVar('errorColor'),
|
color: cssVar('errorColor'),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -4,15 +4,23 @@ import { useMemo } from 'react';
|
|||||||
|
|
||||||
import type { Status } from './index';
|
import type { Status } from './index';
|
||||||
import { tag } from './style.css';
|
import { tag } from './style.css';
|
||||||
export const Tag: FC<{ status: Status }> = ({ status }) => {
|
|
||||||
|
type TagProps = {
|
||||||
|
status: Status;
|
||||||
|
minimum: string;
|
||||||
|
maximum: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Tag: FC<TagProps> = ({ status, minimum, maximum }) => {
|
||||||
const textMap = useMemo<{ [K in Status]: string }>(() => {
|
const textMap = useMemo<{ [K in Status]: string }>(() => {
|
||||||
return {
|
return {
|
||||||
weak: 'Weak',
|
weak: 'Weak',
|
||||||
medium: 'Medium',
|
medium: 'Medium',
|
||||||
strong: 'Strong',
|
strong: 'Strong',
|
||||||
maximum: 'Maximum',
|
minimum,
|
||||||
|
maximum,
|
||||||
};
|
};
|
||||||
}, []);
|
}, [minimum, maximum]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -20,6 +28,7 @@ export const Tag: FC<{ status: Status }> = ({ status }) => {
|
|||||||
weak: status === 'weak',
|
weak: status === 'weak',
|
||||||
medium: status === 'medium',
|
medium: status === 'medium',
|
||||||
strong: status === 'strong',
|
strong: status === 'strong',
|
||||||
|
minimum: status === 'minimum',
|
||||||
maximum: status === 'maximum',
|
maximum: status === 'maximum',
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type { PasswordLimitsFragment } from '@affine/graphql';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import { useSetAtom } from 'jotai';
|
import { useSetAtom } from 'jotai';
|
||||||
import type { FC } from 'react';
|
import type { FC } from 'react';
|
||||||
@@ -11,9 +12,15 @@ import type { User } from './type';
|
|||||||
|
|
||||||
export const SetPasswordPage: FC<{
|
export const SetPasswordPage: FC<{
|
||||||
user: User;
|
user: User;
|
||||||
|
passwordLimits: PasswordLimitsFragment;
|
||||||
onSetPassword: (password: string) => Promise<void>;
|
onSetPassword: (password: string) => Promise<void>;
|
||||||
onOpenAffine: () => void;
|
onOpenAffine: () => void;
|
||||||
}> = ({ user: { email }, onSetPassword: propsOnSetPassword, onOpenAffine }) => {
|
}> = ({
|
||||||
|
user: { email },
|
||||||
|
passwordLimits,
|
||||||
|
onSetPassword: propsOnSetPassword,
|
||||||
|
onOpenAffine,
|
||||||
|
}) => {
|
||||||
const t = useAFFiNEI18N();
|
const t = useAFFiNEI18N();
|
||||||
const [hasSetUp, setHasSetUp] = useState(false);
|
const [hasSetUp, setHasSetUp] = useState(false);
|
||||||
const pushNotification = useSetAtom(pushNotificationAtom);
|
const pushNotification = useSetAtom(pushNotificationAtom);
|
||||||
@@ -46,7 +53,10 @@ export const SetPasswordPage: FC<{
|
|||||||
t['com.affine.auth.sent.set.password.success.message']()
|
t['com.affine.auth.sent.set.password.success.message']()
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{t['com.affine.auth.page.sent.email.subtitle']()}
|
{t['com.affine.auth.page.sent.email.subtitle']({
|
||||||
|
min: String(passwordLimits.minLength),
|
||||||
|
max: String(passwordLimits.maxLength),
|
||||||
|
})}
|
||||||
<a href={`mailto:${email}`}>{email}</a>
|
<a href={`mailto:${email}`}>{email}</a>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
@@ -57,7 +67,10 @@ export const SetPasswordPage: FC<{
|
|||||||
{t['com.affine.auth.open.affine']()}
|
{t['com.affine.auth.open.affine']()}
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<SetPassword onSetPassword={onSetPassword} />
|
<SetPassword
|
||||||
|
passwordLimits={passwordLimits}
|
||||||
|
onSetPassword={onSetPassword}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</AuthPageContainer>
|
</AuthPageContainer>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type { PasswordLimitsFragment } from '@affine/graphql';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import type { FC } from 'react';
|
import type { FC } from 'react';
|
||||||
import { useCallback, useRef, useState } from 'react';
|
import { useCallback, useRef, useState } from 'react';
|
||||||
@@ -7,10 +8,11 @@ import { Wrapper } from '../../ui/layout';
|
|||||||
import { PasswordInput } from './password-input';
|
import { PasswordInput } from './password-input';
|
||||||
|
|
||||||
export const SetPassword: FC<{
|
export const SetPassword: FC<{
|
||||||
|
passwordLimits: PasswordLimitsFragment;
|
||||||
showLater?: boolean;
|
showLater?: boolean;
|
||||||
onLater?: () => void;
|
onLater?: () => void;
|
||||||
onSetPassword: (password: string) => void;
|
onSetPassword: (password: string) => void;
|
||||||
}> = ({ onLater, onSetPassword, showLater = false }) => {
|
}> = ({ passwordLimits, onLater, onSetPassword, showLater = false }) => {
|
||||||
const t = useAFFiNEI18N();
|
const t = useAFFiNEI18N();
|
||||||
|
|
||||||
const [passwordPass, setPasswordPass] = useState(false);
|
const [passwordPass, setPasswordPass] = useState(false);
|
||||||
@@ -20,6 +22,7 @@ export const SetPassword: FC<{
|
|||||||
<>
|
<>
|
||||||
<Wrapper marginTop={30} marginBottom={42}>
|
<Wrapper marginTop={30} marginBottom={42}>
|
||||||
<PasswordInput
|
<PasswordInput
|
||||||
|
passwordLimits={passwordLimits}
|
||||||
onPass={useCallback(password => {
|
onPass={useCallback(password => {
|
||||||
setPasswordPass(true);
|
setPasswordPass(true);
|
||||||
passwordRef.current = password;
|
passwordRef.current = password;
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type { PasswordLimitsFragment } from '@affine/graphql';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import { useSetAtom } from 'jotai';
|
import { useSetAtom } from 'jotai';
|
||||||
import type { FC } from 'react';
|
import type { FC } from 'react';
|
||||||
@@ -10,11 +11,13 @@ import { SetPassword } from './set-password';
|
|||||||
import type { User } from './type';
|
import type { User } from './type';
|
||||||
|
|
||||||
export const SignUpPage: FC<{
|
export const SignUpPage: FC<{
|
||||||
|
passwordLimits: PasswordLimitsFragment;
|
||||||
user: User;
|
user: User;
|
||||||
onSetPassword: (password: string) => Promise<void>;
|
onSetPassword: (password: string) => Promise<void>;
|
||||||
openButtonText?: string;
|
openButtonText?: string;
|
||||||
onOpenAffine: () => void;
|
onOpenAffine: () => void;
|
||||||
}> = ({
|
}> = ({
|
||||||
|
passwordLimits,
|
||||||
user: { email },
|
user: { email },
|
||||||
onSetPassword: propsOnSetPassword,
|
onSetPassword: propsOnSetPassword,
|
||||||
onOpenAffine,
|
onOpenAffine,
|
||||||
@@ -55,7 +58,10 @@ export const SignUpPage: FC<{
|
|||||||
t['com.affine.auth.sign.up.success.subtitle']()
|
t['com.affine.auth.sign.up.success.subtitle']()
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{t['com.affine.auth.page.sent.email.subtitle']()}
|
{t['com.affine.auth.page.sent.email.subtitle']({
|
||||||
|
min: String(passwordLimits.minLength),
|
||||||
|
max: String(passwordLimits.maxLength),
|
||||||
|
})}
|
||||||
<a href={`mailto:${email}`}>{email}</a>
|
<a href={`mailto:${email}`}>{email}</a>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
@@ -67,6 +73,7 @@ export const SignUpPage: FC<{
|
|||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<SetPassword
|
<SetPassword
|
||||||
|
passwordLimits={passwordLimits}
|
||||||
onSetPassword={onSetPassword}
|
onSetPassword={onSetPassword}
|
||||||
onLater={onLater}
|
onLater={onLater}
|
||||||
showLater={true}
|
showLater={true}
|
||||||
|
|||||||
@@ -7,8 +7,10 @@ import {
|
|||||||
} from '@affine/component/auth-components';
|
} from '@affine/component/auth-components';
|
||||||
import { pushNotificationAtom } from '@affine/component/notification-center';
|
import { pushNotificationAtom } from '@affine/component/notification-center';
|
||||||
import { Button } from '@affine/component/ui/button';
|
import { Button } from '@affine/component/ui/button';
|
||||||
|
import { useCredentialsRequirement } from '@affine/core/hooks/affine/use-server-config';
|
||||||
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
||||||
import {
|
import {
|
||||||
|
type PasswordLimitsFragment,
|
||||||
sendChangeEmailMutation,
|
sendChangeEmailMutation,
|
||||||
sendChangePasswordEmailMutation,
|
sendChangePasswordEmailMutation,
|
||||||
sendSetPasswordEmailMutation,
|
sendSetPasswordEmailMutation,
|
||||||
@@ -35,12 +37,19 @@ const useEmailTitle = (emailType: AuthPanelProps['emailType']) => {
|
|||||||
return t['com.affine.settings.email.action.verify']();
|
return t['com.affine.settings.email.action.verify']();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const useContent = (emailType: AuthPanelProps['emailType'], email: string) => {
|
const useContent = (
|
||||||
|
emailType: AuthPanelProps['emailType'],
|
||||||
|
email: string,
|
||||||
|
passwordLimits: PasswordLimitsFragment
|
||||||
|
) => {
|
||||||
const t = useAFFiNEI18N();
|
const t = useAFFiNEI18N();
|
||||||
|
|
||||||
switch (emailType) {
|
switch (emailType) {
|
||||||
case 'setPassword':
|
case 'setPassword':
|
||||||
return t['com.affine.auth.set.password.message']();
|
return t['com.affine.auth.set.password.message']({
|
||||||
|
min: String(passwordLimits.minLength),
|
||||||
|
max: String(passwordLimits.maxLength),
|
||||||
|
});
|
||||||
case 'changePassword':
|
case 'changePassword':
|
||||||
return t['com.affine.auth.reset.password.message']();
|
return t['com.affine.auth.reset.password.message']();
|
||||||
case 'changeEmail':
|
case 'changeEmail':
|
||||||
@@ -154,12 +163,13 @@ export const SendEmail = ({
|
|||||||
emailType,
|
emailType,
|
||||||
}: AuthPanelProps) => {
|
}: AuthPanelProps) => {
|
||||||
const t = useAFFiNEI18N();
|
const t = useAFFiNEI18N();
|
||||||
|
const { password: passwordLimits } = useCredentialsRequirement();
|
||||||
const [hasSentEmail, setHasSentEmail] = useState(false);
|
const [hasSentEmail, setHasSentEmail] = useState(false);
|
||||||
const pushNotification = useSetAtom(pushNotificationAtom);
|
const pushNotification = useSetAtom(pushNotificationAtom);
|
||||||
|
|
||||||
const title = useEmailTitle(emailType);
|
const title = useEmailTitle(emailType);
|
||||||
const hint = useNotificationHint(emailType);
|
const hint = useNotificationHint(emailType);
|
||||||
const content = useContent(emailType, email);
|
const content = useContent(emailType, email, passwordLimits);
|
||||||
const buttonContent = useButtonContent(emailType);
|
const buttonContent = useButtonContent(emailType);
|
||||||
const { loading, sendEmail } = useSendEmail(emailType);
|
const { loading, sendEmail } = useSendEmail(emailType);
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { SignUpPage } from '@affine/component/auth-components';
|
|||||||
import { Button } from '@affine/component/ui/button';
|
import { Button } from '@affine/component/ui/button';
|
||||||
import { Loading } from '@affine/component/ui/loading';
|
import { Loading } from '@affine/component/ui/loading';
|
||||||
import { AffineShapeIcon } from '@affine/core/components/page-list';
|
import { AffineShapeIcon } from '@affine/core/components/page-list';
|
||||||
|
import { useCredentialsRequirement } from '@affine/core/hooks/affine/use-server-config';
|
||||||
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
||||||
import type { SubscriptionPlan, SubscriptionRecurring } from '@affine/graphql';
|
import type { SubscriptionPlan, SubscriptionRecurring } from '@affine/graphql';
|
||||||
import {
|
import {
|
||||||
@@ -106,6 +107,7 @@ const SubscriptionRedirectWithData = () => {
|
|||||||
const user = useCurrentUser();
|
const user = useCurrentUser();
|
||||||
const searchData = useSubscriptionSearch();
|
const searchData = useSubscriptionSearch();
|
||||||
const openPaymentUrl = usePaymentRedirect();
|
const openPaymentUrl = usePaymentRedirect();
|
||||||
|
const { password: passwordLimits } = useCredentialsRequirement();
|
||||||
|
|
||||||
const { trigger: changePassword } = useMutation({
|
const { trigger: changePassword } = useMutation({
|
||||||
mutation: changePasswordMutation,
|
mutation: changePasswordMutation,
|
||||||
@@ -128,6 +130,7 @@ const SubscriptionRedirectWithData = () => {
|
|||||||
return (
|
return (
|
||||||
<SignUpPage
|
<SignUpPage
|
||||||
user={user}
|
user={user}
|
||||||
|
passwordLimits={passwordLimits}
|
||||||
onSetPassword={onSetPassword}
|
onSetPassword={onSetPassword}
|
||||||
onOpenAffine={openPaymentUrl}
|
onOpenAffine={openPaymentUrl}
|
||||||
openButtonText={t['com.affine.payment.subscription.go-to-subscribe']()}
|
openButtonText={t['com.affine.payment.subscription.go-to-subscribe']()}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { ServerFeature } from '@affine/graphql';
|
import type { ServerConfigQuery, ServerFeature } from '@affine/graphql';
|
||||||
import { oauthProvidersQuery, serverConfigQuery } from '@affine/graphql';
|
import { oauthProvidersQuery, serverConfigQuery } from '@affine/graphql';
|
||||||
import type { BareFetcher, Middleware } from 'swr';
|
import type { BareFetcher, Middleware } from 'swr';
|
||||||
|
|
||||||
@@ -73,3 +73,13 @@ export const useServerBaseUrl = () => {
|
|||||||
|
|
||||||
return config.baseUrl;
|
return config.baseUrl;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useCredentialsRequirement = () => {
|
||||||
|
const config = useServerConfig();
|
||||||
|
|
||||||
|
if (!config) {
|
||||||
|
return {} as ServerConfigQuery['serverConfig']['credentialsRequirement'];
|
||||||
|
}
|
||||||
|
|
||||||
|
return config.credentialsRequirement;
|
||||||
|
};
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
SignUpPage,
|
SignUpPage,
|
||||||
} from '@affine/component/auth-components';
|
} from '@affine/component/auth-components';
|
||||||
import { pushNotificationAtom } from '@affine/component/notification-center';
|
import { pushNotificationAtom } from '@affine/component/notification-center';
|
||||||
|
import { useCredentialsRequirement } from '@affine/core/hooks/affine/use-server-config';
|
||||||
import {
|
import {
|
||||||
changeEmailMutation,
|
changeEmailMutation,
|
||||||
changePasswordMutation,
|
changePasswordMutation,
|
||||||
@@ -45,6 +46,7 @@ const authTypeSchema = z.enum([
|
|||||||
export const AuthPage = (): ReactElement | null => {
|
export const AuthPage = (): ReactElement | null => {
|
||||||
const user = useCurrentUser();
|
const user = useCurrentUser();
|
||||||
const t = useAFFiNEI18N();
|
const t = useAFFiNEI18N();
|
||||||
|
const { password: passwordLimits } = useCredentialsRequirement();
|
||||||
|
|
||||||
const { authType } = useParams();
|
const { authType } = useParams();
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
@@ -112,6 +114,7 @@ export const AuthPage = (): ReactElement | null => {
|
|||||||
return (
|
return (
|
||||||
<SignUpPage
|
<SignUpPage
|
||||||
user={user}
|
user={user}
|
||||||
|
passwordLimits={passwordLimits}
|
||||||
onSetPassword={onSetPassword}
|
onSetPassword={onSetPassword}
|
||||||
onOpenAffine={onOpenAffine}
|
onOpenAffine={onOpenAffine}
|
||||||
/>
|
/>
|
||||||
@@ -124,6 +127,7 @@ export const AuthPage = (): ReactElement | null => {
|
|||||||
return (
|
return (
|
||||||
<ChangePasswordPage
|
<ChangePasswordPage
|
||||||
user={user}
|
user={user}
|
||||||
|
passwordLimits={passwordLimits}
|
||||||
onSetPassword={onSetPassword}
|
onSetPassword={onSetPassword}
|
||||||
onOpenAffine={onOpenAffine}
|
onOpenAffine={onOpenAffine}
|
||||||
/>
|
/>
|
||||||
@@ -133,6 +137,7 @@ export const AuthPage = (): ReactElement | null => {
|
|||||||
return (
|
return (
|
||||||
<SetPasswordPage
|
<SetPasswordPage
|
||||||
user={user}
|
user={user}
|
||||||
|
passwordLimits={passwordLimits}
|
||||||
onSetPassword={onSetPassword}
|
onSetPassword={onSetPassword}
|
||||||
onOpenAffine={onOpenAffine}
|
onOpenAffine={onOpenAffine}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
fragment CredentialsRequirement on CredentialsRequirementType {
|
||||||
|
password {
|
||||||
|
...PasswordLimits
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
fragment PasswordLimits on PasswordLimitsType {
|
||||||
|
minLength
|
||||||
|
maxLength
|
||||||
|
}
|
||||||
@@ -7,6 +7,17 @@ export interface GraphQLQuery {
|
|||||||
containsFile?: boolean;
|
containsFile?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const passwordLimitsFragment = `
|
||||||
|
fragment PasswordLimits on PasswordLimitsType {
|
||||||
|
minLength
|
||||||
|
maxLength
|
||||||
|
}`
|
||||||
|
export const credentialsRequirementFragment = `
|
||||||
|
fragment CredentialsRequirement on CredentialsRequirementType {
|
||||||
|
password {
|
||||||
|
...PasswordLimits
|
||||||
|
}
|
||||||
|
}`
|
||||||
export const checkBlobSizesQuery = {
|
export const checkBlobSizesQuery = {
|
||||||
id: 'checkBlobSizesQuery' as const,
|
id: 'checkBlobSizesQuery' as const,
|
||||||
operationName: 'checkBlobSizes',
|
operationName: 'checkBlobSizes',
|
||||||
@@ -708,8 +719,12 @@ query serverConfig {
|
|||||||
name
|
name
|
||||||
features
|
features
|
||||||
type
|
type
|
||||||
|
credentialsRequirement {
|
||||||
|
...CredentialsRequirement
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}`,
|
}${passwordLimitsFragment}
|
||||||
|
${credentialsRequirementFragment}`,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const setWorkspacePublicByIdMutation = {
|
export const setWorkspacePublicByIdMutation = {
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
#import './fragments/password-limits.gql'
|
||||||
|
#import './fragments/credentials-requirement.gql'
|
||||||
|
|
||||||
query serverConfig {
|
query serverConfig {
|
||||||
serverConfig {
|
serverConfig {
|
||||||
version
|
version
|
||||||
@@ -5,5 +8,8 @@ query serverConfig {
|
|||||||
name
|
name
|
||||||
features
|
features
|
||||||
type
|
type
|
||||||
|
credentialsRequirement {
|
||||||
|
...CredentialsRequirement
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -292,6 +292,21 @@ export type RemoveEarlyAccessMutation = {
|
|||||||
removeEarlyAccess: number;
|
removeEarlyAccess: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type CredentialsRequirementFragment = {
|
||||||
|
__typename?: 'CredentialsRequirementType';
|
||||||
|
password: {
|
||||||
|
__typename?: 'PasswordLimitsType';
|
||||||
|
minLength: number;
|
||||||
|
maxLength: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PasswordLimitsFragment = {
|
||||||
|
__typename?: 'PasswordLimitsType';
|
||||||
|
minLength: number;
|
||||||
|
maxLength: number;
|
||||||
|
};
|
||||||
|
|
||||||
export type GetCurrentUserQueryVariables = Exact<{ [key: string]: never }>;
|
export type GetCurrentUserQueryVariables = Exact<{ [key: string]: never }>;
|
||||||
|
|
||||||
export type GetCurrentUserQuery = {
|
export type GetCurrentUserQuery = {
|
||||||
@@ -701,6 +716,14 @@ export type ServerConfigQuery = {
|
|||||||
name: string;
|
name: string;
|
||||||
features: Array<ServerFeature>;
|
features: Array<ServerFeature>;
|
||||||
type: ServerDeploymentType;
|
type: ServerDeploymentType;
|
||||||
|
credentialsRequirement: {
|
||||||
|
__typename?: 'CredentialsRequirementType';
|
||||||
|
password: {
|
||||||
|
__typename?: 'PasswordLimitsType';
|
||||||
|
minLength: number;
|
||||||
|
maxLength: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -423,7 +423,7 @@
|
|||||||
"com.affine.auth.open.affine.download-app": "Download App",
|
"com.affine.auth.open.affine.download-app": "Download App",
|
||||||
"com.affine.auth.open.affine.prompt": "Opening <1>AFFiNE</1> app now",
|
"com.affine.auth.open.affine.prompt": "Opening <1>AFFiNE</1> app now",
|
||||||
"com.affine.auth.open.affine.try-again": "Try again",
|
"com.affine.auth.open.affine.try-again": "Try again",
|
||||||
"com.affine.auth.page.sent.email.subtitle": "Please set a password of 8-20 characters with both letters and numbers to continue signing up with ",
|
"com.affine.auth.page.sent.email.subtitle": "Please set a password of {{min}}-{{max}} characters with both letters and numbers to continue signing up with ",
|
||||||
"com.affine.auth.page.sent.email.title": "Welcome to AFFiNE Cloud, you are almost there!",
|
"com.affine.auth.page.sent.email.title": "Welcome to AFFiNE Cloud, you are almost there!",
|
||||||
"com.affine.auth.password": "Password",
|
"com.affine.auth.password": "Password",
|
||||||
"com.affine.auth.password.error": "Invalid password",
|
"com.affine.auth.password.error": "Invalid password",
|
||||||
@@ -444,10 +444,12 @@
|
|||||||
"com.affine.auth.sent.set.password.success.message": "Your password has saved! You can sign in AFFiNE Cloud with email and password!",
|
"com.affine.auth.sent.set.password.success.message": "Your password has saved! You can sign in AFFiNE Cloud with email and password!",
|
||||||
"com.affine.auth.set.email.save": "Save Email",
|
"com.affine.auth.set.email.save": "Save Email",
|
||||||
"com.affine.auth.set.password": "Set password",
|
"com.affine.auth.set.password": "Set password",
|
||||||
"com.affine.auth.set.password.message": "Please set a password of 8-20 characters with both letters and numbers to continue signing up with ",
|
"com.affine.auth.set.password.message": "Please set a password of {{min}}-{{max}} characters with both letters and numbers to continue signing up with ",
|
||||||
|
"com.affine.auth.set.password.message.minlength": "Minimum {{min}} characters",
|
||||||
|
"com.affine.auth.set.password.message.maxlength": "Maximum {{max}} characters",
|
||||||
"com.affine.auth.set.password.page.success": "Password set successful",
|
"com.affine.auth.set.password.page.success": "Password set successful",
|
||||||
"com.affine.auth.set.password.page.title": "Set your AFFiNE Cloud password",
|
"com.affine.auth.set.password.page.title": "Set your AFFiNE Cloud password",
|
||||||
"com.affine.auth.set.password.placeholder": "Set a password at least 8 letters long",
|
"com.affine.auth.set.password.placeholder": "Set a password at least {{min}} letters long",
|
||||||
"com.affine.auth.set.password.placeholder.confirm": "Confirm password",
|
"com.affine.auth.set.password.placeholder.confirm": "Confirm password",
|
||||||
"com.affine.auth.set.password.save": "Save Password",
|
"com.affine.auth.set.password.save": "Save Password",
|
||||||
"com.affine.auth.sign-out.confirm-modal.cancel": "Cancel",
|
"com.affine.auth.sign-out.confirm-modal.cancel": "Cancel",
|
||||||
|
|||||||
@@ -409,7 +409,7 @@
|
|||||||
"com.affine.auth.open.affine.download-app": "下载应用",
|
"com.affine.auth.open.affine.download-app": "下载应用",
|
||||||
"com.affine.auth.open.affine.prompt": "正在打开 <1>AFFiNE</1> 应用\n",
|
"com.affine.auth.open.affine.prompt": "正在打开 <1>AFFiNE</1> 应用\n",
|
||||||
"com.affine.auth.open.affine.try-again": "重试",
|
"com.affine.auth.open.affine.try-again": "重试",
|
||||||
"com.affine.auth.page.sent.email.subtitle": "请输入一个长度在8-20个字符之间,同时包含字母和数字的密码以继续注册",
|
"com.affine.auth.page.sent.email.subtitle": "请输入一个长度在 {{min}}-{{max}} 个字符之间,同时包含字母和数字的密码以继续注册",
|
||||||
"com.affine.auth.page.sent.email.title": "欢迎来到 AFFiNE Cloud,即将完成!",
|
"com.affine.auth.page.sent.email.title": "欢迎来到 AFFiNE Cloud,即将完成!",
|
||||||
"com.affine.auth.password": "密码",
|
"com.affine.auth.password": "密码",
|
||||||
"com.affine.auth.password.error": "无效密码",
|
"com.affine.auth.password.error": "无效密码",
|
||||||
@@ -429,10 +429,12 @@
|
|||||||
"com.affine.auth.sent.set.password.success.message": "您的密码已保存!您可以使用邮箱和密码登录 AFFiNE Cloud!",
|
"com.affine.auth.sent.set.password.success.message": "您的密码已保存!您可以使用邮箱和密码登录 AFFiNE Cloud!",
|
||||||
"com.affine.auth.set.email.save": "保存电子邮件",
|
"com.affine.auth.set.email.save": "保存电子邮件",
|
||||||
"com.affine.auth.set.password": "设置密码",
|
"com.affine.auth.set.password": "设置密码",
|
||||||
"com.affine.auth.set.password.message": "请输入一个长度在8-20个字符之间,同时包含字母和数字的密码以继续注册",
|
"com.affine.auth.set.password.message": "请输入一个长度在 {{min}}-{{max}} 个字符之间,同时包含字母和数字的密码以继续注册",
|
||||||
|
"com.affine.auth.set.password.message.minlength": "至少 {{min}} 个字符",
|
||||||
|
"com.affine.auth.set.password.message.maxlength": "至多 {{max}} 个字符",
|
||||||
"com.affine.auth.set.password.page.success": "密码设置成功",
|
"com.affine.auth.set.password.page.success": "密码设置成功",
|
||||||
"com.affine.auth.set.password.page.title": "设置您的 AFFiNE Cloud 密码",
|
"com.affine.auth.set.password.page.title": "设置您的 AFFiNE Cloud 密码",
|
||||||
"com.affine.auth.set.password.placeholder": "密码长度至少需要8个字符",
|
"com.affine.auth.set.password.placeholder": "密码长度至少需要 {{min}} 个字符",
|
||||||
"com.affine.auth.set.password.placeholder.confirm": "确认密码",
|
"com.affine.auth.set.password.placeholder.confirm": "确认密码",
|
||||||
"com.affine.auth.set.password.save": "保存密码",
|
"com.affine.auth.set.password.save": "保存密码",
|
||||||
"com.affine.auth.sign-out.confirm-modal.cancel": "取消",
|
"com.affine.auth.sign-out.confirm-modal.cancel": "取消",
|
||||||
|
|||||||
@@ -329,7 +329,7 @@
|
|||||||
"com.affine.auth.has.signed": "已登入!",
|
"com.affine.auth.has.signed": "已登入!",
|
||||||
"com.affine.auth.later": "之後",
|
"com.affine.auth.later": "之後",
|
||||||
"com.affine.auth.open.affine": "打開 AFFiNE",
|
"com.affine.auth.open.affine": "打開 AFFiNE",
|
||||||
"com.affine.auth.page.sent.email.subtitle": "請設定由8-20位字母和數字組成的密碼",
|
"com.affine.auth.page.sent.email.subtitle": "請設定由 {{min}}-{{max}} 位字母和數字組成的密碼",
|
||||||
"com.affine.auth.page.sent.email.title": "歡迎來到 AFFiNE Cloud,即將完成",
|
"com.affine.auth.page.sent.email.title": "歡迎來到 AFFiNE Cloud,即將完成",
|
||||||
"com.affine.auth.password": "密碼",
|
"com.affine.auth.password": "密碼",
|
||||||
"com.affine.auth.password.error": "無效的密碼",
|
"com.affine.auth.password.error": "無效的密碼",
|
||||||
@@ -345,10 +345,12 @@
|
|||||||
"com.affine.auth.sent.set.password.hint": "設定密碼連結已發送。",
|
"com.affine.auth.sent.set.password.hint": "設定密碼連結已發送。",
|
||||||
"com.affine.auth.set.email.save": "保存電子郵件地址",
|
"com.affine.auth.set.email.save": "保存電子郵件地址",
|
||||||
"com.affine.auth.set.password": "設定密碼",
|
"com.affine.auth.set.password": "設定密碼",
|
||||||
"com.affine.auth.set.password.message": "請設定由8-20位字母和數字組成的密碼",
|
"com.affine.auth.set.password.message": "請設定由 {{min}}-{{max}} 位字母和數字組成的密碼",
|
||||||
|
"com.affine.auth.set.password.message.minlength": "至少 {{min}} 位字符",
|
||||||
|
"com.affine.auth.set.password.message.maxlength": "至多 {{max}} 位字符",
|
||||||
"com.affine.auth.set.password.page.success": "成功設定密碼",
|
"com.affine.auth.set.password.page.success": "成功設定密碼",
|
||||||
"com.affine.auth.set.password.page.title": "重定您的 AFFiNE Cloud 密碼",
|
"com.affine.auth.set.password.page.title": "重定您的 AFFiNE Cloud 密碼",
|
||||||
"com.affine.auth.set.password.placeholder": "設定至少包含 8 位字符的密碼",
|
"com.affine.auth.set.password.placeholder": "設定至少包含 {{min}} 位字符的密碼",
|
||||||
"com.affine.auth.set.password.placeholder.confirm": "確認密碼",
|
"com.affine.auth.set.password.placeholder.confirm": "確認密碼",
|
||||||
"com.affine.auth.set.password.save": "保存密碼",
|
"com.affine.auth.set.password.save": "保存密碼",
|
||||||
"com.affine.auth.sign.auth.code.error.hint": "驗證碼錯誤,請重試",
|
"com.affine.auth.sign.auth.code.error.hint": "驗證碼錯誤,請重試",
|
||||||
|
|||||||
@@ -304,6 +304,7 @@ __metadata:
|
|||||||
vite: "npm:^5.1.4"
|
vite: "npm:^5.1.4"
|
||||||
vitest: "npm:1.4.0"
|
vitest: "npm:1.4.0"
|
||||||
yjs: "npm:^13.6.12"
|
yjs: "npm:^13.6.12"
|
||||||
|
zod: "npm:^3.22.4"
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
"@blocksuite/blocks": "*"
|
"@blocksuite/blocks": "*"
|
||||||
"@blocksuite/global": "*"
|
"@blocksuite/global": "*"
|
||||||
|
|||||||
Reference in New Issue
Block a user