增加“验证码登录”模式
This commit is contained in:
@@ -21,6 +21,7 @@ import {
|
|||||||
markWorkerOrderRead,
|
markWorkerOrderRead,
|
||||||
getWorkerMyOrderEvents,
|
getWorkerMyOrderEvents,
|
||||||
loginWorker,
|
loginWorker,
|
||||||
|
loginWorkerBySmsCode,
|
||||||
getWorkerSessions,
|
getWorkerSessions,
|
||||||
deleteWorkerSession,
|
deleteWorkerSession,
|
||||||
manageWorkerSessions,
|
manageWorkerSessions,
|
||||||
@@ -28,6 +29,7 @@ import {
|
|||||||
logoutWorkerSession,
|
logoutWorkerSession,
|
||||||
registerWorker,
|
registerWorker,
|
||||||
sendWorkerSmsCode,
|
sendWorkerSmsCode,
|
||||||
|
sendWorkerLoginSmsCode,
|
||||||
sendWorkerPasswordResetSmsCode,
|
sendWorkerPasswordResetSmsCode,
|
||||||
resetWorkerPassword,
|
resetWorkerPassword,
|
||||||
saveWorkerOrderAcceptanceDraft,
|
saveWorkerOrderAcceptanceDraft,
|
||||||
@@ -133,6 +135,46 @@ router.post(
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
'/auth/login/sms-code',
|
||||||
|
createRateLimitMiddleware({
|
||||||
|
scope: 'worker:login-sms-code',
|
||||||
|
windowMs: 60_000,
|
||||||
|
max: 5,
|
||||||
|
key: getBodyFieldRateLimitKey('phone'),
|
||||||
|
}),
|
||||||
|
createRouteHandler((req) => sendWorkerLoginSmsCode(req.body || {}), {
|
||||||
|
successMessage: '验证码已发送',
|
||||||
|
errorMessage: '验证码发送失败',
|
||||||
|
scope: '[worker/auth/login/sms-code]',
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
'/auth/login/sms',
|
||||||
|
createRateLimitMiddleware({
|
||||||
|
scope: 'worker:login-sms',
|
||||||
|
windowMs: 60_000,
|
||||||
|
max: 10,
|
||||||
|
key: getBodyFieldRateLimitKey('phone'),
|
||||||
|
}),
|
||||||
|
createRouteHandler(
|
||||||
|
(req) =>
|
||||||
|
loginWorkerBySmsCode(req.body?.phone, req.body?.code, {
|
||||||
|
deviceId: req.body?.deviceId,
|
||||||
|
deviceType: req.body?.deviceType,
|
||||||
|
deviceName: req.body?.deviceName,
|
||||||
|
userAgent: req.headers['user-agent'],
|
||||||
|
ipAddress: req.ip,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
successMessage: '登录成功',
|
||||||
|
errorMessage: '接单端验证码登录失败',
|
||||||
|
scope: '[worker/auth/login/sms]',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
router.post(
|
router.post(
|
||||||
'/auth/devices/manage/list',
|
'/auth/devices/manage/list',
|
||||||
createRateLimitMiddleware({
|
createRateLimitMiddleware({
|
||||||
|
|||||||
@@ -287,6 +287,52 @@ export async function sendWorkerPasswordResetSmsCode(payload: JsonObject = {}) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function sendWorkerLoginSmsCode(payload: JsonObject = {}) {
|
||||||
|
const phone = normalizeWorkerPhone(payload.phone)
|
||||||
|
const config = runtimeConfig.worker.sms
|
||||||
|
const cooldownMs = Math.max(10, Number(config.cooldownSeconds || 60)) * 1000
|
||||||
|
const dailyLimit = Math.max(1, Number(config.dailyLimit || 10))
|
||||||
|
const ttlSeconds = Math.max(60, Number(config.codeTtlSeconds || 300))
|
||||||
|
if (!(await getWorkerUserByPhone(phone))) {
|
||||||
|
throw createHttpError('该手机号未注册', {
|
||||||
|
statusCode: 404,
|
||||||
|
errorCode: 'worker_phone_not_registered',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const latest = await getLatestWorkerSmsCode(phone, 'login')
|
||||||
|
if (
|
||||||
|
latest &&
|
||||||
|
!latest.consumed_at &&
|
||||||
|
new Date(latest.created_at).getTime() > Date.now() - cooldownMs
|
||||||
|
) {
|
||||||
|
throw createHttpError('验证码发送过于频繁,请稍后再试', {
|
||||||
|
statusCode: 429,
|
||||||
|
errorCode: 'worker_sms_cooldown',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const dayRange = resolveChinaDayRange()
|
||||||
|
if ((await countWorkerSmsCodesOnDay(phone, dayRange.start, dayRange.end)) >= dailyLimit) {
|
||||||
|
throw createHttpError('今日验证码发送次数已达上限,请明天再试', {
|
||||||
|
statusCode: 429,
|
||||||
|
errorCode: 'worker_sms_daily_limit',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const code = generateSmsCode()
|
||||||
|
await getSmsProvider().sendCode(phone, code)
|
||||||
|
await createWorkerSmsCode({
|
||||||
|
phone,
|
||||||
|
purpose: 'login',
|
||||||
|
codeHash: hashSmsCode(code),
|
||||||
|
expiresAt: new Date(Date.now() + ttlSeconds * 1000).toISOString(),
|
||||||
|
now: nowIso(),
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
sent: true,
|
||||||
|
expiresInSeconds: ttlSeconds,
|
||||||
|
provider: String(runtimeConfig.worker.sms.provider || 'mock'),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function resetWorkerPassword(payload: JsonObject = {}) {
|
export async function resetWorkerPassword(payload: JsonObject = {}) {
|
||||||
const phone = normalizeWorkerPhone(payload.phone)
|
const phone = normalizeWorkerPhone(payload.phone)
|
||||||
const code = String(payload.code || payload.smsCode || '').trim()
|
const code = String(payload.code || payload.smsCode || '').trim()
|
||||||
@@ -522,6 +568,77 @@ export async function loginWorker(
|
|||||||
const normalizedPassword = normalizePassword(password)
|
const normalizedPassword = normalizePassword(password)
|
||||||
const worker = await authenticateWorkerCredentials(username, password)
|
const worker = await authenticateWorkerCredentials(username, password)
|
||||||
|
|
||||||
|
return createWorkerLoginSession(worker, deviceInfo, !isWorkerPasswordStrong(normalizedPassword))
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loginWorkerBySmsCode(
|
||||||
|
phoneValue: unknown,
|
||||||
|
codeValue: unknown,
|
||||||
|
deviceInfo: {
|
||||||
|
deviceId?: unknown
|
||||||
|
deviceType?: unknown
|
||||||
|
deviceName?: unknown
|
||||||
|
userAgent?: unknown
|
||||||
|
ipAddress?: unknown
|
||||||
|
} = {},
|
||||||
|
) {
|
||||||
|
ensureWorkerAuthConfigured()
|
||||||
|
const phone = normalizeWorkerPhone(phoneValue)
|
||||||
|
const code = String(codeValue || '').trim()
|
||||||
|
if (!code) {
|
||||||
|
throw createHttpError('请输入短信验证码', {
|
||||||
|
statusCode: 400,
|
||||||
|
errorCode: 'worker_sms_code_required',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const worker = await getWorkerUserByPhone(phone)
|
||||||
|
const latest = await getLatestWorkerSmsCode(phone, 'login')
|
||||||
|
const codeValid = Boolean(
|
||||||
|
latest &&
|
||||||
|
!latest.consumed_at &&
|
||||||
|
new Date(latest.expires_at).getTime() > Date.now() &&
|
||||||
|
latest.code_hash === hashSmsCode(code),
|
||||||
|
)
|
||||||
|
if (!worker || !codeValid) {
|
||||||
|
if (latest && !latest.consumed_at) {
|
||||||
|
const failed = await recordWorkerSmsCodeFailure(
|
||||||
|
latest.id,
|
||||||
|
nowIso(),
|
||||||
|
WORKER_SMS_MAX_VERIFY_ATTEMPTS,
|
||||||
|
)
|
||||||
|
if (failed?.consumed_at) {
|
||||||
|
throw createHttpError('验证码错误次数过多,请重新获取验证码', {
|
||||||
|
statusCode: 400,
|
||||||
|
errorCode: 'worker_sms_code_locked',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw createHttpError('手机号或验证码错误', {
|
||||||
|
statusCode: 401,
|
||||||
|
errorCode: 'worker_sms_code_invalid',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (!(await consumeWorkerSmsCode(latest!.id, nowIso()))) {
|
||||||
|
throw createHttpError('验证码已失效,请重新获取', {
|
||||||
|
statusCode: 400,
|
||||||
|
errorCode: 'worker_sms_code_invalid',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
assertWorkerLoginAllowed(worker)
|
||||||
|
return createWorkerLoginSession(worker, deviceInfo, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createWorkerLoginSession(
|
||||||
|
worker: WorkerUserRow,
|
||||||
|
deviceInfo: {
|
||||||
|
deviceId?: unknown
|
||||||
|
deviceType?: unknown
|
||||||
|
deviceName?: unknown
|
||||||
|
userAgent?: unknown
|
||||||
|
ipAddress?: unknown
|
||||||
|
},
|
||||||
|
passwordWeak: boolean,
|
||||||
|
) {
|
||||||
const session = createWorkerSession(worker)
|
const session = createWorkerSession(worker)
|
||||||
const deviceType = normalizeWorkerDeviceType(deviceInfo.deviceType)
|
const deviceType = normalizeWorkerDeviceType(deviceInfo.deviceType)
|
||||||
const deviceId = normalizeWorkerDeviceId(deviceInfo.deviceId, session.sessionId)
|
const deviceId = normalizeWorkerDeviceId(deviceInfo.deviceId, session.sessionId)
|
||||||
@@ -551,7 +668,7 @@ export async function loginWorker(
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
...session,
|
...session,
|
||||||
passwordWeak: !isWorkerPasswordStrong(normalizedPassword),
|
passwordWeak,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,18 +7,32 @@ import {
|
|||||||
ThunderboltOutlined,
|
ThunderboltOutlined,
|
||||||
UserOutlined,
|
UserOutlined,
|
||||||
} from '@ant-design/icons'
|
} from '@ant-design/icons'
|
||||||
import { Alert, App, Button, Form, Input, Modal, Space, Table, Tabs, Typography } from 'antd'
|
import {
|
||||||
|
Alert,
|
||||||
|
App,
|
||||||
|
Button,
|
||||||
|
Form,
|
||||||
|
Input,
|
||||||
|
Modal,
|
||||||
|
Segmented,
|
||||||
|
Space,
|
||||||
|
Table,
|
||||||
|
Tabs,
|
||||||
|
Typography,
|
||||||
|
} from 'antd'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { Navigate, useNavigate } from 'react-router'
|
import { Navigate, useNavigate } from 'react-router'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
deleteWorkerSessionWithCredentials,
|
deleteWorkerSessionWithCredentials,
|
||||||
loginWorker,
|
loginWorker,
|
||||||
|
loginWorkerBySmsCode,
|
||||||
manageWorkerSessions,
|
manageWorkerSessions,
|
||||||
registerWorker,
|
registerWorker,
|
||||||
resetWorkerPassword,
|
resetWorkerPassword,
|
||||||
sendWorkerSmsCode,
|
sendWorkerSmsCode,
|
||||||
sendWorkerPasswordResetSmsCode,
|
sendWorkerPasswordResetSmsCode,
|
||||||
|
sendWorkerLoginSmsCode,
|
||||||
} from '@/services/worker'
|
} from '@/services/worker'
|
||||||
import type { WorkerSessionDevice } from '@/types/worker-platform'
|
import type { WorkerSessionDevice } from '@/types/worker-platform'
|
||||||
import { hasWorkerSession, setWorkerSession } from '@/utils/worker-auth'
|
import { hasWorkerSession, setWorkerSession } from '@/utils/worker-auth'
|
||||||
@@ -49,9 +63,12 @@ export default function WorkerLoginPage() {
|
|||||||
const { message, modal } = App.useApp()
|
const { message, modal } = App.useApp()
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [registerForm] = Form.useForm()
|
const [registerForm] = Form.useForm()
|
||||||
|
const [loginSmsForm] = Form.useForm<{ phone: string; code: string }>()
|
||||||
const [resetForm] = Form.useForm<{ phone: string; code: string; password: string }>()
|
const [resetForm] = Form.useForm<{ phone: string; code: string; password: string }>()
|
||||||
const [deviceForm] = Form.useForm<{ username: string; password: string }>()
|
const [deviceForm] = Form.useForm<{ username: string; password: string }>()
|
||||||
const [countdown, setCountdown] = useState(0)
|
const [countdown, setCountdown] = useState(0)
|
||||||
|
const [loginSmsCountdown, setLoginSmsCountdown] = useState(0)
|
||||||
|
const [loginMode, setLoginMode] = useState<'password' | 'sms'>('password')
|
||||||
const [resetCountdown, setResetCountdown] = useState(0)
|
const [resetCountdown, setResetCountdown] = useState(0)
|
||||||
const [forgotPasswordOpen, setForgotPasswordOpen] = useState(false)
|
const [forgotPasswordOpen, setForgotPasswordOpen] = useState(false)
|
||||||
const [deviceManagerOpen, setDeviceManagerOpen] = useState(false)
|
const [deviceManagerOpen, setDeviceManagerOpen] = useState(false)
|
||||||
@@ -64,6 +81,12 @@ export default function WorkerLoginPage() {
|
|||||||
return () => window.clearInterval(timer)
|
return () => window.clearInterval(timer)
|
||||||
}, [countdown])
|
}, [countdown])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (loginSmsCountdown <= 0) return
|
||||||
|
const timer = window.setInterval(() => setLoginSmsCountdown((value) => value - 1), 1000)
|
||||||
|
return () => window.clearInterval(timer)
|
||||||
|
}, [loginSmsCountdown])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (resetCountdown <= 0) return
|
if (resetCountdown <= 0) return
|
||||||
const timer = window.setInterval(() => setResetCountdown((value) => value - 1), 1000)
|
const timer = window.setInterval(() => setResetCountdown((value) => value - 1), 1000)
|
||||||
@@ -105,6 +128,42 @@ export default function WorkerLoginPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function requestLoginSmsCode() {
|
||||||
|
const phone = String(loginSmsForm.getFieldValue('phone') || '').trim()
|
||||||
|
if (!/^1[3-9]\d{9}$/.test(phone)) {
|
||||||
|
message.error('请先填写正确的手机号')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setLoading(true)
|
||||||
|
try {
|
||||||
|
const response = await sendWorkerLoginSmsCode({ phone })
|
||||||
|
setLoginSmsCountdown(SMS_COUNTDOWN_SECONDS)
|
||||||
|
message.success(
|
||||||
|
response.data.provider === 'mock'
|
||||||
|
? '验证码已发送(开发模式,验证码见服务日志)'
|
||||||
|
: `验证码已发送,${response.data.expiresInSeconds} 秒内有效`,
|
||||||
|
)
|
||||||
|
} catch (error) {
|
||||||
|
message.error(error instanceof Error ? error.message : '验证码发送失败')
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitSmsLogin(values: { phone: string; code: string }) {
|
||||||
|
setLoading(true)
|
||||||
|
try {
|
||||||
|
const response = await loginWorkerBySmsCode(values)
|
||||||
|
setWorkerSession(response.data.token, response.data.expiresAt, response.data.worker)
|
||||||
|
message.success('登录成功')
|
||||||
|
void navigate('/hall', { replace: true })
|
||||||
|
} catch (error) {
|
||||||
|
message.error(error instanceof Error ? error.message : '验证码登录失败')
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function loadManagedDevices(values?: { username: string; password: string }) {
|
async function loadManagedDevices(values?: { username: string; password: string }) {
|
||||||
const credentials = values || deviceForm.getFieldsValue()
|
const credentials = values || deviceForm.getFieldsValue()
|
||||||
if (!credentials.username || !credentials.password) {
|
if (!credentials.username || !credentials.password) {
|
||||||
@@ -275,51 +334,116 @@ export default function WorkerLoginPage() {
|
|||||||
<>
|
<>
|
||||||
<div className="auth-form-header">
|
<div className="auth-form-header">
|
||||||
<h1>欢迎回来</h1>
|
<h1>欢迎回来</h1>
|
||||||
<p>使用手机号、昵称或账号登录,进入抢单大厅</p>
|
<p>使用密码或短信验证码登录,进入抢单大厅</p>
|
||||||
</div>
|
</div>
|
||||||
<Form layout="vertical" onFinish={submitLogin} requiredMark={false}>
|
<Segmented
|
||||||
<Form.Item
|
block
|
||||||
label="手机号 / 昵称 / 账号"
|
value={loginMode}
|
||||||
name="username"
|
options={[
|
||||||
rules={[{ required: true, message: '请输入手机号、昵称或账号' }]}
|
{ label: '密码登录', value: 'password' },
|
||||||
|
{ label: '验证码登录', value: 'sms' },
|
||||||
|
]}
|
||||||
|
onChange={(value) => setLoginMode(value as 'password' | 'sms')}
|
||||||
|
style={{ marginBottom: 20 }}
|
||||||
|
/>
|
||||||
|
{loginMode === 'password' ? (
|
||||||
|
<Form layout="vertical" onFinish={submitLogin} requiredMark={false}>
|
||||||
|
<Form.Item
|
||||||
|
label="手机号 / 昵称 / 账号"
|
||||||
|
name="username"
|
||||||
|
rules={[{ required: true, message: '请输入手机号、昵称或账号' }]}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
prefix={<UserOutlined />}
|
||||||
|
autoComplete="username"
|
||||||
|
placeholder="手机号 / 昵称 / 账号"
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label="密码"
|
||||||
|
name="password"
|
||||||
|
rules={[{ required: true, message: '请输入密码' }]}
|
||||||
|
>
|
||||||
|
<Input.Password
|
||||||
|
prefix={<LockOutlined />}
|
||||||
|
autoComplete="current-password"
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
htmlType="submit"
|
||||||
|
loading={loading}
|
||||||
|
size="large"
|
||||||
|
block
|
||||||
|
>
|
||||||
|
登录
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="link"
|
||||||
|
icon={<GlobalOutlined />}
|
||||||
|
block
|
||||||
|
onClick={() => setDeviceManagerOpen(true)}
|
||||||
|
>
|
||||||
|
登录设备管理
|
||||||
|
</Button>
|
||||||
|
<Button type="link" block onClick={() => setForgotPasswordOpen(true)}>
|
||||||
|
忘记密码?
|
||||||
|
</Button>
|
||||||
|
</Form>
|
||||||
|
) : (
|
||||||
|
<Form
|
||||||
|
form={loginSmsForm}
|
||||||
|
layout="vertical"
|
||||||
|
onFinish={submitSmsLogin}
|
||||||
|
requiredMark={false}
|
||||||
>
|
>
|
||||||
<Input
|
<Form.Item
|
||||||
prefix={<UserOutlined />}
|
label="手机号"
|
||||||
autoComplete="username"
|
name="phone"
|
||||||
placeholder="手机号 / 昵称 / 账号"
|
rules={[
|
||||||
/>
|
{ required: true, message: '请输入手机号' },
|
||||||
</Form.Item>
|
{ pattern: /^1[3-9]\d{9}$/, message: '手机号格式不正确' },
|
||||||
<Form.Item
|
]}
|
||||||
label="密码"
|
>
|
||||||
name="password"
|
<Input
|
||||||
rules={[{ required: true, message: '请输入密码' }]}
|
prefix={<MobileOutlined />}
|
||||||
>
|
maxLength={11}
|
||||||
<Input.Password
|
placeholder="注册时使用的手机号"
|
||||||
prefix={<LockOutlined />}
|
/>
|
||||||
autoComplete="current-password"
|
</Form.Item>
|
||||||
/>
|
<Form.Item
|
||||||
</Form.Item>
|
label="短信验证码"
|
||||||
<Button
|
name="code"
|
||||||
type="primary"
|
rules={[{ required: true, message: '请输入短信验证码' }]}
|
||||||
htmlType="submit"
|
>
|
||||||
loading={loading}
|
<Space.Compact style={{ width: '100%' }}>
|
||||||
size="large"
|
<Input
|
||||||
block
|
prefix={<SafetyCertificateOutlined />}
|
||||||
>
|
maxLength={6}
|
||||||
登录
|
placeholder="6 位数字验证码"
|
||||||
</Button>
|
/>
|
||||||
<Button
|
<Button
|
||||||
type="link"
|
loading={loading}
|
||||||
icon={<GlobalOutlined />}
|
disabled={loginSmsCountdown > 0}
|
||||||
block
|
onClick={requestLoginSmsCode}
|
||||||
onClick={() => setDeviceManagerOpen(true)}
|
>
|
||||||
>
|
{loginSmsCountdown > 0
|
||||||
登录设备管理
|
? `${loginSmsCountdown}s 后重发`
|
||||||
</Button>
|
: '获取验证码'}
|
||||||
<Button type="link" block onClick={() => setForgotPasswordOpen(true)}>
|
</Button>
|
||||||
忘记密码?
|
</Space.Compact>
|
||||||
</Button>
|
</Form.Item>
|
||||||
</Form>
|
<Button
|
||||||
|
type="primary"
|
||||||
|
htmlType="submit"
|
||||||
|
loading={loading}
|
||||||
|
size="large"
|
||||||
|
block
|
||||||
|
>
|
||||||
|
验证码登录
|
||||||
|
</Button>
|
||||||
|
</Form>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -64,6 +64,30 @@ export function loginWorker(payload: { username: string; password: string }) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildWorkerDevicePayload() {
|
||||||
|
const userAgent = navigator.userAgent
|
||||||
|
const isMobile = /Android|iPhone|iPad|iPod|Mobile/i.test(userAgent)
|
||||||
|
return {
|
||||||
|
deviceId: getWorkerDeviceId(),
|
||||||
|
deviceType: isMobile ? 'mobile' : 'pc',
|
||||||
|
deviceName: isMobile ? '手机浏览器' : 'PC 浏览器',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sendWorkerLoginSmsCode(payload: { phone: string }) {
|
||||||
|
return apiPost<{ sent: boolean; expiresInSeconds: number; provider: string }>(
|
||||||
|
'/api/v1/worker/auth/login/sms-code',
|
||||||
|
payload,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function loginWorkerBySmsCode(payload: { phone: string; code: string }) {
|
||||||
|
return apiPost<WorkerLoginResponse>('/api/v1/worker/auth/login/sms', {
|
||||||
|
...payload,
|
||||||
|
...buildWorkerDevicePayload(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export function fetchWorkerSessions() {
|
export function fetchWorkerSessions() {
|
||||||
return apiGet<{ maxDevices: number; items: WorkerSessionDevice[] }>('/api/v1/worker/auth/devices')
|
return apiGet<{ maxDevices: number; items: WorkerSessionDevice[] }>('/api/v1/worker/auth/devices')
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user