Skip to content

Commit 43b71e4

Browse files
committed
fix(ui,backend): migrate to history-mode routing and harden auth flows
1 parent 5133b95 commit 43b71e4

16 files changed

Lines changed: 397 additions & 143 deletions

File tree

backend/mail/models.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ def mimetype(self) -> str:
6060
class ResetPasswordMail(TemplatedMail):
6161
def __init__(self, user) -> None:
6262
self.user = user
63-
reset_url = f"{settings.FRONTEND_URL}/#reset-password?token={user.password_reset_token}"
63+
reset_url = f"{settings.FRONTEND_URL}/reset-password?token={user.password_reset_token}"
6464
params = {
6565
"user": user,
6666
"reset_url": reset_url,
@@ -78,7 +78,7 @@ def dispatch(self):
7878
class ConfirmationMail(TemplatedMail):
7979
def __init__(self, user) -> None:
8080
self.user = user
81-
confirm_url = f"{settings.FRONTEND_URL}/#confirm-email?token={user.email_confirmation_token}"
81+
confirm_url = f"{settings.FRONTEND_URL}/confirm-email?token={user.email_confirmation_token}"
8282
params = {
8383
"user": user,
8484
"confirmation_url": confirm_url,
@@ -136,7 +136,7 @@ def dispatch(self):
136136
class NewUserNotificationMail(TemplatedMail):
137137
def __init__(self, user, new_user) -> None:
138138
self.user = user
139-
activate_url = f"{settings.FRONTEND_URL}/#account/{new_user.id}"
139+
activate_url = f"{settings.FRONTEND_URL}/account/{new_user.id}"
140140
params = {
141141
"admin": user,
142142
"new_user": new_user,
@@ -159,7 +159,7 @@ def __init__(self, user, published_report) -> None:
159159
params = {
160160
"user": user,
161161
"report": published_report,
162-
"report_location": f"{settings.FRONTEND_URL}/#reports/{published_report.id}",
162+
"report_location": f"{settings.FRONTEND_URL}/reports/{published_report.id}",
163163
}
164164
super().__init__(
165165
subject="CRADLE - Your Report is Ready",
@@ -198,7 +198,7 @@ def __init__(self, user, enrichment_request) -> None:
198198
params = {
199199
"user": user,
200200
"enrichment": enrichment_request,
201-
"enrichment_location": f"{settings.FRONTEND_URL}/#enrichments/{enrichment_request.id}",
201+
"enrichment_location": f"{settings.FRONTEND_URL}/enrichments/{enrichment_request.id}",
202202
}
203203
super().__init__(
204204
subject="CRADLE - Your Enrichment is Complete",

backend/user/models.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ def send_email_confirmation(self):
120120

121121
# Generate a token and set its expiration
122122
self.email_confirmation_token = uuid.uuid4().hex
123-
self.email_confirmation_token_expiry = datetime.now() + timedelta(hours=24)
123+
self.email_confirmation_token_expiry = timezone.now() + timedelta(hours=24)
124124
self.save(
125125
update_fields=[
126126
"email_confirmation_token",
@@ -135,7 +135,7 @@ def send_password_reset(self):
135135
"""Send a password reset email to the user."""
136136
# Generate a token and set its expiration
137137
self.password_reset_token = uuid.uuid4().hex
138-
self.password_reset_token_expiry = datetime.now() + timedelta(hours=1)
138+
self.password_reset_token_expiry = timezone.now() + timedelta(hours=1)
139139
self.save(update_fields=["password_reset_token", "password_reset_token_expiry"])
140140

141141
mail = ResetPasswordMail(self)
@@ -341,7 +341,7 @@ def __str__(self):
341341

342342
def is_expired(self):
343343
"""Check if the session has expired."""
344-
return datetime.now() > self.expires_at
344+
return timezone.now() > self.expires_at
345345

346346

347347
class BlacklistedToken(models.Model):

backend/user/serializers.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -281,18 +281,18 @@ class EmailConfirmSerializer(serializers.Serializer):
281281
def validate(self, data):
282282
token = data["token"]
283283

284+
if not token:
285+
raise ValidationError("We had trouble confirming with this token.")
286+
284287
try:
285288
self.user = CradleUser.objects.get(email_confirmation_token=token)
286-
except CradleUser.DoesNotExist:
289+
except (CradleUser.DoesNotExist, CradleUser.MultipleObjectsReturned):
287290
raise ValidationError("We had trouble confirming with this token.")
288291

289292
if self.user.email_confirmed:
290293
raise ValidationError("We had trouble confirming with this token.")
291294

292-
if not token or self.user.email_confirmation_token != token:
293-
raise ValidationError("We had trouble confirming with this token.")
294-
295-
return True
295+
return data
296296

297297

298298
class Enable2FASerializer(serializers.Serializer):

backend/user/views/user_view.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -545,7 +545,8 @@ def post(self, request):
545545
raise ValidationException(detail="Email confirmation token has expired a new one was sent.")
546546

547547
user.email_confirmed = True
548-
user.email_confirmation_token = ""
548+
user.email_confirmation_token = None
549+
user.email_confirmation_token_expiry = None
549550
user.save()
550551

551552
return Response({"message": "Email confirmed."}, status=status.HTTP_200_OK)

ui/src/components/base/loading/loading.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Spinner } from '@/components/ui/spinner';
22
import React from 'react';
3-
import Logo from '../Logo/Logo';
3+
import Logo from '../logo/logo';
44

55
interface LoadingProps {
66
logo?: boolean;

ui/src/components/domain/admin/user/add-user-form.tsx

Lines changed: 47 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,12 @@ import {
77
FieldGroup,
88
FieldLabel,
99
} from '@/components/ui/field';
10-
import { Input } from '@/components/ui/input';
11-
import { InputGroup, InputGroupInput } from '@/components/ui/input-group';
10+
import {
11+
InputGroup,
12+
InputGroupAddon,
13+
InputGroupButton,
14+
InputGroupInput,
15+
} from '@/components/ui/input-group';
1216
import {
1317
Select,
1418
SelectContent,
@@ -19,9 +23,11 @@ import {
1923
import { Switch } from '@/components/ui/switch';
2024
import useApi from '@/hooks/api/use-api';
2125
import { zodResolver } from '@hookform/resolvers/zod';
26+
import { EyeIcon, EyeSlashIcon } from '@phosphor-icons/react';
2227
import { UserRetrieve } from '@services/cradle/models';
2328
import { useMutation } from '@tanstack/react-query';
2429
import bytes from 'bytes';
30+
import { useState } from 'react';
2531
import { Controller, useForm } from 'react-hook-form';
2632
import { z } from 'zod';
2733

@@ -61,6 +67,7 @@ const addUserSchema = z.object({
6167
type FormData = z.infer<typeof addUserSchema>;
6268

6369
export default function AddUserForm({ onAdd }: AddUserFormProps) {
70+
const [showPassword, setShowPassword] = useState(false);
6471
const { usersApi } = useApi();
6572

6673
const {
@@ -168,14 +175,44 @@ export default function AddUserForm({ onAdd }: AddUserFormProps) {
168175
Password
169176
<span className='text-destructive ml-1'>*</span>
170177
</FieldLabel>
171-
<Input
172-
id='password'
173-
type='password'
174-
placeholder='Password'
175-
{...register('password')}
176-
aria-invalid={Boolean(errors.password)}
177-
required
178-
/>
178+
<InputGroup>
179+
<InputGroupInput
180+
id='password'
181+
type={showPassword ? 'text' : 'password'}
182+
placeholder='Password'
183+
{...register('password')}
184+
aria-invalid={Boolean(errors.password)}
185+
required
186+
/>
187+
<InputGroupAddon align='inline-end'>
188+
<InputGroupButton
189+
type='button'
190+
onClick={() => setShowPassword(!showPassword)}
191+
aria-label={
192+
showPassword
193+
? 'Hide password'
194+
: 'Show password'
195+
}
196+
title={
197+
showPassword
198+
? 'Hide password'
199+
: 'Show password'
200+
}
201+
>
202+
{showPassword ? (
203+
<EyeSlashIcon
204+
className='size-4'
205+
weight='bold'
206+
/>
207+
) : (
208+
<EyeIcon
209+
className='size-4'
210+
weight='bold'
211+
/>
212+
)}
213+
</InputGroupButton>
214+
</InputGroupAddon>
215+
</InputGroup>
179216
<FieldDescription>
180217
Minimum 8 characters recommended
181218
</FieldDescription>

ui/src/components/domain/auth/confirm-email.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import useApi from '@/hooks/api/use-api';
22
import { useMutation } from '@tanstack/react-query';
33
import { Link, useSearch } from '@tanstack/react-router';
4-
import { useEffect } from 'react';
4+
import { useEffect, useRef } from 'react';
55
import { toast } from 'sonner';
66

77
/**
@@ -21,11 +21,15 @@ export default function ConfirmEmail() {
2121
onSuccess: () => toast.success('Email confirmed successfully.'),
2222
});
2323

24+
const calledRef = useRef(false);
25+
2426
useEffect(() => {
27+
if (calledRef.current) return;
2528
if (!token) {
2629
toast.error('No confirmation token provided.');
2730
return;
2831
}
32+
calledRef.current = true;
2933
confirmEmail(token);
3034
}, [token, confirmEmail]);
3135

ui/src/components/domain/auth/login.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -527,7 +527,7 @@ export default function Login() {
527527
showAtmosphere={true}
528528
autoRotate={true}
529529
autoRotateSpeed={0.5}
530-
initialView={{ lat: 20, lng: 0, altitude: 1.8 }}
530+
initialView={{ lat: 20, lng: 0, altitude: 3.5 }}
531531
viewOffsetX={120}
532532
/>
533533
</Suspense>

0 commit comments

Comments
 (0)