Unify website and manager experience with updated logo and manager entry links, and document the current production topology and URLs in CLAUDE.md for consistent future operations. Made-with: Cursor
263 lines
7.2 KiB
TypeScript
Vendored
263 lines
7.2 KiB
TypeScript
Vendored
import { api } from '@/lib/api'
|
|
import type {
|
|
LoginPayload,
|
|
LoginResponse,
|
|
Login2FAResponse,
|
|
TwoFAPayload,
|
|
RegisterPayload,
|
|
ApiResponse,
|
|
} from './types'
|
|
|
|
const AUTH_BASE_URL = (
|
|
(import.meta.env.VITE_HEICODE_AUTH_BASE_URL as string | undefined) ||
|
|
'https://apimtaiji.azure-api.net/api/mcp'
|
|
).trim()
|
|
|
|
const ACCESS_TOKEN_KEY = 'heicode_access_token'
|
|
const REFRESH_TOKEN_KEY = 'heicode_refresh_token'
|
|
|
|
function readToken(key: string): string {
|
|
if (typeof window === 'undefined') return ''
|
|
return window.localStorage.getItem(key) || ''
|
|
}
|
|
|
|
function writeTokens(accessToken?: string, refreshToken?: string) {
|
|
if (typeof window === 'undefined') return
|
|
if (accessToken) {
|
|
window.localStorage.setItem(ACCESS_TOKEN_KEY, accessToken)
|
|
}
|
|
if (refreshToken) {
|
|
window.localStorage.setItem(REFRESH_TOKEN_KEY, refreshToken)
|
|
}
|
|
}
|
|
|
|
export function clearHeicodeTokens() {
|
|
if (typeof window === 'undefined') return
|
|
window.localStorage.removeItem(ACCESS_TOKEN_KEY)
|
|
window.localStorage.removeItem(REFRESH_TOKEN_KEY)
|
|
}
|
|
|
|
async function callHeicodeAuth<T>(
|
|
path: string,
|
|
init: RequestInit = {},
|
|
useRefreshToken = false
|
|
): Promise<T> {
|
|
const requestId = `heicode-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`
|
|
const token = useRefreshToken
|
|
? readToken(REFRESH_TOKEN_KEY)
|
|
: readToken(ACCESS_TOKEN_KEY)
|
|
const res = await fetch(`${AUTH_BASE_URL}${path}`, {
|
|
...init,
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-Request-Id': requestId,
|
|
...(init.headers || {}),
|
|
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
|
},
|
|
})
|
|
let data: unknown = null
|
|
try {
|
|
data = await res.json()
|
|
} catch {
|
|
data = null
|
|
}
|
|
if (!res.ok) {
|
|
const message =
|
|
(data as { detail?: string; message?: string } | null)?.detail ||
|
|
(data as { detail?: string; message?: string } | null)?.message ||
|
|
`Auth request failed (${res.status})`
|
|
throw new Error(message)
|
|
}
|
|
return data
|
|
}
|
|
|
|
// ============================================================================
|
|
// Authentication APIs
|
|
// ============================================================================
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// Login & Logout
|
|
// ----------------------------------------------------------------------------
|
|
|
|
// User login with username and password
|
|
export async function login(payload: LoginPayload) {
|
|
const res = await callHeicodeAuth<{
|
|
success: boolean
|
|
message?: string
|
|
detail?: string
|
|
data?: {
|
|
token?: string
|
|
refreshToken?: string
|
|
user?: {
|
|
id?: string
|
|
name?: string
|
|
email?: string
|
|
role?: string
|
|
channelId?: string
|
|
}
|
|
}
|
|
}>('/api/auth/login', {
|
|
method: 'POST',
|
|
body: JSON.stringify({
|
|
email: payload.username,
|
|
password: payload.password,
|
|
role: 'user',
|
|
}),
|
|
})
|
|
if (res?.success) {
|
|
writeTokens(res.data?.token, res.data?.refreshToken)
|
|
} else if (res?.detail || res?.message) {
|
|
throw new Error(res.detail || res.message || 'Login failed')
|
|
}
|
|
return {
|
|
success: Boolean(res?.success),
|
|
message: res?.message || res?.detail || '',
|
|
data: {
|
|
id: 1,
|
|
user: res?.data?.user,
|
|
},
|
|
}
|
|
}
|
|
|
|
// Two-factor authentication login
|
|
export async function login2fa(payload: TwoFAPayload) {
|
|
const res = await api.post<Login2FAResponse>('/api/user/login/2fa', payload)
|
|
return res.data
|
|
}
|
|
|
|
// User logout
|
|
export async function logout(): Promise<ApiResponse> {
|
|
try {
|
|
await callHeicodeAuth('/api/auth/logout', { method: 'POST' })
|
|
} finally {
|
|
clearHeicodeTokens()
|
|
}
|
|
return { success: true, message: '' }
|
|
}
|
|
|
|
export async function refreshHeicodeTokenIfNeeded() {
|
|
const refreshToken = readToken(REFRESH_TOKEN_KEY)
|
|
if (!refreshToken) return false
|
|
const res = await callHeicodeAuth<{
|
|
success: boolean
|
|
detail?: string
|
|
data?: { token?: string; refreshToken?: string }
|
|
}>('/api/auth/refresh', { method: 'POST' }, true).catch(() => null)
|
|
if (res?.success) {
|
|
writeTokens(res.data?.token, res.data?.refreshToken)
|
|
return true
|
|
}
|
|
clearHeicodeTokens()
|
|
return false
|
|
}
|
|
|
|
export async function getHeicodeCurrentUser() {
|
|
let me = await callHeicodeAuth<{
|
|
success: boolean
|
|
data?: {
|
|
id?: string
|
|
email?: string
|
|
name?: string
|
|
role?: string
|
|
channelId?: string
|
|
status?: string
|
|
}
|
|
}>('/api/auth/me', { method: 'GET' })
|
|
|
|
if (!me?.success) {
|
|
const refreshed = await refreshHeicodeTokenIfNeeded()
|
|
if (!refreshed) return null
|
|
me = await callHeicodeAuth('/api/auth/me', { method: 'GET' })
|
|
}
|
|
|
|
if (!me?.success || !me.data) return null
|
|
|
|
const roleStr = String(me.data.role || 'user').toLowerCase()
|
|
const role =
|
|
roleStr === 'root' ? 100 : roleStr === 'admin' ? 10 : roleStr === 'user' ? 1 : 1
|
|
|
|
return {
|
|
id: 1,
|
|
username: me.data.email || me.data.name || 'heicode-user',
|
|
display_name: me.data.name || me.data.email || 'Heicode User',
|
|
email: me.data.email || '',
|
|
role,
|
|
group: me.data.channelId || 'default',
|
|
status: me.data.status === 'active' ? 1 : 1,
|
|
}
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// Password Management
|
|
// ----------------------------------------------------------------------------
|
|
|
|
// Send password reset email
|
|
export async function sendPasswordResetEmail(
|
|
email: string,
|
|
turnstile?: string
|
|
): Promise<ApiResponse> {
|
|
const res = await api.get('/api/reset_password', {
|
|
params: { email, turnstile },
|
|
})
|
|
return res.data
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// OAuth
|
|
// ----------------------------------------------------------------------------
|
|
|
|
// Start GitHub OAuth flow
|
|
export async function githubOAuthStart(clientId: string, state: string) {
|
|
const url = `https://github.com/login/oauth/authorize?client_id=${clientId}&state=${state}&scope=user:email`
|
|
window.open(url)
|
|
}
|
|
|
|
// Get OAuth state for CSRF protection
|
|
export async function getOAuthState(): Promise<string> {
|
|
const aff =
|
|
typeof window !== 'undefined' ? (localStorage.getItem('aff') ?? '') : ''
|
|
const res = await api.get('/api/oauth/state', { params: { aff } })
|
|
if (res.data?.success) return res.data.data
|
|
return ''
|
|
}
|
|
|
|
// WeChat login by authorization code
|
|
export async function wechatLoginByCode(code: string): Promise<ApiResponse> {
|
|
const res = await api.get('/api/oauth/wechat', { params: { code } })
|
|
return res.data
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// Registration
|
|
// ----------------------------------------------------------------------------
|
|
|
|
// User registration
|
|
export async function register(payload: RegisterPayload): Promise<ApiResponse> {
|
|
const res = await api.post(`/api/user/register`, payload, {
|
|
params: { turnstile: payload.turnstile ?? '' },
|
|
})
|
|
return res.data
|
|
}
|
|
|
|
// Send email verification code
|
|
export async function sendEmailVerification(
|
|
email: string,
|
|
turnstile?: string
|
|
): Promise<ApiResponse> {
|
|
const res = await api.get('/api/verification', {
|
|
params: { email, turnstile },
|
|
})
|
|
return res.data
|
|
}
|
|
|
|
// Bind email to OAuth account
|
|
export async function bindEmail(
|
|
email: string,
|
|
code: string
|
|
): Promise<ApiResponse> {
|
|
const res = await api.get('/api/oauth/email/bind', {
|
|
params: { email, code },
|
|
})
|
|
return res.data
|
|
}
|