李双才 3 years ago
parent
commit
e1a5bee49a
100 changed files with 10363 additions and 2716 deletions
  1. 96 0
      android/.gitignore
  2. 0 3
      android/.idea/.gitignore
  3. 0 0
      android/.idea/gradle.properties
  4. 2 6
      android/.idea/misc.xml
  5. 2 0
      android/app/.gitignore
  6. 55 0
      android/app/build.gradle
  7. 22 0
      android/app/capacitor.build.gradle
  8. 21 0
      android/app/proguard-rules.pro
  9. 26 0
      android/app/src/androidTest/java/com/getcapacitor/myapp/ExampleInstrumentedTest.java
  10. 50 0
      android/app/src/main/AndroidManifest.xml
  11. 6 0
      android/app/src/main/assets/capacitor.config.json
  12. 18 0
      android/app/src/main/assets/capacitor.plugins.json
  13. 5 0
      android/app/src/main/java/io/ionic/starter/MainActivity.java
  14. BIN
      android/app/src/main/res/drawable-land-hdpi/splash.png
  15. BIN
      android/app/src/main/res/drawable-land-mdpi/splash.png
  16. BIN
      android/app/src/main/res/drawable-land-xhdpi/splash.png
  17. BIN
      android/app/src/main/res/drawable-land-xxhdpi/splash.png
  18. BIN
      android/app/src/main/res/drawable-land-xxxhdpi/splash.png
  19. BIN
      android/app/src/main/res/drawable-port-hdpi/splash.png
  20. BIN
      android/app/src/main/res/drawable-port-mdpi/splash.png
  21. BIN
      android/app/src/main/res/drawable-port-xhdpi/splash.png
  22. BIN
      android/app/src/main/res/drawable-port-xxhdpi/splash.png
  23. BIN
      android/app/src/main/res/drawable-port-xxxhdpi/splash.png
  24. 34 0
      android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
  25. 170 0
      android/app/src/main/res/drawable/ic_launcher_background.xml
  26. BIN
      android/app/src/main/res/drawable/splash.png
  27. 12 0
      android/app/src/main/res/layout/activity_main.xml
  28. 5 0
      android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
  29. 5 0
      android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
  30. BIN
      android/app/src/main/res/mipmap-hdpi/ic_launcher.png
  31. BIN
      android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
  32. BIN
      android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
  33. BIN
      android/app/src/main/res/mipmap-mdpi/ic_launcher.png
  34. BIN
      android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
  35. BIN
      android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
  36. BIN
      android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
  37. BIN
      android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
  38. BIN
      android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
  39. BIN
      android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
  40. BIN
      android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png
  41. BIN
      android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
  42. BIN
      android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
  43. BIN
      android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png
  44. BIN
      android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
  45. 4 0
      android/app/src/main/res/values/ic_launcher_background.xml
  46. 7 0
      android/app/src/main/res/values/strings.xml
  47. 22 0
      android/app/src/main/res/values/styles.xml
  48. 32 0
      android/app/src/main/res/xml/config.xml
  49. 5 0
      android/app/src/main/res/xml/file_paths.xml
  50. 4 0
      android/app/src/main/res/xml/network_security_config.xml
  51. 18 0
      android/app/src/test/java/com/getcapacitor/myapp/ExampleUnitTest.java
  52. 29 0
      android/build.gradle
  53. 15 0
      android/capacitor.settings.gradle
  54. 24 0
      android/gradle.properties
  55. BIN
      android/gradle/wrapper/gradle-wrapper.jar
  56. 5 0
      android/gradle/wrapper/gradle-wrapper.properties
  57. 185 0
      android/gradlew
  58. 89 0
      android/gradlew.bat
  59. 5 0
      android/settings.gradle
  60. 14 0
      android/variables.gradle
  61. 14 3
      angular.json
  62. BIN
      lc.pepk
  63. 3834 2586
      package-lock.json
  64. 38 20
      package.json
  65. 46 5
      src/app/app-routing.module.ts
  66. 0 3
      src/app/app.component.html
  67. 0 23
      src/app/app.component.spec.ts
  68. 19 5
      src/app/app.component.ts
  69. 55 7
      src/app/app.module.ts
  70. 57 0
      src/app/core/oauth2-oidc/angular-oauth-oidc.module.ts
  71. 280 0
      src/app/core/oauth2-oidc/auth.config.ts
  72. 18 0
      src/app/core/oauth2-oidc/base64-helper.ts
  73. 17 0
      src/app/core/oauth2-oidc/date-time-provider.ts
  74. 1 0
      src/app/core/oauth2-oidc/deps.d.ts
  75. 21 0
      src/app/core/oauth2-oidc/encoder.ts
  76. 52 0
      src/app/core/oauth2-oidc/events.ts
  77. 12 0
      src/app/core/oauth2-oidc/factories.ts
  78. 89 0
      src/app/core/oauth2-oidc/interceptors/default-oauth.interceptor.ts
  79. 14 0
      src/app/core/oauth2-oidc/interceptors/resource-server-error-handler.ts
  80. 14 0
      src/app/core/oauth2-oidc/oauth-module.config.ts
  81. 3169 0
      src/app/core/oauth2-oidc/oauth-service.ts
  82. 89 0
      src/app/core/oauth2-oidc/token-validation/hash-handler.ts
  83. 510 0
      src/app/core/oauth2-oidc/token-validation/js-sha256.js
  84. 153 0
      src/app/core/oauth2-oidc/token-validation/jwks-validation-handler.ts
  85. 14 0
      src/app/core/oauth2-oidc/token-validation/null-validation-handler.ts
  86. 92 0
      src/app/core/oauth2-oidc/token-validation/validation-handler.ts
  87. 4 0
      src/app/core/oauth2-oidc/tokens.ts
  88. 204 0
      src/app/core/oauth2-oidc/types.ts
  89. 59 0
      src/app/core/oauth2-oidc/url-helper.service.ts
  90. 140 0
      src/app/core/services/auth.service.ts
  91. 38 0
      src/app/core/services/button.service.ts
  92. 25 0
      src/app/core/services/menu.service.ts
  93. 22 0
      src/app/core/services/route-auth-guard.service.ts
  94. 17 0
      src/app/core/services/startup.service.ts
  95. 158 0
      src/app/core/services/storage.service.ts
  96. 101 0
      src/app/core/services/update.service.ts
  97. 0 16
      src/app/home/home-routing.module.ts
  98. 0 19
      src/app/home/home.module.ts
  99. 0 20
      src/app/home/home.page.html
  100. 0 0
      src/app/home/home.page.scss

+ 96 - 0
android/.gitignore

@@ -0,0 +1,96 @@
+# Using Android gitignore template: https://github.com/github/gitignore/blob/HEAD/Android.gitignore
+
+# Built application files
+*.apk
+*.aar
+*.ap_
+*.aab
+
+# Files for the ART/Dalvik VM
+*.dex
+
+# Java class files
+*.class
+
+# Generated files
+bin/
+gen/
+out/
+#  Uncomment the following line in case you need and you don't have the release build type files in your app
+# release/
+
+# Gradle files
+.gradle/
+build/
+
+# Local configuration file (sdk path, etc)
+local.properties
+
+# Proguard folder generated by Eclipse
+proguard/
+
+# Log Files
+*.log
+
+# Android Studio Navigation editor temp files
+.navigation/
+
+# Android Studio captures folder
+captures/
+
+# IntelliJ
+*.iml
+.idea/workspace.xml
+.idea/tasks.xml
+.idea/gradle.xml
+.idea/assetWizardSettings.xml
+.idea/dictionaries
+.idea/libraries
+# Android Studio 3 in .gitignore file.
+.idea/caches
+.idea/modules.xml
+# Comment next line if keeping position of elements in Navigation Editor is relevant for you
+.idea/navEditor.xml
+
+# Keystore files
+# Uncomment the following lines if you do not want to check your keystore files in.
+#*.jks
+#*.keystore
+
+# External native build folder generated in Android Studio 2.2 and later
+.externalNativeBuild
+.cxx/
+
+# Google Services (e.g. APIs or Firebase)
+# google-services.json
+
+# Freeline
+freeline.py
+freeline/
+freeline_project_description.json
+
+# fastlane
+fastlane/report.xml
+fastlane/Preview.html
+fastlane/screenshots
+fastlane/test_output
+fastlane/readme.md
+
+# Version control
+vcs.xml
+
+# lint
+lint/intermediates/
+lint/generated/
+lint/outputs/
+lint/tmp/
+# lint/reports/
+
+# Android Profiling
+*.hprof
+
+# Cordova plugins for Capacitor
+capacitor-cordova-android-plugins
+
+# Copied web assets
+app/src/main/assets/public

+ 0 - 3
android/.idea/.gitignore

@@ -1,3 +0,0 @@
-# Default ignored files
-/shelf/
-/workspace.xml

src/app/app.component.scss → android/.idea/gradle.properties


+ 2 - 6
android/.idea/misc.xml

@@ -1,9 +1,5 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <project version="4">
-  <component name="ProjectRootManager" version="2" project-jdk-name="Android Studio default JDK" project-jdk-type="JavaSDK">
-    <output url="file://$PROJECT_DIR$/build/classes" />
-  </component>
-  <component name="ProjectType">
-    <option name="id" value="Android" />
-  </component>
+  <component name="ExternalStorageConfigurationManager" enabled="true" />
+  <component name="ProjectRootManager" version="2" languageLevel="JDK_11" project-jdk-name="11" project-jdk-type="JavaSDK" />
 </project>

+ 2 - 0
android/app/.gitignore

@@ -0,0 +1,2 @@
+/build/*
+!/build/.npmkeep

+ 55 - 0
android/app/build.gradle

@@ -0,0 +1,55 @@
+apply plugin: 'com.android.application'
+
+android {
+    compileSdkVersion rootProject.ext.compileSdkVersion
+    lintOptions {
+        checkReleaseBuilds false
+        abortOnError false
+    }
+    defaultConfig {
+        applicationId "io.ionic.starter"
+        minSdkVersion rootProject.ext.minSdkVersion
+        targetSdkVersion rootProject.ext.targetSdkVersion
+        versionCode 5
+        versionName "5.0"
+        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+        aaptOptions {
+             // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
+             // Default: https://android.googlesource.com/platform/frameworks/base/+/282e181b58cf72b6ca770dc7ca5f91f135444502/tools/aapt/AaptAssets.cpp#61
+            ignoreAssetsPattern '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~'
+        }
+    }
+    buildTypes {
+        release {
+            minifyEnabled false
+            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+        }
+    }
+}
+
+repositories {
+    flatDir{
+        dirs '../capacitor-cordova-android-plugins/src/main/libs', 'libs'
+    }
+}
+
+dependencies {
+    implementation fileTree(include: ['*.jar'], dir: 'libs')
+    implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
+    implementation project(':capacitor-android')
+  testImplementation "junit:junit:$junitVersion"
+    androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
+    androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
+    implementation project(':capacitor-cordova-android-plugins')
+}
+
+apply from: 'capacitor.build.gradle'
+
+try {
+    def servicesJSON = file('google-services.json')
+    if (servicesJSON.text) {
+        apply plugin: 'com.google.gms.google-services'
+    }
+} catch(Exception e) {
+    logger.warn("google-services.json not found, google-services plugin not applied. Push Notifications won't work")
+}

+ 22 - 0
android/app/capacitor.build.gradle

@@ -0,0 +1,22 @@
+// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN
+
+android {
+  compileOptions {
+      sourceCompatibility JavaVersion.VERSION_1_8
+      targetCompatibility JavaVersion.VERSION_1_8
+  }
+}
+
+apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
+dependencies {
+    implementation project(':capacitor-app')
+    implementation project(':capacitor-haptics')
+    implementation project(':capacitor-keyboard')
+    implementation project(':capacitor-status-bar')
+    implementation "com.android.support:support-v4:27.+"
+}
+
+
+if (hasProperty('postBuildExtras')) {
+  postBuildExtras()
+}

+ 21 - 0
android/app/proguard-rules.pro

@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+#   http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+#   public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile

+ 26 - 0
android/app/src/androidTest/java/com/getcapacitor/myapp/ExampleInstrumentedTest.java

@@ -0,0 +1,26 @@
+package com.getcapacitor.myapp;
+
+import static org.junit.Assert.*;
+
+import android.content.Context;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.platform.app.InstrumentationRegistry;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
+ */
+@RunWith(AndroidJUnit4.class)
+public class ExampleInstrumentedTest {
+
+    @Test
+    public void useAppContext() throws Exception {
+        // Context of the app under test.
+        Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
+
+        assertEquals("com.getcapacitor.app", appContext.getPackageName());
+    }
+}

+ 50 - 0
android/app/src/main/AndroidManifest.xml

@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="io.ionic.starter">
+
+    <application
+        android:allowBackup="true"
+        android:icon="@mipmap/ic_launcher"
+        android:label="@string/app_name"
+        android:roundIcon="@mipmap/ic_launcher_round"
+        android:supportsRtl="true"
+        android:networkSecurityConfig="@xml/network_security_config"
+        android:theme="@style/AppTheme">
+
+        <activity
+            android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode"
+            android:name="io.ionic.starter.MainActivity"
+            android:label="@string/title_activity_main"
+            android:theme="@style/AppTheme.NoActionBarLaunch"
+            android:launchMode="singleTask">
+
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+
+            <intent-filter>
+                <action android:name="android.intent.action.VIEW"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+                <category android:name="android.intent.category.BROWSABLE"/>
+                <data android:scheme="http" android:host="@string/custom_url_scheme" />
+            </intent-filter>
+
+        </activity>
+
+        <provider
+            android:name="androidx.core.content.FileProvider"
+            android:authorities="${applicationId}.fileprovider"
+            android:exported="false"
+            android:grantUriPermissions="true">
+            <meta-data
+                android:name="android.support.FILE_PROVIDER_PATHS"
+                android:resource="@xml/file_paths"></meta-data>
+        </provider>
+    </application>
+
+    <!-- Permissions -->
+
+    <uses-permission android:name="android.permission.INTERNET" />
+    <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
+</manifest>

+ 6 - 0
android/app/src/main/assets/capacitor.config.json

@@ -0,0 +1,6 @@
+{
+	"appId": "io.ionic.starter",
+	"appName": "mobile-app",
+	"webDir": "www",
+	"bundledWebRuntime": false
+}

+ 18 - 0
android/app/src/main/assets/capacitor.plugins.json

@@ -0,0 +1,18 @@
+[
+	{
+		"pkg": "@capacitor/app",
+		"classpath": "com.capacitorjs.plugins.app.AppPlugin"
+	},
+	{
+		"pkg": "@capacitor/haptics",
+		"classpath": "com.capacitorjs.plugins.haptics.HapticsPlugin"
+	},
+	{
+		"pkg": "@capacitor/keyboard",
+		"classpath": "com.capacitorjs.plugins.keyboard.KeyboardPlugin"
+	},
+	{
+		"pkg": "@capacitor/status-bar",
+		"classpath": "com.capacitorjs.plugins.statusbar.StatusBarPlugin"
+	}
+]

+ 5 - 0
android/app/src/main/java/io/ionic/starter/MainActivity.java

@@ -0,0 +1,5 @@
+package io.ionic.starter;
+
+import com.getcapacitor.BridgeActivity;
+
+public class MainActivity extends BridgeActivity {}

BIN
android/app/src/main/res/drawable-land-hdpi/splash.png


