Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/authentication frontend #64

Merged
merged 54 commits into from
Jun 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
b630407
Fix import error
XanderVertegaal Apr 26, 2024
a092da6
Refactor auth service
XanderVertegaal Apr 26, 2024
9977613
Early out
XanderVertegaal Apr 26, 2024
1437df3
Registration form
XanderVertegaal Apr 27, 2024
af5580a
Tweak register form
XanderVertegaal Apr 28, 2024
e6b81ca
Login component; extract utils
XanderVertegaal Apr 28, 2024
fcd78f0
Minor cleanup
XanderVertegaal May 2, 2024
bd98611
Updated verify email
XanderVertegaal May 2, 2024
ec7c9b5
Password forgotten page WIP
XanderVertegaal May 2, 2024
e3b579a
Password forgotten component
XanderVertegaal May 2, 2024
ca42ab0
Unified error interface
XanderVertegaal May 2, 2024
8a6fe42
Added password forgotten route
XanderVertegaal May 2, 2024
6db917f
Reset password component
XanderVertegaal May 3, 2024
b726f93
Generalize subject naming
XanderVertegaal May 3, 2024
570944d
identicalPasswordsValidator needs arguments
XanderVertegaal May 3, 2024
77fb3e9
Generalize styling rules
XanderVertegaal May 3, 2024
49d46fa
Central error message mapping
XanderVertegaal May 3, 2024
836a6cf
Merge user streams
XanderVertegaal May 3, 2024
b7d3e6f
Update user-menu
XanderVertegaal May 3, 2024
4efc9d6
Added user-settings form
XanderVertegaal May 3, 2024
33df81b
Cleanup
XanderVertegaal May 3, 2024
2458b79
Password reset request from user settings form
XanderVertegaal May 3, 2024
40e97c9
Loading spinner in LoginComponent
XanderVertegaal May 3, 2024
c538653
Merge error messages
XanderVertegaal May 3, 2024
d8824d9
Loading spinners
XanderVertegaal May 4, 2024
d299490
Delete user frontend
XanderVertegaal May 4, 2024
3d25b7d
Rudimentary deletion backend
XanderVertegaal May 4, 2024
38151a1
Added ESLint
XanderVertegaal May 4, 2024
685aa09
Linting changes
XanderVertegaal May 4, 2024
45b32ad
merge feature/eslint
XanderVertegaal May 4, 2024
4d31818
Linting checks
XanderVertegaal May 4, 2024
0461b5d
Toasts
XanderVertegaal May 4, 2024
64afba1
Add toasters
XanderVertegaal May 4, 2024
214a3d8
Simplify keyInfo calls
XanderVertegaal May 4, 2024
6ab145a
Add throttleTime
XanderVertegaal May 4, 2024
35edac0
Styling changes
XanderVertegaal May 4, 2024
fb4bbde
Fix login component tests
XanderVertegaal May 5, 2024
0aca792
Password forgotten tests
XanderVertegaal May 5, 2024
db3999e
ToastService tests
XanderVertegaal May 5, 2024
0a3a16d
Add user utils tests
XanderVertegaal May 5, 2024
1c6cc18
Formatting
XanderVertegaal May 5, 2024
1041a0e
Register component tests
XanderVertegaal May 5, 2024
2fb0d03
Reset password unit tests
XanderVertegaal May 5, 2024
b01b376
UserSettings tests
XanderVertegaal May 5, 2024
36e190c
VerifyEmail tests
XanderVertegaal May 8, 2024
e2f9ea0
Use Bootstrap text-bg classes
XanderVertegaal May 8, 2024
0699ddd
UserMenu tests (WIP)
XanderVertegaal May 14, 2024
cb009ab
Merge branch 'develop' into feature/authentication-frontend
XanderVertegaal May 14, 2024
1a0ea4d
UserMenu tests
XanderVertegaal May 14, 2024
5f92ea3
Use correct headers in template
XanderVertegaal May 14, 2024
adb00d2
Remove initialAuth$ subject
XanderVertegaal May 14, 2024
09891b0
Refactor to general Request class
XanderVertegaal May 15, 2024
6f0f281
Move user settings quirk to authService
XanderVertegaal May 15, 2024
78414d2
Fix tests
XanderVertegaal May 15, 2024
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
8 changes: 7 additions & 1 deletion backend/user/urls.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from django.urls import include, path, re_path
from .views import redirect_confirm, KeyInfoView
from .views import DeleteUser, redirect_confirm, KeyInfoView
from dj_rest_auth.registration.views import VerifyEmailView

