|
|
@ -34,10 +34,28 @@ export interface AuthTokens { |
|
|
expiresAt?: number; |
|
|
expiresAt?: number; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
* 登录成功时后端返回的完整字段(含用户身份)。 |
|
|
|
|
|
* 后端约定 expiresIn 为秒级 TTL,前端转成 expiresAt(毫秒绝对时间戳)。 |
|
|
|
|
|
* role 为后端原始值(STUDENT / COMPANY / ADMIN),由调用方映射为 AppRole。 |
|
|
|
|
|
*/ |
|
|
|
|
|
export interface AuthLoginData extends AuthTokens { |
|
|
|
|
|
userId: number; |
|
|
|
|
|
phone: string; |
|
|
|
|
|
nickname: string; |
|
|
|
|
|
role: BackendRole; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** 后端 expiresIn(秒)→ expiresAt(毫秒,从现在起算) */ |
|
|
|
|
|
function toExpiresAt(expiresIn?: number): number | undefined { |
|
|
|
|
|
if (!expiresIn || expiresIn <= 0) return undefined; |
|
|
|
|
|
return Date.now() + expiresIn * 1000; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
export interface SmsSendPayload { |
|
|
export interface SmsSendPayload { |
|
|
phone: string; |
|
|
phone: string; |
|
|
/** purpose 必须使用后端支持的固定值,如 login / register / company-register */ |
|
|
/** 后端只接受这三个值:login / company-register / password-reset */ |
|
|
purpose: string; |
|
|
purpose: 'login' | 'company-register' | 'password-reset'; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
/** 客户端手机号格式预检(与后端保持一致;无效输入直接本地 reject,不发请求) */ |
|
|
/** 客户端手机号格式预检(与后端保持一致;无效输入直接本地 reject,不发请求) */ |
|
|
@ -51,7 +69,12 @@ export async function sendSmsCode(payload: SmsSendPayload): Promise<void> { |
|
|
throw new Error('手机号格式不正确'); |
|
|
throw new Error('手机号格式不正确'); |
|
|
} |
|
|
} |
|
|
if (isMockMode()) return; |
|
|
if (isMockMode()) return; |
|
|
await request('/auth/sms/send', { method: 'POST', auth: false, body: payload }); |
|
|
// 后端约束:/auth/sms/send 只接受 Query 参数(body 传参会被当作缺失)
|
|
|
|
|
|
await request('/auth/sms/send', { |
|
|
|
|
|
method: 'POST', |
|
|
|
|
|
auth: false, |
|
|
|
|
|
query: { phone: payload.phone, purpose: payload.purpose }, |
|
|
|
|
|
}); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
export interface PasswordLoginPayload { |
|
|
export interface PasswordLoginPayload { |
|
|
@ -59,19 +82,39 @@ export interface PasswordLoginPayload { |
|
|
password: string; |
|
|
password: string; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
/** 手机号 + 密码登录(§4 步骤2A):成功后返回双 Token */ |
|
|
/** 手机号 + 密码登录(§4 步骤2A):成功后返回双 Token + 用户身份 */ |
|
|
export async function loginByPassword(payload: PasswordLoginPayload): Promise<AuthTokens> { |
|
|
export async function loginByPassword(payload: PasswordLoginPayload): Promise<AuthLoginData> { |
|
|
if (!isValidPhone(payload.phone)) { |
|
|
if (!isValidPhone(payload.phone)) { |
|
|
throw new Error('手机号格式不正确'); |
|
|
throw new Error('手机号格式不正确'); |
|
|
} |
|
|
} |
|
|
if (isMockMode()) { |
|
|
if (isMockMode()) { |
|
|
return mockAuthTokens(`pwd_${payload.phone}`); |
|
|
const mock = mockAuthTokens(`pwd_${payload.phone}`); |
|
|
|
|
|
return { |
|
|
|
|
|
...mock, |
|
|
|
|
|
userId: 0, |
|
|
|
|
|
phone: payload.phone, |
|
|
|
|
|
nickname: 'Mock 用户', |
|
|
|
|
|
role: 'STUDENT', |
|
|
|
|
|
}; |
|
|
} |
|
|
} |
|
|
return request<AuthTokens>('/auth/password/login', { |
|
|
const raw = await request<{ |
|
|
method: 'POST', |
|
|
userId: number; |
|
|
auth: false, |
|
|
phone: string; |
|
|
body: payload, |
|
|
nickname: string; |
|
|
}); |
|
|
role: BackendRole; |
|
|
|
|
|
accessToken: string; |
|
|
|
|
|
refreshToken: string; |
|
|
|
|
|
expiresIn?: number; |
|
|
|
|
|
}>('/auth/password/login', { method: 'POST', auth: false, body: payload }); |
|
|
|
|
|
return { |
|
|
|
|
|
userId: raw.userId, |
|
|
|
|
|
phone: raw.phone, |
|
|
|
|
|
nickname: raw.nickname, |
|
|
|
|
|
role: raw.role, |
|
|
|
|
|
accessToken: raw.accessToken, |
|
|
|
|
|
refreshToken: raw.refreshToken, |
|
|
|
|
|
expiresAt: toExpiresAt(raw.expiresIn), |
|
|
|
|
|
}; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
/** mock 形态下返回的双 Token(开发期不触网) */ |
|
|
/** mock 形态下返回的双 Token(开发期不触网) */ |
|
|
@ -90,34 +133,87 @@ export interface SmsLoginPayload { |
|
|
|
|
|
|
|
|
/** |
|
|
/** |
|
|
* 短信验证码登录(§4 步骤2B)。 |
|
|
* 短信验证码登录(§4 步骤2B)。 |
|
|
* 注意:接口文档标注为 Mock 登录,生产前端不要作为正式入口, |
|
|
* 后端自动创建 STUDENT;已有账号返回其实际角色(不一定是学生)。 |
|
|
* 除非产品和后端明确确认。保留实现以便联调与兜底。 |
|
|
|
|
|
*/ |
|
|
*/ |
|
|
export async function loginBySms(payload: SmsLoginPayload): Promise<AuthTokens> { |
|
|
export async function loginBySms(payload: SmsLoginPayload): Promise<AuthLoginData> { |
|
|
if (isMockMode()) { |
|
|
if (isMockMode()) { |
|
|
return mockAuthTokens(`sms_${payload.phone}`); |
|
|
const mock = mockAuthTokens(`sms_${payload.phone}`); |
|
|
|
|
|
return { |
|
|
|
|
|
...mock, |
|
|
|
|
|
userId: 0, |
|
|
|
|
|
phone: payload.phone, |
|
|
|
|
|
nickname: 'Mock 用户', |
|
|
|
|
|
role: 'STUDENT', |
|
|
|
|
|
}; |
|
|
} |
|
|
} |
|
|
return request<AuthTokens>('/auth/login', { method: 'POST', auth: false, body: payload }); |
|
|
const raw = await request<{ |
|
|
|
|
|
userId: number; |
|
|
|
|
|
phone: string; |
|
|
|
|
|
nickname: string; |
|
|
|
|
|
role: BackendRole; |
|
|
|
|
|
accessToken: string; |
|
|
|
|
|
refreshToken: string; |
|
|
|
|
|
expiresIn?: number; |
|
|
|
|
|
}>('/auth/login', { method: 'POST', auth: false, body: payload }); |
|
|
|
|
|
return { |
|
|
|
|
|
userId: raw.userId, |
|
|
|
|
|
phone: raw.phone, |
|
|
|
|
|
nickname: raw.nickname, |
|
|
|
|
|
role: raw.role, |
|
|
|
|
|
accessToken: raw.accessToken, |
|
|
|
|
|
refreshToken: raw.refreshToken, |
|
|
|
|
|
expiresAt: toExpiresAt(raw.expiresIn), |
|
|
|
|
|
}; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** 后端 /auth/me 返回的当前用户信息(role 已映射为应用内部角色) */ |
|
|
export interface CurrentUser { |
|
|
export interface CurrentUser { |
|
|
|
|
|
/** 用户 id(后端为 int64,前端按字符串处理避免精度丢失) */ |
|
|
id: string; |
|
|
id: string; |
|
|
/** 已由后端角色映射为应用内部角色 */ |
|
|
/** 已由后端角色映射为应用内部角色(candidate / enterprise / admin) */ |
|
|
role: AppRole; |
|
|
role: AppRole; |
|
|
/** 其余身份字段按 OpenAPI 透传 */ |
|
|
/** 手机号 */ |
|
|
[key: string]: unknown; |
|
|
phone: string; |
|
|
|
|
|
/** 昵称 */ |
|
|
|
|
|
nickname: string; |
|
|
|
|
|
/** 是否已实名认证 */ |
|
|
|
|
|
verified: boolean; |
|
|
|
|
|
/** 账号是否启用 */ |
|
|
|
|
|
active: boolean; |
|
|
|
|
|
/** 注册时间(ISO 字符串) */ |
|
|
|
|
|
createdAt?: string; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
/** 拉取当前用户与角色(§4 步骤3):角色以后端为准并映射 */ |
|
|
/** 拉取当前用户与角色(§4 步骤3):角色以后端为准并映射 */ |
|
|
export async function fetchCurrentUser(): Promise<CurrentUser> { |
|
|
export async function fetchCurrentUser(): Promise<CurrentUser> { |
|
|
if (isMockMode()) { |
|
|
if (isMockMode()) { |
|
|
return { id: 'mock-user', role: 'candidate' }; |
|
|
return { |
|
|
|
|
|
id: 'mock-user', |
|
|
|
|
|
role: 'candidate', |
|
|
|
|
|
phone: '', |
|
|
|
|
|
nickname: 'Mock 用户', |
|
|
|
|
|
verified: true, |
|
|
|
|
|
active: true, |
|
|
|
|
|
}; |
|
|
} |
|
|
} |
|
|
const data = await request<{ id: string; role: BackendRole } & Record<string, unknown>>( |
|
|
const raw = await request<{ |
|
|
'/auth/me', |
|
|
id: number; |
|
|
{ method: 'GET' }, |
|
|
role: BackendRole; |
|
|
); |
|
|
phone: string; |
|
|
return { ...data, role: mapBackendRole(data.role) }; |
|
|
nickname: string; |
|
|
|
|
|
verified: boolean; |
|
|
|
|
|
active: boolean; |
|
|
|
|
|
createdAt?: string; |
|
|
|
|
|
}>('/auth/me', { method: 'GET' }); |
|
|
|
|
|
return { |
|
|
|
|
|
id: String(raw.id), |
|
|
|
|
|
role: mapBackendRole(raw.role), |
|
|
|
|
|
phone: raw.phone, |
|
|
|
|
|
nickname: raw.nickname, |
|
|
|
|
|
verified: raw.verified, |
|
|
|
|
|
active: raw.active, |
|
|
|
|
|
createdAt: raw.createdAt, |
|
|
|
|
|
}; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
/** 退出登录(§4 步骤5):前端清会话由 AuthContext 负责,这里只通知后端 */ |
|
|
/** 退出登录(§4 步骤5):前端清会话由 AuthContext 负责,这里只通知后端 */ |
|
|
@ -140,19 +236,38 @@ export interface CompanyRegisterPayload { |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
/** |
|
|
/** |
|
|
* 企业专用注册(§6):成功直接返回 COMPANY 双 Token。 |
|
|
* 企业专用注册(§6):成功直接返回 COMPANY 双 Token + 身份。 |
|
|
* 客户端不能提交 userId / role / verified / passwordHash(后端生成)。 |
|
|
* 客户端不能提交 userId / role / verified / passwordHash(后端生成)。 |
|
|
*/ |
|
|
*/ |
|
|
export async function registerCompany(payload: CompanyRegisterPayload): Promise<AuthTokens> { |
|
|
export async function registerCompany(payload: CompanyRegisterPayload): Promise<AuthLoginData> { |
|
|
if (!isValidPhone(payload.phone)) { |
|
|
if (!isValidPhone(payload.phone)) { |
|
|
throw new Error('手机号格式不正确'); |
|
|
throw new Error('手机号格式不正确'); |
|
|
} |
|
|
} |
|
|
if (isMockMode()) { |
|
|
if (isMockMode()) { |
|
|
return mockAuthTokens(`company_${payload.phone}`); |
|
|
const mock = mockAuthTokens(`company_${payload.phone}`); |
|
|
|
|
|
return { |
|
|
|
|
|
...mock, |
|
|
|
|
|
userId: 0, |
|
|
|
|
|
phone: payload.phone, |
|
|
|
|
|
nickname: '企业用户', |
|
|
|
|
|
role: 'COMPANY', |
|
|
|
|
|
}; |
|
|
} |
|
|
} |
|
|
return request<AuthTokens>('/auth/company/register', { |
|
|
const raw = await request<{ |
|
|
method: 'POST', |
|
|
userId: number; |
|
|
auth: false, |
|
|
companyId?: number; |
|
|
body: payload, |
|
|
role: BackendRole; |
|
|
}); |
|
|
accessToken: string; |
|
|
|
|
|
refreshToken: string; |
|
|
|
|
|
expiresIn?: number; |
|
|
|
|
|
}>('/auth/company/register', { method: 'POST', auth: false, body: payload }); |
|
|
|
|
|
return { |
|
|
|
|
|
userId: raw.userId, |
|
|
|
|
|
phone: payload.phone, |
|
|
|
|
|
nickname: payload.company.name || '企业用户', |
|
|
|
|
|
role: raw.role, |
|
|
|
|
|
accessToken: raw.accessToken, |
|
|
|
|
|
refreshToken: raw.refreshToken, |
|
|
|
|
|
expiresAt: toExpiresAt(raw.expiresIn), |
|
|
|
|
|
}; |
|
|
} |
|
|
} |
|
|
|