added jitsi api + improvements

This commit is contained in:
_Bastler 2021-04-16 08:57:53 +02:00
parent 56362da724
commit ffe9e6f5ab
31 changed files with 4682 additions and 3202 deletions

View File

@ -43,7 +43,6 @@
"optimization": true, "optimization": true,
"outputHashing": "all", "outputHashing": "all",
"sourceMap": false, "sourceMap": false,
"extractCss": true,
"namedChunks": false, "namedChunks": false,
"extractLicenses": true, "extractLicenses": true,
"vendorChunk": false, "vendorChunk": false,

7051
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -11,39 +11,40 @@
}, },
"private": true, "private": true,
"dependencies": { "dependencies": {
"@angular/animations": "~10.1.5", "@angular-material-components/datetime-picker": "^5.1.0",
"@angular/cdk": "^10.2.4", "@angular/animations": "~11.2.10",
"@angular/common": "~10.1.5", "@angular/cdk": "^11.2.9",
"@angular/compiler": "~10.1.5", "@angular/common": "~11.2.10",
"@angular/core": "~10.1.5", "@angular/compiler": "~11.2.10",
"@angular/core": "~11.2.10",
"@angular/flex-layout": "^11.0.0-beta.33", "@angular/flex-layout": "^11.0.0-beta.33",
"@angular/forms": "~10.1.5", "@angular/forms": "~11.2.10",
"@angular/material": "^10.2.4", "@angular/material": "^11.2.9",
"@angular/material-moment-adapter": "^10.2.7", "@angular/material-moment-adapter": "^11.2.9",
"@angular/platform-browser": "~10.1.5", "@angular/platform-browser": "~11.2.10",
"@angular/platform-browser-dynamic": "~10.1.5", "@angular/platform-browser-dynamic": "~11.2.10",
"@angular/router": "~10.1.5", "@angular/router": "~11.2.10",
"angularx-qrcode": "^10.0.11", "angularx-qrcode": "^10.0.11",
"moment": "^2.29.1", "moment": "^2.29.1",
"openpgp": "^4.10.8", "openpgp": "^4.10.8",
"rxjs": "~6.6.0", "rxjs": "~6.6.7",
"tslib": "^2.0.0", "tslib": "^2.0.0",
"unique-names-generator": "^4.3.1", "unique-names-generator": "^4.3.1",
"zone.js": "~0.10.2" "zone.js": "~0.10.2"
}, },
"devDependencies": { "devDependencies": {
"@angular-devkit/build-angular": "^0.1002.0", "@angular-devkit/build-angular": "^0.1102.9",
"@angular/cli": "~10.1.6", "@angular/cli": "~11.2.9",
"@angular/compiler-cli": "~10.1.5", "@angular/compiler-cli": "~11.2.10",
"@angular/localize": "^10.1.5", "@angular/localize": "^11.2.10",
"@types/jasmine": "~3.5.0", "@types/jasmine": "~3.6.0",
"@types/jasminewd2": "~2.0.3", "@types/jasminewd2": "~2.0.3",
"@types/node": "^12.12.67", "@types/node": "^12.12.67",
"@types/openpgp": "^4.4.14", "@types/openpgp": "^4.4.14",
"codelyzer": "^6.0.0", "codelyzer": "^6.0.0",
"jasmine-core": "~3.6.0", "jasmine-core": "~3.6.0",
"jasmine-spec-reporter": "~5.0.0", "jasmine-spec-reporter": "~5.0.0",
"karma": "~5.0.0", "karma": "~6.3.2",
"karma-chrome-launcher": "~3.1.0", "karma-chrome-launcher": "~3.1.0",
"karma-coverage-istanbul-reporter": "~3.0.2", "karma-coverage-istanbul-reporter": "~3.0.2",
"karma-jasmine": "~4.0.0", "karma-jasmine": "~4.0.0",

View File

@ -20,6 +20,7 @@ import {SecurityComponent} from './pages/account/security/security.component';
import {UnavailableComponent} from './pages/unavailable/unavailable.component'; import {UnavailableComponent} from './pages/unavailable/unavailable.component';
import {NotfoundComponent} from './pages/notfound/notfound.component'; import {NotfoundComponent} from './pages/notfound/notfound.component';
import {UserComponent} from './pages/user/user.component' import {UserComponent} from './pages/user/user.component'
import {JitsiComponent} from './pages/jitsi/jitsi.component'
import {AliasesComponent} from './pages/account/aliases/aliases.component'; import {AliasesComponent} from './pages/account/aliases/aliases.component';
import {DomainsComponent} from './pages/account/domains/domains.component'; import {DomainsComponent} from './pages/account/domains/domains.component';
@ -48,13 +49,14 @@ const routes: Routes = [
}, },
{path: 'register', component: RegisterComponent, canActivate: [AnonymousGuard]}, {path: 'register', component: RegisterComponent, canActivate: [AnonymousGuard]},
{path: 'tokens', component: TokensComponent, canActivate: [AuthGuard]}, {path: 'tokens', component: TokensComponent, canActivate: [AuthGuard]},
{path: 'jitsi', component: JitsiComponent, canActivate: [AuthenticatedGuard]},
{path: 'unavailable', component: UnavailableComponent}, {path: 'unavailable', component: UnavailableComponent},
{path: 'p/:username', component: UserComponent, canActivate: [AuthUpdateGuard]}, {path: 'p/:username', component: UserComponent, canActivate: [AuthUpdateGuard]},
{path: '**', component: NotfoundComponent, pathMatch: 'full', canActivate: [AuthUpdateGuard]}, {path: '**', component: NotfoundComponent, pathMatch: 'full', canActivate: [AuthUpdateGuard]},
]; ];
@NgModule({ @NgModule({
imports: [RouterModule.forRoot(routes, {onSameUrlNavigation: 'reload'})], imports: [RouterModule.forRoot(routes, { onSameUrlNavigation: 'reload', relativeLinkResolution: 'legacy' })],
exports: [RouterModule] exports: [RouterModule]
}) })
export class AppRoutingModule {} export class AppRoutingModule {}

View File

