diff --git a/package-lock.json b/package-lock.json index 19258ae..97b8de1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "@angular/platform-browser-dynamic": "^18.2.0", "@angular/router": "^18.2.0", "@ng-select/ng-select": "^13.9.1", + "crypto-js": "^4.2.0", "rxjs": "~7.8.0", "tslib": "^2.3.0", "zone.js": "~0.14.10" @@ -25,6 +26,7 @@ "@angular-devkit/build-angular": "^18.2.7", "@angular/cli": "^18.2.7", "@angular/compiler-cli": "^18.2.0", + "@types/crypto-js": "^4.2.2", "@types/jasmine": "~5.1.0", "jasmine-core": "~5.2.0", "karma": "~6.4.0", @@ -4392,6 +4394,13 @@ "@types/node": "*" } }, + "node_modules/@types/crypto-js": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@types/crypto-js/-/crypto-js-4.2.2.tgz", + "integrity": "sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", @@ -6224,6 +6233,12 @@ "node": ">= 8" } }, + "node_modules/crypto-js": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", + "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==", + "license": "MIT" + }, "node_modules/css-loader": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-7.1.2.tgz", diff --git a/package.json b/package.json index c45dcfb..cbc0dc7 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "@angular/platform-browser-dynamic": "^18.2.0", "@angular/router": "^18.2.0", "@ng-select/ng-select": "^13.9.1", + "crypto-js": "^4.2.0", "rxjs": "~7.8.0", "tslib": "^2.3.0", "zone.js": "~0.14.10" @@ -27,6 +28,7 @@ "@angular-devkit/build-angular": "^18.2.7", "@angular/cli": "^18.2.7", "@angular/compiler-cli": "^18.2.0", + "@types/crypto-js": "^4.2.2", "@types/jasmine": "~5.1.0", "jasmine-core": "~5.2.0", "karma": "~6.4.0", @@ -36,4 +38,4 @@ "karma-jasmine-html-reporter": "~2.1.0", "typescript": "~5.5.2" } -} \ No newline at end of file +} diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index 2c5014a..2c162a0 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -1,8 +1,12 @@ import { Routes } from '@angular/router'; import { ChatComponent } from './chat/chat.component'; +import { LoginComponent } from './login/login.component'; +import { AuthGuard } from './utils/auth.guard'; export const routes: Routes = [ - { path: 'chat/en', component: ChatComponent, data: { language: 'en' } }, - { path: 'chat/de', component: ChatComponent, data: { language: 'de' } }, - { path: '', redirectTo: 'chat/en', pathMatch: 'full' } // Default to English chat + { path: 'login', component: LoginComponent }, + { path: 'chat/en', component: ChatComponent, canActivate: [AuthGuard], data: { language: 'en' } }, + { path: 'chat/de', component: ChatComponent, canActivate: [AuthGuard], data: { language: 'de' } }, + { path: '', redirectTo: 'login', pathMatch: 'full' }, + { path: '**', redirectTo: 'login', pathMatch: 'full' } ]; \ No newline at end of file diff --git a/src/app/login/login.component.html b/src/app/login/login.component.html new file mode 100644 index 0000000..764b0f7 --- /dev/null +++ b/src/app/login/login.component.html @@ -0,0 +1,28 @@ +
+

Login

