261 lines
6.4 KiB
TypeScript
261 lines
6.4 KiB
TypeScript
const API_BASE = import.meta.env.VITE_API_URL || '/api/v1'
|
|
|
|
export interface User {
|
|
id: number
|
|
email: string
|
|
username: string
|
|
full_name: string
|
|
employee_id: string | null
|
|
phone: string | null
|
|
occupation: string | null
|
|
is_active: boolean
|
|
created_at: string
|
|
updated_at: string
|
|
}
|
|
|
|
export interface AuthTokens {
|
|
access_token: string
|
|
refresh_token: string
|
|
token_type: string
|
|
user: User
|
|
}
|
|
|
|
export interface RegisterPayload {
|
|
email: string
|
|
username: string
|
|
password: string
|
|
full_name: string
|
|
employee_id?: string
|
|
phone?: string
|
|
occupation?: string
|
|
}
|
|
|
|
export interface LoginPayload {
|
|
username_or_email: string
|
|
password: string
|
|
remember_me?: boolean
|
|
}
|
|
|
|
export interface UserUpdatePayload {
|
|
full_name?: string
|
|
employee_id?: string
|
|
phone?: string
|
|
occupation?: string
|
|
}
|
|
|
|
class ApiError extends Error {
|
|
status: number
|
|
|
|
constructor(message: string, status: number) {
|
|
super(message)
|
|
this.status = status
|
|
}
|
|
}
|
|
|
|
function parseErrorMessage(data: unknown, status: number): string {
|
|
if (!data || typeof data !== 'object') {
|
|
if (status >= 500) return 'Server error. Please ensure the backend is running.'
|
|
return 'Something went wrong'
|
|
}
|
|
|
|
const record = data as Record<string, unknown>
|
|
const detail = record.detail
|
|
|
|
if (typeof detail === 'string') return detail
|
|
|
|
if (Array.isArray(detail)) {
|
|
return detail
|
|
.map((item) => {
|
|
if (item && typeof item === 'object' && 'msg' in item) {
|
|
return String((item as { msg: unknown }).msg)
|
|
}
|
|
return JSON.stringify(item)
|
|
})
|
|
.join(', ')
|
|
}
|
|
|
|
if (typeof record.message === 'string') return record.message
|
|
|
|
if (status >= 500) return 'Server error. Please ensure the backend is running.'
|
|
|
|
return 'Something went wrong'
|
|
}
|
|
|
|
async function request<T>(path: string, options: RequestInit = {}): Promise<T> {
|
|
const token = localStorage.getItem('silos_access_token')
|
|
const headers: Record<string, string> = {
|
|
'Content-Type': 'application/json',
|
|
...(options.headers as Record<string, string>),
|
|
}
|
|
|
|
if (token) {
|
|
headers.Authorization = `Bearer ${token}`
|
|
}
|
|
|
|
let response: Response
|
|
try {
|
|
response = await fetch(`${API_BASE}${path}`, {
|
|
...options,
|
|
headers,
|
|
})
|
|
} catch {
|
|
throw new ApiError('Cannot reach the server. Is the backend running on port 8000?', 0)
|
|
}
|
|
|
|
const data = await response.json().catch(() => ({}))
|
|
|
|
if (!response.ok) {
|
|
throw new ApiError(parseErrorMessage(data, response.status), response.status)
|
|
}
|
|
|
|
if (response.status === 204) {
|
|
return undefined as T
|
|
}
|
|
|
|
return data as T
|
|
}
|
|
|
|
export const authApi = {
|
|
register: (payload: RegisterPayload) =>
|
|
request<User>('/auth/register', {
|
|
method: 'POST',
|
|
body: JSON.stringify(payload),
|
|
}),
|
|
|
|
login: (payload: LoginPayload) =>
|
|
request<AuthTokens>('/auth/login', {
|
|
method: 'POST',
|
|
body: JSON.stringify(payload),
|
|
}),
|
|
|
|
me: () => request<User>('/auth/me'),
|
|
|
|
updateMe: (payload: UserUpdatePayload) =>
|
|
request<User>('/auth/me', {
|
|
method: 'PATCH',
|
|
body: JSON.stringify(payload),
|
|
}),
|
|
|
|
forgotPassword: (email: string) =>
|
|
request<{ message: string; reset_token?: string }>('/auth/forgot-password', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ email }),
|
|
}),
|
|
|
|
resetPassword: (token: string, new_password: string) =>
|
|
request<{ message: string }>('/auth/reset-password', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ token, new_password }),
|
|
}),
|
|
}
|
|
|
|
export function saveAuth(tokens: AuthTokens) {
|
|
localStorage.setItem('silos_access_token', tokens.access_token)
|
|
localStorage.setItem('silos_refresh_token', tokens.refresh_token)
|
|
localStorage.setItem('silos_user', JSON.stringify(tokens.user))
|
|
}
|
|
|
|
export function clearAuth() {
|
|
localStorage.removeItem('silos_access_token')
|
|
localStorage.removeItem('silos_refresh_token')
|
|
localStorage.removeItem('silos_user')
|
|
}
|
|
|
|
export function getStoredUser(): User | null {
|
|
const raw = localStorage.getItem('silos_user')
|
|
if (!raw) return null
|
|
try {
|
|
return JSON.parse(raw) as User
|
|
} catch {
|
|
return null
|
|
}
|
|
}
|
|
|
|
export { ApiError }
|
|
|
|
export interface DocumentMember {
|
|
user_id: number
|
|
role: 'editor' | 'viewer'
|
|
name: string
|
|
email: string
|
|
}
|
|
|
|
export interface ApiDocument {
|
|
id: string
|
|
type: 'shed' | 'silo' | 'sketch' | 'stead'
|
|
title: string
|
|
template_id?: string | null
|
|
stead_id?: string | null
|
|
content: Record<string, unknown>
|
|
created_by: string
|
|
created_at: string
|
|
updated_at: string
|
|
starred: boolean
|
|
last_visited_at: string | null
|
|
owner_id?: number | null
|
|
visibility?: 'private' | 'members'
|
|
members?: DocumentMember[]
|
|
}
|
|
|
|
export interface DocumentCreatePayload {
|
|
type: 'shed' | 'silo' | 'sketch' | 'stead'
|
|
title: string
|
|
content?: Record<string, unknown>
|
|
template_id?: string
|
|
stead_id?: string
|
|
}
|
|
|
|
export interface DocumentUpdatePayload {
|
|
title?: string
|
|
content?: Record<string, unknown>
|
|
starred?: boolean
|
|
}
|
|
|
|
export const documentsApi = {
|
|
list: (params?: {
|
|
type?: string
|
|
stead_id?: string
|
|
starred?: boolean
|
|
sort?: 'updated_at' | 'last_visited_at'
|
|
}) => {
|
|
const search = new URLSearchParams()
|
|
if (params?.type) search.set('type', params.type)
|
|
if (params?.stead_id) search.set('stead_id', params.stead_id)
|
|
if (params?.starred !== undefined) search.set('starred', String(params.starred))
|
|
if (params?.sort) search.set('sort', params.sort)
|
|
const qs = search.toString()
|
|
return request<ApiDocument[]>(`/documents${qs ? `?${qs}` : ''}`)
|
|
},
|
|
|
|
search: (q: string) =>
|
|
request<ApiDocument[]>(`/documents/search?q=${encodeURIComponent(q)}`),
|
|
|
|
get: (id: string) => request<ApiDocument>(`/documents/${id}`),
|
|
|
|
create: (payload: DocumentCreatePayload) =>
|
|
request<ApiDocument>('/documents', {
|
|
method: 'POST',
|
|
body: JSON.stringify(payload),
|
|
}),
|
|
|
|
update: (id: string, payload: DocumentUpdatePayload) =>
|
|
request<ApiDocument>(`/documents/${id}`, {
|
|
method: 'PATCH',
|
|
body: JSON.stringify(payload),
|
|
}),
|
|
|
|
visit: (id: string) =>
|
|
request<ApiDocument>(`/documents/${id}/visit`, { method: 'POST' }),
|
|
|
|
share: (id: string, email_or_username: string, role: 'editor' | 'viewer' = 'editor') =>
|
|
request<ApiDocument>(`/documents/${id}/share`, {
|
|
method: 'POST',
|
|
body: JSON.stringify({ email_or_username, role }),
|
|
}),
|
|
|
|
unshare: (id: string, userId: number) =>
|
|
request<ApiDocument>(`/documents/${id}/share/${userId}`, { method: 'DELETE' }),
|
|
|
|
delete: (id: string) =>
|
|
request<void>(`/documents/${id}`, { method: 'DELETE' }),
|
|
}
|