@ -19,6 +19,7 @@ export class AppComponent {
darkTheme = "false"; darkTheme = "false";
title = 'we.bstly'; title = 'we.bstly';
currentLocale: String; currentLocale: String;
datetimeformat: String;
locales; locales;
auth; auth;
@ -34,7 +35,7 @@ export class AppComponent {
} }
ngOnInit() { ngOnInit() {
this.datetimeformat = this.i18n.get('format.datetime', []);
this.currentLocale = this.i18n.getLocale(); this.currentLocale = this.i18n.getLocale();
this.locales = this.i18n.getLocales(); this.locales = this.i18n.getLocales();
this.authService.auth.subscribe(data => { this.authService.auth.subscribe(data => {

View File

@ -37,6 +37,7 @@ import {NotfoundComponent} from './pages/notfound/notfound.component';
import {HtmlComponent} from './utils/html/html.component'; import {HtmlComponent} from './utils/html/html.component';
import {ConfirmDialog} from './ui/confirm/confirm.component' import {ConfirmDialog} from './ui/confirm/confirm.component'
import {UserComponent} from './pages/user/user.component' import {UserComponent} from './pages/user/user.component'
import {JitsiComponent, JitsiShareDialog} from './pages/jitsi/jitsi.component'
import {I18nService} from './services/i18n.service'; import {I18nService} from './services/i18n.service';
@ -89,7 +90,8 @@ export class XhrInterceptor implements HttpInterceptor {
NotfoundComponent, NotfoundComponent,
HtmlComponent, HtmlComponent,
ConfirmDialog, ConfirmDialog,
UserComponent UserComponent,
JitsiComponent, JitsiShareDialog
], ],
imports: [ imports: [
BrowserModule, BrowserModule,

View File

@ -29,7 +29,7 @@ export class AuthGuard implements CanActivate {
return this.authService.getAuth().then(response => { return this.authService.getAuth().then(response => {
return true; return true;
}).catch(function(error) { }).catch(function(error) {
return that.router.parseUrl('/unavailable'); return that.router.parseUrl('/unavailable?target=' + state.url);
}); });
} }
} }
@ -44,11 +44,9 @@ export class AuthenticatedGuard implements CanActivate {
const that = this; const that = this;
return this.authService.getAuth().then((data: any) => { return this.authService.getAuth().then((data: any) => {
if(!data.authenticated) { if(!data.authenticated) {
this.router.navigateByUrl('/login'); return that.router.parseUrl('/login?target=' + state.url);
return false;
} }
this.profileService.get(["locale", "darkTheme"]).subscribe((profileFields: any) => { this.profileService.get(["locale", "darkTheme"]).subscribe((profileFields: any) => {
let updateLocale = false; let updateLocale = false;
let darktheme = 'false'; let darktheme = 'false';
@ -76,7 +74,7 @@ export class AuthenticatedGuard implements CanActivate {
return true; return true;
}).catch(function(error) { }).catch(function(error) {
return that.router.parseUrl('/unavailable'); return that.router.parseUrl('/unavailable?target=' + state.url);
}); });
} }
} }
@ -96,7 +94,7 @@ export class AnonymousGuard implements CanActivate {
} }
return true; return true;
}).catch(function(error) { }).catch(function(error) {
return that.router.parseUrl('/unavailable'); return that.router.parseUrl('/unavailable?target=' + state.url);
}); });
} }

View File

@ -1,49 +1,55 @@
import { NgModule } from '@angular/core'; import {NgModule} from '@angular/core';
import { CommonModule } from '@angular/common'; import {CommonModule} from '@angular/common';
// Material Form Controls // Material Form Controls
import { MatAutocompleteModule } from '@angular/material/autocomplete'; import {MatAutocompleteModule} from '@angular/material/autocomplete';
import { MatCheckboxModule } from '@angular/material/checkbox'; import {MatCheckboxModule} from '@angular/material/checkbox';
import { MatDatepickerModule } from '@angular/material/datepicker'; import {MatDatepickerModule} from '@angular/material/datepicker';
import { MatFormFieldModule } from '@angular/material/form-field'; import {MatFormFieldModule} from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input'; import {MatInputModule} from '@angular/material/input';
import { MatRadioModule } from '@angular/material/radio'; import {MatRadioModule} from '@angular/material/radio';
import { MatSelectModule } from '@angular/material/select'; import {MatSelectModule} from '@angular/material/select';
import { MatSliderModule } from '@angular/material/slider'; import {MatSliderModule} from '@angular/material/slider';
import { MatSlideToggleModule } from '@angular/material/slide-toggle'; import {MatSlideToggleModule} from '@angular/material/slide-toggle';
// Material Navigation // Material Navigation
import { MatMenuModule } from '@angular/material/menu'; import {MatMenuModule} from '@angular/material/menu';
import { MatSidenavModule } from '@angular/material/sidenav'; import {MatSidenavModule} from '@angular/material/sidenav';
import { MatToolbarModule } from '@angular/material/toolbar'; import {MatToolbarModule} from '@angular/material/toolbar';
// Material Layout // Material Layout
import { MatCardModule } from '@angular/material/card'; import {MatCardModule} from '@angular/material/card';
import { MatDividerModule } from '@angular/material/divider'; import {MatDividerModule} from '@angular/material/divider';
import { MatExpansionModule } from '@angular/material/expansion'; import {MatExpansionModule} from '@angular/material/expansion';
import { MatGridListModule } from '@angular/material/grid-list'; import {MatGridListModule} from '@angular/material/grid-list';
import { MatListModule } from '@angular/material/list'; import {MatListModule} from '@angular/material/list';
import { MatStepperModule } from '@angular/material/stepper'; import {MatStepperModule} from '@angular/material/stepper';
import { MatTabsModule } from '@angular/material/tabs'; import {MatTabsModule} from '@angular/material/tabs';
import { MatTreeModule } from '@angular/material/tree'; import {MatTreeModule} from '@angular/material/tree';
// Material Buttons & Indicators // Material Buttons & Indicators
import { MatButtonModule } from '@angular/material/button'; import {MatButtonModule} from '@angular/material/button';
import { MatButtonToggleModule } from '@angular/material/button-toggle'; import {MatButtonToggleModule} from '@angular/material/button-toggle';
import { MatBadgeModule } from '@angular/material/badge'; import {MatBadgeModule} from '@angular/material/badge';
import { MatChipsModule } from '@angular/material/chips'; import {MatChipsModule} from '@angular/material/chips';
import { MatIconModule } from '@angular/material/icon'; import {MatIconModule} from '@angular/material/icon';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import {MatProgressSpinnerModule} from '@angular/material/progress-spinner';
import { MatProgressBarModule } from '@angular/material/progress-bar'; import {MatProgressBarModule} from '@angular/material/progress-bar';
import { MatRippleModule } from '@angular/material/core'; import {MatRippleModule} from '@angular/material/core';
// Material Popups & Modals // Material Popups & Modals
import { MatBottomSheetModule } from '@angular/material/bottom-sheet'; import {MatBottomSheetModule} from '@angular/material/bottom-sheet';
import { MatDialogModule } from '@angular/material/dialog'; import {MatDialogModule} from '@angular/material/dialog';
import { MatSnackBarModule } from '@angular/material/snack-bar'; import {MatSnackBarModule} from '@angular/material/snack-bar';
import { MatTooltipModule } from '@angular/material/tooltip'; import {MatTooltipModule} from '@angular/material/tooltip';
// Material Data tables // Material Data tables
import { MatPaginatorModule } from '@angular/material/paginator'; import {MatPaginatorModule} from '@angular/material/paginator';
import { MatSortModule } from '@angular/material/sort'; import {MatSortModule} from '@angular/material/sort';
import { MatTableModule } from '@angular/material/table'; import {MatTableModule} from '@angular/material/table';
import { MatMomentDateModule } from '@angular/material-moment-adapter'; import {MatMomentDateModule} from '@angular/material-moment-adapter';
import { FlexLayoutModule } from '@angular/flex-layout'; import {FlexLayoutModule} from '@angular/flex-layout';
import {
NgxMatDatetimePickerModule,
NgxMatNativeDateModule,
NgxMatTimepickerModule
} from '@angular-material-components/datetime-picker';
@NgModule({ @NgModule({
declarations: [], declarations: [],
@ -85,7 +91,10 @@ import { FlexLayoutModule } from '@angular/flex-layout';
MatSortModule, MatSortModule,
MatTableModule, MatTableModule,
MatMomentDateModule, MatMomentDateModule,
FlexLayoutModule FlexLayoutModule,
NgxMatDatetimePickerModule,
NgxMatNativeDateModule,
NgxMatTimepickerModule
], ],
exports: [ exports: [
MatAutocompleteModule, MatAutocompleteModule,
@ -123,7 +132,10 @@ import { FlexLayoutModule } from '@angular/flex-layout';
MatPaginatorModule, MatPaginatorModule,
MatSortModule, MatSortModule,
MatTableModule, MatTableModule,
FlexLayoutModule FlexLayoutModule,
NgxMatDatetimePickerModule,
NgxMatNativeDateModule,
NgxMatTimepickerModule
] ]
}) })
export class MaterialModule { } export class MaterialModule {}

