eletrotupi / tcc/ frontend/lib/api/client.ts2984ae1cefef3e0e22e3367867ce09be0d69026e
10.8 KB | 410 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
  CareAction,
32
  CareActionType,
33
  CareActionResponse,
34
  CreateCareActionPayload,
35
  TriggerMoodLink,
36
  LinkMoodPayload,
37
} from "@/lib/api/types";
38

39
const API_BASE_URL = process.env.EXPO_PUBLIC_API_BASE_URL;
40
const TOKEN_KEY = process.env.EXPO_PUBLIC_TOKEN_KEY;
41

42
class ApiClient {
43
  private async getStoredToken(): Promise<string | null> {
44
    try {
45
      return await SecureStore.getItemAsync(TOKEN_KEY!);
46
    } catch (error) {
47
      console.error("Failed to get stored token:", error);
48
      return null;
49
    }
50
  }
51

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

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

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

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

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

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

92
    if (!response.ok) {
93
      const error = await response
94
        .json()
95
        .catch(() => ({ error: "Network error" }));
96

97
      console.error("[API]: ", error, response.status);
98

99
      throw new Error(error.error || `HTTP ${response.status}`);
100
    }
101

102
    return response.json();
103
  }
104

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

112
    if (response.token) {
113
      await this.storeToken(response.token);
114
    }
115

116
    return response;
117
  }
118

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

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

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

138
    await this.storeToken(response.token);
139
    return response;
140
  }
141

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

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

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

169
  async logout(): Promise<void> {
170
    await this.removeToken();
171
  }
172

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

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

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

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

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

219
    const qs = query.toString();
220
    return this.request(`/moods${qs ? `?${qs}` : ""}`);
221
  }
222

223
  async getMoodEntry(id: string): Promise<MoodEntry> {
224
    return this.request(`/moods/${id}`);
225
  }
226

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

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

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

253
    const qs = query.toString();
254
    return this.request(`/sleep_records${qs ? `?${qs}` : ""}`);
255
  }
256

257
  async getSleepRecord(id: string): Promise<SleepRecord> {
258
    return this.request(`/sleep_records/${id}`);
259
  }
260

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

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

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

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

295
    const qs = query.toString();
296
    return this.request(`/triggers${qs ? `?${qs}` : ""}`);
297
  }
298

299
  async getTrigger(id: string): Promise<Trigger> {
300
    return this.request(`/triggers/${id}`);
301
  }
302

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

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

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

328
    const qs = query.toString();
329
    return this.request(`/insights${qs ? `?${qs}` : ""}`);
330
  }
331

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

337
  async createMedicineRegimen(
338
    regimen: CreateMedicineRegimenPayload
339
  ): Promise<MedicineRegimenResponse> {
340
    return this.request('/medicine-regimens', {
341
      method: 'POST',
342
      body: JSON.stringify({ regimen })
343
    });
344
  }
345

346
  async updateMedicineRegimen(
347
    id: string,
348
    regimen: UpdateMedicineRegimenPayload
349
  ): Promise<MedicineRegimenResponse> {
350
    return this.request(`/medicine-regimens/${id}`, {
351
      method: 'PATCH',
352
      body: JSON.stringify({ regimen })
353
    });
354
  }
355

356
  async deleteMedicineRegimen(id: string): Promise<void> {
357
    return this.request(`/medicine-regimens/${id}`, { method: 'DELETE' });
358
  }
359

360
  // Care Actions
361
  async getCareActions(
362
    params?: {
363
      type?: CareActionType;
364
      from?: string;
365
      to?: string;
366
      page?: number;
367
      limit?: number
368
    }): Promise<PaginatedResponse<CareAction>> {
369
    const query = new URLSearchParams();
370
    if (params?.type) query.set('type', params.type);
371
    if (params?.from) query.set('from', params.from);
372
    if (params?.to) query.set('to', params.to);
373
    if (params?.page) query.set('page', String(params.page));
374
    if (params?.limit) query.set('limit', String(params.limit));
375

376
    const qs = query.toString();
377

378
    return this.request(`/care-actions${qs ? `?${qs}` : ''}`);
379
  }
380

381
  async getCareAction(id: string): Promise<CareAction> {
382
    return this.request(`/care-actions/${id}`);
383
  }
384

385
  async createCareAction(payload: CreateCareActionPayload): Promise<CareActionResponse> {
386
    return this.request('/care-actions', {
387
      method: 'POST', body: JSON.stringify({ careAction: payload })
388
    });
389
  }
390

391
  async deleteCareAction(id: string): Promise<void> {
392
    return this.request(`/care-actions/${id}`, { method: 'DELETE' });
393
  }
394

395
  // Trigger to Mood linking
396
  async linkMoodToTrigger(
397
    triggerId: string, payload: LinkMoodPayload
398
  ): Promise<{ link: TriggerMoodLink }> {
399
    return this.request(`/triggers/${triggerId}/link-mood`, {
400
      method: 'POST',
401
      body: JSON.stringify(payload)
402
    });
403
  }
404

405
  async unlinkMoodFromTrigger(triggerId: string, moodId: string): Promise<void> {
406
    return this.request(`/triggers/${triggerId}/link-mood/${moodId}`, { method: 'DELETE' });
407
  }
408
}
409

410
export const apiClient = new ApiClient();