Browse Source

complete create rules component

master
Christian Ziermann 3 years ago
parent
commit
abd3bc6e2c
36 changed files with 910 additions and 31 deletions
  1. +65
    -0
      api/api/rules.service.ts
  2. +8
    -0
      api/api/rules.serviceInterface.ts
  3. +1
    -0
      api/model/createRuleDto.ts
  4. +22
    -0
      package-lock.json
  5. +2
    -0
      package.json
  6. +20
    -2
      src/app/admin/admin.module.ts
  7. +77
    -0
      src/app/admin/create-rule/class/dynamic-data-source.ts
  8. +175
    -0
      src/app/admin/create-rule/class/dynamic-database.ts
  9. +7
    -0
      src/app/admin/create-rule/class/rule-flat-node.ts
  10. +34
    -1
      src/app/admin/create-rule/create-rule.component.html
  11. +20
    -0
      src/app/admin/create-rule/create-rule.component.scss
  12. +92
    -1
      src/app/admin/create-rule/create-rule.component.ts
  13. +19
    -1
      src/app/app-routing.module.ts
  14. +12
    -2
      src/app/app.module.ts
  15. +36
    -0
      src/app/auth/auth.module.ts
  16. +16
    -0
      src/app/auth/guard/auth.guard.spec.ts
  17. +27
    -0
      src/app/auth/guard/auth.guard.ts
  18. +16
    -0
      src/app/auth/interceptor/auth.interceptor.spec.ts
  19. +31
    -0
      src/app/auth/interceptor/auth.interceptor.ts
  20. +16
    -0
      src/app/auth/interceptor/error.interceptor.spec.ts
  21. +29
    -0
      src/app/auth/interceptor/error.interceptor.ts
  22. +20
    -0
      src/app/auth/login/login.component.html
  23. +11
    -0
      src/app/auth/login/login.component.scss
  24. +25
    -0
      src/app/auth/login/login.component.spec.ts
  25. +35
    -0
      src/app/auth/login/login.component.ts
  26. +16
    -0
      src/app/auth/service/auth.service.spec.ts
  27. +35
    -0
      src/app/auth/service/auth.service.ts
  28. +7
    -0
      src/app/class/user.spec.ts
  29. +4
    -0
      src/app/class/user.ts
  30. +1
    -1
      src/app/main/main.component.html
  31. +3
    -0
      src/app/rules/rule-list/rule-list.component.scss
  32. +1
    -1
      src/app/rules/rules/rules.component.html
  33. +3
    -19
      src/app/rules/rules/rules.component.scss
  34. +2
    -1
      src/environments/environment.prod.ts
  35. +2
    -1
      src/environments/environment.ts
  36. +20
    -1
      src/styles.scss

+ 65
- 0
api/api/rules.service.ts View File

@@ -297,4 +297,69 @@ export class RulesService implements RulesServiceInterface {
);
}

