Feat: Add Login Page & Auth Method
增加登录界面和授权验证逻辑
This commit is contained in:
@@ -8,7 +8,10 @@
|
||||
"start": "node src/index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fastify/cookie": "^11.0.0",
|
||||
"@fastify/session": "^11.0.0",
|
||||
"@fastify/static": "^8.0.0",
|
||||
"bcryptjs": "^3.0.2",
|
||||
"better-sqlite3": "^11.0.0",
|
||||
"fastify": "^5.0.0"
|
||||
}
|
||||
|
||||
@@ -1,21 +1,32 @@
|
||||
import Database from "better-sqlite3";
|
||||
import { readFileSync } from "fs";
|
||||
import { readFileSync, mkdirSync } from "fs";
|
||||
import { join, dirname } from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import bcrypt from "bcryptjs";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
const DB_PATH = join(__dirname, "../../data/cicd.db");
|
||||
|
||||
// ensure data dir exists
|
||||
import { mkdirSync } from "fs";
|
||||
mkdirSync(join(__dirname, "../../data"), { recursive: true });
|
||||
|
||||
const db = new Database(DB_PATH);
|
||||
const db = new Database(join(__dirname, "../../data/cicd.db"));
|
||||
db.pragma("journal_mode = WAL");
|
||||
db.pragma("foreign_keys = ON");
|
||||
|
||||
const schema = readFileSync(join(__dirname, "schema.sql"), "utf8");
|
||||
db.exec(schema);
|
||||
db.exec(readFileSync(join(__dirname, "schema.sql"), "utf8"));
|
||||
|
||||
// Seed default admin if no users exist
|
||||
const userCount = db.prepare("SELECT COUNT(*) as c FROM users").get().c;
|
||||
if (userCount === 0) {
|
||||
const username = process.env.ADMIN_USER || "admin";
|
||||
const password = process.env.ADMIN_PASS || "admin123";
|
||||
const hash = bcrypt.hashSync(password, 10);
|
||||
db.prepare("INSERT INTO users (username, password_hash) VALUES (?, ?)").run(
|
||||
username,
|
||||
hash
|
||||
);
|
||||
console.log(
|
||||
`[cicd] Default admin created: ${username} / ${password} ← change this!`
|
||||
);
|
||||
}
|
||||
|
||||
export default db;
|
||||
|
||||
@@ -7,6 +7,13 @@ CREATE TABLE IF NOT EXISTS projects (
|
||||
created_at INTEGER DEFAULT (unixepoch())
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
username TEXT NOT NULL UNIQUE,
|
||||
password_hash TEXT NOT NULL,
|
||||
created_at INTEGER DEFAULT (unixepoch())
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS builds (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
project_id INTEGER NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { randomBytes } from "crypto";
|
||||
import Fastify from "fastify";
|
||||
import staticPlugin from "@fastify/static";
|
||||
import cookie from "@fastify/cookie";
|
||||
import session from "@fastify/session";
|
||||
import { join, dirname } from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import { existsSync } from "fs";
|
||||
@@ -7,22 +10,53 @@ import { existsSync } from "fs";
|
||||
import projectRoutes from "./routes/projects.js";
|
||||
import buildRoutes from "./routes/builds.js";
|
||||
import webhookRoutes from "./routes/webhook.js";
|
||||
import authRoutes from "./routes/auth.js";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
const fastify = Fastify({ logger: true });
|
||||
|
||||
// Store raw body for webhook signature verification
|
||||
fastify.addContentTypeParser("application/json", { parseAs: "buffer" }, (req, body, done) => {
|
||||
req.rawBody = body;
|
||||
try {
|
||||
done(null, JSON.parse(body.toString()));
|
||||
} catch (err) {
|
||||
done(err);
|
||||
// Raw body for webhook signature verification
|
||||
fastify.addContentTypeParser(
|
||||
"application/json",
|
||||
{ parseAs: "buffer" },
|
||||
(req, body, done) => {
|
||||
req.rawBody = body;
|
||||
try {
|
||||
done(null, JSON.parse(body.toString()));
|
||||
} catch (err) {
|
||||
done(err);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Auth plugins
|
||||
await fastify.register(cookie);
|
||||
await fastify.register(session, {
|
||||
secret: process.env.SESSION_SECRET || randomBytes(32).toString("hex"),
|
||||
saveUninitialized: false,
|
||||
rolling: false,
|
||||
cookie: {
|
||||
secure: false,
|
||||
httpOnly: true,
|
||||
maxAge: 7 * 24 * 3600 * 1000,
|
||||
},
|
||||
});
|
||||
|
||||
// Auth guard: protect all /api/* except login, me, and webhooks
|
||||
const AUTH_WHITELIST = ["/api/auth/login", "/api/auth/me"];
|
||||
fastify.addHook("onRequest", async (request, reply) => {
|
||||
const { url, method } = request;
|
||||
if (!url.startsWith("/api/")) return;
|
||||
if (AUTH_WHITELIST.includes(url)) return;
|
||||
if (url.startsWith("/api/webhook/")) return;
|
||||
if (!request.session.userId) {
|
||||
return reply.code(401).send({ error: "Not authenticated" });
|
||||
}
|
||||
});
|
||||
|
||||
// API routes
|
||||
// Routes
|
||||
fastify.register(authRoutes);
|
||||
fastify.register(projectRoutes);
|
||||
fastify.register(buildRoutes);
|
||||
fastify.register(webhookRoutes);
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import bcrypt from "bcryptjs";
|
||||
import db from "../db/index.js";
|
||||
|
||||
export default async function authRoutes(fastify) {
|
||||
fastify.post("/api/auth/login", async (request, reply) => {
|
||||
const { username, password } = request.body ?? {};
|
||||
if (!username || !password)
|
||||
return reply.code(400).send({ error: "username and password required" });
|
||||
|
||||
const user = db
|
||||
.prepare("SELECT * FROM users WHERE username = ?")
|
||||
.get(username);
|
||||
if (!user || !(await bcrypt.compare(password, user.password_hash)))
|
||||
return reply.code(401).send({ error: "Invalid credentials" });
|
||||
|
||||
request.session.userId = user.id;
|
||||
request.session.username = user.username;
|
||||
return { id: user.id, username: user.username };
|
||||
});
|
||||
|
||||
fastify.post("/api/auth/logout", async (request, reply) => {
|
||||
await request.session.destroy();
|
||||
return reply.code(204).send();
|
||||
});
|
||||
|
||||
fastify.get("/api/auth/me", async (request, reply) => {
|
||||
if (!request.session.userId)
|
||||
return reply.code(401).send({ error: "Not authenticated" });
|
||||
return { id: request.session.userId, username: request.session.username };
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user