131 lines
7.9 KiB
JavaScript
131 lines
7.9 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}/`),
|
|
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),
|
|
patch:(id,updates)=>api.patch(`/drive/items/${id}/`,updates).then(r=>r.data),
|
|
trash:id=>api.post(`/drive/items/${id}/trash/`),
|
|
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':''}`
|