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: {
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