This commit is contained in:
parent
fa2be029a2
commit
724d70e58b
3339 changed files with 1075535 additions and 0 deletions
357
frontend/src/lib/api.ts
Normal file
357
frontend/src/lib/api.ts
Normal file
|
|
@ -0,0 +1,357 @@
|
|||
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();
|
||||
}
|
||||
|
||||
// --- Files ---
|
||||
async listDirectory(path: string = '.') {
|
||||
const res = await this.request(`/api/files/${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/${encodeURIComponent(path)}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
return res.json();
|
||||
}
|
||||
|
||||
upload(file: File, destPath: string): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const upload = new tus.Upload(file, {
|
||||
endpoint: `${API_BASE}/api/tus/`,
|
||||
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).toFixed(2);
|
||||
console.log(bytesUploaded, bytesTotal, percentage + '%');
|
||||
},
|
||||
onSuccess: function () {
|
||||
resolve();
|
||||
},
|
||||
});
|
||||
upload.start();
|
||||
});
|
||||
}
|
||||
|
||||
getDownloadUrl(path: string) {
|
||||
const url = `${API_BASE}/api/files/download/${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 },
|
||||
});
|
||||
return res.json();
|
||||
}
|
||||
|
||||
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) {
|
||||
// Note: No auth headers here, so we use fetch directly to avoid interceptors
|
||||
const res = await fetch(`${API_BASE}/api/public/share/${id}`, {
|
||||
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) {
|
||||
let url = `${API_BASE}/api/public/download/${id}`;
|
||||
if (password) {
|
||||
url += `?pwd=${encodeURIComponent(password)}`;
|
||||
}
|
||||
return url;
|
||||
}
|
||||
}
|
||||
|
||||
export const api = new ApiClient();
|
||||
export default api;
|
||||
Reference in a new issue