Download server.mjs from ghuser1/notes: direct link, hf CLI and curl.
- Browser
- Download file 8.11 kB
-
https://huggingface.co/spaces/ghuser1/notes/resolve/main/server.mjs
- Command line
-
hf download hf://spaces/ghuser1/notes/server.mjs
-
curl -L -o server.mjs https://huggingface.co/spaces/ghuser1/notes/resolve/main/server.mjs
8.11 kB
| import { serve } from "@hono/node-server"; | |
| import { Hono } from "hono"; | |
| import { deleteCookie, getCookie, setCookie } from "hono/cookie"; | |
| import { mkdirSync } from "node:fs"; | |
| import { readFile } from "node:fs/promises"; | |
| import { extname, join, normalize } from "node:path"; | |
| import { createSessionToken, hashPassword, hashSessionToken, verifyPassword } from "./auth.mjs"; | |
| import { createRepository } from "./src/data/repository.mjs"; | |
| import { maxNoteBodyLength } from "./src/data/state-utils.mjs"; | |
| const port = Number(process.env.PORT || 4173); | |
| const root = process.cwd(); | |
| const dataDir = join(root, "data"); | |
| const app = new Hono(); | |
| mkdirSync(dataDir, { recursive: true }); | |
| const repository = createRepository({ dbPath: join(dataDir, "notes.db") }); | |
| const sessionCookie = "web_notes_session"; | |
| const sessionTtlMs = 1000 * 60 * 60 * 24 * 30; | |
| await repository.init(); | |
| app.get("/api/health", (c) => c.json({ ok: true })); | |
| app.get("/api/auth/status", async (c) => { | |
| const user = await repository.getUser(); | |
| return c.json({ | |
| configured: Boolean(user), | |
| authenticated: await isAuthenticated(c) | |
| }); | |
| }); | |
| app.post("/api/auth/setup", async (c) => { | |
| const existing = await repository.getUser(); | |
| if (existing) return c.json({ ok: false, error: "already_configured" }, 409); | |
| const { password } = await c.req.json(); | |
| if (!isValidPassword(password)) { | |
| return c.json({ ok: false, error: "password_too_short" }, 400); | |
| } | |
| await repository.createUser({ passwordHash: await hashPassword(password) }); | |
| await createSession(c); | |
| return c.json({ ok: true }); | |
| }); | |
| app.post("/api/auth/login", async (c) => { | |
| const user = await repository.getUser(); | |
| if (!user) return c.json({ ok: false, error: "not_configured" }, 400); | |
| const { password } = await c.req.json(); | |
| if (!await verifyPassword(password, user.passwordHash)) { | |
| return c.json({ ok: false, error: "invalid_password" }, 401); | |
| } | |
| await createSession(c); | |
| return c.json({ ok: true }); | |
| }); | |
| app.post("/api/auth/logout", async (c) => { | |
| const token = getCookie(c, sessionCookie); | |
| if (token) { | |
| await repository.deleteSession(await hashSessionToken(token)); | |
| } | |
| deleteCookie(c, sessionCookie, { path: "/" }); | |
| return c.json({ ok: true }); | |
| }); | |
| app.post("/api/auth/password", async (c) => { | |
| const user = await repository.getUser(); | |
| if (!user) return c.json({ ok: false, error: "not_configured" }, 400); | |
| if (!await isAuthenticated(c)) return c.json({ ok: false, error: "unauthorized" }, 401); | |
| const { currentPassword, nextPassword } = await c.req.json(); | |
| if (!await verifyPassword(currentPassword, user.passwordHash)) { | |
| return c.json({ ok: false, error: "invalid_password" }, 401); | |
| } | |
| if (!isValidPassword(nextPassword)) { | |
| return c.json({ ok: false, error: "password_too_short" }, 400); | |
| } | |
| await repository.updateUserPassword({ passwordHash: await hashPassword(nextPassword) }); | |
| await repository.deleteAllSessions(); | |
| await createSession(c); | |
| return c.json({ ok: true }); | |
| }); | |
| app.use("/api/state", async (c, next) => { | |
| const user = await repository.getUser(); | |
| if (!user) return c.json({ ok: false, error: "not_configured" }, 401); | |
| if (!await isAuthenticated(c)) return c.json({ ok: false, error: "unauthorized" }, 401); | |
| await next(); | |
| }); | |
| app.get("/api/state", async (c) => { | |
| return c.json(await repository.readState()); | |
| }); | |
| app.put("/api/state", async (c) => { | |
| const body = await c.req.json(); | |
| const folders = Array.isArray(body.folders) ? body.folders : []; | |
| const notes = Array.isArray(body.notes) ? body.notes : []; | |
| return c.json(await repository.writeState({ folders, notes })); | |
| }); | |
| app.post("/api/notes", async (c) => { | |
| const authError = await getAuthError(c); | |
| if (authError) return authError; | |
| const body = await c.req.json(); | |
| const lengthError = validateBodyLength(body.body); | |
| if (lengthError) return c.json(lengthError, 413); | |
| return c.json(await repository.createNote(body), 201); | |
| }); | |
| app.patch("/api/notes/:id", async (c) => { | |
| const authError = await getAuthError(c); | |
| if (authError) return authError; | |
| const body = await c.req.json(); | |
| const lengthError = validateBodyLength(body.body); | |
| if (lengthError) return c.json(lengthError, 413); | |
| const result = await repository.updateNote(c.req.param("id"), body); | |
| if (result.status === "missing") return c.json({ ok: false, error: "not_found" }, 404); | |
| if (result.status === "conflict") return c.json({ ok: false, error: "conflict", note: result.note }, 409); | |
| return c.json(result.note); | |
| }); | |
| app.delete("/api/notes/:id", async (c) => { | |
| const authError = await getAuthError(c); | |
| if (authError) return authError; | |
| await repository.deleteNote(c.req.param("id")); | |
| return c.json({ ok: true }); | |
| }); | |
| app.post("/api/folders", async (c) => { | |
| const authError = await getAuthError(c); | |
| if (authError) return authError; | |
| const body = await c.req.json(); | |
| return c.json(await repository.createFolder(body), 201); | |
| }); | |
| app.patch("/api/folders/:id", async (c) => { | |
| const authError = await getAuthError(c); | |
| if (authError) return authError; | |
| const body = await c.req.json(); | |
| const result = await repository.updateFolder(c.req.param("id"), body); | |
| if (result.status === "missing") return c.json({ ok: false, error: "not_found" }, 404); | |
| if (result.status === "conflict") return c.json({ ok: false, error: "conflict", folder: result.folder }, 409); | |
| return c.json(result.folder); | |
| }); | |
| app.delete("/api/folders/:id", async (c) => { | |
| const authError = await getAuthError(c); | |
| if (authError) return authError; | |
| const result = await repository.deleteFolder(c.req.param("id")); | |
| if (result.status === "missing") return c.json({ ok: false, error: "not_found" }, 404); | |
| return c.json(result); | |
| }); | |
| app.get("*", async (c) => { | |
| const url = new URL(c.req.url); | |
| const requestPath = url.pathname === "/" ? "/index.html" : url.pathname; | |
| const safePath = normalize(requestPath).replace(/^(\.\.[/\\])+/, ""); | |
| const filePath = join(root, safePath); | |
| if (!filePath.startsWith(root)) { | |
| return c.text("Not found", 404); | |
| } | |
| try { | |
| const file = await readFile(filePath); | |
| return new Response(file, { | |
| headers: { | |
| "content-type": mimeType(extname(filePath)) | |
| } | |
| }); | |
| } catch { | |
| const index = await readFile(join(root, "index.html")); | |
| return new Response(index, { | |
| headers: { | |
| "content-type": "text/html; charset=utf-8" | |
| } | |
| }); | |
| } | |
| }); | |
| serve({ fetch: app.fetch, port }, () => { | |
| console.log(`备忘录 running at http://127.0.0.1:${port}`); | |
| console.log(`Data repository runtime: ${repository.runtime}`); | |
| }); | |
| function mimeType(ext) { | |
| return { | |
| ".html": "text/html; charset=utf-8", | |
| ".css": "text/css; charset=utf-8", | |
| ".js": "text/javascript; charset=utf-8", | |
| ".json": "application/json; charset=utf-8", | |
| ".png": "image/png", | |
| ".svg": "image/svg+xml; charset=utf-8" | |
| }[ext] || "application/octet-stream"; | |
| } | |
| async function createSession(c) { | |
| const token = createSessionToken(); | |
| const expiresAt = Date.now() + sessionTtlMs; | |
| await repository.createSession({ | |
| tokenHash: await hashSessionToken(token), | |
| expiresAt | |
| }); | |
| setCookie(c, sessionCookie, token, { | |
| path: "/", | |
| httpOnly: true, | |
| sameSite: "Lax", | |
| maxAge: Math.floor(sessionTtlMs / 1000), | |
| secure: new URL(c.req.url).protocol === "https:" | |
| }); | |
| } | |
| async function isAuthenticated(c) { | |
| const token = getCookie(c, sessionCookie); | |
| if (!token) return false; | |
| return Boolean(await repository.getSession(await hashSessionToken(token))); | |
| } | |
| async function getAuthError(c) { | |
| const user = await repository.getUser(); | |
| if (!user) return c.json({ ok: false, error: "not_configured" }, 401); | |
| if (!await isAuthenticated(c)) return c.json({ ok: false, error: "unauthorized" }, 401); | |
| return null; | |
| } | |
| function isValidPassword(password) { | |
| return typeof password === "string" && password.length >= 8; | |
| } | |
| function validateBodyLength(body) { | |
| if (body === undefined) return null; | |
| if (String(body).length <= maxNoteBodyLength) return null; | |
| return { | |
| ok: false, | |
| error: "note_too_large", | |
| maxLength: maxNoteBodyLength | |
| }; | |
| } | |