Skip to content

Commit a5a1841

Browse files
Merge branch 'feature/pw-reset' into develop
2 parents 199a425 + dcdabee commit a5a1841

18 files changed

+532
-26
lines changed
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
from django.core import mail
2+
import random
3+
import re
4+
import string
5+
6+
from django.core.cache import cache
7+
from django.urls import reverse
8+
from allauth.account.models import EmailAddress
9+
from rest_framework import status
10+
11+
12+
def test_register_verification(client, db, django_user_model):
13+
creds = {
14+
'username': 'ThomasCromwell',
15+
'password1': 'annaregina',
16+
'password2': 'annaregina',
17+
'email': 'thomas@wolfhall.com'
18+
}
19+
response = client.post('/rest-auth/registration/', creds)
20+
assert response
21+
22+
# Check if user was registered
23+
db_user = django_user_model.objects.get(
24+
username=creds['username'])
25+
assert db_user
26+
27+
# Check if registration mail was sent
28+
box = mail.outbox
29+
assert len(box) == 1
30+
verify_mail = box[0]
31+
assert verify_mail.to[0] == creds['email']
32+
33+
key = re.search(r'confirm-email\/(.+)\/',
34+
verify_mail.body).group(1)
35+
assert key
36+
37+
# Check key information
38+
key_info = client.get(f'/rest-auth/infofromkey/{key}/')
39+
assert key_info.status_code == 200
40+
assert key_info.json().get('username') == creds.get('username')
41+
42+
# Check verification
43+
verify_response = client.post(
44+
'/rest-auth/registration/verify-email/', {'key': key})
45+
assert verify_response.status_code == 200
46+
47+
# Check if email address was verified
48+
allauth_email = EmailAddress.objects.get(user=db_user)
49+
assert allauth_email.email == creds.get('email')
50+
assert allauth_email.verified is True
51+
assert allauth_email.primary is True
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import re
2+
3+
from django.core import mail
4+
5+
6+
def test_password_reset(db, auth_user, auth_client):
7+
request_res = auth_client.post(
8+
'/rest-auth/password/reset/', {'email': auth_user.email})
9+
assert request_res.status_code == 200
10+
11+
# Check if email was sent
12+
reset_mail = mail.outbox[0]
13+
assert reset_mail
14+
15+
reset_url = re.search(r'password-reset\/(.+)\/(.+)\/',
16+
reset_mail.body)
17+
uid = reset_url.group(1)
18+
token = reset_url.group(2)
19+
20+
# Confirm the reset
21+
new_pass = 'new_password'
22+
reset_body = {
23+
'uid': uid,
24+
'token': token,
25+
'new_password1': new_pass,
26+
'new_password2': new_pass
27+
}
28+
reset_res = auth_client.post(
29+
'/rest-auth/password/reset/confirm/',
30+
reset_body
31+
)
32+
assert reset_res.status_code == 200
33+
34+
# Login with the new password
35+
assert auth_client.post(
36+
'/rest-auth/login/', {'username': auth_user.username, 'password': new_pass}
37+
).status_code == 200
38+
39+
assert True

backend/authentication/urls.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
from django.urls import include, path
22

3-
from .views import info_from_confirm_key, has_admin_access
3+
from .views import info_from_confirm_key, has_admin_access, redirect_reset
44

55
urlpatterns = [
66
path('infofromkey/<key>/', info_from_confirm_key),
77
path('', include('rest_auth.urls')),
8+
# password reset
9+
path('password-reset/<uidb64>/<token>/',
10+
redirect_reset, name='password_reset_confirm'),
811
path('registration/', include('rest_auth.registration.urls')),
912
path('has_admin_access/', has_admin_access),
1013
]

backend/authentication/views.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@ def redirect_confirm(request, key):
1212
return HttpResponseRedirect('/confirm-email/{}/'.format(key))
1313

1414

15+
def redirect_reset(request, uidb64, token):
16+
'''Redirects password reset to the frontend'''
17+
return HttpResponseRedirect('/reset-password/{}/{}/'.format(uidb64, token))
18+
19+
1520
def has_admin_access(request):
1621
current_user = request.user
1722
if current_user.is_staff or current_user.is_superuser:

backend/conftest.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from lxml import etree
1616

1717
from parse.parse_utils import create_utterance_objects
18+
from allauth.account.models import EmailAddress
1819

1920

2021
def _get_transcript_filenames(name: str, dir: Optional[str] = None):
@@ -224,3 +225,32 @@ def single_utt_allresults(testfiles_dir):
224225
@pytest.fixture
225226
def all_transcripts(asta_transcripts, tarsp_transcripts):
226227
return Transcript.objects.all()
228+
229+
230+
@pytest.fixture
231+
def user_credentials():
232+
return {'username': 'basic_user',
233+
'password': 'basic_user',
234+
'email': 'basicuser@textcavator.com'}
235+
236+
237+
@pytest.fixture
238+
def auth_user(django_user_model, user_credentials):
239+
user = django_user_model.objects.create_user(
240+
username=user_credentials['username'],
241+
password=user_credentials['password'],
242+
email=user_credentials['email'])
243+
EmailAddress.objects.create(user=user,
244+
email=user.email,
245+
verified=True,
246+
primary=True)
247+
return user
248+
249+
250+
@pytest.fixture
251+
def auth_client(client, auth_user, user_credentials):
252+
client.login(
253+
username=user_credentials['username'],
254+
password=user_credentials['password'])
255+
yield client
256+
client.logout()

frontend/src/app/app-routing.module.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import { TranscriptComponent } from './transcript/transcript.component';
99
import { ProcessComponent } from './process/process.component';
1010
import { UploadComponent } from './upload/upload.component';
1111
import { HomeComponent } from './core/home/home.component';
12+
import { RequestResetComponent } from './auth/reset-password/request-reset.component';
13+
import { ResetPasswordComponent } from './auth/reset-password/reset-password.component';
1214