/**
* @param id
* @param body
* @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body.
* @param reportProgress flag to report request and response progress.
*/
public update(id: string, body: object, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable<object>;
public update(id: string, body: object, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable<HttpResponse<object>>;
public update(id: string, body: object, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json'}): Observable<HttpEvent<object>>;
public update(id: string, body: object, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json'}): Observable<any> {
if (id === null || id === undefined) {
throw new Error('Required parameter id was null or undefined when calling update.');
}
if (body === null || body === undefined) {
throw new Error('Required parameter body was null or undefined when calling update.');
}

let headers = this.defaultHeaders;

let credential: string | undefined;
// authentication (basic) required
credential = this.configuration.lookupCredential('basic');
if (credential) {
headers = headers.set('Authorization', 'Basic ' + credential);
}

let httpHeaderAcceptSelected: string | undefined = options && options.httpHeaderAccept;
if (httpHeaderAcceptSelected === undefined) {
// to determine the Accept header
const httpHeaderAccepts: string[] = [
'application/json'
];
httpHeaderAcceptSelected = this.configuration.selectHeaderAccept(httpHeaderAccepts);
}
if (httpHeaderAcceptSelected !== undefined) {
headers = headers.set('Accept', httpHeaderAcceptSelected);
}


// to determine the Content-Type header
const consumes: string[] = [
'application/json'
];
const httpContentTypeSelected: string | undefined = this.configuration.selectHeaderContentType(consumes);
if (httpContentTypeSelected !== undefined) {
headers = headers.set('Content-Type', httpContentTypeSelected);
}

let responseType_: 'text' | 'json' = 'json';
if(httpHeaderAcceptSelected && httpHeaderAcceptSelected.startsWith('text')) {
responseType_ = 'text';
}

return this.httpClient.patch<object>(`${this.configuration.basePath}/rules/${encodeURIComponent(String(id))}`,
body,
{
responseType: <any>responseType_,
withCredentials: this.configuration.withCredentials,
headers: headers,
observe: observe,
reportProgress: reportProgress
}
);
}

}

+ 8
- 0
api/api/rules.serviceInterface.ts View File

@@ -52,4 +52,12 @@ export interface RulesServiceInterface {
*/
remove(id: string, extraHttpRequestParams?: any): Observable<{}>;

/**
*
*
* @param id
* @param body
*/
update(id: string, body: object, extraHttpRequestParams?: any): Observable<object>;

}

+ 1
- 0
api/model/createRuleDto.ts View File

@@ -21,5 +21,6 @@ export interface CreateRuleDto {
*/
text: string;
subRuleIds: Array<string>;
parentId: string;
}


+ 22
- 0
package-lock.json View File

@@ -18,6 +18,8 @@
"@angular/platform-browser-dynamic": "~11.2.11",
"@angular/router": "~11.2.11",
"@openapitools/openapi-generator-cli": "^2.2.6",
"@types/base-64": "^1.0.0",
"base-64": "^1.0.0",
"rxjs": "~6.6.0",
"tslib": "^2.0.0",
"zone.js": "~0.11.3"
@@ -2668,6 +2670,11 @@
"node": ">= 10.0.0"
}
},
"node_modules/@types/base-64": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@types/base-64/-/base-64-1.0.0.tgz",
"integrity": "sha512-AvCJx/HrfYHmOQRFdVvgKMplXfzTUizmh0tz9GFTpDePWgCY4uoKll84zKlaRoeiYiCr7c9ZnqSTzkl0BUVD6g=="
},
"node_modules/@types/component-emitter": {
"version": "1.2.10",
"dev": true,
@@ -3487,6 +3494,11 @@
"node": ">=0.10.0"
}
},
"node_modules/base-64": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/base-64/-/base-64-1.0.0.tgz",
"integrity": "sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg=="
},
"node_modules/base/node_modules/define-property": {
"version": "1.0.0",
"dev": true,
@@ -20132,6 +20144,11 @@
}
}
},
"@types/base-64": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@types/base-64/-/base-64-1.0.0.tgz",
"integrity": "sha512-AvCJx/HrfYHmOQRFdVvgKMplXfzTUizmh0tz9GFTpDePWgCY4uoKll84zKlaRoeiYiCr7c9ZnqSTzkl0BUVD6g=="
},
"@types/component-emitter": {
"version": "1.2.10",
"dev": true
@@ -20741,6 +20758,11 @@
}
}
},
"base-64": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/base-64/-/base-64-1.0.0.tgz",
"integrity": "sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg=="
},
"base64-arraybuffer": {
"version": "0.1.4",
"dev": true

+ 2
- 0
package.json View File

@@ -25,6 +25,8 @@
"@angular/platform-browser-dynamic": "~11.2.11",
"@angular/router": "~11.2.11",
"@openapitools/openapi-generator-cli": "^2.2.6",
"@types/base-64": "^1.0.0",
"base-64": "^1.0.0",
"rxjs": "~6.6.0",
"tslib": "^2.0.0",
"zone.js": "~0.11.3"

+ 20
- 2
src/app/admin/admin.module.ts View File

@@ -1,7 +1,14 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { CreateRuleComponent } from './create-rule/create-rule.component';

import { MatCardModule } from '@angular/material/card';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatTreeModule } from '@angular/material/tree';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { MatCommonModule } from '@angular/material/core';
import { MatInputModule } from '@angular/material/input';
import { DynamicDatabase } from './create-rule/class/dynamic-database';


@NgModule({
@@ -9,7 +16,18 @@ import { CreateRuleComponent } from './create-rule/create-rule.component';
CreateRuleComponent
],
imports: [
CommonModule
CommonModule,
MatCardModule,
MatFormFieldModule,
MatTreeModule,
MatButtonModule,
MatIconModule,
MatCommonModule,
MatFormFieldModule,
MatInputModule,
],
providers: [
DynamicDatabase,
]
})
export class AdminModule { }

+ 77
- 0
src/app/admin/create-rule/class/dynamic-data-source.ts View File

@@ -0,0 +1,77 @@
import { CollectionViewer, DataSource, SelectionChange } from "@angular/cdk/collections";
import { FlatTreeControl } from "@angular/cdk/tree";
import { BehaviorSubject, merge, Observable } from "rxjs";
import { map } from "rxjs/operators";
import { DynamicDatabase } from "./dynamic-database";
import { RuleFlatNode } from "./rule-flat-node";

