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
5 changes: 5 additions & 0 deletions backend/app/routes/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@
# TODO: ADD RATE LIMITING
@router.post("/register", response_model=UserCreateResponse)
async def register_user(user: UserCreateRequest, user_service: UserService = Depends(get_user_service)):
allowed_Admins = ["[email protected]", "[email protected]"]
if user.role == UserRole.ADMIN:
if user.email not in allowed_Admins:
raise HTTPException(status_code=403, detail="Access denied. Admin privileges required for admin portal")

try:
return await user_service.create_user(user)
except HTTPException as http_ex:
Expand Down
52 changes: 36 additions & 16 deletions frontend/src/APIClients/authAPIClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,11 @@ export interface AuthResult {
validationErrors?: string[];
}

export const login = async (email: string, password: string): Promise<AuthResult> => {
export const login = async (
email: string,
password: string,
isAdminPortal: boolean = false,
): Promise<AuthResult> => {
try {
// Validate inputs
if (!validateEmail(email)) {
Expand Down Expand Up @@ -63,12 +67,31 @@ export const login = async (email: string, password: string): Promise<AuthResult
// Attempt backend login
try {
const loginRequest: LoginRequest = { email, password };
const { data } = await baseAPIClient.post<AuthResponse>('/auth/login', loginRequest, {
withCredentials: true,
});
const headers: any = { withCredentials: true };

// Add admin portal header if this is an admin login
if (isAdminPortal) {
headers.headers = { 'X-Admin-Portal': 'true' };
}

const { data } = await baseAPIClient.post<AuthResponse>('/auth/login', loginRequest, headers);
localStorage.setItem(AUTHENTICATED_USER_KEY, JSON.stringify(data));
return { success: true, user: { ...data.user, ...data } as AuthenticatedUser };
} catch {
return { success: true, user: { ...data.user, ...data } };
} catch (error) {
// Handle admin privilege errors specifically
if (error && typeof error === 'object' && 'response' in error) {
const response = (error as { response?: { status?: number; data?: { detail?: string } } })
.response;
if (response?.status === 403 && isAdminPortal) {
return {
success: false,
error:
'Access denied. You do not have admin privileges. Please contact an administrator.',
errorCode: 'auth/insufficient-privileges',
};
}
}

// Backend login failure is not critical since Firebase auth succeeded
return {
success: true,
Expand Down Expand Up @@ -217,22 +240,19 @@ export const register = async ({
} else {
console.warn('[REGISTER] Failed to send email verification after registration');
}

// Return success with user info - don't try to login since email isn't verified yet
return {
success: true,
user: { email: user.email, uid: user.uid } as unknown as AuthenticatedUser,
};
} catch (firebaseError) {
console.error('[REGISTER] Firebase sign-in failed:', firebaseError);
// Continue with registration even if Firebase sign-in fails
// The user can still verify their email later
}

// Try backend login but don't fail if it doesn't work
try {
const loginResult = await login(email, password);
return loginResult;
} catch (loginError) {
console.warn('[REGISTER] Backend login failed, but registration was successful:', loginError);
// Return success even if backend login fails, since Firebase user was created
return {
success: true,
user: { email, uid: auth.currentUser?.uid || 'unknown' } as unknown as AuthenticatedUser,
user: { email, uid: 'unknown' } as unknown as AuthenticatedUser,
};
}
} catch (error) {
Expand Down
9 changes: 5 additions & 4 deletions frontend/src/pages/admin-login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,12 @@ export default function AdminLogin() {

try {
const result = await login(email, password);
if (result) {
console.log('Admin login success:', result);
if (result.success) {
router.push('/admin/dashboard');
} else if (result.errorCode === 'auth/email-not-verified') {
router.push(`/admin-verify?email=${encodeURIComponent(email)}&role=admin`);
} else {
setError('Invalid email or password');
setError('Invalid email or password. Please check your credentials and try again.');
}
} catch (err: unknown) {
console.error('Admin login error:', err);
Expand Down Expand Up @@ -203,7 +204,7 @@ export default function AdminLogin() {
>
Don&apos;t have an account?{' '}
<Link
href="/participant-form"
href="/admin-signup"
style={{
color: teal,
textDecoration: 'underline',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,25 +38,25 @@ export default function AdminLoginPage() {
signupMethod: SignUpMethod.PASSWORD,
};
const result = await register(userData);
console.log('Admin registration success:', result);
router.push(`/verify?email=${encodeURIComponent(email)}&role=admin`);
} catch (err: unknown) {
console.error('Admin registration error:', err);
if (
err &&
typeof err === 'object' &&
'response' in err &&
err.response &&
typeof err.response === 'object' &&
'data' in err.response &&
err.response.data &&
typeof err.response.data === 'object' &&
'detail' in err.response.data
) {
setError((err.response.data as { detail: string }).detail || 'Registration failed');
console.log('?', result);
// Check if it's an admin privilege error
if (!result.success && result.error && result.error.includes('Admin privileges required')) {
setError(
'Access denied. Admin registration is restricted. Please contact an administrator.',
);
return;
}

// If successful (even if success is false, check if we got a user)
if (result.user || result.success) {
console.log('Admin registration success:', result);
router.push(`/admin-verify?email=${encodeURIComponent(email)}&role=admin`);
} else {
setError('Registration failed');
setError(result.error || 'Registration failed');
}
} catch (err: unknown) {
console.error('Admin registration error:', err);
setError('Registration failed');
}
};

Expand Down Expand Up @@ -247,7 +247,7 @@ export default function AdminLoginPage() {
>
Already have an account?{' '}
<Link
href="/admin"
href="/admin-login"
style={{
color: teal,
textDecoration: 'underline',
Expand Down
189 changes: 189 additions & 0 deletions frontend/src/pages/admin-verify.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
import { useRouter } from 'next/router';
import { Box, Flex, Heading, Text, Link } from '@chakra-ui/react';
import { useEffect, useState } from 'react';
import { useEmailVerification } from '@/hooks/useEmailVerification';
import {
getEmailForSignIn,
clearEmailForSignIn,
setEmailForSignIn,
} from '@/services/firebaseAuthService';
import { auth } from '@/config/firebase';
import Image from 'next/image';

const veniceBlue = '#1d3448';

export default function AdminVerifyPage() {
const router = useRouter();
const { email, role } = router.query;
const [displayEmail, setDisplayEmail] = useState<string>('');
const [message, setMessage] = useState<{ type: 'success' | 'error' | null; text: string }>({
type: null,
text: '',
});

const { sendVerificationEmail, sendSignInLink, isLoading, error, success } =
useEmailVerification();

useEffect(() => {
// Get email from query params or localStorage
const emailFromQuery = email as string;
const emailFromStorage = getEmailForSignIn();
const finalEmail = emailFromQuery || emailFromStorage || '[email protected]';
setDisplayEmail(finalEmail);

// Store the email from query params if available
if (emailFromQuery) {
setEmailForSignIn(emailFromQuery);
}
}, [email]);

useEffect(() => {
if (success) {
setMessage({ type: 'success', text: 'Email sent successfully! Please check your inbox.' });
}
}, [success]);

useEffect(() => {
if (error) {
setMessage({ type: 'error', text: error });
}
}, [error]);

const handleResendEmail = async () => {
setMessage({ type: null, text: '' });

// For admin users, send verification email instead of sign-in link
await sendVerificationEmail();
};

const handleBackToLogin = () => {
clearEmailForSignIn();
router.push('/admin-login');
};

return (
<Flex minH="100vh" direction={{ base: 'column', md: 'row' }}>
{/* Left: Admin Verification Content */}
<Flex
flex="1"
align="center"
justify="center"
px={{ base: 4, md: 12 }}
py={{ base: 16, md: 0 }}
bg="white"
minH={{ base: '60vh', md: '100vh' }}
>
<Box w="full" maxW="520px">
<Heading
as="h1"
fontFamily="'Open Sans', sans-serif"
fontWeight={600}
color={veniceBlue}
fontSize={{ base: '3xl', md: '4xl', lg: '5xl' }}
lineHeight="50px"
mb={2}
>
Admin Portal - First Connection Peer Support Program
</Heading>
<Heading
as="h2"
fontFamily="'Open Sans', sans-serif"
fontWeight={600}
color={veniceBlue}
fontSize={{ base: 'xl', md: '2xl' }}
mb={6}
mt={8}
>
Verify Your Admin Account
</Heading>
<Text
mb={8}
color={veniceBlue}
fontFamily="'Open Sans', sans-serif"
fontWeight={400}
fontSize="lg"
>
We sent a confirmation link to <b>{displayEmail}</b>
</Text>

{message.type === 'success' && (
<Text
mb={4}
color="green.600"
fontFamily="'Open Sans', sans-serif"
fontWeight={600}
fontSize="md"
>
{message.text}
</Text>
)}

{message.type === 'error' && (
<Text
mb={4}
color="red.600"
fontFamily="'Open Sans', sans-serif"
fontWeight={600}
fontSize="md"
>
{message.text}
</Text>
)}

<Text
color={veniceBlue}
fontFamily="'Open Sans', sans-serif"
fontWeight={400}
fontSize="md"
mb={4}
>
Didn&apos;t get a link?{' '}
<Link
onClick={handleResendEmail}
style={{
color: '#056067',
textDecoration: 'underline',
fontWeight: 600,
fontFamily: 'Open Sans, sans-serif',
cursor: 'pointer',
}}
>
Click here to resend.
</Link>
</Text>

<Text
color={veniceBlue}
fontFamily="'Open Sans', sans-serif"
fontWeight={400}
fontSize="md"
>
Remember your password?{' '}
<Link
href="/admin-login"
style={{
color: '#056067',
textDecoration: 'underline',
fontWeight: 600,
fontFamily: 'Open Sans, sans-serif',
}}
>
Back to login
</Link>
</Text>
</Box>
</Flex>
{/* Right: Image - Using admin.png from admin-login.tsx */}
<Box flex="1" display={{ base: 'none', md: 'block' }} position="relative" minH="100vh">
<Image
src="/admin.png"
alt="Admin Portal Visual"
fill
sizes="(max-width: 768px) 100vw, 50vw"
style={{ objectFit: 'cover', objectPosition: '90% 50%' }}
priority
/>
</Box>
</Flex>
);
}
Loading