feat: add user management features with CRUD operations and password policies

- Implemented PATCH and DELETE endpoints for bug reports.
- Created user management page with user creation, display name update, password reset, and user deletion functionalities.
- Added password validation and hashing for user creation and password reset.
- Integrated dialogs for user actions (create, update, reset password, delete).
- Configured SvelteKit with Node adapter and Vite for build process.
- Set up Tailwind CSS for styling.
This commit is contained in:
Flavio Fois
2026-02-16 13:30:01 +01:00
parent 7ddd6ebac5
commit c49692a75b
182 changed files with 6617 additions and 0 deletions

View File

@@ -0,0 +1,33 @@
{
"/": [
"src/routes/+page.server.ts",
"src/routes/+layout.server.ts",
"src/routes/+layout.server.ts"
],
"/api/reports/refresh": [
"src/routes/api/reports/refresh/+server.ts"
],
"/api/reports/[id]/download": [
"src/routes/api/reports/[id]/download/+server.ts"
],
"/api/reports/[id]/files/[fileId]": [
"src/routes/api/reports/[id]/files/[fileId]/+server.ts"
],
"/login": [
"src/routes/login/+page.server.ts",
"src/routes/+layout.server.ts"
],
"/logout": [
"src/routes/logout/+page.server.ts",
"src/routes/+layout.server.ts"
],
"/reports/[id]": [
"src/routes/reports/[id]/+page.server.ts",
"src/routes/+layout.server.ts",
"src/routes/reports/[id]/+server.ts"
],
"/users": [
"src/routes/users/+page.server.ts",
"src/routes/+layout.server.ts"
]
}

View File

@@ -0,0 +1,34 @@
import type * as Kit from '@sveltejs/kit';
type Expand<T> = T extends infer O ? { [K in keyof O]: O[K] } : never;
// @ts-ignore
type MatcherParam<M> = M extends (param : string) => param is infer U ? U extends string ? U : string : string;
type RouteParams = { };
type RouteId = '/';
type MaybeWithVoid<T> = {} extends T ? T | void : T;
export type RequiredKeys<T> = { [K in keyof T]-?: {} extends { [P in K]: T[K] } ? never : K; }[keyof T];
type OutputDataShape<T> = MaybeWithVoid<Omit<App.PageData, RequiredKeys<T>> & Partial<Pick<App.PageData, keyof T & keyof App.PageData>> & Record<string, any>>
type EnsureDefined<T> = T extends null | undefined ? {} : T;
type OptionalUnion<U extends Record<string, any>, A extends keyof U = U extends U ? keyof U : never> = U extends unknown ? { [P in Exclude<A, keyof U>]?: never } & U : never;
export type Snapshot<T = any> = Kit.Snapshot<T>;
type PageServerParentData = EnsureDefined<LayoutServerData>;
type PageParentData = EnsureDefined<LayoutData>;
type LayoutRouteId = RouteId | "/" | "/login" | "/logout" | "/reports/[id]" | "/users" | null
type LayoutParams = RouteParams & { id?: string }
type LayoutServerParentData = EnsureDefined<{}>;
type LayoutParentData = EnsureDefined<{}>;
export type PageServerLoad<OutputData extends OutputDataShape<PageServerParentData> = OutputDataShape<PageServerParentData>> = Kit.ServerLoad<RouteParams, PageServerParentData, OutputData, RouteId>;
export type PageServerLoadEvent = Parameters<PageServerLoad>[0];
export type ActionData = unknown;
export type PageServerData = Expand<OptionalUnion<EnsureDefined<Kit.LoadProperties<Awaited<ReturnType<typeof import('./proxy+page.server.js').load>>>>>>;
export type PageData = Expand<Omit<PageParentData, keyof PageServerData> & EnsureDefined<PageServerData>>;
export type Action<OutputData extends Record<string, any> | void = Record<string, any> | void> = Kit.Action<RouteParams, OutputData, RouteId>
export type Actions<OutputData extends Record<string, any> | void = Record<string, any> | void> = Kit.Actions<RouteParams, OutputData, RouteId>
export type PageProps = { params: RouteParams; data: PageData; form: ActionData }
export type LayoutServerLoad<OutputData extends Partial<App.PageData> & Record<string, any> | void = Partial<App.PageData> & Record<string, any> | void> = Kit.ServerLoad<LayoutParams, LayoutServerParentData, OutputData, LayoutRouteId>;
export type LayoutServerLoadEvent = Parameters<LayoutServerLoad>[0];
export type LayoutServerData = Expand<OptionalUnion<EnsureDefined<Kit.LoadProperties<Awaited<ReturnType<typeof import('./proxy+layout.server.js').load>>>>>>;
export type LayoutData = Expand<Omit<LayoutParentData, keyof LayoutServerData> & EnsureDefined<LayoutServerData>>;
export type LayoutProps = { params: LayoutParams; data: LayoutData; children: import("svelte").Snippet }
export type RequestEvent = Kit.RequestEvent<RouteParams, RouteId>;

