feat(website): 培训网站登录迁移到平台 /auth(统一账号 + 平台 JWT)

阶段2 网站侧补全(阶段3 已停用 /api/auth 登录):
- services/auth.js 端点 /api/auth/*→/auth/*:sendCode/phone-login/login/update-profile 走平台,
  pine_token 存平台 JWT;用户端 X-Client:web→source=web;sendCode 兼容 stub_code→debugCode。
- updateProfile/phoneLogin/login 映射平台 profile→本地 user 契约
  (username/name/avatar/phone/phoneBound/status/topics/source),与小程序一致。
- logout 改本地清除;业务(报名/测评/政策等)用平台 JWT(Phase3 培训 require_auth 已验证)。
- 不再产生 accounts 双套账号;手机验证码统一由平台下发校验。

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Pine
2026-08-25 20:28:53 +08:00
parent 2914b40171
commit 18a2ad04f7
+46 -56
View File
@@ -1,79 +1,75 @@
/**
* 认证服务(auth service
* 认证服务(auth service——统一账号
* -------------------------------------------------------------
* 用户端:手机号 + 短信验证码登录 / 注册(手机号即账号,未注册自动注册)。
* POST /api/auth/send-code 发送验证码(演示环境响应返回 debugCode
* POST /api/auth/phone-login 手机号 + 验证码 → 自动注册或登录 → token
* 用户端:手机号 + 短信验证码登录(手机号即账号,未注册自动注册)。
* POST /auth/send-code 发送验证码(stub 环境返回 stub_code/debugCode
* POST /auth/phone-login 手机号 + 验证码 → 自动注册或登录 → 平台 JWT
* POST /auth/update-profile 更新资料 → 平台 profile
* 平台端(/pine 内部管理):账号密码登录(内置 pine/123456)。
* POST /api/auth/login
* 迁移真实后端:只改 API_BASE 与保持 /api/auth/* 契约即可
* POST /auth/login 密码登录 → 平台 JWT + 身份列表
* 统一账号:所有登录写全局 users + 平台 JWT(与小程序一致,见 分端口同步约束.md)
*/
const API_BASE = 'https://opc.pinesound.cn'; // 云超服 FastAPI 后端
const TOKEN_KEY = 'pine_token';
const USER_KEY = 'pine_user';
/** 平台 profile → 本地 user 契约字段(各页面读取 name/status/topics/source/phoneBound 等) */
function pickUser(d) {
return {
username: d.username,
name: d.name || d.nickname || d.username || '用户',
avatar: d.avatar || '',
phone: d.phone || '',
phoneBound: !!d.phoneBound,
status: d.status || '',
topics: Array.isArray(d.topics) ? d.topics : [],
source: d.source || ''
};
}
export function saveSession(data) {
if (!data || !data.token) return;
localStorage.setItem(TOKEN_KEY, data.token);
localStorage.setItem(USER_KEY, JSON.stringify({ username: data.username, name: data.name || data.username }));
localStorage.setItem(USER_KEY, JSON.stringify(pickUser(data)));
}
async function req(path, { method = 'POST', body, auth = false } = {}) {
const headers = { 'Content-Type': 'application/json', 'X-Client': 'web' };
if (auth) { const t = getToken(); if (t) headers.Authorization = `Bearer ${t}`; }
let res;
try {
res = await fetch(`${API_BASE}${path}`, { method, headers, body: body ? JSON.stringify(body) : undefined });
} catch {
throw new Error('网络连接失败,请检查网络后重试');
}
const data = await res.json().catch(() => ({}));
if (!res.ok) throw new Error(data.detail || data.error || '请求失败,请重试');
return data; // 平台契约:token/username/name/.../auth/*
}
/* ---------------- 用户端:手机号 + 验证码 ---------------- */
export async function sendCode(phone) {
let res;
try {
res = await fetch(`${API_BASE}/api/auth/send-code`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-Client': 'web' },
body: JSON.stringify({ phone })
});
} catch {
throw new Error('网络连接失败,请检查网络后重试');
}
const data = await res.json().catch(() => ({}));
if (!res.ok || !data.ok) throw new Error(data.error || '验证码发送失败,请重试');
return data; // { ok, sent, debugCode }
const data = await req('/auth/send-code', { body: { phone } });
return { ok: true, sent: true, debugCode: data.stub_code ?? data.debugCode };
}
export async function phoneLogin(phone, code) {
let res;
try {
res = await fetch(`${API_BASE}/api/auth/phone-login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-Client': 'web' },
body: JSON.stringify({ phone, code })
});
} catch {
throw new Error('网络连接失败,请检查网络后重试');
}
const data = await res.json().catch(() => ({}));
if (!res.ok || !data.token) throw new Error(data.error || '登录失败,请重试');
saveSession(data);
const data = await req('/auth/phone-login', { body: { phone, code } });
if (data.token) saveSession(data);
return data;
}
/* ---------------- 平台端:账号密码登录(内部管理) ---------------- */
export async function login(username, password) {
let res;
try {
res = await fetch(`${API_BASE}/api/auth/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-Client': 'web' },
body: JSON.stringify({ username, password })
});
} catch {
throw new Error('网络连接失败,请检查网络后重试');
}
const data = await res.json().catch(() => ({}));
if (!res.ok || !data.token) throw new Error(data.error || '登录失败,请重试');
saveSession(data);
const data = await req('/auth/login', { body: { username, password } });
if (data.token) saveSession(data);
return data;
}
export function logout() {
localStorage.removeItem(TOKEN_KEY);
localStorage.removeItem(USER_KEY);
try { fetch(`${API_BASE}/api/auth/logout`, { method: 'POST' }).catch(() => {}); } catch { /* noop */ }
// 平台用 /auth/revoke-token;这里本地退出即可(与小程序一致)
}
export function getToken() {
@@ -89,15 +85,9 @@ export function saveUser(user) {
/** 更新当前用户资料(昵称/头像 + 报名资料 status/topics/source),需登录(token */
export async function updateProfile(data) {
const token = getToken();
const res = await fetch(`${API_BASE}/api/auth/update-profile`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', ...(token ? { Authorization: `Bearer ${token}` } : {}) },
body: JSON.stringify(data)
});
const r = await res.json().catch(() => ({}));
if (res.ok && r.ok && r.user) saveUser(r.user);
return r;
const r = await req('/auth/update-profile', { body: data, auth: true });
if (r.username) saveUser(pickUser(r));
return { ok: true, user: pickUser(r) };
}
export function isAuthed() {