export class DynamicDataSource implements DataSource<RuleFlatNode> {

dataChange = new BehaviorSubject<RuleFlatNode[]>([]);

get data(): RuleFlatNode[] { return this.dataChange.value; }
set data(value: RuleFlatNode[]) {
this._treeControl.dataNodes = value;
this.dataChange.next(value);
}

constructor(private _treeControl: FlatTreeControl<RuleFlatNode>,
private _database: DynamicDatabase) {
}

connect(collectionViewer: CollectionViewer): Observable<RuleFlatNode[]> {
this._treeControl.expansionModel.changed.subscribe(change => {
if ((change as SelectionChange<RuleFlatNode>).added ||
(change as SelectionChange<RuleFlatNode>).removed) {
this.handleTreeControl(change as SelectionChange<RuleFlatNode>);
}
});

return merge(collectionViewer.viewChange, this.dataChange).pipe(map(() => this.data));
}

disconnect(collectionViewer: CollectionViewer): void { }

/** Handle expand/collapse behaviors */
handleTreeControl(change: SelectionChange<RuleFlatNode>) {
if (change.added) {
change.added.forEach(node => this.toggleNode(node, true));
}
if (change.removed) {
change.removed.slice().reverse().forEach(node => this.toggleNode(node, false));
}
}

/**
* Toggle the node, remove from display list
*/
toggleNode(node: RuleFlatNode, expand: boolean) {
const children = node.item.subRule;
const index = this.data.indexOf(node);
if (!children || index < 0) { // If no children, or cannot find the node, no op
return;
}

setTimeout(() => {
if (expand) {
const nodes = children.map(name =>
new RuleFlatNode(
name,
node.level + 1,
!!node.item.subRule ? node.item.subRule.length > 0 : false)
);
this.data.splice(index + 1, 0, ...nodes);
} else {
let count = 0;
for (let i = index + 1; i < this.data.length
&& this.data[i].level > node.level; i++, count++) { }
this.data.splice(index + 1, count);
}

// notify the change
this.dataChange.next(this.data);
node.isLoading = false;
}, 1000);
}
}

+ 175
- 0
src/app/admin/create-rule/class/dynamic-database.ts View File

@@ -0,0 +1,175 @@
import { Injectable } from "@angular/core"
import { RippleRef } from "@angular/material/core";
import { BehaviorSubject, Subject } from "rxjs";
import { map } from "rxjs/operators";
import { CreateRuleDto, Rule, RulesService } from "../../../../../api";
import { environment } from "../../../../environments/environment";
import { RuleFlatNode } from "./rule-flat-node";