BIN
android/app/src/main/res/drawable-land-mdpi/splash.png


BIN
android/app/src/main/res/drawable-land-xhdpi/splash.png


BIN
android/app/src/main/res/drawable-land-xxhdpi/splash.png


BIN
android/app/src/main/res/drawable-land-xxxhdpi/splash.png


BIN
android/app/src/main/res/drawable-port-hdpi/splash.png


BIN
android/app/src/main/res/drawable-port-mdpi/splash.png


BIN
android/app/src/main/res/drawable-port-xhdpi/splash.png


BIN
android/app/src/main/res/drawable-port-xxhdpi/splash.png


BIN
android/app/src/main/res/drawable-port-xxxhdpi/splash.png


File diff suppressed because it is too large
+ 34 - 0
android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml


+ 170 - 0
android/app/src/main/res/drawable/ic_launcher_background.xml

@@ -0,0 +1,170 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="108dp"
+    android:height="108dp"
+    android:viewportHeight="108"
+    android:viewportWidth="108">
+    <path
+        android:fillColor="#26A69A"
+        android:pathData="M0,0h108v108h-108z" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M9,0L9,108"
+        android:strokeColor="#33FFFFFF"
+        android:strokeWidth="0.8" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,0L19,108"
+        android:strokeColor="#33FFFFFF"
+        android:strokeWidth="0.8" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M29,0L29,108"
+        android:strokeColor="#33FFFFFF"
+        android:strokeWidth="0.8" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M39,0L39,108"
+        android:strokeColor="#33FFFFFF"
+        android:strokeWidth="0.8" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M49,0L49,108"
+        android:strokeColor="#33FFFFFF"
+        android:strokeWidth="0.8" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M59,0L59,108"
+        android:strokeColor="#33FFFFFF"
+        android:strokeWidth="0.8" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M69,0L69,108"
+        android:strokeColor="#33FFFFFF"
+        android:strokeWidth="0.8" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M79,0L79,108"
+        android:strokeColor="#33FFFFFF"
+        android:strokeWidth="0.8" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M89,0L89,108"
+        android:strokeColor="#33FFFFFF"
+        android:strokeWidth="0.8" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M99,0L99,108"
+        android:strokeColor="#33FFFFFF"
+        android:strokeWidth="0.8" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,9L108,9"
+        android:strokeColor="#33FFFFFF"
+        android:strokeWidth="0.8" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,19L108,19"
+        android:strokeColor="#33FFFFFF"
+        android:strokeWidth="0.8" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,29L108,29"
+        android:strokeColor="#33FFFFFF"
+        android:strokeWidth="0.8" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,39L108,39"
+        android:strokeColor="#33FFFFFF"
+        android:strokeWidth="0.8" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,49L108,49"
+        android:strokeColor="#33FFFFFF"
+        android:strokeWidth="0.8" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,59L108,59"
+        android:strokeColor="#33FFFFFF"
+        android:strokeWidth="0.8" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,69L108,69"
+        android:strokeColor="#33FFFFFF"
+        android:strokeWidth="0.8" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,79L108,79"
+        android:strokeColor="#33FFFFFF"
+        android:strokeWidth="0.8" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,89L108,89"
+        android:strokeColor="#33FFFFFF"
+        android:strokeWidth="0.8" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,99L108,99"
+        android:strokeColor="#33FFFFFF"
+        android:strokeWidth="0.8" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,29L89,29"
+        android:strokeColor="#33FFFFFF"
+        android:strokeWidth="0.8" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,39L89,39"
+        android:strokeColor="#33FFFFFF"
+        android:strokeWidth="0.8" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,49L89,49"
+        android:strokeColor="#33FFFFFF"
+        android:strokeWidth="0.8" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,59L89,59"
+        android:strokeColor="#33FFFFFF"
+        android:strokeWidth="0.8" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,69L89,69"
+        android:strokeColor="#33FFFFFF"
+        android:strokeWidth="0.8" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,79L89,79"
+        android:strokeColor="#33FFFFFF"
+        android:strokeWidth="0.8" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M29,19L29,89"
+        android:strokeColor="#33FFFFFF"
+        android:strokeWidth="0.8" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M39,19L39,89"
+        android:strokeColor="#33FFFFFF"
+        android:strokeWidth="0.8" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M49,19L49,89"
+        android:strokeColor="#33FFFFFF"
+        android:strokeWidth="0.8" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M59,19L59,89"
+        android:strokeColor="#33FFFFFF"
+        android:strokeWidth="0.8" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M69,19L69,89"
+        android:strokeColor="#33FFFFFF"
+        android:strokeWidth="0.8" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M79,19L79,89"
+        android:strokeColor="#33FFFFFF"
+        android:strokeWidth="0.8" />
+</vector>

BIN
android/app/src/main/res/drawable/splash.png


+ 12 - 0
android/app/src/main/res/layout/activity_main.xml

@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    tools:context=".MainActivity">
+
+    <WebView
+        android:layout_width="match_parent"
+        android:layout_height="match_parent" />
+</androidx.coordinatorlayout.widget.CoordinatorLayout>

+ 5 - 0
android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml

@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+    <background android:drawable="@color/ic_launcher_background"/>
+    <foreground android:drawable="@mipmap/ic_launcher_foreground"/>
+</adaptive-icon>

+ 5 - 0
android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml

@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+    <background android:drawable="@color/ic_launcher_background"/>
+    <foreground android:drawable="@mipmap/ic_launcher_foreground"/>
+</adaptive-icon>

BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher.png


BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png


BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png


BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher.png


BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png


BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png


BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher.png


BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png


BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png


BIN
android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png


BIN
android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png


BIN
android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png


BIN
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png


BIN
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png


BIN
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png


+ 4 - 0
android/app/src/main/res/values/ic_launcher_background.xml

@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <color name="ic_launcher_background">#FFFFFF</color>
+</resources>

+ 7 - 0
android/app/src/main/res/values/strings.xml

@@ -0,0 +1,7 @@
+<?xml version='1.0' encoding='utf-8'?>
+<resources>
+    <string name="app_name">mobile-app</string>
+    <string name="title_activity_main">mobile-app</string>
+    <string name="package_name">io.ionic.starter</string>
+    <string name="custom_url_scheme">mobileapp</string>
+</resources>

+ 22 - 0
android/app/src/main/res/values/styles.xml

@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+    <!-- Base application theme. -->
+    <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
+        <!-- Customize your theme here. -->
+        <item name="colorPrimary">@color/colorPrimary</item>
+        <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
+        <item name="colorAccent">@color/colorAccent</item>
+    </style>
+
+    <style name="AppTheme.NoActionBar" parent="Theme.AppCompat.NoActionBar">
+        <item name="windowActionBar">false</item>
+        <item name="windowNoTitle">true</item>
+        <item name="android:background">@null</item>
+    </style>
+
+
+    <style name="AppTheme.NoActionBarLaunch" parent="AppTheme.NoActionBar">
+        <item name="android:background">@drawable/splash</item>
+    </style>
+</resources>

+ 32 - 0
android/app/src/main/res/xml/config.xml

@@ -0,0 +1,32 @@
+<?xml version='1.0' encoding='utf-8'?>
+<widget version="1.0.0" xmlns="http://www.w3.org/ns/widgets" xmlns:cdv="http://cordova.apache.org/ns/1.0">
+  <access origin="*" />
+  
+  <feature name="AppVersion">
+    <param name="android-package" value="uk.co.whiteoctober.cordova.AppVersion"/>
+  </feature>
+
+  <feature name="File">
+    <param name="android-package" value="org.apache.cordova.file.FileUtils"/>
+    <param name="onload" value="true"/>
+  </feature>
+
+  <feature name="FileOpener2">
+    <param name="android-package" value="io.github.pwlin.cordova.plugins.fileopener2.FileOpener2"/>
+  </feature>
+
+  <feature name="FileTransfer">
+    <param name="android-package" value="org.apache.cordova.filetransfer.FileTransfer"/>
+  </feature>
+
+  <feature name="InAppBrowser">
+    <param name="android-package" value="org.apache.cordova.inappbrowser.InAppBrowser"/>
+  </feature>
+
+  <feature name="Whitelist">
+    <param name="android-package" value="org.apache.cordova.whitelist.WhitelistPlugin"/>
+    <param name="onload" value="true"/>
+  </feature>
+
+  
+</widget>

+ 5 - 0
android/app/src/main/res/xml/file_paths.xml

@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<paths xmlns:android="http://schemas.android.com/apk/res/android">
+    <external-path name="my_images" path="." />
+    <cache-path name="my_cache_images" path="." />
+</paths>

+ 4 - 0
android/app/src/main/res/xml/network_security_config.xml

@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<network-security-config>
+    <base-config cleartextTrafficPermitted="true" />
+</network-security-config>

+ 18 - 0
android/app/src/test/java/com/getcapacitor/myapp/ExampleUnitTest.java

@@ -0,0 +1,18 @@
+package com.getcapacitor.myapp;
+
+import static org.junit.Assert.*;
+
+import org.junit.Test;
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
+ */
+public class ExampleUnitTest {
+
+    @Test
+    public void addition_isCorrect() throws Exception {
+        assertEquals(4, 2 + 2);
+    }
+}

+ 29 - 0
android/build.gradle

@@ -0,0 +1,29 @@
+// 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.google.gms:google-services:4.3.5'
+
+        // NOTE: Do not place your application dependencies here; they belong
+        // in the individual module build.gradle files
+    }
+}
+
+apply from: "variables.gradle"
+
+allprojects {
+    repositories {
+        google()
+        jcenter()
+    }
+}
+
+task clean(type: Delete) {
+    delete rootProject.buildDir
+}

+ 15 - 0
android/capacitor.settings.gradle

@@ -0,0 +1,15 @@
+// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN
+include ':capacitor-android'
+project(':capacitor-android').projectDir = new File('../node_modules/@capacitor/android/capacitor')
+
+include ':capacitor-app'
+project(':capacitor-app').projectDir = new File('../node_modules/@capacitor/app/android')
+
+include ':capacitor-haptics'
+project(':capacitor-haptics').projectDir = new File('../node_modules/@capacitor/haptics/android')
+
+include ':capacitor-keyboard'
+project(':capacitor-keyboard').projectDir = new File('../node_modules/@capacitor/keyboard/android')
+
+include ':capacitor-status-bar'
+project(':capacitor-status-bar').projectDir = new File('../node_modules/@capacitor/status-bar/android')

+ 24 - 0
android/gradle.properties

@@ -0,0 +1,24 @@
+# Project-wide Gradle settings.
+
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx1536m
+
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
+
+# AndroidX package structure to make it clearer which packages are bundled with the
+# Android operating system, and which are packaged with your app's APK
+# https://developer.android.com/topic/libraries/support-library/androidx-rn
+android.useAndroidX=true
+# Automatically convert third-party libraries to use AndroidX
+android.enableJetifier=true

BIN
android/gradle/wrapper/gradle-wrapper.jar


+ 5 - 0
android/gradle/wrapper/gradle-wrapper.properties

@@ -0,0 +1,5 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-7.0-all.zip
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists

+ 185 - 0
android/gradlew

@@ -0,0 +1,185 @@
+#!/usr/bin/env sh
+
+#
+# Copyright 2015 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+##############################################################################
+##
+##  Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+    ls=`ls -ld "$PRG"`
+    link=`expr "$ls" : '.*-> \(.*\)$'`
+    if expr "$link" : '/.*' > /dev/null; then
+        PRG="$link"
+    else
+        PRG=`dirname "$PRG"`"/$link"
+    fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+    echo "$*"
+}
+
+die () {
+    echo
+    echo "$*"
+    echo
+    exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+  CYGWIN* )
+    cygwin=true
+    ;;
+  Darwin* )
+    darwin=true
+    ;;
+  MINGW* )
+    msys=true
+    ;;
+  NONSTOP* )
+    nonstop=true
+    ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+    if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+        # IBM's JDK on AIX uses strange locations for the executables
+        JAVACMD="$JAVA_HOME/jre/sh/java"
+    else
+        JAVACMD="$JAVA_HOME/bin/java"
+    fi
+    if [ ! -x "$JAVACMD" ] ; then
+        die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+    fi
+else
+    JAVACMD="java"
+    which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+    MAX_FD_LIMIT=`ulimit -H -n`
+    if [ $? -eq 0 ] ; then
+        if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+            MAX_FD="$MAX_FD_LIMIT"
+        fi
+        ulimit -n $MAX_FD
+        if [ $? -ne 0 ] ; then
+            warn "Could not set maximum file descriptor limit: $MAX_FD"
+        fi
+    else
+        warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+    fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+    GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
+    APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+    CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+
+    JAVACMD=`cygpath --unix "$JAVACMD"`
+
+    # We build the pattern for arguments to be converted via cygpath
+    ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+    SEP=""
+    for dir in $ROOTDIRSRAW ; do
+        ROOTDIRS="$ROOTDIRS$SEP$dir"
+        SEP="|"
+    done
+    OURCYGPATTERN="(^($ROOTDIRS))"
+    # Add a user-defined pattern to the cygpath arguments
+    if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+        OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+    fi
+    # Now convert the arguments - kludge to limit ourselves to /bin/sh
+    i=0
+    for arg in "$@" ; do
+        CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+        CHECK2=`echo "$arg"|egrep -c "^-"`                                 ### Determine if an option
+
+        if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then                    ### Added a condition
+            eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+        else
+            eval `echo args$i`="\"$arg\""
+        fi
+        i=`expr $i + 1`
+    done
+    case $i in
+        0) set -- ;;
+        1) set -- "$args0" ;;
+        2) set -- "$args0" "$args1" ;;
+        3) set -- "$args0" "$args1" "$args2" ;;
+        4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+        5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+        6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+        7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+        8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+        9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+    esac
+fi
+
+# Escape application args
+save () {
+    for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+    echo " "
+}
+APP_ARGS=`save "$@"`
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+exec "$JAVACMD" "$@"

+ 89 - 0
android/gradlew.bat

@@ -0,0 +1,89 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem      https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem  Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if  not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega

+ 5 - 0
android/settings.gradle

@@ -0,0 +1,5 @@
+include ':app'
+include ':capacitor-cordova-android-plugins'
+project(':capacitor-cordova-android-plugins').projectDir = new File('./capacitor-cordova-android-plugins/')
+
+apply from: 'capacitor.settings.gradle'

+ 14 - 0
android/variables.gradle

@@ -0,0 +1,14 @@
+ext {
+    minSdkVersion = 21
+    compileSdkVersion = 30
+    targetSdkVersion = 30
+    androidxActivityVersion = '1.2.0'
+    androidxAppCompatVersion = '1.2.0'
+    androidxCoordinatorLayoutVersion = '1.1.0'
+    androidxCoreVersion = '1.3.2'
+    androidxFragmentVersion = '1.3.0'
+    junitVersion = '4.13.1'
+    androidxJunitVersion = '1.1.2'
+    androidxEspressoCoreVersion = '3.3.0'
+    cordovaAndroidVersion = '7.0.0'
+}

+ 14 - 3
angular.json

@@ -29,9 +29,18 @@
                 "glob": "**/*.svg",
                 "input": "node_modules/ionicons/dist/ionicons/svg",
                 "output": "./svg"
+              },
+              {
+                "glob": "**/*",
+                "input": "./node_modules/@ant-design/icons-angular/src/inline-svg/",
+                "output": "icons-angular/assets"
               }
             ],
