Files
Kordant/web/src/server/api/routers/user.ts
Michael Freno 7cbcde6a6b feat: wire frontend pages to tRPC APIs
- Add hooks (useAuth, useSubscription, useNotifications) for real API data
- Add auth service (login/signup) with password hashing and session support
- Replace stub auth with real tRPC calls in login/signup/onboarding pages
- Replace mock dashboard data with real API data from hooks
- Create service pages: DarkWatch, VoicePrint, SpamShield, HomeTitle, RemoveBrokers, Settings
- Update Navbar, TopBar, Sidebar with real user data and correct routes
- Add passwordHash field to users schema for credential auth
- Fix tests to work with real hooks (mock tRPC/hooks)
2026-05-25 17:34:48 -04:00

115 lines
3.3 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 } 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)]),
});
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 }) => {
const user = await createUserWithPassword(input.name, input.email, input.password);
const { createSession } = await import("~/server/auth/session");
const session = await createSession(user.id);
return { user, sessionToken: session.sessionToken };
}),
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;
}),
});