mirror of
https://github.com/KuekHaoYang/KVideo.git
synced 2026-08-17 01:33:43 +08:00
feat: Implement new authentication and account management system, refactoring password gates and settings components.
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { getSession, clearSession } from '@/lib/store/auth-store';
|
||||
import { SettingsSection } from './SettingsSection';
|
||||
import { LogOut, Shield, Info } from 'lucide-react';
|
||||
|
||||
export function AccountSettings() {
|
||||
const [session, setSessionState] = useState<ReturnType<typeof getSession>>(null);
|
||||
const [hasAuth, setHasAuth] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setSessionState(getSession());
|
||||
|
||||
fetch('/api/auth')
|
||||
.then(res => res.json())
|
||||
.then(data => setHasAuth(data.hasAuth))
|
||||
.catch(() => {});
|
||||
}, []);
|
||||
|
||||
const handleLogout = () => {
|
||||
clearSession();
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
if (!hasAuth && !session) return null;
|
||||
|
||||
return (
|
||||
<SettingsSection title="账户管理" description="查看当前登录用户信息和账户配置。">
|
||||
<div className="space-y-6">
|
||||
{/* Current User Info */}
|
||||
{session && (
|
||||
<div className="flex items-center justify-between p-4 bg-[var(--glass-bg)] border border-[var(--glass-border)] rounded-[var(--radius-2xl)]">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-[var(--radius-full)] bg-[var(--accent-color)]/10 flex items-center justify-center text-[var(--accent-color)] font-bold text-lg border border-[var(--glass-border)]">
|
||||
{session.name.charAt(0)}
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-[var(--text-color)]">{session.name}</p>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Shield size={12} className={session.role === 'admin' ? 'text-[var(--accent-color)]' : 'text-[var(--text-color-secondary)]'} />
|
||||
<span className="text-xs text-[var(--text-color-secondary)]">
|
||||
{session.role === 'admin' ? '管理员' : '观众'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="flex items-center gap-2 px-3 py-1.5 text-sm bg-[var(--glass-bg)] border border-[var(--glass-border)] rounded-[var(--radius-full)] text-[var(--text-color-secondary)] hover:text-red-500 hover:border-red-500/30 transition-all duration-200 cursor-pointer"
|
||||
>
|
||||
<LogOut size={14} />
|
||||
退出登录
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Config Notice */}
|
||||
<div className="flex items-start gap-3 p-4 bg-[color-mix(in_srgb,var(--accent-color)_5%,transparent)] border border-[var(--glass-border)] rounded-[var(--radius-2xl)]">
|
||||
<Info className="text-[var(--text-color-secondary)] shrink-0 mt-0.5" size={16} />
|
||||
<div className="space-y-1">
|
||||
<p className="text-xs text-[var(--text-color-secondary)]">
|
||||
账户通过环境变量配置:
|
||||
</p>
|
||||
<div className="text-xs text-[var(--text-color-secondary)] space-y-0.5">
|
||||
<p><code className="px-1 py-0.5 bg-[var(--glass-bg)] rounded text-[10px]">ADMIN_PASSWORD</code> — 单管理员密码</p>
|
||||
<p><code className="px-1 py-0.5 bg-[var(--glass-bg)] rounded text-[10px]">ACCOUNTS</code> — 多账户(密码:名称[:角色])</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SettingsSection>
|
||||
);
|
||||
}
|
||||
@@ -1,158 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { SettingsSection } from './SettingsSection';
|
||||
import { Trash2, Plus, Eye, EyeOff, Shield, ShieldCheck } from 'lucide-react';
|
||||
import { Switch } from '@/components/ui/Switch';
|
||||
|
||||
interface PasswordSettingsProps {
|
||||
enabled: boolean;
|
||||
passwords: string[];
|
||||
envPasswordSet: boolean;
|
||||
onToggle: (enabled: boolean) => void;
|
||||
onAdd: (password: string) => void;
|
||||
onRemove: (password: string) => void;
|
||||
}
|
||||
|
||||
export function PasswordSettings({
|
||||
enabled,
|
||||
passwords,
|
||||
envPasswordSet,
|
||||
onToggle,
|
||||
onAdd,
|
||||
onRemove,
|
||||
}: PasswordSettingsProps) {
|
||||
const [newPassword, setNewPassword] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
|
||||
const handleAdd = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!newPassword) return;
|
||||
|
||||
if (passwords.includes(newPassword)) {
|
||||
setError('密码已存在');
|
||||
return;
|
||||
}
|
||||
|
||||
onAdd(newPassword);
|
||||
setNewPassword('');
|
||||
setError('');
|
||||
};
|
||||
|
||||
// If env password is set, access control is automatically enabled
|
||||
const isActive = enabled || envPasswordSet;
|
||||
|
||||
return (
|
||||
<SettingsSection title="访问控制" description="为应用启用密码保护功能。">
|
||||
<div className="space-y-6">
|
||||
{/* Toggle - only shown if no env password */}
|
||||
{!envPasswordSet && (
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="text-sm font-medium text-[var(--text-color)]">
|
||||
启用密码访问
|
||||
</label>
|
||||
<Switch
|
||||
checked={enabled}
|
||||
onChange={onToggle}
|
||||
ariaLabel="启用密码访问开关"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Env Password Notice */}
|
||||
{envPasswordSet && (
|
||||
<div className="flex items-center gap-3 p-4 bg-[color-mix(in_srgb,var(--accent-color)_10%,transparent)] border border-[var(--accent-color)]/30 rounded-[var(--radius-2xl)]">
|
||||
<ShieldCheck className="text-[var(--accent-color)] shrink-0" size={24} />
|
||||
<div>
|
||||
<p className="text-sm font-medium text-[var(--text-color)]">
|
||||
环境变量密码已启用
|
||||
</p>
|
||||
<p className="text-xs text-[var(--text-color-secondary)]">
|
||||
通过 <code className="px-1 py-0.5 bg-[var(--glass-bg)] rounded">ACCESS_PASSWORD</code> 环境变量设置,无法在此删除
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isActive && (
|
||||
<div className="space-y-4 pt-4 border-t border-[var(--glass-border)] animate-in fade-in slide-in-from-top-2">
|
||||
{/* Local Passwords Section */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Shield size={16} className="text-[var(--text-color-secondary)]" />
|
||||
<h4 className="text-sm font-medium text-[var(--text-color)]">本地保存密码</h4>
|
||||
</div>
|
||||
<p className="text-xs text-[var(--text-color-secondary)]">
|
||||
仅在当前浏览器/设备有效,可随时添加或删除
|
||||
</p>
|
||||
|
||||
{passwords.length === 0 && !envPasswordSet && (
|
||||
<p className="text-sm text-[var(--text-color-secondary)] italic">
|
||||
未设置本地密码。在至少添加一个密码之前,任何人都可以访问。
|
||||
</p>
|
||||
)}
|
||||
|
||||
{passwords.length === 0 && envPasswordSet && (
|
||||
<p className="text-sm text-[var(--text-color-secondary)] italic">
|
||||
暂无本地密码。可以添加额外的本地密码作为备用。
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{passwords.map((pwd, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex items-center gap-2 px-3 py-1.5 bg-[var(--glass-bg)] border border-[var(--glass-border)] rounded-[var(--radius-full)] text-sm transition-all duration-300 hover:scale-105"
|
||||
>
|
||||
<span className="font-mono">{showPassword ? pwd : '••••••'}</span>
|
||||
<button
|
||||
onClick={() => onRemove(pwd)}
|
||||
className="text-[var(--text-color-secondary)] hover:text-red-500 transition-colors cursor-pointer"
|
||||
title="删除密码"
|
||||
>
|
||||
<Trash2 size={14} />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleAdd} className="flex gap-2 items-start">
|
||||
<div className="flex-1 space-y-1">
|
||||
<div className="relative">
|
||||
<input
|
||||
type={showPassword ? "text" : "password"}
|
||||
value={newPassword}
|
||||
onChange={(e) => {
|
||||
setNewPassword(e.target.value);
|
||||
setError('');
|
||||
}}
|
||||
placeholder="添加新的本地密码..."
|
||||
className="w-full px-4 py-2 pr-10 rounded-[var(--radius-2xl)] bg-[var(--glass-bg)] border border-[var(--glass-border)] focus:outline-none focus:border-[var(--accent-color)] focus:shadow-[0_0_0_3px_color-mix(in_srgb,var(--accent-color)_30%,transparent)] transition-all duration-[0.4s] cubic-bezier(0.2,0.8,0.2,1) text-sm"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-[var(--text-color-secondary)] hover:text-[var(--text-color)] transition-colors cursor-pointer"
|
||||
>
|
||||
{showPassword ? <EyeOff size={16} /> : <Eye size={16} />}
|
||||
</button>
|
||||
</div>
|
||||
{error && <p className="text-xs text-red-500 pl-2">{error}</p>}
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!newPassword}
|
||||
className="p-2 bg-[var(--accent-color)] text-white rounded-[var(--radius-2xl)] hover:translate-y-[-2px] hover:brightness-110 shadow-[var(--shadow-sm)] hover:shadow-[0_4px_8px_var(--shadow-color)] disabled:opacity-50 disabled:cursor-not-allowed disabled:transform-none disabled:shadow-none transition-all duration-200 cursor-pointer"
|
||||
>
|
||||
<Plus size={20} />
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</SettingsSection>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,156 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { SettingsSection } from './SettingsSection';
|
||||
import { Trash2, Plus, Eye, EyeOff, Shield, ShieldCheck } from 'lucide-react';
|
||||
import { Switch } from '@/components/ui/Switch';
|
||||
|
||||
interface SettingsPasswordSettingsProps {
|
||||
enabled: boolean;
|
||||
passwords: string[];
|
||||
envSettingsPasswordSet: boolean;
|
||||
onToggle: (enabled: boolean) => void;
|
||||
onAdd: (password: string) => void;
|
||||
onRemove: (password: string) => void;
|
||||
}
|
||||
|
||||
export function SettingsPasswordSettings({
|
||||
enabled,
|
||||
passwords,
|
||||
envSettingsPasswordSet,
|
||||
onToggle,
|
||||
onAdd,
|
||||
onRemove,
|
||||
}: SettingsPasswordSettingsProps) {
|
||||
const [newPassword, setNewPassword] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
|
||||
const handleAdd = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!newPassword) return;
|
||||
|
||||
if (passwords.includes(newPassword)) {
|
||||
setError('密码已存在');
|
||||
return;
|
||||
}
|
||||
|
||||
onAdd(newPassword);
|
||||
setNewPassword('');
|
||||
setError('');
|
||||
};
|
||||
|
||||
const isActive = enabled || envSettingsPasswordSet;
|
||||
|
||||
return (
|
||||
<SettingsSection title="设置页密码保护" description="为设置页面单独设置密码保护,防止他人修改应用配置。">
|
||||
<div className="space-y-6">
|
||||
{/* Toggle - only shown if no env password */}
|
||||
{!envSettingsPasswordSet && (
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="text-sm font-medium text-[var(--text-color)]">
|
||||
启用设置页密码
|
||||
</label>
|
||||
<Switch
|
||||
checked={enabled}
|
||||
onChange={onToggle}
|
||||
ariaLabel="启用设置页密码开关"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Env Password Notice */}
|
||||
{envSettingsPasswordSet && (
|
||||
<div className="flex items-center gap-3 p-4 bg-[color-mix(in_srgb,var(--accent-color)_10%,transparent)] border border-[var(--accent-color)]/30 rounded-[var(--radius-2xl)]">
|
||||
<ShieldCheck className="text-[var(--accent-color)] shrink-0" size={24} />
|
||||
<div>
|
||||
<p className="text-sm font-medium text-[var(--text-color)]">
|
||||
环境变量密码已启用
|
||||
</p>
|
||||
<p className="text-xs text-[var(--text-color-secondary)]">
|
||||
通过 <code className="px-1 py-0.5 bg-[var(--glass-bg)] rounded">SETTINGS_PASSWORD</code> 环境变量设置,无法在此删除
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isActive && (
|
||||
<div className="space-y-4 pt-4 border-t border-[var(--glass-border)] animate-in fade-in slide-in-from-top-2">
|
||||
{/* Local Passwords Section */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Shield size={16} className="text-[var(--text-color-secondary)]" />
|
||||
<h4 className="text-sm font-medium text-[var(--text-color)]">本地保存密码</h4>
|
||||
</div>
|
||||
<p className="text-xs text-[var(--text-color-secondary)]">
|
||||
仅在当前浏览器/设备有效,可随时添加或删除
|
||||
</p>
|
||||
|
||||
{passwords.length === 0 && !envSettingsPasswordSet && (
|
||||
<p className="text-sm text-[var(--text-color-secondary)] italic">
|
||||
未设置本地密码。在至少添加一个密码之前,任何人都可以访问设置。
|
||||
</p>
|
||||
)}
|
||||
|
||||
{passwords.length === 0 && envSettingsPasswordSet && (
|
||||
<p className="text-sm text-[var(--text-color-secondary)] italic">
|
||||
暂无本地密码。可以添加额外的本地密码作为备用。
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{passwords.map((pwd, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex items-center gap-2 px-3 py-1.5 bg-[var(--glass-bg)] border border-[var(--glass-border)] rounded-[var(--radius-full)] text-sm transition-all duration-300 hover:scale-105"
|
||||
>
|
||||
<span className="font-mono">{showPassword ? pwd : '••••••'}</span>
|
||||
<button
|
||||
onClick={() => onRemove(pwd)}
|
||||
className="text-[var(--text-color-secondary)] hover:text-red-500 transition-colors cursor-pointer"
|
||||
title="删除密码"
|
||||
>
|
||||
<Trash2 size={14} />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleAdd} className="flex gap-2 items-start">
|
||||
<div className="flex-1 space-y-1">
|
||||
<div className="relative">
|
||||
<input
|
||||
type={showPassword ? "text" : "password"}
|
||||
value={newPassword}
|
||||
onChange={(e) => {
|
||||
setNewPassword(e.target.value);
|
||||
setError('');
|
||||
}}
|
||||
placeholder="添加新的设置密码..."
|
||||
className="w-full px-4 py-2 pr-10 rounded-[var(--radius-2xl)] bg-[var(--glass-bg)] border border-[var(--glass-border)] focus:outline-none focus:border-[var(--accent-color)] focus:shadow-[0_0_0_3px_color-mix(in_srgb,var(--accent-color)_30%,transparent)] transition-all duration-[0.4s] cubic-bezier(0.2,0.8,0.2,1) text-sm"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-[var(--text-color-secondary)] hover:text-[var(--text-color)] transition-colors cursor-pointer"
|
||||
>
|
||||
{showPassword ? <EyeOff size={16} /> : <Eye size={16} />}
|
||||
</button>
|
||||
</div>
|
||||
{error && <p className="text-xs text-red-500 pl-2">{error}</p>}
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!newPassword}
|
||||
className="p-2 bg-[var(--accent-color)] text-white rounded-[var(--radius-2xl)] hover:translate-y-[-2px] hover:brightness-110 shadow-[var(--shadow-sm)] hover:shadow-[0_4px_8px_var(--shadow-color)] disabled:opacity-50 disabled:cursor-not-allowed disabled:transform-none disabled:shadow-none transition-all duration-200 cursor-pointer"
|
||||
>
|
||||
<Plus size={20} />
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</SettingsSection>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user