feature/2-secure-configurations #2

Merged
william merged 4 commits from feature/2-secure-configurations into develop 2021-08-09 22:16:29 -04:00
24 changed files with 353 additions and 75 deletions
Showing only changes of commit 28cf2d04cd - Show all commits

View File

@ -8,18 +8,18 @@ services:
MYSQL_ROOT_PASSWORD: "pass"
MYSQL_DATABASE: "cre"
ports:
- 3306:3306
- "3306:3306"
backend:
image: fyloz.dev:5443/color-recipes-explorer/backend:master
image: registry.fyloz.dev:5443/colorrecipesexplorer/backend:latest
environment:
spring_profiles_active: "mysql,debug"
cre_database_url: "mysql://database:3306/cre"
cre_database_username: "root"
cre_database_password: "pass"
CRE_ENABLE_DB_UPDATE: 0
CRE_ENABLE_DB_UPDATE: 1
server_port: 9090
ports:
- 9090:9090
- "9090:9090"
volumes:
- cre_data:/usr/bin/cre/data
- cre_config:/usr/bin/cre/config

20
src/_variables.scss Normal file
View File

@ -0,0 +1,20 @@
@import "assets/sass/modules/fonts";
@import "custom-theme";
@import "~material-design-icons/iconfont/material-icons.css";
// Spacing
$spacer: 1rem;
$spacers: (
1: $spacer * 0.5,
2: $spacer * 0.75,
3: $spacer,
4: $spacer * 1.5,
5: $spacer * 2
);
// Colors
$color-primary: map-get($theme-primary, 500);
$text-color-primary: white;
$color-accent: map-get($theme-accent, 500);
$color-warn: map-get($theme-error, 500);

View File

@ -1,4 +1,4 @@
@import '../../../../../custom-theme'
@import "~src/variables"
mat-expansion-panel
width: 48rem

View File

