Files
kan/packages/api/src/routers/user.ts
Henry 76b2c58461 fix: security improvements to checklist input (#444)
* feat: sanitize input for checklists

* feat: add basic editor

* feat: use plain text editor for checklist items

* feat: remove key from user response
2026-03-16 14:31:13 +00:00

118 lines
2.8 KiB
TypeScript

import { TRPCError } from "@trpc/server";
import { z } from "zod";
import * as userRepo from "@kan/db/repository/user.repo";
import { createTRPCRouter, protectedProcedure } from "../trpc";
import { generateAvatarUrl } from "@kan/shared/utils";
export const userRouter = createTRPCRouter({
getUser: protectedProcedure
.meta({
openapi: {
method: "GET",
path: "/users/me",
summary: "Get user",
description:
"Retrieves the currently authenticated user's profile information",
tags: ["Users"],
protect: true,
},
})
.input(z.void())
.output(
z.object({
id: z.string(),
email: z.string(),
name: z.string().nullable(),
image: z.string().nullable(),
stripeCustomerId: z.string().nullable(),
apiKey: z
.object({
id: z.number(),
prefix: z.string().nullable(),
})
.nullable(),
}),
)
.query(async ({ ctx }) => {
const userId = ctx.user?.id;
if (!userId)
throw new TRPCError({
message: `User not authenticated`,
code: "UNAUTHORIZED",
});
const result = await userRepo.getById(ctx.db, userId);
if (!result) {
throw new TRPCError({
message: `User not found`,
code: "NOT_FOUND",
});
}
const apiKey = result.apiKeys[0];
// Generate presigned URL for avatar
const imageUrl = await generateAvatarUrl(result.image);
return {
...result,
image: imageUrl,
apiKey: apiKey ? { id: apiKey.id, prefix: apiKey.prefix } : null,
};
}),
update: protectedProcedure
.meta({
openapi: {
method: "PUT",
path: "/users",
summary: "Update user",
description:
"Updates the currently authenticated user's profile information",
tags: ["Users"],
protect: true,
},
})
.input(
z.object({
name: z.string().optional(),
image: z.string().optional(),
}),
)
.output(
z.object({
name: z.string().nullable(),
image: z.string().nullable(),
}),
)
.mutation(async ({ ctx, input }) => {
const userId = ctx.user?.id;
if (!userId)
throw new TRPCError({
message: `User not authenticated`,
code: "UNAUTHORIZED",
});
const result = await userRepo.update(ctx.db, userId, input);
if (!result) {
throw new TRPCError({
message: `User not found`,
code: "NOT_FOUND",
});
}
// Generate presigned URL for avatar
const imageUrl = await generateAvatarUrl(result.image);
return {
...result,
image: imageUrl,
};
}),
});