forked from Tadrisino/MathApp_frontend
add a typed API layer for the Tadrisino backend
اپ تا الان هیچ درخواستی به سرور نمیزد و همهی دادهها داخلِ خودِ کامپوننتها
هاردکد بود. این کامیت لایهی ارتباط را اضافه میکند، بدونِ اینکه هیچ صفحهای
را تغییر دهد.
عمداً فقط فایلِ جدید اضافه شده و App.tsx دست نخورده است، چون کارِ در جریانِ
برنچِ updateApp روی همان فایل است و نباید با هم تصادف کنند. وصل کردنِ
صفحهها به این لایه کارِ بعدیست.
هیچ پکیجِ جدیدی اضافه نشد — فقط fetch استاندارد.
چیزهایی که در client.ts هست برای حالتِ خراباند، نه حالتِ خوب:
• timeout روی هر درخواست — وگرنه بکاندِ کند سوکتها را باز نگه میدارد تا
مرورگر به سقفِ ۶ درخواستِ همزمان بخورد و اپ قفل شود.
• backoff با jitter — بدونِ jitter همهی کلاینتها بعد از یک خطا همزمان
دوباره میزنند و موجِ دوم از موجِ اول بدتر است.
• تکرارِ خودکار فقط روی GET؛ POST فقط وقتی Idempotency-Key دارد (ثبتِ پاسخ،
سفارش) تا دابلکلیکِ کاربر دو بار XP یا دو سفارش نسازد.
• refresh تکنسخهای — ۲۰ درخواستِ همزمانِ ۴۰۱ نباید ۲۰ بار refresh بزنند.
• ادغامِ GETهای یکسانِ در جریان.
• احترام به هدرِ Retry-After که بکاند موقعِ throttle میفرستد.
credentials: 'omit' عمدیست: احرازِ هویت توکنِ Bearer در هدر است و سمتِ سرور
CORS_ALLOW_CREDENTIALS=False تنظیم شده.
.env هم به .gitignore اضافه شد؛ قبلاً فقط node_modules داخلش بود و آدرس/
تنظیماتِ محلی میتوانست ناخواسته کامیت شود.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
118
src/api/index.ts
Normal file
118
src/api/index.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
/**
|
||||
* API تدریسینو — تنها جایی که فرانت باید برای صدا زدنِ بکاند به آن نگاه کند.
|
||||
*
|
||||
* import { api } from '@/api';
|
||||
* const home = await api.home();
|
||||
*
|
||||
* هیچجای اپ نباید مستقیم fetch بزند: مسیرِ خام، مدیریتِ توکن و رفتارِ
|
||||
* درستِ خطا همه اینجا یکجا جمعاند.
|
||||
*/
|
||||
import { createHttpClient, ApiError, localTokenStorage } from './client';
|
||||
import type { ClientOptions, HttpClient, RequestOptions, Tokens } from './client';
|
||||
import type {
|
||||
AnswerResult, AskResponse, Grade, HomeResponse, LessonIntro, Plan, Question, UserProfile,
|
||||
} from './types';
|
||||
|
||||
export { ApiError, localTokenStorage, createHttpClient };
|
||||
export type { Tokens, HttpClient, ClientOptions, RequestOptions };
|
||||
export * from './types';
|
||||
|
||||
/** پرسش از دستیار کند است (تولیدِ متن)؛ ۱۵ ثانیهی پیشفرض کم است. */
|
||||
const ASK_TIMEOUT_MS = 45_000;
|
||||
|
||||
export function createApi(options: ClientOptions = {}) {
|
||||
const http = createHttpClient(options);
|
||||
|
||||
return {
|
||||
http,
|
||||
|
||||
/* ── ورود ─────────────────────────────────────────────────────────── */
|
||||
auth: {
|
||||
/** «شروع سریع» بدونِ شماره. برای دمو کافیست. */
|
||||
async guest(deviceId: string) {
|
||||
const data = await http.post<{ userId: string; token: string; isNewUser: boolean }>(
|
||||
'/auth/session', { type: 'guest', deviceId },
|
||||
);
|
||||
http.storage.set({ token: data.token, refreshToken: '' });
|
||||
return data;
|
||||
},
|
||||
/** گامِ ۱ — در حالتِ توسعه `devOtp` هم برمیگردد. */
|
||||
requestOtp(phone: string) {
|
||||
return http.post<{ success: boolean; devOtp?: string }>(
|
||||
'/auth/otp/request', { phone },
|
||||
);
|
||||
},
|
||||
/** گامِ ۲ — نامِ فیلد در سرور `otp` است، نه `code`. */
|
||||
async login(phone: string, otp: string) {
|
||||
const data = await http.post<{ userId: string } & Tokens>(
|
||||
'/auth/login', { phone, otp },
|
||||
);
|
||||
http.storage.set({ token: data.token, refreshToken: data.refreshToken });
|
||||
return data;
|
||||
},
|
||||
logout() {
|
||||
http.storage.set(null);
|
||||
},
|
||||
isSignedIn() {
|
||||
return Boolean(http.storage.get()?.token);
|
||||
},
|
||||
},
|
||||
|
||||
/* ── کاربر ────────────────────────────────────────────────────────── */
|
||||
profile: {
|
||||
get: (opts?: RequestOptions) => http.get<UserProfile>('/user/profile', opts),
|
||||
update: (patch: Partial<Pick<UserProfile, 'name' | 'avatar' | 'birthYear' | 'parentPhone' | 'bio'>>) =>
|
||||
http.patch<UserProfile>('/user/profile', patch),
|
||||
setGrade: (gradeId: number) =>
|
||||
http.post<{ success: boolean; gradeId: number }>('/user/grade', { gradeId }),
|
||||
},
|
||||
|
||||
/** پایهها را هاردکد نکنید — اضافه میشوند. */
|
||||
grades: (opts?: RequestOptions) =>
|
||||
http.get<{ grades: Grade[] }>('/grades', opts).then((r) => r.grades),
|
||||
|
||||
/** بدونِ gradeId، پایهی ذخیرهشدهی خودِ کاربر استفاده میشود. */
|
||||
home: (gradeId?: number, opts?: RequestOptions) =>
|
||||
http.get<HomeResponse>(gradeId ? `/home?gradeId=${gradeId}` : '/home', opts),
|
||||
|
||||
/* ── درس و آزمون ──────────────────────────────────────────────────── */
|
||||
lessons: {
|
||||
intro: (lessonId: number, opts?: RequestOptions) =>
|
||||
http.get<LessonIntro>(`/lessons/${lessonId}/intro`, opts),
|
||||
questions: (lessonId: number, opts?: RequestOptions) =>
|
||||
http.get<{ questions: Question[] }>(`/lessons/${lessonId}/questions`, opts)
|
||||
.then((r) => r.questions),
|
||||
/**
|
||||
* ثبتِ پاسخ. کلیدِ یکتا میگیرد تا دابلکلیکِ کاربر یا تکرارِ خودکارِ
|
||||
* شبکه، دو بار XP ندهد.
|
||||
*/
|
||||
answer: (lessonId: number, questionId: number, choiceId: number) =>
|
||||
http.create<AnswerResult>(`/lessons/${lessonId}/answer`, { questionId, choiceId }),
|
||||
},
|
||||
|
||||
/* ── دستیار ───────────────────────────────────────────────────────── */
|
||||
ai: {
|
||||
/**
|
||||
* فقط `lessonId` کافیست: سرور خودش میفهمد این درس مالِ کدام پایه و
|
||||
* فصل است و پاسخ را به کتاب مستند میکند.
|
||||
*/
|
||||
ask: (question: string, ctx: { lessonId?: number; conversationId?: number } = {}, opts?: RequestOptions) =>
|
||||
http.post<AskResponse>('/ai/ask', { question, ...ctx }, {
|
||||
timeoutMs: ASK_TIMEOUT_MS,
|
||||
...opts,
|
||||
}),
|
||||
/** `reason` اختیاریست ولی برای تحلیلِ کیفیت خیلی ارزش دارد. */
|
||||
feedback: (messageId: number, vote: 'up' | 'down', reason?: string) =>
|
||||
http.post<{ success: boolean }>(`/ai/messages/${messageId}/feedback`, { vote, reason }),
|
||||
},
|
||||
|
||||
/* ── اشتراک ───────────────────────────────────────────────────────── */
|
||||
plans: (opts?: RequestOptions) =>
|
||||
http.get<{ plans: Plan[] }>('/subscription/plans', opts).then((r) => r.plans),
|
||||
};
|
||||
}
|
||||
|
||||
export type Api = ReturnType<typeof createApi>;
|
||||
|
||||
/** نمونهی پیشفرض — برای حالتِ عادی همین کافیست. */
|
||||
export const api = createApi();
|
||||
Reference in New Issue
Block a user