diff --git a/CHANGELOG.md b/CHANGELOG.md index 46a7cf91..9b5223ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline #### New features * **(shared)** : Navigation, possibilité de forcer l'affichage pleine page (*full*) pour les fonctionnalités positionnées dans le menu *tabs*. +* **(multi-tenant)** : On peut désormais avoir plusieurs établissements (tenants) dans la même application, chacun pouvant définir son logo, ses traductions et son backend #### Autres * Migration de [NodeJS](https://nodejs.org/docs/latest-v20.x/api/index.html) : Version 18 → Version 20 diff --git a/dev/user-frontend-ionic/README.md b/dev/user-frontend-ionic/README.md index 7e620327..01757348 100644 --- a/dev/user-frontend-ionic/README.md +++ b/dev/user-frontend-ionic/README.md @@ -257,3 +257,237 @@ Exemple 2 : Alors le layout FULL est utilisé pour la route /schedule/calendar#month Et le layout TABS est utilisé pour les routes /schedule/list, /schedule/calendar#week, /schedule/calendar#day + +### Personnalisation du thème d'un tenant. + +Dans les fichiers src/environments/environment.*.ts, on peut ajouter un thème par défaut dans l'attribut ```defaultTheme``` +``` + production: false, + languages: ['fr', 'en'], + defaultLanguage: 'fr', + firebase: firebasePwaEnvironment, + guidedTourEnabled: true, + defaultTheme: 'default', + tenants: [ + ... + ] +``` +Ce thème est utilisé par défaut si aucun thème n'est défini dans les prefs utilisateurs. + +A la sélection d'un tenant, on sauvegarde l'identifiant du tenant comme étant le thème de l'application et on charge ce thème. +Au démarrage de l'application, on charge le thème sauvegardé dans les préférences utilisateurs. +S'il n'y en a pas, on charge le thème defaultTheme. Si ce dernier n'est pas défini, on ne fait rien. + +Quand on désélectionne un tenant, on utilise defaultTheme ou rien - si ce dernier n'est pas défini. + +Pour appliquer un thème, on ajoute un classe au body du document - à l'instar du thème sombre. + +#### Modification du logo par défaut + +Le fichier d'environnement, qui contient la description des tenants, a été modifié. + +Un paramètre global ```defaultLogo``` a été ajouté. Il s'agit du chemin relatif (par rapport à la racine du projet) du logo à utiliser si on n'a pas de tenant sélectionné ou si le tenant n'a pas son propre logo. + +Un tenant peut avoir son propre logo grace à la propriété ```logo```. Il s'agit du chemin relatif (par rapport à la racine du projet) du logo du tenant. Si la propriété n'est pas présente ou si elle est vide (ex: ''), alors le logo par défaut est utilisé. + +Exemple: +``` +export const environment = { + production: false, + ... + defaultLogo: 'assets/logos/white-logo.svg', + tenants: [ + { + id: 'other', + logo: 'assets/logos/other.svg' + ... +``` + +## Fonctionnalité multi établissement (multi tenant) + +Dans les fichiers src/environments/environment.*.ts, on peut définir plusieurs établissement à travers l'attribut `tenants` : +``` + tenants: [ + { + id: 'etablissement1', + name: 'Etablissement 1', + logo: 'assets/logos/logo1.svg', + apiEndpoint: 'http://localhost:3000', + cmsPublicAssetsEndpoint: 'http://localhost:8055/assets/', + topic: 'etablissement1', + modulesConfigurations: { + chatbot: { + logoRegex: /_chacha5/i + }, + map: { + defaultLocation: { + longitude: 2.3488596, + latitude: 48.8533249 + } + }, + reservation: { + ssoServiceName: 'https://mon-espace-de-resa.fr', + ssoUrlTemplate: 'https://mon-espace-de-resa.fr/auth?ticket={st}', + } + }, + }, + { + id: 'etablissement2', + name: 'Etablissement 2', + logo: 'assets/logos/logo2.svg', + apiEndpoint: 'http://localhost:3000', + cmsPublicAssetsEndpoint: 'http://localhost:8055/assets/', + topic: 'etablissement2', + modulesConfigurations: { + chatbot: { + logoRegex: /_chacha5/i + }, + map: { + defaultLocation: { + longitude: 2.3488596, + latitude: 48.8533249 + } + }, + reservation: { + ssoServiceName: 'https://mon-espace-de-resa.fr', + ssoUrlTemplate: 'https://mon-espace-de-resa.fr/auth?ticket={st}', + } + }, + } + ], +``` + +Ainsi, on peut facilement configurer un `apiEndpoint` ainsi qu'un `cmsPublicAssetsEndpoint` différent pour chacun d'entre eux afin d'utiliser un backend et/ou un cms différent selon l'établissement sélectionné. + +D'autres configurations peuvent également varier en fonction de l'établissement : +- `topic` firebase utilisé +- `logo` +- `modulesConfigurations` configurations des différents modules pour lesquelles cela a du sens d'avoir une configuration propre par établissement + +### Group de tenants + +Il est également possible de regrouper les tenants dans un groupe de tenant, en définissant l'attribut forceSelect à false, l'application chargera la configuration du groupe sans forcer l'utilisateur à sélectionner un tenant. +Il n'est possible de créer qu'un seul groupe de tenant, celui ci devant alors se situer en première et unique position du tableau de tenant situé à la racine de la configuration: +``` + tenants: [ + { + id: 'etablissement', + name: 'Groupe', + isGroup: true, + forceSelect: false, + logo: 'assets/logos/logo.svg', + apiEndpoint: 'http://localhost:3000', + cmsPublicAssetsEndpoint: 'http://localhost:8055/assets/', + modulesConfigurations: { + chatbot: { + logoRegex: /_chacha5/i + }, + map: { + defaultLocation: { + longitude: 2.3488596, + latitude: 48.8533249 + } + }, + reservation: { + ssoServiceName: 'https://mon-espace-de-resa.fr', + ssoUrlTemplate: 'https://mon-espace-de-resa.fr/auth?ticket={st}', + } + }, + tenants: [ + { + id: 'etablissement1', + name: 'Etablissement 1', + logo: 'assets/logos/logo1.svg', + apiEndpoint: 'http://localhost:3000', + cmsPublicAssetsEndpoint: 'http://localhost:8055/assets/', + topic: 'etablissement1', + modulesConfigurations: { + chatbot: { + logoRegex: /_chacha5/i + }, + map: { + defaultLocation: { + longitude: 2.3488596, + latitude: 48.8533249 + } + }, + reservation: { + ssoServiceName: 'https://mon-espace-de-resa.fr', + ssoUrlTemplate: 'https://mon-espace-de-resa.fr/auth?ticket={st}', + } + }, + }, + { + id: 'etablissement2', + name: 'Etablissement 2', + logo: 'assets/logos/logo2.svg', + apiEndpoint: 'http://localhost:3000', + cmsPublicAssetsEndpoint: 'http://localhost:8055/assets/', + topic: 'etablissement2', + modulesConfigurations: { + chatbot: { + logoRegex: /_chacha5/i + }, + map: { + defaultLocation: { + longitude: 2.3488596, + latitude: 48.8533249 + } + }, + reservation: { + ssoServiceName: 'https://mon-espace-de-resa.fr', + ssoUrlTemplate: 'https://mon-espace-de-resa.fr/auth?ticket={st}', + } + }, + } + ] + } + ], +``` + +### Translations par tenant + +Il est possible de définir des translations par tenant. Pour cela, il suffit de créer un dossier nommé suivant l'id du tenant +à l'endroit où se trouvent les translations que l'on souhaite changer, et y placer les fichiers de translations contenant ces +dernières (en.json, fr.json). + +Par exemple, pour avoir des translations propres à `etablissement1` pour le module `auth`, on aura les fichiers suivants : +- `src/theme/app-theme/i18n/module/auth/en.json` : translations en par défaut +- `src/theme/app-theme/i18n/module/auth/fr.json` : translations fr par défaut +- `src/theme/app-theme/i18n/module/auth/etablissement1/en.json` : translations en propres à `etablissement1` +- `src/theme/app-theme/i18n/module/auth/etablissement1/fr.json` : translations fr propres à `etablissement1` + +Les translations ajoutées de cette manière seront fusionnées avec les translations par défaut, les clés définies dans les nouveaux +fichiers écrasant les mêmes clefs des translations par défaut. + +Ainsi, si on a la translation par défaut fr suivante : + +```json +{ + "MENU": "Menu", + "VERSION" : { + "VERSION": "Version", + "VERSION_NOT_FOUND": "Indisponible" + } +} +``` +.. et que l'on veut changer la clé `VERSION_NOT_FOUND` uniquement, on va créer un fichier `etablissement1/fr.json` : + +```json +{ + "VERSION" : { + "VERSION_NOT_FOUND": "Version indisponible" + } +} +``` + +La translation finale qui sera chargée ressemblera alors à ça : +```json +{ + "MENU": "Menu", + "VERSION" : { + "VERSION": "Version", + "VERSION_NOT_FOUND": "Version indisponible" + } +} +``` diff --git a/dev/user-frontend-ionic/projects/auth/src/lib/auth-routing.module.ts b/dev/user-frontend-ionic/projects/auth/src/lib/auth-routing.module.ts index 471e41ae..d9f5613b 100644 --- a/dev/user-frontend-ionic/projects/auth/src/lib/auth-routing.module.ts +++ b/dev/user-frontend-ionic/projects/auth/src/lib/auth-routing.module.ts @@ -40,11 +40,13 @@ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { LoginPage } from './login/login.page'; +import { IsTenantSelectedGuard } from '@multi/shared'; const routes: Routes = [ { path: 'auth', component: LoginPage, + canActivate: [IsTenantSelectedGuard] } ]; diff --git a/dev/user-frontend-ionic/projects/auth/src/lib/common/auth.service.ts b/dev/user-frontend-ionic/projects/auth/src/lib/common/auth.service.ts index 539f2e9c..76369a64 100644 --- a/dev/user-frontend-ionic/projects/auth/src/lib/common/auth.service.ts +++ b/dev/user-frontend-ionic/projects/auth/src/lib/common/auth.service.ts @@ -40,11 +40,11 @@ import { Injectable } from '@angular/core'; import { Actions } from '@ngneat/effects-ng'; import { - authenticate, AuthenticatedUser, - cleanupPrivateData, getAuthToken, getRefreshAuthToken, updateAuthenticatedUsername + authenticate, AuthenticatedUser, userIsAuthenticated$, + cleanupPrivateData, getAuthToken, getRefreshAuthToken, updateAuthenticatedUsername, MultiTenantService } from '@multi/shared'; -import { Observable } from 'rxjs'; -import { concatMap, take, tap } from 'rxjs/operators'; +import { Observable, withLatestFrom } from 'rxjs'; +import { concatMap, filter, switchMap, take, tap } from 'rxjs/operators'; import { saveCredentialsOnAuthentication$ } from '../preferences/preferences.repository'; import { KeepAuthService } from './keep-auth.service'; import { StandardAuthService } from './standard-auth.service'; @@ -58,7 +58,15 @@ export class AuthService { private actions: Actions, private standardAuthService: StandardAuthService, private keepAuthService: KeepAuthService, - ) { } + private multiTenantService: MultiTenantService, + ) { + // Si l'utilisateur passe en mode anonyme et qu'il est toujours connecté, on le déconnecte + this.multiTenantService.tenantChange$.pipe( + withLatestFrom(userIsAuthenticated$), + filter(([tenant, isAuthenticated]) => tenant === undefined && isAuthenticated), + switchMap(() => this.logout()) + ).subscribe(); + } login(username: string, password: string): Observable { return saveCredentialsOnAuthentication$.pipe( diff --git a/dev/user-frontend-ionic/projects/auth/src/lib/common/keep-auth.service.ts b/dev/user-frontend-ionic/projects/auth/src/lib/common/keep-auth.service.ts index 91746512..d639a253 100644 --- a/dev/user-frontend-ionic/projects/auth/src/lib/common/keep-auth.service.ts +++ b/dev/user-frontend-ionic/projects/auth/src/lib/common/keep-auth.service.ts @@ -38,8 +38,8 @@ */ import { HttpClient, HttpErrorResponse } from '@angular/common/http'; -import { Inject, Injectable } from '@angular/core'; -import { AuthenticatedUser, getAuthToken, updateAuthToken, updateRefreshAuthToken, updateUser } from '@multi/shared'; +import { Injectable } from '@angular/core'; +import { AuthenticatedUser, getAuthToken, MultiTenantService, updateAuthToken, updateRefreshAuthToken, updateUser } from '@multi/shared'; import { EMPTY, Observable, of, throwError, zip } from 'rxjs'; import { catchError, concatMap, delayWhen, take } from 'rxjs/operators'; @@ -55,13 +55,12 @@ export class KeepAuthService { constructor( - @Inject('environment') - private environment: any, + private multiTenantService: MultiTenantService, private http: HttpClient) { } login(username: string, password: string): Observable { - const url = `${this.environment.apiEndpoint}/keep-auth/auth`; + const url = `${this.multiTenantService.getApiEndpoint()}/keep-auth/auth`; const data = { username, password, @@ -96,7 +95,7 @@ export class KeepAuthService { } logout(refreshAuthToken: string): Observable { - const url = `${this.environment.apiEndpoint}/keep-auth/auth`; + const url = `${this.multiTenantService.getApiEndpoint()}/keep-auth/auth`; const headers = { // eslint-disable-next-line @typescript-eslint/naming-convention Authorization: `Bearer ${refreshAuthToken}` @@ -120,7 +119,7 @@ export class KeepAuthService { } removeSavedCredentials(refreshAuthToken: string): Observable { - const url = `${this.environment.apiEndpoint}/keep-auth/reauth`; + const url = `${this.multiTenantService.getApiEndpoint()}/keep-auth/reauth`; const headers = { // eslint-disable-next-line @typescript-eslint/naming-convention Authorization: `Bearer ${refreshAuthToken}` diff --git a/dev/user-frontend-ionic/projects/auth/src/lib/common/login.service.ts b/dev/user-frontend-ionic/projects/auth/src/lib/common/login.service.ts index 733d4b30..edb09115 100644 --- a/dev/user-frontend-ionic/projects/auth/src/lib/common/login.service.ts +++ b/dev/user-frontend-ionic/projects/auth/src/lib/common/login.service.ts @@ -38,7 +38,8 @@ */ import { HttpClient } from '@angular/common/http'; -import { Inject, Injectable } from '@angular/core'; +import { Injectable } from '@angular/core'; +import { MultiTenantService } from '@multi/shared'; import { Observable } from 'rxjs'; import { tap } from 'rxjs/operators'; import { LoginPageContent, LoginRepository } from './login.repository'; @@ -48,13 +49,12 @@ import { LoginPageContent, LoginRepository } from './login.repository'; }) export class LoginService { constructor( - @Inject('environment') - private environment: any, + private multiTenantService: MultiTenantService, private http: HttpClient, private loginRepository: LoginRepository) { } public loadAndStoreLoginPageContent(): Observable { - const url = `${this.environment.apiEndpoint}/auth/login-page-content`; + const url = `${this.multiTenantService.getApiEndpoint()}/auth/login-page-content`; return this.http.get(url).pipe( tap((pageContent) => { diff --git a/dev/user-frontend-ionic/projects/auth/src/lib/common/standard-auth.service.ts b/dev/user-frontend-ionic/projects/auth/src/lib/common/standard-auth.service.ts index b915d6b0..fed68610 100644 --- a/dev/user-frontend-ionic/projects/auth/src/lib/common/standard-auth.service.ts +++ b/dev/user-frontend-ionic/projects/auth/src/lib/common/standard-auth.service.ts @@ -38,8 +38,8 @@ */ import { HttpClient, HttpErrorResponse } from '@angular/common/http'; -import { Inject, Injectable } from '@angular/core'; -import { AuthenticatedUser, getAuthToken, updateAuthToken, updateUser } from '@multi/shared'; +import { Injectable } from '@angular/core'; +import { AuthenticatedUser, getAuthToken, MultiTenantService, updateAuthToken, updateUser } from '@multi/shared'; import { Observable, of, throwError } from 'rxjs'; import { catchError, concatMap, delayWhen, take } from 'rxjs/operators'; @@ -54,13 +54,12 @@ export class StandardAuthService { constructor( - @Inject('environment') - private environment: any, + private multiTenantService: MultiTenantService, private http: HttpClient) { } login(username: string, password: string): Observable { - const url = `${this.environment.apiEndpoint}/auth`; + const url = `${this.multiTenantService.getApiEndpoint()}/auth`; const data = { username, password, @@ -85,7 +84,7 @@ export class StandardAuthService { } logout(): Observable { - const url = `${this.environment.apiEndpoint}/auth`; + const url = `${this.multiTenantService.getApiEndpoint()}/auth`; return getAuthToken().pipe( take(1), diff --git a/dev/user-frontend-ionic/projects/calendar/src/lib/calendar.service.ts b/dev/user-frontend-ionic/projects/calendar/src/lib/calendar.service.ts index 9079d21b..070c234a 100644 --- a/dev/user-frontend-ionic/projects/calendar/src/lib/calendar.service.ts +++ b/dev/user-frontend-ionic/projects/calendar/src/lib/calendar.service.ts @@ -39,7 +39,7 @@ import { HttpClient } from '@angular/common/http'; import { Inject, Injectable } from '@angular/core'; -import { getAuthToken, NetworkService } from '@multi/shared'; +import { getAuthToken, NetworkService, MultiTenantService } from '@multi/shared'; import { isAfter } from 'date-fns'; import { from, iif, Observable, of } from 'rxjs'; import { map, switchMap, take, tap } from 'rxjs/operators'; @@ -55,6 +55,7 @@ export class CalendarService { @Inject(CALENDAR_CONFIG) private config: CalendarModuleConfig, @Inject('environment') private environment: any, + private multiTenantService: MultiTenantService, private http: HttpClient, private networkService: NetworkService ) { } @@ -83,7 +84,7 @@ export class CalendarService { } private getMailCalendar(): Observable { - const url = `${this.environment.apiEndpoint}/mail-calendar`; + const url = `${this.multiTenantService.getApiEndpoint()}/mail-calendar`; return getAuthToken().pipe( take(1), switchMap(authToken => this.http.post(url, { authToken })) diff --git a/dev/user-frontend-ionic/projects/cards/src/lib/cards.service.ts b/dev/user-frontend-ionic/projects/cards/src/lib/cards.service.ts index c7946cab..31f3c863 100644 --- a/dev/user-frontend-ionic/projects/cards/src/lib/cards.service.ts +++ b/dev/user-frontend-ionic/projects/cards/src/lib/cards.service.ts @@ -38,7 +38,8 @@ */ import { HttpClient } from '@angular/common/http'; -import { Inject, Injectable } from '@angular/core'; +import { Injectable } from '@angular/core'; +import { MultiTenantService } from '@multi/shared'; import { Observable } from 'rxjs'; import { UserAndCardsData } from './cards.repository'; @@ -48,14 +49,13 @@ import { UserAndCardsData } from './cards.repository'; export class CardsService { constructor( - @Inject('environment') - private environment: any, + private multiTenantService: MultiTenantService, private http: HttpClient, ) { } public getUserAndCardsData(authToken: string): Observable { - const url = `${this.environment.apiEndpoint}/cards`; + const url = `${this.multiTenantService.getApiEndpoint()}/cards`; const data = { authToken }; diff --git a/dev/user-frontend-ionic/projects/chatbot/src/lib/chatbot.page.ts b/dev/user-frontend-ionic/projects/chatbot/src/lib/chatbot.page.ts index 6456b099..ba20fd9b 100644 --- a/dev/user-frontend-ionic/projects/chatbot/src/lib/chatbot.page.ts +++ b/dev/user-frontend-ionic/projects/chatbot/src/lib/chatbot.page.ts @@ -37,16 +37,16 @@ * termes. */ -import { Component, ElementRef, Inject, OnInit, ViewChild } from '@angular/core'; +import { Component, ElementRef, OnInit, ViewChild } from '@angular/core'; import { Capacitor } from '@capacitor/core'; import { Keyboard } from '@capacitor/keyboard'; import { IonContent } from '@ionic/angular'; import { BehaviorSubject } from 'rxjs'; import { finalize, take } from 'rxjs/operators'; -import { ChatbotModuleConfig, CHATBOT_CONFIG } from './chatbot.config'; import { ChatbotMessage, ChatButton, Message, MessageType, UserMessage } from './chatbot.dto'; import { ChatbotService } from './chatbot.service'; import { UserIdGeneratorService } from './user-id-generator.service'; +import { MultiTenantService } from '@multi/shared'; @Component({ selector: 'app-chatbot', @@ -69,7 +69,7 @@ export class ChatbotPage implements OnInit { private domMessageListObserver: MutationObserver; constructor( - @Inject(CHATBOT_CONFIG) private config: ChatbotModuleConfig, + private multiTenantService: MultiTenantService, private chatbotService: ChatbotService ) { } @@ -164,7 +164,7 @@ export class ChatbotPage implements OnInit { } isChatbotLogo(message: ChatbotMessage) { - return message?.card?.file?.type === 'image' - && message?.card?.file?.name.match(this.config.chatbotLogoRegex); + const chatbotLogoRegex = this.multiTenantService.getModuleConfiguration('chatbot.logoRegex'); + return message?.card?.file?.type === 'image' && message?.card?.file?.name.match(chatbotLogoRegex); } } diff --git a/dev/user-frontend-ionic/projects/chatbot/src/lib/chatbot.service.ts b/dev/user-frontend-ionic/projects/chatbot/src/lib/chatbot.service.ts index 8904cbe5..ad9b2026 100644 --- a/dev/user-frontend-ionic/projects/chatbot/src/lib/chatbot.service.ts +++ b/dev/user-frontend-ionic/projects/chatbot/src/lib/chatbot.service.ts @@ -38,7 +38,8 @@ */ import { HttpClient } from '@angular/common/http'; -import { Inject, Injectable } from '@angular/core'; +import { Injectable } from '@angular/core'; +import { MultiTenantService } from '@multi/shared'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; import { ChatbotButtonPayloadRequest, ChatbotMessage, ChatbotTextRequest, MessageType } from './chatbot.dto'; @@ -49,8 +50,7 @@ import { ChatbotButtonPayloadRequest, ChatbotMessage, ChatbotTextRequest, Messag export class ChatbotService { constructor( - @Inject('environment') - private environment: any, + private multiTenantService: MultiTenantService, private http: HttpClient) { } textRequest(text: string, userId: string): Observable { @@ -58,7 +58,7 @@ export class ChatbotService { return; } - const url = `${this.environment.apiEndpoint}/chatbot/text-request`; + const url = `${this.multiTenantService.getApiEndpoint()}/chatbot/text-request`; const request: ChatbotTextRequest = { query: text, userId @@ -71,7 +71,7 @@ export class ChatbotService { buttonPayloadRequest(buttonPayload: string, userId: string): Observable { const request: ChatbotButtonPayloadRequest = { payload: buttonPayload, userId }; - const url = `${this.environment.apiEndpoint}/chatbot/button-payload-request`; + const url = `${this.multiTenantService.getApiEndpoint()}/chatbot/button-payload-request`; return this.http.post(url , request).pipe( map(chatbotResponses => this.setMessageTypeToBot(chatbotResponses))); diff --git a/dev/user-frontend-ionic/projects/clocking/src/lib/clocking.service.ts b/dev/user-frontend-ionic/projects/clocking/src/lib/clocking.service.ts index 5ce74c55..b871a392 100644 --- a/dev/user-frontend-ionic/projects/clocking/src/lib/clocking.service.ts +++ b/dev/user-frontend-ionic/projects/clocking/src/lib/clocking.service.ts @@ -39,7 +39,7 @@ import { HttpClient } from '@angular/common/http'; import { Inject, Injectable } from '@angular/core'; -import { getAuthToken, NetworkService } from '@multi/shared'; +import { getAuthToken, NetworkService, MultiTenantService } from '@multi/shared'; import { from, iif, Observable, of } from 'rxjs'; import { map, switchMap, take, tap } from 'rxjs/operators'; import { Clocking, setClocking } from './clocking.repository'; @@ -52,6 +52,7 @@ export class ClockingService { constructor( @Inject('environment') private environment: any, + private multiTenantService: MultiTenantService, private http: HttpClient, private networkService: NetworkService, ) {} @@ -74,7 +75,7 @@ export class ClockingService { } private getClocking(): Observable { - const url = `${this.environment.apiEndpoint}/clocking`; + const url = `${this.multiTenantService.getApiEndpoint()}/clocking`; return getAuthToken().pipe( take(1), switchMap(authToken => this.http.post(url, { authToken })) @@ -89,7 +90,7 @@ export class ClockingService { } private addClocking(): Observable { - const url = `${this.environment.apiEndpoint}/clock-in`; + const url = `${this.multiTenantService.getApiEndpoint()}/clock-in`; return getAuthToken().pipe( take(1), switchMap(authToken => this.http.post(url, { authToken })) diff --git a/dev/user-frontend-ionic/projects/contact-us/src/lib/contact-us.service.ts b/dev/user-frontend-ionic/projects/contact-us/src/lib/contact-us.service.ts index 32560049..e6705967 100644 --- a/dev/user-frontend-ionic/projects/contact-us/src/lib/contact-us.service.ts +++ b/dev/user-frontend-ionic/projects/contact-us/src/lib/contact-us.service.ts @@ -38,11 +38,11 @@ */ import { HttpClient } from '@angular/common/http'; -import { Inject, Injectable } from '@angular/core'; +import { Injectable } from '@angular/core'; import { App } from '@capacitor/app'; import { Capacitor } from '@capacitor/core'; import { Platform } from '@ionic/angular'; -import { getAuthToken, NetworkService } from '@multi/shared'; +import { getAuthToken, NetworkService, MultiTenantService } from '@multi/shared'; import { combineLatest, from, Observable, of } from 'rxjs'; import { map, switchMap, take, tap } from 'rxjs/operators'; import { ContactUsPageContent, ContactUsRepository } from './contact-us.repository'; @@ -65,8 +65,7 @@ export interface ContactMessageQueryDto { export class ContactUsService { constructor( - @Inject('environment') - private environment: any, + private multiTenantService: MultiTenantService, private http: HttpClient, private contactUsRepository: ContactUsRepository, private networkService: NetworkService, @@ -74,7 +73,7 @@ export class ContactUsService { ) {} public loadAndStoreContactUsPageContent(): Observable { - const url = `${this.environment.apiEndpoint}/contact-us`; + const url = `${this.multiTenantService.getApiEndpoint()}/contact-us`; return this.http.get(url).pipe( tap((pageContent) => { @@ -83,7 +82,7 @@ export class ContactUsService { } public sendContactMessage(query: ContactMessageQueryDto): Observable { - const url = `${this.environment.apiEndpoint}/contact-us`; + const url = `${this.multiTenantService.getApiEndpoint()}/contact-us`; const appVersion = !Capacitor.isNativePlatform() ? of(null) : from(App.getInfo()).pipe(map(info => info.version)); return combineLatest([getAuthToken(), appVersion, from(this.networkService.getConnectionStatus())]).pipe( diff --git a/dev/user-frontend-ionic/projects/contacts/src/lib/contacts.service.ts b/dev/user-frontend-ionic/projects/contacts/src/lib/contacts.service.ts index 63d467e8..ed9b7515 100644 --- a/dev/user-frontend-ionic/projects/contacts/src/lib/contacts.service.ts +++ b/dev/user-frontend-ionic/projects/contacts/src/lib/contacts.service.ts @@ -38,9 +38,9 @@ */ import { HttpClient } from '@angular/common/http'; -import { Inject, Injectable } from '@angular/core'; +import { Injectable } from '@angular/core'; import { Contacts, EmailType, PhoneType } from '@capacitor-community/contacts'; -import { getAuthToken } from '@multi/shared'; +import { getAuthToken, MultiTenantService } from '@multi/shared'; import { Observable } from 'rxjs'; import { switchMap, take } from 'rxjs/operators'; @@ -65,8 +65,7 @@ export interface ContactsBody { export class ContactsService { constructor( - @Inject('environment') - private environment: any, + private multiTenantService: MultiTenantService, private http: HttpClient,) { } public getContacts(body: ContactsBody): Observable { @@ -108,7 +107,7 @@ export class ContactsService { private fetchContacts(body: ContactsBody, authToken: string): Observable { body.authToken = authToken || null; - return this.http.post(`${this.environment.apiEndpoint}/contacts`, body); + return this.http.post(`${this.multiTenantService.getApiEndpoint()}/contacts`, body); } } diff --git a/dev/user-frontend-ionic/projects/features/src/lib/pages/widgets/widgets.page.ts b/dev/user-frontend-ionic/projects/features/src/lib/pages/widgets/widgets.page.ts index 0e1f1fae..8588409e 100644 --- a/dev/user-frontend-ionic/projects/features/src/lib/pages/widgets/widgets.page.ts +++ b/dev/user-frontend-ionic/projects/features/src/lib/pages/widgets/widgets.page.ts @@ -38,7 +38,7 @@ */ import { Component } from '@angular/core'; -import { FeaturesService, GuidedTourService, TranslatedFeature, WidgetLifecycleService } from '@multi/shared'; +import { FeaturesService, GuidedTourService, TranslatedFeature, WidgetLifecycleService, MultiTenantService } from '@multi/shared'; import { Observable } from 'rxjs'; import { debounceTime, distinctUntilChanged, filter, map, take } from 'rxjs/operators'; @@ -54,7 +54,8 @@ export class WidgetsPage { constructor( private featuresService: FeaturesService, private widgetLifecycleService: WidgetLifecycleService, - private guidedTourService: GuidedTourService + private guidedTourService: GuidedTourService, + private multiTenantService: MultiTenantService ) { this.translatedFeatures$ = this.featuresService.translatedFeatures$.pipe( debounceTime(0), // Only get the last value of the replay subject @@ -80,7 +81,10 @@ export class WidgetsPage { this.widgetLifecycleService.sendWidgetViewDidEnter(features.map(feature => feature.widget)); }); - this.guidedTourService.startGlobalTour(); + if(this.multiTenantService.isCurrentTenantStateAllowed()) { + // If the current tenant state is not allowed, we will get redirected to the tenant selection page, so no need to show the guided tour yet + this.guidedTourService.startGlobalTour(); + } } ionViewWillLeave() { diff --git a/dev/user-frontend-ionic/projects/important-news/src/lib/important-news.service.ts b/dev/user-frontend-ionic/projects/important-news/src/lib/important-news.service.ts index ecab4d5d..bfb84201 100644 --- a/dev/user-frontend-ionic/projects/important-news/src/lib/important-news.service.ts +++ b/dev/user-frontend-ionic/projects/important-news/src/lib/important-news.service.ts @@ -39,7 +39,7 @@ import { HttpClient } from '@angular/common/http'; import { Inject, Injectable } from '@angular/core'; -import { getAuthToken, NetworkService } from '@multi/shared'; +import { getAuthToken, NetworkService, MultiTenantService } from '@multi/shared'; import { from, Observable } from 'rxjs'; import { filter, switchMap, take } from 'rxjs/operators'; import { ImportantNews, TranslatedImportantNews } from './important-news.repository'; @@ -52,6 +52,7 @@ export class ImportantNewsService { constructor( @Inject('environment') private environment: any, + private multiTenantService: MultiTenantService, private http: HttpClient, private networkService: NetworkService, ) { @@ -88,7 +89,7 @@ export class ImportantNewsService { } private getImportantNews(authToken: string): Observable { - const url = `${this.environment.apiEndpoint}/important-news`; + const url = `${this.multiTenantService.getApiEndpoint()}/important-news`; const data = { authToken }; diff --git a/dev/user-frontend-ionic/projects/important-news/src/lib/widgets/important-news/important-news.component.ts b/dev/user-frontend-ionic/projects/important-news/src/lib/widgets/important-news/important-news.component.ts index 86f39ba0..a028439b 100644 --- a/dev/user-frontend-ionic/projects/important-news/src/lib/widgets/important-news/important-news.component.ts +++ b/dev/user-frontend-ionic/projects/important-news/src/lib/widgets/important-news/important-news.component.ts @@ -40,7 +40,7 @@ import { Component, Inject } from '@angular/core'; import { Router } from '@angular/router'; import { Browser } from '@capacitor/browser'; -import { currentLanguage$, StatisticsService, ThemeService } from '@multi/shared'; +import { currentLanguage$, MultiTenantService, StatisticsService, ThemeService } from '@multi/shared'; import { combineLatest, Observable } from 'rxjs'; import { finalize, map, take } from 'rxjs/operators'; import { ImportantNews, importantNewsList$, setImportantNews as setImportantNewsList } from '../../important-news.repository'; @@ -61,13 +61,12 @@ export class ImportantNewsComponent { public importantNewsList$: Observable = importantNewsList$; public isEmpty$: Observable; public translatedImportantNewsList$: Observable; - public cmsPublicAssetsEndpoint = this.environment.cmsPublicAssetsEndpoint; + public cmsPublicAssetsEndpoint; public randomImportantNews$: Observable; constructor( - @Inject('environment') - private environment: any, + private multiTenantService: MultiTenantService, private importantNewsService: ImportantNewsService, private router: Router, private statisticsService: StatisticsService, @@ -95,6 +94,7 @@ export class ImportantNewsComponent { } widgetViewDidEnter(): void { + this.cmsPublicAssetsEndpoint = this.multiTenantService.getCmsPublicAssetsEndpoint(); this.isLoading = true; this.importantNewsService.loadImportantNewsList().pipe( take(1), diff --git a/dev/user-frontend-ionic/projects/map/src/lib/map.page.ts b/dev/user-frontend-ionic/projects/map/src/lib/map.page.ts index fcb555ac..f2f2b05b 100644 --- a/dev/user-frontend-ionic/projects/map/src/lib/map.page.ts +++ b/dev/user-frontend-ionic/projects/map/src/lib/map.page.ts @@ -37,14 +37,13 @@ * termes. */ -import { Component, DestroyRef, inject, Inject, ViewChild } from '@angular/core'; +import { Component, DestroyRef, inject, ViewChild } from '@angular/core'; import { FormArray, FormBuilder, FormControl, FormGroup } from '@angular/forms'; import { Geolocation } from '@capacitor/geolocation'; import { TranslateService } from '@ngx-translate/core'; -import { NetworkService } from '@multi/shared'; +import { NetworkService, MultiTenantService } from '@multi/shared'; import * as Leaflet from 'leaflet'; import { finalize, take } from 'rxjs/operators'; -import { MapModuleConfig, MAP_CONFIG } from './map.config'; import { Marker, markersList$, setMarkers } from './map.repository'; import { MapService } from './map.service'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @@ -83,7 +82,7 @@ export class MapPage { private mapService: MapService, private translateService: TranslateService, private formBuilder: FormBuilder, - @Inject(MAP_CONFIG) private config: MapModuleConfig, + private multiTenantService: MultiTenantService, private networkService: NetworkService, ) { this.initCategoriesForm(); @@ -194,7 +193,12 @@ export class MapPage { this.map.setView(latLng, zoomLevel); } catch (error) { console.error('Error getting current position:', error); - const latLngOfTheUniversity: Leaflet.LatLngTuple = [this.config.defaultMapLocation.latitude, this.config.defaultMapLocation.longitude]; + const {latitude, longitude} = this.multiTenantService.getModuleConfiguration('map.defaultLocation'); + const latLngOfTheUniversity: Leaflet.LatLngTuple = [ + latitude, + longitude + ]; + if (this.positionLayerGroup) { this.positionLayerGroup.remove(); } diff --git a/dev/user-frontend-ionic/projects/map/src/lib/map.service.ts b/dev/user-frontend-ionic/projects/map/src/lib/map.service.ts index 5f3ccb3d..eacba51f 100644 --- a/dev/user-frontend-ionic/projects/map/src/lib/map.service.ts +++ b/dev/user-frontend-ionic/projects/map/src/lib/map.service.ts @@ -37,10 +37,11 @@ * termes. */ -import { Inject, Injectable } from '@angular/core'; +import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { Marker } from './map.repository'; import { HttpClient } from '@angular/common/http'; +import { MultiTenantService } from '@multi/shared'; @Injectable({ providedIn: 'root' @@ -49,12 +50,11 @@ export class MapService { constructor( - @Inject('environment') - private environment: any, + private multiTenantService: MultiTenantService, private http: HttpClient, ) { } getMarkers(): Observable { - return this.http.get(`${this.environment.apiEndpoint}/map`); + return this.http.get(`${this.multiTenantService.getApiEndpoint()}/map`); } } diff --git a/dev/user-frontend-ionic/projects/menu/src/lib/burger-menu/burger-menu.page.html b/dev/user-frontend-ionic/projects/menu/src/lib/burger-menu/burger-menu.page.html index 3481c632..54fe84d9 100644 --- a/dev/user-frontend-ionic/projects/menu/src/lib/burger-menu/burger-menu.page.html +++ b/dev/user-frontend-ionic/projects/menu/src/lib/burger-menu/burger-menu.page.html @@ -42,6 +42,16 @@
+ + + {{"MULTI-TENANT.UNIVERSITY_CHANGE" | translate}} + ; public darkModeEnabled: boolean; isDarkTheme$: Observable; + tenantThemeApplied$: Observable; + isUniversitiesButtonVisible: boolean; + private defaultTenantThemeSubscription: Subscription; constructor( @Inject('environment') @@ -73,10 +78,14 @@ export class BurgerMenuPage { private widgetLifecycleService: WidgetLifecycleService, private guidedTourService: GuidedTourService, private versionService: VersionService, - public menuOpenerService: MenuOpenerService + private alertController: AlertController, + private translateService: TranslateService, + public menuOpenerService: MenuOpenerService, + public multiTenantService: MultiTenantService ) { this.languages = this.environment.languages; this.authenticatedUser$ = authenticatedUser$; + this.tenantThemeApplied$ = tenantThemeApplied$; this.staticMenuItems$ = this.sharedMenuService.burgerMenuItems$.pipe( map(menuItems => menuItems.filter(menuItem => menuItem.type === 'static')) ); @@ -86,6 +95,10 @@ export class BurgerMenuPage { this.appVersion$ = this.versionService.getCurrentAppVersion(); this.isDarkTheme$ = isDarkTheme$; + this.isUniversitiesButtonVisible = this.displayUniversitiesButton(); + this.defaultTenantThemeSubscription = this.tenantThemeApplied$.subscribe(() => { + this.isUniversitiesButtonVisible = this.displayUniversitiesButton(); + }); } useLanguage(language: string): void { @@ -118,4 +131,39 @@ export class BurgerMenuPage { getMenuId(menuItem: MenuItem){ return this.guidedTourService.generateMenuItemIdFromTitle(menuItem); } + + async clearSelectedTenant() { + const header = this.translateService.instant('MULTI-TENANT.CHANGE_UNIVERSITY_CONFIRMATION.HEADER'); + const message = this.translateService.instant('MULTI-TENANT.CHANGE_UNIVERSITY_CONFIRMATION.MESSAGE'); + const confirmation = await this.alertController.create({ + header, + message, + buttons: [ + { + text: 'Cancel', + role: 'cancel', + }, + { + text: 'OK', + role: 'confirm', + handler: () => { + this.multiTenantService.disconnectFromTenant(); + this.multiTenantService.redirectToTenantSelection(); + }, + }, + ], + }); + + await confirmation.present(); + } + + displayUniversitiesButton(): boolean { + const isSingleTenant: boolean = this.multiTenantService.getFlattenTenantObjects(undefined, 0).length === 1; + const currentTenant: Tenant = this.multiTenantService.getCurrentTenantOrThrowError(); + return !isSingleTenant || currentTenant.isGroup === true; + } + + ngOnDestroy() { + this.defaultTenantThemeSubscription.unsubscribe(); + } } diff --git a/dev/user-frontend-ionic/projects/notifications/src/lib/notifications.effects.ts b/dev/user-frontend-ionic/projects/notifications/src/lib/notifications.effects.ts index eb235f48..ba261c44 100644 --- a/dev/user-frontend-ionic/projects/notifications/src/lib/notifications.effects.ts +++ b/dev/user-frontend-ionic/projects/notifications/src/lib/notifications.effects.ts @@ -46,7 +46,7 @@ import { concatMap, filter, tap } from 'rxjs/operators'; export class NotificationsEffects { sendFCMToken$ = createEffect(actions => actions.pipe( ofType(authenticate), - tap(() => this.notificationsService.saveFCMToken()), + tap(() => this.notificationsService.registerNotificationsAndSaveFCMToken()), )); cleanupPrivateData$ = createEffect(actions => actions.pipe( ofType(cleanupPrivateData), diff --git a/dev/user-frontend-ionic/projects/reservation/src/lib/reservation.service.ts b/dev/user-frontend-ionic/projects/reservation/src/lib/reservation.service.ts index e631ab77..a83198ee 100644 --- a/dev/user-frontend-ionic/projects/reservation/src/lib/reservation.service.ts +++ b/dev/user-frontend-ionic/projects/reservation/src/lib/reservation.service.ts @@ -37,10 +37,9 @@ * termes. */ -import { Inject, Injectable } from '@angular/core'; +import { Injectable } from '@angular/core'; import { Browser } from '@capacitor/browser'; -import { SsoService } from '@multi/shared'; -import { ReservationModuleConfig, RESERVATION_CONFIG } from './reservation.config'; +import { MultiTenantService, SsoService } from '@multi/shared'; @Injectable({ providedIn: 'root' @@ -48,14 +47,15 @@ import { ReservationModuleConfig, RESERVATION_CONFIG } from './reservation.confi export class ReservationService { constructor( - @Inject(RESERVATION_CONFIG) private config: ReservationModuleConfig, private ssoService: SsoService, + private multiTenantService: MultiTenantService ) {} public openReservationService() { + const {ssoServiceName, ssoUrlTemplate} = this.multiTenantService.getModuleConfiguration('reservation'); this.ssoService.getSsoExternalLink({ - service: this.config.reservationSsoServiceName, - urlTemplate: this.config.reservationSsoUrlTemplate + service: ssoServiceName, + urlTemplate: ssoUrlTemplate }) .subscribe(url => Browser.open({ url })); } diff --git a/dev/user-frontend-ionic/projects/restaurants/src/lib/restaurant-menus-page/restaurant-menus.service.ts b/dev/user-frontend-ionic/projects/restaurants/src/lib/restaurant-menus-page/restaurant-menus.service.ts index 5b008a3a..ef12faaa 100644 --- a/dev/user-frontend-ionic/projects/restaurants/src/lib/restaurant-menus-page/restaurant-menus.service.ts +++ b/dev/user-frontend-ionic/projects/restaurants/src/lib/restaurant-menus-page/restaurant-menus.service.ts @@ -38,7 +38,8 @@ */ import { HttpClient, HttpParams } from '@angular/common/http'; -import { Inject, Injectable } from '@angular/core'; +import { Injectable } from '@angular/core'; +import { MultiTenantService } from '@multi/shared'; import { Observable } from 'rxjs'; import { tap } from 'rxjs/operators'; import { Menu, upsertMenus } from './menus.repository'; @@ -50,13 +51,12 @@ import { Menu, upsertMenus } from './menus.repository'; export class RestaurantMenusService { constructor( - @Inject('environment') - private environment: any, + private multiTenantService: MultiTenantService, private http: HttpClient, ) { } public loadAndStoreMenus(restaurantId: number, date?: string): Observable { - const url = `${this.environment.apiEndpoint}/restaurant/menus`; + const url = `${this.multiTenantService.getApiEndpoint()}/restaurant/menus`; const params = new HttpParams() .set('id', restaurantId) .set('date', date || ''); diff --git a/dev/user-frontend-ionic/projects/restaurants/src/lib/restaurants.service.ts b/dev/user-frontend-ionic/projects/restaurants/src/lib/restaurants.service.ts index 3f89b919..41e06f38 100644 --- a/dev/user-frontend-ionic/projects/restaurants/src/lib/restaurants.service.ts +++ b/dev/user-frontend-ionic/projects/restaurants/src/lib/restaurants.service.ts @@ -38,10 +38,11 @@ */ import { HttpClient } from '@angular/common/http'; -import { Inject, Injectable } from '@angular/core'; +import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { Restaurant, setRestaurants } from './restaurants.repository'; import { tap } from 'rxjs/operators'; +import { MultiTenantService } from '@multi/shared'; @Injectable({ @@ -50,13 +51,12 @@ import { tap } from 'rxjs/operators'; export class RestaurantsService { constructor( - @Inject('environment') - private environment: any, + private multiTenantService: MultiTenantService, private http: HttpClient, ) {} public loadAndStoreRestaurants(): Observable { - const url = `${this.environment.apiEndpoint}/restaurants`; + const url = `${this.multiTenantService.getApiEndpoint()}/restaurants`; return this.http.get(url).pipe( tap(restaurants => setRestaurants(restaurants))); diff --git a/dev/user-frontend-ionic/projects/rss/src/lib/rss.service.ts b/dev/user-frontend-ionic/projects/rss/src/lib/rss.service.ts index 71c50ad7..9f1c4ca2 100644 --- a/dev/user-frontend-ionic/projects/rss/src/lib/rss.service.ts +++ b/dev/user-frontend-ionic/projects/rss/src/lib/rss.service.ts @@ -38,7 +38,8 @@ */ import { HttpClient } from '@angular/common/http'; -import { Inject, Injectable } from '@angular/core'; +import { Injectable } from '@angular/core'; +import { MultiTenantService } from '@multi/shared'; import { Observable } from 'rxjs'; import { FeedItem } from './rss.repository'; @@ -48,12 +49,11 @@ import { FeedItem } from './rss.repository'; export class RssService { constructor( - @Inject('environment') - private environment: any, + private multiTenantService: MultiTenantService, private http: HttpClient ) {} public getRssFeed(): Observable { - return this.http.get(`${this.environment.apiEndpoint}/rss`); + return this.http.get(`${this.multiTenantService.getApiEndpoint()}/rss`); } } diff --git a/dev/user-frontend-ionic/projects/schedule/src/lib/schedule.service.ts b/dev/user-frontend-ionic/projects/schedule/src/lib/schedule.service.ts index 279c5ff8..e3671d3c 100644 --- a/dev/user-frontend-ionic/projects/schedule/src/lib/schedule.service.ts +++ b/dev/user-frontend-ionic/projects/schedule/src/lib/schedule.service.ts @@ -39,7 +39,7 @@ import { HttpClient } from '@angular/common/http'; import { Inject, Injectable } from '@angular/core'; -import { getAuthToken, NetworkService } from '@multi/shared'; +import { getAuthToken, MultiTenantService, NetworkService } from '@multi/shared'; import { add, format, startOfWeek, sub } from 'date-fns'; import { BehaviorSubject, combineLatest, from, Observable, of, Subject } from 'rxjs'; import { filter, finalize, map, mergeMap, switchMap, take, tap } from 'rxjs/operators'; @@ -68,8 +68,7 @@ export class ScheduleService { private isLoadingSubject = new Subject(); constructor( - @Inject('environment') - private environment: any, + private multiTenantService: MultiTenantService, private http: HttpClient, @Inject(SCHEDULE_CONFIG) private config: ScheduleModuleConfig, private networkService: NetworkService, @@ -86,7 +85,7 @@ export class ScheduleService { public getSchedule(authToken: string, startDate: string, endDate: string): Observable { - const url = `${this.environment.apiEndpoint}/schedule`; + const url = `${this.multiTenantService.getApiEndpoint()}/schedule`; const data = { authToken, startDate, diff --git a/dev/user-frontend-ionic/projects/shared/src/lib/auth/authenticated-user.repository.ts b/dev/user-frontend-ionic/projects/shared/src/lib/auth/authenticated-user.repository.ts index 0530dac1..5964fa82 100644 --- a/dev/user-frontend-ionic/projects/shared/src/lib/auth/authenticated-user.repository.ts +++ b/dev/user-frontend-ionic/projects/shared/src/lib/auth/authenticated-user.repository.ts @@ -66,6 +66,8 @@ export const persistAuthenticatedUser = persistState(authStore, { storage: localForageStore, }); +export const authenticatedUserRepoInitialized$ = persistAuthenticatedUser.initialized$; + export const authenticatedUser$ = authStore.pipe(select((state) => state.authenticatedUser)); export const updateUser = (authenticatedUser: AuthProps['authenticatedUser']) => { diff --git a/dev/user-frontend-ionic/projects/shared/src/lib/auth/keep-auth.service.ts b/dev/user-frontend-ionic/projects/shared/src/lib/auth/keep-auth.service.ts index 86ec39de..1effdb84 100644 --- a/dev/user-frontend-ionic/projects/shared/src/lib/auth/keep-auth.service.ts +++ b/dev/user-frontend-ionic/projects/shared/src/lib/auth/keep-auth.service.ts @@ -38,8 +38,9 @@ */ import { HttpClient } from '@angular/common/http'; -import { Inject, Injectable } from '@angular/core'; +import { Injectable } from '@angular/core'; import { Actions } from '@ngneat/effects-ng'; +import { MultiTenantService } from '../multi-tenant/multi-tenant.service'; import { Observable, of } from 'rxjs'; import { concatMap, delayWhen } from 'rxjs/operators'; import { authenticate, cleanupPrivateData } from '../shared.actions'; @@ -57,8 +58,7 @@ interface ReauthResult extends AuthenticatedUser { export class KeepAuthService { constructor( - @Inject('environment') - private environment: any, + private multiTenantService: MultiTenantService, private http: HttpClient, private actions: Actions, ) {} @@ -85,7 +85,7 @@ export class KeepAuthService { } private reauthenticate(refreshAuthToken: string) { - const url = `${this.environment.apiEndpoint}/keep-auth/reauth`; + const url = `${this.multiTenantService.getApiEndpoint()}/keep-auth/reauth`; const headers = { // eslint-disable-next-line @typescript-eslint/naming-convention Authorization: `Bearer ${refreshAuthToken}` diff --git a/dev/user-frontend-ionic/projects/shared/src/lib/fcm/fcm-global.service.ts b/dev/user-frontend-ionic/projects/shared/src/lib/fcm/fcm-global.service.ts new file mode 100644 index 00000000..e3580e0c --- /dev/null +++ b/dev/user-frontend-ionic/projects/shared/src/lib/fcm/fcm-global.service.ts @@ -0,0 +1,114 @@ +/* + * Copyright ou © ou Copr. Université de Lorraine, (2022) + * + * Direction du Numérique de l'Université de Lorraine - SIED + * (dn-mobile-dev@univ-lorraine.fr) + * JNESIS (contact@jnesis.com) + * + * Ce logiciel est un programme informatique servant à rendre accessible + * sur mobile divers services universitaires aux étudiants et aux personnels + * de l'université. + * + * Ce logiciel est régi par la licence CeCILL 2.1, soumise au droit français + * et respectant les principes de diffusion des logiciels libres. Vous pouvez + * utiliser, modifier et/ou redistribuer ce programme sous les conditions + * de la licence CeCILL telle que diffusée par le CEA, le CNRS et INRIA + * sur le site "http://cecill.info". + * + * En contrepartie de l'accessibilité au code source et des droits de copie, + * de modification et de redistribution accordés par cette licence, il n'est + * offert aux utilisateurs qu'une garantie limitée. Pour les mêmes raisons, + * seule une responsabilité restreinte pèse sur l'auteur du programme, le + * titulaire des droits patrimoniaux et les concédants successifs. + * + * À cet égard, l'attention de l'utilisateur est attirée sur les risques + * associés au chargement, à l'utilisation, à la modification et/ou au + * développement et à la reproduction du logiciel par l'utilisateur étant + * donné sa spécificité de logiciel libre, qui peut le rendre complexe à + * manipuler et qui le réserve donc à des développeurs et des professionnels + * avertis possédant des connaissances informatiques approfondies. Les + * utilisateurs sont donc invités à charger et à tester l'adéquation du + * logiciel à leurs besoins dans des conditions permettant d'assurer la + * sécurité de leurs systèmes et/ou de leurs données et, plus généralement, + * à l'utiliser et à l'exploiter dans les mêmes conditions de sécurité. + * + * Le fait que vous puissiez accéder à cet en-tête signifie que vous avez + * pris connaissance de la licence CeCILL 2.1, et que vous en avez accepté les + * termes. + */ + +import { Inject, Injectable } from '@angular/core'; +import { FirebaseMessaging, GetTokenOptions } from '@capacitor-firebase/messaging'; +import { Platform } from '@ionic/angular'; +import { FCMRepository } from './fcm.repository'; + +@Injectable({ + providedIn: 'root' +}) +export class FCMService { + + private currentTopic: string; + + constructor( + @Inject('environment') + private environment: any, + public fcmRepository: FCMRepository, + private platform: Platform + ) { + } + + public unsubscribeFromTopic() { + if(this.currentTopic) { + FirebaseMessaging.unsubscribeFromTopic({ topic: this.currentTopic }); + } + } + + public removeAllDeliveredNotifications() { + if (this.platform.is('capacitor')) { + FirebaseMessaging.removeAllDeliveredNotifications(); + } + } + + public async registerPushNotifications(topic?: string): Promise { + // Vérifie que l'utilisateur a bien autorisé les notifications + const notificationPermissions = await FirebaseMessaging.requestPermissions(); + + if (notificationPermissions.receive !== 'granted') { + return null; + } + + // Fonction qui enregistre le token FCM dans le state + const handleToken = (tokenResult: { token: string }) => { + // NOTE: on web browser when the user resets the notifications authorization and wants to allow it again, + // this will trigger a 404 error from firebase followed by this message in the console: + // "FirebaseError: Messaging: A problem occured while unsubscribing the user from FCM", + // it has been reported since 2019 in this thread but hasn't been solved since: + // https://github.com/firebase/firebase-js-sdk/issues/2364 + // It could be fixed by firebase in a future release + this.fcmRepository.setFcmToken(tokenResult.token); + return tokenResult.token || null; + } + + if (!this.platform.is('capacitor')) { // Web + const options: GetTokenOptions = { + vapidKey: this.environment.firebase.vapidKey, + serviceWorkerRegistration: await navigator.serviceWorker.register('firebase-messaging-sw.js'), + }; + + const tokenResult = await FirebaseMessaging.getToken(options); + return handleToken(tokenResult); + } else { // Mobile + const tokenResult = await FirebaseMessaging.getToken(); + if(topic) { + this.subscribeToTopic(topic) + } + return handleToken(tokenResult); + } + } + + private subscribeToTopic(topic: string) { + FirebaseMessaging.subscribeToTopic({ topic }).then(() => { + this.currentTopic = topic; + }); + } +} diff --git a/dev/user-frontend-ionic/projects/shared/src/lib/fcm/fcm.repository.ts b/dev/user-frontend-ionic/projects/shared/src/lib/fcm/fcm.repository.ts new file mode 100644 index 00000000..0c79043a --- /dev/null +++ b/dev/user-frontend-ionic/projects/shared/src/lib/fcm/fcm.repository.ts @@ -0,0 +1,83 @@ +/* + * Copyright ou © ou Copr. Université de Lorraine, (2022) + * + * Direction du Numérique de l'Université de Lorraine - SIED + * (dn-mobile-dev@univ-lorraine.fr) + * JNESIS (contact@jnesis.com) + * + * Ce logiciel est un programme informatique servant à rendre accessible + * sur mobile divers services universitaires aux étudiants et aux personnels + * de l'université. + * + * Ce logiciel est régi par la licence CeCILL 2.1, soumise au droit français + * et respectant les principes de diffusion des logiciels libres. Vous pouvez + * utiliser, modifier et/ou redistribuer ce programme sous les conditions + * de la licence CeCILL telle que diffusée par le CEA, le CNRS et INRIA + * sur le site "http://cecill.info". + * + * En contrepartie de l'accessibilité au code source et des droits de copie, + * de modification et de redistribution accordés par cette licence, il n'est + * offert aux utilisateurs qu'une garantie limitée. Pour les mêmes raisons, + * seule une responsabilité restreinte pèse sur l'auteur du programme, le + * titulaire des droits patrimoniaux et les concédants successifs. + * + * À cet égard, l'attention de l'utilisateur est attirée sur les risques + * associés au chargement, à l'utilisation, à la modification et/ou au + * développement et à la reproduction du logiciel par l'utilisateur étant + * donné sa spécificité de logiciel libre, qui peut le rendre complexe à + * manipuler et qui le réserve donc à des développeurs et des professionnels + * avertis possédant des connaissances informatiques approfondies. Les + * utilisateurs sont donc invités à charger et à tester l'adéquation du + * logiciel à leurs besoins dans des conditions permettant d'assurer la + * sécurité de leurs systèmes et/ou de leurs données et, plus généralement, + * à l'utiliser et à l'exploiter dans les mêmes conditions de sécurité. + * + * Le fait que vous puissiez accéder à cet en-tête signifie que vous avez + * pris connaissance de la licence CeCILL 2.1, et que vous en avez accepté les + * termes. + */ + +import { Inject, Injectable } from '@angular/core'; +import { createStore, select, withProps } from '@ngneat/elf'; +import { persistState } from '@ngneat/elf-persist-state'; +import { localForageStore } from '../store/local-forage'; + +const FCM_STORE = 'fcm'; + +export interface FCMProps { + fcmToken: string; +} + +const fcmStore = createStore( + { name: FCM_STORE }, + withProps({ + fcmToken: null, + }) +); + +@Injectable({ providedIn: 'root' }) +export class FCMRepository { + + public fcmToken$ = fcmStore.pipe(select((state) => state.fcmToken)); + + private persistFCMStore = persistState(fcmStore, { + key: FCM_STORE, + storage: localForageStore, + }); + + constructor( + @Inject('environment') + private environment: any, + ) {} + + public setFcmToken(fcmToken: string) { + fcmStore.update((state) => ({ + ...state, + fcmToken, + })); + } + + public deleteFcmToken() { + fcmStore.reset(); + } +} diff --git a/dev/user-frontend-ionic/projects/shared/src/lib/features/features.service.ts b/dev/user-frontend-ionic/projects/shared/src/lib/features/features.service.ts index 0fbe7229..ae2c12b8 100644 --- a/dev/user-frontend-ionic/projects/shared/src/lib/features/features.service.ts +++ b/dev/user-frontend-ionic/projects/shared/src/lib/features/features.service.ts @@ -39,6 +39,7 @@ import { HttpClient } from '@angular/common/http'; import { Inject, Injectable } from '@angular/core'; +import { MultiTenantService } from '../multi-tenant/multi-tenant.service'; import { combineLatest, Observable, ReplaySubject } from 'rxjs'; import { filter, map, share, switchMap, take, tap } from 'rxjs/operators'; import { getAuthToken } from '../auth/auth.repository'; @@ -87,6 +88,7 @@ export class FeaturesService { constructor( @Inject('environment') private environment: any, + private multiTenantService: MultiTenantService, private http: HttpClient, ) { this.translatedFeatures$ = this.translatedFeaturesSubject$; @@ -113,7 +115,7 @@ export class FeaturesService { } private getFeatures(authToken: string): Observable { - const url = `${this.environment.apiEndpoint}/features`; + const url = `${this.multiTenantService.getApiEndpoint()}/features`; const data = { authToken, diff --git a/dev/user-frontend-ionic/projects/shared/src/lib/guided-tour/guided-tour.service.ts b/dev/user-frontend-ionic/projects/shared/src/lib/guided-tour/guided-tour.service.ts index d77cc7a4..31735371 100644 --- a/dev/user-frontend-ionic/projects/shared/src/lib/guided-tour/guided-tour.service.ts +++ b/dev/user-frontend-ionic/projects/shared/src/lib/guided-tour/guided-tour.service.ts @@ -101,7 +101,7 @@ export class GuidedTourService { take(1), switchMap(() => userIsAuthenticated$.pipe(take(1))), ), - this.translateService.get('GUIDED_TOUR') // on ne le récupère pas, mais permet d'attendre que la traduction soit chargée, sinon .instant() ne fonctionne pas à chaque fois + this.translateService.get('GUIDED-TOUR') // on ne le récupère pas, mais permet d'attendre que la traduction soit chargée, sinon .instant() ne fonctionne pas à chaque fois ).subscribe(([userIsAuthenticated]) => { if ( !isLoggedTourViewed() && diff --git a/dev/user-frontend-ionic/projects/shared/src/lib/multi-tenant/is-tenant-selectable.guard.ts b/dev/user-frontend-ionic/projects/shared/src/lib/multi-tenant/is-tenant-selectable.guard.ts new file mode 100644 index 00000000..9a3b2529 --- /dev/null +++ b/dev/user-frontend-ionic/projects/shared/src/lib/multi-tenant/is-tenant-selectable.guard.ts @@ -0,0 +1,68 @@ +/* + * Copyright ou © ou Copr. Université de Lorraine, (2022) + * + * Direction du Numérique de l'Université de Lorraine - SIED + * (dn-mobile-dev@univ-lorraine.fr) + * JNESIS (contact@jnesis.com) + * + * Ce logiciel est un programme informatique servant à rendre accessible + * sur mobile divers services universitaires aux étudiants et aux personnels + * de l'université. + * + * Ce logiciel est régi par la licence CeCILL 2.1, soumise au droit français + * et respectant les principes de diffusion des logiciels libres. Vous pouvez + * utiliser, modifier et/ou redistribuer ce programme sous les conditions + * de la licence CeCILL telle que diffusée par le CEA, le CNRS et INRIA + * sur le site "http://cecill.info". + * + * En contrepartie de l'accessibilité au code source et des droits de copie, + * de modification et de redistribution accordés par cette licence, il n'est + * offert aux utilisateurs qu'une garantie limitée. Pour les mêmes raisons, + * seule une responsabilité restreinte pèse sur l'auteur du programme, le + * titulaire des droits patrimoniaux et les concédants successifs. + * + * À cet égard, l'attention de l'utilisateur est attirée sur les risques + * associés au chargement, à l'utilisation, à la modification et/ou au + * développement et à la reproduction du logiciel par l'utilisateur étant + * donné sa spécificité de logiciel libre, qui peut le rendre complexe à + * manipuler et qui le réserve donc à des développeurs et des professionnels + * avertis possédant des connaissances informatiques approfondies. Les + * utilisateurs sont donc invités à charger et à tester l'adéquation du + * logiciel à leurs besoins dans des conditions permettant d'assurer la + * sécurité de leurs systèmes et/ou de leurs données et, plus généralement, + * à l'utiliser et à l'exploiter dans les mêmes conditions de sécurité. + * + * Le fait que vous puissiez accéder à cet en-tête signifie que vous avez + * pris connaissance de la licence CeCILL 2.1, et que vous en avez accepté les + * termes. + */ + +import { CanActivate, Router } from '@angular/router'; +import { Injectable } from '@angular/core'; +import { MultiTenantService } from './multi-tenant.service'; + +@Injectable({ + providedIn: 'root' +}) +export class IsTenantSelectableGuard implements CanActivate { + + constructor( + private multiTenantService: MultiTenantService, + private router: Router + ) + {} + + canActivate() { + const isSingleTenant: boolean = this.multiTenantService.isSingleTenant(); + const hasCurrentTenant: boolean = this.multiTenantService.hasCurrentTenant(); + const availableTenants: any[] = this.multiTenantService.getAvailableTenants(); + + if (availableTenants && availableTenants.length === 1 && availableTenants[0].isGroup === true) { + return true; + } + if (isSingleTenant || hasCurrentTenant) { + this.router.navigate(['/']); + return false; + } + } +} diff --git a/dev/user-frontend-ionic/projects/shared/src/lib/multi-tenant/is-tenant-selected.guard.ts b/dev/user-frontend-ionic/projects/shared/src/lib/multi-tenant/is-tenant-selected.guard.ts new file mode 100644 index 00000000..224f2ddd --- /dev/null +++ b/dev/user-frontend-ionic/projects/shared/src/lib/multi-tenant/is-tenant-selected.guard.ts @@ -0,0 +1,68 @@ +/* + * Copyright ou © ou Copr. Université de Lorraine, (2022) + * + * Direction du Numérique de l'Université de Lorraine - SIED + * (dn-mobile-dev@univ-lorraine.fr) + * JNESIS (contact@jnesis.com) + * + * Ce logiciel est un programme informatique servant à rendre accessible + * sur mobile divers services universitaires aux étudiants et aux personnels + * de l'université. + * + * Ce logiciel est régi par la licence CeCILL 2.1, soumise au droit français + * et respectant les principes de diffusion des logiciels libres. Vous pouvez + * utiliser, modifier et/ou redistribuer ce programme sous les conditions + * de la licence CeCILL telle que diffusée par le CEA, le CNRS et INRIA + * sur le site "http://cecill.info". + * + * En contrepartie de l'accessibilité au code source et des droits de copie, + * de modification et de redistribution accordés par cette licence, il n'est + * offert aux utilisateurs qu'une garantie limitée. Pour les mêmes raisons, + * seule une responsabilité restreinte pèse sur l'auteur du programme, le + * titulaire des droits patrimoniaux et les concédants successifs. + * + * À cet égard, l'attention de l'utilisateur est attirée sur les risques + * associés au chargement, à l'utilisation, à la modification et/ou au + * développement et à la reproduction du logiciel par l'utilisateur étant + * donné sa spécificité de logiciel libre, qui peut le rendre complexe à + * manipuler et qui le réserve donc à des développeurs et des professionnels + * avertis possédant des connaissances informatiques approfondies. Les + * utilisateurs sont donc invités à charger et à tester l'adéquation du + * logiciel à leurs besoins dans des conditions permettant d'assurer la + * sécurité de leurs systèmes et/ou de leurs données et, plus généralement, + * à l'utiliser et à l'exploiter dans les mêmes conditions de sécurité. + * + * Le fait que vous puissiez accéder à cet en-tête signifie que vous avez + * pris connaissance de la licence CeCILL 2.1, et que vous en avez accepté les + * termes. + */ + +import { CanActivate, Router } from '@angular/router'; +import { Injectable } from '@angular/core'; +import { MultiTenantService } from './multi-tenant.service'; + +@Injectable({ + providedIn: 'root' +}) +export class IsTenantSelectedGuard implements CanActivate { + + constructor( + private multiTenantService: MultiTenantService, + private router: Router + ) {} + + canActivate() { + const isSingleTenant: boolean = this.multiTenantService.isSingleTenant(); + const hasCurrentTenant: boolean = this.multiTenantService.hasCurrentTenant(); + const availableTenants: any[] = this.multiTenantService.getAvailableTenants(); + + const groupExists = availableTenants && availableTenants.length === 1 && availableTenants[0].isGroup === true; + + if ((!hasCurrentTenant && (groupExists || !isSingleTenant))) { // The tenant should have been selected if group or if not single tenant + this.multiTenantService.redirectToTenantSelection(true).then(); + return false; + } + + return true; + } +} diff --git a/dev/user-frontend-ionic/projects/shared/src/lib/multi-tenant/multi-tenant-error-handler.ts b/dev/user-frontend-ionic/projects/shared/src/lib/multi-tenant/multi-tenant-error-handler.ts new file mode 100644 index 00000000..f35d0d90 --- /dev/null +++ b/dev/user-frontend-ionic/projects/shared/src/lib/multi-tenant/multi-tenant-error-handler.ts @@ -0,0 +1,61 @@ +/* + * Copyright ou © ou Copr. Université de Lorraine, (2022) + * + * Direction du Numérique de l'Université de Lorraine - SIED + * (dn-mobile-dev@univ-lorraine.fr) + * JNESIS (contact@jnesis.com) + * + * Ce logiciel est un programme informatique servant à rendre accessible + * sur mobile divers services universitaires aux étudiants et aux personnels + * de l'université. + * + * Ce logiciel est régi par la licence CeCILL 2.1, soumise au droit français + * et respectant les principes de diffusion des logiciels libres. Vous pouvez + * utiliser, modifier et/ou redistribuer ce programme sous les conditions + * de la licence CeCILL telle que diffusée par le CEA, le CNRS et INRIA + * sur le site "http://cecill.info". + * + * En contrepartie de l'accessibilité au code source et des droits de copie, + * de modification et de redistribution accordés par cette licence, il n'est + * offert aux utilisateurs qu'une garantie limitée. Pour les mêmes raisons, + * seule une responsabilité restreinte pèse sur l'auteur du programme, le + * titulaire des droits patrimoniaux et les concédants successifs. + * + * À cet égard, l'attention de l'utilisateur est attirée sur les risques + * associés au chargement, à l'utilisation, à la modification et/ou au + * développement et à la reproduction du logiciel par l'utilisateur étant + * donné sa spécificité de logiciel libre, qui peut le rendre complexe à + * manipuler et qui le réserve donc à des développeurs et des professionnels + * avertis possédant des connaissances informatiques approfondies. Les + * utilisateurs sont donc invités à charger et à tester l'adéquation du + * logiciel à leurs besoins dans des conditions permettant d'assurer la + * sécurité de leurs systèmes et/ou de leurs données et, plus généralement, + * à l'utiliser et à l'exploiter dans les mêmes conditions de sécurité. + * + * Le fait que vous puissiez accéder à cet en-tête signifie que vous avez + * pris connaissance de la licence CeCILL 2.1, et que vous en avez accepté les + * termes. + */ + +import {ErrorHandler, Injectable, NgZone} from '@angular/core'; +import { Router } from '@angular/router'; +import { NoTenantSelectedError } from './multi-tenant.error'; + +@Injectable({ + providedIn: 'root', +}) +export class MultiTenantErrorHandler implements ErrorHandler { + constructor( + private router: Router, + private zone: NgZone + ) { + } + + handleError(error: any): void { + if (error instanceof NoTenantSelectedError) { + this.zone.run(() => this.router.navigate(['/multi-tenant/select'])); + return; + } + throw error; + } +} diff --git a/dev/user-frontend-ionic/projects/shared/src/lib/multi-tenant/multi-tenant-routing.module.ts b/dev/user-frontend-ionic/projects/shared/src/lib/multi-tenant/multi-tenant-routing.module.ts new file mode 100644 index 00000000..5781ffe9 --- /dev/null +++ b/dev/user-frontend-ionic/projects/shared/src/lib/multi-tenant/multi-tenant-routing.module.ts @@ -0,0 +1,62 @@ +/* + * Copyright ou © ou Copr. Université de Lorraine, (2022) + * + * Direction du Numérique de l'Université de Lorraine - SIED + * (dn-mobile-dev@univ-lorraine.fr) + * JNESIS (contact@jnesis.com) + * + * Ce logiciel est un programme informatique servant à rendre accessible + * sur mobile divers services universitaires aux étudiants et aux personnels + * de l'université. + * + * Ce logiciel est régi par la licence CeCILL 2.1, soumise au droit français + * et respectant les principes de diffusion des logiciels libres. Vous pouvez + * utiliser, modifier et/ou redistribuer ce programme sous les conditions + * de la licence CeCILL telle que diffusée par le CEA, le CNRS et INRIA + * sur le site "http://cecill.info". + * + * En contrepartie de l'accessibilité au code source et des droits de copie, + * de modification et de redistribution accordés par cette licence, il n'est + * offert aux utilisateurs qu'une garantie limitée. Pour les mêmes raisons, + * seule une responsabilité restreinte pèse sur l'auteur du programme, le + * titulaire des droits patrimoniaux et les concédants successifs. + * + * À cet égard, l'attention de l'utilisateur est attirée sur les risques + * associés au chargement, à l'utilisation, à la modification et/ou au + * développement et à la reproduction du logiciel par l'utilisateur étant + * donné sa spécificité de logiciel libre, qui peut le rendre complexe à + * manipuler et qui le réserve donc à des développeurs et des professionnels + * avertis possédant des connaissances informatiques approfondies. Les + * utilisateurs sont donc invités à charger et à tester l'adéquation du + * logiciel à leurs besoins dans des conditions permettant d'assurer la + * sécurité de leurs systèmes et/ou de leurs données et, plus généralement, + * à l'utiliser et à l'exploiter dans les mêmes conditions de sécurité. + * + * Le fait que vous puissiez accéder à cet en-tête signifie que vous avez + * pris connaissance de la licence CeCILL 2.1, et que vous en avez accepté les + * termes. + */ + +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; +import { MultiTenantComponent } from './multi-tenant.component'; +import { IsTenantSelectableGuard } from './is-tenant-selectable.guard'; + +const routes: Routes = [ + { + path: 'multi-tenant', + children: [ + { + path: 'select', + component: MultiTenantComponent, + canActivate: [IsTenantSelectableGuard] + } + ] + }, +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], +}) +export class MultiTenantRoutingModule {} diff --git a/dev/user-frontend-ionic/projects/shared/src/lib/multi-tenant/multi-tenant-selected.repository.ts b/dev/user-frontend-ionic/projects/shared/src/lib/multi-tenant/multi-tenant-selected.repository.ts new file mode 100644 index 00000000..b92fff4a --- /dev/null +++ b/dev/user-frontend-ionic/projects/shared/src/lib/multi-tenant/multi-tenant-selected.repository.ts @@ -0,0 +1,68 @@ +/* + * Copyright ou © ou Copr. Université de Lorraine, (2022) + * + * Direction du Numérique de l'Université de Lorraine - SIED + * (dn-mobile-dev@univ-lorraine.fr) + * JNESIS (contact@jnesis.com) + * + * Ce logiciel est un programme informatique servant à rendre accessible + * sur mobile divers services universitaires aux étudiants et aux personnels + * de l'université. + * + * Ce logiciel est régi par la licence CeCILL 2.1, soumise au droit français + * et respectant les principes de diffusion des logiciels libres. Vous pouvez + * utiliser, modifier et/ou redistribuer ce programme sous les conditions + * de la licence CeCILL telle que diffusée par le CEA, le CNRS et INRIA + * sur le site "http://cecill.info". + * + * En contrepartie de l'accessibilité au code source et des droits de copie, + * de modification et de redistribution accordés par cette licence, il n'est + * offert aux utilisateurs qu'une garantie limitée. Pour les mêmes raisons, + * seule une responsabilité restreinte pèse sur l'auteur du programme, le + * titulaire des droits patrimoniaux et les concédants successifs. + * + * À cet égard, l'attention de l'utilisateur est attirée sur les risques + * associés au chargement, à l'utilisation, à la modification et/ou au + * développement et à la reproduction du logiciel par l'utilisateur étant + * donné sa spécificité de logiciel libre, qui peut le rendre complexe à + * manipuler et qui le réserve donc à des développeurs et des professionnels + * avertis possédant des connaissances informatiques approfondies. Les + * utilisateurs sont donc invités à charger et à tester l'adéquation du + * logiciel à leurs besoins dans des conditions permettant d'assurer la + * sécurité de leurs systèmes et/ou de leurs données et, plus généralement, + * à l'utiliser et à l'exploiter dans les mêmes conditions de sécurité. + * + * Le fait que vous puissiez accéder à cet en-tête signifie que vous avez + * pris connaissance de la licence CeCILL 2.1, et que vous en avez accepté les + * termes. + */ + +import {createStore, select, withProps} from '@ngneat/elf'; +import {localStorageStrategy, persistState} from '@ngneat/elf-persist-state'; + +const STORE_NAME = 'multi-tenant-selected'; + +interface MultiTenantProps { + selectedTenantId: string; +} + +const selectedTenantStore = createStore( + {name: STORE_NAME}, + withProps({selectedTenantId: null}) +); + +export const persistSelectedTenant = persistState(selectedTenantStore, { + key: STORE_NAME, + storage: localStorageStrategy, +}); + +export const selectedTenantId$ = selectedTenantStore.pipe(select((state: MultiTenantProps) => state.selectedTenantId)); + +export const getSelectedTenantId = () => selectedTenantStore.getValue()?.selectedTenantId; + +export const updateSelectedTenantId = (selectedTenantId: MultiTenantProps['selectedTenantId']) => { + selectedTenantStore.update((state: MultiTenantProps) => ({ + ...state, + selectedTenantId + })); +}; diff --git a/dev/user-frontend-ionic/projects/shared/src/lib/multi-tenant/multi-tenant.component.html b/dev/user-frontend-ionic/projects/shared/src/lib/multi-tenant/multi-tenant.component.html new file mode 100644 index 00000000..357663d5 --- /dev/null +++ b/dev/user-frontend-ionic/projects/shared/src/lib/multi-tenant/multi-tenant.component.html @@ -0,0 +1,75 @@ + + + + + + + + + + + +

{{ "MULTI-TENANT.CHOICE_UNIVERSITY" | translate }}

+
+ +

{{tenant.name}}

+
+
+ + + + + + + + + + + + + + + {{ 'MENU.VERSION' | translate }} {{ version }} + + + diff --git a/dev/user-frontend-ionic/projects/shared/src/lib/multi-tenant/multi-tenant.component.ts b/dev/user-frontend-ionic/projects/shared/src/lib/multi-tenant/multi-tenant.component.ts new file mode 100644 index 00000000..d94e63b2 --- /dev/null +++ b/dev/user-frontend-ionic/projects/shared/src/lib/multi-tenant/multi-tenant.component.ts @@ -0,0 +1,112 @@ +/* + * Copyright ou © ou Copr. Université de Lorraine, (2022) + * + * Direction du Numérique de l'Université de Lorraine - SIED + * (dn-mobile-dev@univ-lorraine.fr) + * JNESIS (contact@jnesis.com) + * + * Ce logiciel est un programme informatique servant à rendre accessible + * sur mobile divers services universitaires aux étudiants et aux personnels + * de l'université. + * + * Ce logiciel est régi par la licence CeCILL 2.1, soumise au droit français + * et respectant les principes de diffusion des logiciels libres. Vous pouvez + * utiliser, modifier et/ou redistribuer ce programme sous les conditions + * de la licence CeCILL telle que diffusée par le CEA, le CNRS et INRIA + * sur le site "http://cecill.info". + * + * En contrepartie de l'accessibilité au code source et des droits de copie, + * de modification et de redistribution accordés par cette licence, il n'est + * offert aux utilisateurs qu'une garantie limitée. Pour les mêmes raisons, + * seule une responsabilité restreinte pèse sur l'auteur du programme, le + * titulaire des droits patrimoniaux et les concédants successifs. + * + * À cet égard, l'attention de l'utilisateur est attirée sur les risques + * associés au chargement, à l'utilisation, à la modification et/ou au + * développement et à la reproduction du logiciel par l'utilisateur étant + * donné sa spécificité de logiciel libre, qui peut le rendre complexe à + * manipuler et qui le réserve donc à des développeurs et des professionnels + * avertis possédant des connaissances informatiques approfondies. Les + * utilisateurs sont donc invités à charger et à tester l'adéquation du + * logiciel à leurs besoins dans des conditions permettant d'assurer la + * sécurité de leurs systèmes et/ou de leurs données et, plus généralement, + * à l'utiliser et à l'exploiter dans les mêmes conditions de sécurité. + * + * Le fait que vous puissiez accéder à cet en-tête signifie que vous avez + * pris connaissance de la licence CeCILL 2.1, et que vous en avez accepté les + * termes. + */ + +import { Component, Inject } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; +import { Tenant } from './multi-tenant.model'; +import { MultiTenantService } from './multi-tenant.service'; +import { from, Observable, of } from 'rxjs'; +import { Capacitor } from '@capacitor/core'; +import { App } from '@capacitor/app'; +import { map } from 'rxjs/operators'; +import { setLanguage } from '../i18n/i18n.repository'; +import { isDarkTheme, isDarkTheme$, setIsDarkTheme, setUserHaveSetThemeInApp } from '../theme/theme.repository'; + +@Component({ + selector: 'app-multi-tenant', + templateUrl: './multi-tenant.component.html', + styleUrls: ['../../../../../src/theme/app-theme/styles/multi-tenant/multi-tenant.component.scss'], +}) +export class MultiTenantComponent { + + public availableTenants: Tenant[]; + public selectedTenantId: string; + public languages: Array = []; + appVersion$: Observable; + public darkModeEnabled: boolean; + isDarkTheme$: Observable; + + constructor( + @Inject('environment') + private environment: any, + private router: Router, + private multiTenantService: MultiTenantService, + private activatedRoute: ActivatedRoute + ) { + this.availableTenants = this.getAvailableTenants(); + this.selectedTenantId = this.multiTenantService.getSelectedTenantId(); + + this.languages = this.environment.languages; + this.appVersion$ = !Capacitor.isNativePlatform() + ? of(this.environment.appVersion || '0.0.0') + : from(App.getInfo()).pipe(map(info => info.version)); + this.isDarkTheme$ = isDarkTheme$; + } + + getAvailableTenants(): Tenant[] { + const theTenants: Tenant[] = this.multiTenantService.getAvailableTenants(); + if (theTenants.length === 1 && theTenants[0].isGroup === true) { + return theTenants[0].tenants; + } + + return theTenants; + } + + onValidate(tenantId: string) { + this.multiTenantService.setCurrentTenantById(tenantId); + const redirectToAuth = this.activatedRoute.snapshot.queryParams['redirectToAuth']; + + if (redirectToAuth === 'true') { + this.router.navigate(['/auth']); + } else { + this.router.navigate(['/']); + } + } + + useLanguage(language: string): void { + setLanguage(language); + } + + toggleDarkMode() { + this.darkModeEnabled = isDarkTheme(); + this.darkModeEnabled = !this.darkModeEnabled; + setUserHaveSetThemeInApp(true); + setIsDarkTheme(this.darkModeEnabled); + } +} diff --git a/dev/user-frontend-ionic/projects/shared/src/lib/multi-tenant/multi-tenant.error.ts b/dev/user-frontend-ionic/projects/shared/src/lib/multi-tenant/multi-tenant.error.ts new file mode 100644 index 00000000..6e360f2e --- /dev/null +++ b/dev/user-frontend-ionic/projects/shared/src/lib/multi-tenant/multi-tenant.error.ts @@ -0,0 +1,62 @@ +/* + * Copyright ou © ou Copr. Université de Lorraine, (2022) + * + * Direction du Numérique de l'Université de Lorraine - SIED + * (dn-mobile-dev@univ-lorraine.fr) + * JNESIS (contact@jnesis.com) + * + * Ce logiciel est un programme informatique servant à rendre accessible + * sur mobile divers services universitaires aux étudiants et aux personnels + * de l'université. + * + * Ce logiciel est régi par la licence CeCILL 2.1, soumise au droit français + * et respectant les principes de diffusion des logiciels libres. Vous pouvez + * utiliser, modifier et/ou redistribuer ce programme sous les conditions + * de la licence CeCILL telle que diffusée par le CEA, le CNRS et INRIA + * sur le site "http://cecill.info". + * + * En contrepartie de l'accessibilité au code source et des droits de copie, + * de modification et de redistribution accordés par cette licence, il n'est + * offert aux utilisateurs qu'une garantie limitée. Pour les mêmes raisons, + * seule une responsabilité restreinte pèse sur l'auteur du programme, le + * titulaire des droits patrimoniaux et les concédants successifs. + * + * À cet égard, l'attention de l'utilisateur est attirée sur les risques + * associés au chargement, à l'utilisation, à la modification et/ou au + * développement et à la reproduction du logiciel par l'utilisateur étant + * donné sa spécificité de logiciel libre, qui peut le rendre complexe à + * manipuler et qui le réserve donc à des développeurs et des professionnels + * avertis possédant des connaissances informatiques approfondies. Les + * utilisateurs sont donc invités à charger et à tester l'adéquation du + * logiciel à leurs besoins dans des conditions permettant d'assurer la + * sécurité de leurs systèmes et/ou de leurs données et, plus généralement, + * à l'utiliser et à l'exploiter dans les mêmes conditions de sécurité. + * + * Le fait que vous puissiez accéder à cet en-tête signifie que vous avez + * pris connaissance de la licence CeCILL 2.1, et que vous en avez accepté les + * termes. + */ + +export class NoTenantsError extends Error { + constructor() { + super('No tenants in environment configuration'); + } +} + +export class NoTenantSelectedError extends Error { + constructor() { + super('No tenant selected'); + } +} + +export class NoTenantWithIdError extends Error { + constructor(tenantId: string) { + super(`No tenant with id '${tenantId}'`); + } +} + +export class NoModulesConfigurationError extends Error { + constructor(configurationName: string, tenantId: string) { + super(`No modules configuration '${configurationName}' in tenant with id '${tenantId}'`); + } +} diff --git a/dev/user-frontend-ionic/projects/shared/src/lib/multi-tenant/multi-tenant.model.ts b/dev/user-frontend-ionic/projects/shared/src/lib/multi-tenant/multi-tenant.model.ts new file mode 100644 index 00000000..51ba6fcd --- /dev/null +++ b/dev/user-frontend-ionic/projects/shared/src/lib/multi-tenant/multi-tenant.model.ts @@ -0,0 +1,65 @@ +/* + * Copyright ou © ou Copr. Université de Lorraine, (2022) + * + * Direction du Numérique de l'Université de Lorraine - SIED + * (dn-mobile-dev@univ-lorraine.fr) + * JNESIS (contact@jnesis.com) + * + * Ce logiciel est un programme informatique servant à rendre accessible + * sur mobile divers services universitaires aux étudiants et aux personnels + * de l'université. + * + * Ce logiciel est régi par la licence CeCILL 2.1, soumise au droit français + * et respectant les principes de diffusion des logiciels libres. Vous pouvez + * utiliser, modifier et/ou redistribuer ce programme sous les conditions + * de la licence CeCILL telle que diffusée par le CEA, le CNRS et INRIA + * sur le site "http://cecill.info". + * + * En contrepartie de l'accessibilité au code source et des droits de copie, + * de modification et de redistribution accordés par cette licence, il n'est + * offert aux utilisateurs qu'une garantie limitée. Pour les mêmes raisons, + * seule une responsabilité restreinte pèse sur l'auteur du programme, le + * titulaire des droits patrimoniaux et les concédants successifs. + * + * À cet égard, l'attention de l'utilisateur est attirée sur les risques + * associés au chargement, à l'utilisation, à la modification et/ou au + * développement et à la reproduction du logiciel par l'utilisateur étant + * donné sa spécificité de logiciel libre, qui peut le rendre complexe à + * manipuler et qui le réserve donc à des développeurs et des professionnels + * avertis possédant des connaissances informatiques approfondies. Les + * utilisateurs sont donc invités à charger et à tester l'adéquation du + * logiciel à leurs besoins dans des conditions permettant d'assurer la + * sécurité de leurs systèmes et/ou de leurs données et, plus généralement, + * à l'utiliser et à l'exploiter dans les mêmes conditions de sécurité. + * + * Le fait que vous puissiez accéder à cet en-tête signifie que vous avez + * pris connaissance de la licence CeCILL 2.1, et que vous en avez accepté les + * termes. + */ + +export interface Tenant { + id: string; + name: string; + isGroup?: boolean; + forceSelect?: boolean; + apiEndpoint: string; + cmsPublicAssetsEndpoint: string; + logo: string; + topic?: string; + tenants?: Tenant[]; + modulesConfigurations: { + chatbot?: { + logoRegex?: string; + }; + map?: { + defaultLocation?: { + longitude: number; + latitude: number; + }; + }; + reservation?: { + ssoServiceName?: string; + ssoUrlTemplate?: string; + }; + }; +} diff --git a/dev/user-frontend-ionic/projects/shared/src/lib/multi-tenant/multi-tenant.module.ts b/dev/user-frontend-ionic/projects/shared/src/lib/multi-tenant/multi-tenant.module.ts new file mode 100644 index 00000000..aef19ed8 --- /dev/null +++ b/dev/user-frontend-ionic/projects/shared/src/lib/multi-tenant/multi-tenant.module.ts @@ -0,0 +1,78 @@ +/* + * Copyright ou © ou Copr. Université de Lorraine, (2022) + * + * Direction du Numérique de l'Université de Lorraine - SIED + * (dn-mobile-dev@univ-lorraine.fr) + * JNESIS (contact@jnesis.com) + * + * Ce logiciel est un programme informatique servant à rendre accessible + * sur mobile divers services universitaires aux étudiants et aux personnels + * de l'université. + * + * Ce logiciel est régi par la licence CeCILL 2.1, soumise au droit français + * et respectant les principes de diffusion des logiciels libres. Vous pouvez + * utiliser, modifier et/ou redistribuer ce programme sous les conditions + * de la licence CeCILL telle que diffusée par le CEA, le CNRS et INRIA + * sur le site "http://cecill.info". + * + * En contrepartie de l'accessibilité au code source et des droits de copie, + * de modification et de redistribution accordés par cette licence, il n'est + * offert aux utilisateurs qu'une garantie limitée. Pour les mêmes raisons, + * seule une responsabilité restreinte pèse sur l'auteur du programme, le + * titulaire des droits patrimoniaux et les concédants successifs. + * + * À cet égard, l'attention de l'utilisateur est attirée sur les risques + * associés au chargement, à l'utilisation, à la modification et/ou au + * développement et à la reproduction du logiciel par l'utilisateur étant + * donné sa spécificité de logiciel libre, qui peut le rendre complexe à + * manipuler et qui le réserve donc à des développeurs et des professionnels + * avertis possédant des connaissances informatiques approfondies. Les + * utilisateurs sont donc invités à charger et à tester l'adéquation du + * logiciel à leurs besoins dans des conditions permettant d'assurer la + * sécurité de leurs systèmes et/ou de leurs données et, plus généralement, + * à l'utiliser et à l'exploiter dans les mêmes conditions de sécurité. + * + * Le fait que vous puissiez accéder à cet en-tête signifie que vous avez + * pris connaissance de la licence CeCILL 2.1, et que vous en avez accepté les + * termes. + */ + +import { CommonModule } from '@angular/common'; +import {APP_INITIALIZER, ErrorHandler, NgModule} from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { IonicModule } from '@ionic/angular'; +import { MultiTenantErrorHandler } from './multi-tenant-error-handler'; +import { MultiTenantRoutingModule } from './multi-tenant-routing.module'; +import { MultiTenantComponent } from './multi-tenant.component'; +import {TranslateModule} from '@ngx-translate/core'; +import {ProjectModuleService} from '../project-module/project-module.service'; + +const initModule = (projectModuleService: ProjectModuleService) => + () => projectModuleService.initProjectModule({ + name: 'multi-tenant', + translation: true + }); +@NgModule({ + declarations: [ + MultiTenantComponent + ], + imports: [ + CommonModule, + FormsModule, + IonicModule, + MultiTenantRoutingModule, + TranslateModule + ], + providers: [{ + provide: APP_INITIALIZER, + useFactory: initModule, + deps:[ProjectModuleService], + multi: true + }, + { + provide: ErrorHandler, + useClass: MultiTenantErrorHandler + } + ] +}) +export class MultiTenantModule { } diff --git a/dev/user-frontend-ionic/projects/shared/src/lib/multi-tenant/multi-tenant.service.ts b/dev/user-frontend-ionic/projects/shared/src/lib/multi-tenant/multi-tenant.service.ts new file mode 100644 index 00000000..a481417e --- /dev/null +++ b/dev/user-frontend-ionic/projects/shared/src/lib/multi-tenant/multi-tenant.service.ts @@ -0,0 +1,250 @@ +/* + * Copyright ou © ou Copr. Université de Lorraine, (2022) + * + * Direction du Numérique de l'Université de Lorraine - SIED + * (dn-mobile-dev@univ-lorraine.fr) + * JNESIS (contact@jnesis.com) + * + * Ce logiciel est un programme informatique servant à rendre accessible + * sur mobile divers services universitaires aux étudiants et aux personnels + * de l'université. + * + * Ce logiciel est régi par la licence CeCILL 2.1, soumise au droit français + * et respectant les principes de diffusion des logiciels libres. Vous pouvez + * utiliser, modifier et/ou redistribuer ce programme sous les conditions + * de la licence CeCILL telle que diffusée par le CEA, le CNRS et INRIA + * sur le site "http://cecill.info". + * + * En contrepartie de l'accessibilité au code source et des droits de copie, + * de modification et de redistribution accordés par cette licence, il n'est + * offert aux utilisateurs qu'une garantie limitée. Pour les mêmes raisons, + * seule une responsabilité restreinte pèse sur l'auteur du programme, le + * titulaire des droits patrimoniaux et les concédants successifs. + * + * À cet égard, l'attention de l'utilisateur est attirée sur les risques + * associés au chargement, à l'utilisation, à la modification et/ou au + * développement et à la reproduction du logiciel par l'utilisateur étant + * donné sa spécificité de logiciel libre, qui peut le rendre complexe à + * manipuler et qui le réserve donc à des développeurs et des professionnels + * avertis possédant des connaissances informatiques approfondies. Les + * utilisateurs sont donc invités à charger et à tester l'adéquation du + * logiciel à leurs besoins dans des conditions permettant d'assurer la + * sécurité de leurs systèmes et/ou de leurs données et, plus généralement, + * à l'utiliser et à l'exploiter dans les mêmes conditions de sécurité. + * + * Le fait que vous puissiez accéder à cet en-tête signifie que vous avez + * pris connaissance de la licence CeCILL 2.1, et que vous en avez accepté les + * termes. + */ + +import {Inject, Injectable} from '@angular/core'; +import { Router } from '@angular/router'; +import { + NoModulesConfigurationError, + NoTenantSelectedError, + NoTenantsError, + NoTenantWithIdError +} from './multi-tenant.error'; +import { Tenant } from './multi-tenant.model'; +import { updateSelectedTenantId, getSelectedTenantId } from './multi-tenant-selected.repository'; +import { getRegistry } from '@ngneat/elf'; +import { setTenantThemeApplied } from '../theme/theme.repository'; +import { FCMService } from '../fcm/fcm-global.service'; +import { BehaviorSubject, Observable, Subject, withLatestFrom } from 'rxjs'; +import { authenticatedUserRepoInitialized$, userIsAuthenticated$ } from '../auth/authenticated-user.repository'; +import { filter } from 'rxjs/operators'; + +@Injectable({ + providedIn: 'root' +}) +export class MultiTenantService { + public currentTenantLogo$: Observable; + public tenantChange$: Observable; + + private currentTenant: Tenant; + private currentTenantLogoSubject = new BehaviorSubject(this.environment.defaultLogo); + private tenantChangeSubject = new Subject(); + + constructor( + @Inject('environment') + private environment: any, + private router: Router, + private fcmService: FCMService, + ) { + this.currentTenantLogo$ = this.currentTenantLogoSubject.asObservable(); + this.tenantChange$ = this.tenantChangeSubject.asObservable(); + + const tenantId = getSelectedTenantId(); + if(tenantId) { + this.setCurrentTenantById(tenantId); + } + + userIsAuthenticated$.pipe( + withLatestFrom(authenticatedUserRepoInitialized$), + filter(([isAuth, isAuthRepoInit]) => !isAuth && isAuthRepoInit) + ).subscribe(() => { + if(this.currentTenant && this.canUseGroupMode()) { // Only disconnect from tenant when we can use the group mode + this.disconnectFromTenant(); + } + }); + } + + public getApiEndpoint(): string { + return this.getCurrentTenantOrThrowError().apiEndpoint; + } + + public getCmsPublicAssetsEndpoint(): string { + return this.getCurrentTenantOrThrowError().cmsPublicAssetsEndpoint; + } + + public getCurrentTenantOrThrowError(): Tenant { + if(!this.isCurrentTenantStateAllowed()) { + throw new NoTenantSelectedError(); + } + + return this.currentTenant ?? this.getAvailableTenants()[0]; + } + + public isCurrentTenantStateAllowed(): boolean { + if(this.currentTenant) { + if(this.currentTenant.isGroup === false) { + // There's a tenant and it's not a group, all good + return true; + } + + // Group: tenant must be selected if forceSelect is true + return !(this.currentTenant.forceSelect === true); + } + + // No tenant is selected + const availableTenants = this.getAvailableTenants(); + + if(!this.isSingleTenant()) { + // There are multiple tenants, one must be selected + return false; + } + + // There is only one tenant at root level, so unless it is a group and forceSelect is true, the state is allowed + return availableTenants[0].isGroup === false || availableTenants[0].forceSelect === false; + } + + public getAvailableTenants(): Tenant[] { + return this.getTenants(); + } + + public setCurrentTenantById(tenantId: string): void { + const foundTenant = this.findTenant(tenantId); + const previousTenantId = this.getSelectedTenantId(); + if (!foundTenant) { + this.currentTenantLogoSubject.next(this.environment.defaultLogo); + throw new NoTenantWithIdError(tenantId); + } + updateSelectedTenantId(tenantId); + this.currentTenant = foundTenant; + this.applyTenantTheme(this.currentTenant.id); + if(previousTenantId !== tenantId) { // If the tenant changes from previous value + this.tenantChangeSubject.next(foundTenant); + } + if(this.currentTenant?.logo) { + this.currentTenantLogoSubject.next(this.currentTenant.logo); + } + } + + public getSelectedTenantId(): string { + const foundTenant = this.findTenant(getSelectedTenantId()); + return foundTenant?.id || this.getAvailableTenants()[0]?.id; + } + + public isSingleTenant(): boolean { + const tenants = this.getTenants(); + return tenants?.length === 1; + } + + public hasCurrentTenant(): boolean { + return this.currentTenant !== undefined; + } + + public getModuleConfiguration(configName: string): any { + const modulesConfiguration = this.getCurrentTenantOrThrowError().modulesConfigurations; + if (!modulesConfiguration) { + throw new NoModulesConfigurationError(configName, this.currentTenant.id); + } + // Get module configuration with configName and dot notation + return configName.split('.').reduce((modulesConfig: any, config: string) => { + const currentModuleConfig = modulesConfig[config]; + if (!currentModuleConfig) { + throw new NoModulesConfigurationError(configName, this.currentTenant.id); + } + return currentModuleConfig; + }, modulesConfiguration); + } + + public async redirectToTenantSelection(backToAuth: boolean = false) { + if(backToAuth) { + await this.router.navigate(['/multi-tenant/select'], {queryParams: {redirectToAuth: true}}); + return; + } + await this.router.navigate(['/multi-tenant/select']); + } + + public disconnectFromTenant() { + getRegistry().forEach(store => store.reset()); + updateSelectedTenantId(undefined); + const defaultTheme = this.environment.defaultTheme || ''; + this.currentTenant = undefined; + this.applyTenantTheme(defaultTheme); + this.tenantChangeSubject.next(undefined); + this.currentTenantLogoSubject.next(this.environment.defaultLogo); + this.fcmService.unsubscribeFromTopic(); + } + + public getFlattenTenantObjects(t: Tenant[], level: number): Tenant[] { + let tenants: Tenant[] = t; + if (t === undefined && level === 0) { + tenants = this.environment.tenants; + } + + return tenants.reduce((flattened, tenant) => { + flattened.push(tenant); + if (tenant.tenants) { + flattened.push(...this.getFlattenTenantObjects(tenant.tenants, 1)); + } + return flattened; + }, []); + } + + public flattenTenantObjects(tenants: Tenant[]): Tenant[] { + return tenants.reduce((flattened, tenant) => { + flattened.push(tenant); + if (tenant.tenants) { + flattened.push(...this.flattenTenantObjects(tenant.tenants)); + } + return flattened; + }, []); + } + + public applyTenantTheme(theme: string): void { + setTenantThemeApplied(theme); + } + + private getTenants(): Tenant[] { + const tenants = this.environment.tenants; + if (!tenants) { + throw new NoTenantsError(); + } + return tenants; + } + + private findTenant(tenantId: string): Tenant { + + const flattenedTenants = this.getFlattenTenantObjects(undefined, 0); + + return flattenedTenants.find((tenant: Tenant) => tenant.id === tenantId); + } + + // Tells if we can use the tenant group, i.e. not having a specific tenant selected + private canUseGroupMode(): boolean { + // There is a group defined and the group does not force tenant selection + return this.getAvailableTenants()[0].isGroup === true && this.getAvailableTenants()[0].forceSelect === false + } +} diff --git a/dev/user-frontend-ionic/projects/shared/src/lib/notifications/notifications.repository.ts b/dev/user-frontend-ionic/projects/shared/src/lib/notifications/notifications.repository.ts index d8077050..c21798de 100644 --- a/dev/user-frontend-ionic/projects/shared/src/lib/notifications/notifications.repository.ts +++ b/dev/user-frontend-ionic/projects/shared/src/lib/notifications/notifications.repository.ts @@ -51,11 +51,6 @@ const CHANNELS_STORE= 'channels'; const defaultNotificationColor = 'black'; const defaultNotificationIcon = 'information-circle'; - -export interface NotificationsProps { - fcmToken: string; -} - export interface ChannelsProps { unsubscribedChannels: string[]; } @@ -63,6 +58,7 @@ export interface ChannelsProps { export interface Notification { id: string; author: string; + topic: string; channel: string; channelLabel: string; icon: string; @@ -98,9 +94,6 @@ export interface TranslatedChannel { const notificationsStore = createStore( { name: NOTIFICATIONS_STORE }, - withProps({ - fcmToken: null, - }), withEntities(), ); @@ -115,7 +108,6 @@ const channelsStore = createStore( @Injectable({ providedIn: 'root' }) export class NotificationsRepository { - public fcmToken$ = notificationsStore.pipe(select((state) => state.fcmToken)); public channels$ = channelsStore.pipe(selectAllEntities()); public unsubscribedChannels$ = channelsStore.pipe(select((state) => state.unsubscribedChannels)); @@ -159,15 +151,8 @@ export class NotificationsRepository { constructor( @Inject('environment') - private environment: any,) { - } - - public setFcmToken(fcmToken: string) { - notificationsStore.update((state) => ({ - ...state, - fcmToken, - })); - } + private environment: any, + ) {} public setNotifications(notifications: Notification[]) { notificationsStore.update(setEntities(notifications)); diff --git a/dev/user-frontend-ionic/projects/shared/src/lib/notifications/notifications.service.ts b/dev/user-frontend-ionic/projects/shared/src/lib/notifications/notifications.service.ts index e620c0d2..2be7a6b1 100644 --- a/dev/user-frontend-ionic/projects/shared/src/lib/notifications/notifications.service.ts +++ b/dev/user-frontend-ionic/projects/shared/src/lib/notifications/notifications.service.ts @@ -39,13 +39,15 @@ import { HttpClient } from '@angular/common/http'; import { Inject, Injectable } from '@angular/core'; -import { FirebaseMessaging, GetTokenOptions } from '@capacitor-firebase/messaging'; import { Platform } from '@ionic/angular'; import { getAuthToken } from '../auth/auth.repository'; import { combineLatest, first, firstValueFrom, Observable, of } from 'rxjs'; import { filter, switchMap, take, tap } from 'rxjs/operators'; import { Channel, Notification, NotificationsRepository } from './notifications.repository'; import { Badge } from '@capawesome/capacitor-badge'; +import { MultiTenantService } from '../multi-tenant/multi-tenant.service'; +import { FCMService } from '../fcm/fcm-global.service'; +import { FCMRepository } from '../fcm/fcm.repository'; import { NetworkService } from '../network/network.service'; import { Capacitor } from '@capacitor/core'; @@ -57,15 +59,18 @@ export class NotificationsService { constructor( @Inject('environment') private environment: any, + private multiTenantService: MultiTenantService, + private fcmService: FCMService, private http: HttpClient, public notificationRepository: NotificationsRepository, + private fcmRepository: FCMRepository, private platform: Platform, private networkService: NetworkService ) { } public getNotifications(authToken: string, offset: number, length: number): Observable { - const url = `${this.environment.apiEndpoint}/notifications`; + const url = `${this.multiTenantService.getApiEndpoint()}/notifications`; const data = { authToken, offset, @@ -76,7 +81,7 @@ export class NotificationsService { } public loadAndStoreChannels(): Observable { - const url = `${this.environment.apiEndpoint}/notifications/channels`; + const url = `${this.multiTenantService.getApiEndpoint()}/notifications/channels`; return this.http.get(url).pipe( tap((channels) => { @@ -113,7 +118,7 @@ export class NotificationsService { return getAuthToken().pipe( filter(authToken => authToken != null), switchMap(authToken => { - const url = `${this.environment.apiEndpoint}/notifications/unsubscribed-channels`; + const url = `${this.multiTenantService.getApiEndpoint()}/notifications/unsubscribed-channels`; const data = { authToken }; @@ -130,7 +135,7 @@ export class NotificationsService { return getAuthToken().pipe( filter(authToken => authToken != null), switchMap(authToken => { - const url = `${this.environment.apiEndpoint}/notifications/channels`; + const url = `${this.multiTenantService.getApiEndpoint()}/notifications/channels`; const data = { authToken, channelCodes: options.channelCodes, @@ -143,15 +148,13 @@ export class NotificationsService { public markUnreadNotificationsAsRead(notificationIds: string[]): Observable { Badge.clear(); - if (this.platform.is('capacitor')) { - FirebaseMessaging.removeAllDeliveredNotifications(); - } + this.fcmService.removeAllDeliveredNotifications(); return getAuthToken().pipe( // On ne balance la requête au serveur que si la liste des notifications à marquer comme lues n'est pas vide filter(authToken => authToken != null && notificationIds.length > 0), switchMap(authToken => { - const url = `${this.environment.apiEndpoint}/notifications/read`; + const url = `${this.multiTenantService.getApiEndpoint()}/notifications/read`; const data = { authToken, notificationIds @@ -162,16 +165,27 @@ export class NotificationsService { ); } - public async saveFCMToken(): Promise { - // On requête un token auprès des serveurs Firebase - const fcmToken = await this.registerPushNotifications(); + public async registerNotificationsAndSaveFCMToken(): Promise { + if (!Capacitor.isNativePlatform() && !this.environment.firebase) { + // On est en web et il n'y a pas de conf firebase dans le env, on ne fait rien + return; + } + + const currentTenant = this.multiTenantService.getCurrentTenantOrThrowError(); + if (!currentTenant) { // A priori pas besoin de check le current tenant car ça ne sera appelé qu'une fois connecté + return; + } + + // On souscrit aux notifs et récupère le token auprès des serveurs Firebase + const fcmToken = await this.fcmService.registerPushNotifications(currentTenant.topic); // Topic is not mandatory - // Si on a bien récupéré le token - if (fcmToken) { + // Si on a bien récupéré le token et qu'il y a quelque chose en place pour gérer les notifs dans le backend (useExternalNotificationSystem) + if (fcmToken && this.environment.useExternalNotificationSystem) { + // On envoie le token au backend const authToken$ = getAuthToken().pipe( filter(authToken => !!authToken), switchMap(authToken => { - const url = `${this.environment.apiEndpoint}/notifications/register`; + const url = `${this.multiTenantService.getApiEndpoint()}/notifications/register`; const data = { authToken, token: fcmToken, @@ -190,17 +204,31 @@ export class NotificationsService { } public async unregisterFCMToken(authToken: string) { + if (!Capacitor.isNativePlatform() && !this.environment.firebase) { + // On est en web et il n'y a pas de conf firebase dans le env, on ne fait rien + return; + } + if (!authToken) { return; } - this.notificationRepository.fcmToken$ + + this.fcmService.unsubscribeFromTopic(); + + if (!this.environment.useExternalNotificationSystem) { + this.notificationRepository.clearNotifications(); + this.fcmRepository.deleteFcmToken(); + return; + } + + this.fcmRepository.fcmToken$ .pipe( take(1), switchMap((fcmToken) => { if (!fcmToken) { return of(null); } - const url = `${this.environment.apiEndpoint}/notifications/unregister`; + const url = `${this.multiTenantService.getApiEndpoint()}/notifications/unregister`; const data = { authToken, @@ -210,17 +238,14 @@ export class NotificationsService { }) ) .subscribe(() => { - this.deleteFCMToken(); + this.notificationRepository.clearNotifications(); + this.fcmRepository.deleteFcmToken(); return; }); } - public async deleteFCMToken() { - this.notificationRepository.clearNotifications(); - } - private removeNotification(authToken: string, notificationId: string) { - const url = `${this.environment.apiEndpoint}/notifications/delete`; + const url = `${this.multiTenantService.getApiEndpoint()}/notifications/delete`; const data = { authToken, notificationId @@ -228,38 +253,4 @@ export class NotificationsService { return this.http.delete(url, { body: data }); } - - private async registerPushNotifications(): Promise { - // Vérifie que l'utilisateur a bien autorisé les notifications - const notificationPermissions = await FirebaseMessaging.requestPermissions(); - - if (notificationPermissions.receive !== 'granted') { - return null; - } - - // Fonction qui nregistre le token FCM dans le state - const handleToken = (tokenResult: { token: string }) => { - // NOTE: on web browser when the user resets the notifications authorisation and wants to allow it again, - // this will trigger a 404 error from firebase followed by this message in the console: - // "FirebaseError: Messaging: A problem occured while unsubscribing the user from FCM", - // it has been reported since 2019 in this thread but hasn't been solved since: - // https://github.com/firebase/firebase-js-sdk/issues/2364 - // It could be fixed by firebase in a future release - this.notificationRepository.setFcmToken(tokenResult.token); - return tokenResult.token || null; - } - - if (!this.platform.is('capacitor')) { // Web - const options: GetTokenOptions = { - vapidKey: this.environment.firebase.vapidKey, - serviceWorkerRegistration: await navigator.serviceWorker.register('firebase-messaging-sw.js'), - }; - - const tokenResult = await FirebaseMessaging.getToken(options); - return handleToken(tokenResult); - } else { // Mobile - const tokenResult = await FirebaseMessaging.getToken(); - return handleToken(tokenResult); - } - } } diff --git a/dev/user-frontend-ionic/projects/shared/src/lib/project-module/translations/module-translate-loader.ts b/dev/user-frontend-ionic/projects/shared/src/lib/project-module/translations/module-translate-loader.ts new file mode 100644 index 00000000..b04f3a81 --- /dev/null +++ b/dev/user-frontend-ionic/projects/shared/src/lib/project-module/translations/module-translate-loader.ts @@ -0,0 +1,258 @@ +/** + * Ce fichier inclut du code provenant de ngx-translate-module-loader, qui est + * licencié sous la licence MIT. + * + * Avis de Licence MIT: + * + * Copyright (c) 2019 Lars Kniep + * + * La présente autorisation est accordée, gracieusement, à toute personne + * obtenant une copie de ce logiciel et des fichiers de documentation + * associés (le « logiciel »), pour interagir avec le logiciel sans + * restriction, y compris, sans limitation, les droits à utiliser, copier, + * modifier, fusionner, publier, distribuer, accorder des sous-licences et/ou + * vendre des copies du logiciel, et de permettre aux personnes à qui le + * logiciel est fourni à le faire sous réserve des conditions suivantes : + * + * La mention de droit d’auteur ci-dessus et cette clause d’autorisation + * doivent être incluses dans toutes les copies ou parties substantielles du + * logiciel. + * + * LE LOGICIEL EST FOURNI « TEL QUEL », SANS GARANTIE D’AUCUNE SORTE, EXPRESSE + * OU TACITE, Y COMPRIS, MAIS SANS S’Y LIMITER, LES GARANTIES DE QUALITÉ + * MARCHANDE,D’ADÉQUATION À UN USAGE PARTICULIER ET LE RESPECT DE LA PROPRIÉTÉ + * INTELLECTUELLE. EN AUCUN CAS, LES AUTEURS OU LES DÉTENTEURS DE DROITS + * D’AUTEUR NE PEUVENT ÊTRE TENUS RESPONSABLES DE TOUTE RÉCLAMATION, DOMMAGE + * OU AUTRE RESPONSABILITÉ, QUE CE SOIT DANS LE CADRE D’UNE ACTION + * CONTRACTUELLE, DÉLICTUELLE OU AUTRE, DÉCOULANT DU LOGICIEL OU DE SON + * UTILISATION OU D’AUTRES INTERACTIONS AVEC LE LOGICIEL, OU EN RELATION AVEC + * CEUX-CI. + * + * + * + * Tout autre code dans ce fichier est licencié sous la licence CeCILL 2.1 : + * + * Copyright ou © ou Copr. Université de Lorraine, (2022) + * + * Direction du Numérique de l'Université de Lorraine - SIED + * (dn-mobile-dev@univ-lorraine.fr) + * JNESIS (contact@jnesis.com) + * + * Ce logiciel est un programme informatique servant à rendre accessible + * sur mobile divers services universitaires aux étudiants et aux personnels + * de l'université. + * + * Ce logiciel est régi par la licence CeCILL 2.1, soumise au droit français + * et respectant les principes de diffusion des logiciels libres. Vous pouvez + * utiliser, modifier et/ou redistribuer ce programme sous les conditions + * de la licence CeCILL telle que diffusée par le CEA, le CNRS et INRIA + * sur le site "http://cecill.info". + * + * En contrepartie de l'accessibilité au code source et des droits de copie, + * de modification et de redistribution accordés par cette licence, il n'est + * offert aux utilisateurs qu'une garantie limitée. Pour les mêmes raisons, + * seule une responsabilité restreinte pèse sur l'auteur du programme, le + * titulaire des droits patrimoniaux et les concédants successifs. + * + * À cet égard, l'attention de l'utilisateur est attirée sur les risques + * associés au chargement, à l'utilisation, à la modification et/ou au + * développement et à la reproduction du logiciel par l'utilisateur étant + * donné sa spécificité de logiciel libre, qui peut le rendre complexe à + * manipuler et qui le réserve donc à des développeurs et des professionnels + * avertis possédant des connaissances informatiques approfondies. Les + * utilisateurs sont donc invités à charger et à tester l'adéquation du + * logiciel à leurs besoins dans des conditions permettant d'assurer la + * sécurité de leurs systèmes et/ou de leurs données et, plus généralement, + * à l'utiliser et à l'exploiter dans les mêmes conditions de sécurité. + * + * Le fait que vous puissiez accéder à cet en-tête signifie que vous avez + * pris connaissance de la licence CeCILL 2.1, et que vous en avez accepté les + * termes. + */ + +import {HttpClient, HttpErrorResponse} from '@angular/common/http'; +import { TranslateLoader } from '@ngx-translate/core'; +import { mergeDeepRight, reduce } from 'ramda'; +import { forkJoin as ForkJoin, MonoTypeOperatorFunction, Observable, of } from 'rxjs'; +import { catchError, map } from 'rxjs/operators'; +import { HttpHeaders } from '@capacitor/core'; +import { MultiTenantService } from '../../multi-tenant/multi-tenant.service'; + +export type Translation = object; + +export interface ModuleTranslationOptions { + modules: ModuleTranslation[]; + disableNamespace?: boolean; + lowercaseNamespace?: boolean; + deepMerge?: boolean; + version?: string | number; + translateError?: (error: any, path: string) => void; + translateMerger?: (translations: Translation[]) => Translation; + headers?: HttpHeaders; +} + +export interface ModuleTranslation { + moduleName?: string; + baseTranslateUrl: string; + namespace?: string; + translateMap?: (translation: Translation) => Translation; + pathTemplate?: string; + headers?: HttpHeaders; +} + +const concatJson = (path: string) => path.concat('.json'); + +const PATH_TEMPLATE_REGEX = /{([^}]+)}/gi; +const PATH_CLEAN_REGEX = /([^:]\/)\/+/gi; +const DEFAULT_PATH_TEMPLATE = '{baseTranslateUrl}/{moduleName}/{fileFolder}/{filename}'; + +export class ModuleTranslateLoader implements TranslateLoader { + private readonly defaultOptions: ModuleTranslationOptions = { + disableNamespace: false, + lowercaseNamespace: false, + deepMerge: true, + ...this.options + }; + + constructor( + private readonly http: HttpClient, + private readonly options: ModuleTranslationOptions, + private multiTenantService: MultiTenantService + ) {} + + public getTranslation(language: string): Observable { + const { defaultOptions: options } = this; + return this.mergeTranslations(this.getModuleTranslations(language, options), options); + } + + private getTenantTranslationFolder() { + const tenantId = this.multiTenantService.getSelectedTenantId(); + if(tenantId) { + return tenantId.toLowerCase(); + } + return ''; + } + + private mergeTranslations( + moduleTranslations: Observable[], + { deepMerge, translateMerger }: ModuleTranslationOptions + ): Observable { + return ForkJoin(moduleTranslations).pipe( + map((translations) => + translateMerger ? + translateMerger(translations) : + deepMerge ? + reduce(mergeDeepRight, Object(), translations) : + translations.reduce((acc, curr) => ({ ...acc, ...curr }), Object())) + ); + } + + private getModuleTranslations(language: string, options: ModuleTranslationOptions): Observable[] { + const { modules } = options; + + // Fetches the translations by module + // If byTenant=true, will also fetch by tenant, retrieving the specific translation file associated with the current tenant + const fetchTranslation = (module: ModuleTranslation, byTenant: boolean): Observable => { + const { moduleName } = module; + return moduleName ? + this.fetchTranslationForModule(language, options, module, byTenant) : + this.fetchTranslation(language, options, module, byTenant); + }; + + // If no tenant, no need to merge + if(!this.multiTenantService.getSelectedTenantId()) { + return modules.map((module) => fetchTranslation(module, false)); + } + + // If there's a tenant currently selected, we'll return a merge between the default translation and the tenant's one, + // with priority to the tenant translation + return modules.map((module) => + this.mergeTranslations( + [fetchTranslation(module, false), fetchTranslation(module, true)], + { ...options, deepMerge: true, translateMerger: null } + ) + ); + } + + private fetchTranslation( + language: string, + { translateError, version, headers }: ModuleTranslationOptions, + { pathTemplate, baseTranslateUrl, translateMap }: ModuleTranslation, + byTenant: boolean = false + ): Observable { + const pathOptions = Object({ + baseTranslateUrl, + filename: language, + fileFolder: byTenant ? this.getTenantTranslationFolder() : '' + }); + const template = pathTemplate || DEFAULT_PATH_TEMPLATE; + + const cleanedPath = concatJson( + template.replace(PATH_TEMPLATE_REGEX, (_, m1: string) => pathOptions[m1] || '') + ).replace(PATH_CLEAN_REGEX, '$1'); + + const path = version ? `${cleanedPath}?v=${version}` : cleanedPath; + + return this.http.get(path, { headers }).pipe( + map((translation) => (translateMap ? translateMap(translation) : translation)), + this.catchError(cleanedPath, translateError, byTenant) + ); + } + + private fetchTranslationForModule( + language: string, + { disableNamespace, lowercaseNamespace, translateError, version, headers }: ModuleTranslationOptions, + { pathTemplate, baseTranslateUrl, moduleName, namespace, translateMap, headers: moduleHeaders }: ModuleTranslation, + byTenant: boolean = false + ): Observable { + const pathOptions = Object({ + baseTranslateUrl: `${baseTranslateUrl}/modules`, + moduleName, + filename: language, + fileFolder: byTenant ? this.getTenantTranslationFolder() : '' + }); + const template = pathTemplate || DEFAULT_PATH_TEMPLATE; + + const namespaceKey = namespace + ? namespace + : lowercaseNamespace + ? moduleName.toLowerCase() + : moduleName.toUpperCase(); + + const cleanedPath = concatJson( + template.replace(PATH_TEMPLATE_REGEX, (_, m1: string) => pathOptions[m1] || '') + ).replace(PATH_CLEAN_REGEX, '$1'); + + const path = version ? `${cleanedPath}?v=${version}` : cleanedPath; + + return this.http.get(path, { headers: moduleHeaders || headers }).pipe( + map((translation) => + translateMap + ? translateMap(translation) + : disableNamespace + ? translation + : Object({ [namespaceKey]: translation }) + ), + this.catchError(cleanedPath, translateError, byTenant) + ); + } + + private catchError( + path: string, + translateError?: (error: any, path: string) => void, + fetchedByTenant: boolean = false + ): MonoTypeOperatorFunction { + return catchError((e: HttpErrorResponse) => { + if(fetchedByTenant && e.status === 404) { + return of(Object()); // Do not throw error if the file wasn't found when fetching for tenant + } + + if (translateError) { + translateError(e, path); + } + + console.error('Unable to load translation file:', path); + return of(Object()); + }); + } +} diff --git a/dev/user-frontend-ionic/projects/shared/src/lib/project-module/translations/translations-loader.factory.ts b/dev/user-frontend-ionic/projects/shared/src/lib/project-module/translations/translations-loader.factory.ts index 118f05a5..55ee1da6 100644 --- a/dev/user-frontend-ionic/projects/shared/src/lib/project-module/translations/translations-loader.factory.ts +++ b/dev/user-frontend-ionic/projects/shared/src/lib/project-module/translations/translations-loader.factory.ts @@ -38,24 +38,35 @@ */ import { HttpClient } from '@angular/common/http'; -import { IModuleTranslationOptions, ModuleTranslateLoader } from '@larscom/ngx-translate-module-loader'; import { ProjectModuleService } from '../project-module.service'; +import { ModuleTranslationOptions, ModuleTranslateLoader } from './module-translate-loader'; +import { MultiTenantService } from '../../multi-tenant/multi-tenant.service'; -export const translationsLoaderFactory = (http: HttpClient, projectModuleService: ProjectModuleService, environment: any) => { - const baseTranslateUrl = './i18n'; - const guidedTourTranslateUrl = { baseTranslateUrl: './i18n/guided-tour'}; +export const translationsLoaderFactory = ( + http: HttpClient, + projectModuleService: ProjectModuleService, + multiTenantService: MultiTenantService, + environment: any +) => { + const baseTranslateUrl = './i18n'; - const translations = projectModuleService.getTranslatedProjectModules().map(projectModule => ({ - baseTranslateUrl, moduleName: projectModule - })); + const translations = projectModuleService.getTranslatedProjectModules().map(projectModule => ({ + baseTranslateUrl, moduleName: projectModule + })); - const options: IModuleTranslationOptions = { - modules: [ - { baseTranslateUrl }, - ...translations, - ...(environment.guidedTourEnabled ? [guidedTourTranslateUrl] : []) - ] - }; + // Conditionally adding guided-tour module translations + if(environment.guidedTourEnabled) { + translations.push({ + baseTranslateUrl, moduleName: 'guided-tour' + }); + } - return new ModuleTranslateLoader(http, options); + const options: ModuleTranslationOptions = { + modules: [ + { baseTranslateUrl }, + ...translations, + ] + }; + + return new ModuleTranslateLoader(http, options, multiTenantService); }; diff --git a/dev/user-frontend-ionic/projects/shared/src/lib/sso/sso.service.ts b/dev/user-frontend-ionic/projects/shared/src/lib/sso/sso.service.ts index b8fd1e09..99f07b98 100644 --- a/dev/user-frontend-ionic/projects/shared/src/lib/sso/sso.service.ts +++ b/dev/user-frontend-ionic/projects/shared/src/lib/sso/sso.service.ts @@ -38,7 +38,8 @@ */ import { HttpClient, HttpErrorResponse } from '@angular/common/http'; -import { Inject, Injectable } from '@angular/core'; +import { Injectable } from '@angular/core'; +import { MultiTenantService } from '../multi-tenant/multi-tenant.service'; import { Observable, throwError } from 'rxjs'; import { catchError, concatMap, filter, map, switchMap, take } from 'rxjs/operators'; import { getAuthToken } from '../auth/auth.repository'; @@ -51,8 +52,7 @@ import { SsoExternalLinkQueryDto, SsoServiceTokenQueryDto } from './sso.dto'; export class SsoService { constructor( - @Inject('environment') - private environment: any, + private multiTenantService: MultiTenantService, private http: HttpClient, private keepAuthService: KeepAuthService, ) {} @@ -70,7 +70,7 @@ export class SsoService { } private requestSsoServiceToken(query: SsoServiceTokenQueryDto): Observable { - const url = `${this.environment.apiEndpoint}/sso-service-token`; + const url = `${this.multiTenantService.getApiEndpoint()}/sso-service-token`; return this.http.post(url, query, {responseType: 'text' as 'json'}).pipe( catchError(err => { diff --git a/dev/user-frontend-ionic/projects/shared/src/lib/statistics/statistics.service.ts b/dev/user-frontend-ionic/projects/shared/src/lib/statistics/statistics.service.ts index ab005fa4..f856815c 100644 --- a/dev/user-frontend-ionic/projects/shared/src/lib/statistics/statistics.service.ts +++ b/dev/user-frontend-ionic/projects/shared/src/lib/statistics/statistics.service.ts @@ -38,13 +38,14 @@ */ import { HttpClient } from '@angular/common/http'; -import { Inject, Injectable } from '@angular/core'; +import { Injectable } from '@angular/core'; import { combineLatest, from, Observable, of } from 'rxjs'; import { catchError, switchMap, take } from 'rxjs/operators'; import { getAuthToken } from '../auth/auth.repository'; import { NetworkService } from '../network/network.service'; import { Capacitor } from '@capacitor/core'; import { statsUid$, updateStatsUid } from './statistics.repository'; +import { MultiTenantService } from '../multi-tenant/multi-tenant.service'; interface UserActionRequestData { authToken: string; @@ -67,8 +68,7 @@ interface UserActionDetails { }) export class StatisticsService { constructor( - @Inject('environment') - private environment: any, + private multiTenantService: MultiTenantService, private http: HttpClient, private networkService: NetworkService, ) {} @@ -89,7 +89,7 @@ export class StatisticsService { } private postUserActionStatistic(userActionDetails: UserActionDetails): Observable { - const url = `${this.environment.apiEndpoint}/statistics/user-action`; + const url = `${this.multiTenantService.getApiEndpoint()}/statistics/user-action`; return combineLatest([getAuthToken(), statsUid$, from(this.networkService.getConnectionStatus())]).pipe( take(1), diff --git a/dev/user-frontend-ionic/projects/shared/src/lib/theme/theme.repository.ts b/dev/user-frontend-ionic/projects/shared/src/lib/theme/theme.repository.ts index bbc873b9..9d4a6812 100644 --- a/dev/user-frontend-ionic/projects/shared/src/lib/theme/theme.repository.ts +++ b/dev/user-frontend-ionic/projects/shared/src/lib/theme/theme.repository.ts @@ -48,13 +48,15 @@ const STORE_NAME = 'theme'; export interface ThemeProps { isDarkTheme: boolean; userHadSetThemeInApp: boolean; + tenantThemeApplied: string; } const store = createStore( { name: STORE_NAME }, withProps({ isDarkTheme: false, - userHadSetThemeInApp: false + userHadSetThemeInApp: false, + tenantThemeApplied: '' }) ); @@ -69,6 +71,7 @@ export const isDarkTheme$ = store.pipe(select((state) => state.isDarkTheme)); export const userHadSetThemeInApp$ = store.pipe(select((state) => state.userHadSetThemeInApp)); +export const tenantThemeApplied$ = store.pipe(select((state) => state.tenantThemeApplied)); export const setIsDarkTheme = (isDarkThemeProps: ThemeProps['isDarkTheme']) => { store.update(setProps({ @@ -82,6 +85,12 @@ export const setUserHaveSetThemeInApp = (userHadSetThemeInAppProps: ThemeProps[' })); }; +export const setTenantThemeApplied = (tenantThemeAppliedProps: ThemeProps['tenantThemeApplied']) => { + store.update(setProps({ + tenantThemeApplied: tenantThemeAppliedProps + })); +}; + export const isDarkTheme = () => store.getValue()?.isDarkTheme; export const userHadSetThemeInApp = () => store.getValue()?.userHadSetThemeInApp; diff --git a/dev/user-frontend-ionic/projects/shared/src/public-api.ts b/dev/user-frontend-ionic/projects/shared/src/public-api.ts index a3d85ee6..67ab9afd 100644 --- a/dev/user-frontend-ionic/projects/shared/src/public-api.ts +++ b/dev/user-frontend-ionic/projects/shared/src/public-api.ts @@ -86,3 +86,10 @@ export * from './lib/store/user-store-helper'; export * from './lib/theme/theme.repository'; export * from './lib/theme/theme.service'; export * from './lib/version/version.service'; +export * from './lib/fcm/fcm-global.service'; +export * from './lib/fcm/fcm.repository'; +export * from './lib/multi-tenant/multi-tenant.module'; +export * from './lib/multi-tenant/multi-tenant.service'; +export * from './lib/multi-tenant/multi-tenant-selected.repository'; +export * from './lib/multi-tenant/multi-tenant.model'; +export * from './lib/multi-tenant/is-tenant-selected.guard'; diff --git a/dev/user-frontend-ionic/projects/social-network/src/lib/social-network.service.ts b/dev/user-frontend-ionic/projects/social-network/src/lib/social-network.service.ts index bbb6b906..5aab07da 100644 --- a/dev/user-frontend-ionic/projects/social-network/src/lib/social-network.service.ts +++ b/dev/user-frontend-ionic/projects/social-network/src/lib/social-network.service.ts @@ -38,7 +38,8 @@ */ import { HttpClient } from '@angular/common/http'; -import { Inject, Injectable } from '@angular/core'; +import { Injectable } from '@angular/core'; +import { MultiTenantService } from '@multi/shared'; import { Observable } from 'rxjs'; import { tap } from 'rxjs/operators'; import { setSocialNetworks, SocialNetwork } from './social-network.repository'; @@ -49,11 +50,10 @@ import { setSocialNetworks, SocialNetwork } from './social-network.repository'; export class SocialNetworkService { constructor( - @Inject('environment') - private environment: any, - private http: HttpClient) { } + private multiTenantService: MultiTenantService, + private http: HttpClient) { } public loadAndStoreSocialNetworks(): Observable { - const url = `${this.environment.apiEndpoint}/social-network`; + const url = `${this.multiTenantService.getApiEndpoint()}/social-network`; return this.http.get(url).pipe( tap(socialNetworks => setSocialNetworks(socialNetworks))); diff --git a/dev/user-frontend-ionic/projects/static-pages/src/lib/static-pages.service.ts b/dev/user-frontend-ionic/projects/static-pages/src/lib/static-pages.service.ts index a4f9ab9d..337552e6 100644 --- a/dev/user-frontend-ionic/projects/static-pages/src/lib/static-pages.service.ts +++ b/dev/user-frontend-ionic/projects/static-pages/src/lib/static-pages.service.ts @@ -38,7 +38,8 @@ */ import { HttpClient } from '@angular/common/http'; -import { Inject, Injectable } from '@angular/core'; +import { Injectable } from '@angular/core'; +import { MultiTenantService } from '@multi/shared'; import { Observable } from 'rxjs'; import { tap } from 'rxjs/operators'; import { StaticPage, StaticPagesRepository } from './static-pages.repository'; @@ -49,14 +50,13 @@ import { StaticPage, StaticPagesRepository } from './static-pages.repository'; export class StaticPagesService { constructor( - @Inject('environment') - private environment: any, + private multiTenantService: MultiTenantService, private http: HttpClient, private staticPagesRepository: StaticPagesRepository - ) { } + ) { } public loadAndStoreStaticPages(): Observable { - const url = `${this.environment.apiEndpoint}/static-pages`; + const url = `${this.multiTenantService.getApiEndpoint()}/static-pages`; return this.http.get(url).pipe( tap((staticPages) => { diff --git a/dev/user-frontend-ionic/projects/unread-mail/src/lib/unread-mail.service.ts b/dev/user-frontend-ionic/projects/unread-mail/src/lib/unread-mail.service.ts index 487a5d11..7a238354 100644 --- a/dev/user-frontend-ionic/projects/unread-mail/src/lib/unread-mail.service.ts +++ b/dev/user-frontend-ionic/projects/unread-mail/src/lib/unread-mail.service.ts @@ -38,8 +38,8 @@ */ import { HttpClient } from '@angular/common/http'; -import { Inject, Injectable } from '@angular/core'; -import { getAuthToken, NetworkService } from '@multi/shared'; +import { Injectable } from '@angular/core'; +import { getAuthToken, MultiTenantService, NetworkService } from '@multi/shared'; import { filter, first, Observable } from 'rxjs'; import { map, switchMap, take, tap } from 'rxjs/operators'; import { MailCalendar, setMails } from './unread-mail.repository'; @@ -49,8 +49,7 @@ import { MailCalendar, setMails } from './unread-mail.repository'; }) export class UnreadMailService { constructor( - @Inject('environment') - private environment: any, + private multiTenantService: MultiTenantService, private http: HttpClient, private networkService: NetworkService ) { } @@ -64,7 +63,7 @@ export class UnreadMailService { } private getMailCalendar(): Observable { - const url = `${this.environment.apiEndpoint}/mail-calendar`; + const url = `${this.multiTenantService.getApiEndpoint()}/mail-calendar`; return getAuthToken().pipe( take(1), switchMap(authToken => this.http.post(url, { authToken })) diff --git a/dev/user-frontend-ionic/src/app/app.component.ts b/dev/user-frontend-ionic/src/app/app.component.ts index f1b975bf..7edabe7d 100644 --- a/dev/user-frontend-ionic/src/app/app.component.ts +++ b/dev/user-frontend-ionic/src/app/app.component.ts @@ -49,12 +49,12 @@ import { Badge } from '@capawesome/capacitor-badge'; import { ModalController, Platform, PopoverController } from '@ionic/angular'; import { TranslateService } from '@ngx-translate/core'; import { - currentLanguage$, features$, FeaturesService, isDarkTheme$, isFeatureStoreInitialized$, NavigationService, + currentLanguage$, features$, FeaturesService, FCMService, isDarkTheme$, isFeatureStoreInitialized$, NavigationService, NotificationsService, NetworkService, PageLayout, PageLayoutService, setIsDarkTheme, StatisticsService, - themeRepoInitialized$, userHadSetThemeInApp, userHadSetThemeInApp$ + themeRepoInitialized$, userHadSetThemeInApp, userHadSetThemeInApp$, tenantThemeApplied$, MultiTenantService } from '@multi/shared'; import { initializeApp } from 'firebase/app'; -import { combineLatest, Observable, of } from 'rxjs'; +import { combineLatest, Observable, of, Subscription } from 'rxjs'; import { distinctUntilChanged, filter, map, switchMap } from 'rxjs/operators'; import { Title } from '@angular/platform-browser'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @@ -70,10 +70,13 @@ export class AppComponent implements OnInit, OnDestroy { public currentPageLayout$: Observable; public isOnline$: Observable; public isNothingToShow$: Observable; + private subscriptions: Subscription[] = []; private backButtonListener: Promise; private appResumeListener: Promise; private destroyRef = inject(DestroyRef); private prefersDark: MediaQueryList; + private themeToApply: string; + private defaultTheme: string; constructor( @Inject('environment') @@ -82,6 +85,7 @@ export class AppComponent implements OnInit, OnDestroy { private translateService: TranslateService, private pageLayoutService: PageLayoutService, private navigationService: NavigationService, + private fcmService: FCMService, private modalController: ModalController, private popoverController: PopoverController, private renderer: Renderer2, @@ -91,18 +95,19 @@ export class AppComponent implements OnInit, OnDestroy { private notificationsService: NotificationsService, private statisticsService: StatisticsService, private titleService: Title, + private multiTenantService: MultiTenantService ) { this.initializeApp(); } - ngOnInit() { + async ngOnInit() { this.titleService.setTitle(this.environment.appTitle); this.initializeBackButton(); this.initializeAppResume(); this.initializeTheme(); this.handleBadge(); - if (!Capacitor.isNativePlatform()) { + if (!Capacitor.isNativePlatform() && this.environment.firebase) { this.initializeFirebase(); } } @@ -144,14 +149,27 @@ export class AppComponent implements OnInit, OnDestroy { themeRepoInitialized$.pipe( filter((isInitialized: boolean) => isInitialized), - switchMap(() => combineLatest([isDarkTheme$, userHadSetThemeInApp$])), + switchMap(() => combineLatest([isDarkTheme$, userHadSetThemeInApp$, tenantThemeApplied$])), takeUntilDestroyed(this.destroyRef) - ).subscribe(([isDarkTheme, userHadSetThemeInApplication]) => { + ).subscribe(([isDarkTheme, userHadSetThemeInApplication, tenantThemeApplied]) => { if (!userHadSetThemeInApplication) { isDarkTheme = this.prefersDark.matches; setIsDarkTheme(isDarkTheme); } this.toggleDarkTheme(isDarkTheme); + + // Remove the current theme from the body + if (this.themeToApply !== '') { + this.disableTenantTheme(this.themeToApply); + } + + // Assign the current theme + this.themeToApply = (tenantThemeApplied !== '') ? tenantThemeApplied : this.defaultTheme; + + // Add the current theme as a class for the body element + if (this.themeToApply !== '') { + this.enableTenantTheme(this.themeToApply); + } }); } @@ -174,6 +192,8 @@ export class AppComponent implements OnInit, OnDestroy { this.initializeSplashScreen(); this.initializeStatusBar(); this.statisticsService.checkAndGenerateStatsUid(); + this.initializeDefaultTheme(); + this.handleTranslationsChangeForTenant(); } private initializeLanguage(): void { @@ -265,4 +285,28 @@ export class AppComponent implements OnInit, OnDestroy { await fixBadgeCount(); } + + private enableTenantTheme(theme: string): void { + const body = document.body; + this.renderer.addClass(body, theme); + } + + private disableTenantTheme(theme: string): void { + const body = document.body; + this.renderer.removeClass(body, theme); + } + + private initializeDefaultTheme() { + this.defaultTheme = this.environment.defaultTheme || ''; + this.themeToApply = this.defaultTheme; + } + + private handleTranslationsChangeForTenant() { + this.subscriptions.push(this.multiTenantService.tenantChange$.subscribe(() => { + this.translateService.setTranslation( + this.translateService.currentLang, + this.translateService.getTranslation(this.translateService.currentLang) + ); // Workaround to force the translateService to register the translations change, not working by simply calling reloadLang() + })); + } } diff --git a/dev/user-frontend-ionic/src/app/app.module.ts b/dev/user-frontend-ionic/src/app/app.module.ts index 24c76443..2cc2cc76 100644 --- a/dev/user-frontend-ionic/src/app/app.module.ts +++ b/dev/user-frontend-ionic/src/app/app.module.ts @@ -47,7 +47,7 @@ import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; import { FeaturesModule } from '@multi/features'; import { MenuModule } from '@multi/menu'; import { PreferencesPageModule } from '@multi/preferences'; -import { AuthInterceptor, ProjectModuleService, translationsLoaderFactory } from '@multi/shared'; +import { AuthInterceptor, MultiTenantModule, ProjectModuleService, MultiTenantService, translationsLoaderFactory } from '@multi/shared'; import { environment } from '../environments/environment'; import { AppRoutingModule } from './app-routing.module'; import { AppComponent } from './app.component'; @@ -74,11 +74,12 @@ import { PageLayoutsModule } from './layout/layouts.module'; FeaturesModule, MenuModule, PreferencesPageModule, + MultiTenantModule, TranslateModule.forRoot({ loader: { provide: TranslateLoader, useFactory: translationsLoaderFactory, - deps: [HttpClient, ProjectModuleService, 'environment'] + deps: [HttpClient, ProjectModuleService, MultiTenantService, 'environment'] } }), ...environment.enabledModules, diff --git a/dev/user-frontend-ionic/src/app/error/app.error-handler.ts b/dev/user-frontend-ionic/src/app/error/app.error-handler.ts index 90d7536e..9502a804 100644 --- a/dev/user-frontend-ionic/src/app/error/app.error-handler.ts +++ b/dev/user-frontend-ionic/src/app/error/app.error-handler.ts @@ -1,3 +1,42 @@ +/* + * Copyright ou © ou Copr. Université de Lorraine, (2022) + * + * Direction du Numérique de l'Université de Lorraine - SIED + * (dn-mobile-dev@univ-lorraine.fr) + * JNESIS (contact@jnesis.com) + * + * Ce logiciel est un programme informatique servant à rendre accessible + * sur mobile divers services universitaires aux étudiants et aux personnels + * de l'université. + * + * Ce logiciel est régi par la licence CeCILL 2.1, soumise au droit français + * et respectant les principes de diffusion des logiciels libres. Vous pouvez + * utiliser, modifier et/ou redistribuer ce programme sous les conditions + * de la licence CeCILL telle que diffusée par le CEA, le CNRS et INRIA + * sur le site "http://cecill.info". + * + * En contrepartie de l'accessibilité au code source et des droits de copie, + * de modification et de redistribution accordés par cette licence, il n'est + * offert aux utilisateurs qu'une garantie limitée. Pour les mêmes raisons, + * seule une responsabilité restreinte pèse sur l'auteur du programme, le + * titulaire des droits patrimoniaux et les concédants successifs. + * + * À cet égard, l'attention de l'utilisateur est attirée sur les risques + * associés au chargement, à l'utilisation, à la modification et/ou au + * développement et à la reproduction du logiciel par l'utilisateur étant + * donné sa spécificité de logiciel libre, qui peut le rendre complexe à + * manipuler et qui le réserve donc à des développeurs et des professionnels + * avertis possédant des connaissances informatiques approfondies. Les + * utilisateurs sont donc invités à charger et à tester l'adéquation du + * logiciel à leurs besoins dans des conditions permettant d'assurer la + * sécurité de leurs systèmes et/ou de leurs données et, plus généralement, + * à l'utiliser et à l'exploiter dans les mêmes conditions de sécurité. + * + * Le fait que vous puissiez accéder à cet en-tête signifie que vous avez + * pris connaissance de la licence CeCILL 2.1, et que vous en avez accepté les + * termes. + */ + import { HttpErrorResponse } from '@angular/common/http'; import { ErrorHandler, Injectable, Injector } from '@angular/core'; import { Actions } from '@ngneat/effects-ng'; diff --git a/dev/user-frontend-ionic/src/app/layout/layout.page.html b/dev/user-frontend-ionic/src/app/layout/layout.page.html index c899e8b2..895e3201 100644 --- a/dev/user-frontend-ionic/src/app/layout/layout.page.html +++ b/dev/user-frontend-ionic/src/app/layout/layout.page.html @@ -40,7 +40,7 @@ - + = new BehaviorSubject([]); public layoutChangeSubject$: Subject = new Subject(); private destroyRef = inject(DestroyRef); + public defaultLogo: string; + private defaultLogoSubscription: Subscription; constructor( private navController: NavController, @@ -101,16 +106,22 @@ export class LayoutPage implements AfterViewInit, OnChanges { private networkService: NetworkService, private notificationsRepository: NotificationsRepository, private notificationsService: NotificationsService, + private multiTenantService: MultiTenantService, private navigationService: NavigationService ) { this.initializeObservables(); this.setupSubscriptions(); + this.handleSingleTenant(); } ngAfterViewInit() { this.loadFeatures(); } + ngOnDestroy() { + this.defaultLogoSubscription.unsubscribe(); + } + ngOnChanges(changes: SimpleChanges): void { if (changes.currentPageLayout) { this.layoutChangeSubject$.next(changes.currentPageLayout.currentValue); @@ -168,6 +179,13 @@ export class LayoutPage implements AfterViewInit, OnChanges { ).subscribe(values => { this.menuItemHasBadgeState$.next(values); }); + + this.defaultLogo = environment.defaultLogo; + this.defaultLogoSubscription = this.multiTenantService.currentTenantLogo$.subscribe(logo => { + if(logo) { + this.defaultLogo = logo; + } + }); } private mapNotificationsToMenuItems(notifications: Notification[], menuItems: MenuItem[]): boolean[] { @@ -213,4 +231,13 @@ export class LayoutPage implements AfterViewInit, OnChanges { public getMenuId(menuItem: MenuItem) { return this.guidedTourService.generateMenuItemIdFromTitle(menuItem); } + + private handleSingleTenant() { + const availableTenants: any[] = this.multiTenantService.getAvailableTenants(); + const isGroup = availableTenants && availableTenants.length === 1 && availableTenants[0].isGroup === true; + + if (this.multiTenantService.isSingleTenant() && !isGroup) { + this.multiTenantService.setCurrentTenantById(this.multiTenantService.getSelectedTenantId()); + } + } } diff --git a/dev/user-frontend-ionic/src/environments/environment.ts.dist b/dev/user-frontend-ionic/src/environments/environment.ts.dist index 0f5c7ca1..e31b3efe 100644 --- a/dev/user-frontend-ionic/src/environments/environment.ts.dist +++ b/dev/user-frontend-ionic/src/environments/environment.ts.dist @@ -61,20 +61,40 @@ import { StaticPagesModule } from '@multi/static-pages'; import { UnreadMailModule } from '@multi/unread-mail'; // import { MatomoModule } from 'ngx-matomo'; - import firebasePwaEnvironment from './firebase/web/firebase-environment.pwa.json'; export const environment: any = { - firebase: firebasePwaEnvironment, + firebase: firebasePwaEnvironment, // A commenter pour désactiver les notifs web production: false, - apiEndpoint: 'http://localhost:3000', languages: ['fr', 'en'], defaultLanguage: 'fr', - cmsPublicAssetsEndpoint: 'http://localhost:8055/assets/', guidedTourEnabled: true, appTitle: 'Esup-Multi', appVersion: '1.0.0', forceFullLayoutFeatures: [], + useExternalNotificationSystem: false, + defaultLogo: 'assets/logos/white-logo.svg', + tenants: [ + { + apiEndpoint: 'http://localhost:3000', + cmsPublicAssetsEndpoint: 'http://localhost:8055/assets/', + modulesConfigurations: { + chatbot: { + logoRegex: /_chacha5/i + }, + map: { + defaultLocation: { + longitude: 2.3488596, + latitude: 48.8533249 + } + }, + reservation: { + ssoServiceName: 'https://mon-espace-de-resa.fr', + ssoUrlTemplate: 'https://mon-espace-de-resa.fr/auth?ticket={st}', + } + }, + } + ], enabledModules: [ AppUpdateModule, AuthModule, @@ -83,27 +103,19 @@ export const environment: any = { display: 'list', }), CardsPageModule.forRoot({ knownErrors: ['NO_PHOTO', 'NO_ACTIVE_CARD', 'UNPAID_FEES'] }), - ChatbotModule.forRoot({ chatbotLogoRegex: /_chacha5/i }), + ChatbotModule, ClockingModule, ContactUsModule, ContactsModule.forRoot({ contactTypes: ['STUDENT', 'STAFF', 'STANDIN'] }), ImportantNewsModule.forRoot({ display: 'vertically' }), - MapModule.forRoot({ - defaultMapLocation: { - longitude: 2.3488596, - latitude: 48.8533249 - } - }), + MapModule, NotificationsModule.forRoot({ numberOfNotificationsOnFirstLoad: 20, numberOfNotificationsToLoadOnScroll: 10 }), - ReservationModule.forRoot({ - reservationSsoServiceName: 'https://mon-espace-de-resa.fr', - reservationSsoUrlTemplate: 'https://mon-espace-de-resa.fr/auth?ticket={st}', - }), + ReservationModule, RestaurantsModule, RssPageModule.forRoot({ latestNewsWidget: { diff --git a/dev/user-frontend-ionic/src/theme/app-theme-dist/guided-tour/src/lib/config/anonymous-guided-tour.config.ts b/dev/user-frontend-ionic/src/theme/app-theme-dist/guided-tour/src/lib/config/anonymous-guided-tour.config.ts index 94e7b78c..e8af782c 100644 --- a/dev/user-frontend-ionic/src/theme/app-theme-dist/guided-tour/src/lib/config/anonymous-guided-tour.config.ts +++ b/dev/user-frontend-ionic/src/theme/app-theme-dist/guided-tour/src/lib/config/anonymous-guided-tour.config.ts @@ -44,11 +44,11 @@ import { TranslateService} from '@ngx-translate/core'; export const anonymousSteps= (router: Router, translateService: TranslateService, onComplete?: () => void): Step.StepOptions[] => [ { id: 'anonymous-step-10', - text: translateService.instant('GUIDED_TOUR.ANONYMOUS.STEP_10.MESSAGE'), + text: translateService.instant('GUIDED-TOUR.ANONYMOUS.STEP_10.MESSAGE'), buttons: [ { classes: 'shepherd-button-secondary', - text: translateService.instant('GUIDED_TOUR.CLOSE'), + text: translateService.instant('GUIDED-TOUR.CLOSE'), action() { onComplete(); return this.complete(); @@ -56,7 +56,7 @@ export const anonymousSteps= (router: Router, translateService: TranslateService }, { classes: 'shepherd-button-primary', - text: translateService.instant('GUIDED_TOUR.NEXT'), + text: translateService.instant('GUIDED-TOUR.NEXT'), action() { this.next(); } @@ -69,11 +69,11 @@ export const anonymousSteps= (router: Router, translateService: TranslateService element: '[data-menu-id="main-tab-bar"]', on: 'top' }, - text: translateService.instant('GUIDED_TOUR.ANONYMOUS.STEP_20.MESSAGE'), + text: translateService.instant('GUIDED-TOUR.ANONYMOUS.STEP_20.MESSAGE'), buttons: [ { classes: 'shepherd-button-secondary', - text: translateService.instant('GUIDED_TOUR.CLOSE'), + text: translateService.instant('GUIDED-TOUR.CLOSE'), action() { onComplete(); return this.complete(); @@ -81,7 +81,7 @@ export const anonymousSteps= (router: Router, translateService: TranslateService }, { classes: 'shepherd-button-primary', - text: translateService.instant('GUIDED_TOUR.NEXT'), + text: translateService.instant('GUIDED-TOUR.NEXT'), action() { this.next(); } @@ -94,11 +94,11 @@ export const anonymousSteps= (router: Router, translateService: TranslateService element: '[data-widget-id="auth:auth-not-authentified-widget"]', on: 'bottom' }, - text: translateService.instant('GUIDED_TOUR.ANONYMOUS.STEP_30.MESSAGE'), + text: translateService.instant('GUIDED-TOUR.ANONYMOUS.STEP_30.MESSAGE'), buttons: [ { classes: 'shepherd-button-primary', - text: translateService.instant('GUIDED_TOUR.FINISH'), + text: translateService.instant('GUIDED-TOUR.FINISH'), action() { onComplete(); return this.complete(); diff --git a/dev/user-frontend-ionic/src/theme/app-theme-dist/guided-tour/src/lib/config/logged-guided-tour.config.ts b/dev/user-frontend-ionic/src/theme/app-theme-dist/guided-tour/src/lib/config/logged-guided-tour.config.ts index cacae8e7..116aaad4 100644 --- a/dev/user-frontend-ionic/src/theme/app-theme-dist/guided-tour/src/lib/config/logged-guided-tour.config.ts +++ b/dev/user-frontend-ionic/src/theme/app-theme-dist/guided-tour/src/lib/config/logged-guided-tour.config.ts @@ -44,11 +44,11 @@ import { TranslateService } from '@ngx-translate/core'; export const loggedSteps= (router: Router, translateService: TranslateService, onComplete?: () => void): Step.StepOptions[] => [ { id: 'logged-step-10', - text: translateService.instant('GUIDED_TOUR.LOGGED.STEP_10.MESSAGE'), + text: translateService.instant('GUIDED-TOUR.LOGGED.STEP_10.MESSAGE'), buttons: [ { classes: 'shepherd-button-secondary', - text: translateService.instant('GUIDED_TOUR.CLOSE'), + text: translateService.instant('GUIDED-TOUR.CLOSE'), action() { onComplete(); return this.complete(); @@ -56,7 +56,7 @@ export const loggedSteps= (router: Router, translateService: TranslateService, o }, { classes: 'shepherd-button-primary', - text: translateService.instant('GUIDED_TOUR.NEXT'), + text: translateService.instant('GUIDED-TOUR.NEXT'), action() { this.next(); } @@ -72,7 +72,7 @@ export const loggedSteps= (router: Router, translateService: TranslateService, o buttons: [ { classes: 'shepherd-button-secondary', - text: translateService.instant('GUIDED_TOUR.CLOSE'), + text: translateService.instant('GUIDED-TOUR.CLOSE'), action() { onComplete(); return this.complete(); @@ -80,13 +80,13 @@ export const loggedSteps= (router: Router, translateService: TranslateService, o }, { classes: 'shepherd-button-primary', - text: translateService.instant('GUIDED_TOUR.NEXT'), + text: translateService.instant('GUIDED-TOUR.NEXT'), action() { this.next(); } } ], - text: `${translateService.instant('GUIDED_TOUR.LOGGED.STEP_20.MESSAGE')} + text: `${translateService.instant('GUIDED-TOUR.LOGGED.STEP_20.MESSAGE')}
`, }, { @@ -98,7 +98,7 @@ export const loggedSteps= (router: Router, translateService: TranslateService, o buttons: [ { classes: 'shepherd-button-secondary', - text: translateService.instant('GUIDED_TOUR.CLOSE'), + text: translateService.instant('GUIDED-TOUR.CLOSE'), action() { onComplete(); return this.complete(); @@ -106,13 +106,13 @@ export const loggedSteps= (router: Router, translateService: TranslateService, o }, { classes: 'shepherd-button-primary', - text: translateService.instant('GUIDED_TOUR.NEXT'), + text: translateService.instant('GUIDED-TOUR.NEXT'), action() { this.next(); } } ], - text: `${translateService.instant('GUIDED_TOUR.LOGGED.STEP_30.MESSAGE')} + text: `${translateService.instant('GUIDED-TOUR.LOGGED.STEP_30.MESSAGE')}
`, }, { @@ -124,7 +124,7 @@ export const loggedSteps= (router: Router, translateService: TranslateService, o buttons: [ { classes: 'shepherd-button-secondary', - text: translateService.instant('GUIDED_TOUR.CLOSE'), + text: translateService.instant('GUIDED-TOUR.CLOSE'), action() { onComplete(); return this.complete(); @@ -132,13 +132,13 @@ export const loggedSteps= (router: Router, translateService: TranslateService, o }, { classes: 'shepherd-button-primary', - text: translateService.instant('GUIDED_TOUR.NEXT'), + text: translateService.instant('GUIDED-TOUR.NEXT'), action() { this.next(); } } ], - text: `${translateService.instant('GUIDED_TOUR.LOGGED.STEP_40.MESSAGE')}`, + text: `${translateService.instant('GUIDED-TOUR.LOGGED.STEP_40.MESSAGE')}`, }, { id: 'logged-step-50', @@ -149,7 +149,7 @@ export const loggedSteps= (router: Router, translateService: TranslateService, o buttons: [ { classes: 'shepherd-button-secondary', - text: translateService.instant('GUIDED_TOUR.CLOSE'), + text: translateService.instant('GUIDED-TOUR.CLOSE'), action() { onComplete(); return this.complete(); @@ -157,13 +157,13 @@ export const loggedSteps= (router: Router, translateService: TranslateService, o }, { classes: 'shepherd-button-primary', - text: translateService.instant('GUIDED_TOUR.NEXT'), + text: translateService.instant('GUIDED-TOUR.NEXT'), action() { this.next(); } } ], - text: `${translateService.instant('GUIDED_TOUR.LOGGED.STEP_50.MESSAGE')}`, + text: `${translateService.instant('GUIDED-TOUR.LOGGED.STEP_50.MESSAGE')}`, }, { id: 'logged-step-60', @@ -171,7 +171,7 @@ export const loggedSteps= (router: Router, translateService: TranslateService, o buttons: [ { classes: 'shepherd-button-secondary', - text: translateService.instant('GUIDED_TOUR.CLOSE'), + text: translateService.instant('GUIDED-TOUR.CLOSE'), action() { onComplete(); return this.complete(); @@ -179,13 +179,13 @@ export const loggedSteps= (router: Router, translateService: TranslateService, o }, { classes: 'shepherd-button-primary', - text: translateService.instant('GUIDED_TOUR.NEXT'), + text: translateService.instant('GUIDED-TOUR.NEXT'), action() { this.next(); } } ], - text: `${translateService.instant('GUIDED_TOUR.LOGGED.STEP_60.MESSAGE')} + text: `${translateService.instant('GUIDED-TOUR.LOGGED.STEP_60.MESSAGE')}
`, }, { @@ -197,7 +197,7 @@ export const loggedSteps= (router: Router, translateService: TranslateService, o buttons: [ { classes: 'shepherd-button-secondary', - text: translateService.instant('GUIDED_TOUR.CLOSE'), + text: translateService.instant('GUIDED-TOUR.CLOSE'), action() { onComplete(); return this.complete(); @@ -205,13 +205,13 @@ export const loggedSteps= (router: Router, translateService: TranslateService, o }, { classes: 'shepherd-button-primary', - text: translateService.instant('GUIDED_TOUR.NEXT'), + text: translateService.instant('GUIDED-TOUR.NEXT'), action() { this.next(); } } ], - text: `${translateService.instant('GUIDED_TOUR.LOGGED.STEP_70.MESSAGE')}`, + text: `${translateService.instant('GUIDED-TOUR.LOGGED.STEP_70.MESSAGE')}`, }, { id: 'logged-step-80', @@ -222,7 +222,7 @@ export const loggedSteps= (router: Router, translateService: TranslateService, o buttons: [ { classes: 'shepherd-button-secondary', - text: translateService.instant('GUIDED_TOUR.CLOSE'), + text: translateService.instant('GUIDED-TOUR.CLOSE'), action() { onComplete(); return this.complete(); @@ -230,13 +230,13 @@ export const loggedSteps= (router: Router, translateService: TranslateService, o }, { classes: 'shepherd-button-primary', - text: translateService.instant('GUIDED_TOUR.NEXT'), + text: translateService.instant('GUIDED-TOUR.NEXT'), action() { this.next(); } } ], - text: `${translateService.instant('GUIDED_TOUR.LOGGED.STEP_80.MESSAGE')} + text: `${translateService.instant('GUIDED-TOUR.LOGGED.STEP_80.MESSAGE')}
`, }, { @@ -248,7 +248,7 @@ export const loggedSteps= (router: Router, translateService: TranslateService, o buttons: [ { classes: 'shepherd-button-secondary', - text: translateService.instant('GUIDED_TOUR.CLOSE'), + text: translateService.instant('GUIDED-TOUR.CLOSE'), action() { onComplete(); return this.complete(); @@ -256,13 +256,13 @@ export const loggedSteps= (router: Router, translateService: TranslateService, o }, { classes: 'shepherd-button-primary', - text: translateService.instant('GUIDED_TOUR.NEXT'), + text: translateService.instant('GUIDED-TOUR.NEXT'), action() { this.next(); } } ], - text: `${translateService.instant('GUIDED_TOUR.LOGGED.STEP_90.MESSAGE')} + text: `${translateService.instant('GUIDED-TOUR.LOGGED.STEP_90.MESSAGE')}
`, }, { @@ -274,7 +274,7 @@ export const loggedSteps= (router: Router, translateService: TranslateService, o buttons: [ { classes: 'shepherd-button-secondary', - text: translateService.instant('GUIDED_TOUR.CLOSE'), + text: translateService.instant('GUIDED-TOUR.CLOSE'), action() { onComplete(); return this.complete(); @@ -282,13 +282,13 @@ export const loggedSteps= (router: Router, translateService: TranslateService, o }, { classes: 'shepherd-button-primary', - text: translateService.instant('GUIDED_TOUR.NEXT'), + text: translateService.instant('GUIDED-TOUR.NEXT'), action() { this.next(); } } ], - text: `${translateService.instant('GUIDED_TOUR.LOGGED.STEP_100.MESSAGE')} + text: `${translateService.instant('GUIDED-TOUR.LOGGED.STEP_100.MESSAGE')}
`, }, { @@ -297,7 +297,7 @@ export const loggedSteps= (router: Router, translateService: TranslateService, o buttons: [ { classes: 'shepherd-button-secondary', - text: translateService.instant('GUIDED_TOUR.CLOSE'), + text: translateService.instant('GUIDED-TOUR.CLOSE'), action() { onComplete(); return this.complete(); @@ -305,13 +305,13 @@ export const loggedSteps= (router: Router, translateService: TranslateService, o }, { classes: 'shepherd-button-primary', - text: translateService.instant('GUIDED_TOUR.NEXT'), + text: translateService.instant('GUIDED-TOUR.NEXT'), action() { this.next(); } } ], - text: `${translateService.instant('GUIDED_TOUR.LOGGED.STEP_110.MESSAGE')}`, + text: `${translateService.instant('GUIDED-TOUR.LOGGED.STEP_110.MESSAGE')}`, }, { id: 'logged-step-120', @@ -322,7 +322,7 @@ export const loggedSteps= (router: Router, translateService: TranslateService, o buttons: [ { classes: 'shepherd-button-secondary', - text: translateService.instant('GUIDED_TOUR.CLOSE'), + text: translateService.instant('GUIDED-TOUR.CLOSE'), action() { onComplete(); return this.complete(); @@ -330,13 +330,13 @@ export const loggedSteps= (router: Router, translateService: TranslateService, o }, { classes: 'shepherd-button-primary', - text: translateService.instant('GUIDED_TOUR.NEXT'), + text: translateService.instant('GUIDED-TOUR.NEXT'), action() { this.next(); } } ], - text: `${translateService.instant('GUIDED_TOUR.LOGGED.STEP_120.MESSAGE')}`, + text: `${translateService.instant('GUIDED-TOUR.LOGGED.STEP_120.MESSAGE')}`, }, { id: 'logged-step-130', @@ -347,7 +347,7 @@ export const loggedSteps= (router: Router, translateService: TranslateService, o buttons: [ { classes: 'shepherd-button-secondary', - text: translateService.instant('GUIDED_TOUR.CLOSE'), + text: translateService.instant('GUIDED-TOUR.CLOSE'), action() { onComplete(); return this.complete(); @@ -355,13 +355,13 @@ export const loggedSteps= (router: Router, translateService: TranslateService, o }, { classes: 'shepherd-button-primary', - text: translateService.instant('GUIDED_TOUR.NEXT'), + text: translateService.instant('GUIDED-TOUR.NEXT'), action() { this.next(); } } ], - text: `${translateService.instant('GUIDED_TOUR.LOGGED.STEP_130.MESSAGE')}`, + text: `${translateService.instant('GUIDED-TOUR.LOGGED.STEP_130.MESSAGE')}`, }, { id: 'logged-step-140', @@ -372,13 +372,13 @@ export const loggedSteps= (router: Router, translateService: TranslateService, o buttons: [ { classes: 'shepherd-button-primary', - text: translateService.instant('GUIDED_TOUR.FINISH'), + text: translateService.instant('GUIDED-TOUR.FINISH'), action() { onComplete(); return this.complete(); } } ], - text: `${translateService.instant('GUIDED_TOUR.LOGGED.STEP_140.MESSAGE')}`, + text: `${translateService.instant('GUIDED-TOUR.LOGGED.STEP_140.MESSAGE')}`, }, ]; diff --git a/dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/guided-tour/en.json b/dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/guided-tour/en.json deleted file mode 100644 index 0f2d24d8..00000000 --- a/dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/guided-tour/en.json +++ /dev/null @@ -1,62 +0,0 @@ -{ - "GUIDED_TOUR": { - "NEXT": "Next", - "CLOSE": "Close", - "FINISH": "Finish", - "ANONYMOUS": { - "STEP_10" : { - "MESSAGE": "Welcome to the new version of your mobile application." - }, - "STEP_20" : { - "MESSAGE": "A new ergonomics allows you to navigate through the services it offers…" - }, - "STEP_30" : { - "MESSAGE": "...but before going any further, you are asked to authenticate yourself." - } - }, - "LOGGED": { - "STEP_10" : { - "MESSAGE": "You are now authenticated and you will be able to access personalized information." - }, - "STEP_20" : { - "MESSAGE": "This screen offers you direct and quick access to your immediate information: next courses, appointments, number of e-mails to read." - }, - "STEP_30" : { - "MESSAGE": "Here you will find your notifications…" - }, - "STEP_40" : { - "MESSAGE": "…and here the news of the institution." - }, - "STEP_50" : { - "MESSAGE": "This screen offers you all the services available in the application." - }, - "STEP_60" : { - "MESSAGE": "You can sort the services as you wish by a long press on the icon." - }, - "STEP_70" : { - "MESSAGE": "You can also filter services through a search." - }, - "STEP_80" : { - "MESSAGE": "Here you will find a dematerialized version of your student or staff cards" - }, - "STEP_90" : { - "MESSAGE": "The chatbot will take care of answering your questions about your computer account, your connection or your schedule." - }, - "STEP_100" : { - "MESSAGE": "Finally, this menu will allow you to access the application settings…" - }, - "STEP_110" : { - "MESSAGE": "…it also offers you additional information." - }, - "STEP_120" : { - "MESSAGE": "ou have the option to change the dark/light theme and the language." - }, - "STEP_130" : { - "MESSAGE": "You can manage your connection settings." - }, - "STEP_140" : { - "MESSAGE": "Finally, these menus will allow you to report a bug or request assistance." - } - } - } -} diff --git a/dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/guided-tour/fr.json b/dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/guided-tour/fr.json deleted file mode 100644 index 7cbdddde..00000000 --- a/dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/guided-tour/fr.json +++ /dev/null @@ -1,62 +0,0 @@ -{ - "GUIDED_TOUR": { - "NEXT": "Suivant", - "CLOSE": "Fermer", - "FINISH": "Terminer", - "ANONYMOUS": { - "STEP_10" : { - "MESSAGE": "Bienvenue sur la nouvelle version de votre application mobile." - }, - "STEP_20" : { - "MESSAGE": "Une nouvelle ergonomie vous permet de naviguer dans les services qu’elle propose..." - }, - "STEP_30" : { - "MESSAGE": "…mais avant d’aller plus loin, vous êtes invité à vous authentifier." - } - }, - "LOGGED": { - "STEP_10" : { - "MESSAGE": "Vous êtes désormais authentifié et vous allez pouvoir accéder à des informations personnalisées." - }, - "STEP_20" : { - "MESSAGE": "Cet écran vous propose un accès direct et rapide à vos informations immédiates : prochains cours, rendez-vous, nombre d’e-mails à lire." - }, - "STEP_30" : { - "MESSAGE": "Vous retrouverez ici vos notifications…" - }, - "STEP_40" : { - "MESSAGE": "… et ici les actualités de l'établissement" - }, - "STEP_50" : { - "MESSAGE": "Cet écran vous propose l’ensemble des services disponibles dans l’application." - }, - "STEP_60" : { - "MESSAGE": "Vous pouvez trier les services comme vous le souhaitez par un appui long sur l’icône." - }, - "STEP_70" : { - "MESSAGE": "Vous pouvez également filtrer les services grâce à une recherche." - }, - "STEP_80" : { - "MESSAGE": "Vous trouverez ici une version dématérialisée de vos cartes d'étudiant ou de personnel." - }, - "STEP_90" : { - "MESSAGE": "Le chatbot se chargera de répondre à vos questions concernant votre compte informatique, votre connexion ou votre emploi du temps." - }, - "STEP_100" : { - "MESSAGE": "Enfin ce menu vous permettra d’accéder aux paramètres de l’application…" - }, - "STEP_110" : { - "MESSAGE": "…il vous propose également des informations complémentaires." - }, - "STEP_120" : { - "MESSAGE": "Vous avez la possibilité de changer le thème sombre/clair et la langue." - }, - "STEP_130" : { - "MESSAGE": "Vous pouvez gérer vos paramètres de connexion." - }, - "STEP_140" : { - "MESSAGE": "Enfin ces menus vous permettront de signaler un dysfonctionnement ou demander de l’aide." - } - } - } -} diff --git a/dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/app-update/en.json b/dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/modules/app-update/en.json similarity index 100% rename from dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/app-update/en.json rename to dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/modules/app-update/en.json diff --git a/dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/app-update/fr.json b/dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/modules/app-update/fr.json similarity index 100% rename from dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/app-update/fr.json rename to dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/modules/app-update/fr.json diff --git a/dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/auth/en.json b/dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/modules/auth/en.json similarity index 100% rename from dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/auth/en.json rename to dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/modules/auth/en.json diff --git a/dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/auth/fr.json b/dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/modules/auth/fr.json similarity index 100% rename from dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/auth/fr.json rename to dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/modules/auth/fr.json diff --git a/dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/calendar/en.json b/dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/modules/calendar/en.json similarity index 100% rename from dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/calendar/en.json rename to dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/modules/calendar/en.json diff --git a/dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/calendar/fr.json b/dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/modules/calendar/fr.json similarity index 100% rename from dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/calendar/fr.json rename to dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/modules/calendar/fr.json diff --git a/dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/cards/en.json b/dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/modules/cards/en.json similarity index 100% rename from dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/cards/en.json rename to dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/modules/cards/en.json diff --git a/dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/cards/fr.json b/dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/modules/cards/fr.json similarity index 100% rename from dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/cards/fr.json rename to dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/modules/cards/fr.json diff --git a/dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/chatbot/en.json b/dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/modules/chatbot/en.json similarity index 100% rename from dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/chatbot/en.json rename to dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/modules/chatbot/en.json diff --git a/dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/chatbot/fr.json b/dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/modules/chatbot/fr.json similarity index 100% rename from dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/chatbot/fr.json rename to dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/modules/chatbot/fr.json diff --git a/dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/clocking/en.json b/dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/modules/clocking/en.json similarity index 100% rename from dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/clocking/en.json rename to dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/modules/clocking/en.json diff --git a/dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/clocking/fr.json b/dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/modules/clocking/fr.json similarity index 100% rename from dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/clocking/fr.json rename to dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/modules/clocking/fr.json diff --git a/dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/contact-us/en.json b/dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/modules/contact-us/en.json similarity index 100% rename from dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/contact-us/en.json rename to dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/modules/contact-us/en.json diff --git a/dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/contact-us/fr.json b/dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/modules/contact-us/fr.json similarity index 100% rename from dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/contact-us/fr.json rename to dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/modules/contact-us/fr.json diff --git a/dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/contacts/en.json b/dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/modules/contacts/en.json similarity index 100% rename from dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/contacts/en.json rename to dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/modules/contacts/en.json diff --git a/dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/contacts/fr.json b/dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/modules/contacts/fr.json similarity index 100% rename from dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/contacts/fr.json rename to dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/modules/contacts/fr.json diff --git a/dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/features/en.json b/dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/modules/features/en.json similarity index 100% rename from dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/features/en.json rename to dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/modules/features/en.json diff --git a/dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/features/fr.json b/dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/modules/features/fr.json similarity index 100% rename from dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/features/fr.json rename to dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/modules/features/fr.json diff --git a/dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/modules/guided-tour/en.json b/dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/modules/guided-tour/en.json new file mode 100644 index 00000000..b93ec31e --- /dev/null +++ b/dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/modules/guided-tour/en.json @@ -0,0 +1,60 @@ +{ + "NEXT": "Next", + "CLOSE": "Close", + "FINISH": "Finish", + "ANONYMOUS": { + "STEP_10" : { + "MESSAGE": "Welcome to the new version of your mobile application." + }, + "STEP_20" : { + "MESSAGE": "A new ergonomics allows you to navigate through the services it offers…" + }, + "STEP_30" : { + "MESSAGE": "...but before going any further, you are asked to authenticate yourself." + } + }, + "LOGGED": { + "STEP_10" : { + "MESSAGE": "You are now authenticated and you will be able to access personalized information." + }, + "STEP_20" : { + "MESSAGE": "This screen offers you direct and quick access to your immediate information: next courses, appointments, number of e-mails to read." + }, + "STEP_30" : { + "MESSAGE": "Here you will find your notifications…" + }, + "STEP_40" : { + "MESSAGE": "…and here the news of the institution." + }, + "STEP_50" : { + "MESSAGE": "This screen offers you all the services available in the application." + }, + "STEP_60" : { + "MESSAGE": "You can sort the services as you wish by a long press on the icon." + }, + "STEP_70" : { + "MESSAGE": "You can also filter services through a search." + }, + "STEP_80" : { + "MESSAGE": "Here you will find a dematerialized version of your student or staff cards" + }, + "STEP_90" : { + "MESSAGE": "The chatbot will take care of answering your questions about your computer account, your connection or your schedule." + }, + "STEP_100" : { + "MESSAGE": "Finally, this menu will allow you to access the application settings…" + }, + "STEP_110" : { + "MESSAGE": "…it also offers you additional information." + }, + "STEP_120" : { + "MESSAGE": "ou have the option to change the dark/light theme and the language." + }, + "STEP_130" : { + "MESSAGE": "You can manage your connection settings." + }, + "STEP_140" : { + "MESSAGE": "Finally, these menus will allow you to report a bug or request assistance." + } + } +} diff --git a/dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/modules/guided-tour/fr.json b/dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/modules/guided-tour/fr.json new file mode 100644 index 00000000..ef9583bf --- /dev/null +++ b/dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/modules/guided-tour/fr.json @@ -0,0 +1,60 @@ +{ + "NEXT": "Suivant", + "CLOSE": "Fermer", + "FINISH": "Terminer", + "ANONYMOUS": { + "STEP_10" : { + "MESSAGE": "Bienvenue sur la nouvelle version de votre application mobile." + }, + "STEP_20" : { + "MESSAGE": "Une nouvelle ergonomie vous permet de naviguer dans les services qu’elle propose..." + }, + "STEP_30" : { + "MESSAGE": "…mais avant d’aller plus loin, vous êtes invité à vous authentifier." + } + }, + "LOGGED": { + "STEP_10" : { + "MESSAGE": "Vous êtes désormais authentifié et vous allez pouvoir accéder à des informations personnalisées." + }, + "STEP_20" : { + "MESSAGE": "Cet écran vous propose un accès direct et rapide à vos informations immédiates : prochains cours, rendez-vous, nombre d’e-mails à lire." + }, + "STEP_30" : { + "MESSAGE": "Vous retrouverez ici vos notifications…" + }, + "STEP_40" : { + "MESSAGE": "… et ici les actualités de l'établissement" + }, + "STEP_50" : { + "MESSAGE": "Cet écran vous propose l’ensemble des services disponibles dans l’application." + }, + "STEP_60" : { + "MESSAGE": "Vous pouvez trier les services comme vous le souhaitez par un appui long sur l’icône." + }, + "STEP_70" : { + "MESSAGE": "Vous pouvez également filtrer les services grâce à une recherche." + }, + "STEP_80" : { + "MESSAGE": "Vous trouverez ici une version dématérialisée de vos cartes d'étudiant ou de personnel." + }, + "STEP_90" : { + "MESSAGE": "Le chatbot se chargera de répondre à vos questions concernant votre compte informatique, votre connexion ou votre emploi du temps." + }, + "STEP_100" : { + "MESSAGE": "Enfin ce menu vous permettra d’accéder aux paramètres de l’application…" + }, + "STEP_110" : { + "MESSAGE": "…il vous propose également des informations complémentaires." + }, + "STEP_120" : { + "MESSAGE": "Vous avez la possibilité de changer le thème sombre/clair et la langue." + }, + "STEP_130" : { + "MESSAGE": "Vous pouvez gérer vos paramètres de connexion." + }, + "STEP_140" : { + "MESSAGE": "Enfin ces menus vous permettront de signaler un dysfonctionnement ou demander de l’aide." + } + } +} diff --git a/dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/map/en.json b/dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/modules/map/en.json similarity index 100% rename from dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/map/en.json rename to dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/modules/map/en.json diff --git a/dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/map/fr.json b/dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/modules/map/fr.json similarity index 100% rename from dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/map/fr.json rename to dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/modules/map/fr.json diff --git a/dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/menu/en.json b/dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/modules/menu/en.json similarity index 100% rename from dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/menu/en.json rename to dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/modules/menu/en.json diff --git a/dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/menu/fr.json b/dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/modules/menu/fr.json similarity index 100% rename from dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/menu/fr.json rename to dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/modules/menu/fr.json diff --git a/dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/modules/multi-tenant/en.json b/dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/modules/multi-tenant/en.json new file mode 100644 index 00000000..ec7b1268 --- /dev/null +++ b/dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/modules/multi-tenant/en.json @@ -0,0 +1,10 @@ +{ + "CHOICE_UNIVERSITY": "Please choose your university to connect:", + "UNIVERSITY": "University", + "VALIDATE": "Validate", + "UNIVERSITY_CHANGE": "Change university", + "CHANGE_UNIVERSITY_CONFIRMATION" : { + "HEADER": "Univeristy change confirmation", + "MESSAGE": "Are you sure you want to change university ?" + } +} diff --git a/dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/modules/multi-tenant/fr.json b/dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/modules/multi-tenant/fr.json new file mode 100644 index 00000000..d6cbdf03 --- /dev/null +++ b/dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/modules/multi-tenant/fr.json @@ -0,0 +1,10 @@ +{ + "CHOICE_UNIVERSITY": "Veuillez sélectionner votre université pour vous connecter :", + "UNIVERSITY": "Université", + "VALIDATE": "Valider", + "UNIVERSITY_CHANGE": "Changer d'université", + "CHANGE_UNIVERSITY_CONFIRMATION" : { + "HEADER": "Confirmation de changement d'université", + "MESSAGE": "Êtes-vous sûr de vouloir changer d'université ?" + } +} diff --git a/dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/notifications/en.json b/dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/modules/notifications/en.json similarity index 100% rename from dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/notifications/en.json rename to dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/modules/notifications/en.json diff --git a/dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/notifications/fr.json b/dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/modules/notifications/fr.json similarity index 100% rename from dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/notifications/fr.json rename to dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/modules/notifications/fr.json diff --git a/dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/preferences/en.json b/dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/modules/preferences/en.json similarity index 100% rename from dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/preferences/en.json rename to dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/modules/preferences/en.json diff --git a/dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/preferences/fr.json b/dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/modules/preferences/fr.json similarity index 100% rename from dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/preferences/fr.json rename to dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/modules/preferences/fr.json diff --git a/dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/reservation/en.json b/dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/modules/reservation/en.json similarity index 100% rename from dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/reservation/en.json rename to dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/modules/reservation/en.json diff --git a/dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/reservation/fr.json b/dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/modules/reservation/fr.json similarity index 100% rename from dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/reservation/fr.json rename to dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/modules/reservation/fr.json diff --git a/dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/restaurants/en.json b/dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/modules/restaurants/en.json similarity index 100% rename from dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/restaurants/en.json rename to dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/modules/restaurants/en.json diff --git a/dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/restaurants/fr.json b/dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/modules/restaurants/fr.json similarity index 100% rename from dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/restaurants/fr.json rename to dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/modules/restaurants/fr.json diff --git a/dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/rss/en.json b/dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/modules/rss/en.json similarity index 100% rename from dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/rss/en.json rename to dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/modules/rss/en.json diff --git a/dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/rss/fr.json b/dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/modules/rss/fr.json similarity index 100% rename from dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/rss/fr.json rename to dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/modules/rss/fr.json diff --git a/dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/schedule/en.json b/dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/modules/schedule/en.json similarity index 100% rename from dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/schedule/en.json rename to dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/modules/schedule/en.json diff --git a/dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/schedule/fr.json b/dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/modules/schedule/fr.json similarity index 100% rename from dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/schedule/fr.json rename to dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/modules/schedule/fr.json diff --git a/dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/unread-mail/en.json b/dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/modules/unread-mail/en.json similarity index 100% rename from dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/unread-mail/en.json rename to dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/modules/unread-mail/en.json diff --git a/dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/unread-mail/fr.json b/dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/modules/unread-mail/fr.json similarity index 100% rename from dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/unread-mail/fr.json rename to dev/user-frontend-ionic/src/theme/app-theme-dist/i18n/modules/unread-mail/fr.json diff --git a/dev/user-frontend-ionic/src/theme/app-theme-dist/styles/multi-tenant/multi-tenant.component.scss b/dev/user-frontend-ionic/src/theme/app-theme-dist/styles/multi-tenant/multi-tenant.component.scss new file mode 100644 index 00000000..e3c5688f --- /dev/null +++ b/dev/user-frontend-ionic/src/theme/app-theme-dist/styles/multi-tenant/multi-tenant.component.scss @@ -0,0 +1,42 @@ +/*! + * Copyright ou © ou Copr. Université de Lorraine, (2022) + * + * Direction du Numérique de l'Université de Lorraine - SIED + * (dn-mobile-dev@univ-lorraine.fr) + * JNESIS (contact@jnesis.com) + * + * Ce logiciel est un programme informatique servant à rendre accessible + * sur mobile divers services universitaires aux étudiants et aux personnels + * de l'université. + * + * Ce logiciel est régi par la licence CeCILL 2.1, soumise au droit français + * et respectant les principes de diffusion des logiciels libres. Vous pouvez + * utiliser, modifier et/ou redistribuer ce programme sous les conditions + * de la licence CeCILL telle que diffusée par le CEA, le CNRS et INRIA + * sur le site "http://cecill.info". + * + * En contrepartie de l'accessibilité au code source et des droits de copie, + * de modification et de redistribution accordés par cette licence, il n'est + * offert aux utilisateurs qu'une garantie limitée. Pour les mêmes raisons, + * seule une responsabilité restreinte pèse sur l'auteur du programme, le + * titulaire des droits patrimoniaux et les concédants successifs. + * + * À cet égard, l'attention de l'utilisateur est attirée sur les risques + * associés au chargement, à l'utilisation, à la modification et/ou au + * développement et à la reproduction du logiciel par l'utilisateur étant + * donné sa spécificité de logiciel libre, qui peut le rendre complexe à + * manipuler et qui le réserve donc à des développeurs et des professionnels + * avertis possédant des connaissances informatiques approfondies. Les + * utilisateurs sont donc invités à charger et à tester l'adéquation du + * logiciel à leurs besoins dans des conditions permettant d'assurer la + * sécurité de leurs systèmes et/ou de leurs données et, plus généralement, + * à l'utiliser et à l'exploiter dans les mêmes conditions de sécurité. + * + * Le fait que vous puissiez accéder à cet en-tête signifie que vous avez + * pris connaissance de la licence CeCILL 2.1, et que vous en avez accepté les + * termes. + */ + +.app-title-4 { + color: #ffffff !important; +}