0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-03-31 22:51:25 -05:00

refactor(core): add required profile validation (#2561)

This commit is contained in:
simeng-li 2022-12-03 22:33:10 +08:00 committed by GitHub
parent aafae8e02b
commit 0e402fdcd3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 418 additions and 62 deletions

View file

@ -1,3 +1,4 @@
import { Event } from '@logto/schemas';
import type { Provider } from 'oidc-provider';
import koaGuard from '#src/middleware/koa-guard.js';
@ -8,7 +9,11 @@ import koaSessionSignInExperienceGuard from './middleware/koa-session-sign-in-ex
import { sendPasscodePayloadGuard, getSocialAuthorizationUrlPayloadGuard } from './types/guard.js';
import { sendPasscodeToIdentifier } from './utils/passcode-validation.js';
import { createSocialAuthorizationUrl } from './utils/social-verification.js';
import { identifierVerification } from './verifications/index.js';
import {
identifierVerification,
profileVerification,
mandatoryUserProfileValidation,
} from './verifications/index.js';
export const identifierPrefix = '/identifier';
export const verificationPrefix = '/verification';
@ -27,6 +32,16 @@ export default function interactionRoutes<T extends AnonymousRouter>(
const verifiedIdentifiers = await identifierVerification(ctx, provider);
const profile = await profileVerification(ctx, verifiedIdentifiers);
const { event } = ctx.interactionPayload;
if (event !== Event.ForgotPassword) {
await mandatoryUserProfileValidation(ctx, verifiedIdentifiers, profile);
}
// TODO: SignIn Register & ResetPassword final step
ctx.status = 200;
return next();

View file

@ -3,8 +3,8 @@ import koaBody from 'koa-body';
import RequestError from '#src/errors/RequestError/index.js';
import type { InteractionPayload } from '../types/guard.js';
import { interactionPayloadGuard } from '../types/guard.js';
import type { InteractionPayload } from '../types/index.js';
export type WithGuardedIdentifierPayloadContext<ContextT> = ContextT & {
interactionPayload: InteractionPayload;

View file

@ -1,3 +1,4 @@
import type { SignInExperience } from '@logto/schemas';
import { Event } from '@logto/schemas';
import type { MiddlewareType } from 'koa';
import type { IRouterParamContext } from 'koa-router';
@ -12,11 +13,17 @@ import {
} from '../utils/sign-in-experience-validation.js';
import type { WithGuardedIdentifierPayloadContext } from './koa-interaction-body-guard.js';
export type WithSignInExperienceContext<ContextT> = ContextT & {
signInExperience: SignInExperience;
};
export default function koaSessionSignInExperienceGuard<
StateT,
ContextT extends WithGuardedIdentifierPayloadContext<IRouterParamContext>,
ResponseBodyT
>(provider: Provider): MiddlewareType<StateT, ContextT, ResponseBodyT> {
>(
provider: Provider
): MiddlewareType<StateT, WithSignInExperienceContext<ContextT>, ResponseBodyT> {
return async (ctx, next) => {
const interaction = await provider.interactionDetails(ctx.req, ctx.res);
const { event, identifier, profile } = ctx.interactionPayload;
@ -39,6 +46,8 @@ export default function koaSessionSignInExperienceGuard<
profileValidation(profile, signInExperience);
}
ctx.signInExperience = signInExperience;
return next();
};
}

View file

@ -39,6 +39,7 @@ describe('koaSessionSignInExperienceGuard', () => {
identifier: { username: 'username', password: 'password' },
profile: { email: 'email' },
}),
signInExperience: mockSignInExperience,
};
await koaSessionSignInExperienceGuard(new Provider(''))(ctx, next);

View file

@ -1,11 +1,4 @@
import { emailRegEx, phoneRegEx, validateRedirectUrl } from '@logto/core-kit';
import type {
UsernamePasswordPayload,
EmailPasswordPayload,
EmailPasscodePayload,
PhonePasswordPayload,
PhonePasscodePayload,
} from '@logto/schemas';
import {
usernamePasswordPayloadGuard,
emailPasscodePayloadGuard,
@ -13,12 +6,14 @@ import {
socialConnectorPayloadGuard,
eventGuard,
profileGuard,
identifierGuard,
identifierPayloadGuard,
Event,
} from '@logto/schemas';
import { z } from 'zod';
// Interaction Route Guard
import { socialUserInfoGuard } from '#src/connectors/types.js';
// Interaction Payload Guard
const forgotPasswordInteractionPayloadGuard = z.object({
event: z.literal(Event.ForgotPassword),
identifier: z.union([emailPasscodePayloadGuard, phonePasscodePayloadGuard]).optional(),
@ -33,7 +28,7 @@ const registerInteractionPayloadGuard = z.object({
const signInInteractionPayloadGuard = z.object({
event: z.literal(Event.SignIn),
identifier: identifierGuard.optional(),
identifier: identifierPayloadGuard.optional(),
profile: profileGuard.optional(),
});
@ -43,16 +38,6 @@ export const interactionPayloadGuard = z.discriminatedUnion('event', [
forgotPasswordInteractionPayloadGuard,
]);
export type InteractionPayload = z.infer<typeof interactionPayloadGuard>;
export type IdentifierPayload = z.infer<typeof identifierGuard>;
export type PasswordIdentifierPayload =
| UsernamePasswordPayload
| EmailPasswordPayload
| PhonePasswordPayload;
export type PasscodeIdentifierPayload = EmailPasscodePayload | PhonePasscodePayload;
// Passcode Send Route Payload Guard
export const sendPasscodePayloadGuard = z.union([
z.object({
@ -64,7 +49,6 @@ export const sendPasscodePayloadGuard = z.union([
phone: z.string().regex(phoneRegEx),
}),
]);
export type SendPasscodePayload = z.infer<typeof sendPasscodePayloadGuard>;
// Social Authorization Uri Route Payload Guard
export const getSocialAuthorizationUrlPayloadGuard = z.object({
@ -72,8 +56,6 @@ export const getSocialAuthorizationUrlPayloadGuard = z.object({
state: z.string(),
redirectUri: z.string().refine((url) => validateRedirectUrl(url, 'web')),
});
export type SocialAuthorizationUrlPayload = z.infer<typeof getSocialAuthorizationUrlPayloadGuard>;
// Register Profile Guard
const emailProfileGuard = emailPasscodePayloadGuard.pick({ email: true });
const phoneProfileGuard = phonePasscodePayloadGuard.pick({ phone: true });
@ -85,3 +67,38 @@ export const registerProfileSafeGuard = z.union([
phoneProfileGuard,
socialProfileGuard,
]);
// Identifier Guard
export const accountIdIdentifierGuard = z.object({
key: z.literal('accountId'),
value: z.string(),
});
export const verifiedEmailIdentifierGuard = z.object({
key: z.literal('emailVerified'),
value: z.string(),
});
export const verifiedPhoneIdentifierGuard = z.object({
key: z.literal('phoneVerified'),
value: z.string(),
});
export const socialIdentifierGuard = z.object({
key: z.literal('social'),
connectorId: z.string(),
value: socialUserInfoGuard,
});
export const identifierGuard = z.discriminatedUnion('key', [
accountIdIdentifierGuard,
verifiedEmailIdentifierGuard,
verifiedPhoneIdentifierGuard,
socialIdentifierGuard,
]);
export const customInteractionResultGuard = z.object({
event: eventGuard.optional(),
profile: profileGuard.optional(),
identifiers: z.array(identifierGuard).optional(),
});

View file

@ -1,31 +1,52 @@
import type {
UsernamePasswordPayload,
EmailPasswordPayload,
EmailPasscodePayload,
PhonePasswordPayload,
PhonePasscodePayload,
} from '@logto/schemas';
import type { Context } from 'koa';
import type { IRouterParamContext } from 'koa-router';
import type { z } from 'zod';
import type { SocialUserInfo } from '#src/connectors/types.js';
import type { WithGuardedIdentifierPayloadContext } from '../middleware/koa-interaction-body-guard.js';
import type {
interactionPayloadGuard,
sendPasscodePayloadGuard,
getSocialAuthorizationUrlPayloadGuard,
accountIdIdentifierGuard,
verifiedEmailIdentifierGuard,
verifiedPhoneIdentifierGuard,
socialIdentifierGuard,
identifierGuard,
} from './guard.js';
export type Identifier =
| AccountIdIdentifier
| VerifiedEmailIdentifier
| VerifiedPhoneIdentifier
| SocialIdentifier;
// Payload Types
export type InteractionPayload = z.infer<typeof interactionPayloadGuard>;
export type AccountIdIdentifier = { key: 'accountId'; value: string };
export type PasswordIdentifierPayload =
| UsernamePasswordPayload
| EmailPasswordPayload
| PhonePasswordPayload;
export type VerifiedEmailIdentifier = { key: 'emailVerified'; value: string };
export type PasscodeIdentifierPayload = EmailPasscodePayload | PhonePasscodePayload;
export type VerifiedPhoneIdentifier = { key: 'phoneVerified'; value: string };
export type SendPasscodePayload = z.infer<typeof sendPasscodePayloadGuard>;
export type SocialIdentifier = { key: 'social'; connectorId: string; value: UseInfo };
export type SocialAuthorizationUrlPayload = z.infer<typeof getSocialAuthorizationUrlPayloadGuard>;
type UseInfo = {
email?: string;
phone?: string;
name?: string;
avatar?: string;
id: string;
};
// Identifier Types
export type AccountIdIdentifier = z.infer<typeof accountIdIdentifierGuard>;
export type VerifiedEmailIdentifier = z.infer<typeof verifiedEmailIdentifierGuard>;
export type VerifiedPhoneIdentifier = z.infer<typeof verifiedPhoneIdentifierGuard>;
export type SocialIdentifier = z.infer<typeof socialIdentifierGuard>;
export type Identifier = z.infer<typeof identifierGuard>;
export type InteractionContext = WithGuardedIdentifierPayloadContext<IRouterParamContext & Context>;

View file

@ -1,10 +1,6 @@
import type { Profile, SocialConnectorPayload, User } from '@logto/schemas';
import type { Profile, SocialConnectorPayload, User, IdentifierPayload } from '@logto/schemas';
import type {
PasscodeIdentifierPayload,
IdentifierPayload,
PasswordIdentifierPayload,
} from '../types/guard.js';
import type { PasscodeIdentifierPayload, PasswordIdentifierPayload } from '../types/index.js';
export const isPasswordIdentifier = (
identifier: IdentifierPayload

View file

@ -2,7 +2,7 @@ import { PasscodeType, Event } from '@logto/schemas';
import { createPasscode, sendPasscode } from '#src/lib/passcode.js';
import type { SendPasscodePayload } from '../types/guard.js';
import type { SendPasscodePayload } from '../types/index.js';
import { sendPasscodeToIdentifier } from './passcode-validation.js';
jest.mock('#src/lib/passcode.js', () => ({

View file

@ -5,7 +5,7 @@ import { createPasscode, sendPasscode, verifyPasscode } from '#src/lib/passcode.
import type { LogContext } from '#src/middleware/koa-log.js';
import { getPasswordlessRelatedLogType } from '#src/routes/session/utils.js';
import type { SendPasscodePayload, PasscodeIdentifierPayload } from '../types/guard.js';
import type { SendPasscodePayload, PasscodeIdentifierPayload } from '../types/index.js';
/**
* Refactor Needed:

View file

@ -1,11 +1,9 @@
import type { SignInExperience, Profile } from '@logto/schemas';
import type { SignInExperience, Profile, IdentifierPayload } from '@logto/schemas';
import { SignInMode, SignInIdentifier, Event } from '@logto/schemas';
import RequestError from '#src/errors/RequestError/index.js';
import assertThat from '#src/utils/assert-that.js';
import type { IdentifierPayload } from '../types/guard.js';
const forbiddenEventError = new RequestError({ code: 'auth.forbidden', status: 403 });
const forbiddenIdentifierError = new RequestError({

View file

@ -7,7 +7,7 @@ import { getUserInfoByAuthCode } from '#src/lib/social.js';
import type { LogContext } from '#src/middleware/koa-log.js';
import assertThat from '#src/utils/assert-that.js';
import type { SocialAuthorizationUrlPayload } from '../types/guard.js';
import type { SocialAuthorizationUrlPayload } from '../types/index.js';
export const createSocialAuthorizationUrl = async (payload: SocialAuthorizationUrlPayload) => {
const { connectorId, state, redirectUri } = payload;

View file

@ -8,8 +8,13 @@ import { verifyUserPassword } from '#src/lib/user.js';
import assertThat from '#src/utils/assert-that.js';
import { maskUserInfo } from '#src/utils/format.js';
import type { PasswordIdentifierPayload, PasscodeIdentifierPayload } from '../types/guard.js';
import type { InteractionContext, Identifier, SocialIdentifier } from '../types/index.js';
import type {
PasswordIdentifierPayload,
PasscodeIdentifierPayload,
InteractionContext,
Identifier,
SocialIdentifier,
} from '../types/index.js';
import findUserByIdentifier from '../utils/find-user-by-identifier.js';
import { isPasscodeIdentifier, isPasswordIdentifier, isProfileIdentifier } from '../utils/index.js';
import { assignIdentifierVerificationResult } from '../utils/interaction.js';
@ -53,7 +58,7 @@ const passcodeIdentifierVerification = async (
const user = await findUserByIdentifier(identifier);
if (!user) {
// Throw verification result and assign verified identifiers
// Throw verification exception and assign verified identifiers to the interaction
if (event !== Event.ForgotPassword) {
await assignIdentifierVerificationResult(
{ event, identifiers: [verifiedPasscodeIdentifier] },
@ -90,7 +95,7 @@ const socialIdentifierVerification = async (
const user = await findUserByIdentifier({ connectorId, userInfo });
if (!user) {
// Throw verification result and assign verified identifiers
// Throw verification exception and assign verified identifiers to the interaction
await assignIdentifierVerificationResult(
{ event, identifiers: [socialIdentifier] },
ctx,

View file

@ -1 +1,3 @@
export { default as identifierVerification } from './identifier-verification.js';
export { default as profileVerification } from './profile-verification.js';
export { default as mandatoryUserProfileValidation } from './mandatory-user-profile-validation.js';

View file

@ -0,0 +1,165 @@
import { MissingProfile, SignInIdentifier } from '@logto/schemas';
import { mockSignInExperience } from '#src/__mocks__/sign-in-experience.js';
import RequestError from '#src/errors/RequestError/index.js';
import { findUserById } from '#src/queries/user.js';
import { createContextWithRouteParameters } from '#src/utils/test-utils.js';
import type { Identifier } from '../types/index.js';
import { isUserPasswordSet } from '../utils/index.js';
import mandatoryUserProfileValidation from './mandatory-user-profile-validation.js';
jest.mock('#src/queries/user.js', () => ({
findUserById: jest.fn(),
}));
jest.mock('../utils/index.js', () => ({
isUserPasswordSet: jest.fn(),
}));
describe('mandatoryUserProfileValidation', () => {
const baseCtx = createContextWithRouteParameters();
const identifiers: Identifier[] = [{ key: 'accountId', value: 'foo' }];
it('username and password missing but required', async () => {
const ctx = {
...baseCtx,
signInExperience: mockSignInExperience,
};
await expect(
mandatoryUserProfileValidation(ctx, identifiers, { email: 'email' })
).rejects.toMatchError(
new RequestError(
{ code: 'user.missing_profile', status: 422 },
{ missingProfile: [MissingProfile.password, MissingProfile.username] }
)
);
await expect(
mandatoryUserProfileValidation(ctx, identifiers, {
username: 'username',
password: 'password',
})
).resolves.not.toThrow();
});
it('user account has username and password', async () => {
(findUserById as jest.Mock).mockResolvedValueOnce({
username: 'foo',
});
(isUserPasswordSet as jest.Mock).mockResolvedValueOnce(true);
const ctx = {
...baseCtx,
signInExperience: mockSignInExperience,
};
await expect(mandatoryUserProfileValidation(ctx, identifiers, {})).resolves.not.toThrow();
});
it('email missing but required', async () => {
const ctx = {
...baseCtx,
signInExperience: {
...mockSignInExperience,
signUp: { identifiers: [SignInIdentifier.Email], password: false, verify: true },
},
};
await expect(
mandatoryUserProfileValidation(ctx, identifiers, { username: 'username' })
).rejects.toMatchError(
new RequestError(
{ code: 'user.missing_profile', status: 422 },
{ missingProfile: [MissingProfile.email] }
)
);
});
it('user account has email', async () => {
(findUserById as jest.Mock).mockResolvedValueOnce({
primaryEmail: 'email',
});
const ctx = {
...baseCtx,
signInExperience: {
...mockSignInExperience,
signUp: { identifiers: [SignInIdentifier.Email], password: false, verify: true },
},
};
await expect(
mandatoryUserProfileValidation(ctx, identifiers, { username: 'username' })
).resolves.not.toThrow();
});
it('phone missing but required', async () => {
const ctx = {
...baseCtx,
signInExperience: {
...mockSignInExperience,
signUp: { identifiers: [SignInIdentifier.Sms], password: false, verify: true },
},
};
await expect(
mandatoryUserProfileValidation(ctx, identifiers, { username: 'username' })
).rejects.toMatchError(
new RequestError(
{ code: 'user.missing_profile', status: 422 },
{ missingProfile: [MissingProfile.phone] }
)
);
});
it('user account has phone', async () => {
(findUserById as jest.Mock).mockResolvedValueOnce({
primaryPhone: 'phone',
});
const ctx = {
...baseCtx,
signInExperience: {
...mockSignInExperience,
signUp: { identifiers: [SignInIdentifier.Sms], password: false, verify: true },
},
};
await expect(
mandatoryUserProfileValidation(ctx, identifiers, { username: 'username' })
).resolves.not.toThrow();
});
it('email or Phone required', async () => {
const ctx = {
...baseCtx,
signInExperience: {
...mockSignInExperience,
signUp: {
identifiers: [SignInIdentifier.Email, SignInIdentifier.Sms],
password: false,
verify: true,
},
},
};
await expect(
mandatoryUserProfileValidation(ctx, identifiers, { username: 'username' })
).rejects.toMatchError(
new RequestError(
{ code: 'user.missing_profile', status: 422 },
{ missingProfile: [MissingProfile.emailOrPhone] }
)
);
await expect(
mandatoryUserProfileValidation(ctx, identifiers, { email: 'email' })
).resolves.not.toThrow();
await expect(
mandatoryUserProfileValidation(ctx, identifiers, { phone: 'phone' })
).resolves.not.toThrow();
});
});

View file

@ -0,0 +1,102 @@
import type { Profile, SignInExperience, User } from '@logto/schemas';
import { MissingProfile, SignInIdentifier } from '@logto/schemas';
import type { Nullable } from '@silverhand/essentials';
import type { Context } from 'koa';
import RequestError from '#src/errors/RequestError/index.js';
import { findUserById } from '#src/queries/user.js';
import assertThat from '#src/utils/assert-that.js';
import type { WithSignInExperienceContext } from '../middleware/koa-session-sign-in-experience-guard.js';
import type { Identifier, AccountIdIdentifier } from '../types/index.js';
import { isUserPasswordSet } from '../utils/index.js';
const findUserByIdentifiers = async (identifiers: Identifier[]) => {
const accountIdentifier = identifiers.find(
(identifier): identifier is AccountIdIdentifier => identifier.key === 'accountId'
);
if (!accountIdentifier) {
return null;
}
return findUserById(accountIdentifier.value);
};
// eslint-disable-next-line complexity
const getMissingProfileBySignUpIdentifiers = ({
signUp,
user,
profile,
}: {
signUp: SignInExperience['signUp'];
user: Nullable<User>;
profile?: Profile;
}) => {
const missingProfile = new Set<MissingProfile>();
if (signUp.password && !(user && isUserPasswordSet(user)) && !profile?.password) {
missingProfile.add(MissingProfile.password);
}
const signUpIdentifiersSet = new Set(signUp.identifiers);
// Username
if (
signUpIdentifiersSet.has(SignInIdentifier.Username) &&
!user?.username &&
!profile?.username
) {
missingProfile.add(MissingProfile.username);
return missingProfile;
}
// Email or phone
if (
signUpIdentifiersSet.has(SignInIdentifier.Email) &&
signUpIdentifiersSet.has(SignInIdentifier.Sms)
) {
if (!user?.primaryPhone && !user?.primaryEmail && !profile?.phone && !profile?.email) {
missingProfile.add(MissingProfile.emailOrPhone);
}
return missingProfile;
}
// Email only
if (signUpIdentifiersSet.has(SignInIdentifier.Email) && !user?.primaryEmail && !profile?.email) {
missingProfile.add(MissingProfile.email);
return missingProfile;
}
// Phone only
if (signUpIdentifiersSet.has(SignInIdentifier.Sms) && !user?.primaryPhone && !profile?.phone) {
missingProfile.add(MissingProfile.phone);
return missingProfile;
}
return missingProfile;
};
export default async function mandatoryUserProfileValidation(
ctx: WithSignInExperienceContext<Context>,
identifiers: Identifier[],
profile?: Profile
) {
const {
signInExperience: { signUp },
} = ctx;
const user = await findUserByIdentifiers(identifiers);
const missingProfileSet = getMissingProfileBySignUpIdentifiers({ signUp, user, profile });
assertThat(
missingProfileSet.size === 0,
new RequestError(
{ code: 'user.missing_profile', status: 422 },
{ missingProfile: Array.from(missingProfileSet) }
)
);
}

View file

@ -177,7 +177,7 @@ const profileExistValidation = async (
export default async function profileVerification(
ctx: InteractionContext,
identifiers: Identifier[]
) {
): Promise<Profile | undefined> {
const { profile, event } = ctx.interactionPayload;
if (!profile) {
@ -193,7 +193,7 @@ export default async function profileVerification(
await profileExistValidation(profile, user);
await profileRegisteredValidation(profile, identifiers);
return;
return profile;
}
if (event === Event.Register) {
@ -206,7 +206,7 @@ export default async function profileVerification(
await profileRegisteredValidation(profile, identifiers);
return;
return profile;
}
// ForgotPassword
@ -216,4 +216,6 @@ export default async function profileVerification(
!oldPasswordEncrypted || !(await argon2Verify({ password, hash: oldPasswordEncrypted })),
new RequestError({ code: 'user.same_password', status: 422 })
);
return profile;
}

View file

@ -57,6 +57,7 @@ const errors = {
require_email_or_sms: 'You need to add an email address or phone number before signing-in.', // UNTRANSLATED
suspended: 'This account is suspended.', // UNTRANSLATED
user_not_exist: 'User with {{ identity }} has not been registered yet', // UNTRANSLATED,
missing_profile: 'You need to provide additional info before signing-in.', // UNTRANSLATED
},
password: {
unsupported_encryption_method: 'Die Verschlüsselungsmethode {{name}} wird nicht unterstützt.',

View file

@ -57,6 +57,7 @@ const errors = {
require_email_or_sms: 'You need to add an email address or phone number before signing-in.',
suspended: 'This account is suspended.',
user_not_exist: 'User with {{ identity }} has not been registered yet',
missing_profile: 'You need to provide additional info before signing-in.',
},
password: {
unsupported_encryption_method: 'The encryption method {{name}} is not supported.',

View file

@ -58,6 +58,7 @@ const errors = {
require_email_or_sms: 'You need to add an email address or phone number before signing-in.', // UNTRANSLATED
suspended: 'This account is suspended.', // UNTRANSLATED
user_not_exist: 'User with {{ identity }} has not been registered yet', // UNTRANSLATED,
missing_profile: 'You need to provide additional info before signing-in.', // UNTRANSLATED
},
password: {
unsupported_encryption_method: "La méthode de cryptage {{name}} n'est pas prise en charge.",

View file

@ -56,6 +56,7 @@ const errors = {
require_email_or_sms: 'You need to add an email address or phone number before signing-in.', // UNTRANSLATED
suspended: 'This account is suspended.', // UNTRANSLATED
user_not_exist: 'User with {{ identity }} has not been registered yet', // UNTRANSLATED,
missing_profile: 'You need to provide additional info before signing-in.', // UNTRANSLATED
},
password: {
unsupported_encryption_method: '{{name}} 암호화 방법을 지원하지 않아요.',

View file

@ -56,6 +56,7 @@ const errors = {
require_email_or_sms: 'You need to add an email address or phone number before signing-in.', // UNTRANSLATED
suspended: 'This account is suspended.', // UNTRANSLATED
user_not_exist: 'User with {{ identity }} has not been registered yet', // UNTRANSLATED,
missing_profile: 'You need to provide additional info before signing-in.', // UNTRANSLATED
},
password: {
unsupported_encryption_method: 'O método de enncriptação {{name}} não é suportado.',

View file

@ -57,6 +57,7 @@ const errors = {
require_email_or_sms: 'You need to add an email address or phone number before signing-in.', // UNTRANSLATED
suspended: 'This account is suspended.', // UNTRANSLATED
user_not_exist: 'User with {{ identity }} has not been registered yet', // UNTRANSLATED,
missing_profile: 'You need to provide additional info before signing-in.', // UNTRANSLATED
},
password: {
unsupported_encryption_method: '{{name}} şifreleme metodu desteklenmiyor.',

View file

@ -56,6 +56,7 @@ const errors = {
require_email_or_sms: '请绑定邮箱地址或手机号码',
suspended: '账号已被禁用',
user_not_exist: 'User with {{ identity }} has not been registered yet', // UNTRANSLATED,
missing_profile: 'You need to provide additional info before signing-in.', // UNTRANSLATED
},
password: {
unsupported_encryption_method: '不支持的加密方法 {{name}}',

View file

@ -52,7 +52,7 @@ export enum Event {
export const eventGuard = z.nativeEnum(Event);
export const identifierGuard = z.union([
export const identifierPayloadGuard = z.union([
usernamePasswordPayloadGuard,
emailPasswordPayloadGuard,
phonePasswordPayloadGuard,
@ -61,6 +61,14 @@ export const identifierGuard = z.union([
socialConnectorPayloadGuard,
]);
export type IdentifierPayload =
| UsernamePasswordPayload
| EmailPasswordPayload
| PhonePasswordPayload
| EmailPasscodePayload
| PhonePasscodePayload
| SocialConnectorPayload;
export const profileGuard = z.object({
username: z.string().regex(usernameRegEx).optional(),
email: z.string().regex(emailRegEx).optional(),
@ -70,3 +78,11 @@ export const profileGuard = z.object({
});
export type Profile = z.infer<typeof profileGuard>;
export enum MissingProfile {
username = 'username',
email = 'email',
phone = 'phone',
password = 'password',
emailOrPhone = 'emailOrPhone',
}