0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2024-12-30 20:33:54 -05:00
logto/packages/core/src/routes/profile.ts

246 lines
7.1 KiB
TypeScript

import { emailRegEx, passwordRegEx, phoneRegEx, usernameRegEx } from '@logto/core-kit';
import { arbitraryObjectGuard, userInfoSelectFields } from '@logto/schemas';
import { has } from '@silverhand/essentials';
import { argon2Verify } from 'hash-wasm';
import pick from 'lodash.pick';
import type { Provider } from 'oidc-provider';
import { object, string, unknown } from 'zod';
import { getLogtoConnectorById } from '#src/connectors/index.js';
import RequestError from '#src/errors/RequestError/index.js';
import { checkSessionHealth } from '#src/libraries/session.js';
import { getUserInfoByAuthCode } from '#src/libraries/social.js';
import { checkIdentifierCollision, encryptUserPassword } from '#src/libraries/user.js';
import koaGuard from '#src/middleware/koa-guard.js';
import { deleteUserIdentity, findUserById, updateUserById } from '#src/queries/user.js';
import assertThat from '#src/utils/assert-that.js';
import { verificationTimeout } from './consts.js';
import type { AnonymousRouter } from './types.js';
export const profileRoute = '/profile';
export default function profileRoutes<T extends AnonymousRouter>(router: T, provider: Provider) {
router.get(profileRoute, async (ctx, next) => {
const { accountId: userId } = await provider.Session.get(ctx);
assertThat(userId, new RequestError({ code: 'auth.unauthorized', status: 401 }));
const user = await findUserById(userId);
ctx.body = {
...pick(user, ...userInfoSelectFields),
hasPasswordSet: Boolean(user.passwordEncrypted),
};
ctx.status = 200;
return next();
});
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();
}
);
router.patch(
`${profileRoute}/username`,
koaGuard({
body: object({ username: string().regex(usernameRegEx) }),
}),
async (ctx, next) => {
const userId = await checkSessionHealth(ctx, provider, verificationTimeout);
assertThat(userId, new RequestError({ code: 'auth.unauthorized', status: 401 }));
const { username } = ctx.guard.body;
await checkIdentifierCollision({ username }, userId);
await updateUserById(userId, { username }, 'replace');
ctx.status = 204;
return next();
}
);
router.patch(
`${profileRoute}/password`,
koaGuard({
body: object({ password: string().regex(passwordRegEx) }),
}),
async (ctx, next) => {
const userId = await checkSessionHealth(ctx, provider, verificationTimeout);
assertThat(userId, new RequestError({ code: 'auth.unauthorized', status: 401 }));
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();
}
);
router.patch(
`${profileRoute}/email`,
koaGuard({
body: object({ primaryEmail: string().regex(emailRegEx) }),
}),
async (ctx, next) => {
const userId = await checkSessionHealth(ctx, provider, verificationTimeout);
assertThat(userId, new RequestError({ code: 'auth.unauthorized', status: 401 }));
const { primaryEmail } = ctx.guard.body;
await checkIdentifierCollision({ primaryEmail });
await updateUserById(userId, { primaryEmail });
ctx.status = 204;
return next();
}
);
router.delete(`${profileRoute}/email`, async (ctx, next) => {
const userId = await checkSessionHealth(ctx, provider, verificationTimeout);
assertThat(userId, new RequestError({ code: 'auth.unauthorized', status: 401 }));
const { primaryEmail } = await findUserById(userId);
assertThat(primaryEmail, new RequestError({ code: 'user.email_not_exist', status: 422 }));
await updateUserById(userId, { primaryEmail: null });
ctx.status = 204;
return next();
});
router.patch(
`${profileRoute}/phone`,
koaGuard({
body: object({ primaryPhone: string().regex(phoneRegEx) }),
}),
async (ctx, next) => {
const userId = await checkSessionHealth(ctx, provider, verificationTimeout);
assertThat(userId, new RequestError({ code: 'auth.unauthorized', status: 401 }));
const { primaryPhone } = ctx.guard.body;
await checkIdentifierCollision({ primaryPhone });
await updateUserById(userId, { primaryPhone });
ctx.status = 204;
return next();
}
);
router.delete(`${profileRoute}/phone`, async (ctx, next) => {
const userId = await checkSessionHealth(ctx, provider, verificationTimeout);
assertThat(userId, new RequestError({ code: 'auth.unauthorized', status: 401 }));
const { primaryPhone } = await findUserById(userId);
assertThat(primaryPhone, new RequestError({ code: 'user.phone_not_exist', status: 422 }));
await updateUserById(userId, { primaryPhone: null });
ctx.status = 204;
return next();
});
router.patch(
`${profileRoute}/identities`,
koaGuard({
body: object({
connectorId: string(),
data: unknown(),
}),
}),
async (ctx, next) => {
const userId = await checkSessionHealth(ctx, provider, verificationTimeout);
assertThat(userId, new RequestError({ code: 'auth.unauthorized', status: 401 }));
const { connectorId, data } = ctx.guard.body;
const {
metadata: { target },
} = await getLogtoConnectorById(connectorId);
const socialUserInfo = await getUserInfoByAuthCode(connectorId, data);
const { identities } = await findUserById(userId);
await updateUserById(userId, {
identities: {
...identities,
[target]: { userId: socialUserInfo.id, details: socialUserInfo },
},
});
ctx.status = 204;
return next();
}
);
router.delete(
`${profileRoute}/identities/:target`,
koaGuard({
params: object({ target: string() }),
}),
async (ctx, next) => {
const { accountId: userId } = await provider.Session.get(ctx);
assertThat(userId, new RequestError({ code: 'auth.unauthorized', status: 401 }));
const { target } = ctx.guard.params;
const { identities } = await findUserById(userId);
assertThat(
has(identities, target),
new RequestError({ code: 'user.identity_not_exist', status: 404 })
);
await deleteUserIdentity(userId, target);
ctx.status = 204;
return next();
}
);
}