2022-11-16 14:35:38 +08:00
|
|
|
import { emailRegEx, passwordRegEx, phoneRegEx, usernameRegEx } from '@logto/core-kit';
|
2022-11-16 14:57:41 +08:00
|
|
|
import { arbitraryObjectGuard, userInfoSelectFields } from '@logto/schemas';
|
2022-11-11 14:29:37 +08:00
|
|
|
import { argon2Verify } from 'hash-wasm';
|
2022-11-11 10:50:20 +08:00
|
|
|
import pick from 'lodash.pick';
|
|
|
|
import type { Provider } from 'oidc-provider';
|
|
|
|
import { object, string } from 'zod';
|
|
|
|
|
|
|
|
import RequestError from '@/errors/RequestError';
|
|
|
|
import { checkSessionHealth } from '@/lib/session';
|
2022-11-11 14:29:37 +08:00
|
|
|
import { encryptUserPassword } from '@/lib/user';
|
2022-11-11 10:50:20 +08:00
|
|
|
import koaGuard from '@/middleware/koa-guard';
|
|
|
|
import { findUserById, updateUserById } from '@/queries/user';
|
|
|
|
import assertThat from '@/utils/assert-that';
|
|
|
|
|
|
|
|
import type { AnonymousRouter } from '../types';
|
|
|
|
import { verificationTimeout } from './consts';
|
|
|
|
import { checkSignUpIdentifierCollision } from './utils';
|
|
|
|
|
|
|
|
export const profileRoute = '/session/profile';
|
|
|
|
|
|
|
|
export default function profileRoutes<T extends AnonymousRouter>(router: T, provider: Provider) {
|
|
|
|
router.get(profileRoute, async (ctx, next) => {
|
2022-11-16 14:57:41 +08:00
|
|
|
const { accountId: userId } = await provider.Session.get(ctx);
|
2022-11-11 10:50:20 +08:00
|
|
|
|
2022-11-16 14:57:41 +08:00
|
|
|
assertThat(userId, new RequestError({ code: 'auth.unauthorized', status: 401 }));
|
2022-11-11 10:50:20 +08:00
|
|
|
|
2022-11-16 14:57:41 +08:00
|
|
|
const user = await findUserById(userId);
|
2022-11-11 10:50:20 +08:00
|
|
|
|
|
|
|
ctx.body = pick(user, ...userInfoSelectFields);
|
|
|
|
|
|
|
|
return next();
|
|
|
|
});
|
|
|
|
|
2022-11-16 14:57:41 +08:00
|
|
|
router.patch(
|
|
|
|
profileRoute,
|
|
|
|
koaGuard({
|
|
|
|
body: object({
|
|
|
|
name: string().nullable().optional(),
|
|
|
|
avatar: string().nullable().optional(),
|
|
|
|
customData: arbitraryObjectGuard.optional(),
|
|
|
|
}),
|
|
|
|
}),
|
|
|
|
async (ctx, next) => {
|
|
|
|
const { accountId: userId } = await provider.Session.get(ctx);
|
|
|
|
|
|
|
|
assertThat(userId, new RequestError({ code: 'auth.unauthorized', status: 401 }));
|
|
|
|
|
|
|
|
const { name, avatar, customData } = ctx.guard.body;
|
|
|
|
|
|
|
|
await updateUserById(userId, { name, avatar, customData });
|
|
|
|
|
|
|
|
ctx.status = 204;
|
|
|
|
|
|
|
|
return next();
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
2022-11-11 10:50:20 +08:00
|
|
|
router.patch(
|
|
|
|
`${profileRoute}/username`,
|
|
|
|
koaGuard({
|
|
|
|
body: object({ username: string().regex(usernameRegEx) }),
|
|
|
|
}),
|
|
|
|
async (ctx, next) => {
|
|
|
|
const userId = await checkSessionHealth(ctx, provider, verificationTimeout);
|
|
|
|
|
2022-11-16 14:57:41 +08:00
|
|
|
assertThat(userId, new RequestError({ code: 'auth.unauthorized', status: 401 }));
|
2022-11-11 10:50:20 +08:00
|
|
|
|
|
|
|
const { username } = ctx.guard.body;
|
|
|
|
|
|
|
|
await checkSignUpIdentifierCollision({ username }, userId);
|
|
|
|
|
|
|
|
const user = await updateUserById(userId, { username }, 'replace');
|
|
|
|
|
|
|
|
ctx.body = pick(user, ...userInfoSelectFields);
|
|
|
|
|
|
|
|
return next();
|
|
|
|
}
|
|
|
|
);
|
2022-11-11 14:29:37 +08:00
|
|
|
|
2022-11-16 13:32:16 +08:00
|
|
|
router.patch(
|
2022-11-11 14:29:37 +08:00
|
|
|
`${profileRoute}/password`,
|
|
|
|
koaGuard({
|
|
|
|
body: object({ password: string().regex(passwordRegEx) }),
|
|
|
|
}),
|
|
|
|
async (ctx, next) => {
|
|
|
|
const userId = await checkSessionHealth(ctx, provider, verificationTimeout);
|
|
|
|
|
2022-11-16 14:57:41 +08:00
|
|
|
assertThat(userId, new RequestError({ code: 'auth.unauthorized', status: 401 }));
|
2022-11-11 14:29:37 +08:00
|
|
|
|
|
|
|
const { password } = ctx.guard.body;
|
|
|
|
const { passwordEncrypted: oldPasswordEncrypted } = await findUserById(userId);
|
|
|
|
|
|
|
|
assertThat(
|
|
|
|
!oldPasswordEncrypted || !(await argon2Verify({ password, hash: oldPasswordEncrypted })),
|
|
|
|
new RequestError({ code: 'user.same_password', status: 422 })
|
|
|
|
);
|
|
|
|
|
|
|
|
const { passwordEncrypted, passwordEncryptionMethod } = await encryptUserPassword(password);
|
|
|
|
|
|
|
|
await updateUserById(userId, { passwordEncrypted, passwordEncryptionMethod });
|
|
|
|
|
|
|
|
ctx.status = 204;
|
|
|
|
|
|
|
|
return next();
|
|
|
|
}
|
|
|
|
);
|
2022-11-16 13:32:16 +08:00
|
|
|
|
|
|
|
router.patch(
|
|
|
|
`${profileRoute}/email`,
|
|
|
|
koaGuard({
|
|
|
|
body: object({ primaryEmail: string().regex(emailRegEx) }),
|
|
|
|
}),
|
|
|
|
async (ctx, next) => {
|
|
|
|
const userId = await checkSessionHealth(ctx, provider, verificationTimeout);
|
|
|
|
|
2022-11-16 14:57:41 +08:00
|
|
|
assertThat(userId, new RequestError({ code: 'auth.unauthorized', status: 401 }));
|
2022-11-16 13:32:16 +08:00
|
|
|
|
|
|
|
const { primaryEmail } = ctx.guard.body;
|
|
|
|
|
|
|
|
await checkSignUpIdentifierCollision({ primaryEmail });
|
|
|
|
await updateUserById(userId, { primaryEmail });
|
|
|
|
|
|
|
|
ctx.status = 204;
|
|
|
|
|
|
|
|
return next();
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
|
|
|
router.delete(`${profileRoute}/email`, async (ctx, next) => {
|
|
|
|
const userId = await checkSessionHealth(ctx, provider, verificationTimeout);
|
|
|
|
|
2022-11-16 14:57:41 +08:00
|
|
|
assertThat(userId, new RequestError({ code: 'auth.unauthorized', status: 401 }));
|
2022-11-16 13:32:16 +08:00
|
|
|
|
|
|
|
const { primaryEmail } = await findUserById(userId);
|
|
|
|
|
|
|
|
assertThat(primaryEmail, new RequestError({ code: 'user.email_not_exists', status: 422 }));
|
|
|
|
|
|
|
|
await updateUserById(userId, { primaryEmail: null });
|
|
|
|
|
|
|
|
ctx.status = 204;
|
|
|
|
|
|
|
|
return next();
|
|
|
|
});
|
2022-11-16 14:35:38 +08:00
|
|
|
|
|
|
|
router.patch(
|
|
|
|
`${profileRoute}/phone`,
|
|
|
|
koaGuard({
|
|
|
|
body: object({ primaryPhone: string().regex(phoneRegEx) }),
|
|
|
|
}),
|
|
|
|
async (ctx, next) => {
|
|
|
|
const userId = await checkSessionHealth(ctx, provider, verificationTimeout);
|
|
|
|
|
2022-11-16 14:57:41 +08:00
|
|
|
assertThat(userId, new RequestError({ code: 'auth.unauthorized', status: 401 }));
|
2022-11-16 14:35:38 +08:00
|
|
|
|
|
|
|
const { primaryPhone } = ctx.guard.body;
|
|
|
|
|
|
|
|
await checkSignUpIdentifierCollision({ primaryPhone });
|
|
|
|
await updateUserById(userId, { primaryPhone });
|
|
|
|
|
|
|
|
ctx.status = 204;
|
|
|
|
|
|
|
|
return next();
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
|
|
|
router.delete(`${profileRoute}/phone`, async (ctx, next) => {
|
|
|
|
const userId = await checkSessionHealth(ctx, provider, verificationTimeout);
|
|
|
|
|
2022-11-16 14:57:41 +08:00
|
|
|
assertThat(userId, new RequestError({ code: 'auth.unauthorized', status: 401 }));
|
2022-11-16 14:35:38 +08:00
|
|
|
|
|
|
|
const { primaryPhone } = await findUserById(userId);
|
|
|
|
|
|
|
|
assertThat(primaryPhone, new RequestError({ code: 'user.phone_not_exists', status: 422 }));
|
|
|
|
|
|
|
|
await updateUserById(userId, { primaryPhone: null });
|
|
|
|
|
|
|
|
ctx.status = 204;
|
|
|
|
|
|
|
|
return next();
|
|
|
|
});
|
2022-11-11 10:50:20 +08:00
|
|
|
}
|