Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
138 changes: 138 additions & 0 deletions frontend/src/entities/user/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import type { AuthTokens, User, UserApis } from '.'

import { ApiError, createApiClient, getApiAccessToken } from '@/shared/api'
import type { ApiClient, ApiClientOptions } from '@/shared/api'

interface BackendUser {
id: number
email: string
nickname: string | null
email_verified_at: string | null
status: number
}

interface BackendAuthTokens {
access_token: string
refresh_token: string
user: BackendUser
}

export interface CreateUserApisOptions extends ApiClientOptions {
client?: ApiClient
}

function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value)
}

function invalidResponse(data: unknown): never {
throw new ApiError('用户认证响应格式无效', { kind: 'invalid-response', data })
}

function toUser(raw: unknown): User {
if (
!isRecord(raw) ||
typeof raw.id !== 'number' ||
typeof raw.email !== 'string' ||
(typeof raw.nickname !== 'string' && raw.nickname !== null) ||
(typeof raw.email_verified_at !== 'string' && raw.email_verified_at !== null) ||
(raw.status !== 0 && raw.status !== 1)
) {
invalidResponse(raw)
}

return {
id: raw.id,
email: raw.email,
nickname: raw.nickname,
emailVerifiedAt: raw.email_verified_at,
status: raw.status === 1 ? 'banned' : 'normal',
}
}

function toAuthTokens(raw: unknown): AuthTokens {
if (
!isRecord(raw) ||
typeof raw.access_token !== 'string' ||
typeof raw.refresh_token !== 'string' ||
!isRecord(raw.user)
) {
invalidResponse(raw)
}

return {
accessToken: raw.access_token,
refreshToken: raw.refresh_token,
user: toUser(raw.user),
}
}

export function createUserApis(options: CreateUserApisOptions = {}): UserApis {
const { client, ...clientOptions } = options
const apiClient =
client ??
createApiClient({
...clientOptions,
getAccessToken: clientOptions.getAccessToken ?? getApiAccessToken,
})

return {
async sendCode(input): Promise<void> {
await apiClient.request<null>('/auth/send-code', { method: 'POST', json: input })
},

async register(input): Promise<AuthTokens> {
return toAuthTokens(
await apiClient.request<BackendAuthTokens>('/auth/register', {
method: 'POST',
json: input,
}),
)
},

async login(input): Promise<AuthTokens> {
return toAuthTokens(
await apiClient.request<BackendAuthTokens>('/auth/login', {
method: 'POST',
json: input,
}),
)
},

async loginByCode(input): Promise<AuthTokens> {
return toAuthTokens(
await apiClient.request<BackendAuthTokens>('/auth/login-by-code', {
method: 'POST',
json: input,
}),
)
},

async refresh(refreshToken): Promise<AuthTokens> {
return toAuthTokens(
await apiClient.request<BackendAuthTokens>('/auth/refresh', {
method: 'POST',
json: { refresh_token: refreshToken },
}),
)
},

async logout(refreshToken): Promise<void> {
await apiClient.request<null>('/auth/logout', {
method: 'POST',
json: { refresh_token: refreshToken },
})
},

async me(): Promise<User> {
return toUser(await apiClient.request<BackendUser>('/auth/me'))
},

async changePassword(input): Promise<void> {
await apiClient.request<null>('/auth/change-password', {
method: 'POST',
json: { old_password: input.oldPassword, new_password: input.newPassword },
})
},
}
}
35 changes: 35 additions & 0 deletions frontend/src/entities/user/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/** 已认证用户的前端领域表示。 */
export interface User {
id: number
email: string
nickname: string | null
emailVerifiedAt: string | null
status: 'normal' | 'banned'
}

/** 一次认证成功后由后端签发的访问与刷新令牌。 */
export interface AuthTokens {
accessToken: string
refreshToken: string
user: User
}

/** 用户认证与账户设置的后端接口。 */
export interface UserApis {
sendCode(input: {
email: string
purpose: 'login' | 'register' | 'reset_password'
}): Promise<void>
register(input: {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这里把认证模型定义成了 access/refresh token + code 登录/注册参数,但仓库现有后端 UserService 仍然是 session_token 语义,RegisterInput / LoginByPasswordInput 也不包含 code。按当前代码库状态,这个前端契约不会和后端对得上。

email: string
password: string
code: string
nickname?: string
}): Promise<AuthTokens>
login(input: { email: string; password: string; code: string }): Promise<AuthTokens>
loginByCode(input: { email: string; code: string }): Promise<AuthTokens>
refresh(refreshToken: string): Promise<AuthTokens>
logout(refreshToken: string): Promise<void>
me(): Promise<User>
changePassword(input: { oldPassword: string; newPassword: string }): Promise<void>
}
Loading
Loading