-            "styles": ["src/theme/variables.scss", "src/global.scss"],
+            "styles": [
+              "node_modules/ng-zorro-antd-mobile/src/ng-zorro-antd-mobile.min.css",
+              "src/theme/variables.scss",
+              "src/global.scss"
+            ],
             "scripts": [],
             "aot": false,
             "vendorChunk": true,
@@ -39,7 +48,8 @@
             "buildOptimizer": false,
             "sourceMap": true,
             "optimization": false,
-            "namedChunks": true
+            "namedChunks": true,
+            "preserveSymlinks": true
           },
           "configurations": {
             "production": {
@@ -171,6 +181,7 @@
     }
   },
   "cli": {
+    "analytics": false,
     "defaultCollection": "@ionic/angular-toolkit"
   },
   "schematics": {
@@ -181,4 +192,4 @@
       "styleext": "scss"
     }
   }
-}
+}

BIN
lc.pepk


File diff suppressed because it is too large
+ 3834 - 2586
package-lock.json


+ 38 - 20
package.json

@@ -1,6 +1,6 @@
 {
   "name": "mobile-app",
-  "version": "0.0.1",
+  "version": "0.0.2",
   "author": "Ionic Framework",
   "homepage": "https://ionicframework.com/",
   "scripts": {
@@ -13,32 +13,50 @@
   },
   "private": true,
   "dependencies": {
-    "@angular/common": "~12.1.1",
-    "@angular/core": "~12.1.1",
-    "@angular/forms": "~12.1.1",
-    "@angular/platform-browser": "~12.1.1",
-    "@angular/platform-browser-dynamic": "~12.1.1",
-    "@angular/router": "~12.1.1",
-    "@capacitor/app": "1.0.6",
-    "@capacitor/core": "3.3.2",
+    "@angular/common": "~12.2.15",
+    "@angular/core": "~12.2.15",
+    "@angular/forms": "~12.2.15",
+    "@angular/platform-browser": "~12.2.15",
+    "@angular/platform-browser-dynamic": "~12.2.15",
+    "@angular/router": "~12.2.15",
+    "@ant-design/icons-angular": "^12.0.3",
+    "@capacitor/android": "3.3.3",
+    "@capacitor/app": "1.0.7",
+    "@capacitor/core": "3.3.3",
     "@capacitor/haptics": "1.1.3",
-    "@capacitor/keyboard": "1.1.3",
+    "@capacitor/keyboard": "1.2.0",
     "@capacitor/status-bar": "1.0.6",
-    "@ionic/angular": "^5.5.2",
+    "@ionic-native/app-version": "^5.36.0",
+    "@ionic-native/file": "^5.36.0",
+    "@ionic-native/file-opener": "^5.36.0",
+    "@ionic-native/file-transfer": "^5.36.0",
+    "@ionic-native/in-app-browser": "^5.36.0",
+    "@ionic/angular": "^6.0.1",
+    "@ionic/storage-angular": "^3.0.6",
+    "cordova-plugin-app-version": "^0.1.12",
+    "cordova-plugin-file": "^6.0.2",
+    "cordova-plugin-file-opener2": "^3.0.5",
+    "cordova-plugin-file-transfer": "^1.7.1",
+    "cordova-plugin-inappbrowser": "^5.0.0",
+    "cordova-plugin-whitelist": "^1.3.5",
+    "fast-sha256": "^1.3.0",
+    "jetifier": "^2.0.0",
+    "jsrsasign": "^10.5.1",
+    "ng-zorro-antd-mobile": "^5.0.1",
     "rxjs": "~6.6.0",
     "tslib": "^2.2.0",
     "zone.js": "~0.11.4"
   },
   "devDependencies": {
-    "@angular-devkit/build-angular": "~12.1.1",
-    "@angular-eslint/builder": "~12.0.0",
-    "@angular-eslint/eslint-plugin": "~12.0.0",
-    "@angular-eslint/eslint-plugin-template": "~12.0.0",
-    "@angular-eslint/template-parser": "~12.0.0",
-    "@angular/cli": "~12.1.1",
-    "@angular/compiler": "~12.1.1",
-    "@angular/compiler-cli": "~12.1.1",
-    "@angular/language-service": "~12.0.1",
+    "@angular-devkit/build-angular": "~12.2.14",
+    "@angular-eslint/builder": "~12.5.0",
+    "@angular-eslint/eslint-plugin": "~12.5.0",
+    "@angular-eslint/eslint-plugin-template": "~12.5.0",
+    "@angular-eslint/template-parser": "~12.5.0",
+    "@angular/cli": "~12.2.14",
+    "@angular/compiler": "~12.2.15",
+    "@angular/compiler-cli": "~12.2.15",
+    "@angular/language-service": "~12.2.15",
     "@capacitor/cli": "3.3.2",
     "@ionic/angular-toolkit": "^4.0.0",
     "@types/jasmine": "~3.6.0",

+ 46 - 5
src/app/app-routing.module.ts

@@ -1,15 +1,56 @@
 import { NgModule } from '@angular/core';
 import { PreloadAllModules, RouterModule, Routes } from '@angular/router';
+import { RouteAuthGuardService } from './core/services/route-auth-guard.service';
+import { GridMenuComponent } from './layout/grid-menu/grid-menu.component';
+import { TabLayoutComponent } from './layout/layouts/tab-layout.component';
+import { ListMenuComponent } from './layout/list-menu/list-menu.component';
+import { UserProfileComponent } from './layout/user-profile/user-profile.component';
+import { MessageComponent } from './pages/product/message/message.component';
 
 const routes: Routes = [
   {
-    path: 'home',
-    loadChildren: () => import('./home/home.module').then( m => m.HomePageModule)
+    path: '',
+    component: TabLayoutComponent,
+    children: [
+      {
+        path: '',
+        redirectTo: 'home',
+        pathMatch: 'full'
+      },
+      {
+        canActivate: [RouteAuthGuardService],
+        path: 'home',
+        component: GridMenuComponent
+      },
+      {
+        path: 'user-profile',
+        component: UserProfileComponent
+      },
+      {
+        canActivate: [RouteAuthGuardService],
+        path: 'message',
+        component: MessageComponent
+      }
+    ]
   },
   {
-    path: '',
-    redirectTo: 'home',
-    pathMatch: 'full'
+    path: 'full',
+    canActivate: [RouteAuthGuardService],
+    children: [
+      {
+        path: 'list-menu',
+        component: ListMenuComponent
+      },
+      {
+        path: 'product',
+        loadChildren: () => import('./pages/product/product.module').then(m => m.ProductModule)
+      }
+    ]
+
+  },
+  {
+    path: 'passport',
+    loadChildren: () => import('./pages/passport/passport.module').then(m => m.PassportModule)
   },
 ];
 

+ 0 - 3
src/app/app.component.html

@@ -1,3 +0,0 @@
-<ion-app>
-  <ion-router-outlet></ion-router-outlet>
-</ion-app>

+ 0 - 23
src/app/app.component.spec.ts

@@ -1,23 +0,0 @@
-import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
-import { TestBed, waitForAsync } from '@angular/core/testing';
-
-import { AppComponent } from './app.component';
-
-describe('AppComponent', () => {
-
-  beforeEach(waitForAsync(() => {
-
-    TestBed.configureTestingModule({
-      declarations: [AppComponent],
-      schemas: [CUSTOM_ELEMENTS_SCHEMA],
-    }).compileComponents();
-  }));
-
-  it('should create the app', () => {
-    const fixture = TestBed.createComponent(AppComponent);
-    const app = fixture.debugElement.componentInstance;
-    expect(app).toBeTruthy();
-  });
-  // TODO: add more tests!
-
-});

+ 19 - 5
src/app/app.component.ts

@@ -1,10 +1,24 @@
-import { Component } from '@angular/core';
+import { AfterViewInit, Component, OnInit, ViewChild } from '@angular/core';
+import { IonRouterOutlet } from '@ionic/angular';
+import { ButtonService } from './core/services/button.service';
 
 @Component({
   selector: 'app-root',
-  templateUrl: 'app.component.html',
-  styleUrls: ['app.component.scss'],
+  template: `
+  <ion-app>
+    <ion-router-outlet></ion-router-outlet>
+  </ion-app>
+  `
 })
-export class AppComponent {
-  constructor() {}
+export class AppComponent implements AfterViewInit {
+
+  @ViewChild(IonRouterOutlet)
+  private routeroutlet!: IonRouterOutlet;
+  constructor(private btn: ButtonService) {
+
+
+  }
+  ngAfterViewInit(): void {
+    this.btn.init(this.routeroutlet); // 注册按钮服务,比如退出什么的。
+  }
 }

+ 55 - 7
src/app/app.module.ts

@@ -1,17 +1,65 @@
-import { NgModule } from '@angular/core';
+import { APP_INITIALIZER, NgModule } from '@angular/core';
 import { BrowserModule } from '@angular/platform-browser';
 import { RouteReuseStrategy } from '@angular/router';
-
-import { IonicModule, IonicRouteStrategy } from '@ionic/angular';
-
+import { IonicModule, IonicRouteStrategy, IonRouterOutlet } from '@ionic/angular';
 import { AppComponent } from './app.component';
 import { AppRoutingModule } from './app-routing.module';
+import { IonicStorageModule } from '@ionic/storage-angular';
+import { OAuthModule } from './core/oauth2-oidc/angular-oauth-oidc.module';
+import { OAuthStorage } from './core/oauth2-oidc/types';
+import { StorageService } from './core/services/storage.service';
+import { CommonModule } from '@angular/common';
+import { StartupService } from './core/services/startup.service';
+import { InAppBrowser } from '@ionic-native/in-app-browser/ngx';
+import { AppVersion } from '@ionic-native/app-version/ngx';
+import { FileTransfer } from '@ionic-native/file-transfer/ngx';
+import { FileOpener } from '@ionic-native/file-opener/ngx';
+import { File } from '@ionic-native/file/ngx';
+import { LayoutModule } from './layout/layout.module';
+import { NgZorroAntdMobileModule } from 'ng-zorro-antd-mobile';
+import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
+import { IconModule } from '@ant-design/icons-angular';
+
+const IonicServices = [
+  InAppBrowser,
+  AppVersion,
+  File,
+  FileTransfer,
+  FileOpener
+];
+
 
 @NgModule({
   declarations: [AppComponent],
   entryComponents: [],
-  imports: [BrowserModule, IonicModule.forRoot(), AppRoutingModule],
-  providers: [{ provide: RouteReuseStrategy, useClass: IonicRouteStrategy }],
+  imports: [
+    BrowserModule,
+    CommonModule,
+    IonicModule.forRoot(),
+    IonicStorageModule.forRoot(),
+    OAuthModule.forRoot({
+      resourceServer: {
+        // allowedUrls: ['http://www.angular.at/api'],
+        sendAccessToken: true
+      }
+    }),
+    LayoutModule,
+    AppRoutingModule,
+    IconModule,
+    BrowserAnimationsModule,
+    NgZorroAntdMobileModule,
+  ],
+  providers: [
+    ...IonicServices,
+    { provide: RouteReuseStrategy, useClass: IonicRouteStrategy },
+    {
+      provide: APP_INITIALIZER,
+      useFactory: (startupService: StartupService) => () => startupService.load(),
+      deps: [StartupService],
+      multi: true,
+    },
+    { provide: OAuthStorage, useFactory: (s: StorageService) => s, deps: [StorageService] }
+  ],
   bootstrap: [AppComponent],
 })
-export class AppModule {}
+export class AppModule { }

+ 57 - 0
src/app/core/oauth2-oidc/angular-oauth-oidc.module.ts

@@ -0,0 +1,57 @@
+import { DateTimeProvider, SystemDateTimeProvider } from './date-time-provider';
+import { OAuthStorage, OAuthLogger } from './types';
+import { NgModule, ModuleWithProviders } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { HTTP_INTERCEPTORS } from '@angular/common/http';
+
+import { OAuthService } from './oauth-service';
+import { UrlHelperService } from './url-helper.service';
+
+import { OAuthModuleConfig } from './oauth-module.config';
+import {
+  OAuthResourceServerErrorHandler,
+  OAuthNoopResourceServerErrorHandler,
+} from './interceptors/resource-server-error-handler';
+import { DefaultOAuthInterceptor } from './interceptors/default-oauth.interceptor';
+import { ValidationHandler } from './token-validation/validation-handler';
+import { NullValidationHandler } from './token-validation/null-validation-handler';
+import { createDefaultLogger, createDefaultStorage } from './factories';
+import {
+  HashHandler,
+  DefaultHashHandler,
+} from './token-validation/hash-handler';
+
+@NgModule({
+  imports: [CommonModule],
+  declarations: [],
+  exports: [],
+})
+export class OAuthModule {
+  static forRoot(
+    config: OAuthModuleConfig = null,
+    validationHandlerClass = NullValidationHandler
+  ): ModuleWithProviders<OAuthModule> {
+    return {
+      ngModule: OAuthModule,
+      providers: [
+        OAuthService,
+        UrlHelperService,
+        { provide: OAuthLogger, useFactory: createDefaultLogger },
+        { provide: OAuthStorage, useFactory: createDefaultStorage },
+        { provide: ValidationHandler, useClass: validationHandlerClass },
+        { provide: HashHandler, useClass: DefaultHashHandler },
+        {
+          provide: OAuthResourceServerErrorHandler,
+          useClass: OAuthNoopResourceServerErrorHandler,
+        },
+        { provide: OAuthModuleConfig, useValue: config },
+        {
+          provide: HTTP_INTERCEPTORS,
+          useClass: DefaultOAuthInterceptor,
+          multi: true,
+        },
+        { provide: DateTimeProvider, useClass: SystemDateTimeProvider },
+      ],
+    };
+  }
+}

+ 280 - 0
src/app/core/oauth2-oidc/auth.config.ts

@@ -0,0 +1,280 @@
+export class AuthConfig {
+  /**
+   * The client's id as registered with the auth server
+   */
+  public clientId? = '';
+
+  /**
+   * The client's redirectUri as registered with the auth server
+   */
+  public redirectUri? = '';
+
+  /**
+   * An optional second redirectUri where the auth server
+   * redirects the user to after logging out.
+   */
+  public postLogoutRedirectUri? = '';
+
+  /**
+   * Defines whether to use 'redirectUri' as a replacement
+   * of 'postLogoutRedirectUri' if the latter is not set.
+   */
+  public redirectUriAsPostLogoutRedirectUriFallback? = true;
+
+  /**
+   * The auth server's endpoint that allows to log
+   * the user in when using implicit flow.
+   */
+  public loginUrl? = '';
+
+  /**
+   * The requested scopes
+   */
+  public scope? = 'openid profile';
+
+  public resource? = '';
+
+  public rngUrl? = '';
+
+  /**
+   * Defines whether to use OpenId Connect during
+   * implicit flow.
+   */
+  public oidc? = true;
+
+  /**
+   * Defines whether to request an access token during
+   * implicit flow.
+   */
+  public requestAccessToken? = true;
+
+  public options?: any = null;
+
+  /**
+   * The issuer's uri.
+   */
+  public issuer? = '';
+
+  /**
+   * The logout url.
+   */
+  public logoutUrl? = '';
+
+  /**
+   * Defines whether to clear the hash fragment after logging in.
+   */
+  public clearHashAfterLogin? = true;
+
+  /**
+   * Url of the token endpoint as defined by OpenId Connect and OAuth 2.
+   */
+  public tokenEndpoint?: string = null;
+
+  /**
+   * Url of the revocation endpoint as defined by OpenId Connect and OAuth 2.
+   */
+  public revocationEndpoint?: string = null;
+
+  /**
+   * Names of known parameters sent out in the TokenResponse. https://tools.ietf.org/html/rfc6749#section-5.1
+   */
+  public customTokenParameters?: string[] = [];
+
+  /**
+   * Url of the userinfo endpoint as defined by OpenId Connect.
+   */
+  public userinfoEndpoint?: string = null;
+
+  public responseType? = '';
+
+  /**
+   * Defines whether additional debug information should
+   * be shown at the console. Note that in certain browsers
+   * the verbosity of the console needs to be explicitly set
+   * to include Debug level messages.
+   */
+  public showDebugInformation? = false;
+
+  /**
+   * The redirect uri used when doing silent refresh.
+   */
+  public silentRefreshRedirectUri? = '';
+
+  public silentRefreshMessagePrefix? = '';
+
+  /**
+   * Set this to true to display the iframe used for
+   * silent refresh for debugging.
+   */
+  public silentRefreshShowIFrame? = false;
+
+  /**
+   * Timeout for silent refresh.
+   * @internal
+   * depreacted b/c of typo, see silentRefreshTimeout
+   */
+  public siletRefreshTimeout?: number = 1000 * 20;
+
+  /**
+   * Timeout for silent refresh.
+   */
+  public silentRefreshTimeout?: number = 1000 * 20;
+
+  /**
+   * Some auth servers don't allow using password flow
+   * w/o a client secret while the standards do not
+   * demand for it. In this case, you can set a password
+   * here. As this password is exposed to the public
+   * it does not bring additional security and is therefore
+   * as good as using no password.
+   */
+  public dummyClientSecret?: string = null;
+
+  /**
+   * Defines whether https is required.
+   * The default value is remoteOnly which only allows
+   * http for localhost, while every other domains need
+   * to be used with https.
+   */
+  public requireHttps?: boolean | 'remoteOnly' = 'remoteOnly';
+
+  /**
+   * Defines whether every url provided by the discovery
+   * document has to start with the issuer's url.
+   */
+  public strictDiscoveryDocumentValidation? = true;
+
+  /**
+   * JSON Web Key Set (https://tools.ietf.org/html/rfc7517)
+   * with keys used to validate received id_tokens.
+   * This is taken out of the disovery document. Can be set manually too.
+   */
+  public jwks?: object = null;
+
+  /**
+   * Map with additional query parameter that are appended to
+   * the request when initializing implicit flow.
+   */
+  public customQueryParams?: object = null;
+
+  public silentRefreshIFrameName? = 'angular-oauth-oidc-silent-refresh-iframe';
+
+  /**
+   * Defines when the token_timeout event should be raised.
+   * If you set this to the default value 0.75, the event
+   * is triggered after 75% of the token's life time.
+   */
+  public timeoutFactor? = 0.75;
+
+  /**
+   * If true, the lib will try to check whether the user
+   * is still logged in on a regular basis as described
+   * in http://openid.net/specs/openid-connect-session-1_0.html#ChangeNotification
+   */
+  public sessionChecksEnabled? = false;
+
+  /**
+   * Interval in msec for checking the session
+   * according to http://openid.net/specs/openid-connect-session-1_0.html#ChangeNotification
+   */
+  public sessionCheckIntervall? = 3 * 1000;
+
+  /**
+   * Url for the iframe used for session checks
+   */
+  public sessionCheckIFrameUrl?: string = null;
+
+  /**
+   * Name of the iframe to use for session checks
+   */
+  public sessionCheckIFrameName? = 'angular-oauth-oidc-check-session-iframe';
+
+  /**
+   * This property has been introduced to disable at_hash checks
+   * and is indented for Identity Provider that does not deliver
+   * an at_hash EVEN THOUGH its recommended by the OIDC specs.
+   * Of course, when disabling these checks then we are bypassing
+   * a security check which means we are more vulnerable.
+   */
+  public disableAtHashCheck? = false;
+
+  /**
+   * Defines wether to check the subject of a refreshed token after silent refresh.
+   * Normally, it should be the same as before.
+   */
+  public skipSubjectCheck? = false;
+
+  public useIdTokenHintForSilentRefresh? = false;
+
+  /**
+   * Defined whether to skip the validation of the issuer in the discovery document.
+   * Normally, the discovey document's url starts with the url of the issuer.
+   */
+  public skipIssuerCheck? = false;
+
+  /**
+   * According to rfc6749 it is recommended (but not required) that the auth
+   * server exposes the access_token's life time in seconds.
+   * This is a fallback value for the case this value is not exposed.
+   */
+  public fallbackAccessTokenExpirationTimeInSec?: number;
+
+  /**
+   * final state sent to issuer is built as follows:
+   * state = nonce + nonceStateSeparator + additional state
+   * Default separator is ';' (encoded %3B).
+   * In rare cases, this character might be forbidden or inconvenient to use by the issuer so it can be customized.
+   */
+  public nonceStateSeparator? = ';';
+
+  /**
+   * Set this to true to use HTTP BASIC auth for AJAX calls
+   */
+  public useHttpBasicAuth? = false;
+
+  /**
+   * The window of time (in seconds) to allow the current time to deviate when validating id_token's iat and exp values.
+   */
+  public clockSkewInSec?: number;
+
+  /**
+   * The interceptors waits this time span if there is no token
+   */
+  public waitForTokenInMsec? = 0;
+
+  /**
+   * Set this to true if you want to use silent refresh together with
+   * code flow. As silent refresh is the only option for refreshing
+   * with implicit flow, you don't need to explicitly turn it on in
+   * this case.
+   */
+  public useSilentRefresh?;
+
+  /**
+   * Code Flow is by defauld used together with PKCI which is also higly recommented.
+   * You can disbale it here by setting this flag to true.
+   * https://tools.ietf.org/html/rfc7636#section-1.1
+   */
+  public disablePKCE? = false;
+
+  /**
+   * Set this to true to preserve the requested route including query parameters after code flow login.
+   * This setting enables deep linking for the code flow.
+   */
+  public preserveRequestedRoute? = false;
+
+  constructor(json?: Partial<AuthConfig>) {
+    if (json) {
+      Object.assign(this, json);
+    }
+  }
+
+  /**
+   * This property allows you to override the method that is used to open the login url,
+   * allowing a way for implementations to specify their own method of routing to new
+   * urls.
+   */
+  public openUri?: (uri: string) => void = (uri) => {
+    location.href = uri;
+  };
+}

+ 18 - 0
src/app/core/oauth2-oidc/base64-helper.ts

@@ -0,0 +1,18 @@
+// see: https://developer.mozilla.org/en-US/docs/Web/API/WindowBase64/Base64_encoding_and_decoding#The_.22Unicode_Problem.22
+export function b64DecodeUnicode(str) {
+  const base64 = str.replace(/\-/g, '+').replace(/\_/g, '/');
+
+  return decodeURIComponent(
+    atob(base64)
+      .split('')
+      .map(function (c) {
+        return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
+      })
+      .join('')
+  );
+}
+
+export function base64UrlEncode(str): string {
+  const base64 = btoa(str);
+  return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
+}

+ 17 - 0
src/app/core/oauth2-oidc/date-time-provider.ts

@@ -0,0 +1,17 @@
+import { Injectable } from '@angular/core';
+
+export abstract class DateTimeProvider {
+  abstract now(): number;
+  abstract new(): Date;
+}
+
+@Injectable()
+export class SystemDateTimeProvider extends DateTimeProvider {
+  now(): number {
+    return Date.now();
+  }
+
+  new(): Date {
+    return new Date();
+  }
+}

+ 1 - 0
src/app/core/oauth2-oidc/deps.d.ts

@@ -0,0 +1 @@
+//declare var require: any;

+ 21 - 0
src/app/core/oauth2-oidc/encoder.ts

@@ -0,0 +1,21 @@
+import { HttpParameterCodec } from '@angular/common/http';
+/**
+ * This custom encoder allows charactes like +, % and / to be used in passwords
+ */
+export class WebHttpUrlEncodingCodec implements HttpParameterCodec {
+  encodeKey(k: string): string {
+    return encodeURIComponent(k);
+  }
+
+  encodeValue(v: string): string {
+    return encodeURIComponent(v);
+  }
+
+  decodeKey(k: string): string {
+    return decodeURIComponent(k);
+  }
+
+  decodeValue(v: string) {
+    return decodeURIComponent(v);
+  }
+}

+ 52 - 0
src/app/core/oauth2-oidc/events.ts

@@ -0,0 +1,52 @@
+export type EventType =
+  | 'discovery_document_loaded'
+  | 'jwks_load_error'
+  | 'invalid_nonce_in_state'
+  | 'discovery_document_load_error'
+  | 'discovery_document_validation_error'
+  | 'user_profile_loaded'
+  | 'user_profile_load_error'
+  | 'token_received'
+  | 'token_error'
+  | 'code_error'
+  | 'token_refreshed'
+  | 'token_refresh_error'
+  | 'silent_refresh_error'
+  | 'silently_refreshed'
+  | 'silent_refresh_timeout'
+  | 'token_validation_error'
+  | 'token_expires'
+  | 'session_changed'
+  | 'session_error'
+  | 'session_terminated'
+  | 'session_unchanged'
+  | 'logout'
+  | 'popup_closed'
+  | 'popup_blocked'
+  | 'token_revoke_error';
+
+export abstract class OAuthEvent {
+  constructor(readonly type: EventType) {}
+}
+
+export class OAuthSuccessEvent extends OAuthEvent {
+  constructor(type: EventType, readonly info: any = null) {
+    super(type);
+  }
+}
+
+export class OAuthInfoEvent extends OAuthEvent {
+  constructor(type: EventType, readonly info: any = null) {
+    super(type);
+  }
+}
+
+export class OAuthErrorEvent extends OAuthEvent {
+  constructor(
+    type: EventType,
+    readonly reason: object,
+    readonly params: object = null
+  ) {
+    super(type);
+  }
+}

+ 12 - 0
src/app/core/oauth2-oidc/factories.ts

@@ -0,0 +1,12 @@
+/* eslint-disable prefer-arrow/prefer-arrow-functions */
+import { MemoryStorage } from './types';
+
+export function createDefaultLogger() {
+  return console;
+}
+
+export function createDefaultStorage() {
+  return typeof sessionStorage !== 'undefined'
+    ? sessionStorage
+    : new MemoryStorage();
+}

+ 89 - 0
src/app/core/oauth2-oidc/interceptors/default-oauth.interceptor.ts

@@ -0,0 +1,89 @@
+import { Injectable, Optional } from '@angular/core';
+
+import {
+  HttpEvent,
+  HttpHandler,
+  HttpInterceptor,
+  HttpRequest,
+} from '@angular/common/http';
+import { Observable, of, merge } from 'rxjs';
+import {
+  catchError,
+  filter,
+  map,
+  take,
+  mergeMap,
+  timeout,
+} from 'rxjs/operators';
+import { OAuthResourceServerErrorHandler } from './resource-server-error-handler';
+import { OAuthModuleConfig } from '../oauth-module.config';
+import { OAuthService } from '../oauth-service';
+
+@Injectable()
+export class DefaultOAuthInterceptor implements HttpInterceptor {
+  constructor(
+    private oAuthService: OAuthService,
+    private errorHandler: OAuthResourceServerErrorHandler,
+    @Optional() private moduleConfig: OAuthModuleConfig
+  ) {}
+
+  private checkUrl(url: string): boolean {
+    if (this.moduleConfig.resourceServer.customUrlValidation) {
+      return this.moduleConfig.resourceServer.customUrlValidation(url);
+    }
+
+    if (this.moduleConfig.resourceServer.allowedUrls) {
+      return !!this.moduleConfig.resourceServer.allowedUrls.find((u) =>
+        url.toLowerCase().startsWith(u.toLowerCase())
+      );
+    }
+
+    return true;
+  }
+
+  public intercept(
+    req: HttpRequest<any>,
+    next: HttpHandler
+  ): Observable<HttpEvent<any>> {
+    const url = req.url.toLowerCase();
+
+    if (
+      !this.moduleConfig ||
+      !this.moduleConfig.resourceServer ||
+      !this.checkUrl(url)
+    ) {
+      return next.handle(req);
+    }
+
+    const sendAccessToken = this.moduleConfig.resourceServer.sendAccessToken;
+
+    if (!sendAccessToken) {
+      return next
+        .handle(req)
+        .pipe(catchError((err) => this.errorHandler.handleError(err)));
+    }
+
+    return merge(
+      of(this.oAuthService.getAccessToken()).pipe(filter((token) => !!token)),
+      this.oAuthService.events.pipe(
+        filter((e) => e.type === 'token_received'),
+        timeout(this.oAuthService.waitForTokenInMsec || 0),
+        catchError((_) => of(null)), // timeout is not an error
+        map((_) => this.oAuthService.getAccessToken())
+      )
+    ).pipe(
+      take(1),
+      mergeMap((token) => {
+        if (token) {
+          const header = 'Bearer ' + token;
+          const headers = req.headers.set('Authorization', header);
+          req = req.clone({ headers });
+        }
+
+        return next
+          .handle(req)
+          .pipe(catchError((err) => this.errorHandler.handleError(err)));
+      })
+    );
+  }
+}

+ 14 - 0
src/app/core/oauth2-oidc/interceptors/resource-server-error-handler.ts

@@ -0,0 +1,14 @@
+import { HttpResponse } from '@angular/common/http';
+import { Observable, throwError } from 'rxjs';
+
+export abstract class OAuthResourceServerErrorHandler {
+  abstract handleError(err: HttpResponse<any>): Observable<any>;
+}
+
+export class OAuthNoopResourceServerErrorHandler
+  implements OAuthResourceServerErrorHandler
+{
+  handleError(err: HttpResponse<any>): Observable<any> {
+    return throwError(err);
+  }
+}

+ 14 - 0
src/app/core/oauth2-oidc/oauth-module.config.ts

@@ -0,0 +1,14 @@
+export abstract class OAuthModuleConfig {
+  resourceServer: OAuthResourceServerConfig;
+}
+
+export abstract class OAuthResourceServerConfig {
+  /**
+   * Urls for which calls should be intercepted.
+   * If there is an ResourceServerErrorHandler registered, it is used for them.
+   * If sendAccessToken is set to true, the access_token is send to them too.
+   */
+  allowedUrls?: Array<string>;
+  sendAccessToken: boolean;
+  customUrlValidation?: (url: string) => boolean;
+}

File diff suppressed because it is too large
+ 3169 - 0
src/app/core/oauth2-oidc/oauth-service.ts


+ 89 - 0
src/app/core/oauth2-oidc/token-validation/hash-handler.ts

@@ -0,0 +1,89 @@
+import { Injectable } from '@angular/core';
+
+import { factory } from './js-sha256';
+const sha256 = factory();
+
+import fsha256 from 'fast-sha256';
+
+/**
+ * Abstraction for crypto algorithms
+ */
+export abstract class HashHandler {
+  abstract calcHash(valueToHash: string, algorithm: string): Promise<string>;
+}
+
+function decodeUTF8(s) {
+  if (typeof s !== 'string') throw new TypeError('expected string');
+  var i,
+    d = s,
+    b = new Uint8Array(d.length);
+  for (i = 0; i < d.length; i++) b[i] = d.charCodeAt(i);
+  return b;
+}
+
+function encodeUTF8(arr) {
+  var i,
+    s = [];
+  for (i = 0; i < arr.length; i++) s.push(String.fromCharCode(arr[i]));
+  return s.join('');
+}
+
+@Injectable()
+export class DefaultHashHandler implements HashHandler {
+  async calcHash(valueToHash: string, algorithm: string): Promise<string> {
+    // const encoder = new TextEncoder();
+    // const hashArray = await window.crypto.subtle.digest(algorithm, data);
+    // const data = encoder.encode(valueToHash);
+
+    // const fhash = fsha256(valueToHash);
+
+    const candHash = encodeUTF8(fsha256(decodeUTF8(valueToHash)));
+
+    // const hashArray = (sha256 as any).array(valueToHash);
+    // // const hashString = this.toHashString(hashArray);
+    // const hashString = this.toHashString2(hashArray);
+
+    // console.debug('hash orig - cand', candHash, hashString);
+    // alert(1);
+
+    return candHash;
+  }
+
+  toHashString2(byteArray: number[]) {
+    let result = '';
+    for (let e of byteArray) {
+      result += String.fromCharCode(e);
+    }
+    return result;
+  }
+
+  toHashString(buffer: ArrayBuffer) {
+    const byteArray = new Uint8Array(buffer);
+    let result = '';
+    for (let e of byteArray) {
+      result += String.fromCharCode(e);
+    }
+    return result;
+  }
+
+  // hexString(buffer) {
+  //     const byteArray = new Uint8Array(buffer);
+  //     const hexCodes = [...byteArray].map(value => {
+  //       const hexCode = value.toString(16);
+  //       const paddedHexCode = hexCode.padStart(2, '0');
+  //       return paddedHexCode;
+  //     });
+
+  //     return hexCodes.join('');
+  //   }
+
+  // toHashString(hexString: string) {
+  //   let result = '';
+  //   for (let i = 0; i < hexString.length; i += 2) {
+  //     let hexDigit = hexString.charAt(i) + hexString.charAt(i + 1);
+  //     let num = parseInt(hexDigit, 16);
+  //     result += String.fromCharCode(num);
+  //   }
+  //   return result;
+  // }
+}

+ 510 - 0
src/app/core/oauth2-oidc/token-validation/js-sha256.js

@@ -0,0 +1,510 @@
+/**
+ * [js-sha256]{@link https://github.com/emn178/js-sha256}
+ *
+ * @version 0.9.0
+ * @author Chen, Yi-Cyuan [emn178@gmail.com]
+ * @copyright Chen, Yi-Cyuan 2014-2017
+ * @license MIT
+ */
+
+/*jslint bitwise: true */
+function factory() {
+  
+    var ERROR = 'input is invalid type';
+    var WINDOW = typeof window === 'object';
+    var root = WINDOW ? window : {};
+    if (root.JS_SHA256_NO_WINDOW) {
+      WINDOW = false;
+    }
+    var WEB_WORKER = !WINDOW && typeof self === 'object';
+    var NODE_JS = !root.JS_SHA256_NO_NODE_JS && typeof process === 'object' && process.versions && process.versions.node;
+    if (NODE_JS) {
+      root = global;
+    } else if (WEB_WORKER) {
+      root = self;
+    }
+    var COMMON_JS = !root.JS_SHA256_NO_COMMON_JS && typeof module === 'object' && module.exports;
+    var AMD = typeof define === 'function' && define.amd;
+    var ARRAY_BUFFER = !root.JS_SHA256_NO_ARRAY_BUFFER && typeof ArrayBuffer !== 'undefined';
+    const HEX_CHARS = '0123456789abcdef'.split('');
+    const EXTRA = [-2147483648, 8388608, 32768, 128];
+    const SHIFT = [24, 16, 8, 0];
+    const K = [
+      0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
+      0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
+      0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
+      0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
+      0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
+      0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
+      0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
+      0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2
+    ];
+    const OUTPUT_TYPES = ['hex', 'array', 'digest', 'arrayBuffer'];
+  
+    var blocks = [];
+  
+    if (root.JS_SHA256_NO_NODE_JS || !Array.isArray) {
+      Array.isArray = function (obj) {
+        return Object.prototype.toString.call(obj) === '[object Array]';
+      };
+    }
+  
+    if (ARRAY_BUFFER && (root.JS_SHA256_NO_ARRAY_BUFFER_IS_VIEW || !ArrayBuffer.isView)) {
+      ArrayBuffer.isView = function (obj) {
+        return typeof obj === 'object' && obj.buffer && obj.buffer.constructor === ArrayBuffer;
+      };
+    }
+  
+    var createOutputMethod = function (outputType, is224) {
+      return function (message) {
+        return new Sha256(is224, true).update(message)[outputType]();
+      };
+    };
+  
+    var createMethod = function (is224) {
+      var method = createOutputMethod('hex', is224);
+      if (NODE_JS) {
+        method = nodeWrap(method, is224);
+      }
+      method.create = function () {
+        return new Sha256(is224);
+      };
+      method.update = function (message) {
+        return method.create().update(message);
+      };
+      for (var i = 0; i < OUTPUT_TYPES.length; ++i) {
+        var type = OUTPUT_TYPES[i];
+        method[type] = createOutputMethod(type, is224);
+      }
+      return method;
+    };
+  
+    var nodeWrap = function (method, is224) {
+      var crypto = eval("require('crypto')");
+      var Buffer = eval("require('buffer').Buffer");
+      var algorithm = is224 ? 'sha224' : 'sha256';
+      var nodeMethod = function (message) {
+        if (typeof message === 'string') {
+          return crypto.createHash(algorithm).update(message, 'utf8').digest('hex');
+        } else {
+          if (message === null || message === undefined) {
+            throw new Error(ERROR);
+          } else if (message.constructor === ArrayBuffer) {
+            message = new Uint8Array(message);
+          }
+        }
+        if (Array.isArray(message) || ArrayBuffer.isView(message) ||
+          message.constructor === Buffer) {
+          return crypto.createHash(algorithm).update(new Buffer(message)).digest('hex');
+        } else {
+          return method(message);
+        }
+      };
+      return nodeMethod;
+    };
+  
+    var createHmacOutputMethod = function (outputType, is224) {
+      return function (key, message) {
+        return new HmacSha256(key, is224, true).update(message)[outputType]();
+      };
+    };
+  
+    var createHmacMethod = function (is224) {
+      var method = createHmacOutputMethod('hex', is224);
+      method.create = function (key) {
+        return new HmacSha256(key, is224);
+      };
+      method.update = function (key, message) {
+        return method.create(key).update(message);
+      };
+      for (var i = 0; i < OUTPUT_TYPES.length; ++i) {
+        var type = OUTPUT_TYPES[i];
+        method[type] = createHmacOutputMethod(type, is224);
+      }
+      return method;
+    };
+  
+    function Sha256(is224, sharedMemory) {
+      if (sharedMemory) {
+        blocks[0] = blocks[16] = blocks[1] = blocks[2] = blocks[3] =
+          blocks[4] = blocks[5] = blocks[6] = blocks[7] =
+          blocks[8] = blocks[9] = blocks[10] = blocks[11] =
+          blocks[12] = blocks[13] = blocks[14] = blocks[15] = 0;
+        this.blocks = blocks;
+      } else {
+        this.blocks = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
+      }
+  
+      if (is224) {
+        this.h0 = 0xc1059ed8;
+        this.h1 = 0x367cd507;
+        this.h2 = 0x3070dd17;
+        this.h3 = 0xf70e5939;
+        this.h4 = 0xffc00b31;
+        this.h5 = 0x68581511;
+        this.h6 = 0x64f98fa7;
+        this.h7 = 0xbefa4fa4;
+      } else { // 256
+        this.h0 = 0x6a09e667;
+        this.h1 = 0xbb67ae85;
+        this.h2 = 0x3c6ef372;
+        this.h3 = 0xa54ff53a;
+        this.h4 = 0x510e527f;
+        this.h5 = 0x9b05688c;
+        this.h6 = 0x1f83d9ab;
+        this.h7 = 0x5be0cd19;
+      }
+  
+      this.block = this.start = this.bytes = this.hBytes = 0;
+      this.finalized = this.hashed = false;
+      this.first = true;
+      this.is224 = is224;
+    }
+  
+    Sha256.prototype.update = function (message) {
+      if (this.finalized) {
+        return;
+      }
+      var notString, type = typeof message;
+      if (type !== 'string') {
+        if (type === 'object') {
+          if (message === null) {
+            throw new Error(ERROR);
+          } else if (ARRAY_BUFFER && message.constructor === ArrayBuffer) {
+            message = new Uint8Array(message);
+          } else if (!Array.isArray(message)) {
+            if (!ARRAY_BUFFER || !ArrayBuffer.isView(message)) {
+              throw new Error(ERROR);
+            }
+          }
+        } else {
+          throw new Error(ERROR);
+        }
+        notString = true;
+      }
+      var code, index = 0, i, length = message.length, blocks = this.blocks;
+  
+      while (index < length) {
+        if (this.hashed) {
+          this.hashed = false;
+          blocks[0] = this.block;
+          blocks[16] = blocks[1] = blocks[2] = blocks[3] =
+            blocks[4] = blocks[5] = blocks[6] = blocks[7] =
+            blocks[8] = blocks[9] = blocks[10] = blocks[11] =
+            blocks[12] = blocks[13] = blocks[14] = blocks[15] = 0;
+        }
+  
+        if (notString) {
+          for (i = this.start; index < length && i < 64; ++index) {
+            blocks[i >> 2] |= message[index] << SHIFT[i++ & 3];
+          }
+        } else {
+          for (i = this.start; index < length && i < 64; ++index) {
+            code = message.charCodeAt(index);
+            if (code < 0x80) {
+              blocks[i >> 2] |= code << SHIFT[i++ & 3];
+            } else if (code < 0x800) {
+              blocks[i >> 2] |= (0xc0 | (code >> 6)) << SHIFT[i++ & 3];
+              blocks[i >> 2] |= (0x80 | (code & 0x3f)) << SHIFT[i++ & 3];
+            } else if (code < 0xd800 || code >= 0xe000) {
+              blocks[i >> 2] |= (0xe0 | (code >> 12)) << SHIFT[i++ & 3];
+              blocks[i >> 2] |= (0x80 | ((code >> 6) & 0x3f)) << SHIFT[i++ & 3];
+              blocks[i >> 2] |= (0x80 | (code & 0x3f)) << SHIFT[i++ & 3];
+            } else {
+              code = 0x10000 + (((code & 0x3ff) << 10) | (message.charCodeAt(++index) & 0x3ff));
+              blocks[i >> 2] |= (0xf0 | (code >> 18)) << SHIFT[i++ & 3];
+              blocks[i >> 2] |= (0x80 | ((code >> 12) & 0x3f)) << SHIFT[i++ & 3];
+              blocks[i >> 2] |= (0x80 | ((code >> 6) & 0x3f)) << SHIFT[i++ & 3];
+              blocks[i >> 2] |= (0x80 | (code & 0x3f)) << SHIFT[i++ & 3];
+            }
+          }
+        }
+  
+        this.lastByteIndex = i;
+        this.bytes += i - this.start;
+        if (i >= 64) {
+          this.block = blocks[16];
+          this.start = i - 64;
+          this.hash();
+          this.hashed = true;
+        } else {
+          this.start = i;
+        }
+      }
+      if (this.bytes > 4294967295) {
+        this.hBytes += this.bytes / 4294967296 << 0;
+        this.bytes = this.bytes % 4294967296;
+      }
+      return this;
+    };
+  
+    Sha256.prototype.finalize = function () {
+      if (this.finalized) {
+        return;
+      }
+      this.finalized = true;
+      var blocks = this.blocks, i = this.lastByteIndex;
+      blocks[16] = this.block;
+      blocks[i >> 2] |= EXTRA[i & 3];
+      this.block = blocks[16];
+      if (i >= 56) {
+        if (!this.hashed) {
+          this.hash();
+        }
+        blocks[0] = this.block;
+        blocks[16] = blocks[1] = blocks[2] = blocks[3] =
+          blocks[4] = blocks[5] = blocks[6] = blocks[7] =
+          blocks[8] = blocks[9] = blocks[10] = blocks[11] =
+          blocks[12] = blocks[13] = blocks[14] = blocks[15] = 0;
+      }
+      blocks[14] = this.hBytes << 3 | this.bytes >>> 29;
+      blocks[15] = this.bytes << 3;
+      this.hash();
+    };
+  
+    Sha256.prototype.hash = function () {
+      var a = this.h0, b = this.h1, c = this.h2, d = this.h3, e = this.h4, f = this.h5, g = this.h6,
+        h = this.h7, blocks = this.blocks, j, s0, s1, maj, t1, t2, ch, ab, da, cd, bc;
+  
+      for (j = 16; j < 64; ++j) {
+        // rightrotate
+        t1 = blocks[j - 15];
+        s0 = ((t1 >>> 7) | (t1 << 25)) ^ ((t1 >>> 18) | (t1 << 14)) ^ (t1 >>> 3);
+        t1 = blocks[j - 2];
+        s1 = ((t1 >>> 17) | (t1 << 15)) ^ ((t1 >>> 19) | (t1 << 13)) ^ (t1 >>> 10);
+        blocks[j] = blocks[j - 16] + s0 + blocks[j - 7] + s1 << 0;
+      }
+  
+      bc = b & c;
+      for (j = 0; j < 64; j += 4) {
+        if (this.first) {
+          if (this.is224) {
+            ab = 300032;
+            t1 = blocks[0] - 1413257819;
+            h = t1 - 150054599 << 0;
+            d = t1 + 24177077 << 0;
+          } else {
+            ab = 704751109;
+            t1 = blocks[0] - 210244248;
+            h = t1 - 1521486534 << 0;
+            d = t1 + 143694565 << 0;
+          }
+          this.first = false;
+        } else {
+          s0 = ((a >>> 2) | (a << 30)) ^ ((a >>> 13) | (a << 19)) ^ ((a >>> 22) | (a << 10));
+          s1 = ((e >>> 6) | (e << 26)) ^ ((e >>> 11) | (e << 21)) ^ ((e >>> 25) | (e << 7));
+          ab = a & b;
+          maj = ab ^ (a & c) ^ bc;
+          ch = (e & f) ^ (~e & g);
+          t1 = h + s1 + ch + K[j] + blocks[j];
+          t2 = s0 + maj;
+          h = d + t1 << 0;
+          d = t1 + t2 << 0;
+        }
+        s0 = ((d >>> 2) | (d << 30)) ^ ((d >>> 13) | (d << 19)) ^ ((d >>> 22) | (d << 10));
+        s1 = ((h >>> 6) | (h << 26)) ^ ((h >>> 11) | (h << 21)) ^ ((h >>> 25) | (h << 7));
+        da = d & a;
+        maj = da ^ (d & b) ^ ab;
+        ch = (h & e) ^ (~h & f);
+        t1 = g + s1 + ch + K[j + 1] + blocks[j + 1];
+        t2 = s0 + maj;
+        g = c + t1 << 0;
+        c = t1 + t2 << 0;
+        s0 = ((c >>> 2) | (c << 30)) ^ ((c >>> 13) | (c << 19)) ^ ((c >>> 22) | (c << 10));
+        s1 = ((g >>> 6) | (g << 26)) ^ ((g >>> 11) | (g << 21)) ^ ((g >>> 25) | (g << 7));
+        cd = c & d;
+        maj = cd ^ (c & a) ^ da;
+        ch = (g & h) ^ (~g & e);
+        t1 = f + s1 + ch + K[j + 2] + blocks[j + 2];
+        t2 = s0 + maj;
+        f = b + t1 << 0;
+        b = t1 + t2 << 0;
+        s0 = ((b >>> 2) | (b << 30)) ^ ((b >>> 13) | (b << 19)) ^ ((b >>> 22) | (b << 10));
+        s1 = ((f >>> 6) | (f << 26)) ^ ((f >>> 11) | (f << 21)) ^ ((f >>> 25) | (f << 7));
+        bc = b & c;
+        maj = bc ^ (b & d) ^ cd;
+        ch = (f & g) ^ (~f & h);
+        t1 = e + s1 + ch + K[j + 3] + blocks[j + 3];
+        t2 = s0 + maj;
+        e = a + t1 << 0;
+        a = t1 + t2 << 0;
+      }
+  
+      this.h0 = this.h0 + a << 0;
+      this.h1 = this.h1 + b << 0;
+      this.h2 = this.h2 + c << 0;
+      this.h3 = this.h3 + d << 0;
+      this.h4 = this.h4 + e << 0;
+      this.h5 = this.h5 + f << 0;
+      this.h6 = this.h6 + g << 0;
+      this.h7 = this.h7 + h << 0;
+    };
+  
+    Sha256.prototype.hex = function () {
+      this.finalize();
+  
+      var h0 = this.h0, h1 = this.h1, h2 = this.h2, h3 = this.h3, h4 = this.h4, h5 = this.h5,
+        h6 = this.h6, h7 = this.h7;
+  
+      var hex = HEX_CHARS[(h0 >> 28) & 0x0F] + HEX_CHARS[(h0 >> 24) & 0x0F] +
+        HEX_CHARS[(h0 >> 20) & 0x0F] + HEX_CHARS[(h0 >> 16) & 0x0F] +
+        HEX_CHARS[(h0 >> 12) & 0x0F] + HEX_CHARS[(h0 >> 8) & 0x0F] +
+        HEX_CHARS[(h0 >> 4) & 0x0F] + HEX_CHARS[h0 & 0x0F] +
+        HEX_CHARS[(h1 >> 28) & 0x0F] + HEX_CHARS[(h1 >> 24) & 0x0F] +
+        HEX_CHARS[(h1 >> 20) & 0x0F] + HEX_CHARS[(h1 >> 16) & 0x0F] +
+        HEX_CHARS[(h1 >> 12) & 0x0F] + HEX_CHARS[(h1 >> 8) & 0x0F] +
+        HEX_CHARS[(h1 >> 4) & 0x0F] + HEX_CHARS[h1 & 0x0F] +
+        HEX_CHARS[(h2 >> 28) & 0x0F] + HEX_CHARS[(h2 >> 24) & 0x0F] +
+        HEX_CHARS[(h2 >> 20) & 0x0F] + HEX_CHARS[(h2 >> 16) & 0x0F] +
+        HEX_CHARS[(h2 >> 12) & 0x0F] + HEX_CHARS[(h2 >> 8) & 0x0F] +
+        HEX_CHARS[(h2 >> 4) & 0x0F] + HEX_CHARS[h2 & 0x0F] +
+        HEX_CHARS[(h3 >> 28) & 0x0F] + HEX_CHARS[(h3 >> 24) & 0x0F] +
+        HEX_CHARS[(h3 >> 20) & 0x0F] + HEX_CHARS[(h3 >> 16) & 0x0F] +
+        HEX_CHARS[(h3 >> 12) & 0x0F] + HEX_CHARS[(h3 >> 8) & 0x0F] +
+        HEX_CHARS[(h3 >> 4) & 0x0F] + HEX_CHARS[h3 & 0x0F] +
+        HEX_CHARS[(h4 >> 28) & 0x0F] + HEX_CHARS[(h4 >> 24) & 0x0F] +
+        HEX_CHARS[(h4 >> 20) & 0x0F] + HEX_CHARS[(h4 >> 16) & 0x0F] +
+        HEX_CHARS[(h4 >> 12) & 0x0F] + HEX_CHARS[(h4 >> 8) & 0x0F] +
+        HEX_CHARS[(h4 >> 4) & 0x0F] + HEX_CHARS[h4 & 0x0F] +
+        HEX_CHARS[(h5 >> 28) & 0x0F] + HEX_CHARS[(h5 >> 24) & 0x0F] +
+        HEX_CHARS[(h5 >> 20) & 0x0F] + HEX_CHARS[(h5 >> 16) & 0x0F] +
+        HEX_CHARS[(h5 >> 12) & 0x0F] + HEX_CHARS[(h5 >> 8) & 0x0F] +
+        HEX_CHARS[(h5 >> 4) & 0x0F] + HEX_CHARS[h5 & 0x0F] +
+        HEX_CHARS[(h6 >> 28) & 0x0F] + HEX_CHARS[(h6 >> 24) & 0x0F] +
+        HEX_CHARS[(h6 >> 20) & 0x0F] + HEX_CHARS[(h6 >> 16) & 0x0F] +
+        HEX_CHARS[(h6 >> 12) & 0x0F] + HEX_CHARS[(h6 >> 8) & 0x0F] +
+        HEX_CHARS[(h6 >> 4) & 0x0F] + HEX_CHARS[h6 & 0x0F];
+      if (!this.is224) {
+        hex += HEX_CHARS[(h7 >> 28) & 0x0F] + HEX_CHARS[(h7 >> 24) & 0x0F] +
+          HEX_CHARS[(h7 >> 20) & 0x0F] + HEX_CHARS[(h7 >> 16) & 0x0F] +
+          HEX_CHARS[(h7 >> 12) & 0x0F] + HEX_CHARS[(h7 >> 8) & 0x0F] +
+          HEX_CHARS[(h7 >> 4) & 0x0F] + HEX_CHARS[h7 & 0x0F];
+      }
+      return hex;
+    };
+  
+    Sha256.prototype.toString = Sha256.prototype.hex;
+  
+    Sha256.prototype.digest = function () {
+      this.finalize();
+  
+      var h0 = this.h0, h1 = this.h1, h2 = this.h2, h3 = this.h3, h4 = this.h4, h5 = this.h5,
+        h6 = this.h6, h7 = this.h7;
+  
+      var arr = [
+        (h0 >> 24) & 0xFF, (h0 >> 16) & 0xFF, (h0 >> 8) & 0xFF, h0 & 0xFF,
+        (h1 >> 24) & 0xFF, (h1 >> 16) & 0xFF, (h1 >> 8) & 0xFF, h1 & 0xFF,
+        (h2 >> 24) & 0xFF, (h2 >> 16) & 0xFF, (h2 >> 8) & 0xFF, h2 & 0xFF,
+        (h3 >> 24) & 0xFF, (h3 >> 16) & 0xFF, (h3 >> 8) & 0xFF, h3 & 0xFF,
+        (h4 >> 24) & 0xFF, (h4 >> 16) & 0xFF, (h4 >> 8) & 0xFF, h4 & 0xFF,
+        (h5 >> 24) & 0xFF, (h5 >> 16) & 0xFF, (h5 >> 8) & 0xFF, h5 & 0xFF,
+        (h6 >> 24) & 0xFF, (h6 >> 16) & 0xFF, (h6 >> 8) & 0xFF, h6 & 0xFF
+      ];
+      if (!this.is224) {
+        arr.push((h7 >> 24) & 0xFF, (h7 >> 16) & 0xFF, (h7 >> 8) & 0xFF, h7 & 0xFF);
+      }
+      return arr;
+    };
+  
+    Sha256.prototype.array = Sha256.prototype.digest;
+  
+    Sha256.prototype.arrayBuffer = function () {
+      this.finalize();
+  
+      var buffer = new ArrayBuffer(this.is224 ? 28 : 32);
+      var dataView = new DataView(buffer);
+      dataView.setUint32(0, this.h0);
+      dataView.setUint32(4, this.h1);
+      dataView.setUint32(8, this.h2);
+      dataView.setUint32(12, this.h3);
+      dataView.setUint32(16, this.h4);
+      dataView.setUint32(20, this.h5);
+      dataView.setUint32(24, this.h6);
+      if (!this.is224) {
+        dataView.setUint32(28, this.h7);
+      }
+      return buffer;
+    };
+  
+    function HmacSha256(key, is224, sharedMemory) {
+      var i, type = typeof key;
+      if (type === 'string') {
+        var bytes = [], length = key.length, index = 0, code;
+        for (i = 0; i < length; ++i) {
+          code = key.charCodeAt(i);
+          if (code < 0x80) {
+            bytes[index++] = code;
+          } else if (code < 0x800) {
+            bytes[index++] = (0xc0 | (code >> 6));
+            bytes[index++] = (0x80 | (code & 0x3f));
+          } else if (code < 0xd800 || code >= 0xe000) {
+            bytes[index++] = (0xe0 | (code >> 12));
+            bytes[index++] = (0x80 | ((code >> 6) & 0x3f));
+            bytes[index++] = (0x80 | (code & 0x3f));
+          } else {
+            code = 0x10000 + (((code & 0x3ff) << 10) | (key.charCodeAt(++i) & 0x3ff));
+            bytes[index++] = (0xf0 | (code >> 18));
+            bytes[index++] = (0x80 | ((code >> 12) & 0x3f));
+            bytes[index++] = (0x80 | ((code >> 6) & 0x3f));
+            bytes[index++] = (0x80 | (code & 0x3f));
+          }
+        }
+        key = bytes;
+      } else {
+        if (type === 'object') {
+          if (key === null) {
+            throw new Error(ERROR);
+          } else if (ARRAY_BUFFER && key.constructor === ArrayBuffer) {
+            key = new Uint8Array(key);
+          } else if (!Array.isArray(key)) {
+            if (!ARRAY_BUFFER || !ArrayBuffer.isView(key)) {
+              throw new Error(ERROR);
+            }
+          }
+        } else {
+          throw new Error(ERROR);
+        }
+      }
+  
+      if (key.length > 64) {
+        key = (new Sha256(is224, true)).update(key).array();
+      }
+  
+      var oKeyPad = [], iKeyPad = [];
+      for (i = 0; i < 64; ++i) {
+        var b = key[i] || 0;
+        oKeyPad[i] = 0x5c ^ b;
+        iKeyPad[i] = 0x36 ^ b;
+      }
+  
+      Sha256.call(this, is224, sharedMemory);
+  
+      this.update(iKeyPad);
+      this.oKeyPad = oKeyPad;
+      this.inner = true;
+      this.sharedMemory = sharedMemory;
+    }
+    HmacSha256.prototype = new Sha256();
+  
+    HmacSha256.prototype.finalize = function () {
+      Sha256.prototype.finalize.call(this);
+      if (this.inner) {
+        this.inner = false;
+        var innerHash = this.array();
+        Sha256.call(this, this.is224, this.sharedMemory);
+        this.update(this.oKeyPad);
+        this.update(innerHash);
+        Sha256.prototype.finalize.call(this);
+      }
+    };
+  
+    var exports = createMethod();
+    exports.sha256 = exports;
+    exports.sha224 = createMethod(true);
+    exports.sha256.hmac = createHmacMethod();
+    exports.sha224.hmac = createHmacMethod(true);
+
+    return exports;
+}
+
+export  { factory };

+ 153 - 0
src/app/core/oauth2-oidc/token-validation/jwks-validation-handler.ts

@@ -0,0 +1,153 @@
+import * as rs from 'jsrsasign';
+import {
+  AbstractValidationHandler,
+  ValidationParams,
+} from 'angular-oauth2-oidc';
+
+/**
+ * Validates the signature of an id_token against one
+ * of the keys of an JSON Web Key Set (jwks).
+ *
+ * This jwks can be provided by the discovery document.
+ */
+export class JwksValidationHandler extends AbstractValidationHandler {
+  /**
+   * Allowed algorithms
+   */
+  allowedAlgorithms: string[] = [
+    'HS256',
+    'HS384',
+    'HS512',
+    'RS256',
+    'RS384',
+    'RS512',
+    'ES256',
+    'ES384',
+    'PS256',
+    'PS384',
+    'PS512',
+  ];
+
+  /**
+   * Time period in seconds the timestamp in the signature can
+   * differ from the current time.
+   */
+  gracePeriodInSec = 600;
+
+  validateSignature(params: ValidationParams, retry = false): Promise<any> {
+    if (!params.idToken) throw new Error('Parameter idToken expected!');
+    if (!params.idTokenHeader)
+      throw new Error('Parameter idTokenHandler expected.');
+    if (!params.jwks) throw new Error('Parameter jwks expected!');
+
+    if (
+      !params.jwks['keys'] ||
+      !Array.isArray(params.jwks['keys']) ||
+      params.jwks['keys'].length === 0
+    ) {
+      throw new Error('Array keys in jwks missing!');
+    }
+
+    // console.debug('validateSignature: retry', retry);
+
+    let kid: string = params.idTokenHeader['kid'];
+    let keys: object[] = params.jwks['keys'];
+    let key: object;
+
+    let alg = params.idTokenHeader['alg'];
+
+    if (kid) {
+      key = keys.find((k) => k['kid'] === kid /* && k['use'] === 'sig' */);
+    } else {
+      let kty = this.alg2kty(alg);
+      let matchingKeys = keys.filter(
+        (k) => k['kty'] === kty && k['use'] === 'sig'
+      );
+
+      /*
+            if (matchingKeys.length == 0) {
+                let error = 'No matching key found.';
+                console.error(error);
+                return Promise.reject(error);
+            }*/
+      if (matchingKeys.length > 1) {
+        let error =
+          'More than one matching key found. Please specify a kid in the id_token header.';
+        console.error(error);
+        return Promise.reject(error);
+      } else if (matchingKeys.length === 1) {
+        key = matchingKeys[0];
+      }
+    }
+
+    if (!key && !retry && params.loadKeys) {
+      return params
+        .loadKeys()
+        .then((loadedKeys) => (params.jwks = loadedKeys))
+        .then((_) => this.validateSignature(params, true));
+    }
+
+    if (!key && retry && !kid) {
+      let error = 'No matching key found.';
+      console.error(error);
+      return Promise.reject(error);
+    }
+
+    if (!key && retry && kid) {
+      let error =
+        'expected key not found in property jwks. ' +
+        'This property is most likely loaded with the ' +
+        'discovery document. ' +
+        'Expected key id (kid): ' +
+        kid;
+
+      console.error(error);
+      return Promise.reject(error);
+    }
+
+    let keyObj = rs.KEYUTIL.getKey(key);
+    let validationOptions = {
+      alg: this.allowedAlgorithms,
+      gracePeriod: this.gracePeriodInSec,
+    };
+    let isValid = rs.KJUR.jws.JWS.verifyJWT(
+      params.idToken,
+      keyObj,
+      validationOptions
+    );
+
+    if (isValid) {
+      return Promise.resolve();
+    } else {
+      return Promise.reject('Signature not valid');
+    }
+  }
+
+  private alg2kty(alg: string) {
+    switch (alg.charAt(0)) {
+      case 'R':
+        return 'RSA';
+      case 'E':
+        return 'EC';
+      default:
+        throw new Error('Cannot infer kty from alg: ' + alg);
+    }
+  }
+
+  calcHash(valueToHash: string, algorithm: string): Promise<string> {
+    let hashAlg = new rs.KJUR.crypto.MessageDigest({ alg: algorithm });
+    let result = hashAlg.digestString(valueToHash);
+    let byteArrayAsString = this.toByteArrayAsString(result);
+    return Promise.resolve(byteArrayAsString);
+  }
+
+  toByteArrayAsString(hexString: string) {
+    let result = '';
+    for (let i = 0; i < hexString.length; i += 2) {
+      let hexDigit = hexString.charAt(i) + hexString.charAt(i + 1);
+      let num = parseInt(hexDigit, 16);
+      result += String.fromCharCode(num);
+    }
+    return result;
+  }
+}

+ 14 - 0
src/app/core/oauth2-oidc/token-validation/null-validation-handler.ts

@@ -0,0 +1,14 @@
+import { ValidationHandler, ValidationParams } from './validation-handler';
+
+/**
+ * A validation handler that isn't validating nothing.
+ * Can be used to skip validation (at your own risk).
+ */
+export class NullValidationHandler implements ValidationHandler {
+  validateSignature(validationParams: ValidationParams): Promise<any> {
+    return Promise.resolve(null);
+  }
+  validateAtHash(validationParams: ValidationParams): Promise<boolean> {
+    return Promise.resolve(true);
+  }
+}

+ 92 - 0
src/app/core/oauth2-oidc/token-validation/validation-handler.ts

@@ -0,0 +1,92 @@
+import { base64UrlEncode } from '../base64-helper';
+
+export interface ValidationParams {
+  idToken: string;
+  accessToken: string;
+  idTokenHeader: object;
+  idTokenClaims: object;
+  jwks: object;
+  loadKeys: () => Promise<object>;
+}
+
+/**
+ * Interface for Handlers that are hooked in to
+ * validate tokens.
+ */
+export abstract class ValidationHandler {
+  /**
+   * Validates the signature of an id_token.
+   */
+  public abstract validateSignature(
+    validationParams: ValidationParams
+  ): Promise<any>;
+
+  /**
+   * Validates the at_hash in an id_token against the received access_token.
+   */
+  public abstract validateAtHash(
+    validationParams: ValidationParams
+  ): Promise<boolean>;
+}
+
+/**
+ * This abstract implementation of ValidationHandler already implements
+ * the method validateAtHash. However, to make use of it,
+ * you have to override the method calcHash.
+ */
+export abstract class AbstractValidationHandler implements ValidationHandler {
+  /**
+   * Validates the signature of an id_token.
+   */
+  abstract validateSignature(validationParams: ValidationParams): Promise<any>;
+
+  /**
+   * Validates the at_hash in an id_token against the received access_token.
+   */
+  async validateAtHash(params: ValidationParams): Promise<boolean> {
+    let hashAlg = this.inferHashAlgorithm(params.idTokenHeader);
+
+    let tokenHash = await this.calcHash(params.accessToken, hashAlg); // sha256(accessToken, { asString: true });
+
+    let leftMostHalf = tokenHash.substr(0, tokenHash.length / 2);
+
+    let atHash = base64UrlEncode(leftMostHalf);
+
+    let claimsAtHash = params.idTokenClaims['at_hash'].replace(/=/g, '');
+
+    if (atHash !== claimsAtHash) {
+      console.error('exptected at_hash: ' + atHash);
+      console.error('actual at_hash: ' + claimsAtHash);
+    }
+
+    return atHash === claimsAtHash;
+  }
+
+  /**
+   * Infers the name of the hash algorithm to use
+   * from the alg field of an id_token.
+   *
+   * @param jwtHeader the id_token's parsed header
+   */
+  protected inferHashAlgorithm(jwtHeader: object): string {
+    let alg: string = jwtHeader['alg'];
+
+    if (!alg.match(/^.S[0-9]{3}$/)) {
+      throw new Error('Algorithm not supported: ' + alg);
+    }
+
+    return 'sha-' + alg.substr(2);
+  }
+
+  /**
+   * Calculates the hash for the passed value by using
+   * the passed hash algorithm.
+   *
+   * @param valueToHash
+   * @param algorithm
+   */
+  protected abstract calcHash(
+    valueToHash: string,
+    algorithm: string
+  ): Promise<string>;
+}

+ 4 - 0
src/app/core/oauth2-oidc/tokens.ts

@@ -0,0 +1,4 @@
+import { InjectionToken } from '@angular/core';
+import { AuthConfig } from './auth.config';
+
+export const AUTH_CONFIG = new InjectionToken<AuthConfig>('AUTH_CONFIG');

+ 204 - 0
src/app/core/oauth2-oidc/types.ts

@@ -0,0 +1,204 @@
+/* eslint-disable @typescript-eslint/member-ordering */
+/* eslint-disable @typescript-eslint/naming-convention */
+/* eslint-disable @typescript-eslint/ban-types */
+import { Injectable } from '@angular/core';
+
+/**
+ * Additional options that can be passed to tryLogin.
+ */
+export class LoginOptions {
+  /**
+   * Is called, after a token has been received and
+   * successfully validated.
+   *
+   * Deprecated:  Use property ``events`` on OAuthService instead.
+   */
+  onTokenReceived?: (receivedTokens: ReceivedTokens) => void;
+
+  /**
+   * Hook, to validate the received tokens.
+   *
+   * Deprecated:  Use property ``tokenValidationHandler`` on OAuthService instead.
+   */
+  validationHandler?: (receivedTokens: ReceivedTokens) => Promise<any>;
+
+  /**
+   * Called when tryLogin detects that the auth server
+   * included an error message into the hash fragment.
+   *
+   * Deprecated:  Use property ``events`` on OAuthService instead.
+   */
+  onLoginError?: (params: object) => void;
+
+  /**
+   * A custom hash fragment to be used instead of the
+   * actual one. This is used for silent refreshes, to
+   * pass the iframes hash fragment to this method, and
+   * is also used by popup flows in the same manner.
+   * This can be used with code flow, where is must be set
+   * to a hash symbol followed by the querystring. The
+   * question mark is optional, but may be present following
+   * the hash symbol.
+   */
+  customHashFragment?: string;
+
+  /**
+   * Set this to true to disable the oauth2 state
+   * check which is a best practice to avoid
+   * security attacks.
+   * As OIDC defines a nonce check that includes
+   * this, this can be set to true when only doing
+   * OIDC.
+   */
+  disableOAuth2StateCheck?: boolean;
+
+  /**
+   * Set this to true to disable the nonce
+   * check which is used to avoid
+   * replay attacks.
+   * This flag should never be true in
+   * production environments.
+   */
+  disableNonceCheck?= false;
+
+  /**
+   * Normally, you want to clear your hash fragment after
+   * the lib read the token(s) so that they are not displayed
+   * anymore in the url. If not, set this to true. For code flow
+   * this controls removing query string values.
+   */
+  preventClearHashAfterLogin?= false;
+
+  /**
+   * Set this for code flow if you used a custom redirect Uri
+   * when retrieving the code. This is used internally for silent
+   * refresh and popup flows.
+   */
+  customRedirectUri?: string;
+}
+
+/**
+ * Defines the logging interface the OAuthService uses
+ * internally. Is compatible with the `console` object,
+ * but you can provide your own implementation as well
+ * through dependency injection.
+ */
+export abstract class OAuthLogger {
+  abstract debug(message?: any, ...optionalParams: any[]): void;
+  abstract info(message?: any, ...optionalParams: any[]): void;
+  abstract log(message?: any, ...optionalParams: any[]): void;
+  abstract warn(message?: any, ...optionalParams: any[]): void;
+  abstract error(message?: any, ...optionalParams: any[]): void;
+}
+
+/**
+ * Defines a simple storage that can be used for
+ * storing the tokens at client side.
+ * Is compatible to localStorage and sessionStorage,
+ * but you can also create your own implementations.
+ */
+export abstract class OAuthStorage {
+  abstract getItem(key: string): Promise<string>;
+  abstract removeItem(key: string): Promise<void>;
+  abstract setItem(key: string, data: string): Promise<void>;
+}
+
+@Injectable()
+export class MemoryStorage implements OAuthStorage {
+  private data = new Map<string, string>();
+
+  getItem(key: string): Promise<string> {
+    return Promise.reject(this.data.get(key));
+  }
+
+  removeItem(key: string): Promise<void> {
+    return Promise.reject(this.data.delete(key));
+  }
+
+  setItem(key: string, data: string): Promise<void> {
+    return Promise.reject(this.data.set(key, data));
+  }
+}
+
+/**
+ * Represents the received tokens, the received state
+ * and the parsed claims from the id-token.
+ */
+export class ReceivedTokens {
+  idToken: string;
+  accessToken: string;
+  // eslint-disable-next-line @typescript-eslint/ban-types
+  idClaims?: object;
+  state?: string;
+}
+
+/**
+ * Represents the parsed and validated id_token.
+ */
+export interface ParsedIdToken {
+  idToken: string;
+  idTokenClaims: object;
+  idTokenHeader: object;
+  idTokenClaimsJson: string;
+  idTokenHeaderJson: string;
+  idTokenExpiresAt: number;
+}
+
+/**
+ * Represents the response from the token endpoint
+ * http://openid.net/specs/openid-connect-core-1_0.html#TokenEndpoint
+ */
+export interface TokenResponse {
+  access_token: string;
+  id_token: string;
+  token_type: string;
+  expires_in: number;
+  refresh_token: string;
+  scope: string;
+  state?: string;
+}
+
+/**
+ * Represents the response from the user info endpoint
+ * http://openid.net/specs/openid-connect-core-1_0.html#UserInfo
+ */
+export interface UserInfo {
+  sub: string;
+  [key: string]: any;
+}
+
+/**
+ * Represents an OpenID Connect discovery document
+ */
+export interface OidcDiscoveryDoc {
+  issuer: string;
+  authorization_endpoint: string;
+  token_endpoint: string;
+  token_endpoint_auth_methods_supported: string[];
+  token_endpoint_auth_signing_alg_values_supported: string[];
+  userinfo_endpoint: string;
+  check_session_iframe: string;
+  end_session_endpoint: string;
+  jwks_uri: string;
+  registration_endpoint: string;
+  scopes_supported: string[];
+  response_types_supported: string[];
+  acr_values_supported: string[];
+  response_modes_supported: string[];
+  grant_types_supported: string[];
+  subject_types_supported: string[];
+  userinfo_signing_alg_values_supported: string[];
+  userinfo_encryption_alg_values_supported: string[];
+  userinfo_encryption_enc_values_supported: string[];
+  id_token_signing_alg_values_supported: string[];
+  id_token_encryption_alg_values_supported: string[];
+  id_token_encryption_enc_values_supported: string[];
+  request_object_signing_alg_values_supported: string[];
+  display_values_supported: string[];
+  claim_types_supported: string[];
+  claims_supported: string[];
+  claims_parameter_supported: boolean;
+  service_documentation: string;
+  ui_locales_supported: string[];
+  revocation_endpoint: string;
+}

+ 59 - 0
src/app/core/oauth2-oidc/url-helper.service.ts

@@ -0,0 +1,59 @@
+import { Injectable } from '@angular/core';
+
+@Injectable()
+export class UrlHelperService {
+  public getHashFragmentParams(customHashFragment?: string): object {
+    let hash = customHashFragment || window.location.hash;
+
+    hash = decodeURIComponent(hash);
+
+    if (hash.indexOf('#') !== 0) {
+      return {};
+    }
+
+    const questionMarkPosition = hash.indexOf('?');
+
+    if (questionMarkPosition > -1) {
+      hash = hash.substr(questionMarkPosition + 1);
+    } else {
+      hash = hash.substr(1);
+    }
+
+    return this.parseQueryString(hash);
+  }
+
+  public parseQueryString(queryString: string): object {
+    const data = {};
+    let pairs, pair, separatorIndex, escapedKey, escapedValue, key, value;
+
+    if (queryString === null) {
+      return data;
+    }
+
+    pairs = queryString.split('&');
+
+    for (let i = 0; i < pairs.length; i++) {
+      pair = pairs[i];
+      separatorIndex = pair.indexOf('=');
+
+      if (separatorIndex === -1) {
+        escapedKey = pair;
+        escapedValue = null;
+      } else {
+        escapedKey = pair.substr(0, separatorIndex);
+        escapedValue = pair.substr(separatorIndex + 1);
+      }
+
+      key = decodeURIComponent(escapedKey);
+      value = decodeURIComponent(escapedValue);
+
+      if (key.substr(0, 1) === '/') {
+        key = key.substr(1);
+      }
+
+      data[key] = value;
+    }
+
+    return data;
+  }
+}

+ 140 - 0
src/app/core/services/auth.service.ts

@@ -0,0 +1,140 @@
+import { Injectable } from '@angular/core';
+import { OAuthSuccessEvent } from '../oauth2-oidc/events';
+import { OAuthService } from '../oauth2-oidc/oauth-service';
+import { Platform } from '@ionic/angular';
+import { StorageService } from './storage.service';
+import { Router } from '@angular/router';
+import { InAppBrowser } from '@ionic-native/in-app-browser/ngx';
+import { ButtonService } from './button.service';
+
+@Injectable({
+  providedIn: 'root',
+})
+export class AuthService {
+  loginBackKey = 'passport-login-call-back';
+  loginBackRoute = '/passport/login-call-back';
+  loginRoute = '/user-profile';
+  constructor(
+    private oauthService: OAuthService,
+    private platform: Platform,
+    private stoage: StorageService,
+    private router: Router,
+    private iab: InAppBrowser,
+    private btnService: ButtonService,
+  ) {
+    let serUrl = 'http://127.0.0.1:100';
+
+    if (platform.is('hybrid')) {
+      serUrl = 'http://192.168.137.1:100';
+    }
+    this.oauthService.configure({
+      issuer: serUrl,
+      // issuer: 'https://idsvr4.azurewebsites.net',
+
+      // URL of the SPA to redirect the user to after login
+      redirectUri: window.location.origin + this.loginBackRoute,
+
+      // The SPA's id. The SPA is registerd with this id at the auth-server
+      // clientId: 'server.code',
+      clientId: 'SPA',
+      postLogoutRedirectUri: window.location.origin + this.loginRoute,
+      dummyClientSecret: 'secret',
+
+      // Just needed if your auth server demands a secret. In general, this
+      // is a sign that the auth server is not configured with SPAs in mind
+      // and it might not enforce further best practices vital for security
+      // such applications.
+      // dummyClientSecret: 'secret',
+      responseType: 'code',
+      requireHttps: false,
+      // set the scope for the permissions the client should request
+      // The first four are defined by OIDC.
+      // Important: Request offline_access to get a refresh token
+      // The api scope is a usecase specific one
+      scope: 'openid profile',
+
+      showDebugInformation: true,
+    });
+
+    // this.load();
+  }
+
+  async load(): Promise<OAuthSuccessEvent> {
+    const oAuthSuccessEvent = await this.oauthService.loadDiscoveryDocument();
+    const isLogin = await this.oauthService.hasValidAccessToken();
+    if (!isLogin) {
+      await this.oauthService.tryLogin();
+    }
+    return oAuthSuccessEvent;
+  }
+
+  async login() {
+    // 登录前先保存登录地址缓存
+    await this.stoage.setItem(this.loginBackKey, this.router.url);
+    // 登录前先跳转无需权限页面,比如登录等待界面
+    this.router.navigateByUrl(this.loginRoute, { replaceUrl: true });
+    if (this.platform.is('hybrid')) {
+      await this.mobileLogin();
+    } else {
+      await this.webLogin();
+    }
+  }
+
+  async webLogin() {
+    this.oauthService.initLoginFlow();
+  }
+
+  async mobileLogin() {
+    const loginUrl = await this.oauthService.getLoginUrl();
+
+    const browser = this.iab.create(loginUrl, '_blank', {
+      location: 'no',
+      zoom: 'no',
+    });
+    const listener = browser.on('loadstart').subscribe((event) => {
+      if (event.url.indexOf(this.loginBackRoute) > -1) {
+        const customHashFragment = event.url.split(this.loginBackRoute)[1];
+        this.oauthService.tryLoginCodeFlow({ customHashFragment }).then(() => {
+          // 跳转登录成功页面
+          this.router.navigateByUrl(this.loginBackRoute, { replaceUrl: true });
+        });
+
+        listener.unsubscribe();
+        browser.close();
+      }
+    });
+  }
+
+  async isLogined() {
+    const isLogin = await this.oauthService.hasValidAccessToken();
+
+    // return isLogin;
+    if (!isLogin) {
+      this.login();
+    }
+    return isLogin;
+  }
+
+  async token(): Promise<string> {
+    return this.oauthService.getIdToken();
+  }
+
+  async loginOut() {
+    if (this.platform.is('hybrid')) {
+      const loginUrl = await this.oauthService.getLogOutUrl();
+      const browser = this.iab.create(loginUrl, '_blank', {
+        location: 'no',
+        zoom: 'no',
+      });
+      const listener = browser.on('loadstart').subscribe((event) => {
+        if (event.url.indexOf(this.loginRoute) > -1) {
+          listener.unsubscribe();
+          browser.close();
+          this.btnService.exit();
+        }
+      });
+    } else {
+      await this.oauthService.logOut();
+    }
+  }
+}

+ 38 - 0
src/app/core/services/button.service.ts

@@ -0,0 +1,38 @@
+import { Injectable } from '@angular/core';
+import { IonRouterOutlet, Platform } from '@ionic/angular';
+import { App } from '@capacitor/app';
+import { ModalService } from 'ng-zorro-antd-mobile';
+
+@Injectable({
+  providedIn: 'root'
+})
+export class ButtonService {
+  closrModel;
+  constructor(
+    private platform: Platform,
+    private modal: ModalService
+  ) {
+
+  }
+
+  init(routerOutlet: IonRouterOutlet) {
+    this.platform.backButton.subscribeWithPriority(-1, () => {
+      if (!routerOutlet.canGoBack() && !this.closrModel) {
+
+        this.closrModel = this.modal.alert('退出程序', '您是否确认 ?', [
+          { text: '取消', onPress: () => this.closrModel = null },
+          { text: '确定', onPress: () => this.exit() }
+        ]);
+
+      } else
+        if (!routerOutlet.canGoBack() && this.closrModel) {
+          this.modal.close();
+          this.closrModel = null;
+        }
+    });
+  }
+
+  exit() {
+    App.exitApp();
+  }
+}

+ 25 - 0
src/app/core/services/menu.service.ts

@@ -0,0 +1,25 @@
+import { Injectable } from '@angular/core';
+
+@Injectable({
+  providedIn: 'root'
+})
+export class MenuService {
+
+  menu = Array.from(new Array(30)).map((_val, i) => ({
+    icon: '/icons-angular/assets/animal/panda.svg',
+    text: `测试类菜单${i}`,
+    url: i % 2 ? `/full/product/test` : '',
+    children: i % 2 ? undefined : Array.from(new Array(10)).map((_v, j) => ({
+      icon: '/icons-angular/assets/animal/panda.svg',
+      text: `测试子菜单${j}`,
+      url: `/full/product/test`
+    }))
+  }));
+  constructor() {
+
+  }
+
+  getChildMenu(text) {
+    return this.menu.filter(v => v.text === text)[0];
+  }
+}

+ 22 - 0
src/app/core/services/route-auth-guard.service.ts

@@ -0,0 +1,22 @@
+import { Injectable } from '@angular/core';
+import { ActivatedRouteSnapshot, CanActivate, CanActivateChild, Router, RouterStateSnapshot, UrlTree } from '@angular/router';
+import { Observable } from 'rxjs';
+import { AuthService } from './auth.service';
+
+@Injectable({
+  providedIn: 'root'
+})
+export class RouteAuthGuardService implements CanActivate, CanActivateChild {
+
+
+
+  constructor(private authService: AuthService, private router: Router) { }
+  canActivateChild(childRoute: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean | UrlTree | Observable<boolean | UrlTree> | Promise<boolean | UrlTree> {
+    return this.canActivate(childRoute, state);
+  }
+  canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean | UrlTree | Observable<boolean | UrlTree> | Promise<boolean | UrlTree> {
+    return this.authService.isLogined().then(isLogined => {
+      return isLogined;
+    });
+  }
+}

+ 17 - 0
src/app/core/services/startup.service.ts

@@ -0,0 +1,17 @@
+import { Injectable } from '@angular/core';
+import { AuthService } from './auth.service';
+import { IconService } from '@ant-design/icons-angular';
+import { ButtonService } from './button.service';
+
+@Injectable({
+  providedIn: 'root'
+})
+export class StartupService {
+
+  constructor(private authService: AuthService, private iconService: IconService) { }
+
+  async load(): Promise<any> {
+    this.iconService.changeAssetsSource('icons-angular'); // 修改 阿里图标位置
+    await this.authService.load();
+  }
+}

+ 158 - 0
src/app/core/services/storage.service.ts

@@ -0,0 +1,158 @@
+import { Injectable } from '@angular/core';
+import { Storage } from '@ionic/storage-angular';
+import { OAuthStorage } from '../oauth2-oidc/types';
+
+@Injectable({
+  providedIn: 'root'
+})
+export class StorageService implements OAuthStorage {
+  private _storage: Storage | null = null;
+  constructor(private storage: Storage) { }
+  async getItem(key: string): Promise<string> {
+
+    if (!this._storage)
+      await this.init();
+
+    return await this._storage.get(key);
+  }
+  async removeItem(key: string): Promise<void> {
+    if (!this._storage)
+      await this.init();
+
+    await this._storage.remove(key);
+  }
+  async setItem(key: string, data: string): Promise<void> {
+
+    if (!this._storage)
+      await this.init();
+
+    await this._storage.set(key, data);
+  }
+
+  async init() {
+    // If using, define drivers here: await this.storage.defineDriver(/*...*/);
+    const storage = await this.storage.create();
+    this._storage = storage;
+  }
+
+
+}
+
+
+
+// @Injectable({
+//   providedIn: 'root'
+// })
+// export class StorageService implements Storage {
+
+//   private _storage: Storage | null = null;
+//   constructor(private storageConfig: StorageConfig) {
+
+//     switch (storageConfig.storageType) {
+//       case 'loaclStorage':
+//         this._storage = localStorage;
+//         break;
+//       case 'sessionStorage':
+//         this._storage = sessionStorage;
+//         break;
+//       case 'indexDBStorage':
+//         this._storage = new IndexDBStorage();
+//         break;
+
+//       case 'cookieStorage':
+//         this._storage = new CookieStorage();
+//         break;
+
+//       case 'customStorage':
+//         this._storage = new CustomStorage();
+//         break;
+//       default:
+//         break;
+//     }
+//   }
+//   [name: string]: any;
+//   length: number;
+//   clear(): void {
+//     this._storage.clear();
+//   }
+//   getItem(key: string): string {
+//     return this._storage.getItem(key);
+//   }
+//   key(index: number): string {
+//     return this._storage.key(index);
+//   }
+//   removeItem(key: string): void {
+//     return this._storage.removeItem(key);
+//   }
+//   setItem(key: string, value: string): void {
+//     this._storage.setItem(key, value);
+//   }
+
+// }
+
+// class CookieStorage implements Storage {
+//   [name: string]: any;
+//   length: number;
+//   clear(): void {
+//     throw new Error('Method not implemented.');
+//   }
+//   getItem(key: string): string {
+//     throw new Error('Method not implemented.');
+//   }
+//   key(index: number): string {
+//     throw new Error('Method not implemented.');
+//   }
+//   removeItem(key: string): void {
+//     throw new Error('Method not implemented.');
+//   }
+//   setItem(key: string, value: string): void {
+//     throw new Error('Method not implemented.');
+//   }
+
+// }
+
+// class CustomStorage implements Storage {
+//   [name: string]: any;
+//   length: number;
+//   clear(): void {
+//     throw new Error('Method not implemented.');
+//   }
+//   getItem(key: string): string {
+//     throw new Error('Method not implemented.');
+//   }
+//   key(index: number): string {
+//     throw new Error('Method not implemented.');
+//   }
+//   removeItem(key: string): void {
+//     throw new Error('Method not implemented.');
+//   }
+//   setItem(key: string, value: string): void {
+//     throw new Error('Method not implemented.');
+//   }
+
+// }
+
+// class IndexDBStorage implements Storage {
+//   [name: string]: any;
+//   length: number;
+//   clear(): void {
+//     throw new Error('Method not implemented.');
+//   }
+//   getItem(key: string): string {
+//     throw new Error('Method not implemented.');
+//   }
+//   key(index: number): string {
+//     throw new Error('Method not implemented.');
+//   }
+//   removeItem(key: string): void {
+//     throw new Error('Method not implemented.');
+//   }
+//   setItem(key: string, value: string): void {
+//     throw new Error('Method not implemented.');
+//   }
+
+// }
+
+// class StorageConfig {
+//   storageType = '';
+// }

+ 101 - 0
src/app/core/services/update.service.ts

@@ -0,0 +1,101 @@
+import { HttpClient } from '@angular/common/http';
+import { EventEmitter, Injectable } from '@angular/core';
+import { AppVersion } from '@ionic-native/app-version/ngx';
+import { FileTransfer, FileUploadOptions, FileTransferObject } from '@ionic-native/file-transfer/ngx';
+import { FileOpener } from '@ionic-native/file-opener/ngx';
+import { File } from '@ionic-native/file/ngx';
+
+
+@Injectable({
+  providedIn: 'root'
+})
+export class UpdateService {
+  // 获取最新版本地址
+  serviceVersionInfoUrl = 'http://192.168.137.1/';
+  serviceVersionInfo: { elements: { versionName: string, outputFile: string }[] } = null;
+  onDownProgress = new EventEmitter<number>();
+
+  constructor(
+    private appVersion: AppVersion,
+    private file: File,
+    private transfer: FileTransfer,
+    private fileOpener: FileOpener,
+    private http: HttpClient) {
+    this.onDownProgress.subscribe(p => {
+      console.log(`AppDownProgress: ${p}%`);
+    });
+  }
+
+  getAppVersion(): Promise<string> {
+    return this.appVersion.getVersionNumber();
+  }
+
+  getAppName(): Promise<string> {
+    return this.appVersion.getAppName();
+  }
+
+  loadVersionInfo(): Promise<{ elements: { versionName: string, outputFile: string }[] }> {
+    return new Promise((resolve, reject) => {
+      this.http.get<string>(this.serviceVersionInfoUrl + 'output-metadata.json').subscribe(serviceVersion => {
+        console.log('serv: ' + JSON.stringify(serviceVersion));
+        resolve(serviceVersion as any);
+      }, error => {
+        console.log(JSON.stringify(error));
+      })
+    });
+  }
+
+
+  async getServiceVersion(): Promise<string> {
+    if (!this.serviceVersionInfo) {
+      this.serviceVersionInfo = await this.loadVersionInfo();
+    }
+
+    return this.serviceVersionInfo.elements[0].versionName;
+  }
+
+  async getServiceAppName(): Promise<string> {
+    if (!this.serviceVersionInfo) {
+      this.serviceVersionInfo = await this.loadVersionInfo();
+    }
+
+    return this.serviceVersionInfo.elements[0].outputFile;
+  }
+
+  async isNewVersion(): Promise<boolean> {
+    const appVersion = await this.getAppVersion();
+    console.log('appVersion: ' + appVersion);
+    const serviceVersion = await this.getServiceVersion();
+    console.log('serviceVersion: ' + serviceVersion);
+    return appVersion !== serviceVersion;
+  }
+
+  async updateApp() {
+    if (await this.isNewVersion()) {
+      await this.downApp();
+    }
+  }
+
+  async downApp() {
+    const fileTransfer: FileTransferObject = this.transfer.create();
+
+    const serviceAppName = await this.getServiceAppName();
+    const apk = this.file.dataDirectory + serviceAppName;
+    fileTransfer.download(this.serviceVersionInfoUrl + serviceAppName, apk).then((entry) => {
+      this.fileOpener.open(apk, "application/vnd.android.package-archive")
+        .then((e) => console.log(e))
+        .catch(e => console.log(e));
+      console.log('download complete: ');
+    }, (error) => {
+      // handle error
+
+      console.log('download error :' + JSON.stringify(error))
+    });
+    this.onDownProgress.emit(0);
+    fileTransfer.onProgress((event) => {
+      let num = Math.ceil(event.loaded / event.total * 100);  //转化成1-100的进度
+      this.onDownProgress.emit(num);
+    });
+  }
+
+}

+ 0 - 16
src/app/home/home-routing.module.ts

@@ -1,16 +0,0 @@
-import { NgModule } from '@angular/core';
-import { RouterModule, Routes } from '@angular/router';
-import { HomePage } from './home.page';
-
-const routes: Routes = [
-  {
-    path: '',
-    component: HomePage,
-  }
-];
-
-@NgModule({
-  imports: [RouterModule.forChild(routes)],
-  exports: [RouterModule]
-})
-export class HomePageRoutingModule {}

+ 0 - 19
src/app/home/home.module.ts

@@ -1,19 +0,0 @@
-import { NgModule } from '@angular/core';
-import { CommonModule } from '@angular/common';
-import { IonicModule } from '@ionic/angular';
-import { FormsModule } from '@angular/forms';
-import { HomePage } from './home.page';
-
-import { HomePageRoutingModule } from './home-routing.module';
-
-
-@NgModule({
-  imports: [
-    CommonModule,
-    FormsModule,
-    IonicModule,
-    HomePageRoutingModule
-  ],
-  declarations: [HomePage]
-})
-export class HomePageModule {}

+ 0 - 20
src/app/home/home.page.html

@@ -1,20 +0,0 @@
-<ion-header [translucent]="true">
-  <ion-toolbar>
-    <ion-title>
-      Blank
-    </ion-title>
-  </ion-toolbar>
-</ion-header>
-
-<ion-content [fullscreen]="true">
-  <ion-header collapse="condense">
-    <ion-toolbar>
-      <ion-title size="large">Blank</ion-title>
-    </ion-toolbar>
-  </ion-header>
-
-  <div id="container">
-    <strong>Ready to create an app?</strong>
-    <p>Start with Ionic <a target="_blank" rel="noopener noreferrer" href="https://ionicframework.com/docs/components">UI Components</a></p>
-  </div>
-</ion-content>

+ 0 - 0
src/app/home/home.page.scss


Some files were not shown because too many files changed in this diff