diff --git a/.vscode/settings.json b/.vscode/settings.json index 8df1e72..1f27eb9 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,10 @@ { - "eslint.workingDirectories": ["client", "server"] + "eslint.workingDirectories": [ + "client", + "server" + ], + "[html]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "editor.formatOnSave": true, } \ No newline at end of file diff --git a/client/src/app/app.module.ts b/client/src/app/app.module.ts index 79944d4..b07ef29 100644 --- a/client/src/app/app.module.ts +++ b/client/src/app/app.module.ts @@ -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'; @@ -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 = {}; const allStores: any[] = []; export function getAuthToken() { - return localStorage.getItem('authToken'); + return localStorage.getItem('token'); } @NgModule({ @@ -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', { @@ -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 {} diff --git a/client/src/app/auth.service.ts b/client/src/app/auth.service.ts index 4ab4eb8..fa7a9f2 100644 --- a/client/src/app/auth.service.ts +++ b/client/src/app/auth.service.ts @@ -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 }); } } diff --git a/client/src/app/error.interceptor.ts b/client/src/app/error.interceptor.ts new file mode 100644 index 0000000..c047957 --- /dev/null +++ b/client/src/app/error.interceptor.ts @@ -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, next: HttpHandler): Observable> { + 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); + }) + ); + } +} diff --git a/client/src/app/login/login.page.html b/client/src/app/login/login.page.html index 1be48ef..aeae646 100644 --- a/client/src/app/login/login.page.html +++ b/client/src/app/login/login.page.html @@ -1,7 +1,13 @@ - + @@ -9,31 +15,65 @@ - + - - Login - - - Register - + Login + Register - + - + + + + + + {{ loginError }} +
- Clear - Login + Clear + Login
@@ -42,21 +82,61 @@ - + + + + + - + - + + {{ registerError }} +
- Clear - Register + Clear + Register
diff --git a/client/src/app/login/login.page.ts b/client/src/app/login/login.page.ts index 3549dda..7a41f60 100644 --- a/client/src/app/login/login.page.ts +++ b/client/src/app/login/login.page.ts @@ -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'; @@ -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 @@ -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; + } + }); } } diff --git a/client/src/app/notify.service.ts b/client/src/app/notify.service.ts new file mode 100644 index 0000000..552c5d5 --- /dev/null +++ b/client/src/app/notify.service.ts @@ -0,0 +1,35 @@ +import { Injectable } from '@angular/core'; +import { ToastController } from '@ionic/angular'; + +@Injectable({ + providedIn: 'root' +}) +export class NotifyService { + + constructor(private toastController: ToastController) {} + + private async showToast(message: string, color: string) { + const toast = await this.toastController.create({ + message, + duration: 1500, + position: 'bottom', + color, + buttons: [ + { + text: 'Close', + role: 'cancel' + } + ] + }); + + await toast.present(); + } + + async notify(message: string) { + await this.showToast(message, ''); + } + + async error(message: string) { + await this.showToast(message, 'danger'); + } +} diff --git a/server/src/auth/auth.service.ts b/server/src/auth/auth.service.ts index 1cc42ea..44ec724 100644 --- a/server/src/auth/auth.service.ts +++ b/server/src/auth/auth.service.ts @@ -47,6 +47,8 @@ export class AuthService { const hash = await bcrypt.hash(password, 10); const newUser = new User(username, discriminator, hash, email); await this.userService.createUser(newUser); + + return { user: newUser }; } async signIn(username: string, password: string): Promise { @@ -69,6 +71,7 @@ export class AuthService { }; return { + user, access_token: await this.jwtService.signAsync(jwtPayload), }; } diff --git a/server/src/main.ts b/server/src/main.ts index 13cad38..4123572 100644 --- a/server/src/main.ts +++ b/server/src/main.ts @@ -3,6 +3,9 @@ import { AppModule } from './app.module'; async function bootstrap() { const app = await NestFactory.create(AppModule); + app.enableCors({ + origin: ['http://localhost:3699', 'https://play.ateoat.com'], + }); await app.listen(3000); } bootstrap();