View File

@@ -0,0 +1,11 @@
import type * as Kit from '@sveltejs/kit';
type Expand<T> = T extends infer O ? { [K in keyof O]: O[K] } : never;
// @ts-ignore
type MatcherParam<M> = M extends (param : string) => param is infer U ? U extends string ? U : string : string;
type RouteParams = { id: string };
type RouteId = '/api/reports/[id]/download';
export type EntryGenerator = () => Promise<Array<RouteParams>> | Array<RouteParams>;
export type RequestHandler = Kit.RequestHandler<RouteParams, RouteId>;
export type RequestEvent = Kit.RequestEvent<RouteParams, RouteId>;

View File

@@ -0,0 +1,11 @@
import type * as Kit from '@sveltejs/kit';
type Expand<T> = T extends infer O ? { [K in keyof O]: O[K] } : never;
// @ts-ignore
type MatcherParam<M> = M extends (param : string) => param is infer U ? U extends string ? U : string : string;
type RouteParams = { id: string; fileId: string };
type RouteId = '/api/reports/[id]/files/[fileId]';
export type EntryGenerator = () => Promise<Array<RouteParams>> | Array<RouteParams>;
export type RequestHandler = Kit.RequestHandler<RouteParams, RouteId>;
export type RequestEvent = Kit.RequestEvent<RouteParams, RouteId>;

View File

@@ -0,0 +1,10 @@
import type * as Kit from '@sveltejs/kit';
type Expand<T> = T extends infer O ? { [K in keyof O]: O[K] } : never;
// @ts-ignore
type MatcherParam<M> = M extends (param : string) => param is infer U ? U extends string ? U : string : string;
type RouteParams = { };
type RouteId = '/api/reports/refresh';
export type RequestHandler = Kit.RequestHandler<RouteParams, RouteId>;
export type RequestEvent = Kit.RequestEvent<RouteParams, RouteId>;

View File

