diff --git a/.eslintrc.json b/.eslintrc.json index 7e52aa7a..27b197f2 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -41,7 +41,7 @@ "unicorn/no-array-callback-reference": "off", "unicorn/prefer-object-from-entries": "off", "unicorn/prevent-abbreviations": [ - "error", + "warn", { "replacements": { "ref": false, diff --git a/android/app/build.gradle b/android/app/build.gradle index 9092d4e7..5b5b52bf 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -3,7 +3,7 @@ apply plugin: 'com.android.application' android { compileSdkVersion rootProject.ext.compileSdkVersion defaultConfig { - applicationId "de.any_school.app" + applicationId "de.anyschool.app" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion versionCode 1 diff --git a/android/app/capacitor.build.gradle b/android/app/capacitor.build.gradle index 488af28e..9f7cc30b 100644 --- a/android/app/capacitor.build.gradle +++ b/android/app/capacitor.build.gradle @@ -9,12 +9,15 @@ android { apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle" dependencies { + implementation project(':capacitor-community-http') implementation project(':capacitor-app') implementation project(':capacitor-browser') implementation project(':capacitor-haptics') implementation project(':capacitor-keyboard') implementation project(':capacitor-splash-screen') implementation project(':capacitor-status-bar') + implementation project(':capacitor-storage') + implementation project(':capacitor-secure-storage-plugin') implementation "androidx.legacy:legacy-support-v4:1.0.0" implementation "androidx.appcompat:appcompat:1.3.1" } diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 00dd10b5..9316f3e9 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,49 +1,27 @@ - - - - - - - + + + + - - - - - - - + + + + + + + + + + + - - - - + + - - - diff --git a/android/app/src/main/assets/capacitor.config.json b/android/app/src/main/assets/capacitor.config.json index 62c7a3c4..eaf6c883 100644 --- a/android/app/src/main/assets/capacitor.config.json +++ b/android/app/src/main/assets/capacitor.config.json @@ -1,5 +1,5 @@ { - "appId": "de.any_school.app", + "appId": "de.anyschool.app", "appName": "StApps", "webDir": "www", "bundledWebRuntime": false, diff --git a/android/app/src/main/assets/capacitor.plugins.json b/android/app/src/main/assets/capacitor.plugins.json index 51bdacac..27de66f3 100644 --- a/android/app/src/main/assets/capacitor.plugins.json +++ b/android/app/src/main/assets/capacitor.plugins.json @@ -1,4 +1,8 @@ [ + { + "pkg": "@capacitor-community/http", + "classpath": "com.getcapacitor.plugin.http.Http" + }, { "pkg": "@capacitor/app", "classpath": "com.capacitorjs.plugins.app.AppPlugin" @@ -22,5 +26,13 @@ { "pkg": "@capacitor/status-bar", "classpath": "com.capacitorjs.plugins.statusbar.StatusBarPlugin" + }, + { + "pkg": "@capacitor/storage", + "classpath": "com.capacitorjs.plugins.storage.StoragePlugin" + }, + { + "pkg": "capacitor-secure-storage-plugin", + "classpath": "com.whitestein.securestorage.SecureStoragePlugin" } ] diff --git a/android/app/src/main/java/de/any_school/app/MainActivity.java b/android/app/src/main/java/de/anyschool/app/MainActivity.java similarity index 77% rename from android/app/src/main/java/de/any_school/app/MainActivity.java rename to android/app/src/main/java/de/anyschool/app/MainActivity.java index 5275481d..358781e4 100644 --- a/android/app/src/main/java/de/any_school/app/MainActivity.java +++ b/android/app/src/main/java/de/anyschool/app/MainActivity.java @@ -1,4 +1,4 @@ -package de.any_school.app; +package de.anyschool.app; import com.getcapacitor.BridgeActivity; diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index 6a4c038e..8948a9a6 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -2,6 +2,6 @@ StApps StApps - de.any_school.app - de.any_school.app + de.anyschool.app + de.anyschool.app diff --git a/android/build.gradle b/android/build.gradle index ad085c34..fede5697 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,13 +1,13 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { - + repositories { google() jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:4.2.1' + classpath 'com.android.tools.build:gradle:7.0.3' classpath 'com.google.gms:google-services:4.3.5' // NOTE: Do not place your application dependencies here; they belong diff --git a/android/capacitor.settings.gradle b/android/capacitor.settings.gradle index a6873f0b..e1789033 100644 --- a/android/capacitor.settings.gradle +++ b/android/capacitor.settings.gradle @@ -2,6 +2,9 @@ include ':capacitor-android' project(':capacitor-android').projectDir = new File('../node_modules/@capacitor/android/capacitor') +include ':capacitor-community-http' +project(':capacitor-community-http').projectDir = new File('../node_modules/@capacitor-community/http/android') + include ':capacitor-app' project(':capacitor-app').projectDir = new File('../node_modules/@capacitor/app/android') @@ -19,3 +22,9 @@ project(':capacitor-splash-screen').projectDir = new File('../node_modules/@capa include ':capacitor-status-bar' project(':capacitor-status-bar').projectDir = new File('../node_modules/@capacitor/status-bar/android') + +include ':capacitor-storage' +project(':capacitor-storage').projectDir = new File('../node_modules/@capacitor/storage/android') + +include ':capacitor-secure-storage-plugin' +project(':capacitor-secure-storage-plugin').projectDir = new File('../node_modules/capacitor-secure-storage-plugin/android') diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index 3c4101c3..29e41345 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.0-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/capacitor.config.ts b/capacitor.config.ts index 1a583025..9d56b97a 100644 --- a/capacitor.config.ts +++ b/capacitor.config.ts @@ -1,7 +1,7 @@ import {CapacitorConfig} from '@capacitor/cli'; const config: CapacitorConfig = { - appId: 'de.any_school.app', + appId: 'de.anyschool.app', appName: 'StApps', webDir: 'www', bundledWebRuntime: false, diff --git a/config.xml b/config.xml index b480e49e..932a7bff 100644 --- a/config.xml +++ b/config.xml @@ -1,5 +1,5 @@ - + StApps An awesome Ionic/Cordova app. Ionic Framework Team @@ -21,10 +21,6 @@ - - - - diff --git a/ios/.gitignore b/ios/.gitignore new file mode 100644 index 00000000..75e8c5ae --- /dev/null +++ b/ios/.gitignore @@ -0,0 +1,9 @@ +App/build +App/Pods +App/Podfile.lock +App/App/public +DerivedData +xcuserdata + +# Cordova plugins for Capacitor +capacitor-cordova-ios-plugins diff --git a/ios/App/App.xcodeproj/project.pbxproj b/ios/App/App.xcodeproj/project.pbxproj new file mode 100644 index 00000000..3d2cf369 --- /dev/null +++ b/ios/App/App.xcodeproj/project.pbxproj @@ -0,0 +1,428 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 48; + objects = { + +/* Begin PBXBuildFile section */ + 2FAD9763203C412B000D30F8 /* config.xml in Resources */ = {isa = PBXBuildFile; fileRef = 2FAD9762203C412B000D30F8 /* config.xml */; }; + 50379B232058CBB4000EE86E /* capacitor.config.json in Resources */ = {isa = PBXBuildFile; fileRef = 50379B222058CBB4000EE86E /* capacitor.config.json */; }; + 504EC3081FED79650016851F /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504EC3071FED79650016851F /* AppDelegate.swift */; }; + 504EC30D1FED79650016851F /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 504EC30B1FED79650016851F /* Main.storyboard */; }; + 504EC30F1FED79650016851F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 504EC30E1FED79650016851F /* Assets.xcassets */; }; + 504EC3121FED79650016851F /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 504EC3101FED79650016851F /* LaunchScreen.storyboard */; }; + 50B271D11FEDC1A000F3C39B /* public in Resources */ = {isa = PBXBuildFile; fileRef = 50B271D01FEDC1A000F3C39B /* public */; }; + A084ECDBA7D38E1E42DFC39D /* Pods_App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AF277DCFFFF123FFC6DF26C7 /* Pods_App.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 2FAD9762203C412B000D30F8 /* config.xml */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = config.xml; sourceTree = ""; }; + 50379B222058CBB4000EE86E /* capacitor.config.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = capacitor.config.json; sourceTree = ""; }; + 504EC3041FED79650016851F /* App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = App.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 504EC3071FED79650016851F /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 504EC30C1FED79650016851F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 504EC30E1FED79650016851F /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 504EC3111FED79650016851F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 504EC3131FED79650016851F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 50B271D01FEDC1A000F3C39B /* public */ = {isa = PBXFileReference; lastKnownFileType = folder; path = public; sourceTree = ""; }; + AF277DCFFFF123FFC6DF26C7 /* Pods_App.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_App.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + AF51FD2D460BCFE21FA515B2 /* Pods-App.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App.release.xcconfig"; path = "Pods/Target Support Files/Pods-App/Pods-App.release.xcconfig"; sourceTree = ""; }; + E2D249FB277CB255005492AC /* App.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = App.entitlements; sourceTree = ""; }; + FC68EB0AF532CFC21C3344DD /* Pods-App.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App.debug.xcconfig"; path = "Pods/Target Support Files/Pods-App/Pods-App.debug.xcconfig"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 504EC3011FED79650016851F /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + A084ECDBA7D38E1E42DFC39D /* Pods_App.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 27E2DDA53C4D2A4D1A88CE4A /* Frameworks */ = { + isa = PBXGroup; + children = ( + AF277DCFFFF123FFC6DF26C7 /* Pods_App.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 504EC2FB1FED79650016851F = { + isa = PBXGroup; + children = ( + 504EC3061FED79650016851F /* App */, + 504EC3051FED79650016851F /* Products */, + 7F8756D8B27F46E3366F6CEA /* Pods */, + 27E2DDA53C4D2A4D1A88CE4A /* Frameworks */, + ); + sourceTree = ""; + }; + 504EC3051FED79650016851F /* Products */ = { + isa = PBXGroup; + children = ( + 504EC3041FED79650016851F /* App.app */, + ); + name = Products; + sourceTree = ""; + }; + 504EC3061FED79650016851F /* App */ = { + isa = PBXGroup; + children = ( + E2D249FB277CB255005492AC /* App.entitlements */, + 50379B222058CBB4000EE86E /* capacitor.config.json */, + 504EC3071FED79650016851F /* AppDelegate.swift */, + 504EC30B1FED79650016851F /* Main.storyboard */, + 504EC30E1FED79650016851F /* Assets.xcassets */, + 504EC3101FED79650016851F /* LaunchScreen.storyboard */, + 504EC3131FED79650016851F /* Info.plist */, + 2FAD9762203C412B000D30F8 /* config.xml */, + 50B271D01FEDC1A000F3C39B /* public */, + ); + path = App; + sourceTree = ""; + }; + 7F8756D8B27F46E3366F6CEA /* Pods */ = { + isa = PBXGroup; + children = ( + FC68EB0AF532CFC21C3344DD /* Pods-App.debug.xcconfig */, + AF51FD2D460BCFE21FA515B2 /* Pods-App.release.xcconfig */, + ); + name = Pods; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 504EC3031FED79650016851F /* App */ = { + isa = PBXNativeTarget; + buildConfigurationList = 504EC3161FED79650016851F /* Build configuration list for PBXNativeTarget "App" */; + buildPhases = ( + 6634F4EFEBD30273BCE97C65 /* [CP] Check Pods Manifest.lock */, + 504EC3001FED79650016851F /* Sources */, + 504EC3011FED79650016851F /* Frameworks */, + 504EC3021FED79650016851F /* Resources */, + 9592DBEFFC6D2A0C8D5DEB22 /* [CP] Embed Pods Frameworks */, + 0C3780443725062B779B937E /* [CP] Copy Pods Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = App; + productName = App; + productReference = 504EC3041FED79650016851F /* App.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 504EC2FC1FED79650016851F /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 0920; + LastUpgradeCheck = 0920; + TargetAttributes = { + 504EC3031FED79650016851F = { + CreatedOnToolsVersion = 9.2; + LastSwiftMigration = 1100; + ProvisioningStyle = Automatic; + }; + }; + }; + buildConfigurationList = 504EC2FF1FED79650016851F /* Build configuration list for PBXProject "App" */; + compatibilityVersion = "Xcode 8.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 504EC2FB1FED79650016851F; + productRefGroup = 504EC3051FED79650016851F /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 504EC3031FED79650016851F /* App */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 504EC3021FED79650016851F /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 504EC3121FED79650016851F /* LaunchScreen.storyboard in Resources */, + 50B271D11FEDC1A000F3C39B /* public in Resources */, + 504EC30F1FED79650016851F /* Assets.xcassets in Resources */, + 50379B232058CBB4000EE86E /* capacitor.config.json in Resources */, + 504EC30D1FED79650016851F /* Main.storyboard in Resources */, + 2FAD9763203C412B000D30F8 /* config.xml in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 0C3780443725062B779B937E /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "[CP] Copy Pods Resources"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-App/Pods-App-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; + 6634F4EFEBD30273BCE97C65 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-App-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 9592DBEFFC6D2A0C8D5DEB22 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "[CP] Embed Pods Frameworks"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-App/Pods-App-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 504EC3001FED79650016851F /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 504EC3081FED79650016851F /* AppDelegate.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 504EC30B1FED79650016851F /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 504EC30C1FED79650016851F /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 504EC3101FED79650016851F /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 504EC3111FED79650016851F /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 504EC3141FED79650016851F /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 504EC3151FED79650016851F /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 504EC3171FED79650016851F /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = FC68EB0AF532CFC21C3344DD /* Pods-App.debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_ENTITLEMENTS = App/App.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = 8C99SX84P9; + INFOPLIST_FILE = App/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\""; + PRODUCT_BUNDLE_IDENTIFIER = de.anyschool.app; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 504EC3181FED79650016851F /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = AF51FD2D460BCFE21FA515B2 /* Pods-App.release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_ENTITLEMENTS = App/App.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = 8C99SX84P9; + INFOPLIST_FILE = App/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = de.anyschool.app; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = ""; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 504EC2FF1FED79650016851F /* Build configuration list for PBXProject "App" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 504EC3141FED79650016851F /* Debug */, + 504EC3151FED79650016851F /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 504EC3161FED79650016851F /* Build configuration list for PBXNativeTarget "App" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 504EC3171FED79650016851F /* Debug */, + 504EC3181FED79650016851F /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 504EC2FC1FED79650016851F /* Project object */; +} diff --git a/ios/App/App.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/ios/App/App.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..42daef8a --- /dev/null +++ b/ios/App/App.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/ios/App/App.xcodeproj/xcshareddata/xcschemes/App.xcscheme b/ios/App/App.xcodeproj/xcshareddata/xcschemes/App.xcscheme new file mode 100644 index 00000000..fa7d6f4b --- /dev/null +++ b/ios/App/App.xcodeproj/xcshareddata/xcschemes/App.xcscheme @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/App/App.xcworkspace/contents.xcworkspacedata b/ios/App/App.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..b301e824 --- /dev/null +++ b/ios/App/App.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/ios/App/App.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ios/App/App.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/ios/App/App.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/ios/App/App/App.entitlements b/ios/App/App/App.entitlements new file mode 100644 index 00000000..9f7be42c --- /dev/null +++ b/ios/App/App/App.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.developer.associated-domains + + applinks:mobile.app.uni-frankfurt.de + + + diff --git a/ios/App/App/AppDelegate.swift b/ios/App/App/AppDelegate.swift new file mode 100644 index 00000000..53e37603 --- /dev/null +++ b/ios/App/App/AppDelegate.swift @@ -0,0 +1,60 @@ +import UIKit +import Capacitor + +@UIApplicationMain +class AppDelegate: UIResponder, UIApplicationDelegate { + + var window: UIWindow? + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + // Override point for customization after application launch. + return true + } + + func applicationWillResignActive(_ application: UIApplication) { + // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. + // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. + } + + func applicationDidEnterBackground(_ application: UIApplication) { + // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. + // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. + } + + func applicationWillEnterForeground(_ application: UIApplication) { + // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. + } + + func applicationDidBecomeActive(_ application: UIApplication) { + // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. + } + + func applicationWillTerminate(_ application: UIApplication) { + // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. + } + + func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool { + // Called when the app was launched with a url. Feel free to add additional processing here, + // but if you want the App API to support tracking app url opens, make sure to keep this call + return ApplicationDelegateProxy.shared.application(app, open: url, options: options) + } + + func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool { + // Called when the app was launched with an activity, including Universal Links. + // Feel free to add additional processing here, but if you want the App API to support + // tracking app url opens, make sure to keep this call + return ApplicationDelegateProxy.shared.application(application, continue: userActivity, restorationHandler: restorationHandler) + } + + override func touchesBegan(_ touches: Set, with event: UIEvent?) { + super.touchesBegan(touches, with: event) + + let statusBarRect = UIApplication.shared.statusBarFrame + guard let touchPoint = event?.allTouches?.first?.location(in: self.window) else { return } + + if statusBarRect.contains(touchPoint) { + NotificationCenter.default.post(name: .capacitorStatusBarTapped, object: nil) + } + } + +} diff --git a/ios/App/App/Assets.xcassets/AppIcon.appiconset/Contents.json b/ios/App/App/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..90eea7ec --- /dev/null +++ b/ios/App/App/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,116 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "AppIcon-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "AppIcon-20x20@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "AppIcon-29x29@2x-1.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "AppIcon-29x29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "AppIcon-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "AppIcon-40x40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "AppIcon-60x60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "AppIcon-60x60@3x.png", + "scale" : "3x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "AppIcon-20x20@1x.png", + "scale" : "1x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "AppIcon-20x20@2x-1.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "AppIcon-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "AppIcon-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "AppIcon-40x40@1x.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "AppIcon-40x40@2x-1.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "AppIcon-76x76@1x.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "AppIcon-76x76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "AppIcon-83.5x83.5@2x.png", + "scale" : "2x" + }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "AppIcon-512@2x.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/ios/App/App/Assets.xcassets/Contents.json b/ios/App/App/Assets.xcassets/Contents.json new file mode 100644 index 00000000..da4a164c --- /dev/null +++ b/ios/App/App/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/ios/App/App/Assets.xcassets/Splash.imageset/Contents.json b/ios/App/App/Assets.xcassets/Splash.imageset/Contents.json new file mode 100644 index 00000000..d7d96a67 --- /dev/null +++ b/ios/App/App/Assets.xcassets/Splash.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "splash-2732x2732-2.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "splash-2732x2732-1.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "splash-2732x2732.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/ios/App/App/Base.lproj/LaunchScreen.storyboard b/ios/App/App/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 00000000..e7ae5d78 --- /dev/null +++ b/ios/App/App/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/App/App/Base.lproj/Main.storyboard b/ios/App/App/Base.lproj/Main.storyboard new file mode 100644 index 00000000..b44df7be --- /dev/null +++ b/ios/App/App/Base.lproj/Main.storyboard @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/ios/App/App/Info.plist b/ios/App/App/Info.plist new file mode 100644 index 00000000..240b40ca --- /dev/null +++ b/ios/App/App/Info.plist @@ -0,0 +1,68 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleDisplayName + StApps + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleURLTypes + + + CFBundleTypeRole + Editor + CFBundleURLName + de.anyschool.app + CFBundleURLSchemes + + de.anyschool.app + + + + + CFBundleVersion + 1 + LSRequiresIPhoneOS + + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + + diff --git a/ios/App/App/capacitor.config.json b/ios/App/App/capacitor.config.json new file mode 100644 index 00000000..eaf6c883 --- /dev/null +++ b/ios/App/App/capacitor.config.json @@ -0,0 +1,19 @@ +{ + "appId": "de.anyschool.app", + "appName": "StApps", + "webDir": "www", + "bundledWebRuntime": false, + "cordova": { + "preferences": { + "AndroidXEnabled": "true", + "ScrollEnabled": "false", + "android-minSdkVersion": "22", + "BackupWebStorage": "none", + "SplashMaintainAspectRatio": "true", + "FadeSplashScreenDuration": "300", + "SplashShowOnlyFirstTime": "false", + "SplashScreen": "screen", + "SplashScreenDelay": "3000" + } + } +} diff --git a/ios/App/App/config.xml b/ios/App/App/config.xml new file mode 100644 index 00000000..404a2494 --- /dev/null +++ b/ios/App/App/config.xml @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ios/App/Podfile b/ios/App/Podfile new file mode 100644 index 00000000..679c990c --- /dev/null +++ b/ios/App/Podfile @@ -0,0 +1,28 @@ +platform :ios, '12.0' +use_frameworks! + +# workaround to avoid Xcode caching of Pods that requires +# Product -> Clean Build Folder after new Cordova plugins installed +# Requires CocoaPods 1.6 or newer +install! 'cocoapods', :disable_input_output_paths => true + +def capacitor_pods + pod 'Capacitor', :path => '../../node_modules/@capacitor/ios' + pod 'CapacitorCordova', :path => '../../node_modules/@capacitor/ios' + pod 'CapacitorCommunityHttp', :path => '../../node_modules/@capacitor-community/http' + pod 'CapacitorApp', :path => '../../node_modules/@capacitor/app' + pod 'CapacitorBrowser', :path => '../../node_modules/@capacitor/browser' + pod 'CapacitorHaptics', :path => '../../node_modules/@capacitor/haptics' + pod 'CapacitorKeyboard', :path => '../../node_modules/@capacitor/keyboard' + pod 'CapacitorSplashScreen', :path => '../../node_modules/@capacitor/splash-screen' + pod 'CapacitorStatusBar', :path => '../../node_modules/@capacitor/status-bar' + pod 'CapacitorStorage', :path => '../../node_modules/@capacitor/storage' + pod 'CapacitorSecureStoragePlugin', :path => '../../node_modules/capacitor-secure-storage-plugin' + pod 'CordovaPlugins', :path => '../capacitor-cordova-ios-plugins' + pod 'CordovaPluginsResources', :path => '../capacitor-cordova-ios-plugins' +end + +target 'App' do + capacitor_pods + # Add your Pods here +end diff --git a/package-lock.json b/package-lock.json index a8501571..f24e62f0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2067,11 +2067,21 @@ "to-fast-properties": "^2.0.0" } }, + "@capacitor-community/http": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@capacitor-community/http/-/http-1.4.1.tgz", + "integrity": "sha512-+pCkBXrwfm97UfjOgjV950H/qZ8SE36Mrcb46BlL1ps3VIsGuIO+AulL8GqTC6LewheRVtGJpRspNtneXQotNA==", + "requires": { + "@capacitor/android": "^3.0.0", + "@capacitor/core": "^3.0.0", + "@capacitor/filesystem": "^1.0.0", + "@capacitor/ios": "^3.0.0" + } + }, "@capacitor/android": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/@capacitor/android/-/android-3.3.2.tgz", - "integrity": "sha512-TG+tGz0KxkT/BgvSLQfbQwQ9c4Budub5TRijIGdmMbB1ZYB76TFhwvVuwWZ52HFSlKS3sx/UYLlbULL7UQ2aug==", - "dev": true + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/@capacitor/android/-/android-3.4.0.tgz", + "integrity": "sha512-O2hHGVzdTH2Lsmz58EI8zHY5byEFIWl9KrW60WSrO/tV6u9DpfaUq56FaejvfU27GFXDZkmoQNa33EvDYWp4wA==" }, "@capacitor/app": { "version": "1.0.6", @@ -2152,6 +2162,11 @@ } } }, + "@capacitor/filesystem": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@capacitor/filesystem/-/filesystem-1.1.0.tgz", + "integrity": "sha512-8O3UuvL8HNUEJvZnmn8yUmvgB1evtXfcF0oxIo3YbSlylqywJwS3JTiuhKmsvSxCdpbTy8IaTsutVh3gZgWbKg==" + }, "@capacitor/haptics": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@capacitor/haptics/-/haptics-1.1.3.tgz", @@ -2160,8 +2175,7 @@ "@capacitor/ios": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/@capacitor/ios/-/ios-3.3.2.tgz", - "integrity": "sha512-qXbWo9zTtinIYDxKsLdkXXuZD8H+wlJII3+ZF9QzH+38IYyd+sohOG3NLC8EaX7GRtwCymd+mGotoN420SfQ4Q==", - "dev": true + "integrity": "sha512-qXbWo9zTtinIYDxKsLdkXXuZD8H+wlJII3+ZF9QzH+38IYyd+sohOG3NLC8EaX7GRtwCymd+mGotoN420SfQ4Q==" }, "@capacitor/keyboard": { "version": "1.1.3", @@ -2178,6 +2192,11 @@ "resolved": "https://registry.npmjs.org/@capacitor/status-bar/-/status-bar-1.0.6.tgz", "integrity": "sha512-5MGWFq76iiKvHpbZ/Xc0Zig3WZyzWZ62wvC4qxak8OuVHBNG4fA1p/XXY9teQPaU3SupEJHnLkw6Gn1LuDp+ew==" }, + "@capacitor/storage": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@capacitor/storage/-/storage-1.2.3.tgz", + "integrity": "sha512-Rc5CKS53sfxokF5dxzNQDhig4lnZonky6VqskHZKTe3Ltl37FKmrG+I8ttZCinFZ5MPWfGSuP44m93hsQqitjQ==" + }, "@compodoc/compodoc": { "version": "1.1.14", "resolved": "https://registry.npmjs.org/@compodoc/compodoc/-/compodoc-1.1.14.tgz", @@ -2532,6 +2551,40 @@ "@types/cordova": "^0.0.34" } }, + "@ionic-native/http": { + "version": "5.35.0", + "resolved": "https://registry.npmjs.org/@ionic-native/http/-/http-5.35.0.tgz", + "integrity": "sha512-AaLvi59gdOlxpM3x9krk0l8UcLI9OLBm+o7DNu8y1pgsUKIvf7+ZbY4xU1fJG1HDvUyrU8+NQLrvXgpXx6CmAQ==", + "optional": true, + "requires": { + "@types/cordova": "^0.0.34" + }, + "dependencies": { + "@types/cordova": { + "version": "0.0.34", + "resolved": "https://registry.npmjs.org/@types/cordova/-/cordova-0.0.34.tgz", + "integrity": "sha1-6nrd907Ow9dimCegw54smt3HPQQ=", + "optional": true + } + } + }, + "@ionic-native/in-app-browser": { + "version": "5.36.0", + "resolved": "https://registry.npmjs.org/@ionic-native/in-app-browser/-/in-app-browser-5.36.0.tgz", + "integrity": "sha512-tX/FBT0jpkgEefZ8iorv5eDKfgP/ExbYr1AWg6okORQ0dwLfXsD5KDJgKHN9GFZvyuLNeaLpC1mN7CvwvLvmgA==", + "optional": true, + "requires": { + "@types/cordova": "^0.0.34" + }, + "dependencies": { + "@types/cordova": { + "version": "0.0.34", + "resolved": "https://registry.npmjs.org/@types/cordova/-/cordova-0.0.34.tgz", + "integrity": "sha1-6nrd907Ow9dimCegw54smt3HPQQ=", + "optional": true + } + } + }, "@ionic-native/network": { "version": "5.36.0", "resolved": "https://registry.npmjs.org/@ionic-native/network/-/network-5.36.0.tgz", @@ -2540,6 +2593,40 @@ "@types/cordova": "^0.0.34" } }, + "@ionic-native/safari-view-controller": { + "version": "5.36.0", + "resolved": "https://registry.npmjs.org/@ionic-native/safari-view-controller/-/safari-view-controller-5.36.0.tgz", + "integrity": "sha512-pvqnzro3bBZ0bQOMjBRKhmjHDaLKfDS75QY7uqe9UzjufMnHtBUUWgMvTuL7MsjTXRj8iRhe1wnUv8aBkz4SVA==", + "optional": true, + "requires": { + "@types/cordova": "^0.0.34" + }, + "dependencies": { + "@types/cordova": { + "version": "0.0.34", + "resolved": "https://registry.npmjs.org/@types/cordova/-/cordova-0.0.34.tgz", + "integrity": "sha1-6nrd907Ow9dimCegw54smt3HPQQ=", + "optional": true + } + } + }, + "@ionic-native/secure-storage": { + "version": "5.35.0", + "resolved": "https://registry.npmjs.org/@ionic-native/secure-storage/-/secure-storage-5.35.0.tgz", + "integrity": "sha512-QJSMGsvYOFWX95zMbCMMfNdtOnx9su6GKPmw6p+lLYWTXa5bqqRMSRtPInQmv1dz6GCcAMdXbUGADjBgwQQsaA==", + "optional": true, + "requires": { + "@types/cordova": "^0.0.34" + }, + "dependencies": { + "@types/cordova": { + "version": "0.0.34", + "resolved": "https://registry.npmjs.org/@types/cordova/-/cordova-0.0.34.tgz", + "integrity": "sha1-6nrd907Ow9dimCegw54smt3HPQQ=", + "optional": true + } + } + }, "@ionic/angular": { "version": "5.7.0", "resolved": "https://registry.npmjs.org/@ionic/angular/-/angular-5.7.0.tgz", @@ -3216,6 +3303,31 @@ } } }, + "@openid/appauth": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@openid/appauth/-/appauth-1.3.1.tgz", + "integrity": "sha512-e54kpi219wES2ijPzeHe1kMnT8VKH8YeTd1GAn9BzVBmutz3tBgcG1y8a4pziNr4vNjFnuD4W446Ua7ELnNDiA==", + "requires": { + "@types/base64-js": "^1.3.0", + "@types/jquery": "^3.5.5", + "base64-js": "^1.5.1", + "follow-redirects": "^1.13.3", + "form-data": "^4.0.0", + "opener": "^1.5.2" + }, + "dependencies": { + "form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + } + } + }, "@openstapps/api": { "version": "0.35.0", "resolved": "https://registry.npmjs.org/@openstapps/api/-/api-0.35.0.tgz", @@ -3504,6 +3616,11 @@ "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.2.tgz", "integrity": "sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA==" }, + "@types/base64-js": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@types/base64-js/-/base64-js-1.3.0.tgz", + "integrity": "sha512-ZmI0sZGAUNXUfMWboWwi4LcfpoVUYldyN6Oe0oJ5cCsHDU/LlRq8nQKPXhYLOx36QYSW9bNIb1vvRrD6K7Llgw==" + }, "@types/body-parser": { "version": "1.19.2", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", @@ -3698,6 +3815,14 @@ "@types/jasmine": "*" } }, + "@types/jquery": { + "version": "3.5.8", + "resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.8.tgz", + "integrity": "sha512-cXk6NwqjDYg+UI9p2l3x0YmPa4m7RrXqmbK4IpVVpRJiYXU/QTo+UZrn54qfE1+9Gao4qpYqUnxm5ZCy2FTXAw==", + "requires": { + "@types/sizzle": "*" + } + }, "@types/json-patch": { "version": "0.0.30", "resolved": "https://registry.npmjs.org/@types/json-patch/-/json-patch-0.0.30.tgz", @@ -3884,6 +4009,11 @@ } } }, + "@types/sizzle": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.3.tgz", + "integrity": "sha512-JYM8x9EGF163bEyhdJBpR2QX1R5naCJHC8ucJylJ3w9/CVBaskdQ8WqBf8MmQrd1kRvp/a4TS8HJ+bxzR7ZJYQ==" + }, "@types/source-list-map": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/@types/source-list-map/-/source-list-map-0.1.2.tgz", @@ -5108,8 +5238,7 @@ "base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "dev": true + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" }, "base64id": { "version": "2.0.0", @@ -5315,6 +5444,11 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "qs": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", + "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" } } }, @@ -5638,7 +5772,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", - "dev": true, "requires": { "function-bind": "^1.1.1", "get-intrinsic": "^1.0.2" @@ -5704,6 +5837,14 @@ "integrity": "sha512-feylzsbDxi1gPZ1IjystzIQZagYYLvfKrSuygUCgf7z6x790VEzze5QEkdSV1U58RA7Hi0+v6fv4K54atOzATg==", "dev": true }, + "capacitor-secure-storage-plugin": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/capacitor-secure-storage-plugin/-/capacitor-secure-storage-plugin-0.6.2.tgz", + "integrity": "sha512-f05BLb98TG5qqxN8FvTOLPNvtoebdZyrWAueoyw25Xip7nJBkFqrGxZtcFDMONi4DZRz/z9DMz1Bw8F/8fPAJA==", + "requires": { + "@capacitor/core": "^3.0.0" + } + }, "caseless": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", @@ -8770,6 +8911,11 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "qs": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", + "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" } } }, @@ -9298,7 +9444,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", - "dev": true, "requires": { "function-bind": "^1.1.1", "has": "^1.0.3", @@ -9579,6 +9724,11 @@ "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz", "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==" }, + "guid-typescript": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/guid-typescript/-/guid-typescript-1.0.9.tgz", + "integrity": "sha512-Y8T4vYhEfwJOTbouREvG+3XDsjr8E3kIr7uf+JZ0BYloFsttiHU0WfvANVsR7TxNUJa/WpCnw/Ino/p+DeBhBQ==" + }, "handle-thing": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", @@ -9661,8 +9811,7 @@ "has-symbols": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", - "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==", - "dev": true + "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==" }, "has-tostringtag": { "version": "1.0.0", @@ -10307,6 +10456,33 @@ "ipaddr.js": "^1.9.0" } }, + "ionic-appauth": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/ionic-appauth/-/ionic-appauth-0.8.5.tgz", + "integrity": "sha512-4GyFasdqLboGz4mej71UcsSM7XodpFXOcBYdtGvUKJ29p0iIB3/c5BHuGAmpiTqvUfAnGD8insimaQEE3Iensw==", + "requires": { + "@capacitor/browser": "^1.0.2", + "@capacitor/core": "^3.1.2", + "@capacitor/storage": "^1.0.3", + "@ionic-native/core": "^5.34.0", + "@ionic-native/http": "^5.34.0", + "@ionic-native/in-app-browser": "^5.34.0", + "@ionic-native/safari-view-controller": "^5.34.0", + "@ionic-native/secure-storage": "^5.34.0", + "@ionic/storage": "^3.0.6", + "@openid/appauth": "^1.3.1", + "capacitor-secure-storage-plugin": "^0.6.2", + "guid-typescript": "^1.0.9", + "tslib": "^1.9.3" + }, + "dependencies": { + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + } + } + }, "ionicons": { "version": "5.5.4", "resolved": "https://registry.npmjs.org/ionicons/-/ionicons-5.5.4.tgz", @@ -11069,6 +11245,11 @@ "integrity": "sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=", "dev": true }, + "jsonpath-plus": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/jsonpath-plus/-/jsonpath-plus-6.0.1.tgz", + "integrity": "sha512-EvGovdvau6FyLexFH2OeXfIITlgIbgZoAZe3usiySeaIDm5QS+A10DKNpaPBBqqRSZr2HN6HVNXxtwUAr2apEw==" + }, "jsonpointer": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-4.1.0.tgz", @@ -12882,8 +13063,7 @@ "object-inspect": { "version": "1.11.0", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.11.0.tgz", - "integrity": "sha512-jp7ikS6Sd3GxQfZJPyH3cjcbJF6GZPClgdV+EFygjFLQ5FmW/dRUnTd9PQ9k0JhoNDabWFbpF1yCdSWCC6gexg==", - "dev": true + "integrity": "sha512-jp7ikS6Sd3GxQfZJPyH3cjcbJF6GZPClgdV+EFygjFLQ5FmW/dRUnTd9PQ9k0JhoNDabWFbpF1yCdSWCC6gexg==" }, "object-is": { "version": "1.1.5", @@ -12997,6 +13177,11 @@ "integrity": "sha512-8AV/sCtuzUeTo8gQK5qDZzARrulB3egtLzFgteqB2tcT4Mw7B8Kt7JcDHmltjz6FOAHsvTevk70gZEbhM4ZS9Q==", "dev": true }, + "opener": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", + "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==" + }, "opening_hours": { "version": "3.7.0", "resolved": "https://registry.npmjs.org/opening_hours/-/opening_hours-3.7.0.tgz", @@ -15413,9 +15598,12 @@ "dev": true }, "qs": { - "version": "6.7.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", - "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" + "version": "6.10.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.1.tgz", + "integrity": "sha512-M528Hph6wsSVOBiYUnGf+K/7w0hNshs/duGsNXPUCLH5XAqjEtiPGwNONLV0tBH8NoGb0mvD5JubnUTrujKDTg==", + "requires": { + "side-channel": "^1.0.4" + } }, "querystring": { "version": "0.2.0", @@ -16147,9 +16335,9 @@ } }, "rxjs": { - "version": "6.6.3", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.3.tgz", - "integrity": "sha512-trsQc+xYYXZ3urjOiJOuCOa5N3jAZ3eiSpQB5hIT8zGlL2QfnHLJ2r7GMkBGuIausdJN1OneaI6gQlsqNHHmZQ==", + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", "requires": { "tslib": "^1.9.0" }, @@ -16547,6 +16735,16 @@ "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.3.0.tgz", "integrity": "sha1-NZbmMHp4FUT1kfN9phg2DzHbV7E=" }, + "side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "requires": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + } + }, "signal-exit": { "version": "3.0.6", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.6.tgz", diff --git a/package.json b/package.json index e19c7787..45e11e67 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ "@angular/router": "12.2.13", "@asymmetrik/ngx-leaflet": "8.1.0", "@asymmetrik/ngx-leaflet-markercluster": "5.0.1", + "@capacitor-community/http": "1.4.1", "@capacitor/app": "1.0.6", "@capacitor/browser": "1.0.6", "@capacitor/core": "3.3.1", @@ -61,6 +62,7 @@ "@capacitor/keyboard": "1.1.3", "@capacitor/splash-screen": "1.1.6", "@capacitor/status-bar": "1.0.6", + "@capacitor/storage": "1.2.3", "@ionic-native/core": "5.36.0", "@ionic-native/diagnostic": "5.36.0", "@ionic-native/dialogs": "5.36.0", @@ -73,6 +75,7 @@ "@openstapps/api": "0.35.0", "@openstapps/configuration": "0.28.1", "@openstapps/core": "0.53.0", + "capacitor-secure-storage-plugin": "0.6.2", "cordova-plugin-device": "2.0.3", "cordova-plugin-dialogs": "2.0.2", "cordova-plugin-geolocation": "4.1.0", @@ -83,6 +86,8 @@ "deepmerge": "3.3.0", "form-data": "2.5.0", "geojson": "0.5.0", + "ionic-appauth": "0.8.5", + "jsonpath-plus": "6.0.1", "leaflet": "1.7.1", "leaflet.markercluster": "1.5.1", "lodash-es": "4.17.21", @@ -91,7 +96,8 @@ "ngx-markdown": "12.0.1", "ngx-moment": "5.0.0", "opening_hours": "3.7.0", - "rxjs": "6.6.3", + "qs": "6.10.1", + "rxjs": "6.6.7", "swiper": "7.1.0", "tslib": "2.0.0", "zone.js": "0.11.4" @@ -110,7 +116,7 @@ "@angular/compiler": "12.2.13", "@angular/compiler-cli": "12.2.13", "@angular/language-service": "12.2.13", - "@capacitor/android": "3.3.2", + "@capacitor/android": "3.4.0", "@capacitor/cli": "3.3.2", "@capacitor/ios": "3.3.2", "@compodoc/compodoc": "1.1.14", @@ -123,6 +129,7 @@ "@types/leaflet.markercluster": "1.4.5", "@types/lodash-es": "4.17.4", "@types/node": "14.17.16", + "@types/qs": "6.9.7", "@typescript-eslint/eslint-plugin": "4.32.0", "@typescript-eslint/parser": "4.32.0", "conventional-changelog-cli": "2.1.1", diff --git a/src/app/_helpers/data/resources/test-resources.ts b/src/app/_helpers/data/resources/test-resources.ts index cc4aac5d..1703fcdb 100644 --- a/src/app/_helpers/data/resources/test-resources.ts +++ b/src/app/_helpers/data/resources/test-resources.ts @@ -30478,7 +30478,7 @@ export const sampleResources = [{ 'steps': [ { 'type': 'location', - 'location': '#/b-tu/main' + 'location': '/b-tu/main' }, { 'type': 'tooltip', @@ -30494,7 +30494,7 @@ export const sampleResources = [{ 'text': 'Öffne die Suche.', 'resolved': { 'location': { - 'is': '#/b-tu/search' + 'is': '/b-tu/search' } }, 'position': 'bottom' @@ -30529,7 +30529,7 @@ export const sampleResources = [{ 'text': 'Klicke auf eine Veranstaltung...', 'resolved': { 'location': { - 'match': '#/b-tu/data/detail/Event' + 'match': '/b-tu/data/detail/Event' } }, 'position': 'top' @@ -30553,7 +30553,7 @@ export const sampleResources = [{ 'text': 'Öffne deinen Stundenplan.', 'resolved': { 'location': { - 'is': '#/b-tu/events' + 'is': '/b-tu/events' } }, 'position': 'right' @@ -30585,7 +30585,7 @@ export const sampleResources = [{ 'steps': [ { 'type': 'location', - 'location': '#/b-tu/main' + 'location': '/b-tu/main' }, { 'type': 'tooltip', @@ -30632,7 +30632,7 @@ export const sampleResources = [{ 'text': 'Das ist das Widget, dass dir die Speisepläne deiner favorisierten Essensorte anzeigt. Klicke auf "Essensorte", um zur Übersicht der Essensorte zu gelangen.', 'resolved': { 'location': { - 'is': '#/b-tu/places?types=FoodEstablishment' + 'is': '/b-tu/places?types=FoodEstablishment' } } }, @@ -30642,7 +30642,7 @@ export const sampleResources = [{ 'text': 'Wähle die "Mathe Cafeteria" aus, um sie zu favorisieren.', 'resolved': { 'location': { - 'is': '#/b-tu/map?place=MA%20Mathe%20Cafeteria' + 'is': '/b-tu/map?place=MA%20Mathe%20Cafeteria' } } }, @@ -30668,7 +30668,7 @@ export const sampleResources = [{ 'text': 'Klicke auf das TU-Logo oder "StApps", um zur Startseite zurückzukehren.', 'resolved': { 'location': { - 'is': '#/b-tu/main' + 'is': '/b-tu/main' } } }, @@ -30691,7 +30691,7 @@ export const sampleResources = [{ 'text': 'Öffne deine Favoriten.', 'resolved': { 'location': { - 'is': '#/b-tu/favorites' + 'is': '/b-tu/favorites' } } }, diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index 4cba8fd2..42789ec0 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -13,7 +13,7 @@ * this program. If not, see . */ import {NgModule} from '@angular/core'; -import {RouterModule, Routes} from '@angular/router'; +import {PreloadAllModules, RouterModule, Routes} from '@angular/router'; const routes: Routes = [{path: '', redirectTo: '/news', pathMatch: 'full'}]; @@ -22,6 +22,11 @@ const routes: Routes = [{path: '', redirectTo: '/news', pathMatch: 'full'}]; */ @NgModule({ exports: [RouterModule], - imports: [RouterModule.forRoot(routes, {relativeLinkResolution: 'legacy'})], + imports: [ + RouterModule.forRoot(routes, { + enableTracing: true, + preloadingStrategy: PreloadAllModules, + }), + ], }) export class AppRoutingModule {} diff --git a/src/app/app.component.spec.ts b/src/app/app.component.spec.ts index 0ed6b1d3..c8e92f99 100644 --- a/src/app/app.component.spec.ts +++ b/src/app/app.component.spec.ts @@ -20,7 +20,9 @@ import {Platform} from '@ionic/angular'; import {TranslateService} from '@ngx-translate/core'; import {ThingTranslateService} from './translation/thing-translate.service'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; import {AppComponent} from './app.component'; +import {AuthModule} from './modules/auth/auth.module'; import {ConfigProvider} from './modules/config/config.provider'; import {SettingsProvider} from './modules/settings/settings.provider'; import {NGXLogger} from 'ngx-logger'; @@ -35,10 +37,16 @@ describe('AppComponent', () => { let configProvider: jasmine.SpyObj; let ngxLogger: jasmine.SpyObj; + let platformIsSpy; + beforeEach( waitForAsync(() => { platformReadySpy = Promise.resolve(); - platformSpy = jasmine.createSpyObj('Platform', {ready: platformReadySpy}); + platformIsSpy = Promise.resolve(); + platformSpy = jasmine.createSpyObj('Platform', { + ready: platformReadySpy, + is: platformIsSpy, + }); translateServiceSpy = jasmine.createSpyObj('TranslateService', [ 'setDefaultLang', 'use', @@ -55,7 +63,11 @@ describe('AppComponent', () => { ngxLogger = jasmine.createSpyObj('NGXLogger', ['log', 'error', 'warn']); TestBed.configureTestingModule({ - imports: [RouterTestingModule.withRoutes([])], + imports: [ + RouterTestingModule.withRoutes([]), + HttpClientTestingModule, + AuthModule, + ], declarations: [AppComponent], providers: [ {provide: Platform, useValue: platformSpy}, diff --git a/src/app/app.component.ts b/src/app/app.component.ts index fbac57bd..89523345 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -16,10 +16,14 @@ import {Component, NgZone} from '@angular/core'; import {Router} from '@angular/router'; import {App, URLOpenListenerEvent} from '@capacitor/app'; import {SplashScreen} from '@capacitor/splash-screen'; -import {Platform} from '@ionic/angular'; +import {Platform, ToastController} from '@ionic/angular'; import {NGXLogger} from 'ngx-logger'; import {ConfigProvider} from './modules/config/config.provider'; import {SettingsProvider} from './modules/settings/settings.provider'; +import {PAIAAuthService} from './modules/auth/paia/paia-auth.service'; +import {DefaultAuthService} from './modules/auth/default-auth.service'; +import {environment} from '../environments/environment'; +import {AuthHelperService} from './modules/auth/auth-helper.service'; /** * TODO @@ -51,6 +55,10 @@ export class AppComponent { * @param logger An angular logger * @param router The angular router * @param zone The angular zone + * @param defaultAuth Auth Service + * @param paiaAuth Auth Service + * @param authHelperService Helper service for OAuth providers + * @param toastController Toast controller */ constructor( private readonly platform: Platform, @@ -59,6 +67,10 @@ export class AppComponent { private readonly logger: NGXLogger, private readonly router: Router, private readonly zone: NgZone, + private readonly defaultAuth: DefaultAuthService, + private readonly paiaAuth: PAIAAuthService, + private readonly authHelperService: AuthHelperService, + private readonly toastController: ToastController, ) { void this.initializeApp(); } @@ -69,7 +81,7 @@ export class AppComponent { async initializeApp() { App.addListener('appUrlOpen', (event: URLOpenListenerEvent) => { this.zone.run(() => { - const slug = event.url.split('.de').pop(); + const slug = event.url.split(environment.appDomain).pop(); if (slug) { this.router.navigateByUrl(slug); } @@ -78,6 +90,9 @@ export class AppComponent { }); }); this.platform.ready().then(async () => { + await this.authInit(); + await this.defaultAuth.init(); + await this.paiaAuth.init(); await SplashScreen.hide(); // initialise the configProvider @@ -102,4 +117,28 @@ export class AppComponent { ]); }); } + + private async authInit() { + await this.defaultAuth.init(); + await this.paiaAuth.init(); + this.defaultAuth.events$.subscribe(action => + this.showMessage( + this.authHelperService.getAuthMessage('default', action), + ), + ); + this.paiaAuth.events$.subscribe(action => + this.showMessage(this.authHelperService.getAuthMessage('paia', action)), + ); + } + + private async showMessage(message?: string) { + if (typeof message === 'undefined') { + return; + } + const toast = await this.toastController.create({ + message: message, + duration: 2000, + }); + await toast.present(); + } } diff --git a/src/app/app.module.ts b/src/app/app.module.ts index bae50456..7be38a04 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -18,7 +18,7 @@ import { PathLocationStrategy, registerLocaleData, } from '@angular/common'; -import {HttpClient} from '@angular/common/http'; +import {HttpClient, HttpClientModule} from '@angular/common/http'; import localeDe from '@angular/common/locales/de'; import {APP_INITIALIZER, NgModule, Provider} from '@angular/core'; import {BrowserModule} from '@angular/platform-browser'; @@ -55,10 +55,13 @@ import {initLogger} from './_helpers/ts-logger'; import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; import {AboutModule} from './modules/about/about.module'; import {FavoritesModule} from './modules/favorites/favorites.module'; +import {ProfilePageModule} from './modules/profile/profile.module'; +import {EndSessionPageModule} from './modules/auth/end-session/end-session.module'; import {FeedbackModule} from './modules/feedback/feedback.module'; import {DebugDataCollectorService} from './modules/data/debug-data-collector.service'; import {Browser} from './util/browser.factory'; import {browserFactory} from './util/browser.factory'; +import {AuthModule} from './modules/auth/auth.module'; registerLocaleData(localeDe); @@ -143,14 +146,18 @@ const providers: Provider[] = [ imports: [ AboutModule, AppRoutingModule, + AuthModule, BrowserModule, BrowserAnimationsModule, CatalogModule, CommonModule, ConfigModule, DataModule, + EndSessionPageModule, IonicModule.forRoot(), FavoritesModule, + HttpClientModule, + ProfilePageModule, FeedbackModule, MapModule, MenuModule, diff --git a/src/app/modules/auth/auth-callback/page/auth-callback-page.component.html b/src/app/modules/auth/auth-callback/page/auth-callback-page.component.html new file mode 100644 index 00000000..fe073705 --- /dev/null +++ b/src/app/modules/auth/auth-callback/page/auth-callback-page.component.html @@ -0,0 +1,3 @@ +
+

{{ 'auth.messages.default.authorizing' | translate }}

+
diff --git a/src/app/modules/auth/auth-callback/page/auth-callback-page.component.scss b/src/app/modules/auth/auth-callback/page/auth-callback-page.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/app/modules/auth/auth-callback/page/auth-callback-page.component.ts b/src/app/modules/auth/auth-callback/page/auth-callback-page.component.ts new file mode 100644 index 00000000..bfe36521 --- /dev/null +++ b/src/app/modules/auth/auth-callback/page/auth-callback-page.component.ts @@ -0,0 +1,34 @@ +import {Component, OnInit, OnDestroy} from '@angular/core'; +import {NavController} from '@ionic/angular'; +import {Router} from '@angular/router'; +import {IAuthAction} from 'ionic-appauth'; +import {Subscription} from 'rxjs'; +import {DefaultAuthService} from '../../default-auth.service'; + +@Component({ + selector: 'auth-callback', + templateUrl: './auth-callback-page.component.html', + styleUrls: ['./auth-callback-page.component.scss'], +}) +export class AuthCallbackPageComponent implements OnInit, OnDestroy { + sub: Subscription; + + constructor( + private auth: DefaultAuthService, + private navCtrl: NavController, + private router: Router, + ) {} + + ngOnInit() { + this.sub = this.auth.events$.subscribe(action => this.postCallback(action)); + this.auth.authorizationCallback(window.location.origin + this.router.url); + } + + ngOnDestroy() { + this.sub.unsubscribe(); + } + + async postCallback(_action: IAuthAction) { + await this.navCtrl.navigateRoot('profile'); + } +} diff --git a/src/app/modules/auth/auth-guard.service.ts b/src/app/modules/auth/auth-guard.service.ts new file mode 100644 index 00000000..afce0827 --- /dev/null +++ b/src/app/modules/auth/auth-guard.service.ts @@ -0,0 +1,42 @@ +import {Injectable} from '@angular/core'; +import {CanActivate, Router, RouterStateSnapshot} from '@angular/router'; +import {DefaultAuthService} from './default-auth.service'; +import {PAIAAuthService} from './paia/paia-auth.service'; +import {IAuthService} from 'ionic-appauth'; +import {ActivatedAuthRouteSnapshot} from './auth-routes'; + +@Injectable({ + providedIn: 'root', +}) +export class AuthGuardService implements CanActivate { + authService: IAuthService | PAIAAuthService; + + constructor( + private defaultAuth: DefaultAuthService, + private paiaAuth: PAIAAuthService, + private router: Router, + ) {} + + public async canActivate( + route: ActivatedAuthRouteSnapshot, + _state: RouterStateSnapshot, + ) { + switch (route.data.authProvider) { + case 'paia': + this.authService = this.paiaAuth; + break; + default: + this.authService = this.defaultAuth; + break; + } + + try { + await this.authService.getValidToken(); + } catch { + this.router.navigate(['profile']); + return false; + } + + return true; + } +} diff --git a/src/app/modules/auth/auth-helper.service.ts b/src/app/modules/auth/auth-helper.service.ts new file mode 100644 index 00000000..ef2d71d9 --- /dev/null +++ b/src/app/modules/auth/auth-helper.service.ts @@ -0,0 +1,50 @@ +import {Injectable} from '@angular/core'; +import { + SCAuthorizationProviderType, + SCUserConfiguration, + userMapping, +} from '../profile/user'; +import {IPAIAAuthAction} from './paia/paia-auth-action'; +import {AuthActions, IAuthAction} from 'ionic-appauth'; +import {TranslateService} from '@ngx-translate/core'; +import {JSONFile} from '@angular/cli/utilities/json-file'; +import {JSONPath} from 'jsonpath-plus'; + +@Injectable({ + providedIn: 'root', +}) +export class AuthHelperService { + constructor(private translateService: TranslateService) {} + + public getAuthMessage( + provider: SCAuthorizationProviderType, + action: IAuthAction | IPAIAAuthAction, + ) { + let message: string | undefined; + switch (action.action) { + case AuthActions.SignInSuccess: + message = this.translateService.instant( + `auth.messages.${provider}.logged_in_success`, + ); + break; + case AuthActions.SignOutSuccess: + message = this.translateService.instant( + `auth.messages.${provider}.logged_out_success`, + ); + break; + } + return message; + } + + getUserFromUserInfo(userInfo: JSONFile) { + const user: SCUserConfiguration = {id: '', name: '', role: 'student'}; + for (const key in userMapping) { + user[key as keyof SCUserConfiguration] = JSONPath({ + path: userMapping[key as keyof SCUserConfiguration] as string, + json: userInfo, + })[0]; + } + + return user; + } +} diff --git a/src/app/modules/auth/auth-routes.ts b/src/app/modules/auth/auth-routes.ts new file mode 100644 index 00000000..768758d6 --- /dev/null +++ b/src/app/modules/auth/auth-routes.ts @@ -0,0 +1,31 @@ +/* + * 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 . + */ + +import {ActivatedRouteSnapshot, Data, Route} from '@angular/router'; +import {SCAuthorizationProviderType} from '../profile/user'; + +export interface AuthRoute extends Route { + data: { + authProvider: SCAuthorizationProviderType; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + [key: string]: any; + }; +} + +export class ActivatedAuthRouteSnapshot extends ActivatedRouteSnapshot { + data: Data & {authProvider: AuthRoute['data']['authProvider']}; +} + +export type AuthRoutes = AuthRoute[]; diff --git a/src/app/modules/auth/auth-routing.module.ts b/src/app/modules/auth/auth-routing.module.ts new file mode 100644 index 00000000..6591c5bf --- /dev/null +++ b/src/app/modules/auth/auth-routing.module.ts @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2021 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 . + */ + +import {RouterModule, Routes} from '@angular/router'; +import {NgModule} from '@angular/core'; +import {AuthCallbackPageComponent} from './auth-callback/page/auth-callback-page.component'; +import {PAIAAuthCallbackPageComponent} from './paia/auth-callback/page/auth-callback-page.component'; + +const authRoutes: Routes = [ + {path: 'auth/callback', component: AuthCallbackPageComponent}, + {path: 'auth/paia/callback', component: PAIAAuthCallbackPageComponent}, +]; + +/** + * Module defining routes for auth module + */ +@NgModule({ + exports: [RouterModule], + imports: [RouterModule.forChild(authRoutes)], +}) +export class AuthRoutingModule {} diff --git a/src/app/modules/auth/auth.module.ts b/src/app/modules/auth/auth.module.ts new file mode 100644 index 00000000..acc79b9f --- /dev/null +++ b/src/app/modules/auth/auth.module.ts @@ -0,0 +1,47 @@ +import {NgModule} from '@angular/core'; +import {CommonModule} from '@angular/common'; +import {Platform} from '@ionic/angular'; +import {Requestor, StorageBackend} from '@openid/appauth'; +import {authFactory, paiaAuthFactory, storageFactory} from './factories'; +import {DefaultAuthService} from './default-auth.service'; +import {Browser} from 'ionic-appauth'; +import {CapacitorBrowser} from 'ionic-appauth/lib/capacitor'; +import {httpFactory} from './factories/http.factory'; +import {HttpClient} from '@angular/common/http'; +import {PAIAAuthService} from './paia/paia-auth.service'; +import {AuthRoutingModule} from './auth-routing.module'; +import {TranslateModule} from '@ngx-translate/core'; +import {AuthCallbackPageComponent} from './auth-callback/page/auth-callback-page.component'; +import {PAIAAuthCallbackPageComponent} from './paia/auth-callback/page/auth-callback-page.component'; + +@NgModule({ + declarations: [AuthCallbackPageComponent, PAIAAuthCallbackPageComponent], + imports: [CommonModule, AuthRoutingModule, TranslateModule], + providers: [ + { + provide: StorageBackend, + useFactory: storageFactory, + deps: [Platform], + }, + { + provide: Requestor, + useFactory: httpFactory, + deps: [Platform, HttpClient], + }, + { + provide: Browser, + useClass: CapacitorBrowser, + }, + { + provide: DefaultAuthService, + useFactory: authFactory, + deps: [Requestor, Browser, StorageBackend], + }, + { + provide: PAIAAuthService, + useFactory: paiaAuthFactory, + deps: [Requestor, Browser, StorageBackend], + }, + ], +}) +export class AuthModule {} diff --git a/src/app/modules/auth/capacitor-requestor.ts b/src/app/modules/auth/capacitor-requestor.ts new file mode 100644 index 00000000..49e1c485 --- /dev/null +++ b/src/app/modules/auth/capacitor-requestor.ts @@ -0,0 +1,53 @@ +import {Requestor} from '@openid/appauth'; +import {Http, HttpHeaders, HttpResponse} from '@capacitor-community/http'; +import {XhrSettings} from 'ionic-appauth/lib/cordova'; +import qs from 'qs'; + +// REQUIRES CAPACITOR PLUGIN +// @capacitor-community/http +export class CapacitorRequestor extends Requestor { + constructor() { + super(); + } + + public async xhr(settings: XhrSettings): Promise { + if (!settings.method) settings.method = 'GET'; + + 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); + } + } + + private async get(url: string, headers: HttpHeaders) { + return Http.get({url, headers}).then( + (response: HttpResponse) => response.data as T, + ); + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private async post(url: string, data: any, headers: HttpHeaders) { + return Http.post({url, data: qs.parse(data), headers}).then( + (response: HttpResponse) => response.data as T, + ); + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private async put(url: string, data: any, headers: HttpHeaders) { + return Http.put({url, data, headers}).then( + (response: HttpResponse) => response.data as T, + ); + } + + private async delete(url: string, headers: HttpHeaders) { + return Http.del({url, headers}).then( + (response: HttpResponse) => response.data as T, + ); + } +} diff --git a/src/app/modules/auth/default-auth.service.ts b/src/app/modules/auth/default-auth.service.ts new file mode 100644 index 00000000..ad450bc6 --- /dev/null +++ b/src/app/modules/auth/default-auth.service.ts @@ -0,0 +1,53 @@ +import {AuthorizationRequestHandler} from '@openid/appauth'; +import { + StorageBackend, + Requestor, + AuthorizationServiceConfiguration, + LocalStorageBackend, + JQueryRequestor, + TokenRequestHandler, +} from '@openid/appauth'; +import { + UserInfoHandler, + EndSessionHandler, + Browser, + DefaultBrowser, + AuthService, + AuthActionBuilder, +} from 'ionic-appauth'; + +const TOKEN_RESPONSE_KEY = 'token_response'; + +export class DefaultAuthService extends AuthService { + public localConfiguration: AuthorizationServiceConfiguration; + + 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(), + ) { + super(browser, storage, requestor); + } + + get configuration(): Promise { + if (!this.localConfiguration) + throw new Error('Local Configuration Not Defined'); + + return Promise.resolve(this.localConfiguration); + } + + public async signOut() { + await this.storage.removeItem(TOKEN_RESPONSE_KEY).catch(error => { + this.notifyActionListers(AuthActionBuilder.SignOutFailed(error)); + }); + this.notifyActionListers(AuthActionBuilder.SignOutSuccess()); + } +} diff --git a/src/app/modules/auth/end-session/end-session.module.ts b/src/app/modules/auth/end-session/end-session.module.ts new file mode 100644 index 00000000..0e3655a8 --- /dev/null +++ b/src/app/modules/auth/end-session/end-session.module.ts @@ -0,0 +1,26 @@ +import {NgModule} from '@angular/core'; +import {CommonModule} from '@angular/common'; +import {FormsModule} from '@angular/forms'; +import {Routes, RouterModule} from '@angular/router'; + +import {IonicModule} from '@ionic/angular'; + +import {EndSessionPageComponent} from './page/end-session-page.component'; + +const routes: Routes = [ + { + path: 'logout', + component: EndSessionPageComponent, + }, +]; + +@NgModule({ + imports: [ + CommonModule, + FormsModule, + IonicModule, + RouterModule.forChild(routes), + ], + declarations: [EndSessionPageComponent], +}) +export class EndSessionPageModule {} diff --git a/src/app/modules/auth/end-session/page/end-session-page.component.html b/src/app/modules/auth/end-session/page/end-session-page.component.html new file mode 100644 index 00000000..dfb55534 --- /dev/null +++ b/src/app/modules/auth/end-session/page/end-session-page.component.html @@ -0,0 +1 @@ +

Signing out...

diff --git a/src/app/modules/auth/end-session/page/end-session-page.component.scss b/src/app/modules/auth/end-session/page/end-session-page.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/app/modules/auth/end-session/page/end-session-page.component.ts b/src/app/modules/auth/end-session/page/end-session-page.component.ts new file mode 100644 index 00000000..dafab82b --- /dev/null +++ b/src/app/modules/auth/end-session/page/end-session-page.component.ts @@ -0,0 +1,20 @@ +import {Component, OnInit} from '@angular/core'; +import {NavController} from '@ionic/angular'; +import {DefaultAuthService} from '../../default-auth.service'; + +@Component({ + selector: 'end-session', + templateUrl: './end-session-page.component.html', + styleUrls: ['./end-session-page.component.scss'], +}) +export class EndSessionPageComponent implements OnInit { + constructor( + private auth: DefaultAuthService, + private navCtrl: NavController, + ) {} + + async ngOnInit() { + this.auth.endSessionCallback(); + await this.navCtrl.navigateRoot('profile'); + } +} diff --git a/src/app/modules/auth/factories/auth.factory.ts b/src/app/modules/auth/factories/auth.factory.ts new file mode 100644 index 00000000..2c003bad --- /dev/null +++ b/src/app/modules/auth/factories/auth.factory.ts @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2021 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 . + */ + +import { + StorageBackend, + Requestor, + AuthorizationServiceConfiguration, +} from '@openid/appauth'; +import {Browser} from 'ionic-appauth'; +import {environment} from 'src/environments/environment'; +import {DefaultAuthService} from '../default-auth.service'; +import {PAIAAuthService} from '../paia/paia-auth.service'; + +export const authFactory = ( + requestor: Requestor, + browser: Browser, + storage: StorageBackend, +) => { + const authService = new DefaultAuthService(browser, storage, requestor); + authService.authConfig = environment.oauth2.client.his; + authService.localConfiguration = new AuthorizationServiceConfiguration( + environment.oauth2.service.his, + ); + + return authService; +}; + +export const paiaAuthFactory = ( + requestor: Requestor, + browser: Browser, + storage: StorageBackend, +) => { + const authService = new PAIAAuthService(browser, storage, requestor); + authService.authConfig = environment.oauth2.client.paia; + authService.localConfiguration = new AuthorizationServiceConfiguration( + environment.oauth2.service.paia, + ); + + return authService; +}; diff --git a/src/app/modules/auth/factories/browser.factory.ts b/src/app/modules/auth/factories/browser.factory.ts new file mode 100644 index 00000000..ae971d47 --- /dev/null +++ b/src/app/modules/auth/factories/browser.factory.ts @@ -0,0 +1,9 @@ +import {Platform} from '@ionic/angular'; +import {DefaultBrowser} from 'ionic-appauth'; +import {CapacitorBrowser} from 'ionic-appauth/lib/capacitor'; + +export const browserFactory = (platform: Platform) => { + return platform.is('capacitor') + ? new CapacitorBrowser() + : new DefaultBrowser(); +}; diff --git a/src/app/modules/auth/factories/http.factory.ts b/src/app/modules/auth/factories/http.factory.ts new file mode 100644 index 00000000..892bc30d --- /dev/null +++ b/src/app/modules/auth/factories/http.factory.ts @@ -0,0 +1,10 @@ +import {HttpClient} from '@angular/common/http'; +import {Platform} from '@ionic/angular'; +import {CapacitorRequestor} from '../capacitor-requestor'; +import {NgHttpService} from '../ng-http.service'; + +export const httpFactory = (platform: Platform, httpClient: HttpClient) => { + return platform.is('capacitor') + ? new CapacitorRequestor() + : new NgHttpService(httpClient); +}; diff --git a/src/app/modules/auth/factories/index.ts b/src/app/modules/auth/factories/index.ts new file mode 100644 index 00000000..b3f4218f --- /dev/null +++ b/src/app/modules/auth/factories/index.ts @@ -0,0 +1,3 @@ +export * from './auth.factory'; +export * from './browser.factory'; +export * from './storage.factory'; diff --git a/src/app/modules/auth/factories/storage.factory.ts b/src/app/modules/auth/factories/storage.factory.ts new file mode 100644 index 00000000..4cf2edf6 --- /dev/null +++ b/src/app/modules/auth/factories/storage.factory.ts @@ -0,0 +1,9 @@ +import {Platform} from '@ionic/angular'; +import {CapacitorSecureStorage} from 'ionic-appauth/lib/capacitor'; +import {IonicStorage} from 'ionic-appauth/lib'; + +export const storageFactory = (platform: Platform) => { + return platform.is('capacitor') + ? new CapacitorSecureStorage() + : new IonicStorage(); +}; diff --git a/src/app/modules/auth/ng-http.service.ts b/src/app/modules/auth/ng-http.service.ts new file mode 100644 index 00000000..54b1d523 --- /dev/null +++ b/src/app/modules/auth/ng-http.service.ts @@ -0,0 +1,53 @@ +import {Injectable} from '@angular/core'; +import {Requestor} from '@openid/appauth'; +import {HttpClient, HttpHeaders} from '@angular/common/http'; +import {XhrSettings} from 'ionic-appauth/lib/cordova'; + +@Injectable({ + providedIn: 'root', +}) +export class NgHttpService implements Requestor { + constructor(private http: HttpClient) {} + + public async xhr(settings: XhrSettings): Promise { + if (!settings.method) { + settings.method = 'GET'; + } + + switch (settings.method) { + case 'GET': + return this.http + .get(settings.url, {headers: this.getHeaders(settings.headers)}) + .toPromise(); + case 'POST': + return this.http + .post(settings.url, settings.data, { + headers: this.getHeaders(settings.headers), + }) + .toPromise(); + case 'PUT': + return this.http + .put(settings.url, settings.data, { + headers: this.getHeaders(settings.headers), + }) + .toPromise(); + case 'DELETE': + return this.http + .delete(settings.url, {headers: this.getHeaders(settings.headers)}) + .toPromise(); + } + } + + // 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; + } +} diff --git a/src/app/modules/auth/paia/auth-callback/page/auth-callback-page.component.html b/src/app/modules/auth/paia/auth-callback/page/auth-callback-page.component.html new file mode 100644 index 00000000..c531a22b --- /dev/null +++ b/src/app/modules/auth/paia/auth-callback/page/auth-callback-page.component.html @@ -0,0 +1,3 @@ +
+

{{ 'auth.messages.paia.authorizing' | translate }}

+
diff --git a/src/app/modules/auth/paia/auth-callback/page/auth-callback-page.component.scss b/src/app/modules/auth/paia/auth-callback/page/auth-callback-page.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/app/modules/auth/paia/auth-callback/page/auth-callback-page.component.ts b/src/app/modules/auth/paia/auth-callback/page/auth-callback-page.component.ts new file mode 100644 index 00000000..70901f21 --- /dev/null +++ b/src/app/modules/auth/paia/auth-callback/page/auth-callback-page.component.ts @@ -0,0 +1,34 @@ +import {Component, OnInit, OnDestroy} from '@angular/core'; +import {NavController} from '@ionic/angular'; +import {Router} from '@angular/router'; +import {IAuthAction} from 'ionic-appauth'; +import {Subscription} from 'rxjs'; +import {PAIAAuthService} from '../../paia-auth.service'; + +@Component({ + selector: 'auth-callback', + templateUrl: './auth-callback-page.component.html', + styleUrls: ['./auth-callback-page.component.scss'], +}) +export class PAIAAuthCallbackPageComponent implements OnInit, OnDestroy { + sub: Subscription; + + constructor( + private auth: PAIAAuthService, + private navCtrl: NavController, + private router: Router, + ) {} + + ngOnInit() { + this.sub = this.auth.events$.subscribe(action => this.postCallback(action)); + this.auth.authorizationCallback(window.location.origin + this.router.url); + } + + ngOnDestroy() { + this.sub.unsubscribe(); + } + + async postCallback(_action: IAuthAction) { + this.navCtrl.navigateRoot('profile'); + } +} diff --git a/src/app/modules/auth/paia/authorization-request-handler.ts b/src/app/modules/auth/paia/authorization-request-handler.ts new file mode 100644 index 00000000..ee6a380f --- /dev/null +++ b/src/app/modules/auth/paia/authorization-request-handler.ts @@ -0,0 +1,217 @@ +/* + * 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 . + */ + +import { + StorageBackend, + BasicQueryStringUtils, + DefaultCrypto, + AuthorizationServiceConfiguration, + AuthorizationRequest, + StringMap, + AuthorizationError, + AuthorizationErrorJson, +} from '@openid/appauth'; +import {Browser} from 'ionic-appauth'; +import {PAIAAuthorizationNotifier} from './paia-authorization-notifier'; +import {PAIAAuthorizationRequestResponse} from './authorization-request-response'; +import { + PAIAAuthorizationResponse, + PAIAAuthorizationResponseJson, +} from './paia-authorization-response'; + +/** key for authorization request. */ +const authorizationRequestKey = (handle: string) => { + return `${handle}_appauth_authorization_request`; +}; + +/** key in local storage which represents the current authorization request. */ +const AUTHORIZATION_REQUEST_HANDLE_KEY = + 'appauth_current_authorization_request'; +export const AUTHORIZATION_RESPONSE_KEY = 'auth_response'; + +// TODO: PAIA specific ...!!! use whatever you can from the parent class ! + +export class PAIAAuthorizationRequestHandler { + notifier: PAIAAuthorizationNotifier; + + constructor( + private browser: Browser, + private storage: StorageBackend, + public utils = new BasicQueryStringUtils(), + protected crypto = new Crypto(), + private generateRandom = new DefaultCrypto(), + ) {} + + public async performAuthorizationRequest( + configuration: AuthorizationServiceConfiguration, + request: AuthorizationRequest, + ): Promise { + const handle = this.generateRandom.generateRandom(10); + await this.storage.setItem(AUTHORIZATION_REQUEST_HANDLE_KEY, handle); + await this.storage.setItem( + authorizationRequestKey(handle), + JSON.stringify(await request.toJson()), + ); + const url = this.buildRequestUrl(configuration, request); + const returnedUrl: string | undefined = await this.browser.showWindow( + url, + request.redirectUri, + ); + + // callback may come from showWindow or via another method + if (typeof returnedUrl !== 'undefined') { + await this.storage.setItem(AUTHORIZATION_RESPONSE_KEY, returnedUrl); + await this.completeAuthorizationRequestIfPossible(); + } + } + + protected async completeAuthorizationRequest(): Promise { + const handle = await this.storage.getItem(AUTHORIZATION_REQUEST_HANDLE_KEY); + + if (!handle) { + throw new Error('Handle Not Available'); + } + + const request: AuthorizationRequest = this.getAuthorizationRequest( + await this.storage.getItem(authorizationRequestKey(handle)), + ); + const queryParameters = this.getQueryParams( + await this.storage.getItem(AUTHORIZATION_RESPONSE_KEY), + ); + void this.removeItemsFromStorage(handle); + + // const state: string | undefined = queryParams['state']; + const error: string | undefined = queryParameters['error']; + + // TODO: we need state from PAIA (we don't get state at the moment) + // if (state !== request.state) { + // throw new Error("State Does Not Match"); + // } + + return { + request: request, // request + response: !error + ? this.getAuthorizationResponse(queryParameters) + : undefined, + error: error ? this.getAuthorizationError(queryParameters) : undefined, + }; + } + + private getAuthorizationRequest( + authRequest: string | null, + ): AuthorizationRequest { + if (authRequest == undefined) { + throw new Error('No Auth Request Available'); + } + + return new AuthorizationRequest(JSON.parse(authRequest)); + } + + private getAuthorizationError( + queryParameters: StringMap, + ): AuthorizationError { + const authorizationErrorJSON: AuthorizationErrorJson = { + error: queryParameters['error'], + error_description: queryParameters['error_description'], + error_uri: undefined, + state: queryParameters['state'], + }; + return new AuthorizationError(authorizationErrorJSON); + } + + private getAuthorizationResponse( + queryParameters: StringMap, + ): PAIAAuthorizationResponse { + const authorizationResponseJSON: PAIAAuthorizationResponseJson = { + code: queryParameters['code'], + patron: queryParameters['patron'], + // TODO: currently PAIA is not providing state + state: queryParameters['state'] ?? '', + }; + return new PAIAAuthorizationResponse(authorizationResponseJSON); + } + + private async removeItemsFromStorage(handle: string): Promise { + await this.storage.removeItem(AUTHORIZATION_REQUEST_HANDLE_KEY); + await this.storage.removeItem(authorizationRequestKey(handle)); + await this.storage.removeItem(AUTHORIZATION_RESPONSE_KEY); + } + + private getQueryParams(authResponse: string | null): StringMap { + if (authResponse != undefined) { + const querySide: string = authResponse.split('#')[0]; + const parts: string[] = querySide.split('?'); + if (parts.length !== 2) throw new Error('Invalid auth response string'); + const hash = parts[1]; + return this.utils.parseQueryString(hash); + } else { + return {}; + } + } + + setAuthorizationNotifier( + notifier: PAIAAuthorizationNotifier, + ): PAIAAuthorizationRequestHandler { + this.notifier = notifier; + return this; + } + + completeAuthorizationRequestIfPossible(): Promise { + // call complete authorization if possible to see there might + // be a response that needs to be delivered. + console.log( + `Checking to see if there is an authorization response to be delivered.`, + ); + if (!this.notifier) { + console.log(`Notifier is not present on AuthorizationRequest handler. + No delivery of result will be possible`); + } + return this.completeAuthorizationRequest().then(result => { + if (!result) { + console.log(`No result is available yet.`); + } + if (result && this.notifier) { + this.notifier.onAuthorizationComplete( + result.request, + result.response, + result.error, + ); + } + }); + } + + /** + * A utility method to be able to build the authorization request URL. + */ + protected buildRequestUrl( + configuration: AuthorizationServiceConfiguration, + request: AuthorizationRequest, + ) { + // build the query string + // coerce to any type for convenience + const requestMap: StringMap = { + redirect_uri: request.redirectUri, + client_id: request.clientId, + response_type: request.responseType, + state: request.state, + scope: request.scope, + }; + + const query = this.utils.stringify(requestMap); + const baseUrl = configuration.authorizationEndpoint; + + return `${baseUrl}?${query}&grant_type=client_credentials`; + } +} diff --git a/src/app/modules/auth/paia/authorization-request-response.ts b/src/app/modules/auth/paia/authorization-request-response.ts new file mode 100644 index 00000000..d9dd73ac --- /dev/null +++ b/src/app/modules/auth/paia/authorization-request-response.ts @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2021 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 . + */ + +import {AuthorizationError, AuthorizationRequest} from '@openid/appauth'; +import {PAIAAuthorizationResponse} from './paia-authorization-response'; + +/** + * Represents a structural type holding both authorization request and response. + */ +export interface PAIAAuthorizationRequestResponse { + request: AuthorizationRequest; + response: PAIAAuthorizationResponse | null; + error: AuthorizationError | null; +} diff --git a/src/app/modules/auth/paia/paia-auth-action.ts b/src/app/modules/auth/paia/paia-auth-action.ts new file mode 100644 index 00000000..02c6aeec --- /dev/null +++ b/src/app/modules/auth/paia/paia-auth-action.ts @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2021 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 . + */ + +import {PAIATokenResponse} from './paia-token-response'; +import {AuthActionBuilder, IAuthAction} from 'ionic-appauth'; + +export interface IPAIAAuthAction extends IAuthAction { + tokenResponse?: PAIATokenResponse; +} + +export class PAIAAuthActionBuilder extends AuthActionBuilder { + public static Init(): IPAIAAuthAction { + return AuthActionBuilder.Init() as IPAIAAuthAction; + } + + public static SignOutSuccess(): IPAIAAuthAction { + return AuthActionBuilder.SignOutSuccess() as IPAIAAuthAction; + } + + public static SignOutFailed(error: Error): IPAIAAuthAction { + return AuthActionBuilder.SignOutFailed(error) as IPAIAAuthAction; + } + + public static RefreshSuccess( + tokenResponse: PAIATokenResponse, + ): IPAIAAuthAction { + return AuthActionBuilder.RefreshSuccess(tokenResponse) as IPAIAAuthAction; + } + + public static RefreshFailed(error: Error): IPAIAAuthAction { + return AuthActionBuilder.RefreshFailed(error) as IPAIAAuthAction; + } + + public static SignInSuccess( + tokenResponse: PAIATokenResponse, + ): IPAIAAuthAction { + return AuthActionBuilder.SignInSuccess(tokenResponse) as IPAIAAuthAction; + } + + public static SignInFailed(error: Error): IPAIAAuthAction { + return AuthActionBuilder.SignInFailed(error) as IPAIAAuthAction; + } + + public static LoadTokenFromStorageSuccess( + tokenResponse: PAIATokenResponse, + ): IPAIAAuthAction { + return AuthActionBuilder.LoadTokenFromStorageSuccess( + tokenResponse, + ) as IPAIAAuthAction; + } + + public static LoadTokenFromStorageFailed(error: Error): IPAIAAuthAction { + return AuthActionBuilder.LoadTokenFromStorageFailed( + error, + ) as IPAIAAuthAction; + } + + public static RevokeTokensSuccess(): IPAIAAuthAction { + return AuthActionBuilder.RevokeTokensSuccess() as IPAIAAuthAction; + } + + public static RevokeTokensFailed(error: Error): IPAIAAuthAction { + return AuthActionBuilder.RevokeTokensFailed(error) as IPAIAAuthAction; + } + + public static LoadUserInfoSuccess(user: Error): IPAIAAuthAction { + return AuthActionBuilder.LoadUserInfoSuccess(user) as IPAIAAuthAction; + } + + public static LoadUserInfoFailed(error: Error): IPAIAAuthAction { + return AuthActionBuilder.LoadUserInfoFailed(error) as IPAIAAuthAction; + } +} diff --git a/src/app/modules/auth/paia/paia-auth.service.ts b/src/app/modules/auth/paia/paia-auth.service.ts new file mode 100644 index 00000000..b820b0f7 --- /dev/null +++ b/src/app/modules/auth/paia/paia-auth.service.ts @@ -0,0 +1,342 @@ +import { + AuthorizationError, + AuthorizationRequest, + AuthorizationRequestJson, + AuthorizationServiceConfiguration, + BasicQueryStringUtils, + DefaultCrypto, + JQueryRequestor, + LocalStorageBackend, + Requestor, + StorageBackend, + StringMap, +} from '@openid/appauth'; +import { + AuthActions, + AUTHORIZATION_RESPONSE_KEY, + AuthSubject, + Browser, + DefaultBrowser, + EndSessionHandler, + IAuthConfig, + IonicEndSessionHandler, + IonicUserInfoHandler, + UserInfoHandler, +} from 'ionic-appauth'; +import {BehaviorSubject, Observable} from 'rxjs'; +import {PAIATokenRequestHandler} from './token-request-handler'; +import {PAIAAuthorizationRequestHandler} from './authorization-request-handler'; +import {PAIATokenRequest, PAIATokenRequestJson} from './paia-token-request'; +import {PAIAAuthorizationResponse} from './paia-authorization-response'; +import {PAIAAuthorizationNotifier} from './paia-authorization-notifier'; +import {PAIATokenResponse} from './paia-token-response'; +import {IPAIAAuthAction, PAIAAuthActionBuilder} from './paia-auth-action'; + +const TOKEN_KEY = 'auth_paia_token'; +const AUTH_EXPIRY_BUFFER = 10 * 60 * -1; // 10 mins in seconds + +export interface IAuthService { + signIn(authExtras?: StringMap, state?: string): void; + signOut(state?: string, revokeTokens?: boolean): void; + loadUserInfo(): void; + authorizationCallback(callbackUrl: string): void; + loadTokenFromStorage(): void; + getValidToken(buffer?: number): Promise; +} + +export class PAIAAuthService implements IAuthService { + private _authConfig?: IAuthConfig; + + private _authSubject: AuthSubject = new AuthSubject(); + + private _authSubjectV2 = new BehaviorSubject( + PAIAAuthActionBuilder.Init(), + ); + + private _tokenSubject = new BehaviorSubject( + undefined, + ); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private _userSubject = new BehaviorSubject(undefined); + + private _authenticatedSubject = new BehaviorSubject(false); + + private _initComplete = new BehaviorSubject(false); + + protected tokenHandler: PAIATokenRequestHandler; + + protected userInfoHandler: UserInfoHandler; + + protected requestHandler: PAIAAuthorizationRequestHandler; + + protected endSessionHandler: EndSessionHandler; + + public localConfiguration: AuthorizationServiceConfiguration; + + constructor( + protected browser: Browser = new DefaultBrowser(), + protected storage: StorageBackend = new LocalStorageBackend(), + protected requestor: Requestor = new JQueryRequestor(), + utils = new BasicQueryStringUtils(), + ) { + this.tokenHandler = new PAIATokenRequestHandler(requestor); + this.userInfoHandler = new IonicUserInfoHandler(requestor); + this.requestHandler = new PAIAAuthorizationRequestHandler( + browser, + storage, + utils, + crypto, + ); + this.endSessionHandler = new IonicEndSessionHandler(browser); + } + + get token$(): Observable { + return this._tokenSubject.asObservable(); + } + + get isAuthenticated$(): Observable { + return this._authenticatedSubject.asObservable(); + } + + get initComplete$(): Observable { + return this._initComplete.asObservable(); + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + get user$(): Observable { + return this._userSubject.asObservable(); + } + + get events$(): Observable { + 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 { + if (!this.localConfiguration) + throw new Error('Local Configuration Not Defined'); + + return Promise.resolve(this.localConfiguration); + } + + public async init() { + this.setupAuthorizationNotifier(); + this.loadTokenFromStorage(); + } + + protected notifyActionListers(action: IPAIAAuthAction) { + this._authSubjectV2.next(action); + this._authSubject.notify(action); + + /* 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); + break; + case AuthActions.LoadTokenFromStorageFailed: + this._tokenSubject.next(undefined); + this._userSubject.next(undefined); + this._authenticatedSubject.next(false); + this._initComplete.next(true); + break; + case AuthActions.SignInSuccess: + this._tokenSubject.next(action.tokenResponse); + this._authenticatedSubject.next(true); + break; + case AuthActions.LoadTokenFromStorageSuccess: + this._tokenSubject.next(action.tokenResponse); + this._authenticatedSubject.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; + } + } + + protected setupAuthorizationNotifier() { + const notifier = new PAIAAuthorizationNotifier(); + this.requestHandler.setAuthorizationNotifier(notifier); + notifier.setAuthorizationListener((request, response, error) => + this.onAuthorizationNotification(request, response, error), + ); + } + + protected onAuthorizationNotification( + request: AuthorizationRequest, + response: PAIAAuthorizationResponse | 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, response.patron, codeVerifier); + } else if (error != undefined) { + throw new Error(error.errorDescription); + } else { + throw new Error('Unknown Error With Authentication'); + } + } + + protected async internalAuthorizationCallback(url: string) { + this.browser.closeWindow(); + await this.storage.setItem(AUTHORIZATION_RESPONSE_KEY, url); + return this.requestHandler.completeAuthorizationRequestIfPossible(); + } + + protected async performAuthorizationRequest( + authExtras?: StringMap, + state?: string, + ): Promise { + 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, + patron: string, + codeVerifier?: string, + ): Promise { + const requestJSON: PAIATokenRequestJson = { + code: code, + patron: patron, + extras: codeVerifier + ? { + code_verifier: codeVerifier, + } + : {}, + }; + + const token: PAIATokenResponse = + await this.tokenHandler.performTokenRequest( + await this.configuration, + new PAIATokenRequest(requestJSON), + ); + await this.storage.setItem(TOKEN_KEY, JSON.stringify(token.toJson())); + this.notifyActionListers(PAIAAuthActionBuilder.SignInSuccess(token)); + } + + public async revokeTokens() { + // Note: only locally + await this.storage.removeItem(TOKEN_KEY); + this.notifyActionListers(PAIAAuthActionBuilder.RevokeTokensSuccess()); + } + + public async signOut() { + await this.revokeTokens().catch(error => + this.notifyActionListers(PAIAAuthActionBuilder.SignOutFailed(error)), + ); + this.notifyActionListers(PAIAAuthActionBuilder.SignOutSuccess()); + } + + protected async internalLoadTokenFromStorage() { + let token: PAIATokenResponse | undefined; + const tokenResponseString: string | null = await this.storage.getItem( + TOKEN_KEY, + ); + + if (tokenResponseString != undefined) { + token = new PAIATokenResponse(JSON.parse(tokenResponseString)); + + if (token) { + return this.notifyActionListers( + PAIAAuthActionBuilder.LoadTokenFromStorageSuccess(token), + ); + } + } + + throw new Error('No Token In Storage'); + } + + protected async internalRequestUserInfo() { + if (this._tokenSubject.value) { + const userInfo = await this.userInfoHandler.performUserInfoRequest( + await this.configuration, + this._tokenSubject.value, + ); + this.notifyActionListers( + PAIAAuthActionBuilder.LoadUserInfoSuccess(userInfo), + ); + } else { + throw new Error('No Token Available'); + } + } + + public async loadTokenFromStorage() { + await this.internalLoadTokenFromStorage().catch(error => { + this.notifyActionListers( + PAIAAuthActionBuilder.LoadTokenFromStorageFailed(error), + ); + }); + } + + public async signIn(authExtras?: StringMap, state?: string) { + await this.performAuthorizationRequest(authExtras, state).catch(error => { + this.notifyActionListers(PAIAAuthActionBuilder.SignInFailed(error)); + }); + } + + public async loadUserInfo() { + await this.internalRequestUserInfo().catch(error => { + this.notifyActionListers(PAIAAuthActionBuilder.LoadUserInfoFailed(error)); + }); + } + + public authorizationCallback(callbackUrl: string): void { + this.internalAuthorizationCallback(callbackUrl).catch(error => { + this.notifyActionListers(PAIAAuthActionBuilder.SignInFailed(error)); + }); + } + + public async getValidToken( + buffer: number = AUTH_EXPIRY_BUFFER, + ): Promise { + if (this._tokenSubject.value && this._tokenSubject.value.isValid(buffer)) { + return this._tokenSubject.value; + } + + throw new Error('Unable To Obtain Valid Token'); + } +} diff --git a/src/app/modules/auth/paia/paia-authorization-listener.ts b/src/app/modules/auth/paia/paia-authorization-listener.ts new file mode 100644 index 00000000..c2d346d0 --- /dev/null +++ b/src/app/modules/auth/paia/paia-authorization-listener.ts @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2021 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 . + */ + +import {AuthorizationError, AuthorizationRequest} from '@openid/appauth'; +import {PAIAAuthorizationResponse} from './paia-authorization-response'; + +export type PAIAAuthorizationListener = ( + request: AuthorizationRequest, + response: PAIAAuthorizationResponse | null, + error: AuthorizationError | null, +) => void; diff --git a/src/app/modules/auth/paia/paia-authorization-notifier.ts b/src/app/modules/auth/paia/paia-authorization-notifier.ts new file mode 100644 index 00000000..4b2b5613 --- /dev/null +++ b/src/app/modules/auth/paia/paia-authorization-notifier.ts @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2021 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 . + */ +import {PAIAAuthorizationListener} from './paia-authorization-listener'; +import {AuthorizationError, AuthorizationRequest} from '@openid/appauth'; +import {PAIAAuthorizationResponse} from './paia-authorization-response'; + +export class PAIAAuthorizationNotifier { + // eslint-disable-next-line unicorn/no-null + private listener: PAIAAuthorizationListener | null = null; + + setAuthorizationListener(listener: PAIAAuthorizationListener) { + this.listener = listener; + } + + /** + * The authorization complete callback. + */ + onAuthorizationComplete( + request: AuthorizationRequest, + response: PAIAAuthorizationResponse | null, + error: AuthorizationError | null, + ): void { + if (this.listener) { + // complete authorization request + this.listener(request, response, error); + } + } +} diff --git a/src/app/modules/auth/paia/paia-authorization-response.ts b/src/app/modules/auth/paia/paia-authorization-response.ts new file mode 100644 index 00000000..e71999dd --- /dev/null +++ b/src/app/modules/auth/paia/paia-authorization-response.ts @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2021 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 . + */ + +export interface PAIAAuthorizationResponseJson { + code: string; + state: string; + patron: string; +} + +export class PAIAAuthorizationResponse { + code: string; + + state: string; + + patron: string; + + constructor(response: PAIAAuthorizationResponseJson) { + this.code = response.code; + this.state = response.state; + this.patron = response.patron; + } + + toJson(): PAIAAuthorizationResponseJson { + return {code: this.code, state: this.state, patron: this.patron}; + } +} diff --git a/src/app/modules/auth/paia/paia-token-request.ts b/src/app/modules/auth/paia/paia-token-request.ts new file mode 100644 index 00000000..4b12b2f8 --- /dev/null +++ b/src/app/modules/auth/paia/paia-token-request.ts @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2021 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 . + */ +import {StringMap} from '@openid/appauth'; + +// TODO: add documentation +export interface PAIATokenRequestJson { + code: string; + patron: string; + extras?: StringMap; +} + +export class PAIATokenRequest { + code: string; + + patron: string; + + extras?: StringMap; + + constructor(request: PAIATokenRequestJson) { + this.code = request.code; + this.patron = request.patron; + this.extras = request.extras; + } + + /** + * Serializes a TokenRequest to a JavaScript object. + */ + toJson(): PAIATokenRequestJson { + return { + code: this.code, + patron: this.patron, + extras: this.extras, + }; + } + + toStringMap(): StringMap { + const map: StringMap = { + patron: this.patron, + code: this.code, + }; + + // copy over extras + if (this.extras) { + for (const extra in this.extras) { + if (this.extras.hasOwnProperty(extra) && !map.hasOwnProperty(extra)) { + // check before inserting to requestMap + map[extra] = this.extras[extra]; + } + } + } + return map; + } +} diff --git a/src/app/modules/auth/paia/paia-token-response.ts b/src/app/modules/auth/paia/paia-token-response.ts new file mode 100644 index 00000000..f08ee811 --- /dev/null +++ b/src/app/modules/auth/paia/paia-token-response.ts @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2021 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 . + */ + +import {TokenResponse, TokenResponseJson} from '@openid/appauth'; +import {nowInSeconds} from '@openid/appauth'; + +const AUTH_EXPIRY_BUFFER = 10 * 60 * -1; // 10 mins in seconds + +export interface PAIATokenResponseJson extends TokenResponseJson { + patron: string; +} + +export class PAIATokenResponse extends TokenResponse { + patron: string; + + constructor(response: PAIATokenResponseJson) { + super(response); + this.patron = response.patron; + } + + toJson(): PAIATokenResponseJson { + return { + access_token: this.accessToken, + id_token: this.idToken, + refresh_token: this.refreshToken, + scope: this.scope, + token_type: this.tokenType, + issued_at: this.issuedAt, + expires_in: this.expiresIn?.toString(), + patron: this.patron, + }; + } + + isValid(buffer: number = AUTH_EXPIRY_BUFFER): boolean { + if (this.expiresIn) { + const now = nowInSeconds(); + return now < this.issuedAt + this.expiresIn + buffer; + } else { + return true; + } + } +} diff --git a/src/app/modules/auth/paia/paia-user-info-handler.ts b/src/app/modules/auth/paia/paia-user-info-handler.ts new file mode 100644 index 00000000..48f949b5 --- /dev/null +++ b/src/app/modules/auth/paia/paia-user-info-handler.ts @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2021 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 . + */ +import {AuthorizationServiceConfiguration, Requestor} from '@openid/appauth'; +import {PAIATokenResponse} from './paia-token-response'; + +export interface UserInfoHandler { + performUserInfoRequest( + configuration: AuthorizationServiceConfiguration, + token: PAIATokenResponse, + ): Promise; +} + +export class PAIAUserInfoHandler implements UserInfoHandler { + constructor(private requestor: Requestor) {} + + public async performUserInfoRequest( + configuration: AuthorizationServiceConfiguration, + token: PAIATokenResponse, + ): Promise { + const settings: JQueryAjaxSettings = { + url: `${configuration.userInfoEndpoint}/${token.patron}`, + method: 'GET', + headers: { + Authorization: `${ + token.tokenType == 'bearer' ? 'Bearer' : token.tokenType + } ${token.accessToken}`, + }, + }; + + return this.requestor.xhr(settings); + } +} diff --git a/src/app/modules/auth/paia/token-request-handler.ts b/src/app/modules/auth/paia/token-request-handler.ts new file mode 100644 index 00000000..8fc4e697 --- /dev/null +++ b/src/app/modules/auth/paia/token-request-handler.ts @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2021 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 . + */ +import { + TokenErrorJson, + AuthorizationServiceConfiguration, + RevokeTokenRequest, + Requestor, + JQueryRequestor, + BasicQueryStringUtils, + QueryStringUtils, + AppAuthError, + TokenError, +} from '@openid/appauth'; +import {PAIATokenRequest} from './paia-token-request'; +import {PAIATokenResponse, PAIATokenResponseJson} from './paia-token-response'; + +export class PAIATokenRequestHandler { + constructor( + public readonly requestor: Requestor = new JQueryRequestor(), + public readonly utils: QueryStringUtils = new BasicQueryStringUtils(), + ) {} + + private isTokenResponse( + response: PAIATokenResponseJson | TokenErrorJson, + ): response is PAIATokenResponseJson { + return (response as TokenErrorJson).error === undefined; + } + + performRevokeTokenRequest( + configuration: AuthorizationServiceConfiguration, + request: RevokeTokenRequest, + ): Promise { + const revokeTokenResponse = this.requestor.xhr({ + url: configuration.revocationEndpoint, + method: 'GET', + // headers: {'Content-Type': 'application/x-www-form-urlencoded'}, + data: this.utils.stringify(request.toStringMap()), + }); + + return revokeTokenResponse.then(_response => { + return true; + }); + } + + performTokenRequest( + configuration: AuthorizationServiceConfiguration, + request: PAIATokenRequest, + ): Promise { + const tokenResponse = this.requestor.xhr< + PAIATokenResponseJson | TokenErrorJson + >({ + url: configuration.tokenEndpoint, + method: 'POST', + data: { + patron: request.patron, + grant_type: 'client_credentials', + }, + headers: { + 'Authorization': `Basic ${request.code}`, + 'Content-Type': 'application/json', + }, + }); + + return tokenResponse.then(response => { + return this.isTokenResponse(response) + ? new PAIATokenResponse(response) + : Promise.reject( + new AppAuthError(response.error, new TokenError(response)), + ); + }); + } +} diff --git a/src/app/modules/auth/user-info.model.ts b/src/app/modules/auth/user-info.model.ts new file mode 100644 index 00000000..c8bf3aff --- /dev/null +++ b/src/app/modules/auth/user-info.model.ts @@ -0,0 +1,21 @@ +/* + * 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 . + */ + +export interface IUserInfo { + display_name: string; + role: string; + email: string; + user_name: string; +} diff --git a/src/app/modules/menu/navigation/navigation.component.ts b/src/app/modules/menu/navigation/navigation.component.ts index d206edd9..d959d1ee 100644 --- a/src/app/modules/menu/navigation/navigation.component.ts +++ b/src/app/modules/menu/navigation/navigation.component.ts @@ -12,7 +12,7 @@ * You should have received a copy of the GNU General Public License along with * this program. If not, see . */ -import {Component} from '@angular/core'; +import {Component, OnInit} from '@angular/core'; import {LangChangeEvent, TranslateService} from '@ngx-translate/core'; import { SCAppConfigurationMenuCategory, @@ -20,8 +20,7 @@ import { SCThingTranslator, SCTranslations, } from '@openstapps/core'; -import {NGXLogger} from 'ngx-logger'; -import {ConfigProvider} from '../../config/config.provider'; +import {NavigationService} from './navigation.service'; /** * Generated class for the MenuPage page. @@ -34,7 +33,7 @@ import {ConfigProvider} from '../../config/config.provider'; styleUrls: ['navigation.scss'], templateUrl: 'navigation.html', }) -export class NavigationComponent { +export class NavigationComponent implements OnInit { /** * Possible languages to be used for translation */ @@ -60,11 +59,9 @@ export class NavigationComponent { translator: SCThingTranslator; constructor( - private readonly configProvider: ConfigProvider, public translateService: TranslateService, - private readonly logger: NGXLogger, + private navigationService: NavigationService, ) { - void this.loadMenuEntries(); translateService.onLangChange.subscribe((event: LangChangeEvent) => { this.language = event.lang as keyof SCTranslations; this.translator = new SCThingTranslator(this.language); @@ -72,20 +69,7 @@ export class NavigationComponent { this.translator = new SCThingTranslator('en'); } - /** - * Loads menu entries from configProvider - */ - async loadMenuEntries() { - try { - this.menu = (await this.configProvider.getValue( - 'menus', - )) as SCAppConfigurationMenuCategory[]; - } catch (error) { - this.logger.error(`error from loading menu entries: ${error}`); - } + async ngOnInit() { + this.menu = await this.navigationService.getMenu(); } - - // openPage(page) { - // this.nav.setRoot(page.component); - // } } diff --git a/src/app/modules/menu/navigation/navigation.service.ts b/src/app/modules/menu/navigation/navigation.service.ts new file mode 100644 index 00000000..b12c824b --- /dev/null +++ b/src/app/modules/menu/navigation/navigation.service.ts @@ -0,0 +1,40 @@ +import {Injectable} from '@angular/core'; +import {SCAppConfigurationMenuCategory} from '@openstapps/core'; +import {ConfigProvider} from '../../config/config.provider'; +import {NGXLogger} from 'ngx-logger'; + +@Injectable({ + providedIn: 'root', +}) +export class NavigationService { + constructor( + private configProvider: ConfigProvider, + private logger: NGXLogger, + ) {} + + async getMenu() { + let menu: SCAppConfigurationMenuCategory[] = []; + try { + menu = (await this.configProvider.getValue( + 'menus', + )) as SCAppConfigurationMenuCategory[]; + } catch (error) { + this.logger.error(`error from loading menu entries: ${error}`); + } + // TODO: Load if from the backend (config) + menu[1].items.unshift({ + icon: 'person', + route: '/profile', + title: 'profile', + translations: { + de: { + title: 'Profil', + }, + en: { + title: 'profile', + }, + }, + }); + return menu; + } +} diff --git a/src/app/modules/profile/page/profile-page.component.html b/src/app/modules/profile/page/profile-page.component.html new file mode 100644 index 00000000..5a485b64 --- /dev/null +++ b/src/app/modules/profile/page/profile-page.component.html @@ -0,0 +1,75 @@ + + + + + + + {{ 'profile.title' | translate | titlecase }} + + + + +
+ + + + + + + + + + + + + + + {{ userInfo.givenName }} + {{ userInfo.familyName }} + + + {{ userInfo.email }} + {{ + 'profile.userInfo.studentId' | translate | titlecase + }}: {{ userInfo.studentId }} + + + + + + + + + + + + + + + +
+
diff --git a/src/app/modules/profile/page/profile-page.component.scss b/src/app/modules/profile/page/profile-page.component.scss new file mode 100644 index 00000000..8779ae05 --- /dev/null +++ b/src/app/modules/profile/page/profile-page.component.scss @@ -0,0 +1,25 @@ +:host { + ion-col.login { + text-align: center; + a { + cursor: pointer; + } + } + ion-thumbnail { + background: var(--placeholder-gray); + --size: 64px; + align-items: center; + padding: 10px; + margin: 0; + ion-icon { + width: 100%; + height: 100%; + color: white; + display: block; + } + } + ion-row.main-info { + font-weight: bold; + margin-bottom: 2px; + } +} diff --git a/src/app/modules/profile/page/profile-page.component.spec.ts b/src/app/modules/profile/page/profile-page.component.spec.ts new file mode 100644 index 00000000..08e295eb --- /dev/null +++ b/src/app/modules/profile/page/profile-page.component.spec.ts @@ -0,0 +1,50 @@ +/* + * 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 . + */ + +import {CUSTOM_ELEMENTS_SCHEMA} from '@angular/core'; +import {async, ComponentFixture, TestBed} from '@angular/core/testing'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {RouterTestingModule} from '@angular/router/testing'; +import {AuthModule} from '../../auth/auth.module'; +import {ProfilePageComponent} from './profile-page.component'; +import {TranslateModule} from '@ngx-translate/core'; + +describe('ProfilePage', () => { + let component: ProfilePageComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ProfilePageComponent], + imports: [ + HttpClientTestingModule, + RouterTestingModule, + AuthModule, + TranslateModule.forRoot(), + ], + schemas: [CUSTOM_ELEMENTS_SCHEMA], + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ProfilePageComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/modules/profile/page/profile-page.component.ts b/src/app/modules/profile/page/profile-page.component.ts new file mode 100644 index 00000000..3a675e93 --- /dev/null +++ b/src/app/modules/profile/page/profile-page.component.ts @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2021-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 . + */ + +import {Component, OnDestroy, OnInit} from '@angular/core'; +import {IonicUserInfoHandler} from 'ionic-appauth'; +import {DefaultAuthService} from '../../auth/default-auth.service'; +import {Requestor, TokenResponse} from '@openid/appauth'; +import {PAIAAuthService} from '../../auth/paia/paia-auth.service'; +import {SCAuthorizationProviderType, SCUserConfiguration} from '../user'; +import {Subscription} from 'rxjs'; +import {AuthHelperService} from '../../auth/auth-helper.service'; + +@Component({ + selector: 'app-home', + templateUrl: './profile-page.component.html', + styleUrls: ['./profile-page.component.scss'], +}) +export class ProfilePageComponent implements OnInit, OnDestroy { + data: {[key in SCAuthorizationProviderType]: {loggedIn: boolean}} = { + default: {loggedIn: false}, + paia: {loggedIn: false}, + }; + + userInfo?: SCUserConfiguration; + + subscriptions: Subscription[] = []; + + constructor( + private defaultAuth: DefaultAuthService, + private paiaAuth: PAIAAuthService, + private requestor: Requestor, + private authHelperService: AuthHelperService, + ) {} + + ngOnInit() { + this.subscriptions.push( + this.defaultAuth.token$.subscribe(_token => { + this.defaultAuth + .getValidToken() + .then(token => { + this.data.default.loggedIn = true; + this.getUserInfo(token); + }) + .catch(_error => { + this.data.default.loggedIn = false; + }); + }), + this.paiaAuth.token$.subscribe(_token => { + this.paiaAuth + .getValidToken() + .then(_token => { + this.data.paia.loggedIn = true; + }) + .catch(_error => { + this.data.paia.loggedIn = false; + }); + }), + ); + } + + getUserInfo(token: TokenResponse) { + const userInfoHandler = new IonicUserInfoHandler(this.requestor); + + userInfoHandler + .performUserInfoRequest(this.defaultAuth.localConfiguration, token) + .then(userInfo => { + this.userInfo = this.authHelperService.getUserFromUserInfo(userInfo); + }); + } + + public signIn() { + this.defaultAuth.signIn(); + } + + signInPAIA() { + this.paiaAuth.signIn(); + } + + async signOut() { + await this.defaultAuth.signOut(); + this.userInfo = undefined; + } + + async signOutPAIA() { + await this.paiaAuth.signOut(); + } + + ngOnDestroy(): void { + for (const subscription of this.subscriptions) { + subscription.unsubscribe(); + } + } +} diff --git a/src/app/modules/profile/profile.module.ts b/src/app/modules/profile/profile.module.ts new file mode 100644 index 00000000..fae13a46 --- /dev/null +++ b/src/app/modules/profile/profile.module.ts @@ -0,0 +1,26 @@ +import {NgModule} from '@angular/core'; +import {CommonModule} from '@angular/common'; +import {FormsModule} from '@angular/forms'; +import {Routes, RouterModule} from '@angular/router'; +import {IonicModule} from '@ionic/angular'; +import {ProfilePageComponent} from './page/profile-page.component'; +import {TranslateModule} from '@ngx-translate/core'; + +const routes: Routes = [ + { + path: 'profile', + component: ProfilePageComponent, + }, +]; + +@NgModule({ + declarations: [ProfilePageComponent], + imports: [ + CommonModule, + FormsModule, + IonicModule, + RouterModule.forChild(routes), + TranslateModule, + ], +}) +export class ProfilePageModule {} diff --git a/src/app/modules/profile/user.ts b/src/app/modules/profile/user.ts new file mode 100644 index 00000000..7ef39ffb --- /dev/null +++ b/src/app/modules/profile/user.ts @@ -0,0 +1,82 @@ +/* + * 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 . + */ + +/** + * TODO: Take it from the StApps Core + */ +import {SCAcademicPriceGroup} from '@openstapps/core'; + +/** + * A user configuration + */ +export interface SCUserConfiguration { + /** + * User's e-mail + */ + email?: string; + + /** + * User's family name + */ + familyName?: string; + + /** + * User's given name + */ + givenName?: string; + + /** + * ID given to the user + */ + id: string; + + /** + * The complete name of the user combining all the parts of the name into one + */ + name: string; + + /** + * Role assigned to the user + */ + role: keyof SCAcademicPriceGroup; + + /** + * Student ID given to the user + */ + studentId?: string; +} + +/** + * TODO: Take it from the backend's config + */ +type mapping = {[key in keyof SCUserConfiguration]: string}; + +/** + * TODO: Take it from the backend's config + */ +export const userMapping: mapping = { + id: 'id', + name: 'sn', + role: 'attributes.eduPersonPrimaryAffiliation', + email: 'attributes.mailPrimaryAddress', + studentId: 'attributes.employeeNumber', + givenName: 'attributes.givenName', + familyName: 'attributes.sn', +}; + +/** + * TODO: Take it from the StApps Core + */ +export type SCAuthorizationProviderType = 'default' | 'paia'; diff --git a/src/assets/i18n/de.json b/src/assets/i18n/de.json index e2cf620e..429d8411 100644 --- a/src/assets/i18n/de.json +++ b/src/assets/i18n/de.json @@ -13,6 +13,20 @@ "UNKNOWN": "Unbekannter Fehler." } }, + "auth": { + "messages": { + "default": { + "authorizing": "Autorisierung läuft...", + "logged_in_success": "Erfolgreich eingeloggt.", + "logged_out_success": "Erfolgreich ausgeloggt." + }, + "paia": { + "authorizing": "Autorisierung (Bibliothek) läuft...", + "logged_in_success": "Erfolgreich ins Bibliothekskonto eingeloggt.", + "logged_out_success": "Erfolgreich aus dem Bibliothekskonto ausgeloggt." + } + } + }, "common": { "openingHours": { "closed_until": "Geschlossen bis", @@ -202,6 +216,21 @@ "pastEvent": "Event ist vorbei" } }, + "profile": { + "buttons": { + "default": { + "log_in": "Login", + "log_out": "Ausloggen" + }, + "paia": { + "log_in": "Login Bibliothek", + "log_out": "Ausloggen (Bibliothek)" + } + }, + "userInfo": { + "studentId": "Matrikelnr." + } + }, "settings": { "resetAlert.title": "Alle Einstellungen zurücksetzen?", "resetAlert.message": "Sind Sie sich sicher, alle Einstellungen auf ihre Anfangswerte zurückzusetzen?", diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index d7580ff0..952c7dc5 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -13,6 +13,20 @@ "UNKNOWN": "Unknown problem." } }, + "auth": { + "messages": { + "default": { + "authorizing": "Authorizing...", + "logged_in_success": "Successfully logged in.", + "logged_out_success": "Successfully logged out." + }, + "paia": { + "authorizing": "Authorizing (library)...", + "logged_in_success": "Successfully logged in to library.", + "logged_out_success": "Successfully logged out from library." + } + } + }, "common": { "openingHours": { "closed_until": "Closed until", @@ -202,6 +216,22 @@ "pastEvent": "Event is over" } }, + "profile": { + "title": "Profile", + "buttons": { + "default": { + "log_in": "Log In", + "log_out": "Log Out" + }, + "paia": { + "log_in": "Library Login", + "log_out": "Log Out (Library)" + } + }, + "userInfo": { + "studentId": "Matriculation Nr." + } + }, "settings": { "resetAlert.title": "Reset all settings?", "resetAlert.message": "Are you sure to reset all settings to their default values?", diff --git a/src/environments/environment.fake.ts b/src/environments/environment.fake.ts index 29f6a500..f7e1931a 100644 --- a/src/environments/environment.fake.ts +++ b/src/environments/environment.fake.ts @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2020 StApps + * Copyright (C) 2018-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. @@ -12,9 +12,74 @@ * You should have received a copy of the GNU General Public License along with * this program. If not, see . */ +// The file contents for the current environment will overwrite these during build. +// The build system defaults to the dev environment which uses `environment.ts`, but if you do +// `ng build --env=prod` then `environment.prod.ts` will be used instead. +// The list of which env maps to which file can be found in `.angular-cli.json`. +import {AuthorizationServiceConfigurationJson} from '@openid/appauth'; +import {IAuthConfig} from 'ionic-appauth'; +// import config from 'capacitor.config'; + +const appDomain = 'mobile.app.uni-frankfurt.de'; + export const environment = { backend_url: 'https://mobile.server.uni-frankfurt.de', + appDomain: 'mobile.app.uni-frankfurt.de', backend_version: '2.0.0', use_fake_backend: true, production: false, + oauth2: { + client: { + his: { + client_id: '1cac3f99-33fa-4234-8438-979f07e0cdab', + client_secret: 'CLIENT_SECRET', + server_host: 'https://cas.rz.uni-frankfurt.de/cas/oauth2.0', + redirect_url: `https://${appDomain}/auth/callback`, + scopes: '', + pkce: true, + } as IAuthConfig, + paia: { + client_id: '', + client_secret: '', + server_host: + 'https://hds.hebis.de/Shibboleth.sso/UBFFM?target=https://hds.hebis.de/ubffm/paia_login_stub.php', + // TODO: Use Custom URL Scheme (ideally bundle ID from capacitor.config) + redirect_url: `https://${appDomain}/auth/paia/callback`, + scopes: '', + pkce: true, + } as IAuthConfig, + }, + service: { + his: { + authorization_endpoint: + 'https://cas.rz.uni-frankfurt.de/cas/oauth2.0/authorize', + token_endpoint: + 'https://cas.rz.uni-frankfurt.de/cas/oauth2.0/accessToken', + userinfo_endpoint: + 'https://cas.rz.uni-frankfurt.de/cas/oauth2.0/profile', + } as AuthorizationServiceConfigurationJson, + paia: { + authorization_endpoint: + 'https://hds.hebis.de/Shibboleth.sso/UBFFM?target=https://hds.hebis.de/ubffm/paia_login_stub.php', + token_endpoint: 'https://hds.hebis.de:8443/auth/login', + userinfo_endpoint: 'https://hds.hebis.de:8443/core', + } as AuthorizationServiceConfigurationJson, + }, + endpointMappings: { + userinfo: { + id: 'employeeNumber', + given_name: 'givenName', + family_name: 'sn', + email: 'mailPrimaryAddress', + }, + }, + }, }; + +/* + * In development mode, to ignore zone related error stack frames such as + * `zone.run`, `zoneDelegate.invokeTask` for easier debugging, you can + * import the following file, but please comment it out in production mode + * because it will have performance impact when throw error + */ +// import 'zone.js/plugins/zone-error'; // Included with Angular CLI. diff --git a/src/environments/environment.prod.ts b/src/environments/environment.prod.ts index 1749f661..74ec7a3c 100644 --- a/src/environments/environment.prod.ts +++ b/src/environments/environment.prod.ts @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2020 StApps + * Copyright (C) 2018-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. @@ -12,10 +12,74 @@ * You should have received a copy of the GNU General Public License along with * this program. If not, see . */ -// eslint-disable-next-line unicorn/prevent-abbreviations +// The file contents for the current environment will overwrite these during build. +// The build system defaults to the dev environment which uses `environment.ts`, but if you do +// `ng build --env=prod` then `environment.prod.ts` will be used instead. +// The list of which env maps to which file can be found in `.angular-cli.json`. +import {AuthorizationServiceConfigurationJson} from '@openid/appauth'; +import {IAuthConfig} from 'ionic-appauth'; +// import config from 'capacitor.config'; + +const appDomain = 'mobile.app.uni-frankfurt.de'; + export const environment = { backend_url: 'https://mobile.server.uni-frankfurt.de', + appDomain: 'mobile.app.uni-frankfurt.de', backend_version: '2.0.0', use_fake_backend: false, - production: true, + production: false, + oauth2: { + client: { + his: { + client_id: '1cac3f99-33fa-4234-8438-979f07e0cdab', + client_secret: 'CLIENT_SECRET', + server_host: 'https://cas.rz.uni-frankfurt.de/cas/oauth2.0', + redirect_url: `https://${appDomain}/auth/callback`, + scopes: '', + pkce: true, + } as IAuthConfig, + paia: { + client_id: '', + client_secret: '', + server_host: + 'https://hds.hebis.de/Shibboleth.sso/UBFFM?target=https://hds.hebis.de/ubffm/paia_login_stub.php', + // TODO: Use Custom URL Scheme (ideally bundle ID from capacitor.config) + redirect_url: `https://${appDomain}/auth/paia/callback`, + scopes: '', + pkce: true, + } as IAuthConfig, + }, + service: { + his: { + authorization_endpoint: + 'https://cas.rz.uni-frankfurt.de/cas/oauth2.0/authorize', + token_endpoint: + 'https://cas.rz.uni-frankfurt.de/cas/oauth2.0/accessToken', + userinfo_endpoint: + 'https://cas.rz.uni-frankfurt.de/cas/oauth2.0/profile', + } as AuthorizationServiceConfigurationJson, + paia: { + authorization_endpoint: + 'https://hds.hebis.de/Shibboleth.sso/UBFFM?target=https://hds.hebis.de/ubffm/paia_login_stub.php', + token_endpoint: 'https://hds.hebis.de:8443/auth/login', + userinfo_endpoint: 'https://hds.hebis.de:8443/core', + } as AuthorizationServiceConfigurationJson, + }, + endpointMappings: { + userinfo: { + id: 'employeeNumber', + given_name: 'givenName', + family_name: 'sn', + email: 'mailPrimaryAddress', + }, + }, + }, }; + +/* + * In development mode, to ignore zone related error stack frames such as + * `zone.run`, `zoneDelegate.invokeTask` for easier debugging, you can + * import the following file, but please comment it out in production mode + * because it will have performance impact when throw error + */ +// import 'zone.js/plugins/zone-error'; // Included with Angular CLI. diff --git a/src/environments/environment.ts b/src/environments/environment.ts index 1e51857c..341ed618 100644 --- a/src/environments/environment.ts +++ b/src/environments/environment.ts @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2020 StApps + * Copyright (C) 2018-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. @@ -16,11 +16,65 @@ // The build system defaults to the dev environment which uses `environment.ts`, but if you do // `ng build --env=prod` then `environment.prod.ts` will be used instead. // The list of which env maps to which file can be found in `.angular-cli.json`. +import {AuthorizationServiceConfigurationJson} from '@openid/appauth'; +import {IAuthConfig} from 'ionic-appauth'; +// import config from 'capacitor.config'; + +const appDomain = 'mobile.app.uni-frankfurt.de'; + export const environment = { backend_url: 'https://mobile.server.uni-frankfurt.de', + appDomain: 'mobile.app.uni-frankfurt.de', backend_version: '2.0.0', use_fake_backend: false, production: false, + oauth2: { + client: { + his: { + client_id: '1cac3f99-33fa-4234-8438-979f07e0cdab', + client_secret: '', + server_host: 'https://cas.rz.uni-frankfurt.de/cas/oauth2.0', + redirect_url: `https://${appDomain}/auth/callback`, + scopes: '', + pkce: true, + } as IAuthConfig, + paia: { + client_id: '', + client_secret: '', + server_host: + 'https://hds.hebis.de/Shibboleth.sso/UBFFM?target=https://hds.hebis.de/ubffm/paia_login_stub.php', + // TODO: Use Custom URL Scheme (ideally bundle ID from capacitor.config) + redirect_url: `https://${appDomain}/auth/paia/callback`, + scopes: '', + // TODO: PAIA need to support PKCE, it will then work "out-of-the-box" + pkce: true, + } as IAuthConfig, + }, + service: { + his: { + authorization_endpoint: + 'https://cas.rz.uni-frankfurt.de/cas/oauth2.0/authorize', + token_endpoint: + 'https://cas.rz.uni-frankfurt.de/cas/oauth2.0/accessToken', + userinfo_endpoint: + 'https://cas.rz.uni-frankfurt.de/cas/oauth2.0/profile', + } as AuthorizationServiceConfigurationJson, + paia: { + authorization_endpoint: + 'https://hds.hebis.de/Shibboleth.sso/UBFFM?target=https://hds.hebis.de/ubffm/paia_login_stub.php', + token_endpoint: 'https://hds.hebis.de:8443/auth/login', + userinfo_endpoint: 'https://hds.hebis.de:8443/core', + } as AuthorizationServiceConfigurationJson, + }, + endpointMappings: { + userinfo: { + id: 'employeeNumber', + given_name: 'givenName', + family_name: 'sn', + email: 'mailPrimaryAddress', + }, + }, + }, }; /*