diff --git a/Backend/.env_example b/Backend/.env_example index ccca008f..73a55dc7 100644 --- a/Backend/.env_example +++ b/Backend/.env_example @@ -1,3 +1,4 @@ TEST_STAGE=local PASSWORD=SomePassword +SECRET_KEY=Some_random_32byte_key MONGODB_URL=mongodb://localhost:27017/ \ No newline at end of file diff --git a/Backend/app/api/v1/auth_api.py b/Backend/app/api/v1/auth_api.py new file mode 100644 index 00000000..2b91b669 --- /dev/null +++ b/Backend/app/api/v1/auth_api.py @@ -0,0 +1,70 @@ +import os +from dotenv import load_dotenv +from fastapi import APIRouter, Depends, HTTPException, Response +from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm +from app.repository.user_repository import UserRepository +from app.dependency.repository import get_user_repository +from datetime import datetime, timedelta +from jose import JWTError, jwt + +router = APIRouter() +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") + +load_dotenv() +SECRET_KEY = os.getenv("SECRET_KEY") +ALGORITHM = "HS256" + + +def create_access_token(data: dict, expires_delta: timedelta = None): + to_encode = data.copy() + if expires_delta: + expire = datetime.utcnow() + expires_delta + else: + expire = datetime.utcnow() + timedelta(minutes=15) + to_encode.update({"exp": expire}) + encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) + return encoded_jwt + + +@router.post("/token") +async def login_for_access_token( + response: Response, + form_data: OAuth2PasswordRequestForm = Depends(), + user_repo: UserRepository = Depends(get_user_repository), +): + # check userdata + is_authenticated = user_repo.authenticate_user( + email=form_data.username, password=form_data.password + ) + if not is_authenticated: + raise HTTPException( + status_code=402, + detail="Incorrect email or password", + headers={"WWW-Authenticate": "Bearer"}, + ) + # read user data + user_data = user_repo.read_users_by_email(email=form_data.username) + if not user_data: + raise HTTPException(status_code=404, detail="User not found") + + # take first element, because email_adresse should be unique + user = user_data[0] + + access_token_expires = timedelta(minutes=60) + access_token = create_access_token( + data={"sub": user["email_address"]}, expires_delta=access_token_expires + ) + response.set_cookie(key="access_token", value=access_token, httponly=True) + return {"access_token": access_token, "token_type": "bearer", "success": True} + + +@router.get("/verify-token") +async def verify_token(token: str = Depends(oauth2_scheme)): + try: + payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) + email = payload.get("sub") + if email is None: + raise HTTPException(status_code=401, detail="Invalid token") + return {"email": email} + except JWTError: + raise HTTPException(status_code=401, detail="Invalid token") diff --git a/Backend/app/main.py b/Backend/app/main.py index e51427fb..5b1c8fa9 100644 --- a/Backend/app/main.py +++ b/Backend/app/main.py @@ -2,6 +2,8 @@ from fastapi.middleware.cors import CORSMiddleware from app.api.v1 import ticket_api +from app.api.v1 import auth_api + from app.dependency.collection import ( get_user_collection, get_service_collection, @@ -16,6 +18,7 @@ get_category_repository, get_location_repository, ) + from app.repository.user_repository import UserRepository from app.repository.service_repository import ServiceRepository from app.repository.location_repository import LocationRepository @@ -47,6 +50,9 @@ # Include the router from the text_endpoint module app.include_router(ticket_api.router, prefix="/api/v1") +# Include the router from the auth_endpoint module +app.include_router(auth_api.router, prefix="/api/v1") + @app.on_event("startup") async def startup_event(): diff --git a/Backend/app/repository/user_repository.py b/Backend/app/repository/user_repository.py index eec2dd42..b88a382d 100644 --- a/Backend/app/repository/user_repository.py +++ b/Backend/app/repository/user_repository.py @@ -32,3 +32,8 @@ def read_users_by_email(self, email: str) -> list[UserEntity]: logger.info(f"Reading user(s) with email {email} from the database...") users = list(self.collection.find({"email_address": email})) return users if users else [] + + def authenticate_user(self, email: str, password: str) -> bool: + logger.info(f"Authenticating user with email {email}...") + user = self.collection.find_one({"email_address": email, "password": password}) + return user is not None diff --git a/Backend/requirements.txt b/Backend/requirements.txt index 0847781c..05253dd3 100644 --- a/Backend/requirements.txt +++ b/Backend/requirements.txt @@ -13,4 +13,5 @@ fastapi-utils==0.2.1 requests==2.31.0 python-multipart==0.0.6 beautifulsoup4==4.12.2 -openai==1.7.2 \ No newline at end of file +openai==1.7.2 +python-jose==3.3.0 \ No newline at end of file diff --git a/Backend/test/api/v1/auth_api_test.py b/Backend/test/api/v1/auth_api_test.py new file mode 100644 index 00000000..11b357fc --- /dev/null +++ b/Backend/test/api/v1/auth_api_test.py @@ -0,0 +1,73 @@ +import os +from datetime import datetime, timedelta +from dotenv import load_dotenv +from jose import JWTError, jwt +import pytest +from fastapi.testclient import TestClient +from unittest.mock import Mock +from test.config.pytest import SKIP_TEST +from app.main import app +from app.dependency.repository import get_user_repository + + +load_dotenv() +SECRET_KEY = os.getenv("SECRET_KEY") + + +# Mock dependencies +@pytest.fixture +def mock_user_repository(): + user_repo = Mock() + user_repo.authenticate_user.return_value = True + user_repo.read_users_by_email.return_value = [{"email_address": "test@example.com"}] + return user_repo + + +@pytest.fixture +def client(mock_user_repository): + app.dependency_overrides[get_user_repository] = lambda: mock_user_repository + return TestClient(app) + + +def generate_valid_token(): + ALGORITHM = "HS256" + data = {"sub": "test@example.com"} + expire = datetime.utcnow() + timedelta(minutes=15) + to_encode = data.copy() + to_encode.update({"exp": expire}) + return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) + + +@pytest.mark.skipif(condition=SKIP_TEST, reason=".env on git") +class TestAPI: + def test_login_for_access_token_success(self, client): + response = client.post( + "/api/v1/token", + data={"username": "user@example.com", "password": "password"}, + ) + assert response.status_code == 200 + assert "access_token" in response.json() + + def test_login_for_access_token_failure(self, client, mock_user_repository): + # Configure the mock to return False for authentication + mock_user_repository.authenticate_user.return_value = False + response = client.post( + "/api/v1/token", + data={"username": "wrong@example.com", "password": "wrongpassword"}, + ) + assert response.status_code == 402 + + def test_verify_token_success(self, client): + valid_token = generate_valid_token() + response = client.get( + "/api/v1/verify-token", headers={"Authorization": f"Bearer {valid_token}"} + ) + assert response.status_code == 200 + + def test_verify_token_failure(self, client): + # Use an altered valid token for this test + valid_token = generate_valid_token() + "invalid_part" + response = client.get( + "/api/v1/verify-token", headers={"Authorization": f"Bearer {valid_token}"} + ) + assert response.status_code == 401 diff --git a/Backend/test/repository/ticket_repository_test.py b/Backend/test/repository/ticket_repository_test.py index 63529f55..d2062ca2 100644 --- a/Backend/test/repository/ticket_repository_test.py +++ b/Backend/test/repository/ticket_repository_test.py @@ -33,6 +33,7 @@ def setUp(self): description="", priority=Prio.low, attachments=[], + requestType="", ) def test_create_ticket(self): @@ -98,6 +99,7 @@ def setUp(self): "description": "", "priority": Prio.low, "attachments": [], + "requestType": "", } def test_crud(self): diff --git a/Frontend/package-lock.json b/Frontend/package-lock.json index 19bc8c98..e2c9fc3b 100644 --- a/Frontend/package-lock.json +++ b/Frontend/package-lock.json @@ -19,6 +19,8 @@ "@angular/platform-browser-dynamic": "^16.2.0", "@angular/router": "^16.2.0", "cors": "^2.8.5", + "jsonwebtoken": "^9.0.2", + "jwt-decode": "^4.0.0", "rxjs": "~7.8.0", "stylelint": "^15.11.0", "tslib": "^2.3.0", @@ -29,6 +31,7 @@ "@angular/cli": "^16.2.9", "@angular/compiler-cli": "^16.2.0", "@types/jasmine": "~4.3.0", + "@types/jsonwebtoken": "^9.0.5", "@typescript-eslint/eslint-plugin": "^6.10.0", "@typescript-eslint/parser": "^6.10.0", "eslint": "^8.53.0", @@ -4556,6 +4559,15 @@ "integrity": "sha512-U3PUjAudAdJBeC2pgN8uTIKgxrb4nlDF3SF0++EldXQvQBGkpFZMSnwQiIoDU77tv45VgNkl/L4ouD+rEomujw==", "dev": true }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.5.tgz", + "integrity": "sha512-VRLSGzik+Unrup6BsouBeHsf4d1hOEgYWTm/7Nmw1sXoN1+tRly/Gy/po3yeahnP4jfnQWWAhQAqcNfH7ngOkA==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/mime": { "version": "1.3.4", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.4.tgz", @@ -5803,6 +5815,11 @@ "ieee754": "^1.1.13" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -7026,6 +7043,14 @@ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "dev": true }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -8130,9 +8155,9 @@ "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==" }, "node_modules/follow-redirects": { - "version": "1.15.3", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz", - "integrity": "sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==", + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", + "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==", "dev": true, "funding": [ { @@ -9652,6 +9677,54 @@ "node >= 0.2.0" ] }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jwt-decode": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz", + "integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==", + "engines": { + "node": ">=18" + } + }, "node_modules/karma": { "version": "6.4.2", "resolved": "https://registry.npmjs.org/karma/-/karma-6.4.2.tgz", @@ -10020,12 +10093,47 @@ "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", "dev": true }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" + }, "node_modules/lodash.truncate": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", @@ -12925,7 +13033,6 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, "funding": [ { "type": "github", diff --git a/Frontend/package.json b/Frontend/package.json index 1e3b3e4c..7f27c2c7 100644 --- a/Frontend/package.json +++ b/Frontend/package.json @@ -23,6 +23,8 @@ "@angular/platform-browser-dynamic": "^16.2.0", "@angular/router": "^16.2.0", "cors": "^2.8.5", + "jsonwebtoken": "^9.0.2", + "jwt-decode": "^4.0.0", "rxjs": "~7.8.0", "stylelint": "^15.11.0", "tslib": "^2.3.0", @@ -33,6 +35,7 @@ "@angular/cli": "^16.2.9", "@angular/compiler-cli": "^16.2.0", "@types/jasmine": "~4.3.0", + "@types/jsonwebtoken": "^9.0.5", "@typescript-eslint/eslint-plugin": "^6.10.0", "@typescript-eslint/parser": "^6.10.0", "eslint": "^8.53.0", diff --git a/Frontend/src/app/app.component.css b/Frontend/src/app/app.component.css index 7e2673a2..904079fa 100644 --- a/Frontend/src/app/app.component.css +++ b/Frontend/src/app/app.component.css @@ -1,3 +1,13 @@ +body{ + display: flex; + flex-direction: column; +} + +.login-container { + align-self: flex-end; + margin-right: 20px; +} + .cards-container { display: flex; justify-content: space-between; diff --git a/Frontend/src/app/app.component.html b/Frontend/src/app/app.component.html index d733adc1..fe52f683 100644 --- a/Frontend/src/app/app.component.html +++ b/Frontend/src/app/app.component.html @@ -7,7 +7,7 @@ text-align: center; font-size: 5em; margin-top: 30px; - margin-bottom: 30px; + margin-bottom: 10px; margin-right: 80px; size: 45px; " @@ -15,6 +15,15 @@ {{ title }} +
+ +
+
+ +
+
diff --git a/Frontend/src/app/app.component.ts b/Frontend/src/app/app.component.ts index 0a225077..2dc0f6b3 100644 --- a/Frontend/src/app/app.component.ts +++ b/Frontend/src/app/app.component.ts @@ -1,10 +1,15 @@ -import { Component, OnInit, ViewChild, ElementRef, ChangeDetectorRef } from '@angular/core'; -import { TicketService } from './service/ticket.service'; -import { LogService } from './service/logging.service'; -import { DragAndDropComponent } from './drag-and-drop/drag-and-drop.component'; +import {Component, OnInit, ViewChild, ElementRef, ChangeDetectorRef} from '@angular/core'; +import {TicketService} from './service/ticket.service'; +import {LogService} from './service/logging.service'; +import {DragAndDropComponent} from './drag-and-drop/drag-and-drop.component'; import {Ticket} from "./entities/ticket.dto"; -import { MatDialog } from '@angular/material/dialog'; -import { RequestTypeDialogComponent } from './request-type-dialog/request-type-dialog.component'; +import {MatDialog} from '@angular/material/dialog'; +import {RequestTypeDialogComponent} from './request-type-dialog/request-type-dialog.component'; +import {LoginDialogComponent} from './login-dialog/login-dialog.component'; +import {jwtDecode} from "jwt-decode"; +import {HttpClient} from '@angular/common/http'; +import { AuthService } from './service/auth.service'; + interface ChatMessages { messageText: string; @@ -40,13 +45,30 @@ export class AppComponent implements OnInit { recognitionTimeout: any; selectedRequestType: string = ''; createdTicket: Ticket | undefined; + isLoggedIn: boolean = false; + accessToken: string | null = ''; @ViewChild("fileDropRef", { static: true }) fileDropEl!: ElementRef; @ViewChild(DragAndDropComponent) dragAndDropComponent!: DragAndDropComponent; - constructor(private ticketService: TicketService, private logger: LogService, private changeDetector: ChangeDetectorRef, private dialog: MatDialog,) {} - - ngOnInit() {} + constructor( + private ticketService: TicketService, + private logger: LogService, + private changeDetector: ChangeDetectorRef, + private dialog: MatDialog, + private http: HttpClient, + private authService: AuthService, + ) {} + + ngOnInit() { + this.authService.checkTokenValidity(); + this.accessToken = localStorage.getItem("access_token") ? localStorage.getItem("access_token") : null; + if (this.accessToken != null) { + this.isLoggedIn = true; + let email = jwtDecode(this.accessToken).sub; + this.emailInput = email ? email : ''; + } + } getFiles(event: any) { this.files = event; @@ -68,6 +90,15 @@ export class AppComponent implements OnInit { this.files = []; } + openLoginDialog() { + const dialogRef = this.dialog.open(LoginDialogComponent); + dialogRef.afterClosed().subscribe(result => { + // logic after closing dialog + this.emailInput = result.email; + this.isLoggedIn = true; + }); + } + chooseRequestType() { const dialogRef = this.dialog.open(RequestTypeDialogComponent); dialogRef.afterClosed().subscribe(result => { @@ -128,6 +159,16 @@ export class AppComponent implements OnInit { } handleSend(value: string, emailInput: string) { + this.authService.checkTokenValidity(); + this.accessToken = localStorage.getItem("access_token") ? localStorage.getItem("access_token") : null; + if (this.accessToken != null) { + this.isLoggedIn = true; + let email = jwtDecode(this.accessToken).sub; + this.emailInput = email ? email : ''; + }else{ + this.logout() + } + this.errorMessage = ""; if (!value) { @@ -249,4 +290,10 @@ export class AppComponent implements OnInit { this.errorMessage = errorMessage; this.logger.error(errorMessage); } + + logout() { + localStorage.removeItem("access_token"); + this.isLoggedIn = false; + this.emailInput = ''; + } } diff --git a/Frontend/src/app/app.module.ts b/Frontend/src/app/app.module.ts index ba73fa40..22e8f531 100644 --- a/Frontend/src/app/app.module.ts +++ b/Frontend/src/app/app.module.ts @@ -5,17 +5,20 @@ import { AppComponent } from './app.component'; import { FormsModule } from '@angular/forms'; import { HttpClientModule } from '@angular/common/http'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; -import {MatButtonModule} from "@angular/material/button"; -import {MatInputModule} from "@angular/material/input"; +import { MatButtonModule } from "@angular/material/button"; +import { MatInputModule } from "@angular/material/input"; +import { MatIconModule } from '@angular/material/icon'; import { DragAndDropModule } from './drag-and-drop/drag-and-drop.module'; import { MatCardModule } from '@angular/material/card'; import { MatDialogModule } from '@angular/material/dialog'; import { RequestTypeDialogComponent } from './request-type-dialog/request-type-dialog.component'; +import { LoginDialogComponent } from './login-dialog/login-dialog.component'; @NgModule({ declarations: [ AppComponent, - RequestTypeDialogComponent + RequestTypeDialogComponent, + LoginDialogComponent ], imports: [ BrowserModule, @@ -27,7 +30,9 @@ import { RequestTypeDialogComponent } from './request-type-dialog/request-type-d MatInputModule, DragAndDropModule, MatCardModule, - MatDialogModule + MatDialogModule, + MatInputModule, + MatIconModule, ], providers: [], bootstrap: [AppComponent] diff --git a/Frontend/src/app/login-dialog/login-dialog.component.css b/Frontend/src/app/login-dialog/login-dialog.component.css new file mode 100644 index 00000000..40ea6e52 --- /dev/null +++ b/Frontend/src/app/login-dialog/login-dialog.component.css @@ -0,0 +1,66 @@ +:host { + --main-bg-color: #313131; + --accent-color: #10ddd3; + --hover-color: #00828f; + --text-color: #fff; + } + + .dialog-title{ + background-color: var(--main-bg-color); + text-align: center; + } + + .dialog-content { + background-color: var(--main-bg-color); + text-align: center; + width: 400px; + } + + .dialog-title { + font-size: 24px; + color: var(--accent-color); + margin-bottom: 20px; + } + + .error-message { + font-size: 16px; + color: red; + margin: 15px; + } + + .dialog-message { + font-size: 16px; + color: var(--text-color); + } + + mat-dialog-content { + display: flex; + flex-direction: column; + } + + .mat-form-field { + margin-bottom: 20px; + } + + .close-button { + position: absolute; + top: 10px; + right: 10px; + color: white; /* oder die Farbe Ihrer Wahl */ + } + + .login-button { + background-color: var(--accent-color); + color: var(--text-color); + font-size: 18px; + text-transform: capitalize; + padding: 10px 20px; + margin: 10px; + margin-top: 20px; + cursor: pointer; + transition: background-color 0.3s ease-in-out; + } + + .login-button:hover { + background-color: var(--hover-color); + } \ No newline at end of file diff --git a/Frontend/src/app/login-dialog/login-dialog.component.html b/Frontend/src/app/login-dialog/login-dialog.component.html new file mode 100644 index 00000000..9c7447d2 --- /dev/null +++ b/Frontend/src/app/login-dialog/login-dialog.component.html @@ -0,0 +1,20 @@ + +

Login

+ + +
+ {{ errorMessage }} +
+ + E-Mail + + + + Password + + + +
+ \ No newline at end of file diff --git a/Frontend/src/app/login-dialog/login-dialog.component.ts b/Frontend/src/app/login-dialog/login-dialog.component.ts new file mode 100644 index 00000000..fbe7075a --- /dev/null +++ b/Frontend/src/app/login-dialog/login-dialog.component.ts @@ -0,0 +1,41 @@ +import {Component} from '@angular/core'; +import {MatDialogRef} from '@angular/material/dialog'; +import {AuthService} from '../service/auth.service'; + +@Component({ + selector: 'app-login-dialog', + templateUrl: './login-dialog.component.html', + styleUrls: ['./login-dialog.component.css'] +}) +export class LoginDialogComponent { + email: string = ''; + password: string = ''; + errorMessage: string = ''; + loading: boolean = false; + + constructor(private authService: AuthService, private dialogRef: MatDialogRef) { + dialogRef.disableClose = true; + } + closeDialog() { + this.dialogRef.close(); + } + + login() { + this.loading = true; + this.authService.login(this.email, this.password).subscribe( + response => { + + this.loading = false; + if (response.success) { + this.dialogRef.close({ email: this.email }); + } else { + this.errorMessage = 'Login data not correct.'; + } + }, + error => { + this.loading = false; + this.errorMessage = 'An error has occurred. Please try again later.'; + } + ); + } +} diff --git a/Frontend/src/app/request-type-dialog/request-type-dialog.component.css b/Frontend/src/app/request-type-dialog/request-type-dialog.component.css new file mode 100644 index 00000000..a91c327c --- /dev/null +++ b/Frontend/src/app/request-type-dialog/request-type-dialog.component.css @@ -0,0 +1,37 @@ +:host { + --main-bg-color: #313131; + --accent-color: #10ddd3; + --hover-color: #00828f; + --text-color: #fff; + } + + .dialog-title, .dialog-content { + background-color: var(--main-bg-color); + text-align: center; + } + + .dialog-title { + font-size: 24px; + color: var(--accent-color); + margin-bottom: 20px; + } + + .dialog-message { + font-size: 16px; + color: var(--text-color); + } + + .request-type-button { + background-color: var(--accent-color); + color: var(--text-color); + font-size: 18px; + text-transform: capitalize; + padding: 10px 20px; + margin: 10px; + cursor: pointer; + transition: background-color 0.3s ease-in-out; + } + + .request-type-button:hover { + background-color: var(--hover-color); + } diff --git a/Frontend/src/app/request-type-dialog/request-type-dialog.component.html b/Frontend/src/app/request-type-dialog/request-type-dialog.component.html new file mode 100644 index 00000000..632eca4c --- /dev/null +++ b/Frontend/src/app/request-type-dialog/request-type-dialog.component.html @@ -0,0 +1,8 @@ + +

Please choose a request type

+ +

We couldn't automatically detect the request type from your message. Please select the appropriate request type:

+ + +
+ \ No newline at end of file diff --git a/Frontend/src/app/request-type-dialog/request-type-dialog.component.ts b/Frontend/src/app/request-type-dialog/request-type-dialog.component.ts index 2439b089..67ce36cb 100644 --- a/Frontend/src/app/request-type-dialog/request-type-dialog.component.ts +++ b/Frontend/src/app/request-type-dialog/request-type-dialog.component.ts @@ -1,57 +1,10 @@ -import { Component } from '@angular/core'; -import { MatDialogRef } from '@angular/material/dialog'; +import {Component} from '@angular/core'; +import {MatDialogRef} from '@angular/material/dialog'; @Component({ selector: 'app-request-type-dialog', - template: ` - -

Please choose a request type

- -

We couldn't automatically detect the request type from your message. Please select the appropriate request type:

- - -
- - `, - styles: [` - :host { - --main-bg-color: #313131; - --accent-color: #10ddd3; - --hover-color: #00828f; - --text-color: #fff; - } - - .dialog-title, .dialog-content { - background-color: var(--main-bg-color); - text-align: center; - } - - .dialog-title { - font-size: 24px; - color: var(--accent-color); - margin-bottom: 20px; - } - - .dialog-message { - font-size: 16px; - color: var(--text-color); - } - - .request-type-button { - background-color: var(--accent-color); - color: var(--text-color); - font-size: 18px; - text-transform: capitalize; - padding: 10px 20px; - margin: 10px; - cursor: pointer; - transition: background-color 0.3s ease-in-out; - } - - .request-type-button:hover { - background-color: var(--hover-color); - } - `] + templateUrl: './request-type-dialog.component.html', + styleUrls: ['./request-type-dialog.component.css'] }) export class RequestTypeDialogComponent { constructor(private dialogRef: MatDialogRef) { diff --git a/Frontend/src/app/service/auth.service.ts b/Frontend/src/app/service/auth.service.ts new file mode 100644 index 00000000..381b23dd --- /dev/null +++ b/Frontend/src/app/service/auth.service.ts @@ -0,0 +1,69 @@ +import {Injectable} from '@angular/core'; +import {HttpClient} from '@angular/common/http'; +import {Observable, throwError} from 'rxjs'; +import {tap, catchError} from 'rxjs/operators'; +import {LogService} from './logging.service'; +import {environment} from "../../environments/environment"; +import {of} from 'rxjs'; + +@Injectable({ + providedIn: 'root' +}) +export class AuthService { + private apiUrl = environment.apiUrl + 'api/v1/token'; + private apiUrl1 = environment.apiUrl + 'api/v1/verify-token'; + + constructor(private http: HttpClient, private logger: LogService) {} + + login(email: string, password: string): Observable { + const loginData = new FormData(); + loginData.append('username', email); + loginData.append('password', password); + + return this.http.post<{ access_token: string }>(this.apiUrl, loginData) + .pipe( + tap((response: { access_token: string }) => { + // Save accessToken + localStorage.setItem('access_token', response.access_token); + this.logger.log('Login successful:'+ response); + this.checkLoginStatus(); + }), + catchError((error: any) => { + let errorMessage = 'An error has occurred. Please try again later.'; + if (error.status === 400) { + errorMessage = 'Login data not correct.'; + } + this.logger.error('Login failed:'+ error); + return of({ error: true, message: errorMessage }); + }) + ); + } + + checkLoginStatus(): void { + const token = localStorage.getItem('access_token'); + if (token) { + this.logger.log('Token successful: ' + token); + } else { + this.logger.log('No token found, user is not logged in.'); + } + } + checkTokenValidity(): void { + const accessToken = localStorage.getItem('access_token'); + if (accessToken) { + + this.http.get<{ email: string }>(this.apiUrl1, { + headers: { Authorization: `Bearer ${accessToken}` } + }).subscribe({ + next: (response) => { + this.logger.log('Token is valid. Logged in as: ' + response.email); + }, + error: (error) => { + localStorage.removeItem('access_token'); + this.logger.log('Token is invalid or expired. Logged out.'); + } + }); + } else { + this.logger.log('No token found, user is not logged in.'); + } + } +} diff --git a/Frontend/src/app/service/logging.service.ts b/Frontend/src/app/service/logging.service.ts index c9b8fc5e..0c70add7 100644 --- a/Frontend/src/app/service/logging.service.ts +++ b/Frontend/src/app/service/logging.service.ts @@ -1,4 +1,4 @@ -import { Injectable } from "@angular/core"; +import {Injectable} from "@angular/core"; @Injectable({ providedIn: "root"