eletrotupi / tcc/ frontend/lib/api/client.ts26b3197a963f44b8573f14846c3b4ef68daa3839
8.3 KB | 323 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
} from "@/lib/api/types";
28

29
const API_BASE_URL = process.env.EXPO_PUBLIC_API_BASE_URL;
30
const TOKEN_KEY = process.env.EXPO_PUBLIC_TOKEN_KEY;
31

32
class ApiClient {
33
  private async getStoredToken(): Promise<string | null> {
34
    try {
35
      return await SecureStore.getItemAsync(TOKEN_KEY!);
36
    } catch (error) {
37
      console.error("Failed to get stored token:", error);
38
      return null;
39
    }
40
  }
41

42
  private async storeToken(token: string): Promise<void> {
43
    try {
44
      await SecureStore.setItemAsync(TOKEN_KEY!, token);
45
    } catch (error) {
46
      console.error("Failed to store token:", error);
47
      throw error;
48
    }
49
  }
50

51
  private async removeToken(): Promise<void> {
52
    try {
53
      await SecureStore.deleteItemAsync(TOKEN_KEY!);
54
    } catch (error) {
55
      console.error("Failed to remove token:", error);
56
    }
57
  }
58

59
  async getStoredAuthData(): Promise<{ token: string | null }> {
60
    const token = await this.getStoredToken();
61
    return { token };
62
  }
63

64
  private async request<T>(
65
    endpoint: string,
66
    options: RequestInit = {},
67
  ): Promise<T> {
68
    const token = await this.getStoredToken();
69
    const isFormData = options.body instanceof FormData;
70

71
    const config: RequestInit = {
72
      ...options,
73
      headers: {
74
        ...(token && { Authorization: `Bearer ${token}` }),
75
        ...(!isFormData && { "Content-Type": "application/json" }),
76
        ...options.headers,
77
      },
78
    };
79

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

82
    if (!response.ok) {
83
      const error = await response
84
        .json()
85
        .catch(() => ({ error: "Network error" }));
86

87
      console.error("[API]: ", error, response.status);
88

89
      throw new Error(error.error || `HTTP ${response.status}`);
90
    }
91

92
    return response.json();
93
  }
94

95
  // Auth
96
  async login(email: string, password: string): Promise<AuthResponse> {
97
    const response = await this.request<AuthResponse>("/auth/login", {
98
      method: "POST",
99
      body: JSON.stringify({ email, password }),
100
    });
101

102
    if (response.token) {
103
      await this.storeToken(response.token);
104
    }
105

106
    return response;
107
  }
108

109
  async activate(code: number): Promise<ActivateResponse> {
110
    return this.request("/auth/activate", {
111
      method: "POST",
112
      body: JSON.stringify({ code }),
113
    });
114
  }
115

116
  async requestActivateCode(): Promise<void> {
117
    return this.request("/auth/resend_code", {
118
      method: "POST",
119
    });
120
  }
121

122
  async signup(user: SignUpPayload): Promise<AuthResponse> {
123
    const response = await this.request<AuthResponse>("/users", {
124
      method: "POST",
125
      body: JSON.stringify({ user }),
126
    });
127

128
    await this.storeToken(response.token);
129
    return response;
130
  }
131

132
  async forgotPassword(email: string): Promise<ResetPasswordRequestResponse> {
133
    return this.request("/auth/forgot-password", {
134
      method: "POST",
135
      body: JSON.stringify({ email }),
136
    });
137
  }
138

139
  async resetPassword(
140
    token: string,
141
    password: string,
142
  ): Promise<ResetPasswordResponse> {
143
    return this.request("/auth/reset-password", {
144
      method: "POST",
145
      body: JSON.stringify({ token, newPassword: password }),
146
    });
147
  }
148

149
  async verifyToken(): Promise<VerifyTokenResponse> {
150
    try {
151
      return await this.request("/auth/verify");
152
    } catch (error) {
153
      // Token is invalid or expired
154
      await this.removeToken();
155
      throw error;
156
    }
157
  }
158

159
  async logout(): Promise<void> {
160
    await this.removeToken();
161
  }
162

163
  // User-related
164
  async updateUser(user: UserUpdatePayload): Promise<{ user: User }> {
165
    return await this.request(`/users/${user.id}`, {
166
      method: "PUT",
167
      body: JSON.stringify({ user }),
168
    });
169
  }
170

171
  async avatar(formData: FormData): Promise<User> {
172
    return await this.request(`/users/me/avatar`, {
173
      method: "POST",
174
      body: formData,
175
      headers: {
176
        "Content-Type": "multipart/form-data",
177
      } as any,
178
    });
179
  }
180

181
  async removeAvatar(): Promise<void> {
182
    return await this.request(`/users/me/avatar`, {
183
      method: "DELETE",
184
    });
185
  }
186

187
  // Mood-related
188
  async createMoodEntry(
189
    mood: CreateMoodEntryPayload,
190
  ): Promise<MoodEntryResponse> {
191
    return await this.request(`/moods`, {
192
      method: "POST",
193
      body: JSON.stringify({ mood }),
194
    });
195
  }
196

197
  async getMoodEntries(params?: {
198
    from?: string;
199
    to?: string;
200
    page?: number;
201
    limit?: number;
202
  }): Promise<PaginatedResponse<MoodEntry>> {
203
    const query = new URLSearchParams();
204
    if (params?.from) query.set("from", params.from);
205
    if (params?.to) query.set("to", params.to);
206
    if (params?.page) query.set("page", String(params.page));
207
    if (params?.limit) query.set("limit", String(params.limit));
208

209
    const qs = query.toString();
210
    return this.request(`/moods${qs ? `?${qs}` : ""}`);
211
  }
212

213
  async getMoodEntry(id: string): Promise<MoodEntry> {
214
    return this.request(`/moods/${id}`);
215
  }
216

217
  async deleteMoodEntry(id: string): Promise<void> {
218
    return this.request(`/moods/${id}`, { method: "DELETE" });
219
  }
220

221
  // Sleep Record
222
  async createSleepRecord(
223
    sleepRecord: SleepRecordPayload,
224
  ): Promise<SleepRecordResponse> {
225
    return await this.request(`/sleep_records`, {
226
      method: "POST",
227
      body: JSON.stringify({ sleepRecord }),
228
    });
229
  }
230

231
  async getSleepRecords(params?: {
232
    from?: string;
233
    to?: string;
234
    page?: number;
235
    limit?: number;
236
  }): Promise<PaginatedResponse<SleepRecord>> {
237
    const query = new URLSearchParams();
238
    if (params?.from) query.set("from", params.from);
239
    if (params?.to) query.set("to", params.to);
240
    if (params?.page) query.set("page", String(params.page));
241
    if (params?.limit) query.set("limit", String(params.limit));
242

243
    const qs = query.toString();
244
    return this.request(`/sleep_records${qs ? `?${qs}` : ""}`);
245
  }
246

247
  async getSleepRecord(id: string): Promise<SleepRecord> {
248
    return this.request(`/sleep_records/${id}`);
249
  }
250

251
  async updateSleepRecord(
252
    id: string,
253
    sleepRecord: UpdateSleepRecordPayload,
254
  ): Promise<SleepRecordResponse> {
255
    return await this.request(`/sleep_records/${id}`, {
256
      method: "PUT",
257
      body: JSON.stringify({ sleepRecord }),
258
    });
259
  }
260

261
  async deleteSleepRecord(id: string): Promise<void> {
262
    return this.request(`/sleep_records/${id}`, { method: "DELETE" });
263
  }
264

265
  // Trigger
266
  async createTrigger(trigger: CreateTriggerPayload): Promise<TriggerResponse> {
267
    return await this.request(`/triggers`, {
268
      method: "POST",
269
      body: JSON.stringify({ trigger }),
270
    });
271
  }
272

273
  async getTriggers(params?: {
274
    from?: string;
275
    to?: string;
276
    page?: number;
277
    limit?: number;
278
  }): Promise<PaginatedResponse<Trigger>> {
279
    const query = new URLSearchParams();
280
    if (params?.from) query.set("from", params.from);
281
    if (params?.to) query.set("to", params.to);
282
    if (params?.page) query.set("page", String(params.page));
283
    if (params?.limit) query.set("limit", String(params.limit));
284

285
    const qs = query.toString();
286
    return this.request(`/triggers${qs ? `?${qs}` : ""}`);
287
  }
288

289
  async getTrigger(id: string): Promise<Trigger> {
290
    return this.request(`/triggers/${id}`);
291
  }
292

293
  async updateTrigger(
294
    id: string,
295
    trigger: UpdateTriggerPayload,
296
  ): Promise<TriggerResponse> {
297
    return await this.request(`/triggers/${id}`, {
298
      method: "PUT",
299
      body: JSON.stringify({ trigger }),
300
    });
301
  }
302

303
  async deleteTrigger(id: string): Promise<void> {
304
    return this.request(`/triggers/${id}`, { method: "DELETE" });
305
  }
306

307
  // Insights
308
  async getInsights(filters?: {
309
    type?: InsightType;
310
    period?: InsightPeriod;
311
    limit?: number;
312
  }): Promise<{ insights: Insight[] }> {
313
    const query = new URLSearchParams();
314
    if (filters?.type) query.set("type", filters.type);
315
    if (filters?.period) query.set("period", filters.period);
316
    if (filters?.limit) query.set("limit", String(filters.limit));
317

318
    const qs = query.toString();
319
    return this.request(`/insights${qs ? `?${qs}` : ""}`);
320
  }
321
}
322

323
export const apiClient = new ApiClient();