├── 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 |
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 |
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 | Cancel
7 |
14 | {{ data ? "Update" : "Create" }}
15 |
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 |
8 | add
9 |
10 |
11 |
12 | 0; else noItemTemplate">
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 | Please enable JavaScript to continue using this application.
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 |
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 | Fullname
8 | Username
9 | Role
10 |
11 |
12 |
13 |
17 |
18 |
24 | edit
25 |
26 |
27 |
28 |
35 | delete
36 |
37 |
38 |
39 | back_hand
40 | Cancel
41 |
42 |
43 | delete_forever
44 | Delete
45 |
46 |
47 |
48 | {{ user?.fullName }}
49 | {{ user?.email }}
50 | {{ visualizeUserRole(user?.role) }}
51 |
52 |
53 |
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 |
4 |
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 |
5 |
6 |
12 |
13 |
75 |
76 |
77 |
78 |
--------------------------------------------------------------------------------
/src/app/pages/user-list/components/user-from/user-from.component.html:
--------------------------------------------------------------------------------
1 |
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 | 
77 |
78 | 
79 |
80 | 
81 |
82 | 
83 |
84 | ### Customer
85 |
86 | 
87 |
88 | 
89 |
90 | ### Admin
91 |
92 | 
93 |
94 | 
95 |
96 | 
97 |
98 | 
99 |
100 | ### Super Admin
101 |
102 | 
103 |
104 | 
105 |
106 | 
107 |
108 | 
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 | }
--------------------------------------------------------------------------------