feat: custom JWT auth + i18n with Chinese default
Replace Clerk with self-contained JWT auth (bcryptjs + jose), remove all external auth dependencies. Add full i18n system with Chinese as default language and English toggle — React Context + JSON dictionaries, no external i18n library. Auth: - Add passwordHash to User model, generate JWT secret - Create /api/auth/register, login, logout, me endpoints - Rewrite proxy.ts middleware for custom session validation - Add AuthProvider + useAuth hook, sign-in/sign-up pages - Wire auth into layout and all API routes i18n: - React Context I18nProvider with localStorage + cookie persistence - dictionaries/zh.ts (default) and dictionaries/en.ts - LanguageSwitcher component, server-side createServerT() utility - CJK font stack via [lang="zh"] CSS selector - HTML lang attribute synced on language change - All pages, components, TabBar, StageBadge translated - Engine fallback strings (reflection-analyzer, summary-generator) - API routes pass locale from cookie to engines Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
4e83ce95cd
commit
00db3f6c96
@@ -1,6 +1,12 @@
|
||||
import OpenAI from "openai";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { keywordCheck, llmSafetyCheck, type SafetyResult } from "@/lib/safety";
|
||||
import type { Lang } from "@/lib/i18n/types";
|
||||
import { createServerT } from "@/lib/i18n/utils";
|
||||
import zh from "../../../dictionaries/zh";
|
||||
import en from "../../../dictionaries/en";
|
||||
|
||||
const DICTS = { zh, en } as const;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Types
|
||||
@@ -56,8 +62,11 @@ Voice rules:
|
||||
|
||||
export async function generateWeeklySummary(
|
||||
userId: string,
|
||||
openai: OpenAI
|
||||
openai: OpenAI,
|
||||
locale: Lang = "zh"
|
||||
): Promise<GeneratedSummary> {
|
||||
const t = createServerT(DICTS[locale]);
|
||||
|
||||
// Gather week data
|
||||
const weekAgo = new Date();
|
||||
weekAgo.setDate(weekAgo.getDate() - 7);
|
||||
@@ -85,8 +94,7 @@ export async function generateWeeklySummary(
|
||||
|
||||
if (!userState || sessions.length === 0) {
|
||||
return {
|
||||
summary:
|
||||
"No completed practice sessions this week. Every week is a fresh start — your practice is here when you're ready.",
|
||||
summary: t("engine.summary.no_sessions"),
|
||||
safety: { safe: true },
|
||||
};
|
||||
}
|
||||
@@ -124,7 +132,7 @@ export async function generateWeeklySummary(
|
||||
// Safety check
|
||||
const keywordResult = keywordCheck(summary);
|
||||
if (!keywordResult.safe) {
|
||||
return { summary: "Your practice summary for this week.", safety: keywordResult };
|
||||
return { summary: t("engine.summary.safety_fallback"), safety: keywordResult };
|
||||
}
|
||||
|
||||
const llmSafety = await llmSafetyCheck(summary, openai as any);
|
||||
@@ -133,25 +141,34 @@ export async function generateWeeklySummary(
|
||||
const weekStart = new Date(weekAgo);
|
||||
weekStart.setHours(0, 0, 0, 0);
|
||||
|
||||
const safetyFallback = t("engine.summary.safety_fallback");
|
||||
|
||||
await prisma.weeklySummary.upsert({
|
||||
where: { userId },
|
||||
create: {
|
||||
userId,
|
||||
summary: llmSafety.safe ? summary : "Your practice summary for this week.",
|
||||
summary: llmSafety.safe ? summary : safetyFallback,
|
||||
weekStart,
|
||||
},
|
||||
update: {
|
||||
summary: llmSafety.safe ? summary : "Your practice summary for this week.",
|
||||
summary: llmSafety.safe ? summary : safetyFallback,
|
||||
weekStart,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
summary: llmSafety.safe ? summary : "Your practice summary for this week.",
|
||||
summary: llmSafety.safe ? summary : safetyFallback,
|
||||
safety: llmSafety,
|
||||
};
|
||||
} catch {
|
||||
const fallback = `You completed ${sessions.length} practice session${sessions.length === 1 ? "" : "s"} this week. Showing up consistently is the foundation everything else is built on. Your current streak is ${userState.streakDays} day${userState.streakDays === 1 ? "" : "s"}. See you next week.`;
|
||||
const count = sessions.length;
|
||||
const streak = userState.streakDays;
|
||||
const fallback = t("engine.summary.fallback_template", {
|
||||
count,
|
||||
plural: count === 1 ? "" : "s",
|
||||
streak,
|
||||
streak_plural: streak === 1 ? "" : "s",
|
||||
});
|
||||
return { summary: fallback, safety: { safe: true } };
|
||||
}
|
||||
}
|
||||
@@ -161,7 +178,8 @@ export async function generateWeeklySummary(
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export async function generateAllWeeklySummaries(
|
||||
openai: OpenAI
|
||||
openai: OpenAI,
|
||||
locale: Lang = "zh"
|
||||
): Promise<{ userId: string; success: boolean }[]> {
|
||||
// Find users who had sessions in the last 7 days
|
||||
const weekAgo = new Date();
|
||||
@@ -180,7 +198,7 @@ export async function generateAllWeeklySummaries(
|
||||
|
||||
for (const { userId } of activeUsers) {
|
||||
try {
|
||||
await generateWeeklySummary(userId, openai);
|
||||
await generateWeeklySummary(userId, openai, locale);
|
||||
results.push({ userId, success: true });
|
||||
} catch {
|
||||
results.push({ userId, success: false });
|
||||
|
||||
Reference in New Issue
Block a user