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:
2026-05-25 15:57:33 -04:00
parent 71972436b6
commit 28c33a930d
13 changed files with 1124 additions and 5 deletions

View 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;
}