import { wrap } from "@typeschema/valibot"; import { object, string, minLength, email as emailVal } from "valibot"; import { TRPCError } from "@trpc/server"; import { createTRPCRouter, publicProcedure, protectedProcedure, } from "../utils"; import { UpdateUserSchema, InviteMemberSchema, RemoveMemberSchema, UpdateRoleSchema, } from "../schemas/user"; import { getUserById, updateUser, deleteUser, createUserWithPassword, authenticateUser, authenticateWithApple, refreshAccessToken, forgotPassword, resetPassword, revokeUserSessions, } from "~/server/services/user.service"; import { getFamilyGroup, inviteMember, removeMember, updateMemberRole, } from "~/server/services/family.service"; const LoginSchema = object({ email: string([emailVal()]), password: string([minLength(1)]), }); const SignupSchema = object({ name: string([minLength(1)]), email: string([emailVal()]), password: string([minLength(8)]), }); const AppleAuthSchema = object({ identityToken: string([minLength(1)]), authorizationCode: string([minLength(1)]), userIdentifier: string(), }); const RefreshTokenSchema = object({ refreshToken: string([minLength(1)]), }); const ForgotPasswordSchema = object({ email: string([emailVal()]), }); const ResetPasswordSchema = object({ token: string([minLength(1)]), password: string([minLength(8)]), }); export const userRouter = createTRPCRouter({ login: publicProcedure .input(wrap(LoginSchema)) .mutation(async ({ input }) => { return authenticateUser(input.email, input.password); }), signup: publicProcedure .input(wrap(SignupSchema)) .mutation(async ({ input }) => { return createUserWithPassword(input.name, input.email, input.password); }), appleAuth: publicProcedure .input(wrap(AppleAuthSchema)) .mutation(async ({ input }) => { return authenticateWithApple( input.identityToken, input.authorizationCode, input.userIdentifier || null, ); }), refreshToken: publicProcedure .input(wrap(RefreshTokenSchema)) .mutation(async ({ input }) => { return refreshAccessToken(input.refreshToken); }), forgotPassword: publicProcedure .input(wrap(ForgotPasswordSchema)) .mutation(async ({ input }) => { return forgotPassword(input.email); }), resetPassword: publicProcedure .input(wrap(ResetPasswordSchema)) .mutation(async ({ input }) => { return resetPassword(input.token, input.password); }), 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 }; }), logout: protectedProcedure.mutation(async ({ ctx }) => { await revokeUserSessions(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; }), });