@Injectable({ providedIn: 'root' })
export class DynamicDatabase {

dataChange = new BehaviorSubject<Rule[]>([]);
childAdded = new Subject<Rule>();

get data(): Rule[] { return this.dataChange.value; }
get count(): number { return this.dataChange.value.length };
protected addData(rule: Rule) { this.dataChange.next([...this.dataChange.value, rule]) }
constructor(protected rulesService: RulesService) {
this.rulesService.configuration.basePath = environment.apiUrl;
this.rulesService.findAll()
.subscribe(
(rules: Rule[]) => this.dataChange.next(
rules
.filter(this.filterRuleWithParentFromRootLayer)
.sort(this.compareRulesByParagraph)
)
),
(err) => {
//TODO add toaster
}
}
getParagraphOfNextChild = (parentRule): string => `${parentRule.paragraph}.${parentRule.subRule.length + 1}`;
filterRuleWithParentFromRootLayer = (rootLayerRule: Rule) => typeof rootLayerRule.parentRule !== 'number';

compareRulesByParagraph = (ruleA: Rule, ruleB: Rule) => this.compareParahraph(ruleA.paragraph, ruleB.paragraph);


compareParahraph(paragraphA: string, paragraphB: string): 0 | 1 | -1 {
if (paragraphA === paragraphB) {
return 0;
}
const paragrapASections = paragraphA.split('.');
const paragrapBSections = paragraphB.split('.');
if (+paragrapASections[0] > +paragrapBSections[0]) {
return 1;
}
if (+paragrapASections[0] < +paragrapBSections[0]) {
return -1;
}
if (+paragrapASections[0] === +paragrapBSections[0]) {
let shiftedParagraphASections = paragrapASections;
shiftedParagraphASections.shift();
let shiftedParagraphBSections = paragrapBSections;
shiftedParagraphBSections.shift();
return this.compareParahraph(
shiftedParagraphASections.join('.'),
shiftedParagraphBSections.join('.'),
)
}
}
getParentById = (ruleId: string) => this.data.find(rule => rule.id === ruleId)
insertEmptyChild(parent: Rule) {
parent.subRule.push(
{
text: '',
id: '0',
parentRule: parent,
paragraph: this.getParagraphOfNextChild(parent)
}
)
this.dataChange.next(this.data);
this.childAdded.next(parent);
}

insertEmptyParent() {
this.addData({
text: '',
id: '0',
parentRule: null,
paragraph: `${this.count + 2}`
})
}
insertChild(parent: Rule, subRule: Rule) {
const createSubRule: CreateRuleDto = {
paragraph: subRule.paragraph,
text: subRule.text,
subRuleIds: null,
parentId: parent.id
}
this.rulesService.create(createSubRule).subscribe(
newSubRule => {
if (typeof newSubRule.id !== 'undefined' && typeof newSubRule.parentRule?.id !== 'undefined') {
const updateParentRule: CreateRuleDto = {
paragraph: parent.paragraph,
text: parent.text,
subRuleIds: [...parent.subRule ? parent.subRule.map(rule => rule.id) : [], newSubRule.id]
} as CreateRuleDto;
this.rulesService.update(parent.id, updateParentRule).subscribe((updated) => {
//TODO Push updated message
this.update(parent.id, {
paragraph: parent.paragraph,
subRuleIds: [
...parent.subRule
.filter(rule => rule.id === "0")
.map(rule => rule.id) ?? [],
subRule.id,
],
text: parent.text
}).subscribe(
(updated) => {
//todo add update message
},
(err) => {
//TODO Push error message
console.error(err);
}
)
},
(err) => {
//TODO Push error message
console.error(err);
}
)
return;
}
//TODO Push error message
}
)
if (!parent.subRule) {
parent.subRule = [];
}
const lastIndex = parent.subRule.indexOf(subRule);
parent.subRule[lastIndex] = subRule;
this.dataChange.next(this.data);
this.childAdded.next(parent);
}

update(id: string, changedData: Partial<CreateRuleDto>) {
return this.rulesService.update(id, changedData);
}

insertParent(newRule: Rule) {
const createNewRule: CreateRuleDto = {
paragraph: `${this.count + 1}`,
text: newRule.text,
subRuleIds: null,
parentId: null
}
this.rulesService.create(createNewRule).subscribe(
() => {
//TODO created message
},
(err) => {
//TODO Error message
}
)
this.dataChange.next(this.data);
}

removeChild(parent: Rule, subRule: Rule) {
if (parent.subRule) {
const subRuleToRemove = this.findSubRuleFromParent(parent, subRule);
const indexIfSubRule = parent.subRule.indexOf(subRuleToRemove);
if (indexIfSubRule > -1) {
parent.subRule.splice(indexIfSubRule, 1);
}
this.dataChange.next(this.data);
}
}

findSubRuleFromParent = (parent: Rule, subRule: Rule): Rule | null =>
parent.subRule.find(rules => rules.paragraph === subRule.paragraph && rules.id === subRule.id);

}

+ 7
- 0
src/app/admin/create-rule/class/rule-flat-node.ts View File

@@ -0,0 +1,7 @@
import { Rule } from "../../../../../api";

export class RuleFlatNode {
constructor(public item: Rule, public level = 1, public expandable = false,
public isLoading = false,public root = true) { }
}


+ 34
- 1
src/app/admin/create-rule/create-rule.component.html View File

@@ -1 +1,34 @@
<p>create-rule works!</p>
<mat-card class="wood-card">
<mat-tree [dataSource]="dataSource" [treeControl]="treeControl">
<mat-tree-node *matTreeNodeDef="let node" matTreeNodePadding>
<div>
<button mat-icon-button disabled></button> §{{ node.item.paragraph }} {{ node.item.text }}
<button *ngIf="node.root" mat-icon-button (click)="addEmptyRule(node)">
<mat-icon>add</mat-icon>
</button>
</div>
</mat-tree-node>
<mat-tree-node *matTreeNodeDef="let node; when: hasChild" matTreeNodePadding>
<button mat-icon-button matTreeNodeToggle [attr.aria-label]="'Toggle ' + node.text">
<mat-icon class="mat-icon-rtl-mirror">
{{ treeControl.isExpanded(node) ? "expand_more" : "chevron_right" }}
</mat-icon>
</button> §{{ node.item.paragraph }} {{ node.item.text }}
<button *ngIf="node.root" mat-icon-button (click)="addEmptyRule(node)">
<mat-icon>add</mat-icon>
</button>
</mat-tree-node>
<mat-tree-node *matTreeNodeDef="let node; when: hasNoContent" matTreeNodePadding>
<button mat-icon-button disabled></button>
<span>§{{ node.item.paragraph }}</span>
<mat-form-field class="new-node-input">
<mat-label>New Rule</mat-label>
<input [formControl]="text" matInput #itemValue placeholder="text" />
</mat-form-field>
<button mat-button (click)="addNode(node, itemValue.value)">Save</button>
</mat-tree-node>
</mat-tree>
<button mat-icon-button (click)="addEmptyRule()">
<mat-icon>add</mat-icon>
</button>
</mat-card>

