| 1 | export interface LoginRequest {
|
|---|
| 2 | username: string
|
|---|
| 3 | password: string
|
|---|
| 4 | }
|
|---|
| 5 |
|
|---|
| 6 | export interface SignupRequest {
|
|---|
| 7 | username: string
|
|---|
| 8 | email: string
|
|---|
| 9 | password: string
|
|---|
| 10 | firstName: string
|
|---|
| 11 | lastName: string
|
|---|
| 12 | }
|
|---|
| 13 |
|
|---|
| 14 | export interface AuthResult {
|
|---|
| 15 | token?: string
|
|---|
| 16 | user?: {
|
|---|
| 17 | userId: number
|
|---|
| 18 | username: string
|
|---|
| 19 | email: string
|
|---|
| 20 | firstName: string
|
|---|
| 21 | lastName: string
|
|---|
| 22 | userType: string
|
|---|
| 23 | verified: boolean
|
|---|
| 24 | }
|
|---|
| 25 | message?: string
|
|---|
| 26 | }
|
|---|
| 27 |
|
|---|
| 28 | export interface ChangePasswordRequest {
|
|---|
| 29 | userId: number
|
|---|
| 30 | currentPassword: string
|
|---|
| 31 | newPassword: string
|
|---|
| 32 | }
|
|---|
| 33 |
|
|---|
| 34 | export interface ForgotPasswordRequest {
|
|---|
| 35 | identifier: string
|
|---|
| 36 | }
|
|---|
| 37 |
|
|---|
| 38 | function getBaseUrl(): string {
|
|---|
| 39 | const base = (import.meta.env.VITE_API_BASE_URL as string | undefined) ?? ''
|
|---|
| 40 | return base.replace(/\/$/, '')
|
|---|
| 41 | }
|
|---|
| 42 |
|
|---|
| 43 | function joinUrl(base: string, path: string): string {
|
|---|
| 44 | if (!base) return path
|
|---|
| 45 | return `${base}${path.startsWith('/') ? '' : '/'}${path}`
|
|---|
| 46 | }
|
|---|
| 47 |
|
|---|
| 48 | function extractToken(data: unknown): string | null {
|
|---|
| 49 | if (!data || typeof data !== 'object') return null
|
|---|
| 50 | const rec = data as Record<string, unknown>
|
|---|
| 51 | const candidates = [rec.token, rec.accessToken, rec.jwt, rec.idToken]
|
|---|
| 52 | for (const c of candidates) {
|
|---|
| 53 | if (typeof c === 'string' && c.trim()) return c
|
|---|
| 54 | }
|
|---|
| 55 | // Sometimes APIs wrap: { data: { token: '...' } }
|
|---|
| 56 | const nested = rec.data
|
|---|
| 57 | if (nested && typeof nested === 'object') {
|
|---|
| 58 | const n = nested as Record<string, unknown>
|
|---|
| 59 | const nestedCandidates = [n.token, n.accessToken, n.jwt, n.idToken]
|
|---|
| 60 | for (const c of nestedCandidates) {
|
|---|
| 61 | if (typeof c === 'string' && c.trim()) return c
|
|---|
| 62 | }
|
|---|
| 63 | }
|
|---|
| 64 | return null
|
|---|
| 65 | }
|
|---|
| 66 |
|
|---|
| 67 | async function postJson<T>(path: string, body: unknown, options?: { signal?: AbortSignal }): Promise<T> {
|
|---|
| 68 | const url = joinUrl(getBaseUrl(), path)
|
|---|
| 69 | const res = await fetch(url, {
|
|---|
| 70 | method: 'POST',
|
|---|
| 71 | headers: {
|
|---|
| 72 | 'Content-Type': 'application/json',
|
|---|
| 73 | Accept: 'application/json',
|
|---|
| 74 | },
|
|---|
| 75 | body: JSON.stringify(body),
|
|---|
| 76 | signal: options?.signal,
|
|---|
| 77 | })
|
|---|
| 78 |
|
|---|
| 79 | const text = await res.text()
|
|---|
| 80 | let json: unknown = null
|
|---|
| 81 | try {
|
|---|
| 82 | json = text ? (JSON.parse(text) as unknown) : null
|
|---|
| 83 | } catch {
|
|---|
| 84 | json = null
|
|---|
| 85 | }
|
|---|
| 86 |
|
|---|
| 87 | if (!res.ok) {
|
|---|
| 88 | const message =
|
|---|
| 89 | (json && typeof json === 'object' && typeof (json as any).message === 'string' && (json as any).message) ||
|
|---|
| 90 | text ||
|
|---|
| 91 | `Request failed (${res.status})`
|
|---|
| 92 | throw new Error(message)
|
|---|
| 93 | }
|
|---|
| 94 |
|
|---|
| 95 | return json as T
|
|---|
| 96 | }
|
|---|
| 97 |
|
|---|
| 98 | export async function login(payload: LoginRequest, options?: { signal?: AbortSignal }): Promise<AuthResult> {
|
|---|
| 99 | const data = await postJson<any>('/api/auth/login', payload, options)
|
|---|
| 100 |
|
|---|
| 101 | // Check if there's an error message
|
|---|
| 102 | if (data.message && data.message !== "Login successful") {
|
|---|
| 103 | throw new Error(data.message)
|
|---|
| 104 | }
|
|---|
| 105 |
|
|---|
| 106 | // Extract user information from the response
|
|---|
| 107 | if (data.userId && data.username) {
|
|---|
| 108 | return {
|
|---|
| 109 | user: {
|
|---|
| 110 | userId: data.userId,
|
|---|
| 111 | username: data.username,
|
|---|
| 112 | email: data.email,
|
|---|
| 113 | firstName: data.firstName,
|
|---|
| 114 | lastName: data.lastName,
|
|---|
| 115 | userType: data.userType,
|
|---|
| 116 | verified: data.verified || false // Use verified status from API response
|
|---|
| 117 | },
|
|---|
| 118 | message: data.message
|
|---|
| 119 | }
|
|---|
| 120 | }
|
|---|
| 121 |
|
|---|
| 122 | // If we don't get user data, something went wrong
|
|---|
| 123 | throw new Error('Login failed: Invalid response from server')
|
|---|
| 124 | }
|
|---|
| 125 |
|
|---|
| 126 | export async function signup(payload: SignupRequest, options?: { signal?: AbortSignal }): Promise<AuthResult> {
|
|---|
| 127 | const data = await postJson<any>('/api/auth/signup', payload, options)
|
|---|
| 128 |
|
|---|
| 129 | // Check if there's an error message
|
|---|
| 130 | if (data.message && data.message !== "User registered successfully") {
|
|---|
| 131 | throw new Error(data.message)
|
|---|
| 132 | }
|
|---|
| 133 |
|
|---|
| 134 | // Extract user information from the response if successful
|
|---|
| 135 | if (data.userId && data.username) {
|
|---|
| 136 | return {
|
|---|
| 137 | user: {
|
|---|
| 138 | userId: data.userId,
|
|---|
| 139 | username: data.username,
|
|---|
| 140 | email: data.email,
|
|---|
| 141 | firstName: data.firstName,
|
|---|
| 142 | lastName: data.lastName,
|
|---|
| 143 | userType: data.userType,
|
|---|
| 144 | verified: data.verified || false // Use verified status from API response
|
|---|
| 145 | },
|
|---|
| 146 | message: data.message
|
|---|
| 147 | }
|
|---|
| 148 | }
|
|---|
| 149 |
|
|---|
| 150 | // If registration was successful but no user data returned, that's okay
|
|---|
| 151 | return { message: data.message || "Registration successful" }
|
|---|
| 152 | }
|
|---|
| 153 |
|
|---|
| 154 | export async function changePassword(payload: ChangePasswordRequest, options?: { signal?: AbortSignal }): Promise<void> {
|
|---|
| 155 | await postJson<{ message?: string }>('/api/auth/change-password', payload, options)
|
|---|
| 156 | }
|
|---|
| 157 |
|
|---|
| 158 | export async function forgotPassword(payload: ForgotPasswordRequest, options?: { signal?: AbortSignal }): Promise<string> {
|
|---|
| 159 | const data = await postJson<{ message?: string }>('/api/auth/forgot-password', payload, options)
|
|---|
| 160 | return data.message || 'If an account matches that username or email, a temporary password has been sent.'
|
|---|
| 161 | }
|
|---|