eletrotupi / tcc/ frontend/lib/api/client.ts22627d0d2be10a13b161ae26877b3828677f36e1
12.1 KB | 462 lines Raw
1
import * as SecureStore from "expo-secure-store";
2
import {
3
  User,
4
  AuthResponse,
5
  VerifyTokenResponse,
6
  SignUpPayload,
7
  ResetPasswordRequestResponse,
8
  ResetPasswordResponse,
9
  UserUpdatePayload,
10
  CreateMoodEntryPayload,
11
  SleepRecordPayload,
12
  UpdateSleepRecordPayload,
13
  MoodEntry,
14
  SleepRecord,
15
  SleepRecordResponse,
16
  MoodEntryResponse,
17
  Trigger,
18
  TriggerResponse,
19
  CreateTriggerPayload,
20
  UpdateTriggerPayload,
21
  ActivateResponse,
22
  PaginatedResponse,
23
  InsightType,
24
  InsightPeriod,
25
  InsightMetadata,
26
  Insight,
27
  MedicineRegimen,
28
  MedicineRegimenResponse,
29
  CreateMedicineRegimenPayload,
30
  UpdateMedicineRegimenPayload,
31
  TodayMedicineEntry,
32
  UserPreferencesPayload,
33
  CareAction,
34
  CareActionType,
35
  CareActionResponse,
36
  CreateCareActionPayload,
37
  PatchCareActionPayload,
38
  TriggerMoodLink,
39
  LinkMoodPayload,
40
} from "@/lib/api/types";
41

42
const API_BASE_URL = process.env.EXPO_PUBLIC_API_BASE_URL;
43
const TOKEN_KEY = process.env.EXPO_PUBLIC_TOKEN_KEY;
44

