Files
training/website/src/services/auth.js
T
Pine c7d9a74063 feat(profile): 网站个人中心「绑定小程序」—— 小程序码+扫码确认绑定
- 小程序行加「绑定」→ 显示bind-start小程序码, 轮询确认; services/auth 加 mpQrBindStart
2026-08-26 12:48:17 +08:00

138 lines
5.4 KiB
JavaScript
Raw 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 service)——统一账号
* -------------------------------------------------------------
* 用户端:手机号 + 短信验证码登录(手机号即账号,未注册自动注册)。
* POST /auth/send-code 发送验证码(stub 环境返回 stub_code/debugCode
* POST /auth/phone-login 手机号 + 验证码 → 自动注册或登录 → 平台 JWT
* POST /auth/update-profile 更新资料 → 平台 profile
* 平台端(/pine 内部管理):账号密码登录(内置 pine/123456)。
* 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(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) {
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) {
const data = await req('/auth/phone-login', { body: { phone, code } });
if (data.token) saveSession(data);
return data;
}
/* ---------------- 平台端:账号密码登录(内部管理) ---------------- */
export async function login(username, password) {
const data = await req('/auth/login', { body: { username, password } });
if (data.token) saveSession(data);
return data;
}
/* ---------------- 绑定 / 解绑(统一账号,按手机号合并,绝不新建账号) ---------------- */
export async function bindPhone(phone, code) {
const data = await req('/auth/bind-phone', { body: { phone, code } });
if (data.token) saveSession(data); // 可能合并成另一账号,切换新令牌
return data;
}
export async function unbind(type) {
return req('/auth/unbind', { body: { type }, auth: true });
}
/** 拉取当前账号(含 phoneBound/wxBound/wxMiniBound */
export async function me() {
return req('/auth/me', { method: 'GET', auth: true });
}
/* ---------------- 微信扫码 / 小程序扫码登录 ---------------- */
export async function wxQrStart() {
return req('/auth/wx-qr/start', { method: 'GET' });
}
export async function wxQrPoll(scene) {
return req(`/auth/wx-qr/poll?scene=${encodeURIComponent(scene)}`, { method: 'GET' });
}
export async function mpQrStart() {
return req('/auth/mp-qr/start', { method: 'GET' });
}
export async function mpQrPoll(scene) {
return req(`/auth/mp-qr/poll?scene=${encodeURIComponent(scene)}`, { method: 'GET' });
}
/** 发起「绑定小程序」:返回小程序码(微信扫码自动打开小程序确认),绑定到当前账号 */
export async function mpQrBindStart() {
return req('/auth/mp-qr/bind-start', { method: 'GET', auth: true });
}
/** 扫码/短信登录成功:落 session 后返回完整平台 profile */
export function applyAuthResult(data) {
if (data && data.token) saveSession(data);
return data;
}
export function logout() {
localStorage.removeItem(TOKEN_KEY);
localStorage.removeItem(USER_KEY);
// 平台用 /auth/revoke-token;这里本地退出即可(与小程序一致)
}
export function getToken() {
if (typeof localStorage === 'undefined') return null;
return localStorage.getItem(TOKEN_KEY);
}
/** 用完整 user 对象覆盖本地用户缓存(含 status/topics/source 等报名资料) */
export function saveUser(user) {
if (!user) return;
localStorage.setItem(USER_KEY, JSON.stringify(user));
}
/** 更新当前用户资料(昵称/头像 + 报名资料 status/topics/source),需登录(token */
export async function updateProfile(data) {
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() {
return !!getToken();
}
export function getUser() {
if (typeof localStorage === 'undefined') return null;
try { return JSON.parse(localStorage.getItem(USER_KEY) || 'null'); } catch { return null; }
}