Files

210 lines
8.3 KiB
JavaScript
Raw Permalink Normal View History

2026-08-22 22:28:41 +08:00
/* =====================================================================
* 认证模块(auth.js
* 登录 / 注册 / 会话保存 / 当前用户刷新。
* localStorage 只保存 token 与 username(绝不保存密码)。
* ===================================================================== */
import { state } from "./state.js";
import { $, escapeHtml, openModal, showToast } from "./utils.js";
import { api, uploadFile } from "./api.js";
import { render, renderShell } from "./router.js";
export function isLoggedIn() { return !!localStorage.getItem("token"); }
export function saveSession(token, username) {
localStorage.setItem("token", token);
localStorage.setItem("username", username);
}
export function logout() {
localStorage.removeItem("token");
localStorage.removeItem("username");
state.user = null;
state.friendStatus = "none";
renderShell();
render();
showToast("已退出登录");
}
export async function refreshUser() {
if (!isLoggedIn()) { state.user = null; state.friendStatus = "none"; return; }
try {
const data = await api("/api/user/level");
state.user = {
id: data.user_id,
username: localStorage.getItem("username") || "",
role: data.role,
};
if (data.role === "blogger") state.friendStatus = "blogger";
else if (data.role === "friend") state.friendStatus = "accepted";
else {
try {
const fs = await api("/api/friend/status");
state.friendStatus = fs.status;
} catch (err) { state.friendStatus = "none"; }
}
} catch (err) {
if (err.status === 401) logout();
else showToast(err.message, "error");
}
}
export async function afterAuthSuccess(username) {
await refreshUser(); // 拉取真实角色与好友状态
renderShell();
render();
}
export function openLoginModal() {
const form = document.createElement("form");
form.className = "form";
form.innerHTML = `
<label for="login-email">邮箱</label>
<input id="login-email" name="email" type="email" required placeholder="you@example.com">
<label for="login-password">密码</label>
<input id="login-password" name="password" type="password" required placeholder="请输入密码">
<button class="btn btn-primary btn-block" type="submit">登录</button>
<p class="form-hint">还没有账号?<a href="#" id="goto-register">立即注册</a></p>`;
const modal = openModal({ title: "登录", content: form });
$("#goto-register", form).addEventListener("click", (e) => {
e.preventDefault();
modal.close();
openRegisterModal();
});
form.addEventListener("submit", async (e) => {
e.preventDefault();
const email = form.email.value.trim();
const password = form.password.value;
if (!email || !password) { showToast("请填写邮箱和密码", "error"); return; }
const btn = $('button[type="submit"]', form);
btn.disabled = true; btn.textContent = "登录中…";
try {
const data = await api("/api/login", { method: "POST", body: { email, password } });
saveSession(data.token, data.username);
modal.close();
await afterAuthSuccess(data.username);
showToast("登录成功", "success");
} catch (err) {
showToast(err.message, "error");
} finally {
btn.disabled = false; btn.textContent = "登录";
}
});
}
/** 好友个人设置:上传自己的头像(访客禁止上传,后端有角色校验) */
export function openProfileModal() {
const content = document.createElement("div");
content.innerHTML = `
<div class="profile-avatar-row">
<div class="profile-avatar" id="profile-avatar"><span class="muted">…</span></div>
<div>
<button type="button" class="btn btn-outline" id="btn-avatar-upload">📷 上传头像</button>
<input type="file" id="avatar-file" accept="image/jpeg,image/png,image/webp" hidden>
<p class="muted form-hint">支持 jpg / png / webp,最大 4MB</p>
</div>
</div>`;
const modal = openModal({ title: "个人设置", content });
const avatarBox = $("#profile-avatar", content);
const renderPreview = (url) => {
avatarBox.innerHTML = url
? `<img src="${escapeHtml(url)}" alt="头像预览">`
: '<span class="muted">…</span>';
};
api("/api/user/me").then((me) => renderPreview(me.avatar || "")).catch(() => {});
$("#btn-avatar-upload", content).addEventListener("click", () => $("#avatar-file", content).click());
$("#avatar-file", content).addEventListener("change", async () => {
const file = $("#avatar-file", content).files && $("#avatar-file", content).files[0];
if (!file) return;
try {
const data = await uploadFile("/api/upload/avatar", file);
renderPreview(data.url);
showToast("头像上传成功", "success");
} catch (err) { showToast(err.message, "error"); }
$("#avatar-file", content).value = "";
});
}
export function openRegisterModal() {
const form = document.createElement("form");
form.className = "form";
form.innerHTML = `
<label for="reg-email">邮箱</label>
<input id="reg-email" name="email" type="email" required placeholder="you@example.com">
<label for="reg-code">邮箱验证码</label>
<div class="code-row">
<input id="reg-code" name="code" type="text" inputmode="numeric" maxlength="6" placeholder="6 位验证码" required>
<button type="button" class="btn btn-outline" id="btn-send-code">发送验证码</button>
</div>
<label for="reg-username">用户名</label>
<input id="reg-username" name="username" type="text" required minlength="2" maxlength="50" placeholder="用户名(2-50 字符)">
<label for="reg-password">密码</label>
<input id="reg-password" name="password" type="password" required minlength="6" maxlength="128" placeholder="至少 6 位">
<button class="btn btn-primary btn-block" type="submit">注册</button>
<p class="form-hint">已有账号?<a href="#" id="goto-login">直接登录</a></p>`;
const modal = openModal({ title: "注册", content: form });
$("#goto-login", form).addEventListener("click", (e) => {
e.preventDefault();
modal.close();
openLoginModal();
});
// 发送邮箱验证码(60 秒倒计时防重复点击;后端另有按邮箱/IP 的小时限流)
const sendCodeBtn = $("#btn-send-code", form);
sendCodeBtn.addEventListener("click", async () => {
const email = form.email.value.trim();
if (!email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
showToast("请先填写正确的邮箱", "error");
return;
}
sendCodeBtn.disabled = true;
sendCodeBtn.textContent = "发送中…";
try {
await api("/api/email/send-code", { method: "POST", body: { email, purpose: "register" } });
showToast("验证码已发送,请查收邮件", "success");
let countdown = 60;
const timer = setInterval(() => {
countdown -= 1;
if (countdown <= 0) {
clearInterval(timer);
sendCodeBtn.disabled = false;
sendCodeBtn.textContent = "发送验证码";
} else {
sendCodeBtn.textContent = `${countdown}s 后重发`;
}
}, 1000);
} catch (err) {
showToast(err.message, "error");
sendCodeBtn.disabled = false;
sendCodeBtn.textContent = "发送验证码";
}
});
form.addEventListener("submit", async (e) => {
e.preventDefault();
const email = form.email.value.trim();
const username = form.username.value.trim();
const password = form.password.value;
const code = form.code.value.trim();
if (!email || !username || !password || !code) { showToast("请填写完整信息", "error"); return; }
if (password.length < 6) { showToast("密码至少 6 位", "error"); return; }
const btn = $('button[type="submit"]', form);
btn.disabled = true; btn.textContent = "注册中…";
try {
await api("/api/register", { method: "POST", body: { email, username, password, code } });
// 注册成功后自动登录
const loginData = await api("/api/login", { method: "POST", body: { email, password } });
saveSession(loginData.token, username);
modal.close();
await afterAuthSuccess(username);
showToast("注册成功", "success");
} catch (err) {
showToast(err.message, "error");
} finally {
btn.disabled = false; btn.textContent = "注册";
}
});
}