diff --git a/db.ts b/db.ts index 9219b08..2f632d8 100644 --- a/db.ts +++ b/db.ts @@ -127,7 +127,7 @@ export type EhFileMetaRaw = { is_nsfw: number; is_ad: number; }; -export enum UserPermisson { +export enum UserPermission { None = 0, ReadGallery = 1 << 0, EditGallery = 1 << 1, @@ -138,14 +138,14 @@ export type User = { username: string; password: Uint8Array; is_admin: boolean; - permissions: UserPermisson; + permissions: UserPermission; }; type UserRaw = { id: number; username: string; password: Uint8Array; is_admin: number; - permissions: UserPermisson; + permissions: UserPermission; }; export type Token = { id: number; @@ -537,6 +537,15 @@ export class EhDb { pmeta, ); } + add_root_user(username: string, password: Uint8Array) { + this.db.query("INSERT OR REPLACE INTO user VALUES (?, ?, ?, ?, ?);", [ + 0, + username, + password, + true, + UserPermission.All, + ]); + } add_task(task: Task) { return this.transaction(() => { this.db.query( @@ -585,6 +594,33 @@ export class EhDb { if (!t) throw Error("Failed to add token."); return t; } + add_user(user: User) { + if (user.id === 0) { + this.db.query( + "INSERT INTO user (username, password, is_admin, permissions) VALUES (?, ?, ?, ?);", + [ + user.username, + user.password, + user.is_admin, + user.permissions, + ], + ); + } else { + this.db.query( + "INSERT OR REPLACE INTO user VALUES (?, ?, ?, ?, ?);", + [ + user.id, + user.username, + user.password, + user.is_admin, + user.permissions, + ], + ); + } + const u = this.get_user_by_name(user.username); + if (!u) throw Error("Failed to add/update user."); + return u; + } begin(type: SqliteTransactionType) { try { this.db.execute(`BEGIN ${type} TRANSACTION;`); @@ -746,7 +782,7 @@ export class EhDb { const a = m.is_admin !== 0; const t = m; t.is_admin = a; - if (t.is_admin) t.permissions = UserPermisson.All; + if (t.is_admin) t.permissions = UserPermission.All; return t; }); } @@ -784,6 +820,9 @@ export class EhDb { this.db.query("DELETE FROM task WHERE id = ?;", [task.id]); }); } + delete_token(token: string) { + this.db.query("DELETE FROM token WHERE token = ?;", [token]); + } async flock() { if (!this.#file) return; await eval(`Deno.flock(${this.#file.rid}, true);`); @@ -969,6 +1008,15 @@ export class EhDb { ); return s.length ? s[0] : undefined; } + get_user(id: number) { + const s = this.convert_user( + this.db.queryEntries( + "SELECT * FROM user WHERE id = ?;", + [id], + ), + ); + return s.length ? s[0] : undefined; + } get_user_by_name(name: string) { const s = this.convert_user( this.db.queryEntries( diff --git a/fresh.gen.ts b/fresh.gen.ts index 6ec56f8..699af60 100644 --- a/fresh.gen.ts +++ b/fresh.gen.ts @@ -15,11 +15,12 @@ import * as $9 from "./routes/api/filemeta/[token].ts"; import * as $10 from "./routes/api/files/[token].ts"; import * as $11 from "./routes/api/gallery/[gid].ts"; import * as $12 from "./routes/api/gallery/list.ts"; -import * as $13 from "./routes/api/login.ts"; -import * as $14 from "./routes/api/status.ts"; -import * as $15 from "./routes/api/task.ts"; -import * as $16 from "./routes/api/thumbnail/[id].ts"; -import * as $17 from "./routes/index.tsx"; +import * as $13 from "./routes/api/status.ts"; +import * as $14 from "./routes/api/task.ts"; +import * as $15 from "./routes/api/thumbnail/[id].ts"; +import * as $16 from "./routes/api/token.ts"; +import * as $17 from "./routes/api/user.ts"; +import * as $18 from "./routes/index.tsx"; import * as $$0 from "./islands/Container.tsx"; import * as $$1 from "./islands/Settings.tsx"; import * as $$2 from "./islands/TaskManager.tsx"; @@ -39,11 +40,12 @@ const manifest = { "./routes/api/files/[token].ts": $10, "./routes/api/gallery/[gid].ts": $11, "./routes/api/gallery/list.ts": $12, - "./routes/api/login.ts": $13, - "./routes/api/status.ts": $14, - "./routes/api/task.ts": $15, - "./routes/api/thumbnail/[id].ts": $16, - "./routes/index.tsx": $17, + "./routes/api/status.ts": $13, + "./routes/api/task.ts": $14, + "./routes/api/thumbnail/[id].ts": $15, + "./routes/api/token.ts": $16, + "./routes/api/user.ts": $17, + "./routes/index.tsx": $18, }, islands: { "./islands/Container.tsx": $$0, diff --git a/routes/api/_middleware.ts b/routes/api/_middleware.ts index 1d0ac85..3a7ed35 100644 --- a/routes/api/_middleware.ts +++ b/routes/api/_middleware.ts @@ -1,6 +1,39 @@ import { MiddlewareHandlerContext } from "$fresh/server.ts"; +import { get_task_manager } from "../../server.ts"; +import { parse_cookies } from "../../server/cookies.ts"; +import { return_error } from "../../server/utils.ts"; + +function handle_auth(req: Request, ctx: MiddlewareHandlerContext) { + if (req.method === "OPTIONS") return true; + const m = get_task_manager(); + if (m.db.get_user_count() === 0) return true; + const u = new URL(req.url); + let token: string | null | undefined = req.headers.get("X-TOKEN"); + const cookies = parse_cookies(req.headers.get("Cookies")); + if (!token) { + token = cookies.get("token"); + } + if (!token) { + if (u.pathname === "/api/token" && req.method === "PUT") return true; + if (u.pathname === "/api/status" && req.method === "GET") return true; + return false; + } + const t = m.db.get_token(token); + const now = (new Date()).getTime(); + if (!t || t.expired.getTime() < now) return false; + const user = m.db.get_user(t.uid); + if (!user) { + m.db.delete_token(token); + return false; + } + ctx.state.user = user; + return true; +} export async function handler(req: Request, ctx: MiddlewareHandlerContext) { + if (!handle_auth(req, ctx)) { + return return_error(401, "Unauthorized"); + } const res = await ctx.next(); if (req.method === "OPTIONS" && res.status === 405) { const headers = new Headers(); diff --git a/routes/api/login.ts b/routes/api/token.ts similarity index 59% rename from routes/api/login.ts rename to routes/api/token.ts index 8a3e6c9..7a71e74 100644 --- a/routes/api/login.ts +++ b/routes/api/token.ts @@ -9,7 +9,34 @@ import isEqual from "lodash/isEqual"; const USER_PASSWORD_ERROR = "Incorrect username or password."; export const handler: Handlers = { - async POST(req, _ctx) { + async DELETE(req, _ctx) { + const data = await req.formData(); + const t = await get_string(data.get("token")); + if (!t) return return_error(1, "token not specified."); + const m = get_task_manager(); + const token = m.db.get_token(t); + if (!token) return return_error(404, "token not found."); + m.db.delete_token(t); + return return_data(true); + }, + GET(req, _ctx) { + const u = new URL(req.url); + const t = u.searchParams.get("token"); + if (!t) return return_error(1, "token not specififed."); + const m = get_task_manager(); + const token = m.db.get_token(t); + if (!token) return return_error(404, "token not found."); + const user = m.db.get_user(token.uid); + m.db.delete_token(t); + if (!user) return return_error(404, "user not found."); + return return_data({ + token, + name: user.username, + is_admin: user.is_admin, + permissions: user.permissions, + }); + }, + async PUT(req, _ctx) { const data = await req.formData(); const username = await get_string(data.get("username")); if (!username) return return_error(1, "username not specified."); @@ -34,12 +61,12 @@ export const handler: Handlers = { const u = m.db.get_user_by_name(username); if (!u) return return_error(4, USER_PASSWORD_ERROR); const pa = new Uint8Array( - await pbkdf2Hmac(u.password, t.toString(), 1000, 64), + await pbkdf2Hmac(u.password, t.toString(), 1000, 64, "SHA-512"), ); if (!isEqual(pa, password)) { return return_error(4, USER_PASSWORD_ERROR); } const token = m.db.add_token(u.id, now); - return return_data(token); + return return_data(token, 201); }, }; diff --git a/routes/api/user.ts b/routes/api/user.ts new file mode 100644 index 0000000..72894c8 --- /dev/null +++ b/routes/api/user.ts @@ -0,0 +1,77 @@ +import { Handlers } from "$fresh/server.ts"; +import { User, UserPermission } from "../../db.ts"; +import { get_task_manager } from "../../server.ts"; +import { get_string, parse_bool, parse_int } from "../../server/parse_form.ts"; +import { return_data, return_error } from "../../server/utils.ts"; +import pbkdf2Hmac from "pbkdf2-hmac"; + +export const handler: Handlers = { + async GET(req, ctx) { + const u = new URL(req.url); + const id = await parse_int(u.searchParams.get("id"), null); + const username = u.searchParams.get("username"); + const user = ctx.state.user; + if (id === null && !username && !user) { + return return_error(1, "user not specified."); + } + const m = get_task_manager(); + const us = id !== null + ? m.db.get_user(id) + : username + ? m.db.get_user_by_name(username) + : user; + if (!us) return return_error(404, "User not found."); + if (user && !user.is_admin && us.id !== user.id) { + return return_error(403, "Permission denied."); + } + return return_data({ + id: us.id, + username: us.username, + is_admin: us.is_admin, + permissions: us.permissions, + }); + }, + async PUT(req, ctx) { + const data = await req.formData(); + const user = ctx.state.user; + if (user && !user.is_admin) { + return return_error(403, "Permission denied."); + } + const name = await get_string(data.get("name")); + const password = await get_string(data.get("password")); + const is_admin = await parse_bool(data.get("is_admin"), false); + let permissions: UserPermission = await parse_int( + data.get("permissions"), + UserPermission.None, + ); + if (!name) return return_error(1, "name not specified."); + if (!password) return return_error(1, "password not specified."); + if (is_admin) permissions = UserPermission.All; + const m = get_task_manager(); + if (m.db.get_user_by_name(name)) { + return return_error(2, "Please change to another name."); + } + const hpassword = new Uint8Array( + await pbkdf2Hmac( + password, + "eh-downloader-salt", + 210000, + 64, + "SHA-512", + ), + ); + if (m.db.get_user_count() === 0) { + m.db.add_root_user(name, hpassword); + return return_data(0, 201); + } else { + const t = m.db.add_user({ + id: 0, + username: name, + password: hpassword, + is_admin, + permissions, + }); + return return_data(t.id, 201); + } + }, +}; diff --git a/server/cookies.ts b/server/cookies.ts new file mode 100644 index 0000000..b9a573f --- /dev/null +++ b/server/cookies.ts @@ -0,0 +1,12 @@ +export function parse_cookies(c: string | null) { + const m = new Map(); + if (c === null) return m; + for (const a of c.split(";")) { + const b = a.trim(); + const d = b.split("="); + if (d.length > 1) { + m.set(d[0], d.slice(1).join("=")); + } + } + return m; +}