diff options
Diffstat (limited to 'packages/web/server/api/routers')
| -rw-r--r-- | packages/web/server/api/routers/admin.ts | 11 | ||||
| -rw-r--r-- | packages/web/server/api/routers/users.test.ts | 46 | ||||
| -rw-r--r-- | packages/web/server/api/routers/users.ts | 43 |
3 files changed, 86 insertions, 14 deletions
diff --git a/packages/web/server/api/routers/admin.ts b/packages/web/server/api/routers/admin.ts index 92769151..c3f6235a 100644 --- a/packages/web/server/api/routers/admin.ts +++ b/packages/web/server/api/routers/admin.ts @@ -1,6 +1,5 @@ -import { authedProcedure, router } from "../trpc"; +import { adminProcedure, router } from "../trpc"; import { z } from "zod"; -import { TRPCError } from "@trpc/server"; import { count } from "drizzle-orm"; import { bookmarks, users } from "@hoarder/db/schema"; import { @@ -9,14 +8,6 @@ import { SearchIndexingQueue, } from "@hoarder/shared/queues"; -const adminProcedure = authedProcedure.use(function isAdmin(opts) { - const user = opts.ctx.user; - if (user.role != "admin") { - throw new TRPCError({ code: "FORBIDDEN" }); - } - return opts.next(opts); -}); - export const adminAppRouter = router({ stats: adminProcedure .output( diff --git a/packages/web/server/api/routers/users.test.ts b/packages/web/server/api/routers/users.test.ts index b188d3a0..1ee04f99 100644 --- a/packages/web/server/api/routers/users.test.ts +++ b/packages/web/server/api/routers/users.test.ts @@ -1,5 +1,9 @@ -import { CustomTestContext, defaultBeforeEach } from "@/lib/testUtils"; -import { expect, describe, test, beforeEach } from "vitest"; +import { + CustomTestContext, + defaultBeforeEach, + getApiCaller, +} from "@/lib/testUtils"; +import { expect, describe, test, beforeEach, assert } from "vitest"; beforeEach<CustomTestContext>(defaultBeforeEach(false)); @@ -54,4 +58,42 @@ describe("User Routes", () => { }), ).rejects.toThrow(/Email is already taken/); }); + + test<CustomTestContext>("privacy checks", async ({ + db, + unauthedAPICaller, + }) => { + const adminUser = await unauthedAPICaller.users.create({ + name: "Test User", + email: "test123@test.com", + password: "pass1234", + confirmPassword: "pass1234", + }); + const [user1, user2] = await Promise.all( + ["test1234@test.com", "test12345@test.com"].map((e) => + unauthedAPICaller.users.create({ + name: "Test User", + email: e, + password: "pass1234", + confirmPassword: "pass1234", + }), + ), + ); + + assert(adminUser.role == "admin"); + assert(user1.role == "user"); + assert(user2.role == "user"); + + const user2Caller = getApiCaller(db, user2.id); + + // A normal user can't delete other users + await expect(() => + user2Caller.users.delete({ + userId: user1.id, + }), + ).rejects.toThrow(/FORBIDDEN/); + + // A normal user can't list all users + await expect(() => user2Caller.users.list()).rejects.toThrow(/FORBIDDEN/); + }); }); diff --git a/packages/web/server/api/routers/users.ts b/packages/web/server/api/routers/users.ts index 3d5d982d..32d10860 100644 --- a/packages/web/server/api/routers/users.ts +++ b/packages/web/server/api/routers/users.ts @@ -1,23 +1,25 @@ import { zSignUpSchema } from "@/lib/types/api/users"; -import { publicProcedure, router } from "../trpc"; +import { adminProcedure, publicProcedure, router } from "../trpc"; import { SqliteError } from "@hoarder/db"; import { z } from "zod"; import { hashPassword } from "@/server/auth"; import { TRPCError } from "@trpc/server"; import { users } from "@hoarder/db/schema"; -import { count } from "drizzle-orm"; +import { count, eq } from "drizzle-orm"; export const usersAppRouter = router({ create: publicProcedure .input(zSignUpSchema) .output( z.object({ + id: z.string(), name: z.string(), email: z.string(), role: z.enum(["user", "admin"]).nullable(), }), ) .mutation(async ({ input, ctx }) => { + // TODO: This is racy, but that's probably fine. const [{ count: userCount }] = await ctx.db .select({ count: count() }) .from(users); @@ -31,6 +33,7 @@ export const usersAppRouter = router({ role: userCount == 0 ? "admin" : "user", }) .returning({ + id: users.id, name: users.name, email: users.email, role: users.role, @@ -51,4 +54,40 @@ export const usersAppRouter = router({ }); } }), + list: adminProcedure + .output( + z.object({ + users: z.array( + z.object({ + id: z.string(), + name: z.string(), + email: z.string(), + role: z.enum(["user", "admin"]).nullable(), + }), + ), + }), + ) + .query(async ({ ctx }) => { + const users = await ctx.db.query.users.findMany({ + columns: { + id: true, + name: true, + email: true, + role: true, + }, + }); + return { users }; + }), + delete: adminProcedure + .input( + z.object({ + userId: z.string(), + }), + ) + .mutation(async ({ input, ctx }) => { + const res = await ctx.db.delete(users).where(eq(users.id, input.userId)); + if (res.changes == 0) { + throw new TRPCError({ code: "NOT_FOUND" }); + } + }), }); |