@@ -0,0 +1,31 @@
import type * as Kit from '@sveltejs/kit';
type Expand<T> = T extends infer O ? { [K in keyof O]: O[K] } : never;
// @ts-ignore
type MatcherParam<M> = M extends (param : string) => param is infer U ? U extends string ? U : string : string;
type RouteParams = { };
type RouteId = '/login';
type MaybeWithVoid<T> = {} extends T ? T | void : T;
export type RequiredKeys<T> = { [K in keyof T]-?: {} extends { [P in K]: T[K] } ? never : K; }[keyof T];
type OutputDataShape<T> = MaybeWithVoid<Omit<App.PageData, RequiredKeys<T>> & Partial<Pick<App.PageData, keyof T & keyof App.PageData>> & Record<string, any>>
type EnsureDefined<T> = T extends null | undefined ? {} : T;
type OptionalUnion<U extends Record<string, any>, A extends keyof U = U extends U ? keyof U : never> = U extends unknown ? { [P in Exclude<A, keyof U>]?: never } & U : never;
export type Snapshot<T = any> = Kit.Snapshot<T>;
type PageServerParentData = EnsureDefined<import('../$types.js').LayoutServerData>;
type PageParentData = EnsureDefined<import('../$types.js').LayoutData>;
export type PageServerLoad<OutputData extends OutputDataShape<PageServerParentData> = OutputDataShape<PageServerParentData>> = Kit.ServerLoad<RouteParams, PageServerParentData, OutputData, RouteId>;
export type PageServerLoadEvent = Parameters<PageServerLoad>[0];
type ExcludeActionFailure<T> = T extends Kit.ActionFailure<any> ? never : T extends void ? never : T;
type ActionsSuccess<T extends Record<string, (...args: any) => any>> = { [Key in keyof T]: ExcludeActionFailure<Awaited<ReturnType<T[Key]>>>; }[keyof T];
type ExtractActionFailure<T> = T extends Kit.ActionFailure<infer X> ? X extends void ? never : X : never;
type ActionsFailure<T extends Record<string, (...args: any) => any>> = { [Key in keyof T]: Exclude<ExtractActionFailure<Awaited<ReturnType<T[Key]>>>, void>; }[keyof T];
type ActionsExport = typeof import('./proxy+page.server.js').actions
export type SubmitFunction = Kit.SubmitFunction<Expand<ActionsSuccess<ActionsExport>>, Expand<ActionsFailure<ActionsExport>>>
export type ActionData = Expand<Kit.AwaitedActions<ActionsExport>> | null;
export type PageServerData = Expand<OptionalUnion<EnsureDefined<Kit.LoadProperties<Awaited<ReturnType<typeof import('./proxy+page.server.js').load>>>>>>;
export type PageData = Expand<Omit<PageParentData, keyof PageServerData> & EnsureDefined<PageServerData>>;
export type Action<OutputData extends Record<string, any> | void = Record<string, any> | void> = Kit.Action<RouteParams, OutputData, RouteId>
export type Actions<OutputData extends Record<string, any> | void = Record<string, any> | void> = Kit.Actions<RouteParams, OutputData, RouteId>
export type PageProps = { params: RouteParams; data: PageData; form: ActionData }
export type RequestEvent = Kit.RequestEvent<RouteParams, RouteId>;

View File

@@ -0,0 +1,61 @@
// @ts-nocheck
import type { Actions, PageServerLoad } from './$types';
import { fail, redirect } from '@sveltejs/kit';
import { verify } from '@node-rs/argon2';
import { lucia } from '$lib/server/auth';
import { db } from '$lib/server/db';
import { userTable } from '$lib/schema';
import { eq } from 'drizzle-orm';
export const load = async ({ locals }: Parameters<PageServerLoad>[0]) => {
if (locals.user) {
redirect(302, '/');
}
};
export const actions = {
default: async ({ request, cookies }: import('./$types').RequestEvent) => {
const formData = await request.formData();
const username = formData.get('username');
const password = formData.get('password');
if (typeof username !== 'string' || typeof password !== 'string') {
return fail(400, { message: 'Invalid input' });
}
if (!username || !password) {
return fail(400, { message: 'Username and password are required' });
}
const [user] = await db
.select()
.from(userTable)
.where(eq(userTable.username, username))
.limit(1);
if (!user) {
return fail(400, { message: 'Invalid username or password' });
}
const validPassword = await verify(user.passwordHash, password, {
memoryCost: 19456,
timeCost: 2,
outputLen: 32,
parallelism: 1
});
if (!validPassword) {
return fail(400, { message: 'Invalid username or password' });
}
const session = await lucia.createSession(user.id, {});
const sessionCookie = lucia.createSessionCookie(session.id);
cookies.set(sessionCookie.name, sessionCookie.value, {
path: '.',
...sessionCookie.attributes
});
redirect(302, '/');
}
};
;null as any as Actions;

View File

