All checks were successful
Automated Container Build / build-and-push (push) Successful in 1m19s
386 lines
10 KiB
TypeScript
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;
|