mirror of
https://github.com/zitadel/zitadel.git
synced 2025-01-12 22:15:52 +00:00
fix(console): toast login handler, show user session loginname, policy value incrementation, accessibility (#413)
* get auth policy, fix increment from 0 * seo, accessibility * ngsw rem check for update * organize interceptors * toast i18n part1 * show loginname * use primary color * toast login handler, fix user session type * Update console/src/assets/i18n/de.json Co-authored-by: Florian Forster <florian@caos.ch> * Update console/src/assets/i18n/de.json Co-authored-by: Florian Forster <florian@caos.ch> * Update console/src/assets/i18n/de.json Co-authored-by: Florian Forster <florian@caos.ch> * Update console/src/assets/i18n/en.json Co-authored-by: Florian Forster <florian@caos.ch> * Update console/src/index.html Co-authored-by: Florian Forster <florian@caos.ch> * Update console/src/assets/i18n/en.json Co-authored-by: Florian Forster <florian@caos.ch> * Update console/src/assets/i18n/en.json Co-authored-by: Florian Forster <florian@caos.ch> Co-authored-by: Florian Forster <florian@caos.ch>
This commit is contained in:
parent
fa57cc48c1
commit
0721acf605
@ -4,10 +4,10 @@
|
||||
<mat-icon aria-label="Side nav toggle icon">menu</mat-icon>
|
||||
</button>
|
||||
<a *ngIf="(isHandset$ | async) == false" class="title" [routerLink]="['/']">
|
||||
<img class="logo" *ngIf="componentCssClass == 'dark-theme'; else lighttheme"
|
||||
src="./assets/images/zitadel-logo-oneline-darkdesign.svg" />
|
||||
<img class="logo" alt="zitadel logo" *ngIf="componentCssClass == 'dark-theme'; else lighttheme"
|
||||
src="../assets/images/zitadel-logo-oneline-darkdesign.svg" />
|
||||
<ng-template #lighttheme>
|
||||
<img class="logo" src="./assets/images/zitadel-logo-oneline-lightdesign.svg" />
|
||||
<img alt="zitadel logo" class="logo" src="../assets/images/zitadel-logo-oneline-lightdesign.svg" />
|
||||
</ng-template>
|
||||
</a>
|
||||
|
||||
@ -126,4 +126,4 @@
|
||||
</div>
|
||||
</mat-drawer-content>
|
||||
</mat-drawer-container>
|
||||
</ng-container>
|
||||
</ng-container>
|
@ -10,7 +10,7 @@ import { TranslateService } from '@ngx-translate/core';
|
||||
import { Observable, of, Subscription } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
|
||||
import { Org, UserProfile } from './proto/generated/auth_pb';
|
||||
import { Org, UserProfileView } from './proto/generated/auth_pb';
|
||||
import { AuthUserService } from './services/auth-user.service';
|
||||
import { AuthService } from './services/auth.service';
|
||||
import { ThemeService } from './services/theme.service';
|
||||
@ -128,7 +128,7 @@ export class AppComponent implements OnDestroy {
|
||||
public showAccount: boolean = false;
|
||||
public org!: Org.AsObject;
|
||||
public orgs: Org.AsObject[] = [];
|
||||
public profile!: UserProfile.AsObject;
|
||||
public profile!: UserProfileView.AsObject;
|
||||
public isDarkTheme: Observable<boolean> = of(true);
|
||||
|
||||
public orgLoading: boolean = false;
|
||||
@ -248,7 +248,7 @@ export class AppComponent implements OnDestroy {
|
||||
this.orgs = res.toObject().resultList;
|
||||
this.orgLoading = false;
|
||||
}).catch(error => {
|
||||
this.toast.showError(error.message);
|
||||
this.toast.showError(error);
|
||||
this.orgLoading = false;
|
||||
});
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { OverlayModule } from '@angular/cdk/overlay';
|
||||
import { APP_BASE_HREF, CommonModule, registerLocaleData, PlatformLocation } from '@angular/common';
|
||||
import { CommonModule, registerLocaleData } from '@angular/common';
|
||||
import { HttpClient, HttpClientModule } from '@angular/common/http';
|
||||
import localeDe from '@angular/common/locales/de';
|
||||
import { APP_INITIALIZER, NgModule } from '@angular/core';
|
||||
@ -29,10 +29,10 @@ import { AvatarModule } from './modules/avatar/avatar.module';
|
||||
import { SignedoutComponent } from './pages/signedout/signedout.component';
|
||||
import { AuthUserService } from './services/auth-user.service';
|
||||
import { AuthService } from './services/auth.service';
|
||||
import { GrpcAuthInterceptor } from './services/grpc-auth.interceptor';
|
||||
import { GRPC_INTERCEPTORS } from './services/grpc-interceptor';
|
||||
import { GrpcOrgInterceptor } from './services/grpc-org.interceptor';
|
||||
import { GrpcService } from './services/grpc.service';
|
||||
import { GrpcAuthInterceptor } from './services/interceptors/grpc-auth.interceptor';
|
||||
import { GRPC_INTERCEPTORS } from './services/interceptors/grpc-interceptor';
|
||||
import { GrpcOrgInterceptor } from './services/interceptors/grpc-org.interceptor';
|
||||
import { StatehandlerProcessorService, StatehandlerProcessorServiceImpl } from './services/statehandler-processor.service';
|
||||
import { StatehandlerService, StatehandlerServiceImpl } from './services/statehandler.service';
|
||||
import { StorageService } from './services/storage.service';
|
||||
@ -42,7 +42,7 @@ registerLocaleData(localeDe);
|
||||
|
||||
// AoT requires an exported function for factories
|
||||
export function HttpLoaderFactory(http: HttpClient): TranslateHttpLoader {
|
||||
return new TranslateHttpLoader(http, './assets/i18n/');
|
||||
return new TranslateHttpLoader(http, './assets/i18n/');
|
||||
}
|
||||
|
||||
const appInitializerFn = (grpcServ: GrpcService) => {
|
||||
@ -151,7 +151,5 @@ const authConfig: AuthConfig = {
|
||||
})
|
||||
export class AppModule {
|
||||
|
||||
constructor() {
|
||||
console.log(window.location.href);
|
||||
}
|
||||
constructor() { }
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from '@angular/router';
|
||||
import { ActivatedRouteSnapshot, CanActivate, RouterStateSnapshot } from '@angular/router';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
import { AuthService } from '../services/auth.service';
|
||||
@ -9,7 +9,7 @@ import { AuthService } from '../services/auth.service';
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class AuthGuard implements CanActivate {
|
||||
constructor(private auth: AuthService, private router: Router) { }
|
||||
constructor(private auth: AuthService) { }
|
||||
|
||||
public canActivate(
|
||||
_: ActivatedRouteSnapshot,
|
||||
|
@ -3,8 +3,8 @@
|
||||
[name]="profile.displayName ? profile.displayName : (profile.firstName + ' '+ profile.lastName)" [size]="80">
|
||||
</app-avatar>
|
||||
|
||||
<span class="u-name">{{profile?.firstName}} {{profile?.lastName}}</span>
|
||||
<span class="u-email">{{profile?.userName}}</span>
|
||||
<span class="u-name">{{profile.displayName ? profile.displayName : profile.userName}}</span>
|
||||
<span class="u-email">{{profile?.preferredLoginName}}</span>
|
||||
<span class="iamuser" *ngIf="iamuser">IAM USER</span>
|
||||
|
||||
<button (click)="editUserProfile()" mat-stroked-button>{{'USER.EDITACCOUNT' | translate}}</button>
|
||||
@ -16,7 +16,8 @@
|
||||
</app-avatar>
|
||||
|
||||
<div class="col">
|
||||
<span class="title">{{user.userName}}</span>
|
||||
<span class="title">{{user.displayName ? user.displayName : user.userName}} </span>
|
||||
<span class="loginname">{{user.loginName}}</span>
|
||||
<span class="email">{{'USER.STATE.'+user.authState | translate}}</span>
|
||||
</div>
|
||||
<span class="fill-space"></span>
|
||||
|
@ -90,7 +90,7 @@
|
||||
line-height: 1rem;
|
||||
}
|
||||
|
||||
.email {
|
||||
.email, .loginname {
|
||||
color: #8795a1;
|
||||
font-size: .8rem;
|
||||
line-height: 1rem;
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { AuthConfig } from 'angular-oauth2-oidc';
|
||||
import { UserProfile, UserSessionView } from 'src/app/proto/generated/auth_pb';
|
||||
import { UserProfileView, UserSessionView } from 'src/app/proto/generated/auth_pb';
|
||||
import { AuthUserService } from 'src/app/services/auth-user.service';
|
||||
import { AuthService } from 'src/app/services/auth.service';
|
||||
|
||||
@ -11,7 +11,7 @@ import { AuthService } from 'src/app/services/auth.service';
|
||||
styleUrls: ['./accounts-card.component.scss'],
|
||||
})
|
||||
export class AccountsCardComponent implements OnInit {
|
||||
@Input() public profile!: UserProfile.AsObject;
|
||||
@Input() public profile!: UserProfileView.AsObject;
|
||||
@Input() public iamuser: boolean = false;
|
||||
|
||||
@Output() public close: EventEmitter<void> = new EventEmitter();
|
||||
@ -20,7 +20,6 @@ export class AccountsCardComponent implements OnInit {
|
||||
constructor(public authService: AuthService, private router: Router, private userService: AuthUserService) {
|
||||
this.userService.getMyUserSessions().then(sessions => {
|
||||
this.users = sessions.toObject().userSessionsList;
|
||||
|
||||
const index = this.users.findIndex(user => user.userName === this.profile.userName);
|
||||
this.users.splice(index, 1);
|
||||
|
||||
@ -45,14 +44,14 @@ export class AccountsCardComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
public selectAccount(loginName?: string): void {
|
||||
public selectAccount(loginHint?: string): void {
|
||||
const configWithPrompt: Partial<AuthConfig> = {
|
||||
customQueryParams: {
|
||||
// prompt: 'select_account',
|
||||
} as any,
|
||||
};
|
||||
if (loginName) {
|
||||
(configWithPrompt as any).customQueryParams['login_hint'] = loginName;
|
||||
if (loginHint) {
|
||||
(configWithPrompt as any).customQueryParams['login_hint'] = loginHint;
|
||||
}
|
||||
this.authService.authenticate(configWithPrompt);
|
||||
}
|
||||
|
@ -37,19 +37,19 @@ export class MemberCreateDialogComponent {
|
||||
this.projectService.GetProjectGrantMemberRoles().then(resp => {
|
||||
this.memberRoleOptions = resp.toObject().rolesList;
|
||||
}).catch(error => {
|
||||
toastService.showError(error.message);
|
||||
toastService.showError(error);
|
||||
});
|
||||
} else if (this.creationType === CreationType.PROJECT_OWNED) {
|
||||
this.projectService.GetProjectMemberRoles().then(resp => {
|
||||
this.memberRoleOptions = resp.toObject().rolesList;
|
||||
}).catch(error => {
|
||||
toastService.showError(error.message);
|
||||
toastService.showError(error);
|
||||
});
|
||||
} else if (this.creationType === CreationType.IAM) {
|
||||
this.adminService.GetIamMemberRoles().then(resp => {
|
||||
this.memberRoleOptions = resp.toObject().rolesList;
|
||||
}).catch(error => {
|
||||
toastService.showError(error.message);
|
||||
toastService.showError(error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -105,7 +105,6 @@ export class ChangesComponent implements OnInit {
|
||||
return from(col).pipe(
|
||||
tap((res: Changes) => {
|
||||
let values = res.toObject().changesList;
|
||||
console.log(values);
|
||||
// If prepending, reverse the batch order
|
||||
values = false ? values.reverse() : values;
|
||||
|
||||
|
@ -58,7 +58,6 @@ export class ProjectContributorsComponent implements OnInit {
|
||||
finalize(() => this.loadingSubject.next(false)),
|
||||
).subscribe(members => {
|
||||
this.membersSubject.next(members);
|
||||
console.log(members);
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -83,7 +82,7 @@ export class ProjectContributorsComponent implements OnInit {
|
||||
})).then(() => {
|
||||
this.toast.showError('members added');
|
||||
}).catch(error => {
|
||||
this.toast.showError(error.message);
|
||||
this.toast.showError(error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -42,7 +42,6 @@ export class ProjectMembersDataSource extends DataSource<ProjectMember.AsObject>
|
||||
finalize(() => this.loadingSubject.next(false)),
|
||||
).subscribe(members => {
|
||||
this.membersSubject.next(members);
|
||||
console.log(members);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -42,9 +42,6 @@ export class ProjectMembersComponent implements AfterViewInit {
|
||||
this.getRoleOptions();
|
||||
|
||||
this.route.params.subscribe(params => {
|
||||
console.log(params);
|
||||
|
||||
console.log(this.projectType);
|
||||
this.projectService.GetProjectById(params.projectid).then(project => {
|
||||
this.project = project.toObject();
|
||||
this.dataSource = new ProjectMembersDataSource(this.projectService);
|
||||
@ -69,13 +66,13 @@ export class ProjectMembersComponent implements AfterViewInit {
|
||||
this.projectService.GetProjectGrantMemberRoles().then(resp => {
|
||||
this.memberRoleOptions = resp.toObject().rolesList;
|
||||
}).catch(error => {
|
||||
this.toast.showError(error.message);
|
||||
this.toast.showError(error);
|
||||
});
|
||||
} else if (this.projectType === ProjectType.PROJECTTYPE_OWNED) {
|
||||
this.projectService.GetProjectMemberRoles().then(resp => {
|
||||
this.memberRoleOptions = resp.toObject().rolesList;
|
||||
}).catch(error => {
|
||||
this.toast.showError(error.message);
|
||||
this.toast.showError(error);
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -92,18 +89,18 @@ export class ProjectMembersComponent implements AfterViewInit {
|
||||
public removeProjectMemberSelection(): void {
|
||||
Promise.all(this.selection.selected.map(member => {
|
||||
return this.projectService.RemoveProjectMember(this.project.projectId, member.userId).then(() => {
|
||||
this.toast.showInfo('Removed successfully');
|
||||
this.toast.showInfo('PROJECT.TOAST.MEMBERREMOVED', true);
|
||||
}).catch(error => {
|
||||
this.toast.showError(error.message);
|
||||
this.toast.showError(error);
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
public removeMember(member: ProjectMember.AsObject): void {
|
||||
this.projectService.RemoveProjectMember(this.project.projectId, member.userId).then(() => {
|
||||
this.toast.showInfo('Member removed successfully');
|
||||
this.toast.showInfo('PROJECT.TOAST.MEMBERREMOVED', true);
|
||||
}).catch(error => {
|
||||
this.toast.showError(error.message);
|
||||
this.toast.showError(error);
|
||||
});
|
||||
}
|
||||
|
||||
@ -137,9 +134,9 @@ export class ProjectMembersComponent implements AfterViewInit {
|
||||
Promise.all(users.map(user => {
|
||||
return this.projectService.AddProjectMember(this.project.projectId, user.id, roles);
|
||||
})).then(() => {
|
||||
this.toast.showError('members added');
|
||||
this.toast.showInfo('PROJECT.TOAST.MEMBERSADDED', true);
|
||||
}).catch(error => {
|
||||
this.toast.showError(error.message);
|
||||
this.toast.showError(error);
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -147,13 +144,11 @@ export class ProjectMembersComponent implements AfterViewInit {
|
||||
}
|
||||
|
||||
updateRoles(member: ProjectMember.AsObject, selectionChange: MatSelectChange): void {
|
||||
console.log(member, selectionChange.value);
|
||||
this.projectService.ChangeProjectMember(this.project.projectId, member.userId, selectionChange.value)
|
||||
.then((newmember: ProjectMember) => {
|
||||
console.log(newmember.toObject());
|
||||
this.toast.showInfo('Member updated!');
|
||||
this.toast.showInfo('PROJECT.TOAST.MEMBERADDED', true);
|
||||
}).catch(error => {
|
||||
this.toast.showError(error.message);
|
||||
this.toast.showError(error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -23,7 +23,6 @@ export class ProjectRoleDetailComponent implements OnInit {
|
||||
displayName: new FormControl(''),
|
||||
group: new FormControl(''),
|
||||
});
|
||||
console.log(data);
|
||||
|
||||
this.formGroup.patchValue(data.role);
|
||||
}
|
||||
@ -35,10 +34,10 @@ export class ProjectRoleDetailComponent implements OnInit {
|
||||
if (this.formGroup.valid && this.key?.value && this.group?.value && this.displayName?.value) {
|
||||
this.projectService.ChangeProjectRole(this.projectId, this.key.value, this.key.value, this.group.value)
|
||||
.then(() => {
|
||||
this.toast.showInfo('Role updated');
|
||||
this.toast.showInfo('PROJECT.TOAST.ROLECHANGED', true);
|
||||
this.dialogRef.close(true);
|
||||
}).catch(error => {
|
||||
this.toast.showError(error.message);
|
||||
this.toast.showError(error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -33,7 +33,6 @@ export class ProjectRolesComponent implements AfterViewInit, OnInit {
|
||||
constructor(private projectService: ProjectService, private toast: ToastService, private dialog: MatDialog) { }
|
||||
|
||||
public ngOnInit(): void {
|
||||
console.log(this.projectId);
|
||||
this.dataSource = new ProjectRolesDataSource(this.projectService);
|
||||
this.dataSource.loadRoles(this.projectId, 0, 25, 'asc');
|
||||
|
||||
@ -85,7 +84,7 @@ export class ProjectRolesComponent implements AfterViewInit, OnInit {
|
||||
return Promise.all(this.selection.selected.map(role => {
|
||||
return this.projectService.RemoveProjectRole(role.projectId, role.key);
|
||||
})).then(() => {
|
||||
this.toast.showInfo('Deleted');
|
||||
this.toast.showInfo('PROJECT.TOAST.ROLEREMOVED', true);
|
||||
indexes.forEach(index => {
|
||||
if (index > -1) {
|
||||
oldState.splice(index, 1);
|
||||
@ -102,7 +101,7 @@ export class ProjectRolesComponent implements AfterViewInit, OnInit {
|
||||
this.projectService
|
||||
.RemoveProjectRole(role.projectId, role.key)
|
||||
.then(() => {
|
||||
this.toast.showInfo('Role removed');
|
||||
this.toast.showInfo('PROJECT.TOAST.ROLEREMOVED', true);
|
||||
this.dataSource.rolesSubject.value.splice(index, 1);
|
||||
this.dataSource.rolesSubject.next(this.dataSource.rolesSubject.value);
|
||||
})
|
||||
|
@ -56,7 +56,6 @@ export class SearchProjectAutocompleteComponent {
|
||||
).subscribe(([granted, owned]) => {
|
||||
this.isLoading = false;
|
||||
this.filteredProjects = [...owned.toObject().resultList, ...granted.toObject().resultList];
|
||||
console.log(this.filteredProjects);
|
||||
});
|
||||
}
|
||||
|
||||
@ -102,7 +101,6 @@ export class SearchProjectAutocompleteComponent {
|
||||
}
|
||||
|
||||
public selected(event: MatAutocompleteSelectedEvent): void {
|
||||
console.log(event.option.value);
|
||||
if (this.singleOutput) {
|
||||
this.selectionChanged.emit(event.option.value);
|
||||
} else {
|
||||
|
@ -140,7 +140,7 @@ export class SearchUserAutocompleteComponent {
|
||||
this.users = [user.toObject()];
|
||||
this.selectionChanged.emit(this.users);
|
||||
}).catch(error => {
|
||||
this.toast.showError(error.message);
|
||||
this.toast.showError(error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -77,7 +77,6 @@ export class UserGrantsDataSource extends DataSource<UserGrant.AsObject> {
|
||||
from(promise).pipe(
|
||||
map(resp => {
|
||||
this.totalResult = resp.toObject().totalResult;
|
||||
console.log(resp.toObject().resultList);
|
||||
return resp.toObject().resultList;
|
||||
}),
|
||||
catchError(() => of([])),
|
||||
|
@ -56,7 +56,6 @@ export class UserGrantsComponent implements OnInit, AfterViewInit {
|
||||
grantId: this.grantId,
|
||||
userId: this.userId,
|
||||
};
|
||||
console.log(this.context);
|
||||
|
||||
switch (this.context) {
|
||||
case UserGrantContext.OWNED_PROJECT:
|
||||
@ -78,7 +77,6 @@ export class UserGrantsComponent implements OnInit, AfterViewInit {
|
||||
default:
|
||||
this.routerLink = ['/grant-create'];
|
||||
}
|
||||
console.log(data);
|
||||
this.dataSource.loadGrants(this.context, 0, 25, data);
|
||||
}
|
||||
|
||||
@ -129,20 +127,18 @@ export class UserGrantsComponent implements OnInit, AfterViewInit {
|
||||
|
||||
updateRoles(grant: UserGrant.AsObject, selectionChange: MatSelectChange): void {
|
||||
this.userService.UpdateUserGrant(grant.id, grant.userId, selectionChange.value)
|
||||
.then((newmember: UserGrant) => {
|
||||
this.toast.showInfo('Grant updated!');
|
||||
.then(() => {
|
||||
this.toast.showInfo('GRANTS.TOAST.UPDATED', true);
|
||||
}).catch(error => {
|
||||
this.toast.showError(error.message);
|
||||
this.toast.showError(error);
|
||||
});
|
||||
}
|
||||
|
||||
deleteGrantSelection(): void {
|
||||
this.userService.BulkRemoveUserGrant(this.selection.selected.map(grant => grant.id)).then(() => {
|
||||
this.toast.showInfo('Grants deleted');
|
||||
this.toast.showInfo('GRANTS.TOAST.BULKREMOVED', true);
|
||||
const data = this.dataSource.grantsSubject.getValue();
|
||||
console.log(data);
|
||||
this.selection.selected.forEach((item) => {
|
||||
console.log(item);
|
||||
const index = data.findIndex(i => i.id === item.id);
|
||||
if (index > -1) {
|
||||
data.splice(index, 1);
|
||||
@ -151,7 +147,7 @@ export class UserGrantsComponent implements OnInit, AfterViewInit {
|
||||
});
|
||||
this.selection.clear();
|
||||
}).catch(error => {
|
||||
this.toast.showError(error.message);
|
||||
this.toast.showError(error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -137,7 +137,7 @@ export class AppDetailComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
}).catch(error => {
|
||||
console.error(error);
|
||||
this.toast.showError(error.message);
|
||||
this.toast.showError(error);
|
||||
this.errorMessage = error.message;
|
||||
});
|
||||
}
|
||||
@ -145,15 +145,15 @@ export class AppDetailComponent implements OnInit, OnDestroy {
|
||||
public changeState(event: MatButtonToggleChange): void {
|
||||
if (event.value === AppState.APPSTATE_ACTIVE) {
|
||||
this.projectService.ReactivateApplication(this.projectId, this.app.id).then(() => {
|
||||
this.toast.showInfo('Reactivated Application');
|
||||
this.toast.showInfo('APP.TOAST.REACTIVATED', true);
|
||||
}).catch((error: any) => {
|
||||
this.toast.showError(error.message);
|
||||
this.toast.showError(error);
|
||||
});
|
||||
} else if (event.value === AppState.APPSTATE_INACTIVE) {
|
||||
this.projectService.DeactivateApplication(this.projectId, this.app.id).then(() => {
|
||||
this.toast.showInfo('Deactivated Application');
|
||||
this.toast.showInfo('APP.TOAST.REACTIVATED', true);
|
||||
}).catch((error: any) => {
|
||||
this.toast.showError(error.message);
|
||||
this.toast.showError(error);
|
||||
});
|
||||
}
|
||||
|
||||
@ -222,7 +222,7 @@ export class AppDetailComponent implements OnInit, OnDestroy {
|
||||
this.projectService
|
||||
.UpdateOIDCAppConfig(this.projectId, this.app.id, this.app.oidcConfig)
|
||||
.then((data: OIDCConfig) => {
|
||||
this.toast.showInfo('OIDC Config saved');
|
||||
this.toast.showInfo('APP.TOAST.OIDCUPDATED', true);
|
||||
})
|
||||
.catch(data => {
|
||||
this.toast.showError(data.message);
|
||||
@ -233,7 +233,7 @@ export class AppDetailComponent implements OnInit, OnDestroy {
|
||||
|
||||
public regenerateOIDCClientSecret(): void {
|
||||
this.projectService.RegenerateOIDCClientSecret(this.app.id, this.projectId).then((data: OIDCConfig) => {
|
||||
this.toast.showInfo('OIDC Secret Regenerated');
|
||||
this.toast.showInfo('APP.TOAST.OIDCCLIENTSECRETREGENERATED', true);
|
||||
this.dialog.open(AppSecretDialogComponent, {
|
||||
data: {
|
||||
clientId: data.toObject().clientId,
|
||||
|
@ -91,9 +91,8 @@ export class GrantedProjectDetailComponent implements OnInit, OnDestroy {
|
||||
if (this.projectId && this.grantId) {
|
||||
this.projectService.GetGrantedProjectByID(this.projectId, this.grantId).then(proj => {
|
||||
this.project = proj.toObject();
|
||||
console.log(this.project);
|
||||
}).catch(error => {
|
||||
this.toast.showError(error.message);
|
||||
this.toast.showError(error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -61,7 +61,7 @@ export class GrantedProjectGridComponent implements OnChanges {
|
||||
}
|
||||
|
||||
public ngOnChanges(changes: SimpleChanges): void {
|
||||
if (changes.items.currentValue && changes.items.currentValue.length > 0) {
|
||||
if (changes.items?.currentValue && changes.items.currentValue.length > 0) {
|
||||
this.notPinned = Object.assign([], this.items);
|
||||
this.reorganizeItems();
|
||||
}
|
||||
|
@ -95,7 +95,7 @@ export class GrantedProjectListComponent implements OnInit, OnDestroy {
|
||||
this.loadingSubject.next(false);
|
||||
}).catch(error => {
|
||||
console.error(error);
|
||||
this.toast.showError(error.message);
|
||||
this.toast.showError(error);
|
||||
this.loadingSubject.next(false);
|
||||
});
|
||||
}
|
||||
|
@ -1,8 +1,9 @@
|
||||
<div class="wrapper">
|
||||
<div class="header">
|
||||
<img *ngIf="dark; else lighttheme" src="./assets/images/zitadel-logo-oneline-darkdesign.svg" />
|
||||
<img alt="zitadel logo" *ngIf="dark; else lighttheme"
|
||||
src="../assets/images/zitadel-logo-oneline-darkdesign.svg" />
|
||||
<ng-template #lighttheme>
|
||||
<img src="./assets/images/zitadel-logo-oneline-lightdesign.svg" />
|
||||
<img alt="zitadel logo" src="../assets/images/zitadel-logo-oneline-lightdesign.svg" />
|
||||
</ng-template>
|
||||
</div>
|
||||
|
||||
@ -79,4 +80,4 @@
|
||||
</div>
|
||||
</ng-template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -76,7 +76,7 @@ export class IamContributorsComponent implements OnInit {
|
||||
})).then(() => {
|
||||
this.toast.showError('members added');
|
||||
}).catch(error => {
|
||||
this.toast.showError(error.message);
|
||||
this.toast.showError(error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -55,7 +55,7 @@ export class IamMembersComponent implements AfterViewInit {
|
||||
return this.adminService.RemoveIamMember(member.userId).then(() => {
|
||||
this.toast.showInfo('Removed successfully');
|
||||
}).catch(error => {
|
||||
this.toast.showError(error.message);
|
||||
this.toast.showError(error);
|
||||
});
|
||||
}));
|
||||
}
|
||||
@ -64,7 +64,7 @@ export class IamMembersComponent implements AfterViewInit {
|
||||
this.adminService.RemoveIamMember(member.userId).then(() => {
|
||||
this.toast.showInfo('Member removed successfully');
|
||||
}).catch(error => {
|
||||
this.toast.showError(error.message);
|
||||
this.toast.showError(error);
|
||||
});
|
||||
}
|
||||
|
||||
@ -99,7 +99,7 @@ export class IamMembersComponent implements AfterViewInit {
|
||||
})).then(() => {
|
||||
this.toast.showError('members added');
|
||||
}).catch(error => {
|
||||
this.toast.showError(error.message);
|
||||
this.toast.showError(error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -77,7 +77,7 @@ export class OrgContributorsComponent implements OnInit {
|
||||
})).then(() => {
|
||||
this.toast.showError('members added');
|
||||
}).catch(error => {
|
||||
this.toast.showError(error.message);
|
||||
this.toast.showError(error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -4,9 +4,9 @@ import { Component } from '@angular/core';
|
||||
import { AbstractControl, FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||
import { Router } from '@angular/router';
|
||||
import { CreateOrgRequest, CreateUserRequest, Gender, OrgSetUpResponse } from 'src/app/proto/generated/admin_pb';
|
||||
import { PasswordComplexityPolicy } from 'src/app/proto/generated/management_pb';
|
||||
import { PasswordComplexityPolicy } from 'src/app/proto/generated/auth_pb';
|
||||
import { AdminService } from 'src/app/services/admin.service';
|
||||
import { OrgService } from 'src/app/services/org.service';
|
||||
import { AuthUserService } from 'src/app/services/auth-user.service';
|
||||
import { ToastService } from 'src/app/services/toast.service';
|
||||
|
||||
import { lowerCaseValidator, numberValidator, symbolValidator, upperCaseValidator } from '../../user-detail/validators';
|
||||
@ -62,7 +62,7 @@ export class OrgCreateComponent {
|
||||
private adminService: AdminService,
|
||||
private _location: Location,
|
||||
private fb: FormBuilder,
|
||||
private orgService: OrgService,
|
||||
private authUserService: AuthUserService,
|
||||
) {
|
||||
const validators: Validators[] = [Validators.required];
|
||||
|
||||
@ -70,7 +70,7 @@ export class OrgCreateComponent {
|
||||
name: ['', [Validators.required]],
|
||||
domain: [''],
|
||||
});
|
||||
this.orgService.GetPasswordComplexityPolicy().then(data => {
|
||||
this.authUserService.GetMyPasswordComplexityPolicy().then(data => {
|
||||
this.policy = data.toObject();
|
||||
if (this.policy.minLength) {
|
||||
validators.push(Validators.minLength(this.policy.minLength));
|
||||
|
@ -52,7 +52,7 @@ export class OrgDetailComponent implements OnInit, OnDestroy {
|
||||
this.orgService.GetMyOrg().then((org: Org) => {
|
||||
this.org = org.toObject();
|
||||
}).catch(error => {
|
||||
this.toast.showError(error.message);
|
||||
this.toast.showError(error);
|
||||
});
|
||||
|
||||
this.orgService.SearchMyOrgDomains(0, 100).then(result => {
|
||||
@ -66,13 +66,13 @@ export class OrgDetailComponent implements OnInit, OnDestroy {
|
||||
this.orgService.ReactivateMyOrg().then(() => {
|
||||
this.toast.showInfo('Reactivated Org');
|
||||
}).catch((error) => {
|
||||
this.toast.showError(error.message);
|
||||
this.toast.showError(error);
|
||||
});
|
||||
} else if (event.value === OrgState.ORGSTATE_INACTIVE) {
|
||||
this.orgService.DeactivateMyOrg().then(() => {
|
||||
this.toast.showInfo('Deactivated Org');
|
||||
}).catch((error) => {
|
||||
this.toast.showError(error.message);
|
||||
this.toast.showError(error);
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -104,7 +104,7 @@ export class OrgDetailComponent implements OnInit, OnDestroy {
|
||||
this.domains.splice(index, 1);
|
||||
}
|
||||
}).catch(error => {
|
||||
this.toast.showError(error.message);
|
||||
this.toast.showError(error);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
@ -33,7 +33,7 @@ export class OrgGridComponent {
|
||||
this.orgList = res.toObject().resultList;
|
||||
}).catch(error => {
|
||||
console.error(error);
|
||||
this.toast.showError(error.message);
|
||||
this.toast.showError(error);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -22,7 +22,7 @@ export class OrgMemberRolesAutocompleteComponent {
|
||||
this.orgService.GetOrgMemberRoles().then(resp => {
|
||||
this.allRoles = resp.toObject().rolesList;
|
||||
}).catch(error => {
|
||||
this.toast.showError(error.message);
|
||||
this.toast.showError(error);
|
||||
});
|
||||
|
||||
this.myControl.valueChanges.subscribe(change => {
|
||||
|
@ -59,7 +59,7 @@ export class OrgMembersComponent implements AfterViewInit {
|
||||
return this.orgService.RemoveMyOrgMember(member.userId).then(() => {
|
||||
this.toast.showInfo('Removed successfully');
|
||||
}).catch(error => {
|
||||
this.toast.showError(error.message);
|
||||
this.toast.showError(error);
|
||||
});
|
||||
}));
|
||||
}
|
||||
@ -68,7 +68,7 @@ export class OrgMembersComponent implements AfterViewInit {
|
||||
this.orgService.RemoveMyOrgMember(member.userId).then(() => {
|
||||
this.toast.showInfo('Member removed successfully');
|
||||
}).catch(error => {
|
||||
this.toast.showError(error.message);
|
||||
this.toast.showError(error);
|
||||
});
|
||||
}
|
||||
|
||||
@ -103,7 +103,7 @@ export class OrgMembersComponent implements AfterViewInit {
|
||||
})).then(() => {
|
||||
this.toast.showError('members added');
|
||||
}).catch(error => {
|
||||
this.toast.showError(error.message);
|
||||
this.toast.showError(error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -35,7 +35,7 @@
|
||||
<div class="row">
|
||||
<span class="left-desc">{{'ORG.POLICY.DATA.SHOWLOCKOUTFAILURES' | translate}}</span>
|
||||
<span class="fill-space"></span>
|
||||
<mat-slide-toggle name="showLockOutFailures" ngDefaultControl
|
||||
<mat-slide-toggle color="primary" name="showLockOutFailures" ngDefaultControl
|
||||
[(ngModel)]="complexityData.showLockOutFailures">
|
||||
</mat-slide-toggle>
|
||||
</div>
|
||||
@ -63,26 +63,28 @@
|
||||
<div class="row">
|
||||
<span class="left-desc">{{'ORG.POLICY.DATA.HASNUMBER' | translate}}</span>
|
||||
<span class="fill-space"></span>
|
||||
<mat-slide-toggle name="hasNumber" ngDefaultControl [(ngModel)]="complexityData.hasNumber">
|
||||
<mat-slide-toggle color="primary" name="hasNumber" ngDefaultControl
|
||||
[(ngModel)]="complexityData.hasNumber">
|
||||
</mat-slide-toggle>
|
||||
</div>
|
||||
<div class="row">
|
||||
<span class="left-desc">{{'ORG.POLICY.DATA.HASSYMBOL' | translate}}</span>
|
||||
<span class="fill-space"></span>
|
||||
<mat-slide-toggle name="hasSymbol" ngDefaultControl [(ngModel)]="complexityData.hasSymbol">
|
||||
<mat-slide-toggle color="primary" name="hasSymbol" ngDefaultControl
|
||||
[(ngModel)]="complexityData.hasSymbol">
|
||||
</mat-slide-toggle>
|
||||
</div>
|
||||
<div class="row">
|
||||
<span class="left-desc">{{'ORG.POLICY.DATA.HASLOWERCASE' | translate}}</span>
|
||||
<span class="fill-space"></span>
|
||||
<mat-slide-toggle name="hasLowercase" ngDefaultControl
|
||||
<mat-slide-toggle color="primary" name="hasLowercase" ngDefaultControl
|
||||
[(ngModel)]="complexityData.hasLowercase">
|
||||
</mat-slide-toggle>
|
||||
</div>
|
||||
<div class="row">
|
||||
<span class="left-desc">{{'ORG.POLICY.DATA.HASUPPERCASE' | translate}}</span>
|
||||
<span class="fill-space"></span>
|
||||
<mat-slide-toggle name="hasUppercase" ngDefaultControl
|
||||
<mat-slide-toggle color="primary" name="hasUppercase" ngDefaultControl
|
||||
[(ngModel)]="complexityData.hasUppercase">
|
||||
</mat-slide-toggle>
|
||||
</div>
|
||||
@ -132,13 +134,14 @@
|
||||
<div class="row">
|
||||
<span class="left-desc">{{'ORG.POLICY.DATA.USERLOGINMUSTBEDOMAIN' | translate}}</span>
|
||||
<span class="fill-space"></span>
|
||||
<mat-slide-toggle name="hasNumber" ngDefaultControl [(ngModel)]="iamData.userLoginMustBeDomain">
|
||||
<mat-slide-toggle color="primary" name="hasNumber" ngDefaultControl
|
||||
[(ngModel)]="iamData.userLoginMustBeDomain">
|
||||
</mat-slide-toggle>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="btn-container">
|
||||
<button (click)="savePolicy()" color="accent" type="submit"
|
||||
<button (click)="savePolicy()" color="primary" type="submit"
|
||||
mat-raised-button>{{ 'ACTIONS.SAVE' | translate }}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -160,7 +160,7 @@ export class PasswordPolicyComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
public incrementLength(): void {
|
||||
if (this.complexityData?.minLength) {
|
||||
if (this.complexityData?.minLength !== undefined) {
|
||||
this.complexityData.minLength++;
|
||||
}
|
||||
}
|
||||
@ -172,7 +172,7 @@ export class PasswordPolicyComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
public incrementMaxAttempts(): void {
|
||||
if (this.lockoutData?.maxAttempts) {
|
||||
if (this.lockoutData?.maxAttempts !== undefined) {
|
||||
this.lockoutData.maxAttempts++;
|
||||
}
|
||||
}
|
||||
@ -184,7 +184,7 @@ export class PasswordPolicyComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
public incrementExpireWarnDays(): void {
|
||||
if (this.ageData?.expireWarnDays) {
|
||||
if (this.ageData?.expireWarnDays !== undefined) {
|
||||
this.ageData.expireWarnDays++;
|
||||
}
|
||||
}
|
||||
@ -196,7 +196,7 @@ export class PasswordPolicyComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
public incrementMaxAgeDays(): void {
|
||||
if (this.ageData?.maxAgeDays) {
|
||||
if (this.ageData?.maxAgeDays !== undefined) {
|
||||
this.ageData.maxAgeDays++;
|
||||
}
|
||||
}
|
||||
@ -218,7 +218,7 @@ export class PasswordPolicyComponent implements OnInit, OnDestroy {
|
||||
).then(() => {
|
||||
this.router.navigate(['org']);
|
||||
}).catch(error => {
|
||||
this.toast.showError(error.message);
|
||||
this.toast.showError(error);
|
||||
});
|
||||
|
||||
break;
|
||||
@ -230,7 +230,7 @@ export class PasswordPolicyComponent implements OnInit, OnDestroy {
|
||||
).then(() => {
|
||||
this.router.navigate(['org']);
|
||||
}).catch(error => {
|
||||
this.toast.showError(error.message);
|
||||
this.toast.showError(error);
|
||||
});
|
||||
|
||||
break;
|
||||
@ -245,7 +245,7 @@ export class PasswordPolicyComponent implements OnInit, OnDestroy {
|
||||
).then(() => {
|
||||
this.router.navigate(['org']);
|
||||
}).catch(error => {
|
||||
this.toast.showError(error.message);
|
||||
this.toast.showError(error);
|
||||
});
|
||||
break;
|
||||
|
||||
@ -259,7 +259,7 @@ export class PasswordPolicyComponent implements OnInit, OnDestroy {
|
||||
).then(() => {
|
||||
this.router.navigate(['org']);
|
||||
}).catch(error => {
|
||||
this.toast.showError(error.message);
|
||||
this.toast.showError(error);
|
||||
});
|
||||
}
|
||||
break;
|
||||
@ -274,7 +274,7 @@ export class PasswordPolicyComponent implements OnInit, OnDestroy {
|
||||
).then(() => {
|
||||
this.router.navigate(['org']);
|
||||
}).catch(error => {
|
||||
this.toast.showError(error.message);
|
||||
this.toast.showError(error);
|
||||
});
|
||||
|
||||
break;
|
||||
@ -286,7 +286,7 @@ export class PasswordPolicyComponent implements OnInit, OnDestroy {
|
||||
).then(() => {
|
||||
this.router.navigate(['org']);
|
||||
}).catch(error => {
|
||||
this.toast.showError(error.message);
|
||||
this.toast.showError(error);
|
||||
});
|
||||
|
||||
break;
|
||||
@ -301,7 +301,7 @@ export class PasswordPolicyComponent implements OnInit, OnDestroy {
|
||||
).then(() => {
|
||||
this.router.navigate(['org']);
|
||||
}).catch(error => {
|
||||
this.toast.showError(error.message);
|
||||
this.toast.showError(error);
|
||||
});
|
||||
break;
|
||||
|
||||
@ -315,7 +315,7 @@ export class PasswordPolicyComponent implements OnInit, OnDestroy {
|
||||
).then(() => {
|
||||
this.router.navigate(['org']);
|
||||
}).catch(error => {
|
||||
this.toast.showError(error.message);
|
||||
this.toast.showError(error);
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
@ -54,21 +54,21 @@ export class PolicyGridComponent implements OnInit {
|
||||
this.orgService.DeletePasswordLockoutPolicy(this.lockoutPolicy.id).then(() => {
|
||||
this.toast.showInfo('Successfully deleted');
|
||||
}).catch(error => {
|
||||
this.toast.showError(error.message);
|
||||
this.toast.showError(error);
|
||||
});
|
||||
break;
|
||||
case PolicyComponentType.AGE:
|
||||
this.orgService.DeletePasswordAgePolicy(this.agePolicy.id).then(() => {
|
||||
this.toast.showInfo('Successfully deleted');
|
||||
}).catch(error => {
|
||||
this.toast.showError(error.message);
|
||||
this.toast.showError(error);
|
||||
});
|
||||
break;
|
||||
case PolicyComponentType.COMPLEXITY:
|
||||
this.orgService.DeletePasswordLockoutPolicy(this.lockoutPolicy.id).then(() => {
|
||||
this.toast.showInfo('Successfully deleted');
|
||||
}).catch(error => {
|
||||
this.toast.showError(error.message);
|
||||
this.toast.showError(error);
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
@ -36,7 +36,6 @@ export class SearchOrgAutocompleteComponent {
|
||||
}
|
||||
}).catch(error => {
|
||||
this.isLoading = false;
|
||||
// this.toast.showInfo(error.message);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -82,7 +82,6 @@ export class OwnedProjectDetailComponent implements OnInit, OnDestroy {
|
||||
|
||||
private async getData({ id }: Params): Promise<void> {
|
||||
this.projectId = id;
|
||||
console.log(this.projectId);
|
||||
|
||||
this.orgService.GetIam().then(iam => {
|
||||
this.isZitadel = iam.toObject().iamProjectId === this.projectId;
|
||||
@ -93,7 +92,7 @@ export class OwnedProjectDetailComponent implements OnInit, OnDestroy {
|
||||
this.project = proj.toObject();
|
||||
}).catch(error => {
|
||||
console.error(error);
|
||||
this.toast.showError(error.message);
|
||||
this.toast.showError(error);
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -103,13 +102,13 @@ export class OwnedProjectDetailComponent implements OnInit, OnDestroy {
|
||||
this.projectService.ReactivateProject(this.projectId).then(() => {
|
||||
this.toast.showInfo('Reactivated Project');
|
||||
}).catch(error => {
|
||||
this.toast.showError(error.message);
|
||||
this.toast.showError(error);
|
||||
});
|
||||
} else if (newState === ProjectState.PROJECTSTATE_INACTIVE) {
|
||||
this.projectService.DeactivateProject(this.projectId).then(() => {
|
||||
this.toast.showInfo('Deactivated Project');
|
||||
}).catch(error => {
|
||||
this.toast.showError(error.message);
|
||||
this.toast.showError(error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
<div class="view-toggle">
|
||||
<button [disabled]="selection.selected.length > 0" (click)="changedView.emit(true)" mat-icon-button>
|
||||
<button (click)="closeGridView()" mat-icon-button>
|
||||
<i class="show list view las la-th-list"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
@ -115,4 +115,8 @@ export class OwnedProjectGridComponent implements OnChanges {
|
||||
this.router.navigate(['/projects', id]);
|
||||
}
|
||||
}
|
||||
|
||||
public closeGridView(): void {
|
||||
this.changedView.emit(true);
|
||||
}
|
||||
}
|
||||
|
@ -95,7 +95,7 @@ export class OwnedProjectListComponent implements OnInit, OnDestroy {
|
||||
this.loadingSubject.next(false);
|
||||
}).catch(error => {
|
||||
console.error(error);
|
||||
this.toast.showError(error.message);
|
||||
this.toast.showError(error);
|
||||
this.loadingSubject.next(false);
|
||||
});
|
||||
|
||||
|
@ -35,7 +35,6 @@ export class ProjectGrantDetailDataSource extends DataSource<ProjectMemberView.A
|
||||
finalize(() => this.loadingSubject.next(false)),
|
||||
).subscribe(members => {
|
||||
this.membersSubject.next(members);
|
||||
console.log(members);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -25,8 +25,6 @@ export class ProjectGrantMembersDataSource extends DataSource<ProjectMember.AsOb
|
||||
|
||||
this.loadingSubject.next(true);
|
||||
|
||||
console.log(projectId, grantId);
|
||||
|
||||
from(this.projectService.SearchProjectGrantMembers(projectId,
|
||||
grantId, pageSize, offset)).pipe(
|
||||
map(resp => {
|
||||
|
@ -63,15 +63,14 @@ export class ProjectGrantMembersComponent implements AfterViewInit, OnInit {
|
||||
if (this.type === ProjectType.PROJECTTYPE_GRANTED) {
|
||||
this.projectService.GetProjectGrantMemberRoles().then(resp => {
|
||||
this.memberRoleOptions = resp.toObject().rolesList;
|
||||
console.log(this.memberRoleOptions);
|
||||
}).catch(error => {
|
||||
this.toast.showError(error.message);
|
||||
this.toast.showError(error);
|
||||
});
|
||||
} else if (this.type === ProjectType.PROJECTTYPE_OWNED) {
|
||||
this.projectService.GetProjectMemberRoles().then(resp => {
|
||||
this.memberRoleOptions = resp.toObject().rolesList;
|
||||
}).catch(error => {
|
||||
this.toast.showError(error.message);
|
||||
this.toast.showError(error);
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -90,7 +89,7 @@ export class ProjectGrantMembersComponent implements AfterViewInit, OnInit {
|
||||
return this.projectService.RemoveProjectGrantMember(this.projectId, this.grantId, member.userId).then(() => {
|
||||
this.toast.showInfo('Removed successfully');
|
||||
}).catch(error => {
|
||||
this.toast.showError(error.message);
|
||||
this.toast.showError(error);
|
||||
});
|
||||
}));
|
||||
}
|
||||
@ -99,7 +98,7 @@ export class ProjectGrantMembersComponent implements AfterViewInit, OnInit {
|
||||
this.projectService.RemoveProjectGrantMember(this.projectId, this.grantId, member.userId).then(() => {
|
||||
this.toast.showInfo('Member removed successfully');
|
||||
}).catch(error => {
|
||||
this.toast.showError(error.message);
|
||||
this.toast.showError(error);
|
||||
});
|
||||
}
|
||||
|
||||
@ -117,7 +116,6 @@ export class ProjectGrantMembersComponent implements AfterViewInit, OnInit {
|
||||
|
||||
public async openAddMember(): Promise<any> {
|
||||
const keysList = (await this.projectService.GetProjectGrantMemberRoles()).toObject();
|
||||
console.log(keysList);
|
||||
|
||||
const dialogRef = this.dialog.open(ProjectGrantMembersCreateDialogComponent, {
|
||||
data: {
|
||||
@ -127,7 +125,6 @@ export class ProjectGrantMembersComponent implements AfterViewInit, OnInit {
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe((dataToAdd: ProjectGrantMembersCreateDialogExportType) => {
|
||||
console.log(dataToAdd);
|
||||
if (dataToAdd) {
|
||||
dataToAdd.userIds.forEach((userid: string) => {
|
||||
this.projectService.AddProjectGrantMember(
|
||||
@ -138,7 +135,7 @@ export class ProjectGrantMembersComponent implements AfterViewInit, OnInit {
|
||||
).then(() => {
|
||||
this.toast.showInfo('Project Grant Member successfully added!');
|
||||
}).catch(error => {
|
||||
this.toast.showError(error.message);
|
||||
this.toast.showError(error);
|
||||
});
|
||||
});
|
||||
|
||||
@ -147,13 +144,11 @@ export class ProjectGrantMembersComponent implements AfterViewInit, OnInit {
|
||||
}
|
||||
|
||||
updateRoles(member: ProjectMember.AsObject, selectionChange: MatSelectChange): void {
|
||||
console.log(this.projectId, this.grantId, member.userId, selectionChange.value);
|
||||
this.projectService.ChangeProjectGrantMember(this.projectId, this.grantId, member.userId, selectionChange.value)
|
||||
.then((newmember: ProjectMember) => {
|
||||
console.log(newmember.toObject());
|
||||
this.toast.showInfo('Member updated!');
|
||||
}).catch(error => {
|
||||
this.toast.showError(error.message);
|
||||
this.toast.showError(error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -43,20 +43,16 @@ export class ProjectGrantCreateComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
public searchOrg(domain: string): void {
|
||||
console.log(domain);
|
||||
this.orgService.getOrgByDomainGlobal(domain).then((ret) => {
|
||||
const tmp = ret.toObject();
|
||||
console.log(ret.toObject());
|
||||
this.authService.GetActiveOrg().then((org) => {
|
||||
console.log(org);
|
||||
if (tmp !== org) {
|
||||
this.org = tmp;
|
||||
}
|
||||
});
|
||||
this.org = ret.toObject();
|
||||
console.log(this.org);
|
||||
}).catch(error => {
|
||||
this.toast.showError(error.message);
|
||||
this.toast.showError(error);
|
||||
});
|
||||
}
|
||||
|
||||
@ -71,7 +67,7 @@ export class ProjectGrantCreateComponent implements OnInit, OnDestroy {
|
||||
this.close();
|
||||
})
|
||||
.catch(error => {
|
||||
this.toast.showError(error.message);
|
||||
this.toast.showError(error);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -1,9 +1,10 @@
|
||||
<div class="wrap">
|
||||
<div class="block">
|
||||
<div class="header">
|
||||
<img *ngIf="dark; else lighttheme" src="./assets/images/zitadel-logo-oneline-darkdesign.svg" />
|
||||
<img alt="zitadel logo" *ngIf="dark; else lighttheme"
|
||||
src="../../../assets/images/zitadel-logo-oneline-darkdesign.svg" />
|
||||
<ng-template #lighttheme>
|
||||
<img src="./assets/images/zitadel-logo-oneline-lightdesign.svg" />
|
||||
<img alt="zitadel logo" src="../../../assets/images/zitadel-logo-oneline-lightdesign.svg" />
|
||||
</ng-template>
|
||||
<p>{{'USER.SIGNEDOUT' | translate}}</p>
|
||||
|
||||
@ -11,4 +12,4 @@
|
||||
[routerLink]="[ '/users/me' ]">{{'USER.SIGNEDOUT_BTN' | translate}}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -86,7 +86,7 @@ export class UserCreateComponent implements OnDestroy {
|
||||
this.userService
|
||||
.CreateUser(this.user)
|
||||
.then((data: User) => {
|
||||
this.toast.showInfo('User created');
|
||||
this.toast.showInfo('USER.TOAST.CREATED', true);
|
||||
this.router.navigate(['users', data.getId()]);
|
||||
})
|
||||
.catch(data => {
|
||||
|
@ -21,8 +21,7 @@
|
||||
</app-card>
|
||||
|
||||
<div class="col" *ngIf="user">
|
||||
<app-card title="{{ 'USER.PROFILE.TITLE' | translate }}"
|
||||
description="{{'USER.PROFILE.DESCRIPTION' | translate}}">
|
||||
<app-card title="{{ 'USER.PROFILE.TITLE' | translate }}">
|
||||
<app-detail-form [genders]="genders" [languages]="languages" [profile]="user"
|
||||
(changedLanguage)="changedLanguage($event)" (submitData)="saveProfile($event)">
|
||||
</app-detail-form>
|
||||
|
@ -66,7 +66,7 @@ export class AuthUserDetailComponent implements OnDestroy {
|
||||
this.user.gender,
|
||||
)
|
||||
.then((data: UserProfile) => {
|
||||
this.toast.showInfo('Saved Profile');
|
||||
this.toast.showInfo('USER.TOAST.SAVED', true);
|
||||
this.user = Object.assign(this.user, data.toObject());
|
||||
})
|
||||
.catch(data => {
|
||||
@ -79,7 +79,7 @@ export class AuthUserDetailComponent implements OnDestroy {
|
||||
|
||||
this.userService
|
||||
.SaveMyUserEmail(this.user.email).then((data: UserEmail) => {
|
||||
this.toast.showInfo('Saved Email');
|
||||
this.toast.showInfo('USER.TOAST.EMAILSAVED', true);
|
||||
this.user.email = data.toObject().email;
|
||||
this.emailEditState = false;
|
||||
}).catch(data => {
|
||||
@ -99,9 +99,9 @@ export class AuthUserDetailComponent implements OnDestroy {
|
||||
dialogRef.afterClosed().subscribe(code => {
|
||||
if (code) {
|
||||
this.userService.VerifyMyUserPhone(code).then(() => {
|
||||
this.toast.showInfo('Verified Phone');
|
||||
this.toast.showInfo('USER.TOAST.PHONESAVED', true);
|
||||
}).catch(error => {
|
||||
this.toast.showError(error.message);
|
||||
this.toast.showError(error);
|
||||
});
|
||||
}
|
||||
});
|
||||
@ -113,7 +113,7 @@ export class AuthUserDetailComponent implements OnDestroy {
|
||||
|
||||
public resendVerification(): void {
|
||||
this.userService.ResendEmailVerification().then(() => {
|
||||
this.toast.showInfo('Saved Email');
|
||||
this.toast.showInfo('USER.TOAST.EMAILSAVED', true);
|
||||
}).catch(data => {
|
||||
this.toast.showError(data.message);
|
||||
});
|
||||
@ -121,7 +121,7 @@ export class AuthUserDetailComponent implements OnDestroy {
|
||||
|
||||
public resendPhoneVerification(): void {
|
||||
this.userService.ResendPhoneVerification().then(() => {
|
||||
this.toast.showInfo('Phoneverification was successfully sent!');
|
||||
this.toast.showInfo('USER.TOAST.PHONEVERIFICATIONSENT', true);
|
||||
}).catch(data => {
|
||||
this.toast.showError(data.message);
|
||||
});
|
||||
@ -129,7 +129,7 @@ export class AuthUserDetailComponent implements OnDestroy {
|
||||
|
||||
public deletePhone(): void {
|
||||
this.userService.RemoveMyUserPhone().then(() => {
|
||||
this.toast.showInfo('Phone removed with success!');
|
||||
this.toast.showInfo('USER.TOAST.PHONEREMOVED', true);
|
||||
this.user.phone = '';
|
||||
this.phoneEditState = false;
|
||||
}).catch(data => {
|
||||
@ -141,7 +141,7 @@ export class AuthUserDetailComponent implements OnDestroy {
|
||||
this.phoneEditState = false;
|
||||
this.userService
|
||||
.SaveMyUserPhone(this.user.phone).then((data: UserPhone) => {
|
||||
this.toast.showInfo('Saved Phone');
|
||||
this.toast.showInfo('USER.TOAST.PHONESAVED', true);
|
||||
this.user.phone = data.toObject().phone;
|
||||
this.phoneEditState = false;
|
||||
}).catch(data => {
|
||||
@ -154,7 +154,7 @@ export class AuthUserDetailComponent implements OnDestroy {
|
||||
this.userService.GetMyUser().then(user => {
|
||||
this.user = user.toObject();
|
||||
}).catch(err => {
|
||||
console.error(err);
|
||||
this.toast.showError(err);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -48,7 +48,7 @@ export class AuthUserMfaComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
});
|
||||
}, error => {
|
||||
this.toast.showError(error.message);
|
||||
this.toast.showError(error);
|
||||
});
|
||||
}
|
||||
|
||||
@ -64,7 +64,7 @@ export class AuthUserMfaComponent implements OnInit, OnDestroy {
|
||||
public deleteMFA(type: MfaType): void {
|
||||
if (type === MfaType.MFATYPE_OTP) {
|
||||
this.userService.RemoveMfaOTP().then(() => {
|
||||
this.toast.showInfo('OTP Deleted');
|
||||
this.toast.showInfo('USER.TOAST.OTPREMOVED', true);
|
||||
|
||||
const index = this.mfaSubject.value.findIndex(mfa => mfa.type === type);
|
||||
if (index > -1) {
|
||||
@ -74,7 +74,7 @@ export class AuthUserMfaComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
}).catch(error => {
|
||||
this.toast.showError(error.message);
|
||||
this.toast.showError(error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +1,9 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { AbstractControl, FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { PasswordComplexityPolicy } from 'src/app/proto/generated/management_pb';
|
||||
import { PasswordComplexityPolicy } from 'src/app/proto/generated/auth_pb';
|
||||
import { AuthUserService } from 'src/app/services/auth-user.service';
|
||||
import { MgmtUserService } from 'src/app/services/mgmt-user.service';
|
||||
import { OrgService } from 'src/app/services/org.service';
|
||||
import { ToastService } from 'src/app/services/toast.service';
|
||||
|
||||
import { lowerCaseValidator, numberValidator, symbolValidator, upperCaseValidator } from '../validators';
|
||||
@ -36,7 +35,6 @@ export class PasswordComponent implements OnInit {
|
||||
public passwordForm!: FormGroup;
|
||||
|
||||
constructor(
|
||||
private orgService: OrgService,
|
||||
activatedRoute: ActivatedRoute,
|
||||
private fb: FormBuilder,
|
||||
private userService: AuthUserService,
|
||||
@ -51,7 +49,7 @@ export class PasswordComponent implements OnInit {
|
||||
}
|
||||
|
||||
const validators: Validators[] = [Validators.required];
|
||||
this.orgService.GetPasswordComplexityPolicy().then(complexity => {
|
||||
this.userService.GetMyPasswordComplexityPolicy().then(complexity => {
|
||||
this.policy = complexity.toObject();
|
||||
if (this.policy.minLength) {
|
||||
validators.push(Validators.minLength(this.policy.minLength));
|
||||
@ -97,7 +95,7 @@ export class PasswordComponent implements OnInit {
|
||||
public setInitialPassword(userId: string): void {
|
||||
if (this.passwordForm.valid && this.password && this.password.value) {
|
||||
this.mgmtUserService.SetInitialPassword(userId, this.password.value).then((data: any) => {
|
||||
this.toast.showInfo('Set initial Password');
|
||||
this.toast.showInfo('USER.TOAST.INITIALPASSWORDSET', true);
|
||||
window.history.back();
|
||||
}).catch(data => {
|
||||
this.toast.showError(data.message);
|
||||
@ -111,7 +109,7 @@ export class PasswordComponent implements OnInit {
|
||||
this.newPassword && this.newPassword.value && this.newPassword.valid) {
|
||||
this.userService
|
||||
.ChangeMyPassword(this.currentPassword.value, this.newPassword.value).then((data: any) => {
|
||||
this.toast.showInfo('Password Set');
|
||||
this.toast.showInfo('USER.TOAST.PASSWORDCHANGED', true);
|
||||
window.history.back();
|
||||
}).catch(data => {
|
||||
this.toast.showError(data.message);
|
||||
|
@ -16,8 +16,6 @@
|
||||
class="state-button"
|
||||
(click)="changeState(UserState.USERSTATE_ACTIVE)">{{'USER.PAGES.REACTIVATE' | translate}}</button>
|
||||
</ng-template>
|
||||
|
||||
<p class="desc">{{ 'USER.PROFILE.DESCRIPTION' | translate }}</p>
|
||||
</div>
|
||||
|
||||
<mat-progress-bar *ngIf="loading" color="accent" mode="indeterminate"></mat-progress-bar>
|
||||
|
@ -3,23 +3,18 @@
|
||||
align-items: center;
|
||||
border-bottom: 1px solid #ffffff20;
|
||||
flex-wrap: wrap;
|
||||
padding-bottom: .5rem;
|
||||
|
||||
a {
|
||||
display: block;
|
||||
margin-right: 2rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2rem;
|
||||
margin: 0 1rem;
|
||||
margin-left: 2rem;
|
||||
margin: 0;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.desc {
|
||||
width: 100%;
|
||||
display: block;
|
||||
font-size: .9rem;
|
||||
color: #8795a1;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
.fill-space {
|
||||
|
@ -65,15 +65,15 @@ export class UserDetailComponent implements OnInit, OnDestroy {
|
||||
public changeState(newState: UserState): void {
|
||||
if (newState === UserState.USERSTATE_ACTIVE) {
|
||||
this.mgmtUserService.ReactivateUser(this.user.id).then(() => {
|
||||
this.toast.showInfo('reactivated User');
|
||||
this.toast.showInfo('USER.TOAST.REACTIVATED', true);
|
||||
}).catch(error => {
|
||||
this.toast.showError(error.message);
|
||||
this.toast.showError(error);
|
||||
});
|
||||
} else if (newState === UserState.USERSTATE_INACTIVE) {
|
||||
this.mgmtUserService.DeactivateUser(this.user.id).then(() => {
|
||||
this.toast.showInfo('deactivated User');
|
||||
this.toast.showInfo('USER.TOAST.DEACTIVATED', true);
|
||||
}).catch(error => {
|
||||
this.toast.showError(error.message);
|
||||
this.toast.showError(error);
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -94,7 +94,7 @@ export class UserDetailComponent implements OnInit, OnDestroy {
|
||||
this.user.preferredLanguage,
|
||||
this.user.gender)
|
||||
.then((data: UserProfile) => {
|
||||
this.toast.showInfo('Saved Profile');
|
||||
this.toast.showInfo('USER.TOAST.SAVED', true);
|
||||
this.user = Object.assign(this.user, data.toObject());
|
||||
})
|
||||
.catch(data => {
|
||||
@ -104,7 +104,7 @@ export class UserDetailComponent implements OnInit, OnDestroy {
|
||||
|
||||
public resendVerification(): void {
|
||||
this.mgmtUserService.ResendEmailVerification(this.user.id).then(() => {
|
||||
this.toast.showInfo('Email was successfully sent!');
|
||||
this.toast.showInfo('USER.TOAST.EMAILVERIFICATIONSENT', true);
|
||||
}).catch(data => {
|
||||
this.toast.showError(data.message);
|
||||
});
|
||||
@ -112,7 +112,7 @@ export class UserDetailComponent implements OnInit, OnDestroy {
|
||||
|
||||
public resendPhoneVerification(): void {
|
||||
this.mgmtUserService.ResendPhoneVerification(this.user.id).then(() => {
|
||||
this.toast.showInfo('Phoneverification was successfully sent!');
|
||||
this.toast.showInfo('USER.TOAST.PHONEVERIFICATIONSENT', true);
|
||||
}).catch(data => {
|
||||
this.toast.showError(data.message);
|
||||
});
|
||||
@ -120,7 +120,7 @@ export class UserDetailComponent implements OnInit, OnDestroy {
|
||||
|
||||
public deletePhone(): void {
|
||||
this.mgmtUserService.RemoveUserPhone(this.user.id).then(() => {
|
||||
this.toast.showInfo('Phone removed with success!');
|
||||
this.toast.showInfo('USER.TOAST.PHONEREMOVED', true);
|
||||
this.user.phone = '';
|
||||
this.phoneEditState = false;
|
||||
}).catch(data => {
|
||||
@ -132,7 +132,7 @@ export class UserDetailComponent implements OnInit, OnDestroy {
|
||||
this.emailEditState = false;
|
||||
this.mgmtUserService
|
||||
.SaveUserEmail(this.user.id, this.user.email).then((data: UserEmail) => {
|
||||
this.toast.showInfo('Saved Email');
|
||||
this.toast.showInfo('USER.TOAST.EMAILSENT', true);
|
||||
this.user.email = data.toObject().email;
|
||||
}).catch(data => {
|
||||
this.toast.showError(data.message);
|
||||
@ -143,7 +143,7 @@ export class UserDetailComponent implements OnInit, OnDestroy {
|
||||
this.phoneEditState = false;
|
||||
this.mgmtUserService
|
||||
.SaveUserPhone(this.user.id, this.user.phone).then((data: UserPhone) => {
|
||||
this.toast.showInfo('Saved Phone');
|
||||
this.toast.showInfo('USER.TOAST.PHONESAVED', true);
|
||||
this.user.phone = data.toObject().phone;
|
||||
this.phoneEditState = false;
|
||||
}).catch(data => {
|
||||
@ -167,7 +167,7 @@ export class UserDetailComponent implements OnInit, OnDestroy {
|
||||
public sendSetPasswordNotification(): void {
|
||||
this.mgmtUserService.SendSetPasswordNotification(this.user.id, NotificationType.NOTIFICATIONTYPE_EMAIL)
|
||||
.then(() => {
|
||||
this.toast.showInfo('Set initial Password');
|
||||
this.toast.showInfo('USER.TOAST.PASSWORDNOTIFICATIONSENT', true);
|
||||
}).catch(data => {
|
||||
this.toast.showError(data.message);
|
||||
});
|
||||
|
@ -39,7 +39,6 @@ export class UserGrantCreateComponent implements OnDestroy {
|
||||
private route: ActivatedRoute,
|
||||
) {
|
||||
this.subscription = this.route.params.subscribe((params: Params) => {
|
||||
console.log(params);
|
||||
const { context, projectid, grantid, userid } = params;
|
||||
this.context = context;
|
||||
|
||||
@ -76,7 +75,7 @@ export class UserGrantCreateComponent implements OnDestroy {
|
||||
this.toast.showInfo('User Grant added');
|
||||
this.close();
|
||||
}).catch(error => {
|
||||
this.toast.showError(error.message);
|
||||
this.toast.showError(error);
|
||||
});
|
||||
break;
|
||||
case UserGrantContext.OWNED_PROJECT:
|
||||
@ -88,7 +87,7 @@ export class UserGrantCreateComponent implements OnDestroy {
|
||||
this.toast.showInfo('Project User Grant added');
|
||||
this.close();
|
||||
}).catch(error => {
|
||||
this.toast.showError(error.message);
|
||||
this.toast.showError(error);
|
||||
});
|
||||
break;
|
||||
case UserGrantContext.GRANTED_PROJECT:
|
||||
@ -102,7 +101,7 @@ export class UserGrantCreateComponent implements OnDestroy {
|
||||
this.toast.showInfo('Project Grant User Grant added');
|
||||
this.close();
|
||||
}).catch(error => {
|
||||
this.toast.showError(error.message);
|
||||
this.toast.showError(error);
|
||||
});
|
||||
break;
|
||||
|
||||
@ -111,12 +110,10 @@ export class UserGrantCreateComponent implements OnDestroy {
|
||||
}
|
||||
|
||||
public selectProject(project: ProjectView.AsObject | ProjectGrantView.AsObject | any): void {
|
||||
console.log(project);
|
||||
this.projectId = project.projectId;
|
||||
}
|
||||
|
||||
public selectUser(user: User.AsObject): void {
|
||||
console.log(user);
|
||||
this.userId = user.id;
|
||||
}
|
||||
|
||||
|
@ -79,7 +79,7 @@ export class UserListComponent implements OnDestroy {
|
||||
this.dataSource.data = resp.toObject().resultList;
|
||||
this.loadingSubject.next(false);
|
||||
}).catch(error => {
|
||||
this.toast.showError(error.message);
|
||||
this.toast.showError(error);
|
||||
this.loadingSubject.next(false);
|
||||
});
|
||||
}
|
||||
|
@ -1,15 +0,0 @@
|
||||
import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class AuthInterceptorService implements HttpInterceptor {
|
||||
|
||||
constructor() { }
|
||||
|
||||
public intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
|
||||
return next.handle(req);
|
||||
}
|
||||
}
|
@ -2,7 +2,7 @@ import { Injectable } from '@angular/core';
|
||||
import { Empty } from 'google-protobuf/google/protobuf/empty_pb';
|
||||
import { Metadata } from 'grpc-web';
|
||||
import { from, Observable, of } from 'rxjs';
|
||||
import { switchMap } from 'rxjs/operators';
|
||||
import { catchError, switchMap } from 'rxjs/operators';
|
||||
|
||||
import { AuthServicePromiseClient } from '../proto/generated/auth_grpc_web_pb';
|
||||
import {
|
||||
@ -15,6 +15,7 @@ import {
|
||||
MyProjectOrgSearchRequest,
|
||||
MyProjectOrgSearchResponse,
|
||||
PasswordChange,
|
||||
PasswordComplexityPolicy,
|
||||
UpdateUserAddressRequest,
|
||||
UpdateUserEmailRequest,
|
||||
UpdateUserPhoneRequest,
|
||||
@ -23,6 +24,7 @@ import {
|
||||
UserEmail,
|
||||
UserPhone,
|
||||
UserProfile,
|
||||
UserProfileView,
|
||||
UserSessionViews,
|
||||
UserView,
|
||||
VerifyMfaOtp,
|
||||
@ -57,7 +59,7 @@ export class AuthUserService {
|
||||
return responseMapper(response);
|
||||
}
|
||||
|
||||
public async GetMyUserProfile(): Promise<UserProfile> {
|
||||
public async GetMyUserProfile(): Promise<UserProfileView> {
|
||||
return await this.request(
|
||||
c => c.getMyUserProfile,
|
||||
new Empty(),
|
||||
@ -65,6 +67,15 @@ export class AuthUserService {
|
||||
);
|
||||
}
|
||||
|
||||
public async GetMyPasswordComplexityPolicy(): Promise<PasswordComplexityPolicy> {
|
||||
return await this.request(
|
||||
c => c.getMyPasswordComplexityPolicy,
|
||||
new Empty(),
|
||||
f => f,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
public async GetMyUser(): Promise<UserView> {
|
||||
return await this.request(
|
||||
c => c.getMyUser,
|
||||
@ -319,6 +330,9 @@ export class AuthUserService {
|
||||
this._roleCache = userRoles;
|
||||
return of(this.hasRoles(userRoles, roles, each));
|
||||
}),
|
||||
catchError((err) => {
|
||||
return of(false);
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
return of(false);
|
||||
|
@ -4,7 +4,7 @@ import { AuthConfig, OAuthService } from 'angular-oauth2-oidc';
|
||||
import { BehaviorSubject, from, merge, Observable, of, Subject } from 'rxjs';
|
||||
import { catchError, filter, map, mergeMap, take, timeout } from 'rxjs/operators';
|
||||
|
||||
import { Org, UserProfile } from '../proto/generated/auth_pb';
|
||||
import { Org, UserProfileView } from '../proto/generated/auth_pb';
|
||||
import { AuthUserService } from './auth-user.service';
|
||||
import { GrpcService } from './grpc.service';
|
||||
import { StatehandlerService } from './statehandler.service';
|
||||
@ -16,7 +16,7 @@ import { StorageKey, StorageService } from './storage.service';
|
||||
export class AuthService {
|
||||
private cachedOrgs: Org.AsObject[] = [];
|
||||
private _activeOrgChanged: Subject<Org.AsObject> = new Subject();
|
||||
public user!: Observable<UserProfile.AsObject>;
|
||||
public user!: Observable<UserProfileView.AsObject>;
|
||||
private _authenticated: boolean = false;
|
||||
private readonly _authenticationChanged: BehaviorSubject<
|
||||
boolean
|
||||
@ -61,7 +61,11 @@ export class AuthService {
|
||||
return from(this.oauthService.loadUserProfile());
|
||||
}
|
||||
|
||||
public async authenticate(config?: Partial<AuthConfig>, setState: boolean = true): Promise<boolean> {
|
||||
public async authenticate(
|
||||
config?: Partial<AuthConfig>,
|
||||
setState: boolean = true,
|
||||
force: boolean = false,
|
||||
): Promise<boolean> {
|
||||
this.config.issuer = config?.issuer || this.grpcService.issuer;
|
||||
this.config.clientId = config?.clientId || this.grpcService.clientid;
|
||||
this.config.redirectUri = config?.redirectUri || this.grpcService.redirectUri;
|
||||
@ -73,7 +77,8 @@ export class AuthService {
|
||||
await this.oauthService.loadDiscoveryDocumentAndTryLogin();
|
||||
|
||||
this._authenticated = this.oauthService.hasValidAccessToken();
|
||||
if (!this.oauthService.hasValidIdToken() || !this.authenticated || config) {
|
||||
|
||||
if (!this.oauthService.hasValidIdToken() || !this.authenticated || config || force) {
|
||||
const newState = setState ? await this.statehandler.createState().toPromise() : undefined;
|
||||
this.oauthService.initCodeFlow(newState);
|
||||
}
|
||||
@ -89,7 +94,6 @@ export class AuthService {
|
||||
this.router.navigate(['/']);
|
||||
}
|
||||
|
||||
|
||||
public get activeOrgChanged(): Observable<Org.AsObject> {
|
||||
return this._activeOrgChanged;
|
||||
}
|
||||
|
@ -1,31 +1,27 @@
|
||||
import { Inject, Injectable } from '@angular/core';
|
||||
import { Metadata } from 'grpc-web';
|
||||
import {
|
||||
GrpcDefaultHandler,
|
||||
GrpcHandler,
|
||||
GrpcInterceptorHandler,
|
||||
GrpcRequestFn,
|
||||
} from './grpc-handler';
|
||||
import { GRPC_INTERCEPTORS, GrpcInterceptor } from './grpc-interceptor';
|
||||
|
||||
import { GrpcDefaultHandler, GrpcHandler, GrpcInterceptorHandler, GrpcRequestFn } from './grpc-handler';
|
||||
import { GRPC_INTERCEPTORS, GrpcInterceptor } from './interceptors/grpc-interceptor';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class GrpcBackendService {
|
||||
constructor(
|
||||
@Inject(GRPC_INTERCEPTORS) private readonly interceptors: GrpcInterceptor[],
|
||||
) {}
|
||||
constructor(
|
||||
@Inject(GRPC_INTERCEPTORS) private readonly interceptors: GrpcInterceptor[],
|
||||
) { }
|
||||
|
||||
public async runRequest<TReq, TResp>(
|
||||
requestFn: GrpcRequestFn<TReq, TResp>,
|
||||
request: TReq,
|
||||
metadata: Metadata = {},
|
||||
): Promise<TResp> {
|
||||
const defaultHandler: GrpcHandler = new GrpcDefaultHandler(requestFn);
|
||||
const chain = this.interceptors.reduceRight(
|
||||
(next, interceptor) => new GrpcInterceptorHandler(next, interceptor),
|
||||
defaultHandler,
|
||||
);
|
||||
return chain.handle(request, metadata);
|
||||
}
|
||||
public async runRequest<TReq, TResp>(
|
||||
requestFn: GrpcRequestFn<TReq, TResp>,
|
||||
request: TReq,
|
||||
metadata: Metadata = {},
|
||||
): Promise<TResp> {
|
||||
const defaultHandler: GrpcHandler = new GrpcDefaultHandler(requestFn);
|
||||
const chain = this.interceptors.reduceRight(
|
||||
(next, interceptor) => new GrpcInterceptorHandler(next, interceptor),
|
||||
defaultHandler,
|
||||
);
|
||||
return chain.handle(request, metadata);
|
||||
}
|
||||
}
|
||||
|
@ -1,31 +1,31 @@
|
||||
import { Metadata } from 'grpc-web';
|
||||
|
||||
import { GrpcInterceptor } from './grpc-interceptor';
|
||||
import { GrpcInterceptor } from './interceptors/grpc-interceptor';
|
||||
|
||||
export type GrpcRequestFn<TReq, TResp> = (
|
||||
request: TReq,
|
||||
metadata?: Metadata,
|
||||
request: TReq,
|
||||
metadata?: Metadata,
|
||||
) => Promise<TResp>;
|
||||
|
||||
export interface GrpcHandler {
|
||||
handle(req: unknown, metadata: Metadata): Promise<any>;
|
||||
handle(req: unknown, metadata: Metadata): Promise<any>;
|
||||
}
|
||||
|
||||
export class GrpcInterceptorHandler implements GrpcHandler {
|
||||
constructor(
|
||||
private readonly next: GrpcHandler,
|
||||
private interceptor: GrpcInterceptor,
|
||||
) {}
|
||||
constructor(
|
||||
private readonly next: GrpcHandler,
|
||||
private interceptor: GrpcInterceptor,
|
||||
) { }
|
||||
|
||||
public handle(req: unknown, metadata: Metadata): Promise<unknown> {
|
||||
return this.interceptor.intercept(req, metadata, this.next);
|
||||
}
|
||||
public handle(req: unknown, metadata: Metadata): Promise<unknown> {
|
||||
return this.interceptor.intercept(req, metadata, this.next);
|
||||
}
|
||||
}
|
||||
|
||||
export class GrpcDefaultHandler<TReq, TResp> implements GrpcHandler {
|
||||
constructor(private readonly requestFn: GrpcRequestFn<TReq, TResp>) {}
|
||||
constructor(private readonly requestFn: GrpcRequestFn<TReq, TResp>) { }
|
||||
|
||||
public handle(req: TReq, metadata: Metadata): Promise<TResp> {
|
||||
return this.requestFn(req, metadata);
|
||||
}
|
||||
public handle(req: TReq, metadata: Metadata): Promise<TResp> {
|
||||
return this.requestFn(req, metadata);
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Metadata } from 'grpc-web';
|
||||
|
||||
import { GrpcHandler } from './grpc-handler';
|
||||
import { GrpcHandler } from '../grpc-handler';
|
||||
import { StorageService } from '../storage.service';
|
||||
import { GrpcInterceptor } from './grpc-interceptor';
|
||||
import { StorageService } from './storage.service';
|
||||
|
||||
const authorizationKey = 'Authorization';
|
||||
const bearerPrefix = 'Bearer ';
|
@ -1,11 +1,12 @@
|
||||
import { InjectionToken } from '@angular/core';
|
||||
import { Metadata } from 'grpc-web';
|
||||
import { GrpcHandler } from './grpc-handler';
|
||||
|
||||
import { GrpcHandler } from '../grpc-handler';
|
||||
|
||||
export const GRPC_INTERCEPTORS = new InjectionToken<GrpcInterceptor[]>(
|
||||
'GRPC_INTERCEPTORS',
|
||||
'GRPC_INTERCEPTORS',
|
||||
);
|
||||
|
||||
export interface GrpcInterceptor {
|
||||
intercept(req: unknown, metadata: Metadata, next: GrpcHandler): Promise<any>;
|
||||
intercept(req: unknown, metadata: Metadata, next: GrpcHandler): Promise<any>;
|
||||
}
|
@ -1,10 +1,10 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Metadata } from 'grpc-web';
|
||||
|
||||
import { Org } from '../proto/generated/auth_pb';
|
||||
import { GrpcHandler } from './grpc-handler';
|
||||
import { Org } from '../../proto/generated/auth_pb';
|
||||
import { GrpcHandler } from '../grpc-handler';
|
||||
import { StorageService } from '../storage.service';
|
||||
import { GrpcInterceptor } from './grpc-interceptor';
|
||||
import { StorageService } from './storage.service';
|
||||
|
||||
const orgKey = 'x-zitadel-orgid';
|
||||
|
@ -1,23 +1,46 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { MatSnackBar, MatSnackBarConfig } from '@angular/material/snack-bar';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import { take } from 'rxjs/operators';
|
||||
|
||||
import { AuthService } from './auth.service';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class ToastService {
|
||||
constructor(private snackBar: MatSnackBar) { }
|
||||
constructor(private snackBar: MatSnackBar, private translate: TranslateService, private authService: AuthService) { }
|
||||
|
||||
public showInfo(message: string): void {
|
||||
this.showMessage(message, 'close');
|
||||
public showInfo(message: string, i18nkey: boolean = false): void {
|
||||
if (i18nkey) {
|
||||
this.translate
|
||||
.get(message)
|
||||
.subscribe(data => {
|
||||
this.showMessage(data, 'close');
|
||||
});
|
||||
} else {
|
||||
this.showMessage(message, 'close');
|
||||
}
|
||||
}
|
||||
|
||||
public showError(message: string): void {
|
||||
this.showMessage(decodeURI(message), 'close');
|
||||
public showError(grpcError: any): void {
|
||||
const { message, code, metadata } = grpcError;
|
||||
// TODO: remove check for code === 7
|
||||
if (code === 16 || (code === 7 && message === 'invalid token')) {
|
||||
const actionObserver$ = this.showMessage(decodeURI(message), 'Login');
|
||||
|
||||
actionObserver$.pipe(take(1)).subscribe(() => {
|
||||
this.authService.authenticate(undefined, true, true);
|
||||
});
|
||||
} else {
|
||||
const actionObserver$ = this.showMessage(decodeURI(message), 'close', { duration: 5000 });
|
||||
}
|
||||
}
|
||||
|
||||
private showMessage(message: string, action: string): void {
|
||||
this.snackBar.open(message, action, {
|
||||
duration: 5000,
|
||||
});
|
||||
private showMessage(message: string, action: string, config?: MatSnackBarConfig): Observable<void> {
|
||||
const ref = this.snackBar.open(message, action, config);
|
||||
|
||||
return ref.onAction();
|
||||
}
|
||||
}
|
||||
|
@ -7,8 +7,6 @@ import { SwUpdate } from '@angular/service-worker';
|
||||
})
|
||||
export class UpdateService {
|
||||
constructor(private swUpdate: SwUpdate, snackbar: MatSnackBar) {
|
||||
this.swUpdate.checkForUpdate();
|
||||
|
||||
this.swUpdate.available.subscribe((evt) => {
|
||||
const snack = snackbar.open('Update Available', 'Reload');
|
||||
|
||||
|
@ -191,7 +191,23 @@
|
||||
"SIGNEDOUT":"Du bist abgemeldet. Klicke auf den Button um dich erneut anzumelden!",
|
||||
"SIGNEDOUT_BTN":"Anmelden",
|
||||
"EDITACCOUNT":"Account bearbeiten",
|
||||
"ADDACCOUNT":"Account hinzufügen"
|
||||
"ADDACCOUNT":"Account hinzufügen",
|
||||
"TOAST": {
|
||||
"CREATED":"User erfolgreich erstellt",
|
||||
"SAVED":"Profile gespeichert!",
|
||||
"EMAILSAVED":"Email gespeichert!",
|
||||
"PHONESAVED":"Telefonnummer gespeichert!",
|
||||
"PHONEREMOVED":"Telefonnummer gelöscht!",
|
||||
"PHONEVERIFIED":"Telefonnummer bestätigt!",
|
||||
"PHONEVERIFICATIONSENT":"Tel. Bestätigungscode gesendet!",
|
||||
"EMAILVERIFICATIONSENT":"Email Bestätigungscode gesendet!",
|
||||
"OTPREMOVED":"OTP entfernt!",
|
||||
"INITIALPASSWORDSET":"Initiales Passwort gesetzt!",
|
||||
"PASSWORDNOTIFICATIONSENT":"Passwortänderung mittgeteilt!",
|
||||
"PASSWORDCHANGED":"Passwort geändert!",
|
||||
"REACTIVATED":"User reaktiviert!",
|
||||
"DEACTIVATED":"User deaktiviert!"
|
||||
}
|
||||
},
|
||||
"IAM": {
|
||||
"DETAIL": {
|
||||
@ -437,6 +453,13 @@
|
||||
"CREATIONDATE":"Erstelldatum",
|
||||
"CHANGEDATE":"Letzte Änderung",
|
||||
"RESOURCEOWNER":"Besitzer"
|
||||
},
|
||||
"TOAST":{
|
||||
"MEMBERREMOVED":"Manager entfernt!",
|
||||
"MEMBERSADDED": "Manager hinzugefügt!",
|
||||
"MEMBERADDED": "Manager hinzugefügt!",
|
||||
"ROLEREMOVED":"Rolle entfernt!",
|
||||
"ROLECHANGED":"Rolle verändert!"
|
||||
}
|
||||
},
|
||||
"APP": {
|
||||
@ -480,6 +503,12 @@
|
||||
"AUTHMETHOD0":"Basic",
|
||||
"AUTHMETHOD1":"Post",
|
||||
"AUTHMETHOD2":"None"
|
||||
},
|
||||
"TOAST": {
|
||||
"REACTIVATED":"App reaktiviert!",
|
||||
"DEACTIVATED":"App deaktiviert!",
|
||||
"OIDCUPDATED":"OIDC Konfiguration geändert!",
|
||||
"OIDCCLIENTSECRETREGENERATED":"OIDC Client Secret generiert!"
|
||||
}
|
||||
},
|
||||
"GENDERS": {
|
||||
@ -555,6 +584,10 @@
|
||||
"DETAIL": {
|
||||
"TITLE":"Authorisierung Detail",
|
||||
"DESCRIPTION":""
|
||||
},
|
||||
"TOAST": {
|
||||
"UPDATED":"Berechtigung geändert!",
|
||||
"BULKREMOVED":"Berechtigungen entfernt!"
|
||||
}
|
||||
},
|
||||
"CHANGES": {
|
||||
|
@ -191,7 +191,23 @@
|
||||
"SIGNEDOUT":"You are signed out. Click the button below to sign in again!",
|
||||
"SIGNEDOUT_BTN":"Sign In",
|
||||
"EDITACCOUNT":"Edit Account",
|
||||
"ADDACCOUNT":"log in with another account"
|
||||
"ADDACCOUNT":"Log in with another account",
|
||||
"TOAST": {
|
||||
"CREATED":"User created successful!",
|
||||
"SAVED":"Profile saved successful!",
|
||||
"EMAILSAVED":"Email saved successful!",
|
||||
"PHONESAVED":"Phone saved successful!",
|
||||
"PHONEREMOVED":"Phone has been removed!",
|
||||
"PHONEVERIFIED":"Phone verified successful!",
|
||||
"PHONEVERIFICATIONSENT":"Phone verification code sent!",
|
||||
"EMAILVERIFICATIONSENT":"Email verification code sent!",
|
||||
"OTPREMOVED":"OTP removed!",
|
||||
"INITIALPASSWORDSET":"Initial password set!",
|
||||
"PASSWORDNOTIFICATIONSENT":"Password Change Notification sent",
|
||||
"PASSWORDCHANGED":"Password changed successful!",
|
||||
"REACTIVATED":"User reactivated",
|
||||
"DEACTIVATED":"User deactivated"
|
||||
}
|
||||
},
|
||||
"IAM": {
|
||||
"DETAIL": {
|
||||
@ -437,6 +453,13 @@
|
||||
"CREATIONDATE":"Created At",
|
||||
"CHANGEDATE":"Last Modified",
|
||||
"RESOURCEOWNER":"Owner"
|
||||
},
|
||||
"TOAST":{
|
||||
"MEMBERREMOVED":"Manager removed!",
|
||||
"MEMBERSADDED": "Managers added!",
|
||||
"MEMBERADDED": "Manager added!",
|
||||
"ROLEREMOVED":"Role removed!",
|
||||
"ROLECHANGED":"Role changed!"
|
||||
}
|
||||
},
|
||||
"APP": {
|
||||
@ -480,6 +503,12 @@
|
||||
"AUTHMETHOD0":"Basic",
|
||||
"AUTHMETHOD1":"Post",
|
||||
"AUTHMETHOD2":"None"
|
||||
},
|
||||
"TOAST": {
|
||||
"REACTIVATED":"App reactivated!",
|
||||
"DEACTIVATED":"App deactivated!",
|
||||
"OIDCUPDATED":"OIDC Config updated!",
|
||||
"OIDCCLIENTSECRETREGENERATED":"OIDC Client Secret generated!"
|
||||
}
|
||||
},
|
||||
"GENDERS": {
|
||||
@ -555,6 +584,10 @@
|
||||
"DETAIL": {
|
||||
"TITLE":"Authorization Detail",
|
||||
"DESCRIPTION":""
|
||||
},
|
||||
"TOAST": {
|
||||
"UPDATED":"Grant updated!",
|
||||
"BULKREMOVED":"Grants removed!"
|
||||
}
|
||||
},
|
||||
"CHANGES": {
|
||||
|
@ -15,6 +15,16 @@
|
||||
href="https://maxst.icons8.com/vue-static/landings/line-awesome/line-awesome/1.3.0/css/line-awesome.min.css">
|
||||
<link rel="manifest" href="manifest.webmanifest">
|
||||
<meta name="theme-color" content="#e6768b">
|
||||
|
||||
|
||||
<meta property="og:url" content="https://console.zitadel.dev" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:title" content="ZITADEL Console - Identity and Access Management" />
|
||||
<meta property="og:description" content="Console Management Platform for ZITADEL IAM" />
|
||||
<meta property="description" content="Console Management Platform for ZITADEL IAM" />
|
||||
|
||||
<meta property="og:image"
|
||||
content="https://console.zitadel.dev/assets/images/zitadel-logo-oneline-lightdesign.svg" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
@ -16,6 +16,11 @@
|
||||
"src": "assets/icons/android-chrome-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "assets/icons/apple-touch-icon.png",
|
||||
"sizes": "180x180",
|
||||
"type": "image/png"
|
||||
}
|
||||
]
|
||||
}
|
@ -1,451 +1,508 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
<title>{{.Title}}</title>
|
||||
<style>
|
||||
@import url('https://fonts.googleapis.com/css?family=Roboto:300,400&display=swap');
|
||||
|
||||
@import url('https://fonts.googleapis.com/css?family=Roboto:300,400&display=swap');
|
||||
|
||||
@font-face {
|
||||
font-family: Ailerons;
|
||||
src: url("https://www.caos.ch/fonts/Ailerons-Typeface.otf") format("opentype");
|
||||
}
|
||||
@font-face {
|
||||
font-family: Ailerons;
|
||||
src: url("https://www.caos.ch/fonts/Ailerons-Typeface.otf") format("opentype");
|
||||
}
|
||||
|
||||
|
||||
/* -------------------------------------
|
||||
/* -------------------------------------
|
||||
GLOBAL RESETS
|
||||
------------------------------------- */
|
||||
|
||||
img {
|
||||
border: none;
|
||||
-ms-interpolation-mode: bicubic;
|
||||
max-width: 100%;
|
||||
}
|
||||
body {
|
||||
background: #222324 url('waves-bottomleft.png') bottom left no-repeat;
|
||||
background-size: 150px;
|
||||
font-family: 'Roboto', sans-serif;
|
||||
font-weight: 300;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
-ms-text-size-adjust: 100%;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
}
|
||||
table {
|
||||
border-collapse: separate;
|
||||
mso-table-lspace: 0pt;
|
||||
mso-table-rspace: 0pt;
|
||||
width: 100%; }
|
||||
|
||||
img {
|
||||
border: none;
|
||||
-ms-interpolation-mode: bicubic;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
background: #222324 url('waves-bottomleft.png') bottom left no-repeat;
|
||||
background-size: 150px;
|
||||
font-family: 'Roboto', sans-serif;
|
||||
font-weight: 300;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
-ms-text-size-adjust: 100%;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: separate;
|
||||
mso-table-lspace: 0pt;
|
||||
mso-table-rspace: 0pt;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
table td {
|
||||
font-family: 'Roboto', sans-serif;
|
||||
font-size: 14px;
|
||||
font-weight: 300;
|
||||
vertical-align: top;
|
||||
color: #fff;
|
||||
}
|
||||
/* -------------------------------------
|
||||
font-family: 'Roboto', sans-serif;
|
||||
font-size: 14px;
|
||||
font-weight: 300;
|
||||
vertical-align: top;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
BODY & CONTAINER
|
||||
------------------------------------- */
|
||||
.body {
|
||||
background: url('waves-topright.png') top right no-repeat;
|
||||
background-size: 150px;
|
||||
width: 100%;
|
||||
}
|
||||
/* Set a max-width, and make it display as block so it will automatically stretch to that width, but will also shrink down on a phone or something */
|
||||
.container {
|
||||
display: block;
|
||||
margin: 0 auto !important;
|
||||
/* makes it centered */
|
||||
max-width: 580px;
|
||||
padding: 40px;
|
||||
width: 580px;
|
||||
}
|
||||
/* This should also be a block element, so that it will fill 100% of the .container */
|
||||
.content {
|
||||
box-sizing: border-box;
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
max-width: 580px;
|
||||
padding: 10px;
|
||||
}
|
||||
/* -------------------------------------
|
||||
.body {
|
||||
background: url('waves-topright.png') top right no-repeat;
|
||||
background-size: 150px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Set a max-width, and make it display as block so it will automatically stretch to that width, but will also shrink down on a phone or something */
|
||||
.container {
|
||||
display: block;
|
||||
margin: 0 auto !important;
|
||||
/* makes it centered */
|
||||
max-width: 580px;
|
||||
padding: 40px;
|
||||
width: 580px;
|
||||
}
|
||||
|
||||
/* This should also be a block element, so that it will fill 100% of the .container */
|
||||
.content {
|
||||
box-sizing: border-box;
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
max-width: 580px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
HEADER, FOOTER, MAIN
|
||||
------------------------------------- */
|
||||
.main {
|
||||
background: #191919;
|
||||
width: 100%;
|
||||
border-radius: 16px;
|
||||
}
|
||||
.wrapper {
|
||||
box-sizing: border-box;
|
||||
padding: 50px;
|
||||
}
|
||||
.content-block {
|
||||
padding-bottom: 10px;
|
||||
padding-top: 10px;
|
||||
}
|
||||
.footer {
|
||||
clear: both;
|
||||
margin-top: 10px;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
}
|
||||
.main {
|
||||
background: #191919;
|
||||
width: 100%;
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
box-sizing: border-box;
|
||||
padding: 50px;
|
||||
}
|
||||
|
||||
.content-block {
|
||||
padding-bottom: 10px;
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
clear: both;
|
||||
margin-top: 10px;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.footer td,
|
||||
.footer p,
|
||||
.footer span,
|
||||
.footer a {
|
||||
color: #999999;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.footer a{
|
||||
color: #FE00FF;
|
||||
text-decoration: none;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
color: #999999;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.footer a:hover{
|
||||
text-decoration: underline;
|
||||
.footer a {
|
||||
color: #FE00FF;
|
||||
text-decoration: none;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.apple-link{
|
||||
margin-bottom: 10px;
|
||||
display: inline-block;
|
||||
.footer a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
.apple-link {
|
||||
margin-bottom: 10px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
TYPOGRAPHY
|
||||
------------------------------------- */
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4 {
|
||||
color: #fff;
|
||||
font-family: 'Roboto', sans-serif;
|
||||
font-weight: 300;
|
||||
line-height: 1.4;
|
||||
margin: 0;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
h1 {
|
||||
font-size: 35px;
|
||||
font-weight: 300;
|
||||
text-align: center;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
p,
|
||||
ul,
|
||||
ol {
|
||||
font-family: 'Roboto', sans-serif;
|
||||
font-size: 14px;
|
||||
font-weight: 300;
|
||||
margin: 0;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4 {
|
||||
color: #fff;
|
||||
font-family: 'Roboto', sans-serif;
|
||||
font-weight: 300;
|
||||
line-height: 1.4;
|
||||
margin: 0;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 35px;
|
||||
font-weight: 300;
|
||||
text-align: center;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
p,
|
||||
ul,
|
||||
ol {
|
||||
font-family: 'Roboto', sans-serif;
|
||||
font-size: 14px;
|
||||
font-weight: 300;
|
||||
margin: 0;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
p li,
|
||||
ul li,
|
||||
ol li {
|
||||
list-style-position: inside;
|
||||
margin-left: 5px;
|
||||
}
|
||||
a {
|
||||
color: #0FBDA6;
|
||||
text-decoration: underline;
|
||||
}
|
||||
/* -------------------------------------
|
||||
list-style-position: inside;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #0FBDA6;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
BUTTONS
|
||||
------------------------------------- */
|
||||
.btn {
|
||||
box-sizing: border-box;
|
||||
width: 100%; }
|
||||
.btn > tbody > tr > td {
|
||||
padding-bottom: 15px; }
|
||||
.btn {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.btn>tbody>tr>td {
|
||||
padding-bottom: 15px;
|
||||
}
|
||||
|
||||
.btn table {
|
||||
width: auto;
|
||||
}
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.btn table td {
|
||||
background-color: #ffffff;
|
||||
border-radius: 5px;
|
||||
text-align: center;
|
||||
}
|
||||
background-color: #ffffff;
|
||||
border-radius: 5px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.btn a {
|
||||
background-color: #ffffff;
|
||||
border: solid 1px #0FBDA6;
|
||||
border-radius: 5px;
|
||||
box-sizing: border-box;
|
||||
color: #0FBDA6;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
margin: 0;
|
||||
padding: 20px 60px;
|
||||
text-decoration: none;
|
||||
}
|
||||
background-color: #ffffff;
|
||||
border: solid 1px #0FBDA6;
|
||||
border-radius: 5px;
|
||||
box-sizing: border-box;
|
||||
color: #0FBDA6;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
margin: 0;
|
||||
padding: 20px 60px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
margin-top: 50px;
|
||||
}
|
||||
.btn-primary {
|
||||
margin-top: 50px;
|
||||
}
|
||||
|
||||
.btn-primary table td {
|
||||
background-color: #0FBDA6;
|
||||
}
|
||||
.btn-primary table td {
|
||||
background-color: #0FBDA6;
|
||||
}
|
||||
|
||||
.btn-primary a {
|
||||
background-color: #0FBDA6;
|
||||
border-color: #0FBDA6;
|
||||
color: #ffffff;
|
||||
}
|
||||
/* -------------------------------------
|
||||
.btn-primary a {
|
||||
background-color: #0FBDA6;
|
||||
border-color: #0FBDA6;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
OTHER STYLES THAT MIGHT BE USEFUL
|
||||
------------------------------------- */
|
||||
.mheader {
|
||||
background-color: #131313;
|
||||
}
|
||||
.mheader {
|
||||
background-color: #131313;
|
||||
}
|
||||
|
||||
.headercell {
|
||||
color: #FFFFFF;
|
||||
}
|
||||
.headercell {
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
.headertitle{
|
||||
padding: 30px 30px 70px;
|
||||
margin: 0;
|
||||
font-size: 42px;
|
||||
font-family: 'Ailerons', sans-serif;
|
||||
text-align: center;
|
||||
}
|
||||
.headertitle {
|
||||
padding: 30px 30px 70px;
|
||||
margin: 0;
|
||||
font-size: 42px;
|
||||
font-family: 'Ailerons', sans-serif;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.logo{
|
||||
height: 50px;
|
||||
margin: 10px;
|
||||
margin-left: 30px;
|
||||
}
|
||||
.logo {
|
||||
height: 50px;
|
||||
margin: 10px;
|
||||
margin-left: 30px;
|
||||
}
|
||||
|
||||
.hello {
|
||||
font-size: 22px;
|
||||
}
|
||||
.hello {
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.last {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.first {
|
||||
margin-top: 0;
|
||||
}
|
||||
.align-center {
|
||||
text-align: center;
|
||||
}
|
||||
.align-right {
|
||||
text-align: right;
|
||||
}
|
||||
.align-left {
|
||||
text-align: left;
|
||||
}
|
||||
.clear {
|
||||
clear: both;
|
||||
}
|
||||
.mt0 {
|
||||
margin-top: 0;
|
||||
}
|
||||
.mb0 {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.preheader {
|
||||
color: transparent;
|
||||
display: none;
|
||||
height: 0;
|
||||
max-height: 0;
|
||||
max-width: 0;
|
||||
opacity: 0;
|
||||
overflow: hidden;
|
||||
mso-hide: all;
|
||||
visibility: hidden;
|
||||
width: 0;
|
||||
}
|
||||
.subject {
|
||||
color: transparent;
|
||||
display: none;
|
||||
height: 0;
|
||||
max-height: 0;
|
||||
max-width: 0;
|
||||
opacity: 0;
|
||||
overflow: hidden;
|
||||
mso-hide: all;
|
||||
visibility: hidden;
|
||||
width: 0;
|
||||
}
|
||||
.powered-by a {
|
||||
text-decoration: none;
|
||||
}
|
||||
hr {
|
||||
border: 0;
|
||||
border-bottom: 1px solid #f6f6f6;
|
||||
margin: 20px 0;
|
||||
}
|
||||
/* -------------------------------------
|
||||
|
||||
.last {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.first {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.align-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.align-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.align-left {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.clear {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.mt0 {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.mb0 {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.preheader {
|
||||
color: transparent;
|
||||
display: none;
|
||||
height: 0;
|
||||
max-height: 0;
|
||||
max-width: 0;
|
||||
opacity: 0;
|
||||
overflow: hidden;
|
||||
mso-hide: all;
|
||||
visibility: hidden;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.subject {
|
||||
color: transparent;
|
||||
display: none;
|
||||
height: 0;
|
||||
max-height: 0;
|
||||
max-width: 0;
|
||||
opacity: 0;
|
||||
overflow: hidden;
|
||||
mso-hide: all;
|
||||
visibility: hidden;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.powered-by a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
hr {
|
||||
border: 0;
|
||||
border-bottom: 1px solid #f6f6f6;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
RESPONSIVE AND MOBILE FRIENDLY STYLES
|
||||
------------------------------------- */
|
||||
@media only screen and (max-width: 620px) {
|
||||
table[class=body] h1 {
|
||||
font-size: 28px !important;
|
||||
margin-bottom: 10px !important;
|
||||
@media only screen and (max-width: 620px) {
|
||||
table[class=body] h1 {
|
||||
font-size: 28px !important;
|
||||
margin-bottom: 10px !important;
|
||||
}
|
||||
|
||||
table[class=body] p,
|
||||
table[class=body] ul,
|
||||
table[class=body] ol,
|
||||
table[class=body] td,
|
||||
table[class=body] span,
|
||||
table[class=body] a {
|
||||
font-size: 16px !important;
|
||||
}
|
||||
|
||||
table[class=body] .wrapper,
|
||||
table[class=body] .article {
|
||||
padding: 10px !important;
|
||||
}
|
||||
|
||||
table[class=body] .content {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
table[class=body] .container {
|
||||
padding: 0 !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
table[class=body] .main {
|
||||
border-left-width: 0 !important;
|
||||
border-radius: 0 !important;
|
||||
border-right-width: 0 !important;
|
||||
}
|
||||
|
||||
table[class=body] .btn table {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
table[class=body] .btn a {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
table[class=body] .img-responsive {
|
||||
height: auto !important;
|
||||
max-width: 100% !important;
|
||||
width: auto !important;
|
||||
}
|
||||
}
|
||||
table[class=body] p,
|
||||
table[class=body] ul,
|
||||
table[class=body] ol,
|
||||
table[class=body] td,
|
||||
table[class=body] span,
|
||||
table[class=body] a {
|
||||
font-size: 16px !important;
|
||||
}
|
||||
table[class=body] .wrapper,
|
||||
table[class=body] .article {
|
||||
padding: 10px !important;
|
||||
}
|
||||
table[class=body] .content {
|
||||
padding: 0 !important;
|
||||
}
|
||||
table[class=body] .container {
|
||||
padding: 0 !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
table[class=body] .main {
|
||||
border-left-width: 0 !important;
|
||||
border-radius: 0 !important;
|
||||
border-right-width: 0 !important;
|
||||
}
|
||||
table[class=body] .btn table {
|
||||
width: 100% !important;
|
||||
}
|
||||
table[class=body] .btn a {
|
||||
width: 100% !important;
|
||||
}
|
||||
table[class=body] .img-responsive {
|
||||
height: auto !important;
|
||||
max-width: 100% !important;
|
||||
width: auto !important;
|
||||
}
|
||||
}
|
||||
/* -------------------------------------
|
||||
|
||||
/* -------------------------------------
|
||||
PRESERVE THESE STYLES IN THE HEAD
|
||||
------------------------------------- */
|
||||
@media all {
|
||||
.ExternalClass {
|
||||
width: 100%;
|
||||
@media all {
|
||||
.ExternalClass {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.ExternalClass,
|
||||
.ExternalClass p,
|
||||
.ExternalClass span,
|
||||
.ExternalClass font,
|
||||
.ExternalClass td,
|
||||
.ExternalClass div {
|
||||
line-height: 100%;
|
||||
}
|
||||
|
||||
.apple-link a {
|
||||
color: inherit !important;
|
||||
font-family: inherit !important;
|
||||
font-size: inherit !important;
|
||||
font-weight: inherit !important;
|
||||
line-height: inherit !important;
|
||||
text-decoration: none !important;
|
||||
}
|
||||
|
||||
.btn-primary table td:hover {
|
||||
background-color: #1ADFC5 !important;
|
||||
}
|
||||
|
||||
.btn-primary a:hover {
|
||||
background-color: #1ADFC5 !important;
|
||||
border-color: #1ADFC5 !important;
|
||||
}
|
||||
}
|
||||
.ExternalClass,
|
||||
.ExternalClass p,
|
||||
.ExternalClass span,
|
||||
.ExternalClass font,
|
||||
.ExternalClass td,
|
||||
.ExternalClass div {
|
||||
line-height: 100%;
|
||||
}
|
||||
.apple-link a {
|
||||
color: inherit !important;
|
||||
font-family: inherit !important;
|
||||
font-size: inherit !important;
|
||||
font-weight: inherit !important;
|
||||
line-height: inherit !important;
|
||||
text-decoration: none !important;
|
||||
}
|
||||
.btn-primary table td:hover {
|
||||
background-color: #1ADFC5 !important;
|
||||
}
|
||||
.btn-primary a:hover {
|
||||
background-color: #1ADFC5 !important;
|
||||
border-color: #1ADFC5 !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="">
|
||||
</head>
|
||||
|
||||
<body class="">
|
||||
<span class="preheader">{{.PreHeader}}</span>
|
||||
<span class="subject">{{.Subject}}</span>
|
||||
<table role="pheader" class="mheader" border="0" cellpadding="0" cellspacing="0">
|
||||
<tr>
|
||||
<td class="mheadercell">
|
||||
|
||||
</td>
|
||||
<td class="logo">
|
||||
<img class="logo" src="https://caos.ch/images/LogoCaos.png" alt="CAOS AG - Logo">
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0" class="body">
|
||||
<tr>
|
||||
<td></td>
|
||||
<td class="container">
|
||||
<div class="content">
|
||||
<tr>
|
||||
<td class="mheadercell">
|
||||
|
||||
<!-- START CENTERED WHITE CONTAINER -->
|
||||
<table role="pheader" class="tableheader" border="0" cellpadding="0" cellspacing="0">
|
||||
<tr>
|
||||
<td class="headercell">
|
||||
<p class="headertitle">Zitadel</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<table role="presentation" class="main">
|
||||
|
||||
<!-- START MAIN CONTENT AREA -->
|
||||
<tr>
|
||||
<td class="wrapper">
|
||||
<table role="presentation" class="maincontent" border="0" cellpadding="0" cellspacing="0">
|
||||
<tr>
|
||||
<td>
|
||||
<p class="hello">{{.Greeting}}</p>
|
||||
<p>{{.Text}}</p>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0" class="btn btn-primary">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td> <a href="{{.URL}}" target="_blank" rel="noopener noreferrer">{{.ButtonText}}</a> </td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- END MAIN CONTENT AREA -->
|
||||
</table>
|
||||
<!-- END CENTERED WHITE CONTAINER -->
|
||||
|
||||
<!-- START FOOTER -->
|
||||
<div class="footer">
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||
<tr>
|
||||
<td class="content-block">
|
||||
<span class="apple-link">CAOS AG | Teufener Strasse 19 | CH-9000 St.Gallen</span>
|
||||
<br> <a href="http://www.caos.ch">caos.ch</a>.
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<!-- END FOOTER -->
|
||||
|
||||
</div>
|
||||
</td>
|
||||
<td> </td>
|
||||
</tr>
|
||||
</td>
|
||||
<td class="logo">
|
||||
<img class="logo" src="https://caos.ch/images/LogoCaos.png" alt="CAOS AG - Logo">
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0" class="body">
|
||||
<tr>
|
||||
<td></td>
|
||||
<td class="container">
|
||||
<div class="content">
|
||||
|
||||
<!-- START CENTERED WHITE CONTAINER -->
|
||||
<table role="pheader" class="tableheader" border="0" cellpadding="0" cellspacing="0">
|
||||
<tr>
|
||||
<td class="headercell">
|
||||
<p class="headertitle">Zitadel</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<table role="presentation" class="main">
|
||||
|
||||
<!-- START MAIN CONTENT AREA -->
|
||||
<tr>
|
||||
<td class="wrapper">
|
||||
<table role="presentation" class="maincontent" border="0" cellpadding="0"
|
||||
cellspacing="0">
|
||||
<tr>
|
||||
<td>
|
||||
<p class="hello">{{.Greeting}}</p>
|
||||
<p>{{.Text}}</p>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0"
|
||||
class="btn btn-primary">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<table role="presentation" border="0" cellpadding="0"
|
||||
cellspacing="0">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td> <a href="{{.URL}}" target="_blank"
|
||||
rel="noopener noreferrer">{{.ButtonText}}</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- END MAIN CONTENT AREA -->
|
||||
</table>
|
||||
<!-- END CENTERED WHITE CONTAINER -->
|
||||
|
||||
<!-- START FOOTER -->
|
||||
<div class="footer">
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||
<tr>
|
||||
<td class="content-block">
|
||||
<span class="apple-link">CAOS AG | Teufener Strasse 19
|
||||
| CH-9000 St.Gallen</span>
|
||||
<br> <a href="http://www.caos.ch">caos.ch</a>.
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<!-- END FOOTER -->
|
||||
|
||||
</div>
|
||||
</td>
|
||||
<td> </td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
|
||||
</html>
|
Loading…
x
Reference in New Issue
Block a user