Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 49 additions & 19 deletions src/db/db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,40 +4,70 @@ import { Database } from "bun:sqlite";
mkdirSync("./data", { recursive: true });
const db = new Database("./data/mydb.sqlite", { create: true });

if (!db.query("SELECT * FROM sqlite_master WHERE type='table'").get()) {
// Always keep foreign keys on (SQLite defaults to off).
db.exec("PRAGMA foreign_keys = ON;");

const hasAnyTable = db.query("SELECT 1 FROM sqlite_master WHERE type='table' LIMIT 1").get();
if (!hasAnyTable) {
db.exec(`
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
email TEXT NOT NULL,
password TEXT NOT NULL
id INTEGER PRIMARY KEY AUTOINCREMENT,
email TEXT NOT NULL UNIQUE,
password TEXT NOT NULL,
role TEXT NOT NULL DEFAULT 'user'
);

CREATE TABLE IF NOT EXISTS jobs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
date_created TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'queued',
num_files INTEGER NOT NULL DEFAULT 0,
finished_files INTEGER NOT NULL DEFAULT 0,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);

CREATE TABLE IF NOT EXISTS file_names (
id INTEGER PRIMARY KEY AUTOINCREMENT,
job_id INTEGER NOT NULL,
file_name TEXT NOT NULL,
output_file_name TEXT NOT NULL,
status TEXT DEFAULT 'not started',
FOREIGN KEY (job_id) REFERENCES jobs(id)
);
CREATE TABLE IF NOT EXISTS jobs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
date_created TEXT NOT NULL,
status TEXT DEFAULT 'not started',
num_files INTEGER DEFAULT 0,
FOREIGN KEY (user_id) REFERENCES users(id)
status TEXT NOT NULL DEFAULT 'queued',
FOREIGN KEY (job_id) REFERENCES jobs(id) ON DELETE CASCADE
);
PRAGMA user_version = 1;`);

PRAGMA user_version = 1;
`);
}

const dbVersion = (db.query("PRAGMA user_version").get() as { user_version?: number }).user_version;
if (dbVersion === 0) {
db.exec("ALTER TABLE file_names ADD COLUMN status TEXT DEFAULT 'not started';");

const dbVersion = (db.query("PRAGMA user_version").get() as { user_version?: number })
.user_version;

if ((dbVersion ?? 0) === 0) {
db.exec("PRAGMA user_version = 1;");
console.log("Updated database to version 1.");
}

// enable WAL mode
const userColumns = db.query("PRAGMA table_info(users)").all() as { name: string }[];
const hasRoleColumn = userColumns.some((col) => col.name === "role");

if (!hasRoleColumn) {
db.exec("ALTER TABLE users ADD COLUMN role TEXT NOT NULL DEFAULT 'user';");

const oldest = db
.query("SELECT id FROM users ORDER BY id ASC LIMIT 1")
.get() as { id: number } | null;

if (oldest) {
db.query("UPDATE users SET role = 'admin' WHERE id = ?").run(oldest.id);
console.log("Added 'role' column; promoted oldest existing user to admin (Policy A).");
} else {
console.log("Added 'role' column to users table (no users to promote).");
}
}

// enable WAL mode (better concurrency for Bun + SQLite)
db.exec("PRAGMA journal_mode = WAL;");

export default db;
1 change: 1 addition & 0 deletions src/db/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,5 @@ export class User {
id!: number;
email!: string;
password!: string;
role!: string; // 'admin' | 'user'
}
30 changes: 27 additions & 3 deletions src/pages/root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,18 +30,22 @@ export const root = new Elysia().use(userService).get(
}

// validate jwt
let user: ({ id: string } & JWTPayloadSpec) | false = false;
let user: ({ id: string; role: string } & JWTPayloadSpec) | false = false;

if (ALLOW_UNAUTHENTICATED) {
const newUserId = String(
UNAUTHENTICATED_USER_SHARING
? 0
: randomInt(2 ** 24, Math.min(2 ** 48 + 2 ** 24 - 1, Number.MAX_SAFE_INTEGER)),
);

const accessToken = await jwt.sign({
id: newUserId,
role: "user",
});

user = { id: newUserId };
user = { id: newUserId, role: "user" };

if (!auth) {
return {
message: "No auth cookie, perhaps your browser is blocking cookies.",
Expand All @@ -57,7 +61,24 @@ export const root = new Elysia().use(userService).get(
sameSite: "strict",
});
} else if (auth?.value) {
user = await jwt.verify(auth.value);
const verified = await jwt.verify(auth.value);

// If verification fails, keep user as false
if (verified === false) {
user = false;
} else {
// Ensure role is present (defensive, for older tokens)
const verifiedUser = verified as { id?: string; role?: string } & JWTPayloadSpec;

if (!verifiedUser.id) {
user = false;
} else if (!verifiedUser.role) {
// fallback: treat as normal user if role missing
user = { ...verifiedUser, id: verifiedUser.id, role: "user" };
} else {
user = verifiedUser as { id: string; role: string } & JWTPayloadSpec;
}
}

if (
user !== false &&
Expand All @@ -73,6 +94,9 @@ export const root = new Elysia().use(userService).get(
}
return redirect(`${WEBROOT}/login`, 302);
}

// Optional: if you want DB to be the source of truth for role, uncomment below:
// user = { ...user, role: existingUser.role ?? "user" };
}
}

Expand Down
Loading