112 lines
3.8 KiB
TypeScript
112 lines
3.8 KiB
TypeScript
import { getUsers, getUserByEmail, getUserById, createUser, deleteUser } from 'officerdb';
|
|
import { createRouter } from '@@/create-router';
|
|
import { sign } from '@@/jwt';
|
|
import { USER_ROLES } from 'definitions';
|
|
import { sendMail } from 'emailer';
|
|
import * as errors from '@@/custom-errors';
|
|
import { originMiddleware } from '@@/_middlewares';
|
|
import { updateUserHandler } from './update-user';
|
|
import { deprovisionUserEnvironment } from './provision';
|
|
|
|
export const usersRouter = createRouter();
|
|
usersRouter.use(originMiddleware);
|
|
|
|
// List all users (Super Admin only)
|
|
usersRouter.get('/', async (ctx) => {
|
|
const user = ctx.get('user');
|
|
if (user.role !== 'Super Admin') throw errors.FORBIDDEN();
|
|
|
|
const users = await getUsers();
|
|
const sanitized = users.map(({ password, ...rest }) => rest);
|
|
|
|
return ctx.json(sanitized);
|
|
});
|
|
|
|
// Self-update (any authenticated user)
|
|
usersRouter.put('/', updateUserHandler);
|
|
|
|
// Invite a new user (Super Admin only)
|
|
usersRouter.post('/invite', async (ctx) => {
|
|
const reqUser = ctx.get('user');
|
|
if (reqUser.role !== 'Super Admin') throw errors.FORBIDDEN();
|
|
|
|
const { email, role } = ctx.get('body');
|
|
if (!email || typeof email !== 'string' || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
|
|
throw errors.BAD_REQUEST('Invalid email address');
|
|
}
|
|
|
|
const validRoles = USER_ROLES.filter((r) => r !== 'Super Admin');
|
|
const assignedRole =
|
|
typeof role === 'string' && validRoles.includes(role as (typeof validRoles)[number])
|
|
? (role as (typeof USER_ROLES)[number])
|
|
: ('Member' as const);
|
|
|
|
const existing = await getUserByEmail(email);
|
|
if (existing) throw errors.CONFLICT('A user with this email already exists');
|
|
|
|
const dbUser = await createUser({
|
|
email,
|
|
role: assignedRole,
|
|
status: 'Invited',
|
|
});
|
|
|
|
const origin = ctx.get('origin');
|
|
const verificationCode = await sign({ id: dbUser.id, email: dbUser.email }, '24h');
|
|
const url = `${origin}/auth/verify?verificationCode=${verificationCode}`;
|
|
|
|
await sendMail({
|
|
template: 'UserInvite',
|
|
subject: 'You have been invited to officer.dev',
|
|
to: email,
|
|
data: { invitedBy: reqUser.name ?? reqUser.email, url },
|
|
});
|
|
|
|
const { password, ...safeUser } = dbUser;
|
|
return ctx.json(safeUser);
|
|
});
|
|
|
|
// Resend invite (Super Admin only, status must be Invited)
|
|
usersRouter.post('/:id/resend-invite', async (ctx) => {
|
|
const reqUser = ctx.get('user');
|
|
if (reqUser.role !== 'Super Admin') throw errors.FORBIDDEN();
|
|
|
|
const id = Number(ctx.req.param('id'));
|
|
if (!id || isNaN(id)) throw errors.BAD_REQUEST('Invalid user ID');
|
|
|
|
const target = await getUserById(id);
|
|
if (!target) throw errors.NOT_FOUND('User not found');
|
|
if (target.status !== 'Invited') throw errors.BAD_REQUEST('User is not in Invited status');
|
|
|
|
const origin = ctx.get('origin');
|
|
const verificationCode = await sign({ id: target.id, email: target.email }, '24h');
|
|
const url = `${origin}/auth/verify?verificationCode=${verificationCode}`;
|
|
|
|
await sendMail({
|
|
template: 'UserInvite',
|
|
subject: 'You have been invited to officer.dev',
|
|
to: target.email,
|
|
data: { invitedBy: reqUser.name ?? reqUser.email, url },
|
|
});
|
|
|
|
return ctx.json({ ok: true });
|
|
});
|
|
|
|
// Delete a user (Super Admin only, cannot delete self)
|
|
usersRouter.delete('/:id', async (ctx) => {
|
|
const reqUser = ctx.get('user');
|
|
if (reqUser.role !== 'Super Admin') throw errors.FORBIDDEN();
|
|
|
|
const id = Number(ctx.req.param('id'));
|
|
if (!id || isNaN(id)) throw errors.BAD_REQUEST('Invalid user ID');
|
|
if (id === reqUser.id) throw errors.BAD_REQUEST('Cannot delete yourself');
|
|
|
|
const target = await getUserById(id);
|
|
if (!target) throw errors.NOT_FOUND('User not found');
|
|
|
|
// Deprovision user environment before deleting from database
|
|
deprovisionUserEnvironment(target.email, target.username ?? '');
|
|
|
|
await deleteUser(id);
|
|
return ctx.json({ ok: true });
|
|
});
|