From c26e0c99f1be015a03f5c4c47a1573d84a9ac592 Mon Sep 17 00:00:00 2001 From: Anish Gurung Date: Wed, 20 Mar 2024 11:37:59 -0700 Subject: [PATCH] Firebase authentication completed --- src/app/app-routing.module.ts | 3 +- src/app/app.module.ts | 12 ++-- src/app/auth/auth-firebase.config.ts | 72 +++++++++++++++++++ src/app/auth/auth.module.ts | 27 ++++++- src/app/auth/login/login.component.html | 8 +-- src/app/auth/login/login.component.ts | 66 ++++++++++++++++- src/app/auth/register/register.component.html | 4 +- src/app/auth/register/register.component.ts | 39 +++++++++- src/app/service/auth-guard.service.ts | 29 ++++++++ src/app/service/auth.service.ts | 30 ++++++++ src/app/utils/response-wrapper.ts | 6 ++ 11 files changed, 277 insertions(+), 19 deletions(-) create mode 100644 src/app/auth/auth-firebase.config.ts create mode 100644 src/app/service/auth-guard.service.ts create mode 100644 src/app/service/auth.service.ts create mode 100644 src/app/utils/response-wrapper.ts diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index cca6bc30..73c58b15 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -1,9 +1,11 @@ import { NgModule } from '@angular/core'; import { ExtraOptions, RouterModule, Routes } from '@angular/router'; +import { AuthGuard } from './service/auth-guard.service'; export const routes: Routes = [ { path: 'pages', + canActivate: [AuthGuard], loadChildren: () => import('./pages/pages.module') .then(m => m.PagesModule), }, @@ -11,7 +13,6 @@ export const routes: Routes = [ path: 'auth', loadChildren: () => import('./auth/auth.module').then(m => m.NgxAuthModule) }, - { path: '', redirectTo: 'auth', pathMatch: 'full' }, { path: '**', redirectTo: 'pages' }, ]; diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 0710cf73..1ce3a588 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -1,8 +1,3 @@ -/** - * @license - * Copyright Akveo. All Rights Reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - */ import { HttpClientModule } from '@angular/common/http'; import { NgModule } from '@angular/core'; import { AngularFireModule } from '@angular/fire/compat'; @@ -22,6 +17,9 @@ import { CoreModule } from './@core/core.module'; import { ThemeModule } from './@theme/theme.module'; import { AppRoutingModule } from './app-routing.module'; import { AppComponent } from './app.component'; +import { AngularFireAuthModule } from '@angular/fire/compat/auth'; + +import { AuthGuard } from './service/auth-guard.service'; @NgModule({ declarations: [AppComponent], @@ -31,6 +29,7 @@ import { AppComponent } from './app.component'; HttpClientModule, AppRoutingModule, AngularFireModule.initializeApp(environment.firebase), + AngularFireAuthModule, NbSidebarModule.forRoot(), NbMenuModule.forRoot(), NbDatepickerModule.forRoot(), @@ -43,6 +42,9 @@ import { AppComponent } from './app.component'; CoreModule.forRoot(), ThemeModule.forRoot(), ], + providers: [ + AuthGuard + ], bootstrap: [AppComponent], }) export class AppModule { diff --git a/src/app/auth/auth-firebase.config.ts b/src/app/auth/auth-firebase.config.ts new file mode 100644 index 00000000..53aa078b --- /dev/null +++ b/src/app/auth/auth-firebase.config.ts @@ -0,0 +1,72 @@ +import { NbAuthStrategyOptions, NbAuthJWTToken, NbPasswordStrategyModule, NbPasswordStrategyMessage } from "@nebular/auth"; + +export class NbFirebasePasswordStrategyOptions extends NbAuthStrategyOptions { + name: string = 'email'; + token = { + class: NbAuthJWTToken, + }; + register?: boolean | NbPasswordStrategyModule = { + redirect: { + success: '/auth/login', + failure: null, + }, + defaultErrors: ['Something went wrong, please try again.'], + defaultMessages: ['You have been successfully registered.'], + }; + login?: boolean | NbPasswordStrategyModule = { + redirect: { + success: '/dashboard', + failure: null, + }, + defaultErrors: ['Login/Email combination is not correct, please try again.'], + defaultMessages: ['You have been successfully logged in.'], + }; + logout?: boolean | NbPasswordStrategyModule = { + redirect: { + success: '/', + failure: null, + }, + defaultErrors: ['Something went wrong, please try again.'], + defaultMessages: ['You have been successfully logged out.'], + }; + refreshToken?: boolean | NbPasswordStrategyModule = { + redirect: { + success: null, + failure: null, + }, + defaultErrors: ['Something went wrong, please try again.'], + defaultMessages: ['Your token has been successfully refreshed.'], + }; + // requestPassword?: boolean | NbPasswordStrategyModule = { + // redirect: { + // success: '/', + // failure: null, + // }, + // defaultErrors: ['Something went wrong, please try again.'], + // defaultMessages: ['Reset password instructions have been sent to your email.'], + // }; + // resetPassword?: boolean | NbPasswordStrategyModule = { + // redirect: { + // success: '/', + // failure: null, + // }, + // defaultErrors: ['Something went wrong, please try again.'], + // defaultMessages: ['Your password has been successfully changed.'], + // }; + // errors?: NbPasswordStrategyMessage = { + // key: 'message', + // getter: (module: string, res, options: NbFirebasePasswordStrategyOptions) => getDeepFromObject( + // res, + // options.errors.key, + // options[module].defaultErrors, + // ), + // }; + // messages?: NbPasswordStrategyMessage = { + // key: 'messages', + // getter: (module: string, res, options: NbFirebasePasswordStrategyOptions) => getDeepFromObject( + // res.body, + // options.messages.key, + // options[module].defaultMessages, + // ), + // }; + } \ No newline at end of file diff --git a/src/app/auth/auth.module.ts b/src/app/auth/auth.module.ts index c0f9ef19..4a542756 100644 --- a/src/app/auth/auth.module.ts +++ b/src/app/auth/auth.module.ts @@ -4,7 +4,7 @@ import { FormsModule } from '@angular/forms'; import { RouterModule } from '@angular/router'; import { NgxAuthRoutingModule } from './auth-routing.module'; -import { NbAuthModule } from '@nebular/auth'; +import { NbAuthModule, NbPasswordAuthStrategy } from '@nebular/auth'; import { NbAlertModule, NbButtonModule, @@ -13,6 +13,7 @@ import { } from '@nebular/theme'; import { NgxLoginComponent } from './login/login.component'; import { NgxRegisterComponent } from './register/register.component'; +import { NbFirebasePasswordStrategyOptions } from './auth-firebase.config'; @NgModule({ @@ -26,7 +27,29 @@ import { NgxRegisterComponent } from './register/register.component'; NbCheckboxModule, NgxAuthRoutingModule, - NbAuthModule, + NbAuthModule.forRoot({ + strategies: [ + NbPasswordAuthStrategy.setup({ + name: 'email', + + login: { + redirect: { + success: '/dashboard', + failure: null, // stay on the same page + }, + }, + + register: { + redirect: { + success: '/auth/login', + failure: null, // stay on the same page + }, + }, + ...new NbFirebasePasswordStrategyOptions() + }), + ], + forms: {}, + }), ], declarations: [ // ... here goes our new components diff --git a/src/app/auth/login/login.component.html b/src/app/auth/login/login.component.html index 1889aeec..741f26dc 100644 --- a/src/app/auth/login/login.component.html +++ b/src/app/auth/login/login.component.html @@ -1,7 +1,7 @@

Login

Welcome to RESUME TAILOR!!!

-
  • {{ error }}
  • -
    + --> - - + -->
    diff --git a/src/app/auth/login/login.component.ts b/src/app/auth/login/login.component.ts index 5ddc4636..1adc3262 100644 --- a/src/app/auth/login/login.component.ts +++ b/src/app/auth/login/login.component.ts @@ -1,4 +1,7 @@ -import { Component } from '@angular/core'; +import { Component, ChangeDetectorRef } from '@angular/core'; +import { AngularFireAuth } from '@angular/fire/compat/auth'; +import { Router } from '@angular/router'; // Import Router from @angular/router +import { NbAuthService } from '@nebular/auth'; import { NbLoginComponent } from '@nebular/auth'; @Component({ @@ -6,4 +9,63 @@ import { NbLoginComponent } from '@nebular/auth'; templateUrl: './login.component.html', }) export class NgxLoginComponent extends NbLoginComponent { -} \ No newline at end of file + constructor( + private fireAuth: AngularFireAuth, + private authService: NbAuthService, + public cd: ChangeDetectorRef, // Change to public or protected if NbLoginComponent's cd is public or protected + public router: Router // Import Router from @angular/router + ) { + super(authService, {}, cd, router); + } + + async login() { + try { + await this.fireAuth.signInWithEmailAndPassword(this.user.email, this.user.password); + const user = await this.fireAuth.currentUser; // Get current user + if (user) { + const accessToken = await user.getIdToken(); // Get Firebase Authentication token (access token) + const expiresIn = new Date().getTime() + 3600 * 1000; // Token expiration time (1 hour) + localStorage.setItem('accessToken', accessToken); + localStorage.setItem('accessTokenExpiresIn', expiresIn.toString()); // Store token expiration time + this.router.navigate(['dashboard']); // Redirect to dashboard + this.scheduleTokenRefresh(); // Schedule token refresh + } + console.log("Successfully Logged in.") + } catch (error) { + // Handle login errors here + console.error('Error signing in:', error); + // You can also display error messages to the user using this.errors array + this.errors = ['Error signing in. Please try again.']; + } + } + + private async scheduleTokenRefresh(): Promise { + const expiresInString = localStorage.getItem('accessTokenExpiresIn'); + if (expiresInString) { + const expiresIn = +expiresInString; + const timeUntilRefresh = expiresIn - new Date().getTime() - 300000; // Refresh token 5 minutes before expiration + if (timeUntilRefresh > 0) { + setTimeout(async () => { + try { + const user = await this.fireAuth.currentUser; + if (user) { + const newAccessToken = await user.getIdToken(); + localStorage.setItem('accessToken', newAccessToken); + const newExpiresIn = new Date().getTime() + 3600 * 1000; // Extend expiration time by 1 hour + localStorage.setItem('accessTokenExpiresIn', newExpiresIn.toString()); + this.scheduleTokenRefresh(); + } + } catch (error) { + console.error('Token refresh failed:', error); + } + }, timeUntilRefresh); + } + } else { + console.error('Access token expiration time is not available.'); + } + } + + getConfigValue(key: string): any { + // Implement this method based on your configuration retrieval logic + } +} diff --git a/src/app/auth/register/register.component.html b/src/app/auth/register/register.component.html index ecc94d13..3b26bf70 100644 --- a/src/app/auth/register/register.component.html +++ b/src/app/auth/register/register.component.html @@ -1,6 +1,6 @@

    Register

    - - + -->
    diff --git a/src/app/auth/register/register.component.ts b/src/app/auth/register/register.component.ts index e7bddf31..2dbd5813 100644 --- a/src/app/auth/register/register.component.ts +++ b/src/app/auth/register/register.component.ts @@ -1,9 +1,42 @@ import { Component } from '@angular/core'; -import { NbRegisterComponent } from '@nebular/auth'; +import { AngularFireAuth } from '@angular/fire/compat/auth'; +import { Router } from '@angular/router'; @Component({ selector: 'ngx-register', templateUrl: './register.component.html', }) -export class NgxRegisterComponent extends NbRegisterComponent { -} \ No newline at end of file +export class NgxRegisterComponent { + user = { + fullName: '', + email: '', + password: '', + confirmPassword: '' + }; + submitted = false; + errors: string[] = []; + + constructor(private fireAuth: AngularFireAuth, private router: Router) {} + + async register() { + this.submitted = true; + try { + if (this.user.password !== this.user.confirmPassword) { + throw new Error('Password does not match the confirm password.'); + } + + await this.fireAuth.createUserWithEmailAndPassword(this.user.email, this.user.password); + // Registration successful, redirect to login page or dashboard + this.router.navigate(['auth/login']); + } catch (error) { + // Handle registration errors + console.error('Error registering user:', error); + this.errors.push(error.message || 'An error occurred. Please try again later.'); + } + this.submitted = false; + } + + getConfigValue(key: string): any { + // Implement this method based on your configuration retrieval logic + } +} diff --git a/src/app/service/auth-guard.service.ts b/src/app/service/auth-guard.service.ts new file mode 100644 index 00000000..64b698cb --- /dev/null +++ b/src/app/service/auth-guard.service.ts @@ -0,0 +1,29 @@ +import { Injectable } from '@angular/core'; +import { CanActivate, Router } from '@angular/router'; + +@Injectable({ + providedIn: 'root' +}) +export class AuthGuard implements CanActivate { + + constructor(private router: Router) {} + + canActivate(): boolean { + const accessToken = localStorage.getItem('accessToken'); + const accessTokenExpiresIn = localStorage.getItem('accessTokenExpiresIn'); + + // Check if access token and expiration time exist + if (accessToken && accessTokenExpiresIn) { + // Check if the access token is not expired + const currentTime = new Date().getTime(); + const expiresIn = +accessTokenExpiresIn; + if (currentTime < expiresIn) { + return true; // Allow access to the route + } + } + + // If access token is not found or expired, navigate to login page + this.router.navigate(['/auth/login']); + return false; + } +} diff --git a/src/app/service/auth.service.ts b/src/app/service/auth.service.ts new file mode 100644 index 00000000..4273f091 --- /dev/null +++ b/src/app/service/auth.service.ts @@ -0,0 +1,30 @@ +import { Injectable } from '@angular/core'; +import { AngularFireAuth } from '@angular/fire/compat/auth'; +import { Router } from '@angular/router'; + +import { Observable } from 'rxjs'; + + +@Injectable({ + providedIn: 'root' +}) +export class AuthService { + + user$: Observable; + + constructor(private fireAuth: AngularFireAuth, private router: Router) { + this.user$ = this.fireAuth.authState; + } + + // logout method + async logout(): Promise { + try { + await this.fireAuth.signOut(); + localStorage.removeItem('accessToken'); + localStorage.removeItem('accessTokenExpiresIn'); + this.router.navigate(['dashboard'], { queryParams: { logout: true } }); + } catch (error: any) { + throw error; + } + } +} diff --git a/src/app/utils/response-wrapper.ts b/src/app/utils/response-wrapper.ts new file mode 100644 index 00000000..cee0f620 --- /dev/null +++ b/src/app/utils/response-wrapper.ts @@ -0,0 +1,6 @@ +export interface ResponseWrapper { + success: boolean; + message: string; + data: T; + error?: any; + } \ No newline at end of file