feat: add user lifecycle management

This commit is contained in:
Syahdan 2026-06-29 21:22:33 +07:00
parent 8ab9a9a497
commit 11b16ac3cf
8 changed files with 331 additions and 47 deletions

View file

@ -110,3 +110,10 @@ export const updateUserRolesSchema = z.object({
userId: entityIdSchema,
roleIds: z.array(entityIdSchema).min(1, "Select at least one role"),
});
export const userCreateSchema = z.object({
name: z.string().trim().min(2, "Name must be at least 2 characters").max(120),
email: z.email("Invalid email address").max(255),
password: z.string().min(8, "Password must be at least 8 characters").max(255),
roleIds: z.array(entityIdSchema).min(1, "Select at least one role"),
});

View file

@ -1,9 +1,10 @@
import { db, role, user, userRole } from "@minmon/db";
import { db, account, role, user, userRole } from "@minmon/db";
import { hashCredentialPassword } from "@minmon/db/auth-utils";
import { asc, eq } from "drizzle-orm";
import { permissionedProcedure, protectedProcedure, router } from "../index";
import { updateUserRolesSchema } from "./schemas";
import { notFound } from "./utils";
import { byIdSchema, updateUserRolesSchema, userCreateSchema } from "./schemas";
import { badRequest, createEntityId, notFound } from "./utils";
async function getUsersWithRoles() {
const [users, assignments] = await Promise.all([
@ -38,6 +39,19 @@ async function getUsersWithRoles() {
}));
}
async function validateRoleIds(roleIds: string[]) {
const roles = await db.select({ id: role.id }).from(role);
const validRoleIds = new Set(roles.map((item) => item.id));
const uniqueRoleIds = Array.from(new Set(roleIds));
const invalidRoleId = uniqueRoleIds.find((roleId) => !validRoleIds.has(roleId));
if (invalidRoleId) {
notFound(`Role not found: ${invalidRoleId}`);
}
return uniqueRoleIds;
}
export const usersRouter = router({
me: protectedProcedure.query(({ ctx }) => ({
user: ctx.session.user,
@ -58,6 +72,50 @@ export const usersRouter = router({
.orderBy(asc(role.name)),
),
create: permissionedProcedure("users.create")
.input(userCreateSchema)
.mutation(async ({ input }) => {
const [existingUser] = await db.select({ id: user.id }).from(user).where(eq(user.email, input.email)).limit(1);
if (existingUser) {
badRequest("A user with this email already exists");
}
const uniqueRoleIds = await validateRoleIds(input.roleIds);
const userId = createEntityId();
const now = new Date();
const passwordHash = await hashCredentialPassword(input.password);
await db.insert(user).values({
id: userId,
name: input.name,
email: input.email,
emailVerified: true,
createdAt: now,
updatedAt: now,
});
await db.insert(account).values({
id: createEntityId(),
accountId: userId,
providerId: "credential",
userId,
password: passwordHash,
createdAt: now,
updatedAt: now,
});
await db.insert(userRole).values(
uniqueRoleIds.map((roleId) => ({
userId,
roleId,
})),
);
const users = await getUsersWithRoles();
return users.find((item) => item.id === userId);
}),
updateRoles: permissionedProcedure("users.assignRoles")
.input(updateUserRolesSchema)
.mutation(async ({ input }) => {
@ -67,14 +125,7 @@ export const usersRouter = router({
notFound("User not found");
}
const roles = await db.select({ id: role.id }).from(role);
const validRoleIds = new Set(roles.map((item) => item.id));
const uniqueRoleIds = Array.from(new Set(input.roleIds));
const invalidRoleId = uniqueRoleIds.find((roleId) => !validRoleIds.has(roleId));
if (invalidRoleId) {
notFound(`Role not found: ${invalidRoleId}`);
}
const uniqueRoleIds = await validateRoleIds(input.roleIds);
await db.delete(userRole).where(eq(userRole.userId, input.userId));
await db.insert(userRole).values(
@ -87,4 +138,19 @@ export const usersRouter = router({
const updated = await getUsersWithRoles();
return updated.find((item) => item.id === input.userId);
}),
delete: permissionedProcedure("users.delete").input(byIdSchema).mutation(async ({ ctx, input }) => {
if (ctx.session.user.id === input.id) {
badRequest("You cannot delete your own user account while signed in");
}
const [existingUser] = await db.select({ id: user.id }).from(user).where(eq(user.id, input.id)).limit(1);
if (!existingUser) {
notFound("User not found");
}
await db.delete(user).where(eq(user.id, input.id));
return { id: input.id };
}),
});