fix(frontend): say the database is missing, not Supabase
Self-hosted PostgreSQL is the runtime. A missing APP_DATABASE_URL must not tell users the retired hosted service is unconfigured. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -142,7 +142,7 @@
|
||||
|
||||
基线 **2196** → 本轮 **2205 pass / 0 fail / 0 skipped**(+9:chart-library-session 5、home-streaming-render-split 1、site-style-isolation 2、account-dialog-overlay 1)。
|
||||
|
||||
浏览器(`next start` PORT 3010,无后端):`/login` 可见压缩 logo 与 Inter 变量 class、Link 头 `woff2` preload;`/membership` 站点样式完整;`/admin/users` 仍 403。未登录 `/` 因本机 Supabase 未配置走恢复页,不是本轮回归。后台已登录四页目视仍见 `BLOCKED.md`。
|
||||
浏览器(`next start` PORT 3010,未注入 `AUTH_PROVIDER=self-hosted` 与本地 PostgreSQL URL):`/login` 可见压缩 logo 与 Inter 变量 class、Link 头 `woff2` preload;`/membership` 站点样式完整;`/admin/users` 仍 403。未登录 `/` 因本机没有业务库连接而走恢复页,不是本轮回归。运行时是 Better Auth + 自维护 PostgreSQL,不是 Supabase。后台已登录四页目视仍见 `BLOCKED.md`。
|
||||
|
||||
## 验收命令(本机)
|
||||
|
||||
|
||||
@@ -6589,3 +6589,19 @@
|
||||
- 复发自:无
|
||||
- 修复版本:待发布
|
||||
|
||||
## BUG-431 | 自托管 PostgreSQL 缺库时仍对用户写“Supabase 尚未配置”
|
||||
|
||||
- 状态:resolved
|
||||
- 首次发现:2026-08-29
|
||||
- 最近更新:2026-08-29
|
||||
- 影响面:`GET /api/account`、会员余额、登录失败提示、会话/报告/星盘库等 503
|
||||
- 用户现象:运行时已是 Better Auth + 本地 PostgreSQL,缺业务库 URL 或未设 `AUTH_PROVIDER=self-hosted` 时,会员页“当前余额”和首页仍显示“Supabase 尚未配置”。
|
||||
- 触发条件:未注入 `APP_DATABASE_URL` / `SERVICE_DATABASE_URL`,或 `AUTH_PROVIDER` 未设为 `self-hosted` 因而落到旧适配器路径。
|
||||
- 根因:BUG-010 已禁止浏览器直连 Supabase,但 API 与 `friendlyError` 仍把配置失败翻译成 Supabase 文案和 `SUPABASE_NOT_CONFIGURED`。产品已不用 Supabase。
|
||||
- 修复:对外统一为“数据库尚未配置” / `DATABASE_NOT_CONFIGURED`,以及“请先配置数据库环境变量。”。内部类名仍叫 `SupabaseConfigurationError`,避免本轮改适配器标识。
|
||||
- 验证:`frontend/tests/self-hosted-error-copy.test.ts` 扫描 `frontend/src` 禁止上述旧文案,并锁定错误码与内部消息。
|
||||
- 防复发:用户可见中文不得再出现“Supabase 尚未配置”;缺库 503 必须说数据库,不得说已退役的托管服务。
|
||||
- 相关记录:BUG-010
|
||||
- 复发自:BUG-010
|
||||
- 修复版本:待发布
|
||||
|
||||
|
||||
@@ -161,7 +161,7 @@ export async function GET() {
|
||||
});
|
||||
} catch (error) {
|
||||
if (isSupabaseConfigurationError(error)) {
|
||||
return NextResponse.json({ error: "Supabase 尚未配置", code: "SUPABASE_NOT_CONFIGURED" }, { status: 503 });
|
||||
return NextResponse.json({ error: "数据库尚未配置", code: "DATABASE_NOT_CONFIGURED" }, { status: 503 });
|
||||
}
|
||||
return NextResponse.json({ error: "账户服务暂时不可用" }, { status: 500 });
|
||||
}
|
||||
@@ -346,7 +346,7 @@ export async function PATCH(request: Request) {
|
||||
});
|
||||
} catch (error) {
|
||||
if (isSupabaseConfigurationError(error)) {
|
||||
return NextResponse.json({ error: "Supabase 尚未配置", code: "SUPABASE_NOT_CONFIGURED" }, { status: 503 });
|
||||
return NextResponse.json({ error: "数据库尚未配置", code: "DATABASE_NOT_CONFIGURED" }, { status: 503 });
|
||||
}
|
||||
return NextResponse.json({ error: "账户服务暂时不可用" }, { status: 500 });
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ export async function POST(request: Request) {
|
||||
} catch (error) {
|
||||
if (isSupabaseConfigurationError(error)) {
|
||||
return NextResponse.json(
|
||||
{ error: "服务尚未配置", message: "请先配置 Supabase 环境变量。" },
|
||||
{ error: "服务尚未配置", message: "请先配置数据库环境变量。" },
|
||||
{ status: 503 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ export async function POST(request: Request) {
|
||||
} catch (error) {
|
||||
if (isSupabaseConfigurationError(error)) {
|
||||
return NextResponse.json(
|
||||
{ error: "服务尚未配置", message: "请先配置 Supabase 环境变量。" },
|
||||
{ error: "服务尚未配置", message: "请先配置数据库环境变量。" },
|
||||
{ status: 503 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ export async function DELETE(_request: Request, context: RouteContext) {
|
||||
return NextResponse.json({ ok: true });
|
||||
} catch (error) {
|
||||
if (isSupabaseConfigurationError(error)) {
|
||||
return NextResponse.json({ error: "Supabase 尚未配置", code: "SUPABASE_NOT_CONFIGURED" }, { status: 503 });
|
||||
return NextResponse.json({ error: "数据库尚未配置", code: "DATABASE_NOT_CONFIGURED" }, { status: 503 });
|
||||
}
|
||||
return NextResponse.json({ error: errorMessage(error, "星盘删除失败") }, { status: 500 });
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ export async function GET() {
|
||||
return NextResponse.json({ profiles: data ?? [] });
|
||||
} catch (error) {
|
||||
if (isSupabaseConfigurationError(error)) {
|
||||
return NextResponse.json({ error: "Supabase 尚未配置", code: "SUPABASE_NOT_CONFIGURED" }, { status: 503 });
|
||||
return NextResponse.json({ error: "数据库尚未配置", code: "DATABASE_NOT_CONFIGURED" }, { status: 503 });
|
||||
}
|
||||
return NextResponse.json({ error: errorMessage(error, "星盘库暂时不可用") }, { status: 500 });
|
||||
}
|
||||
@@ -81,7 +81,7 @@ export async function POST(request: Request) {
|
||||
return NextResponse.json({ profile: data });
|
||||
} catch (error) {
|
||||
if (isSupabaseConfigurationError(error)) {
|
||||
return NextResponse.json({ error: "Supabase 尚未配置", code: "SUPABASE_NOT_CONFIGURED" }, { status: 503 });
|
||||
return NextResponse.json({ error: "数据库尚未配置", code: "DATABASE_NOT_CONFIGURED" }, { status: 503 });
|
||||
}
|
||||
return NextResponse.json({ error: errorMessage(error, "星盘保存失败") }, { status: 500 });
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ export async function POST(request: Request) {
|
||||
accounting = createAdminSupabaseClient();
|
||||
} catch {
|
||||
return NextResponse.json(
|
||||
{ error: "服务尚未配置", message: "请先配置 Supabase 环境变量。" },
|
||||
{ error: "服务尚未配置", message: "请先配置数据库环境变量。" },
|
||||
{ status: 503 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -234,7 +234,7 @@ export async function POST(request: Request) {
|
||||
accounting = createAdminSupabaseClient();
|
||||
} catch {
|
||||
return NextResponse.json(
|
||||
{ error: "服务尚未配置", message: "请先配置 Supabase 环境变量。" },
|
||||
{ error: "服务尚未配置", message: "请先配置数据库环境变量。" },
|
||||
{ status: 503 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ export async function GET() {
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
return NextResponse.json(
|
||||
{ error: "服务尚未配置", message: "请先配置 Supabase 环境变量。" },
|
||||
{ error: "服务尚未配置", message: "请先配置数据库环境变量。" },
|
||||
{ status: 503 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ export async function GET() {
|
||||
} catch (error) {
|
||||
if (isSupabaseConfigurationError(error)) {
|
||||
return NextResponse.json(
|
||||
{ error: "Supabase 尚未配置", code: "SUPABASE_NOT_CONFIGURED" },
|
||||
{ error: "数据库尚未配置", code: "DATABASE_NOT_CONFIGURED" },
|
||||
{ status: 503 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -75,7 +75,7 @@ export async function POST(request: Request) {
|
||||
} catch (error) {
|
||||
if (isSupabaseConfigurationError(error)) {
|
||||
return NextResponse.json(
|
||||
{ error: "Supabase 尚未配置", code: "SUPABASE_NOT_CONFIGURED" },
|
||||
{ error: "数据库尚未配置", code: "DATABASE_NOT_CONFIGURED" },
|
||||
{ status: 503 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@ export async function GET(request: Request, context: RouteContext) {
|
||||
} catch (error) {
|
||||
if (isSupabaseConfigurationError(error)) {
|
||||
return NextResponse.json(
|
||||
{ error: "Supabase 尚未配置", code: "SUPABASE_NOT_CONFIGURED" },
|
||||
{ error: "数据库尚未配置", code: "DATABASE_NOT_CONFIGURED" },
|
||||
{ status: 503 },
|
||||
);
|
||||
}
|
||||
@@ -122,7 +122,7 @@ export async function DELETE(request: Request, context: RouteContext) {
|
||||
} catch (error) {
|
||||
if (isSupabaseConfigurationError(error)) {
|
||||
return NextResponse.json(
|
||||
{ error: "Supabase 尚未配置", code: "SUPABASE_NOT_CONFIGURED" },
|
||||
{ error: "数据库尚未配置", code: "DATABASE_NOT_CONFIGURED" },
|
||||
{ status: 503 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -93,7 +93,7 @@ export async function GET() {
|
||||
} catch (error) {
|
||||
if (isSupabaseConfigurationError(error)) {
|
||||
return NextResponse.json(
|
||||
{ error: "Supabase 尚未配置", code: "SUPABASE_NOT_CONFIGURED" },
|
||||
{ error: "数据库尚未配置", code: "DATABASE_NOT_CONFIGURED" },
|
||||
{ status: 503 },
|
||||
);
|
||||
}
|
||||
@@ -211,7 +211,7 @@ export async function POST(request: Request) {
|
||||
} catch (error) {
|
||||
if (isSupabaseConfigurationError(error)) {
|
||||
return NextResponse.json(
|
||||
{ error: "Supabase 尚未配置", code: "SUPABASE_NOT_CONFIGURED" },
|
||||
{ error: "数据库尚未配置", code: "DATABASE_NOT_CONFIGURED" },
|
||||
{ status: 503 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ export async function PATCH(request: Request, context: RouteContext) {
|
||||
return NextResponse.json({ error: error.message }, { status: 413 });
|
||||
}
|
||||
if (isSupabaseConfigurationError(error)) {
|
||||
return NextResponse.json({ error: "Supabase 尚未配置", code: "SUPABASE_NOT_CONFIGURED" }, { status: 503 });
|
||||
return NextResponse.json({ error: "数据库尚未配置", code: "DATABASE_NOT_CONFIGURED" }, { status: 503 });
|
||||
}
|
||||
return NextResponse.json({ error: "聊天记录暂时无法同步" }, { status: 500 });
|
||||
}
|
||||
@@ -74,7 +74,7 @@ export async function DELETE(_request: Request, context: RouteContext) {
|
||||
return NextResponse.json({ ok: true });
|
||||
} catch (error) {
|
||||
if (isSupabaseConfigurationError(error)) {
|
||||
return NextResponse.json({ error: "Supabase 尚未配置", code: "SUPABASE_NOT_CONFIGURED" }, { status: 503 });
|
||||
return NextResponse.json({ error: "数据库尚未配置", code: "DATABASE_NOT_CONFIGURED" }, { status: 503 });
|
||||
}
|
||||
return NextResponse.json({ error: error instanceof Error ? error.message : "删除聊天记录失败" }, { status: 500 });
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ export async function POST(request: Request) {
|
||||
return NextResponse.json({ error: error.message }, { status: 413 });
|
||||
}
|
||||
if (isSupabaseConfigurationError(error)) {
|
||||
return NextResponse.json({ error: "Supabase 尚未配置", code: "SUPABASE_NOT_CONFIGURED" }, { status: 503 });
|
||||
return NextResponse.json({ error: "数据库尚未配置", code: "DATABASE_NOT_CONFIGURED" }, { status: 503 });
|
||||
}
|
||||
return NextResponse.json({ error: "聊天记录暂时无法同步" }, { status: 500 });
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ export async function GET() {
|
||||
return NextResponse.json({ reports: data ?? [] });
|
||||
} catch (error) {
|
||||
if (isSupabaseConfigurationError(error)) {
|
||||
return NextResponse.json({ error: "Supabase 尚未配置", code: "SUPABASE_NOT_CONFIGURED" }, { status: 503 });
|
||||
return NextResponse.json({ error: "数据库尚未配置", code: "DATABASE_NOT_CONFIGURED" }, { status: 503 });
|
||||
}
|
||||
return NextResponse.json({ error: errorMessage(error, "合盘历史暂时不可用") }, { status: 500 });
|
||||
}
|
||||
@@ -78,7 +78,7 @@ export async function POST(request: Request) {
|
||||
return NextResponse.json({ report: data });
|
||||
} catch (error) {
|
||||
if (isSupabaseConfigurationError(error)) {
|
||||
return NextResponse.json({ error: "Supabase 尚未配置", code: "SUPABASE_NOT_CONFIGURED" }, { status: 503 });
|
||||
return NextResponse.json({ error: "数据库尚未配置", code: "DATABASE_NOT_CONFIGURED" }, { status: 503 });
|
||||
}
|
||||
return NextResponse.json({ error: errorMessage(error, "合盘历史保存失败") }, { status: 500 });
|
||||
}
|
||||
|
||||
@@ -944,8 +944,11 @@ function isProfileComplete(profile: Profile) {
|
||||
}
|
||||
|
||||
function friendlyError(message: string) {
|
||||
return message.includes("Supabase") && (message.includes("配置") || message.includes("environment") || message.includes("URL"))
|
||||
? "Supabase 尚未配置"
|
||||
return (
|
||||
message.includes("数据库配置缺失")
|
||||
|| (message.includes("Supabase") && (message.includes("配置") || message.includes("environment") || message.includes("URL")))
|
||||
)
|
||||
? "数据库尚未配置"
|
||||
: message;
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ function authMessage(caught: unknown) {
|
||||
message.includes("environment") ||
|
||||
message.includes("URL")
|
||||
)
|
||||
return "Supabase 尚未配置";
|
||||
return "数据库尚未配置";
|
||||
if (lower.includes("expired") || lower.includes("invalid"))
|
||||
return "验证码错误或已过期,请重新获取";
|
||||
if (lower.includes("rate limit")) return "发送过于频繁,请稍后再试";
|
||||
|
||||
@@ -148,7 +148,7 @@ export function createOnboardingPost(dependencies: OnboardingPostDependencies):
|
||||
session = await dependencies.openSession();
|
||||
} catch { // no-excuse-ok: catch -- route boundary converts missing configuration.
|
||||
return Response.json(
|
||||
{ error: "服务尚未配置", message: "请先配置 Supabase 环境变量。" },
|
||||
{ error: "服务尚未配置", message: "请先配置数据库环境变量。" },
|
||||
{ status: 503 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
export class SupabaseConfigurationError extends Error {
|
||||
readonly code = "SUPABASE_NOT_CONFIGURED";
|
||||
readonly code = "DATABASE_NOT_CONFIGURED";
|
||||
|
||||
constructor(missing: string[]) {
|
||||
super(`Supabase 配置缺失:${missing.join(", ")}`);
|
||||
super(`数据库配置缺失:${missing.join(", ")}`);
|
||||
this.name = "SupabaseConfigurationError";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readdirSync, readFileSync, statSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import test from "node:test";
|
||||
import { SupabaseConfigurationError } from "../src/lib/supabase/config.ts";
|
||||
|
||||
const srcRoot = fileURLToPath(new URL("../src", import.meta.url));
|
||||
|
||||
function walk(directory: string): string[] {
|
||||
const entries = readdirSync(directory);
|
||||
const files: string[] = [];
|
||||
for (const name of entries) {
|
||||
const path = join(directory, name);
|
||||
if (statSync(path).isDirectory()) {
|
||||
files.push(...walk(path));
|
||||
continue;
|
||||
}
|
||||
if (path.endsWith(".ts") || path.endsWith(".tsx")) files.push(path);
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
test("user-visible copy never names Supabase after the PostgreSQL cutover", () => {
|
||||
const stale = [
|
||||
"Supabase 尚未配置",
|
||||
"请先配置 Supabase 环境变量。",
|
||||
"Supabase 配置缺失",
|
||||
"SUPABASE_NOT_CONFIGURED",
|
||||
];
|
||||
const hits: string[] = [];
|
||||
for (const file of walk(srcRoot)) {
|
||||
const source = readFileSync(file, "utf8");
|
||||
for (const needle of stale) {
|
||||
if (source.includes(needle)) hits.push(`${file.slice(srcRoot.length + 1)}: ${needle}`);
|
||||
}
|
||||
}
|
||||
assert.deepEqual(hits, []);
|
||||
const missing = new SupabaseConfigurationError(["APP_DATABASE_URL"]);
|
||||
assert.equal(missing.code, "DATABASE_NOT_CONFIGURED");
|
||||
assert.match(missing.message, /数据库配置缺失:APP_DATABASE_URL/);
|
||||
assert.doesNotMatch(missing.message, /Supabase/);
|
||||
});
|
||||
Reference in New Issue
Block a user