Compare commits

...

2 Commits

Author SHA1 Message Date
09de4fd033 feat: outdated version handling 2023-10-05 09:56:25 +02:00
Rainer Killinger
cb196afded feat: communicate outdated app/api client version 2023-10-02 08:48:07 +00:00
46 changed files with 374 additions and 883 deletions

View File

@@ -0,0 +1,5 @@
---
'@openstapps/app': minor
---
Queue config update for next launch to not block app launches

View File

@@ -0,0 +1,5 @@
---
'@openstapps/proxy': minor
---
Send 426 to outdated clients instead of 404

View File

@@ -31,10 +31,11 @@ To Provide your own configuration file you can create a `default.json` file in t
## Status Codes ## Status Codes
- OutdatedVersions return a `HTTP 404` - Successfull reponses come with a `HTTP 200`
- ActiveVersions return a `HTTP 503` if currently unavailable or the given code by running backend-node
- Unsupported versions (not configured as outdated or active) return a `HTTP 404`
- No version header given returns a `HTTP 300` - No version header given returns a `HTTP 300`
- ActiveVersions return a `HTTP 503` if currently unavailable or the given code by running backend-node
- OutdatedVersions return a `HTTP 426`
- Unsupported versions (not configured as outdated or active) return a `HTTP 426`
**NOTE:** The default configuration expects the client to set a version header: `X-StApps-Version=<version of app>` **NOTE:** The default configuration expects the client to set a version header: `X-StApps-Version=<version of app>`

View File

@@ -14,9 +14,8 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
// ESM is not supported, and cts is not detected, so we use type-checked cjs instead.
/** @type {import('../src/common').ConfigFile} */ /** @type {import('../src/common').ConfigFile} */
const configFile = { module.exports = {
activeVersions: ['1\\.0\\.\\d+', '2\\.0\\.\\d+'], activeVersions: ['1\\.0\\.\\d+', '2\\.0\\.\\d+'],
hiddenRoutes: ['/bulk'], hiddenRoutes: ['/bulk'],
logFormat: 'default', logFormat: 'default',
@@ -31,5 +30,3 @@ const configFile = {
dhparam: '/etc/nginx/certs/dhparam.pem', dhparam: '/etc/nginx/certs/dhparam.pem',
}, },
}; };
export default configFile;

View File

