eletrotupi / tcc/ frontend/lib/api/client.ts9879196f5eaf3242b6682db701cf97ceb37b4487
11.8 KB | 448 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 async getStoredToken(): Promise<string | null> {
47
    try {
48
      return await SecureStore.getItemAsync(TOKEN_KEY!);
49
    } catch (error) {
50
      console.error("Failed to get stored token:", error);
51
      return null;
52
    }
53
  }
54

55
  private async storeToken(token: string): Promise<void> {
56
    try {
57
      await SecureStore.setItemAsync(TOKEN_KEY!, token);
58
    } catch (error) {
59
      console.error("Failed to store token:", error);
60
      throw error;
61
    }
62
  }
63

64
  private async removeToken(): Promise<void> {
65
    try {
66
      await SecureStore.deleteItemAsync(TOKEN_KEY!);
67
    } catch (error) {
68
      console.error("Failed to remove token:", error);
69
    }
70
  }
71

72
  async getStoredAuthData(): Promise<{ token: string | null }> {
73
    const token = await this.getStoredToken();
74
    return { token };
75
  }
76

77
  private async request<T>(
78
    endpoint: string,
79
    options: RequestInit = {},
80
  ): Promise<T> {
81
    const token = await this.getStoredToken();
82
    const isFormData = options.body instanceof FormData;
83

84
    const config: RequestInit = {
85
      ...options,
86
      headers: {
87
        ...(token && { Authorization: `Bearer ${token}` }),
88
        ...(!isFormData && { "Content-Type": "application/json" }),
89
        ...options.headers,
90
      },
91
    };
92

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

95
    if (!response.ok) {
96
      const error = await response
97
        .json()
98
        .catch(() => ({ error: "Network error" }));
99

100
      console.error("[API]: ", error, response.status);
101

102
      throw new Error(error.error || `HTTP ${response.status}`);
103
    }
104

105
    return response.json();
106
  }
107

108
  // Auth
109
  async login(email: string, password: string): Promise<AuthResponse> {
110
    const response = await this.request<AuthResponse>("/auth/login", {
111
      method: "POST",
112
      body: JSON.stringify({ email, password }),
113
    });
114

115
    if (response.token) {
116
      await this.storeToken(response.token);
117
    }
118

119
    return response;
120
  }
121

122
  async activate(code: number): Promise<ActivateResponse> {
123
    return this.request("/auth/activate", {
124
      method: "POST",
125
      body: JSON.stringify({ code }),
126
    });
127
  }
128

129
  async requestActivateCode(): Promise<void> {
130
    return this.request("/auth/resend_code", {
131
      method: "POST",
132
    });
133
  }
134

135
  async signup(user: SignUpPayload): Promise<AuthResponse> {
136
    const response = await this.request<AuthResponse>("/users", {
137
      method: "POST",
138
      body: JSON.stringify({ user }),
139
    });
140

141
    await this.storeToken(response.token);
142
    return response;
143
  }
144

145
  async forgotPassword(email: string): Promise<ResetPasswordRequestResponse> {
146
    return this.request("/auth/forgot-password", {
147
      method: "POST",
148
      body: JSON.stringify({ email }),
149
    });
150
  }
151

152
  async resetPassword(
153
    token: string,
154
    password: string,
155
  ): Promise<ResetPasswordResponse> {
156
    return this.request("/auth/reset-password", {
157
      method: "POST",
158
      body: JSON.stringify({ token, newPassword: password }),
159
    });
160
  }
161

162
  async verifyToken(): Promise<VerifyTokenResponse> {
163
    try {
164
      return await this.request("/auth/verify");
165
    } catch (error) {
166
      // Token is invalid or expired
167
      await this.removeToken();
168
      throw error;
169
    }
170
  }
171

172
  async logout(): Promise<void> {
173
    await this.removeToken();
174
  }
175

176
  // User-related
177
  async updateUser(user: UserUpdatePayload): Promise<{ user: User }> {
178
    return await this.request(`/users/${user.id}`, {
179
      method: "PUT",
180
      body: JSON.stringify({ user }),
181
    });
182
  }
183

184
  async avatar(formData: FormData): Promise<User> {
185
    return await this.request(`/users/me/avatar`, {
186
      method: "POST",
187
      body: formData,
188
      headers: {
189
        "Content-Type": "multipart/form-data",
190
      } as any,
191
    });
192
  }
193

