Pārlūkot izejas kodu

Implement Angular JWT auth with login, register, and guards

Eldar Mukhtarov 9 mēneši atpakaļ
vecāks
revīzija
43dec4db02
32 mainītis faili ar 561 papildinājumiem un 199 dzēšanām
  1. 0 0
      project/frontend-angular/src/app/admin/admin.css
  2. 4 0
      project/frontend-angular/src/app/admin/admin.html
  3. 25 0
      project/frontend-angular/src/app/admin/admin.spec.ts
  4. 16 0
      project/frontend-angular/src/app/admin/admin.ts
  5. 13 5
      project/frontend-angular/src/app/app.config.ts
  6. 0 191
      project/frontend-angular/src/app/app.html
  7. 33 1
      project/frontend-angular/src/app/app.ts
  8. 7 0
      project/frontend-angular/src/app/auth/auth-interceptor.spec.ts
  9. 22 0
      project/frontend-angular/src/app/auth/auth-interceptor.ts
  10. 12 0
      project/frontend-angular/src/app/auth/auth.service.spec.ts
  11. 30 0
      project/frontend-angular/src/app/auth/auth.service.ts
  12. 7 0
      project/frontend-angular/src/app/auth/jwt-response.spec.ts
  13. 7 0
      project/frontend-angular/src/app/auth/jwt-response.ts
  14. 7 0
      project/frontend-angular/src/app/auth/login-info.spec.ts
  15. 11 0
      project/frontend-angular/src/app/auth/login-info.ts
  16. 7 0
      project/frontend-angular/src/app/auth/signup-info.spec.ts
  17. 16 0
      project/frontend-angular/src/app/auth/signup-info.ts
  18. 12 0
      project/frontend-angular/src/app/auth/token-storage.service.spec.ts
  19. 52 0
      project/frontend-angular/src/app/auth/token-storage.service.ts
  20. 17 0
      project/frontend-angular/src/app/guards/auth.guard.spec.ts
  21. 22 0
      project/frontend-angular/src/app/guards/auth.guard.ts
  22. 16 0
      project/frontend-angular/src/app/guards/role.guard.spec.ts
  23. 32 0
      project/frontend-angular/src/app/guards/role.guard.ts
  24. 23 2
      project/frontend-angular/src/app/home/home.ts
  25. 0 0
      project/frontend-angular/src/app/login/login.css
  26. 1 0
      project/frontend-angular/src/app/login/login.html
  27. 23 0
      project/frontend-angular/src/app/login/login.spec.ts
  28. 68 0
      project/frontend-angular/src/app/login/login.ts
  29. 0 0
      project/frontend-angular/src/app/register/register.css
  30. 1 0
      project/frontend-angular/src/app/register/register.html
  31. 23 0
      project/frontend-angular/src/app/register/register.spec.ts
  32. 54 0
      project/frontend-angular/src/app/register/register.ts

+ 0 - 0
project/frontend-angular/src/app/admin/admin.css


+ 4 - 0
project/frontend-angular/src/app/admin/admin.html

@@ -0,0 +1,4 @@
+<h4>Content from Server</h4>
+{{board}}
+{{errorMessage}}
+

+ 25 - 0
project/frontend-angular/src/app/admin/admin.spec.ts

@@ -0,0 +1,25 @@
+import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { Admin } from './admin';
+
+describe('Admin', () => {
+  let component: Admin;
+  let fixture: ComponentFixture<Admin>;
+
+  beforeEach(waitForAsync(() => {
+    TestBed.configureTestingModule({
+      declarations: [ Admin ]
+    })
+    .compileComponents();
+  }));
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(Admin);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});

+ 16 - 0
project/frontend-angular/src/app/admin/admin.ts

@@ -0,0 +1,16 @@
+import {Component, inject, OnInit} from '@angular/core';
+
+@Component({
+  selector: 'app-admin',
+  templateUrl: './admin.html',
+  standalone: true,
+  styleUrls: ['./admin.css']
+})
+export class Admin implements OnInit {
+  board?: string;
+  errorMessage?: string;
+
+  ngOnInit() {
+  }
+}
+

