Changing front

This commit is contained in:
2023-01-16 17:49:38 +01:00
parent 0b8a93b256
commit 4fe4be7730
48586 changed files with 4725790 additions and 17464 deletions
+118
View File
@@ -0,0 +1,118 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { DefaultLayoutComponent } from './containers';
import { Page404Component } from './views/pages/page404/page404.component';
import { Page500Component } from './views/pages/page500/page500.component';
import { LoginComponent } from './views/pages/login/login.component';
import { RegisterComponent } from './views/pages/register/register.component';
const routes: Routes = [
{
path: '',
redirectTo: 'dashboard',
pathMatch: 'full'
},
{
path: '',
component: DefaultLayoutComponent,
data: {
title: 'Home'
},
children: [
{
path: 'dashboard',
loadChildren: () =>
import('./views/dashboard/dashboard.module').then((m) => m.DashboardModule)
},
{
path: 'theme',
loadChildren: () =>
import('./views/theme/theme.module').then((m) => m.ThemeModule)
},
{
path: 'base',
loadChildren: () =>
import('./views/base/base.module').then((m) => m.BaseModule)
},
{
path: 'buttons',
loadChildren: () =>
import('./views/buttons/buttons.module').then((m) => m.ButtonsModule)
},
{
path: 'forms',
loadChildren: () =>
import('./views/forms/forms.module').then((m) => m.CoreUIFormsModule)
},
{
path: 'charts',
loadChildren: () =>
import('./views/charts/charts.module').then((m) => m.ChartsModule)
},
{
path: 'icons',
loadChildren: () =>
import('./views/icons/icons.module').then((m) => m.IconsModule)
},
{
path: 'notifications',
loadChildren: () =>
import('./views/notifications/notifications.module').then((m) => m.NotificationsModule)
},
{
path: 'widgets',
loadChildren: () =>
import('./views/widgets/widgets.module').then((m) => m.WidgetsModule)
},
{
path: 'pages',
loadChildren: () =>
import('./views/pages/pages.module').then((m) => m.PagesModule)
},
]
},
{
path: '404',
component: Page404Component,
data: {
title: 'Page 404'
}
},
{
path: '500',
component: Page500Component,
data: {
title: 'Page 500'
}
},
{
path: 'login',
component: LoginComponent,
data: {
title: 'Login Page'
}
},
{
path: 'register',
component: RegisterComponent,
data: {
title: 'Register Page'
}
},
{path: '**', redirectTo: 'dashboard'}
];
@NgModule({
imports: [
RouterModule.forRoot(routes, {
scrollPositionRestoration: 'top',
anchorScrolling: 'enabled',
initialNavigation: 'enabledBlocking'
// relativeLinkResolution: 'legacy'
})
],
exports: [RouterModule]
})
export class AppRoutingModule {
}
+28
View File
@@ -0,0 +1,28 @@
import { TestBed } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { AppComponent } from './app.component';
describe('AppComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [
RouterTestingModule
],
declarations: [
AppComponent
],
}).compileComponents();
});
it('should create the app', () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance;
expect(app).toBeTruthy();
});
it(`should have as title 'CoreUI Free Angular Admin Template'`, () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance;
expect(app.title).toEqual('CoreUI Free Angular Admin Template');
});
});
+23 -7
View File
@@ -1,16 +1,32 @@
import { Component, OnInit, AfterViewInit } from '@angular/core';
import { PreloaderService } from '@core';
import { Component, OnInit } from '@angular/core';
import { Router, NavigationEnd } from '@angular/router';
import { IconSetService } from '@coreui/icons-angular';
import { iconSubset } from './icons/icon-subset';
import { Title } from '@angular/platform-browser';
@Component({
selector: 'app-root',
template: '<router-outlet></router-outlet>',
})
export class AppComponent implements OnInit, AfterViewInit {
constructor(private preloader: PreloaderService) {}
export class AppComponent implements OnInit {
title = 'CoreUI Free Angular Admin Template';
ngOnInit() {}
constructor(
private router: Router,
private titleService: Title,
private iconSetService: IconSetService
) {
titleService.setTitle(this.title);
// iconSet singleton
iconSetService.icons = { ...iconSubset };
}
ngAfterViewInit() {
this.preloader.hide();
ngOnInit(): void {
this.router.events.subscribe((evt) => {
if (!(evt instanceof NavigationEnd)) {
return;
}
});
}
}
+87 -41
View File
@@ -1,58 +1,104 @@
import { NgModule } from '@angular/core';
import { HttpClientModule, HttpClient } from '@angular/common/http';
import { BrowserModule } from '@angular/platform-browser';
import { HashLocationStrategy, LocationStrategy, PathLocationStrategy } from '@angular/common';
import { BrowserModule, Title } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { ReactiveFormsModule } from '@angular/forms';
import {
PERFECT_SCROLLBAR_CONFIG,
PerfectScrollbarConfigInterface,
PerfectScrollbarModule,
} from 'ngx-perfect-scrollbar';
// Import routing module
import { AppRoutingModule } from './app-routing.module';
// Import app component
import { AppComponent } from './app.component';
import { CoreModule } from '@core/core.module';
import { ThemeModule } from '@theme/theme.module';
import { SharedModule } from '@shared/shared.module';
import { RoutesModule } from './routes/routes.module';
import { FormlyConfigModule } from './formly-config.module';
import { NgxPermissionsModule } from 'ngx-permissions';
import { ToastrModule } from 'ngx-toastr';
import { TranslateModule, TranslateLoader } from '@ngx-translate/core';
import { TranslateHttpLoader } from '@ngx-translate/http-loader';
// Import containers
import {
DefaultFooterComponent,
DefaultHeaderComponent,
DefaultLayoutComponent,
} from './containers';
import { environment } from '@env/environment';
import { BASE_URL, httpInterceptorProviders, appInitializerProviders } from '@core';
import {
AvatarModule,
BadgeModule,
BreadcrumbModule,
ButtonGroupModule,
ButtonModule,
CardModule,
DropdownModule,
FooterModule,
FormModule,
GridModule,
HeaderModule,
ListGroupModule,
NavModule,
ProgressModule,
SharedModule,
SidebarModule,
TabsModule,
UtilitiesModule,
} from '@coreui/angular';
// Required for AOT compilation
export function TranslateHttpLoaderFactory(http: HttpClient) {
return new TranslateHttpLoader(http, './assets/i18n/', '.json');
}
import { IconModule, IconSetService } from '@coreui/icons-angular';
import { LoginService } from '@core/authentication/login.service';
import { ChtloginService } from '@core/../custom/chtlogin.service';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
const DEFAULT_PERFECT_SCROLLBAR_CONFIG: PerfectScrollbarConfigInterface = {
suppressScrollX: true,
};
const APP_CONTAINERS = [
DefaultFooterComponent,
DefaultHeaderComponent,
DefaultLayoutComponent,
];
@NgModule({
declarations: [AppComponent],
declarations: [AppComponent, ...APP_CONTAINERS],
imports: [
BrowserModule,
HttpClientModule,
CoreModule,
ThemeModule,
RoutesModule,
SharedModule,
FormlyConfigModule.forRoot(),
NgxPermissionsModule.forRoot(),
ToastrModule.forRoot(),
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useFactory: TranslateHttpLoaderFactory,
deps: [HttpClient],
},
}),
BrowserAnimationsModule,
AppRoutingModule,
AvatarModule,
BreadcrumbModule,
FooterModule,
DropdownModule,
GridModule,
HeaderModule,
SidebarModule,
IconModule,
PerfectScrollbarModule,
NavModule,
ButtonModule,
FormModule,
UtilitiesModule,
ButtonGroupModule,
ReactiveFormsModule,
SidebarModule,
SharedModule,
TabsModule,
ListGroupModule,
ProgressModule,
BadgeModule,
ListGroupModule,
CardModule,
],
providers: [
{ provide: BASE_URL, useValue: environment.baseUrl },
{ provide: LoginService, useClass: ChtloginService }, // <= Remove it in the real APP
httpInterceptorProviders,
appInitializerProviders,
{
provide: LocationStrategy,
useClass: HashLocationStrategy,
},
{
provide: PERFECT_SCROLLBAR_CONFIG,
useValue: DEFAULT_PERFECT_SCROLLBAR_CONFIG,
},
IconSetService,
Title
],
bootstrap: [AppComponent],
})
export class AppModule {}
export class AppModule {
}
@@ -0,0 +1,244 @@
import { INavData } from '@coreui/angular';
export const navItems: INavData[] = [
{
name: 'Dashboard',
url: '/dashboard',
iconComponent: { name: 'cil-speedometer' },
badge: {
color: 'info',
text: 'NEW'
}
},
{
title: true,
name: 'Theme'
},
{
name: 'Colors',
url: '/theme/colors',
iconComponent: { name: 'cil-drop' }
},
{
name: 'Typography',
url: '/theme/typography',
linkProps: { fragment: 'someAnchor' },
iconComponent: { name: 'cil-pencil' }
},
{
name: 'Components',
title: true
},
{
name: 'Base',
url: '/base',
iconComponent: { name: 'cil-puzzle' },
children: [
{
name: 'Accordion',
url: '/base/accordion'
},
{
name: 'Breadcrumbs',
url: '/base/breadcrumbs'
},
{
name: 'Cards',
url: '/base/cards'
},
{
name: 'Carousel',
url: '/base/carousel'
},
{
name: 'Collapse',
url: '/base/collapse'
},
{
name: 'List Group',
url: '/base/list-group'
},
{
name: 'Navs & Tabs',
url: '/base/navs'
},
{
name: 'Pagination',
url: '/base/pagination'
},
{
name: 'Placeholder',
url: '/base/placeholder'
},
{
name: 'Popovers',
url: '/base/popovers'
},
{
name: 'Progress',
url: '/base/progress'
},
{
name: 'Spinners',
url: '/base/spinners'
},
{
name: 'Tables',
url: '/base/tables'
},
{
name: 'Tabs',
url: '/base/tabs'
},
{
name: 'Tooltips',
url: '/base/tooltips'
}
]
},
{
name: 'Buttons',
url: '/buttons',
iconComponent: { name: 'cil-cursor' },
children: [
{
name: 'Buttons',
url: '/buttons/buttons'
},
{
name: 'Button groups',
url: '/buttons/button-groups'
},
{
name: 'Dropdowns',
url: '/buttons/dropdowns'
},
]
},
{
name: 'Forms',
url: '/forms',
iconComponent: { name: 'cil-notes' },
children: [
{
name: 'Form Control',
url: '/forms/form-control'
},
{
name: 'Select',
url: '/forms/select'
},
{
name: 'Checks & Radios',
url: '/forms/checks-radios'
},
{
name: 'Range',
url: '/forms/range'
},
{
name: 'Input Group',
url: '/forms/input-group'
},
{
name: 'Floating Labels',
url: '/forms/floating-labels'
},
{
name: 'Layout',
url: '/forms/layout'
},
{
name: 'Validation',
url: '/forms/validation'
}
]
},
{
name: 'Charts',
url: '/charts',
iconComponent: { name: 'cil-chart-pie' }
},
{
name: 'Icons',
iconComponent: { name: 'cil-star' },
url: '/icons',
children: [
{
name: 'CoreUI Free',
url: '/icons/coreui-icons',
badge: {
color: 'success',
text: 'FREE'
}
},
{
name: 'CoreUI Flags',
url: '/icons/flags'
},
{
name: 'CoreUI Brands',
url: '/icons/brands'
}
]
},
{
name: 'Notifications',
url: '/notifications',
iconComponent: { name: 'cil-bell' },
children: [
{
name: 'Alerts',
url: '/notifications/alerts'
},
{
name: 'Badges',
url: '/notifications/badges'
},
{
name: 'Modal',
url: '/notifications/modal'
},
{
name: 'Toast',
url: '/notifications/toasts'
}
]
},
{
name: 'Widgets',
url: '/widgets',
iconComponent: { name: 'cil-calculator' },
badge: {
color: 'info',
text: 'NEW'
}
},
{
title: true,
name: 'Extras'
},
{
name: 'Pages',
url: '/login',
iconComponent: { name: 'cil-star' },
children: [
{
name: 'Login',
url: '/login'
},
{
name: 'Register',
url: '/register'
},
{
name: 'Error 404',
url: '/404'
},
{
name: 'Error 500',
url: '/500'
}
]
},
];
@@ -0,0 +1,12 @@
<!--<c-footer>-->
<div>
<a href="https://coreui.io/pro/angular/" target="_blank">CoreUI</a>
<span> &copy; 2022 creativeLabs</span>
</div>
<div class="ms-auto">
Powered by
<a href="https://coreui.io/pro/angular" target="_blank">
<span> CoreUI v4 for Angular</span>
</a>
</div>
<!--</c-footer>-->
@@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { DefaultFooterComponent } from './default-footer.component';
describe('DefaultFooterComponent', () => {
let component: DefaultFooterComponent;
let fixture: ComponentFixture<DefaultFooterComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ DefaultFooterComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(DefaultFooterComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,13 @@
import { Component } from '@angular/core';
import { FooterComponent } from '@coreui/angular';
@Component({
selector: 'app-default-footer',
templateUrl: './default-footer.component.html',
styleUrls: ['./default-footer.component.scss'],
})
export class DefaultFooterComponent extends FooterComponent {
constructor() {
super();
}
}
@@ -0,0 +1,165 @@
<!--<c-header class="mb-4 d-print-none" position="sticky">-->
<ng-container>
<c-container [fluid]="true">
<button
toggle="visible"
cHeaderToggler
[cSidebarToggle]="sidebarId"
class="ps-1"
>
<svg cIcon
name="cilMenu"
size="lg"
></svg>
</button>
<c-header-nav class="d-none d-lg-flex me-auto">
<c-nav-item>
<a cNavLink routerLink="/dashboard" routerLinkActive="active">
Dashboard
</a>
</c-nav-item>
<c-nav-item>
<a cNavLink routerLink="/users" routerLinkActive="active">Users</a>
</c-nav-item>
<c-nav-item>
<a cNavLink routerLink="/settings" routerLinkActive="active">
Settings
</a>
</c-nav-item>
</c-header-nav>
<c-header-nav class="d-none d-lg-flex">
<c-nav-item>
<a routerLink="./" cNavLink>
<svg cIcon name="cilBell" size="lg"></svg>
</a>
</c-nav-item>
<c-nav-item>
<a routerLink="./" cNavLink>
<svg cIcon name="cilList" size="lg"></svg>
</a>
</c-nav-item>
<c-nav-item>
<a routerLink="./" cNavLink>
<svg cIcon name="cilEnvelopeOpen" size="lg"></svg>
</a>
</c-nav-item>
</c-header-nav>
<c-header-nav class="ms-3">
<ng-container *ngTemplateOutlet="userDropdown"></ng-container>
</c-header-nav>
</c-container>
<c-header-divider></c-header-divider>
<c-container [fluid]="true">
<c-breadcrumb-router class="ms-2"></c-breadcrumb-router>
</c-container>
</ng-container>
<!--</c-header>-->
<ng-template #userDropdown>
<c-dropdown alignment="end" variant="nav-item">
<button
cButton
color=""
[caret]="false"
cDropdownToggle
class="py-0"
>
<c-avatar
cTextColor="primary"
shape="rounded-1"
size="md"
src="./assets/img/avatars/8.jpg"
status="success"
>
</c-avatar>
</button>
<ul cDropdownMenu class="pt-0 pr-5 w-auto">
<li>
<h6 cDropdownHeader class="bg-light fw-semibold py-2">Account</h6>
</li>
<li>
<a routerLink="./" cDropdownItem>
<svg cIcon class="me-2" name="cilBell"></svg>
Updates
<c-badge class="ms-2 float-end" color="info">
42
</c-badge>
</a>
</li>
<li>
<a routerLink="./" cDropdownItem>
<svg cIcon class="me-2" name="cilEnvelopeOpen"></svg>
Messages
<c-badge class="ms-2 float-end" color="success">
42
</c-badge>
</a>
</li>
<li>
<a routerLink="./" cDropdownItem>
<svg cIcon class="me-2" name="cilTask"></svg>
Tasks
<c-badge class="ms-2 float-end" color="danger">
42
</c-badge>
</a>
</li>
<li>
<a routerLink="./" cDropdownItem>
<svg cIcon class="me-2" name='cilCommentSquare'></svg>
Comments
<c-badge class="ms-auto" color="warning">
42
</c-badge>
</a>
</li>
<li>
<h6 cDropdownHeader class="bg-light fw-semibold py-2">Settings</h6>
</li>
<li></li>
<li>
<a routerLink="./" cDropdownItem>
<svg cIcon class="me-2" name="cilUser"></svg>
Profile
</a>
</li>
<li>
<a routerLink="./" cDropdownItem>
<svg cIcon class="me-2" name="cilSettings"></svg>
Settings
</a>
</li>
<li>
<a routerLink="./" cDropdownItem>
<svg cIcon class="me-2" name="cilCreditCard"></svg>
Payments
<c-badge class="ms-2 float-end" color="secondary">
42
</c-badge>
</a>
</li>
<li>
<a routerLink="./" cDropdownItem>
<svg cIcon class="me-2" name="cilFile"></svg>
Projects
<c-badge class="ms-2 float-end" color="primary">
42
</c-badge>
</a>
</li>
<li>
<hr cDropdownDivider/>
</li>
<li>
<a routerLink="./" cDropdownItem>
<svg cIcon class="me-2" name="cilLockLocked"></svg>
Lock Account
</a>
</li>
<li></li>
</ul>
</c-dropdown>
</ng-template>
@@ -0,0 +1,43 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import {
AvatarModule,
BadgeModule,
BreadcrumbModule,
DropdownModule,
GridModule,
HeaderModule,
NavModule, SidebarModule
} from '@coreui/angular';
import { IconSetService } from '@coreui/icons-angular';
import { iconSubset } from '../../../icons/icon-subset';
import { DefaultHeaderComponent } from './default-header.component';
import { RouterTestingModule } from '@angular/router/testing';
describe('DefaultHeaderComponent', () => {
let component: DefaultHeaderComponent;
let fixture: ComponentFixture<DefaultHeaderComponent>;
let iconSetService: IconSetService;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [DefaultHeaderComponent],
imports: [GridModule, HeaderModule, NavModule, BadgeModule, AvatarModule, DropdownModule, BreadcrumbModule, RouterTestingModule, SidebarModule],
providers: [IconSetService]
})
.compileComponents();
});
beforeEach(() => {
iconSetService = TestBed.inject(IconSetService);
iconSetService.icons = { ...iconSubset };
fixture = TestBed.createComponent(DefaultHeaderComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,21 @@
import { Component, Input } from '@angular/core';
import { FormControl, FormGroup } from '@angular/forms';
import { ClassToggleService, HeaderComponent } from '@coreui/angular';
@Component({
selector: 'app-default-header',
templateUrl: './default-header.component.html',
})
export class DefaultHeaderComponent extends HeaderComponent {
@Input() sidebarId: string = "sidebar";
public newMessages = new Array(4)
public newTasks = new Array(5)
public newNotifications = new Array(5)
constructor(private classToggler: ClassToggleService) {
super();
}
}
@@ -0,0 +1,51 @@
<!--sidebar-->
<c-sidebar
#sidebar="cSidebar"
class="d-print-none sidebar sidebar-fixed"
id="sidebar"
visible
>
<c-sidebar-brand
[brandFull]="{
src: 'assets/img/brand/coreui-angular.svg',
width: 200,
height: 46,
alt: 'CoreUI Logo'
}"
[brandNarrow]="{
src: 'assets/img/brand/coreui-signet-white.svg',
width: 46,
height: 46,
alt: 'CoreUI Logo'
}"
routerLink="./"
>
</c-sidebar-brand>
<perfect-scrollbar [config]="perfectScrollbarConfig">
<!-- [perfectScrollbar]="perfectScrollbarConfig"-->
<c-sidebar-nav
[navItems]="navItems"
dropdownMode="close"
>
</c-sidebar-nav>
</perfect-scrollbar>
<c-sidebar-toggler
*ngIf="!sidebar.narrow"
toggle="unfoldable"
cSidebarToggle="sidebar"
></c-sidebar-toggler>
</c-sidebar>
<!--main-->
<div class="wrapper d-flex flex-column min-vh-100 bg-light dark:bg-transparent">
<!--app-header-->
<app-default-header class="mb-4 d-print-none header header-sticky" position="sticky" sidebarId="sidebar"></app-default-header>
<!--app-body-->
<div class="body flex-grow-1 px-3">
<c-container breakpoint="lg" class="h-auto">
<router-outlet></router-outlet>
</c-container>
</div>
<!--app footer-->
<app-default-footer></app-default-footer>
</div>
@@ -0,0 +1,18 @@
import { Component } from '@angular/core';
import { navItems } from './_nav';
@Component({
selector: 'app-dashboard',
templateUrl: './default-layout.component.html',
})
export class DefaultLayoutComponent {
public navItems = navItems;
public perfectScrollbarConfig = {
suppressScrollX: true,
};
constructor() {}
}
@@ -0,0 +1,3 @@
export * from './default-footer/default-footer.component';
export * from './default-header/default-header.component';
export * from './default-layout.component';
+1
View File
@@ -0,0 +1 @@
export * from './default-layout';
@@ -1,17 +0,0 @@
# Authentication
1. Modify the token key at `token.service` to another name such as `TOKEN` or `your-app-token`. By default set to `ng-matero-token`.
2. Replace the APIs at `login.service` with your owns.
- `/auth/login` Login
- `/auth/refresh` Refresh
- `/auth/logout` Logout
- `/me` Get user information
- `/me/menu` Get user menu
3. If you have modified the login url (defaults to `auth/login`), you should correct it in the following files.
- `auth.guard.ts`
- `error-interceptor.ts`
- `token-interceptor.ts`
@@ -1,56 +0,0 @@
import { TestBed } from '@angular/core/testing';
import { Router } from '@angular/router';
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { Component } from '@angular/core';
import { RouterTestingModule } from '@angular/router/testing';
import { LocalStorageService, MemoryStorageService } from '@shared/services/storage.service';
import { TokenService, AuthGuard, AuthService } from '@core/authentication';
@Component({ template: '' })
class DummyComponent {}
describe('AuthGuard', () => {
const route: any = {};
const state: any = {};
let router: Router;
let authGuard: AuthGuard;
let authService: AuthService;
let tokenService: TokenService;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [
HttpClientTestingModule,
RouterTestingModule.withRoutes([
{ path: 'dashboard', component: DummyComponent, canActivate: [AuthGuard] },
{ path: 'auth/login', component: DummyComponent },
]),
],
declarations: [DummyComponent],
providers: [{ provide: LocalStorageService, useClass: MemoryStorageService }],
});
TestBed.createComponent(DummyComponent);
router = TestBed.inject(Router);
authGuard = TestBed.inject(AuthGuard);
authService = TestBed.inject(AuthService);
tokenService = TestBed.inject(TokenService);
});
it('should be created', () => {
expect(authGuard).toBeTruthy();
});
it('should be authenticated', () => {
tokenService.set({ access_token: 'token', token_type: 'bearer' });
expect(authGuard.canActivate(route, state)).toBeTrue();
});
it('should redirect to /auth/login when authenticate failed', () => {
spyOn(authService, 'check').and.returnValue(false);
expect(authGuard.canActivate(route, state)).toEqual(router.parseUrl('/auth/login'));
});
});
@@ -1,32 +0,0 @@
import { Injectable } from '@angular/core';
import {
ActivatedRouteSnapshot,
CanActivate,
CanActivateChild,
Router,
RouterStateSnapshot,
UrlTree,
} from '@angular/router';
import { AuthService } from './auth.service';
@Injectable({
providedIn: 'root',
})
export class AuthGuard implements CanActivate, CanActivateChild {
constructor(private auth: AuthService, private router: Router) {}
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
return this.authenticate();
}
canActivateChild(
childRoute: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): boolean | UrlTree {
return this.authenticate();
}
private authenticate(): boolean | UrlTree {
return this.auth.check() ? true : this.router.parseUrl('/auth/login');
}
}
@@ -1,133 +0,0 @@
import { fakeAsync, TestBed, tick } from '@angular/core/testing';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { Observable } from 'rxjs';
import { skip } from 'rxjs/operators';
import { HttpRequest } from '@angular/common/http';
import { LocalStorageService, MemoryStorageService } from '@shared/services/storage.service';
import { AuthService, LoginService, TokenService, User } from '@core/authentication';
describe('AuthService', () => {
let authService: AuthService;
let loginService: LoginService;
let tokenService: TokenService;
let httpMock: HttpTestingController;
let user$: Observable<User>;
const email = 'foo@bar.com';
const token = { access_token: 'token', token_type: 'bearer' };
const user = { id: 1, email };
beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [{ provide: LocalStorageService, useClass: MemoryStorageService }],
});
loginService = TestBed.inject(LoginService);
authService = TestBed.inject(AuthService);
tokenService = TestBed.inject(TokenService);
httpMock = TestBed.inject(HttpTestingController);
user$ = authService.user();
authService.change().subscribe(user => {
expect(user).toBeInstanceOf(Object);
});
});
afterEach(() => httpMock.verify());
it('should be created', () => {
expect(authService).toBeTruthy();
});
it('should log in failed', () => {
authService.login(email, 'password', false).subscribe(isLogin => expect(isLogin).toBeFalse());
httpMock.expectOne('/auth/login').flush({});
expect(authService.check()).toBeFalse();
});
it('should log in successful and get user info', () => {
user$.pipe(skip(1)).subscribe(currentUser => expect(currentUser.id).toEqual(user.id));
authService.login(email, 'password', false).subscribe(isLogin => expect(isLogin).toBeTrue());
httpMock.expectOne('/auth/login').flush(token);
expect(authService.check()).toBeTrue();
httpMock.expectOne('/me').flush(user);
});
it('should log out failed when user is not login', () => {
spyOn(loginService, 'logout').and.callThrough();
expect(authService.check()).toBeFalse();
authService.logout().subscribe();
httpMock.expectOne('/auth/logout');
expect(authService.check()).toBeFalse();
expect(loginService.logout).toHaveBeenCalled();
});
it('should log out successful when user is login', () => {
tokenService.set(token);
expect(authService.check()).toBeTrue();
httpMock.expectOne('/me').flush(user);
user$.pipe(skip(1)).subscribe(currentUser => expect(currentUser.id).toBeUndefined());
authService.logout().subscribe();
httpMock.expectOne('/auth/logout').flush({});
expect(authService.check()).toBeFalse();
});
it('should refresh token when access_token is valid', fakeAsync(() => {
tokenService.set(Object.assign({ expires_in: 5 }, token));
expect(authService.check()).toBeTrue();
httpMock.expectOne('/me').flush(user);
const match = (req: HttpRequest<any>) => req.url === '/auth/refresh' && !req.body.refresh_token;
tick(4000);
expect(authService.check()).toBeTrue();
httpMock.match(match)[0].flush(token);
expect(authService.check()).toBeTrue();
httpMock.expectNone('/me');
tokenService.ngOnDestroy();
}));
it('should refresh token when access_token is invalid and refresh_token is valid', fakeAsync(() => {
tokenService.set(Object.assign({ expires_in: 5, refresh_token: 'foo' }, token));
const match = (req: HttpRequest<any>) =>
req.url === '/auth/refresh' && req.body.refresh_token === 'foo';
expect(authService.check()).toBeTrue();
httpMock.expectOne('/me').flush(user);
tick(10000);
expect(authService.check()).toBeFalse();
httpMock.match(match)[0].flush(token);
expect(authService.check()).toBeTrue();
httpMock.expectNone('/me');
tokenService.ngOnDestroy();
}));
it('it should clear token when access_token is invalid and refresh token response is 401', fakeAsync(() => {
spyOn(tokenService, 'set').and.callThrough();
tokenService.set(Object.assign({ expires_in: 5, refresh_token: 'foo' }, token));
const match = (req: HttpRequest<any>) =>
req.url === '/auth/refresh' && req.body.refresh_token === 'foo';
tick(10000);
expect(authService.check()).toBeFalse();
httpMock.expectOne('/me').flush({});
httpMock.match(match)[0].flush({}, { status: 401, statusText: 'Unauthorized' });
expect(authService.check()).toBeFalse();
expect(tokenService.set).toHaveBeenCalledWith(undefined);
tokenService.ngOnDestroy();
}));
it('it only call http request once when on change subscribe twice', () => {
authService.change().subscribe();
tokenService.set(token);
httpMock.expectOne('/me').flush({});
});
});
@@ -1,79 +0,0 @@
import { Injectable } from '@angular/core';
import { BehaviorSubject, iif, merge, of } from 'rxjs';
import { catchError, map, share, switchMap, tap } from 'rxjs/operators';
import { TokenService } from './token.service';
import { LoginService } from './login.service';
import { filterObject, isEmptyObject } from './helpers';
import { User } from './interface';
@Injectable({
providedIn: 'root',
})
export class AuthService {
private user$ = new BehaviorSubject<User>({});
private change$ = merge(
this.tokenService.change(),
this.tokenService.refresh().pipe(switchMap(() => this.refresh()))
).pipe(
switchMap(() => this.assignUser()),
share()
);
constructor(private loginService: LoginService, private tokenService: TokenService) {}
init() {
return new Promise<void>(resolve => this.change$.subscribe(() => resolve()));
}
change() {
return this.change$;
}
check() {
return this.tokenService.valid();
}
login(username: string, password: string, rememberMe = false) {
return this.loginService.login(username, password, rememberMe).pipe(
tap(token => this.tokenService.set(token)),
map(() => this.check())
);
}
refresh() {
return this.loginService
.refresh(filterObject({ refresh_token: this.tokenService.getRefreshToken() }))
.pipe(
catchError(() => of(undefined)),
tap(token => this.tokenService.set(token)),
map(() => this.check())
);
}
logout() {
return this.loginService.logout().pipe(
tap(() => this.tokenService.clear()),
map(() => !this.check())
);
}
user() {
return this.user$.pipe(share());
}
menu() {
return iif(() => this.check(), this.loginService.menu(), of([]));
}
private assignUser() {
if (!this.check()) {
return of({}).pipe(tap(user => this.user$.next(user)));
}
if (!isEmptyObject(this.user$.getValue())) {
return of(this.user$.getValue());
}
return this.loginService.me().pipe(tap(user => this.user$.next(user)));
}
}
@@ -1,57 +0,0 @@
import { fromByteArray, toByteArray } from 'base64-js';
export class Base64 {
static encode(plainText: string): string {
return fromByteArray(pack(plainText)).replace(/[+/=]/g, m => {
return { '+': '-', '/': '_', '=': '' }[m] as string;
});
}
static decode(b64: string): string {
b64 = b64.replace(/[-_]/g, m => {
return { '-': '+', '_': '/' }[m] as string;
});
while (b64.length % 4) {
b64 += '=';
}
return unpack(toByteArray(b64));
}
}
export function pack(str: string) {
const bytes: any = [];
for (let i = 0; i < str.length; i++) {
bytes.push(...[str.charCodeAt(i)]);
}
return bytes;
}
export function unpack(byteArray: any) {
return String.fromCharCode(...byteArray);
}
export const base64 = { encode: Base64.encode, decode: Base64.decode };
export function capitalize(text: string): string {
return text.substring(0, 1).toUpperCase() + text.substring(1, text.length).toLowerCase();
}
export function currentTimestamp(): number {
return Math.ceil(new Date().getTime() / 1000);
}
export function timeLeft(expiredAt: number): number {
return Math.max(0, expiredAt - currentTimestamp());
}
export function filterObject<T extends Record<string, unknown>>(obj: T) {
return Object.fromEntries(
Object.entries(obj).filter(([, value]) => value !== undefined && value !== null)
);
}
export function isEmptyObject(obj: Record<string, any>) {
return Object.keys(obj).length === 0;
}
@@ -1,9 +0,0 @@
export * from './interface';
export * from './auth.guard';
export * from './auth.service';
export * from './token-factory.service';
export * from './token.service';
export * from './token';
export * from './login.service';
export * from './user';
export * from './helpers';
@@ -1,20 +0,0 @@
export interface User {
[prop: string]: any;
id?: number | string | null;
name?: string;
email?: string;
avatar?: string;
roles?: any[];
permissions?: any[];
}
export interface Token {
[prop: string]: any;
access_token: string;
token_type?: string;
expires_in?: number;
exp?: number;
refresh_token?: string;
}
@@ -1,32 +0,0 @@
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Token, User } from './interface';
import { Menu } from '@core';
import { map } from 'rxjs/operators';
@Injectable({
providedIn: 'root',
})
export class LoginService {
constructor(protected http: HttpClient) {}
login(username: string, password: string, rememberMe = false) {
return this.http.post<Token>('/auth/login', { username, password, rememberMe });
}
refresh(params: Record<string, any>) {
return this.http.post<Token>('/auth/refresh', params);
}
logout() {
return this.http.post<any>('/auth/logout', {});
}
me() {
return this.http.get<User>('/me');
}
menu() {
return this.http.get<{ menu: Menu[] }>('/me/menu').pipe(map(res => res.menu));
}
}
@@ -1,20 +0,0 @@
import { Injectable } from '@angular/core';
import { Token } from './interface';
import { SimpleToken, JwtToken, BaseToken } from './token';
@Injectable({
providedIn: 'root',
})
export class TokenFactory {
create(attributes: Token): BaseToken | undefined {
if (!attributes.access_token) {
return undefined;
}
if (JwtToken.is(attributes.access_token)) {
return new JwtToken(attributes);
}
return new SimpleToken(attributes);
}
}
@@ -1,52 +0,0 @@
import { TestBed } from '@angular/core/testing';
import { tap } from 'rxjs/operators';
import { MemoryStorageService, LocalStorageService } from '@shared/services/storage.service';
import { TokenService, currentTimestamp, TokenFactory, SimpleToken } from '@core/authentication';
describe('TokenService', () => {
let tokenService: TokenService;
let tokenFactory: TokenFactory;
beforeEach(() => {
TestBed.configureTestingModule({
providers: [{ provide: LocalStorageService, useClass: MemoryStorageService }],
});
tokenService = TestBed.inject(TokenService);
tokenFactory = TestBed.inject(TokenFactory);
});
it('should be created', () => {
expect(tokenService).toBeTruthy();
});
it('should get authorization header value', () => {
tokenService.set({ access_token: 'token', token_type: 'bearer' });
expect(tokenService.getBearerToken()).toEqual('Bearer token');
});
it('cannot get authorization header value', () => {
tokenService.set({ access_token: '', token_type: 'bearer' });
expect(tokenService.getBearerToken()).toBe('');
});
it('should not has exp when token has expires_in', () => {
tokenService.set({ access_token: 'token', token_type: 'bearer' });
tokenService
.change()
.pipe(tap(token => expect(token!.exp).toBeUndefined()))
.subscribe();
});
it('should has exp when token has expires_in', () => {
const expiresIn = 3600;
tokenService.set({ access_token: 'token', token_type: 'bearer', expires_in: expiresIn });
tokenService
.change()
.pipe(tap(token => expect(token!.exp).toEqual(currentTimestamp() + expiresIn)))
.subscribe();
});
});
@@ -1,99 +0,0 @@
import { Injectable, OnDestroy } from '@angular/core';
import { BehaviorSubject, Observable, Subject, Subscription, timer } from 'rxjs';
import { share } from 'rxjs/operators';
import { LocalStorageService } from '@shared';
import { Token } from './interface';
import { BaseToken } from './token';
import { TokenFactory } from './token-factory.service';
import { currentTimestamp, filterObject } from './helpers';
@Injectable({
providedIn: 'root',
})
export class TokenService implements OnDestroy {
private key = 'ng-matero-token';
private change$ = new BehaviorSubject<BaseToken | undefined>(undefined);
private refresh$ = new Subject<BaseToken | undefined>();
private timer$?: Subscription;
private _token?: BaseToken;
constructor(private store: LocalStorageService, private factory: TokenFactory) {}
private get token(): BaseToken | undefined {
if (!this._token) {
this._token = this.factory.create(this.store.get(this.key));
}
return this._token;
}
change(): Observable<BaseToken | undefined> {
return this.change$.pipe(share());
}
refresh(): Observable<BaseToken | undefined> {
this.buildRefresh();
return this.refresh$.pipe(share());
}
set(token?: Token): TokenService {
this.save(token);
return this;
}
clear(): void {
this.save();
}
valid(): boolean {
return this.token?.valid() ?? false;
}
getBearerToken(): string {
return this.token?.getBearerToken() ?? '';
}
getRefreshToken(): string | void {
return this.token?.refresh_token;
}
ngOnDestroy(): void {
this.clearRefresh();
}
private save(token?: Token): void {
this._token = undefined;
if (!token) {
this.store.remove(this.key);
} else {
const value = Object.assign({ access_token: '', token_type: 'Bearer' }, token, {
exp: token.expires_in ? currentTimestamp() + token.expires_in : null,
});
this.store.set(this.key, filterObject(value));
}
this.change$.next(this.token);
this.buildRefresh();
}
private buildRefresh() {
this.clearRefresh();
if (this.token?.needRefresh()) {
this.timer$ = timer(this.token.getRefreshTime() * 1000).subscribe(() => {
this.refresh$.next(this.token);
});
}
}
private clearRefresh() {
if (this.timer$ && !this.timer$.closed) {
this.timer$.unsubscribe();
}
}
}
@@ -1,41 +0,0 @@
import { base64, currentTimestamp, JwtToken } from '@core/authentication';
describe('Token', () => {
describe('JwtToken', () => {
function generateToken(params: any, typ = 'JWT') {
return [
base64.encode(JSON.stringify({ typ, alg: 'HS256' })),
base64.encode(JSON.stringify(params)),
base64.encode('ng-matero'),
].join('.');
}
const exp = currentTimestamp() + 3600;
const token = new JwtToken({
access_token: generateToken({ exp }, 'at+JWT'),
token_type: 'Bearer',
});
it('test access_token is JWT', () => {
expect(JwtToken.is(token.access_token)).toBeTrue();
});
it('test bearer token', function () {
expect(token.getBearerToken()).toBe(`Bearer ${token.access_token}`);
});
it('test payload has exp attribute', () => {
expect(token.exp).toEqual(exp);
});
it('test payload does not has exp attribute', () => {
expect(token.exp).toEqual(exp);
});
it('test does not has exp attribute', () => {
const token = new JwtToken({ access_token: generateToken({}), token_type: 'Bearer' });
expect(token.exp).toBeUndefined();
});
});
});
@@ -1,87 +0,0 @@
import { base64, capitalize, currentTimestamp, timeLeft } from './helpers';
import { Token } from './interface';
export abstract class BaseToken {
constructor(protected attributes: Token) {}
get access_token(): string {
return this.attributes.access_token;
}
get refresh_token(): string | void {
return this.attributes.refresh_token;
}
get token_type(): string {
return this.attributes.token_type ?? 'bearer';
}
get exp(): number | void {
return this.attributes.exp;
}
valid(): boolean {
return this.hasAccessToken() && !this.isExpired();
}
getBearerToken(): string {
return this.access_token
? [capitalize(this.token_type), this.access_token].join(' ').trim()
: '';
}
needRefresh(): boolean {
return this.exp !== undefined && this.exp >= 0;
}
getRefreshTime(): number {
return timeLeft((this.exp ?? 0) - 5);
}
private hasAccessToken(): boolean {
return !!this.access_token;
}
private isExpired(): boolean {
return this.exp !== undefined && this.exp - currentTimestamp() <= 0;
}
}
export class SimpleToken extends BaseToken {}
export class JwtToken extends SimpleToken {
private _payload?: { exp?: number | void };
static is(accessToken: string): boolean {
try {
const [_header] = accessToken.split('.');
const header = JSON.parse(base64.decode(_header));
return header.typ.toUpperCase().includes('JWT');
} catch (e) {
return false;
}
}
get exp(): number | void {
return this.payload?.exp;
}
private get payload(): { exp?: number | void } {
if (!this.access_token) {
return {};
}
if (this._payload) {
return this._payload;
}
const [, payload] = this.access_token.split('.');
const data = JSON.parse(base64.decode(payload));
if (!data.exp) {
data.exp = this.attributes.exp;
}
return (this._payload = data);
}
}
@@ -1,14 +0,0 @@
import { User } from './interface';
export const admin: User = {
id: 1,
name: 'Zongbin',
email: 'nzb329@163.com',
avatar: './assets/images/avatar.jpg',
};
export const guest: User = {
name: 'unknown',
email: 'unknown',
avatar: './assets/images/avatar-default.jpg',
};
@@ -1,3 +0,0 @@
# Bootstrap
The services in this folder should be singletons and used for sharing data and functionality.
@@ -1,150 +0,0 @@
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
import { share } from 'rxjs/operators';
export interface MenuTag {
color: string; // background color
value: string;
}
export interface MenuPermissions {
only?: string | string[];
except?: string | string[];
}
export interface MenuChildrenItem {
route: string;
name: string;
type: 'link' | 'sub' | 'extLink' | 'extTabLink';
children?: MenuChildrenItem[];
permissions?: MenuPermissions;
}
export interface Menu {
route: string;
name: string;
type: 'link' | 'sub' | 'extLink' | 'extTabLink';
icon: string;
label?: MenuTag;
badge?: MenuTag;
children?: MenuChildrenItem[];
permissions?: MenuPermissions;
}
@Injectable({
providedIn: 'root',
})
export class MenuService {
private menu$: BehaviorSubject<Menu[]> = new BehaviorSubject<Menu[]>([]);
/** Get all the menu data. */
getAll(): Observable<Menu[]> {
return this.menu$.asObservable();
}
/** Observe the change of menu data. */
change(): Observable<Menu[]> {
return this.menu$.pipe(share());
}
/** Initialize the menu data. */
set(menu: Menu[]): Observable<Menu[]> {
this.menu$.next(menu);
return this.menu$.asObservable();
}
/** Add one item to the menu data. */
add(menu: Menu) {
const tmpMenu = this.menu$.value;
tmpMenu.push(menu);
this.menu$.next(tmpMenu);
}
/** Reset the menu data. */
reset() {
this.menu$.next([]);
}
/** Delete empty values and rebuild route. */
buildRoute(routeArr: string[]): string {
let route = '';
routeArr.forEach(item => {
if (item && item.trim()) {
route += '/' + item.replace(/^\/+|\/+$/g, '');
}
});
return route;
}
/** Get the menu item name based on current route. */
getItemName(routeArr: string[]): string {
return this.getLevel(routeArr)[routeArr.length - 1];
}
// Whether is a leaf menu
private isLeafItem(item: MenuChildrenItem): boolean {
const cond0 = item.route === undefined;
const cond1 = item.children === undefined;
const cond2 = !cond1 && item.children?.length === 0;
return cond0 || cond1 || cond2;
}
// Deep clone object could be jsonized
private deepClone(obj: any): any {
return JSON.parse(JSON.stringify(obj));
}
// Whether two objects could be jsonized equal
private isJsonObjEqual(obj0: any, obj1: any): boolean {
return JSON.stringify(obj0) === JSON.stringify(obj1);
}
// Whether routeArr equals realRouteArr (after remove empty route element)
private isRouteEqual(routeArr: Array<string>, realRouteArr: Array<string>): boolean {
realRouteArr = this.deepClone(realRouteArr);
realRouteArr = realRouteArr.filter(r => r !== '');
return this.isJsonObjEqual(routeArr, realRouteArr);
}
/** Get the menu level. */
getLevel(routeArr: string[]): string[] {
let tmpArr: any[] = [];
this.menu$.value.forEach(item => {
// Breadth-first traverse
let unhandledLayer = [{ item, parentNamePathList: [], realRouteArr: [] }];
while (unhandledLayer.length > 0) {
let nextUnhandledLayer: any[] = [];
for (const ele of unhandledLayer) {
const eachItem = ele.item;
const currentNamePathList = this.deepClone(ele.parentNamePathList).concat(eachItem.name);
const currentRealRouteArr = this.deepClone(ele.realRouteArr).concat(eachItem.route);
// Compare the full Array for expandable
if (this.isRouteEqual(routeArr, currentRealRouteArr)) {
tmpArr = currentNamePathList;
break;
}
if (!this.isLeafItem(eachItem)) {
const wrappedChildren = eachItem.children?.map(child => ({
item: child,
parentNamePathList: currentNamePathList,
realRouteArr: currentRealRouteArr,
}));
nextUnhandledLayer = nextUnhandledLayer.concat(wrappedChildren);
}
}
unhandledLayer = nextUnhandledLayer;
}
});
return tmpArr;
}
/** Add namespace for translation. */
addNamespace(menu: Menu[] | MenuChildrenItem[], namespace: string) {
menu.forEach(menuItem => {
menuItem.name = `${namespace}.${menuItem.name}`;
if (menuItem.children && menuItem.children.length > 0) {
this.addNamespace(menuItem.children, menuItem.name);
}
});
}
}
@@ -1,28 +0,0 @@
import { Inject, Injectable } from '@angular/core';
import { DOCUMENT } from '@angular/common';
@Injectable({
providedIn: 'root',
})
export class PreloaderService {
private selector = 'globalLoader';
constructor(@Inject(DOCUMENT) private document: Document) {}
private getElement() {
return this.document.getElementById(this.selector);
}
hide() {
const el = this.getElement();
if (el) {
el.addEventListener('transitionend', () => {
el.className = 'global-loader-hidden';
});
if (!el.classList.contains('global-loader-hidden')) {
el.className += ' global-loader-fade-in';
}
}
}
}
@@ -1,78 +0,0 @@
import { TestBed } from '@angular/core/testing';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { HttpClient } from '@angular/common/http';
import { BASE_URL } from '../interceptors/base-url-interceptor';
import { SANCTUM_PREFIX, SanctumService } from '@core';
describe('SanctumService', () => {
let httpMock: HttpTestingController;
let http: HttpClient;
let sanctumService: SanctumService;
const setBaseUrlAndSanctumPrefix = (baseUrl: string | null, sanctumPrefix: string | null) => {
TestBed.overrideProvider(BASE_URL, { useValue: baseUrl });
TestBed.overrideProvider(SANCTUM_PREFIX, { useValue: sanctumPrefix });
httpMock = TestBed.inject(HttpTestingController);
http = TestBed.inject(HttpClient);
sanctumService = TestBed.inject(SanctumService);
};
beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [
{ provide: BASE_URL, useValue: null },
{ provide: SANCTUM_PREFIX, useValue: null },
SanctumService,
],
});
});
afterEach(() => httpMock.verify());
it('should get csrf cookie once', done => {
setBaseUrlAndSanctumPrefix(null, null);
sanctumService.load().then(data => {
expect(data).toEqual({ cookie: true });
done();
});
httpMock.expectOne('/sanctum/csrf-cookie').flush({ cookie: true });
});
it('should get csrf cookie with base url', done => {
setBaseUrlAndSanctumPrefix('http://foo.bar/api', '');
sanctumService.load().then((data: any) => {
expect(data).toEqual({ cookie: true });
done();
});
httpMock.expectOne('http://foo.bar/sanctum/csrf-cookie').flush({ cookie: true });
});
it('should get csrf cookie with sanctum prefix', done => {
setBaseUrlAndSanctumPrefix(null, '/foobar/');
sanctumService.load().then((data: any) => {
expect(data).toEqual({ cookie: true });
done();
});
httpMock.expectOne('/foobar/csrf-cookie').flush({ cookie: true });
});
it('should get csrf cookie with base url and sanctum prefix', done => {
setBaseUrlAndSanctumPrefix('http://foo.bar/api/', '/foobar');
sanctumService.load().then((data: any) => {
expect(data).toEqual({ cookie: true });
done();
});
httpMock.expectOne('http://foo.bar/foobar/csrf-cookie').flush({ cookie: true });
});
});
@@ -1,38 +0,0 @@
import { Inject, Injectable, InjectionToken, Optional } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { BASE_URL } from '../interceptors/base-url-interceptor';
export const SANCTUM_PREFIX = new InjectionToken<string>('SANCTUM_PREFIX');
@Injectable({
providedIn: 'root',
})
export class SanctumService {
constructor(
private http: HttpClient,
@Optional() @Inject(BASE_URL) private baseUrl?: string,
@Optional() @Inject(SANCTUM_PREFIX) private prefix?: string
) {}
load(): Promise<unknown> {
return new Promise(resolve => this.toObservable().subscribe(resolve));
}
toObservable(): Observable<any> {
return this.http.get(this.getUrl());
}
private getUrl(): string {
const prefix = this.prefix || 'sanctum';
const path = `/${prefix.replace(/^\/|\/$/g, '')}/csrf-cookie`;
if (!this.baseUrl) {
return path;
}
const url = new URL(this.baseUrl);
return url.origin + path;
}
}
@@ -1,34 +0,0 @@
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
import { AppSettings, defaults } from '../settings';
@Injectable({
providedIn: 'root',
})
export class SettingsService {
get notify(): Observable<Record<string, any>> {
return this.notify$.asObservable();
}
private notify$ = new BehaviorSubject<Record<string, any>>({});
getOptions() {
return this.options;
}
setOptions(options: AppSettings) {
this.options = Object.assign(defaults, options);
this.notify$.next(this.options);
}
private options = defaults;
getLanguage() {
return this.options.language;
}
setLanguage(lang: string) {
this.options.language = lang;
this.notify$.next({ lang });
}
}
@@ -1,87 +0,0 @@
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { TestBed } from '@angular/core/testing';
import { NgxPermissionsModule, NgxPermissionsService, NgxRolesService } from 'ngx-permissions';
import { LocalStorageService, MemoryStorageService } from '@shared/services/storage.service';
import { admin, TokenService } from '@core/authentication';
import { MenuService } from '@core/bootstrap/menu.service';
import { StartupService } from '@core/bootstrap/startup.service';
describe('StartupService', () => {
let httpMock: HttpTestingController;
let startup: StartupService;
let tokenService: TokenService;
let menuService: MenuService;
let mockPermissionsService: NgxPermissionsService;
let mockRolesService: NgxRolesService;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule, NgxPermissionsModule.forRoot()],
providers: [
{
provide: LocalStorageService,
useClass: MemoryStorageService,
},
{
provide: NgxPermissionsService,
useValue: {
loadPermissions: (permissions: string[]) => void 0,
},
},
{
provide: NgxRolesService,
useValue: {
flushRoles: () => void 0,
addRoles: (params: { ADMIN: string[] }) => void 0,
},
},
StartupService,
],
});
httpMock = TestBed.inject(HttpTestingController);
startup = TestBed.inject(StartupService);
tokenService = TestBed.inject(TokenService);
menuService = TestBed.inject(MenuService);
mockPermissionsService = TestBed.inject(NgxPermissionsService);
mockRolesService = TestBed.inject(NgxRolesService);
});
afterEach(() => httpMock.verify());
it('should load menu when token changed and token valid', async () => {
const menuData = { menu: [] };
const permissions = ['canAdd', 'canDelete', 'canEdit', 'canRead'];
spyOn(menuService, 'addNamespace');
spyOn(menuService, 'set');
spyOn(mockPermissionsService, 'loadPermissions');
spyOn(mockRolesService, 'flushRoles');
spyOn(mockRolesService, 'addRoles');
await startup.load();
tokenService.set({ access_token: 'token', token_type: 'bearer' });
httpMock.expectOne('/me').flush(admin);
httpMock.expectOne('/me/menu').flush(menuData);
expect(menuService.addNamespace).toHaveBeenCalledWith(menuData.menu, 'menu');
expect(menuService.set).toHaveBeenCalledWith(menuData.menu);
expect(mockPermissionsService.loadPermissions).toHaveBeenCalledWith(permissions);
expect(mockRolesService.flushRoles).toHaveBeenCalledWith();
expect(mockRolesService.addRoles).toHaveBeenCalledWith({ ADMIN: permissions });
});
it('should clear menu when token changed and token invalid', async () => {
spyOn(menuService, 'addNamespace');
spyOn(menuService, 'set');
await startup.load();
tokenService.set({ access_token: '', token_type: 'bearer' });
httpMock.expectNone('/me/menu');
expect(menuService.addNamespace).toHaveBeenCalledWith([], 'menu');
expect(menuService.set).toHaveBeenCalledWith([]);
});
});
@@ -1,53 +0,0 @@
import { Injectable } from '@angular/core';
import { switchMap, tap } from 'rxjs/operators';
import { NgxPermissionsService, NgxRolesService } from 'ngx-permissions';
import { AuthService, User } from '@core/authentication';
import { Menu, MenuService } from './menu.service';
@Injectable({
providedIn: 'root',
})
export class StartupService {
constructor(
private authService: AuthService,
private menuService: MenuService,
private permissonsService: NgxPermissionsService,
private rolesService: NgxRolesService
) {}
/**
* Load the application only after get the menu or other essential informations
* such as permissions and roles.
*/
load() {
return new Promise<void>((resolve, reject) => {
this.authService
.change()
.pipe(
tap(user => this.setPermissions(user)),
switchMap(() => this.authService.menu()),
tap(menu => this.setMenu(menu))
)
.subscribe(
() => resolve(),
() => resolve()
);
});
}
private setMenu(menu: Menu[]) {
this.menuService.addNamespace(menu, 'menu');
this.menuService.set(menu);
}
private setPermissions(user: User) {
// In a real app, you should get permissions and roles from the user information.
const permissions = ['canAdd', 'canDelete', 'canEdit', 'canRead'];
this.permissonsService.loadPermissions(permissions);
this.rolesService.flushRoles();
this.rolesService.addRoles({ ADMIN: permissions });
// Tips: Alternatively you can add permissions with role at the same time.
// this.rolesService.addRolesWithPermissions({ ADMIN: permissions });
}
}
@@ -1,33 +0,0 @@
import { Injectable, Injector } from '@angular/core';
import { LOCATION_INITIALIZED } from '@angular/common';
import { TranslateService } from '@ngx-translate/core';
import { SettingsService } from './settings.service';
@Injectable({
providedIn: 'root',
})
export class TranslateLangService {
constructor(
private injector: Injector,
private translate: TranslateService,
private settings: SettingsService
) {}
load() {
return new Promise<void>(resolve => {
const locationInitialized = this.injector.get(LOCATION_INITIALIZED, Promise.resolve());
locationInitialized.then(() => {
const browserLang = navigator.language;
const defaultLang = browserLang.match(/en-US|zh-CN|zh-TW/) ? browserLang : 'en-US';
this.settings.setLanguage(defaultLang);
this.translate.setDefaultLang(defaultLang);
this.translate.use(defaultLang).subscribe(
() => console.log(`Successfully initialized '${defaultLang}' language.'`),
() => console.error(`Problem with '${defaultLang}' language initialization.'`),
() => resolve()
);
});
});
}
}
-13
View File
@@ -1,13 +0,0 @@
import { NgModule, Optional, SkipSelf } from '@angular/core';
import { CommonModule } from '@angular/common';
import { throwIfAlreadyLoaded } from './module-import-guard';
@NgModule({
declarations: [],
imports: [CommonModule],
})
export class CoreModule {
constructor(@Optional() @SkipSelf() parentModule: CoreModule) {
throwIfAlreadyLoaded(parentModule, 'CoreModule');
}
}
-16
View File
@@ -1,16 +0,0 @@
export * from './settings';
export * from './initializers';
// Bootstrap
export * from './bootstrap/menu.service';
export * from './bootstrap/settings.service';
export * from './bootstrap/startup.service';
export * from './bootstrap/preloader.service';
export * from './bootstrap/translate-lang.service';
export * from './bootstrap/sanctum.service';
// Interceptors
export * from './interceptors';
// Authentication
export * from './authentication';
-37
View File
@@ -1,37 +0,0 @@
import { APP_INITIALIZER } from '@angular/core';
// import { SanctumService } from './bootstrap/sanctum.service';
// export function SanctumServiceFactory(sanctumService: SanctumService) {
// return () => sanctumService.load();
// }
import { TranslateLangService } from './bootstrap/translate-lang.service';
export function TranslateLangServiceFactory(translateLangService: TranslateLangService) {
return () => translateLangService.load();
}
import { StartupService } from './bootstrap/startup.service';
export function StartupServiceFactory(startupService: StartupService) {
return () => startupService.load();
}
export const appInitializerProviders = [
// {
// provide: APP_INITIALIZER,
// useFactory: SanctumServiceFactory,
// deps: [SanctumService],
// multi: true,
// },
{
provide: APP_INITIALIZER,
useFactory: TranslateLangServiceFactory,
deps: [TranslateLangService],
multi: true,
},
{
provide: APP_INITIALIZER,
useFactory: StartupServiceFactory,
deps: [StartupService],
multi: true,
},
];
@@ -1,3 +0,0 @@
# Interceptors
https://angular.io/guide/http#intercepting-requests-and-responses
@@ -1,47 +0,0 @@
import { TestBed } from '@angular/core/testing';
import { HTTP_INTERCEPTORS, HttpClient } from '@angular/common/http';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { BASE_URL, BaseUrlInterceptor } from './base-url-interceptor';
describe('BaseUrlInterceptor', () => {
let httpMock: HttpTestingController;
let http: HttpClient;
const baseUrl = 'https://foo.bar';
const setBaseUrl = (url: string | null) => {
TestBed.overrideProvider(BASE_URL, { useValue: url });
httpMock = TestBed.inject(HttpTestingController);
http = TestBed.inject(HttpClient);
};
beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [
{ provide: BASE_URL, useValue: null },
{ provide: HTTP_INTERCEPTORS, useClass: BaseUrlInterceptor, multi: true },
],
});
});
afterEach(() => httpMock.verify());
it('should not prepend base url when base url is empty', () => {
setBaseUrl(null);
http.get('/me').subscribe(data => expect(data).toEqual({ success: true }));
httpMock.expectOne('/me').flush({ success: true });
});
it('should prepend base url when request url does not has http scheme', () => {
setBaseUrl(baseUrl);
http.get('./me').subscribe(data => expect(data).toEqual({ success: true }));
httpMock.expectOne(baseUrl + '/me').flush({ success: true });
http.get('').subscribe(data => expect(data).toEqual({ success: true }));
httpMock.expectOne(baseUrl).flush({ success: true });
});
});
@@ -1,24 +0,0 @@
import { Inject, Injectable, InjectionToken, Optional } from '@angular/core';
import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http';
import { Observable } from 'rxjs';
export const BASE_URL = new InjectionToken<string>('BASE_URL');
@Injectable()
export class BaseUrlInterceptor implements HttpInterceptor {
private hasScheme = (url: string) => this.baseUrl && new RegExp('^http(s)?://', 'i').test(url);
constructor(@Optional() @Inject(BASE_URL) private baseUrl?: string) {}
intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
return this.hasScheme(request.url) === false
? next.handle(request.clone({ url: this.prependBaseUrl(request.url) }))
: next.handle(request);
}
private prependBaseUrl(url: string) {
return [this.baseUrl?.replace(/\/$/g, ''), url.replace(/^\.?\//, '')]
.filter(val => val)
.join('/');
}
}
@@ -1,41 +0,0 @@
import { Injectable } from '@angular/core';
import {
HttpEvent,
HttpHandler,
HttpInterceptor,
HttpRequest,
HttpResponse,
} from '@angular/common/http';
import { Observable, of, throwError } from 'rxjs';
import { mergeMap } from 'rxjs/operators';
import { ToastrService } from 'ngx-toastr';
@Injectable()
export class DefaultInterceptor implements HttpInterceptor {
constructor(private toast: ToastrService) {}
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
if (!req.url.includes('/api/')) {
return next.handle(req);
}
return next.handle(req).pipe(mergeMap((event: HttpEvent<any>) => this.handleOkReq(event)));
}
private handleOkReq(event: HttpEvent<any>): Observable<any> {
if (event instanceof HttpResponse) {
const body: any = event.body;
// failure: { code: **, msg: 'failure' }
// success: { code: 0, msg: 'success', data: {} }
if (body && 'code' in body && body.code !== 0) {
if (body.msg) {
this.toast.error(body.msg);
}
return throwError([]);
}
}
// Pass down event if everything is OK
return of(event);
}
}
@@ -1,75 +0,0 @@
import { TestBed } from '@angular/core/testing';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { HTTP_INTERCEPTORS, HttpClient } from '@angular/common/http';
import { RouterTestingModule } from '@angular/router/testing';
import { Router } from '@angular/router';
import { ToastrModule, ToastrService } from 'ngx-toastr';
import { ErrorInterceptor } from './error-interceptor';
describe('ErrorInterceptor', () => {
let httpMock: HttpTestingController;
let http: HttpClient;
let router: Router;
let toast: ToastrService;
const emptyFn = () => {};
function assertStatus(status: number, statusText: string) {
spyOn(router, 'navigateByUrl');
http.get('/me').subscribe(emptyFn, emptyFn, emptyFn);
httpMock.expectOne('/me').flush({}, { status, statusText });
expect(router.navigateByUrl).toHaveBeenCalledWith(`/${status}`, {
skipLocationChange: true,
});
}
beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule, RouterTestingModule, ToastrModule.forRoot()],
providers: [{ provide: HTTP_INTERCEPTORS, useClass: ErrorInterceptor, multi: true }],
});
httpMock = TestBed.inject(HttpTestingController);
http = TestBed.inject(HttpClient);
router = TestBed.inject(Router);
toast = TestBed.inject(ToastrService);
});
afterEach(() => httpMock.verify());
it('should handle status code 401', () => {
spyOn(router, 'navigateByUrl');
spyOn(toast, 'error');
http.get('/me').subscribe(emptyFn, emptyFn, emptyFn);
httpMock.expectOne('/me').flush({}, { status: 401, statusText: 'Unauthorized' });
expect(toast.error).toHaveBeenCalledWith('401 Unauthorized');
expect(router.navigateByUrl).toHaveBeenCalledWith('/auth/login');
});
it('should handle status code 403', () => {
assertStatus(403, 'Forbidden');
});
it('should handle status code 404', () => {
assertStatus(404, 'Not Found');
});
it('should handle status code 500', () => {
assertStatus(500, 'Internal Server Error');
});
it('should handle others status code', () => {
spyOn(toast, 'error');
http.get('/me').subscribe(emptyFn, emptyFn, emptyFn);
httpMock.expectOne('/me').flush({}, { status: 504, statusText: 'Gateway Timeout' });
expect(toast.error).toHaveBeenCalledWith('504 Gateway Timeout');
});
});
@@ -1,60 +0,0 @@
import { Injectable } from '@angular/core';
import {
HttpErrorResponse,
HttpEvent,
HttpHandler,
HttpInterceptor,
HttpRequest,
} from '@angular/common/http';
import { Router } from '@angular/router';
import { Observable, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';
import { ToastrService } from 'ngx-toastr';
export enum STATUS {
UNAUTHORIZED = 401,
FORBIDDEN = 403,
NOT_FOUND = 404,
INTERNAL_SERVER_ERROR = 500,
}
@Injectable()
export class ErrorInterceptor implements HttpInterceptor {
private errorPages = [STATUS.FORBIDDEN, STATUS.NOT_FOUND, STATUS.INTERNAL_SERVER_ERROR];
private getMessage = (error: HttpErrorResponse) => {
if (error.error?.message) {
return error.error.message;
}
if (error.error?.msg) {
return error.error.msg;
}
return `${error.status} ${error.statusText}`;
};
constructor(private router: Router, private toast: ToastrService) {}
intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
return next
.handle(request)
.pipe(catchError((error: HttpErrorResponse) => this.handleError(error)));
}
private handleError(error: HttpErrorResponse) {
if (this.errorPages.includes(error.status)) {
this.router.navigateByUrl(`/${error.status}`, {
skipLocationChange: true,
});
} else {
console.error('ERROR', error);
this.toast.error(this.getMessage(error));
if (error.status === STATUS.UNAUTHORIZED) {
this.router.navigateByUrl('/auth/login');
}
}
return throwError(error);
}
}
@@ -1,31 +0,0 @@
import { HTTP_INTERCEPTORS } from '@angular/common/http';
import { NoopInterceptor } from './noop-interceptor';
// import { SanctumInterceptor } from './sanctum-interceptor';
import { BaseUrlInterceptor } from './base-url-interceptor';
import { SettingsInterceptor } from './settings-interceptor';
import { TokenInterceptor } from './token-interceptor';
import { DefaultInterceptor } from './default-interceptor';
import { ErrorInterceptor } from './error-interceptor';
import { LoggingInterceptor } from './logging-interceptor';
export * from './noop-interceptor';
// export * from './sanctum-interceptor';
export * from './base-url-interceptor';
export * from './settings-interceptor';
export * from './token-interceptor';
export * from './default-interceptor';
export * from './error-interceptor';
export * from './logging-interceptor';
/** Http interceptor providers in outside-in order */
export const httpInterceptorProviders = [
{ provide: HTTP_INTERCEPTORS, useClass: NoopInterceptor, multi: true },
// { provide: HTTP_INTERCEPTORS, useClass: SanctumInterceptor, multi: true },
{ provide: HTTP_INTERCEPTORS, useClass: BaseUrlInterceptor, multi: true },
{ provide: HTTP_INTERCEPTORS, useClass: SettingsInterceptor, multi: true },
{ provide: HTTP_INTERCEPTORS, useClass: TokenInterceptor, multi: true },
{ provide: HTTP_INTERCEPTORS, useClass: DefaultInterceptor, multi: true },
{ provide: HTTP_INTERCEPTORS, useClass: ErrorInterceptor, multi: true },
{ provide: HTTP_INTERCEPTORS, useClass: LoggingInterceptor, multi: true },
];
@@ -1,30 +0,0 @@
import { Injectable } from '@angular/core';
import { HttpInterceptor, HttpHandler, HttpRequest, HttpResponse } from '@angular/common/http';
import { finalize, tap } from 'rxjs/operators';
import { MessageService } from '@shared';
@Injectable()
export class LoggingInterceptor implements HttpInterceptor {
constructor(private messenger: MessageService) {}
intercept(req: HttpRequest<any>, next: HttpHandler) {
const started = Date.now();
let ok: string;
// extend server response observable with logging
return next.handle(req).pipe(
tap(
// Succeeds when there is a response; ignore other events
event => (ok = event instanceof HttpResponse ? 'succeeded' : ''),
// Operation failed; error is an HttpErrorResponse
error => (ok = 'failed')
),
// Log when response observable either completes or errors
finalize(() => {
const elapsed = Date.now() - started;
const msg = `${req.method} "${req.urlWithParams}" ${ok} in ${elapsed} ms.`;
this.messenger.add(msg);
})
);
}
}
@@ -1,10 +0,0 @@
import { Injectable } from '@angular/core';
import { HttpEvent, HttpInterceptor, HttpHandler, HttpRequest } from '@angular/common/http';
import { Observable } from 'rxjs';
@Injectable()
export class NoopInterceptor implements HttpInterceptor {
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
return next.handle(req);
}
}
@@ -1,98 +0,0 @@
import { TestBed } from '@angular/core/testing';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { HTTP_INTERCEPTORS, HttpClient } from '@angular/common/http';
import { switchMap } from 'rxjs/operators';
import { SanctumInterceptor } from './sanctum-interceptor';
import { BASE_URL } from './base-url-interceptor';
import { SANCTUM_PREFIX } from '@core/bootstrap/sanctum.service';
describe('SanctumInterceptor', () => {
let httpMock: HttpTestingController;
let http: HttpClient;
const setBaseUrlAndSanctumPrefix = (baseUrl: string | null, sanctumPrefix: string | null) => {
TestBed.overrideProvider(BASE_URL, { useValue: baseUrl });
TestBed.overrideProvider(SANCTUM_PREFIX, { useValue: sanctumPrefix });
httpMock = TestBed.inject(HttpTestingController);
http = TestBed.inject(HttpClient);
};
beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [
{ provide: BASE_URL, useValue: null },
{ provide: SANCTUM_PREFIX, useValue: null },
{ provide: HTTP_INTERCEPTORS, useClass: SanctumInterceptor, multi: true },
],
});
});
afterEach(() => httpMock.verify());
it('should get csrf cookie once', () => {
setBaseUrlAndSanctumPrefix(null, null);
http
.post('/auth/login', {
username: 'foo',
password: 'bar',
})
.pipe(switchMap(() => http.get('/me')))
.subscribe(data => expect(data).toEqual({ me: true }));
httpMock.expectOne('/sanctum/csrf-cookie').flush({ cookie: true });
httpMock.expectOne('/auth/login').flush({ login: true });
httpMock.expectOne('/me').flush({ me: true });
});
it('should get csrf cookie with base url', () => {
setBaseUrlAndSanctumPrefix('https://foo.bar/api', null);
http
.post('/auth/login', {
username: 'foo',
password: 'bar',
})
.pipe(switchMap(() => http.get('/me')))
.subscribe(data => expect(data).toEqual({ me: true }));
httpMock.expectOne('https://foo.bar/sanctum/csrf-cookie').flush({ cookie: true });
httpMock.expectOne('/auth/login').flush({ login: true });
httpMock.expectOne('/me').flush({ me: true });
});
it('should get csrf cookie with sanctum prefix', () => {
setBaseUrlAndSanctumPrefix(null, 'foobar');
http
.post('/auth/login', {
username: 'foo',
password: 'bar',
})
.pipe(switchMap(() => http.get('/me')))
.subscribe(data => expect(data).toEqual({ me: true }));
httpMock.expectOne('/foobar/csrf-cookie').flush({ cookie: true });
httpMock.expectOne('/auth/login').flush({ login: true });
httpMock.expectOne('/me').flush({ me: true });
});
it('should get csrf cookie with base url and sanctum prefix', () => {
setBaseUrlAndSanctumPrefix('https://foo.bar/api', 'foobar');
http
.post('/auth/login', {
username: 'foo',
password: 'bar',
})
.pipe(switchMap(() => http.get('/me')))
.subscribe(data => expect(data).toEqual({ me: true }));
httpMock.expectOne('https://foo.bar/foobar/csrf-cookie').flush({ cookie: true });
httpMock.expectOne('/auth/login').flush({ login: true });
httpMock.expectOne('/me').flush({ me: true });
});
});
@@ -1,22 +0,0 @@
import { Injectable } from '@angular/core';
import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http';
import { Observable } from 'rxjs';
import { switchMap } from 'rxjs/operators';
import { SanctumService } from '@core';
@Injectable()
export class SanctumInterceptor implements HttpInterceptor {
private ready = false;
constructor(private sanctum: SanctumService) {}
intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
if (!this.ready) {
this.ready = true;
return this.sanctum.toObservable().pipe(switchMap(() => next.handle(request)));
}
return next.handle(request);
}
}
@@ -1,33 +0,0 @@
import { TestBed } from '@angular/core/testing';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { HTTP_INTERCEPTORS, HttpClient } from '@angular/common/http';
import { SettingsService } from '@core/bootstrap/settings.service';
import { SettingsInterceptor } from './settings-interceptor';
describe('SettingsInterceptor', () => {
let httpMock: HttpTestingController;
let http: HttpClient;
let settings: SettingsService;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [{ provide: HTTP_INTERCEPTORS, useClass: SettingsInterceptor, multi: true }],
});
httpMock = TestBed.inject(HttpTestingController);
http = TestBed.inject(HttpClient);
settings = TestBed.inject(SettingsService);
});
it('should set accept language', () => {
settings.setLanguage('zh-TW');
http.get('/me').subscribe();
const testRequest = httpMock.expectOne('/me');
testRequest.flush({ me: true });
expect(testRequest.request.headers.get('Accept-Language')).toEqual('zh-TW');
});
});
@@ -1,17 +0,0 @@
import { Injectable } from '@angular/core';
import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor } from '@angular/common/http';
import { Observable } from 'rxjs';
import { SettingsService } from '@core';
@Injectable()
export class SettingsInterceptor implements HttpInterceptor {
constructor(private settings: SettingsService) {}
intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
return next.handle(
request.clone({
headers: request.headers.append('Accept-Language', this.settings.getLanguage()),
})
);
}
}
@@ -1,116 +0,0 @@
import { TestBed } from '@angular/core/testing';
import { TokenInterceptor } from './token-interceptor';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { HTTP_INTERCEPTORS, HttpClient } from '@angular/common/http';
import { STATUS } from 'angular-in-memory-web-api';
import { RouterTestingModule } from '@angular/router/testing';
import { Router } from '@angular/router';
import { LocalStorageService, MemoryStorageService } from '@shared/services/storage.service';
import { TokenService, User } from '@core/authentication';
import { BASE_URL } from './base-url-interceptor';
describe('TokenInterceptor', () => {
let httpMock: HttpTestingController;
let http: HttpClient;
let router: Router;
let tokenService: TokenService;
const emptyFn = () => {};
const baseUrl = 'https://foo.bar';
const user: User = { id: 1, email: 'foo@bar.com' };
function init(url: string, access_token: string) {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule, RouterTestingModule],
providers: [
{ provide: LocalStorageService, useClass: MemoryStorageService },
{ provide: BASE_URL, useValue: url },
{ provide: HTTP_INTERCEPTORS, useClass: TokenInterceptor, multi: true },
],
});
httpMock = TestBed.inject(HttpTestingController);
http = TestBed.inject(HttpClient);
router = TestBed.inject(Router);
tokenService = TestBed.inject(TokenService).set({ access_token, token_type: 'bearer' });
}
function mockRequest(url: string, body?: any, headers?: any) {
http.get(url).subscribe(emptyFn, emptyFn, emptyFn);
const testRequest = httpMock.expectOne(url);
testRequest.flush(body ?? {}, headers ?? {});
return testRequest;
}
afterEach(() => httpMock.verify());
it('should append token when url does not has http scheme', () => {
init('', 'token');
const headers = mockRequest('/me', user).request.headers;
expect(headers.get('Authorization')).toEqual('Bearer token');
});
it('should append token when url does not has http and base url not empty', () => {
init(baseUrl, 'token');
const headers = mockRequest('/me', user).request.headers;
expect(headers.get('Authorization')).toEqual('Bearer token');
});
it('should append token when url include base url', () => {
init(baseUrl, 'token');
const headers = mockRequest(`${baseUrl}/me`, user).request.headers;
expect(headers.get('Authorization')).toEqual('Bearer token');
});
it('should not append token when url not include baseUrl', () => {
init(baseUrl, 'token');
const headers = mockRequest('https://api.github.com', { success: true }).request.headers;
expect(headers.has('Authorization')).toBeFalse();
});
it('should not append token when base url is empty and url is not same site', () => {
init('', 'token');
const headers = mockRequest('https://api.github.com', { success: true }).request.headers;
expect(headers.has('Authorization')).toBeFalse();
});
it('should clear token when response status is unauthorized', () => {
init('', 'token');
spyOn(tokenService, 'clear');
mockRequest('/me', {}, { status: STATUS.UNAUTHORIZED, statusText: 'Unauthorized' });
expect(tokenService.clear).toHaveBeenCalled();
});
it('should navigate /auth/login when api url is /auth/logout and token is valid', () => {
init('', 'token');
const navigateByUrl = spyOn(router, 'navigateByUrl');
navigateByUrl.and.returnValue(Promise.resolve(true));
mockRequest('/auth/logout');
expect(navigateByUrl).toHaveBeenCalledWith('/auth/login');
});
it('should navigate /auth/login when api url is /auth/logout and token is invalid', () => {
init('', '');
const navigateByUrl = spyOn(router, 'navigateByUrl');
navigateByUrl.and.returnValue(Promise.resolve(true));
mockRequest('/auth/logout');
expect(navigateByUrl).toHaveBeenCalledWith('/auth/login');
});
});
@@ -1,71 +0,0 @@
import { Inject, Injectable, Optional } from '@angular/core';
import {
HttpErrorResponse,
HttpEvent,
HttpHandler,
HttpInterceptor,
HttpRequest,
} from '@angular/common/http';
import { Router } from '@angular/router';
import { Observable, throwError } from 'rxjs';
import { catchError, tap } from 'rxjs/operators';
import { TokenService } from '@core/authentication';
import { BASE_URL } from './base-url-interceptor';
@Injectable()
export class TokenInterceptor implements HttpInterceptor {
private hasHttpScheme = (url: string) => new RegExp('^http(s)?://', 'i').test(url);
constructor(
private tokenService: TokenService,
private router: Router,
@Optional() @Inject(BASE_URL) private baseUrl?: string
) {}
intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
const handler = () => {
if (request.url.includes('/auth/logout')) {
this.router.navigateByUrl('/auth/login');
}
if (this.router.url.includes('/auth/login')) {
this.router.navigateByUrl('/dashboard');
}
};
if (this.tokenService.valid() && this.shouldAppendToken(request.url)) {
return next
.handle(
request.clone({
headers: request.headers.append('Authorization', this.tokenService.getBearerToken()),
withCredentials: true,
})
)
.pipe(
catchError((error: HttpErrorResponse) => {
if (error.status === 401) {
this.tokenService.clear();
}
return throwError(error);
}),
tap(() => handler())
);
}
return next.handle(request).pipe(tap(() => handler()));
}
private shouldAppendToken(url: string) {
return !this.hasHttpScheme(url) || this.includeBaseUrl(url);
}
private includeBaseUrl(url: string) {
if (!this.baseUrl) {
return false;
}
const baseUrl = this.baseUrl.replace(/\/$/, '');
return new RegExp(`^${baseUrl}`, 'i').test(url);
}
}
@@ -1,7 +0,0 @@
export function throwIfAlreadyLoaded(parentModule: any, moduleName: string) {
if (parentModule) {
throw new Error(
`${moduleName} has already been loaded. Import Core modules in the AppModule only.`
);
}
}
-23
View File
@@ -1,23 +0,0 @@
export interface AppSettings {
navPos: 'side' | 'top';
theme: 'light' | 'dark' | 'auto';
dir: 'ltr' | 'rtl';
showHeader: boolean;
headerPos: 'fixed' | 'static' | 'above';
showUserPanel: boolean;
sidenavOpened: boolean;
sidenavCollapsed: boolean;
language: string;
}
export const defaults: AppSettings = {
navPos: 'side',
theme: 'auto',
dir: 'ltr',
showHeader: true,
headerPos: 'fixed',
showUserPanel: true,
sidenavOpened: true,
sidenavCollapsed: false,
language: 'en-US',
};
@@ -1,41 +0,0 @@
import { Injectable } from '@angular/core';
import { HttpHeaders, HttpParams } from '@angular/common/http';
import { Token, User } from '../core/authentication/interface';
import { Menu } from '@core';
import { map } from 'rxjs/operators';
import { LoginService } from '../core/authentication/login.service'
@Injectable({
providedIn: 'root',
})
export class ChtloginService extends LoginService {
login(username: string, password: string, rememberMe = false) {
const body = new HttpParams()
.set('username', username)
.set('password', password);
return this.http.post<Token>('/api/v1/auth/login', body.toString(), {
headers: new HttpHeaders()
.set('Content-Type', 'application/x-www-form-urlencoded')
});
}
refresh(params: Record<string, any>) {
return this.http.post<Token>('/api/v1/auth/refresh', params);
}
logout() {
return this.http.post<any>('/api/v1/auth/logout', {});
}
me() {
return this.http.get<User>('/api/v1/users/me');
}
menu() {
return this.http
.get<{ menu: Menu[] }>('assets/data/menu.json?_t=' + Date.now())
.pipe(map(res => res.menu));
}
}
-34
View File
@@ -1,34 +0,0 @@
import { Injectable } from '@angular/core';
import { of } from 'rxjs';
import { admin, LoginService, Menu } from '@core';
import { map } from 'rxjs/operators';
/**
* You should delete this file in the real APP.
*/
@Injectable()
export class FakeLoginService extends LoginService {
private token = { access_token: 'MW56YjMyOUAxNjMuY29tWm9uZ2Jpbg==', token_type: 'bearer' };
login() {
return of(this.token);
}
refresh() {
return of(this.token);
}
logout() {
return of({});
}
me() {
return of(admin);
}
menu() {
return this.http
.get<{ menu: Menu[] }>('assets/data/menu.json?_t=' + Date.now())
.pipe(map(res => res.menu));
}
}
-53
View File
@@ -1,53 +0,0 @@
import { NgModule, ModuleWithProviders, Provider } from '@angular/core';
import { SharedModule } from './shared/shared.module';
import { FormlyModule } from '@ngx-formly/core';
import { FormlyFieldComboboxComponent } from './formly-templates';
import { FormlyWrapperCardComponent, FormlyWrapperDivComponent } from './formly-wrappers';
import { FormlyValidations } from './formly-validations';
/**
* Formly global configuration
*/
const formlyModuleProviders = FormlyModule.forRoot({
types: [
{
name: 'combobox',
component: FormlyFieldComboboxComponent,
wrappers: ['form-field'],
},
],
wrappers: [
{
name: 'card',
component: FormlyWrapperCardComponent,
},
{
name: 'div',
component: FormlyWrapperDivComponent,
},
],
validationMessages: [],
}).providers as Provider[];
@NgModule({
imports: [SharedModule],
declarations: [
FormlyFieldComboboxComponent,
FormlyWrapperCardComponent,
FormlyWrapperDivComponent,
],
providers: [FormlyValidations],
})
export class FormlyConfigModule {
constructor(formlyValidations: FormlyValidations) {
formlyValidations.init();
}
static forRoot(): ModuleWithProviders<FormlyConfigModule> {
return {
ngModule: FormlyConfigModule,
providers: [formlyModuleProviders],
};
}
}
-45
View File
@@ -1,45 +0,0 @@
import { ViewChild, ChangeDetectionStrategy, Component } from '@angular/core';
import { FieldType } from '@ngx-formly/material/form-field';
import { MtxSelect } from '@ng-matero/extensions/select';
import { FieldTypeConfig } from '@ngx-formly/core';
/**
* This is just an example.
*/
@Component({
selector: 'formly-field-combobox',
template: `<mtx-select
#select
[formControl]="formControl"
[items]="props.options | toObservable | async"
[bindLabel]="bindLabel"
[bindValue]="bindValue!"
[multiple]="props.multiple"
[placeholder]="props.placeholder!"
[required]="props.required!"
[closeOnSelect]="!props.multiple"
[compareWith]="props.compareWith"
>
</mtx-select>`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class FormlyFieldComboboxComponent extends FieldType<FieldTypeConfig> {
@ViewChild('select', { static: true }) select!: MtxSelect;
get bindLabel() {
return typeof this.props.labelProp === 'string' ? this.props.labelProp : '';
}
get bindValue() {
return typeof this.props.valueProp === 'string' ? this.props.valueProp : undefined;
}
// The original `onContainerClick` has been covered up in FieldType, so we should redefine it.
onContainerClick(event: MouseEvent) {
const target = event.target as HTMLElement;
if (/mat-form-field|mtx-select/g.test(target.parentElement?.classList[0] || '')) {
this.select.focus();
this.select.open();
}
}
}
-53
View File
@@ -1,53 +0,0 @@
import { Injectable } from '@angular/core';
import { FormlyFieldConfig, FormlyConfig } from '@ngx-formly/core';
import { TranslateService } from '@ngx-translate/core';
@Injectable()
export class FormlyValidations {
constructor(private translate: TranslateService, private formlyConfig: FormlyConfig) {}
init(): void {
// message without params
this.formlyConfig.addValidatorMessage('required', (_err, _field) =>
this.translate.stream('validations.required')
);
// message with params
this.formlyConfig.addValidatorMessage('minLength', (err, field) =>
this.minLengthValidationMessage(err, field, this.translate)
);
this.formlyConfig.addValidatorMessage('maxLength', (err, field) =>
this.maxLengthValidationMessage(err, field, this.translate)
);
this.formlyConfig.addValidatorMessage('min', (err, field) =>
this.minValidationMessage(err, field, this.translate)
);
this.formlyConfig.addValidatorMessage('max', (err, field) =>
this.maxValidationMessage(err, field, this.translate)
);
}
private minLengthValidationMessage(
err: any,
field: FormlyFieldConfig,
translate: TranslateService
) {
return translate.stream('validations.minlength', { number: field.props?.minLength });
}
private maxLengthValidationMessage(
err: any,
field: FormlyFieldConfig,
translate: TranslateService
) {
return translate.stream('validations.maxlength', { number: field.props?.maxLength });
}
private minValidationMessage(err: any, field: FormlyFieldConfig, translate: TranslateService) {
return translate.stream('validations.min', { number: field.props?.min });
}
private maxValidationMessage(err: any, field: FormlyFieldConfig, translate: TranslateService) {
return translate.stream('validations.max', { number: field.props?.max });
}
}
-29
View File
@@ -1,29 +0,0 @@
import { Component } from '@angular/core';
import { FieldWrapper } from '@ngx-formly/core';
/**
* This is just an example.
*/
@Component({
selector: 'formly-wrapper-card',
template: `
<div class="card">
<h3 class="card-header">Its time to party</h3>
<h3 class="card-header">{{ props.label }}</h3>
<div class="card-body">
<ng-container #fieldComponent></ng-container>
</div>
</div>
`,
})
export class FormlyWrapperCardComponent extends FieldWrapper {}
@Component({
selector: 'formly-wrapper-div',
template: `
<div>
<ng-container #fieldComponent></ng-container>
</div>
`,
})
export class FormlyWrapperDivComponent extends FieldWrapper {}
+275
View File
@@ -0,0 +1,275 @@
import {
cibCcAmex,
cibCcApplePay,
cibCcMastercard,
cibCcPaypal,
cibCcStripe,
cibCcVisa,
cibFacebook,
cibGoogle,
cibLinkedin,
cibSkype,
cibTwitter,
cifBr,
cifEs,
cifFr,
cifIn,
cifPl,
cifUs,
cilAlignCenter,
cilAlignLeft,
cilAlignRight,
cilApplicationsSettings,
cilArrowBottom,
cilArrowRight,
cilArrowTop,
cilBasket,
cilBell,
cilBold,
cilBookmark,
cilCalculator,
cilCalendar,
cilChart,
cilChartPie,
cilCheck,
cilChevronLeft,
cilChevronRight,
cilCloudDownload,
cilCode,
cilCommentSquare,
cilCreditCard,
cilCursor,
cilDollar,
cilDrop,
cilEnvelopeClosed,
cilEnvelopeOpen,
cilFile,
cilHome,
cilInbox,
cilIndentDecrease,
cilIndentIncrease,
cilItalic,
cilJustifyCenter,
cilLayers,
cilList,
cilListNumbered,
cilLocationPin,
cilLockLocked,
cilMagnifyingGlass,
cilMap,
cilMediaPlay,
cilMenu,
cilMoon,
cilNotes,
cilOptions,
cilPaperclip,
cilPaperPlane,
cilPen,
cilPencil,
cilPeople,
cilPrint,
cilPuzzle,
cilReportSlash,
cilSave,
cilSettings,
cilShare,
cilShareAll,
cilShareBoxed,
cilSpeech,
cilSpeedometer,
cilSpreadsheet,
cilStar,
cilSun,
cilTags,
cilTask,
cilTrash,
cilUnderline,
cilUser,
cilUserFemale,
cilUserFollow,
cilUserUnfollow,
} from '@coreui/icons';
export const iconSubset = {
cibCcAmex,
cibCcApplePay,
cibCcMastercard,
cibCcPaypal,
cibCcStripe,
cibCcVisa,
cibFacebook,
cibGoogle,
cibLinkedin,
cibSkype,
cibTwitter,
cifBr,
cifEs,
cifFr,
cifIn,
cifPl,
cifUs,
cilAlignCenter,
cilAlignLeft,
cilAlignRight,
cilApplicationsSettings,
cilArrowBottom,
cilArrowRight,
cilArrowTop,
cilBasket,
cilBell,
cilBold,
cilBookmark,
cilCalculator,
cilCalendar,
cilChart,
cilChartPie,
cilCheck,
cilChevronLeft,
cilChevronRight,
cilCloudDownload,
cilCode,
cilCommentSquare,
cilCreditCard,
cilCursor,
cilDollar,
cilDrop,
cilEnvelopeClosed,
cilEnvelopeOpen,
cilFile,
cilHome,
cilInbox,
cilIndentDecrease,
cilIndentIncrease,
cilItalic,
cilJustifyCenter,
cilLayers,
cilList,
cilListNumbered,
cilLocationPin,
cilLockLocked,
cilMagnifyingGlass,
cilMap,
cilMediaPlay,
cilMenu,
cilMoon,
cilNotes,
cilOptions,
cilPaperclip,
cilPaperPlane,
cilPen,
cilPencil,
cilPeople,
cilPrint,
cilPuzzle,
cilReportSlash,
cilSave,
cilSettings,
cilShare,
cilShareAll,
cilShareBoxed,
cilSpeech,
cilSpeedometer,
cilSpreadsheet,
cilStar,
cilSun,
cilTags,
cilTask,
cilTrash,
cilUnderline,
cilUser,
cilUserFemale,
cilUserFollow,
cilUserUnfollow,
};
export enum IconSubset {
cibCcAmex = 'cibCcAmex',
cibCcApplePay = 'cibCcApplePay',
cibCcMastercard = 'cibCcMastercard',
cibCcPaypal = 'cibCcPaypal',
cibCcStripe = 'cibCcStripe',
cibCcVisa = 'cibCcVisa',
cibFacebook = 'cibFacebook',
cibGoogle = 'cibGoogle',
cibLinkedin = 'cibLinkedin',
cibSkype = 'cibSkype',
cibTwitter = 'cibTwitter',
cifBr = 'cifBr',
cifEs = 'cifEs',
cifFr = 'cifFr',
cifIn = 'cifIn',
cifPl = 'cifPl',
cifUs = 'cifUs',
cilAlignCenter = 'cilAlignCenter',
cilAlignLeft = 'cilAlignLeft',
cilAlignRight = 'cilAlignRight',
cilApplicationsSettings = 'cilApplicationsSettings',
cilArrowBottom = 'cilArrowBottom',
cilArrowRight = 'cilArrowRight',
cilArrowTop = 'cilArrowTop',
cilBasket = 'cilBasket',
cilBell = 'cilBell',
cilBold = 'cilBold',
cilBookmark = 'cilBookmark',
cilCalculator = 'cilCalculator',
cilCalendar = 'cilCalendar',
cilChart = 'cilChart',
cilChartPie = 'cilChartPie',
cilCheck='cilCheck',
cilChevronLeft = 'cilChevronLeft',
cilChevronRight = 'cilChevronRight',
cilCloudDownload = 'cilCloudDownload',
cilCode = 'cilCode',
cilCommentSquare = 'cilCommentSquare',
cilCreditCard = 'cilCreditCard',
cilCursor = 'cilCursor',
cilDollar = 'cilDollar',
cilDrop = 'cilDrop',
cilEnvelopeClosed = 'cilEnvelopeClosed',
cilEnvelopeOpen = 'cilEnvelopeOpen',
cilFile = 'cilFile',
cilHome = 'cilHome',
cilInbox = 'cilInbox',
cilIndentDecrease = 'cilIndentDecrease',
cilIndentIncrease = 'cilIndentIncrease',
cilItalic = 'cilItalic',
cilJustifyCenter = 'cilJustifyCenter',
cilLayers = 'cilLayers',
cilList = 'cilList',
cilListNumbered = 'cilListNumbered',
cilLocationPin = 'cilLocationPin',
cilLockLocked = 'cilLockLocked',
cilMagnifyingGlass = 'cilMagnifyingGlass',
cilMap = 'cilMap',
cilMediaPlay = 'cilMediaPlay',
cilMenu = 'cilMenu',
cilMoon = 'cilMoon',
cilNotes = 'cilNotes',
cilOptions = 'cilOptions',
cilPaperclip = 'cilPaperclip',
cilPaperPlane = 'cilPaperPlane',
cilPen = 'cilPen',
cilPencil = 'cilPencil',
cilPeople = 'cilPeople',
cilPrint = 'cilPrint',
cilPuzzle = 'cilPuzzle',
cilReportSlash = 'cilReportSlash',
cilSave = 'cilSave',
cilSettings = 'cilSettings',
cilShare = 'cilShare',
cilShareAll = 'cilShareAll',
cilShareBoxed = 'cilShareBoxed',
cilSpeech = 'cilSpeech',
cilSpeedometer = 'cilSpeedometer',
cilSpreadsheet = 'cilSpreadsheet',
cilStar = 'cilStar',
cilSun = 'cilSun',
cilTags = 'cilTags',
cilTask = 'cilTask',
cilTrash = 'cilTrash',
cilUnderline = 'cilUnderline',
cilUser = 'cilUser',
cilUserFemale = 'cilUserFemale',
cilUserFollow = 'cilUserFollow',
cilUserUnfollow = 'cilUserUnfollow',
}
@@ -1,66 +0,0 @@
import { NgModule } from '@angular/core';
import { MtxAlertModule } from '@ng-matero/extensions/alert';
import { MtxButtonModule } from '@ng-matero/extensions/button';
import { MtxCheckboxGroupModule } from '@ng-matero/extensions/checkbox-group';
import { MtxColorpickerModule } from '@ng-matero/extensions/colorpicker';
import { MtxDatetimepickerModule } from '@ng-matero/extensions/datetimepicker';
import { MtxDialogModule } from '@ng-matero/extensions/dialog';
import { MtxDrawerModule } from '@ng-matero/extensions/drawer';
import { MtxGridModule } from '@ng-matero/extensions/grid';
import { MtxLoaderModule } from '@ng-matero/extensions/loader';
import { MtxPopoverModule } from '@ng-matero/extensions/popover';
import { MtxProgressModule } from '@ng-matero/extensions/progress';
import { MtxSelectModule } from '@ng-matero/extensions/select';
import { MtxSliderModule } from '@ng-matero/extensions/slider';
import { MtxSplitModule } from '@ng-matero/extensions/split';
import { MtxTooltipModule } from '@ng-matero/extensions/tooltip';
import { MTX_DATETIME_FORMATS } from '@ng-matero/extensions/core';
import { MtxMomentDatetimeModule } from '@ng-matero/extensions-moment-adapter';
@NgModule({
exports: [
MtxAlertModule,
MtxButtonModule,
MtxCheckboxGroupModule,
MtxColorpickerModule,
MtxDatetimepickerModule,
MtxDialogModule,
MtxDrawerModule,
MtxGridModule,
MtxLoaderModule,
MtxPopoverModule,
MtxProgressModule,
MtxSelectModule,
MtxSliderModule,
MtxSplitModule,
MtxTooltipModule,
MtxMomentDatetimeModule, // <= You can import the other adapter you need (luxon, date-fns)
],
providers: [
{
provide: MTX_DATETIME_FORMATS,
useValue: {
parse: {
dateInput: 'YYYY-MM-DD',
yearInput: 'YYYY',
monthInput: 'MMMM',
datetimeInput: 'YYYY-MM-DD HH:mm',
timeInput: 'HH:mm',
},
display: {
dateInput: 'YYYY-MM-DD',
yearInput: 'YYYY',
monthInput: 'MMMM',
datetimeInput: 'YYYY-MM-DD HH:mm',
timeInput: 'HH:mm',
monthYearLabel: 'YYYY MMMM',
dateA11yLabel: 'LL',
monthYearA11yLabel: 'MMMM YYYY',
popupHeaderDateLabel: 'MMM DD, ddd',
},
},
},
],
})
export class MaterialExtensionsModule {}
-117
View File
@@ -1,117 +0,0 @@
import { NgModule } from '@angular/core';
import { MatAutocompleteModule } from '@angular/material/autocomplete';
import { MatBadgeModule } from '@angular/material/badge';
import { MatBottomSheetModule } from '@angular/material/bottom-sheet';
import { MatButtonModule } from '@angular/material/button';
import { MatButtonToggleModule } from '@angular/material/button-toggle';
import { MatCardModule } from '@angular/material/card';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatChipsModule } from '@angular/material/chips';
import { MatRippleModule, MAT_DATE_FORMATS, MAT_DATE_LOCALE } from '@angular/material/core';
import { MatDatepickerModule } from '@angular/material/datepicker';
import {
MatDialogConfig,
MatDialogModule,
MAT_DIALOG_DEFAULT_OPTIONS,
} from '@angular/material/dialog';
import { MatDividerModule } from '@angular/material/divider';
import { MatExpansionModule } from '@angular/material/expansion';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatGridListModule } from '@angular/material/grid-list';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { MatListModule } from '@angular/material/list';
import { MatMenuModule } from '@angular/material/menu';
import { MatPaginatorIntl, MatPaginatorModule } from '@angular/material/paginator';
import { MatProgressBarModule } from '@angular/material/progress-bar';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatRadioModule } from '@angular/material/radio';
import { MatSelectModule } from '@angular/material/select';
import { MatSidenavModule } from '@angular/material/sidenav';
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
import { MatSliderModule } from '@angular/material/slider';
import { MatSnackBarModule } from '@angular/material/snack-bar';
import { MatSortModule } from '@angular/material/sort';
import { MatStepperModule } from '@angular/material/stepper';
import { MatTableModule } from '@angular/material/table';
import { MatTabsModule } from '@angular/material/tabs';
import { MatToolbarModule } from '@angular/material/toolbar';
import { MatTooltipModule } from '@angular/material/tooltip';
import { MatTreeModule } from '@angular/material/tree';
import { MatMomentDateModule } from '@angular/material-moment-adapter';
import { PaginatorI18nService } from '@shared/services/paginator-i18n.service';
@NgModule({
exports: [
MatAutocompleteModule,
MatBadgeModule,
MatBottomSheetModule,
MatButtonModule,
MatButtonToggleModule,
MatCardModule,
MatCheckboxModule,
MatChipsModule,
MatStepperModule,
MatDatepickerModule,
MatMomentDateModule,
MatDialogModule,
MatDividerModule,
MatExpansionModule,
MatFormFieldModule,
MatGridListModule,
MatIconModule,
MatInputModule,
MatListModule,
MatMenuModule,
MatPaginatorModule,
MatProgressBarModule,
MatProgressSpinnerModule,
MatRadioModule,
MatRippleModule,
MatSelectModule,
MatSidenavModule,
MatSliderModule,
MatSlideToggleModule,
MatSnackBarModule,
MatSortModule,
MatTableModule,
MatTabsModule,
MatToolbarModule,
MatTooltipModule,
MatTreeModule,
],
providers: [
{
provide: MatPaginatorIntl,
deps: [PaginatorI18nService],
useFactory: (paginatorI18nSrv: PaginatorI18nService) => paginatorI18nSrv.getPaginatorIntl(),
},
{
provide: MAT_DIALOG_DEFAULT_OPTIONS,
useValue: {
...new MatDialogConfig(),
},
},
{
provide: MAT_DATE_LOCALE,
useFactory: () => navigator.language, // <= This will be overrided by runtime setting
},
{
provide: MAT_DATE_FORMATS,
useValue: {
parse: {
dateInput: 'YYYY-MM-DD',
},
display: {
dateInput: 'YYYY-MM-DD',
monthYearLabel: 'YYYY MMM',
dateA11yLabel: 'LL',
monthYearA11yLabel: 'YYYY MMM',
},
},
},
],
})
export class MaterialModule {}
@@ -1,28 +0,0 @@
<page-header></page-header>
<button routerLink="../../list" mat-fab extended>
<mat-icon>arrow_back</mat-icon>
Back
</button>
<form [formGroup]="cardForm" (ngSubmit)="onSubmit()">
<mat-form-field appearance="fill" formControlName="name">
<mat-label>Name</mat-label>
<input matInput [value]="this.item.name">
</mat-form-field>
<mat-form-field appearance="fill" formControlName="address">
<mat-label>Address</mat-label>
<input matInput [value]="this.item.address">
</mat-form-field>
<mat-form-field appearance="fill" formControlName="type">
<mat-label>Type</mat-label>
<mat-select [value]="this.item.type">
<mat-option value="individual">Particulier</mat-option>
<mat-option value="corporation">Entreprise</mat-option>
<mat-option value="institution">Institution</mat-option>
</mat-select>
</mat-form-field>
<button class="button" type="submit">Modifier</button>
</form>
@@ -1,25 +0,0 @@
import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing';
import { ClientsCardComponent } from './card.component';
describe('ClientsCardComponent', () => {
let component: ClientsCardComponent;
let fixture: ComponentFixture<ClientsCardComponent>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [ ClientsCardComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ClientsCardComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -1,48 +0,0 @@
import { Component, OnInit, Input } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { ClientsService } from '../clients.service'
export interface Client {
_id: string;
name: string;
address: string;
type: string;
}
@Component({
selector: 'app-clients-card',
templateUrl: './card.component.html',
styleUrls: ['./card.component.css']
})
export class ClientsCardComponent implements OnInit {
@Input() id: string = "";
item: Client = {_id: '', name: '', address: '', type: ''};
cardForm = this.formBuilder.group({
name: '',
address: '',
type: ''
});
constructor(private formBuilder: FormBuilder, private clientsService: ClientsService) {
if (this.id == "") {
const url_parts = window.location.href.split('/')
this.id = url_parts[url_parts.length - 1];
}
this.item = {_id: '', name: '', address: '', type: ''}
}
ngOnInit() {
this.getData();
}
private getData() {
this.clientsService.get(this.id).subscribe((data: any) => {
this.item = data;
});
}
onSubmit(): void {
}
}
@@ -1,14 +0,0 @@
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { ClientsListComponent } from './list/list.component';
import { ClientsCardComponent } from './card/card.component';
const routes: Routes = [{ path: 'list', component: ClientsListComponent },
{ path: 'card/:id', component: ClientsCardComponent }
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class ClientsRoutingModule { }
@@ -1,20 +0,0 @@
import { NgModule } from '@angular/core';
import { SharedModule } from '@shared/shared.module';
import { ClientsRoutingModule } from './clients-routing.module';
import { ClientsListComponent } from './list/list.component';
import { ClientsCardComponent } from './card/card.component';
const COMPONENTS: any[] = [ClientsListComponent, ClientsCardComponent];
const COMPONENTS_DYNAMIC: any[] = [];
@NgModule({
imports: [
SharedModule,
ClientsRoutingModule
],
declarations: [
...COMPONENTS,
...COMPONENTS_DYNAMIC
]
})
export class ClientsModule { }
@@ -1,23 +0,0 @@
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import {Client} from "./list/list.component";
@Injectable({
providedIn: 'root'
})
export class ClientsService {
constructor(private http: HttpClient) { }
public getList(page: number, size: number, sortColumn: string, sortDirection: string) {
return this.http.get<{ menu: Client[] }>(
`/api/v1/entity/?size=${size}&page=${page + 1}&sort_by=${sortDirection}(${sortColumn})`
);
}
public get(id: string) {
return this.http.get<{ menu: Client[] }>(
`/api/v1/entity/${id}`
);
}
}
@@ -1,43 +0,0 @@
<page-header></page-header>
<mat-form-field>
<mat-label>Filter</mat-label>
<input matInput (keyup)="applyFilter($event)" placeholder="Ex. Mia" #input>
</mat-form-field>
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8 items" matSort (matSortChange)="handleSortChange($event)">
<ng-container matColumnDef="_id">
<th mat-header-cell *matHeaderCellDef mat-sort-header> ID </th>
<td mat-cell *matCellDef="let item"> {{item._id}} </td>
</ng-container>
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef mat-sort-header> Nom </th>
<td mat-cell *matCellDef="let item"> {{item.name}} </td>
</ng-container>
<ng-container matColumnDef="type">
<th mat-header-cell *matHeaderCellDef mat-sort-header> Type </th>
<td mat-cell *matCellDef="let item"> {{item.type}} </td>
</ng-container>
<ng-container matColumnDef="address">
<th mat-header-cell *matHeaderCellDef mat-sort-header> Adresse </th>
<td mat-cell *matCellDef="let item"> {{item.address}} </td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row
routerLink="../card/{{row._id}}"
*matRowDef="let row; columns: displayedColumns;"
></tr>
</table>
<mat-paginator (page)="handlePageEvent($event)"
[pageSizeOptions]="[5, 10, 15, 25, 50]"
[pageSize]="pageSize"
[length]="length"
showFirstLastButtons
aria-label="Select page">
</mat-paginator>
@@ -1,25 +0,0 @@
import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing';
import { ClientsListComponent } from './list.component';
describe('ClientsListComponent', () => {
let component: ClientsListComponent;
let fixture: ComponentFixture<ClientsListComponent>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [ ClientsListComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ClientsListComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -1,84 +0,0 @@
import { AfterViewInit, Component, OnInit, ViewChild } from '@angular/core';
import { MatPaginator, PageEvent } from '@angular/material/paginator';
import { MatSort, Sort} from '@angular/material/sort';
import { Injectable } from '@angular/core';
import { Menu } from "@core";
import { BehaviorSubject, forkJoin, fromEvent, Observable } from "rxjs";
import { map, take } from "rxjs/operators";
import { ClientsService } from '../clients.service'
import {Location} from '@angular/common';
import { Router } from '@angular/router';
export interface Client {
_id: string;
name: string;
address: string;
type: string;
}
@Component({
selector: 'app-clients-list',
templateUrl: './list.component.html',
styleUrls: ['./list.component.css']
})
export class ClientsListComponent implements OnInit {
displayedColumns: string[] = ['type', 'name', 'address', '_id',];
dataSource: Client[] = [];
length: number = 0;
pageIndex: number = 0;
pageSize: number = 15;
sortColumn: string = "name";
sortDirection: string = "asc";
//@ViewChild(MatSort) sort: MatSort;
constructor(private location: Location, private clientsService: ClientsService, private router: Router) { }
ngAfterViewInit() {
}
ngOnInit() {
this.getData();
}
handlePageEvent(e: PageEvent) {
this.length = e.length;
this.pageSize = e.pageSize;
this.pageIndex = e.pageIndex;
this.getData();
}
handleSortChange(s: Sort) {
this.sortColumn = s.active;
this.sortDirection = s.direction;
this.getData();
}
handleClickRow(row: any) {
window.location.href=window.location.href.replace('list', `card/${row._id}`)
this.router.navigateByUrl("card");
}
private getData() {
this.clientsService.getList(this.pageIndex, this.pageSize, this.sortColumn, this.sortDirection).subscribe((data: any) => {
this.dataSource = data.items;
this.length = data.total;
this.pageSize = data.size;
this.pageIndex = data.page - 1;
});
}
applyFilter(event: Event) {
// const filterValue = (event.target as HTMLInputElement).value;
// this.dataSource.filter = filterValue.trim().toLowerCase();
//
// if (this.dataSource.paginator) {
// this.dataSource.paginator.firstPage();
// }
}
}
@@ -1 +0,0 @@
<page-header></page-header>
@@ -1,13 +0,0 @@
import { Component, OnInit, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
@Component({
selector: 'app-dashboard',
templateUrl: './dashboard.component.html',
styleUrls: ['./dashboard.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class DashboardComponent implements OnInit {
constructor(private cdr: ChangeDetectorRef) {}
ngOnInit() {}
}
@@ -1,49 +0,0 @@
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { environment } from '@env/environment';
import { AdminLayoutComponent } from '../theme/admin-layout/admin-layout.component';
import { AuthLayoutComponent } from '../theme/auth-layout/auth-layout.component';
import { DashboardComponent } from './dashboard/dashboard.component';
import { LoginComponent } from './sessions/login/login.component';
import { RegisterComponent } from './sessions/register/register.component';
import { Error403Component } from './sessions/403.component';
import { Error404Component } from './sessions/404.component';
import { Error500Component } from './sessions/500.component';
import { AuthGuard } from '@core';
const routes: Routes = [
{
path: '',
component: AdminLayoutComponent,
canActivate: [AuthGuard],
canActivateChild: [AuthGuard],
children: [
{ path: '', redirectTo: 'dashboard', pathMatch: 'full' },
{ path: 'dashboard', component: DashboardComponent },
{ path: '403', component: Error403Component },
{ path: '404', component: Error404Component },
{ path: '500', component: Error500Component },
{ path: 'clients', loadChildren: () => import('./clients/clients.module').then(m => m.ClientsModule) },
],
},
{
path: 'auth',
component: AuthLayoutComponent,
children: [
{ path: 'login', component: LoginComponent },
{ path: 'register', component: RegisterComponent },
],
},
{ path: '**', redirectTo: 'dashboard' },
];
@NgModule({
imports: [
RouterModule.forRoot(routes, {
useHash: environment.useHash,
}),
],
exports: [RouterModule],
})
export class RoutesRoutingModule {}
-26
View File
@@ -1,26 +0,0 @@
import { NgModule } from '@angular/core';
import { SharedModule } from '@shared/shared.module';
import { RoutesRoutingModule } from './routes-routing.module';
import { DashboardComponent } from './dashboard/dashboard.component';
import { LoginComponent } from './sessions/login/login.component';
import { RegisterComponent } from './sessions/register/register.component';
import { Error403Component } from './sessions/403.component';
import { Error404Component } from './sessions/404.component';
import { Error500Component } from './sessions/500.component';
const COMPONENTS: any[] = [
DashboardComponent,
LoginComponent,
RegisterComponent,
Error403Component,
Error404Component,
Error500Component,
];
const COMPONENTS_DYNAMIC: any[] = [];
@NgModule({
imports: [SharedModule, RoutesRoutingModule],
declarations: [...COMPONENTS, ...COMPONENTS_DYNAMIC],
})
export class RoutesModule {}
@@ -1,13 +0,0 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-error-403',
template: `
<error-code
code="403"
[title]="'Permission denied!'"
[message]="'You do not have permission to access the requested data.'"
></error-code>
`,
})
export class Error403Component {}
@@ -1,13 +0,0 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-error-404',
template: `
<error-code
code="404"
[title]="'Page not found!'"
[message]="'This is not the web page you are looking for.'"
></error-code>
`,
})
export class Error404Component {}
@@ -1,14 +0,0 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-error-500',
template: `
<error-code
code="500"
[title]="'Server went wrong!'"
[message]="'Just kidding, looks like we have an internal issue, please try refreshing.'"
>
</error-code>
`,
})
export class Error500Component {}
@@ -1,45 +0,0 @@
<div class="d-flex w-full h-full">
<mat-card class="m-auto" style="max-width: 380px;">
<mat-card-header class="m-b-24">
<mat-card-title>{{'login.title' | translate}}!</mat-card-title>
</mat-card-header>
<mat-card-content>
<form class="form-field-full" [formGroup]="loginForm">
<mat-form-field appearance="outline">
<mat-label>{{'login.username' | translate}}: ng-matero</mat-label>
<input matInput placeholder="ng-matero" formControlName="username" required>
<mat-error *ngIf="username.invalid">
<span *ngIf="username.errors?.required">{{'login.please_enter' | translate}}
<strong>ng-matero</strong>
</span>
<span *ngIf="username.errors?.remote">{{ username.errors?.remote }}</span>
</mat-error>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>{{'login.password' | translate}}: ng-matero</mat-label>
<input matInput placeholder="ng-matero" type="password"
formControlName="password" required>
<mat-error *ngIf="password.invalid">
<span *ngIf="password.errors?.required">{{'login.please_enter' | translate}}
<strong>ng-matero</strong>
</span>
<span *ngIf="password.errors?.remote">{{ password.errors?.remote }}</span>
</mat-error>
</mat-form-field>
<mat-checkbox formControlName="rememberMe">{{'login.remember_me' | translate}}
</mat-checkbox>
<button class="w-full m-y-16" mat-raised-button color="primary"
[disabled]="!!loginForm.invalid" [loading]="isSubmitting"
(click)="login()">{{'login.login' | translate}}</button>
<div>{{'login.have_no_account' | translate}}?
<a routerLink="/auth/register">{{'login.create_one' | translate}}</a>
</div>
</form>
</mat-card-content>
</mat-card>
</div>
@@ -1,58 +0,0 @@
import { Component } from '@angular/core';
import { FormBuilder, Validators } from '@angular/forms';
import { Router } from '@angular/router';
import { HttpErrorResponse } from '@angular/common/http';
import { filter } from 'rxjs/operators';
import { AuthService } from '@core/authentication';
@Component({
selector: 'app-login',
templateUrl: './login.component.html',
styleUrls: ['./login.component.scss'],
})
export class LoginComponent {
isSubmitting = false;
loginForm = this.fb.nonNullable.group({
username: ['ng-matero', [Validators.required]],
password: ['ng-matero', [Validators.required]],
rememberMe: [false],
});
constructor(private fb: FormBuilder, private router: Router, private auth: AuthService) {}
get username() {
return this.loginForm.get('username')!;
}
get password() {
return this.loginForm.get('password')!;
}
get rememberMe() {
return this.loginForm.get('rememberMe')!;
}
login() {
this.isSubmitting = true;
this.auth
.login(this.username.value, this.password.value, this.rememberMe.value)
.pipe(filter(authenticated => authenticated))
.subscribe(
() => this.router.navigateByUrl('/'),
(errorRes: HttpErrorResponse) => {
if (errorRes.status === 422) {
const form = this.loginForm;
const errors = errorRes.error.errors;
Object.keys(errors).forEach(key => {
form.get(key === 'email' ? 'username' : key)?.setErrors({
remote: errors[key][0],
});
});
}
this.isSubmitting = false;
}
);
}
}
@@ -1,52 +0,0 @@
<div class="d-flex w-full h-full">
<mat-card class="m-auto" style="max-width: 380px;">
<mat-card-header class="m-b-24">
<mat-card-title>
{{'register.welcome' | translate}}, <br />
{{'register.title' | translate}}
</mat-card-title>
</mat-card-header>
<mat-card-content>
<form class="form-field-full" [formGroup]="registerForm">
<mat-form-field appearance="outline">
<mat-label>{{'login.username' | translate}}</mat-label>
<input matInput formControlName="username" required>
<mat-error *ngIf="registerForm.get('username')?.invalid">
<span>{{'validations.required' | translate}}</span>
</mat-error>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>{{'login.password' | translate}}</mat-label>
<input matInput type="password" formControlName="password" required>
<mat-error *ngIf="registerForm.get('password')?.invalid">
<span>{{'validations.required' | translate}}</span>
</mat-error>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>{{'register.confirm_password' | translate}}</mat-label>
<input matInput type="password" formControlName="confirmPassword" required>
<mat-error *ngIf="registerForm.get('confirmPassword')?.hasError('required')">
<span>{{'validations.required' | translate}}</span>
</mat-error>
<mat-error *ngIf="registerForm.get('confirmPassword')?.hasError('mismatch')"
translate [translateParams]="{value: 'login.password' | translate}">
<span>{{'validations.inconsistent'}}</span>
</mat-error>
</mat-form-field>
<mat-checkbox>{{'register.agree' | translate}}</mat-checkbox>
<button class="w-full m-y-16" mat-raised-button color="primary">
{{'register.register' | translate}}
</button>
<div>{{'register.have_an_account' | translate}}?
<a routerLink="/auth/login">{{'login.login' | translate}}</a>
</div>
</form>
</mat-card-content>
</mat-card>
</div>
@@ -1,39 +0,0 @@
import { Component } from '@angular/core';
import { FormBuilder, Validators, AbstractControl } from '@angular/forms';
@Component({
selector: 'app-register',
templateUrl: './register.component.html',
styleUrls: ['./register.component.scss'],
})
export class RegisterComponent {
registerForm = this.fb.nonNullable.group(
{
username: ['', [Validators.required]],
password: ['', [Validators.required]],
confirmPassword: ['', [Validators.required]],
},
{
validators: [this.matchValidator('password', 'confirmPassword')],
}
);
constructor(private fb: FormBuilder) {}
matchValidator(source: string, target: string) {
return (control: AbstractControl) => {
const sourceControl = control.get(source)!;
const targetControl = control.get(target)!;
if (targetControl.errors && !targetControl.errors.mismatch) {
return null;
}
if (sourceControl.value !== targetControl.value) {
targetControl.setErrors({ mismatch: true });
return { mismatch: true };
} else {
targetControl.setErrors(null);
return null;
}
};
}
}
@@ -1,12 +0,0 @@
<nav aria-label="breadcrumb">
<ol class="matero-breadcrumb">
<li class="matero-breadcrumb-item"
*ngFor="let navLink of nav; trackBy: trackByNavlink; first as isFirst;">
<a href="#" class="link" *ngIf="isFirst">{{navLink}}</a>
<ng-container *ngIf="!isFirst">
<mat-icon class="chevron">chevron_right</mat-icon>
<span>{{navLink | translate}}</span>
</ng-container>
</li>
</ol>
</nav>
@@ -1,33 +0,0 @@
.matero-breadcrumb {
display: flex;
flex-wrap: wrap;
margin-bottom: 1rem;
padding: 0;
font-size: .875rem;
list-style: none;
}
.matero-breadcrumb-item {
line-height: 18px;
text-transform: capitalize;
> * {
vertical-align: middle;
}
> a.link {
color: currentColor;
&:hover {
color: currentColor;
text-decoration: underline;
}
}
> .chevron {
width: 18px;
height: 18px;
font-size: 18px;
user-select: none;
}
}
@@ -1,33 +0,0 @@
import { Component, OnInit, ViewEncapsulation, Input } from '@angular/core';
import { Router } from '@angular/router';
import { MenuService } from '@core/bootstrap/menu.service';
@Component({
selector: 'breadcrumb',
templateUrl: './breadcrumb.component.html',
styleUrls: ['./breadcrumb.component.scss'],
encapsulation: ViewEncapsulation.None,
})
export class BreadcrumbComponent implements OnInit {
@Input() nav: string[] = [];
constructor(private router: Router, private menu: MenuService) {}
ngOnInit() {
this.nav = Array.isArray(this.nav) ? this.nav : [];
if (this.nav.length === 0) {
this.genBreadcrumb();
}
}
trackByNavlink(index: number, navLink: string): string {
return navLink;
}
genBreadcrumb() {
const routes = this.router.url.slice(1).split('/');
this.nav = this.menu.getLevel(routes);
this.nav.unshift('home');
}
}
@@ -1,11 +0,0 @@
@use 'sass:map';
@use '@angular/material' as mat;
@mixin theme($theme) {
$background: map.get($theme, background);
$foreground: map.get($theme, foreground);
.matero-error-code {
color: mat.get-color-from-palette($foreground, text);
}
}
@@ -1,55 +0,0 @@
// Long Shadow
//
// https://codepen.io/c_fitzmaurice/pen/ZYJeRY
@use 'sass:color';
@use 'sass:list';
@use 'sass:meta';
@use 'sass:map';
@use 'sass:math';
@function long-shadow($direction, $length, $color, $fade: false, $shadow-count: 100) {
$shadows: ();
$conversion-map: (
to top: 180deg,
to top right: 135deg,
to right top: 135deg,
to right: 90deg,
to bottom right: 45deg,
to right bottom: 45deg,
to bottom: 0deg,
to bottom left: 315deg,
to left bottom: 315deg,
to left: 270deg,
to left top: 225deg,
to top left: 225deg
);
@if map-has-key($conversion-map, $direction) {
$direction: map.get($conversion-map, $direction);
}
@for $i from 1 through $shadow-count {
$current-step: math.div($i * $length, $shadow-count);
$current-color: if(
not $fade,
$color,
if(
meta.type-of($fade) == 'color',
color.mix($fade, $color, math.div($i, $shadow-count) * 100%),
color.rgba($color, 1 - math.div($i, $shadow-count))
)
);
$shadows: list.append(
$shadows,
(math.sin(0deg + $direction) * $current-step)
(math.cos(0deg + $direction) * $current-step)
0
$current-color,
'comma'
);
}
@return $shadows;
}
@@ -1,6 +0,0 @@
<div class="matero-error-wrap">
<div class="matero-error-code">{{code}}</div>
<div class="matero-error-title" *ngIf="title">{{title}}</div>
<div class="matero-error-message" *ngIf="message">{{message}}</div>
<div><a mat-raised-button color="primary" routerLink="/">Back to Home</a></div>
</div>
@@ -1,32 +0,0 @@
@use 'long-shadow';
.matero-error-wrap {
text-align: center;
}
.matero-error-code {
padding: 20px 0;
font-size: 150px;
text-shadow:
long-shadow.long-shadow(
$direction: 45deg,
$length: 60px,
$color: rgba(0, 0, 0, .03),
$fade: rgba(0, 0, 0, .0015),
$shadow-count: 20
);
}
.matero-error-title {
margin: 0 0 16px;
font-weight: 500;
font-size: 20px;
line-height: 32px;
}
.matero-error-message {
margin: 0 0 16px;
font-weight: 400;
font-size: 16px;
line-height: 28px;
}
@@ -1,13 +0,0 @@
import { Component, ViewEncapsulation, Input } from '@angular/core';
@Component({
selector: 'error-code',
templateUrl: './error-code.component.html',
styleUrls: ['./error-code.component.scss'],
encapsulation: ViewEncapsulation.None,
})
export class ErrorCodeComponent {
@Input() code = '';
@Input() title = '';
@Input() message = '';
}
@@ -1,4 +0,0 @@
<div class="matero-page-header-inner">
<h1 class="matero-page-title">{{title | translate}} <small>{{subtitle}}</small></h1>
<breadcrumb *ngIf="!hideBreadcrumb" [nav]="nav"></breadcrumb>
</div>
@@ -1,18 +0,0 @@
.matero-page-header {
display: block;
margin: -16px -16px 16px;
padding: 16px;
color: #fff;
background-color: #3f51b5;
.matero-breadcrumb {
margin-top: 8px;
margin-bottom: 0;
}
}
.matero-page-title {
margin: 0;
font-weight: 400;
font-size: 24px;
}

Some files were not shown because too many files have changed in this diff Show More