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
24 changes: 24 additions & 0 deletions src/api/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { apiRequest } from './client';
import type { AuthUser } from '../types/User.ts';

export const authApi = {
// Get current user
getMe: () => apiRequest<AuthUser>('/api/v1/auth/me'),

// Initiate Auth
login: (provider: string) => {
window.location.href = `/api/v1/auth/${provider}`;
},

// Logout
logout: () =>
apiRequest<void>('/api/v1/auth/logout', {
method: 'POST',
}),

// Refresh token
refresh: () =>
apiRequest<void>('/api/v1/auth/refresh', {
method: 'POST',
}),
};
38 changes: 38 additions & 0 deletions src/api/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';

// All fields are optional since there are defaults
interface RequestOptions<TBody> {
method?: HttpMethod;
body?: TBody;
headers?: HeadersInit;
credentials?: RequestCredentials;
}

// TResponse: expected response from the server
// TBody: expected shape of the body (default: unknown)
// Params: endpoint and RequestOptions
export async function apiRequest<TResponse, TBody = unknown>(
endpoint: string,
options: RequestOptions<TBody> = {}
): Promise<TResponse> {
const { method = 'GET', body, headers = {}, credentials = 'include' } = options; // defaults

const response = await fetch(endpoint, {
method,
headers: {
'Content-Type': 'application/json',
...headers,
},
credentials,
body: body ? JSON.stringify(body) : undefined,
});

// Error handling
if (!response.ok) {
const errorText = await response.text();
throw new Error(`API Error: ${response.status} - ${errorText}`);
}

// Success
return response.json();
}
71 changes: 71 additions & 0 deletions src/api/events.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { apiRequest } from './client';
import type { CreateEvent, UpdateEvent, RegisterEvent, Event, EventUser } from '../types/Event.ts';
import type { QueryParams } from '../types/QueryParams.ts';

export const eventsApi = {
// Get all events
getAll: (params?: QueryParams) => {
const query = new URLSearchParams();
if (params?.limit !== undefined) {
query.append('limit', params.limit.toString());
}
if (params?.offset !== undefined) {
query.append('offset', params.offset.toString());
}
const queryString = query.toString();
return apiRequest<Event[]>(queryString ? `/events?${queryString}` : '/events');
},

// Create an event
create: (data: CreateEvent) =>
apiRequest<Event>(`/events`, {
method: 'POST',
body: data,
}),

// List events by organization
getFromOrganization: (oid: string, params?: QueryParams) => {
const query = new URLSearchParams();
if (params?.limit !== undefined) {
query.append('limit', params.limit.toString());
}
if (params?.offset !== undefined) {
query.append('offset', params.offset.toString());
}
const queryString = query.toString();
return apiRequest<Event[]>(queryString ? `/events/org/${oid}?${queryString}` : `/events/org/${oid}`);
},

// Get event
getById: (eid: string) => apiRequest<Event>(`/events/${eid}`),

// Update event
update: (eid: string, data: UpdateEvent) =>
apiRequest<Event, UpdateEvent>(`/events/${eid}`, {
method: 'PUT',
body: data,
}),

// Delete event
delete: (eid: string) =>
apiRequest<void>(`/events/${eid}`, {
method: 'DELETE',
}),

// Register for event
register: (eid: string, data: RegisterEvent) =>
apiRequest<void, RegisterEvent>(`/events/${eid}/register`, {
method: 'POST',
body: data,
}),

// Unregister from event
unregister: (eid: string, data: RegisterEvent) =>
apiRequest<void, RegisterEvent>(`/events/${eid}/register`, {
method: 'DELETE',
body: data,
}),

// Get users registered for an event
getUsers: (eid: string) => apiRequest<EventUser>(`/events/${eid}/registrations`),
};
79 changes: 79 additions & 0 deletions src/api/organizations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { apiRequest } from './client';
import type {
Organization,
CreateOrganization,
UpdateOrganization,
OrganizationUser,
ManageOrganizationUser,
} from '../types/Organization.ts';
import type { Event } from '../types/Event.ts';
import type { QueryParams } from '../types/QueryParams.ts';

