Files
hjc-web/app/composables/useHjcAuth.ts
T
weicw1996 d0c2dd17cb feat(tender): 登录加图形验证码、注册加短信验证码;改按 body.code 判定 401/403
- 新增 captcha / sms / logout 三个 Nuxt server 代理
- 修复代理把 ApiResult 拍扁成 data 的问题(my-orders、enterprise、order、pay、
  mark-paid):登录态失败被前端当成「没有订单 / 未提交资质」,无法区分
- 新增 handleAuthCode 统一处理:401 → 清凭据并跳 /login?redirect=…;
  403 → 只提示「没有访问权限」,不清 token、不跳登录页
- 登录页与注册页分别接入图形验证码与短信验证码(含 60s 倒计时)
- 修复回跳开放重定向:redirect 参数安全解码且只接受站内路径
  (拒绝 //evil.com、https://… 及反斜杠变体 /\evil.com)
- 下单成功但发起支付失败时停在支付步,避免用户重提交产生重复订单
2026-09-10 23:44:19 +08:00

160 lines
5.4 KiB
TypeScript
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.
import type { HjcUser } from '~/types/tender'
/**
* 后端 ApiResult.code 的登录态语义。
*
* <b>HTTP 状态码恒为 200</b>,失败一律靠 body 的 code 判定:
* - 0 成功
* - 401 未登录或 token 失效 → 清本地凭据并跳登录页
* - 403 已登录但无权限(message「没有访问权限」)→ 只提示,<b>不清 token、不跳登录页</b>
* - 其他非 0 → 业务失败,展示 message
*/
export const HJC_CODE_UNAUTHORIZED = 401
export const HJC_CODE_FORBIDDEN = 403
/** 登录态(token 存 cookieSSR 安全;服务器端代理从 cookie 读取并转发 Authorization */
export function useHjcAuth() {
const token = useCookie<string | null>('hjc_token', {
maxAge: 60 * 60 * 24 * 7,
sameSite: 'lax'
})
const user = useState<HjcUser | null>('hjc-user', () => null)
function isLoggedIn() {
return !!token.value
}
/** 清本地凭据(token 是无状态 JWT,后端无会话,清本地即退出) */
function clearCredentials() {
token.value = null
user.value = null
}
/**
* 统一处理 ApiResult 的登录态失败:**只看 body.code,不看 HTTP 状态码**。
*
* @param res 服务器代理透传的 ApiResult
* @param redirect 401 跳登录页时带上,便于登录后回跳
* @returns handled=true 表示已按登录态问题处理完(401 已跳转 / 403 带提示文案);
* handled=false 表示不是登录态问题,交调用方按业务失败处理
*/
async function handleAuthCode(
res: any,
redirect = '/'
): Promise<{ handled: boolean; message?: string }> {
const code = Number(res?.code)
if (code === HJC_CODE_UNAUTHORIZED) {
clearCredentials()
await navigateTo(`/login?redirect=${encodeURIComponent(redirect)}`)
return { handled: true }
}
if (code === HJC_CODE_FORBIDDEN) {
// 已登录但无权限:不清 token、不跳登录页,仅提示后端 message
return { handled: true, message: res?.message || '没有访问权限' }
}
return { handled: false }
}
/**
* 拉取登录用图形验证码。
* 后端只返回图片(data:image/png;base64,...),不下发答案,需用户肉眼识别后输入。
*/
async function fetchCaptcha(): Promise<{ ok: boolean; image?: string; message?: string }> {
try {
// 带时间戳,避免浏览器/中间层缓存旧验证码图片
const res: any = await $fetch('/api/tender/captcha', { query: { t: Date.now() } })
if (res?.code === 0 && res.data?.image) {
return { ok: true, image: res.data.image }
}
return { ok: false, message: res?.message || '验证码获取失败' }
} catch (e: any) {
return { ok: false, message: e?.data?.statusMessage || e?.data?.message || e?.message || '验证码获取失败' }
}
}
/**
* 发送注册短信验证码。
* @param phone 经办人手机号(即账号手机号),后端据此发码
*/
async function sendSms(phone: string): Promise<{ ok: boolean; message?: string }> {
try {
const res: any = await $fetch('/api/tender/sms', {
method: 'POST',
body: { phone }
})
if (res?.code === 0) {
return { ok: true, message: res?.message || '验证码已发送' }
}
return { ok: false, message: res?.message || '验证码发送失败' }
} catch (e: any) {
return { ok: false, message: e?.data?.statusMessage || e?.data?.message || e?.message || '验证码发送失败' }
}
}
/**
* 企业登录
* @param code 图形验证码(必填,后端先校验非空)
*/
async function login(enterpriseName: string, password: string, code: string) {
try {
const res: any = await $fetch('/api/tender/login', {
method: 'POST',
body: { enterpriseName, password, code }
})
// mp-api 统一返回 ApiResult{code,message,data}code===0 成功
if (res?.code === 0 && res.data?.access_token) {
token.value = res.data.access_token
user.value = res.data.user || null
return { ok: true, user: res.data.user }
}
return { ok: false, message: res?.message || '登录失败' }
} catch (e: any) {
return { ok: false, message: e?.data?.statusMessage || e?.message || '登录失败' }
}
}
/**
* 企业注册(一站式)
* @param payload 注册字段 + `code`(短信验证码,发给经办人手机号)
*/
async function register(payload: Record<string, any>) {
try {
const res: any = await $fetch('/api/tender/register', {
method: 'POST',
body: payload
})
if (res?.code === 0 && res.data?.access_token) {
token.value = res.data.access_token
user.value = res.data.user || null
return { ok: true, user: res.data.user }
}
return { ok: false, message: res?.message || '注册失败' }
} catch (e: any) {
return { ok: false, message: e?.data?.statusMessage || e?.message || '注册失败' }
}
}
/** 退出登录:先通知后端(无服务端会话,仅为契约完整),再清本地凭据 */
async function logout() {
try {
await $fetch('/api/tender/logout', { method: 'POST' })
} catch {
// 退出接口失败不阻断本地退出:token 是无状态 JWT,清本地凭据即已登出
}
clearCredentials()
}
return {
token,
user,
isLoggedIn,
clearCredentials,
handleAuthCode,
fetchCaptcha,
sendSms,
login,
register,
logout
}
}