@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user