+ 20
- 0
src/app/admin/create-rule/create-rule.component.scss View File

@@ -0,0 +1,20 @@
mat-card {
margin: 15px;
color: white;
}

mat-tree-node {
color: white;
}

mat-tree {
background-color: transparent;
}

mat-label {
color: white;
}

.new-node-input {
padding-left: 2vw;
}

+ 92
- 1
src/app/admin/create-rule/create-rule.component.ts View File

@@ -1,4 +1,11 @@
import { FlatTreeControl } from '@angular/cdk/tree';
import { Component, OnInit } from '@angular/core';
import { FormControl } from '@angular/forms';
import { MatTreeFlatDataSource, MatTreeFlattener } from '@angular/material/tree';
import { Rule } from '../../../../api/model/rule';
import { DynamicDatabase } from './class/dynamic-database';
import { RuleFlatNode } from './class/rule-flat-node';


@Component({
selector: 'app-create-rule',
@@ -7,9 +14,93 @@ import { Component, OnInit } from '@angular/core';
})
export class CreateRuleComponent implements OnInit {

constructor() { }
flatNodeMap = new Map<RuleFlatNode, Rule>();
nestedNodeMap = new Map<Rule, RuleFlatNode>();
text = new FormControl('');


private _transformer = (rule: Rule, level: number) => {
const existingNode = this.nestedNodeMap.get(rule);
const flatNode = existingNode && existingNode.item.id === rule.id
? existingNode
: new RuleFlatNode(rule, level, !!rule.subRule?.length, false, level === 0)
this.flatNodeMap.set(flatNode, rule);
// dirty hack to expand node with new child node
const lastChildNode = !!rule.subRule ? rule.subRule[rule.subRule.length - 1] : undefined;
if (typeof lastChildNode !== 'undefined') {
if (flatNode.expandable) {
this.treeControl.expand(flatNode);
}
}
return flatNode;
}

treeControl: FlatTreeControl<RuleFlatNode>;
treeFlattener: MatTreeFlattener<Rule, RuleFlatNode>;
dataSource: MatTreeFlatDataSource<Rule, RuleFlatNode>;
_database: DynamicDatabase;

constructor(database: DynamicDatabase) {
this._database = database;
this.treeControl = new FlatTreeControl<RuleFlatNode>(this.getLevel, this.isExpandable);
this.treeFlattener = new MatTreeFlattener<Rule, RuleFlatNode, RuleFlatNode>(
this._transformer, this.getLevel,
this.isExpandable, this.getChildren
);
this.dataSource = new MatTreeFlatDataSource(this.treeControl, this.treeFlattener);
this._database.dataChange.subscribe(data => {
this.dataSource.data = data;
});
this._database.childAdded.subscribe(parent => this.treeControl.expand(this.nestedNodeMap.get(parent)))
}

ngOnInit(): void {
}

getLevel = (node: RuleFlatNode) => node.level;
isExpandable = (node: RuleFlatNode) => node.expandable;
getChildren = (node: Rule): Rule[] => node.subRule;
hasChild = (_: number, node: RuleFlatNode) => node.expandable;
hasNoContent = (_: number, _nodeData: RuleFlatNode) => _nodeData.item.text === '';
hasContent = (_nodeData: RuleFlatNode) => _nodeData.item.text !== "";


addEmptyRule(parentRuleNode?: RuleFlatNode) {
const parentRule = this.flatNodeMap.get(parentRuleNode);
if (typeof parentRule === 'undefined') {
this._database.insertEmptyParent();
return;
}
this._database.insertEmptyChild(parentRule);
}

getParentNode(node: RuleFlatNode): RuleFlatNode | null {
const currentLevel = this.getLevel(node);

if (currentLevel < 1) {
return null;
}

const startIndex = this.treeControl.dataNodes.indexOf(node) - 1;

for (let i = startIndex; i >= 0; i--) {
const currentNode = this.treeControl.dataNodes[i];

if (this.getLevel(currentNode) < currentLevel) {
return currentNode;
}
}
return null;
}

async addNode(newNode: RuleFlatNode, text: string) {
const newRule = this.flatNodeMap.get(newNode);
newRule.text = text;
if (!!newRule.parentRule) {
const parentRule = this._database.getParentById(newRule.parentRule.id)
this._database.insertChild(parentRule, newRule)
}
this._database.insertParent(newRule)
}

}

