Files
Kordant/web/src/server/api/routers/user.ts
2026-06-03 14:05:49 -04:00

180 lines
4.2 KiB
TypeScript

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;
}),
});