diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2MTestClient.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2MTestClient.java index 54a21da51e..00a5510788 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2MTestClient.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2MTestClient.java @@ -67,6 +67,7 @@ import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; import static org.eclipse.californium.scandium.config.DtlsConfig.DTLS_CONNECTION_ID_LENGTH; import static org.eclipse.californium.scandium.config.DtlsConfig.DTLS_RECOMMENDED_CIPHER_SUITES_ONLY; @@ -147,9 +148,9 @@ public class LwM2MTestClient { initializer.setClassForObject(SECURITY, Security.class); initializer.setInstancesForObject(SECURITY, instances); // SERVER - Server lwm2mServer = new Server(shortServerId, 300); + Server lwm2mServer = new Server(shortServerId, TimeUnit.MINUTES.toSeconds(60)); lwm2mServer.setId(serverId); - Server serverBs = new Server(shortServerIdBs0, 300); + Server serverBs = new Server(shortServerIdBs0, TimeUnit.MINUTES.toSeconds(60)); serverBs.setId(serverIdBs); instances = new LwM2mInstanceEnabler[]{serverBs, lwm2mServer}; initializer.setClassForObject(SERVER, Server.class); @@ -163,7 +164,7 @@ public class LwM2MTestClient { // SECURITY initializer.setInstancesForObject(SECURITY, security); // SERVER - Server lwm2mServer = new Server(shortServerId, 300); + Server lwm2mServer = new Server(shortServerId, TimeUnit.MINUTES.toSeconds(60)); lwm2mServer.setId(serverId); initializer.setInstancesForObject(SERVER, lwm2mServer ); } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mRedisRegistrationStore.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mRedisRegistrationStore.java index 436fb479e9..b52764187d 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mRedisRegistrationStore.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mRedisRegistrationStore.java @@ -90,7 +90,7 @@ public class TbLwM2mRedisRegistrationStore implements RegistrationStore, Startab private static final Logger LOG = LoggerFactory.getLogger(RedisRegistrationStore.class); // Redis key prefixes - private static final String REG_EP = "REG:EP:"; // (Endpoint => Registration) + public static final String REG_EP = "REG:EP:"; // (Endpoint => Registration) private static final String REG_EP_REGID_IDX = "EP:REGID:"; // secondary index key (Registration ID => Endpoint) private static final String REG_EP_ADDR_IDX = "EP:ADDR:"; // secondary index key (Socket Address => Endpoint) private static final String REG_EP_IDENTITY = "EP:IDENTITY:"; // secondary index key (Identity => Endpoint) diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mRedisSecurityStore.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mRedisSecurityStore.java index ff72850c45..f837931b19 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mRedisSecurityStore.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mRedisSecurityStore.java @@ -15,17 +15,24 @@ */ package org.thingsboard.server.transport.lwm2m.server.store; +import com.fasterxml.jackson.databind.JsonNode; +import lombok.extern.slf4j.Slf4j; import org.eclipse.leshan.core.SecurityMode; import org.eclipse.leshan.core.peer.OscoreIdentity; import org.eclipse.leshan.server.security.NonUniqueSecurityInfoException; import org.eclipse.leshan.server.security.SecurityInfo; +import org.springframework.data.redis.connection.RedisConnection; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.integration.redis.util.RedisLockRegistry; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.JavaSerDesUtil; import org.thingsboard.server.transport.lwm2m.secure.TbLwM2MSecurityInfo; import java.util.concurrent.locks.Lock; +import static org.thingsboard.server.transport.lwm2m.server.store.TbLwM2mRedisRegistrationStore.REG_EP; + +@Slf4j public class TbLwM2mRedisSecurityStore implements TbEditableSecurityStore { private static final String SEC_EP = "SEC#EP#"; private static final String LOCK_EP = "LOCK#EP#"; @@ -49,11 +56,31 @@ public class TbLwM2mRedisSecurityStore implements TbEditableSecurityStore { if (data == null || data.length == 0) { return null; } else { - if (SecurityMode.NO_SEC.equals(((TbLwM2MSecurityInfo) JavaSerDesUtil.decode(data)).getSecurityMode())) { + TbLwM2MSecurityInfo tbLwM2MSecurityInfo = JavaSerDesUtil.decode(data); + if (tbLwM2MSecurityInfo != null) { + if (SecurityMode.NO_SEC.equals(tbLwM2MSecurityInfo.getSecurityMode())){ + + // for tests: redis connect NoSec (securityInfo == null) + log.info("lwm2m redis securityStore (decode -ok). Endpoint: [{}], secMode: [NoSec] key: [{}], data [{}]", endpoint, SEC_EP, data); + + return SecurityInfo.newPreSharedKeyInfo(SecurityMode.NO_SEC.toString(), SecurityMode.NO_SEC.toString(), + SecurityMode.NO_SEC.toString().getBytes()); + } else { + return tbLwM2MSecurityInfo.getSecurityInfo(); + } + } else if (SecurityMode.NO_SEC.equals(getSecurityModeByRegistration (connection, endpoint))){ + + // for tests: redis connect NoSec (securityInfo == null) + log.info("lwm2m redis securityStore (decode - bad, registration - unsecure). Endpoint: [{}], secMode: [NoSec] key: [{}], data [{}]", endpoint, SEC_EP, data); + return SecurityInfo.newPreSharedKeyInfo(SecurityMode.NO_SEC.toString(), SecurityMode.NO_SEC.toString(), SecurityMode.NO_SEC.toString().getBytes()); } else { - return ((TbLwM2MSecurityInfo) JavaSerDesUtil.decode(data)).getSecurityInfo(); + + // for tests: redis connect NoSec (securityInfo == null) + log.info("lwm2m redis securityStore (decode - bad, registration is not unsecure) - return null. Endpoint: [{}], key: [{}], data [{}]", endpoint, SEC_EP, data); + + return null; } } } finally { @@ -112,6 +139,11 @@ public class TbLwM2mRedisSecurityStore implements TbEditableSecurityStore { } byte[] previousData = connection.getSet((SEC_EP + tbSecurityInfo.getEndpoint()).getBytes(), tbSecurityInfoSerialized); + + // for tests: redis connect NoSec (securityInfo == null) + log.info("lwm2m redis connect. Endpoint: [{}], secMode: [{}] key: [{}], tbSecurityInfoSerialized [{}]", + tbSecurityInfo.getEndpoint(), tbSecurityInfo.getSecurityMode().name(), SEC_EP, tbSecurityInfoSerialized); + if (previousData != null && info != null) { String previousIdentity = ((TbLwM2MSecurityInfo) JavaSerDesUtil.decode(previousData)).getSecurityInfo().getPskIdentity(); if (previousIdentity != null && !previousIdentity.equals(info.getPskIdentity())) { @@ -168,4 +200,17 @@ public class TbLwM2mRedisSecurityStore implements TbEditableSecurityStore { private String toLockKey(String endpoint) { return LOCK_EP + endpoint; } + + private SecurityMode getSecurityModeByRegistration (RedisConnection connection, String endpoint) { + try { + byte[] data = connection.get((REG_EP + endpoint).getBytes()); + JsonNode registrationNode = JacksonUtil.fromString(new String(data != null ? data : new byte[0]), JsonNode.class); + String typeModeStr = registrationNode.get("transportdata").get("identity").get("type").asText(); + return "unsecure".equals(typeModeStr) ? SecurityMode.NO_SEC : null; + } catch (Exception e) { + log.error("Redis: Failed get SecurityMode by Registration, endpoint: [{}]", endpoint, e); + return null; + } + + } } diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/lwm2m/LwM2MTestClient.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/lwm2m/LwM2MTestClient.java index 4c20199c8e..9b1ab4bd22 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/lwm2m/LwM2MTestClient.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/lwm2m/LwM2MTestClient.java @@ -61,6 +61,7 @@ import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.concurrent.TimeUnit; import static org.eclipse.californium.scandium.config.DtlsConfig.DTLS_CONNECTION_ID_LENGTH; import static org.eclipse.californium.scandium.config.DtlsConfig.DTLS_RECOMMENDED_CIPHER_SUITES_ONLY; @@ -118,7 +119,7 @@ public class LwM2MTestClient { // SECURITY initializer.setInstancesForObject(SECURITY, security); // SERVER - Server lwm2mServer = new Server(shortServerId, 300); + Server lwm2mServer = new Server(shortServerId, TimeUnit.MINUTES.toSeconds(60)); lwm2mServer.setId(serverId); initializer.setInstancesForObject(SERVER, lwm2mServer); diff --git a/ui-ngx/package.json b/ui-ngx/package.json index a5304207f5..b8a8e4c9a9 100644 --- a/ui-ngx/package.json +++ b/ui-ngx/package.json @@ -43,7 +43,6 @@ "@ngrx/store": "^15.4.0", "@ngrx/store-devtools": "^15.4.0", "@ngx-translate/core": "^14.0.0", - "@ngx-translate/http-loader": "^7.0.0", "@svgdotjs/svg.filter.js": "^3.0.8", "@svgdotjs/svg.js": "^3.2.0", "@tinymce/tinymce-angular": "^7.0.0", diff --git a/ui-ngx/src/app/app.component.ts b/ui-ngx/src/app/app.component.ts index 479e0465e5..cf53929e54 100644 --- a/ui-ngx/src/app/app.component.ts +++ b/ui-ngx/src/app/app.component.ts @@ -27,10 +27,13 @@ import { LocalStorageService } from '@core/local-storage/local-storage.service'; import { DomSanitizer } from '@angular/platform-browser'; import { MatIconRegistry } from '@angular/material/icon'; import { combineLatest } from 'rxjs'; -import { selectIsAuthenticated, selectIsUserLoaded } from '@core/auth/auth.selectors'; -import { distinctUntilChanged, filter, map, skip } from 'rxjs/operators'; +import { getCurrentAuthState, selectIsAuthenticated, selectIsUserLoaded } from '@core/auth/auth.selectors'; +import { distinctUntilChanged, filter, map, skip, tap } from 'rxjs/operators'; import { AuthService } from '@core/auth/auth.service'; import { svgIcons, svgIconsUrl } from '@shared/models/icon.models'; +import { isEqual } from '@core/utils'; +import { ActionSettingsChangeLanguage } from '@core/settings/settings.actions'; +import { SETTINGS_KEY } from '@core/settings/settings.effects'; @Component({ selector: 'tb-root', @@ -92,8 +95,16 @@ export class AppComponent implements OnInit { this.store.pipe(select(selectIsUserLoaded))] ).pipe( map(results => ({isAuthenticated: results[0], isUserLoaded: results[1]})), - distinctUntilChanged(), - filter((data) => data.isUserLoaded ), + filter((data) => data.isUserLoaded), + distinctUntilChanged((a, b) => isEqual(a, b)), + tap((data) => { + let userLang = getCurrentAuthState(this.store).userDetails?.additionalInfo?.lang ?? null; + if (!userLang && !data.isAuthenticated) { + const settings = this.storageService.getItem(SETTINGS_KEY); + userLang = settings?.userLang ?? null; + } + this.notifyUserLang(userLang); + }), skip(1), ).subscribe((data) => { this.authService.gotoDefaultPlace(data.isAuthenticated); @@ -111,4 +122,8 @@ export class AppComponent implements OnInit { } } + private notifyUserLang(userLang: string) { + this.store.dispatch(new ActionSettingsChangeLanguage({userLang})); + } + } diff --git a/ui-ngx/src/app/core/auth/auth.service.ts b/ui-ngx/src/app/core/auth/auth.service.ts index 0cc0ca4567..ea3dfb4143 100644 --- a/ui-ngx/src/app/core/auth/auth.service.ts +++ b/ui-ngx/src/app/core/auth/auth.service.ts @@ -22,7 +22,7 @@ import { Observable, of, ReplaySubject, throwError } from 'rxjs'; import { catchError, map, mergeMap, tap } from 'rxjs/operators'; import { LoginRequest, LoginResponse, PublicLoginRequest } from '@shared/models/login.models'; -import { ActivatedRoute, Router, UrlTree } from '@angular/router'; +import { Router, UrlTree } from '@angular/router'; import { defaultHttpOptions, defaultHttpOptionsFromConfig, RequestConfig } from '../http/http-utils'; import { UserService } from '../http/user.service'; import { Store } from '@ngrx/store'; @@ -35,7 +35,6 @@ import { } from './auth.actions'; import { getCurrentAuthState, getCurrentAuthUser } from './auth.selectors'; import { Authority } from '@shared/models/authority.enum'; -import { ActionSettingsChangeLanguage } from '@app/core/settings/settings.actions'; import { AuthPayload, AuthState, SysParams, SysParamsState } from '@core/auth/auth.models'; import { TranslateService } from '@ngx-translate/core'; import { AuthUser } from '@shared/models/user.model'; @@ -59,7 +58,6 @@ export class AuthService { private userService: UserService, private timeService: TimeService, private router: Router, - private route: ActivatedRoute, private zone: NgZone, private utils: UtilsService, private translate: TranslateService, @@ -419,14 +417,7 @@ export class AuthService { this.loadSystemParams().subscribe( (sysParams) => { authPayload = {...authPayload, ...sysParams}; - let userLang; - if (authPayload.userDetails.additionalInfo && authPayload.userDetails.additionalInfo.lang) { - userLang = authPayload.userDetails.additionalInfo.lang; - } else { - userLang = null; - } loadUserSubject.next(authPayload); - this.notifyUserLang(userLang); loadUserSubject.complete(); }, (err) => { @@ -607,10 +598,6 @@ export class AuthService { this.store.dispatch(new ActionAuthAuthenticated(authPayload)); } - private notifyUserLang(userLang: string) { - this.store.dispatch(new ActionSettingsChangeLanguage({userLang})); - } - private updateAndValidateToken(token, prefix, notify) { let valid = false; const tokenData = this.jwtHelper.decodeToken(token); diff --git a/ui-ngx/src/app/core/core.module.ts b/ui-ngx/src/app/core/core.module.ts index 2f713e405f..a72eb1ada7 100644 --- a/ui-ngx/src/app/core/core.module.ts +++ b/ui-ngx/src/app/core/core.module.ts @@ -31,7 +31,6 @@ import { TranslateModule, TranslateParser } from '@ngx-translate/core'; -import { TranslateHttpLoader } from '@ngx-translate/http-loader'; import { TbMissingTranslationHandler } from './translate/missing-translate-handler'; import { MatButtonModule } from '@angular/material/button'; import { MAT_DIALOG_DEFAULT_OPTIONS, MatDialogConfig, MatDialogModule } from '@angular/material/dialog'; @@ -41,9 +40,10 @@ import { TranslateDefaultCompiler } from '@core/translate/translate-default-comp import { WINDOW_PROVIDERS } from '@core/services/window.service'; import { HotkeyModule } from 'angular2-hotkeys'; import { TranslateDefaultParser } from '@core/translate/translate-default-parser'; +import { TranslateDefaultLoader } from '@core/translate/translate-default-loader'; export function HttpLoaderFactory(http: HttpClient) { - return new TranslateHttpLoader(http, './assets/locale/locale.constant-', '.json'); + return new TranslateDefaultLoader(http); } @NgModule({ diff --git a/ui-ngx/src/app/core/services/resources.service.ts b/ui-ngx/src/app/core/services/resources.service.ts index bd2402b44c..449e950a65 100644 --- a/ui-ngx/src/app/core/services/resources.service.ts +++ b/ui-ngx/src/app/core/services/resources.service.ts @@ -34,6 +34,7 @@ import { select, Store } from '@ngrx/store'; import { selectIsAuthenticated } from '@core/auth/auth.selectors'; import { AppState } from '@core/core.state'; import { map, tap } from 'rxjs/operators'; +import { RequestConfig } from '@core/http/http-utils'; declare const System; @@ -106,11 +107,11 @@ export class ResourcesService { return this.loadResourceByType(fileType, url); } - public downloadResource(downloadUrl: string): Observable { - return this.http.get(downloadUrl, { + public downloadResource(downloadUrl: string, config?: RequestConfig): Observable { + return this.http.get(downloadUrl, {...config, ...{ responseType: 'arraybuffer', observe: 'response' - }).pipe( + }}).pipe( map((response) => { const headers = response.headers; const filename = headers.get('x-filename'); diff --git a/ui-ngx/src/app/core/settings/settings.effects.ts b/ui-ngx/src/app/core/settings/settings.effects.ts index 56615bf62c..1564814a74 100644 --- a/ui-ngx/src/app/core/settings/settings.effects.ts +++ b/ui-ngx/src/app/core/settings/settings.effects.ts @@ -28,7 +28,6 @@ import { AppState } from '@app/core/core.state'; import { LocalStorageService } from '@app/core/local-storage/local-storage.service'; import { TitleService } from '@app/core/services/title.service'; import { updateUserLang } from '@app/core/settings/settings.utils'; -import { AuthService } from '@core/auth/auth.service'; import { UtilsService } from '@core/services/utils.service'; import { getCurrentAuthUser } from '@core/auth/auth.selectors'; import { ActionAuthUpdateLastPublicDashboardId } from '../auth/auth.actions'; @@ -40,7 +39,6 @@ export class SettingsEffects { constructor( private actions$: Actions, private store: Store, - private authService: AuthService, private utils: UtilsService, private router: Router, private localStorageService: LocalStorageService, @@ -49,26 +47,19 @@ export class SettingsEffects { ) { } - - persistSettings = createEffect(() => this.actions$.pipe( + setTranslateServiceLanguage = createEffect(() => this.actions$.pipe( ofType( SettingsActionTypes.CHANGE_LANGUAGE, ), withLatestFrom(this.store.pipe(select(selectSettingsState))), - tap(([action, settings]) => - this.localStorageService.setItem(SETTINGS_KEY, settings) - ) + map(settings => settings[1]), + distinctUntilChanged((a, b) => a?.userLang === b?.userLang), + tap(setting => { + this.localStorageService.setItem(SETTINGS_KEY, setting); + updateUserLang(this.translate, setting.userLang); + }) ), {dispatch: false}); - - setTranslateServiceLanguage = createEffect(() => this.store.pipe( - select(selectSettingsState), - map(settings => settings.userLang), - distinctUntilChanged(), - tap(userLang => updateUserLang(this.translate, userLang)) - ), {dispatch: false}); - - setTitle = createEffect(() => merge( this.actions$.pipe(ofType(SettingsActionTypes.CHANGE_LANGUAGE)), this.router.events.pipe(filter(event => event instanceof ActivationEnd)) @@ -81,7 +72,6 @@ export class SettingsEffects { }) ), {dispatch: false}); - setPublicId = createEffect(() => merge( this.router.events.pipe(filter(event => event instanceof ActivationEnd)) ).pipe( diff --git a/ui-ngx/src/app/core/settings/settings.utils.ts b/ui-ngx/src/app/core/settings/settings.utils.ts index dfe8b37519..e816bbffb0 100644 --- a/ui-ngx/src/app/core/settings/settings.utils.ts +++ b/ui-ngx/src/app/core/settings/settings.utils.ts @@ -18,7 +18,7 @@ import { environment as env } from '@env/environment'; import { TranslateService } from '@ngx-translate/core'; import * as _moment from 'moment'; -export function updateUserLang(translate: TranslateService, userLang: string) { +export function updateUserLang(translate: TranslateService, userLang: string, translations = env.supportedLangs) { let targetLang = userLang; if (!env.production) { console.log(`User lang: ${targetLang}`); @@ -29,7 +29,7 @@ export function updateUserLang(translate: TranslateService, userLang: string) { console.log(`Fallback to browser lang: ${targetLang}`); } } - const detectedSupportedLang = detectSupportedLang(targetLang); + const detectedSupportedLang = detectSupportedLang(targetLang, translations); if (!env.production) { console.log(`Detected supported lang: ${detectedSupportedLang}`); } @@ -37,10 +37,10 @@ export function updateUserLang(translate: TranslateService, userLang: string) { _moment.locale([detectedSupportedLang]); } -function detectSupportedLang(targetLang: string): string { +function detectSupportedLang(targetLang: string, translations: string[]): string { const langTag = (targetLang || '').split('-').join('_'); if (langTag.length) { - if (env.supportedLangs.indexOf(langTag) > -1) { + if (translations.indexOf(langTag) > -1) { return langTag; } else { const parts = langTag.split('_'); @@ -50,7 +50,7 @@ function detectSupportedLang(targetLang: string): string { } else { lang = langTag; } - const foundLangs = env.supportedLangs.filter( + const foundLangs = translations.filter( (supportedLang: string) => { const supportedLangParts = supportedLang.split('_'); return supportedLangParts[0] === lang; diff --git a/ui-ngx/src/app/core/translate/translate-default-loader.ts b/ui-ngx/src/app/core/translate/translate-default-loader.ts new file mode 100644 index 0000000000..7f26170c9c --- /dev/null +++ b/ui-ngx/src/app/core/translate/translate-default-loader.ts @@ -0,0 +1,30 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { TranslateLoader } from '@ngx-translate/core'; +import { Observable } from 'rxjs'; +import { HttpClient } from '@angular/common/http'; + +export class TranslateDefaultLoader implements TranslateLoader { + + constructor(private http: HttpClient) { + + } + + getTranslation(lang: string): Observable { + return this.http.get(`/assets/locale/locale.constant-${lang}.json`); + } +} diff --git a/ui-ngx/src/app/core/utils.ts b/ui-ngx/src/app/core/utils.ts index 999d05b3a8..dc9ed5004b 100644 --- a/ui-ngx/src/app/core/utils.ts +++ b/ui-ngx/src/app/core/utils.ts @@ -338,6 +338,8 @@ export const isEmpty = (a: any): boolean => _.isEmpty(a); export const unset = (object: any, path: string | symbol): boolean => _.unset(object, path); +export const setByPath = (object: T, path: string | number | symbol, value: any): T => _.set(object, path, value); + export const isEqualIgnoreUndefined = (a: any, b: any): boolean => { if (a === b) { return true; diff --git a/ui-ngx/src/app/modules/login/pages/login/login.component.html b/ui-ngx/src/app/modules/login/pages/login/login.component.html index c623d61b9a..8a9b9959c3 100644 --- a/ui-ngx/src/app/modules/login/pages/login/login.component.html +++ b/ui-ngx/src/app/modules/login/pages/login/login.component.html @@ -46,7 +46,7 @@ email - {{ 'user.invalid-email-format' | translate }} + {{ 'login.invalid-email-format' | translate }} diff --git a/ui-ngx/src/app/shared/components/json-content.component.html b/ui-ngx/src/app/shared/components/json-content.component.html index 33b1936213..1a09d539c8 100644 --- a/ui-ngx/src/app/shared/components/json-content.component.html +++ b/ui-ngx/src/app/shared/components/json-content.component.html @@ -18,7 +18,7 @@
-
+