Initial commit: MyBlog full stack blog
This commit is contained in:
@@ -0,0 +1,210 @@
|
||||
/* =====================================================================
|
||||
* 认证模块(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 = "注册";
|
||||
}
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user