+
+
+ + +
+ Username is required +
+
+ +
+ + +
+ Password is required +
+
+ +
+ {{ errorMessage }} +
+ + +
+
\ No newline at end of file diff --git a/src/app/login/login.component.scss b/src/app/login/login.component.scss new file mode 100644 index 0000000..a4903a7 --- /dev/null +++ b/src/app/login/login.component.scss @@ -0,0 +1,53 @@ +.login-container { + width: 300px; + margin: 0 auto; + padding: 2rem; + border: 1px solid #ddd; + border-radius: 8px; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); +} + +h2 { + text-align: center; +} + +.form-group { + margin-bottom: 1rem; +} + +label { + display: block; + font-weight: bold; + margin-bottom: 0.5rem; +} + +input { + width: 100%; + padding: 0.5rem; + border: 1px solid #ccc; + border-radius: 4px; +} + +button { + width: 100%; + padding: 0.7rem; + background-color: #007bff; + color: #fff; + border: none; + border-radius: 4px; + cursor: pointer; +} + +button:disabled { + background-color: #a9a9a9; + cursor: not-allowed; +} + +.error { + color: red; + font-size: 0.85rem; +} + +.invalid { + border-color: red; +} \ No newline at end of file diff --git a/src/app/login/login.component.spec.ts b/src/app/login/login.component.spec.ts new file mode 100644 index 0000000..18f3685 --- /dev/null +++ b/src/app/login/login.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { LoginComponent } from './login.component'; + +describe('LoginComponent', () => { + let component: LoginComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [LoginComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(LoginComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/login/login.component.ts b/src/app/login/login.component.ts new file mode 100644 index 0000000..8127075 --- /dev/null +++ b/src/app/login/login.component.ts @@ -0,0 +1,40 @@ +import { Component } from '@angular/core'; +import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms'; +import { Router } from '@angular/router'; +import { AuthService } from '../services/auth.service'; + +@Component({ + selector: 'app-login', + standalone: true, + imports: [ReactiveFormsModule], + templateUrl: './login.component.html', + styleUrl: './login.component.scss' +}) +export class LoginComponent { + public loginForm: FormGroup; + errorMessage: string | null = null; + + constructor( + private authService: AuthService, + private router: Router + ) { + this.loginForm = new FormGroup({ + username: new FormControl('', [Validators.required]), + password: new FormControl('', [Validators.required]) + }); + } + + onSubmit(): void { + if (this.loginForm.valid) { + const { username, password } = this.loginForm.value; + this.authService.login(username, password).subscribe({ + next: () => this.router.navigate(['/chat/en']), // Redirect to chat after login + error: (err) => (this.errorMessage = 'Invalid username or password') + }); + } + } + + get f() { + return this.loginForm.controls; + } +} diff --git a/src/app/services/auth.service.ts b/src/app/services/auth.service.ts index ad6d225..ef40d41 100644 --- a/src/app/services/auth.service.ts +++ b/src/app/services/auth.service.ts @@ -1,37 +1,42 @@ import { HttpClient, HttpHeaders } from '@angular/common/http'; import { Injectable } from '@angular/core'; -import { Observable, tap } from 'rxjs'; +import { BehaviorSubject, Observable, tap } from 'rxjs'; import { environment } from '../../environments/environment'; +import * as CryptoJS from 'crypto-js'; export interface AuthResponse { access_token: string; token_type: string; } - @Injectable({ providedIn: 'root' }) export class AuthService { + private isAuthenticated = new BehaviorSubject(this.getToken() !== null); constructor(private http: HttpClient) { } - login(): Observable { + login(username: string, password: string): Observable { const headers = new HttpHeaders().set('x-api-key', environment.angelosAppApiKey); - if (environment.angelosAppApiKey.length === 0) { - console.log('Please provide a valid API key'); - } else { - console.log(environment.angelosAppApiKey.at(0)); - } - - return this.http.post(environment.angelosToken, {}, { headers }).pipe( + const body = { username: username, password: password }; + return this.http.post(environment.angelosToken, body, { headers }).pipe( tap((response: AuthResponse) => { sessionStorage.setItem('access_token', response.access_token); + this.isAuthenticated.next(true); }) ); } - // Method to retrieve the stored token - public getToken(): string | null { + logout(): void { + sessionStorage.removeItem('access_token'); + this.isAuthenticated.next(false); + } + + getToken(): string | null { return sessionStorage.getItem('access_token'); } + + isLoggedIn(): Observable { + return this.isAuthenticated.asObservable(); + } } diff --git a/src/app/services/chatbot.service.ts b/src/app/services/chatbot.service.ts index ff416ec..3c6733f 100644 --- a/src/app/services/chatbot.service.ts +++ b/src/app/services/chatbot.service.ts @@ -25,13 +25,7 @@ export class ChatbotService { if (token) { return this.sendBotRequest(token, chatHistory, study_program); } else { - // Login if no token is stored, then proceed with the bot request - return this.authService.login().pipe( - switchMap(() => { - const newToken = this.authService.getToken(); - return this.sendBotRequest(newToken, chatHistory, study_program); - }) - ); + throw new Error('No token found. Access should have been restricted by AuthGuard.'); } } } diff --git a/src/app/utils/auth.guard.ts b/src/app/utils/auth.guard.ts new file mode 100644 index 0000000..8a9b8a5 --- /dev/null +++ b/src/app/utils/auth.guard.ts @@ -0,0 +1,22 @@ +import { Injectable } from '@angular/core'; +import { CanActivate, Router } from '@angular/router'; +import { Observable } from 'rxjs'; +import { map, tap } from 'rxjs/operators'; +import { AuthService } from '../services/auth.service'; + +@Injectable({ + providedIn: 'root' +}) +export class AuthGuard implements CanActivate { + constructor(private authService: AuthService, private router: Router) { } + + canActivate(): Observable { + return this.authService.isLoggedIn().pipe( + tap(isLoggedIn => { + if (!isLoggedIn) { + this.router.navigate(['/login']); + } + }) + ); + } +}