203 lines
5.4 KiB
TypeScript
203 lines
5.4 KiB
TypeScript
import { Category, Feed, FeedEntry, Stats, User } from '../types';
|
|
|
|
const API_BASE = '/api';
|
|
|
|
async function request<T>(path: string, options: RequestInit = {}): Promise<T> {
|
|
const token = localStorage.getItem('rsspipe_jwt');
|
|
const headers: Record<string, string> = {
|
|
'Content-Type': 'application/json',
|
|
...(options.headers as Record<string, string> || {}),
|
|
};
|
|
|
|
if (token) {
|
|
headers['Authorization'] = `Bearer ${token}`;
|
|
}
|
|
|
|
const res = await fetch(`${API_BASE}${path}`, {
|
|
...options,
|
|
headers,
|
|
credentials: 'omit',
|
|
});
|
|
|
|
if (res.status === 401 && path !== '/auth/login') {
|
|
localStorage.removeItem('rsspipe_jwt');
|
|
window.dispatchEvent(new CustomEvent('auth:unauthorized'));
|
|
throw new Error('Session expired or unauthorized');
|
|
}
|
|
|
|
if (!res.ok) {
|
|
let errMsg = `Request failed: ${res.status} ${res.statusText}`;
|
|
try {
|
|
const errJson = await res.json();
|
|
if (errJson.error) errMsg = errJson.error;
|
|
} catch {
|
|
// ignore
|
|
}
|
|
throw new Error(errMsg);
|
|
}
|
|
|
|
return res.json();
|
|
}
|
|
|
|
export const api = {
|
|
// Auth
|
|
async login(username: string, password: string): Promise<{ token: string; user: User }> {
|
|
const res = await request<{ token: string; user: User }>('/auth/login', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ username, password }),
|
|
});
|
|
if (res.token) {
|
|
localStorage.setItem('rsspipe_jwt', res.token);
|
|
}
|
|
return res;
|
|
},
|
|
|
|
async logout(): Promise<void> {
|
|
try {
|
|
await request('/auth/logout', { method: 'POST' });
|
|
} finally {
|
|
localStorage.removeItem('rsspipe_jwt');
|
|
}
|
|
},
|
|
|
|
async getMe(): Promise<User> {
|
|
return request<User>('/auth/me');
|
|
},
|
|
|
|
async changePassword(old_password: string, new_password: string): Promise<{ message: string }> {
|
|
return request<{ message: string }>('/auth/password', {
|
|
method: 'PUT',
|
|
body: JSON.stringify({ old_password, new_password }),
|
|
});
|
|
},
|
|
|
|
// Stats
|
|
async getStats(): Promise<Stats> {
|
|
return request<Stats>('/stats');
|
|
},
|
|
|
|
// Categories
|
|
async listCategories(): Promise<Category[]> {
|
|
return request<Category[]>('/categories');
|
|
},
|
|
|
|
async createCategory(title: string): Promise<Category> {
|
|
return request<Category>('/categories', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ title }),
|
|
});
|
|
},
|
|
|
|
async deleteCategory(id: string): Promise<void> {
|
|
return request<void>(`/categories/${id}`, { method: 'DELETE' });
|
|
},
|
|
|
|
// Feeds
|
|
async listFeeds(): Promise<Feed[]> {
|
|
return request<Feed[]>('/feeds');
|
|
},
|
|
|
|
async createFeed(feed_url: string, category_id?: string, title?: string): Promise<Feed> {
|
|
return request<Feed>('/feeds', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ feed_url, category_id, title }),
|
|
});
|
|
},
|
|
|
|
async updateFeed(
|
|
id: string,
|
|
data: {
|
|
title?: string;
|
|
category_id?: string;
|
|
refresh_interval_minutes?: number;
|
|
timeout_seconds?: number;
|
|
}
|
|
): Promise<Feed> {
|
|
return request<Feed>(`/feeds/${id}`, {
|
|
method: 'PUT',
|
|
body: JSON.stringify(data),
|
|
});
|
|
},
|
|
|
|
async deleteFeed(id: string): Promise<void> {
|
|
return request<void>(`/feeds/${id}`, { method: 'DELETE' });
|
|
},
|
|
|
|
async refreshFeed(id: string): Promise<Feed> {
|
|
return request<Feed>(`/feeds/${id}/refresh`, { method: 'POST' });
|
|
},
|
|
|
|
async refreshAllFeeds(): Promise<void> {
|
|
return request<void>('/feeds/refresh-all', { method: 'POST' });
|
|
},
|
|
|
|
// Entries
|
|
async listEntries(params: {
|
|
view?: string;
|
|
feed_id?: string;
|
|
category_id?: string;
|
|
search?: string;
|
|
limit?: number;
|
|
offset?: number;
|
|
}): Promise<{ entries: FeedEntry[]; total: number; limit: number; offset: number }> {
|
|
const query = new URLSearchParams();
|
|
if (params.view) query.set('view', params.view);
|
|
if (params.feed_id) query.set('feed_id', params.feed_id);
|
|
if (params.category_id) query.set('category_id', params.category_id);
|
|
if (params.search) query.set('search', params.search);
|
|
if (params.limit) query.set('limit', params.limit.toString());
|
|
if (params.offset) query.set('offset', params.offset.toString());
|
|
|
|
return request(`/entries?${query.toString()}`);
|
|
},
|
|
|
|
async updateRead(id: string, is_read: boolean): Promise<void> {
|
|
return request(`/entries/${id}/read`, {
|
|
method: 'PUT',
|
|
body: JSON.stringify({ is_read }),
|
|
});
|
|
},
|
|
|
|
async updateStar(id: string, is_starred: boolean): Promise<void> {
|
|
return request(`/entries/${id}/star`, {
|
|
method: 'PUT',
|
|
body: JSON.stringify({ is_starred }),
|
|
});
|
|
},
|
|
|
|
async markAllRead(feed_id?: string, category_id?: string): Promise<void> {
|
|
return request('/entries/mark-all-read', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ feed_id, category_id }),
|
|
});
|
|
},
|
|
|
|
// OPML
|
|
async importOPML(file: File): Promise<{ imported_count: number; total_parsed: number }> {
|
|
const token = localStorage.getItem('rsspipe_jwt');
|
|
const formData = new FormData();
|
|
formData.append('file', file);
|
|
|
|
const headers: Record<string, string> = {};
|
|
if (token) headers['Authorization'] = `Bearer ${token}`;
|
|
|
|
const res = await fetch(`${API_BASE}/opml/import`, {
|
|
method: 'POST',
|
|
headers,
|
|
body: formData,
|
|
});
|
|
|
|
if (!res.ok) {
|
|
const err = await res.json().catch(() => ({}));
|
|
throw new Error(err.error || 'Failed to import OPML');
|
|
}
|
|
|
|
return res.json();
|
|
},
|
|
|
|
exportOPMLUrl(): string {
|
|
const token = localStorage.getItem('rsspipe_jwt');
|
|
return `${API_BASE}/opml/export?token=${token || ''}`;
|
|
}
|
|
};
|