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:
87
web/src/server/api/routers/user.ts
Normal file
87
web/src/server/api/routers/user.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import { wrap } from "@typeschema/valibot";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { createTRPCRouter, protectedProcedure } from "../utils";
|
||||
import {
|
||||
UpdateUserSchema,
|
||||
InviteMemberSchema,
|
||||
RemoveMemberSchema,
|
||||
UpdateRoleSchema,
|
||||
} from "../schemas/user";
|
||||
import { getUserById, updateUser, deleteUser } from "~/server/services/user.service";
|
||||
import {
|
||||
getFamilyGroup,
|
||||
inviteMember,
|
||||
removeMember,
|
||||
updateMemberRole,
|
||||
} from "~/server/services/family.service";
|
||||
|
||||
export const userRouter = createTRPCRouter({
|
||||
me: protectedProcedure.query(async ({ ctx }) => {
|
||||
const user = await getUserById(ctx.user.id);
|
||||
return user;
|
||||
}),
|
||||
|
||||
update: protectedProcedure
|
||||
.input(wrap(UpdateUserSchema))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const updated = await updateUser(ctx.user.id, input);
|
||||
return updated;
|
||||
}),
|
||||
|
||||
delete: protectedProcedure.mutation(async ({ ctx }) => {
|
||||
await deleteUser(ctx.user.id);
|
||||
return { success: true };
|
||||
}),
|
||||
|
||||
listFamilyMembers: protectedProcedure.query(async ({ ctx }) => {
|
||||
const group = await getFamilyGroup(ctx.user.id);
|
||||
return group.members;
|
||||
}),
|
||||
|
||||
inviteFamilyMember: protectedProcedure
|
||||
.input(wrap(InviteMemberSchema))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const group = await getFamilyGroup(ctx.user.id);
|
||||
|
||||
const callerMember = group.members.find(
|
||||
(m) => m.userId === ctx.user.id,
|
||||
);
|
||||
|
||||
if (!callerMember || (callerMember.role !== "owner" && callerMember.role !== "admin")) {
|
||||
throw new TRPCError({
|
||||
code: "FORBIDDEN",
|
||||
message: "Only owner or admin can invite members",
|
||||
});
|
||||
}
|
||||
|
||||
const invitation = await inviteMember(
|
||||
group.id,
|
||||
input.email,
|
||||
ctx.user.id,
|
||||
input.role,
|
||||
);
|
||||
|
||||
return invitation;
|
||||
}),
|
||||
|
||||
removeFamilyMember: protectedProcedure
|
||||
.input(wrap(RemoveMemberSchema))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const group = await getFamilyGroup(ctx.user.id);
|
||||
await removeMember(group.id, input.userId, ctx.user.id);
|
||||
return { success: true };
|
||||
}),
|
||||
|
||||
updateFamilyMemberRole: protectedProcedure
|
||||
.input(wrap(UpdateRoleSchema))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const group = await getFamilyGroup(ctx.user.id);
|
||||
const updated = await updateMemberRole(
|
||||
group.id,
|
||||
input.userId,
|
||||
input.role,
|
||||
ctx.user.id,
|
||||
);
|
||||
return updated;
|
||||
}),
|
||||
});
|
||||
Reference in New Issue
Block a user