+ 13 - 5
project/frontend-angular/src/app/app.config.ts

@@ -1,12 +1,20 @@
-import { ApplicationConfig, provideBrowserGlobalErrorListeners, provideZoneChangeDetection } from '@angular/core';
+import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
 import { provideRouter } from '@angular/router';
 
 import { routes } from './app.routes';
+import {
+  HTTP_INTERCEPTORS,
+  provideHttpClient,
+  withFetch,
+  withInterceptors,
+  withInterceptorsFromDi
+} from '@angular/common/http';
+import {AuthInterceptor} from './auth/auth-interceptor';
 
 export const appConfig: ApplicationConfig = {
-  providers: [
-    provideBrowserGlobalErrorListeners(),
-    provideZoneChangeDetection({ eventCoalescing: true }),
-    provideRouter(routes)
+  providers: [provideZoneChangeDetection({ eventCoalescing: true }),
+    provideRouter(routes),
+    provideHttpClient(withInterceptorsFromDi()),
+    {provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true}
   ]
 };

Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 0 - 191
project/frontend-angular/src/app/app.html


+ 33 - 1
project/frontend-angular/src/app/app.ts

@@ -2,6 +2,7 @@ import { Component } from '@angular/core';
 import { RouterLink, RouterOutlet } from '@angular/router';
 import {Student} from './student/student';
 import {Teacher} from './teacher/teacher';
+import {TokenStorageService} from './auth/token-storage.service';
 
 @Component({
   selector: 'app-root',
@@ -10,5 +11,36 @@ import {Teacher} from './teacher/teacher';
   styleUrl: './app.css'
 })
 export class App {
-  protected title = 'frontend-angular';
+  title = 'better WIKAMP';
+
+  private roles?: string[];
+  authority?: string;
+  loggedUser?: string;
+
+  constructor(private tokenStorage: TokenStorageService) {  }
+
+  ngOnInit() {
+    console.log("init");
+    if (this.tokenStorage.getToken()) {
+      console.log(this.tokenStorage.getToken());
+      this.roles = this.tokenStorage.getAuthorities();
+      this.roles.every(role => {
+        if (role === 'ROLE_ADMIN') {
+          this.authority = 'admin';
+          return false;
+        } else if (role === 'ROLE_TEACHER') {
+          this.authority = 'teacher';
+          return false;
+        }
+        this.authority = 'student';
+        return true;
+      });
+      this.loggedUser = this.tokenStorage.getUsername();
+    }
+  }
+
+  logout() {
+    this.tokenStorage.signOut();
+    window.location.reload();
+  }
 }

+ 7 - 0
project/frontend-angular/src/app/auth/auth-interceptor.spec.ts

@@ -0,0 +1,7 @@
+import { AuthInterceptor } from './auth-interceptor';
+
+describe('AuthInterceptor', () => {
+  it('should create an instance', () => {
+    //expect(new AuthInterceptor()).toBeTruthy();
+  });
+});

+ 22 - 0
project/frontend-angular/src/app/auth/auth-interceptor.ts

@@ -0,0 +1,22 @@
+import {inject, Injectable} from '@angular/core';
+import {HTTP_INTERCEPTORS, HttpHandler, HttpInterceptor, HttpRequest} from '@angular/common/http';
+import {TokenStorageService} from './token-storage.service';
+
+const TOKEN_HEADER_KEY = 'Authorization';
+
+@Injectable()
+export class AuthInterceptor implements HttpInterceptor {
+
+  private tokenStorage = inject(TokenStorageService);
+
+  intercept(req: HttpRequest<any>, next: HttpHandler) {
+    console.log("class-based-interceptor");
+    let authReq = req;
+    const token = this.tokenStorage.getToken();
+    if (token != "{}") {
+      authReq = req.clone({ headers: req.headers.set(TOKEN_HEADER_KEY, 'Bearer ' + token) });
+    }
+    return next.handle(authReq);
+  }
+}
+

+ 12 - 0
project/frontend-angular/src/app/auth/auth.service.spec.ts

@@ -0,0 +1,12 @@
+import { TestBed } from '@angular/core/testing';
+
+import { AuthService } from './auth.service';
+
+describe('AuthService', () => {
+  beforeEach(() => TestBed.configureTestingModule({}));
+
+  it('should be created', () => {
+    const service: AuthService = TestBed.inject(AuthService);
+    expect(service).toBeTruthy();
+  });
+});

+ 30 - 0
project/frontend-angular/src/app/auth/auth.service.ts

@@ -0,0 +1,30 @@
+import { Injectable } from '@angular/core';
+import {HttpClient, HttpHeaders} from '@angular/common/http';
+import {LoginInfo} from './login-info';
+import {Observable} from 'rxjs';
+import {JwtResponse} from './jwt-response';
+import {SignupInfo} from './signup-info';
+
+const httpOptions = {
+  headers: new HttpHeaders({'Content-Type': 'application/json'})
+};
+
+@Injectable({
+  providedIn: 'root'
+})
+export class AuthService {
+
+  private loginUrl = 'http://localhost:8080/auth/signin';
+  private signupUrl = 'http://localhost:8080/auth/signup';
+
+  constructor(private http: HttpClient) { }
+
+  attemptAuth(credentials: LoginInfo): Observable<JwtResponse> {
+    return this.http.post<JwtResponse>(this.loginUrl, credentials, httpOptions);
+  }
+
+  signUp(info: SignupInfo): Observable<string> {
+    return this.http.post<string>(this.signupUrl, info, httpOptions);
+  }
+}
+

+ 7 - 0
project/frontend-angular/src/app/auth/jwt-response.spec.ts

@@ -0,0 +1,7 @@
+import { JwtResponse } from './jwt-response';
+
+describe('JwtResponse', () => {
+  it('should create an instance', () => {
+    expect(new JwtResponse()).toBeTruthy();
+  });
+});

+ 7 - 0
project/frontend-angular/src/app/auth/jwt-response.ts

@@ -0,0 +1,7 @@
+export class JwtResponse {
+  accessToken?: string;
+  type?: string;
+  username?: string;
+  authorities?: string[];
+}
+

+ 7 - 0
project/frontend-angular/src/app/auth/login-info.spec.ts

@@ -0,0 +1,7 @@
+import { LoginInfo } from './login-info';
+
+describe('LoginInfo', () => {
+  it('should create an instance', () => {
+    // expect(new LoginInfo()).toBeTruthy();
+  });
+});

+ 11 - 0
project/frontend-angular/src/app/auth/login-info.ts

@@ -0,0 +1,11 @@
+export class LoginInfo {
+
+  username: string;
+  password: string;
+
+  constructor(username: string, password: string) {
+    this.username = username;
+    this.password = password;
+  }
+}
+

+ 7 - 0
project/frontend-angular/src/app/auth/signup-info.spec.ts

@@ -0,0 +1,7 @@
+import { SignupInfo } from './signup-info';
+
+describe('SignupInfo', () => {
+  it('should create an instance', () => {
+    expect(new SignupInfo()).toBeTruthy();
+  });
+});

+ 16 - 0
project/frontend-angular/src/app/auth/signup-info.ts

@@ -0,0 +1,16 @@
+export class SignupInfo {
+
+  username: string;
+  role: string[];
+  password: string;
+  firstname: string;
+  lastname: string;
+
+  constructor(username: string, password: string, role: string[], firstname: string, lastname: string) {
+    this.username = username;
+    this.role = role;
+    this.password = password;
+    this.firstname = firstname
+    this.lastname = lastname;
+  }
+}

+ 12 - 0
project/frontend-angular/src/app/auth/token-storage.service.spec.ts

@@ -0,0 +1,12 @@
+import { TestBed } from '@angular/core/testing';
+
+import { TokenStorageService } from './token-storage.service';
+
+describe('TokenStorageService', () => {
+  beforeEach(() => TestBed.configureTestingModule({}));
+
+  it('should be created', () => {
+    const service: TokenStorageService = TestBed.get(TokenStorageService);
+    expect(service).toBeTruthy();
+  });
+});

+ 52 - 0
project/frontend-angular/src/app/auth/token-storage.service.ts

@@ -0,0 +1,52 @@
+import { Injectable } from '@angular/core';
+
+const TOKEN_KEY = 'AuthToken';
+const USERNAME_KEY = 'AuthUsername';
+const AUTHORITIES_KEY = 'AuthAuthorities';
+
+@Injectable({
+  providedIn: 'root'
+})
+export class TokenStorageService {
+  private roles: Array<string> = [];
+  constructor() { }
+
+  signOut() {
+    window.sessionStorage.clear();
+  }
+
+  public saveToken(token: string) {
+    window.sessionStorage.removeItem(TOKEN_KEY);
+    window.sessionStorage.setItem(TOKEN_KEY, token);
+  }
+
+  public getToken(): string {
+    return sessionStorage.getItem(TOKEN_KEY) || '{}';
+  }
+
+  public saveUsername(username: string) {
+    window.sessionStorage.removeItem(USERNAME_KEY);
+    window.sessionStorage.setItem(USERNAME_KEY, username);
+  }
+
+  public getUsername(): string {
+    return sessionStorage.getItem(USERNAME_KEY) || '{}';
+  }
+
+  public saveAuthorities(authorities: string[]) {
+    window.sessionStorage.removeItem(AUTHORITIES_KEY);
+    window.sessionStorage.setItem(AUTHORITIES_KEY, JSON.stringify(authorities));
+  }
+
+  public getAuthorities(): string[] {
+    this.roles = [];
+
+    if (sessionStorage.getItem(TOKEN_KEY)) {
+      JSON.parse(sessionStorage.getItem(AUTHORITIES_KEY) || '{}').forEach((authority: { authority: string; }) => {
+        this.roles.push(authority.authority);
+      });
+    }
+
+    return this.roles;
+  }
+}

+ 17 - 0
project/frontend-angular/src/app/guards/auth.guard.spec.ts

@@ -0,0 +1,17 @@
+import { TestBed } from '@angular/core/testing';
+import { CanActivateFn } from '@angular/router';
+
+import { authGuard } from './auth.guard';
+
+describe('authGuard', () => {
+  const executeGuard: CanActivateFn = (...guardParameters) => 
+      TestBed.runInInjectionContext(() => authGuard(...guardParameters));
+
+  beforeEach(() => {
+    TestBed.configureTestingModule({});
+  });
+
+  it('should be created', () => {
+    expect(executeGuard).toBeTruthy();
+  });
+});

+ 22 - 0
project/frontend-angular/src/app/guards/auth.guard.ts

@@ -0,0 +1,22 @@
+import {CanActivateFn, Router} from '@angular/router';
+import {TokenStorageService} from "../auth/token-storage.service";
+import {inject} from "@angular/core";
+
+export const authGuard: CanActivateFn = (route, state) => {
+
+  const tokenStorageService = inject(TokenStorageService);
+  const router = inject(Router);
+
+  // check if any role from authorities list is in the routing list defined
+  for (let i = 0; i < route.data['roles'].length; i++) {
+    for (let j = 0; j < tokenStorageService.getAuthorities().length; j++) {
+      if (route.data['roles'][i] === tokenStorageService.getAuthorities()[j]) {
+        return true;
+      }
+    }
+  }
+
+  // otherwise redirect to specified url
+  // this.router.navigateByUrl('').then();
+  return router.parseUrl('');
+};

+ 16 - 0
project/frontend-angular/src/app/guards/role.guard.spec.ts

@@ -0,0 +1,16 @@
+import { TestBed } from '@angular/core/testing';
+
+import { RoleGuard } from './role.guard';
+
+describe('RoleGuard', () => {
+  let guard: RoleGuard;
+
+  beforeEach(() => {
+    TestBed.configureTestingModule({});
+    guard = TestBed.inject(RoleGuard);
+  });
+
+  it('should be created', () => {
+    expect(guard).toBeTruthy();
+  });
+});

+ 32 - 0
project/frontend-angular/src/app/guards/role.guard.ts

@@ -0,0 +1,32 @@
+import {inject, Injectable} from '@angular/core';
+import {ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot, UrlTree} from '@angular/router';
+import { Observable } from 'rxjs';
+import {TokenStorageService} from "../auth/token-storage.service";
+
+@Injectable({
+  providedIn: 'root'
+})
+export class RoleGuard implements CanActivate {
+
+  private tokenStorageService = inject(TokenStorageService);
+  private router = inject(Router);
+
+  canActivate(
+    route: ActivatedRouteSnapshot,
+    state: RouterStateSnapshot): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
+
+    // check if any role from authorities list is in the routing list defined
+    for (let i = 0; i < route.data['roles'].length; i++) {
+      for (let j = 0; j < this.tokenStorageService.getAuthorities().length; j++) {
+        if (route.data['roles'][i] === this.tokenStorageService.getAuthorities()[j]) {
+          return true;
+        }
+      }
+    }
+
+    // otherwise redirect to specified url
+      this.router.navigateByUrl('').then();
+      return false;
+  }
+}
+

