Files

210 lines
8.3 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* =====================================================================
* 认证模块(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 = "注册";
}
});
}