eletrotupi / tcc / frontend/lib/api/client.ts master
7.5 KB Raw
import * as SecureStore from "expo-secure-store";
import {
  User,
  AuthResponse,
  VerifyTokenResponse,
  SignUpPayload,
  ResetPasswordRequestResponse,
  ResetPasswordResponse,
  UserUpdatePayload,
  CreateMoodEntryPayload,
  SleepRecordPayload,
  MoodEntry,
  SleepRecord,
  SleepRecordResponse,
  MoodEntryResponse,
  Trigger,
  TriggerResponse,
  CreateTriggerPayload,
  ActivateResponse,
  PaginatedResponse,
  InsightType,
  InsightPeriod,
  InsightMetadata,
  Insight,
} from "@/lib/api/types";

const API_BASE_URL = process.env.EXPO_PUBLIC_API_BASE_URL;
const TOKEN_KEY = process.env.EXPO_PUBLIC_TOKEN_KEY;

class ApiClient {
  private async getStoredToken(): Promise<string | null> {
    try {
      return await SecureStore.getItemAsync(TOKEN_KEY!);
    } catch (error) {
      console.error("Failed to get stored token:", error);
      return null;
    }
  }

  private async storeToken(token: string): Promise<void> {
    try {
      await SecureStore.setItemAsync(TOKEN_KEY!, token);
    } catch (error) {
      console.error("Failed to store token:", error);
      throw error;
    }
  }

  private async removeToken(): Promise<void> {
    try {
      await SecureStore.deleteItemAsync(TOKEN_KEY!);
    } catch (error) {
      console.error("Failed to remove token:", error);
    }
  }

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

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

    const config: RequestInit = {
      ...options,
      headers: {
        ...(token && { Authorization: `Bearer ${token}` }),
        ...(!isFormData && { "Content-Type": "application/json" }),
        ...options.headers,
      },
    };

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

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

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

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

    return response.json();
  }

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

    if (response.token) {
      await this.storeToken(response.token);
    }

    return response;
  }

  async activate(code: number): Promise<ActivateResponse> {
    return this.request("/auth/activate", {
      method: "POST",
      body: JSON.stringify({ code }),
    });
  }

  async requestActivateCode(): Promise<void> {
    return this.request("/auth/resend_code", {
      method: "POST",
    });
  }

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

    await this.storeToken(response.token);
    return response;
  }

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

  async resetPassword(
    token: string,
    password: string,
  ): Promise<ResetPasswordResponse> {
    return this.request("/auth/reset-password", {
      method: "POST",
      body: JSON.stringify({ token, newPassword: password }),
    });
  }

  async verifyToken(): Promise<VerifyTokenResponse> {
    try {
      return await this.request("/auth/verify");
    } catch (error) {
      // Token is invalid or expired
      await this.removeToken();
      throw error;
    }
  }

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

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

  async avatar(formData: FormData): Promise<User> {
    return await this.request(`/users/me/avatar`, {
      method: "POST",
      body: formData,
      headers: {
        "Content-Type": "multipart/form-data",
      } as any,
    });
  }

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

  // Mood-related
  async createMoodEntry(
    mood: CreateMoodEntryPayload,
  ): Promise<MoodEntryResponse> {
    return await this.request(`/moods`, {
      method: "POST",
      body: JSON.stringify({ mood }),
    });
  }

  async getMoodEntries(params?: {
    from?: string;
    to?: string;
    page?: number;
    limit?: number;
  }): Promise<PaginatedResponse<MoodEntry>> {
    const query = new URLSearchParams();
    if (params?.from) query.set("from", params.from);
    if (params?.to) query.set("to", params.to);
    if (params?.page) query.set("page", String(params.page));
    if (params?.limit) query.set("limit", String(params.limit));

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

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

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

  // Sleep Record
  async createSleepRecord(
    sleepRecord: SleepRecordPayload,
  ): Promise<SleepRecordResponse> {
    return await this.request(`/sleep_records`, {
      method: "POST",
      body: JSON.stringify({ sleepRecord }),
    });
  }

  async getSleepRecords(params?: {
    from?: string;
    to?: string;
    page?: number;
    limit?: number;
  }): Promise<PaginatedResponse<SleepRecord>> {
    const query = new URLSearchParams();
    if (params?.from) query.set("from", params.from);
    if (params?.to) query.set("to", params.to);
    if (params?.page) query.set("page", String(params.page));
    if (params?.limit) query.set("limit", String(params.limit));

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

  // Trigger
  async createTrigger(trigger: CreateTriggerPayload): Promise<TriggerResponse> {
    return await this.request(`/triggers`, {
      method: "POST",
      body: JSON.stringify({ trigger }),
    });
  }

  async getTriggers(params?: {
    from?: string;
    to?: string;
    page?: number;
    limit?: number;
  }): Promise<PaginatedResponse<Trigger>> {
    const query = new URLSearchParams();
    if (params?.from) query.set("from", params.from);
    if (params?.to) query.set("to", params.to);
    if (params?.page) query.set("page", String(params.page));
    if (params?.limit) query.set("limit", String(params.limit));

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

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

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

  // Insights
  async getInsights(filters?: {
    type?: InsightType;
    period?: InsightPeriod;
    limit?: number;
  }): Promise<{ insights: Insight[] }> {
    const query = new URLSearchParams();
    if (filters?.type) query.set("type", filters.type);
    if (filters?.period) query.set("period", filters.period);
    if (filters?.limit) query.set("limit", String(filters.limit));

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

export const apiClient = new ApiClient();