Skip to content
Merged
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
35 changes: 0 additions & 35 deletions .github/workflows/cd-workflow.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,41 +11,6 @@ jobs:
- name: Checkout code
uses: actions/checkout@v2

- name: context
uses: okteto/context@latest
with:
token: ${{ secrets.OKTETO_TOKEN }}

- name: 'Activate Namespace'
uses: okteto/namespace@latest
with:
namespace: pratyush1712

- name: 'Trigger the pipeline'
uses: okteto/pipeline@latest
with:
name: carriage-web
timeout: 30m
variables:
'AWS_SECRET_ACCESS_KEY=${{ secrets.AWS_SECRET_ACCESS_KEY }},
AWS_ACCESS_KEY_ID=${{ secrets.AWS_ACCESS_KEY_ID }},
JWT_SECRET=${{ secrets.JWT_SECRET }},
PUBLIC_VAPID_KEY=${{ secrets.PUBLIC_VAPID_KEY }},
PRIVATE_VAPID_KEY=${{ secrets.PRIVATE_VAPID_KEY }},
WEB_PUSH_CONTACT=${{ secrets.WEB_PUSH_CONTACT }},
IOS_DRIVER_ARN=${{ secrets.IOS_DRIVER_ARN }},
IOS_RIDER_ARN=${{ secrets.IOS_RIDER_ARN }},
ANDROID_ARN=${{ secrets.ANDROID_ARN }},
HOSTNAME=${{ secrets.HOSTNAME }},
USE_HOSTNAME=${{ secrets.USE_HOSTNAME }},
OAUTH_CLIENT_ID=${{ secrets.REACT_APP_CLIENT_ID }}
OAUTH_CLIENT_SECRET=${{ secrets.OAUTH_CLIENT_SECRET }}
REACT_APP_SERVER_URL=${{ secrets.REACT_APP_SERVER_URL }},
REACT_APP_CLIENT_ID=${{ secrets.REACT_APP_CLIENT_ID }},
REACT_APP_PUBLIC_VAPID_KEY=${{ secrets.REACT_APP_PUBLIC_VAPID_KEY }},
REACT_APP_ENCRYPTION_KEY=${{ secrets.REACT_APP_ENCRYPTION_KEY }},
DOMAIN=${{ secrets.DOMAIN }}'

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2

Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ scripts

server/.env

