Merge pull request '#440' (#441) from #440 into dev
All checks were successful
Deploy dev on push / on-push-deploy_sh-edraft (push) Successful in 4m43s

Reviewed-on: #441
This commit is contained in:
Sven Heidemann 2023-11-19 14:18:54 +01:00
commit 026331b397
14 changed files with 204 additions and 87 deletions

View File

@ -14,7 +14,10 @@ from bot_api.model.reset_password_dto import ResetPasswordDTO
from bot_api.model.token_dto import TokenDTO from bot_api.model.token_dto import TokenDTO
from bot_api.model.update_auth_user_dto import UpdateAuthUserDTO from bot_api.model.update_auth_user_dto import UpdateAuthUserDTO
from bot_api.route.route import Route from bot_api.route.route import Route
from bot_core.configuration.feature_flags_enum import FeatureFlagsEnum
from bot_core.configuration.feature_flags_settings import FeatureFlagsSettings
from bot_data.model.auth_role_enum import AuthRoleEnum from bot_data.model.auth_role_enum import AuthRoleEnum
from bot_data.model.technician_config import TechnicianConfig
class AuthController: class AuthController:
@ -30,6 +33,7 @@ class AuthController:
mail_settings: EMailClientSettings, mail_settings: EMailClientSettings,
mailer: EMailClientABC, mailer: EMailClientABC,
auth_service: AuthServiceABC, auth_service: AuthServiceABC,
technician_config: TechnicianConfig,
): ):
self._config = config self._config = config
self._env = env self._env = env
@ -39,6 +43,7 @@ class AuthController:
self._mail_settings = mail_settings self._mail_settings = mail_settings
self._mailer = mailer self._mailer = mailer
self._auth_service = auth_service self._auth_service = auth_service
self._technician_config = technician_config
@Route.get(f"{BasePath}/users") @Route.get(f"{BasePath}/users")
@Route.authorize(role=AuthRoleEnum.admin) @Route.authorize(role=AuthRoleEnum.admin)
@ -70,17 +75,32 @@ class AuthController:
@Route.post(f"{BasePath}/register") @Route.post(f"{BasePath}/register")
async def register(self): async def register(self):
if not FeatureFlagsSettings.get_flag_from_dict(
self._technician_config.feature_flags, FeatureFlagsEnum.basic_registration
):
return
dto: AuthUserDTO = JSONProcessor.process(AuthUserDTO, request.get_json(force=True, silent=True)) dto: AuthUserDTO = JSONProcessor.process(AuthUserDTO, request.get_json(force=True, silent=True))
self._auth_service.add_auth_user(dto) self._auth_service.add_auth_user(dto)
return "", 200 return "", 200
@Route.post(f"{BasePath}/register-by-id/<id>") @Route.post(f"{BasePath}/register-by-id/<id>")
async def register_id(self, id: str): async def register_id(self, id: str):
if not FeatureFlagsSettings.get_flag_from_dict(
self._technician_config.feature_flags, FeatureFlagsEnum.basic_registration
):
return
result = await self._auth_service.confirm_email_async(id) result = await self._auth_service.confirm_email_async(id)
return jsonify(result) return jsonify(result)
@Route.post(f"{BasePath}/login") @Route.post(f"{BasePath}/login")
async def login(self) -> Response: async def login(self) -> Response:
if not FeatureFlagsSettings.get_flag_from_dict(
self._technician_config.feature_flags, FeatureFlagsEnum.basic_login
):
return jsonify({})
dto: AuthUserDTO = JSONProcessor.process(AuthUserDTO, request.get_json(force=True, silent=True)) dto: AuthUserDTO = JSONProcessor.process(AuthUserDTO, request.get_json(force=True, silent=True))
result = await self._auth_service.login_async(dto) result = await self._auth_service.login_async(dto)
return jsonify(result.to_dict()) return jsonify(result.to_dict())
@ -100,6 +120,11 @@ class AuthController:
@Route.post(f"{BasePath}/forgot-password/<email>") @Route.post(f"{BasePath}/forgot-password/<email>")
async def forgot_password(self, email: str): async def forgot_password(self, email: str):
if not FeatureFlagsSettings.get_flag_from_dict(
self._technician_config.feature_flags, FeatureFlagsEnum.basic_login
):
return "", 409
await self._auth_service.forgot_password_async(email) await self._auth_service.forgot_password_async(email)
return "", 200 return "", 200
@ -110,6 +135,11 @@ class AuthController:
@Route.post(f"{BasePath}/reset-password") @Route.post(f"{BasePath}/reset-password")
async def reset_password(self): async def reset_password(self):
if not FeatureFlagsSettings.get_flag_from_dict(
self._technician_config.feature_flags, FeatureFlagsEnum.basic_login
):
return "", 409
dto: ResetPasswordDTO = JSONProcessor.process(ResetPasswordDTO, request.get_json(force=True, silent=True)) dto: ResetPasswordDTO = JSONProcessor.process(ResetPasswordDTO, request.get_json(force=True, silent=True))
await self._auth_service.reset_password_async(dto) await self._auth_service.reset_password_async(dto)
return "", 200 return "", 200

