aboutsummaryrefslogtreecommitdiffstats
path: root/packages/trpc/routers
diff options
context:
space:
mode:
authorkamtschatka <simon.schatka@gmx.at>2024-10-19 23:48:14 +0200
committerGitHub <noreply@github.com>2024-10-19 22:48:14 +0100
commit9a56e58bd54b2bfe11104e80c602493d720ff6be (patch)
treebd5ac7c8dd386881f077519e40d911d8e07dbace /packages/trpc/routers
parent0debc6b415baa466245901fb52c009d09ef3ba15 (diff)
downloadkarakeep-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.ts64
-rw-r--r--packages/trpc/routers/users.ts109
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(