# Secret certificates (should not be committed)
server/config/*.crt

# build
build/

Expand Down
14 changes: 7 additions & 7 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,15 @@ FROM node:16-alpine
# Create a directory for the app
WORKDIR /app

# Set production environment for node
ENV NODE_ENV=production

# Read build-time environment variables
ARG REACT_APP_SERVER_URL
ENV REACT_APP_SERVER_URL ${REACT_APP_SERVER_URL}
ENV REACT_APP_SERVER_URL=${REACT_APP_SERVER_URL}
ARG REACT_APP_CLIENT_ID
ENV REACT_APP_CLIENT_ID ${REACT_APP_CLIENT_ID}
ENV REACT_APP_CLIENT_ID=${REACT_APP_CLIENT_ID}
ARG REACT_APP_PUBLIC_VAPID_KEY
ENV REACT_APP_PUBLIC_VAPID_KEY ${REACT_APP_PUBLIC_VAPID_KEY}
ENV REACT_APP_PUBLIC_VAPID_KEY=${REACT_APP_PUBLIC_VAPID_KEY}
ARG REACT_APP_ENCRYPTION_KEY
ENV REACT_APP_ENCRYPTION_KEY ${REACT_APP_ENCRYPTION_KEY}
ENV REACT_APP_ENCRYPTION_KEY=${REACT_APP_ENCRYPTION_KEY}

# Copy package.jsons first to install
COPY package.json package-lock.json /app/
Expand All @@ -36,6 +33,9 @@ RUN npm run build
WORKDIR /app/server
RUN npm run build

# Set production environment after build
ENV NODE_ENV=production

# Expose port 3001 for the server
EXPOSE 3001

Expand Down
3 changes: 1 addition & 2 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
"@mui/material": "^6.1.10",
"@mui/x-date-pickers": "^7.23.1",
"@react-aria/utils": "^3.25.2",
"@react-oauth/google": "^0.12.1",
"@types/crypto-js": "^4.2.2",
"@types/jest": "^29.5.13",
"@types/node": "^22.5.5",
Expand All @@ -22,7 +21,6 @@
"@types/react-dom": "^18.3.0",
"@types/react-router-dom": "^5.3.3",
"@types/react-router-hash-link": "^2.4.9",
"@types/uuid": "^10.0.0",
"@vis.gl/react-google-maps": "^1.4.0",
"addresser": "^1.1.20",
"axios": "^1.12.0",
Expand Down Expand Up @@ -74,6 +72,7 @@
]
},
"devDependencies": {
"@types/uuid": "^10.0.0",
"@typescript-eslint/eslint-plugin": "^5.62.0",
"@typescript-eslint/parser": "^5.62.0",
"autoprefixer": "^10.4.21",
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React, { useEffect } from 'react';
import { BrowserRouter as Router } from 'react-router-dom';
import { ToastProvider } from './context/toastContext';
import { GoogleAuth } from './components/AuthManager/GoogleAuth';
import AuthManager from './components/AuthManager/AuthManager';
import './styles/App.css';
import { setAuthToken } from './util/axios';

Expand All @@ -19,7 +19,7 @@ const App = () => {
return (
<Router>
<ToastProvider>
<GoogleAuth />
<AuthManager />
</ToastProvider>
</Router>
);
Expand Down
199 changes: 132 additions & 67 deletions frontend/src/components/AuthManager/AuthManager.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import React, { useState, useEffect } from 'react';
import {
useGoogleLogin as googleAuth,
googleLogout,
} from '@react-oauth/google';
import { useNavigate, Navigate, Route, Routes } from 'react-router-dom';
useNavigate,
Navigate,
Route,
Routes,
useSearchParams,
} from 'react-router-dom';
import { jwtDecode } from 'jwt-decode';
import AuthContext from '../../context/auth';

Expand Down Expand Up @@ -47,8 +49,10 @@ const AuthManager = () => {
const [refreshUser, setRefreshUser] = useState(() =>
createRefresh(id, localStorage.getItem('userType') || '', jwtValue())
);
const [ssoError, setSsoError] = useState<string>('');

const navigate = useNavigate();
const [searchParams] = useSearchParams();

useEffect(() => {
const token = jwtValue();
Expand All @@ -57,6 +61,96 @@ const AuthManager = () => {
}
}, []);

// SSO Callback handler - fetches profile and JWT after successful SSO login
const handleSSOCallback = async () => {
try {
const response = await fetch(
`${process.env.REACT_APP_SERVER_URL}/api/sso/profile`,
{
credentials: 'include', // CRITICAL: Sends session cookie
}
);

if (!response.ok) {
throw new Error('Failed to fetch SSO profile');
}

const data = await response.json();
const { user: ssoUser, token: serverJWT } = data;

if (serverJWT && ssoUser) {
// Store JWT in encrypted cookie (matching Google OAuth pattern)
setCookie('jwt', serverJWT);

// Decode JWT to get user info
const decoded: any = jwtDecode(serverJWT);

// Set auth state
setId(decoded.id);
localStorage.setItem('userId', decoded.id);
localStorage.setItem('userType', decoded.userType);
setAuthToken(serverJWT);

// Refresh user data
const refreshFunc = createRefresh(
decoded.id,
decoded.userType,
serverJWT
);
refreshFunc();
setRefreshUser(() => refreshFunc);
setSignedIn(true);

// Navigate to appropriate dashboard based on userType
if (decoded.userType === 'Admin') {
navigate('/admin/home', { replace: true });
} else if (decoded.userType === 'Driver') {
navigate('/driver/rides', { replace: true });
} else if (decoded.userType === 'Rider') {
navigate('/rider/schedule', { replace: true });
} else {
// Invalid userType - this should never happen if backend is working correctly
setSsoError('Invalid user type received. Please contact support.');
logout();
}
} else {
setSsoError('Failed to complete SSO login. Please try again.');
logout();
}
} catch (error) {
console.error('SSO callback error:', error);
setSsoError('Failed to complete login. Please try again.');
logout();
}
};

// SSO callback handler
useEffect(() => {
const authParam = searchParams.get('auth');
const errorParam = searchParams.get('error');

if (errorParam) {
// Handle SSO errors
const errorMessages: { [key: string]: string } = {
user_not_found:
'Your Cornell account is not registered. Please contact support.',
'User not active': 'Your account is inactive. Please contact support.',
sso_failed: 'SSO authentication failed. Please try again.',
};
setSsoError(
errorMessages[errorParam] || 'Authentication failed. Please try again.'
);
navigate('/', { replace: true });
return;
}

if (authParam === 'sso_success') {
// Fetch profile and JWT token from backend
handleSSOCallback();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [searchParams]);

function getCookie(name: string) {
return document.cookie.split(';').some((c) => {
return c.trim().startsWith(name + '=');
Expand Down Expand Up @@ -90,73 +184,31 @@ const AuthManager = () => {
document.cookie = `${cookieName}=${encrypt(value)};secure=true;path=/;`;
}

function signIn(userType: 'Admin' | 'Rider' | 'Driver', code: string) {
const table = `${userType}s`;
const localUserType = localStorage.getItem('userType');
if (!localUserType || localUserType === userType) {
axios
.post('/api/auth', { code, table })
.then((res) => res.data.jwt)
.then((serverJWT) => {
if (serverJWT) {
setCookie('jwt', serverJWT);
const decoded: any = jwtDecode(serverJWT);
setId(decoded.id);
localStorage.setItem('userId', decoded.id);
localStorage.setItem('userType', decoded.userType);
setAuthToken(serverJWT);
console.log('Auth Token : ', serverJWT);
const refreshFunc = createRefresh(decoded.id, userType, serverJWT);
refreshFunc();
setRefreshUser(() => refreshFunc);
setSignedIn(true);

// Navigate based on user type
if (userType === 'Admin') {
navigate('/admin/home', { replace: true });
} else if (userType === 'Driver') {
navigate('/driver/rides', { replace: true });
} else {
navigate('/rider/schedule', { replace: true });
}
} else {
logout();
}
})
.catch((error) => {
console.error('Login error:', error);
logout();
});
}
}

const adminLogin = googleAuth({
flow: 'auth-code',
onSuccess: async (res) => signIn('Admin', res.code),
onError: (errorResponse) => console.error(errorResponse),
});
// SSO Login handlers
function handleSSOLogin(isAdmin: boolean = false, isDriver: boolean = false) {
const frontendUrl = window.location.origin;
const redirectUri = encodeURIComponent(`${frontendUrl}/`);

const studentLogin = googleAuth({
flow: 'auth-code',
onSuccess: async (res) => signIn('Rider', res.code),
onError: (errorResponse) => console.error(errorResponse),
});
// Determine user type based on button clicked (matching Google OAuth pattern)
let userType = 'Rider';
if (isAdmin) {
userType = 'Admin';
} else if (isDriver) {
userType = 'Driver';
}

const driverLogin = googleAuth({
flow: 'auth-code',
onSuccess: async (res) => signIn('Driver', res.code),
onError: (errorResponse) => console.error(errorResponse),
});
const ssoUrl = `${process.env.REACT_APP_SERVER_URL}/api/sso/login?redirect_uri=${redirectUri}&userType=${userType}`;
window.location.href = ssoUrl;
}

function logout() {
googleLogout();
localStorage.removeItem('userType');
localStorage.removeItem('userId');
localStorage.removeItem('user');
deleteCookie('jwt');
setAuthToken('');
setSignedIn(false);
navigate('/', { replace: true });
window.location.href = `${process.env.REACT_APP_SERVER_URL}/api/sso/logout`;
}

function createRefresh(userId: string, userType: string, token: string) {
Expand Down Expand Up @@ -190,29 +242,42 @@ const AuthManager = () => {
path="/"
element={
<LandingPage
ssoError={ssoError}
students={
<button onClick={() => studentLogin()} className={styles.btn}>
<button
onClick={() => handleSSOLogin(false, false)}
className={styles.ssoBtn}
>
<img
src={studentLanding}
className={styles.icon}
alt="student logo"
/>
<div className={styles.heading}>Students</div>
Sign in with Google
<div>Sign in with</div>
<div>Cornell NetID</div>
</button>
}
admins={
<button onClick={() => adminLogin()} className={styles.btn}>
<button
onClick={() => handleSSOLogin(true, false)}
className={styles.ssoBtn}
>
<img src={admin} className={styles.icon} alt="admin logo" />
<div className={styles.heading}>Admins</div>
Sign in with Google
<div>Sign in with</div>
<div>Cornell NetID</div>
</button>
}
drivers={
<button onClick={() => driverLogin()} className={styles.btn}>
<button
onClick={() => handleSSOLogin(false, true)}
className={styles.ssoBtn}
>
<img src={car} className={styles.icon} alt="car logo" />
<div className={styles.heading}>Drivers</div>
Sign in with Google
<div>Sign in with</div>
<div>Cornell NetID</div>
</button>
}
/>
Expand Down
Loading
Loading