├── src ├── assets │ ├── .gitkeep │ ├── images │ │ ├── logo.png │ │ └── logo-header.png │ └── icons │ │ ├── icon-72x72.png │ │ ├── icon-96x96.png │ │ ├── icon-128x128.png │ │ ├── icon-144x144.png │ │ ├── icon-152x152.png │ │ ├── icon-192x192.png │ │ ├── icon-384x384.png │ │ └── icon-512x512.png ├── app │ ├── app.component.scss │ ├── pages │ │ ├── pages.component.scss │ │ ├── error-pages │ │ │ ├── contents │ │ │ │ ├── forbidden │ │ │ │ │ ├── forbidden.component.scss │ │ │ │ │ ├── forbidden.component.html │ │ │ │ │ ├── forbidden.component.ts │ │ │ │ │ └── forbidden.component.spec.ts │ │ │ │ ├── not-found │ │ │ │ │ ├── not-found.component.scss │ │ │ │ │ ├── not-found.component.html │ │ │ │ │ ├── not-found.component.ts │ │ │ │ │ └── not-found.component.spec.ts │ │ │ │ └── index.ts │ │ │ ├── error-pages.component.html │ │ │ ├── error-pages.component.scss │ │ │ ├── error-pages.component.ts │ │ │ ├── error-pages.module.ts │ │ │ ├── error-pages.component.spec.ts │ │ │ └── error-pages-routing.module.ts │ │ ├── user-list │ │ │ ├── components │ │ │ │ ├── user-from │ │ │ │ │ ├── user-from.component.scss │ │ │ │ │ ├── user-from.component.spec.ts │ │ │ │ │ ├── user-from.component.html │ │ │ │ │ └── user-from.component.ts │ │ │ │ ├── user-modal │ │ │ │ │ ├── user-modal.component.scss │ │ │ │ │ ├── user-modal.component.html │ │ │ │ │ ├── user-modal.component.spec.ts │ │ │ │ │ └── user-modal.component.ts │ │ │ │ ├── user-table │ │ │ │ │ ├── user-table.component.scss │ │ │ │ │ ├── user-table.component.spec.ts │ │ │ │ │ ├── user-table.component.ts │ │ │ │ │ └── user-table.component.html │ │ │ │ └── index.ts │ │ │ ├── user-list.component.scss │ │ │ ├── user-list.component.html │ │ │ ├── user-list.component.spec.ts │ │ │ └── user-list.component.ts │ │ ├── pages.component.html │ │ ├── home │ │ │ ├── home.component.scss │ │ │ ├── home.component.spec.ts │ │ │ ├── home.component.ts │ │ │ └── home.component.html │ │ ├── auth │ │ │ ├── contents │ │ │ │ ├── index.ts │ │ │ │ ├── login │ │ │ │ │ ├── login.component.scss │ │ │ │ │ ├── login.component.spec.ts │ │ │ │ │ ├── login.component.html │ │ │ │ │ └── login.component.ts │ │ │ │ └── register │ │ │ │ │ ├── register.component.scss │ │ │ │ │ ├── register.component.spec.ts │ │ │ │ │ ├── register.component.html │ │ │ │ │ └── register.component.ts │ │ │ ├── auth.component.ts │ │ │ ├── auth.component.html │ │ │ ├── auth.component.scss │ │ │ ├── auth.module.ts │ │ │ ├── auth.component.spec.ts │ │ │ └── auth-routing.module.ts │ │ ├── pages.component.ts │ │ ├── pages.component.spec.ts │ │ ├── pages-routing.module.ts │ │ └── pages.module.ts │ ├── shared │ │ ├── components │ │ │ ├── navbar │ │ │ │ ├── navbar.component.scss │ │ │ │ ├── navbar.component.ts │ │ │ │ ├── navbar.component.spec.ts │ │ │ │ └── navbar.component.html │ │ │ ├── index.ts │ │ │ └── loading-spinner │ │ │ │ ├── loading-spinner.component.html │ │ │ │ ├── loading-spinner.component.scss │ │ │ │ ├── loading-spinner.component.ts │ │ │ │ └── loading-spinner.component.spec.ts │ │ └── shared.module.ts │ ├── app.component.html │ ├── models │ │ ├── common │ │ │ ├── index.ts │ │ │ └── http.model.ts │ │ ├── notification │ │ │ ├── index.ts │ │ │ └── snack-message.model.ts │ │ └── auth │ │ │ ├── index.ts │ │ │ ├── user.model.ts │ │ │ └── auth-forms.model.ts │ ├── core │ │ ├── services │ │ │ ├── form │ │ │ │ ├── index.ts │ │ │ │ ├── form-validation.service.spec.ts │ │ │ │ └── form-validation.service.ts │ │ │ ├── notifcation │ │ │ │ ├── index.ts │ │ │ │ ├── snack-message.service.spec.ts │ │ │ │ └── snack-message.service.ts │ │ │ ├── auth │ │ │ │ ├── index.ts │ │ │ │ ├── auth.service.spec.ts │ │ │ │ ├── user-list.service.spec.ts │ │ │ │ ├── auth.service.ts │ │ │ │ └── user-list.service.ts │ │ │ └── common │ │ │ │ ├── index.ts │ │ │ │ ├── global-data.service.ts │ │ │ │ ├── api.service.spec.ts │ │ │ │ ├── global-data.service.spec.ts │ │ │ │ ├── loading-spinner.service.spec.ts │ │ │ │ ├── loading-spinner.service.ts │ │ │ │ └── api.service.ts │ │ ├── guards │ │ │ ├── index.ts │ │ │ ├── admin.guard.spec.ts │ │ │ ├── login.guard.spec.ts │ │ │ ├── not-login.guard.spec.ts │ │ │ ├── not-login.guard.ts │ │ │ ├── login.guard.ts │ │ │ └── admin.guard.ts │ │ ├── interceptors │ │ │ ├── index.ts │ │ │ ├── ErrorInterceptor.ts │ │ │ └── AuthInterceptor.ts │ │ └── core.module.ts │ ├── app.component.ts │ ├── app.module.ts │ ├── app-routing.module.ts │ └── app.component.spec.ts ├── favicon.ico ├── styles │ ├── _bootstrap.scss │ └── _angular-material.scss ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── styles.scss ├── main.ts ├── index.html ├── test.ts ├── manifest.webmanifest └── polyfills.ts ├── routes.json ├── screen-shoots ├── auth │ ├── login_screen.png │ ├── register_screen.png │ ├── register_form_valid.png │ └── register_form_validation_error.png ├── customer │ ├── user_menu.png │ └── customer_home_screen.png ├── admin │ ├── admin_edit_user.png │ ├── admin_user_list.png │ ├── admin_delete_user.png │ └── admin_create_new_user.png └── super-admin │ ├── super_admin_edit_user.png │ ├── super_admin_user_list.png │ ├── super_admin_create_user.png │ └── super_admin_delete_user.png ├── .editorconfig ├── tsconfig.app.json ├── tsconfig.spec.json ├── .browserslistrc ├── ngsw-config.json ├── .gitignore ├── tsconfig.json ├── karma.conf.js ├── package.json ├── angular.json ├── README.md └── db.json /src/assets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/app.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/pages/pages.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/shared/components/navbar/navbar.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /routes.json: -------------------------------------------------------------------------------- 1 | { 2 | "users": 400, 3 | "profiles": 660 4 | } 5 | -------------------------------------------------------------------------------- /src/app/pages/error-pages/contents/forbidden/forbidden.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/pages/error-pages/contents/not-found/not-found.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/pages/user-list/components/user-from/user-from.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/pages/user-list/components/user-modal/user-modal.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/pages/user-list/components/user-table/user-table.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/models/common/index.ts: -------------------------------------------------------------------------------- 1 | export { HTTP_REQ, HTTP_RES } from './http.model'; 2 | -------------------------------------------------------------------------------- /src/app/models/notification/index.ts: -------------------------------------------------------------------------------- 1 | export { SNACK_DATA } from './snack-message.model'; 2 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pikselinweb/user-management/HEAD/src/favicon.ico -------------------------------------------------------------------------------- /src/app/core/services/form/index.ts: -------------------------------------------------------------------------------- 1 | export { FormValidationService } from './form-validation.service'; 2 | -------------------------------------------------------------------------------- /src/app/core/services/notifcation/index.ts: -------------------------------------------------------------------------------- 1 | export { SnackMessageService } from './snack-message.service'; 2 | -------------------------------------------------------------------------------- /src/assets/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pikselinweb/user-management/HEAD/src/assets/images/logo.png -------------------------------------------------------------------------------- /src/assets/icons/icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pikselinweb/user-management/HEAD/src/assets/icons/icon-72x72.png -------------------------------------------------------------------------------- /src/assets/icons/icon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pikselinweb/user-management/HEAD/src/assets/icons/icon-96x96.png -------------------------------------------------------------------------------- /screen-shoots/auth/login_screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pikselinweb/user-management/HEAD/screen-shoots/auth/login_screen.png -------------------------------------------------------------------------------- /src/assets/icons/icon-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pikselinweb/user-management/HEAD/src/assets/icons/icon-128x128.png -------------------------------------------------------------------------------- /src/assets/icons/icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pikselinweb/user-management/HEAD/src/assets/icons/icon-144x144.png -------------------------------------------------------------------------------- /src/assets/icons/icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pikselinweb/user-management/HEAD/src/assets/icons/icon-152x152.png -------------------------------------------------------------------------------- /src/assets/icons/icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pikselinweb/user-management/HEAD/src/assets/icons/icon-192x192.png -------------------------------------------------------------------------------- /src/assets/icons/icon-384x384.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pikselinweb/user-management/HEAD/src/assets/icons/icon-384x384.png -------------------------------------------------------------------------------- /src/assets/icons/icon-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pikselinweb/user-management/HEAD/src/assets/icons/icon-512x512.png -------------------------------------------------------------------------------- /src/assets/images/logo-header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pikselinweb/user-management/HEAD/src/assets/images/logo-header.png -------------------------------------------------------------------------------- /screen-shoots/customer/user_menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pikselinweb/user-management/HEAD/screen-shoots/customer/user_menu.png -------------------------------------------------------------------------------- /src/app/pages/pages.component.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 |
5 | -------------------------------------------------------------------------------- /screen-shoots/admin/admin_edit_user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pikselinweb/user-management/HEAD/screen-shoots/admin/admin_edit_user.png -------------------------------------------------------------------------------- /screen-shoots/admin/admin_user_list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pikselinweb/user-management/HEAD/screen-shoots/admin/admin_user_list.png -------------------------------------------------------------------------------- /screen-shoots/auth/register_screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pikselinweb/user-management/HEAD/screen-shoots/auth/register_screen.png -------------------------------------------------------------------------------- /src/app/pages/user-list/user-list.component.scss: -------------------------------------------------------------------------------- 1 | .add-button-wrapper { 2 | position: fixed; 3 | bottom: 20px; 4 | right: 20px; 5 | } 6 | -------------------------------------------------------------------------------- /screen-shoots/admin/admin_delete_user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pikselinweb/user-management/HEAD/screen-shoots/admin/admin_delete_user.png -------------------------------------------------------------------------------- /screen-shoots/auth/register_form_valid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pikselinweb/user-management/HEAD/screen-shoots/auth/register_form_valid.png -------------------------------------------------------------------------------- /src/app/core/services/auth/index.ts: -------------------------------------------------------------------------------- 1 | export { AuthService } from './auth.service'; 2 | export { UserListService } from './user-list.service'; 3 | -------------------------------------------------------------------------------- /screen-shoots/admin/admin_create_new_user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pikselinweb/user-management/HEAD/screen-shoots/admin/admin_create_new_user.png -------------------------------------------------------------------------------- /screen-shoots/customer/customer_home_screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pikselinweb/user-management/HEAD/screen-shoots/customer/customer_home_screen.png -------------------------------------------------------------------------------- /src/app/models/auth/index.ts: -------------------------------------------------------------------------------- 1 | export { LOGIN_FORM_DATA, REGISTER_FORM_DATA } from './auth-forms.model'; 2 | export { USER, PROFILE } from './user.model'; 3 | -------------------------------------------------------------------------------- /src/app/models/notification/snack-message.model.ts: -------------------------------------------------------------------------------- 1 | export interface SNACK_DATA { 2 | message: string; 3 | duration?: number; 4 | action?: string; 5 | } 6 | -------------------------------------------------------------------------------- /screen-shoots/super-admin/super_admin_edit_user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pikselinweb/user-management/HEAD/screen-shoots/super-admin/super_admin_edit_user.png -------------------------------------------------------------------------------- /screen-shoots/super-admin/super_admin_user_list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pikselinweb/user-management/HEAD/screen-shoots/super-admin/super_admin_user_list.png -------------------------------------------------------------------------------- /screen-shoots/auth/register_form_validation_error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pikselinweb/user-management/HEAD/screen-shoots/auth/register_form_validation_error.png -------------------------------------------------------------------------------- /screen-shoots/super-admin/super_admin_create_user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pikselinweb/user-management/HEAD/screen-shoots/super-admin/super_admin_create_user.png -------------------------------------------------------------------------------- /screen-shoots/super-admin/super_admin_delete_user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pikselinweb/user-management/HEAD/screen-shoots/super-admin/super_admin_delete_user.png -------------------------------------------------------------------------------- /src/app/core/guards/index.ts: -------------------------------------------------------------------------------- 1 | export { AdminGuard } from './admin.guard'; 2 | export { LoginGuard } from './login.guard'; 3 | export { NotLoginGuard } from './not-login.guard'; 4 | -------------------------------------------------------------------------------- /src/app/pages/error-pages/error-pages.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 |
6 | -------------------------------------------------------------------------------- /src/app/pages/home/home.component.scss: -------------------------------------------------------------------------------- 1 | .home-card { 2 | .home-header-image { 3 | background-image: url("/assets/icons/icon-96x96.png"); 4 | background-size: cover; 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/app/pages/auth/contents/index.ts: -------------------------------------------------------------------------------- 1 | export { LoginComponent as LoginPage } from './login/login.component'; 2 | export { RegisterComponent as RegisterPage } from './register/register.component'; 3 | -------------------------------------------------------------------------------- /src/app/pages/error-pages/contents/forbidden/forbidden.component.html: -------------------------------------------------------------------------------- 1 |

401 FORBIDDEN

2 |

The page you have requested can't authorized.

3 | Return Home Page 4 | -------------------------------------------------------------------------------- /src/app/pages/error-pages/contents/not-found/not-found.component.html: -------------------------------------------------------------------------------- 1 |

404 NOT FOUND

2 |

The link is broken or page has been removed.

3 | Return Home Page 4 | -------------------------------------------------------------------------------- /src/styles/_bootstrap.scss: -------------------------------------------------------------------------------- 1 | @import "~bootstrap/scss/bootstrap-reboot"; 2 | @import "~bootstrap/scss/bootstrap-grid"; 3 | @import "~bootstrap/scss/alert"; 4 | @import "~bootstrap/scss/tables"; 5 | 6 | -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true, 3 | apiUrl: 'http://localhost:3000', 4 | userRoles: ['Banned', 'Customer', 'Admin', 'Super Admin'], 5 | }; 6 | -------------------------------------------------------------------------------- /src/app/pages/auth/contents/login/login.component.scss: -------------------------------------------------------------------------------- 1 | .login-header-wrapper{ 2 | text-align: center; 3 | margin-bottom: 40px; 4 | } 5 | .navigation-link{ 6 | text-align: center; 7 | margin-top:30px; 8 | } 9 | -------------------------------------------------------------------------------- /src/app/shared/components/index.ts: -------------------------------------------------------------------------------- 1 | export { NavbarComponent as Navbar } from './navbar/navbar.component'; 2 | export { LoadingSpinnerComponent as LoadingSpinner } from './loading-spinner/loading-spinner.component'; 3 | -------------------------------------------------------------------------------- /src/app/core/services/common/index.ts: -------------------------------------------------------------------------------- 1 | export { ApiService } from './api.service'; 2 | export { LoadingSpinnerService } from './loading-spinner.service'; 3 | export { GlobalDataService } from './global-data.service'; 4 | -------------------------------------------------------------------------------- /src/app/pages/auth/contents/register/register.component.scss: -------------------------------------------------------------------------------- 1 | .register-header-wrapper{ 2 | text-align: center; 3 | margin-bottom: 20px; 4 | } 5 | .navigation-link{ 6 | text-align: center; 7 | margin-top:30px; 8 | } 9 | -------------------------------------------------------------------------------- /src/app/pages/error-pages/contents/index.ts: -------------------------------------------------------------------------------- 1 | export { ForbiddenComponent as ForbiddenPage } from './forbidden/forbidden.component'; 2 | export { NotFoundComponent as NotfoundPage } from './not-found/not-found.component'; 3 | -------------------------------------------------------------------------------- /src/app/models/auth/user.model.ts: -------------------------------------------------------------------------------- 1 | export interface USER { 2 | id: number; 3 | email: string; 4 | } 5 | export interface PROFILE extends USER { 6 | userId: string; 7 | fullName: string; 8 | role: number; 9 | } 10 | -------------------------------------------------------------------------------- /src/app/shared/components/loading-spinner/loading-spinner.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |
5 | -------------------------------------------------------------------------------- /src/app/models/common/http.model.ts: -------------------------------------------------------------------------------- 1 | export interface HTTP_REQ { 2 | url: string; 3 | params?: any; 4 | headers?: any; 5 | body?: any; 6 | } 7 | export interface HTTP_RES { 8 | success: boolean; 9 | data: any; 10 | error?: any; 11 | } 12 | -------------------------------------------------------------------------------- /src/styles.scss: -------------------------------------------------------------------------------- 1 | @import "./styles/angular-material"; 2 | @import "./styles/bootstrap"; 3 | html, 4 | body { 5 | height: 100%; 6 | } 7 | body { 8 | margin: 0; 9 | font-family: Roboto, "Helvetica Neue", sans-serif; 10 | background-color: #fafafa; 11 | } 12 | -------------------------------------------------------------------------------- /src/app/models/auth/auth-forms.model.ts: -------------------------------------------------------------------------------- 1 | export interface LOGIN_FORM_DATA { 2 | email:string; 3 | password:string 4 | } 5 | export interface REGISTER_FORM_DATA extends LOGIN_FORM_DATA{ 6 | fullName:string; 7 | passwordConfirm?:string; 8 | role?:number; 9 | } 10 | -------------------------------------------------------------------------------- /src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: false, 3 | apiUrl: 'http://localhost:3000', 4 | userRoles: ['Banned', 'Customer', 'Admin', 'Super Admin'], 5 | }; 6 | 7 | // import 'zone.js/plugins/zone-error'; // Included with Angular CLI. 8 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-root', 5 | templateUrl: './app.component.html', 6 | styleUrls: ['./app.component.scss'] 7 | }) 8 | export class AppComponent { 9 | title = 'user-management-interview'; 10 | } 11 | -------------------------------------------------------------------------------- /src/app/pages/user-list/components/index.ts: -------------------------------------------------------------------------------- 1 | export { UserTableComponent as UserTable } from './user-table/user-table.component'; 2 | export { UserModalComponent as UserModal } from './user-modal/user-modal.component'; 3 | export { UserFromComponent as UserForm } from './user-from/user-from.component'; 4 | -------------------------------------------------------------------------------- /src/app/shared/components/loading-spinner/loading-spinner.component.scss: -------------------------------------------------------------------------------- 1 | .loading-spinner-wrapper{ 2 | 3 | padding: 40px 40px; 4 | display: flex; 5 | flex-direction: column; 6 | align-items: center; 7 | border-radius: 4px; 8 | .loading-status-text{ 9 | padding-top: 20px; 10 | font-style: italic; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.ts] 12 | quote_type = single 13 | 14 | [*.md] 15 | max_line_length = off 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /src/app/pages/auth/auth.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-auth', 5 | templateUrl: './auth.component.html', 6 | styleUrls: ['./auth.component.scss'] 7 | }) 8 | export class AuthComponent implements OnInit { 9 | 10 | constructor() { } 11 | 12 | ngOnInit(): void { 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/app/pages/error-pages/error-pages.component.scss: -------------------------------------------------------------------------------- 1 | .error-wrapper{ 2 | width: 100%; 3 | position: relative; 4 | min-height: 100vh; 5 | .error-container{ 6 | position: absolute; 7 | left: 50%; 8 | top: 50%; 9 | -webkit-transform: translate(-50%, -50%); 10 | transform: translate(-50%, -50%); 11 | text-align: center; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/app/pages/pages.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-pages', 5 | templateUrl: './pages.component.html', 6 | styleUrls: ['./pages.component.scss'] 7 | }) 8 | export class PagesComponent implements OnInit { 9 | 10 | constructor() { } 11 | 12 | ngOnInit(): void { 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/app", 6 | "types": [] 7 | }, 8 | "files": [ 9 | "src/main.ts", 10 | "src/polyfills.ts" 11 | ], 12 | "include": [ 13 | "src/**/*.d.ts" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /src/app/core/services/common/global-data.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | // RXJS 3 | import { BehaviorSubject } from 'rxjs'; 4 | // MODELS 5 | import { PROFILE } from '@models/auth'; 6 | @Injectable({ 7 | providedIn: 'root', 8 | }) 9 | export class GlobalDataService { 10 | currentUser$ = new BehaviorSubject(null); 11 | constructor() {} 12 | } 13 | -------------------------------------------------------------------------------- /src/app/pages/auth/auth.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | site logo 5 |
6 | 7 | 8 | 9 | 10 | 11 |
12 |
13 | -------------------------------------------------------------------------------- /src/app/core/interceptors/index.ts: -------------------------------------------------------------------------------- 1 | import { HTTP_INTERCEPTORS } from '@angular/common/http'; 2 | import { AuthInterceptor } from './AuthInterceptor'; 3 | import { ErrorInterceptor } from './ErrorInterceptor'; 4 | export const httpInterceptorProviders = [ 5 | { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true }, 6 | { provide: HTTP_INTERCEPTORS, useClass: ErrorInterceptor, multi: true }, 7 | ]; 8 | -------------------------------------------------------------------------------- /src/app/pages/error-pages/error-pages.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-error-pages', 5 | templateUrl: './error-pages.component.html', 6 | styleUrls: ['./error-pages.component.scss'] 7 | }) 8 | export class ErrorPagesComponent implements OnInit { 9 | 10 | constructor() { } 11 | 12 | ngOnInit(): void { 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/app/pages/error-pages/contents/forbidden/forbidden.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-forbidden', 5 | templateUrl: './forbidden.component.html', 6 | styleUrls: ['./forbidden.component.scss'] 7 | }) 8 | export class ForbiddenComponent implements OnInit { 9 | 10 | constructor() { } 11 | 12 | ngOnInit(): void { 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/app/pages/error-pages/contents/not-found/not-found.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-not-found', 5 | templateUrl: './not-found.component.html', 6 | styleUrls: ['./not-found.component.scss'] 7 | }) 8 | export class NotFoundComponent implements OnInit { 9 | 10 | constructor() { } 11 | 12 | ngOnInit(): void { 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/spec", 6 | "types": [ 7 | "jasmine" 8 | ] 9 | }, 10 | "files": [ 11 | "src/test.ts", 12 | "src/polyfills.ts" 13 | ], 14 | "include": [ 15 | "src/**/*.spec.ts", 16 | "src/**/*.d.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic().bootstrapModule(AppModule) 12 | .catch(err => console.error(err)); 13 | -------------------------------------------------------------------------------- /src/app/core/guards/admin.guard.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { AdminGuard } from './admin.guard'; 4 | 5 | describe('AdminGuard', () => { 6 | let guard: AdminGuard; 7 | 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({}); 10 | guard = TestBed.inject(AdminGuard); 11 | }); 12 | 13 | it('should be created', () => { 14 | expect(guard).toBeTruthy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /src/app/core/guards/login.guard.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { LoginGuard } from './login.guard'; 4 | 5 | describe('LoginGuard', () => { 6 | let guard: LoginGuard; 7 | 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({}); 10 | guard = TestBed.inject(LoginGuard); 11 | }); 12 | 13 | it('should be created', () => { 14 | expect(guard).toBeTruthy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /src/app/shared/components/loading-spinner/loading-spinner.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-loading-spinner', 5 | templateUrl: './loading-spinner.component.html', 6 | styleUrls: ['./loading-spinner.component.scss'] 7 | }) 8 | export class LoadingSpinnerComponent implements OnInit { 9 | 10 | constructor() { } 11 | 12 | ngOnInit(): void { 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/app/core/services/common/api.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { ApiService } from './api.service'; 4 | 5 | describe('ApiService', () => { 6 | let service: ApiService; 7 | 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({}); 10 | service = TestBed.inject(ApiService); 11 | }); 12 | 13 | it('should be created', () => { 14 | expect(service).toBeTruthy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /src/app/core/guards/not-login.guard.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { NotLoginGuard } from './not-login.guard'; 4 | 5 | describe('NotLoginGuard', () => { 6 | let guard: NotLoginGuard; 7 | 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({}); 10 | guard = TestBed.inject(NotLoginGuard); 11 | }); 12 | 13 | it('should be created', () => { 14 | expect(guard).toBeTruthy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /src/app/core/services/auth/auth.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { AuthService } from './auth.service'; 4 | 5 | describe('AuthService', () => { 6 | let service: AuthService; 7 | 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({}); 10 | service = TestBed.inject(AuthService); 11 | }); 12 | 13 | it('should be created', () => { 14 | expect(service).toBeTruthy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /src/app/core/services/auth/user-list.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { UserListService } from './user-list.service'; 4 | 5 | describe('UserListService', () => { 6 | let service: UserListService; 7 | 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({}); 10 | service = TestBed.inject(UserListService); 11 | }); 12 | 13 | it('should be created', () => { 14 | expect(service).toBeTruthy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /src/app/core/services/common/global-data.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { GlobalDataService } from './global-data.service'; 4 | 5 | describe('GlobalDataService', () => { 6 | let service: GlobalDataService; 7 | 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({}); 10 | service = TestBed.inject(GlobalDataService); 11 | }); 12 | 13 | it('should be created', () => { 14 | expect(service).toBeTruthy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /src/app/core/services/notifcation/snack-message.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { SnackMessageService } from './snack-message.service'; 4 | 5 | describe('SnackMessageService', () => { 6 | let service: SnackMessageService; 7 | 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({}); 10 | service = TestBed.inject(SnackMessageService); 11 | }); 12 | 13 | it('should be created', () => { 14 | expect(service).toBeTruthy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /src/app/core/services/form/form-validation.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { FormValidationService } from './form-validation.service'; 4 | 5 | describe('FormValidationService', () => { 6 | let service: FormValidationService; 7 | 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({}); 10 | service = TestBed.inject(FormValidationService); 11 | }); 12 | 13 | it('should be created', () => { 14 | expect(service).toBeTruthy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /src/app/core/services/common/loading-spinner.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { LoadingSpinnerService } from './loading-spinner.service'; 4 | 5 | describe('LoadingSpinnerService', () => { 6 | let service: LoadingSpinnerService; 7 | 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({}); 10 | service = TestBed.inject(LoadingSpinnerService); 11 | }); 12 | 13 | it('should be created', () => { 14 | expect(service).toBeTruthy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /src/app/pages/auth/auth.component.scss: -------------------------------------------------------------------------------- 1 | .auth-wrapper { 2 | width: 100%; 3 | position: relative; 4 | min-height: 100vh; 5 | .auth-form { 6 | position: absolute; 7 | left: 50%; 8 | top: 50%; 9 | -webkit-transform: translate(-50%, -50%); 10 | transform: translate(-50%, -50%); 11 | width: 400px; 12 | max-width: 100%; 13 | 14 | .img-wrapper{ 15 | margin-bottom: 30px; 16 | text-align: center; 17 | img{ 18 | max-width: 60%; 19 | } 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/app/pages/error-pages/error-pages.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | 4 | import { ErrorPagesRoutingModule } from './error-pages-routing.module'; 5 | // WRAPPER 6 | import { ErrorPagesComponent } from './error-pages.component'; 7 | // PAGES 8 | import { ForbiddenPage, NotfoundPage } from './contents'; 9 | 10 | @NgModule({ 11 | declarations: [ErrorPagesComponent, ForbiddenPage, NotfoundPage], 12 | imports: [CommonModule, ErrorPagesRoutingModule], 13 | }) 14 | export class ErrorPagesModule {} 15 | -------------------------------------------------------------------------------- /src/app/core/services/notifcation/snack-message.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | // SNACKBAR 3 | import {MatSnackBar} from '@angular/material/snack-bar' 4 | // MODELS 5 | import { SNACK_DATA } from '@models/notification'; 6 | @Injectable({ 7 | providedIn: 'root' 8 | }) 9 | export class SnackMessageService { 10 | 11 | constructor(private snackbar:MatSnackBar) { } 12 | public show(snackData: SNACK_DATA) { 13 | this.snackbar.open(snackData?.message, snackData?.action || 'OK', { 14 | duration: snackData?.duration || 4000, 15 | }); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/app/pages/auth/auth.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | // SHARED MODULE 4 | import { SharedModule } from '@shared/shared.module'; 5 | 6 | import { AuthRoutingModule } from './auth-routing.module'; 7 | // AUTH LAYOUT 8 | import { AuthComponent } from './auth.component'; 9 | // AUTH PAGES 10 | import { LoginPage, RegisterPage } from './contents'; 11 | @NgModule({ 12 | declarations: [AuthComponent, LoginPage, RegisterPage], 13 | imports: [CommonModule, SharedModule, AuthRoutingModule], 14 | }) 15 | export class AuthModule {} 16 | -------------------------------------------------------------------------------- /src/app/pages/user-list/components/user-modal/user-modal.component.html: -------------------------------------------------------------------------------- 1 |

{{ data ? "Edit User" : "Add New User" }}

2 |
3 | 4 |
5 |
6 | 7 | 16 |
17 | -------------------------------------------------------------------------------- /.browserslistrc: -------------------------------------------------------------------------------- 1 | # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | 5 | # For the full list of supported browsers by the Angular framework, please see: 6 | # https://angular.io/guide/browser-support 7 | 8 | # You can see what browsers were selected by your queries by running: 9 | # npx browserslist 10 | 11 | last 1 Chrome version 12 | last 1 Firefox version 13 | last 2 Edge major versions 14 | last 2 Safari major versions 15 | last 2 iOS major versions 16 | Firefox ESR 17 | -------------------------------------------------------------------------------- /src/app/shared/components/navbar/navbar.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | // SERVICES 3 | import { AuthService } from '@core/services/auth'; 4 | // MODELS 5 | import { PROFILE } from '@models/auth'; 6 | @Component({ 7 | selector: 'navbar', 8 | templateUrl: './navbar.component.html', 9 | styleUrls: ['./navbar.component.scss'], 10 | }) 11 | export class NavbarComponent implements OnInit { 12 | currentUser!: Promise; 13 | constructor(private authService: AuthService) {} 14 | 15 | ngOnInit(): void { 16 | this.currentUser = this.authService.userProfile(); 17 | } 18 | logOut() { 19 | this.authService.logOut(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/app/pages/auth/auth.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { AuthComponent } from './auth.component'; 4 | 5 | describe('AuthComponent', () => { 6 | let component: AuthComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ AuthComponent ] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(AuthComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/pages/home/home.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { HomeComponent } from './home.component'; 4 | 5 | describe('HomeComponent', () => { 6 | let component: HomeComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ HomeComponent ] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(HomeComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/pages/pages.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { PagesComponent } from './pages.component'; 4 | 5 | describe('PagesComponent', () => { 6 | let component: PagesComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ PagesComponent ] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(PagesComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/pages/user-list/user-list.component.html: -------------------------------------------------------------------------------- 1 |
2 | 10 |
11 | 12 | 13 | 18 | 19 | 20 |
21 |

User list have no item

22 |
23 |
24 |
25 | -------------------------------------------------------------------------------- /ngsw-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/service-worker/config/schema.json", 3 | "index": "/index.html", 4 | "assetGroups": [ 5 | { 6 | "name": "app", 7 | "installMode": "prefetch", 8 | "resources": { 9 | "files": [ 10 | "/favicon.ico", 11 | "/index.html", 12 | "/manifest.webmanifest", 13 | "/*.css", 14 | "/*.js" 15 | ] 16 | } 17 | }, 18 | { 19 | "name": "assets", 20 | "installMode": "lazy", 21 | "updateMode": "prefetch", 22 | "resources": { 23 | "files": [ 24 | "/assets/**", 25 | "/*.(svg|cur|jpg|jpeg|png|apng|webp|avif|gif|otf|ttf|woff|woff2)" 26 | ] 27 | } 28 | } 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /src/app/pages/auth/contents/login/login.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { LoginComponent } from './login.component'; 4 | 5 | describe('LoginComponent', () => { 6 | let component: LoginComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ LoginComponent ] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(LoginComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/shared/components/navbar/navbar.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { NavbarComponent } from './navbar.component'; 4 | 5 | describe('NavbarComponent', () => { 6 | let component: NavbarComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ NavbarComponent ] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(NavbarComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/pages/user-list/user-list.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { UserListComponent } from './user-list.component'; 4 | 5 | describe('UserListComponent', () => { 6 | let component: UserListComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ UserListComponent ] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(UserListComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/styles/_angular-material.scss: -------------------------------------------------------------------------------- 1 | @use "@angular/material" as mat; 2 | @include mat.core(); 3 | 4 | $user-management-interview-primary: mat.define-palette(mat.$deep-purple-palette); 5 | $user-management-interview-accent: mat.define-palette( 6 | mat.$blue-grey-palette, 7 | 100 8 | ); 9 | $user-management-interview-warn: mat.define-palette(mat.$red-palette); 10 | $user-management-interview-theme: mat.define-light-theme( 11 | ( 12 | color: ( 13 | primary: $user-management-interview-primary, 14 | accent: $user-management-interview-accent, 15 | warn: $user-management-interview-warn, 16 | ), 17 | ) 18 | ); 19 | @include mat.all-component-themes($user-management-interview-theme); 20 | .overlay-backdrop { 21 | background-color: rgba($color: #fff, $alpha: 0.6); 22 | } 23 | -------------------------------------------------------------------------------- /src/app/pages/auth/contents/register/register.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { RegisterComponent } from './register.component'; 4 | 5 | describe('RegisterComponent', () => { 6 | let component: RegisterComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ RegisterComponent ] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(RegisterComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/pages/error-pages/error-pages.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ErrorPagesComponent } from './error-pages.component'; 4 | 5 | describe('ErrorPagesComponent', () => { 6 | let component: ErrorPagesComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ ErrorPagesComponent ] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ErrorPagesComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/pages/error-pages/contents/not-found/not-found.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { NotFoundComponent } from './not-found.component'; 4 | 5 | describe('NotFoundComponent', () => { 6 | let component: NotFoundComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ NotFoundComponent ] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(NotFoundComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/pages/user-list/components/user-from/user-from.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { UserFromComponent } from './user-from.component'; 4 | 5 | describe('UserFromComponent', () => { 6 | let component: UserFromComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ UserFromComponent ] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(UserFromComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/pages/error-pages/contents/forbidden/forbidden.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ForbiddenComponent } from './forbidden.component'; 4 | 5 | describe('ForbiddenComponent', () => { 6 | let component: ForbiddenComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ ForbiddenComponent ] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ForbiddenComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/pages/user-list/components/user-modal/user-modal.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { UserModalComponent } from './user-modal.component'; 4 | 5 | describe('UserModalComponent', () => { 6 | let component: UserModalComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ UserModalComponent ] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(UserModalComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/pages/user-list/components/user-table/user-table.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { UserTableComponent } from './user-table.component'; 4 | 5 | describe('UserTableComponent', () => { 6 | let component: UserTableComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ UserTableComponent ] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(UserTableComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/pages/error-pages/error-pages-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule, Routes } from '@angular/router'; 3 | import { ErrorPagesComponent } from './error-pages.component'; 4 | // PAGES 5 | import { ForbiddenPage, NotfoundPage } from './contents'; 6 | const routes: Routes = [ 7 | { 8 | path: '', 9 | component: ErrorPagesComponent, 10 | children: [ 11 | 12 | // 401 AND 404 PAGES 13 | { path: '401', component: ForbiddenPage }, 14 | { path: '404', component: NotfoundPage }, 15 | // DEFAULT ROUTE 16 | { path: '', redirectTo: '404',pathMatch:'full' }, 17 | ], 18 | }, 19 | ]; 20 | 21 | @NgModule({ 22 | imports: [RouterModule.forChild(routes)], 23 | exports: [RouterModule], 24 | }) 25 | export class ErrorPagesRoutingModule {} 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | # Only exists if Bazel was run 8 | /bazel-out 9 | 10 | # dependencies 11 | /node_modules 12 | 13 | # profiling files 14 | chrome-profiler-events*.json 15 | 16 | # IDEs and editors 17 | /.idea 18 | .project 19 | .classpath 20 | .c9/ 21 | *.launch 22 | .settings/ 23 | *.sublime-workspace 24 | 25 | # IDE - VSCode 26 | .vscode/* 27 | !.vscode/settings.json 28 | !.vscode/tasks.json 29 | !.vscode/launch.json 30 | !.vscode/extensions.json 31 | .history/* 32 | 33 | # misc 34 | /.angular/cache 35 | /.sass-cache 36 | /connect.lock 37 | /coverage 38 | /libpeerconnection.log 39 | npm-debug.log 40 | yarn-error.log 41 | testem.log 42 | /typings 43 | 44 | # System Files 45 | .DS_Store 46 | Thumbs.db 47 | -------------------------------------------------------------------------------- /src/app/shared/components/loading-spinner/loading-spinner.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { LoadingSpinnerComponent } from './loading-spinner.component'; 4 | 5 | describe('LoadingSpinnerComponent', () => { 6 | let component: LoadingSpinnerComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ LoadingSpinnerComponent ] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(LoadingSpinnerComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | UserManagementInterview 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/testing'; 4 | import { getTestBed } from '@angular/core/testing'; 5 | import { 6 | BrowserDynamicTestingModule, 7 | platformBrowserDynamicTesting 8 | } from '@angular/platform-browser-dynamic/testing'; 9 | 10 | declare const require: { 11 | context(path: string, deep?: boolean, filter?: RegExp): { 12 | keys(): string[]; 13 | (id: string): T; 14 | }; 15 | }; 16 | 17 | // First, initialize the Angular testing environment. 18 | getTestBed().initTestEnvironment( 19 | BrowserDynamicTestingModule, 20 | platformBrowserDynamicTesting(), 21 | ); 22 | 23 | // Then we find all the tests. 24 | const context = require.context('./', true, /\.spec\.ts$/); 25 | // And load the modules. 26 | context.keys().map(context); 27 | -------------------------------------------------------------------------------- /src/app/pages/auth/auth-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule, Routes } from '@angular/router'; 3 | // PARENT COMPONENT 4 | import { AuthComponent } from './auth.component'; 5 | // CHILD COMPONENTS 6 | import { LoginPage, RegisterPage } from './contents'; 7 | 8 | const routes: Routes = [ 9 | { 10 | path: '', 11 | component: AuthComponent, 12 | children: [ 13 | // SET DEFAULT LOGIN PAGE 14 | // LOGIN AND REGISTER PAGES 15 | { path: 'login', component: LoginPage }, 16 | { path: 'register', component: RegisterPage }, 17 | { path: '', redirectTo: 'login', pathMatch: 'full' }, 18 | { path: '**', redirectTo: '/error' }, 19 | ], 20 | }, 21 | ]; 22 | 23 | @NgModule({ 24 | imports: [RouterModule.forChild(routes)], 25 | exports: [RouterModule], 26 | }) 27 | export class AuthRoutingModule {} 28 | -------------------------------------------------------------------------------- /src/app/pages/home/home.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | import { Observable } from 'rxjs'; 4 | // SERVICES 5 | import { GlobalDataService } from '@core/services/common'; 6 | import { AuthService } from '@core/services/auth'; 7 | // MODELS 8 | import { PROFILE } from '@models/auth'; 9 | // ENV 10 | import { environment } from '@environments/environment'; 11 | 12 | @Component({ 13 | selector: 'app-home', 14 | templateUrl: './home.component.html', 15 | styleUrls: ['./home.component.scss'], 16 | }) 17 | export class HomeComponent implements OnInit { 18 | readonly userRoles: string[] = environment.userRoles; 19 | currentUser$: Observable = 20 | this.globalData.currentUser$.asObservable(); 21 | constructor( 22 | private globalData: GlobalDataService, 23 | private authService: AuthService 24 | ) {} 25 | 26 | ngOnInit(): void {} 27 | logOut() { 28 | this.authService.logOut(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/app/shared/shared.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | 4 | // ANGULAR FORM MODULES 5 | import { FormsModule, ReactiveFormsModule } from '@angular/forms'; 6 | // SHARED ANGULAR MATERIAL MODULES 7 | import { MatButtonModule } from '@angular/material/button'; 8 | import { MatInputModule } from '@angular/material/input'; 9 | import { MatCardModule } from '@angular/material/card'; 10 | import { MatFormFieldModule } from '@angular/material/form-field'; 11 | import { MatIconModule } from '@angular/material/icon'; 12 | 13 | @NgModule({ 14 | declarations: [], 15 | imports: [CommonModule], 16 | exports: [ 17 | // FORM MODULES 18 | FormsModule, 19 | ReactiveFormsModule, 20 | // ANGULAR MATERIAL MODULES 21 | MatButtonModule, 22 | MatInputModule, 23 | MatCardModule, 24 | MatFormFieldModule, 25 | MatIconModule, 26 | MatInputModule, 27 | ], 28 | }) 29 | export class SharedModule {} 30 | -------------------------------------------------------------------------------- /src/app/pages/pages-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule, Routes } from '@angular/router'; 3 | // GUARDS 4 | import { AdminGuard } from '@core/guards'; 5 | // PARENT 6 | import { PagesComponent } from './pages.component'; 7 | // PAGES 8 | import { HomeComponent as HomePage } from './home/home.component'; 9 | import { UserListComponent as UserListPage } from './user-list/user-list.component'; 10 | const routes: Routes = [ 11 | { 12 | path: '', 13 | 14 | component: PagesComponent, 15 | children: [ 16 | { path: 'home', component: HomePage }, 17 | { 18 | path: 'user-list', 19 | component: UserListPage, 20 | canLoad: [AdminGuard], 21 | canActivate: [AdminGuard], 22 | }, 23 | {path:'',pathMatch:'full',redirectTo:'home'} 24 | ], 25 | }, 26 | ]; 27 | 28 | @NgModule({ 29 | imports: [RouterModule.forChild(routes)], 30 | exports: [RouterModule], 31 | }) 32 | export class PagesRoutingModule {} 33 | -------------------------------------------------------------------------------- /src/app/core/core.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | 4 | // COOKIE SERVICE 5 | import { CookieService } from 'ngx-cookie-service'; 6 | // HTTP CLIENT 7 | import { HttpClientModule } from '@angular/common/http'; 8 | // SNACKBAR MODULE FOR NOTIFICATIONS 9 | import { MatSnackBarModule } from '@angular/material/snack-bar'; 10 | // LOADING SPINNER 11 | import { OverlayModule } from '@angular/cdk/overlay'; 12 | import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; 13 | import { LoadingSpinner } from '@shared/components'; 14 | // HTTP INTERCEPTOR 15 | import { httpInterceptorProviders } from './interceptors'; 16 | @NgModule({ 17 | declarations: [LoadingSpinner], 18 | imports: [ 19 | CommonModule, 20 | HttpClientModule, 21 | MatSnackBarModule, 22 | OverlayModule, 23 | MatProgressSpinnerModule, 24 | ], 25 | providers: [CookieService, httpInterceptorProviders], 26 | }) 27 | export class CoreModule {} 28 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { BrowserModule } from '@angular/platform-browser'; 3 | 4 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 5 | import { ServiceWorkerModule } from '@angular/service-worker'; 6 | import { environment } from '@environments/environment'; 7 | import { CoreModule } from '@core/core.module'; 8 | import { AppRoutingModule } from './app-routing.module'; 9 | import { AppComponent } from './app.component'; 10 | @NgModule({ 11 | declarations: [AppComponent], 12 | imports: [ 13 | BrowserModule, 14 | AppRoutingModule, 15 | BrowserAnimationsModule, 16 | ServiceWorkerModule.register('ngsw-worker.js', { 17 | enabled: environment.production, 18 | // Register the ServiceWorker as soon as the app is stable 19 | // or after 30 seconds (whichever comes first). 20 | registrationStrategy: 'registerWhenStable:30000', 21 | }), 22 | CoreModule, 23 | ], 24 | providers: [], 25 | bootstrap: [AppComponent], 26 | }) 27 | export class AppModule {} 28 | -------------------------------------------------------------------------------- /src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule, Routes } from '@angular/router'; 3 | // ROUTER GUARDS 4 | import { LoginGuard, NotLoginGuard } from '@core/guards'; 5 | const routes: Routes = [ 6 | 7 | { 8 | path: 'auth', 9 | canActivate: [NotLoginGuard], 10 | canLoad: [NotLoginGuard], 11 | loadChildren: () => 12 | import('./pages/auth/auth.module').then((m) => m.AuthModule), 13 | }, 14 | { 15 | path: 'error', 16 | loadChildren: () => 17 | import('./pages/error-pages/error-pages.module').then( 18 | (m) => m.ErrorPagesModule 19 | ), 20 | }, 21 | { 22 | path: '', 23 | canActivate: [LoginGuard], 24 | canLoad: [LoginGuard], 25 | loadChildren: () => 26 | import('./pages/pages.module').then((m) => m.PagesModule), 27 | }, 28 | 29 | // WRONG URL REDIRECT TO 404 30 | // { path: '', redirectTo: '/', pathMatch: 'full' }, 31 | { path: '**', redirectTo: 'error' }, 32 | ]; 33 | 34 | @NgModule({ 35 | imports: [RouterModule.forRoot(routes)], 36 | exports: [RouterModule], 37 | }) 38 | export class AppRoutingModule {} 39 | -------------------------------------------------------------------------------- /src/app/pages/user-list/components/user-modal/user-modal.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, Inject } from '@angular/core'; 2 | import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; 3 | import { UserListService } from '@core/services/auth'; 4 | // MODELS 5 | import { PROFILE } from '@models/auth'; 6 | @Component({ 7 | selector: 'app-user-modal', 8 | templateUrl: './user-modal.component.html', 9 | styleUrls: ['./user-modal.component.scss'], 10 | }) 11 | export class UserModalComponent implements OnInit { 12 | constructor( 13 | private userListService: UserListService, 14 | private dialogRef: MatDialogRef, 15 | @Inject(MAT_DIALOG_DATA) public data: PROFILE 16 | ) {} 17 | async save(formData: any) { 18 | const { success, user } = this.data 19 | ? await this.userListService.updateUser({ 20 | ...this.data, 21 | fullName: formData?.fullName, 22 | role: formData?.role, 23 | }) 24 | : await this.userListService.addNewUser(formData); 25 | if (success) { 26 | this.dialogRef.close({ success: true, userData: user }); 27 | } 28 | } 29 | ngOnInit(): void {} 30 | } 31 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "compileOnSave": false, 4 | "compilerOptions": { 5 | "baseUrl": "./", 6 | "outDir": "./dist/out-tsc", 7 | "forceConsistentCasingInFileNames": true, 8 | "strict": true, 9 | "noImplicitOverride": true, 10 | "noPropertyAccessFromIndexSignature": true, 11 | "noImplicitReturns": true, 12 | "noFallthroughCasesInSwitch": true, 13 | "sourceMap": true, 14 | "declaration": false, 15 | "downlevelIteration": true, 16 | "experimentalDecorators": true, 17 | "moduleResolution": "node", 18 | "importHelpers": true, 19 | "target": "es2017", 20 | "module": "es2020", 21 | "paths": { 22 | "@environments/*": ["./src/environments/*"], 23 | "@shared/*": ["./src/app/shared/*"], 24 | "@core/*": ["./src/app/core/*"], 25 | "@models/*": ["./src/app/models/*"] 26 | }, 27 | "lib": ["es2020", "dom"] 28 | }, 29 | "angularCompilerOptions": { 30 | "enableI18nLegacyMessageIdFormat": false, 31 | "strictInjectionParameters": true, 32 | "strictInputAccessModifiers": true, 33 | "strictTemplates": true 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/app/core/interceptors/ErrorInterceptor.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { 3 | HttpEvent, 4 | HttpInterceptor, 5 | HttpHandler, 6 | HttpRequest, 7 | HttpErrorResponse, 8 | } from '@angular/common/http'; 9 | import { Observable, throwError } from 'rxjs'; 10 | import { catchError } from 'rxjs/operators'; 11 | // SERVICES 12 | import { LoadingSpinnerService } from '@core/services/common'; 13 | @Injectable() 14 | export class ErrorInterceptor implements HttpInterceptor { 15 | constructor(private spinnerService: LoadingSpinnerService) {} 16 | intercept( 17 | request: HttpRequest, 18 | next: HttpHandler 19 | ): Observable> { 20 | return next.handle(request).pipe( 21 | catchError((error: HttpErrorResponse) => { 22 | let errorMessage = ''; 23 | if (error.error instanceof ErrorEvent) { 24 | // frontend error 25 | errorMessage = `Error: ${error.error.message}`; 26 | } else { 27 | // backend error 28 | errorMessage = error.error || error.message; 29 | } 30 | this.spinnerService.removeQuene(); 31 | return throwError({ status: error.status, message: errorMessage }); 32 | }) 33 | ); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/app/pages/user-list/components/user-table/user-table.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core'; 2 | // SERVICES 3 | import { GlobalDataService } from '@core/services/common'; 4 | // MODELS 5 | import { PROFILE } from '@models/auth'; 6 | // ENV 7 | import { environment } from '@environments/environment'; 8 | @Component({ 9 | selector: 'user-table', 10 | templateUrl: './user-table.component.html', 11 | styleUrls: ['./user-table.component.scss'], 12 | }) 13 | export class UserTableComponent implements OnInit { 14 | private readonly userRoles = environment?.userRoles; 15 | @Input() userList!: PROFILE[]; 16 | @Output() update = new EventEmitter(); 17 | @Output() delete = new EventEmitter(); 18 | 19 | constructor(private globalData: GlobalDataService) {} 20 | 21 | ngOnInit(): void {} 22 | visualizeUserRole(roleIndex: number | undefined): string { 23 | return this.userRoles[roleIndex ? roleIndex : 0]; 24 | } 25 | // AVOID TO DELETE CURRENT USER 26 | isOwner(user: PROFILE): boolean { 27 | return this.globalData.currentUser$.getValue()?.id === user?.id; 28 | } 29 | // FOR LOOP PERFORMANCE 30 | trackByFn(index: number, user: PROFILE): number { 31 | return user?.id; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | import { RouterTestingModule } from '@angular/router/testing'; 3 | import { AppComponent } from './app.component'; 4 | 5 | describe('AppComponent', () => { 6 | beforeEach(async () => { 7 | await TestBed.configureTestingModule({ 8 | imports: [ 9 | RouterTestingModule 10 | ], 11 | declarations: [ 12 | AppComponent 13 | ], 14 | }).compileComponents(); 15 | }); 16 | 17 | it('should create the app', () => { 18 | const fixture = TestBed.createComponent(AppComponent); 19 | const app = fixture.componentInstance; 20 | expect(app).toBeTruthy(); 21 | }); 22 | 23 | it(`should have as title 'user-management-interview'`, () => { 24 | const fixture = TestBed.createComponent(AppComponent); 25 | const app = fixture.componentInstance; 26 | expect(app.title).toEqual('user-management-interview'); 27 | }); 28 | 29 | it('should render title', () => { 30 | const fixture = TestBed.createComponent(AppComponent); 31 | fixture.detectChanges(); 32 | const compiled = fixture.nativeElement as HTMLElement; 33 | expect(compiled.querySelector('.content span')?.textContent).toContain('user-management-interview app is running!'); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /src/app/core/services/common/loading-spinner.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Overlay } from '@angular/cdk/overlay'; 3 | import { ComponentPortal } from '@angular/cdk/portal'; 4 | // COMPONENTS 5 | import { LoadingSpinner } from '@shared/components'; 6 | 7 | @Injectable({ 8 | providedIn: 'root', 9 | }) 10 | export class LoadingSpinnerService { 11 | private overlayRef = this.cdkOverlayCreate(); 12 | private spinnerCount = 0; 13 | constructor(private overlay: Overlay) {} 14 | private cdkOverlayCreate() { 15 | return this.overlay.create({ 16 | hasBackdrop: true, 17 | backdropClass: 'overlay-backdrop', 18 | positionStrategy: this.overlay 19 | .position() 20 | .global() 21 | .bottom() 22 | .right(), 23 | }); 24 | } 25 | public addQuene() { 26 | if (this.spinnerCount > 0) { 27 | this.spinnerCount++; 28 | } else { 29 | this.showSpinner(); 30 | this.spinnerCount = 1; 31 | } 32 | } 33 | public removeQuene() { 34 | this.spinnerCount--; 35 | if (this.spinnerCount < 1) { 36 | this.stopSpinner(); 37 | } 38 | } 39 | showSpinner() { 40 | this.overlayRef.attach(new ComponentPortal(LoadingSpinner)); 41 | } 42 | 43 | stopSpinner() { 44 | this.overlayRef.detach(); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/app/core/guards/not-login.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { 3 | ActivatedRouteSnapshot, 4 | CanActivate, 5 | CanLoad, 6 | Route, 7 | Router, 8 | RouterStateSnapshot, 9 | UrlSegment, 10 | UrlTree, 11 | } from '@angular/router'; 12 | 13 | import { Observable } from 'rxjs'; 14 | // SERVICES 15 | import { CookieService } from 'ngx-cookie-service'; 16 | @Injectable({ 17 | providedIn: 'root', 18 | }) 19 | export class NotLoginGuard implements CanActivate, CanLoad { 20 | constructor(private cookieService: CookieService, private router: Router) {} 21 | // CHECK IF NOT LOGGED 22 | get checkAuth() { 23 | const isNotLogged = this.cookieService.get('authToken') ? false : true; 24 | if (!isNotLogged) { 25 | return this.router.createUrlTree(['/']); 26 | } 27 | return isNotLogged; 28 | } 29 | canActivate( 30 | route: ActivatedRouteSnapshot, 31 | state: RouterStateSnapshot 32 | ): 33 | | Observable 34 | | Promise 35 | | boolean 36 | | UrlTree { 37 | return this.checkAuth; 38 | } 39 | 40 | canLoad( 41 | route: Route, 42 | segments: UrlSegment[] 43 | ): 44 | | Observable 45 | | Promise 46 | | boolean 47 | | UrlTree { 48 | return this.checkAuth; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/app/pages/pages.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | // EXTRA MATERIAL MODULES 4 | import { MatIconModule } from '@angular/material/icon'; 5 | import { MatDialogModule } from '@angular/material/dialog'; 6 | import { MatMenuModule } from '@angular/material/menu'; 7 | import { MatSelectModule } from '@angular/material/select'; 8 | import { MatToolbarModule } from '@angular/material/toolbar'; 9 | import { MatTooltipModule } from '@angular/material/tooltip'; 10 | // SHARED MODULE 11 | import { SharedModule } from '@shared/shared.module'; 12 | import { PagesRoutingModule } from './pages-routing.module'; 13 | // COMPONENTS 14 | import { PagesComponent } from './pages.component'; 15 | import { Navbar } from '@shared/components'; 16 | import { UserTable, UserModal, UserForm } from './user-list/components'; 17 | //PAGES 18 | import { HomeComponent as HomePage } from './home/home.component'; 19 | import { UserListComponent as UserListPage } from './user-list/user-list.component'; 20 | 21 | @NgModule({ 22 | declarations: [ 23 | PagesComponent, 24 | Navbar, 25 | HomePage, 26 | UserListPage, 27 | UserTable, 28 | UserModal, 29 | UserForm, 30 | ], 31 | imports: [ 32 | CommonModule, 33 | MatIconModule, 34 | MatDialogModule, 35 | MatMenuModule, 36 | MatSelectModule, 37 | MatToolbarModule, 38 | MatTooltipModule, 39 | SharedModule, 40 | PagesRoutingModule, 41 | ], 42 | }) 43 | export class PagesModule {} 44 | -------------------------------------------------------------------------------- /src/app/core/interceptors/AuthInterceptor.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { 3 | HttpRequest, 4 | HttpHandler, 5 | HttpEvent, 6 | HttpInterceptor, 7 | HttpResponse, 8 | } from '@angular/common/http'; 9 | import { Observable } from 'rxjs'; 10 | import { delay, map } from 'rxjs/operators'; 11 | import { CookieService } from 'ngx-cookie-service'; 12 | // SERVICES 13 | import { LoadingSpinnerService } from '@core/services/common'; 14 | 15 | @Injectable() 16 | export class AuthInterceptor implements HttpInterceptor { 17 | constructor( 18 | private cookieService: CookieService, 19 | private spinnerService: LoadingSpinnerService 20 | ) {} 21 | intercept( 22 | req: HttpRequest, 23 | next: HttpHandler 24 | ): Observable> { 25 | // ADD TOKEN TO REQUEST HEADER 26 | this.spinnerService.addQuene(); 27 | const clonedRequest = this.cookieService.check('authToken') 28 | ? req.clone({ 29 | headers: req.headers.set( 30 | 'Authorization', 31 | 'Bearer ' + this.cookieService.get('authToken') 32 | ), 33 | }) 34 | : req; 35 | // ! REQUEST DELAYED FOR SHOWING SPINNER 36 | return next.handle(clonedRequest).pipe(delay(500), 37 | map, any>((evt: HttpEvent) => { 38 | if (evt instanceof HttpResponse) { 39 | // İstek bitiminde kuyruktan çıkarma 40 | this.spinnerService.removeQuene(); 41 | } 42 | return evt; 43 | }) 44 | ); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/app/core/services/form/form-validation.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | 3 | @Injectable({ 4 | providedIn: 'root', 5 | }) 6 | export class FormValidationService { 7 | constructor() {} 8 | // FIELD ERROR 9 | fieldHasError(fieldName: string, targetForm: any): boolean { 10 | const formField = targetForm?.controls[fieldName]; 11 | return formField?.invalid && formField?.touched ? true : false; 12 | } 13 | // FIELD ERROR MESSAGE 14 | getErrorMessage(fieldName: string, targetForm: any): string { 15 | const formField = targetForm?.get(fieldName); 16 | const fieldErrors = targetForm?.controls[fieldName].errors; 17 | return formField?.hasError('required') 18 | ? 'Reuired field' 19 | : // JSON SERVER ONLY APPLY EMAIL 20 | formField?.hasError('email') 21 | ? 'Username must be email' 22 | : formField?.hasError('minlength') 23 | ? `Input should contain at least 24 | ${this.getLengthError(fieldErrors?.['minlength'])} characters` 25 | : formField?.hasError('maxlength') 26 | ? `Input should contain max 27 | ${this.getLengthError(fieldErrors?.['maxlength'])} characters` 28 | : formField?.hasError('pattern') 29 | ? 'Password must contain one uppercase, one lowercase and one special characters of #?!@$%^&*-' 30 | : formField?.hasError('mismatch') 31 | ? 'Passwords mismatch' 32 | : 'Unknown error'; 33 | } 34 | // MAKE LENGTH ERRORS SHORTER 35 | private getLengthError(fieldError: any): string { 36 | return `(${fieldError?.actualLength} / ${fieldError?.requiredLength})`; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | jasmine: { 17 | // you can add configuration options for Jasmine here 18 | // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html 19 | // for example, you can disable the random execution with `random: false` 20 | // or set a specific seed with `seed: 4321` 21 | }, 22 | clearContext: false // leave Jasmine Spec Runner output visible in browser 23 | }, 24 | jasmineHtmlReporter: { 25 | suppressAll: true // removes the duplicated traces 26 | }, 27 | coverageReporter: { 28 | dir: require('path').join(__dirname, './coverage/user-management-interview'), 29 | subdir: '.', 30 | reporters: [ 31 | { type: 'html' }, 32 | { type: 'text-summary' } 33 | ] 34 | }, 35 | reporters: ['progress', 'kjhtml'], 36 | port: 9876, 37 | colors: true, 38 | logLevel: config.LOG_INFO, 39 | autoWatch: true, 40 | browsers: ['Chrome'], 41 | singleRun: false, 42 | restartOnFileChange: true 43 | }); 44 | }; 45 | -------------------------------------------------------------------------------- /src/manifest.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "user-management-interview", 3 | "short_name": "user-management-interview", 4 | "theme_color": "#1976d2", 5 | "background_color": "#fafafa", 6 | "display": "standalone", 7 | "scope": "./", 8 | "start_url": "./", 9 | "icons": [ 10 | { 11 | "src": "assets/icons/icon-72x72.png", 12 | "sizes": "72x72", 13 | "type": "image/png", 14 | "purpose": "maskable any" 15 | }, 16 | { 17 | "src": "assets/icons/icon-96x96.png", 18 | "sizes": "96x96", 19 | "type": "image/png", 20 | "purpose": "maskable any" 21 | }, 22 | { 23 | "src": "assets/icons/icon-128x128.png", 24 | "sizes": "128x128", 25 | "type": "image/png", 26 | "purpose": "maskable any" 27 | }, 28 | { 29 | "src": "assets/icons/icon-144x144.png", 30 | "sizes": "144x144", 31 | "type": "image/png", 32 | "purpose": "maskable any" 33 | }, 34 | { 35 | "src": "assets/icons/icon-152x152.png", 36 | "sizes": "152x152", 37 | "type": "image/png", 38 | "purpose": "maskable any" 39 | }, 40 | { 41 | "src": "assets/icons/icon-192x192.png", 42 | "sizes": "192x192", 43 | "type": "image/png", 44 | "purpose": "maskable any" 45 | }, 46 | { 47 | "src": "assets/icons/icon-384x384.png", 48 | "sizes": "384x384", 49 | "type": "image/png", 50 | "purpose": "maskable any" 51 | }, 52 | { 53 | "src": "assets/icons/icon-512x512.png", 54 | "sizes": "512x512", 55 | "type": "image/png", 56 | "purpose": "maskable any" 57 | } 58 | ] 59 | } 60 | -------------------------------------------------------------------------------- /src/app/core/guards/login.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { 3 | ActivatedRouteSnapshot, 4 | CanActivate, 5 | CanActivateChild, 6 | CanLoad, 7 | Route, 8 | Router, 9 | RouterStateSnapshot, 10 | UrlSegment, 11 | UrlTree, 12 | } from '@angular/router'; 13 | 14 | import { Observable } from 'rxjs'; 15 | // SERVICES 16 | import { CookieService } from 'ngx-cookie-service'; 17 | 18 | @Injectable({ 19 | providedIn: 'root', 20 | }) 21 | export class LoginGuard implements CanActivate, CanActivateChild, CanLoad { 22 | constructor(private cookieService: CookieService, private router: Router) {} 23 | // CHECK IF LOGGED 24 | get checkAuth() { 25 | const isLogged = this.cookieService.get('authToken') ? true : false; 26 | if (!isLogged) { 27 | return this.router.createUrlTree(['/auth']); 28 | } 29 | return isLogged; 30 | } 31 | canActivate( 32 | route: ActivatedRouteSnapshot, 33 | state: RouterStateSnapshot 34 | ): 35 | | Observable 36 | | Promise 37 | | boolean 38 | | UrlTree { 39 | return this.checkAuth; 40 | } 41 | canActivateChild( 42 | childRoute: ActivatedRouteSnapshot, 43 | state: RouterStateSnapshot 44 | ): 45 | | Observable 46 | | Promise 47 | | boolean 48 | | UrlTree { 49 | return this.checkAuth; 50 | } 51 | canLoad( 52 | route: Route, 53 | segments: UrlSegment[] 54 | ): 55 | | Observable 56 | | Promise 57 | | boolean 58 | | UrlTree { 59 | return this.checkAuth; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "user-management-interview", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "concurrently --kill-others \"npm run server\" \"ng serve\"", 7 | "build": "ng build", 8 | "watch": "ng build --watch --configuration development", 9 | "test": "ng test", 10 | "server": "json-server-auth db.json --routes routes.json" 11 | }, 12 | "private": true, 13 | "dependencies": { 14 | "@angular/animations": "~13.0.0", 15 | "@angular/cdk": "^13.0.0", 16 | "@angular/common": "~13.0.0", 17 | "@angular/compiler": "~13.0.0", 18 | "@angular/core": "~13.0.0", 19 | "@angular/forms": "~13.0.0", 20 | "@angular/material": "^13.0.0", 21 | "@angular/platform-browser": "~13.0.0", 22 | "@angular/platform-browser-dynamic": "~13.0.0", 23 | "@angular/router": "~13.0.0", 24 | "@angular/service-worker": "~13.0.0", 25 | "bootstrap": "^5.1.3", 26 | "ngx-cookie-service": "^13.0.0", 27 | "rxjs": "~7.4.0", 28 | "tslib": "^2.3.0", 29 | "uuid": "^8.3.2", 30 | "zone.js": "~0.11.4" 31 | }, 32 | "devDependencies": { 33 | "@angular-devkit/build-angular": "~13.0.1", 34 | "@angular/cli": "~13.0.1", 35 | "@angular/compiler-cli": "~13.0.0", 36 | "@types/jasmine": "~3.10.0", 37 | "@types/node": "^12.11.1", 38 | "@types/uuid": "^8.3.1", 39 | "concurrently": "^6.3.0", 40 | "jasmine-core": "~3.10.0", 41 | "json-server": "^0.17.0", 42 | "json-server-auth": "^2.1.0", 43 | "karma": "~6.3.0", 44 | "karma-chrome-launcher": "~3.1.0", 45 | "karma-coverage": "~2.0.3", 46 | "karma-jasmine": "~4.0.0", 47 | "karma-jasmine-html-reporter": "~1.7.0", 48 | "typescript": "~4.4.3" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/app/pages/auth/contents/login/login.component.html: -------------------------------------------------------------------------------- 1 | 5 |
6 | 7 | Username 8 | 9 | 10 | {{ getErrorMessage("email") }} 11 | 12 | 13 | 14 | Password 15 | 21 | 22 | 32 | 33 | 34 | {{ getErrorMessage("password") }} 35 | 36 | 37 | 38 |
39 | 47 |
48 | 51 |
52 | -------------------------------------------------------------------------------- /src/app/core/guards/admin.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { 3 | ActivatedRouteSnapshot, 4 | CanActivate, 5 | CanActivateChild, 6 | CanLoad, 7 | Route, 8 | Router, 9 | RouterStateSnapshot, 10 | UrlSegment, 11 | UrlTree, 12 | } from '@angular/router'; 13 | import { Observable } from 'rxjs'; 14 | 15 | // SERVICES 16 | import { AuthService } from '@core/services/auth'; 17 | 18 | @Injectable({ 19 | providedIn: 'root', 20 | }) 21 | export class AdminGuard implements CanActivate, CanActivateChild, CanLoad { 22 | constructor(private authService: AuthService, private router: Router) {} 23 | // CHECK IF IS ADMIN FROM SERVER 24 | private async isAdmin(): Promise { 25 | const currentUser = await this.authService.userProfile(); 26 | const userIsAdmin = currentUser && currentUser?.role > 1 ? true : false; 27 | if (!userIsAdmin) { 28 | return this.router.createUrlTree(['/error/401']); 29 | } 30 | return userIsAdmin; 31 | } 32 | canActivate( 33 | route: ActivatedRouteSnapshot, 34 | state: RouterStateSnapshot 35 | ): 36 | | Observable 37 | | Promise 38 | | boolean 39 | | UrlTree { 40 | return this.isAdmin(); 41 | } 42 | canActivateChild( 43 | childRoute: ActivatedRouteSnapshot, 44 | state: RouterStateSnapshot 45 | ): 46 | | Observable 47 | | Promise 48 | | boolean 49 | | UrlTree { 50 | return this.isAdmin(); 51 | } 52 | canLoad( 53 | route: Route, 54 | segments: UrlSegment[] 55 | ): 56 | | Observable 57 | | Promise 58 | | boolean 59 | | UrlTree { 60 | return this.isAdmin(); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/app/pages/user-list/components/user-table/user-table.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 17 | 27 | 48 | 49 | 50 | 51 | 52 | 53 |
FullnameUsernameRole
18 | 26 | 28 | 37 | 38 | 42 | 46 | 47 | {{ user?.fullName }}{{ user?.email }}{{ visualizeUserRole(user?.role) }}
54 |
55 | -------------------------------------------------------------------------------- /src/app/pages/auth/contents/login/login.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | // ANGULAR FORM 3 | import { FormBuilder, FormGroup, Validators } from '@angular/forms'; 4 | // SERVICES 5 | import { AuthService } from '@core/services/auth'; 6 | import { FormValidationService } from '@core/services/form'; 7 | @Component({ 8 | selector: 'app-login', 9 | templateUrl: './login.component.html', 10 | styleUrls: ['./login.component.scss'], 11 | }) 12 | export class LoginComponent implements OnInit { 13 | showPassword: boolean = false; 14 | // INIT LOGIN FORM DIRECTLY 15 | loginForm: FormGroup = this.formBuilder.group({ 16 | email: [ 17 | '', 18 | Validators.compose([ 19 | Validators.required, 20 | Validators.email, 21 | Validators.minLength(5), 22 | Validators.maxLength(30), 23 | ]), 24 | ], 25 | password: [ 26 | '', 27 | Validators.compose([ 28 | Validators.required, 29 | Validators.minLength(8), 30 | Validators.maxLength(16), 31 | Validators.pattern( 32 | '(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!@$%^&*-]).{8,}$' 33 | ), 34 | ]), 35 | ], 36 | }); 37 | 38 | constructor( 39 | private formBuilder: FormBuilder, 40 | private authService: AuthService, 41 | private formValidationService: FormValidationService 42 | ) {} 43 | 44 | ngOnInit(): void {} 45 | // FIELD ERROR 46 | fieldHasError(fieldName: string): boolean { 47 | return this.formValidationService.fieldHasError(fieldName, this.loginForm); 48 | } 49 | // FIELD ERROR MESSAGE 50 | getErrorMessage(fieldName: string): string { 51 | return this.formValidationService.getErrorMessage( 52 | fieldName, 53 | this.loginForm 54 | ); 55 | } 56 | // SUBMIT LOGIN FORM 57 | onLoginSubmit() { 58 | if (this.loginForm.valid) { 59 | this.authService.login(this.loginForm.value); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/app/pages/auth/contents/register/register.component.html: -------------------------------------------------------------------------------- 1 |
2 |

Create new Account

3 |
4 |
5 | 6 | Username 7 | 8 | 9 | {{ getErrorMessage("email") }} 10 | 11 | 12 | 13 | Fullname 14 | 15 | 16 | {{ getErrorMessage("fullName") }} 17 | 18 | 19 | 20 | Password 21 | 27 | 28 | 38 | 39 | 40 | {{ getErrorMessage("password") }} 41 | 42 | 43 | 44 | Password Repeat 45 | 51 | 52 | {{ getErrorMessage("passwordConfirm") }} 53 | 54 | 55 | 56 |
57 | 65 |
66 | 69 |
70 | -------------------------------------------------------------------------------- /src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes recent versions of Safari, Chrome (including 12 | * Opera), Edge on the desktop, and iOS and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/guide/browser-support 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** 22 | * By default, zone.js will patch all possible macroTask and DomEvents 23 | * user can disable parts of macroTask/DomEvents patch by setting following flags 24 | * because those flags need to be set before `zone.js` being loaded, and webpack 25 | * will put import in the top of bundle, so user need to create a separate file 26 | * in this directory (for example: zone-flags.ts), and put the following flags 27 | * into that file, and then add the following code before importing zone.js. 28 | * import './zone-flags'; 29 | * 30 | * The flags allowed in zone-flags.ts are listed here. 31 | * 32 | * The following flags will work for all browsers. 33 | * 34 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 35 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 36 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 37 | * 38 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 39 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 40 | * 41 | * (window as any).__Zone_enable_cross_context_check = true; 42 | * 43 | */ 44 | 45 | /*************************************************************************************************** 46 | * Zone JS is required by default for Angular itself. 47 | */ 48 | import 'zone.js'; // Included with Angular CLI. 49 | 50 | 51 | /*************************************************************************************************** 52 | * APPLICATION IMPORTS 53 | */ 54 | -------------------------------------------------------------------------------- /src/app/shared/components/navbar/navbar.component.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 | 13 |
14 | 15 | 21 | home 22 | 23 | 30 | group 31 | 32 | 33 | 34 | 42 | 43 | 47 | 51 | 52 | 53 | 54 | 62 | 63 | 67 | 71 | 72 | 73 | 74 |
75 |
76 |
77 |
78 | -------------------------------------------------------------------------------- /src/app/pages/user-list/components/user-from/user-from.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | Username 4 | 5 | 6 | {{ getErrorMessage("email") }} 7 | 8 | 9 | 10 | Fullname 11 | 12 | 13 | {{ getErrorMessage("fullName") }} 14 | 15 | 16 | 17 | 18 | 19 | Role 20 | 21 | 22 | {{ userRole.viewVal }} 23 | 24 | 25 | 26 | {{ getErrorMessage("role") }} 27 | 28 | 29 | 30 | 31 | 32 | Password 33 | 39 | 40 | 52 | 53 | 54 | {{ getErrorMessage("password") }} 55 | 56 | 57 | 58 | Password Repeat 59 | 65 | 66 | {{ getErrorMessage("passwordConfirm") }} 67 | 68 | 69 | 70 |
71 | -------------------------------------------------------------------------------- /src/app/pages/user-list/user-list.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { MatDialog } from '@angular/material/dialog'; 3 | 4 | // SERVICES 5 | import { UserListService } from '@core/services/auth'; 6 | import { SnackMessageService } from '@core/services/notifcation'; 7 | // MODELS 8 | import { PROFILE } from '@models/auth'; 9 | import { UserModal } from './components'; 10 | // COMP 11 | @Component({ 12 | selector: 'app-user-list', 13 | templateUrl: './user-list.component.html', 14 | styleUrls: ['./user-list.component.scss'], 15 | }) 16 | export class UserListComponent implements OnInit { 17 | userList!: PROFILE[]; 18 | constructor( 19 | private userListService: UserListService, 20 | private dialog: MatDialog, 21 | private messageService: SnackMessageService 22 | ) {} 23 | 24 | async ngOnInit() { 25 | this.userList = await this.userListService.getAllUsers(); 26 | } 27 | async createNewUser() { 28 | try { 29 | const { success, userData } = await this.openUserModal(); 30 | if (success) { 31 | this.userList.push(userData); 32 | } 33 | } catch (error: any) { 34 | this.messageService.show({ 35 | message: error?.message || 'An error occoured when creating new user', 36 | }); 37 | } 38 | } 39 | 40 | async updateUser(user: PROFILE) { 41 | try { 42 | const { success, userData } = await this.openUserModal(user); 43 | if (success) { 44 | const userIndex = this.userList.findIndex( 45 | (usr) => usr?.id === user?.id 46 | ); 47 | if (userIndex >= 0) { 48 | this.userList[userIndex] = userData; 49 | this.messageService.show({ 50 | message: `User (${userData?.fullName}) has been updated successfully`, 51 | duration: 4000, 52 | }); 53 | } 54 | } 55 | } catch (error: any) { 56 | this.messageService.show({ 57 | message: error?.message || 'An error occoured when updating user', 58 | }); 59 | } 60 | } 61 | async deleteUser(userData: PROFILE) { 62 | const { success } = await this.userListService.deleteUser(userData?.id); 63 | if (success) { 64 | const userIndex = this.userList.findIndex( 65 | (usr) => usr.id === userData?.id 66 | ); 67 | if (userIndex >= 0) { 68 | this.userList.splice(userIndex, 1); 69 | this.messageService.show({ 70 | message: `User (${userData?.fullName}) has been removed successfully`, 71 | }); 72 | } 73 | } 74 | } 75 | // OPEN MODAL WITH SOME CONFIGRATION 76 | private async openUserModal(user?: PROFILE) { 77 | const userDialog = this.dialog.open(UserModal, { 78 | width: '450px', 79 | maxWidth: '100%', 80 | data: user, 81 | disableClose: true, 82 | }); 83 | return await userDialog.afterClosed().toPromise(); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/app/pages/auth/contents/register/register.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | // ANGULAR FORM 3 | import { 4 | FormControl, 5 | Validators, 6 | FormGroup, 7 | AbstractControl, 8 | ValidatorFn, 9 | ValidationErrors, 10 | } from '@angular/forms'; 11 | // SERVICES 12 | import { AuthService } from '@core/services/auth'; 13 | import { FormValidationService } from '@core/services/form'; 14 | @Component({ 15 | selector: 'app-register', 16 | templateUrl: './register.component.html', 17 | styleUrls: ['./register.component.scss'], 18 | }) 19 | export class RegisterComponent implements OnInit { 20 | // SHOW AND HIDE PW FOR USER EXPERIENCE 21 | showPassword: boolean = false; 22 | // REGISTER FORM GROUP 23 | registerForm: FormGroup; 24 | constructor( 25 | private authService: AuthService, 26 | private formValidationService: FormValidationService 27 | ) { 28 | // INIT REGISTER FORM 29 | this.registerForm = this.initRegisterForm; 30 | } 31 | 32 | ngOnInit(): void {} 33 | // REGISTER FORM PROPERTIES 34 | private get initRegisterForm() { 35 | return new FormGroup( 36 | { 37 | fullName: new FormControl('', [ 38 | Validators.required, 39 | Validators.minLength(3), 40 | Validators.maxLength(60), 41 | ]), 42 | email: new FormControl('', [ 43 | Validators.required, 44 | Validators.email, 45 | Validators.minLength(5), 46 | Validators.maxLength(30), 47 | ]), 48 | password: new FormControl('', [ 49 | Validators.required, 50 | Validators.minLength(8), 51 | Validators.maxLength(16), 52 | Validators.pattern( 53 | '(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!@$%^&*-]).{8,}$' 54 | ), 55 | ]), 56 | passwordConfirm: new FormControl('', [ 57 | Validators.required, 58 | Validators.minLength(8), 59 | Validators.maxLength(16), 60 | 61 | Validators.pattern( 62 | '(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!@$%^&*-]).{8,}$' 63 | ), 64 | 65 | this.passwordMatchValidator(), 66 | ]), 67 | }, 68 | // TODO CAN ACTIVATE FOR BETTER PERFORMANCE 69 | // { updateOn: 'blur' } 70 | ); 71 | } 72 | // FIELD ERROR 73 | fieldHasError(fieldName: string): boolean { 74 | return this.formValidationService.fieldHasError( 75 | fieldName, 76 | this.registerForm 77 | ); 78 | } 79 | // FIELD ERROR MESSAGE 80 | getErrorMessage(fieldName: string): string { 81 | return this.formValidationService.getErrorMessage( 82 | fieldName, 83 | this.registerForm 84 | ); 85 | } 86 | // SUBMIT REGISTER FORM 87 | onRegisterSubmit() { 88 | if (this.registerForm.valid) { 89 | this.authService.register(this.registerForm.value); 90 | } 91 | } 92 | // CUSTOM VALIDATOR 93 | private passwordMatchValidator(): ValidatorFn { 94 | return (control: AbstractControl): ValidationErrors | null => { 95 | const passwordVal = this.registerForm?.get('password')?.value; 96 | const forbidden = control.value !== passwordVal; 97 | return forbidden ? { mismatch: true } : null; 98 | }; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/app/core/services/common/api.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | // FOR HTTP REQ 3 | import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http'; 4 | import { shareReplay } from 'rxjs/operators'; 5 | // FOR DIFFRENT DEPLOYMENT OPTIONS 6 | import { environment } from '@environments/environment'; 7 | // HTTP MODELS 8 | import { HTTP_REQ, HTTP_RES } from '@models/common'; 9 | @Injectable({ 10 | providedIn: 'root', 11 | }) 12 | export class ApiService { 13 | // API URL 14 | private readonly apiUrl = environment.apiUrl; 15 | constructor(private http: HttpClient) {} 16 | // GET REQUEST 17 | public async get(httpData: HTTP_REQ):Promise { 18 | try { 19 | const httpOptions = this.generateHttpOptions( 20 | httpData.params, 21 | httpData.headers 22 | ); 23 | const result: any = await this.http 24 | .get(`${this.apiUrl}/${httpData.url}`, httpOptions) 25 | .pipe(shareReplay()) 26 | .toPromise(); 27 | return { success: true, data: result, error: null }; 28 | } catch (error:any) { 29 | return { success: false, data: null, error }; 30 | } 31 | } 32 | // POST REQUEST 33 | public async post(httpData: HTTP_REQ) { 34 | try { 35 | const httpOptions = this.generateHttpOptions( 36 | httpData.params, 37 | httpData.headers 38 | ); 39 | const result: any = await this.http 40 | .post(`${this.apiUrl}/${httpData.url}`, httpData.body, httpOptions) 41 | .pipe(shareReplay()) 42 | .toPromise(); 43 | return { success: true, data: result, error: null }; 44 | } catch (error:any) { 45 | return { success: false, data: null, error }; 46 | } 47 | } 48 | // PUT REQUEST 49 | public async put(httpData: HTTP_REQ) { 50 | try { 51 | const httpOptions = this.generateHttpOptions( 52 | httpData.params, 53 | httpData.headers 54 | ); 55 | const result: any = await this.http 56 | .put(`${this.apiUrl}/${httpData.url}`, httpData.body, httpOptions) 57 | .pipe(shareReplay()) 58 | .toPromise(); 59 | return { success: true, data: result, error: null }; 60 | } catch (error:any) { 61 | return { success: false, data: null, error }; 62 | } 63 | } 64 | // DELETE REQUEST 65 | public async delete(httpData: HTTP_REQ) { 66 | try { 67 | const result: any = await this.http 68 | .delete(`${this.apiUrl}/${httpData.url}`, httpData.body) 69 | .pipe(shareReplay()) 70 | .toPromise(); 71 | return { success: true, data: result, error: null }; 72 | } catch (error:any) { 73 | return { success: false, data: null, error }; 74 | } 75 | } 76 | // DYNAMIC HTTP OPTIONS 77 | private generateHttpOptions(params: any, headers: any) { 78 | const httpOptions: any = {}; 79 | if (params) { 80 | let httpParams = new HttpParams(); 81 | for (const key in params) { 82 | if (Object.prototype.hasOwnProperty.call(params, key)) { 83 | const paramValue = params[key]; 84 | httpParams = httpParams.append(key, paramValue); 85 | } 86 | } 87 | httpOptions.params = httpParams; 88 | } 89 | if (headers) { 90 | let httpHeaders = new HttpHeaders(); 91 | for (const key in headers) { 92 | if (Object.prototype.hasOwnProperty.call(headers, key)) { 93 | const headerValue = headers[key]; 94 | httpHeaders = httpHeaders.append(key, headerValue); 95 | } 96 | } 97 | httpOptions.headers = httpHeaders; 98 | } 99 | return httpOptions; 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/app/pages/user-list/components/user-from/user-from.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, Input } from '@angular/core'; 2 | import { 3 | FormControl, 4 | Validators, 5 | FormGroup, 6 | AbstractControl, 7 | ValidatorFn, 8 | ValidationErrors, 9 | } from '@angular/forms'; 10 | 11 | // SERVICES 12 | import { GlobalDataService } from '@core/services/common'; 13 | import { FormValidationService } from '@core/services/form'; 14 | // MODELS 15 | import { PROFILE } from '@models/auth'; 16 | 17 | @Component({ 18 | selector: 'user-from', 19 | templateUrl: './user-from.component.html', 20 | styleUrls: ['./user-from.component.scss'], 21 | }) 22 | export class UserFromComponent implements OnInit { 23 | @Input() userData!: PROFILE; 24 | readonly userRoles = [ 25 | { val: 1, viewVal: 'Customer' }, 26 | { val: 2, viewVal: 'Admin' }, 27 | { val: 3, viewVal: 'Super Admin' }, 28 | ]; 29 | currentUser: PROFILE | null = this.globalData.currentUser$.getValue(); 30 | // SHOW AND HIDE PW FOR USER EXPERIENCE 31 | showPassword: boolean = false; 32 | // USER FORM GROUP 33 | userForm!: FormGroup; 34 | constructor( 35 | private formValidationService: FormValidationService, 36 | private globalData: GlobalDataService 37 | ) { 38 | // INIT USER FORM 39 | } 40 | 41 | ngOnInit(): void { 42 | this.userForm = this.inituserForm; 43 | } 44 | // GET USER FORM DATA 45 | get getFormData() { 46 | return { ...this.userForm.value, role: this.userForm.value?.role || 1 }; 47 | } 48 | // USER FORM PROPERTIES 49 | private get inituserForm() { 50 | const passwordValidator = [ 51 | Validators.required, 52 | Validators.minLength(8), 53 | Validators.maxLength(16), 54 | Validators.pattern( 55 | '(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!@$%^&*-]).{8,}$' 56 | ), 57 | ]; 58 | const passwordConfirmValidator = [ 59 | ...passwordValidator, 60 | this.passwordMatchValidator(), 61 | ]; 62 | 63 | return new FormGroup( 64 | { 65 | fullName: new FormControl(this.userData?.fullName || '', [ 66 | Validators.required, 67 | Validators.minLength(3), 68 | Validators.maxLength(60), 69 | ]), 70 | email: new FormControl( 71 | { 72 | value: this.userData?.email || '', 73 | //! EMAIL CANT BE CHANGE BECAUSE OF USED TO AUTH 74 | disabled: this.userData ? true : false, 75 | }, 76 | [ 77 | Validators.required, 78 | Validators.email, 79 | Validators.minLength(5), 80 | Validators.maxLength(30), 81 | ] 82 | ), 83 | role: new FormControl(this.userData?.role || '', []), 84 | password: new FormControl('', this.userData ? [] : passwordValidator), 85 | passwordConfirm: new FormControl( 86 | '', 87 | this.userData ? [] : passwordConfirmValidator 88 | ), 89 | } 90 | // TODO CAN ACTIVATE FOR BETTER PERFORMANCE 91 | // { updateOn: 'blur' } 92 | ); 93 | } 94 | // FIELD ERROR 95 | fieldHasError(fieldName: string): boolean { 96 | return this.formValidationService.fieldHasError(fieldName, this.userForm); 97 | } 98 | // FIELD ERROR MESSAGE 99 | getErrorMessage(fieldName: string): string { 100 | return this.formValidationService.getErrorMessage(fieldName, this.userForm); 101 | } 102 | 103 | // CUSTOM VALIDATOR 104 | private passwordMatchValidator(): ValidatorFn { 105 | return (control: AbstractControl): ValidationErrors | null => { 106 | const passwordVal = this.userForm?.get('password')?.value; 107 | const forbidden = control.value !== passwordVal; 108 | return forbidden ? { mismatch: true } : null; 109 | }; 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "user-management-interview": { 7 | "projectType": "application", 8 | "schematics": { 9 | "@schematics/angular:component": { 10 | "style": "scss" 11 | }, 12 | "@schematics/angular:application": { 13 | "strict": true 14 | } 15 | }, 16 | "root": "", 17 | "sourceRoot": "src", 18 | "prefix": "app", 19 | "architect": { 20 | "build": { 21 | "builder": "@angular-devkit/build-angular:browser", 22 | "options": { 23 | "outputPath": "dist/user-management-interview", 24 | "index": "src/index.html", 25 | "main": "src/main.ts", 26 | "polyfills": "src/polyfills.ts", 27 | "tsConfig": "tsconfig.app.json", 28 | "inlineStyleLanguage": "scss", 29 | "assets": [ 30 | "src/favicon.ico", 31 | "src/assets", 32 | "src/manifest.webmanifest" 33 | ], 34 | "styles": [ 35 | "src/styles.scss" 36 | ], 37 | "scripts": [], 38 | "serviceWorker": true, 39 | "ngswConfigPath": "ngsw-config.json" 40 | }, 41 | "configurations": { 42 | "production": { 43 | "budgets": [ 44 | { 45 | "type": "initial", 46 | "maximumWarning": "500kb", 47 | "maximumError": "1mb" 48 | }, 49 | { 50 | "type": "anyComponentStyle", 51 | "maximumWarning": "2kb", 52 | "maximumError": "4kb" 53 | } 54 | ], 55 | "fileReplacements": [ 56 | { 57 | "replace": "src/environments/environment.ts", 58 | "with": "src/environments/environment.prod.ts" 59 | } 60 | ], 61 | "outputHashing": "all" 62 | }, 63 | "development": { 64 | "buildOptimizer": false, 65 | "optimization": false, 66 | "vendorChunk": true, 67 | "extractLicenses": false, 68 | "sourceMap": true, 69 | "namedChunks": true 70 | } 71 | }, 72 | "defaultConfiguration": "production" 73 | }, 74 | "serve": { 75 | "builder": "@angular-devkit/build-angular:dev-server", 76 | "configurations": { 77 | "production": { 78 | "browserTarget": "user-management-interview:build:production" 79 | }, 80 | "development": { 81 | "browserTarget": "user-management-interview:build:development" 82 | } 83 | }, 84 | "defaultConfiguration": "development" 85 | }, 86 | "extract-i18n": { 87 | "builder": "@angular-devkit/build-angular:extract-i18n", 88 | "options": { 89 | "browserTarget": "user-management-interview:build" 90 | } 91 | }, 92 | "test": { 93 | "builder": "@angular-devkit/build-angular:karma", 94 | "options": { 95 | "main": "src/test.ts", 96 | "polyfills": "src/polyfills.ts", 97 | "tsConfig": "tsconfig.spec.json", 98 | "karmaConfig": "karma.conf.js", 99 | "inlineStyleLanguage": "scss", 100 | "assets": [ 101 | "src/favicon.ico", 102 | "src/assets", 103 | "src/manifest.webmanifest" 104 | ], 105 | "styles": [ 106 | "src/styles.scss" 107 | ], 108 | "scripts": [] 109 | } 110 | } 111 | } 112 | } 113 | }, 114 | "defaultProject": "user-management-interview" 115 | } 116 | -------------------------------------------------------------------------------- /src/app/core/services/auth/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | // TO NAVIGATE AFTER LOGIN 3 | import { Router } from '@angular/router'; 4 | 5 | import { v4 as uuidv4 } from 'uuid'; 6 | // SAVE TOKEN TO COOKIES 7 | import { CookieService } from 'ngx-cookie-service'; 8 | // SERVICES 9 | import { ApiService, GlobalDataService } from '../common'; 10 | import { SnackMessageService } from '../notifcation'; 11 | // MODELS 12 | import { HTTP_REQ } from '@models/common'; 13 | import { LOGIN_FORM_DATA, PROFILE, REGISTER_FORM_DATA } from '@models/auth'; 14 | 15 | @Injectable({ 16 | providedIn: 'root', 17 | }) 18 | export class AuthService { 19 | constructor( 20 | private cookieService: CookieService, 21 | private router: Router, 22 | private apiService: ApiService, 23 | private snackMessage: SnackMessageService, 24 | private globalDataService: GlobalDataService 25 | ) {} 26 | // REGISTER 27 | async register(formData: REGISTER_FORM_DATA) { 28 | delete formData.passwordConfirm; 29 | // ! JSON SERVER NOT RETURN ID VALUE 30 | const userUUID = uuidv4(); 31 | const httpData: HTTP_REQ = { 32 | url: 'register', 33 | body: { 34 | email: formData.email, 35 | password: formData.password, 36 | id: userUUID, 37 | }, 38 | }; 39 | const { success, data, error } = await this.apiService.post(httpData); 40 | 41 | if (success && data?.accessToken) { 42 | // ! JSON AUTH SERVER HAS NOT PUT OR DELETE FOR USERS SCHEMA 43 | // ! ADDITIONAL INFO WILL BE SAVE IN PROFILES SCHEMA 44 | this.setCookies(data?.accessToken, formData?.email); 45 | 46 | const profileHttpData: HTTP_REQ = { 47 | url: 'profiles', 48 | body: { 49 | userId: userUUID, 50 | email: formData.email, 51 | fullName: formData.fullName, 52 | role: 1, 53 | }, 54 | }; 55 | const profileResult = await this.apiService.post(profileHttpData); 56 | if (profileResult?.success) { 57 | this.router.navigate(['']); 58 | } 59 | } else { 60 | this.snackMessage.show({ 61 | message: error?.message || 'Failure during register', 62 | }); 63 | } 64 | } 65 | // LOGIN 66 | async login(formData: LOGIN_FORM_DATA) { 67 | const httpData: HTTP_REQ = { url: 'login', body: formData }; 68 | const { success, data, error } = await this.apiService.post(httpData); 69 | if (success && data?.accessToken) { 70 | this.setCookies(data?.accessToken, formData?.email); 71 | this.router.navigate(['']); 72 | } else { 73 | this.snackMessage.show({ 74 | message: error?.message || 'Failure during login', 75 | }); 76 | } 77 | } 78 | async userProfile(): Promise { 79 | const userMail = this.cookieService.get('email'); 80 | const httpData: HTTP_REQ = { url: 'profiles', params: { email: userMail } }; 81 | const { success, error, data } = await this.apiService.get(httpData); 82 | if (success && data?.length > 0) { 83 | const userInfo: PROFILE = data[0]; 84 | this.globalDataService.currentUser$.next(userInfo); 85 | return userInfo; 86 | } else { 87 | this.snackMessage.show({ 88 | message: error?.message || 'Failure during get profile', 89 | }); 90 | return null; 91 | } 92 | } 93 | // LOGOUT 94 | logOut() { 95 | this.cookieService.deleteAll(); 96 | this.globalDataService.currentUser$.next(null); 97 | this.router.navigate(['/auth']); 98 | } 99 | private setCookies(oAuthToken: string, email: string) { 100 | // JSON-SERVER TOKEN EXPIRES IN 1 HOUR 101 | const expires = this.expireTime1Hour; 102 | this.cookieService.set('authToken', oAuthToken, { 103 | path: '/', 104 | expires, 105 | }); 106 | this.cookieService.set('email', email, { path: '/', expires }); 107 | } 108 | // GET NEXT 1 HOUR 109 | private get expireTime1Hour() { 110 | const dNow = new Date(); 111 | let dTime = dNow.getTime(); 112 | dTime += 3600 * 1000; 113 | dNow.setTime(dTime); 114 | return dNow; 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/app/core/services/auth/user-list.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { v4 as uuidv4 } from 'uuid'; 3 | // SERVICES 4 | import { ApiService, GlobalDataService } from '../common'; 5 | import { SnackMessageService } from '../notifcation'; 6 | // MODELS 7 | import { HTTP_REQ } from '@models/common'; 8 | import { PROFILE, REGISTER_FORM_DATA } from '@models/auth'; 9 | @Injectable({ 10 | providedIn: 'root', 11 | }) 12 | export class UserListService { 13 | constructor( 14 | private apiService: ApiService, 15 | private snackMessage: SnackMessageService, 16 | private globalDataService: GlobalDataService 17 | ) {} 18 | // LIST USERS 19 | async getAllUsers(): Promise { 20 | const currentUser: PROFILE | null = 21 | this.globalDataService.currentUser$.getValue(); 22 | 23 | const httpData: HTTP_REQ = { 24 | url: 'profiles', 25 | params: { role_lte: this.getRoleLTE(currentUser?.role) }, 26 | }; 27 | const { success, error, data } = await this.apiService.get(httpData); 28 | if (success && data?.length > 0) { 29 | return data; 30 | } else { 31 | this.snackMessage.show({ 32 | message: error?.message || 'Failure during list users profile', 33 | }); 34 | return []; 35 | } 36 | } 37 | // ADD NEW USER 38 | async addNewUser( 39 | formData: REGISTER_FORM_DATA 40 | ): Promise<{ success: boolean; user: PROFILE }> { 41 | const userUUID = uuidv4(); 42 | // REGISTER USER 43 | const httpData: HTTP_REQ = { 44 | url: 'register', 45 | body: { 46 | email: formData?.email, 47 | password: formData?.password, 48 | id: userUUID, 49 | }, 50 | }; 51 | const { success, data, error } = await this.apiService.post(httpData); 52 | if (success && data?.accessToken) { 53 | // IF USER REGISTERED SUCCESSFULLY THEN CREATE PROFILE DATA 54 | const profileHttpData: HTTP_REQ = { 55 | url: 'profiles', 56 | body: { 57 | userId: userUUID, 58 | email: formData.email, 59 | fullName: formData.fullName, 60 | role: formData?.role, 61 | }, 62 | }; 63 | const profileResult = await this.apiService.post(profileHttpData); 64 | if (profileResult?.success) { 65 | this.snackMessage.show({ 66 | message: `User (${formData?.fullName}) has been created`, 67 | }); 68 | return { success: true, user: profileResult?.data }; 69 | } else { 70 | return { success: false, user: profileResult?.data }; 71 | } 72 | } else { 73 | this.snackMessage.show({ 74 | message: error?.message || 'Failure during register', 75 | }); 76 | return { success: false, user: data }; 77 | } 78 | } 79 | 80 | async updateUser( 81 | user: PROFILE 82 | ): Promise<{ success: boolean; user: PROFILE }> { 83 | const httpData: HTTP_REQ = { 84 | url: `profiles/${user.id}`, 85 | body: user, 86 | }; 87 | const { success, error, data } = await this.apiService.put(httpData); 88 | if (success) { 89 | return { success: true, user: data }; 90 | } else { 91 | this.snackMessage.show({ 92 | message: error?.message || 'Failure during update', 93 | }); 94 | return { success: false, user: data }; 95 | } 96 | } 97 | async deleteUser( 98 | userID: number 99 | ): Promise<{ success: boolean; user: PROFILE }> { 100 | const httpData: HTTP_REQ = { 101 | url: `profiles/${userID}`, 102 | }; 103 | const { success, error, data } = await this.apiService.delete(httpData); 104 | if (success) { 105 | return { success: true, user: data }; 106 | } else { 107 | this.snackMessage.show({ 108 | message: error?.message || 'Failure during update', 109 | }); 110 | return { success: false, user: data }; 111 | } 112 | } 113 | // LIST USERS WITH ROLE 114 | private getRoleLTE(userRole: number | undefined) { 115 | switch (userRole) { 116 | // SUPER ADMIN CAN LIST ALL 117 | case 3: 118 | return 3; 119 | // ADMIN CAN LIST USERS 120 | case 2: 121 | return 1; 122 | // OTHERS CANT LIST 123 | default: 124 | return -1; 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/app/pages/home/home.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | {{ currentUser?.fullName }} 6 | {{ userRoles[currentUser.role] }} 7 |
8 |
9 | 10 | 11 |

User Authorization Project

12 |

13 | Welcome to User Management App! There is 3 types (Super Admin, Admin, 14 | Customer) of roles. This app has 6 pages (Login, Register, Home, User 15 | Listing, 401 and 404). 16 |

17 | 18 |
19 | Some available accounts: 20 |
    21 |
  • superadmin@mail.com
  • 22 |
  • admin@mail.com
  • 23 |
  • customer@mail.com
  • 24 |
25 | 26 | All passwords are same: 12345@Aa 27 | 28 |

29 | You could take a look to db.json for more account data. 30 |

31 |
32 |

Rules

33 |
    34 |
  • 35 | All users can view Homepage when they logged in. Homepage protected by 36 | LoginGuard (Router-Guard) that allow to only authorized 37 | users. If user have not token will be redirect to Login page. 38 |
  • 39 |
  • 40 | Navigation provided by Navbar buttons by using RouterLink. 41 | User List button will be hidden if you are not Super Admin. 42 |
  • 43 |
  • 44 | User List page protected by 45 | AdminGuard (Router-Guard) that allow to access only users 46 | has role Super Admin. If Customer try to go User List page 47 | will be redirect to 401 - Forbidden page. 48 |
  • 49 |
  • 50 | Edit user, add user, delete user functions available in User List page. 51 |
  • 52 |
  • 53 | Username cannot be change because of used to login. Also changing the 54 | password is not allowed. 55 |
  • 56 |
  • 57 | Admin can't change user roles and can't list other admins. When admin 58 | create a user, user's role will be Customer by the default. Also 59 | Admin can delete only customers. 60 |
  • 61 |
  • Super Admin can update role and can list all users.
  • 62 |
  • 63 | Super Admin can't delete itself, but can delete other Super Admin's 64 | accounts. 65 |
  • 66 |
  • 67 | Everyone can login or register to application. But if logged user try to 68 | reach Login or Register page will be redirect to Home page. Login and 69 | Register pages protected by NotLoginGuard (Router-Guard). 70 |
  • 71 |
  • 72 | This app uses JSON Server for creating fake database and 73 | JSON Server Auth to manage login - register operations. By 74 | the way auth token has 1 hour life time. 75 |
  • 76 |
77 |

Roles

78 |
    79 |
  • Super Admin (role value: 3)
  • 80 |
  • Admin (role value: 2)
  • 81 |
  • Customer (role value: 1)
  • 82 |
83 |

About App

84 |
    85 |
  • Created with Angular v13
  • 86 |
  • Uses Angular Material Framework for app compoments
  • 87 |
  • CSS template engine is SCSS
  • 88 |
  • 89 | Bootstrap Reboot, Bootstrap Grid, Bootstrap Tables and Bootsrap Aler 90 | imported with Sass 91 |
  • 92 |
  • Ngx Cookie Service used to handle cookies
  • 93 |
  • User ids gererated with uuid
  • 94 |
  • concurrently used to run multiple script commands in same time
  • 95 |
96 |

TO DO

97 |
    98 |
  • NGRX should be use to state managament
  • 99 |
  • 100 | Usualy i use toPromise() for converting observables to 101 | promise then use in async functions with await. I think it make codes 102 | more readable and avoid problems like cold observale and unsubscribe. 103 | Now toPromise() is depraced, need to find new method 104 | instead of it. 105 |
  • 106 |
107 |

Known Issues

108 |
    109 |
  • 110 | When Super Admin change its account role to Admin still can use Super 111 | Admin privileges. To fix this issue should be disabled role input on 112 | user form if current user is same. 113 |
  • 114 |
115 |
116 |
117 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | - [User Authorization Project](#user-authorization-project) 2 | - [Running App](#running-app) 3 | - [Rules](#rules) 4 | - [Roles](#roles) 5 | - [About App](#about-app) 6 | - [TO DO](#to-do) 7 | - [Known Issues](#known-issues) 8 | - [Screen Shoots](#screen-shoots) 9 | - [Login - Register And Angular Form Validation](#login---register-and-angular-form-validation) 10 | - [Customer](#customer) 11 | - [Admin](#admin) 12 | - [Super Admin](#super-admin) 13 | 14 | # User Authorization Project 15 | 16 | Welcome to User Management App! There is 3 types (Super Admin, Admin, 17 | Customer) of roles. This app has 6 pages (Login, Register, Home, User 18 | Listing, 401 and 404). 19 | 20 | > Some available accounts: 21 | > 22 | > - superadmin@mail.com 23 | > - admin@mail.com 24 | > - customer@mail.com 25 | > 26 | > All passwords are same: `12345@Aa` 27 | > 28 | > You could take a look to db.json for more account data. 29 | 30 | ## Running App 31 | 32 | After install depencies with `npm start` command you can run json server and Angular development server easily. 33 | 34 | ## Rules 35 | 36 | - All users can view Homepage when they logged in. Homepage protected by `LoginGuard (Router-Guard)` that allow to only authorized users. If user have not token will be redirect to Login page. 37 | - Navigation provided by Navbar buttons by using `RouterLink`. User List button will be hidden if you are not Super Admin. 38 | - User List page protected by `AdminGuard (Router-Guard)` that allow to access only users has role Super Admin. If Customer try to go User List page will be redirect to 401 - Forbidden page. 39 | - Edit user, add user, delete user functions available in User List page. 40 | - Username cannot be change because of used to login. Also changing the password is not allowed. 41 | - Admin can't change user roles and can't list other admins. When admin create a user, user's role will be Customer by the default. Also Admin can delete only customers. 42 | - Super Admin can update role and can list all users. 43 | - Super Admin can't delete itself, but can delete other Super Admin's accounts. 44 | - Everyone can login or register to application. But if logged user try to reach Login or Register page will be redirect to Home page. Login and Register pages protected by `NotLoginGuard (Router-Guard)`. 45 | - This app uses `JSON Server` for creating fake database and `JSON Server Auth` to manage login - register operations. By the way auth token has 1 hour life time. 46 | 47 | ## Roles 48 | 49 | - Super Admin _(role value: 3)_ 50 | - Admin _(role value: 2)_ 51 | - Customer _(role value: 1)_ 52 | 53 | ## About App 54 | 55 | - Created with Angular v13 56 | - Uses Angular Material Framework for app compoments 57 | - CSS template engine is SCSS 58 | - Bootstrap Reboot, Bootstrap Grid, Bootstrap Tables and Bootsrap Aler imported with Sass 59 | - Ngx Cookie Service used to handle cookies 60 | - User ids gererated with uuid 61 | - concurrently used to run multiple script commands in same time 62 | 63 | ## TO DO 64 | 65 | - NGRX should be use to state managament 66 | - Usualy i use `toPromise()` for converting observables to promise then use in async functions with await. I think it make codes more readable and avoid problems like cold observale and unsubscribe. Now `toPromise()` is depraced, need to find new method instead of it. 67 | 68 | ## Known Issues 69 | 70 | - When Super Admin change its account role to Admin still can use Super Admin privileges. To fix this issue should be disabled role input on user form if current user is same. 71 | 72 | ## Screen Shoots 73 | 74 | ### Login - Register And Angular Form Validation 75 | 76 | ![Login Page](screen-shoots/auth/login_screen.png) 77 | 78 | ![Register Page](screen-shoots/auth/register_screen.png) 79 | 80 | ![Angular Form Validation Error](screen-shoots/auth/register_form_validation_error.png) 81 | 82 | ![Angular Form Validation](screen-shoots/auth/register_form_valid.png) 83 | 84 | ### Customer 85 | 86 | ![Home Page](screen-shoots/customer/customer_home_screen.png) 87 | 88 | ![User Menu](screen-shoots/customer/user_menu.png) 89 | 90 | ### Admin 91 | 92 | ![Admin User List](screen-shoots/admin/admin_user_list.png) 93 | 94 | ![Admin Create New User](screen-shoots/admin/admin_create_new_user.png) 95 | 96 | ![Admin Edit User](screen-shoots/admin/admin_edit_user.png) 97 | 98 | ![Admin User List](screen-shoots/admin/admin_delete_user.png) 99 | 100 | ### Super Admin 101 | 102 | ![Super Admin User List](screen-shoots/super-admin/super_admin_user_list.png) 103 | 104 | ![Super Admin Create New User](screen-shoots/super-admin/super_admin_create_user.png) 105 | 106 | ![Super Admin Edit User](screen-shoots/super-admin/super_admin_edit_user.png) 107 | 108 | ![Super Admin User List](screen-shoots/super-admin/super_admin_delete_user.png) 109 | -------------------------------------------------------------------------------- /db.json: -------------------------------------------------------------------------------- 1 | { 2 | "users": [ 3 | { 4 | "email": "demo1@ho.co", 5 | "password": "$2a$10$u60zXCkdabNd/vopiqwxxOiLI.W3OVX.VZSDaTdrIo/Q.QKkiLrHS", 6 | "id": 1 7 | }, 8 | { 9 | "email": "demo2@ho.co", 10 | "password": "$2a$10$WI0bJdJQKIYI6kA2jDjNE.NRK5FXyqTVEHwg2HC0E63v5X/PMrk7m", 11 | "id": 2 12 | }, 13 | { 14 | "email": "zt@ho.co", 15 | "password": "$2a$10$KlFT7PEynd8wsRb9isnjcOb9cGGKsYyJL/ncLfPGmnLtP0GLgGG46", 16 | "id": 3 17 | }, 18 | { 19 | "email": "john@mail.com", 20 | "password": "$2a$10$dNDigDdaBrs0SpGQ8SV8fuO66FW01wdNmVvOANl84Cc73DnyGYGL.", 21 | "id": 4 22 | }, 23 | { 24 | "email": "kg@denx.com", 25 | "password": "$2a$10$eGvYAT8f/jde7G8WbY1mnutBOLwMXEf.ku23zm8KSeyX..jvZxjui", 26 | "id": 5 27 | }, 28 | { 29 | "email": "sjda@ho.co", 30 | "password": "$2a$10$Qt1Lpi1.D3PaglysU20EBOnR/9UmwJvQWk5IXjCODtnvDGL9ony.i", 31 | "id": 6 32 | }, 33 | { 34 | "email": "ksa@ho.co", 35 | "password": "$2a$10$x20Nt6eJIeB6DgSncxj0P.u9B8nacb57RZ/AbUd4dXkktGc0ZOvsm", 36 | "id": "4f69000d-65a3-4ef3-9d28-ff8485350048" 37 | }, 38 | { 39 | "email": "keas@ho.co", 40 | "password": "$2a$10$D2/Hl9ChzlgYviIf8vCom.e19S3HWoLJfgjb6Dipt3Ly2KpSSUjyK", 41 | "id": "979eee30-ec88-471b-a2f9-35100f15b879" 42 | }, 43 | { 44 | "email": "hsaw@ho.co", 45 | "password": "$2a$10$AjQihvATQv6Vhk56jCPxL.NrhJkvm7Hf1U9pO.rZbErB2qtcnMnUu", 46 | "id": "ad5ddcf2-b493-4022-86b6-f5e954a91908" 47 | }, 48 | { 49 | "email": "kas@ho.co", 50 | "password": "$2a$10$dvT4QNXERLed00BrG7FtGekVC8ATvC/BRkOfsDH/t4cix/wch8j4q", 51 | "id": "a145423c-09c8-418e-ac6d-43ca74ee84b2" 52 | }, 53 | { 54 | "email": "laksd@ho.co", 55 | "password": "$2a$10$5JO7behk9ABG3mcUk6gh6umb4FvX6ebtr/VO6paO5rFjLOCB9OvzO", 56 | "id": "0f314007-e17e-4aaf-bd6e-58aed309076e" 57 | }, 58 | { 59 | "email": "lkasd@ho.co", 60 | "password": "$2a$10$9pLHHvAV7n.Y94d0Y9E3Pe.Sl7y/f6D0EdIKuICuKUxJ0syNPGNF2", 61 | "id": "06d82de7-6463-4d08-a68d-9a4a4239436e" 62 | }, 63 | { 64 | "email": "tam@ho.co", 65 | "password": "$2a$10$miSGZU0wAsphm4o1bdlA3uwZZ3V.FBA.lH.laERvpmaFhm7U6peka", 66 | "id": "32611d04-4783-49ea-af4e-faf41554ab8c" 67 | }, 68 | { 69 | "email": "dasdasd@ho.co", 70 | "password": "$2a$10$5pJ/9G/nBEOJqMV8OzVEo.p0AszSwqvSBX/9VYDUACWtS5K3tOMFm", 71 | "id": "47e1c686-7171-4089-8db7-acae7e0684e7" 72 | }, 73 | { 74 | "email": "kokoca@mail.com", 75 | "password": "$2a$10$szkbGjAAap4VgTcj6.KyMORRxxLn3Pe0h8JUFO299I08SE7jAcTwe", 76 | "id": "d2c510bd-2782-463f-afb6-6d09e1d679ab" 77 | }, 78 | { 79 | "email": "kralsasd@ho.co", 80 | "password": "$2a$10$1rXuaUyN2bqiTFqY681zluLS6mHFW1RXCtvfDR9VBWmbNtIdXAGEW", 81 | "id": "075f1e6b-89a2-4e3b-95ea-3751d9bbff29" 82 | }, 83 | { 84 | "email": "joasd@ho.co", 85 | "password": "$2a$10$7iAVTjmG2oRNzkZqlzUnNu3cQqfIfoOWmrEHQsjYP9rsaG4c.axiO", 86 | "id": "82342477-d53d-4091-8c70-f5aac17304e6" 87 | }, 88 | { 89 | "email": "dasdasdasd@ho.co", 90 | "password": "$2a$10$kdMJw1NzB3Yu//2iKqY85euR0E7O1IiQhIUxmBFnxsDIEmmsT76eC", 91 | "id": "1ff6ef5e-e6ca-414d-b006-dd1531b4f5d6" 92 | }, 93 | { 94 | "email": "superadmin@mail.com", 95 | "password": "$2a$10$TcZPIQWMZR4cEbcUR/mM6umq404RPomkSmAR4hxghTdHdXzGfDvC2", 96 | "id": "64fe14fc-f071-455e-88d0-102003470312" 97 | }, 98 | { 99 | "email": "admin@mail.com", 100 | "password": "$2a$10$qeUAuo4rc44UG2r28pHpTePwQJdCkHuo5Y6r5bXybngeFBfyGbSiO", 101 | "id": "b692129e-ef7f-4f5c-996e-8df976c6fccb" 102 | }, 103 | { 104 | "email": "customer@mail.com", 105 | "password": "$2a$10$zSiP7wDew8eqzQ9jZlIHQedS7vIQB/Q5/ca4p9QxSrnk1sKbXk6PK", 106 | "id": "62cb2b27-f892-4be1-b462-d8ae26283e1d" 107 | }, 108 | { 109 | "email": "dadad@ho.co", 110 | "password": "$2a$10$SxqQLd.FWhWHFN/M/mCjvuPVpbHnBNJUVlwPiKdoLk662i/6.FCBa", 111 | "id": "e3f63ea8-8bcf-4f72-8600-3aa947afccf5" 112 | }, 113 | { 114 | "email": "lasttest@ho.co", 115 | "password": "$2a$10$iYxvE9cDdAf2.pK1/NmuHeZmcmOgQDhHpvaOowYrTb55ox4HUw3WC", 116 | "id": "c2ab5d60-b034-42c8-bc9d-7861e8b19317" 117 | }, 118 | { 119 | "email": "dfsdfdsf@ho.co", 120 | "password": "$2a$10$Ja5TTI98Wz72e4vqKl6/J.U3YOiKiqiHN1gadSzTOtk4G51KuBh5S", 121 | "id": "d3fdfdba-620b-4bcb-a42a-d6dbd24db565" 122 | }, 123 | { 124 | "email": "demo@mail.com", 125 | "password": "$2a$10$COSO26sqq4SA5ABslccvQeCZa67MRm5/3fYOMAeA9idrnX8RKoGRi", 126 | "id": "7c8ffecc-19e3-474d-9690-f3b396d81f6e" 127 | }, 128 | { 129 | "email": "ldksad@ho.co", 130 | "password": "$2a$10$UxFLgHC5taEKVnJOJaw4OOLljBQRhpN46jHtUfk4YjwPTIgzwggVG", 131 | "id": "68d906d4-06b6-4b7b-ab74-d1a934965957" 132 | }, 133 | { 134 | "email": "noobadmin@ho.co", 135 | "password": "$2a$10$vfiVlTxKJBVEnz3xf/12qOrIGNPEsdzXoyXTnIWv2d4gY9XdJVShO", 136 | "id": "849c94e2-f2d6-41e3-ae19-5349f0d97d7f" 137 | } 138 | ], 139 | "profiles": [ 140 | { 141 | "email": "demo1@ho.co", 142 | "fullName": "Demo Lord", 143 | "role": 3, 144 | "id": 1, 145 | "userId": 1 146 | }, 147 | { 148 | "email": "demo2@ho.co", 149 | "fullName": "Demo Super Admin", 150 | "role": 2, 151 | "id": 2, 152 | "userId": 2 153 | }, 154 | { 155 | "email": "zt@ho.co", 156 | "fullName": "Ziya Taşkın", 157 | "role": 1, 158 | "id": 3, 159 | "userId": 3 160 | }, 161 | { 162 | "email": "john@mail.com", 163 | "fullName": "John Green", 164 | "role": 1, 165 | "id": 4 166 | }, 167 | { 168 | "email": "kg@denx.com", 169 | "fullName": "Kang Chencu", 170 | "role": 1, 171 | "id": 5, 172 | "userId": 5 173 | }, 174 | { 175 | "id": 7, 176 | "email": "ksa@ho.co", 177 | "fullName": "Kei Slazse", 178 | "role": 1, 179 | "userId": "4f69000d-65a3-4ef3-9d28-ff8485350048" 180 | }, 181 | { 182 | "userId": "979eee30-ec88-471b-a2f9-35100f15b879", 183 | "email": "keas@ho.co", 184 | "fullName": "Kaes Slamsa", 185 | "role": 1, 186 | "id": 8 187 | }, 188 | { 189 | "userId": "ad5ddcf2-b493-4022-86b6-f5e954a91908", 190 | "email": "hsaw@ho.co", 191 | "fullName": "Holasea Saersa", 192 | "role": 1, 193 | "id": 9 194 | }, 195 | { 196 | "fullName": "Keoas sa", 197 | "role": "1", 198 | "email": "kas@ho.co", 199 | "userId": "a145423c-09c8-418e-ac6d-43ca74ee84b2", 200 | "id": 10 201 | }, 202 | { 203 | "userId": "d2c510bd-2782-463f-afb6-6d09e1d679ab", 204 | "email": "kokoca@mail.com", 205 | "fullName": "Koko Caszsi", 206 | "role": 1, 207 | "id": 11 208 | }, 209 | { 210 | "userId": "075f1e6b-89a2-4e3b-95ea-3751d9bbff29", 211 | "email": "kralsasd@ho.co", 212 | "fullName": "Krals Cra Rca", 213 | "role": 1, 214 | "id": 12 215 | }, 216 | { 217 | "userId": "82342477-d53d-4091-8c70-f5aac17304e6", 218 | "email": "joasd@ho.co", 219 | "fullName": "Joa Sdal", 220 | "role": 1, 221 | "id": 13 222 | }, 223 | { 224 | "userId": "64fe14fc-f071-455e-88d0-102003470312", 225 | "email": "superadmin@mail.com", 226 | "fullName": "Super Admin", 227 | "role": 3, 228 | "id": 14 229 | }, 230 | { 231 | "userId": "b692129e-ef7f-4f5c-996e-8df976c6fccb", 232 | "email": "admin@mail.com", 233 | "fullName": "Admin", 234 | "role": 2, 235 | "id": 15 236 | }, 237 | { 238 | "userId": "62cb2b27-f892-4be1-b462-d8ae26283e1d", 239 | "email": "customer@mail.com", 240 | "fullName": "Customer", 241 | "role": 1, 242 | "id": 16 243 | }, 244 | { 245 | "userId": "7c8ffecc-19e3-474d-9690-f3b396d81f6e", 246 | "email": "demo@mail.com", 247 | "fullName": "Demo Account", 248 | "role": 1, 249 | "id": 17 250 | }, 251 | { 252 | "userId": "68d906d4-06b6-4b7b-ab74-d1a934965957", 253 | "email": "ldksad@ho.co", 254 | "fullName": "Ronca Mestip", 255 | "role": 1, 256 | "id": 18 257 | }, 258 | { 259 | "userId": "849c94e2-f2d6-41e3-ae19-5349f0d97d7f", 260 | "email": "noobadmin@ho.co", 261 | "fullName": "Noob Customer", 262 | "role": 1, 263 | "id": 19 264 | } 265 | ] 266 | } --------------------------------------------------------------------------------