45
class ApiClient {
46
  private onUnauthorizedCallback?: () => void;
47

48
  registerUnauthorizedHandler(cb: () => void) {
49
    this.onUnauthorizedCallback = cb;
50
  }
51

52
  private async getStoredToken(): Promise<string | null> {
53
    try {
54
      return await SecureStore.getItemAsync(TOKEN_KEY!);
55
    } catch (error) {
56
      console.error("Failed to get stored token:", error);
57
      return null;
58
    }
59
  }
60

61
  private async storeToken(token: string): Promise<void> {
62
    try {
63
      await SecureStore.setItemAsync(TOKEN_KEY!, token);
64
    } catch (error) {
65
      console.error("Failed to store token:", error);
66
      throw error;
67
    }
68
  }
69

70
  private async removeToken(): Promise<void> {
71
    try {
72
      await SecureStore.deleteItemAsync(TOKEN_KEY!);
73
    } catch (error) {
74
      console.error("Failed to remove token:", error);
75
    }
76
  }
77

78
  async getStoredAuthData(): Promise<{ token: string | null }> {
79
    const token = await this.getStoredToken();
80
    return { token };
81
  }
82

83
  private async request<T>(
84
    endpoint: string,
85
    options: RequestInit = {},
86
  ): Promise<T> {
87
    const token = await this.getStoredToken();
88
    const isFormData = options.body instanceof FormData;
89

90
    const config: RequestInit = {
91
      ...options,
92
      headers: {
93
        ...(token && { Authorization: `Bearer ${token}` }),
94
        ...(!isFormData && { "Content-Type": "application/json" }),
95
        ...options.headers,
96
      },
97
    };
98

99
    const response = await fetch(`${API_BASE_URL}${endpoint}`, config);
100

101
    if (!response.ok) {
102
      if (response.status === 401) {
103
        this.onUnauthorizedCallback?.();
104
      }
105

106
      const error = await response
107
        .json()
108
        .catch(() => ({ error: "Network error" }));
109

110
      console.error("[API]: ", error, response.status);
111

112
      throw new Error(error.error || `HTTP ${response.status}`);
113
    }
114

115
    return response.json();
116
  }
117

118
  // Auth
119
  async login(email: string, password: string): Promise<AuthResponse> {
120
    const response = await this.request<AuthResponse>("/auth/login", {
121
      method: "POST",
122
      body: JSON.stringify({ email, password }),
123
    });
124

125
    if (response.token) {
126
      await this.storeToken(response.token);
127
    }
128

129
    return response;
130
  }
131

132
  async activate(code: number): Promise<ActivateResponse> {
133
    return this.request("/auth/activate", {
134
      method: "POST",
135
      body: JSON.stringify({ code }),
136
    });
137
  }
138

139
  async requestActivateCode(): Promise<void> {
140
    return this.request("/auth/resend_code", {
141
      method: "POST",
142
    });
143
  }
144

145
  async signup(user: SignUpPayload): Promise<AuthResponse> {
146
    const response = await this.request<AuthResponse>("/users", {
147
      method: "POST",
148
      body: JSON.stringify({ user }),
149
    });
150

151
    await this.storeToken(response.token);
152
    return response;
153
  }
154

155
  async forgotPassword(email: string): Promise<ResetPasswordRequestResponse> {
156
    return this.request("/auth/forgot-password", {
157
      method: "POST",
158
      body: JSON.stringify({ email }),
159
    });
160
  }
161

162
  async resetPassword(
163
    token: string,
164
    password: string,
165
  ): Promise<ResetPasswordResponse> {
166
    return this.request("/auth/reset-password", {
167
      method: "POST",
168
      body: JSON.stringify({ token, newPassword: password }),
169
    });
170
  }
171

172
  async verifyToken(): Promise<VerifyTokenResponse> {
173
    try {
174
      return await this.request("/auth/verify");
175
    } catch (error) {
176
      // Token is invalid or expired
177
      await this.removeToken();
178
      throw error;
179
    }
180
  }
181

182
  async logout(): Promise<void> {
183
    await this.removeToken();
184
  }
185

186
  // User-related
187
  async getMe(): Promise<{ user: User }> {
188
    return this.request('/users/me');
189
  }
190

191
  async updateUser(user: UserUpdatePayload): Promise<{ user: User }> {
192
    return await this.request(`/users/${user.id}`, {
193
      method: "PUT",
194
      body: JSON.stringify({ user }),
195
    });
196
  }
197

198
  async avatar(formData: FormData): Promise<User> {
199
    return await this.request(`/users/me/avatar`, {
200
      method: "POST",
201
      body: formData,
202
      headers: {
203
        "Content-Type": "multipart/form-data",
204
      } as any,
205
    });
206
  }
207

208
  async removeAvatar(): Promise<void> {
209
    return await this.request(`/users/me/avatar`, {
210
      method: "DELETE",
211
    });
212
  }
213

214
  // Mood-related
215
  async createMoodEntry(
216
    mood: CreateMoodEntryPayload,
217
  ): Promise<MoodEntryResponse> {
218
    return await this.request(`/moods`, {
219
      method: "POST",
220
      body: JSON.stringify({ mood }),
221
    });
222
  }
223

224
  async getMoodEntries(params?: {
225
    from?: string;
226
    to?: string;
227
    page?: number;
228
    limit?: number;
229
  }): Promise<PaginatedResponse<MoodEntry>> {
230
    const query = new URLSearchParams();
231
    if (params?.from) query.set("from", params.from);
232
    if (params?.to) query.set("to", params.to);
233
    if (params?.page) query.set("page", String(params.page));
234
    if (params?.limit) query.set("limit", String(params.limit));
235

236
    const qs = query.toString();
237
    return this.request(`/moods${qs ? `?${qs}` : ""}`);
238
  }
239

240
  async getMoodEntry(id: string): Promise<MoodEntry> {
241
    return this.request(`/moods/${id}`);
242
  }
243

244
  async deleteMoodEntry(id: string): Promise<void> {
245
    return this.request(`/moods/${id}`, { method: "DELETE" });
246
  }
247

248
  // Sleep Record
249
  async createSleepRecord(
250
    sleepRecord: SleepRecordPayload,
251
  ): Promise<SleepRecordResponse> {
252
    return await this.request(`/sleep_records`, {
253
      method: "POST",
254
      body: JSON.stringify({ sleepRecord }),
255
    });
256
  }
257

258
  async getSleepRecords(params?: {
259
    from?: string;
260
    to?: string;
261
    page?: number;
262
    limit?: number;
263
  }): Promise<PaginatedResponse<SleepRecord>> {
264
    const query = new URLSearchParams();
265
    if (params?.from) query.set("from", params.from);
266
    if (params?.to) query.set("to", params.to);
267
    if (params?.page) query.set("page", String(params.page));
268
    if (params?.limit) query.set("limit", String(params.limit));
269

270
    const qs = query.toString();
271
    return this.request(`/sleep_records${qs ? `?${qs}` : ""}`);
272
  }
273

274
  async getSleepRecord(id: string): Promise<SleepRecord> {
275
    return this.request(`/sleep_records/${id}`);
276
  }
277

278
  async updateSleepRecord(
279
    id: string,
280
    sleepRecord: UpdateSleepRecordPayload,
281
  ): Promise<SleepRecordResponse> {
282
    return await this.request(`/sleep_records/${id}`, {
283
      method: "PUT",
284
      body: JSON.stringify({ sleepRecord }),
285
    });
286
  }
287

288
  async deleteSleepRecord(id: string): Promise<void> {
289
    return this.request(`/sleep_records/${id}`, { method: "DELETE" });
290
  }
291

292
  // Trigger
293
  async createTrigger(trigger: CreateTriggerPayload): Promise<TriggerResponse> {
294
    return await this.request(`/triggers`, {
295
      method: "POST",
296
      body: JSON.stringify({ trigger }),
297
    });
298
  }
299

300
  async getTriggers(params?: {
301
    from?: string;
302
    to?: string;
303
    page?: number;
304
    limit?: number;
305
  }): Promise<PaginatedResponse<Trigger>> {
306
    const query = new URLSearchParams();
307
    if (params?.from) query.set("from", params.from);
308
    if (params?.to) query.set("to", params.to);
309
    if (params?.page) query.set("page", String(params.page));
310
    if (params?.limit) query.set("limit", String(params.limit));
311

312
    const qs = query.toString();
313
    return this.request(`/triggers${qs ? `?${qs}` : ""}`);
314
  }
315

316
  async getTrigger(id: string): Promise<Trigger> {
317
    return this.request(`/triggers/${id}`);
318
  }
319

320
  async updateTrigger(
321
    id: string,
322
    trigger: UpdateTriggerPayload,
323
  ): Promise<TriggerResponse> {
324
    return await this.request(`/triggers/${id}`, {
325
      method: "PUT",
326
      body: JSON.stringify({ trigger }),
327
    });
328
  }
329

330
  async deleteTrigger(id: string): Promise<void> {
331
    return this.request(`/triggers/${id}`, { method: "DELETE" });
332
  }
333

334
  // Insights
335
  async getInsights(filters?: {
336
    type?: InsightType;
337
    period?: InsightPeriod;
338
    limit?: number;
339
  }): Promise<{ insights: Insight[] }> {
340
    const query = new URLSearchParams();
341
    if (filters?.type) query.set("type", filters.type);
342
    if (filters?.period) query.set("period", filters.period);
343
    if (filters?.limit) query.set("limit", String(filters.limit));
344

345
    const qs = query.toString();
346
    return this.request(`/insights${qs ? `?${qs}` : ""}`);
347
  }
348

349
  // Medicine Regimens
350
  async getMedicineRegimens(): Promise<{ regimens: MedicineRegimen[] }> {
351
    return this.request('/medicine-regimens');
352
  }
353

354
  async getMedicineRegimen(id: string): Promise<{ regimen: MedicineRegimen }> {
355
    return this.request(`/medicine-regimens/${id}`);
356
  }
357

358
  async createMedicineRegimen(
359
    regimen: CreateMedicineRegimenPayload
360
  ): Promise<MedicineRegimenResponse> {
361
    return this.request('/medicine-regimens', {
362
      method: 'POST',
363
      body: JSON.stringify({ regimen })
364
    });
365
  }
366

367
  async updateMedicineRegimen(
368
    id: string,
369
    regimen: UpdateMedicineRegimenPayload
370
  ): Promise<MedicineRegimenResponse> {
371
    return this.request(`/medicine-regimens/${id}`, {
372
      method: 'PATCH',
373
      body: JSON.stringify({ regimen })
374
    });
375
  }
376

377
  async deleteMedicineRegimen(id: string): Promise<void> {
378
    return this.request(`/medicine-regimens/${id}`, { method: 'DELETE' });
379
  }
380

381
  async toggleMedicineRegimen(
382
    id: string,
383
    active: boolean,
384
  ): Promise<MedicineRegimenResponse> {
385
    return this.request(`/medicine-regimens/${id}/active`, {
386
      method: 'PATCH',
387
      body: JSON.stringify({ active }),
388
    });
389
  }
390

391
  async getMedicineRegimensToday(): Promise<{ regimens: TodayMedicineEntry[] }> {
392
    return this.request('/medicine-regimens/today');
393
  }
394

395
  async patchUserMe(payload: UserPreferencesPayload): Promise<{ user: User }> {
396
    return this.request('/users/me', {
397
      method: 'PATCH',
398
      body: JSON.stringify({ user: payload }),
399
    });
400
  }
401

402
  // Care Actions
403
  async getCareActions(
404
    params?: {
405
      type?: CareActionType;
406
      from?: string;
407
      to?: string;
408
      page?: number;
409
      limit?: number
410
    }): Promise<PaginatedResponse<CareAction>> {
411
    const query = new URLSearchParams();
412
    if (params?.type) query.set('type', params.type);
413
    if (params?.from) query.set('from', params.from);
414
    if (params?.to) query.set('to', params.to);
415
    if (params?.page) query.set('page', String(params.page));
416
    if (params?.limit) query.set('limit', String(params.limit));
417

418
    const qs = query.toString();
419

420
    return this.request(`/care-actions${qs ? `?${qs}` : ''}`);
421
  }
422

423
  async getCareAction(id: string): Promise<CareAction> {
424
    return this.request(`/care-actions/${id}`);
425
  }
426

427
  async createCareAction(payload: CreateCareActionPayload): Promise<CareActionResponse> {
428
    return this.request('/care-actions', {
429
      method: 'POST', body: JSON.stringify({ careAction: payload })
430
    });
431
  }
432

433
  async patchCareAction(
434
    id: string,
435
    payload: PatchCareActionPayload,
436
  ): Promise<CareAction> {
437
    return this.request(`/care-actions/${id}`, {
438
      method: 'PATCH',
439
      body: JSON.stringify({ careAction: payload }),
440
    });
441
  }
442

443
  async deleteCareAction(id: string): Promise<void> {
444
    return this.request(`/care-actions/${id}`, { method: 'DELETE' });
445
  }
446

447
  // Trigger to Mood linking
448
  async linkMoodToTrigger(
449
    triggerId: string, payload: LinkMoodPayload
450
  ): Promise<{ link: TriggerMoodLink }> {
451
    return this.request(`/triggers/${triggerId}/link-mood`, {
452
      method: 'POST',
453
      body: JSON.stringify(payload)
454
    });
455
  }
456

457
  async unlinkMoodFromTrigger(triggerId: string, moodId: string): Promise<void> {
458
    return this.request(`/triggers/${triggerId}/link-mood/${moodId}`, { method: 'DELETE' });
459
  }
460
}
461

462
export const apiClient = new ApiClient();