from .views import KeyInfoView, redirect_confirm, redirect_reset
Expand All @@ -23,6 +23,12 @@
redirect_reset,
name="password_reset_confirm",
),
# delete user
path(
"delete/",
DeleteUser.as_view(),
name="delete user"
),
# generic routes (login, logout, pw reset etc.)
path("", include("dj_rest_auth.urls")),
path("registration/", include("dj_rest_auth.registration.urls")),
Expand Down
6 changes: 5 additions & 1 deletion backend/user/views.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from allauth.account.models import EmailConfirmationHMAC
from django.http import HttpResponseRedirect
from django.http import HttpRequest, HttpResponseRedirect
from rest_framework.exceptions import APIException
from rest_framework.permissions import AllowAny
from rest_framework.response import Response
Expand Down Expand Up @@ -43,6 +43,10 @@ def post(self, request):
except Exception as e:
raise APIException(e)

class DeleteUser(APIView):
def delete(self, request: HttpRequest):
XanderVertegaal marked this conversation as resolved.
Show resolved Hide resolved
user = request.user
user.delete()

class UserViewSet(ModelViewSet):
permission_classes = [IsAuthenticated]
Expand Down
1 change: 1 addition & 0 deletions frontend/src/app/app.component.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<div class="app">
<lc-menu role="banner"></lc-menu>
<lc-toast-container aria-live="polite" aria-atomic="true" />
<main class="main-content mt-5 mb-5">
<div class="container">
<router-outlet></router-outlet>
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/app/app.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@ import { Component } from '@angular/core';
styleUrls: ['./app.component.scss']
})
export class AppComponent {
title = 'lettercraft';
title = 'lettercraft';
}
3 changes: 3 additions & 0 deletions frontend/src/app/core/core.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { SharedModule } from '../shared/shared.module';
import { FooterComponent } from './footer/footer.component';
import { MenuComponent } from './menu/menu.component';
import { UserMenuComponent } from './menu/user-menu/user-menu.component';
import { ToastContainerComponent } from './toast-container/toast-container.component';



Expand All @@ -11,13 +12,15 @@ import { UserMenuComponent } from './menu/user-menu/user-menu.component';
FooterComponent,
MenuComponent,
UserMenuComponent,
ToastContainerComponent,
],
imports: [
SharedModule
],
exports: [
FooterComponent,
MenuComponent,
ToastContainerComponent
]
})
export class CoreModule { }
17 changes: 13 additions & 4 deletions frontend/src/app/core/menu/user-menu/user-menu.component.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<ul class="navbar-nav">
<li class="navbar-text" *ngIf="isLoading$ | async">
<li class="navbar-text" *ngIf="authLoading$ | async">
<div class="spinner-border" role="status">
<span class="visually-hidden">Loading user data...</span>
</div>
Expand All @@ -12,9 +12,11 @@
{{user.username}}
</button>
<div ngbDropdownMenu aria-labelledby="userDropdown" class="dropdown-menu dropdown-menu-end">
<a ngbDropdownItem [routerLink]="['/settings']" hidden>Settings</a> <!-- hide settings link until this is implemented -->
<a ngbDropdownItem [routerLink]="['/user-settings']">Settings</a>
<a *ngIf="user.isStaff" ngbDropdownItem href="/admin/">Administration</a>
<button ngbDropdownItem (click)="logout()">Sign out</button>
<button ngbDropdownItem (click)="logout()">
Sign out
</button>
</div>
</li>
</ng-container>
Expand All @@ -24,11 +26,18 @@
Sign in
</a>
</li>
<li class="nav-item" hidden> <!-- hide registration link until this is implemented -->
<li class="nav-item">
<a [routerLink]="['/register']" routerLinkActive="active" class="nav-link">
Register
</a>
</li>
</ng-container>
<ng-container *ngIf="logoutLoading$ | async">
<li class="d-flex align-items-center">
<div class="spinner-border spinner-border-sm text-light" role="status">
<span class="visually-hidden">Signing out...</span>
</div>
</li>
</ng-container>
</ul>