@ -7,7 +7,7 @@ import {
CreImageConfig,
CreConfigList,
CreConfigActions,
CreConfigTooltip, CrePeriodConfig, CreBoolConfig, CreDateConfig
CreConfigTooltip, CrePeriodConfig, CreBoolConfig, CreDateConfig, CreSecureConfig
} from './config'
import {SharedModule} from '../shared/shared.module'
import {CreInputsModule} from '../shared/components/inputs/inputs.module'
@ -26,7 +26,8 @@ import {CreButtonsModule} from '../shared/components/buttons/buttons.module'
CreConfigActions,
CreBoolConfig,
CrePeriodConfig,
CreDateConfig
CreDateConfig,
CreSecureConfig
],
imports: [
SharedModule,

View File

@ -50,3 +50,6 @@ cre-image-config
mat-hint
margin-top: .2em
//cre-secure-config button
//

View File

@ -18,7 +18,8 @@ import {ActivatedRoute, Router} from '@angular/router'
import {formatDate, formatDateTime, getFileUrl, readFile} from '../shared/utils/utils'
import {FormControl, Validators} from '@angular/forms'
import {ConfirmBoxComponent} from '../shared/components/confirm-box/confirm-box.component'
import {environment} from '../../../environments/environment'
import {MatDialog} from '@angular/material/dialog'
import {CrePromptDialog} from '../shared/components/dialogs/dialogs'
@Directive({
selector: 'cre-config-label'
@ -108,7 +109,7 @@ export class CreConfig extends SubscribingComponent {
)
}
setConfig(config: Config) {
protected setConfig(config: Config) {
this.configuration = config
this.config.control.setValue(config.content)
if (!config.editable) {
@ -158,7 +159,7 @@ export class CreImageConfig extends CreConfig {
templateUrl: 'bool.html'
})
export class CreBoolConfig extends CreConfig {
setConfig(config: Config) {
protected setConfig(config: Config) {
super.setConfig(config)
this.config.control.setValue(config.content === 'true')
}
@ -176,12 +177,46 @@ export class CrePeriodConfig extends CreConfig {
templateUrl: 'date.html'
})
export class CreDateConfig extends CreConfig {
setConfig(config: Config) {
super.setConfig(config);
protected setConfig(config: Config) {
super.setConfig(config)
this.config.control.setValue(formatDate(config.content))
}
}
@Component({
selector: 'cre-secure-config',
templateUrl: 'secure.html'
})
export class CreSecureConfig extends CreConfig {
@ViewChild(CrePromptDialog) dialog: CrePromptDialog
@Input() buttonLabel: string
private initialValue: string | null
constructor(
configService: ConfigService,
errorService: ErrorService,
activatedRoute: ActivatedRoute,
router: Router
) {
super(configService, errorService, activatedRoute, router);
}
protected setConfig(config: Config) {
super.setConfig(config)
}
onOpen() {
this.initialValue = this.config.control.value
this.dialog.show()
}
onCancel() {
this.config.control.setValue(this.initialValue)
}
}
@Component({
selector: 'cre-config-editor',
templateUrl: 'editor.html'
@ -218,7 +253,7 @@ export class CreConfigEditor extends ErrorHandlingComponent {
super(errorService, activatedRoute, router)
for (let key in this.keys) {
this.controls[this.keys[key]] = new FormControl(null, Validators.required)
this.controls.set(this.keys[key], new FormControl(null, Validators.required))
}
}
@ -232,7 +267,7 @@ export class CreConfigEditor extends ErrorHandlingComponent {
}
getConfig(key: string) {
return {key, control: this.controls[key]}
return {key, control: this.controls.get(key)}
}
save() {

View File

@ -11,12 +11,12 @@
<cre-config-section *ngIf="emergencyMode === 'false'">
<cre-config-label>Apparence</cre-config-label>
<cre-config-list>
<!-- <cre-config [config]="getConfig(keys.INSTANCE_NAME)">-->
<!-- <cre-config-label>Nom de l'instance</cre-config-label>-->
<!-- <cre-config-tooltip>-->
<!-- Affiché dans la barre de titre du navigateur ou en survolant l'onglet de la page dans le navigateur.-->
<!-- </cre-config-tooltip>-->
<!-- </cre-config>-->
<!-- <cre-config [config]="getConfig(keys.INSTANCE_NAME)">-->
<!-- <cre-config-label>Nom de l'instance</cre-config-label>-->
<!-- <cre-config-tooltip>-->
<!-- Affiché dans la barre de titre du navigateur ou en survolant l'onglet de la page dans le navigateur.-->
<!-- </cre-config-tooltip>-->
<!-- </cre-config>-->
<cre-image-config [config]="getConfig(keys.INSTANCE_LOGO_PATH)" previewWidth="170px"
(invalidFormat)="invalidFormatConfirmBox.show()">
@ -50,7 +50,8 @@
<cre-period-config [config]="getConfig(keys.TOUCH_UP_KIT_EXPIRATION)">
<cre-config-label>Période d'expiration des kits de retouches complets</cre-config-label>
<cre-config-tooltip>
Les kits de retouche complétés expirent après la période configurée. Les kits de retouche expirés seront supprimés automatiquement.
Les kits de retouche complétés expirent après la période configurée. Les kits de retouche expirés seront
supprimés automatiquement.
</cre-config-tooltip>
</cre-period-config>
@ -83,9 +84,15 @@
<cre-config-label>Utilisateur de la base de données</cre-config-label>
</cre-config>
<cre-config [config]="getConfig(keys.DATABASE_PASSWORD)">
<!-- <cre-config [config]="getConfig(keys.DATABASE_PASSWORD)">-->
<!-- <cre-config-label>Mot de passe de la base de données</cre-config-label>-->
<!-- </cre-config>-->
<cre-secure-config
[config]="getConfig(keys.DATABASE_PASSWORD)"
buttonLabel="Modifier le mot de passe de la base de données">
<cre-config-label>Mot de passe de la base de données</cre-config-label>
</cre-config>
</cre-secure-config>
<cre-config [config]="getConfig(keys.DATABASE_VERSION)">
<cre-config-label>Version de la base de données</cre-config-label>
@ -114,7 +121,8 @@
</div>
<cre-confirm-box #invalidFormatConfirmBox message="Le format du fichier choisi n'est pas valide"></cre-confirm-box>
<cre-confirm-box #restartConfirmBox message="Voulez-vous vraiment redémarrer le serveur? Les changements nécessitant un redémarrage seront appliqués."
<cre-confirm-box #restartConfirmBox
message="Voulez-vous vraiment redémarrer le serveur? Les changements nécessitant un redémarrage seront appliqués."
(confirm)="restart()"></cre-confirm-box>
<cre-confirm-box #restartingConfirmBox message="Le serveur est en cours de redémarrage" (cancel)="reload()"
(confirm)="reload()"></cre-confirm-box>

View File

@ -0,0 +1,25 @@
<div *ngIf="configuration" [attr.title]="tooltip?.content">
<cre-primary-button
class="w-100 mb-3"
(click)="onOpen()">
{{buttonLabel}}
</cre-primary-button>
<cre-prompt-dialog
[title]="label.content"
(cancel)="onCancel()">
<cre-dialog-body>
<cre-input
[class.has-hint]="configuration.editable"
class="w-100"
type="password"
label="Nouvelle valeur"
[hint]="configuration.editable ? lastUpdated : null"
[control]="config.control"
[icon]="configuration.requireRestart ? 'alert' : null"
[iconTitle]="configuration.requireRestart ? 'Requiert un redémarrage' : null"
iconColor="warning">
</cre-input>
</cre-dialog-body>
</cre-prompt-dialog>
</div>

View File

@ -0,0 +1,6 @@
cre-button, cre-primary-button, cre-accent-button, cre-warn-button
display: inline-block
width: inherit
button
width: 100%

View File

@ -1,4 +1,4 @@
import {Component, Input} from '@angular/core'
import {Component, Input, ViewEncapsulation} from '@angular/core'
import {ThemePalette} from '@angular/material/core'
@Component({
@ -7,7 +7,9 @@ import {ThemePalette} from '@angular/material/core'
<button mat-raised-button [color]="color" [disabled]="disabled">
<ng-content></ng-content>
</button>
`
`,
styleUrls: ['buttons.sass'],
encapsulation: ViewEncapsulation.None
})
export class CreButtonComponent {
@Input() color: ThemePalette
@ -20,7 +22,8 @@ export class CreButtonComponent {
<cre-button color="primary" [disabled]="disabled">
<ng-content></ng-content>
</cre-button>
`
`,
styleUrls: ['buttons.sass']
})
export class CrePrimaryButtonComponent {
@Input() disabled = false
@ -32,7 +35,8 @@ export class CrePrimaryButtonComponent {
<cre-button color="accent" [disabled]="disabled">
<ng-content></ng-content>
</cre-button>
`
`,
styleUrls: ['buttons.sass']
})
export class CreAccentButtonComponent {
@Input() disabled = false
@ -44,7 +48,8 @@ export class CreAccentButtonComponent {
<cre-button color="warn" [disabled]="disabled">
<ng-content></ng-content>
</cre-button>
`
`,
styleUrls: ['buttons.sass']
})
export class CreWarnButtonComponent {
@Input() disabled = false

View File

@ -0,0 +1,21 @@
import {NgModule} from '@angular/core'
import {CreDialogBody, CrePromptDialog} from './dialogs'
import {MatDialogModule} from '@angular/material/dialog'
import {CreButtonsModule} from '../buttons/buttons.module'
@NgModule({
declarations: [
CrePromptDialog,
CreDialogBody
],
exports: [
CrePromptDialog,
CreDialogBody
],
imports: [
MatDialogModule,
CreButtonsModule
]
})
export class CreDialogsModule {
}

View File

@ -0,0 +1,26 @@
@import "~src/variables";
.cre-dialog-panel {
min-width: 20rem;
mat-dialog-container {
padding: 0;
.mat-dialog-title, .mat-dialog-content, .mat-dialog-actions {
margin: 0;
padding: $spacer;
}
.mat-dialog-title {
background-color: $color-primary;
color: $text-color-primary;
}
.mat-dialog-actions {
min-height: auto;
justify-content: end;
gap: map-get($spacers, 1);
padding-top: 0;
}
}
}

View File

@ -0,0 +1,74 @@
import {Component, Directive, EventEmitter, Input, Output, TemplateRef, ViewChild, ViewEncapsulation} from '@angular/core'
import {MatDialog, MatDialogRef} from '@angular/material/dialog'
@Directive({
selector: 'cre-dialog-body'
})
export class CreDialogBody {
}
@Directive()
abstract class CreDialog<D = CreDialogData> {
@ViewChild(TemplateRef) dialogTemplate: TemplateRef<any>
@Output() cancel = new EventEmitter<void>();
@Output() continue = new EventEmitter<void>();
private dialogRef: MatDialogRef<TemplateRef<any>> | null
constructor(
protected dialog: MatDialog
) {
}
protected abstract get data(): D
show() {
this.open()
}
onCancel() {
this.close()
this.cancel.emit();
}
onContinue() {
this.close()
this.continue.emit();
}
private open() {
const config = {
panelClass: 'cre-dialog-panel',
data: this.data
}
this.dialogRef = this.dialog.open(this.dialogTemplate, config)
}
private close() {
this.dialogRef.close()
}
}
@Component({
selector: 'cre-prompt-dialog',
templateUrl: 'prompt.html',
styleUrls: ['dialogs.scss'],
encapsulation: ViewEncapsulation.None
})
export class CrePromptDialog extends CreDialog<CrePromptDialogData> {
@Input() title: string
protected get data(): CrePromptDialogData {
return {
title: this.title
}
}
}
abstract class CreDialogData {
title: string
}
class CrePromptDialogData extends CreDialogData {
}

View File

@ -0,0 +1,10 @@
<ng-template let-data>
<h1 mat-dialog-title>{{data.title}}</h1>
<div mat-dialog-content>
<ng-content select="cre-dialog-body"></ng-content>
</div>
<div mat-dialog-actions>
<cre-primary-button (click)="onCancel()">Annuler</cre-primary-button>
<cre-accent-button (click)="onContinue()">Continuer</cre-accent-button>
</div>
</ng-template>

View File

@ -1,4 +1,4 @@
@import "~src/custom-theme"
@import "~src/variables"
.info-banner-wrapper
background-color: $color-primary

View File

@ -1,4 +1,4 @@
@import "~src/custom-theme"
@import "../../../../../custom-theme"
cre-table
display: block

View File

@ -1,4 +1,4 @@
@import "../../../../../custom-theme"
@import "~src/variables"
p, labeled-icon
margin: 0

View File

@ -1,3 +1,6 @@
import {Form, FormControl} from '@angular/forms'
import {filterMap} from '../utils/map.utils'
export class Config {
static readonly INSTANCE_NAME = 'instance.name'
static readonly INSTANCE_LOGO_PATH = 'instance.logo.path'
@ -16,12 +19,46 @@ export class Config {
static readonly JAVA_VERSION = 'env.java.version'
static readonly OPERATING_SYSTEM = 'env.os'
constructor(
public key: string,
public content: string,
public lastUpdated: string,
public requireRestart: boolean,
public editable: boolean
) {
}
static readonly IMAGE_CONFIG_KEYS = [
Config.INSTANCE_LOGO_PATH,
Config.INSTANCE_ICON_PATH
]
public key: string
public requireRestart: boolean
public editable: boolean
public content?: string
public lastUpdated?: string
}
export class ConfigKeyContent {
public key: string
public content: string
}
export function filterConfigKeyControlMap(map: Map<string, FormControl>): Map<string, FormControl> {
return filterMap(map, (key, control) => {
return control.dirty &&
Config.IMAGE_CONFIG_KEYS.indexOf(key) < 0 && // Filter image configs because they are sent to a different endpoint
control.value !== undefined &&
control.value !== null
})
}
export function filterImageConfigKeyControlMap(map: Map<string, FormControl>): Map<string, FormControl> {
return filterMap(map, (key, control) => {
return Config.IMAGE_CONFIG_KEYS.indexOf(key) >= 0 && control.dirty
})
}
export function mapToConfigKeyContent(key: string, control: FormControl): ConfigKeyContent {
return {key, content: control.value}
}
export function mapToConfigKeyContentArray(map: Map<string, FormControl>): ConfigKeyContent[] {
const array: ConfigKeyContent[] = []
map.forEach((control, key) => {
array.push(mapToConfigKeyContent(key, control))
})
return array
}

View File

@ -1,13 +1,9 @@
import {Injectable} from '@angular/core'
import {Config} from '../model/config.model'
import {Config, filterConfigKeyControlMap, filterImageConfigKeyControlMap, mapToConfigKeyContentArray} from '../model/config.model'
import {Observable} from 'rxjs'
import {ApiService} from './api.service'
import {FormControl} from '@angular/forms'
const imageConfigsKeys = [
Config.INSTANCE_LOGO_PATH,
Config.INSTANCE_ICON_PATH
]
import {transformMap} from '../utils/map.utils'
@Injectable({
providedIn: 'root'
@ -23,27 +19,10 @@ export class ConfigService {
}
set(configs: Map<string, FormControl>): Observable<void> {
const body = []
for (let key in configs) {
const control = configs[key]
if (control.dirty && key.indexOf('path') < 0) {
body.push({key, content: control.value})
}
}
const subscriptions = []
imageConfigsKeys.forEach(key => {
if (configs[key].dirty) {
subscriptions.push(this.setImage(key, configs[key].value))
}
})
while (subscriptions.length > 0) {
const subscription = subscriptions.pop().subscribe({
next: () => subscription.unsubscribe()
})
}
const body = mapToConfigKeyContentArray(filterConfigKeyControlMap(configs))
const imageConfigs = filterImageConfigKeyControlMap(configs)
this.setImages(imageConfigs)
return this.api.put<void>('/config', body)
}
@ -58,4 +37,19 @@ export class ConfigService {
restart(): Observable<void> {
return this.api.post<void>('/config/restart')
}
private setImages(configs: Map<string, FormControl>) {
const subscriptions = this.getImageConfigsSubscriptions(configs)
while (subscriptions.length > 0) {
const subscription = subscriptions.pop().subscribe({
next: () => subscription.unsubscribe()
})
}
}
private getImageConfigsSubscriptions(configs: Map<string, FormControl>): Observable<void>[] {
return transformMap(configs, (key, control) => {
return this.setImage(key, control.value)
})
}
}

View File

@ -36,6 +36,7 @@ import {InfoBannerModule} from './components/info-banner/info-banner.module'
import {CreFormsModule} from './components/forms/forms.module'
import {VarDirective} from './directives/var.directive'
import {CreColorPreview} from './components/color-preview/color-preview'
import {CreDialogsModule} from './components/dialogs/dialogs.module'
@NgModule({
declarations: [VarDirective, HeaderComponent, UserMenuComponent, LabeledIconComponent, ConfirmBoxComponent, PermissionsListComponent, PermissionsFieldComponent, NavComponent, EntityListComponent, EntityAddComponent, EntityEditComponent, FileButtonComponent, GlobalAlertHandlerComponent, SliderFieldComponent, LoadingWheelComponent, CreColorPreview],
@ -71,7 +72,8 @@ import {CreColorPreview} from './components/color-preview/color-preview'
InfoBannerModule,
CreFormsModule,
VarDirective,
CreColorPreview
CreColorPreview,
CreDialogsModule
],
imports: [
MatTabsModule,

View File

@ -0,0 +1,17 @@
export function filterMap<K, V>(map: Map<K, V>, predicate: (key: K, value: V) => boolean): Map<K, V> {
const filteredMap = new Map<K, V>()
map.forEach((value, key) => {
if (predicate(key, value)) {
filteredMap.set(key, value)
}
})
return filteredMap
}
export function transformMap<K, V, T>(map: Map<K, V>, transform: (key: K, value: V) => T): T[] {
const transformedArray = []
map.forEach((value, key) => {
transformedArray.push(transform(key, value))
})
return transformedArray
}

View File

@ -1,4 +1,4 @@
@import '~src/custom-theme'
@import '../../../../custom-theme'
.touchupkit-finish-container
display: inline-block

View File

@ -122,10 +122,6 @@ $color-recipes-explorer-frontend-theme: mat-light-theme($theme-primary, $theme-a
// that you are using.
@include angular-material-theme($color-recipes-explorer-frontend-theme);
$color-primary: map-get($theme-primary, 500);
$color-accent: map-get($theme-accent, 500);
$color-warn: map-get($theme-error, 500);
html, body {
height: 100%;

View File

@ -1,6 +1,4 @@
@import 'assets/sass/modules/_fonts.sass'
@import "custom-theme"
@import "~material-design-icons/iconfont/material-icons.css"
@import "variables"
mat-card
padding: 0 !important