Skip to content

Commit b581a2d

Browse files
committed
fix: resolve i18n misconfig, API issues, and add missing route boundaries
- Fix i18n: resolve localePrefix contradiction, set middleware locale cookie for anonymous users, dynamic html lang, add missing setRequestLocale - Fix API: stop process.env per-request mutation, proper error responses, remove dynamic await import() from request handlers - Fix auth: align signup password validation to min(8), fix ActiveOrganizationProvider loading state when query is disabled - Fix config: expand .env.example, clean up dead deps, align tailwind versions, fix Biome jsxRuntime conflict with react-jsx transform - Add missing loading.tsx and error.tsx to (account), (app), (docs) groups - Remove duplicate @account/* tsconfig key, deduplicate animation keyframes
1 parent 6f8781b commit b581a2d

30 files changed

Lines changed: 501 additions & 631 deletions

‎apps/web/.env.example‎

Lines changed: 120 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,129 @@
11
# Build-time environment variables for CNB pipeline
22
# These are public values that can be safely committed to git
33

4+
# ============================================
5+
# Site / Application URLs
6+
# ============================================
7+
48
# Site URL - used for static generation, sitemap, SEO, etc.
59
NEXT_PUBLIC_SITE_URL=
610

7-
# Public S3 bucket configuration ( Tencent Cloud COS )
11+
# Application URL - used for email links, redirects, etc. Falls back to NEXT_PUBLIC_SITE_URL.
12+
NEXT_PUBLIC_APP_URL=
13+
14+
# Public API URL (optional, defaults to NEXT_PUBLIC_SITE_URL if not set)
15+
NEXT_PUBLIC_API_URL=
16+
17+
# ============================================
18+
# Database
19+
# ============================================
20+
21+
# PostgreSQL connection string for Prisma
22+
DATABASE_URL=
23+
24+
# ============================================
25+
# Authentication (Better Auth)
26+
# ============================================
27+
28+
# Auth secret - generate with: openssl rand -hex 32
29+
BETTER_AUTH_SECRET=
30+
31+
# Auth URL - must match your app's base URL (e.g., http://localhost:3000 for dev)
32+
BETTER_AUTH_URL=
33+
34+
# Trusted Origins (comma-separated list of domains allowed for auth)
35+
TRUSTED_ORIGINS=
36+
37+
# GitHub OAuth
38+
GITHUB_CLIENT_ID=
39+
GITHUB_CLIENT_SECRET=
40+
41+
# Google OAuth
42+
GOOGLE_CLIENT_ID=
43+
GOOGLE_CLIENT_SECRET=
44+
45+
# WeChat Website Login
46+
WECHAT_WEBSITE_APP_ID=
47+
WECHAT_WEBSITE_APP_SECRET=
48+
49+
# WeChat Service Account
50+
WECHAT_SERVICE_ACCOUNT_APP_ID=
51+
WECHAT_SERVICE_ACCOUNT_APP_SECRET=
52+
53+
# ============================================
54+
# Storage (Tencent Cloud COS / S3-compatible)
55+
# ============================================
56+
57+
S3_ACCESS_KEY_ID=
58+
S3_SECRET_ACCESS_KEY=
59+
S3_ENDPOINT=
60+
S3_REGION=
61+
62+
# Public S3 bucket configuration (Tencent Cloud COS)
863
# Bucket has public read, private write permissions
964
NEXT_PUBLIC_BUCKET_NAME=
1065
NEXT_PUBLIC_S3_ENDPOINT=
66+
67+
# ============================================
68+
# Email
69+
# ============================================
70+
71+
PLUNK_API_KEY=
72+
73+
# Enable real email sending in development
74+
ENABLE_EMAIL_IN_DEV=true
75+
76+
# ============================================
77+
# Tencent Cloud (SMS, Content Moderation, etc.)
78+
# ============================================
79+
80+
TENCENT_CLOUD_SECRET_ID=
81+
TENCENT_CLOUD_SECRET_KEY=
82+
TENCENT_CLOUD_REGION=ap-shanghai
83+
84+
# SMS configuration
85+
TENCENT_SMS_REGION=ap-guangzhou
86+
TENCENT_SMS_SDK_APP_ID=
87+
TENCENT_SMS_SIGN_NAME=
88+
TENCENT_SMS_TEMPLATE_ID=
89+
TENCENT_SMS_EVENT_APPROVED_TEMPLATE_ID=
90+
TENCENT_SMS_EVENT_REJECTED_TEMPLATE_ID=
91+
92+
# Content Moderation biztype
93+
TENCENT_CMS_BIZTYPE=content_input
94+
95+
# ============================================
96+
# AI / OpenAI-compatible API
97+
# ============================================
98+
99+
OPENAI_API_KEY=
100+
OPENAI_BASE_URL=
101+
OPENAI_MODEL=
102+
103+
# ============================================
104+
# Analytics
105+
# ============================================
106+
107+
# Priority: Umami > Google Analytics > Baidu Analytics
108+
# Umami (privacy-friendly, open-source)
109+
NEXT_PUBLIC_UMAMI_WEBSITE_ID=
110+
NEXT_PUBLIC_UMAMI_SCRIPT_URL=https://cloud.umami.is/script.js
111+
112+
# Google Analytics (optional)
113+
NEXT_PUBLIC_GOOGLE_ANALYTICS_ID=
114+
115+
# Baidu Analytics (optional)
116+
NEXT_PUBLIC_BAIDU_ANALYTICS_ID=
117+
118+
# ============================================
119+
# Telemetry & Debug
120+
# ============================================
121+
122+
# Set to 1 to disable Next.js telemetry
123+
NEXT_TELEMETRY_DISABLED=1
124+
125+
# Suppress punycode deprecation warning from Prisma
126+
NODE_OPTIONS=--no-deprecation
127+
128+
# Disable verbose HTTP request logging in development
129+
DISABLE_HTTP_LOGGING=false

‎apps/web/knip.json‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@
2525
"ignoreDependencies": [
2626
"@types/*",
2727
"prettier",
28-
"autoprefixer",
2928
"start-server-and-test",
3029
"markdown-toc",
3130
"prisma-json-types-generator",

‎apps/web/middleware.ts‎

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,9 +92,23 @@ export default async function middleware(req: NextRequest) {
9292
const response = NextResponse.next();
9393

9494
if (localePrefixPattern.test(pathname)) {
95+
const localeMatch = pathname.match(localePrefixPattern);
96+
const matchedLocale = localeMatch?.[1];
9597
const redirectUrl = req.nextUrl.clone();
9698
redirectUrl.pathname = stripLocalePrefix(pathname);
97-
return NextResponse.redirect(redirectUrl, 301);
99+
const redirectResponse = NextResponse.redirect(redirectUrl, 301);
100+
if (matchedLocale) {
101+
const existingLocale = req.cookies.get(
102+
appConfig.i18n.localeCookieName,
103+
)?.value;
104+
if (existingLocale !== matchedLocale) {
105+
redirectResponse.cookies.set(
106+
appConfig.i18n.localeCookieName,
107+
matchedLocale,
108+
);
109+
}
110+
}
111+
return redirectResponse;
98112
}
99113

100114
const legacyAppRedirect = mapLegacyAppPath(pathname);
@@ -169,12 +183,17 @@ export default async function middleware(req: NextRequest) {
169183
}
170184

171185
if (session) {
172-
let locale = req.cookies.get(appConfig.i18n.localeCookieName)?.value;
186+
const existingLocale = req.cookies.get(
187+
appConfig.i18n.localeCookieName,
188+
)?.value;
189+
let locale = existingLocale;
173190
if (
174191
!locale ||
175192
(session.user.locale && locale !== session.user.locale)
176193
) {
177194
locale = session.user.locale ?? appConfig.i18n.defaultLocale;
195+
}
196+
if (locale !== existingLocale) {
178197
response.cookies.set(appConfig.i18n.localeCookieName, locale);
179198
}
180199
}

‎apps/web/package.json‎

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,6 @@
132132
"react-dom": "^19.2.0",
133133
"react-dropzone": "^14.3.8",
134134
"react-hook-form": "^7.60.0",
135-
"react-hot-toast": "^2.6.0",
136135
"react-markdown": "^10.1.0",
137136
"react-qr-code": "^2.0.18",
138137
"react-resizable-panels": "^3.0.3",
@@ -178,25 +177,22 @@
178177
"@types/react": "19.1.3",
179178
"@types/react-dom": "19.1.3",
180179
"@types/uuid": "^10.0.0",
181-
"autoprefixer": "10.4.21",
182180
"baseline-browser-mapping": "^2.9.19",
183181
"dotenv": "^16.6.1",
184182
"dotenv-cli": "^8.0.0",
185183
"drizzle-kit": "^0.31.4",
186184
"knip": "^5.63.1",
187185
"markdown-toc": "^1.2.0",
188-
"mdx": "^0.3.1",
189186
"oxlint": "^1.23.0",
190187
"pg": "^8.14.1",
191188
"postcss": "8.5.3",
192189
"prettier": "3.5.3",
193190
"prisma": "^7.0.1",
194191
"prisma-json-types-generator": "^4.1.1",
195-
"rehype-img-size": "^1.0.1",
196192
"rehype-katex": "^7.0.1",
197193
"remark-math": "^6.0.0",
198194
"start-server-and-test": "^2.0.12",
199-
"tailwindcss": "4.1.5",
195+
"tailwindcss": "^4.1.11",
200196
"ts-node": "^10.9.2",
201197
"tsx": "^4.20.3",
202198
"typescript": "5.9.3",

‎apps/web/src/app/(app)/error.tsx‎

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
"use client";
2+
3+
import { Button } from "@community/ui/ui/button";
4+
import Link from "next/link";
5+
import { useEffect } from "react";
6+
7+
export default function AppError({
8+
error,
9+
reset,
10+
}: {
11+
error: Error & { digest?: string };
12+
reset: () => void;
13+
}) {
14+
useEffect(() => {
15+
console.error(error);
16+
}, [error]);
17+
18+
return (
19+
<div className="min-h-screen bg-slate-50">
20+
<div className="container max-w-3xl py-16 space-y-4">
21+
<h1 className="text-2xl font-semibold">页面加载失败</h1>
22+
<p className="text-sm text-muted-foreground">
23+
可能是网络波动或页面临时异常。你可以重试,或稍后再打开。
24+
</p>
25+
<div className="flex gap-2">
26+
<Button onClick={reset}>重试</Button>
27+
<Button
28+
variant="outline"
29+
onClick={() => window.location.reload()}
30+
>
31+
刷新
32+
</Button>
33+
<Button variant="link" asChild>
34+
<Link href="/auth/login">返回登录</Link>
35+
</Button>
36+
</div>
37+
</div>
38+
</div>
39+
);
40+
}

‎apps/web/src/app/(app)/layout.tsx‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
import { Providers } from "@community/ui/shared/Providers";
22
import { NextIntlClientProvider } from "next-intl";
3-
import { getLocale, getMessages } from "next-intl/server";
3+
import { getLocale, getMessages, setRequestLocale } from "next-intl/server";
44
import type { PropsWithChildren } from "react";
55

66
export default async function LocaleLayout({ children }: PropsWithChildren) {
77
const locale = await getLocale();
88
const messages = await getMessages();
9+
setRequestLocale(locale);
910

1011
return (
1112
<Providers>
12-
<NextIntlClientProvider messages={messages}>
13+
<NextIntlClientProvider locale={locale} messages={messages}>
1314
{children}
1415
</NextIntlClientProvider>
1516
</Providers>

‎apps/web/src/app/(app)/loading.tsx‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
"use client";
2+
3+
import { useState, useEffect } from "react";
4+
import { Skeleton } from "@community/ui/ui/skeleton";
5+
6+
export default function AppLoading() {
7+
const [show, setShow] = useState(false);
8+
9+
useEffect(() => {
10+
const timer = setTimeout(() => setShow(true), 300);
11+
return () => clearTimeout(timer);
12+
}, []);
13+
14+
if (!show) return null;
15+
16+
return (
17+
<div className="flex min-h-screen items-center justify-center">
18+
<div className="space-y-4 w-full max-w-sm px-4">
19+
<div className="flex justify-center">
20+
<Skeleton className="h-12 w-12 rounded-full" />
21+
</div>
22+
<Skeleton className="h-8 w-48 mx-auto" />
23+
<Skeleton className="h-4 w-64 mx-auto" />
24+
<div className="pt-4 space-y-3">
25+
<Skeleton className="h-10 w-full rounded-lg" />
26+
<Skeleton className="h-10 w-full rounded-lg" />
27+
</div>
28+
</div>
29+
</div>
30+
);
31+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
"use client";
2+
3+
import { Button } from "@community/ui/ui/button";
4+
import { useEffect } from "react";
5+
6+
export default function AccountError({
7+
error,
8+
reset,
9+
}: {
10+
error: Error & { digest?: string };
11+
reset: () => void;
12+
}) {
13+
useEffect(() => {
14+
console.error(error);
15+
}, [error]);
16+
17+
return (
18+
<div className="min-h-screen bg-slate-50">
19+
<div className="container max-w-3xl py-16 space-y-4">
20+
<h1 className="text-2xl font-semibold">服务端错误</h1>
21+
<p className="text-sm text-muted-foreground">
22+
服务器暂时不可用,请稍后再试。你可以重试或刷新页面。
23+
</p>
24+
<div className="flex gap-2">
25+
<Button onClick={reset}>重试</Button>
26+
<Button
27+
variant="outline"
28+
onClick={() => window.location.reload()}
29+
>
30+
刷新
31+
</Button>
32+
</div>
33+
</div>
34+
</div>
35+
);
36+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
"use client";
2+
3+
import { useState, useEffect } from "react";
4+
import { Skeleton } from "@community/ui/ui/skeleton";
5+
6+
export default function AccountLoading() {
7+
const [show, setShow] = useState(false);
8+
9+
useEffect(() => {
10+
const timer = setTimeout(() => setShow(true), 300);
11+
return () => clearTimeout(timer);
12+
}, []);
13+
14+
if (!show) return null;
15+
16+
return (
17+
<div className="flex min-h-screen w-full">
18+
<div className="hidden lg:block">
19+
<Skeleton className="h-screen w-60 rounded-none" />
20+
</div>
21+
<div className="flex-1">
22+
<div className="hidden border-b p-4 lg:block">
23+
<Skeleton className="h-8 w-96" />
24+
</div>
25+
<div className="border-b p-3 lg:hidden">
26+
<div className="flex gap-2">
27+
<Skeleton className="h-8 w-16 rounded-full" />
28+
<Skeleton className="h-8 w-16 rounded-full" />
29+
<Skeleton className="h-8 w-16 rounded-full" />
30+
</div>
31+
</div>
32+
<div className="mx-auto max-w-6xl space-y-4 p-4 lg:p-8">
33+
<Skeleton className="h-8 w-2/3" />
34+
<Skeleton className="h-4 w-1/3" />
35+
<div className="grid gap-4 pt-4 md:grid-cols-2">
36+
<div className="space-y-3">
37+
<Skeleton className="h-32 w-full rounded-xl" />
38+
<Skeleton className="h-32 w-full rounded-xl" />
39+
</div>
40+
<div className="space-y-3">
41+
<Skeleton className="h-32 w-full rounded-xl" />
42+
<Skeleton className="h-32 w-full rounded-xl" />
43+
</div>
44+
</div>
45+
</div>
46+
</div>
47+
</div>
48+
);
49+
}

0 commit comments

Comments
 (0)