+ 19
- 1
src/app/app-routing.module.ts View File

@@ -1,16 +1,34 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { AuthGuard } from './auth/guard/auth.guard';
import { CreateRuleComponent } from './admin/create-rule/create-rule.component';
import { LoginComponent } from './auth/login/login.component';
import { GridComponent } from './grid/grid.component';
import { RulesComponent } from './rules/rules/rules.component';

const routes: Routes = [
{
path: '',
component: RulesComponent
},
{
path: 'login',
component: LoginComponent
},
{
path: 'admin',
canActivate: [AuthGuard],
children: [
{
path: 'create',
component: CreateRuleComponent
}
]
}
];

@NgModule({
imports: [RouterModule.forRoot(routes)],
imports: [RouterModule.forRoot(routes, { enableTracing: true })],
exports: [RouterModule]
})
export class AppRoutingModule { }

+ 12
- 2
src/app/app.module.ts View File

@@ -17,6 +17,11 @@ import { MatToolbarModule } from '@angular/material/toolbar';
import { MatSidenavModule } from '@angular/material/sidenav';
import { MatListModule } from '@angular/material/list';
import { RuleListComponent } from './rules/rule-list/rule-list.component';
import { AdminModule } from './admin/admin.module';
import { AuthModule } from './auth/auth.module';
import { HTTP_INTERCEPTORS } from '@angular/common/http';
import { AuthInterceptor } from './auth/interceptor/auth.interceptor';
import { ErrorInterceptor } from './auth/interceptor/error.interceptor';
@NgModule({
declarations: [
DashboardComponent,
@@ -37,9 +42,14 @@ import { RuleListComponent } from './rules/rule-list/rule-list.component';
FooterModule,
MatToolbarModule,
MatSidenavModule,
MatListModule
MatListModule,
AdminModule,
AuthModule,
],
providers: [
{ provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true },
{ provide: HTTP_INTERCEPTORS, useClass: ErrorInterceptor, multi: true }
],
providers: [],
bootstrap: [MainComponent]
})
export class AppModule { }

+ 36
- 0
src/app/auth/auth.module.ts View File

@@ -0,0 +1,36 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { AuthGuard } from './guard/auth.guard';
import { AuthService } from './service/auth.service';
import { HttpClientModule } from '@angular/common/http';
import { LoginComponent } from './login/login.component';
import { MatCardModule } from '@angular/material/card';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatIconModule } from '@angular/material/icon';
import { MatButtonModule } from '@angular/material/button';
import {MatFormFieldModule} from '@angular/material/form-field';
import {MatInputModule} from '@angular/material/input';



@NgModule({
declarations: [
LoginComponent
],
imports: [
CommonModule,
MatCardModule,
MatButtonModule,
HttpClientModule,
MatFormFieldModule,
MatInputModule,
MatIconModule,
FormsModule,
ReactiveFormsModule,
],
providers: [
AuthGuard,
AuthService,
],
})
export class AuthModule { }

+ 16
- 0
src/app/auth/guard/auth.guard.spec.ts View File

@@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';

import { AuthGuard } from './auth.guard';

describe('AuthGuard', () => {
let guard: AuthGuard;

beforeEach(() => {
TestBed.configureTestingModule({});
guard = TestBed.inject(AuthGuard);
});

it('should be created', () => {
expect(guard).toBeTruthy();
});
});

+ 27
- 0
src/app/auth/guard/auth.guard.ts View File

@@ -0,0 +1,27 @@
import { Injectable } from '@angular/core';
import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, UrlTree, Router } from '@angular/router';
import { Observable } from 'rxjs';
import { AuthService } from '../service/auth.service';

@Injectable({
providedIn: 'root'
})
export class AuthGuard implements CanActivate {
constructor(
private router: Router,
private authenticationService: AuthService
) { }

canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
const user = this.authenticationService.user;
if (user) {
// logged in so return true
return true;
}

// not logged in so redirect to login page with the return url
this.router.navigate(['/login'], { queryParams: { returnUrl: state.url } });
return false;
}

}

