新版首页支持弹窗登录并接入Google后台配置
This commit is contained in:
parent
bc5838f9e1
commit
3f3523f1bb
@ -217,7 +217,11 @@ type AuthSettings = {
|
||||
// 保留单一配置用于兼容旧数据(后端会自动同步为第一个组织)
|
||||
dingTalkAppKey?: string | null;
|
||||
dingTalkAppSecret?: string | null;
|
||||
googleClientId?: string | null;
|
||||
googleClientSecret?: string | null;
|
||||
authUrl?: string | null;
|
||||
isDingTalkEnabled: boolean;
|
||||
isGoogleEnabled: boolean;
|
||||
isRegistrationEnabled?: boolean;
|
||||
dingTalkOrganizations: DingTalkOrganization[];
|
||||
};
|
||||
@ -776,10 +780,15 @@ export function AdminSettingsClient() {
|
||||
const [authSettings, setAuthSettings] = useState<AuthSettings>({
|
||||
dingTalkAppKey: "",
|
||||
dingTalkAppSecret: "",
|
||||
googleClientId: "",
|
||||
googleClientSecret: "",
|
||||
authUrl: "",
|
||||
isDingTalkEnabled: false,
|
||||
isGoogleEnabled: false,
|
||||
isRegistrationEnabled: true,
|
||||
dingTalkOrganizations: [],
|
||||
});
|
||||
const [isGoogleConfigured, setIsGoogleConfigured] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!authSettingsData) return;
|
||||
@ -804,10 +813,15 @@ export function AdminSettingsClient() {
|
||||
setAuthSettings({
|
||||
dingTalkAppKey: authSettingsData.dingTalkAppKey || "",
|
||||
dingTalkAppSecret: authSettingsData.dingTalkAppSecret || "",
|
||||
googleClientId: authSettingsData.googleClientId || "",
|
||||
googleClientSecret: authSettingsData.googleClientSecret || "",
|
||||
authUrl: authSettingsData.authUrl || "",
|
||||
isDingTalkEnabled: authSettingsData.isDingTalkEnabled || false,
|
||||
isGoogleEnabled: authSettingsData.isGoogleEnabled || false,
|
||||
isRegistrationEnabled: authSettingsData.isRegistrationEnabled ?? true,
|
||||
dingTalkOrganizations: normalizedOrgs,
|
||||
});
|
||||
setIsGoogleConfigured(Boolean(authSettingsData.isGoogleConfigured));
|
||||
}, [authSettingsData]);
|
||||
|
||||
// AI 提供商 & 模型状态
|
||||
@ -3863,6 +3877,69 @@ export function AdminSettingsClient() {
|
||||
</div>
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="space-y-3">
|
||||
<h3 className="text-lg font-semibold">Google 登录</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
通过 Google OAuth 登录,可直接在这里配置 Google 凭据与 AUTH_URL。
|
||||
</p>
|
||||
<div className="grid grid-cols-1 gap-3 rounded-md border p-3 md:grid-cols-2">
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="googleClientId">Google Client ID</Label>
|
||||
<Input
|
||||
id="googleClientId"
|
||||
value={authSettings.googleClientId || ""}
|
||||
onChange={(e) =>
|
||||
setAuthSettings({ ...authSettings, googleClientId: e.target.value })
|
||||
}
|
||||
placeholder="Google OAuth Client ID"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="googleClientSecret">Google Client Secret</Label>
|
||||
<Input
|
||||
id="googleClientSecret"
|
||||
type="password"
|
||||
value={authSettings.googleClientSecret || ""}
|
||||
onChange={(e) =>
|
||||
setAuthSettings({ ...authSettings, googleClientSecret: e.target.value })
|
||||
}
|
||||
placeholder="Google OAuth Client Secret"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1 md:col-span-2">
|
||||
<Label htmlFor="authUrl">AUTH_URL(站点地址)</Label>
|
||||
<Input
|
||||
id="authUrl"
|
||||
value={authSettings.authUrl || ""}
|
||||
onChange={(e) =>
|
||||
setAuthSettings({ ...authSettings, authUrl: e.target.value })
|
||||
}
|
||||
placeholder="https://your-domain.com"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between rounded-md border p-3">
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="googleEnabled">启用 Google 登录</Label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
当前服务器配置状态:{isGoogleConfigured ? "已配置" : "未配置"}
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
id="googleEnabled"
|
||||
checked={authSettings.isGoogleEnabled}
|
||||
onCheckedChange={(checked) =>
|
||||
setAuthSettings({ ...authSettings, isGoogleEnabled: checked })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
{!isGoogleConfigured ? (
|
||||
<p className="text-xs text-amber-600">
|
||||
未检测到完整 Google 配置(Client ID / Client Secret)。
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
<Separator />
|
||||
<Button onClick={handleSaveAuthSettings}>
|
||||
<Save className="mr-2 h-4 w-4" /> 保存认证配置
|
||||
</Button>
|
||||
|
||||
@ -1,49 +1,48 @@
|
||||
import type { NextAuthConfig } from "next-auth";
|
||||
|
||||
// 检测是否为 HTTPS 环境
|
||||
const isSecure = process.env.AUTH_URL?.startsWith("https://") ?? false;
|
||||
const hostName = process.env.AUTH_URL ? new URL(process.env.AUTH_URL).hostname : undefined;
|
||||
const cookieDomain = hostName && hostName !== "localhost" ? hostName : undefined;
|
||||
export function buildAuthConfig(authUrl?: string | null): NextAuthConfig {
|
||||
const resolvedAuthUrl = authUrl || process.env.AUTH_URL || "";
|
||||
const isSecure = resolvedAuthUrl.startsWith("https://");
|
||||
const hostName = resolvedAuthUrl ? new URL(resolvedAuthUrl).hostname : undefined;
|
||||
const cookieDomain = hostName && hostName !== "localhost" ? hostName : undefined;
|
||||
|
||||
export const authConfig = {
|
||||
trustHost: true,
|
||||
pages: {
|
||||
signIn: "/login",
|
||||
newUser: "/",
|
||||
},
|
||||
cookies: {
|
||||
sessionToken: {
|
||||
name: isSecure ? "__Secure-authjs.session-token" : "authjs.session-token",
|
||||
options: {
|
||||
httpOnly: true,
|
||||
sameSite: "lax",
|
||||
path: "/",
|
||||
secure: isSecure,
|
||||
domain: cookieDomain,
|
||||
return {
|
||||
trustHost: true,
|
||||
pages: {
|
||||
signIn: "/login",
|
||||
newUser: "/",
|
||||
},
|
||||
cookies: {
|
||||
sessionToken: {
|
||||
name: isSecure ? "__Secure-authjs.session-token" : "authjs.session-token",
|
||||
options: {
|
||||
httpOnly: true,
|
||||
sameSite: "lax",
|
||||
path: "/",
|
||||
secure: isSecure,
|
||||
domain: cookieDomain,
|
||||
},
|
||||
},
|
||||
callbackUrl: {
|
||||
name: isSecure ? "__Secure-authjs.callback-url" : "authjs.callback-url",
|
||||
options: {
|
||||
sameSite: "lax",
|
||||
path: "/",
|
||||
secure: isSecure,
|
||||
domain: cookieDomain,
|
||||
},
|
||||
},
|
||||
csrfToken: {
|
||||
name: isSecure ? "__Host-authjs.csrf-token" : "authjs.csrf-token",
|
||||
options: {
|
||||
httpOnly: true,
|
||||
sameSite: "lax",
|
||||
path: "/",
|
||||
secure: isSecure,
|
||||
},
|
||||
},
|
||||
},
|
||||
callbackUrl: {
|
||||
name: isSecure ? "__Secure-authjs.callback-url" : "authjs.callback-url",
|
||||
options: {
|
||||
sameSite: "lax",
|
||||
path: "/",
|
||||
secure: isSecure,
|
||||
domain: cookieDomain,
|
||||
},
|
||||
},
|
||||
csrfToken: {
|
||||
name: isSecure ? "__Host-authjs.csrf-token" : "authjs.csrf-token",
|
||||
options: {
|
||||
httpOnly: true,
|
||||
sameSite: "lax",
|
||||
path: "/",
|
||||
secure: isSecure,
|
||||
},
|
||||
},
|
||||
},
|
||||
providers: [
|
||||
// added later in auth.ts since it requires bcrypt which is only compatible with Node.js
|
||||
// while this file is also used in non-Node.js environments
|
||||
],
|
||||
callbacks: {},
|
||||
} satisfies NextAuthConfig;
|
||||
providers: [],
|
||||
callbacks: {},
|
||||
};
|
||||
}
|
||||
|
||||
@ -2,11 +2,12 @@ import { compare } from "bcrypt-ts";
|
||||
import NextAuth, { type DefaultSession } from "next-auth";
|
||||
import type { DefaultJWT } from "next-auth/jwt";
|
||||
import Credentials from "next-auth/providers/credentials";
|
||||
import Google from "next-auth/providers/google";
|
||||
import { DUMMY_PASSWORD } from "@/lib/constants";
|
||||
import { createGuestUser, getUser } from "@/lib/db/queries";
|
||||
import { authConfig } from "./auth.config";
|
||||
import { createGuestUser, getAuthSettings, getUser } from "@/lib/db/queries";
|
||||
import { buildAuthConfig } from "./auth.config";
|
||||
import { db } from "@/lib/db";
|
||||
import { loginToken } from "@/lib/db/schema";
|
||||
import { loginToken, user as userTable } from "@/lib/db/schema";
|
||||
import { eq, and, gt } from "drizzle-orm";
|
||||
|
||||
export type UserType = "guest" | "regular" | "admin";
|
||||
@ -25,7 +26,7 @@ declare module "next-auth" {
|
||||
id?: string;
|
||||
email?: string | null;
|
||||
name?: string | null;
|
||||
type: UserType;
|
||||
type?: UserType;
|
||||
}
|
||||
}
|
||||
|
||||
@ -37,15 +38,28 @@ declare module "next-auth/jwt" {
|
||||
}
|
||||
}
|
||||
|
||||
export const {
|
||||
handlers: { GET, POST },
|
||||
auth,
|
||||
signIn,
|
||||
signOut,
|
||||
} = NextAuth({
|
||||
...authConfig,
|
||||
trustHost: true,
|
||||
providers: [
|
||||
async function getOrCreateGoogleUser(email: string, name?: string | null) {
|
||||
const existingUsers = await getUser(email);
|
||||
if (existingUsers.length > 0) {
|
||||
return existingUsers[0];
|
||||
}
|
||||
|
||||
const [createdUser] = await db
|
||||
.insert(userTable)
|
||||
.values({
|
||||
email,
|
||||
password: "",
|
||||
name: (name || email.split("@")[0] || "").slice(0, 100),
|
||||
role: "regular",
|
||||
isActive: true,
|
||||
})
|
||||
.returning();
|
||||
|
||||
return createdUser || null;
|
||||
}
|
||||
|
||||
function createBaseProviders(googleClientId?: string, googleClientSecret?: string) {
|
||||
const providers: any[] = [
|
||||
Credentials({
|
||||
credentials: {},
|
||||
async authorize(credentials: any) {
|
||||
@ -141,25 +155,101 @@ export const {
|
||||
return { ...guestUser, type: "guest" };
|
||||
},
|
||||
}),
|
||||
],
|
||||
callbacks: {
|
||||
jwt({ token, user }) {
|
||||
if (user) {
|
||||
token.id = user.id as string;
|
||||
token.type = user.type;
|
||||
token.name = user.name;
|
||||
}
|
||||
];
|
||||
|
||||
return token;
|
||||
},
|
||||
session({ session, token }) {
|
||||
if (session.user) {
|
||||
session.user.id = token.id;
|
||||
session.user.type = token.type;
|
||||
session.user.name = token.name as string;
|
||||
}
|
||||
if (googleClientId && googleClientSecret) {
|
||||
providers.push(
|
||||
Google({
|
||||
clientId: googleClientId,
|
||||
clientSecret: googleClientSecret,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return session;
|
||||
return providers;
|
||||
}
|
||||
|
||||
export const {
|
||||
handlers: { GET, POST },
|
||||
auth,
|
||||
signIn,
|
||||
signOut,
|
||||
} = NextAuth(async () => {
|
||||
const runtimeAuthSettings = await getAuthSettings().catch(() => null as any);
|
||||
const resolvedAuthUrl =
|
||||
runtimeAuthSettings?.authUrl || process.env.AUTH_URL || undefined;
|
||||
const googleClientId =
|
||||
runtimeAuthSettings?.googleClientId || process.env.GOOGLE_CLIENT_ID || "";
|
||||
const googleClientSecret =
|
||||
runtimeAuthSettings?.googleClientSecret || process.env.GOOGLE_CLIENT_SECRET || "";
|
||||
|
||||
return {
|
||||
...buildAuthConfig(resolvedAuthUrl),
|
||||
trustHost: true,
|
||||
providers: createBaseProviders(googleClientId, googleClientSecret),
|
||||
callbacks: {
|
||||
async signIn({ account, user, profile }) {
|
||||
if (account?.provider !== "google") {
|
||||
return true;
|
||||
}
|
||||
|
||||
const authSettings = await getAuthSettings();
|
||||
if (!authSettings.isGoogleEnabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const email = (user?.email || (profile as any)?.email || "").trim().toLowerCase();
|
||||
if (!email) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const dbUser = await getOrCreateGoogleUser(
|
||||
email,
|
||||
user?.name || (profile as any)?.name || null
|
||||
);
|
||||
|
||||
return Boolean(dbUser?.isActive);
|
||||
},
|
||||
async jwt({ token, user, account, profile }) {
|
||||
if (user?.id && user?.type) {
|
||||
token.id = user.id as string;
|
||||
token.type = user.type;
|
||||
token.name = user.name;
|
||||
}
|
||||
|
||||
if (account?.provider === "google") {
|
||||
const email = String(
|
||||
user?.email || (profile as any)?.email || token.email || ""
|
||||
)
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
|
||||
if (email) {
|
||||
const dbUser = await getOrCreateGoogleUser(
|
||||
email,
|
||||
user?.name || (profile as any)?.name || null
|
||||
);
|
||||
|
||||
if (dbUser) {
|
||||
token.id = dbUser.id;
|
||||
token.type = (dbUser.role as UserType) || "regular";
|
||||
token.name = dbUser.name;
|
||||
token.email = dbUser.email;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return token;
|
||||
},
|
||||
session({ session, token }) {
|
||||
if (session.user) {
|
||||
session.user.id = token.id;
|
||||
session.user.type = token.type || "regular";
|
||||
session.user.name = token.name as string;
|
||||
}
|
||||
|
||||
return session;
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@ -28,6 +28,8 @@ export default function Page() {
|
||||
const [siteLogo, setSiteLogo] = useState<string | null>(null);
|
||||
const [siteLogoDark, setSiteLogoDark] = useState<string | null>(null);
|
||||
const [isDingTalkEnabled, setIsDingTalkEnabled] = useState(false);
|
||||
const [isGoogleEnabled, setIsGoogleEnabled] = useState(false);
|
||||
const [isGoogleConfigured, setIsGoogleConfigured] = useState(false);
|
||||
const [dingTalkAppKey, setDingTalkAppKey] = useState<string | null>(null);
|
||||
const [dingTalkOrganizations, setDingTalkOrganizations] = useState<string[]>([]);
|
||||
const [isRegistrationEnabled, setIsRegistrationEnabled] = useState(true);
|
||||
@ -215,6 +217,8 @@ export default function Page() {
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
if (data.isDingTalkEnabled) setIsDingTalkEnabled(true);
|
||||
if (data.isGoogleEnabled) setIsGoogleEnabled(true);
|
||||
if (data.isGoogleConfigured) setIsGoogleConfigured(true);
|
||||
if (data.dingTalkAppKey) setDingTalkAppKey(data.dingTalkAppKey);
|
||||
if (Array.isArray(data.organizations)) {
|
||||
setDingTalkOrganizations(
|
||||
@ -230,6 +234,20 @@ export default function Page() {
|
||||
.catch(console.error);
|
||||
}, []);
|
||||
|
||||
const handleGoogleLogin = async () => {
|
||||
if (!isGoogleEnabled) {
|
||||
toast({ type: "error", description: "Google 登录未启用" });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isGoogleConfigured) {
|
||||
toast({ type: "error", description: "Google 登录未完成服务器配置" });
|
||||
return;
|
||||
}
|
||||
|
||||
await signIn("google", { callbackUrl: "/" });
|
||||
};
|
||||
|
||||
// 检查 URL 中是否有钉钉 Token(页面加载时自动登录)
|
||||
useEffect(() => {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
@ -413,6 +431,9 @@ export default function Page() {
|
||||
const displayLogo = mounted
|
||||
? (resolvedTheme === "dark" && siteLogoDark ? siteLogoDark : siteLogo)
|
||||
: siteLogo;
|
||||
const hasThirdPartyLogin =
|
||||
(isDingTalkEnabled && (Boolean(dingTalkAppKey) || dingTalkOrganizations.length > 0)) ||
|
||||
(isGoogleEnabled && isGoogleConfigured);
|
||||
|
||||
return (
|
||||
<div className="flex h-dvh w-screen items-start justify-center bg-background pt-12 md:items-center md:pt-0 relative overflow-hidden">
|
||||
@ -448,7 +469,7 @@ export default function Page() {
|
||||
<AuthForm action={handleSubmit} defaultEmail={email} className="pointer-events-auto">
|
||||
<SubmitButton isSuccessful={isSuccessful}>登录</SubmitButton>
|
||||
|
||||
{isDingTalkEnabled && (dingTalkAppKey || dingTalkOrganizations.length > 0) && (
|
||||
{hasThirdPartyLogin && (
|
||||
<div className="mt-4 w-full relative">
|
||||
<div className="relative mb-4">
|
||||
<div className="absolute inset-0 flex items-center">
|
||||
@ -460,19 +481,36 @@ export default function Page() {
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleDingTalkLogin}
|
||||
className="flex w-full items-center justify-center gap-2 rounded-md border bg-white px-4 py-2 text-sm font-medium text-gray-700 shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:bg-zinc-900 dark:text-zinc-200 dark:hover:bg-zinc-800"
|
||||
>
|
||||
<svg style={{ width: "20px", height: "20px" }} viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M512 2C230.2 2 2 230.2 2 512s228.2 510 510 510 510-228.2 510-510S793.3 2 512 2z m235.9 442c-1 4.6-3.6 10.8-7.2 19.1l-0.5 0.5c-21.6 45.8-77.3 135.5-77.3 135.5l-0.5-0.5-16.5 28.3h78.8L574.3 826.8l34-136h-61.8l21.6-90.2c-17.5 4.1-38.1 9.8-62.3 18 0 0-33 19.1-94.8-37.1 0 0-41.7-37.1-17.5-45.8 10.3-4.1 50-8.8 81.4-12.9 42.2-5.7 68.5-8.8 68.5-8.8s-130.3 2.1-161.2-3.1c-30.9-4.6-70.1-56.7-78.3-102 0 0-12.9-24.7 27.8-12.9 40.2 11.8 209.2 45.8 209.2 45.8S321.4 375 307 358.5c-14.4-16.5-42.8-89.6-39.2-134.5 0 0 1.5-11.3 12.9-8.2 0 0 161.8 74.2 272.5 114.4C664.5 371.4 760.8 392 747.9 444z"
|
||||
fill="#3296FA"
|
||||
/>
|
||||
</svg>
|
||||
钉钉登录
|
||||
</button>
|
||||
<div className="space-y-2">
|
||||
{isDingTalkEnabled && (dingTalkAppKey || dingTalkOrganizations.length > 0) ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleDingTalkLogin}
|
||||
className="flex w-full items-center justify-center gap-2 rounded-md border bg-white px-4 py-2 text-sm font-medium text-gray-700 shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:bg-zinc-900 dark:text-zinc-200 dark:hover:bg-zinc-800"
|
||||
>
|
||||
<svg style={{ width: "20px", height: "20px" }} viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M512 2C230.2 2 2 230.2 2 512s228.2 510 510 510 510-228.2 510-510S793.3 2 512 2z m235.9 442c-1 4.6-3.6 10.8-7.2 19.1l-0.5 0.5c-21.6 45.8-77.3 135.5-77.3 135.5l-0.5-0.5-16.5 28.3h78.8L574.3 826.8l34-136h-61.8l21.6-90.2c-17.5 4.1-38.1 9.8-62.3 18 0 0-33 19.1-94.8-37.1 0 0-41.7-37.1-17.5-45.8 10.3-4.1 50-8.8 81.4-12.9 42.2-5.7 68.5-8.8 68.5-8.8s-130.3 2.1-161.2-3.1c-30.9-4.6-70.1-56.7-78.3-102 0 0-12.9-24.7 27.8-12.9 40.2 11.8 209.2 45.8 209.2 45.8S321.4 375 307 358.5c-14.4-16.5-42.8-89.6-39.2-134.5 0 0 1.5-11.3 12.9-8.2 0 0 161.8 74.2 272.5 114.4C664.5 371.4 760.8 392 747.9 444z"
|
||||
fill="#3296FA"
|
||||
/>
|
||||
</svg>
|
||||
钉钉登录
|
||||
</button>
|
||||
) : null}
|
||||
|
||||
{isGoogleEnabled && isGoogleConfigured ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleGoogleLogin}
|
||||
className="flex w-full items-center justify-center gap-2 rounded-md border bg-white px-4 py-2 text-sm font-medium text-gray-700 shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:bg-zinc-900 dark:text-zinc-200 dark:hover:bg-zinc-800"
|
||||
>
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path fill="#EA4335" d="M12 10.2v3.9h5.5c-.2 1.2-1.4 3.5-5.5 3.5-3.3 0-6-2.7-6-6s2.7-6 6-6c1.9 0 3.2.8 3.9 1.5l2.7-2.6C16.9 2.9 14.6 2 12 2 6.5 2 2 6.5 2 12s4.5 10 10 10c5.8 0 9.6-4.1 9.6-9.8 0-.7-.1-1.2-.2-1.7H12z" />
|
||||
</svg>
|
||||
Google 登录
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{/* 多组织下的小型选择框(悬浮在按钮附近) */}
|
||||
{dingTalkOrganizations.length > 1 && showOrgMenu && (
|
||||
|
||||
@ -34,11 +34,6 @@ async function NewChatPage() {
|
||||
getSystemSetting({ key: "home_surprise_categories" }).catch(() => undefined),
|
||||
]);
|
||||
|
||||
// 如果用户未登录,重定向到登录页面
|
||||
if (!session?.user) {
|
||||
redirect("/login");
|
||||
}
|
||||
|
||||
const homeVariant = homeVariantSetting?.value === "lovart" ? "lovart" : "classic";
|
||||
const surpriseCategories = Array.isArray(surpriseCategoriesSetting?.value)
|
||||
? surpriseCategoriesSetting?.value
|
||||
@ -46,15 +41,24 @@ async function NewChatPage() {
|
||||
.filter(Boolean)
|
||||
: ["全部", "惊喜"];
|
||||
|
||||
if (!session?.user && homeVariant !== "lovart") {
|
||||
redirect("/login");
|
||||
}
|
||||
|
||||
if (homeVariant === "lovart") {
|
||||
return (
|
||||
<LovartHome
|
||||
user={{
|
||||
id: session.user.id,
|
||||
email: session.user.email,
|
||||
name: session.user.name,
|
||||
type: session.user.type,
|
||||
}}
|
||||
user={
|
||||
session?.user
|
||||
? {
|
||||
id: session.user.id,
|
||||
email: session.user.email,
|
||||
name: session.user.name,
|
||||
type: session.user.type,
|
||||
}
|
||||
: null
|
||||
}
|
||||
isAuthenticated={Boolean(session?.user)}
|
||||
siteName={siteSettings.siteName || "AI Chatbot"}
|
||||
siteLogo={siteSettings.siteLogo || siteSettings.siteLogoDark || ""}
|
||||
surpriseCategories={surpriseCategories}
|
||||
|
||||
@ -12,6 +12,14 @@ export async function GET() {
|
||||
const isAdmin = session?.user?.type === "admin";
|
||||
|
||||
const [settings] = await db.select().from(authSettings).limit(1);
|
||||
const resolvedGoogleClientId =
|
||||
settings?.googleClientId || process.env.GOOGLE_CLIENT_ID || "";
|
||||
const resolvedGoogleClientSecret =
|
||||
settings?.googleClientSecret || process.env.GOOGLE_CLIENT_SECRET || "";
|
||||
const resolvedAuthUrl = settings?.authUrl || process.env.AUTH_URL || "";
|
||||
const isGoogleConfigured = Boolean(
|
||||
resolvedGoogleClientId && resolvedGoogleClientSecret
|
||||
);
|
||||
|
||||
// 如果没有配置记录,返回默认值
|
||||
if (!settings) {
|
||||
@ -20,6 +28,11 @@ export async function GET() {
|
||||
dingTalkAppSecret: "", // 即使是空的,非管理员也不应该看到这个字段名,但因为是空字符串,问题不大
|
||||
dingTalkOrganizations: [],
|
||||
isDingTalkEnabled: false,
|
||||
isGoogleEnabled: false,
|
||||
googleClientId: "",
|
||||
googleClientSecret: "",
|
||||
authUrl: process.env.AUTH_URL || "",
|
||||
isGoogleConfigured,
|
||||
isRegistrationEnabled: true,
|
||||
});
|
||||
}
|
||||
@ -27,6 +40,10 @@ export async function GET() {
|
||||
if (isAdmin) {
|
||||
return NextResponse.json({
|
||||
...settings,
|
||||
googleClientId: resolvedGoogleClientId,
|
||||
googleClientSecret: resolvedGoogleClientSecret,
|
||||
authUrl: resolvedAuthUrl,
|
||||
isGoogleConfigured,
|
||||
dingTalkOrganizations: settings.dingTalkOrganizations ?? [],
|
||||
});
|
||||
}
|
||||
@ -40,6 +57,9 @@ export async function GET() {
|
||||
return NextResponse.json({
|
||||
dingTalkAppKey: settings.dingTalkAppKey,
|
||||
isDingTalkEnabled: settings.isDingTalkEnabled,
|
||||
isGoogleEnabled: settings.isGoogleEnabled,
|
||||
authUrl: resolvedAuthUrl,
|
||||
isGoogleConfigured,
|
||||
isRegistrationEnabled: settings.isRegistrationEnabled,
|
||||
organizations,
|
||||
// 不返回 dingTalkAppSecret 以及各组织的密钥
|
||||
@ -64,6 +84,10 @@ const updateSchema = z.object({
|
||||
dingTalkAppSecret: z.string().optional(),
|
||||
dingTalkOrganizations: z.array(orgSchema).optional(),
|
||||
isDingTalkEnabled: z.boolean().optional(),
|
||||
isGoogleEnabled: z.boolean().optional(),
|
||||
googleClientId: z.string().optional(),
|
||||
googleClientSecret: z.string().optional(),
|
||||
authUrl: z.string().optional(),
|
||||
isRegistrationEnabled: z.boolean().optional(),
|
||||
});
|
||||
|
||||
@ -95,6 +119,11 @@ export async function PUT(request: NextRequest) {
|
||||
data.dingTalkAppKey ?? primaryOrg?.appKey ?? existing?.dingTalkAppKey ?? null;
|
||||
const resolvedAppSecret =
|
||||
data.dingTalkAppSecret ?? primaryOrg?.appSecret ?? existing?.dingTalkAppSecret ?? null;
|
||||
const resolvedGoogleClientId =
|
||||
data.googleClientId ?? existing?.googleClientId ?? "";
|
||||
const resolvedGoogleClientSecret =
|
||||
data.googleClientSecret ?? existing?.googleClientSecret ?? "";
|
||||
const resolvedAuthUrl = data.authUrl ?? existing?.authUrl ?? "";
|
||||
|
||||
if (existing) {
|
||||
// 更新现有配置
|
||||
@ -103,9 +132,13 @@ export async function PUT(request: NextRequest) {
|
||||
.set({
|
||||
dingTalkAppKey: resolvedAppKey,
|
||||
dingTalkAppSecret: resolvedAppSecret,
|
||||
googleClientId: resolvedGoogleClientId,
|
||||
googleClientSecret: resolvedGoogleClientSecret,
|
||||
authUrl: resolvedAuthUrl,
|
||||
dingTalkOrganizations: organizations,
|
||||
isDingTalkEnabled:
|
||||
data.isDingTalkEnabled ?? existing.isDingTalkEnabled,
|
||||
isGoogleEnabled: data.isGoogleEnabled ?? existing.isGoogleEnabled,
|
||||
isRegistrationEnabled:
|
||||
data.isRegistrationEnabled ?? existing.isRegistrationEnabled,
|
||||
updatedAt: new Date(),
|
||||
@ -123,8 +156,12 @@ export async function PUT(request: NextRequest) {
|
||||
.values({
|
||||
dingTalkAppKey: resolvedAppKey,
|
||||
dingTalkAppSecret: resolvedAppSecret,
|
||||
googleClientId: resolvedGoogleClientId,
|
||||
googleClientSecret: resolvedGoogleClientSecret,
|
||||
authUrl: resolvedAuthUrl,
|
||||
dingTalkOrganizations: organizations,
|
||||
isDingTalkEnabled: data.isDingTalkEnabled || false,
|
||||
isGoogleEnabled: data.isGoogleEnabled || false,
|
||||
isRegistrationEnabled: data.isRegistrationEnabled ?? true,
|
||||
updatedBy: session.user.id,
|
||||
})
|
||||
|
||||
@ -2,17 +2,10 @@ import { NextRequest, NextResponse } from "next/server";
|
||||
import { db } from "@/lib/db";
|
||||
import { generatedImage, user, aiModel } from "@/lib/db/schema";
|
||||
import { eq, desc, sql } from "drizzle-orm";
|
||||
import { auth } from "@/app/(auth)/auth";
|
||||
|
||||
// GET: 获取精选图片列表
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const session = await auth();
|
||||
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
const searchParams = request.nextUrl.searchParams;
|
||||
const page = Number.parseInt(searchParams.get("page") || "1");
|
||||
const pageSize = Number.parseInt(searchParams.get("pageSize") || "24");
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
import { Fragment, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { signIn } from "next-auth/react";
|
||||
import useSWR from "swr";
|
||||
import useSWRInfinite from "swr/infinite";
|
||||
import {
|
||||
@ -25,6 +26,7 @@ import {
|
||||
Star,
|
||||
Upload,
|
||||
User,
|
||||
X,
|
||||
Zap,
|
||||
} from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
@ -32,6 +34,7 @@ import { Button } from "@/components/ui/button";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||
import { HoverCard, HoverCardContent, HoverCardTrigger } from "@/components/ui/hover-card";
|
||||
import { ImageParamsBar, type ImageGenerationParams } from "@/components/image-params-bar";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger } from "@/components/ui/select";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { fetcher } from "@/lib/utils";
|
||||
@ -87,6 +90,12 @@ type ModelOption = {
|
||||
supportsImageGen?: boolean;
|
||||
};
|
||||
|
||||
type PublicAuthSettings = {
|
||||
isDingTalkEnabled?: boolean;
|
||||
isGoogleEnabled?: boolean;
|
||||
isGoogleConfigured?: boolean;
|
||||
};
|
||||
|
||||
type MembershipSummaryResponse = {
|
||||
config: MembershipConfig;
|
||||
currentPlanId: string;
|
||||
@ -112,7 +121,8 @@ type MembershipSummaryResponse = {
|
||||
};
|
||||
|
||||
type LovartHomeProps = {
|
||||
user: HomeUser;
|
||||
user: HomeUser | null;
|
||||
isAuthenticated: boolean;
|
||||
siteName: string;
|
||||
siteLogo?: string | null;
|
||||
surpriseCategories: string[];
|
||||
@ -171,6 +181,7 @@ function formatPrice(currency: string, value: number) {
|
||||
|
||||
export function LovartHome({
|
||||
user,
|
||||
isAuthenticated,
|
||||
siteName,
|
||||
siteLogo,
|
||||
surpriseCategories,
|
||||
@ -193,6 +204,10 @@ export function LovartHome({
|
||||
imageSize: "2K",
|
||||
imageCount: 1,
|
||||
});
|
||||
const [loginDialogOpen, setLoginDialogOpen] = useState(false);
|
||||
const [loginEmail, setLoginEmail] = useState("");
|
||||
const [loginPassword, setLoginPassword] = useState("");
|
||||
const [isLoggingIn, setIsLoggingIn] = useState(false);
|
||||
const [typedPreviewText, setTypedPreviewText] = useState("");
|
||||
const [previewCursorVisible, setPreviewCursorVisible] = useState(true);
|
||||
const projectSectionRef = useRef<HTMLDivElement | null>(null);
|
||||
@ -202,7 +217,7 @@ export function LovartHome({
|
||||
const { data: canvasData, mutate: mutateCanvas } = useSWR<{
|
||||
success: boolean;
|
||||
data: CanvasProject[];
|
||||
}>("/api/canvas", fetcher);
|
||||
}>(isAuthenticated ? "/api/canvas" : null, fetcher);
|
||||
|
||||
const { data: featuredPages, size: featuredPageCount, setSize: setFeaturedPageCount, isValidating: isFeaturedValidating } =
|
||||
useSWRInfinite<FeaturedResponse>(
|
||||
@ -224,19 +239,24 @@ export function LovartHome({
|
||||
);
|
||||
|
||||
const { data: membershipData } = useSWR<MembershipSummaryResponse>(
|
||||
"/api/user/membership-summary",
|
||||
isAuthenticated ? "/api/user/membership-summary" : null,
|
||||
fetcher
|
||||
);
|
||||
|
||||
const { data: modelsData } = useSWR<{
|
||||
models: ModelOption[];
|
||||
}>("/api/models", fetcher);
|
||||
}>(isAuthenticated ? "/api/models" : null, fetcher);
|
||||
|
||||
const { data: siteSettingsData } = useSWR<{
|
||||
canvasDefaultModelId?: string | null;
|
||||
lovartComposerPreviewTexts?: string[];
|
||||
}>("/api/site-settings", fetcher);
|
||||
|
||||
const { data: authSettingsData } = useSWR<PublicAuthSettings>(
|
||||
"/api/auth-settings",
|
||||
fetcher
|
||||
);
|
||||
|
||||
const membershipConfig = membershipData?.config;
|
||||
const membershipPlans = membershipConfig?.plans || [];
|
||||
const currentPlan = membershipData?.currentPlan;
|
||||
@ -337,6 +357,12 @@ export function LovartHome({
|
||||
return (canvasData?.data || []).length > 4;
|
||||
}, [canvasData?.data]);
|
||||
|
||||
const canUseGoogleLogin = Boolean(
|
||||
authSettingsData?.isGoogleEnabled && authSettingsData?.isGoogleConfigured
|
||||
);
|
||||
const canUseDingTalkLogin = Boolean(authSettingsData?.isDingTalkEnabled);
|
||||
const hasThirdPartyLoginOptions = canUseGoogleLogin || canUseDingTalkLogin;
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedModelId) return;
|
||||
const defaultModelId = siteSettingsData?.canvasDefaultModelId || "auto";
|
||||
@ -351,6 +377,18 @@ export function LovartHome({
|
||||
setSelectedModelId("auto");
|
||||
}, [allComposerModels, selectedModelId, modelsData]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isAuthenticated) return;
|
||||
setLoginDialogOpen(false);
|
||||
}, [isAuthenticated]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!loginDialogOpen) {
|
||||
setIsLoggingIn(false);
|
||||
setLoginPassword("");
|
||||
}
|
||||
}, [loginDialogOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window === "undefined") return;
|
||||
try {
|
||||
@ -508,7 +546,56 @@ export function LovartHome({
|
||||
return result;
|
||||
};
|
||||
|
||||
const openLoginDialog = () => {
|
||||
setLoginDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleEmailLogin = async () => {
|
||||
const email = loginEmail.trim();
|
||||
if (!email || !loginPassword) {
|
||||
toast.error("请输入邮箱和密码");
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoggingIn(true);
|
||||
try {
|
||||
const result = await signIn("credentials", {
|
||||
email,
|
||||
password: loginPassword,
|
||||
redirect: false,
|
||||
});
|
||||
|
||||
if (result?.error) {
|
||||
toast.error("登录失败,请检查账号密码");
|
||||
return;
|
||||
}
|
||||
|
||||
window.location.href = "/";
|
||||
} catch (_error) {
|
||||
toast.error("登录失败,请稍后重试");
|
||||
} finally {
|
||||
setIsLoggingIn(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleGoogleLogin = async () => {
|
||||
if (!canUseGoogleLogin) {
|
||||
toast.error("Google 登录未启用");
|
||||
return;
|
||||
}
|
||||
await signIn("google", { callbackUrl: "/" });
|
||||
};
|
||||
|
||||
const handleDingTalkLogin = () => {
|
||||
window.location.href = "/login";
|
||||
};
|
||||
|
||||
const createCanvasFromComposer = async () => {
|
||||
if (!isAuthenticated) {
|
||||
openLoginDialog();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!prompt.trim() && files.length === 0) return;
|
||||
|
||||
setIsSubmitting(true);
|
||||
@ -562,10 +649,18 @@ export function LovartHome({
|
||||
};
|
||||
|
||||
const openNewCanvas = () => {
|
||||
if (!isAuthenticated) {
|
||||
openLoginDialog();
|
||||
return;
|
||||
}
|
||||
router.push("/canvas?new=1");
|
||||
};
|
||||
|
||||
const openAllProjectsInPage = () => {
|
||||
if (!isAuthenticated) {
|
||||
openLoginDialog();
|
||||
return;
|
||||
}
|
||||
setShowAllProjects(true);
|
||||
projectSectionRef.current?.scrollIntoView({ behavior: "smooth", block: "start" });
|
||||
};
|
||||
@ -658,9 +753,19 @@ export function LovartHome({
|
||||
</HoverCardContent>
|
||||
</HoverCard>
|
||||
|
||||
<button className="flex h-8 w-8 items-center justify-center rounded-full bg-[#d4551f] text-[14px] font-semibold text-white">
|
||||
{(user.name || user.email || "mu").slice(0, 2).toLowerCase()}
|
||||
</button>
|
||||
{isAuthenticated ? (
|
||||
<button className="flex h-8 w-8 items-center justify-center rounded-full bg-[#d4551f] text-[14px] font-semibold text-white">
|
||||
{(user?.name || user?.email || "mu").slice(0, 2).toLowerCase()}
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex h-8 items-center justify-center rounded-full border border-black/15 bg-white px-3 text-xs font-medium text-black/80 hover:bg-black/5"
|
||||
onClick={openLoginDialog}
|
||||
>
|
||||
登录
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
@ -705,7 +810,7 @@ export function LovartHome({
|
||||
>
|
||||
<Info className="h-[18px] w-[18px]" />
|
||||
</button>
|
||||
{user.type === "admin" ? (
|
||||
{user?.type === "admin" ? (
|
||||
<button
|
||||
type="button"
|
||||
className="flex h-10 w-10 items-center justify-center rounded-full hover:bg-black/5"
|
||||
@ -990,6 +1095,120 @@ export function LovartHome({
|
||||
<Send className="h-4 w-4 -rotate-90" />
|
||||
</button>
|
||||
|
||||
{loginDialogOpen ? (
|
||||
<div
|
||||
className="fixed inset-0 z-50 flex items-center justify-center bg-black/55 p-4 backdrop-blur-[1px]"
|
||||
onClick={() => setLoginDialogOpen(false)}
|
||||
>
|
||||
<div
|
||||
className="w-full max-w-[580px] rounded-[28px] bg-white p-8 shadow-2xl sm:p-10"
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
>
|
||||
<div className="mb-8 flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
{siteLogo ? (
|
||||
<img
|
||||
src={siteLogo}
|
||||
alt={siteName}
|
||||
className="h-8 w-8 rounded-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-black text-white">
|
||||
<Sparkles className="h-4 w-4" />
|
||||
</div>
|
||||
)}
|
||||
<span className="text-[20px] font-semibold leading-none tracking-tight text-black">
|
||||
{siteName}
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="rounded-full p-1 text-black/45 hover:bg-black/5 hover:text-black/70"
|
||||
onClick={() => setLoginDialogOpen(false)}
|
||||
>
|
||||
<X className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="mb-7 text-center">
|
||||
<h3 className="text-[22px] font-semibold text-black">欢迎来到 {siteName}</h3>
|
||||
<p className="mt-2 text-sm text-black/45">登录或注册以继续</p>
|
||||
</div>
|
||||
|
||||
{hasThirdPartyLoginOptions ? (
|
||||
<div className="space-y-2.5">
|
||||
{canUseGoogleLogin ? (
|
||||
<button
|
||||
type="button"
|
||||
className="flex h-14 w-full items-center justify-center gap-2 rounded-xl border border-black/10 text-[18px] font-medium text-black hover:bg-black/[0.03]"
|
||||
onClick={handleGoogleLogin}
|
||||
>
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path
|
||||
fill="#EA4335"
|
||||
d="M12 10.2v3.9h5.5c-.2 1.2-1.4 3.5-5.5 3.5-3.3 0-6-2.7-6-6s2.7-6 6-6c1.9 0 3.2.8 3.9 1.5l2.7-2.6C16.9 2.9 14.6 2 12 2 6.5 2 2 6.5 2 12s4.5 10 10 10c5.8 0 9.6-4.1 9.6-9.8 0-.7-.1-1.2-.2-1.7H12z"
|
||||
/>
|
||||
</svg>
|
||||
使用 Google
|
||||
</button>
|
||||
) : null}
|
||||
{canUseDingTalkLogin ? (
|
||||
<button
|
||||
type="button"
|
||||
className="flex h-14 w-full items-center justify-center gap-2 rounded-xl border border-black/10 text-[18px] font-medium text-black hover:bg-black/[0.03]"
|
||||
onClick={handleDingTalkLogin}
|
||||
>
|
||||
使用钉钉
|
||||
</button>
|
||||
) : null}
|
||||
<div className="my-2 border-t border-black/10" />
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<form
|
||||
className="space-y-3"
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault();
|
||||
handleEmailLogin();
|
||||
}}
|
||||
>
|
||||
<Input
|
||||
value={loginEmail}
|
||||
onChange={(event) => setLoginEmail(event.target.value)}
|
||||
placeholder="电子邮箱"
|
||||
type="email"
|
||||
className="h-14 rounded-xl border-black/20 px-4 text-base"
|
||||
/>
|
||||
<Input
|
||||
value={loginPassword}
|
||||
onChange={(event) => setLoginPassword(event.target.value)}
|
||||
placeholder="密码"
|
||||
type="password"
|
||||
className="h-14 rounded-xl border-black/20 px-4 text-base"
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
className="mt-1 h-14 w-full rounded-xl bg-black text-[17px] text-white hover:bg-black/90"
|
||||
disabled={isLoggingIn}
|
||||
>
|
||||
{isLoggingIn ? (
|
||||
<span className="inline-flex items-center gap-2">
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
登录中...
|
||||
</span>
|
||||
) : (
|
||||
"使用邮箱继续"
|
||||
)}
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
<p className="mt-5 text-center text-xs text-black/40">
|
||||
继续即表示您同意使用条款和隐私政策
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<Dialog open={upgradeDialogOpen} onOpenChange={setUpgradeDialogOpen}>
|
||||
<DialogContent className="max-h-[92vh] w-[95vw] max-w-[1240px] overflow-hidden rounded-3xl border-black/10 bg-[#f7f7f7] p-0">
|
||||
<div className="flex h-full max-h-[92vh] flex-col">
|
||||
|
||||
2
lib/db/migrations/0030_add_google_auth_toggle.sql
Normal file
2
lib/db/migrations/0030_add_google_auth_toggle.sql
Normal file
@ -0,0 +1,2 @@
|
||||
ALTER TABLE "AuthSettings"
|
||||
ADD COLUMN IF NOT EXISTS "isGoogleEnabled" boolean NOT NULL DEFAULT false;
|
||||
4
lib/db/migrations/0031_add_google_oauth_settings.sql
Normal file
4
lib/db/migrations/0031_add_google_oauth_settings.sql
Normal file
@ -0,0 +1,4 @@
|
||||
ALTER TABLE "AuthSettings"
|
||||
ADD COLUMN IF NOT EXISTS "googleClientId" text,
|
||||
ADD COLUMN IF NOT EXISTS "googleClientSecret" text,
|
||||
ADD COLUMN IF NOT EXISTS "authUrl" text;
|
||||
@ -113,6 +113,20 @@
|
||||
"when": 1769251200000,
|
||||
"tag": "0029_add_lovart_home_copy_settings",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 16,
|
||||
"version": "7",
|
||||
"when": 1769255400000,
|
||||
"tag": "0030_add_google_auth_toggle",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 17,
|
||||
"version": "7",
|
||||
"when": 1769257800000,
|
||||
"tag": "0031_add_google_oauth_settings",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@ -1605,16 +1605,24 @@ export async function getAuthSettings() {
|
||||
return {
|
||||
dingTalkAppKey: "",
|
||||
dingTalkAppSecret: "",
|
||||
googleClientId: "",
|
||||
googleClientSecret: "",
|
||||
authUrl: process.env.AUTH_URL || "",
|
||||
dingTalkOrganizations: [],
|
||||
isDingTalkEnabled: false,
|
||||
isGoogleEnabled: false,
|
||||
isRegistrationEnabled: true,
|
||||
};
|
||||
} catch (_error) {
|
||||
return {
|
||||
dingTalkAppKey: "",
|
||||
dingTalkAppSecret: "",
|
||||
googleClientId: "",
|
||||
googleClientSecret: "",
|
||||
authUrl: process.env.AUTH_URL || "",
|
||||
dingTalkOrganizations: [],
|
||||
isDingTalkEnabled: false,
|
||||
isGoogleEnabled: false,
|
||||
isRegistrationEnabled: true,
|
||||
};
|
||||
}
|
||||
|
||||
@ -358,11 +358,15 @@ export const authSettings = pgTable("AuthSettings", {
|
||||
id: uuid("id").primaryKey().notNull().defaultRandom(),
|
||||
dingTalkAppKey: text("dingTalkAppKey"),
|
||||
dingTalkAppSecret: text("dingTalkAppSecret"),
|
||||
googleClientId: text("googleClientId"),
|
||||
googleClientSecret: text("googleClientSecret"),
|
||||
authUrl: text("authUrl"),
|
||||
// 多组织钉钉配置,存储为 JSON 数组:[{ name, appKey, appSecret }]
|
||||
dingTalkOrganizations: jsonb("dingTalkOrganizations").$type<
|
||||
{ name: string; appKey: string; appSecret: string }[] | null
|
||||
>(),
|
||||
isDingTalkEnabled: boolean("isDingTalkEnabled").notNull().default(false),
|
||||
isGoogleEnabled: boolean("isGoogleEnabled").notNull().default(false),
|
||||
isRegistrationEnabled: boolean("isRegistrationEnabled").notNull().default(true),
|
||||
updatedAt: timestamp("updatedAt").notNull().defaultNow(),
|
||||
updatedBy: uuid("updatedBy").references(() => user.id),
|
||||
|
||||
14
proxy.ts
14
proxy.ts
@ -69,6 +69,16 @@ export async function proxy(request: NextRequest) {
|
||||
return NextResponse.next();
|
||||
}
|
||||
|
||||
// 允许访问公开的认证配置(登录弹窗需要)
|
||||
if (pathname === "/api/auth-settings") {
|
||||
return NextResponse.next();
|
||||
}
|
||||
|
||||
// 允许匿名访问新版首页灵感数据
|
||||
if (pathname === "/api/featured" || pathname.startsWith("/api/featured/")) {
|
||||
return NextResponse.next();
|
||||
}
|
||||
|
||||
// 允许访问钉钉回调页面(public 文件夹中的文件)
|
||||
if (pathname === "/dingtalk-callback.html") {
|
||||
return NextResponse.next();
|
||||
@ -88,8 +98,8 @@ export async function proxy(request: NextRequest) {
|
||||
process.env.AUTH_URL ??
|
||||
`${request.nextUrl.protocol}//${request.headers.get("host") ?? request.nextUrl.host}`;
|
||||
|
||||
// 未登录用户只能访问 /login 和 /register
|
||||
if (!token && !["/login", "/register"].includes(pathname)) {
|
||||
// 未登录用户允许访问 /(新版首页)、/login、/register,其余需要登录
|
||||
if (!token && !["/", "/login", "/register"].includes(pathname)) {
|
||||
// 对于 API 路由的非 GET 请求,直接返回 401 JSON 响应(不重定向)
|
||||
if (pathname.startsWith("/api/") && request.method !== "GET") {
|
||||
return NextResponse.json(
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user