mirror of
https://gitlab.com/openstapps/openstapps.git
synced 2025-12-13 17:56:20 +00:00
Compare commits
2 Commits
@openstapp
...
18-overhau
| Author | SHA1 | Date | |
|---|---|---|---|
|
09de4fd033
|
|||
|
|
cb196afded |
5
.changeset/silver-bobcats-cry.md
Normal file
5
.changeset/silver-bobcats-cry.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@openstapps/app': minor
|
||||
---
|
||||
|
||||
Queue config update for next launch to not block app launches
|
||||
5
.changeset/twelve-planes-knock.md
Normal file
5
.changeset/twelve-planes-knock.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@openstapps/proxy': minor
|
||||
---
|
||||
|
||||
Send 426 to outdated clients instead of 404
|
||||
@@ -31,10 +31,11 @@ To Provide your own configuration file you can create a `default.json` file in t
|
||||
|
||||
## Status Codes
|
||||
|
||||
- OutdatedVersions return a `HTTP 404`
|
||||
- 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`
|
||||
- Successfull reponses come with a `HTTP 200`
|
||||
- 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>`
|
||||
|
||||
|
||||
@@ -14,9 +14,8 @@
|
||||
* 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} */
|
||||
const configFile = {
|
||||
module.exports = {
|
||||
activeVersions: ['1\\.0\\.\\d+', '2\\.0\\.\\d+'],
|
||||
hiddenRoutes: ['/bulk'],
|
||||
logFormat: 'default',
|
||||
@@ -31,5 +30,3 @@ const configFile = {
|
||||
dhparam: '/etc/nginx/certs/dhparam.pem',
|
||||
},
|
||||
};
|
||||
|
||||
export default configFile;
|
||||
|
||||
@@ -19,14 +19,15 @@ location {{{ route }}} {
|
||||
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) {
|
||||
{{{ 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) {
|
||||
return 404;
|
||||
{{{ cors }}}
|
||||
return 426;
|
||||
}
|
||||
# The version is correct, but backend is not responding
|
||||
if ($proxyurl = unavailable) {
|
||||
|
||||
@@ -34,10 +34,12 @@
|
||||
"build": "tsup-node --dts",
|
||||
"build:docker": "docker build -t openstapps:proxy ../../.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:fix": "prettier --write . --ignore-path ../../.gitignore",
|
||||
"lint": "eslint --ext .ts src/",
|
||||
"lint:fix": "eslint --fix --ext .ts src/",
|
||||
"start": "node app.js",
|
||||
"test": "c8 mocha"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
async function updateNginxConfig() {
|
||||
const containers = await getContainers();
|
||||
const containers = await getContainers(process.env.DOCKER_SOCKET);
|
||||
|
||||
const containerHash = containers
|
||||
.map((container: ContainerInfo) => {
|
||||
@@ -78,4 +78,4 @@ async function updateNginxConfig() {
|
||||
|
||||
// start the process that checks the docker socket periodically
|
||||
// eslint-disable-next-line unicorn/prefer-top-level-await
|
||||
updateNginxConfig();
|
||||
await updateNginxConfig();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
version: '3.7'
|
||||
services:
|
||||
database:
|
||||
image: registry.gitlab.com/openstapps/openstapps/database:2.0.0
|
||||
image: registry.gitlab.com/openstapps/openstapps/database:3.0.0-next.4
|
||||
volumes:
|
||||
- ./database:/usr/share/elasticsearch/data
|
||||
expose:
|
||||
@@ -9,7 +9,7 @@ services:
|
||||
restart: unless-stopped
|
||||
|
||||
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:
|
||||
ES_ADDR: "http://database:9200"
|
||||
NODE_CONFIG_ENV: "elasticsearch"
|
||||
@@ -27,17 +27,17 @@ services:
|
||||
- database
|
||||
|
||||
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:
|
||||
- "backend"
|
||||
|
||||
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
|
||||
command: ["http://backend:3000", "minimal-connector", "f-u"]
|
||||
|
||||
app:
|
||||
image: registry.gitlab.com/openstapps/app/executable:core-0.23
|
||||
image: registry.gitlab.com/openstapps/openstapps/app:3.0.0-next.4
|
||||
expose:
|
||||
- 8100
|
||||
ports:
|
||||
|
||||
@@ -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);
|
||||
@@ -21,17 +21,13 @@ import {RouteReuseStrategy} from '@angular/router';
|
||||
import {IonicModule, IonicRouteStrategy, Platform} from '@ionic/angular';
|
||||
import {TranslateLoader, TranslateModule, TranslateService} from '@ngx-translate/core';
|
||||
import {TranslateHttpLoader} from '@ngx-translate/http-loader';
|
||||
import moment from 'moment';
|
||||
import 'moment/min/locales';
|
||||
import {LoggerModule, NGXLogger, NgxLoggerLevel} from 'ngx-logger';
|
||||
import {LoggerModule, NgxLoggerLevel} from 'ngx-logger';
|
||||
import SwiperCore, {FreeMode, Navigation} from 'swiper';
|
||||
|
||||
import {environment} from '../environments/environment';
|
||||
import {AppRoutingModule} from './app-routing.module';
|
||||
import {AppComponent} from './app.component';
|
||||
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 {DataModule} from './modules/data/data.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 {ScheduleModule} from './modules/schedule/schedule.module';
|
||||
import {SettingsModule} from './modules/settings/settings.module';
|
||||
import {SettingsProvider} from './modules/settings/settings.provider';
|
||||
import {StorageModule} from './modules/storage/storage.module';
|
||||
import {ThingTranslateModule} from './translation/thing-translate.module';
|
||||
import {UtilModule} from './util/util.module';
|
||||
import {initLogger} from './_helpers/ts-logger';
|
||||
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
|
||||
import {AboutModule} from './modules/about/about.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 {BackgroundModule} from './modules/background/background.module';
|
||||
import {LibraryModule} from './modules/library/library.module';
|
||||
import {StorageProvider} from './modules/storage/storage.provider';
|
||||
import {AssessmentsModule} from './modules/assessments/assessments.module';
|
||||
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 {NavigationModule} from './modules/menu/navigation/navigation.module';
|
||||
import {browserFactory, SimpleBrowser} from './util/browser.factory';
|
||||
import {getDateFnsLocale} from './translation/dfns-locale';
|
||||
import {setDefaultOptions} from 'date-fns';
|
||||
import {DateFnsConfigurationService} from 'ngx-date-fns';
|
||||
import {ConfigProvider} from './modules/config/config.provider';
|
||||
import {SettingsProvider} from './modules/settings/settings.provider';
|
||||
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);
|
||||
|
||||
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({
|
||||
bootstrap: [AppComponent],
|
||||
declarations: [AppComponent],
|
||||
@@ -141,7 +77,6 @@ export function createTranslateLoader(http: HttpClient) {
|
||||
BrowserAnimationsModule,
|
||||
CatalogModule,
|
||||
CommonModule,
|
||||
ConfigModule,
|
||||
DashboardModule,
|
||||
DataModule,
|
||||
HebisModule,
|
||||
@@ -165,7 +100,9 @@ export function createTranslateLoader(http: HttpClient) {
|
||||
loader: {
|
||||
deps: [HttpClient],
|
||||
provide: TranslateLoader,
|
||||
useFactory: createTranslateLoader,
|
||||
useFactory(http: HttpClient) {
|
||||
return new TranslateHttpLoader(http, './assets/i18n/', '.json');
|
||||
},
|
||||
},
|
||||
}),
|
||||
UtilModule,
|
||||
@@ -175,6 +112,30 @@ export function createTranslateLoader(http: HttpClient) {
|
||||
}),
|
||||
],
|
||||
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,
|
||||
useClass: IonicRouteStrategy,
|
||||
@@ -188,22 +149,6 @@ export function createTranslateLoader(http: HttpClient) {
|
||||
useFactory: browserFactory,
|
||||
deps: [Platform],
|
||||
},
|
||||
{
|
||||
provide: APP_INITIALIZER,
|
||||
multi: true,
|
||||
deps: [
|
||||
StorageProvider,
|
||||
NGXLogger,
|
||||
SettingsProvider,
|
||||
ConfigProvider,
|
||||
TranslateService,
|
||||
RoutingStackService,
|
||||
DefaultAuthService,
|
||||
PAIAAuthService,
|
||||
DateFnsConfigurationService,
|
||||
],
|
||||
useFactory: initializerFactory,
|
||||
},
|
||||
{
|
||||
provide: HTTP_INTERCEPTORS,
|
||||
useClass: ServiceHandlerInterceptor,
|
||||
|
||||
10
frontend/app/src/app/before-app-init.ts
Normal file
10
frontend/app/src/app/before-app-init.ts
Normal 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>;
|
||||
}
|
||||
@@ -14,10 +14,9 @@
|
||||
*/
|
||||
import {Component, OnInit} from '@angular/core';
|
||||
import {ActivatedRoute} from '@angular/router';
|
||||
import {SCAboutPage, SCAppConfiguration} from '@openstapps/core';
|
||||
import {ConfigProvider} from '../../config/config.provider';
|
||||
import {SCAboutPage} from '@openstapps/core';
|
||||
import packageJson from '../../../../../package.json';
|
||||
import config from 'capacitor.config';
|
||||
import {ConfigProvider} from '../../config/config.provider';
|
||||
|
||||
@Component({
|
||||
selector: 'about-page',
|
||||
@@ -27,15 +26,12 @@ import config from 'capacitor.config';
|
||||
export class AboutPageComponent implements OnInit {
|
||||
content: SCAboutPage;
|
||||
|
||||
appName = config.appName;
|
||||
|
||||
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('/');
|
||||
this.content =
|
||||
(this.configProvider.getValue('aboutPages') as SCAppConfiguration['aboutPages'])[route] ?? {};
|
||||
this.content = this.config.app.aboutPages[route] ?? {};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<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">
|
||||
<about-page-content *ngFor="let element of content.content" [content]="element"></about-page-content>
|
||||
</div>
|
||||
|
||||
@@ -19,7 +19,6 @@ import {FormsModule} from '@angular/forms';
|
||||
import {IonicModule} from '@ionic/angular';
|
||||
import {TranslateModule} from '@ngx-translate/core';
|
||||
import {ThingTranslateModule} from '../../translation/thing-translate.module';
|
||||
import {ConfigProvider} from '../config/config.provider';
|
||||
import {AboutPageComponent} from './about-page/about-page.component';
|
||||
import {MarkdownModule} from 'ngx-markdown';
|
||||
import {AboutPageContentComponent} from './about-page/about-page-content.component';
|
||||
@@ -64,6 +63,5 @@ const settingsRoutes: Routes = [
|
||||
ScrollingModule,
|
||||
UtilModule,
|
||||
],
|
||||
providers: [ConfigProvider],
|
||||
})
|
||||
export class AboutModule {}
|
||||
|
||||
@@ -13,11 +13,12 @@
|
||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import {Injectable} from '@angular/core';
|
||||
import {ConfigProvider} from '../config/config.provider';
|
||||
import {SCAssessment, SCUuid} from '@openstapps/core';
|
||||
import {DefaultAuthService} from '../auth/default-auth.service';
|
||||
import {HttpClient} from '@angular/common/http';
|
||||
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;
|
||||
|
||||
constructor(
|
||||
readonly configProvider: ConfigProvider,
|
||||
readonly config: ConfigProvider,
|
||||
readonly defaultAuth: DefaultAuthService,
|
||||
readonly http: HttpClient,
|
||||
) {}
|
||||
@@ -91,21 +92,20 @@ export class AssessmentsProvider {
|
||||
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');
|
||||
|
||||
this.cache = this.http
|
||||
.get<{data: SCAssessment[]}>(`${url}/${this.assessmentPath}`, {
|
||||
this.cache = firstValueFrom(
|
||||
this.http.get<{data: SCAssessment[]}>(`${url}/${this.assessmentPath}`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken ?? (await this.defaultAuth.getValidToken()).accessToken}`,
|
||||
},
|
||||
})
|
||||
.toPromise()
|
||||
.then(it => {
|
||||
this.cacheTimestamp = Date.now();
|
||||
}),
|
||||
).then(it => {
|
||||
this.cacheTimestamp = Date.now();
|
||||
|
||||
return it?.data ?? [];
|
||||
});
|
||||
return it?.data ?? [];
|
||||
});
|
||||
this.assessments = this.cache.then(toAssessmentMap);
|
||||
|
||||
return this.cache;
|
||||
|
||||
@@ -12,24 +12,18 @@
|
||||
* 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 {IPAIAAuthAction} from './paia/paia-auth-action';
|
||||
import {AuthActions, IAuthAction} from 'ionic-appauth';
|
||||
import {TranslateService} from '@ngx-translate/core';
|
||||
import {JSONPath} from 'jsonpath-plus';
|
||||
import {
|
||||
SCAuthorizationProvider,
|
||||
SCAuthorizationProviderType,
|
||||
SCUserConfiguration,
|
||||
SCUserConfigurationMap,
|
||||
} from '@openstapps/core';
|
||||
import {ConfigProvider} from '../config/config.provider';
|
||||
import {SCAuthorizationProviderType, SCUserConfiguration, SCUserConfigurationMap} from '@openstapps/core';
|
||||
import {StorageProvider} from '../storage/storage.provider';
|
||||
import {DefaultAuthService} from './default-auth.service';
|
||||
import {PAIAAuthService} from './paia/paia-auth.service';
|
||||
import {SimpleBrowser} from '../../util/browser.factory';
|
||||
import {AlertController} from '@ionic/angular';
|
||||
import {ConfigProvider} from '../config/config.provider';
|
||||
|
||||
const AUTH_ORIGIN_PATH = 'stapps.auth.origin_path';
|
||||
|
||||
@@ -37,23 +31,19 @@ const AUTH_ORIGIN_PATH = 'stapps.auth.origin_path';
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class AuthHelperService {
|
||||
userConfigurationMap: SCUserConfigurationMap;
|
||||
get userConfigurationMap(): SCUserConfigurationMap {
|
||||
return this.config.auth.default!.endpoints.mapping;
|
||||
}
|
||||
|
||||
constructor(
|
||||
private translateService: TranslateService,
|
||||
private configProvider: ConfigProvider,
|
||||
private storageProvider: StorageProvider,
|
||||
private defaultAuth: DefaultAuthService,
|
||||
private paiaAuth: PAIAAuthService,
|
||||
private browser: SimpleBrowser,
|
||||
private alertController: AlertController,
|
||||
) {
|
||||
this.userConfigurationMap = (
|
||||
this.configProvider.getAnyValue('auth') as {
|
||||
default: SCAuthorizationProvider;
|
||||
}
|
||||
).default.endpoints.mapping;
|
||||
}
|
||||
private config: ConfigProvider,
|
||||
) {}
|
||||
|
||||
public getAuthMessage(provider: SCAuthorizationProviderType, action: IAuthAction | IPAIAAuthAction) {
|
||||
let message: string | undefined;
|
||||
|
||||
@@ -175,7 +175,7 @@ export abstract class AuthService implements IAuthService {
|
||||
|
||||
public async init() {
|
||||
this.setupAuthorizationNotifier();
|
||||
this.loadTokenFromStorage();
|
||||
await this.loadTokenFromStorage();
|
||||
this.addActionObserver(this._actionHistory);
|
||||
this.addActionObserver(this._session);
|
||||
}
|
||||
|
||||
@@ -12,29 +12,26 @@
|
||||
* 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 {
|
||||
AuthorizationRequestHandler,
|
||||
AuthorizationServiceConfiguration,
|
||||
JQueryRequestor,
|
||||
LocalStorageBackend,
|
||||
Requestor,
|
||||
StorageBackend,
|
||||
TokenRequestHandler,
|
||||
} from '@openid/appauth';
|
||||
import {AuthActionBuilder, Browser, DefaultBrowser, EndSessionHandler, UserInfoHandler} from 'ionic-appauth';
|
||||
import {ConfigProvider} from '../config/config.provider';
|
||||
import {SCAuthorizationProvider} from '@openstapps/core';
|
||||
import {AuthActionBuilder, DefaultBrowser, EndSessionHandler, UserInfoHandler} from 'ionic-appauth';
|
||||
import {getClientConfig, getEndpointsConfig} from './auth.provider.methods';
|
||||
import {Injectable} from '@angular/core';
|
||||
import {AuthService} from './auth.service';
|
||||
import {ConfigProvider} from '../config/config.provider';
|
||||
import {BeforeAppInit} from '../../before-app-init';
|
||||
|
||||
const TOKEN_RESPONSE_KEY = 'token_response';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class DefaultAuthService extends AuthService {
|
||||
export class DefaultAuthService extends AuthService implements BeforeAppInit {
|
||||
public localConfiguration: AuthorizationServiceConfiguration;
|
||||
|
||||
protected tokenHandler: TokenRequestHandler;
|
||||
@@ -45,13 +42,17 @@ export class DefaultAuthService extends AuthService {
|
||||
|
||||
protected endSessionHandler: EndSessionHandler;
|
||||
|
||||
constructor(
|
||||
protected browser: Browser = new DefaultBrowser(),
|
||||
protected storage: StorageBackend = new LocalStorageBackend(),
|
||||
protected requestor: Requestor = new JQueryRequestor(),
|
||||
private readonly configProvider: ConfigProvider,
|
||||
) {
|
||||
super(browser, storage, requestor);
|
||||
constructor(private config: ConfigProvider) {
|
||||
super(new DefaultBrowser(), new LocalStorageBackend(), new JQueryRequestor());
|
||||
}
|
||||
|
||||
async beforeAppInit() {
|
||||
this.authConfig = getClientConfig('default', this.config.auth);
|
||||
this.localConfiguration = new AuthorizationServiceConfiguration(
|
||||
getEndpointsConfig('default', this.config.auth),
|
||||
);
|
||||
|
||||
await super.init();
|
||||
}
|
||||
|
||||
get configuration(): Promise<AuthorizationServiceConfiguration> {
|
||||
@@ -60,22 +61,6 @@ export class DefaultAuthService extends AuthService {
|
||||
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() {
|
||||
await this.revokeTokens().catch(error => {
|
||||
this.notifyActionListers(AuthActionBuilder.SignOutFailed(error));
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
* 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 {
|
||||
AuthorizationError,
|
||||
AuthorizationRequest,
|
||||
@@ -47,10 +46,10 @@ import {PAIAAuthorizationResponse} from './paia-authorization-response';
|
||||
import {PAIAAuthorizationNotifier} from './paia-authorization-notifier';
|
||||
import {PAIATokenResponse} from './paia-token-response';
|
||||
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 {Injectable} from '@angular/core';
|
||||
import {ConfigProvider} from '../../config/config.provider';
|
||||
import {BeforeAppInit} from '../../../before-app-init';
|
||||
|
||||
const TOKEN_RESPONSE_KEY = 'paia_token_response';
|
||||
const AUTH_EXPIRY_BUFFER = 10 * 60 * -1; // 10 minutes in seconds
|
||||
@@ -64,10 +63,8 @@ export interface IAuthService {
|
||||
getValidToken(buffer?: number): Promise<PAIATokenResponse>;
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class PAIAAuthService {
|
||||
@Injectable({providedIn: 'root'})
|
||||
export class PAIAAuthService implements BeforeAppInit {
|
||||
private _authConfig?: IAuthConfig;
|
||||
|
||||
private _authSubject: AuthSubject = new AuthSubject();
|
||||
@@ -97,7 +94,7 @@ export class PAIAAuthService {
|
||||
protected browser: Browser = new DefaultBrowser(),
|
||||
protected storage: StorageBackend = new LocalStorageBackend(),
|
||||
protected requestor: Requestor = new JQueryRequestor(),
|
||||
private readonly configProvider: ConfigProvider,
|
||||
private config: ConfigProvider,
|
||||
) {
|
||||
this.tokenHandler = new PAIATokenRequestHandler(requestor);
|
||||
this.userInfoHandler = new IonicUserInfoHandler(requestor);
|
||||
@@ -110,6 +107,16 @@ export class PAIAAuthService {
|
||||
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> {
|
||||
return this._tokenSubject.asObservable();
|
||||
}
|
||||
@@ -147,20 +154,6 @@ export class PAIAAuthService {
|
||||
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) {
|
||||
/* eslint-disable unicorn/no-useless-undefined */
|
||||
switch (action.action) {
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
* 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 {Calendar} from '@awesome-cordova-plugins/calendar/ngx';
|
||||
import {Injectable} from '@angular/core';
|
||||
import {ICalEvent} from './ical/ical';
|
||||
@@ -29,18 +28,18 @@ const RECURRENCE_PATTERNS: Partial<Record<unitOfTime.Diff, string | undefined>>
|
||||
day: 'daily',
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
@Injectable({providedIn: 'root'})
|
||||
export class CalendarService {
|
||||
goToDate = new Subject<number>();
|
||||
|
||||
goToDateClicked = this.goToDate.asObservable();
|
||||
|
||||
calendarName = 'StApps';
|
||||
get calendarName(): string {
|
||||
return this.config.app.name ?? 'StApps';
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
constructor(readonly calendar: Calendar, private readonly configProvider: ConfigProvider) {
|
||||
this.calendarName = (this.configProvider.getValue('name') as string) ?? 'StApps';
|
||||
}
|
||||
constructor(readonly calendar: Calendar, readonly config: ConfigProvider) {}
|
||||
|
||||
async createCalendar(): Promise<CalendarInfo | undefined> {
|
||||
await this.calendar.createCalendar({
|
||||
|
||||
@@ -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 {}
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable @typescript-eslint/ban-types */
|
||||
/*
|
||||
* Copyright (C) 2022 StApps
|
||||
* This program is free software: you can redistribute it and/or modify it
|
||||
@@ -14,19 +15,17 @@
|
||||
*/
|
||||
import {Injectable} from '@angular/core';
|
||||
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 {
|
||||
ConfigFetchError,
|
||||
ConfigInitError,
|
||||
ConfigValueNotAvailable,
|
||||
SavedConfigNotAvailable,
|
||||
WrongConfigVersionInStorage,
|
||||
} from './errors';
|
||||
SCAppConfiguration,
|
||||
SCAuthorizationProvider,
|
||||
SCBackendConfiguration,
|
||||
SCIndexResponse,
|
||||
} from '@openstapps/core';
|
||||
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
|
||||
@@ -35,145 +34,55 @@ import {
|
||||
*/
|
||||
export const STORAGE_KEY_CONFIG = 'stapps.config';
|
||||
|
||||
/**
|
||||
* Provides configuration
|
||||
*/
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class ConfigProvider {
|
||||
/**
|
||||
* Api client
|
||||
*/
|
||||
client: Client;
|
||||
export class ConfigProvider implements SCIndexResponse, BeforeAppInit {
|
||||
private client: Client;
|
||||
|
||||
/**
|
||||
* App configuration as IndexResponse
|
||||
*/
|
||||
config: SCIndexResponse;
|
||||
constructor(private storageProvider: StorageProvider, httpClient: StAppsWebHttpClient) {
|
||||
this.client = new Client(httpClient, environment.backend_url, environment.backend_version);
|
||||
}
|
||||
|
||||
/**
|
||||
* Version of the @openstapps/core package that app is using
|
||||
*/
|
||||
scVersion = packageInfo.version;
|
||||
async beforeAppInit() {
|
||||
this.isFirstSession = !(await this.storageProvider.has(STORAGE_KEY_CONFIG));
|
||||
// Queue config update for next launch; don't block current launch
|
||||
const configUpdate = this.updateConfig();
|
||||
console.log('Config update queued');
|
||||
|
||||
/**
|
||||
* First session indicator (config not found in storage)
|
||||
*/
|
||||
firstSession = true;
|
||||
const config = await this.storageProvider
|
||||
.get<SCIndexResponse>(STORAGE_KEY_CONFIG)
|
||||
.then(it => it ?? configUpdate);
|
||||
|
||||
/**
|
||||
* Constructor, initialise api client
|
||||
* @param storageProvider StorageProvider to load persistent configuration
|
||||
* @param swHttpClient Api client
|
||||
* @param logger An angular logger
|
||||
*/
|
||||
constructor(
|
||||
private readonly storageProvider: StorageProvider,
|
||||
swHttpClient: StAppsWebHttpClient,
|
||||
private readonly logger: NGXLogger,
|
||||
) {
|
||||
this.client = new Client(swHttpClient, environment.backend_url, environment.backend_version);
|
||||
Object.assign(this, config);
|
||||
|
||||
console.assert(
|
||||
this.backend.SCVersion === coreInfo.version,
|
||||
'Wrong config version in storage.',
|
||||
'Expected:',
|
||||
coreInfo.version,
|
||||
'Actual:',
|
||||
this.backend.SCVersion,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches configuration from backend
|
||||
* Updates the config from remote
|
||||
*/
|
||||
async fetch(): Promise<SCIndexResponse> {
|
||||
try {
|
||||
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> {
|
||||
async updateConfig(): Promise<SCIndexResponse> {
|
||||
const config = await this.client.handshake(coreInfo.version);
|
||||
await this.storageProvider.put(STORAGE_KEY_CONFIG, config);
|
||||
|
||||
console.log(`Config updated`);
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the configuration in the module and writes it into app storage
|
||||
* @param config SCIndexResponse to set
|
||||
*/
|
||||
async set(config: SCIndexResponse): Promise<void> {
|
||||
this.config = config;
|
||||
await this.save(this.config);
|
||||
}
|
||||
app: SCAppConfiguration;
|
||||
|
||||
auth: {default?: SCAuthorizationProvider | undefined; paia?: SCAuthorizationProvider | undefined};
|
||||
|
||||
backend: SCBackendConfiguration;
|
||||
|
||||
isFirstSession: boolean;
|
||||
}
|
||||
|
||||
@@ -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.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -30,9 +30,8 @@ export class OffersInListComponent {
|
||||
@Input() set offers(it: Array<SCThingThatCanBeOfferedOffer<SCAcademicPriceGroup>>) {
|
||||
this._offers = it;
|
||||
this.price = it[0].prices?.default;
|
||||
this.settingsProvider.getSetting('profile', 'group').then(group => {
|
||||
this.price = it[0].prices?.[(group.value as string).replace(/s$/, '') as never];
|
||||
});
|
||||
const group = this.settingsProvider.getSetting('profile', 'group');
|
||||
this.price = it[0].prices?.[(group.value as string).replace(/s$/, '') as never];
|
||||
|
||||
const availabilities = new Set(it.map(offer => offer.availability));
|
||||
this.soldOut = availabilities.has('out of stock') && availabilities.size === 1;
|
||||
|
||||
@@ -15,16 +15,9 @@
|
||||
import {Component, DestroyRef, inject, Input, OnInit} from '@angular/core';
|
||||
import {ActivatedRoute, Router} from '@angular/router';
|
||||
import {Keyboard} from '@capacitor/keyboard';
|
||||
import {AlertController, AnimationBuilder, AnimationController} from '@ionic/angular';
|
||||
import {AlertController, AnimationController} from '@ionic/angular';
|
||||
import {Capacitor} from '@capacitor/core';
|
||||
import {
|
||||
SCFacet,
|
||||
SCFeatureConfiguration,
|
||||
SCSearchFilter,
|
||||
SCSearchQuery,
|
||||
SCSearchSort,
|
||||
SCThings,
|
||||
} from '@openstapps/core';
|
||||
import {SCFacet, SCSearchFilter, SCSearchQuery, SCSearchSort, SCThings} from '@openstapps/core';
|
||||
import {NGXLogger} from 'ngx-logger';
|
||||
import {combineLatest, Subject} from 'rxjs';
|
||||
import {debounceTime, distinctUntilChanged, startWith} from 'rxjs/operators';
|
||||
@@ -33,9 +26,9 @@ import {SettingsProvider} from '../../settings/settings.provider';
|
||||
import {DataRoutingService} from '../data-routing.service';
|
||||
import {DataProvider} from '../data.provider';
|
||||
import {PositionService} from '../../map/position.service';
|
||||
import {ConfigProvider} from '../../config/config.provider';
|
||||
import {searchPageSwitchAnimation} from './search-page-switch-animation';
|
||||
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
|
||||
@@ -144,21 +137,8 @@ export class SearchPageComponent implements OnInit {
|
||||
|
||||
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(
|
||||
protected readonly alertController: AlertController,
|
||||
protected dataProvider: DataProvider,
|
||||
@@ -169,11 +149,8 @@ export class SearchPageComponent implements OnInit {
|
||||
protected router: Router,
|
||||
private readonly route: ActivatedRoute,
|
||||
protected positionService: PositionService,
|
||||
private readonly configProvider: ConfigProvider,
|
||||
animationController: AnimationController,
|
||||
) {
|
||||
this.routeAnimation = searchPageSwitchAnimation(animationController);
|
||||
}
|
||||
private readonly config: ConfigProvider,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Fetches items with set query configuration
|
||||
@@ -347,8 +324,7 @@ export class SearchPageComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
try {
|
||||
const features = this.configProvider.getValue('features') as SCFeatureConfiguration;
|
||||
this.isHebisAvailable = !!features.plugins?.['hebis-plugin']?.urlPath;
|
||||
this.isHebisAvailable = !!this.config.app.features.plugins?.['hebis-plugin']?.urlPath;
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
}
|
||||
|
||||
@@ -62,10 +62,8 @@ export class RatingProvider {
|
||||
return new Date(today.getFullYear(), today.getMonth(), today.getDate()).toISOString();
|
||||
}
|
||||
|
||||
private get userGroup(): Promise<SCUserGroup> {
|
||||
return this.settingsProvider
|
||||
.getSetting('profile', 'group')
|
||||
.then(it => (it as SCUserGroupSetting).value as SCUserGroup);
|
||||
private get userGroup(): SCUserGroup {
|
||||
return (this.settingsProvider.getSetting('profile', 'group') as SCUserGroupSetting).value as SCUserGroup;
|
||||
}
|
||||
|
||||
private async getStoredRatings(): Promise<RatingStorage> {
|
||||
|
||||
@@ -12,21 +12,13 @@
|
||||
* 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 {Component, OnInit} from '@angular/core';
|
||||
import {AlertController, AnimationController} from '@ionic/angular';
|
||||
import {ActivatedRoute, Router} from '@angular/router';
|
||||
import {NGXLogger} from 'ngx-logger';
|
||||
import {Component, inject, OnInit} from '@angular/core';
|
||||
import {debounceTime, distinctUntilChanged, startWith, take} from 'rxjs/operators';
|
||||
import {combineLatest} from 'rxjs';
|
||||
import {SCThingType} from '@openstapps/core';
|
||||
import {FavoritesService} from './favorites.service';
|
||||
import {DataRoutingService} from '../data/data-routing.service';
|
||||
import {ContextMenuService} from '../menu/context/context-menu.service';
|
||||
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';
|
||||
|
||||
/**
|
||||
@@ -42,34 +34,7 @@ export class FavoritesPageComponent extends SearchPageComponent implements OnIni
|
||||
|
||||
showNavigation = false;
|
||||
|
||||
constructor(
|
||||
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,
|
||||
);
|
||||
}
|
||||
favoritesService = inject(FavoritesService);
|
||||
|
||||
ngOnInit() {
|
||||
super.ngOnInit(false);
|
||||
|
||||
@@ -16,10 +16,9 @@ import {Injectable} from '@angular/core';
|
||||
import {DaiaAvailabilityResponse, DaiaHolding, DaiaService} from './protocol/response';
|
||||
import {StorageProvider} from '../storage/storage.provider';
|
||||
import {HttpClient, HttpHeaders} from '@angular/common/http';
|
||||
import {ConfigProvider} from '../config/config.provider';
|
||||
import {SCFeatureConfiguration} from '@openstapps/core';
|
||||
import {NGXLogger} from 'ngx-logger';
|
||||
import {TranslateService} from '@ngx-translate/core';
|
||||
import {ConfigProvider} from '../config/config.provider';
|
||||
|
||||
/**
|
||||
* Generated class for the DataProvider provider.
|
||||
@@ -44,18 +43,10 @@ export class DaiaDataProvider {
|
||||
|
||||
clientHeaders = new HttpHeaders();
|
||||
|
||||
/**
|
||||
* TODO
|
||||
* @param storageProvider TODO
|
||||
* @param httpClient TODO
|
||||
* @param configProvider TODO
|
||||
* @param logger TODO
|
||||
* @param translateService TODO
|
||||
*/
|
||||
constructor(
|
||||
storageProvider: StorageProvider,
|
||||
httpClient: HttpClient,
|
||||
private configProvider: ConfigProvider,
|
||||
private config: ConfigProvider,
|
||||
private readonly logger: NGXLogger,
|
||||
private translateService: TranslateService,
|
||||
) {
|
||||
@@ -67,15 +58,14 @@ export class DaiaDataProvider {
|
||||
async getAvailability(id: string): Promise<DaiaHolding[] | undefined> {
|
||||
if (this.daiaServiceUrl === undefined) {
|
||||
try {
|
||||
const features = this.configProvider.getValue('features') as SCFeatureConfiguration;
|
||||
if (features.extern?.daia?.url) {
|
||||
this.daiaServiceUrl = features.extern?.daia?.url;
|
||||
if (this.config.app.features.extern?.daia?.url) {
|
||||
this.daiaServiceUrl = this.config.app.features.extern?.daia?.url;
|
||||
} else {
|
||||
this.logger.error('Daia service url undefined');
|
||||
return undefined;
|
||||
}
|
||||
if (features.extern?.hebisProxy?.url) {
|
||||
this.hebisProxyUrl = features.extern?.hebisProxy?.url;
|
||||
if (this.config.app.features.extern?.hebisProxy?.url) {
|
||||
this.hebisProxyUrl = this.config.app.features.extern?.hebisProxy?.url;
|
||||
} else {
|
||||
this.logger.error('HeBIS proxy url undefined');
|
||||
return undefined;
|
||||
|
||||
@@ -12,22 +12,17 @@
|
||||
* 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 {JQueryRequestor, Requestor} from '@openid/appauth';
|
||||
import {
|
||||
SCAuthorizationProviderType,
|
||||
SCFeatureConfiguration,
|
||||
SCFeatureConfigurationExtern,
|
||||
} from '@openstapps/core';
|
||||
import {SCAuthorizationProviderType} from '@openstapps/core';
|
||||
import {DocumentAction, PAIADocument, PAIADocumentStatus, PAIAFees, PAIAItems, PAIAPatron} from '../types';
|
||||
import {HebisDataProvider} from '../../hebis/hebis-data.provider';
|
||||
import {PAIATokenResponse} from '../../auth/paia/paia-token-response';
|
||||
import {AuthHelperService} from '../../auth/auth-helper.service';
|
||||
import {ConfigProvider} from '../../config/config.provider';
|
||||
import {TranslateService} from '@ngx-translate/core';
|
||||
import {AlertController, ToastController} from '@ionic/angular';
|
||||
import {HebisSearchResponse} from '../../hebis/protocol/response';
|
||||
import {ConfigProvider} from '../../config/config.provider';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
@@ -36,29 +31,26 @@ export class LibraryAccountService {
|
||||
/**
|
||||
* Base url of the external service
|
||||
*/
|
||||
baseUrl: string;
|
||||
get baseUrl(): string {
|
||||
return this.config.app.features.extern!['paia'].url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Authorization provider type
|
||||
*/
|
||||
authType: SCAuthorizationProviderType;
|
||||
get authType(): SCAuthorizationProviderType {
|
||||
return this.config.app.features.extern!['paia'].authProvider!;
|
||||
}
|
||||
|
||||
constructor(
|
||||
protected requestor: Requestor = new JQueryRequestor(),
|
||||
private readonly hebisDataProvider: HebisDataProvider,
|
||||
private readonly authHelper: AuthHelperService,
|
||||
readonly configProvider: ConfigProvider,
|
||||
private readonly translateService: TranslateService,
|
||||
private readonly alertController: AlertController,
|
||||
private readonly toastController: ToastController,
|
||||
) {
|
||||
// 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;
|
||||
}
|
||||
private readonly config: ConfigProvider,
|
||||
) {}
|
||||
|
||||
async getProfile() {
|
||||
const patron = ((await this.getValidToken()) as PAIATokenResponse).patron;
|
||||
|
||||
@@ -19,9 +19,7 @@ import {LeafletModule} from '@asymmetrik/ngx-leaflet';
|
||||
import {LeafletMarkerClusterModule} from '@asymmetrik/ngx-leaflet-markercluster';
|
||||
import {IonicModule} from '@ionic/angular';
|
||||
import {TranslateModule} from '@ngx-translate/core';
|
||||
import {Polygon} from 'geojson';
|
||||
import {ThingTranslateModule} from '../../translation/thing-translate.module';
|
||||
import {ConfigProvider} from '../config/config.provider';
|
||||
import {DataFacetsProvider} from '../data/data-facets.provider';
|
||||
import {DataModule} from '../data/data.module';
|
||||
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 {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 = [
|
||||
{path: 'map', component: MapPageComponent},
|
||||
{path: 'map/:uid', component: MapPageComponent},
|
||||
|
||||
@@ -21,12 +21,11 @@ import {
|
||||
SCThingType,
|
||||
SCUuid,
|
||||
} from '@openstapps/core';
|
||||
import {Point, Polygon} from 'geojson';
|
||||
import {Point} from 'geojson';
|
||||
import {divIcon, geoJSON, LatLng, Map, marker, Marker} from 'leaflet';
|
||||
import {DataProvider} from '../data/data.provider';
|
||||
import {MapPosition, PositionService} from './position.service';
|
||||
import {hasValidLocation} from '../data/types/place/place-types';
|
||||
import {ConfigProvider} from '../config/config.provider';
|
||||
import {SCIcon} from '../../util/ion-icon/icon';
|
||||
|
||||
/**
|
||||
@@ -36,11 +35,6 @@ import {SCIcon} from '../../util/ion-icon/icon';
|
||||
providedIn: 'root',
|
||||
})
|
||||
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
|
||||
* @param point Point to get marker for
|
||||
@@ -111,13 +105,7 @@ export class MapProvider {
|
||||
clearInterval(interval);
|
||||
};
|
||||
|
||||
constructor(
|
||||
private dataProvider: DataProvider,
|
||||
private positionService: PositionService,
|
||||
private configProvider: ConfigProvider,
|
||||
) {
|
||||
this.defaultPolygon = this.configProvider.getValue('campusPolygon') as Polygon;
|
||||
}
|
||||
constructor(private dataProvider: DataProvider, private positionService: PositionService) {}
|
||||
|
||||
/**
|
||||
* Provide the specific place by its UID
|
||||
|
||||
@@ -30,6 +30,7 @@ import {Capacitor} from '@capacitor/core';
|
||||
import {pauseWhen} from '../../../util/rxjs/pause-when';
|
||||
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
|
||||
import {startViewTransition} from '../../../util/view-transition';
|
||||
import {ConfigProvider} from '../../config/config.provider';
|
||||
|
||||
/**
|
||||
* The main page of the map
|
||||
@@ -102,7 +103,7 @@ export class MapPageComponent implements OnInit {
|
||||
* Options of the leaflet map
|
||||
*/
|
||||
options: MapOptions = {
|
||||
center: geoJSON(this.mapProvider.defaultPolygon).getBounds().getCenter(),
|
||||
center: geoJSON(inject(ConfigProvider).app.campusPolygon).getBounds().getCenter(),
|
||||
layers: [
|
||||
tileLayer('https://osm.server.uni-frankfurt.de/tiles/roads/x={x}&y={y}&z={z}', {
|
||||
attribution: '© <a href="http://osm.org/copyright">OpenStreetMap</a> contributors',
|
||||
|
||||
@@ -12,75 +12,23 @@
|
||||
* 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 {Component, OnInit} 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 {Component, inject} from '@angular/core';
|
||||
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({
|
||||
selector: 'stapps-navigation',
|
||||
styleUrls: ['navigation.scss'],
|
||||
templateUrl: 'navigation.html',
|
||||
})
|
||||
export class NavigationComponent implements OnInit {
|
||||
showTabbar = true;
|
||||
|
||||
export class NavigationComponent {
|
||||
/**
|
||||
* Name of the app
|
||||
* TODO: What was this for???
|
||||
*/
|
||||
appName = config.appName;
|
||||
showTabBar$ = inject(BreakpointObserver)
|
||||
.observe(['(min-width: 768px)'])
|
||||
.pipe(map(({matches}) => !matches));
|
||||
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
constructor(readonly config: ConfigProvider) {}
|
||||
}
|
||||
|
||||
@@ -24,8 +24,8 @@
|
||||
</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<ion-content *ngIf="menu">
|
||||
<ion-list *ngFor="let category of menu; first as isFirst">
|
||||
<ion-content *ngIf="config.app.menus">
|
||||
<ion-list *ngFor="let category of config.app.menus; first as isFirst">
|
||||
<ion-item
|
||||
*ngIf="category.title !== ''"
|
||||
[rootLink]="category.route"
|
||||
@@ -34,11 +34,11 @@
|
||||
class="menu-category"
|
||||
>
|
||||
<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 *ngFor="let item of category.items" [rootLink]="item.route" [redirectedFrom]="item.route">
|
||||
<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-list>
|
||||
</ion-content>
|
||||
|
||||
@@ -22,10 +22,11 @@ import {IonIconModule} from '../../../util/ion-icon/ion-icon.module';
|
||||
import {TranslateModule} from '@ngx-translate/core';
|
||||
import {RouterModule} from '@angular/router';
|
||||
import {OfflineNoticeComponent} from './offline-notice.component';
|
||||
import {ThingTranslateModule} from '../../../translation/thing-translate.module';
|
||||
|
||||
@NgModule({
|
||||
declarations: [RootLinkDirective, NavigationComponent, TabsComponent, OfflineNoticeComponent],
|
||||
imports: [CommonModule, IonicModule, IonIconModule, TranslateModule, RouterModule],
|
||||
imports: [CommonModule, IonicModule, IonIconModule, TranslateModule, RouterModule, ThingTranslateModule],
|
||||
exports: [TabsComponent, RootLinkDirective, NavigationComponent],
|
||||
})
|
||||
export class NavigationModule {}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -12,93 +12,44 @@
|
||||
* 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 {Component} from '@angular/core';
|
||||
import {NavigationEnd, Router} from '@angular/router';
|
||||
import {
|
||||
SCAppConfigurationMenuCategory,
|
||||
SCLanguage,
|
||||
SCThingTranslator,
|
||||
SCTranslations,
|
||||
} from '@openstapps/core';
|
||||
import {ChangeDetectionStrategy, Component, inject} from '@angular/core';
|
||||
import {Event, NavigationEnd, Router} from '@angular/router';
|
||||
import {SCAppConfigurationMenuCategory} from '@openstapps/core';
|
||||
import {ConfigProvider} from '../../config/config.provider';
|
||||
import {LangChangeEvent, TranslateService} from '@ngx-translate/core';
|
||||
import {NGXLogger} from 'ngx-logger';
|
||||
import {filter} from 'rxjs';
|
||||
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({
|
||||
selector: 'stapps-navigation-tabs',
|
||||
templateUrl: 'tabs.template.html',
|
||||
styleUrls: ['./tabs.component.scss'],
|
||||
templateUrl: 'tabs.html',
|
||||
styleUrls: ['./tabs.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class TabsComponent {
|
||||
/**
|
||||
* Possible languages to be used for translation
|
||||
*/
|
||||
language: keyof SCTranslations<SCLanguage>;
|
||||
menus = inject(ConfigProvider).app.menus.slice(0, 5);
|
||||
|
||||
/**
|
||||
* Menu entries from config module
|
||||
*/
|
||||
menu: SCAppConfigurationMenuCategory[];
|
||||
selectedTab$ = this.router.events.pipe(
|
||||
filter(isNavigationEnd),
|
||||
map(event => findTabFromUrl(event.url, this.menus)),
|
||||
startWith(findTabFromUrl(this.router.url, this.menus)),
|
||||
);
|
||||
|
||||
/**
|
||||
* 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 ?? '';
|
||||
}
|
||||
constructor(private readonly router: Router) {}
|
||||
}
|
||||
|
||||
@@ -33,19 +33,19 @@
|
||||
</ion-tab-bar>
|
||||
-->
|
||||
|
||||
<ion-tab-bar slot="bottom" [selectedTab]="selectedTab">
|
||||
<ion-tab-bar slot="bottom" [selectedTab]="selectedTab$ | async">
|
||||
<ion-menu-toggle>
|
||||
<ion-tab-button class="menu-button">
|
||||
<ion-icon name="menu"></ion-icon>
|
||||
</ion-tab-button>
|
||||
</ion-menu-toggle>
|
||||
<ion-tab-button
|
||||
*ngFor="let category of menu; first as isFirst"
|
||||
*ngFor="let category of menus; first as isFirst"
|
||||
[rootLink]="category.route"
|
||||
[redirectedFrom]="category.route"
|
||||
[tab]="category.title"
|
||||
>
|
||||
<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-bar>
|
||||
@@ -18,9 +18,7 @@ import {FormsModule} from '@angular/forms';
|
||||
import {RouterModule, Routes} from '@angular/router';
|
||||
import {IonicModule} from '@ionic/angular';
|
||||
import {TranslateModule} from '@ngx-translate/core';
|
||||
|
||||
import {ThingTranslateModule} from '../../translation/thing-translate.module';
|
||||
import {ConfigProvider} from '../config/config.provider';
|
||||
import {SettingsItemComponent} from './item/settings-item.component';
|
||||
import {SettingsPageComponent} from './page/settings-page.component';
|
||||
import {SettingTranslatePipe} from './setting-translate.pipe';
|
||||
@@ -60,13 +58,6 @@ const settingsRoutes: Routes = [{path: 'settings', component: SettingsPageCompon
|
||||
RouterModule.forChild(settingsRoutes),
|
||||
UtilModule,
|
||||
],
|
||||
providers: [
|
||||
ScheduleSyncService,
|
||||
ConfigProvider,
|
||||
SettingsProvider,
|
||||
CalendarService,
|
||||
ScheduleProvider,
|
||||
ThingTranslatePipe,
|
||||
],
|
||||
providers: [ScheduleSyncService, SettingsProvider, CalendarService, ScheduleProvider, ThingTranslatePipe],
|
||||
})
|
||||
export class SettingsModule {}
|
||||
|
||||
@@ -16,8 +16,9 @@ import {Injectable} from '@angular/core';
|
||||
import {SCSetting, SCSettingValue, SCSettingValues} from '@openstapps/core';
|
||||
import deepMerge from 'deepmerge';
|
||||
import {Subject} from 'rxjs';
|
||||
import {ConfigProvider} from '../config/config.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_SEPARATOR = '.';
|
||||
@@ -89,19 +90,19 @@ export interface SettingsAction {
|
||||
/**
|
||||
* Provider for app settings
|
||||
*/
|
||||
@Injectable()
|
||||
export class SettingsProvider {
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class SettingsProvider implements BeforeAppInit {
|
||||
/**
|
||||
* Source of settings actions
|
||||
*/
|
||||
private settingsActionSource = new Subject<SettingsAction>();
|
||||
|
||||
private needsInit = true;
|
||||
|
||||
/**
|
||||
* Order of the setting categories
|
||||
*/
|
||||
categoriesOrder: string[];
|
||||
categoriesOrder: string[] = [];
|
||||
|
||||
/**
|
||||
* Settings actions observable
|
||||
@@ -111,7 +112,45 @@ export class SettingsProvider {
|
||||
/**
|
||||
* 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
|
||||
@@ -192,21 +231,11 @@ export class SettingsProvider {
|
||||
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
|
||||
* @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])) {
|
||||
this.provideCategory(setting.categories[0]);
|
||||
}
|
||||
@@ -265,9 +294,7 @@ export class SettingsProvider {
|
||||
/**
|
||||
* Returns copy of cached settings
|
||||
*/
|
||||
public async getCache(): Promise<SettingsCache> {
|
||||
await this.init();
|
||||
|
||||
public getCache(): SettingsCache {
|
||||
return JSON.parse(JSON.stringify(this.settingsCache));
|
||||
}
|
||||
|
||||
@@ -284,8 +311,7 @@ export class SettingsProvider {
|
||||
* @param name the name of requested setting
|
||||
* @throws Exception if setting is not provided
|
||||
*/
|
||||
public async getSetting(category: string, name: string): Promise<SCSetting> {
|
||||
await this.init();
|
||||
public getSetting(category: string, name: string): SCSetting {
|
||||
if (this.settingExists(category, name)) {
|
||||
// return a copy of the settings
|
||||
return JSON.parse(JSON.stringify(this.settingsCache[category].settings[name]));
|
||||
@@ -299,8 +325,7 @@ export class SettingsProvider {
|
||||
* @param name the name of requested setting
|
||||
* @throws Exception if setting is not provided
|
||||
*/
|
||||
public async getValue(category: string, name: string): Promise<SCSettingValue | SCSettingValues> {
|
||||
await this.init();
|
||||
public getValue(category: string, name: string): SCSettingValue | SCSettingValues {
|
||||
if (this.settingExists(category, name)) {
|
||||
// return a copy of the settings 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`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @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> {
|
||||
await this.storage.put(STORAGE_KEY_SETTING_VALUES, {});
|
||||
this.needsInit = true;
|
||||
await this.init();
|
||||
await this.storageProvider.put(STORAGE_KEY_SETTING_VALUES, {});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -383,15 +365,14 @@ export class SettingsProvider {
|
||||
* Saves cached settings in app storage
|
||||
*/
|
||||
public async saveSettingValues(): Promise<void> {
|
||||
if (await this.storage.has(STORAGE_KEY_SETTING_VALUES)) {
|
||||
const savedSettingsValues: SettingValuesContainer = await this.storage.get<SettingValuesContainer>(
|
||||
STORAGE_KEY_SETTING_VALUES,
|
||||
);
|
||||
if (await this.storageProvider.has(STORAGE_KEY_SETTING_VALUES)) {
|
||||
const savedSettingsValues: SettingValuesContainer =
|
||||
await this.storageProvider.get<SettingValuesContainer>(STORAGE_KEY_SETTING_VALUES);
|
||||
const cacheSettingsValues = this.getSettingValuesFromCache();
|
||||
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 {
|
||||
await this.storage.put<SettingValuesContainer>(
|
||||
await this.storageProvider.put<SettingValuesContainer>(
|
||||
STORAGE_KEY_SETTING_VALUES,
|
||||
this.getSettingValuesFromCache(),
|
||||
);
|
||||
@@ -418,7 +399,6 @@ export class SettingsProvider {
|
||||
name: string,
|
||||
value: SCSettingValue | SCSettingValues,
|
||||
): Promise<void> {
|
||||
await this.init();
|
||||
if (this.settingExists(category, name)) {
|
||||
const setting: SCSetting = this.settingsCache[category].settings[name];
|
||||
const isValueValid = SettingsProvider.validateValue(setting, value);
|
||||
|
||||
@@ -14,17 +14,22 @@
|
||||
*/
|
||||
import {Injectable} from '@angular/core';
|
||||
import {Storage} from '@ionic/storage-angular';
|
||||
import {BeforeAppInit} from '../../before-app-init';
|
||||
|
||||
/**
|
||||
* Provides interaction with the (ionic) storage on the device (in the browser)
|
||||
*/
|
||||
@Injectable()
|
||||
export class StorageProvider {
|
||||
@Injectable({providedIn: 'root'})
|
||||
export class StorageProvider implements BeforeAppInit {
|
||||
/**
|
||||
* @param storage TODO
|
||||
*/
|
||||
constructor(private readonly storage: Storage) {}
|
||||
|
||||
async beforeAppInit() {
|
||||
await this.storage.create();
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes storage entries using keys used to save them
|
||||
* @param keys Unique identifiers of the resources for deletion
|
||||
@@ -95,13 +100,6 @@ export class StorageProvider {
|
||||
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
|
||||
*/
|
||||
|
||||
@@ -16,7 +16,6 @@ import {Injectable, OnDestroy, Pipe, PipeTransform} from '@angular/core';
|
||||
import {LangChangeEvent, TranslateService} from '@ngx-translate/core';
|
||||
import moment from 'moment';
|
||||
import {Subscription} from 'rxjs';
|
||||
import {logger} from '../_helpers/ts-logger';
|
||||
|
||||
@Injectable()
|
||||
@Pipe({
|
||||
@@ -163,7 +162,7 @@ export class DurationLocalizedPipe implements PipeTransform, OnDestroy {
|
||||
|
||||
updateValue(value: string | unknown, isFrequency = false): void {
|
||||
if (typeof value !== 'string') {
|
||||
logger.warn(`durationLocalized pipe unable to parse input: ${value}`);
|
||||
console.warn('durationLocalized pipe unable to parse input', value);
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -223,7 +222,7 @@ export class MetersLocalizedPipe implements PipeTransform, OnDestroy {
|
||||
|
||||
updateValue(value: string | number | unknown) {
|
||||
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;
|
||||
}
|
||||
@@ -321,7 +320,7 @@ export class NumberLocalizedPipe implements PipeTransform, OnDestroy {
|
||||
|
||||
updateValue(value: string | number | unknown, formatOptions?: string): void {
|
||||
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;
|
||||
}
|
||||
@@ -387,7 +386,7 @@ export class DateLocalizedFormatPipe implements PipeTransform, OnDestroy {
|
||||
|
||||
updateValue(value: string | Date | unknown, formatOptions?: string): void {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
race,
|
||||
RetryConfig,
|
||||
share,
|
||||
skip,
|
||||
Subject,
|
||||
takeUntil,
|
||||
} from 'rxjs';
|
||||
@@ -61,7 +62,7 @@ export class InternetConnectionService {
|
||||
private doRetry(error: unknown, retryCount: number): ObservableInput<unknown> {
|
||||
return race(
|
||||
this.offline$.pipe(
|
||||
tap(it => console.log(it)),
|
||||
skip(1),
|
||||
filter(it => !it),
|
||||
take(1),
|
||||
delay(Math.min(retryCount ** 4 + 100, 10_000)),
|
||||
|
||||
@@ -21,7 +21,7 @@ export const environment = {
|
||||
backend_url: 'https://mobile.server.uni-frankfurt.de',
|
||||
app_host: 'mobile.app.uni-frankfurt.de',
|
||||
custom_url_scheme: 'de.anyschool.app',
|
||||
backend_version: '4.0.0',
|
||||
backend_version: '0.0.0',
|
||||
production: false,
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user