mirror of
https://github.com/lifegpc/eh-downloader.git
synced 2026-06-22 12:04:21 +08:00
Add shared_token API
This commit is contained in:
76
db.ts
76
db.ts
@@ -141,7 +141,8 @@ export enum UserPermission {
|
||||
EditGallery = 1 << 1,
|
||||
DeleteGallery = 1 << 2,
|
||||
ManageTasks = 1 << 3,
|
||||
All = ~(~0 << 4),
|
||||
ShareGallery = 1 << 4,
|
||||
All = ~(~0 << 5),
|
||||
}
|
||||
export type User = {
|
||||
id: number | bigint;
|
||||
@@ -189,6 +190,29 @@ export type ClientConfig = {
|
||||
name: string;
|
||||
data: string;
|
||||
};
|
||||
export enum SharedTokenType {
|
||||
Gallery,
|
||||
}
|
||||
export type GallerySharedTokenInfo = {
|
||||
gid: number | bigint;
|
||||
};
|
||||
type SharedTokenTypeMap = {
|
||||
[SharedTokenType.Gallery]: GallerySharedTokenInfo;
|
||||
};
|
||||
export type SharedToken<T extends SharedTokenType = SharedTokenType> = {
|
||||
id: number | bigint;
|
||||
token: string;
|
||||
expired: Date | null;
|
||||
type: T;
|
||||
info: SharedTokenTypeMap[T];
|
||||
};
|
||||
type SharedTokenRaw = {
|
||||
id: number | bigint;
|
||||
token: string;
|
||||
expired: Date | null;
|
||||
type: SharedTokenType;
|
||||
info: string;
|
||||
};
|
||||
const ALL_TABLES = [
|
||||
"version",
|
||||
"task",
|
||||
@@ -202,6 +226,7 @@ const ALL_TABLES = [
|
||||
"token",
|
||||
"ehmeta",
|
||||
"client_config",
|
||||
"shared_token",
|
||||
];
|
||||
const VERSION_TABLE = `CREATE TABLE version (
|
||||
id TEXT,
|
||||
@@ -302,6 +327,13 @@ const CLIENT_CONFIG_TABLE = `CREATE TABLE client_config (
|
||||
data TEXT,
|
||||
PRIMARY KEY (uid, client, name)
|
||||
);`;
|
||||
const SHARED_TOKEN_TABLE = `CREATE TABLE shared_token (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
token TEXT,
|
||||
expired TEXT,
|
||||
type INT,
|
||||
info TEXT
|
||||
);`;
|
||||
|
||||
function escape_fields(fields: string, namespace: string) {
|
||||
const fs = fields.split(",");
|
||||
@@ -323,7 +355,7 @@ export class EhDb {
|
||||
#base_path: string;
|
||||
#db_path: string;
|
||||
#use_ffi = false;
|
||||
readonly version = parse_ver("1.0.0-12");
|
||||
readonly version = parse_ver("1.0.0-13");
|
||||
constructor(base_path: string) {
|
||||
this.#base_path = base_path;
|
||||
this.#db_path = join(base_path, "data.db");
|
||||
@@ -531,6 +563,9 @@ export class EhDb {
|
||||
if (!this.#exist_table.has("client_config")) {
|
||||
this.db.execute(CLIENT_CONFIG_TABLE);
|
||||
}
|
||||
if (!this.#exist_table.has("shared_token")) {
|
||||
this.db.execute(SHARED_TOKEN_TABLE);
|
||||
}
|
||||
this.#updateExistsTable();
|
||||
}
|
||||
#read_version() {
|
||||
@@ -667,6 +702,23 @@ export class EhDb {
|
||||
UserPermission.All,
|
||||
]);
|
||||
}
|
||||
add_shared_token<T extends SharedTokenType = SharedTokenType>(
|
||||
type: T,
|
||||
info: SharedTokenTypeMap[T],
|
||||
expired: Date | null = null,
|
||||
) {
|
||||
let token = randomstring();
|
||||
while (this.get_token(token) || this.get_shared_token(token)) {
|
||||
token = randomstring();
|
||||
}
|
||||
this.db.query(
|
||||
"INSERT INTO shared_token (token, expired, type, info) VALUES (?, ?, ?, ?);",
|
||||
[token, expired, type, toJSON(info)],
|
||||
);
|
||||
const t = this.get_shared_token(token);
|
||||
if (!t) throw Error("Failed to add shared token");
|
||||
return t;
|
||||
}
|
||||
add_task(task: Task) {
|
||||
return this.transaction(() => {
|
||||
this.db.query(
|
||||
@@ -713,7 +765,7 @@ export class EhDb {
|
||||
client_platform: string | null,
|
||||
): Token {
|
||||
let token = randomstring();
|
||||
while (this.get_token(token)) {
|
||||
while (this.get_token(token) || this.get_shared_token(token)) {
|
||||
token = randomstring();
|
||||
}
|
||||
this.db.query(
|
||||
@@ -916,6 +968,15 @@ export class EhDb {
|
||||
return t;
|
||||
});
|
||||
}
|
||||
convert_shared_token(m: SharedTokenRaw[]) {
|
||||
return m.map((m) => {
|
||||
const e = m.expired ? new Date(m.expired) : null;
|
||||
const t = <SharedToken> <unknown> m;
|
||||
t.expired = e;
|
||||
t.info = JSON.parse(m.info);
|
||||
return t;
|
||||
});
|
||||
}
|
||||
convert_token(m: TokenRaw[]) {
|
||||
return m.map((m) => {
|
||||
const e = new Date(m.expired);
|
||||
@@ -1305,6 +1366,15 @@ export class EhDb {
|
||||
])
|
||||
);
|
||||
}
|
||||
get_shared_token(token: string) {
|
||||
const s = this.convert_shared_token(
|
||||
this.db.queryEntries<SharedTokenRaw>(
|
||||
"SELECT * FROM shared_token WHERE token = ?;",
|
||||
[token],
|
||||
),
|
||||
);
|
||||
return s.length ? s[0] : undefined;
|
||||
}
|
||||
get_token(token: string) {
|
||||
const s = this.convert_token(
|
||||
this.db.queryEntries<TokenRaw>(
|
||||
|
||||
@@ -22,6 +22,7 @@ import * as $api_gallery_gid_ from "./routes/api/gallery/[gid].ts";
|
||||
import * as $api_gallery_list from "./routes/api/gallery/list.ts";
|
||||
import * as $api_gallery_meta_gids_ from "./routes/api/gallery/meta/[gids].ts";
|
||||
import * as $api_health_check from "./routes/api/health_check.ts";
|
||||
import * as $api_shared_token from "./routes/api/shared_token.ts";
|
||||
import * as $api_status from "./routes/api/status.ts";
|
||||
import * as $api_tag_id_ from "./routes/api/tag/[id].ts";
|
||||
import * as $api_tag_rows from "./routes/api/tag/rows.ts";
|
||||
@@ -69,6 +70,7 @@ const manifest = {
|
||||
"./routes/api/gallery/list.ts": $api_gallery_list,
|
||||
"./routes/api/gallery/meta/[gids].ts": $api_gallery_meta_gids_,
|
||||
"./routes/api/health_check.ts": $api_health_check,
|
||||
"./routes/api/shared_token.ts": $api_shared_token,
|
||||
"./routes/api/status.ts": $api_status,
|
||||
"./routes/api/tag/[id].ts": $api_tag_id_,
|
||||
"./routes/api/tag/rows.ts": $api_tag_rows,
|
||||
|
||||
@@ -2,7 +2,7 @@ import { FreshContext } 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";
|
||||
import type { Token } from "../../db.ts";
|
||||
import { SharedTokenType, type Token } from "../../db.ts";
|
||||
|
||||
function handle_auth(req: Request, ctx: FreshContext) {
|
||||
if (req.method === "OPTIONS") return true;
|
||||
@@ -44,7 +44,45 @@ function handle_auth(req: Request, ctx: FreshContext) {
|
||||
if (!token) return check();
|
||||
const t = m.db.get_token(token);
|
||||
const now = (new Date()).getTime();
|
||||
if (!t || t.expired.getTime() < now) return check();
|
||||
if (!t) {
|
||||
const st = m.db.get_shared_token(token);
|
||||
if (!st || (st.expired !== null && st.expired.getTime() < now)) {
|
||||
return check();
|
||||
}
|
||||
if (
|
||||
u.pathname === "/api/shared_token" && req.method === "GET"
|
||||
) { /*Nothing to do*/ } else if (st.type == SharedTokenType.Gallery) {
|
||||
const check_g = () => {
|
||||
if (
|
||||
u.pathname === `/api/gallery/${st.info.gid}` &&
|
||||
req.method === "GET"
|
||||
) return true;
|
||||
if (u.pathname === "/api/tag/rows" && req.method === "GET") {
|
||||
return true;
|
||||
}
|
||||
// Follow API need extra checks.
|
||||
if (
|
||||
u.pathname.match(/^\/api\/file\/\d+$/) &&
|
||||
req.method === "GET"
|
||||
) return true;
|
||||
if (
|
||||
u.pathname.match(/^\/api\/thumbnail\/\d+$/) &&
|
||||
req.method === "GET"
|
||||
) return true;
|
||||
if (
|
||||
u.pathname.startsWith("/api/files/") && req.method === "GET"
|
||||
) return true;
|
||||
return false;
|
||||
};
|
||||
if (!check_g()) return check();
|
||||
} else {
|
||||
return check();
|
||||
}
|
||||
ctx.state.is_from_cookie = false;
|
||||
ctx.state.shared_token = st;
|
||||
return true;
|
||||
}
|
||||
if (t.expired.getTime() < now) return check();
|
||||
const user = m.db.get_user(t.uid);
|
||||
if (!user) {
|
||||
m.db.delete_token(token);
|
||||
|
||||
@@ -9,9 +9,14 @@ import pbkdf2Hmac from "pbkdf2-hmac";
|
||||
import { encodeBase64 as encode } from "@std/encoding/base64";
|
||||
import { get_host, return_data, return_error } from "../../../server/utils.ts";
|
||||
import type { EhFileExtend } from "../../../server/files.ts";
|
||||
import { User, UserPermission } from "../../../db.ts";
|
||||
import {
|
||||
SharedToken,
|
||||
SharedTokenType,
|
||||
User,
|
||||
UserPermission,
|
||||
} from "../../../db.ts";
|
||||
import { SortableURLSearchParams } from "../../../server/SortableURLSearchParams.ts";
|
||||
import { isNumNaN, parseBigInt } from "../../../utils.ts";
|
||||
import { compareNum, isNumNaN, parseBigInt } from "../../../utils.ts";
|
||||
import { extname } from "@std/path";
|
||||
|
||||
export const handler: Handlers = {
|
||||
@@ -23,6 +28,7 @@ export const handler: Handlers = {
|
||||
) {
|
||||
return return_error(403, "Permission denied.");
|
||||
}
|
||||
const st = <SharedToken | undefined> ctx.state.shared_token;
|
||||
const u = new URL(req.url);
|
||||
const m = get_task_manager();
|
||||
const token = u.searchParams.get("token");
|
||||
@@ -51,9 +57,20 @@ export const handler: Handlers = {
|
||||
}
|
||||
const f = m.db.get_file(id);
|
||||
if (!f) {
|
||||
if (st && st.type == SharedTokenType.Gallery) {
|
||||
if (data) return return_error(403, "Permission denied.");
|
||||
return new Response("Permission denied.", { status: 403 });
|
||||
}
|
||||
if (data) return return_error(404, "File not found.");
|
||||
return new Response("File not found.", { status: 404 });
|
||||
}
|
||||
if (st && st.type == SharedTokenType.Gallery) {
|
||||
const pmetas = m.db.get_pmeta_by_token_only(f.token);
|
||||
if (!pmetas.some((m) => !compareNum(m.gid, st.info.gid))) {
|
||||
if (data) return return_error(403, "Permission denied.");
|
||||
return new Response("Permission denied.", { status: 403 });
|
||||
}
|
||||
}
|
||||
if (data) {
|
||||
return return_data<EhFileExtend>({
|
||||
id: f.id,
|
||||
|
||||
@@ -2,7 +2,13 @@ import { Handlers } from "$fresh/server.ts";
|
||||
import { get_task_manager } from "../../../server.ts";
|
||||
import type { EhFiles } from "../../../server/files.ts";
|
||||
import { return_data, return_error } from "../../../server/utils.ts";
|
||||
import { User, UserPermission } from "../../../db.ts";
|
||||
import {
|
||||
SharedToken,
|
||||
SharedTokenType,
|
||||
User,
|
||||
UserPermission,
|
||||
} from "../../../db.ts";
|
||||
import { compareNum } from "../../../utils.ts";
|
||||
|
||||
export const handler: Handlers = {
|
||||
GET(_req, ctx) {
|
||||
@@ -10,6 +16,7 @@ export const handler: Handlers = {
|
||||
if (u && !u.is_admin && !(u.permissions & UserPermission.ReadGallery)) {
|
||||
return return_error(403, "Permission denied.");
|
||||
}
|
||||
const st = <SharedToken | undefined> ctx.state.shared_token;
|
||||
const tokens = ctx.params.token.split(",");
|
||||
const m = get_task_manager();
|
||||
const enable_server_timing = m.cfg.enable_server_timing;
|
||||
@@ -17,6 +24,12 @@ export const handler: Handlers = {
|
||||
const headers: HeadersInit = {};
|
||||
const data: EhFiles = {};
|
||||
for (const token of tokens) {
|
||||
if (st && st.type == SharedTokenType.Gallery) {
|
||||
const pmetas = m.db.get_pmeta_by_token_only(token);
|
||||
if (!pmetas.some((m) => !compareNum(m.gid, st.info.gid))) {
|
||||
return return_error(403, "Permission denied.");
|
||||
}
|
||||
}
|
||||
data[token] = m.db.get_files(token).map((d) => {
|
||||
/**@ts-ignore */
|
||||
delete d.path;
|
||||
|
||||
64
routes/api/shared_token.ts
Normal file
64
routes/api/shared_token.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import { Handlers } from "$fresh/server.ts";
|
||||
import { exists } from "@std/fs/exists";
|
||||
import {
|
||||
SharedToken,
|
||||
SharedTokenType,
|
||||
User,
|
||||
UserPermission,
|
||||
} from "../../db.ts";
|
||||
import { get_task_manager } from "../../server.ts";
|
||||
import {
|
||||
get_string,
|
||||
parse_big_int,
|
||||
parse_int,
|
||||
} from "../../server/parse_form.ts";
|
||||
import { get_host, return_data, return_error } from "../../server/utils.ts";
|
||||
|
||||
export const handler: Handlers = {
|
||||
GET(_req, ctx) {
|
||||
const st = <SharedToken | undefined> ctx.state.shared_token;
|
||||
if (!st) return return_error(1, "No token.");
|
||||
return return_data(st);
|
||||
},
|
||||
async PUT(req, ctx) {
|
||||
const user = <User | undefined> ctx.state.user;
|
||||
let form: FormData | undefined;
|
||||
try {
|
||||
form = await req.formData();
|
||||
} catch (_) {
|
||||
return return_error(400, "Bad Request");
|
||||
}
|
||||
const typ = await get_string(form.get("type"));
|
||||
const expired = await parse_int(form.get("expired"), null);
|
||||
if (typ == "gallery") {
|
||||
if (
|
||||
user && !user.is_admin &&
|
||||
!(user.permissions & UserPermission.ShareGallery)
|
||||
) {
|
||||
return return_error(403, "Permission denied.");
|
||||
}
|
||||
const gid = await parse_big_int(form.get("gid"), null);
|
||||
if (!gid) return return_error(2, "gid not specified.");
|
||||
const m = get_task_manager();
|
||||
const st = m.db.add_shared_token(SharedTokenType.Gallery, {
|
||||
gid: gid,
|
||||
}, expired ? new Date(expired) : null);
|
||||
let flutter_base = import.meta.resolve("../../static/flutter")
|
||||
.slice(7);
|
||||
if (Deno.build.os === "windows") {
|
||||
flutter_base = flutter_base.slice(1);
|
||||
}
|
||||
if (m.cfg.flutter_frontend) {
|
||||
flutter_base = m.cfg.flutter_frontend;
|
||||
}
|
||||
const existed = await exists(flutter_base);
|
||||
const base = existed
|
||||
? `${get_host(req)}/flutter`
|
||||
: "https://dev.ehf.lifegpc.com/#";
|
||||
const url = `${base}/gallery/${gid}?share=${st.token}`;
|
||||
return return_data({ url, token: st }, 201);
|
||||
} else {
|
||||
return return_error(1, "Unknown type");
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
ThumbnailConfig,
|
||||
ThumbnailGenMethod,
|
||||
} from "../../../thumbnail/base.ts";
|
||||
import { isNumNaN, parseBigInt, sure_dir } from "../../../utils.ts";
|
||||
import { compareNum, isNumNaN, parseBigInt, sure_dir } from "../../../utils.ts";
|
||||
import { ThumbnailMethod } from "../../../config.ts";
|
||||
import { fb_generate_thumbnail } from "../../../thumbnail/ffmpeg_binary.ts";
|
||||
import {
|
||||
@@ -22,7 +22,12 @@ import pbkdf2Hmac from "pbkdf2-hmac";
|
||||
import { encodeBase64 as encode } from "@std/encoding/base64";
|
||||
import { SortableURLSearchParams } from "../../../server/SortableURLSearchParams.ts";
|
||||
import type * as FFMPEG_API from "../../../thumbnail/ffmpeg_api.ts";
|
||||
import { User, UserPermission } from "../../../db.ts";
|
||||
import {
|
||||
SharedToken,
|
||||
SharedTokenType,
|
||||
User,
|
||||
UserPermission,
|
||||
} from "../../../db.ts";
|
||||
|
||||
let ffmpeg_api: typeof FFMPEG_API | undefined;
|
||||
|
||||
@@ -35,6 +40,7 @@ export const handler: Handlers = {
|
||||
) {
|
||||
return new Response("Permission denied", { status: 403 });
|
||||
}
|
||||
const st = <SharedToken | undefined> ctx.state.shared_token;
|
||||
const id = parseBigInt(ctx.params.id);
|
||||
const m = get_task_manager();
|
||||
const u = new URL(req.url);
|
||||
@@ -64,8 +70,17 @@ export const handler: Handlers = {
|
||||
await sure_dir(b);
|
||||
const f = m.db.get_file(id);
|
||||
if (!f) {
|
||||
if (st && st.type == SharedTokenType.Gallery) {
|
||||
return new Response("Permission denied.", { status: 403 });
|
||||
}
|
||||
return new Response("File not found.", { status: 404 });
|
||||
}
|
||||
if (st && st.type == SharedTokenType.Gallery) {
|
||||
const pmetas = m.db.get_pmeta_by_token_only(f.token);
|
||||
if (!pmetas.some((m) => !compareNum(m.gid, st.info.gid))) {
|
||||
return new Response("Permission denied.", { status: 403 });
|
||||
}
|
||||
}
|
||||
const max = await parse_int(u.searchParams.get("max"), 400);
|
||||
const width = await parse_int(u.searchParams.get("width"), null);
|
||||
const height = await parse_int(u.searchParams.get("height"), null);
|
||||
|
||||
Reference in New Issue
Block a user