Skip to content

Commit

Permalink
error interceptor, token refresh, login, autologin on refresh, regist…
Browse files Browse the repository at this point in the history
…er, error showing for auth errors
  • Loading branch information
seiyria committed Jun 7, 2023
1 parent 82ae40b commit f2bd670
Show file tree
Hide file tree
Showing 9 changed files with 268 additions and 29 deletions.
9 changes: 8 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
{
"eslint.workingDirectories": ["client", "server"]
"eslint.workingDirectories": [
"client",
"server"
],
"[html]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"editor.formatOnSave": true,
}
14 changes: 10 additions & 4 deletions client/src/app/app.module.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { HttpClientModule } from "@angular/common/http";
import { HTTP_INTERCEPTORS, HttpClientModule } from "@angular/common/http";
import { NgModule, isDevMode } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { RouteReuseStrategy } from '@angular/router';
Expand All @@ -13,12 +13,13 @@ import { NgxsModule } from '@ngxs/store';
import { ServiceWorkerModule } from '@angular/service-worker';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { ErrorInterceptor } from './error.interceptor';

const Migrations: Record<any, any> = {};
const allStores: any[] = [];

export function getAuthToken() {
return localStorage.getItem('authToken');
return localStorage.getItem('token');
}

@NgModule({
Expand All @@ -31,7 +32,7 @@ export function getAuthToken() {
JwtModule.forRoot({
config: {
tokenGetter: getAuthToken,
allowedDomains: ["ateoat.com"],
allowedDomains: ["localhost:3000", "ateoat.com"],
},
}),
ServiceWorkerModule.register('ngsw-worker.js', {
Expand All @@ -54,7 +55,12 @@ export function getAuthToken() {
}),
NgxsReduxDevtoolsPluginModule.forRoot(),
],
providers: [{ provide: RouteReuseStrategy, useClass: IonicRouteStrategy }],
providers: [{ provide: RouteReuseStrategy, useClass: IonicRouteStrategy },
{
provide: HTTP_INTERCEPTORS,
useClass: ErrorInterceptor,
multi: true
}],
bootstrap: [AppComponent],
})
export class AppModule {}
39 changes: 36 additions & 3 deletions client/src/app/auth.service.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,56 @@
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { JwtHelperService } from '@auth0/angular-jwt';
import { interval, tap } from 'rxjs';
import { environment } from '../environments/environment';

@Injectable({
providedIn: 'root'
})
export class AuthService {

constructor(public jwtHelper: JwtHelperService) { }
constructor(private http: HttpClient, private jwtHelper: JwtHelperService) {
this.authIfPossible();
this.watchToken();
}

public isAuthenticated(): boolean {
const token = localStorage.getItem('token');
return !this.jwtHelper.isTokenExpired(token);
}

public login(email: string, password: string): void {
private authIfPossible() {
const lastEmail = localStorage.getItem('lastEmail');
const lastPassword = localStorage.getItem('lastPassword');
if(!lastEmail || !lastPassword) return;

this.login(lastEmail, lastPassword).subscribe(() => this.updateToken());
}

private updateToken() {
if(!this.isAuthenticated()) return;

this.http.get(`${environment.apiUrl}/auth/refresh`).subscribe((res: any) => {
localStorage.setItem('token', res.access_token);
});
}

public register(email: string, password: string, username: string): void {
private watchToken() {
interval(5 * 60 * 1000).subscribe(() => this.updateToken());
}

public login(email: string, password: string) {
return this.http.post(`${environment.apiUrl}/auth/login`, { email, password })
.pipe(tap((res: any) => {
if(!res.access_token) return;

localStorage.setItem('lastEmail', email);
localStorage.setItem('lastPassword', password);
localStorage.setItem('token', res.access_token);
}));
}

public register(email: string, password: string, username: string) {
return this.http.post(`${environment.apiUrl}/auth/register`, { email, password, username });
}
}
40 changes: 40 additions & 0 deletions client/src/app/error.interceptor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import {
HttpErrorResponse,
HttpEvent,
HttpHandler,
HttpInterceptor,
HttpRequest
} from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable, catchError, throwError } from 'rxjs';
import { NotifyService } from './notify.service';

@Injectable()
export class ErrorInterceptor implements HttpInterceptor {

constructor(private notify: NotifyService) {}

intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
return next.handle(request)
.pipe(
catchError((error: HttpErrorResponse) => {
let errorMsg = '';

// client side error
if (error.error instanceof ErrorEvent) {
errorMsg = `Error: ${error.error?.message || error.message}`;

// server side error
} else {
errorMsg = `Error: ${error.error?.message || error.message}`;
}

if(!request.url.includes('/auth/')) {
this.notify.error(errorMsg);
}

return throwError(error);
})
);
}
}
114 changes: 97 additions & 17 deletions client/src/app/login/login.page.html
Original file line number Diff line number Diff line change
@@ -1,39 +1,79 @@
<ion-content [fullscreen]="true">
<ion-grid>
<ion-row class="full-width">
<ion-col sizeSm="12" sizeXs="12" sizeMd="6" sizeXl="6" class="full-width image-container">
<ion-col
sizeSm="12"
sizeXs="12"
sizeMd="6"
sizeXl="6"
class="full-width image-container"
>
<ion-card>
<ion-img src="assets/bg/home.png" />
</ion-card>
</ion-col>
</ion-row>

<ion-row class="full-width">
<ion-col sizeSm="12" sizeXs="12" sizeMd="6" sizeXl="6" class="full-width auth-container">
<ion-col
sizeSm="12"
sizeXs="12"
sizeMd="6"
sizeXl="6"
class="full-width auth-container"
>
<ion-segment [(ngModel)]="authType">
<ion-segment-button value="login">
Login
</ion-segment-button>
<ion-segment-button value="register">
Register
</ion-segment-button>
<ion-segment-button value="login"> Login </ion-segment-button>
<ion-segment-button value="register"> Register </ion-segment-button>
</ion-segment>

<ion-card class="full-width auth" *ngIf="authType === 'login'">
<ion-card-content [formGroup]="loginForm">
<ion-list>
<ion-item>
<ion-input label="Email" labelPlacement="stacked" type="text" placeholder="[email protected]" formControlName="email" errorText="Email must be valid."></ion-input>
<ion-input
label="Email"
labelPlacement="stacked"
type="text"
placeholder="[email protected]"
formControlName="email"
errorText="Email must be valid."
(keyup.enter)="login()"
></ion-input>
</ion-item>

<ion-item>
<ion-input label="Password" labelPlacement="stacked" type="password" placeholder="Minimum of 8 characters" formControlName="password" errorText="Password must be at least 8 characters."></ion-input>
<ion-input
label="Password"
labelPlacement="stacked"
type="password"
placeholder="Minimum of 8 characters"
formControlName="password"
errorText="Password must be at least 8 characters."
(keyup.enter)="login()"
></ion-input>
</ion-item>

<ion-item>
<ion-text color="danger" *ngIf="loginError">
{{ loginError }}
</ion-text>
</ion-item>
</ion-list>

<div class="actions">
<ion-button fill="clear" color="warning" (click)="loginForm.reset()">Clear</ion-button>
<ion-button fill="clear" [disabled]="!loginForm.valid" (click)="login()">Login</ion-button>
<ion-button
fill="clear"
color="warning"
(click)="loginForm.reset()"
>Clear</ion-button
>
<ion-button
fill="clear"
[disabled]="!loginForm.valid"
(click)="login()"
>Login</ion-button
>
</div>
</ion-card-content>
</ion-card>
Expand All @@ -42,21 +82,61 @@
<ion-card-content [formGroup]="registerForm">
<ion-list>
<ion-item>
<ion-input label="Email" labelPlacement="stacked" type="text" placeholder="[email protected]" formControlName="email" errorText="Email must be valid."></ion-input>
<ion-input
label="Email"
labelPlacement="stacked"
type="text"
placeholder="[email protected]"
formControlName="email"
errorText="Email must be valid."
(keyup.enter)="register()"
></ion-input>
</ion-item>

<ion-item>
<ion-input
label="Password"
labelPlacement="stacked"
type="password"
placeholder="Minimum of 8 characters"
formControlName="password"
errorText="Password must be at least 8 characters."
(keyup.enter)="register()"
></ion-input>
</ion-item>

<ion-item>
<ion-input label="Password" labelPlacement="stacked" type="password" placeholder="Minimum of 8 characters" formControlName="password" errorText="Password must be at least 8 characters."></ion-input>
<ion-input
label="Username"
labelPlacement="stacked"
type="text"
placeholder="Between 2 and 20 characters"
formControlName="username"
errorText="Username must be between 2 and 20 characters."
(keyup.enter)="register()"
></ion-input>
</ion-item>

<ion-item>
<ion-input label="Username" labelPlacement="stacked" type="text" placeholder="Between 2 and 20 characters" formControlName="username" errorText="Username must be between 2 and 20 characters."></ion-input>
<ion-text color="danger" *ngIf="registerError">
{{ registerError }}
</ion-text>
</ion-item>
</ion-list>

<div class="actions">
<ion-button fill="clear" color="warning" (click)="registerForm.reset()">Clear</ion-button>
<ion-button fill="clear" [disabled]="!registerForm.valid" (click)="register()">Register</ion-button>
<ion-button
fill="clear"
color="warning"
(click)="registerForm.reset()"
>Clear</ion-button
>
<ion-button
fill="clear"
[disabled]="!registerForm.valid"
(click)="register()"
>Register</ion-button
>
</div>
</ion-card-content>
</ion-card>
Expand Down
40 changes: 36 additions & 4 deletions client/src/app/login/login.page.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Component, OnInit } from '@angular/core';
import { FormControl, FormGroup, Validators } from '@angular/forms';
import { Router } from '@angular/router';
import { MenuController } from '@ionic/angular';
import { AuthService } from '../auth.service';