@@ -0,0 +1,31 @@
import type * as Kit from '@sveltejs/kit';
type Expand<T> = T extends infer O ? { [K in keyof O]: O[K] } : never;
// @ts-ignore
type MatcherParam<M> = M extends (param : string) => param is infer U ? U extends string ? U : string : string;
type RouteParams = { };
type RouteId = '/logout';
type MaybeWithVoid<T> = {} extends T ? T | void : T;
export type RequiredKeys<T> = { [K in keyof T]-?: {} extends { [P in K]: T[K] } ? never : K; }[keyof T];
type OutputDataShape<T> = MaybeWithVoid<Omit<App.PageData, RequiredKeys<T>> & Partial<Pick<App.PageData, keyof T & keyof App.PageData>> & Record<string, any>>
type EnsureDefined<T> = T extends null | undefined ? {} : T;
type OptionalUnion<U extends Record<string, any>, A extends keyof U = U extends U ? keyof U : never> = U extends unknown ? { [P in Exclude<A, keyof U>]?: never } & U : never;
export type Snapshot<T = any> = Kit.Snapshot<T>;
type PageServerParentData = EnsureDefined<import('../$types.js').LayoutServerData>;
type PageParentData = EnsureDefined<import('../$types.js').LayoutData>;
export type PageServerLoad<OutputData extends OutputDataShape<PageServerParentData> = OutputDataShape<PageServerParentData>> = Kit.ServerLoad<RouteParams, PageServerParentData, OutputData, RouteId>;
export type PageServerLoadEvent = Parameters<PageServerLoad>[0];
type ExcludeActionFailure<T> = T extends Kit.ActionFailure<any> ? never : T extends void ? never : T;
type ActionsSuccess<T extends Record<string, (...args: any) => any>> = { [Key in keyof T]: ExcludeActionFailure<Awaited<ReturnType<T[Key]>>>; }[keyof T];
type ExtractActionFailure<T> = T extends Kit.ActionFailure<infer X> ? X extends void ? never : X : never;
type ActionsFailure<T extends Record<string, (...args: any) => any>> = { [Key in keyof T]: Exclude<ExtractActionFailure<Awaited<ReturnType<T[Key]>>>, void>; }[keyof T];
type ActionsExport = typeof import('./proxy+page.server.js').actions
export type SubmitFunction = Kit.SubmitFunction<Expand<ActionsSuccess<ActionsExport>>, Expand<ActionsFailure<ActionsExport>>>
export type ActionData = Expand<Kit.AwaitedActions<ActionsExport>> | null;
export type PageServerData = Expand<OptionalUnion<EnsureDefined<Kit.LoadProperties<Awaited<ReturnType<typeof import('./proxy+page.server.js').load>>>>>>;
export type PageData = Expand<Omit<PageParentData, keyof PageServerData> & EnsureDefined<PageServerData>>;
export type Action<OutputData extends Record<string, any> | void = Record<string, any> | void> = Kit.Action<RouteParams, OutputData, RouteId>
export type Actions<OutputData extends Record<string, any> | void = Record<string, any> | void> = Kit.Actions<RouteParams, OutputData, RouteId>
export type PageProps = { params: RouteParams; data: PageData; form: ActionData }
export type RequestEvent = Kit.RequestEvent<RouteParams, RouteId>;

View File

@@ -0,0 +1,26 @@
// @ts-nocheck
import type { Actions, PageServerLoad } from './$types';
import { redirect } from '@sveltejs/kit';
import { lucia } from '$lib/server/auth';
export const load = async () => {
redirect(302, '/');
};
export const actions = {
default: async ({ locals, cookies }: import('./$types').RequestEvent) => {
if (!locals.session) {
redirect(302, '/login');
}
await lucia.invalidateSession(locals.session.id);
const sessionCookie = lucia.createBlankSessionCookie();
cookies.set(sessionCookie.name, sessionCookie.value, {
path: '.',
...sessionCookie.attributes
});
redirect(302, '/login');
}
};
;null as any as PageServerLoad;;null as any as Actions;

