eletrotupi / tcc/ frontend/lib/api/client.tsd662feb63b3670f5b5628fa35af62a7dfd06aa89
7.0 KB | 277 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
} from "@/lib/api/types";
22

23
const API_BASE_URL = process.env.EXPO_PUBLIC_API_BASE_URL;
24
const TOKEN_KEY = process.env.EXPO_PUBLIC_TOKEN_KEY;
25

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

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

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

53
  async getStoredAuthData(): Promise<{ token: string | null }> {
54
    const token = await this.getStoredToken();
55
    return { token };
56
  }
57

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

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

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

76
    if (!response.ok) {
77
      const error = await response
78
        .json()
79
        .catch(() => ({ error: "Network error" }));
80

81
      console.error("[API]: ", error, response.status);
82

83
      throw new Error(error.error || `HTTP ${response.status}`);
84
    }
85

86
    return response.json();
87
  }
88

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

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

103
    return response;
104
  }
105

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

277
export const apiClient = new ApiClient();