+ 23 - 2
project/frontend-angular/src/app/home/home.ts

@@ -1,4 +1,5 @@
-import { Component } from '@angular/core';
+import {Component, OnInit} from '@angular/core';
+import {TokenStorageService} from '../auth/token-storage.service';
 
 @Component({
   selector: 'app-home',
@@ -6,6 +7,26 @@ import { Component } from '@angular/core';
   templateUrl: './home.html',
   styleUrl: './home.css'
 })
-export class Home {
+export class Home implements OnInit {
 
+  info: any;
+  constructor(private token: TokenStorageService) {}
+  ngOnInit() {
+    this.info = {
+      token: this.token.getToken(),
+      username: this.token.getUsername(),
+      authorities: this.token.getAuthorities()
+    };
+    this.mapAuthorities();
+  }
+
+  mapAuthorities(){
+    if(Array.isArray(this.info.authorities)){
+      this.info.authorities = this.info.authorities.map((authority: string) => {
+        const trimmedAuthority = authority.replace('ROLE_', '').toLowerCase();
+        return trimmedAuthority.charAt(0).toUpperCase() + trimmedAuthority.slice(1);
+      });
+    }
+  }
 }
+

+ 0 - 0
project/frontend-angular/src/app/login/login.css


+ 1 - 0
project/frontend-angular/src/app/login/login.html

@@ -0,0 +1 @@
+<p>login works!</p>

+ 23 - 0
project/frontend-angular/src/app/login/login.spec.ts

@@ -0,0 +1,23 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { Login } from './login';
+
+describe('Login', () => {
+  let component: Login;
+  let fixture: ComponentFixture<Login>;
+
+  beforeEach(async () => {
+    await TestBed.configureTestingModule({
+      imports: [Login]
+    })
+    .compileComponents();
+
+    fixture = TestBed.createComponent(Login);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});

+ 68 - 0
project/frontend-angular/src/app/login/login.ts

@@ -0,0 +1,68 @@
+import {Component, inject, OnInit} from '@angular/core';
+import {LoginInfo} from '../auth/login-info';
+import {AuthService} from '../auth/auth.service';
+import {TokenStorageService} from '../auth/token-storage.service';
+import {FormsModule} from '@angular/forms';
+
+@Component({
+  selector: 'app-login',
+  templateUrl: './login.html',
+  imports: [FormsModule],
+  standalone: true,
+  styleUrls: ['./login.css']
+})
+export class Login implements OnInit {
+  form: any = {};
+  token?: string;
+  isLoggedIn = false;
+  isLoginFailed = false;
+  errorMessage = '';
+  roles: string[] = [];
+  private loginInfo?: LoginInfo;
+
+  private authService =inject(AuthService);
+  private tokenStorage = inject(TokenStorageService);
+
+  // older approach/syntax for DI that still works
+  // constructor(private authService: AuthService, private tokenStorage: TokenStorageService) { }
+
+  ngOnInit() {
+    if (this.tokenStorage.getToken() != null && this.tokenStorage.getToken() != '{}') {
+      this.isLoggedIn = true;
+      this.roles = this.tokenStorage.getAuthorities();
+    }
+  }
+
+  onSubmit() {
+    console.log(this.form);
+
+    this.loginInfo = new LoginInfo(this.form.username, this.form.password);
+
+    this.authService.attemptAuth(this.loginInfo).subscribe({
+      next:(data)  =>
+      {
+        this.tokenStorage.saveToken(data.accessToken || '{}');
+        this.tokenStorage.saveUsername(data.username || '{}');
+        this.tokenStorage.saveAuthorities(data.authorities || []);
+
+        this.isLoginFailed = false;
+        this.isLoggedIn = true;
+        this.token = this.tokenStorage.getToken();
+        this.roles = this.tokenStorage.getAuthorities();
+        this.reloadPage();
+      }
+      ,
+      error: (error) => {
+        console.log(error);
+        this.errorMessage = error.error.message;
+        this.isLoginFailed = true;
+      }
+    });
+  }
+
+  reloadPage() {
+    window.location.reload();
+  }
+
+}
+

+ 0 - 0
project/frontend-angular/src/app/register/register.css


+ 1 - 0
project/frontend-angular/src/app/register/register.html

@@ -0,0 +1 @@
+<p>register works!</p>

+ 23 - 0
project/frontend-angular/src/app/register/register.spec.ts

@@ -0,0 +1,23 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { Register } from './register';
+
+describe('Register', () => {
+  let component: Register;
+  let fixture: ComponentFixture<Register>;
+
+  beforeEach(async () => {
+    await TestBed.configureTestingModule({
+      imports: [Register]
+    })
+    .compileComponents();
+
+    fixture = TestBed.createComponent(Register);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});

+ 54 - 0
project/frontend-angular/src/app/register/register.ts

@@ -0,0 +1,54 @@
+import {Component, inject, OnInit} from '@angular/core';
+import {AuthService} from '../auth/auth.service';
+import {SignupInfo} from '../auth/signup-info';
+import {FormsModule} from '@angular/forms';
+
+@Component({
+  selector: 'app-register',
+  templateUrl: './register.html',
+  imports: [FormsModule],
+  standalone: true,
+  styleUrls: ['./register.css']
+})
+export class Register implements OnInit {
+  form: any = {};
+  signupInfo?: SignupInfo;
+  isSignedUp = false;
+  isSignUpFailed = false;
+  errorMessage = '';
+
+  private authService = inject(AuthService);
+
+  ngOnInit() { }
+
+  onSubmit() {
+    console.log(this.form);
+
+    this.signupInfo = new SignupInfo(
+      this.form.username,
+      this.form.password,
+      [this.form.role],
+      this.form.firstname,
+      this.form.lastname);
+
+    this.authService.signUp(this.signupInfo).subscribe({
+      next: (data) =>
+      {
+        console.log(data);
+        this.isSignedUp = true;
+        this.isSignUpFailed = false;
+        this.reloadPage();
+      }
+      ,
+      error: (error) => {
+        console.log(error);
+        this.errorMessage = error.error.message;
+        this.isSignUpFailed = true;
+      }
+    });
+  }
+
+  reloadPage(): void {
+    window.location.reload();
+  }
+}

Daži faili netika attēloti, jo izmaiņu fails ir pārāk liels