import axios from 'axios' const ACCESS = 'drive_access', REFRESH = 'drive_refresh' // Single origin for JSON, previews, videos, and token renewal. export const api = axios.create({ baseURL: import.meta.env?.PROD ? '/api' : `http://${window.location.hostname}:1029/api`, headers: { Accept: 'application/json' }, }); const raw = axios.create({baseURL:'/api', headers:{Accept:'application/json'}}) // Public links must remain accessible without a session, even if a browser // has a previously expired access token stored locally. const publicApi = axios.create({ baseURL: api.defaults.baseURL, headers: { Accept: 'application/json' }, }) export const savedToken = () => sessionStorage.getItem(ACCESS) export const storeTokens = data => { if (data.access) sessionStorage.setItem(ACCESS,data.access) if (data.refresh) sessionStorage.setItem(REFRESH,data.refresh) } export const logoutTokens = () => { sessionStorage.removeItem(ACCESS) sessionStorage.removeItem(REFRESH) } api.interceptors.request.use(config => { const token = savedToken() if (token && !config.url?.startsWith('/invitations/')) { config.headers.Authorization = `Bearer ${token}` } return config }) let refreshPromise = null export async function refreshAccess() { const token = sessionStorage.getItem(REFRESH) if (!token) throw new Error('Aucun jeton de renouvellement') if (!refreshPromise) { refreshPromise = raw.post('/auth/refresh/',{refresh:token}).then(response => { storeTokens(response.data) return response.data.access }).finally(() => { refreshPromise = null }) } return refreshPromise } const authError = error => error?.response?.status === 401 api.interceptors.response.use(response => response, async error => { const original = error.config if (error.response?.status !== 401 || !original || original._retry || original.url?.startsWith('/auth/') || original.url?.startsWith('/invitations/')) { return Promise.reject(error) } original._retry = true try { const access = await refreshAccess() original.headers.Authorization = `Bearer ${access}` return await api(original) } catch (refreshError) { // Temporary network outages and server 502/503 must NEVER log out a user. // Only a definitively rejected/expired refresh token revokes the session. if (authError(refreshError)) { logoutTokens() window.dispatchEvent(new Event('drive:logout')) } return Promise.reject(refreshError) } }) export const drive={ list:params=>api.get('/drive/items/',{params}).then(r=>r.data), shareUsers:q=>api.get('/drive/shares/users/search/',{params:{q}}).then(r=>r.data.users), itemShares:id=>api.get(`/drive/items/${id}/shares/`).then(r=>r.data), shareItem:(id,user_id,permissions)=>api.post(`/drive/items/${id}/shares/`,{user_id,permissions}).then(r=>r.data), shareItemPermissions:(id,grant_id,permissions)=>api.patch(`/drive/items/${id}/shares/${grant_id}/permissions/`,{permissions}).then(r=>r.data), unshareItem:(id,grant_id)=>api.delete(`/drive/items/${id}/shares/${grant_id}/`), leaveItemShare:id=>api.delete(`/drive/items/${id}/my-share/`), publicItem:id=>api.post(`/drive/items/${id}/public-links/`).then(r=>r.data), revokePublic:(id,link_id)=>api.delete(`/drive/items/${id}/public-links/${link_id}/`), publicBrowse:(token,parent)=>publicApi.get(`/public/${encodeURIComponent(token)}/`,{params:parent?{parent}:{}}).then(r=>r.data), adminUsers:q=>api.get('/drive/admin/users/',{params:{q}}).then(r=>r.data), adminDeleteUser:(id,confirm_username)=>api.delete(`/drive/admin/users/${id}/`,{data:{confirm_username}}), adminUserAccess:id=>api.get(`/drive/admin/users/${id}/access/`).then(r=>r.data.accesses), revokeUserAccess:(id,kind,spaceId)=>api.delete(`/drive/admin/users/${id}/access/${kind}/${spaceId}/`), updateUserAccess:(id,kind,spaceId,permissions)=>api.patch(`/drive/admin/users/${id}/access/${kind}/${spaceId}/`,{permissions}).then(r=>r.data), stats:params=>api.get('/drive/stats/',{params}).then(r=>r.data), folder:(name,parent,space)=>api.post('/drive/folders/',{name,parent,space}).then(r=>r.data), createFile:(name,type,parent,space)=>api.post('/drive/files/create/',{name,type,parent,space}).then(r=>r.data), patch:(id,updates)=>api.patch(`/drive/items/${id}/`,updates).then(r=>r.data), trash:id=>api.post(`/drive/items/${id}/trash/`), emptyTrash:()=>api.delete('/drive/trash/').then(r=>r.data), restore:id=>api.post(`/drive/items/${id}/restore/`), remove:id=>api.delete(`/drive/items/${id}/delete/`), link:(id,inline=false)=>api.post(`/drive/items/${id}/link/`,{inline}).then(r=>r.data.url), upload:(files,parent,space,progress)=>{const data=new FormData();if(parent)data.append('parent',parent);data.append('space',space);for(const file of files)data.append('files',file);return api.post('/drive/uploads/',data,{timeout:0,onUploadProgress:e=>progress?.(e.total?Math.round((e.loaded/e.total)*100):null)}).then(r=>r.data)}, uploadResumable:async(file,parent,space,progress)=>{ const {data:started}=await api.post('/drive/uploads/start/',{name:file.name,size:file.size,parent,space}) const chunkSize=8*1024*1024;let offset=started.offset try{ while(offsetapi.post('/drive/uploads/start/',{name:file.name,size:file.size,parent,space}).then(r=>r.data), uploadStatus:id=>api.get(`/drive/uploads/${id}/`).then(r=>r.data), uploadChunk:(id,offset,part,options={})=>{ const form=new FormData();form.append('offset',offset);form.append('part',part,'chunk.bin') return api.post(`/drive/uploads/${id}/chunk/`,form,{timeout:0,...options}).then(r=>r.data) }, finishUpload:id=>api.post(`/drive/uploads/${id}/finish/`,{},{timeout:0}).then(r=>r.data), cancelUpload:id=>api.delete(`/drive/uploads/${id}/cancel/`).then(r=>r.data), pendingUploads:()=>api.get('/drive/uploads/pending/').then(r=>r.data.sessions), spaces:()=>api.get('/drive/spaces/').then(r=>r.data), createSpace:data=>api.post('/drive/spaces/create/',data).then(r=>r.data), editSpace:(id,data)=>api.patch(`/drive/spaces/${id}/`,data).then(r=>r.data), deleteSpace:(id,confirm_name)=>api.delete(`/drive/spaces/${id}/`,{data:{confirm_name}}).then(r=>r.data), renameSpace:display_name=>api.patch('/drive/spaces/me/',{display_name}).then(r=>r.data), searchUsers:(q,parent_space)=>api.get('/drive/spaces/users/search/',{params:{q,parent_space}}).then(r=>r.data.users), addUser:(user_id,parent_space,permissions)=>api.post('/drive/spaces/users/',{user_id,permissions},{params:{parent_space}}).then(r=>r.data), grantPatch:(id,data)=>api.patch(`/drive/spaces/users/${id}/`,data).then(r=>r.data), grantRemove:id=>api.delete(`/drive/spaces/users/${id}/`), members:parent_space=>api.get('/drive/spaces/members/',{params:parent_space?{parent_space}:{}}).then(r=>r.data), memberPatch:(id,data)=>api.patch(`/drive/spaces/members/${id}/`,data).then(r=>r.data), spaceQuota:(id,quota_bytes)=>api.patch(`/drive/spaces/${id}/quota/`,{quota_bytes}).then(r=>r.data), invite:data=>api.post('/drive/invitations/',data).then(r=>r.data), revokeInvite:id=>api.post(`/drive/invitations/${id}/revoke/`).then(r=>r.data), invitePreview:token=>api.get(`/invitations/${encodeURIComponent(token)}/`).then(r=>r.data), inviteAccept:(token,data)=>api.post(`/invitations/${encodeURIComponent(token)}/accept/`,data).then(r=>r.data), } export function message(error){let d=error?.response?.data;if(typeof d?.detail==='string')return d.detail;if(typeof d==='object' && d!==null)return Object.entries(d).map(([k,v])=>`${k}: ${Array.isArray(v)?v.join(', '):v}`).join(' · ');return 'Une erreur est survenue. Réessaie.'} export const publicFileUrl = (token,id,inline=false) => `${publicApi.defaults.baseURL}/public/${encodeURIComponent(token)}/files/${id}/${inline?'?inline=1':''}`