1262 lines
40 KiB
TypeScript
1262 lines
40 KiB
TypeScript
import type * as Types from './types';
|
|
import { apiBaseUrl } from '@/lib/site';
|
|
|
|
const API_BASE_URL =
|
|
apiBaseUrl;
|
|
|
|
type ApiErrorBody = {
|
|
error?: string;
|
|
detail?: string;
|
|
message?: string;
|
|
};
|
|
|
|
class ApiClient {
|
|
private baseUrl: string;
|
|
private isRefreshing = false;
|
|
private refreshSubscribers: Array<(token: string) => void> = [];
|
|
|
|
constructor(baseUrl: string) {
|
|
this.baseUrl = baseUrl;
|
|
}
|
|
|
|
private getStorageValue(key: string) {
|
|
if (typeof window === 'undefined') {
|
|
return null;
|
|
}
|
|
|
|
return window.localStorage.getItem(key);
|
|
}
|
|
|
|
private setStorageValue(key: string, value: string) {
|
|
if (typeof window === 'undefined') {
|
|
return;
|
|
}
|
|
|
|
window.localStorage.setItem(key, value);
|
|
}
|
|
|
|
private removeStorageValue(key: string) {
|
|
if (typeof window === 'undefined') {
|
|
return;
|
|
}
|
|
|
|
window.localStorage.removeItem(key);
|
|
}
|
|
|
|
private getAuthHeaders(): HeadersInit {
|
|
const token = this.getStorageValue('access_token');
|
|
return {
|
|
'Content-Type': 'application/json',
|
|
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
|
};
|
|
}
|
|
|
|
private async refreshAccessToken(): Promise<string> {
|
|
const refreshToken = this.getStorageValue('refresh_token');
|
|
if (!refreshToken) {
|
|
throw new Error('No refresh token available');
|
|
}
|
|
|
|
const response = await fetch(`${this.baseUrl}/api/auth/refresh`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ refresh_token: refreshToken }),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
this.removeStorageValue('access_token');
|
|
this.removeStorageValue('refresh_token');
|
|
throw new Error('Session expired. Please login again.');
|
|
}
|
|
|
|
const data: Types.TokenSchema = await response.json();
|
|
this.setStorageValue('access_token', data.access_token);
|
|
this.setStorageValue('refresh_token', data.refresh_token);
|
|
return data.access_token;
|
|
}
|
|
|
|
private onRefreshed(token: string) {
|
|
this.refreshSubscribers.forEach(callback => callback(token));
|
|
this.refreshSubscribers = [];
|
|
}
|
|
|
|
private addRefreshSubscriber(callback: (token: string) => void) {
|
|
this.refreshSubscribers.push(callback);
|
|
}
|
|
|
|
async request<T>(
|
|
endpoint: string,
|
|
options: RequestInit = {}
|
|
): Promise<T> {
|
|
const url = `${this.baseUrl}${endpoint}`;
|
|
const config: RequestInit = {
|
|
...options,
|
|
headers: {
|
|
...this.getAuthHeaders(),
|
|
...options.headers,
|
|
},
|
|
};
|
|
|
|
const response = await fetch(url, config);
|
|
|
|
// Handle 401 with automatic token refresh
|
|
if (response.status === 401 && this.getStorageValue('refresh_token')) {
|
|
if (!this.isRefreshing) {
|
|
this.isRefreshing = true;
|
|
try {
|
|
const newToken = await this.refreshAccessToken();
|
|
this.isRefreshing = false;
|
|
this.onRefreshed(newToken);
|
|
|
|
// After you obtained `newToken` successfully:
|
|
const retryConfig: RequestInit = {
|
|
...options,
|
|
headers: {
|
|
...(options.headers || {}),
|
|
Authorization: `Bearer ${newToken}`, // put last so it can't be overwritten
|
|
},
|
|
};
|
|
const retryResponse = await fetch(url, retryConfig);
|
|
if (!retryResponse.ok) {
|
|
const err = await retryResponse.json().catch(() => ({}));
|
|
throw new Error(err.error || err.detail || 'Request failed');
|
|
}
|
|
return retryResponse.json();
|
|
|
|
} catch (error) {
|
|
this.isRefreshing = false;
|
|
throw error;
|
|
}
|
|
} else {
|
|
return new Promise((resolve, reject) => {
|
|
this.addRefreshSubscriber(async (token: string) => {
|
|
try {
|
|
const retryConfig: RequestInit = {
|
|
...options,
|
|
headers: {
|
|
...(options.headers || {}),
|
|
Authorization: `Bearer ${token}`,
|
|
},
|
|
};
|
|
const retryResponse = await fetch(url, retryConfig);
|
|
if (!retryResponse.ok) {
|
|
const err = (await retryResponse.json().catch(() => ({}))) as ApiErrorBody;
|
|
reject(new Error(err.error || err.detail || 'Request failed after refresh'));
|
|
} else {
|
|
resolve(retryResponse.json());
|
|
}
|
|
} catch (e) {
|
|
reject(e);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
}
|
|
|
|
if (!response.ok) {
|
|
const body = (await response.json().catch(() => ({}))) as ApiErrorBody;
|
|
const message =
|
|
body?.error || body?.detail || body?.message || 'خطای ناشناخته رخ داد';
|
|
throw new Error(message);
|
|
}
|
|
|
|
return response.json() as Promise<T>;
|
|
}
|
|
|
|
// ============= Auth Endpoints =============
|
|
|
|
async register(data: Types.UserRegistrationSchema) {
|
|
return this.request<Types.MessageSchema>('/api/auth/register', {
|
|
method: 'POST',
|
|
body: JSON.stringify(data),
|
|
});
|
|
}
|
|
|
|
async login(data: Types.UserLoginSchema) {
|
|
return this.request<Types.TokenSchema>('/api/auth/login', {
|
|
method: 'POST',
|
|
body: JSON.stringify(data),
|
|
});
|
|
}
|
|
|
|
async loginWithOtp(data: Types.UserOtpLoginSchema) {
|
|
return this.request<Types.TokenSchema>('/api/auth/login/otp', {
|
|
method: 'POST',
|
|
body: JSON.stringify(data),
|
|
});
|
|
}
|
|
|
|
async sendOtp(data: Types.OtpSendSchema) {
|
|
return this.request<Types.OtpSendResponseSchema>('/api/auth/otp/send', {
|
|
method: 'POST',
|
|
body: JSON.stringify(data),
|
|
});
|
|
}
|
|
|
|
async verifyRegisterOtp(data: Types.RegisterOtpVerifySchema) {
|
|
return this.request<Types.MessageSchema>('/api/auth/otp/verify-register', {
|
|
method: 'POST',
|
|
body: JSON.stringify(data),
|
|
});
|
|
}
|
|
|
|
async refreshToken(data: Types.TokenRefreshIn) {
|
|
return this.request<Types.TokenSchema>('/api/auth/refresh', {
|
|
method: 'POST',
|
|
body: JSON.stringify(data),
|
|
});
|
|
}
|
|
|
|
async resetPassword(data: Types.PasswordResetSchema) {
|
|
return this.request<Types.MessageSchema>('/api/auth/reset-password', {
|
|
method: 'POST',
|
|
body: JSON.stringify(data),
|
|
});
|
|
}
|
|
|
|
async sendMobileVerificationOtp(data: Types.MobileOtpSendSchema) {
|
|
return this.request<Types.OtpSendResponseSchema>('/api/auth/mobile/send-otp', {
|
|
method: 'POST',
|
|
body: JSON.stringify(data),
|
|
});
|
|
}
|
|
|
|
async verifyMobile(data: Types.MobileOtpVerifySchema) {
|
|
return this.request<Types.UserProfileSchema>('/api/auth/mobile/verify', {
|
|
method: 'POST',
|
|
body: JSON.stringify(data),
|
|
});
|
|
}
|
|
|
|
async startGoogleLogin() {
|
|
if (typeof window !== 'undefined') {
|
|
window.location.href = `${this.baseUrl}/api/auth/oauth/google/start`;
|
|
}
|
|
}
|
|
|
|
async getGoogleFlow(flow: string) {
|
|
return this.request<Types.GoogleFlowResponseSchema>(
|
|
`/api/auth/oauth/google/flow?flow=${encodeURIComponent(flow)}`
|
|
);
|
|
}
|
|
|
|
async completeGoogleSignup(data: Types.GoogleCompleteSchema) {
|
|
return this.request<Types.GoogleFlowResponseSchema>('/api/auth/oauth/google/complete', {
|
|
method: 'POST',
|
|
body: JSON.stringify(data),
|
|
});
|
|
}
|
|
|
|
async resendGoogleClaimOtp(flow: string) {
|
|
return this.request<Types.MessageSchema>('/api/auth/oauth/google/claim/send-otp', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ flow }),
|
|
});
|
|
}
|
|
|
|
async verifyGoogleClaim(flow: string, code: string) {
|
|
return this.request<Types.GoogleFlowResponseSchema>('/api/auth/oauth/google/claim/verify', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ flow, code }),
|
|
});
|
|
}
|
|
|
|
async verifyEmail(token: string): Promise<Types.MessageSchema> {
|
|
const url = `${this.baseUrl}/api/auth/verify-email/${encodeURIComponent(token)}`;
|
|
const response = await fetch(url, { method: 'GET' });
|
|
|
|
if (response.ok) {
|
|
return response.json() as Promise<Types.MessageSchema>;
|
|
}
|
|
|
|
const data = (await response.json().catch(() => ({}))) as ApiErrorBody;
|
|
const errMsg: string =
|
|
(data && (data.error || data.detail)) || 'خطای ناشناخته رخ داد';
|
|
throw new Error(errMsg);
|
|
}
|
|
|
|
|
|
async resendVerification(email: string) {
|
|
return this.request<Types.MessageSchema>(
|
|
`/api/auth/resend-verification?email=${encodeURIComponent(email)}`,
|
|
{ method: 'POST' }
|
|
);
|
|
}
|
|
|
|
async getProfile() {
|
|
return this.request<Types.UserProfileSchema>('/api/auth/profile'
|
|
);
|
|
}
|
|
|
|
async updateProfile(data: Types.UserUpdateSchema) {
|
|
return this.request<Types.UserProfileSchema>('/api/auth/profile', {
|
|
method: 'PUT',
|
|
body: JSON.stringify(data),
|
|
});
|
|
}
|
|
|
|
async uploadProfilePicture(file: File) {
|
|
const formData = new FormData();
|
|
formData.append('file', file);
|
|
|
|
const token = this.getStorageValue('access_token');
|
|
const response = await fetch(`${this.baseUrl}/api/auth/profile/picture`, {
|
|
method: 'POST',
|
|
headers: {
|
|
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
|
},
|
|
body: formData,
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const error: Types.ErrorSchema = await response.json().catch(() => ({
|
|
detail: 'خطای آپلود تصویر',
|
|
}));
|
|
throw new Error(error.detail);
|
|
}
|
|
|
|
return response.json() as Promise<Types.MessageSchema>;
|
|
}
|
|
|
|
async deleteProfilePicture() {
|
|
return this.request<Types.MessageSchema>('/api/auth/profile/picture', {
|
|
method: 'DELETE',
|
|
});
|
|
}
|
|
|
|
async requestPasswordReset(email: string) {
|
|
return this.request<Types.MessageSchema>('/api/auth/request-password-reset', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ email }),
|
|
});
|
|
}
|
|
|
|
async resetPasswordConfirm(token: string, new_password: string) {
|
|
return this.request<Types.MessageSchema>('/api/auth/reset-password-confirm', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ token, new_password }),
|
|
});
|
|
}
|
|
|
|
async getLegacyVerifyEmailMessage(token: string) {
|
|
return this.request<Types.MessageSchema>(`/api/auth/verify-email/${encodeURIComponent(token)}`);
|
|
}
|
|
|
|
async getLegacyResetTokenMessage(token: string) {
|
|
return this.request<Types.MessageSchema>('/api/auth/reset-password-confirm', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ token }),
|
|
});
|
|
}
|
|
|
|
|
|
async checkUsername(username: string) {
|
|
return this.request<Types.UsernameCheckSchema>(
|
|
`/api/auth/check-username?username=${encodeURIComponent(username)}`
|
|
);
|
|
}
|
|
|
|
async checkMobile(mobile: string) {
|
|
return this.request<Types.MobileLookupSchema>(
|
|
`/api/auth/check-mobile?mobile=${encodeURIComponent(mobile)}`
|
|
);
|
|
}
|
|
|
|
// Admin auth endpoints
|
|
async listDeletedUsers() {
|
|
return this.request<Types.UserProfileSchema[]>('/api/auth/users/deleted');
|
|
}
|
|
|
|
async restoreUser(userId: number) {
|
|
return this.request<Types.MessageSchema>(`/api/auth/users/${userId}/restore`, {
|
|
method: 'POST',
|
|
});
|
|
}
|
|
|
|
async listUsers(params?: {
|
|
search?: string;
|
|
role?: 'staff' | 'superuser';
|
|
student_id?: string;
|
|
university?: string;
|
|
major?: string;
|
|
is_active?: 'true' | 'false';
|
|
limit?: number;
|
|
offset?: number;
|
|
}) {
|
|
const query = new URLSearchParams();
|
|
if (params?.search) query.set('search', params.search);
|
|
if (params?.role) query.set('role', params.role);
|
|
if (params?.student_id) query.set('student_id', params.student_id);
|
|
if (params?.university) query.set('university', params.university);
|
|
if (params?.major) query.set('major', params.major);
|
|
if (params?.is_active) query.set('is_active', params.is_active);
|
|
if (params?.limit != null) query.set('limit', String(params.limit));
|
|
if (params?.offset != null) query.set('offset', String(params.offset));
|
|
return this.request<Types.UserListSchema[]>(`/api/auth/users${query.toString() ? `?${query.toString()}` : ''}`);
|
|
}
|
|
|
|
async getUserDetail(userId: number) {
|
|
return this.request<Types.UserProfileSchema>(`/api/auth/users/${userId}`);
|
|
}
|
|
|
|
async listAuthorizationRoles() {
|
|
return this.request<Types.AuthorizationRoleSchema[]>('/api/auth/roles');
|
|
}
|
|
|
|
async getUserAuthorization(userId: number) {
|
|
return this.request<Types.UserAuthorizationSchema>(`/api/auth/users/${userId}/authorization`);
|
|
}
|
|
|
|
async updateUserAuthorization(userId: number, data: Types.UserAuthorizationUpdateSchema) {
|
|
return this.request<Types.UserAuthorizationSchema>(`/api/auth/users/${userId}/authorization`, {
|
|
method: 'PUT',
|
|
body: JSON.stringify(data),
|
|
});
|
|
}
|
|
|
|
// ============= Blog Endpoints =============
|
|
|
|
async getPosts(params?: {
|
|
page?: number;
|
|
limit?: number;
|
|
category?: string;
|
|
tag?: string | string[];
|
|
search?: string;
|
|
featured?: boolean;
|
|
author?: string | string[];
|
|
}) {
|
|
const queryParams = new URLSearchParams();
|
|
if (params?.page) queryParams.append('page', params.page.toString());
|
|
if (params?.limit) queryParams.append('limit', params.limit.toString());
|
|
if (params?.category) queryParams.append('category', params.category);
|
|
if (Array.isArray(params?.tag)) {
|
|
params.tag.forEach((tag) => queryParams.append('tag', tag));
|
|
} else if (params?.tag) {
|
|
queryParams.append('tag', params.tag);
|
|
}
|
|
if (params?.search) queryParams.append('search', params.search);
|
|
if (params?.featured !== undefined) queryParams.append('featured', params.featured.toString());
|
|
if (Array.isArray(params?.author)) {
|
|
params.author.forEach((author) => queryParams.append('author', author));
|
|
} else if (params?.author) {
|
|
queryParams.append('author', params.author);
|
|
}
|
|
|
|
const query = queryParams.toString();
|
|
return this.request<Types.PostListSchema[]>(`/api/blog/posts${query ? `?${query}` : ''}`);
|
|
}
|
|
|
|
async getBlogFilters() {
|
|
return this.request<Types.BlogFiltersSchema>('/api/blog/filters');
|
|
}
|
|
|
|
async getPost(slug: string) {
|
|
return this.request<Types.PostDetailSchema>(`/api/blog/posts/${encodeURIComponent(slug)}`);
|
|
}
|
|
|
|
async createPost(data: Types.PostCreateSchema) {
|
|
return this.request<Types.PostDetailSchema>('/api/blog/admin/posts', {
|
|
method: 'POST',
|
|
body: JSON.stringify(data),
|
|
});
|
|
}
|
|
|
|
async updatePost(postId: number, data: Types.PostCreateSchema) {
|
|
return this.request<Types.PostDetailSchema>(`/api/blog/admin/posts/${postId}`, {
|
|
method: 'PUT',
|
|
body: JSON.stringify(data),
|
|
});
|
|
}
|
|
|
|
async listAdminBlogPosts(params?: {
|
|
status?: string;
|
|
search?: string;
|
|
mine?: boolean;
|
|
limit?: number;
|
|
offset?: number;
|
|
}) {
|
|
const query = new URLSearchParams();
|
|
if (params?.status) query.set('status', params.status);
|
|
if (params?.search) query.set('search', params.search);
|
|
if (params?.mine != null) query.set('mine', String(params.mine));
|
|
if (params?.limit != null) query.set('limit', String(params.limit));
|
|
if (params?.offset != null) query.set('offset', String(params.offset));
|
|
return this.request<Types.PostListSchema[]>(`/api/blog/admin/posts${query.toString() ? `?${query.toString()}` : ''}`);
|
|
}
|
|
|
|
async listBlogWriters() {
|
|
return this.request<NonNullable<Types.PostListSchema['writers']>>('/api/blog/admin/writers');
|
|
}
|
|
|
|
async getAdminBlogPost(postId: number) {
|
|
return this.request<Types.PostDetailSchema>(`/api/blog/admin/posts/${postId}`);
|
|
}
|
|
|
|
async submitBlogPost(postId: number) {
|
|
return this.request<Types.PostDetailSchema>(`/api/blog/admin/posts/${postId}/submit`, {
|
|
method: 'POST',
|
|
});
|
|
}
|
|
|
|
async reviewBlogPost(postId: number, data: Types.PostReviewSchema) {
|
|
return this.request<Types.PostDetailSchema>(`/api/blog/admin/posts/${postId}/review`, {
|
|
method: 'POST',
|
|
body: JSON.stringify(data),
|
|
});
|
|
}
|
|
|
|
async listBlogPostAssets(postId: number) {
|
|
return this.request<Types.PostAssetSchema[]>(`/api/blog/admin/posts/${postId}/assets`);
|
|
}
|
|
|
|
async uploadBlogPostFeaturedImage(postId: number, file: File) {
|
|
const formData = new FormData();
|
|
formData.append('file', file);
|
|
|
|
const token = this.getStorageValue('access_token');
|
|
const response = await fetch(`${this.baseUrl}/api/blog/admin/posts/${postId}/featured-image`, {
|
|
method: 'POST',
|
|
headers: {
|
|
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
|
},
|
|
body: formData,
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const error = (await response.json().catch(() => ({}))) as ApiErrorBody;
|
|
throw new Error(error.error || error.detail || 'Featured image upload failed');
|
|
}
|
|
|
|
return response.json() as Promise<Types.PostDetailSchema>;
|
|
}
|
|
|
|
async deleteBlogPostFeaturedImage(postId: number) {
|
|
return this.request<Types.PostDetailSchema>(`/api/blog/admin/posts/${postId}/featured-image`, {
|
|
method: 'DELETE',
|
|
});
|
|
}
|
|
|
|
async uploadBlogPostAsset(
|
|
postId: number,
|
|
file: File,
|
|
data: { title?: string; alt_text?: string; caption?: string } = {},
|
|
) {
|
|
const formData = new FormData();
|
|
formData.append('file', file);
|
|
formData.append('title', data.title ?? '');
|
|
formData.append('alt_text', data.alt_text ?? '');
|
|
formData.append('caption', data.caption ?? '');
|
|
|
|
const token = this.getStorageValue('access_token');
|
|
const response = await fetch(`${this.baseUrl}/api/blog/admin/posts/${postId}/assets`, {
|
|
method: 'POST',
|
|
headers: {
|
|
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
|
},
|
|
body: formData,
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const error = (await response.json().catch(() => ({}))) as ApiErrorBody;
|
|
throw new Error(error.error || error.detail || 'Asset upload failed');
|
|
}
|
|
|
|
return response.json() as Promise<Types.PostAssetSchema>;
|
|
}
|
|
|
|
uploadBlogPostAssetWithProgress(
|
|
postId: number,
|
|
file: File,
|
|
data: { title?: string; alt_text?: string; caption?: string } = {},
|
|
onProgress?: (progress: number) => void,
|
|
) {
|
|
const formData = new FormData();
|
|
formData.append('file', file);
|
|
formData.append('title', data.title ?? '');
|
|
formData.append('alt_text', data.alt_text ?? '');
|
|
formData.append('caption', data.caption ?? '');
|
|
|
|
const token = this.getStorageValue('access_token');
|
|
|
|
return new Promise<Types.PostAssetSchema>((resolve, reject) => {
|
|
const request = new XMLHttpRequest();
|
|
request.open('POST', `${this.baseUrl}/api/blog/admin/posts/${postId}/assets`);
|
|
if (token) {
|
|
request.setRequestHeader('Authorization', `Bearer ${token}`);
|
|
}
|
|
|
|
request.upload.onprogress = (event) => {
|
|
if (!event.lengthComputable) return;
|
|
onProgress?.(Math.round((event.loaded / event.total) * 100));
|
|
};
|
|
|
|
request.onload = () => {
|
|
let body: (Types.PostAssetSchema & ApiErrorBody) | null = null;
|
|
try {
|
|
body = request.responseText ? JSON.parse(request.responseText) as Types.PostAssetSchema & ApiErrorBody : null;
|
|
} catch {
|
|
body = null;
|
|
}
|
|
if (request.status >= 200 && request.status < 300 && body) {
|
|
onProgress?.(100);
|
|
resolve(body);
|
|
return;
|
|
}
|
|
reject(new Error(body?.error || body?.detail || 'Asset upload failed'));
|
|
};
|
|
|
|
request.onerror = () => reject(new Error('Asset upload failed'));
|
|
request.send(formData);
|
|
});
|
|
}
|
|
|
|
async deleteBlogPostAsset(postId: number, assetId: number) {
|
|
return this.request<Types.MessageSchema>(`/api/blog/admin/posts/${postId}/assets/${assetId}`, {
|
|
method: 'DELETE',
|
|
});
|
|
}
|
|
|
|
async deletePost(slug: string) {
|
|
return this.request<Types.MessageSchema>(`/api/blog/posts/${encodeURIComponent(slug)}`, {
|
|
method: 'DELETE',
|
|
});
|
|
}
|
|
|
|
async listDeletedPosts() {
|
|
return this.request<Types.PostListSchema[]>('/api/blog/deleted/posts');
|
|
}
|
|
|
|
async restorePost(postId: number) {
|
|
return this.request<Types.MessageSchema>(`/api/blog/deleted/posts/${postId}/restore`, {
|
|
method: 'POST',
|
|
});
|
|
}
|
|
|
|
// Comments
|
|
async getComments(slug: string) {
|
|
return this.request<Types.CommentSchema[]>(`/api/blog/posts/${encodeURIComponent(slug)}/comments`);
|
|
}
|
|
|
|
async createComment(slug: string, data: Types.CommentCreateSchema) {
|
|
return this.request<Types.CommentSchema>(`/api/blog/posts/${encodeURIComponent(slug)}/comments`, {
|
|
method: 'POST',
|
|
body: JSON.stringify(data),
|
|
});
|
|
}
|
|
|
|
async updateComment(commentId: number, data: Types.CommentUpdateSchema) {
|
|
return this.request<Types.CommentSchema>(`/api/blog/comments/${commentId}`, {
|
|
method: 'PUT',
|
|
body: JSON.stringify(data),
|
|
});
|
|
}
|
|
|
|
async hideComment(commentId: number, note?: string) {
|
|
return this.request<Types.MessageSchema>(`/api/blog/comments/${commentId}/hide`, {
|
|
method: 'POST',
|
|
body: JSON.stringify({ note: note ?? '' }),
|
|
});
|
|
}
|
|
|
|
async unhideComment(commentId: number) {
|
|
return this.request<Types.MessageSchema>(`/api/blog/comments/${commentId}/unhide`, {
|
|
method: 'POST',
|
|
});
|
|
}
|
|
|
|
async deleteComment(commentId: number, note?: string) {
|
|
return this.request<Types.MessageSchema>(`/api/blog/comments/${commentId}/delete`, {
|
|
method: 'POST',
|
|
body: JSON.stringify({ note: note ?? '' }),
|
|
});
|
|
}
|
|
|
|
async listDeletedComments() {
|
|
return this.request<Types.CommentSchema[]>('/api/blog/deleted/comments');
|
|
}
|
|
|
|
async restoreComment(commentId: number) {
|
|
return this.request<Types.MessageSchema>(`/api/blog/deleted/comments/${commentId}/restore`, {
|
|
method: 'POST',
|
|
});
|
|
}
|
|
|
|
// Likes
|
|
async toggleLike(slug: string) {
|
|
return this.request<Types.BlogInteractionSchema>(`/api/blog/posts/${encodeURIComponent(slug)}/like`, {
|
|
method: 'POST',
|
|
});
|
|
}
|
|
|
|
async toggleSave(slug: string) {
|
|
return this.request<Types.BlogInteractionSchema>(`/api/blog/posts/${encodeURIComponent(slug)}/save`, {
|
|
method: 'POST',
|
|
});
|
|
}
|
|
|
|
async getBlogInteraction(slug: string) {
|
|
return this.request<Types.BlogInteractionSchema>(`/api/blog/posts/${encodeURIComponent(slug)}/interaction`);
|
|
}
|
|
|
|
async getLikesCount(slug: string) {
|
|
return this.request<Types.MessageSchema>(`/api/blog/posts/${encodeURIComponent(slug)}/likes`);
|
|
}
|
|
|
|
async getMyBlogActivity() {
|
|
return this.request<Types.BlogProfileActivitySchema>('/api/blog/me/activity');
|
|
}
|
|
|
|
// Categories
|
|
async getCategories() {
|
|
return this.request<Types.CategorySchema[]>('/api/blog/categories');
|
|
}
|
|
|
|
async listAdminCategories() {
|
|
return this.request<Types.AdminCategorySchema[]>('/api/blog/admin/categories');
|
|
}
|
|
|
|
async createCategory(data: Types.CategoryWriteSchema) {
|
|
return this.request<Types.AdminCategorySchema>('/api/blog/admin/categories', {
|
|
method: 'POST',
|
|
body: JSON.stringify(data),
|
|
});
|
|
}
|
|
|
|
async updateCategory(categoryId: number, data: Types.CategoryWriteSchema) {
|
|
return this.request<Types.AdminCategorySchema>(`/api/blog/admin/categories/${categoryId}`, {
|
|
method: 'PUT',
|
|
body: JSON.stringify(data),
|
|
});
|
|
}
|
|
|
|
async deleteCategory(categoryId: number) {
|
|
return this.request<Types.MessageSchema>(`/api/blog/admin/categories/${categoryId}`, {
|
|
method: 'DELETE',
|
|
});
|
|
}
|
|
|
|
async getCategory(slug: string) {
|
|
return this.request<Types.CategorySchema>(`/api/blog/categories/${slug}`);
|
|
}
|
|
|
|
async listDeletedCategories() {
|
|
return this.request<Types.CategorySchema[]>('/api/blog/deleted/categories');
|
|
}
|
|
|
|
async restoreCategory(categoryId: number) {
|
|
return this.request<Types.MessageSchema>(`/api/blog/deleted/categories/${categoryId}/restore`, {
|
|
method: 'POST',
|
|
});
|
|
}
|
|
|
|
// Tags
|
|
async getTags() {
|
|
return this.request<Types.TagSchema[]>('/api/blog/tags');
|
|
}
|
|
|
|
async listAdminTags() {
|
|
return this.request<Types.AdminTagSchema[]>('/api/blog/admin/tags');
|
|
}
|
|
|
|
async createTag(data: Types.TagWriteSchema) {
|
|
return this.request<Types.AdminTagSchema>('/api/blog/admin/tags', {
|
|
method: 'POST',
|
|
body: JSON.stringify(data),
|
|
});
|
|
}
|
|
|
|
async updateTag(tagId: number, data: Types.TagWriteSchema) {
|
|
return this.request<Types.AdminTagSchema>(`/api/blog/admin/tags/${tagId}`, {
|
|
method: 'PUT',
|
|
body: JSON.stringify(data),
|
|
});
|
|
}
|
|
|
|
async deleteTag(tagId: number) {
|
|
return this.request<Types.MessageSchema>(`/api/blog/admin/tags/${tagId}`, {
|
|
method: 'DELETE',
|
|
});
|
|
}
|
|
|
|
async getTag(slug: string) {
|
|
return this.request<Types.TagSchema>(`/api/blog/tags/${slug}`);
|
|
}
|
|
|
|
async listDeletedTags() {
|
|
return this.request<Types.TagSchema[]>('/api/blog/deleted/tags');
|
|
}
|
|
|
|
async restoreTag(tagId: number) {
|
|
return this.request<Types.MessageSchema>(`/api/blog/deleted/tags/${tagId}/restore`, {
|
|
method: 'POST',
|
|
});
|
|
}
|
|
|
|
// ============= Events Endpoints =============
|
|
|
|
async getEvents(params: {
|
|
status?: 'draft' | 'published' | 'cancelled' | 'completed';
|
|
statuses?: Array<'draft' | 'published' | 'cancelled' | 'completed'>; // جدید: چندتا وضعیت
|
|
event_type?: 'online' | 'on_site' | 'hybrid';
|
|
search?: string;
|
|
limit?: number;
|
|
offset?: number;
|
|
} = {}) {
|
|
const q = new URLSearchParams();
|
|
|
|
if (params.statuses?.length) {
|
|
params.statuses.forEach(s => q.append('status', s));
|
|
} else if (params.status) {
|
|
q.set('status', params.status);
|
|
}
|
|
if (params.event_type) q.set('event_type', params.event_type);
|
|
if (params.search) q.set('search', params.search);
|
|
if (params.limit != null) q.set('limit', String(params.limit));
|
|
if (params.offset != null) q.set('offset', String(params.offset));
|
|
const url = `/api/events/${q.toString() ? `?${q.toString()}` : ''}`;
|
|
return this.request<Types.EventListItemSchema[]>(url, { method: 'GET' });
|
|
}
|
|
|
|
async getEventBySlug(slug: string) {
|
|
return this.request<Types.EventDetailSchema>(`/api/events/slug/${encodeURIComponent(slug)}`, { method: 'GET' });
|
|
}
|
|
|
|
async getEventAdminDetail(eventId: number) {
|
|
return this.request<Types.EventAdminDetailSchema>(`/api/events/${eventId}/admin-detail`);
|
|
}
|
|
|
|
async listEventRegistrationsAdmin(
|
|
eventId: number,
|
|
params?: {
|
|
statuses?: string[];
|
|
university?: string;
|
|
major?: string;
|
|
search?: string;
|
|
limit?: number;
|
|
offset?: number;
|
|
}
|
|
) {
|
|
const query = new URLSearchParams();
|
|
if (params?.statuses?.length) {
|
|
params.statuses.forEach((status) => query.append('status', status));
|
|
}
|
|
if (params?.university) query.set('university', params.university);
|
|
if (params?.major) query.set('major', params.major);
|
|
if (params?.search) query.set('search', params.search);
|
|
if (params?.limit != null) query.set('limit', String(params.limit));
|
|
if (params?.offset != null) query.set('offset', String(params.offset));
|
|
return this.request<Types.PaginatedResponse<Types.RegistrationAdminSchema>>(
|
|
`/api/events/${eventId}/admin-registrations${query.toString() ? `?${query.toString()}` : ''}`
|
|
);
|
|
}
|
|
|
|
async updateEvent(eventId: number, data: Types.EventUpdateSchema) {
|
|
return this.request<Types.EventDetailSchema>(`/api/events/${eventId}`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(data),
|
|
});
|
|
}
|
|
|
|
async createEvent(data: Types.EventCreateSchema) {
|
|
return this.request<Types.EventDetailSchema>('/api/events/', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(data),
|
|
});
|
|
}
|
|
|
|
async uploadEventFeaturedImage(eventId: number, file: File) {
|
|
const formData = new FormData();
|
|
formData.append('file', file);
|
|
const token = this.getStorageValue('access_token');
|
|
const response = await fetch(`${this.baseUrl}/api/events/${eventId}/featured-image`, {
|
|
method: 'POST',
|
|
headers: {
|
|
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
|
},
|
|
body: formData,
|
|
});
|
|
if (!response.ok) {
|
|
const body = (await response.json().catch(() => ({}))) as ApiErrorBody;
|
|
throw new Error(body.error || body.detail || 'Event image upload failed');
|
|
}
|
|
return response.json() as Promise<Types.EventDetailSchema>;
|
|
}
|
|
|
|
async deleteEventFeaturedImage(eventId: number) {
|
|
return this.request<Types.EventDetailSchema>(`/api/events/${eventId}/featured-image`, {
|
|
method: 'DELETE',
|
|
});
|
|
}
|
|
|
|
async listEventGallery(eventId: number) {
|
|
return this.request<Types.EventGalleryItem[]>(`/api/events/${eventId}/gallery`);
|
|
}
|
|
|
|
async uploadEventGalleryImage(eventId: number, file: File, data: { title?: string; alt_text?: string } = {}) {
|
|
const formData = new FormData();
|
|
formData.append('file', file);
|
|
if (data.title) formData.append('title', data.title);
|
|
if (data.alt_text) formData.append('alt_text', data.alt_text);
|
|
const token = this.getStorageValue('access_token');
|
|
const response = await fetch(`${this.baseUrl}/api/events/${eventId}/gallery`, {
|
|
method: 'POST',
|
|
headers: {
|
|
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
|
},
|
|
body: formData,
|
|
});
|
|
if (!response.ok) {
|
|
const body = (await response.json().catch(() => ({}))) as ApiErrorBody;
|
|
throw new Error(body.error || body.detail || 'Event gallery upload failed');
|
|
}
|
|
return response.json() as Promise<Types.EventGalleryItem>;
|
|
}
|
|
|
|
async deleteEventGalleryImage(eventId: number, imageId: number) {
|
|
return this.request<Types.MessageSchema>(`/api/events/${eventId}/gallery/${imageId}`, {
|
|
method: 'DELETE',
|
|
});
|
|
}
|
|
|
|
async deleteEvent(eventId: number) {
|
|
return this.request<Types.MessageSchema>(`/api/events/${eventId}`, {
|
|
method: 'DELETE',
|
|
});
|
|
}
|
|
|
|
async registerForEvent(eventId: number, discountCode?: string | null) {
|
|
const payload = (discountCode ?? '').trim();
|
|
const init: RequestInit = { method: 'POST' };
|
|
if (payload) {
|
|
init.headers = { 'Content-Type': 'application/json' };
|
|
init.body = JSON.stringify({ discount_code: payload });
|
|
}
|
|
return this.request<Types.EventRegistrationSchema>(`/api/events/${eventId}/register`, init);
|
|
}
|
|
|
|
async ChangeRegistrationStatus(registrationId: number, status: string) {
|
|
return this.request(
|
|
`/api/events/registrations/${registrationId}`,
|
|
{
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ status: status }),
|
|
}
|
|
);
|
|
}
|
|
|
|
async listEventRegistrations(eventId: number, limit = 20, offset = 0) {
|
|
const url = `/api/events/${eventId}/registrations?limit=${limit}&offset=${offset}`;
|
|
return this.request<Types.EventRegistrationSchema[]>(url, { method: 'GET' });
|
|
}
|
|
|
|
async cancelEventRegistration(eventId: number) {
|
|
return this.request<Types.MessageSchema>(`/api/events/${eventId}/register`, {
|
|
method: 'DELETE',
|
|
});
|
|
}
|
|
|
|
async verifyMyRegistration(ticket_id: string) {
|
|
return this.request<{
|
|
event_image: string;
|
|
event_title: string;
|
|
event_type: string;
|
|
ticket_id: string;
|
|
status: string;
|
|
registered_at: string;
|
|
success_markdown: string;
|
|
}>(`/api/events/registerations/verify/${ticket_id}`, {method: 'GET'});
|
|
}
|
|
|
|
async getMyRegistrations() {
|
|
return this.request<Types.MyEventRegistrationSchema[]>(
|
|
`/api/events/my-registrations`,
|
|
{ method: 'GET' }
|
|
);
|
|
}
|
|
|
|
async getRegistrationStatus(eventId: number) {
|
|
return this.request<Types.RegistrationStatusSchema>(
|
|
`/api/events/${eventId}/is-registered`,
|
|
{ method: 'GET' }
|
|
);
|
|
}
|
|
|
|
// ============= Payment Endpoints =============
|
|
async createPayment(input: {
|
|
event_id: number;
|
|
description: string;
|
|
discount_code?: string | null;
|
|
mobile?: string | null;
|
|
email?: string | null;
|
|
}) {
|
|
return this.request<Types.CreatePaymentOut>(
|
|
'/api/payments/create',
|
|
{
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(input),
|
|
}
|
|
);
|
|
}
|
|
|
|
async getPaymentByRef(refId: string) {
|
|
return this.request<{
|
|
ref_id: string;
|
|
authority: string;
|
|
base_amount: number;
|
|
discount_amount: number;
|
|
amount: number;
|
|
status: 'INIT' | 'PENDING' | 'PAID' | 'FAILED' | 'CANCELED';
|
|
verified_at?: string | null;
|
|
event: {
|
|
id: number;
|
|
title: string;
|
|
slug: string;
|
|
image_url?: string | null;
|
|
success_markdown?: string | null;
|
|
};
|
|
}>(`/api/payments/by-ref/${encodeURIComponent(refId)}`, { method: 'GET' });
|
|
}
|
|
|
|
async checkDiscountCode(event_id: number, code: string) {
|
|
return this.request<{
|
|
discount_amount: number;
|
|
final_price: number;
|
|
}>(
|
|
`/api/payments/coupon/check`,
|
|
{
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({code: code, event_id: event_id}),
|
|
}
|
|
);
|
|
}
|
|
|
|
async listDiscountCodes(params?: {
|
|
search?: string;
|
|
is_active?: boolean;
|
|
type?: 'percent' | 'fixed';
|
|
limit?: number;
|
|
offset?: number;
|
|
}) {
|
|
const query = new URLSearchParams();
|
|
if (params?.search) query.set('search', params.search);
|
|
if (params?.is_active != null) query.set('is_active', String(params.is_active));
|
|
if (params?.type) query.set('type', params.type);
|
|
if (params?.limit != null) query.set('limit', String(params.limit));
|
|
if (params?.offset != null) query.set('offset', String(params.offset));
|
|
return this.request<Types.PagedDiscountCodeSchema>(
|
|
`/api/payments/admin/discount-codes${query.toString() ? `?${query.toString()}` : ''}`,
|
|
);
|
|
}
|
|
|
|
async createDiscountCode(data: Types.DiscountCodeWriteSchema) {
|
|
return this.request<Types.DiscountCodeSchema>('/api/payments/admin/discount-codes', {
|
|
method: 'POST',
|
|
body: JSON.stringify(data),
|
|
});
|
|
}
|
|
|
|
async updateDiscountCode(codeId: number, data: Types.DiscountCodeWriteSchema) {
|
|
return this.request<Types.DiscountCodeSchema>(`/api/payments/admin/discount-codes/${codeId}`, {
|
|
method: 'PUT',
|
|
body: JSON.stringify(data),
|
|
});
|
|
}
|
|
|
|
async deleteDiscountCode(codeId: number) {
|
|
return this.request<Types.MessageSchema>(`/api/payments/admin/discount-codes/${codeId}`, {
|
|
method: 'DELETE',
|
|
});
|
|
}
|
|
|
|
// ============= Gallery Endpoints =============
|
|
|
|
async getGalleryImages(params?: {
|
|
page?: number;
|
|
limit?: number;
|
|
tag?: string;
|
|
}) {
|
|
const queryParams = new URLSearchParams();
|
|
if (params?.page) queryParams.append('page', params.page.toString());
|
|
if (params?.limit) queryParams.append('limit', params.limit.toString());
|
|
if (params?.tag) queryParams.append('tag', params.tag);
|
|
|
|
const query = queryParams.toString();
|
|
return this.request<Types.GalleryImageSchema[]>(`/api/gallery/images${query ? `?${query}` : ''}`);
|
|
}
|
|
|
|
async uploadGalleryImage(file: File, data: Types.GalleryImageCreateSchema) {
|
|
const formData = new FormData();
|
|
formData.append('file', file);
|
|
formData.append('title', data.title);
|
|
if (data.description) formData.append('description', data.description);
|
|
if (data.tag_ids) formData.append('tag_ids', JSON.stringify(data.tag_ids));
|
|
|
|
const token = this.getStorageValue('access_token');
|
|
const response = await fetch(`${this.baseUrl}/api/gallery/images`, {
|
|
method: 'POST',
|
|
headers: {
|
|
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
|
},
|
|
body: formData,
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const error: Types.ErrorSchema = await response.json().catch(() => ({
|
|
detail: 'خطای آپلود تصویر',
|
|
}));
|
|
throw new Error(error.detail);
|
|
}
|
|
|
|
return response.json() as Promise<Types.GalleryImageSchema>;
|
|
}
|
|
|
|
async deleteGalleryImage(imageId: number) {
|
|
return this.request<Types.MessageSchema>(`/api/gallery/images/${imageId}`, {
|
|
method: 'DELETE',
|
|
});
|
|
}
|
|
|
|
async getMajors(params?: { search?: string; limit?: number; offset?: number }): Promise<Types.MajorOption[]> {
|
|
const data = await this.getMajorsPaged({ limit: 100, offset: 0, ...params });
|
|
return data.results.map((item) => ({ code: item.code, label: item.label, id: item.id, name: item.name, user_count: item.user_count }));
|
|
}
|
|
|
|
async getUniversities(params?: { search?: string; limit?: number; offset?: number }): Promise<Types.MajorOption[]> {
|
|
const data = await this.getUniversitiesPaged({ limit: 100, offset: 0, ...params });
|
|
return data.results.map((item) => ({ code: item.code, label: item.label, id: item.id, name: item.name, user_count: item.user_count }));
|
|
}
|
|
|
|
async getMajorsPaged(params?: { search?: string; limit?: number; offset?: number }) {
|
|
const query = new URLSearchParams();
|
|
if (params?.search) query.set('search', params.search);
|
|
if (params?.limit != null) query.set('limit', String(params.limit));
|
|
if (params?.offset != null) query.set('offset', String(params.offset));
|
|
return this.request<Types.PagedMetaOptionSchema>(`/api/meta/majors${query.toString() ? `?${query.toString()}` : ''}`);
|
|
}
|
|
|
|
async getUniversitiesPaged(params?: { search?: string; limit?: number; offset?: number }) {
|
|
const query = new URLSearchParams();
|
|
if (params?.search) query.set('search', params.search);
|
|
if (params?.limit != null) query.set('limit', String(params.limit));
|
|
if (params?.offset != null) query.set('offset', String(params.offset));
|
|
return this.request<Types.PagedMetaOptionSchema>(`/api/meta/universities${query.toString() ? `?${query.toString()}` : ''}`);
|
|
}
|
|
|
|
async listAdminMajors(params?: { search?: string; limit?: number; offset?: number }) {
|
|
const query = new URLSearchParams();
|
|
if (params?.search) query.set('search', params.search);
|
|
if (params?.limit != null) query.set('limit', String(params.limit));
|
|
if (params?.offset != null) query.set('offset', String(params.offset));
|
|
return this.request<Types.PagedMetaOptionSchema>(`/api/meta/admin/majors${query.toString() ? `?${query.toString()}` : ''}`);
|
|
}
|
|
|
|
async createMajor(data: Types.MetaOptionWriteSchema) {
|
|
return this.request<Types.MetaOptionSchema>('/api/meta/admin/majors', {
|
|
method: 'POST',
|
|
body: JSON.stringify(data),
|
|
});
|
|
}
|
|
|
|
async updateMajor(id: number, data: Types.MetaOptionWriteSchema) {
|
|
return this.request<Types.MetaOptionSchema>(`/api/meta/admin/majors/${id}`, {
|
|
method: 'PUT',
|
|
body: JSON.stringify(data),
|
|
});
|
|
}
|
|
|
|
async deleteMajor(id: number) {
|
|
return this.request<Types.MessageSchema>(`/api/meta/admin/majors/${id}`, {
|
|
method: 'DELETE',
|
|
});
|
|
}
|
|
|
|
async listAdminUniversities(params?: { search?: string; limit?: number; offset?: number }) {
|
|
const query = new URLSearchParams();
|
|
if (params?.search) query.set('search', params.search);
|
|
if (params?.limit != null) query.set('limit', String(params.limit));
|
|
if (params?.offset != null) query.set('offset', String(params.offset));
|
|
return this.request<Types.PagedMetaOptionSchema>(`/api/meta/admin/universities${query.toString() ? `?${query.toString()}` : ''}`);
|
|
}
|
|
|
|
async createUniversity(data: Types.MetaOptionWriteSchema) {
|
|
return this.request<Types.MetaOptionSchema>('/api/meta/admin/universities', {
|
|
method: 'POST',
|
|
body: JSON.stringify(data),
|
|
});
|
|
}
|
|
|
|
async updateUniversity(id: number, data: Types.MetaOptionWriteSchema) {
|
|
return this.request<Types.MetaOptionSchema>(`/api/meta/admin/universities/${id}`, {
|
|
method: 'PUT',
|
|
body: JSON.stringify(data),
|
|
});
|
|
}
|
|
|
|
async deleteUniversity(id: number) {
|
|
return this.request<Types.MessageSchema>(`/api/meta/admin/universities/${id}`, {
|
|
method: 'DELETE',
|
|
});
|
|
}
|
|
|
|
async subscribeNewsletter(email: string) {
|
|
return this.request<{ message: string, success: boolean }>(
|
|
`/api/communications/newsletter/subscribe/`,
|
|
{
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ email: email }),
|
|
}
|
|
);
|
|
}
|
|
|
|
async getNotifications(params?: { limit?: number; offset?: number; type?: string }) {
|
|
const query = new URLSearchParams();
|
|
if (params?.limit != null) query.set('limit', String(params.limit));
|
|
if (params?.offset != null) query.set('offset', String(params.offset));
|
|
if (params?.type) query.set('type', params.type);
|
|
return this.request<Types.NotificationListSchema>(
|
|
`/api/notifications/${query.toString() ? `?${query.toString()}` : ''}`
|
|
);
|
|
}
|
|
|
|
async markNotificationSeen(id: string) {
|
|
return this.request<Types.NotificationSeenResponseSchema>('/api/notifications/mark-seen', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ id }),
|
|
});
|
|
}
|
|
|
|
async deleteNotification(id: string) {
|
|
return this.request<Types.NotificationDeleteResponseSchema>(`/api/notifications/${encodeURIComponent(id)}`, {
|
|
method: 'DELETE',
|
|
});
|
|
}
|
|
|
|
async markAllNotificationsRead(type?: string) {
|
|
const query = type ? `?type=${encodeURIComponent(type)}` : '';
|
|
return this.request<{ marked_read: number }>(`/api/notifications/mark-all-read${query}`, {
|
|
method: 'POST',
|
|
});
|
|
}
|
|
|
|
async issueNotificationStreamToken() {
|
|
return this.request<Types.NotificationStreamTokenResponseSchema>('/api/notifications/stream-token', {
|
|
method: 'POST',
|
|
});
|
|
}
|
|
|
|
buildNotificationStreamUrl(token: string) {
|
|
const cleanBaseUrl = this.baseUrl.replace(/\/+$/, '');
|
|
return `${cleanBaseUrl}/api/notifications/stream/?token=${encodeURIComponent(token)}`;
|
|
}
|
|
}
|
|
|
|
export const api = new ApiClient(API_BASE_URL);
|