mirror of
https://github.com/logto-io/logto.git
synced 2025-03-10 22:22:45 -05:00
fix(core,schemas): check email verification status in me api (#6507)
This commit is contained in:
parent
8edbff2f8a
commit
14d25bab1f
8 changed files with 74 additions and 41 deletions
|
@ -116,6 +116,7 @@ function Providers() {
|
|||
<AppThemeProvider>
|
||||
<Helmet titleTemplate={`%s - ${mainTitle}`} defaultTitle={mainTitle} />
|
||||
<Toast />
|
||||
<AppConfirmModalProvider>
|
||||
<ErrorBoundary>
|
||||
<LogtoErrorBoundary>
|
||||
{/**
|
||||
|
@ -124,15 +125,14 @@ function Providers() {
|
|||
*/}
|
||||
{!isCloud || currentTenantId ? (
|
||||
<AppDataProvider>
|
||||
<AppConfirmModalProvider>
|
||||
<AppRoutes />
|
||||
</AppConfirmModalProvider>
|
||||
</AppDataProvider>
|
||||
) : (
|
||||
<CloudAppRoutes />
|
||||
)}
|
||||
</LogtoErrorBoundary>
|
||||
</ErrorBoundary>
|
||||
</AppConfirmModalProvider>
|
||||
</AppThemeProvider>
|
||||
</LogtoProvider>
|
||||
);
|
||||
|
|
|
@ -61,7 +61,7 @@ function VerificationCodeModal() {
|
|||
}
|
||||
|
||||
try {
|
||||
await api.post(`me/verification-codes/verify`, { json: { verificationCode, email, action } });
|
||||
await api.post(`me/verification-codes/verify`, { json: { verificationCode, email } });
|
||||
|
||||
if (action === 'changeEmail') {
|
||||
await api.patch('me', { json: { primaryEmail: email } });
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { generateStandardId } from '@logto/shared';
|
||||
import { type Nullable } from '@silverhand/essentials';
|
||||
|
||||
import RequestError from '#src/errors/RequestError/index.js';
|
||||
import type Queries from '#src/tenants/Queries.js';
|
||||
|
@ -13,17 +14,21 @@ export const createVerificationStatusLibrary = (queries: Queries) => {
|
|||
deleteVerificationStatusesByUserId,
|
||||
} = queries.verificationStatuses;
|
||||
|
||||
const createVerificationStatus = async (userId: string) => {
|
||||
const createVerificationStatus = async (userId: string, identifier: Nullable<string>) => {
|
||||
// Remove existing verification statuses for current user.
|
||||
await deleteVerificationStatusesByUserId(userId);
|
||||
|
||||
return insertVerificationStatus({
|
||||
id: generateStandardId(),
|
||||
userId,
|
||||
verifiedIdentifier: identifier,
|
||||
});
|
||||
};
|
||||
|
||||
const checkVerificationStatus = async (userId: string): Promise<void> => {
|
||||
const checkVerificationStatus = async (
|
||||
userId: string,
|
||||
identifier: Nullable<string>
|
||||
): Promise<void> => {
|
||||
const verificationStatus = await findVerificationStatusByUserId(userId);
|
||||
|
||||
assertThat(verificationStatus, 'session.verification_session_not_found');
|
||||
|
@ -31,7 +36,15 @@ export const createVerificationStatusLibrary = (queries: Queries) => {
|
|||
// The user verification status is considered valid if the user is verified within 10 minutes.
|
||||
const isValid = Date.now() - verificationStatus.createdAt < verificationTimeout;
|
||||
assertThat(isValid, new RequestError({ code: 'session.verification_failed', status: 422 }));
|
||||
|
||||
assertThat(
|
||||
verificationStatus.verifiedIdentifier === identifier,
|
||||
new RequestError({ code: 'session.verification_failed', status: 422 })
|
||||
);
|
||||
};
|
||||
|
||||
return { createVerificationStatus, checkVerificationStatus };
|
||||
return {
|
||||
createVerificationStatus,
|
||||
checkVerificationStatus,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import type { VerificationStatus } from '@logto/schemas';
|
||||
import { VerificationStatuses } from '@logto/schemas';
|
||||
import { type VerificationStatus, VerificationStatuses } from '@logto/schemas';
|
||||
import type { CommonQueryMethods } from '@silverhand/slonik';
|
||||
import { sql } from '@silverhand/slonik';
|
||||
|
||||
|
|
|
@ -44,8 +44,8 @@ export default function userRoutes<T extends AuthedMeRouter>(
|
|||
'/',
|
||||
koaGuard({
|
||||
body: object({
|
||||
username: string().regex(usernameRegEx),
|
||||
primaryEmail: string().regex(emailRegEx),
|
||||
username: string().regex(usernameRegEx), // OSS only
|
||||
primaryEmail: string().regex(emailRegEx), // Cloud only
|
||||
name: string().or(literal('')).nullable(),
|
||||
avatar: string().url().or(literal('')).nullable(),
|
||||
}).partial(),
|
||||
|
@ -57,6 +57,12 @@ export default function userRoutes<T extends AuthedMeRouter>(
|
|||
const user = await findUserById(userId);
|
||||
assertThat(!user.isSuspended, new RequestError({ code: 'user.suspended', status: 401 }));
|
||||
|
||||
const { primaryEmail } = body;
|
||||
if (primaryEmail) {
|
||||
// Check if user has verified email within 10 minutes.
|
||||
await checkVerificationStatus(userId, primaryEmail);
|
||||
}
|
||||
|
||||
await checkIdentifierCollision(body, userId);
|
||||
|
||||
const updatedUser = await updateUserById(userId, body);
|
||||
|
@ -113,7 +119,7 @@ export default function userRoutes<T extends AuthedMeRouter>(
|
|||
|
||||
await verifyUserPassword(user, password);
|
||||
|
||||
await createVerificationStatus(userId);
|
||||
await createVerificationStatus(userId, null);
|
||||
|
||||
ctx.status = 204;
|
||||
|
||||
|
@ -128,13 +134,11 @@ export default function userRoutes<T extends AuthedMeRouter>(
|
|||
const { id: userId } = ctx.auth;
|
||||
const { password } = ctx.guard.body;
|
||||
|
||||
const { isSuspended, passwordEncrypted: oldPasswordEncrypted } = await findUserById(userId);
|
||||
const { isSuspended } = await findUserById(userId);
|
||||
|
||||
assertThat(!isSuspended, new RequestError({ code: 'user.suspended', status: 401 }));
|
||||
|
||||
if (oldPasswordEncrypted) {
|
||||
await checkVerificationStatus(userId);
|
||||
}
|
||||
await checkVerificationStatus(userId, null);
|
||||
|
||||
const { passwordEncrypted, passwordEncryptionMethod } = await encryptUserPassword(password);
|
||||
await updateUserById(userId, { passwordEncrypted, passwordEncryptionMethod });
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
import { TemplateType } from '@logto/connector-kit';
|
||||
import { emailRegEx } from '@logto/core-kit';
|
||||
import { literal, object, string, union } from 'zod';
|
||||
import { object, string } from 'zod';
|
||||
|
||||
import RequestError from '#src/errors/RequestError/index.js';
|
||||
import koaGuard from '#src/middleware/koa-guard.js';
|
||||
import type { RouterInitArgs } from '#src/routes/types.js';
|
||||
import assertThat from '#src/utils/assert-that.js';
|
||||
|
||||
import RequestError from '../errors/RequestError/index.js';
|
||||
import assertThat from '../utils/assert-that.js';
|
||||
|
||||
import type { AuthedMeRouter } from './types.js';
|
||||
|
||||
|
@ -44,21 +45,18 @@ export default function verificationCodeRoutes<T extends AuthedMeRouter>(
|
|||
body: object({
|
||||
email: string().regex(emailRegEx),
|
||||
verificationCode: string().min(1),
|
||||
action: union([literal('changeEmail'), literal('changePassword')]),
|
||||
}),
|
||||
}),
|
||||
async (ctx, next) => {
|
||||
const { id: userId } = ctx.auth;
|
||||
const { verificationCode, action, ...identifier } = ctx.guard.body;
|
||||
await verifyPasscode(undefined, codeType, verificationCode, identifier);
|
||||
const { verificationCode, ...identifier } = ctx.guard.body;
|
||||
|
||||
if (action === 'changePassword') {
|
||||
// Store password verification status
|
||||
const user = await findUserById(userId);
|
||||
assertThat(!user.isSuspended, new RequestError({ code: 'user.suspended', status: 401 }));
|
||||
|
||||
await createVerificationStatus(userId);
|
||||
}
|
||||
await verifyPasscode(undefined, codeType, verificationCode, identifier);
|
||||
|
||||
await createVerificationStatus(userId, identifier.email);
|
||||
|
||||
ctx.status = 204;
|
||||
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
import { sql } from '@silverhand/slonik';
|
||||
|
||||
import type { AlterationScript } from '../lib/types/alteration.js';
|
||||
|
||||
const alteration: AlterationScript = {
|
||||
up: async (pool) => {
|
||||
await pool.query(sql`
|
||||
alter table verification_statuses add column verified_identifier varchar(255);
|
||||
`);
|
||||
},
|
||||
down: async (pool) => {
|
||||
await pool.query(sql`
|
||||
alter table verification_statuses drop column verified_identifier;
|
||||
`);
|
||||
},
|
||||
};
|
||||
|
||||
export default alteration;
|
|
@ -5,6 +5,7 @@ create table verification_statuses (
|
|||
user_id varchar(21) not null
|
||||
references users (id) on update cascade on delete cascade,
|
||||
created_at timestamptz not null default(now()),
|
||||
verified_identifier varchar(255),
|
||||
primary key (id)
|
||||
);
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue