feat: 添加用户注册登录功能,实现数据和配置的用户隔离
- 新增 users 和 user_settings 表,repositories/analyses/review_notes 添加 user_id 列 - 实现基于邮箱+密码的注册登录,密码使用 bcrypt 哈希 - 使用 gin-contrib/sessions cookie-based session 管理 - 所有仓库、分析记录、review notes 按用户隔离 - 用户设置(LLM 配置、review 参数、缓存配置)独立存储 - 新增登录/注册页面,导航栏显示用户邮箱和退出按钮 - 前端 fetch 请求统一添加 credentials: 'same-origin' - 支持 SESSION_SECRET 环境变量配置会话密钥
This commit is contained in:
@@ -60,6 +60,14 @@
|
||||
<a href="/" class="text-sm text-gray-600 hover:text-gray-900">首页</a>
|
||||
<a href="/settings" class="text-sm text-gray-600 hover:text-gray-900">设置</a>
|
||||
</div>
|
||||
<div class="flex items-center space-x-4">
|
||||
{{if .User}}
|
||||
<span class="text-sm text-gray-500">{{.User.Email}}</span>
|
||||
<form method="POST" action="/api/auth/logout" class="inline">
|
||||
<button type="submit" class="text-sm text-gray-500 hover:text-gray-700">退出</button>
|
||||
</form>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
@@ -91,14 +91,14 @@
|
||||
// Load refs on page load
|
||||
async function loadRefs() {
|
||||
try {
|
||||
const resp = await fetch(`/api/repos/${repoId}/refs`);
|
||||
const resp = await fetch(`/api/repos/${repoId}/refs`, { credentials: 'same-origin' });
|
||||
if (!resp.ok) throw new Error('Failed to load refs');
|
||||
const refs = await resp.json();
|
||||
|
||||
// Fetch recent commits for the "Recent Commits" group
|
||||
let commits = [];
|
||||
try {
|
||||
const graphResp = await fetch(`/api/repos/${repoId}/graph?max_commits=20`);
|
||||
const graphResp = await fetch(`/api/repos/${repoId}/graph?max_commits=20`, { credentials: 'same-origin' });
|
||||
if (graphResp.ok) {
|
||||
const gd = await graphResp.json();
|
||||
commits = gd.commits || [];
|
||||
|
||||
@@ -88,6 +88,7 @@ document.getElementById('clone-form').addEventListener('submit', async function(
|
||||
const resp = await fetch('/api/repos', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
credentials: 'same-origin',
|
||||
body: JSON.stringify({url: url})
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN" class="h-full">
|
||||
{{template "head" .}}
|
||||
<body class="h-full bg-gray-50 text-gray-900">
|
||||
<div class="min-h-full flex flex-col">
|
||||
{{template "nav" .}}
|
||||
<main class="flex-1 flex items-center justify-center">
|
||||
<div class="w-full max-w-md px-4">
|
||||
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-8">
|
||||
<h1 class="text-2xl font-bold text-center mb-6">登录</h1>
|
||||
<form id="login-form" class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">邮箱</label>
|
||||
<input type="email" name="email" required
|
||||
class="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
||||
placeholder="your@email.com">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">密码</label>
|
||||
<input type="password" name="password" required
|
||||
class="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
||||
placeholder="••••••">
|
||||
</div>
|
||||
<div id="login-error" class="text-sm text-red-600 hidden"></div>
|
||||
<button type="submit"
|
||||
class="w-full bg-indigo-600 text-white px-5 py-2 rounded-md text-sm font-medium hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2">
|
||||
登录
|
||||
</button>
|
||||
</form>
|
||||
<p class="mt-4 text-center text-sm text-gray-500">
|
||||
还没有账号?<a href="/register" class="text-indigo-600 hover:text-indigo-700">注册</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
{{template "footer" .}}
|
||||
</div>
|
||||
<script>
|
||||
document.getElementById('login-form').addEventListener('submit', async function(e) {
|
||||
e.preventDefault();
|
||||
const form = new FormData(this);
|
||||
const errorEl = document.getElementById('login-error');
|
||||
errorEl.classList.add('hidden');
|
||||
try {
|
||||
const resp = await fetch('/api/auth/login', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
credentials: 'same-origin',
|
||||
body: JSON.stringify({
|
||||
email: form.get('email'),
|
||||
password: form.get('password')
|
||||
})
|
||||
});
|
||||
const data = await resp.json();
|
||||
if (!resp.ok) {
|
||||
errorEl.textContent = data.error || '登录失败';
|
||||
errorEl.classList.remove('hidden');
|
||||
return;
|
||||
}
|
||||
window.location.href = '/';
|
||||
} catch (err) {
|
||||
errorEl.textContent = '网络错误,请重试';
|
||||
errorEl.classList.remove('hidden');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,83 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN" class="h-full">
|
||||
{{template "head" .}}
|
||||
<body class="h-full bg-gray-50 text-gray-900">
|
||||
<div class="min-h-full flex flex-col">
|
||||
{{template "nav" .}}
|
||||
<main class="flex-1 flex items-center justify-center">
|
||||
<div class="w-full max-w-md px-4">
|
||||
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-8">
|
||||
<h1 class="text-2xl font-bold text-center mb-6">注册</h1>
|
||||
<form id="register-form" class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">邮箱</label>
|
||||
<input type="email" name="email" required
|
||||
class="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
||||
placeholder="your@email.com">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">密码</label>
|
||||
<input type="password" name="password" required minlength="6"
|
||||
class="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
||||
placeholder="至少 6 位">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">确认密码</label>
|
||||
<input type="password" name="confirm_password" required
|
||||
class="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
||||
placeholder="再次输入密码">
|
||||
</div>
|
||||
<div id="register-error" class="text-sm text-red-600 hidden"></div>
|
||||
<button type="submit"
|
||||
class="w-full bg-indigo-600 text-white px-5 py-2 rounded-md text-sm font-medium hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2">
|
||||
注册
|
||||
</button>
|
||||
</form>
|
||||
<p class="mt-4 text-center text-sm text-gray-500">
|
||||
已有账号?<a href="/login" class="text-indigo-600 hover:text-indigo-700">登录</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
{{template "footer" .}}
|
||||
</div>
|
||||
<script>
|
||||
document.getElementById('register-form').addEventListener('submit', async function(e) {
|
||||
e.preventDefault();
|
||||
const form = new FormData(this);
|
||||
const errorEl = document.getElementById('register-error');
|
||||
errorEl.classList.add('hidden');
|
||||
|
||||
const password = form.get('password');
|
||||
const confirmPassword = form.get('confirm_password');
|
||||
if (password !== confirmPassword) {
|
||||
errorEl.textContent = '两次输入的密码不一致';
|
||||
errorEl.classList.remove('hidden');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const resp = await fetch('/api/auth/register', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
credentials: 'same-origin',
|
||||
body: JSON.stringify({
|
||||
email: form.get('email'),
|
||||
password: password
|
||||
})
|
||||
});
|
||||
const data = await resp.json();
|
||||
if (!resp.ok) {
|
||||
errorEl.textContent = data.error || '注册失败';
|
||||
errorEl.classList.remove('hidden');
|
||||
return;
|
||||
}
|
||||
window.location.href = '/';
|
||||
} catch (err) {
|
||||
errorEl.textContent = '网络错误,请重试';
|
||||
errorEl.classList.remove('hidden');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -177,7 +177,7 @@
|
||||
async function init() {
|
||||
try {
|
||||
// Load repo info
|
||||
const reposResp = await fetch('/api/repos');
|
||||
const reposResp = await fetch('/api/repos', { credentials: 'same-origin' });
|
||||
if (reposResp.ok) {
|
||||
const repos = await reposResp.json();
|
||||
const repo = repos.find(r => r.id == repoId || r.id === parseInt(repoId));
|
||||
@@ -192,14 +192,14 @@
|
||||
}
|
||||
|
||||
// Load refs
|
||||
const refsResp = await fetch(`/api/repos/${repoId}/refs`);
|
||||
const refsResp = await fetch(`/api/repos/${repoId}/refs`, { credentials: 'same-origin' });
|
||||
if (refsResp.ok) {
|
||||
currentRefs = await refsResp.json();
|
||||
|
||||
// Fetch recent commits independently
|
||||
let commits = [];
|
||||
try {
|
||||
const graphResp = await fetch(`/api/repos/${repoId}/graph?max_commits=20`);
|
||||
const graphResp = await fetch(`/api/repos/${repoId}/graph?max_commits=20`, { credentials: 'same-origin' });
|
||||
if (graphResp.ok) { const gd = await graphResp.json(); commits = gd.commits || []; }
|
||||
} catch (e) { /* ignore */ }
|
||||
|
||||
|
||||
@@ -129,14 +129,14 @@
|
||||
// Load refs on page load
|
||||
async function loadRefs() {
|
||||
try {
|
||||
const resp = await fetch(`/api/repos/${repoId}/refs`);
|
||||
const resp = await fetch(`/api/repos/${repoId}/refs`, { credentials: 'same-origin' });
|
||||
if (!resp.ok) throw new Error('Failed to load refs');
|
||||
const refs = await resp.json();
|
||||
|
||||
// Fetch recent commits for the "Recent Commits" group
|
||||
let commits = [];
|
||||
try {
|
||||
const graphResp = await fetch(`/api/repos/${repoId}/graph?max_commits=20`);
|
||||
const graphResp = await fetch(`/api/repos/${repoId}/graph?max_commits=20`, { credentials: 'same-origin' });
|
||||
if (graphResp.ok) {
|
||||
const gd = await graphResp.json();
|
||||
commits = gd.commits || [];
|
||||
@@ -437,7 +437,7 @@
|
||||
|
||||
async function loadHistoryList() {
|
||||
try {
|
||||
const resp = await fetch(`/api/repos/${repoId}/review/analyses`);
|
||||
const resp = await fetch(`/api/repos/${repoId}/review/analyses`, { credentials: 'same-origin' });
|
||||
if (!resp.ok) return;
|
||||
const analyses = await resp.json();
|
||||
|
||||
@@ -465,7 +465,7 @@
|
||||
|
||||
try {
|
||||
// Fetch the review result
|
||||
const resp = await fetch(`/api/repos/${repoId}/review/analyses/${analysisId}`);
|
||||
const resp = await fetch(`/api/repos/${repoId}/review/analyses/${analysisId}`, { credentials: 'same-origin' });
|
||||
if (!resp.ok) throw new Error('Failed to load review');
|
||||
const data = await resp.json();
|
||||
|
||||
@@ -518,7 +518,7 @@
|
||||
const head = data.head_ref;
|
||||
|
||||
// Load diff from API
|
||||
const diffResp = await fetch(`/api/repos/${repoId}/diff?base=${encodeURIComponent(base)}&head=${encodeURIComponent(head)}`);
|
||||
const diffResp = await fetch(`/api/repos/${repoId}/diff?base=${encodeURIComponent(base)}&head=${encodeURIComponent(head)}`, { credentials: 'same-origin' });
|
||||
if (diffResp.ok) {
|
||||
const diffData = await diffResp.json();
|
||||
const diffString = diffData.diff || '';
|
||||
|
||||
@@ -87,6 +87,7 @@ document.getElementById('settings-form').addEventListener('submit', async functi
|
||||
const resp = await fetch('/api/settings', {
|
||||
method: 'PUT',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
credentials: 'same-origin',
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
if (!resp.ok) throw new Error('保存失败');
|
||||
|
||||
Reference in New Issue
Block a user