View File

@@ -0,0 +1,26 @@
// @ts-nocheck
import type { LayoutServerLoad } from './$types';
import { redirect } from '@sveltejs/kit';
import { db } from '$lib/server/db';
import { bugReports } from '$lib/schema';
import { eq, count } from 'drizzle-orm';
export const load = async ({ locals, url }: Parameters<LayoutServerLoad>[0]) => {
if (url.pathname === '/login') {
return { newCount: 0, user: null };
}
if (!locals.user) {
redirect(302, '/login');
}
const [result] = await db
.select({ count: count() })
.from(bugReports)
.where(eq(bugReports.status, 'new'));
return {
newCount: result.count,
user: locals.user
};
};

View File

@@ -0,0 +1,71 @@
// @ts-nocheck
import type { PageServerLoad } from './$types';
import { db } from '$lib/server/db';
import { bugReports, bugReportFiles } from '$lib/schema';
import { eq, like, or, count, sql, desc, and } from 'drizzle-orm';
export const load = async ({ url }: Parameters<PageServerLoad>[0]) => {
const page = Math.max(1, Number(url.searchParams.get('page')) || 1);
const pageSize = Math.min(50, Math.max(10, Number(url.searchParams.get('pageSize')) || 20));
const status = url.searchParams.get('status') || '';
const search = url.searchParams.get('search') || '';
const conditions = [];
if (status && ['new', 'in_review', 'resolved', 'closed'].includes(status)) {
conditions.push(eq(bugReports.status, status as 'new' | 'in_review' | 'resolved' | 'closed'));
}
if (search) {
conditions.push(
or(
like(bugReports.hostname, `%${search}%`),
like(bugReports.os_user, `%${search}%`),
like(bugReports.name, `%${search}%`),
like(bugReports.email, `%${search}%`)
)
);
}
const where = conditions.length > 0 ? and(...conditions) : undefined;
// Get total count
const [{ total }] = await db
.select({ total: count() })
.from(bugReports)
.where(where);
// Get paginated reports with file count
const reports = await db
.select({
id: bugReports.id,
name: bugReports.name,
email: bugReports.email,
hostname: bugReports.hostname,
os_user: bugReports.os_user,
status: bugReports.status,
created_at: bugReports.created_at,
file_count: count(bugReportFiles.id)
})
.from(bugReports)
.leftJoin(bugReportFiles, eq(bugReports.id, bugReportFiles.report_id))
.where(where)
.groupBy(bugReports.id)
.orderBy(desc(bugReports.created_at))
.limit(pageSize)
.offset((page - 1) * pageSize);
return {
reports,
pagination: {
page,
pageSize,
total,
totalPages: Math.ceil(total / pageSize)
},
filters: {
status,
search
}
};
};

View File