View File

@ -0,0 +1,94 @@
<h3>{{'jitsi.rooms' | i18n}}</h3>
<table mat-table matSort [dataSource]="jitsiRooms" (matSortChange)="sortData($event)">
<ng-container matColumnDef="room">
<th mat-header-cell *matHeaderCellDef mat-sort-header="room"> {{'jitsi.rooms.room' | i18n}} </th>
<td mat-cell *matCellDef="let jitsiRoom">
<a mat-button color="accent" href="{{ jitsiRoom.url }}" target="_blank">{{ jitsiRoom.room }}<mat-icon
style="font-size: 1em;">open_in_new
</mat-icon></a> <button mat-icon-button (click)="share(jitsiRoom)">
<mat-icon>share</mat-icon>
</button>
</td>
</ng-container>
<ng-container matColumnDef="starts">
<th mat-header-cell *matHeaderCellDef mat-sort-header="starts"> {{'jitsi.rooms.starts' | i18n}} </th>
<td mat-cell *matCellDef="let jitsiRoom">{{ jitsiRoom.starts | date:datetimeformat}} </td>
</ng-container>
<ng-container matColumnDef="expires">
<th mat-header-cell *matHeaderCellDef mat-sort-header="expires"> {{'jitsi.rooms.expires' | i18n}} </th>
<td mat-cell *matCellDef="let jitsiRoom"> {{ jitsiRoom.expires | date:datetimeformat}} </td>
</ng-container>
<ng-container matColumnDef="moderationUrl">
<th mat-header-cell *matHeaderCellDef mat-sort-header="moderationUrl"> {{'jitsi.rooms.moderationUrl' | i18n}} </th>
<td mat-cell *matCellDef="let jitsiRoom">
<a mat-button color="primary" class="url" href="{{ jitsiRoom.moderationUrl}}" target="_blank">{{
jitsiRoom.moderationUrl }}
<mat-icon style="font-size: 1em;">open_in_new
</mat-icon>
</a>
</td>
</ng-container>
<ng-container matColumnDef="delete">
<th mat-header-cell *matHeaderCellDef class="align-right"> {{'jitsi.rooms.delete' | i18n}} </th>
<td mat-cell *matCellDef="let jitsiRoom" class="text-right">
<a mat-icon-button>
<mat-icon (click)="confirmDelete(jitsiRoom)">delete</mat-icon>
</a>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="jitsiRoomsColumns"></tr>
<tr mat-row *matRowDef="let myRowData; columns: jitsiRoomsColumns"></tr>
</table>
<form [formGroup]="form" (ngSubmit)="create()" #formDirective="ngForm">
<mat-card>
<mat-card-content>
<p>{{'jitsi.rooms.info' | i18n}}</p>
<p *ngIf="!jitsiRoomsQuota">{{'jitsi.rooms.noQuota' | i18n}}</p>
<div *ngIf="jitsiRoomsQuota">
<p>{{'jitsi.rooms.left' | i18n:jitsiRoomsQuota}}</p>
<mat-form-field>
<input matInput placeholder="{{'jitsi.rooms.room' | i18n}}" formControlName="room"
[(ngModel)]="jitsiRoom.room" required pattern="[a-zA-Z0-9]+">
<mat-error>
{{'jitsi.rooms.error.room' | i18n}}
</mat-error>
</mat-form-field>
<mat-form-field>
<input matInput [ngxMatDatetimePicker]="expiresPicker" [(ngModel)]="jitsiRoom.expires"
formControlName="expires" placeholder="{{'jitsi.rooms.expires' | i18n}}" required>
<mat-datepicker-toggle matSuffix [for]="expiresPicker"></mat-datepicker-toggle>
<ngx-mat-datetime-picker #expiresPicker></ngx-mat-datetime-picker>
<mat-error>
{{'jitsi.rooms.error.expires' | i18n}}
</mat-error>
</mat-form-field>
<mat-form-field>
<input matInput [ngxMatDatetimePicker]="startsPicker" [(ngModel)]="jitsiRoom.starts" formControlName="starts"
placeholder="{{'jitsi.rooms.starts' | i18n}}">
<mat-datepicker-toggle matSuffix [for]="startsPicker"></mat-datepicker-toggle>
<ngx-mat-datetime-picker #startsPicker></ngx-mat-datetime-picker>
<mat-error>
{{'jitsi.rooms.error.starts' | i18n}}
</mat-error>
</mat-form-field>
</div>
</mat-card-content>
<mat-card-actions>
<button *ngIf="jitsiRoomsQuota && !working" mat-raised-button color="primary" [disabled]="form.invalid">
{{'jitsi.rooms.create' | i18n}}
</button>
</mat-card-actions>
</mat-card>
</form>

View File

@ -0,0 +1,25 @@
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;
}
.url {
display: block;
width: 200px;
max-width: 200px;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}

View File

