GooDrive/frontend/src/api/client.js
Louis Mazin 3fea8a85c8 perfect
2026-10-14 19:17:56 +02:00

134 lines
8.2 KiB
JavaScript

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(offset<file.size){
const part=file.slice(offset,Math.min(offset+chunkSize,file.size))
const data=new FormData();data.append('offset',offset);data.append('part',part,file.name)
const result=await api.post(`/drive/uploads/${started.id}/chunk/`,data,{timeout:0})
offset=result.data.offset;progress?.(Math.round(100*offset/file.size))
}
return (await api.post(`/drive/uploads/${started.id}/finish/`,{}, {timeout:0})).data
} catch(error){throw error}
},
// Transfer-control API. Every file, including small files, can pause/resume/cancel.
startUpload:(file,parent,space)=>api.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':''}`