v3: added a roles/permissions system, added user management tab on user profile
All checks were successful
Build and Publish Docker Image / build-and-push (push) Successful in 42s

This commit is contained in:
khannurien
2026-06-28 06:21:23 +00:00
parent b567e390d9
commit 810faaf21a
22 changed files with 341 additions and 60 deletions

View File

@@ -3,10 +3,12 @@ import { DatabaseSync, type SQLOutputValue } from "node:sqlite";
import {
type Comment,
Dump,
isRole,
type Notification,
type NotificationType,
type Playlist,
type RichContent,
type Role,
type User,
} from "./interfaces.ts";
import {
@@ -53,7 +55,7 @@ if (userCount.count === 0) {
const hash = scryptSync("admin", salt, 64).toString("hex");
const passwordHash = `${hash}.${salt}`;
db.prepare(
`INSERT INTO users (id, username, password_hash, is_admin, created_at, email) VALUES (?, 'admin', ?, 1, datetime('now'), 'admin@localhost')`,
`INSERT INTO users (id, username, password_hash, role, created_at, email) VALUES (?, 'admin', ?, 'admin', datetime('now'), 'admin@localhost')`,
).run(crypto.randomUUID(), passwordHash);
console.log("Created default admin user (username: admin, password: admin)");
}
@@ -101,7 +103,7 @@ export interface UserRow {
id: string;
username: string;
password_hash: string;
is_admin: number;
role: Role;
created_at: string;
updated_at: string | null;
avatar_mime: string | null;
@@ -153,7 +155,7 @@ export function isUserRow(obj: unknown): obj is UserRow {
"id" in obj && typeof obj.id === "string" &&
"username" in obj && typeof obj.username === "string" &&
"password_hash" in obj && typeof obj.password_hash === "string" &&
"is_admin" in obj && typeof obj.is_admin === "number" &&
"role" in obj && isRole(obj.role) &&
"created_at" in obj && typeof obj.created_at === "string" &&
"updated_at" in obj &&
(typeof obj.updated_at === "string" || obj.updated_at === null) &&
@@ -221,7 +223,7 @@ export function userRowToApi(row: UserRow): User {
id: row.id,
username: row.username,
passwordHash: row.password_hash,
isAdmin: Boolean(row.is_admin),
role: row.role,
createdAt: new Date(row.created_at),
updatedAt: row.updated_at ? new Date(row.updated_at) : undefined,
avatarMime: row.avatar_mime ?? undefined,
@@ -238,7 +240,7 @@ export function userApiToRow(user: User): UserRow {
id: user.id,
username: user.username,
password_hash: user.passwordHash,
is_admin: user.isAdmin ? 1 : 0,
role: user.role,
created_at: user.createdAt.toISOString(),
updated_at: user.updatedAt?.toISOString() ?? null,
avatar_mime: user.avatarMime ?? null,

View File

@@ -39,11 +39,19 @@ export interface Dump {
* Authentication
*/
export type Role = "user" | "moderator" | "admin";
export const ROLES: readonly Role[] = ["user", "moderator", "admin"];
export function isRole(value: unknown): value is Role {
return typeof value === "string" && (ROLES as readonly string[]).includes(value);
}
export interface User {
id: string;
username: string;
passwordHash: string;
isAdmin: boolean;
role: Role;
createdAt: Date;
updatedAt?: Date;
avatarMime?: string;
@@ -67,7 +75,7 @@ export interface RegisterUserRequest {
export interface UpdateUserRequest {
username?: string;
password?: string;
isAdmin?: boolean;
role?: Role;
description?: string | null;
email?: string;
}
@@ -128,7 +136,7 @@ export function isUpdateUserRequest(obj: unknown): obj is UpdateUserRequest {
return false;
}
}
if ("isAdmin" in o && typeof o.isAdmin !== "boolean") return false;
if ("role" in o && !isRole(o.role)) return false;
if (
"description" in o && typeof o.description !== "string" &&
o.description !== null
@@ -152,7 +160,7 @@ export interface AuthResponse {
export interface AuthPayload {
userId: string;
username: string;
isAdmin: boolean;
role: Role;
exp: number;
}
@@ -161,7 +169,7 @@ export function isAuthPayload(obj: unknown): obj is AuthPayload {
typeof obj === "object" &&
"userId" in obj && typeof obj.userId === "string" &&
"username" in obj && typeof obj.username === "string" &&
"isAdmin" in obj && typeof obj.isAdmin === "boolean" &&
"role" in obj && isRole(obj.role) &&
"exp" in obj && typeof obj.exp === "number";
}