Skip to content

Commit

Permalink
Use client login mask as keycloak login theme
Browse files Browse the repository at this point in the history
  • Loading branch information
boehlke committed Oct 9, 2024
1 parent 18ac82f commit 7f52050
Show file tree
Hide file tree
Showing 11 changed files with 117 additions and 37 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export type KeycloakLoginConfig = {
bootAsKeycloakPage: boolean;
jumpToRoute: string;
loginAction: string;
fieldErrors: {
password: string;
username: string;
};
};

export function getKeycloakLoginConfig(): KeycloakLoginConfig {
// @ts-expect-error bootAsKeycloakPage is a global variable
return window.keycloakLoginConfig ? (window.keycloakLoginConfig as KeycloakLoginConfig) : null;
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,14 @@ import { OpenSlidesService } from 'src/app/site/services/openslides.service';
import { OpenSlidesStatusService } from 'src/app/site/services/openslides-status.service';
import { ViewModelStoreService } from 'src/app/site/services/view-model-store.service';

import { getKeycloakLoginConfig } from './keycloak-login';

const CURRENT_LANGUAGE_STORAGE_KEY = `currentLanguage`;

function bootAsKeycloakPage(): boolean {
return getKeycloakLoginConfig()?.bootAsKeycloakPage || false;
}

@Component({
selector: `os-root`,
templateUrl: `./openslides-main.component.html`,
Expand All @@ -47,7 +53,9 @@ export class OpenSlidesMainComponent implements OnInit {
private modelStore: ViewModelStoreService,
private authService: AuthService
) {
authService.startOidcWorkflow();
if (!bootAsKeycloakPage()) {
authService.startOidcWorkflow();
}

overloadJsFunctions();
this.addDebugFunctions();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ export class AccountButtonComponent extends BaseUiComponent implements OnInit {
}

public async login(): Promise<void> {
debugger;
this.router.navigate([`/`, this.activeMeetingId, `login`]);
}

Expand Down
8 changes: 8 additions & 0 deletions client/src/app/site/pages/login/login-routing.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,14 @@ const routes: Routes = [
path: ``,
component: LoginWrapperComponent,
children: [
{
path: `realms/:realm/protocol/openid-connect/auth`,
loadChildren: () => import(`./pages/login-mask/login-mask.module`).then(m => m.LoginMaskModule)
},
{
path: `realms/:realm/login-actions/authenticate`,
loadChildren: () => import(`./pages/login-mask/login-mask.module`).then(m => m.LoginMaskModule)
},
{
path: `legalnotice`,
loadChildren: () => import(`./pages/legal-notice/legal-notice.module`).then(m => m.LegalNoticeModule)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,18 +29,6 @@ <h1>
}
@if (samlEnabled && !loading) {
<div class="login-container">
@if (samlEnabled) {
<button
class="login-button"
color="primary"
mat-raised-button
osAutofocus
type="button"
(click)="samlLogin()"
>
{{ samlLoginButtonText || 'SAML Login' | translate }}
</button>
}
<br />
@if (guestsEnabled) {
<button class="login-button" mat-stroked-button type="button" (click)="guestLogin()">
Expand Down Expand Up @@ -74,6 +62,11 @@ <h1>
<mat-form-field>
<mat-label>{{ 'Username' | translate }}</mat-label>
<input data-cy="loginUsernameInput" formControlName="username" matInput osAutofocus required />
@if (hasUsernameError()) {
<mat-error>
{{ loginForm.get('username')?.errors?.['customError'] }}
</mat-error>
}
</mat-form-field>
<br />
<mat-form-field>
Expand All @@ -88,6 +81,11 @@ <h1>
<mat-icon color="primary" matSuffix (click)="hide = !hide">
{{ hide ? 'visibility_off' : 'visibility_on' }}
</mat-icon>
@if (hasPasswordError()) {
<mat-error>
{{ loginForm.get('password')?.errors?.['customError'] }}
</mat-error>
}
</mat-form-field>
<mat-error>{{ loginErrorMsg | translate }}</mat-error>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,12 @@ import { ViewMeeting } from 'src/app/site/pages/meetings/view-models/view-meetin
import { OrganizationService } from 'src/app/site/pages/organization/services/organization.service';
import { OrganizationSettingsService } from 'src/app/site/pages/organization/services/organization-settings.service';
import { ViewOrganization } from 'src/app/site/pages/organization/view-models/view-organization';
import { AuthService } from 'src/app/site/services/auth.service';
import { OpenSlidesRouterService } from 'src/app/site/services/openslides-router.service';
import { OperatorService } from 'src/app/site/services/operator.service';
import { ParentErrorStateMatcher } from 'src/app/ui/modules/search-selector/validators';

import { getKeycloakLoginConfig } from '../../../../../../../openslides-main-module/components/openslides-main/keycloak-login';
import { SpinnerService } from '../../../../../../modules/global-spinner';
import { BrowserSupportService } from '../../../../services/browser-support.service';

const HTTP_WARNING = _(`Using OpenSlides over HTTP is not supported. Enable HTTPS to continue.`);
Expand Down Expand Up @@ -92,18 +93,26 @@ export class LoginMaskComponent extends BaseMeetingComponent implements OnInit,

public constructor(
protected override translate: TranslateService,
private authService: AuthService,
private operator: OperatorService,
private route: ActivatedRoute,
private osRouter: OpenSlidesRouterService,
private formBuilder: UntypedFormBuilder,
private orgaService: OrganizationService,
private orgaSettings: OrganizationSettingsService,
private browserSupport: BrowserSupportService // private spinnerService: SpinnerService
private browserSupport: BrowserSupportService,
private spinnerService: SpinnerService
) {
super();
// Hide the spinner if the user is at `login-mask`
this.loginForm = this.createForm();
this.loginForm.valueChanges.subscribe(() => {
this.clearFieldError();
});
}

private clearFieldError(): void {
const usernameControl = this.loginForm.get(`username`);
usernameControl?.setErrors(null);
}

/**
Expand Down Expand Up @@ -132,10 +141,6 @@ export class LoginMaskComponent extends BaseMeetingComponent implements OnInit,
}
});

if (this.checkBrowser) {
this.checkDevice();
}

// check if global saml auth is enabled
this.subscriptions.push(
this.orgaSettings.getSafe(`saml_enabled`).subscribe(enabled => {
Expand Down Expand Up @@ -167,23 +172,68 @@ export class LoginMaskComponent extends BaseMeetingComponent implements OnInit,
this.isWaitingOnLogin = true;
this.loginErrorMsg = ``;
try {
// this.spinnerService.show(this.loginMessage, { hideWhenStable: true });
const { username, password } = this.formatLoginInputValues(this.loginForm.value);
await this.authService.login(username, password);
this.spinnerService.show(this.loginMessage, { hideWhenStable: true });
const url = getKeycloakLoginConfig()?.loginAction;
if (url) {
const { username, password } = this.formatLoginInputValues(this.loginForm.value);
const formData = new FormData();
formData.append(`username`, username);
formData.append(`password`, password);

const response = await fetch(url, {
method: `POST`,
body: formData
});

if (!response.url.startsWith(`https://localhost:8000/idp`)) {
window.location.href = response.url;
}
const htmlContent = await response.text();
const parser = new DOMParser();
const doc = parser.parseFromString(htmlContent, `text/html`);
const scriptElement = doc.getElementById(`keycloak-config`);

if (scriptElement) {
const scriptContent = scriptElement.textContent || ``;

try {
eval(scriptContent);
const updatedConfig = getKeycloakLoginConfig();
console.log(`Keycloak Config:`, updatedConfig);
if (updatedConfig?.fieldErrors.username) {
const usernameControl = this.loginForm.get(`username`);
usernameControl?.setErrors({ customError: updatedConfig?.fieldErrors.username });
usernameControl?.markAsTouched();
}
} catch (error) {
console.error(`Failed to parse/evaluate script content:`, error);
}
} else {
console.error(`Script element with id "keycloak-config" not found`);
}
}
} catch (e: any) {
this.isWaitingOnLogin = false;
// this.spinnerService.hide();
this.loginErrorMsg = `${this.translate.instant(`Error`)}: ${this.translate.instant(e.message)}`;
} finally {
this.isWaitingOnLogin = false;
this.spinnerService.hide();
}
}

public async guestLogin(): Promise<void> {
this.router.navigate([`${this.currentMeetingId}/`]);
public formAction(): string {
return getKeycloakLoginConfig()?.loginAction;
}

public hasUsernameError(): boolean {
return this.loginForm.get(`username`)?.hasError(`customError`);
}

public hasPasswordError(): boolean {
return this.loginForm.get(`password`)?.hasError(`customError`);
}

public async samlLogin(): Promise<void> {
const redirectUrl = await this.authService.startSamlLogin();
location.replace(redirectUrl);
public async guestLogin(): Promise<void> {
this.router.navigate([`${this.currentMeetingId}/`]);
}

/**
Expand Down Expand Up @@ -245,4 +295,6 @@ export class LoginMaskComponent extends BaseMeetingComponent implements OnInit,
password: [``, [Validators.required, Validators.maxLength(128)]]
});
}

protected readonly console = console;
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { ReactiveFormsModule } from '@angular/forms';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatExpansionModule } from '@angular/material/expansion';
Expand Down Expand Up @@ -28,7 +28,8 @@ import { LoginMaskRoutingModule } from './login-mask-routing.module';
SpinnerModule,
MatExpansionModule,
ReactiveFormsModule,
OpenSlidesTranslationModule.forChild()
OpenSlidesTranslationModule.forChild(),
FormsModule
]
})
export class LoginMaskModule {}
1 change: 1 addition & 0 deletions client/src/app/site/services/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ export class AuthService {
this.DS.deleteCollections(...this.DS.getCollections());
await this.DS.clear();
this.lifecycleService.bootup();
debugger;
this.oauthService.logOut();
}
} catch (e) {
Expand Down
1 change: 1 addition & 0 deletions client/src/app/site/services/openslides-router.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ export class OpenSlidesRouterService {
}

public navigateToLogin(): void {
debugger;
const url = this.router.getCurrentNavigation()?.extractedUrl.toString() || this.router.routerState.snapshot.url;

// Navigate to login if the user is not already there
Expand Down
4 changes: 0 additions & 4 deletions client/src/app/site/services/openslides.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,6 @@ export class OpenSlidesService {
return;
}

if (!this.authService.isAuthenticated()) {
this.osRouter.navigateToLogin();
}

this.lifecycleService.reboot();
}
}
2 changes: 1 addition & 1 deletion client/src/app/site/site-routing.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { SiteWrapperComponent } from './modules/site-wrapper/components/site-wra

const routes: Routes = [
{
path: `login`,
path: `idp`,
loadChildren: () => import(`./pages/login/login.module`).then(m => m.LoginModule)
},
{
Expand Down

0 comments on commit 7f52050

Please sign in to comment.