Shinhati2023 commited on
Commit
82c6fa5
·
verified ·
1 Parent(s): 140bf22

Create server.js

Browse files
Files changed (1) hide show
  1. server.js +251 -0
server.js ADDED
@@ -0,0 +1,251 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import express from "express";
2
+ import helmet from "helmet";
3
+ import session from "express-session";
4
+ import cookieParser from "cookie-parser";
5
+ import rateLimit from "express-rate-limit";
6
+ import bcrypt from "bcryptjs";
7
+ import db from "./db.js";
8
+
9
+ const app = express();
10
+
11
+ // ---- config ----
12
+ const PORT = process.env.PORT || 7860;
13
+
14
+ // Set these in HF Space “Secrets”:
15
+ // ADMIN_USERNAME, ADMIN_PASSWORD
16
+ const ADMIN_USERNAME = process.env.ADMIN_USERNAME || "admin";
17
+ const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD || null;
18
+
19
+ // Optional: allow your Netlify domain to read the API
20
+ const ALLOWED_ORIGIN = process.env.ALLOWED_ORIGIN || "https://tektrey.online";
21
+
22
+ // ---- security basics ----
23
+ app.use(helmet({
24
+ contentSecurityPolicy: false // keep simple for now; can harden later
25
+ }));
26
+ app.use(cookieParser());
27
+ app.use(express.json({ limit: "1mb" }));
28
+
29
+ app.set("trust proxy", 1);
30
+
31
+ app.use(session({
32
+ name: "tektrey.sid",
33
+ secret: process.env.SESSION_SECRET || "change-me-in-secrets",
34
+ resave: false,
35
+ saveUninitialized: false,
36
+ cookie: {
37
+ httpOnly: true,
38
+ sameSite: "lax",
39
+ secure: true // HF is https
40
+ }
41
+ }));
42
+
43
+ // Rate limit login + write endpoints
44
+ const writeLimiter = rateLimit({
45
+ windowMs: 60_000,
46
+ max: 60
47
+ });
48
+ app.use("/api/", writeLimiter);
49
+
50
+ // CORS (minimal)
51
+ app.use((req, res, next) => {
52
+ res.setHeader("Access-Control-Allow-Origin", ALLOWED_ORIGIN);
53
+ res.setHeader("Vary", "Origin");
54
+ res.setHeader("Access-Control-Allow-Credentials", "true");
55
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type");
56
+ res.setHeader("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,OPTIONS");
57
+ if (req.method === "OPTIONS") return res.sendStatus(204);
58
+ next();
59
+ });
60
+
61
+ // ---- seed admin user if not exists (requires ADMIN_PASSWORD set once) ----
62
+ function ensureAdminUser() {
63
+ const existing = db.prepare("SELECT * FROM users WHERE username=?").get(ADMIN_USERNAME);
64
+ if (existing) return;
65
+
66
+ if (!ADMIN_PASSWORD) {
67
+ console.warn("[WARN] No ADMIN_PASSWORD set; cannot seed initial admin user.");
68
+ return;
69
+ }
70
+
71
+ const password_hash = bcrypt.hashSync(ADMIN_PASSWORD, 12);
72
+ db.prepare("INSERT INTO users (username, password_hash, role) VALUES (?, ?, 'admin')")
73
+ .run(ADMIN_USERNAME, password_hash);
74
+
75
+ console.log("[OK] Seeded admin user:", ADMIN_USERNAME);
76
+ }
77
+ ensureAdminUser();
78
+
79
+ function requireAuth(req, res, next) {
80
+ if (!req.session?.user) return res.status(401).json({ error: "Unauthorized" });
81
+ next();
82
+ }
83
+
84
+ function audit(actor, action, entity, entity_id = null, meta = {}) {
85
+ db.prepare(`
86
+ INSERT INTO audit_log (actor, action, entity, entity_id, meta_json)
87
+ VALUES (?, ?, ?, ?, ?)
88
+ `).run(actor, action, entity, entity_id, JSON.stringify(meta));
89
+ }
90
+
91
+ // ---- static admin UI ----
92
+ app.use("/admin", express.static("public/admin", { fallthrough: true }));
93
+ app.get("/admin/*", (req, res) => res.sendFile(process.cwd() + "/public/admin/index.html"));
94
+
95
+ // ---- auth ----
96
+ app.post("/api/login", (req, res) => {
97
+ const { username, password } = req.body || {};
98
+ const user = db.prepare("SELECT username, password_hash, role FROM users WHERE username=?").get(username);
99
+ if (!user) return res.status(401).json({ error: "Invalid credentials" });
100
+
101
+ const ok = bcrypt.compareSync(password, user.password_hash);
102
+ if (!ok) return res.status(401).json({ error: "Invalid credentials" });
103
+
104
+ req.session.user = { username: user.username, role: user.role };
105
+ audit(user.username, "login", "auth", null, {});
106
+ res.json({ ok: true, user: req.session.user });
107
+ });
108
+
109
+ app.post("/api/logout", requireAuth, (req, res) => {
110
+ const actor = req.session.user.username;
111
+ req.session.destroy(() => {
112
+ audit(actor, "logout", "auth", null, {});
113
+ res.json({ ok: true });
114
+ });
115
+ });
116
+
117
+ app.get("/api/me", (req, res) => {
118
+ res.json({ user: req.session?.user || null });
119
+ });
120
+
121
+ // ---- PUBLIC content endpoint (Netlify site reads this) ----
122
+ app.get("/api/public/content", (req, res) => {
123
+ const content = db.prepare("SELECT hero_headline, hero_subtitle, updated_at FROM content WHERE id=1").get();
124
+ const skills = db.prepare("SELECT id, label, percent FROM skills ORDER BY sort ASC, id ASC").all();
125
+ const projects = db.prepare(`
126
+ SELECT id, title, summary, stack, links_json, featured, sort, updated_at
127
+ FROM projects
128
+ ORDER BY featured DESC, sort ASC, id DESC
129
+ `).all().map(p => ({ ...p, links: JSON.parse(p.links_json || "[]") }));
130
+
131
+ res.json({ content, skills, projects });
132
+ });
133
+
134
+ // ---- ADMIN CRUD ----
135
+ app.get("/api/admin/content", requireAuth, (req, res) => {
136
+ const content = db.prepare("SELECT hero_headline, hero_subtitle, updated_at FROM content WHERE id=1").get();
137
+ res.json({ content });
138
+ });
139
+
140
+ app.put("/api/admin/content", requireAuth, (req, res) => {
141
+ const { hero_headline, hero_subtitle } = req.body || {};
142
+ if (!hero_headline || !hero_subtitle) return res.status(400).json({ error: "Missing fields" });
143
+
144
+ db.prepare(`
145
+ UPDATE content
146
+ SET hero_headline=?, hero_subtitle=?, updated_at=datetime('now')
147
+ WHERE id=1
148
+ `).run(hero_headline.trim(), hero_subtitle.trim());
149
+
150
+ audit(req.session.user.username, "update", "content", 1, {});
151
+ res.json({ ok: true });
152
+ });
153
+
154
+ // Skills
155
+ app.get("/api/admin/skills", requireAuth, (req, res) => {
156
+ const skills = db.prepare("SELECT id, label, percent, sort FROM skills ORDER BY sort ASC, id ASC").all();
157
+ res.json({ skills });
158
+ });
159
+
160
+ app.post("/api/admin/skills", requireAuth, (req, res) => {
161
+ const { label, percent, sort = 0 } = req.body || {};
162
+ if (!label) return res.status(400).json({ error: "Label required" });
163
+ const pct = Number(percent);
164
+ if (!Number.isFinite(pct) || pct < 0 || pct > 100) return res.status(400).json({ error: "Percent 0-100" });
165
+
166
+ const info = db.prepare("INSERT INTO skills (label, percent, sort) VALUES (?, ?, ?)").run(label.trim(), pct, sort);
167
+ audit(req.session.user.username, "create", "skill", info.lastInsertRowid, { label, percent: pct });
168
+ res.json({ ok: true, id: info.lastInsertRowid });
169
+ });
170
+
171
+ app.put("/api/admin/skills/:id", requireAuth, (req, res) => {
172
+ const id = Number(req.params.id);
173
+ const { label, percent, sort = 0 } = req.body || {};
174
+ const pct = Number(percent);
175
+ if (!label) return res.status(400).json({ error: "Label required" });
176
+ if (!Number.isFinite(pct) || pct < 0 || pct > 100) return res.status(400).json({ error: "Percent 0-100" });
177
+
178
+ db.prepare("UPDATE skills SET label=?, percent=?, sort=? WHERE id=?").run(label.trim(), pct, sort, id);
179
+ audit(req.session.user.username, "update", "skill", id, { label, percent: pct });
180
+ res.json({ ok: true });
181
+ });
182
+
183
+ app.delete("/api/admin/skills/:id", requireAuth, (req, res) => {
184
+ const id = Number(req.params.id);
185
+ db.prepare("DELETE FROM skills WHERE id=?").run(id);
186
+ audit(req.session.user.username, "delete", "skill", id, {});
187
+ res.json({ ok: true });
188
+ });
189
+
190
+ // Projects
191
+ app.get("/api/admin/projects", requireAuth, (req, res) => {
192
+ const projects = db.prepare(`
193
+ SELECT id, title, summary, stack, links_json, featured, sort, updated_at
194
+ FROM projects
195
+ ORDER BY featured DESC, sort ASC, id DESC
196
+ `).all().map(p => ({ ...p, links: JSON.parse(p.links_json || "[]") }));
197
+
198
+ res.json({ projects });
199
+ });
200
+
201
+ app.post("/api/admin/projects", requireAuth, (req, res) => {
202
+ const { title, summary, stack = "", links = [], featured = 0, sort = 0 } = req.body || {};
203
+ if (!title || !summary) return res.status(400).json({ error: "Title and summary required" });
204
+
205
+ const info = db.prepare(`
206
+ INSERT INTO projects (title, summary, stack, links_json, featured, sort)
207
+ VALUES (?, ?, ?, ?, ?, ?)
208
+ `).run(
209
+ title.trim(),
210
+ summary.trim(),
211
+ String(stack || "").trim(),
212
+ JSON.stringify(Array.isArray(links) ? links : []),
213
+ featured ? 1 : 0,
214
+ sort
215
+ );
216
+
217
+ audit(req.session.user.username, "create", "project", info.lastInsertRowid, { title });
218
+ res.json({ ok: true, id: info.lastInsertRowid });
219
+ });
220
+
221
+ app.put("/api/admin/projects/:id", requireAuth, (req, res) => {
222
+ const id = Number(req.params.id);
223
+ const { title, summary, stack = "", links = [], featured = 0, sort = 0 } = req.body || {};
224
+ if (!title || !summary) return res.status(400).json({ error: "Title and summary required" });
225
+
226
+ db.prepare(`
227
+ UPDATE projects
228
+ SET title=?, summary=?, stack=?, links_json=?, featured=?, sort=?, updated_at=datetime('now')
229
+ WHERE id=?
230
+ `).run(
231
+ title.trim(),
232
+ summary.trim(),
233
+ String(stack || "").trim(),
234
+ JSON.stringify(Array.isArray(links) ? links : []),
235
+ featured ? 1 : 0,
236
+ sort,
237
+ id
238
+ );
239
+
240
+ audit(req.session.user.username, "update", "project", id, { title });
241
+ res.json({ ok: true });
242
+ });
243
+
244
+ app.delete("/api/admin/projects/:id", requireAuth, (req, res) => {
245
+ const id = Number(req.params.id);
246
+ db.prepare("DELETE FROM projects WHERE id=?").run(id);
247
+ audit(req.session.user.username, "delete", "project", id, {});
248
+ res.json({ ok: true });
249
+ });
250
+
251
+ app.listen(PORT, () => console.log(`Admin/API running on :${PORT}`));