From 1fcfed11000059a2f9d3fa33f3750ca7c6361fe7 Mon Sep 17 00:00:00 2001 From: lyonbot Date: Fri, 10 Jan 2025 14:33:09 +0800 Subject: [PATCH] feat: slug and password --- pnpm-lock.yaml | 20 ++++++++++++ src/database.ts | 30 ++++++++++++++--- src/index.ts | 31 ++++++++++++------ web/package.json | 1 + web/src/App.scss | 5 +++ web/src/App.tsx | 7 +++- web/src/components/ContentInput.tsx | 2 +- web/src/components/PasswordInput.tsx | 49 ++++++++++++++++++++++++++++ web/src/components/UploadRecords.tsx | 16 +++++---- web/src/index.tsx | 6 +++- web/src/store/auth.ts | 38 +++++++++++++++++++++ web/src/store/store.ts | 4 +++ web/src/sw-client.tsx | 2 +- wrangler.toml | 4 +-- 14 files changed, 187 insertions(+), 28 deletions(-) create mode 100644 web/src/components/PasswordInput.tsx create mode 100644 web/src/store/auth.ts create mode 100644 web/src/store/store.ts diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5d8c4a8..0eae572 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -24,6 +24,9 @@ importers: web: dependencies: + jotai: + specifier: ^2.11.0 + version: 2.11.0(@types/react@19.0.2)(react@19.0.0) react: specifier: ^19.0.0 version: 19.0.0 @@ -2126,6 +2129,18 @@ packages: resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==} hasBin: true + jotai@2.11.0: + resolution: {integrity: sha512-zKfoBBD1uDw3rljwHkt0fWuja1B76R7CjznuBO+mSX6jpsO1EBeWNRKpeaQho9yPI/pvCv4recGfgOXGxwPZvQ==} + engines: {node: '>=12.20.0'} + peerDependencies: + '@types/react': '>=17.0.0' + react: '>=17.0.0' + peerDependenciesMeta: + '@types/react': + optional: true + react: + optional: true + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -5270,6 +5285,11 @@ snapshots: jiti@2.4.2: {} + jotai@2.11.0(@types/react@19.0.2)(react@19.0.0): + optionalDependencies: + '@types/react': 19.0.2 + react: 19.0.0 + js-tokens@4.0.0: {} js-yaml@4.1.0: diff --git a/src/database.ts b/src/database.ts index f0d5730..f34a4db 100644 --- a/src/database.ts +++ b/src/database.ts @@ -3,6 +3,7 @@ export async function migrateTables(db: D1Database) { ` CREATE TABLE IF NOT EXISTS upload_record ( id INTEGER PRIMARY KEY, + slug TEXT NOT NULL UNIQUE, uploader TEXT NOT NULL, ctime TIMESTAMP DEFAULT CURRENT_TIMESTAMP, size INTEGER DEFAULT 0, @@ -15,6 +16,7 @@ export async function migrateTables(db: D1Database) { export interface UploadRecord { id: number; + slug: string; uploader: string; ctime: number; size: number; @@ -37,6 +39,7 @@ function fromDB(data: any): UploadRecord { return { id: data.id, + slug: data.slug, uploader: data.uploader, ctime: +new Date(data.ctime), size: data.size, @@ -48,6 +51,7 @@ function fromDB(data: any): UploadRecord { function toDB(record: UploadRecord) { return { id: record.id, + slug: record.slug, uploader: record.uploader, ctime: new Date(+record.ctime), size: record.size, @@ -72,7 +76,7 @@ function toDB(record: UploadRecord) { * * to fetch older records, pass `beforeId` option (which is last id of prev page) */ -export async function getUploadRecords( +export async function listUploadRecords( db: D1Database, opts: { beforeId?: number } = {} ) { @@ -91,7 +95,7 @@ export async function getUploadRecords( return records; } -export async function getSingleUploadRecord(db: D1Database, id: number) { +export async function getUploadRecord(db: D1Database, id: number) { const record = await db .prepare(`SELECT * FROM upload_record WHERE id = ?`) .bind(id) @@ -100,16 +104,31 @@ export async function getSingleUploadRecord(db: D1Database, id: number) { return fromDB(record); } +export async function getUploadRecordBySlug(db: D1Database, slug: string) { + const record = await db + .prepare(`SELECT * FROM upload_record WHERE slug = ?`) + .bind(slug) + .first(); + if (!record) return null; + return fromDB(record); +} + +export function randomId() { + return Date.now().toString(36) + Math.random().toString(36).slice(-4); +} + export async function createUploadRecord( db: D1Database, - record: Omit + record: Omit & { slug?: string } ) { const inserting = toDB(record as UploadRecord); + const slug = inserting.slug || randomId(); const res = await db .prepare( - "INSERT INTO upload_record (uploader, size, files, message) VALUES (?, ?, ?, ?)" + "INSERT INTO upload_record (slug, uploader, size, files, message) VALUES (?, ?, ?, ?, ?)" ) .bind( + slug, inserting.uploader, inserting.size, inserting.files, @@ -121,6 +140,7 @@ export async function createUploadRecord( ...(record as UploadRecord), ctime: +new Date(), id, + slug, }; return { id, inserted }; } @@ -156,7 +176,7 @@ export async function deleteRecord( deleteFiles: (path: string[]) => Promise ) { // 1. get record - const record = await getSingleUploadRecord(db, id); + const record = await getUploadRecord(db, id); if (!record) return; // 2. delete files diff --git a/src/index.ts b/src/index.ts index ded68f0..ec38006 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,25 +2,36 @@ import { Hono } from "hono"; import { createUploadRecord, deleteRecord, - getSingleUploadRecord, - getUploadRecords, + listUploadRecords, + getUploadRecordBySlug, migrateTables, purgeRecordsBeforeId, } from "./database"; +import { H } from "hono/types"; type Bindings = { ASSETS: { fetch: typeof fetch }; DB: D1Database; MY_BUCKET: R2Bucket; + PASSWORD: string; }; const app = new Hono<{ Bindings: Bindings }>(); -app.get("/api/list", async (c) => { +const authWithPassword: H<{ Bindings: Bindings }> = async (c, next) => { + if (c.env.PASSWORD && c.req.header("x-password") !== c.env.PASSWORD) { + c.status(401); + return c.json({ error: "Password required" }); + } + + return await next(); +}; + +app.get("/api/list", authWithPassword, async (c) => { await migrateTables(c.env.DB); const beforeId = +c.req.query("beforeId")!; - const list = await getUploadRecords(c.env.DB, { beforeId }); + const list = await listUploadRecords(c.env.DB, { beforeId }); return c.json(list); // const r = await createUploadRecord(c.env.DB, { @@ -32,7 +43,7 @@ app.get("/api/list", async (c) => { // return c.json(r) }); -app.post("/api/upload", async (c) => { +app.post("/api/upload", authWithPassword, async (c) => { const uploader = c.req.header("x-uploader") || "unknown"; const body = await c.req.formData(); @@ -73,10 +84,10 @@ app.post("/api/upload", async (c) => { return c.json({ record }); }); -app.get("/api/download/:id/:index", async (c) => { - const id = +c.req.param("id"); +app.get("/api/download/:slug/:index", async (c) => { + const slug = c.req.param("slug"); const index = c.req.param("index"); - const record = await getSingleUploadRecord(c.env.DB, id); + const record = await getUploadRecordBySlug(c.env.DB, slug); if (!record) { return c.status(404); } @@ -113,14 +124,14 @@ app.get("/api/download/:id/:index", async (c) => { return new Response(r.body, { headers }); }); -app.post("/api/delete", async (c) => { +app.post("/api/delete", authWithPassword, async (c) => { const body = await c.req.json(); const id = +body.id; await deleteRecord(c.env.DB, id, (paths) => c.env.MY_BUCKET.delete(paths)); return c.json({ ok: true }); }); -app.post("/api/purge", async (c) => { +app.post("/api/purge", authWithPassword, async (c) => { const beforeId = 9999999; await purgeRecordsBeforeId(c.env.DB, beforeId, (paths) => c.env.MY_BUCKET.delete(paths) diff --git a/web/package.json b/web/package.json index 519907b..6eac4e4 100644 --- a/web/package.json +++ b/web/package.json @@ -8,6 +8,7 @@ "preview": "rsbuild preview" }, "dependencies": { + "jotai": "^2.11.0", "react": "^19.0.0", "react-dom": "^19.0.0", "swr": "^2.3.0", diff --git a/web/src/App.scss b/web/src/App.scss index 641fec4..5aa0427 100644 --- a/web/src/App.scss +++ b/web/src/App.scss @@ -15,6 +15,11 @@ textarea { font-family: inherit; font-size: inherit; border: 0; + outline: 0px solid transparent; + + &:focus { + @apply ring ring-brand-3 outline-1 outline-brand-6; + } } button { diff --git a/web/src/App.tsx b/web/src/App.tsx index 3d2e2d1..824c244 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -2,6 +2,9 @@ import { useState } from 'react'; import './App.scss'; import { ContentInput } from './components/ContentInput'; import { UploadRecords } from './components/UploadRecords'; +import { $password } from './store/auth'; +import { store } from './store/store'; +import { PasswordInput } from './components/PasswordInput'; const App = () => { const [progress, setProgress] = useState(0); @@ -15,6 +18,7 @@ const App = () => { const xhr = new XMLHttpRequest(); xhr.open('POST', '/api/upload'); xhr.setRequestHeader('x-uploader', 'yon'); + xhr.setRequestHeader('x-password', store.get($password)); xhr.upload.onprogress = (e) => { if (e.lengthComputable) { @@ -29,7 +33,7 @@ const App = () => { if (xhr.status === 200) { // const data = JSON.parse(xhr.responseText); setProgress(0); - window.dispatchEvent(new Event('upload-complete')); + window.dispatchEvent(new Event('records-updated')); resolve(); return; } @@ -53,6 +57,7 @@ const App = () => {
+ ); }; diff --git a/web/src/components/ContentInput.tsx b/web/src/components/ContentInput.tsx index daadebb..c4d23fa 100644 --- a/web/src/components/ContentInput.tsx +++ b/web/src/components/ContentInput.tsx @@ -155,7 +155,7 @@ export const ContentInput = memo((props) => { } }} > -
+