Skip to content

Commit

Permalink
fix(core,schemas): check email verification status in me api (#6507)
Browse files Browse the repository at this point in the history
  • Loading branch information
charIeszhao authored Aug 22, 2024
1 parent 8edbff2 commit 14d25ba
Show file tree
Hide file tree
Showing 8 changed files with 74 additions and 41 deletions.
32 changes: 16 additions & 16 deletions packages/console/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -116,23 +116,23 @@ function Providers() {
<AppThemeProvider>
<Helmet titleTemplate={`%s - ${mainTitle}`} defaultTitle={mainTitle} />
<Toast />
<ErrorBoundary>
<LogtoErrorBoundary>
{/**
* If it's not Cloud (OSS), render the tenant app container directly since only default tenant is available;
* if it's Cloud, render the tenant app container only when a tenant ID is available (in a tenant context).
*/}
{!isCloud || currentTenantId ? (
<AppDataProvider>
<AppConfirmModalProvider>
<AppConfirmModalProvider>
<ErrorBoundary>
<LogtoErrorBoundary>
{/**
* If it's not Cloud (OSS), render the tenant app container directly since only default tenant is available;
* if it's Cloud, render the tenant app container only when a tenant ID is available (in a tenant context).
*/}
{!isCloud || currentTenantId ? (
<AppDataProvider>
<AppRoutes />
</AppConfirmModalProvider>
</AppDataProvider>
) : (
<CloudAppRoutes />
)}
</LogtoErrorBoundary>
</ErrorBoundary>
</AppDataProvider>
) : (
<CloudAppRoutes />
)}
</LogtoErrorBoundary>
</ErrorBoundary>
</AppConfirmModalProvider>
</AppThemeProvider>
</LogtoProvider>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 } });
Expand Down
19 changes: 16 additions & 3 deletions packages/core/src/libraries/verification-status.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -13,25 +14,37 @@ 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');

// 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,
};
};
3 changes: 1 addition & 2 deletions packages/core/src/queries/verification-status.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down
18 changes: 11 additions & 7 deletions packages/core/src/routes-me/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand All @@ -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);
Expand Down Expand Up @@ -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;

Expand All @@ -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 });
Expand Down
22 changes: 10 additions & 12 deletions packages/core/src/routes-me/verification-code.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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 }));
const user = await findUserById(userId);
assertThat(!user.isSuspended, new RequestError({ code: 'user.suspended', status: 401 }));

await verifyPasscode(undefined, codeType, verificationCode, identifier);

await createVerificationStatus(userId);
}
await createVerificationStatus(userId, identifier.email);

ctx.status = 204;

Expand Down
Original file line number Diff line number Diff line change
@@ -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;
1 change: 1 addition & 0 deletions packages/schemas/tables/verification_statuses.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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)
);

Expand Down

0 comments on commit 14d25ba

Please sign in to comment.