194
  async removeAvatar(): Promise<void> {
195
    return await this.request(`/users/me/avatar`, {
196
      method: "DELETE",
197
    });
198
  }
199

200
  // Mood-related
201
  async createMoodEntry(
202
    mood: CreateMoodEntryPayload,
203
  ): Promise<MoodEntryResponse> {
204
    return await this.request(`/moods`, {
205
      method: "POST",
206
      body: JSON.stringify({ mood }),
207
    });
208
  }
209

210
  async getMoodEntries(params?: {
211
    from?: string;
212
    to?: string;
213
    page?: number;
214
    limit?: number;
215
  }): Promise<PaginatedResponse<MoodEntry>> {
216
    const query = new URLSearchParams();
217
    if (params?.from) query.set("from", params.from);
218
    if (params?.to) query.set("to", params.to);
219
    if (params?.page) query.set("page", String(params.page));
220
    if (params?.limit) query.set("limit", String(params.limit));
221

222
    const qs = query.toString();
223
    return this.request(`/moods${qs ? `?${qs}` : ""}`);
224
  }
225

226
  async getMoodEntry(id: string): Promise<MoodEntry> {
227
    return this.request(`/moods/${id}`);
228
  }
229

230
  async deleteMoodEntry(id: string): Promise<void> {
231
    return this.request(`/moods/${id}`, { method: "DELETE" });
232
  }
233

234
  // Sleep Record
235
  async createSleepRecord(
236
    sleepRecord: SleepRecordPayload,
237
  ): Promise<SleepRecordResponse> {
238
    return await this.request(`/sleep_records`, {
239
      method: "POST",
240
      body: JSON.stringify({ sleepRecord }),
241
    });
242
  }
243

244
  async getSleepRecords(params?: {
245
    from?: string;
246
    to?: string;
247
    page?: number;
248
    limit?: number;
249
  }): Promise<PaginatedResponse<SleepRecord>> {
250
    const query = new URLSearchParams();
251
    if (params?.from) query.set("from", params.from);
252
    if (params?.to) query.set("to", params.to);
253
    if (params?.page) query.set("page", String(params.page));
254
    if (params?.limit) query.set("limit", String(params.limit));
255

256
    const qs = query.toString();
257
    return this.request(`/sleep_records${qs ? `?${qs}` : ""}`);
258
  }
259

260
  async getSleepRecord(id: string): Promise<SleepRecord> {
261
    return this.request(`/sleep_records/${id}`);
262
  }
263

264
  async updateSleepRecord(
265
    id: string,
266
    sleepRecord: UpdateSleepRecordPayload,
267
  ): Promise<SleepRecordResponse> {
268
    return await this.request(`/sleep_records/${id}`, {
269
      method: "PUT",
270
      body: JSON.stringify({ sleepRecord }),
271
    });
272
  }
273

274
  async deleteSleepRecord(id: string): Promise<void> {
275
    return this.request(`/sleep_records/${id}`, { method: "DELETE" });
276
  }
277

278
  // Trigger
279
  async createTrigger(trigger: CreateTriggerPayload): Promise<TriggerResponse> {
280
    return await this.request(`/triggers`, {
281
      method: "POST",
282
      body: JSON.stringify({ trigger }),
283
    });
284
  }
285

286
  async getTriggers(params?: {
287
    from?: string;
288
    to?: string;
289
    page?: number;
290
    limit?: number;
291
  }): Promise<PaginatedResponse<Trigger>> {
292
    const query = new URLSearchParams();
293
    if (params?.from) query.set("from", params.from);
294
    if (params?.to) query.set("to", params.to);
295
    if (params?.page) query.set("page", String(params.page));
296
    if (params?.limit) query.set("limit", String(params.limit));
297

298
    const qs = query.toString();
299
    return this.request(`/triggers${qs ? `?${qs}` : ""}`);
300
  }
301

302
  async getTrigger(id: string): Promise<Trigger> {
303
    return this.request(`/triggers/${id}`);
304
  }
305

306
  async updateTrigger(
307
    id: string,
308
    trigger: UpdateTriggerPayload,
309
  ): Promise<TriggerResponse> {
310
    return await this.request(`/triggers/${id}`, {
311
      method: "PUT",
312
      body: JSON.stringify({ trigger }),
313
    });
314
  }
315

316
  async deleteTrigger(id: string): Promise<void> {
317
    return this.request(`/triggers/${id}`, { method: "DELETE" });
318
  }
319

320
  // Insights
321
  async getInsights(filters?: {
322
    type?: InsightType;
323
    period?: InsightPeriod;
324
    limit?: number;
325
  }): Promise<{ insights: Insight[] }> {
326
    const query = new URLSearchParams();
327
    if (filters?.type) query.set("type", filters.type);
328
    if (filters?.period) query.set("period", filters.period);
329
    if (filters?.limit) query.set("limit", String(filters.limit));
330

331
    const qs = query.toString();
332
    return this.request(`/insights${qs ? `?${qs}` : ""}`);
333
  }
334

335
  // Medicine Regimens
336
  async getMedicineRegimens(): Promise<{ regimens: MedicineRegimen[] }> {
337
    return this.request('/medicine-regimens');
338
  }
339

340
  async getMedicineRegimen(id: string): Promise<{ regimen: MedicineRegimen }> {
341
    return this.request(`/medicine-regimens/${id}`);
342
  }
343

344
  async createMedicineRegimen(
345
    regimen: CreateMedicineRegimenPayload
346
  ): Promise<MedicineRegimenResponse> {
347
    return this.request('/medicine-regimens', {
348
      method: 'POST',
349
      body: JSON.stringify({ regimen })
350
    });
351
  }
352

353
  async updateMedicineRegimen(
354
    id: string,
355
    regimen: UpdateMedicineRegimenPayload
356
  ): Promise<MedicineRegimenResponse> {
357
    return this.request(`/medicine-regimens/${id}`, {
358
      method: 'PATCH',
359
      body: JSON.stringify({ regimen })
360
    });
361
  }
362

363
  async deleteMedicineRegimen(id: string): Promise<void> {
364
    return this.request(`/medicine-regimens/${id}`, { method: 'DELETE' });
365
  }
366

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

377
  async getMedicineRegimensToday(): Promise<{ regimens: TodayMedicineEntry[] }> {
378
    return this.request('/medicine-regimens/today');
379
  }
380

381
  async patchUserMe(payload: UserPreferencesPayload): Promise<{ user: User }> {
382
    return this.request('/users/me', {
383
      method: 'PATCH',
384
      body: JSON.stringify({ user: payload }),
385
    });
386
  }
387

388
  // Care Actions
389
  async getCareActions(
390
    params?: {
391
      type?: CareActionType;
392
      from?: string;
393
      to?: string;
394
      page?: number;
395
      limit?: number
396
    }): Promise<PaginatedResponse<CareAction>> {
397
    const query = new URLSearchParams();
398
    if (params?.type) query.set('type', params.type);
399
    if (params?.from) query.set('from', params.from);
400
    if (params?.to) query.set('to', params.to);
401
    if (params?.page) query.set('page', String(params.page));
402
    if (params?.limit) query.set('limit', String(params.limit));
403

404
    const qs = query.toString();
405

406
    return this.request(`/care-actions${qs ? `?${qs}` : ''}`);
407
  }
408

409
  async getCareAction(id: string): Promise<CareAction> {
410
    return this.request(`/care-actions/${id}`);
411
  }
412

413
  async createCareAction(payload: CreateCareActionPayload): Promise<CareActionResponse> {
414
    return this.request('/care-actions', {
415
      method: 'POST', body: JSON.stringify({ careAction: payload })
416
    });
417
  }
418

419
  async patchCareAction(
420
    id: string,
421
    payload: PatchCareActionPayload,
422
  ): Promise<CareAction> {
423
    return this.request(`/care-actions/${id}`, {
424
      method: 'PATCH',
425
      body: JSON.stringify({ careAction: payload }),
426
    });
427
  }
428

429
  async deleteCareAction(id: string): Promise<void> {
430
    return this.request(`/care-actions/${id}`, { method: 'DELETE' });
431
  }
432

433
  // Trigger to Mood linking
434
  async linkMoodToTrigger(
435
    triggerId: string, payload: LinkMoodPayload
436
  ): Promise<{ link: TriggerMoodLink }> {
437
    return this.request(`/triggers/${triggerId}/link-mood`, {
438
      method: 'POST',
439
      body: JSON.stringify(payload)
440
    });
441
  }
442

443
  async unlinkMoodFromTrigger(triggerId: string, moodId: string): Promise<void> {
444
    return this.request(`/triggers/${triggerId}/link-mood/${moodId}`, { method: 'DELETE' });
445
  }
446
}
447

448
export const apiClient = new ApiClient();