From 56362da7241f9bca48231f7c023fe86f0818a2d9 Mon Sep 17 00:00:00 2001 From: _Bastler <_Bastler@bstly.de> Date: Thu, 8 Apr 2021 09:45:50 +0200 Subject: [PATCH] add domain feature --- src/app/app-routing.module.ts | 4 +- src/app/app.module.ts | 2 + src/app/pages/account/account.component.html | 8 +- src/app/pages/account/account.component.ts | 6 +- .../account/domains/domains.component.html | 59 ++++++++ .../account/domains/domains.component.scss | 31 ++++ .../account/domains/domains.component.spec.ts | 25 ++++ .../account/domains/domains.component.ts | 132 ++++++++++++++++++ src/app/services/userdomain.service.ts | 30 ++++ src/assets/i18n/de-informal.json | 30 +++- src/assets/i18n/en.json | 30 +++- 11 files changed, 342 insertions(+), 15 deletions(-) create mode 100644 src/app/pages/account/domains/domains.component.html create mode 100644 src/app/pages/account/domains/domains.component.scss create mode 100644 src/app/pages/account/domains/domains.component.spec.ts create mode 100644 src/app/pages/account/domains/domains.component.ts create mode 100644 src/app/services/userdomain.service.ts diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index 16da065..dc1c834 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -21,6 +21,7 @@ import {UnavailableComponent} from './pages/unavailable/unavailable.component'; import {NotfoundComponent} from './pages/notfound/notfound.component'; import {UserComponent} from './pages/user/user.component' import {AliasesComponent} from './pages/account/aliases/aliases.component'; +import {DomainsComponent} from './pages/account/domains/domains.component'; const routes: Routes = [ {path: '', redirectTo: "/services", pathMatch: 'full'}, @@ -41,7 +42,8 @@ const routes: Routes = [ {path: 'profile', component: ProfileComponent, canActivate: [AuthenticatedGuard]}, {path: 'security', component: SecurityComponent, canActivate: [AuthenticatedGuard]}, {path: 'voucher', component: VoucherComponent, canActivate: [AuthenticatedGuard]}, - {path: 'aliases', component: AliasesComponent, canActivate: [AuthenticatedGuard]} + {path: 'aliases', component: AliasesComponent, canActivate: [AuthenticatedGuard]}, + {path: 'domains', component: DomainsComponent, canActivate: [AuthenticatedGuard]} ] }, {path: 'register', component: RegisterComponent, canActivate: [AnonymousGuard]}, diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 05bd79b..5f57fef 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -26,6 +26,7 @@ import {VoucherComponent} from './pages/account/voucher/voucher.component'; import {VoucherDialog} from './pages/account/voucher/voucher.component'; import {InfoComponent} from './pages/account/info/info.component'; import {AliasesComponent} from './pages/account/aliases/aliases.component'; +import {DomainsComponent} from './pages/account/domains/domains.component'; import {ProfileComponent} from './pages/account/profile/profile.component'; import {PasswordComponent} from './pages/password/password.component'; import {PasswordResetComponent} from './pages/password-reset/password-reset.component'; @@ -77,6 +78,7 @@ export class XhrInterceptor implements HttpInterceptor { VoucherDialog, InfoComponent, AliasesComponent, + DomainsComponent, ProfileComponent, PasswordComponent, PasswordResetComponent, diff --git a/src/app/pages/account/account.component.html b/src/app/pages/account/account.component.html index 7537e54..c56c37e 100644 --- a/src/app/pages/account/account.component.html +++ b/src/app/pages/account/account.component.html @@ -6,7 +6,13 @@ {{'profile' | i18n}} {{'security' | i18n}} {{'vouchers' | i18n}} - {{'user.aliases' | i18n}} + {{'user.aliases' | i18n}} + {{'user.domains' | i18n}} + + + {{'account.advanced' | i18n}} + + \ No newline at end of file diff --git a/src/app/pages/account/account.component.ts b/src/app/pages/account/account.component.ts index 32c23bf..7175b5f 100644 --- a/src/app/pages/account/account.component.ts +++ b/src/app/pages/account/account.component.ts @@ -1,6 +1,6 @@ -import { Component, OnInit } from '@angular/core'; +import {Component, OnInit} from '@angular/core'; -import { AuthService } from './../../services/auth.service'; +import {AuthService} from './../../services/auth.service'; @Component({ selector: 'app-account', @@ -9,7 +9,7 @@ import { AuthService } from './../../services/auth.service'; }) export class AccountComponent implements OnInit { - + advancedView: boolean = false; auth; constructor(private authService: AuthService) { diff --git a/src/app/pages/account/domains/domains.component.html b/src/app/pages/account/domains/domains.component.html new file mode 100644 index 0000000..249c7b5 --- /dev/null +++ b/src/app/pages/account/domains/domains.component.html @@ -0,0 +1,59 @@ +

{{'user.domains' | i18n}}

+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
{{'user.domains.domain' | i18n}} {{ domain.domain}} {{'user.domains.secret' | i18n}} + {{ domain.secret}} + {{'user.domains.validated' | i18n}} + + {{'user.domains.delete' | i18n}} + + delete + +
+ +
+ + +

{{'user.domains.info' | i18n}}

+ + + + {{'user.domains.error' | i18n}} + + +
+ + + +
+ +
\ No newline at end of file diff --git a/src/app/pages/account/domains/domains.component.scss b/src/app/pages/account/domains/domains.component.scss new file mode 100644 index 0000000..864fb8b --- /dev/null +++ b/src/app/pages/account/domains/domains.component.scss @@ -0,0 +1,31 @@ +mat-form-field { + display: block; +} + +.mat-header-cell, +.mat-cell { + &.text-right { + text-align: right; + } +} + +.align-right { + display: flex; + padding: 21px 0; + justify-content: flex-end; +} + +.secret { + width: 200px; + max-width: 200px; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; +} + +.secret:hover { + cursor: pointer; + max-width: 100%; + width: 100%; + margin-right: 25px; +} diff --git a/src/app/pages/account/domains/domains.component.spec.ts b/src/app/pages/account/domains/domains.component.spec.ts new file mode 100644 index 0000000..9df4628 --- /dev/null +++ b/src/app/pages/account/domains/domains.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { DomainsComponent } from './domains.component'; + +describe('DomainsComponent', () => { + let component: DomainsComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ DomainsComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(DomainsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/pages/account/domains/domains.component.ts b/src/app/pages/account/domains/domains.component.ts new file mode 100644 index 0000000..2060639 --- /dev/null +++ b/src/app/pages/account/domains/domains.component.ts @@ -0,0 +1,132 @@ +import {Component, OnInit, ViewChild} from '@angular/core'; +import {I18nService} from '../../../services/i18n.service'; +import {Sort} from '@angular/material/sort'; +import {UserDomainService} from '../../../services/userdomain.service'; +import {FormBuilder, FormGroup, Validators, NgForm} from '@angular/forms'; +import {MatDialog} from '@angular/material/dialog'; +import {ConfirmDialog} from '../../../ui/confirm/confirm.component'; +import {MatSnackBar} from '@angular/material/snack-bar'; + +@Component({ + selector: 'app-account-domains', + templateUrl: './domains.component.html', + styleUrls: ['./domains.component.scss'] +}) +export class DomainsComponent implements OnInit { + + form: FormGroup; + @ViewChild('formDirective') private formDirective: NgForm; + domains: any[] = []; + domain: any = { + visibility: "PRIVATE" + }; + success: boolean; + working: boolean; + + domainsColumns = ["domain", "secret", "validated", "delete"]; + visibilities = ["PRIVATE", "PROTECTED", "PUBLIC"]; + + constructor( + private formBuilder: FormBuilder, + private userDomainService: UserDomainService, + private i18n: I18nService, + private snackBar: MatSnackBar, + public dialog: MatDialog) {} + + ngOnInit(): void { + this.form = this.formBuilder.group({ + domain: ['', Validators.required], + // visibility: ['', Validators.required], + }); + + this.update(); + } + + create(): void { + this.working = true; + this.userDomainService.create(this.domain).subscribe(response => { + this.update(); + this.formDirective.resetForm(); + this.domain = { + visibility: "PRIVATE" + }; + this.working = false; + }, (error) => { + this.working = false; + if(error.status == 409) { + let errors = {}; + for(let code of error.error) { + errors[code.field] = errors[code.field] || {}; + errors[code.field][code.code] = true; + } + + for(let code in errors) { + this.form.get(code).setErrors(errors[code]); + } + } + }) + } + + updateDomain(domain) { + this.userDomainService.update(domain).subscribe((response: any) => { + domain = response; + }) + } + + update() { + this.userDomainService.get().subscribe((data: any) => { + this.domains = data; + }) + } + + confirmDelete(domain) { + const dialogRef = this.dialog.open(ConfirmDialog, { + data: { + 'label': 'user.domains.confirmDelete', + 'args': [domain.domain] + } + }) + + dialogRef.afterClosed().subscribe(result => { + if(result) { + this.userDomainService.delete(domain.id).subscribe((result: any) => { + this.update(); + }) + } + }); + } + + copySecret(domain: any) { + const selBox = document.createElement('textarea'); + selBox.value = domain.secret; + document.body.appendChild(selBox); + selBox.focus(); + selBox.select(); + document.execCommand('copy'); + document.body.removeChild(selBox); + this.snackBar.open(this.i18n.get("user.domains.secret.copied", []), this.i18n.get("close", []), { + duration: 3000 + }); + } + + sortData(sort: Sort) { + const data = this.domains.slice(); + if(!sort.active || sort.direction === '') { + this.domains = data; + return; + } + + this.domains = data.sort((a, b) => { + const isAsc = sort.direction === 'asc'; + switch(sort.active) { + case 'domain': return this.compare(a.domain, b.domain, isAsc); + case 'validated': return this.compare(a.validated, b.validated, isAsc); + default: return 0; + } + }); + } + + compare(a: number | string | String, b: number | string | String, isAsc: boolean) { + return (a < b ? -1 : 1) * (isAsc ? 1 : -1); + } +} diff --git a/src/app/services/userdomain.service.ts b/src/app/services/userdomain.service.ts new file mode 100644 index 0000000..3f13b67 --- /dev/null +++ b/src/app/services/userdomain.service.ts @@ -0,0 +1,30 @@ +import {Injectable} from '@angular/core'; +import {HttpClient} from '@angular/common/http'; + +import {environment} from '../../environments/environment'; + +@Injectable({ + providedIn: 'root', +}) +export class UserDomainService { + + constructor(private http: HttpClient) { + } + + get() { + return this.http.get(environment.apiUrl + "/users/domains"); + } + + create(alias) { + return this.http.post(environment.apiUrl + "/users/domains", alias); + } + + update(alias) { + return this.http.patch(environment.apiUrl + "/users/domains", alias); + } + + delete(id) { + return this.http.delete(environment.apiUrl + "/users/domains/" + id); + } + +} \ No newline at end of file diff --git a/src/assets/i18n/de-informal.json b/src/assets/i18n/de-informal.json index d66526e..f05576f 100644 --- a/src/assets/i18n/de-informal.json +++ b/src/assets/i18n/de-informal.json @@ -1,5 +1,10 @@ { - "account": "Account", + "account": { + ".": "Account", + "advanced": { + ".": "Erweitert" + } + }, "cancel": "Abbrechen", "close": "Schließen", "confirm": "Bestätigen", @@ -315,10 +320,25 @@ "create": "Alternativen Namen anlegen", "delete": "Löschen", "error": "Dieser alternative Name ist leider nicht zulässig.", - "info": "Du kannst hier alternative Namen erstellen. Die Anzahl wird über eine Quota begrenzt.", + "info": "Du kannst hier alternative Namen anlegen. Die Anzahl wird über eine Quota begrenzt.", "left": "Du kannst noch {0} alternative(n) Namen anlegen.", "noQuota": "Deine Quota für alternative Namen ist leider aufgebraucht." }, + "domains": { + ".": "Domains", + "domain": "Domain", + "confirmDelete": "Möchtest du wirklich die Domain '{0}' löschen?", + "create": "Domain hinzufügen", + "delete": "Löschen", + "error": "Diese Domain ist leider nicht zulässig.", + "info": "Du kannst hier Domains hinzufügen.", + "secret": { + ".": "Secret", + "copy": "Secret in die Zwischenablage kopieren", + "copied": "In die Zwischenablage kopiert" + }, + "validated": "Validiert" + }, "unavailable": { ".": "Zugriff verweigert", "text": "Dieser Benutzer existiert nicht oder du hast keine Berechtigungen, um auf das Profil zuzugreifen." @@ -333,15 +353,15 @@ ".": "Sichtbarkeit", "PRIVATE": { ".": "Privat", - "icon" : "lock" + "icon": "lock" }, "PROTECTED": { ".": "Geschützt", - "icon" : "shield" + "icon": "shield" }, "PUBLIC": { ".": "Öffentlich", - "icon" : "public" + "icon": "public" } }, "voucher": { diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index bcc4310..2b827b6 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -1,5 +1,10 @@ { - "account": "Account", + "account": { + ".": "Account", + "advanced": { + ".": "Advanced" + } + }, "cancel": "Cancel", "close": "Close", "confirm": "Confirm", @@ -230,7 +235,7 @@ "text": "You can add additional aliases besides your username.", "title": "Aliases" }, - "empty" : "You have insufficient permissions to use any services.", + "empty": "You have insufficient permissions to use any services.", "gitea": { "icon": "code", "subtitle": "Git-Repositories", @@ -319,6 +324,21 @@ "left": "You have {0} Alias(es) left.", "noQuota": "Your quota for Aliases is depleted." }, + "domains": { + ".": "Domains", + "domain": "Domain", + "confirmDelete": "Are you sure you want to delete your domain '{0}'?", + "create": "Add Domain", + "delete": "Delete", + "error": "This domain is invalid.", + "info": "You can add Domains here.", + "secret": { + ".": "Secret", + "copy": "Copy secret to clipboard", + "copied": "Copied to clipboard" + }, + "validated": "Validated" + }, "unavailable": { ".": "Access denied", "text": "The provided user does not exists or you have insufficient privileges to access profile." @@ -333,15 +353,15 @@ ".": "Visibility", "PRIVATE": { ".": "Private", - "icon" : "lock" + "icon": "lock" }, "PROTECTED": { ".": "Protected", - "icon" : "shield" + "icon": "shield" }, "PUBLIC": { ".": "Public", - "icon" : "public" + "icon": "public" } }, "voucher": {