| import "dotenv/config"; |
| import express from "express"; |
| import path from "path"; |
| import { fileURLToPath } from "url"; |
| import { client, all, get, run, initDb } from "./db.js"; |
| import { |
| noteToMarkdown, |
| notesToHtml, |
| htmlToPdf, |
| categoryPath, |
| safeName, |
| makeZip, |
| } from "./export.js"; |
| import { requireApiAuth, isAuthed, loginHandler, logoutHandler } from "./auth.js"; |
|
|
| const __dirname = path.dirname(fileURLToPath(import.meta.url)); |
| const PUBLIC = path.join(__dirname, "public"); |
| const app = express(); |
| const PORT = process.env.PORT || 3000; |
|
|
| app.set("trust proxy", 1); |
| app.use(express.json()); |
|
|
| |
| app.post("/api/login", loginHandler); |
| app.post("/api/logout", logoutHandler); |
| app.get("/api/me", (req, res) => res.json({ authed: isAuthed(req) })); |
|
|
| |
| app.use(express.static(PUBLIC, { index: false })); |
|
|
| |
| app.use("/api", requireApiAuth); |
|
|
| |
| const bool = (v) => (v ? 1 : 0); |
| |
| const h = (fn) => (req, res) => |
| fn(req, res).catch((e) => { |
| console.error(e); |
| if (!res.headersSent) res.status(500).json({ error: e.message }); |
| }); |
|
|
| function buildCategoryTree(rows) { |
| const map = new Map(); |
| rows.forEach((r) => map.set(r.id, { ...r, children: [] })); |
| const roots = []; |
| for (const cat of map.values()) { |
| if (cat.parent_id && map.has(cat.parent_id)) { |
| map.get(cat.parent_id).children.push(cat); |
| } else { |
| roots.push(cat); |
| } |
| } |
| return roots; |
| } |
|
|
| |
| |
| |
| app.get("/api/categories", h(async (req, res) => { |
| const { kind } = req.query; |
| const rows = kind |
| ? await all("SELECT * FROM categories WHERE kind = ? ORDER BY id", [kind]) |
| : await all("SELECT * FROM categories ORDER BY id"); |
| res.json({ flat: rows, tree: buildCategoryTree(rows) }); |
| })); |
|
|
| app.post("/api/categories", h(async (req, res) => { |
| const { name, parent_id = null, kind = "task", color = null } = req.body; |
| if (!name?.trim()) return res.status(400).json({ error: "İsim gerekli" }); |
| const info = await run( |
| "INSERT INTO categories (name, parent_id, kind, color) VALUES (?, ?, ?, ?)", |
| [name.trim(), parent_id || null, kind, color] |
| ); |
| res.json(await get("SELECT * FROM categories WHERE id = ?", [info.lastInsertRowid])); |
| })); |
|
|
| app.put("/api/categories/:id", h(async (req, res) => { |
| const { name, color, parent_id } = req.body; |
| const cur = await get("SELECT * FROM categories WHERE id = ?", [req.params.id]); |
| if (!cur) return res.status(404).json({ error: "Bulunamadı" }); |
| await run("UPDATE categories SET name = ?, color = ?, parent_id = ? WHERE id = ?", [ |
| name ?? cur.name, |
| color ?? cur.color, |
| parent_id === undefined ? cur.parent_id : parent_id, |
| req.params.id, |
| ]); |
| res.json(await get("SELECT * FROM categories WHERE id = ?", [req.params.id])); |
| })); |
|
|
| app.delete("/api/categories/:id", h(async (req, res) => { |
| await run("DELETE FROM categories WHERE id = ?", [req.params.id]); |
| res.json({ ok: true }); |
| })); |
|
|
| |
| |
| |
| app.get("/api/todos", h(async (req, res) => { |
| const { category_id, priority, status, due_from, due_to, search } = req.query; |
| const where = []; |
| const params = []; |
|
|
| if (category_id) { |
| |
| const subIds = ( |
| await all("SELECT id FROM categories WHERE id = ? OR parent_id = ?", [category_id, category_id]) |
| ).map((r) => r.id); |
| where.push(`t.category_id IN (${subIds.map(() => "?").join(",")})`); |
| params.push(...subIds); |
| } |
| if (priority) { |
| where.push("t.priority = ?"); |
| params.push(priority); |
| } |
| if (status === "active") where.push("t.completed = 0"); |
| if (status === "done") where.push("t.completed = 1"); |
| if (due_from) { |
| where.push("t.due_date >= ?"); |
| params.push(due_from); |
| } |
| if (due_to) { |
| where.push("t.due_date <= ?"); |
| params.push(due_to); |
| } |
| if (search) { |
| where.push("(t.title LIKE ? OR t.description LIKE ?)"); |
| params.push(`%${search}%`, `%${search}%`); |
| } |
|
|
| const sql = ` |
| SELECT t.*, c.name AS category_name, c.color AS category_color |
| FROM todos t LEFT JOIN categories c ON c.id = t.category_id |
| ${where.length ? "WHERE " + where.join(" AND ") : ""} |
| ORDER BY t.completed ASC, t.sort_order ASC, t.created_at DESC`; |
| res.json(await all(sql, params)); |
| })); |
|
|
| |
| app.put("/api/todos/reorder", h(async (req, res) => { |
| const { ids } = req.body; |
| if (!Array.isArray(ids)) return res.status(400).json({ error: "ids dizisi gerekli" }); |
| await client.batch( |
| ids.map((id, i) => ({ sql: "UPDATE todos SET sort_order = ? WHERE id = ?", args: [i, id] })), |
| "write" |
| ); |
| res.json({ ok: true }); |
| })); |
|
|
| app.post("/api/todos", h(async (req, res) => { |
| const { title, description = "", category_id = null, priority = "P3", due_date = null } = req.body; |
| if (!title?.trim()) return res.status(400).json({ error: "Başlık gerekli" }); |
| |
| const minOrder = (await get("SELECT MIN(sort_order) AS m FROM todos")).m; |
| const sortOrder = (minOrder ?? 0) - 1; |
| const info = await run( |
| `INSERT INTO todos (title, description, category_id, priority, due_date, sort_order) |
| VALUES (?, ?, ?, ?, ?, ?)`, |
| [title.trim(), description, category_id || null, priority, due_date || null, sortOrder] |
| ); |
| res.json(await get("SELECT * FROM todos WHERE id = ?", [info.lastInsertRowid])); |
| })); |
|
|
| app.put("/api/todos/:id", h(async (req, res) => { |
| const cur = await get("SELECT * FROM todos WHERE id = ?", [req.params.id]); |
| if (!cur) return res.status(404).json({ error: "Bulunamadı" }); |
| const b = req.body; |
| await run( |
| `UPDATE todos SET |
| title = ?, description = ?, category_id = ?, priority = ?, |
| due_date = ?, completed = ?, updated_at = datetime('now') |
| WHERE id = ?`, |
| [ |
| b.title ?? cur.title, |
| b.description ?? cur.description, |
| b.category_id === undefined ? cur.category_id : b.category_id || null, |
| b.priority ?? cur.priority, |
| b.due_date === undefined ? cur.due_date : b.due_date || null, |
| b.completed === undefined ? cur.completed : bool(b.completed), |
| req.params.id, |
| ] |
| ); |
| res.json(await get("SELECT * FROM todos WHERE id = ?", [req.params.id])); |
| })); |
|
|
| app.delete("/api/todos/:id", h(async (req, res) => { |
| await run("DELETE FROM todos WHERE id = ?", [req.params.id]); |
| res.json({ ok: true }); |
| })); |
|
|
| |
| |
| |
| |
| async function descendantCategoryIds(rootId) { |
| const cats = await all("SELECT id, parent_id FROM categories"); |
| const ids = [Number(rootId)]; |
| let added = true; |
| while (added) { |
| added = false; |
| for (const c of cats) { |
| if (c.parent_id && ids.includes(c.parent_id) && !ids.includes(c.id)) { |
| ids.push(c.id); |
| added = true; |
| } |
| } |
| } |
| return ids; |
| } |
|
|
| async function queryNotes({ category_id, search } = {}) { |
| const where = []; |
| const params = []; |
| if (category_id) { |
| const ids = await descendantCategoryIds(category_id); |
| where.push(`n.category_id IN (${ids.map(() => "?").join(",")})`); |
| params.push(...ids); |
| } |
| if (search) { |
| where.push("(n.title LIKE ? OR n.content LIKE ?)"); |
| params.push(`%${search}%`, `%${search}%`); |
| } |
| return all( |
| `SELECT n.*, c.name AS category_name, c.color AS category_color |
| FROM notes n LEFT JOIN categories c ON c.id = n.category_id |
| ${where.length ? "WHERE " + where.join(" AND ") : ""} |
| ORDER BY n.pinned DESC, n.updated_at DESC`, |
| params |
| ); |
| } |
|
|
| app.get("/api/notes", h(async (req, res) => { |
| res.json(await queryNotes(req.query)); |
| })); |
|
|
| app.post("/api/notes", h(async (req, res) => { |
| const { title, content = "", category_id = null, pinned = false } = req.body; |
| if (!title?.trim()) return res.status(400).json({ error: "Başlık gerekli" }); |
| const info = await run( |
| "INSERT INTO notes (title, content, category_id, pinned) VALUES (?, ?, ?, ?)", |
| [title.trim(), content, category_id || null, bool(pinned)] |
| ); |
| res.json(await get("SELECT * FROM notes WHERE id = ?", [info.lastInsertRowid])); |
| })); |
|
|
| app.put("/api/notes/:id", h(async (req, res) => { |
| const cur = await get("SELECT * FROM notes WHERE id = ?", [req.params.id]); |
| if (!cur) return res.status(404).json({ error: "Bulunamadı" }); |
| const b = req.body; |
| await run( |
| `UPDATE notes SET title = ?, content = ?, category_id = ?, pinned = ?, updated_at = datetime('now') WHERE id = ?`, |
| [ |
| b.title ?? cur.title, |
| b.content ?? cur.content, |
| b.category_id === undefined ? cur.category_id : b.category_id || null, |
| b.pinned === undefined ? cur.pinned : bool(b.pinned), |
| req.params.id, |
| ] |
| ); |
| res.json(await get("SELECT * FROM notes WHERE id = ?", [req.params.id])); |
| })); |
|
|
| app.delete("/api/notes/:id", h(async (req, res) => { |
| await run("DELETE FROM notes WHERE id = ?", [req.params.id]); |
| res.json({ ok: true }); |
| })); |
|
|
| |
| |
| |
| function noteById(id) { |
| return get( |
| `SELECT n.*, c.name AS category_name FROM notes n |
| LEFT JOIN categories c ON c.id = n.category_id WHERE n.id = ?`, |
| [id] |
| ); |
| } |
|
|
| const dl = (res, name) => { |
| |
| const ascii = name.replace(/[^\x20-\x7E]/g, "_").replace(/"/g, "'"); |
| res.setHeader( |
| "Content-Disposition", |
| `attachment; filename="${ascii}"; filename*=UTF-8''${encodeURIComponent(name)}` |
| ); |
| }; |
| const stamp = () => new Date().toISOString().slice(0, 10); |
|
|
| |
| app.get("/api/notes/export/md", h(async (req, res) => { |
| const notes = await queryNotes(req.query); |
| const cats = await all("SELECT * FROM categories"); |
| const files = []; |
| const used = new Set(); |
| for (const n of notes) { |
| const folder = n.category_id ? categoryPath(n.category_id, cats) : "Kategorisiz"; |
| let entry = `${folder}/${safeName(n.title)}.md`; |
| let i = 2; |
| while (used.has(entry)) entry = `${folder}/${safeName(n.title)} (${i++}).md`; |
| used.add(entry); |
| files.push({ name: entry, data: noteToMarkdown(n, folder) }); |
| } |
| if (!files.length) files.push({ name: "BOS.md", data: "Not bulunamadı.\n" }); |
| const zip = makeZip(files); |
| res.setHeader("Content-Type", "application/zip"); |
| dl(res, `notlar-${stamp()}.zip`); |
| res.end(zip); |
| })); |
|
|
| |
| app.get("/api/notes/export/pdf", h(async (req, res) => { |
| const notes = await queryNotes(req.query); |
| const cats = await all("SELECT * FROM categories"); |
| const pathFn = (id) => (id ? categoryPath(id, cats) : ""); |
| const html = notesToHtml(notes, pathFn, "Bilgi Merkezi"); |
| const pdf = await htmlToPdf(html); |
| res.setHeader("Content-Type", "application/pdf"); |
| dl(res, `notlar-${stamp()}.pdf`); |
| res.end(pdf); |
| })); |
|
|
| |
| app.get("/api/notes/:id/export/md", h(async (req, res) => { |
| const note = await noteById(req.params.id); |
| if (!note) return res.status(404).json({ error: "Bulunamadı" }); |
| const cats = await all("SELECT * FROM categories"); |
| const folder = note.category_id ? categoryPath(note.category_id, cats) : ""; |
| res.setHeader("Content-Type", "text/markdown; charset=utf-8"); |
| dl(res, `${safeName(note.title)}.md`); |
| res.end(noteToMarkdown(note, folder)); |
| })); |
|
|
| |
| app.get("/api/notes/:id/export/pdf", h(async (req, res) => { |
| const note = await noteById(req.params.id); |
| if (!note) return res.status(404).json({ error: "Bulunamadı" }); |
| const cats = await all("SELECT * FROM categories"); |
| const pathFn = (id) => (id ? categoryPath(id, cats) : ""); |
| const html = notesToHtml([note], pathFn, note.title); |
| const pdf = await htmlToPdf(html); |
| res.setHeader("Content-Type", "application/pdf"); |
| dl(res, `${safeName(note.title)}.pdf`); |
| res.end(pdf); |
| })); |
|
|
| |
| app.get("*", (req, res) => { |
| if (!isAuthed(req)) return res.sendFile(path.join(PUBLIC, "login.html")); |
| res.sendFile(path.join(PUBLIC, "index.html")); |
| }); |
|
|
| |
| await initDb(); |
| app.listen(PORT, () => { |
| console.log(`\n🚀 Uygulama çalışıyor: http://localhost:${PORT}\n`); |
| }); |
|
|