parent
6ff64d4c80
commit
19d14bd6a3
|
|
@ -1,19 +1,40 @@
|
||||||
import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom";
|
import { BrowserRouter, Routes, Route, Navigate, Outlet, useNavigate } from "react-router-dom";
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
import Projects from "./pages/Projects";
|
import Projects from "./pages/Projects";
|
||||||
import ProjectDetail from "./pages/ProjectDetail";
|
import ProjectDetail from "./pages/ProjectDetail";
|
||||||
import BuildDetail from "./pages/BuildDetail";
|
import BuildDetail from "./pages/BuildDetail";
|
||||||
|
import Login from "./pages/Login";
|
||||||
import Layout from "./components/Layout";
|
import Layout from "./components/Layout";
|
||||||
|
|
||||||
|
function AuthGuard() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [state, setState] = useState<"loading" | "ok" | "unauth">("loading");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetch("/api/auth/me").then((r) => {
|
||||||
|
if (r.ok) setState("ok");
|
||||||
|
else setState("unauth");
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (state === "loading") return null;
|
||||||
|
if (state === "unauth") return <Navigate to="/login" replace />;
|
||||||
|
return <Outlet />;
|
||||||
|
}
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
return (
|
return (
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<Routes>
|
<Routes>
|
||||||
|
<Route path="/login" element={<Login />} />
|
||||||
|
<Route element={<AuthGuard />}>
|
||||||
<Route element={<Layout />}>
|
<Route element={<Layout />}>
|
||||||
<Route path="/" element={<Navigate to="/projects" replace />} />
|
<Route path="/" element={<Navigate to="/projects" replace />} />
|
||||||
<Route path="/projects" element={<Projects />} />
|
<Route path="/projects" element={<Projects />} />
|
||||||
<Route path="/projects/:id" element={<ProjectDetail />} />
|
<Route path="/projects/:id" element={<ProjectDetail />} />
|
||||||
<Route path="/builds/:id" element={<BuildDetail />} />
|
<Route path="/builds/:id" element={<BuildDetail />} />
|
||||||
</Route>
|
</Route>
|
||||||
|
</Route>
|
||||||
</Routes>
|
</Routes>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,30 @@
|
||||||
import { Outlet, NavLink } from "react-router-dom";
|
import { Outlet, NavLink, useNavigate } from "react-router-dom";
|
||||||
import { GitBranch } from "lucide-react";
|
import { GitBranch, LogOut } from "lucide-react";
|
||||||
|
|
||||||
export default function Layout() {
|
export default function Layout() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const logout = async () => {
|
||||||
|
await fetch("/api/auth/logout", { method: "POST" });
|
||||||
|
navigate("/login", { replace: true });
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-950 text-gray-100">
|
<div className="min-h-screen bg-gray-950 text-gray-100">
|
||||||
<header className="border-b border-gray-800 px-6 py-4 flex items-center gap-3">
|
<header className="border-b border-gray-800 px-6 py-4 flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
<GitBranch className="w-5 h-5 text-blue-400" />
|
<GitBranch className="w-5 h-5 text-blue-400" />
|
||||||
<NavLink
|
<NavLink to="/projects" className="text-lg font-semibold tracking-tight">
|
||||||
to="/projects"
|
|
||||||
className="text-lg font-semibold tracking-tight"
|
|
||||||
>
|
|
||||||
Paul CI/CD
|
Paul CI/CD
|
||||||
</NavLink>
|
</NavLink>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={logout}
|
||||||
|
className="flex items-center gap-1.5 text-xs text-gray-400 hover:text-gray-200 transition-colors"
|
||||||
|
>
|
||||||
|
<LogOut className="w-3.5 h-3.5" />
|
||||||
|
Logout
|
||||||
|
</button>
|
||||||
</header>
|
</header>
|
||||||
<main className="max-w-5xl mx-auto px-6 py-8">
|
<main className="max-w-5xl mx-auto px-6 py-8">
|
||||||
<Outlet />
|
<Outlet />
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,74 @@
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { GitBranch } from "lucide-react";
|
||||||
|
|
||||||
|
export default function Login() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [form, setForm] = useState({ username: "", password: "" });
|
||||||
|
const [error, setError] = useState("");
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const submit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setError("");
|
||||||
|
setLoading(true);
|
||||||
|
const res = await fetch("/api/auth/login", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(form),
|
||||||
|
});
|
||||||
|
setLoading(false);
|
||||||
|
if (res.ok) {
|
||||||
|
navigate("/projects", { replace: true });
|
||||||
|
} else {
|
||||||
|
const data = await res.json();
|
||||||
|
setError(data.error || "Login failed");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-950 flex items-center justify-center px-4">
|
||||||
|
<div className="w-full max-w-sm">
|
||||||
|
<div className="flex items-center justify-center gap-2 mb-8">
|
||||||
|
<GitBranch className="w-6 h-6 text-blue-400" />
|
||||||
|
<span className="text-xl font-semibold text-gray-100">Paul CI/CD</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form
|
||||||
|
onSubmit={submit}
|
||||||
|
className="bg-gray-900 text-white border border-gray-800 rounded-lg p-6 space-y-4"
|
||||||
|
>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<label className="text-xs text-gray-400">Username</label>
|
||||||
|
<input
|
||||||
|
autoFocus
|
||||||
|
value={form.username}
|
||||||
|
onChange={(e) => setForm((f) => ({ ...f, username: e.target.value }))}
|
||||||
|
className="w-full bg-gray-800 border border-gray-700 rounded-md px-3 py-2 text-sm outline-none focus:border-blue-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-1">
|
||||||
|
<label className="text-xs text-gray-400">Password</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
value={form.password}
|
||||||
|
onChange={(e) => setForm((f) => ({ ...f, password: e.target.value }))}
|
||||||
|
className="w-full bg-gray-800 border border-gray-700 rounded-md px-3 py-2 text-sm outline-none focus:border-blue-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && <p className="text-xs text-red-400">{error}</p>}
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={loading}
|
||||||
|
className="w-full bg-blue-600 hover:bg-blue-500 disabled:opacity-50 text-white text-sm py-2 rounded-md transition-colors"
|
||||||
|
>
|
||||||
|
{loading ? "Signing in..." : "Sign in"}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1 +1 @@
|
||||||
{"root":["./src/app.tsx","./src/main.tsx","./src/components/layout.tsx","./src/pages/builddetail.tsx","./src/pages/projectdetail.tsx","./src/pages/projects.tsx"],"version":"5.9.3"}
|
{"root":["./src/app.tsx","./src/main.tsx","./src/components/layout.tsx","./src/pages/builddetail.tsx","./src/pages/login.tsx","./src/pages/projectdetail.tsx","./src/pages/projects.tsx"],"version":"5.9.3"}
|
||||||
|
|
@ -8,7 +8,10 @@
|
||||||
"start": "node src/index.js"
|
"start": "node src/index.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@fastify/cookie": "^11.0.0",
|
||||||
|
"@fastify/session": "^11.0.0",
|
||||||
"@fastify/static": "^8.0.0",
|
"@fastify/static": "^8.0.0",
|
||||||
|
"bcryptjs": "^3.0.2",
|
||||||
"better-sqlite3": "^11.0.0",
|
"better-sqlite3": "^11.0.0",
|
||||||
"fastify": "^5.0.0"
|
"fastify": "^5.0.0"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,32 @@
|
||||||
import Database from "better-sqlite3";
|
import Database from "better-sqlite3";
|
||||||
import { readFileSync } from "fs";
|
import { readFileSync, mkdirSync } from "fs";
|
||||||
import { join, dirname } from "path";
|
import { join, dirname } from "path";
|
||||||
import { fileURLToPath } from "url";
|
import { fileURLToPath } from "url";
|
||||||
|
import bcrypt from "bcryptjs";
|
||||||
|
|
||||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
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 });
|
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("journal_mode = WAL");
|
||||||
db.pragma("foreign_keys = ON");
|
db.pragma("foreign_keys = ON");
|
||||||
|
|
||||||
const schema = readFileSync(join(__dirname, "schema.sql"), "utf8");
|
db.exec(readFileSync(join(__dirname, "schema.sql"), "utf8"));
|
||||||
db.exec(schema);
|
|
||||||
|
// 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;
|
export default db;
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,13 @@ CREATE TABLE IF NOT EXISTS projects (
|
||||||
created_at INTEGER DEFAULT (unixepoch())
|
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 (
|
CREATE TABLE IF NOT EXISTS builds (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
project_id INTEGER NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
project_id INTEGER NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
|
import { randomBytes } from "crypto";
|
||||||
import Fastify from "fastify";
|
import Fastify from "fastify";
|
||||||
import staticPlugin from "@fastify/static";
|
import staticPlugin from "@fastify/static";
|
||||||
|
import cookie from "@fastify/cookie";
|
||||||
|
import session from "@fastify/session";
|
||||||
import { join, dirname } from "path";
|
import { join, dirname } from "path";
|
||||||
import { fileURLToPath } from "url";
|
import { fileURLToPath } from "url";
|
||||||
import { existsSync } from "fs";
|
import { existsSync } from "fs";
|
||||||
|
|
@ -7,22 +10,53 @@ import { existsSync } from "fs";
|
||||||
import projectRoutes from "./routes/projects.js";
|
import projectRoutes from "./routes/projects.js";
|
||||||
import buildRoutes from "./routes/builds.js";
|
import buildRoutes from "./routes/builds.js";
|
||||||
import webhookRoutes from "./routes/webhook.js";
|
import webhookRoutes from "./routes/webhook.js";
|
||||||
|
import authRoutes from "./routes/auth.js";
|
||||||
|
|
||||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
|
|
||||||
const fastify = Fastify({ logger: true });
|
const fastify = Fastify({ logger: true });
|
||||||
|
|
||||||
// Store raw body for webhook signature verification
|
// Raw body for webhook signature verification
|
||||||
fastify.addContentTypeParser("application/json", { parseAs: "buffer" }, (req, body, done) => {
|
fastify.addContentTypeParser(
|
||||||
|
"application/json",
|
||||||
|
{ parseAs: "buffer" },
|
||||||
|
(req, body, done) => {
|
||||||
req.rawBody = body;
|
req.rawBody = body;
|
||||||
try {
|
try {
|
||||||
done(null, JSON.parse(body.toString()));
|
done(null, JSON.parse(body.toString()));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
done(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,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// API routes
|
// 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" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Routes
|
||||||
|
fastify.register(authRoutes);
|
||||||
fastify.register(projectRoutes);
|
fastify.register(projectRoutes);
|
||||||
fastify.register(buildRoutes);
|
fastify.register(buildRoutes);
|
||||||
fastify.register(webhookRoutes);
|
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 };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -60,9 +60,18 @@ importers:
|
||||||
|
|
||||||
packages/server:
|
packages/server:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
'@fastify/cookie':
|
||||||
|
specifier: ^11.0.0
|
||||||
|
version: 11.0.2
|
||||||
|
'@fastify/session':
|
||||||
|
specifier: ^11.0.0
|
||||||
|
version: 11.1.1
|
||||||
'@fastify/static':
|
'@fastify/static':
|
||||||
specifier: ^8.0.0
|
specifier: ^8.0.0
|
||||||
version: 8.3.0
|
version: 8.3.0
|
||||||
|
bcryptjs:
|
||||||
|
specifier: ^3.0.2
|
||||||
|
version: 3.0.3
|
||||||
better-sqlite3:
|
better-sqlite3:
|
||||||
specifier: ^11.0.0
|
specifier: ^11.0.0
|
||||||
version: 11.10.0
|
version: 11.10.0
|
||||||
|
|
@ -321,6 +330,9 @@ packages:
|
||||||
'@fastify/ajv-compiler@4.0.5':
|
'@fastify/ajv-compiler@4.0.5':
|
||||||
resolution: {integrity: sha512-KoWKW+MhvfTRWL4qrhUwAAZoaChluo0m0vbiJlGMt2GXvL4LVPQEjt8kSpHI3IBq5Rez8fg+XeH3cneztq+C7A==}
|
resolution: {integrity: sha512-KoWKW+MhvfTRWL4qrhUwAAZoaChluo0m0vbiJlGMt2GXvL4LVPQEjt8kSpHI3IBq5Rez8fg+XeH3cneztq+C7A==}
|
||||||
|
|
||||||
|
'@fastify/cookie@11.0.2':
|
||||||
|
resolution: {integrity: sha512-GWdwdGlgJxyvNv+QcKiGNevSspMQXncjMZ1J8IvuDQk0jvkzgWWZFNC2En3s+nHndZBGV8IbLwOI/sxCZw/mzA==}
|
||||||
|
|
||||||
'@fastify/error@4.2.0':
|
'@fastify/error@4.2.0':
|
||||||
resolution: {integrity: sha512-RSo3sVDXfHskiBZKBPRgnQTtIqpi/7zhJOEmAxCiBcM7d0uwdGdxLlsCaLzGs8v8NnxIRlfG0N51p5yFaOentQ==}
|
resolution: {integrity: sha512-RSo3sVDXfHskiBZKBPRgnQTtIqpi/7zhJOEmAxCiBcM7d0uwdGdxLlsCaLzGs8v8NnxIRlfG0N51p5yFaOentQ==}
|
||||||
|
|
||||||
|
|
@ -339,6 +351,9 @@ packages:
|
||||||
'@fastify/send@4.1.0':
|
'@fastify/send@4.1.0':
|
||||||
resolution: {integrity: sha512-TMYeQLCBSy2TOFmV95hQWkiTYgC/SEx7vMdV+wnZVX4tt8VBLKzmH8vV9OzJehV0+XBfg+WxPMt5wp+JBUKsVw==}
|
resolution: {integrity: sha512-TMYeQLCBSy2TOFmV95hQWkiTYgC/SEx7vMdV+wnZVX4tt8VBLKzmH8vV9OzJehV0+XBfg+WxPMt5wp+JBUKsVw==}
|
||||||
|
|
||||||
|
'@fastify/session@11.1.1':
|
||||||
|
resolution: {integrity: sha512-nuKwTHxh3eJsI4NJeXoYVGzXUsg+kH1WfHgS7IofVyVhmjc+A6qGr+29WQy8hYZiNtmCjfG415COpf5xTBkW4Q==}
|
||||||
|
|
||||||
'@fastify/static@8.3.0':
|
'@fastify/static@8.3.0':
|
||||||
resolution: {integrity: sha512-yKxviR5PH1OKNnisIzZKmgZSus0r2OZb8qCSbqmw34aolT4g3UlzYfeBRym+HJ1J471CR8e2ldNub4PubD1coA==}
|
resolution: {integrity: sha512-yKxviR5PH1OKNnisIzZKmgZSus0r2OZb8qCSbqmw34aolT4g3UlzYfeBRym+HJ1J471CR8e2ldNub4PubD1coA==}
|
||||||
|
|
||||||
|
|
@ -596,6 +611,10 @@ packages:
|
||||||
engines: {node: '>=6.0.0'}
|
engines: {node: '>=6.0.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
bcryptjs@3.0.3:
|
||||||
|
resolution: {integrity: sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
better-sqlite3@11.10.0:
|
better-sqlite3@11.10.0:
|
||||||
resolution: {integrity: sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ==}
|
resolution: {integrity: sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ==}
|
||||||
|
|
||||||
|
|
@ -1649,6 +1668,11 @@ snapshots:
|
||||||
ajv-formats: 3.0.1(ajv@8.18.0)
|
ajv-formats: 3.0.1(ajv@8.18.0)
|
||||||
fast-uri: 3.1.0
|
fast-uri: 3.1.0
|
||||||
|
|
||||||
|
'@fastify/cookie@11.0.2':
|
||||||
|
dependencies:
|
||||||
|
cookie: 1.1.1
|
||||||
|
fastify-plugin: 5.1.0
|
||||||
|
|
||||||
'@fastify/error@4.2.0': {}
|
'@fastify/error@4.2.0': {}
|
||||||
|
|
||||||
'@fastify/fast-json-stringify-compiler@5.0.3':
|
'@fastify/fast-json-stringify-compiler@5.0.3':
|
||||||
|
|
@ -1674,6 +1698,11 @@ snapshots:
|
||||||
http-errors: 2.0.1
|
http-errors: 2.0.1
|
||||||
mime: 3.0.0
|
mime: 3.0.0
|
||||||
|
|
||||||
|
'@fastify/session@11.1.1':
|
||||||
|
dependencies:
|
||||||
|
fastify-plugin: 5.1.0
|
||||||
|
safe-stable-stringify: 2.5.0
|
||||||
|
|
||||||
'@fastify/static@8.3.0':
|
'@fastify/static@8.3.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@fastify/accept-negotiator': 2.0.1
|
'@fastify/accept-negotiator': 2.0.1
|
||||||
|
|
@ -1890,6 +1919,8 @@ snapshots:
|
||||||
|
|
||||||
baseline-browser-mapping@2.10.0: {}
|
baseline-browser-mapping@2.10.0: {}
|
||||||
|
|
||||||
|
bcryptjs@3.0.3: {}
|
||||||
|
|
||||||
better-sqlite3@11.10.0:
|
better-sqlite3@11.10.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
bindings: 1.5.0
|
bindings: 1.5.0
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue