diff options
| author | kamtschatka <simon.schatka@gmx.at> | 2024-10-19 23:48:14 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2024-10-19 22:48:14 +0100 |
| commit | 9a56e58bd54b2bfe11104e80c602493d720ff6be (patch) | |
| tree | bd5ac7c8dd386881f077519e40d911d8e07dbace /packages/trpc/routers | |
| parent | 0debc6b415baa466245901fb52c009d09ef3ba15 (diff) | |
| download | karakeep-9a56e58bd54b2bfe11104e80c602493d720ff6be.tar.zst | |
feature: Allow reseting user password, change their roles and create new users. Fixes #495 (#567)
* How do I set the variable "user" or "system" for AI inference #262
changed from system to user
* Make Myself an Admin #560
added user management functionality to the admin page
* A bunch of UI fixes and simplifications
---------
Co-authored-by: Mohamed Bassem <me@mbassem.com>
Diffstat (limited to 'packages/trpc/routers')
| -rw-r--r-- | packages/trpc/routers/admin.ts | 64 | ||||
| -rw-r--r-- | packages/trpc/routers/users.ts | 109 |
2 files changed, 130 insertions, 43 deletions
diff --git a/packages/trpc/routers/admin.ts b/packages/trpc/routers/admin.ts index d8ffe9d3..38aa0031 100644 --- a/packages/trpc/routers/admin.ts +++ b/packages/trpc/routers/admin.ts @@ -1,3 +1,4 @@ +import { TRPCError } from "@trpc/server"; import { count, eq, sum } from "drizzle-orm"; import { z } from "zod"; @@ -9,8 +10,15 @@ import { TidyAssetsQueue, triggerSearchReindex, } from "@hoarder/shared/queues"; +import { + changeRoleSchema, + resetPasswordSchema, + zAdminCreateUserSchema, +} from "@hoarder/shared/types/admin"; +import { hashPassword } from "../auth"; import { adminProcedure, router } from "../index"; +import { createUser } from "./users"; export const adminAppRouter = router({ stats: adminProcedure @@ -213,4 +221,60 @@ export const adminAppRouter = router({ return results; }), + createUser: adminProcedure + .input(zAdminCreateUserSchema) + .output( + z.object({ + id: z.string(), + name: z.string(), + email: z.string(), + role: z.enum(["user", "admin"]).nullable(), + }), + ) + .mutation(async ({ input, ctx }) => { + return createUser(input, ctx, input.role); + }), + changeRole: adminProcedure + .input(changeRoleSchema) + .mutation(async ({ input, ctx }) => { + if (ctx.user.id == input.userId) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Cannot change own role", + }); + } + const result = await ctx.db + .update(users) + .set({ role: input.role }) + .where(eq(users.id, input.userId)); + + if (!result.changes) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "User not found", + }); + } + }), + resetPassword: adminProcedure + .input(resetPasswordSchema) + .mutation(async ({ input, ctx }) => { + if (ctx.user.id == input.userId) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Cannot reset own password", + }); + } + const hashedPassword = await hashPassword(input.newPassword); + const result = await ctx.db + .update(users) + .set({ password: hashedPassword }) + .where(eq(users.id, input.userId)); + + if (result.changes == 0) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "User not found", + }); + } + }), }); diff --git a/packages/trpc/routers/users.ts b/packages/trpc/routers/users.ts index 87d0fa2d..ca46d9f7 100644 --- a/packages/trpc/routers/users.ts +++ b/packages/trpc/routers/users.ts @@ -13,10 +13,58 @@ import { hashPassword, validatePassword } from "../auth"; import { adminProcedure, authedProcedure, + Context, publicProcedure, router, } from "../index"; +export async function createUser( + input: z.infer<typeof zSignUpSchema>, + ctx: Context, + role?: "user" | "admin", +) { + return ctx.db.transaction(async (trx) => { + let userRole = role; + if (!userRole) { + const [{ count: userCount }] = await trx + .select({ count: count() }) + .from(users); + userRole = userCount == 0 ? "admin" : "user"; + } + + try { + const result = await trx + .insert(users) + .values({ + name: input.name, + email: input.email, + password: await hashPassword(input.password), + role: userRole, + }) + .returning({ + id: users.id, + name: users.name, + email: users.email, + role: users.role, + }); + return result[0]; + } catch (e) { + if (e instanceof SqliteError) { + if (e.code == "SQLITE_CONSTRAINT_UNIQUE") { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Email is already taken", + }); + } + } + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "Something went wrong", + }); + } + }); +} + export const usersAppRouter = router({ create: publicProcedure .input(zSignUpSchema) @@ -41,40 +89,7 @@ export const usersAppRouter = router({ message: errorMessage, }); } - // TODO: This is racy, but that's probably fine. - const [{ count: userCount }] = await ctx.db - .select({ count: count() }) - .from(users); - try { - const result = await ctx.db - .insert(users) - .values({ - name: input.name, - email: input.email, - password: await hashPassword(input.password), - role: userCount == 0 ? "admin" : "user", - }) - .returning({ - id: users.id, - name: users.name, - email: users.email, - role: users.role, - }); - return result[0]; - } catch (e) { - if (e instanceof SqliteError) { - if (e.code == "SQLITE_CONSTRAINT_UNIQUE") { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Email is already taken", - }); - } - } - throw new TRPCError({ - code: "INTERNAL_SERVER_ERROR", - message: "Something went wrong", - }); - } + return createUser(input, ctx); }), list: adminProcedure .output( @@ -85,20 +100,28 @@ export const usersAppRouter = router({ name: z.string(), email: z.string(), role: z.enum(["user", "admin"]).nullable(), + localUser: z.boolean(), }), ), }), ) .query(async ({ ctx }) => { - const users = await ctx.db.query.users.findMany({ - columns: { - id: true, - name: true, - email: true, - role: true, - }, - }); - return { users }; + const dbUsers = await ctx.db + .select({ + id: users.id, + name: users.name, + email: users.email, + role: users.role, + password: users.password, + }) + .from(users); + + return { + users: dbUsers.map(({ password, ...user }) => ({ + ...user, + localUser: password !== null, + })), + }; }), changePassword: authedProcedure .input( |
