first commit
This commit is contained in:
commit
6e354ba041
109 changed files with 12110 additions and 0 deletions
261
frontend/src/lib/api.ts
Normal file
261
frontend/src/lib/api.ts
Normal file
|
|
@ -0,0 +1,261 @@
|
|||
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' }),
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue