This repository has been archived on 2026-07-15. You can view files and clone it, but you cannot make any changes to it's state, such as pushing and creating new issues, pull requests or comments.
drive/frontend/src/lib/api.ts
Elijah 5200654d0f
All checks were successful
Automated Container Build / build-and-push (push) Successful in 1m19s
New context menu, info pane fixes, pinned item syncing, and navigation bug fixes
2026-05-22 19:40:08 -07:00

386 lines
10 KiB
TypeScript

const API_BASE = process.env.NEXT_PUBLIC_API_URL || '';
import * as tus from 'tus-js-client';
interface RequestOptions {
method?: string;
body?: any;
headers?: Record<string, string>;
}
class ApiClient {
private accessToken: string | null = null;
private refreshToken: string | null = null;
constructor() {
if (typeof window !== 'undefined') {
this.accessToken = localStorage.getItem('access_token');
this.refreshToken = localStorage.getItem('refresh_token');
}
}
setTokens(accessToken: string, refreshToken: string) {
this.accessToken = accessToken;
this.refreshToken = refreshToken;
localStorage.setItem('access_token', accessToken);
localStorage.setItem('refresh_token', refreshToken);
}
clearTokens() {
this.accessToken = null;
this.refreshToken = null;
localStorage.removeItem('access_token');
localStorage.removeItem('refresh_token');
}
getAccessToken() {
return this.accessToken;
}
isAuthenticated() {
return !!this.accessToken;
}
private async request(endpoint: string, options: RequestOptions = {}) {
const headers: Record<string, string> = {
...options.headers,
};
if (this.accessToken) {
headers['Authorization'] = `Bearer ${this.accessToken}`;
}
if (options.body && !(options.body instanceof FormData)) {
headers['Content-Type'] = 'application/json';
}
const response = await fetch(`${API_BASE}${endpoint}`, {
method: options.method || 'GET',
headers,
body: options.body instanceof FormData
? options.body
: options.body
? JSON.stringify(options.body)
: undefined,
});
// Try to refresh token on 401
if (response.status === 401 && this.refreshToken && endpoint !== '/api/auth/refresh') {
const refreshed = await this.refreshAccessToken();
if (refreshed) {
headers['Authorization'] = `Bearer ${this.accessToken}`;
return fetch(`${API_BASE}${endpoint}`, {
method: options.method || 'GET',
headers,
body: options.body instanceof FormData
? options.body
: options.body
? JSON.stringify(options.body)
: undefined,
});
}
}
return response;
}
private async refreshAccessToken(): Promise<boolean> {
try {
const response = await fetch(`${API_BASE}/api/auth/refresh`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${this.refreshToken}`,
},
});
if (response.ok) {
const data = await response.json();
this.accessToken = data.access_token;
localStorage.setItem('access_token', data.access_token);
return true;
}
} catch {
// Refresh failed
}
this.clearTokens();
return false;
}
// --- Setup ---
async checkSetup() {
const res = await this.request('/api/setup/status');
return res.json();
}
async setup(username: string, password: string) {
const res = await this.request('/api/setup', {
method: 'POST',
body: { username, password },
});
return res.json();
}
// --- Auth ---
async login(username: string, password: string, totpCode?: string) {
const res = await this.request('/api/auth/login', {
method: 'POST',
body: { username, password, totp_code: totpCode },
});
const data = await res.json();
if (res.ok && data.access_token) {
this.setTokens(data.access_token, data.refresh_token);
}
return { ok: res.ok, status: res.status, data };
}
logout() {
this.clearTokens();
}
async changePassword(oldPassword: string, newPassword: string) {
const res = await this.request('/api/auth/password', {
method: 'PUT',
body: { old_password: oldPassword, new_password: newPassword },
});
return res.json();
}
// --- Files ---
async listDirectory(path: string = '.') {
const res = await this.request(`/api/files/?path=${encodeURIComponent(path)}`);
return res.json();
}
async getExtendedFileInfo(path: string) {
const res = await this.request(`/api/files/info/?path=${encodeURIComponent(path)}`);
return res.json();
}
async createFolder(path: string) {
const res = await this.request('/api/files/mkdir', {
method: 'POST',
body: { path },
});
return res.json();
}
async rename(oldPath: string, newName: string) {
const res = await this.request('/api/files/rename', {
method: 'POST',
body: { old_path: oldPath, new_name: newName },
});
return res.json();
}
async move(sourcePath: string, destPath: string) {
const res = await this.request('/api/files/move', {
method: 'POST',
body: { source_path: sourcePath, dest_path: destPath },
});
return res.json();
}
async copy(sourcePath: string, destPath: string) {
const res = await this.request('/api/files/copy', {
method: 'POST',
body: { source_path: sourcePath, dest_path: destPath },
});
return res.json();
}
async deleteFile(path: string) {
const res = await this.request(`/api/files/?path=${encodeURIComponent(path)}`, {
method: 'DELETE',
});
return res.json();
}
upload(file: File, destPath: string, callbacks?: { onProgress?: (percent: number) => void }): Promise<void> {
return new Promise((resolve, reject) => {
const upload = new tus.Upload(file, {
endpoint: `${API_BASE}/api/tus/`,
chunkSize: 5 * 1024 * 1024, // 5MB chunks bypass Next.js 10MB proxy limit
retryDelays: [0, 3000, 5000, 10000, 20000],
headers: this.getStreamHeaders(),
metadata: {
filename: file.name,
filetype: file.type,
path: destPath,
},
onError: function (error) {
reject(error);
},
onProgress: function (bytesUploaded, bytesTotal) {
const percentage = (bytesUploaded / bytesTotal) * 100;
callbacks?.onProgress?.(percentage);
},
onSuccess: function () {
resolve();
},
});
upload.start();
});
}
getDownloadUrl(path: string) {
const url = `${API_BASE}/api/files/download/?path=${encodeURIComponent(path)}`;
return this.accessToken ? `${url}&token=${this.accessToken}` : url;
}
getDownloadFolderUrl(path: string) {
const url = `${API_BASE}/api/files/download-folder?path=${encodeURIComponent(path)}`;
return this.accessToken ? `${url}&token=${this.accessToken}` : url;
}
getThumbnailUrl(checksum: string) {
const url = `${API_BASE}/api/files/thumbnail/${encodeURIComponent(checksum)}`;
return this.accessToken ? `${url}?token=${this.accessToken}` : url;
}
getStreamHeaders(): Record<string, string> {
return this.accessToken ? { Authorization: `Bearer ${this.accessToken}` } : {};
}
// --- Search ---
async search(query: string) {
const res = await this.request(`/api/search?q=${encodeURIComponent(query)}`);
return res.json();
}
// --- Pinned ---
async listPinned() {
const res = await this.request('/api/files/pinned');
return res.json();
}
async togglePin(path: string) {
const res = await this.request('/api/files/pin', {
method: 'POST',
body: { path },
});
return res.json();
}
async getTasks() {
const res = await this.request('/api/tasks');
return res.json();
}
async getSettings() {
const res = await this.request('/api/settings');
return res.json();
}
async updateSettings(settings: any) {
const res = await this.request('/api/settings', {
method: 'PUT',
body: settings,
});
return res.json();
}
async getStorageDashboard() {
const res = await this.request('/api/storage/dashboard');
return res.json();
}
// --- Trash ---
async listTrash() {
const res = await this.request('/api/trash');
return res.json();
}
async restoreFromTrash(path: string) {
const res = await this.request('/api/trash/restore', {
method: 'POST',
body: { path },
});
return res.json();
}
async emptyTrash() {
const res = await this.request('/api/trash', { method: 'DELETE' });
return res.json();
}
// --- Audit ---
async getAuditLog(limit = 100, offset = 0) {
const res = await this.request(`/api/audit?limit=${limit}&offset=${offset}`);
return res.json();
}
// --- 2FA ---
async enable2FA() {
const res = await this.request('/api/auth/2fa/enable', { method: 'POST' });
return res.json();
}
async confirm2FA(code: string) {
const res = await this.request('/api/auth/2fa/confirm', {
method: 'POST',
body: { code },
});
return res.json();
}
async disable2FA() {
const res = await this.request('/api/auth/2fa/disable', { method: 'POST' });
return res.json();
}
// --- Sharing ---
async createShare(path: string, password?: string, expiresIn?: number, maxDownloads?: number) {
const res = await this.request('/api/share', {
method: 'POST',
body: { path, password, expires_in: expiresIn, max_downloads: maxDownloads },
});
const data = await res.json();
if (!res.ok) throw new Error(data.error || 'Failed to create share');
return data;
}
async listShares() {
const res = await this.request('/api/share');
return res.json();
}
async revokeShare(id: string) {
const res = await this.request(`/api/share/${id}`, { method: 'DELETE' });
return res.json();
}
async getPublicShare(id: string, password?: string, path?: string) {
// Note: No auth headers here, so we use fetch directly to avoid interceptors
let url = `${API_BASE}/api/public/share/${id}`;
if (path) {
url += `?path=${encodeURIComponent(path)}`;
}
const res = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ password }),
});
if (!res.ok) {
const err = await res.json().catch(() => ({}));
throw err;
}
return res.json();
}
getPublicDownloadUrl(id: string, password?: string, path?: string) {
let url = `${API_BASE}/api/public/download/${id}`;
const params = new URLSearchParams();
if (password) params.set('pwd', password);
if (path) params.set('path', path);
const qs = params.toString();
if (qs) {
url += `?${qs}`;
}
return url;
}
}
export const api = new ApiClient();
export default api;