- Stash permalink hash in sessionStorage before login so it survives the auth flow and navigates after login completes - Wait for DOM render (double rAF) before scrolling to target message - Skip scrollToBottom when navigating via permalink - Show error screen for 403 (no access) and 404 (room not found) - Attach HTTP status code to API errors for proper error differentiation Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
132 lines
3.5 KiB
JavaScript
132 lines
3.5 KiB
JavaScript
const API_BASE = '/api'
|
|
|
|
// Global callback for 401 responses (set by app component to trigger auto-logout)
|
|
let onUnauthorized = null
|
|
|
|
export function setOnUnauthorized(callback) {
|
|
onUnauthorized = callback
|
|
}
|
|
|
|
function getToken() {
|
|
return localStorage.getItem('token')
|
|
}
|
|
|
|
function authHeaders() {
|
|
const token = getToken()
|
|
return token ? { Authorization: `Bearer ${token}` } : {}
|
|
}
|
|
|
|
async function request(method, path, body) {
|
|
const opts = {
|
|
method,
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
...authHeaders(),
|
|
},
|
|
}
|
|
|
|
if (body) {
|
|
opts.body = JSON.stringify(body)
|
|
}
|
|
|
|
const res = await fetch(`${API_BASE}${path}`, opts)
|
|
|
|
if (!res.ok) {
|
|
// Auto-logout on 401 for any authenticated request (not login/register)
|
|
if (res.status === 401 && path !== '/auth/login' && path !== '/auth/register') {
|
|
if (onUnauthorized) onUnauthorized()
|
|
throw new Error('Session expired — please log in again')
|
|
}
|
|
const text = await res.text()
|
|
const err = new Error(text || `HTTP ${res.status}`)
|
|
err.status = res.status
|
|
throw err
|
|
}
|
|
|
|
if (res.status === 204 || res.headers.get('content-length') === '0') {
|
|
return null
|
|
}
|
|
|
|
return res.json()
|
|
}
|
|
|
|
export const api = {
|
|
// Auth
|
|
register: (data) => request('POST', '/auth/register', data),
|
|
login: (data) => request('POST', '/auth/login', data),
|
|
me: () => request('GET', '/auth/me'),
|
|
|
|
// Profile
|
|
updateProfile: (data) => request('PUT', '/auth/profile', data),
|
|
uploadAvatar: async (file) => {
|
|
const formData = new FormData()
|
|
formData.append('avatar', file)
|
|
const res = await fetch(`${API_BASE}/auth/avatar`, {
|
|
method: 'POST',
|
|
headers: authHeaders(),
|
|
body: formData,
|
|
})
|
|
if (!res.ok) {
|
|
const text = await res.text()
|
|
throw new Error(text || `HTTP ${res.status}`)
|
|
}
|
|
return res.json()
|
|
},
|
|
deleteAvatar: () => request('DELETE', '/auth/avatar'),
|
|
|
|
// Rooms
|
|
listRooms: () => request('GET', '/rooms'),
|
|
createRoom: (data) => request('POST', '/rooms', data),
|
|
getRoom: (roomId) => request('GET', `/rooms/${roomId}`),
|
|
getMessages: (roomId, limit = 50, before) => {
|
|
const params = new URLSearchParams({ limit: String(limit) })
|
|
if (before) params.set('before', before)
|
|
return request('GET', `/rooms/${roomId}/messages?${params}`)
|
|
},
|
|
joinRoom: (roomId) => request('POST', `/rooms/${roomId}/join`),
|
|
deleteRoom: (roomId) => request('DELETE', `/rooms/${roomId}`),
|
|
clearRoom: (roomId) => request('POST', `/rooms/${roomId}/clear`),
|
|
|
|
// Models
|
|
listModels: () => request('GET', '/models'),
|
|
|
|
// Upload (chat images)
|
|
uploadChatImage: async (file) => {
|
|
const formData = new FormData()
|
|
formData.append('image', file)
|
|
const res = await fetch(`${API_BASE}/upload`, {
|
|
method: 'POST',
|
|
headers: authHeaders(),
|
|
body: formData,
|
|
})
|
|
if (!res.ok) {
|
|
const text = await res.text()
|
|
throw new Error(text || `HTTP ${res.status}`)
|
|
}
|
|
return res.json()
|
|
},
|
|
|
|
// Invites
|
|
createInvite: (data) => request('POST', '/invites', data),
|
|
acceptInvite: (token) => request('POST', `/invites/${token}/accept`),
|
|
}
|
|
|
|
export function saveAuth(token, user) {
|
|
localStorage.setItem('token', token)
|
|
localStorage.setItem('user', JSON.stringify(user))
|
|
}
|
|
|
|
export function getUser() {
|
|
const raw = localStorage.getItem('user')
|
|
return raw ? JSON.parse(raw) : null
|
|
}
|
|
|
|
export function clearAuth() {
|
|
localStorage.removeItem('token')
|
|
localStorage.removeItem('user')
|
|
}
|
|
|
|
export function isAuthenticated() {
|
|
return !!getToken()
|
|
}
|