eletrotupi / tcc/ frontend/lib/api/client.ts23893a7b41f6b4b4ee7958a5d186afebd3f12827
11.1 KB | 421 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
  PatchCareActionPayload,
36
  TriggerMoodLink,
37
  LinkMoodPayload,
38
} from "@/lib/api/types";
39

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

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

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

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

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

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

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

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

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

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

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

103
    return response.json();
104
  }
105

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

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

117
    return response;
118
  }
119

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

377
    const qs = query.toString();
378

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

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

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

392
  async patchCareAction(
393
    id: string,
394
    payload: PatchCareActionPayload,
395
  ): Promise<CareAction> {
396
    return this.request(`/care-actions/${id}`, {
397
      method: 'PATCH',
398
      body: JSON.stringify({ careAction: payload }),
399
    });
400
  }
401

402
  async deleteCareAction(id: string): Promise<void> {
403
    return this.request(`/care-actions/${id}`, { method: 'DELETE' });
404
  }
405

406
  // Trigger to Mood linking
407
  async linkMoodToTrigger(
408
    triggerId: string, payload: LinkMoodPayload
409
  ): Promise<{ link: TriggerMoodLink }> {
410
    return this.request(`/triggers/${triggerId}/link-mood`, {
411
      method: 'POST',
412
      body: JSON.stringify(payload)
413
    });
414
  }
415

416
  async unlinkMoodFromTrigger(triggerId: string, moodId: string): Promise<void> {
417
    return this.request(`/triggers/${triggerId}/link-mood/${moodId}`, { method: 'DELETE' });
418
  }
419
}
420

421
export const apiClient = new ApiClient();