mirror of
https://github.com/logto-io/logto.git
synced 2025-02-17 22:04:19 -05:00
feat(core): require set a password (#2272)
This commit is contained in:
parent
e1d3d34523
commit
bf73837839
15 changed files with 283 additions and 19 deletions
|
@ -8,8 +8,8 @@ export const mockUser: User = {
|
||||||
primaryEmail: 'foo@logto.io',
|
primaryEmail: 'foo@logto.io',
|
||||||
primaryPhone: '111111',
|
primaryPhone: '111111',
|
||||||
roleNames: ['admin'],
|
roleNames: ['admin'],
|
||||||
passwordEncrypted: null,
|
passwordEncrypted: 'password',
|
||||||
passwordEncryptionMethod: null,
|
passwordEncryptionMethod: UsersPasswordEncryptionMethod.Argon2i,
|
||||||
name: null,
|
name: null,
|
||||||
avatar: null,
|
avatar: null,
|
||||||
identities: {
|
identities: {
|
||||||
|
|
|
@ -1 +1,2 @@
|
||||||
export const verificationTimeout = 10 * 60; // 10 mins.
|
export const verificationTimeout = 10 * 60; // 10 mins.
|
||||||
|
export const continueSignInTimeout = 10 * 60; // 10 mins.
|
||||||
|
|
113
packages/core/src/routes/session/continue.test.ts
Normal file
113
packages/core/src/routes/session/continue.test.ts
Normal file
|
@ -0,0 +1,113 @@
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import { Provider } from 'oidc-provider';
|
||||||
|
|
||||||
|
import { mockUser } from '@/__mocks__';
|
||||||
|
import { createRequester } from '@/utils/test-utils';
|
||||||
|
|
||||||
|
import continueRoutes, { continueRoute } from './continue';
|
||||||
|
|
||||||
|
const checkRequiredProfile = jest.fn();
|
||||||
|
jest.mock('./utils', () => ({
|
||||||
|
...jest.requireActual('./utils'),
|
||||||
|
checkRequiredProfile: () => checkRequiredProfile(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('@/queries/sign-in-experience', () => ({
|
||||||
|
findDefaultSignInExperience: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const updateUserById = jest.fn(async (..._args: unknown[]) => mockUser);
|
||||||
|
const findUserById = jest.fn(async (..._args: unknown[]) => mockUser);
|
||||||
|
|
||||||
|
jest.mock('@/queries/user', () => ({
|
||||||
|
updateUserById: async (...args: unknown[]) => updateUserById(...args),
|
||||||
|
findUserById: async () => findUserById(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const interactionResult = jest.fn(async () => 'redirectTo');
|
||||||
|
const interactionDetails: jest.MockedFunction<() => Promise<unknown>> = jest.fn(async () => ({}));
|
||||||
|
|
||||||
|
jest.mock('oidc-provider', () => ({
|
||||||
|
Provider: jest.fn(() => ({
|
||||||
|
interactionDetails,
|
||||||
|
interactionResult,
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
interactionResult.mockClear();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('session -> continueRoutes', () => {
|
||||||
|
const sessionRequest = createRequester({
|
||||||
|
anonymousRoutes: continueRoutes,
|
||||||
|
provider: new Provider(''),
|
||||||
|
middlewares: [
|
||||||
|
async (ctx, next) => {
|
||||||
|
ctx.addLogContext = jest.fn();
|
||||||
|
ctx.log = jest.fn();
|
||||||
|
|
||||||
|
return next();
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('POST /session/sign-in/continue/password', () => {
|
||||||
|
it('updates user password, checks required profile, and sign in', async () => {
|
||||||
|
interactionDetails.mockResolvedValueOnce({
|
||||||
|
jti: 'jti',
|
||||||
|
result: {
|
||||||
|
continueSignIn: {
|
||||||
|
userId: mockUser.id,
|
||||||
|
expiresAt: dayjs().add(1, 'day').toISOString(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
findUserById.mockResolvedValueOnce({
|
||||||
|
...mockUser,
|
||||||
|
passwordEncrypted: null,
|
||||||
|
identities: {},
|
||||||
|
});
|
||||||
|
const response = await sessionRequest.post(`${continueRoute}/password`).send({
|
||||||
|
password: 'password',
|
||||||
|
});
|
||||||
|
expect(response.statusCode).toEqual(200);
|
||||||
|
expect(checkRequiredProfile).toHaveBeenCalled();
|
||||||
|
expect(updateUserById).toHaveBeenCalledWith(mockUser.id, expect.anything());
|
||||||
|
expect(response.body).toHaveProperty('redirectTo');
|
||||||
|
expect(interactionResult).toHaveBeenCalledWith(
|
||||||
|
expect.anything(),
|
||||||
|
expect.anything(),
|
||||||
|
expect.objectContaining({ login: { accountId: mockUser.id } }),
|
||||||
|
expect.anything()
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws on empty continue sign in storage', async () => {
|
||||||
|
interactionDetails.mockResolvedValueOnce({
|
||||||
|
jti: 'jti',
|
||||||
|
result: {},
|
||||||
|
});
|
||||||
|
const response = await sessionRequest.post(`${continueRoute}/password`).send({
|
||||||
|
password: 'password',
|
||||||
|
});
|
||||||
|
expect(response.statusCode).toEqual(401);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws on expired continue sign in storage', async () => {
|
||||||
|
interactionDetails.mockResolvedValueOnce({
|
||||||
|
jti: 'jti',
|
||||||
|
result: {
|
||||||
|
continueSignIn: {
|
||||||
|
userId: mockUser.id,
|
||||||
|
expiresAt: dayjs().subtract(1, 'second').toISOString(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const response = await sessionRequest.post(`${continueRoute}/password`).send({
|
||||||
|
password: 'password',
|
||||||
|
});
|
||||||
|
expect(response.statusCode).toEqual(401);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
51
packages/core/src/routes/session/continue.ts
Normal file
51
packages/core/src/routes/session/continue.ts
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
import { passwordRegEx } from '@logto/core-kit';
|
||||||
|
import type { Provider } from 'oidc-provider';
|
||||||
|
import { object, string } from 'zod';
|
||||||
|
|
||||||
|
import RequestError from '@/errors/RequestError';
|
||||||
|
import { assignInteractionResults } from '@/lib/session';
|
||||||
|
import { encryptUserPassword } from '@/lib/user';
|
||||||
|
import koaGuard from '@/middleware/koa-guard';
|
||||||
|
import { findDefaultSignInExperience } from '@/queries/sign-in-experience';
|
||||||
|
import { findUserById, updateUserById } from '@/queries/user';
|
||||||
|
import assertThat from '@/utils/assert-that';
|
||||||
|
|
||||||
|
import type { AnonymousRouter } from '../types';
|
||||||
|
import { checkRequiredProfile, getContinueSignInResult, getRoutePrefix } from './utils';
|
||||||
|
|
||||||
|
export const continueRoute = getRoutePrefix('sign-in', 'continue');
|
||||||
|
|
||||||
|
export default function continueRoutes<T extends AnonymousRouter>(router: T, provider: Provider) {
|
||||||
|
router.post(
|
||||||
|
`${continueRoute}/password`,
|
||||||
|
koaGuard({
|
||||||
|
body: object({
|
||||||
|
password: string().regex(passwordRegEx),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
async (ctx, next) => {
|
||||||
|
const { password } = ctx.guard.body;
|
||||||
|
const { userId } = await getContinueSignInResult(ctx, provider);
|
||||||
|
const user = await findUserById(userId);
|
||||||
|
|
||||||
|
// Social identities can take place the role of password
|
||||||
|
assertThat(
|
||||||
|
!user.passwordEncrypted && Object.keys(user.identities).length === 0,
|
||||||
|
new RequestError({
|
||||||
|
code: 'user.password_exists',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const { passwordEncrypted, passwordEncryptionMethod } = await encryptUserPassword(password);
|
||||||
|
const updatedUser = await updateUserById(userId, {
|
||||||
|
passwordEncrypted,
|
||||||
|
passwordEncryptionMethod,
|
||||||
|
});
|
||||||
|
const signInExperience = await findDefaultSignInExperience();
|
||||||
|
await checkRequiredProfile(ctx, provider, updatedUser, signInExperience);
|
||||||
|
await assignInteractionResults(ctx, provider, { login: { accountId: updatedUser.id } });
|
||||||
|
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
|
@ -13,6 +13,7 @@ import { findUserById } from '@/queries/user';
|
||||||
import assertThat from '@/utils/assert-that';
|
import assertThat from '@/utils/assert-that';
|
||||||
|
|
||||||
import type { AnonymousRouter } from '../types';
|
import type { AnonymousRouter } from '../types';
|
||||||
|
import continueRoutes from './continue';
|
||||||
import forgotPasswordRoutes from './forgot-password';
|
import forgotPasswordRoutes from './forgot-password';
|
||||||
import koaGuardSessionAction from './middleware/koa-guard-session-action';
|
import koaGuardSessionAction from './middleware/koa-guard-session-action';
|
||||||
import passwordRoutes from './password';
|
import passwordRoutes from './password';
|
||||||
|
@ -103,6 +104,7 @@ export default function sessionRoutes<T extends AnonymousRouter>(router: T, prov
|
||||||
passwordRoutes(router, provider);
|
passwordRoutes(router, provider);
|
||||||
passwordlessRoutes(router, provider);
|
passwordlessRoutes(router, provider);
|
||||||
socialRoutes(router, provider);
|
socialRoutes(router, provider);
|
||||||
|
continueRoutes(router, provider);
|
||||||
|
|
||||||
forgotPasswordRoutes(router, provider);
|
forgotPasswordRoutes(router, provider);
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,7 @@ import {
|
||||||
getVerificationStorageFromInteraction,
|
getVerificationStorageFromInteraction,
|
||||||
getPasswordlessRelatedLogType,
|
getPasswordlessRelatedLogType,
|
||||||
checkValidateExpiration,
|
checkValidateExpiration,
|
||||||
|
checkRequiredProfile,
|
||||||
} from '../utils';
|
} from '../utils';
|
||||||
|
|
||||||
export const smsSignInAction = <StateT, ContextT extends WithLogContext, ResponseBodyT>(
|
export const smsSignInAction = <StateT, ContextT extends WithLogContext, ResponseBodyT>(
|
||||||
|
@ -57,9 +58,11 @@ export const smsSignInAction = <StateT, ContextT extends WithLogContext, Respons
|
||||||
new RequestError({ code: 'user.phone_not_exists', status: 404 })
|
new RequestError({ code: 'user.phone_not_exists', status: 404 })
|
||||||
);
|
);
|
||||||
|
|
||||||
const { id } = await findUserByPhone(phone);
|
const user = await findUserByPhone(phone);
|
||||||
|
const { id } = user;
|
||||||
ctx.log(type, { userId: id });
|
ctx.log(type, { userId: id });
|
||||||
|
|
||||||
|
await checkRequiredProfile(ctx, provider, user, signInExperience);
|
||||||
await updateUserById(id, { lastSignInAt: Date.now() });
|
await updateUserById(id, { lastSignInAt: Date.now() });
|
||||||
await assignInteractionResults(ctx, provider, { login: { accountId: id } });
|
await assignInteractionResults(ctx, provider, { login: { accountId: id } });
|
||||||
|
|
||||||
|
@ -101,9 +104,11 @@ export const emailSignInAction = <StateT, ContextT extends WithLogContext, Respo
|
||||||
new RequestError({ code: 'user.email_not_exists', status: 404 })
|
new RequestError({ code: 'user.email_not_exists', status: 404 })
|
||||||
);
|
);
|
||||||
|
|
||||||
const { id } = await findUserByEmail(email);
|
const user = await findUserByEmail(email);
|
||||||
|
const { id } = user;
|
||||||
ctx.log(type, { userId: id });
|
ctx.log(type, { userId: id });
|
||||||
|
|
||||||
|
await checkRequiredProfile(ctx, provider, user, signInExperience);
|
||||||
await updateUserById(id, { lastSignInAt: Date.now() });
|
await updateUserById(id, { lastSignInAt: Date.now() });
|
||||||
await assignInteractionResults(ctx, provider, { login: { accountId: id } });
|
await assignInteractionResults(ctx, provider, { login: { accountId: id } });
|
||||||
|
|
||||||
|
@ -145,7 +150,8 @@ export const smsRegisterAction = <StateT, ContextT extends WithLogContext, Respo
|
||||||
const id = await generateUserId();
|
const id = await generateUserId();
|
||||||
ctx.log(type, { userId: id });
|
ctx.log(type, { userId: id });
|
||||||
|
|
||||||
await insertUser({ id, primaryPhone: phone, lastSignInAt: Date.now() });
|
const user = await insertUser({ id, primaryPhone: phone, lastSignInAt: Date.now() });
|
||||||
|
await checkRequiredProfile(ctx, provider, user, signInExperience);
|
||||||
await assignInteractionResults(ctx, provider, { login: { accountId: id } });
|
await assignInteractionResults(ctx, provider, { login: { accountId: id } });
|
||||||
|
|
||||||
return next();
|
return next();
|
||||||
|
@ -186,7 +192,8 @@ export const emailRegisterAction = <StateT, ContextT extends WithLogContext, Res
|
||||||
const id = await generateUserId();
|
const id = await generateUserId();
|
||||||
ctx.log(type, { userId: id });
|
ctx.log(type, { userId: id });
|
||||||
|
|
||||||
await insertUser({ id, primaryEmail: email, lastSignInAt: Date.now() });
|
const user = await insertUser({ id, primaryEmail: email, lastSignInAt: Date.now() });
|
||||||
|
await checkRequiredProfile(ctx, provider, user, signInExperience);
|
||||||
await assignInteractionResults(ctx, provider, { login: { accountId: id } });
|
await assignInteractionResults(ctx, provider, { login: { accountId: id } });
|
||||||
|
|
||||||
return next();
|
return next();
|
||||||
|
|
|
@ -12,14 +12,17 @@ import { verificationTimeout } from './consts';
|
||||||
import * as passwordlessActions from './middleware/passwordless-action';
|
import * as passwordlessActions from './middleware/passwordless-action';
|
||||||
import passwordlessRoutes, { registerRoute, signInRoute } from './passwordless';
|
import passwordlessRoutes, { registerRoute, signInRoute } from './passwordless';
|
||||||
|
|
||||||
const insertUser = jest.fn(async (..._args: unknown[]) => ({ id: 'id' }));
|
const insertUser = jest.fn(async (..._args: unknown[]) => ({ id: 'foo' }));
|
||||||
const findUserById = jest.fn(async (): Promise<User> => mockUser);
|
const findUserById = jest.fn(async (): Promise<User> => mockUser);
|
||||||
const updateUserById = jest.fn(async (..._args: unknown[]) => ({ id: 'id' }));
|
const findUserByEmail = jest.fn(async (): Promise<User> => mockUser);
|
||||||
|
const updateUserById = jest.fn(async (..._args: unknown[]) => ({ id: 'foo' }));
|
||||||
const findDefaultSignInExperience = jest.fn(async () => ({
|
const findDefaultSignInExperience = jest.fn(async () => ({
|
||||||
...mockSignInExperience,
|
...mockSignInExperience,
|
||||||
signUp: {
|
signUp: {
|
||||||
...mockSignInExperience.signUp,
|
...mockSignInExperience.signUp,
|
||||||
identifier: SignUpIdentifier.Username,
|
identifier: SignUpIdentifier.Username,
|
||||||
|
password: false,
|
||||||
|
verify: true,
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
@ -30,8 +33,8 @@ jest.mock('@/lib/user', () => ({
|
||||||
|
|
||||||
jest.mock('@/queries/user', () => ({
|
jest.mock('@/queries/user', () => ({
|
||||||
findUserById: async () => findUserById(),
|
findUserById: async () => findUserById(),
|
||||||
findUserByPhone: async () => ({ id: 'id' }),
|
findUserByPhone: async () => ({ id: 'foo' }),
|
||||||
findUserByEmail: async () => ({ id: 'id' }),
|
findUserByEmail: async () => findUserByEmail(),
|
||||||
updateUserById: async (...args: unknown[]) => updateUserById(...args),
|
updateUserById: async (...args: unknown[]) => updateUserById(...args),
|
||||||
hasUser: async (username: string) => username === 'username1',
|
hasUser: async (username: string) => username === 'username1',
|
||||||
hasUserWithPhone: async (phone: string) => phone === '13000000000',
|
hasUserWithPhone: async (phone: string) => phone === '13000000000',
|
||||||
|
@ -247,7 +250,7 @@ describe('session -> passwordlessRoutes', () => {
|
||||||
expect.anything(),
|
expect.anything(),
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
verification: {
|
verification: {
|
||||||
userId: 'id',
|
userId: 'foo',
|
||||||
expiresAt: dayjs(fakeTime).add(verificationTimeout, 'second').toISOString(),
|
expiresAt: dayjs(fakeTime).add(verificationTimeout, 'second').toISOString(),
|
||||||
flow: PasscodeType.ForgotPassword,
|
flow: PasscodeType.ForgotPassword,
|
||||||
},
|
},
|
||||||
|
@ -343,7 +346,7 @@ describe('session -> passwordlessRoutes', () => {
|
||||||
expect.anything(),
|
expect.anything(),
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
verification: {
|
verification: {
|
||||||
userId: 'id',
|
userId: 'foo',
|
||||||
expiresAt: dayjs(fakeTime).add(verificationTimeout, 'second').toISOString(),
|
expiresAt: dayjs(fakeTime).add(verificationTimeout, 'second').toISOString(),
|
||||||
flow: PasscodeType.ForgotPassword,
|
flow: PasscodeType.ForgotPassword,
|
||||||
},
|
},
|
||||||
|
@ -388,7 +391,7 @@ describe('session -> passwordlessRoutes', () => {
|
||||||
expect.anything(),
|
expect.anything(),
|
||||||
expect.anything(),
|
expect.anything(),
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
login: { accountId: 'id' },
|
login: { accountId: 'foo' },
|
||||||
}),
|
}),
|
||||||
expect.anything()
|
expect.anything()
|
||||||
);
|
);
|
||||||
|
@ -410,7 +413,7 @@ describe('session -> passwordlessRoutes', () => {
|
||||||
expect.anything(),
|
expect.anything(),
|
||||||
expect.anything(),
|
expect.anything(),
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
login: { accountId: 'id' },
|
login: { accountId: 'foo' },
|
||||||
}),
|
}),
|
||||||
expect.anything()
|
expect.anything()
|
||||||
);
|
);
|
||||||
|
@ -523,6 +526,8 @@ describe('session -> passwordlessRoutes', () => {
|
||||||
signUp: {
|
signUp: {
|
||||||
...mockSignInExperience.signUp,
|
...mockSignInExperience.signUp,
|
||||||
identifier: SignUpIdentifier.Email,
|
identifier: SignUpIdentifier.Email,
|
||||||
|
password: false,
|
||||||
|
verify: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -549,7 +554,7 @@ describe('session -> passwordlessRoutes', () => {
|
||||||
expect.anything(),
|
expect.anything(),
|
||||||
expect.anything(),
|
expect.anything(),
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
login: { accountId: 'id' },
|
login: { accountId: 'foo' },
|
||||||
}),
|
}),
|
||||||
expect.anything()
|
expect.anything()
|
||||||
);
|
);
|
||||||
|
@ -573,7 +578,7 @@ describe('session -> passwordlessRoutes', () => {
|
||||||
expect.anything(),
|
expect.anything(),
|
||||||
expect.anything(),
|
expect.anything(),
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
login: { accountId: 'id' },
|
login: { accountId: 'foo' },
|
||||||
}),
|
}),
|
||||||
expect.anything()
|
expect.anything()
|
||||||
);
|
);
|
||||||
|
@ -657,6 +662,7 @@ describe('session -> passwordlessRoutes', () => {
|
||||||
signUp: {
|
signUp: {
|
||||||
...mockSignInExperience.signUp,
|
...mockSignInExperience.signUp,
|
||||||
identifier: SignUpIdentifier.Sms,
|
identifier: SignUpIdentifier.Sms,
|
||||||
|
password: false,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -784,6 +790,7 @@ describe('session -> passwordlessRoutes', () => {
|
||||||
signUp: {
|
signUp: {
|
||||||
...mockSignInExperience.signUp,
|
...mockSignInExperience.signUp,
|
||||||
identifier: SignUpIdentifier.Email,
|
identifier: SignUpIdentifier.Email,
|
||||||
|
password: false,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -51,3 +51,10 @@ export type VerificationStorage =
|
||||||
| ForgotPasswordSessionStorage;
|
| ForgotPasswordSessionStorage;
|
||||||
|
|
||||||
export type VerificationResult<T = VerificationStorage> = { verification: T };
|
export type VerificationResult<T = VerificationStorage> = { verification: T };
|
||||||
|
|
||||||
|
export const continueSignInStorageGuard = z.object({
|
||||||
|
userId: z.string(),
|
||||||
|
expiresAt: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type ContinueSignInStorage = z.infer<typeof continueSignInStorageGuard>;
|
||||||
|
|
|
@ -1,4 +1,11 @@
|
||||||
import type { LogPayload, LogType, PasscodeType, SignInIdentifier, User } from '@logto/schemas';
|
import type {
|
||||||
|
LogPayload,
|
||||||
|
LogType,
|
||||||
|
PasscodeType,
|
||||||
|
SignInExperience,
|
||||||
|
SignInIdentifier,
|
||||||
|
User,
|
||||||
|
} from '@logto/schemas';
|
||||||
import { logTypeGuard } from '@logto/schemas';
|
import { logTypeGuard } from '@logto/schemas';
|
||||||
import type { Nullable, Truthy } from '@silverhand/essentials';
|
import type { Nullable, Truthy } from '@silverhand/essentials';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
|
@ -15,12 +22,13 @@ import { findDefaultSignInExperience } from '@/queries/sign-in-experience';
|
||||||
import { updateUserById } from '@/queries/user';
|
import { updateUserById } from '@/queries/user';
|
||||||
import assertThat from '@/utils/assert-that';
|
import assertThat from '@/utils/assert-that';
|
||||||
|
|
||||||
import { verificationTimeout } from './consts';
|
import { continueSignInTimeout, verificationTimeout } from './consts';
|
||||||
import type { Method, Operation, VerificationResult, VerificationStorage } from './types';
|
import type { Method, Operation, VerificationResult, VerificationStorage } from './types';
|
||||||
|
import { continueSignInStorageGuard } from './types';
|
||||||
|
|
||||||
export const getRoutePrefix = (
|
export const getRoutePrefix = (
|
||||||
type: 'sign-in' | 'register' | 'forgot-password',
|
type: 'sign-in' | 'register' | 'forgot-password',
|
||||||
method?: 'passwordless' | 'password' | 'social'
|
method?: 'passwordless' | 'password' | 'social' | 'continue'
|
||||||
) => {
|
) => {
|
||||||
return ['session', type, method]
|
return ['session', type, method]
|
||||||
.filter((value): value is Truthy<typeof value> => value !== undefined)
|
.filter((value): value is Truthy<typeof value> => value !== undefined)
|
||||||
|
@ -100,6 +108,62 @@ export const clearVerificationResult = async (ctx: Context, provider: Provider)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const assignContinueSignInResult = async (
|
||||||
|
ctx: Context,
|
||||||
|
provider: Provider,
|
||||||
|
payload: { userId: string }
|
||||||
|
) => {
|
||||||
|
await provider.interactionResult(ctx.req, ctx.res, {
|
||||||
|
continueSignIn: {
|
||||||
|
...payload,
|
||||||
|
expiresAt: dayjs().add(continueSignInTimeout, 'second').toISOString(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getContinueSignInResult = async (ctx: Context, provider: Provider) => {
|
||||||
|
const { result } = await provider.interactionDetails(ctx.req, ctx.res);
|
||||||
|
|
||||||
|
const signInResult = z
|
||||||
|
.object({
|
||||||
|
continueSignIn: continueSignInStorageGuard,
|
||||||
|
})
|
||||||
|
.safeParse(result);
|
||||||
|
|
||||||
|
if (!signInResult.success) {
|
||||||
|
throw new RequestError({
|
||||||
|
code: 'session.unauthorized',
|
||||||
|
status: 401,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const { expiresAt, ...rest } = signInResult.data.continueSignIn;
|
||||||
|
|
||||||
|
assertThat(
|
||||||
|
dayjs(expiresAt).isValid() && dayjs(expiresAt).isAfter(dayjs()),
|
||||||
|
new RequestError({ code: 'session.unauthorized', status: 401 })
|
||||||
|
);
|
||||||
|
|
||||||
|
return rest;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const checkRequiredProfile = async (
|
||||||
|
ctx: Context,
|
||||||
|
provider: Provider,
|
||||||
|
user: User,
|
||||||
|
signInExperience: SignInExperience
|
||||||
|
) => {
|
||||||
|
const { signUp } = signInExperience;
|
||||||
|
const { passwordEncrypted, id } = user;
|
||||||
|
|
||||||
|
// If check failed, save the sign in result, the user can continue after requirements are meet
|
||||||
|
|
||||||
|
if (signUp.password && !passwordEncrypted) {
|
||||||
|
await assignContinueSignInResult(ctx, provider, { userId: id });
|
||||||
|
throw new RequestError({ code: 'user.require_password', status: 422 });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
type SignInWithPasswordParameter = {
|
type SignInWithPasswordParameter = {
|
||||||
identifier: SignInIdentifier;
|
identifier: SignInIdentifier;
|
||||||
password: string;
|
password: string;
|
||||||
|
|
|
@ -45,6 +45,8 @@ const errors = {
|
||||||
sign_up_method_not_enabled: 'This sign up method is not enabled.',
|
sign_up_method_not_enabled: 'This sign up method is not enabled.',
|
||||||
sign_in_method_not_enabled: 'This sign in method is not enabled.',
|
sign_in_method_not_enabled: 'This sign in method is not enabled.',
|
||||||
same_password: 'Your new password can’t be the same as your current password.',
|
same_password: 'Your new password can’t be the same as your current password.',
|
||||||
|
require_password: 'You need to set a password before sign in.',
|
||||||
|
password_exists: 'Your password has been set.',
|
||||||
},
|
},
|
||||||
password: {
|
password: {
|
||||||
unsupported_encryption_method: 'The encryption method {{name}} is not supported.',
|
unsupported_encryption_method: 'The encryption method {{name}} is not supported.',
|
||||||
|
|
|
@ -46,6 +46,8 @@ const errors = {
|
||||||
sign_up_method_not_enabled: 'This sign up method is not enabled.', // UNTRANSLATED
|
sign_up_method_not_enabled: 'This sign up method is not enabled.', // UNTRANSLATED
|
||||||
sign_in_method_not_enabled: 'This sign in method is not enabled.', // UNTRANSLATED
|
sign_in_method_not_enabled: 'This sign in method is not enabled.', // UNTRANSLATED
|
||||||
same_password: 'Your new password can’t be the same as your current password.', // UNTRANSLATED
|
same_password: 'Your new password can’t be the same as your current password.', // UNTRANSLATED
|
||||||
|
require_password: 'You need to set a password before sign in.', // UNTRANSLATED
|
||||||
|
password_exists: 'Your password has been set.', // UNTRANSLATED
|
||||||
},
|
},
|
||||||
password: {
|
password: {
|
||||||
unsupported_encryption_method: "La méthode de cryptage {{name}} n'est pas prise en charge.",
|
unsupported_encryption_method: "La méthode de cryptage {{name}} n'est pas prise en charge.",
|
||||||
|
|
|
@ -44,6 +44,8 @@ const errors = {
|
||||||
sign_up_method_not_enabled: 'This sign up method is not enabled.', // UNTRANSLATED
|
sign_up_method_not_enabled: 'This sign up method is not enabled.', // UNTRANSLATED
|
||||||
sign_in_method_not_enabled: 'This sign in method is not enabled.', // UNTRANSLATED
|
sign_in_method_not_enabled: 'This sign in method is not enabled.', // UNTRANSLATED
|
||||||
same_password: 'Your new password can’t be the same as your current password.', // UNTRANSLATED
|
same_password: 'Your new password can’t be the same as your current password.', // UNTRANSLATED
|
||||||
|
require_password: 'You need to set a password before sign in.', // UNTRANSLATED
|
||||||
|
password_exists: 'Your password has been set.', // UNTRANSLATED
|
||||||
},
|
},
|
||||||
password: {
|
password: {
|
||||||
unsupported_encryption_method: '{{name}} 암호화 방법을 지원하지 않아요.',
|
unsupported_encryption_method: '{{name}} 암호화 방법을 지원하지 않아요.',
|
||||||
|
|
|
@ -44,6 +44,8 @@ const errors = {
|
||||||
sign_up_method_not_enabled: 'This sign up method is not enabled.', // UNTRANSLATED
|
sign_up_method_not_enabled: 'This sign up method is not enabled.', // UNTRANSLATED
|
||||||
sign_in_method_not_enabled: 'This sign in method is not enabled.', // UNTRANSLATED
|
sign_in_method_not_enabled: 'This sign in method is not enabled.', // UNTRANSLATED
|
||||||
same_password: 'Your new password can’t be the same as your current password.', // UNTRANSLATED
|
same_password: 'Your new password can’t be the same as your current password.', // UNTRANSLATED
|
||||||
|
require_password: 'You need to set a password before sign in.', // UNTRANSLATED
|
||||||
|
password_exists: 'Your password has been set.', // UNTRANSLATED
|
||||||
},
|
},
|
||||||
password: {
|
password: {
|
||||||
unsupported_encryption_method: 'O método de enncriptação {{name}} não é suportado.',
|
unsupported_encryption_method: 'O método de enncriptação {{name}} não é suportado.',
|
||||||
|
|
|
@ -45,6 +45,8 @@ const errors = {
|
||||||
sign_up_method_not_enabled: 'This sign up method is not enabled.', // UNTRANSLATED
|
sign_up_method_not_enabled: 'This sign up method is not enabled.', // UNTRANSLATED
|
||||||
sign_in_method_not_enabled: 'This sign in method is not enabled.', // UNTRANSLATED
|
sign_in_method_not_enabled: 'This sign in method is not enabled.', // UNTRANSLATED
|
||||||
same_password: 'Your new password can’t be the same as your current password.', // UNTRANSLATED
|
same_password: 'Your new password can’t be the same as your current password.', // UNTRANSLATED
|
||||||
|
require_password: 'You need to set a password before sign in.', // UNTRANSLATED
|
||||||
|
password_exists: 'Your password has been set.', // UNTRANSLATED
|
||||||
},
|
},
|
||||||
password: {
|
password: {
|
||||||
unsupported_encryption_method: '{{name}} şifreleme metodu desteklenmiyor.',
|
unsupported_encryption_method: '{{name}} şifreleme metodu desteklenmiyor.',
|
||||||
|
|
|
@ -44,6 +44,8 @@ const errors = {
|
||||||
sign_up_method_not_enabled: '注册方式尚未启用',
|
sign_up_method_not_enabled: '注册方式尚未启用',
|
||||||
sign_in_method_not_enabled: '登录方式尚未启用',
|
sign_in_method_not_enabled: '登录方式尚未启用',
|
||||||
same_password: '为确保你的账户安全,新密码不能与旧密码一致',
|
same_password: '为确保你的账户安全,新密码不能与旧密码一致',
|
||||||
|
require_password: '请设置密码',
|
||||||
|
password_exists: '密码已设置过',
|
||||||
},
|
},
|
||||||
password: {
|
password: {
|
||||||
unsupported_encryption_method: '不支持的加密方法 {{name}}',
|
unsupported_encryption_method: '不支持的加密方法 {{name}}',
|
||||||
|
|
Loading…
Add table
Reference in a new issue