feat: implement user & family group management tRPC router
- Add user router with me/update/delete procedures (protected) - Add family router with listMembers/invite/remove/updateRole procedures - Create user service layer (getUserById, updateUser, deleteUser) - Create family service layer (getFamilyGroup, inviteMember, removeMember, updateMemberRole, transferOwnership) - Add Valibot input schemas for all procedures - Add invitations table with status tracking and expiration - Add deletedAt column to users table (soft-delete) - Wire user router into app root router - Write unit tests for service functions and tRPC procedures - Update schema tests for new table/columns
This commit is contained in:
82
web/src/server/services/user.service.ts
Normal file
82
web/src/server/services/user.service.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { db } from "~/server/db";
|
||||
import { users } from "~/server/db/schema/auth";
|
||||
|
||||
export async function getUserById(id: string) {
|
||||
const user = await db.query.users.findFirst({
|
||||
where: eq(users.id, id),
|
||||
with: {
|
||||
accounts: true,
|
||||
sessions: true,
|
||||
deviceTokens: true,
|
||||
familyGroups: true,
|
||||
familyGroupOwned: true,
|
||||
subscriptions: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw new TRPCError({ code: "NOT_FOUND", message: "User not found" });
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
export async function updateUser(
|
||||
id: string,
|
||||
data: { name?: string; email?: string; image?: string },
|
||||
) {
|
||||
const [existing] = await db
|
||||
.select()
|
||||
.from(users)
|
||||
.where(eq(users.id, id))
|
||||
.limit(1);
|
||||
|
||||
if (!existing) {
|
||||
throw new TRPCError({ code: "NOT_FOUND", message: "User not found" });
|
||||
}
|
||||
|
||||
if (data.email && data.email !== existing.email) {
|
||||
const [duplicate] = await db
|
||||
.select()
|
||||
.from(users)
|
||||
.where(eq(users.email, data.email))
|
||||
.limit(1);
|
||||
|
||||
if (duplicate) {
|
||||
throw new TRPCError({
|
||||
code: "CONFLICT",
|
||||
message: "Email already in use",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const [updated] = await db
|
||||
.update(users)
|
||||
.set(data)
|
||||
.where(eq(users.id, id))
|
||||
.returning();
|
||||
|
||||
return updated;
|
||||
}
|
||||
|
||||
export async function deleteUser(id: string) {
|
||||
const [existing] = await db
|
||||
.select()
|
||||
.from(users)
|
||||
.where(eq(users.id, id))
|
||||
.limit(1);
|
||||
|
||||
if (!existing) {
|
||||
throw new TRPCError({ code: "NOT_FOUND", message: "User not found" });
|
||||
}
|
||||
|
||||
const [deleted] = await db
|
||||
.update(users)
|
||||
.set({ deletedAt: new Date() })
|
||||
.where(eq(users.id, id))
|
||||
.returning();
|
||||
|
||||
return deleted;
|
||||
}
|
||||
Reference in New Issue
Block a user