Expand All @@ -23,7 +24,10 @@ export class LoginPage implements OnInit {
username: new FormControl('', [Validators.required, Validators.minLength(2), Validators.maxLength(20)]),
});

constructor(public menu: MenuController, private authService: AuthService) { }
public loginError = '';
public registerError = '';

constructor(public menu: MenuController, private authService: AuthService, private router: Router) { }

ngOnInit() {
this.loginForm.controls.email.errors
Expand All @@ -40,15 +44,43 @@ export class LoginPage implements OnInit {
}

login() {
if(!this.loginForm.value.email || !this.loginForm.value.password) return;
if(!this.loginForm.value.email || !this.loginForm.value.password || !this.loginForm.valid) return;
this.loginError = '';

this.authService.login(this.loginForm.value.email, this.loginForm.value.password)
.subscribe({
next: () => {
this.router.navigate(['/']);
},
error: () => {
this.loginError = 'Invalid email or password.';
}
});
}

register() {
if(!this.registerForm.value.email || !this.registerForm.value.password || !this.registerForm.value.username) return;
if(!this.registerForm.value.email || !this.registerForm.value.password || !this.registerForm.value.username || !this.registerForm.valid) return;
this.registerError = '';

this.authService.register(this.registerForm.value.email, this.registerForm.value.password, this.registerForm.value.username)
.subscribe({
next: () => {
if(!this.registerForm.value.email || !this.registerForm.value.password) return;

this.authService.register(this.registerForm.value.email, this.registerForm.value.password, this.registerForm.value.username);
this.authService.login(this.registerForm.value.email, this.registerForm.value.password)
.subscribe({
next: () => {
this.router.navigate(['/']);
},
error: (err) => {
this.registerError = err.error.message;
}
});
},
error: (err) => {
this.registerError = err.error.message;
}
});
}

}
Loading

0 comments on commit f2bd670

Please sign in to comment.