支持固定登录启动模式
This commit is contained in:
@@ -3,6 +3,12 @@ PORT=3001
|
|||||||
JWT_SECRET=change-me
|
JWT_SECRET=change-me
|
||||||
DB_PATH=
|
DB_PATH=
|
||||||
|
|
||||||
|
# login: show login page
|
||||||
|
# fixed: auto login with FIXED_LOGIN_USERNAME and open model library directly
|
||||||
|
APP_AUTH_MODE=login
|
||||||
|
FIXED_LOGIN_USERNAME=viewer
|
||||||
|
FIXED_LOGIN_ROLE=user
|
||||||
|
|
||||||
# local: write to server/storage and serve via /storage
|
# local: write to server/storage and serve via /storage
|
||||||
# cos: upload models to Tencent Cloud COS and read through CDN_BASE_URL
|
# cos: upload models to Tencent Cloud COS and read through CDN_BASE_URL
|
||||||
STORAGE_DRIVER=local
|
STORAGE_DRIVER=local
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import bcrypt from "bcryptjs";
|
import bcrypt from "bcryptjs";
|
||||||
import { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
|
import { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import { config } from "./config.js";
|
||||||
import { db } from "./db.js";
|
import { db } from "./db.js";
|
||||||
import { AuthUser, UserRow } from "./types.js";
|
import { AuthUser, UserRow } from "./types.js";
|
||||||
|
|
||||||
@@ -60,7 +61,57 @@ function normalizeExpiresAt(value: string | null | undefined) {
|
|||||||
return clean.length === 10 ? `${clean}T23:59:59` : clean;
|
return clean.length === 10 ? `${clean}T23:59:59` : clean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function ensureFixedLoginUser() {
|
||||||
|
const username = config.fixedLogin.username.trim() || "viewer";
|
||||||
|
const existing = db.prepare(`
|
||||||
|
SELECT id, username, password_hash, role, enabled, expires_at, created_at, updated_at
|
||||||
|
FROM users
|
||||||
|
WHERE username = ?
|
||||||
|
`).get(username) as unknown as UserRow | undefined;
|
||||||
|
if (existing) {
|
||||||
|
db.prepare(`
|
||||||
|
UPDATE users
|
||||||
|
SET role = ?, enabled = 1, expires_at = NULL, updated_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE id = ?
|
||||||
|
`).run(config.fixedLogin.role, existing.id);
|
||||||
|
return db.prepare(`
|
||||||
|
SELECT id, username, password_hash, role, enabled, expires_at, created_at, updated_at
|
||||||
|
FROM users
|
||||||
|
WHERE id = ?
|
||||||
|
`).get(existing.id) as unknown as UserRow;
|
||||||
|
}
|
||||||
|
|
||||||
|
const passwordHash = await bcrypt.hash(`fixed-${Date.now()}-${Math.random()}`, 10);
|
||||||
|
const result = db.prepare(`
|
||||||
|
INSERT INTO users (username, password_hash, role, enabled, expires_at)
|
||||||
|
VALUES (?, ?, ?, 1, NULL)
|
||||||
|
`).run(username, passwordHash, config.fixedLogin.role);
|
||||||
|
return db.prepare(`
|
||||||
|
SELECT id, username, password_hash, role, enabled, expires_at, created_at, updated_at
|
||||||
|
FROM users
|
||||||
|
WHERE id = ?
|
||||||
|
`).get(Number(result.lastInsertRowid)) as unknown as UserRow;
|
||||||
|
}
|
||||||
|
|
||||||
|
function authUser(row: Pick<UserRow, "id" | "username" | "role">): AuthUser {
|
||||||
|
return {
|
||||||
|
id: row.id,
|
||||||
|
username: row.username,
|
||||||
|
role: row.role
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export async function authRoutes(app: FastifyInstance) {
|
export async function authRoutes(app: FastifyInstance) {
|
||||||
|
app.get("/api/auth/bootstrap", async () => {
|
||||||
|
if (config.authMode !== "fixed") {
|
||||||
|
return { mode: "login" };
|
||||||
|
}
|
||||||
|
const user = await ensureFixedLoginUser();
|
||||||
|
const payload = authUser(user);
|
||||||
|
const token = app.jwt.sign(payload);
|
||||||
|
return { mode: "fixed", token, user: payload };
|
||||||
|
});
|
||||||
|
|
||||||
app.post("/api/auth/register", async (request, reply) => {
|
app.post("/api/auth/register", async (request, reply) => {
|
||||||
const body = registerSchema.parse(request.body);
|
const body = registerSchema.parse(request.body);
|
||||||
const passwordHash = await bcrypt.hash(body.password, 10);
|
const passwordHash = await bcrypt.hash(body.password, 10);
|
||||||
@@ -98,8 +149,9 @@ export async function authRoutes(app: FastifyInstance) {
|
|||||||
return reply.code(403).send({ message: "账号授权已过期" });
|
return reply.code(403).send({ message: "账号授权已过期" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const token = app.jwt.sign({ id: user.id, username: user.username, role: user.role });
|
const payload = authUser(user);
|
||||||
return { token, user: { id: user.id, username: user.username, role: user.role } };
|
const token = app.jwt.sign(payload);
|
||||||
|
return { token, user: payload };
|
||||||
});
|
});
|
||||||
|
|
||||||
app.get("/api/auth/me", { preHandler: [requireAuth] }, async (request) => {
|
app.get("/api/auth/me", { preHandler: [requireAuth] }, async (request) => {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
import dotenv from "dotenv";
|
import dotenv from "dotenv";
|
||||||
|
import type { UserRole } from "./types.js";
|
||||||
|
|
||||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
const serverRoot = path.resolve(__dirname, "..");
|
const serverRoot = path.resolve(__dirname, "..");
|
||||||
@@ -8,10 +9,19 @@ const serverRoot = path.resolve(__dirname, "..");
|
|||||||
dotenv.config({ path: path.resolve(serverRoot, "..", ".env") });
|
dotenv.config({ path: path.resolve(serverRoot, "..", ".env") });
|
||||||
dotenv.config({ path: path.resolve(serverRoot, ".env") });
|
dotenv.config({ path: path.resolve(serverRoot, ".env") });
|
||||||
|
|
||||||
|
function fixedLoginRole(): UserRole {
|
||||||
|
return process.env.FIXED_LOGIN_ROLE === "admin" ? "admin" : "user";
|
||||||
|
}
|
||||||
|
|
||||||
export const config = {
|
export const config = {
|
||||||
host: process.env.HOST ?? "0.0.0.0",
|
host: process.env.HOST ?? "0.0.0.0",
|
||||||
port: Number(process.env.PORT ?? 3001),
|
port: Number(process.env.PORT ?? 3001),
|
||||||
jwtSecret: process.env.JWT_SECRET ?? "dev-only-change-me",
|
jwtSecret: process.env.JWT_SECRET ?? "dev-only-change-me",
|
||||||
|
authMode: process.env.APP_AUTH_MODE === "fixed" ? "fixed" : "login",
|
||||||
|
fixedLogin: {
|
||||||
|
username: process.env.FIXED_LOGIN_USERNAME ?? "viewer",
|
||||||
|
role: fixedLoginRole()
|
||||||
|
},
|
||||||
dataDir: path.resolve(serverRoot, "data"),
|
dataDir: path.resolve(serverRoot, "data"),
|
||||||
storageDir: path.resolve(serverRoot, "storage"),
|
storageDir: path.resolve(serverRoot, "storage"),
|
||||||
databasePath: process.env.DB_PATH ?? path.resolve(serverRoot, "data", "model-library.db"),
|
databasePath: process.env.DB_PATH ?? path.resolve(serverRoot, "data", "model-library.db"),
|
||||||
|
|||||||
@@ -2,12 +2,26 @@ import "jstree/dist/themes/default/style.min.css";
|
|||||||
import "w2ui/w2ui-2.0.min.css";
|
import "w2ui/w2ui-2.0.min.css";
|
||||||
import "./styles/base.css";
|
import "./styles/base.css";
|
||||||
import { api } from "./services/api";
|
import { api } from "./services/api";
|
||||||
import { getToken, setCurrentUser, clearSession } from "./services/authState";
|
import { getToken, setCurrentUser, setSession, clearSession } from "./services/authState";
|
||||||
import type { AuthUser } from "./types";
|
import type { AuthBootstrapResponse, AuthUser } from "./types";
|
||||||
import { renderLogin } from "./pages/login/login";
|
import { renderLogin } from "./pages/login/login";
|
||||||
import { renderApp } from "./pages/app/layout";
|
import { renderApp } from "./pages/app/layout";
|
||||||
|
|
||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
|
try {
|
||||||
|
const boot = await api<AuthBootstrapResponse>("/api/auth/bootstrap");
|
||||||
|
if (boot.mode === "fixed") {
|
||||||
|
document.body.dataset.authMode = "fixed";
|
||||||
|
setSession(boot.token, boot.user);
|
||||||
|
await renderApp();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
delete document.body.dataset.authMode;
|
||||||
|
} catch {
|
||||||
|
delete document.body.dataset.authMode;
|
||||||
|
clearSession();
|
||||||
|
}
|
||||||
|
|
||||||
if (!getToken()) {
|
if (!getToken()) {
|
||||||
renderLogin(renderApp);
|
renderLogin(renderApp);
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import "./dialogs.css";
|
|||||||
export async function renderApp() {
|
export async function renderApp() {
|
||||||
const currentUser = getCurrentUser();
|
const currentUser = getCurrentUser();
|
||||||
const isAdmin = currentUser?.role === "admin";
|
const isAdmin = currentUser?.role === "admin";
|
||||||
|
const isFixedLogin = document.body.dataset.authMode === "fixed";
|
||||||
document.querySelector<HTMLDivElement>("#app")!.innerHTML = `
|
document.querySelector<HTMLDivElement>("#app")!.innerHTML = `
|
||||||
<div class="app-shell">
|
<div class="app-shell">
|
||||||
<header class="topbar">
|
<header class="topbar">
|
||||||
@@ -18,7 +19,7 @@ export async function renderApp() {
|
|||||||
</div>
|
</div>
|
||||||
<div class="topbar-actions">
|
<div class="topbar-actions">
|
||||||
<span>${currentUser?.username ?? ""}</span>
|
<span>${currentUser?.username ?? ""}</span>
|
||||||
<button id="logoutBtn" class="icon-text-btn">退出</button>
|
${isFixedLogin ? "" : `<button id="logoutBtn" class="icon-text-btn">退出</button>`}
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<section id="workspaceLayout" class="workspace"></section>
|
<section id="workspaceLayout" class="workspace"></section>
|
||||||
@@ -67,7 +68,7 @@ export async function renderApp() {
|
|||||||
|
|
||||||
renderWorkspaceLayout();
|
renderWorkspaceLayout();
|
||||||
|
|
||||||
document.querySelector("#logoutBtn")!.addEventListener("click", () => {
|
document.querySelector("#logoutBtn")?.addEventListener("click", () => {
|
||||||
clearSession();
|
clearSession();
|
||||||
renderLogin(renderApp);
|
renderLogin(renderApp);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,6 +6,10 @@ export type AuthUser = {
|
|||||||
|
|
||||||
export type UserRole = "admin" | "user";
|
export type UserRole = "admin" | "user";
|
||||||
|
|
||||||
|
export type AuthBootstrapResponse =
|
||||||
|
| { mode: "login" }
|
||||||
|
| { mode: "fixed"; token: string; user: AuthUser };
|
||||||
|
|
||||||
export type ManagedUser = {
|
export type ManagedUser = {
|
||||||
id: number;
|
id: number;
|
||||||
username: string;
|
username: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user