@@ -0,0 +1,27 @@
import type * as Kit from '@sveltejs/kit';
type Expand<T> = T extends infer O ? { [K in keyof O]: O[K] } : never;
// @ts-ignore
type MatcherParam<M> = M extends (param : string) => param is infer U ? U extends string ? U : string : string;
type RouteParams = { id: string };
type RouteId = '/reports/[id]';
type MaybeWithVoid<T> = {} extends T ? T | void : T;
export type RequiredKeys<T> = { [K in keyof T]-?: {} extends { [P in K]: T[K] } ? never : K; }[keyof T];
type OutputDataShape<T> = MaybeWithVoid<Omit<App.PageData, RequiredKeys<T>> & Partial<Pick<App.PageData, keyof T & keyof App.PageData>> & Record<string, any>>
type EnsureDefined<T> = T extends null | undefined ? {} : T;
type OptionalUnion<U extends Record<string, any>, A extends keyof U = U extends U ? keyof U : never> = U extends unknown ? { [P in Exclude<A, keyof U>]?: never } & U : never;
export type Snapshot<T = any> = Kit.Snapshot<T>;
type PageServerParentData = EnsureDefined<import('../../$types.js').LayoutServerData>;
type PageParentData = EnsureDefined<import('../../$types.js').LayoutData>;
export type EntryGenerator = () => Promise<Array<RouteParams>> | Array<RouteParams>;
export type PageServerLoad<OutputData extends OutputDataShape<PageServerParentData> = OutputDataShape<PageServerParentData>> = Kit.ServerLoad<RouteParams, PageServerParentData, OutputData, RouteId>;
export type PageServerLoadEvent = Parameters<PageServerLoad>[0];
export type ActionData = unknown;
export type PageServerData = Expand<OptionalUnion<EnsureDefined<Kit.LoadProperties<Awaited<ReturnType<typeof import('./proxy+page.server.js').load>>>>>>;
export type PageData = Expand<Omit<PageParentData, keyof PageServerData> & EnsureDefined<PageServerData>>;
export type Action<OutputData extends Record<string, any> | void = Record<string, any> | void> = Kit.Action<RouteParams, OutputData, RouteId>
export type Actions<OutputData extends Record<string, any> | void = Record<string, any> | void> = Kit.Actions<RouteParams, OutputData, RouteId>
export type PageProps = { params: RouteParams; data: PageData; form: ActionData }
export type RequestHandler = Kit.RequestHandler<RouteParams, RouteId>;
export type RequestEvent = Kit.RequestEvent<RouteParams, RouteId>;

View File

@@ -0,0 +1,45 @@
// @ts-nocheck
import type { PageServerLoad } from './$types';
import { db } from '$lib/server/db';
import { bugReports, bugReportFiles } from '$lib/schema';
import { eq } from 'drizzle-orm';
import { error } from '@sveltejs/kit';
export const load = async ({ params }: Parameters<PageServerLoad>[0]) => {
const id = Number(params.id);
if (isNaN(id)) throw error(400, 'Invalid report ID');
const [report] = await db
.select()
.from(bugReports)
.where(eq(bugReports.id, id))
.limit(1);
if (!report) throw error(404, 'Report not found');
const files = await db
.select({
id: bugReportFiles.id,
report_id: bugReportFiles.report_id,
file_role: bugReportFiles.file_role,
filename: bugReportFiles.filename,
mime_type: bugReportFiles.mime_type,
file_size: bugReportFiles.file_size,
created_at: bugReportFiles.created_at
})
.from(bugReportFiles)
.where(eq(bugReportFiles.report_id, id));
return {
report: {
...report,
system_info: report.system_info ? JSON.stringify(report.system_info, null, 2) : null,
created_at: report.created_at.toISOString(),
updated_at: report.updated_at.toISOString()
},
files: files.map((f) => ({
...f,
created_at: f.created_at.toISOString()
}))
};
};

View File

@@ -0,0 +1,31 @@
import type * as Kit from '@sveltejs/kit';
type Expand<T> = T extends infer O ? { [K in keyof O]: O[K] } : never;
// @ts-ignore
type MatcherParam<M> = M extends (param : string) => param is infer U ? U extends string ? U : string : string;
type RouteParams = { };
type RouteId = '/users';
type MaybeWithVoid<T> = {} extends T ? T | void : T;
export type RequiredKeys<T> = { [K in keyof T]-?: {} extends { [P in K]: T[K] } ? never : K; }[keyof T];
type OutputDataShape<T> = MaybeWithVoid<Omit<App.PageData, RequiredKeys<T>> & Partial<Pick<App.PageData, keyof T & keyof App.PageData>> & Record<string, any>>
type EnsureDefined<T> = T extends null | undefined ? {} : T;
type OptionalUnion<U extends Record<string, any>, A extends keyof U = U extends U ? keyof U : never> = U extends unknown ? { [P in Exclude<A, keyof U>]?: never } & U : never;
export type Snapshot<T = any> = Kit.Snapshot<T>;
type PageServerParentData = EnsureDefined<import('../$types.js').LayoutServerData>;
type PageParentData = EnsureDefined<import('../$types.js').LayoutData>;
export type PageServerLoad<OutputData extends OutputDataShape<PageServerParentData> = OutputDataShape<PageServerParentData>> = Kit.ServerLoad<RouteParams, PageServerParentData, OutputData, RouteId>;
export type PageServerLoadEvent = Parameters<PageServerLoad>[0];
type ExcludeActionFailure<T> = T extends Kit.ActionFailure<any> ? never : T extends void ? never : T;
type ActionsSuccess<T extends Record<string, (...args: any) => any>> = { [Key in keyof T]: ExcludeActionFailure<Awaited<ReturnType<T[Key]>>>; }[keyof T];
type ExtractActionFailure<T> = T extends Kit.ActionFailure<infer X> ? X extends void ? never : X : never;
type ActionsFailure<T extends Record<string, (...args: any) => any>> = { [Key in keyof T]: Exclude<ExtractActionFailure<Awaited<ReturnType<T[Key]>>>, void>; }[keyof T];
type ActionsExport = typeof import('./proxy+page.server.js').actions
export type SubmitFunction = Kit.SubmitFunction<Expand<ActionsSuccess<ActionsExport>>, Expand<ActionsFailure<ActionsExport>>>
export type ActionData = Expand<Kit.AwaitedActions<ActionsExport>> | null;
export type PageServerData = Expand<OptionalUnion<EnsureDefined<Kit.LoadProperties<Awaited<ReturnType<typeof import('./proxy+page.server.js').load>>>>>>;
export type PageData = Expand<Omit<PageParentData, keyof PageServerData> & EnsureDefined<PageServerData>>;
export type Action<OutputData extends Record<string, any> | void = Record<string, any> | void> = Kit.Action<RouteParams, OutputData, RouteId>
export type Actions<OutputData extends Record<string, any> | void = Record<string, any> | void> = Kit.Actions<RouteParams, OutputData, RouteId>
export type PageProps = { params: RouteParams; data: PageData; form: ActionData }
export type RequestEvent = Kit.RequestEvent<RouteParams, RouteId>;

View File

