mirror of
https://github.com/logto-io/logto.git
synced 2025-01-06 20:40:08 -05:00
feat(core): add POST /session/forgot-password/reset (#1972)
This commit is contained in:
parent
7cc2f4d142
commit
acdc86c856
10 changed files with 261 additions and 11 deletions
|
@ -1,4 +1,4 @@
|
|||
import { User, userInfoSelectFields } from '@logto/schemas';
|
||||
import { userInfoSelectFields, User, UsersPasswordEncryptionMethod } from '@logto/schemas';
|
||||
import pick from 'lodash.pick';
|
||||
|
||||
export const mockUser: User = {
|
||||
|
@ -21,6 +21,25 @@ export const mockUser: User = {
|
|||
|
||||
export const mockUserResponse = pick(mockUser, ...userInfoSelectFields);
|
||||
|
||||
export const mockPasswordEncrypted = 'a1b2c3';
|
||||
export const mockUserWithPassword: User = {
|
||||
id: 'id',
|
||||
username: 'username',
|
||||
primaryEmail: 'foo@logto.io',
|
||||
primaryPhone: '111111',
|
||||
roleNames: ['admin'],
|
||||
passwordEncrypted: mockPasswordEncrypted,
|
||||
passwordEncryptionMethod: UsersPasswordEncryptionMethod.Argon2i,
|
||||
name: null,
|
||||
avatar: null,
|
||||
identities: {
|
||||
connector1: { userId: 'connector1', details: {} },
|
||||
},
|
||||
customData: {},
|
||||
applicationId: 'bar',
|
||||
lastSignInAt: 1_650_969_465_789,
|
||||
};
|
||||
|
||||
export const mockUserList: User[] = [
|
||||
{
|
||||
id: '1',
|
||||
|
|
|
@ -1,16 +1,34 @@
|
|||
import { User } from '@logto/schemas';
|
||||
import dayjs from 'dayjs';
|
||||
import { Provider } from 'oidc-provider';
|
||||
|
||||
import { mockPasswordEncrypted, mockUserWithPassword } from '@/__mocks__';
|
||||
import RequestError from '@/errors/RequestError';
|
||||
import { createRequester } from '@/utils/test-utils';
|
||||
|
||||
import { forgotPasswordVerificationTimeout } from './consts';
|
||||
import forgotPasswordRoutes, { forgotPasswordRoute } from './forgot-password';
|
||||
|
||||
const encryptUserPassword = jest.fn(async (password: string) => ({
|
||||
passwordEncrypted: password + '_user1',
|
||||
passwordEncryptionMethod: 'Argon2i',
|
||||
}));
|
||||
const findUserById = jest.fn(async (): Promise<User> => mockUserWithPassword);
|
||||
const updateUserById = jest.fn(async (..._args: unknown[]) => ({ id: 'id' }));
|
||||
|
||||
jest.mock('@/lib/user', () => ({
|
||||
...jest.requireActual('@/lib/user'),
|
||||
encryptUserPassword: async (password: string) => encryptUserPassword(password),
|
||||
}));
|
||||
|
||||
jest.mock('@/queries/user', () => ({
|
||||
...jest.requireActual('@/queries/user'),
|
||||
hasUserWithPhone: async (phone: string) => phone === '13000000000',
|
||||
findUserByPhone: async () => ({ id: 'id' }),
|
||||
hasUserWithEmail: async (email: string) => email === 'a@a.com',
|
||||
findUserByEmail: async () => ({ id: 'id' }),
|
||||
findUserById: async () => findUserById(),
|
||||
updateUserById: async (...args: unknown[]) => updateUserById(...args),
|
||||
}));
|
||||
|
||||
const sendPasscode = jest.fn(async () => ({ dbEntry: { id: 'connectorIdValue' } }));
|
||||
|
@ -24,6 +42,11 @@ jest.mock('@/lib/passcode', () => ({
|
|||
},
|
||||
}));
|
||||
|
||||
const mockArgon2Verify = jest.fn(async (password: string) => password === mockPasswordEncrypted);
|
||||
jest.mock('hash-wasm', () => ({
|
||||
argon2Verify: async (password: string) => mockArgon2Verify(password),
|
||||
}));
|
||||
|
||||
const interactionResult = jest.fn(async () => 'redirectTo');
|
||||
const interactionDetails: jest.MockedFunction<() => Promise<unknown>> = jest.fn(async () => ({}));
|
||||
|
||||
|
@ -69,6 +92,8 @@ describe('session -> forgotPasswordRoutes', () => {
|
|||
|
||||
describe('POST /session/forgot-password/sms/verify-passcode', () => {
|
||||
it('assign result and redirect', async () => {
|
||||
const fakeTime = new Date();
|
||||
jest.useFakeTimers().setSystemTime(fakeTime);
|
||||
const response = await sessionRequest
|
||||
.post(`${forgotPasswordRoute}/sms/verify-passcode`)
|
||||
.send({ phone: '13000000000', code: '1234' });
|
||||
|
@ -80,12 +105,14 @@ describe('session -> forgotPasswordRoutes', () => {
|
|||
expect.objectContaining({
|
||||
login: { accountId: 'id' },
|
||||
forgotPassword: {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
expiresAt: expect.any(dayjs),
|
||||
expiresAt: dayjs(fakeTime)
|
||||
.add(forgotPasswordVerificationTimeout, 'second')
|
||||
.toISOString(),
|
||||
},
|
||||
}),
|
||||
expect.anything()
|
||||
);
|
||||
jest.useRealTimers();
|
||||
});
|
||||
it('throw error if phone number does not exist', async () => {
|
||||
const response = await sessionRequest
|
||||
|
@ -118,6 +145,8 @@ describe('session -> forgotPasswordRoutes', () => {
|
|||
|
||||
describe('POST /session/forgot-password/email/verify-passcode', () => {
|
||||
it('assign result and redirect', async () => {
|
||||
const fakeTime = new Date();
|
||||
jest.useFakeTimers().setSystemTime(fakeTime);
|
||||
const response = await sessionRequest
|
||||
.post(`${forgotPasswordRoute}/email/verify-passcode`)
|
||||
.send({ email: 'a@a.com', code: '1234' });
|
||||
|
@ -129,12 +158,14 @@ describe('session -> forgotPasswordRoutes', () => {
|
|||
expect.objectContaining({
|
||||
login: { accountId: 'id' },
|
||||
forgotPassword: {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
expiresAt: expect.any(dayjs),
|
||||
expiresAt: dayjs(fakeTime)
|
||||
.add(forgotPasswordVerificationTimeout, 'second')
|
||||
.toISOString(),
|
||||
},
|
||||
}),
|
||||
expect.anything()
|
||||
);
|
||||
jest.useRealTimers();
|
||||
});
|
||||
it('throw error if email does not exist', async () => {
|
||||
const response = await sessionRequest
|
||||
|
@ -149,4 +180,118 @@ describe('session -> forgotPasswordRoutes', () => {
|
|||
expect(response.statusCode).toEqual(400);
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /session/forgot-password/reset', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
it('assign result and redirect', async () => {
|
||||
interactionDetails.mockResolvedValueOnce({
|
||||
result: {
|
||||
login: { accountId: 'id' },
|
||||
forgotPassword: { expiresAt: dayjs().add(1, 'day').toISOString() },
|
||||
},
|
||||
});
|
||||
const response = await sessionRequest
|
||||
.post(`${forgotPasswordRoute}/reset`)
|
||||
.send({ password: mockPasswordEncrypted });
|
||||
expect(updateUserById).toBeCalledWith(
|
||||
'id',
|
||||
expect.objectContaining({
|
||||
passwordEncrypted: 'a1b2c3_user1',
|
||||
passwordEncryptionMethod: 'Argon2i',
|
||||
})
|
||||
);
|
||||
expect(response.statusCode).toEqual(204);
|
||||
});
|
||||
it('should throw when `accountId` is missing', async () => {
|
||||
interactionDetails.mockResolvedValueOnce({
|
||||
result: {
|
||||
forgotPassword: { expiresAt: dayjs().add(1, 'day').toISOString() },
|
||||
},
|
||||
});
|
||||
const response = await sessionRequest
|
||||
.post(`${forgotPasswordRoute}/reset`)
|
||||
.send({ password: mockPasswordEncrypted });
|
||||
expect(response).toHaveProperty('status', 404);
|
||||
expect(updateUserById).toBeCalledTimes(0);
|
||||
});
|
||||
it('should throw when `forgotPassword.expiresAt` is not string', async () => {
|
||||
interactionDetails.mockResolvedValueOnce({
|
||||
result: {
|
||||
login: { accountId: 'id' },
|
||||
forgotPassword: { expiresAt: 0 },
|
||||
},
|
||||
});
|
||||
const response = await sessionRequest
|
||||
.post(`${forgotPasswordRoute}/reset`)
|
||||
.send({ password: mockPasswordEncrypted });
|
||||
expect(response).toHaveProperty('status', 404);
|
||||
expect(updateUserById).toBeCalledTimes(0);
|
||||
});
|
||||
it('should throw when `expiresAt` is not a valid date string', async () => {
|
||||
interactionDetails.mockResolvedValueOnce({
|
||||
result: {
|
||||
login: { accountId: 'id' },
|
||||
forgotPassword: { expiresAt: 'invalid date string' },
|
||||
},
|
||||
});
|
||||
const response = await sessionRequest
|
||||
.post(`${forgotPasswordRoute}/reset`)
|
||||
.send({ password: mockPasswordEncrypted });
|
||||
expect(response).toHaveProperty('status', 401);
|
||||
expect(updateUserById).toBeCalledTimes(0);
|
||||
});
|
||||
it('should throw when verification expires', async () => {
|
||||
interactionDetails.mockResolvedValueOnce({
|
||||
result: {
|
||||
login: { accountId: 'id' },
|
||||
forgotPassword: { expiresAt: dayjs().subtract(1, 'day').toISOString() },
|
||||
},
|
||||
});
|
||||
const response = await sessionRequest
|
||||
.post(`${forgotPasswordRoute}/reset`)
|
||||
.send({ password: mockPasswordEncrypted });
|
||||
expect(response).toHaveProperty('status', 401);
|
||||
expect(updateUserById).toBeCalledTimes(0);
|
||||
});
|
||||
it('should throw when new password is the same as old one', async () => {
|
||||
interactionDetails.mockResolvedValueOnce({
|
||||
result: {
|
||||
login: { accountId: 'id' },
|
||||
forgotPassword: { expiresAt: dayjs().add(1, 'day').toISOString() },
|
||||
},
|
||||
});
|
||||
mockArgon2Verify.mockResolvedValueOnce(true);
|
||||
const response = await sessionRequest
|
||||
.post(`${forgotPasswordRoute}/reset`)
|
||||
.send({ password: mockPasswordEncrypted });
|
||||
expect(response).toHaveProperty('status', 400);
|
||||
expect(updateUserById).toBeCalledTimes(0);
|
||||
});
|
||||
it('should redirect when there was no old password', async () => {
|
||||
interactionDetails.mockResolvedValueOnce({
|
||||
result: {
|
||||
login: { accountId: 'id' },
|
||||
forgotPassword: { expiresAt: dayjs().add(1, 'day').toISOString() },
|
||||
},
|
||||
});
|
||||
findUserById.mockResolvedValueOnce({
|
||||
...mockUserWithPassword,
|
||||
passwordEncrypted: null,
|
||||
passwordEncryptionMethod: null,
|
||||
});
|
||||
const response = await sessionRequest
|
||||
.post(`${forgotPasswordRoute}/reset`)
|
||||
.send({ password: mockPasswordEncrypted });
|
||||
expect(updateUserById).toBeCalledWith(
|
||||
'id',
|
||||
expect.objectContaining({
|
||||
passwordEncrypted: 'a1b2c3_user1',
|
||||
passwordEncryptionMethod: 'Argon2i',
|
||||
})
|
||||
);
|
||||
expect(response.statusCode).toEqual(204);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,18 +1,22 @@
|
|||
import { emailRegEx, phoneRegEx } from '@logto/core-kit';
|
||||
import { emailRegEx, passwordRegEx, phoneRegEx } from '@logto/core-kit';
|
||||
import { PasscodeType } from '@logto/schemas';
|
||||
import dayjs from 'dayjs';
|
||||
import { argon2Verify } from 'hash-wasm';
|
||||
import { Provider } from 'oidc-provider';
|
||||
import { z } from 'zod';
|
||||
|
||||
import RequestError from '@/errors/RequestError';
|
||||
import { createPasscode, sendPasscode, verifyPasscode } from '@/lib/passcode';
|
||||
import { assignInteractionResults } from '@/lib/session';
|
||||
import { encryptUserPassword } from '@/lib/user';
|
||||
import koaGuard from '@/middleware/koa-guard';
|
||||
import {
|
||||
findUserByEmail,
|
||||
findUserById,
|
||||
findUserByPhone,
|
||||
hasUserWithEmail,
|
||||
hasUserWithPhone,
|
||||
updateUserById,
|
||||
} from '@/queries/user';
|
||||
import assertThat from '@/utils/assert-that';
|
||||
|
||||
|
@ -22,6 +26,10 @@ import { getRoutePrefix } from './utils';
|
|||
|
||||
export const forgotPasswordRoute = getRoutePrefix('forgot-password');
|
||||
|
||||
const forgotPasswordVerificationGuard = z.object({
|
||||
forgotPassword: z.object({ expiresAt: z.string() }),
|
||||
});
|
||||
|
||||
export default function forgotPasswordRoutes<T extends AnonymousRouter>(
|
||||
router: T,
|
||||
provider: Provider
|
||||
|
@ -65,7 +73,7 @@ export default function forgotPasswordRoutes<T extends AnonymousRouter>(
|
|||
await assignInteractionResults(ctx, provider, {
|
||||
login: { accountId: id },
|
||||
forgotPassword: {
|
||||
expiresAt: dayjs().add(forgotPasswordVerificationTimeout, 'second'),
|
||||
expiresAt: dayjs().add(forgotPasswordVerificationTimeout, 'second').toISOString(),
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -110,11 +118,56 @@ export default function forgotPasswordRoutes<T extends AnonymousRouter>(
|
|||
await assignInteractionResults(ctx, provider, {
|
||||
login: { accountId: id },
|
||||
forgotPassword: {
|
||||
expiresAt: dayjs().add(forgotPasswordVerificationTimeout, 'second'),
|
||||
expiresAt: dayjs().add(forgotPasswordVerificationTimeout, 'second').toISOString(),
|
||||
},
|
||||
});
|
||||
|
||||
return next();
|
||||
}
|
||||
);
|
||||
|
||||
router.post(
|
||||
`${forgotPasswordRoute}/reset`,
|
||||
koaGuard({ body: z.object({ password: z.string().regex(passwordRegEx) }) }),
|
||||
async (ctx, next) => {
|
||||
const { result } = await provider.interactionDetails(ctx.req, ctx.res);
|
||||
const { password } = ctx.guard.body;
|
||||
const forgotPasswordVerificationResult = forgotPasswordVerificationGuard.safeParse(result);
|
||||
|
||||
assertThat(
|
||||
result?.login?.accountId && forgotPasswordVerificationResult.success,
|
||||
new RequestError({ code: 'session.forgot_password_session_not_found', status: 404 })
|
||||
);
|
||||
|
||||
const {
|
||||
login: { accountId: id },
|
||||
} = result;
|
||||
const {
|
||||
forgotPassword: { expiresAt },
|
||||
} = forgotPasswordVerificationResult.data;
|
||||
|
||||
assertThat(
|
||||
dayjs(expiresAt).isValid() && dayjs(expiresAt).isAfter(dayjs()),
|
||||
new RequestError({ code: 'session.forgot_password_verification_expired', status: 401 })
|
||||
);
|
||||
|
||||
const { passwordEncrypted: oldPasswordEncrypted } = await findUserById(id);
|
||||
|
||||
assertThat(
|
||||
!oldPasswordEncrypted ||
|
||||
(oldPasswordEncrypted && !(await argon2Verify({ password, hash: oldPasswordEncrypted }))),
|
||||
new RequestError({ code: 'user.same_password', status: 400 })
|
||||
);
|
||||
|
||||
const { passwordEncrypted, passwordEncryptionMethod } = await encryptUserPassword(password);
|
||||
|
||||
const type = 'ForgotPasswordReset';
|
||||
ctx.log(type, { userId: id });
|
||||
|
||||
await updateUserById(id, { passwordEncrypted, passwordEncryptionMethod });
|
||||
ctx.status = 204;
|
||||
|
||||
return next();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
|
@ -42,6 +42,7 @@ const errors = {
|
|||
identity_exists: 'The social account has been registered.',
|
||||
invalid_role_names: 'role names ({{roleNames}}) are not valid',
|
||||
cannot_delete_self: 'You cannot delete yourself.',
|
||||
same_password: 'Your new password can not be the same as current password.',
|
||||
},
|
||||
password: {
|
||||
unsupported_encryption_method: 'The encryption method {{name}} is not supported.',
|
||||
|
@ -55,6 +56,10 @@ const errors = {
|
|||
insufficient_info: 'Insufficient sign-in info.',
|
||||
connector_id_mismatch: 'The connectorId is mismatched with session record.',
|
||||
connector_session_not_found: 'Connector session not found. Please go back and sign in again.',
|
||||
forgot_password_session_not_found:
|
||||
'Forgot password session not found. Please go back and verify.',
|
||||
forgot_password_verification_expired:
|
||||
'Forgot password verification has expired. Please go back and verify again.',
|
||||
unauthorized: 'Please sign in first.',
|
||||
unsupported_prompt_name: 'Unsupported prompt name.',
|
||||
},
|
||||
|
|
|
@ -42,7 +42,8 @@ const errors = {
|
|||
identity_not_exists: "Le compte social n'a pas encore été enregistré.",
|
||||
identity_exists: 'Le compte social a été enregistré.',
|
||||
invalid_role_names: 'les noms de rôles ({{roleNames}}) ne sont pas valides',
|
||||
cannot_delete_self: 'You cannot delete yourself.',
|
||||
cannot_delete_self: 'You cannot delete yourself.', // UNTRANSLATED
|
||||
same_password: 'Your new password can not be the same as current password.', // UNTRANSLATED
|
||||
},
|
||||
password: {
|
||||
unsupported_encryption_method: "La méthode de cryptage {{name}} n'est pas prise en charge.",
|
||||
|
@ -60,6 +61,10 @@ const errors = {
|
|||
connector_id_mismatch: "Le connectorId ne correspond pas à l'enregistrement de la session.",
|
||||
connector_session_not_found:
|
||||
"La session du connecteur n'a pas été trouvée. Veuillez revenir en arrière et vous connecter à nouveau.",
|
||||
forgot_password_session_not_found:
|
||||
'Forgot password session not found. Please go back and verify.', // UNTRANSLATED
|
||||
forgot_password_verification_expired:
|
||||
'Forgot password verification has expired. Please go back and verify again.', // UNTRANSLATED
|
||||
unauthorized: "Veuillez vous enregistrer d'abord.",
|
||||
unsupported_prompt_name: "Nom d'invite non supporté.",
|
||||
},
|
||||
|
|
|
@ -40,7 +40,8 @@ const errors = {
|
|||
identity_not_exists: '소셜 계정이 아직 등록되지 않았어요.',
|
||||
identity_exists: '소셜 계정이 이미 등록되있어요.',
|
||||
invalid_role_names: '직책 명({{roleNames}})이 유효하지 않아요.',
|
||||
cannot_delete_self: 'You cannot delete yourself.',
|
||||
cannot_delete_self: 'You cannot delete yourself.', // UNTRANSLATED
|
||||
same_password: 'Your new password can not be the same as current password.', // UNTRANSLATED
|
||||
},
|
||||
password: {
|
||||
unsupported_encryption_method: '{{name}} 암호화 방법을 지원하지 않아요.',
|
||||
|
@ -54,6 +55,10 @@ const errors = {
|
|||
insufficient_info: '로그인 정보가 충분하지 않아요.',
|
||||
connector_id_mismatch: '연동 ID가 세션 정보와 일치하지 않아요.',
|
||||
connector_session_not_found: '연동 세션을 찾을 수 없어요. 다시 로그인해주세요.',
|
||||
forgot_password_session_not_found:
|
||||
'Forgot password session not found. Please go back and verify.', // UNTRANSLATED
|
||||
forgot_password_verification_expired:
|
||||
'Forgot password verification has expired. Please go back and verify again.', // UNTRANSLATED
|
||||
unauthorized: '로그인을 먼저 해주세요.',
|
||||
unsupported_prompt_name: '지원하지 않는 Prompt 이름이예요.',
|
||||
},
|
||||
|
|
|
@ -41,6 +41,7 @@ const errors = {
|
|||
identity_exists: 'A conta social foi registada.',
|
||||
invalid_role_names: '({{roleNames}}) não são válidos',
|
||||
cannot_delete_self: 'Não se pode remover a si mesmo.',
|
||||
same_password: 'Your new password can not be the same as current password.', // UNTRANSLATED
|
||||
},
|
||||
password: {
|
||||
unsupported_encryption_method: 'O método de enncriptação {{name}} não é suportado.',
|
||||
|
@ -56,6 +57,10 @@ const errors = {
|
|||
connector_id_mismatch: 'O connectorId não corresponde ao registado na sessão.',
|
||||
connector_session_not_found:
|
||||
'Sessão do conector não encontrada. Por favor, volte e faça login novamente.',
|
||||
forgot_password_session_not_found:
|
||||
'Forgot password session not found. Please go back and verify.', // UNTRANSLATED
|
||||
forgot_password_verification_expired:
|
||||
'Forgot password verification has expired. Please go back and verify again.', // UNTRANSLATED
|
||||
unauthorized: 'Faça login primeiro.',
|
||||
unsupported_prompt_name: 'Nome de prompt não suportado.',
|
||||
},
|
||||
|
|
|
@ -41,7 +41,8 @@ const errors = {
|
|||
identity_not_exists: 'Sosyal platform hesabı henüz kaydedilmedi.',
|
||||
identity_exists: 'Sosyal platform hesabı kaydedildi.',
|
||||
invalid_role_names: '({{roleNames}}) rol adları geçerli değil.',
|
||||
cannot_delete_self: 'You cannot delete yourself.',
|
||||
cannot_delete_self: 'You cannot delete yourself.', // UNTRANSLATED
|
||||
same_password: 'Your new password can not be the same as current password.', // UNTRANSLATED
|
||||
},
|
||||
password: {
|
||||
unsupported_encryption_method: '{{name}} şifreleme metodu desteklenmiyor.',
|
||||
|
@ -56,6 +57,10 @@ const errors = {
|
|||
connector_id_mismatch: 'connectorId, oturum kaydı ile eşleşmiyor.',
|
||||
connector_session_not_found:
|
||||
'Bağlayıcı oturum bulunamadı. Lütfen geri dönüp tekrardan giriş yapınız.',
|
||||
forgot_password_session_not_found:
|
||||
'Forgot password session not found. Please go back and verify.', // UNTRANSLATED
|
||||
forgot_password_verification_expired:
|
||||
'Forgot password verification has expired. Please go back and verify again.', // UNTRANSLATED
|
||||
unauthorized: 'Lütfen önce oturum açın.',
|
||||
unsupported_prompt_name: 'Desteklenmeyen prompt adı.',
|
||||
},
|
||||
|
|
|
@ -41,6 +41,7 @@ const errors = {
|
|||
identity_exists: '该社交帐号已被注册',
|
||||
invalid_role_names: '角色名称({{roleNames}})无效',
|
||||
cannot_delete_self: '你无法删除自己',
|
||||
same_password: '新设置的密码不可与当前密码相同',
|
||||
},
|
||||
password: {
|
||||
unsupported_encryption_method: '不支持的加密方法 {{name}}',
|
||||
|
@ -54,6 +55,8 @@ const errors = {
|
|||
insufficient_info: '登录信息缺失,请检查你的输入。',
|
||||
connector_id_mismatch: '传入的连接器 ID 与 session 中保存的记录不一致',
|
||||
connector_session_not_found: '无法找到连接器登录信息,请尝试重新登录。',
|
||||
forgot_password_session_not_found: '无法找到忘记密码验证信息,请尝试重新验证。',
|
||||
forgot_password_verification_expired: '忘记密码验证已过期,请尝试重新验证。',
|
||||
unauthorized: '请先登录',
|
||||
unsupported_prompt_name: '不支持的 prompt name',
|
||||
},
|
||||
|
|
|
@ -118,6 +118,10 @@ type ForgotPasswordEmailLogPayload = ArbitraryLogPayload & {
|
|||
userId?: string;
|
||||
};
|
||||
|
||||
type ForgotPasswordResetLogPayload = ArbitraryLogPayload & {
|
||||
userId?: string;
|
||||
};
|
||||
|
||||
export enum TokenType {
|
||||
AccessToken = 'AccessToken',
|
||||
RefreshToken = 'RefreshToken',
|
||||
|
@ -157,6 +161,7 @@ export type LogPayloads = {
|
|||
ForgotPasswordSms: ForgotPasswordSmsLogPayload;
|
||||
ForgotPasswordEmailSendPasscode: ForgotPasswordEmailSendPasscodeLogPayload;
|
||||
ForgotPasswordEmail: ForgotPasswordEmailLogPayload;
|
||||
ForgotPasswordReset: ForgotPasswordResetLogPayload;
|
||||
CodeExchangeToken: ExchangeTokenLogPayload;
|
||||
RefreshTokenExchangeToken: ExchangeTokenLogPayload;
|
||||
RevokeToken: RevokeTokenLogPayload;
|
||||
|
|
Loading…
Reference in a new issue