eletrotupi / tcc/ frontend/lib/api/client.tsdf8bcbc39421fa0025e20d31bf3b1fd58cbda641
7.5 KB | 293 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
  MoodEntry,
13
  SleepRecord,
14
  SleepRecordResponse,
15
  MoodEntryResponse,
16
  Trigger,
17
  TriggerResponse,
18
  CreateTriggerPayload,
19
  ActivateResponse,
20
  PaginatedResponse,
21
  InsightType,
22
  InsightPeriod,
23
  InsightMetadata,
24
  Insight,
25
} from "@/lib/api/types";
26

27
const API_BASE_URL = process.env.EXPO_PUBLIC_API_BASE_URL;
28
const TOKEN_KEY = process.env.EXPO_PUBLIC_TOKEN_KEY;
29

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

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

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

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

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

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

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

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

85
      console.error("[API]: ", error, response.status);
86

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

90
    return response.json();
91
  }
92

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

100
    if (response.token) {
101
      await this.storeToken(response.token);
102
    }
103

104
    return response;
105
  }
106

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

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

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

126
    await this.storeToken(response.token);
127
    return response;
128
  }
129

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

245
  // Trigger
246
  async createTrigger(trigger: CreateTriggerPayload): Promise<TriggerResponse> {
247
    return await this.request(`/triggers`, {
248
      method: "POST",
249
      body: JSON.stringify({ trigger }),
250
    });
251
  }
252

253
  async getTriggers(params?: {
254
    from?: string;
255
    to?: string;
256
    page?: number;
257
    limit?: number;
258
  }): Promise<PaginatedResponse<Trigger>> {
259
    const query = new URLSearchParams();
260
    if (params?.from) query.set("from", params.from);
261
    if (params?.to) query.set("to", params.to);
262
    if (params?.page) query.set("page", String(params.page));
263
    if (params?.limit) query.set("limit", String(params.limit));
264

265
    const qs = query.toString();
266
    return this.request(`/triggers${qs ? `?${qs}` : ""}`);
267
  }
268

269
  async getTrigger(id: string): Promise<Trigger> {
270
    return this.request(`/triggers/${id}`);
271
  }
272

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

277
  // Insights
278
  async getInsights(filters?: {
279
    type?: InsightType;
280
    period?: InsightPeriod;
281
    limit?: number;
282
  }): Promise<{ insights: Insight[] }> {
283
    const query = new URLSearchParams();
284
    if (filters?.type) query.set("type", filters.type);
285
    if (filters?.period) query.set("period", filters.period);
286
    if (filters?.limit) query.set("limit", String(filters.limit));
287

288
    const qs = query.toString();
289
    return this.request(`/insights${qs ? `?${qs}` : ""}`);
290
  }
291
}
292

293
export const apiClient = new ApiClient();