@@ -19,14 +19,15 @@ location {{{ route }}} {
return 300 'You have to supply a client/app version via the X-StApps-Version header!'; return 300 'You have to supply a client/app version via the X-StApps-Version header!';
} }
# Version is unsupported or never existed # Version is unsupported by now or never existed (App/Client has to update)
if ($proxyurl = unsupported) { if ($proxyurl = unsupported) {
{{{ cors }}} {{{ cors }}}
return 404; return 426;
} }
# The version existed, but is outdated now (App should update) # The version existed, but is outdated now (App/Client should update)
if ($proxyurl = outdated) { if ($proxyurl = outdated) {
return 404; {{{ cors }}}
return 426;
} }
# The version is correct, but backend is not responding # The version is correct, but backend is not responding
if ($proxyurl = unavailable) { if ($proxyurl = unavailable) {

View File

@@ -34,10 +34,12 @@
"build": "tsup-node --dts", "build": "tsup-node --dts",
"build:docker": "docker build -t openstapps:proxy ../../.deploy/proxy", "build:docker": "docker build -t openstapps:proxy ../../.deploy/proxy",
"deploy": "pnpm --prod --filter=@openstapps/proxy deploy ../../.deploy/proxy", "deploy": "pnpm --prod --filter=@openstapps/proxy deploy ../../.deploy/proxy",
"dev": "tsup --watch --onSuccess \"pnpm run start\"",
"format": "prettier . -c --ignore-path ../../.gitignore", "format": "prettier . -c --ignore-path ../../.gitignore",
"format:fix": "prettier --write . --ignore-path ../../.gitignore", "format:fix": "prettier --write . --ignore-path ../../.gitignore",
"lint": "eslint --ext .ts src/", "lint": "eslint --ext .ts src/",
"lint:fix": "eslint --fix --ext .ts src/", "lint:fix": "eslint --fix --ext .ts src/",
"start": "node app.js",
"test": "c8 mocha" "test": "c8 mocha"
}, },
"dependencies": { "dependencies": {

View File

@@ -37,7 +37,7 @@ const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
* Reads the container information from the docker socket and updates the nginx config if necessary * Reads the container information from the docker socket and updates the nginx config if necessary
*/ */
async function updateNginxConfig() { async function updateNginxConfig() {
const containers = await getContainers(); const containers = await getContainers(process.env.DOCKER_SOCKET);
const containerHash = containers const containerHash = containers
.map((container: ContainerInfo) => { .map((container: ContainerInfo) => {
@@ -78,4 +78,4 @@ async function updateNginxConfig() {
// start the process that checks the docker socket periodically // start the process that checks the docker socket periodically
// eslint-disable-next-line unicorn/prefer-top-level-await // eslint-disable-next-line unicorn/prefer-top-level-await
updateNginxConfig(); await updateNginxConfig();

View File

@@ -1,7 +1,7 @@
version: '3.7' version: '3.7'
services: services:
database: database:
image: registry.gitlab.com/openstapps/openstapps/database:2.0.0 image: registry.gitlab.com/openstapps/openstapps/database:3.0.0-next.4
volumes: volumes:
- ./database:/usr/share/elasticsearch/data - ./database:/usr/share/elasticsearch/data
expose: expose:
@@ -9,7 +9,7 @@ services:
restart: unless-stopped restart: unless-stopped
backend: backend:
image: registry.gitlab.com/openstapps/openstapps/backend:3.0.0-next.0 image: registry.gitlab.com/openstapps/openstapps/backend:3.0.0-next.4
environment: environment:
ES_ADDR: "http://database:9200" ES_ADDR: "http://database:9200"
NODE_CONFIG_ENV: "elasticsearch" NODE_CONFIG_ENV: "elasticsearch"
@@ -27,17 +27,17 @@ services:
- database - database
api: api:
image: registry.gitlab.com/openstapps/openstapps/api:3.0.0-next.0 image: registry.gitlab.com/openstapps/openstapps/api:3.0.0-next.4
links: links:
- "backend" - "backend"
minimal-connector: minimal-connector:
image: registry.gitlab.com/openstapps/minimal-connector:core-0.23 image: registry.gitlab.com/openstapps/openstapps/minimal-connector:3.0.0-next.4
container_name: minimal-connector-0.23 container_name: minimal-connector-0.23
command: ["http://backend:3000", "minimal-connector", "f-u"] command: ["http://backend:3000", "minimal-connector", "f-u"]
app: app:
image: registry.gitlab.com/openstapps/app/executable:core-0.23 image: registry.gitlab.com/openstapps/openstapps/app:3.0.0-next.4
expose: expose:
- 8100 - 8100
ports: ports:

View File

@@ -1,19 +0,0 @@
/*
* Copyright (C) 2020 StApps
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {NGXLogger} from 'ngx-logger';
export let logger: NGXLogger;
export const initLogger = (newLogger: NGXLogger) => (logger = newLogger);

View File

@@ -21,17 +21,13 @@ import {RouteReuseStrategy} from '@angular/router';
import {IonicModule, IonicRouteStrategy, Platform} from '@ionic/angular'; import {IonicModule, IonicRouteStrategy, Platform} from '@ionic/angular';
import {TranslateLoader, TranslateModule, TranslateService} from '@ngx-translate/core'; import {TranslateLoader, TranslateModule, TranslateService} from '@ngx-translate/core';
import {TranslateHttpLoader} from '@ngx-translate/http-loader'; import {TranslateHttpLoader} from '@ngx-translate/http-loader';
import moment from 'moment';
import 'moment/min/locales'; import 'moment/min/locales';
import {LoggerModule, NGXLogger, NgxLoggerLevel} from 'ngx-logger'; import {LoggerModule, NgxLoggerLevel} from 'ngx-logger';
import SwiperCore, {FreeMode, Navigation} from 'swiper'; import SwiperCore, {FreeMode, Navigation} from 'swiper';
import {environment} from '../environments/environment'; import {environment} from '../environments/environment';
import {AppRoutingModule} from './app-routing.module'; import {AppRoutingModule} from './app-routing.module';
import {AppComponent} from './app.component'; import {AppComponent} from './app.component';
import {CatalogModule} from './modules/catalog/catalog.module'; import {CatalogModule} from './modules/catalog/catalog.module';
import {ConfigModule} from './modules/config/config.module';
import {ConfigProvider} from './modules/config/config.provider';
import {DashboardModule} from './modules/dashboard/dashboard.module'; import {DashboardModule} from './modules/dashboard/dashboard.module';
import {DataModule} from './modules/data/data.module'; import {DataModule} from './modules/data/data.module';
import {HebisModule} from './modules/hebis/hebis.module'; import {HebisModule} from './modules/hebis/hebis.module';
@@ -40,11 +36,9 @@ import {MenuModule} from './modules/menu/menu.module';
import {NewsModule} from './modules/news/news.module'; import {NewsModule} from './modules/news/news.module';
import {ScheduleModule} from './modules/schedule/schedule.module'; import {ScheduleModule} from './modules/schedule/schedule.module';
import {SettingsModule} from './modules/settings/settings.module'; import {SettingsModule} from './modules/settings/settings.module';
import {SettingsProvider} from './modules/settings/settings.provider';
import {StorageModule} from './modules/storage/storage.module'; import {StorageModule} from './modules/storage/storage.module';
import {ThingTranslateModule} from './translation/thing-translate.module'; import {ThingTranslateModule} from './translation/thing-translate.module';
import {UtilModule} from './util/util.module'; import {UtilModule} from './util/util.module';
import {initLogger} from './_helpers/ts-logger';
import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
import {AboutModule} from './modules/about/about.module'; import {AboutModule} from './modules/about/about.module';
import {FavoritesModule} from './modules/favorites/favorites.module'; import {FavoritesModule} from './modules/favorites/favorites.module';
@@ -54,80 +48,22 @@ import {DebugDataCollectorService} from './modules/data/debug-data-collector.ser
import {AuthModule} from './modules/auth/auth.module'; import {AuthModule} from './modules/auth/auth.module';
import {BackgroundModule} from './modules/background/background.module'; import {BackgroundModule} from './modules/background/background.module';
import {LibraryModule} from './modules/library/library.module'; import {LibraryModule} from './modules/library/library.module';
import {StorageProvider} from './modules/storage/storage.provider';
import {AssessmentsModule} from './modules/assessments/assessments.module'; import {AssessmentsModule} from './modules/assessments/assessments.module';
import {ServiceHandlerInterceptor} from './_helpers/service-handler.interceptor'; import {ServiceHandlerInterceptor} from './_helpers/service-handler.interceptor';
import {RoutingStackService} from './util/routing-stack.service';
import {SCLanguageCode, SCSettingValue} from '@openstapps/core';
import {DefaultAuthService} from './modules/auth/default-auth.service';
import {PAIAAuthService} from './modules/auth/paia/paia-auth.service';
import {IonIconModule} from './util/ion-icon/ion-icon.module'; import {IonIconModule} from './util/ion-icon/ion-icon.module';
import {NavigationModule} from './modules/menu/navigation/navigation.module'; import {NavigationModule} from './modules/menu/navigation/navigation.module';
import {browserFactory, SimpleBrowser} from './util/browser.factory'; import {browserFactory, SimpleBrowser} from './util/browser.factory';
import {getDateFnsLocale} from './translation/dfns-locale'; import {ConfigProvider} from './modules/config/config.provider';
import {setDefaultOptions} from 'date-fns'; import {SettingsProvider} from './modules/settings/settings.provider';
import {DateFnsConfigurationService} from 'ngx-date-fns'; import {TranslateServiceWrapper} from './translation/translate-service-wrapper';
import {DefaultAuthService} from './modules/auth/default-auth.service';
import {PAIAAuthService} from './modules/auth/paia/paia-auth.service';
import {StorageProvider} from './modules/storage/storage.provider';
registerLocaleData(localeDe); registerLocaleData(localeDe);
SwiperCore.use([FreeMode, Navigation]); SwiperCore.use([FreeMode, Navigation]);
/**
* Initializes data needed on startup
*/
export function initializerFactory(
storageProvider: StorageProvider,
logger: NGXLogger,
settingsProvider: SettingsProvider,
configProvider: ConfigProvider,
translateService: TranslateService,
_routingStackService: RoutingStackService,
defaultAuthService: DefaultAuthService,
paiaAuthService: PAIAAuthService,
dateFnsConfigurationService: DateFnsConfigurationService,
) {
return async () => {
initLogger(logger);
await storageProvider.init();
await configProvider.init();
await settingsProvider.init();
try {
if (configProvider.firstSession) {
// set language from browser
await settingsProvider.setSettingValue(
'profile',
'language',
translateService.getBrowserLang() as SCSettingValue,
);
}
const languageCode = (await settingsProvider.getValue('profile', 'language')) as string;
// this language will be used as a fallback when a translation isn't found in the current language
translateService.setDefaultLang('en');
translateService.use(languageCode);
moment.locale(languageCode);
const dateFnsLocale = await getDateFnsLocale(languageCode as SCLanguageCode);
setDefaultOptions({locale: dateFnsLocale});
dateFnsConfigurationService.setLocale(dateFnsLocale);
await defaultAuthService.init();
await paiaAuthService.init();
} catch (error) {
logger.warn(error);
}
};
}
/**
* TODO
* @param http TODO
*/
export function createTranslateLoader(http: HttpClient) {
return new TranslateHttpLoader(http, './assets/i18n/', '.json');
}
/**
* TODO
*/
@NgModule({ @NgModule({
bootstrap: [AppComponent], bootstrap: [AppComponent],
declarations: [AppComponent], declarations: [AppComponent],
@@ -141,7 +77,6 @@ export function createTranslateLoader(http: HttpClient) {
BrowserAnimationsModule, BrowserAnimationsModule,
CatalogModule, CatalogModule,
CommonModule, CommonModule,
ConfigModule,
DashboardModule, DashboardModule,
DataModule, DataModule,
HebisModule, HebisModule,
@@ -165,7 +100,9 @@ export function createTranslateLoader(http: HttpClient) {
loader: { loader: {
deps: [HttpClient], deps: [HttpClient],
provide: TranslateLoader, provide: TranslateLoader,
useFactory: createTranslateLoader, useFactory(http: HttpClient) {
return new TranslateHttpLoader(http, './assets/i18n/', '.json');
},
}, },
}), }),
UtilModule, UtilModule,
@@ -175,6 +112,30 @@ export function createTranslateLoader(http: HttpClient) {
}), }),
], ],
providers: [ providers: [
{
provide: APP_INITIALIZER,
useFactory:
(...providers: Array<{beforeAppInit(): Promise<void>}>) =>
async () => {
for (const provider of providers) {
await provider.beforeAppInit();
}
},
// Declare initialization (order matters)
deps: [
StorageProvider,
ConfigProvider,
SettingsProvider,
TranslateService,
DefaultAuthService,
PAIAAuthService,
],
multi: true,
},
{
provide: TranslateService,
useClass: TranslateServiceWrapper,
},
{ {
provide: RouteReuseStrategy, provide: RouteReuseStrategy,
useClass: IonicRouteStrategy, useClass: IonicRouteStrategy,
@@ -188,22 +149,6 @@ export function createTranslateLoader(http: HttpClient) {
useFactory: browserFactory, useFactory: browserFactory,
deps: [Platform], deps: [Platform],
}, },
{
provide: APP_INITIALIZER,
multi: true,
deps: [
StorageProvider,
NGXLogger,
SettingsProvider,
ConfigProvider,
TranslateService,
RoutingStackService,
DefaultAuthService,
PAIAAuthService,
DateFnsConfigurationService,
],
useFactory: initializerFactory,
},
{ {
provide: HTTP_INTERCEPTORS, provide: HTTP_INTERCEPTORS,
useClass: ServiceHandlerInterceptor, useClass: ServiceHandlerInterceptor,

View File

@@ -0,0 +1,10 @@
/**
* Services or providers implementing this interface
* must be added to the `APP_INITIALIZER` deps
*/
export interface BeforeAppInit {
/**
* Any logic that has to run before the app is initialized
*/
beforeAppInit(): Promise<void>;
}

View File

@@ -14,10 +14,9 @@
*/ */
import {Component, OnInit} from '@angular/core'; import {Component, OnInit} from '@angular/core';
import {ActivatedRoute} from '@angular/router'; import {ActivatedRoute} from '@angular/router';
import {SCAboutPage, SCAppConfiguration} from '@openstapps/core'; import {SCAboutPage} from '@openstapps/core';
import {ConfigProvider} from '../../config/config.provider';
import packageJson from '../../../../../package.json'; import packageJson from '../../../../../package.json';
import config from 'capacitor.config'; import {ConfigProvider} from '../../config/config.provider';
@Component({ @Component({
selector: 'about-page', selector: 'about-page',
@@ -27,15 +26,12 @@ import config from 'capacitor.config';
export class AboutPageComponent implements OnInit { export class AboutPageComponent implements OnInit {
content: SCAboutPage; content: SCAboutPage;
appName = config.appName;
version = packageJson.version; version = packageJson.version;
constructor(private readonly route: ActivatedRoute, private readonly configProvider: ConfigProvider) {} constructor(readonly route: ActivatedRoute, readonly config: ConfigProvider) {}
async ngOnInit() { ngOnInit() {
const route = this.route.snapshot.url.map(it => it.path).join('/'); const route = this.route.snapshot.url.map(it => it.path).join('/');
this.content = this.content = this.config.app.aboutPages[route] ?? {};
(this.configProvider.getValue('aboutPages') as SCAppConfiguration['aboutPages'])[route] ?? {};
} }
} }

View File

@@ -25,7 +25,7 @@
</ion-toolbar> </ion-toolbar>
</ion-header> </ion-header>
<ion-content parallax *ngIf="content"> <ion-content parallax *ngIf="content">
<ion-text>{{ appName }} v{{ version }}</ion-text> <ion-text>{{ config.app.name }} v{{ version }}</ion-text>
<div class="page-content"> <div class="page-content">
<about-page-content *ngFor="let element of content.content" [content]="element"></about-page-content> <about-page-content *ngFor="let element of content.content" [content]="element"></about-page-content>
</div> </div>

View File

@@ -19,7 +19,6 @@ import {FormsModule} from '@angular/forms';
import {IonicModule} from '@ionic/angular'; import {IonicModule} from '@ionic/angular';
import {TranslateModule} from '@ngx-translate/core'; import {TranslateModule} from '@ngx-translate/core';
import {ThingTranslateModule} from '../../translation/thing-translate.module'; import {ThingTranslateModule} from '../../translation/thing-translate.module';
import {ConfigProvider} from '../config/config.provider';
import {AboutPageComponent} from './about-page/about-page.component'; import {AboutPageComponent} from './about-page/about-page.component';
import {MarkdownModule} from 'ngx-markdown'; import {MarkdownModule} from 'ngx-markdown';
import {AboutPageContentComponent} from './about-page/about-page-content.component'; import {AboutPageContentComponent} from './about-page/about-page-content.component';
@@ -64,6 +63,5 @@ const settingsRoutes: Routes = [
ScrollingModule, ScrollingModule,
UtilModule, UtilModule,
], ],
providers: [ConfigProvider],
}) })
export class AboutModule {} export class AboutModule {}

View File

@@ -13,11 +13,12 @@
* this program. If not, see <https://www.gnu.org/licenses/>. * this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import {Injectable} from '@angular/core'; import {Injectable} from '@angular/core';
import {ConfigProvider} from '../config/config.provider';
import {SCAssessment, SCUuid} from '@openstapps/core'; import {SCAssessment, SCUuid} from '@openstapps/core';
import {DefaultAuthService} from '../auth/default-auth.service'; import {DefaultAuthService} from '../auth/default-auth.service';
import {HttpClient} from '@angular/common/http'; import {HttpClient} from '@angular/common/http';
import {uniqBy, keyBy} from '@openstapps/collection-utils'; import {uniqBy, keyBy} from '@openstapps/collection-utils';
import {firstValueFrom} from 'rxjs';
import {ConfigProvider} from '../config/config.provider';
/** /**
* *
@@ -65,7 +66,7 @@ export class AssessmentsProvider {
cacheMaxAge = 15 * 60 * 1000; cacheMaxAge = 15 * 60 * 1000;
constructor( constructor(
readonly configProvider: ConfigProvider, readonly config: ConfigProvider,
readonly defaultAuth: DefaultAuthService, readonly defaultAuth: DefaultAuthService,
readonly http: HttpClient, readonly http: HttpClient,
) {} ) {}
@@ -91,21 +92,20 @@ export class AssessmentsProvider {
return this.cache; return this.cache;
} }
const url = this.configProvider.config.app.features.extern?.hisometry.url; const url = this.config.app.features.extern?.hisometry.url;
if (!url) throw new Error('Config lacks url for hisometry'); if (!url) throw new Error('Config lacks url for hisometry');
this.cache = this.http this.cache = firstValueFrom(
.get<{data: SCAssessment[]}>(`${url}/${this.assessmentPath}`, { this.http.get<{data: SCAssessment[]}>(`${url}/${this.assessmentPath}`, {
headers: { headers: {
Authorization: `Bearer ${accessToken ?? (await this.defaultAuth.getValidToken()).accessToken}`, Authorization: `Bearer ${accessToken ?? (await this.defaultAuth.getValidToken()).accessToken}`,
}, },
}) }),
.toPromise() ).then(it => {
.then(it => { this.cacheTimestamp = Date.now();
this.cacheTimestamp = Date.now();
return it?.data ?? []; return it?.data ?? [];
}); });
this.assessments = this.cache.then(toAssessmentMap); this.assessments = this.cache.then(toAssessmentMap);
return this.cache; return this.cache;

View File

@@ -12,24 +12,18 @@
* You should have received a copy of the GNU General Public License along with * You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>. * this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import {Injectable} from '@angular/core'; import {Injectable} from '@angular/core';
import {IPAIAAuthAction} from './paia/paia-auth-action'; import {IPAIAAuthAction} from './paia/paia-auth-action';
import {AuthActions, IAuthAction} from 'ionic-appauth'; import {AuthActions, IAuthAction} from 'ionic-appauth';
import {TranslateService} from '@ngx-translate/core'; import {TranslateService} from '@ngx-translate/core';
import {JSONPath} from 'jsonpath-plus'; import {JSONPath} from 'jsonpath-plus';
import { import {SCAuthorizationProviderType, SCUserConfiguration, SCUserConfigurationMap} from '@openstapps/core';
SCAuthorizationProvider,
SCAuthorizationProviderType,
SCUserConfiguration,
SCUserConfigurationMap,
} from '@openstapps/core';
import {ConfigProvider} from '../config/config.provider';
import {StorageProvider} from '../storage/storage.provider'; import {StorageProvider} from '../storage/storage.provider';
import {DefaultAuthService} from './default-auth.service'; import {DefaultAuthService} from './default-auth.service';
import {PAIAAuthService} from './paia/paia-auth.service'; import {PAIAAuthService} from './paia/paia-auth.service';
import {SimpleBrowser} from '../../util/browser.factory'; import {SimpleBrowser} from '../../util/browser.factory';
import {AlertController} from '@ionic/angular'; import {AlertController} from '@ionic/angular';
import {ConfigProvider} from '../config/config.provider';
const AUTH_ORIGIN_PATH = 'stapps.auth.origin_path'; const AUTH_ORIGIN_PATH = 'stapps.auth.origin_path';
@@ -37,23 +31,19 @@ const AUTH_ORIGIN_PATH = 'stapps.auth.origin_path';
providedIn: 'root', providedIn: 'root',
}) })
export class AuthHelperService { export class AuthHelperService {
userConfigurationMap: SCUserConfigurationMap; get userConfigurationMap(): SCUserConfigurationMap {
return this.config.auth.default!.endpoints.mapping;
}
constructor( constructor(
private translateService: TranslateService, private translateService: TranslateService,
private configProvider: ConfigProvider,
private storageProvider: StorageProvider, private storageProvider: StorageProvider,
private defaultAuth: DefaultAuthService, private defaultAuth: DefaultAuthService,
private paiaAuth: PAIAAuthService, private paiaAuth: PAIAAuthService,
private browser: SimpleBrowser, private browser: SimpleBrowser,
private alertController: AlertController, private alertController: AlertController,
) { private config: ConfigProvider,
this.userConfigurationMap = ( ) {}
this.configProvider.getAnyValue('auth') as {
default: SCAuthorizationProvider;
}
).default.endpoints.mapping;
}
public getAuthMessage(provider: SCAuthorizationProviderType, action: IAuthAction | IPAIAAuthAction) { public getAuthMessage(provider: SCAuthorizationProviderType, action: IAuthAction | IPAIAAuthAction) {
let message: string | undefined; let message: string | undefined;

View File

@@ -175,7 +175,7 @@ export abstract class AuthService implements IAuthService {
public async init() { public async init() {
this.setupAuthorizationNotifier(); this.setupAuthorizationNotifier();
this.loadTokenFromStorage(); await this.loadTokenFromStorage();
this.addActionObserver(this._actionHistory); this.addActionObserver(this._actionHistory);
this.addActionObserver(this._session); this.addActionObserver(this._session);
} }

View File

@@ -12,29 +12,26 @@
* You should have received a copy of the GNU General Public License along with * You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>. * this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { import {
AuthorizationRequestHandler, AuthorizationRequestHandler,
AuthorizationServiceConfiguration, AuthorizationServiceConfiguration,
JQueryRequestor, JQueryRequestor,
LocalStorageBackend, LocalStorageBackend,
Requestor,
StorageBackend,
TokenRequestHandler, TokenRequestHandler,
} from '@openid/appauth'; } from '@openid/appauth';
import {AuthActionBuilder, Browser, DefaultBrowser, EndSessionHandler, UserInfoHandler} from 'ionic-appauth'; import {AuthActionBuilder, DefaultBrowser, EndSessionHandler, UserInfoHandler} from 'ionic-appauth';
import {ConfigProvider} from '../config/config.provider';
import {SCAuthorizationProvider} from '@openstapps/core';
import {getClientConfig, getEndpointsConfig} from './auth.provider.methods'; import {getClientConfig, getEndpointsConfig} from './auth.provider.methods';
import {Injectable} from '@angular/core'; import {Injectable} from '@angular/core';
import {AuthService} from './auth.service'; import {AuthService} from './auth.service';
import {ConfigProvider} from '../config/config.provider';
import {BeforeAppInit} from '../../before-app-init';
const TOKEN_RESPONSE_KEY = 'token_response'; const TOKEN_RESPONSE_KEY = 'token_response';
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
}) })
export class DefaultAuthService extends AuthService { export class DefaultAuthService extends AuthService implements BeforeAppInit {
public localConfiguration: AuthorizationServiceConfiguration; public localConfiguration: AuthorizationServiceConfiguration;
protected tokenHandler: TokenRequestHandler; protected tokenHandler: TokenRequestHandler;
@@ -45,13 +42,17 @@ export class DefaultAuthService extends AuthService {
protected endSessionHandler: EndSessionHandler; protected endSessionHandler: EndSessionHandler;
constructor( constructor(private config: ConfigProvider) {
protected browser: Browser = new DefaultBrowser(), super(new DefaultBrowser(), new LocalStorageBackend(), new JQueryRequestor());
protected storage: StorageBackend = new LocalStorageBackend(), }
protected requestor: Requestor = new JQueryRequestor(),
private readonly configProvider: ConfigProvider, async beforeAppInit() {
) { this.authConfig = getClientConfig('default', this.config.auth);
super(browser, storage, requestor); this.localConfiguration = new AuthorizationServiceConfiguration(
getEndpointsConfig('default', this.config.auth),
);
await super.init();
} }
get configuration(): Promise<AuthorizationServiceConfiguration> { get configuration(): Promise<AuthorizationServiceConfiguration> {
@@ -60,22 +61,6 @@ export class DefaultAuthService extends AuthService {
return Promise.resolve(this.localConfiguration); return Promise.resolve(this.localConfiguration);
} }
public async init() {
this.setupConfiguration();
this.setupAuthorizationNotifier();
await this.loadTokenFromStorage();
}
setupConfiguration() {
const authConfig = this.configProvider.getAnyValue('auth') as {
default: SCAuthorizationProvider;
};
this.authConfig = getClientConfig('default', authConfig);
this.localConfiguration = new AuthorizationServiceConfiguration(
getEndpointsConfig('default', authConfig),
);
}
public async signOut() { public async signOut() {
await this.revokeTokens().catch(error => { await this.revokeTokens().catch(error => {
this.notifyActionListers(AuthActionBuilder.SignOutFailed(error)); this.notifyActionListers(AuthActionBuilder.SignOutFailed(error));

View File

@@ -12,7 +12,6 @@
* You should have received a copy of the GNU General Public License along with * You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>. * this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { import {
AuthorizationError, AuthorizationError,
AuthorizationRequest, AuthorizationRequest,
@@ -47,10 +46,10 @@ import {PAIAAuthorizationResponse} from './paia-authorization-response';
import {PAIAAuthorizationNotifier} from './paia-authorization-notifier'; import {PAIAAuthorizationNotifier} from './paia-authorization-notifier';
import {PAIATokenResponse} from './paia-token-response'; import {PAIATokenResponse} from './paia-token-response';
import {IPAIAAuthAction, PAIAAuthActionBuilder} from './paia-auth-action'; import {IPAIAAuthAction, PAIAAuthActionBuilder} from './paia-auth-action';
import {SCAuthorizationProvider} from '@openstapps/core';
import {ConfigProvider} from '../../config/config.provider';
import {getClientConfig, getEndpointsConfig} from '../auth.provider.methods'; import {getClientConfig, getEndpointsConfig} from '../auth.provider.methods';
import {Injectable} from '@angular/core'; import {Injectable} from '@angular/core';
import {ConfigProvider} from '../../config/config.provider';
import {BeforeAppInit} from '../../../before-app-init';
const TOKEN_RESPONSE_KEY = 'paia_token_response'; const TOKEN_RESPONSE_KEY = 'paia_token_response';
const AUTH_EXPIRY_BUFFER = 10 * 60 * -1; // 10 minutes in seconds const AUTH_EXPIRY_BUFFER = 10 * 60 * -1; // 10 minutes in seconds
@@ -64,10 +63,8 @@ export interface IAuthService {
getValidToken(buffer?: number): Promise<PAIATokenResponse>; getValidToken(buffer?: number): Promise<PAIATokenResponse>;
} }
@Injectable({ @Injectable({providedIn: 'root'})
providedIn: 'root', export class PAIAAuthService implements BeforeAppInit {
})
export class PAIAAuthService {
private _authConfig?: IAuthConfig; private _authConfig?: IAuthConfig;
private _authSubject: AuthSubject = new AuthSubject(); private _authSubject: AuthSubject = new AuthSubject();
@@ -97,7 +94,7 @@ export class PAIAAuthService {
protected browser: Browser = new DefaultBrowser(), protected browser: Browser = new DefaultBrowser(),
protected storage: StorageBackend = new LocalStorageBackend(), protected storage: StorageBackend = new LocalStorageBackend(),
protected requestor: Requestor = new JQueryRequestor(), protected requestor: Requestor = new JQueryRequestor(),
private readonly configProvider: ConfigProvider, private config: ConfigProvider,
) { ) {
this.tokenHandler = new PAIATokenRequestHandler(requestor); this.tokenHandler = new PAIATokenRequestHandler(requestor);
this.userInfoHandler = new IonicUserInfoHandler(requestor); this.userInfoHandler = new IonicUserInfoHandler(requestor);
@@ -110,6 +107,16 @@ export class PAIAAuthService {
this.endSessionHandler = new IonicEndSessionHandler(browser); this.endSessionHandler = new IonicEndSessionHandler(browser);
} }
async beforeAppInit() {
this.authConfig = getClientConfig('paia', this.config.auth);
this.localConfiguration = new AuthorizationServiceConfiguration(
getEndpointsConfig('paia', this.config.auth),
);
this.setupAuthorizationNotifier();
await this.loadTokenFromStorage();
}
get token$(): Observable<PAIATokenResponse | undefined> { get token$(): Observable<PAIATokenResponse | undefined> {
return this._tokenSubject.asObservable(); return this._tokenSubject.asObservable();
} }
@@ -147,20 +154,6 @@ export class PAIAAuthService {
return Promise.resolve(this.localConfiguration); return Promise.resolve(this.localConfiguration);
} }
public async init() {
this.setupConfiguration();
this.setupAuthorizationNotifier();
await this.loadTokenFromStorage();
}
setupConfiguration() {
const authConfig = this.configProvider.getAnyValue('auth') as {
paia: SCAuthorizationProvider;
};
this.authConfig = getClientConfig('paia', authConfig);
this.localConfiguration = new AuthorizationServiceConfiguration(getEndpointsConfig('paia', authConfig));
}
protected notifyActionListers(action: IPAIAAuthAction) { protected notifyActionListers(action: IPAIAAuthAction) {
/* eslint-disable unicorn/no-useless-undefined */ /* eslint-disable unicorn/no-useless-undefined */
switch (action.action) { switch (action.action) {

View File

@@ -12,7 +12,6 @@
* You should have received a copy of the GNU General Public License along with * You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>. * this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import {Calendar} from '@awesome-cordova-plugins/calendar/ngx'; import {Calendar} from '@awesome-cordova-plugins/calendar/ngx';
import {Injectable} from '@angular/core'; import {Injectable} from '@angular/core';
import {ICalEvent} from './ical/ical'; import {ICalEvent} from './ical/ical';
@@ -29,18 +28,18 @@ const RECURRENCE_PATTERNS: Partial<Record<unitOfTime.Diff, string | undefined>>
day: 'daily', day: 'daily',
}; };
@Injectable() @Injectable({providedIn: 'root'})
export class CalendarService { export class CalendarService {
goToDate = new Subject<number>(); goToDate = new Subject<number>();
goToDateClicked = this.goToDate.asObservable(); goToDateClicked = this.goToDate.asObservable();
calendarName = 'StApps'; get calendarName(): string {
return this.config.app.name ?? 'StApps';
}
// eslint-disable-next-line @typescript-eslint/no-empty-function // eslint-disable-next-line @typescript-eslint/no-empty-function
constructor(readonly calendar: Calendar, private readonly configProvider: ConfigProvider) { constructor(readonly calendar: Calendar, readonly config: ConfigProvider) {}
this.calendarName = (this.configProvider.getValue('name') as string) ?? 'StApps';
}
async createCalendar(): Promise<CalendarInfo | undefined> { async createCalendar(): Promise<CalendarInfo | undefined> {
await this.calendar.createCalendar({ await this.calendar.createCalendar({

View File

@@ -1,27 +0,0 @@
/*
* Copyright (C) 2019 StApps
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {NgModule} from '@angular/core';
import {DataModule} from '../data/data.module';
import {StorageModule} from '../storage/storage.module';
import {ConfigProvider} from './config.provider';
/**
* TODO
*/
@NgModule({
imports: [StorageModule, DataModule],
providers: [ConfigProvider],
})
export class ConfigModule {}

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/ban-types */
/* /*
* Copyright (C) 2022 StApps * Copyright (C) 2022 StApps
* This program is free software: you can redistribute it and/or modify it * This program is free software: you can redistribute it and/or modify it
@@ -14,19 +15,17 @@
*/ */
import {Injectable} from '@angular/core'; import {Injectable} from '@angular/core';
import {Client} from '@openstapps/api'; import {Client} from '@openstapps/api';
import {SCAppConfiguration, SCIndexResponse} from '@openstapps/core';
import packageInfo from '@openstapps/core/package.json';
import {NGXLogger} from 'ngx-logger';
import {environment} from '../../../environments/environment';
import {StAppsWebHttpClient} from '../data/stapps-web-http-client.provider';
import {StorageProvider} from '../storage/storage.provider';
import { import {
ConfigFetchError, SCAppConfiguration,
ConfigInitError, SCAuthorizationProvider,
ConfigValueNotAvailable, SCBackendConfiguration,
SavedConfigNotAvailable, SCIndexResponse,
WrongConfigVersionInStorage, } from '@openstapps/core';
} from './errors'; import coreInfo from '@openstapps/core/package.json';
import {environment} from '../../../environments/environment';
import {StorageProvider} from '../storage/storage.provider';
import {BeforeAppInit} from '../../before-app-init';
import {StAppsWebHttpClient} from '../data/stapps-web-http-client.provider';
/** /**
* Key to store config in storage module * Key to store config in storage module
@@ -35,145 +34,55 @@ import {
*/ */
export const STORAGE_KEY_CONFIG = 'stapps.config'; export const STORAGE_KEY_CONFIG = 'stapps.config';
/**
* Provides configuration
*/
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
}) })
export class ConfigProvider { export class ConfigProvider implements SCIndexResponse, BeforeAppInit {
/** private client: Client;
* Api client
*/
client: Client;
/** constructor(private storageProvider: StorageProvider, httpClient: StAppsWebHttpClient) {
* App configuration as IndexResponse this.client = new Client(httpClient, environment.backend_url, environment.backend_version);
*/ }
config: SCIndexResponse;
/** async beforeAppInit() {
* Version of the @openstapps/core package that app is using this.isFirstSession = !(await this.storageProvider.has(STORAGE_KEY_CONFIG));
*/ // Queue config update for next launch; don't block current launch
scVersion = packageInfo.version; const configUpdate = this.updateConfig();
console.log('Config update queued');
/** const config = await this.storageProvider
* First session indicator (config not found in storage) .get<SCIndexResponse>(STORAGE_KEY_CONFIG)
*/ .then(it => it ?? configUpdate);
firstSession = true;
/** Object.assign(this, config);
* Constructor, initialise api client
* @param storageProvider StorageProvider to load persistent configuration console.assert(
* @param swHttpClient Api client this.backend.SCVersion === coreInfo.version,
* @param logger An angular logger 'Wrong config version in storage.',
*/ 'Expected:',
constructor( coreInfo.version,
private readonly storageProvider: StorageProvider, 'Actual:',
swHttpClient: StAppsWebHttpClient, this.backend.SCVersion,
private readonly logger: NGXLogger, );
) {
this.client = new Client(swHttpClient, environment.backend_url, environment.backend_version);
} }
/** /**
* Fetches configuration from backend * Updates the config from remote
*/ */
async fetch(): Promise<SCIndexResponse> { async updateConfig(): Promise<SCIndexResponse> {
try { const config = await this.client.handshake(coreInfo.version);
return await this.client.handshake(this.scVersion);
} catch {
throw new ConfigFetchError();
}
}
/**
* Returns the value of an app configuration
* @param attribute requested attribute from app configuration
*/
public getValue(attribute: keyof SCAppConfiguration) {
if (this.config.app[attribute] !== undefined) {
return this.config.app[attribute];
}
throw new ConfigValueNotAvailable(attribute);
}
/**
* Returns a value of the configuration (not only app configuration)
* @param attribute requested attribute from the configuration
*/
public getAnyValue(attribute: keyof SCIndexResponse) {
if (this.config[attribute] !== undefined) {
return this.config[attribute];
}
throw new ConfigValueNotAvailable(attribute);
}
/**
* Initialises the ConfigProvider
* @throws ConfigInitError if no configuration could be loaded.
* @throws WrongConfigVersionInStorage if fetch failed and saved config has wrong SCVersion
*/
async init(): Promise<void> {
let loadError;
let fetchError;
// load saved configuration
try {
this.config = await this.loadLocal();
this.firstSession = false;
this.logger.log(`initialised configuration from storage`);
if (this.config.backend.SCVersion !== this.scVersion) {
loadError = new WrongConfigVersionInStorage(this.scVersion, this.config.backend.SCVersion);
}
} catch (error) {
loadError = error;
}
// fetch remote configuration from backend
try {
const fetchedConfig: SCIndexResponse = await this.fetch();
await this.set(fetchedConfig);
this.logger.log(`initialised configuration from remote`);
} catch (error) {
fetchError = error;
}
// check for occurred errors and throw them
if (loadError !== undefined && fetchError !== undefined) {
throw new ConfigInitError();
}
if (loadError !== undefined) {
this.logger.warn(loadError);
}
if (fetchError !== undefined) {
this.logger.warn(fetchError);
}
}
/**
* Returns saved configuration from StorageModule
* @throws SavedConfigNotAvailable if no configuration could be loaded
*/
async loadLocal(): Promise<SCIndexResponse> {
// get local configuration
if (await this.storageProvider.has(STORAGE_KEY_CONFIG)) {
return this.storageProvider.get<SCIndexResponse>(STORAGE_KEY_CONFIG);
}
throw new SavedConfigNotAvailable();
}
/**
* Saves the configuration from the provider
* @param config configuration to save
*/
async save(config: SCIndexResponse): Promise<void> {
await this.storageProvider.put(STORAGE_KEY_CONFIG, config); await this.storageProvider.put(STORAGE_KEY_CONFIG, config);
console.log(`Config updated`);
return config;
} }
/** app: SCAppConfiguration;
* Sets the configuration in the module and writes it into app storage
* @param config SCIndexResponse to set auth: {default?: SCAuthorizationProvider | undefined; paia?: SCAuthorizationProvider | undefined};
*/
async set(config: SCIndexResponse): Promise<void> { backend: SCBackendConfiguration;
this.config = config;
await this.save(this.config); isFirstSession: boolean;
}
} }

View File

@@ -1,65 +0,0 @@
/*
* Copyright (C) 2022 StApps
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {AppError} from '../../_helpers/errors';
/**
* Error that is thrown when fetching from backend fails
*/
export class ConfigFetchError extends AppError {
constructor() {
super('ConfigFetchError', 'App configuration could not be fetched!');
}
}
/**
* Error that is thrown when the ConfigProvider could be initialised
*/
export class ConfigInitError extends AppError {
constructor() {
super('ConfigInitError', 'App configuration could not be initialised!');
}
}
/**
* Error that is thrown when the requested config value is not available
*/
export class ConfigValueNotAvailable extends AppError {
constructor(valueKey: string) {
super('ConfigValueNotAvailable', `No attribute "${valueKey}" in config available!`);
}
}
/**
* Error that is thrown when no saved config is available
*/
export class SavedConfigNotAvailable extends AppError {
constructor() {
super('SavedConfigNotAvailable', 'No saved app configuration available.');
}
}
/**
* Error that is thrown when the SCVersion of the saved config is not compatible with the app
*/
export class WrongConfigVersionInStorage extends AppError {
constructor(correctVersion: string, savedVersion: string) {
super(
'WrongConfigVersionInStorage',
`The saved configs backend version ${savedVersion} ` +
`does not equal the configured backend version ${correctVersion} of the app.`,
);
}
}

View File

@@ -30,9 +30,8 @@ export class OffersInListComponent {
@Input() set offers(it: Array<SCThingThatCanBeOfferedOffer<SCAcademicPriceGroup>>) { @Input() set offers(it: Array<SCThingThatCanBeOfferedOffer<SCAcademicPriceGroup>>) {
this._offers = it; this._offers = it;
this.price = it[0].prices?.default; this.price = it[0].prices?.default;
this.settingsProvider.getSetting('profile', 'group').then(group => { const group = this.settingsProvider.getSetting('profile', 'group');
this.price = it[0].prices?.[(group.value as string).replace(/s$/, '') as never]; this.price = it[0].prices?.[(group.value as string).replace(/s$/, '') as never];
});
const availabilities = new Set(it.map(offer => offer.availability)); const availabilities = new Set(it.map(offer => offer.availability));
this.soldOut = availabilities.has('out of stock') && availabilities.size === 1; this.soldOut = availabilities.has('out of stock') && availabilities.size === 1;

View File

@@ -15,16 +15,9 @@
import {Component, DestroyRef, inject, Input, OnInit} from '@angular/core'; import {Component, DestroyRef, inject, Input, OnInit} from '@angular/core';
import {ActivatedRoute, Router} from '@angular/router'; import {ActivatedRoute, Router} from '@angular/router';
import {Keyboard} from '@capacitor/keyboard'; import {Keyboard} from '@capacitor/keyboard';
import {AlertController, AnimationBuilder, AnimationController} from '@ionic/angular'; import {AlertController, AnimationController} from '@ionic/angular';
import {Capacitor} from '@capacitor/core'; import {Capacitor} from '@capacitor/core';
import { import {SCFacet, SCSearchFilter, SCSearchQuery, SCSearchSort, SCThings} from '@openstapps/core';
SCFacet,
SCFeatureConfiguration,
SCSearchFilter,
SCSearchQuery,
SCSearchSort,
SCThings,
} from '@openstapps/core';
import {NGXLogger} from 'ngx-logger'; import {NGXLogger} from 'ngx-logger';
import {combineLatest, Subject} from 'rxjs'; import {combineLatest, Subject} from 'rxjs';
import {debounceTime, distinctUntilChanged, startWith} from 'rxjs/operators'; import {debounceTime, distinctUntilChanged, startWith} from 'rxjs/operators';
@@ -33,9 +26,9 @@ import {SettingsProvider} from '../../settings/settings.provider';
import {DataRoutingService} from '../data-routing.service'; import {DataRoutingService} from '../data-routing.service';
import {DataProvider} from '../data.provider'; import {DataProvider} from '../data.provider';
import {PositionService} from '../../map/position.service'; import {PositionService} from '../../map/position.service';
import {ConfigProvider} from '../../config/config.provider';
import {searchPageSwitchAnimation} from './search-page-switch-animation'; import {searchPageSwitchAnimation} from './search-page-switch-animation';
import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
import {ConfigProvider} from '../../config/config.provider';
/** /**
* SearchPageComponent queries things and shows list of things as search results and filter as context menu * SearchPageComponent queries things and shows list of things as search results and filter as context menu
@@ -144,21 +137,8 @@ export class SearchPageComponent implements OnInit {
destroy$ = inject(DestroyRef); destroy$ = inject(DestroyRef);
routeAnimation: AnimationBuilder; routeAnimation = searchPageSwitchAnimation(inject(AnimationController));
/**
* Injects the providers and creates subscriptions
* @param alertController AlertController
* @param dataProvider DataProvider
* @param contextMenuService ContextMenuService
* @param settingsProvider SettingsProvider
* @param logger An angular logger
* @param dataRoutingService DataRoutingService
* @param router Router
* @param route ActivatedRoute
* @param positionService PositionService
* @param configProvider ConfigProvider
*/
constructor( constructor(
protected readonly alertController: AlertController, protected readonly alertController: AlertController,
protected dataProvider: DataProvider, protected dataProvider: DataProvider,
@@ -169,11 +149,8 @@ export class SearchPageComponent implements OnInit {
protected router: Router, protected router: Router,
private readonly route: ActivatedRoute, private readonly route: ActivatedRoute,
protected positionService: PositionService, protected positionService: PositionService,
private readonly configProvider: ConfigProvider, private readonly config: ConfigProvider,
animationController: AnimationController, ) {}
) {
this.routeAnimation = searchPageSwitchAnimation(animationController);
}
/** /**
* Fetches items with set query configuration * Fetches items with set query configuration
@@ -347,8 +324,7 @@ export class SearchPageComponent implements OnInit {
}); });
} }
try { try {
const features = this.configProvider.getValue('features') as SCFeatureConfiguration; this.isHebisAvailable = !!this.config.app.features.plugins?.['hebis-plugin']?.urlPath;
this.isHebisAvailable = !!features.plugins?.['hebis-plugin']?.urlPath;
} catch (error) { } catch (error) {
this.logger.error(error); this.logger.error(error);
} }

View File

@@ -62,10 +62,8 @@ export class RatingProvider {
return new Date(today.getFullYear(), today.getMonth(), today.getDate()).toISOString(); return new Date(today.getFullYear(), today.getMonth(), today.getDate()).toISOString();
} }
private get userGroup(): Promise<SCUserGroup> { private get userGroup(): SCUserGroup {
return this.settingsProvider return (this.settingsProvider.getSetting('profile', 'group') as SCUserGroupSetting).value as SCUserGroup;
.getSetting('profile', 'group')
.then(it => (it as SCUserGroupSetting).value as SCUserGroup);
} }
private async getStoredRatings(): Promise<RatingStorage> { private async getStoredRatings(): Promise<RatingStorage> {

View File

@@ -12,21 +12,13 @@
* You should have received a copy of the GNU General Public License along with * You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>. * this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import {Component, OnInit} from '@angular/core'; import {Component, inject, OnInit} from '@angular/core';
import {AlertController, AnimationController} from '@ionic/angular';
import {ActivatedRoute, Router} from '@angular/router';
import {NGXLogger} from 'ngx-logger';
import {debounceTime, distinctUntilChanged, startWith, take} from 'rxjs/operators'; import {debounceTime, distinctUntilChanged, startWith, take} from 'rxjs/operators';
import {combineLatest} from 'rxjs'; import {combineLatest} from 'rxjs';
import {SCThingType} from '@openstapps/core'; import {SCThingType} from '@openstapps/core';
import {FavoritesService} from './favorites.service'; import {FavoritesService} from './favorites.service';
import {DataRoutingService} from '../data/data-routing.service';
import {ContextMenuService} from '../menu/context/context-menu.service'; import {ContextMenuService} from '../menu/context/context-menu.service';
import {SearchPageComponent} from '../data/list/search-page.component'; import {SearchPageComponent} from '../data/list/search-page.component';
import {DataProvider} from '../data/data.provider';
import {SettingsProvider} from '../settings/settings.provider';
import {PositionService} from '../map/position.service';
import {ConfigProvider} from '../config/config.provider';
import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
/** /**
@@ -42,34 +34,7 @@ export class FavoritesPageComponent extends SearchPageComponent implements OnIni
showNavigation = false; showNavigation = false;
constructor( favoritesService = inject(FavoritesService);
alertController: AlertController,
dataProvider: DataProvider,
contextMenuService: ContextMenuService,
settingsProvider: SettingsProvider,
logger: NGXLogger,
dataRoutingService: DataRoutingService,
router: Router,
route: ActivatedRoute,
positionService: PositionService,
private favoritesService: FavoritesService,
configProvider: ConfigProvider,
animationController: AnimationController,
) {
super(
alertController,
dataProvider,
contextMenuService,
settingsProvider,
logger,
dataRoutingService,
router,
route,
positionService,
configProvider,
animationController,
);
}
ngOnInit() { ngOnInit() {
super.ngOnInit(false); super.ngOnInit(false);

View File

@@ -16,10 +16,9 @@ import {Injectable} from '@angular/core';
import {DaiaAvailabilityResponse, DaiaHolding, DaiaService} from './protocol/response'; import {DaiaAvailabilityResponse, DaiaHolding, DaiaService} from './protocol/response';
import {StorageProvider} from '../storage/storage.provider'; import {StorageProvider} from '../storage/storage.provider';
import {HttpClient, HttpHeaders} from '@angular/common/http'; import {HttpClient, HttpHeaders} from '@angular/common/http';
import {ConfigProvider} from '../config/config.provider';
import {SCFeatureConfiguration} from '@openstapps/core';
import {NGXLogger} from 'ngx-logger'; import {NGXLogger} from 'ngx-logger';
import {TranslateService} from '@ngx-translate/core'; import {TranslateService} from '@ngx-translate/core';
import {ConfigProvider} from '../config/config.provider';
/** /**
* Generated class for the DataProvider provider. * Generated class for the DataProvider provider.
@@ -44,18 +43,10 @@ export class DaiaDataProvider {
clientHeaders = new HttpHeaders(); clientHeaders = new HttpHeaders();
/**
* TODO
* @param storageProvider TODO
* @param httpClient TODO
* @param configProvider TODO
* @param logger TODO
* @param translateService TODO
*/
constructor( constructor(
storageProvider: StorageProvider, storageProvider: StorageProvider,
httpClient: HttpClient, httpClient: HttpClient,
private configProvider: ConfigProvider, private config: ConfigProvider,
private readonly logger: NGXLogger, private readonly logger: NGXLogger,
private translateService: TranslateService, private translateService: TranslateService,
) { ) {
@@ -67,15 +58,14 @@ export class DaiaDataProvider {
async getAvailability(id: string): Promise<DaiaHolding[] | undefined> { async getAvailability(id: string): Promise<DaiaHolding[] | undefined> {
if (this.daiaServiceUrl === undefined) { if (this.daiaServiceUrl === undefined) {
try { try {
const features = this.configProvider.getValue('features') as SCFeatureConfiguration; if (this.config.app.features.extern?.daia?.url) {
if (features.extern?.daia?.url) { this.daiaServiceUrl = this.config.app.features.extern?.daia?.url;
this.daiaServiceUrl = features.extern?.daia?.url;
} else { } else {
this.logger.error('Daia service url undefined'); this.logger.error('Daia service url undefined');
return undefined; return undefined;
} }
if (features.extern?.hebisProxy?.url) { if (this.config.app.features.extern?.hebisProxy?.url) {
this.hebisProxyUrl = features.extern?.hebisProxy?.url; this.hebisProxyUrl = this.config.app.features.extern?.hebisProxy?.url;
} else { } else {
this.logger.error('HeBIS proxy url undefined'); this.logger.error('HeBIS proxy url undefined');
return undefined; return undefined;

View File

@@ -12,22 +12,17 @@
* You should have received a copy of the GNU General Public License along with * You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>. * this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import {Injectable} from '@angular/core'; import {Injectable} from '@angular/core';
import {JQueryRequestor, Requestor} from '@openid/appauth'; import {JQueryRequestor, Requestor} from '@openid/appauth';
import { import {SCAuthorizationProviderType} from '@openstapps/core';
SCAuthorizationProviderType,
SCFeatureConfiguration,
SCFeatureConfigurationExtern,
} from '@openstapps/core';
import {DocumentAction, PAIADocument, PAIADocumentStatus, PAIAFees, PAIAItems, PAIAPatron} from '../types'; import {DocumentAction, PAIADocument, PAIADocumentStatus, PAIAFees, PAIAItems, PAIAPatron} from '../types';
import {HebisDataProvider} from '../../hebis/hebis-data.provider'; import {HebisDataProvider} from '../../hebis/hebis-data.provider';
import {PAIATokenResponse} from '../../auth/paia/paia-token-response'; import {PAIATokenResponse} from '../../auth/paia/paia-token-response';
import {AuthHelperService} from '../../auth/auth-helper.service'; import {AuthHelperService} from '../../auth/auth-helper.service';
import {ConfigProvider} from '../../config/config.provider';
import {TranslateService} from '@ngx-translate/core'; import {TranslateService} from '@ngx-translate/core';
import {AlertController, ToastController} from '@ionic/angular'; import {AlertController, ToastController} from '@ionic/angular';
import {HebisSearchResponse} from '../../hebis/protocol/response'; import {HebisSearchResponse} from '../../hebis/protocol/response';
import {ConfigProvider} from '../../config/config.provider';
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
@@ -36,29 +31,26 @@ export class LibraryAccountService {
/** /**
* Base url of the external service * Base url of the external service
*/ */
baseUrl: string; get baseUrl(): string {
return this.config.app.features.extern!['paia'].url;
}
/** /**
* Authorization provider type * Authorization provider type
*/ */
authType: SCAuthorizationProviderType; get authType(): SCAuthorizationProviderType {
return this.config.app.features.extern!['paia'].authProvider!;
}
constructor( constructor(
protected requestor: Requestor = new JQueryRequestor(), protected requestor: Requestor = new JQueryRequestor(),
private readonly hebisDataProvider: HebisDataProvider, private readonly hebisDataProvider: HebisDataProvider,
private readonly authHelper: AuthHelperService, private readonly authHelper: AuthHelperService,
readonly configProvider: ConfigProvider,
private readonly translateService: TranslateService, private readonly translateService: TranslateService,
private readonly alertController: AlertController, private readonly alertController: AlertController,
private readonly toastController: ToastController, private readonly toastController: ToastController,
) { private readonly config: ConfigProvider,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion ) {}
const config: SCFeatureConfigurationExtern = (
configProvider.getValue('features') as SCFeatureConfiguration
).extern!.paia;
this.baseUrl = config.url;
this.authType = config.authProvider as SCAuthorizationProviderType;
}
async getProfile() { async getProfile() {
const patron = ((await this.getValidToken()) as PAIATokenResponse).patron; const patron = ((await this.getValidToken()) as PAIATokenResponse).patron;

View File

@@ -19,9 +19,7 @@ import {LeafletModule} from '@asymmetrik/ngx-leaflet';
import {LeafletMarkerClusterModule} from '@asymmetrik/ngx-leaflet-markercluster'; import {LeafletMarkerClusterModule} from '@asymmetrik/ngx-leaflet-markercluster';
import {IonicModule} from '@ionic/angular'; import {IonicModule} from '@ionic/angular';
import {TranslateModule} from '@ngx-translate/core'; import {TranslateModule} from '@ngx-translate/core';
import {Polygon} from 'geojson';
import {ThingTranslateModule} from '../../translation/thing-translate.module'; import {ThingTranslateModule} from '../../translation/thing-translate.module';
import {ConfigProvider} from '../config/config.provider';
import {DataFacetsProvider} from '../data/data-facets.provider'; import {DataFacetsProvider} from '../data/data-facets.provider';
import {DataModule} from '../data/data.module'; import {DataModule} from '../data/data.module';
import {DataProvider} from '../data/data.provider'; import {DataProvider} from '../data/data.provider';
@@ -35,17 +33,6 @@ import {UtilModule} from '../../util/util.module';
import {IonIconModule} from '../../util/ion-icon/ion-icon.module'; import {IonIconModule} from '../../util/ion-icon/ion-icon.module';
import {GeoNavigationDirective} from './geo-navigation.directive'; import {GeoNavigationDirective} from './geo-navigation.directive';
/**
* Initializes the default area to show in advance (before components are initialized)
* @param configProvider An instance of the ConfigProvider to read the campus polygon from
* @param mapProvider An instance of the MapProvider to set the default polygon (area to show on the map)
*/
export function initMapConfigFactory(configProvider: ConfigProvider, mapProvider: MapProvider) {
return async () => {
mapProvider.defaultPolygon = (await configProvider.getValue('campusPolygon')) as Polygon;
};
}
const mapRoutes: Routes = [ const mapRoutes: Routes = [
{path: 'map', component: MapPageComponent}, {path: 'map', component: MapPageComponent},
{path: 'map/:uid', component: MapPageComponent}, {path: 'map/:uid', component: MapPageComponent},

View File

@@ -21,12 +21,11 @@ import {
SCThingType, SCThingType,
SCUuid, SCUuid,
} from '@openstapps/core'; } from '@openstapps/core';
import {Point, Polygon} from 'geojson'; import {Point} from 'geojson';
import {divIcon, geoJSON, LatLng, Map, marker, Marker} from 'leaflet'; import {divIcon, geoJSON, LatLng, Map, marker, Marker} from 'leaflet';
import {DataProvider} from '../data/data.provider'; import {DataProvider} from '../data/data.provider';
import {MapPosition, PositionService} from './position.service'; import {MapPosition, PositionService} from './position.service';
import {hasValidLocation} from '../data/types/place/place-types'; import {hasValidLocation} from '../data/types/place/place-types';
import {ConfigProvider} from '../config/config.provider';
import {SCIcon} from '../../util/ion-icon/icon'; import {SCIcon} from '../../util/ion-icon/icon';
/** /**
@@ -36,11 +35,6 @@ import {SCIcon} from '../../util/ion-icon/icon';
providedIn: 'root', providedIn: 'root',
}) })
export class MapProvider { export class MapProvider {
/**
* Area to show when the map is initialized (shown for the first time)
*/
defaultPolygon: Polygon;
/** /**
* Provide a point marker for a leaflet map * Provide a point marker for a leaflet map
* @param point Point to get marker for * @param point Point to get marker for
@@ -111,13 +105,7 @@ export class MapProvider {
clearInterval(interval); clearInterval(interval);
}; };
constructor( constructor(private dataProvider: DataProvider, private positionService: PositionService) {}
private dataProvider: DataProvider,
private positionService: PositionService,
private configProvider: ConfigProvider,
) {
this.defaultPolygon = this.configProvider.getValue('campusPolygon') as Polygon;
}
/** /**
* Provide the specific place by its UID * Provide the specific place by its UID

View File

@@ -30,6 +30,7 @@ import {Capacitor} from '@capacitor/core';
import {pauseWhen} from '../../../util/rxjs/pause-when'; import {pauseWhen} from '../../../util/rxjs/pause-when';
import {takeUntilDestroyed} from '@angular/core/rxjs-interop'; import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
import {startViewTransition} from '../../../util/view-transition'; import {startViewTransition} from '../../../util/view-transition';
import {ConfigProvider} from '../../config/config.provider';
/** /**
* The main page of the map * The main page of the map
@@ -102,7 +103,7 @@ export class MapPageComponent implements OnInit {
* Options of the leaflet map * Options of the leaflet map
*/ */
options: MapOptions = { options: MapOptions = {
center: geoJSON(this.mapProvider.defaultPolygon).getBounds().getCenter(), center: geoJSON(inject(ConfigProvider).app.campusPolygon).getBounds().getCenter(),
layers: [ layers: [
tileLayer('https://osm.server.uni-frankfurt.de/tiles/roads/x={x}&y={y}&z={z}', { tileLayer('https://osm.server.uni-frankfurt.de/tiles/roads/x={x}&y={y}&z={z}', {
attribution: '&copy; <a href="http://osm.org/copyright">OpenStreetMap</a> contributors', attribution: '&copy; <a href="http://osm.org/copyright">OpenStreetMap</a> contributors',

View File

@@ -12,75 +12,23 @@
* You should have received a copy of the GNU General Public License along with * You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>. * this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import {Component, OnInit} from '@angular/core'; import {Component, inject} from '@angular/core';
import {LangChangeEvent, TranslateService} from '@ngx-translate/core';
import {
SCAppConfigurationMenuCategory,
SCLanguage,
SCThingTranslator,
SCTranslations,
} from '@openstapps/core';
import {NavigationService} from './navigation.service';
import config from 'capacitor.config';
import {SettingsProvider} from '../../settings/settings.provider';
import {BreakpointObserver} from '@angular/cdk/layout'; import {BreakpointObserver} from '@angular/cdk/layout';
import {map} from 'rxjs/operators';
import {ConfigProvider} from '../../config/config.provider';
/**
* Generated class for the MenuPage page.
*
* See https://ionicframework.com/docs/components/#navigation for more info on
* Ionic pages and navigation.
*/
@Component({ @Component({
selector: 'stapps-navigation', selector: 'stapps-navigation',
styleUrls: ['navigation.scss'], styleUrls: ['navigation.scss'],
templateUrl: 'navigation.html', templateUrl: 'navigation.html',
}) })
export class NavigationComponent implements OnInit { export class NavigationComponent {
showTabbar = true;
/** /**
* Name of the app * TODO: What was this for???
*/ */
appName = config.appName; showTabBar$ = inject(BreakpointObserver)
.observe(['(min-width: 768px)'])
.pipe(map(({matches}) => !matches));
/** constructor(readonly config: ConfigProvider) {}
* Possible languages to be used for translation
*/
language: keyof SCTranslations<SCLanguage>;
/**
* Menu entries from config module
*/
menu: SCAppConfigurationMenuCategory[];
/**
* Core translator
*/
translator: SCThingTranslator;
constructor(
public translateService: TranslateService,
private navigationService: NavigationService,
private settingsProvider: SettingsProvider,
private responsive: BreakpointObserver,
) {
translateService.onLangChange.subscribe((event: LangChangeEvent) => {
this.language = event.lang as keyof SCTranslations<SCLanguage>;
this.translator = new SCThingTranslator(this.language);
});
this.responsive.observe(['(min-width: 768px)']).subscribe(result => {
this.showTabbar = !result.matches;
});
}
async ngOnInit() {
this.language = (await this.settingsProvider.getValue(
'profile',
'language',
)) as keyof SCTranslations<SCLanguage>;
this.translator = new SCThingTranslator(this.language);
this.menu = await this.navigationService.getMenu();
}
} }

View File

@@ -24,8 +24,8 @@
</ion-title> </ion-title>
</ion-toolbar> </ion-toolbar>
</ion-header> </ion-header>
<ion-content *ngIf="menu"> <ion-content *ngIf="config.app.menus">
<ion-list *ngFor="let category of menu; first as isFirst"> <ion-list *ngFor="let category of config.app.menus; first as isFirst">
<ion-item <ion-item
*ngIf="category.title !== ''" *ngIf="category.title !== ''"
[rootLink]="category.route" [rootLink]="category.route"
@@ -34,11 +34,11 @@
class="menu-category" class="menu-category"
> >
<ion-icon slot="end" [name]="category.icon"></ion-icon> <ion-icon slot="end" [name]="category.icon"></ion-icon>
<ion-label> {{ category.translations[language].title | titlecase }} </ion-label> <ion-label> {{ 'title' | translateSimple: category | titlecase }} </ion-label>
</ion-item> </ion-item>
<ion-item *ngFor="let item of category.items" [rootLink]="item.route" [redirectedFrom]="item.route"> <ion-item *ngFor="let item of category.items" [rootLink]="item.route" [redirectedFrom]="item.route">
<ion-icon slot="end" [name]="item.icon"></ion-icon> <ion-icon slot="end" [name]="item.icon"></ion-icon>
<ion-label> {{ item.translations[language].title | titlecase }} </ion-label> <ion-label> {{ 'title' | translateSimple: item | titlecase }} </ion-label>
</ion-item> </ion-item>
</ion-list> </ion-list>
</ion-content> </ion-content>

View File

@@ -22,10 +22,11 @@ import {IonIconModule} from '../../../util/ion-icon/ion-icon.module';
import {TranslateModule} from '@ngx-translate/core'; import {TranslateModule} from '@ngx-translate/core';
import {RouterModule} from '@angular/router'; import {RouterModule} from '@angular/router';
import {OfflineNoticeComponent} from './offline-notice.component'; import {OfflineNoticeComponent} from './offline-notice.component';
import {ThingTranslateModule} from '../../../translation/thing-translate.module';
@NgModule({ @NgModule({
declarations: [RootLinkDirective, NavigationComponent, TabsComponent, OfflineNoticeComponent], declarations: [RootLinkDirective, NavigationComponent, TabsComponent, OfflineNoticeComponent],
imports: [CommonModule, IonicModule, IonIconModule, TranslateModule, RouterModule], imports: [CommonModule, IonicModule, IonIconModule, TranslateModule, RouterModule, ThingTranslateModule],
exports: [TabsComponent, RootLinkDirective, NavigationComponent], exports: [TabsComponent, RootLinkDirective, NavigationComponent],
}) })
export class NavigationModule {} export class NavigationModule {}

View File

@@ -1,37 +0,0 @@
/*
* Copyright (C) 2022 StApps
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {Injectable} from '@angular/core';
import {SCAppConfigurationMenuCategory} from '@openstapps/core';
import {ConfigProvider} from '../../config/config.provider';
import {NGXLogger} from 'ngx-logger';
@Injectable({
providedIn: 'root',
})
export class NavigationService {
constructor(private configProvider: ConfigProvider, private logger: NGXLogger) {}
async getMenu() {
let menu: SCAppConfigurationMenuCategory[] = [];
try {
menu = this.configProvider.getValue('menus') as SCAppConfigurationMenuCategory[];
} catch (error) {
this.logger.error(`error from loading menu entries: ${error}`);
}
return menu;
}
}

View File

@@ -12,93 +12,44 @@
* You should have received a copy of the GNU General Public License along with * You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>. * this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import {ChangeDetectionStrategy, Component, inject} from '@angular/core';
import {Component} from '@angular/core'; import {Event, NavigationEnd, Router} from '@angular/router';
import {NavigationEnd, Router} from '@angular/router'; import {SCAppConfigurationMenuCategory} from '@openstapps/core';
import {
SCAppConfigurationMenuCategory,
SCLanguage,
SCThingTranslator,
SCTranslations,
} from '@openstapps/core';
import {ConfigProvider} from '../../config/config.provider'; import {ConfigProvider} from '../../config/config.provider';
import {LangChangeEvent, TranslateService} from '@ngx-translate/core'; import {filter} from 'rxjs';
import {NGXLogger} from 'ngx-logger'; import {map, startWith} from 'rxjs/operators';
/**
* Finds a tab name based on urls
*/
function findTabFromUrl(url: string, menus: SCAppConfigurationMenuCategory[]): string {
if (url === '/') {
return menus[0]?.title ?? '';
}
return menus.find(category => url.includes(category.route))?.title ?? '';
}
/**
* Type guard for event
*/
function isNavigationEnd(event: Event): event is NavigationEnd {
return event instanceof NavigationEnd;
}
@Component({ @Component({
selector: 'stapps-navigation-tabs', selector: 'stapps-navigation-tabs',
templateUrl: 'tabs.template.html', templateUrl: 'tabs.html',
styleUrls: ['./tabs.component.scss'], styleUrls: ['./tabs.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class TabsComponent { export class TabsComponent {
/** menus = inject(ConfigProvider).app.menus.slice(0, 5);
* Possible languages to be used for translation
*/
language: keyof SCTranslations<SCLanguage>;
/** selectedTab$ = this.router.events.pipe(
* Menu entries from config module filter(isNavigationEnd),
*/ map(event => findTabFromUrl(event.url, this.menus)),
menu: SCAppConfigurationMenuCategory[]; startWith(findTabFromUrl(this.router.url, this.menus)),
);
/** constructor(private readonly router: Router) {}
* Core translator
*/
translator: SCThingTranslator;
/**
* Name of selected tab
*/
selectedTab: string;
constructor(
private readonly configProvider: ConfigProvider,
public translateService: TranslateService,
private readonly logger: NGXLogger,
private readonly router: Router,
) {
this.language = this.translateService.currentLang as keyof SCTranslations<SCLanguage>;
this.translator = new SCThingTranslator(this.language);
void this.loadMenuEntries();
this.router.events.subscribe((event: unknown) => {
if (event instanceof NavigationEnd) {
this.selectTab(event.url);
}
});
this.selectTab(router.url);
translateService.onLangChange?.subscribe((event: LangChangeEvent) => {
this.language = event.lang as keyof SCTranslations<SCLanguage>;
this.translator = new SCThingTranslator(this.language);
});
}
/**
* Loads menu entries from configProvider
*/
async loadMenuEntries() {
try {
const menus = (await this.configProvider.getValue('menus')) as SCAppConfigurationMenuCategory[];
const menu = menus.slice(0, 5);
if (menu) {
this.menu = menu;
}
} catch (error) {
this.logger.error(`error from loading menu entries: ${error}`);
}
}
/* eslint-disable @typescript-eslint/no-explicit-any */
selectTab(url: string) {
if (!this.menu) {
return;
}
if (url === '/') {
this.selectedTab = (this.menu[0] as any)?.title ?? '';
return;
}
const candidate = this.menu.slice(0, 5).find(category => url.includes(category.route));
this.selectedTab = candidate?.title ?? '';
}
} }

View File

@@ -33,19 +33,19 @@
</ion-tab-bar> </ion-tab-bar>
--> -->
<ion-tab-bar slot="bottom" [selectedTab]="selectedTab"> <ion-tab-bar slot="bottom" [selectedTab]="selectedTab$ | async">
<ion-menu-toggle> <ion-menu-toggle>
<ion-tab-button class="menu-button"> <ion-tab-button class="menu-button">
<ion-icon name="menu"></ion-icon> <ion-icon name="menu"></ion-icon>
</ion-tab-button> </ion-tab-button>
</ion-menu-toggle> </ion-menu-toggle>
<ion-tab-button <ion-tab-button
*ngFor="let category of menu; first as isFirst" *ngFor="let category of menus; first as isFirst"
[rootLink]="category.route" [rootLink]="category.route"
[redirectedFrom]="category.route" [redirectedFrom]="category.route"
[tab]="category.title" [tab]="category.title"
> >
<ion-icon [name]="category.icon"></ion-icon> <ion-icon [name]="category.icon"></ion-icon>
<ion-label>{{ category.translations[language].title | titlecase }}</ion-label> <ion-label>{{ 'title' | translateSimple: category | titlecase }}</ion-label>
</ion-tab-button> </ion-tab-button>
</ion-tab-bar> </ion-tab-bar>

View File

@@ -18,9 +18,7 @@ import {FormsModule} from '@angular/forms';
import {RouterModule, Routes} from '@angular/router'; import {RouterModule, Routes} from '@angular/router';
import {IonicModule} from '@ionic/angular'; import {IonicModule} from '@ionic/angular';
import {TranslateModule} from '@ngx-translate/core'; import {TranslateModule} from '@ngx-translate/core';
import {ThingTranslateModule} from '../../translation/thing-translate.module'; import {ThingTranslateModule} from '../../translation/thing-translate.module';
import {ConfigProvider} from '../config/config.provider';
import {SettingsItemComponent} from './item/settings-item.component'; import {SettingsItemComponent} from './item/settings-item.component';
import {SettingsPageComponent} from './page/settings-page.component'; import {SettingsPageComponent} from './page/settings-page.component';
import {SettingTranslatePipe} from './setting-translate.pipe'; import {SettingTranslatePipe} from './setting-translate.pipe';
@@ -60,13 +58,6 @@ const settingsRoutes: Routes = [{path: 'settings', component: SettingsPageCompon
RouterModule.forChild(settingsRoutes), RouterModule.forChild(settingsRoutes),
UtilModule, UtilModule,
], ],
providers: [ providers: [ScheduleSyncService, SettingsProvider, CalendarService, ScheduleProvider, ThingTranslatePipe],
ScheduleSyncService,
ConfigProvider,
SettingsProvider,
CalendarService,
ScheduleProvider,
ThingTranslatePipe,
],
}) })
export class SettingsModule {} export class SettingsModule {}

View File

@@ -16,8 +16,9 @@ import {Injectable} from '@angular/core';
import {SCSetting, SCSettingValue, SCSettingValues} from '@openstapps/core'; import {SCSetting, SCSettingValue, SCSettingValues} from '@openstapps/core';
import deepMerge from 'deepmerge'; import deepMerge from 'deepmerge';
import {Subject} from 'rxjs'; import {Subject} from 'rxjs';
import {ConfigProvider} from '../config/config.provider';
import {StorageProvider} from '../storage/storage.provider'; import {StorageProvider} from '../storage/storage.provider';
import {ConfigProvider} from '../config/config.provider';
import {BeforeAppInit} from '../../before-app-init';
export const STORAGE_KEY_SETTINGS = 'settings'; export const STORAGE_KEY_SETTINGS = 'settings';
export const STORAGE_KEY_SETTINGS_SEPARATOR = '.'; export const STORAGE_KEY_SETTINGS_SEPARATOR = '.';
@@ -89,19 +90,19 @@ export interface SettingsAction {
/** /**
* Provider for app settings * Provider for app settings
*/ */
@Injectable() @Injectable({
export class SettingsProvider { providedIn: 'root',
})
export class SettingsProvider implements BeforeAppInit {
/** /**
* Source of settings actions * Source of settings actions
*/ */
private settingsActionSource = new Subject<SettingsAction>(); private settingsActionSource = new Subject<SettingsAction>();
private needsInit = true;
/** /**
* Order of the setting categories * Order of the setting categories
*/ */
categoriesOrder: string[]; categoriesOrder: string[] = [];
/** /**
* Settings actions observable * Settings actions observable
@@ -111,7 +112,45 @@ export class SettingsProvider {
/** /**
* Cache for the imported settings * Cache for the imported settings
*/ */
settingsCache: SettingsCache; settingsCache: SettingsCache = {};
constructor(private storageProvider: StorageProvider, private config: ConfigProvider) {}
async beforeAppInit() {
try {
for (const setting of this.config.app.settings) {
this.addSetting(setting);
}
for (const category of Object.keys(this.settingsCache)) {
if (!this.categoriesOrder.includes(category)) {
this.categoriesOrder.push(category);
}
}
} catch {
this.settingsCache = {};
}
if (await this.storageProvider.has(STORAGE_KEY_SETTING_VALUES)) {
// get setting values from StorageProvider into settingsCache
const valuesContainer: SettingValuesContainer = await this.storageProvider.get<SettingValuesContainer>(
STORAGE_KEY_SETTING_VALUES,
);
// iterate through keys of categories
for (const categoryKey of Object.keys(this.settingsCache)) {
// iterate through setting keys of category
for (const settingKey of Object.keys(this.settingsCache[categoryKey].settings)) {
// if saved setting value exists set it, otherwise set to default value
this.settingsCache[categoryKey].settings[settingKey].value =
valuesContainer[categoryKey] !== undefined &&
valuesContainer[categoryKey][settingKey] !== undefined
? valuesContainer[categoryKey][settingKey]
: this.settingsCache[categoryKey].settings[settingKey].defaultValue;
}
}
await this.saveSettingValues();
}
}
/** /**
* Return true if all given values are valid to possible values in given settingInput * Return true if all given values are valid to possible values in given settingInput
@@ -192,21 +231,11 @@ export class SettingsProvider {
return isValueValid; return isValueValid;
} }
/**
*
* @param storage TODO
* @param configProvider TODO
*/
constructor(private readonly storage: StorageProvider, private readonly configProvider: ConfigProvider) {
this.categoriesOrder = [];
this.settingsCache = {};
}
/** /**
* Add an Setting to the Cache if not exist and set undefined value to defaultValue * Add an Setting to the Cache if not exist and set undefined value to defaultValue
* @param setting Setting with categories, defaultValue, name, input type and valid values * @param setting Setting with categories, defaultValue, name, input type and valid values
*/ */
private addSetting(setting: SCSetting): void { addSetting(setting: SCSetting): void {
if (!this.categoryExists(setting.categories[0])) { if (!this.categoryExists(setting.categories[0])) {
this.provideCategory(setting.categories[0]); this.provideCategory(setting.categories[0]);
} }
@@ -265,9 +294,7 @@ export class SettingsProvider {
/** /**
* Returns copy of cached settings * Returns copy of cached settings
*/ */
public async getCache(): Promise<SettingsCache> { public getCache(): SettingsCache {
await this.init();
return JSON.parse(JSON.stringify(this.settingsCache)); return JSON.parse(JSON.stringify(this.settingsCache));
} }
@@ -284,8 +311,7 @@ export class SettingsProvider {
* @param name the name of requested setting * @param name the name of requested setting
* @throws Exception if setting is not provided * @throws Exception if setting is not provided
*/ */
public async getSetting(category: string, name: string): Promise<SCSetting> { public getSetting(category: string, name: string): SCSetting {
await this.init();
if (this.settingExists(category, name)) { if (this.settingExists(category, name)) {
// return a copy of the settings // return a copy of the settings
return JSON.parse(JSON.stringify(this.settingsCache[category].settings[name])); return JSON.parse(JSON.stringify(this.settingsCache[category].settings[name]));
@@ -299,8 +325,7 @@ export class SettingsProvider {
* @param name the name of requested setting * @param name the name of requested setting
* @throws Exception if setting is not provided * @throws Exception if setting is not provided
*/ */
public async getValue(category: string, name: string): Promise<SCSettingValue | SCSettingValues> { public getValue(category: string, name: string): SCSettingValue | SCSettingValues {
await this.init();
if (this.settingExists(category, name)) { if (this.settingExists(category, name)) {
// return a copy of the settings value // return a copy of the settings value
return JSON.parse(JSON.stringify(this.settingsCache[category].settings[name].value)); return JSON.parse(JSON.stringify(this.settingsCache[category].settings[name].value));
@@ -308,47 +333,6 @@ export class SettingsProvider {
throw new Error(`Setting "${name}" not provided`); throw new Error(`Setting "${name}" not provided`);
} }
/**
* Initializes settings from config and stored values if exist
*/
public async init(): Promise<void> {
if (!this.needsInit) return;
this.needsInit = false;
try {
const settings: SCSetting[] = this.configProvider.getValue('settings') as SCSetting[];
for (const setting of settings) this.addSetting(setting);
for (const category of Object.keys(this.settingsCache)) {
if (!this.categoriesOrder.includes(category)) {
this.categoriesOrder.push(category);
}
}
} catch {
this.settingsCache = {};
}
if (await this.storage.has(STORAGE_KEY_SETTING_VALUES)) {
// get setting values from StorageProvider into settingsCache
const valuesContainer: SettingValuesContainer = await this.storage.get<SettingValuesContainer>(
STORAGE_KEY_SETTING_VALUES,
);
// iterate through keys of categories
for (const categoryKey of Object.keys(this.settingsCache)) {
// iterate through setting keys of category
for (const settingKey of Object.keys(this.settingsCache[categoryKey].settings)) {
// if saved setting value exists set it, otherwise set to default value
this.settingsCache[categoryKey].settings[settingKey].value =
valuesContainer[categoryKey] !== undefined &&
valuesContainer[categoryKey][settingKey] !== undefined
? valuesContainer[categoryKey][settingKey]
: this.settingsCache[categoryKey].settings[settingKey].defaultValue;
}
}
await this.saveSettingValues();
}
}
/** /**
* Adds given setting and its category if not exist * Adds given setting and its category if not exist
* @param setting the setting to add * @param setting the setting to add
@@ -358,12 +342,10 @@ export class SettingsProvider {
} }
/** /**
* Deletes saved values and reinitialising the settings * Delete saved values and reinitialize the settings
*/ */
public async reset(): Promise<void> { public async reset(): Promise<void> {
await this.storage.put(STORAGE_KEY_SETTING_VALUES, {}); await this.storageProvider.put(STORAGE_KEY_SETTING_VALUES, {});
this.needsInit = true;
await this.init();
} }
/** /**
@@ -383,15 +365,14 @@ export class SettingsProvider {
* Saves cached settings in app storage * Saves cached settings in app storage
*/ */
public async saveSettingValues(): Promise<void> { public async saveSettingValues(): Promise<void> {
if (await this.storage.has(STORAGE_KEY_SETTING_VALUES)) { if (await this.storageProvider.has(STORAGE_KEY_SETTING_VALUES)) {
const savedSettingsValues: SettingValuesContainer = await this.storage.get<SettingValuesContainer>( const savedSettingsValues: SettingValuesContainer =
STORAGE_KEY_SETTING_VALUES, await this.storageProvider.get<SettingValuesContainer>(STORAGE_KEY_SETTING_VALUES);
);
const cacheSettingsValues = this.getSettingValuesFromCache(); const cacheSettingsValues = this.getSettingValuesFromCache();
const mergedSettingValues = deepMerge(savedSettingsValues, cacheSettingsValues); const mergedSettingValues = deepMerge(savedSettingsValues, cacheSettingsValues);
await this.storage.put<SettingValuesContainer>(STORAGE_KEY_SETTING_VALUES, mergedSettingValues); await this.storageProvider.put<SettingValuesContainer>(STORAGE_KEY_SETTING_VALUES, mergedSettingValues);
} else { } else {
await this.storage.put<SettingValuesContainer>( await this.storageProvider.put<SettingValuesContainer>(
STORAGE_KEY_SETTING_VALUES, STORAGE_KEY_SETTING_VALUES,
this.getSettingValuesFromCache(), this.getSettingValuesFromCache(),
); );
@@ -418,7 +399,6 @@ export class SettingsProvider {
name: string, name: string,
value: SCSettingValue | SCSettingValues, value: SCSettingValue | SCSettingValues,
): Promise<void> { ): Promise<void> {
await this.init();
if (this.settingExists(category, name)) { if (this.settingExists(category, name)) {
const setting: SCSetting = this.settingsCache[category].settings[name]; const setting: SCSetting = this.settingsCache[category].settings[name];
const isValueValid = SettingsProvider.validateValue(setting, value); const isValueValid = SettingsProvider.validateValue(setting, value);

View File

@@ -14,17 +14,22 @@
*/ */
import {Injectable} from '@angular/core'; import {Injectable} from '@angular/core';
import {Storage} from '@ionic/storage-angular'; import {Storage} from '@ionic/storage-angular';
import {BeforeAppInit} from '../../before-app-init';
/** /**
* Provides interaction with the (ionic) storage on the device (in the browser) * Provides interaction with the (ionic) storage on the device (in the browser)
*/ */
@Injectable() @Injectable({providedIn: 'root'})
export class StorageProvider { export class StorageProvider implements BeforeAppInit {
/** /**
* @param storage TODO * @param storage TODO
*/ */
constructor(private readonly storage: Storage) {} constructor(private readonly storage: Storage) {}
async beforeAppInit() {
await this.storage.create();
}
/** /**
* Deletes storage entries using keys used to save them * Deletes storage entries using keys used to save them
* @param keys Unique identifiers of the resources for deletion * @param keys Unique identifiers of the resources for deletion
@@ -95,13 +100,6 @@ export class StorageProvider {
return (await this.storage.keys()).includes(key); return (await this.storage.keys()).includes(key);
} }
/**
* Initializes the storage (waits until it's ready)
*/
async init(): Promise<void> {
await this.storage.create();
}
/** /**
* Provides information if storage is empty or not * Provides information if storage is empty or not
*/ */

View File

@@ -16,7 +16,6 @@ import {Injectable, OnDestroy, Pipe, PipeTransform} from '@angular/core';
import {LangChangeEvent, TranslateService} from '@ngx-translate/core'; import {LangChangeEvent, TranslateService} from '@ngx-translate/core';
import moment from 'moment'; import moment from 'moment';
import {Subscription} from 'rxjs'; import {Subscription} from 'rxjs';
import {logger} from '../_helpers/ts-logger';
@Injectable() @Injectable()
@Pipe({ @Pipe({
@@ -163,7 +162,7 @@ export class DurationLocalizedPipe implements PipeTransform, OnDestroy {
updateValue(value: string | unknown, isFrequency = false): void { updateValue(value: string | unknown, isFrequency = false): void {
if (typeof value !== 'string') { if (typeof value !== 'string') {
logger.warn(`durationLocalized pipe unable to parse input: ${value}`); console.warn('durationLocalized pipe unable to parse input', value);
return; return;
} }
@@ -223,7 +222,7 @@ export class MetersLocalizedPipe implements PipeTransform, OnDestroy {
updateValue(value: string | number | unknown) { updateValue(value: string | number | unknown) {
if (typeof value !== 'string' && typeof value !== 'number') { if (typeof value !== 'string' && typeof value !== 'number') {
logger.warn(`metersLocalized pipe unable to parse input: ${value}`); console.warn('metersLocalized pipe unable to parse input:', value);
return; return;
} }
@@ -321,7 +320,7 @@ export class NumberLocalizedPipe implements PipeTransform, OnDestroy {
updateValue(value: string | number | unknown, formatOptions?: string): void { updateValue(value: string | number | unknown, formatOptions?: string): void {
if (typeof value !== 'string' && typeof value !== 'number') { if (typeof value !== 'string' && typeof value !== 'number') {
logger.warn(`numberLocalized pipe unable to parse input: ${value}`); console.warn('numberLocalized pipe unable to parse input:', value);
return; return;
} }
@@ -387,7 +386,7 @@ export class DateLocalizedFormatPipe implements PipeTransform, OnDestroy {
updateValue(value: string | Date | unknown, formatOptions?: string): void { updateValue(value: string | Date | unknown, formatOptions?: string): void {
if (typeof value !== 'string' && Object.prototype.toString.call(value) !== '[object Date]') { if (typeof value !== 'string' && Object.prototype.toString.call(value) !== '[object Date]') {
logger.warn(`dateFormat pipe unable to parse input: ${value}`); console.warn('dateFormat pipe unable to parse input:', value);
return; return;
} }

View File

@@ -0,0 +1,38 @@
import {SCLanguageCode, SCSettingValue} from '@openstapps/core';
import moment from 'moment/moment';
import {getDateFnsLocale} from './dfns-locale';
import {setDefaultOptions} from 'date-fns';
import {SettingsProvider} from '../modules/settings/settings.provider';
import {TranslateService} from '@ngx-translate/core';
import {DateFnsConfigurationService} from 'ngx-date-fns';
import {ConfigProvider} from '../modules/config/config.provider';
import {BeforeAppInit} from '../before-app-init';
import {inject, Injectable} from '@angular/core';
@Injectable({providedIn: 'root'})
export class TranslateServiceWrapper extends TranslateService implements BeforeAppInit {
private config = inject(ConfigProvider);
private settingsProvider = inject(SettingsProvider);
private dateFnsConfigurationService = inject(DateFnsConfigurationService);
async beforeAppInit() {
if (this.config.isFirstSession) {
// set language from browser
await this.settingsProvider.setSettingValue(
'profile',
'language',
this.getBrowserLang() as SCSettingValue,
);
}
const languageCode = this.settingsProvider.getValue('profile', 'language') as string;
// this language will be used as a fallback when a translation isn't found in the current language
this.setDefaultLang('en');
this.use(languageCode);
moment.locale(languageCode);
const dateFnsLocale = await getDateFnsLocale(languageCode as SCLanguageCode);
setDefaultOptions({locale: dateFnsLocale});
this.dateFnsConfigurationService.setLocale(dateFnsLocale);
}
}

View File

@@ -21,6 +21,7 @@ import {
race, race,
RetryConfig, RetryConfig,
share, share,
skip,
Subject, Subject,
takeUntil, takeUntil,
} from 'rxjs'; } from 'rxjs';
@@ -61,7 +62,7 @@ export class InternetConnectionService {
private doRetry(error: unknown, retryCount: number): ObservableInput<unknown> { private doRetry(error: unknown, retryCount: number): ObservableInput<unknown> {
return race( return race(
this.offline$.pipe( this.offline$.pipe(
tap(it => console.log(it)), skip(1),
filter(it => !it), filter(it => !it),
take(1), take(1),
delay(Math.min(retryCount ** 4 + 100, 10_000)), delay(Math.min(retryCount ** 4 + 100, 10_000)),

View File

@@ -21,7 +21,7 @@ export const environment = {
backend_url: 'https://mobile.server.uni-frankfurt.de', backend_url: 'https://mobile.server.uni-frankfurt.de',
app_host: 'mobile.app.uni-frankfurt.de', app_host: 'mobile.app.uni-frankfurt.de',
custom_url_scheme: 'de.anyschool.app', custom_url_scheme: 'de.anyschool.app',
backend_version: '4.0.0', backend_version: '0.0.0',
production: false, production: false,
}; };