#1616 better user management (#1748)

This commit is contained in:
Manuel
2023-12-20 10:18:24 +01:00
committed by GitHub
parent 199b711324
commit 553fa98e61
11 changed files with 760 additions and 127 deletions

View File

@@ -1,11 +1,19 @@
import { TRPCError } from '@trpc/server';
import bcrypt from 'bcryptjs';
import { randomUUID } from 'crypto';
import { eq, like, sql } from 'drizzle-orm';
import { and, eq, like, sql } from 'drizzle-orm';
import { z } from 'zod';
import { COOKIE_COLOR_SCHEME_KEY, COOKIE_LOCALE_KEY } from '../../../../data/constants';
import { adminProcedure, createTRPCRouter, protectedProcedure, publicProcedure } from '../trpc';
import { db } from '~/server/db';
import { getTotalUserCountAsync } from '~/server/db/queries/user';
import { UserSettings, invites, userSettings, users } from '~/server/db/schema';
import { invites, sessions, users, userSettings, UserSettings } from '~/server/db/schema';
import { hashPassword } from '~/utils/security';
import {
colorSchemeParser,
@@ -13,9 +21,7 @@ import {
signUpFormSchema,
updateSettingsValidationSchema,
} from '~/validations/user';
import { COOKIE_COLOR_SCHEME_KEY, COOKIE_LOCALE_KEY } from '../../../../data/constants';
import { adminProcedure, createTRPCRouter, protectedProcedure, publicProcedure } from '../trpc';
import { PossibleRoleFilter } from '~/pages/manage/users';
export const userRouter = createTRPCRouter({
createOwnerAccount: publicProcedure.input(signUpFormSchema).mutation(async ({ ctx, input }) => {
@@ -34,6 +40,47 @@ export const userRouter = createTRPCRouter({
isOwner: true,
});
}),
updatePassword: adminProcedure
.input(
z.object({
userId: z.string(),
newPassword: z.string().min(3),
terminateExistingSessions: z.boolean(),
}),
)
.mutation(async ({ input, ctx }) => {
const user = await db.query.users.findFirst({
where: eq(users.id, input.userId),
});
if (!user) {
throw new TRPCError({
code: 'NOT_FOUND',
});
}
if (user.isOwner && user.id !== ctx.session.user.id) {
throw new TRPCError({
code: 'FORBIDDEN',
message: 'Operation not allowed or incorrect user',
});
}
const salt = bcrypt.genSaltSync(10);
const hashedPassword = hashPassword(input.newPassword, salt);
if (input.terminateExistingSessions) {
await db.delete(sessions).where(eq(sessions.userId, input.userId));
}
await db
.update(users)
.set({
password: hashedPassword,
salt: salt,
})
.where(eq(users.id, input.userId));
}),
count: publicProcedure.query(async () => {
return await getTotalUserCountAsync();
}),
@@ -42,8 +89,8 @@ export const userRouter = createTRPCRouter({
signUpFormSchema.and(
z.object({
inviteToken: z.string(),
})
)
}),
),
)
.mutation(async ({ ctx, input }) => {
const invite = await db.query.invites.findFirst({
@@ -75,7 +122,7 @@ export const userRouter = createTRPCRouter({
.input(
z.object({
colorScheme: colorSchemeParser,
})
}),
)
.mutation(async ({ ctx, input }) => {
await db
@@ -122,7 +169,7 @@ export const userRouter = createTRPCRouter({
.input(
z.object({
language: z.string(),
})
}),
)
.mutation(async ({ ctx, input }) => {
await db
@@ -184,24 +231,48 @@ export const userRouter = createTRPCRouter({
z.object({
limit: z.number().min(1).max(100).default(10),
page: z.number().min(0),
search: z
.string()
.optional()
.transform((value) => (value === '' ? undefined : value)),
})
search: z.object({
fullTextSearch: z
.string()
.optional()
.transform((value) => (value === '' ? undefined : value)),
role: z
.string()
.transform((value) => (value.length > 0 ? value : undefined))
.optional(),
}),
}),
)
.query(async ({ ctx, input }) => {
const roleFilter = () => {
if (input.search.role === PossibleRoleFilter[1].id) {
return eq(users.isOwner, true);
}
if (input.search.role === PossibleRoleFilter[2].id) {
return eq(users.isAdmin, true);
}
if (input.search.role === PossibleRoleFilter[3].id) {
return and(eq(users.isAdmin, false), eq(users.isOwner, false));
}
return undefined;
};
const limit = input.limit;
const dbUsers = await db.query.users.findMany({
limit: limit + 1,
offset: limit * input.page,
where: input.search ? like(users.name, `%${input.search}%`) : undefined,
where: and(input.search.fullTextSearch ? like(users.name, `%${input.search.fullTextSearch}%`) : undefined, roleFilter()),
});
const countUsers = await db
.select({ count: sql<number>`count(*)` })
.from(users)
.where(input.search ? like(users.name, `%${input.search}%`) : undefined)
.where(input.search.fullTextSearch ? like(users.name, `%${input.search.fullTextSearch}%`) : undefined)
.where(roleFilter())
.then((rows) => rows[0].count);
return {
@@ -213,17 +284,54 @@ export const userRouter = createTRPCRouter({
isOwner: user.isOwner,
})),
countPages: Math.ceil(countUsers / limit),
stats: {
roles: {
all: (await db.select({ count: sql<number>`count(*)` }).from(users))[0]['count'],
owner: (
await db
.select({ count: sql<number>`count(*)` })
.from(users)
.where(eq(users.isOwner, true))
)[0]['count'],
admin: (
await db
.select({ count: sql<number>`count(*)` })
.from(users)
.where(and(eq(users.isAdmin, true), eq(users.isOwner, false)))
)[0]['count'],
normal: (
await db
.select({ count: sql<number>`count(*)` })
.from(users)
.where(and(eq(users.isAdmin, false), eq(users.isOwner, false)))
)[0]['count'],
} as Record<string, number>,
},
};
}),
create: adminProcedure.input(createNewUserSchema).mutation(async ({ ctx, input }) => {
create: adminProcedure.input(createNewUserSchema).mutation(async ({ input }) => {
await createUserIfNotPresent(input);
}),
details: adminProcedure.input(z.object({ userId: z.string() })).query(async ({ input }) => {
return db.query.users.findFirst({
where: eq(users.id, input.userId),
});
}),
updateDetails: adminProcedure.input(z.object({
userId: z.string(),
username: z.string(),
eMail: z.string().optional().transform(value => value?.length === 0 ? null : value),
})).mutation(async ({ input }) => {
await db.update(users).set({
name: input.username,
email: input.eMail as string | null,
}).where(eq(users.id, input.userId));
}),
deleteUser: adminProcedure
.input(
z.object({
id: z.string(),
})
}),
)
.mutation(async ({ ctx, input }) => {
const user = await db.query.users.findFirst({
@@ -259,7 +367,7 @@ const createUserIfNotPresent = async (
options: {
defaultSettings?: Partial<UserSettings>;
isOwner?: boolean;
} | void
} | void,
) => {
const existingUser = await db.query.users.findFirst({
where: eq(users.name, input.username),