export const organizationsApi = {
// Get all organizations
getAll: (params?: QueryParams) => {
const query = new URLSearchParams();
if (params?.limit !== undefined) {
query.append('limit', params.limit.toString());
}
if (params?.offset !== undefined) {
query.append('offset', params.offset.toString());
}
const queryString = query.toString();
return apiRequest<Organization[]>(queryString ? `/organizations?${queryString}` : '/organizations');
},

// Create organization
create: (data: CreateOrganization) =>
apiRequest<Organization, CreateOrganization>(`/events`, {
method: 'POST',
body: data,
}),

// Get organization
getById: (oid: string) => apiRequest<Organization>(`/organizations/${oid}`),

// Update organization
update: (oid: string, data: UpdateOrganization) =>
apiRequest<Organization, UpdateOrganization>(`/organizations/${oid}`, {
method: 'PUT',
body: data,
}),

// Delete organization
delete: (oid: string) =>
apiRequest<void>(`/organizations/${oid}`, {
method: 'DELETE',
}),

// Get organization's events
events: (oid: string, params?: QueryParams) => {
const query = new URLSearchParams();
if (params?.limit !== undefined) {
query.append('limit', params.limit.toString());
}
if (params?.offset !== undefined) {
query.append('offset', params.offset.toString());
}
const queryString = query.toString();
return apiRequest<Event[]>(
queryString ? `/organizations/${oid}/events?${queryString}` : `/organizations/${oid}/events`
);
},
// Get organization's members
getMembers: (oid: string) => apiRequest<OrganizationUser>(`/organizations/${oid}/members`),

// Add member to organization
addMember: (oid: string, data: ManageOrganizationUser) =>
apiRequest<void, ManageOrganizationUser>(`/organizations/${oid}/members`, {
method: 'POST',
body: data,
}),

// Remove organization member
deleteMember: (oid: string, data: ManageOrganizationUser) =>
apiRequest<void, ManageOrganizationUser>(`/organizations/${oid}/members`, {
method: 'DELETE',
body: data,
}),
};
28 changes: 28 additions & 0 deletions src/api/users.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { apiRequest } from './client';
import type { User } from '../types/User.ts';
import type { Event } from '../types/Event.ts';
import type { Organization } from '../types/Organization.ts';

export const usersApi = {
// Get user by ID
getById: (id: string) => apiRequest<User>(`/users/${id}`),

// Update user
update: (id: string, data: User) =>
apiRequest<User>(`/users/${id}`, {
method: 'POST',
body: data,
}),

// Delete user
delete: (id: string) =>
apiRequest<void>(`/users/${id}`, {
method: 'DELETE',
}),

// Get user's events
getEvents: (id: string) => apiRequest<Event[]>(`/users/${id}/events`),

// Get user's organizations
getOrganizations: (id: string) => apiRequest<Organization[]>(`/users/${id}/organizations`),
};
79 changes: 22 additions & 57 deletions src/contexts/AuthContext.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/* eslint-disable react-refresh/only-export-components*/
import React, { createContext, useContext, useState, type ReactNode } from 'react';
import React, { createContext, useContext, useState, useEffect, type ReactNode } from 'react';
import { authApi } from '../api/auth';

type User = {
uid?: string;
Expand All @@ -23,7 +24,6 @@ type AuthProviderProps = {
};

const AuthContext = createContext<AuthContextType | undefined>(undefined);
const AUTH_API_BASE = '/api/v1/auth';

export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
const [user, setUser] = useState<User | null>(null);
Expand All @@ -33,77 +33,42 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
const fetchMe = async (): Promise<void> => {
try {
setLoading(true);
setError(null);

const response = await fetch(`${AUTH_API_BASE}/me`, {
headers: { Accept: 'application/json' },
credentials: 'include',
});

if (response.ok) {
const data: User = await response.json();
setUser(data);
} else {
setUser(null);
}
const data = await authApi.getMe();
setUser(data);
} catch (err: unknown) {
if (err instanceof Error) {
console.error('Failed to fetch user:', err);
setError(err.message);
} else {
console.error('Unknown error:', err);
setError('Unknown error');
}

setUser(null);
setError(err instanceof Error ? err.message : 'Unknown error');
} finally {
setLoading(false);
}
};

useEffect(() => {
fetchMe();

const timeout = setTimeout(() => {
fetchMe();
}, 1000);

return () => clearTimeout(timeout);
}, []);

const login = (provider: 'google' | 'microsoft'): void => {
const url = provider === 'google' ? `${AUTH_API_BASE}/google` : `${AUTH_API_BASE}/microsoft`;

window.open(url, '_blank');

const pollInterval = setInterval(async () => {
try {
const response = await fetch(`${AUTH_API_BASE}/me`, {
headers: { Accept: 'application/json' },
credentials: 'include',
});

if (response.ok) {
const data: User = await response.json();
setUser(data);
clearInterval(pollInterval);
}
} catch (err: unknown) {
if (err instanceof Error) {
console.error('Polling failed:', err.message);
}
}
}, 3000);

setTimeout(() => {
clearInterval(pollInterval);
}, 120000);
authApi.login(provider);
};

const logout = async (): Promise<void> => {
try {
await fetch(`${AUTH_API_BASE}/logout`, {
method: 'POST',
credentials: 'include',
});

await authApi.logout();
setUser(null);

window.location.href = '/app/';
window.location.href = '/app';
} catch (err: unknown) {
if (err instanceof Error) {
console.error('Logout failed:', err.message);
}
console.error('Logout failed:', err);
}
};

const value: AuthContextType = {
user,
loading,
Expand Down
Loading
Loading