1315
const routes: Routes = [
1416
{
@@ -48,6 +50,14 @@ const routes: Routes = [
4850
path: 'confirm-email/:key',
4951
component: VerifyComponent,
5052
},
53+
{
54+
path: 'reset',
55+
component: RequestResetComponent,
56+
},
57+
{
58+
path: 'reset-password/:uid/:token',
59+
component: ResetPasswordComponent,
60+
},
5161
{
5262
path: '',
5363
component: HomeComponent,

frontend/src/app/auth/auth.module.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,17 @@ import { LoginComponent } from './login.component';
44
import { RegisterComponent } from './register.component';
55
import { VerifyComponent } from './verify.component';
66
import { SharedModule } from '../shared/shared.module';
7+
import { RequestResetComponent } from './reset-password/request-reset.component';
8+
import { ResetPasswordComponent } from './reset-password//reset-password.component';
79

810
@NgModule({
9-
declarations: [LoginComponent, RegisterComponent, VerifyComponent],
11+
declarations: [
12+
LoginComponent,
13+
RegisterComponent,
14+
VerifyComponent,
15+
RequestResetComponent,
16+
ResetPasswordComponent,
17+
],
1018
imports: [CommonModule, SharedModule],
1119
exports: [LoginComponent, RegisterComponent, VerifyComponent],
1220
})
Lines changed: 46 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
11
<div class="columns is-centered">
22
<div class="column is-5-tablet is-4-desktop is-3-widescreen">
3-
<form (submit)=login() class="box">
3+
<form (submit)="login()" class="box">
44
<div class="field">
55
<label for="" class="label">Username</label>
66
<div class="control has-icons-left">
7-
<input type="username" placeholder="*******" class="input" [(ngModel)]="username" name="username"
8-
required>
7+
<input
8+
type="username"
9+
placeholder="*******"
10+
class="input"
11+
[(ngModel)]="username"
12+
name="username"
13+
required
14+
/>
915
<span class="icon is-small is-left">
1016
<fa-icon [icon]="faUser"></fa-icon>
1117
</span>
@@ -14,23 +20,51 @@
1420
<div class="field">
1521
<label for="" class="label">Password</label>
1622
<div class="control has-icons-left">
17-
<input type="password" placeholder="*******" class="input" [(ngModel)]="password" name="password"
18-
required>
23+
<input
24+
type="password"
25+
placeholder="*******"
26+
class="input"
27+
[(ngModel)]="password"
28+
name="password"
29+
required
30+
/>
1931
<span class="icon is-small is-left">
2032
<fa-icon [icon]="faLock"></fa-icon>
2133
</span>
2234
</div>
2335
</div>
24-
<div *ngIf="errors$| async as errors" class="notification is-danger">
25-
<p *ngFor="let error of errors">{{error}}</p>
36+
<div
37+
*ngIf="errors$ | async as errors"
38+
class="notification is-danger"
39+
>
40+
<p *ngFor="let error of errors">{{ error }}</p>
2641
</div>
2742
<p-messages [(value)]="messages" [closable]="false"></p-messages>
28-
<div class="field is-grouped">
29-
<div class="control">
30-
<button class="button is-primary" [ngClass]=" {'is-loading': processing}"
31-
[disabled]="!username || !password">Login</button>
43+
<div class="level">
44+
<div class="level-left">
45+
<div class="level-item">
46+
<p class="control">
47+
<button
48+
class="button is-primary"
49+
[ngClass]="{ 'is-loading': processing }"
50+
[disabled]="!username || !password"
51+
>
52+
Login
53+
</button>
54+
</p>
55+
</div>
56+
</div>
57+
<div class="level-right">
58+
<div class="level-item">
59+
<a
60+
[routerLinkActive]="'is-active'"
61+
[routerLink]="['/reset']"
62+
>
63+
Forgot password?
64+
</a>
65+
</div>
66+
</div>
3267
</div>
33-
</div>
3468
</form>
3569
</div>
3670
</div>
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<section class="section">
2+
<div class="columns is-centered">
3+
<div>
4+
<form (ngSubmit)="requestReset(requestResetForm)" class="container is-readable" #requestResetForm="ngForm">
5+
<div class="container is-login">
6+
<h1 class="title">Reset password</h1>
7+
<div class="field">
8+
<label class="label" for="email">Email</label>
9+
<div class="control has-icons-left">
10+
<input class="input" type="email" name="email" ngModel #email="ngModel" required email
11+
(input)="disableNotification()" />
12+
<span class="icon is-left">
13+
<fa-icon [icon]="faEnvelope" aria-hidden="true"></fa-icon>
14+
</span>
15+
</div>
16+
<p class="help">Enter the e-mail address with which you registered for SASTA.</p>
17+
<div class="notification is-warning" *ngIf="email.errors?.required && (email.dirty || email.touched)">
18+
Email is required.
19+
</div>
20+
<div class="notification is-warning" class="notification is-warning" *ngIf="email.errors && email.touched">
21+
Please enter a valid email address.
22+
</div>
23+
<div *ngIf="showMessage && message" class="notification" [ngClass]="{'is-danger': !success, 'is-success': success}">{{message}}</div>
24+
</div>
25+
26+
<div class="field">
27+
<p class="control">
28+
<button class="button is-primary" [disabled]="!requestResetForm.form.valid">Request password reset<link rel="stylesheet" href=""></button>
29+
</p>
30+
</div>
31+
</div>
32+
</form>
33+
</div>
34+
</div>
35+
</section>

0 commit comments

Comments
 (0)