View File

@ -12,6 +12,9 @@ from bot_api.logging.api_logger import ApiLogger
from bot_api.model.settings_dto import SettingsDTO from bot_api.model.settings_dto import SettingsDTO
from bot_api.model.version_dto import VersionDTO from bot_api.model.version_dto import VersionDTO
from bot_api.route.route import Route from bot_api.route.route import Route
from bot_core.configuration.feature_flags_enum import FeatureFlagsEnum
from bot_core.configuration.feature_flags_settings import FeatureFlagsSettings
from bot_data.model.technician_config import TechnicianConfig
class GuiController: class GuiController:
@ -82,3 +85,11 @@ class GuiController:
) )
self._mailer.send_mail(mail) self._mailer.send_mail(mail)
return "", 200 return "", 200
@Route.get(f"{BasePath}/has-feature-flag/<flag>")
async def has_feature_flag(self, flag: str):
settings: TechnicianConfig = self._config.get_configuration(TechnicianConfig)
return {
"key": flag,
"value": FeatureFlagsSettings.get_flag_from_dict(settings.feature_flags, FeatureFlagsEnum(flag)),
}

View File

@ -28,3 +28,5 @@ class FeatureFlagsEnum(Enum):
technician_full_access = "TechnicianFullAccess" technician_full_access = "TechnicianFullAccess"
steam_special_offers = "SteamSpecialOffers" steam_special_offers = "SteamSpecialOffers"
scheduled_events = "ScheduledEvents" scheduled_events = "ScheduledEvents"
basic_registration = "BasicRegistration"
basic_login = "BasicLogin"

View File

@ -30,6 +30,8 @@ class FeatureFlagsSettings(ConfigurationModelABC):
FeatureFlagsEnum.technician_full_access.value: False, # 03.10.2023 #393 FeatureFlagsEnum.technician_full_access.value: False, # 03.10.2023 #393
FeatureFlagsEnum.steam_special_offers.value: False, # 11.10.2023 #188 FeatureFlagsEnum.steam_special_offers.value: False, # 11.10.2023 #188
FeatureFlagsEnum.scheduled_events.value: False, # 14.11.2023 #410 FeatureFlagsEnum.scheduled_events.value: False, # 14.11.2023 #410
FeatureFlagsEnum.basic_registration.value: False, # 19.11.2023 #440
FeatureFlagsEnum.basic_login.value: False, # 19.11.2023 #440
} }
def __init__(self, **kwargs: dict): def __init__(self, **kwargs: dict):

View File

@ -50,4 +50,6 @@ type Query {
technicianConfig: TechnicianConfig technicianConfig: TechnicianConfig
possibleFeatureFlags: [String] possibleFeatureFlags: [String]
discord: Discord discord: Discord
hasFeatureFlag(flag: String): FeatureFlag
} }

View File

