Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
197 changes: 196 additions & 1 deletion client/src/lib/api.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { EnvironmentManager } from "./environment";
import type { isProcessed, ProcessedEvents, UserSettings } from "./types";
import type { isProcessed, ProcessedEvents, UniversityCalendarEvent, UniversityEventCategoryWithCount, UserSettings } from "./types";

export class API {
private static async getBaseUrl(): Promise<string> {
Expand Down Expand Up @@ -111,4 +111,199 @@ export class API {
return response.json();
}

public static async getUniversityEventCategories(): Promise<{ categories: UniversityEventCategoryWithCount[] }> {
const baseUrl = await this.getBaseUrl();
const token = await this.getJwtToken();
const response = await fetch(`${baseUrl}/university_calendar_events/categories`, {
method: 'GET',
headers: {
'Authorization': `Bearer ${token}`
}
});
return response.json();
}

public static async getUniversityEvents(params?: { category?: string; categories?: string; start_date?: string; end_date?: string; term_id?: string; page?: number; per_page?: number }): Promise<{ events: UniversityCalendarEvent[]; meta: { current_page: number; total_pages: number; total_count: number; per_page: number } }> {
const baseUrl = await this.getBaseUrl();
const token = await this.getJwtToken();
const searchParams = new URLSearchParams();
if (params?.category) searchParams.append('category', params.category);
if (params?.categories) searchParams.append('categories', params.categories);
if (params?.start_date) searchParams.append('start_date', params.start_date);
if (params?.end_date) searchParams.append('end_date', params.end_date);
if (params?.term_id) searchParams.append('term_id', params.term_id);
if (params?.page) searchParams.append('page', params.page.toString());
if (params?.per_page) searchParams.append('per_page', params.per_page.toString());

const url = `${baseUrl}/university_calendar_events${searchParams.toString() ? '?' + searchParams.toString() : ''}`;
const response = await fetch(url, {
method: 'GET',
headers: {
'Authorization': `Bearer ${token}`
}
});
return response.json();
}

public static async getHolidays(params?: { term_id?: string; start_date?: string; end_date?: string }): Promise<{ holidays: UniversityCalendarEvent[] }> {
const baseUrl = await this.getBaseUrl();
const token = await this.getJwtToken();
const searchParams = new URLSearchParams();
if (params?.term_id) searchParams.append('term_id', params.term_id);
if (params?.start_date) searchParams.append('start_date', params.start_date);
if (params?.end_date) searchParams.append('end_date', params.end_date);

const url = `${baseUrl}/university_calendar_events/holidays${searchParams.toString() ? '?' + searchParams.toString() : ''}`;
const response = await fetch(url, {
method: 'GET',
headers: {
'Authorization': `Bearer ${token}`
}
});
return response.json();
}

// Course processing endpoints
public static async processCourses(courses: any[]): Promise<{ user_pub: string; ics_url: string }> {
const baseUrl = await this.getBaseUrl();
const token = await this.getJwtToken();
const response = await fetch(`${baseUrl}/process_courses`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(courses)
});
return response.json();
}
Comment on lines +167 to +179
Copy link

Copilot AI Dec 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The processCourses method doesn't check if the response is successful before attempting to parse JSON. If the fetch fails with a non-2xx status code, calling .json() could throw an error or return unexpected data. Consider adding response status validation similar to the pattern used in the old inline fetch code.

Copilot uses AI. Check for mistakes.

public static async reprocessCourses(courses: any[]): Promise<{
ics_url: string;
removed_enrollments: number;
removed_courses: Array<{ crn: number; title: string; course_number: number }>;
processed_courses: any[];
}> {
const baseUrl = await this.getBaseUrl();
const token = await this.getJwtToken();
const response = await fetch(`${baseUrl}/courses/reprocess`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ courses })
});
Copy link

Copilot AI Dec 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reprocessCourses method doesn't validate the response status before parsing JSON. If the API returns an error status, the method will still attempt to parse the response as JSON, which may not contain the expected structure with ics_url, removed_enrollments, etc.

Suggested change
});
});
if (!response.ok) {
const errorText = await response.text().catch(() => '');
throw new Error(
`Failed to reprocess courses: ${response.status} ${response.statusText}` +
(errorText ? ` - ${errorText}` : '')
);
}

Copilot uses AI. Check for mistakes.
return response.json();
}

