Compare commits

..

3 Commits
v0.3.4 ... main

Author SHA1 Message Date
iye
9a122ffa27 feat(brand): update CSG logo and site title
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 3m20s
2026-06-02 11:00:14 +08:00
zyc
85cf284848 chore(otp): raise per-IP send-otp limit from 5 to 100 / 5min
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 2m59s
放宽同一出口 IP 5 分钟内可发送的验证码次数,避免办公网 / 校园网 / NAT
下多个真实用户互相挤掉配额。单手机号 60s 限频不变。

注意:当前 REDIS_URL 未配置,限流走进程内 Map,多副本部署时该阈值
按 pod 各自计数,实际放大为 N × 100。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 17:54:56 +08:00
iye
9772ba88ae fix(auth): 僵尸 JWT session 兜底 —— /api/me 返回 NOT_FOUND/UNAUTHORIZED 时自动登出
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 3m54s
NextAuth 用 JWT 策略,cookie 签名不会因 DB user 被删而失效,
导致 dev 清数据后浏览器仍显示"已登录"但拉不到任何数据(假登录)。

useSyncMe 现在识别 /api/me 的 401/NOT_FOUND/UNAUTHORIZED 三种信号,
命中后调用 signOut({ redirect: false }) + reset(),把 UI 切回未登录态。

生产环境不会清 user,主要受益是 dev/staging 重置数据后无需手动清浏览器 cookie。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 17:05:50 +08:00
14 changed files with 65 additions and 27 deletions

BIN
public/logo-v4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

View File

