Skip to content

Commit

Permalink
Implement sign in and fix auth
Browse files Browse the repository at this point in the history
  • Loading branch information
AmirAgassi committed Dec 7, 2024
1 parent 118a991 commit a8f7a53
Show file tree
Hide file tree
Showing 4 changed files with 97 additions and 84 deletions.
55 changes: 22 additions & 33 deletions frontend/src/contexts/AuthContext.tsx
Original file line number Diff line number Diff line change
@@ -1,52 +1,41 @@
import React, { createContext, useContext, useState, useEffect } from 'react';
import React, { createContext, useContext, useState } from 'react';
import type { User } from '@/types';

interface AuthContextType {
user: User | null;
companyId: string | null;
setUser: (user: User | null) => void;
setCompanyId: (id: string | null) => void;
accessToken: string | null;
setAuth: (user: User | null, token: string | null, companyId?: string | null) => void;
clearAuth: () => void;
}

const AuthContext = createContext<AuthContextType | undefined>(undefined);

export function AuthProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const [companyId, setCompanyId] = useState<string | null>(null);
const [accessToken, setAccessToken] = useState<string | null>(null);

// Load user from localStorage on mount
useEffect(() => {
const storedUser = localStorage.getItem('user');
const storedCompanyId = localStorage.getItem('company_id');

if (storedUser) {
setUser(JSON.parse(storedUser));
}
if (storedCompanyId) {
setCompanyId(storedCompanyId);
}
}, []);
const setAuth = (user: User | null, token: string | null, companyId: string | null = null) => {
setUser(user);
setAccessToken(token);
setCompanyId(companyId);
};

// Save user to localStorage when it changes
useEffect(() => {
if (user) {
localStorage.setItem('user', JSON.stringify(user));
} else {
localStorage.removeItem('user');
}
}, [user]);

// Save companyId to localStorage when it changes
useEffect(() => {
if (companyId) {
localStorage.setItem('company_id', companyId);
} else {
localStorage.removeItem('company_id');
}
}, [companyId]);
const clearAuth = () => {
setUser(null);
setAccessToken(null);
setCompanyId(null);
};

return (
<AuthContext.Provider value={{ user, companyId, setUser, setCompanyId }}>
<AuthContext.Provider value={{
user,
companyId,
accessToken,
setAuth,
clearAuth
}}>
{children}
</AuthContext.Provider>
);
Expand Down
72 changes: 61 additions & 11 deletions frontend/src/pages/Register.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { useState, useEffect, FormEvent } from 'react';
import { Button, TextInput, TextArea } from '@components';
import { register, RegisterError } from '@services';
import { register, signin, RegisterError, ApiError } from '@services';
import { useAuth } from '@/contexts/AuthContext';
import { useNavigate } from 'react-router-dom';

type RegistrationStep =
| 'login-register'
Expand All @@ -27,7 +28,8 @@ interface FormErrors {
}

