Aam-Digital/ndb-core

View on GitHub
src/app/core/user/user-security/user-security.component.ts

Summary

Maintainability
A
0 mins
Test Coverage
B
88%
import { Component, Input, OnInit } from "@angular/core";
import { DynamicComponent } from "../../config/dynamic-components/dynamic-component.decorator";
import {
  FormBuilder,
  FormControl,
  ReactiveFormsModule,
  Validators,
} from "@angular/forms";
import {
  KeycloakAuthService,
  KeycloakUser,
  Role,
} from "../../session/auth/keycloak/keycloak-auth.service";
import { AlertService } from "../../alerts/alert.service";
import { HttpClient } from "@angular/common/http";
import { AppSettings } from "../../app-settings";
import { NgForOf, NgIf } from "@angular/common";
import { MatButtonModule } from "@angular/material/button";
import { MatTooltipModule } from "@angular/material/tooltip";
import { MatFormFieldModule } from "@angular/material/form-field";
import { MatInputModule } from "@angular/material/input";
import { MatSelectModule } from "@angular/material/select";
import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy";
import { SessionSubject } from "../../session/auth/session-info";
import { Entity } from "../../entity/model/entity";
import { catchError } from "rxjs/operators";

@UntilDestroy()
@DynamicComponent("UserSecurity")
@Component({
  selector: "app-user-security",
  templateUrl: "./user-security.component.html",
  styleUrls: ["./user-security.component.scss"],
  imports: [
    NgIf,
    MatButtonModule,
    MatTooltipModule,
    ReactiveFormsModule,
    MatFormFieldModule,
    MatInputModule,
    MatSelectModule,
    NgForOf,
  ],
  standalone: true,
})
export class UserSecurityComponent implements OnInit {
  @Input() entity: Entity;
  form = this.fb.group({
    username: [{ value: "", disabled: true }],
    email: ["", [Validators.required, Validators.email]],
    roles: new FormControl<Role[]>([]),
  });
  availableRoles: Role[] = [];
  user: KeycloakUser;
  editing = true;
  userIsPermitted = false;

  constructor(
    private authService: KeycloakAuthService,
    sessionInfo: SessionSubject,
    private fb: FormBuilder,
    private alertService: AlertService,
    private http: HttpClient,
  ) {
    if (
      sessionInfo.value?.roles.includes(
        KeycloakAuthService.ACCOUNT_MANAGER_ROLE,
      )
    ) {
      this.userIsPermitted = true;
    } else {
      return;
    }
    // automatically skip trailing and leading whitespaces when the form changes
    this.form.valueChanges.pipe(untilDestroyed(this)).subscribe((next) => {
      if (next.email?.startsWith(" ") || next.email?.endsWith(" ")) {
        this.form.get("email").setValue(next.email.trim());
      }
    });
    this.authService
      .getRoles()
      .subscribe((roles) => this.initializeRoles(roles));
  }

  private initializeRoles(roles: Role[]) {
    this.availableRoles = roles;
    if (!this.user) {
      // assign "user_app" as default role for new users
      const userAppRole = roles.find(({ name }) => name === "user_app");
      if (userAppRole) {
        this.form.get("roles").setValue([userAppRole]);
      }
    }
  }

  ngOnInit() {
    if (!this.userIsPermitted) {
      return;
    }
    this.form.get("username").setValue(this.entity.getId());
    this.authService
      .getUser(this.entity.getId(true))
      .pipe(catchError(() => this.authService.getUser(this.entity.getId())))
      .subscribe({
        next: (res) => this.assignUser(res),
        error: () => undefined,
      });
  }

  private assignUser(user: KeycloakUser) {
    this.user = user;
    this.initializeForm();
    if (this.user) {
      this.disableForm();
    }
  }

  private initializeForm() {
    this.form.get("email").setValue(this.user.email);
    this.form
      .get("roles")
      .setValue(
        this.user.roles.map((role) =>
          this.availableRoles.find((r) => r.id === role.id),
        ),
      );
    this.form.markAsPristine();
  }

  toggleAccount(enabled: boolean) {
    let message = $localize`:Snackbar message:Account has been disabled, user will not be able to login anymore.`;
    if (enabled) {
      message = $localize`:Snackbar message:Account has been activated, user can login again.`;
    }
    this.updateKeycloakUser({ enabled }, message);
  }

  editForm() {
    this.editing = true;
    if (this.user.enabled) {
      this.form.enable();
    }
  }

  disableForm() {
    this.editing = false;
    this.initializeForm();
    this.form.disable();
  }

  createAccount() {
    const user = this.getFormValues();
    if (!user) {
      return;
    }
    user.enabled = true;
    if (user) {
      this.authService.createUser(user).subscribe({
        next: () => {
          this.alertService.addInfo(
            $localize`:Snackbar message:Account created. An email has been sent to ${
              this.form.get("email").value
            }`,
          );
          this.user = user as KeycloakUser;
          this.disableForm();
        },
        error: ({ error }) => this.form.setErrors({ failed: error.message }),
      });
    }
  }

  updateAccount() {
    const update = this.getFormValues();
    // only send values that have changed
    Object.keys(this.form.controls).forEach((control) =>
      this.form.get(control).pristine ? delete update[control] : undefined,
    );
    if (update) {
      this.updateKeycloakUser(
        update,
        $localize`:Snackbar message:Successfully updated user`,
      );
    }
  }

  private updateKeycloakUser(update: Partial<KeycloakUser>, message: string) {
    this.authService.updateUser(this.user.id, update).subscribe({
      next: () => {
        this.alertService.addInfo(message);
        Object.assign(this.user, update);
        this.disableForm();
        if (update.roles?.length > 0) {
          // roles changed, user might have more permissions now
          this.triggerSyncReset();
        }
      },
      error: ({ error }) => this.form.setErrors({ failed: error.message }),
    });
  }

  getFormValues(): Partial<KeycloakUser> {
    if (this.form.invalid) {
      this.form.markAllAsTouched();
      return;
    }
    this.form.setErrors({});
    return this.form.getRawValue();
  }

  private triggerSyncReset() {
    this.http
      .post(
        `${AppSettings.DB_PROXY_PREFIX}/${AppSettings.DB_NAME}/clear_local`,
        undefined,
      )
      .subscribe({
        next: () => undefined,
        // request fails if no permission backend is used - this is fine
        error: () => undefined,
      });
  }
}