fix(nav,auth): trim nav to wireframe pages; auth gracefully degrades when DB unavailable in dev
This commit is contained in:
parent
854a162109
commit
7949f9bcd1
@ -1,55 +1,21 @@
|
|||||||
import Link from "next/link";
|
|
||||||
import Logo from "./Logo";
|
import Logo from "./Logo";
|
||||||
|
|
||||||
const FOOTER_LINKS = [
|
|
||||||
{ label: "活动规则", href: "/rules" },
|
|
||||||
{ label: "隐私协议", href: "/privacy" },
|
|
||||||
{ label: "用户协议", href: "/terms" },
|
|
||||||
{ label: "联系客服", href: "/support" },
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
export default function Footer() {
|
export default function Footer() {
|
||||||
return (
|
return (
|
||||||
<footer className="border-t border-white/[0.06] bg-[rgba(8,5,26,0.6)] backdrop-blur-sm mt-20">
|
<footer className="border-t border-white/[0.06] bg-[rgba(8,5,26,0.6)] backdrop-blur-sm mt-20">
|
||||||
<div className="max-w-7xl mx-auto px-6 sm:px-8 py-10 grid gap-8 md:grid-cols-3 items-start">
|
<div className="max-w-7xl mx-auto px-6 sm:px-8 py-8 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||||
{/* Brand */}
|
|
||||||
<div>
|
<div>
|
||||||
<Logo size="sm" href={null} />
|
<Logo size="sm" href={null} />
|
||||||
<p className="mt-3 text-xs text-white/45 leading-relaxed">
|
<p className="mt-2 text-xs text-white/45 leading-relaxed">
|
||||||
虚拟偶像 Top12 出道企划
|
虚拟偶像 Top12 出道企划 · Cyber Star · Virtual Idol Debut Project
|
||||||
<br />
|
|
||||||
Cyber Star · Virtual Idol Debut Project
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Links */}
|
<div className="text-xs text-white/40 sm:text-right">
|
||||||
<nav className="md:col-span-1">
|
<p className="font-label tracking-[0.25em] uppercase text-purple-300/70 mb-1">
|
||||||
<p className="font-label text-[10px] tracking-[0.3em] uppercase text-purple-300 mb-3">
|
|
||||||
Information
|
|
||||||
</p>
|
|
||||||
<ul className="space-y-2 text-sm">
|
|
||||||
{FOOTER_LINKS.map((link) => (
|
|
||||||
<li key={link.href}>
|
|
||||||
<Link
|
|
||||||
href={link.href}
|
|
||||||
className="text-white/55 hover:text-purple-300 transition-colors"
|
|
||||||
>
|
|
||||||
{link.label}
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
{/* Caption */}
|
|
||||||
<div className="text-xs text-white/40 md:text-right">
|
|
||||||
<p className="font-label tracking-[0.25em] uppercase text-purple-300/70 mb-2">
|
|
||||||
© 2026 Cyber Star
|
© 2026 Cyber Star
|
||||||
</p>
|
</p>
|
||||||
<p>All rights reserved.</p>
|
<p className="font-mono text-white/30">airlabs.art</p>
|
||||||
<p className="mt-1 font-mono text-white/30">
|
|
||||||
airlabs.art · powered by Next.js
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|||||||
@ -6,10 +6,8 @@ import { cn } from "@/lib/cn";
|
|||||||
|
|
||||||
const NAV_ITEMS = [
|
const NAV_ITEMS = [
|
||||||
{ label: "HOME", href: "/" },
|
{ label: "HOME", href: "/" },
|
||||||
{ label: "VOTE", href: "/vote" },
|
|
||||||
{ label: "RANKING", href: "/ranking" },
|
{ label: "RANKING", href: "/ranking" },
|
||||||
{ label: "NEWS", href: "/news" },
|
{ label: "ME", href: "/me" },
|
||||||
{ label: "ABOUT", href: "/about" },
|
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
interface NavLinksProps {
|
interface NavLinksProps {
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import NextAuth, { type NextAuthConfig } from "next-auth";
|
import NextAuth, { type NextAuthConfig } from "next-auth";
|
||||||
import Credentials from "next-auth/providers/credentials";
|
import Credentials from "next-auth/providers/credentials";
|
||||||
import { PrismaAdapter } from "@auth/prisma-adapter";
|
|
||||||
import { prisma } from "./prisma";
|
import { prisma } from "./prisma";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
@ -9,11 +8,11 @@ import { z } from "zod";
|
|||||||
*
|
*
|
||||||
* 支持渠道(按优先级):
|
* 支持渠道(按优先级):
|
||||||
* 1. 手机号 + 短信 OTP(国内主推)
|
* 1. 手机号 + 短信 OTP(国内主推)
|
||||||
* 2. 微信扫码(待团队配置 appId/secret 后开启)
|
* 2. 微信扫码(待团队配置 appId/secret 后开启 · 启用时需把 PrismaAdapter 加回来)
|
||||||
* 3. 邮箱(境外用户备用)
|
* 3. 邮箱(境外用户备用)
|
||||||
*
|
*
|
||||||
* 当前阶段:手机号 OTP 已接入,OTP 校验逻辑使用 Redis 缓存验证码
|
* 当前阶段:使用 JWT session 策略,Credentials 不需要 Adapter。
|
||||||
* (Phase 11 完整版需团队配置短信服务,本文件已留好接入位)。
|
* 数据库不可用时自动降级为内存用户(开发态友好)。
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const OtpCredentials = z.object({
|
const OtpCredentials = z.object({
|
||||||
@ -24,7 +23,8 @@ const OtpCredentials = z.object({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const authConfig: NextAuthConfig = {
|
export const authConfig: NextAuthConfig = {
|
||||||
adapter: PrismaAdapter(prisma),
|
// NOTE: Credentials + JWT 策略不需要 adapter。
|
||||||
|
// 启用微信 OAuth 时再把 PrismaAdapter 加回来。
|
||||||
session: { strategy: "jwt" },
|
session: { strategy: "jwt" },
|
||||||
pages: {
|
pages: {
|
||||||
signIn: "/login",
|
signIn: "/login",
|
||||||
@ -42,14 +42,11 @@ export const authConfig: NextAuthConfig = {
|
|||||||
if (!parsed.success) return null;
|
if (!parsed.success) return null;
|
||||||
const { phone, code } = parsed.data;
|
const { phone, code } = parsed.data;
|
||||||
|
|
||||||
// TODO[团队]: 接入真实 OTP 校验
|
|
||||||
// 1) 从 Redis 取 sms:otp:${phone} 比对 code
|
|
||||||
// 2) 校验失败 / 过期 → return null
|
|
||||||
// 3) 成功 → 删除验证码,继续创建/查询用户
|
|
||||||
const validOtp = await verifyOtp(phone, code);
|
const validOtp = await verifyOtp(phone, code);
|
||||||
if (!validOtp) return null;
|
if (!validOtp) return null;
|
||||||
|
|
||||||
// 查询或创建用户
|
// 尝试持久化到数据库;失败则降级为内存身份(开发态)
|
||||||
|
try {
|
||||||
const user = await prisma.user.upsert({
|
const user = await prisma.user.upsert({
|
||||||
where: { phone },
|
where: { phone },
|
||||||
create: {
|
create: {
|
||||||
@ -59,32 +56,30 @@ export const authConfig: NextAuthConfig = {
|
|||||||
},
|
},
|
||||||
update: { lastLoginAt: new Date() },
|
update: { lastLoginAt: new Date() },
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: String(user.id),
|
id: String(user.id),
|
||||||
name: user.nickname,
|
name: user.nickname,
|
||||||
image: user.avatar ?? undefined,
|
image: user.avatar ?? undefined,
|
||||||
};
|
};
|
||||||
|
} catch (err) {
|
||||||
|
if (process.env.NODE_ENV === "production") {
|
||||||
|
console.error("[auth] DB upsert failed:", err);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// 开发态:用手机号末 9 位做确定性 ID,便于跨重启保持身份
|
||||||
|
const fakeId = String(parseInt(phone.slice(-9), 10));
|
||||||
|
console.warn(
|
||||||
|
`[auth] DB 不可用,开发态降级身份 id=${fakeId} phone=${phone}`,
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
id: fakeId,
|
||||||
|
name: `粉丝_${phone.slice(-4)}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// TODO[团队]: 启用微信扫码登录
|
// TODO[团队]: 启用微信扫码登录(需配置 WECHAT_APP_ID/SECRET 并加回 PrismaAdapter)
|
||||||
// 需要 WECHAT_APP_ID / WECHAT_APP_SECRET 环境变量。
|
|
||||||
// 实现可参考 https://authjs.dev/guides/configuring-oauth-providers
|
|
||||||
//
|
|
||||||
// {
|
|
||||||
// id: "wechat",
|
|
||||||
// name: "WeChat",
|
|
||||||
// type: "oauth",
|
|
||||||
// authorization: { url: "https://open.weixin.qq.com/connect/qrconnect", params: { scope: "snsapi_login" } },
|
|
||||||
// token: "https://api.weixin.qq.com/sns/oauth2/access_token",
|
|
||||||
// userinfo: "https://api.weixin.qq.com/sns/userinfo",
|
|
||||||
// clientId: process.env.WECHAT_APP_ID,
|
|
||||||
// clientSecret: process.env.WECHAT_APP_SECRET,
|
|
||||||
// profile(p) {
|
|
||||||
// return { id: p.openid, name: p.nickname, image: p.headimgurl };
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
],
|
],
|
||||||
callbacks: {
|
callbacks: {
|
||||||
async jwt({ token, user }) {
|
async jwt({ token, user }) {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user