const Register = () => {
const { setUser, setCompanyId } = useAuth();
const navigate = useNavigate();
const { setAuth } = useAuth();
const [currentStep, setCurrentStep] = useState<RegistrationStep>('login-register');
const [formData, setFormData] = useState<FormData>({
firstName: '',
Expand All @@ -39,6 +41,7 @@ const Register = () => {
password: '',
});
const [errors, setErrors] = useState<FormErrors>({});
const [isLoading, setIsLoading] = useState(false);

const LINKEDIN_REGEX =
/^(https?:\/\/)?([\w]+\.)?linkedin\.com\/(pub|in|profile)\/([-a-zA-Z0-9]+)\/?$/;
Expand Down Expand Up @@ -87,18 +90,56 @@ const Register = () => {

const handleInitialSubmit = async (e: FormEvent) => {
e.preventDefault();
setIsLoading(true);
try {
const regResp = await register(formData.email, formData.password);

setUser(regResp.user);
setCompanyId('mock-company-id');
setAuth(regResp.user, regResp.accessToken, 'mock-company-id');
setCurrentStep('verify-email');
} catch (error) {
if (error instanceof RegisterError) {
console.log('do something here', error.statusCode, error.body);
setErrors(prev => ({
...prev,
email: error.body.message || 'Registration failed'
}));
} else {
setErrors(prev => ({
...prev,
email: 'An unexpected error occurred'
}));
}
} finally {
setIsLoading(false);
}
};

const handleLogin = async () => {
setIsLoading(true);
try {
const signinResp = await signin(formData.email, formData.password);
setAuth(signinResp.user, signinResp.accessToken);

// Redirect based on user role
if (signinResp.user.role === 'admin') {
navigate('/admin/dashboard');
} else if (signinResp.user.role === 'startup_owner') {
navigate('/dashboard');
} else if (signinResp.user.role === 'investor') {
navigate('/dashboard'); // or a specific investor dashboard
}
} catch (error) {
if (error instanceof ApiError) {
setErrors(prev => ({
...prev,
email: 'Invalid email or password'
}));
} else {
// TODO: handle error with some kind of notification
setErrors(prev => ({
...prev,
email: 'An unexpected error occurred'
}));
}
} finally {
setIsLoading(false);
}
};

Expand Down Expand Up @@ -134,6 +175,7 @@ const Register = () => {
name="email"
value={formData.email}
onChange={handleChange}
error={errors.email}
/>

<TextInput
Expand All @@ -143,10 +185,17 @@ const Register = () => {
name="password"
value={formData.password}
onChange={handleChange}
error={errors.password}
/>

<Button type="submit" size="lg" liquid variant="primary">
Register
<Button
type="submit"
size="lg"
liquid
variant="primary"
disabled={isLoading}
>
{isLoading ? 'Please wait...' : 'Register'}
</Button>

<div className="text-center mt-4">
Expand All @@ -155,9 +204,10 @@ const Register = () => {
type="button"
liquid
size="lg"
// TODO: onClick to handle login
onClick={handleLogin}
disabled={isLoading}
>
Login
{isLoading ? 'Please wait...' : 'Login'}
</Button>
</div>
</form>
Expand Down
52 changes: 13 additions & 39 deletions frontend/src/services/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,17 @@
*/

import { getApiUrl, HttpStatusCode } from '@utils';
import { RegisterError } from './errors';
import { RegisterError, ApiError } from './errors';

import type { User, UserRole } from '@t';

export interface RegisterResponse {
export interface AuthResponse {
accessToken: string;
user: User;
}

export interface SigninResponse {
accessToken: string;
user: User;
}
export interface RegisterReponse extends AuthResponse {}
export interface SigninResponse extends AuthResponse {}

/**
* Registers a user if the given email is not already registered.
Expand All @@ -24,7 +22,7 @@ export async function register(
email: string,
password: string,
role: UserRole = 'startup_owner'
): Promise<RegisterResponse> {
): Promise<RegisterReponse> {
const url = getApiUrl('/auth/signup');
const body = {
email,
Expand All @@ -38,20 +36,19 @@ export async function register(
headers: {
'Content-Type': 'application/json',
},
credentials: 'include', // needed for cookies
});

// the backend should always return json for the api calls
const json = await res.json();

if (res.status !== HttpStatusCode.CREATED) {
throw new RegisterError('Failed to register', res.status, json);
}

return json as RegisterResponse;
return json as RegisterReponse;
}

/**
* Signs in a user with email and password.
* Signs in a user with email and password
*/
export async function signin(
email: string,
Expand All @@ -69,46 +66,23 @@ export async function signin(
headers: {
'Content-Type': 'application/json',
},
credentials: 'include', // needed for cookies
});

const json = await res.json();

if (res.status !== HttpStatusCode.OK) {
throw new RegisterError('Failed to sign in', res.status, json);
throw new ApiError('Failed to sign in', res.status, json);
}

return json as SigninResponse;
}

/**
* Refreshes the access token using the refresh token stored in HTTP-only cookie.
* Returns the new access token if successful.
*/
export async function refreshAccessToken(): Promise<string> {
const url = getApiUrl('/auth/refresh');

const res = await fetch(url, {
method: 'POST',
credentials: 'include', // needed for cookies
});

if (!res.ok) {
throw new Error('Failed to refresh access token');
}

const json = await res.json();
return json.access_token;
}

/**
* Signs out the user by clearing the refresh token cookie.
* Signs out the current user by:
* 1. Calling the signout endpoint to clear the refresh token cookie
* 2. Clearing the auth context
*/
export async function signout(): Promise<void> {
const url = getApiUrl('/auth/signout');

await fetch(url, {
method: 'POST',
credentials: 'include',
});
await fetch(url, { method: 'POST' });
}
2 changes: 1 addition & 1 deletion frontend/src/services/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export { register, saveRefreshToken } from './auth';
export { register, signin, signout } from './auth';
export { RegisterError, ApiError, API_ERROR, REGISTER_ERROR } from './errors';
export { createProject } from './project';
export { createCompany } from './company';
Expand Down

0 comments on commit a8f7a53

Please sign in to comment.