diff --git a/packages/backend/server/src/config/affine.ts b/packages/backend/server/src/config/affine.ts index 1c22b3e88..0dcec63c4 100644 --- a/packages/backend/server/src/config/affine.ts +++ b/packages/backend/server/src/config/affine.ts @@ -56,7 +56,7 @@ AFFiNE.port = 3010; // /* User Signup password limitation */ // AFFiNE.auth.password = { // minLength: 8, -// maxLength: 20, +// maxLength: 32, // }; // // /* How long the login session would last by default */ diff --git a/packages/backend/server/src/core/config.ts b/packages/backend/server/src/core/config.ts index 828027df5..01830f11e 100644 --- a/packages/backend/server/src/core/config.ts +++ b/packages/backend/server/src/core/config.ts @@ -22,6 +22,20 @@ export function ADD_ENABLED_FEATURES(feature: ServerFeature) { ENABLED_FEATURES.add(feature); } +@ObjectType() +export class PasswordLimitsType { + @Field() + minLength!: number; + @Field() + maxLength!: number; +} + +@ObjectType() +export class CredentialsRequirementType { + @Field() + password!: PasswordLimitsType; +} + @ObjectType() export class ServerConfigType { @Field({ @@ -47,6 +61,11 @@ export class ServerConfigType { @Field(() => [ServerFeature], { description: 'enabled server features' }) features!: ServerFeature[]; + + @Field(() => CredentialsRequirementType, { + description: 'credentials requirement', + }) + credentialsRequirement!: CredentialsRequirementType; } export class ServerConfigResolver { @@ -65,6 +84,9 @@ export class ServerConfigResolver { // this field should be removed after frontend feature flags implemented flavor: AFFiNE.type, features: Array.from(ENABLED_FEATURES), + credentialsRequirement: { + password: AFFiNE.auth.password, + }, }; } } diff --git a/packages/backend/server/src/fundamentals/config/def.ts b/packages/backend/server/src/fundamentals/config/def.ts index 48ca191f9..c4c110be3 100644 --- a/packages/backend/server/src/fundamentals/config/def.ts +++ b/packages/backend/server/src/fundamentals/config/def.ts @@ -214,9 +214,14 @@ export interface AFFiNEConfig { * authentication config */ auth: { + /** + * The minimum and maximum length of the password when registering new users + * + * @default [8,32] + */ password: { /** - * The minimum and maximum length of the password when registering new users + * The minimum length of the password * * @default 8 */ @@ -224,7 +229,7 @@ export interface AFFiNEConfig { /** * The maximum length of the password * - * @default 20 + * @default 32 */ maxLength: number; }; diff --git a/packages/backend/server/src/schema.gql b/packages/backend/server/src/schema.gql index e91fb6502..e76bda04f 100644 --- a/packages/backend/server/src/schema.gql +++ b/packages/backend/server/src/schema.gql @@ -10,6 +10,10 @@ input CreateCheckoutSessionInput { 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. """ @@ -164,6 +168,11 @@ enum OAuthProviderType { Google } +type PasswordLimitsType { + maxLength: Int! + minLength: Int! +} + """User permission in workspace""" enum Permission { Admin @@ -236,6 +245,9 @@ type ServerConfigType { """server base url""" baseUrl: String! + """credentials requirement""" + credentialsRequirement: CredentialsRequirementType! + """enabled server features""" features: [ServerFeature!]! diff --git a/packages/frontend/component/package.json b/packages/frontend/component/package.json index ad49eb91f..378865bd9 100644 --- a/packages/frontend/component/package.json +++ b/packages/frontend/component/package.json @@ -70,7 +70,8 @@ "react-virtuoso": "^4.7.0", "rxjs": "^7.8.1", "swr": "^2.2.5", - "uuid": "^9.0.1" + "uuid": "^9.0.1", + "zod": "^3.22.4" }, "devDependencies": { "@blocksuite/block-std": "0.14.0-canary-202403250855-4171ecd", diff --git a/packages/frontend/component/src/components/auth-components/change-password-page.tsx b/packages/frontend/component/src/components/auth-components/change-password-page.tsx index cb11b6898..f4b1f6674 100644 --- a/packages/frontend/component/src/components/auth-components/change-password-page.tsx +++ b/packages/frontend/component/src/components/auth-components/change-password-page.tsx @@ -1,3 +1,4 @@ +import type { PasswordLimitsFragment } from '@affine/graphql'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useSetAtom } from 'jotai'; import type { FC } from 'react'; @@ -11,9 +12,15 @@ import type { User } from './type'; export const ChangePasswordPage: FC<{ user: User; + passwordLimits: PasswordLimitsFragment; onSetPassword: (password: string) => Promise; onOpenAffine: () => void; -}> = ({ user: { email }, onSetPassword: propsOnSetPassword, onOpenAffine }) => { +}> = ({ + user: { email }, + passwordLimits, + onSetPassword: propsOnSetPassword, + onOpenAffine, +}) => { const t = useAFFiNEI18N(); const [hasSetUp, setHasSetUp] = useState(false); 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.page.sent.email.subtitle']()} + {t['com.affine.auth.page.sent.email.subtitle']({ + min: String(passwordLimits.minLength), + max: String(passwordLimits.maxLength), + })} {email} ) @@ -57,7 +67,10 @@ export const ChangePasswordPage: FC<{ {t['com.affine.auth.open.affine']()} ) : ( - + )} ); diff --git a/packages/frontend/component/src/components/auth-components/password-input/index.tsx b/packages/frontend/component/src/components/auth-components/password-input/index.tsx index 223d39349..ff8567fb2 100644 --- a/packages/frontend/component/src/components/auth-components/password-input/index.tsx +++ b/packages/frontend/component/src/components/auth-components/password-input/index.tsx @@ -1,104 +1,199 @@ +import { type PasswordLimitsFragment } from '@affine/graphql'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; -import { passwordStrength } from 'check-password-strength'; -import type { FC } from 'react'; -import { useCallback, useEffect, useState } from 'react'; +import { type Options, passwordStrength } from 'check-password-strength'; +import { type FC, useEffect, useMemo } from 'react'; +import { useCallback, useState } from 'react'; +import { z, type ZodCustomIssue, ZodIssueCode } from 'zod'; import type { InputProps } from '../../../ui/input'; import { Input } from '../../../ui/input'; import * as styles from '../share.css'; import { ErrorIcon } from './error'; +import { statusWrapper } from './style.css'; import { SuccessIcon } from './success'; import { Tag } from './tag'; -export type Status = 'weak' | 'medium' | 'strong' | 'maximum'; +export type Status = 'weak' | 'medium' | 'strong' | 'minimum' | 'maximum'; + +const PASSWORD_STRENGTH_OPTIONS: Options = [ + { + 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< InputProps & { + passwordLimits: PasswordLimitsFragment; onPass: (password: string) => void; onPrevent: () => void; } -> = ({ onPass, onPrevent, ...inputProps }) => { +> = ({ passwordLimits, onPass, onPrevent, ...inputProps }) => { const t = useAFFiNEI18N(); const [status, setStatus] = useState(null); const [confirmStatus, setConfirmStatus] = useState< 'success' | 'error' | null >(null); + const [canSubmit, setCanSubmit] = useState(false); const [password, setPassWord] = 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) => { - setPassWord(value); - if (!value) { - return setStatus(null); - } - if (value.length > 20) { - return setStatus('maximum'); - } - switch (passwordStrength(value).id) { - case 0: - case 1: - setStatus('weak'); - break; - case 2: - setStatus('medium'); - break; - case 3: - setStatus('strong'); - break; - } - }, []); + ctx.addIssue({ + code: ZodIssueCode.custom, + message: 'password strength', + path: ['strength'], + params: { + status, + }, + }); + }); + }, [passwordLimits]); - const onConfirmPasswordChange = useCallback((value: string) => { - setConfirmPassword(value); - }, []); + const validatePasswords = useCallback( + (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(() => { - if (!password || !confirmPassword) { - return; - } - if (password === confirmPassword) { - setConfirmStatus('success'); - } else { - setConfirmStatus('error'); - } - }, [confirmPassword, password]); - - useEffect(() => { - if (confirmStatus === 'success' && password.length > 7) { + if (canSubmit) { onPass(password); } else { onPrevent(); } - }, [confirmStatus, onPass, onPrevent, password]); + }, [canSubmit, password, onPass, onPrevent]); return ( <> : null} + endFix={ +
+ {status ? ( + + ) : null} +
+ } {...inputProps} /> - ) : ( - - ) - ) : null +
+ {confirmStatus ? ( + confirmStatus === 'success' ? ( + + ) : ( + + ) + ) : null} +
} {...inputProps} /> diff --git a/packages/frontend/component/src/components/auth-components/password-input/style.css.ts b/packages/frontend/component/src/components/auth-components/password-input/style.css.ts index 2d9e66e2e..738f7ba8b 100644 --- a/packages/frontend/component/src/components/auth-components/password-input/style.css.ts +++ b/packages/frontend/component/src/components/auth-components/password-input/style.css.ts @@ -1,7 +1,11 @@ import { cssVar } from '@toeverything/theme'; import { style } from '@vanilla-extract/css'; +export const statusWrapper = style({ + marginLeft: 8, + marginRight: 10, +}); export const tag = style({ - padding: '0 15px', + padding: '2px 15px', height: 20, lineHeight: '20px', borderRadius: 10, @@ -19,7 +23,7 @@ export const tag = style({ backgroundColor: cssVar('tagGreen'), color: cssVar('successColor'), }, - '&.maximum': { + '&.minimum, &.maximum': { backgroundColor: cssVar('tagRed'), color: cssVar('errorColor'), }, diff --git a/packages/frontend/component/src/components/auth-components/password-input/tag.tsx b/packages/frontend/component/src/components/auth-components/password-input/tag.tsx index ccaf06c39..9a0702ee5 100644 --- a/packages/frontend/component/src/components/auth-components/password-input/tag.tsx +++ b/packages/frontend/component/src/components/auth-components/password-input/tag.tsx @@ -4,15 +4,23 @@ import { useMemo } from 'react'; import type { Status } from './index'; import { tag } from './style.css'; -export const Tag: FC<{ status: Status }> = ({ status }) => { + +type TagProps = { + status: Status; + minimum: string; + maximum: string; +}; + +export const Tag: FC = ({ status, minimum, maximum }) => { const textMap = useMemo<{ [K in Status]: string }>(() => { return { weak: 'Weak', medium: 'Medium', strong: 'Strong', - maximum: 'Maximum', + minimum, + maximum, }; - }, []); + }, [minimum, maximum]); return (
= ({ status }) => { weak: status === 'weak', medium: status === 'medium', strong: status === 'strong', + minimum: status === 'minimum', maximum: status === 'maximum', })} > diff --git a/packages/frontend/component/src/components/auth-components/set-password-page.tsx b/packages/frontend/component/src/components/auth-components/set-password-page.tsx index 09639d596..6859707ba 100644 --- a/packages/frontend/component/src/components/auth-components/set-password-page.tsx +++ b/packages/frontend/component/src/components/auth-components/set-password-page.tsx @@ -1,3 +1,4 @@ +import type { PasswordLimitsFragment } from '@affine/graphql'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useSetAtom } from 'jotai'; import type { FC } from 'react'; @@ -11,9 +12,15 @@ import type { User } from './type'; export const SetPasswordPage: FC<{ user: User; + passwordLimits: PasswordLimitsFragment; onSetPassword: (password: string) => Promise; onOpenAffine: () => void; -}> = ({ user: { email }, onSetPassword: propsOnSetPassword, onOpenAffine }) => { +}> = ({ + user: { email }, + passwordLimits, + onSetPassword: propsOnSetPassword, + onOpenAffine, +}) => { const t = useAFFiNEI18N(); const [hasSetUp, setHasSetUp] = useState(false); 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.page.sent.email.subtitle']()} + {t['com.affine.auth.page.sent.email.subtitle']({ + min: String(passwordLimits.minLength), + max: String(passwordLimits.maxLength), + })} {email} ) @@ -57,7 +67,10 @@ export const SetPasswordPage: FC<{ {t['com.affine.auth.open.affine']()} ) : ( - + )} ); diff --git a/packages/frontend/component/src/components/auth-components/set-password.tsx b/packages/frontend/component/src/components/auth-components/set-password.tsx index c77195518..9989d5187 100644 --- a/packages/frontend/component/src/components/auth-components/set-password.tsx +++ b/packages/frontend/component/src/components/auth-components/set-password.tsx @@ -1,3 +1,4 @@ +import type { PasswordLimitsFragment } from '@affine/graphql'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; import type { FC } from 'react'; import { useCallback, useRef, useState } from 'react'; @@ -7,10 +8,11 @@ import { Wrapper } from '../../ui/layout'; import { PasswordInput } from './password-input'; export const SetPassword: FC<{ + passwordLimits: PasswordLimitsFragment; showLater?: boolean; onLater?: () => void; onSetPassword: (password: string) => void; -}> = ({ onLater, onSetPassword, showLater = false }) => { +}> = ({ passwordLimits, onLater, onSetPassword, showLater = false }) => { const t = useAFFiNEI18N(); const [passwordPass, setPasswordPass] = useState(false); @@ -20,6 +22,7 @@ export const SetPassword: FC<{ <> { setPasswordPass(true); passwordRef.current = password; diff --git a/packages/frontend/component/src/components/auth-components/sign-up-page.tsx b/packages/frontend/component/src/components/auth-components/sign-up-page.tsx index 684b6cdd9..11a4804fc 100644 --- a/packages/frontend/component/src/components/auth-components/sign-up-page.tsx +++ b/packages/frontend/component/src/components/auth-components/sign-up-page.tsx @@ -1,3 +1,4 @@ +import type { PasswordLimitsFragment } from '@affine/graphql'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useSetAtom } from 'jotai'; import type { FC } from 'react'; @@ -10,11 +11,13 @@ import { SetPassword } from './set-password'; import type { User } from './type'; export const SignUpPage: FC<{ + passwordLimits: PasswordLimitsFragment; user: User; onSetPassword: (password: string) => Promise; openButtonText?: string; onOpenAffine: () => void; }> = ({ + passwordLimits, user: { email }, onSetPassword: propsOnSetPassword, onOpenAffine, @@ -55,7 +58,10 @@ export const SignUpPage: FC<{ 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), + })} {email} ) @@ -67,6 +73,7 @@ export const SignUpPage: FC<{ ) : ( { 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(); switch (emailType) { 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': return t['com.affine.auth.reset.password.message'](); case 'changeEmail': @@ -154,12 +163,13 @@ export const SendEmail = ({ emailType, }: AuthPanelProps) => { const t = useAFFiNEI18N(); + const { password: passwordLimits } = useCredentialsRequirement(); const [hasSentEmail, setHasSentEmail] = useState(false); const pushNotification = useSetAtom(pushNotificationAtom); const title = useEmailTitle(emailType); const hint = useNotificationHint(emailType); - const content = useContent(emailType, email); + const content = useContent(emailType, email, passwordLimits); const buttonContent = useButtonContent(emailType); const { loading, sendEmail } = useSendEmail(emailType); diff --git a/packages/frontend/core/src/components/affine/auth/subscription-redirect.tsx b/packages/frontend/core/src/components/affine/auth/subscription-redirect.tsx index 56271fe18..65a2987fd 100644 --- a/packages/frontend/core/src/components/affine/auth/subscription-redirect.tsx +++ b/packages/frontend/core/src/components/affine/auth/subscription-redirect.tsx @@ -2,6 +2,7 @@ import { SignUpPage } from '@affine/component/auth-components'; import { Button } from '@affine/component/ui/button'; import { Loading } from '@affine/component/ui/loading'; 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 type { SubscriptionPlan, SubscriptionRecurring } from '@affine/graphql'; import { @@ -106,6 +107,7 @@ const SubscriptionRedirectWithData = () => { const user = useCurrentUser(); const searchData = useSubscriptionSearch(); const openPaymentUrl = usePaymentRedirect(); + const { password: passwordLimits } = useCredentialsRequirement(); const { trigger: changePassword } = useMutation({ mutation: changePasswordMutation, @@ -128,6 +130,7 @@ const SubscriptionRedirectWithData = () => { return ( { return config.baseUrl; }; + +export const useCredentialsRequirement = () => { + const config = useServerConfig(); + + if (!config) { + return {} as ServerConfigQuery['serverConfig']['credentialsRequirement']; + } + + return config.credentialsRequirement; +}; diff --git a/packages/frontend/core/src/pages/auth.tsx b/packages/frontend/core/src/pages/auth.tsx index 732ffe2bd..d70850dd2 100644 --- a/packages/frontend/core/src/pages/auth.tsx +++ b/packages/frontend/core/src/pages/auth.tsx @@ -8,6 +8,7 @@ import { SignUpPage, } from '@affine/component/auth-components'; import { pushNotificationAtom } from '@affine/component/notification-center'; +import { useCredentialsRequirement } from '@affine/core/hooks/affine/use-server-config'; import { changeEmailMutation, changePasswordMutation, @@ -45,6 +46,7 @@ const authTypeSchema = z.enum([ export const AuthPage = (): ReactElement | null => { const user = useCurrentUser(); const t = useAFFiNEI18N(); + const { password: passwordLimits } = useCredentialsRequirement(); const { authType } = useParams(); const [searchParams] = useSearchParams(); @@ -112,6 +114,7 @@ export const AuthPage = (): ReactElement | null => { return ( @@ -124,6 +127,7 @@ export const AuthPage = (): ReactElement | null => { return ( @@ -133,6 +137,7 @@ export const AuthPage = (): ReactElement | null => { return ( diff --git a/packages/frontend/graphql/src/graphql/fragments/credentials-requirement.gql b/packages/frontend/graphql/src/graphql/fragments/credentials-requirement.gql new file mode 100644 index 000000000..e76587f8b --- /dev/null +++ b/packages/frontend/graphql/src/graphql/fragments/credentials-requirement.gql @@ -0,0 +1,5 @@ +fragment CredentialsRequirement on CredentialsRequirementType { + password { + ...PasswordLimits + } +} diff --git a/packages/frontend/graphql/src/graphql/fragments/password-limits.gql b/packages/frontend/graphql/src/graphql/fragments/password-limits.gql new file mode 100644 index 000000000..27cbaa581 --- /dev/null +++ b/packages/frontend/graphql/src/graphql/fragments/password-limits.gql @@ -0,0 +1,4 @@ +fragment PasswordLimits on PasswordLimitsType { + minLength + maxLength +} diff --git a/packages/frontend/graphql/src/graphql/index.ts b/packages/frontend/graphql/src/graphql/index.ts index 6c1acd451..188af142f 100644 --- a/packages/frontend/graphql/src/graphql/index.ts +++ b/packages/frontend/graphql/src/graphql/index.ts @@ -7,6 +7,17 @@ export interface GraphQLQuery { containsFile?: boolean; } +export const passwordLimitsFragment = ` +fragment PasswordLimits on PasswordLimitsType { + minLength + maxLength +}` +export const credentialsRequirementFragment = ` +fragment CredentialsRequirement on CredentialsRequirementType { + password { + ...PasswordLimits + } +}` export const checkBlobSizesQuery = { id: 'checkBlobSizesQuery' as const, operationName: 'checkBlobSizes', @@ -708,8 +719,12 @@ query serverConfig { name features type + credentialsRequirement { + ...CredentialsRequirement + } } -}`, +}${passwordLimitsFragment} +${credentialsRequirementFragment}`, }; export const setWorkspacePublicByIdMutation = { diff --git a/packages/frontend/graphql/src/graphql/server-config.gql b/packages/frontend/graphql/src/graphql/server-config.gql index b92710164..ef17ef188 100644 --- a/packages/frontend/graphql/src/graphql/server-config.gql +++ b/packages/frontend/graphql/src/graphql/server-config.gql @@ -1,3 +1,6 @@ +#import './fragments/password-limits.gql' +#import './fragments/credentials-requirement.gql' + query serverConfig { serverConfig { version @@ -5,5 +8,8 @@ query serverConfig { name features type + credentialsRequirement { + ...CredentialsRequirement + } } } diff --git a/packages/frontend/graphql/src/schema.ts b/packages/frontend/graphql/src/schema.ts index c3a40dd9b..258ab904f 100644 --- a/packages/frontend/graphql/src/schema.ts +++ b/packages/frontend/graphql/src/schema.ts @@ -292,6 +292,21 @@ export type RemoveEarlyAccessMutation = { 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 GetCurrentUserQuery = { @@ -701,6 +716,14 @@ export type ServerConfigQuery = { name: string; features: Array; type: ServerDeploymentType; + credentialsRequirement: { + __typename?: 'CredentialsRequirementType'; + password: { + __typename?: 'PasswordLimitsType'; + minLength: number; + maxLength: number; + }; + }; }; }; diff --git a/packages/frontend/i18n/src/resources/en.json b/packages/frontend/i18n/src/resources/en.json index d7c274ac3..e1f7dfaed 100644 --- a/packages/frontend/i18n/src/resources/en.json +++ b/packages/frontend/i18n/src/resources/en.json @@ -423,7 +423,7 @@ "com.affine.auth.open.affine.download-app": "Download App", "com.affine.auth.open.affine.prompt": "Opening <1>AFFiNE app now", "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.password": "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.set.email.save": "Save Email", "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.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.save": "Save Password", "com.affine.auth.sign-out.confirm-modal.cancel": "Cancel", diff --git a/packages/frontend/i18n/src/resources/zh-Hans.json b/packages/frontend/i18n/src/resources/zh-Hans.json index b2e1f21a6..4cc1dffb7 100644 --- a/packages/frontend/i18n/src/resources/zh-Hans.json +++ b/packages/frontend/i18n/src/resources/zh-Hans.json @@ -409,7 +409,7 @@ "com.affine.auth.open.affine.download-app": "下载应用", "com.affine.auth.open.affine.prompt": "正在打开 <1>AFFiNE 应用\n", "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.password": "密码", "com.affine.auth.password.error": "无效密码", @@ -429,10 +429,12 @@ "com.affine.auth.sent.set.password.success.message": "您的密码已保存!您可以使用邮箱和密码登录 AFFiNE Cloud!", "com.affine.auth.set.email.save": "保存电子邮件", "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.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.save": "保存密码", "com.affine.auth.sign-out.confirm-modal.cancel": "取消", diff --git a/packages/frontend/i18n/src/resources/zh-Hant.json b/packages/frontend/i18n/src/resources/zh-Hant.json index 45e570445..65042065b 100644 --- a/packages/frontend/i18n/src/resources/zh-Hant.json +++ b/packages/frontend/i18n/src/resources/zh-Hant.json @@ -329,7 +329,7 @@ "com.affine.auth.has.signed": "已登入!", "com.affine.auth.later": "之後", "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.password": "密碼", "com.affine.auth.password.error": "無效的密碼", @@ -345,10 +345,12 @@ "com.affine.auth.sent.set.password.hint": "設定密碼連結已發送。", "com.affine.auth.set.email.save": "保存電子郵件地址", "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.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.save": "保存密碼", "com.affine.auth.sign.auth.code.error.hint": "驗證碼錯誤,請重試", diff --git a/yarn.lock b/yarn.lock index 9087911a3..4139fd031 100644 --- a/yarn.lock +++ b/yarn.lock @@ -304,6 +304,7 @@ __metadata: vite: "npm:^5.1.4" vitest: "npm:1.4.0" yjs: "npm:^13.6.12" + zod: "npm:^3.22.4" peerDependencies: "@blocksuite/blocks": "*" "@blocksuite/global": "*"