2022-02-27 22:22:48 -05:00
|
|
|
import { arbitraryObjectGuard, userInfoSelectFields } from '@logto/schemas';
|
2022-03-25 02:48:53 -05:00
|
|
|
import { has } from '@silverhand/essentials';
|
2022-01-28 00:33:57 -05:00
|
|
|
import pick from 'lodash.pick';
|
|
|
|
import { InvalidInputError } from 'slonik';
|
|
|
|
import { object, string } from 'zod';
|
|
|
|
|
2022-02-16 03:34:32 -05:00
|
|
|
import RequestError from '@/errors/RequestError';
|
|
|
|
import { encryptUserPassword, generateUserId } from '@/lib/user';
|
2022-01-28 00:33:57 -05:00
|
|
|
import koaGuard from '@/middleware/koa-guard';
|
2022-02-16 02:55:08 -05:00
|
|
|
import koaPagination from '@/middleware/koa-pagination';
|
2022-01-28 00:33:57 -05:00
|
|
|
import { findRolesByRoleNames } from '@/queries/roles';
|
2022-02-16 03:34:32 -05:00
|
|
|
import {
|
2022-02-17 01:10:26 -05:00
|
|
|
clearUserCustomDataById,
|
2022-02-18 04:02:08 -05:00
|
|
|
deleteUserById,
|
2022-03-25 02:48:53 -05:00
|
|
|
deleteUserIdentity,
|
2022-02-23 23:29:34 -05:00
|
|
|
findUsers,
|
|
|
|
countUsers,
|
2022-02-16 03:34:32 -05:00
|
|
|
findUserById,
|
|
|
|
hasUser,
|
|
|
|
insertUser,
|
|
|
|
updateUserById,
|
|
|
|
} from '@/queries/user';
|
|
|
|
import assertThat from '@/utils/assert-that';
|
2022-02-28 00:16:02 -05:00
|
|
|
import { nameRegEx, passwordRegEx, usernameRegEx } from '@/utils/regex';
|
2022-01-28 00:33:57 -05:00
|
|
|
|
|
|
|
import { AuthedRouter } from './types';
|
|
|
|
|
|
|
|
export default function adminUserRoutes<T extends AuthedRouter>(router: T) {
|
2022-02-23 23:29:34 -05:00
|
|
|
router.get(
|
|
|
|
'/users',
|
|
|
|
koaPagination(),
|
|
|
|
koaGuard({ query: object({ search: string().optional() }) }),
|
|
|
|
async (ctx, next) => {
|
|
|
|
const { limit, offset } = ctx.pagination;
|
|
|
|
const {
|
|
|
|
query: { search },
|
|
|
|
} = ctx.guard;
|
2022-02-16 02:55:08 -05:00
|
|
|
|
2022-02-23 23:29:34 -05:00
|
|
|
const [{ count }, users] = await Promise.all([
|
|
|
|
countUsers(search),
|
|
|
|
findUsers(limit, offset, search),
|
|
|
|
]);
|
2022-02-16 02:55:08 -05:00
|
|
|
|
2022-02-23 23:29:34 -05:00
|
|
|
ctx.pagination.totalCount = count;
|
|
|
|
ctx.body = users.map((user) => pick(user, ...userInfoSelectFields));
|
2022-01-28 00:33:57 -05:00
|
|
|
|
2022-02-23 23:29:34 -05:00
|
|
|
return next();
|
|
|
|
}
|
|
|
|
);
|
2022-01-28 00:33:57 -05:00
|
|
|
|
2022-02-16 02:56:51 -05:00
|
|
|
router.get(
|
|
|
|
'/users/:userId',
|
2022-02-20 21:45:31 -05:00
|
|
|
// TODO: No need to guard
|
2022-02-16 02:56:51 -05:00
|
|
|
koaGuard({
|
2022-02-18 04:02:08 -05:00
|
|
|
params: object({ userId: string() }),
|
2022-02-16 02:56:51 -05:00
|
|
|
}),
|
|
|
|
async (ctx, next) => {
|
|
|
|
const {
|
|
|
|
params: { userId },
|
|
|
|
} = ctx.guard;
|
|
|
|
|
|
|
|
const user = await findUserById(userId);
|
|
|
|
|
|
|
|
ctx.body = pick(user, ...userInfoSelectFields);
|
|
|
|
|
|
|
|
return next();
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
2022-02-16 03:34:32 -05:00
|
|
|
router.post(
|
|
|
|
'/users',
|
|
|
|
koaGuard({
|
|
|
|
body: object({
|
2022-02-28 00:16:02 -05:00
|
|
|
username: string().regex(usernameRegEx),
|
|
|
|
password: string().regex(passwordRegEx),
|
|
|
|
name: string().regex(nameRegEx),
|
2022-02-16 03:34:32 -05:00
|
|
|
}),
|
|
|
|
}),
|
|
|
|
async (ctx, next) => {
|
|
|
|
const { username, password, name } = ctx.guard.body;
|
|
|
|
assertThat(
|
|
|
|
!(await hasUser(username)),
|
|
|
|
new RequestError({
|
|
|
|
code: 'user.username_exists_register',
|
|
|
|
status: 422,
|
|
|
|
})
|
|
|
|
);
|
|
|
|
|
|
|
|
const id = await generateUserId();
|
|
|
|
|
|
|
|
const { passwordEncryptionSalt, passwordEncrypted, passwordEncryptionMethod } =
|
|
|
|
encryptUserPassword(id, password);
|
|
|
|
|
|
|
|
const user = await insertUser({
|
|
|
|
id,
|
|
|
|
username,
|
|
|
|
passwordEncrypted,
|
|
|
|
passwordEncryptionMethod,
|
|
|
|
passwordEncryptionSalt,
|
|
|
|
name,
|
|
|
|
});
|
|
|
|
|
|
|
|
ctx.body = pick(user, ...userInfoSelectFields);
|
|
|
|
|
|
|
|
return next();
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
2022-02-18 01:34:22 -05:00
|
|
|
router.patch(
|
|
|
|
'/users/:userId',
|
|
|
|
koaGuard({
|
2022-02-18 04:02:08 -05:00
|
|
|
params: object({ userId: string() }),
|
2022-02-18 01:34:22 -05:00
|
|
|
body: object({
|
2022-02-28 00:16:02 -05:00
|
|
|
name: string().regex(nameRegEx).optional(),
|
2022-04-20 23:38:54 -05:00
|
|
|
avatar: string().url().nullable().optional(),
|
2022-03-20 22:48:27 -05:00
|
|
|
customData: arbitraryObjectGuard.optional(),
|
2022-04-20 23:38:54 -05:00
|
|
|
roleNames: string().array().optional(),
|
2022-02-18 01:34:22 -05:00
|
|
|
}),
|
|
|
|
}),
|
|
|
|
async (ctx, next) => {
|
|
|
|
const {
|
|
|
|
params: { userId },
|
2022-02-28 00:18:17 -05:00
|
|
|
body,
|
2022-02-18 01:34:22 -05:00
|
|
|
} = ctx.guard;
|
|
|
|
|
|
|
|
await findUserById(userId);
|
|
|
|
|
2022-03-20 22:48:27 -05:00
|
|
|
// Clear customData to achieve full replacement,
|
|
|
|
// to partial update, call patch /users/:userId/customData
|
|
|
|
if (body.customData) {
|
|
|
|
await clearUserCustomDataById(userId);
|
|
|
|
}
|
|
|
|
|
2022-04-20 23:38:54 -05:00
|
|
|
// Temp solution to validate the existence of input roleNames
|
|
|
|
if (body.roleNames) {
|
|
|
|
const { roleNames } = body;
|
|
|
|
const roles = await findRolesByRoleNames(roleNames);
|
|
|
|
|
|
|
|
if (roles.length !== roleNames.length) {
|
|
|
|
const resourcesNotFound = roleNames.filter(
|
|
|
|
(roleName) => !roles.some(({ name }) => roleName === name)
|
|
|
|
);
|
|
|
|
// TODO: Should be cached by the error handler and return request error
|
|
|
|
throw new InvalidInputError(`role names (${resourcesNotFound.join(',')}) are not valid`);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-02-18 01:34:22 -05:00
|
|
|
const user = await updateUserById(userId, {
|
2022-02-28 00:18:17 -05:00
|
|
|
...body,
|
2022-02-18 01:34:22 -05:00
|
|
|
});
|
|
|
|
|
|
|
|
ctx.body = pick(user, ...userInfoSelectFields);
|
|
|
|
|
|
|
|
return next();
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
2022-02-18 04:02:08 -05:00
|
|
|
router.patch(
|
|
|
|
'/users/:userId/password',
|
|
|
|
koaGuard({
|
|
|
|
params: object({ userId: string() }),
|
2022-02-28 00:16:02 -05:00
|
|
|
body: object({ password: string().regex(passwordRegEx) }),
|
2022-02-18 04:02:08 -05:00
|
|
|
}),
|
|
|
|
async (ctx, next) => {
|
|
|
|
const {
|
|
|
|
params: { userId },
|
|
|
|
body: { password },
|
|
|
|
} = ctx.guard;
|
|
|
|
|
|
|
|
await findUserById(userId);
|
|
|
|
|
|
|
|
const { passwordEncryptionSalt, passwordEncrypted, passwordEncryptionMethod } =
|
|
|
|
encryptUserPassword(userId, password);
|
|
|
|
|
|
|
|
const user = await updateUserById(userId, {
|
|
|
|
passwordEncrypted,
|
|
|
|
passwordEncryptionMethod,
|
|
|
|
passwordEncryptionSalt,
|
|
|
|
});
|
|
|
|
|
|
|
|
ctx.body = pick(user, ...userInfoSelectFields);
|
|
|
|
|
|
|
|
return next();
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
|
|
|
router.delete(
|
|
|
|
'/users/:userId',
|
|
|
|
koaGuard({
|
|
|
|
params: object({ userId: string() }),
|
|
|
|
}),
|
|
|
|
async (ctx, next) => {
|
|
|
|
const {
|
|
|
|
params: { userId },
|
|
|
|
} = ctx.guard;
|
|
|
|
|
|
|
|
await findUserById(userId);
|
|
|
|
|
|
|
|
await deleteUserById(userId);
|
|
|
|
|
|
|
|
ctx.status = 204;
|
|
|
|
|
|
|
|
return next();
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
2022-02-16 05:06:08 -05:00
|
|
|
router.patch(
|
|
|
|
'/users/:userId/custom-data',
|
|
|
|
koaGuard({
|
2022-02-18 04:02:08 -05:00
|
|
|
params: object({ userId: string() }),
|
2022-02-27 22:22:48 -05:00
|
|
|
body: object({ customData: arbitraryObjectGuard }),
|
2022-02-16 05:06:08 -05:00
|
|
|
}),
|
|
|
|
async (ctx, next) => {
|
|
|
|
const {
|
|
|
|
params: { userId },
|
|
|
|
body: { customData },
|
|
|
|
} = ctx.guard;
|
|
|
|
|
|
|
|
await findUserById(userId);
|
|
|
|
|
|
|
|
const user = await updateUserById(userId, {
|
|
|
|
customData,
|
|
|
|
});
|
|
|
|
|
|
|
|
ctx.body = pick(user, ...userInfoSelectFields);
|
|
|
|
|
|
|
|
return next();
|
|
|
|
}
|
|
|
|
);
|
2022-02-17 01:10:26 -05:00
|
|
|
|
|
|
|
router.delete(
|
|
|
|
'/users/:userId/custom-data',
|
|
|
|
koaGuard({
|
2022-02-18 04:02:08 -05:00
|
|
|
params: object({ userId: string() }),
|
2022-02-17 01:10:26 -05:00
|
|
|
}),
|
|
|
|
async (ctx, next) => {
|
|
|
|
const {
|
|
|
|
params: { userId },
|
|
|
|
} = ctx.guard;
|
|
|
|
|
|
|
|
await findUserById(userId);
|
|
|
|
|
|
|
|
await clearUserCustomDataById(userId);
|
|
|
|
|
|
|
|
ctx.status = 200;
|
|
|
|
|
|
|
|
return next();
|
|
|
|
}
|
|
|
|
);
|
2022-03-25 02:48:53 -05:00
|
|
|
|
|
|
|
router.delete(
|
|
|
|
'/users/:userId/identities/:connectorId',
|
|
|
|
koaGuard({ params: object({ userId: string(), connectorId: string() }) }),
|
|
|
|
async (ctx, next) => {
|
|
|
|
const {
|
|
|
|
params: { userId, connectorId },
|
|
|
|
} = ctx.guard;
|
|
|
|
|
|
|
|
const { identities } = await findUserById(userId);
|
|
|
|
|
|
|
|
if (!has(identities, connectorId)) {
|
|
|
|
throw new RequestError({ code: 'user.identity_not_exists', status: 404 });
|
|
|
|
}
|
|
|
|
|
|
|
|
const updatedUser = await deleteUserIdentity(userId, connectorId);
|
|
|
|
ctx.body = pick(updatedUser, ...userInfoSelectFields);
|
|
|
|
|
|
|
|
return next();
|
|
|
|
}
|
|
|
|
);
|
2022-01-28 00:33:57 -05:00
|
|
|
}
|