Commit 60b89930 authored by Almouhannad's avatar Almouhannad

(F) Add authentication service, add login and logout to header

parent a029e1d6
......@@ -3,9 +3,9 @@ import { RouterModule, Routes } from '@angular/router';
import { HomeComponent } from './components/home/home.component';
const routes: Routes = [
{ path: '', component: HomeComponent }
{ path: '', redirectTo: 'home', pathMatch: 'full' },
{ path: 'home', component: HomeComponent }
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
......
......@@ -13,6 +13,8 @@ import { FooterComponent } from './components/template/footer/footer.component';
import { HomeComponent } from './components/home/home.component';
import { LoginFormComponent } from './components/Authentication/login-form/login-form.component';
import { FormsModule } from '@angular/forms';
import { HttpClientModule } from '@angular/common/http';
import { AuthenticationService } from './services/authentication/authentication.service';
@NgModule({
imports: [
......@@ -22,12 +24,15 @@ import { FormsModule } from '@angular/forms';
NgbModule,
ToastrModule.forRoot(),
FormsModule,
HttpClientModule,
],
// creators of services that this module contributes to the
// global collection of services; they become accessible in
// all parts of the app
providers: [],
providers: [
AuthenticationService,
],
// components and directives that belong to this module
// the subset of declarations that should be visible and usable in
......
export class LoginCommand {
export class LoginCommand
{
public userName!: string;
public password!: string;
}
export class LoginResponse
{
public jwt!: string;
}
export class LoginResult {
constructor(status: boolean, errorMessage?: string)
{
this.status = status;
this.errorMessage = errorMessage;
}
public status: Boolean;
public errorMessage?: string;
}
\ No newline at end of file
export class Roles {
public static readonly Admin: string = "admin";
public static readonly Doctor: string = "Doctor";
public static readonly Receptionist: string = "receptionist";
}
\ No newline at end of file
export class UserData {
public id: number;
public userName!: string;
public role!: string;
public fullName?: string;
constructor(id: number, userName: string, role: string, fullName?: string){
this.id = id;
this.userName = userName;
this.role = role;
this.fullName = fullName;
}
}
\ No newline at end of file
<div class="custom-child" dir="rtl">
<div>
<div class="text-center" >
<div class="text-center">
<h3 style="font-weight: 800;">تسجيل الدخول</h3>
</div>
<hr>
<div *ngIf="isFailure" class="d-grid mb-2">
<button type="button" class="btn btn-danger" style="cursor: auto;">خطأ: {{errorMessage}}</button>
</div>
<form #loginForm="ngForm" (ngSubmit)="onSubmit()" class="text-center" autocomplete="off">
<!-- To avoid first field auto focus -->
......@@ -15,36 +19,34 @@
</div>
<div class="form-group mb-4">
<label for="username" class="col-form-label custom-label mb-2">اسم المستخدم <span class="text-danger">*</span></label>
<input type="text" class="form-control text-center custom-input"
placeholder="ادخل اسم المستخدم" dir="ltr"
[(ngModel)]="formModel.userName" name="userName"
#userName="ngModel" required maxlength="50"
>
<label for="username" class="col-form-label custom-label mb-2">اسم المستخدم <span
class="text-danger">*</span></label>
<input type="text" class="form-control text-center custom-input" placeholder="ادخل اسم المستخدم"
dir="ltr" [(ngModel)]="formModel.userName" name="userName" #userName="ngModel" required
maxlength="50">
<div *ngIf="(userName.touched || userName.dirty) && userName.errors" class="mt-2">
<span class="text-danger">
{{
userName.errors['required'] ? 'هذا الحقل مطلوب'
: ''
userName.errors['required'] ? 'هذا الحقل مطلوب'
: ''
}}
</span>
</div>
</div>
<div class="form-group mb-4">
<label for="password" class="col-form-label custom-label mb-2">كلمة المرور <span class="text-danger">*</span></label>
<input type="password" class="form-control text-center custom-input mb-2"
placeholder="ادخل كلمة المرور" dir="ltr"
[(ngModel)]="formModel.password" name="password"
#password="ngModel" required maxlength="50"
>
<label for="password" class="col-form-label custom-label mb-2">كلمة المرور <span
class="text-danger">*</span></label>
<input type="password" class="form-control text-center custom-input mb-2" placeholder="ادخل كلمة المرور"
dir="ltr" [(ngModel)]="formModel.password" name="password" #password="ngModel" required
maxlength="50">
<div *ngIf="(password.touched || password.dirty) && password.errors" class="mt-2">
<span class="text-danger">
{{
password.errors['required'] ? 'هذا الحقل مطلوب'
: ''
password.errors['required'] ? 'هذا الحقل مطلوب'
: ''
}}
</span>
</div>
......@@ -52,12 +54,11 @@
</div>
<div class="d-grid gap-3">
<button
type="submit" class="btn btn-outline-primary"
[disabled]="!loginForm.dirty || loginForm.invalid">دخول</button>
<button type="submit" class="btn btn-outline-primary"
[disabled]="!loginForm.dirty || loginForm.invalid">دخول</button>
<button type="button" class="btn btn-outline-secondary custom-cancel-button"
(click)="parentModal.dismiss()">الغاء</button>
(click)="parentModal.dismiss()">الغاء</button>
</div>
</form>
</div>
......
import { Component, Input, ViewChild } from '@angular/core';
import { Component, EventEmitter, Input, Output, ViewChild } from '@angular/core';
import { NgForm } from '@angular/forms';
import { LoginCommand } from '../../../classes/Authentication/login-command';
import { LoginCommand } from '../../../classes/Authentication/Login/login-command';
import { AuthenticationService } from '../../../services/authentication/authentication.service';
import { UserData } from '../../../classes/Authentication/user-data';
@Component({
selector: 'app-login-form',
......@@ -9,18 +11,48 @@ import { LoginCommand } from '../../../classes/Authentication/login-command';
})
export class LoginFormComponent {
//#region CTOR DI
constructor(private authenticationService: AuthenticationService) {}
//#endregion
//#region Inputs
@Input("parentModal") parentModal : any;
//#endregion
//#region Outputs
@Output("loggedIn") loggedIn: EventEmitter<UserData> = new EventEmitter();
//#endregion
//#region Variables
@ViewChild("loginForm") loginForm: NgForm;
formModel: LoginCommand = new LoginCommand();
isFailure: boolean = false;
errorMessage: string = '';
//#endregion
//#region On submit
onSubmit(): void {}
onSubmit(): void
{
if (this.loginForm.valid){
this.isFailure = false;
this.errorMessage = '';
this.authenticationService.login(this.formModel)
.subscribe(
result => {
if (result.status === false){
this.isFailure = true;
this.errorMessage = result.errorMessage!;
}
else{
this.loggedIn.emit(this.authenticationService.getUserData()!);
}
}
);
this.loginForm.form.markAsPristine();
}
}
//#endregion
}
......@@ -15,21 +15,7 @@
<div class="content row gy-4">
<div class="col-lg-4 d-flex align-items-stretch">
<div class="why-box">
<h3 class="text-center">تسجيل الدخول</h3>
<p class="text-center text-light">
للاستفادة من خدمات هذا التطبيق، يحب عليك تسجيل الدخول أولاً
</p>
<div class="text-center">
<button (click)="openLoginForm(loginModal)" class="btn more-btn">
<a>تسجيل الدخول<i class="bi bi-chevron-right"></i></a>
</button>
</div>
</div>
</div>
<div class="col-lg-8 d-flex align-items-stretch">
<div class="col-lg-11 d-flex align-items-stretch">
<div class="d-flex flex-column justify-content-center">
<div class="row gy-4">
......@@ -37,7 +23,7 @@
<div class="col-xl-4 d-flex align-items-stretch">
<div class="icon-box">
<i class="bi bi-clipboard-data"></i>
<h4>التاريخ المرضي</h4>
<h4>استعراض التاريخ المرضي</h4>
<p class="text-center" style="font-weight: 500; color:var(--heading-color);">يوفر
التطبيق امكانية استعراض التاريخ المرضي لموظفي المركز وأفراد عائلتهم
</p>
......@@ -47,9 +33,9 @@
<div class="col-xl-4 d-flex align-items-stretch">
<div class="icon-box">
<i class="bi bi-graph-up"></i>
<h4>احصائيات</h4>
<h4>استخلاص الاحصائيات الطبيّة</h4>
<p class="text-center" style="font-weight: 500; color:var(--heading-color);">من
مزايا هذا التطبيق امكانية عرض احصائيات طبيّة لموظفي المركز
مزايا هذا التطبيق امكانية عرض احصائيات طبيّة عن موظفي المركز
</p>
</div>
</div>
......@@ -57,12 +43,11 @@
<div class="col-xl-4 d-flex align-items-stretch">
<div class="icon-box">
<i class="bi bi-clock-history"></i>
<h4>دور الانتظار</h4>
<h4>معالجة قائمة الانتظار</h4>
<p style="font-weight: 500; color:var(--heading-color);">يتيح هذا التطبيق إمكانية
تنظيم أدوار انتظار المرضى </p>
</div>
</div>
</div>
</div>
</div>
......@@ -199,12 +184,4 @@
</section>
<!-- #endregion -->
<!-- #region login pop-up -->
<ng-template #loginModal let-modal>
<div class="modal-body">
<app-login-form [parentModal]="modal"></app-login-form>
</div>
</ng-template>
<!-- #endregion -->
</div>
\ No newline at end of file
import { Component } from '@angular/core';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
@Component({
selector: 'app-home',
templateUrl: './home.component.html',
styleUrl: './home.component.css'
})
export class HomeComponent {
export class HomeComponent{
//#region CTOR DI
constructor (private modalService: NgbModal) {}
//#endregion
//#region Login
openLoginForm (content: any): void {
this.modalService.open(content);
}
//#endregion
}
......@@ -5,21 +5,40 @@
color: var(--heading-color);
}
.custom-buttons .btn-outline-primary{
font-weight: 650;
.custom-buttons .btn-outline-primary {
font-weight: 700;
}
.custom-buttons .btn-outline-secondary {
color: var(--heading-color);
font-weight: 650;
font-weight: 700;
border: none;
}
.custom-buttons .btn-outline-secondary:hover {
box-shadow: inset 0 0 0 2px gray ;
box-shadow: inset 0 0 0 2px gray;
background-color: inherit;
}
.navmenu .dropdown ul li a {
cursor: pointer;
font-weight: 700;
}
.navmenu .dropdown ul li a {
display: block;
text-align: center;
}
.navmenu .dropdown ul li:not(:last-child) {
border-bottom: 1px solid var(--heading-color);
}
.custom-selected-dropdown {
box-shadow: inset 0 0 0 2px gray;
}
/* #endregion */
/* #region Header*/
......@@ -219,7 +238,7 @@
.navmenu .dropdown ul {
margin: 0;
padding: 10px 0;
background: var(--nav-dropdown-background-color);
background: var(--background-color);
display: block;
position: absolute;
visibility: hidden;
......@@ -253,22 +272,15 @@
color: var(--nav-dropdown-hover-color);
}
.navmenu .dropdown:hover>ul {
opacity: 1;
.navmenu .dropdown ul {
opacity: 0;
top: 100%;
visibility: visible;
}
.navmenu .dropdown .dropdown ul {
top: 0;
left: -90%;
visibility: hidden;
}
.navmenu .dropdown .dropdown:hover>ul {
.navmenu .dropdown ul.show {
opacity: 1;
top: 0;
left: -100%;
top: 100%;
visibility: visible;
}
}
......
<div class="custom-child">
<div class="header">
<div class="branding d-flex align-items-center">
......@@ -10,16 +11,34 @@
<h1 class="h3 m-auto custom-app-name">المركز الطبي - برنامج الأطباء</h1>
<nav class="navmenu custom-buttons">
<ul>
<li><a href="#about"><button class="btn"
<li *ngIf="userData!==null" class="dropdown">
<button
[ngClass]="{'btn': true, 'btn-outline-secondary': true, 'custom-selected-dropdown': showDropdown}"
(click)="showDropdown = !showDropdown">خيارات <i
class="bi bi-chevron-down toggle-dropdown"></i></button>
<ul [ngClass]="{'show': showDropdown}">
<li (click)="onLogout();"><a>تسجيل الخروج</a></li>
</ul>
</li>
<li *ngIf="userData===null"><a><button class="btn"
[class]="{'btn-outline-secondary': true}"
(click)="openLoginForm(loginModal)">تسجيل الدخول</button></a></li>
<li><a href="home#about"><button class="btn"
[class]="{'btn-outline-primary': isSelected('About'), 'btn-outline-secondary': !isSelected('About')}"
(click)="selectButton('About')">عن المستوصف</button></a></li>
<li><a href="#doctors"><button class="btn"
<li><a href="home#doctors"><button class="btn"
[class]="{'btn-outline-primary': isSelected('Doctors'), 'btn-outline-secondary': !isSelected('Doctors')}"
(click)="selectButton('Doctors')">الفريق الطبي</button></a></li>
<li><a href="#"><button class="btn"
<li><a href="home#"><button class="btn"
[class]="{'btn-outline-primary': isSelected('Home'), 'btn-outline-secondary': !isSelected('Home')}"
(click)="selectButton('Home')">الصفحة الرئيسية</button></a></li>
</ul>
</nav>
</div>
......@@ -27,4 +46,13 @@
</div>
</div>
<!-- #region login pop-up -->
<ng-template #loginModal let-modal>
<div *ngIf="userData===null" class="modal-body">
<app-login-form (loggedIn)="onLogin($event); modal.dismiss();" [parentModal]="modal"></app-login-form>
</div>
</ng-template>
<!-- #endregion -->
</div>
\ No newline at end of file
import { Component } from '@angular/core';
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { UserData } from '../../../classes/Authentication/user-data';
import { AuthenticationService } from '../../../services/authentication/authentication.service';
import { Router } from '@angular/router';
import { HomeComponent } from '../../home/home.component';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
@Component({
selector: 'app-header',
......@@ -7,6 +12,50 @@ import { Component } from '@angular/core';
})
export class HeaderComponent {
//#region CTOR DI
constructor(private authenticationService: AuthenticationService,
private router: Router,
private modalService: NgbModal) { }
//#endregion
//#region Inputs
@Input("userData") userData: UserData | null = null;
//#endregion
//#region Variables
//#region Dropdown
showDropdown: boolean = false;
//#endregion
//#endregion
//#region Login
//#region Login form pop-up
openLoginForm(content: any): void {
this.modalService.open(content);
}
//#endregion
//#region Login event
onLogin(userData: UserData)
{
this.userData = userData;
}
//#endregion
//#endregion
//#region On logout
onLogout(): void {
this.authenticationService.logout();
this.userData = null;
this.router.navigate(['']);
}
//#endregion
//#region Selected button
private selectedButton: string = 'Home';
......
<app-header class="sticky-top"></app-header>
<app-header class="sticky-top" [userData]="userData"></app-header>
<router-outlet></router-outlet>
......
import { Component } from '@angular/core';
import { Component, OnInit } from '@angular/core';
import { UserData } from '../../../classes/Authentication/user-data';
import { AuthenticationService } from '../../../services/authentication/authentication.service';
@Component({
selector: 'app-layout',
templateUrl: './layout.component.html',
styleUrl: './layout.component.css'
})
export class LayoutComponent {
export class LayoutComponent implements OnInit {
//#region CTOR DI
constructor(private authenticationService: AuthenticationService){}
//#endregion
//#region On init
ngOnInit(): void {
this.userData = this.authenticationService.getUserData();
}
//#endregion
//#region Variables
userData: UserData | null = null;
//#endregion
}
import { Injectable } from '@angular/core';
import { AppModule } from '../../app.module';
import { HttpClient, HttpErrorResponse, HttpHeaders, HttpResponse } from '@angular/common/http';
import * as config from "../../../../config";
import { LoginCommand } from '../../classes/Authentication/Login/login-command';
import { LoginResponse } from '../../classes/Authentication/Login/login-response';
import { catchError, map, Observable, of } from 'rxjs';
import { LoginResult } from '../../classes/Authentication/Login/login-result';
import { JWTHandler } from './jwtHandler';
import { UserData } from '../../classes/Authentication/user-data';
import { Roles } from '../../classes/Authentication/roles';
@Injectable({
providedIn: AppModule
})
export class AuthenticationService {
//#region CTOR DI
constructor(private http: HttpClient) { }
//#endregion
//#region Constants
private readonly USERS_ENDPOINT: string = `${config.apiUrl}/Users`
//#region HTTP headers
private readonly HTTP_HEADERS: HttpHeaders = this.getHeaders();
getHeaders(): HttpHeaders {
return new HttpHeaders().set('Content-Type', 'application/json');
}
//#endregion
//#endregion
//#region Login
private postLogin(loginCommand: LoginCommand): Observable<LoginResponse> {
return this.http.post<LoginResponse>(
this.USERS_ENDPOINT, loginCommand, { headers: this.HTTP_HEADERS });
}
login(loginCommand: LoginCommand): Observable<LoginResult> {
return this.postLogin(loginCommand).pipe(
map((loginResponse: LoginResponse) => {
JWTHandler.storeJWT(loginResponse.jwt);
return new LoginResult(true, '');
}),
catchError((error: HttpErrorResponse) => {
return of(new LoginResult(false, error.error.detail));
})
);
}
//#endregion
//#region Logout
logout(): void {
JWTHandler.deleteJwtCookie();
}
//#endregion
//#region Get user data
getUserData(): UserData | null{
let jwt = JWTHandler.getJwtFromCookie();
if (jwt)
{
let jwtJson = JWTHandler.decodeJWT(jwt);
let id = jwtJson.id;
let userName = jwtJson.userName;
let role = jwtJson.role;
let fullName = jwtJson.fullName;
if (fullName)
return new UserData(id, userName, role, fullName);
if (role == Roles.Admin)
return new UserData(id, userName, role, Roles.Admin);
return null;
}
return null;
}
//#endregion
}
export class JWTHandler {
//#region JWT Decoder
public static decodeJWT(jwt: string) {
const payload = jwt.split('.')[1];
const decodedPayload = atob(payload);
const textDecoder = new TextDecoder('utf-8');
const claimsJson = JSON.parse(
textDecoder.decode(
new Uint8Array(
decodedPayload.split('').map(c => c.charCodeAt(0))
)
)
);
return claimsJson;
}
//#endregion
//#region Sotre JWT in a cookie
public static storeJWT(jwt: string): void {
const claims = this.decodeJWT(jwt);
const expiresAt = claims.exp * 1000; // convert seconds to milliseconds
const cookieOptions = {
expires: new Date(expiresAt),
path: '/'
};
let cookieString = `jwt=${jwt};`;
if (cookieOptions.expires) {
cookieString += `expires=${cookieOptions.expires.toUTCString()};`;
}
if (cookieOptions.path) {
cookieString += `path=${cookieOptions.path};`;
}
document.cookie = cookieString;
}
//#endregion
//#region Get JWT from cookie
public static getJwtFromCookie(): string | null {
const cookieString = document.cookie;
const cookiePairs = cookieString.split(';');
for (const cookie of cookiePairs) {
const [key, value] = cookie.trim().split('=');
if (key === 'jwt') {
return value
}
}
return null;
}
//#endregion
//#region Delete JWT cookie
public static deleteJwtCookie(): void {
document.cookie = 'jwt=; expires=Thu, 01 Jan 1970 00:00:00 GMT;';
}
//#endregion
}
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment