Skip to content

Commit 10d7876

Browse files
Admin login, signup and password reset (#60)
## Notion ticket link <!-- Please replace with your ticket's URL --> [Ticket Name](https://www.notion.so/uwblueprintexecs/29c47ce13e7c45d69f4a5db9c333940a?v=4d15fa5e11da46c888136f31f84b5b0a&p=1fd10f3fb1dc809686a7ea3b4f7c2ca0&pm=s) <!-- Give a quick summary of the implementation details, provide design justifications if necessary --> ## Implementation description * Added admin signup, login and reset password * Verification flow for users who did not verify on first register might not work properly (will fix after) * Created list in auth.py that has valid emails for admins <!-- What should the reviewer do to verify your changes? Describe expected results and include screenshots when appropriate --> ## Steps to test 1. Go to http://localhost:3000/admin-signup 2. Test 3 admin functions (add your email to valid email list in auth.py) 3. Test if regular user authentication still works 4. Test logging in in the wrong side of the website <!-- Draw attention to the substantial parts of your PR or anything you'd like a second opinion on --> ## What should reviewers focus on? * ## Checklist - [ ] My PR name is descriptive and in imperative tense - [ ] My commit messages are descriptive and in imperative tense. My commits are atomic and trivial commits are squashed or fixup'd into non-trivial commits - [ ] I have run the appropriate linter(s) - [ ] I have requested a review from the PL, as well as other devs who have background knowledge on this PR or who will be building on top of this PR
1 parent 06a7c69 commit 10d7876

File tree

7 files changed

+259
-60
lines changed

7 files changed

+259
-60
lines changed

backend/app/routes/auth.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@
1717
# TODO: ADD RATE LIMITING
1818
@router.post("/register", response_model=UserCreateResponse)
1919
async def register_user(user: UserCreateRequest, user_service: UserService = Depends(get_user_service)):
20+
allowed_Admins = ["[email protected]", "[email protected]"]
21+
if user.role == UserRole.ADMIN:
22+
if user.email not in allowed_Admins:
23+
raise HTTPException(status_code=403, detail="Access denied. Admin privileges required for admin portal")
24+
2025
try:
2126
return await user_service.create_user(user)
2227
except HTTPException as http_ex:

frontend/src/APIClients/authAPIClient.ts

Lines changed: 36 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,11 @@ export interface AuthResult {
3535
validationErrors?: string[];
3636
}
3737

38-
export const login = async (email: string, password: string): Promise<AuthResult> => {
38+
export const login = async (
39+
email: string,
40+
password: string,
41+
isAdminPortal: boolean = false,
42+
): Promise<AuthResult> => {
3943
try {
4044
// Validate inputs
4145
if (!validateEmail(email)) {
@@ -63,12 +67,31 @@ export const login = async (email: string, password: string): Promise<AuthResult
6367
// Attempt backend login
6468
try {
6569
const loginRequest: LoginRequest = { email, password };
66-
const { data } = await baseAPIClient.post<AuthResponse>('/auth/login', loginRequest, {
67-
withCredentials: true,
68-
});
70+
const headers: any = { withCredentials: true };
71+
72+
// Add admin portal header if this is an admin login
73+
if (isAdminPortal) {
74+
headers.headers = { 'X-Admin-Portal': 'true' };
75+
}
76+
77+
const { data } = await baseAPIClient.post<AuthResponse>('/auth/login', loginRequest, headers);
6978
localStorage.setItem(AUTHENTICATED_USER_KEY, JSON.stringify(data));
70-
return { success: true, user: { ...data.user, ...data } as AuthenticatedUser };
71-
} catch {
79+
return { success: true, user: { ...data.user, ...data } };
80+
} catch (error) {
81+
// Handle admin privilege errors specifically
82+
if (error && typeof error === 'object' && 'response' in error) {
83+
const response = (error as { response?: { status?: number; data?: { detail?: string } } })
84+
.response;
85+
if (response?.status === 403 && isAdminPortal) {
86+
return {
87+
success: false,
88+
error:
89+
'Access denied. You do not have admin privileges. Please contact an administrator.',
90+
errorCode: 'auth/insufficient-privileges',
91+
};
92+
}
93+
}
94+
7295
// Backend login failure is not critical since Firebase auth succeeded
7396
return {
7497
success: true,
@@ -217,22 +240,19 @@ export const register = async ({
217240
} else {
218241
console.warn('[REGISTER] Failed to send email verification after registration');
219242
}
243+
244+
// Return success with user info - don't try to login since email isn't verified yet
245+
return {
246+
success: true,
247+
user: { email: user.email, uid: user.uid } as unknown as AuthenticatedUser,
248+
};
220249
} catch (firebaseError) {
221250
console.error('[REGISTER] Firebase sign-in failed:', firebaseError);
222251
// Continue with registration even if Firebase sign-in fails
223252
// The user can still verify their email later
224-
}
225-
226-
// Try backend login but don't fail if it doesn't work
227-
try {
228-
const loginResult = await login(email, password);
229-
return loginResult;
230-
} catch (loginError) {
231-
console.warn('[REGISTER] Backend login failed, but registration was successful:', loginError);
232-
// Return success even if backend login fails, since Firebase user was created
233253
return {
234254
success: true,
235-
user: { email, uid: auth.currentUser?.uid || 'unknown' } as unknown as AuthenticatedUser,
255+
user: { email, uid: 'unknown' } as unknown as AuthenticatedUser,
236256
};
237257
}
238258
} catch (error) {

frontend/src/pages/admin-login.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,12 @@ export default function AdminLogin() {
2323

2424
try {
2525
const result = await login(email, password);
26-
if (result) {
27-
console.log('Admin login success:', result);
26+
if (result.success) {
2827
router.push('/admin/dashboard');
28+
} else if (result.errorCode === 'auth/email-not-verified') {
29+
router.push(`/admin-verify?email=${encodeURIComponent(email)}&role=admin`);
2930
} else {
30-
setError('Invalid email or password');
31+
setError('Invalid email or password. Please check your credentials and try again.');
3132
}
3233
} catch (err: unknown) {
3334
console.error('Admin login error:', err);
@@ -203,7 +204,7 @@ export default function AdminLogin() {
203204
>
204205
Don&apos;t have an account?{' '}
205206
<Link
206-
href="/participant-form"
207+
href="/admin-signup"
207208
style={{
208209
color: teal,
209210
textDecoration: 'underline',
Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -38,25 +38,25 @@ export default function AdminLoginPage() {
3838
signupMethod: SignUpMethod.PASSWORD,
3939
};
4040
const result = await register(userData);
41-
console.log('Admin registration success:', result);
42-
router.push(`/verify?email=${encodeURIComponent(email)}&role=admin`);
43-
} catch (err: unknown) {
44-
console.error('Admin registration error:', err);
45-
if (
46-
err &&
47-
typeof err === 'object' &&
48-
'response' in err &&
49-
err.response &&
50-
typeof err.response === 'object' &&
51-
'data' in err.response &&
52-
err.response.data &&
53-
typeof err.response.data === 'object' &&
54-
'detail' in err.response.data
55-
) {
56-
setError((err.response.data as { detail: string }).detail || 'Registration failed');
41+
console.log('?', result);
42+
// Check if it's an admin privilege error
43+
if (!result.success && result.error && result.error.includes('Admin privileges required')) {
44+
setError(
45+
'Access denied. Admin registration is restricted. Please contact an administrator.',
46+
);
47+
return;
48+
}
49+
50+
// If successful (even if success is false, check if we got a user)
51+
if (result.user || result.success) {
52+
console.log('Admin registration success:', result);
53+
router.push(`/admin-verify?email=${encodeURIComponent(email)}&role=admin`);
5754
} else {
58-
setError('Registration failed');
55+
setError(result.error || 'Registration failed');
5956
}
57+
} catch (err: unknown) {
58+
console.error('Admin registration error:', err);
59+
setError('Registration failed');
6060
}
6161
};
6262

@@ -247,7 +247,7 @@ export default function AdminLoginPage() {
247247
>
248248
Already have an account?{' '}
249249
<Link
250-
href="/admin"
250+
href="/admin-login"
251251
style={{
252252
color: teal,
253253
textDecoration: 'underline',
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
import { useRouter } from 'next/router';
2+
import { Box, Flex, Heading, Text, Link } from '@chakra-ui/react';
3+
import { useEffect, useState } from 'react';
4+
import { useEmailVerification } from '@/hooks/useEmailVerification';
5+
import {
6+
getEmailForSignIn,
7+
clearEmailForSignIn,
8+
setEmailForSignIn,
9+
} from '@/services/firebaseAuthService';
10+
import { auth } from '@/config/firebase';
11+
import Image from 'next/image';
12+
13+
const veniceBlue = '#1d3448';
14+
15+
export default function AdminVerifyPage() {
16+
const router = useRouter();
17+
const { email, role } = router.query;
18+
const [displayEmail, setDisplayEmail] = useState<string>('');
19+
const [message, setMessage] = useState<{ type: 'success' | 'error' | null; text: string }>({
20+
type: null,
21+
text: '',
22+
});
23+
24+
const { sendVerificationEmail, sendSignInLink, isLoading, error, success } =
25+
useEmailVerification();
26+
27+
useEffect(() => {
28+
// Get email from query params or localStorage
29+
const emailFromQuery = email as string;
30+
const emailFromStorage = getEmailForSignIn();
31+
const finalEmail = emailFromQuery || emailFromStorage || '[email protected]';
32+
setDisplayEmail(finalEmail);
33+
34+
// Store the email from query params if available
35+
if (emailFromQuery) {
36+
setEmailForSignIn(emailFromQuery);
37+
}
38+
}, [email]);
39+
40+
useEffect(() => {
41+
if (success) {
42+
setMessage({ type: 'success', text: 'Email sent successfully! Please check your inbox.' });
43+
}
44+
}, [success]);
45+
46+
useEffect(() => {
47+
if (error) {
48+
setMessage({ type: 'error', text: error });
49+
}
50+
}, [error]);
51+
52+
const handleResendEmail = async () => {
53+
setMessage({ type: null, text: '' });
54+
55+
// For admin users, send verification email instead of sign-in link
56+
await sendVerificationEmail();
57+
};
58+
59+
const handleBackToLogin = () => {
60+
clearEmailForSignIn();
61+
router.push('/admin-login');
62+
};
63+
64+
return (
65+
<Flex minH="100vh" direction={{ base: 'column', md: 'row' }}>
66+
{/* Left: Admin Verification Content */}
67+
<Flex
68+
flex="1"
69+
align="center"
70+
justify="center"
71+
px={{ base: 4, md: 12 }}
72+
py={{ base: 16, md: 0 }}
73+
bg="white"
74+
minH={{ base: '60vh', md: '100vh' }}
75+
>
76+
<Box w="full" maxW="520px">
77+
<Heading
78+
as="h1"
79+
fontFamily="'Open Sans', sans-serif"
80+
fontWeight={600}
81+
color={veniceBlue}
82+
fontSize={{ base: '3xl', md: '4xl', lg: '5xl' }}
83+
lineHeight="50px"
84+
mb={2}
85+
>
86+
Admin Portal - First Connection Peer Support Program
87+
</Heading>
88+
<Heading
89+
as="h2"
90+
fontFamily="'Open Sans', sans-serif"
91+
fontWeight={600}
92+
color={veniceBlue}
93+
fontSize={{ base: 'xl', md: '2xl' }}
94+
mb={6}
95+
mt={8}
96+
>
97+
Verify Your Admin Account
98+
</Heading>
99+
<Text
100+
mb={8}
101+
color={veniceBlue}
102+
fontFamily="'Open Sans', sans-serif"
103+
fontWeight={400}
104+
fontSize="lg"
105+
>
106+
We sent a confirmation link to <b>{displayEmail}</b>
107+
</Text>
108+
109+
{message.type === 'success' && (
110+
<Text
111+
mb={4}
112+
color="green.600"
113+
fontFamily="'Open Sans', sans-serif"
114+
fontWeight={600}
115+
fontSize="md"
116+
>
117+
{message.text}
118+
</Text>
119+
)}
120+
121+
{message.type === 'error' && (
122+
<Text
123+
mb={4}
124+
color="red.600"
125+
fontFamily="'Open Sans', sans-serif"
126+
fontWeight={600}
127+
fontSize="md"
128+
>
129+
{message.text}
130+
</Text>
131+
)}
132+
133+
<Text
134+
color={veniceBlue}
135+
fontFamily="'Open Sans', sans-serif"
136+
fontWeight={400}
137+
fontSize="md"
138+
mb={4}
139+
>
140+
Didn&apos;t get a link?{' '}
141+
<Link
142+
onClick={handleResendEmail}
143+
style={{
144+
color: '#056067',
145+
textDecoration: 'underline',
146+
fontWeight: 600,
147+
fontFamily: 'Open Sans, sans-serif',
148+
cursor: 'pointer',
149+
}}
150+
>
151+
Click here to resend.
152+
</Link>
153+
</Text>
154+
155+
<Text
156+
color={veniceBlue}
157+
fontFamily="'Open Sans', sans-serif"
158+
fontWeight={400}
159+
fontSize="md"
160+
>
161+
Remember your password?{' '}
162+
<Link
163+
href="/admin-login"
164+
style={{
165+
color: '#056067',
166+
textDecoration: 'underline',
167+
fontWeight: 600,
168+
fontFamily: 'Open Sans, sans-serif',
169+
}}
170+
>
171+
Back to login
172+
</Link>
173+
</Text>
174+
</Box>
175+
</Flex>
176+
{/* Right: Image - Using admin.png from admin-login.tsx */}
177+
<Box flex="1" display={{ base: 'none', md: 'block' }} position="relative" minH="100vh">
178+
<Image
179+
src="/admin.png"
180+
alt="Admin Portal Visual"
181+
fill
182+
sizes="(max-width: 768px) 100vw, 50vw"
183+
style={{ objectFit: 'cover', objectPosition: '90% 50%' }}
184+
priority
185+
/>
186+
</Box>
187+
</Flex>
188+
);
189+
}

0 commit comments

Comments
 (0)