210 lines
8.3 KiB
JavaScript
210 lines
8.3 KiB
JavaScript
/* =====================================================================
|
||
* 认证模块(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 = "注册";
|
||
}
|
||
});
|
||
} |