@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { JitsiComponent } from './jitsi.component';
describe('JitsiComponent', () => {
let component: JitsiComponent;
let fixture: ComponentFixture<JitsiComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ JitsiComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(JitsiComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,165 @@
import {Component, OnInit, ViewChild, Inject} from '@angular/core';
import {MatSnackBar} from '@angular/material/snack-bar';
import {Sort} from '@angular/material/sort';
import {FormBuilder, FormGroup, Validators, NgForm} from '@angular/forms';
import {MatDialog, MatDialogRef, MAT_DIALOG_DATA} from '@angular/material/dialog';
import {QuotaService} from '../../services/quota.service';
import {JitsiService} from '../../services/jitsi.service';
import {ConfirmDialog} from '../../ui/confirm/confirm.component';
import {I18nService} from './../../services/i18n.service';
@Component({
selector: 'app-account-jitsi',
templateUrl: './jitsi.component.html',
styleUrls: ['./jitsi.component.scss']
})
export class JitsiComponent implements OnInit {
form: FormGroup;
@ViewChild('formDirective') private formDirective: NgForm;
jitsiRoomsQuota: number = 0;
jitsiRooms: any[] = [];
jitsiRoom: any = {};
success: boolean;
working: boolean;
datetimeformat: String;
jitsiRoomsColumns = ["room", "starts", "expires", "moderationUrl", "delete"];
constructor(
private quotaService: QuotaService,
private formBuilder: FormBuilder,
private jitsiService: JitsiService,
private i18n: I18nService,
public dialog: MatDialog) {}
ngOnInit(): void {
this.datetimeformat = this.i18n.get('format.datetime', []);
this.form = this.formBuilder.group({
room: ['', Validators.required],
starts: ['', Validators.nullValidator],
expires: ['', Validators.required],
});
this.update();
}
create(): void {
this.working = true;
this.jitsiService.create(this.jitsiRoom).subscribe(response => {
this.update();
this.formDirective.resetForm();
this.jitsiRoom = {};
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]);
}
}
})
}
update() {
this.jitsiRoomsQuota = 0;
this.quotaService.quotas().subscribe((data: any) => {
for(let quota of data) {
if(quota.name == "jitsi") {
this.jitsiRoomsQuota = quota.value;
}
}
})
this.jitsiService.get().subscribe((data: any) => {
this.jitsiRooms = data;
})
}
confirmDelete(jitsiRoom) {
const dialogRef = this.dialog.open(ConfirmDialog, {
data: {
'label': 'jitsi.rooms.confirmDelete',
'args': [jitsiRoom.room]
}
})
dialogRef.afterClosed().subscribe(result => {
if(result) {
this.jitsiService.delete(jitsiRoom.id).subscribe((result: any) => {
this.update();
})
}
});
}
sortData(sort: Sort) {
const data = this.jitsiRooms.slice();
if(!sort.active || sort.direction === '') {
this.jitsiRooms = data;
return;
}
this.jitsiRooms = data.sort((a, b) => {
const isAsc = sort.direction === 'asc';
switch(sort.active) {
case 'room': return this.compare(a.room, b.room, isAsc);
case 'starts': return this.compare(a.room, b.room, isAsc);
case 'expires': return this.compare(a.room, b.room, isAsc);
default: return 0;
}
});
}
compare(a: number | string | String, b: number | string | String, isAsc: boolean) {
return (a < b ? -1 : 1) * (isAsc ? 1 : -1);
}
share(jitsiRoom) {
const dialogRef = this.dialog.open(JitsiShareDialog, {
data: jitsiRoom,
minWidth: '300px',
});
}
}
@Component({
selector: 'app-jitsi-share-dialog',
templateUrl: 'jitsi.share.html',
styleUrls: ['./jitsi.share.scss']
})
export class JitsiShareDialog {
jitsiRoom: any;
constructor(
private i18n: I18nService,
private snackBar: MatSnackBar,
public dialogRef: MatDialogRef<JitsiShareDialog>,
@Inject(MAT_DIALOG_DATA) public data: any) {
this.jitsiRoom = data;
}
copyToClipboard(jitsiRoom: any) {
const selBox = document.createElement('textarea');
selBox.value = jitsiRoom.url;
document.body.appendChild(selBox);
selBox.focus();
selBox.select();
document.execCommand('copy');
document.body.removeChild(selBox);
this.snackBar.open(this.i18n.get("jitsi.share.clipboard.copied", []), this.i18n.get("close", []), {
duration: 3000
});
}
}

View File

@ -0,0 +1,20 @@
<h1 mat-dialog-title><mat-icon inline="true">share</mat-icon> {{'jitsi.share' | i18n}}</h1>
<div mat-dialog-content>
<mat-list>
<mat-list-item>
<a mat-raised-button color="accent"
href="mailto:?&subject={{'jitsi.share.email.subject' | i18n:jitsiRoom.room}}&body={{jitsiRoom.url}}">
<mat-icon>mail</mat-icon> {{'jitsi.share.email' | i18n}}
</a>
</mat-list-item>
<mat-list-item>
<a mat-raised-button color="accent" (click)="copyToClipboard(jitsiRoom)">
<mat-icon>content_paste</mat-icon> {{'jitsi.share.clipboard' | i18n}}
</a>
</mat-list-item>
</mat-list>
</div>
<div mat-dialog-actions>
<button mat-button mat-dialog-close>{{'close' | i18n}}</button>
</div>

View File

@ -0,0 +1,4 @@
.mat-raised-button {
display: block;
width: 100%;
}

View File

