ework-web 0.10.28 → 0.10.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ework-web",
3
- "version": "0.10.28",
3
+ "version": "0.10.30",
4
4
  "type": "module",
5
5
  "description": "ework-web — standalone multi-project issue tracker. Local SQLite-backed, no external API dependency. Bun + TypeScript + SSR HTML.",
6
6
  "license": "MIT",
package/src/auth.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import type { Config } from "./config";
2
2
  import { getUserByLogin, ensureUser, verifyPat, type UserRow } from "./store";
3
3
  import { runAuthHook } from "./auth-hook";
4
+ import { readFileSync, existsSync } from "fs";
4
5
 
5
6
  // Per-user token-cookie auth. Cookie value is HMAC-signed and carries login +
6
7
  // issued-at, so the server is stateless (no session table). Two cookie formats
@@ -191,7 +192,16 @@ function esc(s: string): string {
191
192
  .replace(/"/g, """);
192
193
  }
193
194
 
194
- export function loginHTML(next: string, error?: string): string {
195
+ export function loginHTML(next: string, error?: string, cfg?: Config): string {
196
+ if (cfg?.loginPage && existsSync(cfg.loginPage)) {
197
+ try {
198
+ const tpl = readFileSync(cfg.loginPage, "utf-8");
199
+ return tpl
200
+ .replace(/\{\{next\}\}/g, esc(sanitizeNext(next)))
201
+ .replace(/\{\{error\}\}/g, error ? esc(error) : "");
202
+ } catch {
203
+ }
204
+ }
195
205
  const err = error ? `<div class="err">${esc(error)}</div>` : "";
196
206
  return `<!doctype html><html lang="zh"><head><meta charset="utf-8">
197
207
  <meta name="viewport" content="width=device-width,initial-scale=1">
package/src/config.ts CHANGED
@@ -81,6 +81,7 @@ export const configSchema = z.object({
81
81
  routerAdminToken: z.string().default(""),
82
82
  internalAuthHook: z.string().default(""),
83
83
  userAuthHook: z.string().default(""),
84
+ loginPage: z.string().default(""),
84
85
  // Default "provider/model" string passed to `opencode run --model <X>`.
85
86
  // Empty = let opencode pick per its own opencode.json + env. ework-daemon
86
87
  // pushes this (or the per-project override) on every spawn to defend
@@ -183,6 +184,7 @@ export async function loadConfig(): Promise<Config> {
183
184
  routerAdminToken: process.env.WORK_ROUTER_ADMIN_TOKEN ?? "",
184
185
  internalAuthHook: process.env.WORK_INTERNAL_AUTH_HOOK ?? "",
185
186
  userAuthHook: process.env.WORK_USER_AUTH_HOOK ?? "",
187
+ loginPage: process.env.WORK_LOGIN_PAGE ?? "",
186
188
  defaultModel: db.defaultModel ?? process.env.WORK_DEFAULT_MODEL,
187
189
  autowireActive: process.env.WORK_AUTOWIRE_ACTIVE !== "false",
188
190
  webhookMaxConcurrent: Number(process.env.WORK_WEBHOOK_MAX_CONCURRENT ?? "6"),
package/src/index.ts CHANGED
@@ -10,6 +10,7 @@ import { getActiveDaemons, listAllDaemons, getSessionDaemonMap, resolveDaemonEnd
10
10
  import { testMysqlConnection, migrateSqliteToMysql, writeMysqlEnv, migrateMysqlToSqlite, writeSqliteEnv, migrateDaemonSqliteToMysql, generateMysqlDDL } from "./db-admin";
11
11
  import type { MysqlTargetOpts } from "./db-admin";
12
12
  import { checkAuth, makeAuthCookieHeader, clearAuthCookieHeader, loginHTML, sanitizeNext, ensureBootstrapAdmin, ensureBootstrapSystem, isReservedSystemLogin } from "./auth";
13
+ import { runAuthHook } from "./auth-hook";
13
14
  import { OpencodeError, createOpencodeClient, MultiDaemonOpencodeClient, RemoteOpencodeClient, isLocalhost, type OpencodeClientInterface } from "./opencode";
14
15
  import { renderMarkdown } from "./render/markdown";
15
16
  import { log, uptimeSeconds, version } from "./logger";
@@ -480,7 +481,7 @@ async function handle(req: Request, url: URL, ip: string, ctx: { authed: boolean
480
481
  const form = await req.formData().catch(() => new FormData());
481
482
  const next = sanitizeNext(String(form.get("next") ?? "/"));
482
483
  if (!rateLimit(`login:${ip}`, 5, 5 / (15 * 60))) {
483
- return html(loginHTML(next, "尝试过多,15 分钟后再试"), 429);
484
+ return html(loginHTML(next, "尝试过多,15 分钟后再试", cfg), 429);
484
485
  }
485
486
  const login = String(form.get("login") ?? "").trim();
486
487
  const password = String(form.get("password") ?? "");
@@ -495,6 +496,14 @@ async function handle(req: Request, url: URL, ip: string, ctx: { authed: boolean
495
496
  if (u) resolvedLogin = u.login;
496
497
  }
497
498
 
499
+ if (!resolvedLogin && token && cfg.userAuthHook) {
500
+ const hookReq = new Request(url, {
501
+ headers: { authorization: `Bearer ${token}` },
502
+ });
503
+ const user = await runAuthHook(cfg.userAuthHook, hookReq);
504
+ if (user) resolvedLogin = user.login;
505
+ }
506
+
498
507
  if (resolvedLogin) {
499
508
  const setCookie = await makeAuthCookieHeader(cfg, resolvedLogin);
500
509
  return new Response(null, {
@@ -507,10 +516,10 @@ async function handle(req: Request, url: URL, ip: string, ctx: { authed: boolean
507
516
  : login || password
508
517
  ? "用户名或密码错误"
509
518
  : "请填写用户名密码或共享 token";
510
- return html(loginHTML(next, err), 401);
519
+ return html(loginHTML(next, err, cfg), 401);
511
520
  }
512
521
  const next = sanitizeNext(url.searchParams.get("next") ?? "/");
513
- return html(loginHTML(next));
522
+ return html(loginHTML(next, undefined, cfg));
514
523
  }
515
524
 
516
525
  if (url.pathname === "/logout" && req.method === "POST") {