mirror of
https://gitlab.com/openstapps/openstapps.git
synced 2026-10-21 16:59:40 +00:00
Compare commits
18
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f3f6181c29 | ||
|
|
13e95c96d0 | ||
|
|
ae1cad3bb5 | ||
|
|
93c38b749c | ||
|
|
3547ad19f6 | ||
|
|
d5fbd8654c | ||
|
|
c9ada93f87 | ||
|
|
6d782ecb10 | ||
|
|
1cc3ffedf8 | ||
|
|
05a0525320 | ||
|
|
3c08704345 | ||
|
|
6b17416d61 | ||
|
|
019f80134d | ||
|
|
ba728d551b | ||
|
|
c6dfbd6add | ||
|
|
27877c6607 | ||
|
|
f552586b50 | ||
|
|
4c3f3ef14c |
No files matched your search
@@ -1,5 +1,23 @@
|
||||
# @openstapps/backend
|
||||
|
||||
## 4.0.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- ae1cad3b: upgrade project to Typescript 5.9
|
||||
- 93c38b74: dependency updates
|
||||
- Updated dependencies [ae1cad3b]
|
||||
- Updated dependencies [93c38b74]
|
||||
- @openstapps/core-tools@4.0.7
|
||||
- @openstapps/core@4.0.7
|
||||
- @openstapps/logger@4.0.7
|
||||
|
||||
## 4.0.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 019f8013: Enable ID cards and assessments using Shibboleth as identity provider
|
||||
|
||||
## 4.0.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -13,23 +13,24 @@ const config = {
|
||||
default: {
|
||||
client: {
|
||||
clientId: '1cac3f99-33fa-4234-8438-979f07e0cdab',
|
||||
scopes: '',
|
||||
url: 'https://cas.rz.uni-frankfurt.de/cas/oauth2.0',
|
||||
scopes: 'openid profile email offline_access',
|
||||
url: 'https://idp.ub.uni-frankfurt.de/idp/profile/oidc',
|
||||
},
|
||||
endpoints: {
|
||||
authorization: 'https://cas.rz.uni-frankfurt.de/cas/oauth2.0/authorize',
|
||||
endSession: 'https://cas.rz.uni-frankfurt.de/cas/logout',
|
||||
authorization: 'https://idp.ub.uni-frankfurt.de/idp/profile/oidc/authorize',
|
||||
endSession: 'https://idp.ub.uni-frankfurt.de/idp/profile/oidc/end-session',
|
||||
mapping: {
|
||||
id: '$.id',
|
||||
email: '$.attributes.mailPrimaryAddress',
|
||||
familyName: '$.attributes.sn',
|
||||
givenName: '$.attributes.givenName',
|
||||
name: '$.attributes.givenName',
|
||||
role: '$.attributes.eduPersonPrimaryAffiliation',
|
||||
studentId: '$.attributes.employeeNumber',
|
||||
id: '$.preferred_username',
|
||||
email: '$.mailPrimaryAddress',
|
||||
familyName: '$.family_name',
|
||||
givenName: '$.given_name',
|
||||
name: '$.name',
|
||||
role: '$.eduPersonPrimaryAffiliation',
|
||||
studentId: '$.employeeNumber',
|
||||
},
|
||||
token: 'https://cas.rz.uni-frankfurt.de/cas/oauth2.0/accessToken',
|
||||
userinfo: 'https://cas.rz.uni-frankfurt.de/cas/oauth2.0/profile',
|
||||
token: 'https://idp.ub.uni-frankfurt.de/idp/profile/oidc/token',
|
||||
userinfo: 'https://idp.ub.uni-frankfurt.de/idp/profile/oidc/userinfo',
|
||||
revoke: 'https://idp.ub.uni-frankfurt.de/idp/profile/oauth2/revocation',
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -39,12 +40,12 @@ const config = {
|
||||
extern: {
|
||||
hisometry: {
|
||||
authProvider: 'default',
|
||||
url: 'https://his-self-service.rz.uni-frankfurt.de',
|
||||
url: 'https://his-self-service.rz.uni-frankfurt.de/v2',
|
||||
},
|
||||
/** TODO: idCards: {
|
||||
idCards: {
|
||||
authProvider: 'default',
|
||||
url: 'TODO',
|
||||
} */
|
||||
url: 'https://id-cards.rz.uni-frankfurt.de/v2/id-cards',
|
||||
},
|
||||
},
|
||||
},
|
||||
versionHistory,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@openstapps/backend",
|
||||
"description": "A reference implementation for a StApps backend",
|
||||
"version": "4.0.5",
|
||||
"version": "4.0.7",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "AGPL-3.0-only",
|
||||
@@ -53,7 +53,6 @@
|
||||
"@types/geojson": "1.0.6",
|
||||
"@types/node": "22.15.31",
|
||||
"@types/node-cron": "3.0.7",
|
||||
"@types/nodemailer": "8.0.2",
|
||||
"@types/promise-queue": "2.2.0",
|
||||
"@types/uuid": "8.3.4",
|
||||
"body-parser": "1.20.3",
|
||||
@@ -63,13 +62,13 @@
|
||||
"express": "4.21.2",
|
||||
"express-prom-bundle": "6.6.0",
|
||||
"express-promise-router": "4.1.1",
|
||||
"got": "12.6.0",
|
||||
"got": "14.6.6",
|
||||
"moment": "2.30.1",
|
||||
"morgan": "1.10.1",
|
||||
"nock": "13.3.1",
|
||||
"node-cache": "5.1.2",
|
||||
"node-cron": "3.0.2",
|
||||
"nodemailer": "8.0.11",
|
||||
"nodemailer": "10.1.7",
|
||||
"prom-client": "14.1.1",
|
||||
"promise-queue": "2.2.5",
|
||||
"uuid": "8.3.2"
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# @openstapps/database
|
||||
|
||||
## 4.0.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- ae1cad3b: upgrade project to Typescript 5.9
|
||||
|
||||
## 4.0.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openstapps/database",
|
||||
"version": "4.0.1",
|
||||
"version": "4.0.7",
|
||||
"private": true,
|
||||
"files": [
|
||||
"config",
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
# @openstapps/proxy
|
||||
|
||||
## 4.0.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- ae1cad3b: upgrade project to Typescript 5.9
|
||||
- 93c38b74: dependency updates
|
||||
- Updated dependencies [ae1cad3b]
|
||||
- Updated dependencies [93c38b74]
|
||||
- @openstapps/logger@4.0.7
|
||||
|
||||
## 4.0.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@openstapps/proxy",
|
||||
"description": "NGINX proxy that is dynamically configured by a Node.js script",
|
||||
"version": "4.0.5",
|
||||
"version": "4.0.7",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "AGPL-3.0-only",
|
||||
@@ -43,11 +43,11 @@
|
||||
"dependencies": {
|
||||
"@openstapps/logger": "workspace:*",
|
||||
"@types/config": "3.3.0",
|
||||
"@types/dockerode": "3.3.17",
|
||||
"@types/dockerode": "4.0.2",
|
||||
"@types/node": "22.15.31",
|
||||
"@types/sha1": "1.1.3",
|
||||
"config": "3.3.9",
|
||||
"dockerode": "3.3.5",
|
||||
"dockerode": "5.0.1",
|
||||
"is-cidr": "4.0.2",
|
||||
"mustache": "4.2.0",
|
||||
"semver": "7.6.0",
|
||||
@@ -60,7 +60,7 @@
|
||||
"@openstapps/tsconfig": "workspace:*",
|
||||
"@types/chai": "4.3.20",
|
||||
"@types/config": "3.3.0",
|
||||
"@types/dockerode": "3.3.17",
|
||||
"@types/dockerode": "4.0.2",
|
||||
"@types/mocha": "10.0.10",
|
||||
"@types/mustache": "4.2.2",
|
||||
"@types/node": "22.15.31",
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# @openstapps/backend-config
|
||||
|
||||
## 4.0.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- ae1cad3b: upgrade project to Typescript 5.9
|
||||
|
||||
## 4.0.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@openstapps/backend-config",
|
||||
"description": "Backend Configuration for OpenStApps",
|
||||
"version": "4.0.3",
|
||||
"version": "4.0.7",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "GPL-3.0-only",
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# @openstapps/eslint-config
|
||||
|
||||
## 4.0.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- ae1cad3b: upgrade project to Typescript 5.9
|
||||
|
||||
## 4.0.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@openstapps/eslint-config",
|
||||
"description": "A collection of configuration base files for StApps projects. Just an (unused) experiment for now.",
|
||||
"version": "4.0.3",
|
||||
"version": "4.0.7",
|
||||
"type": "commonjs",
|
||||
"license": "GPL-3.0-only",
|
||||
"repository": "git@gitlab.com:openstapps/eslint-config.git",
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# @openstapps/prettier-config
|
||||
|
||||
## 4.0.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- ae1cad3b: upgrade project to Typescript 5.9
|
||||
|
||||
## 4.0.0
|
||||
|
||||
### Major Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@openstapps/prettier-config",
|
||||
"description": "StApps Prettier Config",
|
||||
"version": "4.0.0",
|
||||
"version": "4.0.7",
|
||||
"type": "commonjs",
|
||||
"license": "GPL-3.0-only",
|
||||
"repository": "git@gitlab.com:openstapps/prettier-config.git",
|
||||
|
||||
@@ -1,5 +1,16 @@
|
||||
# @openstapps/projectmanagement
|
||||
|
||||
## 4.0.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- ae1cad3b: upgrade project to Typescript 5.9
|
||||
- Updated dependencies [ae1cad3b]
|
||||
- Updated dependencies [93c38b74]
|
||||
- @openstapps/collection-utils@4.0.7
|
||||
- @openstapps/gitlab-api@4.0.7
|
||||
- @openstapps/logger@4.0.7
|
||||
|
||||
## 4.0.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@openstapps/projectmanagement",
|
||||
"description": "Main documentation and scripts for maintenance.",
|
||||
"version": "4.0.5",
|
||||
"version": "4.0.7",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "GPL-3.0-only",
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# @openstapps/tsconfig
|
||||
|
||||
## 4.0.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- ae1cad3b: upgrade project to Typescript 5.9
|
||||
|
||||
## 4.0.0
|
||||
|
||||
### Major Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@openstapps/tsconfig",
|
||||
"description": "The tsconfig for the openstapps project",
|
||||
"version": "4.0.0",
|
||||
"version": "4.0.7",
|
||||
"type": "commonjs",
|
||||
"license": "GPL-3.0-only",
|
||||
"repository": "git@gitlab.com:openstapps/eslint-config.git",
|
||||
|
||||
@@ -1,5 +1,16 @@
|
||||
# @openstapps/minimal-connector
|
||||
|
||||
## 4.0.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- ae1cad3b: upgrade project to Typescript 5.9
|
||||
- Updated dependencies [ae1cad3b]
|
||||
- Updated dependencies [93c38b74]
|
||||
- @openstapps/core@4.0.7
|
||||
- @openstapps/api@4.0.7
|
||||
- @openstapps/logger@4.0.7
|
||||
|
||||
## 4.0.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@openstapps/minimal-connector",
|
||||
"description": "This is a minimal connector which serves as an example",
|
||||
"version": "4.0.5",
|
||||
"version": "4.0.7",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "GPL-3.0-only",
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# @openstapps/minimal-deployment
|
||||
|
||||
## 4.0.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- ae1cad3b: upgrade project to Typescript 5.9
|
||||
|
||||
## 4.0.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openstapps/minimal-deployment",
|
||||
"version": "4.0.1",
|
||||
"version": "4.0.7",
|
||||
"private": true,
|
||||
"files": [
|
||||
"database",
|
||||
|
||||
@@ -1,5 +1,18 @@
|
||||
# @openstapps/minimal-plugin
|
||||
|
||||
## 4.0.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- ae1cad3b: upgrade project to Typescript 5.9
|
||||
- Updated dependencies [ae1cad3b]
|
||||
- Updated dependencies [93c38b74]
|
||||
- @openstapps/api-plugin@4.0.7
|
||||
- @openstapps/core-tools@4.0.7
|
||||
- @openstapps/core@4.0.7
|
||||
- @openstapps/api@4.0.7
|
||||
- @openstapps/logger@4.0.7
|
||||
|
||||
## 4.0.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@openstapps/minimal-plugin",
|
||||
"description": "Minimal Plugin",
|
||||
"version": "4.0.5",
|
||||
"version": "4.0.7",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "GPL-3.0-only",
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
# @openstapps/app
|
||||
|
||||
## 4.0.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- ae1cad3b: upgrade project to Typescript 5.9
|
||||
- 93c38b74: dependency updates
|
||||
- Updated dependencies [ae1cad3b]
|
||||
- Updated dependencies [93c38b74]
|
||||
- @openstapps/core@4.0.7
|
||||
- @openstapps/api@4.0.7
|
||||
- @openstapps/collection-utils@4.0.7
|
||||
|
||||
## 4.0.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* Copyright (C) 2026 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/>.
|
||||
*/
|
||||
|
||||
/*
|
||||
* tsconfig.spec.json maps every `@capacitor/*` import to this folder, so this
|
||||
* file replaces `@capacitor/core` in all tests – including the imports made by
|
||||
* real Capacitor plugins (registerPlugin, WebPlugin, CapacitorHttp, …).
|
||||
*
|
||||
* Therefore everything is re-exported from the real package (imported via a
|
||||
* relative path, which bypasses the path mapping) and only `Capacitor` is
|
||||
* replaced by a controllable mock. The local export shadows the one from
|
||||
* `export *`.
|
||||
*/
|
||||
export * from '../../node_modules/@capacitor/core';
|
||||
|
||||
type Platform = 'web' | 'ios' | 'android';
|
||||
|
||||
export class CapacitorMock {
|
||||
private platform: Platform = 'web';
|
||||
|
||||
getPlatform(): Platform {
|
||||
return this.platform;
|
||||
}
|
||||
|
||||
isNativePlatform(): boolean {
|
||||
return this.platform !== 'web';
|
||||
}
|
||||
|
||||
isPluginAvailable(_name: string): boolean {
|
||||
return this.isNativePlatform();
|
||||
}
|
||||
|
||||
convertFileSrc(filePath: string): string {
|
||||
return filePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test helper: simulate running on the given platform.
|
||||
*/
|
||||
setPlatform(platform: Platform): void {
|
||||
this.platform = platform;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test helper: back to the default (web), call this in afterEach.
|
||||
*/
|
||||
reset(): void {
|
||||
this.platform = 'web';
|
||||
}
|
||||
}
|
||||
|
||||
export const Capacitor = new CapacitorMock();
|
||||
@@ -1,14 +1,14 @@
|
||||
{
|
||||
"images" : [
|
||||
"images": [
|
||||
{
|
||||
"filename" : "AppIcon-512@2x.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
"idiom": "universal",
|
||||
"size": "1024x1024",
|
||||
"filename": "AppIcon-512@2x.png",
|
||||
"platform": "ios"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
"info": {
|
||||
"author": "xcode",
|
||||
"version": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@openstapps/app",
|
||||
"description": "The generic app tailored to fulfill needs of German universities, written using Ionic Framework.",
|
||||
"version": "4.0.5",
|
||||
"version": "4.0.7",
|
||||
"private": true,
|
||||
"license": "GPL-3.0-only",
|
||||
"author": "Karl-Philipp Wulfert <krlwlfrt@gmail.com>",
|
||||
@@ -38,7 +38,7 @@
|
||||
"minify-icons": "node scripts/minify-icon-font.mjs",
|
||||
"preview": "http-server www/browser --p 8101 -o",
|
||||
"push": "git push && git push origin \"v$npm_package_version\"",
|
||||
"resources:ios": "capacitor-assets generate --ios --iconBackgroundColor $(grep -oE \"^@include ion-color\\(primary, #[a-fA-F0-9]{3,6}\" src/theme/colors.scss | grep -oE \"#[a-fA-F0-9]{3,6}\") --splashBackgroundColor $(grep -oE \"^@include ion-color\\(primary, #[a-fA-F0-9]{3,6}\" src/theme/colors.scss | grep -oE \"#[a-fA-F0-9]{3,6}\")",
|
||||
"resources:ios": "capacitor-assets generate --ios --iconBackgroundColor $(grep -oE \"^@include color-system\\.ion-color\\(primary, #[a-fA-F0-9]{3,6}\" src/theme/colors.scss | grep -oE \"#[a-fA-F0-9]{3,6}\") --splashBackgroundColor $(grep -oE \"^@include color-system\\.ion-color\\(primary, #[a-fA-F0-9]{3,6}\" src/theme/colors.scss | grep -oE \"#[a-fA-F0-9]{3,6}\")",
|
||||
"run:android": "ionic capacitor run android --livereload --external",
|
||||
"start": "ionic serve",
|
||||
"start:external": "ionic serve --external",
|
||||
@@ -48,13 +48,13 @@
|
||||
"test:integration": "sh integration-test.sh"
|
||||
},
|
||||
"dependencies": {
|
||||
"@angular/animations": "20.3.31",
|
||||
"@angular/animations": "20.3.33",
|
||||
"@angular/cdk": "20.2.14",
|
||||
"@angular/common": "20.3.31",
|
||||
"@angular/core": "20.3.31",
|
||||
"@angular/forms": "20.3.31",
|
||||
"@angular/platform-browser": "20.3.31",
|
||||
"@angular/router": "20.3.31",
|
||||
"@angular/common": "20.3.33",
|
||||
"@angular/core": "20.3.33",
|
||||
"@angular/forms": "20.3.33",
|
||||
"@angular/platform-browser": "20.3.33",
|
||||
"@angular/router": "20.3.33",
|
||||
"@awesome-cordova-plugins/calendar": "8.1.0",
|
||||
"@awesome-cordova-plugins/core": "8.1.0",
|
||||
"@capacitor-community/in-app-review": "8.0.0",
|
||||
@@ -81,7 +81,7 @@
|
||||
"@maplibre/ngx-maplibre-gl": "22.1.0",
|
||||
"@ngx-translate/core": "15.0.0",
|
||||
"@ngx-translate/http-loader": "8.0.0",
|
||||
"@openid/appauth": "1.3.2",
|
||||
"@openid/appauth": "1.4.0",
|
||||
"@openstapps/api": "workspace:*",
|
||||
"@openstapps/collection-utils": "workspace:*",
|
||||
"@openstapps/core": "workspace:*",
|
||||
@@ -93,7 +93,6 @@
|
||||
"deepmerge": "4.3.1",
|
||||
"form-data": "4.0.6",
|
||||
"geojson": "0.5.0",
|
||||
"ionic-appauth": "2.1.0",
|
||||
"ionicons": "8.1.0",
|
||||
"jsonpath-plus": "10.3.0",
|
||||
"maplibre-gl": "6.4.1",
|
||||
|
||||
@@ -73,7 +73,6 @@ export const sampleAggregations: SCBackendAggregationConfiguration[] = [
|
||||
|
||||
export const sampleAuthConfiguration: {
|
||||
default: SCAuthorizationProvider;
|
||||
paia: SCAuthorizationProvider;
|
||||
} = {
|
||||
default: {
|
||||
client: {clientId: '', scopes: '', url: ''},
|
||||
@@ -84,15 +83,6 @@ export const sampleAuthConfiguration: {
|
||||
userinfo: '',
|
||||
},
|
||||
},
|
||||
paia: {
|
||||
client: {clientId: '', scopes: '', url: ''},
|
||||
endpoints: {
|
||||
authorization: '',
|
||||
mapping: {id: '', name: ''},
|
||||
token: '',
|
||||
userinfo: '',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const sampleDefaultPolygon: Polygon = {
|
||||
|
||||
@@ -18,7 +18,11 @@ import {AnimationOptions} from '@ionic/angular/common/providers/nav-controller';
|
||||
import {iosDuration, iosEasing, mdDuration, mdEasing} from './easings';
|
||||
|
||||
/**
|
||||
*
|
||||
* Creates the page transition used when opening a page via a FAB: the FAB grows
|
||||
* towards the screen while the new page fades in on top of it. Going back, the
|
||||
* page fades out and reveals the previous page beneath it.
|
||||
* @param animationController Ionic animation controller used to build the animation
|
||||
* @returns animation builder to pass to `[routerAnimation]`
|
||||
*/
|
||||
export function fabExpand(animationController: AnimationController): AnimationBuilder {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
@@ -37,18 +41,11 @@ export function fabExpand(animationController: AnimationController): AnimationBu
|
||||
|
||||
const useReducedMotion = viewBounds.width > 500;
|
||||
const reducedMotionTransform = `${Math.min(viewBounds.width * 0.3, 200)}px`;
|
||||
const reducedMotionViewBorderRadius = '128px';
|
||||
const reducedMotionFabGrow = '2';
|
||||
const reducedMotionViewShrink = '0.9';
|
||||
|
||||
const viewCenterX = (viewBounds.width - viewBounds.x) / 2;
|
||||
const viewCenterY = (viewBounds.height - viewBounds.y) / 2;
|
||||
|
||||
const viewOnFab = useReducedMotion
|
||||
? `translate(${reducedMotionTransform}, ${reducedMotionTransform}) scale(${reducedMotionViewShrink})`
|
||||
: `translate(${(fabBounds.x - viewBounds.x) / 2}px, ${(fabBounds.y - viewBounds.y) / 2}px) scale(${
|
||||
fabBounds.width / viewBounds.width
|
||||
}, ${fabBounds.height / viewBounds.height})`;
|
||||
const fabOnView = useReducedMotion
|
||||
? `translate(-${reducedMotionTransform}, -${reducedMotionTransform}) scale(${reducedMotionFabGrow})`
|
||||
: `translate(${viewCenterX - fabBounds.x}px, ${viewCenterY - fabBounds.y}px) scale(${
|
||||
@@ -56,8 +53,6 @@ export function fabExpand(animationController: AnimationController): AnimationBu
|
||||
}, ${viewBounds.height / fabBounds.height})`;
|
||||
const transformNormal = `translate(0px, 0px) scale(1, 1)`;
|
||||
|
||||
const viewBorderRadius = useReducedMotion ? reducedMotionViewBorderRadius : '50%';
|
||||
|
||||
const fabViewFade = animationController
|
||||
.create()
|
||||
.beforeStyles({zIndex: -1})
|
||||
@@ -69,13 +64,12 @@ export function fabExpand(animationController: AnimationController): AnimationBu
|
||||
.fromTo('transform', back ? fabOnView : transformNormal, back ? transformNormal : fabOnView)
|
||||
.fromTo('opacity', back ? '0' : '1', back ? '1' : '0')
|
||||
.fromTo('borderRadius', back ? '0' : '50%', back ? '50%' : '0')
|
||||
.addElement(fab);
|
||||
.addElement(back ? [] : fab);
|
||||
const viewGrow = animationController
|
||||
.create()
|
||||
.beforeStyles({zIndex: 200, overflow: 'hidden', transformOrigin: 'center'})
|
||||
.fromTo('transform', back ? transformNormal : viewOnFab, back ? viewOnFab : transformNormal)
|
||||
.fromTo('opacity', back ? '1' : '0', back ? '0' : '1')
|
||||
.fromTo('borderRadius', back ? '0' : viewBorderRadius, back ? viewBorderRadius : '0')
|
||||
.afterStyles(back ? {display: 'none'} : {})
|
||||
.addElement(otherView);
|
||||
|
||||
return rootTransition.addAnimation(fabGrow).addAnimation(viewGrow).addAnimation(fabViewFade);
|
||||
|
||||
@@ -18,16 +18,18 @@ import {App, URLOpenListenerEvent} from '@capacitor/app';
|
||||
import {Platform, ToastController} from '@ionic/angular/standalone';
|
||||
import {SettingsProvider} from './modules/settings/settings.provider';
|
||||
import {InAppReviewProvider} from './modules/settings/in-app-review/in-app-review.provider';
|
||||
import {AuthHelperService} from './modules/auth/auth-helper.service';
|
||||
import {AuthActions, AuthProvider} from './modules/auth/auth.provider';
|
||||
import {environment} from '../environments/environment';
|
||||
import {Capacitor} from '@capacitor/core';
|
||||
import {ScheduleSyncService} from './modules/background/schedule/schedule-sync.service';
|
||||
import {Keyboard, KeyboardResize} from '@capacitor/keyboard';
|
||||
import {AppVersionService} from './modules/about/app-version.service';
|
||||
import {SplashScreen} from '@capacitor/splash-screen';
|
||||
import {AuthHelperService} from './modules/auth/auth-helper.service';
|
||||
|
||||
/**
|
||||
* TODO
|
||||
* Root component of the application responsible for global initializations,
|
||||
* deep linking, splash screen management, and global event bindings.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
@@ -45,6 +47,8 @@ export class AppComponent implements AfterContentInit {
|
||||
|
||||
private readonly zone = inject(NgZone);
|
||||
|
||||
private readonly authProvider = inject(AuthProvider);
|
||||
|
||||
private readonly authHelper = inject(AuthHelperService);
|
||||
|
||||
private readonly toastController = inject(ToastController);
|
||||
@@ -129,19 +133,21 @@ export class AppComponent implements AfterContentInit {
|
||||
}
|
||||
|
||||
private async authNotificationsInit() {
|
||||
this.authHelper
|
||||
.getProvider()
|
||||
.events$.subscribe(action => this.showMessage(this.authHelper.getAuthMessage('default', action)));
|
||||
this.authProvider.events$.subscribe(action => {
|
||||
const isWarning = action.action === AuthActions.SessionExpired;
|
||||
|
||||
void this.showMessage(this.authHelper.getAuthMessage(action), isWarning ? 'warning' : 'success');
|
||||
});
|
||||
}
|
||||
|
||||
private async showMessage(message?: string) {
|
||||
private async showMessage(message: string | undefined, color: 'success' | 'warning' = 'success') {
|
||||
if (message === undefined) {
|
||||
return;
|
||||
}
|
||||
const toast = await this.toastController.create({
|
||||
message: message,
|
||||
duration: 2000,
|
||||
color: 'success',
|
||||
duration: color === 'warning' ? 4000 : 2000,
|
||||
color,
|
||||
});
|
||||
await toast.present();
|
||||
}
|
||||
|
||||
@@ -57,7 +57,6 @@ 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 {NavigationModule} from './modules/menu/navigation/navigation.module';
|
||||
import {browserFactory, SimpleBrowser} from './util/browser.factory';
|
||||
import {getDateFnsLocale} from './translation/dfns-locale';
|
||||
@@ -67,6 +66,7 @@ import {Capacitor} from '@capacitor/core';
|
||||
import {SplashScreen} from '@capacitor/splash-screen';
|
||||
import * as maplibregl from 'maplibre-gl';
|
||||
import {Protocol} from 'pmtiles';
|
||||
import {AuthProvider} from './modules/auth/auth.provider';
|
||||
|
||||
registerLocaleData(localeDe);
|
||||
|
||||
@@ -82,7 +82,7 @@ export function initializerFactory(
|
||||
configProvider: ConfigProvider,
|
||||
translateService: TranslateService,
|
||||
_routingStackService: RoutingStackService,
|
||||
defaultAuthService: DefaultAuthService,
|
||||
authProvider: AuthProvider,
|
||||
dateFnsConfigurationService: DateFnsConfigurationService,
|
||||
) {
|
||||
return async () => {
|
||||
@@ -111,7 +111,7 @@ export function initializerFactory(
|
||||
setDefaultOptions({locale: dateFnsLocale});
|
||||
dateFnsConfigurationService.setLocale(dateFnsLocale);
|
||||
|
||||
await defaultAuthService.init();
|
||||
await authProvider.init();
|
||||
} catch (error) {
|
||||
logger.warn(error);
|
||||
}
|
||||
@@ -201,7 +201,7 @@ export function createTranslateLoader(http: HttpClient) {
|
||||
ConfigProvider,
|
||||
TranslateService,
|
||||
RoutingStackService,
|
||||
DefaultAuthService,
|
||||
AuthProvider,
|
||||
DateFnsConfigurationService,
|
||||
],
|
||||
useFactory: initializerFactory,
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
.release-notes {
|
||||
max-width: 16cm;
|
||||
}
|
||||
|
||||
.release-notes ::ng-deep {
|
||||
h1 {
|
||||
font-size: 1.5em;
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
* Copyright (C) 2026 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 {TestBed} from '@angular/core/testing';
|
||||
import {HttpClient} from '@angular/common/http';
|
||||
import {BehaviorSubject, of} from 'rxjs';
|
||||
import {SCAssessment} from '@openstapps/core';
|
||||
import {AssessmentsProvider} from './assessments.provider';
|
||||
import {ConfigProvider} from '../config/config.provider';
|
||||
import {AuthActions, AuthProvider, IAuthAction} from '../auth/auth.provider';
|
||||
|
||||
class FakeAuth {
|
||||
events$ = new BehaviorSubject<IAuthAction>({action: AuthActions.Init});
|
||||
|
||||
accessToken = 'token-a';
|
||||
|
||||
getValidToken() {
|
||||
return Promise.resolve({accessToken: this.accessToken});
|
||||
}
|
||||
}
|
||||
|
||||
const gradesA = [{uid: 'a', name: 'Grade of A'}] as unknown as SCAssessment[];
|
||||
const gradesB = [{uid: 'b', name: 'Grade of B'}] as unknown as SCAssessment[];
|
||||
|
||||
describe('AssessmentsProvider', () => {
|
||||
let provider: AssessmentsProvider;
|
||||
let fakeAuth: FakeAuth;
|
||||
let httpClient: jasmine.SpyObj<HttpClient>;
|
||||
|
||||
beforeEach(() => {
|
||||
fakeAuth = new FakeAuth();
|
||||
httpClient = jasmine.createSpyObj<HttpClient>('HttpClient', ['get']);
|
||||
httpClient.get.and.callFake(((_url: string, options: {headers: {Authorization: string}}) =>
|
||||
of({data: options.headers.Authorization === 'Bearer token-a' ? gradesA : gradesB})) as never);
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
AssessmentsProvider,
|
||||
{
|
||||
provide: ConfigProvider,
|
||||
useValue: {config: {app: {features: {extern: {hisometry: {url: 'http://hisometry.local'}}}}}},
|
||||
},
|
||||
{provide: AuthProvider, useValue: fakeAuth},
|
||||
{provide: HttpClient, useValue: httpClient},
|
||||
],
|
||||
});
|
||||
|
||||
provider = TestBed.inject(AssessmentsProvider);
|
||||
});
|
||||
|
||||
it('should use the cache while the same user is logged in', async () => {
|
||||
await provider.getAssessments();
|
||||
fakeAuth.events$.next({action: AuthActions.RefreshSuccess});
|
||||
|
||||
expect(await provider.getAssessments()).toEqual(gradesA);
|
||||
expect(httpClient.get).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
for (const action of [AuthActions.SignOutSuccess, AuthActions.SessionExpired]) {
|
||||
it(`should clear the cache on ${action}`, async () => {
|
||||
await provider.getAssessments();
|
||||
fakeAuth.events$.next({action});
|
||||
|
||||
expect(provider.cache).toBeUndefined();
|
||||
expect(await provider.assessments).toEqual({});
|
||||
});
|
||||
}
|
||||
|
||||
it('should not show the grades of the previous user after a user change', async () => {
|
||||
expect(await provider.getAssessments()).toEqual(gradesA);
|
||||
|
||||
fakeAuth.events$.next({action: AuthActions.SignOutSuccess});
|
||||
fakeAuth.accessToken = 'token-b';
|
||||
fakeAuth.events$.next({action: AuthActions.SignInSuccess});
|
||||
|
||||
expect(await provider.getAssessments()).toEqual(gradesB);
|
||||
expect(httpClient.get).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
@@ -15,9 +15,9 @@
|
||||
import {Injectable, inject} 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 {AuthActions, AuthProvider} from '../auth/auth.provider';
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -53,10 +53,28 @@ export function toAssessmentMap(data: SCAssessment[]): Record<SCUuid, SCAssessme
|
||||
export class AssessmentsProvider {
|
||||
readonly configProvider = inject(ConfigProvider);
|
||||
|
||||
readonly defaultAuth = inject(DefaultAuthService);
|
||||
readonly authProvider = inject(AuthProvider);
|
||||
|
||||
readonly http = inject(HttpClient);
|
||||
|
||||
constructor() {
|
||||
/*
|
||||
* The grades belong to the logged-in user. The next user
|
||||
* should never get the previous user's grades from the cache.
|
||||
*/
|
||||
this.authProvider.events$.subscribe(({action}) => {
|
||||
switch (action) {
|
||||
case AuthActions.SignInSuccess:
|
||||
case AuthActions.SignOutSuccess:
|
||||
case AuthActions.SignOutFailed:
|
||||
case AuthActions.SessionExpired: {
|
||||
this.clearCache();
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
assessmentPath = 'assessments';
|
||||
|
||||
// usually this wouldn't be necessary, but the assessment service
|
||||
@@ -97,7 +115,7 @@ export class AssessmentsProvider {
|
||||
this.cache = this.http
|
||||
.get<{data: SCAssessment[]}>(`${url}/${this.assessmentPath}`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken ?? (await this.defaultAuth.getValidToken()).accessToken}`,
|
||||
Authorization: `Bearer ${accessToken ?? (await this.authProvider.getValidToken()).accessToken}`,
|
||||
},
|
||||
})
|
||||
.toPromise()
|
||||
@@ -110,4 +128,13 @@ export class AssessmentsProvider {
|
||||
|
||||
return this.cache;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all cached grades.
|
||||
*/
|
||||
clearCache(): void {
|
||||
this.cache = undefined;
|
||||
this.assessments = Promise.resolve({});
|
||||
this.cacheTimestamp = 0;
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,7 @@ import {DataProvider, DataScope} from '../../data/data.provider';
|
||||
import {DataRoutingService} from '../../data/data-routing.service';
|
||||
import {groupBy, mapValues} from '@openstapps/collection-utils';
|
||||
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
|
||||
import {environment} from '../../../../environments/environment';
|
||||
|
||||
@Component({
|
||||
selector: 'app-assessments-page',
|
||||
@@ -62,6 +63,26 @@ export class AssessmentsPageComponent implements OnInit, AfterViewInit {
|
||||
|
||||
destroy$ = inject(DestroyRef);
|
||||
|
||||
/**
|
||||
* ?token=... (e.g. ?token=mock) is a development aid only.
|
||||
* In production, no token is read from or passed on in URLs.
|
||||
*/
|
||||
private readonly urlTokenAllowed = !environment.production;
|
||||
|
||||
/**
|
||||
* Token from the URL (?token=...), development only.
|
||||
* Always undefined in production, so no token is read from or passed on in URLs.
|
||||
*/
|
||||
get urlToken(): string | undefined {
|
||||
if (!this.urlTokenAllowed) {
|
||||
return;
|
||||
}
|
||||
|
||||
const token = this.activatedRoute.snapshot.queryParamMap.get('token');
|
||||
|
||||
return token === null ? undefined : token;
|
||||
}
|
||||
|
||||
ngAfterViewInit() {
|
||||
this.segmentView.value = this.sharedAxisChoreographer.currentValue;
|
||||
}
|
||||
@@ -73,7 +94,7 @@ export class AssessmentsPageComponent implements OnInit, AfterViewInit {
|
||||
.subscribe(thing => {
|
||||
void this.router.navigate(['assessments', 'detail', thing.uid], {
|
||||
queryParams: {
|
||||
token: this.activatedRoute.snapshot.queryParamMap.get('token'),
|
||||
token: this.urlToken,
|
||||
},
|
||||
state: {item: thing},
|
||||
});
|
||||
@@ -82,7 +103,7 @@ export class AssessmentsPageComponent implements OnInit, AfterViewInit {
|
||||
this.activatedRoute.queryParams.subscribe(parameters => {
|
||||
try {
|
||||
this.assessments = this.assessmentsProvider
|
||||
.getAssessments(parameters.token)
|
||||
.getAssessments(this.urlTokenAllowed ? parameters.token : undefined)
|
||||
.then(assessments => groupBy(assessments, it => it.courseOfStudy?.uid ?? 'unknown'))
|
||||
.then(it => {
|
||||
this.assessmentKeys = Object.keys(it);
|
||||
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
* Copyright (C) 2026 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 {TestBed} from '@angular/core/testing';
|
||||
import {Router} from '@angular/router';
|
||||
import {NavController} from '@ionic/angular/standalone';
|
||||
import {Subject} from 'rxjs';
|
||||
import {AuthActions, AuthProvider, IAuthAction} from '../../auth.provider';
|
||||
import {AuthHelperService} from '../../auth-helper.service';
|
||||
import {AuthCallbackPageComponent} from './auth-callback-page.component';
|
||||
|
||||
/** Lets pending promise callbacks run. */
|
||||
async function flushPromises(): Promise<void> {
|
||||
for (let index = 0; index < 10; index++) {
|
||||
await Promise.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
/** Creates the page like the router does when the IdP redirects back. */
|
||||
function openCallbackPage(): AuthCallbackPageComponent {
|
||||
return TestBed.runInInjectionContext(() => new AuthCallbackPageComponent());
|
||||
}
|
||||
|
||||
describe('AuthCallbackPageComponent', () => {
|
||||
let events: Subject<IAuthAction>;
|
||||
let authProvider: {events$: Subject<IAuthAction>; authorizationCallback: jasmine.Spy};
|
||||
let authHelper: jasmine.SpyObj<Pick<AuthHelperService, 'getOriginPath' | 'deleteOriginPath'>>;
|
||||
let navController: jasmine.SpyObj<Pick<NavController, 'navigateRoot'>>;
|
||||
|
||||
beforeEach(() => {
|
||||
events = new Subject<IAuthAction>();
|
||||
authProvider = {events$: events, authorizationCallback: jasmine.createSpy('authorizationCallback')};
|
||||
|
||||
authHelper = jasmine.createSpyObj('AuthHelperService', ['getOriginPath', 'deleteOriginPath']);
|
||||
authHelper.deleteOriginPath.and.resolveTo();
|
||||
|
||||
navController = jasmine.createSpyObj('NavController', ['navigateRoot']);
|
||||
navController.navigateRoot.and.resolveTo(true);
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
{provide: AuthProvider, useValue: authProvider},
|
||||
{provide: AuthHelperService, useValue: authHelper},
|
||||
{provide: NavController, useValue: navController},
|
||||
{provide: Router, useValue: {}},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('hands the callback URL to AuthProvider', () => {
|
||||
openCallbackPage();
|
||||
|
||||
expect(authProvider.authorizationCallback).toHaveBeenCalledOnceWith(window.location.href);
|
||||
});
|
||||
|
||||
it('goes back to the page the login was started from', async () => {
|
||||
authHelper.getOriginPath.and.resolveTo('/assessments');
|
||||
openCallbackPage();
|
||||
|
||||
events.next({action: AuthActions.SignInSuccess});
|
||||
await flushPromises();
|
||||
|
||||
expect(navController.navigateRoot).toHaveBeenCalledOnceWith('/assessments');
|
||||
expect(authHelper.deleteOriginPath).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('goes to the profile after login when no origin page is known', async () => {
|
||||
openCallbackPage();
|
||||
|
||||
events.next({action: AuthActions.SignInSuccess});
|
||||
await flushPromises();
|
||||
|
||||
expect(navController.navigateRoot).toHaveBeenCalledOnceWith('profile');
|
||||
});
|
||||
|
||||
it('goes to the profile when the login failed', async () => {
|
||||
openCallbackPage();
|
||||
|
||||
events.next({action: AuthActions.SignInFailed, error: new Error('access_denied')});
|
||||
await flushPromises();
|
||||
|
||||
expect(navController.navigateRoot).toHaveBeenCalledOnceWith('profile');
|
||||
expect(authHelper.getOriginPath).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('ignores other auth events', async () => {
|
||||
openCallbackPage();
|
||||
|
||||
events.next({action: AuthActions.RefreshSuccess});
|
||||
events.next({action: AuthActions.LoadUserInfoSuccess});
|
||||
await flushPromises();
|
||||
|
||||
expect(navController.navigateRoot).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
+26
-17
@@ -13,13 +13,12 @@
|
||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import {Component, inject} from '@angular/core';
|
||||
import {NavController} from '@ionic/angular/standalone';
|
||||
import {Router} from '@angular/router';
|
||||
import {AuthActions, IAuthAction} from 'ionic-appauth';
|
||||
import {AuthHelperService} from '../../auth-helper.service';
|
||||
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
|
||||
import {Observable} from 'rxjs';
|
||||
import {DefaultAuthService} from '../../default-auth.service';
|
||||
import {NavController} from '@ionic/angular/standalone';
|
||||
|
||||
import {AuthActions, AuthProvider, IAuthAction} from '../../auth.provider';
|
||||
import {AuthHelperService} from '../../auth-helper.service';
|
||||
|
||||
@Component({
|
||||
templateUrl: 'auth-callback-page.component.html',
|
||||
@@ -33,23 +32,33 @@ export class AuthCallbackPageComponent {
|
||||
|
||||
private authHelper = inject(AuthHelperService);
|
||||
|
||||
private auth = inject(DefaultAuthService);
|
||||
private authProvider = inject(AuthProvider);
|
||||
|
||||
constructor() {
|
||||
const events: Observable<IAuthAction> = this.auth.events$;
|
||||
this.authProvider.events$.pipe(takeUntilDestroyed()).subscribe(action => {
|
||||
void this.postCallback(action);
|
||||
});
|
||||
|
||||
events.pipe(takeUntilDestroyed()).subscribe((action: IAuthAction) => this.postCallback(action));
|
||||
this.auth.authorizationCallback(window.location.origin + this.router.url);
|
||||
this.authProvider.authorizationCallback(window.location.href);
|
||||
}
|
||||
|
||||
async postCallback(action: IAuthAction) {
|
||||
if (action.action === AuthActions.SignInSuccess) {
|
||||
const originPath = await this.authHelper.getOriginPath();
|
||||
this.navCtrl.navigateRoot(originPath ?? 'profile');
|
||||
this.authHelper.deleteOriginPath();
|
||||
}
|
||||
if (action.action === AuthActions.SignInFailed) {
|
||||
this.navCtrl.navigateRoot('profile');
|
||||
private async postCallback(action: IAuthAction): Promise<void> {
|
||||
switch (action.action) {
|
||||
case AuthActions.SignInSuccess: {
|
||||
const originPath = await this.authHelper.getOriginPath();
|
||||
|
||||
await this.authHelper.deleteOriginPath();
|
||||
|
||||
await this.navCtrl.navigateRoot(originPath ?? 'profile');
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case AuthActions.SignInFailed: {
|
||||
await this.navCtrl.navigateRoot('profile');
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,13 +16,13 @@
|
||||
import {Injectable, inject} from '@angular/core';
|
||||
import {CanActivate, NavigationExtras, Router, RouterStateSnapshot} from '@angular/router';
|
||||
import {ActivatedProtectedRouteSnapshot} from './protected.routes';
|
||||
import {AuthHelperService} from './auth-helper.service';
|
||||
import {AuthProvider} from './auth.provider';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class AuthGuardService implements CanActivate {
|
||||
private authHelper = inject(AuthHelperService);
|
||||
private authProvider = inject(AuthProvider);
|
||||
|
||||
private router = inject(Router);
|
||||
|
||||
@@ -32,7 +32,7 @@ export class AuthGuardService implements CanActivate {
|
||||
}
|
||||
|
||||
try {
|
||||
await this.authHelper.getProvider().getValidToken();
|
||||
await this.authProvider.getValidToken();
|
||||
} catch {
|
||||
const originNavigation = this.router.currentNavigation();
|
||||
let extras: NavigationExtras = {};
|
||||
@@ -41,7 +41,7 @@ export class AuthGuardService implements CanActivate {
|
||||
extras = {queryParams: {origin_path: url}};
|
||||
}
|
||||
this.router.navigate(['profile'], extras);
|
||||
await this.authHelper.getProvider().signIn();
|
||||
await this.authProvider.signIn();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1,118 +1,288 @@
|
||||
/*
|
||||
* Copyright (C) 2023 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 {TestBed} from '@angular/core/testing';
|
||||
import {AuthHelperService} from './auth-helper.service';
|
||||
import {AlertController} from '@ionic/angular/standalone';
|
||||
import {TranslateService} from '@ngx-translate/core';
|
||||
|
||||
import {SimpleBrowser} from '../../util/browser.factory';
|
||||
import {ConfigProvider} from '../config/config.provider';
|
||||
import {StorageProvider} from '../storage/storage.provider';
|
||||
import {DefaultAuthService} from './default-auth.service';
|
||||
import {Browser} from 'ionic-appauth';
|
||||
import {Requestor, StorageBackend} from '@openid/appauth';
|
||||
import {TranslateService} from '@ngx-translate/core';
|
||||
import {StAppsWebHttpClient} from '../data/stapps-web-http-client.provider';
|
||||
import {provideHttpClient, withInterceptorsFromDi} from '@angular/common/http';
|
||||
import {SimpleBrowser} from '../../util/browser.factory';
|
||||
import {LoggerTestingModule} from 'ngx-logger/testing';
|
||||
import {AuthActions, AuthProvider} from './auth.provider';
|
||||
import {AuthHelperService} from './auth-helper.service';
|
||||
|
||||
const AUTH_ORIGIN_PATH = 'stapps.auth.origin_path';
|
||||
const AUTH_MESSAGE_PREFIX = 'auth.messages.default';
|
||||
const END_SESSION_ENDPOINT = 'https://idp.example.org/logout';
|
||||
|
||||
const USER_MAPPING = {
|
||||
id: '$.sub',
|
||||
givenName: '$.given_name',
|
||||
familyName: '$.family_name',
|
||||
email: '$.attributes.mail',
|
||||
role: '$.role',
|
||||
};
|
||||
|
||||
const DEFAULT_AUTH_CONFIG = {default: {endpoints: {mapping: USER_MAPPING}}};
|
||||
|
||||
interface TestAlertButton {
|
||||
text: string;
|
||||
role?: string;
|
||||
cssClass?: string;
|
||||
handler?: () => Promise<void> | void;
|
||||
}
|
||||
|
||||
interface TestAlertOptions {
|
||||
header: string;
|
||||
message: string;
|
||||
buttons: TestAlertButton[];
|
||||
}
|
||||
|
||||
describe('AuthHelperService', () => {
|
||||
let authHelperService: AuthHelperService;
|
||||
const storageProviderSpy = jasmine.createSpyObj('StorageProvider', ['init', 'get', 'has', 'put', 'search']);
|
||||
const translateServiceSpy = jasmine.createSpyObj('TranslateService', ['setDefaultLang', 'use']);
|
||||
const defaultAuthServiceMock = jasmine.createSpyObj('DefaultAuthService', ['init', 'setupConfiguration']);
|
||||
const authHelperServiceMock = jasmine.createSpyObj('AuthHelperService', ['constructor']);
|
||||
const simpleBrowserMock = jasmine.createSpyObj('SimpleBrowser', ['open']);
|
||||
const configProvider = jasmine.createSpyObj('ConfigProvider', {
|
||||
getAnyValue: {
|
||||
default: {
|
||||
endpoints: {
|
||||
mapping: {
|
||||
id: '$.id',
|
||||
email: '$.attributes.mailPrimaryAddress',
|
||||
givenName: '$.attributes.givenName',
|
||||
familyName: '$.attributes.sn',
|
||||
name: '$.attributes.sn',
|
||||
role: '$.attributes.eduPersonPrimaryAffiliation',
|
||||
studentId: '$.attributes.employeeNumber',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
let service: AuthHelperService;
|
||||
|
||||
beforeEach(() => {
|
||||
let translateService: jasmine.SpyObj<{instant(key: string): string}>;
|
||||
let configProvider: jasmine.SpyObj<{getAnyValue(key: string): unknown}>;
|
||||
let storageProvider: jasmine.SpyObj<{
|
||||
get<T>(key: string): Promise<T>;
|
||||
put(key: string, value: unknown): Promise<unknown>;
|
||||
delete(key: string): Promise<unknown>;
|
||||
}>;
|
||||
let authProvider: jasmine.SpyObj<Pick<AuthProvider, 'getEndSessionEndpoint'>>;
|
||||
let browser: jasmine.SpyObj<{open(url: string): Promise<void>}>;
|
||||
let alertController: jasmine.SpyObj<{create(options: unknown): Promise<unknown>}>;
|
||||
let alert: jasmine.SpyObj<{present(): Promise<void>}>;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
function createService(authConfig: unknown = DEFAULT_AUTH_CONFIG) {
|
||||
configProvider.getAnyValue.and.returnValue(authConfig);
|
||||
|
||||
// AuthHelperService uses inject(), so it has to be created inside an injection context.
|
||||
TestBed.resetTestingModule();
|
||||
TestBed.configureTestingModule({
|
||||
imports: [LoggerTestingModule],
|
||||
providers: [
|
||||
StAppsWebHttpClient,
|
||||
{
|
||||
provide: TranslateService,
|
||||
useValue: translateServiceSpy,
|
||||
},
|
||||
{
|
||||
provide: StorageProvider,
|
||||
useValue: storageProviderSpy,
|
||||
},
|
||||
{
|
||||
provider: DefaultAuthService,
|
||||
useValue: defaultAuthServiceMock,
|
||||
},
|
||||
{
|
||||
provide: ConfigProvider,
|
||||
useValue: configProvider,
|
||||
},
|
||||
Browser,
|
||||
StorageBackend,
|
||||
Requestor,
|
||||
{
|
||||
provider: AuthHelperService,
|
||||
useValue: authHelperServiceMock,
|
||||
},
|
||||
{
|
||||
provide: SimpleBrowser,
|
||||
useValue: simpleBrowserMock,
|
||||
},
|
||||
provideHttpClient(withInterceptorsFromDi()),
|
||||
AuthHelperService,
|
||||
{provide: TranslateService, useValue: translateService},
|
||||
{provide: ConfigProvider, useValue: configProvider},
|
||||
{provide: StorageProvider, useValue: storageProvider},
|
||||
{provide: AuthProvider, useValue: authProvider},
|
||||
{provide: SimpleBrowser, useValue: browser},
|
||||
{provide: AlertController, useValue: alertController},
|
||||
],
|
||||
});
|
||||
authHelperService = TestBed.inject(AuthHelperService);
|
||||
|
||||
return TestBed.inject(AuthHelperService);
|
||||
}
|
||||
|
||||
/** Options passed to the most recent AlertController.create() call. */
|
||||
function lastAlertOptions(): TestAlertOptions {
|
||||
return alertController.create.calls.mostRecent().args[0] as TestAlertOptions;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
function alertButton(role: string): TestAlertButton {
|
||||
const button = lastAlertOptions().buttons.find(b => b.role === role);
|
||||
if (!button) {
|
||||
throw new Error(`No alert button with role "${role}"`);
|
||||
}
|
||||
return button;
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
translateService = jasmine.createSpyObj('TranslateService', ['instant']);
|
||||
translateService.instant.and.callFake((key: string) => `translated:${key}`);
|
||||
|
||||
configProvider = jasmine.createSpyObj('ConfigProvider', ['getAnyValue']);
|
||||
|
||||
storageProvider = jasmine.createSpyObj('StorageProvider', ['get', 'put', 'delete']);
|
||||
storageProvider.put.and.resolveTo();
|
||||
storageProvider.delete.and.resolveTo();
|
||||
|
||||
authProvider = jasmine.createSpyObj('AuthProvider', ['getEndSessionEndpoint']);
|
||||
authProvider.getEndSessionEndpoint.and.resolveTo(END_SESSION_ENDPOINT);
|
||||
|
||||
browser = jasmine.createSpyObj('SimpleBrowser', ['open']);
|
||||
browser.open.and.resolveTo();
|
||||
|
||||
alert = jasmine.createSpyObj('HTMLIonAlertElement', ['present']);
|
||||
alert.present.and.resolveTo();
|
||||
|
||||
alertController = jasmine.createSpyObj('AlertController', ['create']);
|
||||
alertController.create.and.resolveTo(alert);
|
||||
|
||||
service = createService();
|
||||
});
|
||||
|
||||
describe('userConfigurationMap', () => {
|
||||
it('reads the user mapping from the default auth provider', () => {
|
||||
expect(configProvider.getAnyValue).toHaveBeenCalledWith('auth');
|
||||
expect(service.userConfigurationMap as unknown).toEqual(USER_MAPPING);
|
||||
});
|
||||
|
||||
it('falls back to an empty mapping when no default auth provider is configured', () => {
|
||||
service = createService({});
|
||||
|
||||
expect(service.userConfigurationMap as unknown).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAuthMessage', () => {
|
||||
it('returns the translated message for SignInSuccess', () => {
|
||||
expect(service.getAuthMessage({action: AuthActions.SignInSuccess})).toBe(
|
||||
`translated:${AUTH_MESSAGE_PREFIX}.logged_in_success`,
|
||||
);
|
||||
});
|
||||
|
||||
it('returns the translated message for SignOutSuccess', () => {
|
||||
expect(service.getAuthMessage({action: AuthActions.SignOutSuccess})).toBe(
|
||||
`translated:${AUTH_MESSAGE_PREFIX}.logged_out_success`,
|
||||
);
|
||||
});
|
||||
|
||||
it('returns the translated message for SessionExpired', () => {
|
||||
expect(service.getAuthMessage({action: AuthActions.SessionExpired})).toBe(
|
||||
`translated:${AUTH_MESSAGE_PREFIX}.session_expired`,
|
||||
);
|
||||
});
|
||||
|
||||
it('returns undefined for all other actions without translating', () => {
|
||||
const messageActions = new Set([
|
||||
AuthActions.SignInSuccess,
|
||||
AuthActions.SignOutSuccess,
|
||||
AuthActions.SessionExpired,
|
||||
]);
|
||||
const otherActions = Object.values(AuthActions).filter(action => !messageActions.has(action));
|
||||
|
||||
for (const action of otherActions) {
|
||||
expect(service.getAuthMessage({action})).withContext(action).toBeUndefined();
|
||||
}
|
||||
expect(translateService.instant).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getUserFromUserInfo', () => {
|
||||
it('should provide user configuration from userInfo', async () => {
|
||||
const userConfiguration = authHelperService.getUserFromUserInfo({
|
||||
attributes: {
|
||||
eduPersonPrimaryAffiliation: 'student',
|
||||
employeeNumber: '123456',
|
||||
givenName: 'Erika',
|
||||
mailPrimaryAddress: 'emuster@anyschool.de',
|
||||
oauthClientId: '123-abc-123',
|
||||
sn: 'Musterfrau',
|
||||
uid: 'emuster',
|
||||
},
|
||||
id: 'emuster',
|
||||
client_id: '123-abc-123',
|
||||
it('maps user info fields via the configured JSONPath mapping', () => {
|
||||
const user = service.getUserFromUserInfo({
|
||||
sub: 'user-123',
|
||||
given_name: 'Erika',
|
||||
family_name: 'Mustermann',
|
||||
attributes: {mail: 'erika@example.org'},
|
||||
role: 'employee',
|
||||
});
|
||||
|
||||
expect(userConfiguration).toEqual({
|
||||
id: 'emuster',
|
||||
givenName: 'Erika',
|
||||
familyName: 'Musterfrau',
|
||||
name: 'Erika Musterfrau',
|
||||
email: 'emuster@anyschool.de',
|
||||
expect(user).toEqual(
|
||||
jasmine.objectContaining({
|
||||
id: 'user-123',
|
||||
givenName: 'Erika',
|
||||
familyName: 'Mustermann',
|
||||
email: 'erika@example.org',
|
||||
role: 'employee',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('builds the name from given name and family name', () => {
|
||||
const user = service.getUserFromUserInfo({
|
||||
sub: 'user-123',
|
||||
given_name: 'Erika',
|
||||
family_name: 'Mustermann',
|
||||
role: 'student',
|
||||
});
|
||||
|
||||
expect(user.name).toBe('Erika Mustermann');
|
||||
});
|
||||
|
||||
it('keeps the default name when the family name is missing', () => {
|
||||
const user = service.getUserFromUserInfo({
|
||||
sub: 'user-123',
|
||||
given_name: 'Erika',
|
||||
role: 'student',
|
||||
});
|
||||
|
||||
expect(user.name).toBe('');
|
||||
});
|
||||
|
||||
it('returns the default user when no mapping is configured', () => {
|
||||
service = createService({});
|
||||
|
||||
expect(service.getUserFromUserInfo({sub: 'user-123'})).toEqual({
|
||||
id: '',
|
||||
name: '',
|
||||
role: 'student',
|
||||
studentId: '123456',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('origin path', () => {
|
||||
it('stores the origin path', async () => {
|
||||
await service.setOriginPath('/profile');
|
||||
|
||||
expect(storageProvider.put).toHaveBeenCalledOnceWith(AUTH_ORIGIN_PATH, '/profile');
|
||||
});
|
||||
|
||||
it('returns the stored origin path', async () => {
|
||||
storageProvider.get.and.resolveTo('/profile');
|
||||
|
||||
expect(await service.getOriginPath()).toBe('/profile');
|
||||
expect(storageProvider.get).toHaveBeenCalledOnceWith(AUTH_ORIGIN_PATH);
|
||||
});
|
||||
|
||||
it('returns undefined when reading the origin path fails', async () => {
|
||||
storageProvider.get.and.rejectWith(new Error('Value not found'));
|
||||
|
||||
expect(await service.getOriginPath()).toBeUndefined();
|
||||
});
|
||||
|
||||
it('deletes the origin path', async () => {
|
||||
await service.deleteOriginPath();
|
||||
|
||||
expect(storageProvider.delete).toHaveBeenCalledOnceWith(AUTH_ORIGIN_PATH);
|
||||
});
|
||||
});
|
||||
|
||||
describe('endBrowserSession', () => {
|
||||
it('does nothing when the provider has no end-session endpoint', async () => {
|
||||
authProvider.getEndSessionEndpoint.and.resolveTo();
|
||||
|
||||
await service.endBrowserSession();
|
||||
|
||||
expect(alertController.create).not.toHaveBeenCalled();
|
||||
expect(browser.open).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('presents a translated confirmation alert', async () => {
|
||||
await service.endBrowserSession();
|
||||
|
||||
const options = lastAlertOptions();
|
||||
expect(options.header).toBe(`translated:${AUTH_MESSAGE_PREFIX}.log_out_alert.header`);
|
||||
expect(options.message).toBe(`translated:${AUTH_MESSAGE_PREFIX}.log_out_alert.message`);
|
||||
expect(options.buttons.map(b => b.role)).toEqual(['cancel', 'confirm']);
|
||||
expect(alert.present).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('does not open the end-session endpoint before confirmation', async () => {
|
||||
await service.endBrowserSession();
|
||||
|
||||
expect(browser.open).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('opens the end-session endpoint when the user confirms', async () => {
|
||||
await service.endBrowserSession();
|
||||
|
||||
await alertButton('confirm').handler?.();
|
||||
|
||||
expect(browser.open).toHaveBeenCalledOnceWith(END_SESSION_ENDPOINT);
|
||||
});
|
||||
|
||||
it('does not open the end-session endpoint when the user cancels', async () => {
|
||||
await service.endBrowserSession();
|
||||
|
||||
const cancelButton = alertButton('cancel');
|
||||
await cancelButton.handler?.();
|
||||
|
||||
expect(cancelButton.handler).toBeUndefined();
|
||||
expect(browser.open).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -14,35 +14,31 @@
|
||||
*/
|
||||
|
||||
import {Injectable, inject} from '@angular/core';
|
||||
import {AuthActions, IAuthAction} from 'ionic-appauth';
|
||||
import {AlertController} from '@ionic/angular/standalone';
|
||||
import {TranslateService} from '@ngx-translate/core';
|
||||
import {SCAuthorizationProvider, SCUserConfiguration, SCUserConfigurationMap} from '@openstapps/core';
|
||||
import {JSONPath} from 'jsonpath-plus';
|
||||
import {
|
||||
SCAuthorizationProvider,
|
||||
SCAuthorizationProviderType,
|
||||
SCUserConfiguration,
|
||||
SCUserConfigurationMap,
|
||||
} from '@openstapps/core';
|
||||
|
||||
import {SimpleBrowser} from '../../util/browser.factory';
|
||||
import {ConfigProvider} from '../config/config.provider';
|
||||
import {StorageProvider} from '../storage/storage.provider';
|
||||
import {DefaultAuthService} from './default-auth.service';
|
||||
import {SimpleBrowser} from '../../util/browser.factory';
|
||||
import {AlertController} from '@ionic/angular/standalone';
|
||||
import {AuthActions, AuthProvider, IAuthAction} from './auth.provider';
|
||||
|
||||
const AUTH_ORIGIN_PATH = 'stapps.auth.origin_path';
|
||||
const AUTH_MESSAGE_PREFIX = 'auth.messages.default';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class AuthHelperService {
|
||||
private authProvider = inject(AuthProvider);
|
||||
|
||||
private translateService = inject(TranslateService);
|
||||
|
||||
private configProvider = inject(ConfigProvider);
|
||||
|
||||
private storageProvider = inject(StorageProvider);
|
||||
|
||||
private defaultAuth = inject(DefaultAuthService);
|
||||
|
||||
private browser = inject(SimpleBrowser);
|
||||
|
||||
private alertController = inject(AlertController);
|
||||
@@ -58,93 +54,101 @@ export class AuthHelperService {
|
||||
).default?.endpoints.mapping ?? {};
|
||||
}
|
||||
|
||||
public getAuthMessage(provider: SCAuthorizationProviderType, action: IAuthAction) {
|
||||
let message: string | undefined;
|
||||
public getAuthMessage(action: IAuthAction): string | undefined {
|
||||
switch (action.action) {
|
||||
case AuthActions.SignInSuccess: {
|
||||
message = this.translateService.instant(`auth.messages.${provider}.logged_in_success`);
|
||||
break;
|
||||
return this.translateService.instant(`${AUTH_MESSAGE_PREFIX}.logged_in_success`);
|
||||
}
|
||||
|
||||
case AuthActions.SignOutSuccess: {
|
||||
message = this.translateService.instant(`auth.messages.${provider}.logged_out_success`);
|
||||
break;
|
||||
return this.translateService.instant(`${AUTH_MESSAGE_PREFIX}.logged_out_success`);
|
||||
}
|
||||
|
||||
case AuthActions.SessionExpired: {
|
||||
return this.translateService.instant(`${AUTH_MESSAGE_PREFIX}.session_expired`);
|
||||
}
|
||||
|
||||
default: {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
return message;
|
||||
}
|
||||
|
||||
getUserFromUserInfo(userInfo: object) {
|
||||
public getUserFromUserInfo(userInfo: object): SCUserConfiguration {
|
||||
const user: SCUserConfiguration = {
|
||||
id: '',
|
||||
name: '',
|
||||
role: 'student',
|
||||
};
|
||||
|
||||
for (const key in this.userConfigurationMap) {
|
||||
user[key as keyof SCUserConfiguration] = JSONPath({
|
||||
path: this.userConfigurationMap[key as keyof SCUserConfiguration] as string,
|
||||
const userKey = key as keyof SCUserConfiguration;
|
||||
|
||||
user[userKey] = JSONPath({
|
||||
path: this.userConfigurationMap[userKey] as string,
|
||||
json: userInfo,
|
||||
})[0];
|
||||
}
|
||||
if (user.givenName && user.givenName.length > 0 && user.familyName && user.familyName.length > 0) {
|
||||
|
||||
if (user.givenName && user.familyName) {
|
||||
user.name = `${user.givenName} ${user.familyName}`;
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
async deleteOriginPath() {
|
||||
return this.storageProvider.delete(AUTH_ORIGIN_PATH);
|
||||
public async deleteOriginPath(): Promise<void> {
|
||||
await this.storageProvider.delete(AUTH_ORIGIN_PATH);
|
||||
}
|
||||
|
||||
async setOriginPath(path: string) {
|
||||
return this.storageProvider.put<string>(AUTH_ORIGIN_PATH, path);
|
||||
public async setOriginPath(path: string): Promise<void> {
|
||||
await this.storageProvider.put(AUTH_ORIGIN_PATH, path);
|
||||
}
|
||||
|
||||
async getOriginPath() {
|
||||
let originPath: string;
|
||||
public async getOriginPath(): Promise<string | undefined> {
|
||||
try {
|
||||
originPath = await this.storageProvider.get<string>(AUTH_ORIGIN_PATH);
|
||||
return await this.storageProvider.get<string>(AUTH_ORIGIN_PATH);
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
public async endBrowserSession(): Promise<void> {
|
||||
const endSessionEndpoint = await this.authProvider.getEndSessionEndpoint();
|
||||
|
||||
if (!endSessionEndpoint) {
|
||||
return;
|
||||
}
|
||||
return originPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides appropriate auth service instance based on type (string) parameter
|
||||
*/
|
||||
getProvider(): DefaultAuthService {
|
||||
return this.defaultAuth;
|
||||
}
|
||||
const alert = await this.alertController.create({
|
||||
header: this.translateService.instant(`${AUTH_MESSAGE_PREFIX}.log_out_alert.header`),
|
||||
|
||||
/**
|
||||
* Ends browser session by opening endSessionEndpoint URL of the provider
|
||||
* @param providerType Type of the provider (e.g. 'default' or 'paia')
|
||||
*/
|
||||
async endBrowserSession(providerType: SCAuthorizationProviderType) {
|
||||
const endSessionEndpoint = (await this.getProvider().configuration).endSessionEndpoint;
|
||||
message: this.translateService.instant(`${AUTH_MESSAGE_PREFIX}.log_out_alert.message`),
|
||||
|
||||
if (endSessionEndpoint) {
|
||||
const alert: HTMLIonAlertElement = await this.alertController.create({
|
||||
header: this.translateService.instant(`auth.messages.${providerType}.log_out_alert.header`),
|
||||
message: this.translateService.instant(`auth.messages.${providerType}.log_out_alert.message`),
|
||||
buttons: [
|
||||
{
|
||||
text: this.translateService.instant('no'),
|
||||
cssClass: 'default',
|
||||
buttons: [
|
||||
{
|
||||
text: this.translateService.instant('no'),
|
||||
|
||||
role: 'cancel',
|
||||
cssClass: 'default',
|
||||
},
|
||||
{
|
||||
text: this.translateService.instant('yes'),
|
||||
|
||||
role: 'confirm',
|
||||
cssClass: 'preferred',
|
||||
|
||||
handler: async () => {
|
||||
/*
|
||||
* Only now, after explicit confirmation,
|
||||
* open the IdP end-session endpoint.
|
||||
*/
|
||||
await this.browser.open(endSessionEndpoint);
|
||||
},
|
||||
{
|
||||
text: this.translateService.instant('yes'),
|
||||
role: 'confirm',
|
||||
cssClass: 'preferred',
|
||||
handler: () => {
|
||||
this.browser.open(new URL(endSessionEndpoint).href);
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await alert.present();
|
||||
}
|
||||
await alert.present();
|
||||
}
|
||||
}
|
||||
@@ -12,15 +12,5 @@
|
||||
* 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 {SCAuthorizationProviderType} from '@openstapps/core';
|
||||
|
||||
export const authPaths: {
|
||||
[key in SCAuthorizationProviderType]: {redirect_path: string};
|
||||
} = {
|
||||
default: {
|
||||
redirect_path: 'auth/callback',
|
||||
},
|
||||
paia: {
|
||||
redirect_path: 'auth/paia/callback',
|
||||
},
|
||||
};
|
||||
export const AUTH_REDIRECT_PATH = 'auth/callback';
|
||||
@@ -15,12 +15,12 @@
|
||||
|
||||
import {RouterModule, Routes} from '@angular/router';
|
||||
import {NgModule} from '@angular/core';
|
||||
import {authPaths} from './auth-paths';
|
||||
import {AUTH_REDIRECT_PATH} from './auth-paths';
|
||||
import {AuthCallbackPageComponent} from './auth-callback/page/auth-callback-page.component';
|
||||
|
||||
const authRoutes: Routes = [
|
||||
{
|
||||
path: authPaths.default.redirect_path,
|
||||
path: AUTH_REDIRECT_PATH,
|
||||
component: AuthCallbackPageComponent,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -3,14 +3,12 @@ import {CommonModule} from '@angular/common';
|
||||
import {Platform} from '@ionic/angular/standalone';
|
||||
import {Requestor, StorageBackend} from '@openid/appauth';
|
||||
import {storageFactory} from './factories';
|
||||
import {Browser} from 'ionic-appauth';
|
||||
import {CapacitorBrowser} from 'ionic-appauth/lib/capacitor';
|
||||
import {httpFactory} from './factories/http.factory';
|
||||
import {requestorFactory} from './factories/requestor.factory';
|
||||
import {HttpClient} from '@angular/common/http';
|
||||
import {AuthRoutingModule} from './auth-routing.module';
|
||||
import {TranslateModule} from '@ngx-translate/core';
|
||||
import {AuthCallbackPageComponent} from './auth-callback/page/auth-callback-page.component';
|
||||
import {DefaultAuthService} from './default-auth.service';
|
||||
import {AuthProvider} from './auth.provider';
|
||||
|
||||
@NgModule({
|
||||
declarations: [AuthCallbackPageComponent],
|
||||
@@ -23,14 +21,10 @@ import {DefaultAuthService} from './default-auth.service';
|
||||
},
|
||||
{
|
||||
provide: Requestor,
|
||||
useFactory: httpFactory,
|
||||
useFactory: requestorFactory,
|
||||
deps: [Platform, HttpClient],
|
||||
},
|
||||
{
|
||||
provide: Browser,
|
||||
useClass: CapacitorBrowser,
|
||||
},
|
||||
DefaultAuthService,
|
||||
AuthProvider,
|
||||
],
|
||||
})
|
||||
export class AuthModule {}
|
||||
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* Copyright (C) 2026 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 {Capacitor} from '@capacitor/core';
|
||||
import {SCAuthorizationProvider} from '@openstapps/core';
|
||||
|
||||
import type {CapacitorMock} from '../../../../__mocks__/@capacitor/core';
|
||||
import {getClientConfig, getPlatformScopes} from './auth.provider.methods';
|
||||
|
||||
const capacitor = Capacitor as unknown as CapacitorMock;
|
||||
|
||||
const authConfig = {
|
||||
default: {
|
||||
client: {
|
||||
clientId: 'client-id',
|
||||
scopes: 'openid profile email offline_access',
|
||||
url: 'https://idp.example.com',
|
||||
},
|
||||
endpoints: {
|
||||
authorization: 'https://idp.example.com/authorize',
|
||||
token: 'https://idp.example.com/token',
|
||||
userinfo: 'https://idp.example.com/userinfo',
|
||||
mapping: {},
|
||||
},
|
||||
} as unknown as SCAuthorizationProvider,
|
||||
};
|
||||
|
||||
describe('auth.provider.methods', () => {
|
||||
afterEach(() => {
|
||||
capacitor.reset();
|
||||
});
|
||||
|
||||
describe('getClientConfig', () => {
|
||||
it('requests offline_access on iOS', () => {
|
||||
capacitor.setPlatform('ios');
|
||||
|
||||
expect(getClientConfig(authConfig).scopes).toBe('openid profile email offline_access');
|
||||
});
|
||||
|
||||
it('requests offline_access on Android', () => {
|
||||
capacitor.setPlatform('android');
|
||||
|
||||
expect(getClientConfig(authConfig).scopes).toBe('openid profile email offline_access');
|
||||
});
|
||||
|
||||
it('does not request offline_access in the browser', () => {
|
||||
capacitor.setPlatform('web');
|
||||
|
||||
expect(getClientConfig(authConfig).scopes).toBe('openid profile email');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPlatformScopes', () => {
|
||||
it('leaves scopes without offline_access unchanged in the browser', () => {
|
||||
expect(getPlatformScopes('openid profile')).toBe('openid profile');
|
||||
});
|
||||
|
||||
it('normalizes extra whitespace', () => {
|
||||
expect(getPlatformScopes(' openid offline_access profile ')).toBe('openid profile');
|
||||
});
|
||||
|
||||
it('still accepts an explicit platform flag', () => {
|
||||
expect(getPlatformScopes('openid offline_access', true)).toBe('openid offline_access');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -13,69 +13,71 @@
|
||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {AuthorizationServiceConfigurationJson} from '@openid/appauth';
|
||||
import {IAuthConfig} from 'ionic-appauth';
|
||||
import {SCAuthorizationProvider, SCAuthorizationProviderType} from '@openstapps/core';
|
||||
import {Capacitor} from '@capacitor/core';
|
||||
import {authPaths} from './auth-paths';
|
||||
import {SCAuthorizationProvider} from '@openstapps/core';
|
||||
import {AuthorizationServiceConfiguration} from '@openid/appauth';
|
||||
|
||||
import {environment} from '../../../environments/environment';
|
||||
import {AUTH_REDIRECT_PATH} from './auth-paths';
|
||||
import type {AuthConfig} from './auth.provider';
|
||||
|
||||
export const OFFLINE_ACCESS_SCOPE = 'offline_access';
|
||||
|
||||
/**
|
||||
* Get configuration of an OAuth2 client
|
||||
* Returns the configuration of the OIDC client.
|
||||
*/
|
||||
export function getClientConfig(
|
||||
providerType: SCAuthorizationProviderType,
|
||||
authConfig: {
|
||||
default?: SCAuthorizationProvider;
|
||||
paia?: SCAuthorizationProvider;
|
||||
},
|
||||
): IAuthConfig {
|
||||
const providerConfig = authConfig[providerType] as SCAuthorizationProvider;
|
||||
export function getClientConfig(authConfig: {default: SCAuthorizationProvider}): AuthConfig {
|
||||
const provider = authConfig.default;
|
||||
|
||||
return {
|
||||
end_session_redirect_url: '',
|
||||
server_host: provider.client.url,
|
||||
client_id: provider.client.clientId,
|
||||
scopes: getPlatformScopes(provider.client.scopes),
|
||||
redirect_url: getRedirectUrl(),
|
||||
pkce: true,
|
||||
scopes: providerConfig.client.scopes,
|
||||
server_host: providerConfig.client.url,
|
||||
client_id: providerConfig.client.clientId,
|
||||
redirect_url: getRedirectUrl(authPaths[providerType].redirect_path),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get configuration about endpoints of an OAuth2 server
|
||||
* Returns the scopes to request on the current platform.
|
||||
*
|
||||
* `offline_access` (long-lived refresh tokens) is only requested in the
|
||||
* native apps, where tokens live in the Keychain / Keystore. In the browser,
|
||||
* tokens live in web storage and are reachable by any XSS, so the session is
|
||||
* limited to the access token lifetime instead.
|
||||
*/
|
||||
export function getEndpointsConfig(
|
||||
providerType: SCAuthorizationProviderType,
|
||||
authConfig: {
|
||||
default?: SCAuthorizationProvider;
|
||||
paia?: SCAuthorizationProvider;
|
||||
},
|
||||
): AuthorizationServiceConfigurationJson {
|
||||
const providerConfig = authConfig[providerType] as SCAuthorizationProvider;
|
||||
return {
|
||||
authorization_endpoint: providerConfig.endpoints.authorization,
|
||||
end_session_endpoint: providerConfig.endpoints.endSession,
|
||||
revocation_endpoint: providerConfig.endpoints.revoke ?? '',
|
||||
token_endpoint: providerConfig.endpoints.token,
|
||||
userinfo_endpoint: providerConfig.endpoints.userinfo,
|
||||
};
|
||||
export function getPlatformScopes(scopes: string, isNative: boolean = Capacitor.isNativePlatform()): string {
|
||||
const scopeList = scopes.split(/\s+/).filter(scope => scope.length > 0);
|
||||
|
||||
return (isNative ? scopeList : scopeList.filter(scope => scope !== OFFLINE_ACCESS_SCOPE)).join(' ');
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a URL of the app, depending on the platform where it is running
|
||||
* Returns the AppAuth OAuth/OIDC service configuration.
|
||||
*/
|
||||
function getRedirectUrl(routePath: string): string {
|
||||
let appHost: string;
|
||||
let appSchema: string;
|
||||
if (environment.production) {
|
||||
appSchema = Capacitor.isNativePlatform() ? environment.custom_url_scheme : 'https';
|
||||
appHost = environment.app_host;
|
||||
} else {
|
||||
appSchema = Capacitor.isNativePlatform()
|
||||
? environment.custom_url_scheme
|
||||
: window.location.protocol.split(':')[0];
|
||||
export function getServiceConfiguration(authConfig: {
|
||||
default: SCAuthorizationProvider;
|
||||
}): AuthorizationServiceConfiguration {
|
||||
const endpoints = authConfig.default.endpoints;
|
||||
|
||||
appHost = Capacitor.isNativePlatform() ? environment.app_host : window.location.host;
|
||||
return new AuthorizationServiceConfiguration({
|
||||
authorization_endpoint: endpoints.authorization,
|
||||
token_endpoint: endpoints.token,
|
||||
revocation_endpoint: endpoints.revoke || '',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the OIDC redirect URL depending on the current platform.
|
||||
*/
|
||||
function getRedirectUrl(): string {
|
||||
if (Capacitor.isNativePlatform()) {
|
||||
return `${environment.custom_url_scheme}://${environment.app_host}/${AUTH_REDIRECT_PATH}`;
|
||||
}
|
||||
return `${appSchema}://${appHost}/${routePath}`;
|
||||
|
||||
if (environment.production) {
|
||||
return `https://${environment.app_host}/${AUTH_REDIRECT_PATH}`;
|
||||
}
|
||||
|
||||
return `${window.location.origin}/${AUTH_REDIRECT_PATH}`;
|
||||
}
|
||||
@@ -0,0 +1,613 @@
|
||||
import {TestBed} from '@angular/core/testing';
|
||||
import {Platform} from '@ionic/angular/standalone';
|
||||
import {
|
||||
AppAuthError,
|
||||
AuthorizationServiceConfiguration,
|
||||
RevokeTokenRequest,
|
||||
StorageBackend,
|
||||
TokenRequestHandler,
|
||||
TokenResponse,
|
||||
} from '@openid/appauth';
|
||||
import {BehaviorSubject, firstValueFrom, skip} from 'rxjs';
|
||||
|
||||
import {ConfigProvider} from '../config/config.provider';
|
||||
import {AuthActions, AuthProvider, IAuthAction, INVALID_GRANT, isInvalidGrant} from './auth.provider';
|
||||
|
||||
const TOKEN_RESPONSE_KEY = 'token_response';
|
||||
const TOKEN_LIFETIME = 3600;
|
||||
/** Shibboleth OP default access token lifetime. */
|
||||
const SHORT_TOKEN_LIFETIME = 600;
|
||||
const NOW_MS = Date.parse('2026-01-01T12:00:00Z');
|
||||
const NOW = NOW_MS / 1000;
|
||||
|
||||
// eslint-disable-next-line unicorn/no-null -- StorageBackend.getItem resolves to null for missing keys
|
||||
const MISSING_STORAGE_VALUE = null;
|
||||
|
||||
/**
|
||||
* Creates a token with a real lifetime instead of spying on isValid().
|
||||
*
|
||||
* remainingSeconds > 0 → still valid for that long
|
||||
* remainingSeconds < 0 → expired that many seconds ago
|
||||
* refreshToken: false → token without refresh token
|
||||
*/
|
||||
function makeToken(
|
||||
options: {
|
||||
accessToken?: string;
|
||||
refreshToken?: string | false;
|
||||
remainingSeconds?: number;
|
||||
lifetime?: number;
|
||||
} = {},
|
||||
): TokenResponse {
|
||||
const lifetime = options.lifetime ?? TOKEN_LIFETIME;
|
||||
const remaining = options.remainingSeconds ?? lifetime;
|
||||
|
||||
return new TokenResponse({
|
||||
access_token: options.accessToken ?? 'access-token',
|
||||
refresh_token: options.refreshToken === false ? undefined : options.refreshToken ?? 'refresh-token',
|
||||
token_type: 'bearer',
|
||||
expires_in: String(lifetime),
|
||||
issued_at: NOW - (lifetime - remaining),
|
||||
});
|
||||
}
|
||||
|
||||
/** Lets pending promise callbacks run (the jasmine clock only drives timers). */
|
||||
async function flushPromises(): Promise<void> {
|
||||
for (let index = 0; index < 50; index++) {
|
||||
await Promise.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
/** Typed access to the private members the tests depend on. */
|
||||
interface AuthProviderInternals {
|
||||
storage: StorageBackend;
|
||||
tokenHandler: TokenRequestHandler;
|
||||
tokenSubject: BehaviorSubject<TokenResponse | undefined>;
|
||||
authConfigValue: unknown;
|
||||
localConfiguration: AuthorizationServiceConfiguration;
|
||||
notify(action: IAuthAction): void;
|
||||
}
|
||||
|
||||
describe('AuthProvider', () => {
|
||||
let provider: AuthProvider;
|
||||
let internals: AuthProviderInternals;
|
||||
|
||||
let storageGetItem: jasmine.Spy;
|
||||
let storageSetItem: jasmine.Spy;
|
||||
let storageRemoveItem: jasmine.Spy;
|
||||
let tokenRequest: jasmine.Spy;
|
||||
let revokeRequest: jasmine.Spy;
|
||||
|
||||
/** Seeds storage with the given token and loads it through the public API. */
|
||||
async function givenLoadedToken(token?: TokenResponse): Promise<void> {
|
||||
storageGetItem.and.resolveTo(token ? JSON.stringify(token.toJson()) : MISSING_STORAGE_VALUE);
|
||||
await provider.loadTokenFromStorage();
|
||||
}
|
||||
|
||||
/** Records every auth event emitted after this call (skips the replayed last one). */
|
||||
function recordEvents(): AuthActions[] {
|
||||
const events: AuthActions[] = [];
|
||||
provider.events$.pipe(skip(1)).subscribe(event => events.push(event.action));
|
||||
return events;
|
||||
}
|
||||
|
||||
const isAuthenticated = () => firstValueFrom(provider.isAuthenticated$);
|
||||
const isLoggedIn = () => firstValueFrom(provider.isLoggedIn$);
|
||||
|
||||
beforeEach(() => {
|
||||
jasmine.clock().install();
|
||||
jasmine.clock().mockDate(new Date(NOW_MS));
|
||||
|
||||
// AuthProvider uses inject(), so it has to be created inside an injection context.
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
{provide: Platform, useValue: {is: () => false}},
|
||||
{provide: ConfigProvider, useValue: {}},
|
||||
],
|
||||
});
|
||||
|
||||
provider = TestBed.inject(AuthProvider);
|
||||
internals = provider as unknown as AuthProviderInternals;
|
||||
|
||||
internals.authConfigValue = {
|
||||
server_host: 'https://idp.example.org',
|
||||
client_id: 'test-client',
|
||||
redirect_url: 'https://app.example.org/callback',
|
||||
scopes: 'openid profile',
|
||||
pkce: true,
|
||||
};
|
||||
|
||||
internals.localConfiguration = new AuthorizationServiceConfiguration({
|
||||
authorization_endpoint: 'https://idp.example.org/authorize',
|
||||
token_endpoint: 'https://idp.example.org/token',
|
||||
revocation_endpoint: 'https://idp.example.org/revoke',
|
||||
});
|
||||
|
||||
// Never touch real storage or the network.
|
||||
storageGetItem = spyOn(internals.storage, 'getItem').and.resolveTo(MISSING_STORAGE_VALUE);
|
||||
storageSetItem = spyOn(internals.storage, 'setItem').and.resolveTo();
|
||||
storageRemoveItem = spyOn(internals.storage, 'removeItem').and.resolveTo();
|
||||
tokenRequest = spyOn(internals.tokenHandler, 'performTokenRequest').and.rejectWith(
|
||||
new Error('Unexpected token request'),
|
||||
);
|
||||
revokeRequest = spyOn(internals.tokenHandler, 'performRevokeTokenRequest').and.resolveTo(true);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jasmine.clock().uninstall();
|
||||
});
|
||||
|
||||
describe('isAuthenticated$', () => {
|
||||
it('should provide false through isAuthenticated$ when there is no token response', async () => {
|
||||
// Start authenticated so the test cannot pass on the initial value alone.
|
||||
await givenLoadedToken(makeToken());
|
||||
expect(await isAuthenticated()).toBeTrue();
|
||||
|
||||
await givenLoadedToken();
|
||||
|
||||
expect(internals.tokenSubject.value).toBeUndefined();
|
||||
expect(await isAuthenticated()).toBeFalse();
|
||||
});
|
||||
|
||||
it('should provide true through isAuthenticated$ when access token is valid', async () => {
|
||||
await givenLoadedToken(makeToken());
|
||||
|
||||
expect(await isAuthenticated()).toBeTrue();
|
||||
});
|
||||
|
||||
it('should provide false through isAuthenticated$ when access token is invalid', async () => {
|
||||
await givenLoadedToken(makeToken({remainingSeconds: -TOKEN_LIFETIME}));
|
||||
|
||||
expect(await isAuthenticated()).toBeFalse();
|
||||
// Still logged in: a refresh token is available.
|
||||
expect(await isLoggedIn()).toBeTrue();
|
||||
});
|
||||
|
||||
it('should provide true through isAuthenticated$ after an expired token was refreshed', async () => {
|
||||
await givenLoadedToken(makeToken({remainingSeconds: -60}));
|
||||
expect(await isAuthenticated()).toBeFalse();
|
||||
|
||||
tokenRequest.and.resolveTo(makeToken({accessToken: 'new-access-token'}));
|
||||
await provider.getValidToken();
|
||||
|
||||
expect(await isAuthenticated()).toBeTrue();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getValidToken', () => {
|
||||
it('returns the current token without refreshing when it is valid', async () => {
|
||||
await givenLoadedToken(makeToken({accessToken: 'current-access-token'}));
|
||||
|
||||
const token = await provider.getValidToken();
|
||||
|
||||
expect(tokenRequest).not.toHaveBeenCalled();
|
||||
expect(token.accessToken).toBe('current-access-token');
|
||||
});
|
||||
|
||||
it('refreshes the token when the access token is expired', async () => {
|
||||
await givenLoadedToken(makeToken({remainingSeconds: -60}));
|
||||
tokenRequest.and.resolveTo(
|
||||
makeToken({accessToken: 'new-access-token', refreshToken: 'new-refresh-token'}),
|
||||
);
|
||||
|
||||
const token = await provider.getValidToken();
|
||||
|
||||
expect(tokenRequest).toHaveBeenCalledTimes(1);
|
||||
expect(token.accessToken).toBe('new-access-token');
|
||||
expect(token.refreshToken).toBe('new-refresh-token');
|
||||
expect(storageSetItem).toHaveBeenCalledOnceWith(TOKEN_RESPONSE_KEY, jasmine.any(String));
|
||||
});
|
||||
|
||||
it('refreshes the token when the access token expires within the buffer', async () => {
|
||||
// 30 seconds left, the buffer for a 1 hour token is 60 seconds.
|
||||
await givenLoadedToken(makeToken({remainingSeconds: 30}));
|
||||
tokenRequest.and.resolveTo(makeToken({accessToken: 'new-access-token'}));
|
||||
|
||||
const token = await provider.getValidToken();
|
||||
|
||||
expect(tokenRequest).toHaveBeenCalledTimes(1);
|
||||
expect(token.accessToken).toBe('new-access-token');
|
||||
});
|
||||
|
||||
it('does not refresh a fresh short-lived (10 minute) token', async () => {
|
||||
await givenLoadedToken(
|
||||
makeToken({accessToken: 'current-access-token', lifetime: SHORT_TOKEN_LIFETIME}),
|
||||
);
|
||||
|
||||
const token = await provider.getValidToken();
|
||||
|
||||
expect(tokenRequest).not.toHaveBeenCalled();
|
||||
expect(token.accessToken).toBe('current-access-token');
|
||||
});
|
||||
|
||||
it('accepts a freshly refreshed short-lived (10 minute) token', async () => {
|
||||
await givenLoadedToken(makeToken({remainingSeconds: -60, lifetime: SHORT_TOKEN_LIFETIME}));
|
||||
tokenRequest.and.resolveTo(
|
||||
makeToken({accessToken: 'new-access-token', lifetime: SHORT_TOKEN_LIFETIME}),
|
||||
);
|
||||
|
||||
const token = await provider.getValidToken();
|
||||
|
||||
expect(tokenRequest).toHaveBeenCalledTimes(1);
|
||||
expect(token.accessToken).toBe('new-access-token');
|
||||
});
|
||||
|
||||
it('keeps the previous refresh token when the refresh response contains none', async () => {
|
||||
await givenLoadedToken(makeToken({remainingSeconds: -60, refreshToken: 'old-refresh-token'}));
|
||||
tokenRequest.and.resolveTo(makeToken({accessToken: 'new-access-token', refreshToken: false}));
|
||||
|
||||
const token = await provider.getValidToken();
|
||||
|
||||
expect(token.accessToken).toBe('new-access-token');
|
||||
expect(token.refreshToken).toBe('old-refresh-token');
|
||||
});
|
||||
|
||||
it('rejects without a request when the token is expired and has no refresh token', async () => {
|
||||
await givenLoadedToken(makeToken({remainingSeconds: -60, refreshToken: false}));
|
||||
|
||||
await expectAsync(provider.getValidToken()).toBeRejectedWithError(/No Refresh Token Available/);
|
||||
expect(tokenRequest).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('sends only one refresh request for parallel callers', async () => {
|
||||
await givenLoadedToken(makeToken({remainingSeconds: -60}));
|
||||
tokenRequest.and.resolveTo(makeToken({accessToken: 'new-access-token'}));
|
||||
|
||||
const tokens = await Promise.all([
|
||||
provider.getValidToken(),
|
||||
provider.getValidToken(),
|
||||
provider.getValidToken(),
|
||||
]);
|
||||
|
||||
expect(tokenRequest).toHaveBeenCalledTimes(1);
|
||||
expect(tokens.map(token => token.accessToken)).toEqual([
|
||||
'new-access-token',
|
||||
'new-access-token',
|
||||
'new-access-token',
|
||||
]);
|
||||
});
|
||||
|
||||
it('refreshes again after a previous refresh has finished', async () => {
|
||||
await givenLoadedToken(makeToken({remainingSeconds: -60}));
|
||||
tokenRequest.and.resolveTo(makeToken({accessToken: 'first', remainingSeconds: -60}));
|
||||
|
||||
// The first refresh returns an already expired token, so the call fails ...
|
||||
await expectAsync(provider.getValidToken()).toBeRejected();
|
||||
|
||||
// ... and the next call must start a new request instead of reusing the old one.
|
||||
tokenRequest.and.resolveTo(makeToken({accessToken: 'second'}));
|
||||
expect((await provider.getValidToken()).accessToken).toBe('second');
|
||||
expect(tokenRequest).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
describe('without a refresh token (browser)', () => {
|
||||
it('uses the token within the buffer instead of failing early', async () => {
|
||||
await givenLoadedToken(
|
||||
makeToken({accessToken: 'current-access-token', remainingSeconds: 60, refreshToken: false}),
|
||||
);
|
||||
|
||||
const token = await provider.getValidToken();
|
||||
|
||||
expect(token.accessToken).toBe('current-access-token');
|
||||
expect(tokenRequest).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('logs the user out when the token expires while the app is open', async () => {
|
||||
await givenLoadedToken(makeToken({remainingSeconds: 60, refreshToken: false}));
|
||||
expect(await isLoggedIn()).toBeTrue();
|
||||
const events = recordEvents();
|
||||
|
||||
jasmine.clock().tick(2 * 60 * 1000);
|
||||
|
||||
await expectAsync(provider.getValidToken()).toBeRejectedWithError(/No Refresh Token Available/);
|
||||
expect(await isLoggedIn()).toBeFalse();
|
||||
expect(internals.tokenSubject.value).toBeUndefined();
|
||||
expect(storageRemoveItem).toHaveBeenCalledOnceWith(TOKEN_RESPONSE_KEY);
|
||||
expect(events).toContain(AuthActions.SessionExpired);
|
||||
expect(tokenRequest).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not restore an expired token from storage', async () => {
|
||||
await givenLoadedToken(makeToken({remainingSeconds: -60, refreshToken: false}));
|
||||
|
||||
expect(await isLoggedIn()).toBeFalse();
|
||||
expect(internals.tokenSubject.value).toBeUndefined();
|
||||
expect(storageRemoveItem).toHaveBeenCalledOnceWith(TOKEN_RESPONSE_KEY);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when refresh fails with invalid_grant from the native requestor', () => {
|
||||
// CapacitorRequestor rejects non-2xx responses itself and keeps the OAuth body in extras.
|
||||
const refreshError = new AppAuthError('HTTP 400', {error: INVALID_GRANT});
|
||||
|
||||
it('logs the user out', async () => {
|
||||
await givenLoadedToken(makeToken({remainingSeconds: -60}));
|
||||
const events = recordEvents();
|
||||
tokenRequest.and.rejectWith(refreshError);
|
||||
|
||||
await expectAsync(provider.getValidToken()).toBeRejectedWith(refreshError);
|
||||
|
||||
expect(await isLoggedIn()).toBeFalse();
|
||||
expect(storageRemoveItem).toHaveBeenCalledOnceWith(TOKEN_RESPONSE_KEY);
|
||||
expect(events).toEqual([AuthActions.RefreshFailed, AuthActions.SessionExpired]);
|
||||
});
|
||||
|
||||
it('logs the user out only once for parallel callers', async () => {
|
||||
await givenLoadedToken(makeToken({remainingSeconds: -60}));
|
||||
const events = recordEvents();
|
||||
tokenRequest.and.rejectWith(refreshError);
|
||||
|
||||
const results = await Promise.allSettled([provider.getValidToken(), provider.getValidToken()]);
|
||||
|
||||
expect(results.map(result => result.status)).toEqual(['rejected', 'rejected']);
|
||||
expect(tokenRequest).toHaveBeenCalledTimes(1);
|
||||
expect(events.filter(event => event === AuthActions.SessionExpired).length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when refresh fails with invalid_grant', () => {
|
||||
const refreshError = new AppAuthError(INVALID_GRANT);
|
||||
|
||||
beforeEach(async () => {
|
||||
await givenLoadedToken(makeToken({remainingSeconds: -60, refreshToken: 'invalid-refresh-token'}));
|
||||
expect(await isLoggedIn()).toBeTrue();
|
||||
|
||||
tokenRequest.and.rejectWith(refreshError);
|
||||
|
||||
await expectAsync(provider.getValidToken()).toBeRejectedWith(refreshError);
|
||||
});
|
||||
|
||||
it('changes isLoggedIn to false', async () => {
|
||||
expect(await isLoggedIn()).toBeFalse();
|
||||
});
|
||||
|
||||
it('removes the token from storage', () => {
|
||||
expect(storageRemoveItem).toHaveBeenCalledOnceWith(TOKEN_RESPONSE_KEY);
|
||||
});
|
||||
|
||||
it('clears the current token', () => {
|
||||
expect(internals.tokenSubject.value).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when refresh fails with a temporary error', () => {
|
||||
// An AppAuthError that is not invalid_grant, e.g. a network failure.
|
||||
const refreshError = new AppAuthError('Network Error');
|
||||
|
||||
beforeEach(async () => {
|
||||
await givenLoadedToken(makeToken({remainingSeconds: -60}));
|
||||
tokenRequest.and.rejectWith(refreshError);
|
||||
|
||||
await expectAsync(provider.getValidToken()).toBeRejectedWith(refreshError);
|
||||
});
|
||||
|
||||
it('keeps the user logged in', async () => {
|
||||
expect(await isLoggedIn()).toBeTrue();
|
||||
});
|
||||
|
||||
it('does not remove the token from storage', () => {
|
||||
expect(storageRemoveItem).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('keeps the current token for a later retry', () => {
|
||||
expect(internals.tokenSubject.value?.refreshToken).toBe('refresh-token');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('expiry timer', () => {
|
||||
it('logs the user out when the token expires, without any page asking for a token', async () => {
|
||||
await givenLoadedToken(makeToken({remainingSeconds: 60, refreshToken: false}));
|
||||
const events = recordEvents();
|
||||
|
||||
jasmine.clock().tick(59 * 1000);
|
||||
await flushPromises();
|
||||
expect(await isLoggedIn()).toBeTrue();
|
||||
|
||||
jasmine.clock().tick(2 * 1000);
|
||||
await flushPromises();
|
||||
|
||||
expect(await isLoggedIn()).toBeFalse();
|
||||
expect(storageRemoveItem).toHaveBeenCalledOnceWith(TOKEN_RESPONSE_KEY);
|
||||
expect(events).toEqual([AuthActions.SessionExpired]);
|
||||
});
|
||||
|
||||
it('refreshes the token shortly before it expires when a refresh token is available', async () => {
|
||||
await givenLoadedToken(makeToken({remainingSeconds: 120, lifetime: SHORT_TOKEN_LIFETIME}));
|
||||
tokenRequest.and.resolveTo(
|
||||
makeToken({accessToken: 'new-access-token', lifetime: SHORT_TOKEN_LIFETIME}),
|
||||
);
|
||||
|
||||
// Due 60 seconds before expiry.
|
||||
jasmine.clock().tick(61 * 1000);
|
||||
await flushPromises();
|
||||
|
||||
expect(tokenRequest).toHaveBeenCalledTimes(1);
|
||||
expect(internals.tokenSubject.value?.accessToken).toBe('new-access-token');
|
||||
expect(await isLoggedIn()).toBeTrue();
|
||||
});
|
||||
|
||||
it('logs the user out when the refresh token is rejected', async () => {
|
||||
await givenLoadedToken(makeToken({remainingSeconds: 30}));
|
||||
tokenRequest.and.rejectWith(new AppAuthError(INVALID_GRANT));
|
||||
|
||||
jasmine.clock().tick(2 * 1000);
|
||||
await flushPromises();
|
||||
|
||||
expect(await isLoggedIn()).toBeFalse();
|
||||
});
|
||||
|
||||
it('stays logged in and retries later after a temporary error', async () => {
|
||||
await givenLoadedToken(makeToken({remainingSeconds: 30}));
|
||||
tokenRequest.and.rejectWith(new AppAuthError('Network Error'));
|
||||
|
||||
jasmine.clock().tick(2 * 1000);
|
||||
await flushPromises();
|
||||
|
||||
expect(await isLoggedIn()).toBeTrue();
|
||||
expect(tokenRequest).toHaveBeenCalledTimes(1);
|
||||
|
||||
tokenRequest.and.resolveTo(makeToken({accessToken: 'new-access-token'}));
|
||||
jasmine.clock().tick(61 * 1000);
|
||||
await flushPromises();
|
||||
|
||||
expect(tokenRequest).toHaveBeenCalledTimes(2);
|
||||
expect(internals.tokenSubject.value?.accessToken).toBe('new-access-token');
|
||||
});
|
||||
|
||||
it('does not fire after logout', async () => {
|
||||
await givenLoadedToken(makeToken({remainingSeconds: 60, refreshToken: false}));
|
||||
await provider.signOut();
|
||||
const events = recordEvents();
|
||||
|
||||
jasmine.clock().tick(2 * 60 * 1000);
|
||||
await flushPromises();
|
||||
|
||||
expect(events).not.toContain(AuthActions.SessionExpired);
|
||||
});
|
||||
});
|
||||
|
||||
describe('failed sign-in', () => {
|
||||
it('keeps an existing session', async () => {
|
||||
await givenLoadedToken(makeToken({accessToken: 'still-valid'}));
|
||||
|
||||
internals.notify({action: AuthActions.SignInFailed, error: new Error('state mismatch')});
|
||||
|
||||
expect(await isLoggedIn()).toBeTrue();
|
||||
expect(internals.tokenSubject.value?.accessToken).toBe('still-valid');
|
||||
expect(storageRemoveItem).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('stays logged out without a session', async () => {
|
||||
await givenLoadedToken();
|
||||
|
||||
internals.notify({action: AuthActions.SignInFailed, error: new Error('access_denied')});
|
||||
|
||||
expect(await isLoggedIn()).toBeFalse();
|
||||
});
|
||||
});
|
||||
|
||||
describe('signOut', () => {
|
||||
/** The [token, token_type_hint, client_id] of each revoke request, in order. */
|
||||
const revokedTokens = () =>
|
||||
revokeRequest.calls.allArgs().map(([, request]) => {
|
||||
const json = (request as RevokeTokenRequest).toJson();
|
||||
return [json.token, json.token_type_hint, json.client_id];
|
||||
});
|
||||
|
||||
it('revokes the refresh token, then the access token', async () => {
|
||||
await givenLoadedToken(makeToken({accessToken: 'access', refreshToken: 'refresh'}));
|
||||
|
||||
await provider.signOut();
|
||||
|
||||
expect(revokedTokens()).toEqual([
|
||||
['refresh', 'refresh_token', 'test-client'],
|
||||
['access', 'access_token', 'test-client'],
|
||||
]);
|
||||
expect(await isLoggedIn()).toBeFalse();
|
||||
expect(storageRemoveItem).toHaveBeenCalledWith(TOKEN_RESPONSE_KEY);
|
||||
});
|
||||
|
||||
it('revokes only the access token when there is no refresh token (browser)', async () => {
|
||||
await givenLoadedToken(makeToken({accessToken: 'access', refreshToken: false}));
|
||||
|
||||
await provider.signOut();
|
||||
|
||||
expect(revokedTokens()).toEqual([['access', 'access_token', 'test-client']]);
|
||||
});
|
||||
|
||||
it('logs out locally before the IdP answers', async () => {
|
||||
await givenLoadedToken(makeToken());
|
||||
let loggedInWhileRevoking: boolean | undefined;
|
||||
revokeRequest.and.callFake(async () => {
|
||||
loggedInWhileRevoking = await isLoggedIn();
|
||||
return true;
|
||||
});
|
||||
|
||||
await provider.signOut();
|
||||
|
||||
expect(loggedInWhileRevoking).toBeFalse();
|
||||
});
|
||||
|
||||
it('still logs out and tries the access token when revoking the refresh token fails', async () => {
|
||||
await givenLoadedToken(makeToken());
|
||||
const events = recordEvents();
|
||||
revokeRequest.and.returnValues(
|
||||
Promise.reject(new AppAuthError('Network Error')),
|
||||
Promise.resolve(true),
|
||||
);
|
||||
|
||||
await provider.signOut();
|
||||
|
||||
expect(revokeRequest).toHaveBeenCalledTimes(2);
|
||||
expect(await isLoggedIn()).toBeFalse();
|
||||
expect(events).toEqual([AuthActions.SignOutSuccess, AuthActions.RevokeTokensFailed]);
|
||||
});
|
||||
|
||||
it('does not wait for an IdP that does not answer', async () => {
|
||||
await givenLoadedToken(makeToken());
|
||||
revokeRequest.and.returnValue(new Promise(() => {}));
|
||||
let done = false;
|
||||
|
||||
const signOut = provider.signOut().then(() => (done = true));
|
||||
await flushPromises();
|
||||
expect(done).toBeFalse();
|
||||
expect(await isLoggedIn()).toBeFalse();
|
||||
|
||||
jasmine.clock().tick(5000);
|
||||
await signOut;
|
||||
|
||||
expect(done).toBeTrue();
|
||||
});
|
||||
|
||||
it('revokes nothing when no revocation endpoint is configured', async () => {
|
||||
internals.localConfiguration = new AuthorizationServiceConfiguration({
|
||||
authorization_endpoint: 'https://idp.example.org/authorize',
|
||||
token_endpoint: 'https://idp.example.org/token',
|
||||
revocation_endpoint: '',
|
||||
});
|
||||
await givenLoadedToken(makeToken());
|
||||
|
||||
await provider.signOut();
|
||||
|
||||
expect(revokeRequest).not.toHaveBeenCalled();
|
||||
expect(await isLoggedIn()).toBeFalse();
|
||||
});
|
||||
|
||||
it('revokes nothing when asked not to', async () => {
|
||||
await givenLoadedToken(makeToken());
|
||||
|
||||
await provider.signOut(undefined, false);
|
||||
|
||||
expect(revokeRequest).not.toHaveBeenCalled();
|
||||
expect(await isLoggedIn()).toBeFalse();
|
||||
});
|
||||
|
||||
it('revokes nothing when nobody is logged in', async () => {
|
||||
await givenLoadedToken();
|
||||
|
||||
await provider.signOut();
|
||||
|
||||
expect(revokeRequest).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('isInvalidGrant', () => {
|
||||
it('detects invalid_grant as error message', () => {
|
||||
expect(isInvalidGrant(new AppAuthError(INVALID_GRANT))).toBeTrue();
|
||||
});
|
||||
|
||||
it('detects invalid_grant in the response body', () => {
|
||||
expect(isInvalidGrant(new AppAuthError('HTTP 400', {error: INVALID_GRANT}))).toBeTrue();
|
||||
});
|
||||
|
||||
it('ignores other OAuth errors', () => {
|
||||
expect(isInvalidGrant(new AppAuthError('HTTP 400', {error: 'invalid_request'}))).toBeFalse();
|
||||
});
|
||||
|
||||
it('ignores network and non-AppAuth errors', () => {
|
||||
expect(isInvalidGrant(new AppAuthError('HTTP 503'))).toBeFalse();
|
||||
expect(isInvalidGrant(new Error(INVALID_GRANT))).toBeFalse();
|
||||
expect(isInvalidGrant()).toBeFalse();
|
||||
});
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,526 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 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/>.
|
||||
*/
|
||||
// Temporary use of direct file until the version with bug fix is released
|
||||
// https://github.com/wi3land/ionic-appauth/blob/3716f4fc6b5491b0b75e049be0a47a5af8c4da6f/src/auth-service.ts
|
||||
// Bug: https://github.com/wi3land/ionic-appauth/issues/154
|
||||
import {
|
||||
AuthorizationError,
|
||||
AuthorizationNotifier,
|
||||
AuthorizationRequest,
|
||||
AuthorizationRequestHandler,
|
||||
AuthorizationRequestJson,
|
||||
AuthorizationResponse,
|
||||
AuthorizationServiceConfiguration,
|
||||
BaseTokenRequestHandler,
|
||||
DefaultCrypto,
|
||||
GRANT_TYPE_AUTHORIZATION_CODE,
|
||||
GRANT_TYPE_REFRESH_TOKEN,
|
||||
JQueryRequestor,
|
||||
LocalStorageBackend,
|
||||
Requestor,
|
||||
RevokeTokenRequest,
|
||||
RevokeTokenRequestJson,
|
||||
StorageBackend,
|
||||
StringMap,
|
||||
TokenRequest,
|
||||
TokenRequestHandler,
|
||||
TokenRequestJson,
|
||||
TokenResponse,
|
||||
} from '@openid/appauth';
|
||||
import {
|
||||
ActionHistoryObserver,
|
||||
AuthActionBuilder,
|
||||
AuthActions,
|
||||
AuthObserver,
|
||||
AUTHORIZATION_RESPONSE_KEY,
|
||||
AuthSubject,
|
||||
BaseAuthObserver,
|
||||
Browser,
|
||||
DefaultBrowser,
|
||||
EndSessionHandler,
|
||||
EndSessionRequest,
|
||||
EndSessionRequestJson,
|
||||
IAuthAction,
|
||||
IAuthConfig,
|
||||
IAuthService,
|
||||
IonicAuthorizationRequestHandler,
|
||||
IonicEndSessionHandler,
|
||||
IonicUserInfoHandler,
|
||||
SessionObserver,
|
||||
UserInfoHandler,
|
||||
} from 'ionic-appauth';
|
||||
import {BehaviorSubject, Observable} from 'rxjs';
|
||||
|
||||
const TOKEN_RESPONSE_KEY = 'token_response';
|
||||
const AUTH_EXPIRY_BUFFER = 10 * 60 * -1; // 10 mins in seconds
|
||||
|
||||
export abstract class AuthService implements IAuthService {
|
||||
private _configuration?: AuthorizationServiceConfiguration;
|
||||
|
||||
private _authConfig?: IAuthConfig;
|
||||
|
||||
private _authSubject: AuthSubject = new AuthSubject();
|
||||
|
||||
private _actionHistory: ActionHistoryObserver = new ActionHistoryObserver();
|
||||
|
||||
private _session: SessionObserver = new SessionObserver();
|
||||
|
||||
private _authSubjectV2 = new BehaviorSubject<IAuthAction>(AuthActionBuilder.Init());
|
||||
|
||||
private _tokenSubject = new BehaviorSubject<TokenResponse | undefined>(undefined);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
private _userSubject = new BehaviorSubject<any>(undefined);
|
||||
|
||||
private _authenticatedSubject = new BehaviorSubject<boolean>(false);
|
||||
|
||||
private _loggedInSubject = new BehaviorSubject<boolean>(false);
|
||||
|
||||
private _initComplete = new BehaviorSubject<boolean>(false);
|
||||
|
||||
protected tokenHandler: TokenRequestHandler;
|
||||
|
||||
protected userInfoHandler: UserInfoHandler;
|
||||
|
||||
protected requestHandler: AuthorizationRequestHandler;
|
||||
|
||||
protected endSessionHandler: EndSessionHandler;
|
||||
|
||||
constructor(
|
||||
protected browser: Browser = new DefaultBrowser(),
|
||||
protected storage: StorageBackend = new LocalStorageBackend(),
|
||||
protected requestor: Requestor = new JQueryRequestor(),
|
||||
) {
|
||||
this.tokenHandler = new BaseTokenRequestHandler(requestor);
|
||||
this.userInfoHandler = new IonicUserInfoHandler(requestor);
|
||||
this.requestHandler = new IonicAuthorizationRequestHandler(browser, storage);
|
||||
this.endSessionHandler = new IonicEndSessionHandler(browser);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated independant observers have been replaced by Rxjs
|
||||
* this will be removed in a future release
|
||||
* please use $ suffixed observers in future
|
||||
*/
|
||||
get history(): IAuthAction[] {
|
||||
return [...this._actionHistory.history];
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated independant observers have been replaced by Rxjs
|
||||
* this will be removed in a future release
|
||||
* please use $ suffixed observers in future
|
||||
*/
|
||||
get session() {
|
||||
return this._session.session;
|
||||
}
|
||||
|
||||
get token$(): Observable<TokenResponse | undefined> {
|
||||
return this._tokenSubject.asObservable();
|
||||
}
|
||||
|
||||
get isAuthenticated$(): Observable<boolean> {
|
||||
return this._authenticatedSubject.asObservable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Similar to isAuthenticated$, but will also return true if the token is expired
|
||||
*/
|
||||
get isLoggedIn$(): Observable<boolean> {
|
||||
return this._loggedInSubject.asObservable();
|
||||
}
|
||||
|
||||
get initComplete$(): Observable<boolean> {
|
||||
return this._initComplete.asObservable();
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
get user$(): Observable<any> {
|
||||
return this._userSubject.asObservable();
|
||||
}
|
||||
|
||||
get events$(): Observable<IAuthAction> {
|
||||
return this._authSubjectV2.asObservable();
|
||||
}
|
||||
|
||||
get authConfig(): IAuthConfig {
|
||||
if (!this._authConfig) throw new Error('AuthConfig Not Defined');
|
||||
|
||||
return this._authConfig;
|
||||
}
|
||||
|
||||
set authConfig(value: IAuthConfig) {
|
||||
this._authConfig = value;
|
||||
}
|
||||
|
||||
get configuration(): Promise<AuthorizationServiceConfiguration> {
|
||||
if (!this._configuration) {
|
||||
return AuthorizationServiceConfiguration.fetchFromIssuer(
|
||||
this.authConfig.server_host,
|
||||
this.requestor,
|
||||
).catch(() => {
|
||||
throw new Error('Unable To Obtain Server Configuration');
|
||||
});
|
||||
}
|
||||
|
||||
if (this._configuration == undefined) {
|
||||
throw new Error('Unable To Obtain Server Configuration');
|
||||
} else {
|
||||
return Promise.resolve(this._configuration);
|
||||
}
|
||||
}
|
||||
|
||||
public async init() {
|
||||
this.setupAuthorizationNotifier();
|
||||
this.loadTokenFromStorage();
|
||||
this.addActionObserver(this._actionHistory);
|
||||
this.addActionObserver(this._session);
|
||||
}
|
||||
|
||||
protected notifyActionListers(action: IAuthAction) {
|
||||
/* eslint-disable unicorn/no-useless-undefined */
|
||||
switch (action.action) {
|
||||
case AuthActions.SignInFailed:
|
||||
case AuthActions.SignOutSuccess:
|
||||
case AuthActions.SignOutFailed: {
|
||||
this._tokenSubject.next(undefined);
|
||||
this._userSubject.next(undefined);
|
||||
this._authenticatedSubject.next(false);
|
||||
this._loggedInSubject.next(false);
|
||||
break;
|
||||
}
|
||||
case AuthActions.LoadTokenFromStorageFailed: {
|
||||
this._tokenSubject.next(undefined);
|
||||
this._userSubject.next(undefined);
|
||||
this._authenticatedSubject.next(false);
|
||||
this._loggedInSubject.next(false);
|
||||
this._initComplete.next(true);
|
||||
break;
|
||||
}
|
||||
case AuthActions.SignInSuccess:
|
||||
case AuthActions.RefreshSuccess: {
|
||||
this._tokenSubject.next(action.tokenResponse);
|
||||
this._authenticatedSubject.next(true);
|
||||
this._loggedInSubject.next(true);
|
||||
break;
|
||||
}
|
||||
case AuthActions.LoadTokenFromStorageSuccess: {
|
||||
this._tokenSubject.next(action.tokenResponse);
|
||||
this._authenticatedSubject.next((action.tokenResponse as TokenResponse).isValid(0));
|
||||
this._loggedInSubject.next(true);
|
||||
this._initComplete.next(true);
|
||||
break;
|
||||
}
|
||||
case AuthActions.RevokeTokensSuccess: {
|
||||
this._tokenSubject.next(undefined);
|
||||
break;
|
||||
}
|
||||
case AuthActions.LoadUserInfoSuccess: {
|
||||
this._userSubject.next(action.user);
|
||||
break;
|
||||
}
|
||||
case AuthActions.LoadUserInfoFailed: {
|
||||
this._userSubject.next(undefined);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
this._authSubjectV2.next(action);
|
||||
this._authSubject.notify(action);
|
||||
}
|
||||
|
||||
protected setupAuthorizationNotifier() {
|
||||
const notifier = new AuthorizationNotifier();
|
||||
this.requestHandler.setAuthorizationNotifier(notifier);
|
||||
notifier.setAuthorizationListener((request, response, error) =>
|
||||
this.onAuthorizationNotification(request, response, error),
|
||||
);
|
||||
}
|
||||
|
||||
protected onAuthorizationNotification(
|
||||
request: AuthorizationRequest,
|
||||
response: AuthorizationResponse | null,
|
||||
error: AuthorizationError | null,
|
||||
) {
|
||||
const codeVerifier: string | undefined =
|
||||
request.internal != undefined && this.authConfig.pkce ? request.internal.code_verifier : undefined;
|
||||
|
||||
if (response != undefined) {
|
||||
this.requestAccessToken(response.code, codeVerifier);
|
||||
} else if (error == undefined) {
|
||||
throw new Error('Unknown Error With Authentication');
|
||||
} else {
|
||||
throw new Error(error.errorDescription);
|
||||
}
|
||||
}
|
||||
|
||||
protected async internalAuthorizationCallback(url: string) {
|
||||
this.browser.closeWindow();
|
||||
await this.storage.setItem(AUTHORIZATION_RESPONSE_KEY, url);
|
||||
return this.requestHandler.completeAuthorizationRequestIfPossible();
|
||||
}
|
||||
|
||||
protected async internalEndSessionCallback() {
|
||||
this.browser.closeWindow();
|
||||
this._actionHistory.clear();
|
||||
this.notifyActionListers(AuthActionBuilder.SignOutSuccess());
|
||||
}
|
||||
|
||||
protected async performEndSessionRequest(state?: string): Promise<void> {
|
||||
if (this._tokenSubject.value == undefined) {
|
||||
//if user has no token they should not be logged in in the first place
|
||||
this.endSessionCallback();
|
||||
} else {
|
||||
const requestJson: EndSessionRequestJson = {
|
||||
postLogoutRedirectURI: this.authConfig.end_session_redirect_url,
|
||||
idTokenHint: this._tokenSubject.value.idToken || '',
|
||||
state: state || undefined,
|
||||
};
|
||||
|
||||
const request: EndSessionRequest = new EndSessionRequest(requestJson);
|
||||
const returnedUrl: string | undefined = await this.endSessionHandler.performEndSessionRequest(
|
||||
await this.configuration,
|
||||
request,
|
||||
);
|
||||
|
||||
//callback may come from showWindow or via another method
|
||||
if (returnedUrl != undefined) {
|
||||
this.endSessionCallback();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected async performAuthorizationRequest(authExtras?: StringMap, state?: string): Promise<void> {
|
||||
const requestJson: AuthorizationRequestJson = {
|
||||
response_type: AuthorizationRequest.RESPONSE_TYPE_CODE,
|
||||
client_id: this.authConfig.client_id,
|
||||
redirect_uri: this.authConfig.redirect_url,
|
||||
scope: this.authConfig.scopes,
|
||||
extras: authExtras,
|
||||
state: state || undefined,
|
||||
};
|
||||
|
||||
const request = new AuthorizationRequest(requestJson, new DefaultCrypto(), this.authConfig.pkce);
|
||||
|
||||
if (this.authConfig.pkce) await request.setupCodeVerifier();
|
||||
|
||||
return this.requestHandler.performAuthorizationRequest(await this.configuration, request);
|
||||
}
|
||||
|
||||
protected async requestAccessToken(code: string, codeVerifier?: string): Promise<void> {
|
||||
const requestJSON: TokenRequestJson = {
|
||||
grant_type: GRANT_TYPE_AUTHORIZATION_CODE,
|
||||
code: code,
|
||||
refresh_token: undefined,
|
||||
redirect_uri: this.authConfig.redirect_url,
|
||||
client_id: this.authConfig.client_id,
|
||||
extras: codeVerifier
|
||||
? {
|
||||
code_verifier: codeVerifier,
|
||||
client_secret: this.authConfig.client_secret as string,
|
||||
}
|
||||
: {
|
||||
client_secret: this.authConfig.client_secret as string,
|
||||
},
|
||||
};
|
||||
|
||||
const token: TokenResponse = await this.tokenHandler.performTokenRequest(
|
||||
await this.configuration,
|
||||
new TokenRequest(requestJSON),
|
||||
);
|
||||
await this.storage.setItem(TOKEN_RESPONSE_KEY, JSON.stringify(token.toJson()));
|
||||
this.notifyActionListers(AuthActionBuilder.SignInSuccess(token));
|
||||
}
|
||||
|
||||
protected async requestTokenRefresh() {
|
||||
if (!this._tokenSubject.value) {
|
||||
throw new Error('No Token Defined!');
|
||||
}
|
||||
|
||||
const requestJSON: TokenRequestJson = {
|
||||
grant_type: GRANT_TYPE_REFRESH_TOKEN,
|
||||
refresh_token: this._tokenSubject.value?.refreshToken,
|
||||
redirect_uri: this.authConfig.redirect_url,
|
||||
client_id: this.authConfig.client_id,
|
||||
};
|
||||
|
||||
const token: TokenResponse = await this.tokenHandler.performTokenRequest(
|
||||
await this.configuration,
|
||||
new TokenRequest(requestJSON),
|
||||
);
|
||||
if (!token.accessToken) {
|
||||
throw new Error('No Access Token Defined In Refresh Response');
|
||||
}
|
||||
await this.storage.setItem(TOKEN_RESPONSE_KEY, JSON.stringify(token.toJson()));
|
||||
this.notifyActionListers(AuthActionBuilder.RefreshSuccess(token));
|
||||
}
|
||||
|
||||
protected async internalLoadTokenFromStorage() {
|
||||
let token: TokenResponse | undefined;
|
||||
const tokenResponseString: string | null = await this.storage.getItem(TOKEN_RESPONSE_KEY);
|
||||
|
||||
if (tokenResponseString != undefined) {
|
||||
token = new TokenResponse(JSON.parse(tokenResponseString));
|
||||
|
||||
if (token) {
|
||||
return this.notifyActionListers(AuthActionBuilder.LoadTokenFromStorageSuccess(token));
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error('No Token In Storage');
|
||||
}
|
||||
|
||||
protected async requestTokenRevoke() {
|
||||
const revokeRefreshJson: RevokeTokenRequestJson = {
|
||||
token: (this._tokenSubject.value as TokenResponse).refreshToken as string,
|
||||
token_type_hint: 'refresh_token',
|
||||
client_id: this.authConfig.client_id,
|
||||
};
|
||||
|
||||
const revokeAccessJson: RevokeTokenRequestJson = {
|
||||
token: (this._tokenSubject.value as TokenResponse).accessToken,
|
||||
token_type_hint: 'access_token',
|
||||
client_id: this.authConfig.client_id,
|
||||
};
|
||||
|
||||
await this.tokenHandler.performRevokeTokenRequest(
|
||||
await this.configuration,
|
||||
new RevokeTokenRequest(revokeRefreshJson),
|
||||
);
|
||||
await this.tokenHandler.performRevokeTokenRequest(
|
||||
await this.configuration,
|
||||
new RevokeTokenRequest(revokeAccessJson),
|
||||
);
|
||||
await this.storage.removeItem(TOKEN_RESPONSE_KEY);
|
||||
this.notifyActionListers(AuthActionBuilder.RevokeTokensSuccess());
|
||||
}
|
||||
|
||||
protected async internalRequestUserInfo() {
|
||||
if (this._tokenSubject.value) {
|
||||
const userInfo = await this.userInfoHandler.performUserInfoRequest(
|
||||
await this.configuration,
|
||||
this._tokenSubject.value,
|
||||
);
|
||||
this.notifyActionListers(AuthActionBuilder.LoadUserInfoSuccess(userInfo));
|
||||
} else {
|
||||
throw new Error('No Token Available');
|
||||
}
|
||||
}
|
||||
|
||||
public async loadTokenFromStorage() {
|
||||
await this.internalLoadTokenFromStorage().catch(error => {
|
||||
this.notifyActionListers(AuthActionBuilder.LoadTokenFromStorageFailed(error));
|
||||
});
|
||||
}
|
||||
|
||||
public async signIn(authExtras?: StringMap, state?: string) {
|
||||
await this.performAuthorizationRequest(authExtras, state).catch(error => {
|
||||
this.notifyActionListers(AuthActionBuilder.SignInFailed(error));
|
||||
});
|
||||
}
|
||||
|
||||
public async signOut(state?: string, revokeTokens?: boolean) {
|
||||
if (revokeTokens) {
|
||||
await this.revokeTokens();
|
||||
}
|
||||
|
||||
await this.storage.removeItem(TOKEN_RESPONSE_KEY);
|
||||
|
||||
if ((await this.configuration).endSessionEndpoint) {
|
||||
await this.performEndSessionRequest(state).catch(error => {
|
||||
this.notifyActionListers(AuthActionBuilder.SignOutFailed(error));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public async revokeTokens() {
|
||||
await this.requestTokenRevoke().catch(error => {
|
||||
this.storage.removeItem(TOKEN_RESPONSE_KEY);
|
||||
this.notifyActionListers(AuthActionBuilder.RevokeTokensFailed(error));
|
||||
});
|
||||
}
|
||||
|
||||
public async refreshToken() {
|
||||
await this.requestTokenRefresh().catch(error => {
|
||||
this.notifyActionListers(AuthActionBuilder.RefreshFailed(error));
|
||||
});
|
||||
}
|
||||
|
||||
public async loadUserInfo() {
|
||||
await this.internalRequestUserInfo().catch(error => {
|
||||
this.notifyActionListers(AuthActionBuilder.LoadUserInfoFailed(error));
|
||||
});
|
||||
}
|
||||
|
||||
public authorizationCallback(callbackUrl: string): void {
|
||||
this.internalAuthorizationCallback(callbackUrl).catch(error => {
|
||||
this.notifyActionListers(AuthActionBuilder.SignInFailed(error));
|
||||
});
|
||||
}
|
||||
|
||||
public endSessionCallback(): void {
|
||||
this.internalEndSessionCallback().catch(error => {
|
||||
this.notifyActionListers(AuthActionBuilder.SignOutFailed(error));
|
||||
});
|
||||
}
|
||||
|
||||
public async getValidToken(buffer: number = AUTH_EXPIRY_BUFFER): Promise<TokenResponse> {
|
||||
if (this._tokenSubject.value) {
|
||||
if (this._tokenSubject.value.isValid(buffer)) {
|
||||
return this._tokenSubject.value;
|
||||
} else {
|
||||
await this.refreshToken();
|
||||
if (this._tokenSubject.value) {
|
||||
return this._tokenSubject.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error('Unable To Obtain Valid Token');
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated independant observers have been replaced by Rxjs
|
||||
* this will be removed in a future release
|
||||
* please use $ suffixed observers in future
|
||||
*/
|
||||
public addActionListener(function_: (action: IAuthAction) => void): AuthObserver {
|
||||
const observer: AuthObserver = AuthObserver.Create(function_);
|
||||
this.addActionObserver(observer);
|
||||
return observer;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated independant observers have been replaced by Rxjs
|
||||
* this will be removed in a future release
|
||||
* please use $ suffixed observers in future
|
||||
*/
|
||||
public addActionObserver(observer: BaseAuthObserver): void {
|
||||
if (this._actionHistory.lastAction) {
|
||||
observer.update(this._actionHistory.lastAction);
|
||||
}
|
||||
|
||||
this._authSubject.attach(observer);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated independant observers have been replaced by Rxjs
|
||||
* this will be removed in a future release
|
||||
* please use $ suffixed observers in future
|
||||
*/
|
||||
public removeActionObserver(observer: BaseAuthObserver): void {
|
||||
this._authSubject.detach(observer);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,328 @@
|
||||
/*
|
||||
* Copyright (C) 2026 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 {
|
||||
AuthorizationError,
|
||||
AuthorizationNotifier,
|
||||
AuthorizationRequest,
|
||||
AuthorizationResponse,
|
||||
AuthorizationServiceConfiguration,
|
||||
LocalStorageBackend,
|
||||
StorageBackend,
|
||||
} from '@openid/appauth';
|
||||
import {CapacitorAuthorizationRequestHandler} from './capacitor-authorization-request-handler';
|
||||
|
||||
const PENDING_REQUEST_KEY = 'appauth_capacitor_authorization_request';
|
||||
|
||||
/** Redirect URI of the native app (custom URL scheme). */
|
||||
const NATIVE_REDIRECT_URI = 'de.example.app://app.example.org/auth/callback';
|
||||
|
||||
/** Redirect URI of the web app (callback page). */
|
||||
const WEB_REDIRECT_URI = 'https://app.example.org/auth/callback';
|
||||
|
||||
const configuration = new AuthorizationServiceConfiguration({
|
||||
authorization_endpoint: 'https://idp.example.org/authorize',
|
||||
token_endpoint: 'https://idp.example.org/token',
|
||||
revocation_endpoint: '',
|
||||
});
|
||||
|
||||
type AuthorizationListener = jasmine.Spy<
|
||||
(
|
||||
request: AuthorizationRequest,
|
||||
response: AuthorizationResponse | null,
|
||||
error: AuthorizationError | null,
|
||||
) => void
|
||||
>;
|
||||
|
||||
/**
|
||||
* In-memory StorageBackend, so the tests can look at what the handler stores
|
||||
* (stands in for the secure storage of the native app).
|
||||
*/
|
||||
class MemoryStorage extends StorageBackend {
|
||||
readonly items = new Map<string, string>();
|
||||
|
||||
async getItem(name: string): Promise<string | null> {
|
||||
return this.items.get(name) ?? null; // eslint-disable-line unicorn/no-null
|
||||
}
|
||||
|
||||
async removeItem(name: string): Promise<void> {
|
||||
this.items.delete(name);
|
||||
}
|
||||
|
||||
async clear(): Promise<void> {
|
||||
this.items.clear();
|
||||
}
|
||||
|
||||
async setItem(name: string, value: string): Promise<void> {
|
||||
this.items.set(name, value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Connects a spy that receives the result of each completed login.
|
||||
*/
|
||||
function listenForResults(handler: CapacitorAuthorizationRequestHandler): AuthorizationListener {
|
||||
const notifier = new AuthorizationNotifier();
|
||||
const listener: AuthorizationListener = jasmine.createSpy('authorizationListener');
|
||||
notifier.setAuthorizationListener(listener);
|
||||
handler.setAuthorizationNotifier(notifier);
|
||||
return listener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts a login like AuthProvider.signIn() does and returns its state.
|
||||
*/
|
||||
async function startLogin(
|
||||
handler: CapacitorAuthorizationRequestHandler,
|
||||
redirectUri: string,
|
||||
): Promise<string> {
|
||||
const request = new AuthorizationRequest({
|
||||
response_type: AuthorizationRequest.RESPONSE_TYPE_CODE,
|
||||
client_id: 'test-client',
|
||||
redirect_uri: redirectUri,
|
||||
scope: 'openid',
|
||||
});
|
||||
|
||||
await handler.performAuthorizationRequest(configuration, request);
|
||||
|
||||
return request.state;
|
||||
}
|
||||
|
||||
/**
|
||||
* The callback URL as the IdP would send it back.
|
||||
*/
|
||||
function callbackUrl(redirectUri: string, parameters: Record<string, string>): string {
|
||||
return `${redirectUri}?${new URLSearchParams(parameters).toString()}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* The AppAuth keys in localStorage (pending logins on the web).
|
||||
*/
|
||||
function appAuthKeys(): string[] {
|
||||
return Object.keys(localStorage).filter(key => key.includes('appauth'));
|
||||
}
|
||||
|
||||
describe('CapacitorAuthorizationRequestHandler', () => {
|
||||
describe('in the native app', () => {
|
||||
let storage: MemoryStorage;
|
||||
let handler: CapacitorAuthorizationRequestHandler;
|
||||
let listener: AuthorizationListener;
|
||||
let windowOpen: jasmine.Spy;
|
||||
let browserClose: jasmine.Spy;
|
||||
|
||||
const login = () => startLogin(handler, NATIVE_REDIRECT_URI);
|
||||
const callback = (parameters: Record<string, string>) => callbackUrl(NATIVE_REDIRECT_URI, parameters);
|
||||
|
||||
beforeEach(() => {
|
||||
storage = new MemoryStorage();
|
||||
// Same setup as AuthProvider in the native app: Capacitor browser (the defaults).
|
||||
handler = new CapacitorAuthorizationRequestHandler(storage);
|
||||
listener = listenForResults(handler);
|
||||
|
||||
// In tests, @capacitor/browser uses its web implementation,
|
||||
// which opens the page with window.open() and closes that window again.
|
||||
browserClose = jasmine.createSpy('close');
|
||||
windowOpen = spyOn(window, 'open').and.returnValue({close: browserClose} as unknown as Window);
|
||||
});
|
||||
|
||||
describe('starting the login', () => {
|
||||
it('opens the authorization URL with state and PKCE (S256) in the in-app browser', async () => {
|
||||
const state = await login();
|
||||
|
||||
const url = new URL(windowOpen.calls.mostRecent().args[0] as string);
|
||||
expect(url.origin + url.pathname).toBe('https://idp.example.org/authorize');
|
||||
expect(url.searchParams.get('state')).toBe(state);
|
||||
expect(url.searchParams.get('redirect_uri')).toBe(NATIVE_REDIRECT_URI);
|
||||
expect(url.searchParams.get('code_challenge_method')).toBe('S256');
|
||||
expect(url.searchParams.get('code_challenge')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('stores the pending request including the PKCE code_verifier', async () => {
|
||||
const state = await login();
|
||||
|
||||
const stored = JSON.parse(storage.items.get(PENDING_REQUEST_KEY)!);
|
||||
expect(stored.state).toBe(state);
|
||||
expect(stored.internal.code_verifier).toBeTruthy();
|
||||
});
|
||||
|
||||
it('keeps only one pending login, however often the login is started', async () => {
|
||||
await login();
|
||||
await login();
|
||||
await login();
|
||||
|
||||
expect([...storage.items.keys()]).toEqual([PENDING_REQUEST_KEY]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('completing the login', () => {
|
||||
it('delivers the code together with the matching request', async () => {
|
||||
const state = await login();
|
||||
const verifier = JSON.parse(storage.items.get(PENDING_REQUEST_KEY)!).internal.code_verifier;
|
||||
|
||||
await handler.completeAuthorizationRequestFromUrl(callback({code: 'auth-code', state}));
|
||||
|
||||
expect(listener).toHaveBeenCalledTimes(1);
|
||||
const [request, response, error] = listener.calls.mostRecent().args;
|
||||
expect(response?.code).toBe('auth-code');
|
||||
expect(error).toBeNull();
|
||||
expect(request.internal?.['code_verifier']).toBe(verifier);
|
||||
});
|
||||
|
||||
it('closes the in-app browser when the callback arrives', async () => {
|
||||
const state = await login();
|
||||
|
||||
await handler.completeAuthorizationRequestFromUrl(callback({code: 'auth-code', state}));
|
||||
|
||||
expect(browserClose).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('accepts a callback only once', async () => {
|
||||
const state = await login();
|
||||
const url = callback({code: 'auth-code', state});
|
||||
|
||||
await handler.completeAuthorizationRequestFromUrl(url);
|
||||
expect(storage.items.has(PENDING_REQUEST_KEY)).toBeFalse();
|
||||
|
||||
await expectAsync(handler.completeAuthorizationRequestFromUrl(url)).toBeRejectedWithError(
|
||||
/No pending authorization request/,
|
||||
);
|
||||
expect(listener).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('rejects a callback with a different state and keeps the pending request', async () => {
|
||||
const state = await login();
|
||||
|
||||
await expectAsync(
|
||||
handler.completeAuthorizationRequestFromUrl(
|
||||
callback({code: 'attacker-code', state: 'other-state'}),
|
||||
),
|
||||
).toBeRejectedWithError(/state does not match/);
|
||||
expect(listener).not.toHaveBeenCalled();
|
||||
|
||||
// The real callback still works afterwards.
|
||||
await handler.completeAuthorizationRequestFromUrl(callback({code: 'auth-code', state}));
|
||||
expect(listener.calls.mostRecent().args[1]?.code).toBe('auth-code');
|
||||
});
|
||||
|
||||
it('rejects a callback without state', async () => {
|
||||
await login();
|
||||
|
||||
await expectAsync(
|
||||
handler.completeAuthorizationRequestFromUrl(callback({code: 'auth-code'})),
|
||||
).toBeRejectedWithError(/state does not match/);
|
||||
expect(listener).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('rejects a callback when no login was started', async () => {
|
||||
await expectAsync(
|
||||
handler.completeAuthorizationRequestFromUrl(callback({code: 'auth-code', state: 'any-state'})),
|
||||
).toBeRejectedWithError(/No pending authorization request/);
|
||||
expect(listener).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('delivers an error response from the IdP and ends the pending request', async () => {
|
||||
const state = await login();
|
||||
|
||||
await handler.completeAuthorizationRequestFromUrl(
|
||||
callback({error: 'access_denied', error_description: 'User cancelled', state}),
|
||||
);
|
||||
|
||||
const [, response, error] = listener.calls.mostRecent().args;
|
||||
expect(response).toBeNull();
|
||||
expect(error?.error).toBe('access_denied');
|
||||
expect(error?.errorDescription).toBe('User cancelled');
|
||||
expect(storage.items.has(PENDING_REQUEST_KEY)).toBeFalse();
|
||||
});
|
||||
|
||||
it('rejects a callback with neither code nor error', async () => {
|
||||
const state = await login();
|
||||
|
||||
await expectAsync(
|
||||
handler.completeAuthorizationRequestFromUrl(callback({state})),
|
||||
).toBeRejectedWithError(/neither an authorization code nor an error/);
|
||||
expect(listener).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('on the web', () => {
|
||||
let handler: CapacitorAuthorizationRequestHandler;
|
||||
let listener: AuthorizationListener;
|
||||
let openAuthorizationUrl: jasmine.Spy<(url: string) => Promise<void>>;
|
||||
|
||||
const login = () => startLogin(handler, WEB_REDIRECT_URI);
|
||||
const callback = (parameters: Record<string, string>) => callbackUrl(WEB_REDIRECT_URI, parameters);
|
||||
|
||||
beforeEach(() => {
|
||||
for (const key of appAuthKeys()) localStorage.removeItem(key);
|
||||
|
||||
// Same setup as AuthProvider on the web: localStorage, page redirect, nothing to close.
|
||||
// The redirect is a spy, so the test does not actually leave the page.
|
||||
openAuthorizationUrl = jasmine.createSpy('openAuthorizationUrl').and.resolveTo();
|
||||
handler = new CapacitorAuthorizationRequestHandler(
|
||||
new LocalStorageBackend(),
|
||||
openAuthorizationUrl,
|
||||
async () => {},
|
||||
);
|
||||
listener = listenForResults(handler);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
for (const key of appAuthKeys()) localStorage.removeItem(key);
|
||||
});
|
||||
|
||||
it('opens the IdP login through the given function (page redirect)', async () => {
|
||||
const state = await login();
|
||||
|
||||
const url = new URL(openAuthorizationUrl.calls.mostRecent().args[0]);
|
||||
expect(url.searchParams.get('state')).toBe(state);
|
||||
expect(url.searchParams.get('code_challenge_method')).toBe('S256');
|
||||
});
|
||||
|
||||
it('keeps only one pending login, however often the login is started', async () => {
|
||||
await login();
|
||||
await login();
|
||||
await login();
|
||||
|
||||
expect(appAuthKeys()).toEqual([PENDING_REQUEST_KEY]);
|
||||
});
|
||||
|
||||
it('completes the login from the callback page URL and leaves nothing behind', async () => {
|
||||
const state = await login();
|
||||
|
||||
await handler.completeAuthorizationRequestFromUrl(callback({code: 'auth-code', state}));
|
||||
|
||||
expect(listener.calls.mostRecent().args[1]?.code).toBe('auth-code');
|
||||
expect(appAuthKeys()).toEqual([]);
|
||||
});
|
||||
|
||||
it('only accepts the callback of the latest login', async () => {
|
||||
const firstState = await login();
|
||||
const latestState = await login();
|
||||
|
||||
await expectAsync(
|
||||
handler.completeAuthorizationRequestFromUrl(callback({code: 'old', state: firstState})),
|
||||
).toBeRejectedWithError(/state does not match/);
|
||||
|
||||
await handler.completeAuthorizationRequestFromUrl(callback({code: 'new', state: latestState}));
|
||||
expect(listener.calls.mostRecent().args[1]?.code).toBe('new');
|
||||
});
|
||||
|
||||
it('reports an opened callback without a pending login instead of waiting forever', async () => {
|
||||
await expectAsync(
|
||||
handler.completeAuthorizationRequestFromUrl(callback({code: 'bookmarked', state: 'old'})),
|
||||
).toBeRejectedWithError(/No pending authorization request/);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,232 @@
|
||||
/*
|
||||
* Copyright (C) 2026 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 {Browser} from '@capacitor/browser';
|
||||
import {
|
||||
AuthorizationError,
|
||||
AuthorizationErrorJson,
|
||||
AuthorizationRequest,
|
||||
AuthorizationRequestHandler,
|
||||
AuthorizationRequestJson,
|
||||
AuthorizationRequestResponse,
|
||||
AuthorizationResponse,
|
||||
AuthorizationResponseJson,
|
||||
AuthorizationServiceConfiguration,
|
||||
BasicQueryStringUtils,
|
||||
DefaultCrypto,
|
||||
StorageBackend,
|
||||
StringMap,
|
||||
} from '@openid/appauth';
|
||||
|
||||
const AUTHORIZATION_REQUEST_KEY = 'appauth_capacitor_authorization_request';
|
||||
|
||||
/**
|
||||
* AppAuth authorization request handler for the native apps and the web.
|
||||
*
|
||||
* AppAuth-JS handles:
|
||||
* - state generation and validation data
|
||||
* - PKCE code_verifier / code_challenge
|
||||
* - authorization URL construction
|
||||
* - AuthorizationNotifier
|
||||
*
|
||||
* This handler adds:
|
||||
* - persist the pending authorization request
|
||||
* - open the authorization URL (native: @capacitor/browser, web: page redirect)
|
||||
* - process the callback URL (native: delivered through appUrlOpen,
|
||||
* web: the URL of the callback page)
|
||||
*
|
||||
* The pending request is stored under one fixed key, so each new login
|
||||
* replaces a canceled one instead of leaving it behind.
|
||||
*/
|
||||
export class CapacitorAuthorizationRequestHandler extends AuthorizationRequestHandler {
|
||||
private callbackUrl?: string;
|
||||
|
||||
/**
|
||||
* @param storage Where the pending request is kept until the callback.
|
||||
* @param openAuthorizationUrl Opens the IdP login (default: Capacitor browser).
|
||||
* @param closeBrowser Closes it after the callback (default: Capacitor browser).
|
||||
*/
|
||||
constructor(
|
||||
private readonly storage: StorageBackend,
|
||||
private readonly openAuthorizationUrl: (url: string) => Promise<void> = async url => {
|
||||
await Browser.open({url});
|
||||
},
|
||||
private readonly closeBrowser: () => Promise<void> = async () => {
|
||||
await Browser.close();
|
||||
},
|
||||
) {
|
||||
super(new BasicQueryStringUtils(), new DefaultCrypto());
|
||||
}
|
||||
|
||||
public async performAuthorizationRequest(
|
||||
configuration: AuthorizationServiceConfiguration,
|
||||
request: AuthorizationRequest,
|
||||
): Promise<void> {
|
||||
/*
|
||||
* Important:
|
||||
*
|
||||
* toJson() calls setupCodeVerifier() internally.
|
||||
* Therefore PKCE is initialized before buildRequestUrl()
|
||||
* constructs the authorization URL.
|
||||
*/
|
||||
const requestJson = await request.toJson();
|
||||
|
||||
await this.storage.setItem(AUTHORIZATION_REQUEST_KEY, JSON.stringify(requestJson));
|
||||
|
||||
const url = this.buildRequestUrl(configuration, request);
|
||||
|
||||
await this.openAuthorizationUrl(url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Completes the authorization flow using the URL received from
|
||||
* Capacitor's App.addListener('appUrlOpen', ...).
|
||||
*/
|
||||
public async completeAuthorizationRequestFromUrl(callbackUrl: string): Promise<void> {
|
||||
this.callbackUrl = callbackUrl;
|
||||
|
||||
/*
|
||||
* The deep link has returned control to the application.
|
||||
* Close the SFSafariViewController / Custom Tab if it is still open.
|
||||
*/
|
||||
try {
|
||||
await this.closeBrowser();
|
||||
} catch {
|
||||
// Browser might already have been closed by the platform.
|
||||
}
|
||||
|
||||
await this.completeAuthorizationRequestIfPossible();
|
||||
}
|
||||
|
||||
protected async completeAuthorizationRequest(): Promise<AuthorizationRequestResponse | null> {
|
||||
if (!this.callbackUrl) {
|
||||
// eslint-disable-next-line unicorn/no-null -- AppAuth's AuthorizationRequestHandler contract
|
||||
return null;
|
||||
}
|
||||
|
||||
const storedRequest = await this.storage.getItem(AUTHORIZATION_REQUEST_KEY);
|
||||
|
||||
if (!storedRequest) {
|
||||
this.callbackUrl = undefined;
|
||||
|
||||
throw new Error('No pending authorization request found.');
|
||||
}
|
||||
|
||||
const requestJson = JSON.parse(storedRequest) as AuthorizationRequestJson;
|
||||
|
||||
/*
|
||||
* The serialized request also contains `internal`, including
|
||||
* AppAuth's PKCE code_verifier.
|
||||
*/
|
||||
const request = new AuthorizationRequest(requestJson);
|
||||
|
||||
const parameters = this.parseCallbackUrl(this.callbackUrl);
|
||||
|
||||
const state = parameters['state'];
|
||||
|
||||
/*
|
||||
* Never accept an authorization response for a different request.
|
||||
*/
|
||||
if (!state || state !== request.state) {
|
||||
this.callbackUrl = undefined;
|
||||
|
||||
throw new Error('Authorization response state does not match the authorization request.');
|
||||
}
|
||||
|
||||
const error = parameters['error'];
|
||||
|
||||
let response: // eslint-disable-next-line unicorn/no-null -- AppAuth's AuthorizationRequestHandler contract
|
||||
AuthorizationResponse | null = null;
|
||||
|
||||
let authorizationError: // eslint-disable-next-line unicorn/no-null -- AppAuth's AuthorizationRequestHandler contract
|
||||
AuthorizationError | null = null;
|
||||
|
||||
if (error) {
|
||||
const errorJson: AuthorizationErrorJson = {
|
||||
error,
|
||||
|
||||
error_description: parameters['error_description'],
|
||||
|
||||
error_uri: parameters['error_uri'],
|
||||
|
||||
state,
|
||||
};
|
||||
|
||||
authorizationError = new AuthorizationError(errorJson);
|
||||
} else {
|
||||
const code = parameters['code'];
|
||||
|
||||
if (!code) {
|
||||
this.callbackUrl = undefined;
|
||||
|
||||
throw new Error('Authorization callback contains neither an authorization code nor an error.');
|
||||
}
|
||||
|
||||
const responseJson: AuthorizationResponseJson = {
|
||||
code,
|
||||
state,
|
||||
};
|
||||
|
||||
response = new AuthorizationResponse(responseJson);
|
||||
}
|
||||
|
||||
/*
|
||||
* The response has been successfully associated with the
|
||||
* pending request. It can now be consumed exactly once.
|
||||
*/
|
||||
await this.storage.removeItem(AUTHORIZATION_REQUEST_KEY);
|
||||
|
||||
this.callbackUrl = undefined;
|
||||
|
||||
return {
|
||||
request,
|
||||
response,
|
||||
error: authorizationError,
|
||||
};
|
||||
}
|
||||
|
||||
private parseCallbackUrl(callbackUrl: string): StringMap {
|
||||
const result: StringMap = {};
|
||||
|
||||
const url = new URL(callbackUrl);
|
||||
|
||||
/*
|
||||
* Authorization Code Flow normally returns code/state in
|
||||
* the query string:
|
||||
*
|
||||
* openstapps:/callback?code=...&state=...
|
||||
*/
|
||||
for (const [key, value] of url.searchParams.entries()) {
|
||||
result[key] = value;
|
||||
}
|
||||
|
||||
/*
|
||||
* Also accept parameters in the fragment as a fallback.
|
||||
*/
|
||||
if (url.hash.length > 1) {
|
||||
const hash = url.hash.slice(1);
|
||||
|
||||
const hashParameters = new URLSearchParams(hash);
|
||||
|
||||
for (const [key, value] of hashParameters.entries()) {
|
||||
if (!(key in result)) {
|
||||
result[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -13,61 +13,89 @@
|
||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {Requestor} from '@openid/appauth';
|
||||
import {CapacitorHttp, HttpHeaders, HttpResponse} from '@capacitor/core';
|
||||
import {XhrSettings} from 'ionic-appauth/lib/cordova';
|
||||
import {AppAuthError, Requestor} from '@openid/appauth';
|
||||
import {CapacitorHttp, HttpHeaders, HttpOptions} from '@capacitor/core';
|
||||
|
||||
// REQUIRES CAPACITOR PLUGIN
|
||||
// @capacitor-community/http
|
||||
export class CapacitorRequestor extends Requestor {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
type XhrSettings = Parameters<Requestor['xhr']>[0];
|
||||
|
||||
export class CapacitorRequestor implements Requestor {
|
||||
public async xhr<T>(settings: XhrSettings): Promise<T> {
|
||||
if (!settings.method) settings.method = 'GET';
|
||||
if (!settings.url) {
|
||||
throw new AppAuthError('A URL must be provided.');
|
||||
}
|
||||
|
||||
switch (settings.method) {
|
||||
case 'GET': {
|
||||
return this.get(settings.url, settings.headers);
|
||||
}
|
||||
case 'POST': {
|
||||
return this.post(settings.url, settings.data, settings.headers);
|
||||
}
|
||||
case 'PUT': {
|
||||
return this.put(settings.url, settings.data, settings.headers);
|
||||
}
|
||||
case 'DELETE': {
|
||||
return this.delete(settings.url, settings.headers);
|
||||
const method = (settings.method ?? 'GET').toUpperCase();
|
||||
const url = new URL(settings.url);
|
||||
|
||||
let data: unknown;
|
||||
|
||||
if (settings.data) {
|
||||
if (method === 'POST') {
|
||||
data = settings.data;
|
||||
} else {
|
||||
const searchParameters = new URLSearchParams(settings.data);
|
||||
|
||||
for (const [key, value] of searchParameters.entries()) {
|
||||
url.searchParams.append(key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async get<T>(url: string, headers: HttpHeaders) {
|
||||
return CapacitorHttp.get({url, headers}).then((response: HttpResponse) => response.data as T);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
private async post<T>(url: string, data: any, headers: HttpHeaders) {
|
||||
return CapacitorHttp.post({
|
||||
url,
|
||||
const options: HttpOptions = {
|
||||
url: url.toString(),
|
||||
method,
|
||||
headers: this.getHeaders(settings.headers),
|
||||
data,
|
||||
headers,
|
||||
}).then((response: HttpResponse) => {
|
||||
return response.data as T;
|
||||
});
|
||||
};
|
||||
|
||||
if (settings.dataType?.toLowerCase() === 'json') {
|
||||
options.responseType = 'json';
|
||||
}
|
||||
|
||||
const response = await CapacitorHttp.request(options);
|
||||
|
||||
if (response.status < 200 || response.status >= 300) {
|
||||
const body = this.parseBody(response.data);
|
||||
|
||||
/*
|
||||
* Use the OAuth error code (e.g. 'invalid_grant') as message when the
|
||||
* server sent one (RFC 6749, 5.2), so callers can tell a rejected
|
||||
* refresh token apart from temporary failures.
|
||||
*/
|
||||
throw new AppAuthError(
|
||||
typeof body?.error === 'string' ? body.error : `HTTP ${response.status}`,
|
||||
body ?? response.data,
|
||||
);
|
||||
}
|
||||
|
||||
return response.data as T;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
private async put<T>(url: string, data: any, headers: HttpHeaders) {
|
||||
return CapacitorHttp.put({
|
||||
url,
|
||||
data,
|
||||
headers,
|
||||
}).then((response: HttpResponse) => response.data as T);
|
||||
private parseBody(data: unknown): Record<string, unknown> | undefined {
|
||||
if (typeof data === 'string') {
|
||||
try {
|
||||
data = JSON.parse(data);
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
return data !== null && typeof data === 'object' ? (data as Record<string, unknown>) : undefined;
|
||||
}
|
||||
|
||||
private async delete<T>(url: string, headers: HttpHeaders) {
|
||||
return CapacitorHttp.delete({url, headers}).then((response: HttpResponse) => response.data as T);
|
||||
private getHeaders(headers: XhrSettings['headers']): HttpHeaders {
|
||||
const result: HttpHeaders = {};
|
||||
|
||||
if (!headers) {
|
||||
return result;
|
||||
}
|
||||
|
||||
for (const [key, value] of Object.entries(headers)) {
|
||||
if (value !== undefined && value !== null) {
|
||||
result[key] = String(value);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -1,104 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 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 {TestBed} from '@angular/core/testing';
|
||||
import {ConfigProvider} from '../config/config.provider';
|
||||
import {StorageProvider} from '../storage/storage.provider';
|
||||
import {DefaultAuthService} from './default-auth.service';
|
||||
import {Browser} from 'ionic-appauth';
|
||||
import {nowInSeconds, Requestor, StorageBackend} from '@openid/appauth';
|
||||
import {TranslateService} from '@ngx-translate/core';
|
||||
import {StAppsWebHttpClient} from '../data/stapps-web-http-client.provider';
|
||||
import {provideHttpClient, withInterceptorsFromDi} from '@angular/common/http';
|
||||
import {IonicStorage} from 'ionic-appauth/lib';
|
||||
import {RouterModule} from '@angular/router';
|
||||
import {LoggerTestingModule} from 'ngx-logger/testing';
|
||||
|
||||
describe('AuthService', () => {
|
||||
let defaultAuthService: DefaultAuthService;
|
||||
let storageBackendSpy: jasmine.SpyObj<StorageBackend>;
|
||||
const storageProviderSpy = jasmine.createSpyObj('StorageProvider', ['init', 'get', 'has', 'put', 'search']);
|
||||
const translateServiceSpy = jasmine.createSpyObj('TranslateService', ['setDefaultLang', 'use']);
|
||||
|
||||
beforeEach(() => {
|
||||
storageBackendSpy = jasmine.createSpyObj('StorageBackend', ['getItem']);
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
imports: [LoggerTestingModule, RouterModule.forRoot([])],
|
||||
providers: [
|
||||
StAppsWebHttpClient,
|
||||
{
|
||||
provide: TranslateService,
|
||||
useValue: translateServiceSpy,
|
||||
},
|
||||
{
|
||||
provide: StorageProvider,
|
||||
useValue: storageProviderSpy,
|
||||
},
|
||||
IonicStorage,
|
||||
ConfigProvider,
|
||||
Browser,
|
||||
{
|
||||
provide: StorageBackend,
|
||||
useValue: storageBackendSpy,
|
||||
},
|
||||
Requestor,
|
||||
provideHttpClient(withInterceptorsFromDi()),
|
||||
],
|
||||
});
|
||||
defaultAuthService = TestBed.inject(DefaultAuthService);
|
||||
});
|
||||
|
||||
describe('loadTokenFromStorage', () => {
|
||||
it('should provide false through isAuthenticated$ when there is no token response', async () => {
|
||||
// eslint-disable-next-line unicorn/no-null
|
||||
storageBackendSpy.getItem.and.returnValue(Promise.resolve(null));
|
||||
let loggedInHolder;
|
||||
defaultAuthService.isAuthenticated$.subscribe(loggedIn => {
|
||||
loggedInHolder = loggedIn;
|
||||
});
|
||||
await defaultAuthService.loadTokenFromStorage();
|
||||
|
||||
expect(loggedInHolder).toBeFalse();
|
||||
});
|
||||
|
||||
it('should provide true through isAuthenticated$ when access token is valid', async () => {
|
||||
const validToken = `{"access_token":"AT-XXXX","refresh_token":"RT-XXXX","scope":"","token_type":"bearer","issued_at":${nowInSeconds()},"expires_in":"${
|
||||
8 * 60 * 60
|
||||
}"}`;
|
||||
storageBackendSpy.getItem.and.returnValue(Promise.resolve(validToken));
|
||||
let loggedInHolder;
|
||||
defaultAuthService.isAuthenticated$.subscribe(loggedIn => {
|
||||
loggedInHolder = loggedIn;
|
||||
});
|
||||
await defaultAuthService.loadTokenFromStorage();
|
||||
|
||||
expect(loggedInHolder).toBeTrue();
|
||||
});
|
||||
|
||||
it('should provide false through isAuthenticated$ when access token is invalid', async () => {
|
||||
const invalidToken = `{"access_token":"AT-INVALID-XXXX","refresh_token":"RT-XXXX","scope":"","token_type":"bearer","issued_at":${
|
||||
nowInSeconds() - 9 * 60 * 60
|
||||
},"expires_in":"${8 * 60 * 60}"}`;
|
||||
storageBackendSpy.getItem.and.returnValue(Promise.resolve(invalidToken));
|
||||
let loggedInHolder;
|
||||
defaultAuthService.isAuthenticated$.subscribe(loggedIn => {
|
||||
loggedInHolder = loggedIn;
|
||||
});
|
||||
await defaultAuthService.loadTokenFromStorage();
|
||||
|
||||
expect(loggedInHolder).toBeFalse();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,102 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 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 {
|
||||
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 {getClientConfig, getEndpointsConfig} from './auth.provider.methods';
|
||||
import {Injectable, inject} from '@angular/core';
|
||||
import {AuthService} from './auth.service';
|
||||
|
||||
const TOKEN_RESPONSE_KEY = 'token_response';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class DefaultAuthService extends AuthService {
|
||||
protected browser: Browser;
|
||||
|
||||
protected storage: StorageBackend;
|
||||
|
||||
protected requestor: Requestor;
|
||||
|
||||
private readonly configProvider = inject(ConfigProvider);
|
||||
|
||||
public localConfiguration: AuthorizationServiceConfiguration;
|
||||
|
||||
protected tokenHandler: TokenRequestHandler;
|
||||
|
||||
protected userInfoHandler: UserInfoHandler;
|
||||
|
||||
protected requestHandler: AuthorizationRequestHandler;
|
||||
|
||||
protected endSessionHandler: EndSessionHandler;
|
||||
|
||||
constructor() {
|
||||
const browser = inject(Browser) ?? new DefaultBrowser();
|
||||
const storage = inject(StorageBackend) ?? new LocalStorageBackend();
|
||||
const requestor = inject(Requestor) ?? new JQueryRequestor();
|
||||
|
||||
super(browser, storage, requestor);
|
||||
|
||||
this.browser = browser;
|
||||
this.storage = storage;
|
||||
this.requestor = requestor;
|
||||
}
|
||||
|
||||
get configuration(): Promise<AuthorizationServiceConfiguration> {
|
||||
if (!this.localConfiguration) throw new Error('Local Configuration Not Defined');
|
||||
|
||||
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));
|
||||
});
|
||||
this.notifyActionListers(AuthActionBuilder.SignOutSuccess());
|
||||
}
|
||||
|
||||
public async revokeTokens() {
|
||||
// Note: only locally
|
||||
await this.storage.removeItem(TOKEN_RESPONSE_KEY);
|
||||
this.notifyActionListers(AuthActionBuilder.RevokeTokensSuccess());
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -14,9 +14,9 @@
|
||||
*/
|
||||
|
||||
import {Platform} from '@ionic/angular/standalone';
|
||||
import {FetchRequestor, Requestor} from '@openid/appauth';
|
||||
import {CapacitorRequestor} from '../capacitor-requestor';
|
||||
import {NgHttpService} from '../ng-http.service';
|
||||
|
||||
export const httpFactory = (platform: Platform) => {
|
||||
return platform.is('capacitor') ? new CapacitorRequestor() : new NgHttpService();
|
||||
export const requestorFactory = (platform: Platform): Requestor => {
|
||||
return platform.is('capacitor') ? new CapacitorRequestor() : new FetchRequestor();
|
||||
};
|
||||
@@ -14,9 +14,9 @@
|
||||
*/
|
||||
|
||||
import {Platform} from '@ionic/angular/standalone';
|
||||
import {IonicStorage} from 'ionic-appauth/lib';
|
||||
import {LocalStorageBackend, StorageBackend} from '@openid/appauth';
|
||||
import {SafeCapacitorSecureStorage} from '../../storage/capacitor-secure-storage';
|
||||
|
||||
export const storageFactory = (platform: Platform) => {
|
||||
return platform.is('capacitor') ? new SafeCapacitorSecureStorage() : new IonicStorage();
|
||||
export const storageFactory = (platform: Platform): StorageBackend => {
|
||||
return platform.is('capacitor') ? new SafeCapacitorSecureStorage() : new LocalStorageBackend();
|
||||
};
|
||||
@@ -1,77 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 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, inject} from '@angular/core';
|
||||
import {Requestor} from '@openid/appauth';
|
||||
import {HttpClient, HttpHeaders} from '@angular/common/http';
|
||||
import {XhrSettings} from 'ionic-appauth/lib/cordova';
|
||||
import {firstValueFrom, Observable} from 'rxjs';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class NgHttpService implements Requestor {
|
||||
private http = inject(HttpClient);
|
||||
|
||||
public async xhr<T>(settings: XhrSettings): Promise<T> {
|
||||
if (!settings.method) {
|
||||
settings.method = 'GET';
|
||||
}
|
||||
|
||||
let observable: Observable<T>;
|
||||
|
||||
switch (settings.method) {
|
||||
case 'GET': {
|
||||
observable = this.http.get<T>(settings.url, {
|
||||
headers: this.getHeaders(settings.headers),
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'POST': {
|
||||
observable = this.http.post<T>(settings.url, settings.data, {
|
||||
headers: this.getHeaders(settings.headers),
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'PUT': {
|
||||
observable = this.http.put<T>(settings.url, settings.data, {
|
||||
headers: this.getHeaders(settings.headers),
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'DELETE': {
|
||||
observable = this.http.delete<T>(settings.url, {
|
||||
headers: this.getHeaders(settings.headers),
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return firstValueFrom(observable);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
private getHeaders(headers: any): HttpHeaders {
|
||||
let httpHeaders: HttpHeaders = new HttpHeaders();
|
||||
|
||||
if (headers !== undefined) {
|
||||
for (const key of Object.keys(headers)) {
|
||||
httpHeaders = httpHeaders.append(key, headers[key]);
|
||||
}
|
||||
}
|
||||
|
||||
return httpHeaders;
|
||||
}
|
||||
}
|
||||
@@ -1,21 +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/>.
|
||||
*/
|
||||
|
||||
export interface IUserInfo {
|
||||
display_name: string;
|
||||
role: string;
|
||||
email: string;
|
||||
user_name: string;
|
||||
}
|
||||
+9
-1
@@ -17,7 +17,12 @@ import {AnimationController} from '@ionic/angular/standalone';
|
||||
import {AnimationOptions} from '@ionic/angular/common/providers/nav-controller';
|
||||
|
||||
/**
|
||||
*
|
||||
* Creates the page transition used when opening the search from the search field
|
||||
* on the home page: the search bar moves up into the header of the search page,
|
||||
* which fades in on top while the home page content slides away. Going back plays
|
||||
* the same animation in reverse.
|
||||
* @param animationController Ionic animation controller used to build the animation
|
||||
* @returns animation builder to pass to `[routerAnimation]`
|
||||
*/
|
||||
export function homePageSearchTransition(animationController: AnimationController) {
|
||||
let scheduleTransform: WebKitCSSMatrix;
|
||||
@@ -53,6 +58,9 @@ export function homePageSearchTransition(animationController: AnimationControlle
|
||||
const headerDelta = homeHeaderHeight - searchHeaderHeight;
|
||||
|
||||
const enterTransition = animationController.create().fromTo('opacity', '0', '1').addElement(searchPage);
|
||||
if (back) {
|
||||
enterTransition.afterStyles({display: 'none'});
|
||||
}
|
||||
const exitTransition = animationController.create().fromTo('opacity', '1', '1').addElement(homePage);
|
||||
|
||||
const homePageSlide = animationController
|
||||
|
||||
@@ -101,6 +101,7 @@ export function mapMaximizeAnimation(animationController: AnimationController):
|
||||
const exitTransition = animationController
|
||||
.create()
|
||||
.fromTo('opacity', '1', options.direction === 'back' ? '0' : '1')
|
||||
.afterStyles(options.direction === 'back' ? {display: 'none'} : {})
|
||||
.addElement(options.leavingEl);
|
||||
|
||||
rootTransition.addAnimation([enterTransition, exitTransition, mapEnterTransition, mapExitTransition]);
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@
|
||||
[displayValue]="setting | settingValueTranslate | titlecase"
|
||||
[value]="setting"
|
||||
[active]="!!filtersMap.get($any(setting.name))"
|
||||
(toggle)="stateChanged($any($event))"
|
||||
(toggled)="stateChanged($any($event))"
|
||||
>
|
||||
</stapps-chip-filter>
|
||||
}
|
||||
@@ -1,9 +1,6 @@
|
||||
import {ChangeDetectionStrategy, Component, ElementRef, Input, inject} from '@angular/core';
|
||||
import {SCIdCard} from '@openstapps/core';
|
||||
import {ThingTranslateModule} from '../../translation/thing-translate.module';
|
||||
import {AsyncPipe, TitleCasePipe} from '@angular/common';
|
||||
import {IntervalIsNowPipe, ToDateIntervalPipe} from '../../util/in-range.pipe';
|
||||
import {TranslateModule} from '@ngx-translate/core';
|
||||
import {AnimationController, ModalController} from '@ionic/angular/standalone';
|
||||
import {ScreenBrightness} from '@capacitor-community/screen-brightness';
|
||||
import {ScreenOrientation} from '@capacitor/screen-orientation';
|
||||
@@ -15,14 +12,7 @@ import {iosDuration, iosEasing, mdDuration, mdEasing} from 'src/app/animation/ea
|
||||
templateUrl: 'id-card.html',
|
||||
styleUrls: ['id-card.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [
|
||||
ThingTranslateModule,
|
||||
IntervalIsNowPipe,
|
||||
ToDateIntervalPipe,
|
||||
AsyncPipe,
|
||||
TranslateModule,
|
||||
TitleCasePipe,
|
||||
],
|
||||
imports: [ThingTranslateModule],
|
||||
})
|
||||
export class IdCardComponent {
|
||||
private modalController = inject(ModalController);
|
||||
|
||||
@@ -4,6 +4,3 @@
|
||||
draggable="false"
|
||||
(click)="presentFullscreen()"
|
||||
/>
|
||||
@if (item.validity && (item.validity | rangeToDateInterval | dfnsIntervalIsNow | async) === false) {
|
||||
<div class="expired">{{ 'profile.userInfo.expired' | translate | titlecase }}</div>
|
||||
}
|
||||
@@ -6,24 +6,3 @@
|
||||
img {
|
||||
border-radius: 3mm;
|
||||
}
|
||||
|
||||
.expired {
|
||||
position: absolute;
|
||||
top: 48px;
|
||||
left: 48px;
|
||||
transform-origin: center;
|
||||
translate: -50% -50%;
|
||||
rotate: -45deg;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
width: 256px;
|
||||
padding: var(--spacing-xs);
|
||||
|
||||
font-weight: bold;
|
||||
color: var(--ion-color-danger-contrast);
|
||||
|
||||
background: var(--ion-color-danger);
|
||||
}
|
||||
@@ -1,16 +1,19 @@
|
||||
import {Injectable, inject} from '@angular/core';
|
||||
import {SCIdCard, SCThingOriginType, SCThingType, SCUserConfiguration} from '@openstapps/core';
|
||||
import {from, of, Observable} from 'rxjs';
|
||||
import {AuthHelperService} from '../auth/auth-helper.service';
|
||||
import {mergeMap, concatWith, filter, map, startWith, catchError, tap} from 'rxjs/operators';
|
||||
import {ConfigProvider} from '../config/config.provider';
|
||||
import {HttpClient} from '@angular/common/http';
|
||||
import {EncryptedStorageProvider} from '../storage/encrypted-storage.provider';
|
||||
import {AuthProvider} from '../auth/auth.provider';
|
||||
import {AuthHelperService} from '../auth/auth-helper.service';
|
||||
|
||||
@Injectable({providedIn: 'root'})
|
||||
export class IdCardsProvider {
|
||||
private authHelper = inject(AuthHelperService);
|
||||
|
||||
private authProvider = inject(AuthProvider);
|
||||
|
||||
private config = inject(ConfigProvider);
|
||||
|
||||
private httpClient = inject(HttpClient);
|
||||
@@ -19,7 +22,7 @@ export class IdCardsProvider {
|
||||
|
||||
getIdCards(): Observable<SCIdCard[]> {
|
||||
const feature = this.config.config.app.features.extern?.['idCards'];
|
||||
const auth = this.authHelper.getProvider();
|
||||
const auth = this.authProvider;
|
||||
const storedIdCards = from(
|
||||
this.encryptedStorageProvider.get<SCIdCard[]>('id-cards') as Promise<SCIdCard[]>,
|
||||
).pipe(filter(it => it !== undefined));
|
||||
|
||||
@@ -2,19 +2,18 @@ import {ConfigProvider} from '../config/config.provider';
|
||||
import {IdCardsProvider} from './id-cards.provider';
|
||||
import {HttpClient} from '@angular/common/http';
|
||||
import {AuthHelperService} from '../auth/auth-helper.service';
|
||||
import {AuthProvider} from '../auth/auth.provider';
|
||||
import {BehaviorSubject, firstValueFrom, of} from 'rxjs';
|
||||
import {EncryptedStorageProvider} from '../storage/encrypted-storage.provider';
|
||||
import {TestBed} from '@angular/core/testing';
|
||||
import {TranslateService} from '@ngx-translate/core';
|
||||
import {StorageBackend, Requestor} from '@openid/appauth';
|
||||
import {IonicStorage, Browser} from 'ionic-appauth';
|
||||
import {LoggerTestingModule} from 'ngx-logger/testing';
|
||||
import {StAppsWebHttpClient} from '../data/stapps-web-http-client.provider';
|
||||
import {SimpleBrowser} from '../../util/browser.factory';
|
||||
|
||||
class FakeAuth {
|
||||
isLoggedIn$ = new BehaviorSubject(false);
|
||||
|
||||
user$ = new BehaviorSubject<unknown>(undefined);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
getValidToken() {}
|
||||
}
|
||||
@@ -25,16 +24,14 @@ describe('IdCards', () => {
|
||||
let encryptedStorageProvider: EncryptedStorageProvider;
|
||||
let fakeAuth: FakeAuth;
|
||||
|
||||
const authHelperServiceMock = jasmine.createSpyObj('AuthHelperService', ['getProvider']);
|
||||
const authHelperServiceMock = jasmine.createSpyObj('AuthHelperService', ['getUserFromUserInfo']);
|
||||
const translateServiceSpy = jasmine.createSpyObj('TranslateService', ['setDefaultLang', 'use']);
|
||||
let storageBackendSpy: jasmine.SpyObj<StorageBackend>;
|
||||
const simpleBrowserMock = jasmine.createSpyObj('SimpleBrowser', ['open']);
|
||||
|
||||
beforeEach(() => {
|
||||
storageBackendSpy = jasmine.createSpyObj('StorageBackend', ['getItem']);
|
||||
fakeAuth = new FakeAuth();
|
||||
configProviderSpy = jasmine.createSpyObj('ConfigProvider', ['config', 'getAnyValue']);
|
||||
configProviderSpy.config = {
|
||||
app: {features: {extern: {idCards: {url: 'http://id-cards.local', authProvider: 'fakeAuth'}}}},
|
||||
app: {features: {extern: {idCards: {url: 'http://id-cards.local', authProvider: 'default'}}}},
|
||||
} as never;
|
||||
configProviderSpy.getAnyValue = () =>
|
||||
({
|
||||
@@ -62,7 +59,6 @@ describe('IdCards', () => {
|
||||
imports: [LoggerTestingModule],
|
||||
providers: [
|
||||
IdCardsProvider,
|
||||
StAppsWebHttpClient,
|
||||
{
|
||||
provide: TranslateService,
|
||||
useValue: translateServiceSpy,
|
||||
@@ -71,40 +67,29 @@ describe('IdCards', () => {
|
||||
provide: EncryptedStorageProvider,
|
||||
useValue: encryptedStorageProvider,
|
||||
},
|
||||
IonicStorage,
|
||||
{
|
||||
provide: ConfigProvider,
|
||||
useValue: configProviderSpy,
|
||||
},
|
||||
Browser,
|
||||
{
|
||||
provide: StorageBackend,
|
||||
useValue: storageBackendSpy,
|
||||
},
|
||||
{
|
||||
provide: AuthHelperService,
|
||||
useValue: authHelperServiceMock,
|
||||
},
|
||||
{
|
||||
provide: SimpleBrowser,
|
||||
useValue: simpleBrowserMock,
|
||||
provide: AuthProvider,
|
||||
useValue: fakeAuth,
|
||||
},
|
||||
{
|
||||
provide: HttpClient,
|
||||
useValue: httpClient,
|
||||
},
|
||||
Requestor,
|
||||
],
|
||||
});
|
||||
|
||||
fakeAuth = new FakeAuth();
|
||||
authHelperServiceMock.getProvider = jasmine.createSpy().and.returnValue(fakeAuth);
|
||||
});
|
||||
|
||||
it('should emit undefined if not logged in', async () => {
|
||||
const provider = TestBed.inject(IdCardsProvider);
|
||||
expect(await firstValueFrom(provider.getIdCards())).toEqual([]);
|
||||
expect(authHelperServiceMock.getProvider).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should emit network result when logged in', async () => {
|
||||
@@ -113,7 +98,6 @@ describe('IdCards', () => {
|
||||
fakeAuth.getValidToken = jasmine.createSpy().and.resolveTo({accessToken: 'fake-token'});
|
||||
const provider = TestBed.inject(IdCardsProvider);
|
||||
expect(await firstValueFrom(provider.getIdCards())).toEqual(['abc' as never]);
|
||||
expect(authHelperServiceMock.getProvider).toHaveBeenCalledTimes(1);
|
||||
// eslint-disable-next-line unicorn/no-null
|
||||
expect(httpClient.get).toHaveBeenCalledOnceWith('http://id-cards.local', {
|
||||
headers: {
|
||||
|
||||
@@ -19,6 +19,7 @@ import {AuthHelperService} from '../../auth/auth-helper.service';
|
||||
import {mergeMap, of} from 'rxjs';
|
||||
import Swiper from 'swiper';
|
||||
import {toObservable, toSignal} from '@angular/core/rxjs-interop';
|
||||
import {AuthProvider} from '../../auth/auth.provider';
|
||||
|
||||
@Component({
|
||||
selector: 'stapps-profile-page-section',
|
||||
@@ -33,9 +34,11 @@ export class ProfilePageSectionComponent {
|
||||
|
||||
authHelper = inject(AuthHelperService);
|
||||
|
||||
authProvider = inject(AuthProvider);
|
||||
|
||||
loggedIn = toSignal(
|
||||
toObservable(this.item).pipe(
|
||||
mergeMap(item => (item.authProvider ? this.authHelper.getProvider().isLoggedIn$ : of(false))),
|
||||
mergeMap(item => (item.authProvider ? this.authProvider.isLoggedIn$ : of(false))),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -66,13 +69,11 @@ export class ProfilePageSectionComponent {
|
||||
}
|
||||
|
||||
async toggleLogIn() {
|
||||
const providerType = this.item().authProvider;
|
||||
if (!providerType) return;
|
||||
if (this.loggedIn()) {
|
||||
await this.authHelper.getProvider().signOut();
|
||||
await this.authHelper.endBrowserSession(providerType);
|
||||
await this.authProvider.signOut();
|
||||
await this.authHelper.endBrowserSession();
|
||||
} else {
|
||||
await this.authHelper.getProvider().signIn();
|
||||
await this.authProvider.signIn();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import {Component, inject} from '@angular/core';
|
||||
import {AuthHelperService} from '../../auth/auth-helper.service';
|
||||
import {AuthProvider} from '../../auth/auth.provider';
|
||||
import {ActivatedRoute} from '@angular/router';
|
||||
import {ScheduleProvider} from '../../calendar/schedule.provider';
|
||||
import {profilePageSections} from '../../../../config/profile-page-sections';
|
||||
@@ -25,7 +25,7 @@ import {profilePageSections} from '../../../../config/profile-page-sections';
|
||||
standalone: false,
|
||||
})
|
||||
export class ProfilePageComponent {
|
||||
readonly authHelper = inject(AuthHelperService);
|
||||
readonly authProvider = inject(AuthProvider);
|
||||
|
||||
readonly activatedRoute = inject(ActivatedRoute);
|
||||
|
||||
@@ -33,23 +33,7 @@ export class ProfilePageComponent {
|
||||
|
||||
sections = profilePageSections;
|
||||
|
||||
async signIn() {
|
||||
const originPath = this.activatedRoute.snapshot.queryParamMap.get('origin_path');
|
||||
await (originPath ? this.authHelper.setOriginPath(originPath) : this.authHelper.deleteOriginPath());
|
||||
await this.authHelper.getProvider().signIn();
|
||||
}
|
||||
|
||||
async signOut() {
|
||||
await this.authHelper.getProvider().signOut();
|
||||
}
|
||||
|
||||
ionViewWillEnter() {
|
||||
this.authHelper
|
||||
.getProvider()
|
||||
.getValidToken()
|
||||
.then(() => void this.authHelper.getProvider().loadUserInfo())
|
||||
.catch(() => {
|
||||
// noop
|
||||
});
|
||||
ionViewWillEnter(): void {
|
||||
void this.authProvider.loadUserInfo();
|
||||
}
|
||||
}
|
||||
@@ -12,19 +12,56 @@
|
||||
* 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 {CapacitorSecureStorage} from 'ionic-appauth/lib/capacitor';
|
||||
|
||||
/*
|
||||
* Removes an item from storage before entering the new one to avoid issues
|
||||
* after iOS upgrade (iOS 16)
|
||||
import {StorageBackend} from '@openid/appauth';
|
||||
import {SecureStoragePlugin} from 'capacitor-secure-storage-plugin';
|
||||
|
||||
/**
|
||||
* Secure storage backend for AppAuth on native Capacitor platforms.
|
||||
*
|
||||
* Removes an existing item before writing it again to avoid issues
|
||||
* observed after iOS upgrades.
|
||||
*/
|
||||
export class SafeCapacitorSecureStorage extends CapacitorSecureStorage {
|
||||
async setItem(name: string, value: string): Promise<void> {
|
||||
if (!Storage) throw new Error('Capacitor Storage Is Undefined!');
|
||||
|
||||
export class SafeCapacitorSecureStorage implements StorageBackend {
|
||||
public async getItem(name: string): Promise<string | null> {
|
||||
try {
|
||||
await super.removeItem(name);
|
||||
} catch {}
|
||||
return super.setItem(name, value);
|
||||
const {value} = await SecureStoragePlugin.get({
|
||||
key: name,
|
||||
});
|
||||
|
||||
return value;
|
||||
} catch {
|
||||
// eslint-disable-next-line unicorn/no-null -- StorageBackend.getItem resolves to null for missing keys
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public async setItem(name: string, value: string): Promise<void> {
|
||||
try {
|
||||
await SecureStoragePlugin.remove({
|
||||
key: name,
|
||||
});
|
||||
} catch {
|
||||
// Item does not exist yet.
|
||||
}
|
||||
|
||||
await SecureStoragePlugin.set({
|
||||
key: name,
|
||||
value,
|
||||
});
|
||||
}
|
||||
|
||||
public async removeItem(name: string): Promise<void> {
|
||||
try {
|
||||
await SecureStoragePlugin.remove({
|
||||
key: name,
|
||||
});
|
||||
} catch {
|
||||
// Removing a non-existing item is fine.
|
||||
}
|
||||
}
|
||||
|
||||
public async clear(): Promise<void> {
|
||||
await SecureStoragePlugin.clear();
|
||||
}
|
||||
}
|
||||
@@ -1,80 +1,80 @@
|
||||
[
|
||||
{
|
||||
"licenseText": "The MIT License\n\nCopyright (c) 2010-2024 Google LLC. https://angular.dev/license\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n",
|
||||
"name": "@angular/animations@18.2.13",
|
||||
"licenseText": "The MIT License\n\nCopyright (c) 2010-2025 Google LLC. https://angular.dev/license\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n",
|
||||
"name": "@angular/animations@20.3.31",
|
||||
"licenses": "MIT",
|
||||
"repository": "https://github.com/angular/angular",
|
||||
"publisher": "angular"
|
||||
},
|
||||
{
|
||||
"licenseText": "The MIT License\n\nCopyright (c) 2024 Google LLC.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n",
|
||||
"name": "@angular/cdk@18.2.13",
|
||||
"licenseText": "The MIT License\n\nCopyright (c) 2025 Google LLC.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n",
|
||||
"name": "@angular/cdk@20.2.14",
|
||||
"licenses": "MIT",
|
||||
"repository": "https://github.com/angular/components"
|
||||
},
|
||||
{
|
||||
"licenseText": "The MIT License\n\nCopyright (c) 2010-2024 Google LLC. https://angular.dev/license\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n",
|
||||
"name": "@angular/common@18.2.13",
|
||||
"licenseText": "The MIT License\n\nCopyright (c) 2010-2025 Google LLC. https://angular.dev/license\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n",
|
||||
"name": "@angular/common@20.3.31",
|
||||
"licenses": "MIT",
|
||||
"repository": "https://github.com/angular/angular",
|
||||
"publisher": "angular"
|
||||
},
|
||||
{
|
||||
"licenseText": "The MIT License\n\nCopyright (c) 2010-2024 Google LLC. https://angular.dev/license\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n",
|
||||
"name": "@angular/core@18.2.13",
|
||||
"licenseText": "The MIT License\n\nCopyright (c) 2010-2025 Google LLC. https://angular.dev/license\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n",
|
||||
"name": "@angular/core@20.3.31",
|
||||
"licenses": "MIT",
|
||||
"repository": "https://github.com/angular/angular",
|
||||
"publisher": "angular"
|
||||
},
|
||||
{
|
||||
"licenseText": "The MIT License\n\nCopyright (c) 2010-2024 Google LLC. https://angular.dev/license\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n",
|
||||
"name": "@angular/forms@18.2.13",
|
||||
"licenseText": "The MIT License\n\nCopyright (c) 2010-2025 Google LLC. https://angular.dev/license\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n",
|
||||
"name": "@angular/forms@20.3.31",
|
||||
"licenses": "MIT",
|
||||
"repository": "https://github.com/angular/angular",
|
||||
"publisher": "angular"
|
||||
},
|
||||
{
|
||||
"licenseText": "The MIT License\n\nCopyright (c) 2010-2024 Google LLC. https://angular.dev/license\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n",
|
||||
"name": "@angular/platform-browser@18.2.13",
|
||||
"licenseText": "The MIT License\n\nCopyright (c) 2010-2025 Google LLC. https://angular.dev/license\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n",
|
||||
"name": "@angular/platform-browser@20.3.31",
|
||||
"licenses": "MIT",
|
||||
"repository": "https://github.com/angular/angular",
|
||||
"publisher": "angular"
|
||||
},
|
||||
{
|
||||
"licenseText": "The MIT License\n\nCopyright (c) 2010-2024 Google LLC. https://angular.dev/license\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n",
|
||||
"name": "@angular/router@18.2.13",
|
||||
"licenseText": "The MIT License\n\nCopyright (c) 2010-2025 Google LLC. https://angular.dev/license\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n",
|
||||
"name": "@angular/router@20.3.31",
|
||||
"licenses": "MIT",
|
||||
"repository": "https://github.com/angular/angular",
|
||||
"publisher": "angular"
|
||||
},
|
||||
{
|
||||
"name": "@awesome-cordova-plugins/calendar@6.6.0",
|
||||
"name": "@awesome-cordova-plugins/calendar@8.1.0",
|
||||
"licenses": "MIT",
|
||||
"repository": "https://github.com/danielsogl/awesome-cordova-plugins",
|
||||
"publisher": "ionic"
|
||||
},
|
||||
{
|
||||
"name": "@awesome-cordova-plugins/core@6.6.0",
|
||||
"name": "@awesome-cordova-plugins/core@8.1.0",
|
||||
"licenses": "MIT",
|
||||
"repository": "https://github.com/danielsogl/awesome-cordova-plugins",
|
||||
"publisher": "ionic"
|
||||
},
|
||||
{
|
||||
"licenseText": "The MIT License (MIT)\n\nCopyright (c) 2022 Daniel Suchý\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n",
|
||||
"name": "@capacitor-community/in-app-review@7.0.1",
|
||||
"name": "@capacitor-community/in-app-review@8.0.0",
|
||||
"licenses": "MIT",
|
||||
"repository": "https://github.com/capacitor-community/in-app-review"
|
||||
},
|
||||
{
|
||||
"licenseText": "MIT License\n\nCopyright (c) 2020 Drifty Co.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n",
|
||||
"name": "@capacitor-community/screen-brightness@7.0.0",
|
||||
"name": "@capacitor-community/screen-brightness@8.0.0",
|
||||
"licenses": "MIT",
|
||||
"repository": "https://github.com/capacitor-community/screen-brightness",
|
||||
"publisher": "Ely Lucas"
|
||||
},
|
||||
{
|
||||
"licenseText": "Copyright 2020-present Ionic\nhttps://ionic.io\n\nMIT License\n\nPermission is hereby granted, free of charge, to any person obtaining\na copy of this software and associated documentation files (the\n\"Software\"), to deal in the Software without restriction, including\nwithout limitation the rights to use, copy, modify, merge, publish,\ndistribute, sublicense, and/or sell copies of the Software, and to\npermit persons to whom the Software is furnished to do so, subject to\nthe following conditions:\n\nThe above copyright notice and this permission notice shall be\nincluded in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\nNONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE\nLIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\nOF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION\nWITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n",
|
||||
"name": "@capacitor/app@7.0.2",
|
||||
"name": "@capacitor/app@8.1.1",
|
||||
"licenses": "MIT",
|
||||
"repository": "https://github.com/ionic-team/capacitor-plugins",
|
||||
"publisher": "Ionic",
|
||||
@@ -82,7 +82,7 @@
|
||||
},
|
||||
{
|
||||
"licenseText": "Copyright 2020-present Ionic\nhttps://ionic.io\n\nMIT License\n\nPermission is hereby granted, free of charge, to any person obtaining\na copy of this software and associated documentation files (the\n\"Software\"), to deal in the Software without restriction, including\nwithout limitation the rights to use, copy, modify, merge, publish,\ndistribute, sublicense, and/or sell copies of the Software, and to\npermit persons to whom the Software is furnished to do so, subject to\nthe following conditions:\n\nThe above copyright notice and this permission notice shall be\nincluded in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\nNONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE\nLIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\nOF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION\nWITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.",
|
||||
"name": "@capacitor/browser@7.0.2",
|
||||
"name": "@capacitor/browser@8.0.4",
|
||||
"licenses": "MIT",
|
||||
"repository": "https://github.com/ionic-team/capacitor-plugins",
|
||||
"publisher": "Ionic",
|
||||
@@ -90,7 +90,7 @@
|
||||
},
|
||||
{
|
||||
"licenseText": "Copyright 2020-present Ionic\nhttps://ionic.io\n\nMIT License\n\nPermission is hereby granted, free of charge, to any person obtaining\na copy of this software and associated documentation files (the\n\"Software\"), to deal in the Software without restriction, including\nwithout limitation the rights to use, copy, modify, merge, publish,\ndistribute, sublicense, and/or sell copies of the Software, and to\npermit persons to whom the Software is furnished to do so, subject to\nthe following conditions:\n\nThe above copyright notice and this permission notice shall be\nincluded in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\nNONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE\nLIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\nOF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION\nWITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n",
|
||||
"name": "@capacitor/clipboard@7.0.2",
|
||||
"name": "@capacitor/clipboard@8.0.1",
|
||||
"licenses": "MIT",
|
||||
"repository": "https://github.com/ionic-team/capacitor-plugins",
|
||||
"publisher": "Ionic",
|
||||
@@ -98,7 +98,7 @@
|
||||
},
|
||||
{
|
||||
"licenseText": "MIT License\n\nCopyright (c) 2017-present Drifty Co.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n",
|
||||
"name": "@capacitor/core@7.4.2",
|
||||
"name": "@capacitor/core@8.5.2",
|
||||
"licenses": "MIT",
|
||||
"repository": "https://github.com/ionic-team/capacitor",
|
||||
"publisher": "Ionic Team",
|
||||
@@ -107,7 +107,7 @@
|
||||
},
|
||||
{
|
||||
"licenseText": "Copyright 2020-present Ionic\nhttps://ionic.io\n\nMIT License\n\nPermission is hereby granted, free of charge, to any person obtaining\na copy of this software and associated documentation files (the\n\"Software\"), to deal in the Software without restriction, including\nwithout limitation the rights to use, copy, modify, merge, publish,\ndistribute, sublicense, and/or sell copies of the Software, and to\npermit persons to whom the Software is furnished to do so, subject to\nthe following conditions:\n\nThe above copyright notice and this permission notice shall be\nincluded in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\nNONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE\nLIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\nOF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION\nWITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n",
|
||||
"name": "@capacitor/device@7.0.2",
|
||||
"name": "@capacitor/device@8.0.3",
|
||||
"licenses": "MIT",
|
||||
"repository": "https://github.com/ionic-team/capacitor-plugins",
|
||||
"publisher": "Ionic",
|
||||
@@ -115,7 +115,7 @@
|
||||
},
|
||||
{
|
||||
"licenseText": "Copyright 2020-present Ionic\nhttps://ionic.io\n\nMIT License\n\nPermission is hereby granted, free of charge, to any person obtaining\na copy of this software and associated documentation files (the\n\"Software\"), to deal in the Software without restriction, including\nwithout limitation the rights to use, copy, modify, merge, publish,\ndistribute, sublicense, and/or sell copies of the Software, and to\npermit persons to whom the Software is furnished to do so, subject to\nthe following conditions:\n\nThe above copyright notice and this permission notice shall be\nincluded in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\nNONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE\nLIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\nOF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION\nWITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.",
|
||||
"name": "@capacitor/dialog@7.0.2",
|
||||
"name": "@capacitor/dialog@8.0.1",
|
||||
"licenses": "MIT",
|
||||
"repository": "https://github.com/ionic-team/capacitor-plugins",
|
||||
"publisher": "Ionic",
|
||||
@@ -123,21 +123,45 @@
|
||||
},
|
||||
{
|
||||
"licenseText": "MIT License\n\nCopyright (c) 2025 Ionic\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n",
|
||||
"name": "@capacitor/filesystem@7.1.4",
|
||||
"name": "@capacitor/filesystem@8.1.3",
|
||||
"licenses": "MIT",
|
||||
"repository": "https://github.com/ionic-team/capacitor-filesystem",
|
||||
"publisher": "Outsystems"
|
||||
},
|
||||
{
|
||||
"licenseText": "MIT License\n\nCopyright (c) 2025 Ionic\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n",
|
||||
"name": "@capacitor/geolocation@7.1.5",
|
||||
"name": "@capacitor/geolocation@8.2.2",
|
||||
"licenses": "MIT",
|
||||
"repository": "https://github.com/ionic-team/capacitor-geolocation",
|
||||
"publisher": "Outsystems"
|
||||
},
|
||||
{
|
||||
"licenseText": "Copyright 2020-present Ionic\nhttps://ionic.io\n\nMIT License\n\nPermission is hereby granted, free of charge, to any person obtaining\na copy of this software and associated documentation files (the\n\"Software\"), to deal in the Software without restriction, including\nwithout limitation the rights to use, copy, modify, merge, publish,\ndistribute, sublicense, and/or sell copies of the Software, and to\npermit persons to whom the Software is furnished to do so, subject to\nthe following conditions:\n\nThe above copyright notice and this permission notice shall be\nincluded in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\nNONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE\nLIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\nOF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION\nWITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n",
|
||||
"name": "@capacitor/haptics@7.0.2",
|
||||
"name": "@capacitor/haptics@8.0.2",
|
||||
"licenses": "MIT",
|
||||
"repository": "https://github.com/ionic-team/capacitor-haptics",
|
||||
"publisher": "Ionic",
|
||||
"email": "hi@ionicframework.com"
|
||||
},
|
||||
{
|
||||
"licenseText": "Copyright 2020-present Ionic\nhttps://ionic.io\n\nMIT License\n\nPermission is hereby granted, free of charge, to any person obtaining\na copy of this software and associated documentation files (the\n\"Software\"), to deal in the Software without restriction, including\nwithout limitation the rights to use, copy, modify, merge, publish,\ndistribute, sublicense, and/or sell copies of the Software, and to\npermit persons to whom the Software is furnished to do so, subject to\nthe following conditions:\n\nThe above copyright notice and this permission notice shall be\nincluded in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\nNONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE\nLIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\nOF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION\nWITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n",
|
||||
"name": "@capacitor/keyboard@8.0.5",
|
||||
"licenses": "MIT",
|
||||
"repository": "https://github.com/ionic-team/capacitor-keyboard",
|
||||
"publisher": "Ionic",
|
||||
"email": "hi@ionicframework.com"
|
||||
},
|
||||
{
|
||||
"licenseText": "Copyright 2020-present Ionic\nhttps://ionic.io\n\nMIT License\n\nPermission is hereby granted, free of charge, to any person obtaining\na copy of this software and associated documentation files (the\n\"Software\"), to deal in the Software without restriction, including\nwithout limitation the rights to use, copy, modify, merge, publish,\ndistribute, sublicense, and/or sell copies of the Software, and to\npermit persons to whom the Software is furnished to do so, subject to\nthe following conditions:\n\nThe above copyright notice and this permission notice shall be\nincluded in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\nNONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE\nLIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\nOF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION\nWITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n",
|
||||
"name": "@capacitor/local-notifications@8.3.1",
|
||||
"licenses": "MIT",
|
||||
"repository": "https://github.com/ionic-team/capacitor-local-notifications",
|
||||
"publisher": "Ionic",
|
||||
"email": "hi@ionicframework.com"
|
||||
},
|
||||
{
|
||||
"licenseText": "Copyright 2020-present Ionic\nhttps://ionic.io\n\nMIT License\n\nPermission is hereby granted, free of charge, to any person obtaining\na copy of this software and associated documentation files (the\n\"Software\"), to deal in the Software without restriction, including\nwithout limitation the rights to use, copy, modify, merge, publish,\ndistribute, sublicense, and/or sell copies of the Software, and to\npermit persons to whom the Software is furnished to do so, subject to\nthe following conditions:\n\nThe above copyright notice and this permission notice shall be\nincluded in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\nNONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE\nLIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\nOF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION\nWITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n",
|
||||
"name": "@capacitor/network@8.0.1",
|
||||
"licenses": "MIT",
|
||||
"repository": "https://github.com/ionic-team/capacitor-plugins",
|
||||
"publisher": "Ionic",
|
||||
@@ -145,7 +169,7 @@
|
||||
},
|
||||
{
|
||||
"licenseText": "Copyright 2020-present Ionic\nhttps://ionic.io\n\nMIT License\n\nPermission is hereby granted, free of charge, to any person obtaining\na copy of this software and associated documentation files (the\n\"Software\"), to deal in the Software without restriction, including\nwithout limitation the rights to use, copy, modify, merge, publish,\ndistribute, sublicense, and/or sell copies of the Software, and to\npermit persons to whom the Software is furnished to do so, subject to\nthe following conditions:\n\nThe above copyright notice and this permission notice shall be\nincluded in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\nNONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE\nLIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\nOF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION\nWITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n",
|
||||
"name": "@capacitor/keyboard@7.0.2",
|
||||
"name": "@capacitor/preferences@8.0.1",
|
||||
"licenses": "MIT",
|
||||
"repository": "https://github.com/ionic-team/capacitor-plugins",
|
||||
"publisher": "Ionic",
|
||||
@@ -153,31 +177,7 @@
|
||||
},
|
||||
{
|
||||
"licenseText": "Copyright 2020-present Ionic\nhttps://ionic.io\n\nMIT License\n\nPermission is hereby granted, free of charge, to any person obtaining\na copy of this software and associated documentation files (the\n\"Software\"), to deal in the Software without restriction, including\nwithout limitation the rights to use, copy, modify, merge, publish,\ndistribute, sublicense, and/or sell copies of the Software, and to\npermit persons to whom the Software is furnished to do so, subject to\nthe following conditions:\n\nThe above copyright notice and this permission notice shall be\nincluded in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\nNONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE\nLIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\nOF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION\nWITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n",
|
||||
"name": "@capacitor/local-notifications@7.0.2",
|
||||
"licenses": "MIT",
|
||||
"repository": "https://github.com/ionic-team/capacitor-plugins",
|
||||
"publisher": "Ionic",
|
||||
"email": "hi@ionicframework.com"
|
||||
},
|
||||
{
|
||||
"licenseText": "Copyright 2020-present Ionic\nhttps://ionic.io\n\nMIT License\n\nPermission is hereby granted, free of charge, to any person obtaining\na copy of this software and associated documentation files (the\n\"Software\"), to deal in the Software without restriction, including\nwithout limitation the rights to use, copy, modify, merge, publish,\ndistribute, sublicense, and/or sell copies of the Software, and to\npermit persons to whom the Software is furnished to do so, subject to\nthe following conditions:\n\nThe above copyright notice and this permission notice shall be\nincluded in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\nNONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE\nLIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\nOF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION\nWITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n",
|
||||
"name": "@capacitor/network@7.0.2",
|
||||
"licenses": "MIT",
|
||||
"repository": "https://github.com/ionic-team/capacitor-plugins",
|
||||
"publisher": "Ionic",
|
||||
"email": "hi@ionicframework.com"
|
||||
},
|
||||
{
|
||||
"licenseText": "Copyright 2020-present Ionic\nhttps://ionic.io\n\nMIT License\n\nPermission is hereby granted, free of charge, to any person obtaining\na copy of this software and associated documentation files (the\n\"Software\"), to deal in the Software without restriction, including\nwithout limitation the rights to use, copy, modify, merge, publish,\ndistribute, sublicense, and/or sell copies of the Software, and to\npermit persons to whom the Software is furnished to do so, subject to\nthe following conditions:\n\nThe above copyright notice and this permission notice shall be\nincluded in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\nNONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE\nLIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\nOF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION\nWITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n",
|
||||
"name": "@capacitor/preferences@7.0.2",
|
||||
"licenses": "MIT",
|
||||
"repository": "https://github.com/ionic-team/capacitor-plugins",
|
||||
"publisher": "Ionic",
|
||||
"email": "hi@ionicframework.com"
|
||||
},
|
||||
{
|
||||
"licenseText": "Copyright 2020-present Ionic\nhttps://ionic.io\n\nMIT License\n\nPermission is hereby granted, free of charge, to any person obtaining\na copy of this software and associated documentation files (the\n\"Software\"), to deal in the Software without restriction, including\nwithout limitation the rights to use, copy, modify, merge, publish,\ndistribute, sublicense, and/or sell copies of the Software, and to\npermit persons to whom the Software is furnished to do so, subject to\nthe following conditions:\n\nThe above copyright notice and this permission notice shall be\nincluded in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\nNONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE\nLIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\nOF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION\nWITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n",
|
||||
"name": "@capacitor/screen-orientation@7.0.2",
|
||||
"name": "@capacitor/screen-orientation@8.0.1",
|
||||
"licenses": "MIT",
|
||||
"repository": "https://github.com/ionic-team/capacitor-plugins",
|
||||
"publisher": "Ionic",
|
||||
@@ -185,7 +185,7 @@
|
||||
},
|
||||
{
|
||||
"licenseText": "Copyright 2020-present Ionic\nhttps://ionic.io\n\nMIT License\n\nPermission is hereby granted, free of charge, to any person obtaining\na copy of this software and associated documentation files (the\n\"Software\"), to deal in the Software without restriction, including\nwithout limitation the rights to use, copy, modify, merge, publish,\ndistribute, sublicense, and/or sell copies of the Software, and to\npermit persons to whom the Software is furnished to do so, subject to\nthe following conditions:\n\nThe above copyright notice and this permission notice shall be\nincluded in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\nNONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE\nLIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\nOF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION\nWITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.",
|
||||
"name": "@capacitor/share@7.0.2",
|
||||
"name": "@capacitor/share@8.0.2",
|
||||
"licenses": "MIT",
|
||||
"repository": "https://github.com/ionic-team/capacitor-plugins",
|
||||
"publisher": "Ionic",
|
||||
@@ -193,21 +193,21 @@
|
||||
},
|
||||
{
|
||||
"licenseText": "Copyright 2020-present Ionic\nhttps://ionic.io\n\nMIT License\n\nPermission is hereby granted, free of charge, to any person obtaining\na copy of this software and associated documentation files (the\n\"Software\"), to deal in the Software without restriction, including\nwithout limitation the rights to use, copy, modify, merge, publish,\ndistribute, sublicense, and/or sell copies of the Software, and to\npermit persons to whom the Software is furnished to do so, subject to\nthe following conditions:\n\nThe above copyright notice and this permission notice shall be\nincluded in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\nNONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE\nLIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\nOF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION\nWITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n",
|
||||
"name": "@capacitor/splash-screen@7.0.2",
|
||||
"name": "@capacitor/splash-screen@8.0.2",
|
||||
"licenses": "MIT",
|
||||
"repository": "https://github.com/ionic-team/capacitor-plugins",
|
||||
"publisher": "Ionic",
|
||||
"email": "hi@ionicframework.com"
|
||||
},
|
||||
{
|
||||
"name": "@ionic/angular-server@8.7.3",
|
||||
"name": "@ionic/angular-server@8.8.19",
|
||||
"licenses": "MIT",
|
||||
"repository": "https://github.com/ionic-team/ionic-framework",
|
||||
"publisher": "Ionic Team"
|
||||
},
|
||||
{
|
||||
"licenseText": "# @ionic/angular\n\nIonic Angular specific building blocks on top of [@ionic/core](https://www.npmjs.com/package/@ionic/core) components.\n\n\n## Related\n\n* [Ionic Core Components](https://www.npmjs.com/package/@ionic/core)\n* [Ionic Documentation](https://ionicframework.com/docs/)\n* [Ionic Discord](https://ionic.link/discord)\n* [Ionic Forum](https://forum.ionicframework.com/)\n* [Ionicons](http://ionicons.com/)\n* [Stencil](https://stenciljs.com/)\n* [Capacitor](https://capacitor.ionicframework.com/)\n\n\n## License\n\n* [MIT](https://raw.githubusercontent.com/ionic-team/ionic/main/LICENSE)\n\n## Testing Local Ionic Framework with `ng add`\n\nThis guide shows you how to test the local Ionic Framework build with a new Angular application using `ng add`. This is useful for development and testing changes before publishing.\n\n### Prerequisites\n\n- Node.js and npm installed\n- Angular CLI installed globally (`npm install -g @angular/cli`)\n\n### Build Local Ionic Framework\n\n1. Clone the repository (if not already done):\n ```sh\n git clone https://github.com/ionic-team/ionic-framework.git\n cd ionic-framework\n ```\n\n2. Pull the latest from `main`\n ```sh\n git pull origin main\n ```\n\n3. Install dependencies and build the `core` package:\n ```sh\n cd core\n npm install\n npm run build\n ```\n\n4. Install dependencies, sync the `core` build and build the Angular package:\n ```sh\n cd ../packages/angular\n npm install\n npm run sync\n npm run build\n ```\n\n5. Create a tarball:\n ```sh\n cd dist\n npm pack\n ```\n\n6. Copy the tarball to Downloads:\n ```sh\n cp ionic-angular-*.tgz ~/Downloads/ionic-angular.tgz\n ```\n\n### Test with New Angular App\n\n7. Create a new Angular app:\n ```sh\n # Change to whichever directory you want the app in\n cd ~/Documents/\n ng new my-app --style=css --ssr=false --zoneless=false\n cd my-app\n ```\n\n8. Install the local `@ionic/angular` package:\n ```sh\n npm install ~/Downloads/ionic-angular.tgz\n ```\n\n9. Run `ng add`:\n ```sh\n ng add @ionic/angular --skip-confirmation\n ```\n\n10. Serve the app:\n ```sh\n ng serve\n ```\n\nThe local Ionic Framework build is now active in the Angular app. Changes to the Ionic source code require rebuilding the packages and reinstalling the tarball to see updates.\n\n## Project Structure\n\n**common**\n\nThis is where logic that is shared between lazy loaded and standalone components live. For example, the lazy loaded IonPopover and standalone IonPopover components extend from a base IonPopover implementation that exists in this directory.\n\n**Note:** This directory exposes internal APIs and is only accessed in the `standalone` and `src` submodules. Ionic developers should never import directly from `@ionic/angular/common`. Instead, they should import from `@ionic/angular` or `@ionic/angular/standalone`.\n\n**standalone**\n\nThis is where the standalone component implementations live. It was added as a separate entry point to avoid any lazy loaded logic from accidentally being pulled in to the final build. Having a separate directory allows the lazy loaded implementation to remain accessible from `@ionic/angular` for backwards compatibility.\n\nIonic developers can access this by importing from `@ionic/angular/standalone`.\n\n**src**\n\nThis is where the lazy loaded component implementations live.\n\nIonic developers can access this by importing from `@ionic/angular`.\n",
|
||||
"name": "@ionic/angular@8.7.3",
|
||||
"name": "@ionic/angular@8.8.19",
|
||||
"licenses": "MIT",
|
||||
"repository": "https://github.com/ionic-team/ionic-framework"
|
||||
},
|
||||
@@ -217,23 +217,16 @@
|
||||
"repository": "https://github.com/ionic-team/ionic-storage"
|
||||
},
|
||||
{
|
||||
"licenseText": "\n<div align=\"center\">\n <img src=\"./projects/showcase/src/assets/ngx-maplibre-gl-red.svg\" height=\"128\">\n</div>\n<div align=\"center\">\n <h1>ngx-maplibre-gl</h1>\n</div>\n\nAngular wrapper for [maplibre-gl](https://www.maplibre.org/). It exposes a bunch of components meant to be simple to use with Angular.\n\n[](https://www.npmjs.com/package/@maplibre/ngx-maplibre-gl)\n\n### Demo site\n\nCan be found here (based on the generated gh-pages in this repo):\nhttps://maplibre.org/ngx-maplibre-gl/\n\n### Attribution\n\nThis is a fork of [ngx-mapbox-gl](https://github.com/Wykks/ngx-mapbox-gl) and I would like to thank the maintainers there for thier amazing work to build this up. It's truely a great piece of sotware!\n\n### Components\n\n- [mgl-map](docs/API.md#mgl-map-mapbox-gl-api)\n- [mgl-layer](docs/API.md#mgl-layer-mapbox-gl-style-spec)\n- [mgl-geojson-source](docs/API.md#mgl-geojson-source-mapbox-gl-style-spec)\n- [mgl-canvas-source](docs/API.md#mgl-canvas-source-mapbox-gl-style-spec)\n- [mgl-image-source](docs/API.md#mgl-image-source-mapbox-gl-style-spec)\n- [mgl-raster-dem-source](docs/API.md#mgl-raster-dem-source-mapbox-gl-style-spec)\n- [mgl-raster-source](docs/API.md#mgl-raster-source-mapbox-gl-style-spec)\n- [mgl-vector-source](docs/API.md#mgl-vector-source-mapbox-gl-style-spec)\n- [mgl-video-source](docs/API.md#mgl-video-source-mapbox-gl-style-spec)\n- [mgl-image](docs/API.md#mgl-image-mapbox-gl-api)\n- [mgl-control](docs/API.md#mgl-control) with builtin control:\n - mglScale\n - mglFullscreen\n - mglAttribution\n - mglGeolocate\n - mglNavigation\n- [mgl-marker](docs/API.md#mgl-marker-mapbox-gl-api)\n- [mgl-popup](docs/API.md#mgl-popup-mapbox-gl-api)\n- [mgl-marker-cluster](docs/API.md#ngx-mgl-marker-cluster-supercluster-api)\n\n## How to start\n\n```\nnpm install @maplibre/ngx-maplibre-gl maplibre-gl\nyarn add @maplibre/ngx-maplibre-gl maplibre-gl\n```\n\nThere might be a need to add the following configuration to `tsconfig.json` file\n\n```\n\"compilerOptions\": {\n ...\n \"strictNullChecks\": false,\n \"skipLibCheck\": true,\n}\n```\n\nLoad the CSS of `maplibre-gl`\n\nFor example, with _angular-cli_ add this in `angular.json`:\n\n```json\n\"styles\": [\n ...,\n \"./node_modules/maplibre-gl/dist/maplibre-gl.css\"\n],\n```\n\nOr in the global CSS file (called `styles.css` for example in _angular-cli_):\n\n```css\n@import '~maplibre-gl/dist/maplibre-gl.css';\n```\n\nAdd this in your polyfill.ts file (https://github.com/Wykks/ngx-mapbox-gl/issues/136#issuecomment-496224634):\n\n```\n(window as any).global = window;\n```\n\nThen, in your app's main module (or in any other module), import the `MapComponent`:\n\n```ts\nimport { Component } from '@angular/core';\nimport { MapComponent } from '@maplibre/ngx-maplibre-gl';\n\n@NgModule({\n template: `\n <mgl-map\n [style]=\"'https://demotiles.maplibre.org/style.json'\"\n [zoom]=\"[9]\"\n [center]=\"[-74.5, 40]\"\n >\n </mgl-map>\n `,\n styles: [\n `\n mgl-map {\n height: 100%;\n width: 100%;\n }\n `,\n ],\n standalone: true,\n imports: [MapComponent],\n})\nexport class AppComponent {}\n```\n",
|
||||
"name": "@maplibre/ngx-maplibre-gl@17.4.1",
|
||||
"licenseText": "\n<div align=\"center\">\n <img src=\"https://raw.githubusercontent.com/maplibre/ngx-maplibre-gl/main/projects/showcase/src/assets/ngx-maplibre-gl-red.svg\" height=\"128\">\n</div>\n<div align=\"center\">\n <h1>ngx-maplibre-gl</h1>\n</div>\n\nAngular wrapper for [maplibre-gl](https://www.maplibre.org/). It exposes a bunch of components meant to be simple to use with Angular.\n\n[](https://www.npmjs.com/package/@maplibre/ngx-maplibre-gl)\n\n### Demo site\n\nCan be found here (based on the generated gh-pages in this repo):\nhttps://maplibre.org/ngx-maplibre-gl/\n\n### Attribution\n\nThis is a fork of [ngx-mapbox-gl](https://github.com/Wykks/ngx-mapbox-gl) and I would like to thank the maintainers there for their amazing work to build this up. It's truly a great piece of software!\n\n### API Documentation\n\n[The API documentation can be found here](https://maplibre.org/ngx-maplibre-gl/API/).\n\n## How to start\n\n```\nnpm install @maplibre/ngx-maplibre-gl maplibre-gl\nyarn add @maplibre/ngx-maplibre-gl maplibre-gl\n```\n\nThere might be a need to add the following configuration to `tsconfig.json` file\n\n```\n\"compilerOptions\": {\n ...\n \"strictNullChecks\": false,\n \"skipLibCheck\": true,\n}\n```\n\nLoad the CSS of `maplibre-gl`\n\nFor example, with _angular-cli_ add this in `angular.json`:\n\n```json\n\"styles\": [\n ...,\n \"./node_modules/maplibre-gl/dist/maplibre-gl.css\"\n],\n```\n\nOr in the global CSS file (called `styles.css` for example in _angular-cli_):\n\n```css\n@import 'maplibre-gl/dist/maplibre-gl.css';\n```\n\n### Serve the MapLibre GL JS web worker\n\nMapLibre GL JS v6 is ESM-only and loads its web worker from a separate file at\nruntime. Bundlers cannot rewrite that URL, so every bundler-based app must point\nMapLibre at it before the first map is created — see\n[`setWorkerUrl()` is bundler-only](https://maplibre.org/maplibre-gl-js/docs/guides/v5-to-v6-migration-guide/#setworkerurl-is-bundler-only).\nWithout it **the worker 404s and no tiles render**. Here is the Angular setup.\n\nAdd both files to the `assets` of your `build` target in `angular.json`. The worker\nimports `maplibre-gl-shared.mjs` as a sibling at runtime, so they must be copied\ntogether, into the same directory:\n\n```json\n\"assets\": [\n ...,\n {\n \"glob\": \"maplibre-gl-worker.mjs\",\n \"input\": \"node_modules/maplibre-gl/dist\",\n \"output\": \"/\"\n },\n {\n \"glob\": \"maplibre-gl-shared.mjs\",\n \"input\": \"node_modules/maplibre-gl/dist\",\n \"output\": \"/\"\n }\n]\n```\n\nThen provide the worker URL with `provideMaplibreWorker` in your application config:\n\n```ts\nimport { ApplicationConfig } from '@angular/core';\nimport { provideMaplibreWorker } from '@maplibre/ngx-maplibre-gl/config';\n\nexport const appConfig: ApplicationConfig = {\n providers: [provideMaplibreWorker('maplibre-gl-worker.mjs')],\n};\n```\n\nThe URL is resolved against `document.baseURI`, so it stays correct when your app\nis deployed under a sub-path via `--base-href`. It can also be provided in a\ncomponent's `providers` to scope it to a subtree. The URL is applied lazily, right\nbefore the first map is created — unlike calling `setWorkerUrl()` in `main.ts`,\nthis keeps `maplibre-gl` out of your initial bundle.\n\nIf you cannot use the provider, call `setWorkerUrl()` once yourself before the\nfirst map is created instead:\n\n```ts\nimport { setWorkerUrl } from 'maplibre-gl';\n\nsetWorkerUrl(new URL('maplibre-gl-worker.mjs', document.baseURI).href);\n```\n\nIf you also run browser-based unit tests, do the same in your test setup file. When\ntests are served by Vite, a `?url` import resolves the worker straight out of\n`node_modules`:\n\n```ts\nimport { setWorkerUrl } from 'maplibre-gl';\nimport workerUrl from 'maplibre-gl/dist/maplibre-gl-worker.mjs?url';\n\nsetWorkerUrl(new URL(workerUrl, document.baseURI).href);\n```\n\n### Upgrading MapLibre GL JS\n\nUpgrading from v5? Follow the\n[v5 to v6 migration guide](https://maplibre.org/maplibre-gl-js/docs/guides/v5-to-v6-migration-guide/).\n\n### Breaking change: camera inputs are plain values\n\n`[zoom]`, `[bearing]`, `[pitch]` and `[roll]` took a single-element array, so that\nre-assigning the same number still counted as a change. They are now plain numbers,\nand two-way capable:\n\n```diff\n- <mgl-map [zoom]=\"[9]\" [pitch]=\"[45]\" [bearing]=\"[-17.6]\" />\n+ <mgl-map [zoom]=\"9\" [pitch]=\"45\" [bearing]=\"-17.6\" />\n```\n\nUpdating one axis has never dragged the others back with it - that is handled by\nonly sending the axes that actually changed - so nothing else changes for one-way\nbindings.\n\nIf you relied on the array to return the map to a position the user had since\nmoved away from, bind two-way instead. The camera models follow the map, so the\nearlier value is a real change again:\n\n```html\n<mgl-map [(zoom)]=\"zoom\" [(center)]Line truncated
|
||||
"name": "@maplibre/ngx-maplibre-gl@22.1.0",
|
||||
"licenses": "MIT",
|
||||
"repository": "https://github.com/maplibre/ngx-maplibre-gl"
|
||||
},
|
||||
{"name": "@ngx-translate/core@15.0.0", "licenses": "Custom: LICENSE"},
|
||||
{"name": "@ngx-translate/http-loader@8.0.0", "licenses": "Custom: LICENSE"},
|
||||
{
|
||||
"licenseText": " Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under thiLine truncated
|
||||
"name": "@openid/appauth@1.3.2",
|
||||
"licenses": "Apache-2.0",
|
||||
"repository": "https://github.com/openid/AppAuth-JS",
|
||||
"publisher": "The OpenID Foundation"
|
||||
},
|
||||
{
|
||||
"licenseText": "# @transistorsoft/capacitor-background-fetch\n\n[](https://www.transistorsoft.com)\n\nBy [**Transistor Software**](http://transistorsoft.com), creators of [**Capacitor Background Geolocation**](http://www.transistorsoft.com/shop/products/capacitor-background-geolocation)\n\n------------------------------------------------------------------------------\n\n*Background Fetch* is a *very* simple plugin which attempts to awaken an app in the background about **every 15 minutes**, providing a short period of background running-time. This plugin will execute your provided `callbackFn` whenever a background-fetch event occurs.\n\nThere is **no way** to increase the rate which a fetch-event occurs and this plugin sets the rate to the most frequent possible — you will **never** receive an event faster than **15 minutes**. The operating-system will automatically throttle the rate the background-fetch events occur based upon usage patterns. Eg: if user hasn't turned on their phone for a long period of time, fetch events will occur less frequently or if an iOS user disables background refresh they may not happen at all.\n\n> ### :rotating_light: This plugin requires Capacitor 5 :rotating_light:\n>\n> For Capacitor 4, use the 1.x version of the plugin.\n\n\n:new: Background Fetch now provides a [__`scheduleTask`__](#executing-custom-tasks) method for scheduling arbitrary \"one-shot\" or periodic tasks.\n\n### iOS\n- There is **no way** to increase the rate which a fetch-event occurs and this plugin sets the rate to the most frequent possible — you will **never** receive an event faster than **15 minutes**. The operating-system will automatically throttle the rate the background-fetch events occur based upon usage patterns. Eg: if user hasn't turned on their phone for a long period of time, fetch events will occur less frequently.\n- [__`scheduleTask`__](#executing-custom-tasks) seems only to fire when the device is plugged into power.\n- ⚠️ When your app is **terminated**, iOS *no longer fires events* — There is *no such thing* as [__`stopOnTerminate: false`__](https://transistorsoft.github.io/capacitor-background-fetch/interfaces/backgroundfetchconfig.html#stoponterminate) for iOS.\n- iOS can take *days* before Apple's machine-learning algorithm settles in and begins regularly firing events. Do not sit staring at your logs waiting for an event to fire. If your [*simulated events*](#debugging) work, that's all you need to know that everything is correctly configured.\n- If the user doesn't open your *iOS* app for long periods of time, *iOS* will **stop firing events**.\n\n### Android\n- The Android plugin provides a __*Headless*__ mechanism allowing you to continue handling events even after app-termination (see [Receiving Events After App Termination](#receiving-events-after-app-termination-1))\n\n-------------------------------------------------------------\n\n# Contents\n- ### :books: [API Documentation](https://transistorsoft.github.io/capacitor-background-fetch/)\n- ### [Installing the Plugin](#installing-the-plugin)\n- ### [Setup Guides](#setup-guides)\n - [iOS Setup](help/INSTALL-IOS.md)\n - [Android Setup](help/INSTALL-ANDROID.md)\n- ### [Example](#example)\n- ### [Receiving events after app termination](#receiving-events-after-app-termination-1)\n- ### [Debugging](#debugging)\n\n-------------------------------------------------------------\n\n## Installing the plugin\n\n### With `yarn`\n\n```bash\n$ yarn add @transistorsoft/capacitor-background-fetch\n$ npx cap sync\n```\n\n### With `npm`\n```bash\n$ npm install --save @transistorsoft/capacitor-background-fetch\n$ npx cap sync\n```\n\n- Proceed to [Required Setup Guides](#setup-guides)\n\n## Setup Guides\n\n### iOS Setup\n\n- [Required Setup](help/INSTALL-IOS.md)\n\n### Android Setup\n\n- [Required Setup](help/INSTALL-ANDROID.md)\n\n## Example ##\n\n:information_source: This repo contains its own *Example App*. See [`/example`](./example/README.md)\n\n#### Angular Example:\n\n- See API Docs [__`BackgroundFetch.configure`__](https://transistorsoft.github.io/capacitor-background-fetch/classes/backgroundfetch.html#configure)\n\n```javascript\nimport { Component } from '@angular/core';\n\nimport {BackgroundFetch} from '@transistorsoft/capacitor-background-fetch';\n\n@Component({\n selector: 'app-home',\n templateUrl: 'home.page.html',\n styleUrls: ['home.page.scss'],\n})\nexport class HomePage {\n constructor() {}\n\n // Initialize in ngAfterContentInit\n // [WARNING] DO NOT use ionViewWillEnter, as that method won't run when app is launched in background.\n ngAfterContentInit() {\n this.initBackgroundFetch();\n }\n\n async initBackgroundFetch() {\n const status = await BackgroundFetch.configure({\n minimumFetchInterval: 15\n }, async (taskId) => {\n console.log('[BackgroundFetch] EVENT:', taskId);\n //Line truncated
|
||||
"name": "@transistorsoft/capacitor-background-fetch@7.1.0",
|
||||
"licenseText": "<p align=\"center\">\n <img src=\"https://raw.githubusercontent.com/transistorsoft/assets/master/images/logos/transistor/transistor-logo-panel-ts.svg\" alt=\"Background Fetch for Capacitor\" width=\"635\">\n</p>\n\n@transistorsoft/capacitor-background-fetch · [](https://www.npmjs.com/package/@transistorsoft/capacitor-background-fetch)\n==============================================================================\n\nBy [**Transistor Software**](http://transistorsoft.com), creators of [**Capacitor Background Geolocation**](http://www.transistorsoft.com/shop/products/capacitor-background-geolocation)\n\n------------------------------------------------------------------------------\n\nBackground Fetch is a *very* simple plugin which attempts to awaken an app in the background about **every 15 minutes**, providing a short period of background running-time. This plugin will execute your provided `callbackFn` whenever a background-fetch event occurs.\n\nThere is **no way** to increase the rate which a fetch-event occurs and this plugin sets the rate to the most frequent possible — you will **never** receive an event faster than **15 minutes**. The operating-system will automatically throttle the rate the background-fetch events occur based upon usage patterns. Eg: if user hasn't turned on their phone for a long period of time, fetch events will occur less frequently or if an iOS user disables background refresh they may not happen at all.\n\n:new: Background Fetch now provides a [`scheduleTask`](https://fetch.transistorsoft.com/capacitor/BackgroundFetch#scheduletask) method for scheduling arbitrary \"one-shot\" or periodic tasks.\n\n### iOS\n- There is **no way** to increase the rate which a fetch-event occurs and this plugin sets the rate to the most frequent possible — you will **never** receive an event faster than **15 minutes**. The operating-system will automatically throttle the rate the background-fetch events occur based upon usage patterns. Eg: if user hasn't turned on their phone for a long period of time, fetch events will occur less frequently.\n- [`scheduleTask`](https://fetch.transistorsoft.com/capacitor/BackgroundFetch#scheduletask) seems only to fire when the device is plugged into power.\n- :warning: When your app is **terminated**, iOS *no longer fires events* — There is *no such thing* as **`stopOnTerminate: false`** for iOS.\n- iOS can take *days* before Apple's machine-learning algorithm settles in and begins regularly firing events. Do not sit staring at your logs waiting for an event to fire. If your [*simulated events*](https://fetch.transistorsoft.com/capacitor/debugging) work, that's all you need to know that everything is correctly configured.\n- If the user doesn't open your *iOS* app for long periods of time, *iOS* will **stop firing events**.\n\n### Android\n- The Android plugin provides a **Headless** mechanism allowing you to continue handling events even after app-termination (see [`enableHeadless`](https://fetch.transistorsoft.com/capacitor/BackgroundFetchConfig#enableheadless)).\n\n-------------------------------------------------------------\n\n# Contents\n\n- :wrench: [Setup Guide](https://fetch.transistorsoft.com/capacitor/setup)\n- :books: [API Documentation](https://fetch.transistorsoft.com/capacitor/BackgroundFetch)\n- :rocket: [Examples](https://fetch.transistorsoft.com/capacitor/examples)\n- :beetle: [Debugging](https://fetch.transistorsoft.com/capacitor/debugging)\n- :open_file_folder: [Example App](./example)\n\n## Installing the plugin\n\n```bash\nnpm install @transistorsoft/capacitor-background-fetch\nnpx cap sync\n```\n\n## Licence\n\nThe MIT License\n\nCopyright (c) 2013 Chris Scott, Transistor Software <chris@transistorsoft.com>\nhttp://transistorsoft.com\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n",
|
||||
"name": "@transistorsoft/capacitor-background-fetch@8.1.3",
|
||||
"licenses": "MIT",
|
||||
"repository": "https://github.com/transistorsoft/capacitor-background-fetch",
|
||||
"publisher": "Transistor Software",
|
||||
@@ -241,7 +234,7 @@
|
||||
},
|
||||
{
|
||||
"licenseText": " MIT License\n\n Copyright (c) Microsoft Corporation.\n\n Permission is hereby granted, free of charge, to any person obtaining a copy\n of this software and associated documentation files (the \"Software\"), to deal\n in the Software without restriction, including without limitation the rights\n to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n copies of the Software, and to permit persons to whom the Software is\n furnished to do so, subject to the following conditions:\n\n The above copyright notice and this permission notice shall be included in all\n copies or substantial portions of the Software.\n\n THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n SOFTWARE\n",
|
||||
"name": "@types/dom-view-transitions@1.0.4",
|
||||
"name": "@types/dom-view-transitions@1.0.6",
|
||||
"licenses": "MIT",
|
||||
"repository": "https://github.com/DefinitelyTyped/DefinitelyTyped"
|
||||
},
|
||||
@@ -254,7 +247,7 @@
|
||||
},
|
||||
{
|
||||
"licenseText": "MIT License\n\nCopyright (c) 2019 martinkasa\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n",
|
||||
"name": "capacitor-secure-storage-plugin@0.11.0",
|
||||
"name": "capacitor-secure-storage-plugin@0.13.0",
|
||||
"licenses": "MIT",
|
||||
"repository": "https://github.com/martinkasa/capacitor-secure-storage-plugin",
|
||||
"publisher": "martinkasa"
|
||||
@@ -282,7 +275,7 @@
|
||||
},
|
||||
{
|
||||
"licenseText": "Copyright (c) 2012 Felix Geisendörfer (felix@debuggable.com) and contributors\n\n Permission is hereby granted, free of charge, to any person obtaining a copy\n of this software and associated documentation files (the \"Software\"), to deal\n in the Software without restriction, including without limitation the rights\n to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n copies of the Software, and to permit persons to whom the Software is\n furnished to do so, subject to the following conditions:\n\n The above copyright notice and this permission notice shall be included in\n all copies or substantial portions of the Software.\n\n THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n THE SOFTWARE.\n",
|
||||
"name": "form-data@4.0.4",
|
||||
"name": "form-data@4.0.6",
|
||||
"licenses": "MIT",
|
||||
"repository": "https://github.com/form-data/form-data",
|
||||
"publisher": "Felix Geisendörfer",
|
||||
@@ -296,16 +289,9 @@
|
||||
"repository": "https://github.com/caseycesari/geojson.js",
|
||||
"publisher": "Casey Cesari"
|
||||
},
|
||||
{
|
||||
"licenseText": "MIT License\n\nCopyright (c) 2019 Wi3land\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n",
|
||||
"name": "ionic-appauth@2.1.0",
|
||||
"licenses": "MIT",
|
||||
"repository": "https://github.com/wi3land/ionic-appauth",
|
||||
"publisher": "wi3land"
|
||||
},
|
||||
{
|
||||
"licenseText": "The MIT License (MIT)\n\nCopyright (c) 2015-present Ionic (http://ionic.io/)\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n",
|
||||
"name": "ionicons@8.0.13",
|
||||
"name": "ionicons@8.1.0",
|
||||
"licenses": "MIT",
|
||||
"repository": "https://github.com/ionic-team/ionicons",
|
||||
"publisher": "Ben Sperry",
|
||||
@@ -320,7 +306,7 @@
|
||||
},
|
||||
{
|
||||
"licenseText": "Copyright (c) 2023, MapLibre contributors\n\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without modification,\nare permitted provided that the following conditions are met:\n\n * Redistributions of source code must retain the above copyright notice,\n this list of conditions and the following disclaimer.\n * Redistributions in binary form must reproduce the above copyright notice,\n this list of conditions and the following disclaimer in the documentation\n and/or other materials provided with the distribution.\n * Neither the name of MapLibre GL JS nor the names of its contributors\n may be used to endorse or promote products derived from this software\n without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR\nCONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,\nEXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,\nPROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR\nPROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF\nLIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING\nNEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\nSOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n\n-------------------------------------------------------------------------------\n\nContains code from mapbox-gl-js v1.13 and earlier\n\nVersion v1.13 of mapbox-gl-js and earlier are licensed under a BSD-3-Clause license\n\nCopyright (c) 2020, Mapbox\nRedistribution and use in source and binary forms, with or without modification,\nare permitted provided that the following conditions are met:\n\n* Redistributions of source code must retain the above copyright notice,\n this list of conditions and the following disclaimer.\n* Redistributions in binary form must reproduce the above copyright notice,\n this list of conditions and the following disclaimer in the documentation\n and/or other materials provided with the distribution.\n* Neither the name of Mapbox GL JS nor the names of its contributors\n may be used to endorse or promote products derived from this software\n without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR\nCONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,\nEXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,\nPROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR\nPROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF\nLIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING\nNEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,\nEVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n\n-------------------------------------------------------------------------------\n\nContains code from glfx.js\n\nCopyright (C) 2011 by Evan Wallace\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n\n--------------------------------------------------------------------------------\n\nContains a portion of d3-color https://github.com/d3/d3-color\n\nCopyright 2010-2016 Mike Bostock\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without modification,\nare permitted provided that the following conditions are met:\n\n* Redistributions of source code must retain the above copyright notice, this\n list of conditions and the following disclaimer.\n\n* Redistributions in binary form muLine truncated
|
||||
"name": "maplibre-gl@4.0.2",
|
||||
"name": "maplibre-gl@6.4.1",
|
||||
"licenses": "BSD-3-Clause",
|
||||
"repository": "https://github.com/maplibre/maplibre-gl-js"
|
||||
},
|
||||
@@ -354,7 +340,7 @@
|
||||
},
|
||||
{
|
||||
"licenseText": "MIT License\r\n\r\nCopyright (c) 2017-2024 Jean-Francois Cere\r\n\r\nPermission is hereby granted, free of charge, to any person obtaining a copy\r\nof this software and associated documentation files (the \"Software\"), to deal\r\nin the Software without restriction, including without limitation the rights\r\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\r\ncopies of the Software, and to permit persons to whom the Software is\r\nfurnished to do so, subject to the following conditions:\r\n\r\nThe above copyright notice and this permission notice shall be included in all\r\ncopies or substantial portions of the Software.\r\n\r\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\r\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\r\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\r\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\r\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\r\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\r\nSOFTWARE.\r\n",
|
||||
"name": "ngx-markdown@17.2.1",
|
||||
"name": "ngx-markdown@18.1.0",
|
||||
"licenses": "MIT",
|
||||
"repository": "https://github.com/jfcere/ngx-markdown",
|
||||
"publisher": "Jean-Francois Cere",
|
||||
@@ -390,7 +376,7 @@
|
||||
},
|
||||
{
|
||||
"licenseText": " Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/\n\n TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n 1. Definitions.\n\n \"License\" shall mean the terms and conditions for use, reproduction,\n and distribution as defined by Sections 1 through 9 of this document.\n\n \"Licensor\" shall mean the copyright owner or entity authorized by\n the copyright owner that is granting the License.\n\n \"Legal Entity\" shall mean the union of the acting entity and all\n other entities that control, are controlled by, or are under common\n control with that entity. For the purposes of this definition,\n \"control\" means (i) the power, direct or indirect, to cause the\n direction or management of such entity, whether by contract or\n otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\n\n \"You\" (or \"Your\") shall mean an individual or Legal Entity\n exercising permissions granted by this License.\n\n \"Source\" form shall mean the preferred form for making modifications,\n including but not limited to software source code, documentation\n source, and configuration files.\n\n \"Object\" form shall mean any form resulting from mechanical\n transformation or translation of a Source form, including but\n not limited to compiled object code, generated documentation,\n and conversions to other media types.\n\n \"Work\" shall mean the work of authorship, whether in Source or\n Object form, made available under the License, as indicated by a\n copyright notice that is included in or attached to the work\n (an example is provided in the Appendix below).\n\n \"Derivative Works\" shall mean any work, whether in Source or Object\n form, that is based on (or derived from) the Work and for which the\n editorial revisions, annotations, elaborations, or other modifications\n represent, as a whole, an original work of authorship. For the purposes\n of this License, Derivative Works shall not include works that remain\n separable from, or merely link (or bind by name) to the interfaces of,\n the Work and Derivative Works thereof.\n\n \"Contribution\" shall mean any work of authorship, including\n the original version of the Work and any modifications or additions\n to that Work or Derivative Works thereof, that is intentionally\n submitted to Licensor for inclusion in the Work by the copyright owner\n or by an individual or Legal Entity authorized to submit on behalf of\n the copyright owner. For the purposes of this definition, \"submitted\"\n means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems,\n and issue tracking systems that are managed by, or on behalf of, the\n Licensor for the purpose of discussing and improving the Work, but\n excluding communication that is conspicuously marked or otherwise\n designated in writing by the copyright owner as \"Not a Contribution.\"\n\n \"Contributor\" shall mean Licensor and any individual or Legal Entity\n on behalf of whom a Contribution has been received by Licensor and\n subsequently incorporated within the Work.\n\n 2. Grant of Copyright License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n copyright license to reproduce, prepare Derivative Works of,\n publicly display, publicly perform, sublicense, and distribute the\n Work and such Derivative Works in Source or Object form.\n\n 3. Grant of Patent License. Subject to the terms and conditions of\n this License, each Contributor hereby grants to You a perpetual,\n worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n (except as stated in this section) patent license to make, have made,\n use, offer to sell, sell, import, and otherwise transfer the Work,\n where such license applies only to those patent claims licensable\n by such Contributor that are necessarily infringed by their\n Contribution(s) alone or by combination of their Contribution(s)\n with the Work to which such Contribution(s) was submitted. If You\n institute patent litigation against any entity (including a\n cross-claim or counterclaim in a lawsuit) alleging that the Work\n or a Contribution incorporated within the Work constitutes direct\n or contributory patent infringement, then any patent licenses\n granted to You under this License for that Work shall terminate\n as of the date such litigation is filed.\n\n 4. Redistribution. You may reproduce and distribute coLine truncated
|
||||
"name": "rxjs@7.8.1",
|
||||
"name": "rxjs@7.8.2",
|
||||
"licenses": "Apache-2.0",
|
||||
"repository": "https://github.com/reactivex/rxjs",
|
||||
"publisher": "Ben Lesh",
|
||||
@@ -418,8 +404,8 @@
|
||||
"publisher": "Microsoft Corp."
|
||||
},
|
||||
{
|
||||
"licenseText": "The MIT License\n\nCopyright (c) 2010-2024 Google LLC. https://angular.io/license\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n",
|
||||
"name": "zone.js@0.14.10",
|
||||
"licenseText": "The MIT License\n\nCopyright (c) 2010-2026 Google LLC. https://angular.dev/license\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n",
|
||||
"name": "zone.js@0.16.2",
|
||||
"licenses": "MIT",
|
||||
"repository": "https://github.com/angular/angular",
|
||||
"publisher": "Brian Ford"
|
||||
|
||||
@@ -63,6 +63,7 @@
|
||||
"default": {
|
||||
"logged_in_success": "Erfolgreich eingeloggt.",
|
||||
"logged_out_success": "Erfolgreich ausgeloggt.",
|
||||
"session_expired": "Deine Sitzung ist abgelaufen. Bitte melde dich erneut an.",
|
||||
"log_out_alert": {
|
||||
"header": "Komplett ausloggen",
|
||||
"message": "Du wurdest innerhalb der App ausgeloggt. Soll zudem die Browser Session beendet werden? Wähle im Zweifelsfall \"Ja\"."
|
||||
@@ -372,9 +373,9 @@
|
||||
"view": {
|
||||
"today": "Heute"
|
||||
},
|
||||
"recurring": "Wochen­ansicht",
|
||||
"recurring": "Wochen\u00ADansicht",
|
||||
"calendar": "Kalender",
|
||||
"single": "Einzel­termine",
|
||||
"single": "Einzel\u00ADtermine",
|
||||
"addEventPage": {
|
||||
"TITLE": "Termine Hinzufügen",
|
||||
"PLACEHOLDER": "Termine",
|
||||
@@ -404,7 +405,6 @@
|
||||
}
|
||||
},
|
||||
"userInfo": {
|
||||
"expired": "Abgelaufen",
|
||||
"studentId": "Matrikelnr.",
|
||||
"username": "Nutzername",
|
||||
"email": "Email",
|
||||
|
||||
@@ -63,6 +63,7 @@
|
||||
"default": {
|
||||
"logged_in_success": "Successfully logged in.",
|
||||
"logged_out_success": "Successfully logged out.",
|
||||
"session_expired": "Your session has expired. Please log in again.",
|
||||
"log_out_alert": {
|
||||
"header": "Complete logout",
|
||||
"message": "You are now logged out in your app. Do you want to log out from your identity provider as well? If unsure, please choose \"Yes\"."
|
||||
@@ -404,7 +405,6 @@
|
||||
}
|
||||
},
|
||||
"userInfo": {
|
||||
"expired": "expired",
|
||||
"studentId": "Matriculation Nr.",
|
||||
"username": "Username",
|
||||
"email": "Email",
|
||||
|
||||
@@ -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: '4.1.0',
|
||||
production: true,
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# @openstapps/app-builder-image
|
||||
|
||||
## 4.0.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- ae1cad3b: upgrade project to Typescript 5.9
|
||||
|
||||
## 4.0.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openstapps/app-builder-image",
|
||||
"version": "4.0.3",
|
||||
"version": "4.0.7",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "GPL-3.0-only",
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# @openstapps/app-cypress
|
||||
|
||||
## 4.0.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- ae1cad3b: upgrade project to Typescript 5.9
|
||||
|
||||
## 4.0.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openstapps/app-cypress",
|
||||
"version": "4.0.5",
|
||||
"version": "4.0.7",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "GPL-3.0-only",
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# @openstapps/node-base
|
||||
|
||||
## 4.0.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- ae1cad3b: upgrade project to Typescript 5.9
|
||||
|
||||
## 4.0.0
|
||||
|
||||
### Major Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openstapps/node-base",
|
||||
"version": "4.0.0",
|
||||
"version": "4.0.7",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "GPL-3.0-only",
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# @openstapps/node-builder
|
||||
|
||||
## 4.0.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- ae1cad3b: upgrade project to Typescript 5.9
|
||||
|
||||
## 4.0.0
|
||||
|
||||
### Major Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openstapps/node-builder",
|
||||
"version": "4.0.0",
|
||||
"version": "4.0.7",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "GPL-3.0-only",
|
||||
|
||||
@@ -1,5 +1,18 @@
|
||||
# @openstapps/api-cli
|
||||
|
||||
## 4.0.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- ae1cad3b: upgrade project to Typescript 5.9
|
||||
- 93c38b74: dependency updates
|
||||
- Updated dependencies [ae1cad3b]
|
||||
- Updated dependencies [93c38b74]
|
||||
- @openstapps/core-tools@4.0.7
|
||||
- @openstapps/core@4.0.7
|
||||
- @openstapps/api@4.0.7
|
||||
- @openstapps/logger@4.0.7
|
||||
|
||||
## 4.0.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@openstapps/api-cli",
|
||||
"description": "CLI client for @openstapps/api",
|
||||
"version": "4.0.5",
|
||||
"version": "4.0.7",
|
||||
"type": "module",
|
||||
"license": "GPL-3.0-only",
|
||||
"repository": "git@gitlab.com:openstapps/api.git",
|
||||
@@ -51,7 +51,7 @@
|
||||
"commander": "10.0.0",
|
||||
"fs-extra": "10.1.0",
|
||||
"junit-report-builder": "3.0.1",
|
||||
"wait-on": "6.0.1"
|
||||
"wait-on": "9.5.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@openstapps/eslint-config": "workspace:*",
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
# @openstapps/api-plugin
|
||||
|
||||
## 4.0.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- ae1cad3b: upgrade project to Typescript 5.9
|
||||
- Updated dependencies [ae1cad3b]
|
||||
- Updated dependencies [93c38b74]
|
||||
- @openstapps/core-tools@4.0.7
|
||||
- @openstapps/core@4.0.7
|
||||
- @openstapps/api@4.0.7
|
||||
- @openstapps/logger@4.0.7
|
||||
|
||||
## 4.0.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@openstapps/api-plugin",
|
||||
"description": "Node.js library to interact with the StApps backend service",
|
||||
"version": "4.0.5",
|
||||
"version": "4.0.7",
|
||||
"type": "module",
|
||||
"license": "GPL-3.0-only",
|
||||
"repository": "git@gitlab.com:openstapps/api.git",
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
# @openstapps/api
|
||||
|
||||
## 4.0.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- ae1cad3b: upgrade project to Typescript 5.9
|
||||
- 93c38b74: dependency updates
|
||||
- Updated dependencies [ae1cad3b]
|
||||
- @openstapps/core@4.0.7
|
||||
|
||||
## 4.0.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@openstapps/api",
|
||||
"description": "Node.js library to interact with the StApps backend service",
|
||||
"version": "4.0.5",
|
||||
"version": "4.0.7",
|
||||
"type": "module",
|
||||
"license": "GPL-3.0-only",
|
||||
"repository": "git@gitlab.com:openstapps/api.git",
|
||||
@@ -58,7 +58,7 @@
|
||||
"tsup": "8.5.0",
|
||||
"typedoc": "0.25.12",
|
||||
"typescript": "5.9.3",
|
||||
"undici": "6.21.3"
|
||||
"undici": "6.29.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@openstapps/core": "workspace:*"
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# @openstapps/collection-utils
|
||||
|
||||
## 4.0.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- ae1cad3b: upgrade project to Typescript 5.9
|
||||
|
||||
## 4.0.0
|
||||
|
||||
### Major Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@openstapps/collection-utils",
|
||||
"version": "4.0.0",
|
||||
"version": "4.0.7",
|
||||
"type": "module",
|
||||
"license": "GPL-3.0-only",
|
||||
"main": "lib/index.js",
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
# @openstapps/core-tools
|
||||
|
||||
## 4.0.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- ae1cad3b: upgrade project to Typescript 5.9
|
||||
- 93c38b74: dependency updates
|
||||
- Updated dependencies [ae1cad3b]
|
||||
- Updated dependencies [93c38b74]
|
||||
- @openstapps/easy-ast@4.0.7
|
||||
- @openstapps/collection-utils@4.0.7
|
||||
- @openstapps/logger@4.0.7
|
||||
|
||||
## 4.0.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@openstapps/core-tools",
|
||||
"description": "Tools to convert and validate StAppsCore",
|
||||
"version": "4.0.5",
|
||||
"version": "4.0.7",
|
||||
"type": "module",
|
||||
"license": "GPL-3.0-only",
|
||||
"repository": "git@gitlab.com:openstapps/core-tools.git",
|
||||
@@ -52,19 +52,19 @@
|
||||
"better-ajv-errors": "1.2.0",
|
||||
"commander": "10.0.0",
|
||||
"deepmerge": "4.3.1",
|
||||
"del": "6.1.1",
|
||||
"flatted": "3.2.7",
|
||||
"del": "9.0.1",
|
||||
"flatted": "3.4.4",
|
||||
"fs-extra": "10.1.0",
|
||||
"glob": "10.5.0",
|
||||
"got": "12.6.0",
|
||||
"got": "14.6.6",
|
||||
"humanize-string": "3.0.0",
|
||||
"json-schema": "0.4.0",
|
||||
"mustache": "4.2.0",
|
||||
"openapi-types": "12.1.0",
|
||||
"plantuml-encoder": "1.4.0",
|
||||
"re2": "1.21.0",
|
||||
"re2": "1.27.0",
|
||||
"toposort": "2.0.2",
|
||||
"ts-json-schema-generator": "1.2.0"
|
||||
"ts-json-schema-generator": "1.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@openstapps/eslint-config": "workspace:*",
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
# @openstapps/core
|
||||
|
||||
## 4.0.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- ae1cad3b: upgrade project to Typescript 5.9
|
||||
- Updated dependencies [ae1cad3b]
|
||||
- Updated dependencies [93c38b74]
|
||||
- @openstapps/core-tools@4.0.7
|
||||
|
||||
## 4.0.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@openstapps/core",
|
||||
"description": "StAppsCore - Generalized model of data",
|
||||
"version": "4.0.5",
|
||||
"version": "4.0.7",
|
||||
"type": "module",
|
||||
"license": "GPL-3.0-only",
|
||||
"repository": "git@gitlab.com:openstapps/core.git",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {defineConfig} from 'tsup';
|
||||
import packageJson from './package.json' assert {type: 'json'};
|
||||
import packageJson from './package.json' with {type: 'json'};
|
||||
|
||||
export default defineConfig({
|
||||
entry: ['src/index.ts'],
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
# @openstapps/easy-ast
|
||||
|
||||
## 4.0.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- ae1cad3b: upgrade project to Typescript 5.9
|
||||
- Updated dependencies [ae1cad3b]
|
||||
- Updated dependencies [93c38b74]
|
||||
- @openstapps/collection-utils@4.0.7
|
||||
- @openstapps/logger@4.0.7
|
||||
|
||||
## 4.0.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@openstapps/easy-ast",
|
||||
"description": "Tool to easily handle TypeScript AST",
|
||||
"version": "4.0.5",
|
||||
"version": "4.0.7",
|
||||
"type": "module",
|
||||
"license": "GPL-3.0-only",
|
||||
"repository": "git@gitlab.com:openstapps/core-tools.git",
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
# @openstapps/es-mapping-generator
|
||||
|
||||
## 4.0.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- ae1cad3b: upgrade project to Typescript 5.9
|
||||
- 93c38b74: dependency updates
|
||||
|
||||
## 4.0.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@openstapps/es-mapping-generator",
|
||||
"description": "Tool to convert TypeScript Interfaces to Elasticsearch Mappings",
|
||||
"version": "4.0.1",
|
||||
"version": "4.0.7",
|
||||
"type": "commonjs",
|
||||
"license": "GPL-3.0-only",
|
||||
"author": "Thea Schöbl <dev@theaninova.de>",
|
||||
@@ -29,7 +29,7 @@
|
||||
"@elastic/elasticsearch": "8.14.1",
|
||||
"commander": "10.0.0",
|
||||
"deepmerge": "4.3.1",
|
||||
"flatted": "3.2.7",
|
||||
"flatted": "3.4.4",
|
||||
"typedoc": "0.18.0",
|
||||
"typescript": "3.8.3"
|
||||
},
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
# @openstapps/gitlab-api
|
||||
|
||||
## 4.0.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- ae1cad3b: upgrade project to Typescript 5.9
|
||||
- 93c38b74: dependency updates
|
||||
- Updated dependencies [ae1cad3b]
|
||||
- Updated dependencies [93c38b74]
|
||||
- @openstapps/logger@4.0.7
|
||||
|
||||
## 4.0.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@openstapps/gitlab-api",
|
||||
"description": "Wraps common GitLab API calls in a TypeScript class.",
|
||||
"version": "4.0.2",
|
||||
"version": "4.0.7",
|
||||
"type": "module",
|
||||
"license": "GPL-3.0-only",
|
||||
"repository": "git@gitlab.com:openstapps/gitlab-api.git",
|
||||
@@ -31,7 +31,7 @@
|
||||
"dependencies": {
|
||||
"@openstapps/logger": "workspace:*",
|
||||
"commander": "10.0.0",
|
||||
"got": "12.6.0"
|
||||
"got": "14.6.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@openstapps/eslint-config": "workspace:*",
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
# @openstapps/logger
|
||||
|
||||
## 4.0.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- ae1cad3b: upgrade project to Typescript 5.9
|
||||
- 93c38b74: dependency updates
|
||||
|
||||
## 4.0.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
Loaded 100 of 104 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user