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 +1 -1
- package/src/auth.ts +11 -1
- package/src/config.ts +2 -0
- package/src/index.ts +12 -3
package/package.json
CHANGED
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") {
|