+ 16
- 0
src/app/auth/interceptor/auth.interceptor.spec.ts View File

@@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';

import { AuthInterceptor } from './auth.interceptor';

describe('AuthInterceptor', () => {
beforeEach(() => TestBed.configureTestingModule({
providers: [
AuthInterceptor
]
}));

it('should be created', () => {
const interceptor: AuthInterceptor = TestBed.inject(AuthInterceptor);
expect(interceptor).toBeTruthy();
});
});

+ 31
- 0
src/app/auth/interceptor/auth.interceptor.ts View File

@@ -0,0 +1,31 @@
import { Injectable } from '@angular/core';
import {
HttpRequest,
HttpHandler,
HttpEvent,
HttpInterceptor
} from '@angular/common/http';
import { Observable } from 'rxjs';
import { AuthService } from '../service/auth.service';
import { environment } from '../../../environments/environment';

@Injectable()
export class AuthInterceptor implements HttpInterceptor {
constructor(private authenticationService: AuthService) { }

intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
// add header with basic auth credentials if user is logged in and request is to the api url
const user = this.authenticationService.user;
const isLoggedIn = user && user.username && user.password;
const isAdminUrl = request.url.startsWith(environment.apiUrl);
if (isLoggedIn && isAdminUrl) {
request = request.clone({
setHeaders: {
Authorization: this.authenticationService.getUserToken()
}
});
}

return next.handle(request);
}
}

+ 16
- 0
src/app/auth/interceptor/error.interceptor.spec.ts View File

@@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';

import { ErrorInterceptor } from './error.interceptor';

describe('ErrorInterceptor', () => {
beforeEach(() => TestBed.configureTestingModule({
providers: [
ErrorInterceptor
]
}));

it('should be created', () => {
const interceptor: ErrorInterceptor = TestBed.inject(ErrorInterceptor);
expect(interceptor).toBeTruthy();
});
});

+ 29
- 0
src/app/auth/interceptor/error.interceptor.ts View File

@@ -0,0 +1,29 @@
import { Injectable } from '@angular/core';
import {
HttpRequest,
HttpHandler,
HttpEvent,
HttpInterceptor
} from '@angular/common/http';
import { Observable } from 'rxjs';
import { AuthService } from '../service/auth.service';
import { catchError } from 'rxjs/operators';
import { throwError } from 'rxjs';

@Injectable()
export class ErrorInterceptor implements HttpInterceptor {

constructor(private authenticationService: AuthService) { }

intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
return next.handle(request).pipe(catchError(err => {
if (err.status === 401) {
// auto logout if 401 response returned from api
this.authenticationService.logout();
}

const error = err.error.message || err.statusText;
return throwError(error);
}))
}
}

+ 20
- 0
src/app/auth/login/login.component.html View File

@@ -0,0 +1,20 @@
<mat-card class="wood-card">
<mat-card-title> Login </mat-card-title>
<div>
<p>
<mat-form-field appearance="standard">
<mat-label>Username</mat-label>
<input [formControl]="username" matInput type="text" />
</mat-form-field>
</p>
<p>
<mat-form-field appearance="standard">
<mat-label>Password</mat-label>
<input [formControl]="password" matInput type="password" />
</mat-form-field>
</p>
<p>
<button (click)="onSubmit()" mat-button>Bestätigen</button>
</p>
</div>
</mat-card>

+ 11
- 0
src/app/auth/login/login.component.scss View File

@@ -0,0 +1,11 @@
mat-card {
color: white;
margin: 10vw;
padding-left: 10vw;
padding-right: 10vw;
padding-top: 10vh;
padding-bottom: 10vh;
}
mat-label {
color: white;
}

+ 25
- 0
src/app/auth/login/login.component.spec.ts View File

@@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';

import { LoginComponent } from './login.component';

describe('LoginComponent', () => {
let component: LoginComponent;
let fixture: ComponentFixture<LoginComponent>;

beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ LoginComponent ]
})
.compileComponents();
});