@ -1,7 +1,9 @@
from cpl_core.configuration import ConfigurationABC
from cpl_discord.service import DiscordBotServiceABC from cpl_discord.service import DiscordBotServiceABC
from cpl_query.extension import List from cpl_query.extension import List
from bot_core.configuration.feature_flags_enum import FeatureFlagsEnum from bot_core.configuration.feature_flags_enum import FeatureFlagsEnum
from bot_core.configuration.feature_flags_settings import FeatureFlagsSettings
from bot_data.abc.achievement_repository_abc import AchievementRepositoryABC from bot_data.abc.achievement_repository_abc import AchievementRepositoryABC
from bot_data.abc.auto_role_repository_abc import AutoRoleRepositoryABC from bot_data.abc.auto_role_repository_abc import AutoRoleRepositoryABC
from bot_data.abc.client_repository_abc import ClientRepositoryABC from bot_data.abc.client_repository_abc import ClientRepositoryABC
@ -22,6 +24,7 @@ from bot_data.abc.user_joined_voice_channel_repository_abc import (
from bot_data.abc.user_repository_abc import UserRepositoryABC from bot_data.abc.user_repository_abc import UserRepositoryABC
from bot_data.abc.user_warnings_repository_abc import UserWarningsRepositoryABC from bot_data.abc.user_warnings_repository_abc import UserWarningsRepositoryABC
from bot_data.model.short_role_name_position_enum import ShortRoleNamePositionEnum from bot_data.model.short_role_name_position_enum import ShortRoleNamePositionEnum
from bot_data.model.technician_config import TechnicianConfig
from bot_graphql.abc.query_abc import QueryABC from bot_graphql.abc.query_abc import QueryABC
from bot_graphql.filter.achievement_filter import AchievementFilter from bot_graphql.filter.achievement_filter import AchievementFilter
from bot_graphql.filter.auto_role_filter import AutoRoleFilter from bot_graphql.filter.auto_role_filter import AutoRoleFilter
@ -45,6 +48,7 @@ from modules.achievements.achievement_service import AchievementService
class Query(QueryABC): class Query(QueryABC):
def __init__( def __init__(
self, self,
config: ConfigurationABC,
bot: DiscordBotServiceABC, bot: DiscordBotServiceABC,
auto_roles: AutoRoleRepositoryABC, auto_roles: AutoRoleRepositoryABC,
clients: ClientRepositoryABC, clients: ClientRepositoryABC,
@ -65,6 +69,8 @@ class Query(QueryABC):
): ):
QueryABC.__init__(self, "Query") QueryABC.__init__(self, "Query")
self._config = config
self.add_collection("autoRole", lambda *_: auto_roles.get_auto_roles(), AutoRoleFilter) self.add_collection("autoRole", lambda *_: auto_roles.get_auto_roles(), AutoRoleFilter)
self.add_collection( self.add_collection(
"autoRoleRule", "autoRoleRule",
@ -120,3 +126,17 @@ class Query(QueryABC):
self.set_field("possibleFeatureFlags", lambda *_: [e.value for e in FeatureFlagsEnum]) self.set_field("possibleFeatureFlags", lambda *_: [e.value for e in FeatureFlagsEnum])
self.set_field("discord", lambda *_: Discord(bot.guilds, List(any).extend(bot.users))) self.set_field("discord", lambda *_: Discord(bot.guilds, List(any).extend(bot.users)))
self.set_field(
"hasFeatureFlag",
lambda *_, **kwargs: self._resolve_has_feature_flag(*_, **kwargs),
)
def _resolve_has_feature_flag(self, *_, **kwargs):
settings: TechnicianConfig = self._config.get_configuration(TechnicianConfig)
if "flag" not in kwargs:
return False
return {
"key": kwargs["flag"],
"value": FeatureFlagsSettings.get_flag_from_dict(settings.feature_flags, FeatureFlagsEnum(kwargs["flag"])),
}

View File

@ -1,6 +1,6 @@
{ {
"name": "web", "name": "web",
"version": "1.2.dev410", "version": "1.2.2",
"scripts": { "scripts": {
"ng": "ng", "ng": "ng",
"update-version": "ts-node update-version.ts", "update-version": "ts-node update-version.ts",

View File

@ -108,6 +108,15 @@ export class Queries {
} }
`; `;
static hasFeatureFlag = `
query HasFeatureFlag($flag: String) {
hasFeatureFlag(flag: $flag) {
key
value
}
}
`;
static hasServerFeatureFlag = ` static hasServerFeatureFlag = `
query HasServerFeatureFlag($filter: ServerFilter, $flag: String) { query HasServerFeatureFlag($filter: ServerFilter, $flag: String) {
servers(filter: $filter) { servers(filter: $filter) {

View File

@ -4,7 +4,9 @@
<form [formGroup]="loginForm"> <form [formGroup]="loginForm">
<h1>{{'auth.header' | translate}}</h1> <h1>{{'auth.header' | translate}}</h1>
<div class="input-field"> <div class="input-field">
<input type="email" pInputText formControlName="email" placeholder="{{'common.email' | translate}}" [ngClass]="{ 'invalid-feedback-input': submitted && ( <input [disabled]="!basicLoginFeatureFlags" type="email" pInputText formControlName="email"
placeholder="{{'common.email' | translate}}"
[ngClass]="{ 'invalid-feedback-input': submitted && (
(loginForm.controls.email.errors && loginForm.controls.email.errors['required'] || authUserAtrErrors.email.required) || (loginForm.controls.email.errors && loginForm.controls.email.errors['required'] || authUserAtrErrors.email.required) ||
(authUserAtrErrors.email.wrongData) || (authUserAtrErrors.email.wrongData) ||
(authUserAtrErrors.email.notConfirmed) (authUserAtrErrors.email.notConfirmed)
@ -24,7 +26,8 @@
styleClass="p-password p-component p-inputwrapper p-input-icon-right" styleClass="p-password p-component p-inputwrapper p-input-icon-right"
Remove after update! Remove after update!
--> -->
<p-password formControlName="password" placeholder="{{'auth.login.password' | translate}}" [ngClass]="{ 'invalid-feedback-input': submitted && ( <p-password [disabled]="!basicLoginFeatureFlags" formControlName="password"
placeholder="{{'auth.login.password' | translate}}" [ngClass]="{ 'invalid-feedback-input': submitted && (
(loginForm.controls.password.errors && loginForm.controls.password.errors['required'] || authUserAtrErrors.password.required) || (loginForm.controls.password.errors && loginForm.controls.password.errors['required'] || authUserAtrErrors.password.required) ||
(authUserAtrErrors.password.wrongData) (authUserAtrErrors.password.wrongData)
)}" [toggleMask]="true" [feedback]="false" )}" [toggleMask]="true" [feedback]="false"
@ -39,17 +42,18 @@
</div> </div>
<div class="login-form-submit"> <div class="login-form-submit">
<button pButton label="{{'auth.login.login' | translate}}" class="btn login-form-submit-btn" (click)="login()" <button pButton label="{{'auth.login.login' | translate}}" class="btn login-form-submit-btn" (click)="login()"
[disabled]="loginForm.invalid"></button> [disabled]="loginForm.invalid || !basicLoginFeatureFlags"></button>
</div> </div>
<div class="login-form-sub-button-wrapper" *ngIf="!code && !state"> <div class="login-form-sub-button-wrapper" *ngIf="!code && !state">
<div class="login-form-sub-btn-wrapper"> <div class="login-form-sub-btn-wrapper">
<button pButton label="{{'auth.login.login_with_discord' | translate}}" class="btn login-form-sub-btn" (click)="discordLogin()"></button> <button pButton label="{{'auth.login.login_with_discord' | translate}}" class="btn login-form-sub-btn"
(click)="discordLogin()"></button>
</div> </div>
</div> </div>
<div class="login-form-sub-button-wrapper"> <div class="login-form-sub-button-wrapper">
<div class="login-form-sub-btn-wrapper"> <div class="login-form-sub-btn-wrapper">
<button pButton label="{{'auth.login.register' | translate}}" class="btn login-form-sub-btn" <button pButton label="{{'auth.login.register' | translate}}" class="btn login-form-sub-btn"
(click)="register()"></button> (click)="register()" [disabled]="!basicRegistrationFeatureFlags"></button>
</div> </div>
<div class="login-form-sub-btn-wrapper"> <div class="login-form-sub-btn-wrapper">
<button pButton label="{{'auth.login.forgot_password' | translate}}" <button pButton label="{{'auth.login.forgot_password' | translate}}"

View File

@ -13,6 +13,7 @@ import { ThemeService } from "src/app/services/theme/theme.service";
import { throwError } from "rxjs"; import { throwError } from "rxjs";
import { TranslateService } from "@ngx-translate/core"; import { TranslateService } from "@ngx-translate/core";
import { ConfirmationDialogService } from "../../../../services/confirmation-dialog/confirmation-dialog.service"; import { ConfirmationDialogService } from "../../../../services/confirmation-dialog/confirmation-dialog.service";
import { GuiService } from "../../../../services/gui/gui.service";
@Component({ @Component({
selector: "app-login", selector: "app-login",
@ -32,6 +33,8 @@ export class LoginComponent implements OnInit {
state!: string; state!: string;
user!: AuthUserDTO; user!: AuthUserDTO;
oAuthId!: string; oAuthId!: string;
public basicLoginFeatureFlags = false;
public basicRegistrationFeatureFlags = false;
constructor( constructor(
private authService: AuthService, private authService: AuthService,
@ -41,11 +44,19 @@ export class LoginComponent implements OnInit {
private themeService: ThemeService, private themeService: ThemeService,
private route: ActivatedRoute, private route: ActivatedRoute,
private confirmDialog: ConfirmationDialogService, private confirmDialog: ConfirmationDialogService,
private translate: TranslateService private translate: TranslateService,
private gui: GuiService
) { ) {
} }
ngOnInit(): void { ngOnInit(): void {
this.gui.hasFeatureFlag("BasicLogin").subscribe(flag => {
this.basicLoginFeatureFlags = flag.value;
});
this.gui.hasFeatureFlag("BasicRegistration").subscribe(flag => {
this.basicRegistrationFeatureFlags = flag.value;
});
this.initLoginForm(); this.initLoginForm();
this.spinnerService.showSpinner(); this.spinnerService.showSpinner();
if (this.authService.isLoggedIn$.value) { if (this.authService.isLoggedIn$.value) {
@ -107,8 +118,8 @@ export class LoginComponent implements OnInit {
initLoginForm(): void { initLoginForm(): void {
this.loginForm = this.formBuilder.group({ this.loginForm = this.formBuilder.group({
email: ["", [Validators.required, Validators.email]], email: [{ value: "", disabled: !this.basicLoginFeatureFlags }, [Validators.required, Validators.email]],
password: ["", [Validators.required, Validators.minLength(8)]] password: [{ value: "", disabled: !this.basicLoginFeatureFlags }, [Validators.required, Validators.minLength(8)]]
}); });
} }

View File

@ -2,9 +2,10 @@
<div class="login-form-wrapper register-form-wrapper"> <div class="login-form-wrapper register-form-wrapper">
<div class="login-form"> <div class="login-form">
<form [formGroup]="loginForm"> <form [formGroup]="loginForm">
<h1>sh-edraft.de</h1> <h1>{{'auth.header' | translate}}</h1>
<div class="input-field"> <div class="input-field">
<input type="text" pInputText formControlName="firstName" placeholder="{{'auth.register.first_name' | translate}}" <input type="text" pInputText formControlName="firstName"
placeholder="{{'auth.register.first_name' | translate}}"
autocomplete="given-name"> autocomplete="given-name">
<div *ngIf="submitted" class="invalid-feedback"> <div *ngIf="submitted" class="invalid-feedback">
<div <div
@ -15,7 +16,8 @@
</div> </div>
<div class="input-field"> <div class="input-field">
<input type="text" pInputText formControlName="lastName" placeholder="{{'auth.register.last_name' | translate}}" <input type="text" pInputText formControlName="lastName"
placeholder="{{'auth.register.last_name' | translate}}"
autocomplete="family-name"> autocomplete="family-name">
<div *ngIf="submitted" class="invalid-feedback"> <div *ngIf="submitted" class="invalid-feedback">
<div <div
@ -74,12 +76,14 @@
</div> </div>
<div class="login-form-submit"> <div class="login-form-submit">
<button pButton label="{{'auth.register.register' | translate}}" class="btn login-form-submit-btn" (click)="register()" <button pButton label="{{'auth.register.register' | translate}}" class="btn login-form-submit-btn"
(click)="register()"
[disabled]="loginForm.invalid"></button> [disabled]="loginForm.invalid"></button>
</div> </div>
<div class="login-form-sub-button-wrapper"> <div class="login-form-sub-button-wrapper">
<div class="login-form-sub-btn-wrapper"> <div class="login-form-sub-btn-wrapper">
<button pButton label="{{'auth.register.login' | translate}}" class="btn login-form-sub-btn" (click)="login()"></button> <button pButton label="{{'auth.register.login' | translate}}" class="btn login-form-sub-btn"
(click)="login()"></button>
</div> </div>
</div> </div>
</form> </form>

View File

@ -12,6 +12,7 @@ import { SpinnerService } from "src/app/services/spinner/spinner.service";
import { Subject, throwError } from "rxjs"; import { Subject, throwError } from "rxjs";
import { TranslateService } from "@ngx-translate/core"; import { TranslateService } from "@ngx-translate/core";
import { SettingsService } from "../../../../services/settings/settings.service"; import { SettingsService } from "../../../../services/settings/settings.service";
import { GuiService } from "../../../../services/gui/gui.service";
@Component({ @Component({
selector: "app-registration", selector: "app-registration",
@ -46,7 +47,8 @@ export class RegistrationComponent implements OnInit, OnDestroy {
private spinnerService: SpinnerService, private spinnerService: SpinnerService,
private route: ActivatedRoute, private route: ActivatedRoute,
private translate: TranslateService, private translate: TranslateService,
private settings: SettingsService private settings: SettingsService,
private gui: GuiService
) { ) {
this.spinnerService.showSpinner(); this.spinnerService.showSpinner();
if (this.authService.isLoggedIn$.value) { if (this.authService.isLoggedIn$.value) {
@ -56,6 +58,14 @@ export class RegistrationComponent implements OnInit, OnDestroy {
} }
ngOnInit(): void { ngOnInit(): void {
this.gui.hasFeatureFlag("BasicRegistration").subscribe(flag => {
if (flag.value) {
return;
}
this.router.navigate(["/auth/login"]);
});
this.translate.onLangChange.pipe(takeUntil(this.unsubscriber)).subscribe(lang => { this.translate.onLangChange.pipe(takeUntil(this.unsubscriber)).subscribe(lang => {
this.confirmPrivacyString = this.translate.instant("auth.register.confirm_privacy", { url: this.settings.getPrivacyURL() }); this.confirmPrivacyString = this.translate.instant("auth.register.confirm_privacy", { url: this.settings.getPrivacyURL() });
}); });

View File

@ -1,24 +1,25 @@
import { HttpClient, HttpHeaders } from '@angular/common/http'; import { HttpClient, HttpHeaders } from "@angular/common/http";
import { Injectable } from '@angular/core'; import { Injectable } from "@angular/core";
import { Observable, Subscribable } from 'rxjs'; import { Observable } from "rxjs";
import { SettingsDTO } from 'src/app/models/config/settings.dto'; import { SettingsDTO } from "src/app/models/config/settings.dto";
import { SoftwareVersionDTO } from 'src/app/models/config/software-version.dto'; import { SoftwareVersionDTO } from "src/app/models/config/software-version.dto";
import { SettingsService } from '../settings/settings.service'; import { SettingsService } from "../settings/settings.service";
@Injectable({ @Injectable({
providedIn: 'root' providedIn: "root"
}) })
export class GuiService { export class GuiService {
constructor( constructor(
private appsettings: SettingsService, private appsettings: SettingsService,
private http: HttpClient, private http: HttpClient
) { } ) {
}
getApiVersion(): Observable<SoftwareVersionDTO> { getApiVersion(): Observable<SoftwareVersionDTO> {
return this.http.get<SoftwareVersionDTO>(`${this.appsettings.getApiURL()}/api/gui/api-version`, { return this.http.get<SoftwareVersionDTO>(`${this.appsettings.getApiURL()}/api/gui/api-version`, {
headers: new HttpHeaders({ headers: new HttpHeaders({
'Content-Type': 'application/json' "Content-Type": "application/json"
}) })
}); });
} }
@ -26,7 +27,7 @@ export class GuiService {
getSettings(): Observable<SettingsDTO> { getSettings(): Observable<SettingsDTO> {
return this.http.get<SettingsDTO>(`${this.appsettings.getApiURL()}/api/gui/settings`, { return this.http.get<SettingsDTO>(`${this.appsettings.getApiURL()}/api/gui/settings`, {
headers: new HttpHeaders({ headers: new HttpHeaders({
'Content-Type': 'application/json' "Content-Type": "application/json"
}) })
}); });
} }
@ -34,7 +35,18 @@ export class GuiService {
sendTestMail(mail: string): Observable<unknown> { sendTestMail(mail: string): Observable<unknown> {
return this.http.post(`${this.appsettings.getApiURL()}/api/gui/send-test-mail/${mail}`, { return this.http.post(`${this.appsettings.getApiURL()}/api/gui/send-test-mail/${mail}`, {
headers: new HttpHeaders({ headers: new HttpHeaders({
'Content-Type': 'application/json' "Content-Type": "application/json"
})
});
}
hasFeatureFlag(flag: string): Observable<{ key: string, value: boolean }> {
return this.http.get<{
key: string,
value: boolean
}>(`${this.appsettings.getApiURL()}/api/gui/has-feature-flag/${flag}`, {
headers: new HttpHeaders({
"Content-Type": "application/json"
}) })
}); });
} }

View File

@ -2,6 +2,6 @@
"WebVersion": { "WebVersion": {
"Major": "1", "Major": "1",
"Minor": "2", "Minor": "2",
"Micro": "dev410" "Micro": "2"
} }
} }