@ -14,7 +14,7 @@ const Body = z.object({
/**
* POST /api/auth/send-otp
* · 60s / IP 5 5
* · 60s / IP 5 100
*/
export async function POST(req: NextRequest) {
try {
@ -31,7 +31,7 @@ export async function POST(req: NextRequest) {
}
const ip = await getClientIp();
if (ip) {
const ipRl = await rateLimit(`otp:ip:${ip}`, 300, 5);
const ipRl = await rateLimit(`otp:ip:${ip}`, 300, 100);
if (!ipRl.allowed) return ERR.RATE_LIMITED();
}

View File

@ -16,10 +16,10 @@ export async function generateMetadata({
}: ArtistPageProps): Promise<Metadata> {
const { id } = await params;
const artist = getArtist(id);
if (!artist) return { title: "艺人不存在 · CYBER STAR" };
if (!artist) return { title: "艺人不存在 · 银河初星计划 C . S . G" };
return {
title: `${artist.name} · ${artist.enName} · CYBER STAR`,
title: `${artist.name} · ${artist.enName} · 银河初星计划 C . S . G`,
description: artist.bio.slice(0, 120),
};
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 81 KiB

View File

@ -33,15 +33,27 @@ const inter = Inter({
});
export const metadata: Metadata = {
title: "CYBER ✦ STAR · 虚拟偶像 Top12 出道企划",
title: "银河初星计划 C . S . G",
description:
"36 位虚拟偶像候选人,由你投票决出最终出道 Top12。Cyber Star · Virtual Idol Debut Project.",
keywords: ["虚拟偶像", "出道", "投票", "Top12", "Cyber Star", "Virtual Idol"],
"36 位虚拟偶像候选人,由你投票决出最终出道 Top12。银河初星计划 C . S . G。",
keywords: [
"银河初星计划",
"C . S . G",
"虚拟偶像",
"出道",
"投票",
"Top12",
"Virtual Idol",
],
openGraph: {
title: "CYBER ✦ STAR",
title: "银河初星计划 C . S . G",
description: "虚拟偶像 Top12 出道企划",
type: "website",
},
icons: {
icon: "/favicon.ico?v=4",
shortcut: "/favicon.ico?v=4",
},
};
export default function RootLayout({

View File

@ -93,8 +93,8 @@ export default function LoginForm() {
{/* Logo */}
<div className="flex flex-col items-center mb-8">
<img
src="/logo-v3.png"
alt="CYBER STAR"
src="/logo-v4.png?v=4"
alt="银河初星计划 C . S . G"
decoding="async"
draggable={false}
className="block select-none h-20 sm:h-24 w-auto"

View File

@ -2,7 +2,7 @@ import { Suspense } from "react";
import LoginForm from "./LoginForm";
export const metadata = {
title: "登录 · CYBER STAR",
title: "登录 · 银河初星计划 C . S . G",
};
export default function LoginPage() {

View File

@ -4,7 +4,7 @@ import { auth } from "@/lib/auth";
import MeContent from "./MeContent";
export const metadata: Metadata = {
title: "个人中心 · CYBER STAR",
title: "个人中心 · 银河初星计划 C . S . G",
};
export default async function MePage() {

View File

@ -4,7 +4,7 @@ export default function Footer() {
<footer className="border-t border-white/[0.06] bg-deep mt-16">
<div className="max-w-[1500px] mx-auto px-6 sm:px-8 h-16 flex items-center justify-center text-center">
<p className="text-[11px] text-white/35 tracking-[0.05em]">
© {year} CYBER STAR · All Rights Reserved
© {year} C . S . G · All Rights Reserved
</p>
</div>
</footer>

View File

@ -90,8 +90,8 @@ export default function HeroBanner({
<div className="absolute inset-0 max-w-[1500px] mx-auto px-4 sm:px-6 lg:px-8">
{/* Eyebrow 左上 · 紧贴导航下方 */}
<div className="absolute top-[6.5rem] sm:top-[7.5rem] left-4 sm:left-6 lg:left-8 z-10">
<p className="font-label text-[10px] sm:text-xs tracking-[0.4em] uppercase text-purple-200/90">
Top 12 · Cyber Star Debut Survival
<p className="font-label text-[10px] sm:text-xs tracking-[0.28em] uppercase text-purple-200/90">
C . S . G
</p>
</div>

View File

@ -8,7 +8,7 @@ interface LogoProps {
className?: string;
}
// 高度由 size 控制,宽度按 logo.png 实际比例(约 5.41:1单行 CYBER STAR + 星环)自适应
// 高度由 size 控制,宽度按 logo-v4.png 实际比例自适应
const HEIGHT_PX: Record<LogoSize, number> = {
sm: 24,
md: 44,
@ -25,11 +25,11 @@ export default function Logo({
// 用原生 <img> 绕开 Next/Image 的格式转换 —— 某些环境下 sharp 把透明 PNG
// 转 webp/avif 时会铺白底,导致 logo 在深色 nav 上出现白色矩形。
// ?v=2 缓存破坏:logo 改版时 +1,浏览器立刻拉新版而不读老缓存。
// ?v=4 缓存破坏:logo 改版时 +1,浏览器立刻拉新版而不读老缓存。
const inner = (
<img
src="/logo.png?v=3"
alt="CYBER STAR"
src="/logo-v4.png?v=4"
alt="银河初星计划 C . S . G"
decoding="async"
draggable={false}
style={{
@ -46,7 +46,7 @@ export default function Logo({
<Link
href={href}
className="inline-flex items-center hover:opacity-90 transition-opacity"
aria-label="CYBER STAR · 首页"
aria-label="银河初星计划 C . S . G · 首页"
style={{ background: "transparent" }}
>
{inner}

View File

@ -2,6 +2,7 @@
import { useEffect, useState } from "react";
import { usePathname } from "next/navigation";
import Logo from "./Logo";
import NavLinks from "./NavLinks";
import SearchTrigger from "./SearchTrigger";
import AuthMenu from "./auth/AuthMenu";
@ -27,6 +28,7 @@ export default function Navigation() {
// nav 关掉自己的玻璃,避免双重 backdrop-filter 在 y=80 处出现拼接线。
const filterStuck = useUIStore((s) => s.filterStuck);
const glassOff = isTransparent || filterStuck;
const showLogo = pathname !== "/";
// 维护一个站内导航计数器(per-tab),供 FloatingBackButton 判断 router.back() 是否安全
useEffect(() => {
@ -81,7 +83,16 @@ export default function Navigation() {
)}
/>
<nav className="relative max-w-[1500px] mx-auto h-20 px-4 sm:px-6 lg:px-8 flex items-center gap-4 sm:gap-8">
{/* 左侧:首页 / 排行榜 / 我的(logo 已移除,所有屏宽都显示在第一行) */}
{showLogo && (
<div className="hidden sm:flex shrink-0 items-center">
<Logo
size="md"
className="drop-shadow-[0_0_14px_rgba(139,92,246,0.55)]"
/>
</div>
)}
{/* 左侧:首页 / 排行榜 / 我的 */}
<NavLinks />
{/* 右侧:搜索 + 今日余票 + 登录/注册 (或 头像+下拉) */}

View File

@ -173,8 +173,8 @@ export default function LoginModal({ open, onClose, onSuccess }: LoginModalProps
<div className="flex flex-col items-center mb-6">
<img
src="/logo-v3.png"
alt="CYBER STAR"
src="/logo-v4.png?v=4"
alt="银河初星计划 C . S . G"
decoding="async"
draggable={false}
className="block select-none h-16 sm:h-20 w-auto"

View File

@ -1,7 +1,7 @@
"use client";
import { useEffect, useRef } from "react";
import { useSession } from "next-auth/react";
import { useSession, signOut } from "next-auth/react";
import { useVoteStore } from "@/lib/store";
/**
@ -11,6 +11,10 @@ import { useVoteStore } from "@/lib/store";
* - status === "unauthenticated" ()
* - (uid )
*
* session 兜底:NextAuth JWT ,cookie DB user
* /api/me 401() NOT_FOUND(DB user ) ,
* signOut() cookie "假登录"()
*
* localStorage ,
*/
export function useSyncMe() {
@ -45,12 +49,23 @@ export function useSyncMe() {
credentials: "include",
signal: ctrl.signal,
})
.then((r) => r.json())
.then((res) => {
.then(async (r) => {
if (cancelled) return;
if (res?.ok && Array.isArray(res.data?.votedArtists)) {
const res = await r.json().catch(() => null);
if (r.ok && res?.ok && Array.isArray(res.data?.votedArtists)) {
hydrateFromServer(res.data.votedArtists as string[]);
lastSyncedUidRef.current = uid;
return;
}
// 僵尸 session:JWT 还有效,但 DB 里 user 已不存在(或鉴权失效)。
// 直接登出清 cookie,UI 状态切换为未登录,避免"显示已登录但拉不到数据"。
const code: string | undefined = res?.error?.code;
if (r.status === 401 || code === "UNAUTHORIZED" || code === "NOT_FOUND") {
signOut({ redirect: false });
reset();
lastSyncedUidRef.current = null;
}
})
.catch(() => {