60 changes: 48 additions & 12 deletions frontend/src/app/core/menu/user-menu/user-menu.component.spec.ts
Original file line number Diff line number Diff line change
@@ -1,56 +1,92 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';

import { UserMenuComponent } from './user-menu.component';
import { AuthService } from '@services/auth.service';
import { AuthServiceMock, testUser } from '@mock/auth.service.mock';
import { SharedTestingModule } from '@shared/shared-testing.module';
import { HttpTestingController } from '@angular/common/http/testing';
import { UserResponse } from 'src/app/user/models/user';

const fakeUserResponse: UserResponse = {
id: 1,
username: "frodo",
email: "[email protected]",
first_name: "Frodo",
last_name: "Baggins",
is_staff: false
}

const fakeAdminResponse: UserResponse = {
id: 1,
username: "gandalf",
email: "[email protected]",
first_name: "Gandalf",
last_name: "The Grey",
is_staff: true
}


describe('UserMenuComponent', () => {
let component: UserMenuComponent;
let fixture: ComponentFixture<UserMenuComponent>;
let authService: AuthServiceMock;
let httpTestingController: HttpTestingController;

const spinner = () => fixture.debugElement.query(By.css('.spinner-border'));
const signInLink = () => fixture.debugElement.query(By.css('a[href="/login"]'));
const userDropdownTrigger = () => fixture.debugElement.query(By.css('#userDropdown'))

beforeEach(() => {
TestBed.configureTestingModule({
providers: [
{ provide: AuthService, useClass: AuthServiceMock }
],
declarations: [UserMenuComponent],
providers: [AuthService],
imports: [SharedTestingModule],
});
authService = TestBed.inject(AuthService) as any as AuthServiceMock;
httpTestingController = TestBed.inject(HttpTestingController);
fixture = TestBed.createComponent(UserMenuComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

afterEach(() => {
httpTestingController.verify();
});

it('should create', () => {
httpTestingController.expectOne('/users/user/');
expect(component).toBeTruthy();
});

it('should show sign-in when not logged in', () => {
const req = httpTestingController.expectOne('/users/user/');
req.flush(null);
fixture.detectChanges();

expect(spinner()).toBeFalsy();
expect(signInLink()).toBeTruthy();
expect(userDropdownTrigger()).toBeFalsy();
});

it('should show a loading spinner', () => {
httpTestingController.expectOne('/users/user/');
expect(spinner()).toBeTruthy();
expect(signInLink()).toBeFalsy();
expect(userDropdownTrigger()).toBeFalsy();
});

it('should show sign-in when not logged in', () => {
authService._setUser(null);
it('should show an admin menu when user is a staff member', () => {
const req = httpTestingController.expectOne('/users/user/');
req.flush(fakeAdminResponse);
fixture.detectChanges();

expect(spinner()).toBeFalsy();
expect(signInLink()).toBeTruthy();
expect(userDropdownTrigger()).toBeFalsy();
expect(signInLink()).toBeFalsy();
expect(userDropdownTrigger()).toBeTruthy();
expect(userDropdownTrigger().nativeElement.textContent).toContain('gandalf');
});


it('should show a user menu when logged in', () => {
authService._setUser(testUser({}));
const req = httpTestingController.expectOne('/users/user/');
req.flush(fakeUserResponse);
fixture.detectChanges();

expect(spinner()).toBeFalsy();
Expand Down
66 changes: 48 additions & 18 deletions frontend/src/app/core/menu/user-menu/user-menu.component.ts
Original file line number Diff line number Diff line change
@@ -1,36 +1,66 @@
import { Component } from '@angular/core';
import { Component, DestroyRef, OnInit } from '@angular/core';
import { faUser } from '@fortawesome/free-solid-svg-icons';
import { Observable, map } from 'rxjs';
import { User } from '../../../user/models/user';
import { map } from 'rxjs';
import { AuthService } from '@services/auth.service';
import _ from 'underscore';
import { ToastService } from '@services/toast.service';
import { Router } from '@angular/router';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';

@Component({
selector: 'lc-user-menu',
templateUrl: './user-menu.component.html',
styleUrls: ['./user-menu.component.scss']
})
export class UserMenuComponent {
isLoading$: Observable<boolean>;
user$: Observable<User | null | undefined>;
showSignIn$: Observable<boolean>;
export class UserMenuComponent implements OnInit {
public authLoading$ = this.authService.currentUser$.pipe(
map(_.isUndefined)
);

icons = {
public user$ = this.authService.currentUser$;

public showSignIn$ = this.authService.currentUser$.pipe(
map(_.isNull)
);

public icons = {
user: faUser,
};

constructor(private authService: AuthService) {
this.isLoading$ = this.authService.currentUser$.pipe(
map(_.isUndefined)
);
this.user$ = this.authService.currentUser$;
this.showSignIn$ = this.authService.currentUser$.pipe(
map(_.isNull)
);
public logoutLoading$ = this.authService.logout.loading$;

constructor(
private authService: AuthService,
private toastService: ToastService,
private router: Router,
private destroyRef: DestroyRef
) { }

ngOnInit(): void {
this.authService.logout.error$
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(() => {
this.toastService.show({
header: 'Sign out failed',
body: 'There was an error signing you out. Please try again.',
type: 'danger'
});
});

this.authService.logout.success$
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(() => {
this.toastService.show({
header: 'Sign out successful',
body: 'You have been successfully signed out.',
type: 'success'
});
this.router.navigate(['/']);
});
}

logout() {
this.authService.logout(false);
public logout(): void {
this.authService.logout.subject.next();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<ngb-toast
*ngFor="let toast of toastService.toasts"
class="mt-2"
[class]="toast.className"
[header]="toast.header"
[autohide]="true"
[delay]="toast.delay"
(hidden)="toastService.remove(toast)"
>
{{ toast.body }}
</ngb-toast>
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
:host {
position: fixed;
bottom: 0;
right: 0;
margin: 0.5em;
z-index: 1200;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';

import { ToastContainerComponent } from './toast-container.component';

describe('ToastContainerComponent', () => {
let component: ToastContainerComponent;
let fixture: ComponentFixture<ToastContainerComponent>;

beforeEach(() => {
TestBed.configureTestingModule({
declarations: [ToastContainerComponent]
});
fixture = TestBed.createComponent(ToastContainerComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

it('should create', () => {
expect(component).toBeTruthy();
});
});
11 changes: 11 additions & 0 deletions frontend/src/app/core/toast-container/toast-container.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Component } from '@angular/core';
import { ToastService } from '@services/toast.service';

@Component({
selector: 'lc-toast-container',
templateUrl: './toast-container.component.html',
styleUrls: ['./toast-container.component.scss']
})
export class ToastContainerComponent {
constructor(public toastService: ToastService) { }
}
20 changes: 20 additions & 0 deletions frontend/src/app/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ import { Routes } from '@angular/router';
import { HomeComponent } from './home/home.component';
import { LoginComponent } from './user/login/login.component';
import { VerifyEmailComponent } from './user/verify-email/verify-email.component';
import { RegisterComponent } from './user/register/register.component';
import { PasswordForgottenComponent } from './user/password-forgotten/password-forgotten.component';
import { ResetPasswordComponent } from './user/reset-password/reset-password.component';
import { UserSettingsComponent } from './user/user-settings/user-settings.component';

const routes: Routes = [
{
Expand All @@ -13,10 +17,26 @@ const routes: Routes = [
path: 'login',
component: LoginComponent,
},
{
path: 'register',
component: RegisterComponent,
},
{
path: 'confirm-email/:key',
component: VerifyEmailComponent,
},
{
path: 'password-forgotten',
component: PasswordForgottenComponent
},
{
path: 'reset-password/:uid/:token',
component: ResetPasswordComponent
},
{
path: 'user-settings',
component: UserSettingsComponent
},
{
path: '',
redirectTo: '/home',
Expand Down
Loading
Loading