// Event preferences endpoints
public static async getMeetingTimePreference(meetingTimeId: number | string): Promise<any> {
const baseUrl = await this.getBaseUrl();
const token = await this.getJwtToken();
const response = await fetch(`${baseUrl}/meeting_times/${meetingTimeId}/preference`, {
method: 'GET',
headers: {
'Authorization': `Bearer ${token}`
}
});
Copy link

Copilot AI Dec 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The getMeetingTimePreference method doesn't check the response status. If the request fails (e.g., 404 for non-existent meeting time), calling .json() on the error response could produce unexpected results or throw an error that isn't properly handled.

Suggested change
});
});
if (!response.ok) {
let errorBody: string | undefined;
try {
errorBody = await response.text();
} catch {
// Ignore body parsing errors; we'll fall back to status text.
}
const message = `Failed to fetch meeting time preference (status ${response.status}): ` +
(errorBody && errorBody.trim().length > 0 ? errorBody : response.statusText);
throw new Error(message);
}

Copilot uses AI. Check for mistakes.
return response.json();
}

public static async updateMeetingTimePreference(meetingTimeId: number | string, preferences: any): Promise<any> {
const baseUrl = await this.getBaseUrl();
const token = await this.getJwtToken();
const response = await fetch(`${baseUrl}/meeting_times/${meetingTimeId}/preference`, {
method: 'PUT',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(preferences)
});
Copy link

Copilot AI Dec 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The updateMeetingTimePreference method doesn't validate the response status. If the API returns an error (e.g., 401, 403, or 500), the method will still attempt to parse JSON and return it, which could lead to unexpected behavior in the calling code.

Suggested change
});
});
if (!response.ok) {
throw new Error(`Failed to update meeting time preference: ${response.status} ${response.statusText}`);
}

Copilot uses AI. Check for mistakes.
return response.json();
}

public static async deleteMeetingTimePreference(meetingTimeId: number | string): Promise<any> {
const baseUrl = await this.getBaseUrl();
const token = await this.getJwtToken();
const response = await fetch(`${baseUrl}/meeting_times/${meetingTimeId}/preference`, {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${token}`
}
});
return response.json();
}

// Global calendar preferences
public static async getGlobalCalendarPreference(): Promise<any> {
const baseUrl = await this.getBaseUrl();
const token = await this.getJwtToken();
const response = await fetch(`${baseUrl}/calendar_preferences/global`, {
method: 'GET',
headers: {
'Authorization': `Bearer ${token}`
}
});
return response.json();
}

public static async setGlobalCalendarPreference(preferences: {
reminder_settings?: any[];
title_template?: string;
description_template?: string;
color_id?: string;
}): Promise<any> {
const baseUrl = await this.getBaseUrl();
const token = await this.getJwtToken();
const response = await fetch(`${baseUrl}/calendar_preferences/global`, {
method: 'PUT',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ calendar_preference: preferences })
});
return response.json();
}

// Connected Google accounts
public static async getConnectedAccounts(): Promise<{ oauth_credentials: Array<{id: number, email: string, provider: string}> }> {
const baseUrl = await this.getBaseUrl();
const token = await this.getJwtToken();
const response = await fetch(`${baseUrl}/user/oauth_credentials`, {
method: 'GET',
headers: {
'Authorization': `Bearer ${token}`
}
});
return response.json();
}

public static async requestOAuthForEmail(email: string): Promise<{ oauth_url?: string, calendar_id?: string, error?: string }> {
const baseUrl = await this.getBaseUrl();
const token = await this.getJwtToken();
const response = await fetch(`${baseUrl}/user/gcal`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ email })
});
return response.json();
}

public static async disconnectAccount(credentialId: number): Promise<void> {
const baseUrl = await this.getBaseUrl();
const token = await this.getJwtToken();
await fetch(`${baseUrl}/user/oauth_credentials/${credentialId}`, {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${token}`
}
});
Comment on lines +301 to +306
Copy link

Copilot AI Dec 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The disconnectAccount API method doesn't check the response status or handle potential errors from the fetch call. If the DELETE request fails (network error, 404, 500, etc.), the promise will silently resolve without throwing, and the calling code won't know the disconnection failed.

Suggested change
await fetch(`${baseUrl}/user/oauth_credentials/${credentialId}`, {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${token}`
}
});
const response = await fetch(`${baseUrl}/user/oauth_credentials/${credentialId}`, {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${token}`
}
});
if (!response.ok) {
throw new Error(`Failed to disconnect account (status ${response.status} ${response.statusText})`);
}

Copilot uses AI. Check for mistakes.
}

}
Loading