@@ -0,0 +1,217 @@
// @ts-nocheck
import type { Actions, PageServerLoad } from './$types';
import { fail, redirect } from '@sveltejs/kit';
import { hash } from '@node-rs/argon2';
import { generateIdFromEntropySize } from 'lucia';
import { db } from '$lib/server/db';
import { userTable } from '$lib/schema';
import { eq } from 'drizzle-orm';
const PASSWORD_MIN_LENGTH = 8;
const PASSWORD_MAX_LENGTH = 255;
function validatePassword(password: string): string | null {
if (password.length < PASSWORD_MIN_LENGTH || password.length > PASSWORD_MAX_LENGTH) {
return `Password must be ${PASSWORD_MIN_LENGTH}-${PASSWORD_MAX_LENGTH} characters`;
}
if (!/[A-Z]/.test(password)) {
return 'Password must contain at least one uppercase letter';
}
if (!/[a-z]/.test(password)) {
return 'Password must contain at least one lowercase letter';
}
if (!/[0-9]/.test(password)) {
return 'Password must contain at least one number';
}
if (!/[^A-Za-z0-9]/.test(password)) {
return 'Password must contain at least one special character';
}
return null;
}
async function hashPassword(password: string): Promise<string> {
return hash(password, {
memoryCost: 19456,
timeCost: 2,
outputLen: 32,
parallelism: 1
});
}
export const load = async ({ locals }: Parameters<PageServerLoad>[0]) => {
if (!locals.user || locals.user.role !== 'admin') {
redirect(302, '/');
}
const users = await db
.select({
id: userTable.id,
username: userTable.username,
displayname: userTable.displayname,
role: userTable.role,
createdAt: userTable.createdAt
})
.from(userTable)
.orderBy(userTable.createdAt);
return { users };
};
export const actions = {
create: async ({ request, locals }: import('./$types').RequestEvent) => {
if (!locals.user || locals.user.role !== 'admin') {
return fail(403, { message: 'Unauthorized' });
}
const formData = await request.formData();
const username = formData.get('username');
const displayname = formData.get('displayname') || '';
const password = formData.get('password');
const confirmPassword = formData.get('confirmPassword');
const role = formData.get('role');
if (
typeof username !== 'string' ||
typeof displayname !== 'string' ||
typeof password !== 'string' ||
typeof confirmPassword !== 'string' ||
typeof role !== 'string'
) {
return fail(400, { message: 'Invalid input' });
}
if (!username || !password) {
return fail(400, { message: 'Username and password are required' });
}
if (username.length < 3 || username.length > 255) {
return fail(400, { message: 'Username must be 3-255 characters' });
}
if (password !== confirmPassword) {
return fail(400, { message: 'Passwords do not match' });
}
const passwordError = validatePassword(password);
if (passwordError) {
return fail(400, { message: passwordError });
}
if (role !== 'admin' && role !== 'user') {
return fail(400, { message: 'Invalid role' });
}
// Check if username already exists
const [existing] = await db
.select({ id: userTable.id })
.from(userTable)
.where(eq(userTable.username, username))
.limit(1);
if (existing) {
return fail(400, { message: 'Username already exists' });
}
const passwordHash = await hashPassword(password);
const userId = generateIdFromEntropySize(10);
await db.insert(userTable).values({
id: userId,
username,
displayname,
passwordHash,
role: role as 'admin' | 'user'
});
return { success: true };
},
updateDisplayname: async ({ request, locals }: import('./$types').RequestEvent) => {
if (!locals.user || locals.user.role !== 'admin') {
return fail(403, { message: 'Unauthorized' });
}
const formData = await request.formData();
const userId = formData.get('userId');
const displayname = formData.get('displayname');
if (typeof userId !== 'string' || typeof displayname !== 'string') {
return fail(400, { message: 'Invalid input' });
}
await db.update(userTable).set({ displayname }).where(eq(userTable.id, userId));
return { success: true };
},
resetPassword: async ({ request, locals }: import('./$types').RequestEvent) => {
if (!locals.user || locals.user.role !== 'admin') {
return fail(403, { message: 'Unauthorized' });
}
const formData = await request.formData();
const userId = formData.get('userId');
if (typeof userId === 'string' && userId === locals.user.id) {
return fail(400, { message: 'Cannot reset your own password from here' });
}
const newPassword = formData.get('newPassword');
const confirmPassword = formData.get('confirmPassword');
if (
typeof userId !== 'string' ||
typeof newPassword !== 'string' ||
typeof confirmPassword !== 'string'
) {
return fail(400, { message: 'Invalid input' });
}
if (newPassword !== confirmPassword) {
return fail(400, { message: 'Passwords do not match' });
}
const passwordError = validatePassword(newPassword);
if (passwordError) {
return fail(400, { message: passwordError });
}
const passwordHash = await hashPassword(newPassword);
await db.update(userTable).set({ passwordHash }).where(eq(userTable.id, userId));
return { success: true };
},
delete: async ({ request, locals }: import('./$types').RequestEvent) => {
if (!locals.user || locals.user.role !== 'admin') {
return fail(403, { message: 'Unauthorized' });
}
const formData = await request.formData();
const userId = formData.get('userId');
if (typeof userId !== 'string') {
return fail(400, { message: 'Invalid input' });
}
if (userId === locals.user.id) {
return fail(400, { message: 'Cannot delete your own account' });
}
// Prevent deleting admin users
const [targetUser] = await db
.select({ role: userTable.role })
.from(userTable)
.where(eq(userTable.id, userId))
.limit(1);
if (targetUser?.role === 'admin') {
return fail(400, { message: 'Cannot delete an admin user' });
}
await db.delete(userTable).where(eq(userTable.id, userId));
return { success: true };
}
};
;null as any as Actions;