From a8f7a5308687ed37976269cbfacebab738c5b224 Mon Sep 17 00:00:00 2001 From: AmirAgassi <33383085+AmirAgassi@users.noreply.github.com> Date: Sat, 7 Dec 2024 17:05:55 -0500 Subject: [PATCH] Implement sign in and fix auth --- frontend/src/contexts/AuthContext.tsx | 55 ++++++++------------ frontend/src/pages/Register.tsx | 72 +++++++++++++++++++++++---- frontend/src/services/auth.ts | 52 +++++-------------- frontend/src/services/index.ts | 2 +- 4 files changed, 97 insertions(+), 84 deletions(-) diff --git a/frontend/src/contexts/AuthContext.tsx b/frontend/src/contexts/AuthContext.tsx index 088334e0..f5f3689b 100644 --- a/frontend/src/contexts/AuthContext.tsx +++ b/frontend/src/contexts/AuthContext.tsx @@ -1,11 +1,12 @@ -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(undefined); @@ -13,40 +14,28 @@ const AuthContext = createContext(undefined); export function AuthProvider({ children }: { children: React.ReactNode }) { const [user, setUser] = useState(null); const [companyId, setCompanyId] = useState(null); + const [accessToken, setAccessToken] = useState(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 ( - + {children} ); diff --git a/frontend/src/pages/Register.tsx b/frontend/src/pages/Register.tsx index e18cb14a..6dbb768b 100644 --- a/frontend/src/pages/Register.tsx +++ b/frontend/src/pages/Register.tsx @@ -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' @@ -27,7 +28,8 @@ interface FormErrors { } const Register = () => { - const { setUser, setCompanyId } = useAuth(); + const navigate = useNavigate(); + const { setAuth } = useAuth(); const [currentStep, setCurrentStep] = useState('login-register'); const [formData, setFormData] = useState({ firstName: '', @@ -39,6 +41,7 @@ const Register = () => { password: '', }); const [errors, setErrors] = useState({}); + const [isLoading, setIsLoading] = useState(false); const LINKEDIN_REGEX = /^(https?:\/\/)?([\w]+\.)?linkedin\.com\/(pub|in|profile)\/([-a-zA-Z0-9]+)\/?$/; @@ -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); } }; @@ -134,6 +175,7 @@ const Register = () => { name="email" value={formData.email} onChange={handleChange} + error={errors.email} /> { name="password" value={formData.password} onChange={handleChange} + error={errors.password} /> -
@@ -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'}
diff --git a/frontend/src/services/auth.ts b/frontend/src/services/auth.ts index e94a514e..4b57ab8e 100644 --- a/frontend/src/services/auth.ts +++ b/frontend/src/services/auth.ts @@ -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. @@ -24,7 +22,7 @@ export async function register( email: string, password: string, role: UserRole = 'startup_owner' -): Promise { +): Promise { const url = getApiUrl('/auth/signup'); const body = { email, @@ -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, @@ -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 { - 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 { const url = getApiUrl('/auth/signout'); - - await fetch(url, { - method: 'POST', - credentials: 'include', - }); + await fetch(url, { method: 'POST' }); } diff --git a/frontend/src/services/index.ts b/frontend/src/services/index.ts index 58303032..68d3232e 100644 --- a/frontend/src/services/index.ts +++ b/frontend/src/services/index.ts @@ -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';