@ -1,9 +1,9 @@
import { Component, OnInit } from '@angular/core'; import {Component, OnInit} from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import {FormBuilder, FormGroup, Validators} from '@angular/forms';
import { AuthService } from './../../services/auth.service'; import {AuthService} from './../../services/auth.service';
import { Router, ActivatedRoute } from '@angular/router'; import {Router, ActivatedRoute} from '@angular/router';
import { environment } from './../../../environments/environment'; import {environment} from './../../../environments/environment';
@Component({ @Component({
selector: 'app-login', selector: 'app-login',
@ -18,7 +18,11 @@ export class LoginComponent implements OnInit {
targetRoute = '/services'; targetRoute = '/services';
loginModel = {}; loginModel = {};
constructor(private formBuilder: FormBuilder, private authService: AuthService, private router: Router, private route: ActivatedRoute) { } constructor(
private formBuilder: FormBuilder,
private authService: AuthService,
private router: Router,
private route: ActivatedRoute) {}
async ngOnInit() { async ngOnInit() {
this.form = this.formBuilder.group({ this.form = this.formBuilder.group({
@ -28,15 +32,16 @@ export class LoginComponent implements OnInit {
}); });
this.route.queryParams.subscribe(params => { this.route.queryParams.subscribe(params => {
if (params['target']) { if(params['target']) {
this.targetRoute = params['target']; this.targetRoute = params['target'];
this.router.navigate([], {queryParams: {target: null}, queryParamsHandling: 'merge'});
} }
}); });
} }
async login() { async login() {
this.loginInvalid = false; this.loginInvalid = false;
if (this.form.valid) { if(this.form.valid) {
const loginModel = { const loginModel = {
username: this.form.get('username').value, username: this.form.get('username').value,
@ -47,8 +52,8 @@ export class LoginComponent implements OnInit {
this.authService.login(loginModel).subscribe((response: any) => { this.authService.login(loginModel).subscribe((response: any) => {
this.router.navigate([this.targetRoute]); this.router.navigate([this.targetRoute]);
}, error => { }, error => {
if (error.status == 428) { if(error.status == 428) {
this.router.navigate(["/login/totp"], { queryParams: { target: this.targetRoute } }); this.router.navigate(["/login/totp"], {queryParams: {target: this.targetRoute}});
} else { } else {
this.loginInvalid = true; this.loginInvalid = true;
} }

View File

@ -16,8 +16,9 @@
</p> </p>
</mat-card-content> </mat-card-content>
<mat-card-actions> <mat-card-actions>
<a href="{{service.url}}" target="_blank" mat-raised-button color="primary">{{'services.goto' | <a href="{{service.url}}" [target]="service.sameSite ? '_self' : '_blank'" mat-raised-button
i18n}}</a> color="accent">{{'services.goto' | i18n}} <mat-icon *ngIf="!service.sameSite" inline="true">
open_in_new</mat-icon></a>
</mat-card-actions> </mat-card-actions>
</mat-card> </mat-card>
</div> </div>

View File

@ -1,5 +1,6 @@
import {Component, OnInit} from '@angular/core'; import {Component, OnInit} from '@angular/core';
import { Location } from '@angular/common' import {Location} from '@angular/common'
import {Router, ActivatedRoute} from '@angular/router';
@Component({ @Component({
selector: 'app-unavailable', selector: 'app-unavailable',
@ -7,14 +8,29 @@ import { Location } from '@angular/common'
}) })
export class UnavailableComponent implements OnInit { export class UnavailableComponent implements OnInit {
targetRoute = '';
constructor( constructor(
private location: Location) {} private location: Location,
private router: Router,
private route: ActivatedRoute) {}
ngOnInit(): void { ngOnInit(): void {
this.route.queryParams.subscribe(params => {
if(params['target']) {
this.targetRoute = params['target'];
this.router.navigate([], {queryParams: {target: null}, queryParamsHandling: 'merge'});
}
});
} }
retry() { retry() {
this.location.back(); if(!this.targetRoute || this.targetRoute === "unavailable" || this.targetRoute === "/unavailable") {
this.location.back;
} else {
this.router.navigate([this.targetRoute]);
}
} }
} }

View File

@ -71,9 +71,12 @@ export class I18nService {
if(!from) { if(!from) {
return key; return key;
} else if(from[key]) { } else if(from[key]) {
if(typeof from[key] === 'object') {
if(from[key]["."]) { if(from[key]["."]) {
return this.insertArguments(from[key]["."], args); return this.insertArguments(from[key]["."], args);
} }
return key;
}
return this.insertArguments(from[key], args); return this.insertArguments(from[key], args);
} else { } else {
let keys = key.split("."); let keys = key.split(".");

View File

@ -0,0 +1,26 @@
import {Injectable} from '@angular/core';
import {HttpClient} from '@angular/common/http';
import {environment} from '../../environments/environment';
@Injectable({
providedIn: 'root',
})
export class JitsiService {
constructor(private http: HttpClient) {
}
get() {
return this.http.get(environment.apiUrl + "/jitsi/rooms");
}
create(jitsiRoom) {
return this.http.post(environment.apiUrl + "/jitsi/rooms", jitsiRoom);
}
delete(id) {
return this.http.delete(environment.apiUrl + "/jitsi/rooms/" + id);
}
}

View File

@ -16,7 +16,7 @@ export class PermissionsComponent implements OnInit {
constructor(private i18n: I18nService) {} constructor(private i18n: I18nService) {}
ngOnInit(): void { ngOnInit(): void {
this.datetimeformat = this.i18n.get('date-time-format', []); this.datetimeformat = this.i18n.get('format.datetime', []);
} }
sortData(sort: Sort) { sortData(sort: Sort) {

View File

@ -29,14 +29,23 @@
</mat-error> </mat-error>
</mat-form-field> </mat-form-field>
<mat-form-field *ngSwitchCase="'DATE'"> <mat-form-field *ngSwitchCase="'DATE'">
<input matInput [matDatepicker]="picker" [(ngModel)]="profileField.value" formControlName="value" <input matInput [matDatepicker]="datePicker" [(ngModel)]="profileField.value" formControlName="value"
placeholder="{{'profileField.value' | i18n}}"> placeholder="{{'profileField.value' | i18n}}">
<mat-datepicker-toggle matSuffix [for]="picker"></mat-datepicker-toggle> <mat-datepicker-toggle matSuffix [for]="datePicker"></mat-datepicker-toggle>
<mat-datepicker #picker></mat-datepicker> <mat-datepicker #datePicker></mat-datepicker>
<mat-error> <mat-error>
{{'profileField.error.DATE' | i18n}} {{'profileField.error.DATE' | i18n}}
</mat-error> </mat-error>
</mat-form-field> </mat-form-field>
<mat-form-field *ngSwitchCase="'DATETIME'">
<input matInput [ngxMatDatetimePicker]="datetimePicker" [(ngModel)]="profileField.value" formControlName="value"
placeholder="{{'profileField.value' | i18n}}">
<mat-datepicker-toggle matSuffix [for]="datetimePicker"></mat-datepicker-toggle>
<ngx-mat-datetime-picker #datetimePicker></ngx-mat-datetime-picker>
<mat-error>
{{'profileField.error.DATETIME' | i18n}}
</mat-error>
</mat-form-field>
<mat-form-field *ngSwitchCase="'URL'"> <mat-form-field *ngSwitchCase="'URL'">
<input matInput type="url" [(ngModel)]="profileField.value" formControlName="value" <input matInput type="url" [(ngModel)]="profileField.value" formControlName="value"
placeholder="{{'profileField.value' | i18n}}"> placeholder="{{'profileField.value' | i18n}}">
@ -81,7 +90,8 @@
</mat-option> </mat-option>
<mat-select-trigger> <mat-select-trigger>
<mat-icon inline="true">{{'visibility.' + profileField.visibility + '.icon' | i18n}}</mat-icon> {{'visibility.' + <mat-icon inline="true">{{'visibility.' + profileField.visibility + '.icon' | i18n}}</mat-icon>
{{'visibility.' +
profileField.visibility | i18n}} profileField.visibility | i18n}}
</mat-select-trigger> </mat-select-trigger>
</mat-select> </mat-select>

View File

@ -11,11 +11,14 @@
<td mat-cell *matCellDef="let profileField"> <td mat-cell *matCellDef="let profileField">
<div [ngSwitch]="profileField.type"> <div [ngSwitch]="profileField.type">
<span *ngSwitchCase="'TEXT'">{{profileField.value}}</span> <span *ngSwitchCase="'TEXT'">{{profileField.value}}</span>
<span *ngSwitchCase="'DATE'">{{profileField.value | date:datetimeformat}}</span> <span *ngSwitchCase="'DATE'">{{profileField.value | date:dateformat}}</span>
<a *ngSwitchCase="'URL'" href="{{profileField.value}}">{{profileField.value}}</a> <span *ngSwitchCase="'DATETIME'">{{profileField.value | date:datetimeformat}}</span>
<a *ngSwitchCase="'EMAIL'" href="mailto:{{profileField.value}}">{{profileField.value}}</a> <span *ngSwitchCase="'TIME'">{{profileField.value | date:timeformat}}</span>
<a *ngSwitchCase="'URL'" class="accent" href="{{profileField.value}}">{{profileField.value}}</a>
<a *ngSwitchCase="'EMAIL'" class="accent"
href="mailto:{{profileField.value}}">{{profileField.value}}</a>
<span *ngSwitchCase="'NUMBER'">{{profileField.value}}</span> <span *ngSwitchCase="'NUMBER'">{{profileField.value}}</span>
<button *ngSwitchCase="'BLOB'" mat-raised-button <button *ngSwitchCase="'BLOB'" mat-raised-buttonu
(click)="openBlob(profileField)">{{'profileField.openBlob' | i18n}}</button> (click)="openBlob(profileField)">{{'profileField.openBlob' | i18n}}</button>
<mat-slide-toggle *ngSwitchCase="'BOOL'" [checked]="profileField.value == 'true'" disabled> <mat-slide-toggle *ngSwitchCase="'BOOL'" [checked]="profileField.value == 'true'" disabled>
</mat-slide-toggle> </mat-slide-toggle>
@ -25,7 +28,9 @@
<ng-container matColumnDef="visibility" *ngIf="edit"> <ng-container matColumnDef="visibility" *ngIf="edit">
<th mat-header-cell *matHeaderCellDef mat-sort-header="visibility"> {{'visibility' | i18n}} </th> <th mat-header-cell *matHeaderCellDef mat-sort-header="visibility"> {{'visibility' | i18n}} </th>
<td mat-cell *matCellDef="let profileField"> <mat-icon inline="true">{{'visibility.' + profileField.visibility + '.icon' | i18n}}</mat-icon> {{'visibility.' + profileField.visibility | i18n}} <td mat-cell *matCellDef="let profileField">
<mat-icon inline="true">{{'visibility.' + profileField.visibility + '.icon' | i18n}}</mat-icon>
{{'visibility.' + profileField.visibility | i18n}}
</td> </td>
</ng-container> </ng-container>

View File

@ -17,6 +17,9 @@ export class ProfileFieldsComponent implements OnInit {
@Input() edit; @Input() edit;
profileFieldColumns = ["name", "value"]; profileFieldColumns = ["name", "value"];
profileFields: Array<any> = []; profileFields: Array<any> = [];
datetimeformat: String;
dateformat: String;
timeformat: String;
constructor(private i18n: I18nService, private profileService: ProfileService, public dialog: MatDialog) {} constructor(private i18n: I18nService, private profileService: ProfileService, public dialog: MatDialog) {}
@ -27,6 +30,11 @@ export class ProfileFieldsComponent implements OnInit {
this.profileFieldColumns.push("delete"); this.profileFieldColumns.push("delete");
} }
this.update(); this.update();
this.dateformat = this.i18n.get('format.date', []);
this.datetimeformat = this.i18n.get('format.datetime', []);
this.timeformat = this.i18n.get('format.time', []);
} }
@ -140,7 +148,7 @@ export class ProfileFieldDialog {
form: FormGroup; form: FormGroup;
profileField; profileField;
types = ["TEXT", "NUMBER", "DATE", "URL", "EMAIL", "BOOL", "BLOB"]; types = ["TEXT", "NUMBER", "DATE", "DATETIME", "URL", "EMAIL", "BOOL", "BLOB"];
visibilities = ["PRIVATE", "PROTECTED", "PUBLIC"]; visibilities = ["PRIVATE", "PROTECTED", "PUBLIC"];
constructor( constructor(

View File

@ -8,7 +8,6 @@
"cancel": "Abbrechen", "cancel": "Abbrechen",
"close": "Schließen", "close": "Schließen",
"confirm": "Bestätigen", "confirm": "Bestätigen",
"date-time-format": "dd.MM.yyyy HH:mm:ss",
"email": { "email": {
".": "E-Mail Adresse", ".": "E-Mail Adresse",
"invalid": "ungültige E-Mail Adresse", "invalid": "ungültige E-Mail Adresse",
@ -17,12 +16,48 @@
"hint": "Eine primäre E-Mail Adresse dient dazu eine andere Kontaktmöglichkeit als deine we.bstly Adresse anzugeben." "hint": "Eine primäre E-Mail Adresse dient dazu eine andere Kontaktmöglichkeit als deine we.bstly Adresse anzugeben."
} }
}, },
"format": {
"date": "dd.MM.yyyy",
"datetime": "dd.MM.yyyy HH:mm:ss",
"time": "HH:mm:ss"
},
"greet": "Hallo {0}", "greet": "Hallo {0}",
"help": "Hilfe", "help": "Hilfe",
"imprint": "Impressum", "imprint": "Impressum",
"info": { "info": {
".": "Info" ".": "Info"
}, },
"jitsi": {
"rooms": {
".": "Jitsi Räume",
"confirmDelete": "Möchtest du wirklich deinen Jitsi Raum '{0}' löschen?",
"create": "Jitsi Raum erstellen",
"delete": "Löschen",
"error": {
"expires": "Ungültiges Ende.",
"room": "Bitte wähle einen anderen Namen für den Raum. Erlaubt sind nur Buchstaben und Zahlen.",
"starts": "Ungültiger Beginn."
},
"expires": "Ende",
"info": "Du kannst hier Jitsi Räume erstellen. Die Anzahl wird über eine Quota begrenzt.",
"left": "Du kannst noch {0} Jitsi Räume erstellen.",
"moderationUrl": "Url für Moderation",
"noQuota": "Deine Quota für Jitsi Räume ist leider aufgebraucht.",
"room": "Name",
"starts": "Beginn"
},
"share": {
".": "Teilen",
"clipboard": {
".": "In Zwischenablage kopieren",
"copied": "In die Zwischenablage kopiert"
},
"email": {
".": "Via E-Mail teilen",
"subject": "Einladung in Videokonferenz {0}"
}
}
},
"locale": { "locale": {
"de-informal": { "de-informal": {
"long": "Deutsch", "long": "Deutsch",
@ -57,7 +92,8 @@
"INSUFFICIENT_LOWERCASE": "Bitte mindestens einen Kleinbuchstaben eingeben.", "INSUFFICIENT_LOWERCASE": "Bitte mindestens einen Kleinbuchstaben eingeben.",
"INSUFFICIENT_SPECIAL": "Bitte mindestens ein Sonderzeichen eingeben.", "INSUFFICIENT_SPECIAL": "Bitte mindestens ein Sonderzeichen eingeben.",
"INSUFFICIENT_UPPERCASE": "Bitte mindestens einen Großbuchstaben eingeben.", "INSUFFICIENT_UPPERCASE": "Bitte mindestens einen Großbuchstaben eingeben.",
"TOO_SHORT": "Bitte ein längeres Passwort wählen." "TOO_SHORT": "Bitte ein längeres Passwort wählen.",
"NOT_MATCH": "Passwörter stimmen nicht überein."
}, },
"forgot": "Passwort vergessen", "forgot": "Passwort vergessen",
"invalid": { "invalid": {
@ -109,6 +145,9 @@
"DATE": { "DATE": {
".": "Kein gültiges Datum" ".": "Kein gültiges Datum"
}, },
"DATETIME": {
".": "Kein gültiges Datum oder Uhrzeit"
},
"EMAIL": { "EMAIL": {
".": "Keine gültige E-Mail Adresse" ".": "Keine gültige E-Mail Adresse"
}, },
@ -119,6 +158,9 @@
"TEXT": { "TEXT": {
".": "Textfeld zu lang" ".": "Textfeld zu lang"
}, },
"TIME": {
".": "Keine gültige Uhrzeit"
},
"type": "Ungültiger Typ für dieses Profilfeld", "type": "Ungültiger Typ für dieses Profilfeld",
"URL": { "URL": {
".": "Keine gültige URL" ".": "Keine gültige URL"
@ -147,6 +189,9 @@
"DATE": { "DATE": {
".": "Datum" ".": "Datum"
}, },
"DATETIME": {
".": "Datum mit Uhrzeit"
},
"EMAIL": { "EMAIL": {
".": "E-Mail" ".": "E-Mail"
}, },
@ -156,6 +201,9 @@
"TEXT": { "TEXT": {
".": "Textfeld" ".": "Textfeld"
}, },
"TIME": {
".": "Uhrzeit"
},
"URL": { "URL": {
".": "URL" ".": "URL"
} }
@ -243,6 +291,12 @@
"title": "Gitea" "title": "Gitea"
}, },
"goto": "Zum Dienst", "goto": "Zum Dienst",
"jitsi": {
"icon": "video_call",
"subtitle": "Video Konferenzen",
"text": "Video Konferenzen mit allen Funktionen für Online-Treffen mit Video und Audio Streams.",
"title": "Jitsi Meet"
},
"mail": { "mail": {
"icon": "email", "icon": "email",
"subtitle": "E-Mail Konto", "subtitle": "E-Mail Konto",
@ -261,6 +315,12 @@
"text": "Dateiverwaltung, Kalender, Aufgabenmanagement, Kontaktmanagement, Abstimmungen und mehr.", "text": "Dateiverwaltung, Kalender, Aufgabenmanagement, Kontaktmanagement, Abstimmungen und mehr.",
"title": "Nextcloud" "title": "Nextcloud"
}, },
"owncast": {
"icon": "videocam",
"subtitle": "Livestreams",
"text": "Livestreams von Bastelei e. V.",
"title": "Owncast"
},
"partey": { "partey": {
"icon": "celebration", "icon": "celebration",
"subtitle": "Virtuelles Vereinsheim", "subtitle": "Virtuelles Vereinsheim",

View File

@ -8,7 +8,6 @@
"cancel": "Cancel", "cancel": "Cancel",
"close": "Close", "close": "Close",
"confirm": "Confirm", "confirm": "Confirm",
"date-time-format": "MM/dd/yyy h:mm:ss a",
"email": { "email": {
".": "Email address", ".": "Email address",
"invalid": "invalid email address", "invalid": "invalid email address",
@ -17,12 +16,48 @@
"hint": "A primary email address is used for contact you instead of you we.bstly address." "hint": "A primary email address is used for contact you instead of you we.bstly address."
} }
}, },
"format": {
"date": "MM/dd/yyy",
"datetime": "MM/dd/yyy h:mm:ss a",
"time": "h:mm:ss a"
},
"greet": "Hello {0}", "greet": "Hello {0}",
"help": "Help", "help": "Help",
"imprint": "Imprint", "imprint": "Imprint",
"info": { "info": {
".": "Info" ".": "Info"
}, },
"jitsi": {
"rooms": {
".": "Jitsi Rooms",
"confirmDelete": "Are you sure you want to delete your Jitsi Room '{0}'?",
"create": "Create Jitsi Room",
"delete": "Delete",
"error": {
"expires": "Invalid expiry.",
"room": "Please choose another name for the room. Only letters and numbers are allowed",
"starts": "Invalid start."
},
"expires": "Expires",
"info": "You can create new Jitsi Rooms here. The number is limited due to a quota.",
"left": "You have {0} Jitsi Room(s) left.",
"moderationUrl": "Moderation url",
"noQuota": "Your quota for Jitsi Rooms is depleted.",
"room": "Name",
"starts": "Starts"
},
"share" : {
"." : "Share",
"clipboard" : {
"." : "Copy to clipboard",
"copied": "Copied to clipboard"
},
"email" : {
"." : "Share via email",
"subject" : "Invite to videoconference {0}"
}
}
},
"locale": { "locale": {
"de-informal": { "de-informal": {
"long": "Deutsch", "long": "Deutsch",
@ -243,6 +278,12 @@
"title": "Gitea" "title": "Gitea"
}, },
"goto": "To service", "goto": "To service",
"jitsi" : {
"icon": "video_call",
"subtitle": "Video conferencing",
"text": "Video conferencing with all functionality needed for meeting online with Video and Audio streams.",
"title": "Jitsi Meet"
},
"mail": { "mail": {
"icon": "email", "icon": "email",
"subtitle": "Email Account", "subtitle": "Email Account",
@ -261,6 +302,12 @@
"text": "File management, calendar, tasks, contacts, polls and more.", "text": "File management, calendar, tasks, contacts, polls and more.",
"title": "Nextcloud" "title": "Nextcloud"
}, },
"owncast": {
"icon": "videocam",
"subtitle": "Livestreams",
"text": "Livestreams of Bastelei e. V.",
"title": "Owncast"
},
"partey": { "partey": {
"icon": "celebration", "icon": "celebration",
"subtitle": "Virtual clubhouse", "subtitle": "Virtual clubhouse",

View File

@ -903,7 +903,7 @@
<h4 id="Rechenzentrum">Rechenzentrum</h4> <h4 id="Rechenzentrum">Rechenzentrum</h4>
<p>Die Daten werden im Rechenzentrum der <a href="https://www.netcup.de/ueber-netcup/rechenzentrum.php" <p>Die Daten werden im Rechenzentrum der <a class="accent" href="https://www.netcup.de/ueber-netcup/rechenzentrum.php"
target="_blank">netcup GmbH</a> gespeichert. Eine regelmäßige, automatisierte Datensicherung der target="_blank">netcup GmbH</a> gespeichert. Eine regelmäßige, automatisierte Datensicherung der
Bestandsdaten Bestandsdaten
wird durchgeführt. wird durchgeführt.

View File

@ -1,7 +1,7 @@
<h2>Nutzungsbedingungen</h2> <h2>Nutzungsbedingungen</h2>
<h3>Bereitstellung</h3> <h3>Bereitstellung</h3>
<p>Mit we.bstly stellt der <a href="https://www.bstly.de" target="_blank">Bastelei e. V.</a> eine Plattform <p>Mit we.bstly stellt der <a class="accent" href="https://www.bstly.de" target="_blank">Bastelei e. V.</a> eine Plattform
bereit, um verschiedene digitale Services zeitlich befristet zur Verfügung zu stellen. </p> bereit, um verschiedene digitale Services zeitlich befristet zur Verfügung zu stellen. </p>
<p>we.bstly und damit verbundene Services stehen allen Vereinsmitgliedern des Bastelei e. V. sowie allen <p>we.bstly und damit verbundene Services stehen allen Vereinsmitgliedern des Bastelei e. V. sowie allen
NutzerInnen, die die entsprechenden Services direkt gebucht haben, zur Verfügung.</p> NutzerInnen, die die entsprechenden Services direkt gebucht haben, zur Verfügung.</p>
@ -21,7 +21,7 @@
<h3>Datensicherheit</h3> <h3>Datensicherheit</h3>
<p>we.bstly und damit verbundene Services haben Sicherheitsmerkmale implementiert, die Schutz vor Verlust, Missbrauch <p>we.bstly und damit verbundene Services haben Sicherheitsmerkmale implementiert, die Schutz vor Verlust, Missbrauch
und Manipulation der Daten und Dateien bieten. Dennoch kann ein 100% Schutz nicht gewährleistet werden (siehe <a und Manipulation der Daten und Dateien bieten. Dennoch kann ein 100% Schutz nicht gewährleistet werden (siehe <a class="accent"
href="/terms-of-service#availability">Verfügbarkeit und Leistungsstörungen</a>).</p> href="/terms-of-service#availability">Verfügbarkeit und Leistungsstörungen</a>).</p>
<p>Die NutzerInnen sind für die Daten und Dateien, die sie in we.bstly und damit verbundenen Services ablegen, <p>Die NutzerInnen sind für die Daten und Dateien, die sie in we.bstly und damit verbundenen Services ablegen,
@ -99,7 +99,7 @@ darstellen, betreffen oder beinhalten.
<h3>Datenschutz</h3> <h3>Datenschutz</h3>
<p>Personenbezogene Daten werden ausschließlich zur Bereitstellung von we.bstly und damit verbundenen Services <p>Personenbezogene Daten werden ausschließlich zur Bereitstellung von we.bstly und damit verbundenen Services
verarbeitet. Die Details dazu sind in der <a href="/privacy-policy">Datenschutzerklärung</a> zu finden. verarbeitet. Die Details dazu sind in der <a class="accent" href="/privacy-policy">Datenschutzerklärung</a> zu finden.
</p> </p>

View File

@ -905,7 +905,7 @@
<h4 id="Rechenzentrum">Rechenzentrum</h4> <h4 id="Rechenzentrum">Rechenzentrum</h4>
<p>Die Daten werden im Rechenzentrum der <a href="https://www.netcup.de/ueber-netcup/rechenzentrum.php" <p>Die Daten werden im Rechenzentrum der <a class="accent" href="https://www.netcup.de/ueber-netcup/rechenzentrum.php"
target="_blank">netcup GmbH</a> gespeichert. Eine regelmäßige, automatisierte Datensicherung der target="_blank">netcup GmbH</a> gespeichert. Eine regelmäßige, automatisierte Datensicherung der
Bestandsdaten Bestandsdaten
wird durchgeführt. wird durchgeführt.

View File

@ -3,13 +3,13 @@
<h2>Nutzungsbedingungen</h2> <h2>Nutzungsbedingungen</h2>
<h3>Bereitstellung</h3> <h3>Bereitstellung</h3>
<p>Mit we.bstly stellt der <a href="https://www.bstly.de" target="_blank">Bastelei e. V.</a> eine Plattform <p>Mit we.bstly stellt der <a class="accent" href="https://www.bstly.de" target="_blank">Bastelei e. V.</a> eine Plattform
bereit, um verschiedene digitale Services zeitlich befristet zur Verfügung zu stellen. </p> bereit, um verschiedene digitale Services zeitlich befristet zur Verfügung zu stellen. </p>
<p>we.bstly und damit verbundene Services stehen allen Vereinsmitgliedern des Bastelei e. V. sowie allen <p>we.bstly und damit verbundene Services stehen allen Vereinsmitgliedern des Bastelei e. V. sowie allen
NutzerInnen, die die entsprechenden Services direkt gebucht haben, zur Verfügung.</p> NutzerInnen, die die entsprechenden Services direkt gebucht haben, zur Verfügung.</p>
<p>Gegenstand der Nutzung ist die Bereitstellung we.bstly und den damit verbundenen Services zur Online-Nutzung über das <p>Gegenstand der Nutzung ist die Bereitstellung we.bstly und den damit verbundenen Services zur Online-Nutzung über das
Internet und die Übermittlung und Speicherung von Daten und Dateien der NutzerInnen.</p> Internet und die Übermittlung und Speicherung von Daten und Dateien der NutzerInnen.</p>
<p>we.bstly sowie die verschiedenen Services sind einzeln mit ihren Funktionen unter <a href="/services">Aktive <p>we.bstly sowie die verschiedenen Services sind einzeln mit ihren Funktionen unter <a class="accent" href="/services">Aktive
Services</a> aufgelistet.</p> Services</a> aufgelistet.</p>
@ -23,7 +23,7 @@
<h3>Datensicherheit</h3> <h3>Datensicherheit</h3>
<p>we.bstly und damit verbundene Services haben Sicherheitsmerkmale implementiert, die Schutz vor Verlust, Missbrauch <p>we.bstly und damit verbundene Services haben Sicherheitsmerkmale implementiert, die Schutz vor Verlust, Missbrauch
und Manipulation der Daten und Dateien bieten. Dennoch kann ein 100% Schutz nicht gewährleistet werden (siehe <a und Manipulation der Daten und Dateien bieten. Dennoch kann ein 100% Schutz nicht gewährleistet werden (siehe <a class="accent"
href="/terms-of-service#availability">Verfügbarkeit und Leistungsstörungen</a>).</p> href="/terms-of-service#availability">Verfügbarkeit und Leistungsstörungen</a>).</p>
<p>Die NutzerInnen sind für die Daten und Dateien, die sie in we.bstly und damit verbundenen Services ablegen, <p>Die NutzerInnen sind für die Daten und Dateien, die sie in we.bstly und damit verbundenen Services ablegen,

View File

@ -25,12 +25,13 @@ $dark-theme: mat-dark-theme((color: (primary: $dark-primary,
warn: $light-warn, warn: $light-warn,
))); )));
// Include theme styles for core and each component used in your app. // Include theme styles for core and each component used in your app.
// Alternatively, you can import and @include the theme mixins for each component // Alternatively, you can import and @include the theme mixins for each component
// that you are using. // that you are using.
@include angular-material-theme($light-theme); @include angular-material-theme($light-theme);
.dark-theme { .dark-theme {
@include angular-material-color($dark-theme); @include angular-material-color($dark-theme);
} }
@ -47,6 +48,11 @@ $dark-theme: mat-dark-theme((color: (primary: $dark-primary,
src: url(assets/fonts/material_icons.woff2) format('woff2'); src: url(assets/fonts/material_icons.woff2) format('woff2');
} }
a.accent {
color: $accent;
}
.material-icons { .material-icons {
font-family: 'Material Icons'; font-family: 'Material Icons';
font-weight: normal; font-weight: normal;
@ -194,7 +200,7 @@ mat-sidenav-container {
color: $warn; color: $warn;
} }
.align-right{ .align-right {
display: flex; display: flex;
padding: 21px 0; padding: 21px 0;
justify-content: flex-end; justify-content: flex-end;
@ -239,6 +245,7 @@ table {
border: 0; border: 0;
border-spacing: 0; border-spacing: 0;
width: 100%; width: 100%;
background: white;
th, th,
td, td,
@ -270,3 +277,27 @@ table {
} }
} }
} }
.dark-theme {
table {
background: #424242;
th,
td,
td {
color: white;
border-bottom-color: rgba(255, 255, 255, 0.12);
}
thead {
tr {
th {
color: rgba(255, 255, 255, 0.7);
}
}
}
}
}

View File

@ -4,7 +4,6 @@ $light-primary: mat-palette($mat-gray, 800);
$light-accent: mat-palette($mat-pink, A200, A100, A400); $light-accent: mat-palette($mat-pink, A200, A100, A400);
$light-warn: mat-palette($mat-red); $light-warn: mat-palette($mat-red);
$primary: mat-color($light-primary); $primary: mat-color($light-primary);
$accent: mat-color($light-accent); $accent: mat-color($light-accent);
$warn: mat-color($light-warn); $warn: mat-color($light-warn);