Unverified Commit 92cd910a authored by Almouhannad Hafez's avatar Almouhannad Hafez Committed by GitHub

Merge pull request #12 from Almouhannad/F_Add-authentication-service

F add authentication service
parents 9bec7b49 92c4497b
using Domain.Entities.Identity.Users; using Domain.Entities.Identity.Users;
using Domain.Entities.People.Shared;
namespace Application.Abstractions.JWT; namespace Application.Abstractions.JWT;
public interface IJWTProvider public interface IJWTProvider
{ {
string Generate(User user); string Generate(User user, PersonalInfo? personalInfo = null);
} }
...@@ -33,37 +33,38 @@ public class LoginCommandHandler : CommandHandlerBase<LoginCommand, LoginRespons ...@@ -33,37 +33,38 @@ public class LoginCommandHandler : CommandHandlerBase<LoginCommand, LoginRespons
return Result.Failure<LoginResponse>(IdentityErrors.PasswordMismatch); return Result.Failure<LoginResponse>(IdentityErrors.PasswordMismatch);
#endregion #endregion
#region 2. Generate JWT
User user = loginResult.Value!; User user = loginResult.Value!;
string token = _jwtProvider.Generate(user);
#endregion
#region 3. Generate Response #region 2. Generate Response
#region 3.1. Admin #region 2.1. Admin
if (user.Role == Roles.Admin) if (user.Role == Roles.Admin)
{ {
return LoginResponse.GetResponse(user, token); var token = _jwtProvider.Generate(user);
return LoginResponse.GetResponse(token);
} }
#endregion #endregion
#region 3.2. Doctor #region 2.2. Doctor
if (user.Role == Roles.Doctor) if (user.Role == Roles.Doctor)
{ {
var doctorUserResult = await _userRepository.GetDoctorUserByUserNameFullAsync(user.UserName); var doctorUserResult = await _userRepository.GetDoctorUserByUserNameFullAsync(user.UserName);
if (doctorUserResult.IsFailure) if (doctorUserResult.IsFailure)
return Result.Failure<LoginResponse>(doctorUserResult.Error); return Result.Failure<LoginResponse>(doctorUserResult.Error);
return LoginResponse.GetResponse(doctorUserResult.Value.User, token, doctorUserResult.Value.Doctor.PersonalInfo);
var token = _jwtProvider.Generate(user, doctorUserResult.Value.Doctor.PersonalInfo);
return LoginResponse.GetResponse(token);
} }
#endregion #endregion
#region 3.3. Receptionist user #region 2.3. Receptionist user
if (user.Role == Roles.Receptionist) if (user.Role == Roles.Receptionist)
{ {
var receptionistUser = await _userRepository.GetReceptionistUserByUserNameFullAsync(user.UserName); var receptionistUser = await _userRepository.GetReceptionistUserByUserNameFullAsync(user.UserName);
if (receptionistUser.IsFailure) if (receptionistUser.IsFailure)
return Result.Failure<LoginResponse>(receptionistUser.Error); return Result.Failure<LoginResponse>(receptionistUser.Error);
return LoginResponse.GetResponse(receptionistUser.Value.User, token, receptionistUser.Value.PersonalInfo); var token = _jwtProvider.Generate(user, receptionistUser.Value.PersonalInfo);
return LoginResponse.GetResponse(token);
} }
#endregion #endregion
......
...@@ -8,27 +8,16 @@ namespace Application.Users.Commands.Login; ...@@ -8,27 +8,16 @@ namespace Application.Users.Commands.Login;
public class LoginResponse public class LoginResponse
{ {
public int Id { get; set; }
public string UserName { get; set; } = null!;
public string JWT { get; set; } = null!; public string JWT { get; set; } = null!;
public string FullName { get; set; } = null!;
public static Result<LoginResponse> GetResponse(User user, string jwt, PersonalInfo? personalInfo = null) public static Result<LoginResponse> GetResponse(string jwt)
{ {
if (jwt is null)
return Result.Failure< LoginResponse>(IdentityErrors.NotFound);
var response = new LoginResponse var response = new LoginResponse
{ {
Id = user.Id,
UserName = user.UserName,
JWT = jwt JWT = jwt
}; };
if (personalInfo is null)
{
if (user.Role != Roles.Admin)
return Result.Failure<LoginResponse>(IdentityErrors.NotFound);
response.FullName = Roles.AdminName;
return response;
}
response.FullName = personalInfo.FullName;
return response; return response;
} }
} }
using Application.Abstractions.JWT; using Application.Abstractions.JWT;
using Domain.Entities.Identity.Users; using Domain.Entities.Identity.Users;
using Domain.Entities.People.Shared;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens; using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt; using System.IdentityModel.Tokens.Jwt;
...@@ -17,15 +18,20 @@ public sealed class JWTProvider : IJWTProvider ...@@ -17,15 +18,20 @@ public sealed class JWTProvider : IJWTProvider
_options = options.Value; _options = options.Value;
} }
public string Generate(User user) public string Generate(User user, PersonalInfo? personalInfo = null)
{ {
var claims = new Claim[] var claims = new List<Claim>
{ {
new(ClaimTypes.Name, user.UserName), new("id", user.Id.ToString()),
new(ClaimTypes.Role, user.Role.Name) new("userName", user.UserName),
new("role", user.Role.Name)
}; };
if (personalInfo is not null)
{
claims.Add(new Claim("fullName", personalInfo.FullName));
}
var signingCredentials = new SigningCredentials( var signingCredentials = new SigningCredentials(
new SymmetricSecurityKey( new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(_options.SecretKey)), Encoding.UTF8.GetBytes(_options.SecretKey)),
...@@ -34,7 +40,7 @@ public sealed class JWTProvider : IJWTProvider ...@@ -34,7 +40,7 @@ public sealed class JWTProvider : IJWTProvider
var token = new JwtSecurityToken( var token = new JwtSecurityToken(
_options.Issuer, _options.Issuer,
_options.Audience, _options.Audience,
claims, claims.ToArray(),
null, null,
DateTime.UtcNow.AddDays(30), DateTime.UtcNow.AddDays(30),
signingCredentials); signingCredentials);
......
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router'; import { RouterModule, Routes } from '@angular/router';
import { HomeComponent } from './components/home/home.component'; import { HomeComponent } from './components/home/home.component';
import { RoleGuard } from './services/authentication/guards/role-guard';
import { Roles } from './classes/Authentication/roles';
import { ForbiddenComponent } from './components/errors/forbidden/forbidden.component';
import { NotFoundComponent } from './components/errors/not-found/not-found.component';
const routes: Routes = [ const routes: Routes = [
{ path: '', component: HomeComponent } {
]; path: '',
redirectTo: 'home',
pathMatch: 'full',
},
{
path: 'home',
component: HomeComponent,
canActivate: [RoleGuard],
data: { role: Roles.NotRegistered }
},
{
path: 'errors/forbidden',
component: ForbiddenComponent,
canActivate: [RoleGuard],
data: { role: Roles.NotRegistered }
},
// Everything else
{
path: '**',
component: NotFoundComponent
}
];
@NgModule({ @NgModule({
imports: [RouterModule.forRoot(routes)], imports: [RouterModule.forRoot(routes)],
exports: [RouterModule] exports: [RouterModule]
......
...@@ -13,6 +13,12 @@ import { FooterComponent } from './components/template/footer/footer.component'; ...@@ -13,6 +13,12 @@ import { FooterComponent } from './components/template/footer/footer.component';
import { HomeComponent } from './components/home/home.component'; import { HomeComponent } from './components/home/home.component';
import { LoginFormComponent } from './components/Authentication/login-form/login-form.component'; import { LoginFormComponent } from './components/Authentication/login-form/login-form.component';
import { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms';
import { HTTP_INTERCEPTORS, HttpClientModule } from '@angular/common/http';
import { AuthenticationService } from './services/authentication/authentication.service';
import { AuthenticationInterceptor } from './services/authentication/interceptor/authentication.interceptor';
import { ForbiddenComponent } from './components/errors/forbidden/forbidden.component';
import { NotFoundComponent } from './components/errors/not-found/not-found.component';
@NgModule({ @NgModule({
imports: [ imports: [
...@@ -22,12 +28,16 @@ import { FormsModule } from '@angular/forms'; ...@@ -22,12 +28,16 @@ import { FormsModule } from '@angular/forms';
NgbModule, NgbModule,
ToastrModule.forRoot(), ToastrModule.forRoot(),
FormsModule, FormsModule,
HttpClientModule,
], ],
// creators of services that this module contributes to the // creators of services that this module contributes to the
// global collection of services; they become accessible in // global collection of services; they become accessible in
// all parts of the app // all parts of the app
providers: [], providers: [
AuthenticationService,
{ provide: HTTP_INTERCEPTORS, useClass: AuthenticationInterceptor, multi: true},
],
// components and directives that belong to this module // components and directives that belong to this module
// the subset of declarations that should be visible and usable in // the subset of declarations that should be visible and usable in
...@@ -38,7 +48,9 @@ import { FormsModule } from '@angular/forms'; ...@@ -38,7 +48,9 @@ import { FormsModule } from '@angular/forms';
HeaderComponent, HeaderComponent,
FooterComponent, FooterComponent,
HomeComponent, HomeComponent,
LoginFormComponent LoginFormComponent,
ForbiddenComponent,
NotFoundComponent
], ],
// identifies the root component that Angular should // identifies the root component that Angular should
......
export class LoginCommand { export class LoginCommand
{
public userName!: string; public userName!: string;
public password!: 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";
public static readonly NotRegistered: string = "notRegistered";
}
\ 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 class="custom-child" dir="rtl">
<div> <div>
<div class="text-center" > <div class="text-center">
<h3 style="font-weight: 800;">تسجيل الدخول</h3> <h3 style="font-weight: 800;">تسجيل الدخول</h3>
</div> </div>
<hr> <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"> <form #loginForm="ngForm" (ngSubmit)="onSubmit()" class="text-center" autocomplete="off">
<!-- To avoid first field auto focus --> <!-- To avoid first field auto focus -->
...@@ -15,36 +19,34 @@ ...@@ -15,36 +19,34 @@
</div> </div>
<div class="form-group mb-4"> <div class="form-group mb-4">
<label for="username" class="col-form-label custom-label mb-2">اسم المستخدم <span class="text-danger">*</span></label> <label for="username" class="col-form-label custom-label mb-2">اسم المستخدم <span
<input type="text" class="form-control text-center custom-input" class="text-danger">*</span></label>
placeholder="ادخل اسم المستخدم" dir="ltr" <input type="text" class="form-control text-center custom-input" placeholder="ادخل اسم المستخدم"
[(ngModel)]="formModel.userName" name="userName" dir="ltr" [(ngModel)]="formModel.userName" name="userName" #userName="ngModel" required
#userName="ngModel" required maxlength="50" maxlength="50">
>
<div *ngIf="(userName.touched || userName.dirty) && userName.errors" class="mt-2"> <div *ngIf="(userName.touched || userName.dirty) && userName.errors" class="mt-2">
<span class="text-danger"> <span class="text-danger">
{{ {{
userName.errors['required'] ? 'هذا الحقل مطلوب' userName.errors['required'] ? 'هذا الحقل مطلوب'
: '' : ''
}} }}
</span> </span>
</div> </div>
</div> </div>
<div class="form-group mb-4"> <div class="form-group mb-4">
<label for="password" class="col-form-label custom-label mb-2">كلمة المرور <span class="text-danger">*</span></label> <label for="password" class="col-form-label custom-label mb-2">كلمة المرور <span
<input type="password" class="form-control text-center custom-input mb-2" class="text-danger">*</span></label>
placeholder="ادخل كلمة المرور" dir="ltr" <input type="password" class="form-control text-center custom-input mb-2" placeholder="ادخل كلمة المرور"
[(ngModel)]="formModel.password" name="password" dir="ltr" [(ngModel)]="formModel.password" name="password" #password="ngModel" required
#password="ngModel" required maxlength="50" maxlength="50">
>
<div *ngIf="(password.touched || password.dirty) && password.errors" class="mt-2"> <div *ngIf="(password.touched || password.dirty) && password.errors" class="mt-2">
<span class="text-danger"> <span class="text-danger">
{{ {{
password.errors['required'] ? 'هذا الحقل مطلوب' password.errors['required'] ? 'هذا الحقل مطلوب'
: '' : ''
}} }}
</span> </span>
</div> </div>
...@@ -52,12 +54,11 @@ ...@@ -52,12 +54,11 @@
</div> </div>
<div class="d-grid gap-3"> <div class="d-grid gap-3">
<button <button type="submit" class="btn btn-outline-primary"
type="submit" class="btn btn-outline-primary" [disabled]="!loginForm.dirty || loginForm.invalid">دخول</button>
[disabled]="!loginForm.dirty || loginForm.invalid">دخول</button>
<button type="button" class="btn btn-outline-secondary custom-cancel-button" <button type="button" class="btn btn-outline-secondary custom-cancel-button"
(click)="parentModal.dismiss()">الغاء</button> (click)="parentModal.dismiss()">الغاء</button>
</div> </div>
</form> </form>
</div> </div>
......
import { Component, Input, ViewChild } from '@angular/core'; import { Component, EventEmitter, Input, Output, ViewChild } from '@angular/core';
import { NgForm } from '@angular/forms'; 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({ @Component({
selector: 'app-login-form', selector: 'app-login-form',
...@@ -9,18 +11,48 @@ import { LoginCommand } from '../../../classes/Authentication/login-command'; ...@@ -9,18 +11,48 @@ import { LoginCommand } from '../../../classes/Authentication/login-command';
}) })
export class LoginFormComponent { export class LoginFormComponent {
//#region CTOR DI
constructor(private authenticationService: AuthenticationService) {}
//#endregion
//#region Inputs //#region Inputs
@Input("parentModal") parentModal : any; @Input("parentModal") parentModal : any;
//#endregion //#endregion
//#region Outputs
@Output("loggedIn") loggedIn: EventEmitter<UserData> = new EventEmitter();
//#endregion
//#region Variables //#region Variables
@ViewChild("loginForm") loginForm: NgForm; @ViewChild("loginForm") loginForm: NgForm;
formModel: LoginCommand = new LoginCommand(); formModel: LoginCommand = new LoginCommand();
isFailure: boolean = false;
errorMessage: string = '';
//#endregion //#endregion
//#region On submit //#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 //#endregion
} }
<div class="cutom-child text-center" style="padding: 6em;">
<i class="bi bi-exclamation-triangle-fill text-danger" style="font-size: 4em;"></i>
<h1 class="text-danger text-center" style="font-family: Cairo;">خطأ: لا يمكنك الوصول إلى الصفحة التي طلبتها</h1>
</div>
\ No newline at end of file
import { Component } from '@angular/core';
@Component({
selector: 'app-forbidden',
templateUrl: './forbidden.component.html',
styleUrl: './forbidden.component.css'
})
export class ForbiddenComponent {
}
<div class="cutom-child text-center" style="padding: 6em;">
<i class="bi bi-exclamation-triangle-fill text-danger" style="font-size: 4em;"></i>
<h1 class="text-danger text-center" style="font-family: Cairo;">خطأ: الصفحة المطلوبة غير موجودة</h1>
</div>
\ No newline at end of file
import { Component } from '@angular/core';
@Component({
selector: 'app-not-found',
templateUrl: './not-found.component.html',
styleUrl: './not-found.component.css'
})
export class NotFoundComponent {
}
...@@ -15,21 +15,7 @@ ...@@ -15,21 +15,7 @@
<div class="content row gy-4"> <div class="content row gy-4">
<div class="col-lg-4 d-flex align-items-stretch"> <div class="col-lg-11 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="d-flex flex-column justify-content-center"> <div class="d-flex flex-column justify-content-center">
<div class="row gy-4"> <div class="row gy-4">
...@@ -37,7 +23,7 @@ ...@@ -37,7 +23,7 @@
<div class="col-xl-4 d-flex align-items-stretch"> <div class="col-xl-4 d-flex align-items-stretch">
<div class="icon-box"> <div class="icon-box">
<i class="bi bi-clipboard-data"></i> <i class="bi bi-clipboard-data"></i>
<h4>التاريخ المرضي</h4> <h4>استعراض التاريخ المرضي</h4>
<p class="text-center" style="font-weight: 500; color:var(--heading-color);">يوفر <p class="text-center" style="font-weight: 500; color:var(--heading-color);">يوفر
التطبيق امكانية استعراض التاريخ المرضي لموظفي المركز وأفراد عائلتهم التطبيق امكانية استعراض التاريخ المرضي لموظفي المركز وأفراد عائلتهم
</p> </p>
...@@ -47,9 +33,9 @@ ...@@ -47,9 +33,9 @@
<div class="col-xl-4 d-flex align-items-stretch"> <div class="col-xl-4 d-flex align-items-stretch">
<div class="icon-box"> <div class="icon-box">
<i class="bi bi-graph-up"></i> <i class="bi bi-graph-up"></i>
<h4>احصائيات</h4> <h4>استخلاص الاحصائيات الطبيّة</h4>
<p class="text-center" style="font-weight: 500; color:var(--heading-color);">من <p class="text-center" style="font-weight: 500; color:var(--heading-color);">من
مزايا هذا التطبيق امكانية عرض احصائيات طبيّة لموظفي المركز مزايا هذا التطبيق امكانية عرض احصائيات طبيّة عن موظفي المركز
</p> </p>
</div> </div>
</div> </div>
...@@ -57,12 +43,11 @@ ...@@ -57,12 +43,11 @@
<div class="col-xl-4 d-flex align-items-stretch"> <div class="col-xl-4 d-flex align-items-stretch">
<div class="icon-box"> <div class="icon-box">
<i class="bi bi-clock-history"></i> <i class="bi bi-clock-history"></i>
<h4>دور الانتظار</h4> <h4>معالجة قائمة الانتظار</h4>
<p style="font-weight: 500; color:var(--heading-color);">يتيح هذا التطبيق إمكانية <p style="font-weight: 500; color:var(--heading-color);">يتيح هذا التطبيق إمكانية
تنظيم أدوار انتظار المرضى </p> تنظيم أدوار انتظار المرضى </p>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
...@@ -199,12 +184,4 @@ ...@@ -199,12 +184,4 @@
</section> </section>
<!-- #endregion --> <!-- #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> </div>
\ No newline at end of file
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
@Component({ @Component({
selector: 'app-home', selector: 'app-home',
templateUrl: './home.component.html', templateUrl: './home.component.html',
styleUrl: './home.component.css' 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,46 @@ ...@@ -5,21 +5,46 @@
color: var(--heading-color); color: var(--heading-color);
} }
.custom-buttons .btn-outline-primary{ .custom-buttons .btn-outline-primary {
font-weight: 650; font-weight: 700;
} }
.custom-buttons .btn-outline-secondary { .custom-buttons .btn-outline-secondary {
color: var(--heading-color); color: var(--heading-color);
font-weight: 650; font-weight: 700;
border: none; border: none;
} }
.custom-buttons .btn-outline-secondary:hover { .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; 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;
}
.app-name-container {
text-align: center; /* or any other alignment you prefer */
}
/* #endregion */ /* #endregion */
/* #region Header*/ /* #region Header*/
...@@ -219,7 +244,7 @@ ...@@ -219,7 +244,7 @@
.navmenu .dropdown ul { .navmenu .dropdown ul {
margin: 0; margin: 0;
padding: 10px 0; padding: 10px 0;
background: var(--nav-dropdown-background-color); background: var(--background-color);
display: block; display: block;
position: absolute; position: absolute;
visibility: hidden; visibility: hidden;
...@@ -253,22 +278,15 @@ ...@@ -253,22 +278,15 @@
color: var(--nav-dropdown-hover-color); color: var(--nav-dropdown-hover-color);
} }
.navmenu .dropdown:hover>ul { .navmenu .dropdown ul {
opacity: 1; opacity: 0;
top: 100%; top: 100%;
visibility: visible;
}
.navmenu .dropdown .dropdown ul {
top: 0;
left: -90%;
visibility: hidden; visibility: hidden;
} }
.navmenu .dropdown .dropdown:hover>ul { .navmenu .dropdown ul.show {
opacity: 1; opacity: 1;
top: 0; top: 100%;
left: -100%;
visibility: visible; visibility: visible;
} }
} }
......
<div class="custom-child"> <div class="custom-child">
<div class="header"> <div class="header">
<div class="branding d-flex align-items-center"> <div class="branding d-flex align-items-center">
...@@ -7,24 +8,119 @@ ...@@ -7,24 +8,119 @@
<img src="assets/images/logo.png" alt="Logo" width="100em" height="100em"> <img src="assets/images/logo.png" alt="Logo" width="100em" height="100em">
</div> </div>
<h1 class="h3 m-auto custom-app-name">المركز الطبي - برنامج الأطباء</h1> <div class="container app-name-container m-auto">
<h1 class="h3 custom-app-name mb-4">المركز الطبي - برنامج الأطباء</h1>
<h3 dir="rtl" *ngIf="userData!==null" class="h5 text-primary">أهلاَ <span *ngIf="userData.role===DOCTOR">د.</span> {{userData.fullName}}!</h3>
</div>
<!-- #region unregistered buttons -->
<nav class="navmenu custom-buttons" *ngIf="userData === null">
<nav class="navmenu custom-buttons">
<ul> <ul>
<li><a href="#about"><button class="btn" <li><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')}" [class]="{'btn-outline-primary': isSelected('About'), 'btn-outline-secondary': !isSelected('About')}"
(click)="selectButton('About')">عن المستوصف</button></a></li> (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')}" [class]="{'btn-outline-primary': isSelected('Doctors'), 'btn-outline-secondary': !isSelected('Doctors')}"
(click)="selectButton('Doctors')">الفريق الطبي</button></a></li> (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>
<!-- #endregion -->
<!-- #region admin buttons -->
<nav class="navmenu custom-buttons" *ngIf="userData?.role === ADMIN">
<ul>
<li 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><a><button class="btn"
[class]="{'btn-outline-primary': isSelected('Admin-Receptionists'), 'btn-outline-secondary': !isSelected('Admin-Receptionists')}"
(click)="selectButton('Admin-Receptionists')">موظفو الاستقبال</button></a></li>
<li><a><button class="btn"
[class]="{'btn-outline-primary': isSelected('Admin-Dorctors'), 'btn-outline-secondary': !isSelected('Admin-Dorctors')}"
(click)="selectButton('Admin-Dorctors')">الأطباء</button></a></li>
<li><a [routerLink]="'home'"><button class="btn"
[class]="{'btn-outline-primary': isSelected('Home'), 'btn-outline-secondary': !isSelected('Home')}" [class]="{'btn-outline-primary': isSelected('Home'), 'btn-outline-secondary': !isSelected('Home')}"
(click)="selectButton('Home')">الصفحة الرئيسية</button></a></li> (click)="selectButton('Home')">الصفحة الرئيسية</button></a></li>
</ul> </ul>
</nav> </nav>
<!-- #endregion -->
<!-- #region doctor buttons -->
<nav class="navmenu custom-buttons" *ngIf="userData?.role === DOCTOR">
<ul>
<li 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><a [routerLink]="'home'"><button class="btn"
[class]="{'btn-outline-primary': isSelected('Home'), 'btn-outline-secondary': !isSelected('Home')}"
(click)="selectButton('Home')">الصفحة الرئيسية</button></a></li>
</ul>
</nav>
<!-- #endregion -->
<!-- #region receptionist buttons -->
<nav class="navmenu custom-buttons" *ngIf="userData?.role === RECEPTIONIST">
<ul>
<li 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><a [routerLink]="'home'"><button class="btn"
[class]="{'btn-outline-primary': isSelected('Home'), 'btn-outline-secondary': !isSelected('Home')}"
(click)="selectButton('Home')">الصفحة الرئيسية</button></a></li>
</ul>
</nav>
<!-- #endregion -->
</div> </div>
</div> </div>
</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> </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 { Roles } from '../../../classes/Authentication/roles';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
@Component({ @Component({
selector: 'app-header', selector: 'app-header',
...@@ -7,6 +12,57 @@ import { Component } from '@angular/core'; ...@@ -7,6 +12,57 @@ import { Component } from '@angular/core';
}) })
export class HeaderComponent { 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 Roles
readonly ADMIN: string = Roles.Admin;
readonly DOCTOR: string = Roles.Doctor;
readonly RECEPTIONIST: string = Roles.Receptionist;
//#endregion
//#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.showDropdown = false;
this.router.navigate(['']);
}
//#endregion
//#region Selected button //#region Selected button
private selectedButton: string = 'Home'; private selectedButton: string = 'Home';
......
/* #region Custom */
.custom-parent {
display: flex;
flex-direction:column;
min-height:100vh;
}
app-footer {
margin-top: auto;
}
/* #endregion */
/* #region General */ /* #region General */
::ng-deep .custom-child{ ::ng-deep .custom-child{
color: var(--default-color); color: var(--default-color);
......
<app-header class="sticky-top"></app-header> <div class="custom-parent">
<app-header class="sticky-top" [userData]="userData"></app-header>
<router-outlet></router-outlet> <router-outlet></router-outlet>
<app-footer></app-footer>
</div>
<app-footer></app-footer>
\ No newline at end of file
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({ @Component({
selector: 'app-layout', selector: 'app-layout',
templateUrl: './layout.component.html', templateUrl: './layout.component.html',
styleUrl: './layout.component.css' 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`
//#endregion
//#region Login
private postLogin(loginCommand: LoginCommand): Observable<LoginResponse> {
return this.http.post<LoginResponse>(
this.USERS_ENDPOINT, loginCommand);
}
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
}
import { Injectable } from '@angular/core';
import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, Router } from '@angular/router';
import { AuthenticationService } from '../authentication.service';
import { Roles } from '../../../classes/Authentication/roles';
import { UserData } from '../../../classes/Authentication/user-data';
@Injectable({
providedIn: 'root'
})
export class RoleGuard implements CanActivate {
constructor(private authenticationService: AuthenticationService, private router: Router) { }
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
const userData: UserData | null = this.authenticationService.getUserData();
const requiredRole: string = route.data['role'];
// No required role
if (!requiredRole)
return true;
// No required role
if (requiredRole === Roles.NotRegistered)
return true;
// Reauired role, but not registered user
if (!userData)
{
this.router.navigate(["errors/forbidden"]);
return false;
}
if (userData.role !== requiredRole)
{
this.router.navigate(["errors/forbidden"]);
return false;
}
return true;
}
}
\ No newline at end of file
import { Injectable } from '@angular/core';
import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent, HttpHeaders } from '@angular/common/http';
import { JWTHandler } from '../jwtHandler';
import { Observable } from 'rxjs';
@Injectable()
export class AuthenticationInterceptor implements HttpInterceptor {
//#region HTTP headers
private readonly HTTP_HEADERS: HttpHeaders = this.getHeaders();
getHeaders(): HttpHeaders {
return new HttpHeaders().set('Content-Type', 'application/json');
}
//#endregion
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const jwt = JWTHandler.getJwtFromCookie();
// console.log(req);
req = req.clone({
headers: this.HTTP_HEADERS
});
if (jwt !== null) {
req = req.clone({
setHeaders: {
Authorization: `Bearer ${jwt}`
}
});
}
// console.log(req);
return next.handle(req);
}
}
\ No newline at end of file
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