beforeEach(() => {
fixture = TestBed.createComponent(LoginComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

it('should create', () => {
expect(component).toBeTruthy();
});
});

+ 35
- 0
src/app/auth/login/login.component.ts View File

@@ -0,0 +1,35 @@
import { Component, OnInit } from '@angular/core';
import { FormControl } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import { User } from '../../class/user';
import { AuthService } from '../service/auth.service';

@Component({
selector: 'app-login',
templateUrl: './login.component.html',
styleUrls: ['./login.component.scss']
})
export class LoginComponent implements OnInit {
username = new FormControl('');
password = new FormControl('');
constructor(protected authService: AuthService, private route: ActivatedRoute, protected router: Router) { }

onSubmit() {
let user = new User();
user.username = this.username.value;
user.password = this.password.value;
this.authService.user = user;
const redirectUrl = this.route.queryParams
.subscribe(value => {
if (value?.returnUrl) {
this.router.navigate([value.returnUrl]);
return;
}
this.router.navigate(['']);
})
}
ngOnInit(): void {

}

}

+ 16
- 0
src/app/auth/service/auth.service.spec.ts View File

@@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';

import { AuthService } from './auth.service';

describe('AuthService', () => {
let service: AuthService;

beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(AuthService);
});

it('should be created', () => {
expect(service).toBeTruthy();
});
});

+ 35
- 0
src/app/auth/service/auth.service.ts View File

@@ -0,0 +1,35 @@
import { Injectable } from '@angular/core';
import { User } from '../../class/user';
import {encode, decode} from 'base-64';
@Injectable({
providedIn: 'root'
})
export class AuthService {
set user (val: User) {
localStorage.setItem("user", encode(`${val.username}:${val.password}`));
}
get user () {
if (localStorage.getItem("user")) {
const userValues = decode(localStorage.getItem("user")).split(':');
const user: User = {username: userValues[0], password: userValues[1]}
return user;
}
return null;
}

getUserToken () {
if (localStorage.getItem("user")) {
return `Basic ${localStorage.getItem("user")}`;
}
return ''
}

logout() {
if (localStorage.getItem("user")) {
localStorage.removeItem("user");
}
}
constructor() { }


}

+ 7
- 0
src/app/class/user.spec.ts View File

@@ -0,0 +1,7 @@
import { User } from './user';

describe('User', () => {
it('should create an instance', () => {
expect(new User()).toBeTruthy();
});
});

+ 4
- 0
src/app/class/user.ts View File

@@ -0,0 +1,4 @@
export class User {
username: string;
password: string;
}

+ 1
- 1
src/app/main/main.component.html View File

@@ -6,7 +6,7 @@
>
</mat-toolbar>
<main class="content">
<app-grid></app-grid>
<router-outlet></router-outlet>
</main>
</mat-sidenav-content>
</mat-sidenav-container>

+ 3
- 0
src/app/rules/rule-list/rule-list.component.scss View File

@@ -0,0 +1,3 @@
div {
color: white;
}

+ 1
- 1
src/app/rules/rules/rules.component.html View File

@@ -1,4 +1,4 @@
<mat-card class="dashboard-card rule-card">
<mat-card class="dashboard-card wood-card">
<mat-card-header>
<mat-card-title class="title mat-title">
<mat-icon mat-list-icon class="logo">gavel</mat-icon> Regeln

+ 3
- 19
src/app/rules/rules/rules.component.scss View File

@@ -1,21 +1,5 @@

:host ::ng-deep .mat-list-base .mat-list-item .mat-list-item-content {
justify-content: left;
text-align: left;
}

div {
color: white;
}

.rule-card {
background-image: url('/assets/wood.jpeg');
background-repeat: no-repeat;
background-size: cover;
mat-card {
margin: 15px;
width: 90%;
color: white;
position: absolute;
top: 15px;
left: 15px;
right: 15px;
bottom: 15px;
}

+ 2
- 1
src/environments/environment.prod.ts View File

@@ -1,3 +1,4 @@
export const environment = {
production: true
production: true,
apiUrl: 'api.hoppe.ziermach.de',
};

+ 2
- 1
src/environments/environment.ts View File

@@ -3,7 +3,8 @@
// The list of file replacements can be found in `angular.json`.

export const environment = {
production: false
production: false,
apiUrl: 'http://localhost:3000'
};

/*

+ 20
- 1
src/styles.scss View File

@@ -1,5 +1,7 @@
/* You can add global styles to this file, and also import other style files */

@import '~@angular/material/prebuilt-themes/deeppurple-amber.css';

html,
body {
height: 100vh !important;
@@ -13,7 +15,6 @@ body {
}

.logo {

padding-right: 1vw !important;
}

@@ -21,3 +22,21 @@ body {
display: flex !important;
align-items: center !important;
}


:host ::ng-deep .mat-list-base .mat-list-item .mat-list-item-content {
justify-content: left;
text-align: left;
}

.wood-card {
background-image: url('/assets/wood.jpeg');
background-repeat: no-repeat;
background-size: cover;
color: white;
position: absolute;
top: 15px;
left: 15px;
right: 15px;
bottom: 15px;
}

Loading…
Cancel
Save