fix(saas): P0 安全修复 + P1 功能补全 — 角色提升、Admin 引导、IP 记录、密码修改
P0 安全修复: - 修复 account update 自角色提升漏洞: 非 admin 用户更新自己时剥离 role 字段 - 添加 Admin 引导机制: accounts 表为空时自动从环境变量创建 super_admin P1 功能补全: - 所有 17 个 log_operation 调用点传入真实客户端 IP (ConnectInfo + X-Forwarded-For) - AuthContext 新增 client_ip 字段, middleware 层自动提取 - main.rs 使用 into_make_service_with_connect_info 启用 SocketAddr 注入 - 新增 PUT /api/v1/auth/password 密码修改端点 (验证旧密码 + argon2 哈希) - 桌面端 SaaS 设置页添加密码修改 UI (折叠式表单) - SaaSClient 添加 changePassword() 方法 - 集成测试修复: 注入模拟 ConnectInfo 适配 onshot 测试模式
This commit is contained in:
@@ -2,7 +2,8 @@ import { useState } from 'react';
|
||||
import { useSaaSStore } from '../../store/saasStore';
|
||||
import { SaaSLogin } from './SaaSLogin';
|
||||
import { SaaSStatus } from './SaaSStatus';
|
||||
import { Cloud, Info } from 'lucide-react';
|
||||
import { Cloud, Info, KeyRound } from 'lucide-react';
|
||||
import { saasClient } from '../../lib/saas-client';
|
||||
|
||||
export function SaaSSettings() {
|
||||
const isLoggedIn = useSaaSStore((s) => s.isLoggedIn);
|
||||
@@ -125,6 +126,9 @@ export function SaaSSettings() {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Password change section */}
|
||||
{isLoggedIn && !showLogin && <ChangePasswordSection />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -156,3 +160,121 @@ function CloudFeatureRow({
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ChangePasswordSection() {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [oldPassword, setOldPassword] = useState('');
|
||||
const [newPassword, setNewPassword] = useState('');
|
||||
const [confirmPassword, setConfirmPassword] = useState('');
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [success, setSuccess] = useState(false);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setError(null);
|
||||
setSuccess(false);
|
||||
|
||||
if (newPassword.length < 8) {
|
||||
setError('新密码至少 8 个字符');
|
||||
return;
|
||||
}
|
||||
if (newPassword !== confirmPassword) {
|
||||
setError('两次输入的新密码不一致');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
await saasClient.changePassword(oldPassword, newPassword);
|
||||
setSuccess(true);
|
||||
setOldPassword('');
|
||||
setNewPassword('');
|
||||
setConfirmPassword('');
|
||||
} catch (err: unknown) {
|
||||
const message = err instanceof Error ? err.message : '密码修改失败';
|
||||
setError(message);
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mt-6">
|
||||
<div
|
||||
className="flex items-center justify-between cursor-pointer"
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
>
|
||||
<h2 className="text-sm font-medium text-gray-500 uppercase tracking-wide">
|
||||
账号安全
|
||||
</h2>
|
||||
<span className="text-xs text-gray-400">{isOpen ? '收起' : '展开'}</span>
|
||||
</div>
|
||||
|
||||
{isOpen && (
|
||||
<div className="bg-white rounded-xl border border-gray-200 p-5 shadow-sm mt-3">
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<KeyRound className="w-4 h-4 text-gray-400" />
|
||||
<span className="text-sm font-medium text-gray-700">修改密码</span>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-3">
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-500 mb-1">
|
||||
当前密码
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
value={oldPassword}
|
||||
onChange={(e) => setOldPassword(e.target.value)}
|
||||
required
|
||||
className="w-full px-3 py-2 text-sm border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:border-transparent"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-500 mb-1">
|
||||
新密码
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
value={newPassword}
|
||||
onChange={(e) => setNewPassword(e.target.value)}
|
||||
required
|
||||
minLength={8}
|
||||
className="w-full px-3 py-2 text-sm border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:border-transparent"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-500 mb-1">
|
||||
确认新密码
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
value={confirmPassword}
|
||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||
required
|
||||
minLength={8}
|
||||
className="w-full px-3 py-2 text-sm border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:border-transparent"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<p className="text-xs text-red-500">{error}</p>
|
||||
)}
|
||||
{success && (
|
||||
<p className="text-xs text-emerald-600">密码修改成功</p>
|
||||
)}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
className="w-full py-2 text-sm font-medium text-white bg-emerald-600 rounded-lg hover:bg-emerald-700 disabled:opacity-50 transition-colors"
|
||||
>
|
||||
{isSubmitting ? '修改中...' : '修改密码'}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user