summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.browserslistrc7
-rwxr-xr-x.dockerignore2
-rw-r--r--.editorconfig16
-rw-r--r--.eslintrc.json39
-rw-r--r--.gitignore59
-rw-r--r--.prettierignore6
-rw-r--r--.prettierrc.json6
-rw-r--r--Dockerfile9
-rw-r--r--LICENSE201
-rw-r--r--LICENSE_HEADER15
-rw-r--r--README.md81
-rw-r--r--angular.json135
-rw-r--r--development/.env52
-rw-r--r--development/config/onap-realm.json228
-rw-r--r--development/docker-compose.yml90
-rw-r--r--development/request.http159
-rwxr-xr-xdevelopment/run.sh7
-rwxr-xr-xdevelopment/stop.sh8
-rwxr-xr-xdocker_entrypoint.sh27
-rw-r--r--e2e/protractor.conf.js38
-rw-r--r--e2e/src/app.e2e-spec.ts44
-rw-r--r--e2e/src/app.po.ts30
-rw-r--r--e2e/tsconfig.json9
-rw-r--r--karma.conf.js45
-rw-r--r--license-check-and-add-config.json22
-rw-r--r--license-report-config.json12
-rw-r--r--local.proxy.config.json20
-rwxr-xr-xlocal.sh25
-rw-r--r--openapi/config.json6
-rw-r--r--openapi/input/api.yaml1083
-rwxr-xr-xopenapi/openapi-generator-cli70
-rw-r--r--package.json102
-rw-r--r--server/nginx.template111
-rw-r--r--server/resty/evp.lua804
-rw-r--r--server/resty/hmac.lua167
-rw-r--r--server/resty/http.lua1178
-rw-r--r--server/resty/http_connect.lua274
-rw-r--r--server/resty/http_headers.lua44
-rw-r--r--server/resty/jwt-validators.lua412
-rw-r--r--server/resty/jwt.lua959
-rw-r--r--server/resty/openidc.lua1870
-rw-r--r--server/resty/openssl.lua476
-rw-r--r--server/resty/openssl/asn1.lua91
-rw-r--r--server/resty/openssl/auxiliary/bio.lua43
-rw-r--r--server/resty/openssl/auxiliary/ctypes.lua28
-rw-r--r--server/resty/openssl/auxiliary/jwk.lua261
-rw-r--r--server/resty/openssl/auxiliary/nginx.lua318
-rw-r--r--server/resty/openssl/auxiliary/nginx_c.lua154
-rw-r--r--server/resty/openssl/bn.lua416
-rw-r--r--server/resty/openssl/cipher.lua300
-rw-r--r--server/resty/openssl/ctx.lua78
-rw-r--r--server/resty/openssl/dh.lua142
-rw-r--r--server/resty/openssl/digest.lua116
-rw-r--r--server/resty/openssl/ec.lua186
-rw-r--r--server/resty/openssl/ecx.lua67
-rw-r--r--server/resty/openssl/err.lua62
-rw-r--r--server/resty/openssl/hmac.lua90
-rw-r--r--server/resty/openssl/include/asn1.lua94
-rw-r--r--server/resty/openssl/include/bio.lua13
-rw-r--r--server/resty/openssl/include/bn.lua77
-rw-r--r--server/resty/openssl/include/conf.lua9
-rw-r--r--server/resty/openssl/include/crypto.lua31
-rw-r--r--server/resty/openssl/include/dh.lua80
-rw-r--r--server/resty/openssl/include/ec.lua59
-rw-r--r--server/resty/openssl/include/err.lua9
-rw-r--r--server/resty/openssl/include/evp.lua109
-rw-r--r--server/resty/openssl/include/evp/cipher.lua123
-rw-r--r--server/resty/openssl/include/evp/kdf.lua148
-rw-r--r--server/resty/openssl/include/evp/mac.lua38
-rw-r--r--server/resty/openssl/include/evp/md.lua86
-rw-r--r--server/resty/openssl/include/evp/pkey.lua234
-rw-r--r--server/resty/openssl/include/hmac.lua48
-rw-r--r--server/resty/openssl/include/objects.lua19
-rw-r--r--server/resty/openssl/include/ossl_typ.lua71
-rw-r--r--server/resty/openssl/include/param.lua71
-rw-r--r--server/resty/openssl/include/pem.lua50
-rw-r--r--server/resty/openssl/include/pkcs12.lua31
-rw-r--r--server/resty/openssl/include/provider.lua27
-rw-r--r--server/resty/openssl/include/rand.lua24
-rw-r--r--server/resty/openssl/include/rsa.lua70
-rw-r--r--server/resty/openssl/include/ssl.lua113
-rw-r--r--server/resty/openssl/include/stack.lua95
-rw-r--r--server/resty/openssl/include/x509/altname.lua49
-rw-r--r--server/resty/openssl/include/x509/crl.lua86
-rw-r--r--server/resty/openssl/include/x509/csr.lua88
-rw-r--r--server/resty/openssl/include/x509/extension.lua44
-rw-r--r--server/resty/openssl/include/x509/init.lua138
-rw-r--r--server/resty/openssl/include/x509/name.lua21
-rw-r--r--server/resty/openssl/include/x509/revoked.lua17
-rw-r--r--server/resty/openssl/include/x509_vfy.lua108
-rw-r--r--server/resty/openssl/include/x509v3.lua108
-rw-r--r--server/resty/openssl/kdf.lua388
-rw-r--r--server/resty/openssl/mac.lua96
-rw-r--r--server/resty/openssl/objects.lua74
-rw-r--r--server/resty/openssl/param.lua322
-rw-r--r--server/resty/openssl/pkcs12.lua168
-rw-r--r--server/resty/openssl/pkey.lua942
-rw-r--r--server/resty/openssl/provider.lua136
-rw-r--r--server/resty/openssl/rand.lua51
-rw-r--r--server/resty/openssl/rsa.lua155
-rw-r--r--server/resty/openssl/ssl.lua353
-rw-r--r--server/resty/openssl/ssl_ctx.lua95
-rw-r--r--server/resty/openssl/stack.lua159
-rw-r--r--server/resty/openssl/version.lua117
-rw-r--r--server/resty/openssl/x509/altname.lua248
-rw-r--r--server/resty/openssl/x509/chain.lua76
-rw-r--r--server/resty/openssl/x509/crl.lua607
-rw-r--r--server/resty/openssl/x509/csr.lua531
-rw-r--r--server/resty/openssl/x509/extension.lua281
-rw-r--r--server/resty/openssl/x509/extension/dist_points.lua75
-rw-r--r--server/resty/openssl/x509/extension/info_access.lua137
-rw-r--r--server/resty/openssl/x509/extensions.lua84
-rw-r--r--server/resty/openssl/x509/init.lua1071
-rw-r--r--server/resty/openssl/x509/name.lua156
-rw-r--r--server/resty/openssl/x509/revoked.lua108
-rw-r--r--server/resty/openssl/x509/store.lua227
-rw-r--r--server/resty/session.lua771
-rw-r--r--server/resty/session/ciphers/aes.lua113
-rw-r--r--server/resty/session/ciphers/none.lua15
-rw-r--r--server/resty/session/compressors/none.lua15
-rw-r--r--server/resty/session/compressors/zlib.lua43
-rw-r--r--server/resty/session/encoders/base16.lua29
-rw-r--r--server/resty/session/encoders/base64.lua39
-rw-r--r--server/resty/session/encoders/hex.lua1
-rw-r--r--server/resty/session/hmac/sha1.lua1
-rw-r--r--server/resty/session/identifiers/random.lua13
-rw-r--r--server/resty/session/serializers/json.lua6
-rw-r--r--server/resty/session/storage/cookie.lua7
-rw-r--r--server/resty/session/storage/dshm.lua163
-rw-r--r--server/resty/session/storage/memcache.lua303
-rw-r--r--server/resty/session/storage/memcached.lua1
-rw-r--r--server/resty/session/storage/redis.lua478
-rw-r--r--server/resty/session/storage/shm.lua125
-rw-r--r--server/resty/session/strategies/default.lua232
-rw-r--r--server/resty/session/strategies/regenerate.lua43
-rw-r--r--sonar-project.properties33
-rw-r--r--src/app/app-routing.module.ts55
-rw-r--r--src/app/app.component.css82
-rw-r--r--src/app/app.component.html52
-rw-r--r--src/app/app.component.spec.ts64
-rw-r--r--src/app/app.component.ts50
-rw-r--r--src/app/app.module.ts103
-rw-r--r--src/app/components/layout/header/header/header.component.css114
-rw-r--r--src/app/components/layout/header/header/header.component.html142
-rw-r--r--src/app/components/layout/header/header/header.component.spec.ts43
-rw-r--r--src/app/components/layout/header/header/header.component.ts113
-rw-r--r--src/app/components/layout/sidemenu/sidemenu.component.css113
-rw-r--r--src/app/components/layout/sidemenu/sidemenu.component.html51
-rw-r--r--src/app/components/layout/sidemenu/sidemenu.component.spec.ts43
-rw-r--r--src/app/components/layout/sidemenu/sidemenu.component.ts47
-rw-r--r--src/app/components/modal/modal-content.html30
-rw-r--r--src/app/components/modal/modal-content.ts32
-rw-r--r--src/app/components/modal/modal.service.ts38
-rw-r--r--src/app/components/page-not-found/page-not-found.css36
-rw-r--r--src/app/components/page-not-found/page-not-found.html29
-rw-r--r--src/app/components/page-not-found/page-not-found.ts27
-rw-r--r--src/app/components/shared/breadcrumb-item/breadcrumb-item.component.html22
-rw-r--r--src/app/components/shared/breadcrumb-item/breadcrumb-item.component.ts29
-rw-r--r--src/app/components/shared/breadcrumb/breadcrumb.component.html26
-rw-r--r--src/app/components/shared/breadcrumb/breadcrumb.component.ts30
-rw-r--r--src/app/components/shared/confirmation-modal/confirmation-modal.component.html35
-rw-r--r--src/app/components/shared/confirmation-modal/confirmation-modal.component.spec.ts19
-rw-r--r--src/app/components/shared/confirmation-modal/confirmation-modal.component.ts39
-rw-r--r--src/app/components/shared/loading-spinner/loading-spinner.component.html21
-rw-r--r--src/app/components/shared/loading-spinner/loading-spinner.component.ts26
-rw-r--r--src/app/components/shared/pagination/pagination.component.css23
-rw-r--r--src/app/components/shared/pagination/pagination.component.html48
-rw-r--r--src/app/components/shared/pagination/pagination.component.spec.ts43
-rw-r--r--src/app/components/shared/pagination/pagination.component.ts88
-rw-r--r--src/app/components/shared/status-page/status-page.component.css19
-rw-r--r--src/app/components/shared/status-page/status-page.component.html23
-rw-r--r--src/app/components/shared/status-page/status-page.component.spec.ts44
-rw-r--r--src/app/components/shared/status-page/status-page.component.ts37
-rw-r--r--src/app/components/shared/table-skeleton/table-skeleton.component.css37
-rw-r--r--src/app/components/shared/table-skeleton/table-skeleton.component.html31
-rw-r--r--src/app/components/shared/table-skeleton/table-skeleton.component.ts29
-rw-r--r--src/app/directives/has-permissions.directive.ts57
-rw-r--r--src/app/directives/hide-empty.directive.ts65
-rw-r--r--src/app/errorhandling/global-error-handler.ts35
-rw-r--r--src/app/guards/auth.guard.ts39
-rw-r--r--src/app/guards/edit-user.can-activate.guard.ts51
-rw-r--r--src/app/guards/has-permissions.guard.ts72
-rw-r--r--src/app/guards/pending-changes.guard.ts42
-rw-r--r--src/app/helpers/filter-helpers.ts256
-rw-r--r--src/app/helpers/form-validators.ts32
-rw-r--r--src/app/helpers/helpers.ts76
-rw-r--r--src/app/helpers/listing.ts90
-rw-r--r--src/app/helpers/sorting-helpers.ts59
-rw-r--r--src/app/http-interceptors/auth.interceptor.ts49
-rw-r--r--src/app/http-interceptors/caching-interceptor.ts94
-rw-r--r--src/app/http-interceptors/http-error.interceptor.ts142
-rw-r--r--src/app/http-interceptors/interceptors.ts42
-rw-r--r--src/app/http-interceptors/loading-indicator.interceptor.ts41
-rw-r--r--src/app/http-interceptors/logging.interceptor.ts64
-rw-r--r--src/app/http-interceptors/mock.interceptor.ts50
-rw-r--r--src/app/http-interceptors/requestid.interceptor.ts37
-rw-r--r--src/app/http-interceptors/timeout.interceptor.ts51
-rw-r--r--src/app/model/dashboard.model.ts26
-rw-r--r--src/app/model/environment.model.ts45
-rw-r--r--src/app/model/tile.ts47
-rw-r--r--src/app/model/user-last-action.model.ts75
-rw-r--r--src/app/model/user-preferences.model.ts87
-rw-r--r--src/app/model/validation-pattern.model.ts26
-rw-r--r--src/app/modules/alerting/alert.component.css63
-rw-r--r--src/app/modules/alerting/alert.component.html106
-rw-r--r--src/app/modules/alerting/alert.component.spec.ts49
-rw-r--r--src/app/modules/alerting/alert.component.ts138
-rw-r--r--src/app/modules/alerting/alert.model.ts47
-rw-r--r--src/app/modules/alerting/alert.module.ts32
-rw-r--r--src/app/modules/alerting/alert.service.spec.ts93
-rw-r--r--src/app/modules/alerting/alert.service.ts66
-rw-r--r--src/app/modules/alerting/index.ts22
-rw-r--r--src/app/modules/app-starter/app-starter-routing.module.ts31
-rw-r--r--src/app/modules/app-starter/app-starter.component.css122
-rw-r--r--src/app/modules/app-starter/app-starter.component.html47
-rw-r--r--src/app/modules/app-starter/app-starter.component.ts45
-rw-r--r--src/app/modules/app-starter/app-starter.module.ts29
-rw-r--r--src/app/modules/auth/auth.config.module.ts46
-rw-r--r--src/app/modules/auth/auth.config.ts48
-rw-r--r--src/app/modules/auth/injection-tokens.ts26
-rw-r--r--src/app/modules/dashboard/apps/user-last-action-tile/action-button/action-button.component.css19
-rw-r--r--src/app/modules/dashboard/apps/user-last-action-tile/action-button/action-button.component.html63
-rw-r--r--src/app/modules/dashboard/apps/user-last-action-tile/action-button/action-button.component.ts40
-rw-r--r--src/app/modules/dashboard/apps/user-last-action-tile/action-row/action-row.component.css23
-rw-r--r--src/app/modules/dashboard/apps/user-last-action-tile/action-row/action-row.component.html105
-rw-r--r--src/app/modules/dashboard/apps/user-last-action-tile/action-row/action-row.component.ts35
-rw-r--r--src/app/modules/dashboard/apps/user-last-action-tile/entity-user-administration-row/entity-user-administration-row.component.css19
-rw-r--r--src/app/modules/dashboard/apps/user-last-action-tile/entity-user-administration-row/entity-user-administration-row.component.html33
-rw-r--r--src/app/modules/dashboard/apps/user-last-action-tile/entity-user-administration-row/entity-user-administration-row.component.ts39
-rw-r--r--src/app/modules/dashboard/apps/user-last-action-tile/user-last-action-tile.component.css27
-rw-r--r--src/app/modules/dashboard/apps/user-last-action-tile/user-last-action-tile.component.html85
-rw-r--r--src/app/modules/dashboard/apps/user-last-action-tile/user-last-action-tile.component.ts145
-rw-r--r--src/app/modules/dashboard/dashboard-routing.module.ts31
-rw-r--r--src/app/modules/dashboard/dashboard.component.css34
-rw-r--r--src/app/modules/dashboard/dashboard.component.html77
-rw-r--r--src/app/modules/dashboard/dashboard.component.ts88
-rw-r--r--src/app/modules/dashboard/dashboard.module.ts41
-rw-r--r--src/app/modules/i18n/i18n.module.ts51
-rw-r--r--src/app/modules/user-administration/user-administration-form/user-administration-form.component.css38
-rw-r--r--src/app/modules/user-administration/user-administration-form/user-administration-form.component.html205
-rw-r--r--src/app/modules/user-administration/user-administration-form/user-administration-form.component.spec.ts43
-rw-r--r--src/app/modules/user-administration/user-administration-form/user-administration-form.component.ts232
-rw-r--r--src/app/modules/user-administration/user-administration-list/user-administration-list.component.css28
-rw-r--r--src/app/modules/user-administration/user-administration-list/user-administration-list.component.html159
-rw-r--r--src/app/modules/user-administration/user-administration-list/user-administration-list.component.spec.ts43
-rw-r--r--src/app/modules/user-administration/user-administration-list/user-administration-list.component.ts118
-rw-r--r--src/app/modules/user-administration/user-administration-routing.module.ts54
-rw-r--r--src/app/modules/user-administration/user-administration.module.ts30
-rw-r--r--src/app/package.json6
-rw-r--r--src/app/pipes/colon.pipe.ts29
-rw-r--r--src/app/pipes/guard-type.pipe.ts32
-rw-r--r--src/app/pipes/has-permission.pipe.ts58
-rw-r--r--src/app/pipes/in.pipe.ts36
-rw-r--r--src/app/pipes/is-today.pipe.ts37
-rw-r--r--src/app/pipes/map.pipe.ts48
-rw-r--r--src/app/pipes/translate-mock.pipe.ts42
-rw-r--r--src/app/router.strategy.ts48
-rw-r--r--src/app/services/auth.service.ts94
-rw-r--r--src/app/services/authconfig.service.ts47
-rw-r--r--src/app/services/cacheservice/request-cache.service.spec.ts35
-rw-r--r--src/app/services/cacheservice/request-cache.service.ts89
-rw-r--r--src/app/services/fullscreen.service.ts58
-rw-r--r--src/app/services/history.service.ts67
-rw-r--r--src/app/services/loading-indicator.service.ts47
-rw-r--r--src/app/services/logging.service.ts34
-rw-r--r--src/app/services/shortcut.service.ts49
-rw-r--r--src/app/services/tileservice/tiles.service.spec.ts488
-rw-r--r--src/app/services/tileservice/tiles.service.ts88
-rw-r--r--src/app/services/unsubscribe/unsubscribe.service.ts32
-rw-r--r--src/app/services/user-settings.service.ts111
-rw-r--r--src/app/shared.module.ts78
-rw-r--r--src/app/tilesmock.ts41
-rw-r--r--src/assets/.gitkeep0
-rw-r--r--src/assets/acl.json44
-rw-r--r--src/assets/css/bootstrap-icons.css5501
-rw-r--r--src/assets/css/bootstrap.css11887
-rw-r--r--src/assets/css/onap-styles.css10755
-rw-r--r--src/assets/fonts/bootstrap-icons.woffbin0 -> 120468 bytes
-rw-r--r--src/assets/fonts/bootstrap-icons.woff2bin0 -> 90528 bytes
-rw-r--r--src/assets/i18n/de.json355
-rw-r--r--src/assets/i18n/en.json351
-rw-r--r--src/assets/images/icons/Asterisk.svg5
-rw-r--r--src/assets/images/icons/arrows-fullscreen-dark.svg3
-rw-r--r--src/assets/images/icons/brush_graphical.svg15
-rw-r--r--src/assets/images/icons/caret-down-fill-dark.svg3
-rw-r--r--src/assets/images/icons/crane_graphical.svg11
-rw-r--r--src/assets/images/icons/delete-icon.svg7
-rw-r--r--src/assets/images/icons/edit_graphical.svg14
-rw-r--r--src/assets/images/icons/eraser_graphical.svg7
-rw-r--r--src/assets/images/icons/install_graphical.svg15
-rw-r--r--src/assets/images/icons/list-dark.svg3
-rw-r--r--src/assets/images/icons/person-fill-dark.svg3
-rw-r--r--src/assets/images/icons/question-circle.svg4
-rw-r--r--src/assets/images/icons/search_graphical.svg16
-rw-r--r--src/assets/images/icons/standing-woman.svg1
-rw-r--r--src/assets/images/icons/thumbs-down_graphical.svg8
-rw-r--r--src/assets/images/icons/thumbs-up_graphical.svg7
-rw-r--r--src/assets/images/icons/triangular-warning-sign.svg18
-rw-r--r--src/assets/images/icons/visible_graphical.svg18
-rw-r--r--src/assets/images/onap-logo.pngbin0 -> 18232 bytes
-rw-r--r--src/assets/images/tiles/cds.svg220
-rw-r--r--src/assets/images/tiles/dcae-mod.svg279
-rw-r--r--src/assets/images/tiles/kibana.svg84
-rw-r--r--src/assets/images/tiles/onap.svg157
-rw-r--r--src/assets/images/tiles/sdc.svg96
-rw-r--r--src/assets/images/tiles/sdnc-dg.svg534
-rw-r--r--src/assets/images/tiles/sdnc-odl.svg534
-rw-r--r--src/assets/js/bootstrap.min.js1345
-rw-r--r--src/assets/js/jquery-2.1.3.min.js4462
-rw-r--r--src/assets/tiles/tiles.json160
-rw-r--r--src/assets/version.json3
-rw-r--r--src/environments/environment.prod.ts45
-rw-r--r--src/environments/environment.ts58
-rw-r--r--src/favicon.icobin0 -> 432254 bytes
-rw-r--r--src/index.html34
-rw-r--r--src/keycloak-error.html196
-rw-r--r--src/main.ts45
-rw-r--r--src/polyfills.ts78
-rw-r--r--src/styles.css630
-rw-r--r--src/test.ts42
-rw-r--r--staging.proxy.config.json.template31
-rwxr-xr-xstaging.sh24
-rw-r--r--tsconfig.app.json9
-rw-r--r--tsconfig.json28
-rw-r--r--tsconfig.spec.json9
-rw-r--r--version1
326 files changed, 69223 insertions, 0 deletions
diff --git a/.browserslistrc b/.browserslistrc
new file mode 100644
index 0000000..16e3010
--- /dev/null
+++ b/.browserslistrc
@@ -0,0 +1,7 @@
+last 1 Chrome version
+last 1 Firefox version
+last 2 Edge major versions
+last 2 Safari major versions
+last 2 iOS major versions
+Firefox ESR
+not IE 11 # Angular supports IE 11 only as an opt-in. To opt-in, remove the 'not' prefix on this line. \ No newline at end of file
diff --git a/.dockerignore b/.dockerignore
new file mode 100755
index 0000000..d8e2f9d
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,2 @@
+.gitignore
+node_modules \ No newline at end of file
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..59d9a3a
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,16 @@
+# Editor configuration, see https://editorconfig.org
+root = true
+
+[*]
+charset = utf-8
+indent_style = space
+indent_size = 2
+insert_final_newline = true
+trim_trailing_whitespace = true
+
+[*.ts]
+quote_type = single
+
+[*.md]
+max_line_length = off
+trim_trailing_whitespace = false
diff --git a/.eslintrc.json b/.eslintrc.json
new file mode 100644
index 0000000..a4c8c1c
--- /dev/null
+++ b/.eslintrc.json
@@ -0,0 +1,39 @@
+{
+ "root": true,
+ "ignorePatterns": ["projects/**/*"],
+ "overrides": [
+ {
+ "files": ["*.ts"],
+ "parserOptions": {
+ "project": ["tsconfig.json", "e2e/tsconfig.json"],
+ "createDefaultProgram": true
+ },
+ "extends": ["plugin:@angular-eslint/recommended", "plugin:@angular-eslint/template/process-inline-templates"],
+ "rules": {
+ "no-unused-vars": "off",
+ "@typescript-eslint/no-unused-vars": ["error"],
+ "@angular-eslint/component-selector": [
+ "error",
+ {
+ "prefix": "app",
+ "style": "kebab-case",
+ "type": "element"
+ }
+ ],
+ "@angular-eslint/directive-selector": [
+ "error",
+ {
+ "prefix": "app",
+ "style": "camelCase",
+ "type": "attribute"
+ }
+ ]
+ }
+ },
+ {
+ "files": ["*.html"],
+ "extends": ["plugin:@angular-eslint/template/recommended"],
+ "rules": {}
+ }
+ ]
+}
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..200a708
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,59 @@
+# See http://help.github.com/ignore-files/ for more about ignoring files.
+
+# compiled output
+/dist
+/tmp
+/out-tsc
+# Only exists if Bazel was run
+/bazel-out
+# Exists if npm audit was run
+/target
+
+# dependencies
+/node_modules
+package-lock.json
+
+# profiling files
+chrome-profiler-events*.json
+speed-measure-plugin*.json
+
+# IDEs and editors
+.c9/
+.classpath
+/.idea
+*.launch
+.project
+.settings/
+*.sublime-workspace
+.vscode
+
+# IDE - VSCode
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
+.history/*
+
+# misc
+/.sass-cache
+/connect.lock
+/coverage
+/libpeerconnection.log
+npm-debug.log
+yarn-error.log
+testem.log
+/typings
+/.cache
+
+# System Files
+.DS_Store
+Thumbs.db
+
+# Open API
+/openapi/output
+/openapi/openapi-generator-cli-*.jar
+/.angular
+.cache
+
+staging.proxy.config.json \ No newline at end of file
diff --git a/.prettierignore b/.prettierignore
new file mode 100644
index 0000000..cac7a41
--- /dev/null
+++ b/.prettierignore
@@ -0,0 +1,6 @@
+.idea/
+dist/
+node_modules/
+package.json
+package-lock.json
+openapi/
diff --git a/.prettierrc.json b/.prettierrc.json
new file mode 100644
index 0000000..9672924
--- /dev/null
+++ b/.prettierrc.json
@@ -0,0 +1,6 @@
+{
+ "trailingComma": "all",
+ "arrowParens": "avoid",
+ "singleQuote": true,
+ "printWidth": 120
+}
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..4b0f004
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,9 @@
+FROM openresty/openresty:1.21.4.1-4-alpine
+RUN apk add gettext
+COPY server/resty /usr/local/openresty/lualib/resty/
+COPY server/nginx* ./
+COPY docker_entrypoint.sh .
+COPY dist/frontend /usr/share/nginx/html
+ENTRYPOINT ["/docker_entrypoint.sh"]
+EXPOSE ${NGINX_PORT}
+CMD ["nginx", "-g", "daemon off;"]
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..abe3069
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright 2021 TNAP / development / system-team
+
+ 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
+
+ http://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.
diff --git a/LICENSE_HEADER b/LICENSE_HEADER
new file mode 100644
index 0000000..c97f698
--- /dev/null
+++ b/LICENSE_HEADER
@@ -0,0 +1,15 @@
+Copyright (c) ##[0-9]{4}##. Deutsche Telekom AG
+
+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
+
+http://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.
+
+SPDX-License-Identifier: Apache-2.0
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..99ea425
--- /dev/null
+++ b/README.md
@@ -0,0 +1,81 @@
+# portal-ng
+
+## Getting started
+The portal-ng ui can either be developed against a remote cluster or it's dependencies can be run locally on your machine.
+
+### Developing against a remote cluster
+To develop against a remote cluster, the webpack proxy needs to be configured to forward requests to the remote cluster urls.
+For that we are providing a `staging.proxy.config.json.template` file that needs to be adjusted with your cluster hostnames and then saved as `staging.proxy.config.json`.
+After that, you can either use `npm start` or the `staging.sh` to launch the portal-ng in development mode:
+```sh
+./staging.sh
+```
+```sh
+npm start
+```
+
+### Developing against local containers
+We are providing a docker-compose file that can be used to spin up the portal-ng and it's dependencies (like Keycloak or the portal-bff) on your machine.
+
+To do that, execute the `run.sh` in the development folder:
+```shell
+development/run.sh
+```
+
+To stop the portal-ng, portal backend services, Keycloak and the databases run:
+
+```shell
+development/stop.sh
+```
+
+### Access the ui
+Example requests against the portal backend service can be run in your preferred IDE with the `request.http` file from the development folder.
+
+You can access the portal-ng UI via browser with different default user accounts. Note that these accounts have different roles and differ accordingly
+in what they are allowed to see in the portal.
+
+URL: http://localhost
+```
+username: onap-admin
+password: password
+
+username: onap-designer
+password: password
+
+username: onap-operator
+password: password
+```
+You can access the Keycloak UI via browser.
+
+URL: http://localhost:8080
+```
+username: admin
+password: password
+```
+
+## Docker
+### Build the docker image
+
+Run `npm run build -- --prod --base-href=/portal-ui/` to get a production build of the project, this will be used in the `docker build`.
+
+In the configuration of nginx (the `nginx.template`) we have a few environment variables that need to be set.
+
+```bash
+export NGINX_PORT=80
+export BFF_URL=http://portal-bff:9080/
+export WIREMOCK_URL=http://wiremock:8080/
+```
+
+Finally, build the image with
+
+```bash
+docker build -t portal-ng .
+```
+
+### Run the docker image
+
+```bash
+docker run -e "NGINX_PORT=80" -e "BFF_URL=http:portal-bff:9080/" -e "WIREMOCK_URL=http://wiremock:8080/" -p 8080:80 portal-ng
+```
+
+Note that this will not work on its own, because the referenced containers (`BFF` and `WIREMOCK`) are most likely not available in your local environment. You would have to run them as well, or pass in other urls (like `example.com`) to get the container running locally. Obviously this does not get you very far though.
diff --git a/angular.json b/angular.json
new file mode 100644
index 0000000..f749b51
--- /dev/null
+++ b/angular.json
@@ -0,0 +1,135 @@
+{
+ "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
+ "version": 1,
+ "newProjectRoot": "projects",
+ "projects": {
+ "frontend": {
+ "projectType": "application",
+ "schematics": {},
+ "root": "",
+ "sourceRoot": "src",
+ "prefix": "app",
+ "architect": {
+ "build": {
+ "builder": "@angular-devkit/build-angular:browser",
+ "options": {
+ "allowedCommonJsDependencies": [ "clone-deep"],
+ "aot": true,
+ "outputPath": "dist/frontend",
+ "index": "src/index.html",
+ "main": "src/main.ts",
+ "polyfills": "src/polyfills.ts",
+ "tsConfig": "tsconfig.app.json",
+ "assets": ["src/favicon.ico", "src/assets", "src/keycloak-error.html"],
+ "styles": [
+ "node_modules/bootstrap/dist/css/bootstrap.css",
+ "src/assets/css/bootstrap-icons.css",
+ "src/styles.css",
+ "./node_modules/@angular/cdk/overlay-prebuilt.css",
+ {
+ "input": "src/assets/css/onap-styles.css",
+ "bundleName": "onap-styles",
+ "inject": true
+ }
+ ],
+ "scripts": [],
+ "vendorChunk": true,
+ "extractLicenses": false,
+ "buildOptimizer": false,
+ "sourceMap": true,
+ "optimization": false,
+ "namedChunks": true
+ },
+ "configurations": {
+ "production": {
+ "fileReplacements": [
+ {
+ "replace": "src/environments/environment.ts",
+ "with": "src/environments/environment.prod.ts"
+ }
+ ],
+ "optimization": true,
+ "outputHashing": "all",
+ "sourceMap": false,
+ "namedChunks": false,
+ "extractLicenses": true,
+ "vendorChunk": false,
+ "buildOptimizer": true,
+ "budgets": [
+ {
+ "type": "initial",
+ "maximumWarning": "2mb",
+ "maximumError": "4mb"
+ },
+ {
+ "type": "anyComponentStyle",
+ "maximumWarning": "2kb",
+ "maximumError": "4kb"
+ }
+ ]
+ }
+ },
+ "defaultConfiguration": ""
+ },
+ "serve": {
+ "builder": "@angular-devkit/build-angular:dev-server",
+ "options": {
+ "browserTarget": "frontend:build"
+ },
+ "configurations": {
+ "production": {
+ "browserTarget": "frontend:build:production"
+ }
+ }
+ },
+ "extract-i18n": {
+ "builder": "@angular-devkit/build-angular:extract-i18n",
+ "options": {
+ "browserTarget": "frontend:build"
+ }
+ },
+ "test": {
+ "builder": "@angular-devkit/build-angular:karma",
+ "options": {
+ "codeCoverage": true,
+ "main": "src/test.ts",
+ "polyfills": "src/polyfills.ts",
+ "tsConfig": "tsconfig.spec.json",
+ "karmaConfig": "karma.conf.js",
+ "assets": ["src/favicon.ico", "src/assets", "src/keycloak-error.html"],
+ "styles": ["src/styles.css"],
+ "scripts": []
+ }
+ },
+ "lint": {
+ "builder": "@angular-eslint/builder:lint",
+ "options": {
+ "lintFilePatterns": ["src/**/*.ts", "src/**/*.html"]
+ }
+ },
+ "e2e": {
+ "builder": "@angular-devkit/build-angular:protractor",
+ "options": {
+ "protractorConfig": "e2e/protractor.conf.js",
+ "devServerTarget": "frontend:serve"
+ },
+ "configurations": {
+ "production": {
+ "devServerTarget": "frontend:serve:production"
+ }
+ }
+ }
+ }
+ }
+ },
+ "defaultProject": "frontend",
+ "cli": {
+ "cache": {
+ "enabled": true,
+ "path": ".cache",
+ "environment": "all"
+ },
+ "analytics": "8f37c777-0688-43ed-a5a5-b315e820eff9",
+ "defaultCollection": "@angular-eslint/schematics"
+ }
+}
diff --git a/development/.env b/development/.env
new file mode 100644
index 0000000..e3b9b54
--- /dev/null
+++ b/development/.env
@@ -0,0 +1,52 @@
+# General image repository
+IMAGE_REPOSITORY=nexus.onap.org/content/repositories/snapshots/org/onap/
+
+# Keycloak
+KEYCLOAK_IMAGE=quay.io/keycloak/keycloak
+KEYCLOAK_VERSION=18.0.2-legacy
+KEYCLOAK_USER=admin
+KEYCLOAK_PASSWORD=password
+KEYCLOAK_DB=keycloak
+KEYCLOAK_DB_USER=keycloak
+KEYCLOAK_DB_PASSWORD=password
+KEYCLOAK_URL:http://keycloak-bff:8080
+KEYCLOAK_REALM:ONAP
+
+# Postgres database for keycloak
+POSTGRES_IMAGE=postgres
+POSTGRES_VERSION=15rc1
+
+# Mongo database for portal-prefs and portal-history
+MONGO_IMAGE=mongo
+MONGO_VERSION=latest
+
+# portal-service
+PORTAL_SERVICE_URL=http://portal-service:9000
+PORTAL_SERVICE_IMAGE_NAME=portal-service
+PORTAL_SERVICE_IMAGE_TAG=0.1.0-d486ddb4
+
+# portal-prefs
+PORTAL_PREFS_URL=http://portal-prefs:9001
+PORTAL_PREFS_IMAGE_NAME=portal-prefs
+PORTAL_PREFS_IMAGE_TAG=0.1.0-master-faef0c0e
+PORTALPREFS_USERNAME=root
+PORTALPREFS_PASSWORD=password
+PORTALPREFS_DATABASE=Portalprefs
+PORTALPREFS_HOST=mongo-prefs
+PORTALPREFS_PORT=27017
+
+# portal-history
+PORTAL_HISTORY_URL=http://portal-history:9002
+PORTAL_HISTORY_IMAGE_NAME=portal-history
+PORTAL_HISTORY_IMAGE_TAG=0.1.1-de369ace
+PORTALHISTORY_USERNAME=root
+PORTALHISTORY_PASSWORD=password
+PORTALHISTORY_DATABASE=Portalhist
+PORTALHISTORY_HOST=mongo-history
+PORTALHISTORY_PORT=27017
+
+# portal-bff
+PORTAL_BFF_IMAGE_NAME=portal-bff
+PORTAL_BFF_IMAGE_TAG=0.1.9-389a09e1
+KEYCLOAK_CLIENT_ID=portal-bff
+KEYCLOAK_CLIENT_SECRET=pKOuVH1bwRZoNzp5P5t4GV8CqcCJYVtr
diff --git a/development/config/onap-realm.json b/development/config/onap-realm.json
new file mode 100644
index 0000000..e8000ea
--- /dev/null
+++ b/development/config/onap-realm.json
@@ -0,0 +1,228 @@
+{
+ "id": "ONAP",
+ "realm": "ONAP",
+ "enabled": true,
+ "clients": [
+ {
+ "clientId": "portal-app",
+ "surrogateAuthRequired": false,
+ "enabled": true,
+ "alwaysDisplayInConsole": false,
+ "clientAuthenticatorType": "client-secret",
+ "redirectUris": [
+ "http://localhost/*"
+ ],
+ "webOrigins": [
+ "*"
+ ],
+ "notBefore": 0,
+ "bearerOnly": false,
+ "consentRequired": false,
+ "standardFlowEnabled": true,
+ "implicitFlowEnabled": false,
+ "directAccessGrantsEnabled": true,
+ "serviceAccountsEnabled": false,
+ "publicClient": true,
+ "frontchannelLogout": false,
+ "protocol": "openid-connect",
+ "attributes": {
+ "backchannel.logout.session.required": "true",
+ "backchannel.logout.revoke.offline.tokens": "false"
+ },
+ "authenticationFlowBindingOverrides": {},
+ "fullScopeAllowed": true,
+ "nodeReRegistrationTimeout": -1,
+ "protocolMappers": [
+ {
+ "name": "User-Roles",
+ "protocol": "openid-connect",
+ "protocolMapper": "oidc-usermodel-realm-role-mapper",
+ "consentRequired": false,
+ "config": {
+ "id.token.claim": "true",
+ "access.token.claim": "true",
+ "claim.name": "roles",
+ "multivalued": "true",
+ "userinfo.token.claim": "true"
+ }
+ },
+ {
+ "name": "SDC-User",
+ "protocol": "openid-connect",
+ "protocolMapper": "oidc-usermodel-attribute-mapper",
+ "consentRequired": false,
+ "config": {
+ "userinfo.token.claim": "true",
+ "user.attribute": "sdc_user",
+ "id.token.claim": "true",
+ "access.token.claim": "true",
+ "claim.name": "sdc_user",
+ "jsonType.label": "String"
+ }
+ }
+ ],
+ "defaultClientScopes": [
+ "web-origins",
+ "acr",
+ "profile",
+ "roles",
+ "email"
+ ],
+ "optionalClientScopes": [
+ "address",
+ "phone",
+ "offline_access",
+ "microprofile-jwt"
+ ]
+ }, {
+ "clientId" : "portal-bff",
+ "surrogateAuthRequired" : false,
+ "enabled" : true,
+ "alwaysDisplayInConsole" : false,
+ "clientAuthenticatorType" : "client-secret",
+ "secret" : "pKOuVH1bwRZoNzp5P5t4GV8CqcCJYVtr",
+ "redirectUris" : [ ],
+ "webOrigins" : [ ],
+ "notBefore" : 0,
+ "bearerOnly" : false,
+ "consentRequired" : false,
+ "standardFlowEnabled" : false,
+ "implicitFlowEnabled" : false,
+ "directAccessGrantsEnabled" : false,
+ "serviceAccountsEnabled" : true,
+ "publicClient" : false,
+ "frontchannelLogout" : false,
+ "protocol" : "openid-connect",
+ "attributes" : {
+ "saml.force.post.binding" : "false",
+ "saml.multivalued.roles" : "false",
+ "frontchannel.logout.session.required" : "false",
+ "oauth2.device.authorization.grant.enabled" : "false",
+ "backchannel.logout.revoke.offline.tokens" : "false",
+ "saml.server.signature.keyinfo.ext" : "false",
+ "use.refresh.tokens" : "true",
+ "oidc.ciba.grant.enabled" : "false",
+ "backchannel.logout.session.required" : "true",
+ "client_credentials.use_refresh_token" : "false",
+ "require.pushed.authorization.requests" : "false",
+ "saml.client.signature" : "false",
+ "saml.allow.ecp.flow" : "false",
+ "id.token.as.detached.signature" : "false",
+ "saml.assertion.signature" : "false",
+ "client.secret.creation.time" : "1665048112",
+ "saml.encrypt" : "false",
+ "saml.server.signature" : "false",
+ "exclude.session.state.from.auth.response" : "false",
+ "saml.artifact.binding" : "false",
+ "saml_force_name_id_format" : "false",
+ "acr.loa.map" : "{}",
+ "tls.client.certificate.bound.access.tokens" : "false",
+ "saml.authnstatement" : "false",
+ "display.on.consent.screen" : "false",
+ "token.response.type.bearer.lower-case" : "false",
+ "saml.onetimeuse.condition" : "false"
+ },
+ "authenticationFlowBindingOverrides" : { },
+ "fullScopeAllowed" : true,
+ "nodeReRegistrationTimeout" : -1,
+ "protocolMappers" : [ {
+ "name" : "Client Host",
+ "protocol" : "openid-connect",
+ "protocolMapper" : "oidc-usersessionmodel-note-mapper",
+ "consentRequired" : false,
+ "config" : {
+ "user.session.note" : "clientHost",
+ "id.token.claim" : "true",
+ "access.token.claim" : "true",
+ "claim.name" : "clientHost",
+ "jsonType.label" : "String"
+ }
+ }, {
+ "name" : "Client IP Address",
+ "protocol" : "openid-connect",
+ "protocolMapper" : "oidc-usersessionmodel-note-mapper",
+ "consentRequired" : false,
+ "config" : {
+ "user.session.note" : "clientAddress",
+ "id.token.claim" : "true",
+ "access.token.claim" : "true",
+ "claim.name" : "clientAddress",
+ "jsonType.label" : "String"
+ }
+ } ],
+ "defaultClientScopes" : [ "web-origins", "acr", "profile", "roles", "email" ],
+ "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ]
+ }],
+ "users": [
+ {
+ "createdTimestamp" : 1664965113698,
+ "username" : "onap-admin",
+ "enabled" : true,
+ "totp" : false,
+ "emailVerified" : false,
+ "attributes" : {
+ "sdc_user" : [ "cs0008" ]
+ },
+ "credentials" : [ {
+ "type" : "password",
+ "createdDate" : 1664965134586,
+ "secretData" : "{\"value\":\"nD4K4x8HEgk6xlWIAgzZOE+EOjdbovJfEa7N3WXwIMCWCfdXpn7Riys7hZhI1NbKcc9QPI9j8LQB/JSuZVcXKA==\",\"salt\":\"T8X9A9tT2cyLvEjHFo+zuQ==\",\"additionalParameters\":{}}",
+ "credentialData" : "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}"
+ } ],
+ "disableableCredentialTypes" : [ ],
+ "requiredActions" : [ ],
+ "realmRoles" : [ "default-roles-onap", "onap_admin" ],
+ "notBefore" : 0,
+ "groups" : [ ]
+ }, {
+ "createdTimestamp" : 1665048354760,
+ "username" : "onap-designer",
+ "enabled" : true,
+ "totp" : false,
+ "emailVerified" : false,
+ "attributes" : {
+ "sec_user" : [ "cs0008" ]
+ },
+ "credentials" : [ ],
+ "disableableCredentialTypes" : [ ],
+ "requiredActions" : [ ],
+ "realmRoles" : [ "default-roles-onap", "onap_designer" ],
+ "notBefore" : 0,
+ "groups" : [ ]
+ }, {
+ "createdTimestamp" : 1665048547054,
+ "username" : "onap-operator",
+ "enabled" : true,
+ "totp" : false,
+ "emailVerified" : false,
+ "attributes" : {
+ "sdc_user" : [ "cs0008" ]
+ },
+ "credentials" : [ ],
+ "disableableCredentialTypes" : [ ],
+ "requiredActions" : [ ],
+ "realmRoles" : [ "default-roles-onap", "onap_operator" ],
+ "notBefore" : 0,
+ "groups" : [ ]
+ }, {
+ "createdTimestamp" : 1665048112458,
+ "username" : "service-account-portal-bff",
+ "enabled" : true,
+ "totp" : false,
+ "emailVerified" : false,
+ "serviceAccountClientId" : "portal-bff",
+ "credentials" : [ ],
+ "disableableCredentialTypes" : [ ],
+ "requiredActions" : [ ],
+ "realmRoles" : [ "default-roles-onap" ],
+ "clientRoles" : {
+ "realm-management" : [ "manage-realm", "manage-users" ]
+ },
+ "notBefore" : 0,
+ "groups" : [ ]
+ }
+ ],
+ "attributes": {
+ "frontendUrl": "http://localhost:8080/auth/"
+ }
+}
diff --git a/development/docker-compose.yml b/development/docker-compose.yml
new file mode 100644
index 0000000..6c6a0ad
--- /dev/null
+++ b/development/docker-compose.yml
@@ -0,0 +1,90 @@
+version: '3'
+
+volumes:
+ postgres_data:
+ driver: local
+
+services:
+ postgres:
+ container_name: postgres-keycloak
+ image: "${POSTGRES_IMAGE}:${POSTGRES_VERSION}"
+ volumes:
+ - postgres_data:/var/lib/postgresql/data
+ environment:
+ POSTGRES_DB: ${KEYCLOAK_DB}
+ POSTGRES_USER: ${KEYCLOAK_DB_USER}
+ POSTGRES_PASSWORD: ${KEYCLOAK_DB_PASSWORD}
+ keycloak:
+ container_name: keycloak-bff
+ image: "${KEYCLOAK_IMAGE}:${KEYCLOAK_VERSION}"
+ environment:
+ DB_VENDOR: POSTGRES
+ DB_ADDR: postgres-keycloak
+ DB_DATABASE: ${KEYCLOAK_DB}
+ DB_USER: ${KEYCLOAK_DB_USER}
+ DB_SCHEMA: public
+ DB_PASSWORD: ${KEYCLOAK_DB_PASSWORD}
+ KEYCLOAK_USER: ${KEYCLOAK_USER}
+ KEYCLOAK_PASSWORD: ${KEYCLOAK_PASSWORD}
+ KEYCLOAK_IMPORT: /config/onap-realm.json
+ ports:
+ - 8080:8080
+ volumes:
+ - ./config:/config
+ depends_on:
+ - postgres
+ mongo-history:
+ container_name: mongo-history
+ image: "${MONGO_IMAGE}:${MONGO_VERSION}"
+ environment:
+ MONGO_INITDB_ROOT_USERNAME: ${PORTALHISTORY_USERNAME}
+ MONGO_INITDB_ROOT_PASSWORD: ${PORTALHISTORY_PASSWORD}
+ portal-history:
+ container_name: portal-history
+ image: "${IMAGE_REPOSITORY}/${PORTAL_HISTORY_IMAGE_NAME}:${PORTAL_HISTORY_IMAGE_TAG}"
+ ports:
+ - 9002:9002
+ environment:
+ PORTALHISTORY_USERNAME: ${PORTALHISTORY_USERNAME}
+ PORTALHISTORY_PASSWORD: ${PORTALHISTORY_PASSWORD}
+ PORTALHISTORY_DATABASE: ${PORTALHISTORY_DATABASE}
+ KEYCLOAK_URL: ${KEYCLOAK_URL}
+ KEYCLOAK_REALM: ${KEYCLOAK_REALM}
+ PORTALHISTORY_HOST: ${PORTALHISTORY_HOST}
+ PORTALHISTORY_PORT: ${PORTALHISTORY_PORT}
+ depends_on:
+ - mongo-history
+ mongo-prefs:
+ container_name: mongo-prefs
+ image: "${MONGO_IMAGE}:${MONGO_VERSION}"
+ environment:
+ MONGO_INITDB_ROOT_USERNAME: ${PORTALPREFS_USERNAME}
+ MONGO_INITDB_ROOT_PASSWORD: ${PORTALPREFS_PASSWORD}
+ portal-prefs:
+ container_name: portal-prefs
+ image: "${IMAGE_REPOSITORY}/${PORTAL_PREFS_IMAGE_NAME}:${PORTAL_PREFS_IMAGE_TAG}"
+ ports:
+ - 9001:9001
+ environment:
+ PORTALPREFS_USERNAME: ${PORTALPREFS_USERNAME}
+ PORTALPREFS_PASSWORD: ${PORTALPREFS_PASSWORD}
+ PORTALPREFS_DATABASE: ${PORTALPREFS_DATABASE}
+ KEYCLOAK_URL: ${KEYCLOAK_URL}
+ KEYCLOAK_REALM: ${KEYCLOAK_REALM}
+ PORTALPREFS_HOST: ${PORTALPREFS_HOST}
+ PORTALPREFS_PORT: ${PORTALPREFS_PORT}
+ depends_on:
+ - mongo-prefs
+ portal-bff:
+ container_name: portal-bff
+ image: "${IMAGE_REPOSITORY}/${PORTAL_BFF_IMAGE_NAME}:${PORTAL_BFF_IMAGE_TAG}"
+ ports:
+ - 9080:9080
+ environment:
+ KEYCLOAK_URL: ${KEYCLOAK_URL}
+ KEYCLOAK_REALM: ${KEYCLOAK_REALM}
+ KEYCLOAK_CLIENT_ID: ${KEYCLOAK_CLIENT_ID}
+ KEYCLOAK_CLIENT_SECRET: ${KEYCLOAK_CLIENT_SECRET}
+ PORTAL_SERVICE_URL: ${PORTAL_SERVICE_URL}
+ PORTAL_PREFS_URL: ${PORTAL_PREFS_URL}
+ PORTAL_HISTORY_URL: ${PORTAL_HISTORY_URL}
diff --git a/development/request.http b/development/request.http
new file mode 100644
index 0000000..8c2a695
--- /dev/null
+++ b/development/request.http
@@ -0,0 +1,159 @@
+
+POST http://localhost:8080/auth/realms/ONAP/protocol/openid-connect/token
+Content-Type: application/x-www-form-urlencoded
+
+client_id=portal-app&client_secret=&scope=openid&grant_type=password&username=onap-admin&password=password
+> {%
+ client.global.set("access_token", response.body.access_token);
+ client.global.set("id_token", response.body.id_token);
+ %}
+
+###
+
+GET http://localhost:8080/auth/realms/ONAP/protocol/openid-connect/userinfo
+Authorization: Bearer {{access_token}}
+X-Auth-Identity: Bearer {{id_token}}
+
+> {%
+ client.global.set("user_id", response.body.sub);
+ client.global.set("user_name", response.body.preferred_username);
+ %}
+
+###
+
+POST http://localhost:9080/preferences
+X-Request-Id: {{$uuid}}
+Accept: application/json
+Content-Type: application/json
+Authorization: Bearer {{access_token}}
+X-Auth-Identity: Bearer {{id_token}}
+
+
+{
+ "properties": {
+ "dashboard": {
+ "apps": {
+ "availableTiles": [
+ {
+ "type": "USER_LAST_ACTION_TILE",
+ "displayed": false
+ }
+ ],
+ "lastUserAction": {
+ "interval": "1H",
+ "filterType": "ALL"
+ }
+ }
+ }
+ }
+}
+
+###
+
+GET http://localhost:9080/preferences
+Accept: application/json
+Authorization: Bearer {{access_token}}
+X-Auth-Identity: Bearer {{id_token}}
+X-Request-Id: {{$uuid}}
+
+###
+
+POST http://localhost:9080/actions/{{user_id}}
+X-Request-Id: {{$uuid}}
+Accept: application/json
+Authorization: Bearer {{access_token}}
+X-Auth-Identity: Bearer {{id_token}}
+Content-Type: application/json
+
+{
+ "userId": "{{user_id}}",
+ "actionCreatedAt": "{{$timestamp}}",
+ "action": {
+ "type": "DELETE",
+ "entity": "USERADMINISTRATION",
+ "entityParams": {
+ "userName": "uli",
+ "userId": "{{$randomInt}}"
+ }
+ }
+}
+
+###
+
+GET http://localhost:9080/actions/{{user_id}}?page=1&pageSize=10&showLastHours=1
+X-Request-Id: {{$uuid}}
+Accept: application/json
+Authorization: Bearer {{access_token}}
+X-Auth-Identity: Bearer {{id_token}}
+
+###
+
+GET http://localhost:9080/actions?page=1&pageSize=10&showLastHours=1
+X-Request-Id: {{$uuid}}
+Accept: application/json
+Authorization: Bearer {{access_token}}
+X-Auth-Identity: Bearer {{id_token}}
+
+### request agains portal-service
+
+GET http://localhost:9080/key
+X-Request-Id: {{$uuid}}
+Accept: text/plain
+Authorization: Bearer {{access_token}}
+X-Auth-Identity: Bearer {{id_token}}
+
+###
+
+GET http://localhost:9080/key/{{user_name}}
+X-Request-Id: {{$uuid}}
+Accept: text/plain
+Authorization: Bearer {{access_token}}
+X-Auth-Identity: Bearer {{id_token}}
+
+###
+
+GET http://localhost:9080/tiles
+X-Request-Id: {{$uuid}}
+Accept: application/json
+Authorization: Bearer {{access_token}}
+X-Auth-Identity: Bearer {{id_token}}
+
+###
+
+GET http://localhost:9080/tiles/1
+X-Request-Id: {{$uuid}}
+Accept: application/json
+Authorization: Bearer {{access_token}}
+X-Auth-Identity: Bearer {{id_token}}
+
+### request against keycloak
+
+GET http://localhost:9080/users
+X-Request-Id: {{$uuid}}
+Accept: application/json
+Authorization: Bearer {{access_token}}
+X-Auth-Identity: Bearer {{id_token}}
+
+###
+
+GET http://localhost:9080/users/{{user_id}}
+X-Request-Id: {{$uuid}}
+Accept: application/json
+Authorization: Bearer {{access_token}}
+X-Auth-Identity: Bearer {{id_token}}
+
+###
+
+GET http://localhost:9080/users/{{user_id}}/roles
+X-Request-Id: {{$uuid}}
+Accept: application/json
+Authorization: Bearer {{access_token}}
+X-Auth-Identity: Bearer {{id_token}}
+
+###
+
+
+
+
+
+
diff --git a/development/run.sh b/development/run.sh
new file mode 100755
index 0000000..72d8acf
--- /dev/null
+++ b/development/run.sh
@@ -0,0 +1,7 @@
+#!/usr/bin/env bash
+SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
+
+docker compose -f "$SCRIPT_DIR/docker-compose.yml" up -d
+
+cd $SCRIPT_DIR/..
+./local.sh
diff --git a/development/stop.sh b/development/stop.sh
new file mode 100755
index 0000000..4ef4493
--- /dev/null
+++ b/development/stop.sh
@@ -0,0 +1,8 @@
+#!/usr/bin/env bash
+SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
+
+# shutdown all docker container
+docker compose -f "$SCRIPT_DIR/docker-compose.yml" down -v
+
+# kill the npm process which server on port 80
+kill `lsof -t -i:80`
diff --git a/docker_entrypoint.sh b/docker_entrypoint.sh
new file mode 100755
index 0000000..823086f
--- /dev/null
+++ b/docker_entrypoint.sh
@@ -0,0 +1,27 @@
+#!/usr/bin/env sh
+#
+#
+# Copyright (c) 2022. Deutsche Telekom AG
+#
+# 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
+#
+# http://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.
+#
+# SPDX-License-Identifier: Apache-2.0
+#
+#
+#
+
+set -eu
+
+envsubst '${BFF_URL} ${NGINX_PORT} ${KEYCLOAK_URL} ${KEYCLOAK_REALM} ${KEYCLOAK_INTERNAL_URL} ${CLUSTER_NAMESERVER_IP}' < ./nginx.template > /etc/nginx/conf.d/default.conf
+
+exec "$@"
diff --git a/e2e/protractor.conf.js b/e2e/protractor.conf.js
new file mode 100644
index 0000000..b18b7b6
--- /dev/null
+++ b/e2e/protractor.conf.js
@@ -0,0 +1,38 @@
+
+
+// @ts-check
+// Protractor configuration file, see link for more information
+// https://github.com/angular/protractor/blob/master/lib/config.ts
+
+const { SpecReporter, StacktraceOption } = require('jasmine-spec-reporter');
+
+/**
+ * @type { import("protractor").Config }
+ */
+exports.config = {
+ allScriptsTimeout: 11000,
+ specs: ['./src/**/*.e2e-spec.ts'],
+ capabilities: {
+ browserName: 'chrome',
+ },
+ directConnect: true,
+ baseUrl: 'http://localhost:4200/',
+ framework: 'jasmine',
+ jasmineNodeOpts: {
+ showColors: true,
+ defaultTimeoutInterval: 30000,
+ print: function () {},
+ },
+ onPrepare() {
+ require('ts-node').register({
+ project: require('path').join(__dirname, './tsconfig.json'),
+ });
+ jasmine.getEnv().addReporter(
+ new SpecReporter({
+ spec: {
+ displayStacktrace: StacktraceOption.PRETTY,
+ },
+ }),
+ );
+ },
+};
diff --git a/e2e/src/app.e2e-spec.ts b/e2e/src/app.e2e-spec.ts
new file mode 100644
index 0000000..143c2ba
--- /dev/null
+++ b/e2e/src/app.e2e-spec.ts
@@ -0,0 +1,44 @@
+/*
+ * Copyright (c) 2022. Deutsche Telekom AG
+ *
+ * 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
+ *
+ * http://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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+
+import { AppPage } from './app.po';
+import { browser, logging } from 'protractor';
+
+describe('workspace-project App', () => {
+ let page: AppPage;
+
+ beforeEach(() => {
+ page = new AppPage();
+ });
+
+ it('should display welcome message', () => {
+ page.navigateTo();
+ expect(page.getTitleText()).toEqual('frontend app is running!');
+ });
+
+ afterEach(async () => {
+ // Assert that there are no errors emitted from the browser
+ const logs = await browser.manage().logs().get(logging.Type.BROWSER);
+ expect(logs).not.toContain(
+ jasmine.objectContaining({
+ level: logging.Level.SEVERE,
+ } as logging.Entry),
+ );
+ });
+});
diff --git a/e2e/src/app.po.ts b/e2e/src/app.po.ts
new file mode 100644
index 0000000..f0d1d23
--- /dev/null
+++ b/e2e/src/app.po.ts
@@ -0,0 +1,30 @@
+/*
+ * Copyright (c) 2022. Deutsche Telekom AG
+ *
+ * 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
+ *
+ * http://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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+
+import { browser, by, element } from 'protractor';
+
+export class AppPage {
+ navigateTo(): Promise<unknown> {
+ return browser.get(browser.baseUrl) as Promise<unknown>;
+ }
+
+ getTitleText(): Promise<string> {
+ return element(by.css('app-root .content span')).getText() as Promise<string>;
+ }
+}
diff --git a/e2e/tsconfig.json b/e2e/tsconfig.json
new file mode 100644
index 0000000..dbf470a
--- /dev/null
+++ b/e2e/tsconfig.json
@@ -0,0 +1,9 @@
+{
+ "extends": "../tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../out-tsc/e2e",
+ "module": "commonjs",
+ "target": "es2018",
+ "types": ["jasmine", "jasminewd2", "node"]
+ }
+}
diff --git a/karma.conf.js b/karma.conf.js
new file mode 100644
index 0000000..403490a
--- /dev/null
+++ b/karma.conf.js
@@ -0,0 +1,45 @@
+
+
+// Karma configuration file, see link for more information
+// https://karma-runner.github.io/1.0/config/configuration-file.html
+
+module.exports = function (config) {
+ config.set({
+ basePath: '',
+ frameworks: ['jasmine', '@angular-devkit/build-angular'],
+ plugins: [
+ require('karma-jasmine'),
+ require('karma-jasmine-html-reporter'),
+ require('@angular-devkit/build-angular/plugins/karma'),
+ require('karma-spec-reporter'),
+ require('karma-firefox-launcher'),
+ require('karma-chrome-launcher'),
+ require('karma-sonarqube-unit-reporter'),
+ require('karma-coverage'),
+ ],
+ client: {
+ clearContext: true, // leave Jasmine Spec Runner output visible in browser
+ },
+ coverageReporter: {
+ dir: require('path').join(__dirname, './coverage'),
+ reports: ['html', 'lcovonly', 'text-summary'],
+ fixWebpackSourcePaths: true,
+ },
+ sonarQubeUnitReporter: {
+ sonarQubeVersion: 'LATEST',
+ outputFile: 'reports/ut_report.xml',
+ overrideTestDescription: true,
+ testPaths: ['./src'],
+ testFilePattern: '.spec.ts',
+ useBrowserName: false
+ },
+ reporters: ['progress', 'coverage','spec', 'kjhtml', 'sonarqubeUnit'],
+ port: 4200,
+ colors: true,
+ logLevel: config.LOG_INFO,
+ autoWatch: true,
+ browsers: ['FirefoxHeadless, ChromiumHeadless', 'Firefox'],
+ singleRun: true,
+ restartOnFileChange: true,
+ });
+};
diff --git a/license-check-and-add-config.json b/license-check-and-add-config.json
new file mode 100644
index 0000000..c092951
--- /dev/null
+++ b/license-check-and-add-config.json
@@ -0,0 +1,22 @@
+{
+ "ignore":["openapi","src/assets","server","Dockerfile","*.yml","*.json","*.md","**/*.conf.js",".*","*.sh","*.properties","version","development","*.json.template",".npm"],
+ "license": "LICENSE_HEADER",
+ "licenseFormats": {
+ "js|ts|css": {
+ "prepend": "/*",
+ "append": " */",
+ "eachLine": {
+ "prepend": " * "
+ }
+ },
+ "html": {
+ "prepend": "<!--",
+ "append": " -->",
+ "eachLine": {
+ "prepend": " ~ "
+ }
+ }
+ },
+ "trailingWhitespace": "TRIM",
+ "regexIdentifier": "##"
+}
diff --git a/license-report-config.json b/license-report-config.json
new file mode 100644
index 0000000..d36db0b
--- /dev/null
+++ b/license-report-config.json
@@ -0,0 +1,12 @@
+{
+"fields": [
+"name",
+"licensePeriod",
+"licenseType",
+"link",
+"remoteVersion",
+"installedVersion",
+"definedVersion",
+"author"
+]
+}
diff --git a/local.proxy.config.json b/local.proxy.config.json
new file mode 100644
index 0000000..769e90d
--- /dev/null
+++ b/local.proxy.config.json
@@ -0,0 +1,20 @@
+{
+ "/api": {
+ "target": "http://localhost:9080",
+ "secure": false,
+ "pathRewrite": {
+ "^/api": ""
+ }
+ },
+ "/mock-api": {
+ "target": "http://localhost:8089",
+ "secure": false,
+ "pathRewrite": {
+ "^/mock-api": "/portal-bff"
+ }
+ },
+ "/auth": {
+ "target": "http://localhost:8080",
+ "secure": false
+ }
+}
diff --git a/local.sh b/local.sh
new file mode 100755
index 0000000..1b13616
--- /dev/null
+++ b/local.sh
@@ -0,0 +1,25 @@
+#!/bin/sh
+#
+#
+# Copyright (c) 2022. Deutsche Telekom AG
+#
+# 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
+#
+# http://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.
+#
+# SPDX-License-Identifier: Apache-2.0
+#
+#
+#
+
+[ ! -d "./node_modules" ] && npm install
+npm start -- --proxy-config local.proxy.config.json --port 80 --host 0.0.0.0 --disable-host-check
+
diff --git a/openapi/config.json b/openapi/config.json
new file mode 100644
index 0000000..cbce309
--- /dev/null
+++ b/openapi/config.json
@@ -0,0 +1,6 @@
+{
+ "ngVersion": "12.1.0",
+ "modelFileSuffix": ".model",
+ "fileNaming": "camelCase",
+ "enumPropertyNaming": "UPPERCASE"
+}
diff --git a/openapi/input/api.yaml b/openapi/input/api.yaml
new file mode 100644
index 0000000..efe5d50
--- /dev/null
+++ b/openapi/input/api.yaml
@@ -0,0 +1,1083 @@
+openapi: 3.0.3
+info:
+ title: Portal BFF
+ version: '1.0'
+ description: Portal BFF API
+ contact:
+ name: Team Tesla
+servers:
+ - url: 'http://localhost:9080'
+tags:
+ - name: users
+ - name: roles
+ - name: tiles
+ - name: keys
+ - name: preferences
+ - name: actions
+paths:
+ /key:
+ get:
+ summary: Encrypt the logged in user name
+ description: Encrypt the logged in user name
+ operationId: encryptUser
+ parameters:
+ - $ref: '#/components/parameters/xRequestIdHeader'
+ tags:
+ - keys
+ responses:
+ '200':
+ description: Encrypted user
+ content:
+ text/plain:
+ schema:
+ type: string
+ headers:
+ X-Request-Id:
+ schema:
+ type: string
+ description: A <uuid4> in each response
+ '400':
+ $ref: '#/components/responses/BadRequest'
+ '401':
+ $ref: '#/components/responses/Unauthorized'
+ '403':
+ $ref: '#/components/responses/Forbidden'
+ '500':
+ $ref: '#/components/responses/InternalServerError'
+ '502':
+ $ref: '#/components/responses/BadGateway'
+ '/key/{keyValue}':
+ get:
+ summary: Encrypt the given key
+ description: Encrypt the given key
+ operationId: encryptKey
+ tags:
+ - keys
+ parameters:
+ - name: keyValue
+ in: path
+ required: true
+ schema:
+ $ref: '#/components/schemas/ValidString'
+ - $ref: '#/components/parameters/xRequestIdHeader'
+ responses:
+ '200':
+ description: Encrypted key
+ content:
+ text/plain:
+ schema:
+ type: string
+ headers:
+ X-Request-Id:
+ schema:
+ type: string
+ description: A <uuid4> in each response
+ '400':
+ $ref: '#/components/responses/BadRequest'
+ '401':
+ $ref: '#/components/responses/Unauthorized'
+ '403':
+ $ref: '#/components/responses/Forbidden'
+ '500':
+ $ref: '#/components/responses/InternalServerError'
+ '502':
+ $ref: '#/components/responses/BadGateway'
+ /tiles:
+ get:
+ summary: List tiles
+ description: List tiles
+ operationId: listTiles
+ parameters:
+ - $ref: '#/components/parameters/xRequestIdHeader'
+ tags:
+ - tiles
+ responses:
+ '200':
+ description: List of tiles
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/TileListResponse'
+ headers:
+ X-Request-Id:
+ schema:
+ type: string
+ description: A <uuid4> in each request
+ '400':
+ $ref: '#/components/responses/BadRequest'
+ '401':
+ $ref: '#/components/responses/Unauthorized'
+ '403':
+ $ref: '#/components/responses/Forbidden'
+ '500':
+ $ref: '#/components/responses/InternalServerError'
+ '502':
+ $ref: '#/components/responses/BadGateway'
+ '/tiles/{tileId}':
+ get:
+ summary: Retrieve detail of tile
+ description: Retrieve detail of tile
+ operationId: getTile
+ tags:
+ - tiles
+ parameters:
+ - $ref: '#/components/parameters/tileIdPathParam'
+ - $ref: '#/components/parameters/xRequestIdHeader'
+ responses:
+ '200':
+ description: Detail of tile
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/TileResponse'
+ headers:
+ X-Request-Id:
+ schema:
+ type: string
+ description: A <uuid4> in each response
+ '400':
+ $ref: '#/components/responses/BadRequest'
+ '401':
+ $ref: '#/components/responses/Unauthorized'
+ '403':
+ $ref: '#/components/responses/Forbidden'
+ '500':
+ $ref: '#/components/responses/InternalServerError'
+ '502':
+ $ref: '#/components/responses/BadGateway'
+ /users:
+ get:
+ summary: List users
+ description: List users
+ operationId: listUsers
+ tags:
+ - users
+ parameters:
+ - $ref: '#/components/parameters/pageQueryParam'
+ - $ref: '#/components/parameters/pageSizeQueryParam'
+ - $ref: '#/components/parameters/xRequestIdHeader'
+ responses:
+ '200':
+ description: List of users
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/UserListResponse'
+ headers:
+ X-Request-Id:
+ schema:
+ type: string
+ description: A <uuid4> in each response
+ '400':
+ $ref: '#/components/responses/BadRequest'
+ '401':
+ $ref: '#/components/responses/Unauthorized'
+ '403':
+ $ref: '#/components/responses/Forbidden'
+ '500':
+ $ref: '#/components/responses/InternalServerError'
+ '502':
+ $ref: '#/components/responses/BadGateway'
+ post:
+ summary: Create user
+ description: Create user
+ operationId: createUser
+ tags:
+ - users
+ parameters:
+ - $ref: '#/components/parameters/xRequestIdHeader'
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/CreateUserRequest'
+ responses:
+ '200':
+ description: Detail of user
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/UserResponse'
+ headers:
+ X-Request-Id:
+ schema:
+ type: string
+ description: A <uuid4> in each response
+ '400':
+ $ref: '#/components/responses/BadRequest'
+ '401':
+ $ref: '#/components/responses/Unauthorized'
+ '403':
+ $ref: '#/components/responses/Forbidden'
+ '500':
+ $ref: '#/components/responses/InternalServerError'
+ '502':
+ $ref: '#/components/responses/BadGateway'
+ '/users/{userId}':
+ get:
+ summary: Retrieve detail of user
+ description: Retrieve detail of user
+ operationId: getUser
+ tags:
+ - users
+ parameters:
+ - $ref: '#/components/parameters/userIdPathParam'
+ - $ref: '#/components/parameters/xRequestIdHeader'
+ responses:
+ '200':
+ description: Detail of user
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/UserResponse'
+ headers:
+ X-Request-Id:
+ schema:
+ type: string
+ description: A <uuid4> in each response
+ '400':
+ $ref: '#/components/responses/BadRequest'
+ '401':
+ $ref: '#/components/responses/Unauthorized'
+ '403':
+ $ref: '#/components/responses/Forbidden'
+ '500':
+ $ref: '#/components/responses/InternalServerError'
+ '502':
+ $ref: '#/components/responses/BadGateway'
+ put:
+ summary: Update user
+ description: Update user
+ operationId: updateUser
+ tags:
+ - users
+ parameters:
+ - $ref: '#/components/parameters/userIdPathParam'
+ - $ref: '#/components/parameters/xRequestIdHeader'
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/UpdateUserRequest'
+ responses:
+ '200':
+ description: User updated successfully
+ headers:
+ X-Request-Id:
+ schema:
+ type: string
+ description: A <uuid4> in each response
+ '400':
+ $ref: '#/components/responses/BadRequest'
+ '401':
+ $ref: '#/components/responses/Unauthorized'
+ '403':
+ $ref: '#/components/responses/Forbidden'
+ '500':
+ $ref: '#/components/responses/InternalServerError'
+ '502':
+ $ref: '#/components/responses/BadGateway'
+ delete:
+ summary: Delete user
+ description: Delete user
+ operationId: deleteUser
+ tags:
+ - users
+ parameters:
+ - $ref: '#/components/parameters/userIdPathParam'
+ - $ref: '#/components/parameters/xRequestIdHeader'
+ responses:
+ '204':
+ description: No Content
+ headers:
+ X-Request-Id:
+ schema:
+ type: string
+ description: A <uuid4> in each response
+ '400':
+ $ref: '#/components/responses/BadRequest'
+ '401':
+ $ref: '#/components/responses/Unauthorized'
+ '403':
+ $ref: '#/components/responses/Forbidden'
+ '500':
+ $ref: '#/components/responses/InternalServerError'
+ '502':
+ $ref: '#/components/responses/BadGateway'
+ '/users/{userId}/roles':
+ get:
+ summary: List assigned roles
+ description: List assigned roles
+ operationId: listAssignedRoles
+ tags:
+ - users
+ parameters:
+ - $ref: '#/components/parameters/userIdPathParam'
+ - $ref: '#/components/parameters/xRequestIdHeader'
+ responses:
+ '200':
+ description: List of assigned roles
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/RoleListResponse'
+ headers:
+ X-Request-Id:
+ schema:
+ type: string
+ description: A <uuid4> in each response
+ '400':
+ $ref: '#/components/responses/BadRequest'
+ '401':
+ $ref: '#/components/responses/Unauthorized'
+ '403':
+ $ref: '#/components/responses/Forbidden'
+ '500':
+ $ref: '#/components/responses/InternalServerError'
+ '502':
+ $ref: '#/components/responses/BadGateway'
+ put:
+ summary: Update assigned roles
+ description: Update assigned roles
+ operationId: updateAssignedRoles
+ tags:
+ - users
+ parameters:
+ - $ref: '#/components/parameters/userIdPathParam'
+ - $ref: '#/components/parameters/xRequestIdHeader'
+ requestBody:
+ description: IDs of roles which should be assigned
+ required: false
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/UpdateAssignedRolesRequest'
+ responses:
+ '200':
+ description: List of assigned roles
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/RoleListResponse'
+ headers:
+ X-Request-Id:
+ schema:
+ type: string
+ description: A <uuid4> in each response
+ '400':
+ $ref: '#/components/responses/BadRequest'
+ '401':
+ $ref: '#/components/responses/Unauthorized'
+ '403':
+ $ref: '#/components/responses/Forbidden'
+ '500':
+ $ref: '#/components/responses/InternalServerError'
+ '502':
+ $ref: '#/components/responses/BadGateway'
+ '/users/{userId}/roles/available':
+ get:
+ summary: List available roles
+ description: List available roles
+ operationId: listAvailableRoles
+ tags:
+ - users
+ parameters:
+ - $ref: '#/components/parameters/userIdPathParam'
+ - $ref: '#/components/parameters/xRequestIdHeader'
+ responses:
+ '200':
+ description: List of available roles
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/RoleListResponse'
+ headers:
+ X-Request-Id:
+ schema:
+ type: string
+ description: A <uuid4> in each response
+ '400':
+ $ref: '#/components/responses/BadRequest'
+ '401':
+ $ref: '#/components/responses/Unauthorized'
+ '403':
+ $ref: '#/components/responses/Forbidden'
+ '500':
+ $ref: '#/components/responses/InternalServerError'
+ '502':
+ $ref: '#/components/responses/BadGateway'
+ '/users/{userId}/password':
+ put:
+ summary: Update password
+ description: Update password
+ operationId: updatePassword
+ tags:
+ - users
+ parameters:
+ - $ref: '#/components/parameters/userIdPathParam'
+ - $ref: '#/components/parameters/xRequestIdHeader'
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/UpdateUserPasswordRequest'
+ responses:
+ '204':
+ description: Password was changed successfully
+ headers:
+ X-Request-Id:
+ schema:
+ type: string
+ description: A <uuid4> in each response
+ '400':
+ $ref: '#/components/responses/BadRequest'
+ '401':
+ $ref: '#/components/responses/Unauthorized'
+ '403':
+ $ref: '#/components/responses/Forbidden'
+ '500':
+ $ref: '#/components/responses/InternalServerError'
+ '502':
+ $ref: '#/components/responses/BadGateway'
+ /roles:
+ get:
+ summary: List roles
+ description: List roles
+ operationId: listRoles
+ tags:
+ - roles
+ parameters:
+ - $ref: '#/components/parameters/xRequestIdHeader'
+ responses:
+ '200':
+ description: List of roles
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/RoleListResponse'
+ headers:
+ X-Request-Id:
+ schema:
+ type: string
+ description: A <uuid4> in each response
+ '400':
+ $ref: '#/components/responses/BadRequest'
+ '401':
+ $ref: '#/components/responses/Unauthorized'
+ '403':
+ $ref: '#/components/responses/Forbidden'
+ '500':
+ $ref: '#/components/responses/InternalServerError'
+ '502':
+ $ref: '#/components/responses/BadGateway'
+ /preferences:
+ get:
+ description: Returns user preferences
+ summary: Get user preferences
+ operationId: getPreferences
+ parameters:
+ - $ref: '#/components/parameters/xRequestIdHeader'
+ tags:
+ - preferences
+ responses:
+ '200':
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/PreferencesResponse'
+ '400':
+ $ref: '#/components/responses/BadRequest'
+ '401':
+ $ref: '#/components/responses/Unauthorized'
+ '403':
+ $ref: '#/components/responses/Forbidden'
+ '500':
+ $ref: '#/components/responses/InternalServerError'
+ '502':
+ $ref: '#/components/responses/BadGateway'
+ put:
+ description: Updates user preferences
+ summary: Update user preferences
+ operationId: updatePreferences
+ parameters:
+ - $ref: '#/components/parameters/xRequestIdHeader'
+ tags:
+ - preferences
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/CreatePreferencesRequest'
+ responses:
+ '200':
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/PreferencesResponse'
+ '400':
+ $ref: '#/components/responses/BadRequest'
+ '401':
+ $ref: '#/components/responses/Unauthorized'
+ '403':
+ $ref: '#/components/responses/Forbidden'
+ '500':
+ $ref: '#/components/responses/InternalServerError'
+ '502':
+ $ref: '#/components/responses/BadGateway'
+ post:
+ description: Save user preferences
+ summary: Save user preferences
+ operationId: savePreferences
+ parameters:
+ - $ref: '#/components/parameters/xRequestIdHeader'
+ tags:
+ - preferences
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/CreatePreferencesRequest'
+ responses:
+ '200':
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/PreferencesResponse'
+ '400':
+ $ref: '#/components/responses/BadRequest'
+ '401':
+ $ref: '#/components/responses/Unauthorized'
+ '403':
+ $ref: '#/components/responses/Forbidden'
+ '500':
+ $ref: '#/components/responses/InternalServerError'
+ '502':
+ $ref: '#/components/responses/BadGateway'
+ '/actions/{userId}':
+ get:
+ summary: Retrieve all actions for a specific user
+ responses:
+ '200':
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ActionsListResponse'
+ '400':
+ $ref: '#/components/responses/BadRequest'
+ '401':
+ $ref: '#/components/responses/Unauthorized'
+ '403':
+ $ref: '#/components/responses/Forbidden'
+ '500':
+ $ref: '#/components/responses/InternalServerError'
+ '502':
+ $ref: '#/components/responses/BadGateway'
+ operationId: getActions
+ parameters:
+ - $ref: '#/components/parameters/pageQueryParam'
+ - $ref: '#/components/parameters/pageSizeQueryParam'
+ - schema:
+ type: integer
+ format: int32
+ in: query
+ name: showLastHours
+ description: Get all actions within the last X hours.
+ - $ref: '#/components/parameters/xRequestIdHeader'
+ description: Get actions for the given userId
+ tags:
+ - actions
+ parameters:
+ - $ref: '#/components/parameters/userIdPathParam'
+ post:
+ summary: Create an action for a given user
+ operationId: createAction
+ responses:
+ '200':
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ActionsResponse'
+ '400':
+ $ref: '#/components/responses/BadRequest'
+ '401':
+ $ref: '#/components/responses/Unauthorized'
+ '403':
+ $ref: '#/components/responses/Forbidden'
+ '500':
+ $ref: '#/components/responses/InternalServerError'
+ '502':
+ $ref: '#/components/responses/BadGateway'
+ requestBody:
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/CreateActionRequest'
+ description: Only one action in each POST request
+ parameters:
+ - $ref: '#/components/parameters/xRequestIdHeader'
+ description: Create a user action
+ tags:
+ - actions
+ /actions:
+ get:
+ summary: Retrieve all actions from the portal with an optional timeframe
+ responses:
+ '200':
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ActionsListResponse'
+ '400':
+ $ref: '#/components/responses/BadRequest'
+ '401':
+ $ref: '#/components/responses/Unauthorized'
+ '403':
+ $ref: '#/components/responses/Forbidden'
+ '500':
+ $ref: '#/components/responses/InternalServerError'
+ '502':
+ $ref: '#/components/responses/BadGateway'
+ operationId: listActions
+ parameters:
+ - $ref: '#/components/parameters/pageQueryParam'
+ - $ref: '#/components/parameters/pageSizeQueryParam'
+ - schema:
+ type: integer
+ format: int32
+ in: query
+ name: showLastHours
+ description: Get all actions within the last X hours.
+ - $ref: '#/components/parameters/xRequestIdHeader'
+ description: Get portal actions from all users
+ tags:
+ - actions
+ parameters: []
+components:
+ responses:
+ BadRequest:
+ description: '400: Bad Request'
+ content:
+ application/problem+json:
+ schema:
+ $ref: '#/components/schemas/Problem'
+ headers:
+ X-Request-Id:
+ schema:
+ type: string
+ description: A <uuid4> in each response
+ Unauthorized:
+ description: '401: Unauthorized'
+ content:
+ application/problem+json:
+ schema:
+ $ref: '#/components/schemas/Problem'
+ headers:
+ X-Request-Id:
+ schema:
+ type: string
+ description: A <uuid4> in each response
+ Forbidden:
+ description: '403: Forbidden'
+ content:
+ application/problem+json:
+ schema:
+ $ref: '#/components/schemas/Problem'
+ headers:
+ X-Request-Id:
+ schema:
+ type: string
+ description: A <uuid4> in each response
+ NotFound:
+ description: '404: Not Found'
+ content:
+ application/problem+json:
+ schema:
+ $ref: '#/components/schemas/Problem'
+ headers:
+ X-Request-Id:
+ schema:
+ type: string
+ description: A <uuid4> in each response
+ NotAllowed:
+ description: '405: Method Not Allowed'
+ content:
+ application/problem+json:
+ schema:
+ $ref: '#/components/schemas/Problem'
+ headers:
+ X-Request-Id:
+ schema:
+ type: string
+ description: A <uuid4> in each response
+ Conflict:
+ description: '409: Conflict'
+ content:
+ application/problem+json:
+ schema:
+ $ref: '#/components/schemas/Problem'
+ headers:
+ X-Request-Id:
+ schema:
+ type: string
+ description: A <uuid4> in each response
+ InternalServerError:
+ description: Internal Server Error
+ content:
+ application/problem+json:
+ schema:
+ $ref: '#/components/schemas/Problem'
+ headers:
+ X-Request-Id:
+ schema:
+ type: string
+ description: A <uuid4> in each response
+ BadGateway:
+ description: Bad Gateway
+ content:
+ application/problem+json:
+ schema:
+ $ref: '#/components/schemas/Problem'
+ headers:
+ X-Request-Id:
+ schema:
+ type: string
+ description: A <uuid4> in each response
+ securitySchemes:
+ bearerAuth:
+ type: http
+ scheme: bearer
+ bearerFormat: JWT
+ headers:
+ X-Request-Id:
+ description: The unique identifier of the request
+ schema:
+ type: string
+ parameters:
+ xRequestIdHeader:
+ name: X-Request-Id
+ in: header
+ schema:
+ type: string
+ description: The unique identifier of the request
+ required: false
+ userIdPathParam:
+ name: userId
+ in: path
+ description: User ID
+ required: true
+ schema:
+ $ref: '#/components/schemas/ValidString'
+ tileIdPathParam:
+ name: tileId
+ in: path
+ description: Tile ID
+ required: true
+ schema:
+ type: integer
+ format: int64
+ pageQueryParam:
+ name: page
+ in: query
+ description: Page index (1..N)
+ required: false
+ schema:
+ type: integer
+ format: int32
+ minimum: 1
+ default: 1
+ pageSizeQueryParam:
+ name: pageSize
+ in: query
+ description: The size of the page to be returned
+ required: false
+ schema:
+ type: integer
+ format: int32
+ minimum: 1
+ maximum: 5000
+ default: 10
+ schemas:
+ CreatePreferencesRequest:
+ type: object
+ required:
+ - keys
+ properties:
+ properties:
+ type: object
+ title: CreatePreferencesRequest
+ x-internal: false
+ PreferencesResponse:
+ type: object
+ title: PreferencesResponse
+ properties:
+ properties:
+ type: object
+ required:
+ - properties
+ ValidString:
+ type: string
+ pattern: '[\w,/!=§#@€:µ.*+?'' \-\u00C0-\u017F]*'
+ Problem:
+ type: object
+ required:
+ - status
+ - title
+ properties:
+ type:
+ type: string
+ format: uri-reference
+ description: |
+ A URI reference that uniquely identifies the problem type only in the context of the provided API. Opposed to the specification in RFC-7807, it is neither recommended to be dereferencable and point to a human-readable documentation nor globally unique for the problem type.
+ default: 'about:blank'
+ example: /problem/connection-error
+ title:
+ type: string
+ description: |
+ A short summary of the problem type. Written in English and readable for engineers, usually not suited for non technical stakeholders and not localized.
+ example: Service Unavailable
+ status:
+ type: integer
+ format: int32
+ description: |
+ The HTTP status code generated by the origin server for this occurrence of the problem.
+ minimum: 100
+ maximum: 600
+ exclusiveMaximum: true
+ example: 503
+ detail:
+ type: string
+ description: |
+ A human readable explanation specific to this occurrence of the problem that is helpful to locate the problem and give advice on how to proceed. Written in English and readable for engineers, usually not suited for non technical stakeholders and not localized.
+ example: Connection to database timed out
+ downstreamSystem:
+ type: string
+ description: The downstream system that responded with error
+ enum:
+ - KEYCLOAK
+ - PORTAL_SERVICE
+ - PORTAL_PREFS
+ - PORTAL_HISTORY
+ downstreamStatus:
+ type: integer
+ format: int32
+ description: |
+ Response status from the downstream system.
+ example: 401
+ downstreamMessageId:
+ type: string
+ description: |
+ The identifier of the error message from the downstream system.
+ example: SVC3001
+ instance:
+ type: string
+ format: uri-reference
+ description: |
+ A URI reference that identifies the specific occurrence of the problem, e.g. by adding a fragment identifier or sub-path to the problem type. May be used to locate the root of this problem in the source code.
+ example: /problem/connection-error#token-info-read-timed-out
+ UserResponse:
+ type: object
+ required:
+ - id
+ - username
+ - enabled
+ properties:
+ id:
+ type: string
+ username:
+ type: string
+ email:
+ type: string
+ firstName:
+ type: string
+ lastName:
+ type: string
+ enabled:
+ type: boolean
+ realmRoles:
+ type: array
+ items:
+ type: string
+ UserListResponse:
+ type: object
+ required:
+ - items
+ - totalCount
+ properties:
+ items:
+ type: array
+ items:
+ $ref: '#/components/schemas/UserResponse'
+ totalCount:
+ type: integer
+ format: int32
+ CreateUserRequest:
+ type: object
+ required:
+ - username
+ - enabled
+ - email
+ - roles
+ properties:
+ username:
+ $ref: '#/components/schemas/ValidString'
+ email:
+ $ref: '#/components/schemas/ValidString'
+ firstName:
+ $ref: '#/components/schemas/ValidString'
+ lastName:
+ $ref: '#/components/schemas/ValidString'
+ enabled:
+ type: boolean
+ roles:
+ type: array
+ items:
+ $ref: '#/components/schemas/Role'
+ UpdateUserRequest:
+ type: object
+ required:
+ - enabled
+ properties:
+ email:
+ $ref: '#/components/schemas/ValidString'
+ firstName:
+ $ref: '#/components/schemas/ValidString'
+ lastName:
+ $ref: '#/components/schemas/ValidString'
+ enabled:
+ type: boolean
+ UpdateUserPasswordRequest:
+ type: object
+ required:
+ - value
+ - temporary
+ properties:
+ value:
+ $ref: '#/components/schemas/ValidString'
+ temporary:
+ type: boolean
+ Role:
+ type: object
+ required:
+ - id
+ - name
+ properties:
+ id:
+ $ref: '#/components/schemas/ValidString'
+ name:
+ $ref: '#/components/schemas/ValidString'
+ RoleListResponse:
+ type: object
+ required:
+ - items
+ - totalCount
+ properties:
+ items:
+ type: array
+ items:
+ $ref: '#/components/schemas/Role'
+ totalCount:
+ type: integer
+ format: int32
+ UpdateAssignedRolesRequest:
+ type: array
+ items:
+ $ref: '#/components/schemas/Role'
+ TileListResponse:
+ type: object
+ required:
+ - items
+ properties:
+ items:
+ type: array
+ items:
+ $ref: '#/components/schemas/TileResponse'
+ TileResponse:
+ type: object
+ required:
+ - id
+ - title
+ - imageUrl
+ - redirectUrl
+ - groups
+ - roles
+ properties:
+ id:
+ type: integer
+ format: int64
+ title:
+ type: string
+ description:
+ type: string
+ imageUrl:
+ type: string
+ imageAltText:
+ type: string
+ redirectUrl:
+ type: string
+ headers:
+ type: string
+ groups:
+ type: array
+ items:
+ type: string
+ roles:
+ type: array
+ items:
+ type: string
+ ActionsResponse:
+ title: ActionsResponse
+ type: object
+ properties:
+ actionCreatedAt:
+ type: string
+ format: date-time
+ action:
+ type: object
+ saveInterval:
+ type: integer
+ format: int32
+ required:
+ - actionCreatedAt
+ - action
+ CreateActionRequest:
+ title: CreateActionRequest
+ type: object
+ properties:
+ userId:
+ type: string
+ actionCreatedAt:
+ type: string
+ format: date-time
+ action:
+ type: object
+ required:
+ - userId
+ - actionCreatedAt
+ - action
+ ActionsListResponse:
+ title: ActionsListResponse
+ type: object
+ properties:
+ items:
+ type: array
+ items:
+ $ref: '#/components/schemas/ActionsResponse'
+ totalCount:
+ type: integer
+ format: int32
+ required:
+ - items
+ - totalCount
+security:
+ - bearerAuth: []
diff --git a/openapi/openapi-generator-cli b/openapi/openapi-generator-cli
new file mode 100755
index 0000000..9b59d6d
--- /dev/null
+++ b/openapi/openapi-generator-cli
@@ -0,0 +1,70 @@
+#!/usr/bin/env bash
+####
+# Save as openapi-generator-cli on your PATH. chmod u+x. Enjoy.
+#
+# This script will query github on every invocation to pull the latest released version
+# of openapi-generator.
+#
+# If you want repeatable executions, you can explicitly set a version via
+# OPENAPI_GENERATOR_VERSION
+# e.g. (in Bash)
+# export OPENAPI_GENERATOR_VERSION=3.1.0
+# openapi-generator-cli.sh
+# or
+# OPENAPI_GENERATOR_VERSION=3.1.0 openapi-generator-cli.sh
+#
+# This is also helpful, for example, if you want to evaluate a SNAPSHOT version.
+#
+# NOTE: Jars are downloaded on demand from maven into the same directory as this script
+# for every 'latest' version pulled from github. Consider putting this under its own directory.
+####
+set -o pipefail
+
+for cmd in {mvn,jq,curl}; do
+ if ! command -v ${cmd} > /dev/null; then
+ >&2 echo "This script requires '${cmd}' to be installed."
+ exit 1
+ fi
+done
+
+function latest.tag {
+ local uri="https://api.github.com/repos/${1}/releases"
+ local ver=$(curl -s ${uri} | jq -r 'first(.[]|select(.prerelease==false)).tag_name')
+ if [[ $ver == v* ]]; then
+ ver=${ver:1}
+ fi
+ echo $ver
+}
+
+ghrepo=openapitools/openapi-generator
+groupid=org.openapitools
+artifactid=openapi-generator-cli
+ver=${OPENAPI_GENERATOR_VERSION:-$(latest.tag $ghrepo)}
+
+jar=${artifactid}-${ver}.jar
+cachedir=${OPENAPI_GENERATOR_DOWNLOAD_CACHE_DIR}
+
+DIR=${cachedir:-"$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"}
+
+if [ ! -d "${DIR}" ]; then
+ mkdir -p "${DIR}"
+fi
+
+if [ ! -f ${DIR}/${jar} ]; then
+ repo="central::default::https://repo1.maven.org/maven2/"
+ if [[ ${ver} =~ ^.*-SNAPSHOT$ ]]; then
+ repo="central::default::https://oss.sonatype.org/content/repositories/snapshots"
+ fi
+ mvn org.apache.maven.plugins:maven-dependency-plugin:2.9:get \
+ -DremoteRepositories=${repo} \
+ -Dartifact=${groupid}:${artifactid}:${ver} \
+ -Dtransitive=false \
+ -Ddest=${DIR}/${jar}
+fi
+
+java -ea \
+ ${JAVA_OPTS} \
+ -Xms512M \
+ -Xmx1024M \
+ -server \
+ -jar ${DIR}/${jar} "$@" \ No newline at end of file
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..b1e036e
--- /dev/null
+++ b/package.json
@@ -0,0 +1,102 @@
+{
+ "name": "frontend",
+ "version": "0.0.0",
+ "scripts": {
+ "openapi": "rm -rf ./openapi/output && bash ./openapi/openapi-generator-cli generate -i ./openapi/input/api.yaml -g typescript-angular -o ./openapi/output -c ./openapi/config.json",
+ "ng": "./node-modules/.bin/ng",
+ "prestart": "npm run openapi",
+ "start": "ng serve -- --proxy-config staging.proxy.config.json --host 0.0.0.0 --port 80 --disable-host-check",
+ "prebuild": "npm run openapi",
+ "build": "ng build",
+ "pretest": "npm run openapi",
+ "test": "ng test --browsers FirefoxHeadless",
+ "prelint": "npm run openapi",
+ "lint": "ng lint",
+ "pree2e": "npm run openapi",
+ "e2e": "ng e2e",
+ "compodoc": "compodoc -p tsconfig.json",
+ "compodoc-4200": "compodoc -p tsconfig.json -s -r 4200",
+ "sonar": "sonar-scanner",
+ "licences": "license-report --output=html > ./portal_ui_licences.html --config ./license-report-config.json"
+ },
+ "private": true,
+ "dependencies": {
+ "@angular/animations": "13.3.10",
+ "@angular/cdk": "13.3.8",
+ "@angular/common": "13.3.10",
+ "@angular/compiler": "13.3.10",
+ "@angular/core": "13.3.10",
+ "@angular/forms": "13.3.10",
+ "@angular/localize": "13.3.10",
+ "@angular/platform-browser": "13.3.10",
+ "@angular/platform-browser-dynamic": "13.3.10",
+ "@angular/router": "13.3.10",
+ "@ng-bootstrap/ng-bootstrap": "12.1.2",
+ "@ngx-translate/core": "13.0.0",
+ "@ngx-translate/http-loader": "6.0.0",
+ "@popperjs/core": "2.11.5",
+ "angular-oauth2-oidc": "12.1.0",
+ "bootstrap": "5.1.3",
+ "license-report": "5.0.2",
+ "lodash": "4.17.21",
+ "ng-bootstrap": "1.6.3",
+ "rxjs": "6.6.7",
+ "tslib": "2.2.0",
+ "zone.js": "0.11.5"
+ },
+ "devDependencies": {
+ "@angular-devkit/build-angular": "13.3.7",
+ "@angular-eslint/builder": "13.2.1",
+ "@angular-eslint/eslint-plugin": "13.2.1",
+ "@angular-eslint/eslint-plugin-template": "13.2.1",
+ "@angular-eslint/schematics": "13.2.1",
+ "@angular-eslint/template-parser": "13.2.1",
+ "@angular/cli": "13.3.7",
+ "@angular/compiler-cli": "13.3.10",
+ "@types/jasmine": "~3.6.0",
+ "@types/jasminewd2": "2.0.3",
+ "@types/lodash": "^4.14.188",
+ "@types/node": "16.0.0",
+ "@types/uuid": "^8.3.4",
+ "@typescript-eslint/eslint-plugin": "5.17.0",
+ "@typescript-eslint/parser": "5.17.0",
+ "eslint": "7.32.0",
+ "git-format-staged": "3.0.0",
+ "husky": "8.0.1",
+ "jasmine-core": "~4.0.0",
+ "jasmine-spec-reporter": "~7.0.0",
+ "karma": "6.3.20",
+ "karma-chrome-launcher": "3.1.1",
+ "karma-coverage": "2.2.0",
+ "karma-coverage-istanbul-reporter": "3.0.3",
+ "karma-firefox-launcher": "2.1.2",
+ "karma-jasmine": "4.0.2",
+ "karma-jasmine-html-reporter": "1.7.0",
+ "karma-sonarqube-unit-reporter": "0.0.23",
+ "karma-spec-reporter": "0.0.33",
+ "license-check-and-add": "4.0.5",
+ "license-checker": "25.0.1",
+ "lint-staged": "12.4.1",
+ "prettier": "2.6.2",
+ "protractor": "7.0.0",
+ "sonar-scanner": "3.1.0",
+ "ts-node": "10.0.0",
+ "typescript": "4.6.4"
+ },
+ "husky": {
+ "hooks": {
+ "pre-commit": "lint-staged"
+ }
+ },
+ "lint-staged": {
+ "*.{js,ts}": [
+ "prettier --write .",
+ "eslint --fix",
+ "git add"
+ ],
+ "*.{html,css}": [
+ "prettier --write .",
+ "git add"
+ ]
+ }
+}
diff --git a/server/nginx.template b/server/nginx.template
new file mode 100644
index 0000000..98798bc
--- /dev/null
+++ b/server/nginx.template
@@ -0,0 +1,111 @@
+# Log format for onap logging
+log_format onap_logging '"$request_body"';
+
+lua_package_path '/usr/local/openresty/lualib/?.lua;;';
+
+# cache for discovery metadata documents
+lua_shared_dict discovery 1m;
+
+# cache for JWKs
+lua_shared_dict jwks 1m;
+
+# if run in local docker container add this resolver for the DNS to connect to Keycloak
+resolver ${CLUSTER_NAMESERVER_IP};
+
+error_log logs/error.log error;
+
+server { # simple reverse-proxy
+ listen ${NGINX_PORT};
+
+ location / {
+ root /usr/share/nginx/html;
+ index index.html;
+ try_files $uri $uri/ /index.html =404;
+ }
+
+ location /api/ {
+ add_header Access-Control-Allow-Origin *;
+ proxy_pass ${BFF_URL}/;
+ proxy_http_version 1.1;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Host $host;
+ proxy_set_header X-Forwarded-Server $host;
+ proxy_set_header X-Forwarded-Port $server_port;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ }
+
+ location /auth/ {
+ add_header Access-Control-Allow-Origin *;
+ proxy_pass ${KEYCLOAK_INTERNAL_URL}/auth/;
+ proxy_http_version 1.1;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Host $host;
+ proxy_set_header X-Forwarded-Server $host;
+ proxy_set_header X-Forwarded-Port $server_port;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ }
+
+ location = /onap_logging {
+ access_by_lua '
+ local openidc = require("resty.openidc");
+ -- uncomment for logging next line
+ -- openidc.set_logging(nil, { DEBUG = ngx.DEBUG });
+ local opts = {
+ discovery = "${KEYCLOAK_INTERNAL_URL}/auth/realms/${KEYCLOAK_REALM}/.well-known/openid-configuration",
+
+ -- the signature algorithm that you expect has been used;
+ -- can be a single string or a table.
+ -- You should set this for security reasons in order to
+ -- avoid accepting a token claiming to be signed by HMAC
+ -- using a public RSA key.
+ -- token_signing_alg_values_expected = { "HS256" },
+
+ -- if you want to accept unsigned tokens (using the
+ -- "none" signature algorithm) then set this to true.
+ accept_none_alg = false,
+
+ -- if you want to reject tokens signed using an algorithm
+ -- not supported by lua-resty-jwt set this to false. If
+ -- you leave it unset, the token signature will not be
+ -- verified at all.
+ accept_unsupported_alg = false
+ }
+ -- call introspect for OAuth 2.0 Bearer Access Token validation
+ local res, err = require("resty.openidc").bearer_jwt_verify(opts)
+
+ if err then
+ ngx.status = 403
+ ngx.say(err)
+ ngx.exit(ngx.HTTP_FORBIDDEN)
+ end
+
+ ';
+ access_log /dev/stdout onap_logging;
+ proxy_pass http://portal-ui/onap_logging_proxy;
+ proxy_http_version 1.1;
+ }
+
+ location = /onap_logging_proxy {
+ access_log off;
+ return 200 'Message logged';
+ }
+}
+
+##
+# Gzip Settings
+##
+
+gzip on;
+
+gzip_vary on;
+gzip_proxied any;
+gzip_min_length 1100;
+gzip_comp_level 6;
+gzip_buffers 16 8k;
+gzip_http_version 1.1;
+gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
+
diff --git a/server/resty/evp.lua b/server/resty/evp.lua
new file mode 100644
index 0000000..584ff5a
--- /dev/null
+++ b/server/resty/evp.lua
@@ -0,0 +1,804 @@
+local ffi = require "ffi"
+local ffi_copy = ffi.copy
+local ffi_gc = ffi.gc
+local ffi_new = ffi.new
+local ffi_string = ffi.string
+local ffi_cast = ffi.cast
+local _C = ffi.C
+
+local _M = { _VERSION = "0.2.3" }
+
+local ngx = ngx
+
+
+local CONST = {
+ SHA256_DIGEST = "SHA256",
+ SHA512_DIGEST = "SHA512",
+ -- ref : https://github.com/openssl/openssl/blob/master/include/openssl/rsa.h
+ RSA_PKCS1_PADDING = 1,
+ RSA_SSLV23_PADDING = 2,
+ RSA_NO_PADDING = 3,
+ RSA_PKCS1_OAEP_PADDING = 4,
+ RSA_X931_PADDING = 5,
+ RSA_PKCS1_PSS_PADDING = 6,
+ -- ref : https://github.com/openssl/openssl/blob/master/include/openssl/evp.h
+ NID_rsaEncryption = 6,
+ EVP_PKEY_RSA = 6,
+ EVP_PKEY_ALG_CTRL = 0x1000,
+ EVP_PKEY_CTRL_RSA_PADDING = 0x1000 + 1,
+
+ EVP_PKEY_OP_TYPE_CRYPT = 768,
+ EVP_PKEY_CTRL_RSA_OAEP_MD = 0x1000 + 9
+}
+_M.CONST = CONST
+
+
+-- Reference: https://wiki.openssl.org/index.php/EVP_Signing_and_Verifying
+ffi.cdef[[
+// Error handling
+unsigned long ERR_get_error(void);
+const char * ERR_reason_error_string(unsigned long e);
+
+// Basic IO
+typedef struct bio_st BIO;
+typedef struct bio_method_st BIO_METHOD;
+BIO_METHOD *BIO_s_mem(void);
+BIO * BIO_new(BIO_METHOD *type);
+int BIO_puts(BIO *bp,const char *buf);
+void BIO_vfree(BIO *a);
+int BIO_write(BIO *b, const void *buf, int len);
+
+// RSA
+typedef struct rsa_st RSA;
+int RSA_size(const RSA *rsa);
+void RSA_free(RSA *rsa);
+typedef int pem_password_cb(char *buf, int size, int rwflag, void *userdata);
+RSA * PEM_read_bio_RSAPrivateKey(BIO *bp, RSA **rsa, pem_password_cb *cb,
+ void *u);
+RSA * PEM_read_bio_RSAPublicKey(BIO *bp, RSA **rsa, pem_password_cb *cb,
+ void *u);
+
+// EC_KEY
+typedef struct ec_key_st EC_KEY;
+void EC_KEY_free(EC_KEY *key);
+EC_KEY * PEM_read_bio_ECPrivateKey(BIO *bp, EC_KEY **key, pem_password_cb *cb,
+ void *u);
+EC_KEY * PEM_read_bio_ECPublicKey(BIO *bp, EC_KEY **key, pem_password_cb *cb,
+ void *u);
+// EVP PKEY
+typedef struct evp_pkey_st EVP_PKEY;
+typedef struct engine_st ENGINE;
+EVP_PKEY *EVP_PKEY_new(void);
+int EVP_PKEY_set1_RSA(EVP_PKEY *pkey,RSA *key);
+int EVP_PKEY_set1_EC_KEY(EVP_PKEY *pkey,EC_KEY *key);
+EVP_PKEY *EVP_PKEY_new_mac_key(int type, ENGINE *e,
+ const unsigned char *key, int keylen);
+void EVP_PKEY_free(EVP_PKEY *key);
+int i2d_RSA(RSA *a, unsigned char **out);
+
+// Additional typedef of ECC operations (DER/RAW sig conversion)
+typedef struct bignum_st BIGNUM;
+BIGNUM *BN_new(void);
+void BN_free(BIGNUM *a);
+int BN_num_bits(const BIGNUM *a);
+int BN_bn2bin(const BIGNUM *a, unsigned char *to);
+BIGNUM *BN_bin2bn(const unsigned char *s, int len, BIGNUM *ret);
+char *BN_bn2hex(const BIGNUM *a);
+
+
+typedef struct ECDSA_SIG_st {
+ BIGNUM *r;
+ BIGNUM *s;} ECDSA_SIG;
+ECDSA_SIG* ECDSA_SIG_new(void);
+int i2d_ECDSA_SIG(const ECDSA_SIG *sig, unsigned char **pp);
+ECDSA_SIG* d2i_ECDSA_SIG(ECDSA_SIG **sig, unsigned char **pp,
+long len);
+void ECDSA_SIG_free(ECDSA_SIG *sig);
+
+typedef struct ecgroup_st EC_GROUP;
+
+EC_GROUP *EC_KEY_get0_group(const EC_KEY *key);
+EC_KEY *EVP_PKEY_get0_EC_KEY(EVP_PKEY *pkey);
+int EC_GROUP_get_order(const EC_GROUP *group, BIGNUM *order, void *ctx);
+
+
+// PUBKEY
+EVP_PKEY *PEM_read_bio_PUBKEY(BIO *bp, EVP_PKEY **x,
+ pem_password_cb *cb, void *u);
+
+// X509
+typedef struct x509_st X509;
+X509 *PEM_read_bio_X509(BIO *bp, X509 **x, pem_password_cb *cb, void *u);
+EVP_PKEY * X509_get_pubkey(X509 *x);
+void X509_free(X509 *a);
+void EVP_PKEY_free(EVP_PKEY *key);
+int i2d_X509(X509 *a, unsigned char **out);
+X509 *d2i_X509_bio(BIO *bp, X509 **x);
+
+// X509 store
+typedef struct x509_store_st X509_STORE;
+typedef struct X509_crl_st X509_CRL;
+X509_STORE *X509_STORE_new(void );
+int X509_STORE_add_cert(X509_STORE *ctx, X509 *x);
+ // Use this if we want to load the certs directly from a variables
+int X509_STORE_add_crl(X509_STORE *ctx, X509_CRL *x);
+int X509_STORE_load_locations (X509_STORE *ctx,
+ const char *file, const char *dir);
+void X509_STORE_free(X509_STORE *v);
+
+// X509 store context
+typedef struct x509_store_ctx_st X509_STORE_CTX;
+X509_STORE_CTX *X509_STORE_CTX_new(void);
+int X509_STORE_CTX_init(X509_STORE_CTX *ctx, X509_STORE *store,
+ X509 *x509, void *chain);
+int X509_verify_cert(X509_STORE_CTX *ctx);
+void X509_STORE_CTX_cleanup(X509_STORE_CTX *ctx);
+int X509_STORE_CTX_get_error(X509_STORE_CTX *ctx);
+const char *X509_verify_cert_error_string(long n);
+void X509_STORE_CTX_free(X509_STORE_CTX *ctx);
+
+// EVP Sign/Verify
+typedef struct env_md_ctx_st EVP_MD_CTX;
+typedef struct env_md_st EVP_MD;
+typedef struct evp_pkey_ctx_st EVP_PKEY_CTX;
+const EVP_MD *EVP_get_digestbyname(const char *name);
+
+//OpenSSL 1.0
+EVP_MD_CTX *EVP_MD_CTX_create(void);
+void EVP_MD_CTX_destroy(EVP_MD_CTX *ctx);
+
+//OpenSSL 1.1
+EVP_MD_CTX *EVP_MD_CTX_new(void);
+void EVP_MD_CTX_free(EVP_MD_CTX *ctx);
+
+int EVP_DigestInit_ex(EVP_MD_CTX *ctx, const EVP_MD *type, ENGINE *impl);
+int EVP_DigestSignInit(EVP_MD_CTX *ctx, EVP_PKEY_CTX **pctx,
+ const EVP_MD *type, ENGINE *e, EVP_PKEY *pkey);
+int EVP_DigestUpdate(EVP_MD_CTX *ctx,const void *d,
+ size_t cnt);
+int EVP_DigestSignFinal(EVP_MD_CTX *ctx,
+ unsigned char *sigret, size_t *siglen);
+
+int EVP_DigestVerifyInit(EVP_MD_CTX *ctx, EVP_PKEY_CTX **pctx,
+ const EVP_MD *type, ENGINE *e, EVP_PKEY *pkey);
+int EVP_DigestVerifyFinal(EVP_MD_CTX *ctx,
+ unsigned char *sig, size_t siglen);
+
+// Fingerprints
+int X509_digest(const X509 *data,const EVP_MD *type,
+ unsigned char *md, unsigned int *len);
+
+//EVP encrypt decrypt
+EVP_PKEY_CTX *EVP_PKEY_CTX_new(EVP_PKEY *pkey, ENGINE *e);
+void EVP_PKEY_CTX_free(EVP_PKEY_CTX *ctx);
+
+int EVP_PKEY_CTX_ctrl(EVP_PKEY_CTX *ctx, int keytype, int optype,
+ int cmd, int p1, void *p2);
+
+int EVP_PKEY_size(EVP_PKEY *pkey);
+
+int EVP_PKEY_encrypt_init(EVP_PKEY_CTX *ctx);
+int EVP_PKEY_encrypt(EVP_PKEY_CTX *ctx,
+ unsigned char *out, size_t *outlen,
+ const unsigned char *in, size_t inlen);
+
+int EVP_PKEY_decrypt_init(EVP_PKEY_CTX *ctx);
+int EVP_PKEY_decrypt(EVP_PKEY_CTX *ctx,
+ unsigned char *out, size_t *outlen,
+ const unsigned char *in, size_t inlen);
+
+
+]]
+
+
+local function _err(ret)
+ -- The openssl error queue can have multiple items, print them all separated by ': '
+ local errs = {}
+ local code = _C.ERR_get_error()
+ while code ~= 0 do
+ table.insert(errs, 1, ffi_string(_C.ERR_reason_error_string(code)))
+ code = _C.ERR_get_error()
+ end
+
+ if #errs == 0 then
+ return ret, "Zero error code (null arguments?)"
+ end
+ return ret, table.concat(errs, ": ")
+end
+
+local ctx_new, ctx_free
+local openssl11, e = pcall(function ()
+ local ctx = _C.EVP_MD_CTX_new()
+ _C.EVP_MD_CTX_free(ctx)
+end)
+
+ngx.log(ngx.DEBUG, "openssl11=", openssl11, " err=", e)
+
+if openssl11 then
+ ctx_new = function ()
+ return _C.EVP_MD_CTX_new()
+ end
+ ctx_free = function (ctx)
+ ffi_gc(ctx, _C.EVP_MD_CTX_free)
+ end
+else
+ ctx_new = function ()
+ local ctx = _C.EVP_MD_CTX_create()
+ return ctx
+ end
+ ctx_free = function (ctx)
+ ffi_gc(ctx, _C.EVP_MD_CTX_destroy)
+ end
+end
+
+local function _new_key(self, opts)
+ local bio = _C.BIO_new(_C.BIO_s_mem())
+ ffi_gc(bio, _C.BIO_vfree)
+ if _C.BIO_puts(bio, opts.pem_private_key) < 0 then
+ return _err()
+ end
+
+ local pass
+ if opts.password then
+ local plen = #opts.password
+ pass = ffi_new("unsigned char[?]", plen + 1)
+ ffi_copy(pass, opts.password, plen)
+ end
+
+ local key = nil
+ if self.algo == "RSA" then
+ key = _C.PEM_read_bio_RSAPrivateKey(bio, nil, nil, pass)
+ ffi_gc(key, _C.RSA_free)
+ elseif self.algo == "ECDSA" then
+ key = _C.PEM_read_bio_ECPrivateKey(bio, nil, nil, pass)
+ ffi_gc(key, _C.EC_KEY_free)
+ end
+
+ if not key then
+ return _err()
+ end
+
+ local evp_pkey = _C.EVP_PKEY_new()
+ if evp_pkey == nil then
+ return _err()
+ end
+
+ ffi_gc(evp_pkey, _C.EVP_PKEY_free)
+ if self.algo == "RSA" then
+ if _C.EVP_PKEY_set1_RSA(evp_pkey, key) ~= 1 then
+ return _err()
+ end
+ elseif self.algo == "ECDSA" then
+ if _C.EVP_PKEY_set1_EC_KEY(evp_pkey, key) ~= 1 then
+ return _err()
+ end
+ end
+
+ self.evp_pkey = evp_pkey
+ return self, nil
+end
+
+local function _create_evp_ctx(self, encrypt)
+ self.ctx = _C.EVP_PKEY_CTX_new(self.evp_pkey, nil)
+ if self.ctx == nil then
+ return _err()
+ end
+
+ ffi_gc(self.ctx, _C.EVP_PKEY_CTX_free)
+
+ local md = _C.EVP_get_digestbyname(self.digest_alg)
+ if ffi_cast("void *", md) == nil then
+ return nil, "Unknown message digest"
+ end
+
+ if encrypt then
+ if _C.EVP_PKEY_encrypt_init(self.ctx) <= 0 then
+ return _err()
+ end
+ else
+ if _C.EVP_PKEY_decrypt_init(self.ctx) <= 0 then
+ return _err()
+ end
+ end
+
+ if _C.EVP_PKEY_CTX_ctrl(self.ctx, CONST.EVP_PKEY_RSA, -1, CONST.EVP_PKEY_CTRL_RSA_PADDING,
+ self.padding, nil) <= 0 then
+ return _err()
+ end
+
+ if self.padding == CONST.RSA_PKCS1_OAEP_PADDING then
+ if _C.EVP_PKEY_CTX_ctrl(self.ctx, CONST.EVP_PKEY_RSA, CONST.EVP_PKEY_OP_TYPE_CRYPT,
+ CONST.EVP_PKEY_CTRL_RSA_OAEP_MD, 0, ffi_cast("void *", md)) <= 0 then
+ return _err()
+ end
+ end
+
+ return self.ctx
+end
+
+local RSASigner = {algo="RSA"}
+_M.RSASigner = RSASigner
+
+--- Create a new RSASigner
+-- @param pem_private_key A private key string in PEM format
+-- @param password password for the private key (if required)
+-- @returns RSASigner, err_string
+function RSASigner.new(self, pem_private_key, password)
+ return _new_key (
+ self,
+ {
+ pem_private_key = pem_private_key,
+ password = password
+ }
+ )
+end
+
+
+--- Sign a message
+-- @param message The message to sign
+-- @param digest_name The digest format to use (e.g., "SHA256")
+-- @returns signature, error_string
+function RSASigner.sign(self, message, digest_name)
+ local buf = ffi_new("unsigned char[?]", 1024)
+ local len = ffi_new("size_t[1]", 1024)
+
+ local ctx = ctx_new()
+ if ctx == nil then
+ return _err()
+ end
+ ctx_free(ctx)
+
+ local md = _C.EVP_get_digestbyname(digest_name)
+ if md == nil then
+ return _err()
+ end
+
+ if _C.EVP_DigestInit_ex(ctx, md, nil) ~= 1 then
+ return _err()
+ end
+
+ local ret = _C.EVP_DigestSignInit(ctx, nil, md, nil, self.evp_pkey)
+ if ret ~= 1 then
+ return _err()
+ end
+ if _C.EVP_DigestUpdate(ctx, message, #message) ~= 1 then
+ return _err()
+ end
+ if _C.EVP_DigestSignFinal(ctx, buf, len) ~= 1 then
+ return _err()
+ end
+ return ffi_string(buf, len[0]), nil
+end
+
+
+local ECSigner = {algo="ECDSA"}
+_M.ECSigner = ECSigner
+
+--- Create a new ECSigner
+-- @param pem_private_key A private key string in PEM format
+-- @param password password for the private key (if required)
+-- @returns ECSigner, err_string
+function ECSigner.new(self, pem_private_key, password)
+ return RSASigner.new(self, pem_private_key, password)
+end
+
+--- Sign a message with ECDSA
+-- @param message The message to sign
+-- @param digest_name The digest format to use (e.g., "SHA256")
+-- @returns signature, error_string
+function ECSigner.sign(self, message, digest_name)
+ return RSASigner.sign(self, message, digest_name)
+end
+
+--- Converts a ASN.1 DER signature to RAW r,s
+-- @param signature The ASN.1 DER signature
+-- @returns signature, error_string
+function ECSigner.get_raw_sig(self, signature)
+ if not signature then
+ return nil, "Must pass a signature to convert"
+ end
+ local sig_ptr = ffi_new("unsigned char *[1]")
+ local sig_bin = ffi_new("unsigned char [?]", #signature)
+ ffi_copy(sig_bin, signature, #signature)
+
+ sig_ptr[0] = sig_bin
+ local sig = _C.d2i_ECDSA_SIG(nil, sig_ptr, #signature)
+ ffi_gc(sig, _C.ECDSA_SIG_free)
+
+ local rbytes = math.floor((_C.BN_num_bits(sig.r)+7)/8)
+ local sbytes = math.floor((_C.BN_num_bits(sig.s)+7)/8)
+
+ -- Ensure we copy the BN in a padded form
+ local ec = _C.EVP_PKEY_get0_EC_KEY(self.evp_pkey)
+ local ecgroup = _C.EC_KEY_get0_group(ec)
+
+ local order = _C.BN_new()
+ ffi_gc(order, _C.BN_free)
+
+ -- res is an int, if 0, curve not found
+ local res = _C.EC_GROUP_get_order(ecgroup, order, nil)
+
+ -- BN_num_bytes is a #define, so have to use BN_num_bits
+ local order_size_bytes = math.floor((_C.BN_num_bits(order)+7)/8)
+ local resbuf_len = order_size_bytes *2
+ local resbuf = ffi_new("unsigned char[?]", resbuf_len)
+
+ -- Let's whilst preserving MSB
+ _C.BN_bn2bin(sig.r, resbuf + order_size_bytes - rbytes)
+ _C.BN_bn2bin(sig.s, resbuf + (order_size_bytes*2) - sbytes)
+
+ local raw = ffi_string(resbuf, resbuf_len)
+ return raw, nil
+end
+
+local RSAVerifier = {}
+_M.RSAVerifier = RSAVerifier
+
+
+--- Create a new RSAVerifier
+-- @param key_source An instance of Cert or PublicKey used for verification
+-- @returns RSAVerifier, error_string
+function RSAVerifier.new(self, key_source)
+ if not key_source then
+ return nil, "You must pass in an key_source for a public key"
+ end
+ local evp_public_key = key_source.public_key
+ self.evp_pkey = evp_public_key
+ return self, nil
+end
+
+--- Verify a message is properly signed
+-- @param message The original message
+-- @param the signature to verify
+-- @param digest_name The digest type that was used to sign
+-- @returns bool, error_string
+function RSAVerifier.verify(self, message, sig, digest_name)
+ local md = _C.EVP_get_digestbyname(digest_name)
+ if md == nil then
+ return _err(false)
+ end
+
+ local ctx = ctx_new()
+ if ctx == nil then
+ return _err(false)
+ end
+ ctx_free(ctx)
+
+ if _C.EVP_DigestInit_ex(ctx, md, nil) ~= 1 then
+ return _err(false)
+ end
+
+ local ret = _C.EVP_DigestVerifyInit(ctx, nil, md, nil, self.evp_pkey)
+ if ret ~= 1 then
+ return _err(false)
+ end
+ if _C.EVP_DigestUpdate(ctx, message, #message) ~= 1 then
+ return _err(false)
+ end
+ local sig_bin = ffi_new("unsigned char[?]", #sig)
+ ffi_copy(sig_bin, sig, #sig)
+ if _C.EVP_DigestVerifyFinal(ctx, sig_bin, #sig) == 1 then
+ return true, nil
+ else
+ return false, "Verification failed"
+ end
+end
+
+local ECVerifier = {}
+_M.ECVerifier = ECVerifier
+--- Create a new ECVerifier
+-- @param key_source An instance of Cert or PublicKey used for verification
+-- @returns ECVerifier, error_string
+function ECVerifier.new(self, key_source)
+ return RSAVerifier.new(self, key_source)
+end
+
+--- Verify a message is properly signed
+-- @param message The original message
+-- @param the signature to verify
+-- @param digest_name The digest type that was used to sign
+-- @returns bool, error_string
+function ECVerifier.verify(self, message, sig, digest_name)
+ -- We have to convert the signature back from RAW to ASN1 for verification
+ local der_sig, err = self:get_der_sig(sig)
+ if not der_sig then
+ return nil, err
+ end
+ return RSAVerifier.verify(self, message, der_sig, digest_name)
+end
+
+--- Converts a RAW r,s signature to ASN.1 DER signature (ECDSA)
+-- @param signature The raw signature
+-- @returns signature, error_string
+function ECVerifier.get_der_sig(self, signature)
+ if not signature then
+ return nil, "Must pass a signature to convert"
+ end
+ -- inspired from https://bit.ly/2yZxzxJ
+ local ec = _C.EVP_PKEY_get0_EC_KEY(self.evp_pkey)
+ local ecgroup = _C.EC_KEY_get0_group(ec)
+
+ local order = _C.BN_new()
+ ffi_gc(order, _C.BN_free)
+
+ -- res is an int, if 0, curve not found
+ local res = _C.EC_GROUP_get_order(ecgroup, order, nil)
+
+ -- BN_num_bytes is a #define, so have to use BN_num_bits
+ local order_size_bytes = math.floor((_C.BN_num_bits(order)+7)/8)
+
+ if #signature ~= 2 * order_size_bytes then
+ return nil, "signature length != 2 * order length"
+ end
+
+ local sig_bytes = ffi_new("unsigned char [?]", #signature)
+ ffi_copy(sig_bytes, signature, #signature)
+ local ecdsa = _C.ECDSA_SIG_new()
+ ffi_gc(ecdsa, _C.ECDSA_SIG_free)
+
+ -- Those do not need to be GCed as they are cleared by the ECDSA_SIG_free()
+ local r = _C.BN_bin2bn(sig_bytes, order_size_bytes, nil)
+ local s = _C.BN_bin2bn(sig_bytes + order_size_bytes, order_size_bytes, nil)
+
+ ecdsa.r = r
+ ecdsa.s = s
+
+ -- Gives us the buffer size to allocate
+ local der_len = _C.i2d_ECDSA_SIG(ecdsa, nil)
+
+ local der_sig_ptr = ffi_new("unsigned char *[1]")
+ local der_sig_bin = ffi_new("unsigned char [?]", der_len)
+ der_sig_ptr[0] = der_sig_bin
+ der_len = _C.i2d_ECDSA_SIG(ecdsa, der_sig_ptr)
+
+ local der_str = ffi_string(der_sig_bin, der_len)
+ return der_str, nil
+end
+
+
+local Cert = {}
+_M.Cert = Cert
+
+
+--- Create a new Certificate object
+-- @param payload A PEM or DER format X509 certificate
+-- @returns Cert, error_string
+function Cert.new(self, payload)
+ if not payload then
+ return nil, "Must pass a PEM or binary DER cert"
+ end
+ local bio = _C.BIO_new(_C.BIO_s_mem())
+ ffi_gc(bio, _C.BIO_vfree)
+ local x509
+ if payload:find('-----BEGIN') then
+ if _C.BIO_puts(bio, payload) < 0 then
+ return _err()
+ end
+ x509 = _C.PEM_read_bio_X509(bio, nil, nil, nil)
+ else
+ if _C.BIO_write(bio, payload, #payload) < 0 then
+ return _err()
+ end
+ x509 = _C.d2i_X509_bio(bio, nil)
+ end
+ if x509 == nil then
+ return _err()
+ end
+ ffi_gc(x509, _C.X509_free)
+ self.x509 = x509
+ local public_key, err = self:get_public_key()
+ if not public_key then
+ return nil, err
+ end
+
+ ffi_gc(public_key, _C.EVP_PKEY_free)
+
+ self.public_key = public_key
+ return self, nil
+end
+
+
+--- Retrieve the DER format of the certificate
+-- @returns Binary DER format, error_string
+function Cert.get_der(self)
+ local bufp = ffi_new("unsigned char *[1]")
+ local len = _C.i2d_X509(self.x509, bufp)
+ if len < 0 then
+ return _err()
+ end
+ local der = ffi_string(bufp[0], len)
+ return der, nil
+end
+
+--- Retrieve the cert fingerprint
+-- @param digest_name the Type of digest to use (e.g., "SHA256")
+-- @returns fingerprint_string, error_string
+function Cert.get_fingerprint(self, digest_name)
+ local md = _C.EVP_get_digestbyname(digest_name)
+ if md == nil then
+ return _err()
+ end
+ local buf = ffi_new("unsigned char[?]", 32)
+ local len = ffi_new("unsigned int[1]", 32)
+ if _C.X509_digest(self.x509, md, buf, len) ~= 1 then
+ return _err()
+ end
+ local raw = ffi_string(buf, len[0])
+ local t = {}
+ raw:gsub('.', function (c) table.insert(t, string.format('%02X', string.byte(c))) end)
+ return table.concat(t, ":"), nil
+end
+
+--- Retrieve the public key from the CERT
+-- @returns An OpenSSL EVP PKEY object representing the public key, error_string
+function Cert.get_public_key(self)
+ local evp_pkey = _C.X509_get_pubkey(self.x509)
+ if evp_pkey == nil then
+ return _err()
+ end
+
+ return evp_pkey, nil
+end
+
+--- Verify the Certificate is trusted
+-- @param trusted_cert_file File path to a list of PEM encoded trusted certificates
+-- @return bool, error_string
+function Cert.verify_trust(self, trusted_cert_file)
+ local store = _C.X509_STORE_new()
+ if store == nil then
+ return _err(false)
+ end
+ ffi_gc(store, _C.X509_STORE_free)
+ if _C.X509_STORE_load_locations(store, trusted_cert_file, nil) ~=1 then
+ return _err(false)
+ end
+
+ local ctx = _C.X509_STORE_CTX_new()
+ if store == nil then
+ return _err(false)
+ end
+ ffi_gc(ctx, _C.X509_STORE_CTX_free)
+ if _C.X509_STORE_CTX_init(ctx, store, self.x509, nil) ~= 1 then
+ return _err(false)
+ end
+
+ if _C.X509_verify_cert(ctx) ~= 1 then
+ local code = _C.X509_STORE_CTX_get_error(ctx)
+ local msg = ffi_string(_C.X509_verify_cert_error_string(code))
+ _C.X509_STORE_CTX_cleanup(ctx)
+ return false, msg
+ end
+ _C.X509_STORE_CTX_cleanup(ctx)
+ return true, nil
+
+end
+
+local PublicKey = {}
+_M.PublicKey = PublicKey
+
+--- Create a new PublicKey object
+--
+-- If a PEM fornatted key is provided, the key must start with
+--
+-- ----- BEGIN PUBLIC KEY -----
+--
+-- @param payload A PEM or DER format public key file
+-- @return PublicKey, error_string
+function PublicKey.new(self, payload)
+ if not payload then
+ return nil, "Must pass a PEM or binary DER public key"
+ end
+ local bio = _C.BIO_new(_C.BIO_s_mem())
+ ffi_gc(bio, _C.BIO_vfree)
+ local pkey
+ if payload:find('-----BEGIN') then
+ if _C.BIO_puts(bio, payload) < 0 then
+ return _err()
+ end
+ pkey = _C.PEM_read_bio_PUBKEY(bio, nil, nil, nil)
+ else
+ if _C.BIO_write(bio, payload, #payload) < 0 then
+ return _err()
+ end
+ pkey = _C.d2i_PUBKEY_bio(bio, nil)
+ end
+ if pkey == nil then
+ return _err()
+ end
+ ffi_gc(pkey, _C.EVP_PKEY_free)
+ self.public_key = pkey
+ return self, nil
+end
+
+local RSAEncryptor= {}
+_M.RSAEncryptor = RSAEncryptor
+
+--- Create a new RSAEncryptor
+-- @param key_source An instance of Cert or PublicKey used for verification
+-- @param padding padding type to use
+-- @param digest_alg digest algorithm to use
+-- @returns RSAEncryptor, err_string
+function RSAEncryptor.new(self, key_source, padding, digest_alg)
+ if not key_source then
+ return nil, "You must pass in an key_source for a public key"
+ end
+ local evp_public_key = key_source.public_key
+ self.evp_pkey = evp_public_key
+ self.padding = padding or CONST.RSA_PKCS1_OAEP_PADDING
+ self.digest_alg = digest_alg or CONST.SHA256_DIGEST
+ return self, nil
+end
+
+
+
+--- Encrypts the payload
+-- @param payload plain text payload
+-- @returns encrypted payload, error_string
+function RSAEncryptor.encrypt(self, payload)
+
+ local ctx, err_str = _create_evp_ctx(self, true)
+
+ if not ctx then
+ return nil, err_str
+ end
+ local len = ffi_new("size_t [1]")
+ if _C.EVP_PKEY_encrypt(ctx, nil, len, payload, #payload) <= 0 then
+ return _err()
+ end
+ local buf = ffi_new("unsigned char[?]", len[0])
+ if _C.EVP_PKEY_encrypt(ctx, buf, len, payload, #payload) <= 0 then
+ return _err()
+ end
+
+ return ffi_string(buf, len[0])
+
+end
+
+
+local RSADecryptor= {algo="RSA"}
+_M.RSADecryptor = RSADecryptor
+
+--- Create a new RSADecryptor
+-- @param pem_private_key A private key string in PEM format
+-- @param password password for the private key (if required)
+-- @param padding padding type to use
+-- @param digest_alg digest algorithm to use
+-- @returns RSADecryptor, error_string
+function RSADecryptor.new(self, pem_private_key, password, padding, digest_alg)
+ self.padding = padding or CONST.RSA_PKCS1_OAEP_PADDING
+ self.digest_alg = digest_alg or CONST.SHA256_DIGEST
+ return _new_key (
+ self,
+ {
+ pem_private_key = pem_private_key,
+ password = password
+ }
+ )
+end
+
+--- Decrypts the cypher text
+-- @param cypher_text encrypted payload
+-- @param padding rsa pading mode to use, Defaults to RSA_PKCS1_PADDING
+function RSADecryptor.decrypt(self, cypher_text)
+
+ local ctx, err_code, err_str = _create_evp_ctx(self, false)
+
+ if not ctx then
+ return nil, err_code, err_str
+ end
+
+ local len = ffi_new("size_t [1]")
+ if _C.EVP_PKEY_decrypt(ctx, nil, len, cypher_text, #cypher_text) <= 0 then
+ return _err()
+ end
+
+ local buf = ffi_new("unsigned char[?]", len[0])
+ if _C.EVP_PKEY_decrypt(ctx, buf, len, cypher_text, #cypher_text) <= 0 then
+ return _err()
+ end
+
+ return ffi_string(buf, len[0])
+
+end
+
+return _M
diff --git a/server/resty/hmac.lua b/server/resty/hmac.lua
new file mode 100644
index 0000000..8d94a8b
--- /dev/null
+++ b/server/resty/hmac.lua
@@ -0,0 +1,167 @@
+
+local str_util = require "resty.string"
+local to_hex = str_util.to_hex
+local ffi = require "ffi"
+local ffi_new = ffi.new
+local ffi_str = ffi.string
+local ffi_gc = ffi.gc
+local ffi_typeof = ffi.typeof
+local C = ffi.C
+local setmetatable = setmetatable
+local error = error
+
+
+local _M = { _VERSION = '0.04' }
+
+local mt = { __index = _M }
+
+
+ffi.cdef[[
+typedef struct engine_st ENGINE;
+typedef struct evp_pkey_ctx_st EVP_PKEY_CTX;
+typedef struct evp_md_ctx_st EVP_MD_CTX;
+typedef struct evp_md_st EVP_MD;
+typedef struct hmac_ctx_st HMAC_CTX;
+
+//OpenSSL 1.0
+void HMAC_CTX_init(HMAC_CTX *ctx);
+void HMAC_CTX_cleanup(HMAC_CTX *ctx);
+
+//OpenSSL 1.1
+HMAC_CTX *HMAC_CTX_new(void);
+void HMAC_CTX_free(HMAC_CTX *ctx);
+
+int HMAC_Init_ex(HMAC_CTX *ctx, const void *key, int len, const EVP_MD *md, ENGINE *impl);
+int HMAC_Update(HMAC_CTX *ctx, const unsigned char *data, size_t len);
+int HMAC_Final(HMAC_CTX *ctx, unsigned char *md, unsigned int *len);
+
+const EVP_MD *EVP_md5(void);
+const EVP_MD *EVP_sha1(void);
+const EVP_MD *EVP_sha256(void);
+const EVP_MD *EVP_sha512(void);
+]]
+
+local buf = ffi_new("unsigned char[64]")
+local res_len = ffi_new("unsigned int[1]")
+local hashes = {
+ MD5 = C.EVP_md5(),
+ SHA1 = C.EVP_sha1(),
+ SHA256 = C.EVP_sha256(),
+ SHA512 = C.EVP_sha512()
+}
+
+local ctx_new, ctx_free
+local openssl11, e = pcall(function ()
+ local ctx = C.HMAC_CTX_new()
+ C.HMAC_CTX_free(ctx)
+end)
+if openssl11 then
+ ctx_new = function ()
+ return C.HMAC_CTX_new()
+ end
+ ctx_free = function (ctx)
+ C.HMAC_CTX_free(ctx)
+ end
+else
+ ffi.cdef [[
+ struct evp_md_ctx_st
+ {
+ const EVP_MD *digest;
+ ENGINE *engine;
+ unsigned long flags;
+ void *md_data;
+ EVP_PKEY_CTX *pctx;
+ int (*update)(EVP_MD_CTX *ctx,const void *data,size_t count);
+ };
+
+ struct evp_md_st
+ {
+ int type;
+ int pkey_type;
+ int md_size;
+ unsigned long flags;
+ int (*init)(EVP_MD_CTX *ctx);
+ int (*update)(EVP_MD_CTX *ctx,const void *data,size_t count);
+ int (*final)(EVP_MD_CTX *ctx,unsigned char *md);
+ int (*copy)(EVP_MD_CTX *to,const EVP_MD_CTX *from);
+ int (*cleanup)(EVP_MD_CTX *ctx);
+
+ int (*sign)(int type, const unsigned char *m, unsigned int m_length, unsigned char *sigret, unsigned int *siglen, void *key);
+ int (*verify)(int type, const unsigned char *m, unsigned int m_length, const unsigned char *sigbuf, unsigned int siglen, void *key);
+ int required_pkey_type[5];
+ int block_size;
+ int ctx_size;
+ int (*md_ctrl)(EVP_MD_CTX *ctx, int cmd, int p1, void *p2);
+ };
+
+ struct hmac_ctx_st
+ {
+ const EVP_MD *md;
+ EVP_MD_CTX md_ctx;
+ EVP_MD_CTX i_ctx;
+ EVP_MD_CTX o_ctx;
+ unsigned int key_length;
+ unsigned char key[128];
+ };
+ ]]
+
+ local ctx_ptr_type = ffi_typeof("HMAC_CTX[1]")
+
+ ctx_new = function ()
+ local ctx = ffi_new(ctx_ptr_type)
+ C.HMAC_CTX_init(ctx)
+ return ctx
+ end
+ ctx_free = function (ctx)
+ C.HMAC_CTX_cleanup(ctx)
+ end
+end
+
+
+_M.ALGOS = hashes
+
+
+function _M.new(self, key, hash_algo)
+ local ctx = ctx_new()
+
+ local _hash_algo = hash_algo or hashes.MD5
+
+ if C.HMAC_Init_ex(ctx, key, #key, _hash_algo, nil) == 0 then
+ return nil
+ end
+
+ ffi_gc(ctx, ctx_free)
+
+ return setmetatable({ _ctx = ctx }, mt)
+end
+
+
+function _M.update(self, s)
+ return C.HMAC_Update(self._ctx, s, #s) == 1
+end
+
+
+function _M.final(self, s, hex_output)
+
+ if s ~= nil then
+ if C.HMAC_Update(self._ctx, s, #s) == 0 then
+ return nil
+ end
+ end
+
+ if C.HMAC_Final(self._ctx, buf, res_len) == 1 then
+ if hex_output == true then
+ return to_hex(ffi_str(buf, res_len[0]))
+ end
+ return ffi_str(buf, res_len[0])
+ end
+
+ return nil
+end
+
+
+function _M.reset(self)
+ return C.HMAC_Init_ex(self._ctx, nil, 0, nil, nil) == 1
+end
+
+return _M
diff --git a/server/resty/http.lua b/server/resty/http.lua
new file mode 100644
index 0000000..70c3bee
--- /dev/null
+++ b/server/resty/http.lua
@@ -0,0 +1,1178 @@
+local http_headers = require "resty.http_headers"
+
+local ngx = ngx
+local ngx_socket_tcp = ngx.socket.tcp
+local ngx_req = ngx.req
+local ngx_req_socket = ngx_req.socket
+local ngx_req_get_headers = ngx_req.get_headers
+local ngx_req_get_method = ngx_req.get_method
+local str_lower = string.lower
+local str_upper = string.upper
+local str_find = string.find
+local str_sub = string.sub
+local tbl_concat = table.concat
+local tbl_insert = table.insert
+local ngx_encode_args = ngx.encode_args
+local ngx_re_match = ngx.re.match
+local ngx_re_gmatch = ngx.re.gmatch
+local ngx_re_sub = ngx.re.sub
+local ngx_re_gsub = ngx.re.gsub
+local ngx_re_find = ngx.re.find
+local ngx_log = ngx.log
+local ngx_DEBUG = ngx.DEBUG
+local ngx_ERR = ngx.ERR
+local ngx_var = ngx.var
+local ngx_print = ngx.print
+local ngx_header = ngx.header
+local co_yield = coroutine.yield
+local co_create = coroutine.create
+local co_status = coroutine.status
+local co_resume = coroutine.resume
+local setmetatable = setmetatable
+local tonumber = tonumber
+local tostring = tostring
+local unpack = unpack
+local rawget = rawget
+local select = select
+local ipairs = ipairs
+local pairs = pairs
+local pcall = pcall
+local type = type
+
+
+-- http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.5.1
+local HOP_BY_HOP_HEADERS = {
+ ["connection"] = true,
+ ["keep-alive"] = true,
+ ["proxy-authenticate"] = true,
+ ["proxy-authorization"] = true,
+ ["te"] = true,
+ ["trailers"] = true,
+ ["transfer-encoding"] = true,
+ ["upgrade"] = true,
+ ["content-length"] = true, -- Not strictly hop-by-hop, but Nginx will deal
+ -- with this (may send chunked for example).
+}
+
+
+local EXPECTING_BODY = {
+ POST = true,
+ PUT = true,
+ PATCH = true,
+}
+
+
+-- Reimplemented coroutine.wrap, returning "nil, err" if the coroutine cannot
+-- be resumed. This protects user code from infinite loops when doing things like
+-- repeat
+-- local chunk, err = res.body_reader()
+-- if chunk then -- <-- This could be a string msg in the core wrap function.
+-- ...
+-- end
+-- until not chunk
+local co_wrap = function(func)
+ local co = co_create(func)
+ if not co then
+ return nil, "could not create coroutine"
+ else
+ return function(...)
+ if co_status(co) == "suspended" then
+ return select(2, co_resume(co, ...))
+ else
+ return nil, "can't resume a " .. co_status(co) .. " coroutine"
+ end
+ end
+ end
+end
+
+
+-- Returns a new table, recursively copied from the one given.
+--
+-- @param table table to be copied
+-- @return table
+local function tbl_copy(orig)
+ local orig_type = type(orig)
+ local copy
+ if orig_type == "table" then
+ copy = {}
+ for orig_key, orig_value in next, orig, nil do
+ copy[tbl_copy(orig_key)] = tbl_copy(orig_value)
+ end
+ else -- number, string, boolean, etc
+ copy = orig
+ end
+ return copy
+end
+
+
+local _M = {
+ _VERSION = '0.17.0-beta.1',
+}
+_M._USER_AGENT = "lua-resty-http/" .. _M._VERSION .. " (Lua) ngx_lua/" .. ngx.config.ngx_lua_version
+
+local mt = { __index = _M }
+
+
+local HTTP = {
+ [1.0] = " HTTP/1.0\r\n",
+ [1.1] = " HTTP/1.1\r\n",
+}
+
+
+local DEFAULT_PARAMS = {
+ method = "GET",
+ path = "/",
+ version = 1.1,
+}
+
+
+local DEBUG = false
+
+
+function _M.new(_)
+ local sock, err = ngx_socket_tcp()
+ if not sock then
+ return nil, err
+ end
+ return setmetatable({ sock = sock, keepalive = true }, mt)
+end
+
+
+function _M.debug(d)
+ DEBUG = (d == true)
+end
+
+
+function _M.set_timeout(self, timeout)
+ local sock = self.sock
+ if not sock then
+ return nil, "not initialized"
+ end
+
+ return sock:settimeout(timeout)
+end
+
+
+function _M.set_timeouts(self, connect_timeout, send_timeout, read_timeout)
+ local sock = self.sock
+ if not sock then
+ return nil, "not initialized"
+ end
+
+ return sock:settimeouts(connect_timeout, send_timeout, read_timeout)
+end
+
+do
+ local aio_connect = require "resty.http_connect"
+ -- Function signatures to support:
+ -- ok, err, ssl_session = httpc:connect(options_table)
+ -- ok, err = httpc:connect(host, port, options_table?)
+ -- ok, err = httpc:connect("unix:/path/to/unix.sock", options_table?)
+ function _M.connect(self, options, ...)
+ if type(options) == "table" then
+ -- all-in-one interface
+ return aio_connect(self, options)
+ else
+ -- backward compatible
+ return self:tcp_only_connect(options, ...)
+ end
+ end
+end
+
+function _M.tcp_only_connect(self, ...)
+ ngx_log(ngx_DEBUG, "Use of deprecated `connect` method signature")
+
+ local sock = self.sock
+ if not sock then
+ return nil, "not initialized"
+ end
+
+ self.host = select(1, ...)
+ self.port = select(2, ...)
+
+ -- If port is not a number, this is likely a unix domain socket connection.
+ if type(self.port) ~= "number" then
+ self.port = nil
+ end
+
+ self.keepalive = true
+ self.ssl = false
+
+ return sock:connect(...)
+end
+
+
+function _M.set_keepalive(self, ...)
+ local sock = self.sock
+ if not sock then
+ return nil, "not initialized"
+ end
+
+ if self.keepalive == true then
+ return sock:setkeepalive(...)
+ else
+ -- The server said we must close the connection, so we cannot setkeepalive.
+ -- If close() succeeds we return 2 instead of 1, to differentiate between
+ -- a normal setkeepalive() failure and an intentional close().
+ local res, err = sock:close()
+ if res then
+ return 2, "connection must be closed"
+ else
+ return res, err
+ end
+ end
+end
+
+
+function _M.get_reused_times(self)
+ local sock = self.sock
+ if not sock then
+ return nil, "not initialized"
+ end
+
+ return sock:getreusedtimes()
+end
+
+
+function _M.close(self)
+ local sock = self.sock
+ if not sock then
+ return nil, "not initialized"
+ end
+
+ return sock:close()
+end
+
+
+local function _should_receive_body(method, code)
+ if method == "HEAD" then return nil end
+ if code == 204 or code == 304 then return nil end
+ if code >= 100 and code < 200 then return nil end
+ return true
+end
+
+
+function _M.parse_uri(_, uri, query_in_path)
+ if query_in_path == nil then query_in_path = true end
+
+ local m, err = ngx_re_match(
+ uri,
+ [[^(?:(http[s]?):)?//((?:[^\[\]:/\?]+)|(?:\[.+\]))(?::(\d+))?([^\?]*)\??(.*)]],
+ "jo"
+ )
+
+ if not m then
+ if err then
+ return nil, "failed to match the uri: " .. uri .. ", " .. err
+ end
+
+ return nil, "bad uri: " .. uri
+ else
+ -- If the URI is schemaless (i.e. //example.com) try to use our current
+ -- request scheme.
+ if not m[1] then
+ -- Schema-less URIs can occur in client side code, implying "inherit
+ -- the schema from the current request". We support it for a fairly
+ -- specific case; if for example you are using the ESI parser in
+ -- ledge (https://github.com/ledgetech/ledge) to perform in-flight
+ -- sub requests on the edge based on instructions found in markup,
+ -- those URIs may also be schemaless with the intention that the
+ -- subrequest would inherit the schema just like JavaScript would.
+ local scheme = ngx_var.scheme
+ if scheme == "http" or scheme == "https" then
+ m[1] = scheme
+ else
+ return nil, "schemaless URIs require a request context: " .. uri
+ end
+ end
+
+ if m[3] then
+ m[3] = tonumber(m[3])
+ else
+ if m[1] == "https" then
+ m[3] = 443
+ else
+ m[3] = 80
+ end
+ end
+ if not m[4] or "" == m[4] then m[4] = "/" end
+
+ if query_in_path and m[5] and m[5] ~= "" then
+ m[4] = m[4] .. "?" .. m[5]
+ m[5] = nil
+ end
+
+ return m, nil
+ end
+end
+
+
+local function _format_request(self, params)
+ local version = params.version
+ local headers = params.headers or {}
+
+ local query = params.query or ""
+ if type(query) == "table" then
+ query = "?" .. ngx_encode_args(query)
+ elseif query ~= "" and str_sub(query, 1, 1) ~= "?" then
+ query = "?" .. query
+ end
+
+ -- Initialize request
+ local req = {
+ str_upper(params.method),
+ " ",
+ self.path_prefix or "",
+ params.path,
+ query,
+ HTTP[version],
+ -- Pre-allocate slots for minimum headers and carriage return.
+ true,
+ true,
+ true,
+ }
+ local c = 7 -- req table index it's faster to do this inline vs table.insert
+
+ -- Append headers
+ for key, values in pairs(headers) do
+ key = tostring(key)
+
+ if type(values) == "table" then
+ for _, value in pairs(values) do
+ req[c] = key .. ": " .. tostring(value) .. "\r\n"
+ c = c + 1
+ end
+
+ else
+ req[c] = key .. ": " .. tostring(values) .. "\r\n"
+ c = c + 1
+ end
+ end
+
+ -- Close headers
+ req[c] = "\r\n"
+
+ return tbl_concat(req)
+end
+
+
+local function _receive_status(sock)
+ local line, err = sock:receive("*l")
+ if not line then
+ return nil, nil, nil, err
+ end
+
+ local version = tonumber(str_sub(line, 6, 8))
+ if not version then
+ return nil, nil, nil,
+ "couldn't parse HTTP version from response status line: " .. line
+ end
+
+ local status = tonumber(str_sub(line, 10, 12))
+ if not status then
+ return nil, nil, nil,
+ "couldn't parse status code from response status line: " .. line
+ end
+
+ local reason = str_sub(line, 14)
+
+ return status, version, reason
+end
+
+
+local function _receive_headers(sock)
+ local headers = http_headers.new()
+
+ repeat
+ local line, err = sock:receive("*l")
+ if not line then
+ return nil, err
+ end
+
+ local m, err = ngx_re_match(line, "([^:\\s]+):\\s*(.*)", "jo")
+ if err then ngx_log(ngx_ERR, err) end
+
+ if not m then
+ break
+ end
+
+ local key = m[1]
+ local val = m[2]
+ if headers[key] then
+ if type(headers[key]) ~= "table" then
+ headers[key] = { headers[key] }
+ end
+ tbl_insert(headers[key], tostring(val))
+ else
+ headers[key] = tostring(val)
+ end
+ until ngx_re_find(line, "^\\s*$", "jo")
+
+ return headers, nil
+end
+
+
+local function transfer_encoding_is_chunked(headers)
+ local te = headers["Transfer-Encoding"]
+ if not te then
+ return false
+ end
+
+ -- Handle duplicate headers
+ -- This shouldn't happen but can in the real world
+ if type(te) ~= "string" then
+ te = tbl_concat(te, ",")
+ end
+
+ return str_find(str_lower(te), "chunked", 1, true) ~= nil
+end
+_M.transfer_encoding_is_chunked = transfer_encoding_is_chunked
+
+
+local function _chunked_body_reader(sock, default_chunk_size)
+ return co_wrap(function(max_chunk_size)
+ local remaining = 0
+ local length
+ max_chunk_size = max_chunk_size or default_chunk_size
+
+ repeat
+ -- If we still have data on this chunk
+ if max_chunk_size and remaining > 0 then
+
+ if remaining > max_chunk_size then
+ -- Consume up to max_chunk_size
+ length = max_chunk_size
+ remaining = remaining - max_chunk_size
+ else
+ -- Consume all remaining
+ length = remaining
+ remaining = 0
+ end
+ else -- This is a fresh chunk
+
+ -- Receive the chunk size
+ local str, err = sock:receive("*l")
+ if not str then
+ co_yield(nil, err)
+ end
+
+ length = tonumber(str, 16)
+
+ if not length then
+ co_yield(nil, "unable to read chunksize")
+ end
+
+ if max_chunk_size and length > max_chunk_size then
+ -- Consume up to max_chunk_size
+ remaining = length - max_chunk_size
+ length = max_chunk_size
+ end
+ end
+
+ if length > 0 then
+ local str, err = sock:receive(length)
+ if not str then
+ co_yield(nil, err)
+ end
+
+ max_chunk_size = co_yield(str) or default_chunk_size
+
+ -- If we're finished with this chunk, read the carriage return.
+ if remaining == 0 then
+ sock:receive(2) -- read \r\n
+ end
+ else
+ -- Read the last (zero length) chunk's carriage return
+ sock:receive(2) -- read \r\n
+ end
+
+ until length == 0
+ end)
+end
+
+
+local function _body_reader(sock, content_length, default_chunk_size)
+ return co_wrap(function(max_chunk_size)
+ max_chunk_size = max_chunk_size or default_chunk_size
+
+ if not content_length and max_chunk_size then
+ -- We have no length, but wish to stream.
+ -- HTTP 1.0 with no length will close connection, so read chunks to the end.
+ repeat
+ local str, err, partial = sock:receive(max_chunk_size)
+ if not str and err == "closed" then
+ co_yield(partial, err)
+ end
+
+ max_chunk_size = tonumber(co_yield(str) or default_chunk_size)
+ if max_chunk_size and max_chunk_size < 0 then max_chunk_size = nil end
+
+ if not max_chunk_size then
+ ngx_log(ngx_ERR, "Buffer size not specified, bailing")
+ break
+ end
+ until not str
+
+ elseif not content_length then
+ -- We have no length but don't wish to stream.
+ -- HTTP 1.0 with no length will close connection, so read to the end.
+ co_yield(sock:receive("*a"))
+
+ elseif not max_chunk_size then
+ -- We have a length and potentially keep-alive, but want everything.
+ co_yield(sock:receive(content_length))
+
+ else
+ -- We have a length and potentially a keep-alive, and wish to stream
+ -- the response.
+ local received = 0
+ repeat
+ local length = max_chunk_size
+ if received + length > content_length then
+ length = content_length - received
+ end
+
+ if length > 0 then
+ local str, err = sock:receive(length)
+ if not str then
+ co_yield(nil, err)
+ end
+ received = received + length
+
+ max_chunk_size = tonumber(co_yield(str) or default_chunk_size)
+ if max_chunk_size and max_chunk_size < 0 then max_chunk_size = nil end
+
+ if not max_chunk_size then
+ ngx_log(ngx_ERR, "Buffer size not specified, bailing")
+ break
+ end
+ end
+
+ until length == 0
+ end
+ end)
+end
+
+
+local function _no_body_reader()
+ return nil
+end
+
+
+local function _read_body(res)
+ local reader = res.body_reader
+
+ if not reader then
+ -- Most likely HEAD or 304 etc.
+ return nil, "no body to be read"
+ end
+
+ local chunks = {}
+ local c = 1
+
+ local chunk, err
+ repeat
+ chunk, err = reader()
+
+ if err then
+ return nil, err, tbl_concat(chunks) -- Return any data so far.
+ end
+ if chunk then
+ chunks[c] = chunk
+ c = c + 1
+ end
+ until not chunk
+
+ return tbl_concat(chunks)
+end
+
+
+local function _trailer_reader(sock)
+ return co_wrap(function()
+ co_yield(_receive_headers(sock))
+ end)
+end
+
+
+local function _read_trailers(res)
+ local reader = res.trailer_reader
+ if not reader then
+ return nil, "no trailers"
+ end
+
+ local trailers = reader()
+ setmetatable(res.headers, { __index = trailers })
+end
+
+
+local function _send_body(sock, body)
+ if type(body) == "function" then
+ repeat
+ local chunk, err, partial = body()
+
+ if chunk then
+ local ok, err = sock:send(chunk)
+
+ if not ok then
+ return nil, err
+ end
+ elseif err ~= nil then
+ return nil, err, partial
+ end
+
+ until chunk == nil
+ elseif body ~= nil then
+ local bytes, err = sock:send(body)
+
+ if not bytes then
+ return nil, err
+ end
+ end
+ return true, nil
+end
+
+
+local function _handle_continue(sock, body)
+ local status, version, reason, err = _receive_status(sock) --luacheck: no unused
+ if not status then
+ return nil, nil, err
+ end
+
+ -- Only send body if we receive a 100 Continue
+ if status == 100 then
+ local ok, err = sock:receive("*l") -- Read carriage return
+ if not ok then
+ return nil, nil, err
+ end
+ _send_body(sock, body)
+ end
+ return status, version, err
+end
+
+
+function _M.send_request(self, params)
+ -- Apply defaults
+ setmetatable(params, { __index = DEFAULT_PARAMS })
+
+ local sock = self.sock
+ local body = params.body
+ local headers = http_headers.new()
+
+ -- We assign one-by-one so that the metatable can handle case insensitivity
+ -- for us. You can blame the spec for this inefficiency.
+ local params_headers = params.headers or {}
+ for k, v in pairs(params_headers) do
+ headers[k] = v
+ end
+
+ if not headers["Proxy-Authorization"] then
+ -- TODO: next major, change this to always override the provided
+ -- header. Can't do that yet because it would be breaking.
+ -- The connect method uses self.http_proxy_auth in the poolname so
+ -- that should be leading.
+ headers["Proxy-Authorization"] = self.http_proxy_auth
+ end
+
+ -- Ensure we have appropriate message length or encoding.
+ do
+ local is_chunked = transfer_encoding_is_chunked(headers)
+
+ if is_chunked then
+ -- If we have both Transfer-Encoding and Content-Length we MUST
+ -- drop the Content-Length, to help prevent request smuggling.
+ -- https://tools.ietf.org/html/rfc7230#section-3.3.3
+ headers["Content-Length"] = nil
+
+ elseif not headers["Content-Length"] then
+ -- A length was not given, try to calculate one.
+
+ local body_type = type(body)
+
+ if body_type == "function" then
+ return nil, "Request body is a function but a length or chunked encoding is not specified"
+
+ elseif body_type == "table" then
+ local length = 0
+ for _, v in ipairs(body) do
+ length = length + #tostring(v)
+ end
+ headers["Content-Length"] = length
+
+ elseif body == nil and EXPECTING_BODY[str_upper(params.method)] then
+ headers["Content-Length"] = 0
+
+ elseif body ~= nil then
+ headers["Content-Length"] = #tostring(body)
+ end
+ end
+ end
+
+ if not headers["Host"] then
+ if (str_sub(self.host, 1, 5) == "unix:") then
+ return nil, "Unable to generate a useful Host header for a unix domain socket. Please provide one."
+ end
+ -- If we have a port (i.e. not connected to a unix domain socket), and this
+ -- port is non-standard, append it to the Host header.
+ if self.port then
+ if self.ssl and self.port ~= 443 then
+ headers["Host"] = self.host .. ":" .. self.port
+ elseif not self.ssl and self.port ~= 80 then
+ headers["Host"] = self.host .. ":" .. self.port
+ else
+ headers["Host"] = self.host
+ end
+ else
+ headers["Host"] = self.host
+ end
+ end
+ if not headers["User-Agent"] then
+ headers["User-Agent"] = _M._USER_AGENT
+ end
+ if params.version == 1.0 and not headers["Connection"] then
+ headers["Connection"] = "Keep-Alive"
+ end
+
+ params.headers = headers
+
+ -- Format and send request
+ local req = _format_request(self, params)
+ if DEBUG then ngx_log(ngx_DEBUG, "\n", req) end
+ local bytes, err = sock:send(req)
+
+ if not bytes then
+ return nil, err
+ end
+
+ -- Send the request body, unless we expect: continue, in which case
+ -- we handle this as part of reading the response.
+ if headers["Expect"] ~= "100-continue" then
+ local ok, err, partial = _send_body(sock, body)
+ if not ok then
+ return nil, err, partial
+ end
+ end
+
+ return true
+end
+
+
+function _M.read_response(self, params)
+ local sock = self.sock
+
+ local status, version, reason, err
+
+ -- If we expect: continue, we need to handle this, sending the body if allowed.
+ -- If we don't get 100 back, then status is the actual status.
+ if params.headers["Expect"] == "100-continue" then
+ local _status, _version, _err = _handle_continue(sock, params.body)
+ if not _status then
+ return nil, _err
+ elseif _status ~= 100 then
+ status, version, err = _status, _version, _err -- luacheck: no unused
+ end
+ end
+
+ -- Just read the status as normal.
+ if not status then
+ status, version, reason, err = _receive_status(sock)
+ if not status then
+ return nil, err
+ end
+ end
+
+
+ local res_headers, err = _receive_headers(sock)
+ if not res_headers then
+ return nil, err
+ end
+
+ -- keepalive is true by default. Determine if this is correct or not.
+ local ok, connection = pcall(str_lower, res_headers["Connection"])
+ if ok then
+ if (version == 1.1 and str_find(connection, "close", 1, true)) or
+ (version == 1.0 and not str_find(connection, "keep-alive", 1, true)) then
+ self.keepalive = false
+ end
+ else
+ -- no connection header
+ if version == 1.0 then
+ self.keepalive = false
+ end
+ end
+
+ local body_reader = _no_body_reader
+ local trailer_reader, err
+ local has_body = false
+
+ -- Receive the body_reader
+ if _should_receive_body(params.method, status) then
+ has_body = true
+
+ if version == 1.1 and transfer_encoding_is_chunked(res_headers) then
+ body_reader, err = _chunked_body_reader(sock)
+ else
+ local ok, length = pcall(tonumber, res_headers["Content-Length"])
+ if not ok then
+ -- No content-length header, read until connection is closed by server
+ length = nil
+ end
+
+ body_reader, err = _body_reader(sock, length)
+ end
+ end
+
+ if res_headers["Trailer"] then
+ trailer_reader, err = _trailer_reader(sock)
+ end
+
+ if err then
+ return nil, err
+ else
+ return {
+ status = status,
+ reason = reason,
+ headers = res_headers,
+ has_body = has_body,
+ body_reader = body_reader,
+ read_body = _read_body,
+ trailer_reader = trailer_reader,
+ read_trailers = _read_trailers,
+ }
+ end
+end
+
+
+function _M.request(self, params)
+ params = tbl_copy(params) -- Take by value
+ local res, err = self:send_request(params)
+ if not res then
+ return res, err
+ else
+ return self:read_response(params)
+ end
+end
+
+
+function _M.request_pipeline(self, requests)
+ requests = tbl_copy(requests) -- Take by value
+
+ for _, params in ipairs(requests) do
+ if params.headers and params.headers["Expect"] == "100-continue" then
+ return nil, "Cannot pipeline request specifying Expect: 100-continue"
+ end
+
+ local res, err = self:send_request(params)
+ if not res then
+ return res, err
+ end
+ end
+
+ local responses = {}
+ for i, params in ipairs(requests) do
+ responses[i] = setmetatable({
+ params = params,
+ response_read = false,
+ }, {
+ -- Read each actual response lazily, at the point the user tries
+ -- to access any of the fields.
+ __index = function(t, k)
+ local res, err
+ if t.response_read == false then
+ res, err = _M.read_response(self, t.params)
+ t.response_read = true
+
+ if not res then
+ ngx_log(ngx_ERR, err)
+ else
+ for rk, rv in pairs(res) do
+ t[rk] = rv
+ end
+ end
+ end
+ return rawget(t, k)
+ end,
+ })
+ end
+ return responses
+end
+
+
+function _M.request_uri(self, uri, params)
+ params = tbl_copy(params or {}) -- Take by value
+ if self.proxy_opts then
+ params.proxy_opts = tbl_copy(self.proxy_opts or {})
+ end
+
+ do
+ local parsed_uri, err = self:parse_uri(uri, false)
+ if not parsed_uri then
+ return nil, err
+ end
+
+ local path, query
+ params.scheme, params.host, params.port, path, query = unpack(parsed_uri)
+ params.path = params.path or path
+ params.query = params.query or query
+ params.ssl_server_name = params.ssl_server_name or params.host
+ end
+
+ do
+ local proxy_auth = (params.headers or {})["Proxy-Authorization"]
+ if proxy_auth and params.proxy_opts then
+ params.proxy_opts.https_proxy_authorization = proxy_auth
+ params.proxy_opts.http_proxy_authorization = proxy_auth
+ end
+ end
+
+ local ok, err = self:connect(params)
+ if not ok then
+ return nil, err
+ end
+
+ local res, err = self:request(params)
+ if not res then
+ self:close()
+ return nil, err
+ end
+
+ local body, err = res:read_body()
+ if not body then
+ self:close()
+ return nil, err
+ end
+
+ res.body = body
+
+ if params.keepalive == false then
+ local ok, err = self:close()
+ if not ok then
+ ngx_log(ngx_ERR, err)
+ end
+
+ else
+ local ok, err = self:set_keepalive(params.keepalive_timeout, params.keepalive_pool)
+ if not ok then
+ ngx_log(ngx_ERR, err)
+ end
+
+ end
+
+ return res, nil
+end
+
+
+function _M.get_client_body_reader(_, chunksize, sock)
+ chunksize = chunksize or 65536
+
+ if not sock then
+ local ok, err
+ ok, sock, err = pcall(ngx_req_socket)
+
+ if not ok then
+ return nil, sock -- pcall err
+ end
+
+ if not sock then
+ if err == "no body" then
+ return nil
+ else
+ return nil, err
+ end
+ end
+ end
+
+ local headers = ngx_req_get_headers()
+ local length = headers.content_length
+ if length then
+ return _body_reader(sock, tonumber(length), chunksize)
+ elseif transfer_encoding_is_chunked(headers) then
+ -- Not yet supported by ngx_lua but should just work...
+ return _chunked_body_reader(sock, chunksize)
+ else
+ return nil
+ end
+end
+
+
+function _M.set_proxy_options(self, opts)
+ -- TODO: parse and cache these options, instead of parsing them
+ -- on each request over and over again (lru-cache on module level)
+ self.proxy_opts = tbl_copy(opts) -- Take by value
+end
+
+
+function _M.get_proxy_uri(self, scheme, host)
+ if not self.proxy_opts then
+ return nil
+ end
+
+ -- Check if the no_proxy option matches this host. Implementation adapted
+ -- from lua-http library (https://github.com/daurnimator/lua-http)
+ if self.proxy_opts.no_proxy then
+ if self.proxy_opts.no_proxy == "*" then
+ -- all hosts are excluded
+ return nil
+ end
+
+ local no_proxy_set = {}
+ -- wget allows domains in no_proxy list to be prefixed by "."
+ -- e.g. no_proxy=.mit.edu
+ for host_suffix in ngx_re_gmatch(self.proxy_opts.no_proxy, "\\.?([^,]+)", "jo") do
+ no_proxy_set[host_suffix[1]] = true
+ end
+
+ -- From curl docs:
+ -- matched as either a domain which contains the hostname, or the
+ -- hostname itself. For example local.com would match local.com,
+ -- local.com:80, and www.local.com, but not www.notlocal.com.
+ --
+ -- Therefore, we keep stripping subdomains from the host, compare
+ -- them to the ones in the no_proxy list and continue until we find
+ -- a match or until there's only the TLD left
+ repeat
+ if no_proxy_set[host] then
+ return nil
+ end
+
+ -- Strip the next level from the domain and check if that one
+ -- is on the list
+ host = ngx_re_sub(host, "^[^.]+\\.", "", "jo")
+ until not ngx_re_find(host, "\\.", "jo")
+ end
+
+ if scheme == "http" and self.proxy_opts.http_proxy then
+ return self.proxy_opts.http_proxy
+ end
+
+ if scheme == "https" and self.proxy_opts.https_proxy then
+ return self.proxy_opts.https_proxy
+ end
+
+ return nil
+end
+
+
+-- ----------------------------------------------------------------------------
+-- The following functions are considered DEPRECATED and may be REMOVED in
+-- future releases. Please see the notes in `README.md`.
+-- ----------------------------------------------------------------------------
+
+function _M.ssl_handshake(self, ...)
+ ngx_log(ngx_DEBUG, "Use of deprecated function `ssl_handshake`")
+
+ local sock = self.sock
+ if not sock then
+ return nil, "not initialized"
+ end
+
+ self.ssl = true
+
+ return sock:sslhandshake(...)
+end
+
+
+function _M.connect_proxy(self, proxy_uri, scheme, host, port, proxy_authorization)
+ ngx_log(ngx_DEBUG, "Use of deprecated function `connect_proxy`")
+
+ -- Parse the provided proxy URI
+ local parsed_proxy_uri, err = self:parse_uri(proxy_uri, false)
+ if not parsed_proxy_uri then
+ return nil, err
+ end
+
+ -- Check that the scheme is http (https is not supported for
+ -- connections between the client and the proxy)
+ local proxy_scheme = parsed_proxy_uri[1]
+ if proxy_scheme ~= "http" then
+ return nil, "protocol " .. proxy_scheme .. " not supported for proxy connections"
+ end
+
+ -- Make the connection to the given proxy
+ local proxy_host, proxy_port = parsed_proxy_uri[2], parsed_proxy_uri[3]
+ local c, err = self:tcp_only_connect(proxy_host, proxy_port)
+ if not c then
+ return nil, err
+ end
+
+ if scheme == "https" then
+ -- Make a CONNECT request to create a tunnel to the destination through
+ -- the proxy. The request-target and the Host header must be in the
+ -- authority-form of RFC 7230 Section 5.3.3. See also RFC 7231 Section
+ -- 4.3.6 for more details about the CONNECT request
+ local destination = host .. ":" .. port
+ local res, err = self:request({
+ method = "CONNECT",
+ path = destination,
+ headers = {
+ ["Host"] = destination,
+ ["Proxy-Authorization"] = proxy_authorization,
+ }
+ })
+
+ if not res then
+ return nil, err
+ end
+
+ if res.status < 200 or res.status > 299 then
+ return nil, "failed to establish a tunnel through a proxy: " .. res.status
+ end
+ end
+
+ return c, nil
+end
+
+
+function _M.proxy_request(self, chunksize)
+ ngx_log(ngx_DEBUG, "Use of deprecated function `proxy_request`")
+
+ return self:request({
+ method = ngx_req_get_method(),
+ path = ngx_re_gsub(ngx_var.uri, "\\s", "%20", "jo") .. ngx_var.is_args .. (ngx_var.query_string or ""),
+ body = self:get_client_body_reader(chunksize),
+ headers = ngx_req_get_headers(),
+ })
+end
+
+
+function _M.proxy_response(_, response, chunksize)
+ ngx_log(ngx_DEBUG, "Use of deprecated function `proxy_response`")
+
+ if not response then
+ ngx_log(ngx_ERR, "no response provided")
+ return
+ end
+
+ ngx.status = response.status
+
+ -- Filter out hop-by-hop headeres
+ for k, v in pairs(response.headers) do
+ if not HOP_BY_HOP_HEADERS[str_lower(k)] then
+ ngx_header[k] = v
+ end
+ end
+
+ local reader = response.body_reader
+
+ repeat
+ local chunk, ok, read_err, print_err
+
+ chunk, read_err = reader(chunksize)
+ if read_err then
+ ngx_log(ngx_ERR, read_err)
+ end
+
+ if chunk then
+ ok, print_err = ngx_print(chunk)
+ if not ok then
+ ngx_log(ngx_ERR, print_err)
+ end
+ end
+
+ if read_err or print_err then
+ break
+ end
+ until not chunk
+end
+
+
+return _M
diff --git a/server/resty/http_connect.lua b/server/resty/http_connect.lua
new file mode 100644
index 0000000..18a74b1
--- /dev/null
+++ b/server/resty/http_connect.lua
@@ -0,0 +1,274 @@
+local ngx_re_gmatch = ngx.re.gmatch
+local ngx_re_sub = ngx.re.sub
+local ngx_re_find = ngx.re.find
+local ngx_log = ngx.log
+local ngx_WARN = ngx.WARN
+
+--[[
+A connection function that incorporates:
+ - tcp connect
+ - ssl handshake
+ - http proxy
+Due to this it will be better at setting up a socket pool where connections can
+be kept alive.
+
+
+Call it with a single options table as follows:
+
+client:connect {
+ scheme = "https" -- scheme to use, or nil for unix domain socket
+ host = "myhost.com", -- target machine, or a unix domain socket
+ port = nil, -- port on target machine, will default to 80/443 based on scheme
+ pool = nil, -- connection pool name, leave blank! this function knows best!
+ pool_size = nil, -- options as per: https://github.com/openresty/lua-nginx-module#tcpsockconnect
+ backlog = nil,
+
+ -- ssl options as per: https://github.com/openresty/lua-nginx-module#tcpsocksslhandshake
+ ssl_reused_session = nil
+ ssl_server_name = nil,
+ ssl_send_status_req = nil,
+ ssl_verify = true, -- NOTE: defaults to true
+ ctx = nil, -- NOTE: not supported
+
+ -- mTLS options (experimental!)
+ --
+ -- !!! IMPORTANT !!! These options require support for mTLS in cosockets,
+ -- which is currently only available in the following unmerged PRs.
+ --
+ -- * https://github.com/openresty/lua-nginx-module/pull/1602
+ -- * https://github.com/openresty/lua-resty-core/pull/278
+ --
+ -- The details of this feature may change. You have been warned!
+ --
+ ssl_client_cert = nil,
+ ssl_client_priv_key = nil,
+
+ proxy_opts, -- proxy opts, defaults to global proxy options
+}
+]]
+local function connect(self, options)
+ local sock = self.sock
+ if not sock then
+ return nil, "not initialized"
+ end
+
+ local ok, err
+
+ local request_scheme = options.scheme
+ local request_host = options.host
+ local request_port = options.port
+
+ local poolname = options.pool
+ local pool_size = options.pool_size
+ local backlog = options.backlog
+
+ if request_scheme and not request_port then
+ request_port = (request_scheme == "https" and 443 or 80)
+ elseif request_port and not request_scheme then
+ return nil, "'scheme' is required when providing a port"
+ end
+
+ -- ssl settings
+ local ssl, ssl_reused_session, ssl_server_name
+ local ssl_verify, ssl_send_status_req, ssl_client_cert, ssl_client_priv_key
+ if request_scheme == "https" then
+ ssl = true
+ ssl_reused_session = options.ssl_reused_session
+ ssl_server_name = options.ssl_server_name
+ ssl_send_status_req = options.ssl_send_status_req
+ ssl_verify = true -- default
+ if options.ssl_verify == false then
+ ssl_verify = false
+ end
+ ssl_client_cert = options.ssl_client_cert
+ ssl_client_priv_key = options.ssl_client_priv_key
+ end
+
+ -- proxy related settings
+ local proxy, proxy_uri, proxy_authorization, proxy_host, proxy_port, path_prefix
+ proxy = options.proxy_opts or self.proxy_opts
+
+ if proxy then
+ if request_scheme == "https" then
+ proxy_uri = proxy.https_proxy
+ proxy_authorization = proxy.https_proxy_authorization
+ else
+ proxy_uri = proxy.http_proxy
+ proxy_authorization = proxy.http_proxy_authorization
+ -- When a proxy is used, the target URI must be in absolute-form
+ -- (RFC 7230, Section 5.3.2.). That is, it must be an absolute URI
+ -- to the remote resource with the scheme, host and an optional port
+ -- in place.
+ --
+ -- Since _format_request() constructs the request line by concatenating
+ -- params.path and params.query together, we need to modify the path
+ -- to also include the scheme, host and port so that the final form
+ -- in conformant to RFC 7230.
+ path_prefix = "http://" .. request_host .. (request_port == 80 and "" or (":" .. request_port))
+ end
+ if not proxy_uri then
+ proxy = nil
+ proxy_authorization = nil
+ path_prefix = nil
+ end
+ end
+
+ if proxy and proxy.no_proxy then
+ -- Check if the no_proxy option matches this host. Implementation adapted
+ -- from lua-http library (https://github.com/daurnimator/lua-http)
+ if proxy.no_proxy == "*" then
+ -- all hosts are excluded
+ proxy = nil
+
+ else
+ local host = request_host
+ local no_proxy_set = {}
+ -- wget allows domains in no_proxy list to be prefixed by "."
+ -- e.g. no_proxy=.mit.edu
+ for host_suffix in ngx_re_gmatch(proxy.no_proxy, "\\.?([^,]+)") do
+ no_proxy_set[host_suffix[1]] = true
+ end
+
+ -- From curl docs:
+ -- matched as either a domain which contains the hostname, or the
+ -- hostname itself. For example local.com would match local.com,
+ -- local.com:80, and www.local.com, but not www.notlocal.com.
+ --
+ -- Therefore, we keep stripping subdomains from the host, compare
+ -- them to the ones in the no_proxy list and continue until we find
+ -- a match or until there's only the TLD left
+ repeat
+ if no_proxy_set[host] then
+ proxy = nil
+ proxy_uri = nil
+ proxy_authorization = nil
+ break
+ end
+
+ -- Strip the next level from the domain and check if that one
+ -- is on the list
+ host = ngx_re_sub(host, "^[^.]+\\.", "")
+ until not ngx_re_find(host, "\\.")
+ end
+ end
+
+ if proxy then
+ local proxy_uri_t
+ proxy_uri_t, err = self:parse_uri(proxy_uri)
+ if not proxy_uri_t then
+ return nil, "uri parse error: ", err
+ end
+
+ local proxy_scheme = proxy_uri_t[1]
+ if proxy_scheme ~= "http" then
+ return nil, "protocol " .. tostring(proxy_scheme) ..
+ " not supported for proxy connections"
+ end
+ proxy_host = proxy_uri_t[2]
+ proxy_port = proxy_uri_t[3]
+ end
+
+ -- construct a poolname unique within proxy and ssl info
+ if not poolname then
+ poolname = (request_scheme or "")
+ .. ":" .. request_host
+ .. ":" .. tostring(request_port)
+ .. ":" .. tostring(ssl)
+ .. ":" .. (ssl_server_name or "")
+ .. ":" .. tostring(ssl_verify)
+ .. ":" .. (proxy_uri or "")
+ .. ":" .. (request_scheme == "https" and proxy_authorization or "")
+ -- in the above we only add the 'proxy_authorization' as part of the poolname
+ -- when the request is https. Because in that case the CONNECT request (which
+ -- carries the authorization header) is part of the connect procedure, whereas
+ -- with a plain http request the authorization is part of the actual request.
+ end
+
+ -- do TCP level connection
+ local tcp_opts = { pool = poolname, pool_size = pool_size, backlog = backlog }
+ if proxy then
+ -- proxy based connection
+ ok, err = sock:connect(proxy_host, proxy_port, tcp_opts)
+ if not ok then
+ return nil, "failed to connect to: " .. (proxy_host or "") ..
+ ":" .. (proxy_port or "") ..
+ ": ", err
+ end
+
+ if ssl and sock:getreusedtimes() == 0 then
+ -- Make a CONNECT request to create a tunnel to the destination through
+ -- the proxy. The request-target and the Host header must be in the
+ -- authority-form of RFC 7230 Section 5.3.3. See also RFC 7231 Section
+ -- 4.3.6 for more details about the CONNECT request
+ local destination = request_host .. ":" .. request_port
+ local res
+ res, err = self:request({
+ method = "CONNECT",
+ path = destination,
+ headers = {
+ ["Host"] = destination,
+ ["Proxy-Authorization"] = proxy_authorization,
+ }
+ })
+
+ if not res then
+ return nil, "failed to issue CONNECT to proxy:", err
+ end
+
+ if res.status < 200 or res.status > 299 then
+ return nil, "failed to establish a tunnel through a proxy: " .. res.status
+ end
+ end
+
+ elseif not request_port then
+ -- non-proxy, without port -> unix domain socket
+ ok, err = sock:connect(request_host, tcp_opts)
+ if not ok then
+ return nil, err
+ end
+
+ else
+ -- non-proxy, regular network tcp
+ ok, err = sock:connect(request_host, request_port, tcp_opts)
+ if not ok then
+ return nil, err
+ end
+ end
+
+ local ssl_session
+ -- Now do the ssl handshake
+ if ssl and sock:getreusedtimes() == 0 then
+
+ -- Experimental mTLS support
+ if ssl_client_cert and ssl_client_priv_key then
+ if type(sock.setclientcert) ~= "function" then
+ ngx_log(ngx_WARN, "cannot use SSL client cert and key without mTLS support")
+
+ else
+ -- currently no return value
+ ok, err = sock:setclientcert(ssl_client_cert, ssl_client_priv_key)
+ if not ok then
+ ngx_log(ngx_WARN, "could not set client certificate: ", err)
+ end
+ end
+ end
+
+ ssl_session, err = sock:sslhandshake(ssl_reused_session, ssl_server_name, ssl_verify, ssl_send_status_req)
+ if not ssl_session then
+ self:close()
+ return nil, err
+ end
+ end
+
+ self.host = request_host
+ self.port = request_port
+ self.keepalive = true
+ self.ssl = ssl
+ -- set only for http, https has already been handled
+ self.http_proxy_auth = request_scheme ~= "https" and proxy_authorization or nil
+ self.path_prefix = path_prefix
+
+ return true, nil, ssl_session
+end
+
+return connect
diff --git a/server/resty/http_headers.lua b/server/resty/http_headers.lua
new file mode 100644
index 0000000..97e8157
--- /dev/null
+++ b/server/resty/http_headers.lua
@@ -0,0 +1,44 @@
+local rawget, rawset, setmetatable =
+ rawget, rawset, setmetatable
+
+local str_lower = string.lower
+
+local _M = {
+ _VERSION = '0.17.0-beta.1',
+}
+
+
+-- Returns an empty headers table with internalised case normalisation.
+function _M.new()
+ local mt = {
+ normalised = {},
+ }
+
+ mt.__index = function(t, k)
+ return rawget(t, mt.normalised[str_lower(k)])
+ end
+
+ mt.__newindex = function(t, k, v)
+ local k_normalised = str_lower(k)
+
+ -- First time seeing this header field?
+ if not mt.normalised[k_normalised] then
+ -- Create a lowercased entry in the metatable proxy, with the value
+ -- of the given field case
+ mt.normalised[k_normalised] = k
+
+ -- Set the header using the given field case
+ rawset(t, k, v)
+ else
+ -- We're being updated just with a different field case. Use the
+ -- normalised metatable proxy to give us the original key case, and
+ -- perorm a rawset() to update the value.
+ rawset(t, mt.normalised[k_normalised], v)
+ end
+ end
+
+ return setmetatable({}, mt)
+end
+
+
+return _M
diff --git a/server/resty/jwt-validators.lua b/server/resty/jwt-validators.lua
new file mode 100644
index 0000000..df99418
--- /dev/null
+++ b/server/resty/jwt-validators.lua
@@ -0,0 +1,412 @@
+local _M = { _VERSION = "0.2.3" }
+
+--[[
+ This file defines "validators" to be used in validating a spec. A "validator" is simply a function with
+ a signature that matches:
+
+ function(val, claim, jwt_json)
+
+ This function returns either true or false. If a validator needs to give more information on why it failed,
+ then it can also raise an error (which will be used in the "reason" part of the validated jwt_obj). If a
+ validator returns nil, then it is assumed to have passed (same as returning true) and that you just forgot
+ to actually return a value.
+
+ There is a special claim name of "__jwt" that can be used to validate the entire jwt_obj.
+
+ "val" is the value being tested. It may be nil if the claim doesn't exist in the jwt_obj. If the function
+ is being called for the "__jwt" claim, then "val" will contain a deep clone of the full jwt object.
+
+ "claim" is the claim that is being tested. It is passed in just in case a validator needs to do additional
+ checks. It will be the string "__jwt" if the validator is being called for the entire jwt_object.
+
+ "jwt_json" is a json-encoded representation of the full object that is being tested. It will never be nil,
+ and can always be decoded using cjson.decode(jwt_json).
+]]--
+
+
+--[[
+ A function which will define a validator. It creates both "opt_" and required (non-"opt_")
+ versions. The function that is passed in is the *optional* version.
+]]--
+local function define_validator(name, fx)
+ _M["opt_" .. name] = fx
+ _M[name] = function(...) return _M.chain(_M.required(), fx(...)) end
+end
+
+-- Validation messages
+local messages = {
+ nil_validator = "Cannot create validator for nil %s.",
+ wrong_type_validator = "Cannot create validator for non-%s %s.",
+ empty_table_validator = "Cannot create validator for empty table %s.",
+ wrong_table_type_validator = "Cannot create validator for non-%s table %s.",
+ required_claim = "'%s' claim is required.",
+ wrong_type_claim = "'%s' is malformed. Expected to be a %s.",
+ missing_claim = "Missing one of claims - [ %s ]."
+}
+
+-- Local function to make sure that a value is non-nil or raises an error
+local function ensure_not_nil(v, e, ...)
+ return v ~= nil and v or error(string.format(e, ...), 0)
+end
+
+-- Local function to make sure that a value is the given type
+local function ensure_is_type(v, t, e, ...)
+ return type(v) == t and v or error(string.format(e, ...), 0)
+end
+
+-- Local function to make sure that a value is a (non-empty) table
+local function ensure_is_table(v, e, ...)
+ ensure_is_type(v, "table", e, ...)
+ return ensure_not_nil(next(v), e, ...)
+end
+
+-- Local function to make sure all entries in the table are the given type
+local function ensure_is_table_type(v, t, e, ...)
+ if v ~= nil then
+ ensure_is_table(v, e, ...)
+ for _,val in ipairs(v) do
+ ensure_is_type(val, t, e, ...)
+ end
+ end
+ return v
+end
+
+-- Local function to ensure that a number is non-negative (positive or 0)
+local function ensure_is_non_negative(v, e, ...)
+ if v ~= nil then
+ ensure_is_type(v, "number", e, ...)
+ if v >= 0 then
+ return v
+ else
+ error(string.format(e, ...), 0)
+ end
+ end
+end
+
+-- A local function which returns simple equality
+local function equality_function(val, check)
+ return val == check
+end
+
+-- A local function which returns string match
+local function string_match_function(val, pattern)
+ return string.match(val, pattern) ~= nil
+end
+
+--[[
+ A local function which returns truth on existence of check in vals.
+ Adopted from auth0/nginx-jwt table_contains by @twistedstream
+]]--
+local function table_contains_function(vals, check)
+ for _, val in pairs(vals) do
+ if val == check then return true end
+ end
+ return false
+end
+
+
+-- A local function which returns numeric greater than comparison
+local function greater_than_function(val, check)
+ return val > check
+end
+
+-- A local function which returns numeric greater than or equal comparison
+local function greater_than_or_equal_function(val, check)
+ return val >= check
+end
+
+-- A local function which returns numeric less than comparison
+local function less_than_function(val, check)
+ return val < check
+end
+
+-- A local function which returns numeric less than or equal comparison
+local function less_than_or_equal_function(val, check)
+ return val <= check
+end
+
+
+--[[
+ Returns a validator that chains the given functions together, one after
+ another - as long as they keep passing their checks.
+]]--
+function _M.chain(...)
+ local chain_functions = {...}
+ for _, fx in ipairs(chain_functions) do
+ ensure_is_type(fx, "function", messages.wrong_type_validator, "function", "chain_function")
+ end
+
+ return function(val, claim, jwt_json)
+ for _, fx in ipairs(chain_functions) do
+ if fx(val, claim, jwt_json) == false then
+ return false
+ end
+ end
+ return true
+ end
+end
+
+--[[
+ Returns a validator that returns false if a value doesn't exist. If
+ the value exists and a chain_function is specified, then the value of
+ chain_function(val, claim, jwt_json)
+ will be returned, otherwise, true will be returned. This allows for
+ specifying that a value is both required *and* it must match some
+ additional check. This function will be used in the "required_*" shortcut
+ functions for simplification.
+]]--
+function _M.required(chain_function)
+ if chain_function ~= nil then
+ return _M.chain(_M.required(), chain_function)
+ end
+
+ return function(val, claim, jwt_json)
+ ensure_not_nil(val, messages.required_claim, claim)
+ return true
+ end
+end
+
+--[[
+ Returns a validator which errors with a message if *NONE* of the given claim
+ keys exist. It is expected that this function is used against a full jwt object.
+ The claim_keys must be a non-empty table of strings.
+]]--
+function _M.require_one_of(claim_keys)
+ ensure_not_nil(claim_keys, messages.nil_validator, "claim_keys")
+ ensure_is_type(claim_keys, "table", messages.wrong_type_validator, "table", "claim_keys")
+ ensure_is_table(claim_keys, messages.empty_table_validator, "claim_keys")
+ ensure_is_table_type(claim_keys, "string", messages.wrong_table_type_validator, "string", "claim_keys")
+
+ return function(val, claim, jwt_json)
+ ensure_is_type(val, "table", messages.wrong_type_claim, claim, "table")
+ ensure_is_type(val.payload, "table", messages.wrong_type_claim, claim .. ".payload", "table")
+
+ for i, v in ipairs(claim_keys) do
+ if val.payload[v] ~= nil then return true end
+ end
+
+ error(string.format(messages.missing_claim, table.concat(claim_keys, ", ")), 0)
+ end
+end
+
+--[[
+ Returns a validator that checks if the result of calling the given function for
+ the tested value and the check value returns true. The value of check_val and
+ check_function cannot be nil. The optional name is used for error messages and
+ defaults to "check_value". The optional check_type is used to make sure that
+ the check type matches and defaults to type(check_val). The first parameter
+ passed to check_function will *never* be nil (check succeeds if value is nil).
+ Use the required version to fail on nil. If the check_function raises an
+ error, that will be appended to the error message.
+]]--
+define_validator("check", function(check_val, check_function, name, check_type)
+ name = name or "check_val"
+ ensure_not_nil(check_val, messages.nil_validator, name)
+
+ ensure_not_nil(check_function, messages.nil_validator, "check_function")
+ ensure_is_type(check_function, "function", messages.wrong_type_validator, "function", "check_function")
+
+ check_type = check_type or type(check_val)
+ return function(val, claim, jwt_json)
+ if val == nil then return true end
+
+ ensure_is_type(val, check_type, messages.wrong_type_claim, claim, check_type)
+ return check_function(val, check_val)
+ end
+end)
+
+
+--[[
+ Returns a validator that checks if a value exactly equals the given check_value.
+ If the value is nil, then this check succeeds. The value of check_val cannot be
+ nil.
+]]--
+define_validator("equals", function(check_val)
+ return _M.opt_check(check_val, equality_function, "check_val")
+end)
+
+
+--[[
+ Returns a validator that checks if a value matches the given pattern. The value
+ of pattern must be a string.
+]]--
+define_validator("matches", function (pattern)
+ ensure_is_type(pattern, "string", messages.wrong_type_validator, "string", "pattern")
+ return _M.opt_check(pattern, string_match_function, "pattern", "string")
+end)
+
+
+--[[
+ Returns a validator which calls the given function for each of the given values
+ and the tested value. If any of these calls return true, then this function
+ returns true. The value of check_values must be a non-empty table with all the
+ same types, and the value of check_function must not be nil. The optional name
+ is used for error messages and defaults to "check_values". The optional
+ check_type is used to make sure that the check type matches and defaults to
+ type(check_values[1]) - the table type.
+]]--
+define_validator("any_of", function(check_values, check_function, name, check_type, table_type)
+ name = name or "check_values"
+ ensure_not_nil(check_values, messages.nil_validator, name)
+ ensure_is_type(check_values, "table", messages.wrong_type_validator, "table", name)
+ ensure_is_table(check_values, messages.empty_table_validator, name)
+
+ table_type = table_type or type(check_values[1])
+ ensure_is_table_type(check_values, table_type, messages.wrong_table_type_validator, table_type, name)
+
+ ensure_not_nil(check_function, messages.nil_validator, "check_function")
+ ensure_is_type(check_function, "function", messages.wrong_type_validator, "function", "check_function")
+
+ check_type = check_type or table_type
+ return _M.opt_check(check_values, function(v1, v2)
+ for i, v in ipairs(v2) do
+ if check_function(v1, v) then return true end
+ end
+ return false
+ end, name, check_type)
+end)
+
+
+--[[
+ Returns a validator that checks if a value exactly equals any of the given values.
+]]--
+define_validator("equals_any_of", function(check_values)
+ return _M.opt_any_of(check_values, equality_function, "check_values")
+end)
+
+
+--[[
+ Returns a validator that checks if a value matches any of the given patterns.
+]]--
+define_validator("matches_any_of", function(patterns)
+ return _M.opt_any_of(patterns, string_match_function, "patterns", "string", "string")
+end)
+
+--[[
+ Returns a validator that checks if a value of expected type string exists in any of the given values.
+ The value of check_values must be a non-empty table with all the same types.
+ The optional name is used for error messages and defaults to "check_values".
+]]--
+define_validator("contains_any_of", function(check_values, name)
+ return _M.opt_any_of(check_values, table_contains_function, name, "table", "string")
+end)
+
+--[[
+ Returns a validator that checks how a value compares (numerically) to a given
+ check_value. The value of check_val cannot be nil and must be a number.
+]]--
+define_validator("greater_than", function(check_val)
+ ensure_is_type(check_val, "number", messages.wrong_type_validator, "number", "check_val")
+ return _M.opt_check(check_val, greater_than_function, "check_val", "number")
+end)
+define_validator("greater_than_or_equal", function(check_val)
+ ensure_is_type(check_val, "number", messages.wrong_type_validator, "number", "check_val")
+ return _M.opt_check(check_val, greater_than_or_equal_function, "check_val", "number")
+end)
+define_validator("less_than", function(check_val)
+ ensure_is_type(check_val, "number", messages.wrong_type_validator, "number", "check_val")
+ return _M.opt_check(check_val, less_than_function, "check_val", "number")
+end)
+define_validator("less_than_or_equal", function(check_val)
+ ensure_is_type(check_val, "number", messages.wrong_type_validator, "number", "check_val")
+ return _M.opt_check(check_val, less_than_or_equal_function, "check_val", "number")
+end)
+
+
+--[[
+ A function to set the leeway (in seconds) used for is_not_before and is_not_expired. The
+ default is to use 0 seconds
+]]--
+local system_leeway = 0
+function _M.set_system_leeway(leeway)
+ ensure_is_type(leeway, "number", "leeway must be a non-negative number")
+ ensure_is_non_negative(leeway, "leeway must be a non-negative number")
+ system_leeway = leeway
+end
+
+
+--[[
+ A function to set the system clock used for is_not_before and is_not_expired. The
+ default is to use ngx.now
+]]--
+local system_clock = ngx.now
+function _M.set_system_clock(clock)
+ ensure_is_type(clock, "function", "clock must be a function")
+ -- Check that clock returns the correct value
+ local t = clock()
+ ensure_is_type(t, "number", "clock function must return a non-negative number")
+ ensure_is_non_negative(t, "clock function must return a non-negative number")
+ system_clock = clock
+end
+
+-- Local helper function for date validation
+local function validate_is_date(val, claim, jwt_json)
+ ensure_is_non_negative(val, messages.wrong_type_claim, claim, "positive numeric value")
+ return true
+end
+
+-- Local helper for date formatting
+local function format_date_on_error(date_check_function, error_msg)
+ ensure_is_type(date_check_function, "function", messages.wrong_type_validator, "function", "date_check_function")
+ ensure_is_type(error_msg, "string", messages.wrong_type_validator, "string", error_msg)
+ return function(val, claim, jwt_json)
+ local ret = date_check_function(val, claim, jwt_json)
+ if ret == false then
+ error(string.format("'%s' claim %s %s", claim, error_msg, ngx.http_time(val)), 0)
+ end
+ return true
+ end
+end
+
+--[[
+ Returns a validator that checks if the current time is not before the tested value
+ within the system's leeway. This means that:
+ val <= (system_clock() + system_leeway).
+]]--
+define_validator("is_not_before", function()
+ return format_date_on_error(
+ _M.chain(validate_is_date,
+ function(val)
+ return val and less_than_or_equal_function(val, (system_clock() + system_leeway))
+ end),
+ "not valid until"
+ )
+end)
+
+
+--[[
+ Returns a validator that checks if the current time is not equal to or after the
+ tested value within the system's leeway. This means that:
+ val > (system_clock() - system_leeway).
+]]--
+define_validator("is_not_expired", function()
+ return format_date_on_error(
+ _M.chain(validate_is_date,
+ function(val)
+ return val and greater_than_function(val, (system_clock() - system_leeway))
+ end),
+ "expired at"
+ )
+end)
+
+--[[
+ Returns a validator that checks if the current time is the same as the tested value
+ within the system's leeway. This means that:
+ val >= (system_clock() - system_leeway) and val <= (system_clock() + system_leeway).
+]]--
+define_validator("is_at", function()
+ local now = system_clock()
+ return format_date_on_error(
+ _M.chain(validate_is_date,
+ function(val)
+ local now = system_clock()
+ return val and
+ greater_than_or_equal_function(val, now - system_leeway) and
+ less_than_or_equal_function(val, now + system_leeway)
+ end),
+ "is only valid at"
+ )
+end)
+
+
+return _M
diff --git a/server/resty/jwt.lua b/server/resty/jwt.lua
new file mode 100644
index 0000000..accba11
--- /dev/null
+++ b/server/resty/jwt.lua
@@ -0,0 +1,959 @@
+local cjson = require "cjson.safe"
+
+local evp = require "resty.evp"
+local hmac = require "resty.hmac"
+local resty_random = require "resty.random"
+local cipher = require "resty.openssl.cipher"
+
+local _M = { _VERSION = "0.2.3" }
+
+local mt = {
+ __index = _M
+}
+
+local string_rep = string.rep
+local string_format = string.format
+local string_sub = string.sub
+local string_char = string.char
+local table_concat = table.concat
+local ngx_encode_base64 = ngx.encode_base64
+local ngx_decode_base64 = ngx.decode_base64
+local cjson_encode = cjson.encode
+local cjson_decode = cjson.decode
+local tostring = tostring
+local error = error
+local ipairs = ipairs
+local type = type
+local pcall = pcall
+local assert = assert
+local setmetatable = setmetatable
+local pairs = pairs
+
+-- define string constants to avoid string garbage collection
+local str_const = {
+ invalid_jwt= "invalid jwt string",
+ regex_join_msg = "%s.%s",
+ regex_join_delim = "([^%s]+)",
+ regex_split_dot = "%.",
+ regex_jwt_join_str = "%s.%s.%s",
+ raw_underscore = "raw_",
+ dash = "-",
+ empty = "",
+ dotdot = "..",
+ table = "table",
+ plus = "+",
+ equal = "=",
+ underscore = "_",
+ slash = "/",
+ header = "header",
+ typ = "typ",
+ JWT = "JWT",
+ JWE = "JWE",
+ payload = "payload",
+ signature = "signature",
+ encrypted_key = "encrypted_key",
+ alg = "alg",
+ enc = "enc",
+ kid = "kid",
+ exp = "exp",
+ nbf = "nbf",
+ iss = "iss",
+ full_obj = "__jwt",
+ x5c = "x5c",
+ x5u = 'x5u',
+ HS256 = "HS256",
+ HS512 = "HS512",
+ RS256 = "RS256",
+ ES256 = "ES256",
+ ES512 = "ES512",
+ RS512 = "RS512",
+ A128CBC_HS256 = "A128CBC-HS256",
+ A128CBC_HS256_CIPHER_MODE = "aes-128-cbc",
+ A256CBC_HS512 = "A256CBC-HS512",
+ A256CBC_HS512_CIPHER_MODE = "aes-256-cbc",
+ A256GCM = "A256GCM",
+ A256GCM_CIPHER_MODE = "aes-256-gcm",
+ RSA_OAEP_256 = "RSA-OAEP-256",
+ DIR = "dir",
+ reason = "reason",
+ verified = "verified",
+ number = "number",
+ string = "string",
+ funct = "function",
+ boolean = "boolean",
+ valid = "valid",
+ valid_issuers = "valid_issuers",
+ lifetime_grace_period = "lifetime_grace_period",
+ require_nbf_claim = "require_nbf_claim",
+ require_exp_claim = "require_exp_claim",
+ internal_error = "internal error",
+ everything_awesome = "everything is awesome~ :p"
+}
+
+-- @function split string
+local function split_string(str, delim)
+ local result = {}
+ local sep = string_format(str_const.regex_join_delim, delim)
+ for m in str:gmatch(sep) do
+ result[#result+1]=m
+ end
+ return result
+end
+
+-- @function is nil or boolean
+-- @return true if param is nil or true or false; false otherwise
+local function is_nil_or_boolean(arg_value)
+ if arg_value == nil then
+ return true
+ end
+
+ if type(arg_value) ~= str_const.boolean then
+ return false
+ end
+
+ return true
+end
+
+--@function get the raw part
+--@param part_name
+--@param jwt_obj
+local function get_raw_part(part_name, jwt_obj)
+ local raw_part = jwt_obj[str_const.raw_underscore .. part_name]
+ if raw_part == nil then
+ local part = jwt_obj[part_name]
+ if part == nil then
+ error({reason="missing part " .. part_name})
+ end
+ raw_part = _M:jwt_encode(part)
+ end
+ return raw_part
+end
+
+
+--@function decrypt payload
+--@param secret_key to decrypt the payload
+--@param encrypted payload
+--@param encryption algorithm
+--@param iv which was generated while encrypting the payload
+--@param aad additional authenticated data (used when gcm mode is used)
+--@param auth_tag authenticated tag (used when gcm mode is used)
+--@return decrypted payloaf
+local function decrypt_payload(secret_key, encrypted_payload, enc, iv_in, aad, auth_tag )
+ local decrypted_payload, err
+ if enc == str_const.A128CBC_HS256 then
+ local aes_128_cbs_cipher = assert(cipher.new(str_const.A128CBC_HS256_CIPHER_MODE))
+ decrypted_payload, err= aes_128_cbs_cipher:decrypt(secret_key, iv_in, encrypted_payload)
+ elseif enc == str_const.A256CBC_HS512 then
+ local aes_256_cbs_cipher = assert(cipher.new(str_const.A256CBC_HS512_CIPHER_MODE))
+ decrypted_payload, err = aes_256_cbs_cipher:decrypt(secret_key, iv_in, encrypted_payload)
+ elseif enc == str_const.A256GCM then
+ local aes_256_gcm_cipher = assert(cipher.new(str_const.A256GCM_CIPHER_MODE))
+ decrypted_payload, err = aes_256_gcm_cipher:decrypt(secret_key, iv_in, encrypted_payload, false, aad, auth_tag)
+ else
+ return nil, "unsupported enc: " .. enc
+ end
+ if not decrypted_payload or err then
+ return nil, err
+ end
+ return decrypted_payload
+end
+
+-- @function encrypt payload using given secret
+-- @param secret_key secret key to encrypt
+-- @param message data to be encrypted. It could be lua table or string
+-- @param enc algorithm to use for encryption
+-- @param aad additional authenticated data (used when gcm mode is used)
+local function encrypt_payload(secret_key, message, enc, aad )
+
+ if enc == str_const.A128CBC_HS256 then
+ local iv_rand = resty_random.bytes(16,true)
+ local aes_128_cbs_cipher = assert(cipher.new(str_const.A128CBC_HS256_CIPHER_MODE))
+ local encrypted = aes_128_cbs_cipher:encrypt(secret_key, iv_rand, message)
+ return encrypted, iv_rand
+
+ elseif enc == str_const.A256CBC_HS512 then
+ local iv_rand = resty_random.bytes(16,true)
+ local aes_256_cbs_cipher = assert(cipher.new(str_const.A256CBC_HS512_CIPHER_MODE))
+ local encrypted = aes_256_cbs_cipher:encrypt(secret_key, iv_rand, message)
+ return encrypted, iv_rand
+
+ elseif enc == str_const.A256GCM then
+ local iv_rand = resty_random.bytes(12,true) -- 96 bit IV is recommended for efficiency
+ local aes_256_gcm_cipher = assert(cipher.new(str_const.A256GCM_CIPHER_MODE))
+ local encrypted = aes_256_gcm_cipher:encrypt(secret_key, iv_rand, message, false, aad)
+ local auth_tag = assert(aes_256_gcm_cipher:get_aead_tag())
+ return encrypted, iv_rand, auth_tag
+
+ else
+ return nil, nil , nil, "unsupported enc: " .. enc
+ end
+end
+
+--@function hmac_digest : generate hmac digest based on key for input message
+--@param mac_key
+--@param input message
+--@return hmac digest
+local function hmac_digest(enc, mac_key, message)
+ if enc == str_const.A128CBC_HS256 then
+ return hmac:new(mac_key, hmac.ALGOS.SHA256):final(message)
+ elseif enc == str_const.A256CBC_HS512 then
+ return hmac:new(mac_key, hmac.ALGOS.SHA512):final(message)
+ else
+ error({reason="unsupported enc: " .. enc})
+ end
+end
+
+--@function dervice keys: it generates key if null based on encryption algorithm
+--@param encryption type
+--@param secret key
+--@return secret key, mac key and encryption key
+local function derive_keys(enc, secret_key)
+ local mac_key_len, enc_key_len = 16, 16
+
+ if enc == str_const.A256GCM then
+ mac_key_len, enc_key_len = 0, 32 -- we need 256 bit key
+ elseif enc == str_const.A128CBC_HS256 then
+ mac_key_len, enc_key_len = 16, 16
+ elseif enc == str_const.A256CBC_HS512 then
+ mac_key_len, enc_key_len = 32, 32
+ else
+ error({reason="unsupported payload encryption algorithm :" .. enc})
+ end
+
+ local secret_key_len = mac_key_len + enc_key_len
+
+ if not secret_key then
+ secret_key = resty_random.bytes(secret_key_len, true)
+ end
+
+ if #secret_key ~= secret_key_len then
+ error({reason="invalid pre-shared key"})
+ end
+
+ local mac_key = string_sub(secret_key, 1, mac_key_len)
+ local enc_key = string_sub(secret_key, mac_key_len + 1)
+ return secret_key, mac_key, enc_key
+end
+
+local function get_payload_encoder(self)
+ return self.payload_encoder or cjson_encode
+end
+
+local function get_payload_decoder(self)
+ return self.payload_decoder or cjson_decode
+end
+
+--@function parse_jwe
+--@param pre-shared key
+--@encoded-header
+local function parse_jwe(self, preshared_key, encoded_header, encoded_encrypted_key, encoded_iv, encoded_cipher_text, encoded_auth_tag)
+
+
+ local header = _M:jwt_decode(encoded_header, true)
+ if not header then
+ error({reason="invalid header: " .. encoded_header})
+ end
+
+ local alg = header.alg
+ if alg ~= str_const.DIR and alg ~= str_const.RSA_OAEP_256 then
+ error({reason="invalid algorithm: " .. alg})
+ end
+
+ local key, enc_key
+ if alg == str_const.DIR then
+ if not preshared_key then
+ error({reason="preshared key must not be null"})
+ end
+ key, _, enc_key = derive_keys(header.enc, preshared_key)
+ elseif alg == str_const.RSA_OAEP_256 then
+ if not preshared_key then
+ error({reason="rsa private key must not be null"})
+ end
+ local rsa_decryptor, err = evp.RSADecryptor:new(preshared_key, nil, evp.CONST.RSA_PKCS1_OAEP_PADDING, evp.CONST.SHA256_DIGEST)
+ if err then
+ error({reason="failed to create rsa object: ".. err})
+ end
+ local secret_key, err = rsa_decryptor:decrypt(_M:jwt_decode(encoded_encrypted_key))
+ if err or not secret_key then
+ error({reason="failed to decrypt key: " .. err})
+ end
+ key, _, enc_key = derive_keys(header.enc, secret_key)
+ end
+
+ local cipher_text = _M:jwt_decode(encoded_cipher_text)
+ local iv = _M:jwt_decode(encoded_iv)
+ local signature_or_tag = _M:jwt_decode(encoded_auth_tag)
+ local basic_jwe = {
+ internal = {
+ encoded_header = encoded_header,
+ cipher_text = cipher_text,
+ key = key,
+ iv = iv
+ },
+ header = header,
+ signature = signature_or_tag
+ }
+
+ local payload, err = decrypt_payload(enc_key, cipher_text, header.enc, iv, encoded_header, signature_or_tag)
+ if err then
+ error({reason="failed to decrypt payload: " .. err})
+
+ else
+ basic_jwe.payload = get_payload_decoder(self)(payload)
+ basic_jwe.internal.json_payload=payload
+ end
+ return basic_jwe
+end
+
+-- @function parse_jwt
+-- @param encoded header
+-- @param encoded
+-- @param signature
+-- @return jwt table
+local function parse_jwt(encoded_header, encoded_payload, signature)
+ local header = _M:jwt_decode(encoded_header, true)
+ if not header then
+ error({reason="invalid header: " .. encoded_header})
+ end
+
+ local payload = _M:jwt_decode(encoded_payload, true)
+ if not payload then
+ error({reason="invalid payload: " .. encoded_payload})
+ end
+
+ local basic_jwt = {
+ raw_header=encoded_header,
+ raw_payload=encoded_payload,
+ header=header,
+ payload=payload,
+ signature=signature
+ }
+ return basic_jwt
+
+end
+
+-- @function parse token - this can be JWE or JWT token
+-- @param token string
+-- @return jwt/jwe tables
+local function parse(self, secret, token_str)
+ local tokens = split_string(token_str, str_const.regex_split_dot)
+ local num_tokens = #tokens
+ if num_tokens == 3 then
+ return parse_jwt(tokens[1], tokens[2], tokens[3])
+ elseif num_tokens == 4 then
+ return parse_jwe(self, secret, tokens[1], nil, tokens[2], tokens[3], tokens[4])
+ elseif num_tokens == 5 then
+ return parse_jwe(self, secret, tokens[1], tokens[2], tokens[3], tokens[4], tokens[5])
+ else
+ error({reason=str_const.invalid_jwt})
+ end
+end
+
+--@function jwt encode : it converts into base64 encoded string. if input is a table, it convets into
+-- json before converting to base64 string
+--@param payloaf
+--@return base64 encoded payloaf
+function _M.jwt_encode(self, ori, is_payload)
+ if type(ori) == str_const.table then
+ ori = is_payload and get_payload_encoder(self)(ori) or cjson_encode(ori)
+ end
+ local res = ngx_encode_base64(ori):gsub(str_const.plus, str_const.dash):gsub(str_const.slash, str_const.underscore):gsub(str_const.equal, str_const.empty)
+ return res
+end
+
+
+
+--@function jwt decode : decode bas64 encoded string
+function _M.jwt_decode(self, b64_str, json_decode, is_payload)
+ b64_str = b64_str:gsub(str_const.dash, str_const.plus):gsub(str_const.underscore, str_const.slash)
+
+ local reminder = #b64_str % 4
+ if reminder > 0 then
+ b64_str = b64_str .. string_rep(str_const.equal, 4 - reminder)
+ end
+ local data = ngx_decode_base64(b64_str)
+ if not data then
+ return nil
+ end
+ if json_decode then
+ data = is_payload and get_payload_decoder(self)(data) or cjson_decode(data)
+ end
+ return data
+end
+
+--- Initialize the trusted certs
+-- During RS256 verify, we'll make sure the
+-- cert was signed by one of these
+function _M.set_trusted_certs_file(self, filename)
+ self.trusted_certs_file = filename
+end
+_M.trusted_certs_file = nil
+
+--- Set a whitelist of allowed algorithms
+-- E.g., jwt:set_alg_whitelist({RS256=1,HS256=1})
+--
+-- @param algorithms - A table with keys for the supported algorithms
+-- If the table is non-nil, during
+-- verify, the alg must be in the table
+function _M.set_alg_whitelist(self, algorithms)
+ self.alg_whitelist = algorithms
+end
+
+_M.alg_whitelist = nil
+
+
+--- Returns the list of default validations that will be
+--- applied upon the verification of a jwt.
+function _M.get_default_validation_options(self, jwt_obj)
+ return {
+ [str_const.require_exp_claim]=jwt_obj[str_const.payload].exp ~= nil,
+ [str_const.require_nbf_claim]=jwt_obj[str_const.payload].nbf ~= nil
+ }
+end
+
+--- Set a function used to retrieve the content of x5u urls
+--
+-- @param retriever_function - A pointer to a function. This function should be
+-- defined to accept three string parameters. First one
+-- will be the value of the 'x5u' attribute. Second
+-- one will be the value of the 'iss' attribute, would
+-- it be defined in the jwt. Third one will be the value
+-- of the 'iss' attribute, would it be defined in the jwt.
+-- This function should return the matching certificate.
+function _M.set_x5u_content_retriever(self, retriever_function)
+ if type(retriever_function) ~= str_const.funct then
+ error("'retriever_function' is expected to be a function", 0)
+ end
+ self.x5u_content_retriever = retriever_function
+end
+
+_M.x5u_content_retriever = nil
+
+-- https://tools.ietf.org/html/rfc7516#appendix-B.3
+-- TODO: do it in lua way
+local function binlen(s)
+ if type(s) ~= 'string' then return end
+
+ local len = 8 * #s
+
+ return string_char(len / 0x0100000000000000 % 0x100)
+ .. string_char(len / 0x0001000000000000 % 0x100)
+ .. string_char(len / 0x0000010000000000 % 0x100)
+ .. string_char(len / 0x0000000100000000 % 0x100)
+ .. string_char(len / 0x0000000001000000 % 0x100)
+ .. string_char(len / 0x0000000000010000 % 0x100)
+ .. string_char(len / 0x0000000000000100 % 0x100)
+ .. string_char(len / 0x0000000000000001 % 0x100)
+end
+
+--@function sign jwe payload
+--@param secret key : if used pre-shared or RSA key
+--@param jwe payload
+--@return jwe token
+local function sign_jwe(self, secret_key, jwt_obj)
+ local header = jwt_obj.header
+ local enc = header.enc
+ local alg = header.alg
+
+ -- remove type
+ if header.typ then
+ header.typ = nil
+ end
+
+ -- TODO: implement logic for creating enc key and mac key and then encrypt key
+ local key, encrypted_key, mac_key, enc_key
+ local encoded_header = _M:jwt_encode(header)
+ local payload_to_encrypt = get_payload_encoder(self)(jwt_obj.payload)
+ if alg == str_const.DIR then
+ _, mac_key, enc_key = derive_keys(enc, secret_key)
+ encrypted_key = ""
+ elseif alg == str_const.RSA_OAEP_256 then
+ local cert, err
+ if secret_key:find("CERTIFICATE") then
+ cert, err = evp.Cert:new(secret_key)
+ elseif secret_key:find("PUBLIC KEY") then
+ cert, err = evp.PublicKey:new(secret_key)
+ end
+ if not cert then
+ error({reason="Decode secret is not a valid cert/public key: " .. (err and err or secret_key)})
+ end
+ local rsa_encryptor = evp.RSAEncryptor:new(cert, evp.CONST.RSA_PKCS1_OAEP_PADDING, evp.CONST.SHA256_DIGEST)
+ if err then
+ error("failed to create rsa object for encryption ".. err)
+ end
+ key, mac_key, enc_key = derive_keys(enc)
+ encrypted_key, err = rsa_encryptor:encrypt(key)
+ if err or not encrypted_key then
+ error({reason="failed to encrypt key " .. (err or "")})
+ end
+ else
+ error({reason="unsupported alg: " .. alg})
+ end
+
+ local cipher_text, iv, auth_tag, err = encrypt_payload(enc_key, payload_to_encrypt, enc, encoded_header)
+ if err then
+ error({reason="error while encrypting payload. Error: " .. err})
+ end
+
+ if not auth_tag then
+ local encoded_header_length = binlen(encoded_header)
+ local mac_input = table_concat({encoded_header , iv, cipher_text , encoded_header_length})
+ local mac = hmac_digest(enc, mac_key, mac_input)
+ auth_tag = string_sub(mac, 1, #mac/2)
+ end
+
+ local jwe_table = {encoded_header, _M:jwt_encode(encrypted_key), _M:jwt_encode(iv),
+ _M:jwt_encode(cipher_text), _M:jwt_encode(auth_tag)}
+ return table_concat(jwe_table, ".", 1, 5)
+end
+
+--@function get_secret_str : returns the secret if it is a string, or the result of a function
+--@param either the string secret or a function that takes a string parameter and returns a string or nil
+--@param jwt payload
+--@return the secret as a string or as a function
+local function get_secret_str(secret_or_function, jwt_obj)
+ if type(secret_or_function) == str_const.funct then
+ -- Only use with hmac algorithms
+ local alg = jwt_obj[str_const.header][str_const.alg]
+ if alg ~= str_const.HS256 and alg ~= str_const.HS512 then
+ error({reason="secret function can only be used with hmac alg: " .. alg})
+ end
+
+ -- Pull out the kid value from the header
+ local kid_val = jwt_obj[str_const.header][str_const.kid]
+ if kid_val == nil then
+ error({reason="secret function specified without kid in header"})
+ end
+
+ -- Call the function
+ return secret_or_function(kid_val) or error({reason="function returned nil for kid: " .. kid_val})
+ elseif type(secret_or_function) == str_const.string then
+ -- Just return the string
+ return secret_or_function
+ else
+ -- Throw an error
+ error({reason="invalid secret type (must be string or function)"})
+ end
+end
+
+--@function sign : create a jwt/jwe signature from jwt_object
+--@param secret key
+--@param jwt/jwe payload
+function _M.sign(self, secret_key, jwt_obj)
+ -- header typ check
+ local typ = jwt_obj[str_const.header][str_const.typ]
+ -- Optional header typ check [See http://tools.ietf.org/html/draft-ietf-oauth-json-web-token-25#section-5.1]
+ if typ ~= nil then
+ if typ ~= str_const.JWT and typ ~= str_const.JWE then
+ error({reason="invalid typ: " .. typ})
+ end
+ end
+
+ if typ == str_const.JWE or jwt_obj.header.enc then
+ return sign_jwe(self, secret_key, jwt_obj)
+ end
+ -- header alg check
+ local raw_header = get_raw_part(str_const.header, jwt_obj)
+ local raw_payload = get_raw_part(str_const.payload, jwt_obj)
+ local message = string_format(str_const.regex_join_msg, raw_header, raw_payload)
+ local alg = jwt_obj[str_const.header][str_const.alg]
+ local signature = ""
+ if alg == str_const.HS256 then
+ local secret_str = get_secret_str(secret_key, jwt_obj)
+ signature = hmac:new(secret_str, hmac.ALGOS.SHA256):final(message)
+ elseif alg == str_const.HS512 then
+ local secret_str = get_secret_str(secret_key, jwt_obj)
+ signature = hmac:new(secret_str, hmac.ALGOS.SHA512):final(message)
+ elseif alg == str_const.RS256 or alg == str_const.RS512 then
+ local signer, err = evp.RSASigner:new(secret_key)
+ if not signer then
+ error({reason="signer error: " .. err})
+ end
+ if alg == str_const.RS256 then
+ signature = signer:sign(message, evp.CONST.SHA256_DIGEST)
+ elseif alg == str_const.RS512 then
+ signature = signer:sign(message, evp.CONST.SHA512_DIGEST)
+ end
+ elseif alg == str_const.ES256 or alg == str_const.ES512 then
+ local signer, err = evp.ECSigner:new(secret_key)
+ if not signer then
+ error({reason="signer error: " .. err})
+ end
+ -- OpenSSL will generate a DER encoded signature that needs to be converted
+ local der_signature = ""
+ if alg == str_const.ES256 then
+ der_signature = signer:sign(message, evp.CONST.SHA256_DIGEST)
+ elseif alg == str_const.ES512 then
+ der_signature = signer:sign(message, evp.CONST.SHA512_DIGEST)
+ end
+ -- Perform DER to RAW signature conversion
+ signature, err = signer:get_raw_sig(der_signature)
+ if not signature then
+ error({reason="signature error: " .. err})
+ end
+ else
+ error({reason="unsupported alg: " .. alg})
+ end
+ -- return full jwt string
+ return string_format(str_const.regex_join_msg, message , _M:jwt_encode(signature))
+
+end
+
+--@function load jwt
+--@param jwt string token
+--@param secret
+function _M.load_jwt(self, jwt_str, secret)
+ local success, ret = pcall(parse, self, secret, jwt_str)
+ if not success then
+ return {
+ valid=false,
+ verified=false,
+ reason=ret[str_const.reason] or str_const.invalid_jwt
+ }
+ end
+
+ local jwt_obj = ret
+ jwt_obj[str_const.verified] = false
+ jwt_obj[str_const.valid] = true
+ return jwt_obj
+end
+
+--@function verify jwe object
+--@param jwt object
+--@return jwt object with reason whether verified or not
+local function verify_jwe_obj(jwt_obj)
+
+ if jwt_obj[str_const.header][str_const.enc] ~= str_const.A256GCM then -- tag gets authenticated during decryption
+ local _, mac_key, _ = derive_keys(jwt_obj.header.enc, jwt_obj.internal.key)
+ local encoded_header = jwt_obj.internal.encoded_header
+
+ local encoded_header_length = binlen(encoded_header)
+ local mac_input = table_concat({encoded_header , jwt_obj.internal.iv, jwt_obj.internal.cipher_text,
+ encoded_header_length})
+ local mac = hmac_digest(jwt_obj.header.enc, mac_key, mac_input)
+ local auth_tag = string_sub(mac, 1, #mac/2)
+
+ if auth_tag ~= jwt_obj.signature then
+ jwt_obj[str_const.reason] = "signature mismatch: " ..
+ tostring(jwt_obj[str_const.signature])
+ end
+ end
+
+ jwt_obj.internal = nil
+ jwt_obj.signature = nil
+
+ if not jwt_obj[str_const.reason] then
+ jwt_obj[str_const.verified] = true
+ jwt_obj[str_const.reason] = str_const.everything_awesome
+ end
+
+ return jwt_obj
+end
+
+--@function extract certificate
+--@param jwt object
+--@return decoded certificate
+local function extract_certificate(jwt_obj, x5u_content_retriever)
+ local x5c = jwt_obj[str_const.header][str_const.x5c]
+ if x5c ~= nil and x5c[1] ~= nil then
+ -- TODO Might want to add support for intermediaries that we
+ -- don't have in our trusted chain (items 2... if present)
+
+ local cert_str = ngx_decode_base64(x5c[1])
+ if not cert_str then
+ jwt_obj[str_const.reason] = "Malformed x5c header"
+ end
+
+ return cert_str
+ end
+
+ local x5u = jwt_obj[str_const.header][str_const.x5u]
+ if x5u ~= nil then
+ -- TODO Ensure the url starts with https://
+ -- cf. https://tools.ietf.org/html/rfc7517#section-4.6
+
+ if x5u_content_retriever == nil then
+ jwt_obj[str_const.reason] = "No function has been provided to retrieve the content pointed at by the 'x5u'."
+ return nil
+ end
+
+ -- TODO Maybe validate the url against an optional list whitelisted url prefixes?
+ -- cf. https://news.ycombinator.com/item?id=9302394
+
+ local iss = jwt_obj[str_const.payload][str_const.iss]
+ local kid = jwt_obj[str_const.header][str_const.kid]
+ local success, ret = pcall(x5u_content_retriever, x5u, iss, kid)
+
+ if not success then
+ jwt_obj[str_const.reason] = "An error occured while invoking the x5u_content_retriever function."
+ return nil
+ end
+
+ return ret
+ end
+
+ -- TODO When both x5c and x5u are defined, the implementation should
+ -- ensure their content match
+ -- cf. https://tools.ietf.org/html/rfc7517#section-4.6
+
+ jwt_obj[str_const.reason] = "Unsupported RS256 key model"
+ return nil
+ -- TODO - Implement jwk and kid based models...
+end
+
+local function get_claim_spec_from_legacy_options(self, options)
+ local claim_spec = { }
+ local jwt_validators = require "resty.jwt-validators"
+
+ if options[str_const.valid_issuers] ~= nil then
+ claim_spec[str_const.iss] = jwt_validators.equals_any_of(options[str_const.valid_issuers])
+ end
+
+ if options[str_const.lifetime_grace_period] ~= nil then
+ jwt_validators.set_system_leeway(options[str_const.lifetime_grace_period] or 0)
+
+ -- If we have a leeway set, then either an NBF or an EXP should also exist requireds are added below
+ if options[str_const.require_nbf_claim] ~= true and options[str_const.require_exp_claim] ~= true then
+ claim_spec[str_const.full_obj] = jwt_validators.require_one_of({ str_const.nbf, str_const.exp })
+ end
+ end
+
+ if not is_nil_or_boolean(options[str_const.require_nbf_claim]) then
+ error(string.format("'%s' validation option is expected to be a boolean.", str_const.require_nbf_claim), 0)
+ end
+
+ if not is_nil_or_boolean(options[str_const.require_exp_claim]) then
+ error(string.format("'%s' validation option is expected to be a boolean.", str_const.require_exp_claim), 0)
+ end
+
+ if options[str_const.lifetime_grace_period] ~= nil or options[str_const.require_nbf_claim] ~= nil or options[str_const.require_exp_claim] ~= nil then
+ if options[str_const.require_nbf_claim] == true then
+ claim_spec[str_const.nbf] = jwt_validators.is_not_before()
+ else
+ claim_spec[str_const.nbf] = jwt_validators.opt_is_not_before()
+ end
+
+ if options[str_const.require_exp_claim] == true then
+ claim_spec[str_const.exp] = jwt_validators.is_not_expired()
+ else
+ claim_spec[str_const.exp] = jwt_validators.opt_is_not_expired()
+ end
+ end
+
+ return claim_spec
+end
+
+local function is_legacy_validation_options(options)
+
+ -- Validation options MUST be a table
+ if type(options) ~= str_const.table then
+ return false
+ end
+
+ -- Validation options MUST have at least one of these, and must ONLY have these
+ local legacy_options = { }
+ legacy_options[str_const.valid_issuers]=1
+ legacy_options[str_const.lifetime_grace_period]=1
+ legacy_options[str_const.require_nbf_claim]=1
+ legacy_options[str_const.require_exp_claim]=1
+
+ local is_legacy = false
+ for k in pairs(options) do
+ if legacy_options[k] ~= nil then
+ is_legacy = true
+ else
+ return false
+ end
+ end
+ return is_legacy
+end
+
+-- Validates the claims for the given (parsed) object
+local function validate_claims(self, jwt_obj, ...)
+ local claim_specs = {...}
+ if #claim_specs == 0 then
+ table.insert(claim_specs, _M:get_default_validation_options(jwt_obj))
+ end
+
+ if jwt_obj[str_const.reason] ~= nil then
+ return false
+ end
+
+ -- Encode the current jwt_obj and use it when calling the individual validation functions
+ local jwt_json = cjson_encode(jwt_obj)
+
+ -- Validate all our specs
+ for _, claim_spec in ipairs(claim_specs) do
+ if is_legacy_validation_options(claim_spec) then
+ claim_spec = get_claim_spec_from_legacy_options(self, claim_spec)
+ end
+ for claim, fx in pairs(claim_spec) do
+ if type(fx) ~= str_const.funct then
+ error("Claim spec value must be a function - see jwt-validators.lua for helper functions", 0)
+ end
+
+ local val = claim == str_const.full_obj and cjson_decode(jwt_json) or jwt_obj.payload[claim]
+ local success, ret = pcall(fx, val, claim, jwt_json)
+ if not success then
+ jwt_obj[str_const.reason] = ret.reason or string.gsub(ret, "^.-:%d-: ", "")
+ return false
+ elseif ret == false then
+ jwt_obj[str_const.reason] = string.format("Claim '%s' ('%s') returned failure", claim, val)
+ return false
+ end
+ end
+ end
+
+ -- Everything was good
+ return true
+end
+
+--@function verify jwt object
+--@param secret
+--@param jwt_object
+--@leeway
+--@return verified jwt payload or jwt object with error code
+function _M.verify_jwt_obj(self, secret, jwt_obj, ...)
+ if not jwt_obj.valid then
+ return jwt_obj
+ end
+
+ -- validate any claims that have been passed in
+ if not validate_claims(self, jwt_obj, ...) then
+ return jwt_obj
+ end
+
+ -- if jwe, invoked verify jwe
+ if jwt_obj[str_const.header][str_const.enc] then
+ return verify_jwe_obj(jwt_obj)
+ end
+
+ local alg = jwt_obj[str_const.header][str_const.alg]
+
+ local jwt_str = string_format(str_const.regex_jwt_join_str, jwt_obj.raw_header , jwt_obj.raw_payload , jwt_obj.signature)
+
+ if self.alg_whitelist ~= nil then
+ if self.alg_whitelist[alg] == nil then
+ return {verified=false, reason="whitelist unsupported alg: " .. alg}
+ end
+ end
+
+ if alg == str_const.HS256 or alg == str_const.HS512 then
+ local success, ret = pcall(_M.sign, self, secret, jwt_obj)
+ if not success then
+ -- syntax check
+ jwt_obj[str_const.reason] = ret[str_const.reason] or str_const.internal_error
+ elseif jwt_str ~= ret then
+ -- signature check
+ jwt_obj[str_const.reason] = "signature mismatch: " .. jwt_obj[str_const.signature]
+ end
+ elseif alg == str_const.RS256 or alg == str_const.RS512 or alg == str_const.ES256 or alg == str_const.ES512 then
+ local cert, err
+ if self.trusted_certs_file ~= nil then
+ local cert_str = extract_certificate(jwt_obj, self.x5u_content_retriever)
+ if not cert_str then
+ return jwt_obj
+ end
+ cert, err = evp.Cert:new(cert_str)
+ if not cert then
+ jwt_obj[str_const.reason] = "Unable to extract signing cert from JWT: " .. err
+ return jwt_obj
+ end
+ -- Try validating against trusted CA's, then a cert passed as secret
+ local trusted = cert:verify_trust(self.trusted_certs_file)
+ if not trusted then
+ jwt_obj[str_const.reason] = "Cert used to sign the JWT isn't trusted: " .. err
+ return jwt_obj
+ end
+ elseif secret ~= nil then
+ if secret:find("CERTIFICATE") then
+ cert, err = evp.Cert:new(secret)
+ elseif secret:find("PUBLIC KEY") then
+ cert, err = evp.PublicKey:new(secret)
+ end
+ if not cert then
+ jwt_obj[str_const.reason] = "Decode secret is not a valid cert/public key"
+ return jwt_obj
+ end
+ else
+ jwt_obj[str_const.reason] = "No trusted certs loaded"
+ return jwt_obj
+ end
+ local verifier = ''
+ if alg == str_const.RS256 or alg == str_const.RS512 then
+ verifier = evp.RSAVerifier:new(cert)
+ elseif alg == str_const.ES256 or alg == str_const.ES512 then
+ verifier = evp.ECVerifier:new(cert)
+ end
+ if not verifier then
+ -- Internal error case, should not happen...
+ jwt_obj[str_const.reason] = "Failed to build verifier " .. err
+ return jwt_obj
+ end
+
+ -- assemble jwt parts
+ local raw_header = get_raw_part(str_const.header, jwt_obj)
+ local raw_payload = get_raw_part(str_const.payload, jwt_obj)
+
+ local message =string_format(str_const.regex_join_msg, raw_header , raw_payload)
+ local sig = _M:jwt_decode(jwt_obj[str_const.signature], false)
+
+ if not sig then
+ jwt_obj[str_const.reason] = "Wrongly encoded signature"
+ return jwt_obj
+ end
+
+ local verified = false
+ err = "verify error: reason unknown"
+
+ if alg == str_const.RS256 or alg == str_const.ES256 then
+ verified, err = verifier:verify(message, sig, evp.CONST.SHA256_DIGEST)
+ elseif alg == str_const.RS512 or alg == str_const.ES512 then
+ verified, err = verifier:verify(message, sig, evp.CONST.SHA512_DIGEST)
+ end
+ if not verified then
+ jwt_obj[str_const.reason] = err
+ end
+ else
+ jwt_obj[str_const.reason] = "Unsupported algorithm " .. alg
+ end
+
+ if not jwt_obj[str_const.reason] then
+ jwt_obj[str_const.verified] = true
+ jwt_obj[str_const.reason] = str_const.everything_awesome
+ end
+ return jwt_obj
+
+end
+
+
+function _M.verify(self, secret, jwt_str, ...)
+ local jwt_obj = _M.load_jwt(self, jwt_str, secret)
+ if not jwt_obj.valid then
+ return {verified=false, reason=jwt_obj[str_const.reason]}
+ end
+ return _M.verify_jwt_obj(self, secret, jwt_obj, ...)
+
+end
+
+function _M.set_payload_encoder(self, encoder)
+ if type(encoder) ~= "function" then
+ error({reason="payload encoder must be function"})
+ end
+ self.payload_encoder = encoder
+end
+
+
+function _M.set_payload_decoder(self, decoder)
+ if type(decoder) ~= "function" then
+ error({reason="payload decoder must be function"})
+ end
+ self.payload_decoder= decoder
+end
+
+
+function _M.new()
+ return setmetatable({}, mt)
+end
+
+return _M
diff --git a/server/resty/openidc.lua b/server/resty/openidc.lua
new file mode 100644
index 0000000..246414e
--- /dev/null
+++ b/server/resty/openidc.lua
@@ -0,0 +1,1870 @@
+--[[
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you 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
+
+ http://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.
+
+***************************************************************************
+Copyright (C) 2017-2019 ZmartZone IAM
+Copyright (C) 2015-2017 Ping Identity Corporation
+All rights reserved.
+
+For further information please contact:
+
+ Ping Identity Corporation
+ 1099 18th St Suite 2950
+ Denver, CO 80202
+ 303.468.2900
+ http://www.pingidentity.com
+
+DISCLAIMER OF WARRANTIES:
+
+THE SOFTWARE PROVIDED HEREUNDER IS PROVIDED ON AN "AS IS" BASIS, WITHOUT
+ANY WARRANTIES OR REPRESENTATIONS EXPRESS, IMPLIED OR STATUTORY; INCLUDING,
+WITHOUT LIMITATION, WARRANTIES OF QUALITY, PERFORMANCE, NONINFRINGEMENT,
+MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. NOR ARE THERE ANY
+WARRANTIES CREATED BY A COURSE OR DEALING, COURSE OF PERFORMANCE OR TRADE
+USAGE. FURTHERMORE, THERE ARE NO WARRANTIES THAT THE SOFTWARE WILL MEET
+YOUR NEEDS OR BE FREE FROM ERRORS, OR THAT THE OPERATION OF THE SOFTWARE
+WILL BE UNINTERRUPTED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR
+CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+EXEMPLARY, OR CONSEQUENTIAL DAMAGES HOWEVER CAUSED AND ON ANY THEORY OF
+LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+@Author: Hans Zandbelt - hans.zandbelt@zmartzone.eu
+--]]
+
+local require = require
+local cjson = require("cjson")
+local cjson_s = require("cjson.safe")
+local http = require("resty.http")
+local r_session = require("resty.session")
+local string = string
+local ipairs = ipairs
+local pairs = pairs
+local type = type
+local ngx = ngx
+local b64 = ngx.encode_base64
+local unb64 = ngx.decode_base64
+
+local log = ngx.log
+local DEBUG = ngx.DEBUG
+local ERROR = ngx.ERR
+local WARN = ngx.WARN
+
+local function token_auth_method_precondition(method, required_field)
+ return function(opts)
+ if not opts[required_field] then
+ log(DEBUG, "Can't use " .. method .. " without opts." .. required_field)
+ return false
+ end
+ return true
+ end
+end
+
+local supported_token_auth_methods = {
+ client_secret_basic = true,
+ client_secret_post = true,
+ private_key_jwt = token_auth_method_precondition('private_key_jwt', 'client_rsa_private_key'),
+ client_secret_jwt = token_auth_method_precondition('client_secret_jwt', 'client_secret')
+}
+
+local openidc = {
+ _VERSION = "1.7.5"
+}
+openidc.__index = openidc
+
+local function store_in_session(opts, feature)
+ -- We don't have a whitelist of features to enable
+ if not opts.session_contents then
+ return true
+ end
+
+ return opts.session_contents[feature]
+end
+
+-- set value in server-wide cache if available
+local function openidc_cache_set(type, key, value, exp)
+ local dict = ngx.shared[type]
+ if dict and (exp > 0) then
+ local success, err, forcible = dict:set(key, value, exp)
+ log(DEBUG, "cache set: success=", success, " err=", err, " forcible=", forcible)
+ end
+end
+
+-- retrieve value from server-wide cache if available
+local function openidc_cache_get(type, key)
+ local dict = ngx.shared[type]
+ local value
+ if dict then
+ value = dict:get(key)
+ if value then log(DEBUG, "cache hit: type=", type, " key=", key) end
+ end
+ return value
+end
+
+-- invalidate values of server-wide cache
+local function openidc_cache_invalidate(type)
+ local dict = ngx.shared[type]
+ if dict then
+ log(DEBUG, "flushing cache for " .. type)
+ dict.flush_all(dict)
+ local nbr = dict.flush_expired(dict)
+ end
+end
+
+-- invalidate all server-wide caches
+function openidc.invalidate_caches()
+ openidc_cache_invalidate("discovery")
+ openidc_cache_invalidate("jwks")
+ openidc_cache_invalidate("introspection")
+ openidc_cache_invalidate("jwt_verification")
+end
+
+-- validate the contents of and id_token
+local function openidc_validate_id_token(opts, id_token, nonce)
+
+ -- check issuer
+ if opts.discovery.issuer ~= id_token.iss then
+ log(ERROR, "issuer \"", id_token.iss, "\" in id_token is not equal to the issuer from the discovery document \"", opts.discovery.issuer, "\"")
+ return false
+ end
+
+ -- check sub
+ if not id_token.sub then
+ log(ERROR, "no \"sub\" claim found in id_token")
+ return false
+ end
+
+ -- check nonce
+ if nonce and nonce ~= id_token.nonce then
+ log(ERROR, "nonce \"", id_token.nonce, "\" in id_token is not equal to the nonce that was sent in the request \"", nonce, "\"")
+ return false
+ end
+
+ -- check issued-at timestamp
+ if not id_token.iat then
+ log(ERROR, "no \"iat\" claim found in id_token")
+ return false
+ end
+
+ local slack = opts.iat_slack and opts.iat_slack or 120
+ if id_token.iat > (ngx.time() + slack) then
+ log(ERROR, "id_token not yet valid: id_token.iat=", id_token.iat, ", ngx.time()=", ngx.time(), ", slack=", slack)
+ return false
+ end
+
+ -- check expiry timestamp
+ if not id_token.exp then
+ log(ERROR, "no \"exp\" claim found in id_token")
+ return false
+ end
+
+ if (id_token.exp + slack) < ngx.time() then
+ log(ERROR, "token expired: id_token.exp=", id_token.exp, ", ngx.time()=", ngx.time())
+ return false
+ end
+
+ -- check audience (array or string)
+ if not id_token.aud then
+ log(ERROR, "no \"aud\" claim found in id_token")
+ return false
+ end
+
+ if (type(id_token.aud) == "table") then
+ for _, value in pairs(id_token.aud) do
+ if value == opts.client_id then
+ return true
+ end
+ end
+ log(ERROR, "no match found token audience array: client_id=", opts.client_id)
+ return false
+ elseif (type(id_token.aud) == "string") then
+ if id_token.aud ~= opts.client_id then
+ log(ERROR, "token audience does not match: id_token.aud=", id_token.aud, ", client_id=", opts.client_id)
+ return false
+ end
+ end
+ return true
+end
+
+local function get_first(table_or_string)
+ local res = table_or_string
+ if table_or_string and type(table_or_string) == 'table' then
+ res = table_or_string[1]
+ end
+ return res
+end
+
+local function get_first_header(headers, header_name)
+ local header = headers[header_name]
+ return get_first(header)
+end
+
+local function get_first_header_and_strip_whitespace(headers, header_name)
+ local header = get_first_header(headers, header_name)
+ return header and header:gsub('%s', '')
+end
+
+local function get_forwarded_parameter(headers, param_name)
+ local forwarded = get_first_header(headers, 'Forwarded')
+ local params = {}
+ if forwarded then
+ local function parse_parameter(pv)
+ local name, value = pv:match("^%s*([^=]+)%s*=%s*(.-)%s*$")
+ if name and value then
+ if value:sub(1, 1) == '"' then
+ value = value:sub(2, -2)
+ end
+ params[name:lower()] = value
+ end
+ end
+
+ -- this assumes there is no quoted comma inside the header's value
+ -- which should be fine as comma is not legal inside a node name,
+ -- a URI scheme or a host name. The only thing that might bite us
+ -- are extensions.
+ local first_part = forwarded
+ local first_comma = forwarded:find("%s*,%s*")
+ if first_comma then
+ first_part = forwarded:sub(1, first_comma - 1)
+ end
+ first_part:gsub("[^;]+", parse_parameter)
+ end
+ return params[param_name:gsub("^%s*(.-)%s*$", "%1"):lower()]
+end
+
+local function get_scheme(headers)
+ return get_forwarded_parameter(headers, 'proto')
+ or get_first_header_and_strip_whitespace(headers, 'X-Forwarded-Proto')
+ or ngx.var.scheme
+end
+
+local function get_host_name_from_x_header(headers)
+ local header = get_first_header_and_strip_whitespace(headers, 'X-Forwarded-Host')
+ return header and header:gsub('^([^,]+),?.*$', '%1')
+end
+
+local function get_host_name(headers)
+ return get_forwarded_parameter(headers, 'host')
+ or get_host_name_from_x_header(headers)
+ or ngx.var.http_host
+end
+
+-- assemble the redirect_uri
+local function openidc_get_redirect_uri(opts, session)
+ local path = opts.redirect_uri_path
+ if opts.redirect_uri then
+ if opts.redirect_uri:sub(1, 1) == '/' then
+ path = opts.redirect_uri
+ else
+ return opts.redirect_uri
+ end
+ end
+ local headers = ngx.req.get_headers()
+ local scheme = opts.redirect_uri_scheme or get_scheme(headers)
+ local host = get_host_name(headers)
+ if not host then
+ -- possibly HTTP 1.0 and no Host header
+ if session then session:close() end
+ ngx.exit(ngx.HTTP_BAD_REQUEST)
+ end
+ return scheme .. "://" .. host .. path
+end
+
+-- perform base64url decoding
+local function openidc_base64_url_decode(input)
+ local reminder = #input % 4
+ if reminder > 0 then
+ local padlen = 4 - reminder
+ input = input .. string.rep('=', padlen)
+ end
+ input = input:gsub('%-', '+'):gsub('_', '/')
+ return unb64(input)
+end
+
+-- perform base64url encoding
+local function openidc_base64_url_encode(input)
+ local output = b64(input, true)
+ return output:gsub('%+', '-'):gsub('/', '_')
+end
+
+local function openidc_combine_uri(uri, params)
+ if params == nil or next(params) == nil then
+ return uri
+ end
+ local sep = "?"
+ if string.find(uri, "?", 1, true) then
+ sep = "&"
+ end
+ return uri .. sep .. ngx.encode_args(params)
+end
+
+local function decorate_request(http_request_decorator, req)
+ return http_request_decorator and http_request_decorator(req) or req
+end
+
+local function openidc_s256(verifier)
+ local sha256 = (require 'resty.sha256'):new()
+ sha256:update(verifier)
+ return openidc_base64_url_encode(sha256:final())
+end
+
+-- send the browser of to the OP's authorization endpoint
+local function openidc_authorize(opts, session, target_url, prompt)
+ local resty_random = require("resty.random")
+ local resty_string = require("resty.string")
+ local err
+
+ -- generate state and nonce
+ local state = resty_string.to_hex(resty_random.bytes(16))
+ local nonce = (opts.use_nonce == nil or opts.use_nonce)
+ and resty_string.to_hex(resty_random.bytes(16))
+ local code_verifier = opts.use_pkce and openidc_base64_url_encode(resty_random.bytes(32))
+
+ -- assemble the parameters to the authentication request
+ local params = {
+ client_id = opts.client_id,
+ response_type = "code",
+ scope = opts.scope and opts.scope or "openid email profile",
+ redirect_uri = openidc_get_redirect_uri(opts, session),
+ state = state,
+ }
+
+ if nonce then
+ params.nonce = nonce
+ end
+
+ if prompt then
+ params.prompt = prompt
+ end
+
+ if opts.display then
+ params.display = opts.display
+ end
+
+ if code_verifier then
+ params.code_challenge_method = 'S256'
+ params.code_challenge = openidc_s256(code_verifier)
+ end
+
+ -- merge any provided extra parameters
+ if opts.authorization_params then
+ for k, v in pairs(opts.authorization_params) do params[k] = v end
+ end
+
+ -- store state in the session
+ session.data.original_url = target_url
+ session.data.state = state
+ session.data.nonce = nonce
+ session.data.code_verifier = code_verifier
+ session.data.last_authenticated = ngx.time()
+
+ if opts.lifecycle and opts.lifecycle.on_created then
+ err = opts.lifecycle.on_created(session)
+ if err then
+ log(WARN, "failed in `on_created` handler: " .. err)
+ return err
+ end
+ end
+
+ session:save()
+
+ -- redirect to the /authorization endpoint
+ ngx.header["Cache-Control"] = "no-cache, no-store, max-age=0"
+ return ngx.redirect(openidc_combine_uri(opts.discovery.authorization_endpoint, params))
+end
+
+-- parse the JSON result from a call to the OP
+local function openidc_parse_json_response(response, ignore_body_on_success)
+ local ignore_body_on_success = ignore_body_on_success or false
+
+ local err
+ local res
+
+ -- check the response from the OP
+ if response.status ~= 200 then
+ err = "response indicates failure, status=" .. response.status .. ", body=" .. response.body
+ else
+ if ignore_body_on_success then
+ return nil, nil
+ end
+
+ -- decode the response and extract the JSON object
+ res = cjson_s.decode(response.body)
+
+ if not res then
+ err = "JSON decoding failed"
+ end
+ end
+
+ return res, err
+end
+
+local function openidc_configure_timeouts(httpc, timeout)
+ if timeout then
+ if type(timeout) == "table" then
+ local r, e = httpc:set_timeouts(timeout.connect or 0, timeout.send or 0, timeout.read or 0)
+ else
+ local r, e = httpc:set_timeout(timeout)
+ end
+ end
+end
+
+-- Set outgoing proxy options
+local function openidc_configure_proxy(httpc, proxy_opts)
+ if httpc and proxy_opts and type(proxy_opts) == "table" then
+ log(DEBUG, "openidc_configure_proxy : use http proxy")
+ httpc:set_proxy_options(proxy_opts)
+ else
+ log(DEBUG, "openidc_configure_proxy : don't use http proxy")
+ end
+end
+
+-- make a call to the token endpoint
+function openidc.call_token_endpoint(opts, endpoint, body, auth, endpoint_name, ignore_body_on_success)
+ local ignore_body_on_success = ignore_body_on_success or false
+
+ local ep_name = endpoint_name or 'token'
+ if not endpoint then
+ return nil, 'no endpoint URI for ' .. ep_name
+ end
+
+ local headers = {
+ ["Content-Type"] = "application/x-www-form-urlencoded"
+ }
+
+ if auth then
+ if auth == "client_secret_basic" then
+ if opts.client_secret then
+ headers.Authorization = "Basic " .. b64(ngx.escape_uri(opts.client_id) .. ":" .. ngx.escape_uri(opts.client_secret))
+ else
+ -- client_secret must not be set if Windows Integrated Authentication (WIA) is used with
+ -- Active Directory Federation Services (AD FS) 4.0 (or newer) on Windows Server 2016 (or newer)
+ headers.Authorization = "Basic " .. b64(ngx.escape_uri(opts.client_id) .. ":")
+ end
+ log(DEBUG, "client_secret_basic: authorization header '" .. headers.Authorization .. "'")
+
+ elseif auth == "client_secret_post" then
+ body.client_id = opts.client_id
+ if opts.client_secret then
+ body.client_secret = opts.client_secret
+ end
+ log(DEBUG, "client_secret_post: client_id and client_secret being sent in POST body")
+
+ elseif auth == "private_key_jwt" or auth == "client_secret_jwt" then
+ local key = auth == "private_key_jwt" and opts.client_rsa_private_key or opts.client_secret
+ if not key then
+ return nil, "Can't use " .. auth .. " without a key."
+ end
+ body.client_id = opts.client_id
+ body.client_assertion_type = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"
+ local now = ngx.time()
+ local assertion = {
+ header = {
+ typ = "JWT",
+ alg = auth == "private_key_jwt" and "RS256" or "HS256",
+ },
+ payload = {
+ iss = opts.client_id,
+ sub = opts.client_id,
+ aud = endpoint,
+ jti = ngx.var.request_id,
+ exp = now + (opts.client_jwt_assertion_expires_in and opts.client_jwt_assertion_expires_in or 60),
+ iat = now
+ }
+ }
+ if auth == "private_key_jwt" then
+ assertion.header.kid = opts.client_rsa_private_key_id
+ end
+
+ local r_jwt = require("resty.jwt")
+ body.client_assertion = r_jwt:sign(key, assertion)
+ log(DEBUG, auth .. ": client_id, client_assertion_type and client_assertion being sent in POST body")
+ end
+ end
+
+ local pass_cookies = opts.pass_cookies
+ if pass_cookies then
+ if ngx.req.get_headers()["Cookie"] then
+ local t = {}
+ for cookie_name in string.gmatch(pass_cookies, "%S+") do
+ local cookie_value = ngx.var["cookie_" .. cookie_name]
+ if cookie_value then
+ table.insert(t, cookie_name .. "=" .. cookie_value)
+ end
+ end
+ headers.Cookie = table.concat(t, "; ")
+ end
+ end
+
+ log(DEBUG, "request body for " .. ep_name .. " endpoint call: ", ngx.encode_args(body))
+
+ local httpc = http.new()
+ openidc_configure_timeouts(httpc, opts.timeout)
+ openidc_configure_proxy(httpc, opts.proxy_opts)
+ local res, err = httpc:request_uri(endpoint, decorate_request(opts.http_request_decorator, {
+ method = "POST",
+ body = ngx.encode_args(body),
+ headers = headers,
+ ssl_verify = (opts.ssl_verify ~= "no"),
+ keepalive = (opts.keepalive ~= "no")
+ }))
+ if not res then
+ err = "accessing " .. ep_name .. " endpoint (" .. endpoint .. ") failed: " .. err
+ log(ERROR, err)
+ return nil, err
+ end
+
+ log(DEBUG, ep_name .. " endpoint response: ", res.body)
+
+ return openidc_parse_json_response(res, ignore_body_on_success)
+end
+
+-- computes access_token expires_in value (in seconds)
+local function openidc_access_token_expires_in(opts, expires_in)
+ return (expires_in or opts.access_token_expires_in or 3600) - 1 - (opts.access_token_expires_leeway or 0)
+end
+
+local function openidc_load_jwt_none_alg(enc_hdr, enc_payload)
+ local header = cjson_s.decode(openidc_base64_url_decode(enc_hdr))
+ local payload = cjson_s.decode(openidc_base64_url_decode(enc_payload))
+ if header and payload and header.alg == "none" then
+ return {
+ raw_header = enc_hdr,
+ raw_payload = enc_payload,
+ header = header,
+ payload = payload,
+ signature = ''
+ }
+ end
+ return nil
+end
+
+-- get the Discovery metadata from the specified URL
+local function openidc_discover(url, ssl_verify, keepalive, timeout, exptime, proxy_opts, http_request_decorator)
+ log(DEBUG, "openidc_discover: URL is: " .. url)
+
+ local json, err
+ local v = openidc_cache_get("discovery", url)
+ if not v then
+
+ log(DEBUG, "discovery data not in cache, making call to discovery endpoint")
+ -- make the call to the discovery endpoint
+ local httpc = http.new()
+ openidc_configure_timeouts(httpc, timeout)
+ openidc_configure_proxy(httpc, proxy_opts)
+ local res, error = httpc:request_uri(url, decorate_request(http_request_decorator, {
+ ssl_verify = (ssl_verify ~= "no"),
+ keepalive = (keepalive ~= "no")
+ }))
+ if not res then
+ err = "accessing discovery url (" .. url .. ") failed: " .. error
+ log(ERROR, err)
+ else
+ log(DEBUG, "response data: " .. res.body)
+ json, err = openidc_parse_json_response(res)
+ if json then
+ openidc_cache_set("discovery", url, cjson.encode(json), exptime or 24 * 60 * 60)
+ else
+ err = "could not decode JSON from Discovery data" .. (err and (": " .. err) or '')
+ log(ERROR, err)
+ end
+ end
+
+ else
+ json = cjson.decode(v)
+ end
+
+ return json, err
+end
+
+-- turn a discovery url set in the opts dictionary into the discovered information
+local function openidc_ensure_discovered_data(opts)
+ local err
+ if type(opts.discovery) == "string" then
+ local discovery
+ discovery, err = openidc_discover(opts.discovery, opts.ssl_verify, opts.keepalive, opts.timeout, opts.discovery_expires_in, opts.proxy_opts,
+ opts.http_request_decorator)
+ if not err then
+ opts.discovery = discovery
+ end
+ end
+ return err
+end
+
+-- make a call to the userinfo endpoint
+function openidc.call_userinfo_endpoint(opts, access_token)
+ local err = openidc_ensure_discovered_data(opts)
+ if err then
+ return nil, err
+ end
+ if not (opts and opts.discovery and opts.discovery.userinfo_endpoint) then
+ log(DEBUG, "no userinfo endpoint supplied")
+ return nil, nil
+ end
+
+ local headers = {
+ ["Authorization"] = "Bearer " .. access_token,
+ }
+
+ log(DEBUG, "authorization header '" .. headers.Authorization .. "'")
+
+ local httpc = http.new()
+ openidc_configure_timeouts(httpc, opts.timeout)
+ openidc_configure_proxy(httpc, opts.proxy_opts)
+ local res, err = httpc:request_uri(opts.discovery.userinfo_endpoint,
+ decorate_request(opts.http_request_decorator, {
+ headers = headers,
+ ssl_verify = (opts.ssl_verify ~= "no"),
+ keepalive = (opts.keepalive ~= "no")
+ }))
+ if not res then
+ err = "accessing (" .. opts.discovery.userinfo_endpoint .. ") failed: " .. err
+ return nil, err
+ end
+
+ log(DEBUG, "userinfo response: ", res.body)
+
+ -- parse the response from the user info endpoint
+ return openidc_parse_json_response(res)
+end
+
+local function can_use_token_auth_method(method, opts)
+ local supported = supported_token_auth_methods[method]
+ return supported and (type(supported) ~= 'function' or supported(opts))
+end
+
+-- get the token endpoint authentication method
+local function openidc_get_token_auth_method(opts)
+
+ if opts.token_endpoint_auth_method ~= nil and not can_use_token_auth_method(opts.token_endpoint_auth_method, opts) then
+ log(ERROR, "configured value for token_endpoint_auth_method (" .. opts.token_endpoint_auth_method .. ") is not supported, ignoring it")
+ opts.token_endpoint_auth_method = nil
+ end
+
+ local result
+ if opts.discovery.token_endpoint_auth_methods_supported ~= nil then
+ -- if set check to make sure the discovery data includes the selected client auth method
+ if opts.token_endpoint_auth_method ~= nil then
+ for index, value in ipairs(opts.discovery.token_endpoint_auth_methods_supported) do
+ log(DEBUG, index .. " => " .. value)
+ if value == opts.token_endpoint_auth_method then
+ log(DEBUG, "configured value for token_endpoint_auth_method (" .. opts.token_endpoint_auth_method .. ") found in token_endpoint_auth_methods_supported in metadata")
+ result = opts.token_endpoint_auth_method
+ break
+ end
+ end
+ if result == nil then
+ log(ERROR, "configured value for token_endpoint_auth_method (" .. opts.token_endpoint_auth_method .. ") NOT found in token_endpoint_auth_methods_supported in metadata")
+ return nil
+ end
+ else
+ for index, value in ipairs(opts.discovery.token_endpoint_auth_methods_supported) do
+ log(DEBUG, index .. " => " .. value)
+ if can_use_token_auth_method(value, opts) then
+ result = value
+ log(DEBUG, "no configuration setting for option so select the first supported method specified by the OP: " .. result)
+ break
+ end
+ end
+ end
+ else
+ result = opts.token_endpoint_auth_method
+ end
+
+ -- set a sane default if auto-configuration failed
+ if result == nil then
+ result = "client_secret_basic"
+ end
+
+ log(DEBUG, "token_endpoint_auth_method result set to " .. result)
+
+ return result
+end
+
+-- ensure that discovery and token auth configuration is available in opts
+local function ensure_config(opts)
+ local err
+ err = openidc_ensure_discovered_data(opts)
+ if err then
+ return err
+ end
+
+ -- set the authentication method for the token endpoint
+ opts.token_endpoint_auth_method = openidc_get_token_auth_method(opts)
+end
+
+-- query for discovery endpoint data
+function openidc.get_discovery_doc(opts)
+ local err = openidc_ensure_discovered_data(opts)
+ if err then
+ log(ERROR, "error getting endpoints definition using discovery endpoint")
+ end
+
+ return opts.discovery, err
+end
+
+local function openidc_jwks(url, force, ssl_verify, keepalive, timeout, exptime, proxy_opts, http_request_decorator)
+ log(DEBUG, "openidc_jwks: URL is: " .. url .. " (force=" .. force .. ") (decorator=" .. (http_request_decorator and type(http_request_decorator) or "nil"))
+
+ local json, err, v
+
+ if force == 0 then
+ v = openidc_cache_get("jwks", url)
+ end
+
+ if not v then
+
+ log(DEBUG, "cannot use cached JWKS data; making call to jwks endpoint")
+ -- make the call to the jwks endpoint
+ local httpc = http.new()
+ openidc_configure_timeouts(httpc, timeout)
+ openidc_configure_proxy(httpc, proxy_opts)
+ local res, error = httpc:request_uri(url, decorate_request(http_request_decorator, {
+ ssl_verify = (ssl_verify ~= "no"),
+ keepalive = (keepalive ~= "no")
+ }))
+ if not res then
+ err = "accessing jwks url (" .. url .. ") failed: " .. error
+ log(ERROR, err)
+ else
+ log(DEBUG, "response data: " .. res.body)
+ json, err = openidc_parse_json_response(res)
+ if json then
+ openidc_cache_set("jwks", url, cjson.encode(json), exptime or 24 * 60 * 60)
+ end
+ end
+
+ else
+ json = cjson.decode(v)
+ end
+
+ return json, err
+end
+
+local function split_by_chunk(text, chunkSize)
+ local s = {}
+ for i = 1, #text, chunkSize do
+ s[#s + 1] = text:sub(i, i + chunkSize - 1)
+ end
+ return s
+end
+
+local function get_jwk(keys, kid)
+
+ local rsa_keys = {}
+ for _, value in pairs(keys) do
+ if value.kty == "RSA" and (not value.use or value.use == "sig") then
+ table.insert(rsa_keys, value)
+ end
+ end
+
+ if kid == nil then
+ if #rsa_keys == 1 then
+ log(DEBUG, "returning only RSA key of JWKS for keyid-less JWT")
+ return rsa_keys[1], nil
+ else
+ return nil, "JWT doesn't specify kid but the keystore contains multiple RSA keys"
+ end
+ end
+ for _, value in pairs(rsa_keys) do
+ if value.kid == kid then
+ return value, nil
+ end
+ end
+
+ return nil, "RSA key with id " .. kid .. " not found"
+end
+
+local wrap = ('.'):rep(64)
+
+local envelope = "-----BEGIN %s-----\n%s\n-----END %s-----\n"
+
+local function der2pem(data, typ)
+ typ = typ:upper() or "CERTIFICATE"
+ data = b64(data)
+ return string.format(envelope, typ, data:gsub(wrap, '%0\n', (#data - 1) / 64), typ)
+end
+
+
+local function encode_length(length)
+ if length < 0x80 then
+ return string.char(length)
+ elseif length < 0x100 then
+ return string.char(0x81, length)
+ elseif length < 0x10000 then
+ return string.char(0x82, math.floor(length / 0x100), length % 0x100)
+ end
+ error("Can't encode lengths over 65535")
+end
+
+
+local function encode_sequence(array, of)
+ local encoded_array = array
+ if of then
+ encoded_array = {}
+ for i = 1, #array do
+ encoded_array[i] = of(array[i])
+ end
+ end
+ encoded_array = table.concat(encoded_array)
+
+ return string.char(0x30) .. encode_length(#encoded_array) .. encoded_array
+end
+
+local function encode_binary_integer(bytes)
+ if bytes:byte(1) > 127 then
+ -- We currenly only use this for unsigned integers,
+ -- however since the high bit is set here, it would look
+ -- like a negative signed int, so prefix with zeroes
+ bytes = "\0" .. bytes
+ end
+ return "\2" .. encode_length(#bytes) .. bytes
+end
+
+local function encode_sequence_of_integer(array)
+ return encode_sequence(array, encode_binary_integer)
+end
+
+local function encode_bit_string(array)
+ local s = "\0" .. array -- first octet holds the number of unused bits
+ return "\3" .. encode_length(#s) .. s
+end
+
+local function openidc_pem_from_x5c(x5c)
+ log(DEBUG, "Found x5c, getting PEM public key from x5c entry of json public key")
+ local chunks = split_by_chunk(b64(openidc_base64_url_decode(x5c[1])), 64)
+ local pem = "-----BEGIN CERTIFICATE-----\n" ..
+ table.concat(chunks, "\n") ..
+ "\n-----END CERTIFICATE-----"
+ log(DEBUG, "Generated PEM key from x5c:", pem)
+ return pem
+end
+
+local function openidc_pem_from_rsa_n_and_e(n, e)
+ log(DEBUG, "getting PEM public key from n and e parameters of json public key")
+
+ local der_key = {
+ openidc_base64_url_decode(n), openidc_base64_url_decode(e)
+ }
+ local encoded_key = encode_sequence_of_integer(der_key)
+ local pem = der2pem(encode_sequence({
+ encode_sequence({
+ "\6\9\42\134\72\134\247\13\1\1\1" -- OID :rsaEncryption
+ .. "\5\0" -- ASN.1 NULL of length 0
+ }),
+ encode_bit_string(encoded_key)
+ }), "PUBLIC KEY")
+ log(DEBUG, "Generated pem key from n and e: ", pem)
+ return pem
+end
+
+local function openidc_pem_from_jwk(opts, kid)
+ local err = openidc_ensure_discovered_data(opts)
+ if err then
+ return nil, err
+ end
+
+ if not opts.discovery.jwks_uri or not (type(opts.discovery.jwks_uri) == "string") or (opts.discovery.jwks_uri == "") then
+ return nil, "opts.discovery.jwks_uri is not present or not a string"
+ end
+
+ local cache_id = opts.discovery.jwks_uri .. '#' .. (kid or '')
+ local v = openidc_cache_get("jwks", cache_id)
+
+ if v then
+ return v
+ end
+
+ local jwk, jwks
+
+ for force = 0, 1 do
+ jwks, err = openidc_jwks(opts.discovery.jwks_uri, force, opts.ssl_verify, opts.keepalive, opts.timeout, opts.jwk_expires_in, opts.proxy_opts,
+ opts.http_request_decorator)
+ if err then
+ return nil, err
+ end
+
+ jwk, err = get_jwk(jwks.keys, kid)
+
+ if jwk and not err then
+ break
+ end
+ end
+
+ if err then
+ return nil, err
+ end
+
+ local x5c = jwk.x5c
+ if x5c and #(jwk.x5c) == 0 then
+ log(WARN, "Found invalid JWK with empty x5c array, ignoring x5c claim")
+ x5c = nil
+ end
+
+ local pem
+ if x5c then
+ pem = openidc_pem_from_x5c(x5c)
+ elseif jwk.kty == "RSA" and jwk.n and jwk.e then
+ pem = openidc_pem_from_rsa_n_and_e(jwk.n, jwk.e)
+ else
+ return nil, "don't know how to create RSA key/cert for " .. cjson.encode(jwk)
+ end
+
+ openidc_cache_set("jwks", cache_id, pem, opts.jwk_expires_in or 24 * 60 * 60)
+ return pem
+end
+
+-- does lua-resty-jwt and/or we know how to handle the algorithm of the JWT?
+local function is_algorithm_supported(jwt_header)
+ return jwt_header and jwt_header.alg and (jwt_header.alg == "none"
+ or string.sub(jwt_header.alg, 1, 2) == "RS"
+ or string.sub(jwt_header.alg, 1, 2) == "HS")
+end
+
+-- is the JWT signing algorithm an asymmetric one whose key might be
+-- obtained from the discovery endpoint?
+local function uses_asymmetric_algorithm(jwt_header)
+ return string.sub(jwt_header.alg, 1, 2) == "RS"
+end
+
+-- is the JWT signing algorithm one that has been expected?
+local function is_algorithm_expected(jwt_header, expected_algs)
+ if expected_algs == nil or not jwt_header or not jwt_header.alg then
+ return true
+ end
+ if type(expected_algs) == 'string' then
+ expected_algs = { expected_algs }
+ end
+ for _, alg in ipairs(expected_algs) do
+ if alg == jwt_header.alg then
+ return true
+ end
+ end
+ return false
+end
+
+-- parse a JWT and verify its signature (if present)
+local function openidc_load_jwt_and_verify_crypto(opts, jwt_string, asymmetric_secret,
+symmetric_secret, expected_algs, ...)
+ local r_jwt = require("resty.jwt")
+ local enc_hdr, enc_payload, enc_sign = string.match(jwt_string, '^(.+)%.(.+)%.(.*)$')
+ if enc_payload and (not enc_sign or enc_sign == "") then
+ local jwt = openidc_load_jwt_none_alg(enc_hdr, enc_payload)
+ if jwt then
+ if opts.accept_none_alg then
+ log(DEBUG, "accept JWT with alg \"none\" and no signature")
+ return jwt
+ else
+ return jwt, "token uses \"none\" alg but accept_none_alg is not enabled"
+ end
+ end -- otherwise the JWT is invalid and load_jwt produces an error
+ end
+
+ local jwt_obj = r_jwt:load_jwt(jwt_string, nil)
+ if not jwt_obj.valid then
+ local reason = "invalid jwt"
+ if jwt_obj.reason then
+ reason = reason .. ": " .. jwt_obj.reason
+ end
+ return nil, reason
+ end
+
+ if not is_algorithm_expected(jwt_obj.header, expected_algs) then
+ local alg = jwt_obj.header and jwt_obj.header.alg or "no algorithm at all"
+ return nil, "token is signed by unexpected algorithm \"" .. alg .. "\""
+ end
+
+ local secret
+ if is_algorithm_supported(jwt_obj.header) then
+ if uses_asymmetric_algorithm(jwt_obj.header) then
+ if opts.secret then
+ log(WARN, "using deprecated option `opts.secret` for asymmetric key; switch to `opts.public_key` instead")
+ end
+ secret = asymmetric_secret or opts.secret
+ if not secret and opts.discovery then
+ log(DEBUG, "using discovery to find key")
+ local err
+ secret, err = openidc_pem_from_jwk(opts, jwt_obj.header.kid)
+
+ if secret == nil then
+ log(ERROR, err)
+ return nil, err
+ end
+ end
+ else
+ if opts.secret then
+ log(WARN, "using deprecated option `opts.secret` for symmetric key; switch to `opts.symmetric_key` instead")
+ end
+ secret = symmetric_secret or opts.secret
+ end
+ end
+
+ if #{ ... } == 0 then
+ -- an empty list of claim specs makes lua-resty-jwt add default
+ -- validators for the exp and nbf claims if they are
+ -- present. These validators need to know the configured slack
+ -- value
+ local jwt_validators = require("resty.jwt-validators")
+ jwt_validators.set_system_leeway(opts.iat_slack and opts.iat_slack or 120)
+ end
+
+ jwt_obj = r_jwt:verify_jwt_obj(secret, jwt_obj, ...)
+ if jwt_obj then
+ log(DEBUG, "jwt: ", cjson.encode(jwt_obj), " ,valid: ", jwt_obj.valid, ", verified: ", jwt_obj.verified)
+ end
+ if not jwt_obj.verified then
+ local reason = "jwt signature verification failed"
+ if jwt_obj.reason then
+ reason = reason .. ": " .. jwt_obj.reason
+ end
+ return jwt_obj, reason
+ end
+ return jwt_obj
+end
+
+--
+-- Load and validate id token from the id_token properties of the token endpoint response
+-- Parameters :
+-- - opts the openidc module options
+-- - jwt_id_token the id_token from the id_token properties of the token endpoint response
+-- - session the current session
+-- Return the id_token, nil if valid
+-- Return nil, the error if invalid
+--
+local function openidc_load_and_validate_jwt_id_token(opts, jwt_id_token, session)
+
+ local jwt_obj, err = openidc_load_jwt_and_verify_crypto(opts, jwt_id_token, opts.public_key, opts.client_secret,
+ opts.discovery.id_token_signing_alg_values_supported)
+ if err then
+ local alg = (jwt_obj and jwt_obj.header and jwt_obj.header.alg) or ''
+ local is_unsupported_signature_error = jwt_obj and not jwt_obj.verified and not is_algorithm_supported(jwt_obj.header)
+ if is_unsupported_signature_error then
+ if opts.accept_unsupported_alg == nil or opts.accept_unsupported_alg then
+ log(WARN, "ignored id_token signature as algorithm '" .. alg .. "' is not supported")
+ else
+ err = "token is signed using algorithm \"" .. alg .. "\" which is not supported by lua-resty-jwt"
+ log(ERROR, err)
+ return nil, err
+ end
+ else
+ log(ERROR, "id_token '" .. alg .. "' signature verification failed")
+ return nil, err
+ end
+ end
+ local id_token = jwt_obj.payload
+
+ log(DEBUG, "id_token header: ", cjson.encode(jwt_obj.header))
+ log(DEBUG, "id_token payload: ", cjson.encode(jwt_obj.payload))
+
+ -- validate the id_token contents
+ if openidc_validate_id_token(opts, id_token, session.data.nonce) == false then
+ err = "id_token validation failed"
+ log(ERROR, err)
+ return nil, err
+ end
+
+ return id_token
+end
+
+-- handle a "code" authorization response from the OP
+local function openidc_authorization_response(opts, session)
+ local args = ngx.req.get_uri_args()
+ local err, log_err, client_err
+
+ if not args.code or not args.state then
+ err = "unhandled request to the redirect_uri: " .. ngx.var.request_uri
+ log(ERROR, err)
+ return nil, err, session.data.original_url, session
+ end
+
+ -- check that the state returned in the response against the session; prevents CSRF
+ if args.state ~= session.data.state then
+ log_err = "state from argument: " .. (args.state and args.state or "nil") .. " does not match state restored from session: " .. (session.data.state and session.data.state or "nil")
+ client_err = "state from argument does not match state restored from session"
+ log(ERROR, log_err)
+ return nil, client_err, session.data.original_url, session
+ end
+
+ err = ensure_config(opts)
+ if err then
+ return nil, err, session.data.original_url, session
+ end
+
+ -- check the iss if returned from the OP
+ if args.iss and args.iss ~= opts.discovery.issuer then
+ log_err = "iss from argument: " .. args.iss .. " does not match expected issuer: " .. opts.discovery.issuer
+ client_err = "iss from argument does not match expected issuer"
+ log(ERROR, log_err)
+ return nil, client_err, session.data.original_url, session
+ end
+
+ -- check the client_id if returned from the OP
+ if args.client_id and args.client_id ~= opts.client_id then
+ log_err = "client_id from argument: " .. args.client_id .. " does not match expected client_id: " .. opts.client_id
+ client_err = "client_id from argument does not match expected client_id"
+ log(ERROR, log_err)
+ return nil, client_err, session.data.original_url, session
+ end
+
+ -- assemble the parameters to the token endpoint
+ local body = {
+ grant_type = "authorization_code",
+ code = args.code,
+ redirect_uri = openidc_get_redirect_uri(opts, session),
+ state = session.data.state,
+ code_verifier = session.data.code_verifier
+ }
+
+ log(DEBUG, "Authentication with OP done -> Calling OP Token Endpoint to obtain tokens")
+
+ local current_time = ngx.time()
+ -- make the call to the token endpoint
+ local json
+ json, err = openidc.call_token_endpoint(opts, opts.discovery.token_endpoint, body, opts.token_endpoint_auth_method)
+ if err then
+ return nil, err, session.data.original_url, session
+ end
+
+ local id_token, err = openidc_load_and_validate_jwt_id_token(opts, json.id_token, session);
+ if err then
+ return nil, err, session.data.original_url, session
+ end
+
+ -- mark this sessions as authenticated
+ session.data.authenticated = true
+ -- clear state, nonce and code_verifier to protect against potential misuse
+ session.data.nonce = nil
+ session.data.state = nil
+ session.data.code_verifier = nil
+ if store_in_session(opts, 'id_token') then
+ session.data.id_token = id_token
+ end
+
+ if store_in_session(opts, 'user') then
+ -- call the user info endpoint
+ -- TODO: should this error be checked?
+ local user
+ user, err = openidc.call_userinfo_endpoint(opts, json.access_token)
+
+ if err then
+ log(ERROR, "error calling userinfo endpoint: " .. err)
+ elseif user then
+ if id_token.sub ~= user.sub then
+ err = "\"sub\" claim in id_token (\"" .. (id_token.sub or "null") .. "\") is not equal to the \"sub\" claim returned from the userinfo endpoint (\"" .. (user.sub or "null") .. "\")"
+ log(ERROR, err)
+ else
+ session.data.user = user
+ end
+ end
+ end
+
+ if store_in_session(opts, 'enc_id_token') then
+ session.data.enc_id_token = json.id_token
+ end
+
+ if store_in_session(opts, 'access_token') then
+ session.data.access_token = json.access_token
+ session.data.access_token_expiration = current_time
+ + openidc_access_token_expires_in(opts, json.expires_in)
+ if json.refresh_token ~= nil then
+ session.data.refresh_token = json.refresh_token
+ end
+ end
+
+ if opts.lifecycle and opts.lifecycle.on_authenticated then
+ err = opts.lifecycle.on_authenticated(session, id_token, json)
+ if err then
+ log(WARN, "failed in `on_authenticated` handler: " .. err)
+ return nil, err, session.data.original_url, session
+ end
+ end
+
+ -- save the session with the obtained id_token
+ session:save()
+
+ -- redirect to the URL that was accessed originally
+ log(DEBUG, "OIDC Authorization Code Flow completed -> Redirecting to original URL (" .. session.data.original_url .. ")")
+ ngx.redirect(session.data.original_url)
+ return nil, nil, session.data.original_url, session
+end
+
+-- token revocation (RFC 7009)
+local function openidc_revoke_token(opts, token_type_hint, token)
+ if not opts.discovery.revocation_endpoint then
+ log(DEBUG, "no revocation endpoint supplied. unable to revoke " .. token_type_hint .. ".")
+ return nil
+ end
+
+ local token_type_hint = token_type_hint or nil
+ local body = {
+ token = token
+ }
+ if token_type_hint then
+ body['token_type_hint'] = token_type_hint
+ end
+ local token_type_log = token_type_hint or 'token'
+
+ -- ensure revocation endpoint auth method is properly discovered
+ local err = ensure_config(opts)
+ if err then
+ log(ERROR, "revocation of " .. token_type_log .. " unsuccessful: " .. err)
+ return false
+ end
+
+ -- call the revocation endpoint
+ local _
+ _, err = openidc.call_token_endpoint(opts, opts.discovery.revocation_endpoint, body, opts.token_endpoint_auth_method, "revocation", true)
+ if err then
+ log(ERROR, "revocation of " .. token_type_log .. " unsuccessful: " .. err)
+ return false
+ else
+ log(DEBUG, "revocation of " .. token_type_log .. " successful")
+ return true
+ end
+end
+
+function openidc.revoke_token(opts, token_type_hint, token)
+ local err = openidc_ensure_discovered_data(opts)
+ if err then
+ log(ERROR, "revocation of " .. (token_type_hint or "token (no type specified)") .. " unsuccessful: " .. err)
+ return false
+ end
+
+ return openidc_revoke_token(opts, token_type_hint, token)
+end
+
+function openidc.revoke_tokens(opts, session)
+ local err = openidc_ensure_discovered_data(opts)
+ if err then
+ log(ERROR, "revocation of tokens unsuccessful: " .. err)
+ return false
+ end
+
+ local access_token = session.data.access_token
+ local refresh_token = session.data.refresh_token
+
+ local access_token_revoke, refresh_token_revoke
+ if refresh_token then
+ access_token_revoke = openidc_revoke_token(opts, "refresh_token", refresh_token)
+ end
+ if access_token then
+ refresh_token_revoke = openidc_revoke_token(opts, "access_token", access_token)
+ end
+ return access_token_revoke and refresh_token_revoke
+end
+
+local openidc_transparent_pixel = "\137\080\078\071\013\010\026\010\000\000\000\013\073\072\068\082" ..
+ "\000\000\000\001\000\000\000\001\008\004\000\000\000\181\028\012" ..
+ "\002\000\000\000\011\073\068\065\084\120\156\099\250\207\000\000" ..
+ "\002\007\001\002\154\028\049\113\000\000\000\000\073\069\078\068" ..
+ "\174\066\096\130"
+
+-- handle logout
+local function openidc_logout(opts, session)
+ local session_token = session.data.enc_id_token
+ local access_token = session.data.access_token
+ local refresh_token = session.data.refresh_token
+ local err
+
+ if opts.lifecycle and opts.lifecycle.on_logout then
+ err = opts.lifecycle.on_logout(session)
+ if err then
+ log(WARN, "failed in `on_logout` handler: " .. err)
+ return err
+ end
+ end
+
+ session:destroy()
+
+ if opts.revoke_tokens_on_logout then
+ log(DEBUG, "revoke_tokens_on_logout is enabled. " ..
+ "trying to revoke access and refresh tokens...")
+ if refresh_token then
+ openidc_revoke_token(opts, "refresh_token", refresh_token)
+ end
+ if access_token then
+ openidc_revoke_token(opts, "access_token", access_token)
+ end
+ end
+
+ local headers = ngx.req.get_headers()
+ local header = get_first(headers['Accept'])
+ if header and header:find("image/png") then
+ ngx.header["Cache-Control"] = "no-cache, no-store"
+ ngx.header["Pragma"] = "no-cache"
+ ngx.header["P3P"] = "CAO PSA OUR"
+ ngx.header["Expires"] = "0"
+ ngx.header["X-Frame-Options"] = "DENY"
+ ngx.header.content_type = "image/png"
+ ngx.print(openidc_transparent_pixel)
+ ngx.exit(ngx.OK)
+ return
+ elseif opts.redirect_after_logout_uri or opts.discovery.end_session_endpoint then
+ local uri
+ if opts.redirect_after_logout_uri then
+ uri = opts.redirect_after_logout_uri
+ else
+ uri = opts.discovery.end_session_endpoint
+ end
+ local params = {}
+ if (opts.redirect_after_logout_with_id_token_hint or not opts.redirect_after_logout_uri) and session_token then
+ params["id_token_hint"] = session_token
+ end
+ if opts.post_logout_redirect_uri then
+ params["post_logout_redirect_uri"] = opts.post_logout_redirect_uri
+ end
+ return ngx.redirect(openidc_combine_uri(uri, params))
+ elseif opts.discovery.ping_end_session_endpoint then
+ local params = {}
+ if opts.post_logout_redirect_uri then
+ params["TargetResource"] = opts.post_logout_redirect_uri
+ end
+ return ngx.redirect(openidc_combine_uri(opts.discovery.ping_end_session_endpoint, params))
+ end
+
+ ngx.header.content_type = "text/html"
+ ngx.say("<html><body>Logged Out</body></html>")
+ ngx.exit(ngx.OK)
+end
+
+-- returns a valid access_token (eventually refreshing the token)
+local function openidc_access_token(opts, session, try_to_renew)
+
+ local err
+
+ if session.data.access_token == nil then
+ return nil, err
+ end
+ local current_time = ngx.time()
+ if current_time < session.data.access_token_expiration then
+ return session.data.access_token, err
+ end
+ if not try_to_renew then
+ return nil, "token expired"
+ end
+ if session.data.refresh_token == nil then
+ return nil, "token expired and no refresh token available"
+ end
+
+ log(DEBUG, "refreshing expired access_token: ", session.data.access_token, " with: ", session.data.refresh_token)
+
+ -- retrieve token endpoint URL from discovery endpoint if necessary
+ err = ensure_config(opts)
+ if err then
+ return nil, err
+ end
+
+ -- assemble the parameters to the token endpoint
+ local body = {
+ grant_type = "refresh_token",
+ refresh_token = session.data.refresh_token,
+ scope = opts.scope and opts.scope or "openid email profile"
+ }
+
+ local json
+ json, err = openidc.call_token_endpoint(opts, opts.discovery.token_endpoint, body, opts.token_endpoint_auth_method)
+ if err then
+ return nil, err
+ end
+ local id_token
+ if json.id_token then
+ id_token, err = openidc_load_and_validate_jwt_id_token(opts, json.id_token, session)
+ if err then
+ log(ERROR, "invalid id token, discarding tokens returned while refreshing")
+ return nil, err
+ end
+ end
+ log(DEBUG, "access_token refreshed: ", json.access_token, " updated refresh_token: ", json.refresh_token)
+
+ session.data.access_token = json.access_token
+ session.data.access_token_expiration = current_time + openidc_access_token_expires_in(opts, json.expires_in)
+ if json.refresh_token then
+ session.data.refresh_token = json.refresh_token
+ end
+
+ if json.id_token and
+ (store_in_session(opts, 'enc_id_token') or store_in_session(opts, 'id_token')) then
+ log(DEBUG, "id_token refreshed: ", json.id_token)
+ if store_in_session(opts, 'enc_id_token') then
+ session.data.enc_id_token = json.id_token
+ end
+ if store_in_session(opts, 'id_token') then
+ session.data.id_token = id_token
+ end
+ end
+
+ -- save the session with the new access_token and optionally the new refresh_token and id_token using a new sessionid
+ local regenerated
+ regenerated, err = session:regenerate()
+ if err then
+ log(ERROR, "failed to regenerate session: " .. err)
+ return nil, err
+ end
+ if opts.lifecycle and opts.lifecycle.on_regenerated then
+ err = opts.lifecycle.on_regenerated(session)
+ if err then
+ log(WARN, "failed in `on_regenerated` handler: " .. err)
+ return nil, err
+ end
+ end
+
+ return session.data.access_token, err
+end
+
+local function openidc_get_path(uri)
+ local without_query = uri:match("(.-)%?") or uri
+ return without_query:match(".-//[^/]+(/.*)") or without_query
+end
+
+local function openidc_get_redirect_uri_path(opts)
+ return opts.redirect_uri and openidc_get_path(opts.redirect_uri) or opts.redirect_uri_path
+end
+
+local function is_session(o)
+ return o ~= nil and o.start and type(o.start) == "function"
+end
+
+-- main routine for OpenID Connect user authentication
+function openidc.authenticate(opts, target_url, unauth_action, session_or_opts)
+
+ if opts.redirect_uri_path then
+ log(WARN, "using deprecated option `opts.redirect_uri_path`; switch to using an absolute URI and `opts.redirect_uri` instead")
+ end
+
+ local err
+
+ local session
+ if is_session(session_or_opts) then
+ session = session_or_opts
+ else
+ local session_error
+ session, session_error = r_session.start(session_or_opts)
+ if session == nil then
+ log(ERROR, "Error starting session: " .. session_error)
+ return nil, session_error, target_url, session
+ end
+ end
+
+ target_url = target_url or ngx.var.request_uri
+
+ local access_token
+
+ -- see if this is a request to the redirect_uri i.e. an authorization response
+ local path = openidc_get_path(target_url)
+ if path == openidc_get_redirect_uri_path(opts) then
+ log(DEBUG, "Redirect URI path (" .. path .. ") is currently navigated -> Processing authorization response coming from OP")
+
+ if not session.present then
+ err = "request to the redirect_uri path but there's no session state found"
+ log(ERROR, err)
+ return nil, err, target_url, session
+ end
+
+ return openidc_authorization_response(opts, session)
+ end
+
+ -- see if this is a request to logout
+ if path == (opts.logout_path or "/logout") then
+ log(DEBUG, "Logout path (" .. path .. ") is currently navigated -> Processing local session removal before redirecting to next step of logout process")
+
+ err = ensure_config(opts)
+ if err then
+ return nil, err, session.data.original_url, session
+ end
+
+ openidc_logout(opts, session)
+ return nil, nil, target_url, session
+ end
+
+ local token_expired = false
+ local try_to_renew = opts.renew_access_token_on_expiry == nil or opts.renew_access_token_on_expiry
+ if session.present and session.data.authenticated
+ and store_in_session(opts, 'access_token') then
+
+ -- refresh access_token if necessary
+ access_token, err = openidc_access_token(opts, session, try_to_renew)
+ if err then
+ log(ERROR, "lost access token:" .. err)
+ err = nil
+ end
+ if not access_token then
+ token_expired = true
+ end
+ end
+
+ log(DEBUG,
+ "session.present=", session.present,
+ ", session.data.id_token=", session.data.id_token ~= nil,
+ ", session.data.authenticated=", session.data.authenticated,
+ ", opts.force_reauthorize=", opts.force_reauthorize,
+ ", opts.renew_access_token_on_expiry=", opts.renew_access_token_on_expiry,
+ ", try_to_renew=", try_to_renew,
+ ", token_expired=", token_expired)
+
+ -- if we are not authenticated then redirect to the OP for authentication
+ -- the presence of the id_token is check for backwards compatibility
+ if not session.present
+ or not (session.data.id_token or session.data.authenticated)
+ or opts.force_reauthorize
+ or (try_to_renew and token_expired) then
+ if unauth_action == "pass" then
+ if token_expired then
+ session.data.authenticated = false
+ return nil, 'token refresh failed', target_url, session
+ end
+ return nil, err, target_url, session
+ end
+ if unauth_action == 'deny' then
+ return nil, 'unauthorized request', target_url, session
+ end
+
+ err = ensure_config(opts)
+ if err then
+ return nil, err, session.data.original_url, session
+ end
+
+ log(DEBUG, "Authentication is required - Redirecting to OP Authorization endpoint")
+ openidc_authorize(opts, session, target_url, opts.prompt)
+ return nil, nil, target_url, session
+ end
+
+ -- silently reauthenticate if necessary (mainly used for session refresh/getting updated id_token data)
+ if opts.refresh_session_interval ~= nil then
+ if session.data.last_authenticated == nil or (session.data.last_authenticated + opts.refresh_session_interval) < ngx.time() then
+ err = ensure_config(opts)
+ if err then
+ return nil, err, session.data.original_url, session
+ end
+
+ log(DEBUG, "Silent authentication is required - Redirecting to OP Authorization endpoint")
+ openidc_authorize(opts, session, target_url, "none")
+ return nil, nil, target_url, session
+ end
+ end
+
+ if store_in_session(opts, 'id_token') then
+ -- log id_token contents
+ log(DEBUG, "id_token=", cjson.encode(session.data.id_token))
+ end
+
+ -- return the id_token to the caller Lua script for access control purposes
+ return
+ {
+ id_token = session.data.id_token,
+ access_token = access_token,
+ user = session.data.user
+ },
+ err,
+ target_url,
+ session
+end
+
+-- get a valid access_token (eventually refreshing the token), or nil if there's no valid access_token
+function openidc.access_token(opts, session_opts)
+
+ local session = r_session.start(session_opts)
+ local token, err = openidc_access_token(opts, session, true)
+ session:close()
+ return token, err
+end
+
+
+-- get an OAuth 2.0 bearer access token from the HTTP request cookies
+local function openidc_get_bearer_access_token_from_cookie(opts)
+
+ local err
+
+ log(DEBUG, "getting bearer access token from Cookie")
+
+ local accept_token_as = opts.auth_accept_token_as or "header"
+ if accept_token_as:find("cookie") ~= 1 then
+ return nil, "openidc_get_bearer_access_token_from_cookie called but auth_accept_token_as wants "
+ .. opts.auth_accept_token_as
+ end
+ local divider = accept_token_as:find(':')
+ local cookie_name = divider and accept_token_as:sub(divider + 1) or "PA.global"
+
+ log(DEBUG, "bearer access token from cookie named: " .. cookie_name)
+
+ local cookies = ngx.req.get_headers()["Cookie"]
+ if not cookies then
+ err = "no Cookie header found"
+ log(ERROR, err)
+ return nil, err
+ end
+
+ local cookie_value = ngx.var["cookie_" .. cookie_name]
+ if not cookie_value then
+ err = "no Cookie " .. cookie_name .. " found"
+ log(ERROR, err)
+ end
+
+ return cookie_value, err
+end
+
+
+-- get an OAuth 2.0 bearer access token from the HTTP request
+local function openidc_get_bearer_access_token(opts)
+
+ local err
+
+ local accept_token_as = opts.auth_accept_token_as or "header"
+
+ if accept_token_as:find("cookie") == 1 then
+ return openidc_get_bearer_access_token_from_cookie(opts)
+ end
+
+ -- get the access token from the Authorization header
+ local headers = ngx.req.get_headers()
+ local header_name = opts.auth_accept_token_as_header_name or "Authorization"
+ local header = get_first(headers[header_name])
+
+ if header == nil or header:find(" ") == nil then
+ err = "no Authorization header found"
+ log(ERROR, err)
+ return nil, err
+ end
+
+ local divider = header:find(' ')
+ if string.lower(header:sub(0, divider - 1)) ~= string.lower("Bearer") then
+ err = "no Bearer authorization header value found"
+ log(ERROR, err)
+ return nil, err
+ end
+
+ local access_token = header:sub(divider + 1)
+ if access_token == nil then
+ err = "no Bearer access token value found"
+ log(ERROR, err)
+ return nil, err
+ end
+
+ return access_token, err
+end
+
+local function get_introspection_endpoint(opts)
+ local introspection_endpoint = opts.introspection_endpoint
+ if not introspection_endpoint then
+ local err = openidc_ensure_discovered_data(opts)
+ if err then
+ return nil, "opts.introspection_endpoint not said and " .. err
+ end
+ local endpoint = opts.discovery and opts.discovery.introspection_endpoint
+ if endpoint then
+ return endpoint
+ end
+ end
+ return introspection_endpoint
+end
+
+local function get_introspection_cache_prefix(opts)
+ return (opts.cache_segment and opts.cache_segment.gsub(',', '_') or 'DEFAULT') .. ','
+ .. (get_introspection_endpoint(opts) or 'nil-endpoint') .. ','
+ .. (opts.client_id or 'no-client_id') .. ','
+ .. (opts.client_secret and 'secret' or 'no-client_secret') .. ':'
+end
+
+local function get_cached_introspection(opts, access_token)
+ local introspection_cache_ignore = opts.introspection_cache_ignore or false
+ if not introspection_cache_ignore then
+ return openidc_cache_get("introspection",
+ get_introspection_cache_prefix(opts) .. access_token)
+ end
+end
+
+local function set_cached_introspection(opts, access_token, encoded_json, ttl)
+ local introspection_cache_ignore = opts.introspection_cache_ignore or false
+ if not introspection_cache_ignore then
+ openidc_cache_set("introspection",
+ get_introspection_cache_prefix(opts) .. access_token,
+ encoded_json, ttl)
+ end
+end
+
+-- main routine for OAuth 2.0 token introspection
+function openidc.introspect(opts)
+
+ -- get the access token from the request
+ local access_token, err = openidc_get_bearer_access_token(opts)
+ if access_token == nil then
+ return nil, err
+ end
+
+ -- see if we've previously cached the introspection result for this access token
+ local json
+ local v = get_cached_introspection(opts, access_token)
+
+ if v then
+ json = cjson.decode(v)
+ return json, err
+ end
+
+ -- assemble the parameters to the introspection (token) endpoint
+ local token_param_name = opts.introspection_token_param_name and opts.introspection_token_param_name or "token"
+
+ local body = {}
+
+ body[token_param_name] = access_token
+
+ if opts.client_id then
+ body.client_id = opts.client_id
+ end
+ if opts.client_secret then
+ body.client_secret = opts.client_secret
+ end
+
+ -- merge any provided extra parameters
+ if opts.introspection_params then
+ for key, val in pairs(opts.introspection_params) do body[key] = val end
+ end
+
+ -- call the introspection endpoint
+ local introspection_endpoint
+ introspection_endpoint, err = get_introspection_endpoint(opts)
+ if err then
+ return nil, err
+ end
+ json, err = openidc.call_token_endpoint(opts, introspection_endpoint, body, opts.introspection_endpoint_auth_method, "introspection")
+
+
+ if not json then
+ return json, err
+ end
+
+ if not json.active then
+ err = "invalid token"
+ return json, err
+ end
+
+ -- cache the results
+ local introspection_cache_ignore = opts.introspection_cache_ignore or false
+ local expiry_claim = opts.introspection_expiry_claim or "exp"
+
+ if not introspection_cache_ignore and json[expiry_claim] then
+ local introspection_interval = opts.introspection_interval or 0
+ local ttl = json[expiry_claim]
+ if expiry_claim == "exp" then --https://tools.ietf.org/html/rfc7662#section-2.2
+ ttl = ttl - ngx.time()
+ end
+ if introspection_interval > 0 then
+ if ttl > introspection_interval then
+ ttl = introspection_interval
+ end
+ end
+ log(DEBUG, "cache token ttl: " .. ttl)
+ set_cached_introspection(opts, access_token, cjson.encode(json), ttl)
+ end
+
+ return json, err
+
+end
+
+local function get_jwt_verification_cache_prefix(opts)
+ local signing_alg_values_expected = (opts.accept_none_alg and 'none' or 'no-none')
+ local expected_algs = opts.token_signing_alg_values_expected or {}
+ if type(expected_algs) == 'string' then
+ expected_algs = { expected_algs }
+ end
+ for _, alg in ipairs(expected_algs) do
+ signing_alg_values_expected = signing_alg_values_expected .. ',' .. alg
+ end
+ return (opts.cache_segment and opts.cache_segment.gsub(',', '_') or 'DEFAULT') .. ','
+ .. (opts.public_key or 'no-pubkey') .. ','
+ .. (opts.symmetric_key or 'no-symkey') .. ','
+ .. signing_alg_values_expected .. ':'
+end
+
+local function get_cached_jwt_verification(opts, access_token)
+ local jwt_verification_cache_ignore = opts.jwt_verification_cache_ignore or false
+ if not jwt_verification_cache_ignore then
+ return openidc_cache_get("jwt_verification",
+ get_jwt_verification_cache_prefix(opts) .. access_token)
+ end
+end
+
+local function set_cached_jwt_verification(opts, access_token, encoded_json, ttl)
+ local jwt_verification_cache_ignore = opts.jwt_verification_cache_ignore or false
+ if not jwt_verification_cache_ignore then
+ openidc_cache_set("jwt_verification",
+ get_jwt_verification_cache_prefix(opts) .. access_token,
+ encoded_json, ttl)
+ end
+end
+
+-- main routine for OAuth 2.0 JWT token validation
+-- optional args are claim specs, see jwt-validators in resty.jwt
+function openidc.jwt_verify(access_token, opts, ...)
+ local err
+ local json
+ local v = get_cached_jwt_verification(opts, access_token)
+
+ local slack = opts.iat_slack and opts.iat_slack or 120
+ if not v then
+ local jwt_obj
+ jwt_obj, err = openidc_load_jwt_and_verify_crypto(opts, access_token, opts.public_key, opts.symmetric_key,
+ opts.token_signing_alg_values_expected, ...)
+ if not err then
+ json = jwt_obj.payload
+ local encoded_json = cjson.encode(json)
+ log(DEBUG, "jwt: ", encoded_json)
+
+ set_cached_jwt_verification(opts, access_token, encoded_json,
+ json.exp and json.exp - ngx.time() or 120)
+ end
+
+ else
+ -- decode from the cache
+ json = cjson.decode(v)
+ end
+
+ -- check the token expiry
+ if json then
+ if json.exp and json.exp + slack < ngx.time() then
+ log(ERROR, "token expired: json.exp=", json.exp, ", ngx.time()=", ngx.time())
+ err = "JWT expired"
+ end
+ end
+
+ return json, err
+end
+
+function openidc.bearer_jwt_verify(opts, ...)
+ local json
+
+ -- get the access token from the request
+ local access_token, err = openidc_get_bearer_access_token(opts)
+ if access_token == nil then
+ return nil, err
+ end
+
+ log(DEBUG, "access_token: ", access_token)
+
+ json, err = openidc.jwt_verify(access_token, opts, ...)
+ return json, err, access_token
+end
+
+-- Passing nil to any of the arguments resets the configuration to default
+function openidc.set_logging(new_log, new_levels)
+ log = new_log and new_log or ngx.log
+ DEBUG = new_levels.DEBUG and new_levels.DEBUG or ngx.DEBUG
+ ERROR = new_levels.ERROR and new_levels.ERROR or ngx.ERR
+ WARN = new_levels.WARN and new_levels.WARN or ngx.WARN
+end
+
+return openidc
diff --git a/server/resty/openssl.lua b/server/resty/openssl.lua
new file mode 100644
index 0000000..27ef5cc
--- /dev/null
+++ b/server/resty/openssl.lua
@@ -0,0 +1,476 @@
+local ffi = require("ffi")
+local C = ffi.C
+local ffi_cast = ffi.cast
+local ffi_str = ffi.string
+
+local format_error = require("resty.openssl.err").format_error
+
+local OPENSSL_3X, BORINGSSL
+
+local function try_require_modules()
+ package.loaded["resty.openssl.version"] = nil
+
+ local pok, lib = pcall(require, "resty.openssl.version")
+ if pok then
+ OPENSSL_3X = lib.OPENSSL_3X
+ BORINGSSL = lib.BORINGSSL
+
+ require "resty.openssl.include.crypto"
+ require "resty.openssl.include.objects"
+ else
+ package.loaded["resty.openssl.version"] = nil
+ end
+end
+try_require_modules()
+
+
+local _M = {
+ _VERSION = '0.8.16',
+}
+
+local libcrypto_name
+local lib_patterns = {
+ "%s", "%s.so.3", "%s.so.1.1", "%s.so.1.0"
+}
+
+function _M.load_library()
+ for _, pattern in ipairs(lib_patterns) do
+ -- true: load to global namespae
+ local pok, _ = pcall(ffi.load, string.format(pattern, "crypto"), true)
+ if pok then
+ libcrypto_name = string.format(pattern, "crypto")
+ ffi.load(string.format(pattern, "ssl"), true)
+
+ try_require_modules()
+
+ return libcrypto_name
+ end
+ end
+
+ return false, "unable to load crypto library"
+end
+
+function _M.load_modules()
+ _M.bn = require("resty.openssl.bn")
+ _M.cipher = require("resty.openssl.cipher")
+ _M.digest = require("resty.openssl.digest")
+ _M.hmac = require("resty.openssl.hmac")
+ _M.kdf = require("resty.openssl.kdf")
+ _M.pkey = require("resty.openssl.pkey")
+ _M.objects = require("resty.openssl.objects")
+ _M.rand = require("resty.openssl.rand")
+ _M.version = require("resty.openssl.version")
+ _M.x509 = require("resty.openssl.x509")
+ _M.altname = require("resty.openssl.x509.altname")
+ _M.chain = require("resty.openssl.x509.chain")
+ _M.csr = require("resty.openssl.x509.csr")
+ _M.crl = require("resty.openssl.x509.crl")
+ _M.extension = require("resty.openssl.x509.extension")
+ _M.extensions = require("resty.openssl.x509.extensions")
+ _M.name = require("resty.openssl.x509.name")
+ _M.revoked = require("resty.openssl.x509.revoked")
+ _M.store = require("resty.openssl.x509.store")
+ _M.pkcs12 = require("resty.openssl.pkcs12")
+ _M.ssl = require("resty.openssl.ssl")
+ _M.ssl_ctx = require("resty.openssl.ssl_ctx")
+
+ if OPENSSL_3X then
+ _M.provider = require("resty.openssl.provider")
+ _M.mac = require("resty.openssl.mac")
+ _M.ctx = require("resty.openssl.ctx")
+ end
+
+ _M.bignum = _M.bn
+end
+
+function _M.luaossl_compat()
+ _M.load_modules()
+
+ _M.csr.setSubject = _M.csr.set_subject_name
+ _M.csr.setPublicKey = _M.csr.set_pubkey
+
+ _M.x509.setPublicKey = _M.x509.set_pubkey
+ _M.x509.getPublicKey = _M.x509.get_pubkey
+ _M.x509.setSerial = _M.x509.set_serial_number
+ _M.x509.getSerial = _M.x509.get_serial_number
+ _M.x509.setSubject = _M.x509.set_subject_name
+ _M.x509.getSubject = _M.x509.get_subject_name
+ _M.x509.setIssuer = _M.x509.set_issuer_name
+ _M.x509.getIssuer = _M.x509.get_issuer_name
+ _M.x509.getOCSP = _M.x509.get_ocsp_url
+
+ local pkey_new = _M.pkey.new
+ _M.pkey.new = function(a, b)
+ if type(a) == "string" then
+ return pkey_new(a, b and unpack(b))
+ else
+ return pkey_new(a, b)
+ end
+ end
+
+ _M.cipher.encrypt = function(self, key, iv, padding)
+ return self, _M.cipher.init(self, key, iv, true, not padding)
+ end
+ _M.cipher.decrypt = function(self, key, iv, padding)
+ return self, _M.cipher.init(self, key, iv, false, not padding)
+ end
+
+ local digest_update = _M.digest.update
+ _M.digest.update = function(self, ...)
+ local ok, err = digest_update(self, ...)
+ if ok then
+ return self
+ else
+ return nil, err
+ end
+ end
+
+ local store_verify = _M.store.verify
+ _M.store.verify = function(...)
+ local ok, err = store_verify(...)
+ if err then
+ return false, err
+ else
+ return true, ok
+ end
+ end
+
+ local kdf_derive = _M.kdf.derive
+ local kdf_keys_mappings = {
+ iter = "pbkdf2_iter",
+ key = "hkdf_key",
+ info = "hkdf_info",
+ secret = "tls1_prf_secret",
+ seed = "tls1_prf_seed",
+ maxmem_bytes = "scrypt_maxmem",
+ N = "scrypt_N",
+ r = "scrypt_r",
+ p = "scrypt_p",
+ }
+ _M.kdf.derive = function(o)
+ for k1, k2 in pairs(kdf_keys_mappings) do
+ o[k1] = o[k2]
+ o[k2] = nil
+ end
+ local hkdf_mode = o.hkdf_mode
+ if hkdf_mode == "extract_and_expand" then
+ o.hkdf_mode = _M.kdf.HKDEF_MODE_EXTRACT_AND_EXPAND
+ elseif hkdf_mode == "extract_only" then
+ o.hkdf_mode = _M.kdf.HKDEF_MODE_EXTRACT_ONLY
+ elseif hkdf_mode == "expand_only" then
+ o.hkdf_mode = _M.kdf.HKDEF_MODE_EXPAND_ONLY
+ end
+ return kdf_derive(o)
+ end
+
+ _M.pkcs12.new = function(tbl)
+ local certs = {}
+ local passphrase = tbl.passphrase
+ if not tbl.key then
+ return nil, "key must be set"
+ end
+ for _, cert in ipairs(tbl.certs) do
+ if not _M.x509.istype(cert) then
+ return nil, "certs must contains only x509 instance"
+ end
+ if cert:check_private_key(tbl.key) then
+ tbl.cert = cert
+ else
+ certs[#certs+1] = cert
+ end
+ end
+ tbl.cacerts = certs
+ return _M.pkcs12.encode(tbl, passphrase)
+ end
+
+ _M.crl.add = _M.crl.add_revoked
+ _M.crl.lookupSerial = _M.crl.get_by_serial
+
+ for mod, tbl in pairs(_M) do
+ if type(tbl) == 'table' then
+
+ -- avoid using a same table as the iterrator will change
+ local new_tbl = {}
+ -- luaossl always error() out
+ for k, f in pairs(tbl) do
+ if type(f) == 'function' then
+ local of = f
+ new_tbl[k] = function(...)
+ local ret = { of(...) }
+ if ret and #ret > 1 and ret[#ret] then
+ error(mod .. "." .. k .. "(): " .. ret[#ret])
+ end
+ return unpack(ret)
+ end
+ end
+ end
+
+ for k, f in pairs(new_tbl) do
+ tbl[k] = f
+ end
+
+ setmetatable(tbl, {
+ __index = function(t, k)
+ local tok
+ -- handle special case
+ if k == 'toPEM' then
+ tok = 'to_PEM'
+ else
+ tok = k:gsub("(%l)(%u)", function(a, b) return a .. "_" .. b:lower() end)
+ if tok == k then
+ return
+ end
+ end
+ if type(tbl[tok]) == 'function' then
+ return tbl[tok]
+ end
+ end
+ })
+ end
+ end
+
+ -- skip error() conversion
+ _M.pkcs12.parse = function(p12, passphrase)
+ local r, err = _M.pkcs12.decode(p12, passphrase)
+ if err then error(err) end
+ return r.key, r.cert, r.cacerts
+ end
+end
+
+if OPENSSL_3X then
+ require "resty.openssl.include.evp"
+ local provider = require "resty.openssl.provider"
+ local ctx_lib = require "resty.openssl.ctx"
+ local fips_provider_ctx
+
+ function _M.set_fips_mode(enable, self_test)
+ if (not not enable) == _M.get_fips_mode() then
+ return true
+ end
+
+ if enable then
+ local p, err = provider.load("fips")
+ if not p then
+ return false, err
+ end
+ fips_provider_ctx = p
+ if self_test then
+ local ok, err = p:self_test()
+ if not ok then
+ return false, err
+ end
+ end
+
+ elseif fips_provider_ctx then -- disable
+ local p = fips_provider_ctx
+ fips_provider_ctx = nil
+ return p:unload()
+ end
+
+ -- set algorithm in fips mode in default ctx
+ -- this deny/allow non-FIPS compliant algorithms to be used from EVP interface
+ -- and redirect/remove redirect implementation to fips provider
+ if C.EVP_default_properties_enable_fips(ctx_lib.get_libctx(), enable and 1 or 0) == 0 then
+ return false, format_error("openssl.set_fips_mode: EVP_default_properties_enable_fips")
+ end
+
+ return true
+ end
+
+ function _M.get_fips_mode()
+ local pok = provider.is_available("fips")
+ if not pok then
+ return false
+ end
+
+ return C.EVP_default_properties_is_fips_enabled(ctx_lib.get_libctx()) == 1
+ end
+
+else
+ function _M.set_fips_mode(enable)
+ if (not not enable) == _M.get_fips_mode() then
+ return true
+ end
+
+ if C.FIPS_mode_set(enable and 1 or 0) == 0 then
+ return false, format_error("openssl.set_fips_mode")
+ end
+
+ return true
+ end
+
+ function _M.get_fips_mode()
+ return C.FIPS_mode() == 1
+ end
+end
+
+function _M.set_default_properties(props)
+ if not OPENSSL_3X then
+ return nil, "openssl.set_default_properties is only not supported from OpenSSL 3.0"
+ end
+
+ local ctx_lib = require "resty.openssl.ctx"
+
+ if C.EVP_set_default_properties(ctx_lib.get_libctx(), props) == 0 then
+ return false, format_error("openssl.EVP_set_default_properties")
+ end
+
+ return true
+end
+
+local function list_legacy(typ, get_nid_cf)
+ local typ_lower = string.lower(typ:sub(5)) -- cut off EVP_
+ require ("resty.openssl.include.evp." .. typ_lower)
+
+ local ret = {}
+ local fn = ffi_cast("fake_openssl_" .. typ_lower .. "_list_fn*",
+ function(elem, from, to, arg)
+ if elem ~= nil then
+ local nid = get_nid_cf(elem)
+ table.insert(ret, ffi_str(C.OBJ_nid2sn(nid)))
+ end
+ -- from/to (renamings) are ignored
+ end)
+ C[typ .. "_do_all_sorted"](fn, nil)
+ fn:free()
+
+ return ret
+end
+
+local function list_provided(typ)
+ local typ_lower = string.lower(typ:sub(5)) -- cut off EVP_
+ local typ_ptr = typ .. "*"
+ require ("resty.openssl.include.evp." .. typ_lower)
+ local ctx_lib = require "resty.openssl.ctx"
+
+ local ret = {}
+
+ local fn = ffi_cast("fake_openssl_" .. typ_lower .. "_provided_list_fn*",
+ function(elem, _)
+ elem = ffi_cast(typ_ptr, elem)
+ local name = ffi_str(C[typ .. "_get0_name"](elem))
+ -- alternate names are ignored, retrieve use TYPE_names_do_all
+ local prov = ffi_str(C.OSSL_PROVIDER_get0_name(C[typ .. "_get0_provider"](elem)))
+ table.insert(ret, name .. " @ " .. prov)
+ end)
+
+ C[typ .. "_do_all_provided"](ctx_lib.get_libctx(), fn, nil)
+ fn:free()
+
+ table.sort(ret)
+ return ret
+end
+
+function _M.list_cipher_algorithms()
+ if BORINGSSL then
+ return nil, "openssl.list_cipher_algorithms is not supported on BoringSSL"
+ end
+
+ require "resty.openssl.include.evp.cipher"
+ local ret = list_legacy("EVP_CIPHER",
+ OPENSSL_3X and C.EVP_CIPHER_get_nid or C.EVP_CIPHER_nid)
+
+ if OPENSSL_3X then
+ local ret_provided = list_provided("EVP_CIPHER")
+ for _, r in ipairs(ret_provided) do
+ table.insert(ret, r)
+ end
+ end
+
+ return ret
+end
+
+function _M.list_digest_algorithms()
+ if BORINGSSL then
+ return nil, "openssl.list_digest_algorithms is not supported on BoringSSL"
+ end
+
+ require "resty.openssl.include.evp.md"
+ local ret = list_legacy("EVP_MD",
+ OPENSSL_3X and C.EVP_MD_get_type or C.EVP_MD_type)
+
+ if OPENSSL_3X then
+ local ret_provided = list_provided("EVP_MD")
+ for _, r in ipairs(ret_provided) do
+ table.insert(ret, r)
+ end
+ end
+
+ return ret
+end
+
+function _M.list_mac_algorithms()
+ if not OPENSSL_3X then
+ return nil, "openssl.list_mac_algorithms is only supported from OpenSSL 3.0"
+ end
+
+ return list_provided("EVP_MAC")
+end
+
+function _M.list_kdf_algorithms()
+ if not OPENSSL_3X then
+ return nil, "openssl.list_kdf_algorithms is only supported from OpenSSL 3.0"
+ end
+
+ return list_provided("EVP_KDF")
+end
+
+local valid_ssl_protocols = {
+ ["SSLv3"] = 0x0300,
+ ["TLSv1"] = 0x0301,
+ ["TLSv1.1"] = 0x0302,
+ ["TLSv1.2"] = 0x0303,
+ ["TLSv1.3"] = 0x0304,
+}
+
+function _M.list_ssl_ciphers(cipher_list, ciphersuites, protocol)
+ local ssl_lib = require("resty.openssl.ssl")
+ local ssl_macro = require("resty.openssl.include.ssl")
+
+ if protocol then
+ if not valid_ssl_protocols[protocol] then
+ return nil, "unknown protocol \"" .. protocol .. "\""
+ end
+ protocol = valid_ssl_protocols[protocol]
+ end
+
+ local ssl_ctx = C.SSL_CTX_new(C.TLS_server_method())
+ if ssl_ctx == nil then
+ return nil, format_error("SSL_CTX_new")
+ end
+ ffi.gc(ssl_ctx, C.SSL_CTX_free)
+
+ local ssl = C.SSL_new(ssl_ctx)
+ if ssl == nil then
+ return nil, format_error("SSL_new")
+ end
+ ffi.gc(ssl, C.SSL_free)
+
+ if protocol then
+ if ssl_macro.SSL_set_min_proto_version(ssl, protocol) == 0 or
+ ssl_macro.SSL_set_max_proto_version(ssl, protocol) == 0 then
+ return nil, format_error("SSL_set_min/max_proto_version")
+ end
+ end
+
+ ssl = { ctx = ssl }
+
+ local ok, err
+ if cipher_list then
+ ok, err = ssl_lib.set_cipher_list(ssl, cipher_list)
+ if not ok then
+ return nil, err
+ end
+ end
+
+ if ciphersuites then
+ ok, err = ssl_lib.set_ciphersuites(ssl, ciphersuites)
+ if not ok then
+ return nil, err
+ end
+ end
+
+ return ssl_lib.get_ciphers(ssl)
+end
+
+return _M
diff --git a/server/resty/openssl/asn1.lua b/server/resty/openssl/asn1.lua
new file mode 100644
index 0000000..0fa0605
--- /dev/null
+++ b/server/resty/openssl/asn1.lua
@@ -0,0 +1,91 @@
+local ffi = require "ffi"
+local C = ffi.C
+local ffi_str = ffi.string
+local floor = math.floor
+
+local asn1_macro = require("resty.openssl.include.asn1")
+
+-- https://github.com/wahern/luaossl/blob/master/src/openssl.c
+local function isleap(year)
+ return (year % 4) == 0 and ((year % 100) > 0 or (year % 400) == 0)
+end
+
+local past = { 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334 }
+local function yday(year, mon, mday)
+ local d = past[mon] + mday - 1
+ if mon > 2 and isleap(year) then
+ d = d + 1
+ end
+ return d
+end
+
+local function leaps(year)
+ return floor(year / 400) + floor(year / 4) - floor(year / 100)
+end
+
+local function asn1_to_unix(asn1)
+ if asn1 == nil then
+ return nil, "except an ASN1 instance at #1, got nil"
+ end
+
+ local s = asn1_macro.ASN1_STRING_get0_data(asn1)
+ s = ffi_str(s)
+ -- V_ASN1_UTCTIME 190303223958Z
+ -- V_ASN1_GENERALIZEDTIME 21190822162753Z
+ local yyoffset = 2
+ local year
+ -- # define V_ASN1_GENERALIZEDTIME 24
+ if C.ASN1_STRING_type(asn1) == 24 then
+ yyoffset = 4
+ year = tonumber(s:sub(1, yyoffset))
+ else
+ year = tonumber(s:sub(1, yyoffset))
+ year = year + (year < 50 and 2000 or 1900)
+ end
+ local month = tonumber(s:sub(yyoffset+1, yyoffset+2))
+ if month > 12 or month < 1 then
+ return nil, "asn1.asn1_to_unix: bad format " .. s
+ end
+ local day = tonumber(s:sub(yyoffset+3, yyoffset+4))
+ if day > 31 or day < 1 then
+ return nil, "asn1.asn1_to_unix: bad format " .. s
+ end
+ local hour = tonumber(s:sub(yyoffset+5, yyoffset+6))
+ if hour > 23 or hour < 0 then
+ return nil, "asn1.asn1_to_unix: bad format " .. s
+ end
+ local minute = tonumber(s:sub(yyoffset+7, yyoffset+8))
+ if minute > 59 or hour < 0 then
+ return nil, "asn1.asn1_to_unix: bad format " .. s
+ end
+ local second = tonumber(s:sub(yyoffset+9, yyoffset+10))
+ if second > 59 or second < 0 then
+ return nil, "asn1.asn1_to_unix: bad format " .. s
+ end
+
+ local tm
+ tm = (year - 1970) * 365
+ tm = tm + leaps(year - 1) - leaps(1969)
+ tm = (tm + yday(year, month, day)) * 24
+ tm = (tm + hour) * 60
+ tm = (tm + minute) * 60
+ tm = tm + second
+
+ -- offset?
+ local sign = s:sub(yyoffset+11, yyoffset+11)
+ if sign == "+" or sign == "-" then
+ local sgn = sign == "+" and 1 or -1
+ local hh = tonumber(s:sub(yyoffset+12, yyoffset+13) or 'no')
+ local mm = tonumber(s:sub(yyoffset+14, yyoffset+15) or 'no')
+ if not hh or not mm then
+ return nil, "asn1.asn1_to_unix: bad format " .. s
+ end
+ tm = tm + sgn * (hh * 3600 + mm * 60)
+ end
+
+ return tm
+end
+
+return {
+ asn1_to_unix = asn1_to_unix,
+}
diff --git a/server/resty/openssl/auxiliary/bio.lua b/server/resty/openssl/auxiliary/bio.lua
new file mode 100644
index 0000000..3eed9f0
--- /dev/null
+++ b/server/resty/openssl/auxiliary/bio.lua
@@ -0,0 +1,43 @@
+local ffi = require "ffi"
+local C = ffi.C
+local ffi_gc = ffi.gc
+local ffi_new = ffi.new
+local ffi_str = ffi.string
+
+require "resty.openssl.include.bio"
+local format_error = require("resty.openssl.err").format_error
+
+local function read_wrap(f, ...)
+ if type(f) ~= "cdata" then -- should be explictly a function
+ return nil, "bio_util.read_wrap: expect a function at #1"
+ end
+
+ local bio_method = C.BIO_s_mem()
+ if bio_method == nil then
+ return nil, "bio_util.read_wrap: BIO_s_mem() failed"
+ end
+ local bio = C.BIO_new(bio_method)
+ ffi_gc(bio, C.BIO_free)
+
+ -- BIO_reset; #define BIO_CTRL_RESET 1
+ local code = C.BIO_ctrl(bio, 1, 0, nil)
+ if code ~= 1 then
+ return nil, "bio_util.read_wrap: BIO_ctrl() failed: " .. code
+ end
+
+ local code = f(bio, ...)
+ if code ~= 1 then
+ return nil, format_error(f, code)
+ end
+
+ local buf = ffi_new("char *[1]")
+
+ -- BIO_get_mem_data; #define BIO_CTRL_INFO 3
+ local length = C.BIO_ctrl(bio, 3, 0, buf)
+
+ return ffi_str(buf[0], length)
+end
+
+return {
+ read_wrap = read_wrap,
+} \ No newline at end of file
diff --git a/server/resty/openssl/auxiliary/ctypes.lua b/server/resty/openssl/auxiliary/ctypes.lua
new file mode 100644
index 0000000..933822b
--- /dev/null
+++ b/server/resty/openssl/auxiliary/ctypes.lua
@@ -0,0 +1,28 @@
+-- Put common type definition at the same place for convenience
+-- and standarlization
+local ffi = require "ffi"
+
+--[[
+ TYPE_ptr: usually used to define a pointer (to cast or something)
+ char* var_name; // <- we use char_ptr
+
+ ptr_of_TYPE: usually used to pass the pointer of an object that
+ is already allocated. so that we can also set value of it as well
+
+ int p = 2; // ptr_of_int(); ptr_of_int[0] = 2;
+ plus_one(&p); // <- we use ptr_of_int
+]]
+
+return {
+ void_ptr = ffi.typeof("void *"),
+ ptr_of_uint64 = ffi.typeof("uint64_t[1]"),
+ ptr_of_uint = ffi.typeof("unsigned int[1]"),
+ ptr_of_size_t = ffi.typeof("size_t[1]"),
+ ptr_of_int = ffi.typeof("int[1]"),
+ null = ffi.new("void *"), -- hack wher ngx.null is not available
+
+ uchar_array = ffi.typeof("unsigned char[?]"),
+ uchar_ptr = ffi.typeof("unsigned char*"),
+
+ SIZE_MAX = math.pow(2, 64), -- nginx set _FILE_OFFSET_BITS to 64
+} \ No newline at end of file
diff --git a/server/resty/openssl/auxiliary/jwk.lua b/server/resty/openssl/auxiliary/jwk.lua
new file mode 100644
index 0000000..5a505a9
--- /dev/null
+++ b/server/resty/openssl/auxiliary/jwk.lua
@@ -0,0 +1,261 @@
+
+local ffi = require "ffi"
+local C = ffi.C
+
+local cjson = require("cjson.safe")
+local b64 = require("ngx.base64")
+
+local evp_macro = require "resty.openssl.include.evp"
+local rsa_lib = require "resty.openssl.rsa"
+local ec_lib = require "resty.openssl.ec"
+local ecx_lib = require "resty.openssl.ecx"
+local bn_lib = require "resty.openssl.bn"
+local digest_lib = require "resty.openssl.digest"
+
+local _M = {}
+
+local rsa_jwk_params = {"n", "e", "d", "p", "q", "dp", "dq", "qi"}
+local rsa_openssl_params = rsa_lib.params
+
+local function load_jwk_rsa(tbl)
+ if not tbl["n"] or not tbl["e"] then
+ return nil, "at least \"n\" and \"e\" parameter is required"
+ end
+
+ local params = {}
+ local err
+ for i, k in ipairs(rsa_jwk_params) do
+ local v = tbl[k]
+ if v then
+ v = b64.decode_base64url(v)
+ if not v then
+ return nil, "cannot decode parameter \"" .. k .. "\" from base64 " .. tbl[k]
+ end
+
+ params[rsa_openssl_params[i]], err = bn_lib.from_binary(v)
+ if err then
+ return nil, "cannot use parameter \"" .. k .. "\": " .. err
+ end
+ end
+ end
+
+ local key = C.RSA_new()
+ if key == nil then
+ return nil, "RSA_new() failed"
+ end
+
+ local _, err = rsa_lib.set_parameters(key, params)
+ if err ~= nil then
+ C.RSA_free(key)
+ return nil, err
+ end
+
+ return key
+end
+
+local ec_curves = {
+ ["P-256"] = C.OBJ_ln2nid("prime256v1"),
+ ["P-384"] = C.OBJ_ln2nid("secp384r1"),
+ ["P-521"] = C.OBJ_ln2nid("secp521r1"),
+}
+
+local ec_curves_reverse = {}
+for k, v in pairs(ec_curves) do
+ ec_curves_reverse[v] = k
+end
+
+local ec_jwk_params = {"x", "y", "d"}
+
+local function load_jwk_ec(tbl)
+ local curve = tbl['crv']
+ if not curve then
+ return nil, "\"crv\" not defined for EC key"
+ end
+ if not tbl["x"] or not tbl["y"] then
+ return nil, "at least \"x\" and \"y\" parameter is required"
+ end
+ local curve_nid = ec_curves[curve]
+ if not curve_nid then
+ return nil, "curve \"" .. curve .. "\" is not supported by this library"
+ elseif curve_nid == 0 then
+ return nil, "curve \"" .. curve .. "\" is not supported by linked OpenSSL"
+ end
+
+ local params = {}
+ local err
+ for _, k in ipairs(ec_jwk_params) do
+ local v = tbl[k]
+ if v then
+ v = b64.decode_base64url(v)
+ if not v then
+ return nil, "cannot decode parameter \"" .. k .. "\" from base64 " .. tbl[k]
+ end
+
+ params[k], err = bn_lib.from_binary(v)
+ if err then
+ return nil, "cannot use parameter \"" .. k .. "\": " .. err
+ end
+ end
+ end
+
+ -- map to the name we expect
+ if params["d"] then
+ params["private"] = params["d"]
+ params["d"] = nil
+ end
+ params["group"] = curve_nid
+
+ local key = C.EC_KEY_new()
+ if key == nil then
+ return nil, "EC_KEY_new() failed"
+ end
+
+ local _, err = ec_lib.set_parameters(key, params)
+ if err ~= nil then
+ C.EC_KEY_free(key)
+ return nil, err
+ end
+
+ return key
+end
+
+local function load_jwk_okp(key_type, tbl)
+ local params = {}
+ if tbl["d"] then
+ params.private = b64.decode_base64url(tbl["d"])
+ elseif tbl["x"] then
+ params.public = b64.decode_base64url(tbl["x"])
+ else
+ return nil, "at least \"x\" or \"d\" parameter is required"
+ end
+ local key, err = ecx_lib.set_parameters(key_type, nil, params)
+ if err ~= nil then
+ return nil, err
+ end
+ return key
+end
+
+local ecx_curves_reverse = {}
+for k, v in pairs(evp_macro.ecx_curves) do
+ ecx_curves_reverse[v] = k
+end
+
+function _M.load_jwk(txt)
+ local tbl, err = cjson.decode(txt)
+ if err then
+ return nil, "error decoding JSON from JWK: " .. err
+ elseif type(tbl) ~= "table" then
+ return nil, "except input to be decoded as a table, got " .. type(tbl)
+ end
+
+ local key, key_free, key_type, err
+
+ if tbl["kty"] == "RSA" then
+ key_type = evp_macro.EVP_PKEY_RSA
+ if key_type == 0 then
+ return nil, "the linked OpenSSL library doesn't support RSA key"
+ end
+ key, err = load_jwk_rsa(tbl)
+ key_free = C.RSA_free
+ elseif tbl["kty"] == "EC" then
+ key_type = evp_macro.EVP_PKEY_EC
+ if key_type == 0 then
+ return nil, "the linked OpenSSL library doesn't support EC key"
+ end
+ key, err = load_jwk_ec(tbl)
+ key_free = C.EC_KEY_free
+ elseif tbl["kty"] == "OKP" then
+ local curve = tbl["crv"]
+ key_type = evp_macro.ecx_curves[curve]
+ if not key_type then
+ return nil, "unknown curve \"" .. tostring(curve)
+ elseif key_type == 0 then
+ return nil, "the linked OpenSSL library doesn't support \"" .. curve .. "\" key"
+ end
+ key, err = load_jwk_okp(key_type, tbl)
+ if key ~= nil then
+ return key
+ end
+ else
+ return nil, "not yet supported jwk type \"" .. (tbl["kty"] or "nil") .. "\""
+ end
+
+ if err then
+ return nil, "failed to construct " .. tbl["kty"] .. " key from JWK: " .. err
+ end
+
+ local ctx = C.EVP_PKEY_new()
+ if ctx == nil then
+ key_free(key)
+ return nil, "EVP_PKEY_new() failed"
+ end
+
+ local code = C.EVP_PKEY_assign(ctx, key_type, key)
+ if code ~= 1 then
+ key_free(key)
+ C.EVP_PKEY_free(ctx)
+ return nil, "EVP_PKEY_assign() failed"
+ end
+
+ return ctx
+end
+
+function _M.dump_jwk(pkey, is_priv)
+ local jwk
+ if pkey.key_type == evp_macro.EVP_PKEY_RSA then
+ local param_keys = { "n" , "e" }
+ if is_priv then
+ param_keys = rsa_jwk_params
+ end
+ local params, err = pkey:get_parameters()
+ if err then
+ return nil, "jwk.dump_jwk: " .. err
+ end
+ jwk = {
+ kty = "RSA",
+ }
+ for i, p in ipairs(param_keys) do
+ local v = params[rsa_openssl_params[i]]:to_binary()
+ jwk[p] = b64.encode_base64url(v)
+ end
+ elseif pkey.key_type == evp_macro.EVP_PKEY_EC then
+ local params, err = pkey:get_parameters()
+ if err then
+ return nil, "jwk.dump_jwk: " .. err
+ end
+ jwk = {
+ kty = "EC",
+ crv = ec_curves_reverse[params.group],
+ x = b64.encode_base64url(params.x:to_binary()),
+ y = b64.encode_base64url(params.x:to_binary()),
+ }
+ if is_priv then
+ jwk.d = b64.encode_base64url(params.private:to_binary())
+ end
+ elseif ecx_curves_reverse[pkey.key_type] then
+ local params, err = pkey:get_parameters()
+ if err then
+ return nil, "jwk.dump_jwk: " .. err
+ end
+ jwk = {
+ kty = "OKP",
+ crv = ecx_curves_reverse[pkey.key_type],
+ d = b64.encode_base64url(params.private),
+ x = b64.encode_base64url(params.public),
+ }
+ else
+ return nil, "jwk.dump_jwk: not implemented for this key type"
+ end
+
+ local der = pkey:tostring(is_priv and "private" or "public", "DER")
+ local dgst = digest_lib.new("sha256")
+ local d, err = dgst:final(der)
+ if err then
+ return nil, "jwk.dump_jwk: failed to calculate digest for key"
+ end
+ jwk.kid = b64.encode_base64url(d)
+
+ return cjson.encode(jwk)
+end
+
+return _M
diff --git a/server/resty/openssl/auxiliary/nginx.lua b/server/resty/openssl/auxiliary/nginx.lua
new file mode 100644
index 0000000..8adeceb
--- /dev/null
+++ b/server/resty/openssl/auxiliary/nginx.lua
@@ -0,0 +1,318 @@
+local get_req_ssl, get_req_ssl_ctx
+local get_socket_ssl, get_socket_ssl_ctx
+
+local pok, nginx_c = pcall(require, "resty.openssl.auxiliary.nginx_c")
+
+if pok and not os.getenv("CI_SKIP_NGINX_C") then
+ get_req_ssl = nginx_c.get_req_ssl
+ get_req_ssl_ctx = nginx_c.get_req_ssl_ctx
+ get_socket_ssl = nginx_c.get_socket_ssl
+ get_socket_ssl_ctx = nginx_c.get_socket_ssl
+else
+ local ffi = require "ffi"
+
+ ffi.cdef [[
+ // Nginx seems to always config _FILE_OFFSET_BITS=64, this should always be 8 byte
+ typedef long long off_t;
+ typedef unsigned int socklen_t; // windows uses int, same size
+ typedef unsigned short in_port_t;
+
+ typedef struct ssl_st SSL;
+ typedef struct ssl_ctx_st SSL_CTX;
+
+ typedef long (*ngx_recv_pt)(void *c, void *buf, size_t size);
+ typedef long (*ngx_recv_chain_pt)(void *c, void *in,
+ off_t limit);
+ typedef long (*ngx_send_pt)(void *c, void *buf, size_t size);
+ typedef void *(*ngx_send_chain_pt)(void *c, void *in,
+ off_t limit);
+
+ typedef struct {
+ size_t len;
+ void *data;
+ } ngx_str_t;
+
+ typedef struct {
+ SSL *connection;
+ SSL_CTX *session_ctx;
+ // trimmed
+ } ngx_ssl_connection_s;
+ ]]
+
+ local ngx_version = ngx.config.nginx_version
+ if ngx_version == 1017008 or ngx_version == 1019003 or ngx_version == 1019009
+ or ngx_version == 1021004 then
+ -- 1.17.8, 1.19.3, 1.19.9, 1.21.4
+ -- https://github.com/nginx/nginx/blob/master/src/core/ngx_connection.h
+ ffi.cdef [[
+ typedef struct {
+ ngx_str_t src_addr;
+ ngx_str_t dst_addr;
+ in_port_t src_port;
+ in_port_t dst_port;
+ } ngx_proxy_protocol_t;
+
+ typedef struct {
+ void *data;
+ void *read;
+ void *write;
+
+ int fd;
+
+ ngx_recv_pt recv;
+ ngx_send_pt send;
+ ngx_recv_chain_pt recv_chain;
+ ngx_send_chain_pt send_chain;
+
+ void *listening;
+
+ off_t sent;
+
+ void *log;
+
+ void *pool;
+
+ int type;
+
+ void *sockaddr;
+ socklen_t socklen;
+ ngx_str_t addr_text;
+
+ // https://github.com/nginx/nginx/commit/be932e81a1531a3ba032febad968fc2006c4fa48
+ ngx_proxy_protocol_t *proxy_protocol;
+
+ ngx_ssl_connection_s *ssl;
+ // trimmed
+ } ngx_connection_s;
+ ]]
+ else
+ error("resty.openssl.auxiliary.nginx doesn't support Nginx version " .. ngx_version, 2)
+ end
+
+ ffi.cdef [[
+ typedef struct {
+ ngx_connection_s *connection;
+ // trimmed
+ } ngx_stream_lua_request_s;
+
+ typedef struct {
+ unsigned int signature; /* "HTTP" */
+
+ ngx_connection_s *connection;
+ // trimmed
+ } ngx_http_request_s;
+ ]]
+
+ local get_request
+ do
+ local ok, exdata = pcall(require, "thread.exdata")
+ if ok and exdata then
+ function get_request()
+ local r = exdata()
+ if r ~= nil then
+ return r
+ end
+ end
+
+ else
+ local getfenv = getfenv
+
+ function get_request()
+ return getfenv(0).__ngx_req
+ end
+ end
+ end
+
+ local SOCKET_CTX_INDEX = 1
+
+ local NO_C_MODULE_WARNING_MSG_SHOWN = false
+ local NO_C_MODULE_WARNING_MSG = "note resty.openssl.auxiliary.nginx is using plain FFI " ..
+ "and it's only intended to be used in development, " ..
+ "consider using lua-resty-openssl.aux-module in production."
+
+ local function get_ngx_ssl_from_req()
+ if not NO_C_MODULE_WARNING_MSG_SHOWN then
+ ngx.log(ngx.WARN, NO_C_MODULE_WARNING_MSG)
+ NO_C_MODULE_WARNING_MSG_SHOWN = true
+ end
+
+ local c = get_request()
+ if ngx.config.subsystem == "stream" then
+ c = ffi.cast("ngx_stream_lua_request_s*", c)
+ else -- http
+ c = ffi.cast("ngx_http_request_s*", c)
+ end
+
+ local ngx_ssl = c.connection.ssl
+ if ngx_ssl == nil then
+ return nil, "c.connection.ssl is nil"
+ end
+ return ngx_ssl
+ end
+
+ get_req_ssl = function()
+ local ssl, err = get_ngx_ssl_from_req()
+ if err then
+ return nil, err
+ end
+
+ return ssl.connection
+ end
+
+ get_req_ssl_ctx = function()
+ local ssl, err = get_ngx_ssl_from_req()
+ if err then
+ return nil, err
+ end
+
+ return ssl.session_ctx
+ end
+
+ ffi.cdef[[
+ typedef struct ngx_http_lua_socket_tcp_upstream_s
+ ngx_http_lua_socket_tcp_upstream_t;
+
+ typedef struct {
+ ngx_connection_s *connection;
+ // trimmed
+ } ngx_peer_connection_s;
+
+ typedef
+ int (*ngx_http_lua_socket_tcp_retval_handler_masked)(void *r,
+ void *u, void *L);
+
+ typedef void (*ngx_http_lua_socket_tcp_upstream_handler_pt_masked)
+ (void *r, void *u);
+
+
+ typedef
+ int (*ngx_stream_lua_socket_tcp_retval_handler)(void *r,
+ void *u, void *L);
+
+ typedef void (*ngx_stream_lua_socket_tcp_upstream_handler_pt)
+ (void *r, void *u);
+
+ typedef struct {
+ ngx_stream_lua_socket_tcp_retval_handler read_prepare_retvals;
+ ngx_stream_lua_socket_tcp_retval_handler write_prepare_retvals;
+ ngx_stream_lua_socket_tcp_upstream_handler_pt read_event_handler;
+ ngx_stream_lua_socket_tcp_upstream_handler_pt write_event_handler;
+
+ void *socket_pool;
+
+ void *conf;
+ void *cleanup;
+ void *request;
+
+ ngx_peer_connection_s peer;
+ // trimmed
+ } ngx_stream_lua_socket_tcp_upstream_s;
+ ]]
+
+ local ngx_lua_version = ngx.config and
+ ngx.config.ngx_lua_version and
+ ngx.config.ngx_lua_version
+
+ if ngx_lua_version >= 10019 and ngx_lua_version <= 10021 then
+ -- https://github.com/openresty/lua-nginx-module/blob/master/src/ngx_http_lua_socket_tcp.h
+ ffi.cdef[[
+ typedef struct {
+ ngx_http_lua_socket_tcp_retval_handler_masked read_prepare_retvals;
+ ngx_http_lua_socket_tcp_retval_handler_masked write_prepare_retvals;
+ ngx_http_lua_socket_tcp_upstream_handler_pt_masked read_event_handler;
+ ngx_http_lua_socket_tcp_upstream_handler_pt_masked write_event_handler;
+
+ void *udata_queue; // 0.10.19
+
+ void *socket_pool;
+
+ void *conf;
+ void *cleanup;
+ void *request;
+ ngx_peer_connection_s peer;
+ // trimmed
+ } ngx_http_lua_socket_tcp_upstream_s;
+ ]]
+ elseif ngx_lua_version < 10019 then
+ -- the struct doesn't seem to get changed a long time since birth
+ ffi.cdef[[
+ typedef struct {
+ ngx_http_lua_socket_tcp_retval_handler_masked read_prepare_retvals;
+ ngx_http_lua_socket_tcp_retval_handler_masked write_prepare_retvals;
+ ngx_http_lua_socket_tcp_upstream_handler_pt_masked read_event_handler;
+ ngx_http_lua_socket_tcp_upstream_handler_pt_masked write_event_handler;
+
+ void *socket_pool;
+
+ void *conf;
+ void *cleanup;
+ void *request;
+ ngx_peer_connection_s peer;
+ // trimmed
+ } ngx_http_lua_socket_tcp_upstream_s;
+ ]]
+ else
+ error("resty.openssl.auxiliary.nginx doesn't support lua-nginx-module version " .. (ngx_lua_version or "nil"), 2)
+ end
+
+ local function get_ngx_ssl_from_socket_ctx(sock)
+ if not NO_C_MODULE_WARNING_MSG_SHOWN then
+ ngx.log(ngx.WARN, NO_C_MODULE_WARNING_MSG)
+ NO_C_MODULE_WARNING_MSG_SHOWN = true
+ end
+
+ local u = sock[SOCKET_CTX_INDEX]
+ if u == nil then
+ return nil, "lua_socket_tcp_upstream_t not found"
+ end
+
+ if ngx.config.subsystem == "stream" then
+ u = ffi.cast("ngx_stream_lua_socket_tcp_upstream_s*", u)
+ else -- http
+ u = ffi.cast("ngx_http_lua_socket_tcp_upstream_s*", u)
+ end
+
+ local p = u.peer
+ if p == nil then
+ return nil, "u.peer is nil"
+ end
+
+ local uc = p.connection
+ if uc == nil then
+ return nil, "u.peer.connection is nil"
+ end
+
+ local ngx_ssl = uc.ssl
+ if ngx_ssl == nil then
+ return nil, "u.peer.connection.ssl is nil"
+ end
+ return ngx_ssl
+ end
+
+ get_socket_ssl = function(sock)
+ local ssl, err = get_ngx_ssl_from_socket_ctx(sock)
+ if err then
+ return nil, err
+ end
+
+ return ssl.connection
+ end
+
+ get_socket_ssl_ctx = function(sock)
+ local ssl, err = get_ngx_ssl_from_socket_ctx(sock)
+ if err then
+ return nil, err
+ end
+
+ return ssl.session_ctx
+ end
+
+end
+
+
+return {
+ get_req_ssl = get_req_ssl,
+ get_req_ssl_ctx = get_req_ssl_ctx,
+ get_socket_ssl = get_socket_ssl,
+ get_socket_ssl_ctx = get_socket_ssl_ctx,
+}
diff --git a/server/resty/openssl/auxiliary/nginx_c.lua b/server/resty/openssl/auxiliary/nginx_c.lua
new file mode 100644
index 0000000..f50db36
--- /dev/null
+++ b/server/resty/openssl/auxiliary/nginx_c.lua
@@ -0,0 +1,154 @@
+local ffi = require "ffi"
+local C = ffi.C
+
+local SOCKET_CTX_INDEX = 1
+local NGX_OK = ngx.OK
+
+
+local get_req_ssl, get_req_ssl_ctx
+local get_socket_ssl, get_socket_ssl_ctx
+
+local get_request
+do
+ local ok, exdata = pcall(require, "thread.exdata")
+ if ok and exdata then
+ function get_request()
+ local r = exdata()
+ if r ~= nil then
+ return r
+ end
+ end
+
+ else
+ local getfenv = getfenv
+
+ function get_request()
+ return getfenv(0).__ngx_req
+ end
+ end
+end
+
+
+local stream_subsystem = false
+if ngx.config.subsystem == "stream" then
+ stream_subsystem = true
+
+ ffi.cdef [[
+ typedef struct ngx_stream_lua_request_s ngx_stream_lua_request_t;
+ typedef struct ngx_stream_lua_socket_tcp_upstream_s ngx_stream_lua_socket_tcp_upstream_t;
+
+ int ngx_stream_lua_resty_openssl_aux_get_request_ssl(ngx_stream_lua_request_t *r,
+ void **_ssl_conn);
+
+ int ngx_stream_lua_resty_openssl_aux_get_request_ssl_ctx(ngx_stream_lua_request_t *r,
+ void **_sess);
+
+ int ngx_stream_lua_resty_openssl_aux_get_socket_ssl(ngx_stream_lua_socket_tcp_upstream_t *u,
+ void **_ssl_conn);
+
+ int ngx_stream_lua_resty_openssl_aux_get_socket_ssl_ctx(ngx_stream_lua_socket_tcp_upstream_t *u,
+ void **_sess);
+ ]]
+
+ -- sanity test
+ local _ = C.ngx_stream_lua_resty_openssl_aux_get_request_ssl
+else
+ ffi.cdef [[
+ typedef struct ngx_http_request_s ngx_http_request_t;
+ typedef struct ngx_http_lua_socket_tcp_upstream_s ngx_http_lua_socket_tcp_upstream_t;
+
+ int ngx_http_lua_resty_openssl_aux_get_request_ssl(ngx_http_request_t *r,
+ void **_ssl_conn);
+
+ int ngx_http_lua_resty_openssl_aux_get_request_ssl_ctx(ngx_http_request_t *r,
+ void **_sess);
+
+ int ngx_http_lua_resty_openssl_aux_get_socket_ssl(ngx_http_lua_socket_tcp_upstream_t *u,
+ void **_ssl_conn);
+
+ int ngx_http_lua_resty_openssl_aux_get_socket_ssl_ctx(ngx_http_lua_socket_tcp_upstream_t *u,
+ void **_sess);
+ ]]
+
+ -- sanity test
+ local _ = C.ngx_http_lua_resty_openssl_aux_get_request_ssl
+end
+
+local void_pp = ffi.new("void *[1]")
+local ssl_type = ffi.typeof("SSL*")
+local ssl_ctx_type = ffi.typeof("SSL_CTX*")
+
+get_req_ssl = function()
+ local c = get_request()
+
+ local ret
+ if stream_subsystem then
+ ret = C.ngx_stream_lua_resty_openssl_aux_get_request_ssl(c, void_pp)
+ else
+ ret = C.ngx_http_lua_resty_openssl_aux_get_request_ssl(c, void_pp)
+ end
+
+ if ret ~= NGX_OK then
+ return nil, "cannot read r->connection->ssl->connection"
+ end
+
+ return ffi.cast(ssl_type, void_pp[0])
+end
+
+get_req_ssl_ctx = function()
+ local c = get_request()
+
+ local ret
+ if stream_subsystem then
+ ret = C.ngx_stream_lua_resty_openssl_aux_get_request_ssl_ctx(c, void_pp)
+ else
+ ret = C.ngx_http_lua_resty_openssl_aux_get_request_ssl_ctx(c, void_pp)
+ end
+
+ if ret ~= NGX_OK then
+ return nil, "cannot read r->connection->ssl->session_ctx"
+ end
+
+ return ffi.cast(ssl_ctx_type, void_pp[0])
+end
+
+get_socket_ssl = function(sock)
+ local u = sock[SOCKET_CTX_INDEX]
+
+ local ret
+ if stream_subsystem then
+ ret = C.ngx_stream_lua_resty_openssl_aux_get_socket_ssl(u, void_pp)
+ else
+ ret = C.ngx_http_lua_resty_openssl_aux_get_socket_ssl(u, void_pp)
+ end
+
+ if ret ~= NGX_OK then
+ return nil, "cannot read u->peer.connection->ssl->connection"
+ end
+
+ return ffi.cast(ssl_type, void_pp[0])
+end
+
+get_socket_ssl_ctx = function(sock)
+ local u = sock[SOCKET_CTX_INDEX]
+
+ local ret
+ if stream_subsystem then
+ ret = C.ngx_stream_lua_resty_openssl_aux_get_socket_ssl_ctx(u, void_pp)
+ else
+ ret = C.ngx_http_lua_resty_openssl_aux_get_socket_ssl_ctx(u, void_pp)
+ end
+
+ if ret ~= NGX_OK then
+ return nil, "cannot read u->peer.connection->ssl->session_ctx"
+ end
+
+ return ffi.cast(ssl_ctx_type, void_pp[0])
+end
+
+return {
+ get_req_ssl = get_req_ssl,
+ get_req_ssl_ctx = get_req_ssl_ctx,
+ get_socket_ssl = get_socket_ssl,
+ get_socket_ssl_ctx = get_socket_ssl_ctx,
+} \ No newline at end of file
diff --git a/server/resty/openssl/bn.lua b/server/resty/openssl/bn.lua
new file mode 100644
index 0000000..e893e5e
--- /dev/null
+++ b/server/resty/openssl/bn.lua
@@ -0,0 +1,416 @@
+local ffi = require "ffi"
+local C = ffi.C
+local ffi_gc = ffi.gc
+local ffi_new = ffi.new
+local ffi_str = ffi.string
+local floor = math.floor
+
+require "resty.openssl.include.bn"
+local crypto_macro = require("resty.openssl.include.crypto")
+local ctypes = require "resty.openssl.auxiliary.ctypes"
+local format_error = require("resty.openssl.err").format_error
+local OPENSSL_10 = require("resty.openssl.version").OPENSSL_10
+local OPENSSL_3X = require("resty.openssl.version").OPENSSL_3X
+
+local _M = {}
+local mt = {__index = _M}
+
+local bn_ptr_ct = ffi.typeof('BIGNUM*')
+local bn_ptrptr_ct = ffi.typeof('BIGNUM*[1]')
+
+function _M.new(bn)
+ local ctx = C.BN_new()
+ ffi_gc(ctx, C.BN_free)
+
+ if type(bn) == 'number' then
+ if C.BN_set_word(ctx, bn) ~= 1 then
+ return nil, format_error("bn.new")
+ end
+ elseif bn then
+ return nil, "bn.new: expect nil or a number at #1"
+ end
+
+ return setmetatable( { ctx = ctx }, mt), nil
+end
+
+function _M.istype(l)
+ return l and l.ctx and ffi.istype(bn_ptr_ct, l.ctx)
+end
+
+function _M.dup(ctx)
+ if not ffi.istype(bn_ptr_ct, ctx) then
+ return nil, "bn.dup: expect a bn ctx at #1"
+ end
+ local ctx = C.BN_dup(ctx)
+ ffi_gc(ctx, C.BN_free)
+
+ local self = setmetatable({
+ ctx = ctx,
+ }, mt)
+
+ return self
+end
+
+function _M:to_binary()
+ local length = (C.BN_num_bits(self.ctx)+7)/8
+ -- align to bytes
+ length = floor(length)
+ local buf = ctypes.uchar_array(length)
+ local sz = C.BN_bn2bin(self.ctx, buf)
+ if sz == 0 then
+ return nil, format_error("bn:to_binary")
+ end
+ buf = ffi_str(buf, length)
+ return buf
+end
+
+function _M.from_binary(s)
+ if type(s) ~= "string" then
+ return nil, "bn.from_binary: expect a string at #1"
+ end
+
+ local ctx = C.BN_bin2bn(s, #s, nil)
+ if ctx == nil then
+ return nil, format_error("bn.from_binary")
+ end
+ ffi_gc(ctx, C.BN_free)
+ return setmetatable( { ctx = ctx }, mt), nil
+end
+
+function _M:to_hex()
+ local buf = C.BN_bn2hex(self.ctx)
+ if buf == nil then
+ return nil, format_error("bn:to_hex")
+ end
+ ffi_gc(buf, crypto_macro.OPENSSL_free)
+ local s = ffi_str(buf)
+ return s
+end
+
+function _M.from_hex(s)
+ if type(s) ~= "string" then
+ return nil, "bn.from_hex: expect a string at #1"
+ end
+
+ local p = ffi_new(bn_ptrptr_ct)
+
+ if C.BN_hex2bn(p, s) == 0 then
+ return nil, format_error("bn.from_hex")
+ end
+ local ctx = p[0]
+ ffi_gc(ctx, C.BN_free)
+ return setmetatable( { ctx = ctx }, mt), nil
+end
+
+function _M:to_dec()
+ local buf = C.BN_bn2dec(self.ctx)
+ if buf == nil then
+ return nil, format_error("bn:to_dec")
+ end
+ ffi_gc(buf, crypto_macro.OPENSSL_free)
+ local s = ffi_str(buf)
+ return s
+end
+mt.__tostring = _M.to_dec
+
+function _M.from_dec(s)
+ if type(s) ~= "string" then
+ return nil, "bn.from_dec: expect a string at #1"
+ end
+
+ local p = ffi_new(bn_ptrptr_ct)
+
+ if C.BN_dec2bn(p, s) == 0 then
+ return nil, format_error("bn.from_dec")
+ end
+ local ctx = p[0]
+ ffi_gc(ctx, C.BN_free)
+ return setmetatable( { ctx = ctx }, mt), nil
+end
+
+function _M:to_number()
+ return tonumber(C.BN_get_word(self.ctx))
+end
+_M.tonumber = _M.to_number
+
+function _M.generate_prime(bits, safe)
+ local ctx = C.BN_new()
+ ffi_gc(ctx, C.BN_free)
+
+ if C.BN_generate_prime_ex(ctx, bits, safe and 1 or 0, nil, nil, nil) == 0 then
+ return nil, format_error("bn.BN_generate_prime_ex")
+ end
+
+ return setmetatable( { ctx = ctx }, mt), nil
+end
+
+-- BN_CTX is used to store temporary variable
+-- we only need one per worker
+local bn_ctx_tmp = C.BN_CTX_new()
+assert(bn_ctx_tmp ~= nil)
+if OPENSSL_10 then
+ C.BN_CTX_init(bn_ctx_tmp)
+end
+ffi_gc(bn_ctx_tmp, C.BN_CTX_free)
+
+_M.bn_ctx_tmp = bn_ctx_tmp
+
+-- mathematics
+
+local is_negative
+if OPENSSL_10 then
+ local bn_zero = assert(_M.new(0)).ctx
+ is_negative = function(ctx)
+ return C.BN_cmp(ctx, bn_zero) < 0 and 1 or 0
+ end
+else
+ is_negative = C.BN_is_negative
+end
+function mt.__unm(a)
+ local b = _M.dup(a.ctx)
+ if b == nil then
+ error("BN_dup() failed")
+ end
+ local sign = is_negative(b.ctx)
+ C.BN_set_negative(b.ctx, 1-sign)
+ return b
+end
+
+local function check_args(op, ...)
+ local args = {...}
+ for i, arg in ipairs(args) do
+ if type(arg) == 'number' then
+ local b = C.BN_new()
+ if b == nil then
+ error("BN_new() failed")
+ end
+ ffi_gc(b, C.BN_free)
+ if C.BN_set_word(b, arg) ~= 1 then
+ error("BN_set_word() failed")
+ end
+ args[i] = b
+ elseif _M.istype(arg) then
+ args[i] = arg.ctx
+ else
+ error("cannot " .. op .. " a " .. type(arg) .. " to bignum")
+ end
+ end
+ local ctx = C.BN_new()
+ if ctx == nil then
+ error("BN_new() failed")
+ end
+ ffi_gc(ctx, C.BN_free)
+ local r = setmetatable( { ctx = ctx }, mt)
+ return r, unpack(args)
+end
+
+
+function mt.__add(...)
+ local r, a, b = check_args("add", ...)
+ if C.BN_add(r.ctx, a, b) == 0 then
+ error("BN_add() failed")
+ end
+ return r
+end
+_M.add = mt.__add
+
+function mt.__sub(...)
+ local r, a, b = check_args("substract", ...)
+ if C.BN_sub(r.ctx, a, b) == 0 then
+ error("BN_sub() failed")
+ end
+ return r
+end
+_M.sub = mt.__sub
+
+function mt.__mul(...)
+ local r, a, b = check_args("multiply", ...)
+ if C.BN_mul(r.ctx, a, b, bn_ctx_tmp) == 0 then
+ error("BN_mul() failed")
+ end
+ return r
+end
+_M.mul = mt.__mul
+
+-- lua 5.3 only
+function mt.__idiv(...)
+ local r, a, b = check_args("divide", ...)
+ if C.BN_div(r.ctx, nil, a, b, bn_ctx_tmp) == 0 then
+ error("BN_div() failed")
+ end
+ return r
+end
+
+mt.__div = mt.__idiv
+_M.idiv = mt.__idiv
+_M.div = mt.__div
+
+function mt.__mod(...)
+ local r, a, b = check_args("mod", ...)
+ if C.BN_div(nil, r.ctx, a, b, bn_ctx_tmp) == 0 then
+ error("BN_div() failed")
+ end
+ return r
+end
+_M.mod = mt.__mod
+
+-- __concat doesn't make sense at all?
+
+function _M.sqr(...)
+ local r, a = check_args("square", ...)
+ if C.BN_sqr(r.ctx, a, bn_ctx_tmp) == 0 then
+ error("BN_sqr() failed")
+ end
+ return r
+end
+
+function _M.gcd(...)
+ local r, a, b = check_args("extract greatest common divisor", ...)
+ if C.BN_gcd(r.ctx, a, b, bn_ctx_tmp) == 0 then
+ error("BN_gcd() failed")
+ end
+ return r
+end
+
+function _M.exp(...)
+ local r, a, b = check_args("power", ...)
+ if C.BN_exp(r.ctx, a, b, bn_ctx_tmp) == 0 then
+ error("BN_exp() failed")
+ end
+ return r
+end
+_M.pow = _M.exp
+
+for _, op in ipairs({ "add", "sub" , "mul", "exp" }) do
+ local f = "BN_mod_" .. op
+ local cf = C[f]
+ _M["mod_" .. op] = function(...)
+ local r, a, b, m = check_args(op, ...)
+ if cf(r.ctx, a, b, m, bn_ctx_tmp) == 0 then
+ error(f .. " failed")
+ end
+ return r
+ end
+end
+
+function _M.mod_sqr(...)
+ local r, a, m = check_args("mod_sub", ...)
+ if C.BN_mod_sqr(r.ctx, a, m, bn_ctx_tmp) == 0 then
+ error("BN_mod_sqr() failed")
+ end
+ return r
+end
+
+local function nyi()
+ error("NYI")
+end
+
+-- bit operations, lua 5.3
+
+mt.__band = nyi
+mt.__bor = nyi
+mt.__bxor = nyi
+mt.__bnot = nyi
+
+function mt.__shl(a, b)
+ local r, a = check_args("lshift", a)
+ if C.BN_lshift(r.ctx, a, b) == 0 then
+ error("BN_lshift() failed")
+ end
+ return r
+end
+_M.lshift = mt.__shl
+
+function mt.__shr(a, b)
+ local r, a = check_args("rshift", a)
+ if C.BN_rshift(r.ctx, a, b) == 0 then
+ error("BN_lshift() failed")
+ end
+ return r
+end
+_M.rshift = mt.__shr
+
+-- comparaions
+-- those functions are only called when the table
+-- has exact same metamethods, i.e. they are all BN
+-- so we don't need to check args
+
+function mt.__eq(a, b)
+ return C.BN_cmp(a.ctx, b.ctx) == 0
+end
+
+function mt.__lt(a, b)
+ return C.BN_cmp(a.ctx, b.ctx) < 0
+end
+
+function mt.__le(a, b)
+ return C.BN_cmp(a.ctx, b.ctx) <= 0
+end
+
+if OPENSSL_10 then
+ -- in openssl 1.0.x those functions are implemented as macros
+ -- don't want to copy paste all structs here
+ -- the followings are definitely slower, but works
+ local bn_zero = assert(_M.new(0)).ctx
+ local bn_one = assert(_M.new(1)).ctx
+
+ function _M:is_zero()
+ return C.BN_cmp(self.ctx, bn_zero) == 0
+ end
+
+ function _M:is_one()
+ return C.BN_cmp(self.ctx, bn_one) == 0
+ end
+
+ function _M:is_word(n)
+ local ctx = C.BN_new()
+ ffi_gc(ctx, C.BN_free)
+ if ctx == nil then
+ return nil, "bn:is_word: BN_new() failed"
+ end
+ if C.BN_set_word(ctx, n) ~= 1 then
+ return nil, "bn:is_word: BN_set_word() failed"
+ end
+ return C.BN_cmp(self.ctx, ctx) == 0
+ end
+
+ function _M:is_odd()
+ return self:to_number() % 2 == 1
+ end
+else
+ function _M:is_zero()
+ return C.BN_is_zero(self.ctx) == 1
+ end
+
+ function _M:is_one()
+ return C.BN_is_one(self.ctx) == 1
+ end
+
+ function _M:is_word(n)
+ return C.BN_is_word(self.ctx, n) == 1
+ end
+
+ function _M:is_odd()
+ return C.BN_is_odd(self.ctx) == 1
+ end
+end
+
+function _M:is_prime(nchecks)
+ if nchecks and type(nchecks) ~= "number" then
+ return nil, "bn:is_prime: expect a number at #1"
+ end
+ -- if nchecks is not defined, set to BN_prime_checks:
+ -- select number of iterations based on the size of the number
+ local code
+ if OPENSSL_3X then
+ code = C.BN_check_prime(self.ctx, bn_ctx_tmp, nil)
+ else
+ code = C.BN_is_prime_ex(self.ctx, nchecks or 0, bn_ctx_tmp, nil)
+ end
+ if code == -1 then
+ return nil, format_error("bn.is_prime")
+ end
+ return code == 1
+end
+
+return _M
diff --git a/server/resty/openssl/cipher.lua b/server/resty/openssl/cipher.lua
new file mode 100644
index 0000000..693ac09
--- /dev/null
+++ b/server/resty/openssl/cipher.lua
@@ -0,0 +1,300 @@
+local ffi = require "ffi"
+local C = ffi.C
+local ffi_gc = ffi.gc
+local ffi_str = ffi.string
+local ffi_cast = ffi.cast
+
+require "resty.openssl.include.evp.cipher"
+local evp_macro = require "resty.openssl.include.evp"
+local ctypes = require "resty.openssl.auxiliary.ctypes"
+local ctx_lib = require "resty.openssl.ctx"
+local format_error = require("resty.openssl.err").format_error
+local OPENSSL_10 = require("resty.openssl.version").OPENSSL_10
+local OPENSSL_11_OR_LATER = require("resty.openssl.version").OPENSSL_11_OR_LATER
+local OPENSSL_3X = require("resty.openssl.version").OPENSSL_3X
+
+local uchar_array = ctypes.uchar_array
+local void_ptr = ctypes.void_ptr
+local ptr_of_int = ctypes.ptr_of_int
+local uchar_ptr = ctypes.uchar_ptr
+
+local _M = {}
+local mt = {__index = _M}
+
+local cipher_ctx_ptr_ct = ffi.typeof('EVP_CIPHER_CTX*')
+
+local out_length = ptr_of_int()
+-- EVP_MAX_BLOCK_LENGTH is 32, we give it a 64 to be future proof
+local out_buffer = ctypes.uchar_array(1024 + 64)
+
+function _M.new(typ, properties)
+ if not typ then
+ return nil, "cipher.new: expect type to be defined"
+ end
+
+ local ctx
+ if OPENSSL_11_OR_LATER then
+ ctx = C.EVP_CIPHER_CTX_new()
+ ffi_gc(ctx, C.EVP_CIPHER_CTX_free)
+ elseif OPENSSL_10 then
+ ctx = ffi.new('EVP_CIPHER_CTX')
+ C.EVP_CIPHER_CTX_init(ctx)
+ ffi_gc(ctx, C.EVP_CIPHER_CTX_cleanup)
+ end
+ if ctx == nil then
+ return nil, "cipher.new: failed to create EVP_CIPHER_CTX"
+ end
+
+ local ctyp
+ if OPENSSL_3X then
+ ctyp = C.EVP_CIPHER_fetch(ctx_lib.get_libctx(), typ, properties)
+ else
+ ctyp = C.EVP_get_cipherbyname(typ)
+ end
+ local err_new = string.format("cipher.new: invalid cipher type \"%s\"", typ)
+ if ctyp == nil then
+ return nil, format_error(err_new)
+ end
+
+ local code = C.EVP_CipherInit_ex(ctx, ctyp, nil, "", nil, -1)
+ if code ~= 1 then
+ return nil, format_error(err_new)
+ end
+
+ return setmetatable({
+ ctx = ctx,
+ algo = ctyp,
+ initialized = false,
+ block_size = tonumber(OPENSSL_3X and C.EVP_CIPHER_CTX_get_block_size(ctx)
+ or C.EVP_CIPHER_CTX_block_size(ctx)),
+ key_size = tonumber(OPENSSL_3X and C.EVP_CIPHER_CTX_get_key_length(ctx)
+ or C.EVP_CIPHER_CTX_key_length(ctx)),
+ iv_size = tonumber(OPENSSL_3X and C.EVP_CIPHER_CTX_get_iv_length(ctx)
+ or C.EVP_CIPHER_CTX_iv_length(ctx)),
+ }, mt), nil
+end
+
+function _M.istype(l)
+ return l and l.ctx and ffi.istype(cipher_ctx_ptr_ct, l.ctx)
+end
+
+function _M:get_provider_name()
+ if not OPENSSL_3X then
+ return false, "cipher:get_provider_name is not supported"
+ end
+ local p = C.EVP_CIPHER_get0_provider(self.algo)
+ if p == nil then
+ return nil
+ end
+ return ffi_str(C.OSSL_PROVIDER_get0_name(p))
+end
+
+if OPENSSL_3X then
+ local param_lib = require "resty.openssl.param"
+ _M.settable_params, _M.set_params, _M.gettable_params, _M.get_param = param_lib.get_params_func("EVP_CIPHER_CTX")
+end
+
+function _M:init(key, iv, opts)
+ opts = opts or {}
+ if not key or #key ~= self.key_size then
+ return false, string.format("cipher:init: incorrect key size, expect %d", self.key_size)
+ end
+ if not iv or #iv ~= self.iv_size then
+ return false, string.format("cipher:init: incorrect iv size, expect %d", self.iv_size)
+ end
+
+ -- always passed in the `EVP_CIPHER` parameter to reinitialized the cipher
+ -- it will have a same effect as EVP_CIPHER_CTX_cleanup/EVP_CIPHER_CTX_reset then Init_ex with
+ -- empty algo
+ if C.EVP_CipherInit_ex(self.ctx, self.algo, nil, key, iv, opts.is_encrypt and 1 or 0) == 0 then
+ return false, format_error("cipher:init EVP_CipherInit_ex")
+ end
+
+ if opts.no_padding then
+ -- EVP_CIPHER_CTX_set_padding() always returns 1.
+ C.EVP_CIPHER_CTX_set_padding(self.ctx, 0)
+ end
+
+ self.initialized = true
+
+ return true
+end
+
+function _M:encrypt(key, iv, s, no_padding, aead_aad)
+ local _, err = self:init(key, iv, {
+ is_encrypt = true,
+ no_padding = no_padding,
+ })
+ if err then
+ return nil, err
+ end
+ if aead_aad then
+ local _, err = self:update_aead_aad(aead_aad)
+ if err then
+ return nil, err
+ end
+ end
+ return self:final(s)
+end
+
+function _M:decrypt(key, iv, s, no_padding, aead_aad, aead_tag)
+ local _, err = self:init(key, iv, {
+ is_encrypt = false,
+ no_padding = no_padding,
+ })
+ if err then
+ return nil, err
+ end
+ if aead_aad then
+ local _, err = self:update_aead_aad(aead_aad)
+ if err then
+ return nil, err
+ end
+ end
+ if aead_tag then
+ local _, err = self:set_aead_tag(aead_tag)
+ if err then
+ return nil, err
+ end
+ end
+ return self:final(s)
+end
+
+-- https://wiki.openssl.org/index.php/EVP_Authenticated_Encryption_and_Decryption
+function _M:update_aead_aad(aad)
+ if not self.initialized then
+ return nil, "cipher:update_aead_aad: cipher not initalized, call cipher:init first"
+ end
+
+ if C.EVP_CipherUpdate(self.ctx, nil, out_length, aad, #aad) ~= 1 then
+ return false, format_error("cipher:update_aead_aad")
+ end
+ return true
+end
+
+function _M:get_aead_tag(size)
+ if not self.initialized then
+ return nil, "cipher:get_aead_tag: cipher not initalized, call cipher:init first"
+ end
+
+ size = size or self.key_size / 2
+ if size > self.key_size then
+ return nil, string.format("tag size %d is too large", size)
+ end
+ if C.EVP_CIPHER_CTX_ctrl(self.ctx, evp_macro.EVP_CTRL_AEAD_GET_TAG, size, out_buffer) ~= 1 then
+ return nil, format_error("cipher:get_aead_tag")
+ end
+
+ return ffi_str(out_buffer, size)
+end
+
+function _M:set_aead_tag(tag)
+ if not self.initialized then
+ return nil, "cipher:set_aead_tag: cipher not initalized, call cipher:init first"
+ end
+
+ if type(tag) ~= "string" then
+ return false, "cipher:set_aead_tag expect a string at #1"
+ end
+ local tag_void_ptr = ffi_cast(void_ptr, tag)
+ if C.EVP_CIPHER_CTX_ctrl(self.ctx, evp_macro.EVP_CTRL_AEAD_SET_TAG, #tag, tag_void_ptr) ~= 1 then
+ return false, format_error("cipher:set_aead_tag")
+ end
+
+ return true
+end
+
+function _M:update(...)
+ if not self.initialized then
+ return nil, "cipher:update: cipher not initalized, call cipher:init first"
+ end
+
+ local ret = {}
+ for i, s in ipairs({...}) do
+ local inl = #s
+ if inl > 1024 then
+ s = ffi_cast(uchar_ptr, s)
+ for i=0, inl-1, 1024 do
+ local chunk_size = 1024
+ if inl - i < 1024 then
+ chunk_size = inl - i
+ end
+ if C.EVP_CipherUpdate(self.ctx, out_buffer, out_length, s+i, chunk_size) ~= 1 then
+ return nil, format_error("cipher:update")
+ end
+ table.insert(ret, ffi_str(out_buffer, out_length[0]))
+ end
+ else
+ if C.EVP_CipherUpdate(self.ctx, out_buffer, out_length, s, inl) ~= 1 then
+ return nil, format_error("cipher:update")
+ end
+ table.insert(ret, ffi_str(out_buffer, out_length[0]))
+ end
+ end
+ return table.concat(ret, "")
+end
+
+function _M:final(s)
+ local ret, err
+ if s then
+ ret, err = self:update(s)
+ if err then
+ return nil, err
+ end
+ end
+ if C.EVP_CipherFinal_ex(self.ctx, out_buffer, out_length) ~= 1 then
+ return nil, format_error("cipher:final: EVP_CipherFinal_ex")
+ end
+ local final_ret = ffi_str(out_buffer, out_length[0])
+ return ret and (ret .. final_ret) or final_ret
+end
+
+
+function _M:derive(key, salt, count, md, md_properties)
+ if type(key) ~= "string" then
+ return nil, nil, "cipher:derive: expect a string at #1"
+ elseif salt and type(salt) ~= "string" then
+ return nil, nil, "cipher:derive: expect a string at #2"
+ elseif count then
+ count = tonumber(count)
+ if not count then
+ return nil, nil, "cipher:derive: expect a number at #3"
+ end
+ elseif md and type(md) ~= "string" then
+ return nil, nil, "cipher:derive: expect a string or nil at #4"
+ end
+
+ if salt then
+ if #salt > 8 then
+ ngx.log(ngx.WARN, "cipher:derive: salt is too long, truncate salt to 8 bytes")
+ salt = salt:sub(0, 8)
+ elseif #salt < 8 then
+ ngx.log(ngx.WARN, "cipher:derive: salt is too short, padding with zero bytes to length")
+ salt = salt .. string.rep('\000', 8 - #salt)
+ end
+ end
+
+ local mdt
+ if OPENSSL_3X then
+ mdt = C.EVP_MD_fetch(ctx_lib.get_libctx(), md or 'sha1', md_properties)
+ else
+ mdt = C.EVP_get_digestbyname(md or 'sha1')
+ end
+ if mdt == nil then
+ return nil, nil, string.format("cipher:derive: invalid digest type \"%s\"", md)
+ end
+ local cipt = C.EVP_CIPHER_CTX_cipher(self.ctx)
+ local keyb = uchar_array(self.key_size)
+ local ivb = uchar_array(self.iv_size)
+
+ local size = C.EVP_BytesToKey(cipt, mdt, salt,
+ key, #key, count or 1,
+ keyb, ivb)
+ if size == 0 then
+ return nil, nil, format_error("cipher:derive: EVP_BytesToKey")
+ end
+
+ return ffi_str(keyb, size), ffi_str(ivb, self.iv_size)
+end
+
+return _M
diff --git a/server/resty/openssl/ctx.lua b/server/resty/openssl/ctx.lua
new file mode 100644
index 0000000..eaec396
--- /dev/null
+++ b/server/resty/openssl/ctx.lua
@@ -0,0 +1,78 @@
+local ffi = require "ffi"
+local C = ffi.C
+local ffi_gc = ffi.gc
+
+require "resty.openssl.include.ossl_typ"
+local format_error = require("resty.openssl.err").format_error
+local OPENSSL_3X = require("resty.openssl.version").OPENSSL_3X
+
+ffi.cdef [[
+ OSSL_LIB_CTX *OSSL_LIB_CTX_new(void);
+ int OSSL_LIB_CTX_load_config(OSSL_LIB_CTX *ctx, const char *config_file);
+ void OSSL_LIB_CTX_free(OSSL_LIB_CTX *ctx);
+]]
+
+local ossl_lib_ctx
+
+local function new(request_context_only, conf_file)
+ if not OPENSSL_3X then
+ return false, "ctx is only supported from OpenSSL 3.0"
+ end
+
+ local ctx = C.OSSL_LIB_CTX_new()
+ ffi_gc(ctx, C.OSSL_LIB_CTX_free)
+
+ if conf_file and C.OSSL_LIB_CTX_load_config(ctx, conf_file) ~= 1 then
+ return false, format_error("ctx.new")
+ end
+
+ if request_context_only then
+ ngx.ctx.ossl_lib_ctx = ctx
+ else
+ ossl_lib_ctx = ctx
+ end
+
+ return true
+end
+
+local function free(request_context_only)
+ if not OPENSSL_3X then
+ return false, "ctx is only supported from OpenSSL 3.0"
+ end
+
+ if request_context_only then
+ ngx.ctx.ossl_lib_ctx = nil
+ else
+ ossl_lib_ctx = nil
+ end
+
+ return true
+end
+
+local test_request
+
+do
+
+ local ok, exdata = pcall(require, "thread.exdata")
+ if ok and exdata then
+ test_request = function()
+ local r = exdata()
+ if r ~= nil then
+ return not not r
+ end
+ end
+
+ else
+ local getfenv = getfenv
+
+ function test_request()
+ return not not getfenv(0).__ngx_req
+ end
+ end
+end
+
+return {
+ new = new,
+ free = free,
+ get_libctx = function() return test_request() and ngx.ctx.ossl_lib_ctx or ossl_lib_ctx end,
+} \ No newline at end of file
diff --git a/server/resty/openssl/dh.lua b/server/resty/openssl/dh.lua
new file mode 100644
index 0000000..93e4941
--- /dev/null
+++ b/server/resty/openssl/dh.lua
@@ -0,0 +1,142 @@
+local ffi = require "ffi"
+local C = ffi.C
+
+require "resty.openssl.include.dh"
+local bn_lib = require "resty.openssl.bn"
+
+local OPENSSL_10 = require("resty.openssl.version").OPENSSL_10
+local OPENSSL_11_OR_LATER = require("resty.openssl.version").OPENSSL_11_OR_LATER
+local format_error = require("resty.openssl.err").format_error
+
+local _M = {}
+
+_M.params = {"public", "private", "p", "q", "g"}
+
+local empty_table = {}
+local bn_ptrptr_ct = ffi.typeof("const BIGNUM *[1]")
+function _M.get_parameters(dh_st)
+ return setmetatable(empty_table, {
+ __index = function(_, k)
+ local ptr, ret
+ if OPENSSL_11_OR_LATER then
+ ptr = bn_ptrptr_ct()
+ end
+
+ if OPENSSL_11_OR_LATER then
+ ptr = bn_ptrptr_ct()
+ end
+
+ if k == 'p' then
+ if OPENSSL_11_OR_LATER then
+ C.DH_get0_pqg(dh_st, ptr, nil, nil)
+ end
+ elseif k == 'q' then
+ if OPENSSL_11_OR_LATER then
+ C.DH_get0_pqg(dh_st, nil, ptr, nil)
+ end
+ elseif k == 'g' then
+ if OPENSSL_11_OR_LATER then
+ C.DH_get0_pqg(dh_st, nil, nil, ptr)
+ end
+ elseif k == 'public' then
+ if OPENSSL_11_OR_LATER then
+ C.DH_get0_key(dh_st, ptr, nil)
+ end
+ k = "pub_key"
+ elseif k == 'private' then
+ if OPENSSL_11_OR_LATER then
+ C.DH_get0_key(dh_st, nil, ptr)
+ end
+ k = "priv_key"
+ else
+ return nil, "rsa.get_parameters: unknown parameter \"" .. k .. "\" for RSA key"
+ end
+
+ if OPENSSL_11_OR_LATER then
+ ret = ptr[0]
+ elseif OPENSSL_10 then
+ ret = dh_st[k]
+ end
+
+ if ret == nil then
+ return nil
+ end
+ return bn_lib.dup(ret)
+ end
+ }), nil
+end
+
+local function dup_bn_value(v)
+ if not bn_lib.istype(v) then
+ return nil, "expect value to be a bn instance"
+ end
+ local bn = C.BN_dup(v.ctx)
+ if bn == nil then
+ return nil, "BN_dup() failed"
+ end
+ return bn
+end
+
+function _M.set_parameters(dh_st, opts)
+ local err
+ local opts_bn = {}
+ -- remember which parts of BNs has been added to dh_st, they should be freed
+ -- by DH_free and we don't cleanup them on failure
+ local cleanup_from_idx = 1
+ -- dup input
+ local do_set_key, do_set_pqg
+ for k, v in pairs(opts) do
+ opts_bn[k], err = dup_bn_value(v)
+ if err then
+ err = "dh.set_parameters: cannot process parameter \"" .. k .. "\":" .. err
+ goto cleanup_with_error
+ end
+ if k == "private" or k == "public" then
+ do_set_key = true
+ elseif k == "p" or k == "q" or k == "g" then
+ do_set_pqg = true
+ end
+ end
+ if OPENSSL_11_OR_LATER then
+ local code
+ if do_set_key then
+ code = C.DH_set0_key(dh_st, opts_bn["public"], opts_bn["private"])
+ if code == 0 then
+ err = format_error("dh.set_parameters: DH_set0_key")
+ goto cleanup_with_error
+ end
+ end
+ cleanup_from_idx = cleanup_from_idx + 2
+ if do_set_pqg then
+ code = C.DH_set0_pqg(dh_st, opts_bn["p"], opts_bn["q"], opts_bn["g"])
+ if code == 0 then
+ err = format_error("dh.set_parameters: DH_set0_pqg")
+ goto cleanup_with_error
+ end
+ end
+ return true
+ elseif OPENSSL_10 then
+ for k, v in pairs(opts_bn) do
+ if k == "public" then
+ k = "pub_key"
+ elseif k == "private" then
+ k = "priv_key"
+ end
+ if dh_st[k] ~= nil then
+ C.BN_free(dh_st[k])
+ end
+ dh_st[k]= v
+ end
+ return true
+ end
+
+::cleanup_with_error::
+ for i, k in pairs(_M.params) do
+ if i >= cleanup_from_idx then
+ C.BN_free(opts_bn[k])
+ end
+ end
+ return false, err
+end
+
+return _M
diff --git a/server/resty/openssl/digest.lua b/server/resty/openssl/digest.lua
new file mode 100644
index 0000000..cfef9ae
--- /dev/null
+++ b/server/resty/openssl/digest.lua
@@ -0,0 +1,116 @@
+local ffi = require "ffi"
+local C = ffi.C
+local ffi_gc = ffi.gc
+local ffi_str = ffi.string
+
+require "resty.openssl.include.evp.md"
+local ctypes = require "resty.openssl.auxiliary.ctypes"
+local ctx_lib = require "resty.openssl.ctx"
+local format_error = require("resty.openssl.err").format_error
+local OPENSSL_10 = require("resty.openssl.version").OPENSSL_10
+local OPENSSL_11_OR_LATER = require("resty.openssl.version").OPENSSL_11_OR_LATER
+local OPENSSL_3X = require("resty.openssl.version").OPENSSL_3X
+
+local _M = {}
+local mt = {__index = _M}
+
+local md_ctx_ptr_ct = ffi.typeof('EVP_MD_CTX*')
+
+function _M.new(typ, properties)
+ local ctx
+ if OPENSSL_11_OR_LATER then
+ ctx = C.EVP_MD_CTX_new()
+ ffi_gc(ctx, C.EVP_MD_CTX_free)
+ elseif OPENSSL_10 then
+ ctx = C.EVP_MD_CTX_create()
+ ffi_gc(ctx, C.EVP_MD_CTX_destroy)
+ end
+ if ctx == nil then
+ return nil, "digest.new: failed to create EVP_MD_CTX"
+ end
+
+ local err_new = string.format("digest.new: invalid digest type \"%s\"", typ)
+
+ local algo
+ if typ == "null" then
+ algo = C.EVP_md_null()
+ else
+ if OPENSSL_3X then
+ algo = C.EVP_MD_fetch(ctx_lib.get_libctx(), typ or 'sha1', properties)
+ else
+ algo = C.EVP_get_digestbyname(typ or 'sha1')
+ end
+ if algo == nil then
+ return nil, format_error(err_new)
+ end
+ end
+
+ local code = C.EVP_DigestInit_ex(ctx, algo, nil)
+ if code ~= 1 then
+ return nil, format_error(err_new)
+ end
+
+ return setmetatable({
+ ctx = ctx,
+ algo = algo,
+ buf = ctypes.uchar_array(OPENSSL_3X and C.EVP_MD_get_size(algo) or C.EVP_MD_size(algo)),
+ }, mt), nil
+end
+
+function _M.istype(l)
+ return l and l.ctx and ffi.istype(md_ctx_ptr_ct, l.ctx)
+end
+
+function _M:get_provider_name()
+ if not OPENSSL_3X then
+ return false, "digest:get_provider_name is not supported"
+ end
+ local p = C.EVP_MD_get0_provider(self.algo)
+ if p == nil then
+ return nil
+ end
+ return ffi_str(C.OSSL_PROVIDER_get0_name(p))
+end
+
+if OPENSSL_3X then
+ local param_lib = require "resty.openssl.param"
+ _M.settable_params, _M.set_params, _M.gettable_params, _M.get_param = param_lib.get_params_func("EVP_MD_CTX")
+end
+
+function _M:update(...)
+ for _, s in ipairs({...}) do
+ if C.EVP_DigestUpdate(self.ctx, s, #s) ~= 1 then
+ return false, format_error("digest:update")
+ end
+ end
+ return true, nil
+end
+
+local result_length = ctypes.ptr_of_uint()
+
+function _M:final(s)
+ if s then
+ if C.EVP_DigestUpdate(self.ctx, s, #s) ~= 1 then
+ return false, format_error("digest:final")
+ end
+ end
+
+ -- no return value of EVP_DigestFinal_ex
+ C.EVP_DigestFinal_ex(self.ctx, self.buf, result_length)
+ if result_length[0] == nil or result_length[0] <= 0 then
+ return nil, format_error("digest:final: EVP_DigestFinal_ex")
+ end
+ return ffi_str(self.buf, result_length[0])
+end
+
+
+function _M:reset()
+ local code = C.EVP_DigestInit_ex(self.ctx, self.algo, nil)
+ if code ~= 1 then
+ return false, format_error("digest:reset")
+ end
+
+ return true
+end
+
+return _M
diff --git a/server/resty/openssl/ec.lua b/server/resty/openssl/ec.lua
new file mode 100644
index 0000000..2d0dd02
--- /dev/null
+++ b/server/resty/openssl/ec.lua
@@ -0,0 +1,186 @@
+local ffi = require "ffi"
+local C = ffi.C
+local ffi_gc = ffi.gc
+
+require "resty.openssl.include.ec"
+local bn_lib = require "resty.openssl.bn"
+local objects_lib = require "resty.openssl.objects"
+local ctypes = require "resty.openssl.auxiliary.ctypes"
+
+local version_num = require("resty.openssl.version").version_num
+local format_error = require("resty.openssl.err").format_error
+local BORINGSSL = require("resty.openssl.version").BORINGSSL
+
+local _M = {}
+
+_M.params = {"group", "public", "private", "x", "y"}
+
+local empty_table = {}
+
+function _M.get_parameters(ec_key_st)
+ return setmetatable(empty_table, {
+ __index = function(_, k)
+ local group = C.EC_KEY_get0_group(ec_key_st)
+ local bn
+
+ if k == 'group' then
+ local nid = C.EC_GROUP_get_curve_name(group)
+ if nid == 0 then
+ return nil, "ec.get_parameters: EC_GROUP_get_curve_name() failed"
+ end
+ return nid
+ elseif k == 'public' or k == "pub_key" then
+ local pub_point = C.EC_KEY_get0_public_key(ec_key_st)
+ if pub_point == nil then
+ return nil, format_error("ec.get_parameters: EC_KEY_get0_public_key")
+ end
+ local point_form = C.EC_KEY_get_conv_form(ec_key_st)
+ if point_form == nil then
+ return nil, format_error("ec.get_parameters: EC_KEY_get_conv_form")
+ end
+ if BORINGSSL then
+ local sz = tonumber(C.EC_POINT_point2oct(group, pub_point, point_form, nil, 0, bn_lib.bn_ctx_tmp))
+ if sz <= 0 then
+ return nil, format_error("ec.get_parameters: EC_POINT_point2oct")
+ end
+ local buf = ctypes.uchar_array(sz)
+ C.EC_POINT_point2oct(group, pub_point, point_form, buf, sz, bn_lib.bn_ctx_tmp)
+ buf = ffi.string(buf, sz)
+ local err
+ bn, err = bn_lib.from_binary(buf)
+ if bn == nil then
+ return nil, "ec.get_parameters: bn_lib.from_binary: " .. err
+ end
+ return bn
+ else
+ bn = C.EC_POINT_point2bn(group, pub_point, point_form, nil, bn_lib.bn_ctx_tmp)
+ if bn == nil then
+ return nil, format_error("ec.get_parameters: EC_POINT_point2bn")
+ end
+ ffi_gc(bn, C.BN_free)
+ end
+ elseif k == 'private' or k == "priv_key" then
+ -- get0, don't GC
+ bn = C.EC_KEY_get0_private_key(ec_key_st)
+ elseif k == 'x' or k == 'y' then
+ local pub_point = C.EC_KEY_get0_public_key(ec_key_st)
+ if pub_point == nil then
+ return nil, format_error("ec.get_parameters: EC_KEY_get0_public_key")
+ end
+ bn = C.BN_new()
+ if bn == nil then
+ return nil, "ec.get_parameters: BN_new() failed"
+ end
+ ffi_gc(bn, C.BN_free)
+ local f
+ if version_num >= 0x10101000 then
+ f = C.EC_POINT_get_affine_coordinates
+ else
+ f = C.EC_POINT_get_affine_coordinates_GFp
+ end
+ local code
+ if k == 'x' then
+ code = f(group, pub_point, bn, nil, bn_lib.bn_ctx_tmp)
+ else
+ code = f(group, pub_point, nil, bn, bn_lib.bn_ctx_tmp)
+ end
+ if code ~= 1 then
+ return nil, format_error("ec.get_parameters: EC_POINT_get_affine_coordinates")
+ end
+ else
+ return nil, "ec.get_parameters: unknown parameter \"" .. k .. "\" for EC key"
+ end
+
+ if bn == nil then
+ return nil
+ end
+ return bn_lib.dup(bn)
+ end
+ }), nil
+end
+
+function _M.set_parameters(ec_key_st, opts)
+ for _, k in ipairs(_M.params) do
+ if k ~= "group" then
+ if opts[k] and not bn_lib.istype(opts[k]) then
+ return nil, "expect parameter \"" .. k .. "\" to be a bn instance"
+ end
+ end
+ end
+
+ local group_nid = opts["group"]
+ local group
+ if group_nid then
+ local nid, err = objects_lib.txtnid2nid(group_nid)
+ if err then
+ return nil, "ec.set_parameters: cannot use parameter \"group\":" .. err
+ end
+
+ group = C.EC_GROUP_new_by_curve_name(nid)
+ if group == nil then
+ return nil, "ec.set_parameters: EC_GROUP_new_by_curve_name() failed"
+ end
+ ffi_gc(group, C.EC_GROUP_free)
+ -- # define OPENSSL_EC_NAMED_CURVE 0x001
+ C.EC_GROUP_set_asn1_flag(group, 1)
+ C.EC_GROUP_set_point_conversion_form(group, C.POINT_CONVERSION_UNCOMPRESSED)
+
+ if C.EC_KEY_set_group(ec_key_st, group) ~= 1 then
+ return nil, format_error("ec.set_parameters: EC_KEY_set_group")
+ end
+ end
+
+ local x = opts["x"]
+ local y = opts["y"]
+ local pub = opts["public"]
+ if (x and not y) or (y and not x) then
+ return nil, "ec.set_parameters: \"x\" and \"y\" parameter must be defined at same time or both undefined"
+ end
+
+ if x and y then
+ if pub then
+ return nil, "ec.set_parameters: cannot set \"x\" and \"y\" with \"public\" at same time to set public key"
+ end
+ -- double check if we have set group already
+ if group == nil then
+ group = C.EC_KEY_get0_group(ec_key_st)
+ if group == nil then
+ return nil, "ec.set_parameters: cannot set public key without setting \"group\""
+ end
+ end
+
+ if C.EC_KEY_set_public_key_affine_coordinates(ec_key_st, x.ctx, y.ctx) ~= 1 then
+ return nil, format_error("ec.set_parameters: EC_KEY_set_public_key_affine_coordinates")
+ end
+ end
+
+ if pub then
+ if group == nil then
+ group = C.EC_KEY_get0_group(ec_key_st)
+ if group == nil then
+ return nil, "ec.set_parameters: cannot set public key without setting \"group\""
+ end
+ end
+
+ local point = C.EC_POINT_bn2point(group, pub.ctx, nil, bn_lib.bn_ctx_tmp)
+ if point == nil then
+ return nil, format_error("ec.set_parameters: EC_POINT_bn2point")
+ end
+ ffi_gc(point, C.EC_POINT_free)
+
+ if C.EC_KEY_set_public_key(ec_key_st, point) ~= 1 then
+ return nil, format_error("ec.set_parameters: EC_KEY_set_public_key")
+ end
+ end
+
+ local priv = opts["private"]
+ if priv then
+ -- openssl duplicates it inside
+ if C.EC_KEY_set_private_key(ec_key_st, priv.ctx) ~= 1 then
+ return nil, format_error("ec.set_parameters: EC_KEY_set_private_key")
+ end
+ end
+
+end
+
+return _M
diff --git a/server/resty/openssl/ecx.lua b/server/resty/openssl/ecx.lua
new file mode 100644
index 0000000..5ec7162
--- /dev/null
+++ b/server/resty/openssl/ecx.lua
@@ -0,0 +1,67 @@
+local ffi = require "ffi"
+local C = ffi.C
+local ffi_str = ffi.string
+
+require "resty.openssl.include.ec"
+require "resty.openssl.include.evp"
+local ctypes = require "resty.openssl.auxiliary.ctypes"
+local format_error = require("resty.openssl.err").format_error
+
+local _M = {}
+
+_M.params = {"public", "private"}
+
+local empty_table = {}
+
+local MAX_ECX_KEY_SIZE = 114 -- ed448 uses 114 bytes
+
+function _M.get_parameters(evp_pkey_st)
+ return setmetatable(empty_table, {
+ __index = function(_, k)
+ local buf = ctypes.uchar_array(MAX_ECX_KEY_SIZE)
+ local length = ctypes.ptr_of_size_t(MAX_ECX_KEY_SIZE)
+
+ if k == 'public' or k == "pub_key" then
+ if C.EVP_PKEY_get_raw_public_key(evp_pkey_st, buf, length) ~= 1 then
+ error(format_error("ecx.get_parameters: EVP_PKEY_get_raw_private_key"))
+ end
+ elseif k == 'private' or k == "priv ~=_key" then
+ if C.EVP_PKEY_get_raw_private_key(evp_pkey_st, buf, length) ~= 1 then
+ return nil, format_error("ecx.get_parameters: EVP_PKEY_get_raw_private_key")
+ end
+ else
+ return nil, "ecx.get_parameters: unknown parameter \"" .. k .. "\" for EC key"
+ end
+ return ffi_str(buf, length[0])
+ end
+ }), nil
+end
+
+function _M.set_parameters(key_type, evp_pkey_st, opts)
+ -- for ecx keys we always create a new EVP_PKEY and release the old one
+ -- Note: we allow to pass a nil as evp_pkey_st to create a new EVP_PKEY
+ local key
+ if opts.private then
+ local priv = opts.private
+ key = C.EVP_PKEY_new_raw_private_key(key_type, nil, priv, #priv)
+ if key == nil then
+ return nil, format_error("ecx.set_parameters: EVP_PKEY_new_raw_private_key")
+ end
+ elseif opts.public then
+ local pub = opts.public
+ key = C.EVP_PKEY_new_raw_public_key(key_type, nil, pub, #pub)
+ if key == nil then
+ return nil, format_error("ecx.set_parameters: EVP_PKEY_new_raw_public_key")
+ end
+ else
+ return nil, "no parameter is specified"
+ end
+
+ if evp_pkey_st ~= nil then
+ C.EVP_PKEY_free(evp_pkey_st)
+ end
+ return key
+
+end
+
+return _M
diff --git a/server/resty/openssl/err.lua b/server/resty/openssl/err.lua
new file mode 100644
index 0000000..a047a7c
--- /dev/null
+++ b/server/resty/openssl/err.lua
@@ -0,0 +1,62 @@
+local ffi = require "ffi"
+local C = ffi.C
+local ffi_str = ffi.string
+local ffi_sizeof = ffi.sizeof
+
+local ctypes = require "resty.openssl.auxiliary.ctypes"
+require "resty.openssl.include.err"
+
+local constchar_ptrptr = ffi.typeof("const char*[1]")
+
+local buf = ffi.new('char[256]')
+
+local function format_error(ctx, code, all_errors)
+ local errors = {}
+ if code then
+ table.insert(errors, string.format("code: %d", code or 0))
+ end
+ -- get the OpenSSL errors
+ while C.ERR_peek_error() ~= 0 do
+ local line = ctypes.ptr_of_int()
+ local path = constchar_ptrptr()
+ local code
+ if all_errors then
+ code = C.ERR_get_error_line(path, line)
+ else
+ code = C.ERR_peek_last_error_line(path, line)
+ end
+
+ local abs_path = ffi_str(path[0])
+ -- ../crypto/asn1/a_d2i_fp.c => crypto/asn1/a_d2i_fp.c
+ local start = abs_path:find("/")
+ if start then
+ abs_path = abs_path:sub(start+1)
+ end
+
+ C.ERR_error_string_n(code, buf, ffi_sizeof(buf))
+ table.insert(errors, string.format("%s:%d:%s",
+ abs_path, line[0], ffi_str(buf))
+ )
+
+ if not all_errors then
+ break
+ end
+ end
+
+ C.ERR_clear_error()
+
+ if #errors > 0 then
+ return string.format("%s%s%s", (ctx or ""), (ctx and ": " or ""), table.concat(errors, " "))
+ else
+ return string.format("%s failed", ctx)
+ end
+end
+
+local function format_all_error(ctx, code)
+ return format_error(ctx, code, true)
+end
+
+return {
+ format_error = format_error,
+ format_all_error = format_all_error,
+} \ No newline at end of file
diff --git a/server/resty/openssl/hmac.lua b/server/resty/openssl/hmac.lua
new file mode 100644
index 0000000..fe18d2f
--- /dev/null
+++ b/server/resty/openssl/hmac.lua
@@ -0,0 +1,90 @@
+local ffi = require "ffi"
+local C = ffi.C
+local ffi_gc = ffi.gc
+local ffi_str = ffi.string
+
+require "resty.openssl.include.hmac"
+require "resty.openssl.include.evp.md"
+local ctypes = require "resty.openssl.auxiliary.ctypes"
+local format_error = require("resty.openssl.err").format_error
+local OPENSSL_10 = require("resty.openssl.version").OPENSSL_10
+local OPENSSL_11_OR_LATER = require("resty.openssl.version").OPENSSL_11_OR_LATER
+local OPENSSL_3X = require("resty.openssl.version").OPENSSL_3X
+
+local _M = {}
+local mt = {__index = _M}
+
+local hmac_ctx_ptr_ct = ffi.typeof('HMAC_CTX*')
+
+-- Note: https://www.openssl.org/docs/manmaster/man3/HMAC_Init.html
+-- Replace with EVP_MAC_* functions for OpenSSL 3.0
+
+function _M.new(key, typ)
+ local ctx
+ if OPENSSL_11_OR_LATER then
+ ctx = C.HMAC_CTX_new()
+ ffi_gc(ctx, C.HMAC_CTX_free)
+ elseif OPENSSL_10 then
+ ctx = ffi.new('HMAC_CTX')
+ C.HMAC_CTX_init(ctx)
+ ffi_gc(ctx, C.HMAC_CTX_cleanup)
+ end
+ if ctx == nil then
+ return nil, "hmac.new: failed to create HMAC_CTX"
+ end
+
+ local algo = C.EVP_get_digestbyname(typ or 'sha1')
+ if algo == nil then
+ return nil, string.format("hmac.new: invalid digest type \"%s\"", typ)
+ end
+
+ local code = C.HMAC_Init_ex(ctx, key, #key, algo, nil)
+ if code ~= 1 then
+ return nil, format_error("hmac.new")
+ end
+
+ return setmetatable({
+ ctx = ctx,
+ algo = algo,
+ buf = ctypes.uchar_array(OPENSSL_3X and C.EVP_MD_get_size(algo) or C.EVP_MD_size(algo)),
+ }, mt), nil
+end
+
+function _M.istype(l)
+ return l and l.ctx and ffi.istype(hmac_ctx_ptr_ct, l.ctx)
+end
+
+function _M:update(...)
+ for _, s in ipairs({...}) do
+ if C.HMAC_Update(self.ctx, s, #s) ~= 1 then
+ return false, format_error("hmac:update")
+ end
+ end
+ return true, nil
+end
+
+local result_length = ctypes.ptr_of_uint()
+
+function _M:final(s)
+ if s then
+ if C.HMAC_Update(self.ctx, s, #s) ~= 1 then
+ return false, format_error("hmac:final")
+ end
+ end
+
+ if C.HMAC_Final(self.ctx, self.buf, result_length) ~= 1 then
+ return nil, format_error("hmac:final: HMAC_Final")
+ end
+ return ffi_str(self.buf, result_length[0])
+end
+
+function _M:reset()
+ local code = C.HMAC_Init_ex(self.ctx, nil, 0, nil, nil)
+ if code ~= 1 then
+ return false, format_error("hmac:reset")
+ end
+
+ return true
+end
+
+return _M
diff --git a/server/resty/openssl/include/asn1.lua b/server/resty/openssl/include/asn1.lua
new file mode 100644
index 0000000..ba59ebc
--- /dev/null
+++ b/server/resty/openssl/include/asn1.lua
@@ -0,0 +1,94 @@
+local ffi = require "ffi"
+local C = ffi.C
+
+require "resty.openssl.include.ossl_typ"
+local OPENSSL_3X = require("resty.openssl.version").OPENSSL_3X
+
+ffi.cdef [[
+ typedef struct ASN1_VALUE_st ASN1_VALUE;
+
+ typedef struct asn1_type_st ASN1_TYPE;
+
+ ASN1_IA5STRING *ASN1_IA5STRING_new();
+
+ int ASN1_STRING_type(const ASN1_STRING *x);
+ ASN1_STRING *ASN1_STRING_type_new(int type);
+ int ASN1_STRING_set(ASN1_STRING *str, const void *data, int len);
+
+ ASN1_INTEGER *BN_to_ASN1_INTEGER(const BIGNUM *bn, ASN1_INTEGER *ai);
+ BIGNUM *ASN1_INTEGER_to_BN(const ASN1_INTEGER *ai, BIGNUM *bn);
+
+ typedef int time_t;
+ ASN1_TIME *ASN1_TIME_set(ASN1_TIME *s, time_t t);
+
+ int ASN1_INTEGER_set(ASN1_INTEGER *a, long v);
+ long ASN1_INTEGER_get(const ASN1_INTEGER *a);
+ int ASN1_ENUMERATED_set(ASN1_ENUMERATED *a, long v);
+
+ int ASN1_STRING_print(BIO *bp, const ASN1_STRING *v);
+
+ int ASN1_STRING_length(const ASN1_STRING *x);
+]]
+
+local function declare_asn1_functions(typ, has_ex)
+ local t = {}
+ for i=1, 7 do
+ t[i] = typ
+ end
+
+ ffi.cdef(string.format([[
+ %s *%s_new(void);
+ void %s_free(%s *a);
+ %s *%s_dup(%s *a);
+ ]], unpack(t)))
+
+ if OPENSSL_3X and has_ex then
+ ffi.cdef(string.format([[
+ %s *%s_new_ex(OSSL_LIB_CTX *libctx, const char *propq);
+ ]], typ, typ))
+ end
+end
+
+declare_asn1_functions("ASN1_INTEGER")
+declare_asn1_functions("ASN1_OBJECT")
+declare_asn1_functions("ASN1_STRING")
+declare_asn1_functions("ASN1_ENUMERATED")
+
+local OPENSSL_10 = require("resty.openssl.version").OPENSSL_10
+local OPENSSL_11_OR_LATER = require("resty.openssl.version").OPENSSL_11_OR_LATER
+local BORINGSSL_110 = require("resty.openssl.version").BORINGSSL_110
+
+local ASN1_STRING_get0_data
+if OPENSSL_11_OR_LATER then
+ ffi.cdef[[
+ const unsigned char *ASN1_STRING_get0_data(const ASN1_STRING *x);
+ ]]
+ ASN1_STRING_get0_data = C.ASN1_STRING_get0_data
+elseif OPENSSL_10 then
+ ffi.cdef[[
+ unsigned char *ASN1_STRING_data(ASN1_STRING *x);
+ typedef struct ASN1_ENCODING_st {
+ unsigned char *enc; /* DER encoding */
+ long len; /* Length of encoding */
+ int modified; /* set to 1 if 'enc' is invalid */
+ } ASN1_ENCODING;
+ ]]
+ ASN1_STRING_get0_data = C.ASN1_STRING_data
+end
+
+if BORINGSSL_110 then
+ ffi.cdef [[
+ // required by resty/openssl/include/x509/crl.lua
+ typedef struct ASN1_ENCODING_st {
+ unsigned char *enc; /* DER encoding */
+ long len; /* Length of encoding */
+ int modified; /* set to 1 if 'enc' is invalid */
+ } ASN1_ENCODING;
+ ]]
+end
+
+return {
+ ASN1_STRING_get0_data = ASN1_STRING_get0_data,
+ declare_asn1_functions = declare_asn1_functions,
+ has_new_ex = true,
+}
diff --git a/server/resty/openssl/include/bio.lua b/server/resty/openssl/include/bio.lua
new file mode 100644
index 0000000..45297fc
--- /dev/null
+++ b/server/resty/openssl/include/bio.lua
@@ -0,0 +1,13 @@
+local ffi = require "ffi"
+
+require "resty.openssl.include.ossl_typ"
+
+ffi.cdef [[
+ typedef struct bio_method_st BIO_METHOD;
+ long BIO_ctrl(BIO *bp, int cmd, long larg, void *parg);
+ BIO *BIO_new_mem_buf(const void *buf, int len);
+ BIO *BIO_new(const BIO_METHOD *type);
+ int BIO_free(BIO *a);
+ const BIO_METHOD *BIO_s_mem(void);
+ int BIO_read(BIO *b, void *data, int dlen);
+]]
diff --git a/server/resty/openssl/include/bn.lua b/server/resty/openssl/include/bn.lua
new file mode 100644
index 0000000..93d2dda
--- /dev/null
+++ b/server/resty/openssl/include/bn.lua
@@ -0,0 +1,77 @@
+local ffi = require "ffi"
+
+require "resty.openssl.include.ossl_typ"
+local OPENSSL_3X = require("resty.openssl.version").OPENSSL_3X
+
+local BN_ULONG
+if ffi.abi('64bit') then
+ BN_ULONG = 'unsigned long long'
+else -- 32bit
+ BN_ULONG = 'unsigned int'
+end
+
+ffi.cdef(
+[[
+ BIGNUM *BN_new(void);
+ void BN_free(BIGNUM *a);
+
+ BN_CTX *BN_CTX_new(void);
+ void BN_CTX_init(BN_CTX *c);
+ void BN_CTX_free(BN_CTX *c);
+
+ BIGNUM *BN_dup(const BIGNUM *a);
+ int BN_add_word(BIGNUM *a, ]] .. BN_ULONG ..[[ w);
+ int BN_set_word(BIGNUM *a, ]] .. BN_ULONG ..[[ w);
+ ]] .. BN_ULONG ..[[ BN_get_word(BIGNUM *a);
+ int BN_num_bits(const BIGNUM *a);
+ BIGNUM *BN_bin2bn(const unsigned char *s, int len, BIGNUM *ret);
+ int BN_hex2bn(BIGNUM **a, const char *str);
+ int BN_dec2bn(BIGNUM **a, const char *str);
+ int BN_bn2bin(const BIGNUM *a, unsigned char *to);
+ char *BN_bn2hex(const BIGNUM *a);
+ char *BN_bn2dec(const BIGNUM *a);
+
+ void BN_set_negative(BIGNUM *a, int n);
+ int BN_is_negative(const BIGNUM *a);
+
+ int BN_add(BIGNUM *r, const BIGNUM *a, const BIGNUM *b);
+ int BN_sub(BIGNUM *r, const BIGNUM *a, const BIGNUM *b);
+ int BN_mul(BIGNUM *r, BIGNUM *a, BIGNUM *b, BN_CTX *ctx);
+ int BN_sqr(BIGNUM *r, BIGNUM *a, BN_CTX *ctx);
+ int BN_div(BIGNUM *dv, BIGNUM *rem, const BIGNUM *a, const BIGNUM *d,
+ BN_CTX *ctx);
+ int BN_mod_add(BIGNUM *ret, BIGNUM *a, BIGNUM *b, const BIGNUM *m,
+ BN_CTX *ctx);
+ int BN_mod_sub(BIGNUM *ret, BIGNUM *a, BIGNUM *b, const BIGNUM *m,
+ BN_CTX *ctx);
+ int BN_mod_mul(BIGNUM *ret, BIGNUM *a, BIGNUM *b, const BIGNUM *m,
+ BN_CTX *ctx);
+ int BN_mod_sqr(BIGNUM *ret, BIGNUM *a, const BIGNUM *m, BN_CTX *ctx);
+ int BN_exp(BIGNUM *r, BIGNUM *a, BIGNUM *p, BN_CTX *ctx);
+ int BN_mod_exp(BIGNUM *r, BIGNUM *a, const BIGNUM *p,
+ const BIGNUM *m, BN_CTX *ctx);
+ int BN_gcd(BIGNUM *r, BIGNUM *a, BIGNUM *b, BN_CTX *ctx);
+
+ int BN_lshift(BIGNUM *r, const BIGNUM *a, int n);
+ int BN_rshift(BIGNUM *r, BIGNUM *a, int n);
+
+ int BN_cmp(BIGNUM *a, BIGNUM *b);
+ int BN_ucmp(BIGNUM *a, BIGNUM *b);
+
+ // openssl >= 1.1 only
+ int BN_is_zero(BIGNUM *a);
+ int BN_is_one(BIGNUM *a);
+ int BN_is_word(BIGNUM *a, ]] .. BN_ULONG ..[[ w);
+ int BN_is_odd(BIGNUM *a);
+
+ int BN_is_prime_ex(const BIGNUM *p,int nchecks, BN_CTX *ctx, BN_GENCB *cb);
+ int BN_generate_prime_ex(BIGNUM *ret,int bits,int safe, const BIGNUM *add,
+ const BIGNUM *rem, BN_GENCB *cb);
+]]
+)
+
+if OPENSSL_3X then
+ ffi.cdef [[
+ int BN_check_prime(const BIGNUM *p, BN_CTX *ctx, BN_GENCB *cb);
+ ]]
+end \ No newline at end of file
diff --git a/server/resty/openssl/include/conf.lua b/server/resty/openssl/include/conf.lua
new file mode 100644
index 0000000..d655993
--- /dev/null
+++ b/server/resty/openssl/include/conf.lua
@@ -0,0 +1,9 @@
+local ffi = require "ffi"
+
+require "resty.openssl.include.ossl_typ"
+
+ffi.cdef [[
+ CONF *NCONF_new(CONF_METHOD *meth);
+ void NCONF_free(CONF *conf);
+ int NCONF_load_bio(CONF *conf, BIO *bp, long *eline);
+]] \ No newline at end of file
diff --git a/server/resty/openssl/include/crypto.lua b/server/resty/openssl/include/crypto.lua
new file mode 100644
index 0000000..6ca1f08
--- /dev/null
+++ b/server/resty/openssl/include/crypto.lua
@@ -0,0 +1,31 @@
+local ffi = require "ffi"
+local C = ffi.C
+
+local OPENSSL_10 = require("resty.openssl.version").OPENSSL_10
+local OPENSSL_11_OR_LATER = require("resty.openssl.version").OPENSSL_11_OR_LATER
+
+local OPENSSL_free
+if OPENSSL_10 then
+ ffi.cdef [[
+ void CRYPTO_free(void *ptr);
+ ]]
+ OPENSSL_free = C.CRYPTO_free
+elseif OPENSSL_11_OR_LATER then
+ ffi.cdef [[
+ void CRYPTO_free(void *ptr, const char *file, int line);
+ ]]
+ OPENSSL_free = function(ptr)
+ -- file and line is for debuggin only, since we can't know the c file info
+ -- the macro is expanded, just ignore this
+ C.CRYPTO_free(ptr, "", 0)
+ end
+end
+
+ffi.cdef [[
+ int FIPS_mode(void);
+ int FIPS_mode_set(int ONOFF);
+]]
+
+return {
+ OPENSSL_free = OPENSSL_free,
+}
diff --git a/server/resty/openssl/include/dh.lua b/server/resty/openssl/include/dh.lua
new file mode 100644
index 0000000..504879d
--- /dev/null
+++ b/server/resty/openssl/include/dh.lua
@@ -0,0 +1,80 @@
+local ffi = require "ffi"
+local C = ffi.C
+
+require "resty.openssl.include.ossl_typ"
+require "resty.openssl.include.objects"
+local OPENSSL_10 = require("resty.openssl.version").OPENSSL_10
+local OPENSSL_11_OR_LATER = require("resty.openssl.version").OPENSSL_11_OR_LATER
+
+if OPENSSL_11_OR_LATER then
+ ffi.cdef [[
+ void DH_get0_pqg(const DH *dh,
+ const BIGNUM **p, const BIGNUM **q, const BIGNUM **g);
+ int DH_set0_pqg(DH *dh, BIGNUM *p, BIGNUM *q, BIGNUM *g);
+ void DH_get0_key(const DH *dh,
+ const BIGNUM **pub_key, const BIGNUM **priv_key);
+ int DH_set0_key(DH *dh, BIGNUM *pub_key, BIGNUM *priv_key);
+ ]]
+elseif OPENSSL_10 then
+ ffi.cdef [[
+ struct dh_st {
+ /*
+ * This first argument is used to pick up errors when a DH is passed
+ * instead of a EVP_PKEY
+ */
+ int pad;
+ int version;
+ BIGNUM *p;
+ BIGNUM *g;
+ long length; /* optional */
+ BIGNUM *pub_key; /* g^x */
+ BIGNUM *priv_key; /* x */
+ int flags;
+ /*BN_MONT_CTX*/ void *method_mont_p;
+ /* Place holders if we want to do X9.42 DH */
+ BIGNUM *q;
+ BIGNUM *j;
+ unsigned char *seed;
+ int seedlen;
+ BIGNUM *counter;
+ int references;
+ /* trimmer */
+ // CRYPTO_EX_DATA ex_data;
+ // const DH_METHOD *meth;
+ // ENGINE *engine;
+ };
+ ]]
+end
+
+ffi.cdef [[
+ DH *DH_get_1024_160(void);
+ DH *DH_get_2048_224(void);
+ DH *DH_get_2048_256(void);
+ DH *DH_new_by_nid(int nid);
+]];
+
+
+local dh_groups = {
+ -- per https://tools.ietf.org/html/rfc5114
+ dh_1024_160 = function() return C.DH_get_1024_160() end,
+ dh_2048_224 = function() return C.DH_get_2048_224() end,
+ dh_2048_256 = function() return C.DH_get_2048_256() end,
+}
+
+local groups = {
+ "ffdhe2048", "ffdhe3072", "ffdhe4096", "ffdhe6144", "ffdhe8192",
+ "modp_2048", "modp_3072", "modp_4096", "modp_6144", "modp_8192",
+ -- following cannot be used with FIPS provider
+ "modp_1536", -- and the RFC5114 ones
+}
+
+for _, group in ipairs(groups) do
+ local nid = C.OBJ_sn2nid(group)
+ if nid ~= 0 then
+ dh_groups[group] = function() return C.DH_new_by_nid(nid) end
+ end
+end
+
+return {
+ dh_groups = dh_groups,
+}
diff --git a/server/resty/openssl/include/ec.lua b/server/resty/openssl/include/ec.lua
new file mode 100644
index 0000000..674ef42
--- /dev/null
+++ b/server/resty/openssl/include/ec.lua
@@ -0,0 +1,59 @@
+local ffi = require "ffi"
+
+require "resty.openssl.include.ossl_typ"
+
+ffi.cdef [[
+ /** Enum for the point conversion form as defined in X9.62 (ECDSA)
+ * for the encoding of a elliptic curve point (x,y) */
+ typedef enum {
+ /** the point is encoded as z||x, where the octet z specifies
+ * which solution of the quadratic equation y is */
+ POINT_CONVERSION_COMPRESSED = 2,
+ /** the point is encoded as z||x||y, where z is the octet 0x04 */
+ POINT_CONVERSION_UNCOMPRESSED = 4,
+ /** the point is encoded as z||x||y, where the octet z specifies
+ * which solution of the quadratic equation y is */
+ POINT_CONVERSION_HYBRID = 6
+ } point_conversion_form_t;
+
+ EC_KEY *EC_KEY_new(void);
+ void EC_KEY_free(EC_KEY *key);
+
+ EC_GROUP *EC_GROUP_new_by_curve_name(int nid);
+ void EC_GROUP_set_asn1_flag(EC_GROUP *group, int flag);
+ void EC_GROUP_set_point_conversion_form(EC_GROUP *group,
+ point_conversion_form_t form);
+ void EC_GROUP_set_curve_name(EC_GROUP *group, int nid);
+ int EC_GROUP_get_curve_name(const EC_GROUP *group);
+
+
+ void EC_GROUP_free(EC_GROUP *group);
+
+ BIGNUM *EC_POINT_point2bn(const EC_GROUP *, const EC_POINT *,
+ point_conversion_form_t form, BIGNUM *, BN_CTX *);
+ // for BoringSSL
+ size_t EC_POINT_point2oct(const EC_GROUP *group, const EC_POINT *p,
+ point_conversion_form_t form,
+ unsigned char *buf, size_t len, BN_CTX *ctx);
+ // OpenSSL < 1.1.1
+ int EC_POINT_get_affine_coordinates_GFp(const EC_GROUP *group,
+ const EC_POINT *p,
+ BIGNUM *x, BIGNUM *y, BN_CTX *ctx);
+ // OpenSSL >= 1.1.1
+ int EC_POINT_get_affine_coordinates(const EC_GROUP *group, const EC_POINT *p,
+ BIGNUM *x, BIGNUM *y, BN_CTX *ctx);
+ EC_POINT *EC_POINT_bn2point(const EC_GROUP *group, const BIGNUM *bn,
+ EC_POINT *p, BN_CTX *ctx);
+
+ point_conversion_form_t EC_KEY_get_conv_form(const EC_KEY *key);
+
+ const BIGNUM *EC_KEY_get0_private_key(const EC_KEY *key);
+ int EC_KEY_set_private_key(EC_KEY *key, const BIGNUM *prv);
+
+ const EC_POINT *EC_KEY_get0_public_key(const EC_KEY *key);
+ int EC_KEY_set_public_key(EC_KEY *key, const EC_POINT *pub);
+ int EC_KEY_set_public_key_affine_coordinates(EC_KEY *key, BIGNUM *x, BIGNUM *y);
+
+ const EC_GROUP *EC_KEY_get0_group(const EC_KEY *key);
+ int EC_KEY_set_group(EC_KEY *key, const EC_GROUP *group);
+]]
diff --git a/server/resty/openssl/include/err.lua b/server/resty/openssl/include/err.lua
new file mode 100644
index 0000000..142098c
--- /dev/null
+++ b/server/resty/openssl/include/err.lua
@@ -0,0 +1,9 @@
+local ffi = require "ffi"
+
+ffi.cdef [[
+ unsigned long ERR_peek_error(void);
+ unsigned long ERR_peek_last_error_line(const char **file, int *line);
+ unsigned long ERR_get_error_line(const char **file, int *line);
+ void ERR_clear_error(void);
+ void ERR_error_string_n(unsigned long e, char *buf, size_t len);
+]]
diff --git a/server/resty/openssl/include/evp.lua b/server/resty/openssl/include/evp.lua
new file mode 100644
index 0000000..beeaf91
--- /dev/null
+++ b/server/resty/openssl/include/evp.lua
@@ -0,0 +1,109 @@
+local ffi = require "ffi"
+local C = ffi.C
+local bit = require("bit")
+
+require "resty.openssl.include.ossl_typ"
+require "resty.openssl.include.err"
+require "resty.openssl.include.objects"
+local OPENSSL_3X = require("resty.openssl.version").OPENSSL_3X
+local BORINGSSL = require("resty.openssl.version").BORINGSSL
+
+if BORINGSSL then
+ ffi.cdef [[
+ int PKCS5_PBKDF2_HMAC(const char *password, size_t password_len,
+ const uint8_t *salt, size_t salt_len,
+ unsigned iterations, const EVP_MD *digest,
+ size_t key_len, uint8_t *out_key);
+ int EVP_PBE_scrypt(const char *password, size_t password_len,
+ const uint8_t *salt, size_t salt_len,
+ uint64_t N, uint64_t r, uint64_t p,
+ size_t max_mem, uint8_t *out_key,
+ size_t key_len);
+ ]]
+else
+ ffi.cdef [[
+ /* KDF */
+ int PKCS5_PBKDF2_HMAC(const char *pass, int passlen,
+ const unsigned char *salt, int saltlen, int iter,
+ const EVP_MD *digest, int keylen, unsigned char *out);
+
+ int EVP_PBE_scrypt(const char *pass, size_t passlen,
+ const unsigned char *salt, size_t saltlen,
+ uint64_t N, uint64_t r, uint64_t p, uint64_t maxmem,
+ unsigned char *key, size_t keylen);
+ ]]
+end
+
+if OPENSSL_3X then
+ require "resty.openssl.include.provider"
+
+ ffi.cdef [[
+ int EVP_set_default_properties(OSSL_LIB_CTX *libctx, const char *propq);
+ int EVP_default_properties_enable_fips(OSSL_LIB_CTX *libctx, int enable);
+ int EVP_default_properties_is_fips_enabled(OSSL_LIB_CTX *libctx);
+
+ // const OSSL_PROVIDER *EVP_RAND_get0_provider(const EVP_RAND *rand);
+ // EVP_RAND *EVP_RAND_fetch(OSSL_LIB_CTX *libctx, const char *algorithm,
+ // const char *properties);
+ ]]
+end
+
+local EVP_PKEY_ALG_CTRL = 0x1000
+
+local _M = {
+ EVP_PKEY_RSA = C.OBJ_txt2nid("rsaEncryption"),
+ EVP_PKEY_DH = C.OBJ_txt2nid("dhKeyAgreement"),
+ EVP_PKEY_EC = C.OBJ_txt2nid("id-ecPublicKey"),
+ EVP_PKEY_X25519 = C.OBJ_txt2nid("X25519"),
+ EVP_PKEY_ED25519 = C.OBJ_txt2nid("ED25519"),
+ EVP_PKEY_X448 = C.OBJ_txt2nid("X448"),
+ EVP_PKEY_ED448 = C.OBJ_txt2nid("ED448"),
+
+ EVP_PKEY_OP_PARAMGEN = bit.lshift(1, 1),
+ EVP_PKEY_OP_KEYGEN = bit.lshift(1, 2),
+ EVP_PKEY_OP_SIGN = bit.lshift(1, 3),
+ EVP_PKEY_OP_VERIFY = bit.lshift(1, 4),
+ EVP_PKEY_OP_DERIVE = OPENSSL_3X and bit.lshift(1, 12) or bit.lshift(1, 10),
+
+ EVP_PKEY_ALG_CTRL = EVP_PKEY_ALG_CTRL,
+
+
+ EVP_PKEY_CTRL_DH_PARAMGEN_PRIME_LEN = EVP_PKEY_ALG_CTRL + 1,
+ EVP_PKEY_CTRL_EC_PARAMGEN_CURVE_NID = EVP_PKEY_ALG_CTRL + 1,
+ EVP_PKEY_CTRL_EC_PARAM_ENC = EVP_PKEY_ALG_CTRL + 2,
+ EVP_PKEY_CTRL_RSA_KEYGEN_BITS = EVP_PKEY_ALG_CTRL + 3,
+ EVP_PKEY_CTRL_RSA_KEYGEN_PUBEXP = EVP_PKEY_ALG_CTRL + 4,
+ EVP_PKEY_CTRL_RSA_PADDING = EVP_PKEY_ALG_CTRL + 1,
+ EVP_PKEY_CTRL_RSA_PSS_SALTLEN = EVP_PKEY_ALG_CTRL + 2,
+
+ EVP_CTRL_AEAD_SET_IVLEN = 0x9,
+ EVP_CTRL_AEAD_GET_TAG = 0x10,
+ EVP_CTRL_AEAD_SET_TAG = 0x11,
+
+ EVP_PKEY_CTRL_TLS_MD = EVP_PKEY_ALG_CTRL,
+ EVP_PKEY_CTRL_TLS_SECRET = EVP_PKEY_ALG_CTRL + 1,
+ EVP_PKEY_CTRL_TLS_SEED = EVP_PKEY_ALG_CTRL + 2,
+ EVP_PKEY_CTRL_HKDF_MD = EVP_PKEY_ALG_CTRL + 3,
+ EVP_PKEY_CTRL_HKDF_SALT = EVP_PKEY_ALG_CTRL + 4,
+ EVP_PKEY_CTRL_HKDF_KEY = EVP_PKEY_ALG_CTRL + 5,
+ EVP_PKEY_CTRL_HKDF_INFO = EVP_PKEY_ALG_CTRL + 6,
+ EVP_PKEY_CTRL_HKDF_MODE = EVP_PKEY_ALG_CTRL + 7,
+ EVP_PKEY_CTRL_PASS = EVP_PKEY_ALG_CTRL + 8,
+ EVP_PKEY_CTRL_SCRYPT_SALT = EVP_PKEY_ALG_CTRL + 9,
+ EVP_PKEY_CTRL_SCRYPT_N = EVP_PKEY_ALG_CTRL + 10,
+ EVP_PKEY_CTRL_SCRYPT_R = EVP_PKEY_ALG_CTRL + 11,
+ EVP_PKEY_CTRL_SCRYPT_P = EVP_PKEY_ALG_CTRL + 12,
+ EVP_PKEY_CTRL_SCRYPT_MAXMEM_BYTES = EVP_PKEY_ALG_CTRL + 13,
+}
+
+-- clean up error occurs during OBJ_txt2*
+C.ERR_clear_error()
+
+_M.ecx_curves = {
+ Ed25519 = _M.EVP_PKEY_ED25519,
+ X25519 = _M.EVP_PKEY_X25519,
+ Ed448 = _M.EVP_PKEY_ED448,
+ X448 = _M.EVP_PKEY_X448,
+}
+
+return _M
diff --git a/server/resty/openssl/include/evp/cipher.lua b/server/resty/openssl/include/evp/cipher.lua
new file mode 100644
index 0000000..c803766
--- /dev/null
+++ b/server/resty/openssl/include/evp/cipher.lua
@@ -0,0 +1,123 @@
+local ffi = require "ffi"
+
+require "resty.openssl.include.ossl_typ"
+local OPENSSL_10 = require("resty.openssl.version").OPENSSL_10
+local OPENSSL_11_OR_LATER = require("resty.openssl.version").OPENSSL_11_OR_LATER
+local OPENSSL_3X = require("resty.openssl.version").OPENSSL_3X
+local BORINGSSL = require("resty.openssl.version").BORINGSSL
+
+ffi.cdef [[
+ // openssl < 3.0
+ int EVP_CIPHER_CTX_block_size(const EVP_CIPHER_CTX *ctx);
+ int EVP_CIPHER_CTX_key_length(const EVP_CIPHER_CTX *ctx);
+ int EVP_CIPHER_CTX_iv_length(const EVP_CIPHER_CTX *ctx);
+ int EVP_CIPHER_CTX_set_padding(EVP_CIPHER_CTX *c, int pad);
+
+ const EVP_CIPHER *EVP_CIPHER_CTX_cipher(const EVP_CIPHER_CTX *ctx);
+ const EVP_CIPHER *EVP_get_cipherbyname(const char *name);
+ int EVP_CIPHER_CTX_ctrl(EVP_CIPHER_CTX *ctx, int type, int arg, void *ptr);
+ int EVP_EncryptUpdate(EVP_CIPHER_CTX *ctx, unsigned char *out,
+ int *outl, const unsigned char *in, int inl);
+ int EVP_DecryptUpdate(EVP_CIPHER_CTX *ctx, unsigned char *out,
+ int *outl, const unsigned char *in, int inl);
+
+
+ int EVP_CipherInit_ex(EVP_CIPHER_CTX *ctx,
+ const EVP_CIPHER *cipher, ENGINE *impl,
+ const unsigned char *key,
+ const unsigned char *iv, int enc);
+ int EVP_CipherUpdate(EVP_CIPHER_CTX *ctx, unsigned char *out,
+ int *outl, const unsigned char *in, int inl);
+ int EVP_CipherFinal_ex(EVP_CIPHER_CTX *ctx, unsigned char *outm,
+ int *outl);
+
+ // list functions
+ typedef void* fake_openssl_cipher_list_fn(const EVP_CIPHER *ciph, const char *from,
+ const char *to, void *x);
+ //void EVP_CIPHER_do_all_sorted(fake_openssl_cipher_list_fn*, void *arg);
+ void EVP_CIPHER_do_all_sorted(void (*fn)
+ (const EVP_CIPHER *ciph, const char *from,
+ const char *to, void *x), void *arg);
+ int EVP_CIPHER_nid(const EVP_CIPHER *cipher);
+]]
+
+if BORINGSSL then
+ ffi.cdef [[
+ int EVP_BytesToKey(const EVP_CIPHER *type, const EVP_MD *md,
+ const uint8_t *salt, const uint8_t *data,
+ size_t data_len, unsigned count, uint8_t *key,
+ uint8_t *iv);
+ ]]
+else
+ ffi.cdef [[
+ int EVP_BytesToKey(const EVP_CIPHER *type, const EVP_MD *md,
+ const unsigned char *salt,
+ const unsigned char *data, int datal, int count,
+ unsigned char *key, unsigned char *iv);
+ ]]
+end
+
+if OPENSSL_3X then
+ require "resty.openssl.include.provider"
+
+ ffi.cdef [[
+ int EVP_CIPHER_CTX_get_block_size(const EVP_CIPHER_CTX *ctx);
+ int EVP_CIPHER_CTX_get_key_length(const EVP_CIPHER_CTX *ctx);
+ int EVP_CIPHER_CTX_get_iv_length(const EVP_CIPHER_CTX *ctx);
+
+ int EVP_CIPHER_get_nid(const EVP_CIPHER *cipher);
+
+ const OSSL_PROVIDER *EVP_CIPHER_get0_provider(const EVP_CIPHER *cipher);
+ EVP_CIPHER *EVP_CIPHER_fetch(OSSL_LIB_CTX *ctx, const char *algorithm,
+ const char *properties);
+
+ typedef void* fake_openssl_cipher_provided_list_fn(EVP_CIPHER *cipher, void *arg);
+ void EVP_CIPHER_do_all_provided(OSSL_LIB_CTX *libctx,
+ fake_openssl_cipher_provided_list_fn*,
+ void *arg);
+ int EVP_CIPHER_up_ref(EVP_CIPHER *cipher);
+ void EVP_CIPHER_free(EVP_CIPHER *cipher);
+
+ const char *EVP_CIPHER_get0_name(const EVP_CIPHER *cipher);
+
+ int EVP_CIPHER_CTX_set_params(EVP_CIPHER_CTX *ctx, const OSSL_PARAM params[]);
+ const OSSL_PARAM *EVP_CIPHER_CTX_settable_params(EVP_CIPHER_CTX *ctx);
+ int EVP_CIPHER_CTX_get_params(EVP_CIPHER_CTX *ctx, OSSL_PARAM params[]);
+ const OSSL_PARAM *EVP_CIPHER_CTX_gettable_params(EVP_CIPHER_CTX *ctx);
+ ]]
+end
+
+if OPENSSL_11_OR_LATER then
+ ffi.cdef [[
+ EVP_CIPHER_CTX *EVP_CIPHER_CTX_new(void);
+ int EVP_CIPHER_CTX_reset(EVP_CIPHER_CTX *c);
+ void EVP_CIPHER_CTX_free(EVP_CIPHER_CTX *c);
+ ]]
+elseif OPENSSL_10 then
+ ffi.cdef [[
+ void EVP_CIPHER_CTX_init(EVP_CIPHER_CTX *a);
+ int EVP_CIPHER_CTX_cleanup(EVP_CIPHER_CTX *a);
+
+ // # define EVP_MAX_IV_LENGTH 16
+ // # define EVP_MAX_BLOCK_LENGTH 32
+
+ struct evp_cipher_ctx_st {
+ const EVP_CIPHER *cipher;
+ ENGINE *engine; /* functional reference if 'cipher' is
+ * ENGINE-provided */
+ int encrypt; /* encrypt or decrypt */
+ int buf_len; /* number we have left */
+ unsigned char oiv[16]; /* original iv EVP_MAX_IV_LENGTH */
+ unsigned char iv[16]; /* working iv EVP_MAX_IV_LENGTH */
+ unsigned char buf[32]; /* saved partial block EVP_MAX_BLOCK_LENGTH */
+ int num; /* used by cfb/ofb/ctr mode */
+ void *app_data; /* application stuff */
+ int key_len; /* May change for variable length cipher */
+ unsigned long flags; /* Various flags */
+ void *cipher_data; /* per EVP data */
+ int final_used;
+ int block_mask;
+ unsigned char final[32]; /* possible final block EVP_MAX_BLOCK_LENGTH */
+ } /* EVP_CIPHER_CTX */ ;
+ ]]
+end \ No newline at end of file
diff --git a/server/resty/openssl/include/evp/kdf.lua b/server/resty/openssl/include/evp/kdf.lua
new file mode 100644
index 0000000..1fd408f
--- /dev/null
+++ b/server/resty/openssl/include/evp/kdf.lua
@@ -0,0 +1,148 @@
+local ffi = require "ffi"
+local ffi_cast = ffi.cast
+local C = ffi.C
+
+require "resty.openssl.include.ossl_typ"
+require "resty.openssl.include.evp.md"
+local evp = require("resty.openssl.include.evp")
+local ctypes = require "resty.openssl.auxiliary.ctypes"
+local OPENSSL_3X = require("resty.openssl.version").OPENSSL_3X
+local BORINGSSL = require("resty.openssl.version").BORINGSSL
+
+local void_ptr = ctypes.void_ptr
+
+local _M = {
+ EVP_PKEY_HKDEF_MODE_EXTRACT_AND_EXPAND = 0,
+ EVP_PKEY_HKDEF_MODE_EXTRACT_ONLY = 1,
+ EVP_PKEY_HKDEF_MODE_EXPAND_ONLY = 2,
+}
+
+if OPENSSL_3X then
+ require "resty.openssl.include.provider"
+
+ ffi.cdef [[
+ const OSSL_PROVIDER *EVP_KDF_get0_provider(const EVP_KDF *kdf);
+
+ typedef void* fake_openssl_kdf_provided_list_fn(EVP_KDF *kdf, void *arg);
+ void EVP_KDF_do_all_provided(OSSL_LIB_CTX *libctx,
+ fake_openssl_kdf_provided_list_fn*,
+ void *arg);
+ int EVP_KDF_up_ref(EVP_KDF *kdf);
+ void EVP_KDF_free(EVP_KDF *kdf);
+
+ const char *EVP_KDF_get0_name(const EVP_KDF *kdf);
+
+ EVP_KDF *EVP_KDF_fetch(OSSL_LIB_CTX *libctx, const char *algorithm,
+ const char *properties);
+ EVP_KDF_CTX *EVP_KDF_CTX_new(const EVP_KDF *kdf);
+ void EVP_KDF_CTX_free(EVP_KDF_CTX *ctx);
+ void EVP_KDF_CTX_reset(EVP_KDF_CTX *ctx);
+
+ size_t EVP_KDF_CTX_get_kdf_size(EVP_KDF_CTX *ctx);
+ int EVP_KDF_derive(EVP_KDF_CTX *ctx, unsigned char *key, size_t keylen,
+ const OSSL_PARAM params[]);
+
+ int EVP_KDF_CTX_get_params(EVP_KDF_CTX *ctx, OSSL_PARAM params[]);
+ int EVP_KDF_CTX_set_params(EVP_KDF_CTX *ctx, const OSSL_PARAM params[]);
+ const OSSL_PARAM *EVP_KDF_CTX_gettable_params(const EVP_KDF_CTX *ctx);
+ const OSSL_PARAM *EVP_KDF_CTX_settable_params(const EVP_KDF_CTX *ctx);
+ ]]
+end
+
+if OPENSSL_3X or BORINGSSL then
+ ffi.cdef [[
+ int EVP_PKEY_CTX_set_tls1_prf_md(EVP_PKEY_CTX *ctx, const EVP_MD *md);
+ int EVP_PKEY_CTX_set1_tls1_prf_secret(EVP_PKEY_CTX *pctx,
+ const unsigned char *sec, int seclen);
+ int EVP_PKEY_CTX_add1_tls1_prf_seed(EVP_PKEY_CTX *pctx,
+ const unsigned char *seed, int seedlen);
+
+ int EVP_PKEY_CTX_set_hkdf_md(EVP_PKEY_CTX *ctx, const EVP_MD *md);
+ int EVP_PKEY_CTX_set1_hkdf_salt(EVP_PKEY_CTX *ctx,
+ const unsigned char *salt, int saltlen);
+ int EVP_PKEY_CTX_set1_hkdf_key(EVP_PKEY_CTX *ctx,
+ const unsigned char *key, int keylen);
+ int EVP_PKEY_CTX_set_hkdf_mode(EVP_PKEY_CTX *ctx, int mode);
+ int EVP_PKEY_CTX_add1_hkdf_info(EVP_PKEY_CTX *ctx,
+ const unsigned char *info, int infolen);
+ ]]
+
+ _M.EVP_PKEY_CTX_set_tls1_prf_md = function(pctx, md)
+ return C.EVP_PKEY_CTX_set_tls1_prf_md(pctx, md)
+ end
+ _M.EVP_PKEY_CTX_set1_tls1_prf_secret = function(pctx, sec)
+ return C.EVP_PKEY_CTX_set1_tls1_prf_secret(pctx, sec, #sec)
+ end
+ _M.EVP_PKEY_CTX_add1_tls1_prf_seed = function(pctx, seed)
+ return C.EVP_PKEY_CTX_add1_tls1_prf_seed(pctx, seed, #seed)
+ end
+
+ _M.EVP_PKEY_CTX_set_hkdf_md = function(pctx, md)
+ return C.EVP_PKEY_CTX_set_hkdf_md(pctx, md)
+ end
+ _M.EVP_PKEY_CTX_set1_hkdf_salt = function(pctx, salt)
+ return C.EVP_PKEY_CTX_set1_hkdf_salt(pctx, salt, #salt)
+ end
+ _M.EVP_PKEY_CTX_set1_hkdf_key = function(pctx, key)
+ return C.EVP_PKEY_CTX_set1_hkdf_key(pctx, key, #key)
+ end
+ _M.EVP_PKEY_CTX_set_hkdf_mode = function(pctx, mode)
+ return C.EVP_PKEY_CTX_set_hkdf_mode(pctx, mode)
+ end
+ _M.EVP_PKEY_CTX_add1_hkdf_info = function(pctx, info)
+ return C.EVP_PKEY_CTX_add1_hkdf_info(pctx, info, #info)
+ end
+
+else
+ _M.EVP_PKEY_CTX_set_tls1_prf_md = function(pctx, md)
+ return C.EVP_PKEY_CTX_ctrl(pctx, -1,
+ evp.EVP_PKEY_OP_DERIVE,
+ evp.EVP_PKEY_CTRL_TLS_MD,
+ 0, ffi_cast(void_ptr, md))
+ end
+ _M.EVP_PKEY_CTX_set1_tls1_prf_secret = function(pctx, sec)
+ return C.EVP_PKEY_CTX_ctrl(pctx, -1,
+ evp.EVP_PKEY_OP_DERIVE,
+ evp.EVP_PKEY_CTRL_TLS_SECRET,
+ #sec, ffi_cast(void_ptr, sec))
+ end
+ _M.EVP_PKEY_CTX_add1_tls1_prf_seed = function(pctx, seed)
+ return C.EVP_PKEY_CTX_ctrl(pctx, -1,
+ evp.EVP_PKEY_OP_DERIVE,
+ evp.EVP_PKEY_CTRL_TLS_SEED,
+ #seed, ffi_cast(void_ptr, seed))
+ end
+
+ _M.EVP_PKEY_CTX_set_hkdf_md = function(pctx, md)
+ return C.EVP_PKEY_CTX_ctrl(pctx, -1,
+ evp.EVP_PKEY_OP_DERIVE,
+ evp.EVP_PKEY_CTRL_HKDF_MD,
+ 0, ffi_cast(void_ptr, md))
+ end
+ _M.EVP_PKEY_CTX_set1_hkdf_salt = function(pctx, salt)
+ return C.EVP_PKEY_CTX_ctrl(pctx, -1,
+ evp.EVP_PKEY_OP_DERIVE,
+ evp.EVP_PKEY_CTRL_HKDF_SALT,
+ #salt, ffi_cast(void_ptr, salt))
+ end
+ _M.EVP_PKEY_CTX_set1_hkdf_key = function(pctx, key)
+ return C.EVP_PKEY_CTX_ctrl(pctx, -1,
+ evp.EVP_PKEY_OP_DERIVE,
+ evp.EVP_PKEY_CTRL_HKDF_KEY,
+ #key, ffi_cast(void_ptr, key))
+ end
+ _M.EVP_PKEY_CTX_set_hkdf_mode = function(pctx, mode)
+ return C.EVP_PKEY_CTX_ctrl(pctx, -1,
+ evp.EVP_PKEY_OP_DERIVE,
+ evp.EVP_PKEY_CTRL_HKDF_MODE,
+ mode, nil)
+ end
+ _M.EVP_PKEY_CTX_add1_hkdf_info = function(pctx, info)
+ return C.EVP_PKEY_CTX_ctrl(pctx, -1,
+ evp.EVP_PKEY_OP_DERIVE,
+ evp.EVP_PKEY_CTRL_HKDF_INFO,
+ #info, ffi_cast(void_ptr, info))
+ end
+end
+
+return _M \ No newline at end of file
diff --git a/server/resty/openssl/include/evp/mac.lua b/server/resty/openssl/include/evp/mac.lua
new file mode 100644
index 0000000..a831076
--- /dev/null
+++ b/server/resty/openssl/include/evp/mac.lua
@@ -0,0 +1,38 @@
+local ffi = require "ffi"
+
+require "resty.openssl.include.ossl_typ"
+require "resty.openssl.include.provider"
+
+ffi.cdef [[
+ typedef struct evp_mac_st EVP_MAC;
+ typedef struct evp_mac_ctx_st EVP_MAC_CTX;
+
+ EVP_MAC_CTX *EVP_MAC_CTX_new(EVP_MAC *mac);
+ void EVP_MAC_CTX_free(EVP_MAC_CTX *ctx);
+
+ const OSSL_PROVIDER *EVP_MAC_get0_provider(const EVP_MAC *mac);
+ EVP_MAC *EVP_MAC_fetch(OSSL_LIB_CTX *libctx, const char *algorithm,
+ const char *properties);
+
+ int EVP_MAC_init(EVP_MAC_CTX *ctx, const unsigned char *key, size_t keylen,
+ const OSSL_PARAM params[]);
+ int EVP_MAC_update(EVP_MAC_CTX *ctx, const unsigned char *data, size_t datalen);
+ int EVP_MAC_final(EVP_MAC_CTX *ctx,
+ unsigned char *out, size_t *outl, size_t outsize);
+
+ size_t EVP_MAC_CTX_get_mac_size(EVP_MAC_CTX *ctx);
+
+ typedef void* fake_openssl_mac_provided_list_fn(EVP_MAC *mac, void *arg);
+ void EVP_MAC_do_all_provided(OSSL_LIB_CTX *libctx,
+ fake_openssl_mac_provided_list_fn*,
+ void *arg);
+ int EVP_MAC_up_ref(EVP_MAC *mac);
+ void EVP_MAC_free(EVP_MAC *mac);
+
+ const char *EVP_MAC_get0_name(const EVP_MAC *mac);
+
+ int EVP_MAC_CTX_set_params(EVP_MAC_CTX *ctx, const OSSL_PARAM params[]);
+ const OSSL_PARAM *EVP_MAC_CTX_settable_params(EVP_MAC_CTX *ctx);
+ int EVP_MAC_CTX_get_params(EVP_MAC_CTX *ctx, OSSL_PARAM params[]);
+ const OSSL_PARAM *EVP_MAC_CTX_gettable_params(EVP_MAC_CTX *ctx);
+]] \ No newline at end of file
diff --git a/server/resty/openssl/include/evp/md.lua b/server/resty/openssl/include/evp/md.lua
new file mode 100644
index 0000000..1794ce1
--- /dev/null
+++ b/server/resty/openssl/include/evp/md.lua
@@ -0,0 +1,86 @@
+local ffi = require "ffi"
+
+require "resty.openssl.include.ossl_typ"
+local OPENSSL_10 = require("resty.openssl.version").OPENSSL_10
+local OPENSSL_11_OR_LATER = require("resty.openssl.version").OPENSSL_11_OR_LATER
+local OPENSSL_3X = require("resty.openssl.version").OPENSSL_3X
+
+ffi.cdef [[
+ int EVP_DigestInit_ex(EVP_MD_CTX *ctx, const EVP_MD *type,
+ ENGINE *impl);
+ int EVP_DigestUpdate(EVP_MD_CTX *ctx, const void *d,
+ size_t cnt);
+ int EVP_DigestFinal_ex(EVP_MD_CTX *ctx, unsigned char *md,
+ unsigned int *s);
+ const EVP_MD *EVP_get_digestbyname(const char *name);
+ int EVP_DigestUpdate(EVP_MD_CTX *ctx, const void *d,
+ size_t cnt);
+ int EVP_DigestFinal_ex(EVP_MD_CTX *ctx, unsigned char *md,
+ unsigned int *s);
+
+ const EVP_MD *EVP_md_null(void);
+ // openssl < 3.0
+ int EVP_MD_size(const EVP_MD *md);
+ int EVP_MD_type(const EVP_MD *md);
+
+ typedef void* fake_openssl_md_list_fn(const EVP_MD *ciph, const char *from,
+ const char *to, void *x);
+ void EVP_MD_do_all_sorted(fake_openssl_md_list_fn*, void *arg);
+
+ const EVP_MD *EVP_get_digestbyname(const char *name);
+]]
+
+if OPENSSL_3X then
+ require "resty.openssl.include.provider"
+
+ ffi.cdef [[
+ int EVP_MD_get_size(const EVP_MD *md);
+ int EVP_MD_get_type(const EVP_MD *md);
+ const OSSL_PROVIDER *EVP_MD_get0_provider(const EVP_MD *md);
+
+ EVP_MD *EVP_MD_fetch(OSSL_LIB_CTX *ctx, const char *algorithm,
+ const char *properties);
+
+ typedef void* fake_openssl_md_provided_list_fn(EVP_MD *md, void *arg);
+ void EVP_MD_do_all_provided(OSSL_LIB_CTX *libctx,
+ fake_openssl_md_provided_list_fn*,
+ void *arg);
+ int EVP_MD_up_ref(EVP_MD *md);
+ void EVP_MD_free(EVP_MD *md);
+
+ const char *EVP_MD_get0_name(const EVP_MD *md);
+
+ int EVP_MD_CTX_set_params(EVP_MD_CTX *ctx, const OSSL_PARAM params[]);
+ const OSSL_PARAM *EVP_MD_CTX_settable_params(EVP_MD_CTX *ctx);
+ int EVP_MD_CTX_get_params(EVP_MD_CTX *ctx, OSSL_PARAM params[]);
+ const OSSL_PARAM *EVP_MD_CTX_gettable_params(EVP_MD_CTX *ctx);
+ ]]
+end
+
+if OPENSSL_11_OR_LATER then
+ ffi.cdef [[
+ EVP_MD_CTX *EVP_MD_CTX_new(void);
+ void EVP_MD_CTX_free(EVP_MD_CTX *ctx);
+ ]]
+elseif OPENSSL_10 then
+ ffi.cdef [[
+ EVP_MD_CTX *EVP_MD_CTX_create(void);
+ void EVP_MD_CTX_destroy(EVP_MD_CTX *ctx);
+
+ // crypto/evp/evp.h
+ // only needed for openssl 1.0.x where initializer for HMAC_CTX is not avaiable
+ // HACK: renamed from env_md_ctx_st to evp_md_ctx_st to match typedef (lazily)
+ // it's an internal struct thus name is not exported so we will be fine
+ struct evp_md_ctx_st {
+ const EVP_MD *digest;
+ ENGINE *engine; /* functional reference if 'digest' is
+ * ENGINE-provided */
+ unsigned long flags;
+ void *md_data;
+ /* Public key context for sign/verify */
+ EVP_PKEY_CTX *pctx;
+ /* Update function: usually copied from EVP_MD */
+ int (*update) (EVP_MD_CTX *ctx, const void *data, size_t count);
+ } /* EVP_MD_CTX */ ;
+ ]]
+end \ No newline at end of file
diff --git a/server/resty/openssl/include/evp/pkey.lua b/server/resty/openssl/include/evp/pkey.lua
new file mode 100644
index 0000000..ee1a213
--- /dev/null
+++ b/server/resty/openssl/include/evp/pkey.lua
@@ -0,0 +1,234 @@
+local ffi = require "ffi"
+local C = ffi.C
+
+require "resty.openssl.include.ossl_typ"
+require "resty.openssl.include.evp.md"
+local evp = require("resty.openssl.include.evp")
+local OPENSSL_10 = require("resty.openssl.version").OPENSSL_10
+local OPENSSL_3X = require("resty.openssl.version").OPENSSL_3X
+local BORINGSSL = require("resty.openssl.version").BORINGSSL
+
+ffi.cdef [[
+ EVP_PKEY *EVP_PKEY_new(void);
+ void EVP_PKEY_free(EVP_PKEY *pkey);
+
+ RSA *EVP_PKEY_get0_RSA(EVP_PKEY *pkey);
+ EC_KEY *EVP_PKEY_get0_EC_KEY(EVP_PKEY *pkey);
+ DH *EVP_PKEY_get0_DH(EVP_PKEY *pkey);
+
+ int EVP_PKEY_assign(EVP_PKEY *pkey, int type, void *key);
+ // openssl < 3.0
+ int EVP_PKEY_base_id(const EVP_PKEY *pkey);
+ int EVP_PKEY_size(const EVP_PKEY *pkey);
+
+ EVP_PKEY_CTX *EVP_PKEY_CTX_new(EVP_PKEY *pkey, ENGINE *e);
+ EVP_PKEY_CTX *EVP_PKEY_CTX_new_id(int id, ENGINE *e);
+ void EVP_PKEY_CTX_free(EVP_PKEY_CTX *ctx);
+ int EVP_PKEY_CTX_ctrl(EVP_PKEY_CTX *ctx, int keytype, int optype,
+ int cmd, int p1, void *p2);
+ // TODO replace EVP_PKEY_CTX_ctrl with EVP_PKEY_CTX_ctrl_str to reduce
+ // some hardcoded macros
+ int EVP_PKEY_CTX_ctrl_str(EVP_PKEY_CTX *ctx, const char *type,
+ const char *value);
+ int EVP_PKEY_encrypt_init(EVP_PKEY_CTX *ctx);
+ int EVP_PKEY_encrypt(EVP_PKEY_CTX *ctx,
+ unsigned char *out, size_t *outlen,
+ const unsigned char *in, size_t inlen);
+ int EVP_PKEY_decrypt_init(EVP_PKEY_CTX *ctx);
+ int EVP_PKEY_decrypt(EVP_PKEY_CTX *ctx,
+ unsigned char *out, size_t *outlen,
+ const unsigned char *in, size_t inlen);
+
+ int EVP_PKEY_sign_init(EVP_PKEY_CTX *ctx);
+ int EVP_PKEY_sign(EVP_PKEY_CTX *ctx,
+ unsigned char *sig, size_t *siglen,
+ const unsigned char *tbs, size_t tbslen);
+ int EVP_PKEY_verify_recover_init(EVP_PKEY_CTX *ctx);
+ int EVP_PKEY_verify_recover(EVP_PKEY_CTX *ctx,
+ unsigned char *rout, size_t *routlen,
+ const unsigned char *sig, size_t siglen);
+
+ EVP_PKEY *EVP_PKEY_new_raw_private_key(int type, ENGINE *e,
+ const unsigned char *key, size_t keylen);
+ EVP_PKEY *EVP_PKEY_new_raw_public_key(int type, ENGINE *e,
+ const unsigned char *key, size_t keylen);
+
+ int EVP_PKEY_get_raw_private_key(const EVP_PKEY *pkey, unsigned char *priv,
+ size_t *len);
+ int EVP_PKEY_get_raw_public_key(const EVP_PKEY *pkey, unsigned char *pub,
+ size_t *len);
+
+ int EVP_SignFinal(EVP_MD_CTX *ctx, unsigned char *md, unsigned int *s,
+ EVP_PKEY *pkey);
+ int EVP_VerifyFinal(EVP_MD_CTX *ctx, const unsigned char *sigbuf,
+ unsigned int siglen, EVP_PKEY *pkey);
+
+ int EVP_DigestSignInit(EVP_MD_CTX *ctx, EVP_PKEY_CTX **pctx,
+ const EVP_MD *type, ENGINE *e, EVP_PKEY *pkey);
+ int EVP_DigestSign(EVP_MD_CTX *ctx, unsigned char *sigret,
+ size_t *siglen, const unsigned char *tbs,
+ size_t tbslen);
+ int EVP_DigestVerifyInit(EVP_MD_CTX *ctx, EVP_PKEY_CTX **pctx,
+ const EVP_MD *type, ENGINE *e, EVP_PKEY *pkey);
+ int EVP_DigestVerify(EVP_MD_CTX *ctx, const unsigned char *sigret,
+ size_t siglen, const unsigned char *tbs, size_t tbslen);
+
+ int EVP_PKEY_get_default_digest_nid(EVP_PKEY *pkey, int *pnid);
+
+ int EVP_PKEY_derive_init(EVP_PKEY_CTX *ctx);
+ int EVP_PKEY_derive_set_peer(EVP_PKEY_CTX *ctx, EVP_PKEY *peer);
+ int EVP_PKEY_derive(EVP_PKEY_CTX *ctx, unsigned char *key, size_t *keylen);
+
+ int EVP_PKEY_keygen_init(EVP_PKEY_CTX *ctx);
+ int EVP_PKEY_keygen(EVP_PKEY_CTX *ctx, EVP_PKEY **ppkey);
+ int EVP_PKEY_paramgen_init(EVP_PKEY_CTX *ctx);
+ int EVP_PKEY_paramgen(EVP_PKEY_CTX *ctx, EVP_PKEY **ppkey);
+]]
+
+if OPENSSL_3X then
+ require "resty.openssl.include.provider"
+
+ ffi.cdef [[
+ int EVP_PKEY_CTX_set_rsa_padding(EVP_PKEY_CTX *ctx, int pad_mode);
+
+ int EVP_PKEY_get_base_id(const EVP_PKEY *pkey);
+ int EVP_PKEY_get_size(const EVP_PKEY *pkey);
+
+ const OSSL_PROVIDER *EVP_PKEY_get0_provider(const EVP_PKEY *key);
+ const OSSL_PROVIDER *EVP_PKEY_CTX_get0_provider(const EVP_PKEY_CTX *ctx);
+
+ const OSSL_PARAM *EVP_PKEY_settable_params(const EVP_PKEY *pkey);
+ int EVP_PKEY_set_params(EVP_PKEY *pkey, OSSL_PARAM params[]);
+ int EVP_PKEY_get_params(EVP_PKEY *ctx, OSSL_PARAM params[]);
+ const OSSL_PARAM *EVP_PKEY_gettable_params(EVP_PKEY *ctx);
+ ]]
+end
+
+if OPENSSL_10 then
+ ffi.cdef [[
+ // crypto/evp/evp.h
+ // only needed for openssl 1.0.x where getters are not available
+ // needed to get key to extract parameters
+ // Note: this struct is trimmed
+ struct evp_pkey_st {
+ int type;
+ int save_type;
+ const EVP_PKEY_ASN1_METHOD *ameth;
+ ENGINE *engine;
+ ENGINE *pmeth_engine;
+ union {
+ void *ptr;
+ struct rsa_st *rsa;
+ struct dsa_st *dsa;
+ struct dh_st *dh;
+ struct ec_key_st *ec;
+ } pkey;
+ // trimmed
+
+ // CRYPTO_REF_COUNT references;
+ // CRYPTO_RWLOCK *lock;
+ // STACK_OF(X509_ATTRIBUTE) *attributes;
+ // int save_parameters;
+
+ // struct {
+ // EVP_KEYMGMT *keymgmt;
+ // void *provkey;
+ // } pkeys[10];
+ // size_t dirty_cnt_copy;
+ };
+ ]]
+end
+
+local _M = {}
+
+if OPENSSL_3X or BORINGSSL then
+ ffi.cdef [[
+ int EVP_PKEY_CTX_set_ec_paramgen_curve_nid(EVP_PKEY_CTX *ctx, int nid);
+ int EVP_PKEY_CTX_set_ec_param_enc(EVP_PKEY_CTX *ctx, int param_enc);
+
+ int EVP_PKEY_CTX_set_rsa_keygen_bits(EVP_PKEY_CTX *ctx, int mbits);
+ int EVP_PKEY_CTX_set_rsa_keygen_pubexp(EVP_PKEY_CTX *ctx, BIGNUM *pubexp);
+
+ int EVP_PKEY_CTX_set_rsa_padding(EVP_PKEY_CTX *ctx, int pad);
+ int EVP_PKEY_CTX_set_rsa_pss_saltlen(EVP_PKEY_CTX *ctx, int len);
+
+ int EVP_PKEY_CTX_set_dh_paramgen_prime_len(EVP_PKEY_CTX *ctx, int pbits);
+ ]]
+ _M.EVP_PKEY_CTX_set_ec_paramgen_curve_nid = function(pctx, nid)
+ return C.EVP_PKEY_CTX_set_ec_paramgen_curve_nid(pctx, nid)
+ end
+ _M.EVP_PKEY_CTX_set_ec_param_enc = function(pctx, param_enc)
+ return C.EVP_PKEY_CTX_set_ec_param_enc(pctx, param_enc)
+ end
+
+ _M.EVP_PKEY_CTX_set_rsa_keygen_bits = function(pctx, mbits)
+ return C.EVP_PKEY_CTX_set_rsa_keygen_bits(pctx, mbits)
+ end
+ _M.EVP_PKEY_CTX_set_rsa_keygen_pubexp = function(pctx, pubexp)
+ return C.EVP_PKEY_CTX_set_rsa_keygen_pubexp(pctx, pubexp)
+ end
+
+ _M.EVP_PKEY_CTX_set_rsa_padding = function(pctx, pad)
+ return C.EVP_PKEY_CTX_set_rsa_padding(pctx, pad)
+ end
+ _M.EVP_PKEY_CTX_set_rsa_pss_saltlen = function(pctx, len)
+ return C.EVP_PKEY_CTX_set_rsa_pss_saltlen(pctx, len)
+ end
+ _M.EVP_PKEY_CTX_set_dh_paramgen_prime_len = function(pctx, pbits)
+ return C.EVP_PKEY_CTX_set_dh_paramgen_prime_len(pctx, pbits)
+ end
+
+else
+ _M.EVP_PKEY_CTX_set_ec_paramgen_curve_nid = function(pctx, nid)
+ return C.EVP_PKEY_CTX_ctrl(pctx,
+ evp.EVP_PKEY_EC,
+ evp.EVP_PKEY_OP_PARAMGEN + evp.EVP_PKEY_OP_KEYGEN,
+ evp.EVP_PKEY_CTRL_EC_PARAMGEN_CURVE_NID,
+ nid, nil)
+ end
+ _M.EVP_PKEY_CTX_set_ec_param_enc = function(pctx, param_enc)
+ return C.EVP_PKEY_CTX_ctrl(pctx,
+ evp.EVP_PKEY_EC,
+ evp.EVP_PKEY_OP_PARAMGEN + evp.EVP_PKEY_OP_KEYGEN,
+ evp.EVP_PKEY_CTRL_EC_PARAM_ENC,
+ param_enc, nil)
+ end
+
+ _M.EVP_PKEY_CTX_set_rsa_keygen_bits = function(pctx, mbits)
+ return C.EVP_PKEY_CTX_ctrl(pctx,
+ evp.EVP_PKEY_RSA,
+ evp.EVP_PKEY_OP_KEYGEN,
+ evp.EVP_PKEY_CTRL_RSA_KEYGEN_BITS,
+ mbits, nil)
+ end
+ _M.EVP_PKEY_CTX_set_rsa_keygen_pubexp = function(pctx, pubexp)
+ return C.EVP_PKEY_CTX_ctrl(pctx,
+ evp.EVP_PKEY_RSA, evp.EVP_PKEY_OP_KEYGEN,
+ evp.EVP_PKEY_CTRL_RSA_KEYGEN_PUBEXP,
+ 0, pubexp)
+ end
+
+ _M.EVP_PKEY_CTX_set_rsa_padding = function(pctx, pad)
+ return C.EVP_PKEY_CTX_ctrl(pctx,
+ evp.EVP_PKEY_RSA,
+ -1,
+ evp.EVP_PKEY_CTRL_RSA_PADDING,
+ pad, nil)
+ end
+ _M.EVP_PKEY_CTX_set_rsa_pss_saltlen = function(pctx, len)
+ return C.EVP_PKEY_CTX_ctrl(pctx,
+ evp.EVP_PKEY_RSA,
+ evp.EVP_PKEY_OP_SIGN + evp.EVP_PKEY_OP_VERIFY,
+ evp.EVP_PKEY_CTRL_RSA_PSS_SALTLEN,
+ len, nil)
+ end
+
+ _M.EVP_PKEY_CTX_set_dh_paramgen_prime_len = function(pctx, pbits)
+ return C.EVP_PKEY_CTX_ctrl(pctx,
+ evp.EVP_PKEY_DH, evp.EVP_PKEY_OP_PARAMGEN,
+ evp.EVP_PKEY_CTRL_DH_PARAMGEN_PRIME_LEN,
+ pbits, nil)
+ end
+end
+
+return _M \ No newline at end of file
diff --git a/server/resty/openssl/include/hmac.lua b/server/resty/openssl/include/hmac.lua
new file mode 100644
index 0000000..e08f031
--- /dev/null
+++ b/server/resty/openssl/include/hmac.lua
@@ -0,0 +1,48 @@
+local ffi = require "ffi"
+
+require "resty.openssl.include.ossl_typ"
+require "resty.openssl.include.evp"
+local OPENSSL_10 = require("resty.openssl.version").OPENSSL_10
+local OPENSSL_11_OR_LATER = require("resty.openssl.version").OPENSSL_11_OR_LATER
+local BORINGSSL = require("resty.openssl.version").BORINGSSL
+
+if BORINGSSL then
+ ffi.cdef [[
+ int HMAC_Init_ex(HMAC_CTX *ctx, const void *key, size_t key_len,
+ const EVP_MD *md, ENGINE *impl);
+ ]]
+else
+ ffi.cdef [[
+ int HMAC_Init_ex(HMAC_CTX *ctx, const void *key, int len,
+ const EVP_MD *md, ENGINE *impl);
+ ]]
+end
+
+ffi.cdef [[
+ int HMAC_Update(HMAC_CTX *ctx, const unsigned char *data,
+ size_t len);
+ int HMAC_Final(HMAC_CTX *ctx, unsigned char *md,
+ unsigned int *len);
+]]
+
+if OPENSSL_11_OR_LATER then
+ ffi.cdef [[
+ HMAC_CTX *HMAC_CTX_new(void);
+ void HMAC_CTX_free(HMAC_CTX *ctx);
+ ]]
+elseif OPENSSL_10 then
+ ffi.cdef [[
+ // # define HMAC_MAX_MD_CBLOCK 128/* largest known is SHA512 */
+ struct hmac_ctx_st {
+ const EVP_MD *md;
+ EVP_MD_CTX md_ctx;
+ EVP_MD_CTX i_ctx;
+ EVP_MD_CTX o_ctx;
+ unsigned int key_length;
+ unsigned char key[128];
+ };
+
+ void HMAC_CTX_init(HMAC_CTX *ctx);
+ void HMAC_CTX_cleanup(HMAC_CTX *ctx);
+ ]]
+end \ No newline at end of file
diff --git a/server/resty/openssl/include/objects.lua b/server/resty/openssl/include/objects.lua
new file mode 100644
index 0000000..aecd324
--- /dev/null
+++ b/server/resty/openssl/include/objects.lua
@@ -0,0 +1,19 @@
+local ffi = require "ffi"
+
+require "resty.openssl.include.ossl_typ"
+
+ffi.cdef [[
+ int OBJ_obj2txt(char *buf, int buf_len, const ASN1_OBJECT *a, int no_name);
+ ASN1_OBJECT *OBJ_txt2obj(const char *s, int no_name);
+ int OBJ_txt2nid(const char *s);
+ const char *OBJ_nid2sn(int n);
+ int OBJ_ln2nid(const char *s);
+ int OBJ_sn2nid(const char *s);
+ const char *OBJ_nid2ln(int n);
+ const char *OBJ_nid2sn(int n);
+ int OBJ_obj2nid(const ASN1_OBJECT *o);
+ const ASN1_OBJECT *OBJ_nid2obj(int n);
+ int OBJ_create(const char *oid, const char *sn, const char *ln);
+
+ int OBJ_find_sigid_algs(int signid, int *pdig_nid, int *ppkey_nid);
+]]
diff --git a/server/resty/openssl/include/ossl_typ.lua b/server/resty/openssl/include/ossl_typ.lua
new file mode 100644
index 0000000..198c889
--- /dev/null
+++ b/server/resty/openssl/include/ossl_typ.lua
@@ -0,0 +1,71 @@
+local ffi = require "ffi"
+
+ffi.cdef(
+[[
+ typedef struct rsa_st RSA;
+ typedef struct evp_pkey_st EVP_PKEY;
+ typedef struct bignum_st BIGNUM;
+ typedef struct bn_gencb_st BN_GENCB;
+ typedef struct bignum_ctx BN_CTX;
+ typedef struct bio_st BIO;
+ typedef struct evp_cipher_st EVP_CIPHER;
+ typedef struct evp_md_ctx_st EVP_MD_CTX;
+ typedef struct evp_pkey_ctx_st EVP_PKEY_CTX;
+ typedef struct evp_md_st EVP_MD;
+ typedef struct evp_pkey_asn1_method_st EVP_PKEY_ASN1_METHOD;
+ typedef struct evp_cipher_ctx_st EVP_CIPHER_CTX;
+ typedef struct engine_st ENGINE;
+ typedef struct x509_st X509;
+ typedef struct x509_attributes_st X509_ATTRIBUTE;
+ typedef struct X509_extension_st X509_EXTENSION;
+ typedef struct X509_name_st X509_NAME;
+ typedef struct X509_name_entry_st X509_NAME_ENTRY;
+ typedef struct X509_req_st X509_REQ;
+ typedef struct X509_crl_st X509_CRL;
+ typedef struct x509_store_st X509_STORE;
+ typedef struct x509_store_ctx_st X509_STORE_CTX;
+ typedef struct x509_purpose_st X509_PURPOSE;
+ typedef struct v3_ext_ctx X509V3_CTX;
+ typedef struct asn1_string_st ASN1_INTEGER;
+ typedef struct asn1_string_st ASN1_ENUMERATED;
+ typedef struct asn1_string_st ASN1_BIT_STRING;
+ typedef struct asn1_string_st ASN1_OCTET_STRING;
+ typedef struct asn1_string_st ASN1_PRINTABLESTRING;
+ typedef struct asn1_string_st ASN1_T61STRING;
+ typedef struct asn1_string_st ASN1_IA5STRING;
+ typedef struct asn1_string_st ASN1_GENERALSTRING;
+ typedef struct asn1_string_st ASN1_UNIVERSALSTRING;
+ typedef struct asn1_string_st ASN1_BMPSTRING;
+ typedef struct asn1_string_st ASN1_UTCTIME;
+ typedef struct asn1_string_st ASN1_TIME;
+ typedef struct asn1_string_st ASN1_GENERALIZEDTIME;
+ typedef struct asn1_string_st ASN1_VISIBLESTRING;
+ typedef struct asn1_string_st ASN1_UTF8STRING;
+ typedef struct asn1_string_st ASN1_STRING;
+ typedef struct asn1_object_st ASN1_OBJECT;
+ typedef struct conf_st CONF;
+ typedef struct conf_method_st CONF_METHOD;
+ typedef int ASN1_BOOLEAN;
+ typedef int ASN1_NULL;
+ typedef struct ec_key_st EC_KEY;
+ typedef struct ec_method_st EC_METHOD;
+ typedef struct ec_point_st EC_POINT;
+ typedef struct ec_group_st EC_GROUP;
+ typedef struct rsa_meth_st RSA_METHOD;
+ // typedef struct evp_keymgmt_st EVP_KEYMGMT;
+ // typedef struct crypto_ex_data_st CRYPTO_EX_DATA;
+ // typedef struct bn_mont_ctx_st BN_MONT_CTX;
+ // typedef struct bn_blinding_st BN_BLINDING;
+ // crypto.h
+ // typedef void CRYPTO_RWLOCK;
+ typedef struct hmac_ctx_st HMAC_CTX;
+ typedef struct x509_revoked_st X509_REVOKED;
+ typedef struct dh_st DH;
+ typedef struct PKCS12_st PKCS12;
+ typedef struct ssl_st SSL;
+ typedef struct ssl_ctx_st SSL_CTX;
+ typedef struct evp_kdf_st EVP_KDF;
+ typedef struct evp_kdf_ctx_st EVP_KDF_CTX;
+ typedef struct ossl_lib_ctx_st OSSL_LIB_CTX;
+]])
+
diff --git a/server/resty/openssl/include/param.lua b/server/resty/openssl/include/param.lua
new file mode 100644
index 0000000..9c7a2e9
--- /dev/null
+++ b/server/resty/openssl/include/param.lua
@@ -0,0 +1,71 @@
+local ffi = require "ffi"
+
+require "resty.openssl.include.ossl_typ"
+
+ffi.cdef [[
+ typedef struct ossl_param_st {
+ const char *key; /* the name of the parameter */
+ unsigned int data_type; /* declare what kind of content is in buffer */
+ void *data; /* value being passed in or out */
+ size_t data_size; /* data size */
+ size_t return_size; /* returned content size */
+ } OSSL_PARAM;
+
+ OSSL_PARAM OSSL_PARAM_construct_int(const char *key, int *buf);
+ OSSL_PARAM OSSL_PARAM_construct_uint(const char *key, unsigned int *buf);
+ OSSL_PARAM OSSL_PARAM_construct_BN(const char *key, unsigned char *buf,
+ size_t bsize);
+ OSSL_PARAM OSSL_PARAM_construct_double(const char *key, double *buf);
+ OSSL_PARAM OSSL_PARAM_construct_utf8_string(const char *key, char *buf,
+ size_t bsize);
+ OSSL_PARAM OSSL_PARAM_construct_octet_string(const char *key, void *buf,
+ size_t bsize);
+ OSSL_PARAM OSSL_PARAM_construct_utf8_ptr(const char *key, char **buf,
+ size_t bsize);
+ OSSL_PARAM OSSL_PARAM_construct_octet_ptr(const char *key, void **buf,
+ size_t bsize);
+ OSSL_PARAM OSSL_PARAM_construct_end(void);
+
+ int OSSL_PARAM_get_int32(const OSSL_PARAM *p, int32_t *val);
+ int OSSL_PARAM_get_uint32(const OSSL_PARAM *p, uint32_t *val);
+ int OSSL_PARAM_get_int64(const OSSL_PARAM *p, int64_t *val);
+ int OSSL_PARAM_get_uint64(const OSSL_PARAM *p, uint64_t *val);
+ // int OSSL_PARAM_get_size_t(const OSSL_PARAM *p, size_t *val);
+ // int OSSL_PARAM_get_time_t(const OSSL_PARAM *p, time_t *val);
+
+ int OSSL_PARAM_set_int(OSSL_PARAM *p, int val);
+ int OSSL_PARAM_set_uint(OSSL_PARAM *p, unsigned int val);
+ int OSSL_PARAM_set_long(OSSL_PARAM *p, long int val);
+ int OSSL_PARAM_set_ulong(OSSL_PARAM *p, unsigned long int val);
+ int OSSL_PARAM_set_int32(OSSL_PARAM *p, int32_t val);
+ int OSSL_PARAM_set_uint32(OSSL_PARAM *p, uint32_t val);
+ int OSSL_PARAM_set_int64(OSSL_PARAM *p, int64_t val);
+ int OSSL_PARAM_set_uint64(OSSL_PARAM *p, uint64_t val);
+ // int OSSL_PARAM_set_size_t(OSSL_PARAM *p, size_t val);
+ // int OSSL_PARAM_set_time_t(OSSL_PARAM *p, time_t val);
+
+ int OSSL_PARAM_get_double(const OSSL_PARAM *p, double *val);
+ int OSSL_PARAM_set_double(OSSL_PARAM *p, double val);
+
+ int OSSL_PARAM_get_BN(const OSSL_PARAM *p, BIGNUM **val);
+ int OSSL_PARAM_set_BN(OSSL_PARAM *p, const BIGNUM *val);
+
+ int OSSL_PARAM_get_utf8_string(const OSSL_PARAM *p, char **val, size_t max_len);
+ int OSSL_PARAM_set_utf8_string(OSSL_PARAM *p, const char *val);
+
+ int OSSL_PARAM_get_octet_string(const OSSL_PARAM *p, void **val, size_t max_len,
+ size_t *used_len);
+ int OSSL_PARAM_set_octet_string(OSSL_PARAM *p, const void *val, size_t len);
+
+ int OSSL_PARAM_get_utf8_ptr(const OSSL_PARAM *p, const char **val);
+ int OSSL_PARAM_set_utf8_ptr(OSSL_PARAM *p, const char *val);
+
+ int OSSL_PARAM_get_octet_ptr(const OSSL_PARAM *p, const void **val,
+ size_t *used_len);
+ int OSSL_PARAM_set_octet_ptr(OSSL_PARAM *p, const void *val,
+ size_t used_len);
+
+ int OSSL_PARAM_get_utf8_string_ptr(const OSSL_PARAM *p, const char **val);
+ int OSSL_PARAM_get_octet_string_ptr(const OSSL_PARAM *p, const void **val,
+ size_t *used_len);
+]]
diff --git a/server/resty/openssl/include/pem.lua b/server/resty/openssl/include/pem.lua
new file mode 100644
index 0000000..50185e5
--- /dev/null
+++ b/server/resty/openssl/include/pem.lua
@@ -0,0 +1,50 @@
+
+local ffi = require "ffi"
+
+require "resty.openssl.include.ossl_typ"
+
+ffi.cdef [[
+ // all pem_password_cb* has been modified to pem_password_cb to avoid a table overflow issue
+ typedef int (*pem_password_cb)(char *buf, int size, int rwflag, void *userdata);
+ EVP_PKEY *PEM_read_bio_PrivateKey(BIO *bp, EVP_PKEY **x,
+ // the following signature has been modified to avoid ffi.cast
+ pem_password_cb cb, const char *u);
+ // pem_password_cb *cb, void *u);
+ EVP_PKEY *PEM_read_bio_PUBKEY(BIO *bp, EVP_PKEY **x,
+ // the following signature has been modified to avoid ffi.cast
+ pem_password_cb cb, const char *u);
+ // pem_password_cb *cb, void *u);
+ int PEM_write_bio_PrivateKey(BIO *bp, EVP_PKEY *x, const EVP_CIPHER *enc,
+ unsigned char *kstr, int klen,
+ pem_password_cb *cb, void *u);
+ int PEM_write_bio_PUBKEY(BIO *bp, EVP_PKEY *x);
+
+ RSA *PEM_read_bio_RSAPrivateKey(BIO *bp, RSA **x,
+ // the following signature has been modified to avoid ffi.cast
+ pem_password_cb cb, const char *u);
+ // pem_password_cb *cb, void *u);
+ RSA *PEM_read_bio_RSAPublicKey(BIO *bp, RSA **x,
+ // the following signature has been modified to avoid ffi.cast
+ pem_password_cb cb, const char *u);
+ // pem_password_cb *cb, void *u);
+ int PEM_write_bio_RSAPrivateKey(BIO *bp, RSA *x, const EVP_CIPHER *enc,
+ unsigned char *kstr, int klen,
+ pem_password_cb *cb, void *u);
+ int PEM_write_bio_RSAPublicKey(BIO *bp, RSA *x);
+
+ X509_REQ *PEM_read_bio_X509_REQ(BIO *bp, X509_REQ **x, pem_password_cb cb, void *u);
+ int PEM_write_bio_X509_REQ(BIO *bp, X509_REQ *x);
+
+ X509_CRL *PEM_read_bio_X509_CRL(BIO *bp, X509_CRL **x, pem_password_cb cb, void *u);
+ int PEM_write_bio_X509_CRL(BIO *bp, X509_CRL *x);
+
+ X509 *PEM_read_bio_X509(BIO *bp, X509 **x, pem_password_cb cb, void *u);
+ int PEM_write_bio_X509(BIO *bp, X509 *x);
+
+ DH *PEM_read_bio_DHparams(BIO *bp, DH **x, pem_password_cb cb, void *u);
+ int PEM_write_bio_DHparams(BIO *bp, DH *x);
+
+ EC_GROUP *PEM_read_bio_ECPKParameters(BIO *bp, EC_GROUP **x, pem_password_cb cb, void *u);
+ int PEM_write_bio_ECPKParameters(BIO *bp, const EC_GROUP *x);
+
+]]
diff --git a/server/resty/openssl/include/pkcs12.lua b/server/resty/openssl/include/pkcs12.lua
new file mode 100644
index 0000000..fb74025
--- /dev/null
+++ b/server/resty/openssl/include/pkcs12.lua
@@ -0,0 +1,31 @@
+local ffi = require "ffi"
+
+require "resty.openssl.include.ossl_typ"
+require "resty.openssl.include.stack"
+
+local OPENSSL_3X = require("resty.openssl.version").OPENSSL_3X
+
+ffi.cdef [[
+ // hack by changing char* to const char* here
+ PKCS12 *PKCS12_create(const char *pass, const char *name, EVP_PKEY *pkey, X509 *cert,
+ OPENSSL_STACK *ca, // STACK_OF(X509)
+ int nid_key, int nid_cert, int iter, int mac_iter, int keytype);
+
+ int PKCS12_parse(PKCS12 *p12, const char *pass, EVP_PKEY **pkey, X509 **cert,
+ OPENSSL_STACK **ca); // STACK_OF(X509) **ca);
+
+ void PKCS12_free(PKCS12 *p12);
+ int i2d_PKCS12_bio(BIO *bp, PKCS12 *a);
+ PKCS12 *d2i_PKCS12_bio(BIO *bp, PKCS12 **a);
+]]
+
+if OPENSSL_3X then
+ ffi.cdef [[
+ PKCS12 *PKCS12_create_ex(const char *pass, const char *name, EVP_PKEY *pkey,
+ X509 *cert,
+ OPENSSL_STACK *ca, // STACK_OF(X509)
+ int nid_key, int nid_cert,
+ int iter, int mac_iter, int keytype,
+ OSSL_LIB_CTX *ctx, const char *propq);
+ ]]
+end
diff --git a/server/resty/openssl/include/provider.lua b/server/resty/openssl/include/provider.lua
new file mode 100644
index 0000000..a2bb472
--- /dev/null
+++ b/server/resty/openssl/include/provider.lua
@@ -0,0 +1,27 @@
+local ffi = require "ffi"
+
+require "resty.openssl.include.ossl_typ"
+require "resty.openssl.include.param"
+
+ffi.cdef [[
+ typedef struct ossl_provider_st OSSL_PROVIDER;
+ typedef struct ossl_lib_ctx_st OSSL_LIB_CTX;
+
+ void OSSL_PROVIDER_set_default_search_path(OSSL_LIB_CTX *libctx,
+ const char *path);
+
+
+ OSSL_PROVIDER *OSSL_PROVIDER_load(OSSL_LIB_CTX *libctx, const char *name);
+ OSSL_PROVIDER *OSSL_PROVIDER_try_load(OSSL_LIB_CTX *libctx, const char *name);
+ int OSSL_PROVIDER_unload(OSSL_PROVIDER *prov);
+ int OSSL_PROVIDER_available(OSSL_LIB_CTX *libctx, const char *name);
+
+ const OSSL_PARAM *OSSL_PROVIDER_gettable_params(OSSL_PROVIDER *prov);
+ int OSSL_PROVIDER_get_params(OSSL_PROVIDER *prov, OSSL_PARAM params[]);
+
+ // int OSSL_PROVIDER_add_builtin(OSSL_LIB_CTX *libctx, const char *name,
+ // ossl_provider_init_fn *init_fn);
+
+ const char *OSSL_PROVIDER_get0_name(const OSSL_PROVIDER *prov);
+ int OSSL_PROVIDER_self_test(const OSSL_PROVIDER *prov);
+]]
diff --git a/server/resty/openssl/include/rand.lua b/server/resty/openssl/include/rand.lua
new file mode 100644
index 0000000..90f44c1
--- /dev/null
+++ b/server/resty/openssl/include/rand.lua
@@ -0,0 +1,24 @@
+local ffi = require "ffi"
+
+require "resty.openssl.include.ossl_typ"
+local OPENSSL_3X = require("resty.openssl.version").OPENSSL_3X
+local BORINGSSL = require("resty.openssl.version").BORINGSSL
+
+if BORINGSSL then
+ ffi.cdef [[
+ int RAND_bytes(uint8_t *buf, size_t num);
+ int RAND_priv_bytes(uint8_t *buf, size_t num);
+ ]]
+elseif OPENSSL_3X then
+ ffi.cdef [[
+ int RAND_bytes_ex(OSSL_LIB_CTX *ctx, unsigned char *buf, size_t num,
+ unsigned int strength);
+ int RAND_priv_bytes_ex(OSSL_LIB_CTX *ctx, unsigned char *buf, size_t num,
+ unsigned int strength);
+ ]]
+else
+ ffi.cdef [[
+ int RAND_bytes(unsigned char *buf, int num);
+ int RAND_priv_bytes(unsigned char *buf, int num);
+ ]]
+end
diff --git a/server/resty/openssl/include/rsa.lua b/server/resty/openssl/include/rsa.lua
new file mode 100644
index 0000000..d7de5f4
--- /dev/null
+++ b/server/resty/openssl/include/rsa.lua
@@ -0,0 +1,70 @@
+local ffi = require "ffi"
+
+require "resty.openssl.include.ossl_typ"
+local OPENSSL_10 = require("resty.openssl.version").OPENSSL_10
+local OPENSSL_11_OR_LATER = require("resty.openssl.version").OPENSSL_11_OR_LATER
+
+ffi.cdef [[
+ RSA *RSA_new(void);
+ void RSA_free(RSA *r);
+]]
+
+if OPENSSL_11_OR_LATER then
+ ffi.cdef [[
+ void RSA_get0_key(const RSA *r,
+ const BIGNUM **n, const BIGNUM **e, const BIGNUM **d);
+ void RSA_get0_factors(const RSA *r, const BIGNUM **p, const BIGNUM **q);
+ void RSA_get0_crt_params(const RSA *r,
+ const BIGNUM **dmp1, const BIGNUM **dmq1,
+ const BIGNUM **iqmp);
+
+ int RSA_set0_key(RSA *r, BIGNUM *n, BIGNUM *e, BIGNUM *d);
+ int RSA_set0_factors(RSA *r, BIGNUM *p, BIGNUM *q);
+ int RSA_set0_crt_params(RSA *r,BIGNUM *dmp1, BIGNUM *dmq1, BIGNUM *iqmp);
+ struct rsa_st;
+ ]]
+elseif OPENSSL_10 then
+ ffi.cdef [[
+ // crypto/rsa/rsa_locl.h
+ // needed to extract parameters
+ // Note: this struct is trimmed
+ struct rsa_st {
+ int pad;
+ // the following has been changed in OpenSSL 1.1.x to int32_t
+ long version;
+ const RSA_METHOD *meth;
+ ENGINE *engine;
+ BIGNUM *n;
+ BIGNUM *e;
+ BIGNUM *d;
+ BIGNUM *p;
+ BIGNUM *q;
+ BIGNUM *dmp1;
+ BIGNUM *dmq1;
+ BIGNUM *iqmp;
+ // trimmed
+
+ // CRYPTO_EX_DATA ex_data;
+ // int references;
+ // int flags;
+ // BN_MONT_CTX *_method_mod_n;
+ // BN_MONT_CTX *_method_mod_p;
+ // BN_MONT_CTX *_method_mod_q;
+
+ // char *bignum_data;
+ // BN_BLINDING *blinding;
+ // BN_BLINDING *mt_blinding;
+ };
+ ]]
+end
+
+return {
+ paddings = {
+ RSA_PKCS1_PADDING = 1,
+ RSA_SSLV23_PADDING = 2,
+ RSA_NO_PADDING = 3,
+ RSA_PKCS1_OAEP_PADDING = 4,
+ RSA_X931_PADDING = 5,
+ RSA_PKCS1_PSS_PADDING = 6,
+ },
+}
diff --git a/server/resty/openssl/include/ssl.lua b/server/resty/openssl/include/ssl.lua
new file mode 100644
index 0000000..1219ac3
--- /dev/null
+++ b/server/resty/openssl/include/ssl.lua
@@ -0,0 +1,113 @@
+local ffi = require "ffi"
+local C = ffi.C
+
+require "resty.openssl.include.ossl_typ"
+require "resty.openssl.include.stack"
+local OPENSSL_3X = require("resty.openssl.version").OPENSSL_3X
+local BORINGSSL = require("resty.openssl.version").BORINGSSL
+
+ffi.cdef [[
+ // SSL_METHOD
+ typedef struct ssl_method_st SSL_METHOD;
+ const SSL_METHOD *TLS_method(void);
+ const SSL_METHOD *TLS_server_method(void);
+
+ // SSL_CIPHER
+ typedef struct ssl_cipher_st SSL_CIPHER;
+ const char *SSL_CIPHER_get_name(const SSL_CIPHER *cipher);
+ SSL_CIPHER *SSL_get_current_cipher(const SSL *ssl);
+
+ SSL_CTX *SSL_CTX_new(const SSL_METHOD *meth);
+ void SSL_CTX_free(SSL_CTX *a);
+
+ // SSL_SESSION
+ typedef struct ssl_session_st SSL_SESSION;
+ SSL_SESSION *SSL_get_session(const SSL *ssl);
+ long SSL_SESSION_set_timeout(SSL_SESSION *s, long t);
+ long SSL_SESSION_get_timeout(const SSL_SESSION *s);
+
+ typedef int (*SSL_CTX_alpn_select_cb_func)(SSL *ssl,
+ const unsigned char **out,
+ unsigned char *outlen,
+ const unsigned char *in,
+ unsigned int inlen,
+ void *arg);
+ void SSL_CTX_set_alpn_select_cb(SSL_CTX *ctx,
+ SSL_CTX_alpn_select_cb_func cb,
+ void *arg);
+
+ int SSL_select_next_proto(unsigned char **out, unsigned char *outlen,
+ const unsigned char *server,
+ unsigned int server_len,
+ const unsigned char *client,
+ unsigned int client_len);
+
+ SSL *SSL_new(SSL_CTX *ctx);
+ void SSL_free(SSL *ssl);
+
+ int SSL_set_cipher_list(SSL *ssl, const char *str);
+ int SSL_set_ciphersuites(SSL *s, const char *str);
+
+ long SSL_set_options(SSL *ssl, long options);
+ long SSL_clear_options(SSL *ssl, long options);
+ long SSL_get_options(SSL *ssl);
+
+ /*STACK_OF(SSL_CIPHER)*/ OPENSSL_STACK *SSL_get_ciphers(const SSL *ssl);
+ /*STACK_OF(SSL_CIPHER)*/ OPENSSL_STACK *SSL_CTX_get_ciphers(const SSL_CTX *ctx);
+ OPENSSL_STACK *SSL_get_peer_cert_chain(const SSL *ssl);
+
+ typedef int (*verify_callback)(int preverify_ok, X509_STORE_CTX *x509_ctx);
+ void SSL_set_verify(SSL *s, int mode,
+ int (*verify_callback)(int, X509_STORE_CTX *));
+
+ int SSL_add_client_CA(SSL *ssl, X509 *cacert);
+
+ long SSL_ctrl(SSL *ssl, int cmd, long larg, void *parg);
+]]
+
+if OPENSSL_3X then
+ ffi.cdef [[
+ X509 *SSL_get1_peer_certificate(const SSL *ssl);
+ ]]
+else
+ ffi.cdef [[
+ X509 *SSL_get_peer_certificate(const SSL *ssl);
+ ]]
+end
+
+if BORINGSSL then
+ ffi.cdef [[
+ int SSL_set_min_proto_version(SSL *ssl, int version);
+ int SSL_set_max_proto_version(SSL *ssl, int version);
+ ]]
+end
+
+local SSL_CTRL_SET_MIN_PROTO_VERSION = 123
+local SSL_CTRL_SET_MAX_PROTO_VERSION = 124
+
+local SSL_set_min_proto_version
+if BORINGSSL then
+ SSL_set_min_proto_version = function(ctx, version)
+ return C.SSL_set_min_proto_version(ctx, version)
+ end
+else
+ SSL_set_min_proto_version = function(ctx, version)
+ return C.SSL_ctrl(ctx, SSL_CTRL_SET_MIN_PROTO_VERSION, version, nil)
+ end
+end
+
+local SSL_set_max_proto_version
+if BORINGSSL then
+ SSL_set_max_proto_version = function(ctx, version)
+ return C.SSL_set_max_proto_version(ctx, version)
+ end
+else
+ SSL_set_max_proto_version = function(ctx, version)
+ return C.SSL_ctrl(ctx, SSL_CTRL_SET_MAX_PROTO_VERSION, version, nil)
+ end
+end
+
+return {
+ SSL_set_min_proto_version = SSL_set_min_proto_version,
+ SSL_set_max_proto_version = SSL_set_max_proto_version,
+}
diff --git a/server/resty/openssl/include/stack.lua b/server/resty/openssl/include/stack.lua
new file mode 100644
index 0000000..5732608
--- /dev/null
+++ b/server/resty/openssl/include/stack.lua
@@ -0,0 +1,95 @@
+--[[
+ The OpenSSL stack library. Note `safestack` is not usable here in ffi because
+ those symbols are eaten after preprocessing.
+ Instead, we should do a Lua land type checking by having a nested field indicating
+ which type of cdata its ctx holds.
+]]
+
+local ffi = require "ffi"
+local C = ffi.C
+
+require "resty.openssl.include.ossl_typ"
+local OPENSSL_10 = require("resty.openssl.version").OPENSSL_10
+local OPENSSL_11_OR_LATER = require("resty.openssl.version").OPENSSL_11_OR_LATER
+local BORINGSSL = require("resty.openssl.version").BORINGSSL
+
+local _M = {}
+
+ffi.cdef [[
+ typedef char *OPENSSL_STRING;
+]]
+
+if OPENSSL_11_OR_LATER and not BORINGSSL then
+ ffi.cdef [[
+ typedef struct stack_st OPENSSL_STACK;
+
+ OPENSSL_STACK *OPENSSL_sk_new_null(void);
+ int OPENSSL_sk_push(OPENSSL_STACK *st, const void *data);
+ void OPENSSL_sk_pop_free(OPENSSL_STACK *st, void (*func) (void *));
+ int OPENSSL_sk_num(const OPENSSL_STACK *);
+ void *OPENSSL_sk_value(const OPENSSL_STACK *, int);
+ OPENSSL_STACK *OPENSSL_sk_dup(const OPENSSL_STACK *st);
+ void OPENSSL_sk_free(OPENSSL_STACK *);
+ void *OPENSSL_sk_delete(OPENSSL_STACK *st, int loc);
+
+ typedef void (*OPENSSL_sk_freefunc)(void *);
+ typedef void *(*OPENSSL_sk_copyfunc)(const void *);
+ OPENSSL_STACK *OPENSSL_sk_deep_copy(const OPENSSL_STACK *,
+ OPENSSL_sk_copyfunc c,
+ OPENSSL_sk_freefunc f);
+ ]]
+ _M.OPENSSL_sk_pop_free = C.OPENSSL_sk_pop_free
+
+ _M.OPENSSL_sk_new_null = C.OPENSSL_sk_new_null
+ _M.OPENSSL_sk_push = C.OPENSSL_sk_push
+ _M.OPENSSL_sk_pop_free = C.OPENSSL_sk_pop_free
+ _M.OPENSSL_sk_num = C.OPENSSL_sk_num
+ _M.OPENSSL_sk_value = C.OPENSSL_sk_value
+ _M.OPENSSL_sk_dup = C.OPENSSL_sk_dup
+ _M.OPENSSL_sk_delete = C.OPENSSL_sk_delete
+ _M.OPENSSL_sk_free = C.OPENSSL_sk_free
+ _M.OPENSSL_sk_deep_copy = C.OPENSSL_sk_deep_copy
+elseif OPENSSL_10 or BORINGSSL then
+ ffi.cdef [[
+ typedef struct stack_st _STACK;
+ // i made this up
+ typedef struct stack_st OPENSSL_STACK;
+
+ _STACK *sk_new_null(void);
+ void sk_pop_free(_STACK *st, void (*func) (void *));
+ _STACK *sk_dup(_STACK *st);
+ void sk_free(_STACK *st);
+
+ _STACK *sk_deep_copy(_STACK *, void *(*)(void *), void (*)(void *));
+ ]]
+
+ if BORINGSSL then -- indices are using size_t instead of int
+ ffi.cdef [[
+ size_t sk_push(_STACK *st, void *data);
+ size_t sk_num(const _STACK *);
+ void *sk_value(const _STACK *, size_t);
+ void *sk_delete(_STACK *st, size_t loc);
+ ]]
+ else -- normal OpenSSL 1.0
+ ffi.cdef [[
+ int sk_push(_STACK *st, void *data);
+ int sk_num(const _STACK *);
+ void *sk_value(const _STACK *, int);
+ void *sk_delete(_STACK *st, int loc);
+ ]]
+ end
+
+ _M.OPENSSL_sk_pop_free = C.sk_pop_free
+
+ _M.OPENSSL_sk_new_null = C.sk_new_null
+ _M.OPENSSL_sk_push = function(...) return tonumber(C.sk_push(...)) end
+ _M.OPENSSL_sk_pop_free = C.sk_pop_free
+ _M.OPENSSL_sk_num = function(...) return tonumber(C.sk_num(...)) end
+ _M.OPENSSL_sk_value = C.sk_value
+ _M.OPENSSL_sk_delete = C.sk_delete
+ _M.OPENSSL_sk_dup = C.sk_dup
+ _M.OPENSSL_sk_free = C.sk_free
+ _M.OPENSSL_sk_deep_copy = C.sk_deep_copy
+end
+
+return _M
diff --git a/server/resty/openssl/include/x509/altname.lua b/server/resty/openssl/include/x509/altname.lua
new file mode 100644
index 0000000..ce1db67
--- /dev/null
+++ b/server/resty/openssl/include/x509/altname.lua
@@ -0,0 +1,49 @@
+local GEN_OTHERNAME = 0
+local GEN_EMAIL = 1
+local GEN_DNS = 2
+local GEN_X400 = 3
+local GEN_DIRNAME = 4
+local GEN_EDIPARTY = 5
+local GEN_URI = 6
+local GEN_IPADD = 7
+local GEN_RID = 8
+
+local default_types = {
+ OtherName = GEN_OTHERNAME, -- otherName
+ RFC822Name = GEN_EMAIL, -- email
+ RFC822 = GEN_EMAIL,
+ Email = GEN_EMAIL,
+ DNSName = GEN_DNS, -- dns
+ DNS = GEN_DNS,
+ X400 = GEN_X400, -- x400
+ DirName = GEN_DIRNAME, -- dirName
+ EdiParty = GEN_EDIPARTY, -- EdiParty
+ UniformResourceIdentifier = GEN_URI, -- uri
+ URI = GEN_URI,
+ IPAddress = GEN_IPADD, -- ipaddr
+ IP = GEN_IPADD,
+ RID = GEN_RID, -- rid
+}
+
+local literals = {
+ [GEN_OTHERNAME] = "OtherName",
+ [GEN_EMAIL] = "email",
+ [GEN_DNS] = "DNS",
+ [GEN_X400] = "X400",
+ [GEN_DIRNAME] = "DirName",
+ [GEN_EDIPARTY] = "EdiParty",
+ [GEN_URI] = "URI",
+ [GEN_IPADD] = "IP",
+ [GEN_RID] = "RID",
+}
+
+local types = {}
+for t, gid in pairs(default_types) do
+ types[t:lower()] = gid
+ types[t] = gid
+end
+
+return {
+ types = types,
+ literals = literals,
+} \ No newline at end of file
diff --git a/server/resty/openssl/include/x509/crl.lua b/server/resty/openssl/include/x509/crl.lua
new file mode 100644
index 0000000..7870cd3
--- /dev/null
+++ b/server/resty/openssl/include/x509/crl.lua
@@ -0,0 +1,86 @@
+local ffi = require "ffi"
+
+require "resty.openssl.include.ossl_typ"
+require "resty.openssl.include.evp"
+require "resty.openssl.include.objects"
+require "resty.openssl.include.x509"
+require "resty.openssl.include.stack"
+
+local asn1_macro = require "resty.openssl.include.asn1"
+
+local OPENSSL_10 = require("resty.openssl.version").OPENSSL_10
+local OPENSSL_11_OR_LATER = require("resty.openssl.version").OPENSSL_11_OR_LATER
+local BORINGSSL_110 = require("resty.openssl.version").BORINGSSL_110
+
+asn1_macro.declare_asn1_functions("X509_CRL", asn1_macro.has_new_ex)
+
+ffi.cdef [[
+ X509_NAME *X509_CRL_get_issuer(const X509_CRL *crl);
+ int X509_CRL_set_issuer_name(X509_CRL *x, X509_NAME *name);
+ int X509_CRL_set_version(X509_CRL *x, long version);
+
+ int X509_CRL_add_ext(X509_CRL *x, X509_EXTENSION *ex, int loc);
+ X509_EXTENSION *X509_CRL_get_ext(const X509_CRL *x, int loc);
+ int X509_CRL_get_ext_by_NID(const X509_CRL *x, int nid, int lastpos);
+ void *X509_CRL_get_ext_d2i(const X509_CRL *x, int nid, int *crit, int *idx);
+
+ int X509_CRL_sign(X509_CRL *x, EVP_PKEY *pkey, const EVP_MD *md);
+ int X509_CRL_verify(X509_CRL *a, EVP_PKEY *r);
+
+ int i2d_X509_CRL_bio(BIO *bp, X509_CRL *crl);
+ X509_CRL *d2i_X509_CRL_bio(BIO *bp, X509_CRL **crl);
+ int X509_CRL_add0_revoked(X509_CRL *crl, X509_REVOKED *rev);
+
+ int X509_CRL_print(BIO *bio, X509_CRL *crl);
+
+ int X509_CRL_get0_by_serial(X509_CRL *crl,
+ X509_REVOKED **ret, ASN1_INTEGER *serial);
+ int X509_CRL_get0_by_cert(X509_CRL *crl, X509_REVOKED **ret, X509 *x);
+
+ //STACK_OF(X509_REVOKED)
+ OPENSSL_STACK *X509_CRL_get_REVOKED(X509_CRL *crl);
+
+ int X509_CRL_get0_by_serial(X509_CRL *crl,
+ X509_REVOKED **ret, ASN1_INTEGER *serial);
+]]
+
+if OPENSSL_11_OR_LATER then
+ ffi.cdef [[
+ int X509_CRL_set1_lastUpdate(X509_CRL *x, const ASN1_TIME *tm);
+ int X509_CRL_set1_nextUpdate(X509_CRL *x, const ASN1_TIME *tm);
+ /*const*/ ASN1_TIME *X509_CRL_get0_lastUpdate(const X509_CRL *crl);
+ /*const*/ ASN1_TIME *X509_CRL_get0_nextUpdate(const X509_CRL *crl);
+ long X509_CRL_get_version(const X509_CRL *crl);
+
+ X509_EXTENSION *X509_CRL_delete_ext(X509_CRL *x, int loc);
+
+ int X509_CRL_get_signature_nid(const X509_CRL *crl);
+ ]]
+end
+if OPENSSL_10 or BORINGSSL_110 then
+ -- in openssl 1.0.x some getters are direct accessor to struct members (defiend by macros)
+ ffi.cdef [[
+ typedef struct X509_crl_info_st {
+ ASN1_INTEGER *version;
+ X509_ALGOR *sig_alg;
+ X509_NAME *issuer;
+ ASN1_TIME *lastUpdate;
+ ASN1_TIME *nextUpdate;
+ // STACK_OF(X509_REVOKED)
+ OPENSSL_STACK *revoked;
+ // STACK_OF(X509_EXTENSION)
+ OPENSSL_STACK /* [0] */ *extensions;
+ ASN1_ENCODING enc;
+ } X509_CRL_INFO;
+
+ // Note: this struct is trimmed
+ struct X509_crl_st {
+ /* actual signature */
+ X509_CRL_INFO *crl;
+ // trimmed
+ } /* X509_CRL */ ;
+
+ int X509_CRL_set_lastUpdate(X509_CRL *x, const ASN1_TIME *tm);
+ int X509_CRL_set_nextUpdate(X509_CRL *x, const ASN1_TIME *tm);
+ ]]
+end
diff --git a/server/resty/openssl/include/x509/csr.lua b/server/resty/openssl/include/x509/csr.lua
new file mode 100644
index 0000000..44c4801
--- /dev/null
+++ b/server/resty/openssl/include/x509/csr.lua
@@ -0,0 +1,88 @@
+local ffi = require "ffi"
+
+require "resty.openssl.include.ossl_typ"
+require "resty.openssl.include.evp"
+require "resty.openssl.include.objects"
+require "resty.openssl.include.x509"
+require "resty.openssl.include.stack"
+
+local asn1_macro = require "resty.openssl.include.asn1"
+
+local OPENSSL_10 = require("resty.openssl.version").OPENSSL_10
+local OPENSSL_11_OR_LATER = require("resty.openssl.version").OPENSSL_11_OR_LATER
+local OPENSSL_3X = require("resty.openssl.version").OPENSSL_3X
+local BORINGSSL_110 = require("resty.openssl.version").BORINGSSL_110
+
+asn1_macro.declare_asn1_functions("X509_REQ", asn1_macro.has_new_ex)
+
+ffi.cdef [[
+ int X509_REQ_set_subject_name(X509_REQ *req, X509_NAME *name);
+
+ EVP_PKEY *X509_REQ_get_pubkey(X509_REQ *req);
+ int X509_REQ_set_pubkey(X509_REQ *x, EVP_PKEY *pkey);
+
+ int X509_REQ_set_version(X509_REQ *x, long version);
+
+ int X509_REQ_get_attr_count(const X509_REQ *req);
+
+ int X509_CRL_add_ext(X509_CRL *x, X509_EXTENSION *ex, int loc);
+ X509_EXTENSION *X509_CRL_get_ext(const X509_CRL *x, int loc);
+ int X509_CRL_get_ext_by_NID(const X509_CRL *x, int nid, int lastpos);
+
+ int i2d_re_X509_REQ_tbs(X509_REQ *req, unsigned char **pp);
+ void X509_ATTRIBUTE_free(X509_ATTRIBUTE *a);
+ int X509_REQ_get_attr_by_NID(const X509_REQ *req, int nid, int lastpos);
+ X509_ATTRIBUTE *X509_REQ_delete_attr(X509_REQ *req, int loc);
+
+ int *X509_REQ_get_extension_nids(void);
+
+ int X509_REQ_sign(X509_REQ *x, EVP_PKEY *pkey, const EVP_MD *md);
+ int X509_REQ_verify(X509_REQ *a, EVP_PKEY *r);
+
+ int i2d_X509_REQ_bio(BIO *bp, X509_REQ *req);
+ X509_REQ *d2i_X509_REQ_bio(BIO *bp, X509_REQ **req);
+
+ // STACK_OF(X509_EXTENSION)
+ OPENSSL_STACK *X509_REQ_get_extensions(X509_REQ *req);
+ // STACK_OF(X509_EXTENSION)
+ int X509_REQ_add_extensions(X509_REQ *req, OPENSSL_STACK *exts);
+
+ int X509_REQ_check_private_key(X509_REQ *x, EVP_PKEY *k);
+]]
+
+if OPENSSL_11_OR_LATER then
+ ffi.cdef [[
+ X509_NAME *X509_REQ_get_subject_name(const X509_REQ *req);
+ long X509_REQ_get_version(const X509_REQ *req);
+
+ int X509_REQ_get_signature_nid(const X509_REQ *crl);
+ ]]
+end
+if OPENSSL_10 or BORINGSSL_110 then
+ ffi.cdef [[
+ typedef struct X509_req_info_st {
+ ASN1_ENCODING enc;
+ ASN1_INTEGER *version;
+ X509_NAME *subject;
+ /*X509_PUBKEY*/ void *pubkey;
+ /* d=2 hl=2 l= 0 cons: cont: 00 */
+ /*STACK_OF(X509_ATTRIBUTE)*/ OPENSSL_STACK *attributes; /* [ 0 ] */
+ } X509_REQ_INFO;
+
+ // Note: this struct is trimmed
+ typedef struct X509_req_st {
+ X509_REQ_INFO *req_info;
+ X509_ALGOR *sig_alg;
+ // trimmed
+ //ASN1_BIT_STRING *signature;
+ //int references;
+ } X509_REQ;
+ ]]
+end
+
+if OPENSSL_3X then
+ ffi.cdef [[
+ int X509_REQ_verify_ex(X509_REQ *a, EVP_PKEY *pkey, OSSL_LIB_CTX *libctx,
+ const char *propq);
+ ]]
+end
diff --git a/server/resty/openssl/include/x509/extension.lua b/server/resty/openssl/include/x509/extension.lua
new file mode 100644
index 0000000..14b231e
--- /dev/null
+++ b/server/resty/openssl/include/x509/extension.lua
@@ -0,0 +1,44 @@
+local ffi = require "ffi"
+
+require "resty.openssl.include.ossl_typ"
+require "resty.openssl.include.x509v3"
+require "resty.openssl.include.x509"
+local asn1_macro = require "resty.openssl.include.asn1"
+local OPENSSL_3X = require("resty.openssl.version").OPENSSL_3X
+
+asn1_macro.declare_asn1_functions("X509_EXTENSION")
+
+if OPENSSL_3X then
+ ffi.cdef [[
+ struct v3_ext_ctx {
+ int flags;
+ X509 *issuer_cert;
+ X509 *subject_cert;
+ X509_REQ *subject_req;
+ X509_CRL *crl;
+ /*X509V3_CONF_METHOD*/ void *db_meth;
+ void *db;
+ EVP_PKEY *issuer_pkey;
+ };
+
+ int X509V3_set_issuer_pkey(X509V3_CTX *ctx, EVP_PKEY *pkey);
+ ]]
+
+else
+ ffi.cdef [[
+ struct v3_ext_ctx {
+ int flags;
+ X509 *issuer_cert;
+ X509 *subject_cert;
+ X509_REQ *subject_req;
+ X509_CRL *crl;
+ /*X509V3_CONF_METHOD*/ void *db_meth;
+ void *db;
+ };
+ ]]
+end
+
+ffi.cdef [[
+ int X509_EXTENSION_set_data(X509_EXTENSION *ex, ASN1_OCTET_STRING *data);
+ int X509_EXTENSION_set_object(X509_EXTENSION *ex, const ASN1_OBJECT *obj);
+]] \ No newline at end of file
diff --git a/server/resty/openssl/include/x509/init.lua b/server/resty/openssl/include/x509/init.lua
new file mode 100644
index 0000000..ec104ef
--- /dev/null
+++ b/server/resty/openssl/include/x509/init.lua
@@ -0,0 +1,138 @@
+local ffi = require "ffi"
+
+require "resty.openssl.include.ossl_typ"
+require "resty.openssl.include.bio"
+require "resty.openssl.include.pem"
+require "resty.openssl.include.stack"
+local asn1_macro = require "resty.openssl.include.asn1"
+
+local OPENSSL_10 = require("resty.openssl.version").OPENSSL_10
+local OPENSSL_11_OR_LATER = require("resty.openssl.version").OPENSSL_11_OR_LATER
+local BORINGSSL_110 = require("resty.openssl.version").BORINGSSL_110
+
+asn1_macro.declare_asn1_functions("X509", asn1_macro.has_new_ex)
+
+ffi.cdef [[
+ int i2d_X509_bio(BIO *bp, X509 *x509);
+ X509 *d2i_X509_bio(BIO *bp, X509 **x509);
+
+ // STACK_OF(X509)
+ OPENSSL_STACK *X509_chain_up_ref(OPENSSL_STACK *chain);
+
+ int X509_sign(X509 *x, EVP_PKEY *pkey, const EVP_MD *md);
+ int X509_verify(X509 *a, EVP_PKEY *r);
+
+ ASN1_TIME *X509_gmtime_adj(ASN1_TIME *s, long adj);
+
+ int X509_add_ext(X509 *x, X509_EXTENSION *ex, int loc);
+ X509_EXTENSION *X509_get_ext(const X509 *x, int loc);
+ int X509_get_ext_by_NID(const X509 *x, int nid, int lastpos);
+ void *X509_get_ext_d2i(const X509 *x, int nid, int *crit, int *idx);
+
+ int X509_EXTENSION_set_critical(X509_EXTENSION *ex, int crit);
+ int X509_EXTENSION_get_critical(const X509_EXTENSION *ex);
+ ASN1_OBJECT *X509_EXTENSION_get_object(X509_EXTENSION *ex);
+ ASN1_OCTET_STRING *X509_EXTENSION_get_data(X509_EXTENSION *ne);
+ X509_EXTENSION *X509V3_EXT_i2d(int ext_nid, int crit, void *ext_struc);
+ X509_EXTENSION *X509_EXTENSION_create_by_NID(X509_EXTENSION **ex,
+ int nid, int crit,
+ ASN1_OCTET_STRING *data);
+
+ // needed by pkey
+ EVP_PKEY *d2i_PrivateKey_bio(BIO *bp, EVP_PKEY **a);
+ EVP_PKEY *d2i_PUBKEY_bio(BIO *bp, EVP_PKEY **a);
+ int i2d_PrivateKey_bio(BIO *bp, EVP_PKEY *pkey);
+ int i2d_PUBKEY_bio(BIO *bp, EVP_PKEY *pkey);
+
+ EVP_PKEY *X509_get_pubkey(X509 *x);
+ int X509_set_pubkey(X509 *x, EVP_PKEY *pkey);
+ int X509_set_version(X509 *x, long version);
+ int X509_set_serialNumber(X509 *x, ASN1_INTEGER *serial);
+
+ X509_NAME *X509_get_subject_name(const X509 *a);
+ int X509_set_subject_name(X509 *x, X509_NAME *name);
+ X509_NAME *X509_get_issuer_name(const X509 *a);
+ int X509_set_issuer_name(X509 *x, X509_NAME *name);
+
+ int X509_pubkey_digest(const X509 *data, const EVP_MD *type,
+ unsigned char *md, unsigned int *len);
+ int X509_digest(const X509 *data, const EVP_MD *type,
+ unsigned char *md, unsigned int *len);
+
+ const char *X509_verify_cert_error_string(long n);
+ int X509_verify_cert(X509_STORE_CTX *ctx);
+
+ int X509_get_signature_nid(const X509 *x);
+
+ unsigned char *X509_alias_get0(X509 *x, int *len);
+ unsigned char *X509_keyid_get0(X509 *x, int *len);
+ int X509_check_private_key(X509 *x, EVP_PKEY *k);
+]]
+
+if OPENSSL_11_OR_LATER then
+ ffi.cdef [[
+ int X509_up_ref(X509 *a);
+
+ int X509_set1_notBefore(X509 *x, const ASN1_TIME *tm);
+ int X509_set1_notAfter(X509 *x, const ASN1_TIME *tm);
+ /*const*/ ASN1_TIME *X509_get0_notBefore(const X509 *x);
+ /*const*/ ASN1_TIME *X509_get0_notAfter(const X509 *x);
+ long X509_get_version(const X509 *x);
+ const ASN1_INTEGER *X509_get0_serialNumber(X509 *x);
+
+ X509_EXTENSION *X509_delete_ext(X509 *x, int loc);
+ ]]
+elseif OPENSSL_10 then
+ ffi.cdef [[
+ // STACK_OF(X509_EXTENSION)
+ X509_EXTENSION *X509v3_delete_ext(OPENSSL_STACK *x, int loc);
+ ]]
+end
+
+if OPENSSL_10 or BORINGSSL_110 then
+ -- in openssl 1.0.x some getters are direct accessor to struct members (defiend by macros)
+ ffi.cdef [[
+ // crypto/x509/x509.h
+ typedef struct X509_val_st {
+ ASN1_TIME *notBefore;
+ ASN1_TIME *notAfter;
+ } X509_VAL;
+
+ typedef struct X509_algor_st {
+ ASN1_OBJECT *algorithm;
+ ASN1_TYPE *parameter;
+ } X509_ALGOR;
+
+ // Note: this struct is trimmed
+ typedef struct x509_cinf_st {
+ /*ASN1_INTEGER*/ void *version;
+ /*ASN1_INTEGER*/ void *serialNumber;
+ X509_ALGOR *signature;
+ X509_NAME *issuer;
+ X509_VAL *validity;
+ X509_NAME *subject;
+ /*X509_PUBKEY*/ void *key;
+ /*ASN1_BIT_STRING*/ void *issuerUID; /* [ 1 ] optional in v2 */
+ /*ASN1_BIT_STRING*/ void *subjectUID; /* [ 2 ] optional in v2 */
+ /*STACK_OF(X509_EXTENSION)*/ OPENSSL_STACK *extensions; /* [ 3 ] optional in v3 */
+ // trimmed
+ // ASN1_ENCODING enc;
+ } X509_CINF;
+ // Note: this struct is trimmed
+ struct x509_st {
+ X509_CINF *cert_info;
+ // trimmed
+ } X509;
+
+ int X509_set_notBefore(X509 *x, const ASN1_TIME *tm);
+ int X509_set_notAfter(X509 *x, const ASN1_TIME *tm);
+ ASN1_INTEGER *X509_get_serialNumber(X509 *x);
+ ]]
+end
+
+if BORINGSSL_110 then
+ ffi.cdef [[
+ ASN1_TIME *X509_get_notBefore(const X509 *x);
+ ASN1_TIME *X509_get_notAfter(const X509 *x);
+ ]]
+end
diff --git a/server/resty/openssl/include/x509/name.lua b/server/resty/openssl/include/x509/name.lua
new file mode 100644
index 0000000..2f933ae
--- /dev/null
+++ b/server/resty/openssl/include/x509/name.lua
@@ -0,0 +1,21 @@
+local ffi = require "ffi"
+
+require "resty.openssl.include.ossl_typ"
+require "resty.openssl.include.asn1"
+require "resty.openssl.include.objects"
+local asn1_macro = require "resty.openssl.include.asn1"
+
+asn1_macro.declare_asn1_functions("X509_NAME")
+
+ffi.cdef [[
+ int X509_NAME_add_entry_by_OBJ(X509_NAME *name, const ASN1_OBJECT *obj, int type,
+ const unsigned char *bytes, int len, int loc,
+ int set);
+
+ int X509_NAME_entry_count(const X509_NAME *name);
+ X509_NAME_ENTRY *X509_NAME_get_entry(X509_NAME *name, int loc);
+ ASN1_OBJECT *X509_NAME_ENTRY_get_object(const X509_NAME_ENTRY *ne);
+ ASN1_STRING * X509_NAME_ENTRY_get_data(const X509_NAME_ENTRY *ne);
+ int X509_NAME_get_index_by_OBJ(X509_NAME *name, const ASN1_OBJECT *obj,
+ int lastpos);
+]] \ No newline at end of file
diff --git a/server/resty/openssl/include/x509/revoked.lua b/server/resty/openssl/include/x509/revoked.lua
new file mode 100644
index 0000000..c6539c9
--- /dev/null
+++ b/server/resty/openssl/include/x509/revoked.lua
@@ -0,0 +1,17 @@
+local ffi = require "ffi"
+
+require "resty.openssl.include.ossl_typ"
+require "resty.openssl.include.asn1"
+require "resty.openssl.include.objects"
+local asn1_macro = require "resty.openssl.include.asn1"
+
+asn1_macro.declare_asn1_functions("X509_REVOKED")
+
+ffi.cdef [[
+ int X509_REVOKED_set_serialNumber(X509_REVOKED *x, ASN1_INTEGER *serial);
+ int X509_REVOKED_set_revocationDate(X509_REVOKED *r, ASN1_TIME *tm);
+ int X509_REVOKED_add_ext(X509_REVOKED *x, X509_EXTENSION *ex, int loc);
+
+ const ASN1_INTEGER *X509_REVOKED_get0_serialNumber(const X509_REVOKED *r);
+ const ASN1_TIME *X509_REVOKED_get0_revocationDate(const X509_REVOKED *r);
+]] \ No newline at end of file
diff --git a/server/resty/openssl/include/x509_vfy.lua b/server/resty/openssl/include/x509_vfy.lua
new file mode 100644
index 0000000..d783d19
--- /dev/null
+++ b/server/resty/openssl/include/x509_vfy.lua
@@ -0,0 +1,108 @@
+local ffi = require "ffi"
+local C = ffi.C
+
+require "resty.openssl.include.ossl_typ"
+require "resty.openssl.include.stack"
+local OPENSSL_10 = require("resty.openssl.version").OPENSSL_10
+local OPENSSL_11_OR_LATER = require("resty.openssl.version").OPENSSL_11_OR_LATER
+local OPENSSL_3X = require("resty.openssl.version").OPENSSL_3X
+local BORINGSSL_110 = require("resty.openssl.version").BORINGSSL_110
+
+ffi.cdef [[
+ X509_STORE *X509_STORE_new(void);
+ void X509_STORE_free(X509_STORE *v);
+ /* int X509_STORE_lock(X509_STORE *ctx);
+ int X509_STORE_unlock(X509_STORE *ctx);
+ int X509_STORE_up_ref(X509_STORE *v);
+ // STACK_OF(X509_OBJECT)
+ OPENSSL_STACK *X509_STORE_get0_objects(X509_STORE *v);*/
+
+ int X509_STORE_add_cert(X509_STORE *ctx, X509 *x);
+ int X509_STORE_add_crl(X509_STORE *ctx, X509_CRL *x);
+ int X509_STORE_load_locations(X509_STORE *ctx,
+ const char *file, const char *dir);
+ int X509_STORE_set_default_paths(X509_STORE *ctx);
+ int X509_STORE_set_flags(X509_STORE *ctx, unsigned long flags);
+ int X509_STORE_set_depth(X509_STORE *store, int depth);
+ int X509_STORE_set_purpose(X509_STORE *ctx, int purpose);
+
+ X509_STORE_CTX *X509_STORE_CTX_new(void);
+ void X509_STORE_CTX_free(X509_STORE_CTX *ctx);
+ // STACK_OF(X509)
+ int X509_STORE_CTX_init(X509_STORE_CTX *ctx, X509_STORE *store,
+ X509 *x509, OPENSSL_STACK *chain);
+
+ int X509_STORE_CTX_get_error(X509_STORE_CTX *ctx);
+
+ int X509_STORE_CTX_set_default(X509_STORE_CTX *ctx, const char *name);
+
+ int X509_PURPOSE_get_by_sname(char *sname);
+ X509_PURPOSE *X509_PURPOSE_get0(int idx);
+ int X509_PURPOSE_get_id(const X509_PURPOSE *xp);
+]]
+
+local _M = {
+ verify_flags = {
+ X509_V_FLAG_CB_ISSUER_CHECK = 0x0, -- Deprecated
+ X509_V_FLAG_USE_CHECK_TIME = 0x2,
+ X509_V_FLAG_CRL_CHECK = 0x4,
+ X509_V_FLAG_CRL_CHECK_ALL = 0x8,
+ X509_V_FLAG_IGNORE_CRITICAL = 0x10,
+ X509_V_FLAG_X509_STRICT = 0x20,
+ X509_V_FLAG_ALLOW_PROXY_CERTS = 0x40,
+ X509_V_FLAG_POLICY_CHECK = 0x80,
+ X509_V_FLAG_EXPLICIT_POLICY = 0x100,
+ X509_V_FLAG_INHIBIT_ANY = 0x200,
+ X509_V_FLAG_INHIBIT_MAP = 0x400,
+ X509_V_FLAG_NOTIFY_POLICY = 0x800,
+ X509_V_FLAG_EXTENDED_CRL_SUPPORT = 0x1000,
+ X509_V_FLAG_USE_DELTAS = 0x2000,
+ X509_V_FLAG_CHECK_SS_SIGNATURE = 0x4000,
+ X509_V_FLAG_TRUSTED_FIRST = 0x8000,
+ X509_V_FLAG_SUITEB_128_LOS_ONLY = 0x10000,
+ X509_V_FLAG_SUITEB_192_LOS = 0x20000,
+ X509_V_FLAG_SUITEB_128_LOS = 0x30000,
+ X509_V_FLAG_PARTIAL_CHAIN = 0x80000,
+ X509_V_FLAG_NO_ALT_CHAINS = 0x100000,
+ X509_V_FLAG_NO_CHECK_TIME = 0x200000,
+ },
+}
+
+if OPENSSL_10 or BORINGSSL_110 then
+ ffi.cdef [[
+ // STACK_OF(X509)
+ OPENSSL_STACK *X509_STORE_CTX_get_chain(X509_STORE_CTX *ctx);
+ ]];
+ _M.X509_STORE_CTX_get0_chain = C.X509_STORE_CTX_get_chain
+elseif OPENSSL_11_OR_LATER then
+ ffi.cdef [[
+ // STACK_OF(X509)
+ OPENSSL_STACK *X509_STORE_CTX_get0_chain(X509_STORE_CTX *ctx);
+ ]];
+ _M.X509_STORE_CTX_get0_chain = C.X509_STORE_CTX_get0_chain
+end
+
+if OPENSSL_3X then
+ ffi.cdef [[
+ X509_STORE_CTX *X509_STORE_CTX_new_ex(OSSL_LIB_CTX *libctx, const char *propq);
+
+ int X509_STORE_set_default_paths_ex(X509_STORE *ctx, OSSL_LIB_CTX *libctx,
+ const char *propq);
+ /* int X509_STORE_load_file_ex(X509_STORE *ctx, const char *file,
+ OSSL_LIB_CTX *libctx, const char *propq);
+ int X509_STORE_load_store_ex(X509_STORE *ctx, const char *uri,
+ OSSL_LIB_CTX *libctx, const char *propq); */
+ int X509_STORE_load_locations_ex(X509_STORE *ctx, const char *file,
+ const char *dir, OSSL_LIB_CTX *libctx,
+ const char *propq);
+ ]]
+ _M.X509_STORE_set_default_paths = function(...) return C.X509_STORE_set_default_paths_ex(...) end
+ _M.X509_STORE_load_locations = function(...) return C.X509_STORE_load_locations_ex(...) end
+else
+ _M.X509_STORE_set_default_paths = function(s) return C.X509_STORE_set_default_paths(s) end
+ _M.X509_STORE_load_locations = function(s, file, dir) return C.X509_STORE_load_locations(s, file, dir) end
+end
+
+
+return _M
+
diff --git a/server/resty/openssl/include/x509v3.lua b/server/resty/openssl/include/x509v3.lua
new file mode 100644
index 0000000..6882c6e
--- /dev/null
+++ b/server/resty/openssl/include/x509v3.lua
@@ -0,0 +1,108 @@
+local ffi = require "ffi"
+
+require "resty.openssl.include.ossl_typ"
+require "resty.openssl.include.stack"
+local asn1_macro = require "resty.openssl.include.asn1"
+
+ffi.cdef [[
+ // STACK_OF(OPENSSL_STRING)
+ OPENSSL_STACK *X509_get1_ocsp(X509 *x);
+ void X509_email_free(OPENSSL_STACK *sk);
+ void X509V3_set_nconf(X509V3_CTX *ctx, CONF *conf);
+
+ typedef struct EDIPartyName_st EDIPARTYNAME;
+
+ typedef struct otherName_st OTHERNAME;
+
+ typedef struct GENERAL_NAME_st {
+ int type;
+ union {
+ char *ptr;
+ OTHERNAME *otherName; /* otherName */
+ ASN1_IA5STRING *rfc822Name;
+ ASN1_IA5STRING *dNSName;
+ ASN1_TYPE *x400Address;
+ X509_NAME *directoryName;
+ EDIPARTYNAME *ediPartyName;
+ ASN1_IA5STRING *uniformResourceIdentifier;
+ ASN1_OCTET_STRING *iPAddress;
+ ASN1_OBJECT *registeredID;
+ /* Old names */
+ ASN1_OCTET_STRING *ip; /* iPAddress */
+ X509_NAME *dirn; /* dirn */
+ ASN1_IA5STRING *ia5; /* rfc822Name, dNSName,
+ * uniformResourceIdentifier */
+ ASN1_OBJECT *rid; /* registeredID */
+ ASN1_TYPE *other; /* x400Address */
+ } d;
+ } GENERAL_NAME;
+
+ // STACK_OF(GENERAL_NAME)
+ typedef struct stack_st GENERAL_NAMES;
+
+ // STACK_OF(X509_EXTENSION)
+ int X509V3_add1_i2d(OPENSSL_STACK **x, int nid, void *value,
+ int crit, unsigned long flags);
+ void *X509V3_EXT_d2i(X509_EXTENSION *ext);
+ X509_EXTENSION *X509V3_EXT_i2d(int ext_nid, int crit, void *ext_struc);
+ int X509V3_EXT_print(BIO *out, X509_EXTENSION *ext, unsigned long flag,
+ int indent);
+
+ int X509_add1_ext_i2d(X509 *x, int nid, void *value, int crit,
+ unsigned long flags);
+ // although the struct has plural form, it's not a stack
+ typedef struct BASIC_CONSTRAINTS_st {
+ int ca;
+ ASN1_INTEGER *pathlen;
+ } BASIC_CONSTRAINTS;
+
+ void X509V3_set_ctx(X509V3_CTX *ctx, X509 *issuer, X509 *subject,
+ X509_REQ *req, X509_CRL *crl, int flags);
+
+ X509_EXTENSION *X509V3_EXT_nconf_nid(CONF *conf, X509V3_CTX *ctx, int ext_nid,
+ const char *value);
+ X509_EXTENSION *X509V3_EXT_nconf(CONF *conf, X509V3_CTX *ctx, const char *name,
+ const char *value);
+ int X509V3_EXT_print(BIO *out, X509_EXTENSION *ext, unsigned long flag,
+ int indent);
+
+ void *X509V3_get_d2i(const OPENSSL_STACK *x, int nid, int *crit, int *idx);
+
+ int X509v3_get_ext_by_NID(const OPENSSL_STACK *x,
+ int nid, int lastpos);
+
+ X509_EXTENSION *X509v3_get_ext(const OPENSSL_STACK *x, int loc);
+
+ // STACK_OF(ACCESS_DESCRIPTION)
+ typedef struct stack_st AUTHORITY_INFO_ACCESS;
+
+ typedef struct ACCESS_DESCRIPTION_st {
+ ASN1_OBJECT *method;
+ GENERAL_NAME *location;
+ } ACCESS_DESCRIPTION;
+
+ typedef struct DIST_POINT_NAME_st {
+ int type;
+ union {
+ GENERAL_NAMES *fullname;
+ // STACK_OF(X509_NAME_ENTRY)
+ OPENSSL_STACK *relativename;
+ } name;
+ /* If relativename then this contains the full distribution point name */
+ X509_NAME *dpname;
+ } DIST_POINT_NAME;
+
+ typedef struct DIST_POINT_st {
+ DIST_POINT_NAME *distpoint;
+ ASN1_BIT_STRING *reasons;
+ GENERAL_NAMES *CRLissuer;
+ int dp_reasons;
+ } DIST_POINT;
+
+]]
+
+asn1_macro.declare_asn1_functions("GENERAL_NAME")
+asn1_macro.declare_asn1_functions("BASIC_CONSTRAINTS")
+asn1_macro.declare_asn1_functions("AUTHORITY_INFO_ACCESS") -- OCSP responder and CA
+asn1_macro.declare_asn1_functions("ACCESS_DESCRIPTION")
+asn1_macro.declare_asn1_functions("DIST_POINT") -- CRL distribution points
diff --git a/server/resty/openssl/kdf.lua b/server/resty/openssl/kdf.lua
new file mode 100644
index 0000000..62188bc
--- /dev/null
+++ b/server/resty/openssl/kdf.lua
@@ -0,0 +1,388 @@
+local ffi = require "ffi"
+local C = ffi.C
+local ffi_gc = ffi.gc
+local ffi_str = ffi.string
+
+require("resty.openssl.objects")
+require("resty.openssl.include.evp.md")
+-- used by legacy EVP_PKEY_derive interface
+require("resty.openssl.include.evp.pkey")
+local kdf_macro = require "resty.openssl.include.evp.kdf"
+local ctx_lib = require "resty.openssl.ctx"
+local format_error = require("resty.openssl.err").format_error
+local version_num = require("resty.openssl.version").version_num
+local version_text = require("resty.openssl.version").version_text
+local BORINGSSL = require("resty.openssl.version").BORINGSSL
+local OPENSSL_3X = require("resty.openssl.version").OPENSSL_3X
+local ctypes = require "resty.openssl.auxiliary.ctypes"
+
+--[[
+https://wiki.openssl.org/index.php/EVP_Key_Derivation
+
+OpenSSL 1.0.2 and above provides PBKDF2 by way of PKCS5_PBKDF2_HMAC and PKCS5_PBKDF2_HMAC_SHA1.
+OpenSSL 1.1.0 and above additionally provides HKDF and TLS1 PRF KDF by way of EVP_PKEY_derive and Scrypt by way of EVP_PBE_scrypt
+OpenSSL 1.1.1 and above additionally provides Scrypt by way of EVP_PKEY_derive.
+OpenSSL 3.0 additionally provides Single Step KDF, SSH KDF, PBKDF2, Scrypt, HKDF, ANSI X9.42 KDF, ANSI X9.63 KDF and TLS1 PRF KDF by way of EVP_KDF.
+From OpenSSL 3.0 the recommended way of performing key derivation is to use the EVP_KDF functions. If compatibility with OpenSSL 1.1.1 is required then a limited set of KDFs can be used via EVP_PKEY_derive.
+]]
+
+local NID_id_pbkdf2 = -1
+local NID_id_scrypt = -2
+local NID_tls1_prf = -3
+local NID_hkdf = -4
+if version_num >= 0x10002000 then
+ NID_id_pbkdf2 = C.OBJ_txt2nid("PBKDF2")
+ assert(NID_id_pbkdf2 > 0)
+end
+if version_num >= 0x10100000 and not BORINGSSL then
+ NID_hkdf = C.OBJ_txt2nid("HKDF")
+ assert(NID_hkdf > 0)
+ NID_tls1_prf = C.OBJ_txt2nid("TLS1-PRF")
+ assert(NID_tls1_prf > 0)
+ -- we use EVP_PBE_scrypt to do scrypt, so this is supported >= 1.1.0
+ NID_id_scrypt = C.OBJ_txt2nid("id-scrypt")
+ assert(NID_id_scrypt > 0)
+end
+
+local _M = {
+ HKDEF_MODE_EXTRACT_AND_EXPAND = kdf_macro.EVP_PKEY_HKDEF_MODE_EXTRACT_AND_EXPAND,
+ HKDEF_MODE_EXTRACT_ONLY = kdf_macro.EVP_PKEY_HKDEF_MODE_EXTRACT_ONLY,
+ HKDEF_MODE_EXPAND_ONLY = kdf_macro.EVP_PKEY_HKDEF_MODE_EXPAND_ONLY,
+
+ PBKDF2 = NID_id_pbkdf2,
+ SCRYPT = NID_id_scrypt,
+ TLS1_PRF = NID_tls1_prf,
+ HKDF = NID_hkdf,
+}
+
+local type_literals = {
+ [NID_id_pbkdf2] = "PBKDF2",
+ [NID_id_scrypt] = "scrypt",
+ [NID_tls1_prf] = "TLS-1PRF",
+ [NID_hkdf] = "HKDF",
+}
+
+local TYPE_NUMBER = 0x1
+local TYPE_STRING = 0x2
+
+local function check_options(opt, nid, field, typ, is_optional, required_only_if_nid)
+ local v = opt[field]
+ if not v then
+ if is_optional or (required_only_if_nid and required_only_if_nid ~= nid) then
+ return typ == TYPE_NUMBER and 0 or nil
+ else
+ return nil, "\"" .. field .. "\" must be set"
+ end
+ end
+
+ if typ == TYPE_NUMBER then
+ v = tonumber(v)
+ if not typ then
+ return nil, "except a number as \"" .. field .. "\""
+ end
+ elseif typ == TYPE_STRING then
+ if type(v) ~= "string" then
+ return nil, "except a string as \"" .. field .. "\""
+ end
+ else
+ error("don't known how to check " .. typ, 2)
+ end
+
+ return v
+end
+
+local function check_hkdf_options(opt)
+ local mode = opt.hkdf_mode
+ if not mode or version_num < 0x10101000 then
+ mode = _M.HKDEF_MODE_EXTRACT_AND_EXPAND
+ end
+
+ if mode == _M.HKDEF_MODE_EXTRACT_AND_EXPAND and (
+ not opt.salt or not opt.hkdf_info) then
+ return '""salt" and "hkdf_info" are required for EXTRACT_AND_EXPAND mode'
+ elseif mode == _M.HKDEF_MODE_EXTRACT_ONLY and not opt.salt then
+ return '"salt" is required for EXTRACT_ONLY mode'
+ elseif mode == _M.EVP_PKEY_HKDEF_MODE_EXPAND_ONLY and not opt.hkdf_info then
+ return '"hkdf_info" is required for EXPAND_ONLY mode'
+ end
+
+ return nil
+end
+
+local options_schema = {
+ outlen = { TYPE_NUMBER },
+ pass = { TYPE_STRING, true },
+ salt = { TYPE_STRING, true },
+ md = { TYPE_STRING, true },
+ -- pbkdf2 only
+ pbkdf2_iter = { TYPE_NUMBER, true },
+ -- hkdf only
+ hkdf_key = { TYPE_STRING, nil, NID_hkdf },
+ hkdf_mode = { TYPE_NUMBER, true },
+ hkdf_info = { TYPE_STRING, true },
+ -- tls1-prf
+ tls1_prf_secret = { TYPE_STRING, nil, NID_tls1_prf },
+ tls1_prf_seed = { TYPE_STRING, nil, NID_tls1_prf },
+ -- scrypt only
+ scrypt_maxmem = { TYPE_NUMBER, true },
+ scrypt_N = { TYPE_NUMBER, nil, NID_id_scrypt },
+ scrypt_r = { TYPE_NUMBER, nil, NID_id_scrypt },
+ scrypt_p = { TYPE_NUMBER, nil, NID_id_scrypt },
+}
+
+local outlen = ctypes.ptr_of_uint64()
+
+function _M.derive(options)
+ local typ = options.type
+ if not typ then
+ return nil, "kdf.derive: \"type\" must be set"
+ elseif type(typ) ~= "number" then
+ return nil, "kdf.derive: expect a number as \"type\""
+ end
+
+ if typ <= 0 then
+ return nil, "kdf.derive: kdf type " .. (type_literals[typ] or tostring(typ)) ..
+ " not supported in " .. version_text
+ end
+
+ for k, v in pairs(options_schema) do
+ local v, err = check_options(options, typ, k, unpack(v))
+ if err then
+ return nil, "kdf.derive: " .. err
+ end
+ options[k] = v
+ end
+
+ if typ == NID_hkdf then
+ local err = check_hkdf_options(options)
+ if err then
+ return nil, "kdf.derive: " .. err
+ end
+ end
+
+ local salt_len = 0
+ if options.salt then
+ salt_len = #options.salt
+ end
+ local pass_len = 0
+ if options.pass then
+ pass_len = #options.pass
+ end
+
+ local md
+ if OPENSSL_3X then
+ md = C.EVP_MD_fetch(ctx_lib.get_libctx(), options.md or 'sha1', options.properties)
+ else
+ md = C.EVP_get_digestbyname(options.md or 'sha1')
+ end
+ if md == nil then
+ return nil, string.format("kdf.derive: invalid digest type \"%s\"", md)
+ end
+
+ local buf = ctypes.uchar_array(options.outlen)
+
+ -- begin legacay low level routines
+ local code
+ if typ == NID_id_pbkdf2 then
+ -- make openssl 1.0.2 happy
+ if version_num < 0x10100000 and not options.pass then
+ options.pass = ""
+ pass_len = 0
+ end
+ -- https://www.openssl.org/docs/man1.1.0/man3/PKCS5_PBKDF2_HMAC.html
+ local iter = options.pbkdf2_iter
+ if iter < 1 then
+ iter = 1
+ end
+ code = C.PKCS5_PBKDF2_HMAC(
+ options.pass, pass_len,
+ options.salt, salt_len, iter,
+ md, options.outlen, buf
+ )
+ elseif typ == NID_id_scrypt then
+ code = C.EVP_PBE_scrypt(
+ options.pass, pass_len,
+ options.salt, salt_len,
+ options.scrypt_N, options.scrypt_r, options.scrypt_p, options.scrypt_maxmem,
+ buf, options.outlen
+ )
+ elseif typ ~= NID_tls1_prf and typ ~= NID_hkdf then
+ return nil, string.format("kdf.derive: unknown type %d", typ)
+ end
+ if code then
+ if code ~= 1 then
+ return nil, format_error("kdf.derive")
+ else
+ return ffi_str(buf, options.outlen)
+ end
+ end
+ -- end legacay low level routines
+
+ -- begin EVP_PKEY_derive routines
+ outlen[0] = options.outlen
+
+ local ctx = C.EVP_PKEY_CTX_new_id(typ, nil)
+ if ctx == nil then
+ return nil, format_error("kdf.derive: EVP_PKEY_CTX_new_id")
+ end
+ ffi_gc(ctx, C.EVP_PKEY_CTX_free)
+ if C.EVP_PKEY_derive_init(ctx) ~= 1 then
+ return nil, format_error("kdf.derive: EVP_PKEY_derive_init")
+ end
+
+ if typ == NID_tls1_prf then
+ if kdf_macro.EVP_PKEY_CTX_set_tls1_prf_md(ctx, md) ~= 1 then
+ return nil, format_error("kdf.derive: EVP_PKEY_CTX_set_tls1_prf_md")
+ end
+ if kdf_macro.EVP_PKEY_CTX_set1_tls1_prf_secret(ctx, options.tls1_prf_secret) ~= 1 then
+ return nil, format_error("kdf.derive: EVP_PKEY_CTX_set1_tls1_prf_secret")
+ end
+ if kdf_macro.EVP_PKEY_CTX_add1_tls1_prf_seed(ctx, options.tls1_prf_seed) ~= 1 then
+ return nil, format_error("kdf.derive: EVP_PKEY_CTX_add1_tls1_prf_seed")
+ end
+ elseif typ == NID_hkdf then
+ if kdf_macro.EVP_PKEY_CTX_set_hkdf_md(ctx, md) ~= 1 then
+ return nil, format_error("kdf.derive: EVP_PKEY_CTX_set_hkdf_md")
+ end
+ if options.salt and
+ kdf_macro.EVP_PKEY_CTX_set1_hkdf_salt(ctx, options.salt) ~= 1 then
+ return nil, format_error("kdf.derive: EVP_PKEY_CTX_set1_hkdf_salt")
+ end
+ if options.hkdf_key and
+ kdf_macro.EVP_PKEY_CTX_set1_hkdf_key(ctx, options.hkdf_key) ~= 1 then
+ return nil, format_error("kdf.derive: EVP_PKEY_CTX_set1_hkdf_key")
+ end
+ if options.hkdf_info and
+ kdf_macro.EVP_PKEY_CTX_add1_hkdf_info(ctx, options.hkdf_info) ~= 1 then
+ return nil, format_error("kdf.derive: EVP_PKEY_CTX_add1_hkdf_info")
+ end
+ if options.hkdf_mode then
+ if version_num >= 0x10101000 then
+ if kdf_macro.EVP_PKEY_CTX_set_hkdf_mode(ctx, options.hkdf_mode) ~= 1 then
+ return nil, format_error("kdf.derive: EVP_PKEY_CTX_set_hkdf_mode")
+ end
+ if options.hkdf_mode == _M.HKDEF_MODE_EXTRACT_ONLY then
+ local md_size = OPENSSL_3X and C.EVP_MD_get_size(md) or C.EVP_MD_size(md)
+ if options.outlen ~= md_size then
+ options.outlen = md_size
+ ngx.log(ngx.WARN, "hkdf_mode EXTRACT_ONLY outputs fixed length of ", md_size,
+ " key, ignoring options.outlen")
+ end
+ outlen[0] = md_size
+ buf = ctypes.uchar_array(md_size)
+ end
+ else
+ ngx.log(ngx.WARN, "hkdf_mode is not effective in ", version_text)
+ end
+ end
+ else
+ return nil, string.format("kdf.derive: unknown type %d", typ)
+ end
+ code = C.EVP_PKEY_derive(ctx, buf, outlen)
+ if code == -2 then
+ return nil, "kdf.derive: operation is not supported by the public key algorithm"
+ end
+ -- end EVP_PKEY_derive routines
+
+ return ffi_str(buf, options.outlen)
+end
+
+if not OPENSSL_3X then
+ return _M
+end
+
+_M.derive_legacy = _M.derive
+_M.derive = nil
+
+-- OPENSSL 3.0 style API
+local param_lib = require "resty.openssl.param"
+local SIZE_MAX = ctypes.SIZE_MAX
+
+local mt = {__index = _M}
+
+local kdf_ctx_ptr_ct = ffi.typeof('EVP_KDF_CTX*')
+
+function _M.new(typ, properties)
+ local algo = C.EVP_KDF_fetch(ctx_lib.get_libctx(), typ, properties)
+ if algo == nil then
+ return nil, format_error(string.format("mac.new: invalid mac type \"%s\"", typ))
+ end
+
+ local ctx = C.EVP_KDF_CTX_new(algo)
+ if ctx == nil then
+ return nil, "mac.new: failed to create EVP_MAC_CTX"
+ end
+ ffi_gc(ctx, C.EVP_KDF_CTX_free)
+
+ local buf
+ local buf_size = tonumber(C.EVP_KDF_CTX_get_kdf_size(ctx))
+ if buf_size == SIZE_MAX then -- no fixed size
+ buf_size = nil
+ else
+ buf = ctypes.uchar_array(buf_size)
+ end
+
+ return setmetatable({
+ ctx = ctx,
+ algo = algo,
+ buf = buf,
+ buf_size = buf_size,
+ }, mt), nil
+end
+
+function _M.istype(l)
+ return l and l.ctx and ffi.istype(kdf_ctx_ptr_ct, l.ctx)
+end
+
+function _M:get_provider_name()
+ local p = C.EVP_KDF_get0_provider(self.algo)
+ if p == nil then
+ return nil
+ end
+ return ffi_str(C.OSSL_PROVIDER_get0_name(p))
+end
+
+_M.settable_params, _M.set_params, _M.gettable_params, _M.get_param = param_lib.get_params_func("EVP_KDF_CTX")
+
+function _M:derive(outlen, options, options_count)
+ if not _M.istype(self) then
+ return _M.derive_legacy(self)
+ end
+
+ if self.buf_size and outlen then
+ return nil, string.format("kdf:derive: this KDF has fixed output size %d, "..
+ "it can't be set manually", self.buf_size)
+ end
+
+ outlen = self.buf_size or outlen
+ local buf = self.buf or ctypes.uchar_array(outlen)
+
+ if options_count then
+ options_count = options_count - 1
+ else
+ options_count = 0
+ for k, v in pairs(options) do options_count = options_count + 1 end
+ end
+
+ local param, err
+ if options_count > 0 then
+ local schema = self:settable_params(true) -- raw schema
+ param, err = param_lib.construct(options, nil, schema)
+ if err then
+ return nil, "kdf:derive: " .. err
+ end
+ end
+
+ if C.EVP_KDF_derive(self.ctx, buf, outlen, param) ~= 1 then
+ return nil, format_error("kdf:derive")
+ end
+
+ return ffi_str(buf, outlen)
+end
+
+function _M:reset()
+ C.EVP_KDF_CTX_reset(self.ctx)
+ return true
+end
+
+return _M \ No newline at end of file
diff --git a/server/resty/openssl/mac.lua b/server/resty/openssl/mac.lua
new file mode 100644
index 0000000..65f5e38
--- /dev/null
+++ b/server/resty/openssl/mac.lua
@@ -0,0 +1,96 @@
+local ffi = require "ffi"
+local C = ffi.C
+local ffi_gc = ffi.gc
+local ffi_str = ffi.string
+
+require "resty.openssl.include.evp.mac"
+local param_lib = require "resty.openssl.param"
+local ctx_lib = require "resty.openssl.ctx"
+local ctypes = require "resty.openssl.auxiliary.ctypes"
+local format_error = require("resty.openssl.err").format_error
+local OPENSSL_3X = require("resty.openssl.version").OPENSSL_3X
+
+local _M = {}
+local mt = {__index = _M}
+
+local mac_ctx_ptr_ct = ffi.typeof('EVP_MAC_CTX*')
+local param_types = {
+ cipher = param_lib.OSSL_PARAM_UTF8_STRING,
+ digest = param_lib.OSSL_PARAM_UTF8_STRING,
+}
+local params = {}
+
+function _M.new(key, typ, cipher, digest, properties)
+ if not OPENSSL_3X then
+ return false, "EVP_MAC is only supported from OpenSSL 3.0"
+ end
+
+ local algo = C.EVP_MAC_fetch(ctx_lib.get_libctx(), typ, properties)
+ if algo == nil then
+ return nil, format_error(string.format("mac.new: invalid mac type \"%s\"", typ))
+ end
+
+ local ctx = C.EVP_MAC_CTX_new(algo)
+ if ctx == nil then
+ return nil, "mac.new: failed to create EVP_MAC_CTX"
+ end
+ ffi_gc(ctx, C.EVP_MAC_CTX_free)
+
+ params.digest = digest
+ params.cipher = cipher
+ local p = param_lib.construct(params, 2, param_types)
+
+ local code = C.EVP_MAC_init(ctx, key, #key, p)
+ if code ~= 1 then
+ return nil, format_error(string.format("mac.new: invalid cipher or digest type"))
+ end
+
+ local md_size = C.EVP_MAC_CTX_get_mac_size(ctx)
+
+ return setmetatable({
+ ctx = ctx,
+ algo = algo,
+ buf = ctypes.uchar_array(md_size),
+ buf_size = md_size,
+ }, mt), nil
+end
+
+function _M.istype(l)
+ return l and l.ctx and ffi.istype(mac_ctx_ptr_ct, l.ctx)
+end
+
+function _M:get_provider_name()
+ local p = C.EVP_MAC_get0_provider(self.algo)
+ if p == nil then
+ return nil
+ end
+ return ffi_str(C.OSSL_PROVIDER_get0_name(p))
+end
+
+_M.settable_params, _M.set_params, _M.gettable_params, _M.get_param = param_lib.get_params_func("EVP_MAC_CTX")
+
+function _M:update(...)
+ for _, s in ipairs({...}) do
+ if C.EVP_MAC_update(self.ctx, s, #s) ~= 1 then
+ return false, format_error("digest:update")
+ end
+ end
+ return true, nil
+end
+
+function _M:final(s)
+ if s then
+ local _, err = self:update(s)
+ if err then
+ return nil, err
+ end
+ end
+
+ local length = ctypes.ptr_of_size_t()
+ if C.EVP_MAC_final(self.ctx, self.buf, length, self.buf_size) ~= 1 then
+ return nil, format_error("digest:final: EVP_MAC_final")
+ end
+ return ffi_str(self.buf, length[0])
+end
+
+return _M
diff --git a/server/resty/openssl/objects.lua b/server/resty/openssl/objects.lua
new file mode 100644
index 0000000..bd02a38
--- /dev/null
+++ b/server/resty/openssl/objects.lua
@@ -0,0 +1,74 @@
+local ffi = require "ffi"
+local C = ffi.C
+local ffi_str = ffi.string
+local ffi_sizeof = ffi.sizeof
+
+require "resty.openssl.include.objects"
+require "resty.openssl.include.err"
+
+local buf = ffi.new('char[?]', 100)
+
+local function obj2table(obj)
+ local nid = C.OBJ_obj2nid(obj)
+
+ local len = C.OBJ_obj2txt(buf, ffi_sizeof(buf), obj, 1)
+ local oid = ffi_str(buf, len)
+
+ return {
+ id = oid,
+ nid = nid,
+ sn = ffi_str(C.OBJ_nid2sn(nid)),
+ ln = ffi_str(C.OBJ_nid2ln(nid)),
+ }
+end
+
+local function nid2table(nid)
+ return obj2table(C.OBJ_nid2obj(nid))
+end
+
+local function txt2nid(txt)
+ if type(txt) ~= "string" then
+ return nil, "objects.txt2nid: expect a string at #1"
+ end
+ local nid = C.OBJ_txt2nid(txt)
+ if nid == 0 then
+ -- clean up error occurs during OBJ_txt2nid
+ C.ERR_clear_error()
+ return nil, "objects.txt2nid: invalid NID text " .. txt
+ end
+ return nid
+end
+
+local function txtnid2nid(txt_nid)
+ local nid
+ if type(txt_nid) == "string" then
+ nid = C.OBJ_txt2nid(txt_nid)
+ if nid == 0 then
+ -- clean up error occurs during OBJ_txt2nid
+ C.ERR_clear_error()
+ return nil, "objects.txtnid2nid: invalid NID text " .. txt_nid
+ end
+ elseif type(txt_nid) == "number" then
+ nid = txt_nid
+ else
+ return nil, "objects.txtnid2nid: expect string or number at #1"
+ end
+ return nid
+end
+
+local function find_sigid_algs(nid)
+ local out = ffi.new("int[0]")
+ if C.OBJ_find_sigid_algs(nid, out, nil) == 0 then
+ return 0, "objects.find_sigid_algs: invalid sigid " .. nid
+ end
+ return tonumber(out[0])
+end
+
+return {
+ obj2table = obj2table,
+ nid2table = nid2table,
+ txt2nid = txt2nid,
+ txtnid2nid = txtnid2nid,
+ find_sigid_algs = find_sigid_algs,
+ create = C.OBJ_create,
+} \ No newline at end of file
diff --git a/server/resty/openssl/param.lua b/server/resty/openssl/param.lua
new file mode 100644
index 0000000..2c8dcea
--- /dev/null
+++ b/server/resty/openssl/param.lua
@@ -0,0 +1,322 @@
+local ffi = require "ffi"
+local C = ffi.C
+local ffi_new = ffi.new
+local ffi_str = ffi.string
+local ffi_cast = ffi.cast
+
+require "resty.openssl.include.param"
+local format_error = require("resty.openssl.err").format_error
+local bn_lib = require("resty.openssl.bn")
+local null = require("resty.openssl.auxiliary.ctypes").null
+
+local OSSL_PARAM_INTEGER = 1
+local OSSL_PARAM_UNSIGNED_INTEGER = 2
+local OSSL_PARAM_REAL = 3
+local OSSL_PARAM_UTF8_STRING = 4
+local OSSL_PARAM_OCTET_STRING = 5
+local OSSL_PARAM_UTF8_PTR = 6
+local OSSL_PARAM_OCTET_PTR = 7
+
+local alter_type_key = {}
+local buf_param_key = {}
+
+local function construct(buf_t, length, types_map, types_size)
+ if not length then
+ length = 0
+ for k, v in pairs(buf_t) do length = length + 1 end
+ end
+
+ local params = ffi_new("OSSL_PARAM[?]", length + 1)
+
+ local i = 0
+ local buf_param
+ for key, value in pairs(buf_t) do
+ local typ = types_map[key]
+ if not typ then
+ return nil, "param:construct: unknown key \"" .. key .. "\""
+ end
+ local param, buf, size
+ if value == null then -- out
+ value = nil
+ size = types_size and types_size[key] or 100
+ if typ == OSSL_PARAM_UTF8_STRING or typ == OSSL_PARAM_OCTET_STRING then
+ buf = ffi_new("char[?]", size)
+ end
+ else
+ local numeric = type(value) == "number"
+ if (numeric and typ >= OSSL_PARAM_UTF8_STRING) or
+ (not numeric and typ <= OSSL_PARAM_UNSIGNED_INTEGER) then
+ local alter_typ = types_map[alter_type_key] and types_map[alter_type_key][key]
+ if alter_typ and ((numeric and alter_typ <= OSSL_PARAM_UNSIGNED_INTEGER) or
+ (not numeric and alter_typ >= OSSL_PARAM_UTF8_STRING)) then
+ typ = alter_typ
+ else
+ return nil, "param:construct: key \"" .. key .. "\" can't be a " .. type(value)
+ end
+ end
+ end
+
+ if typ == "bn" then -- out only
+ buf = ffi_new("char[?]", size)
+ param = C.OSSL_PARAM_construct_BN(key, buf, size)
+ buf_param = buf_param or {}
+ buf_param[key] = param
+ elseif typ == OSSL_PARAM_INTEGER then
+ buf = value and ffi_new("int[1]", value) or ffi_new("int[1]")
+ param = C.OSSL_PARAM_construct_int(key, buf)
+ elseif typ == OSSL_PARAM_UNSIGNED_INTEGER then
+ buf = value and ffi_new("unsigned int[1]", value) or
+ ffi_new("unsigned int[1]")
+ param = C.OSSL_PARAM_construct_uint(key, buf)
+ elseif typ == OSSL_PARAM_UTF8_STRING then
+ buf = value and ffi_cast("char *", value) or buf
+ param = C.OSSL_PARAM_construct_utf8_string(key, buf, value and #value or size)
+ elseif typ == OSSL_PARAM_OCTET_STRING then
+ buf = value and ffi_cast("char *", value) or buf
+ param = C.OSSL_PARAM_construct_octet_string(key, ffi_cast("void*", buf),
+ value and #value or size)
+ elseif typ == OSSL_PARAM_UTF8_PTR then
+ buf = ffi_new("char*[1]")
+ param = C.OSSL_PARAM_construct_utf8_ptr(key, buf, 0)
+ elseif typ == OSSL_PARAM_OCTET_PTR then
+ buf = ffi_new("char*[1]")
+ param = C.OSSL_PARAM_construct_octet_ptr(key, ffi_cast("void**", buf), 0)
+ else
+ error("type " .. typ .. " is not yet implemented")
+ end
+ if not value then -- out
+ buf_t[key] = buf
+ end
+ params[i] = param
+ i = i + 1
+ end
+
+ buf_t[buf_param_key] = buf_param
+ params[length] = C.OSSL_PARAM_construct_end()
+
+ return params
+end
+
+local function parse(buf_t, length, types_map, types_size)
+ for key, buf in pairs(buf_t) do
+ local typ = types_map[key]
+ local sz = types_size and types_size[key]
+
+ if key == buf_param_key then -- luacheck: ignore
+ -- ignore
+ elseif buf == nil or buf[0] == nil then
+ buf_t[key] = nil
+ elseif typ == "bn" then
+ local bn_t = ffi_new("BIGNUM*[1]")
+ local param = buf_t[buf_param_key][key]
+ if C.OSSL_PARAM_get_BN(param, bn_t) ~= 1 then
+ return nil, format_error("param:parse: OSSL_PARAM_get_BN")
+ end
+ buf_t[key] = bn_lib.dup(bn_t[0])
+ elseif typ == OSSL_PARAM_INTEGER or
+ typ == OSSL_PARAM_UNSIGNED_INTEGER then
+ buf_t[key] = tonumber(buf[0])
+ elseif typ == OSSL_PARAM_UTF8_STRING or
+ typ == OSSL_PARAM_OCTET_STRING then
+ buf_t[key] = sz and ffi_str(buf, sz) or ffi_str(buf)
+ elseif typ == OSSL_PARAM_UTF8_PTR or
+ typ == OSSL_PARAM_OCTET_PTR then
+ buf_t[key] = sz and ffi_str(buf[0], sz) or ffi_str(buf[0])
+ elseif not typ then
+ return nil, "param:parse: unknown key type \"" .. key .. "\""
+ else
+ error("type " .. typ .. " is not yet implemented")
+ end
+ end
+ -- for GC
+ buf_t[buf_param_key] = nil
+
+ return buf_t
+end
+
+local param_type_readable = {
+ [OSSL_PARAM_UNSIGNED_INTEGER] = "unsigned integer",
+ [OSSL_PARAM_INTEGER] = "integer",
+ [OSSL_PARAM_REAL] = "real number",
+ [OSSL_PARAM_UTF8_PTR] = "pointer to a UTF8 encoded string",
+ [OSSL_PARAM_UTF8_STRING] = "UTF8 encoded string",
+ [OSSL_PARAM_OCTET_PTR] = "pointer to an octet string",
+ [OSSL_PARAM_OCTET_STRING] = "octet string",
+}
+
+local function readable_data_type(p)
+ local typ = p.data_type
+ local literal = param_type_readable[typ]
+ if not literal then
+ literal = string.format("unknown type [%d]", typ)
+ end
+
+ local sz = tonumber(p.data_size)
+ if sz == 0 then
+ literal = literal .. " (arbitrary size)"
+ else
+ literal = literal .. string.format(" (max %d bytes large)", sz)
+ end
+ return literal
+end
+
+local function parse_params_schema(params, schema, schema_readable)
+ if params == nil then
+ return nil, format_error("parse_params_schema")
+ end
+
+ local i = 0
+ while true do
+ local p = params[i]
+ if p.key == nil then
+ break
+ end
+ local key = ffi_str(p.key)
+ if schema then
+ -- TODO: don't support same key with different types for now
+ -- prefer string type over integer types
+ local typ = tonumber(p.data_type)
+ if schema[key] then
+ schema[alter_type_key] = schema[alter_type_key] or {}
+ schema[alter_type_key][key] = typ
+ else
+ schema[key] = typ
+ end
+ end
+ -- if schema_return_size then -- only non-ptr string types are needed actually
+ -- schema_return_size[key] = tonumber(p.return_size)
+ -- end
+ if schema_readable then
+ table.insert(schema_readable, { key, readable_data_type(p) })
+ end
+ i = i + 1
+ end
+ return schema
+end
+
+local param_maps_set, param_maps_get = {}, {}
+
+local function get_params_func(typ, field)
+ local typ_lower = typ:sub(5):lower()
+ if typ_lower:sub(-4) == "_ctx" then
+ typ_lower = typ_lower:sub(0, -5)
+ end
+ -- field name for indexing schema, usually the (const) one created by
+ -- EVP_TYP_fetch or EVP_get_typebynam,e
+ field = field or "algo"
+
+ local cf_settable = C[typ .. "_settable_params"]
+ local settable = function(self, raw)
+ local k = self[field]
+ if raw and param_maps_set[k] then
+ return param_maps_set[k]
+ end
+
+ local param = cf_settable(self.ctx)
+ -- no params, this is fine, shouldn't be regarded as an error
+ if param == nil then
+ param_maps_set[k] = {}
+ return {}
+ end
+ local schema, schema_reabale = {}, raw and nil or {}
+ parse_params_schema(param, schema, schema_reabale)
+ param_maps_set[k] = schema
+
+ return raw and schema or schema_reabale
+ end
+
+ local cf_set = C[typ .. "_set_params"]
+ local set = function(self, params)
+ if not param_maps_set[self[field]] then
+ local ok, err = self:settable_params()
+ if not ok then
+ return false, typ_lower .. ":set_params: " .. err
+ end
+ end
+
+ local oparams, err = construct(params, nil, param_maps_set[self[field]])
+ if err then
+ return false, typ_lower .. ":set_params: " .. err
+ end
+
+ if cf_set(self.ctx, oparams) ~= 1 then
+ return false, format_error(typ_lower .. ":set_params: " .. typ .. "_set_params")
+ end
+
+ return true
+ end
+
+ local cf_gettable = C[typ .. "_gettable_params"]
+ local gettable = function(self, raw)
+ local k = self[field]
+ if raw and param_maps_set[k] then
+ return param_maps_set[k]
+ end
+
+ local param = cf_gettable(self.ctx)
+ -- no params, this is fine, shouldn't be regarded as an error
+ if param == nil then
+ param_maps_get[k] = {}
+ return {}
+ end
+ local schema, schema_reabale = {}, raw and nil or {}
+ parse_params_schema(param, schema, schema_reabale)
+ param_maps_set[k] = schema
+
+ return raw and schema or schema_reabale
+ end
+
+ local cf_get = C[typ .. "_get_params"]
+ local get_buffer, get_size_map = {}, {}
+ local get = function(self, key, want_size, want_type)
+ if not param_maps_get[self[field]] then
+ local ok, err = self:gettable_params()
+ if not ok then
+ return false, typ_lower .. ":set_params: " .. err
+ end
+ end
+ local schema = param_maps_set[self[field]]
+ if schema == nil or not schema[key] then -- nil or null
+ return nil, typ_lower .. ":get_param: unknown key \"" .. key .. "\""
+ end
+
+ table.clear(get_buffer)
+ table.clear(get_size_map)
+ get_buffer[key] = null
+ get_size_map[key] = want_size
+ schema = want_type and { [key] = want_type } or schema
+
+ local req, err = construct(get_buffer, 1, schema, get_size_map)
+ if not req then
+ return nil, typ_lower .. ":get_param: failed to construct params: " .. err
+ end
+
+ if cf_get(self.ctx, req) ~= 1 then
+ return nil, format_error(typ_lower .. ":get_param:get")
+ end
+
+ get_buffer, err = parse(get_buffer, 1, schema, get_size_map)
+ if err then
+ return nil, typ_lower .. ":get_param: failed to parse params: " .. err
+ end
+
+ return get_buffer[key]
+ end
+
+ return settable, set, gettable, get
+end
+
+return {
+ OSSL_PARAM_INTEGER = OSSL_PARAM_INTEGER,
+ OSSL_PARAM_UNSIGNED_INTEGER = OSSL_PARAM_INTEGER,
+ OSSL_PARAM_REAL = OSSL_PARAM_REAL,
+ OSSL_PARAM_UTF8_STRING = OSSL_PARAM_UTF8_STRING,
+ OSSL_PARAM_OCTET_STRING = OSSL_PARAM_OCTET_STRING,
+ OSSL_PARAM_UTF8_PTR = OSSL_PARAM_UTF8_PTR,
+ OSSL_PARAM_OCTET_PTR = OSSL_PARAM_OCTET_PTR,
+
+ construct = construct,
+ parse = parse,
+ parse_params_schema = parse_params_schema,
+ get_params_func = get_params_func,
+} \ No newline at end of file
diff --git a/server/resty/openssl/pkcs12.lua b/server/resty/openssl/pkcs12.lua
new file mode 100644
index 0000000..6e3b216
--- /dev/null
+++ b/server/resty/openssl/pkcs12.lua
@@ -0,0 +1,168 @@
+local ffi = require "ffi"
+local C = ffi.C
+local ffi_gc = ffi.gc
+local ffi_str = ffi.string
+
+require "resty.openssl.include.pkcs12"
+require "resty.openssl.include.bio"
+local bio_util = require "resty.openssl.auxiliary.bio"
+local format_error = require("resty.openssl.err").format_error
+local pkey_lib = require "resty.openssl.pkey"
+local x509_lib = require "resty.openssl.x509"
+local stack_macro = require "resty.openssl.include.stack"
+local stack_lib = require "resty.openssl.stack"
+local objects_lib = require "resty.openssl.objects"
+local ctx_lib = require "resty.openssl.ctx"
+local OPENSSL_10 = require("resty.openssl.version").OPENSSL_10
+local OPENSSL_3X = require("resty.openssl.version").OPENSSL_3X
+
+local stack_of_x509_new = stack_lib.new_of("X509")
+local stack_of_x509_add = stack_lib.add_of("X509")
+local stack_of_x509_iter = stack_lib.mt_of("X509", x509_lib.dup, {}).__ipairs
+
+local ptr_ptr_of_pkey = ffi.typeof("EVP_PKEY*[1]")
+local ptr_ptr_of_x509 = ffi.typeof("X509*[1]")
+local ptr_ptr_of_stack = ffi.typeof("OPENSSL_STACK*[1]")
+
+local function decode(p12, passphrase)
+ local bio = C.BIO_new_mem_buf(p12, #p12)
+ if bio == nil then
+ return nil, "pkcs12.decode: BIO_new_mem_buf() failed"
+ end
+ ffi_gc(bio, C.BIO_free)
+
+ local p12 = C.d2i_PKCS12_bio(bio, nil)
+ if p12 == nil then
+ return nil, format_error("pkcs12.decode: d2i_PKCS12_bio")
+ end
+ ffi_gc(p12, C.PKCS12_free)
+
+ local ppkey = ptr_ptr_of_pkey()
+ local px509 = ptr_ptr_of_x509()
+ local pstack = ptr_ptr_of_stack()
+ local stack = stack_of_x509_new()
+ -- assign a valid OPENSSL_STACK so gc is taken care of
+ pstack[0] = stack
+
+ local code = C.PKCS12_parse(p12, passphrase or "", ppkey, px509, pstack)
+ if code ~= 1 then
+ return nil, format_error("pkcs12.decode: PKCS12_parse")
+ end
+
+ local cacerts
+ local n = stack_macro.OPENSSL_sk_num(stack)
+ if n > 0 then
+ cacerts = {}
+ local iter = stack_of_x509_iter({ ctx = stack })
+ for i=1, n do
+ local _, c = iter()
+ cacerts[i] = c
+ end
+ end
+
+ local friendly_name = C.X509_alias_get0(px509[0], nil)
+ if friendly_name ~= nil then
+ friendly_name = ffi_str(friendly_name)
+ end
+
+ return {
+ key = pkey_lib.new(ppkey[0]),
+ cert = x509_lib.new(px509[0]),
+ friendly_name = friendly_name,
+ cacerts = cacerts,
+ -- store reference to the stack, so it's not GC'ed unexpectedly
+ _stack = stack,
+ }
+end
+
+local function encode(opts, passphrase, properties)
+ if passphrase and type(passphrase) ~= "string" then
+ return nil, "pkcs12.encode: expect passphrase to be a string"
+ end
+ local pkey = opts.key
+ if not pkey_lib.istype(pkey) then
+ return nil, "pkcs12.encode: expect key to be a pkey instance"
+ end
+ local cert = opts.cert
+ if not x509_lib.istype(cert) then
+ return nil, "pkcs12.encode: expect cert to be a x509 instance"
+ end
+
+ local ok, err = cert:check_private_key(pkey)
+ if not ok then
+ return nil, "pkcs12.encode: key doesn't match cert: " .. err
+ end
+
+ local nid_key = opts.nid_key
+ if nid_key then
+ nid_key, err = objects_lib.txtnid2nid(nid_key)
+ if err then
+ return nil, "pkcs12.encode: invalid nid_key"
+ end
+ end
+
+ local nid_cert = opts.nid_cert
+ if nid_cert then
+ nid_cert, err = objects_lib.txtnid2nid(nid_cert)
+ if err then
+ return nil, "pkcs12.encode: invalid nid_cert"
+ end
+ end
+
+ local x509stack
+ local cacerts = opts.cacerts
+ if cacerts then
+ if type(cacerts) ~= "table" then
+ return nil, "pkcs12.encode: expect cacerts to be a table"
+ end
+ if #cacerts > 0 then
+ -- stack lib handles gc
+ x509stack = stack_of_x509_new()
+ for _, c in ipairs(cacerts) do
+ if not OPENSSL_10 then
+ if C.X509_up_ref(c.ctx) ~= 1 then
+ return nil, "pkcs12.encode: failed to add cacerts: X509_up_ref failed"
+ end
+ end
+ local ok, err = stack_of_x509_add(x509stack, c.ctx)
+ if not ok then
+ return nil, "pkcs12.encode: failed to add cacerts: " .. err
+ end
+ end
+ if OPENSSL_10 then
+ -- OpenSSL 1.0.2 doesn't have X509_up_ref
+ -- shallow copy the stack, up_ref for each element
+ x509stack = C.X509_chain_up_ref(x509stack)
+ -- use the shallow gc
+ ffi_gc(x509stack, stack_macro.OPENSSL_sk_free)
+ end
+ end
+ end
+
+ local p12
+ if OPENSSL_3X then
+ p12 = C.PKCS12_create_ex(passphrase or "", opts.friendly_name,
+ pkey.ctx, cert.ctx, x509stack,
+ nid_key or 0, nid_cert or 0,
+ opts.iter or 0, opts.mac_iter or 0, 0,
+ ctx_lib.get_libctx(), properties)
+ else
+ p12 = C.PKCS12_create(passphrase or "", opts.friendly_name,
+ pkey.ctx, cert.ctx, x509stack,
+ nid_key or 0, nid_cert or 0,
+ opts.iter or 0, opts.mac_iter or 0, 0)
+ end
+ if p12 == nil then
+ return nil, format_error("pkcs12.encode: PKCS12_create")
+ end
+ ffi_gc(p12, C.PKCS12_free)
+
+ return bio_util.read_wrap(C.i2d_PKCS12_bio, p12)
+end
+
+return {
+ decode = decode,
+ loads = decode,
+ encode = encode,
+ dumps = encode,
+} \ No newline at end of file
diff --git a/server/resty/openssl/pkey.lua b/server/resty/openssl/pkey.lua
new file mode 100644
index 0000000..69c5aae
--- /dev/null
+++ b/server/resty/openssl/pkey.lua
@@ -0,0 +1,942 @@
+local ffi = require "ffi"
+local C = ffi.C
+local ffi_gc = ffi.gc
+local ffi_new = ffi.new
+local ffi_str = ffi.string
+local ffi_cast = ffi.cast
+local ffi_copy = ffi.copy
+
+local rsa_macro = require "resty.openssl.include.rsa"
+local dh_macro = require "resty.openssl.include.dh"
+require "resty.openssl.include.bio"
+require "resty.openssl.include.pem"
+require "resty.openssl.include.x509"
+require "resty.openssl.include.evp.pkey"
+local evp_macro = require "resty.openssl.include.evp"
+local pkey_macro = require "resty.openssl.include.evp.pkey"
+local bio_util = require "resty.openssl.auxiliary.bio"
+local digest_lib = require "resty.openssl.digest"
+local rsa_lib = require "resty.openssl.rsa"
+local dh_lib = require "resty.openssl.dh"
+local ec_lib = require "resty.openssl.ec"
+local ecx_lib = require "resty.openssl.ecx"
+local objects_lib = require "resty.openssl.objects"
+local jwk_lib = require "resty.openssl.auxiliary.jwk"
+local ctx_lib = require "resty.openssl.ctx"
+local ctypes = require "resty.openssl.auxiliary.ctypes"
+local format_error = require("resty.openssl.err").format_error
+
+local OPENSSL_11_OR_LATER = require("resty.openssl.version").OPENSSL_11_OR_LATER
+local OPENSSL_111_OR_LATER = require("resty.openssl.version").OPENSSL_111_OR_LATER
+local OPENSSL_3X = require("resty.openssl.version").OPENSSL_3X
+local BORINGSSL = require("resty.openssl.version").BORINGSSL
+
+local ptr_of_uint = ctypes.ptr_of_uint
+local ptr_of_size_t = ctypes.ptr_of_size_t
+local ptr_of_int = ctypes.ptr_of_int
+
+local null = ctypes.null
+local load_pem_args = { null, null, null }
+local load_der_args = { null }
+
+local get_pkey_key
+if OPENSSL_11_OR_LATER then
+ get_pkey_key = {
+ [evp_macro.EVP_PKEY_RSA] = function(ctx) return C.EVP_PKEY_get0_RSA(ctx) end,
+ [evp_macro.EVP_PKEY_EC] = function(ctx) return C.EVP_PKEY_get0_EC_KEY(ctx) end,
+ [evp_macro.EVP_PKEY_DH] = function(ctx) return C.EVP_PKEY_get0_DH(ctx) end
+ }
+else
+ get_pkey_key = {
+ [evp_macro.EVP_PKEY_RSA] = function(ctx) return ctx.pkey and ctx.pkey.rsa end,
+ [evp_macro.EVP_PKEY_EC] = function(ctx) return ctx.pkey and ctx.pkey.ec end,
+ [evp_macro.EVP_PKEY_DH] = function(ctx) return ctx.pkey and ctx.pkey.dh end,
+ }
+end
+
+local load_rsa_key_funcs
+
+if not OPENSSL_3X then
+ load_rsa_key_funcs= {
+ ['PEM_read_bio_RSAPrivateKey'] = true,
+ ['PEM_read_bio_RSAPublicKey'] = true,
+ } -- those functions return RSA* instead of EVP_PKEY*
+end
+
+local function load_pem_der(txt, opts, funcs)
+ local fmt = opts.format or '*'
+ if fmt ~= 'PEM' and fmt ~= 'DER' and fmt ~= "JWK" and fmt ~= '*' then
+ return nil, "expecting 'DER', 'PEM', 'JWK' or '*' as \"format\""
+ end
+
+ local typ = opts.type or '*'
+ if typ ~= 'pu' and typ ~= 'pr' and typ ~= '*' then
+ return nil, "expecting 'pr', 'pu' or '*' as \"type\""
+ end
+
+ if fmt == "JWK" and (typ == "pu" or type == "pr") then
+ return nil, "explictly load private or public key from JWK format is not supported"
+ end
+
+ ngx.log(ngx.DEBUG, "load key using fmt: ", fmt, ", type: ", typ)
+
+ local bio = C.BIO_new_mem_buf(txt, #txt)
+ if bio == nil then
+ return nil, "BIO_new_mem_buf() failed"
+ end
+ ffi_gc(bio, C.BIO_free)
+
+ local ctx
+
+ local fs = funcs[fmt][typ]
+ local passphrase_cb
+ for f, arg in pairs(fs) do
+ -- don't need BIO when loading JWK key: we parse it in Lua land
+ if f == "load_jwk" then
+ local err
+ ctx, err = jwk_lib[f](txt)
+ if ctx == nil then
+ -- if fmt is explictly set to JWK, we should return an error now
+ if fmt == "JWK" then
+ return nil, err
+ end
+ ngx.log(ngx.DEBUG, "jwk decode failed: ", err, ", continuing")
+ end
+ else
+ -- #define BIO_CTRL_RESET 1
+ local code = C.BIO_ctrl(bio, 1, 0, nil)
+ if code ~= 1 then
+ return nil, "BIO_ctrl() failed"
+ end
+
+ -- only pass in passphrase/passphrase_cb to PEM_* functions
+ if fmt == "PEM" or (fmt == "*" and arg == load_pem_args) then
+ if opts.passphrase then
+ local passphrase = opts.passphrase
+ if type(passphrase) ~= "string" then
+ -- clear errors occur when trying
+ C.ERR_clear_error()
+ return nil, "passphrase must be a string"
+ end
+ arg = { null, nil, passphrase }
+ elseif opts.passphrase_cb then
+ passphrase_cb = passphrase_cb or ffi_cast("pem_password_cb", function(buf, size)
+ local p = opts.passphrase_cb()
+ local len = #p -- 1 byte for \0
+ if len > size then
+ ngx.log(ngx.WARN, "pkey:load_pem_der: passphrase truncated from ", len, " to ", size)
+ len = size
+ end
+ ffi_copy(buf, p, len)
+ return len
+ end)
+ arg = { null, passphrase_cb, null }
+ end
+ end
+
+ ctx = C[f](bio, unpack(arg))
+ end
+
+ if ctx ~= nil then
+ ngx.log(ngx.DEBUG, "pkey:load_pem_der: loaded pkey using function ", f)
+
+ -- pkcs1 functions create a rsa rather than evp_pkey
+ -- disable the checking in openssl 3.0 for sail safe
+ if not OPENSSL_3X and load_rsa_key_funcs[f] then
+ local rsa = ctx
+ ctx = C.EVP_PKEY_new()
+ if ctx == null then
+ return nil, format_error("pkey:load_pem_der: EVP_PKEY_new")
+ end
+
+ if C.EVP_PKEY_assign(ctx, evp_macro.EVP_PKEY_RSA, rsa) ~= 1 then
+ C.RSA_free(rsa)
+ C.EVP_PKEY_free(ctx)
+ return nil, "pkey:load_pem_der: EVP_PKEY_assign() failed"
+ end
+ end
+
+ break
+ end
+ end
+ if passphrase_cb ~= nil then
+ passphrase_cb:free()
+ end
+
+ if ctx == nil then
+ return nil, format_error()
+ end
+ -- clear errors occur when trying
+ C.ERR_clear_error()
+ return ctx, nil
+end
+
+local function generate_param(key_type, config)
+ if key_type == evp_macro.EVP_PKEY_DH then
+ local dh_group = config.group
+ if dh_group then
+ local get_group_func = dh_macro.dh_groups[dh_group]
+ if not get_group_func then
+ return nil, "unknown pre-defined group " .. dh_group
+ end
+ local ctx = get_group_func()
+ if ctx == nil then
+ return nil, format_error("DH_get_x")
+ end
+ local params = C.EVP_PKEY_new()
+ if not params then
+ return nil, format_error("EVP_PKEY_new")
+ end
+ ffi_gc(params, C.EVP_PKEY_free)
+ if C.EVP_PKEY_assign(params, key_type, ctx) ~= 1 then
+ return nil, format_error("EVP_PKEY_assign")
+ end
+ return params
+ end
+ end
+
+ local pctx = C.EVP_PKEY_CTX_new_id(key_type, nil)
+ if pctx == nil then
+ return nil, format_error("EVP_PKEY_CTX_new_id")
+ end
+ ffi_gc(pctx, C.EVP_PKEY_CTX_free)
+
+ if C.EVP_PKEY_paramgen_init(pctx) ~= 1 then
+ return nil, format_error("EVP_PKEY_paramgen_init")
+ end
+
+ if key_type == evp_macro.EVP_PKEY_EC then
+ local curve = config.curve or 'prime192v1'
+ local nid = C.OBJ_ln2nid(curve)
+ if nid == 0 then
+ return nil, "unknown curve " .. curve
+ end
+ if pkey_macro.EVP_PKEY_CTX_set_ec_paramgen_curve_nid(pctx, nid) <= 0 then
+ return nil, format_error("EVP_PKEY_CTX_ctrl: EC: curve_nid")
+ end
+ if not BORINGSSL then
+ -- use the named-curve encoding for best backward-compatibilty
+ -- and for playing well with go:crypto/x509
+ -- # define OPENSSL_EC_NAMED_CURVE 0x001
+ if pkey_macro.EVP_PKEY_CTX_set_ec_param_enc(pctx, 1) <= 0 then
+ return nil, format_error("EVP_PKEY_CTX_ctrl: EC: param_enc")
+ end
+ end
+ elseif key_type == evp_macro.EVP_PKEY_DH then
+ local bits = config.bits
+ if not config.param and not bits then
+ bits = 2048
+ end
+ if bits and pkey_macro.EVP_PKEY_CTX_set_dh_paramgen_prime_len(pctx, bits) <= 0 then
+ return nil, format_error("EVP_PKEY_CTX_ctrl: DH: bits")
+ end
+ end
+
+ local ctx_ptr = ffi_new("EVP_PKEY*[1]")
+ if C.EVP_PKEY_paramgen(pctx, ctx_ptr) ~= 1 then
+ return nil, format_error("EVP_PKEY_paramgen")
+ end
+
+ local params = ctx_ptr[0]
+ ffi_gc(params, C.EVP_PKEY_free)
+
+ return params
+end
+
+local load_param_funcs = {
+ [evp_macro.EVP_PKEY_EC] = {
+ ["*"] = {
+ ["*"] = {
+ ['PEM_read_bio_ECPKParameters'] = load_pem_args,
+ -- ['d2i_ECPKParameters_bio'] = load_der_args,
+ }
+ },
+ },
+ [evp_macro.EVP_PKEY_DH] = {
+ ["*"] = {
+ ["*"] = {
+ ['PEM_read_bio_DHparams'] = load_pem_args,
+ -- ['d2i_DHparams_bio'] = load_der_args,
+ }
+ },
+ },
+}
+
+local function generate_key(config)
+ local typ = config.type or 'RSA'
+ local key_type
+
+ if typ == "RSA" then
+ key_type = evp_macro.EVP_PKEY_RSA
+ elseif typ == "EC" then
+ key_type = evp_macro.EVP_PKEY_EC
+ elseif typ == "DH" then
+ key_type = evp_macro.EVP_PKEY_DH
+ elseif evp_macro.ecx_curves[typ] then
+ key_type = evp_macro.ecx_curves[typ]
+ else
+ return nil, "unsupported type " .. typ
+ end
+ if key_type == 0 then
+ return nil, "the linked OpenSSL library doesn't support " .. typ .. " key"
+ end
+
+ local pctx
+
+ if key_type == evp_macro.EVP_PKEY_EC or key_type == evp_macro.EVP_PKEY_DH then
+ local params, err
+ if config.param then
+ -- HACK
+ config.type = nil
+ local ctx, err = load_pem_der(config.param, config, load_param_funcs[key_type])
+ if err then
+ return nil, "load_pem_der: " .. err
+ end
+ if key_type == evp_macro.EVP_PKEY_EC then
+ local ec_group = ctx
+ ffi_gc(ec_group, C.EC_GROUP_free)
+ ctx = C.EC_KEY_new()
+ if ctx == nil then
+ return nil, "EC_KEY_new() failed"
+ end
+ if C.EC_KEY_set_group(ctx, ec_group) ~= 1 then
+ return nil, format_error("EC_KEY_set_group")
+ end
+ end
+ params = C.EVP_PKEY_new()
+ if not params then
+ return nil, format_error("EVP_PKEY_new")
+ end
+ ffi_gc(params, C.EVP_PKEY_free)
+ if C.EVP_PKEY_assign(params, key_type, ctx) ~= 1 then
+ return nil, format_error("EVP_PKEY_assign")
+ end
+ else
+ params, err = generate_param(key_type, config)
+ if err then
+ return nil, "generate_param: " .. err
+ end
+ end
+ pctx = C.EVP_PKEY_CTX_new(params, nil)
+ if pctx == nil then
+ return nil, format_error("EVP_PKEY_CTX_new")
+ end
+ else
+ pctx = C.EVP_PKEY_CTX_new_id(key_type, nil)
+ if pctx == nil then
+ return nil, format_error("EVP_PKEY_CTX_new_id")
+ end
+ end
+
+ ffi_gc(pctx, C.EVP_PKEY_CTX_free)
+
+ if C.EVP_PKEY_keygen_init(pctx) ~= 1 then
+ return nil, format_error("EVP_PKEY_keygen_init")
+ end
+ -- RSA key parameters are set for keygen ctx not paramgen
+ if key_type == evp_macro.EVP_PKEY_RSA then
+ local bits = config.bits or 2048
+ if bits > 4294967295 then
+ return nil, "bits out of range"
+ end
+
+ if pkey_macro.EVP_PKEY_CTX_set_rsa_keygen_bits(pctx, bits) <= 0 then
+ return nil, format_error("EVP_PKEY_CTX_ctrl: RSA: bits")
+ end
+
+ if config.exp then
+ -- don't free exp as it's used internally in key
+ local exp = C.BN_new()
+ if exp == nil then
+ return nil, "BN_new() failed"
+ end
+ C.BN_set_word(exp, config.exp)
+
+ if pkey_macro.EVP_PKEY_CTX_set_rsa_keygen_pubexp(pctx, exp) <= 0 then
+ return nil, format_error("EVP_PKEY_CTX_ctrl: RSA: exp")
+ end
+ end
+ end
+ local ctx_ptr = ffi_new("EVP_PKEY*[1]")
+ -- TODO: move to use EVP_PKEY_gen after drop support for <1.1.1
+ if C.EVP_PKEY_keygen(pctx, ctx_ptr) ~= 1 then
+ return nil, format_error("EVP_PKEY_gen")
+ end
+ return ctx_ptr[0]
+end
+
+local load_key_try_funcs = {} do
+ -- TODO: pkcs1 load functions are not required in openssl 3.0
+ local _load_key_try_funcs = {
+ PEM = {
+ -- Note: make sure we always try load priv key first
+ pr = {
+ ['PEM_read_bio_PrivateKey'] = load_pem_args,
+ -- disable in openssl3.0, PEM_read_bio_PrivateKey can read pkcs1 in 3.0
+ ['PEM_read_bio_RSAPrivateKey'] = not OPENSSL_3X and load_pem_args or nil,
+ },
+ pu = {
+ ['PEM_read_bio_PUBKEY'] = load_pem_args,
+ -- disable in openssl3.0, PEM_read_bio_PrivateKey can read pkcs1 in 3.0
+ ['PEM_read_bio_RSAPublicKey'] = not OPENSSL_3X and load_pem_args or nil,
+ },
+ },
+ DER = {
+ pr = { ['d2i_PrivateKey_bio'] = load_der_args, },
+ pu = { ['d2i_PUBKEY_bio'] = load_der_args, },
+ },
+ JWK = {
+ pr = { ['load_jwk'] = {}, },
+ }
+ }
+ -- populate * funcs
+ local all_funcs = {}
+ local typ_funcs = {}
+ for fmt, ffs in pairs(_load_key_try_funcs) do
+ load_key_try_funcs[fmt] = ffs
+
+ local funcs = {}
+ for typ, fs in pairs(ffs) do
+ for f, arg in pairs(fs) do
+ funcs[f] = arg
+ all_funcs[f] = arg
+ if not typ_funcs[typ] then
+ typ_funcs[typ] = {}
+ end
+ typ_funcs[typ] = arg
+ end
+ end
+ load_key_try_funcs[fmt]["*"] = funcs
+ end
+ load_key_try_funcs["*"] = {}
+ load_key_try_funcs["*"]["*"] = all_funcs
+ for typ, fs in pairs(typ_funcs) do
+ load_key_try_funcs[typ] = fs
+ end
+end
+
+local function __tostring(self, is_priv, fmt, is_pkcs1)
+ if fmt == "JWK" then
+ return jwk_lib.dump_jwk(self, is_priv)
+ elseif is_pkcs1 then
+ if fmt ~= "PEM" or self.key_type ~= evp_macro.EVP_PKEY_RSA then
+ return nil, "PKCS#1 format is only supported to encode RSA key in \"PEM\" format"
+ elseif OPENSSL_3X then -- maybe possible with OSSL_ENCODER_CTX_new_for_pkey though
+ return nil, "writing out RSA key in PKCS#1 format is not supported in OpenSSL 3.0"
+ end
+ end
+ if is_priv then
+ if fmt == "DER" then
+ return bio_util.read_wrap(C.i2d_PrivateKey_bio, self.ctx)
+ end
+ -- PEM
+ if is_pkcs1 then
+ local rsa = get_pkey_key[evp_macro.EVP_PKEY_RSA](self.ctx)
+ if rsa == nil then
+ return nil, "unable to read RSA key for writing"
+ end
+ return bio_util.read_wrap(C.PEM_write_bio_RSAPrivateKey,
+ rsa,
+ nil, nil, 0, nil, nil)
+ end
+ return bio_util.read_wrap(C.PEM_write_bio_PrivateKey,
+ self.ctx,
+ nil, nil, 0, nil, nil)
+ else
+ if fmt == "DER" then
+ return bio_util.read_wrap(C.i2d_PUBKEY_bio, self.ctx)
+ end
+ -- PEM
+ if is_pkcs1 then
+ local rsa = get_pkey_key[evp_macro.EVP_PKEY_RSA](self.ctx)
+ if rsa == nil then
+ return nil, "unable to read RSA key for writing"
+ end
+ return bio_util.read_wrap(C.PEM_write_bio_RSAPublicKey, rsa)
+ end
+ return bio_util.read_wrap(C.PEM_write_bio_PUBKEY, self.ctx)
+ end
+
+end
+
+local _M = {}
+local mt = { __index = _M, __tostring = __tostring }
+
+local empty_table = {}
+local evp_pkey_ptr_ct = ffi.typeof('EVP_PKEY*')
+
+function _M.new(s, opts)
+ local ctx, err
+ s = s or {}
+ if type(s) == 'table' then
+ ctx, err = generate_key(s)
+ if err then
+ err = "pkey.new:generate_key: " .. err
+ end
+ elseif type(s) == 'string' then
+ ctx, err = load_pem_der(s, opts or empty_table, load_key_try_funcs)
+ if err then
+ err = "pkey.new:load_key: " .. err
+ end
+ elseif type(s) == 'cdata' then
+ if ffi.istype(evp_pkey_ptr_ct, s) then
+ ctx = s
+ else
+ return nil, "pkey.new: expect a EVP_PKEY* cdata at #1"
+ end
+ else
+ return nil, "pkey.new: unexpected type " .. type(s) .. " at #1"
+ end
+
+ if err then
+ return nil, err
+ end
+
+ ffi_gc(ctx, C.EVP_PKEY_free)
+
+ local key_type = OPENSSL_3X and C.EVP_PKEY_get_base_id(ctx) or C.EVP_PKEY_base_id(ctx)
+ if key_type == 0 then
+ return nil, "pkey.new: cannot get key_type"
+ end
+ local key_type_is_ecx = (key_type == evp_macro.EVP_PKEY_ED25519) or
+ (key_type == evp_macro.EVP_PKEY_X25519) or
+ (key_type == evp_macro.EVP_PKEY_ED448) or
+ (key_type == evp_macro.EVP_PKEY_X448)
+
+ -- although OpenSSL discourages to use this size for digest/verify
+ -- but this is good enough for now
+ local buf_size = OPENSSL_3X and C.EVP_PKEY_get_size(ctx) or C.EVP_PKEY_size(ctx)
+
+ local self = setmetatable({
+ ctx = ctx,
+ pkey_ctx = nil,
+ rsa_padding = nil,
+ key_type = key_type,
+ key_type_is_ecx = key_type_is_ecx,
+ buf = ctypes.uchar_array(buf_size),
+ buf_size = buf_size,
+ }, mt)
+
+ return self, nil
+end
+
+function _M.istype(l)
+ return l and l.ctx and ffi.istype(evp_pkey_ptr_ct, l.ctx)
+end
+
+function _M:get_key_type()
+ return objects_lib.nid2table(self.key_type)
+end
+
+function _M:get_default_digest_type()
+ if BORINGSSL then
+ return nil, "BoringSSL doesn't have default digest for pkey"
+ end
+
+ local nid = ptr_of_int()
+ local code = C.EVP_PKEY_get_default_digest_nid(self.ctx, nid)
+ if code == -2 then
+ return nil, "operation is not supported by the public key algorithm"
+ elseif code <= 0 then
+ return nil, format_error("get_default_digest", code)
+ end
+
+ local ret = objects_lib.nid2table(nid[0])
+ ret.mandatory = code == 2
+ return ret
+end
+
+function _M:get_provider_name()
+ if not OPENSSL_3X then
+ return false, "pkey:get_provider_name is not supported"
+ end
+ local p = C.EVP_PKEY_get0_provider(self.ctx)
+ if p == nil then
+ return nil
+ end
+ return ffi_str(C.OSSL_PROVIDER_get0_name(p))
+end
+
+if OPENSSL_3X then
+ local param_lib = require "resty.openssl.param"
+ _M.settable_params, _M.set_params, _M.gettable_params, _M.get_param = param_lib.get_params_func("EVP_PKEY", "key_type")
+end
+
+function _M:get_parameters()
+ if not self.key_type_is_ecx then
+ local getter = get_pkey_key[self.key_type]
+ if not getter then
+ return nil, "key getter not defined"
+ end
+ local key = getter(self.ctx)
+ if key == nil then
+ return nil, format_error("EVP_PKEY_get0_{key}")
+ end
+
+ if self.key_type == evp_macro.EVP_PKEY_RSA then
+ return rsa_lib.get_parameters(key)
+ elseif self.key_type == evp_macro.EVP_PKEY_EC then
+ return ec_lib.get_parameters(key)
+ elseif self.key_type == evp_macro.EVP_PKEY_DH then
+ return dh_lib.get_parameters(key)
+ end
+ else
+ return ecx_lib.get_parameters(self.ctx)
+ end
+end
+
+function _M:set_parameters(opts)
+ if not self.key_type_is_ecx then
+ local getter = get_pkey_key[self.key_type]
+ if not getter then
+ return nil, "key getter not defined"
+ end
+ local key = getter(self.ctx)
+ if key == nil then
+ return nil, format_error("EVP_PKEY_get0_{key}")
+ end
+
+ if self.key_type == evp_macro.EVP_PKEY_RSA then
+ return rsa_lib.set_parameters(key, opts)
+ elseif self.key_type == evp_macro.EVP_PKEY_EC then
+ return ec_lib.set_parameters(key, opts)
+ elseif self.key_type == evp_macro.EVP_PKEY_DH then
+ return dh_lib.set_parameters(key, opts)
+ end
+ else
+ -- for ecx keys we always create a new EVP_PKEY and release the old one
+ local ctx, err = ecx_lib.set_parameters(self.key_type, self.ctx, opts)
+ if err then
+ return false, err
+ end
+ self.ctx = ctx
+ end
+end
+
+function _M:is_private()
+ local params = self:get_parameters()
+ if self.key_type == evp_macro.EVP_PKEY_RSA then
+ return params.d ~= nil
+ else
+ return params.private ~= nil
+ end
+end
+
+local ASYMMETRIC_OP_ENCRYPT = 0x1
+local ASYMMETRIC_OP_DECRYPT = 0x2
+local ASYMMETRIC_OP_SIGN_RAW = 0x4
+local ASYMMETRIC_OP_VERIFY_RECOVER = 0x8
+
+local function asymmetric_routine(self, s, op, padding)
+ local pkey_ctx
+
+ if self.key_type == evp_macro.EVP_PKEY_RSA then
+ if padding then
+ padding = tonumber(padding)
+ if not padding then
+ return nil, "invalid padding: " .. __tostring(padding)
+ end
+ else
+ padding = rsa_macro.paddings.RSA_PKCS1_PADDING
+ end
+ end
+
+ if self.pkey_ctx ~= nil and
+ (self.key_type ~= evp_macro.EVP_PKEY_RSA or self.rsa_padding == padding) then
+ pkey_ctx = self.pkey_ctx
+ else
+ pkey_ctx = C.EVP_PKEY_CTX_new(self.ctx, nil)
+ if pkey_ctx == nil then
+ return nil, format_error("pkey:asymmetric_routine EVP_PKEY_CTX_new()")
+ end
+ ffi_gc(pkey_ctx, C.EVP_PKEY_CTX_free)
+ self.pkey_ctx = pkey_ctx
+ end
+
+ local f, fint, op_name
+ if op == ASYMMETRIC_OP_ENCRYPT then
+ fint = C.EVP_PKEY_encrypt_init
+ f = C.EVP_PKEY_encrypt
+ op_name = "encrypt"
+ elseif op == ASYMMETRIC_OP_DECRYPT then
+ fint = C.EVP_PKEY_decrypt_init
+ f = C.EVP_PKEY_decrypt
+ op_name = "decrypt"
+ elseif op == ASYMMETRIC_OP_SIGN_RAW then
+ fint = C.EVP_PKEY_sign_init
+ f = C.EVP_PKEY_sign
+ op_name = "sign"
+ elseif op == ASYMMETRIC_OP_VERIFY_RECOVER then
+ fint = C.EVP_PKEY_verify_recover_init
+ f = C.EVP_PKEY_verify_recover
+ op_name = "verify_recover"
+ else
+ error("bad \"op\", got " .. op, 2)
+ end
+
+ local code = fint(pkey_ctx)
+ if code < 1 then
+ return nil, format_error("pkey:asymmetric_routine EVP_PKEY_" .. op_name .. "_init", code)
+ end
+
+ -- EVP_PKEY_CTX_ctrl must be called after *_init
+ if self.key_type == evp_macro.EVP_PKEY_RSA and padding then
+ if pkey_macro.EVP_PKEY_CTX_set_rsa_padding(pkey_ctx, padding) ~= 1 then
+ return nil, format_error("pkey:asymmetric_routine EVP_PKEY_CTX_set_rsa_padding")
+ end
+ self.rsa_padding = padding
+ end
+
+ local length = ptr_of_size_t(self.buf_size)
+
+ if f(pkey_ctx, self.buf, length, s, #s) <= 0 then
+ return nil, format_error("pkey:asymmetric_routine EVP_PKEY_" .. op_name)
+ end
+
+ return ffi_str(self.buf, length[0]), nil
+end
+
+_M.PADDINGS = rsa_macro.paddings
+
+function _M:encrypt(s, padding)
+ return asymmetric_routine(self, s, ASYMMETRIC_OP_ENCRYPT, padding)
+end
+
+function _M:decrypt(s, padding)
+ return asymmetric_routine(self, s, ASYMMETRIC_OP_DECRYPT, padding)
+end
+
+function _M:sign_raw(s, padding)
+ -- TODO: temporary hack before OpenSSL has proper check for existence of private key
+ if self.key_type_is_ecx and not self:is_private() then
+ return nil, "pkey:sign_raw: missing private key"
+ end
+
+ return asymmetric_routine(self, s, ASYMMETRIC_OP_SIGN_RAW, padding)
+end
+
+function _M:verify_recover(s, padding)
+ return asymmetric_routine(self, s, ASYMMETRIC_OP_VERIFY_RECOVER, padding)
+end
+
+local evp_pkey_ctx_ptr_ptr_ct = ffi.typeof('EVP_PKEY_CTX*[1]')
+
+local function sign_verify_prepare(self, fint, md_alg, padding, opts)
+ local pkey_ctx
+
+ if self.key_type == evp_macro.EVP_PKEY_RSA and padding then
+ pkey_ctx = C.EVP_PKEY_CTX_new(self.ctx, nil)
+ if pkey_ctx == nil then
+ return nil, format_error("pkey:sign_verify_prepare EVP_PKEY_CTX_new()")
+ end
+ ffi_gc(pkey_ctx, C.EVP_PKEY_CTX_free)
+ end
+
+ local md_ctx = C.EVP_MD_CTX_new()
+ if md_ctx == nil then
+ return nil, "pkey:sign_verify_prepare: EVP_MD_CTX_new() failed"
+ end
+ ffi_gc(md_ctx, C.EVP_MD_CTX_free)
+
+ local algo
+ if md_alg then
+ if OPENSSL_3X then
+ algo = C.EVP_MD_fetch(ctx_lib.get_libctx(), md_alg, nil)
+ else
+ algo = C.EVP_get_digestbyname(md_alg)
+ end
+ if algo == nil then
+ return nil, string.format("pkey:sign_verify_prepare: invalid digest type \"%s\"", md_alg)
+ end
+ end
+
+ local ppkey_ctx = evp_pkey_ctx_ptr_ptr_ct()
+ ppkey_ctx[0] = pkey_ctx
+ if fint(md_ctx, ppkey_ctx, algo, nil, self.ctx) ~= 1 then
+ return nil, format_error("pkey:sign_verify_prepare: Init failed")
+ end
+
+ if self.key_type == evp_macro.EVP_PKEY_RSA then
+ if padding then
+ if pkey_macro.EVP_PKEY_CTX_set_rsa_padding(ppkey_ctx[0], padding) ~= 1 then
+ return nil, format_error("pkey:sign_verify_prepare EVP_PKEY_CTX_set_rsa_padding")
+ end
+ end
+ if opts and opts.pss_saltlen and padding ~= rsa_macro.paddings.RSA_PKCS1_PSS_PADDING then
+ if pkey_macro.EVP_PKEY_CTX_set_rsa_pss_saltlen(ppkey_ctx[0], opts.pss_saltlen) ~= 1 then
+ return nil, format_error("pkey:sign_verify_prepare EVP_PKEY_CTX_set_rsa_pss_saltlen")
+ end
+ end
+ end
+
+ return md_ctx
+end
+
+function _M:sign(digest, md_alg, padding, opts)
+ -- TODO: temporary hack before OpenSSL has proper check for existence of private key
+ if self.key_type_is_ecx and not self:is_private() then
+ return nil, "pkey:sign: missing private key"
+ end
+
+ if digest_lib.istype(digest) then
+ local length = ptr_of_uint()
+ if C.EVP_SignFinal(digest.ctx, self.buf, length, self.ctx) ~= 1 then
+ return nil, format_error("pkey:sign: EVP_SignFinal")
+ end
+ return ffi_str(self.buf, length[0]), nil
+ elseif type(digest) == "string" then
+ if not OPENSSL_111_OR_LATER and not BORINGSSL then
+ -- we can still support earilier version with *Update and *Final
+ -- but we choose to not relying on the legacy interface for simplicity
+ return nil, "pkey:sign: new-style sign only available in OpenSSL 1.1.1 (or BoringSSL 1.1.0) or later"
+ elseif BORINGSSL and not md_alg and not self.key_type_is_ecx then
+ return nil, "pkey:sign: BoringSSL doesn't provide default digest, md_alg must be specified"
+ end
+
+ local md_ctx, err = sign_verify_prepare(self, C.EVP_DigestSignInit, md_alg, padding, opts)
+ if err then
+ return nil, err
+ end
+
+ local length = ptr_of_size_t(self.buf_size)
+ if C.EVP_DigestSign(md_ctx, self.buf, length, digest, #digest) ~= 1 then
+ return nil, format_error("pkey:sign: EVP_DigestSign")
+ end
+ return ffi_str(self.buf, length[0]), nil
+ else
+ return nil, "pkey:sign: expect a digest instance or a string at #1"
+ end
+end
+
+function _M:verify(signature, digest, md_alg, padding, opts)
+ if type(signature) ~= "string" then
+ return nil, "pkey:verify: expect a string at #1"
+ end
+
+ local code
+ if digest_lib.istype(digest) then
+ code = C.EVP_VerifyFinal(digest.ctx, signature, #signature, self.ctx)
+ elseif type(digest) == "string" then
+ if not OPENSSL_111_OR_LATER and not BORINGSSL then
+ -- we can still support earilier version with *Update and *Final
+ -- but we choose to not relying on the legacy interface for simplicity
+ return nil, "pkey:verify: new-style verify only available in OpenSSL 1.1.1 (or BoringSSL 1.1.0) or later"
+ elseif BORINGSSL and not md_alg and not self.key_type_is_ecx then
+ return nil, "pkey:verify: BoringSSL doesn't provide default digest, md_alg must be specified"
+ end
+
+ local md_ctx, err = sign_verify_prepare(self, C.EVP_DigestVerifyInit, md_alg, padding, opts)
+ if err then
+ return nil, err
+ end
+
+ code = C.EVP_DigestVerify(md_ctx, signature, #signature, digest, #digest)
+ else
+ return nil, "pkey:verify: expect a digest instance or a string at #2"
+ end
+
+ if code == 0 then
+ return false, nil
+ elseif code == 1 then
+ return true, nil
+ end
+ return false, format_error("pkey:verify")
+end
+
+function _M:derive(peerkey)
+ if not self.istype(peerkey) then
+ return nil, "pkey:derive: expect a pkey instance at #1"
+ end
+ local pctx = C.EVP_PKEY_CTX_new(self.ctx, nil)
+ if pctx == nil then
+ return nil, "pkey:derive: EVP_PKEY_CTX_new() failed"
+ end
+ ffi_gc(pctx, C.EVP_PKEY_CTX_free)
+ local code = C.EVP_PKEY_derive_init(pctx)
+ if code <= 0 then
+ return nil, format_error("pkey:derive: EVP_PKEY_derive_init", code)
+ end
+
+ code = C.EVP_PKEY_derive_set_peer(pctx, peerkey.ctx)
+ if code <= 0 then
+ return nil, format_error("pkey:derive: EVP_PKEY_derive_set_peer", code)
+ end
+
+ local buflen = ptr_of_size_t()
+ code = C.EVP_PKEY_derive(pctx, nil, buflen)
+ if code <= 0 then
+ return nil, format_error("pkey:derive: EVP_PKEY_derive check buffer size", code)
+ end
+
+ local buf = ctypes.uchar_array(buflen[0])
+ code = C.EVP_PKEY_derive(pctx, buf, buflen)
+ if code <= 0 then
+ return nil, format_error("pkey:derive: EVP_PKEY_derive", code)
+ end
+
+ return ffi_str(buf, buflen[0])
+end
+
+local function pub_or_priv_is_pri(pub_or_priv)
+ if pub_or_priv == 'private' or pub_or_priv == 'PrivateKey' then
+ return true
+ elseif not pub_or_priv or pub_or_priv == 'public' or pub_or_priv == 'PublicKey' then
+ return false
+ else
+ return nil, string.format("can only export private or public key, not %s", pub_or_priv)
+ end
+end
+
+function _M:tostring(pub_or_priv, fmt, pkcs1)
+ local is_priv, err = pub_or_priv_is_pri(pub_or_priv)
+ if err then
+ return nil, "pkey:tostring: " .. err
+ end
+ return __tostring(self, is_priv, fmt, pkcs1)
+end
+
+function _M:to_PEM(pub_or_priv, pkcs1)
+ return self:tostring(pub_or_priv, "PEM", pkcs1)
+end
+
+function _M.paramgen(config)
+ local typ = config.type
+ local key_type, write_func, get_ctx_func
+ if typ == "EC" then
+ key_type = evp_macro.EVP_PKEY_EC
+ if key_type == 0 then
+ return nil, "pkey.paramgen: the linked OpenSSL library doesn't support EC key"
+ end
+ write_func = C.PEM_write_bio_ECPKParameters
+ get_ctx_func = function(ctx)
+ local ctx = get_pkey_key[key_type](ctx)
+ if ctx == nil then
+ error(format_error("pkey.paramgen: EVP_PKEY_get0_{key}"))
+ end
+ return C.EC_KEY_get0_group(ctx)
+ end
+ elseif typ == "DH" then
+ key_type = evp_macro.EVP_PKEY_DH
+ if key_type == 0 then
+ return nil, "pkey.paramgen: the linked OpenSSL library doesn't support DH key"
+ end
+ write_func = C.PEM_write_bio_DHparams
+ get_ctx_func = get_pkey_key[key_type]
+ else
+ return nil, "pkey.paramgen: unsupported type " .. type
+ end
+
+ local params, err = generate_param(key_type, config)
+ if err then
+ return nil, "pkey.paramgen: generate_param: " .. err
+ end
+
+ local ctx = get_ctx_func(params)
+ if ctx == nil then
+ return nil, format_error("pkey.paramgen: EVP_PKEY_get0_{key}")
+ end
+
+ return bio_util.read_wrap(write_func, ctx)
+end
+
+return _M
diff --git a/server/resty/openssl/provider.lua b/server/resty/openssl/provider.lua
new file mode 100644
index 0000000..2879ac3
--- /dev/null
+++ b/server/resty/openssl/provider.lua
@@ -0,0 +1,136 @@
+local ffi = require "ffi"
+local C = ffi.C
+
+require "resty.openssl.include.provider"
+local param_lib = require "resty.openssl.param"
+local ctx_lib = require "resty.openssl.ctx"
+local null = require("resty.openssl.auxiliary.ctypes").null
+local OPENSSL_3X = require("resty.openssl.version").OPENSSL_3X
+local format_error = require("resty.openssl.err").format_error
+
+if not OPENSSL_3X then
+ error("provider is only supported since OpenSSL 3.0")
+end
+
+local _M = {}
+local mt = {__index = _M}
+
+local ossl_provider_ctx_ct = ffi.typeof('OSSL_PROVIDER*')
+
+function _M.load(name, try)
+ local ctx
+ local libctx = ctx_lib.get_libctx()
+ if try then
+ ctx = C.OSSL_PROVIDER_try_load(libctx, name)
+ if ctx == nil then
+ return nil, format_error("provider.try_load")
+ end
+ else
+ ctx = C.OSSL_PROVIDER_load(libctx, name)
+ if ctx == nil then
+ return nil, format_error("provider.load")
+ end
+ end
+
+ return setmetatable({
+ ctx = ctx,
+ param_types = nil,
+ }, mt), nil
+end
+
+function _M.set_default_search_path(path)
+ C.OSSL_PROVIDER_set_default_search_path(ctx_lib.get_libctx(), path)
+end
+
+function _M.is_available(name)
+ return C.OSSL_PROVIDER_available(ctx_lib.get_libctx(), name) == 1
+end
+
+function _M.istype(l)
+ return l and l.ctx and ffi.istype(ossl_provider_ctx_ct, l.ctx)
+end
+
+function _M:unload()
+ if C.OSSL_PROVIDER_unload(self.ctx) == nil then
+ return false, format_error("provider:unload")
+ end
+ return true
+end
+
+function _M:self_test()
+ if C.OSSL_PROVIDER_self_test(self.ctx) == nil then
+ return false, format_error("provider:self_test")
+ end
+ return true
+end
+
+local params_well_known = {
+ -- Well known parameter names that core passes to providers
+ ["openssl-version"] = param_lib.OSSL_PARAM_UTF8_PTR,
+ ["provider-name"] = param_lib.OSSL_PARAM_UTF8_PTR,
+ ["module-filename"] = param_lib.OSSL_PARAM_UTF8_PTR,
+
+ -- Well known parameter names that Providers can define
+ ["name"] = param_lib.OSSL_PARAM_UTF8_PTR,
+ ["version"] = param_lib.OSSL_PARAM_UTF8_PTR,
+ ["buildinfo"] = param_lib.OSSL_PARAM_UTF8_PTR,
+ ["status"] = param_lib.OSSL_PARAM_INTEGER,
+ ["security-checks"] = param_lib.OSSL_PARAM_INTEGER,
+}
+
+local function load_gettable_names(ctx)
+ local schema = {}
+ for k, v in pairs(params_well_known) do
+ schema[k] = v
+ end
+
+ local err
+ schema, err = param_lib.parse_params_schema(
+ C.OSSL_PROVIDER_gettable_params(ctx), schema)
+ if err then
+ return nil, err
+ end
+
+ return schema
+end
+
+function _M:get_params(...)
+ local keys = {...}
+ local key_length = #keys
+ if key_length == 0 then
+ return nil, "provider:get_params: at least one key is required"
+ end
+
+ if not self.param_types then
+ local param_types, err = load_gettable_names(self.ctx)
+ if err then
+ return nil, "provider:get_params: " .. err
+ end
+ self.param_types = param_types
+ end
+
+ local buffers = {}
+ for _, key in ipairs(keys) do
+ buffers[key] = null
+ end
+ local req, err = param_lib.construct(buffers, key_length, self.param_types)
+ if not req then
+ return nil, "provider:get_params: failed to construct params: " .. err
+ end
+
+ if C.OSSL_PROVIDER_get_params(self.ctx, req) ~= 1 then
+ return nil, format_error("provider:get_params")
+ end
+
+ buffers, err = param_lib.parse(buffers, key_length, self.param_types)
+ if err then
+ return nil, "provider:get_params: failed to parse params: " .. err
+ end
+
+ if key_length == 1 then
+ return buffers[keys[1]]
+ end
+ return buffers
+end
+
+return _M
diff --git a/server/resty/openssl/rand.lua b/server/resty/openssl/rand.lua
new file mode 100644
index 0000000..be54da9
--- /dev/null
+++ b/server/resty/openssl/rand.lua
@@ -0,0 +1,51 @@
+local ffi = require "ffi"
+local C = ffi.C
+local ffi_str = ffi.string
+
+require "resty.openssl.include.rand"
+local ctx_lib = require "resty.openssl.ctx"
+local ctypes = require "resty.openssl.auxiliary.ctypes"
+local format_error = require("resty.openssl.err").format_error
+local OPENSSL_3X = require("resty.openssl.version").OPENSSL_3X
+
+local buf
+local buf_size = 0
+local function bytes(length, private, strength)
+ if type(length) ~= "number" then
+ return nil, "rand.bytes: expect a number at #1"
+ elseif strength and type(strength) ~= "number" then
+ return nil, "rand.bytes: expect a number at #3"
+ end
+ -- generally we don't need manually reseed rng
+ -- https://www.openssl.org/docs/man1.1.1/man3/RAND_seed.html
+
+ -- initialize or resize buffer
+ if not buf or buf_size < length then
+ buf = ctypes.uchar_array(length)
+ buf_size = length
+ end
+
+ local code
+ if OPENSSL_3X then
+ if private then
+ code = C.RAND_priv_bytes_ex(ctx_lib.get_libctx(), buf, length, strength or 0)
+ else
+ code = C.RAND_bytes_ex(ctx_lib.get_libctx(), buf, length, strength or 0)
+ end
+ else
+ if private then
+ code = C.RAND_priv_bytes(buf, length)
+ else
+ code = C.RAND_bytes(buf, length)
+ end
+ end
+ if code ~= 1 then
+ return nil, format_error("rand.bytes", code)
+ end
+
+ return ffi_str(buf, length)
+end
+
+return {
+ bytes = bytes,
+}
diff --git a/server/resty/openssl/rsa.lua b/server/resty/openssl/rsa.lua
new file mode 100644
index 0000000..f3af394
--- /dev/null
+++ b/server/resty/openssl/rsa.lua
@@ -0,0 +1,155 @@
+local ffi = require "ffi"
+local C = ffi.C
+
+local bn_lib = require "resty.openssl.bn"
+
+local OPENSSL_10 = require("resty.openssl.version").OPENSSL_10
+local OPENSSL_11_OR_LATER = require("resty.openssl.version").OPENSSL_11_OR_LATER
+local format_error = require("resty.openssl.err").format_error
+
+local _M = {}
+
+_M.params = {"n", "e", "d", "p", "q", "dmp1", "dmq1", "iqmp"}
+
+local empty_table = {}
+local bn_ptrptr_ct = ffi.typeof("const BIGNUM *[1]")
+function _M.get_parameters(rsa_st)
+ -- {"n", "e", "d", "p", "q", "dmp1", "dmq1", "iqmp"}
+ return setmetatable(empty_table, {
+ __index = function(_, k)
+ local ptr, ret
+ if OPENSSL_11_OR_LATER then
+ ptr = bn_ptrptr_ct()
+ end
+
+ if k == 'n' then
+ if OPENSSL_11_OR_LATER then
+ C.RSA_get0_key(rsa_st, ptr, nil, nil)
+ end
+ elseif k == 'e' then
+ if OPENSSL_11_OR_LATER then
+ C.RSA_get0_key(rsa_st, nil, ptr, nil)
+ end
+ elseif k == 'd' then
+ if OPENSSL_11_OR_LATER then
+ C.RSA_get0_key(rsa_st, nil, nil, ptr)
+ end
+ elseif k == 'p' then
+ if OPENSSL_11_OR_LATER then
+ C.RSA_get0_factors(rsa_st, ptr, nil)
+ end
+ elseif k == 'q' then
+ if OPENSSL_11_OR_LATER then
+ C.RSA_get0_factors(rsa_st, nil, ptr)
+ end
+ elseif k == 'dmp1' then
+ if OPENSSL_11_OR_LATER then
+ C.RSA_get0_crt_params(rsa_st, ptr, nil, nil)
+ end
+ elseif k == 'dmq1' then
+ if OPENSSL_11_OR_LATER then
+ C.RSA_get0_crt_params(rsa_st, nil, ptr, nil)
+ end
+ elseif k == 'iqmp' then
+ if OPENSSL_11_OR_LATER then
+ C.RSA_get0_crt_params(rsa_st, nil, nil, ptr)
+ end
+ else
+ return nil, "rsa.get_parameters: unknown parameter \"" .. k .. "\" for RSA key"
+ end
+
+ if OPENSSL_11_OR_LATER then
+ ret = ptr[0]
+ elseif OPENSSL_10 then
+ ret = rsa_st[k]
+ end
+
+ if ret == nil then
+ return nil
+ end
+ return bn_lib.dup(ret)
+ end
+ }), nil
+end
+
+local function dup_bn_value(v)
+ if not bn_lib.istype(v) then
+ return nil, "expect value to be a bn instance"
+ end
+ local bn = C.BN_dup(v.ctx)
+ if bn == nil then
+ return nil, "BN_dup() failed"
+ end
+ return bn
+end
+
+function _M.set_parameters(rsa_st, opts)
+ local err
+ local opts_bn = {}
+ -- remember which parts of BNs has been added to rsa_st, they should be freed
+ -- by RSA_free and we don't cleanup them on failure
+ local cleanup_from_idx = 1
+ -- dup input
+ local do_set_key, do_set_factors, do_set_crt_params
+ for k, v in pairs(opts) do
+ opts_bn[k], err = dup_bn_value(v)
+ if err then
+ err = "rsa.set_parameters: cannot process parameter \"" .. k .. "\":" .. err
+ goto cleanup_with_error
+ end
+ if k == "n" or k == "e" or k == "d" then
+ do_set_key = true
+ elseif k == "p" or k == "q" then
+ do_set_factors = true
+ elseif k == "dmp1" or k == "dmq1" or k == "iqmp" then
+ do_set_crt_params = true
+ end
+ end
+ if OPENSSL_11_OR_LATER then
+ -- "The values n and e must be non-NULL the first time this function is called on a given RSA object."
+ -- thus we force to set them together
+ local code
+ if do_set_key then
+ code = C.RSA_set0_key(rsa_st, opts_bn["n"], opts_bn["e"], opts_bn["d"])
+ if code == 0 then
+ err = format_error("rsa.set_parameters: RSA_set0_key")
+ goto cleanup_with_error
+ end
+ end
+ cleanup_from_idx = cleanup_from_idx + 3
+ if do_set_factors then
+ code = C.RSA_set0_factors(rsa_st, opts_bn["p"], opts_bn["q"])
+ if code == 0 then
+ err = format_error("rsa.set_parameters: RSA_set0_factors")
+ goto cleanup_with_error
+ end
+ end
+ cleanup_from_idx = cleanup_from_idx + 2
+ if do_set_crt_params then
+ code = C.RSA_set0_crt_params(rsa_st, opts_bn["dmp1"], opts_bn["dmq1"], opts_bn["iqmp"])
+ if code == 0 then
+ err = format_error("rsa.set_parameters: RSA_set0_crt_params")
+ goto cleanup_with_error
+ end
+ end
+ return true
+ elseif OPENSSL_10 then
+ for k, v in pairs(opts_bn) do
+ if rsa_st[k] ~= nil then
+ C.BN_free(rsa_st[k])
+ end
+ rsa_st[k]= v
+ end
+ return true
+ end
+
+::cleanup_with_error::
+ for i, k in pairs(_M.params) do
+ if i >= cleanup_from_idx then
+ C.BN_free(opts_bn[k])
+ end
+ end
+ return false, err
+end
+
+return _M
diff --git a/server/resty/openssl/ssl.lua b/server/resty/openssl/ssl.lua
new file mode 100644
index 0000000..d3eee90
--- /dev/null
+++ b/server/resty/openssl/ssl.lua
@@ -0,0 +1,353 @@
+local ffi = require "ffi"
+local C = ffi.C
+local ffi_str = ffi.string
+local ffi_cast = ffi.cast
+
+require "resty.openssl.include.ssl"
+
+local nginx_aux = require("resty.openssl.auxiliary.nginx")
+local x509_lib = require("resty.openssl.x509")
+local chain_lib = require("resty.openssl.x509.chain")
+local stack_lib = require("resty.openssl.stack")
+local OPENSSL_3X = require("resty.openssl.version").OPENSSL_3X
+local OPENSSL_10 = require("resty.openssl.version").OPENSSL_10
+local format_error = require("resty.openssl.err").format_error
+
+local _M = {
+ SSL_VERIFY_NONE = 0x00,
+ SSL_VERIFY_PEER = 0x01,
+ SSL_VERIFY_FAIL_IF_NO_PEER_CERT = 0x02,
+ SSL_VERIFY_CLIENT_ONCE = 0x04,
+ SSL_VERIFY_POST_HANDSHAKE = 0x08,
+}
+
+local ops = {
+ SSL_OP_NO_EXTENDED_MASTER_SECRET = 0x00000001,
+ SSL_OP_CLEANSE_PLAINTEXT = 0x00000002,
+ SSL_OP_LEGACY_SERVER_CONNECT = 0x00000004,
+ SSL_OP_TLSEXT_PADDING = 0x00000010,
+ SSL_OP_SAFARI_ECDHE_ECDSA_BUG = 0x00000040,
+ SSL_OP_IGNORE_UNEXPECTED_EOF = 0x00000080,
+ SSL_OP_DISABLE_TLSEXT_CA_NAMES = 0x00000200,
+ SSL_OP_ALLOW_NO_DHE_KEX = 0x00000400,
+ SSL_OP_DONT_INSERT_EMPTY_FRAGMENTS = 0x00000800,
+ SSL_OP_NO_QUERY_MTU = 0x00001000,
+ SSL_OP_COOKIE_EXCHANGE = 0x00002000,
+ SSL_OP_NO_TICKET = 0x00004000,
+ SSL_OP_CISCO_ANYCONNECT = 0x00008000,
+ SSL_OP_NO_SESSION_RESUMPTION_ON_RENEGOTIATION = 0x00010000,
+ SSL_OP_NO_COMPRESSION = 0x00020000,
+ SSL_OP_ALLOW_UNSAFE_LEGACY_RENEGOTIATION = 0x00040000,
+ SSL_OP_NO_ENCRYPT_THEN_MAC = 0x00080000,
+ SSL_OP_ENABLE_MIDDLEBOX_COMPAT = 0x00100000,
+ SSL_OP_PRIORITIZE_CHACHA = 0x00200000,
+ SSL_OP_CIPHER_SERVER_PREFERENCE = 0x00400000,
+ SSL_OP_TLS_ROLLBACK_BUG = 0x00800000,
+ SSL_OP_NO_ANTI_REPLAY = 0x01000000,
+ SSL_OP_NO_SSLv3 = 0x02000000,
+ SSL_OP_NO_TLSv1 = 0x04000000,
+ SSL_OP_NO_TLSv1_2 = 0x08000000,
+ SSL_OP_NO_TLSv1_1 = 0x10000000,
+ SSL_OP_NO_TLSv1_3 = 0x20000000,
+ SSL_OP_NO_DTLSv1 = 0x04000000,
+ SSL_OP_NO_DTLSv1_2 = 0x08000000,
+ SSL_OP_NO_RENEGOTIATION = 0x40000000,
+ SSL_OP_CRYPTOPRO_TLSEXT_BUG = 0x80000000,
+}
+ops.SSL_OP_NO_SSL_MASK = ops.SSL_OP_NO_SSLv3 + ops.SSL_OP_NO_TLSv1 + ops.SSL_OP_NO_TLSv1_1
+ + ops.SSL_OP_NO_TLSv1_2 + ops.SSL_OP_NO_TLSv1_3
+ops.SSL_OP_NO_DTLS_MASK = ops.SSL_OP_NO_DTLSv1 + ops.SSL_OP_NO_DTLSv1_2
+for k, v in pairs(ops) do
+ _M[k] = v
+end
+
+local mt = {__index = _M}
+
+local ssl_ptr_ct = ffi.typeof('SSL*')
+
+local stack_of_ssl_cipher_iter = function(ctx)
+ return stack_lib.mt_of("SSL_CIPHER", function(x) return x end, {}, true).__ipairs({ctx = ctx})
+end
+
+function _M.from_request()
+ -- don't GC this
+ local ctx, err = nginx_aux.get_req_ssl()
+ if err ~= nil then
+ return nil, err
+ end
+
+ return setmetatable({
+ ctx = ctx,
+ -- the cdata is not manage by Lua, don't GC on Lua side
+ _managed = false,
+ -- this is the client SSL session
+ _server = true,
+ }, mt)
+end
+
+function _M.from_socket(socket)
+ if not socket then
+ return nil, "expect a ngx.socket.tcp instance at #1"
+ end
+ -- don't GC this
+ local ctx, err = nginx_aux.get_socket_ssl(socket)
+ if err ~= nil then
+ return nil, err
+ end
+
+ return setmetatable({
+ ctx = ctx,
+ -- the cdata is not manage by Lua, don't GC on Lua side
+ _managed = false,
+ -- this is the client SSL session
+ _server = false,
+ }, mt)
+end
+
+function _M.istype(l)
+ return l and l.ctx and ffi.istype(ssl_ptr_ct, l.ctx)
+end
+
+function _M:get_peer_certificate()
+ local x509
+ if OPENSSL_3X then
+ x509 = C.SSL_get1_peer_certificate(self.ctx)
+ else
+ x509 = C.SSL_get_peer_certificate(self.ctx)
+ end
+
+ if x509 == nil then
+ return nil
+ end
+ ffi.gc(x509, C.X509_free)
+
+ local err
+ -- always copy, although the ref counter of returned x509 is
+ -- already increased by one.
+ x509, err = x509_lib.dup(x509)
+ if err then
+ return nil, err
+ end
+
+ return x509
+end
+
+function _M:get_peer_cert_chain()
+ local stack = C.SSL_get_peer_cert_chain(self.ctx)
+
+ if stack == nil then
+ return nil
+ end
+
+ return chain_lib.dup(stack)
+end
+
+-- TLSv1.3
+function _M:set_ciphersuites(ciphers)
+ if C.SSL_set_ciphersuites(self.ctx, ciphers) ~= 1 then
+ return false, format_error("ssl:set_ciphers: SSL_set_ciphersuites")
+ end
+
+ return true
+end
+
+-- TLSv1.2 and lower
+function _M:set_cipher_list(ciphers)
+ if C.SSL_set_cipher_list(self.ctx, ciphers) ~= 1 then
+ return false, format_error("ssl:set_ciphers: SSL_set_cipher_list")
+ end
+
+ return true
+end
+
+function _M:get_ciphers()
+ local ciphers = C.SSL_get_ciphers(self.ctx)
+
+ if ciphers == nil then
+ return nil
+ end
+
+ local ret = {}
+
+ for i, cipher in stack_of_ssl_cipher_iter(ciphers) do
+ cipher = C.SSL_CIPHER_get_name(cipher)
+ if cipher == nil then
+ return nil, format_error("ssl:get_ciphers: SSL_CIPHER_get_name")
+ end
+ ret[i] = ffi_str(cipher)
+ end
+
+ return table.concat(ret, ":")
+end
+
+function _M:get_cipher_name()
+ local cipher = C.SSL_get_current_cipher(self.ctx)
+
+ if cipher == nil then
+ return nil
+ end
+
+ cipher = C.SSL_CIPHER_get_name(cipher)
+ if cipher == nil then
+ return nil, format_error("ssl:get_cipher_name: SSL_CIPHER_get_name")
+ end
+ return ffi_str(cipher)
+end
+
+function _M:set_timeout(tm)
+ local session = C.SSL_get_session(self.ctx)
+
+ if session == nil then
+ return false, format_error("ssl:set_timeout: SSL_get_session")
+ end
+
+ if C.SSL_SESSION_set_timeout(session, tm) ~= 1 then
+ return false, format_error("ssl:set_timeout: SSL_SESSION_set_timeout")
+ end
+ return true
+end
+
+function _M:get_timeout()
+ local session = C.SSL_get_session(self.ctx)
+
+ if session == nil then
+ return false, format_error("ssl:get_timeout: SSL_get_session")
+ end
+
+ return tonumber(C.SSL_SESSION_get_timeout(session))
+end
+
+local ssl_verify_default_cb = ffi_cast("verify_callback", function()
+ return 1
+end)
+
+function _M:set_verify(mode, cb)
+ if self._verify_cb then
+ self._verify_cb:free()
+ end
+
+ if cb then
+ cb = ffi_cast("verify_callback", cb)
+ self._verify_cb = cb
+ end
+
+ C.SSL_set_verify(self.ctx, mode, cb or ssl_verify_default_cb)
+
+ return true
+end
+
+function _M:free_verify_cb()
+ if self._verify_cb then
+ self._verify_cb:free()
+ self._verify_cb = nil
+ end
+end
+
+function _M:add_client_ca(x509)
+ if not self._server then
+ return false, "ssl:add_client_ca is only supported on server side"
+ end
+
+ if not x509_lib.istype(x509) then
+ return false, "expect a x509 instance at #1"
+ end
+
+ if C.SSL_add_client_CA(self.ctx, x509.ctx) ~= 1 then
+ return false, format_error("ssl:add_client_ca: SSL_add_client_CA")
+ end
+
+ return true
+end
+
+function _M:set_options(...)
+ local bitmask = 0
+ for _, opt in ipairs({...}) do
+ bitmask = bit.bor(bitmask, opt)
+ end
+
+ if OPENSSL_10 then
+ bitmask = C.SSL_ctrl(self.ctx, 32, bitmask, nil) -- SSL_CTRL_OPTIONS
+ else
+ bitmask = C.SSL_set_options(self.ctx, bitmask)
+ end
+
+ return tonumber(bitmask)
+end
+
+function _M:get_options(readable)
+ local bitmask
+ if OPENSSL_10 then
+ bitmask = C.SSL_ctrl(self.ctx, 32, 0, nil) -- SSL_CTRL_OPTIONS
+ else
+ bitmask = C.SSL_get_options(self.ctx)
+ end
+
+ if not readable then
+ return tonumber(bitmask)
+ end
+
+ local ret = {}
+ for k, v in pairs(ops) do
+ if bit.band(v, bitmask) > 0 then
+ table.insert(ret, k)
+ end
+ end
+ table.sort(ret)
+
+ return ret
+end
+
+function _M:clear_options(...)
+ local bitmask = 0
+ for _, opt in ipairs({...}) do
+ bitmask = bit.bor(bitmask, opt)
+ end
+
+ if OPENSSL_10 then
+ bitmask = C.SSL_ctrl(self.ctx, 77, bitmask, nil) -- SSL_CTRL_CLEAR_OPTIONS
+ else
+ bitmask = C.SSL_clear_options(self.ctx, bitmask)
+ end
+
+ return tonumber(bitmask)
+end
+
+local valid_protocols = {
+ ["SSLv3"] = ops.SSL_OP_NO_SSLv3,
+ ["TLSv1"] = ops.SSL_OP_NO_TLSv1,
+ ["TLSv1.1"] = ops.SSL_OP_NO_TLSv1_1,
+ ["TLSv1.2"] = ops.SSL_OP_NO_TLSv1_2,
+ ["TLSv1.3"] = ops.SSL_OP_NO_TLSv1_3,
+}
+local any_tlsv1 = ops.SSL_OP_NO_TLSv1_1 + ops.SSL_OP_NO_TLSv1_2 + ops.SSL_OP_NO_TLSv1_3
+
+function _M:set_protocols(...)
+ local bitmask = 0
+ for _, prot in ipairs({...}) do
+ local b = valid_protocols[prot]
+ if not b then
+ return nil, "\"" .. prot .. "\" is not a valid protocol"
+ end
+ bitmask = bit.bor(bitmask, b)
+ end
+
+ if bit.band(bitmask, any_tlsv1) > 0 then
+ bitmask = bit.bor(bitmask, ops.SSL_OP_NO_TLSv1)
+ end
+
+ -- first disable all protocols
+ if OPENSSL_10 then
+ C.SSL_ctrl(self.ctx, 32, ops.SSL_OP_NO_SSL_MASK, nil) -- SSL_CTRL_OPTIONS
+ else
+ C.SSL_set_options(self.ctx, ops.SSL_OP_NO_SSL_MASK)
+ end
+
+ -- then enable selected protocols
+ if OPENSSL_10 then
+ return tonumber(C.SSL_clear_options(self.ctx, bitmask))
+ else
+ return tonumber(C.SSL_ctrl(self.ctx, 77, bitmask, nil)) -- SSL_CTRL_CLEAR_OPTIONS)
+ end
+end
+
+return _M \ No newline at end of file
diff --git a/server/resty/openssl/ssl_ctx.lua b/server/resty/openssl/ssl_ctx.lua
new file mode 100644
index 0000000..dd110f9
--- /dev/null
+++ b/server/resty/openssl/ssl_ctx.lua
@@ -0,0 +1,95 @@
+local ffi = require "ffi"
+local C = ffi.C
+local new_tab = table.new
+local char = string.char
+local concat = table.concat
+
+require "resty.openssl.include.ssl"
+
+local nginx_aux = require("resty.openssl.auxiliary.nginx")
+
+local _M = {}
+local mt = {__index = _M}
+
+local ssl_ctx_ptr_ct = ffi.typeof('SSL_CTX*')
+
+function _M.from_request()
+ -- don't GC this
+ local ctx, err = nginx_aux.get_req_ssl_ctx()
+ if err ~= nil then
+ return nil, err
+ end
+
+ return setmetatable({
+ ctx = ctx,
+ -- the cdata is not manage by Lua, don't GC on Lua side
+ _managed = false,
+ -- this is the Server SSL session
+ _server = true,
+ }, mt)
+end
+
+function _M.from_socket(socket)
+ if not socket then
+ return nil, "expect a ngx.socket.tcp instance at #1"
+ end
+ -- don't GC this
+ local ctx, err = nginx_aux.get_socket_ssl_ctx(socket)
+ if err ~= nil then
+ return nil, err
+ end
+
+ return setmetatable({
+ ctx = ctx,
+ -- the cdata is not manage by Lua, don't GC on Lua side
+ _managed = false,
+ -- this is the client SSL session
+ _server = false,
+ }, mt)
+end
+
+function _M.istype(l)
+ return l and l.ctx and ffi.istype(ssl_ctx_ptr_ct, l.ctx)
+end
+
+local function encode_alpn_wire(alpns)
+ local ret = new_tab(#alpns*2, 0)
+ for i, alpn in ipairs(alpns) do
+ ret[i*2-1] = char(#alpn)
+ ret[i*2] = alpn
+ end
+
+ return concat(ret, "")
+end
+
+function _M:set_alpns(alpns)
+ if not self._server then
+ return nil, "ssl_ctx:set_alpns is only supported on server side"
+ end
+
+ alpns = encode_alpn_wire(alpns)
+
+ if self._alpn_select_cb then
+ self._alpn_select_cb:free()
+ end
+
+ local alpn_select_cb = ffi.cast("SSL_CTX_alpn_select_cb_func", function(_, out, outlen, client, client_len)
+ local code = ffi.C.SSL_select_next_proto(
+ ffi.cast("unsigned char **", out), outlen,
+ alpns, #alpns,
+ client, client_len)
+ if code ~= 1 then -- OPENSSL_NPN_NEGOTIATED
+ return 3 -- SSL_TLSEXT_ERR_NOACK
+ end
+ return 0 -- SSL_TLSEXT_ERR_OK
+ end)
+
+ C.SSL_CTX_set_alpn_select_cb(self.ctx, alpn_select_cb, nil)
+ -- store the reference to avoid it being GC'ed
+ self._alpn_select_cb = alpn_select_cb
+
+ return true
+end
+
+
+return _M \ No newline at end of file
diff --git a/server/resty/openssl/stack.lua b/server/resty/openssl/stack.lua
new file mode 100644
index 0000000..9bdc377
--- /dev/null
+++ b/server/resty/openssl/stack.lua
@@ -0,0 +1,159 @@
+
+--[[
+ The OpenSSL stack library. Note `safestack` is not usable here in ffi because
+ those symbols are eaten after preprocessing.
+ Instead, we should do a Lua land type checking by having a nested field indicating
+ which type of cdata its ctx holds.
+]]
+local ffi = require "ffi"
+local C = ffi.C
+local ffi_cast = ffi.cast
+local ffi_gc = ffi.gc
+
+local stack_macro = require "resty.openssl.include.stack"
+local format_error = require("resty.openssl.err").format_error
+
+local _M = {}
+
+local function gc_of(typ)
+ local f = C[typ .. "_free"]
+ return function (st)
+ stack_macro.OPENSSL_sk_pop_free(st, f)
+ end
+end
+
+_M.gc_of = gc_of
+
+_M.mt_of = function(typ, convert, index_tbl, no_gc)
+ if type(typ) ~= "string" then
+ error("expect a string at #1")
+ elseif type(convert) ~= "function" then
+ error("expect a function at #2")
+ end
+
+ local typ_ptr = typ .. "*"
+
+ -- starts from 0
+ local function value_at(ctx, i)
+ local elem = stack_macro.OPENSSL_sk_value(ctx, i)
+ if elem == nil then
+ error(format_error("OPENSSL_sk_value"))
+ end
+ local dup, err = convert(ffi_cast(typ_ptr, elem))
+ if err then
+ error(err)
+ end
+ return dup
+ end
+
+ local function iter(tbl)
+ if not tbl then error("instance is nil") end
+ local i = 0
+ local n = tonumber(stack_macro.OPENSSL_sk_num(tbl.ctx))
+ return function()
+ i = i + 1
+ if i <= n then
+ return i, value_at(tbl.ctx, i-1)
+ end
+ end
+ end
+
+ local ret = {
+ __pairs = iter,
+ __ipairs = iter,
+ __len = function(tbl)
+ if not tbl then error("instance is nil") end
+ return tonumber(stack_macro.OPENSSL_sk_num(tbl.ctx))
+ end,
+ __index = function(tbl, k)
+ if not tbl then error("instance is nil") end
+ local i = tonumber(k)
+ if not i then
+ return index_tbl[k]
+ end
+ local n = stack_macro.OPENSSL_sk_num(tbl.ctx)
+ if i <= 0 or i > n then
+ return nil
+ end
+ return value_at(tbl.ctx, i-1)
+ end,
+ }
+
+ if not no_gc then
+ ret.__gc = gc_of(typ)
+ end
+ return ret
+end
+
+_M.new_of = function(typ)
+ local gc = gc_of(typ)
+ return function()
+ local raw = stack_macro.OPENSSL_sk_new_null()
+ if raw == nil then
+ return nil, "stack.new_of: OPENSSL_sk_new_null() failed"
+ end
+ ffi_gc(raw, gc)
+ return raw
+ end
+end
+
+_M.add_of = function(typ)
+ local ptr = ffi.typeof(typ .. "*")
+ return function(stack, ctx)
+ if not stack then error("instance is nil") end
+ if ctx == nil or not ffi.istype(ptr, ctx) then
+ return false, "stack.add_of: expect a " .. typ .. "* at #1"
+ end
+ local code = stack_macro.OPENSSL_sk_push(stack, ctx)
+ if code == 0 then
+ return false, "stack.add_of: OPENSSL_sk_push() failed"
+ end
+ return true
+ end
+end
+
+local stack_ptr_ct = ffi.typeof("OPENSSL_STACK*")
+_M.dup_of = function(_)
+ return function(ctx)
+ if ctx == nil or not ffi.istype(stack_ptr_ct, ctx) then
+ return nil, "stack.dup_of: expect a stack ctx at #1"
+ end
+ local ctx = stack_macro.OPENSSL_sk_dup(ctx)
+ if ctx == nil then
+ return nil, "stack.dup_of: OPENSSL_sk_dup() failed"
+ end
+ -- if the stack is duplicated: since we don't copy the elements
+ -- then we only control gc of the stack itself here
+ ffi_gc(ctx, stack_macro.OPENSSL_sk_free)
+ return ctx
+ end
+end
+
+-- fallback function to iterate if LUAJIT_ENABLE_LUA52COMPAT not enabled
+_M.all_func = function(mt)
+ return function(stack)
+ if not stack then error("stack is nil") end
+ local ret = {}
+ local _next = mt.__pairs(stack)
+ while true do
+ local i, elem = _next()
+ if elem then
+ ret[i] = elem
+ else
+ break
+ end
+ end
+ return ret
+ end
+end
+
+_M.deep_copy_of = function(typ)
+ local dup = C[typ .. "_dup"]
+ local free = C[typ .. "_free"]
+
+ return function(ctx)
+ return stack_macro.OPENSSL_sk_deep_copy(ctx, dup, free)
+ end
+end
+
+return _M \ No newline at end of file
diff --git a/server/resty/openssl/version.lua b/server/resty/openssl/version.lua
new file mode 100644
index 0000000..f982b61
--- /dev/null
+++ b/server/resty/openssl/version.lua
@@ -0,0 +1,117 @@
+-- https://github.com/GUI/lua-openssl-ffi/blob/master/lib/openssl-ffi/version.lua
+local ffi = require "ffi"
+local C = ffi.C
+local ffi_str = ffi.string
+
+ffi.cdef[[
+ // 1.0
+ unsigned long SSLeay(void);
+ const char *SSLeay_version(int t);
+ // >= 1.1
+ unsigned long OpenSSL_version_num();
+ const char *OpenSSL_version(int t);
+ // >= 3.0
+ const char *OPENSSL_info(int t);
+ // BoringSSL
+ int BORINGSSL_self_test(void);
+]]
+
+local version_func, info_func
+local types_table
+
+-- >= 1.1
+local ok, version_num = pcall(function()
+ local num = C.OpenSSL_version_num()
+ version_func = C.OpenSSL_version
+ types_table = {
+ VERSION = 0,
+ CFLAGS = 1,
+ BUILT_ON = 2,
+ PLATFORM = 3,
+ DIR = 4,
+ ENGINES_DIR = 5,
+ VERSION_STRING = 6,
+ FULL_VERSION_STRING = 7,
+ MODULES_DIR = 8,
+ CPU_INFO = 9,
+ }
+ return num
+end)
+
+
+if not ok then
+ -- 1.0.x
+ ok, version_num = pcall(function()
+ local num = C.SSLeay()
+ version_func = C.SSLeay_version
+ types_table = {
+ VERSION = 0,
+ CFLAGS = 2,
+ BUILT_ON = 3,
+ PLATFORM = 4,
+ DIR = 5,
+ }
+ return num
+ end)
+end
+
+
+if not ok then
+ error(string.format("OpenSSL has encountered an error: %s; is OpenSSL library loaded?",
+ tostring(version_num)))
+elseif type(version_num) == 'number' and version_num < 0x10000000 then
+ error(string.format("OpenSSL version %s is not supported", tostring(version_num or 0)))
+elseif not version_num then
+ error("Can not get OpenSSL version")
+end
+
+if version_num >= 0x30000000 then
+ local info_table = {
+ INFO_CONFIG_DIR = 1001,
+ INFO_ENGINES_DIR = 1002,
+ INFO_MODULES_DIR = 1003,
+ INFO_DSO_EXTENSION = 1004,
+ INFO_DIR_FILENAME_SEPARATOR = 1005,
+ INFO_LIST_SEPARATOR = 1006,
+ INFO_SEED_SOURCE = 1007,
+ INFO_CPU_SETTINGS = 1008,
+ }
+
+ for k, v in pairs(info_table) do
+ types_table[k] = v
+ end
+
+ info_func = C.OPENSSL_info
+else
+ info_func = function(_)
+ error(string.format("OPENSSL_info is not supported on %s", ffi_str(version_func(0))))
+ end
+end
+
+local BORINGSSL = false
+pcall(function()
+ local _ = C.BORINGSSL_self_test
+ BORINGSSL = true
+end)
+
+return setmetatable({
+ version_num = tonumber(version_num),
+ version_text = ffi_str(version_func(0)),
+ version = function(t)
+ return ffi_str(version_func(t))
+ end,
+ info = function(t)
+ return ffi_str(info_func(t))
+ end,
+ OPENSSL_3X = version_num >= 0x30000000 and version_num < 0x30200000,
+ OPENSSL_30 = version_num >= 0x30000000 and version_num < 0x30100000, -- for backward compat, deprecated
+ OPENSSL_11 = version_num >= 0x10100000 and version_num < 0x10200000,
+ OPENSSL_111 = version_num >= 0x10101000 and version_num < 0x10200000,
+ OPENSSL_11_OR_LATER = version_num >= 0x10100000 and version_num < 0x30200000,
+ OPENSSL_111_OR_LATER = version_num >= 0x10101000 and version_num < 0x30200000,
+ OPENSSL_10 = version_num < 0x10100000 and version_num > 0x10000000,
+ BORINGSSL = BORINGSSL,
+ BORINGSSL_110 = BORINGSSL and version_num >= 0x10100000 and version_num < 0x10101000
+ }, {
+ __index = types_table,
+}) \ No newline at end of file
diff --git a/server/resty/openssl/x509/altname.lua b/server/resty/openssl/x509/altname.lua
new file mode 100644
index 0000000..34bf9e0
--- /dev/null
+++ b/server/resty/openssl/x509/altname.lua
@@ -0,0 +1,248 @@
+local ffi = require "ffi"
+local C = ffi.C
+local ffi_gc = ffi.gc
+local ffi_cast = ffi.cast
+local ffi_str = ffi.string
+
+require "resty.openssl.include.x509"
+require "resty.openssl.include.x509v3"
+local asn1_macro = require "resty.openssl.include.asn1"
+local stack_lib = require "resty.openssl.stack"
+local name_lib = require "resty.openssl.x509.name"
+local altname_macro = require "resty.openssl.include.x509.altname"
+
+local _M = {}
+
+local general_names_ptr_ct = ffi.typeof("GENERAL_NAMES*")
+
+local STACK = "GENERAL_NAME"
+local new = stack_lib.new_of(STACK)
+local add = stack_lib.add_of(STACK)
+local dup = stack_lib.dup_of(STACK)
+
+local types = altname_macro.types
+
+local AF_INET = 2
+local AF_INET6 = 10
+if ffi.os == "OSX" then
+ AF_INET6 = 30
+elseif ffi.os == "BSD" then
+ AF_INET6 = 28
+elseif ffi.os == "Windows" then
+ AF_INET6 = 23
+end
+
+ffi.cdef [[
+ typedef int socklen_t;
+ int inet_pton(int af, const char *restrict src, void *restrict dst);
+ const char *inet_ntop(int af, const void *restrict src,
+ char *restrict dst, socklen_t size);
+]]
+
+local ip_buffer = ffi.new("unsigned char [46]") -- 46 bytes enough for both string ipv6 and binary ipv6
+
+-- similar to GENERAL_NAME_print, but returns value instead of print
+local gn_decode = function(ctx)
+ local typ = ctx.type
+ local k = altname_macro.literals[typ]
+ local v
+ if typ == types.OtherName then
+ v = "OtherName:<unsupported>"
+ elseif typ == types.RFC822Name then
+ v = ffi_str(asn1_macro.ASN1_STRING_get0_data(ctx.d.rfc822Name))
+ elseif typ == types.DNS then
+ v = ffi_str(asn1_macro.ASN1_STRING_get0_data(ctx.d.dNSName))
+ elseif typ == types.X400 then
+ v = "X400:<unsupported>"
+ elseif typ == types.DirName then
+ v = name_lib.dup(ctx.d.directoryName)
+ elseif typ == types.EdiParty then
+ v = "EdiParty:<unsupported>"
+ elseif typ == types.URI then
+ v = ffi_str(asn1_macro.ASN1_STRING_get0_data(ctx.d.uniformResourceIdentifier))
+ elseif typ == types.IP then
+ v = asn1_macro.ASN1_STRING_get0_data(ctx.d.iPAddress)
+ local l = tonumber(C.ASN1_STRING_length(ctx.d.iPAddress))
+ if l ~= 4 and l ~= 16 then
+ error("Unknown IP address type")
+ end
+ v = C.inet_ntop(l == 4 and AF_INET or AF_INET6, v, ip_buffer, 46)
+ v = ffi_str(v)
+ elseif typ == types.RID then
+ v = "RID:<unsupported>"
+ else
+ error("unknown type" .. typ .. "-> " .. types.OtherName)
+ end
+ return { k, v }
+end
+
+-- shared with info_access
+_M.gn_decode = gn_decode
+
+local mt = stack_lib.mt_of(STACK, gn_decode, _M)
+local mt__pairs = mt.__pairs
+mt.__pairs = function(tbl)
+ local f = mt__pairs(tbl)
+ return function()
+ local _, e = f()
+ if not e then return end
+ return unpack(e)
+ end
+end
+
+function _M.new()
+ local ctx = new()
+ if ctx == nil then
+ return nil, "x509.altname.new: OPENSSL_sk_new_null() failed"
+ end
+ local cast = ffi_cast("GENERAL_NAMES*", ctx)
+
+ local self = setmetatable({
+ ctx = ctx,
+ cast = cast,
+ _is_shallow_copy = false,
+ }, mt)
+
+ return self, nil
+end
+
+function _M.istype(l)
+ return l and l.cast and ffi.istype(general_names_ptr_ct, l.cast)
+end
+
+function _M.dup(ctx)
+ if ctx == nil or not ffi.istype(general_names_ptr_ct, ctx) then
+ return nil, "x509.altname.dup: expect a GENERAL_NAMES* ctx at #1"
+ end
+
+ local dup_ctx = dup(ctx)
+
+ return setmetatable({
+ cast = ffi_cast("GENERAL_NAMES*", dup_ctx),
+ ctx = dup_ctx,
+ -- don't let lua gc the original stack to keep its elements
+ _dupped_from = ctx,
+ _is_shallow_copy = true,
+ _elem_refs = {},
+ _elem_refs_idx = 1,
+ }, mt), nil
+end
+
+local function gn_set(gn, typ, value)
+ if type(typ) ~= 'string' then
+ return "x509.altname:gn_set: expect a string at #1"
+ end
+ local typ_lower = typ:lower()
+ if type(value) ~= 'string' then
+ return "x509.altname:gn_set: except a string at #2"
+ end
+
+ local txt = value
+ local gn_type = types[typ_lower]
+
+ if not gn_type then
+ return "x509.altname:gn_set: unknown type " .. typ
+ end
+
+ if gn_type == types.IP then
+ if C.inet_pton(AF_INET, txt, ip_buffer) == 1 then
+ txt = ffi_str(ip_buffer, 4)
+ elseif C.inet_pton(AF_INET6, txt, ip_buffer) == 1 then
+ txt = ffi_str(ip_buffer, 16)
+ else
+ return "x509.altname:gn_set: invalid IP address " .. txt
+ end
+
+ elseif gn_type ~= types.Email and
+ gn_type ~= types.URI and
+ gn_type ~= types.DNS then
+ return "x509.altname:gn_set: setting type " .. typ .. " is currently not supported"
+ end
+
+ gn.type = gn_type
+
+ local asn1_string = C.ASN1_IA5STRING_new()
+ if asn1_string == nil then
+ return "x509.altname:gn_set: ASN1_STRING_type_new() failed"
+ end
+
+ local code = C.ASN1_STRING_set(asn1_string, txt, #txt)
+ if code ~= 1 then
+ C.ASN1_STRING_free(asn1_string)
+ return "x509.altname:gn_set: ASN1_STRING_set() failed: " .. code
+ end
+ gn.d.ia5 = asn1_string
+end
+
+-- shared with info_access
+_M.gn_set = gn_set
+
+function _M:add(typ, value)
+
+ -- the stack element stays with stack
+ -- we shouldn't add gc handler if it's already been
+ -- pushed to stack. instead, rely on the gc handler
+ -- of the stack to release all memories
+ local gn = C.GENERAL_NAME_new()
+ if gn == nil then
+ return nil, "x509.altname:add: GENERAL_NAME_new() failed"
+ end
+
+ local err = gn_set(gn, typ, value)
+ if err then
+ C.GENERAL_NAME_free(gn)
+ return nil, err
+ end
+
+ local _, err = add(self.ctx, gn)
+ if err then
+ C.GENERAL_NAME_free(gn)
+ return nil, err
+ end
+
+ -- if the stack is duplicated, the gc handler is not pop_free
+ -- handle the gc by ourselves
+ if self._is_shallow_copy then
+ ffi_gc(gn, C.GENERAL_NAME_free)
+ self._elem_refs[self._elem_refs_idx] = gn
+ self._elem_refs_idx = self._elem_refs_idx + 1
+ end
+ return self
+end
+
+_M.all = function(self)
+ local ret = {}
+ local _next = mt.__pairs(self)
+ while true do
+ local k, v = _next()
+ if k then
+ ret[k] = v
+ else
+ break
+ end
+ end
+ return ret
+end
+
+_M.each = mt.__pairs
+_M.index = mt.__index
+_M.count = mt.__len
+
+mt.__tostring = function(self)
+ local values = {}
+ local _next = mt.__pairs(self)
+ while true do
+ local k, v = _next()
+ if k then
+ table.insert(values, k .. "=" .. v)
+ else
+ break
+ end
+ end
+ table.sort(values)
+ return table.concat(values, "/")
+end
+
+_M.tostring = mt.__tostring
+
+return _M
diff --git a/server/resty/openssl/x509/chain.lua b/server/resty/openssl/x509/chain.lua
new file mode 100644
index 0000000..5557ea0
--- /dev/null
+++ b/server/resty/openssl/x509/chain.lua
@@ -0,0 +1,76 @@
+local ffi = require "ffi"
+local C = ffi.C
+local ffi_gc = ffi.gc
+
+local stack_lib = require "resty.openssl.stack"
+local x509_lib = require "resty.openssl.x509"
+local format_error = require("resty.openssl.err").format_error
+
+local _M = {}
+
+local stack_ptr_ct = ffi.typeof("OPENSSL_STACK*")
+
+local STACK = "X509"
+local gc = stack_lib.gc_of(STACK)
+local new = stack_lib.new_of(STACK)
+local add = stack_lib.add_of(STACK)
+local mt = stack_lib.mt_of(STACK, x509_lib.dup, _M)
+
+function _M.new()
+ local raw = new()
+
+ local self = setmetatable({
+ stack_of = STACK,
+ ctx = raw,
+ }, mt)
+
+ return self, nil
+end
+
+function _M.istype(l)
+ return l and l.ctx and ffi.istype(stack_ptr_ct, l.ctx)
+ and l.stack_of and l.stack_of == STACK
+end
+
+function _M.dup(ctx)
+ if ctx == nil or not ffi.istype(stack_ptr_ct, ctx) then
+ return nil, "x509.chain.dup: expect a stack ctx at #1, got " .. type(ctx)
+ end
+ -- sk_X509_dup plus up ref for each X509 element
+ local ctx = C.X509_chain_up_ref(ctx)
+ if ctx == nil then
+ return nil, "x509.chain.dup: X509_chain_up_ref() failed"
+ end
+ ffi_gc(ctx, gc)
+
+ return setmetatable({
+ stack_of = STACK,
+ ctx = ctx,
+ }, mt)
+end
+
+function _M:add(x509)
+ if not x509_lib.istype(x509) then
+ return nil, "x509.chain:add: expect a x509 instance at #1"
+ end
+
+ local dup = C.X509_dup(x509.ctx)
+ if dup == nil then
+ return nil, format_error("x509.chain:add: X509_dup")
+ end
+
+ local _, err = add(self.ctx, dup)
+ if err then
+ C.X509_free(dup)
+ return nil, err
+ end
+
+ return true
+end
+
+_M.all = stack_lib.all_func(mt)
+_M.each = mt.__ipairs
+_M.index = mt.__index
+_M.count = mt.__len
+
+return _M
diff --git a/server/resty/openssl/x509/crl.lua b/server/resty/openssl/x509/crl.lua
new file mode 100644
index 0000000..3ee4501
--- /dev/null
+++ b/server/resty/openssl/x509/crl.lua
@@ -0,0 +1,607 @@
+local ffi = require "ffi"
+local C = ffi.C
+local ffi_gc = ffi.gc
+
+require "resty.openssl.include.x509.crl"
+require "resty.openssl.include.pem"
+require "resty.openssl.include.x509v3"
+local asn1_lib = require("resty.openssl.asn1")
+local bn_lib = require("resty.openssl.bn")
+local revoked_lib = require("resty.openssl.x509.revoked")
+local digest_lib = require("resty.openssl.digest")
+local extension_lib = require("resty.openssl.x509.extension")
+local pkey_lib = require("resty.openssl.pkey")
+local bio_util = require "resty.openssl.auxiliary.bio"
+local ctx_lib = require "resty.openssl.ctx"
+local stack_lib = require "resty.openssl.stack"
+local txtnid2nid = require("resty.openssl.objects").txtnid2nid
+local find_sigid_algs = require("resty.openssl.objects").find_sigid_algs
+local format_error = require("resty.openssl.err").format_error
+local version = require("resty.openssl.version")
+local OPENSSL_10 = version.OPENSSL_10
+local OPENSSL_11_OR_LATER = version.OPENSSL_11_OR_LATER
+local OPENSSL_3X = version.OPENSSL_3X
+local BORINGSSL = version.BORINGSSL
+local BORINGSSL_110 = version.BORINGSSL_110 -- used in boringssl-fips-20190808
+
+local accessors = {}
+
+accessors.set_issuer_name = C.X509_CRL_set_issuer_name
+accessors.set_version = C.X509_CRL_set_version
+
+
+if OPENSSL_11_OR_LATER and not BORINGSSL_110 then
+ accessors.get_last_update = C.X509_CRL_get0_lastUpdate
+ accessors.set_last_update = C.X509_CRL_set1_lastUpdate
+ accessors.get_next_update = C.X509_CRL_get0_nextUpdate
+ accessors.set_next_update = C.X509_CRL_set1_nextUpdate
+ accessors.get_version = C.X509_CRL_get_version
+ accessors.get_issuer_name = C.X509_CRL_get_issuer -- returns internal ptr
+ accessors.get_signature_nid = C.X509_CRL_get_signature_nid
+ -- BORINGSSL_110 exports X509_CRL_get_signature_nid, but just ignored for simplicity
+ accessors.get_revoked = C.X509_CRL_get_REVOKED
+elseif OPENSSL_10 or BORINGSSL_110 then
+ accessors.get_last_update = function(crl)
+ if crl == nil or crl.crl == nil then
+ return nil
+ end
+ return crl.crl.lastUpdate
+ end
+ accessors.set_last_update = C.X509_CRL_set_lastUpdate
+ accessors.get_next_update = function(crl)
+ if crl == nil or crl.crl == nil then
+ return nil
+ end
+ return crl.crl.nextUpdate
+ end
+ accessors.set_next_update = C.X509_CRL_set_nextUpdate
+ accessors.get_version = function(crl)
+ if crl == nil or crl.crl == nil then
+ return nil
+ end
+ return C.ASN1_INTEGER_get(crl.crl.version)
+ end
+ accessors.get_issuer_name = function(crl)
+ if crl == nil or crl.crl == nil then
+ return nil
+ end
+ return crl.crl.issuer
+ end
+ accessors.get_signature_nid = function(crl)
+ if crl == nil or crl.crl == nil or crl.crl.sig_alg == nil then
+ return nil
+ end
+ return C.OBJ_obj2nid(crl.crl.sig_alg.algorithm)
+ end
+ accessors.get_revoked = function(crl)
+ return crl.crl.revoked
+ end
+end
+
+local function __tostring(self, fmt)
+ if not fmt or fmt == 'PEM' then
+ return bio_util.read_wrap(C.PEM_write_bio_X509_CRL, self.ctx)
+ elseif fmt == 'DER' then
+ return bio_util.read_wrap(C.i2d_X509_CRL_bio, self.ctx)
+ else
+ return nil, "x509.crl:tostring: can only write PEM or DER format, not " .. fmt
+ end
+end
+
+local _M = {}
+local mt = { __index = _M, __tostring = __tostring }
+
+local x509_crl_ptr_ct = ffi.typeof("X509_CRL*")
+
+function _M.new(crl, fmt, properties)
+ local ctx
+ if not crl then
+ if OPENSSL_3X then
+ ctx = C.X509_CRL_new_ex(ctx_lib.get_libctx(), properties)
+ else
+ ctx = C.X509_CRL_new()
+ end
+ if ctx == nil then
+ return nil, "x509.crl.new: X509_CRL_new() failed"
+ end
+ elseif type(crl) == "string" then
+ -- routine for load an existing csr
+ local bio = C.BIO_new_mem_buf(crl, #crl)
+ if bio == nil then
+ return nil, format_error("x509.crl.new: BIO_new_mem_buf")
+ end
+
+ fmt = fmt or "*"
+ while true do -- luacheck: ignore 512 -- loop is executed at most once
+ if fmt == "PEM" or fmt == "*" then
+ ctx = C.PEM_read_bio_X509_CRL(bio, nil, nil, nil)
+ if ctx ~= nil then
+ break
+ elseif fmt == "*" then
+ -- BIO_reset; #define BIO_CTRL_RESET 1
+ local code = C.BIO_ctrl(bio, 1, 0, nil)
+ if code ~= 1 then
+ return nil, "x509.crl.new: BIO_ctrl() failed: " .. code
+ end
+ end
+ end
+ if fmt == "DER" or fmt == "*" then
+ ctx = C.d2i_X509_CRL_bio(bio, nil)
+ end
+ break
+ end
+ C.BIO_free(bio)
+ if ctx == nil then
+ return nil, format_error("x509.crl.new")
+ end
+ -- clear errors occur when trying
+ C.ERR_clear_error()
+ else
+ return nil, "x509.crl.new: expect nil or a string at #1"
+ end
+ ffi_gc(ctx, C.X509_CRL_free)
+
+ local self = setmetatable({
+ ctx = ctx,
+ }, mt)
+
+ return self, nil
+end
+
+function _M.istype(l)
+ return l and l and l.ctx and ffi.istype(x509_crl_ptr_ct, l.ctx)
+end
+
+function _M.dup(ctx)
+ if not ffi.istype(x509_crl_ptr_ct, ctx) then
+ return nil, "x509.crl.dup: expect a x509.crl ctx at #1"
+ end
+ local ctx = C.X509_CRL_dup(ctx)
+ if ctx == nil then
+ return nil, "x509.crl.dup: X509_CRL_dup() failed"
+ end
+
+ ffi_gc(ctx, C.X509_CRL_free)
+
+ local self = setmetatable({
+ ctx = ctx,
+ }, mt)
+
+ return self, nil
+end
+
+function _M:tostring(fmt)
+ return __tostring(self, fmt)
+end
+
+function _M:to_PEM()
+ return __tostring(self, "PEM")
+end
+
+function _M:text()
+ return bio_util.read_wrap(C.X509_CRL_print, self.ctx)
+end
+
+local function revoked_decode(ctx)
+ if OPENSSL_10 then
+ error("x509.crl:revoked_decode: not supported on OpenSSL 1.0")
+ end
+
+ local ret = {}
+ local serial = C.X509_REVOKED_get0_serialNumber(ctx)
+ if serial ~= nil then
+ serial = C.ASN1_INTEGER_to_BN(serial, nil)
+ if serial == nil then
+ error("x509.crl:revoked_decode: ASN1_INTEGER_to_BN() failed")
+ end
+ ffi_gc(serial, C.BN_free)
+ ret["serial_number"] = bn_lib.to_hex({ctx = serial})
+ end
+
+ local date = C.X509_REVOKED_get0_revocationDate(ctx)
+ if date ~= nil then
+ date = asn1_lib.asn1_to_unix(date)
+ ret["revocation_date"] = date
+ end
+
+ return ret
+end
+
+local revoked_mt = stack_lib.mt_of("X509_REVOKED", revoked_decode, _M)
+
+local function nil_iter() return nil end
+local function revoked_iter(self)
+ local stack = accessors.get_revoked(self.ctx)
+ if stack == nil then
+ return nil_iter
+ end
+
+ return revoked_mt.__ipairs({ctx = stack})
+end
+
+mt.__pairs = revoked_iter
+mt.__ipairs = revoked_iter
+mt.__index = function(self, k)
+ local i = tonumber(k)
+ if not i then
+ return _M[k]
+ end
+
+ local stack = accessors.get_revoked(self.ctx)
+ if stack == nil then
+ return nil
+ end
+
+ return revoked_mt.__index({ctx = stack}, i)
+end
+mt.__len = function(self)
+ local stack = accessors.get_revoked(self.ctx)
+ if stack == nil then
+ return 0
+ end
+
+ return revoked_mt.__len({ctx = stack})
+end
+
+_M.all = function(self)
+ local ret = {}
+ local _next = mt.__pairs(self)
+ while true do
+ local k, v = _next()
+ if k then
+ ret[k] = v
+ else
+ break
+ end
+ end
+ return ret
+end
+_M.each = mt.__pairs
+_M.index = mt.__index
+_M.count = mt.__len
+
+--- Adds revoked item to stack of revoked certificates of crl
+-- @tparam table Instance of crl module
+-- @tparam table Instance of revoked module
+-- @treturn boolean true if revoked item was successfully added or false otherwise
+-- @treturn[opt] string Returns optional error message in case of error
+function _M:add_revoked(revoked)
+ if not revoked_lib.istype(revoked) then
+ return false, "x509.crl:add_revoked: expect a revoked instance at #1"
+ end
+ local ctx = C.X509_REVOKED_dup(revoked.ctx)
+ if ctx == nil then
+ return nil, "x509.crl:add_revoked: X509_REVOKED_dup() failed"
+ end
+
+ if C.X509_CRL_add0_revoked(self.ctx, ctx) == 0 then
+ return false, format_error("x509.crl:add_revoked")
+ end
+
+ return true
+end
+
+local ptr_ptr_of_x509_revoked = ffi.typeof("X509_REVOKED*[1]")
+function _M:get_by_serial(sn)
+ local bn, err
+ if bn_lib.istype(sn) then
+ bn = sn
+ elseif type(sn) == "string" then
+ bn, err = bn_lib.from_hex(sn)
+ if err then
+ return nil, "x509.crl:find: can't decode bn: " .. err
+ end
+ else
+ return nil, "x509.crl:find: expect a bn instance at #1"
+ end
+
+ local sn_asn1 = C.BN_to_ASN1_INTEGER(bn.ctx, nil)
+ if sn_asn1 == nil then
+ return nil, "x509.crl:find: BN_to_ASN1_INTEGER() failed"
+ end
+ ffi_gc(sn_asn1, C.ASN1_INTEGER_free)
+
+ local pp = ptr_ptr_of_x509_revoked()
+ local code = C.X509_CRL_get0_by_serial(self.ctx, pp, sn_asn1)
+ if code == 1 then
+ return revoked_decode(pp[0])
+ elseif code == 2 then
+ return nil, "not revoked (removeFromCRL)"
+ end
+
+ -- 0 or other
+ return nil
+end
+
+
+-- START AUTO GENERATED CODE
+
+-- AUTO GENERATED
+function _M:sign(pkey, digest)
+ if not pkey_lib.istype(pkey) then
+ return false, "x509.crl:sign: expect a pkey instance at #1"
+ end
+
+ local digest_algo
+ if digest then
+ if not digest_lib.istype(digest) then
+ return false, "x509.crl:sign: expect a digest instance at #2"
+ elseif not digest.algo then
+ return false, "x509.crl:sign: expect a digest instance to have algo member"
+ end
+ digest_algo = digest.algo
+ elseif BORINGSSL then
+ digest_algo = C.EVP_get_digestbyname('sha256')
+ end
+
+ -- returns size of signature if success
+ if C.X509_CRL_sign(self.ctx, pkey.ctx, digest_algo) == 0 then
+ return false, format_error("x509.crl:sign")
+ end
+
+ return true
+end
+
+-- AUTO GENERATED
+function _M:verify(pkey)
+ if not pkey_lib.istype(pkey) then
+ return false, "x509.crl:verify: expect a pkey instance at #1"
+ end
+
+ local code = C.X509_CRL_verify(self.ctx, pkey.ctx)
+ if code == 1 then
+ return true
+ elseif code == 0 then
+ return false
+ else -- typically -1
+ return false, format_error("x509.crl:verify", code)
+ end
+end
+
+-- AUTO GENERATED
+local function get_extension(ctx, nid_txt, last_pos)
+ last_pos = (last_pos or 0) - 1
+ local nid, err = txtnid2nid(nid_txt)
+ if err then
+ return nil, nil, err
+ end
+ local pos = C.X509_CRL_get_ext_by_NID(ctx, nid, last_pos)
+ if pos == -1 then
+ return nil
+ end
+ local ctx = C.X509_CRL_get_ext(ctx, pos)
+ if ctx == nil then
+ return nil, nil, format_error()
+ end
+ return ctx, pos
+end
+
+-- AUTO GENERATED
+function _M:add_extension(extension)
+ if not extension_lib.istype(extension) then
+ return false, "x509.crl:add_extension: expect a x509.extension instance at #1"
+ end
+
+ -- X509_CRL_add_ext returnes the stack on success, and NULL on error
+ -- the X509_EXTENSION ctx is dupped internally
+ if C.X509_CRL_add_ext(self.ctx, extension.ctx, -1) == nil then
+ return false, format_error("x509.crl:add_extension")
+ end
+
+ return true
+end
+
+-- AUTO GENERATED
+function _M:get_extension(nid_txt, last_pos)
+ local ctx, pos, err = get_extension(self.ctx, nid_txt, last_pos)
+ if err then
+ return nil, nil, "x509.crl:get_extension: " .. err
+ end
+ local ext, err = extension_lib.dup(ctx)
+ if err then
+ return nil, nil, "x509.crl:get_extension: " .. err
+ end
+ return ext, pos+1
+end
+
+local X509_CRL_delete_ext
+if OPENSSL_11_OR_LATER then
+ X509_CRL_delete_ext = C.X509_CRL_delete_ext
+elseif OPENSSL_10 then
+ X509_CRL_delete_ext = function(ctx, pos)
+ return C.X509v3_delete_ext(ctx.crl.extensions, pos)
+ end
+else
+ X509_CRL_delete_ext = function(...)
+ error("X509_CRL_delete_ext undefined")
+ end
+end
+
+-- AUTO GENERATED
+function _M:set_extension(extension, last_pos)
+ if not extension_lib.istype(extension) then
+ return false, "x509.crl:set_extension: expect a x509.extension instance at #1"
+ end
+
+ last_pos = (last_pos or 0) - 1
+
+ local nid = extension:get_object().nid
+ local pos = C.X509_CRL_get_ext_by_NID(self.ctx, nid, last_pos)
+ -- pos may be -1, which means not found, it's fine, we will add new one instead of replace
+
+ local removed = X509_CRL_delete_ext(self.ctx, pos)
+ C.X509_EXTENSION_free(removed)
+
+ if C.X509_CRL_add_ext(self.ctx, extension.ctx, pos) == nil then
+ return false, format_error("x509.crl:set_extension")
+ end
+
+ return true
+end
+
+-- AUTO GENERATED
+function _M:set_extension_critical(nid_txt, crit, last_pos)
+ local ctx, _, err = get_extension(self.ctx, nid_txt, last_pos)
+ if err then
+ return nil, "x509.crl:set_extension_critical: " .. err
+ end
+
+ if C.X509_EXTENSION_set_critical(ctx, crit and 1 or 0) ~= 1 then
+ return false, format_error("x509.crl:set_extension_critical")
+ end
+
+ return true
+end
+
+-- AUTO GENERATED
+function _M:get_extension_critical(nid_txt, last_pos)
+ local ctx, _, err = get_extension(self.ctx, nid_txt, last_pos)
+ if err then
+ return nil, "x509.crl:get_extension_critical: " .. err
+ end
+
+ return C.X509_EXTENSION_get_critical(ctx) == 1
+end
+
+-- AUTO GENERATED
+function _M:get_issuer_name()
+ local got = accessors.get_issuer_name(self.ctx)
+ if got == nil then
+ return nil
+ end
+ local lib = require("resty.openssl.x509.name")
+ -- the internal ptr is returned, ie we need to copy it
+ return lib.dup(got)
+end
+
+-- AUTO GENERATED
+function _M:set_issuer_name(toset)
+ local lib = require("resty.openssl.x509.name")
+ if lib.istype and not lib.istype(toset) then
+ return false, "x509.crl:set_issuer_name: expect a x509.name instance at #1"
+ end
+ toset = toset.ctx
+ if accessors.set_issuer_name(self.ctx, toset) == 0 then
+ return false, format_error("x509.crl:set_issuer_name")
+ end
+ return true
+end
+
+-- AUTO GENERATED
+function _M:get_last_update()
+ local got = accessors.get_last_update(self.ctx)
+ if got == nil then
+ return nil
+ end
+
+ got = asn1_lib.asn1_to_unix(got)
+
+ return got
+end
+
+-- AUTO GENERATED
+function _M:set_last_update(toset)
+ if type(toset) ~= "number" then
+ return false, "x509.crl:set_last_update: expect a number at #1"
+ end
+
+ toset = C.ASN1_TIME_set(nil, toset)
+ ffi_gc(toset, C.ASN1_STRING_free)
+
+ if accessors.set_last_update(self.ctx, toset) == 0 then
+ return false, format_error("x509.crl:set_last_update")
+ end
+ return true
+end
+
+-- AUTO GENERATED
+function _M:get_next_update()
+ local got = accessors.get_next_update(self.ctx)
+ if got == nil then
+ return nil
+ end
+
+ got = asn1_lib.asn1_to_unix(got)
+
+ return got
+end
+
+-- AUTO GENERATED
+function _M:set_next_update(toset)
+ if type(toset) ~= "number" then
+ return false, "x509.crl:set_next_update: expect a number at #1"
+ end
+
+ toset = C.ASN1_TIME_set(nil, toset)
+ ffi_gc(toset, C.ASN1_STRING_free)
+
+ if accessors.set_next_update(self.ctx, toset) == 0 then
+ return false, format_error("x509.crl:set_next_update")
+ end
+ return true
+end
+
+-- AUTO GENERATED
+function _M:get_version()
+ local got = accessors.get_version(self.ctx)
+ if got == nil then
+ return nil
+ end
+
+ got = tonumber(got) + 1
+
+ return got
+end
+
+-- AUTO GENERATED
+function _M:set_version(toset)
+ if type(toset) ~= "number" then
+ return false, "x509.crl:set_version: expect a number at #1"
+ end
+
+ -- Note: this is defined by standards (X.509 et al) to be one less than the certificate version.
+ -- So a version 3 certificate will return 2 and a version 1 certificate will return 0.
+ toset = toset - 1
+
+ if accessors.set_version(self.ctx, toset) == 0 then
+ return false, format_error("x509.crl:set_version")
+ end
+ return true
+end
+
+
+-- AUTO GENERATED
+function _M:get_signature_nid()
+ local nid = accessors.get_signature_nid(self.ctx)
+ if nid <= 0 then
+ return nil, format_error("x509.crl:get_signature_nid")
+ end
+
+ return nid
+end
+
+-- AUTO GENERATED
+function _M:get_signature_name()
+ local nid = accessors.get_signature_nid(self.ctx)
+ if nid <= 0 then
+ return nil, format_error("x509.crl:get_signature_name")
+ end
+
+ return ffi.string(C.OBJ_nid2sn(nid))
+end
+
+-- AUTO GENERATED
+function _M:get_signature_digest_name()
+ local nid = accessors.get_signature_nid(self.ctx)
+ if nid <= 0 then
+ return nil, format_error("x509.crl:get_signature_digest_name")
+ end
+
+ local nid = find_sigid_algs(nid)
+
+ return ffi.string(C.OBJ_nid2sn(nid))
+end
+-- END AUTO GENERATED CODE
+
+return _M
+
diff --git a/server/resty/openssl/x509/csr.lua b/server/resty/openssl/x509/csr.lua
new file mode 100644
index 0000000..08c4860
--- /dev/null
+++ b/server/resty/openssl/x509/csr.lua
@@ -0,0 +1,531 @@
+local ffi = require "ffi"
+local C = ffi.C
+local ffi_gc = ffi.gc
+local ffi_cast = ffi.cast
+
+require "resty.openssl.include.pem"
+require "resty.openssl.include.x509v3"
+require "resty.openssl.include.x509.csr"
+require "resty.openssl.include.asn1"
+local stack_macro = require "resty.openssl.include.stack"
+local stack_lib = require "resty.openssl.stack"
+local pkey_lib = require "resty.openssl.pkey"
+local digest_lib = require("resty.openssl.digest")
+local extension_lib = require("resty.openssl.x509.extension")
+local extensions_lib = require("resty.openssl.x509.extensions")
+local bio_util = require "resty.openssl.auxiliary.bio"
+local ctypes = require "resty.openssl.auxiliary.ctypes"
+local ctx_lib = require "resty.openssl.ctx"
+local txtnid2nid = require("resty.openssl.objects").txtnid2nid
+local find_sigid_algs = require("resty.openssl.objects").find_sigid_algs
+local format_error = require("resty.openssl.err").format_error
+local version = require("resty.openssl.version")
+local OPENSSL_10 = version.OPENSSL_10
+local OPENSSL_11_OR_LATER = version.OPENSSL_11_OR_LATER
+local OPENSSL_3X = version.OPENSSL_3X
+local BORINGSSL = version.BORINGSSL
+local BORINGSSL_110 = version.BORINGSSL_110 -- used in boringssl-fips-20190808
+
+local accessors = {}
+
+accessors.set_subject_name = C.X509_REQ_set_subject_name
+accessors.get_pubkey = C.X509_REQ_get_pubkey
+accessors.set_pubkey = C.X509_REQ_set_pubkey
+accessors.set_version = C.X509_REQ_set_version
+
+if OPENSSL_11_OR_LATER or BORINGSSL_110 then
+ accessors.get_signature_nid = C.X509_REQ_get_signature_nid
+elseif OPENSSL_10 then
+ accessors.get_signature_nid = function(csr)
+ if csr == nil or csr.sig_alg == nil then
+ return nil
+ end
+ return C.OBJ_obj2nid(csr.sig_alg.algorithm)
+ end
+end
+
+if OPENSSL_11_OR_LATER and not BORINGSSL_110 then
+ accessors.get_subject_name = C.X509_REQ_get_subject_name -- returns internal ptr
+ accessors.get_version = C.X509_REQ_get_version
+elseif OPENSSL_10 or BORINGSSL_110 then
+ accessors.get_subject_name = function(csr)
+ if csr == nil or csr.req_info == nil then
+ return nil
+ end
+ return csr.req_info.subject
+ end
+ accessors.get_version = function(csr)
+ if csr == nil or csr.req_info == nil then
+ return nil
+ end
+ return C.ASN1_INTEGER_get(csr.req_info.version)
+ end
+end
+
+local function __tostring(self, fmt)
+ if not fmt or fmt == 'PEM' then
+ return bio_util.read_wrap(C.PEM_write_bio_X509_REQ, self.ctx)
+ elseif fmt == 'DER' then
+ return bio_util.read_wrap(C.i2d_X509_REQ_bio, self.ctx)
+ else
+ return nil, "x509.csr:tostring: can only write PEM or DER format, not " .. fmt
+ end
+end
+
+local _M = {}
+local mt = { __index = _M, __tostring = __tostring }
+
+local x509_req_ptr_ct = ffi.typeof("X509_REQ*")
+
+local stack_ptr_type = ffi.typeof("struct stack_st *[1]")
+local x509_extensions_gc = stack_lib.gc_of("X509_EXTENSION")
+
+function _M.new(csr, fmt, properties)
+ local ctx
+ if not csr then
+ if OPENSSL_3X then
+ ctx = C.X509_REQ_new_ex(ctx_lib.get_libctx(), properties)
+ else
+ ctx = C.X509_REQ_new()
+ end
+ if ctx == nil then
+ return nil, "x509.csr.new: X509_REQ_new() failed"
+ end
+ elseif type(csr) == "string" then
+ -- routine for load an existing csr
+ local bio = C.BIO_new_mem_buf(csr, #csr)
+ if bio == nil then
+ return nil, format_error("x509.csr.new: BIO_new_mem_buf")
+ end
+
+ fmt = fmt or "*"
+ while true do -- luacheck: ignore 512 -- loop is executed at most once
+ if fmt == "PEM" or fmt == "*" then
+ ctx = C.PEM_read_bio_X509_REQ(bio, nil, nil, nil)
+ if ctx ~= nil then
+ break
+ elseif fmt == "*" then
+ -- BIO_reset; #define BIO_CTRL_RESET 1
+ local code = C.BIO_ctrl(bio, 1, 0, nil)
+ if code ~= 1 then
+ return nil, "x509.csr.new: BIO_ctrl() failed: " .. code
+ end
+ end
+ end
+ if fmt == "DER" or fmt == "*" then
+ ctx = C.d2i_X509_REQ_bio(bio, nil)
+ end
+ break
+ end
+ C.BIO_free(bio)
+ if ctx == nil then
+ return nil, format_error("x509.csr.new")
+ end
+ -- clear errors occur when trying
+ C.ERR_clear_error()
+ else
+ return nil, "x509.csr.new: expect nil or a string at #1"
+ end
+ ffi_gc(ctx, C.X509_REQ_free)
+
+ local self = setmetatable({
+ ctx = ctx,
+ }, mt)
+
+ return self, nil
+end
+
+function _M.istype(l)
+ return l and l and l.ctx and ffi.istype(x509_req_ptr_ct, l.ctx)
+end
+
+function _M:tostring(fmt)
+ return __tostring(self, fmt)
+end
+
+function _M:to_PEM()
+ return __tostring(self, "PEM")
+end
+
+function _M:check_private_key(key)
+ if not pkey_lib.istype(key) then
+ return false, "x509.csr:check_private_key: except a pkey instance at #1"
+ end
+
+ if not key:is_private() then
+ return false, "x509.csr:check_private_key: not a private key"
+ end
+
+ if C.X509_REQ_check_private_key(self.ctx, key.ctx) == 1 then
+ return true
+ end
+ return false, format_error("x509.csr:check_private_key")
+end
+
+--- Get all csr extensions
+-- @tparam table self Instance of csr
+-- @treturn Extensions object
+function _M:get_extensions()
+ local extensions = C.X509_REQ_get_extensions(self.ctx)
+ -- GC handler is sk_X509_EXTENSION_pop_free
+ ffi_gc(extensions, x509_extensions_gc)
+
+ return extensions_lib.dup(extensions)
+end
+
+local function get_extension(ctx, nid_txt, last_pos)
+ local nid, err = txtnid2nid(nid_txt)
+ if err then
+ return nil, nil, err
+ end
+
+ local extensions = C.X509_REQ_get_extensions(ctx)
+ if extensions == nil then
+ return nil, nil, format_error("csr.get_extension: X509_REQ_get_extensions")
+ end
+ ffi_gc(extensions, x509_extensions_gc)
+
+ -- make 1-index array to 0-index
+ last_pos = (last_pos or 0) -1
+ local ext_idx = C.X509v3_get_ext_by_NID(extensions, nid, last_pos)
+ if ext_idx == -1 then
+ err = ("X509v3_get_ext_by_NID extension for %d not found"):format(nid)
+ return nil, -1, format_error(err)
+ end
+
+ local ctx = C.X509v3_get_ext(extensions, ext_idx)
+ if ctx == nil then
+ return nil, nil, format_error("X509v3_get_ext")
+ end
+
+ return ctx, ext_idx, nil
+end
+
+--- Get a csr extension
+-- @tparam table self Instance of csr
+-- @tparam string|number Nid number or name of the extension
+-- @tparam number Position to start looking for the extension; default to look from start if omitted
+-- @treturn Parsed extension object or nil if not found
+function _M:get_extension(nid_txt, last_pos)
+ local ctx, pos, err = get_extension(self.ctx, nid_txt, last_pos)
+ if err then
+ return nil, nil, "x509.csr:get_extension: " .. err
+ end
+ local ext, err = extension_lib.dup(ctx)
+ if err then
+ return nil, nil, "x509.csr:get_extension: " .. err
+ end
+ return ext, pos+1
+end
+
+local function modify_extension(replace, ctx, nid, toset, crit)
+ local extensions_ptr = stack_ptr_type()
+ extensions_ptr[0] = C.X509_REQ_get_extensions(ctx)
+ local need_cleanup = extensions_ptr[0] ~= nil and
+ -- extensions_ptr being nil is fine: it may just because there's no extension yet
+ -- https://github.com/openssl/openssl/commit/2039ac07b401932fa30a05ade80b3626e189d78a
+ -- introduces a change that a empty stack instead of NULL will be returned in no extension
+ -- is found. so we need to double check the number if it's not NULL.
+ stack_macro.OPENSSL_sk_num(extensions_ptr[0]) > 0
+
+ local flag
+ if replace then
+ -- x509v3.h: # define X509V3_ADD_REPLACE 2L
+ flag = 0x2
+ else
+ -- x509v3.h: # define X509V3_ADD_APPEND 1L
+ flag = 0x1
+ end
+
+ local code = C.X509V3_add1_i2d(extensions_ptr, nid, toset, crit and 1 or 0, flag)
+ -- when the stack is newly allocated, we want to cleanup the newly created stack as well
+ -- setting the gc handler here as it's mutated in X509V3_add1_i2d if it's pointing to NULL
+ ffi_gc(extensions_ptr[0], x509_extensions_gc)
+ if code ~= 1 then
+ return false, format_error("X509V3_add1_i2d", code)
+ end
+
+ code = C.X509_REQ_add_extensions(ctx, extensions_ptr[0])
+ if code ~= 1 then
+ return false, format_error("X509_REQ_add_extensions", code)
+ end
+
+ if need_cleanup then
+ -- cleanup old attributes
+ -- delete the first only, why?
+ local attr = C.X509_REQ_delete_attr(ctx, 0)
+ if attr ~= nil then
+ C.X509_ATTRIBUTE_free(attr)
+ end
+ end
+
+ -- mark encoded form as invalid so next time it will be re-encoded
+ if OPENSSL_11_OR_LATER then
+ C.i2d_re_X509_REQ_tbs(ctx, nil)
+ else
+ ctx.req_info.enc.modified = 1
+ end
+
+ return true
+end
+
+local function add_extension(...)
+ return modify_extension(false, ...)
+end
+
+local function replace_extension(...)
+ return modify_extension(true, ...)
+end
+
+function _M:add_extension(extension)
+ if not extension_lib.istype(extension) then
+ return false, "x509:set_extension: expect a x509.extension instance at #1"
+ end
+
+ local nid = extension:get_object().nid
+ local toset = extension_lib.to_data(extension, nid)
+ return add_extension(self.ctx, nid, toset.ctx, extension:get_critical())
+end
+
+function _M:set_extension(extension)
+ if not extension_lib.istype(extension) then
+ return false, "x509:set_extension: expect a x509.extension instance at #1"
+ end
+
+ local nid = extension:get_object().nid
+ local toset = extension_lib.to_data(extension, nid)
+ return replace_extension(self.ctx, nid, toset.ctx, extension:get_critical())
+end
+
+function _M:set_extension_critical(nid_txt, crit, last_pos)
+ local nid, err = txtnid2nid(nid_txt)
+ if err then
+ return nil, "x509.csr:set_extension_critical: " .. err
+ end
+
+ local extension, _, err = get_extension(self.ctx, nid, last_pos)
+ if err then
+ return nil, "x509.csr:set_extension_critical: " .. err
+ end
+
+ local toset = extension_lib.to_data({
+ ctx = extension
+ }, nid)
+ return replace_extension(self.ctx, nid, toset.ctx, crit and 1 or 0)
+end
+
+function _M:get_extension_critical(nid_txt, last_pos)
+ local ctx, _, err = get_extension(self.ctx, nid_txt, last_pos)
+ if err then
+ return nil, "x509.csr:get_extension_critical: " .. err
+ end
+
+ return C.X509_EXTENSION_get_critical(ctx) == 1
+end
+
+-- START AUTO GENERATED CODE
+
+-- AUTO GENERATED
+function _M:sign(pkey, digest)
+ if not pkey_lib.istype(pkey) then
+ return false, "x509.csr:sign: expect a pkey instance at #1"
+ end
+
+ local digest_algo
+ if digest then
+ if not digest_lib.istype(digest) then
+ return false, "x509.csr:sign: expect a digest instance at #2"
+ elseif not digest.algo then
+ return false, "x509.csr:sign: expect a digest instance to have algo member"
+ end
+ digest_algo = digest.algo
+ elseif BORINGSSL then
+ digest_algo = C.EVP_get_digestbyname('sha256')
+ end
+
+ -- returns size of signature if success
+ if C.X509_REQ_sign(self.ctx, pkey.ctx, digest_algo) == 0 then
+ return false, format_error("x509.csr:sign")
+ end
+
+ return true
+end
+
+-- AUTO GENERATED
+function _M:verify(pkey)
+ if not pkey_lib.istype(pkey) then
+ return false, "x509.csr:verify: expect a pkey instance at #1"
+ end
+
+ local code = C.X509_REQ_verify(self.ctx, pkey.ctx)
+ if code == 1 then
+ return true
+ elseif code == 0 then
+ return false
+ else -- typically -1
+ return false, format_error("x509.csr:verify", code)
+ end
+end
+-- AUTO GENERATED
+function _M:get_subject_name()
+ local got = accessors.get_subject_name(self.ctx)
+ if got == nil then
+ return nil
+ end
+ local lib = require("resty.openssl.x509.name")
+ -- the internal ptr is returned, ie we need to copy it
+ return lib.dup(got)
+end
+
+-- AUTO GENERATED
+function _M:set_subject_name(toset)
+ local lib = require("resty.openssl.x509.name")
+ if lib.istype and not lib.istype(toset) then
+ return false, "x509.csr:set_subject_name: expect a x509.name instance at #1"
+ end
+ toset = toset.ctx
+ if accessors.set_subject_name(self.ctx, toset) == 0 then
+ return false, format_error("x509.csr:set_subject_name")
+ end
+ return true
+end
+
+-- AUTO GENERATED
+function _M:get_pubkey()
+ local got = accessors.get_pubkey(self.ctx)
+ if got == nil then
+ return nil
+ end
+ local lib = require("resty.openssl.pkey")
+ -- returned a copied instance directly
+ return lib.new(got)
+end
+
+-- AUTO GENERATED
+function _M:set_pubkey(toset)
+ local lib = require("resty.openssl.pkey")
+ if lib.istype and not lib.istype(toset) then
+ return false, "x509.csr:set_pubkey: expect a pkey instance at #1"
+ end
+ toset = toset.ctx
+ if accessors.set_pubkey(self.ctx, toset) == 0 then
+ return false, format_error("x509.csr:set_pubkey")
+ end
+ return true
+end
+
+-- AUTO GENERATED
+function _M:get_version()
+ local got = accessors.get_version(self.ctx)
+ if got == nil then
+ return nil
+ end
+
+ got = tonumber(got) + 1
+
+ return got
+end
+
+-- AUTO GENERATED
+function _M:set_version(toset)
+ if type(toset) ~= "number" then
+ return false, "x509.csr:set_version: expect a number at #1"
+ end
+
+ -- Note: this is defined by standards (X.509 et al) to be one less than the certificate version.
+ -- So a version 3 certificate will return 2 and a version 1 certificate will return 0.
+ toset = toset - 1
+
+ if accessors.set_version(self.ctx, toset) == 0 then
+ return false, format_error("x509.csr:set_version")
+ end
+ return true
+end
+
+local NID_subject_alt_name = C.OBJ_sn2nid("subjectAltName")
+assert(NID_subject_alt_name ~= 0)
+
+-- AUTO GENERATED: EXTENSIONS
+function _M:get_subject_alt_name()
+ local crit = ctypes.ptr_of_int()
+ local extensions = C.X509_REQ_get_extensions(self.ctx)
+ -- GC handler is sk_X509_EXTENSION_pop_free
+ ffi_gc(extensions, x509_extensions_gc)
+ local got = C.X509V3_get_d2i(extensions, NID_subject_alt_name, crit, nil)
+ crit = tonumber(crit[0])
+ if crit == -1 then -- not found
+ return nil
+ elseif crit == -2 then
+ return nil, "x509.csr:get_subject_alt_name: extension of subject_alt_name occurs more than one times, " ..
+ "this is not yet implemented. Please use get_extension instead."
+ elseif got == nil then
+ return nil, format_error("x509.csr:get_subject_alt_name")
+ end
+
+ -- Note: here we only free the stack itself not elements
+ -- since there seems no way to increase ref count for a GENERAL_NAME
+ -- we left the elements referenced by the new-dup'ed stack
+ local got_ref = got
+ ffi_gc(got_ref, stack_lib.gc_of("GENERAL_NAME"))
+ got = ffi_cast("GENERAL_NAMES*", got_ref)
+ local lib = require("resty.openssl.x509.altname")
+ -- the internal ptr is returned, ie we need to copy it
+ return lib.dup(got)
+end
+
+-- AUTO GENERATED: EXTENSIONS
+function _M:set_subject_alt_name(toset)
+ local lib = require("resty.openssl.x509.altname")
+ if lib.istype and not lib.istype(toset) then
+ return false, "x509.csr:set_subject_alt_name: expect a x509.altname instance at #1"
+ end
+ toset = toset.ctx
+ return replace_extension(self.ctx, NID_subject_alt_name, toset)
+end
+
+-- AUTO GENERATED: EXTENSIONS
+function _M:set_subject_alt_name_critical(crit)
+ return _M.set_extension_critical(self, NID_subject_alt_name, crit)
+end
+
+-- AUTO GENERATED: EXTENSIONS
+function _M:get_subject_alt_name_critical()
+ return _M.get_extension_critical(self, NID_subject_alt_name)
+end
+
+
+-- AUTO GENERATED
+function _M:get_signature_nid()
+ local nid = accessors.get_signature_nid(self.ctx)
+ if nid <= 0 then
+ return nil, format_error("x509.csr:get_signature_nid")
+ end
+
+ return nid
+end
+
+-- AUTO GENERATED
+function _M:get_signature_name()
+ local nid = accessors.get_signature_nid(self.ctx)
+ if nid <= 0 then
+ return nil, format_error("x509.csr:get_signature_name")
+ end
+
+ return ffi.string(C.OBJ_nid2sn(nid))
+end
+
+-- AUTO GENERATED
+function _M:get_signature_digest_name()
+ local nid = accessors.get_signature_nid(self.ctx)
+ if nid <= 0 then
+ return nil, format_error("x509.csr:get_signature_digest_name")
+ end
+
+ local nid = find_sigid_algs(nid)
+
+ return ffi.string(C.OBJ_nid2sn(nid))
+end
+-- END AUTO GENERATED CODE
+
+return _M
+
diff --git a/server/resty/openssl/x509/extension.lua b/server/resty/openssl/x509/extension.lua
new file mode 100644
index 0000000..ca23158
--- /dev/null
+++ b/server/resty/openssl/x509/extension.lua
@@ -0,0 +1,281 @@
+local ffi = require "ffi"
+local C = ffi.C
+local ffi_gc = ffi.gc
+local ffi_new = ffi.new
+local ffi_cast = ffi.cast
+local ffi_str = ffi.string
+
+require "resty.openssl.include.x509"
+require "resty.openssl.include.x509.extension"
+require "resty.openssl.include.x509v3"
+require "resty.openssl.include.bio"
+require "resty.openssl.include.conf"
+local asn1_macro = require("resty.openssl.include.asn1")
+local objects_lib = require "resty.openssl.objects"
+local stack_lib = require("resty.openssl.stack")
+local bio_util = require "resty.openssl.auxiliary.bio"
+local format_error = require("resty.openssl.err").format_error
+local OPENSSL_3X = require("resty.openssl.version").OPENSSL_3X
+local BORINGSSL = require("resty.openssl.version").BORINGSSL
+
+local _M = {}
+local mt = { __index = _M }
+
+local x509_extension_ptr_ct = ffi.typeof("X509_EXTENSION*")
+
+local extension_types = {
+ issuer = "resty.openssl.x509",
+ subject = "resty.openssl.x509",
+ request = "resty.openssl.x509.csr",
+ crl = "resty.openssl.x509.crl",
+}
+
+if OPENSSL_3X then
+ extension_types["issuer_pkey"] = "resty.openssl.pkey"
+end
+
+local nconf_load
+if BORINGSSL then
+ nconf_load = function()
+ return nil, "NCONF_load_bio not exported in BoringSSL"
+ end
+else
+ nconf_load = function(conf, str)
+ local bio = C.BIO_new_mem_buf(str, #str)
+ if bio == nil then
+ return format_error("BIO_new_mem_buf")
+ end
+ ffi_gc(bio, C.BIO_free)
+
+ if C.NCONF_load_bio(conf, bio, nil) ~= 1 then
+ return format_error("NCONF_load_bio")
+ end
+ end
+end
+
+function _M.new(txtnid, value, data)
+ local nid, err = objects_lib.txtnid2nid(txtnid)
+ if err then
+ return nil, "x509.extension.new: " .. err
+ end
+ if type(value) ~= 'string' then
+ return nil, "x509.extension.new: expect string at #2"
+ end
+ -- get a ptr and also zerofill the struct
+ local x509_ctx_ptr = ffi_new('X509V3_CTX[1]')
+
+ local conf = C.NCONF_new(nil)
+ if conf == nil then
+ return nil, format_error("NCONF_new")
+ end
+ ffi_gc(conf, C.NCONF_free)
+
+ if type(data) == 'table' then
+ local args = {}
+ if data.db then
+ if type(data.db) ~= 'string' then
+ return nil, "x509.extension.new: expect data.db must be a string"
+ end
+ err = nconf_load(conf, data)
+ if err then
+ return nil, "x509.extension.new: " .. err
+ end
+ end
+
+ for k, t in pairs(extension_types) do
+ if data[k] then
+ local lib = require(t)
+ if not lib.istype(data[k]) then
+ return nil, "x509.extension.new: expect data." .. k .. " to be a " .. t .. " instance"
+ end
+ args[k] = data[k].ctx
+ end
+ end
+ C.X509V3_set_ctx(x509_ctx_ptr[0], args.issuer, args.subject, args.request, args.crl, 0)
+
+ if OPENSSL_3X and args.issuer_pkey then
+ if C.X509V3_set_issuer_pkey(x509_ctx_ptr[0], args.issuer_pkey) ~= 1 then
+ return nil, format_error("x509.extension.new: X509V3_set_issuer_pkey")
+ end
+ end
+
+ elseif type(data) == 'string' then
+ err = nconf_load(conf, data)
+ if err then
+ return nil, "x509.extension.new: " .. err
+ end
+ elseif data then
+ return nil, "x509.extension.new: expect nil, string a table at #3"
+ end
+
+ -- setting conf is required for some extensions to load
+ -- crypto/x509/v3_conf.c:do_ext_conf "else if (method->r2i) {" branch
+ C.X509V3_set_nconf(x509_ctx_ptr[0], conf)
+
+ local ctx = C.X509V3_EXT_nconf_nid(conf, x509_ctx_ptr[0], nid, value)
+ if ctx == nil then
+ return nil, format_error("x509.extension.new")
+ end
+ ffi_gc(ctx, C.X509_EXTENSION_free)
+
+ local self = setmetatable({
+ ctx = ctx,
+ }, mt)
+
+ return self, nil
+end
+
+function _M.istype(l)
+ return l and l.ctx and ffi.istype(x509_extension_ptr_ct, l.ctx)
+end
+
+function _M.dup(ctx)
+ if not ffi.istype(x509_extension_ptr_ct, ctx) then
+ return nil, "x509.extension.dup: expect a x509.extension ctx at #1"
+ end
+ local ctx = C.X509_EXTENSION_dup(ctx)
+ if ctx == nil then
+ return nil, "x509.extension.dup: X509_EXTENSION_dup() failed"
+ end
+
+ ffi_gc(ctx, C.X509_EXTENSION_free)
+
+ local self = setmetatable({
+ ctx = ctx,
+ }, mt)
+
+ return self, nil
+end
+
+function _M.from_der(value, txtnid, crit)
+ local nid, err = objects_lib.txtnid2nid(txtnid)
+ if err then
+ return nil, "x509.extension.from_der: " .. err
+ end
+ if type(value) ~= 'string' then
+ return nil, "x509.extension.from_der: expect string at #1"
+ end
+
+ local asn1 = C.ASN1_STRING_new()
+ if asn1 == nil then
+ return nil, format_error("x509.extension.from_der: ASN1_STRING_new")
+ end
+ ffi_gc(asn1, C.ASN1_STRING_free)
+
+ if C.ASN1_STRING_set(asn1, value, #value) ~= 1 then
+ return nil, format_error("x509.extension.from_der: ASN1_STRING_set")
+ end
+
+ local ctx = C.X509_EXTENSION_create_by_NID(nil, nid, crit and 1 or 0, asn1)
+ if ctx == nil then
+ return nil, format_error("x509.extension.from_der: X509_EXTENSION_create_by_NID")
+ end
+ ffi_gc(ctx, C.X509_EXTENSION_free)
+
+ local self = setmetatable({
+ ctx = ctx,
+ }, mt)
+
+ return self, nil
+end
+
+function _M:to_der()
+ local asn1 = C.X509_EXTENSION_get_data(self.ctx)
+
+ return ffi_str(asn1_macro.ASN1_STRING_get0_data(asn1))
+end
+
+function _M.from_data(any, txtnid, crit)
+ local nid, err = objects_lib.txtnid2nid(txtnid)
+ if err then
+ return nil, "x509.extension.from_der: " .. err
+ end
+
+ if type(any) ~= "table" or type(any.ctx) ~= "cdata" then
+ return nil, "x509.extension.from_data: expect a table with ctx at #1"
+ elseif type(nid) ~= "number" then
+ return nil, "x509.extension.from_data: expect a table at #2"
+ end
+
+ local ctx = C.X509V3_EXT_i2d(nid, crit and 1 or 0, any.ctx)
+ if ctx == nil then
+ return nil, format_error("x509.extension.from_data: X509V3_EXT_i2d")
+ end
+ ffi_gc(ctx, C.X509_EXTENSION_free)
+
+ local self = setmetatable({
+ ctx = ctx,
+ }, mt)
+
+ return self, nil
+end
+
+local NID_subject_alt_name = C.OBJ_sn2nid("subjectAltName")
+assert(NID_subject_alt_name ~= 0)
+
+function _M.to_data(extension, nid)
+ if not _M.istype(extension) then
+ return nil, "x509.extension.dup: expect a x509.extension ctx at #1"
+ elseif type(nid) ~= "number" then
+ return nil, "x509.extension.to_data: expect a table at #2"
+ end
+
+ local void_ptr = C.X509V3_EXT_d2i(extension.ctx)
+ if void_ptr == nil then
+ return nil, format_error("x509.extension:to_data: X509V3_EXT_d2i")
+ end
+
+ if nid == NID_subject_alt_name then
+ -- Note: here we only free the stack itself not elements
+ -- since there seems no way to increase ref count for a GENERAL_NAME
+ -- we left the elements referenced by the new-dup'ed stack
+ ffi_gc(void_ptr, stack_lib.gc_of("GENERAL_NAME"))
+ local got = ffi_cast("GENERAL_NAMES*", void_ptr)
+ local lib = require("resty.openssl.x509.altname")
+ -- the internal ptr is returned, ie we need to copy it
+ return lib.dup(got)
+ end
+
+ return nil, string.format("x509.extension:to_data: don't know how to convert to NID %d", nid)
+end
+
+function _M:get_object()
+ -- retruns the internal pointer
+ local asn1 = C.X509_EXTENSION_get_object(self.ctx)
+
+ return objects_lib.obj2table(asn1)
+end
+
+function _M:get_critical()
+ return C.X509_EXTENSION_get_critical(self.ctx) == 1
+end
+
+function _M:set_critical(crit)
+ if C.X509_EXTENSION_set_critical(self.ctx, crit and 1 or 0) ~= 1 then
+ return false, format_error("x509.extension:set_critical")
+ end
+ return true
+end
+
+function _M:tostring()
+ local ret, err = bio_util.read_wrap(C.X509V3_EXT_print, self.ctx, 0, 0)
+ if not err then
+ return ret
+ end
+ -- fallback to ASN.1 print
+ local asn1 = C.X509_EXTENSION_get_data(self.ctx)
+ return bio_util.read_wrap(C.ASN1_STRING_print, asn1)
+end
+
+_M.text = _M.tostring
+
+mt.__tostring = function(tbl)
+ local txt, err = _M.text(tbl)
+ if err then
+ error(err)
+ end
+ return txt
+end
+
+
+return _M
diff --git a/server/resty/openssl/x509/extension/dist_points.lua b/server/resty/openssl/x509/extension/dist_points.lua
new file mode 100644
index 0000000..b1d419b
--- /dev/null
+++ b/server/resty/openssl/x509/extension/dist_points.lua
@@ -0,0 +1,75 @@
+local ffi = require "ffi"
+
+require "resty.openssl.include.x509"
+require "resty.openssl.include.x509v3"
+local altname_lib = require "resty.openssl.x509.altname"
+local stack_lib = require "resty.openssl.stack"
+
+local _M = {}
+
+local stack_ptr_ct = ffi.typeof("OPENSSL_STACK*")
+
+local STACK = "DIST_POINT"
+local new = stack_lib.new_of(STACK)
+local dup = stack_lib.dup_of(STACK)
+
+-- TODO: return other attributes?
+local cdp_decode_fullname = function(ctx)
+ return altname_lib.dup(ctx.distpoint.name.fullname)
+end
+
+local mt = stack_lib.mt_of(STACK, cdp_decode_fullname, _M)
+
+function _M.new()
+ local ctx = new()
+ if ctx == nil then
+ return nil, "OPENSSL_sk_new_null() failed"
+ end
+
+ local self = setmetatable({
+ ctx = ctx,
+ _is_shallow_copy = false,
+ }, mt)
+
+ return self, nil
+end
+
+function _M.istype(l)
+ return l and l.cast and ffi.istype(stack_ptr_ct, l.cast)
+end
+
+function _M.dup(ctx)
+ if ctx == nil or not ffi.istype(stack_ptr_ct, ctx) then
+ return nil, "expect a stack ctx at #1"
+ end
+ local dup_ctx = dup(ctx)
+
+ return setmetatable({
+ ctx = dup_ctx,
+ -- don't let lua gc the original stack to keep its elements
+ _dupped_from = ctx,
+ _is_shallow_copy = true,
+ _elem_refs = {},
+ _elem_refs_idx = 1,
+ }, mt), nil
+end
+
+_M.all = function(stack)
+ local ret = {}
+ local _next = mt.__ipairs(stack)
+ while true do
+ local i, e = _next()
+ if i then
+ ret[i] = e
+ else
+ break
+ end
+ end
+ return ret
+end
+
+_M.each = mt.__ipairs
+_M.index = mt.__index
+_M.count = mt.__len
+
+return _M
diff --git a/server/resty/openssl/x509/extension/info_access.lua b/server/resty/openssl/x509/extension/info_access.lua
new file mode 100644
index 0000000..21025a8
--- /dev/null
+++ b/server/resty/openssl/x509/extension/info_access.lua
@@ -0,0 +1,137 @@
+local ffi = require "ffi"
+local C = ffi.C
+local ffi_gc = ffi.gc
+local ffi_cast = ffi.cast
+
+require "resty.openssl.include.x509"
+require "resty.openssl.include.x509v3"
+require "resty.openssl.include.err"
+local altname_lib = require "resty.openssl.x509.altname"
+local stack_lib = require "resty.openssl.stack"
+
+local _M = {}
+
+local authority_info_access_ptr_ct = ffi.typeof("AUTHORITY_INFO_ACCESS*")
+
+local STACK = "ACCESS_DESCRIPTION"
+local new = stack_lib.new_of(STACK)
+local add = stack_lib.add_of(STACK)
+local dup = stack_lib.dup_of(STACK)
+
+local aia_decode = function(ctx)
+ local nid = C.OBJ_obj2nid(ctx.method)
+ local gn = altname_lib.gn_decode(ctx.location)
+ return { nid, unpack(gn) }
+end
+
+local mt = stack_lib.mt_of(STACK, aia_decode, _M)
+local mt__pairs = mt.__pairs
+mt.__pairs = function(tbl)
+ local f = mt__pairs(tbl)
+ return function()
+ local _, e = f()
+ if not e then return end
+ return unpack(e)
+ end
+end
+
+function _M.new()
+ local ctx = new()
+ if ctx == nil then
+ return nil, "OPENSSL_sk_new_null() failed"
+ end
+ local cast = ffi_cast("AUTHORITY_INFO_ACCESS*", ctx)
+
+ local self = setmetatable({
+ ctx = ctx,
+ cast = cast,
+ _is_shallow_copy = false,
+ }, mt)
+
+ return self, nil
+end
+
+function _M.istype(l)
+ return l and l.cast and ffi.istype(authority_info_access_ptr_ct, l.cast)
+end
+
+function _M.dup(ctx)
+ if ctx == nil or not ffi.istype(authority_info_access_ptr_ct, ctx) then
+ return nil, "expect a AUTHORITY_INFO_ACCESS* ctx at #1"
+ end
+ local dup_ctx = dup(ctx)
+
+ return setmetatable({
+ ctx = dup_ctx,
+ cast = ffi_cast("AUTHORITY_INFO_ACCESS*", dup_ctx),
+ -- don't let lua gc the original stack to keep its elements
+ _dupped_from = ctx,
+ _is_shallow_copy = true,
+ _elem_refs = {},
+ _elem_refs_idx = 1,
+ }, mt), nil
+end
+
+function _M:add(nid, typ, value)
+ -- the stack element stays with stack
+ -- we shouldn't add gc handler if it's already been
+ -- pushed to stack. instead, rely on the gc handler
+ -- of the stack to release all memories
+ local ad = C.ACCESS_DESCRIPTION_new()
+ if ad == nil then
+ return nil, "ACCESS_DESCRIPTION_new() failed"
+ end
+
+ -- C.ASN1_OBJECT_free(ad.method)
+
+ local asn1 = C.OBJ_txt2obj(nid, 0)
+ if asn1 == nil then
+ C.ACCESS_DESCRIPTION_free(ad)
+ -- clean up error occurs during OBJ_txt2*
+ C.ERR_clear_error()
+ return nil, "invalid NID text " .. (nid or "nil")
+ end
+
+ ad.method = asn1
+
+ local err = altname_lib.gn_set(ad.location, typ, value)
+ if err then
+ C.ACCESS_DESCRIPTION_free(ad)
+ return nil, err
+ end
+
+ local _, err = add(self.ctx, ad)
+ if err then
+ C.ACCESS_DESCRIPTION_free(ad)
+ return nil, err
+ end
+
+ -- if the stack is duplicated, the gc handler is not pop_free
+ -- handle the gc by ourselves
+ if self._is_shallow_copy then
+ ffi_gc(ad, C.ACCESS_DESCRIPTION_free)
+ self._elem_refs[self._elem_refs_idx] = ad
+ self._elem_refs_idx = self._elem_refs_idx + 1
+ end
+ return self
+end
+
+_M.all = function(stack)
+ local ret = {}
+ local _next = mt.__ipairs(stack)
+ while true do
+ local i, e = _next()
+ if i then
+ ret[i] = e
+ else
+ break
+ end
+ end
+ return ret
+end
+
+_M.each = mt.__ipairs
+_M.index = mt.__index
+_M.count = mt.__len
+
+return _M
diff --git a/server/resty/openssl/x509/extensions.lua b/server/resty/openssl/x509/extensions.lua
new file mode 100644
index 0000000..3b64b8a
--- /dev/null
+++ b/server/resty/openssl/x509/extensions.lua
@@ -0,0 +1,84 @@
+local ffi = require "ffi"
+local C = ffi.C
+local ffi_gc = ffi.gc
+
+local stack_lib = require "resty.openssl.stack"
+local extension_lib = require "resty.openssl.x509.extension"
+local format_error = require("resty.openssl.err").format_error
+
+local _M = {}
+
+local stack_ptr_ct = ffi.typeof("OPENSSL_STACK*")
+
+local STACK = "X509_EXTENSION"
+local new = stack_lib.new_of(STACK)
+local add = stack_lib.add_of(STACK)
+local dup = stack_lib.dup_of(STACK)
+local mt = stack_lib.mt_of(STACK, extension_lib.dup, _M)
+
+function _M.new()
+ local raw = new()
+
+ local self = setmetatable({
+ stack_of = STACK,
+ ctx = raw,
+ }, mt)
+
+ return self, nil
+end
+
+function _M.istype(l)
+ return l and l.ctx and ffi.istype(stack_ptr_ct, l.ctx)
+ and l.stack_of and l.stack_of == STACK
+end
+
+function _M.dup(ctx)
+ if ctx == nil or not ffi.istype(stack_ptr_ct, ctx) then
+ return nil, "x509.extensions.dup: expect a stack ctx at #1, got " .. type(ctx)
+ end
+
+ local dup_ctx = dup(ctx)
+
+ return setmetatable({
+ ctx = dup_ctx,
+ -- don't let lua gc the original stack to keep its elements
+ _dupped_from = ctx,
+ _is_shallow_copy = true,
+ _elem_refs = {},
+ _elem_refs_idx = 1,
+ }, mt), nil
+end
+
+function _M:add(extension)
+ if not extension_lib.istype(extension) then
+ return nil, "expect a x509.extension instance at #1"
+ end
+
+ local dup = C.X509_EXTENSION_dup(extension.ctx)
+ if dup == nil then
+ return nil, format_error("extensions:add: X509_EXTENSION_dup")
+ end
+
+ local _, err = add(self.ctx, dup)
+ if err then
+ C.X509_EXTENSION_free(dup)
+ return nil, err
+ end
+
+ -- if the stack is duplicated, the gc handler is not pop_free
+ -- handle the gc by ourselves
+ if self._is_shallow_copy then
+ ffi_gc(dup, C.X509_EXTENSION_free)
+ self._elem_refs[self._elem_refs_idx] = dup
+ self._elem_refs_idx = self._elem_refs_idx + 1
+ end
+
+ return true
+end
+
+_M.all = stack_lib.all_func(mt)
+_M.each = mt.__ipairs
+_M.index = mt.__index
+_M.count = mt.__len
+
+return _M
diff --git a/server/resty/openssl/x509/init.lua b/server/resty/openssl/x509/init.lua
new file mode 100644
index 0000000..5c259c8
--- /dev/null
+++ b/server/resty/openssl/x509/init.lua
@@ -0,0 +1,1071 @@
+local ffi = require "ffi"
+local C = ffi.C
+local ffi_gc = ffi.gc
+local ffi_str = ffi.string
+local ffi_cast = ffi.cast
+
+require "resty.openssl.include.x509"
+require "resty.openssl.include.x509v3"
+require "resty.openssl.include.evp"
+require "resty.openssl.include.objects"
+local stack_macro = require("resty.openssl.include.stack")
+local stack_lib = require("resty.openssl.stack")
+local asn1_lib = require("resty.openssl.asn1")
+local digest_lib = require("resty.openssl.digest")
+local extension_lib = require("resty.openssl.x509.extension")
+local pkey_lib = require("resty.openssl.pkey")
+local bio_util = require "resty.openssl.auxiliary.bio"
+local txtnid2nid = require("resty.openssl.objects").txtnid2nid
+local find_sigid_algs = require("resty.openssl.objects").find_sigid_algs
+local ctypes = require "resty.openssl.auxiliary.ctypes"
+local ctx_lib = require "resty.openssl.ctx"
+local format_error = require("resty.openssl.err").format_error
+local version = require("resty.openssl.version")
+local OPENSSL_10 = version.OPENSSL_10
+local OPENSSL_11_OR_LATER = version.OPENSSL_11_OR_LATER
+local OPENSSL_3X = version.OPENSSL_3X
+local BORINGSSL = version.BORINGSSL
+local BORINGSSL_110 = version.BORINGSSL_110 -- used in boringssl-fips-20190808
+
+-- accessors provides an openssl version neutral interface to lua layer
+-- it doesn't handle any error, expect that to be implemented in
+-- _M.set_X or _M.get_X
+local accessors = {}
+
+accessors.get_pubkey = C.X509_get_pubkey -- returns new evp_pkey instance, don't need to dup
+accessors.set_pubkey = C.X509_set_pubkey
+accessors.set_version = C.X509_set_version
+accessors.set_serial_number = C.X509_set_serialNumber
+accessors.get_subject_name = C.X509_get_subject_name -- returns internal ptr, we dup it
+accessors.set_subject_name = C.X509_set_subject_name
+accessors.get_issuer_name = C.X509_get_issuer_name -- returns internal ptr, we dup it
+accessors.set_issuer_name = C.X509_set_issuer_name
+accessors.get_signature_nid = C.X509_get_signature_nid
+
+-- generally, use get1 if we return a lua table wrapped ctx which doesn't support dup.
+-- in that case, a new struct is returned from C api, and we will handle gc.
+-- openssl will increment the reference count for returned ptr, and won't free it when
+-- parent struct is freed.
+-- otherwise, use get0, which returns an internal pointer, we don't need to free it up.
+-- it will be gone together with the parent struct.
+
+if BORINGSSL_110 then
+ accessors.get_not_before = C.X509_get0_notBefore -- returns internal ptr, we convert to number
+ accessors.set_not_before = C.X509_set_notBefore
+ accessors.get_not_after = C.X509_get0_notAfter -- returns internal ptr, we convert to number
+ accessors.set_not_after = C.X509_set_notAfter
+ accessors.get_version = function(x509)
+ if x509 == nil or x509.cert_info == nil or x509.cert_info.validity == nil then
+ return nil
+ end
+ return C.ASN1_INTEGER_get(x509.cert_info.version)
+ end
+ accessors.get_serial_number = C.X509_get_serialNumber -- returns internal ptr, we convert to bn
+elseif OPENSSL_11_OR_LATER then
+ accessors.get_not_before = C.X509_get0_notBefore -- returns internal ptr, we convert to number
+ accessors.set_not_before = C.X509_set1_notBefore
+ accessors.get_not_after = C.X509_get0_notAfter -- returns internal ptr, we convert to number
+ accessors.set_not_after = C.X509_set1_notAfter
+ accessors.get_version = C.X509_get_version -- returns int
+ accessors.get_serial_number = C.X509_get0_serialNumber -- returns internal ptr, we convert to bn
+elseif OPENSSL_10 then
+ accessors.get_not_before = function(x509)
+ if x509 == nil or x509.cert_info == nil or x509.cert_info.validity == nil then
+ return nil
+ end
+ return x509.cert_info.validity.notBefore
+ end
+ accessors.set_not_before = C.X509_set_notBefore
+ accessors.get_not_after = function(x509)
+ if x509 == nil or x509.cert_info == nil or x509.cert_info.validity == nil then
+ return nil
+ end
+ return x509.cert_info.validity.notAfter
+ end
+ accessors.set_not_after = C.X509_set_notAfter
+ accessors.get_version = function(x509)
+ if x509 == nil or x509.cert_info == nil or x509.cert_info.validity == nil then
+ return nil
+ end
+ return C.ASN1_INTEGER_get(x509.cert_info.version)
+ end
+ accessors.get_serial_number = C.X509_get_serialNumber -- returns internal ptr, we convert to bn
+end
+
+local function __tostring(self, fmt)
+ if not fmt or fmt == 'PEM' then
+ return bio_util.read_wrap(C.PEM_write_bio_X509, self.ctx)
+ elseif fmt == 'DER' then
+ return bio_util.read_wrap(C.i2d_X509_bio, self.ctx)
+ else
+ return nil, "x509:tostring: can only write PEM or DER format, not " .. fmt
+ end
+end
+
+local _M = {}
+local mt = { __index = _M, __tostring = __tostring }
+
+
+local x509_ptr_ct = ffi.typeof("X509*")
+
+-- only PEM format is supported for now
+function _M.new(cert, fmt, properties)
+ local ctx
+ if not cert then
+ -- routine for create a new cert
+ if OPENSSL_3X then
+ ctx = C.X509_new_ex(ctx_lib.get_libctx(), properties)
+ else
+ ctx = C.X509_new()
+ end
+ if ctx == nil then
+ return nil, format_error("x509.new")
+ end
+ ffi_gc(ctx, C.X509_free)
+
+ C.X509_gmtime_adj(accessors.get_not_before(ctx), 0)
+ C.X509_gmtime_adj(accessors.get_not_after(ctx), 0)
+ elseif type(cert) == "string" then
+ -- routine for load an existing cert
+ local bio = C.BIO_new_mem_buf(cert, #cert)
+ if bio == nil then
+ return nil, format_error("x509.new: BIO_new_mem_buf")
+ end
+
+ fmt = fmt or "*"
+ while true do -- luacheck: ignore 512 -- loop is executed at most once
+ if fmt == "PEM" or fmt == "*" then
+ ctx = C.PEM_read_bio_X509(bio, nil, nil, nil)
+ if ctx ~= nil then
+ break
+ elseif fmt == "*" then
+ -- BIO_reset; #define BIO_CTRL_RESET 1
+ local code = C.BIO_ctrl(bio, 1, 0, nil)
+ if code ~= 1 then
+ C.BIO_free(bio)
+ return nil, "x509.new: BIO_ctrl() failed: " .. code
+ end
+ end
+ end
+ if fmt == "DER" or fmt == "*" then
+ ctx = C.d2i_X509_bio(bio, nil)
+ end
+ break
+ end
+ C.BIO_free(bio)
+ if ctx == nil then
+ return nil, format_error("x509.new")
+ end
+ -- clear errors occur when trying
+ C.ERR_clear_error()
+ ffi_gc(ctx, C.X509_free)
+ elseif type(cert) == 'cdata' then
+ if ffi.istype(x509_ptr_ct, cert) then
+ ctx = cert
+ ffi_gc(ctx, C.X509_free)
+ else
+ return nil, "x509.new: expect a X509* cdata at #1"
+ end
+ else
+ return nil, "x509.new: expect nil or a string at #1"
+ end
+
+ local self = setmetatable({
+ ctx = ctx,
+ }, mt)
+
+ return self, nil
+end
+
+function _M.istype(l)
+ return l and l.ctx and ffi.istype(x509_ptr_ct, l.ctx)
+end
+
+function _M.dup(ctx)
+ if not ffi.istype(x509_ptr_ct, ctx) then
+ return nil, "x509.dup: expect a x509 ctx at #1"
+ end
+ local ctx = C.X509_dup(ctx)
+ if ctx == nil then
+ return nil, "x509.dup: X509_dup() failed"
+ end
+
+ ffi_gc(ctx, C.X509_free)
+
+ local self = setmetatable({
+ ctx = ctx,
+ }, mt)
+
+ return self, nil
+end
+
+function _M:tostring(fmt)
+ return __tostring(self, fmt)
+end
+
+function _M:to_PEM()
+ return __tostring(self, "PEM")
+end
+
+function _M:set_lifetime(not_before, not_after)
+ local ok, err
+ if not_before then
+ ok, err = self:set_not_before(not_before)
+ if err then
+ return ok, err
+ end
+ end
+
+ if not_after then
+ ok, err = self:set_not_after(not_after)
+ if err then
+ return ok, err
+ end
+ end
+
+ return true
+end
+
+function _M:get_lifetime()
+ local not_before, err = self:get_not_before()
+ if not_before == nil then
+ return nil, nil, err
+ end
+ local not_after, err = self:get_not_after()
+ if not_after == nil then
+ return nil, nil, err
+ end
+
+ return not_before, not_after, nil
+end
+
+-- note: index is 0 based
+local OPENSSL_STRING_value_at = function(ctx, i)
+ local ct = ffi_cast("OPENSSL_STRING", stack_macro.OPENSSL_sk_value(ctx, i))
+ if ct == nil then
+ return nil
+ end
+ return ffi_str(ct)
+end
+
+function _M:get_ocsp_url(return_all)
+ local st = C.X509_get1_ocsp(self.ctx)
+
+ local count = stack_macro.OPENSSL_sk_num(st)
+ if count == 0 then
+ return
+ end
+
+ local ret
+ if return_all then
+ ret = {}
+ for i=0,count-1 do
+ ret[i+1] = OPENSSL_STRING_value_at(st, i)
+ end
+ else
+ ret = OPENSSL_STRING_value_at(st, 0)
+ end
+
+ C.X509_email_free(st)
+ return ret
+end
+
+function _M:get_ocsp_request()
+
+end
+
+function _M:get_crl_url(return_all)
+ local cdp, err = self:get_crl_distribution_points()
+ if err then
+ return nil, err
+ end
+
+ if not cdp or cdp:count() == 0 then
+ return
+ end
+
+ if return_all then
+ local ret = {}
+ local cdp_iter = cdp:each()
+ while true do
+ local _, gn = cdp_iter()
+ if not gn then
+ break
+ end
+ local gn_iter = gn:each()
+ while true do
+ local k, v = gn_iter()
+ if not k then
+ break
+ elseif k == "URI" then
+ table.insert(ret, v)
+ end
+ end
+ end
+ return ret
+ else
+ local gn, err = cdp:index(1)
+ if err then
+ return nil, err
+ end
+ local iter = gn:each()
+ while true do
+ local k, v = iter()
+ if not k then
+ break
+ elseif k == "URI" then
+ return v
+ end
+ end
+ end
+end
+
+local digest_length = ctypes.ptr_of_uint()
+local digest_buf, digest_buf_size
+local function digest(self, cfunc, typ, properties)
+ -- TODO: dedup the following with resty.openssl.digest
+ local ctx
+ if OPENSSL_11_OR_LATER then
+ ctx = C.EVP_MD_CTX_new()
+ ffi_gc(ctx, C.EVP_MD_CTX_free)
+ elseif OPENSSL_10 then
+ ctx = C.EVP_MD_CTX_create()
+ ffi_gc(ctx, C.EVP_MD_CTX_destroy)
+ end
+ if ctx == nil then
+ return nil, "x509:digest: failed to create EVP_MD_CTX"
+ end
+
+ local algo
+ if OPENSSL_3X then
+ algo = C.EVP_MD_fetch(ctx_lib.get_libctx(), typ or 'sha1', properties)
+ else
+ algo = C.EVP_get_digestbyname(typ or 'sha1')
+ end
+ if algo == nil then
+ return nil, string.format("x509:digest: invalid digest type \"%s\"", typ)
+ end
+
+ local md_size = OPENSSL_3X and C.EVP_MD_get_size(algo) or C.EVP_MD_size(algo)
+ if not digest_buf or digest_buf_size < md_size then
+ digest_buf = ctypes.uchar_array(md_size)
+ digest_buf_size = md_size
+ end
+
+ if cfunc(self.ctx, algo, digest_buf, digest_length) ~= 1 then
+ return nil, format_error("x509:digest")
+ end
+
+ return ffi_str(digest_buf, digest_length[0])
+end
+
+function _M:digest(typ, properties)
+ return digest(self, C.X509_digest, typ, properties)
+end
+
+function _M:pubkey_digest(typ, properties)
+ return digest(self, C.X509_pubkey_digest, typ, properties)
+end
+
+function _M:check_private_key(key)
+ if not pkey_lib.istype(key) then
+ return false, "x509:check_private_key: except a pkey instance at #1"
+ end
+
+ if not key:is_private() then
+ return false, "x509:check_private_key: not a private key"
+ end
+
+ if C.X509_check_private_key(self.ctx, key.ctx) == 1 then
+ return true
+ end
+ return false, format_error("x509:check_private_key")
+end
+
+-- START AUTO GENERATED CODE
+
+-- AUTO GENERATED
+function _M:sign(pkey, digest)
+ if not pkey_lib.istype(pkey) then
+ return false, "x509:sign: expect a pkey instance at #1"
+ end
+
+ local digest_algo
+ if digest then
+ if not digest_lib.istype(digest) then
+ return false, "x509:sign: expect a digest instance at #2"
+ elseif not digest.algo then
+ return false, "x509:sign: expect a digest instance to have algo member"
+ end
+ digest_algo = digest.algo
+ elseif BORINGSSL then
+ digest_algo = C.EVP_get_digestbyname('sha256')
+ end
+
+ -- returns size of signature if success
+ if C.X509_sign(self.ctx, pkey.ctx, digest_algo) == 0 then
+ return false, format_error("x509:sign")
+ end
+
+ return true
+end
+
+-- AUTO GENERATED
+function _M:verify(pkey)
+ if not pkey_lib.istype(pkey) then
+ return false, "x509:verify: expect a pkey instance at #1"
+ end
+
+ local code = C.X509_verify(self.ctx, pkey.ctx)
+ if code == 1 then
+ return true
+ elseif code == 0 then
+ return false
+ else -- typically -1
+ return false, format_error("x509:verify", code)
+ end
+end
+
+-- AUTO GENERATED
+local function get_extension(ctx, nid_txt, last_pos)
+ last_pos = (last_pos or 0) - 1
+ local nid, err = txtnid2nid(nid_txt)
+ if err then
+ return nil, nil, err
+ end
+ local pos = C.X509_get_ext_by_NID(ctx, nid, last_pos)
+ if pos == -1 then
+ return nil
+ end
+ local ctx = C.X509_get_ext(ctx, pos)
+ if ctx == nil then
+ return nil, nil, format_error()
+ end
+ return ctx, pos
+end
+
+-- AUTO GENERATED
+function _M:add_extension(extension)
+ if not extension_lib.istype(extension) then
+ return false, "x509:add_extension: expect a x509.extension instance at #1"
+ end
+
+ -- X509_add_ext returnes the stack on success, and NULL on error
+ -- the X509_EXTENSION ctx is dupped internally
+ if C.X509_add_ext(self.ctx, extension.ctx, -1) == nil then
+ return false, format_error("x509:add_extension")
+ end
+
+ return true
+end
+
+-- AUTO GENERATED
+function _M:get_extension(nid_txt, last_pos)
+ local ctx, pos, err = get_extension(self.ctx, nid_txt, last_pos)
+ if err then
+ return nil, nil, "x509:get_extension: " .. err
+ end
+ local ext, err = extension_lib.dup(ctx)
+ if err then
+ return nil, nil, "x509:get_extension: " .. err
+ end
+ return ext, pos+1
+end
+
+local X509_delete_ext
+if OPENSSL_11_OR_LATER then
+ X509_delete_ext = C.X509_delete_ext
+elseif OPENSSL_10 then
+ X509_delete_ext = function(ctx, pos)
+ return C.X509v3_delete_ext(ctx.cert_info.extensions, pos)
+ end
+else
+ X509_delete_ext = function(...)
+ error("X509_delete_ext undefined")
+ end
+end
+
+-- AUTO GENERATED
+function _M:set_extension(extension, last_pos)
+ if not extension_lib.istype(extension) then
+ return false, "x509:set_extension: expect a x509.extension instance at #1"
+ end
+
+ last_pos = (last_pos or 0) - 1
+
+ local nid = extension:get_object().nid
+ local pos = C.X509_get_ext_by_NID(self.ctx, nid, last_pos)
+ -- pos may be -1, which means not found, it's fine, we will add new one instead of replace
+
+ local removed = X509_delete_ext(self.ctx, pos)
+ C.X509_EXTENSION_free(removed)
+
+ if C.X509_add_ext(self.ctx, extension.ctx, pos) == nil then
+ return false, format_error("x509:set_extension")
+ end
+
+ return true
+end
+
+-- AUTO GENERATED
+function _M:set_extension_critical(nid_txt, crit, last_pos)
+ local ctx, _, err = get_extension(self.ctx, nid_txt, last_pos)
+ if err then
+ return nil, "x509:set_extension_critical: " .. err
+ end
+
+ if C.X509_EXTENSION_set_critical(ctx, crit and 1 or 0) ~= 1 then
+ return false, format_error("x509:set_extension_critical")
+ end
+
+ return true
+end
+
+-- AUTO GENERATED
+function _M:get_extension_critical(nid_txt, last_pos)
+ local ctx, _, err = get_extension(self.ctx, nid_txt, last_pos)
+ if err then
+ return nil, "x509:get_extension_critical: " .. err
+ end
+
+ return C.X509_EXTENSION_get_critical(ctx) == 1
+end
+
+-- AUTO GENERATED
+function _M:get_serial_number()
+ local got = accessors.get_serial_number(self.ctx)
+ if got == nil then
+ return nil
+ end
+
+ -- returns a new BIGNUM instance
+ got = C.ASN1_INTEGER_to_BN(got, nil)
+ if got == nil then
+ return false, format_error("x509:set: BN_to_ASN1_INTEGER")
+ end
+ -- bn will be duplicated thus this ctx should be freed up
+ ffi_gc(got, C.BN_free)
+
+ local lib = require("resty.openssl.bn")
+ -- the internal ptr is returned, ie we need to copy it
+ return lib.dup(got)
+end
+
+-- AUTO GENERATED
+function _M:set_serial_number(toset)
+ local lib = require("resty.openssl.bn")
+ if lib.istype and not lib.istype(toset) then
+ return false, "x509:set_serial_number: expect a bn instance at #1"
+ end
+ toset = toset.ctx
+
+ toset = C.BN_to_ASN1_INTEGER(toset, nil)
+ if toset == nil then
+ return false, format_error("x509:set: BN_to_ASN1_INTEGER")
+ end
+ -- "A copy of the serial number is used internally
+ -- so serial should be freed up after use.""
+ ffi_gc(toset, C.ASN1_INTEGER_free)
+
+ if accessors.set_serial_number(self.ctx, toset) == 0 then
+ return false, format_error("x509:set_serial_number")
+ end
+ return true
+end
+
+-- AUTO GENERATED
+function _M:get_not_before()
+ local got = accessors.get_not_before(self.ctx)
+ if got == nil then
+ return nil
+ end
+
+ got = asn1_lib.asn1_to_unix(got)
+
+ return got
+end
+
+-- AUTO GENERATED
+function _M:set_not_before(toset)
+ if type(toset) ~= "number" then
+ return false, "x509:set_not_before: expect a number at #1"
+ end
+
+ toset = C.ASN1_TIME_set(nil, toset)
+ ffi_gc(toset, C.ASN1_STRING_free)
+
+ if accessors.set_not_before(self.ctx, toset) == 0 then
+ return false, format_error("x509:set_not_before")
+ end
+ return true
+end
+
+-- AUTO GENERATED
+function _M:get_not_after()
+ local got = accessors.get_not_after(self.ctx)
+ if got == nil then
+ return nil
+ end
+
+ got = asn1_lib.asn1_to_unix(got)
+
+ return got
+end
+
+-- AUTO GENERATED
+function _M:set_not_after(toset)
+ if type(toset) ~= "number" then
+ return false, "x509:set_not_after: expect a number at #1"
+ end
+
+ toset = C.ASN1_TIME_set(nil, toset)
+ ffi_gc(toset, C.ASN1_STRING_free)
+
+ if accessors.set_not_after(self.ctx, toset) == 0 then
+ return false, format_error("x509:set_not_after")
+ end
+ return true
+end
+
+-- AUTO GENERATED
+function _M:get_pubkey()
+ local got = accessors.get_pubkey(self.ctx)
+ if got == nil then
+ return nil
+ end
+ local lib = require("resty.openssl.pkey")
+ -- returned a copied instance directly
+ return lib.new(got)
+end
+
+-- AUTO GENERATED
+function _M:set_pubkey(toset)
+ local lib = require("resty.openssl.pkey")
+ if lib.istype and not lib.istype(toset) then
+ return false, "x509:set_pubkey: expect a pkey instance at #1"
+ end
+ toset = toset.ctx
+ if accessors.set_pubkey(self.ctx, toset) == 0 then
+ return false, format_error("x509:set_pubkey")
+ end
+ return true
+end
+
+-- AUTO GENERATED
+function _M:get_subject_name()
+ local got = accessors.get_subject_name(self.ctx)
+ if got == nil then
+ return nil
+ end
+ local lib = require("resty.openssl.x509.name")
+ -- the internal ptr is returned, ie we need to copy it
+ return lib.dup(got)
+end
+
+-- AUTO GENERATED
+function _M:set_subject_name(toset)
+ local lib = require("resty.openssl.x509.name")
+ if lib.istype and not lib.istype(toset) then
+ return false, "x509:set_subject_name: expect a x509.name instance at #1"
+ end
+ toset = toset.ctx
+ if accessors.set_subject_name(self.ctx, toset) == 0 then
+ return false, format_error("x509:set_subject_name")
+ end
+ return true
+end
+
+-- AUTO GENERATED
+function _M:get_issuer_name()
+ local got = accessors.get_issuer_name(self.ctx)
+ if got == nil then
+ return nil
+ end
+ local lib = require("resty.openssl.x509.name")
+ -- the internal ptr is returned, ie we need to copy it
+ return lib.dup(got)
+end
+
+-- AUTO GENERATED
+function _M:set_issuer_name(toset)
+ local lib = require("resty.openssl.x509.name")
+ if lib.istype and not lib.istype(toset) then
+ return false, "x509:set_issuer_name: expect a x509.name instance at #1"
+ end
+ toset = toset.ctx
+ if accessors.set_issuer_name(self.ctx, toset) == 0 then
+ return false, format_error("x509:set_issuer_name")
+ end
+ return true
+end
+
+-- AUTO GENERATED
+function _M:get_version()
+ local got = accessors.get_version(self.ctx)
+ if got == nil then
+ return nil
+ end
+
+ got = tonumber(got) + 1
+
+ return got
+end
+
+-- AUTO GENERATED
+function _M:set_version(toset)
+ if type(toset) ~= "number" then
+ return false, "x509:set_version: expect a number at #1"
+ end
+
+ -- Note: this is defined by standards (X.509 et al) to be one less than the certificate version.
+ -- So a version 3 certificate will return 2 and a version 1 certificate will return 0.
+ toset = toset - 1
+
+ if accessors.set_version(self.ctx, toset) == 0 then
+ return false, format_error("x509:set_version")
+ end
+ return true
+end
+
+local NID_subject_alt_name = C.OBJ_sn2nid("subjectAltName")
+assert(NID_subject_alt_name ~= 0)
+
+-- AUTO GENERATED: EXTENSIONS
+function _M:get_subject_alt_name()
+ local crit = ctypes.ptr_of_int()
+ -- X509_get_ext_d2i returns internal pointer, always dup
+ -- for now this function always returns the first found extension
+ local got = C.X509_get_ext_d2i(self.ctx, NID_subject_alt_name, crit, nil)
+ crit = tonumber(crit[0])
+ if crit == -1 then -- not found
+ return nil
+ elseif crit == -2 then
+ return nil, "x509:get_subject_alt_name: extension of subject_alt_name occurs more than one times, " ..
+ "this is not yet implemented. Please use get_extension instead."
+ elseif got == nil then
+ return nil, format_error("x509:get_subject_alt_name")
+ end
+
+ -- Note: here we only free the stack itself not elements
+ -- since there seems no way to increase ref count for a GENERAL_NAME
+ -- we left the elements referenced by the new-dup'ed stack
+ local got_ref = got
+ ffi_gc(got_ref, stack_lib.gc_of("GENERAL_NAME"))
+ got = ffi_cast("GENERAL_NAMES*", got_ref)
+ local lib = require("resty.openssl.x509.altname")
+ -- the internal ptr is returned, ie we need to copy it
+ return lib.dup(got)
+end
+
+-- AUTO GENERATED: EXTENSIONS
+function _M:set_subject_alt_name(toset)
+ local lib = require("resty.openssl.x509.altname")
+ if lib.istype and not lib.istype(toset) then
+ return false, "x509:set_subject_alt_name: expect a x509.altname instance at #1"
+ end
+ toset = toset.ctx
+ -- x509v3.h: # define X509V3_ADD_REPLACE 2L
+ if C.X509_add1_ext_i2d(self.ctx, NID_subject_alt_name, toset, 0, 0x2) ~= 1 then
+ return false, format_error("x509:set_subject_alt_name")
+ end
+ return true
+end
+
+-- AUTO GENERATED: EXTENSIONS
+function _M:set_subject_alt_name_critical(crit)
+ return _M.set_extension_critical(self, NID_subject_alt_name, crit)
+end
+
+-- AUTO GENERATED: EXTENSIONS
+function _M:get_subject_alt_name_critical()
+ return _M.get_extension_critical(self, NID_subject_alt_name)
+end
+
+local NID_issuer_alt_name = C.OBJ_sn2nid("issuerAltName")
+assert(NID_issuer_alt_name ~= 0)
+
+-- AUTO GENERATED: EXTENSIONS
+function _M:get_issuer_alt_name()
+ local crit = ctypes.ptr_of_int()
+ -- X509_get_ext_d2i returns internal pointer, always dup
+ -- for now this function always returns the first found extension
+ local got = C.X509_get_ext_d2i(self.ctx, NID_issuer_alt_name, crit, nil)
+ crit = tonumber(crit[0])
+ if crit == -1 then -- not found
+ return nil
+ elseif crit == -2 then
+ return nil, "x509:get_issuer_alt_name: extension of issuer_alt_name occurs more than one times, " ..
+ "this is not yet implemented. Please use get_extension instead."
+ elseif got == nil then
+ return nil, format_error("x509:get_issuer_alt_name")
+ end
+
+ -- Note: here we only free the stack itself not elements
+ -- since there seems no way to increase ref count for a GENERAL_NAME
+ -- we left the elements referenced by the new-dup'ed stack
+ local got_ref = got
+ ffi_gc(got_ref, stack_lib.gc_of("GENERAL_NAME"))
+ got = ffi_cast("GENERAL_NAMES*", got_ref)
+ local lib = require("resty.openssl.x509.altname")
+ -- the internal ptr is returned, ie we need to copy it
+ return lib.dup(got)
+end
+
+-- AUTO GENERATED: EXTENSIONS
+function _M:set_issuer_alt_name(toset)
+ local lib = require("resty.openssl.x509.altname")
+ if lib.istype and not lib.istype(toset) then
+ return false, "x509:set_issuer_alt_name: expect a x509.altname instance at #1"
+ end
+ toset = toset.ctx
+ -- x509v3.h: # define X509V3_ADD_REPLACE 2L
+ if C.X509_add1_ext_i2d(self.ctx, NID_issuer_alt_name, toset, 0, 0x2) ~= 1 then
+ return false, format_error("x509:set_issuer_alt_name")
+ end
+ return true
+end
+
+-- AUTO GENERATED: EXTENSIONS
+function _M:set_issuer_alt_name_critical(crit)
+ return _M.set_extension_critical(self, NID_issuer_alt_name, crit)
+end
+
+-- AUTO GENERATED: EXTENSIONS
+function _M:get_issuer_alt_name_critical()
+ return _M.get_extension_critical(self, NID_issuer_alt_name)
+end
+
+local NID_basic_constraints = C.OBJ_sn2nid("basicConstraints")
+assert(NID_basic_constraints ~= 0)
+
+-- AUTO GENERATED: EXTENSIONS
+function _M:get_basic_constraints(name)
+ local crit = ctypes.ptr_of_int()
+ -- X509_get_ext_d2i returns internal pointer, always dup
+ -- for now this function always returns the first found extension
+ local got = C.X509_get_ext_d2i(self.ctx, NID_basic_constraints, crit, nil)
+ crit = tonumber(crit[0])
+ if crit == -1 then -- not found
+ return nil
+ elseif crit == -2 then
+ return nil, "x509:get_basic_constraints: extension of basic_constraints occurs more than one times, " ..
+ "this is not yet implemented. Please use get_extension instead."
+ elseif got == nil then
+ return nil, format_error("x509:get_basic_constraints")
+ end
+
+ local ctx = ffi_cast("BASIC_CONSTRAINTS*", got)
+
+ local ca = ctx.ca == 0xFF
+ local pathlen = tonumber(C.ASN1_INTEGER_get(ctx.pathlen))
+
+ C.BASIC_CONSTRAINTS_free(ctx)
+
+ if not name or type(name) ~= "string" then
+ got = {
+ ca = ca,
+ pathlen = pathlen,
+ }
+ elseif string.lower(name) == "ca" then
+ got = ca
+ elseif string.lower(name) == "pathlen" then
+ got = pathlen
+ end
+
+ return got
+end
+
+-- AUTO GENERATED: EXTENSIONS
+function _M:set_basic_constraints(toset)
+ if type(toset) ~= "table" then
+ return false, "x509:set_basic_constraints: expect a table at #1"
+ end
+
+ local cfg_lower = {}
+ for k, v in pairs(toset) do
+ cfg_lower[string.lower(k)] = v
+ end
+
+ toset = C.BASIC_CONSTRAINTS_new()
+ if toset == nil then
+ return false, format_error("x509:set_BASIC_CONSTRAINTS")
+ end
+ ffi_gc(toset, C.BASIC_CONSTRAINTS_free)
+
+ toset.ca = cfg_lower.ca and 0xFF or 0
+ local pathlen = cfg_lower.pathlen and tonumber(cfg_lower.pathlen)
+ if pathlen then
+ C.ASN1_INTEGER_free(toset.pathlen)
+
+ local asn1 = C.ASN1_STRING_type_new(pathlen)
+ if asn1 == nil then
+ return false, format_error("x509:set_BASIC_CONSTRAINTS: ASN1_STRING_type_new")
+ end
+ toset.pathlen = asn1
+
+ local code = C.ASN1_INTEGER_set(asn1, pathlen)
+ if code ~= 1 then
+ return false, format_error("x509:set_BASIC_CONSTRAINTS: ASN1_INTEGER_set", code)
+ end
+ end
+
+ -- x509v3.h: # define X509V3_ADD_REPLACE 2L
+ if C.X509_add1_ext_i2d(self.ctx, NID_basic_constraints, toset, 0, 0x2) ~= 1 then
+ return false, format_error("x509:set_basic_constraints")
+ end
+ return true
+end
+
+-- AUTO GENERATED: EXTENSIONS
+function _M:set_basic_constraints_critical(crit)
+ return _M.set_extension_critical(self, NID_basic_constraints, crit)
+end
+
+-- AUTO GENERATED: EXTENSIONS
+function _M:get_basic_constraints_critical()
+ return _M.get_extension_critical(self, NID_basic_constraints)
+end
+
+local NID_info_access = C.OBJ_sn2nid("authorityInfoAccess")
+assert(NID_info_access ~= 0)
+
+-- AUTO GENERATED: EXTENSIONS
+function _M:get_info_access()
+ local crit = ctypes.ptr_of_int()
+ -- X509_get_ext_d2i returns internal pointer, always dup
+ -- for now this function always returns the first found extension
+ local got = C.X509_get_ext_d2i(self.ctx, NID_info_access, crit, nil)
+ crit = tonumber(crit[0])
+ if crit == -1 then -- not found
+ return nil
+ elseif crit == -2 then
+ return nil, "x509:get_info_access: extension of info_access occurs more than one times, " ..
+ "this is not yet implemented. Please use get_extension instead."
+ elseif got == nil then
+ return nil, format_error("x509:get_info_access")
+ end
+
+ -- Note: here we only free the stack itself not elements
+ -- since there seems no way to increase ref count for a ACCESS_DESCRIPTION
+ -- we left the elements referenced by the new-dup'ed stack
+ local got_ref = got
+ ffi_gc(got_ref, stack_lib.gc_of("ACCESS_DESCRIPTION"))
+ got = ffi_cast("AUTHORITY_INFO_ACCESS*", got_ref)
+ local lib = require("resty.openssl.x509.extension.info_access")
+ -- the internal ptr is returned, ie we need to copy it
+ return lib.dup(got)
+end
+
+-- AUTO GENERATED: EXTENSIONS
+function _M:set_info_access(toset)
+ local lib = require("resty.openssl.x509.extension.info_access")
+ if lib.istype and not lib.istype(toset) then
+ return false, "x509:set_info_access: expect a x509.extension.info_access instance at #1"
+ end
+ toset = toset.ctx
+ -- x509v3.h: # define X509V3_ADD_REPLACE 2L
+ if C.X509_add1_ext_i2d(self.ctx, NID_info_access, toset, 0, 0x2) ~= 1 then
+ return false, format_error("x509:set_info_access")
+ end
+ return true
+end
+
+-- AUTO GENERATED: EXTENSIONS
+function _M:set_info_access_critical(crit)
+ return _M.set_extension_critical(self, NID_info_access, crit)
+end
+
+-- AUTO GENERATED: EXTENSIONS
+function _M:get_info_access_critical()
+ return _M.get_extension_critical(self, NID_info_access)
+end
+
+local NID_crl_distribution_points = C.OBJ_sn2nid("crlDistributionPoints")
+assert(NID_crl_distribution_points ~= 0)
+
+-- AUTO GENERATED: EXTENSIONS
+function _M:get_crl_distribution_points()
+ local crit = ctypes.ptr_of_int()
+ -- X509_get_ext_d2i returns internal pointer, always dup
+ -- for now this function always returns the first found extension
+ local got = C.X509_get_ext_d2i(self.ctx, NID_crl_distribution_points, crit, nil)
+ crit = tonumber(crit[0])
+ if crit == -1 then -- not found
+ return nil
+ elseif crit == -2 then
+ return nil, "x509:get_crl_distribution_points: extension of crl_distribution_points occurs more than one times, " ..
+ "this is not yet implemented. Please use get_extension instead."
+ elseif got == nil then
+ return nil, format_error("x509:get_crl_distribution_points")
+ end
+
+ -- Note: here we only free the stack itself not elements
+ -- since there seems no way to increase ref count for a DIST_POINT
+ -- we left the elements referenced by the new-dup'ed stack
+ local got_ref = got
+ ffi_gc(got_ref, stack_lib.gc_of("DIST_POINT"))
+ got = ffi_cast("OPENSSL_STACK*", got_ref)
+ local lib = require("resty.openssl.x509.extension.dist_points")
+ -- the internal ptr is returned, ie we need to copy it
+ return lib.dup(got)
+end
+
+-- AUTO GENERATED: EXTENSIONS
+function _M:set_crl_distribution_points(toset)
+ local lib = require("resty.openssl.x509.extension.dist_points")
+ if lib.istype and not lib.istype(toset) then
+ return false, "x509:set_crl_distribution_points: expect a x509.extension.dist_points instance at #1"
+ end
+ toset = toset.ctx
+ -- x509v3.h: # define X509V3_ADD_REPLACE 2L
+ if C.X509_add1_ext_i2d(self.ctx, NID_crl_distribution_points, toset, 0, 0x2) ~= 1 then
+ return false, format_error("x509:set_crl_distribution_points")
+ end
+ return true
+end
+
+-- AUTO GENERATED: EXTENSIONS
+function _M:set_crl_distribution_points_critical(crit)
+ return _M.set_extension_critical(self, NID_crl_distribution_points, crit)
+end
+
+-- AUTO GENERATED: EXTENSIONS
+function _M:get_crl_distribution_points_critical()
+ return _M.get_extension_critical(self, NID_crl_distribution_points)
+end
+
+
+-- AUTO GENERATED
+function _M:get_signature_nid()
+ local nid = accessors.get_signature_nid(self.ctx)
+ if nid <= 0 then
+ return nil, format_error("x509:get_signature_nid")
+ end
+
+ return nid
+end
+
+-- AUTO GENERATED
+function _M:get_signature_name()
+ local nid = accessors.get_signature_nid(self.ctx)
+ if nid <= 0 then
+ return nil, format_error("x509:get_signature_name")
+ end
+
+ return ffi.string(C.OBJ_nid2sn(nid))
+end
+
+-- AUTO GENERATED
+function _M:get_signature_digest_name()
+ local nid = accessors.get_signature_nid(self.ctx)
+ if nid <= 0 then
+ return nil, format_error("x509:get_signature_digest_name")
+ end
+
+ local nid = find_sigid_algs(nid)
+
+ return ffi.string(C.OBJ_nid2sn(nid))
+end
+-- END AUTO GENERATED CODE
+
+return _M
diff --git a/server/resty/openssl/x509/name.lua b/server/resty/openssl/x509/name.lua
new file mode 100644
index 0000000..f83fcc1
--- /dev/null
+++ b/server/resty/openssl/x509/name.lua
@@ -0,0 +1,156 @@
+local ffi = require "ffi"
+local C = ffi.C
+local ffi_gc = ffi.gc
+local ffi_str = ffi.string
+
+require "resty.openssl.include.x509.name"
+require "resty.openssl.include.err"
+local objects_lib = require "resty.openssl.objects"
+local asn1_macro = require "resty.openssl.include.asn1"
+
+-- local MBSTRING_FLAG = 0x1000
+local MBSTRING_ASC = 0x1001 -- (MBSTRING_FLAG|1)
+
+local _M = {}
+
+local x509_name_ptr_ct = ffi.typeof("X509_NAME*")
+
+-- starts from 0
+local function value_at(ctx, i)
+ local entry = C.X509_NAME_get_entry(ctx, i)
+ local obj = C.X509_NAME_ENTRY_get_object(entry)
+ local ret = objects_lib.obj2table(obj)
+
+ local str = C.X509_NAME_ENTRY_get_data(entry)
+ if str ~= nil then
+ ret.blob = ffi_str(asn1_macro.ASN1_STRING_get0_data(str))
+ end
+
+ return ret
+end
+
+local function iter(tbl)
+ local i = 0
+ local n = tonumber(C.X509_NAME_entry_count(tbl.ctx))
+ return function()
+ i = i + 1
+ if i <= n then
+ local obj = value_at(tbl.ctx, i-1)
+ return obj.sn or obj.ln or obj.id, obj
+ end
+ end
+end
+
+local mt = {
+ __index = _M,
+ __pairs = iter,
+ __len = function(tbl) return tonumber(C.X509_NAME_entry_count(tbl.ctx)) end,
+}
+
+function _M.new()
+ local ctx = C.X509_NAME_new()
+ if ctx == nil then
+ return nil, "x509.name.new: X509_NAME_new() failed"
+ end
+ ffi_gc(ctx, C.X509_NAME_free)
+
+ local self = setmetatable({
+ ctx = ctx,
+ }, mt)
+
+ return self, nil
+end
+
+function _M.istype(l)
+ return l and l.ctx and ffi.istype(x509_name_ptr_ct, l.ctx)
+end
+
+function _M.dup(ctx)
+ if not ffi.istype(x509_name_ptr_ct, ctx) then
+ return nil, "x509.name.dup: expect a x509.name ctx at #1, got " .. type(ctx)
+ end
+ local ctx = C.X509_NAME_dup(ctx)
+ ffi_gc(ctx, C.X509_NAME_free)
+
+ local self = setmetatable({
+ ctx = ctx,
+ }, mt)
+
+ return self, nil
+end
+
+function _M:add(nid, txt)
+ local asn1 = C.OBJ_txt2obj(nid, 0)
+ if asn1 == nil then
+ -- clean up error occurs during OBJ_txt2*
+ C.ERR_clear_error()
+ return nil, "x509.name:add: invalid NID text " .. (nid or "nil")
+ end
+
+ local code = C.X509_NAME_add_entry_by_OBJ(self.ctx, asn1, MBSTRING_ASC, txt, #txt, -1, 0)
+ C.ASN1_OBJECT_free(asn1)
+
+ if code ~= 1 then
+ return nil, "x509.name:add: X509_NAME_add_entry_by_OBJ() failed"
+ end
+
+ return self
+end
+
+function _M:find(nid, last_pos)
+ local asn1 = C.OBJ_txt2obj(nid, 0)
+ if asn1 == nil then
+ -- clean up error occurs during OBJ_txt2*
+ C.ERR_clear_error()
+ return nil, nil, "x509.name:find: invalid NID text " .. (nid or "nil")
+ end
+ -- make 1-index array to 0-index
+ last_pos = (last_pos or 0) - 1
+
+ local pos = C.X509_NAME_get_index_by_OBJ(self.ctx, asn1, last_pos)
+ if pos == -1 then
+ return nil
+ end
+
+ C.ASN1_OBJECT_free(asn1)
+
+ return value_at(self.ctx, pos), pos+1
+end
+
+-- fallback function to iterate if LUAJIT_ENABLE_LUA52COMPAT not enabled
+function _M:all()
+ local ret = {}
+ local _next = iter(self)
+ while true do
+ local k, obj = _next()
+ if obj then
+ ret[k] = obj
+ else
+ break
+ end
+ end
+ return ret
+end
+
+function _M:each()
+ return iter(self)
+end
+
+mt.__tostring = function(self)
+ local values = {}
+ local _next = iter(self)
+ while true do
+ local k, v = _next()
+ if k then
+ table.insert(values, k .. "=" .. v.blob)
+ else
+ break
+ end
+ end
+ table.sort(values)
+ return table.concat(values, "/")
+end
+
+_M.tostring = mt.__tostring
+
+return _M
diff --git a/server/resty/openssl/x509/revoked.lua b/server/resty/openssl/x509/revoked.lua
new file mode 100644
index 0000000..9762200
--- /dev/null
+++ b/server/resty/openssl/x509/revoked.lua
@@ -0,0 +1,108 @@
+local ffi = require "ffi"
+local C = ffi.C
+local ffi_gc = ffi.gc
+
+require "resty.openssl.include.x509.crl"
+require "resty.openssl.include.x509.revoked"
+local bn_lib = require("resty.openssl.bn")
+local format_error = require("resty.openssl.err").format_error
+
+local _M = {}
+local mt = { __index = _M }
+
+local x509_revoked_ptr_ct = ffi.typeof('X509_REVOKED*')
+
+local NID_crl_reason = C.OBJ_txt2nid("CRLReason")
+assert(NID_crl_reason > 0)
+
+--- Creates new instance of X509_REVOKED data
+-- @tparam bn|number sn Serial number as number or bn instance
+-- @tparam number time Revocation time
+-- @tparam number reason Revocation reason
+-- @treturn table instance of the module or nil
+-- @treturn[opt] string Returns optional error message in case of error
+function _M.new(sn, time, reason)
+ --- only convert to bn if it is number
+ if type(sn) == "number"then
+ sn = bn_lib.new(sn)
+ end
+ if not bn_lib.istype(sn) then
+ return nil, "x509.revoked.new: sn should be number or a bn instance"
+ end
+
+ if type(time) ~= "number" then
+ return nil, "x509.revoked.new: expect a number at #2"
+ end
+ if type(reason) ~= "number" then
+ return nil, "x509.revoked.new: expect a number at #3"
+ end
+
+ local ctx = C.X509_REVOKED_new()
+ ffi_gc(ctx, C.X509_REVOKED_free)
+
+ -- serial number
+ local sn_asn1 = C.BN_to_ASN1_INTEGER(sn.ctx, nil)
+ if sn_asn1 == nil then
+ return nil, "x509.revoked.new: BN_to_ASN1_INTEGER() failed"
+ end
+ ffi_gc(sn_asn1, C.ASN1_INTEGER_free)
+
+ if C.X509_REVOKED_set_serialNumber(ctx, sn_asn1) == 0 then
+ return nil, format_error("x509.revoked.new: X509_REVOKED_set_serialNumber()")
+ end
+
+ -- time
+ time = C.ASN1_TIME_set(nil, time)
+ if time == nil then
+ return nil, format_error("x509.revoked.new: ASN1_TIME_set()")
+ end
+ ffi_gc(time, C.ASN1_STRING_free)
+
+ if C.X509_REVOKED_set_revocationDate(ctx, time) == 0 then
+ return nil, format_error("x509.revoked.new: X509_REVOKED_set_revocationDate()")
+ end
+
+ -- reason
+ local reason_asn1 = C.ASN1_ENUMERATED_new()
+ if reason_asn1 == nil then
+ return nil, "x509.revoked.new: ASN1_ENUMERATED_new() failed"
+ end
+ ffi_gc(reason_asn1, C.ASN1_ENUMERATED_free)
+
+ local reason_ext = C.X509_EXTENSION_new()
+ if reason_ext == nil then
+ return nil, "x509.revoked.new: X509_EXTENSION_new() failed"
+ end
+ ffi_gc(reason_ext, C.X509_EXTENSION_free)
+
+ if C.ASN1_ENUMERATED_set(reason_asn1, reason) == 0 then
+ return nil, format_error("x509.revoked.new: ASN1_ENUMERATED_set()")
+ end
+
+ if C.X509_EXTENSION_set_data(reason_ext, reason_asn1) == 0 then
+ return nil, format_error("x509.revoked.new: X509_EXTENSION_set_data()")
+ end
+
+ if C.X509_EXTENSION_set_object(reason_ext, C.OBJ_nid2obj(NID_crl_reason)) == 0 then
+ return nil, format_error("x509.revoked.new: X509_EXTENSION_set_object()")
+ end
+
+ if C.X509_REVOKED_add_ext(ctx, reason_ext, 0) == 0 then
+ return nil, format_error("x509.revoked.new: X509_EXTENSION_set_object()")
+ end
+
+ local self = setmetatable({
+ ctx = ctx,
+ }, mt)
+
+ return self, nil
+end
+
+--- Type check
+-- @tparam table Instance of revoked module
+-- @treturn boolean true if instance is instance of revoked module false otherwise
+function _M.istype(l)
+ return l and l.ctx and ffi.istype(x509_revoked_ptr_ct, l.ctx)
+end
+
+return _M
diff --git a/server/resty/openssl/x509/store.lua b/server/resty/openssl/x509/store.lua
new file mode 100644
index 0000000..1722e4c
--- /dev/null
+++ b/server/resty/openssl/x509/store.lua
@@ -0,0 +1,227 @@
+local ffi = require "ffi"
+local C = ffi.C
+local ffi_gc = ffi.gc
+local ffi_str = ffi.string
+local bor = bit.bor
+
+local x509_vfy_macro = require "resty.openssl.include.x509_vfy"
+local x509_lib = require "resty.openssl.x509"
+local chain_lib = require "resty.openssl.x509.chain"
+local crl_lib = require "resty.openssl.x509.crl"
+local ctx_lib = require "resty.openssl.ctx"
+local format_error = require("resty.openssl.err").format_all_error
+local format_all_error = require("resty.openssl.err").format_error
+local OPENSSL_3X = require("resty.openssl.version").OPENSSL_3X
+
+local _M = {}
+local mt = { __index = _M }
+
+_M.verify_flags = x509_vfy_macro.verify_flags
+
+local x509_store_ptr_ct = ffi.typeof('X509_STORE*')
+
+function _M.new()
+ local ctx = C.X509_STORE_new()
+ if ctx == nil then
+ return nil, "x509.store.new: X509_STORE_new() failed"
+ end
+ ffi_gc(ctx, C.X509_STORE_free)
+
+ local self = setmetatable({
+ ctx = ctx,
+ _elem_refs = {},
+ _elem_refs_idx = 1,
+ }, mt)
+
+ return self, nil
+end
+
+function _M.istype(l)
+ return l and l.ctx and ffi.istype(x509_store_ptr_ct, l.ctx)
+end
+
+function _M:use_default(properties)
+ if x509_vfy_macro.X509_STORE_set_default_paths(self.ctx, ctx_lib.get_libctx(), properties) ~= 1 then
+ return false, format_all_error("x509.store:use_default")
+ end
+ return true
+end
+
+function _M:add(item)
+ local dup
+ local err
+ if x509_lib.istype(item) then
+ dup = C.X509_dup(item.ctx)
+ if dup == nil then
+ return false, "x509.store:add: X509_dup() failed"
+ end
+ -- ref counter of dup is increased by 1
+ if C.X509_STORE_add_cert(self.ctx, dup) ~= 1 then
+ err = format_all_error("x509.store:add: X509_STORE_add_cert")
+ end
+ -- decrease the dup ctx ref count immediately to make leak test happy
+ C.X509_free(dup)
+ elseif crl_lib.istype(item) then
+ dup = C.X509_CRL_dup(item.ctx)
+ if dup == nil then
+ return false, "x509.store:add: X509_CRL_dup() failed"
+ end
+ -- ref counter of dup is increased by 1
+ if C.X509_STORE_add_crl(self.ctx, dup) ~= 1 then
+ err = format_all_error("x509.store:add: X509_STORE_add_crl")
+ end
+
+ -- define X509_V_FLAG_CRL_CHECK 0x4
+ -- enables CRL checking for the certificate chain leaf certificate.
+ -- An error occurs if a suitable CRL cannot be found.
+ -- Note: this does not check for certificates in the chain.
+ if C.X509_STORE_set_flags(self.ctx, 0x4) ~= 1 then
+ return false, format_error("x509.store:add: X509_STORE_set_flags")
+ end
+ -- decrease the dup ctx ref count immediately to make leak test happy
+ C.X509_CRL_free(dup)
+ else
+ return false, "x509.store:add: expect an x509 or crl instance at #1"
+ end
+
+ if err then
+ return false, err
+ end
+
+ -- X509_STORE doesn't have stack gc handler, we need to gc by ourselves
+ self._elem_refs[self._elem_refs_idx] = dup
+ self._elem_refs_idx = self._elem_refs_idx + 1
+
+ return true
+end
+
+function _M:load_file(path, properties)
+ if type(path) ~= "string" then
+ return false, "x509.store:load_file: expect a string at #1"
+ else
+ if x509_vfy_macro.X509_STORE_load_locations(self.ctx, path, nil,
+ ctx_lib.get_libctx(), properties) ~= 1 then
+ return false, format_all_error("x509.store:load_file")
+ end
+ end
+
+ return true
+end
+
+function _M:load_directory(path, properties)
+ if type(path) ~= "string" then
+ return false, "x509.store:load_directory expect a string at #1"
+ else
+ if x509_vfy_macro.X509_STORE_load_locations(self.ctx, nil, path,
+ ctx_lib.get_libctx(), properties) ~= 1 then
+ return false, format_all_error("x509.store:load_directory")
+ end
+ end
+
+ return true
+end
+
+function _M:set_depth(depth)
+ depth = depth and tonumber(depth)
+ if not depth then
+ return nil, "x509.store:set_depth: expect a number at #1"
+ end
+
+ if C.X509_STORE_set_depth(self.ctx, depth) ~= 1 then
+ return false, format_error("x509.store:set_depth")
+ end
+
+ return true
+end
+
+function _M:set_purpose(purpose)
+ if type(purpose) ~= "string" then
+ return nil, "x509.store:set_purpose: expect a string at #1"
+ end
+
+ local pchar = ffi.new("char[?]", #purpose, purpose)
+ local idx = C.X509_PURPOSE_get_by_sname(pchar)
+ idx = tonumber(idx)
+
+ if idx == -1 then
+ return false, "invalid purpose \"" .. purpose .. "\""
+ end
+
+ local purp = C.X509_PURPOSE_get0(idx)
+ local i = C.X509_PURPOSE_get_id(purp)
+
+ if C.X509_STORE_set_purpose(self.ctx, i) ~= 1 then
+ return false, format_error("x509.store:set_purpose: X509_STORE_set_purpose")
+ end
+
+ return true
+end
+
+function _M:set_flags(...)
+ local flag = 0
+ for _, f in ipairs({...}) do
+ flag = bor(flag, f)
+ end
+
+ if C.X509_STORE_set_flags(self.ctx, flag) ~= 1 then
+ return false, format_error("x509.store:set_flags: X509_STORE_set_flags")
+ end
+
+ return true
+end
+
+function _M:verify(x509, chain, return_chain, properties, verify_method)
+ if not x509_lib.istype(x509) then
+ return nil, "x509.store:verify: expect a x509 instance at #1"
+ elseif chain and not chain_lib.istype(chain) then
+ return nil, "x509.store:verify: expect a x509.chain instance at #1"
+ end
+
+ local ctx
+ if OPENSSL_3X then
+ ctx = C.X509_STORE_CTX_new_ex(ctx_lib.get_libctx(), properties)
+ else
+ ctx = C.X509_STORE_CTX_new()
+ end
+ if ctx == nil then
+ return nil, "x509.store:verify: X509_STORE_CTX_new() failed"
+ end
+
+ ffi_gc(ctx, C.X509_STORE_CTX_free)
+
+ local chain_dup_ctx
+ if chain then
+ local chain_dup, err = chain_lib.dup(chain.ctx)
+ if err then
+ return nil, err
+ end
+ chain_dup_ctx = chain_dup.ctx
+ end
+
+ if C.X509_STORE_CTX_init(ctx, self.ctx, x509.ctx, chain_dup_ctx) ~= 1 then
+ return nil, format_error("x509.store:verify: X509_STORE_CTX_init")
+ end
+
+ if verify_method and C.X509_STORE_CTX_set_default(ctx, verify_method) ~= 1 then
+ return nil, "x509.store:verify: invalid verify_method \"" .. verify_method .. "\""
+ end
+
+ local code = C.X509_verify_cert(ctx)
+ if code == 1 then -- verified
+ if not return_chain then
+ return true, nil
+ end
+ local ret_chain_ctx = x509_vfy_macro.X509_STORE_CTX_get0_chain(ctx)
+ return chain_lib.dup(ret_chain_ctx)
+ elseif code == 0 then -- unverified
+ local vfy_code = C.X509_STORE_CTX_get_error(ctx)
+
+ return nil, ffi_str(C.X509_verify_cert_error_string(vfy_code))
+ end
+
+ -- error
+ return nil, format_error("x509.store:verify: X509_verify_cert", code)
+
+end
+
+return _M
diff --git a/server/resty/session.lua b/server/resty/session.lua
new file mode 100644
index 0000000..12dfe53
--- /dev/null
+++ b/server/resty/session.lua
@@ -0,0 +1,771 @@
+local require = require
+
+local random = require "resty.random"
+
+local ngx = ngx
+local var = ngx.var
+local time = ngx.time
+local header = ngx.header
+local http_time = ngx.http_time
+local set_header = ngx.req.set_header
+local clear_header = ngx.req.clear_header
+local concat = table.concat
+local ceil = math.ceil
+local max = math.max
+local find = string.find
+local gsub = string.gsub
+local byte = string.byte
+local sub = string.sub
+local type = type
+local pcall = pcall
+local tonumber = tonumber
+local setmetatable = setmetatable
+local getmetatable = getmetatable
+local bytes = random.bytes
+
+local UNDERSCORE = byte("_")
+local EXPIRE_FLAGS = "; Expires=Thu, 01 Jan 1970 00:00:01 GMT; Max-Age=0"
+
+local COOKIE_PARTS = {
+ DEFAULT = {
+ n = 3,
+ "id",
+ "expires", -- may also contain: `expires:usebefore`
+ "hash"
+ },
+ cookie = {
+ n = 4,
+ "id",
+ "expires", -- may also contain: `expires:usebefore`
+ "data",
+ "hash",
+ },
+}
+
+local function enabled(value)
+ if value == nil then
+ return nil
+ end
+
+ return value == true
+ or value == "1"
+ or value == "true"
+ or value == "on"
+end
+
+local function ifnil(value, default)
+ if value == nil then
+ return default
+ end
+
+ return enabled(value)
+end
+
+local function prequire(prefix, package, default)
+ if type(package) == "table" then
+ return package, package.name
+ end
+
+ local ok, module = pcall(require, prefix .. package)
+ if not ok then
+ return require(prefix .. default), default
+ end
+
+ return module, package
+end
+
+local function is_session_cookie(cookie, name, name_len)
+ if not cookie or cookie == "" then
+ return false, nil
+ end
+
+ cookie = gsub(cookie, "^%s+", "")
+ if cookie == "" then
+ return false, nil
+ end
+
+ cookie = gsub(cookie, "%s+$", "")
+ if cookie == "" then
+ return false, nil
+ end
+
+ local eq_pos = find(cookie, "=", 1, true)
+ if not eq_pos then
+ return false, cookie
+ end
+
+ local cookie_name = sub(cookie, 1, eq_pos - 1)
+ if cookie_name == "" then
+ return false, cookie
+ end
+
+ cookie_name = gsub(cookie_name, "%s+$", "")
+ if cookie_name == "" then
+ return false, cookie
+ end
+
+ if cookie_name ~= name then
+ if find(cookie_name, name, 1, true) ~= 1 then
+ return false, cookie
+ end
+
+ if byte(cookie_name, name_len + 1) ~= UNDERSCORE then
+ return false, cookie
+ end
+
+ if not tonumber(sub(cookie_name, name_len + 2), 10) then
+ return false, cookie
+ end
+ end
+
+ return true, cookie
+end
+
+local function set_cookie(session, value, expires)
+ if ngx.headers_sent then
+ return nil, "attempt to set session cookie after sending out response headers"
+ end
+
+ value = value or ""
+
+ local cookie = session.cookie
+ local output = {}
+
+ local i = 3
+
+ -- build cookie parameters, elements 1+2 will be set later
+ if expires then
+ -- we're expiring/deleting the data, so set an expiry in the past
+ output[i] = EXPIRE_FLAGS
+ elseif cookie.persistent then
+ -- persistent cookies have an expiry
+ output[i] = "; Expires=" .. http_time(session.expires) .. "; Max-Age=" .. cookie.lifetime
+ else
+ -- just to reserve index 3 for expiry as cookie might get smaller,
+ -- and some cookies need to be expired.
+ output[i] = ""
+ end
+
+ if cookie.domain and cookie.domain ~= "localhost" and cookie.domain ~= "" then
+ i = i + 1
+ output[i] = "; Domain=" .. cookie.domain
+ end
+
+ i = i + 1
+ output[i] = "; Path=" .. (cookie.path or "/")
+
+ if cookie.samesite == "Lax"
+ or cookie.samesite == "Strict"
+ or cookie.samesite == "None"
+ then
+ i = i + 1
+ output[i] = "; SameSite=" .. cookie.samesite
+ end
+
+ if cookie.secure then
+ i = i + 1
+ output[i] = "; Secure"
+ end
+
+ if cookie.httponly then
+ i = i + 1
+ output[i] = "; HttpOnly"
+ end
+
+ -- How many chunks do we need?
+ local cookie_parts
+ local cookie_chunks
+ if expires then
+ -- expiring cookie, so deleting data. Do not measure data, but use
+ -- existing chunk count to make sure we clear all of them
+ cookie_parts = cookie.chunks or 1
+ else
+ -- calculate required chunks from data
+ cookie_chunks = max(ceil(#value / cookie.maxsize), 1)
+ cookie_parts = max(cookie_chunks, cookie.chunks or 1)
+ end
+
+ local cookie_header = header["Set-Cookie"]
+ for j = 1, cookie_parts do
+ -- create numbered chunk names if required
+ local chunk_name = { session.name }
+ if j > 1 then
+ chunk_name[2] = "_"
+ chunk_name[3] = j
+ chunk_name[4] = "="
+ else
+ chunk_name[2] = "="
+ end
+ chunk_name = concat(chunk_name)
+ output[1] = chunk_name
+
+ if expires then
+ -- expiring cookie, so deleting data; clear it
+ output[2] = ""
+ elseif j > cookie_chunks then
+ -- less chunks than before, clearing excess cookies
+ output[2] = ""
+ output[3] = EXPIRE_FLAGS
+
+ else
+ -- grab the piece for the current chunk
+ local sp = j * cookie.maxsize - (cookie.maxsize - 1)
+ if j < cookie_chunks then
+ output[2] = sub(value, sp, sp + (cookie.maxsize - 1)) .. "0"
+ else
+ output[2] = sub(value, sp)
+ end
+ end
+
+ -- build header value and add it to the header table/string
+ -- replace existing chunk-name, or append
+ local cookie_content = concat(output)
+ local header_type = type(cookie_header)
+ if header_type == "table" then
+ local found = false
+ local cookie_count = #cookie_header
+ for cookie_index = 1, cookie_count do
+ if find(cookie_header[cookie_index], chunk_name, 1, true) == 1 then
+ cookie_header[cookie_index] = cookie_content
+ found = true
+ break
+ end
+ end
+ if not found then
+ cookie_header[cookie_count + 1] = cookie_content
+ end
+ elseif header_type == "string" and find(cookie_header, chunk_name, 1, true) ~= 1 then
+ cookie_header = { cookie_header, cookie_content }
+ else
+ cookie_header = cookie_content
+ end
+ end
+
+ header["Set-Cookie"] = cookie_header
+
+ return true
+end
+
+local function get_cookie(session, i)
+ local cookie_name = { "cookie_", session.name }
+ if i then
+ cookie_name[3] = "_"
+ cookie_name[4] = i
+ else
+ i = 1
+ end
+
+ local cookie = var[concat(cookie_name)]
+ if not cookie then
+ return nil
+ end
+
+ session.cookie.chunks = i
+
+ local cookie_size = #cookie
+ if cookie_size <= session.cookie.maxsize then
+ return cookie
+ end
+
+ return concat{ sub(cookie, 1, session.cookie.maxsize), get_cookie(session, i + 1) or "" }
+end
+
+local function set_usebefore(session)
+ local usebefore = session.usebefore
+ local idletime = session.cookie.idletime
+
+ if idletime == 0 then -- usebefore is disabled
+ if usebefore then
+ session.usebefore = nil
+ return true
+ end
+
+ return false
+ end
+
+ usebefore = usebefore or 0
+
+ local new_usebefore = session.now + idletime
+ if new_usebefore - usebefore > 60 then
+ session.usebefore = new_usebefore
+ return true
+ end
+
+ return false
+end
+
+local function save(session, close)
+ session.expires = session.now + session.cookie.lifetime
+
+ set_usebefore(session)
+
+ local cookie, err = session.strategy.save(session, close)
+ if not cookie then
+ return nil, err or "unable to save session cookie"
+ end
+
+ return set_cookie(session, cookie)
+end
+
+local function touch(session, close)
+ if set_usebefore(session) then
+ -- usebefore was updated, so set cookie
+ local cookie, err = session.strategy.touch(session, close)
+ if not cookie then
+ return nil, err or "unable to touch session cookie"
+ end
+
+ return set_cookie(session, cookie)
+ end
+
+ if close then
+ local ok, err = session.strategy.close(session)
+ if not ok then
+ return nil, err
+ end
+ end
+
+ return true
+end
+
+local function regenerate(session, flush)
+ if session.strategy.destroy then
+ session.strategy.destroy(session)
+ elseif session.strategy.close then
+ session.strategy.close(session)
+ end
+
+ if flush then
+ session.data = {}
+ end
+
+ session.id = session:identifier()
+end
+
+local secret = bytes(32, true) or bytes(32)
+local defaults
+
+local function init()
+ defaults = {
+ name = var.session_name or "session",
+ identifier = var.session_identifier or "random",
+ strategy = var.session_strategy or "default",
+ storage = var.session_storage or "cookie",
+ serializer = var.session_serializer or "json",
+ compressor = var.session_compressor or "none",
+ encoder = var.session_encoder or "base64",
+ cipher = var.session_cipher or "aes",
+ hmac = var.session_hmac or "sha1",
+ cookie = {
+ path = var.session_cookie_path or "/",
+ domain = var.session_cookie_domain,
+ samesite = var.session_cookie_samesite or "Lax",
+ secure = enabled(var.session_cookie_secure),
+ httponly = enabled(var.session_cookie_httponly or true),
+ persistent = enabled(var.session_cookie_persistent or false),
+ discard = tonumber(var.session_cookie_discard, 10) or 10,
+ renew = tonumber(var.session_cookie_renew, 10) or 600,
+ lifetime = tonumber(var.session_cookie_lifetime, 10) or 3600,
+ idletime = tonumber(var.session_cookie_idletime, 10) or 0,
+ maxsize = tonumber(var.session_cookie_maxsize, 10) or 4000,
+
+ }, check = {
+ ssi = enabled(var.session_check_ssi or false),
+ ua = enabled(var.session_check_ua or true),
+ scheme = enabled(var.session_check_scheme or true),
+ addr = enabled(var.session_check_addr or false)
+ }
+ }
+ defaults.secret = var.session_secret or secret
+end
+
+local session = {
+ _VERSION = "3.10"
+}
+
+session.__index = session
+
+function session:get_cookie()
+ return get_cookie(self)
+end
+
+function session:parse_cookie(value)
+ local cookie
+ local cookie_parts = COOKIE_PARTS[self.cookie.storage] or COOKIE_PARTS.DEFAULT
+
+ local count = 1
+ local pos = 1
+
+ local p_pos = find(value, "|", 1, true)
+ while p_pos do
+ if count > (cookie_parts.n - 1) then
+ return nil, "too many session cookie parts"
+ end
+ if not cookie then
+ cookie = {}
+ end
+
+ if count == 2 then
+ local cookie_part = sub(value, pos, p_pos - 1)
+ local c_pos = find(cookie_part, ":", 2, true)
+ if c_pos then
+ cookie.expires = tonumber(sub(cookie_part, 1, c_pos - 1), 10)
+ if not cookie.expires then
+ return nil, "invalid session cookie expiry"
+ end
+
+ cookie.usebefore = tonumber(sub(cookie_part, c_pos + 1), 10)
+ if not cookie.usebefore then
+ return nil, "invalid session cookie usebefore"
+ end
+ else
+ cookie.expires = tonumber(cookie_part, 10)
+ if not cookie.expires then
+ return nil, "invalid session cookie expiry"
+ end
+ end
+ else
+ local name = cookie_parts[count]
+
+ local cookie_part = self.encoder.decode(sub(value, pos, p_pos - 1))
+ if not cookie_part then
+ return nil, "unable to decode session cookie part (" .. name .. ")"
+ end
+
+ cookie[name] = cookie_part
+ end
+
+ count = count + 1
+ pos = p_pos + 1
+
+ p_pos = find(value, "|", pos, true)
+ end
+
+ if count ~= cookie_parts.n then
+ return nil, "invalid number of session cookie parts"
+ end
+
+ local name = cookie_parts[count]
+
+ local cookie_part = self.encoder.decode(sub(value, pos))
+ if not cookie_part then
+ return nil, "unable to decode session cookie part (" .. name .. ")"
+ end
+
+ cookie[name] = cookie_part
+
+ if not cookie.id then
+ return nil, "missing session cookie id"
+ end
+
+ if not cookie.expires then
+ return nil, "missing session cookie expiry"
+ end
+
+ if cookie.expires <= self.now then
+ return nil, "session cookie has expired"
+ end
+
+ if cookie.usebefore and cookie.usebefore <= self.now then
+ return nil, "session cookie idle time has passed"
+ end
+
+ if not cookie.hash then
+ return nil, "missing session cookie signature"
+ end
+
+ return cookie
+end
+
+function session.new(opts)
+ if opts and getmetatable(opts) == session then
+ return opts
+ end
+
+ if not defaults then
+ init()
+ end
+
+ opts = type(opts) == "table" and opts or defaults
+
+ local cookie = opts.cookie or defaults.cookie
+ local name = opts.name or defaults.name
+ local sec = opts.secret or defaults.secret
+
+ local secure
+ local path
+ local domain
+ if find(name, "__Host-", 1, true) == 1 then
+ secure = true
+ path = "/"
+ else
+ if find(name, "__Secure-", 1, true) == 1 then
+ secure = true
+ else
+ secure = ifnil(cookie.secure, defaults.cookie.secure)
+ end
+
+ domain = cookie.domain or defaults.cookie.domain
+ path = cookie.path or defaults.cookie.path
+ end
+
+ local check = opts.check or defaults.check
+
+ local ide, iden = prequire("resty.session.identifiers.", opts.identifier or defaults.identifier, "random")
+ local ser, sern = prequire("resty.session.serializers.", opts.serializer or defaults.serializer, "json")
+ local com, comn = prequire("resty.session.compressors.", opts.compressor or defaults.compressor, "none")
+ local enc, encn = prequire("resty.session.encoders.", opts.encoder or defaults.encoder, "base64")
+ local cip, cipn = prequire("resty.session.ciphers.", opts.cipher or defaults.cipher, "aes")
+ local sto, ston = prequire("resty.session.storage.", opts.storage or defaults.storage, "cookie")
+ local str, strn = prequire("resty.session.strategies.", opts.strategy or defaults.strategy, "default")
+ local hma, hman = prequire("resty.session.hmac.", opts.hmac or defaults.hmac, "sha1")
+
+ local self = {
+ now = time(),
+ name = name,
+ secret = sec,
+ identifier = ide,
+ serializer = ser,
+ strategy = str,
+ encoder = enc,
+ hmac = hma,
+ cookie = {
+ storage = ston,
+ encoder = enc,
+ path = path,
+ domain = domain,
+ secure = secure,
+ samesite = cookie.samesite or defaults.cookie.samesite,
+ httponly = ifnil(cookie.httponly, defaults.cookie.httponly),
+ persistent = ifnil(cookie.persistent, defaults.cookie.persistent),
+ discard = tonumber(cookie.discard, 10) or defaults.cookie.discard,
+ renew = tonumber(cookie.renew, 10) or defaults.cookie.renew,
+ lifetime = tonumber(cookie.lifetime, 10) or defaults.cookie.lifetime,
+ idletime = tonumber(cookie.idletime, 10) or defaults.cookie.idletime,
+ maxsize = tonumber(cookie.maxsize, 10) or defaults.cookie.maxsize,
+ }, check = {
+ ssi = ifnil(check.ssi, defaults.check.ssi),
+ ua = ifnil(check.ua, defaults.check.ua),
+ scheme = ifnil(check.scheme, defaults.check.scheme),
+ addr = ifnil(check.addr, defaults.check.addr),
+ }
+ }
+ if self.cookie.idletime > 0 and self.cookie.discard > self.cookie.idletime then
+ -- if using idletime, then the discard period must be less or equal
+ self.cookie.discard = self.cookie.idletime
+ end
+
+ if iden and not self[iden] then self[iden] = opts[iden] end
+ if sern and not self[sern] then self[sern] = opts[sern] end
+ if comn and not self[comn] then self[comn] = opts[comn] end
+ if encn and not self[encn] then self[encn] = opts[encn] end
+ if cipn and not self[cipn] then self[cipn] = opts[cipn] end
+ if ston and not self[ston] then self[ston] = opts[ston] end
+ if strn and not self[strn] then self[strn] = opts[strn] end
+ if hman and not self[hman] then self[hman] = opts[hman] end
+
+ self.cipher = cip.new(self)
+ self.storage = sto.new(self)
+ self.compressor = com.new(self)
+
+ return setmetatable(self, session)
+end
+
+function session.open(opts, keep_lock)
+ local self = opts
+ if self and getmetatable(self) == session then
+ if self.opened then
+ return self, self.present
+ end
+ else
+ self = session.new(opts)
+ end
+
+ if self.cookie.secure == nil then
+ self.cookie.secure = var.scheme == "https" or var.https == "on"
+ end
+
+ self.now = time()
+ self.key = concat {
+ self.check.ssi and var.ssl_session_id or "",
+ self.check.ua and var.http_user_agent or "",
+ self.check.addr and var.remote_addr or "",
+ self.check.scheme and var.scheme or "",
+ }
+
+ self.opened = true
+
+ local err
+ local cookie = self:get_cookie()
+ if cookie then
+ cookie, err = self:parse_cookie(cookie)
+ if cookie then
+ local ok
+ ok, err = self.strategy.open(self, cookie, keep_lock)
+ if ok then
+ return self, true
+ end
+ end
+ end
+
+ regenerate(self, true)
+
+ return self, false, err
+end
+
+function session.start(opts)
+ if opts and getmetatable(opts) == session and opts.started then
+ return opts, opts.present
+ end
+
+ local self, present, reason = session.open(opts, true)
+
+ self.started = true
+
+ if not present then
+ local ok, err = save(self)
+ if not ok then
+ return nil, err or "unable to save session cookie"
+ end
+
+ return self, present, reason
+ end
+
+ if self.strategy.start then
+ local ok, err = self.strategy.start(self)
+ if not ok then
+ return nil, err or "unable to start session"
+ end
+ end
+
+ if self.expires - self.now < self.cookie.renew
+ or self.expires > self.now + self.cookie.lifetime
+ then
+ local ok, err = save(self)
+ if not ok then
+ return nil, err or "unable to save session cookie"
+ end
+ else
+ -- we're not saving, so we must touch to update idletime/usebefore
+ local ok, err = touch(self)
+ if not ok then
+ return nil, err or "unable to touch session cookie"
+ end
+ end
+
+ return self, true
+end
+
+function session.destroy(opts)
+ if opts and getmetatable(opts) == session and opts.destroyed then
+ return true
+ end
+
+ local self, err = session.start(opts)
+ if not self then
+ return nil, err
+ end
+
+ if self.strategy.destroy then
+ self.strategy.destroy(self)
+ elseif self.strategy.close then
+ self.strategy.close(self)
+ end
+
+ self.data = {}
+ self.present = nil
+ self.opened = nil
+ self.started = nil
+ self.closed = true
+ self.destroyed = true
+
+ return set_cookie(self, "", true)
+end
+
+function session:regenerate(flush, close)
+ close = close ~= false
+ if self.strategy.regenerate then
+ if flush then
+ self.data = {}
+ end
+
+ if not self.id then
+ self.id = session:identifier()
+ end
+ else
+ regenerate(self, flush)
+ end
+
+ return save(self, close)
+end
+
+function session:save(close)
+ close = close ~= false
+
+ if not self.id then
+ self.id = self:identifier()
+ end
+
+ return save(self, close)
+end
+
+function session:close()
+ self.closed = true
+
+ if self.strategy.close then
+ return self.strategy.close(self)
+ end
+
+ return true
+end
+
+function session:hide()
+ local cookies = var.http_cookie
+ if not cookies or cookies == "" then
+ return
+ end
+
+ local results = {}
+ local name = self.name
+ local name_len = #name
+ local found
+ local i = 1
+ local j = 0
+ local sc_pos = find(cookies, ";", i, true)
+ while sc_pos do
+ local isc, cookie = is_session_cookie(sub(cookies, i, sc_pos - 1), name, name_len)
+ if isc then
+ found = true
+ elseif cookie then
+ j = j + 1
+ results[j] = cookie
+ end
+
+ i = sc_pos + 1
+ sc_pos = find(cookies, ";", i, true)
+ end
+
+ local isc, cookie
+ if i == 1 then
+ isc, cookie = is_session_cookie(cookies, name, name_len)
+ else
+ isc, cookie = is_session_cookie(sub(cookies, i), name, name_len)
+ end
+
+ if not isc and cookie then
+ if not found then
+ return
+ end
+
+ j = j + 1
+ results[j] = cookie
+ end
+
+ if j == 0 then
+ clear_header("Cookie")
+ else
+ set_header("Cookie", concat(results, "; ", 1, j))
+ end
+end
+
+return session
diff --git a/server/resty/session/ciphers/aes.lua b/server/resty/session/ciphers/aes.lua
new file mode 100644
index 0000000..9a088ad
--- /dev/null
+++ b/server/resty/session/ciphers/aes.lua
@@ -0,0 +1,113 @@
+local aes = require "resty.aes"
+
+local setmetatable = setmetatable
+local tonumber = tonumber
+local ceil = math.ceil
+local var = ngx.var
+local sub = string.sub
+local rep = string.rep
+
+local HASHES = aes.hash
+
+local CIPHER_MODES = {
+ ecb = "ecb",
+ cbc = "cbc",
+ cfb1 = "cfb1",
+ cfb8 = "cfb8",
+ cfb128 = "cfb128",
+ ofb = "ofb",
+ ctr = "ctr",
+ gcm = "gcm",
+}
+
+local CIPHER_SIZES = {
+ [128] = 128,
+ [192] = 192,
+ [256] = 256,
+}
+
+local defaults = {
+ size = CIPHER_SIZES[tonumber(var.session_aes_size, 10)] or 256,
+ mode = CIPHER_MODES[var.session_aes_mode] or "cbc",
+ hash = HASHES[var.session_aes_hash] or HASHES.sha512,
+ rounds = tonumber(var.session_aes_rounds, 10) or 1,
+}
+
+local function adjust_salt(salt)
+ if not salt then
+ return nil
+ end
+
+ local z = #salt
+ if z < 8 then
+ return sub(rep(salt, ceil(8 / z)), 1, 8)
+ end
+ if z > 8 then
+ return sub(salt, 1, 8)
+ end
+
+ return salt
+end
+
+local function get_cipher(self, key, salt)
+ local mode = aes.cipher(self.size, self.mode)
+ if not mode then
+ return nil, "invalid cipher mode " .. self.mode .. "(" .. self.size .. ")"
+ end
+
+ return aes:new(key, adjust_salt(salt), mode, self.hash, self.rounds)
+end
+
+local cipher = {}
+
+cipher.__index = cipher
+
+function cipher.new(session)
+ local config = session.aes or defaults
+ return setmetatable({
+ size = CIPHER_SIZES[tonumber(config.size, 10)] or defaults.size,
+ mode = CIPHER_MODES[config.mode] or defaults.mode,
+ hash = HASHES[config.hash] or defaults.hash,
+ rounds = tonumber(config.rounds, 10) or defaults.rounds,
+ }, cipher)
+end
+
+function cipher:encrypt(data, key, salt, _)
+ local cip, err = get_cipher(self, key, salt)
+ if not cip then
+ return nil, err or "unable to aes encrypt data"
+ end
+
+ local encrypted_data
+ encrypted_data, err = cip:encrypt(data)
+ if not encrypted_data then
+ return nil, err or "aes encryption failed"
+ end
+
+ if self.mode == "gcm" then
+ return encrypted_data[1], nil, encrypted_data[2]
+ end
+
+ return encrypted_data
+end
+
+function cipher:decrypt(data, key, salt, _, tag)
+ local cip, err = get_cipher(self, key, salt)
+ if not cip then
+ return nil, err or "unable to aes decrypt data"
+ end
+
+ local decrypted_data
+ decrypted_data, err = cip:decrypt(data, tag)
+ if not decrypted_data then
+ return nil, err or "aes decryption failed"
+ end
+
+ if self.mode == "gcm" then
+ return decrypted_data, nil, tag
+ end
+
+ return decrypted_data
+end
+
+return cipher
diff --git a/server/resty/session/ciphers/none.lua b/server/resty/session/ciphers/none.lua
new file mode 100644
index 0000000..b29bb88
--- /dev/null
+++ b/server/resty/session/ciphers/none.lua
@@ -0,0 +1,15 @@
+local cipher = {}
+
+function cipher.new()
+ return cipher
+end
+
+function cipher.encrypt(_, data, _, _)
+ return data
+end
+
+function cipher.decrypt(_, data, _, _, _)
+ return data
+end
+
+return cipher
diff --git a/server/resty/session/compressors/none.lua b/server/resty/session/compressors/none.lua
new file mode 100644
index 0000000..3d14a5c
--- /dev/null
+++ b/server/resty/session/compressors/none.lua
@@ -0,0 +1,15 @@
+local compressor = {}
+
+function compressor.new()
+ return compressor
+end
+
+function compressor.compress(_, data)
+ return data
+end
+
+function compressor.decompress(_, data)
+ return data
+end
+
+return compressor
diff --git a/server/resty/session/compressors/zlib.lua b/server/resty/session/compressors/zlib.lua
new file mode 100644
index 0000000..1d23be0
--- /dev/null
+++ b/server/resty/session/compressors/zlib.lua
@@ -0,0 +1,43 @@
+local zlib = require "ffi-zlib"
+local sio = require "pl.stringio"
+
+local concat = table.concat
+
+local function gzip(func, input)
+ local stream = sio.open(input)
+ local output = {}
+ local n = 0
+
+ local ok, err = func(function(size)
+ return stream:read(size)
+ end, function(data)
+ n = n + 1
+ output[n] = data
+ end, 8192)
+
+ if not ok then
+ return nil, err
+ end
+
+ if n == 0 then
+ return ""
+ end
+
+ return concat(output, nil, 1, n)
+end
+
+local compressor = {}
+
+function compressor.new()
+ return compressor
+end
+
+function compressor.compress(_, data)
+ return gzip(zlib.deflateGzip, data)
+end
+
+function compressor.decompress(_, data)
+ return gzip(zlib.inflateGzip, data)
+end
+
+return compressor
diff --git a/server/resty/session/encoders/base16.lua b/server/resty/session/encoders/base16.lua
new file mode 100644
index 0000000..552f50e
--- /dev/null
+++ b/server/resty/session/encoders/base16.lua
@@ -0,0 +1,29 @@
+local to_hex = require "resty.string".to_hex
+
+local tonumber = tonumber
+local gsub = string.gsub
+local char = string.char
+
+local function chr(c)
+ return char(tonumber(c, 16) or 0)
+end
+
+local encoder = {}
+
+function encoder.encode(value)
+ if not value then
+ return nil, "unable to base16 encode value"
+ end
+
+ return to_hex(value)
+end
+
+function encoder.decode(value)
+ if not value then
+ return nil, "unable to base16 decode value"
+ end
+
+ return (gsub(value, "..", chr))
+end
+
+return encoder
diff --git a/server/resty/session/encoders/base64.lua b/server/resty/session/encoders/base64.lua
new file mode 100644
index 0000000..ddaf4e8
--- /dev/null
+++ b/server/resty/session/encoders/base64.lua
@@ -0,0 +1,39 @@
+local encode_base64 = ngx.encode_base64
+local decode_base64 = ngx.decode_base64
+
+local gsub = string.gsub
+
+local ENCODE_CHARS = {
+ ["+"] = "-",
+ ["/"] = "_",
+}
+
+local DECODE_CHARS = {
+ ["-"] = "+",
+ ["_"] = "/",
+}
+
+local encoder = {}
+
+function encoder.encode(value)
+ if not value then
+ return nil, "unable to base64 encode value"
+ end
+
+ local encoded = encode_base64(value, true)
+ if not encoded then
+ return nil, "unable to base64 encode value"
+ end
+
+ return gsub(encoded, "[+/]", ENCODE_CHARS)
+end
+
+function encoder.decode(value)
+ if not value then
+ return nil, "unable to base64 decode value"
+ end
+
+ return decode_base64((gsub(value, "[-_]", DECODE_CHARS)))
+end
+
+return encoder
diff --git a/server/resty/session/encoders/hex.lua b/server/resty/session/encoders/hex.lua
new file mode 100644
index 0000000..1b94a5a
--- /dev/null
+++ b/server/resty/session/encoders/hex.lua
@@ -0,0 +1 @@
+return require "resty.session.encoders.base16" \ No newline at end of file
diff --git a/server/resty/session/hmac/sha1.lua b/server/resty/session/hmac/sha1.lua
new file mode 100644
index 0000000..1753412
--- /dev/null
+++ b/server/resty/session/hmac/sha1.lua
@@ -0,0 +1 @@
+return ngx.hmac_sha1
diff --git a/server/resty/session/identifiers/random.lua b/server/resty/session/identifiers/random.lua
new file mode 100644
index 0000000..a2f9739
--- /dev/null
+++ b/server/resty/session/identifiers/random.lua
@@ -0,0 +1,13 @@
+local tonumber = tonumber
+local random = require "resty.random".bytes
+local var = ngx.var
+
+local defaults = {
+ length = tonumber(var.session_random_length, 10) or 16
+}
+
+return function(session)
+ local config = session.random or defaults
+ local length = tonumber(config.length, 10) or defaults.length
+ return random(length, true) or random(length)
+end
diff --git a/server/resty/session/serializers/json.lua b/server/resty/session/serializers/json.lua
new file mode 100644
index 0000000..960c4d8
--- /dev/null
+++ b/server/resty/session/serializers/json.lua
@@ -0,0 +1,6 @@
+local json = require "cjson.safe"
+
+return {
+ serialize = json.encode,
+ deserialize = json.decode,
+}
diff --git a/server/resty/session/storage/cookie.lua b/server/resty/session/storage/cookie.lua
new file mode 100644
index 0000000..95e26d1
--- /dev/null
+++ b/server/resty/session/storage/cookie.lua
@@ -0,0 +1,7 @@
+local storage = {}
+
+function storage.new()
+ return storage
+end
+
+return storage
diff --git a/server/resty/session/storage/dshm.lua b/server/resty/session/storage/dshm.lua
new file mode 100644
index 0000000..e6d887f
--- /dev/null
+++ b/server/resty/session/storage/dshm.lua
@@ -0,0 +1,163 @@
+local dshm = require "resty.dshm"
+
+local setmetatable = setmetatable
+local tonumber = tonumber
+local concat = table.concat
+local var = ngx.var
+
+local defaults = {
+ region = var.session_dshm_region or "sessions",
+ connect_timeout = tonumber(var.session_dshm_connect_timeout, 10),
+ read_timeout = tonumber(var.session_dshm_read_timeout, 10),
+ send_timeout = tonumber(var.session_dshm_send_timeout, 10),
+ host = var.session_dshm_host or "127.0.0.1",
+ port = tonumber(var.session_dshm_port, 10) or 4321,
+ pool = {
+ name = var.session_dshm_pool_name,
+ size = tonumber(var.session_dshm_pool_size, 10) or 100,
+ timeout = tonumber(var.session_dshm_pool_timeout, 10) or 1000,
+ backlog = tonumber(var.session_dshm_pool_backlog, 10),
+ },
+}
+
+local storage = {}
+
+storage.__index = storage
+
+function storage.new(session)
+ local config = session.dshm or defaults
+ local pool = config.pool or defaults.pool
+
+ local connect_timeout = tonumber(config.connect_timeout, 10) or defaults.connect_timeout
+
+ local store = dshm:new()
+ if store.set_timeouts then
+ local send_timeout = tonumber(config.send_timeout, 10) or defaults.send_timeout
+ local read_timeout = tonumber(config.read_timeout, 10) or defaults.read_timeout
+
+ if connect_timeout then
+ if send_timeout and read_timeout then
+ store:set_timeouts(connect_timeout, send_timeout, read_timeout)
+ else
+ store:set_timeout(connect_timeout)
+ end
+ end
+
+ elseif store.set_timeout and connect_timeout then
+ store:set_timeout(connect_timeout)
+ end
+
+
+ local self = {
+ store = store,
+ encoder = session.encoder,
+ region = config.region or defaults.region,
+ host = config.host or defaults.host,
+ port = tonumber(config.port, 10) or defaults.port,
+ pool_timeout = tonumber(pool.timeout, 10) or defaults.pool.timeout,
+ connect_opts = {
+ pool = pool.name or defaults.pool.name,
+ pool_size = tonumber(pool.size, 10) or defaults.pool.size,
+ backlog = tonumber(pool.backlog, 10) or defaults.pool.backlog,
+ },
+ }
+
+ return setmetatable(self, storage)
+end
+
+function storage:connect()
+ return self.store:connect(self.host, self.port, self.connect_opts)
+end
+
+function storage:set_keepalive()
+ return self.store:set_keepalive(self.pool_timeout)
+end
+
+function storage:key(id)
+ return concat({ self.region, id }, "::")
+end
+
+function storage:set(key, ttl, data)
+ local ok, err = self:connect()
+ if not ok then
+ return nil, err
+ end
+
+ data, err = self.encoder.encode(data)
+
+ if not data then
+ self:set_keepalive()
+ return nil, err
+ end
+
+ ok, err = self.store:set(key, data, ttl)
+
+ self:set_keepalive()
+
+ return ok, err
+end
+
+function storage:get(key)
+ local ok, err = self:connect()
+ if not ok then
+ return nil, err
+ end
+
+ local data
+ data, err = self.store:get(key)
+ if data then
+ data, err = self.encoder.decode(data)
+ end
+
+ self:set_keepalive()
+
+ return data, err
+end
+
+function storage:delete(key)
+ local ok, err = self:connect()
+ if not ok then
+ return nil, err
+ end
+
+ ok, err = self.store:delete(key)
+
+ self:set_keepalive()
+
+ return ok, err
+end
+
+function storage:touch(key, ttl)
+ local ok, err = self:connect()
+ if not ok then
+ return nil, err
+ end
+
+ ok, err = self.store:touch(key, ttl)
+
+ self:set_keepalive()
+
+ return ok, err
+end
+
+function storage:open(id)
+ local key = self:key(id)
+ return self:get(key)
+end
+
+function storage:save(id, ttl, data)
+ local key = self:key(id)
+ return self:set(key, ttl, data)
+end
+
+function storage:destroy(id)
+ local key = self:key(id)
+ return self:delete(key)
+end
+
+function storage:ttl(id, ttl)
+ local key = self:key(id)
+ return self:touch(key, ttl)
+end
+
+return storage
diff --git a/server/resty/session/storage/memcache.lua b/server/resty/session/storage/memcache.lua
new file mode 100644
index 0000000..da44ba7
--- /dev/null
+++ b/server/resty/session/storage/memcache.lua
@@ -0,0 +1,303 @@
+local memcached = require "resty.memcached"
+local setmetatable = setmetatable
+local tonumber = tonumber
+local concat = table.concat
+local sleep = ngx.sleep
+local null = ngx.null
+local var = ngx.var
+
+local function enabled(value)
+ if value == nil then
+ return nil
+ end
+
+ return value == true
+ or value == "1"
+ or value == "true"
+ or value == "on"
+end
+
+local function ifnil(value, default)
+ if value == nil then
+ return default
+ end
+
+ return enabled(value)
+end
+
+local defaults = {
+ prefix = var.session_memcache_prefix or "sessions",
+ socket = var.session_memcache_socket,
+ host = var.session_memcache_host or "127.0.0.1",
+ uselocking = enabled(var.session_memcache_uselocking or true),
+ connect_timeout = tonumber(var.session_memcache_connect_timeout, 10),
+ read_timeout = tonumber(var.session_memcache_read_timeout, 10),
+ send_timeout = tonumber(var.session_memcache_send_timeout, 10),
+ port = tonumber(var.session_memcache_port, 10) or 11211,
+ spinlockwait = tonumber(var.session_memcache_spinlockwait, 10) or 150,
+ maxlockwait = tonumber(var.session_memcache_maxlockwait, 10) or 30,
+ pool = {
+ name = var.session_memcache_pool_name,
+ timeout = tonumber(var.session_memcache_pool_timeout, 10),
+ size = tonumber(var.session_memcache_pool_size, 10),
+ backlog = tonumber(var.session_memcache_pool_backlog, 10),
+ },
+}
+
+local storage = {}
+
+storage.__index = storage
+
+function storage.new(session)
+ local config = session.memcache or defaults
+ local pool = config.pool or defaults.pool
+ local locking = ifnil(config.uselocking, defaults.uselocking)
+
+ local connect_timeout = tonumber(config.connect_timeout, 10) or defaults.connect_timeout
+
+ local memcache = memcached:new()
+ if memcache.set_timeouts then
+ local send_timeout = tonumber(config.send_timeout, 10) or defaults.send_timeout
+ local read_timeout = tonumber(config.read_timeout, 10) or defaults.read_timeout
+
+ if connect_timeout then
+ if send_timeout and read_timeout then
+ memcache:set_timeouts(connect_timeout, send_timeout, read_timeout)
+ else
+ memcache:set_timeout(connect_timeout)
+ end
+ end
+
+ elseif memcache.set_timeout and connect_timeout then
+ memcache:set_timeout(connect_timeout)
+ end
+
+ local self = {
+ memcache = memcache,
+ prefix = config.prefix or defaults.prefix,
+ uselocking = locking,
+ spinlockwait = tonumber(config.spinlockwait, 10) or defaults.spinlockwait,
+ maxlockwait = tonumber(config.maxlockwait, 10) or defaults.maxlockwait,
+ pool_timeout = tonumber(pool.timeout, 10) or defaults.pool.timeout,
+ connect_opts = {
+ pool = pool.name or defaults.pool.name,
+ pool_size = tonumber(pool.size, 10) or defaults.pool.size,
+ backlog = tonumber(pool.backlog, 10) or defaults.pool.backlog,
+ },
+ }
+
+ local socket = config.socket or defaults.socket
+ if socket and socket ~= "" then
+ self.socket = socket
+ else
+ self.host = config.host or defaults.host
+ self.port = config.port or defaults.port
+ end
+
+ return setmetatable(self, storage)
+end
+
+function storage:connect()
+ local socket = self.socket
+ if socket then
+ return self.memcache:connect(socket, self.connect_opts)
+ end
+ return self.memcache:connect(self.host, self.port, self.connect_opts)
+end
+
+function storage:set_keepalive()
+ return self.memcache:set_keepalive(self.pool_timeout)
+end
+
+function storage:key(id)
+ return concat({ self.prefix, id }, ":" )
+end
+
+function storage:lock(key)
+ if not self.uselocking or self.locked then
+ return true
+ end
+
+ if not self.token then
+ self.token = var.request_id
+ end
+
+ local lock_key = concat({ key, "lock" }, "." )
+ local lock_ttl = self.maxlockwait + 1
+ local attempts = (1000 / self.spinlockwait) * self.maxlockwait
+ local waittime = self.spinlockwait / 1000
+
+ for _ = 1, attempts do
+ local ok = self.memcache:add(lock_key, self.token, lock_ttl)
+ if ok then
+ self.locked = true
+ return true
+ end
+
+ sleep(waittime)
+ end
+
+ return false, "unable to acquire a session lock"
+end
+
+function storage:unlock(key)
+ if not self.uselocking or not self.locked then
+ return true
+ end
+
+ local lock_key = concat({ key, "lock" }, "." )
+ local token = self:get(lock_key)
+
+ if token == self.token then
+ self.memcache:delete(lock_key)
+ self.locked = nil
+ end
+end
+
+function storage:get(key)
+ local data, err = self.memcache:get(key)
+ if not data then
+ return nil, err
+ end
+
+ if data == null then
+ return nil
+ end
+
+ return data
+end
+
+function storage:set(key, data, ttl)
+ return self.memcache:set(key, data, ttl)
+end
+
+function storage:expire(key, ttl)
+ return self.memcache:touch(key, ttl)
+end
+
+function storage:delete(key)
+ return self.memcache:delete(key)
+end
+
+function storage:open(id, keep_lock)
+ local ok, err = self:connect()
+ if not ok then
+ return nil, err
+ end
+
+ local key = self:key(id)
+
+ ok, err = self:lock(key)
+ if not ok then
+ self:set_keepalive()
+ return nil, err
+ end
+
+ local data
+ data, err = self:get(key)
+
+ if err or not data or not keep_lock then
+ self:unlock(key)
+ end
+
+ self:set_keepalive()
+
+ return data, err
+end
+
+function storage:start(id)
+ if not self.uselocking or not self.locked then
+ return true
+ end
+
+ local ok, err = self:connect()
+ if not ok then
+ return nil, err
+ end
+
+ local key = self:key(id)
+
+ ok, err = self:lock(key)
+
+ self:set_keepalive()
+
+ return ok, err
+end
+
+function storage:save(id, ttl, data, close)
+ local ok, err = self:connect()
+ if not ok then
+ return nil, err
+ end
+
+ local key = self:key(id)
+
+ ok, err = self:set(key, data, ttl)
+
+ if close then
+ self:unlock(key)
+ end
+
+ self:set_keepalive()
+
+ if not ok then
+ return nil, err
+ end
+
+ return true
+end
+
+function storage:close(id)
+ if not self.uselocking or not self.locked then
+ return true
+ end
+
+ local ok, err = self:connect()
+ if not ok then
+ return nil, err
+ end
+
+ local key = self:key(id)
+
+ self:unlock(key)
+ self:set_keepalive()
+
+ return true
+end
+
+function storage:destroy(id)
+ local ok, err = self:connect()
+ if not ok then
+ return nil, err
+ end
+
+ local key = self:key(id)
+
+ ok, err = self:delete(key)
+
+ self:unlock(key)
+ self:set_keepalive()
+
+ return ok, err
+end
+
+function storage:ttl(id, ttl, close)
+ local ok, err = self:connect()
+ if not ok then
+ return nil, err
+ end
+
+ local key = self:key(id)
+
+ ok, err = self:expire(key, ttl)
+
+ if close then
+ self:unlock(key)
+ end
+
+ self:set_keepalive()
+
+ return ok, err
+end
+
+return storage
diff --git a/server/resty/session/storage/memcached.lua b/server/resty/session/storage/memcached.lua
new file mode 100644
index 0000000..0ecc508
--- /dev/null
+++ b/server/resty/session/storage/memcached.lua
@@ -0,0 +1 @@
+return require "resty.session.storage.memcache"
diff --git a/server/resty/session/storage/redis.lua b/server/resty/session/storage/redis.lua
new file mode 100644
index 0000000..3de0472
--- /dev/null
+++ b/server/resty/session/storage/redis.lua
@@ -0,0 +1,478 @@
+local setmetatable = setmetatable
+local tonumber = tonumber
+local type = type
+local reverse = string.reverse
+local gmatch = string.gmatch
+local find = string.find
+local byte = string.byte
+local sub = string.sub
+local concat = table.concat
+local sleep = ngx.sleep
+local null = ngx.null
+local var = ngx.var
+
+local LB = byte("[")
+local RB = byte("]")
+
+local function parse_cluster_nodes(nodes)
+ if not nodes or nodes == "" then
+ return nil
+ end
+
+ if type(nodes) == "table" then
+ return nodes
+ end
+
+ local addrs
+ local i
+ for node in gmatch(nodes, "%S+") do
+ local ip = node
+ local port = 6379
+ local pos = find(reverse(ip), ":", 2, true)
+ if pos then
+ local p = tonumber(sub(ip, -pos + 1), 10)
+ if p >= 1 and p <= 65535 then
+ local addr = sub(ip, 1, -pos - 1)
+ if find(addr, ":", 1, true) then
+ if byte(addr, -1) == RB then
+ ip = addr
+ port = p
+ end
+
+ else
+ ip = addr
+ port = p
+ end
+ end
+ end
+
+ if byte(ip, 1, 1) == LB then
+ ip = sub(ip, 2)
+ end
+
+ if byte(ip, -1) == RB then
+ ip = sub(ip, 1, -2)
+ end
+
+ if not addrs then
+ i = 1
+ addrs = {{
+ ip = ip,
+ port = port,
+ }}
+ else
+ i = i + 1
+ addrs[i] = {
+ ip = ip,
+ port = port,
+ }
+ end
+ end
+
+ if not i then
+ return
+ end
+
+ return addrs
+end
+
+local redis_single = require "resty.redis"
+local redis_cluster
+do
+ local pcall = pcall
+ local require = require
+ local ok
+ ok, redis_cluster = pcall(require, "resty.rediscluster")
+ if not ok then
+ ok, redis_cluster = pcall(require, "rediscluster")
+ if not ok then
+ redis_cluster = nil
+ end
+ end
+end
+
+local UNLOCK = [[
+if redis.call("GET", KEYS[1]) == ARGV[1] then
+ return redis.call("DEL", KEYS[1])
+else
+ return 0
+end
+]]
+
+local function enabled(value)
+ if value == nil then return nil end
+ return value == true or (value == "1" or value == "true" or value == "on")
+end
+
+local function ifnil(value, default)
+ if value == nil then
+ return default
+ end
+
+ return enabled(value)
+end
+
+local defaults = {
+ prefix = var.session_redis_prefix or "sessions",
+ socket = var.session_redis_socket,
+ host = var.session_redis_host or "127.0.0.1",
+ username = var.session_redis_username,
+ password = var.session_redis_password or var.session_redis_auth,
+ server_name = var.session_redis_server_name,
+ ssl = enabled(var.session_redis_ssl) or false,
+ ssl_verify = enabled(var.session_redis_ssl_verify) or false,
+ uselocking = enabled(var.session_redis_uselocking or true),
+ port = tonumber(var.session_redis_port, 10) or 6379,
+ database = tonumber(var.session_redis_database, 10) or 0,
+ connect_timeout = tonumber(var.session_redis_connect_timeout, 10),
+ read_timeout = tonumber(var.session_redis_read_timeout, 10),
+ send_timeout = tonumber(var.session_redis_send_timeout, 10),
+ spinlockwait = tonumber(var.session_redis_spinlockwait, 10) or 150,
+ maxlockwait = tonumber(var.session_redis_maxlockwait, 10) or 30,
+ pool = {
+ name = var.session_redis_pool_name,
+ timeout = tonumber(var.session_redis_pool_timeout, 10),
+ size = tonumber(var.session_redis_pool_size, 10),
+ backlog = tonumber(var.session_redis_pool_backlog, 10),
+ },
+}
+
+
+if redis_cluster then
+ defaults.cluster = {
+ name = var.session_redis_cluster_name,
+ dict = var.session_redis_cluster_dict,
+ maxredirections = tonumber(var.session_redis_cluster_maxredirections, 10),
+ nodes = parse_cluster_nodes(var.session_redis_cluster_nodes),
+ }
+end
+
+local storage = {}
+
+storage.__index = storage
+
+function storage.new(session)
+ local config = session.redis or defaults
+ local pool = config.pool or defaults.pool
+ local cluster = config.cluster or defaults.cluster
+ local locking = ifnil(config.uselocking, defaults.uselocking)
+
+ local self = {
+ prefix = config.prefix or defaults.prefix,
+ uselocking = locking,
+ spinlockwait = tonumber(config.spinlockwait, 10) or defaults.spinlockwait,
+ maxlockwait = tonumber(config.maxlockwait, 10) or defaults.maxlockwait,
+ }
+
+ local username = config.username or defaults.username
+ if username == "" then
+ username = nil
+ end
+ local password = config.password or config.auth or defaults.password
+ if password == "" then
+ password = nil
+ end
+
+ local connect_timeout = tonumber(config.connect_timeout, 10) or defaults.connect_timeout
+
+ local cluster_nodes
+ if redis_cluster then
+ cluster_nodes = parse_cluster_nodes(cluster.nodes or defaults.cluster.nodes)
+ end
+
+ local connect_opts = {
+ pool = pool.name or defaults.pool.name,
+ pool_size = tonumber(pool.size, 10) or defaults.pool.size,
+ backlog = tonumber(pool.backlog, 10) or defaults.pool.backlog,
+ server_name = config.server_name or defaults.server_name,
+ ssl = ifnil(config.ssl, defaults.ssl),
+ ssl_verify = ifnil(config.ssl_verify, defaults.ssl_verify),
+ }
+
+ if cluster_nodes then
+ self.redis = redis_cluster:new({
+ name = cluster.name or defaults.cluster.name,
+ dict_name = cluster.dict or defaults.cluster.dict,
+ username = var.session_redis_username,
+ password = var.session_redis_password or defaults.password,
+ connection_timout = connect_timeout, -- typo in library
+ connection_timeout = connect_timeout,
+ keepalive_timeout = tonumber(pool.timeout, 10) or defaults.pool.timeout,
+ keepalive_cons = tonumber(pool.size, 10) or defaults.pool.size,
+ max_redirection = tonumber(cluster.maxredirections, 10) or defaults.cluster.maxredirections,
+ serv_list = cluster_nodes,
+ connect_opts = connect_opts,
+ })
+ self.cluster = true
+
+ else
+ local redis = redis_single:new()
+
+ if redis.set_timeouts then
+ local send_timeout = tonumber(config.send_timeout, 10) or defaults.send_timeout
+ local read_timeout = tonumber(config.read_timeout, 10) or defaults.read_timeout
+
+ if connect_timeout then
+ if send_timeout and read_timeout then
+ redis:set_timeouts(connect_timeout, send_timeout, read_timeout)
+ else
+ redis:set_timeout(connect_timeout)
+ end
+ end
+
+ elseif redis.set_timeout and connect_timeout then
+ redis:set_timeout(connect_timeout)
+ end
+
+ self.redis = redis
+ self.username = username
+ self.password = password
+ self.database = tonumber(config.database, 10) or defaults.database
+ self.pool_timeout = tonumber(pool.timeout, 10) or defaults.pool.timeout
+ self.connect_opts = connect_opts
+
+ local socket = config.socket or defaults.socket
+ if socket and socket ~= "" then
+ self.socket = socket
+ else
+ self.host = config.host or defaults.host
+ self.port = config.port or defaults.port
+ end
+ end
+
+ return setmetatable(self, storage)
+end
+
+function storage:connect()
+ if self.cluster then
+ return true -- cluster handles this on its own
+ end
+
+ local ok, err
+ if self.socket then
+ ok, err = self.redis:connect(self.socket, self.connect_opts)
+ else
+ ok, err = self.redis:connect(self.host, self.port, self.connect_opts)
+ end
+
+ if not ok then
+ return nil, err
+ end
+
+ if self.password and self.redis:get_reused_times() == 0 then
+ -- usernames are supported only on Redis 6+, so use new AUTH form only when absolutely necessary
+ if self.username then
+ ok, err = self.redis:auth(self.username, self.password)
+ else
+ ok, err = self.redis:auth(self.password)
+ end
+ if not ok then
+ self.redis:close()
+ return nil, err
+ end
+ end
+
+ if self.database ~= 0 then
+ ok, err = self.redis:select(self.database)
+ if not ok then
+ self.redis:close()
+ end
+ end
+
+ return ok, err
+end
+
+function storage:set_keepalive()
+ if self.cluster then
+ return true -- cluster handles this on its own
+ end
+
+ return self.redis:set_keepalive(self.pool_timeout)
+end
+
+function storage:key(id)
+ return concat({ self.prefix, id }, ":" )
+end
+
+function storage:lock(key)
+ if not self.uselocking or self.locked then
+ return true
+ end
+
+ if not self.token then
+ self.token = var.request_id
+ end
+
+ local lock_key = concat({ key, "lock" }, "." )
+ local lock_ttl = self.maxlockwait + 1
+ local attempts = (1000 / self.spinlockwait) * self.maxlockwait
+ local waittime = self.spinlockwait / 1000
+
+ for _ = 1, attempts do
+ local ok = self.redis:set(lock_key, self.token, "EX", lock_ttl, "NX")
+ if ok ~= null then
+ self.locked = true
+ return true
+ end
+
+ sleep(waittime)
+ end
+
+ return false, "unable to acquire a session lock"
+end
+
+function storage:unlock(key)
+ if not self.uselocking or not self.locked then
+ return
+ end
+
+ local lock_key = concat({ key, "lock" }, "." )
+
+ self.redis:eval(UNLOCK, 1, lock_key, self.token)
+ self.locked = nil
+end
+
+function storage:get(key)
+ local data, err = self.redis:get(key)
+ if not data then
+ return nil, err
+ end
+
+ if data == null then
+ return nil
+ end
+
+ return data
+end
+
+function storage:set(key, data, lifetime)
+ return self.redis:setex(key, lifetime, data)
+end
+
+function storage:expire(key, lifetime)
+ return self.redis:expire(key, lifetime)
+end
+
+function storage:delete(key)
+ return self.redis:del(key)
+end
+
+function storage:open(id, keep_lock)
+ local ok, err = self:connect()
+ if not ok then
+ return nil, err
+ end
+
+ local key = self:key(id)
+
+ ok, err = self:lock(key)
+ if not ok then
+ self:set_keepalive()
+ return nil, err
+ end
+
+ local data
+ data, err = self:get(key)
+
+ if err or not data or not keep_lock then
+ self:unlock(key)
+ end
+ self:set_keepalive()
+
+ return data, err
+end
+
+function storage:start(id)
+ if not self.uselocking or not self.locked then
+ return true
+ end
+
+ local ok, err = self:connect()
+ if not ok then
+ return nil, err
+ end
+
+ ok, err = self:lock(self:key(id))
+
+ self:set_keepalive()
+
+ return ok, err
+end
+
+function storage:save(id, ttl, data, close)
+ local ok, err = self:connect()
+ if not ok then
+ return nil, err
+ end
+
+ local key = self:key(id)
+
+ ok, err = self:set(key, data, ttl)
+
+ if close then
+ self:unlock(key)
+ end
+
+ self:set_keepalive()
+
+ if not ok then
+ return nil, err
+ end
+
+ return true
+end
+
+function storage:close(id)
+ if not self.uselocking or not self.locked then
+ return true
+ end
+
+ local ok, err = self:connect()
+ if not ok then
+ return nil, err
+ end
+
+ local key = self:key(id)
+
+ self:unlock(key)
+ self:set_keepalive()
+
+ return true
+end
+
+function storage:destroy(id)
+ local ok, err = self:connect()
+ if not ok then
+ return nil, err
+ end
+
+ local key = self:key(id)
+
+ ok, err = self:delete(key)
+
+ self:unlock(key)
+ self:set_keepalive()
+
+ return ok, err
+end
+
+function storage:ttl(id, ttl, close)
+ local ok, err = self:connect()
+ if not ok then
+ return nil, err
+ end
+
+ local key = self:key(id)
+
+ ok, err = self:expire(key, ttl)
+
+ if close then
+ self:unlock(key)
+ end
+
+ self:set_keepalive()
+
+ return ok, err
+end
+
+return storage
diff --git a/server/resty/session/storage/shm.lua b/server/resty/session/storage/shm.lua
new file mode 100644
index 0000000..6f81435
--- /dev/null
+++ b/server/resty/session/storage/shm.lua
@@ -0,0 +1,125 @@
+local lock = require "resty.lock"
+
+local setmetatable = setmetatable
+local tonumber = tonumber
+local concat = table.concat
+local var = ngx.var
+local shared = ngx.shared
+
+local function enabled(value)
+ if value == nil then return nil end
+ return value == true or (value == "1" or value == "true" or value == "on")
+end
+
+local function ifnil(value, default)
+ if value == nil then
+ return default
+ end
+
+ return enabled(value)
+end
+
+local defaults = {
+ store = var.session_shm_store or "sessions",
+ uselocking = enabled(var.session_shm_uselocking or true),
+ lock = {
+ exptime = tonumber(var.session_shm_lock_exptime, 10) or 30,
+ timeout = tonumber(var.session_shm_lock_timeout, 10) or 5,
+ step = tonumber(var.session_shm_lock_step, 10) or 0.001,
+ ratio = tonumber(var.session_shm_lock_ratio, 10) or 2,
+ max_step = tonumber(var.session_shm_lock_max_step, 10) or 0.5,
+ }
+}
+
+local storage = {}
+
+storage.__index = storage
+
+function storage.new(session)
+ local config = session.shm or defaults
+ local store = config.store or defaults.store
+ local locking = ifnil(config.uselocking, defaults.uselocking)
+
+ local self = {
+ store = shared[store],
+ uselocking = locking,
+ }
+
+ if locking then
+ local lock_opts = config.lock or defaults.lock
+ local opts = {
+ exptime = tonumber(lock_opts.exptime, 10) or defaults.exptime,
+ timeout = tonumber(lock_opts.timeout, 10) or defaults.timeout,
+ step = tonumber(lock_opts.step, 10) or defaults.step,
+ ratio = tonumber(lock_opts.ratio, 10) or defaults.ratio,
+ max_step = tonumber(lock_opts.max_step, 10) or defaults.max_step,
+ }
+ self.lock = lock:new(store, opts)
+ end
+
+ return setmetatable(self, storage)
+end
+
+function storage:open(id, keep_lock)
+ if self.uselocking then
+ local ok, err = self.lock:lock(concat{ id, ".lock" })
+ if not ok then
+ return nil, err
+ end
+ end
+
+ local data, err = self.store:get(id)
+
+ if self.uselocking and (err or not data or not keep_lock) then
+ self.lock:unlock()
+ end
+
+ return data, err
+end
+
+function storage:start(id)
+ if self.uselocking then
+ return self.lock:lock(concat{ id, ".lock" })
+ end
+
+ return true
+end
+
+function storage:save(id, ttl, data, close)
+ local ok, err = self.store:set(id, data, ttl)
+ if close and self.uselocking then
+ self.lock:unlock()
+ end
+
+ return ok, err
+end
+
+function storage:close()
+ if self.uselocking then
+ self.lock:unlock()
+ end
+
+ return true
+end
+
+function storage:destroy(id)
+ self.store:delete(id)
+
+ if self.uselocking then
+ self.lock:unlock()
+ end
+
+ return true
+end
+
+function storage:ttl(id, lifetime, close)
+ local ok, err = self.store:expire(id, lifetime)
+
+ if close and self.uselocking then
+ self.lock:unlock()
+ end
+
+ return ok, err
+end
+
+return storage
diff --git a/server/resty/session/strategies/default.lua b/server/resty/session/strategies/default.lua
new file mode 100644
index 0000000..a43ef5a
--- /dev/null
+++ b/server/resty/session/strategies/default.lua
@@ -0,0 +1,232 @@
+local type = type
+local concat = table.concat
+
+local strategy = {}
+
+function strategy.load(session, cookie, key, keep_lock)
+ local storage = session.storage
+ local id = cookie.id
+ local id_encoded = session.encoder.encode(id)
+
+ local data, err, tag
+ if storage.open then
+ data, err = storage:open(id_encoded, keep_lock)
+ if not data then
+ return nil, err or "cookie data was not found"
+ end
+
+ else
+ data = cookie.data
+ end
+
+ local expires = cookie.expires
+ local usebefore = cookie.usebefore
+ local hash = cookie.hash
+
+ if not key then
+ key = concat{ id, expires, usebefore }
+ end
+
+ local hkey = session.hmac(session.secret, key)
+
+ data, err, tag = session.cipher:decrypt(data, hkey, id, session.key, hash)
+ if not data then
+ if storage.close then
+ storage:close(id_encoded)
+ end
+
+ return nil, err or "unable to decrypt data"
+ end
+
+ if tag then
+ if tag ~= hash then
+ if storage.close then
+ storage:close(id_encoded)
+ end
+
+ return nil, "cookie has invalid tag"
+ end
+
+ else
+ local input = concat{ key, data, session.key }
+ if session.hmac(hkey, input) ~= hash then
+ if storage.close then
+ storage:close(id_encoded)
+ end
+
+ return nil, "cookie has invalid signature"
+ end
+ end
+
+ data, err = session.compressor:decompress(data)
+ if not data then
+ if storage.close then
+ storage:close(id_encoded)
+ end
+
+ return nil, err or "unable to decompress data"
+ end
+
+ data, err = session.serializer.deserialize(data)
+ if not data then
+ if storage.close then
+ storage:close(id_encoded)
+ end
+
+ return nil, err or "unable to deserialize data"
+ end
+
+ session.id = id
+ session.expires = expires
+ session.usebefore = usebefore
+ session.data = type(data) == "table" and data or {}
+ session.present = true
+
+ return true
+end
+
+function strategy.open(session, cookie, keep_lock)
+ return strategy.load(session, cookie, nil, keep_lock)
+end
+
+function strategy.start(session)
+ local storage = session.storage
+ if not storage.start then
+ return true
+ end
+
+ local id_encoded = session.encoder.encode(session.id)
+
+ local ok, err = storage:start(id_encoded)
+ if not ok then
+ return nil, err or "unable to start session"
+ end
+
+ return true
+end
+
+function strategy.modify(session, action, close, key)
+ local id = session.id
+ local id_encoded = session.encoder.encode(id)
+ local storage = session.storage
+ local expires = session.expires
+ local usebefore = session.usebefore
+ local ttl = expires - session.now
+
+ if ttl <= 0 then
+ if storage.close then
+ storage:close(id_encoded)
+ end
+
+ return nil, "session is already expired"
+ end
+
+ if not key then
+ key = concat{ id, expires, usebefore }
+ end
+
+ local data, err = session.serializer.serialize(session.data)
+ if not data then
+ if close and storage.close then
+ storage:close(id_encoded)
+ end
+
+ return nil, err or "unable to serialize data"
+ end
+
+ data, err = session.compressor:compress(data)
+ if not data then
+ if close and storage.close then
+ storage:close(id_encoded)
+ end
+
+ return nil, err or "unable to compress data"
+ end
+
+ local hkey = session.hmac(session.secret, key)
+
+ local encrypted_data, tag
+ encrypted_data, err, tag = session.cipher:encrypt(data, hkey, id, session.key)
+ if not encrypted_data then
+ if close and storage.close then
+ storage:close(id_encoded)
+ end
+
+ return nil, err
+ end
+
+ local hash
+ if tag then
+ hash = tag
+ else
+ -- it would be better to calculate signature from encrypted_data,
+ -- but this is kept for backward compatibility
+ hash = session.hmac(hkey, concat{ key, data, session.key })
+ end
+
+ if action == "save" and storage.save then
+ local ok
+ ok, err = storage:save(id_encoded, ttl, encrypted_data, close)
+ if not ok then
+ return nil, err
+ end
+ elseif close and storage.close then
+ local ok
+ ok, err = storage:close(id_encoded)
+ if not ok then
+ return nil, err
+ end
+ end
+
+ if usebefore then
+ expires = expires .. ":" .. usebefore
+ end
+
+ hash = session.encoder.encode(hash)
+
+ local cookie
+ if storage.save then
+ cookie = concat({ id_encoded, expires, hash }, "|")
+ else
+ local encoded_data = session.encoder.encode(encrypted_data)
+ cookie = concat({ id_encoded, expires, encoded_data, hash }, "|")
+ end
+
+ return cookie
+end
+
+function strategy.touch(session, close)
+ return strategy.modify(session, "touch", close)
+end
+
+function strategy.save(session, close)
+ return strategy.modify(session, "save", close)
+end
+
+function strategy.destroy(session)
+ local id = session.id
+ if id then
+ local storage = session.storage
+ if storage.destroy then
+ return storage:destroy(session.encoder.encode(id))
+ elseif storage.close then
+ return storage:close(session.encoder.encode(id))
+ end
+ end
+
+ return true
+end
+
+function strategy.close(session)
+ local id = session.id
+ if id then
+ local storage = session.storage
+ if storage.close then
+ return storage:close(session.encoder.encode(id))
+ end
+ end
+
+ return true
+end
+
+return strategy
diff --git a/server/resty/session/strategies/regenerate.lua b/server/resty/session/strategies/regenerate.lua
new file mode 100644
index 0000000..f2a97dd
--- /dev/null
+++ b/server/resty/session/strategies/regenerate.lua
@@ -0,0 +1,43 @@
+local default = require "resty.session.strategies.default"
+
+local concat = table.concat
+
+local strategy = {
+ regenerate = true,
+ start = default.start,
+ destroy = default.destroy,
+ close = default.close,
+}
+
+local function key(source)
+ if source.usebefore then
+ return concat{ source.id, source.usebefore }
+ end
+
+ return source.id
+end
+
+function strategy.open(session, cookie, keep_lock)
+ return default.load(session, cookie, key(cookie), keep_lock)
+end
+
+function strategy.touch(session, close)
+ return default.modify(session, "touch", close, key(session))
+end
+
+function strategy.save(session, close)
+ if session.present then
+ local storage = session.storage
+ if storage.ttl then
+ storage:ttl(session.encoder.encode(session.id), session.cookie.discard, true)
+ elseif storage.close then
+ storage:close(session.encoder.encode(session.id))
+ end
+
+ session.id = session:identifier()
+ end
+
+ return default.modify(session, "save", close, key(session))
+end
+
+return strategy
diff --git a/sonar-project.properties b/sonar-project.properties
new file mode 100644
index 0000000..df9a2ac
--- /dev/null
+++ b/sonar-project.properties
@@ -0,0 +1,33 @@
+# TODO check
+sonar.projectKey=tnap.SONAR.portal.portal-ui
+sonar.projectName=portal-ui
+sonar.projectDescription="UI Part of Team Tesla's Portal"
+
+
+# Scan settings.
+sonar.projectBaseDir=.
+sonar.java.binaries=.
+
+#Comma-separated paths to directories containing the compiled bytecode files corresponding to your test files
+#sonar.java.libraries=
+
+#Comma-separated paths to files with third-party libraries (JAR or Zip files) used by your project. Wildcards can be used:
+sonar.sources=./src
+
+# Define the directories that should be scanned. Comma separated.
+#sonar.sourceEncoding=UTF-8
+
+#sonar.exclusions=,**/coverage/**
+sonar.exclusions=**/node_modules/**, **/*.css
+
+sonar.dependencyCheck.skip=true
+
+# Fail CI pipeline if Sonar fails.
+sonar.qualitygate.wait=true
+
+# Comma-delimited list of test file path patterns to be included in analysis. When set, only test files matching
+# the paths set here will be included in analysis.
+sonar.test.inclusions=**/*.spec.ts
+
+# Comma-delimited list of paths to LCOV coverage report files. Paths may be absolute or relative to project root.
+sonar.typescript.lcov.reportPaths=coverage/lcov.info
diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts
new file mode 100644
index 0000000..0f5a50e
--- /dev/null
+++ b/src/app/app-routing.module.ts
@@ -0,0 +1,55 @@
+/*
+ * Copyright (c) 2022. Deutsche Telekom AG
+ *
+ * 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
+ *
+ * http://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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+
+import { NgModule } from '@angular/core';
+import { RouterModule, Routes } from '@angular/router';
+import { PageNotFoundComponent } from './components/page-not-found/page-not-found';
+import { StatusPageComponent } from './components/shared/status-page/status-page.component';
+
+const routes: Routes = [
+ {
+ path: 'dashboard',
+ loadChildren: () => import('./modules/dashboard/dashboard.module').then(m => m.DashboardModule),
+ },
+ {
+ path: 'app-starter',
+ loadChildren: () => import('./modules/app-starter/app-starter.module').then(m => m.AppStarterModule),
+ },
+ { path: 'statusPage', component: StatusPageComponent },
+ { path: '', redirectTo: '/dashboard', pathMatch: 'full' },
+ {
+ path: 'user-administration',
+ loadChildren: () =>
+ import('./modules/user-administration/user-administration.module').then(m => m.UserAdministrationModule),
+ },
+ {
+ path: 'not-found',
+ component: PageNotFoundComponent,
+ },
+ {
+ path: '**',
+ redirectTo: 'not-found',
+ },
+];
+
+@NgModule({
+ imports: [RouterModule.forRoot(routes, { relativeLinkResolution: 'legacy', onSameUrlNavigation: 'reload' })],
+ exports: [RouterModule],
+})
+export class AppRoutingModule {}
diff --git a/src/app/app.component.css b/src/app/app.component.css
new file mode 100644
index 0000000..7c4cb60
--- /dev/null
+++ b/src/app/app.component.css
@@ -0,0 +1,82 @@
+/*
+ * Copyright (c) 2022. Deutsche Telekom AG
+ *
+ * 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
+ *
+ * http://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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+
+*:focus {
+ outline: 3px dashed;
+}
+
+.wrapper {
+ display: flex;
+ height: calc(100vh - 55px);
+ width: 100%;
+ align-items: stretch;
+}
+
+a[data-toggle='collapse'] {
+ position: relative;
+}
+
+.dropdown-toggle::after {
+ display: block;
+ position: absolute;
+ top: 50%;
+ right: 20px;
+ transform: translateY(-50%);
+}
+
+.bi-list:focus,
+.bi-list:hover {
+ outline: none;
+}
+
+.bi-list:hover {
+ background-color: #cb026e;
+ border-radius: 2px;
+}
+
+main.container-fluid {
+ overflow-x: hidden;
+}
+
+.loading-spinner {
+ position: fixed;
+ top: 0;
+ left: 0;
+ bottom: 0;
+ right: 0;
+ z-index: 1001;
+ /*background-color: rgba(0, 0, 0, 0.5);*/
+}
+
+.large-spinner {
+ width: 6rem;
+ height: 6rem;
+}
+
+.loading-text {
+ width: 90px;
+ position: absolute;
+ top: calc(50% - 15px);
+ left: calc(50% - 45px);
+ text-align: center;
+}
+
+.spinner-border {
+ animation: spinner-border 1s linear infinite !important;
+}
diff --git a/src/app/app.component.html b/src/app/app.component.html
new file mode 100644
index 0000000..327725c
--- /dev/null
+++ b/src/app/app.component.html
@@ -0,0 +1,52 @@
+<!--
+ ~ Copyright (c) 2022. Deutsche Telekom AG
+ ~
+ ~ 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
+ ~
+ ~ http://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.
+ ~
+ ~ SPDX-License-Identifier: Apache-2.0
+ -->
+
+<app-header
+ (collapse)="collapseChanged()"
+ class="sticky-top"
+></app-header>
+<div class="wrapper">
+ <aside class="h-100 pt-2" id="nav-sidebar" [attr.aria-label]="'layout.sidebar' | translate">
+ <app-sidemenu [isSidebarCollapsed]="isCollapsed"></app-sidemenu>
+ </aside>
+ <main
+ [attr.accesskey]='ACCESS_KEY.SHORTCUT_2'
+ id="main"
+ tabindex="-1"
+ class="container-fluid px-5 position-relative"
+ [attr.aria-label]="'layout.main.mainContent' | translate"
+ >
+ <ng-container *ngIf="loading$ | async">
+ <div
+ [style.left.px]="isCollapsed ? 60 : 250"
+ class="row justify-content-center align-items-center loading-spinner"
+ >
+ <div class="loading-text text-primary">{{ 'common.loading' | translate }}</div>
+ <div class="spinner-border text-primary large-spinner" role="status"></div>
+ </div>
+ </ng-container>
+ <div
+ class="position-absolute right-10 mt-3"
+ style="z-index: 999"
+ [attr.aria-label]="'layout.main.alerts' | translate"
+ >
+ <app-alert></app-alert>
+ </div>
+ <router-outlet class="p-2"></router-outlet>
+ </main>
+</div>
diff --git a/src/app/app.component.spec.ts b/src/app/app.component.spec.ts
new file mode 100644
index 0000000..8e7c92c
--- /dev/null
+++ b/src/app/app.component.spec.ts
@@ -0,0 +1,64 @@
+/*
+ * Copyright (c) 2022. Deutsche Telekom AG
+ *
+ * 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
+ *
+ * http://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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+
+import { TestBed, waitForAsync } from '@angular/core/testing';
+import { RouterTestingModule } from '@angular/router/testing';
+import { AppComponent } from './app.component';
+import { AppStarterComponent } from './modules/app-starter/app-starter.component';
+import { OAuthLogger, OAuthService, UrlHelperService } from 'angular-oauth2-oidc';
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { AlertModule } from './modules/alerting';
+import { SidemenuComponent } from './components/layout/sidemenu/sidemenu.component';
+import { HeaderComponent } from './components/layout/header/header/header.component';
+import { TranslateModule } from '@ngx-translate/core';
+import { HasPermissionsDirective } from './directives/has-permissions.directive';
+import { LoadingIndicatorService } from './services/loading-indicator.service';
+
+describe('AppComponent', () => {
+ let component: AppComponent;
+ beforeEach(
+ waitForAsync(() => {
+ TestBed.configureTestingModule({
+ declarations: [
+ AppComponent,
+ AppStarterComponent,
+ SidemenuComponent,
+ HeaderComponent,
+ HasPermissionsDirective,
+ ],
+ imports: [RouterTestingModule, HttpClientTestingModule, AlertModule, TranslateModule],
+ providers: [
+ AppComponent,
+ UrlHelperService,
+ OAuthService,
+ OAuthLogger,
+ LoadingIndicatorService
+ ],
+ }).compileComponents();
+ component = TestBed.inject(AppComponent);
+ }),
+ );
+ it('should create the app', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it(`should have as title 'frontend'`, () => {
+ expect(component.title).toEqual('frontend');
+ });
+});
diff --git a/src/app/app.component.ts b/src/app/app.component.ts
new file mode 100644
index 0000000..1eacc21
--- /dev/null
+++ b/src/app/app.component.ts
@@ -0,0 +1,50 @@
+/*
+ * Copyright (c) 2022. Deutsche Telekom AG
+ *
+ * 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
+ *
+ * http://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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+
+import { Component, Inject } from '@angular/core';
+import { AlertService } from './modules/alerting';
+import { environment } from '../environments/environment';
+import { DOCUMENT } from '@angular/common';
+import { LoadingIndicatorService } from 'src/app/services/loading-indicator.service';
+import { KeyboardShortcuts } from './services/shortcut.service';
+
+@Component({
+ selector: 'app-root',
+ templateUrl: './app.component.html',
+ styleUrls: ['./app.component.css'],
+})
+export class AppComponent {
+ title = 'frontend';
+ isCustomStyleEnabled = environment.customStyleEnabled;
+ readonly loading$ = this.loadingIndicator.isVisible();
+ public isCollapsed = false
+ public ACCESS_KEY = KeyboardShortcuts;
+
+ constructor(
+ protected alertService: AlertService,
+ private loadingIndicator: LoadingIndicatorService,
+ @Inject(DOCUMENT) private document: Document,
+ ) {
+ }
+
+ collapseChanged() {
+ this.isCollapsed = !this.isCollapsed;
+
+ }
+}
diff --git a/src/app/app.module.ts b/src/app/app.module.ts
new file mode 100644
index 0000000..faeb566
--- /dev/null
+++ b/src/app/app.module.ts
@@ -0,0 +1,103 @@
+/*
+ * Copyright (c) 2022. Deutsche Telekom AG
+ *
+ * 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
+ *
+ * http://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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+
+import { BrowserModule } from '@angular/platform-browser';
+import { NgModule } from '@angular/core';
+import { AppRoutingModule } from './app-routing.module';
+import { AppComponent } from './app.component';
+import { HeaderComponent } from './components/layout/header/header/header.component';
+import { OAuthModule, OAuthStorage } from 'angular-oauth2-oidc';
+import { AuthConfigModule } from './modules/auth/auth.config.module';
+import { HttpClientModule } from '@angular/common/http';
+import { AlertModule } from './modules/alerting';
+import { ModalContentComponent } from './components/modal/modal-content';
+import { RequestCache, RequestCacheService } from './services/cacheservice/request-cache.service';
+import { httpInterceptorProviders } from './http-interceptors/interceptors';
+import { SidemenuComponent } from './components/layout/sidemenu/sidemenu.component';
+import { ConfirmationModalComponent } from './components/shared/confirmation-modal/confirmation-modal.component';
+import { environment } from '../environments/environment';
+import { ApiModule, Configuration } from '../../openapi/output';
+import { LoadingIndicatorService } from 'src/app/services/loading-indicator.service';
+import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
+import { PageNotFoundComponent } from './components/page-not-found/page-not-found';
+import { RouteReuseStrategy } from '@angular/router';
+import { AppRouteReuseStrategy } from './router.strategy';
+import { StatusPageComponent } from './components/shared/status-page/status-page.component';
+import { UserAdministrationModule } from './modules/user-administration/user-administration.module';
+import { SharedModule } from './shared.module';
+import { AppStarterModule } from './modules/app-starter/app-starter.module';
+import { DashboardModule } from './modules/dashboard/dashboard.module';
+// Sentry.init({
+// dsn: 'http://726f0fcf0f55429eb1c7e613d25d2daf@10.212.1.83:9000/2',
+
+// // Enable performance metrics: https://docs.sentry.io/performance-monitoring/getting-started/?platform=javascript
+// integrations: [new TracingIntegrations.BrowserTracing()],
+// tracesSampleRate: 0.2,
+// });
+
+// @Injectable()
+// export class SentryErrorHandler implements ErrorHandler {
+// constructor() {}
+// handleError(error: any) {
+// Sentry.captureException(error.originalError || error);
+// throw error;
+// }
+// }
+
+export function changeConfig() {
+ return new Configuration({ basePath: environment.backendServerUrl });
+}
+
+@NgModule({
+ declarations: [
+ AppComponent,
+ HeaderComponent,
+ ModalContentComponent,
+ SidemenuComponent,
+ PageNotFoundComponent,
+ ConfirmationModalComponent,
+ StatusPageComponent,
+ ],
+ imports: [
+ BrowserModule,
+ AppRoutingModule,
+ HttpClientModule,
+ AuthConfigModule,
+ AlertModule,
+ AuthConfigModule,
+ ApiModule.forRoot(changeConfig),
+ OAuthModule.forRoot({ resourceServer: { sendAccessToken: false } }),
+ BrowserAnimationsModule,
+ UserAdministrationModule,
+ DashboardModule,
+ SharedModule,
+ AppStarterModule,
+ ],
+ // { provide: ErrorHandler, useClass: SentryErrorHandler },
+ providers: [
+ { provide: RequestCache, useClass: RequestCacheService },
+ httpInterceptorProviders,
+ { provide: OAuthStorage, useValue: localStorage },
+ LoadingIndicatorService,
+ { provide: RouteReuseStrategy, useClass: AppRouteReuseStrategy },
+ ],
+ bootstrap: [AppComponent],
+ entryComponents: [ModalContentComponent, ConfirmationModalComponent],
+})
+export class AppModule {}
diff --git a/src/app/components/layout/header/header/header.component.css b/src/app/components/layout/header/header/header.component.css
new file mode 100644
index 0000000..21de90f
--- /dev/null
+++ b/src/app/components/layout/header/header/header.component.css
@@ -0,0 +1,114 @@
+/*
+ * Copyright (c) 2022. Deutsche Telekom AG
+ *
+ * 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
+ *
+ * http://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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+.navbar {
+ background-color: #e7e6e6;
+}
+
+.bi-list {
+ font-size: 1.5rem;
+}
+
+.dropdown-menu {
+ border-radius: 10px;
+ border: 0.5px solid #b2b2b2;
+ text-align: center;
+ box-shadow: 3px 3px 20px lightslategray;
+}
+
+#dropdown-user,
+#dropdown-settings {
+ text-align: left;
+}
+
+#dropdownMenu:hover,
+#dropdownMenu:focus {
+ background-color: #e7e6e6;
+}
+
+.dropdown-divider {
+ border-color: #b2b2b2;
+}
+
+.navbar-collapse.collapse.in {
+ display: inline-block;
+}
+
+.dropdown-toggle::after {
+ content: initial;
+}
+
+.navbar-toggler {
+ color: black;
+}
+
+.sidebar-toggler {
+ background-color: transparent;
+ border-style: none;
+}
+
+.btn-account {
+ color: black;
+ background-color: transparent;
+ border-style: none;
+ font-size: 21px;
+ line-height: 1.225;
+}
+
+.btn:hover {
+ background-color: #e7e6e6;
+}
+
+/* Make ONAP logo as large as the Telekom one */
+.brand-image {
+ height: 36px;
+}
+
+.bi.bi-person-fill,
+.bi.bi-caret-down-fill {
+ color: black;
+}
+
+/* Add a clearly visible outline while tabbing */
+:focus-visible {
+ box-shadow: 0 0 0 2px black;
+}
+dl > * {
+ text-align: left;
+}
+
+hr {
+ margin-top: 1.4rem;
+ margin-bottom: 1.4rem;
+ border: 0;
+ border-top: 1px solid;
+}
+
+.btn {
+ white-space: nowrap;
+ display: block;
+}
+
+.btn:hover {
+ color: var(--black);
+}
+
+.btn-sm,
+.btn-group-sm > .btn {
+ padding: 0.4rem 1.5rem 0.476rem;
+}
diff --git a/src/app/components/layout/header/header/header.component.html b/src/app/components/layout/header/header/header.component.html
new file mode 100644
index 0000000..8e23ffd
--- /dev/null
+++ b/src/app/components/layout/header/header/header.component.html
@@ -0,0 +1,142 @@
+<!--
+ ~ Copyright (c) 2022. Deutsche Telekom AG
+ ~
+ ~ 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
+ ~
+ ~ http://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.
+ ~
+ ~ SPDX-License-Identifier: Apache-2.0
+ -->
+
+<nav id="brand-bar" class="navbar navbar-light navbar-expand pl-2">
+ <button
+ type="button"
+ id="sidebarCollapse"
+ class="sidebar-toggler"
+ (click)="toggleSidenav()"
+ [attr.aria-label]="'layout.header.sidebarToggler' | translate"
+ >
+ <i class="bi bi-list" aria-hidden="true"></i>
+ </button>
+
+ <!-- Logo as Home Button -->
+ <a class="ml-3" [routerLink]="['/']">
+ <img
+ class="brand-image pl-0"
+ id="img-logo"
+ src="assets/images/onap-logo.png"
+ alt="{{ 'layout.header.logo.onap' | translate }}"
+ />
+ </a>
+
+ <div class="d-flex ml-auto align-items-baseline">
+ <button
+ class="btn btn-invisible p-2 pointer"
+ [attr.accesskey]="ACCESS_KEY.SHORTCUT_0"
+ (click)="openCanvas(content)"
+ [attr.aria-label]="'Help'"
+ [ngbTooltip]="'Help'"
+ >
+ <i aria-hidden="true" class="bi bi-question-circle black"></i>
+ </button>
+ <button
+ *ngIf="!isFullScreen"
+ class="btn btn-invisible pointer p-2 qa_btn_open_fullscreen"
+ [attr.aria-label]="'layout.header.button.openFullscreen' | translate"
+ [ngbTooltip]="'Full screen'"
+ (click)="openFullscreen()"
+ >
+ <i aria-hidden="true" class="bi bi-arrows-fullscreen black"></i>
+ </button>
+ <button
+ *ngIf="isFullScreen"
+ class="btn btn-invisible pointer p-2 qa_btn_cls_fullscreen"
+ [attr.aria-label]="'layout.header.button.closeFullscreen' | translate"
+ [ngbTooltip]="'Exit full screen'"
+ (click)="closeFullscreen()"
+ >
+ <i aria-hidden="true" class="bi bi-fullscreen-exit black"></i>
+ </button>
+ </div>
+
+ <!-- Dropdown menu -->
+ <div ngbDropdown #userAccountDropdown="ngbDropdown" display="dynamic">
+ <button
+ ngbDropdownToggle
+ id="dropdownMenu"
+ class="btn btn-account px-3 tab-focus"
+ [attr.aria-label]="'layout.header.button.useraccount' | translate"
+ aria-haspopup="true"
+ >
+ <i class="bi bi-person-fill" aria-hidden="true"></i>
+ <i class="bi bi-caret-down-fill" aria-hidden="true"></i>
+ </button>
+ <div class="dropdown-menu-right px-4" ngbDropdownMenu aria-labelledby="dropdownMenu"
+ style="z-index: 9999; min-width: 380px">
+ <div class="d-flex justify-content-between align-items-center">
+ <div>
+ <i aria-hidden="true" class="bi bi-person"></i>
+ {{ profile }}
+ </div>
+ <button
+ class="btn btn-sm btn-primary font-weight-bold"
+ (click)="userAccountDropdown.close(); logOut()"
+ [attr.aria-label]="'layout.header.button.logout' | translate"
+ >
+ {{ 'layout.header.button.logout' | translate }}
+ </button>
+ </div>
+ <hr />
+ <dl>
+ <dt>{{ 'layout.header.info.name' | translate }}</dt>
+ <dd>{{ profile }}</dd>
+ <dt>{{ 'layout.header.info.mail' | translate }}</dt>
+ <dd>{{ email }}</dd>
+ </dl>
+ <hr />
+ <button
+ type="button"
+ class="btn btn-sm btn-outline-secondary"
+ placement="top"
+ container="body"
+ triggers="click:blur"
+ [ngbTooltip]="'userAdministration.form.title.changePasswordTooltip' | translate"
+ >
+ {{ 'userAdministration.form.title.changePassword' | translate }}
+ </button>
+ </div>
+ </div>
+</nav>
+
+<ng-template #content let-offcanvas>
+ <div class="offcanvas-header">
+ <div class="d-flex">
+ <h3 class="mb-0 mr-1">{{ 'layout.header.shortcuts.heading' | translate }}</h3>
+ </div>
+ <button
+ type="button"
+ class="align-self-center btn-close"
+ [attr.aria-label]="'common.buttons.close' | translate"
+ (click)="offcanvas.dismiss(content)"
+ ></button>
+ </div>
+ <div class="offcanvas-body">
+ <p class="border-bottom pb-2" *ngFor="let shortcut of shortcuts | keyvalue">
+ {{ shortcut.key }} - {{ shortcut.value }}
+ </p>
+ <div class="text-muted small">
+ <p>{{ 'layout.header.shortcuts.helpText' | translate }}</p>
+ <div [innerHTML]="'layout.header.shortcuts.helpBrowser1' | translate"></div>
+ <div [innerHTML]="'layout.header.shortcuts.helpBrowser2' | translate"></div>
+ <div [innerHTML]="'layout.header.shortcuts.helpBrowser3' | translate"></div>
+ </div>
+ </div>
+</ng-template>
diff --git a/src/app/components/layout/header/header/header.component.spec.ts b/src/app/components/layout/header/header/header.component.spec.ts
new file mode 100644
index 0000000..cb40e73
--- /dev/null
+++ b/src/app/components/layout/header/header/header.component.spec.ts
@@ -0,0 +1,43 @@
+/*
+ * Copyright (c) 2022. Deutsche Telekom AG
+ *
+ * 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
+ *
+ * http://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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { HeaderComponent } from './header.component';
+
+describe('HeaderComponent', () => {
+ let component: HeaderComponent;
+ let fixture: ComponentFixture<HeaderComponent>;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ declarations: [HeaderComponent],
+ }).compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(HeaderComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/app/components/layout/header/header/header.component.ts b/src/app/components/layout/header/header/header.component.ts
new file mode 100644
index 0000000..f5c1a1c
--- /dev/null
+++ b/src/app/components/layout/header/header/header.component.ts
@@ -0,0 +1,113 @@
+/*
+ * Copyright (c) 2022. Deutsche Telekom AG
+ *
+ * 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
+ *
+ * http://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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+
+import {
+ Component,
+ Output,
+ EventEmitter,
+ OnInit,
+ HostListener,
+ ElementRef,
+ ViewChild,
+ TemplateRef,
+} from '@angular/core';
+import { OAuthService } from 'angular-oauth2-oidc';
+import { FullscreenService } from 'src/app/services/fullscreen.service';
+import { environment } from 'src/environments/environment';
+import { KeyboardShortcuts, ShortcutService } from 'src/app/services/shortcut.service';
+import { NgbOffcanvas } from '@ng-bootstrap/ng-bootstrap';
+
+@Component({
+ selector: 'app-header',
+ templateUrl: './header.component.html',
+ styleUrls: ['./header.component.css'],
+})
+export class HeaderComponent implements OnInit {
+@Output() collapse = new EventEmitter<void>()
+
+ /**
+ *
+ * @param {OAuthService} oauthService
+ */
+ isOnapTheme = false;
+ switchToMainContent: string = '';
+ isFullScreen = false;
+ changePasswordUrl = `${environment.keycloakEditProfile}/password`;
+ shortcuts: Map<KeyboardShortcuts,string> = this.shortcutService.getShortcuts();
+
+ public ACCESS_KEY = KeyboardShortcuts;
+ @ViewChild('myNavElement') myNavElement!: ElementRef;
+
+ constructor(
+ private readonly fullscreenService: FullscreenService,
+ private readonly oauthService: OAuthService,
+ private offcanvasService: NgbOffcanvas,
+ private shortcutService: ShortcutService
+ ) {}
+ ngOnInit(): void {
+ this.checkScreenMode();
+ }
+
+ @HostListener('document:fullscreenchange', ['$event'])
+ @HostListener('document:webkitfullscreenchange', ['$event'])
+ @HostListener('document:mozfullscreenchange', ['$event'])
+ @HostListener('document:MSFullscreenChange', ['$event'])
+ private checkScreenMode() {
+ this.isFullScreen = !!document.fullscreenElement;
+ }
+
+ public openFullscreen() {
+ this.fullscreenService.enter();
+ }
+ public closeFullscreen() {
+ this.fullscreenService.leave();
+ }
+
+ public logIn() {
+ this.oauthService.initCodeFlow();
+ }
+
+ public logOut() {
+ this.oauthService.logOut();
+ }
+
+ get profile() {
+ const claims = Object(this.oauthService.getIdentityClaims());
+ return claims.given_name ? claims.given_name : 'no Name';
+ }
+
+ get email() {
+ const claims = Object(this.oauthService.getIdentityClaims());
+ return claims.email ? claims.email : 'no Email';
+ }
+
+ public toggleSidenav() {
+ this.collapse.emit();
+ }
+
+
+ public openCanvas(content: TemplateRef<any>) {
+ const isCanvasOpened = this.offcanvasService.hasOpenOffcanvas();
+ if (isCanvasOpened) {
+ this.offcanvasService.dismiss();
+ } else {
+ this.offcanvasService.open(content, { ariaLabelledBy: 'Keyboard shortcuts', position: 'end' });
+ }
+ }
+}
diff --git a/src/app/components/layout/sidemenu/sidemenu.component.css b/src/app/components/layout/sidemenu/sidemenu.component.css
new file mode 100644
index 0000000..23831b7
--- /dev/null
+++ b/src/app/components/layout/sidemenu/sidemenu.component.css
@@ -0,0 +1,113 @@
+/*
+ * Copyright (c) 2022. Deutsche Telekom AG
+ *
+ * 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
+ *
+ * http://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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+
+/* Sidebar container with nav menu */
+#sidebar-container {
+ margin-right: -1px;
+ flex: 0 0 230px;
+ border-right-style: solid;
+ border-right-width: 1px;
+ border-right-color: #b2b2b2;
+}
+
+/* Menu item*/
+.nav a {
+ height: 50px;
+ color: black;
+}
+
+/* Li elements from sidemenu. Thanks to padding is :focus pseudo-class visible for keyboard users*/
+.nav li {
+ padding: 2px;
+}
+
+/* Separators */
+.sidebar-separator-title {
+ height: 2em;
+}
+
+/* Sidebar sizes when collapsed*/
+.sidebar-collapsed {
+ min-width: 60px !important;
+ max-width: 63px !important;
+ height: auto;
+}
+
+/* Sidebar sizes when expanded*/
+.sidebar-expanded {
+ min-width: 250px;
+ max-width: 250px;
+ height: 100%;
+ display: block;
+ border-right-style: solid;
+ border-right-width: 1px;
+ border-right-color: #c5c5c5;
+}
+
+/* Hide list item and grouping item title if container sidebar is collapsed */
+.sidebar-collapsed a.nav-link span,
+.sidebar-collapsed li.sidebar-separator-title small {
+ display: none !important;
+}
+
+/* Hide grouping item icon if container sidebar is collapsed */
+.sidebar-expanded ul li.sidebar-separator-title i,
+.sidebar-expanded ul li.sidebar-separator-title img {
+ display: none !important;
+}
+
+/* Show grouping item icon if container sidebar is collapsed */
+.sidebar-collapsed ul li.sidebar-separator-title i,
+.sidebar-collapsed ul li.sidebar-separator-title img {
+ display: inline-block;
+}
+
+/* All list items */
+.nav-link {
+ border-style: none;
+ display: block !important;
+ padding: 0.75rem 1rem;
+}
+
+ul {
+ list-style-type: none;
+}
+
+.active-menu-item {
+ background-color: var(--active-gray);
+ color: black;
+ border-top-left-radius: 0.25rem !important;
+ border-bottom-left-radius: 0.25rem !important;
+ border-left: 2px solid var(--primary) !important;
+}
+
+.nav.inner li {
+ border: none;
+}
+
+.portal-version-title,
+.portal-version-number {
+ font-size: 13px;
+}
+
+img {
+ width: 18px;
+ height: 19px;
+ vertical-align: -0.125em;
+}
diff --git a/src/app/components/layout/sidemenu/sidemenu.component.html b/src/app/components/layout/sidemenu/sidemenu.component.html
new file mode 100644
index 0000000..fd97c50
--- /dev/null
+++ b/src/app/components/layout/sidemenu/sidemenu.component.html
@@ -0,0 +1,51 @@
+<!--
+ ~ Copyright (c) 2022. Deutsche Telekom AG
+ ~
+ ~ 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
+ ~
+ ~ http://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.
+ ~
+ ~ SPDX-License-Identifier: Apache-2.0
+ -->
+
+
+<nav
+ [class.sidebar-collapsed]="isSidebarCollapsed"
+ id="sidebar-container"
+ class="sidebar-expanded overflow-auto d-flex flex-column justify-content-between"
+ [attr.aria-label]="'layout.menu.mainMenu' | translate"
+>
+ <ul class="nav flex-column flex-nowrap" [attr.aria-label]="'layout.menu.menuItems' | translate">
+ <li class="nav-item">
+ <a #test [attr.accesskey]='ACCESS_KEY.SHORTCUT_6' class="nav-link" routerLinkActive="active-menu-item" [routerLink]="['/dashboard']">
+ <i class="bi bi-house-door mr-4" aria-hidden="true"></i>
+ <span class="d-sm-inline qa_menu_home">{{ 'layout.menu.items.dashboard' | translate }}</span></a
+ >
+ </li>
+ <li class="nav-item">
+ <a class="nav-link" routerLinkActive="active-menu-item" [routerLink]="['/app-starter']">
+ <i class="bi bi-grid mr-4" aria-hidden="true"></i>
+ <span class="d-sm-inline qa_menu_app_starter">{{ 'layout.menu.items.appStarter' | translate }}</span></a
+ >
+ </li>
+
+ <li class="nav-item" [appHasPermissions]="'users.administration.list'">
+ <a class="nav-link" routerLinkActive="active-menu-item" routerLink="/user-administration">
+ <i class="bi bi-people mr-4" aria-hidden="true"></i>
+ <span class="d-sm-inline qa_menu_users">{{ 'layout.menu.items.users' | translate }}</span></a
+ >
+ </li>
+ </ul>
+ <div [ngClass]="isSidebarCollapsed ? 'flex-column' : 'flex-row'" class="d-flex justify-content-center text-center">
+ <h5 class="portal-version-title mr-1" id="portal-version">Portal Version:</h5>
+ <span class="portal-version-number" aria-labelledby="portal-version" (click)='test.blur()'>{{versionNumber}}</span>
+ </div>
+</nav>
diff --git a/src/app/components/layout/sidemenu/sidemenu.component.spec.ts b/src/app/components/layout/sidemenu/sidemenu.component.spec.ts
new file mode 100644
index 0000000..1067c49
--- /dev/null
+++ b/src/app/components/layout/sidemenu/sidemenu.component.spec.ts
@@ -0,0 +1,43 @@
+/*
+ * Copyright (c) 2022. Deutsche Telekom AG
+ *
+ * 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
+ *
+ * http://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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { SidemenuComponent } from './sidemenu.component';
+
+describe('SidemenuComponent', () => {
+ let component: SidemenuComponent;
+ let fixture: ComponentFixture<SidemenuComponent>;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ declarations: [SidemenuComponent],
+ }).compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(SidemenuComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/app/components/layout/sidemenu/sidemenu.component.ts b/src/app/components/layout/sidemenu/sidemenu.component.ts
new file mode 100644
index 0000000..1f5c123
--- /dev/null
+++ b/src/app/components/layout/sidemenu/sidemenu.component.ts
@@ -0,0 +1,47 @@
+/*
+ * Copyright (c) 2022. Deutsche Telekom AG
+ *
+ * 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
+ *
+ * http://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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { Component, Injectable, Input } from '@angular/core';
+import { environment } from 'src/environments/environment';
+import VersionJson from 'src/assets/version.json';
+import { KeyboardShortcuts } from '../../../services/shortcut.service';
+
+@Injectable({
+ providedIn: 'root',
+})
+@Component({
+ selector: 'app-sidemenu',
+ templateUrl: './sidemenu.component.html',
+ styleUrls: ['./sidemenu.component.css'],
+})
+export class SidemenuComponent {
+ versionNumber: string;
+
+ @Input() isSidebarCollapsed = false;
+
+ public ACCESS_KEY = KeyboardShortcuts;
+ public keycloakEditProfile = environment.keycloakEditProfile;
+ public isKpiDashboardSubMenuCollapsed = false;
+
+ constructor() {
+ this.versionNumber = VersionJson.number;
+ }
+ collapsed() {
+ this.isKpiDashboardSubMenuCollapsed = !this.isKpiDashboardSubMenuCollapsed;
+ }
+}
diff --git a/src/app/components/modal/modal-content.html b/src/app/components/modal/modal-content.html
new file mode 100644
index 0000000..ec3b6c4
--- /dev/null
+++ b/src/app/components/modal/modal-content.html
@@ -0,0 +1,30 @@
+<!--
+ ~ Copyright (c) 2022. Deutsche Telekom AG
+ ~
+ ~ 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
+ ~
+ ~ http://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.
+ ~
+ ~ SPDX-License-Identifier: Apache-2.0
+ -->
+
+
+<div class="modal-body m-3" style="text-align: justify">
+ <h4 class="modal-title">{{'modal.error.accessDenied' | translate}}</h4>
+ <p><br />{{'modal.error.accessDenied' | translate}}</p>
+ <details>
+ <summary>{{'modal.error.details' | translate}}</summary>
+ <p>{{message}}</p>
+ </details>
+ <div style="text-align: right">
+ <button class="btn btn-primary" (click)="oauthService.logOut()">Logout</button>
+ </div>
+</div>
diff --git a/src/app/components/modal/modal-content.ts b/src/app/components/modal/modal-content.ts
new file mode 100644
index 0000000..01211df
--- /dev/null
+++ b/src/app/components/modal/modal-content.ts
@@ -0,0 +1,32 @@
+/*
+ * Copyright (c) 2022. Deutsche Telekom AG
+ *
+ * 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
+ *
+ * http://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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+
+import { Component, Input } from '@angular/core';
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+import { OAuthService } from 'angular-oauth2-oidc';
+
+@Component({
+ selector: 'app-modal-content',
+ templateUrl: './modal-content.html',
+})
+export class ModalContentComponent {
+ @Input() message: any;
+
+ constructor(public activeModal: NgbActiveModal, public oauthService: OAuthService) {}
+}
diff --git a/src/app/components/modal/modal.service.ts b/src/app/components/modal/modal.service.ts
new file mode 100644
index 0000000..083bcb1
--- /dev/null
+++ b/src/app/components/modal/modal.service.ts
@@ -0,0 +1,38 @@
+/*
+ * Copyright (c) 2022. Deutsche Telekom AG
+ *
+ * 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
+ *
+ * http://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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+
+import { Injectable } from '@angular/core';
+import { NgbModal, NgbModalConfig } from '@ng-bootstrap/ng-bootstrap';
+import { ModalContentComponent } from './modal-content';
+
+@Injectable({
+ providedIn: 'root',
+})
+export class ModalService {
+ constructor(private modalService: NgbModal, private modalConfig: NgbModalConfig) {
+ // customize default values of modals used by this component tree
+ modalConfig.backdrop = 'static';
+ modalConfig.keyboard = false;
+ }
+
+ open(message: string) {
+ const modalRef = this.modalService.open(ModalContentComponent,{backdropClass:'backdropClass'});
+ modalRef.componentInstance.message = message;
+ }
+}
diff --git a/src/app/components/page-not-found/page-not-found.css b/src/app/components/page-not-found/page-not-found.css
new file mode 100644
index 0000000..2c10482
--- /dev/null
+++ b/src/app/components/page-not-found/page-not-found.css
@@ -0,0 +1,36 @@
+/*
+ * Copyright (c) 2022. Deutsche Telekom AG
+ *
+ * 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
+ *
+ * http://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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+
+.wrapper {
+ position: fixed;
+ top: 50%;
+ left: 50%;
+ transform: translate(-40%, -50%);
+}
+
+.woman-img {
+ height: 350px;
+}
+
+.icon {
+ position: absolute;
+ top: 5px;
+ font-size: 30px;
+ color: var(--primary);
+}
diff --git a/src/app/components/page-not-found/page-not-found.html b/src/app/components/page-not-found/page-not-found.html
new file mode 100644
index 0000000..6ad90c6
--- /dev/null
+++ b/src/app/components/page-not-found/page-not-found.html
@@ -0,0 +1,29 @@
+<!--
+ ~ Copyright (c) 2022. Deutsche Telekom AG
+ ~
+ ~ 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
+ ~
+ ~ http://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.
+ ~
+ ~ SPDX-License-Identifier: Apache-2.0
+ -->
+
+
+<div class="container text-center wrapper">
+ <img
+ class="woman-img mb-5"
+ src="assets/images/icons/standing-woman.svg"
+ alt="{{'pageNotFound.imgAltText' | translate}}"
+ />
+ <i class="bi bi-x-circle-fill icon" aria-hidden="true"></i>
+ <h3 class="mb-3">{{'pageNotFound.text' | translate}}</h3>
+ <button type="button" class="btn btn-primary" routerLink="/dashboard">{{'pageNotFound.button' | translate}}</button>
+</div>
diff --git a/src/app/components/page-not-found/page-not-found.ts b/src/app/components/page-not-found/page-not-found.ts
new file mode 100644
index 0000000..88cdc01
--- /dev/null
+++ b/src/app/components/page-not-found/page-not-found.ts
@@ -0,0 +1,27 @@
+/*
+ * Copyright (c) 2022. Deutsche Telekom AG
+ *
+ * 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
+ *
+ * http://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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+
+import { Component } from '@angular/core';
+
+@Component({
+ selector: 'app-page-not-found',
+ templateUrl: './page-not-found.html',
+ styleUrls: ['./page-not-found.css'],
+})
+export class PageNotFoundComponent {}
diff --git a/src/app/components/shared/breadcrumb-item/breadcrumb-item.component.html b/src/app/components/shared/breadcrumb-item/breadcrumb-item.component.html
new file mode 100644
index 0000000..72d5c79
--- /dev/null
+++ b/src/app/components/shared/breadcrumb-item/breadcrumb-item.component.html
@@ -0,0 +1,22 @@
+<!--
+ ~ Copyright (c) 2022. Deutsche Telekom AG
+ ~
+ ~ 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
+ ~
+ ~ http://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.
+ ~
+ ~ SPDX-License-Identifier: Apache-2.0
+ -->
+
+
+<ng-template>
+ <ng-content></ng-content>
+</ng-template>
diff --git a/src/app/components/shared/breadcrumb-item/breadcrumb-item.component.ts b/src/app/components/shared/breadcrumb-item/breadcrumb-item.component.ts
new file mode 100644
index 0000000..5f74fff
--- /dev/null
+++ b/src/app/components/shared/breadcrumb-item/breadcrumb-item.component.ts
@@ -0,0 +1,29 @@
+/*
+ * Copyright (c) 2022. Deutsche Telekom AG
+ *
+ * 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
+ *
+ * http://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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+
+import { Component, TemplateRef, ViewChild } from '@angular/core';
+
+@Component({
+ selector: 'app-breadcrumb-item',
+ templateUrl: './breadcrumb-item.component.html',
+})
+export class BreadcrumbItemComponent {
+ @ViewChild(TemplateRef, { static: true })
+ readonly template!: TemplateRef<any>;
+}
diff --git a/src/app/components/shared/breadcrumb/breadcrumb.component.html b/src/app/components/shared/breadcrumb/breadcrumb.component.html
new file mode 100644
index 0000000..e6cdba5
--- /dev/null
+++ b/src/app/components/shared/breadcrumb/breadcrumb.component.html
@@ -0,0 +1,26 @@
+<!--
+ ~ Copyright (c) 2022. Deutsche Telekom AG
+ ~
+ ~ 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
+ ~
+ ~ http://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.
+ ~
+ ~ SPDX-License-Identifier: Apache-2.0
+ -->
+
+
+<nav [attr.aria-label]="'layout.main.breadcrumb' | translate">
+ <ol class="breadcrumb">
+ <li *ngFor="let item of items; index as i" class="breadcrumb-item">
+ <ng-container *ngTemplateOutlet="item.template" accessKey='3'></ng-container>
+ </li>
+ </ol>
+</nav>
diff --git a/src/app/components/shared/breadcrumb/breadcrumb.component.ts b/src/app/components/shared/breadcrumb/breadcrumb.component.ts
new file mode 100644
index 0000000..864a9ff
--- /dev/null
+++ b/src/app/components/shared/breadcrumb/breadcrumb.component.ts
@@ -0,0 +1,30 @@
+/*
+ * Copyright (c) 2022. Deutsche Telekom AG
+ *
+ * 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
+ *
+ * http://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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+
+import { Component, ContentChildren, QueryList } from '@angular/core';
+import { BreadcrumbItemComponent } from '../breadcrumb-item/breadcrumb-item.component';
+
+@Component({
+ selector: 'app-breadcrumb',
+ templateUrl: './breadcrumb.component.html',
+})
+export class BreadcrumbComponent {
+ @ContentChildren(BreadcrumbItemComponent)
+ readonly items!: QueryList<BreadcrumbItemComponent>;
+}
diff --git a/src/app/components/shared/confirmation-modal/confirmation-modal.component.html b/src/app/components/shared/confirmation-modal/confirmation-modal.component.html
new file mode 100644
index 0000000..4f26c5d
--- /dev/null
+++ b/src/app/components/shared/confirmation-modal/confirmation-modal.component.html
@@ -0,0 +1,35 @@
+<!--
+ ~ Copyright (c) 2022. Deutsche Telekom AG
+ ~
+ ~ 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
+ ~
+ ~ http://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.
+ ~
+ ~ SPDX-License-Identifier: Apache-2.0
+ -->
+
+
+<div class="modal-header qa_modal_header">
+ <h4 class="modal-title qa_modal_title" id="modal-title">
+ <strong>{{ title }}</strong>
+ </h4>
+</div>
+<div class="modal-body qa_modal_body">
+ <p>
+ {{ text }}
+ </p>
+</div>
+<div *ngIf='showOkBtn || showCancelBtn' class="modal-footer qa_modal_footer">
+ <button *ngIf='showCancelBtn' type="button" class="btn btn-default qa_cancel_button" (click)="activeModal.close(false)">
+ {{ cancelText }}
+ </button>
+ <button *ngIf='showOkBtn' type="button" class="btn btn-danger qa_apply_button" (click)="activeModal.close(true)">{{ okText }}</button>
+</div>
diff --git a/src/app/components/shared/confirmation-modal/confirmation-modal.component.spec.ts b/src/app/components/shared/confirmation-modal/confirmation-modal.component.spec.ts
new file mode 100644
index 0000000..b7b5110
--- /dev/null
+++ b/src/app/components/shared/confirmation-modal/confirmation-modal.component.spec.ts
@@ -0,0 +1,19 @@
+/*
+ * Copyright (c) 2022. Deutsche Telekom AG
+ *
+ * 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
+ *
+ * http://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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+
diff --git a/src/app/components/shared/confirmation-modal/confirmation-modal.component.ts b/src/app/components/shared/confirmation-modal/confirmation-modal.component.ts
new file mode 100644
index 0000000..023ba5e
--- /dev/null
+++ b/src/app/components/shared/confirmation-modal/confirmation-modal.component.ts
@@ -0,0 +1,39 @@
+/*
+ * Copyright (c) 2022. Deutsche Telekom AG
+ *
+ * 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
+ *
+ * http://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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+
+import { Component, Input } from '@angular/core';
+import { TranslateService } from '@ngx-translate/core';
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+
+@Component({
+ selector: 'app-confirmation-modal',
+ templateUrl: './confirmation-modal.component.html'
+})
+export class ConfirmationModalComponent {
+ constructor(public activeModal: NgbActiveModal, private readonly translateService: TranslateService) {}
+
+ @Input() showOkBtn = true;
+ @Input() showCancelBtn = true;
+
+ @Input()
+ okText = this.translateService.instant('common.buttons.save');
+ cancelText = this.translateService.instant('common.buttons.cancel');
+ title = '';
+ text = '';
+}
diff --git a/src/app/components/shared/loading-spinner/loading-spinner.component.html b/src/app/components/shared/loading-spinner/loading-spinner.component.html
new file mode 100644
index 0000000..f9dd6ab
--- /dev/null
+++ b/src/app/components/shared/loading-spinner/loading-spinner.component.html
@@ -0,0 +1,21 @@
+<!--
+ ~ Copyright (c) 2022. Deutsche Telekom AG
+ ~
+ ~ 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
+ ~
+ ~ http://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.
+ ~
+ ~ SPDX-License-Identifier: Apache-2.0
+ -->
+
+<div class="spinner-border spinner-border-sm text-light qa_alarm_spinner" role="status">
+ <span class="sr-only">{{'common.loading' | translate}}</span>
+</div>
diff --git a/src/app/components/shared/loading-spinner/loading-spinner.component.ts b/src/app/components/shared/loading-spinner/loading-spinner.component.ts
new file mode 100644
index 0000000..c99a085
--- /dev/null
+++ b/src/app/components/shared/loading-spinner/loading-spinner.component.ts
@@ -0,0 +1,26 @@
+/*
+ * Copyright (c) 2022. Deutsche Telekom AG
+ *
+ * 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
+ *
+ * http://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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { Component } from '@angular/core';
+
+@Component({
+ selector: 'app-loading-spinner',
+ templateUrl: './loading-spinner.component.html',
+})
+export class LoadingSpinnerComponent {
+}
diff --git a/src/app/components/shared/pagination/pagination.component.css b/src/app/components/shared/pagination/pagination.component.css
new file mode 100644
index 0000000..b864207
--- /dev/null
+++ b/src/app/components/shared/pagination/pagination.component.css
@@ -0,0 +1,23 @@
+/*
+ * Copyright (c) 2022. Deutsche Telekom AG
+ *
+ * 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
+ *
+ * http://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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+
+ngb-pagination::ng-deep li .page-link:focus {
+ box-shadow: 0 0 0 0.2rem rgb(0 123 255 / 25%);
+ border-color: #00a0de;
+}
diff --git a/src/app/components/shared/pagination/pagination.component.html b/src/app/components/shared/pagination/pagination.component.html
new file mode 100644
index 0000000..ad198fc
--- /dev/null
+++ b/src/app/components/shared/pagination/pagination.component.html
@@ -0,0 +1,48 @@
+<!--
+ ~ Copyright (c) 2022. Deutsche Telekom AG
+ ~
+ ~ 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
+ ~
+ ~ http://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.
+ ~
+ ~ SPDX-License-Identifier: Apache-2.0
+ -->
+
+
+<div class="d-flex flex-wrap justify-content-between mt-3">
+ <div>
+ <ngb-pagination
+ [collectionSize]="collectionSize"
+ [pageSize]="pageSize"
+ [page]="page"
+ (pageChange)="emitPageChange($event)"
+ ></ngb-pagination>
+ <span class="ml-2 small">
+ {{ 'common.pagination.totalCount' | translate: { value: collectionSize } }}
+ </span>
+ </div>
+ <div>
+ <label for="item-select" class="sr-only">
+ {{ 'common.select.description' | translate }}
+ </label>
+ <select
+ id="item-select"
+ class="custom-select"
+ style="width: auto"
+ [ngModel]="pageSize"
+ (ngModelChange)="emitModelChange($event)"
+ >
+ <option *ngFor="let n of itemsPerPage" [ngValue]="n">
+ {{ 'common.select.itemsPerPage' | translate: { value: n } }}
+ </option>
+ </select>
+ </div>
+</div>
diff --git a/src/app/components/shared/pagination/pagination.component.spec.ts b/src/app/components/shared/pagination/pagination.component.spec.ts
new file mode 100644
index 0000000..7ffe9fc
--- /dev/null
+++ b/src/app/components/shared/pagination/pagination.component.spec.ts
@@ -0,0 +1,43 @@
+/*
+ * Copyright (c) 2022. Deutsche Telekom AG
+ *
+ * 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
+ *
+ * http://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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { PaginationComponent } from './pagination.component';
+
+describe('PaginationComponent', () => {
+ let component: PaginationComponent;
+ let fixture: ComponentFixture<PaginationComponent>;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ declarations: [PaginationComponent],
+ }).compileComponents();
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(PaginationComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/app/components/shared/pagination/pagination.component.ts b/src/app/components/shared/pagination/pagination.component.ts
new file mode 100644
index 0000000..fee827d
--- /dev/null
+++ b/src/app/components/shared/pagination/pagination.component.ts
@@ -0,0 +1,88 @@
+/*
+ * Copyright (c) 2022. Deutsche Telekom AG
+ *
+ * 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
+ *
+ * http://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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+
+import { Component, EventEmitter, Input, Output } from '@angular/core';
+import { NgbPaginationConfig } from '@ng-bootstrap/ng-bootstrap';
+
+/**
+ * This is a wrapper component for the `ngbPagination` component of ngBootstrap ([official wiki page](https://ng-bootstrap.github.io/#/components/pagination/overview)).
+ * This contains the pagination element, as well as the selection element for the page size.
+ *
+ *
+ * Deal with both using the `pageChange` and `pageSizeChange` events, i.e in your template:
+ * ``` html
+ * <app-pagination
+ * [collectionSize]="..."
+ [pageSize]="..."
+ [page]="..."
+ * (pageChange)="changePage($event)"
+ * (pageSizeChange)="changePageSize($event)"
+ * >
+ * </app-pagination>
+ * ```
+ */
+@Component({
+ selector: 'app-pagination',
+ templateUrl: './pagination.component.html',
+ styleUrls: ['./pagination.component.css'],
+ providers: [NgbPaginationConfig],
+})
+export class PaginationComponent {
+ /**
+ * This event is fired when an item in the `select`-element is changed
+ */
+ @Output() pageSizeChange: EventEmitter<number> = new EventEmitter<number>();
+
+ /**
+ * Specify what page sizes should be selectable by the user.
+ */
+ @Input() itemsPerPage = [10, 20, 50];
+
+ @Output() pageChange: EventEmitter<number> = new EventEmitter<number>();
+ @Input() collectionSize = 10;
+ @Input() pageSize = 10;
+ @Input() page = 1;
+
+ constructor(config: NgbPaginationConfig) {
+ config.boundaryLinks = true;
+ config.directionLinks = true;
+ config.disabled = false;
+ config.ellipses = false;
+ config.maxSize = 3;
+ config.pageSize = 10;
+ config.rotate = true;
+ config.size = 'sm';
+ }
+
+ /**
+ * Emit the currently selected page from the `ngb-Pagination`
+ * @param page the page that is selected
+ */
+ emitPageChange(page: number) {
+ this.pageChange.emit(page);
+ }
+
+ /**
+ * Emit the currently selected page size from the `select`
+ * @param size the number of items per page that is selected
+ */
+ emitModelChange(size: number) {
+ this.pageSizeChange.emit(size);
+ }
+}
diff --git a/src/app/components/shared/status-page/status-page.component.css b/src/app/components/shared/status-page/status-page.component.css
new file mode 100644
index 0000000..b7b5110
--- /dev/null
+++ b/src/app/components/shared/status-page/status-page.component.css
@@ -0,0 +1,19 @@
+/*
+ * Copyright (c) 2022. Deutsche Telekom AG
+ *
+ * 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
+ *
+ * http://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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+
diff --git a/src/app/components/shared/status-page/status-page.component.html b/src/app/components/shared/status-page/status-page.component.html
new file mode 100644
index 0000000..28e65c6
--- /dev/null
+++ b/src/app/components/shared/status-page/status-page.component.html
@@ -0,0 +1,23 @@
+<!--
+ ~ Copyright (c) 2022. Deutsche Telekom AG
+ ~
+ ~ 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
+ ~
+ ~ http://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.
+ ~
+ ~ SPDX-License-Identifier: Apache-2.0
+ -->
+
+
+<div class="d-flex justify-content-center align-items-center flex-column h-100">
+ <h2 class="text-center">{{header}}</h2>
+ <h3>{{message}}</h3>
+</div>
diff --git a/src/app/components/shared/status-page/status-page.component.spec.ts b/src/app/components/shared/status-page/status-page.component.spec.ts
new file mode 100644
index 0000000..f8c547e
--- /dev/null
+++ b/src/app/components/shared/status-page/status-page.component.spec.ts
@@ -0,0 +1,44 @@
+/*
+ * Copyright (c) 2022. Deutsche Telekom AG
+ *
+ * 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
+ *
+ * http://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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { StatusPageComponent } from './status-page.component';
+
+describe('StatusPageComponent', () => {
+ let component: StatusPageComponent;
+ let fixture: ComponentFixture<StatusPageComponent>;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ declarations: [ StatusPageComponent ]
+ })
+ .compileComponents();
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(StatusPageComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/app/components/shared/status-page/status-page.component.ts b/src/app/components/shared/status-page/status-page.component.ts
new file mode 100644
index 0000000..3cbbf46
--- /dev/null
+++ b/src/app/components/shared/status-page/status-page.component.ts
@@ -0,0 +1,37 @@
+/*
+ * Copyright (c) 2022. Deutsche Telekom AG
+ *
+ * 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
+ *
+ * http://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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+
+import { Component } from '@angular/core';
+import { Router } from '@angular/router';
+
+@Component({
+ selector: 'app-status-page',
+ templateUrl: './status-page.component.html',
+ styleUrls: ['./status-page.component.css'],
+})
+export class StatusPageComponent {
+ header: string;
+ message: string;
+
+ constructor(private router: Router) {
+ const data = this.router.getCurrentNavigation();
+ this.header = data?.extras?.state?.header;
+ this.message = data?.extras?.state?.message;
+ }
+}
diff --git a/src/app/components/shared/table-skeleton/table-skeleton.component.css b/src/app/components/shared/table-skeleton/table-skeleton.component.css
new file mode 100644
index 0000000..0870694
--- /dev/null
+++ b/src/app/components/shared/table-skeleton/table-skeleton.component.css
@@ -0,0 +1,37 @@
+/*
+ * Copyright (c) 2022. Deutsche Telekom AG
+ *
+ * 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
+ *
+ * http://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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+
+@keyframes ghost {
+ from {
+ background-position: 0 0;
+ }
+ to {
+ background-position: 100vw 0;
+ }
+}
+
+.line {
+ width: 100%;
+ height: 50px;
+ margin-top: 10px;
+ border-radius: 3px;
+ background: linear-gradient(90deg, #f0f0f0, #d8d6d6, #f0f0f0) 0 0/ 80vh 100% fixed;
+ background-color: var(--secondary);
+ animation: ghost 4000ms infinite linear;
+}
diff --git a/src/app/components/shared/table-skeleton/table-skeleton.component.html b/src/app/components/shared/table-skeleton/table-skeleton.component.html
new file mode 100644
index 0000000..717da1f
--- /dev/null
+++ b/src/app/components/shared/table-skeleton/table-skeleton.component.html
@@ -0,0 +1,31 @@
+<!--
+ ~ Copyright (c) 2022. Deutsche Telekom AG
+ ~
+ ~ 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
+ ~
+ ~ http://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.
+ ~
+ ~ SPDX-License-Identifier: Apache-2.0
+ -->
+
+
+<div id="table-skeleton">
+ <div class="line"></div>
+ <div class="line"></div>
+ <div class="line"></div>
+ <div class="line"></div>
+ <div class="line"></div>
+ <div class="line"></div>
+ <div class="line"></div>
+ <div class="line"></div>
+ <div class="line"></div>
+ <div class="line"></div>
+</div>
diff --git a/src/app/components/shared/table-skeleton/table-skeleton.component.ts b/src/app/components/shared/table-skeleton/table-skeleton.component.ts
new file mode 100644
index 0000000..41638fe
--- /dev/null
+++ b/src/app/components/shared/table-skeleton/table-skeleton.component.ts
@@ -0,0 +1,29 @@
+/*
+ * Copyright (c) 2022. Deutsche Telekom AG
+ *
+ * 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
+ *
+ * http://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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+
+import { ChangeDetectionStrategy, Component } from '@angular/core';
+
+@Component({
+ selector: 'app-table-skeleton',
+ templateUrl: './table-skeleton.component.html',
+ styleUrls: ['./table-skeleton.component.css'],
+ changeDetection: ChangeDetectionStrategy.OnPush
+})
+export class TableSkeletonComponent {
+}
diff --git a/src/app/directives/has-permissions.directive.ts b/src/app/directives/has-permissions.directive.ts
new file mode 100644
index 0000000..994cee8
--- /dev/null
+++ b/src/app/directives/has-permissions.directive.ts
@@ -0,0 +1,57 @@
+/*
+ * Copyright (c) 2022. Deutsche Telekom AG
+ *
+ * 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
+ *
+ * http://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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+
+import { Directive, ElementRef, Inject, Input } from '@angular/core';
+import { HttpClient } from '@angular/common/http';
+import { ACL_CONFIG, AclConfig } from '../modules/auth/injection-tokens';
+import { AuthService } from '../services/auth.service';
+import { takeUntil } from 'rxjs/operators';
+import { UnsubscribeService } from '../services/unsubscribe/unsubscribe.service';
+
+@Directive({
+ selector: '[appHasPermissions]',
+ providers: [UnsubscribeService]
+})
+export class HasPermissionsDirective {
+ @Input() appHasPermissions = '';
+ constructor(
+ private el: ElementRef,
+ readonly httpClient: HttpClient,
+ readonly authService: AuthService,
+ @Inject(ACL_CONFIG) readonly acl: AclConfig,
+ private unsubscribeService: UnsubscribeService
+ ) {
+ // for unknown reasons this must be wrapped in set timeout, otherwise appHasPermissions is sometimes empty string
+ setTimeout(() => {
+ this.el.nativeElement.style.display = 'none';
+ this.authService
+ .loadCachedUserProfile()
+ .pipe(takeUntil(this.unsubscribeService.unsubscribe$))
+ .subscribe(userProfile => {
+ const intersectionOfRoles = Object.keys(acl).filter(value => userProfile?.roles.includes(value));
+ for (const role of intersectionOfRoles) {
+ if (acl[role].includes(this.appHasPermissions)) {
+ this.el.nativeElement.style.display = 'initial';
+ return;
+ }
+ }
+ })
+ }, 0);
+ }
+}
diff --git a/src/app/directives/hide-empty.directive.ts b/src/app/directives/hide-empty.directive.ts
new file mode 100644
index 0000000..c6ee8a6
--- /dev/null
+++ b/src/app/directives/hide-empty.directive.ts
@@ -0,0 +1,65 @@
+/*
+ * Copyright (c) 2022. Deutsche Telekom AG
+ *
+ * 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
+ *
+ * http://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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+
+import { AfterViewChecked, Directive, ElementRef, Input, OnChanges, SimpleChanges } from '@angular/core';
+
+//This directive we are using in switch functionality at attribute pane. We can hide attributes without value or show all the attributes.
+// We are using this directive in the html file in the element (div) that wraps all the app-detail-rows.
+
+@Directive({
+ selector: '[appHideEmptyDetailRow]',
+})
+export class HideEmptyDetailRowDirective implements OnChanges, AfterViewChecked {
+ @Input() appHideEmptyDetailRow!: boolean;
+ constructor(private el: ElementRef) {}
+
+ ngOnChanges(changes: SimpleChanges) {
+ if (changes.appHideEmptyDetailRow && this.el.nativeElement.textContent) {
+ this.setVisibility(this.el);
+ }
+ }
+
+ ngAfterViewChecked() {
+ if (this.appHideEmptyDetailRow && this.el.nativeElement.textContent) {
+ this.setVisibility(this.el);
+ }
+ }
+
+ public setVisibility(ref: ElementRef) {
+ if (this.appHideEmptyDetailRow) {
+ const detailRows = ref.nativeElement.querySelectorAll('app-detail-row');
+ detailRows.forEach((item: any) => {
+ const span = item.querySelector('span');
+ if(!span){
+ return;
+ }
+ if (span.textContent === '' || span.textContent === '-') {
+ item.style.display = 'none';
+ } else {
+ item.style.display = 'block';
+ }
+ });
+ } else {
+ const detailRows = ref.nativeElement.querySelectorAll('app-detail-row');
+ detailRows.forEach((item: any) => {
+ item.style.display = 'block';
+ });
+ }
+ }
+}
diff --git a/src/app/errorhandling/global-error-handler.ts b/src/app/errorhandling/global-error-handler.ts
new file mode 100644
index 0000000..0886232
--- /dev/null
+++ b/src/app/errorhandling/global-error-handler.ts
@@ -0,0 +1,35 @@
+/*
+ * Copyright (c) 2022. Deutsche Telekom AG
+ *
+ * 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
+ *
+ * http://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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+
+import { ErrorHandler, Injectable, Injector } from '@angular/core';
+import { HttpErrorResponse } from '@angular/common/http';
+import { AlertService } from '../modules/alerting';
+
+/**
+ * This class is intended for global error handling
+ */
+// See: https://pusher.com/tutorials/error-handling-angular-part-1
+@Injectable()
+export class GlobalErrorHandler implements ErrorHandler {
+ constructor(private injector: Injector, private alertService: AlertService) {}
+
+ handleError(error: Error | HttpErrorResponse) {
+ this.alertService.error(error.message);
+ }
+}
diff --git a/src/app/guards/auth.guard.ts b/src/app/guards/auth.guard.ts
new file mode 100644
index 0000000..54ede0f
--- /dev/null
+++ b/src/app/guards/auth.guard.ts
@@ -0,0 +1,39 @@
+/*
+ * Copyright (c) 2022. Deutsche Telekom AG
+ *
+ * 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
+ *
+ * http://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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+
+import { Injectable } from '@angular/core';
+import { CanActivate, UrlTree } from '@angular/router';
+import { Observable } from 'rxjs';
+import { AuthService } from '../services/auth.service';
+
+/**
+ * grants permissions based on the `AuthService`
+ */
+@Injectable({
+ providedIn: 'root',
+})
+export class AuthGuard implements CanActivate {
+ roles: string = '';
+
+ constructor(private authService: AuthService) {}
+
+ canActivate(): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
+ return this.authService.hasPermissions();
+ }
+}
diff --git a/src/app/guards/edit-user.can-activate.guard.ts b/src/app/guards/edit-user.can-activate.guard.ts
new file mode 100644
index 0000000..81fc36e
--- /dev/null
+++ b/src/app/guards/edit-user.can-activate.guard.ts
@@ -0,0 +1,51 @@
+/*
+ * Copyright (c) 2022. Deutsche Telekom AG
+ *
+ * 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
+ *
+ * http://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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+
+import { Injectable } from '@angular/core';
+import { ActivatedRouteSnapshot, CanActivate, Router } from '@angular/router';
+import { Observable, of } from 'rxjs';
+import { UsersService } from '../../../openapi/output';
+import { catchError, map } from 'rxjs/operators';
+import { TranslateService } from '@ngx-translate/core';
+@Injectable({
+ providedIn: 'root',
+})
+export class EditUserCanActivateGuard implements CanActivate {
+ constructor(private usersService: UsersService, private router: Router, private translateService: TranslateService) {}
+ canActivate(route: ActivatedRouteSnapshot): Observable<boolean> {
+ const userId = route.paramMap.get('userId');
+ if (userId) {
+ return this.usersService.getUser(userId).pipe(
+ catchError(() => {
+ this.router.navigate(['/statusPage'], {
+ state: {
+ header: this.translateService.instant('userAdministration.messages.warnings.userDeleted.header'),
+ message: this.translateService.instant('userAdministration.messages.warnings.userDeleted.message'),
+ },
+ });
+ return of(false);
+ }),
+ map(() => {
+ return true;
+ }),
+ );
+ }
+ return of(false);
+ }
+}
diff --git a/src/app/guards/has-permissions.guard.ts b/src/app/guards/has-permissions.guard.ts
new file mode 100644
index 0000000..cc04673
--- /dev/null
+++ b/src/app/guards/has-permissions.guard.ts
@@ -0,0 +1,72 @@
+/*
+ * Copyright (c) 2022. Deutsche Telekom AG
+ *
+ * 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
+ *
+ * http://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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+
+import { Inject, Injectable } from '@angular/core';
+import { ActivatedRouteSnapshot, CanActivate, Router, UrlTree } from '@angular/router';
+import { Observable } from 'rxjs';
+import { HttpClient } from '@angular/common/http';
+import { ACL_CONFIG, AclConfig } from '../modules/auth/injection-tokens';
+import { AuthService } from '../services/auth.service';
+import { TranslateService } from '@ngx-translate/core';
+import { map } from 'rxjs/operators';
+
+@Injectable({
+ providedIn: 'root',
+})
+export class HasPermissionsGuard implements CanActivate {
+ constructor(
+ private readonly authService: AuthService,
+ private readonly httpClient: HttpClient,
+ private readonly router: Router,
+ private readonly translateService: TranslateService,
+ @Inject(ACL_CONFIG) readonly acl: AclConfig,
+ ) {}
+
+ canActivate(
+ next: ActivatedRouteSnapshot,
+ ): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
+ return this.authService.loadCachedUserProfile().pipe(
+ map(userProfile => {
+ // filter out the keys (the onap_ roles) that the user does not have
+ const intersectionOfRoles = Object.keys(this.acl).filter(role => userProfile?.roles.includes(role));
+ return this.hasPermissions(next.data.permission, intersectionOfRoles);
+ }));
+ }
+
+ /**
+ * Check if a user has a given permission.
+ * @param permission the permission, as defined in the acl.json
+ * @param roles the roles that the user possesses
+ * @returns true if the user has the needed permission
+ */
+ private hasPermissions(permission: string, roles: string[]) {
+ for (const role of roles) {
+ if (this.acl[role].includes(permission)) {
+ return true;
+ }
+ }
+ this.router.navigate(['/statusPage'], {
+ state: {
+ header: this.translateService.instant('common.noPermissions.noPermissions'),
+ message: this.translateService.instant('common.noPermissions.support'),
+ },
+ });
+ return false;
+ }
+}
diff --git a/src/app/guards/pending-changes.guard.ts b/src/app/guards/pending-changes.guard.ts
new file mode 100644
index 0000000..625a7b1
--- /dev/null
+++ b/src/app/guards/pending-changes.guard.ts
@@ -0,0 +1,42 @@
+/*
+ * Copyright (c) 2022. Deutsche Telekom AG
+ *
+ * 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
+ *
+ * http://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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+
+import { CanDeactivate } from '@angular/router';
+import { Injectable } from '@angular/core';
+import { Observable } from 'rxjs';
+import { TranslateService } from '@ngx-translate/core';
+
+export interface ComponentCanDeactivate {
+ canDeactivate: () => boolean;
+}
+
+@Injectable()
+export class PendingChangesGuard implements CanDeactivate<ComponentCanDeactivate> {
+ constructor(public translateService: TranslateService) {}
+
+ canDeactivate(component: ComponentCanDeactivate): boolean | Observable<boolean> {
+ // if there are no pending changes, just allow deactivation; else confirm first
+ return component.canDeactivate()
+ ? true
+ : // NOTE: this warning message will only be shown when navigating elsewhere within your angular app;
+ // when navigating away from your angular app, the browser will show a generic warning message
+ // see http://stackoverflow.com/a/42207299/7307355
+ confirm(this.translateService.instant('serviceModels.warningMessage.warning'));
+ }
+}
diff --git a/src/app/helpers/filter-helpers.ts b/src/app/helpers/filter-helpers.ts
new file mode 100644
index 0000000..cc9f13e
--- /dev/null
+++ b/src/app/helpers/filter-helpers.ts
@@ -0,0 +1,256 @@
+/*
+ * Copyright (c) 2022. Deutsche Telekom AG
+ *
+ * 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
+ *
+ * http://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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { Filter } from 'src/app/helpers/listing';
+
+/**
+ * The JSONPath Filter Operators that are used for comparison (subset of [these](https://www.postgresql.org/docs/current/functions-json.html#FUNCTIONS-SQLJSON-FILTER-EX-TABLE)).
+ * For instance:
+ * ```
+ * $.id == 1
+ * $.type like_regex "type_(a|b)"
+ * ```
+ */
+export enum FilterOperator {
+ EQUAL = ' == ',
+ NOT_EQUAL = ' != ',
+ LIKE_REGEX = ' like_regex ',
+ OR = ' || ',
+ EQUAL_IN = ' IN '
+}
+
+/**
+ * Turns a Filter that is not formatted into a valid JSONPath one
+ * @param filter the filter that is not in JSONPath format
+ * @param string pass string: true if you want a return type string
+ * @returns a Filter with formatted: true or a string when parameter string: true
+ */
+export function createJsonPathFilter(filter: Filter): Filter;
+export function createJsonPathFilter(filter: Filter, returnsString: true): string;
+export function createJsonPathFilter(filter: Filter, returnsString = false): Filter | string {
+ // like_regex only works with strings
+ if (filter.operator === FilterOperator.LIKE_REGEX) {
+ if (returnsString) {
+ return createFilter(addFieldPrefix(filter.parameter), filter.operator, toSearchRegex(filter.value.toString()));
+ }
+ return {
+ parameter: addFieldPrefix(filter.parameter),
+ operator: filter.operator,
+ value: toSearchRegex(filter.value.toString()),
+ };
+ }
+ if (typeof filter.value === 'number') {
+ if (returnsString) {
+ return createFilter(addFieldPrefix(filter.parameter), filter.operator, filter.value.toString());
+ }
+ return {
+ parameter: addFieldPrefix(filter.parameter),
+ operator: filter.operator,
+ value: filter.value,
+ };
+ }
+ if (returnsString) {
+ return createFilter(addFieldPrefix(filter.parameter), filter.operator, quote(filter.value));
+ }
+ return {
+ parameter: addFieldPrefix(filter.parameter),
+ operator: filter.operator,
+ value: quote(filter.value),
+ };
+}
+
+/**
+ * Concatenates the provided input into a string.
+ * @param parameter the field to filter
+ * @param value the value it should match
+ * @param operator the operator for comparison (`==`,`!=`,`like_regex`,...)
+ * @param forApi
+ * @returns a concatenated string
+ */
+export function createFilter(parameter: string, operator: FilterOperator, value: string | string[], forApi= false): string {
+ if (forApi && operator === FilterOperator.EQUAL_IN) {
+ const valueArray = Array.isArray(value) ? value : value.split(',');
+ return addParentheses(createOrCondition(composeFilterFromArray(valueArray,parameter,FilterOperator.EQUAL)));
+ }
+ return `${parameter}${operator}${value}`;
+}
+/**
+ * Create array of composed filter
+ * @param value array of strings e.g array of IDs for alarms
+ * @param parameter the field to filter e.g $.id
+ * @param operator the operator for comparison
+ * @returns a string[]
+ */
+function composeFilterFromArray(value: string[], parameter: string, operator: FilterOperator): string[] {
+ return value
+ .reduce((acc: string[], curr) => [...acc, `${parameter}${operator}${curr}`], [])
+}
+/**
+ * Join array of string using '||' operator
+ * @param value string[]
+ * @returns a string
+ */
+function createOrCondition(value: string[]) {
+ return value.join(FilterOperator.OR)
+}
+/**
+ * Wraps a string in parentheses `"string"` -> `("string")`
+ * @param value string to wrap in quotes
+ * @returns a string wrapped in quotes
+ */
+function addParentheses(value: string) {
+ return ` ( ${value} ) `
+}
+/**
+ * Wraps a string in quotes `"string"` -> `"'string'"`
+ * @param value string to wrap in quotes
+ * @returns a string wrapped in quotes
+ */
+export function quote(value: string): string {
+ return `"${value}"`;
+}
+
+/**
+ * Add a `$.` prefix to the provided field
+ * @param value the field to match against
+ * @returns the field with a `$.` prefix
+ */
+export function addFieldPrefix(value: string): string {
+ return `$.${value}`;
+}
+
+/**
+ * Concatenate a list of categories into a regex expression for alternative matches.
+ * I.e `[a,b,c]` -> `"^(a|b|c)$"`
+ * @param categories the alternative values that should be matched
+ * @returns a regex expression of alternative matches
+ */
+export function toCategoricalRegex(categories: string[]): string {
+ return `"^(${categories.join('|')})$"`;
+}
+
+/**
+ * Extracts the list of categories from a given regular expression.
+ * I.e `"^(a|b|c)$"` -> `[a,b,c]`
+ * @param regex a regex expression of alternative matches
+ * @returns the categories from the regex
+ */
+export function fromCategoricalRegex(regex: string): string[] {
+ let strippedRegex = regex.slice(3, regex.length - 3);
+ return strippedRegex.split('|');
+}
+
+/**
+ * Turns the given searchTerm into a case insensitive value to be used with like_regex
+ * I.e `term` -> `"term" flag "i"`
+ * @param searchTerm the search term
+ * @returns the term in the format to be used with like_regex
+ */
+export function toSearchRegex(searchTerm: string): string {
+ return `"${searchTerm}" + flag "i"`;
+}
+
+/**
+ * Strips the value of a like_regex filter off its formatting.
+ * I.e `"value" flag "i"` -> `value`
+ * @param regex the value for the `like_regex` operation
+ * @returns the value without quotes and flags
+ */
+export function fromSearchRegex(regex: string): string {
+ const rawValue = regex?.split(' ')[0];
+ if (rawValue.charAt(0) != '"' || rawValue.charAt(rawValue.length - 1) != '"') {
+ throw new Error('Error while extracting the value from the url. Is it in the correct format?');
+ }
+ return rawValue.substring(1, rawValue.length - 1);
+}
+
+/**
+ * Join the individual filters with `&&` into a long expression
+ * @param filters the array of filter strings
+ * @returns a long filter string chained by `&&`'s
+ */
+export function composeFilter(filters: string[]): string {
+ return filters.join('&&');
+}
+
+/**
+ * Turn the provided string of filters into a Map of filters
+ * @param filter the filter string
+ * @returns a Map of filters
+ */
+export function parseFilterFromUrl(filter: string | null): Map<string, Filter> {
+ if (!filter) {
+ return new Map<string, Filter>();
+ }
+ const filters = filter.split('&&');
+ return splitFilterByOperator(filters);
+}
+
+/**
+ * Parse a list of strings in filter format into a Map of Filters
+ * @param filters list of strings in format `['']`
+ * @returns a map of Filters
+ */
+function splitFilterByOperator(filters: string[]): Map<string, Filter> {
+ const mappedFilters = new Map<string, Filter>();
+ filters
+ .map(filter => {
+ if (filter.includes(FilterOperator.EQUAL_IN)) {
+ const [parameter, value] = filter.split(FilterOperator.EQUAL_IN);
+ return { parameter, value, operator: FilterOperator.EQUAL_IN };
+ }
+ if (filter.includes(FilterOperator.EQUAL)) {
+ const [parameter, value] = filter.split(FilterOperator.EQUAL);
+ return { parameter, value, operator: FilterOperator.EQUAL };
+ }
+ if (filter.includes(FilterOperator.NOT_EQUAL)) {
+ const [parameter, value] = filter.split(FilterOperator.NOT_EQUAL);
+ return { parameter, value, operator: FilterOperator.NOT_EQUAL };
+ }
+ if (filter.includes(FilterOperator.LIKE_REGEX)) {
+ const [parameter, value] = filter.split(FilterOperator.LIKE_REGEX);
+ return { parameter, value, operator: FilterOperator.LIKE_REGEX };
+ }
+ throw new Error('Unsupported operator');
+ })
+ .forEach(item => mappedFilters.set(item.parameter, item));
+ return mappedFilters;
+}
+
+/**
+ * Transforms the map of filters back into a JSONPATH filter string
+ * @param filters
+ * @param forApi
+ * @returns a string containing all filter expressions or undefined if size of the Filter Map is 0
+ */
+export function filterBuilder(filters: Map<string, Filter>, forApi=false): string | undefined {
+ if (filters.size === 0) {
+ return undefined;
+ }
+ const filtersString: string[] = [];
+ for (const [, filter] of filters.entries()) {
+ filtersString.push(
+ createFilter(
+ filter.parameter,
+ filter.operator,
+ filter.value.toString(),
+ forApi),
+ );
+ }
+ return composeFilter(filtersString);
+}
diff --git a/src/app/helpers/form-validators.ts b/src/app/helpers/form-validators.ts
new file mode 100644
index 0000000..6cd3acd
--- /dev/null
+++ b/src/app/helpers/form-validators.ts
@@ -0,0 +1,32 @@
+/*
+ * Copyright (c) 2022. Deutsche Telekom AG
+ *
+ * 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
+ *
+ * http://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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+
+import { AbstractControl, ValidationErrors } from '@angular/forms';
+
+export class WhiteSpaceValidator {
+ static noWhiteSpace(control: AbstractControl): ValidationErrors | null {
+ if (control.value !== null) {
+ if ((control.value as string).indexOf(' ') >= 0) {
+ return { noWhiteSpace: true };
+ }
+ return null;
+ }
+ return null;
+ }
+}
diff --git a/src/app/helpers/helpers.ts b/src/app/helpers/helpers.ts
new file mode 100644
index 0000000..7c03dbd
--- /dev/null
+++ b/src/app/helpers/helpers.ts
@@ -0,0 +1,76 @@
+/*
+ * Copyright (c) 2022. Deutsche Telekom AG
+ *
+ * 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
+ *
+ * http://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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+
+import { FormArray, FormGroup } from '@angular/forms';
+
+export function isNotUndefined<T>(val: T | undefined): val is T {
+ return val !== undefined;
+}
+
+export function isNotNull<T>(val: T | null): val is T {
+ return val !== null;
+}
+
+export function markAsDirtyAndValidate(formGroup: FormGroup): void {
+ Object.values(formGroup.controls).forEach(control => {
+ control.markAsDirty();
+ control.updateValueAndValidity();
+ });
+}
+
+export function isNullOrUndefined(val: any): boolean {
+ return val === null || val === undefined;
+}
+
+export function isNotNullOrUndefined(val: any): boolean {
+ return val !== null && val !== undefined;
+}
+
+export function isNullOrUndefinedOrEmptyString(val: any): boolean {
+ return val === null || val === undefined || val === '';
+}
+
+export function isEmptyArray(array: any[]):boolean {
+ return !array.length
+}
+
+export function getRandomNumber(min: number, max: number) {
+ return Math.floor(Math.random() * (max - min + 1)) + min;
+}
+
+export function areFormControlsValid(form: FormGroup): boolean {
+ const formControls = Object.keys(form.controls)
+ .map(key => form.controls[key])
+ .filter(control => !(control instanceof FormArray));
+ return formControls.find(control => control.invalid && (control.dirty || control.touched)) === undefined;
+}
+
+export function isString(value: any): boolean {
+ return typeof value === 'string' || value instanceof String;
+}
+
+export function resetSelectDefaultValue(cssSelector: string): void {
+ setTimeout(() => {
+ const element = document.querySelector(cssSelector);
+ if (element) {
+ //@ts-ignore
+ document.querySelector(cssSelector)?.selectedIndex = -1;
+ }
+ }, 0);
+}
diff --git a/src/app/helpers/listing.ts b/src/app/helpers/listing.ts
new file mode 100644
index 0000000..66d1ae0
--- /dev/null
+++ b/src/app/helpers/listing.ts
@@ -0,0 +1,90 @@
+/*
+ * Copyright (c) 2022. Deutsche Telekom AG
+ *
+ * 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
+ *
+ * http://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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+
+import { Router } from '@angular/router';
+import { filterBuilder, FilterOperator } from 'src/app/helpers/filter-helpers';
+
+/**
+ * A JSONPath filter. One or more `Filter`s can be passed to the `filter=` param of the alarm API.
+ */
+export interface Filter {
+ /**
+ * The field to compare against, i.e `$.type`
+ */
+ parameter: string;
+ /**
+ * The JSONPath Filter Operators that are used for comparison (subset of [these](https://www.postgresql.org/docs/current/functions-json.html#FUNCTIONS-SQLJSON-FILTER-EX-TABLE)).
+ * For instance `==` and `like_regex`:
+ * ```
+ * $.id == 1
+ * $.type like_regex "type_(a|b)"
+ * ```
+ */
+ operator: FilterOperator;
+ /**
+ * The value or pattern that the field should have, i.e `"type_(a|b)"`
+ */
+ value: string | number;
+}
+
+/**
+ * This represents one filter expression for one column.
+ * Use `undefined` to signal removal of the filter.
+ *
+ * Note that parameter needs to be the complete param (i.e `$.column`).
+ * That is necessary because this will be the key when parsing the filter expression into a map.
+ */
+export type ColumnFilter = {
+ parameter: string;
+ filter: Filter | undefined;
+};
+
+export function changePage(router: Router, page: number): void {
+ router.navigate([], { queryParams: { page }, queryParamsHandling: 'merge' });
+}
+
+export function changePageSize(router: Router, pageSize: number): void {
+ router.navigate([], {
+ queryParams: { page: 1, pageSize },
+ queryParamsHandling: 'merge',
+ });
+}
+
+export function changeFilter(router: Router, value: string | undefined): void {
+ router.navigate([], {
+ queryParams: { filter: value ? value.trim() : undefined, page: 1 },
+ queryParamsHandling: 'merge',
+ });
+}
+
+/**
+ * Format a JSONPath filter expression into the format required by the angular router.
+ * Add or update this filter param in the router.
+ * @param router the angular router
+ * @param filters the filter expression in JSONPath format
+ * @param path path for new router link
+ * @param params other queryParams
+ */
+export function changeFiltersInRouter(router: Router, filters: Map<string, Filter>, params?: { [key: string]: any }, path?: any[]): void {
+ const composedFilter = filters.size > 0 ? filterBuilder(filters) : undefined;
+ router.navigate(path ?? [], {
+ queryParams: { filter: composedFilter, ...params, page: 1 },
+ queryParamsHandling: 'merge',
+ });
+}
diff --git a/src/app/helpers/sorting-helpers.ts b/src/app/helpers/sorting-helpers.ts
new file mode 100644
index 0000000..94ec9ef
--- /dev/null
+++ b/src/app/helpers/sorting-helpers.ts
@@ -0,0 +1,59 @@
+/*
+ * Copyright (c) 2022. Deutsche Telekom AG
+ *
+ * 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
+ *
+ * http://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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+
+import { Sort, SortDirection } from '../components/shared/sort/sort.component';
+import { Router } from '@angular/router';
+
+export function parseSortStringToSort(str: string): Sort {
+ return str.startsWith('-')
+ ? { parameter: str.split('-')[1], direction: SortDirection.DESC }
+ : { parameter: str, direction: SortDirection.ASC };
+}
+export function applyNewSorting(router: Router, sort: Sort) {
+ if (sort.direction === SortDirection.NONE) {
+ router.navigate([], {
+ queryParams: { page: 1, sort: null },
+ queryParamsHandling: 'merge',
+ });
+ } else {
+ router.navigate([], {
+ queryParams: { page: 1, sort: sort.direction === SortDirection.ASC ? sort.parameter : '-' + sort.parameter },
+ queryParamsHandling: 'merge',
+ });
+ }
+}
+export function parseSortToSortString(sort: Sort): string {
+ if (sort.direction === SortDirection.ASC) {
+ return sort.parameter;
+ }
+ if (sort.direction === SortDirection.DESC) {
+ return '-' + sort.parameter;
+ }
+ return '';
+}
+
+export function parseSortToSortJsonString(sort: Sort): string {
+ if (sort.direction === SortDirection.ASC) {
+ return '$.' + sort.parameter;
+ }
+ if (sort.direction === SortDirection.DESC) {
+ return '-$.' + sort.parameter;
+ }
+ return '';
+}
diff --git a/src/app/http-interceptors/auth.interceptor.ts b/src/app/http-interceptors/auth.interceptor.ts
new file mode 100644
index 0000000..fc13e70
--- /dev/null
+++ b/src/app/http-interceptors/auth.interceptor.ts
@@ -0,0 +1,49 @@
+/*
+ * Copyright (c) 2022. Deutsche Telekom AG
+ *
+ * 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
+ *
+ * http://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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+
+import { Injectable } from '@angular/core';
+import { OAuthStorage } from 'angular-oauth2-oidc';
+import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http';
+
+import { Observable, throwError } from 'rxjs';
+import { environment } from '../../environments/environment';
+
+@Injectable()
+export class AuthInterceptor implements HttpInterceptor {
+ constructor(private authStorage: OAuthStorage) {}
+
+ public intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
+ const url = req.url.toLowerCase();
+ if (!url.startsWith(environment.backendServerUrl) && !url.startsWith(environment.loggingUrl)) {
+ return next.handle(req);
+ }
+
+ const accessToken = this.authStorage.getItem('access_token');
+ const idToken = this.authStorage.getItem('id_token');
+ if (accessToken === null || idToken === null) {
+ return throwError(new Error('Missing access or ID token'));
+ }
+ const headers = req.headers
+ .set('Authorization', `Bearer ${accessToken}`)
+ .set('X-Auth-Identity', `Bearer ${idToken}`);
+ req = req.clone({ headers });
+
+ return next.handle(req);
+ }
+}
diff --git a/src/app/http-interceptors/caching-interceptor.ts b/src/app/http-interceptors/caching-interceptor.ts
new file mode 100644
index 0000000..df13376
--- /dev/null
+++ b/src/app/http-interceptors/caching-interceptor.ts
@@ -0,0 +1,94 @@
+/*
+ * Copyright (c) 2022. Deutsche Telekom AG
+ *
+ * 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
+ *
+ * http://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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+
+import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest, HttpResponse } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+import { Observable, of } from 'rxjs';
+import { startWith, tap } from 'rxjs/operators';
+
+import { RequestCache } from '../services/cacheservice/request-cache.service';
+import { urlTileApi } from '../services/tileservice/tiles.service';
+
+/**
+ * Interceptor that returns HttpResponses from cache and updates cache if outdated
+ * This applies to GET requests from specified urls (specified in this class)
+ */
+@Injectable()
+// https://angular.io/guide/http#using-interceptors-for-caching
+// https://github.com/angular/angular/blob/master/aio/content/examples/http/src/app/http-interceptors/caching-interceptor.ts
+export class CachingInterceptor implements HttpInterceptor {
+ private cachedUrls: Array<string> = [
+ urlTileApi,
+ window.location.origin + '/keycloak/auth/realms/ONAP/protocol/openid-connect/userinfo',
+ ];
+
+ // cache is the cache service that supports get() and put()
+ constructor(private cache: RequestCache) {}
+
+ intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
+ // continue if not cacheable.
+ if (!this.isCacheable(request)) {
+ return next.handle(request);
+ }
+
+ const cachedResponse = this.cache.get(request);
+
+ // when a custom 'x-refresh' header is set, refresh the cash
+ // (see https://github.com/angular/angular/blob/master/aio/content/examples/http/src/app/package-search/package-search.service.ts)
+ if (request.headers.get('x-refresh')) {
+ const results$ = this.getUpdatedResponse(request, next, this.cache);
+ return cachedResponse ? results$.pipe(startWith(cachedResponse)) : results$;
+ }
+ // cache-or-fetch
+ return cachedResponse ? of(cachedResponse) : this.getUpdatedResponse(request, next, this.cache);
+ }
+
+ /**
+ * Checks if the request is cacheable.
+ * This is true if the request is a http GET and the url is defined in this method
+ * (-> urlTileApi)
+ */
+ private isCacheable(req: HttpRequest<any>): boolean {
+ // Only GET requests are cacheable
+ return (
+ req.method === 'GET' &&
+ // Only the tile api is cacheable in this app
+ -1 < this.cachedUrls.indexOf(req.url)
+ );
+ }
+
+ /**
+ * Get server response observable by sending request to `next()`.
+ * Will add the response to the cache on the way out.
+ */
+ private getUpdatedResponse(
+ req: HttpRequest<any>,
+ next: HttpHandler,
+ cache: RequestCache,
+ ): Observable<HttpEvent<any>> {
+ return next.handle(req.clone()).pipe(
+ tap(event => {
+ // There may be other events besides the response.
+ if (event instanceof HttpResponse) {
+ cache.put(req, event); // Update the cache.
+ }
+ }),
+ );
+ }
+}
diff --git a/src/app/http-interceptors/http-error.interceptor.ts b/src/app/http-interceptors/http-error.interceptor.ts
new file mode 100644
index 0000000..61d55e0
--- /dev/null
+++ b/src/app/http-interceptors/http-error.interceptor.ts
@@ -0,0 +1,142 @@
+/*
+ * Copyright (c) 2022. Deutsche Telekom AG
+ *
+ * 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
+ *
+ * http://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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+
+import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+import { Observable, throwError, TimeoutError } from 'rxjs';
+import { catchError } from 'rxjs/operators';
+import { AlertService } from '../modules/alerting';
+import { TranslateService } from '@ngx-translate/core';
+import { Problem } from '../../../openapi/output';
+import { Router } from '@angular/router';
+import { DEFAULT_TIMEOUT } from './timeout.interceptor';
+
+/**
+ * This class adds global handling of http-request related errors
+ */
+
+export enum RequestMethod {
+ DELETE = 'DELETE',
+ POST = 'POST',
+ GET = 'GET',
+}
+
+interface ProblemDetail {
+ errorDetail: Problem;
+ requestId?: string;
+ urlTree: string[];
+}
+@Injectable()
+export class HttpErrorInterceptor implements HttpInterceptor {
+ errorDetail!: Problem;
+ constructor(private alertService: AlertService, private translateService: TranslateService, private router: Router) {}
+
+ intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
+ return next.handle(request).pipe(
+ catchError(rsp => {
+ const urlTree = this.router.url.split('/');
+ this.errorDetail = this.createErrorDetail(rsp);
+ const requestId = request.headers.get('x-request-id') || undefined;
+ const detail: ProblemDetail = {
+ errorDetail: this.errorDetail,
+ requestId,
+ urlTree,
+ };
+
+ if (request.url.includes('onap_logging')) {
+ this.alertService.warn(this.translateService.instant('common.block.logging'), {
+ id: 'onap_logging',
+ });
+ return throwError(this.errorDetail);
+ }
+ if (request.method === RequestMethod.POST && request.url.includes('keycloak')) {
+ this.alertService.error(this.translateService.instant('common.block.authorization'), {
+ id: 'keycloak',
+ });
+ return throwError(this.errorDetail);
+ }
+ if (request.url.includes('preferences')) {
+ this.getPreferenceMessage(request, detail);
+ return throwError(this.errorDetail);
+ }
+ if (request.url.includes('actions')) {
+ this.getActionMessage(request, detail);
+ return throwError(this.errorDetail);
+ }
+
+ switch (urlTree[1].split('?')[0]) {
+ case 'user-administration':
+ this.getUserAdministrationMessage(request, detail, urlTree);
+ break;
+ case 'dashboard':
+ this.getErrorMessage('dashboard', detail);
+ break;
+ case 'app-starter':
+ this.getErrorMessage('appStarter', detail);
+ break;
+ default:
+ this.getErrorMessage('defaultMessage', detail);
+ break;
+ }
+ return throwError(this.errorDetail);
+ }),
+ );
+ }
+
+ private getUserAdministrationMessage(request: HttpRequest<any>, detail: ProblemDetail, urlTree: string[]) {
+ if (request.method === RequestMethod.DELETE) {
+ return this.getErrorMessage('userAdministration.delete', detail);
+ }
+ if (urlTree.includes('create')) {
+ return this.getErrorMessage('userAdministration.create', detail);
+ }
+ if (urlTree.includes('edit')) {
+ return this.getErrorMessage('userAdministration.edit', detail);
+ }
+ }
+
+ private getActionMessage(request: HttpRequest<any>, detail: ProblemDetail) {
+ if (request.method === RequestMethod.POST) {
+ return this.getErrorMessage('saveAction', detail);
+ }
+ this.getErrorMessage('loadAction', detail);
+ }
+
+ private getPreferenceMessage(request: HttpRequest<any>, detail: ProblemDetail) {
+ if (request.method === RequestMethod.POST) {
+ return this.getErrorMessage('savePreferences', detail);
+ }
+ this.getErrorMessage('loadPreferences', detail);
+ }
+
+ private createErrorDetail(response: any): Problem {
+ if (response instanceof TimeoutError) {
+ return {
+ detail: this.translateService.instant('common.block.timeout', { value: DEFAULT_TIMEOUT / 1000 }),
+ title: response.name,
+ status: 408,
+ };
+ }
+ return response.error ? response.error : response;
+ }
+
+ private getErrorMessage(type: string, detail: ProblemDetail) {
+ this.alertService.error(`${this.translateService.instant('common.block.' + type)}`, detail);
+ }
+}
diff --git a/src/app/http-interceptors/interceptors.ts b/src/app/http-interceptors/interceptors.ts
new file mode 100644
index 0000000..5fd0a97
--- /dev/null
+++ b/src/app/http-interceptors/interceptors.ts
@@ -0,0 +1,42 @@
+/*
+ * Copyright (c) 2022. Deutsche Telekom AG
+ *
+ * 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
+ *
+ * http://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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+
+/* "Barrel" of Http Interceptors */
+import { HTTP_INTERCEPTORS } from '@angular/common/http';
+import { CachingInterceptor } from './caching-interceptor';
+import { HttpErrorInterceptor } from './http-error.interceptor';
+import { MockInterceptor } from './mock.interceptor';
+import { LoadingIndicatorInterceptor } from 'src/app/http-interceptors/loading-indicator.interceptor';
+import { RequestidInterceptor } from './requestid.interceptor';
+import { TimeoutInterceptor } from './timeout.interceptor';
+import { LoggingInterceptor } from './logging.interceptor';
+
+/** Http interceptor providers in outside-in order
+ * Gathers all the interceptor providers into an httpInterceptorProviders array
+ */
+// https://angular.io/guide/http#provide-the-interceptor
+export const httpInterceptorProviders = [
+ { provide: HTTP_INTERCEPTORS, useClass: MockInterceptor, multi: true },
+ { provide: HTTP_INTERCEPTORS, useClass: TimeoutInterceptor, multi: true },
+ { provide: HTTP_INTERCEPTORS, useClass: LoadingIndicatorInterceptor, multi: true },
+ { provide: HTTP_INTERCEPTORS, useClass: RequestidInterceptor, multi: true },
+ { provide: HTTP_INTERCEPTORS, useClass: HttpErrorInterceptor, multi: true },
+ { provide: HTTP_INTERCEPTORS, useClass: CachingInterceptor, multi: true },
+ { provide: HTTP_INTERCEPTORS, useClass: LoggingInterceptor, multi: true },
+];
diff --git a/src/app/http-interceptors/loading-indicator.interceptor.ts b/src/app/http-interceptors/loading-indicator.interceptor.ts
new file mode 100644
index 0000000..d48d04f
--- /dev/null
+++ b/src/app/http-interceptors/loading-indicator.interceptor.ts
@@ -0,0 +1,41 @@
+/*
+ * Copyright (c) 2022. Deutsche Telekom AG
+ *
+ * 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
+ *
+ * http://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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+
+import { Injectable } from '@angular/core';
+import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http';
+import { Observable } from 'rxjs';
+import { finalize } from 'rxjs/operators';
+import { LoadingIndicatorService } from 'src/app/services/loading-indicator.service';
+
+@Injectable()
+export class LoadingIndicatorInterceptor implements HttpInterceptor {
+ readonly excludedUrls = ['preferences', 'actions'];
+
+ constructor(private readonly loadingIndicator: LoadingIndicatorService) {}
+
+ intercept(req: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
+ if (this.excludedUrls.some(url => req.url.includes(url))) {
+ return next.handle(req);
+ }
+
+ this.loadingIndicator.show();
+ return next.handle(req).pipe(finalize(() => this.loadingIndicator.hide()));
+
+ }
+}
diff --git a/src/app/http-interceptors/logging.interceptor.ts b/src/app/http-interceptors/logging.interceptor.ts
new file mode 100644
index 0000000..8975868
--- /dev/null
+++ b/src/app/http-interceptors/logging.interceptor.ts
@@ -0,0 +1,64 @@
+/*
+ * Copyright (c) 2022. Deutsche Telekom AG
+ *
+ * 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
+ *
+ * http://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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+
+import { Injectable } from '@angular/core';
+import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest, HttpResponse } from '@angular/common/http';
+import { Observable } from 'rxjs';
+import { environment } from '../../environments/environment';
+import { take, tap } from 'rxjs/operators';
+import { LoggingService } from '../services/logging.service';
+
+@Injectable()
+export class LoggingInterceptor implements HttpInterceptor {
+ constructor(private readonly loggingService: LoggingService) {}
+
+ intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
+ if (environment.loggingEnabled) {
+ if (
+ request.url.toLowerCase().startsWith(environment.backendServerUrl) &&
+ !request.url.toLowerCase().startsWith(environment.loggingUrl)
+ ) {
+ const requestMessage = `Portal-ui - request - X-Request-Id <${request.headers.get('x-request-id')}> <${
+ request.method
+ }> <${request.url}>`;
+ this.loggingService
+ .writeLog(`'@timestamp': ${new Date().toISOString()}, message: ${requestMessage}`)
+ .pipe(take(1))
+ .subscribe();
+
+ return next.handle(request).pipe(
+ tap((event: HttpEvent<any>) => {
+ if (event instanceof HttpResponse) {
+ const requestId = event.headers.get('x-request-id');
+ const responseMessage = `Portal-ui - response - X-Request-Id <${requestId ?? 'Not set'}> <${
+ event.status
+ }>`;
+ this.loggingService
+ .writeLog(`'@timestamp': ${new Date().toISOString()}, message: ${responseMessage}`)
+ .pipe(take(1))
+ .subscribe();
+ }
+ }),
+ );
+ }
+ return next.handle(request);
+ }
+ return next.handle(request);
+ }
+}
diff --git a/src/app/http-interceptors/mock.interceptor.ts b/src/app/http-interceptors/mock.interceptor.ts
new file mode 100644
index 0000000..6fcb122
--- /dev/null
+++ b/src/app/http-interceptors/mock.interceptor.ts
@@ -0,0 +1,50 @@
+/*
+ * Copyright (c) 2022. Deutsche Telekom AG
+ *
+ * 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
+ *
+ * http://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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+
+import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+import { Observable } from 'rxjs';
+
+/**
+ * This Interceptor reroutes URLs that are defined in the `matchers` array to [Wiremock](https://wiremock.org/) endpoints.
+ * This is useful, when the real api is not yet available.
+ */
+@Injectable()
+export class MockInterceptor implements HttpInterceptor {
+ // list of all available RegExp URL matchers
+ // !! do not forget to remove matcher when API implementation is ready !!
+ private readonly matchers: RegExp[] = [
+ // for example:
+ // /tiles/
+ ];
+
+ intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
+ let newRequest = request;
+
+ if (this.matchers.some(matcher => request.url.match(matcher))) {
+ console.warn(`MockInterceptor is enabled for URL: '${request.url}'`);
+ // intentional usage of .replace instead of .replaceAll to replace only first instance
+ newRequest = request.clone({
+ url: request.url.replace('/api/', '/mock-api/'),
+ });
+ }
+
+ return next.handle(newRequest);
+ }
+}
diff --git a/src/app/http-interceptors/requestid.interceptor.ts b/src/app/http-interceptors/requestid.interceptor.ts
new file mode 100644
index 0000000..6c3c912
--- /dev/null
+++ b/src/app/http-interceptors/requestid.interceptor.ts
@@ -0,0 +1,37 @@
+/*
+ * Copyright (c) 2022. Deutsche Telekom AG
+ *
+ * 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
+ *
+ * http://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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+
+import { Injectable } from '@angular/core';
+import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http';
+import { Observable } from 'rxjs';
+import { environment } from 'src/environments/environment';
+import { v4 as uuid } from 'uuid';
+
+@Injectable()
+export class RequestidInterceptor implements HttpInterceptor {
+ intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
+ // this is skipping everything that is not /api (like /keycloak)
+ if (!request.url.toLowerCase().startsWith(environment.backendServerUrl)) {
+ return next.handle(request);
+ }
+
+ request = request.clone({ setHeaders: { 'X-Request-Id': uuid() } });
+ return next.handle(request);
+ }
+}
diff --git a/src/app/http-interceptors/timeout.interceptor.ts b/src/app/http-interceptors/timeout.interceptor.ts
new file mode 100644
index 0000000..e320b79
--- /dev/null
+++ b/src/app/http-interceptors/timeout.interceptor.ts
@@ -0,0 +1,51 @@
+/*
+ * Copyright (c) 2022. Deutsche Telekom AG
+ *
+ * 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
+ *
+ * http://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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+
+import { Injectable } from '@angular/core';
+import {
+ HttpRequest,
+ HttpHandler,
+ HttpEvent,
+ HttpInterceptor
+} from '@angular/common/http';
+import { Observable, throwError, TimeoutError } from 'rxjs';
+import { catchError, timeout } from 'rxjs/operators';
+import { AlertService } from '../modules/alerting';
+import { TranslateService } from '@ngx-translate/core';
+
+//60 seconds
+export const DEFAULT_TIMEOUT = 60000;
+
+@Injectable()
+export class TimeoutInterceptor implements HttpInterceptor {
+
+ constructor(private alertService: AlertService, private translateService: TranslateService) {}
+ intercept(req: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>>{
+
+ return next.handle(req).pipe(
+ timeout(DEFAULT_TIMEOUT),
+ catchError(err => {
+ if (err instanceof TimeoutError) {
+ this.alertService.error(this.translateService.instant('common.messages.timeout'))
+ }
+ return throwError(err);
+ })
+ );
+ }
+}
diff --git a/src/app/model/dashboard.model.ts b/src/app/model/dashboard.model.ts
new file mode 100644
index 0000000..2f501c3
--- /dev/null
+++ b/src/app/model/dashboard.model.ts
@@ -0,0 +1,26 @@
+/*
+ * Copyright (c) 2022. Deutsche Telekom AG
+ *
+ * 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
+ *
+ * http://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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+
+export enum DashboardApplications {
+ USER_LAST_ACTION_TILE = 'USER_LAST_ACTION_TILE',
+}
+export interface DashboardTileSettings {
+ type: DashboardApplications;
+ displayed: boolean;
+}
diff --git a/src/app/model/environment.model.ts b/src/app/model/environment.model.ts
new file mode 100644
index 0000000..5bfb615
--- /dev/null
+++ b/src/app/model/environment.model.ts
@@ -0,0 +1,45 @@
+/*
+ * Copyright (c) 2022. Deutsche Telekom AG
+ *
+ * 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
+ *
+ * http://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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+
+export interface Environment {
+ customStyleEnabled: boolean;
+ backendServerUrl: string;
+ hostname: string;
+ keycloakEditProfile: string;
+ production: boolean;
+ keycloak: KeycloakEnvironment;
+ dateTimeFormat: string;
+ loggingUrl: string;
+ loggingEnabled: boolean
+ supportUrlLink: string
+
+}
+
+export interface KeycloakEnvironment {
+ issuer: string;
+ redirectUri: string;
+ clientId: string;
+ responseType: string;
+ scope: string;
+ requireHttps: boolean;
+ showDebugInformation: boolean;
+ disableAtHashCheck: boolean;
+ skipIssuerCheck: boolean;
+ strictDiscoveryDocumentValidation: boolean;
+}
diff --git a/src/app/model/tile.ts b/src/app/model/tile.ts
new file mode 100644
index 0000000..c23482e
--- /dev/null
+++ b/src/app/model/tile.ts
@@ -0,0 +1,47 @@
+/*
+ * Copyright (c) 2022. Deutsche Telekom AG
+ *
+ * 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
+ *
+ * http://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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+
+export interface TilesListResponse {
+ items: Tile[];
+}
+export interface Tile {
+ id: number;
+ title: string;
+ imageUrl: string;
+ imageAltText: string;
+ description: string;
+ redirectUrl: string;
+ headers?: string;
+ groups: Group[];
+ roles: Role[];
+}
+
+export enum Group {
+ ADMIN = 'ADMIN',
+ DEVELOPER = 'DEVELOPER',
+ OPERATOR = 'OPERATOR',
+}
+
+export enum Role {
+ ONAP_OPERATOR = 'ONAP_OPERATOR',
+ ONAP_DESIGNER = 'ONAP_DESIGNER',
+ ONAP_ADMIN = 'ONAP_ADMIN',
+}
+
+
diff --git a/src/app/model/user-last-action.model.ts b/src/app/model/user-last-action.model.ts
new file mode 100644
index 0000000..8941f18
--- /dev/null
+++ b/src/app/model/user-last-action.model.ts
@@ -0,0 +1,75 @@
+/*
+ * Copyright (c) 2022. Deutsche Telekom AG
+ *
+ * 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
+ *
+ * http://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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+
+export enum ActionFilter {
+ ALL = 'ALL',
+ SEARCH = 'SEARCH',
+ ACTION = 'ACTION',
+}
+
+export enum ActionType {
+ SEARCH = 'SEARCH',
+ VIEW = 'VIEW',
+ EDIT = 'EDIT',
+ DEPLOY = 'DEPLOY',
+ DELETE = 'DELETE',
+ CREATE = 'CREATE',
+ CLEAR = 'CLEAR',
+ ACK = 'ACK',
+ UNACK = 'UNACK',
+}
+
+export enum ActionInterval {
+ LAST1H = '1H',
+ LAST4H = '4H',
+ LAST1D = '1D',
+ ALL = 'ALL',
+}
+
+export enum EntityType {
+ USERADMINISTRATION = 'USERADMINISTRATION',
+}
+
+export interface EntityUserHistoryActionModel {
+ userId: string;
+ userName: string;
+}
+
+export interface CreateActionModel<T> {
+ type: ActionType;
+ entity: EntityType;
+ entityParams: T;
+}
+
+export interface ActionRowModel<T> {
+ actionCreatedAt: string;
+ type: ActionType;
+ entity: EntityType;
+ entityParams: T;
+}
+
+export interface ActionModel {
+ actionCreatedAt: string;
+ type: ActionType;
+ entity: EntityType;
+ entityParams: EntityTypeModel;
+}
+
+export type EntityTypeModel =
+ | EntityUserHistoryActionModel
diff --git a/src/app/model/user-preferences.model.ts b/src/app/model/user-preferences.model.ts
new file mode 100644
index 0000000..61f9718
--- /dev/null
+++ b/src/app/model/user-preferences.model.ts
@@ -0,0 +1,87 @@
+/*
+ * Copyright (c) 2022. Deutsche Telekom AG
+ *
+ * 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
+ *
+ * http://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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+
+
+import { DashboardApplications } from './dashboard.model';
+import { ActionFilter, ActionInterval } from './user-last-action.model';
+
+export const STATE_KEYS = {
+ DASHBOARD: 'dashboard',
+ APPS: 'apps',
+ TILES: 'availableTiles',
+ USER_ACTIONS: 'lastUserAction',
+ FILTER_TYPE:'filterType',
+ INTERVAL: 'interval'
+};
+
+
+export interface DashboardModel {
+ apps: DashboardAppsModel;
+}
+
+export interface DashboardAppsModel {
+ availableTiles: DashboardTileSettings[];
+ lastUserAction: LastUserActionSettings;
+}
+
+export interface UserPreferencesModel {
+ dashboard: DashboardModel;
+}
+
+export interface UpdateUserPreferenceModel {
+ dashboard?: {
+ apps?: {
+ availableTiles?: DashboardTileSettings[];
+ lastUserAction?: LastUserActionSettings;
+ };
+ };
+}
+
+export interface DashboardTileSettings {
+ type: DashboardApplications;
+ displayed: boolean;
+}
+
+export interface LastUserActionSettings {
+ interval: ActionInterval;
+ filterType: ActionFilter;
+}
+
+const availableDashboardApps: DashboardTileSettings[] = [
+ {
+ type: DashboardApplications.USER_LAST_ACTION_TILE,
+ displayed: true,
+ },
+];
+
+export const defaultLastUserActionSettings: LastUserActionSettings = {
+ interval: ActionInterval.LAST1H,
+ filterType: ActionFilter.ALL,
+};
+
+export const defaultUserSettings: UserPreferencesModel = {
+ dashboard: {
+ apps: {
+ availableTiles: availableDashboardApps,
+ lastUserAction: defaultLastUserActionSettings,
+ },
+ },
+};
+
+export const DASHBOARD_SECTION = 'dashboard';
diff --git a/src/app/model/validation-pattern.model.ts b/src/app/model/validation-pattern.model.ts
new file mode 100644
index 0000000..d611d44
--- /dev/null
+++ b/src/app/model/validation-pattern.model.ts
@@ -0,0 +1,26 @@
+/*
+ * Copyright (c) 2022. Deutsche Telekom AG
+ *
+ * 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
+ *
+ * http://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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+
+export const VALIDATION_PATTERN = "[\\w,/!=§#@€:µ.+?' \\-\\u00C0-\\u017F]*";
+export const NON_WHITE_SPACE_PATTERN = new RegExp('\\S');
+
+ //Info from team Euler --> predefined regexp in SO service instance name is:
+// public static final String VALID_INSTANCE_NAME_FORMAT = "^[a-zA-Z][a-zA-Z0-9._-]*$";
+// thanks to that we will avoid the error during model deployment
+export const VALID_INSTANCE_NAME_FORMAT_PATTERN = "^[a-zA-Z][a-zA-Z0-9._-]*$";
diff --git a/src/app/modules/alerting/alert.component.css b/src/app/modules/alerting/alert.component.css
new file mode 100644
index 0000000..aeadd64
--- /dev/null
+++ b/src/app/modules/alerting/alert.component.css
@@ -0,0 +1,63 @@
+/*
+ * Copyright (c) 2022. Deutsche Telekom AG
+ *
+ * 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
+ *
+ * http://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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+
+span {
+ width: 500px;
+}
+.alert-success {
+ color: #6bb324 !important;
+}
+
+.alert-success > button.close::before {
+ background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M15.81,14.99l-6.99-7l6.99-7c0.24-0.24,0.2-0.63-0.04-0.83c-0.24-0.2-0.59-0.2-0.79,0l-6.99,7l-6.99-7 C0.75-0.08,0.36-0.04,0.16,0.2c-0.2,0.24-0.2,0.59,0,0.79l6.99,7l-6.99,7c-0.24,0.24-0.2,0.63,0.04,0.83c0.24,0.2,0.59,0.2,0.79,0 l6.99-7l6.99,7c0.24,0.24,0.59,0.24,0.83,0.04C16.04,15.66,16.08,15.26,15.81,14.99C15.85,15.03,15.81,15.03,15.81,14.99z' fill='%236bb324'/%3E%3C/svg%3E") !important;
+}
+.alert-info {
+ color: #00a0de !important;
+}
+
+.alert-info > button.close::before {
+ background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M15.81,14.99l-6.99-7l6.99-7c0.24-0.24,0.2-0.63-0.04-0.83c-0.24-0.2-0.59-0.2-0.79,0l-6.99,7l-6.99-7 C0.75-0.08,0.36-0.04,0.16,0.2c-0.2,0.24-0.2,0.59,0,0.79l6.99,7l-6.99,7c-0.24,0.24-0.2,0.63,0.04,0.83c0.24,0.2,0.59,0.2,0.79,0 l6.99-7l6.99,7c0.24,0.24,0.59,0.24,0.83,0.04C16.04,15.66,16.08,15.26,15.81,14.99C15.85,15.03,15.81,15.03,15.81,14.99z' fill='%2300a0de'/%3E%3C/svg%3E") !important;
+}
+.alert-warning {
+ color: #87604e !important;
+ border-color: #87604e !important;
+}
+
+.alert-warning > button.close::before {
+ background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M15.81,14.99l-6.99-7l6.99-7c0.24-0.24,0.2-0.63-0.04-0.83c-0.24-0.2-0.59-0.2-0.79,0l-6.99,7l-6.99-7 C0.75-0.08,0.36-0.04,0.16,0.2c-0.2,0.24-0.2,0.59,0,0.79l6.99,7l-6.99,7c-0.24,0.24-0.2,0.63,0.04,0.83c0.24,0.2,0.59,0.2,0.79,0 l6.99-7l6.99,7c0.24,0.24,0.59,0.24,0.83,0.04C16.04,15.66,16.08,15.26,15.81,14.99C15.85,15.03,15.81,15.03,15.81,14.99z' fill='%2387604E'/%3E%3C/svg%3E") !important;
+}
+.alert-danger {
+ color: #d90000 !important;
+}
+
+.alert-danger > button.close::before {
+ background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M15.81,14.99l-6.99-7l6.99-7c0.24-0.24,0.2-0.63-0.04-0.83c-0.24-0.2-0.59-0.2-0.79,0l-6.99,7l-6.99-7 C0.75-0.08,0.36-0.04,0.16,0.2c-0.2,0.24-0.2,0.59,0,0.79l6.99,7l-6.99,7c-0.24,0.24-0.2,0.63,0.04,0.83c0.24,0.2,0.59,0.2,0.79,0 l6.99-7l6.99,7c0.24,0.24,0.59,0.24,0.83,0.04C16.04,15.66,16.08,15.26,15.81,14.99C15.85,15.03,15.81,15.03,15.81,14.99z' fill='%23d90000'/%3E%3C/svg%3E") !important;
+}
+
+.custom-margin {
+ margin-right: 20px;
+}
+
+i.bi {
+ font-size: 22px;
+}
+
+.text-breaking {
+ word-break: break-word;
+}
diff --git a/src/app/modules/alerting/alert.component.html b/src/app/modules/alerting/alert.component.html
new file mode 100644
index 0000000..157966f
--- /dev/null
+++ b/src/app/modules/alerting/alert.component.html
@@ -0,0 +1,106 @@
+<!--
+ ~ Copyright (c) 2022. Deutsche Telekom AG
+ ~
+ ~ 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
+ ~
+ ~ http://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.
+ ~
+ ~ SPDX-License-Identifier: Apache-2.0
+ -->
+
+
+<div class="d-flex justify-content-center">
+ <span>
+ <ngb-alert *ngFor="let alert of alerts" type="alert" class="{{ cssClass(alert) }}" [dismissible]="false">
+ <button
+ type="button"
+ class="close"
+ [attr.aria-label]="'common.buttons.close' | translate"
+ (click)="removeAlert(alert)"
+ ></button>
+ <div class="d-flex text-breaking">
+ <i
+ class="bi custom-margin"
+ [class.bi-info-circle-fill]="informativeAlerts.includes(alert.type)"
+ [class.bi-exclamation-triangle-fill]="!informativeAlerts.includes(alert.type)"
+ aria-hidden="true"
+ ></i>
+
+ <div *ngIf="alert.type === AlertType.Error">
+ <ng-container *ngIf="alert.id === 'keycloak'; else defaultErrorAlert">
+ <span>{{ alert.message }}</span>
+ <ng-container *ngTemplateOutlet="supportTpl"></ng-container>
+ </ng-container>
+ </div>
+
+ <div *ngIf="alert.type !== AlertType.Error">
+ <span class="text-justify">{{ alert.message }}</span>
+ <ng-container *ngIf="alert.id === 'onap_logging'">
+ <span>{{ 'common.alert.contactSupport.part1' | translate }}</span>
+ <a [href]="environment.supportUrlLink">{{ 'common.alert.support' | translate }}</a>
+ </ng-container>
+ </div>
+ </div>
+
+ <ng-template #defaultErrorAlert>
+ <span *ngIf="alert.urlTree">{{ alert.message }}</span>
+ <span *ngIf="!alert.errorDetail">{{ alert.message }}</span>
+ <div *ngIf="alert?.errorDetail?.downstreamSystem as downstreamSystem">
+ <span *ngIf="downstreamSystem">
+ {{ 'common.alert.errorReporter' | translate: { system: 'common.systems.' + downstreamSystem | translate } }}
+ </span>
+ </div>
+ <div *ngIf="alert.errorDetail?.detail as detail">
+ "{{ alert.errorDetail?.detail }}"
+ <div
+ *ngIf="
+ alert.errorDetail?.downstreamSystem === DownstreamSystem.KEYCLOAK &&
+ alert.errorDetail?.downstreamStatus === 409
+ "
+ >
+ <span *ngIf="detail.split(' ').pop() === 'username'">
+ {{ 'common.block.userAdministration.helpUserNameExists' | translate }}
+ </span>
+ <span *ngIf="detail.split(' ').pop() === 'email'">
+ {{ 'common.block.userAdministration.helpUserEmailExists' | translate }}
+ </span>
+ </div>
+ </div>
+ <ng-container *ngTemplateOutlet="supportTpl"></ng-container>
+ </ng-template>
+ <ng-template #supportTpl>
+ <div>
+ {{ 'common.alert.support' | translate }}
+ <button
+ class="btn btn-sm p-0"
+ (click)="collapse.toggle()"
+ [attr.aria-expanded]="!isCollapsed"
+ aria-controls="collapseSupportInfo"
+ >
+ <i *ngIf="isCollapsed" class="bi bi-chevron-right text-danger" style="font-size: 18px" aria-hidden="true" [attr.aria-label]="'common.buttons.openSupportLink' | translate"></i>
+ <i *ngIf="!isCollapsed" class="bi bi-chevron-down text-danger" style="font-size: 18px" aria-hidden="true" [attr.aria-label]="'common.buttons.closeSupportLink' | translate"></i>
+ </button>
+ </div>
+
+ <div #collapse="ngbCollapse" [(ngbCollapse)]="isCollapsed">
+ <span>{{ 'common.alert.contactSupport.part1' | translate }}</span
+ ><a [href]="environment.supportUrlLink" target="_blank">{{ 'common.alert.support' | translate }}</a>
+ <ng-container *ngIf="alert?.requestId">
+ <span>{{ 'common.alert.contactSupport.part2' | translate }}</span>
+ <div>
+ {{ alert?.requestId }}
+ </div>
+ </ng-container>
+ </div>
+ </ng-template>
+ </ngb-alert>
+ </span>
+</div>
diff --git a/src/app/modules/alerting/alert.component.spec.ts b/src/app/modules/alerting/alert.component.spec.ts
new file mode 100644
index 0000000..abaf52e
--- /dev/null
+++ b/src/app/modules/alerting/alert.component.spec.ts
@@ -0,0 +1,49 @@
+/*
+ * Copyright (c) 2022. Deutsche Telekom AG
+ *
+ * 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
+ *
+ * http://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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+
+import { AlertComponent } from './alert.component';
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+import { Router } from '@angular/router';
+
+describe('AlertComponent', () => {
+ let component: AlertComponent;
+ let fixture: ComponentFixture<AlertComponent>;
+ const router = jasmine.createSpyObj('Router', ['navigate']);
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ providers: [AlertComponent, { provide: Router, useValue: router }],
+ }).compileComponents();
+ fixture = TestBed.createComponent(AlertComponent);
+ component = fixture.componentInstance;
+ }));
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('Setting value to id properties', () => {
+ component.id = 'testId';
+ fixture.detectChanges();
+ });
+ it('Setting value to fade properties', () => {
+ expect(component.fade).toBe(true);
+ component.fade = false;
+ fixture.detectChanges();
+ });
+});
diff --git a/src/app/modules/alerting/alert.component.ts b/src/app/modules/alerting/alert.component.ts
new file mode 100644
index 0000000..91d22f4
--- /dev/null
+++ b/src/app/modules/alerting/alert.component.ts
@@ -0,0 +1,138 @@
+/*
+ * Copyright (c) 2022. Deutsche Telekom AG
+ *
+ * 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
+ *
+ * http://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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+
+import { Component, Input, OnInit } from '@angular/core';
+import { NavigationStart, Router } from '@angular/router';
+import { Subscription } from 'rxjs';
+
+import { Alert, AlertType } from './alert.model';
+import { AlertService } from './alert.service';
+import { UnsubscribeService } from 'src/app/services/unsubscribe/unsubscribe.service';
+import { takeUntil } from 'rxjs/operators';
+import { environment } from 'src/environments/environment';
+import { Problem } from '../../../../openapi/output';
+import DownstreamSystemEnum = Problem.DownstreamSystemEnum;
+
+@Component({
+ selector: 'app-alert',
+ templateUrl: 'alert.component.html',
+ styleUrls: ['alert.component.css'],
+ providers: [UnsubscribeService],
+})
+export class AlertComponent implements OnInit {
+ @Input() id = 'default-alert';
+ @Input() fade = true;
+
+ isCollapsed = true;
+ informativeAlerts: AlertType[] = [AlertType.Success, AlertType.Info];
+ alerts: Alert[] = [];
+ alertSubscription!: Subscription;
+ routeSubscription!: Subscription;
+ AlertType = AlertType;
+ environment = environment;
+ DownstreamSystem = DownstreamSystemEnum;
+ constructor(
+ private router: Router,
+ private alertService: AlertService,
+ private unsubscribeService: UnsubscribeService,
+ ) {}
+
+ ngOnInit() {
+ // subscribe to new alert notifications
+ this.alertSubscription = this.alertService.alerts
+ .pipe(takeUntil(this.unsubscribeService.unsubscribe$))
+ .subscribe(alert => {
+ // clear alerts when an empty alert is received
+ if (!alert.message) {
+ // filter out alerts without 'keepAfterRouteChange' flag
+ this.alerts = this.alerts.filter(x => x.keepAfterRouteChange);
+
+ // remove 'keepAfterRouteChange' flag on the rest
+ this.alerts.forEach(x => delete x.keepAfterRouteChange);
+ return;
+ }
+ if (this.alerts.filter(a => a.message === alert.message).length === 0) {
+ // add alert to array
+ this.alerts.push(alert);
+ }
+ // auto close alert if required
+ if (alert.type === AlertType.Warning) {
+ setTimeout(() => this.removeAlert(alert), 10000);
+ }
+ });
+
+ // clear alerts on location change
+ this.routeSubscription = this.router.events
+ .pipe(takeUntil(this.unsubscribeService.unsubscribe$))
+ .subscribe(event => {
+ if (event instanceof NavigationStart) {
+ this.alertService.clear(this.id);
+ }
+ });
+ }
+
+ removeAlert(alert: Alert) {
+ // check if already removed to prevent error on auto close
+ if (!this.alerts.includes(alert)) {
+ return;
+ }
+
+ if (this.fade) {
+ // fade out alert
+ this.alerts.find(x => x === alert)!.fade = true;
+
+ // remove alert after faded out
+ setTimeout(() => {
+ this.alerts = this.alerts.filter(x => x !== alert);
+ }, 250);
+ } else {
+ // remove alert
+ this.alerts = this.alerts.filter(x => x !== alert);
+ }
+ }
+
+ cssClass(alert: Alert) {
+ if (!alert) {
+ return;
+ }
+
+ const classes = ['show', 'alert', 'alert-dismissable'];
+
+ const alertTypeClass = {
+ /*
+ [AlertType.Success]: 'alert alert-success',
+ [AlertType.Error]: 'alert alert-danger',
+ [AlertType.Info]: 'alert alert-info',
+ [AlertType.Warning]: 'alert alert-warning'
+ */
+ [AlertType.Success]: 'alert-success',
+ [AlertType.Error]: 'alert-danger',
+ [AlertType.Info]: 'alert-info',
+ [AlertType.Warning]: 'alert-warning',
+ };
+
+ classes.push(alertTypeClass[alert.type]);
+
+ if (alert.fade) {
+ classes.push('fade');
+ }
+
+ return classes.join(' ');
+ }
+}
diff --git a/src/app/modules/alerting/alert.model.ts b/src/app/modules/alerting/alert.model.ts
new file mode 100644
index 0000000..6e280ce
--- /dev/null
+++ b/src/app/modules/alerting/alert.model.ts
@@ -0,0 +1,47 @@
+/*
+ * Copyright (c) 2022. Deutsche Telekom AG
+ *
+ * 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
+ *
+ * http://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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+
+import { Inject, Injectable } from '@angular/core';
+import { Problem } from '../../../../openapi/output';
+
+@Injectable({ providedIn: 'root' })
+export class Alert {
+ id?: string;
+ type!: AlertType;
+ message?: string;
+ autoClose?: boolean;
+ keepAfterRouteChange?: boolean;
+ fade?: boolean;
+ errorDetail?: Problem;
+ requestId?: string;
+ urlTree?: string[]
+
+ constructor(@Inject(Alert) init?: Partial<Alert>) {
+ Object.assign(this, init);
+ }
+}
+
+
+
+export enum AlertType {
+ Success,
+ Error,
+ Info,
+ Warning,
+}
diff --git a/src/app/modules/alerting/alert.module.ts b/src/app/modules/alerting/alert.module.ts
new file mode 100644
index 0000000..064bb32
--- /dev/null
+++ b/src/app/modules/alerting/alert.module.ts
@@ -0,0 +1,32 @@
+/*
+ * Copyright (c) 2022. Deutsche Telekom AG
+ *
+ * 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
+ *
+ * http://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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+
+import { NgModule } from '@angular/core';
+import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
+import { CommonModule } from '@angular/common';
+
+import { AlertComponent } from './alert.component';
+import { TranslateModule } from '@ngx-translate/core';
+
+@NgModule({
+ imports: [CommonModule, NgbModule, TranslateModule],
+ declarations: [AlertComponent],
+ exports: [AlertComponent],
+})
+export class AlertModule {}
diff --git a/src/app/modules/alerting/alert.service.spec.ts b/src/app/modules/alerting/alert.service.spec.ts
new file mode 100644
index 0000000..5c9d219
--- /dev/null
+++ b/src/app/modules/alerting/alert.service.spec.ts
@@ -0,0 +1,93 @@
+/*
+ * Copyright (c) 2022. Deutsche Telekom AG
+ *
+ * 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
+ *
+ * http://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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+
+// https://dev.to/coly010/unit-testing-angular-services-1anm
+import { Alert, AlertType } from './alert.model';
+import { TestBed } from '@angular/core/testing';
+import { AlertModule } from './alert.module';
+import { AlertService } from './alert.service';
+import { Subject } from 'rxjs';
+import SpyObj = jasmine.SpyObj;
+
+/**
+ * describe sets up the Test Suite for the TileService
+ */
+describe('AlertService', () => {
+ let service: AlertService;
+ let mockAlert: Alert;
+ let message: string;
+ let spyAlert: SpyObj<any>;
+ let subject: Subject<Alert>;
+
+ /**
+ * beforeEach tells the test runner to run this code before every test in the Test Suite
+ * It is using Angular's TestBed to create the testing environment and finally it is injecting the TilesService
+ * and placing a reference to it in the service variable defined earlier.
+ */
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ providers: [AlertService, AlertModule, Subject],
+ });
+ service = TestBed.inject(AlertService);
+ subject = TestBed.inject(Subject);
+ mockAlert = TestBed.inject(Alert);
+ spyAlert = spyOn(service, 'alert');
+ message = 'This is a test-alert';
+ mockAlert.message = message;
+ });
+
+ it('should be create', () => {
+ expect(service).toBeTruthy();
+ });
+ /**
+ * tests for the alert methods info, warning, error and success with a spyobject
+ */
+ it('should return success alert', () => {
+ mockAlert.type = AlertType.Success;
+ service.success(message);
+ expect(spyAlert).toHaveBeenCalledWith(mockAlert);
+ });
+
+ it('should return warning alert', () => {
+ mockAlert.type = AlertType.Warning;
+ service.warn(message);
+ expect(spyAlert).toHaveBeenCalledWith(mockAlert);
+ });
+
+ it('should return error alert', () => {
+ mockAlert.type = AlertType.Error;
+ service.error(message);
+ expect(spyAlert).toHaveBeenCalledWith(mockAlert);
+ });
+
+ it('should return info alert', () => {
+ mockAlert.type = AlertType.Info;
+ service.info(message);
+ expect(spyAlert).toHaveBeenCalledWith(mockAlert);
+ });
+
+ it('clear ', () => {
+ subject = service['subject'];
+ const spy = spyOn(subject, 'next');
+ const alert = new Alert();
+ alert.id = 'default-alert';
+ service.clear();
+ expect(spy).toHaveBeenCalledWith(alert);
+ });
+});
diff --git a/src/app/modules/alerting/alert.service.ts b/src/app/modules/alerting/alert.service.ts
new file mode 100644
index 0000000..4d81397
--- /dev/null
+++ b/src/app/modules/alerting/alert.service.ts
@@ -0,0 +1,66 @@
+/*
+ * Copyright (c) 2022. Deutsche Telekom AG
+ *
+ * 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
+ *
+ * http://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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+
+import { Injectable } from '@angular/core';
+import { Observable, Subject } from 'rxjs';
+import { filter } from 'rxjs/operators';
+
+import { Alert, AlertType } from './alert.model';
+
+@Injectable({ providedIn: 'root' })
+export class AlertService {
+ private subject = new Subject<Alert>();
+ private defaultId = 'default-alert';
+
+ // enable subscribing to alerts observable
+ onAlert(id = this.defaultId): Observable<Alert> {
+ return this.subject.asObservable().pipe(filter(x => x && x.id === id));
+ }
+ get alerts() {
+ return this.subject;
+ }
+
+ // convenience methods
+ success(message: string, options?: Partial<Alert>) {
+ this.alert(new Alert({ ...options, type: AlertType.Success, message }));
+ }
+
+ error(message: string, options?: Partial<Alert>) {
+ this.alert(new Alert({ ...options, type: AlertType.Error, message }));
+ }
+
+ info(message: string, options?: Partial<Alert>) {
+ this.alert(new Alert({ ...options, type: AlertType.Info, message }));
+ }
+
+ warn(message: string, options?: Partial<Alert>) {
+ this.alert(new Alert({ ...options, type: AlertType.Warning, message }));
+ }
+
+ // main alert method
+ alert(alert: Alert) {
+ alert.id = alert.id || this.defaultId;
+ this.subject.next(alert);
+ }
+
+ // clear alerts
+ clear(id = this.defaultId) {
+ this.subject.next(new Alert({ id }));
+ }
+}
diff --git a/src/app/modules/alerting/index.ts b/src/app/modules/alerting/index.ts
new file mode 100644
index 0000000..492986c
--- /dev/null
+++ b/src/app/modules/alerting/index.ts
@@ -0,0 +1,22 @@
+/*
+ * Copyright (c) 2022. Deutsche Telekom AG
+ *
+ * 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
+ *
+ * http://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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+
+export * from './alert.module';
+export * from './alert.service';
+export * from './alert.model';
diff --git a/src/app/modules/app-starter/app-starter-routing.module.ts b/src/app/modules/app-starter/app-starter-routing.module.ts
new file mode 100644
index 0000000..6696d3a
--- /dev/null
+++ b/src/app/modules/app-starter/app-starter-routing.module.ts
@@ -0,0 +1,31 @@
+/*
+ * Copyright (c) 2022. Deutsche Telekom AG
+ *
+ * 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
+ *
+ * http://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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+
+import { NgModule } from '@angular/core';
+import { RouterModule, Routes } from '@angular/router';
+import { AppStarterComponent } from './app-starter.component';
+import { AuthGuard } from '../../guards/auth.guard';
+
+const routes: Routes = [{ path: '', component: AppStarterComponent, canActivate: [AuthGuard] }];
+
+@NgModule({
+ imports: [RouterModule.forChild(routes)],
+ exports: [RouterModule],
+})
+export class AppStarterRoutingModule {}
diff --git a/src/app/modules/app-starter/app-starter.component.css b/src/app/modules/app-starter/app-starter.component.css
new file mode 100644
index 0000000..8ec276c
--- /dev/null
+++ b/src/app/modules/app-starter/app-starter.component.css
@@ -0,0 +1,122 @@
+/*
+ * Copyright (c) 2022. Deutsche Telekom AG
+ *
+ * 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
+ *
+ * http://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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+
+.card-img-top {
+ width: 60%;
+ height: 5vw;
+ object-fit: contain;
+}
+
+.card-deck > div {
+ display: flex;
+ flex: 1 0 0;
+ flex-direction: column;
+}
+
+.card-deck > div:not(:last-child) {
+ margin-right: 15px;
+}
+
+.card-deck {
+ width: 90%;
+ margin-left: 2%;
+}
+
+.card-deck > div:not(:first-child) {
+ margin-left: 15px;
+}
+
+.my-group-title {
+ color: var(--primary);
+}
+
+.card {
+ border-radius: 20px;
+ cursor: pointer;
+ transition: 0.4s;
+ min-width: 200px;
+ max-width: 200px;
+ min-height: 250px;
+ max-height: 250px;
+ text-align: center;
+ margin-right: 2.25rem;
+}
+
+.card-body {
+ padding-bottom: 0;
+}
+
+.card-title {
+ min-height: 87px;
+ font-size: 14px;
+}
+
+/* Works together with bootstraps responsive image class
+https://stackoverflow.com/questions/53721711/how-to-set-responsive-images-max-width-bootstrap-4#53723494
+*/
+.img-max {
+ max-width: 115px;
+ width: 100%;
+}
+
+.card:hover {
+ transform: scale(1.1, 1.1);
+ box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);
+}
+
+.disabled-card:hover {
+ transform: none !important;
+ box-shadow: none !important;
+ transition: none !important;
+}
+
+a,
+a:hover {
+ color: #262626;
+ text-decoration: none;
+}
+
+a:hover {
+ cursor: pointer;
+}
+
+.nav-tabs,
+.nav-links {
+ border-bottom: 1px solid #b2b2b2;
+}
+
+.nav-link {
+ background-color: transparent;
+}
+.nav-tabs .nav-link.active,
+.nav-tabs .nav-item.show .nav-link {
+ color: var(--primary);
+ background-color: #fff;
+ border-color: #b2b2b2 #b2b2b2 #fff;
+}
+
+.nav-link:focus {
+ border-style: none;
+}
+
+/* I will leave this for future purpose in case we will have disabled tiles in the Portal */
+/* .disabled-tiles {
+ opacity: 0.5;
+ cursor: not-allowed !important;
+} */
diff --git a/src/app/modules/app-starter/app-starter.component.html b/src/app/modules/app-starter/app-starter.component.html
new file mode 100644
index 0000000..aae2bf3
--- /dev/null
+++ b/src/app/modules/app-starter/app-starter.component.html
@@ -0,0 +1,47 @@
+<!--
+ ~ Copyright (c) 2022. Deutsche Telekom AG
+ ~
+ ~ 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
+ ~
+ ~ http://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.
+ ~
+ ~ SPDX-License-Identifier: Apache-2.0
+ -->
+
+
+<app-breadcrumb>
+ <app-breadcrumb-item>
+ <a [routerLink]="['/dashboard']">{{ 'layout.menu.items.home' | translate }}</a>
+ </app-breadcrumb-item>
+ <app-breadcrumb-item>
+ <span aria-current="page">{{ 'appStarter.title' | translate }}</span>
+ </app-breadcrumb-item>
+</app-breadcrumb>
+<h2>{{ 'appStarter.title' | translate }}</h2>
+<hr />
+<div class="d-flex flex-wrap cards my-5">
+ <ng-container *ngIf="tiles$ | async as tiles">
+ <div class="card mb-5 qa_tiles_wrapper" *ngFor="let tile of tiles" [ngbTooltip]="'appStarter.tiles.tooltips.enum.' + tile.id | translate">
+ <a class="card-block stretched-link text-decoration-none my-3 qa_tiles_not_disabled" [href]="tile.redirectUrl" target="_blank">
+ <img
+ src="assets/images/tiles/{{ tile.imageUrl }}"
+ class="img-fluid img-max rounded my-2 qa_tiles_not_disabled_img"
+ alt="{{ tile.imageAltText }}"
+ />
+ <div class="card-body qa_tiles_not_disabled_body">
+ <p class="card-title qa_tiles_not_disabled_title">{{ tile.title }}</p>
+ </div>
+ </a>
+ </div>
+ </ng-container>
+</div>
+
+
diff --git a/src/app/modules/app-starter/app-starter.component.ts b/src/app/modules/app-starter/app-starter.component.ts
new file mode 100644
index 0000000..c08467f
--- /dev/null
+++ b/src/app/modules/app-starter/app-starter.component.ts
@@ -0,0 +1,45 @@
+/*
+ * Copyright (c) 2022. Deutsche Telekom AG
+ *
+ * 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
+ *
+ * http://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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+
+import { Component, OnInit } from '@angular/core';
+import { map } from 'rxjs/operators';
+import { environment } from 'src/environments/environment';
+import { from, Observable, of } from 'rxjs';
+import { Tile } from 'src/app/model/tile';
+
+@Component({
+ selector: 'app-app-starter',
+ templateUrl: './app-starter.component.html',
+ styleUrls: ['./app-starter.component.css'],
+})
+export class AppStarterComponent implements OnInit {
+ //I will leave this for future purpose in case we will have disabled tiles in the Portal
+ // disabledTiles:number[] = [11,12,13]
+
+ private readonly hostname = environment.hostname.replace('portal-ui-', '');
+
+ public readonly tiles$: Observable<Tile[]> = from(fetch('/assets/tiles/tiles.json?t=' + Date.now()).then(rsp => rsp.json()))
+ .pipe(
+ map(tiles => (tiles.items as Tile[])),
+ map(tiles => tiles.map(tile => ({ ...tile, redirectUrl: tile.redirectUrl.replace(/HOSTNAME/i, this.hostname) }))),
+ );
+
+
+ ngOnInit(): void {}
+}
diff --git a/src/app/modules/app-starter/app-starter.module.ts b/src/app/modules/app-starter/app-starter.module.ts
new file mode 100644
index 0000000..ebbd0ce
--- /dev/null
+++ b/src/app/modules/app-starter/app-starter.module.ts
@@ -0,0 +1,29 @@
+/*
+ * Copyright (c) 2022. Deutsche Telekom AG
+ *
+ * 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
+ *
+ * http://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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+
+import { NgModule } from '@angular/core';
+import { AppStarterComponent } from './app-starter.component';
+import { AppStarterRoutingModule } from './app-starter-routing.module';
+import { SharedModule } from '../../shared.module';
+
+@NgModule({
+ declarations: [AppStarterComponent],
+ imports: [AppStarterRoutingModule, SharedModule],
+})
+export class AppStarterModule {}
diff --git a/src/app/modules/auth/auth.config.module.ts b/src/app/modules/auth/auth.config.module.ts
new file mode 100644
index 0000000..7f70ba6
--- /dev/null
+++ b/src/app/modules/auth/auth.config.module.ts
@@ -0,0 +1,46 @@
+/*
+ * Copyright (c) 2022. Deutsche Telekom AG
+ *
+ * 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
+ *
+ * http://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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+
+import { APP_INITIALIZER, NgModule } from '@angular/core';
+import { AuthConfig } from 'angular-oauth2-oidc';
+
+import { authConfig } from './auth.config';
+import { HTTP_INTERCEPTORS } from '@angular/common/http';
+import { AuthInterceptor } from '../../http-interceptors/auth.interceptor';
+import { AuthConfigService } from '../../services/authconfig.service';
+
+export function init_app(authConfigService: AuthConfigService) {
+ return () => authConfigService.initAuth();
+}
+
+@NgModule({
+ providers: [
+ { provide: AuthConfig, useValue: authConfig },
+ AuthConfigService,
+ {
+ provide: APP_INITIALIZER,
+ useFactory: init_app,
+ deps: [AuthConfigService],
+ multi: true,
+ },
+ { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true },
+ ],
+ declarations: [],
+})
+export class AuthConfigModule {}
diff --git a/src/app/modules/auth/auth.config.ts b/src/app/modules/auth/auth.config.ts
new file mode 100644
index 0000000..3414edd
--- /dev/null
+++ b/src/app/modules/auth/auth.config.ts
@@ -0,0 +1,48 @@
+/*
+ * Copyright (c) 2022. Deutsche Telekom AG
+ *
+ * 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
+ *
+ * http://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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+
+import { AuthConfig } from 'angular-oauth2-oidc';
+import { environment } from '../../../environments/environment';
+
+export const authConfig: AuthConfig = {
+ // Url of the Identity Provider
+ issuer: environment.keycloak.issuer,
+
+ // URL of the SPA to redirect the user to after login
+ redirectUri: environment.keycloak.redirectUri,
+
+ // The SPA's id.
+ // The SPA is registerd with this id at the auth-serverß
+ clientId: environment.keycloak.clientId,
+
+ responseType: environment.keycloak.responseType,
+ // set the scope for the permissions the client should request
+ // The first three are defined by OIDC.
+ scope: environment.keycloak.scope,
+ // Remove the requirement of using Https to simplify the demo
+ // THIS SHOULD NOT BE USED IN PRODUCTION
+ // USE A CERTIFICATE FOR YOUR IDP
+ // IN PRODUCTION
+ requireHttps: environment.keycloak.requireHttps,
+ // at_hash is not present in JWT token
+ showDebugInformation: environment.keycloak.showDebugInformation,
+ disableAtHashCheck: environment.keycloak.disableAtHashCheck,
+ skipIssuerCheck: environment.keycloak.skipIssuerCheck,
+ strictDiscoveryDocumentValidation: environment.keycloak.strictDiscoveryDocumentValidation,
+};
diff --git a/src/app/modules/auth/injection-tokens.ts b/src/app/modules/auth/injection-tokens.ts
new file mode 100644
index 0000000..140b83c
--- /dev/null
+++ b/src/app/modules/auth/injection-tokens.ts
@@ -0,0 +1,26 @@
+/*
+ * Copyright (c) 2022. Deutsche Telekom AG
+ *
+ * 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
+ *
+ * http://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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+
+import { InjectionToken } from '@angular/core';
+
+export interface AclConfig {
+ [key: string]: string[];
+}
+
+export const ACL_CONFIG = new InjectionToken<AclConfig>('ACL_CONFIG');
diff --git a/src/app/modules/dashboard/apps/user-last-action-tile/action-button/action-button.component.css b/src/app/modules/dashboard/apps/user-last-action-tile/action-button/action-button.component.css
new file mode 100644
index 0000000..b7b5110
--- /dev/null
+++ b/src/app/modules/dashboard/apps/user-last-action-tile/action-button/action-button.component.css
@@ -0,0 +1,19 @@
+/*
+ * Copyright (c) 2022. Deutsche Telekom AG
+ *
+ * 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
+ *
+ * http://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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+
diff --git a/src/app/modules/dashboard/apps/user-last-action-tile/action-button/action-button.component.html b/src/app/modules/dashboard/apps/user-last-action-tile/action-button/action-button.component.html
new file mode 100644
index 0000000..2c35f19
--- /dev/null
+++ b/src/app/modules/dashboard/apps/user-last-action-tile/action-button/action-button.component.html
@@ -0,0 +1,63 @@
+<!--
+ ~ Copyright (c) 2022. Deutsche Telekom AG
+ ~
+ ~ 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
+ ~
+ ~ http://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.
+ ~
+ ~ SPDX-License-Identifier: Apache-2.0
+ -->
+
+
+<ng-container *ngIf="action">
+ <button
+ *ngIf="action.type | in: REPEAT_ACTIONS"
+ class="btn btn-invisible p-0 qa_repeat_action"
+ (click)="onButtonClick(action)"
+ [attr.aria-label]="'dashboard.apps.userLastAction.tooltip.repeatAction' | translate"
+ [ngbTooltip]="repeatActionTooltipContent"
+ container="body"
+ >
+ <i class="bi bi-arrow-clockwise pointer" aria-hidden="true"></i>
+ </button>
+ <button
+ *ngIf="action.type | in: VIEW_ACTIONS"
+ class="btn btn-invisible p-0 qa_view_action"
+ (click)="onButtonClick(action)"
+ [attr.aria-label]="'dashboard.apps.userLastAction.tooltip.viewAction' | translate"
+ [ngbTooltip]="viewActionTooltipContent"
+ container="body"
+ >
+ <i class="bi bi-eyeglasses" aria-hidden="true"></i>
+ </button>
+
+ <ng-template #repeatActionTooltipContent>
+ <span
+ >{{ 'dashboard.apps.userLastAction.actionType.' + action.type | translate }}
+ {{ 'dashboard.apps.userLastAction.tooltip.again' | translate }}
+ {{ 'dashboard.apps.userLastAction.entityType.' + action.entity | translate }}
+ {{ message }}</span
+ >
+ </ng-template>
+ <ng-template #viewActionTooltipContent>
+ <span
+ >{{ 'dashboard.apps.userLastAction.actionType.' + ActionType.VIEW | translate }}
+ <span *ngIf="action.type === ActionType.DEPLOY">
+ {{ 'dashboard.apps.userLastAction.tooltip.statusOf' | translate }}
+ </span>
+ {{ 'dashboard.apps.userLastAction.entityType.' + action.entity | translate }}
+ <span *ngIf="action.type === ActionType.DEPLOY">
+ {{ 'dashboard.apps.userLastAction.tooltip.deployment' | translate }}
+ </span>
+ {{ message }}</span
+ >
+ </ng-template>
+</ng-container>
diff --git a/src/app/modules/dashboard/apps/user-last-action-tile/action-button/action-button.component.ts b/src/app/modules/dashboard/apps/user-last-action-tile/action-button/action-button.component.ts
new file mode 100644
index 0000000..b30a35e
--- /dev/null
+++ b/src/app/modules/dashboard/apps/user-last-action-tile/action-button/action-button.component.ts
@@ -0,0 +1,40 @@
+/*
+ * Copyright (c) 2022. Deutsche Telekom AG
+ *
+ * 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
+ *
+ * http://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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+
+import { Component, EventEmitter, Input, Output } from '@angular/core';
+import { ActionModel, ActionRowModel, ActionType, EntityTypeModel } from '../../../../../model/user-last-action.model';
+
+@Component({
+ selector: 'app-action-button',
+ templateUrl: './action-button.component.html',
+ styleUrls: ['./action-button.component.css'],
+})
+export class ActionButtonComponent {
+ public readonly VIEW_ACTIONS = [ActionType.ACK, ActionType.UNACK, ActionType.CLEAR, ActionType.DEPLOY];
+ public readonly REPEAT_ACTIONS = [ActionType.SEARCH, ActionType.VIEW, ActionType.EDIT];
+ ActionType = ActionType;
+
+ @Input() action: ActionRowModel<EntityTypeModel> | undefined;
+ @Input() message: string | undefined;
+ @Output() btnClick = new EventEmitter<ActionModel>();
+
+ public onButtonClick(action: ActionModel): void {
+ this.btnClick.emit(action);
+ }
+}
diff --git a/src/app/modules/dashboard/apps/user-last-action-tile/action-row/action-row.component.css b/src/app/modules/dashboard/apps/user-last-action-tile/action-row/action-row.component.css
new file mode 100644
index 0000000..c6f52a8
--- /dev/null
+++ b/src/app/modules/dashboard/apps/user-last-action-tile/action-row/action-row.component.css
@@ -0,0 +1,23 @@
+/*
+ * Copyright (c) 2022. Deutsche Telekom AG
+ *
+ * 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
+ *
+ * http://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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+
+img {
+ height: 20px;
+ width: 20px;
+}
diff --git a/src/app/modules/dashboard/apps/user-last-action-tile/action-row/action-row.component.html b/src/app/modules/dashboard/apps/user-last-action-tile/action-row/action-row.component.html
new file mode 100644
index 0000000..62cf722
--- /dev/null
+++ b/src/app/modules/dashboard/apps/user-last-action-tile/action-row/action-row.component.html
@@ -0,0 +1,105 @@
+<!--
+ ~ Copyright (c) 2022. Deutsche Telekom AG
+ ~
+ ~ 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
+ ~
+ ~ http://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.
+ ~
+ ~ SPDX-License-Identifier: Apache-2.0
+ -->
+
+
+<ng-container *ngIf="action">
+ <div class="row py-2 border-bottom">
+ <div class="col-3">
+ <div class="d-flex justify-content-between">
+ <span
+ class="qa_action_created_at"
+ container="body"
+ [ngbTooltip]="action.actionCreatedAt | date: FULL_DATE_FORMAT"
+ >{{
+ (action.actionCreatedAt | isToday)
+ ? (action.actionCreatedAt | date: TIME_FORMAT)
+ : (action.actionCreatedAt | date: DATE_FORMAT)
+ }}</span
+ >
+ <ng-container [ngSwitch]="action.type">
+ <ng-template [ngSwitchCase]="ActionType.CREATE">
+ <img
+ class="qa_create_icon"
+ src='../../../../../../assets/images/icons/install_graphical.svg'
+ [attr.alt]="'dashboard.apps.userLastAction.actionType.CREATE' | translate"
+ />
+ </ng-template>
+ <ng-template [ngSwitchCase]="ActionType.DELETE">
+ <img
+ class="qa_delete_icon"
+ src='../../../../../../assets/images/icons/eraser_graphical.svg'
+ [attr.alt]="'dashboard.apps.userLastAction.actionType.DELETE' | translate"
+ />
+ </ng-template>
+ <ng-template [ngSwitchCase]="ActionType.SEARCH">
+ <img
+ class="qa_search_icon"
+ src='../../../../../../assets/images/icons/search_graphical.svg'
+ [attr.alt]="'dashboard.apps.userLastAction.actionType.SEARCH' | translate"
+ />
+ </ng-template>
+ <ng-template [ngSwitchCase]="ActionType.VIEW">
+ <img
+ class="qa_view_icon"
+ src='../../../../../../assets/images/icons/visible_graphical.svg'
+ [attr.alt]="'dashboard.apps.userLastAction.actionType.VIEW' | translate"
+ />
+ </ng-template>
+ <ng-template [ngSwitchCase]="ActionType.EDIT">
+ <img
+ class="qa_edit_icon"
+ src='../../../../../../assets/images/icons/edit_graphical.svg'
+ [attr.alt]="'dashboard.apps.userLastAction.actionType.EDIT' | translate"
+ />
+ </ng-template>
+ <ng-template [ngSwitchCase]="ActionType.CLEAR">
+ <img
+ class="qa_clear_icon"
+ src='../../../../../../assets/images/icons/brush_graphical.svg'
+ [attr.alt]="'dashboard.apps.userLastAction.actionType.CLEAR' | translate"
+ />
+ </ng-template>
+ <ng-template [ngSwitchCase]="ActionType.ACK">
+ <img
+ class="qa_ack_icon"
+ src='../../../../../../assets/images/icons/thumbs-up_graphical.svg'
+ [attr.alt]="'dashboard.apps.userLastAction.actionType.ACK' | translate"
+ />
+ </ng-template>
+ <ng-template [ngSwitchCase]="ActionType.UNACK">
+ <img
+ class="qa_unack_icon"
+ src='../../../../../../assets/images/icons/thumbs-down_graphical.svg'
+ [attr.alt]="'dashboard.apps.userLastAction.actionType.UNACK' | translate"
+ />
+ </ng-template>
+ <ng-template [ngSwitchCase]="ActionType.DEPLOY">
+ <img
+ class="qa_deployment_icon"
+ src='../../../../../../assets/images/icons/crane_graphical.svg'
+ [attr.alt]="'dashboard.apps.userLastAction.actionType.DEPLOY' | translate"
+ />
+ </ng-template>
+ </ng-container>
+ </div>
+ </div>
+ <div class="col-9">
+ <ng-content></ng-content>
+ </div>
+ </div>
+</ng-container>
diff --git a/src/app/modules/dashboard/apps/user-last-action-tile/action-row/action-row.component.ts b/src/app/modules/dashboard/apps/user-last-action-tile/action-row/action-row.component.ts
new file mode 100644
index 0000000..89e7950
--- /dev/null
+++ b/src/app/modules/dashboard/apps/user-last-action-tile/action-row/action-row.component.ts
@@ -0,0 +1,35 @@
+/*
+ * Copyright (c) 2022. Deutsche Telekom AG
+ *
+ * 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
+ *
+ * http://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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+
+import { Component, Input } from '@angular/core';
+import { ActionModel, ActionType, EntityType } from '../../../../../model/user-last-action.model';
+
+@Component({
+ selector: 'app-action-row',
+ templateUrl: './action-row.component.html',
+ styleUrls: ['./action-row.component.css'],
+})
+export class ActionRowComponent {
+ readonly FULL_DATE_FORMAT = 'E, d MMM Y HH:mm';
+ readonly TIME_FORMAT = 'HH:mm';
+ readonly DATE_FORMAT = 'd MMM';
+ ActionType = ActionType;
+ EntityType = EntityType;
+ @Input() action: ActionModel | undefined;
+}
diff --git a/src/app/modules/dashboard/apps/user-last-action-tile/entity-user-administration-row/entity-user-administration-row.component.css b/src/app/modules/dashboard/apps/user-last-action-tile/entity-user-administration-row/entity-user-administration-row.component.css
new file mode 100644
index 0000000..b7b5110
--- /dev/null
+++ b/src/app/modules/dashboard/apps/user-last-action-tile/entity-user-administration-row/entity-user-administration-row.component.css
@@ -0,0 +1,19 @@
+/*
+ * Copyright (c) 2022. Deutsche Telekom AG
+ *
+ * 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
+ *
+ * http://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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+
diff --git a/src/app/modules/dashboard/apps/user-last-action-tile/entity-user-administration-row/entity-user-administration-row.component.html b/src/app/modules/dashboard/apps/user-last-action-tile/entity-user-administration-row/entity-user-administration-row.component.html
new file mode 100644
index 0000000..54f71c6
--- /dev/null
+++ b/src/app/modules/dashboard/apps/user-last-action-tile/entity-user-administration-row/entity-user-administration-row.component.html
@@ -0,0 +1,33 @@
+<!--
+ ~ Copyright (c) 2022. Deutsche Telekom AG
+ ~
+ ~ 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
+ ~
+ ~ http://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.
+ ~
+ ~ SPDX-License-Identifier: Apache-2.0
+ -->
+
+
+<div class="d-flex justify-content-between" *ngIf='action'>
+ <span class="d-inline-block text-truncate w-100"
+ >{{ 'dashboard.apps.userLastAction.actionType.' + action.type | translate }}
+ {{ 'dashboard.apps.userLastAction.entityType.' + action.entity | translate | colon }}
+ {{ action.entityParams.userName }}
+ </span>
+
+ <app-action-button
+ *ngIf="action.type === ActionType.EDIT"
+ [message]="action.entityParams.userName"
+ [action]="action"
+ (btnClick)="repeatAction(action)"
+ ></app-action-button>
+</div>
diff --git a/src/app/modules/dashboard/apps/user-last-action-tile/entity-user-administration-row/entity-user-administration-row.component.ts b/src/app/modules/dashboard/apps/user-last-action-tile/entity-user-administration-row/entity-user-administration-row.component.ts
new file mode 100644
index 0000000..b55ad17
--- /dev/null
+++ b/src/app/modules/dashboard/apps/user-last-action-tile/entity-user-administration-row/entity-user-administration-row.component.ts
@@ -0,0 +1,39 @@
+/*
+ * Copyright (c) 2022. Deutsche Telekom AG
+ *
+ * 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
+ *
+ * http://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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+
+import { Component, Input } from '@angular/core';
+import { ActionRowModel, ActionType, EntityUserHistoryActionModel } from '../../../../../model/user-last-action.model';
+import { Router } from '@angular/router';
+
+@Component({
+ selector: 'app-entity-user-administration-row',
+ templateUrl: './entity-user-administration-row.component.html',
+ styleUrls: ['./entity-user-administration-row.component.css'],
+})
+export class EntityUserAdministrationRowComponent {
+ ActionType = ActionType;
+ @Input()
+ action: ActionRowModel<EntityUserHistoryActionModel> | undefined;
+
+ constructor(private router: Router) {}
+
+ public repeatAction(action: ActionRowModel<EntityUserHistoryActionModel>): void {
+ this.router.navigate(['user-administration', action.entityParams.userId, 'edit']);
+ }
+}
diff --git a/src/app/modules/dashboard/apps/user-last-action-tile/user-last-action-tile.component.css b/src/app/modules/dashboard/apps/user-last-action-tile/user-last-action-tile.component.css
new file mode 100644
index 0000000..60842fa
--- /dev/null
+++ b/src/app/modules/dashboard/apps/user-last-action-tile/user-last-action-tile.component.css
@@ -0,0 +1,27 @@
+/*
+ * Copyright (c) 2022. Deutsche Telekom AG
+ *
+ * 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
+ *
+ * http://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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+
+img {
+ height: 20px;
+ width: 20px;
+}
+
+.bg-color-inherit {
+ background-color: inherit;
+}
diff --git a/src/app/modules/dashboard/apps/user-last-action-tile/user-last-action-tile.component.html b/src/app/modules/dashboard/apps/user-last-action-tile/user-last-action-tile.component.html
new file mode 100644
index 0000000..d696728
--- /dev/null
+++ b/src/app/modules/dashboard/apps/user-last-action-tile/user-last-action-tile.component.html
@@ -0,0 +1,85 @@
+<!--
+ ~ Copyright (c) 2022. Deutsche Telekom AG
+ ~
+ ~ 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
+ ~
+ ~ http://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.
+ ~
+ ~ SPDX-License-Identifier: Apache-2.0
+ -->
+
+<ng-template #template>
+ <ng-container *ngIf="actions$ | async as actions">
+ <div class="col-xl-4 col-lg-6 col-sm-12 my-2 qa_USER_LAST_ACTION_TILE" cdkDrag>
+ <div class="shadow card" style="height: 334.867px">
+ <div class="card-header pl-3">
+ <div class="d-flex" *ngIf="actionFilterType$ | async as selectedFilter">
+ <div class="d-flex align-items-center">
+ <i
+ class="bi bi-arrows-move text-primary draggable text-primary pr-2"
+ cdkDragHandle
+ aria-hidden="true"
+ ></i>
+ <label class="d-none" for="filterSelect"
+ >{{ 'dashboard.apps.userLastAction.filter.label' | translate
+ }}{{ 'dashboard.apps.userLastAction.filter.type.' + selectedFilter | translate }}</label
+ >
+ <select
+ id="filterSelect"
+ [ngModel]="selectedFilter"
+ (ngModelChange)="changeFilterType.next($event)"
+ class="form-select-sm font-weight-bolder bg-color-inherit"
+ >
+ <option *ngFor="let filter of actionsFilter" [ngValue]="filter" class="font-weight-normal">
+ {{ 'dashboard.apps.userLastAction.filter.type.' + filter | translate }}
+ </option>
+ </select>
+ </div>
+ <div class="d-flex" *ngIf="actionIntervalType$ | async as selectedInterval">
+ <label class="d-none" for="intervalSelect"
+ >{{ 'dashboard.apps.userLastAction.filter.label' | translate
+ }}{{ 'dashboard.apps.userLastAction.filter.interval.' + selectedInterval | translate }}</label
+ >
+ <select
+ id="intervalSelect"
+ [ngModel]="selectedInterval"
+ (ngModelChange)="changeIntervalType.next($event)"
+ class="form-select-sm font-weight-bold bg-color-inherit"
+ >
+ <option *ngFor="let interval of intervals" [ngValue]="interval" class="font-weight-normal">
+ {{ 'dashboard.apps.userLastAction.filter.interval.' + interval | translate }}
+ </option>
+ </select>
+ </div>
+ </div>
+ </div>
+ <div class="card-body overflow-auto">
+ <ng-container *ngIf="actions.length > 0; else noData">
+ <div *ngFor="let action of actions">
+ <app-action-row [action]="action">
+ <app-entity-user-administration-row
+ *ngIf="action.entity === EntityType.USERADMINISTRATION"
+ [action]="$any(action)"
+ ></app-entity-user-administration-row>
+ </app-action-row>
+ </div>
+ </ng-container>
+ </div>
+ <div class="card-footer"></div>
+ </div>
+ </div>
+ </ng-container>
+</ng-template>
+<ng-template #noData>
+ <div class="d-flex justify-content-center qa_class_no_data">
+ {{ 'common.filters.noResults' | translate }}
+ </div>
+</ng-template>
diff --git a/src/app/modules/dashboard/apps/user-last-action-tile/user-last-action-tile.component.ts b/src/app/modules/dashboard/apps/user-last-action-tile/user-last-action-tile.component.ts
new file mode 100644
index 0000000..c03016f
--- /dev/null
+++ b/src/app/modules/dashboard/apps/user-last-action-tile/user-last-action-tile.component.ts
@@ -0,0 +1,145 @@
+/*
+ * Copyright (c) 2022. Deutsche Telekom AG
+ *
+ * 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
+ *
+ * http://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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+
+import { Component, OnInit, TemplateRef, ViewChild, ViewContainerRef } from '@angular/core';
+import {
+ ActionFilter,
+ ActionInterval,
+ ActionModel,
+ ActionType,
+ EntityType,
+} from '../../../../model/user-last-action.model';
+import { ActionsListResponse } from '../../../../../../openapi/output';
+import { combineLatest, merge, Subject } from 'rxjs';
+import { map, scan, shareReplay, switchMap } from 'rxjs/operators';
+import { UnsubscribeService } from '../../../../services/unsubscribe/unsubscribe.service';
+import { selectDistinctState, UserSettingsService } from '../../../../services/user-settings.service';
+import { LastUserActionSettings, STATE_KEYS } from '../../../../model/user-preferences.model';
+import { HistoryService } from '../../../../services/history.service';
+
+@Component({
+ selector: 'app-user-last-action-tile',
+ templateUrl: './user-last-action-tile.component.html',
+ styleUrls: ['./user-last-action-tile.component.css'],
+ providers: [UnsubscribeService],
+})
+export class UserLastActionTileComponent implements OnInit {
+ public readonly actionsFilter: ActionFilter[] = Object.values(ActionFilter);
+ public readonly intervals: ActionInterval[] = Object.values(ActionInterval);
+ public readonly ActionType = ActionType;
+ public readonly EntityType = EntityType;
+ public changeFilterType: Subject<ActionFilter> = new Subject<ActionFilter>();
+ public changeIntervalType: Subject<ActionInterval> = new Subject<ActionInterval>();
+
+ @ViewChild('template', { static: true }) template!: TemplateRef<unknown>;
+
+ constructor(
+ private viewContainerRef: ViewContainerRef,
+ private historyService: HistoryService,
+ private unsubscribeService: UnsubscribeService,
+ private userSettingsService: UserSettingsService,
+ ) {}
+
+ private userActionsSettings$ = this.userSettingsService
+ .selectLastUserAction()
+ .pipe(shareReplay({ refCount: true, bufferSize: 1 }));
+
+ public actionFilterType$ = this.userActionsSettings$.pipe(
+ selectDistinctState<LastUserActionSettings, ActionFilter>(STATE_KEYS.FILTER_TYPE),
+ shareReplay({ refCount: true, bufferSize: 1 }),
+ );
+
+ public actionIntervalType$ = this.userActionsSettings$.pipe(
+ selectDistinctState<LastUserActionSettings, ActionInterval>(STATE_KEYS.INTERVAL),
+ shareReplay({ refCount: true, bufferSize: 1 }),
+ );
+
+ public actions$ = combineLatest([this.actionFilterType$, this.actionIntervalType$]).pipe(
+ switchMap(([filter, interval]) => {
+ const mappedInterval = UserLastActionTileComponent.mapActionInterval(interval);
+ return this.historyService.getUserActions(mappedInterval).pipe(
+ map(actions => UserLastActionTileComponent.mapActionsFromResponse(actions)),
+ map(actions => this.filterBySelectedFilterType(filter, actions)),
+ );
+ }),
+ );
+
+ private commands$ = merge(
+ this.changeIntervalType.pipe(map(interval => ({ interval: interval }))),
+ this.changeFilterType.pipe(map(filterType => ({ filterType: filterType }))),
+ );
+
+ private settings$ = this.userActionsSettings$.pipe(
+ switchMap(data => this.commands$.pipe(scan((settings, change) => ({ ...settings, ...change }), data))),
+ );
+
+ ngOnInit(): void {
+ this.viewContainerRef.createEmbeddedView(this.template);
+
+ this.settings$
+ .pipe(
+ switchMap(lastUserAction =>
+ this.userSettingsService.updatePreferences({
+ dashboard: {
+ apps: {
+ lastUserAction,
+ },
+ },
+ }),
+ ),
+ )
+ .subscribe();
+ }
+
+ private static mapActionsFromResponse(actions: ActionsListResponse): ActionModel[] {
+ return actions.items.map((action: any) => {
+ return {
+ actionCreatedAt: action.actionCreatedAt,
+ type: action.action.type,
+ entity: action.action.entity,
+ entityParams: {
+ ...action.action.entityParams,
+ },
+ };
+ });
+ }
+
+ private static mapActionInterval(interval: ActionInterval): number | undefined {
+ switch (interval) {
+ case ActionInterval.ALL:
+ return undefined;
+ case ActionInterval.LAST1D:
+ return 24;
+ case ActionInterval.LAST1H:
+ return 1;
+ case ActionInterval.LAST4H:
+ return 4;
+ }
+ }
+
+ private filterBySelectedFilterType(filter: ActionFilter, actions: ActionModel[]): ActionModel[] {
+ if (filter === ActionFilter.ALL) {
+ return actions;
+ } else if (filter === ActionFilter.SEARCH) {
+ return actions.filter(action => action.type === ActionType.SEARCH);
+ } else {
+ return actions.filter(action => action.type !== ActionType.SEARCH);
+ }
+ }
+}
diff --git a/src/app/modules/dashboard/dashboard-routing.module.ts b/src/app/modules/dashboard/dashboard-routing.module.ts
new file mode 100644
index 0000000..68833ba
--- /dev/null
+++ b/src/app/modules/dashboard/dashboard-routing.module.ts
@@ -0,0 +1,31 @@
+/*
+ * Copyright (c) 2022. Deutsche Telekom AG
+ *
+ * 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
+ *
+ * http://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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+
+import { NgModule } from '@angular/core';
+import { RouterModule, Routes } from '@angular/router';
+import { AuthGuard } from '../../guards/auth.guard';
+import { DashboardComponent } from './dashboard.component';
+
+const routes: Routes = [{ path: '', component: DashboardComponent, canActivate: [AuthGuard] }];
+
+@NgModule({
+ imports: [RouterModule.forChild(routes)],
+ exports: [RouterModule],
+})
+export class DashboardRoutingModule {}
diff --git a/src/app/modules/dashboard/dashboard.component.css b/src/app/modules/dashboard/dashboard.component.css
new file mode 100644
index 0000000..bdf57d6
--- /dev/null
+++ b/src/app/modules/dashboard/dashboard.component.css
@@ -0,0 +1,34 @@
+/*
+ * Copyright (c) 2022. Deutsche Telekom AG
+ *
+ * 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
+ *
+ * http://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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+
+p {
+ margin-bottom: 0 !important;
+}
+li {
+ list-style-type: none;
+}
+
+.row > * {
+ flex-shrink: 0;
+ width: initial;
+ max-width: initial;
+ padding-right: initial;
+ padding-left: initial;
+ margin-top: initial;
+}
diff --git a/src/app/modules/dashboard/dashboard.component.html b/src/app/modules/dashboard/dashboard.component.html
new file mode 100644
index 0000000..76a8e96
--- /dev/null
+++ b/src/app/modules/dashboard/dashboard.component.html
@@ -0,0 +1,77 @@
+<!--
+ ~ Copyright (c) 2022. Deutsche Telekom AG
+ ~
+ ~ 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
+ ~
+ ~ http://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.
+ ~
+ ~ SPDX-License-Identifier: Apache-2.0
+ -->
+
+
+<app-breadcrumb>
+ <app-breadcrumb-item>
+ <span aria-current="page">{{ 'layout.menu.items.dashboard' | translate }}</span>
+ </app-breadcrumb-item>
+</app-breadcrumb>
+<ng-container *ngIf="tiles$ | async as apps">
+ <div class="w-100 d-flex justify-content-between">
+ <h2 class="qa_title">{{ 'layout.menu.items.dashboard' | translate }}</h2>
+ <ul>
+ <li
+ #settingsDrop="ngbDropdown"
+ [ngbTooltip]="'dashboard.tooltips.settings' | translate"
+ class="qa_alarm_auto_settings"
+ ngbDropdown
+ >
+ <button
+ [attr.aria-label]="'dashboard.showSettings' | translate"
+ class="btn btn-outline-secondary no-border qa_dashboard_show_and_hide_settings_btn"
+ id="dropdownColumnSettings"
+ ngbDropdownToggle
+ >
+ <i aria-hidden="true" class="bi bi-gear-fill text-muted"></i>
+ </button>
+ <div aria-labelledby="dropdownColumnSettings" ngbDropdownMenu style="min-width: 250px">
+ <p class="px-4 small text-muted mb-1">{{ 'dashboard.selectApplications' | translate }}</p>
+ <form class="px-4 py-3 d-flex flex-column align-items-start">
+ <div
+ [appHasPermissions]="'dashboard.tile.' + app.type"
+ *ngFor="let app of apps"
+ class="d-flex justify-content-center"
+ >
+ <ng-container *ngIf="'dashboard.tile.' + app.type | hasPermission | async">
+ <input
+ type="checkbox"
+ [(ngModel)]="app.displayed"
+ (ngModelChange)="updateAction.next(app)"
+ [ngModelOptions]="{ standalone: true }"
+ [ngClass]="'qa_dashboard_show_app_' + app.type"
+ />
+ <p class="ml-2">{{ 'dashboard.apps.' + app.type | translate }}</p>
+ </ng-container>
+ </div>
+ </form>
+ </div>
+ </li>
+ </ul>
+ </div>
+ <hr />
+ <div class="row" cdkDropList (cdkDropListDropped)="dropAction.next($event)">
+ <ng-container *ngFor="let app of apps | map: filterDisplayedTiles">
+ <ng-container *ngIf="'dashboard.tile.' + app.type | hasPermission | async">
+ <ng-container *ngIf="app.type === DashboardApplications.USER_LAST_ACTION_TILE">
+ <app-user-last-action-tile></app-user-last-action-tile>
+ </ng-container>
+ </ng-container>
+ </ng-container>
+ </div>
+</ng-container>
diff --git a/src/app/modules/dashboard/dashboard.component.ts b/src/app/modules/dashboard/dashboard.component.ts
new file mode 100644
index 0000000..043ab6b
--- /dev/null
+++ b/src/app/modules/dashboard/dashboard.component.ts
@@ -0,0 +1,88 @@
+/*
+ * Copyright (c) 2022. Deutsche Telekom AG
+ *
+ * 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
+ *
+ * http://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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+
+import { Component, OnInit } from '@angular/core';
+import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop';
+import { UserSettingsService } from '../../services/user-settings.service';
+import { DashboardApplications, DashboardTileSettings } from '../../model/dashboard.model';
+import { UnsubscribeService } from '../../services/unsubscribe/unsubscribe.service';
+import { map, shareReplay, switchMap, takeUntil } from 'rxjs/operators';
+import { merge, Observable, Subject } from 'rxjs';
+
+@Component({
+ selector: 'app-dashboard',
+ templateUrl: './dashboard.component.html',
+ styleUrls: ['./dashboard.component.css'],
+ providers: [UnsubscribeService],
+})
+export class DashboardComponent implements OnInit {
+ DashboardApplications = DashboardApplications;
+
+ public dropAction = new Subject<CdkDragDrop<string[]>>();
+ public updateAction = new Subject<DashboardTileSettings>();
+
+ constructor(private unsubscribeService: UnsubscribeService, private userSettingsService: UserSettingsService) {}
+
+ public tiles$: Observable<DashboardTileSettings[]> = this.userSettingsService
+ .selectDashboardAvailableTiles()
+ .pipe(shareReplay({ refCount: true, bufferSize: 1 }));
+
+ filterDisplayedTiles(tiles: DashboardTileSettings[]): DashboardTileSettings[] {
+ return tiles.filter(tile => tile.displayed);
+ }
+
+ public visibleTiles$: Observable<DashboardTileSettings[]> = this.tiles$.pipe(
+ switchMap(tiles =>
+ this.updateAction.pipe(
+ map(updatedTile => {
+ const index = tiles.findIndex(tile => tile.type === updatedTile.type);
+ tiles[index].displayed = updatedTile.displayed;
+ return [...tiles];
+ }),
+ ),
+ ),
+ );
+
+ public movedTiles$: Observable<DashboardTileSettings[]> = this.tiles$.pipe(
+ switchMap(tiles =>
+ this.dropAction.pipe(
+ map(event => {
+ moveItemInArray(tiles, event.previousIndex, event.currentIndex);
+ return tiles;
+ }),
+ ),
+ ),
+ );
+ ngOnInit() {
+ merge(this.visibleTiles$, this.movedTiles$)
+ .pipe(
+ takeUntil(this.unsubscribeService.unsubscribe$),
+ switchMap(availableTiles =>
+ this.userSettingsService.updatePreferences({
+ dashboard: {
+ apps: {
+ availableTiles,
+ },
+ },
+ }),
+ ),
+ )
+ .subscribe();
+ }
+}
diff --git a/src/app/modules/dashboard/dashboard.module.ts b/src/app/modules/dashboard/dashboard.module.ts
new file mode 100644
index 0000000..8d63307
--- /dev/null
+++ b/src/app/modules/dashboard/dashboard.module.ts
@@ -0,0 +1,41 @@
+/*
+ * Copyright (c) 2022. Deutsche Telekom AG
+ *
+ * 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
+ *
+ * http://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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { NgModule } from '@angular/core';
+import { DashboardRoutingModule } from './dashboard-routing.module';
+import { SharedModule } from '../../shared.module';
+import {
+ EntityUserAdministrationRowComponent,
+} from './apps/user-last-action-tile/entity-user-administration-row/entity-user-administration-row.component';
+import { ActionButtonComponent } from './apps/user-last-action-tile/action-button/action-button.component';
+import { ActionRowComponent } from './apps/user-last-action-tile/action-row/action-row.component';
+import { UserLastActionTileComponent } from './apps/user-last-action-tile/user-last-action-tile.component';
+import { DashboardComponent } from './dashboard.component';
+import { DragDropModule } from '@angular/cdk/drag-drop';
+
+@NgModule({
+ declarations: [
+ DashboardComponent,
+ UserLastActionTileComponent,
+ ActionRowComponent,
+ ActionButtonComponent,
+ EntityUserAdministrationRowComponent,
+ ],
+ imports: [DashboardRoutingModule, SharedModule, DragDropModule],
+})
+export class DashboardModule {}
diff --git a/src/app/modules/i18n/i18n.module.ts b/src/app/modules/i18n/i18n.module.ts
new file mode 100644
index 0000000..52bedbe
--- /dev/null
+++ b/src/app/modules/i18n/i18n.module.ts
@@ -0,0 +1,51 @@
+/*
+ * Copyright (c) 2022. Deutsche Telekom AG
+ *
+ * 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
+ *
+ * http://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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+
+import { NgModule } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { HttpClient, HttpClientModule } from '@angular/common/http';
+import { TranslateLoader, TranslateModule, TranslateService } from '@ngx-translate/core';
+import { TranslateHttpLoader } from '@ngx-translate/http-loader';
+
+@NgModule({
+ declarations: [],
+ imports: [
+ CommonModule,
+ HttpClientModule,
+ TranslateModule.forRoot({
+ loader: {
+ provide: TranslateLoader,
+ useFactory: translateLoaderFactory,
+ deps: [HttpClient],
+ },
+ }),
+ ],
+ exports: [TranslateModule],
+})
+export class I18nModule {
+ constructor(translate: TranslateService) {
+ translate.addLangs(['en', 'de']);
+ const browserLang = translate.getBrowserLang();
+ translate.use(browserLang.match(/en|de/) ? browserLang : 'en');
+ }
+}
+
+export function translateLoaderFactory(httpClient: HttpClient) {
+ return new TranslateHttpLoader(httpClient, 'assets/i18n/', `.json?t=${new Date().getTime()}`);
+}
diff --git a/src/app/modules/user-administration/user-administration-form/user-administration-form.component.css b/src/app/modules/user-administration/user-administration-form/user-administration-form.component.css
new file mode 100644
index 0000000..f056ef5
--- /dev/null
+++ b/src/app/modules/user-administration/user-administration-form/user-administration-form.component.css
@@ -0,0 +1,38 @@
+/*
+ * Copyright (c) 2022. Deutsche Telekom AG
+ *
+ * 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
+ *
+ * http://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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+
+.custom-invalid-feedback {
+ width: 100%;
+ margin-top: 0.25rem;
+ font-size: 84%;
+ padding: 0.3rem 0.375rem;
+ color: var(--dark);
+ background-color: rgba(217, 0, 0, 0.1);
+ border-radius: 0.25rem;
+}
+
+.custom-control-input:checked ~ .custom-control-label::before {
+ background-color: var(--primary);
+ border-color: #e20088;
+}
+
+.custom-control-input:focus ~ .custom-control-label::before {
+ box-shadow: 0 0 0 0.2rem rgba(226, 0, 136, 0.25);
+ border-color: rgba(226, 0, 136, 0.25);
+}
diff --git a/src/app/modules/user-administration/user-administration-form/user-administration-form.component.html b/src/app/modules/user-administration/user-administration-form/user-administration-form.component.html
new file mode 100644
index 0000000..66ede05
--- /dev/null
+++ b/src/app/modules/user-administration/user-administration-form/user-administration-form.component.html
@@ -0,0 +1,205 @@
+<!--
+ ~ Copyright (c) 2022. Deutsche Telekom AG
+ ~
+ ~ 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
+ ~
+ ~ http://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.
+ ~
+ ~ SPDX-License-Identifier: Apache-2.0
+ -->
+
+
+<app-breadcrumb>
+ <app-breadcrumb-item>
+ <a [routerLink]="['/dashboard']">{{ 'layout.menu.items.home' | translate }}</a>
+ </app-breadcrumb-item>
+ <app-breadcrumb-item>
+ <a [routerLink]="['/user-administration', 'list']">{{ 'userAdministration.list.title' | translate }}</a>
+ </app-breadcrumb-item>
+ <ng-container *ngIf="userId === null">
+ <app-breadcrumb-item>
+ <span aria-current="page">{{ 'userAdministration.form.title.create' | translate }}</span>
+ </app-breadcrumb-item>
+ </ng-container>
+ <ng-container *ngIf="user">
+ <app-breadcrumb-item>
+ <a [routerLink]="['/user-administration', user.id, 'detail']">{{ user.username }}</a>
+ </app-breadcrumb-item>
+ <app-breadcrumb-item>
+ <span aria-current="page">{{ 'userAdministration.form.title.edit' | translate }}</span>
+ </app-breadcrumb-item>
+ </ng-container>
+</app-breadcrumb>
+
+<h2 class="py-2 qa_title">
+ {{ (userId === null ? 'userAdministration.form.title.create' : 'userAdministration.form.title.edit') | translate }}
+</h2>
+<hr />
+
+<div class="row">
+ <!-- Set User Data-->
+ <div class="col-12 col-lg-6">
+ <h4 class="text-monospace border-bottom text-secondary pb-2">
+ {{ 'userAdministration.form.headings.setUserData' | translate }}
+ </h4>
+ <form class="mb-5" [formGroup]="keycloakUserForm" novalidate>
+ <div class="form-group row">
+ <label class="col-xl-3 col-form-label" for="id">{{ 'userAdministration.fields.id' | translate }}</label>
+ <div class="col-xl-9">
+ <input formControlName="id" class="form-control" id="id" readonly />
+ </div>
+ </div>
+ <div class="form-group row">
+ <label class="col-xl-3 col-form-label" for="username">{{
+ 'userAdministration.fields.userName' | translate
+ }}</label>
+ <div class="col-xl-9">
+ <input
+ formControlName="username"
+ class="form-control"
+ id="username"
+ [attr.readonly]="this.userId"
+ [class.is-invalid]="isFormControlInvalid(userName)"
+ required
+ aria-required="true"
+ />
+ <div *ngIf="userName && userName?.errors?.required" class="invalid-feedback qa_required_user_name">
+ {{ 'common.required' | translate }}
+ </div>
+ <div *ngIf="userName && userName?.errors?.pattern" class="invalid-feedback qa_invalid_user_name">
+ {{ 'common.form.feedback.invalidCharacters' | translate }}
+ </div>
+ </div>
+ </div>
+ <div class="form-group row">
+ <label class="col-xl-3 col-form-label" for="email">{{ 'userAdministration.fields.email' | translate }}</label>
+ <div class="col-xl-9">
+ <input
+ formControlName="email"
+ type="email"
+ class="form-control"
+ id="email"
+ [class.is-invalid]="isFormControlInvalid(email)"
+ />
+ <div *ngIf="email && email?.errors?.email" class="invalid-feedback qa_wrong_format_email">
+ {{ 'common.form.feedback.emailWrongFormat' | translate }}
+ </div>
+ <div *ngIf="email && email?.errors?.pattern" class="invalid-feedback qa_invalid_email">
+ {{ 'common.form.feedback.invalidCharacters' | translate }}
+ </div>
+ <div *ngIf="email && email?.errors?.required" class="invalid-feedback qa_required_email">
+ {{ 'common.form.feedback.required' | translate }}
+ </div>
+ </div>
+ </div>
+ <div class="form-group row">
+ <label class="col-xl-3 col-form-label" for="firstName">{{
+ 'userAdministration.fields.firstName' | translate
+ }}</label>
+ <div class="col-xl-9">
+ <input
+ formControlName="firstName"
+ class="form-control"
+ id="firstName"
+ [class.is-invalid]="isFormControlInvalid(firstName)"
+ />
+ <div *ngIf="firstName && firstName?.errors?.pattern" class="invalid-feedback qa_invalid_first_name">
+ {{ 'common.form.feedback.invalidCharacters' | translate }}
+ </div>
+ </div>
+ </div>
+ <div class="form-group row">
+ <label class="col-xl-3 col-form-label" for="lastName">{{
+ 'userAdministration.fields.lastName' | translate
+ }}</label>
+ <div class="col-xl-9">
+ <input
+ formControlName="lastName"
+ class="form-control"
+ id="lastName"
+ [class.is-invalid]="isFormControlInvalid(lastName)"
+ />
+ <div *ngIf="lastName && lastName?.errors?.pattern" class="invalid-feedback qa_invalid_last_name">
+ {{ 'common.form.feedback.invalidCharacters' | translate }}
+ </div>
+ </div>
+ </div>
+ </form>
+ <!-- SET ROLES-->
+ <div class="mb-5" style="min-height: 150px">
+ <h4 class="text-monospace border-bottom text-secondary pb-2">
+ {{ 'userAdministration.form.headings.setRoles.title' | translate }}
+ </h4>
+
+ <div class="form-row">
+ <div class="col-xl-3 col-form-label">{{ 'userAdministration.form.headings.setRoles.title' | translate }}</div>
+ <div class="col-xl-9">
+ <div class="row" style="padding: 0 15px">
+ <div class="col-xl-5 p-3 border border-radius mb-md-2" style="min-height: 125px">
+ <h5 class="qa_available_roles">
+ {{ 'userAdministration.form.headings.setRoles.available' | translate }}
+ </h5>
+ <ng-container *ngFor="let checkbox of checkBoxes.available">
+ <ng-container *ngIf="checkbox.name.startsWith('onap_')">
+ <div class="form-check">
+ <input
+ type="checkbox"
+ class="form-check-input qa_checkbox_available"
+ [attr.aria-labelledby]="checkbox.name"
+ [value]="checkbox.id"
+ (change)="onCheckboxChange(checkbox.id, true)"
+ />
+ <label class="form-check-label" [attr.id]="checkbox.name">{{ checkbox.name }}</label>
+ </div>
+ </ng-container>
+ </ng-container>
+ </div>
+ <div class="col-xl-2"></div>
+ <div class="col-xl-5 p-3 border border-radius" style="min-height: 125px">
+ <h5 class="qa_assigned_roles">{{ 'userAdministration.form.headings.setRoles.assigned' | translate }}</h5>
+ <ng-container *ngFor="let checkbox of checkBoxes.assigned">
+ <ng-container *ngIf="checkbox.name.startsWith('onap_')">
+ <div class="form-check">
+ <input
+ type="checkbox"
+ class="form-check-input qa_checkbox_assigned"
+ [attr.aria-labelledby]="checkbox.name"
+ [value]="checkbox.id"
+ (change)="onCheckboxChange(checkbox.id, false)"
+ [checked]="true"
+ />
+ <label class="form-check-label" [attr.id]="checkbox.name">{{ checkbox.name }}</label>
+ </div>
+ </ng-container>
+ </ng-container>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <div class="float-right">
+ <ng-container *ngIf="userId === null">
+ <button class="btn btn-secondary qa_submit_cancel" [routerLink]="['../', 'list']">
+ {{ 'common.buttons.cancel' | translate }}
+ </button>
+ </ng-container>
+ <ng-container *ngIf="userId !== null">
+ <button class="btn btn-secondary qa_submit_cancel" [routerLink]="['../../list']">
+ {{ 'common.buttons.cancel' | translate }}
+ </button>
+ </ng-container>
+ <button type="submit" class="btn btn-primary qa_submit_button ml-2" (click)="onSubmit()">
+ {{ 'common.buttons.save' | translate }}
+ </button>
+ </div>
+ </div>
+</div>
diff --git a/src/app/modules/user-administration/user-administration-form/user-administration-form.component.spec.ts b/src/app/modules/user-administration/user-administration-form/user-administration-form.component.spec.ts
new file mode 100644
index 0000000..def957f
--- /dev/null
+++ b/src/app/modules/user-administration/user-administration-form/user-administration-form.component.spec.ts
@@ -0,0 +1,43 @@
+/*
+ * Copyright (c) 2022. Deutsche Telekom AG
+ *
+ * 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
+ *
+ * http://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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { UserAdministrationFormComponent } from './user-administration-form.component';
+
+describe('UserAdministrationFormComponent', () => {
+ let component: UserAdministrationFormComponent;
+ let fixture: ComponentFixture<UserAdministrationFormComponent>;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ declarations: [UserAdministrationFormComponent],
+ }).compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(UserAdministrationFormComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/app/modules/user-administration/user-administration-form/user-administration-form.component.ts b/src/app/modules/user-administration/user-administration-form/user-administration-form.component.ts
new file mode 100644
index 0000000..7df2700
--- /dev/null
+++ b/src/app/modules/user-administration/user-administration-form/user-administration-form.component.ts
@@ -0,0 +1,232 @@
+/*
+ * Copyright (c) 2022. Deutsche Telekom AG
+ *
+ * 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
+ *
+ * http://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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+
+import { Component, OnInit } from '@angular/core';
+import { AbstractControl, FormControl, FormGroup, Validators } from '@angular/forms';
+import {
+ CreateUserRequest,
+ Role,
+ RoleListResponse,
+ RolesService,
+ UpdateUserRequest,
+ UserResponse,
+ UsersService,
+} from 'openapi/output';
+import { AlertService } from 'src/app/modules/alerting';
+import { ActivatedRoute, Router } from '@angular/router';
+import { TranslateService } from '@ngx-translate/core';
+import { UnsubscribeService } from 'src/app/services/unsubscribe/unsubscribe.service';
+import { NON_WHITE_SPACE_PATTERN, VALIDATION_PATTERN } from 'src/app/model/validation-pattern.model';
+import { map, switchMap, take, takeUntil } from 'rxjs/operators';
+import { markAsDirtyAndValidate } from 'src/app/helpers/helpers';
+import { forkJoin, Observable, zip } from 'rxjs';
+import { ActionType, EntityType } from '../../../model/user-last-action.model';
+import { HistoryService } from '../../../services/history.service';
+
+@Component({
+ selector: 'app-user-administration-form',
+ templateUrl: './user-administration-form.component.html',
+ styleUrls: ['./user-administration-form.component.css'],
+ providers: [UnsubscribeService],
+})
+export class UserAdministrationFormComponent implements OnInit {
+ public readonly userId: string | null;
+ public readonly keycloakUserForm: FormGroup;
+ public user: UserResponse | undefined = undefined;
+
+ public checkBoxes: {
+ assigned: Role[];
+ available: Role[];
+ } = {
+ assigned: [],
+ available: [],
+ };
+
+ constructor(
+ private readonly alertService: AlertService,
+ private readonly route: ActivatedRoute,
+ private readonly userAdministrationService: UsersService,
+ private readonly rolesService: RolesService,
+ private readonly router: Router,
+ private readonly translateService: TranslateService,
+ private readonly unsubscribeService: UnsubscribeService,
+ private readonly historyService: HistoryService,
+ ) {
+ this.userId = this.route.snapshot.paramMap.get('userId');
+
+ this.keycloakUserForm = new FormGroup({
+ id: new FormControl({ value: null, disabled: true }),
+ username: new FormControl({ value: null, disabled: this.userId !== null }, [
+ Validators.required,
+ Validators.maxLength(50),
+ Validators.pattern(VALIDATION_PATTERN),
+ Validators.pattern(NON_WHITE_SPACE_PATTERN),
+ ]),
+ email: new FormControl(null, [Validators.email, Validators.required, Validators.pattern(VALIDATION_PATTERN)]),
+ firstName: new FormControl(null, [Validators.pattern(VALIDATION_PATTERN)]),
+ lastName: new FormControl(null, [Validators.pattern(VALIDATION_PATTERN)]),
+ });
+ }
+
+ ngOnInit(): void {
+ if (this.userId !== null) {
+ this.userAdministrationService
+ .getUser(this.userId)
+ .pipe(takeUntil(this.unsubscribeService.unsubscribe$))
+ .subscribe(user => {
+ this.user = user;
+ this.keycloakUserForm.patchValue({
+ id: user.id,
+ username: user.username,
+ email: user.email,
+ firstName: user.firstName,
+ lastName: user.lastName,
+ });
+ });
+
+ zip(
+ this.userAdministrationService.listAvailableRoles(this.userId).pipe(map(available => available.items)),
+ this.userAdministrationService.listAssignedRoles(this.userId).pipe(map(assigned => assigned.items)),
+ )
+ .pipe(takeUntil(this.unsubscribeService.unsubscribe$))
+ .subscribe(([available, assigned]) => {
+ this.checkBoxes = { available, assigned };
+ });
+ } else {
+ this.rolesService
+ .listRoles()
+ .pipe(
+ takeUntil(this.unsubscribeService.unsubscribe$),
+ map(available => available.items),
+ )
+ .subscribe(available => {
+ this.checkBoxes.available = available;
+ });
+ }
+ }
+
+ get userName(): FormControl {
+ return this.keycloakUserForm.get('username') as FormControl;
+ }
+
+ get email(): FormControl {
+ return this.keycloakUserForm.get('email') as FormControl;
+ }
+
+ get firstName(): FormControl {
+ return this.keycloakUserForm.get('firstName') as FormControl;
+ }
+
+ get lastName(): FormControl {
+ return this.keycloakUserForm.get('lastName') as FormControl;
+ }
+
+ public onSubmit(): void {
+ markAsDirtyAndValidate(this.keycloakUserForm);
+ if (this.keycloakUserForm.valid) {
+ const formValue = this.keycloakUserForm.getRawValue();
+ if (this.userId === null) {
+ this.userAdministrationService
+ .createUser(this.createUserRequest(formValue))
+ .pipe(
+ switchMap((data: UserResponse) =>
+ this.historyService.createUserHistoryAction({
+ type: ActionType.CREATE,
+ entity: EntityType.USERADMINISTRATION,
+ entityParams: { userName: data.username, userId: data.id },
+ }),
+ ),
+ take(1),
+ )
+ .subscribe(() => {
+ this.alertService.success(this.translateService.instant('userAdministration.messages.success.created'), {
+ keepAfterRouteChange: true,
+ autoClose: true,
+ });
+ this.router.navigate(['../list'], { relativeTo: this.route });
+ });
+ } else {
+ this.updateUserData(
+ this.userAdministrationService.updateUser(this.userId, this.updateUserRequest(formValue)),
+ this.userAdministrationService.updateAssignedRoles(this.userId, undefined, this.checkBoxes.assigned),
+ );
+ }
+ }
+ }
+
+ public isFormControlInvalid(formControl: AbstractControl | null): boolean {
+ if (formControl !== null) {
+ return formControl && formControl?.invalid && (formControl?.dirty || formControl?.touched);
+ }
+ return false;
+ }
+
+ public onCheckboxChange(roleId: string, checked: boolean): void {
+ if (checked) {
+ const checkedObj = { ...this.checkBoxes.available.find(({ id }) => id === roleId) } as Role;
+ this.checkBoxes.assigned.push(checkedObj);
+ this.checkBoxes.available = this.checkBoxes.available.filter(({ id }) => id !== roleId);
+ } else {
+ const uncheckedObj = { ...this.checkBoxes.assigned.find(({ id }) => id === roleId) } as Role;
+ this.checkBoxes.available.push(uncheckedObj);
+ this.checkBoxes.assigned = this.checkBoxes.assigned.filter(({ id }) => id !== roleId);
+ }
+ }
+
+ private createUserRequest(formValue: any): CreateUserRequest {
+ return {
+ username: formValue.username,
+ email: formValue.email,
+ firstName: formValue.firstName,
+ lastName: formValue.lastName,
+ enabled: true,
+ roles: this.checkBoxes.assigned,
+ };
+ }
+
+ private updateUserRequest(formValue: any): UpdateUserRequest {
+ return {
+ email: formValue.email,
+ firstName: formValue.firstName,
+ lastName: formValue.lastName,
+ enabled: true,
+ };
+ }
+
+ private updateUserData(userResponse: Observable<UserResponse>, roleResponse: Observable<RoleListResponse>): void {
+ forkJoin([userResponse, roleResponse])
+ .pipe(
+ switchMap(([,]) =>
+ this.historyService.createUserHistoryAction({
+ type: ActionType.EDIT,
+ entity: EntityType.USERADMINISTRATION,
+ entityParams: { userName: this.user!.username, userId: this.user!.id },
+ }),
+ ),
+ take(1),
+ )
+ .subscribe(() => {
+ this.alertService.success(this.translateService.instant('userAdministration.messages.success.updated'), {
+ keepAfterRouteChange: true,
+ autoClose: true,
+ });
+ this.router.navigate(['../../list'], { relativeTo: this.route });
+ });
+ }
+}
diff --git a/src/app/modules/user-administration/user-administration-list/user-administration-list.component.css b/src/app/modules/user-administration/user-administration-list/user-administration-list.component.css
new file mode 100644
index 0000000..b8d5a0e
--- /dev/null
+++ b/src/app/modules/user-administration/user-administration-list/user-administration-list.component.css
@@ -0,0 +1,28 @@
+/*
+ * Copyright (c) 2022. Deutsche Telekom AG
+ *
+ * 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
+ *
+ * http://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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+
+.btn-outline-secondary {
+ color: var(--dark-gray) !important;
+ border-color: var(--dark-gray) !important;
+}
+.btn-outline-secondary:hover {
+ color: var(--light-gray) !important;
+ background-color: var(--dark-gray) !important;
+ border-color: var(--dark-gray) !important;
+}
diff --git a/src/app/modules/user-administration/user-administration-list/user-administration-list.component.html b/src/app/modules/user-administration/user-administration-list/user-administration-list.component.html
new file mode 100644
index 0000000..d205ee2
--- /dev/null
+++ b/src/app/modules/user-administration/user-administration-list/user-administration-list.component.html
@@ -0,0 +1,159 @@
+<!--
+ ~ Copyright (c) 2022. Deutsche Telekom AG
+ ~
+ ~ 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
+ ~
+ ~ http://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.
+ ~
+ ~ SPDX-License-Identifier: Apache-2.0
+ -->
+
+
+<ng-container
+ *ngIf="
+ {
+ users: result$ | async,
+ page: page$ | async,
+ pageSize: pageSize$ | async
+ } as vm;
+ else loading
+ "
+>
+ <app-breadcrumb>
+ <app-breadcrumb-item>
+ <a [routerLink]="['/dashboard']">{{ 'layout.menu.items.home' | translate }}</a>
+ </app-breadcrumb-item>
+ <app-breadcrumb-item>
+ <span aria-current="page">{{ 'userAdministration.list.title' | translate }}</span>
+ </app-breadcrumb-item>
+ </app-breadcrumb>
+ <h2>{{ 'userAdministration.list.title' | translate }}</h2>
+ <hr />
+ <div class="d-flex justify-content-between">
+ <button
+ class="btn btn-primary qa_create_button ml-auto"
+ [appHasPermissions]="'users.administration.create'"
+ type="button"
+ [routerLink]="['../', 'create']"
+ >
+ {{ 'userAdministration.buttons.createUser' | translate }}
+ </button>
+ </div>
+
+ <div class="row">
+ <div class="col">
+ <div class="table-responsive">
+ <table class="table table-sm table-striped">
+ <caption>
+ {{
+ 'userAdministration.list.tableCaption' | translate
+ }}
+ </caption>
+ <thead>
+ <tr>
+ <th class="qa_user_name_header" scope="col">{{ 'userAdministration.fields.userName' | translate }}</th>
+ <th class="qa_first_name_header" scope="col">{{ 'userAdministration.fields.firstName' | translate }}</th>
+ <th class="qa_last_name_header" scope="col">{{ 'userAdministration.fields.lastName' | translate }}</th>
+ <th class="qa_email_header" scope="col">{{ 'userAdministration.fields.email' | translate }}</th>
+ <th class="qa_assigned_roles_header" scope="col">
+ {{ 'userAdministration.fields.assignedRoles' | translate }}
+ </th>
+ <th class="qa_actions_header" scope="col" style="width: 11%">
+ {{ 'userAdministration.fields.actions' | translate }}
+ </th>
+ </tr>
+ </thead>
+ <tbody>
+ <ng-container *ngIf="vm.users as users">
+ <tr *ngFor="let user of users.items">
+ <td>{{ user.username }}</td>
+ <td>{{ user.firstName }}</td>
+ <td>{{ user.lastName }}</td>
+ <td>
+ <a [href]="'mailto:' + user.email">{{ user.email }}</a>
+ </td>
+ <td>
+ <ng-container *ngFor="let role of user.realmRoles; let last = last">
+ <span>{{ role }}<span *ngIf="!last">, </span> </span>
+ </ng-container>
+ </td>
+ <td>
+ <div class="d-flex" *ngIf="loggedUserId$ | async as userId">
+ <ng-container *ngIf="userId === user.id; else elseBlock">
+ <span
+ class="d-inline-block"
+ tabindex="0"
+ placement="top"
+ container="body"
+ [ngbTooltip]="'common.buttons.notPossibleDelete' | translate"
+ >
+ <button
+ class="btn btn-sm btn-outline-danger qa_delete_button mr-2"
+ type="button"
+ [appHasPermissions]="'users.administration.delete'"
+ [attr.aria-label]="'common.buttons.delete' | translate"
+ disabled
+ >
+ <i class="bi bi-trash" aria-hidden="true"></i>
+ </button>
+ </span>
+ </ng-container>
+ <ng-template #elseBlock>
+ <button
+ class="btn btn-sm btn-outline-danger qa_delete_button mr-2"
+ type="button"
+ placement="top"
+ container="body"
+ [appHasPermissions]="'users.administration.delete'"
+ [ngbTooltip]="'common.buttons.delete' | translate"
+ [attr.aria-label]="'common.buttons.delete' | translate"
+ (click)="openModal(user.id, user.username)"
+ >
+ <i class="bi bi-trash" aria-hidden="true"></i>
+ </button>
+ </ng-template>
+
+ <button
+ class="btn btn-sm btn-outline-secondary qa_edit_button"
+ type="button"
+ placement="top"
+ container="body"
+ [appHasPermissions]="'users.administration.edit'"
+ [ngbTooltip]="'common.buttons.edit' | translate"
+ [routerLink]="['../', user.id, 'edit']"
+ [attr.aria-label]="'common.buttons.edit' | translate"
+ >
+ <i class="bi bi-pencil" aria-hidden="true"></i>
+ </button>
+ </div>
+ </td>
+ </tr>
+ </ng-container>
+ </tbody>
+ </table>
+ </div>
+
+ <app-pagination
+ *ngIf="vm.users && vm.users.totalCount > 10"
+ [collectionSize]="vm.users.totalCount || 0"
+ [page]="vm.page || 1"
+ [pageSize]="vm.pageSize || 10"
+ (pageChange)="changePage($event)"
+ (pageSizeChange)="changePageSize($event)"
+ >
+ </app-pagination>
+ </div>
+ </div>
+</ng-container>
+
+<ng-template #loading>
+ <app-table-skeleton></app-table-skeleton>
+</ng-template>
diff --git a/src/app/modules/user-administration/user-administration-list/user-administration-list.component.spec.ts b/src/app/modules/user-administration/user-administration-list/user-administration-list.component.spec.ts
new file mode 100644
index 0000000..db24b11
--- /dev/null
+++ b/src/app/modules/user-administration/user-administration-list/user-administration-list.component.spec.ts
@@ -0,0 +1,43 @@
+/*
+ * Copyright (c) 2022. Deutsche Telekom AG
+ *
+ * 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
+ *
+ * http://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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { UserAdministrationListComponent } from './user-administration-list.component';
+
+describe('UserAdministrationComponent', () => {
+ let component: UserAdministrationListComponent;
+ let fixture: ComponentFixture<UserAdministrationListComponent>;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ declarations: [UserAdministrationListComponent],
+ }).compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(UserAdministrationListComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/app/modules/user-administration/user-administration-list/user-administration-list.component.ts b/src/app/modules/user-administration/user-administration-list/user-administration-list.component.ts
new file mode 100644
index 0000000..30637a1
--- /dev/null
+++ b/src/app/modules/user-administration/user-administration-list/user-administration-list.component.ts
@@ -0,0 +1,118 @@
+/*
+ * Copyright (c) 2022. Deutsche Telekom AG
+ *
+ * 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
+ *
+ * http://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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+
+import { Component } from '@angular/core';
+import { map, repeatWhen, shareReplay, switchMap, takeUntil, tap } from 'rxjs/operators';
+import { AlertService } from 'src/app/modules/alerting';
+import { TranslateService } from '@ngx-translate/core';
+import { UnsubscribeService } from 'src/app/services/unsubscribe/unsubscribe.service';
+import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
+import { ConfirmationModalComponent } from 'src/app/components/shared/confirmation-modal/confirmation-modal.component';
+import { BehaviorSubject, combineLatest, EMPTY, Subject } from 'rxjs';
+import { UsersService } from 'openapi/output';
+import { ActionType, EntityType } from '../../../model/user-last-action.model';
+import { HistoryService } from '../../../services/history.service';
+import { AuthService } from '../../../services/auth.service';
+
+@Component({
+ selector: 'app-user-administration-list',
+ templateUrl: './user-administration-list.component.html',
+ styleUrls: ['./user-administration-list.component.css'],
+ providers: [UnsubscribeService],
+})
+export class UserAdministrationListComponent {
+ readonly page$ = new BehaviorSubject<number>(1);
+ readonly pageSize$ = new BehaviorSubject<number>(10);
+ readonly loggedUserId$ = this.authService.loadCachedUserProfile().pipe(
+ takeUntil(this.unsubscribeService.unsubscribe$),
+ map(userInfo => userInfo!.sub));
+
+ private readonly reload$ = new Subject<void>();
+ readonly result$ = combineLatest([this.page$, this.pageSize$]).pipe(
+ switchMap(([page, pageSize]) => {
+ return this.userAdministrationService.listUsers(page, pageSize).pipe(
+ map(response => {
+ return {
+ ...response,
+ items: response.items.map(user => ({
+ ...user,
+ realmRoles: user.realmRoles?.filter(role => role.startsWith('onap_')),
+ })),
+ };
+ }),
+ repeatWhen(() => this.reload$),
+ );
+ }),
+ shareReplay({ refCount: true, bufferSize: 1 }),
+ );
+
+ constructor(
+ private readonly userAdministrationService: UsersService,
+ private readonly alertService: AlertService,
+ private readonly translateService: TranslateService,
+ private readonly modalService: NgbModal,
+ private readonly unsubscribeService: UnsubscribeService,
+ private readonly authService: AuthService,
+ private readonly historyService: HistoryService,
+ ) {
+ }
+
+ changePage(page: number): void {
+ this.page$.next(page);
+ }
+
+ changePageSize(pageSize: number): void {
+ this.pageSize$.next(pageSize);
+ }
+
+ openModal(userId: string, userName: string): void {
+ // open confirmation modal for user deletion
+ const modalRef = this.modalService.open(ConfirmationModalComponent,{backdropClass:'backdropClass'});
+ modalRef.componentInstance.okText = this.translateService.instant('common.buttons.delete');
+ modalRef.componentInstance.title = this.translateService.instant('userAdministration.list.modal.delete.title');
+ modalRef.componentInstance.text = this.translateService.instant('userAdministration.list.modal.delete.text', {
+ userName,
+ });
+ modalRef.closed
+ .pipe(
+ takeUntil(this.unsubscribeService.unsubscribe$),
+ switchMap((confirm: boolean) => {
+ if (confirm) {
+ return this.userAdministrationService.deleteUser(userId).pipe(
+ switchMap(() =>
+ this.historyService.createUserHistoryAction({
+ type: ActionType.DELETE,
+ entity: EntityType.USERADMINISTRATION,
+ entityParams: { userName, userId },
+ }),
+ ),
+ );
+ }
+ return EMPTY;
+ }),
+ tap(() => {
+ this.alertService.success(this.translateService.instant('userAdministration.messages.success.deleted'), {
+ keepAfterRouteChange: true,
+ autoClose: true,
+ });
+ }),
+ )
+ .subscribe(() => this.reload$.next());
+ }
+}
diff --git a/src/app/modules/user-administration/user-administration-routing.module.ts b/src/app/modules/user-administration/user-administration-routing.module.ts
new file mode 100644
index 0000000..7d1a8db
--- /dev/null
+++ b/src/app/modules/user-administration/user-administration-routing.module.ts
@@ -0,0 +1,54 @@
+/*
+ * Copyright (c) 2022. Deutsche Telekom AG
+ *
+ * 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
+ *
+ * http://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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+
+import { NgModule } from '@angular/core';
+import { RouterModule, Routes } from '@angular/router';
+import { UserAdministrationListComponent } from './user-administration-list/user-administration-list.component';
+import { AuthGuard } from '../../guards/auth.guard';
+import { HasPermissionsGuard } from '../../guards/has-permissions.guard';
+import { UserAdministrationFormComponent } from './user-administration-form/user-administration-form.component';
+import { EditUserCanActivateGuard } from '../../guards/edit-user.can-activate.guard';
+
+const routes: Routes = [
+ { path: '', redirectTo: 'list', pathMatch: 'full' },
+ {
+ path: 'list',
+ component: UserAdministrationListComponent,
+ canActivate: [AuthGuard, HasPermissionsGuard],
+ data: { permission: 'users.administration.list' },
+ },
+ {
+ path: 'create',
+ component: UserAdministrationFormComponent,
+ canActivate: [AuthGuard, HasPermissionsGuard],
+ data: { permission: 'users.administration.create' },
+ },
+ {
+ path: ':userId/edit',
+ component: UserAdministrationFormComponent,
+ canActivate: [AuthGuard, HasPermissionsGuard, EditUserCanActivateGuard],
+ data: { permission: 'users.administration.edit' },
+ },
+];
+
+@NgModule({
+ imports: [RouterModule.forChild(routes)],
+ exports: [RouterModule],
+})
+export class UserAdministrationRoutingModule {}
diff --git a/src/app/modules/user-administration/user-administration.module.ts b/src/app/modules/user-administration/user-administration.module.ts
new file mode 100644
index 0000000..799d405
--- /dev/null
+++ b/src/app/modules/user-administration/user-administration.module.ts
@@ -0,0 +1,30 @@
+/*
+ * Copyright (c) 2022. Deutsche Telekom AG
+ *
+ * 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
+ *
+ * http://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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+
+import { NgModule } from '@angular/core';
+import { UserAdministrationListComponent } from './user-administration-list/user-administration-list.component';
+import { UserAdministrationFormComponent } from './user-administration-form/user-administration-form.component';
+import { UserAdministrationRoutingModule } from './user-administration-routing.module';
+import { SharedModule } from '../../shared.module';
+
+@NgModule({
+ declarations: [UserAdministrationListComponent, UserAdministrationFormComponent],
+ imports: [UserAdministrationRoutingModule, SharedModule],
+})
+export class UserAdministrationModule {}
diff --git a/src/app/package.json b/src/app/package.json
new file mode 100644
index 0000000..1706abd
--- /dev/null
+++ b/src/app/package.json
@@ -0,0 +1,6 @@
+{
+ "name": "frontend",
+ "private": true,
+ "description": "This is a special package.json file that is not used by package managers. It is however used to tell the tools and bundlers whether the code under this directory is free of code with non-local side-effect. Any code that does have non-local side-effects can't be well optimized (tree-shaken) and will result in unnecessary increased payload size. It should be safe to set this option to 'false' for new applications, but existing code bases could be broken when built with the production config if the application code does contain non-local side-effects that the application depends on.",
+ "sideEffects": false
+}
diff --git a/src/app/pipes/colon.pipe.ts b/src/app/pipes/colon.pipe.ts
new file mode 100644
index 0000000..fe0b50b
--- /dev/null
+++ b/src/app/pipes/colon.pipe.ts
@@ -0,0 +1,29 @@
+/*
+ * Copyright (c) 2022. Deutsche Telekom AG
+ *
+ * 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
+ *
+ * http://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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+
+import { Pipe, PipeTransform } from '@angular/core';
+
+@Pipe({
+ name: 'colon',
+})
+export class ColonPipe implements PipeTransform {
+ transform(value: string): string {
+ return `${value}: `;
+ }
+}
diff --git a/src/app/pipes/guard-type.pipe.ts b/src/app/pipes/guard-type.pipe.ts
new file mode 100644
index 0000000..0c2b847
--- /dev/null
+++ b/src/app/pipes/guard-type.pipe.ts
@@ -0,0 +1,32 @@
+/*
+ * Copyright (c) 2022. Deutsche Telekom AG
+ *
+ * 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
+ *
+ * http://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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+
+import { Pipe, PipeTransform } from '@angular/core';
+
+export type TypeGuard<A, B extends A> = (a: A) => a is B;
+
+// https://github.com/angular/angular/issues/34522#issuecomment-762973301
+@Pipe({
+ name: 'guardType',
+})
+export class GuardTypePipe implements PipeTransform {
+ transform<A, B extends A>(value: A, typeGuard: TypeGuard<A, B>): B | undefined {
+ return typeGuard(value) ? value : undefined;
+ }
+}
diff --git a/src/app/pipes/has-permission.pipe.ts b/src/app/pipes/has-permission.pipe.ts
new file mode 100644
index 0000000..a22b034
--- /dev/null
+++ b/src/app/pipes/has-permission.pipe.ts
@@ -0,0 +1,58 @@
+/*
+ * Copyright (c) 2022. Deutsche Telekom AG
+ *
+ * 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
+ *
+ * http://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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+
+import { Inject, Pipe, PipeTransform } from '@angular/core';
+import { HttpClient } from '@angular/common/http';
+import { ACL_CONFIG, AclConfig } from '../modules/auth/injection-tokens';
+import { AuthService } from '../services/auth.service';
+import { Observable } from 'rxjs';
+import { map, take } from 'rxjs/operators';
+
+/*
+ hasPermission pipe returns Promise<boolean | void> value based on the authentication file (acl.json) and user's role.
+ Using the pipe we are able to show/hide the elements in the app for specific user role.
+ Input parameter is the string from the acl.json
+ USAGE: *ngIf="'dashboard.tile.KPI_DASHBOARD_TILE' | hasPermission | async"
+*/
+@Pipe({
+ name: 'hasPermission',
+})
+export class HasPermissionPipe implements PipeTransform {
+ constructor(
+ readonly httpClient: HttpClient,
+ readonly authService: AuthService,
+ @Inject(ACL_CONFIG) readonly acl: AclConfig,
+ ) {}
+
+ transform(value: string): Observable<boolean | void> {
+ return this.authService
+ .loadCachedUserProfile()
+ .pipe(
+ take(1),
+ map((userProfile) => {
+ const intersectionOfRoles = Object.keys(this.acl).filter(role => userProfile?.roles.includes(role));
+ for (const role of intersectionOfRoles) {
+ if (this.acl[role].includes(value)) {
+ return true;
+ }
+ }
+ return false;
+ }))
+ }
+}
diff --git a/src/app/pipes/in.pipe.ts b/src/app/pipes/in.pipe.ts
new file mode 100644
index 0000000..df6e8cc
--- /dev/null
+++ b/src/app/pipes/in.pipe.ts
@@ -0,0 +1,36 @@
+/*
+ * Copyright (c) 2022. Deutsche Telekom AG
+ *
+ * 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
+ *
+ * http://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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+
+import { Pipe, PipeTransform } from '@angular/core';
+
+@Pipe({
+ name: 'in',
+})
+export class InPipe implements PipeTransform {
+ transform(value: string, array: string[] | Set<any>): boolean {
+ if (array instanceof Array) {
+ return array.includes(value);
+ }
+ if (array instanceof Set) {
+ return array.has(value);
+ } else {
+ throw new Error('unsupported type');
+ }
+ }
+}
diff --git a/src/app/pipes/is-today.pipe.ts b/src/app/pipes/is-today.pipe.ts
new file mode 100644
index 0000000..1d2f17a
--- /dev/null
+++ b/src/app/pipes/is-today.pipe.ts
@@ -0,0 +1,37 @@
+/*
+ * Copyright (c) 2022. Deutsche Telekom AG
+ *
+ * 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
+ *
+ * http://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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+
+import { Pipe, PipeTransform } from '@angular/core';
+
+// return true if given parameter is today else return false
+
+@Pipe({
+ name: 'isToday',
+})
+export class IsTodayPipe implements PipeTransform {
+ transform(value: string | Date | number): boolean {
+ const date = new Date(value);
+ const today = new Date();
+ return (
+ date.getDate() == today.getDate() &&
+ date.getMonth() == today.getMonth() &&
+ date.getFullYear() == today.getFullYear()
+ );
+ }
+}
diff --git a/src/app/pipes/map.pipe.ts b/src/app/pipes/map.pipe.ts
new file mode 100644
index 0000000..e2c88a9
--- /dev/null
+++ b/src/app/pipes/map.pipe.ts
@@ -0,0 +1,48 @@
+/*
+ * Copyright (c) 2022. Deutsche Telekom AG
+ *
+ * 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
+ *
+ * http://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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+
+import { Pipe, PipeTransform } from '@angular/core';
+
+@Pipe({
+ name: 'map',
+ pure: false
+})
+/*
+ MapPipe allows us to run a function in a template
+ for example, you need to filter out some elements from the array before displaying these elements. You can call a function for getting filtered array
+ directly from template, but this function will be triggered everytime when user interacts with page.
+ MapPipe allows you to call a function through this pipe, so function will be called only when necessary.
+ Usage:
+ we have function in .ts file that's called filterZeroValues
+ in template we can use this function:
+ *ngFor="let item in elements | map : filterZeroValues"
+ where the first parameter of map pipe is function to be called, and other parameters will be passed as arguments to this function
+ Important note: as you can see from implementation, elements array will be passed to your function as a first argument
+*/
+export class MapPipe implements PipeTransform {
+
+ transform<T, R>(
+ thisArg: T,
+ project: (t:T, ...others: any[]) => R,
+ ...args: any[]
+ ): R {
+ return project(thisArg, ...args);
+ }
+
+}
diff --git a/src/app/pipes/translate-mock.pipe.ts b/src/app/pipes/translate-mock.pipe.ts
new file mode 100644
index 0000000..80c68a7
--- /dev/null
+++ b/src/app/pipes/translate-mock.pipe.ts
@@ -0,0 +1,42 @@
+/*
+ * Copyright (c) 2022. Deutsche Telekom AG
+ *
+ * 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
+ *
+ * http://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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+
+import { Pipe, PipeTransform } from "@angular/core";
+/**
+ * This class can be used to mock the `translate` pipe in jasmine test cases.
+ *
+ * Usage:
+ * ``` typescript
+ * TestBed.configureTestingModule({
+ declarations: [TranslatePipeMock,...],
+ providers: [{ provide: TranslatePipe, useClass: TranslatePipeMock },...]
+}).compileComponents();
+* ```
+*/
+// Courtesy of: https://github.com/ngx-translate/core/issues/636#issuecomment-451137902
+@Pipe({
+ name: 'translate'
+})
+export class TranslatePipeMock implements PipeTransform {
+ public name = 'translate';
+
+ public transform(query: string): any {
+ return query;
+ }
+}
diff --git a/src/app/router.strategy.ts b/src/app/router.strategy.ts
new file mode 100644
index 0000000..c80682e
--- /dev/null
+++ b/src/app/router.strategy.ts
@@ -0,0 +1,48 @@
+/*
+ * Copyright (c) 2022. Deutsche Telekom AG
+ *
+ * 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
+ *
+ * http://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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+
+import {ActivatedRouteSnapshot, DetachedRouteHandle, BaseRouteReuseStrategy} from '@angular/router';
+
+export class AppRouteReuseStrategy implements BaseRouteReuseStrategy {
+ public shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean {
+ if(future.data.reuseComponent) {
+ return false
+ }
+ return (future.routeConfig === curr.routeConfig);
+ }
+
+ retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle | null {
+ return true;
+ }
+
+ shouldAttach(route: ActivatedRouteSnapshot): boolean {
+ return false;
+ }
+
+ shouldDetach(route: ActivatedRouteSnapshot): boolean {
+ return false;
+ }
+
+ store(route: ActivatedRouteSnapshot, detachedTree: DetachedRouteHandle): void;
+ store(route: ActivatedRouteSnapshot, handle: DetachedRouteHandle | null): void;
+ store(route: ActivatedRouteSnapshot, detachedTree: DetachedRouteHandle | null): void {
+ //this is intentional
+ }
+
+}
diff --git a/src/app/services/auth.service.ts b/src/app/services/auth.service.ts
new file mode 100644
index 0000000..8988196
--- /dev/null
+++ b/src/app/services/auth.service.ts
@@ -0,0 +1,94 @@
+/*
+ * Copyright (c) 2022. Deutsche Telekom AG
+ *
+ * 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
+ *
+ * http://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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+
+import { Injectable } from '@angular/core';
+import { OAuthService, UserInfo } from 'angular-oauth2-oidc';
+import { catchError, filter, map } from 'rxjs/operators';
+import { NavigationStart, Router } from '@angular/router';
+import { AlertService } from '../modules/alerting';
+import { TranslateService } from '@ngx-translate/core';
+import { BehaviorSubject, EMPTY, Observable, of } from 'rxjs';
+
+/**
+ * Provides check for roles and token
+ */
+@Injectable({
+ providedIn: 'root',
+})
+export class AuthService {
+ userProfile$ = new BehaviorSubject<UserInfo | undefined>(undefined);
+ constructor(private readonly oauthService: OAuthService, private router: Router, private alertService: AlertService, private translateService: TranslateService) {
+ //renew cache every page reload
+ router.events.pipe(
+ filter((e): e is NavigationStart => e instanceof NavigationStart)
+ ).subscribe(() => {
+ try {
+ return this.loadUserProfile().then(userInfo => this.userProfile$.next(userInfo)).catch(e => {throw e})
+ }catch (e) {
+ this.alertService.error(this.translateService.instant('common.messages.keycloakAccessTokenNotValid'), {id: "keycloak", keepAfterRouteChange: true})
+ return Promise.resolve(null);
+ }
+ });
+ }
+
+ userProfileCache: UserInfo | undefined = undefined;
+
+ /**
+ * Convenience method of `hasValidAccessToken()` and `hasSufficientRoles()`
+ * Asynchronous because the needed UserInfo is fetched from Keycloak
+ */
+ hasPermissions(): Observable<boolean> {
+ if (this.hasValidAccessToken()) {
+ return this.hasSufficientRoles();
+ }
+ return of(false);
+ }
+
+ /**
+ * This answers: 'What if the user has an account, but without any permissions?'
+ * Asynchronous because the needed UserInfo is fetched from Keycloak
+ */
+ hasSufficientRoles(): Observable<boolean> {
+ return this.loadCachedUserProfile().pipe(map(info => info?.roles.join(',') !== 'offline_access'));
+ }
+
+ loadCachedUserProfile(): Observable<UserInfo | undefined> {
+ return this.userProfile$.pipe(
+ filter(userProfile => userProfile !== undefined),
+ catchError(err => {
+ console.error(err);
+ return EMPTY
+ }));
+ }
+
+ /**
+ * Wrapper for `hasValidAccessToken` from OAuthService
+ */
+ hasValidAccessToken(): boolean {
+ return this.oauthService.hasValidAccessToken();
+ }
+ /*
+ * Private method = should not be used outside of this class, because it triggers additional request for userprofile
+ * */
+ private loadUserProfile():Promise<UserInfo> {
+ // in version 12.2 loadUserProfile() Promise returns data of type object instead of UserInfo
+ //@ts-ignore
+ return this.oauthService.loadUserProfile().then(userInfo => userInfo.info as UserInfo)
+ }
+}
diff --git a/src/app/services/authconfig.service.ts b/src/app/services/authconfig.service.ts
new file mode 100644
index 0000000..5ced9a1
--- /dev/null
+++ b/src/app/services/authconfig.service.ts
@@ -0,0 +1,47 @@
+/*
+ * Copyright (c) 2022. Deutsche Telekom AG
+ *
+ * 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
+ *
+ * http://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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+
+import { Injectable } from '@angular/core';
+import { AuthConfig, NullValidationHandler, OAuthService } from 'angular-oauth2-oidc';
+
+@Injectable()
+export class AuthConfigService {
+ constructor(private readonly oauthService: OAuthService, private readonly authConfig: AuthConfig) {}
+
+ async initAuth(): Promise<any> {
+ return new Promise<void>((resolveFn, rejectFn) => {
+ // setup oauthService
+ this.oauthService.configure(this.authConfig);
+ this.oauthService.tokenValidationHandler = new NullValidationHandler();
+
+ this.oauthService.loadDiscoveryDocumentAndLogin().then(isLoggedIn => {
+ if (isLoggedIn) {
+ this.oauthService.setupAutomaticSilentRefresh();
+ resolveFn();
+ } else {
+ this.oauthService.initImplicitFlow();
+ rejectFn();
+ }
+ }).catch(() => {
+ //@ts-ignore
+ window.location.href = './keycloak-error.html'
+ });
+ });
+ }
+}
diff --git a/src/app/services/cacheservice/request-cache.service.spec.ts b/src/app/services/cacheservice/request-cache.service.spec.ts
new file mode 100644
index 0000000..c9f931e
--- /dev/null
+++ b/src/app/services/cacheservice/request-cache.service.spec.ts
@@ -0,0 +1,35 @@
+/*
+ * Copyright (c) 2022. Deutsche Telekom AG
+ *
+ * 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
+ *
+ * http://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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+
+import { TestBed } from '@angular/core/testing';
+
+import { RequestCacheService } from './request-cache.service';
+
+describe('RequestCacheService', () => {
+ let service: RequestCacheService;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({});
+ service = TestBed.inject(RequestCacheService);
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+});
diff --git a/src/app/services/cacheservice/request-cache.service.ts b/src/app/services/cacheservice/request-cache.service.ts
new file mode 100644
index 0000000..3d2047f
--- /dev/null
+++ b/src/app/services/cacheservice/request-cache.service.ts
@@ -0,0 +1,89 @@
+/*
+ * Copyright (c) 2022. Deutsche Telekom AG
+ *
+ * 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
+ *
+ * http://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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+
+import { HttpRequest, HttpResponse } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+
+const maxAge = 60000;
+
+@Injectable({
+ providedIn: 'root',
+})
+// https://github.com/angular/angular/blob/master/aio/content/examples/http/src/app/request-cache.service.ts
+export class RequestCacheService implements RequestCache {
+ // The cache is a Map of (most importantly but not exclusively) HttpResponses
+ cache = new Map<string, RequestCacheEntry>();
+
+ /**
+ * Get an http request from cache
+ * @param request the http request that should be retrieved from cache
+ */
+ get(request: HttpRequest<any>): HttpResponse<any> | undefined {
+ const requestUrl = request.urlWithParams;
+ const cachedResponse = this.cache.get(requestUrl);
+
+ if (!cachedResponse) {
+ return undefined;
+ }
+
+ const isExpired = cachedResponse.lastRead < Date.now() - maxAge;
+
+
+ return isExpired ? undefined : cachedResponse.response;
+ }
+
+ /**
+ * Put a http response for a given request url (taken from the request object) in the cache
+ * @param request the http request that should be associated with the http response
+ * @param response the http response that should be stored in cache
+ */
+ put(request: HttpRequest<any>, response: HttpResponse<any>): void {
+ const requestUrl = request.urlWithParams;
+
+ // Map a request url to an object
+ const newEntry = { requestUrl, response, lastRead: Date.now() };
+ this.cache.set(requestUrl, newEntry);
+
+ // Remove expired entries from the cache
+ const expired = Date.now() - maxAge;
+ this.cache.forEach(entry => {
+ if (entry.lastRead < expired) {
+ this.cache.delete(entry.requestUrl);
+ }
+ });
+ }
+}
+
+/**
+ * Service that manages the cache.
+ * `get()` HttpResponses from cache and `put()` responses into the cache
+ */
+export abstract class RequestCache {
+ abstract get(request: HttpRequest<any>): HttpResponse<any> | undefined;
+ abstract put(request: HttpRequest<any>, response: HttpResponse<any>): void;
+}
+
+/**
+ * Wrapper Object that stores the HttpResponse together with the `requestUrl` of the request and the `lastRead` time it was cached
+ */
+export interface RequestCacheEntry {
+ requestUrl: string;
+ response: HttpResponse<any>;
+ lastRead: number;
+}
diff --git a/src/app/services/fullscreen.service.ts b/src/app/services/fullscreen.service.ts
new file mode 100644
index 0000000..91ceec9
--- /dev/null
+++ b/src/app/services/fullscreen.service.ts
@@ -0,0 +1,58 @@
+/*
+ * Copyright (c) 2022. Deutsche Telekom AG
+ *
+ * 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
+ *
+ * http://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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+
+import { Injectable } from '@angular/core';
+
+@Injectable({
+ providedIn: 'root',
+})
+export class FullscreenService {
+ private doc = <FullScreenDocument>document;
+
+ enter() {
+ const el = this.doc.documentElement;
+ if (el.requestFullscreen) el.requestFullscreen();
+ else if (el.msRequestFullscreen) el.msRequestFullscreen();
+ else if (el.mozRequestFullScreen) el.mozRequestFullScreen();
+ else if (el.webkitRequestFullscreen) el.webkitRequestFullscreen();
+ }
+
+ leave() {
+ if (this.doc.exitFullscreen) this.doc.exitFullscreen();
+ else if (this.doc.msExitFullscreen) this.doc.msExitFullscreen();
+ else if (this.doc.mozCancelFullScreen) this.doc.mozCancelFullScreen();
+ else if (this.doc.webkitExitFullscreen) this.doc.webkitExitFullscreen();
+ }
+}
+
+interface FullScreenDocument extends HTMLDocument {
+ documentElement: FullScreenDocumentElement;
+ mozFullScreenElement?: Element;
+ msFullscreenElement?: Element;
+ webkitFullscreenElement?: Element;
+ msExitFullscreen?: () => void;
+ mozCancelFullScreen?: () => void;
+ webkitExitFullscreen?: () => void;
+}
+
+interface FullScreenDocumentElement extends HTMLElement {
+ msRequestFullscreen?: () => void;
+ mozRequestFullScreen?: () => void;
+ webkitRequestFullscreen?: () => void;
+}
diff --git a/src/app/services/history.service.ts b/src/app/services/history.service.ts
new file mode 100644
index 0000000..2924f39
--- /dev/null
+++ b/src/app/services/history.service.ts
@@ -0,0 +1,67 @@
+/*
+ * Copyright (c) 2022. Deutsche Telekom AG
+ *
+ * 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
+ *
+ * http://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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+
+import { Injectable } from '@angular/core';
+import {
+ ActionType,
+ CreateActionModel,
+ EntityType,
+ EntityUserHistoryActionModel,
+} from '../model/user-last-action.model';
+import { ActionsListResponse, ActionsResponse, ActionsService } from '../../../openapi/output';
+import { OAuthService } from 'angular-oauth2-oidc';
+import { Observable } from 'rxjs';
+
+@Injectable({
+ providedIn: 'root',
+})
+export class HistoryService {
+ constructor(private readonly actionService: ActionsService, private readonly oauthService: OAuthService) {}
+
+ public getUserActions(interval: number | undefined): Observable<ActionsListResponse> {
+ const userId = Object(this.oauthService.getIdentityClaims()).sub;
+ return this.actionService.getActions(userId, 1, 1000, interval);
+ }
+
+ public createUserHistoryAction(action: CreateActionModel<EntityUserHistoryActionModel>): Observable<ActionsResponse> {
+ let mappedAction = {
+ type: action.type,
+ entity: action.entity,
+ entityParams: {
+ userName: action.entityParams.userName,
+ userId: action.entityParams.userId,
+ },
+ };
+ return this.createAction(mappedAction);
+ }
+
+ private createAction(action: {
+ type: ActionType;
+ entity: EntityType;
+ entityParams: { [key: string]: string | undefined};
+ }): Observable<ActionsResponse> {
+ const userId = Object(this.oauthService.getIdentityClaims()).sub;
+ const actionCreatedAt = new Date().toISOString();
+ return this.actionService.createAction(userId, undefined, {
+ userId,
+ actionCreatedAt,
+ action,
+ });
+ }
+}
diff --git a/src/app/services/loading-indicator.service.ts b/src/app/services/loading-indicator.service.ts
new file mode 100644
index 0000000..727edd4
--- /dev/null
+++ b/src/app/services/loading-indicator.service.ts
@@ -0,0 +1,47 @@
+/*
+ * Copyright (c) 2022. Deutsche Telekom AG
+ *
+ * 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
+ *
+ * http://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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+
+import { Injectable } from '@angular/core';
+import { BehaviorSubject, Observable } from 'rxjs';
+import { debounceTime } from 'rxjs/operators';
+
+@Injectable()
+export class LoadingIndicatorService {
+ private isVisible$ = new BehaviorSubject<boolean>(false);
+ private timeDelay = 500;
+ private counter = 0;
+
+ show(): void {
+ this.counter++;
+ if (this.counter > 0) {
+ setTimeout(() => this.isVisible$.next(true), 0);
+ }
+ }
+
+ hide(): void {
+ this.counter--;
+ if (this.counter === 0) {
+ setTimeout(() => this.isVisible$.next(false), 0);
+ }
+ }
+
+ isVisible(): Observable<boolean> {
+ return this.isVisible$.pipe(debounceTime(this.timeDelay));
+ }
+}
diff --git a/src/app/services/logging.service.ts b/src/app/services/logging.service.ts
new file mode 100644
index 0000000..5250fda
--- /dev/null
+++ b/src/app/services/logging.service.ts
@@ -0,0 +1,34 @@
+/*
+ * Copyright (c) 2022. Deutsche Telekom AG
+ *
+ * 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
+ *
+ * http://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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+
+import { Injectable } from '@angular/core';
+import { HttpClient } from '@angular/common/http';
+import { Observable } from 'rxjs';
+import { environment } from '../../environments/environment';
+
+@Injectable({
+ providedIn: 'root',
+})
+export class LoggingService {
+ constructor(private readonly httpClient: HttpClient) {}
+
+ writeLog(message: string): Observable<string> {
+ return this.httpClient.post(environment.loggingUrl, message, { responseType: 'text' });
+ }
+}
diff --git a/src/app/services/shortcut.service.ts b/src/app/services/shortcut.service.ts
new file mode 100644
index 0000000..750ab5b
--- /dev/null
+++ b/src/app/services/shortcut.service.ts
@@ -0,0 +1,49 @@
+/*
+ * Copyright (c) 2022. Deutsche Telekom AG
+ *
+ * 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
+ *
+ * http://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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { Injectable } from '@angular/core';
+import { TranslateService } from '@ngx-translate/core';
+
+export enum KeyboardShortcuts {
+ SHORTCUT_0 = '0',
+ SHORTCUT_1 = '1',
+ SHORTCUT_2 = '2',
+ SHORTCUT_4 = '4',
+ SHORTCUT_6 = '6',
+}
+
+@Injectable({
+ providedIn: 'root'
+})
+
+export class ShortcutService {
+
+ private shortcuts = new Map<KeyboardShortcuts, string>([
+ [KeyboardShortcuts.SHORTCUT_0, this.translateService.instant('layout.header.shortcuts.details')],
+ [KeyboardShortcuts.SHORTCUT_1, this.translateService.instant('layout.header.shortcuts.home')],
+ [KeyboardShortcuts.SHORTCUT_2, this.translateService.instant('layout.header.shortcuts.main')],
+ [KeyboardShortcuts.SHORTCUT_4, this.translateService.instant('layout.header.shortcuts.search')],
+ [KeyboardShortcuts.SHORTCUT_6, this.translateService.instant('layout.header.shortcuts.menu')],
+ ]);
+
+ constructor(private translateService: TranslateService) { }
+
+ public getShortcuts(): Map<KeyboardShortcuts,string> {
+ return this.shortcuts;
+ }
+}
diff --git a/src/app/services/tileservice/tiles.service.spec.ts b/src/app/services/tileservice/tiles.service.spec.ts
new file mode 100644
index 0000000..f7f4369
--- /dev/null
+++ b/src/app/services/tileservice/tiles.service.spec.ts
@@ -0,0 +1,488 @@
+/*
+ * Copyright (c) 2022. Deutsche Telekom AG
+ *
+ * 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
+ *
+ * http://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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+
+import { TestBed, waitForAsync } from '@angular/core/testing';
+import { Tile } from '../../model/tile';
+import { TilesService } from './tiles.service';
+import { HttpErrorResponse } from '@angular/common/http';
+import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
+import { environment } from '../../../environments/environment';
+import { OAuthLogger, OAuthService, UrlHelperService } from 'angular-oauth2-oidc';
+
+// https://dev.to/coly010/unit-testing-angular-services-1anm
+
+/**
+ * describe sets up the Test Suite for the TileService
+ */
+describe('TilesService', () => {
+ /**
+ * let service declares a Test Suite-scoped variable where we will store a reference to our service
+ */
+ let service: TilesService;
+ let mockTile: Tile;
+ let httpmock: HttpTestingController;
+ let errmsg: string;
+
+ const backendServerUrlTest = environment.backendServerUrl + '/tiles';
+ /**
+ * beforeEach tells the test runner to run this code before every test in the Test Suite
+ * It is using Angular's TestBed to create the testing environment. Finally it is injecting the TilesService
+ * and placing a reference to it in the service variable defined earlier.
+ */
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ imports: [HttpClientTestingModule],
+ providers: [TilesService, OAuthService, UrlHelperService, OAuthLogger],
+ });
+ service = TestBed.inject(TilesService);
+ httpmock = TestBed.inject(HttpTestingController);
+ mockTile = {
+ id: 1,
+ title: 'NewTile1',
+ description: 'New Tile for frontend test',
+ imageUrl: 'https://www.onap.org/wp-content/uploads/sites/20/2017/02/logo_onap_2017.png',
+ imageAltText: 'Onap Image',
+ redirectUrl: 'www.onap.org',
+ headers: 'This is a header',
+ groups: [],
+ roles: [],
+ };
+ // responseTile = {
+ // id: 2,
+ // title: 'NewTile1',
+ // description: 'New Tile for frontend test',
+ // imageUrl: 'https://www.onap.org/wp-content/uploads/sites/20/2017/02/logo_onap_2017.png',
+ // imageAltText: 'Onap Image',
+ // redirectUrl: 'www.onap.org',
+ // headers: 'This is a header',
+ // groups: [],
+ // roles: [],
+ // };
+ });
+
+ /**
+ * After every test, assert that there are no more pending requests.
+ */
+ afterEach(() => {
+ httpmock.verify();
+ });
+
+ /**
+ * the it() function creates a new test with the title 'should be created'
+ * This test is expecting the service varibale to truthy, in otherwords,
+ * it should have been instantiated correctly by the Angular TestBed.
+ */
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+
+ /**
+ * TileService method tests begin
+ * Testing getTiles
+ */
+ describe('#getTiles', () => {
+ // let expectedTiles: Tile[];
+
+ beforeEach(() => {
+ // expectedTiles = [mockTile, responseTile];
+ });
+
+ /**
+ * testing method getTiles() to get all existing tiles
+ */
+ /*
+ it('should return expected tiles (called once)', (done: DoneFn) => {
+ service.getTiles().subscribe(response => {
+ expect(response).toEqual(expectedTiles, 'should return expected tiles');
+ // done() to be called after asynchronous calls (https://angular.io/guide/testing-services)
+ done();
+ });
+
+ // TileService should have made one request to GET tiles from expected URL
+ const req = httpmock.expectOne(backendServerUrlTest);
+ expect(req.request.method).toEqual('GET');
+
+ // Respond with the expected mock tiles
+ req.flush(expectedTiles);
+ });
+*/
+ /**
+ * TODO: Maybe it makes sense to inform the user that no tiles are displayed
+ * testing method getTiles() in case there are no tiles in the database
+ */
+ /*
+ it('should be OK returning no tiles', (done: DoneFn) => {
+ service.getTiles().subscribe(response => {
+ expect(response.length).toEqual(0, 'should have empty tiles array');
+ done();
+ });
+
+ const req = httpmock.expectOne(backendServerUrlTest);
+ expect(req.request.method).toEqual('GET');
+
+ req.flush([]); // Respond with no tile
+ });
+*/
+ /**
+ * testing method getTiles() in case the backend responds with 404 Not Found
+ * This service reports the error but finds a way to let the app keep going.
+ */
+
+ /*
+ it('should handle 404 error', (done: DoneFn) => {
+ errmsg = '404 error';
+
+ service.getTiles().subscribe(
+ response => fail('should fail with the 404 error'),
+ (err: HttpErrorResponse) => {
+ expect(err.status).toEqual(404);
+ expect(err.error).toEqual(errmsg);
+ },
+ );
+
+ // Make an HTTP Get Request
+ // service.getTiles().then(
+ // response => fail('should have failed with the 404 error'),
+ // (err: HttpErrorResponse) => {
+ // expect(err.status).toEqual(404);
+ // expect(err.error).toEqual(errmsg);
+ // }
+ // );
+
+ const req = httpmock.expectOne(backendServerUrlTest);
+ expect(req.request.method).toEqual('GET');
+
+ // respond with a 404 and the error message in the body --> TODO Frontend GUI must react correctly
+ req.flush(errmsg, { status: 404, statusText: 'Not Found' });
+ });
+ */
+
+ /**
+ * testing getTiles() when method is called multiple times
+ * TODO: expect cached results
+ */
+ /*
+ it('should return expected tiles (called multiple times)', () => {
+ service.getTiles().subscribe();
+ service.getTiles().subscribe();
+ service.getTiles().subscribe(response => {
+ expect(response).toEqual(expectedTiles, 'should return expected tiles');
+ });
+
+ const req = httpmock.match(backendServerUrlTest);
+ expect(req.length).toEqual(3, 'calls to getTiles()');
+
+ // Respond to each request with different mock tile results
+ req[0].flush([]);
+ req[1].flush([mockTile]);
+ req[2].flush(expectedTiles);
+ });*/
+ });
+
+ /**
+ * Tests for getTileByID()
+ */
+ describe('#getTileByID', () => {
+ /**
+ * testing method getTilesById() to return the specific tile with right id
+ */
+ it('should return expected tile by id', () => {
+ service.getTileById(mockTile.id).then(response => {
+ expect(response).toEqual(mockTile, 'should return expected tile');
+ });
+
+ const req = httpmock.expectOne(backendServerUrlTest + '/' + mockTile.id);
+ expect(req.request.method).toEqual('GET');
+
+ // Respond with the mock tiles
+ req.flush(mockTile);
+ });
+
+ /**
+ * testing method getTileByID() in case the backend responds with 404 Not Found and the tile does not exist
+ */
+ it(
+ 'getTileById(): should handle 404 error',
+ waitForAsync(() => {
+ errmsg = '404 error';
+ // Make an HTTP Get Request
+ service.getTileById(mockTile.id).then(
+ () => fail('should have failed with the 404 error'),
+ (err: HttpErrorResponse) => {
+ expect(err.status).toEqual(404);
+ expect(err.error).toEqual(errmsg);
+ },
+ );
+
+ const req = httpmock.expectOne(backendServerUrlTest + '/' + mockTile.id);
+ expect(req.request.method).toEqual('GET');
+
+ req.flush(errmsg, { status: 404, statusText: 'Not Found' });
+ }),
+ );
+ });
+ /**
+ * Tests for update an existing tile
+ */
+ describe('#updateTiles', () => {
+ /**
+ * testing method updateTiles()
+ */
+ it('should update a tile and return it', () => {
+ mockTile.title = 'Update title';
+
+ service.updateTiles(mockTile).then(response => {
+ expect(response.title).toEqual('Update title', 'should return tile');
+ });
+ // TileService should have made one request to PUT
+ const req = httpmock.expectOne(backendServerUrlTest + '/' + mockTile.id);
+ expect(req.request.method).toEqual('PUT');
+ expect(req.request.body).toEqual(mockTile);
+
+ req.flush(mockTile);
+ });
+
+ /**
+ * testing method updateTiles() in case the backend responds with 404 Not Found and the tile does not exist
+ */
+ it(
+ 'updateTiles(): should handle 404 error',
+ waitForAsync(() => {
+ errmsg = '404 error';
+ // Make an HTTP Get Request
+ service.updateTiles(mockTile).then(
+ () => fail('should have failed with the 404 error'),
+ (err: HttpErrorResponse) => {
+ expect(err.status).toEqual(404);
+ expect(err.error).toEqual(errmsg);
+ },
+ );
+
+ const req = httpmock.expectOne(backendServerUrlTest + '/' + mockTile.id);
+ expect(req.request.method).toEqual('PUT');
+
+ req.flush(errmsg, { status: 404, statusText: 'Not Found' });
+ }),
+ );
+
+ /**
+ * testing method updateTiles() in case the backend responds with 401 Unauthorized
+ */
+ it(
+ 'updateTiles(): should handle 401 error',
+ waitForAsync(() => {
+ errmsg = '401 error';
+ // Make an HTTP Get Request
+ service.updateTiles(mockTile).then(
+ () => fail('should have failed with the 401 error'),
+ (err: HttpErrorResponse) => {
+ expect(err.status).toEqual(401);
+ expect(err.error).toEqual(errmsg);
+ },
+ );
+
+ const req = httpmock.expectOne(backendServerUrlTest + '/' + mockTile.id);
+ expect(req.request.method).toEqual('PUT');
+
+ req.flush(errmsg, { status: 401, statusText: 'Not Found' });
+ }),
+ );
+
+ /**
+ * testing method updateTiles() in case the backend responds with 403 Forbidden
+ */
+ it(
+ 'updateTiles(): should handle 403 error',
+ waitForAsync(() => {
+ errmsg = '403 error';
+ // Make an HTTP Get Request
+ service.updateTiles(mockTile).then(
+ () => fail('should have failed with the 404 error'),
+ (err: HttpErrorResponse) => {
+ expect(err.status).toEqual(403);
+ },
+ );
+
+ const req = httpmock.expectOne(backendServerUrlTest + '/' + mockTile.id);
+ expect(req.request.method).toEqual('PUT');
+
+ req.flush(errmsg, { status: 403, statusText: 'Not Found' });
+ }),
+ );
+ });
+ /*
+ * Test save a new tile
+ */
+ describe('#saveTiles', () => {
+ /*
+ * testing saveTiles() to save a new tile
+ */
+ it(
+ 'should save a tile correctly (mocked http post request)',
+ waitForAsync(() => {
+ service.saveTiles(mockTile).then(response => {
+ expect(response.id).toBe(1);
+ expect(response.title).toBe('NewTile1');
+ expect(response.redirectUrl).toBe('www.onap.org');
+ expect(response.imageAltText).toBe('Onap Image');
+ expect(response.imageUrl).toBe('https://www.onap.org/wp-content/uploads/sites/20/2017/02/logo_onap_2017.png');
+ expect(response.description).toBe('New Tile for frontend test');
+ expect(response.headers).toBe('This is a header');
+ });
+ /*
+ * Checking that there ist just one request and check the type of request
+ * 'flush'/ respond with mock data, run then-block in line 64 and check the except commands
+ */
+ const req = httpmock.expectOne(backendServerUrlTest);
+ expect(req.request.method).toEqual('POST');
+ req.flush(mockTile);
+ }),
+ );
+ /**
+ * testing method saveTiles() in case the backend answers with an 401 responds
+ */
+ it(
+ 'saveTiles(): should handle 401 error',
+ waitForAsync(() => {
+ errmsg = '401 error';
+ // Make an HTTP Get Request
+ service.saveTiles(mockTile).then(
+ () => fail('should have failed with the 401 error'),
+ (err: HttpErrorResponse) => {
+ expect(err.status).toEqual(401);
+ expect(err.error).toEqual(errmsg);
+ },
+ );
+
+ const req = httpmock.expectOne(backendServerUrlTest);
+ expect(req.request.method).toEqual('POST');
+
+ req.flush(errmsg, { status: 401, statusText: 'Not Found' });
+ }),
+ );
+
+ it(
+ 'saveTiles(): should handle 403 error',
+ waitForAsync(() => {
+ errmsg = '403 error';
+ // Make an HTTP Get Request
+ service.saveTiles(mockTile).then(
+ () => fail('should have failed with the 401 error'),
+ (err: HttpErrorResponse) => {
+ expect(err.status).toEqual(403);
+ expect(err.error).toEqual(errmsg);
+ },
+ );
+
+ const req = httpmock.expectOne(backendServerUrlTest);
+ expect(req.request.method).toEqual('POST');
+
+ req.flush(errmsg, { status: 403, statusText: 'Forbidden' });
+ }),
+ );
+ });
+ /**
+ * testing delete a tile
+ */
+ describe('#deleteTiles', () => {
+ /**
+ * testing method deleteTile()
+ */
+ it(
+ 'should delete a tile correctly (mocked http delete request)',
+ waitForAsync(() => {
+ service.deleteTile(mockTile).then(response => {
+ expect(response).toBeDefined();
+ });
+ const req = httpmock.expectOne(environment.backendServerUrl + '/tiles/' + mockTile.id);
+ expect(req.request.method).toEqual('DELETE');
+ req.flush({});
+ }),
+ );
+
+ /**
+ * testing method deleteTiles() in case the backend responds with 404 Not Found and the tile does not exist
+ */
+ it(
+ 'deleteTiles(): should handle 404 error',
+ waitForAsync(() => {
+ errmsg = '404 error';
+ // Make an HTTP Get Request
+ service.deleteTile(mockTile).then(
+ () => fail('should have failed with the 404 error'),
+ (err: HttpErrorResponse) => {
+ expect(err.status).toEqual(404);
+ expect(err.error).toEqual(errmsg);
+ },
+ );
+
+ const req = httpmock.expectOne(backendServerUrlTest + '/' + mockTile.id);
+ expect(req.request.method).toEqual('DELETE');
+
+ req.flush(errmsg, { status: 404, statusText: 'Not Found' });
+ }),
+ );
+
+ /**
+ * testing method deleteTiles() in case the backend responds with 401 Unauthorized
+ */
+ it(
+ 'deleteTiles(): should handle 401 error',
+ waitForAsync(() => {
+ errmsg = '401 error';
+ // Make an HTTP Get Request
+ service.deleteTile(mockTile).then(
+ () => fail('should have failed with the 401 error'),
+ (err: HttpErrorResponse) => {
+ expect(err.status).toEqual(401);
+ expect(err.error).toEqual(errmsg);
+ },
+ );
+
+ const req = httpmock.expectOne(backendServerUrlTest + '/' + mockTile.id);
+ expect(req.request.method).toEqual('DELETE');
+
+ req.flush(errmsg, { status: 401, statusText: 'Unauthorized' });
+ }),
+ );
+
+ /**
+ * testing method deleteTiles() in case the backend responds with 403 Forbidden
+ */
+ it(
+ 'deleteTiles(): should handle 403 error',
+ waitForAsync(() => {
+ errmsg = '403 error';
+ // Make an HTTP Get Request
+ service.deleteTile(mockTile).then(
+ () => fail('should have failed with the 404 error'),
+ (err: HttpErrorResponse) => {
+ expect(err.status).toEqual(403);
+ expect(err.error).toEqual(errmsg);
+ },
+ );
+
+ const req = httpmock.expectOne(backendServerUrlTest + '/' + mockTile.id);
+ expect(req.request.method).toEqual('DELETE');
+
+ req.flush(errmsg, { status: 403, statusText: 'Forbidden' });
+ }),
+ );
+ });
+});
diff --git a/src/app/services/tileservice/tiles.service.ts b/src/app/services/tileservice/tiles.service.ts
new file mode 100644
index 0000000..167e42a
--- /dev/null
+++ b/src/app/services/tileservice/tiles.service.ts
@@ -0,0 +1,88 @@
+/*
+ * Copyright (c) 2022. Deutsche Telekom AG
+ *
+ * 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
+ *
+ * http://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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+
+import { HttpClient, HttpHeaders } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+import { Observable } from 'rxjs';
+import { environment } from 'src/environments/environment';
+import { Tile, TilesListResponse } from '../../model/tile';
+import { map } from 'rxjs/operators';
+
+export const urlTileApi = environment.backendServerUrl + '/tiles';
+
+@Injectable({
+ providedIn: 'root',
+})
+// Tutorial on the http client: https://angular.io/tutorial/toh-pt6#get-heroes-with-httpclient
+export class TilesService {
+ constructor(private httpClient: HttpClient) {}
+ /**
+ * GET tiles from the server
+ */
+ getTiles(refresh = false): Observable<Tile[]> {
+ if (refresh) {
+ const headers = new HttpHeaders({ 'x-refresh': 'true' });
+ return this.httpClient
+ .get<TilesListResponse>(urlTileApi, { headers })
+ .pipe(map(tilesListResponse => tilesListResponse.items));
+ }
+
+ return this.httpClient.get<TilesListResponse>(urlTileApi).pipe(map(tilesListResponse => tilesListResponse.items));
+ }
+
+ /**
+ * GET tile by id
+ * @param id to get specific tile
+ */
+ getTileById(id: number): Promise<Tile> {
+ return this.httpClient.get<Tile>(urlTileApi + '/' + id).toPromise();
+ }
+
+ /**
+ * POST: add a new tile to the database
+ * @param tile
+ * @returns the new saved tile
+ */
+ saveTiles(tile: Tile): Promise<Tile> {
+ const options = {
+ headers: new HttpHeaders({ 'Content-Type': 'application/json' }),
+ };
+ return this.httpClient.post<Tile>(urlTileApi, tile, options).toPromise();
+ }
+
+ /**
+ * PUT: update the tile on the server
+ * @returns the updated hero
+ * @param tile
+ */
+ updateTiles(tile: Tile): Promise<Tile> {
+ const options = {
+ headers: new HttpHeaders({ 'Content-Type': 'application/json' }),
+ };
+ return this.httpClient.put<Tile>(urlTileApi + '/' + tile.id, tile, options).toPromise();
+ }
+
+ /**
+ * DELETE: delete the tile from the server
+ * @param tile to delete
+ */
+ deleteTile(tile: Tile): Promise<void> {
+ return this.httpClient.delete<void>(urlTileApi + '/' + tile.id).toPromise();
+ }
+}
diff --git a/src/app/services/unsubscribe/unsubscribe.service.ts b/src/app/services/unsubscribe/unsubscribe.service.ts
new file mode 100644
index 0000000..b27f6d8
--- /dev/null
+++ b/src/app/services/unsubscribe/unsubscribe.service.ts
@@ -0,0 +1,32 @@
+/*
+ * Copyright (c) 2022. Deutsche Telekom AG
+ *
+ * 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
+ *
+ * http://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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+
+import { Injectable, OnDestroy } from '@angular/core';
+import { Subject } from 'rxjs';
+
+@Injectable()
+export class UnsubscribeService implements OnDestroy {
+ private readonly sub$ = new Subject<void>();
+ public readonly unsubscribe$ = this.sub$.asObservable();
+
+ ngOnDestroy(): void {
+ this.sub$.next();
+ this.sub$.complete();
+ }
+}
diff --git a/src/app/services/user-settings.service.ts b/src/app/services/user-settings.service.ts
new file mode 100644
index 0000000..cbaa992
--- /dev/null
+++ b/src/app/services/user-settings.service.ts
@@ -0,0 +1,111 @@
+/*
+ * Copyright (c) 2022. Deutsche Telekom AG
+ *
+ * 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
+ *
+ * http://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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+
+import { distinctUntilChanged, map, pluck, switchMap, take } from 'rxjs/operators';
+import { PreferencesResponse, PreferencesService } from '../../../openapi/output';
+import {
+ DashboardAppsModel,
+ DashboardModel,
+ DashboardTileSettings,
+ defaultUserSettings,
+ LastUserActionSettings, STATE_KEYS,
+ UpdateUserPreferenceModel,
+ UserPreferencesModel,
+} from '../model/user-preferences.model';
+import { BehaviorSubject, Observable, pipe, UnaryFunction } from 'rxjs';
+import { mergeWith as _mergeWith, isObject as _isObject } from 'lodash';
+import { isString } from '../helpers/helpers';
+import { Injectable } from '@angular/core';
+
+@Injectable({
+ providedIn: 'root',
+})
+export class UserSettingsService {
+ private userSettings: UserPreferencesModel = defaultUserSettings;
+ private preferencesTracker$ = new BehaviorSubject<UserPreferencesModel>(this.userSettings);
+
+ constructor(private preferencesService: PreferencesService) {
+ this.getPreferences();
+ }
+
+ getPreferences$(): Observable<UserPreferencesModel> {
+ return this.preferencesTracker$.asObservable();
+ }
+
+ selectDashboard = () =>
+ this.getPreferences$().pipe(selectDistinctState<UserPreferencesModel, DashboardModel>(STATE_KEYS.DASHBOARD));
+ selectDashboardApps = () =>
+ this.selectDashboard().pipe(selectDistinctState<DashboardModel, DashboardAppsModel>(STATE_KEYS.APPS));
+ selectDashboardAvailableTiles = () =>
+ this.selectDashboardApps().pipe(selectDistinctState<DashboardAppsModel, DashboardTileSettings[]>(STATE_KEYS.TILES));
+ selectLastUserAction = () =>
+ this.selectDashboardApps().pipe(
+ selectDistinctState<DashboardAppsModel, LastUserActionSettings>(STATE_KEYS.USER_ACTIONS),
+ );
+
+ getPreferences(): void {
+ this.preferencesService
+ .getPreferences()
+ .pipe(
+ map(preferences => {
+ return _mergeWith({}, defaultUserSettings, preferences.properties, (objValue, srcValue) => {
+ if (
+ (Array.isArray(srcValue) && !srcValue.some(_isObject)) ||
+ isString(srcValue) ||
+ typeof srcValue === 'boolean' ||
+ Number.isInteger(srcValue)
+ ) {
+ return srcValue;
+ }
+ }) as UserPreferencesModel;
+ }),
+ )
+ .subscribe(userPreferences => {
+ this.preferencesTracker$.next(userPreferences);
+ });
+ }
+
+ updatePreferences(preferences: UpdateUserPreferenceModel): Observable<PreferencesResponse> {
+ return this.getPreferences$().pipe(
+ take(1),
+ switchMap(data => {
+ const properties = _mergeWith({}, data, preferences, (objValue, srcValue) => {
+ if (
+ Array.isArray(srcValue) ||
+ isString(srcValue) ||
+ typeof srcValue === 'boolean' ||
+ Number.isInteger(srcValue)
+ ) {
+ return srcValue;
+ }
+ }) as UserPreferencesModel;
+ this.preferencesTracker$.next(properties);
+ return this.preferencesService.savePreferences({ properties });
+ }),
+ );
+ }
+
+ removePreferences(): Observable<PreferencesResponse> {
+ return this.preferencesService.updatePreferences({ properties: {} });
+ }
+}
+
+export function selectDistinctState<T, I>(key: string): UnaryFunction<Observable<T>, Observable<I>> {
+ return pipe(pluck<T, I>(key), distinctUntilChanged<I>());
+}
diff --git a/src/app/shared.module.ts b/src/app/shared.module.ts
new file mode 100644
index 0000000..eef81c0
--- /dev/null
+++ b/src/app/shared.module.ts
@@ -0,0 +1,78 @@
+/*
+ * Copyright (c) 2022. Deutsche Telekom AG
+ *
+ * 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
+ *
+ * http://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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { CommonModule } from '@angular/common';
+import { FormsModule, ReactiveFormsModule } from '@angular/forms';
+import { TableSkeletonComponent } from './components/shared/table-skeleton/table-skeleton.component';
+import { PaginationComponent } from './components/shared/pagination/pagination.component';
+import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
+import { BreadcrumbComponent } from './components/shared/breadcrumb/breadcrumb.component';
+import { BreadcrumbItemComponent } from './components/shared/breadcrumb-item/breadcrumb-item.component';
+import { NgModule } from '@angular/core';
+import { HasPermissionPipe } from './pipes/has-permission.pipe';
+import { HasPermissionsDirective } from './directives/has-permissions.directive';
+import { ColonPipe } from './pipes/colon.pipe';
+import { DragDropModule } from '@angular/cdk/drag-drop';
+import { I18nModule } from './modules/i18n/i18n.module';
+import { AlertModule } from './modules/alerting';
+import { TranslateModule } from '@ngx-translate/core';
+import { LoadingSpinnerComponent } from './components/shared/loading-spinner/loading-spinner.component';
+import { InPipe } from 'src/app/pipes/in.pipe';
+import { IsTodayPipe } from 'src/app/pipes/is-today.pipe';
+import { MapPipe } from 'src/app/pipes/map.pipe';
+
+@NgModule({
+ imports: [CommonModule, NgbModule, I18nModule, FormsModule, ReactiveFormsModule, AlertModule, TranslateModule],
+ declarations: [
+ HasPermissionPipe,
+ ColonPipe,
+ InPipe,
+ IsTodayPipe,
+ MapPipe,
+ HasPermissionsDirective,
+ TableSkeletonComponent,
+ PaginationComponent,
+ BreadcrumbComponent,
+ BreadcrumbItemComponent,
+ LoadingSpinnerComponent,
+ ],
+ exports: [
+ CommonModule,
+ FormsModule,
+ NgbModule,
+ FormsModule,
+ ReactiveFormsModule,
+ DragDropModule,
+ I18nModule,
+ FormsModule,
+ ReactiveFormsModule,
+ AlertModule,
+ HasPermissionPipe,
+ ColonPipe,
+ InPipe,
+ IsTodayPipe,
+ MapPipe,
+ HasPermissionsDirective,
+ PaginationComponent,
+ TableSkeletonComponent,
+ BreadcrumbComponent,
+ BreadcrumbItemComponent,
+ LoadingSpinnerComponent,
+ ],
+})
+export class SharedModule {}
diff --git a/src/app/tilesmock.ts b/src/app/tilesmock.ts
new file mode 100644
index 0000000..fc5aa40
--- /dev/null
+++ b/src/app/tilesmock.ts
@@ -0,0 +1,41 @@
+/*
+ * Copyright (c) 2022. Deutsche Telekom AG
+ *
+ * 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
+ *
+ * http://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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+
+import { Tile } from './model/tile';
+
+export const TILESMOCK: Tile[] = [
+ {
+ id: 1,
+ title: 'tile1',
+ image_url: 'tile1.url',
+ image_alt_text: 'tile1',
+ description: 'tile1 desc',
+ redirect_url: 'redirect_url',
+ headers: 'header tile1',
+ },
+ {
+ id: 2,
+ title: 'tile2',
+ image_url: 'tile2.url',
+ image_alt_text: 'tile2',
+ description: 'tile2 desc',
+ redirect_url: 'redirect_url',
+ headers: 'header tile2',
+ },
+];
diff --git a/src/assets/.gitkeep b/src/assets/.gitkeep
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/assets/.gitkeep
diff --git a/src/assets/acl.json b/src/assets/acl.json
new file mode 100644
index 0000000..73c2fe9
--- /dev/null
+++ b/src/assets/acl.json
@@ -0,0 +1,44 @@
+{
+ "onap_admin": [
+ "users.administration.list",
+ "users.administration.detail",
+ "users.administration.create",
+ "users.administration.edit",
+ "users.administration.delete",
+ "cellSite.map",
+ "serviceInstance.list",
+ "serviceInstance.delete",
+ "serviceModel.deployment",
+ "alarm.list",
+ "topology.instance.view",
+ "kpi",
+ "kpi.graphs",
+ "kpi.grafana",
+ "dashboard.tile.ALARM_COUNT_TILE",
+ "dashboard.tile.KPI_GRAPH_TILE",
+ "dashboard.tile.USER_LAST_ACTION_TILE",
+ "treeview"
+ ],
+ "onap_operator": [
+ "cellSite.map",
+ "serviceInstance.list",
+ "serviceInstance.delete",
+ "serviceModel.deployment",
+ "alarm.list",
+ "topology.instance.view",
+ "kpi",
+ "kpi.graphs",
+ "kpi.grafana",
+ "dashboard.tile.ALARM_COUNT_TILE",
+ "dashboard.tile.KPI_GRAPH_TILE",
+ "dashboard.tile.USER_LAST_ACTION_TILE",
+ "treeview"
+ ],
+ "onap_designer": [
+ "kpi",
+ "kpi.graphs",
+ "kpi.grafana",
+ "dashboard.tile.KPI_GRAPH_TILE",
+ "dashboard.tile.USER_LAST_ACTION_TILE"
+ ]
+}
diff --git a/src/assets/css/bootstrap-icons.css b/src/assets/css/bootstrap-icons.css
new file mode 100644
index 0000000..5cc17e5
--- /dev/null
+++ b/src/assets/css/bootstrap-icons.css
@@ -0,0 +1,5501 @@
+
+
+@font-face {
+ font-family: 'bootstrap-icons';
+ src: url('../fonts/bootstrap-icons.woff2?856008caa5eb66df68595e734e59580d') format('woff2'),
+ url('../fonts/bootstrap-icons.woff?856008caa5eb66df68595e734e59580d') format('woff');
+}
+
+[class^='bi-']::before,
+[class*=' bi-']::before {
+ display: inline-block;
+ font-family: bootstrap-icons !important;
+ font-style: normal;
+ font-weight: normal !important;
+ font-variant: normal;
+ text-transform: none;
+ line-height: 1;
+ vertical-align: -0.125em;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+.bi-alarm-fill::before {
+ content: '\f101';
+}
+
+.bi-alarm::before {
+ content: '\f102';
+}
+
+.bi-align-bottom::before {
+ content: '\f103';
+}
+
+.bi-align-center::before {
+ content: '\f104';
+}
+
+.bi-align-end::before {
+ content: '\f105';
+}
+
+.bi-align-middle::before {
+ content: '\f106';
+}
+
+.bi-align-start::before {
+ content: '\f107';
+}
+
+.bi-align-top::before {
+ content: '\f108';
+}
+
+.bi-alt::before {
+ content: '\f109';
+}
+
+.bi-app-indicator::before {
+ content: '\f10a';
+}
+
+.bi-app::before {
+ content: '\f10b';
+}
+
+.bi-archive-fill::before {
+ content: '\f10c';
+}
+
+.bi-archive::before {
+ content: '\f10d';
+}
+
+.bi-arrow-90deg-down::before {
+ content: '\f10e';
+}
+
+.bi-arrow-90deg-left::before {
+ content: '\f10f';
+}
+
+.bi-arrow-90deg-right::before {
+ content: '\f110';
+}
+
+.bi-arrow-90deg-up::before {
+ content: '\f111';
+}
+
+.bi-arrow-bar-down::before {
+ content: '\f112';
+}
+
+.bi-arrow-bar-left::before {
+ content: '\f113';
+}
+
+.bi-arrow-bar-right::before {
+ content: '\f114';
+}
+
+.bi-arrow-bar-up::before {
+ content: '\f115';
+}
+
+.bi-arrow-clockwise::before {
+ content: '\f116';
+}
+
+.bi-arrow-counterclockwise::before {
+ content: '\f117';
+}
+
+.bi-arrow-down-circle-fill::before {
+ content: '\f118';
+}
+
+.bi-arrow-down-circle::before {
+ content: '\f119';
+}
+
+.bi-arrow-down-left-circle-fill::before {
+ content: '\f11a';
+}
+
+.bi-arrow-down-left-circle::before {
+ content: '\f11b';
+}
+
+.bi-arrow-down-left-square-fill::before {
+ content: '\f11c';
+}
+
+.bi-arrow-down-left-square::before {
+ content: '\f11d';
+}
+
+.bi-arrow-down-left::before {
+ content: '\f11e';
+}
+
+.bi-arrow-down-right-circle-fill::before {
+ content: '\f11f';
+}
+
+.bi-arrow-down-right-circle::before {
+ content: '\f120';
+}
+
+.bi-arrow-down-right-square-fill::before {
+ content: '\f121';
+}
+
+.bi-arrow-down-right-square::before {
+ content: '\f122';
+}
+
+.bi-arrow-down-right::before {
+ content: '\f123';
+}
+
+.bi-arrow-down-short::before {
+ content: '\f124';
+}
+
+.bi-arrow-down-square-fill::before {
+ content: '\f125';
+}
+
+.bi-arrow-down-square::before {
+ content: '\f126';
+}
+
+.bi-arrow-down-up::before {
+ content: '\f127';
+}
+
+.bi-arrow-down::before {
+ content: '\f128';
+}
+
+.bi-arrow-left-circle-fill::before {
+ content: '\f129';
+}
+
+.bi-arrow-left-circle::before {
+ content: '\f12a';
+}
+
+.bi-arrow-left-right::before {
+ content: '\f12b';
+}
+
+.bi-arrow-left-short::before {
+ content: '\f12c';
+}
+
+.bi-arrow-left-square-fill::before {
+ content: '\f12d';
+}
+
+.bi-arrow-left-square::before {
+ content: '\f12e';
+}
+
+.bi-arrow-left::before {
+ content: '\f12f';
+}
+
+.bi-arrow-repeat::before {
+ content: '\f130';
+}
+
+.bi-arrow-return-left::before {
+ content: '\f131';
+}
+
+.bi-arrow-return-right::before {
+ content: '\f132';
+}
+
+.bi-arrow-right-circle-fill::before {
+ content: '\f133';
+}
+
+.bi-arrow-right-circle::before {
+ content: '\f134';
+}
+
+.bi-arrow-right-short::before {
+ content: '\f135';
+}
+
+.bi-arrow-right-square-fill::before {
+ content: '\f136';
+}
+
+.bi-arrow-right-square::before {
+ content: '\f137';
+}
+
+.bi-arrow-right::before {
+ content: '\f138';
+}
+
+.bi-arrow-up-circle-fill::before {
+ content: '\f139';
+}
+
+.bi-arrow-up-circle::before {
+ content: '\f13a';
+}
+
+.bi-arrow-up-left-circle-fill::before {
+ content: '\f13b';
+}
+
+.bi-arrow-up-left-circle::before {
+ content: '\f13c';
+}
+
+.bi-arrow-up-left-square-fill::before {
+ content: '\f13d';
+}
+
+.bi-arrow-up-left-square::before {
+ content: '\f13e';
+}
+
+.bi-arrow-up-left::before {
+ content: '\f13f';
+}
+
+.bi-arrow-up-right-circle-fill::before {
+ content: '\f140';
+}
+
+.bi-arrow-up-right-circle::before {
+ content: '\f141';
+}
+
+.bi-arrow-up-right-square-fill::before {
+ content: '\f142';
+}
+
+.bi-arrow-up-right-square::before {
+ content: '\f143';
+}
+
+.bi-arrow-up-right::before {
+ content: '\f144';
+}
+
+.bi-arrow-up-short::before {
+ content: '\f145';
+}
+
+.bi-arrow-up-square-fill::before {
+ content: '\f146';
+}
+
+.bi-arrow-up-square::before {
+ content: '\f147';
+}
+
+.bi-arrow-up::before {
+ content: '\f148';
+}
+
+.bi-arrows-angle-contract::before {
+ content: '\f149';
+}
+
+.bi-arrows-angle-expand::before {
+ content: '\f14a';
+}
+
+.bi-arrows-collapse::before {
+ content: '\f14b';
+}
+
+.bi-arrows-expand::before {
+ content: '\f14c';
+}
+
+.bi-arrows-fullscreen::before {
+ content: '\f14d';
+}
+
+.bi-arrows-move::before {
+ content: '\f14e';
+}
+
+.bi-aspect-ratio-fill::before {
+ content: '\f14f';
+}
+
+.bi-aspect-ratio::before {
+ content: '\f150';
+}
+
+.bi-asterisk::before {
+ content: '\f151';
+}
+
+.bi-at::before {
+ content: '\f152';
+}
+
+.bi-award-fill::before {
+ content: '\f153';
+}
+
+.bi-award::before {
+ content: '\f154';
+}
+
+.bi-back::before {
+ content: '\f155';
+}
+
+.bi-backspace-fill::before {
+ content: '\f156';
+}
+
+.bi-backspace-reverse-fill::before {
+ content: '\f157';
+}
+
+.bi-backspace-reverse::before {
+ content: '\f158';
+}
+
+.bi-backspace::before {
+ content: '\f159';
+}
+
+.bi-badge-3d-fill::before {
+ content: '\f15a';
+}
+
+.bi-badge-3d::before {
+ content: '\f15b';
+}
+
+.bi-badge-4k-fill::before {
+ content: '\f15c';
+}
+
+.bi-badge-4k::before {
+ content: '\f15d';
+}
+
+.bi-badge-8k-fill::before {
+ content: '\f15e';
+}
+
+.bi-badge-8k::before {
+ content: '\f15f';
+}
+
+.bi-badge-ad-fill::before {
+ content: '\f160';
+}
+
+.bi-badge-ad::before {
+ content: '\f161';
+}
+
+.bi-badge-ar-fill::before {
+ content: '\f162';
+}
+
+.bi-badge-ar::before {
+ content: '\f163';
+}
+
+.bi-badge-cc-fill::before {
+ content: '\f164';
+}
+
+.bi-badge-cc::before {
+ content: '\f165';
+}
+
+.bi-badge-hd-fill::before {
+ content: '\f166';
+}
+
+.bi-badge-hd::before {
+ content: '\f167';
+}
+
+.bi-badge-tm-fill::before {
+ content: '\f168';
+}
+
+.bi-badge-tm::before {
+ content: '\f169';
+}
+
+.bi-badge-vo-fill::before {
+ content: '\f16a';
+}
+
+.bi-badge-vo::before {
+ content: '\f16b';
+}
+
+.bi-badge-vr-fill::before {
+ content: '\f16c';
+}
+
+.bi-badge-vr::before {
+ content: '\f16d';
+}
+
+.bi-badge-wc-fill::before {
+ content: '\f16e';
+}
+
+.bi-badge-wc::before {
+ content: '\f16f';
+}
+
+.bi-bag-check-fill::before {
+ content: '\f170';
+}
+
+.bi-bag-check::before {
+ content: '\f171';
+}
+
+.bi-bag-dash-fill::before {
+ content: '\f172';
+}
+
+.bi-bag-dash::before {
+ content: '\f173';
+}
+
+.bi-bag-fill::before {
+ content: '\f174';
+}
+
+.bi-bag-plus-fill::before {
+ content: '\f175';
+}
+
+.bi-bag-plus::before {
+ content: '\f176';
+}
+
+.bi-bag-x-fill::before {
+ content: '\f177';
+}
+
+.bi-bag-x::before {
+ content: '\f178';
+}
+
+.bi-bag::before {
+ content: '\f179';
+}
+
+.bi-bar-chart-fill::before {
+ content: '\f17a';
+}
+
+.bi-bar-chart-line-fill::before {
+ content: '\f17b';
+}
+
+.bi-bar-chart-line::before {
+ content: '\f17c';
+}
+
+.bi-bar-chart-steps::before {
+ content: '\f17d';
+}
+
+.bi-bar-chart::before {
+ content: '\f17e';
+}
+
+.bi-basket-fill::before {
+ content: '\f17f';
+}
+
+.bi-basket::before {
+ content: '\f180';
+}
+
+.bi-basket2-fill::before {
+ content: '\f181';
+}
+
+.bi-basket2::before {
+ content: '\f182';
+}
+
+.bi-basket3-fill::before {
+ content: '\f183';
+}
+
+.bi-basket3::before {
+ content: '\f184';
+}
+
+.bi-battery-charging::before {
+ content: '\f185';
+}
+
+.bi-battery-full::before {
+ content: '\f186';
+}
+
+.bi-battery-half::before {
+ content: '\f187';
+}
+
+.bi-battery::before {
+ content: '\f188';
+}
+
+.bi-bell-fill::before {
+ content: '\f189';
+}
+
+.bi-bell::before {
+ content: '\f18a';
+}
+
+.bi-bezier::before {
+ content: '\f18b';
+}
+
+.bi-bezier2::before {
+ content: '\f18c';
+}
+
+.bi-bicycle::before {
+ content: '\f18d';
+}
+
+.bi-binoculars-fill::before {
+ content: '\f18e';
+}
+
+.bi-binoculars::before {
+ content: '\f18f';
+}
+
+.bi-blockquote-left::before {
+ content: '\f190';
+}
+
+.bi-blockquote-right::before {
+ content: '\f191';
+}
+
+.bi-book-fill::before {
+ content: '\f192';
+}
+
+.bi-book-half::before {
+ content: '\f193';
+}
+
+.bi-book::before {
+ content: '\f194';
+}
+
+.bi-bookmark-check-fill::before {
+ content: '\f195';
+}
+
+.bi-bookmark-check::before {
+ content: '\f196';
+}
+
+.bi-bookmark-dash-fill::before {
+ content: '\f197';
+}
+
+.bi-bookmark-dash::before {
+ content: '\f198';
+}
+
+.bi-bookmark-fill::before {
+ content: '\f199';
+}
+
+.bi-bookmark-heart-fill::before {
+ content: '\f19a';
+}
+
+.bi-bookmark-heart::before {
+ content: '\f19b';
+}
+
+.bi-bookmark-plus-fill::before {
+ content: '\f19c';
+}
+
+.bi-bookmark-plus::before {
+ content: '\f19d';
+}
+
+.bi-bookmark-star-fill::before {
+ content: '\f19e';
+}
+
+.bi-bookmark-star::before {
+ content: '\f19f';
+}
+
+.bi-bookmark-x-fill::before {
+ content: '\f1a0';
+}
+
+.bi-bookmark-x::before {
+ content: '\f1a1';
+}
+
+.bi-bookmark::before {
+ content: '\f1a2';
+}
+
+.bi-bookmarks-fill::before {
+ content: '\f1a3';
+}
+
+.bi-bookmarks::before {
+ content: '\f1a4';
+}
+
+.bi-bookshelf::before {
+ content: '\f1a5';
+}
+
+.bi-bootstrap-fill::before {
+ content: '\f1a6';
+}
+
+.bi-bootstrap-reboot::before {
+ content: '\f1a7';
+}
+
+.bi-bootstrap::before {
+ content: '\f1a8';
+}
+
+.bi-border-all::before {
+ content: '\f1a9';
+}
+
+.bi-border-bottom::before {
+ content: '\f1aa';
+}
+
+.bi-border-center::before {
+ content: '\f1ab';
+}
+
+.bi-border-inner::before {
+ content: '\f1ac';
+}
+
+.bi-border-left::before {
+ content: '\f1ad';
+}
+
+.bi-border-middle::before {
+ content: '\f1ae';
+}
+
+.bi-border-outer::before {
+ content: '\f1af';
+}
+
+.bi-border-right::before {
+ content: '\f1b0';
+}
+
+.bi-border-style::before {
+ content: '\f1b1';
+}
+
+.bi-border-top::before {
+ content: '\f1b2';
+}
+
+.bi-border-width::before {
+ content: '\f1b3';
+}
+
+.bi-border::before {
+ content: '\f1b4';
+}
+
+.bi-bounding-box-circles::before {
+ content: '\f1b5';
+}
+
+.bi-bounding-box::before {
+ content: '\f1b6';
+}
+
+.bi-box-arrow-down-left::before {
+ content: '\f1b7';
+}
+
+.bi-box-arrow-down-right::before {
+ content: '\f1b8';
+}
+
+.bi-box-arrow-down::before {
+ content: '\f1b9';
+}
+
+.bi-box-arrow-in-down-left::before {
+ content: '\f1ba';
+}
+
+.bi-box-arrow-in-down-right::before {
+ content: '\f1bb';
+}
+
+.bi-box-arrow-in-down::before {
+ content: '\f1bc';
+}
+
+.bi-box-arrow-in-left::before {
+ content: '\f1bd';
+}
+
+.bi-box-arrow-in-right::before {
+ content: '\f1be';
+}
+
+.bi-box-arrow-in-up-left::before {
+ content: '\f1bf';
+}
+
+.bi-box-arrow-in-up-right::before {
+ content: '\f1c0';
+}
+
+.bi-box-arrow-in-up::before {
+ content: '\f1c1';
+}
+
+.bi-box-arrow-left::before {
+ content: '\f1c2';
+}
+
+.bi-box-arrow-right::before {
+ content: '\f1c3';
+}
+
+.bi-box-arrow-up-left::before {
+ content: '\f1c4';
+}
+
+.bi-box-arrow-up-right::before {
+ content: '\f1c5';
+}
+
+.bi-box-arrow-up::before {
+ content: '\f1c6';
+}
+
+.bi-box-seam::before {
+ content: '\f1c7';
+}
+
+.bi-box::before {
+ content: '\f1c8';
+}
+
+.bi-braces::before {
+ content: '\f1c9';
+}
+
+.bi-bricks::before {
+ content: '\f1ca';
+}
+
+.bi-briefcase-fill::before {
+ content: '\f1cb';
+}
+
+.bi-briefcase::before {
+ content: '\f1cc';
+}
+
+.bi-brightness-alt-high-fill::before {
+ content: '\f1cd';
+}
+
+.bi-brightness-alt-high::before {
+ content: '\f1ce';
+}
+
+.bi-brightness-alt-low-fill::before {
+ content: '\f1cf';
+}
+
+.bi-brightness-alt-low::before {
+ content: '\f1d0';
+}
+
+.bi-brightness-high-fill::before {
+ content: '\f1d1';
+}
+
+.bi-brightness-high::before {
+ content: '\f1d2';
+}
+
+.bi-brightness-low-fill::before {
+ content: '\f1d3';
+}
+
+.bi-brightness-low::before {
+ content: '\f1d4';
+}
+
+.bi-broadcast-pin::before {
+ content: '\f1d5';
+}
+
+.bi-broadcast::before {
+ content: '\f1d6';
+}
+
+.bi-brush-fill::before {
+ content: '\f1d7';
+}
+
+.bi-brush::before {
+ content: '\f1d8';
+}
+
+.bi-bucket-fill::before {
+ content: '\f1d9';
+}
+
+.bi-bucket::before {
+ content: '\f1da';
+}
+
+.bi-bug-fill::before {
+ content: '\f1db';
+}
+
+.bi-bug::before {
+ content: '\f1dc';
+}
+
+.bi-building::before {
+ content: '\f1dd';
+}
+
+.bi-bullseye::before {
+ content: '\f1de';
+}
+
+.bi-calculator-fill::before {
+ content: '\f1df';
+}
+
+.bi-calculator::before {
+ content: '\f1e0';
+}
+
+.bi-calendar-check-fill::before {
+ content: '\f1e1';
+}
+
+.bi-calendar-check::before {
+ content: '\f1e2';
+}
+
+.bi-calendar-date-fill::before {
+ content: '\f1e3';
+}
+
+.bi-calendar-date::before {
+ content: '\f1e4';
+}
+
+.bi-calendar-day-fill::before {
+ content: '\f1e5';
+}
+
+.bi-calendar-day::before {
+ content: '\f1e6';
+}
+
+.bi-calendar-event-fill::before {
+ content: '\f1e7';
+}
+
+.bi-calendar-event::before {
+ content: '\f1e8';
+}
+
+.bi-calendar-fill::before {
+ content: '\f1e9';
+}
+
+.bi-calendar-minus-fill::before {
+ content: '\f1ea';
+}
+
+.bi-calendar-minus::before {
+ content: '\f1eb';
+}
+
+.bi-calendar-month-fill::before {
+ content: '\f1ec';
+}
+
+.bi-calendar-month::before {
+ content: '\f1ed';
+}
+
+.bi-calendar-plus-fill::before {
+ content: '\f1ee';
+}
+
+.bi-calendar-plus::before {
+ content: '\f1ef';
+}
+
+.bi-calendar-range-fill::before {
+ content: '\f1f0';
+}
+
+.bi-calendar-range::before {
+ content: '\f1f1';
+}
+
+.bi-calendar-week-fill::before {
+ content: '\f1f2';
+}
+
+.bi-calendar-week::before {
+ content: '\f1f3';
+}
+
+.bi-calendar-x-fill::before {
+ content: '\f1f4';
+}
+
+.bi-calendar-x::before {
+ content: '\f1f5';
+}
+
+.bi-calendar::before {
+ content: '\f1f6';
+}
+
+.bi-calendar2-check-fill::before {
+ content: '\f1f7';
+}
+
+.bi-calendar2-check::before {
+ content: '\f1f8';
+}
+
+.bi-calendar2-date-fill::before {
+ content: '\f1f9';
+}
+
+.bi-calendar2-date::before {
+ content: '\f1fa';
+}
+
+.bi-calendar2-day-fill::before {
+ content: '\f1fb';
+}
+
+.bi-calendar2-day::before {
+ content: '\f1fc';
+}
+
+.bi-calendar2-event-fill::before {
+ content: '\f1fd';
+}
+
+.bi-calendar2-event::before {
+ content: '\f1fe';
+}
+
+.bi-calendar2-fill::before {
+ content: '\f1ff';
+}
+
+.bi-calendar2-minus-fill::before {
+ content: '\f200';
+}
+
+.bi-calendar2-minus::before {
+ content: '\f201';
+}
+
+.bi-calendar2-month-fill::before {
+ content: '\f202';
+}
+
+.bi-calendar2-month::before {
+ content: '\f203';
+}
+
+.bi-calendar2-plus-fill::before {
+ content: '\f204';
+}
+
+.bi-calendar2-plus::before {
+ content: '\f205';
+}
+
+.bi-calendar2-range-fill::before {
+ content: '\f206';
+}
+
+.bi-calendar2-range::before {
+ content: '\f207';
+}
+
+.bi-calendar2-week-fill::before {
+ content: '\f208';
+}
+
+.bi-calendar2-week::before {
+ content: '\f209';
+}
+
+.bi-calendar2-x-fill::before {
+ content: '\f20a';
+}
+
+.bi-calendar2-x::before {
+ content: '\f20b';
+}
+
+.bi-calendar2::before {
+ content: '\f20c';
+}
+
+.bi-calendar3-event-fill::before {
+ content: '\f20d';
+}
+
+.bi-calendar3-event::before {
+ content: '\f20e';
+}
+
+.bi-calendar3-fill::before {
+ content: '\f20f';
+}
+
+.bi-calendar3-range-fill::before {
+ content: '\f210';
+}
+
+.bi-calendar3-range::before {
+ content: '\f211';
+}
+
+.bi-calendar3-week-fill::before {
+ content: '\f212';
+}
+
+.bi-calendar3-week::before {
+ content: '\f213';
+}
+
+.bi-calendar3::before {
+ content: '\f214';
+}
+
+.bi-calendar4-event::before {
+ content: '\f215';
+}
+
+.bi-calendar4-range::before {
+ content: '\f216';
+}
+
+.bi-calendar4-week::before {
+ content: '\f217';
+}
+
+.bi-calendar4::before {
+ content: '\f218';
+}
+
+.bi-camera-fill::before {
+ content: '\f219';
+}
+
+.bi-camera-reels-fill::before {
+ content: '\f21a';
+}
+
+.bi-camera-reels::before {
+ content: '\f21b';
+}
+
+.bi-camera-video-fill::before {
+ content: '\f21c';
+}
+
+.bi-camera-video-off-fill::before {
+ content: '\f21d';
+}
+
+.bi-camera-video-off::before {
+ content: '\f21e';
+}
+
+.bi-camera-video::before {
+ content: '\f21f';
+}
+
+.bi-camera::before {
+ content: '\f220';
+}
+
+.bi-camera2::before {
+ content: '\f221';
+}
+
+.bi-capslock-fill::before {
+ content: '\f222';
+}
+
+.bi-capslock::before {
+ content: '\f223';
+}
+
+.bi-card-checklist::before {
+ content: '\f224';
+}
+
+.bi-card-heading::before {
+ content: '\f225';
+}
+
+.bi-card-image::before {
+ content: '\f226';
+}
+
+.bi-card-list::before {
+ content: '\f227';
+}
+
+.bi-card-text::before {
+ content: '\f228';
+}
+
+.bi-caret-down-fill::before {
+ content: '\f229';
+}
+
+.bi-caret-down-square-fill::before {
+ content: '\f22a';
+}
+
+.bi-caret-down-square::before {
+ content: '\f22b';
+}
+
+.bi-caret-down::before {
+ content: '\f22c';
+}
+
+.bi-caret-left-fill::before {
+ content: '\f22d';
+}
+
+.bi-caret-left-square-fill::before {
+ content: '\f22e';
+}
+
+.bi-caret-left-square::before {
+ content: '\f22f';
+}
+
+.bi-caret-left::before {
+ content: '\f230';
+}
+
+.bi-caret-right-fill::before {
+ content: '\f231';
+}
+
+.bi-caret-right-square-fill::before {
+ content: '\f232';
+}
+
+.bi-caret-right-square::before {
+ content: '\f233';
+}
+
+.bi-caret-right::before {
+ content: '\f234';
+}
+
+.bi-caret-up-fill::before {
+ content: '\f235';
+}
+
+.bi-caret-up-square-fill::before {
+ content: '\f236';
+}
+
+.bi-caret-up-square::before {
+ content: '\f237';
+}
+
+.bi-caret-up::before {
+ content: '\f238';
+}
+
+.bi-cart-check-fill::before {
+ content: '\f239';
+}
+
+.bi-cart-check::before {
+ content: '\f23a';
+}
+
+.bi-cart-dash-fill::before {
+ content: '\f23b';
+}
+
+.bi-cart-dash::before {
+ content: '\f23c';
+}
+
+.bi-cart-fill::before {
+ content: '\f23d';
+}
+
+.bi-cart-plus-fill::before {
+ content: '\f23e';
+}
+
+.bi-cart-plus::before {
+ content: '\f23f';
+}
+
+.bi-cart-x-fill::before {
+ content: '\f240';
+}
+
+.bi-cart-x::before {
+ content: '\f241';
+}
+
+.bi-cart::before {
+ content: '\f242';
+}
+
+.bi-cart2::before {
+ content: '\f243';
+}
+
+.bi-cart3::before {
+ content: '\f244';
+}
+
+.bi-cart4::before {
+ content: '\f245';
+}
+
+.bi-cash-stack::before {
+ content: '\f246';
+}
+
+.bi-cash::before {
+ content: '\f247';
+}
+
+.bi-cast::before {
+ content: '\f248';
+}
+
+.bi-chat-dots-fill::before {
+ content: '\f249';
+}
+
+.bi-chat-dots::before {
+ content: '\f24a';
+}
+
+.bi-chat-fill::before {
+ content: '\f24b';
+}
+
+.bi-chat-left-dots-fill::before {
+ content: '\f24c';
+}
+
+.bi-chat-left-dots::before {
+ content: '\f24d';
+}
+
+.bi-chat-left-fill::before {
+ content: '\f24e';
+}
+
+.bi-chat-left-quote-fill::before {
+ content: '\f24f';
+}
+
+.bi-chat-left-quote::before {
+ content: '\f250';
+}
+
+.bi-chat-left-text-fill::before {
+ content: '\f251';
+}
+
+.bi-chat-left-text::before {
+ content: '\f252';
+}
+
+.bi-chat-left::before {
+ content: '\f253';
+}
+
+.bi-chat-quote-fill::before {
+ content: '\f254';
+}
+
+.bi-chat-quote::before {
+ content: '\f255';
+}
+
+.bi-chat-right-dots-fill::before {
+ content: '\f256';
+}
+
+.bi-chat-right-dots::before {
+ content: '\f257';
+}
+
+.bi-chat-right-fill::before {
+ content: '\f258';
+}
+
+.bi-chat-right-quote-fill::before {
+ content: '\f259';
+}
+
+.bi-chat-right-quote::before {
+ content: '\f25a';
+}
+
+.bi-chat-right-text-fill::before {
+ content: '\f25b';
+}
+
+.bi-chat-right-text::before {
+ content: '\f25c';
+}
+
+.bi-chat-right::before {
+ content: '\f25d';
+}
+
+.bi-chat-square-dots-fill::before {
+ content: '\f25e';
+}
+
+.bi-chat-square-dots::before {
+ content: '\f25f';
+}
+
+.bi-chat-square-fill::before {
+ content: '\f260';
+}
+
+.bi-chat-square-quote-fill::before {
+ content: '\f261';
+}
+
+.bi-chat-square-quote::before {
+ content: '\f262';
+}
+
+.bi-chat-square-text-fill::before {
+ content: '\f263';
+}
+
+.bi-chat-square-text::before {
+ content: '\f264';
+}
+
+.bi-chat-square::before {
+ content: '\f265';
+}
+
+.bi-chat-text-fill::before {
+ content: '\f266';
+}
+
+.bi-chat-text::before {
+ content: '\f267';
+}
+
+.bi-chat::before {
+ content: '\f268';
+}
+
+.bi-check-all::before {
+ content: '\f269';
+}
+
+.bi-check-circle-fill::before {
+ content: '\f26a';
+}
+
+.bi-check-circle::before {
+ content: '\f26b';
+}
+
+.bi-check-square-fill::before {
+ content: '\f26c';
+}
+
+.bi-check-square::before {
+ content: '\f26d';
+}
+
+.bi-check::before {
+ content: '\f26e';
+}
+
+.bi-check2-all::before {
+ content: '\f26f';
+}
+
+.bi-check2-circle::before {
+ content: '\f270';
+}
+
+.bi-check2-square::before {
+ content: '\f271';
+}
+
+.bi-check2::before {
+ content: '\f272';
+}
+
+.bi-chevron-bar-contract::before {
+ content: '\f273';
+}
+
+.bi-chevron-bar-down::before {
+ content: '\f274';
+}
+
+.bi-chevron-bar-expand::before {
+ content: '\f275';
+}
+
+.bi-chevron-bar-left::before {
+ content: '\f276';
+}
+
+.bi-chevron-bar-right::before {
+ content: '\f277';
+}
+
+.bi-chevron-bar-up::before {
+ content: '\f278';
+}
+
+.bi-chevron-compact-down::before {
+ content: '\f279';
+}
+
+.bi-chevron-compact-left::before {
+ content: '\f27a';
+}
+
+.bi-chevron-compact-right::before {
+ content: '\f27b';
+}
+
+.bi-chevron-compact-up::before {
+ content: '\f27c';
+}
+
+.bi-chevron-contract::before {
+ content: '\f27d';
+}
+
+.bi-chevron-double-down::before {
+ content: '\f27e';
+}
+
+.bi-chevron-double-left::before {
+ content: '\f27f';
+}
+
+.bi-chevron-double-right::before {
+ content: '\f280';
+}
+
+.bi-chevron-double-up::before {
+ content: '\f281';
+}
+
+.bi-chevron-down::before {
+ content: '\f282';
+}
+
+.bi-chevron-expand::before {
+ content: '\f283';
+}
+
+.bi-chevron-left::before {
+ content: '\f284';
+}
+
+.bi-chevron-right::before {
+ content: '\f285';
+}
+
+.bi-chevron-up::before {
+ content: '\f286';
+}
+
+.bi-circle-fill::before {
+ content: '\f287';
+}
+
+.bi-circle-half::before {
+ content: '\f288';
+}
+
+.bi-circle-square::before {
+ content: '\f289';
+}
+
+.bi-circle::before {
+ content: '\f28a';
+}
+
+.bi-clipboard-check::before {
+ content: '\f28b';
+}
+
+.bi-clipboard-data::before {
+ content: '\f28c';
+}
+
+.bi-clipboard-minus::before {
+ content: '\f28d';
+}
+
+.bi-clipboard-plus::before {
+ content: '\f28e';
+}
+
+.bi-clipboard-x::before {
+ content: '\f28f';
+}
+
+.bi-clipboard::before {
+ content: '\f290';
+}
+
+.bi-clock-fill::before {
+ content: '\f291';
+}
+
+.bi-clock-history::before {
+ content: '\f292';
+}
+
+.bi-clock::before {
+ content: '\f293';
+}
+
+.bi-cloud-arrow-down-fill::before {
+ content: '\f294';
+}
+
+.bi-cloud-arrow-down::before {
+ content: '\f295';
+}
+
+.bi-cloud-arrow-up-fill::before {
+ content: '\f296';
+}
+
+.bi-cloud-arrow-up::before {
+ content: '\f297';
+}
+
+.bi-cloud-check-fill::before {
+ content: '\f298';
+}
+
+.bi-cloud-check::before {
+ content: '\f299';
+}
+
+.bi-cloud-download-fill::before {
+ content: '\f29a';
+}
+
+.bi-cloud-download::before {
+ content: '\f29b';
+}
+
+.bi-cloud-drizzle-fill::before {
+ content: '\f29c';
+}
+
+.bi-cloud-drizzle::before {
+ content: '\f29d';
+}
+
+.bi-cloud-fill::before {
+ content: '\f29e';
+}
+
+.bi-cloud-fog-fill::before {
+ content: '\f29f';
+}
+
+.bi-cloud-fog::before {
+ content: '\f2a0';
+}
+
+.bi-cloud-fog2-fill::before {
+ content: '\f2a1';
+}
+
+.bi-cloud-fog2::before {
+ content: '\f2a2';
+}
+
+.bi-cloud-hail-fill::before {
+ content: '\f2a3';
+}
+
+.bi-cloud-hail::before {
+ content: '\f2a4';
+}
+
+.bi-cloud-haze-1::before {
+ content: '\f2a5';
+}
+
+.bi-cloud-haze-fill::before {
+ content: '\f2a6';
+}
+
+.bi-cloud-haze::before {
+ content: '\f2a7';
+}
+
+.bi-cloud-haze2-fill::before {
+ content: '\f2a8';
+}
+
+.bi-cloud-lightning-fill::before {
+ content: '\f2a9';
+}
+
+.bi-cloud-lightning-rain-fill::before {
+ content: '\f2aa';
+}
+
+.bi-cloud-lightning-rain::before {
+ content: '\f2ab';
+}
+
+.bi-cloud-lightning::before {
+ content: '\f2ac';
+}
+
+.bi-cloud-minus-fill::before {
+ content: '\f2ad';
+}
+
+.bi-cloud-minus::before {
+ content: '\f2ae';
+}
+
+.bi-cloud-moon-fill::before {
+ content: '\f2af';
+}
+
+.bi-cloud-moon::before {
+ content: '\f2b0';
+}
+
+.bi-cloud-plus-fill::before {
+ content: '\f2b1';
+}
+
+.bi-cloud-plus::before {
+ content: '\f2b2';
+}
+
+.bi-cloud-rain-fill::before {
+ content: '\f2b3';
+}
+
+.bi-cloud-rain-heavy-fill::before {
+ content: '\f2b4';
+}
+
+.bi-cloud-rain-heavy::before {
+ content: '\f2b5';
+}
+
+.bi-cloud-rain::before {
+ content: '\f2b6';
+}
+
+.bi-cloud-slash-fill::before {
+ content: '\f2b7';
+}
+
+.bi-cloud-slash::before {
+ content: '\f2b8';
+}
+
+.bi-cloud-sleet-fill::before {
+ content: '\f2b9';
+}
+
+.bi-cloud-sleet::before {
+ content: '\f2ba';
+}
+
+.bi-cloud-snow-fill::before {
+ content: '\f2bb';
+}
+
+.bi-cloud-snow::before {
+ content: '\f2bc';
+}
+
+.bi-cloud-sun-fill::before {
+ content: '\f2bd';
+}
+
+.bi-cloud-sun::before {
+ content: '\f2be';
+}
+
+.bi-cloud-upload-fill::before {
+ content: '\f2bf';
+}
+
+.bi-cloud-upload::before {
+ content: '\f2c0';
+}
+
+.bi-cloud::before {
+ content: '\f2c1';
+}
+
+.bi-clouds-fill::before {
+ content: '\f2c2';
+}
+
+.bi-clouds::before {
+ content: '\f2c3';
+}
+
+.bi-cloudy-fill::before {
+ content: '\f2c4';
+}
+
+.bi-cloudy::before {
+ content: '\f2c5';
+}
+
+.bi-code-slash::before {
+ content: '\f2c6';
+}
+
+.bi-code-square::before {
+ content: '\f2c7';
+}
+
+.bi-code::before {
+ content: '\f2c8';
+}
+
+.bi-collection-fill::before {
+ content: '\f2c9';
+}
+
+.bi-collection-play-fill::before {
+ content: '\f2ca';
+}
+
+.bi-collection-play::before {
+ content: '\f2cb';
+}
+
+.bi-collection::before {
+ content: '\f2cc';
+}
+
+.bi-columns-gap::before {
+ content: '\f2cd';
+}
+
+.bi-columns::before {
+ content: '\f2ce';
+}
+
+.bi-command::before {
+ content: '\f2cf';
+}
+
+.bi-compass-fill::before {
+ content: '\f2d0';
+}
+
+.bi-compass::before {
+ content: '\f2d1';
+}
+
+.bi-cone-striped::before {
+ content: '\f2d2';
+}
+
+.bi-cone::before {
+ content: '\f2d3';
+}
+
+.bi-controller::before {
+ content: '\f2d4';
+}
+
+.bi-cpu-fill::before {
+ content: '\f2d5';
+}
+
+.bi-cpu::before {
+ content: '\f2d6';
+}
+
+.bi-credit-card-2-back-fill::before {
+ content: '\f2d7';
+}
+
+.bi-credit-card-2-back::before {
+ content: '\f2d8';
+}
+
+.bi-credit-card-2-front-fill::before {
+ content: '\f2d9';
+}
+
+.bi-credit-card-2-front::before {
+ content: '\f2da';
+}
+
+.bi-credit-card-fill::before {
+ content: '\f2db';
+}
+
+.bi-credit-card::before {
+ content: '\f2dc';
+}
+
+.bi-crop::before {
+ content: '\f2dd';
+}
+
+.bi-cup-fill::before {
+ content: '\f2de';
+}
+
+.bi-cup-straw::before {
+ content: '\f2df';
+}
+
+.bi-cup::before {
+ content: '\f2e0';
+}
+
+.bi-cursor-fill::before {
+ content: '\f2e1';
+}
+
+.bi-cursor-text::before {
+ content: '\f2e2';
+}
+
+.bi-cursor::before {
+ content: '\f2e3';
+}
+
+.bi-dash-circle-dotted::before {
+ content: '\f2e4';
+}
+
+.bi-dash-circle-fill::before {
+ content: '\f2e5';
+}
+
+.bi-dash-circle::before {
+ content: '\f2e6';
+}
+
+.bi-dash-square-dotted::before {
+ content: '\f2e7';
+}
+
+.bi-dash-square-fill::before {
+ content: '\f2e8';
+}
+
+.bi-dash-square::before {
+ content: '\f2e9';
+}
+
+.bi-dash::before {
+ content: '\f2ea';
+}
+
+.bi-diagram-2-fill::before {
+ content: '\f2eb';
+}
+
+.bi-diagram-2::before {
+ content: '\f2ec';
+}
+
+.bi-diagram-3-fill::before {
+ content: '\f2ed';
+}
+
+.bi-diagram-3::before {
+ content: '\f2ee';
+}
+
+.bi-diamond-fill::before {
+ content: '\f2ef';
+}
+
+.bi-diamond-half::before {
+ content: '\f2f0';
+}
+
+.bi-diamond::before {
+ content: '\f2f1';
+}
+
+.bi-dice-1-fill::before {
+ content: '\f2f2';
+}
+
+.bi-dice-1::before {
+ content: '\f2f3';
+}
+
+.bi-dice-2-fill::before {
+ content: '\f2f4';
+}
+
+.bi-dice-2::before {
+ content: '\f2f5';
+}
+
+.bi-dice-3-fill::before {
+ content: '\f2f6';
+}
+
+.bi-dice-3::before {
+ content: '\f2f7';
+}
+
+.bi-dice-4-fill::before {
+ content: '\f2f8';
+}
+
+.bi-dice-4::before {
+ content: '\f2f9';
+}
+
+.bi-dice-5-fill::before {
+ content: '\f2fa';
+}
+
+.bi-dice-5::before {
+ content: '\f2fb';
+}
+
+.bi-dice-6-fill::before {
+ content: '\f2fc';
+}
+
+.bi-dice-6::before {
+ content: '\f2fd';
+}
+
+.bi-disc-fill::before {
+ content: '\f2fe';
+}
+
+.bi-disc::before {
+ content: '\f2ff';
+}
+
+.bi-discord::before {
+ content: '\f300';
+}
+
+.bi-display-fill::before {
+ content: '\f301';
+}
+
+.bi-display::before {
+ content: '\f302';
+}
+
+.bi-distribute-horizontal::before {
+ content: '\f303';
+}
+
+.bi-distribute-vertical::before {
+ content: '\f304';
+}
+
+.bi-door-closed-fill::before {
+ content: '\f305';
+}
+
+.bi-door-closed::before {
+ content: '\f306';
+}
+
+.bi-door-open-fill::before {
+ content: '\f307';
+}
+
+.bi-door-open::before {
+ content: '\f308';
+}
+
+.bi-dot::before {
+ content: '\f309';
+}
+
+.bi-download::before {
+ content: '\f30a';
+}
+
+.bi-droplet-fill::before {
+ content: '\f30b';
+}
+
+.bi-droplet-half::before {
+ content: '\f30c';
+}
+
+.bi-droplet::before {
+ content: '\f30d';
+}
+
+.bi-earbuds::before {
+ content: '\f30e';
+}
+
+.bi-easel-fill::before {
+ content: '\f30f';
+}
+
+.bi-easel::before {
+ content: '\f310';
+}
+
+.bi-egg-fill::before {
+ content: '\f311';
+}
+
+.bi-egg-fried::before {
+ content: '\f312';
+}
+
+.bi-egg::before {
+ content: '\f313';
+}
+
+.bi-eject-fill::before {
+ content: '\f314';
+}
+
+.bi-eject::before {
+ content: '\f315';
+}
+
+.bi-emoji-angry-fill::before {
+ content: '\f316';
+}
+
+.bi-emoji-angry::before {
+ content: '\f317';
+}
+
+.bi-emoji-dizzy-fill::before {
+ content: '\f318';
+}
+
+.bi-emoji-dizzy::before {
+ content: '\f319';
+}
+
+.bi-emoji-expressionless-fill::before {
+ content: '\f31a';
+}
+
+.bi-emoji-expressionless::before {
+ content: '\f31b';
+}
+
+.bi-emoji-frown-fill::before {
+ content: '\f31c';
+}
+
+.bi-emoji-frown::before {
+ content: '\f31d';
+}
+
+.bi-emoji-heart-eyes-fill::before {
+ content: '\f31e';
+}
+
+.bi-emoji-heart-eyes::before {
+ content: '\f31f';
+}
+
+.bi-emoji-laughing-fill::before {
+ content: '\f320';
+}
+
+.bi-emoji-laughing::before {
+ content: '\f321';
+}
+
+.bi-emoji-neutral-fill::before {
+ content: '\f322';
+}
+
+.bi-emoji-neutral::before {
+ content: '\f323';
+}
+
+.bi-emoji-smile-fill::before {
+ content: '\f324';
+}
+
+.bi-emoji-smile-upside-down-fill::before {
+ content: '\f325';
+}
+
+.bi-emoji-smile-upside-down::before {
+ content: '\f326';
+}
+
+.bi-emoji-smile::before {
+ content: '\f327';
+}
+
+.bi-emoji-sunglasses-fill::before {
+ content: '\f328';
+}
+
+.bi-emoji-sunglasses::before {
+ content: '\f329';
+}
+
+.bi-emoji-wink-fill::before {
+ content: '\f32a';
+}
+
+.bi-emoji-wink::before {
+ content: '\f32b';
+}
+
+.bi-envelope-fill::before {
+ content: '\f32c';
+}
+
+.bi-envelope-open-fill::before {
+ content: '\f32d';
+}
+
+.bi-envelope-open::before {
+ content: '\f32e';
+}
+
+.bi-envelope::before {
+ content: '\f32f';
+}
+
+.bi-eraser-fill::before {
+ content: '\f330';
+}
+
+.bi-eraser::before {
+ content: '\f331';
+}
+
+.bi-exclamation-circle-fill::before {
+ content: '\f332';
+}
+
+.bi-exclamation-circle::before {
+ content: '\f333';
+}
+
+.bi-exclamation-diamond-fill::before {
+ content: '\f334';
+}
+
+.bi-exclamation-diamond::before {
+ content: '\f335';
+}
+
+.bi-exclamation-octagon-fill::before {
+ content: '\f336';
+}
+
+.bi-exclamation-octagon::before {
+ content: '\f337';
+}
+
+.bi-exclamation-square-fill::before {
+ content: '\f338';
+}
+
+.bi-exclamation-square::before {
+ content: '\f339';
+}
+
+.bi-exclamation-triangle-fill::before {
+ content: '\f33a';
+}
+
+.bi-exclamation-triangle::before {
+ content: '\f33b';
+}
+
+.bi-exclamation::before {
+ content: '\f33c';
+}
+
+.bi-exclude::before {
+ content: '\f33d';
+}
+
+.bi-eye-fill::before {
+ content: '\f33e';
+}
+
+.bi-eye-slash-fill::before {
+ content: '\f33f';
+}
+
+.bi-eye-slash::before {
+ content: '\f340';
+}
+
+.bi-eye::before {
+ content: '\f341';
+}
+
+.bi-eyedropper::before {
+ content: '\f342';
+}
+
+.bi-eyeglasses::before {
+ content: '\f343';
+}
+
+.bi-facebook::before {
+ content: '\f344';
+}
+
+.bi-file-arrow-down-fill::before {
+ content: '\f345';
+}
+
+.bi-file-arrow-down::before {
+ content: '\f346';
+}
+
+.bi-file-arrow-up-fill::before {
+ content: '\f347';
+}
+
+.bi-file-arrow-up::before {
+ content: '\f348';
+}
+
+.bi-file-bar-graph-fill::before {
+ content: '\f349';
+}
+
+.bi-file-bar-graph::before {
+ content: '\f34a';
+}
+
+.bi-file-binary-fill::before {
+ content: '\f34b';
+}
+
+.bi-file-binary::before {
+ content: '\f34c';
+}
+
+.bi-file-break-fill::before {
+ content: '\f34d';
+}
+
+.bi-file-break::before {
+ content: '\f34e';
+}
+
+.bi-file-check-fill::before {
+ content: '\f34f';
+}
+
+.bi-file-check::before {
+ content: '\f350';
+}
+
+.bi-file-code-fill::before {
+ content: '\f351';
+}
+
+.bi-file-code::before {
+ content: '\f352';
+}
+
+.bi-file-diff-fill::before {
+ content: '\f353';
+}
+
+.bi-file-diff::before {
+ content: '\f354';
+}
+
+.bi-file-earmark-arrow-down-fill::before {
+ content: '\f355';
+}
+
+.bi-file-earmark-arrow-down::before {
+ content: '\f356';
+}
+
+.bi-file-earmark-arrow-up-fill::before {
+ content: '\f357';
+}
+
+.bi-file-earmark-arrow-up::before {
+ content: '\f358';
+}
+
+.bi-file-earmark-bar-graph-fill::before {
+ content: '\f359';
+}
+
+.bi-file-earmark-bar-graph::before {
+ content: '\f35a';
+}
+
+.bi-file-earmark-binary-fill::before {
+ content: '\f35b';
+}
+
+.bi-file-earmark-binary::before {
+ content: '\f35c';
+}
+
+.bi-file-earmark-break-fill::before {
+ content: '\f35d';
+}
+
+.bi-file-earmark-break::before {
+ content: '\f35e';
+}
+
+.bi-file-earmark-check-fill::before {
+ content: '\f35f';
+}
+
+.bi-file-earmark-check::before {
+ content: '\f360';
+}
+
+.bi-file-earmark-code-fill::before {
+ content: '\f361';
+}
+
+.bi-file-earmark-code::before {
+ content: '\f362';
+}
+
+.bi-file-earmark-diff-fill::before {
+ content: '\f363';
+}
+
+.bi-file-earmark-diff::before {
+ content: '\f364';
+}
+
+.bi-file-earmark-easel-fill::before {
+ content: '\f365';
+}
+
+.bi-file-earmark-easel::before {
+ content: '\f366';
+}
+
+.bi-file-earmark-excel-fill::before {
+ content: '\f367';
+}
+
+.bi-file-earmark-excel::before {
+ content: '\f368';
+}
+
+.bi-file-earmark-fill::before {
+ content: '\f369';
+}
+
+.bi-file-earmark-font-fill::before {
+ content: '\f36a';
+}
+
+.bi-file-earmark-font::before {
+ content: '\f36b';
+}
+
+.bi-file-earmark-image-fill::before {
+ content: '\f36c';
+}
+
+.bi-file-earmark-image::before {
+ content: '\f36d';
+}
+
+.bi-file-earmark-lock-fill::before {
+ content: '\f36e';
+}
+
+.bi-file-earmark-lock::before {
+ content: '\f36f';
+}
+
+.bi-file-earmark-lock2-fill::before {
+ content: '\f370';
+}
+
+.bi-file-earmark-lock2::before {
+ content: '\f371';
+}
+
+.bi-file-earmark-medical-fill::before {
+ content: '\f372';
+}
+
+.bi-file-earmark-medical::before {
+ content: '\f373';
+}
+
+.bi-file-earmark-minus-fill::before {
+ content: '\f374';
+}
+
+.bi-file-earmark-minus::before {
+ content: '\f375';
+}
+
+.bi-file-earmark-music-fill::before {
+ content: '\f376';
+}
+
+.bi-file-earmark-music::before {
+ content: '\f377';
+}
+
+.bi-file-earmark-person-fill::before {
+ content: '\f378';
+}
+
+.bi-file-earmark-person::before {
+ content: '\f379';
+}
+
+.bi-file-earmark-play-fill::before {
+ content: '\f37a';
+}
+
+.bi-file-earmark-play::before {
+ content: '\f37b';
+}
+
+.bi-file-earmark-plus-fill::before {
+ content: '\f37c';
+}
+
+.bi-file-earmark-plus::before {
+ content: '\f37d';
+}
+
+.bi-file-earmark-post-fill::before {
+ content: '\f37e';
+}
+
+.bi-file-earmark-post::before {
+ content: '\f37f';
+}
+
+.bi-file-earmark-ppt-fill::before {
+ content: '\f380';
+}
+
+.bi-file-earmark-ppt::before {
+ content: '\f381';
+}
+
+.bi-file-earmark-richtext-fill::before {
+ content: '\f382';
+}
+
+.bi-file-earmark-richtext::before {
+ content: '\f383';
+}
+
+.bi-file-earmark-ruled-fill::before {
+ content: '\f384';
+}
+
+.bi-file-earmark-ruled::before {
+ content: '\f385';
+}
+
+.bi-file-earmark-slides-fill::before {
+ content: '\f386';
+}
+
+.bi-file-earmark-slides::before {
+ content: '\f387';
+}
+
+.bi-file-earmark-spreadsheet-fill::before {
+ content: '\f388';
+}
+
+.bi-file-earmark-spreadsheet::before {
+ content: '\f389';
+}
+
+.bi-file-earmark-text-fill::before {
+ content: '\f38a';
+}
+
+.bi-file-earmark-text::before {
+ content: '\f38b';
+}
+
+.bi-file-earmark-word-fill::before {
+ content: '\f38c';
+}
+
+.bi-file-earmark-word::before {
+ content: '\f38d';
+}
+
+.bi-file-earmark-x-fill::before {
+ content: '\f38e';
+}
+
+.bi-file-earmark-x::before {
+ content: '\f38f';
+}
+
+.bi-file-earmark-zip-fill::before {
+ content: '\f390';
+}
+
+.bi-file-earmark-zip::before {
+ content: '\f391';
+}
+
+.bi-file-earmark::before {
+ content: '\f392';
+}
+
+.bi-file-easel-fill::before {
+ content: '\f393';
+}
+
+.bi-file-easel::before {
+ content: '\f394';
+}
+
+.bi-file-excel-fill::before {
+ content: '\f395';
+}
+
+.bi-file-excel::before {
+ content: '\f396';
+}
+
+.bi-file-fill::before {
+ content: '\f397';
+}
+
+.bi-file-font-fill::before {
+ content: '\f398';
+}
+
+.bi-file-font::before {
+ content: '\f399';
+}
+
+.bi-file-image-fill::before {
+ content: '\f39a';
+}
+
+.bi-file-image::before {
+ content: '\f39b';
+}
+
+.bi-file-lock-fill::before {
+ content: '\f39c';
+}
+
+.bi-file-lock::before {
+ content: '\f39d';
+}
+
+.bi-file-lock2-fill::before {
+ content: '\f39e';
+}
+
+.bi-file-lock2::before {
+ content: '\f39f';
+}
+
+.bi-file-medical-fill::before {
+ content: '\f3a0';
+}
+
+.bi-file-medical::before {
+ content: '\f3a1';
+}
+
+.bi-file-minus-fill::before {
+ content: '\f3a2';
+}
+
+.bi-file-minus::before {
+ content: '\f3a3';
+}
+
+.bi-file-music-fill::before {
+ content: '\f3a4';
+}
+
+.bi-file-music::before {
+ content: '\f3a5';
+}
+
+.bi-file-person-fill::before {
+ content: '\f3a6';
+}
+
+.bi-file-person::before {
+ content: '\f3a7';
+}
+
+.bi-file-play-fill::before {
+ content: '\f3a8';
+}
+
+.bi-file-play::before {
+ content: '\f3a9';
+}
+
+.bi-file-plus-fill::before {
+ content: '\f3aa';
+}
+
+.bi-file-plus::before {
+ content: '\f3ab';
+}
+
+.bi-file-post-fill::before {
+ content: '\f3ac';
+}
+
+.bi-file-post::before {
+ content: '\f3ad';
+}
+
+.bi-file-ppt-fill::before {
+ content: '\f3ae';
+}
+
+.bi-file-ppt::before {
+ content: '\f3af';
+}
+
+.bi-file-richtext-fill::before {
+ content: '\f3b0';
+}
+
+.bi-file-richtext::before {
+ content: '\f3b1';
+}
+
+.bi-file-ruled-fill::before {
+ content: '\f3b2';
+}
+
+.bi-file-ruled::before {
+ content: '\f3b3';
+}
+
+.bi-file-slides-fill::before {
+ content: '\f3b4';
+}
+
+.bi-file-slides::before {
+ content: '\f3b5';
+}
+
+.bi-file-spreadsheet-fill::before {
+ content: '\f3b6';
+}
+
+.bi-file-spreadsheet::before {
+ content: '\f3b7';
+}
+
+.bi-file-text-fill::before {
+ content: '\f3b8';
+}
+
+.bi-file-text::before {
+ content: '\f3b9';
+}
+
+.bi-file-word-fill::before {
+ content: '\f3ba';
+}
+
+.bi-file-word::before {
+ content: '\f3bb';
+}
+
+.bi-file-x-fill::before {
+ content: '\f3bc';
+}
+
+.bi-file-x::before {
+ content: '\f3bd';
+}
+
+.bi-file-zip-fill::before {
+ content: '\f3be';
+}
+
+.bi-file-zip::before {
+ content: '\f3bf';
+}
+
+.bi-file::before {
+ content: '\f3c0';
+}
+
+.bi-files-alt::before {
+ content: '\f3c1';
+}
+
+.bi-files::before {
+ content: '\f3c2';
+}
+
+.bi-film::before {
+ content: '\f3c3';
+}
+
+.bi-filter-circle-fill::before {
+ content: '\f3c4';
+}
+
+.bi-filter-circle::before {
+ content: '\f3c5';
+}
+
+.bi-filter-left::before {
+ content: '\f3c6';
+}
+
+.bi-filter-right::before {
+ content: '\f3c7';
+}
+
+.bi-filter-square-fill::before {
+ content: '\f3c8';
+}
+
+.bi-filter-square::before {
+ content: '\f3c9';
+}
+
+.bi-filter::before {
+ content: '\f3ca';
+}
+
+.bi-flag-fill::before {
+ content: '\f3cb';
+}
+
+.bi-flag::before {
+ content: '\f3cc';
+}
+
+.bi-flower1::before {
+ content: '\f3cd';
+}
+
+.bi-flower2::before {
+ content: '\f3ce';
+}
+
+.bi-flower3::before {
+ content: '\f3cf';
+}
+
+.bi-folder-check::before {
+ content: '\f3d0';
+}
+
+.bi-folder-fill::before {
+ content: '\f3d1';
+}
+
+.bi-folder-minus::before {
+ content: '\f3d2';
+}
+
+.bi-folder-plus::before {
+ content: '\f3d3';
+}
+
+.bi-folder-symlink-fill::before {
+ content: '\f3d4';
+}
+
+.bi-folder-symlink::before {
+ content: '\f3d5';
+}
+
+.bi-folder-x::before {
+ content: '\f3d6';
+}
+
+.bi-folder::before {
+ content: '\f3d7';
+}
+
+.bi-folder2-open::before {
+ content: '\f3d8';
+}
+
+.bi-folder2::before {
+ content: '\f3d9';
+}
+
+.bi-fonts::before {
+ content: '\f3da';
+}
+
+.bi-forward-fill::before {
+ content: '\f3db';
+}
+
+.bi-forward::before {
+ content: '\f3dc';
+}
+
+.bi-front::before {
+ content: '\f3dd';
+}
+
+.bi-fullscreen-exit::before {
+ content: '\f3de';
+}
+
+.bi-fullscreen::before {
+ content: '\f3df';
+}
+
+.bi-funnel-fill::before {
+ content: '\f3e0';
+}
+
+.bi-funnel::before {
+ content: '\f3e1';
+}
+
+.bi-gear-fill::before {
+ content: '\f3e2';
+}
+
+.bi-gear-wide-connected::before {
+ content: '\f3e3';
+}
+
+.bi-gear-wide::before {
+ content: '\f3e4';
+}
+
+.bi-gear::before {
+ content: '\f3e5';
+}
+
+.bi-gem::before {
+ content: '\f3e6';
+}
+
+.bi-geo-alt-fill::before {
+ content: '\f3e7';
+}
+
+.bi-geo-alt::before {
+ content: '\f3e8';
+}
+
+.bi-geo-fill::before {
+ content: '\f3e9';
+}
+
+.bi-geo::before {
+ content: '\f3ea';
+}
+
+.bi-gift-fill::before {
+ content: '\f3eb';
+}
+
+.bi-gift::before {
+ content: '\f3ec';
+}
+
+.bi-github::before {
+ content: '\f3ed';
+}
+
+.bi-globe::before {
+ content: '\f3ee';
+}
+
+.bi-globe2::before {
+ content: '\f3ef';
+}
+
+.bi-google::before {
+ content: '\f3f0';
+}
+
+.bi-graph-down::before {
+ content: '\f3f1';
+}
+
+.bi-graph-up::before {
+ content: '\f3f2';
+}
+
+.bi-grid-1x2-fill::before {
+ content: '\f3f3';
+}
+
+.bi-grid-1x2::before {
+ content: '\f3f4';
+}
+
+.bi-grid-3x2-gap-fill::before {
+ content: '\f3f5';
+}
+
+.bi-grid-3x2-gap::before {
+ content: '\f3f6';
+}
+
+.bi-grid-3x2::before {
+ content: '\f3f7';
+}
+
+.bi-grid-3x3-gap-fill::before {
+ content: '\f3f8';
+}
+
+.bi-grid-3x3-gap::before {
+ content: '\f3f9';
+}
+
+.bi-grid-3x3::before {
+ content: '\f3fa';
+}
+
+.bi-grid-fill::before {
+ content: '\f3fb';
+}
+
+.bi-grid::before {
+ content: '\f3fc';
+}
+
+.bi-grip-horizontal::before {
+ content: '\f3fd';
+}
+
+.bi-grip-vertical::before {
+ content: '\f3fe';
+}
+
+.bi-hammer::before {
+ content: '\f3ff';
+}
+
+.bi-hand-index-fill::before {
+ content: '\f400';
+}
+
+.bi-hand-index-thumb-fill::before {
+ content: '\f401';
+}
+
+.bi-hand-index-thumb::before {
+ content: '\f402';
+}
+
+.bi-hand-index::before {
+ content: '\f403';
+}
+
+.bi-hand-thumbs-down-fill::before {
+ content: '\f404';
+}
+
+.bi-hand-thumbs-down::before {
+ content: '\f405';
+}
+
+.bi-hand-thumbs-up-fill::before {
+ content: '\f406';
+}
+
+.bi-hand-thumbs-up::before {
+ content: '\f407';
+}
+
+.bi-handbag-fill::before {
+ content: '\f408';
+}
+
+.bi-handbag::before {
+ content: '\f409';
+}
+
+.bi-hash::before {
+ content: '\f40a';
+}
+
+.bi-hdd-fill::before {
+ content: '\f40b';
+}
+
+.bi-hdd-network-fill::before {
+ content: '\f40c';
+}
+
+.bi-hdd-network::before {
+ content: '\f40d';
+}
+
+.bi-hdd-rack-fill::before {
+ content: '\f40e';
+}
+
+.bi-hdd-rack::before {
+ content: '\f40f';
+}
+
+.bi-hdd-stack-fill::before {
+ content: '\f410';
+}
+
+.bi-hdd-stack::before {
+ content: '\f411';
+}
+
+.bi-hdd::before {
+ content: '\f412';
+}
+
+.bi-headphones::before {
+ content: '\f413';
+}
+
+.bi-headset::before {
+ content: '\f414';
+}
+
+.bi-heart-fill::before {
+ content: '\f415';
+}
+
+.bi-heart-half::before {
+ content: '\f416';
+}
+
+.bi-heart::before {
+ content: '\f417';
+}
+
+.bi-heptagon-fill::before {
+ content: '\f418';
+}
+
+.bi-heptagon-half::before {
+ content: '\f419';
+}
+
+.bi-heptagon::before {
+ content: '\f41a';
+}
+
+.bi-hexagon-fill::before {
+ content: '\f41b';
+}
+
+.bi-hexagon-half::before {
+ content: '\f41c';
+}
+
+.bi-hexagon::before {
+ content: '\f41d';
+}
+
+.bi-hourglass-bottom::before {
+ content: '\f41e';
+}
+
+.bi-hourglass-split::before {
+ content: '\f41f';
+}
+
+.bi-hourglass-top::before {
+ content: '\f420';
+}
+
+.bi-hourglass::before {
+ content: '\f421';
+}
+
+.bi-house-door-fill::before {
+ content: '\f422';
+}
+
+.bi-house-door::before {
+ content: '\f423';
+}
+
+.bi-house-fill::before {
+ content: '\f424';
+}
+
+.bi-house::before {
+ content: '\f425';
+}
+
+.bi-hr::before {
+ content: '\f426';
+}
+
+.bi-hurricane::before {
+ content: '\f427';
+}
+
+.bi-image-alt::before {
+ content: '\f428';
+}
+
+.bi-image-fill::before {
+ content: '\f429';
+}
+
+.bi-image::before {
+ content: '\f42a';
+}
+
+.bi-images::before {
+ content: '\f42b';
+}
+
+.bi-inbox-fill::before {
+ content: '\f42c';
+}
+
+.bi-inbox::before {
+ content: '\f42d';
+}
+
+.bi-inboxes-fill::before {
+ content: '\f42e';
+}
+
+.bi-inboxes::before {
+ content: '\f42f';
+}
+
+.bi-info-circle-fill::before {
+ content: '\f430';
+}
+
+.bi-info-circle::before {
+ content: '\f431';
+}
+
+.bi-info-square-fill::before {
+ content: '\f432';
+}
+
+.bi-info-square::before {
+ content: '\f433';
+}
+
+.bi-info::before {
+ content: '\f434';
+}
+
+.bi-input-cursor-text::before {
+ content: '\f435';
+}
+
+.bi-input-cursor::before {
+ content: '\f436';
+}
+
+.bi-instagram::before {
+ content: '\f437';
+}
+
+.bi-intersect::before {
+ content: '\f438';
+}
+
+.bi-journal-album::before {
+ content: '\f439';
+}
+
+.bi-journal-arrow-down::before {
+ content: '\f43a';
+}
+
+.bi-journal-arrow-up::before {
+ content: '\f43b';
+}
+
+.bi-journal-bookmark-fill::before {
+ content: '\f43c';
+}
+
+.bi-journal-bookmark::before {
+ content: '\f43d';
+}
+
+.bi-journal-check::before {
+ content: '\f43e';
+}
+
+.bi-journal-code::before {
+ content: '\f43f';
+}
+
+.bi-journal-medical::before {
+ content: '\f440';
+}
+
+.bi-journal-minus::before {
+ content: '\f441';
+}
+
+.bi-journal-plus::before {
+ content: '\f442';
+}
+
+.bi-journal-richtext::before {
+ content: '\f443';
+}
+
+.bi-journal-text::before {
+ content: '\f444';
+}
+
+.bi-journal-x::before {
+ content: '\f445';
+}
+
+.bi-journal::before {
+ content: '\f446';
+}
+
+.bi-journals::before {
+ content: '\f447';
+}
+
+.bi-joystick::before {
+ content: '\f448';
+}
+
+.bi-justify-left::before {
+ content: '\f449';
+}
+
+.bi-justify-right::before {
+ content: '\f44a';
+}
+
+.bi-justify::before {
+ content: '\f44b';
+}
+
+.bi-kanban-fill::before {
+ content: '\f44c';
+}
+
+.bi-kanban::before {
+ content: '\f44d';
+}
+
+.bi-key-fill::before {
+ content: '\f44e';
+}
+
+.bi-key::before {
+ content: '\f44f';
+}
+
+.bi-keyboard-fill::before {
+ content: '\f450';
+}
+
+.bi-keyboard::before {
+ content: '\f451';
+}
+
+.bi-ladder::before {
+ content: '\f452';
+}
+
+.bi-lamp-fill::before {
+ content: '\f453';
+}
+
+.bi-lamp::before {
+ content: '\f454';
+}
+
+.bi-laptop-fill::before {
+ content: '\f455';
+}
+
+.bi-laptop::before {
+ content: '\f456';
+}
+
+.bi-layer-backward::before {
+ content: '\f457';
+}
+
+.bi-layer-forward::before {
+ content: '\f458';
+}
+
+.bi-layers-fill::before {
+ content: '\f459';
+}
+
+.bi-layers-half::before {
+ content: '\f45a';
+}
+
+.bi-layers::before {
+ content: '\f45b';
+}
+
+.bi-layout-sidebar-inset-reverse::before {
+ content: '\f45c';
+}
+
+.bi-layout-sidebar-inset::before {
+ content: '\f45d';
+}
+
+.bi-layout-sidebar-reverse::before {
+ content: '\f45e';
+}
+
+.bi-layout-sidebar::before {
+ content: '\f45f';
+}
+
+.bi-layout-split::before {
+ content: '\f460';
+}
+
+.bi-layout-text-sidebar-reverse::before {
+ content: '\f461';
+}
+
+.bi-layout-text-sidebar::before {
+ content: '\f462';
+}
+
+.bi-layout-text-window-reverse::before {
+ content: '\f463';
+}
+
+.bi-layout-text-window::before {
+ content: '\f464';
+}
+
+.bi-layout-three-columns::before {
+ content: '\f465';
+}
+
+.bi-layout-wtf::before {
+ content: '\f466';
+}
+
+.bi-life-preserver::before {
+ content: '\f467';
+}
+
+.bi-lightbulb-fill::before {
+ content: '\f468';
+}
+
+.bi-lightbulb-off-fill::before {
+ content: '\f469';
+}
+
+.bi-lightbulb-off::before {
+ content: '\f46a';
+}
+
+.bi-lightbulb::before {
+ content: '\f46b';
+}
+
+.bi-lightning-charge-fill::before {
+ content: '\f46c';
+}
+
+.bi-lightning-charge::before {
+ content: '\f46d';
+}
+
+.bi-lightning-fill::before {
+ content: '\f46e';
+}
+
+.bi-lightning::before {
+ content: '\f46f';
+}
+
+.bi-link-45deg::before {
+ content: '\f470';
+}
+
+.bi-link::before {
+ content: '\f471';
+}
+
+.bi-linkedin::before {
+ content: '\f472';
+}
+
+.bi-list-check::before {
+ content: '\f473';
+}
+
+.bi-list-nested::before {
+ content: '\f474';
+}
+
+.bi-list-ol::before {
+ content: '\f475';
+}
+
+.bi-list-stars::before {
+ content: '\f476';
+}
+
+.bi-list-task::before {
+ content: '\f477';
+}
+
+.bi-list-ul::before {
+ content: '\f478';
+}
+
+.bi-list::before {
+ content: '\f479';
+}
+
+.bi-lock-fill::before {
+ content: '\f47a';
+}
+
+.bi-lock::before {
+ content: '\f47b';
+}
+
+.bi-mailbox::before {
+ content: '\f47c';
+}
+
+.bi-mailbox2::before {
+ content: '\f47d';
+}
+
+.bi-map-fill::before {
+ content: '\f47e';
+}
+
+.bi-map::before {
+ content: '\f47f';
+}
+
+.bi-markdown-fill::before {
+ content: '\f480';
+}
+
+.bi-markdown::before {
+ content: '\f481';
+}
+
+.bi-mask::before {
+ content: '\f482';
+}
+
+.bi-megaphone-fill::before {
+ content: '\f483';
+}
+
+.bi-megaphone::before {
+ content: '\f484';
+}
+
+.bi-menu-app-fill::before {
+ content: '\f485';
+}
+
+.bi-menu-app::before {
+ content: '\f486';
+}
+
+.bi-menu-button-fill::before {
+ content: '\f487';
+}
+
+.bi-menu-button-wide-fill::before {
+ content: '\f488';
+}
+
+.bi-menu-button-wide::before {
+ content: '\f489';
+}
+
+.bi-menu-button::before {
+ content: '\f48a';
+}
+
+.bi-menu-down::before {
+ content: '\f48b';
+}
+
+.bi-menu-up::before {
+ content: '\f48c';
+}
+
+.bi-mic-fill::before {
+ content: '\f48d';
+}
+
+.bi-mic-mute-fill::before {
+ content: '\f48e';
+}
+
+.bi-mic-mute::before {
+ content: '\f48f';
+}
+
+.bi-mic::before {
+ content: '\f490';
+}
+
+.bi-minecart-loaded::before {
+ content: '\f491';
+}
+
+.bi-minecart::before {
+ content: '\f492';
+}
+
+.bi-moisture::before {
+ content: '\f493';
+}
+
+.bi-moon-fill::before {
+ content: '\f494';
+}
+
+.bi-moon-stars-fill::before {
+ content: '\f495';
+}
+
+.bi-moon-stars::before {
+ content: '\f496';
+}
+
+.bi-moon::before {
+ content: '\f497';
+}
+
+.bi-mouse-fill::before {
+ content: '\f498';
+}
+
+.bi-mouse::before {
+ content: '\f499';
+}
+
+.bi-mouse2-fill::before {
+ content: '\f49a';
+}
+
+.bi-mouse2::before {
+ content: '\f49b';
+}
+
+.bi-mouse3-fill::before {
+ content: '\f49c';
+}
+
+.bi-mouse3::before {
+ content: '\f49d';
+}
+
+.bi-music-note-beamed::before {
+ content: '\f49e';
+}
+
+.bi-music-note-list::before {
+ content: '\f49f';
+}
+
+.bi-music-note::before {
+ content: '\f4a0';
+}
+
+.bi-music-player-fill::before {
+ content: '\f4a1';
+}
+
+.bi-music-player::before {
+ content: '\f4a2';
+}
+
+.bi-newspaper::before {
+ content: '\f4a3';
+}
+
+.bi-node-minus-fill::before {
+ content: '\f4a4';
+}
+
+.bi-node-minus::before {
+ content: '\f4a5';
+}
+
+.bi-node-plus-fill::before {
+ content: '\f4a6';
+}
+
+.bi-node-plus::before {
+ content: '\f4a7';
+}
+
+.bi-nut-fill::before {
+ content: '\f4a8';
+}
+
+.bi-nut::before {
+ content: '\f4a9';
+}
+
+.bi-octagon-fill::before {
+ content: '\f4aa';
+}
+
+.bi-octagon-half::before {
+ content: '\f4ab';
+}
+
+.bi-octagon::before {
+ content: '\f4ac';
+}
+
+.bi-option::before {
+ content: '\f4ad';
+}
+
+.bi-outlet::before {
+ content: '\f4ae';
+}
+
+.bi-paint-bucket::before {
+ content: '\f4af';
+}
+
+.bi-palette-fill::before {
+ content: '\f4b0';
+}
+
+.bi-palette::before {
+ content: '\f4b1';
+}
+
+.bi-palette2::before {
+ content: '\f4b2';
+}
+
+.bi-paperclip::before {
+ content: '\f4b3';
+}
+
+.bi-paragraph::before {
+ content: '\f4b4';
+}
+
+.bi-patch-check-fill::before {
+ content: '\f4b5';
+}
+
+.bi-patch-check::before {
+ content: '\f4b6';
+}
+
+.bi-patch-exclamation-fill::before {
+ content: '\f4b7';
+}
+
+.bi-patch-exclamation::before {
+ content: '\f4b8';
+}
+
+.bi-patch-minus-fill::before {
+ content: '\f4b9';
+}
+
+.bi-patch-minus::before {
+ content: '\f4ba';
+}
+
+.bi-patch-plus-fill::before {
+ content: '\f4bb';
+}
+
+.bi-patch-plus::before {
+ content: '\f4bc';
+}
+
+.bi-patch-question-fill::before {
+ content: '\f4bd';
+}
+
+.bi-patch-question::before {
+ content: '\f4be';
+}
+
+.bi-pause-btn-fill::before {
+ content: '\f4bf';
+}
+
+.bi-pause-btn::before {
+ content: '\f4c0';
+}
+
+.bi-pause-circle-fill::before {
+ content: '\f4c1';
+}
+
+.bi-pause-circle::before {
+ content: '\f4c2';
+}
+
+.bi-pause-fill::before {
+ content: '\f4c3';
+}
+
+.bi-pause::before {
+ content: '\f4c4';
+}
+
+.bi-peace-fill::before {
+ content: '\f4c5';
+}
+
+.bi-peace::before {
+ content: '\f4c6';
+}
+
+.bi-pen-fill::before {
+ content: '\f4c7';
+}
+
+.bi-pen::before {
+ content: '\f4c8';
+}
+
+.bi-pencil-fill::before {
+ content: '\f4c9';
+}
+
+.bi-pencil-square::before {
+ content: '\f4ca';
+}
+
+.bi-pencil::before {
+ content: '\f4cb';
+}
+
+.bi-pentagon-fill::before {
+ content: '\f4cc';
+}
+
+.bi-pentagon-half::before {
+ content: '\f4cd';
+}
+
+.bi-pentagon::before {
+ content: '\f4ce';
+}
+
+.bi-people-fill::before {
+ content: '\f4cf';
+}
+
+.bi-people::before {
+ content: '\f4d0';
+}
+
+.bi-percent::before {
+ content: '\f4d1';
+}
+
+.bi-person-badge-fill::before {
+ content: '\f4d2';
+}
+
+.bi-person-badge::before {
+ content: '\f4d3';
+}
+
+.bi-person-bounding-box::before {
+ content: '\f4d4';
+}
+
+.bi-person-check-fill::before {
+ content: '\f4d5';
+}
+
+.bi-person-check::before {
+ content: '\f4d6';
+}
+
+.bi-person-circle::before {
+ content: '\f4d7';
+}
+
+.bi-person-dash-fill::before {
+ content: '\f4d8';
+}
+
+.bi-person-dash::before {
+ content: '\f4d9';
+}
+
+.bi-person-fill::before {
+ content: '\f4da';
+}
+
+.bi-person-lines-fill::before {
+ content: '\f4db';
+}
+
+.bi-person-plus-fill::before {
+ content: '\f4dc';
+}
+
+.bi-person-plus::before {
+ content: '\f4dd';
+}
+
+.bi-person-square::before {
+ content: '\f4de';
+}
+
+.bi-person-x-fill::before {
+ content: '\f4df';
+}
+
+.bi-person-x::before {
+ content: '\f4e0';
+}
+
+.bi-person::before {
+ content: '\f4e1';
+}
+
+.bi-phone-fill::before {
+ content: '\f4e2';
+}
+
+.bi-phone-landscape-fill::before {
+ content: '\f4e3';
+}
+
+.bi-phone-landscape::before {
+ content: '\f4e4';
+}
+
+.bi-phone-vibrate-fill::before {
+ content: '\f4e5';
+}
+
+.bi-phone-vibrate::before {
+ content: '\f4e6';
+}
+
+.bi-phone::before {
+ content: '\f4e7';
+}
+
+.bi-pie-chart-fill::before {
+ content: '\f4e8';
+}
+
+.bi-pie-chart::before {
+ content: '\f4e9';
+}
+
+.bi-pin-angle-fill::before {
+ content: '\f4ea';
+}
+
+.bi-pin-angle::before {
+ content: '\f4eb';
+}
+
+.bi-pin-fill::before {
+ content: '\f4ec';
+}
+
+.bi-pin::before {
+ content: '\f4ed';
+}
+
+.bi-pip-fill::before {
+ content: '\f4ee';
+}
+
+.bi-pip::before {
+ content: '\f4ef';
+}
+
+.bi-play-btn-fill::before {
+ content: '\f4f0';
+}
+
+.bi-play-btn::before {
+ content: '\f4f1';
+}
+
+.bi-play-circle-fill::before {
+ content: '\f4f2';
+}
+
+.bi-play-circle::before {
+ content: '\f4f3';
+}
+
+.bi-play-fill::before {
+ content: '\f4f4';
+}
+
+.bi-play::before {
+ content: '\f4f5';
+}
+
+.bi-plug-fill::before {
+ content: '\f4f6';
+}
+
+.bi-plug::before {
+ content: '\f4f7';
+}
+
+.bi-plus-circle-dotted::before {
+ content: '\f4f8';
+}
+
+.bi-plus-circle-fill::before {
+ content: '\f4f9';
+}
+
+.bi-plus-circle::before {
+ content: '\f4fa';
+}
+
+.bi-plus-square-dotted::before {
+ content: '\f4fb';
+}
+
+.bi-plus-square-fill::before {
+ content: '\f4fc';
+}
+
+.bi-plus-square::before {
+ content: '\f4fd';
+}
+
+.bi-plus::before {
+ content: '\f4fe';
+}
+
+.bi-power::before {
+ content: '\f4ff';
+}
+
+.bi-printer-fill::before {
+ content: '\f500';
+}
+
+.bi-printer::before {
+ content: '\f501';
+}
+
+.bi-puzzle-fill::before {
+ content: '\f502';
+}
+
+.bi-puzzle::before {
+ content: '\f503';
+}
+
+.bi-question-circle-fill::before {
+ content: '\f504';
+}
+
+.bi-question-circle::before {
+ content: '\f505';
+}
+
+.bi-question-diamond-fill::before {
+ content: '\f506';
+}
+
+.bi-question-diamond::before {
+ content: '\f507';
+}
+
+.bi-question-octagon-fill::before {
+ content: '\f508';
+}
+
+.bi-question-octagon::before {
+ content: '\f509';
+}
+
+.bi-question-square-fill::before {
+ content: '\f50a';
+}
+
+.bi-question-square::before {
+ content: '\f50b';
+}
+
+.bi-question::before {
+ content: '\f50c';
+}
+
+.bi-rainbow::before {
+ content: '\f50d';
+}
+
+.bi-receipt-cutoff::before {
+ content: '\f50e';
+}
+
+.bi-receipt::before {
+ content: '\f50f';
+}
+
+.bi-reception-0::before {
+ content: '\f510';
+}
+
+.bi-reception-1::before {
+ content: '\f511';
+}
+
+.bi-reception-2::before {
+ content: '\f512';
+}
+
+.bi-reception-3::before {
+ content: '\f513';
+}
+
+.bi-reception-4::before {
+ content: '\f514';
+}
+
+.bi-record-btn-fill::before {
+ content: '\f515';
+}
+
+.bi-record-btn::before {
+ content: '\f516';
+}
+
+.bi-record-circle-fill::before {
+ content: '\f517';
+}
+
+.bi-record-circle::before {
+ content: '\f518';
+}
+
+.bi-record-fill::before {
+ content: '\f519';
+}
+
+.bi-record::before {
+ content: '\f51a';
+}
+
+.bi-record2-fill::before {
+ content: '\f51b';
+}
+
+.bi-record2::before {
+ content: '\f51c';
+}
+
+.bi-reply-all-fill::before {
+ content: '\f51d';
+}
+
+.bi-reply-all::before {
+ content: '\f51e';
+}
+
+.bi-reply-fill::before {
+ content: '\f51f';
+}
+
+.bi-reply::before {
+ content: '\f520';
+}
+
+.bi-rss-fill::before {
+ content: '\f521';
+}
+
+.bi-rss::before {
+ content: '\f522';
+}
+
+.bi-rulers::before {
+ content: '\f523';
+}
+
+.bi-save-fill::before {
+ content: '\f524';
+}
+
+.bi-save::before {
+ content: '\f525';
+}
+
+.bi-save2-fill::before {
+ content: '\f526';
+}
+
+.bi-save2::before {
+ content: '\f527';
+}
+
+.bi-scissors::before {
+ content: '\f528';
+}
+
+.bi-screwdriver::before {
+ content: '\f529';
+}
+
+.bi-search::before {
+ content: '\f52a';
+}
+
+.bi-segmented-nav::before {
+ content: '\f52b';
+}
+
+.bi-server::before {
+ content: '\f52c';
+}
+
+.bi-share-fill::before {
+ content: '\f52d';
+}
+
+.bi-share::before {
+ content: '\f52e';
+}
+
+.bi-shield-check::before {
+ content: '\f52f';
+}
+
+.bi-shield-exclamation::before {
+ content: '\f530';
+}
+
+.bi-shield-fill-check::before {
+ content: '\f531';
+}
+
+.bi-shield-fill-exclamation::before {
+ content: '\f532';
+}
+
+.bi-shield-fill-minus::before {
+ content: '\f533';
+}
+
+.bi-shield-fill-plus::before {
+ content: '\f534';
+}
+
+.bi-shield-fill-x::before {
+ content: '\f535';
+}
+
+.bi-shield-fill::before {
+ content: '\f536';
+}
+
+.bi-shield-lock-fill::before {
+ content: '\f537';
+}
+
+.bi-shield-lock::before {
+ content: '\f538';
+}
+
+.bi-shield-minus::before {
+ content: '\f539';
+}
+
+.bi-shield-plus::before {
+ content: '\f53a';
+}
+
+.bi-shield-shaded::before {
+ content: '\f53b';
+}
+
+.bi-shield-slash-fill::before {
+ content: '\f53c';
+}
+
+.bi-shield-slash::before {
+ content: '\f53d';
+}
+
+.bi-shield-x::before {
+ content: '\f53e';
+}
+
+.bi-shield::before {
+ content: '\f53f';
+}
+
+.bi-shift-fill::before {
+ content: '\f540';
+}
+
+.bi-shift::before {
+ content: '\f541';
+}
+
+.bi-shop-window::before {
+ content: '\f542';
+}
+
+.bi-shop::before {
+ content: '\f543';
+}
+
+.bi-shuffle::before {
+ content: '\f544';
+}
+
+.bi-signpost-2-fill::before {
+ content: '\f545';
+}
+
+.bi-signpost-2::before {
+ content: '\f546';
+}
+
+.bi-signpost-fill::before {
+ content: '\f547';
+}
+
+.bi-signpost-split-fill::before {
+ content: '\f548';
+}
+
+.bi-signpost-split::before {
+ content: '\f549';
+}
+
+.bi-signpost::before {
+ content: '\f54a';
+}
+
+.bi-sim-fill::before {
+ content: '\f54b';
+}
+
+.bi-sim::before {
+ content: '\f54c';
+}
+
+.bi-skip-backward-btn-fill::before {
+ content: '\f54d';
+}
+
+.bi-skip-backward-btn::before {
+ content: '\f54e';
+}
+
+.bi-skip-backward-circle-fill::before {
+ content: '\f54f';
+}
+
+.bi-skip-backward-circle::before {
+ content: '\f550';
+}
+
+.bi-skip-backward-fill::before {
+ content: '\f551';
+}
+
+.bi-skip-backward::before {
+ content: '\f552';
+}
+
+.bi-skip-end-btn-fill::before {
+ content: '\f553';
+}
+
+.bi-skip-end-btn::before {
+ content: '\f554';
+}
+
+.bi-skip-end-circle-fill::before {
+ content: '\f555';
+}
+
+.bi-skip-end-circle::before {
+ content: '\f556';
+}
+
+.bi-skip-end-fill::before {
+ content: '\f557';
+}
+
+.bi-skip-end::before {
+ content: '\f558';
+}
+
+.bi-skip-forward-btn-fill::before {
+ content: '\f559';
+}
+
+.bi-skip-forward-btn::before {
+ content: '\f55a';
+}
+
+.bi-skip-forward-circle-fill::before {
+ content: '\f55b';
+}
+
+.bi-skip-forward-circle::before {
+ content: '\f55c';
+}
+
+.bi-skip-forward-fill::before {
+ content: '\f55d';
+}
+
+.bi-skip-forward::before {
+ content: '\f55e';
+}
+
+.bi-skip-start-btn-fill::before {
+ content: '\f55f';
+}
+
+.bi-skip-start-btn::before {
+ content: '\f560';
+}
+
+.bi-skip-start-circle-fill::before {
+ content: '\f561';
+}
+
+.bi-skip-start-circle::before {
+ content: '\f562';
+}
+
+.bi-skip-start-fill::before {
+ content: '\f563';
+}
+
+.bi-skip-start::before {
+ content: '\f564';
+}
+
+.bi-slack::before {
+ content: '\f565';
+}
+
+.bi-slash-circle-fill::before {
+ content: '\f566';
+}
+
+.bi-slash-circle::before {
+ content: '\f567';
+}
+
+.bi-slash-square-fill::before {
+ content: '\f568';
+}
+
+.bi-slash-square::before {
+ content: '\f569';
+}
+
+.bi-slash::before {
+ content: '\f56a';
+}
+
+.bi-sliders::before {
+ content: '\f56b';
+}
+
+.bi-smartwatch::before {
+ content: '\f56c';
+}
+
+.bi-snow::before {
+ content: '\f56d';
+}
+
+.bi-snow2::before {
+ content: '\f56e';
+}
+
+.bi-snow3::before {
+ content: '\f56f';
+}
+
+.bi-sort-alpha-down-alt::before {
+ content: '\f570';
+}
+
+.bi-sort-alpha-down::before {
+ content: '\f571';
+}
+
+.bi-sort-alpha-up-alt::before {
+ content: '\f572';
+}
+
+.bi-sort-alpha-up::before {
+ content: '\f573';
+}
+
+.bi-sort-down-alt::before {
+ content: '\f574';
+}
+
+.bi-sort-down::before {
+ content: '\f575';
+}
+
+.bi-sort-numeric-down-alt::before {
+ content: '\f576';
+}
+
+.bi-sort-numeric-down::before {
+ content: '\f577';
+}
+
+.bi-sort-numeric-up-alt::before {
+ content: '\f578';
+}
+
+.bi-sort-numeric-up::before {
+ content: '\f579';
+}
+
+.bi-sort-up-alt::before {
+ content: '\f57a';
+}
+
+.bi-sort-up::before {
+ content: '\f57b';
+}
+
+.bi-soundwave::before {
+ content: '\f57c';
+}
+
+.bi-speaker-fill::before {
+ content: '\f57d';
+}
+
+.bi-speaker::before {
+ content: '\f57e';
+}
+
+.bi-speedometer::before {
+ content: '\f57f';
+}
+
+.bi-speedometer2::before {
+ content: '\f580';
+}
+
+.bi-spellcheck::before {
+ content: '\f581';
+}
+
+.bi-square-fill::before {
+ content: '\f582';
+}
+
+.bi-square-half::before {
+ content: '\f583';
+}
+
+.bi-square::before {
+ content: '\f584';
+}
+
+.bi-stack::before {
+ content: '\f585';
+}
+
+.bi-star-fill::before {
+ content: '\f586';
+}
+
+.bi-star-half::before {
+ content: '\f587';
+}
+
+.bi-star::before {
+ content: '\f588';
+}
+
+.bi-stars::before {
+ content: '\f589';
+}
+
+.bi-stickies-fill::before {
+ content: '\f58a';
+}
+
+.bi-stickies::before {
+ content: '\f58b';
+}
+
+.bi-sticky-fill::before {
+ content: '\f58c';
+}
+
+.bi-sticky::before {
+ content: '\f58d';
+}
+
+.bi-stop-btn-fill::before {
+ content: '\f58e';
+}
+
+.bi-stop-btn::before {
+ content: '\f58f';
+}
+
+.bi-stop-circle-fill::before {
+ content: '\f590';
+}
+
+.bi-stop-circle::before {
+ content: '\f591';
+}
+
+.bi-stop-fill::before {
+ content: '\f592';
+}
+
+.bi-stop::before {
+ content: '\f593';
+}
+
+.bi-stoplights-fill::before {
+ content: '\f594';
+}
+
+.bi-stoplights::before {
+ content: '\f595';
+}
+
+.bi-stopwatch-fill::before {
+ content: '\f596';
+}
+
+.bi-stopwatch::before {
+ content: '\f597';
+}
+
+.bi-subtract::before {
+ content: '\f598';
+}
+
+.bi-suit-club-fill::before {
+ content: '\f599';
+}
+
+.bi-suit-club::before {
+ content: '\f59a';
+}
+
+.bi-suit-diamond-fill::before {
+ content: '\f59b';
+}
+
+.bi-suit-diamond::before {
+ content: '\f59c';
+}
+
+.bi-suit-heart-fill::before {
+ content: '\f59d';
+}
+
+.bi-suit-heart::before {
+ content: '\f59e';
+}
+
+.bi-suit-spade-fill::before {
+ content: '\f59f';
+}
+
+.bi-suit-spade::before {
+ content: '\f5a0';
+}
+
+.bi-sun-fill::before {
+ content: '\f5a1';
+}
+
+.bi-sun::before {
+ content: '\f5a2';
+}
+
+.bi-sunglasses::before {
+ content: '\f5a3';
+}
+
+.bi-sunrise-fill::before {
+ content: '\f5a4';
+}
+
+.bi-sunrise::before {
+ content: '\f5a5';
+}
+
+.bi-sunset-fill::before {
+ content: '\f5a6';
+}
+
+.bi-sunset::before {
+ content: '\f5a7';
+}
+
+.bi-symmetry-horizontal::before {
+ content: '\f5a8';
+}
+
+.bi-symmetry-vertical::before {
+ content: '\f5a9';
+}
+
+.bi-table::before {
+ content: '\f5aa';
+}
+
+.bi-tablet-fill::before {
+ content: '\f5ab';
+}
+
+.bi-tablet-landscape-fill::before {
+ content: '\f5ac';
+}
+
+.bi-tablet-landscape::before {
+ content: '\f5ad';
+}
+
+.bi-tablet::before {
+ content: '\f5ae';
+}
+
+.bi-tag-fill::before {
+ content: '\f5af';
+}
+
+.bi-tag::before {
+ content: '\f5b0';
+}
+
+.bi-tags-fill::before {
+ content: '\f5b1';
+}
+
+.bi-tags::before {
+ content: '\f5b2';
+}
+
+.bi-telegram::before {
+ content: '\f5b3';
+}
+
+.bi-telephone-fill::before {
+ content: '\f5b4';
+}
+
+.bi-telephone-forward-fill::before {
+ content: '\f5b5';
+}
+
+.bi-telephone-forward::before {
+ content: '\f5b6';
+}
+
+.bi-telephone-inbound-fill::before {
+ content: '\f5b7';
+}
+
+.bi-telephone-inbound::before {
+ content: '\f5b8';
+}
+
+.bi-telephone-minus-fill::before {
+ content: '\f5b9';
+}
+
+.bi-telephone-minus::before {
+ content: '\f5ba';
+}
+
+.bi-telephone-outbound-fill::before {
+ content: '\f5bb';
+}
+
+.bi-telephone-outbound::before {
+ content: '\f5bc';
+}
+
+.bi-telephone-plus-fill::before {
+ content: '\f5bd';
+}
+
+.bi-telephone-plus::before {
+ content: '\f5be';
+}
+
+.bi-telephone-x-fill::before {
+ content: '\f5bf';
+}
+
+.bi-telephone-x::before {
+ content: '\f5c0';
+}
+
+.bi-telephone::before {
+ content: '\f5c1';
+}
+
+.bi-terminal-fill::before {
+ content: '\f5c2';
+}
+
+.bi-terminal::before {
+ content: '\f5c3';
+}
+
+.bi-text-center::before {
+ content: '\f5c4';
+}
+
+.bi-text-indent-left::before {
+ content: '\f5c5';
+}
+
+.bi-text-indent-right::before {
+ content: '\f5c6';
+}
+
+.bi-text-left::before {
+ content: '\f5c7';
+}
+
+.bi-text-paragraph::before {
+ content: '\f5c8';
+}
+
+.bi-text-right::before {
+ content: '\f5c9';
+}
+
+.bi-textarea-resize::before {
+ content: '\f5ca';
+}
+
+.bi-textarea-t::before {
+ content: '\f5cb';
+}
+
+.bi-textarea::before {
+ content: '\f5cc';
+}
+
+.bi-thermometer-half::before {
+ content: '\f5cd';
+}
+
+.bi-thermometer-high::before {
+ content: '\f5ce';
+}
+
+.bi-thermometer-low::before {
+ content: '\f5cf';
+}
+
+.bi-thermometer-snow::before {
+ content: '\f5d0';
+}
+
+.bi-thermometer-sun::before {
+ content: '\f5d1';
+}
+
+.bi-thermometer::before {
+ content: '\f5d2';
+}
+
+.bi-three-dots-vertical::before {
+ content: '\f5d3';
+}
+
+.bi-three-dots::before {
+ content: '\f5d4';
+}
+
+.bi-toggle-off::before {
+ content: '\f5d5';
+}
+
+.bi-toggle-on::before {
+ content: '\f5d6';
+}
+
+.bi-toggle2-off::before {
+ content: '\f5d7';
+}
+
+.bi-toggle2-on::before {
+ content: '\f5d8';
+}
+
+.bi-toggles::before {
+ content: '\f5d9';
+}
+
+.bi-toggles2::before {
+ content: '\f5da';
+}
+
+.bi-tools::before {
+ content: '\f5db';
+}
+
+.bi-tornado::before {
+ content: '\f5dc';
+}
+
+.bi-trash-fill::before {
+ content: '\f5dd';
+}
+
+.bi-trash::before {
+ content: '\f5de';
+}
+
+.bi-trash2-fill::before {
+ content: '\f5df';
+}
+
+.bi-trash2::before {
+ content: '\f5e0';
+}
+
+.bi-tree-fill::before {
+ content: '\f5e1';
+}
+
+.bi-tree::before {
+ content: '\f5e2';
+}
+
+.bi-triangle-fill::before {
+ content: '\f5e3';
+}
+
+.bi-triangle-half::before {
+ content: '\f5e4';
+}
+
+.bi-triangle::before {
+ content: '\f5e5';
+}
+
+.bi-trophy-fill::before {
+ content: '\f5e6';
+}
+
+.bi-trophy::before {
+ content: '\f5e7';
+}
+
+.bi-tropical-storm::before {
+ content: '\f5e8';
+}
+
+.bi-truck-flatbed::before {
+ content: '\f5e9';
+}
+
+.bi-truck::before {
+ content: '\f5ea';
+}
+
+.bi-tsunami::before {
+ content: '\f5eb';
+}
+
+.bi-tv-fill::before {
+ content: '\f5ec';
+}
+
+.bi-tv::before {
+ content: '\f5ed';
+}
+
+.bi-twitch::before {
+ content: '\f5ee';
+}
+
+.bi-twitter::before {
+ content: '\f5ef';
+}
+
+.bi-type-bold::before {
+ content: '\f5f0';
+}
+
+.bi-type-h1::before {
+ content: '\f5f1';
+}
+
+.bi-type-h2::before {
+ content: '\f5f2';
+}
+
+.bi-type-h3::before {
+ content: '\f5f3';
+}
+
+.bi-type-italic::before {
+ content: '\f5f4';
+}
+
+.bi-type-strikethrough::before {
+ content: '\f5f5';
+}
+
+.bi-type-underline::before {
+ content: '\f5f6';
+}
+
+.bi-type::before {
+ content: '\f5f7';
+}
+
+.bi-ui-checks-grid::before {
+ content: '\f5f8';
+}
+
+.bi-ui-checks::before {
+ content: '\f5f9';
+}
+
+.bi-ui-radios-grid::before {
+ content: '\f5fa';
+}
+
+.bi-ui-radios::before {
+ content: '\f5fb';
+}
+
+.bi-umbrella-fill::before {
+ content: '\f5fc';
+}
+
+.bi-umbrella::before {
+ content: '\f5fd';
+}
+
+.bi-union::before {
+ content: '\f5fe';
+}
+
+.bi-unlock-fill::before {
+ content: '\f5ff';
+}
+
+.bi-unlock::before {
+ content: '\f600';
+}
+
+.bi-upc-scan::before {
+ content: '\f601';
+}
+
+.bi-upc::before {
+ content: '\f602';
+}
+
+.bi-upload::before {
+ content: '\f603';
+}
+
+.bi-vector-pen::before {
+ content: '\f604';
+}
+
+.bi-view-list::before {
+ content: '\f605';
+}
+
+.bi-view-stacked::before {
+ content: '\f606';
+}
+
+.bi-vinyl-fill::before {
+ content: '\f607';
+}
+
+.bi-vinyl::before {
+ content: '\f608';
+}
+
+.bi-voicemail::before {
+ content: '\f609';
+}
+
+.bi-volume-down-fill::before {
+ content: '\f60a';
+}
+
+.bi-volume-down::before {
+ content: '\f60b';
+}
+
+.bi-volume-mute-fill::before {
+ content: '\f60c';
+}
+
+.bi-volume-mute::before {
+ content: '\f60d';
+}
+
+.bi-volume-off-fill::before {
+ content: '\f60e';
+}
+
+.bi-volume-off::before {
+ content: '\f60f';
+}
+
+.bi-volume-up-fill::before {
+ content: '\f610';
+}
+
+.bi-volume-up::before {
+ content: '\f611';
+}
+
+.bi-vr::before {
+ content: '\f612';
+}
+
+.bi-wallet-fill::before {
+ content: '\f613';
+}
+
+.bi-wallet::before {
+ content: '\f614';
+}
+
+.bi-wallet2::before {
+ content: '\f615';
+}
+
+.bi-watch::before {
+ content: '\f616';
+}
+
+.bi-water::before {
+ content: '\f617';
+}
+
+.bi-whatsapp::before {
+ content: '\f618';
+}
+
+.bi-wifi-1::before {
+ content: '\f619';
+}
+
+.bi-wifi-2::before {
+ content: '\f61a';
+}
+
+.bi-wifi-off::before {
+ content: '\f61b';
+}
+
+.bi-wifi::before {
+ content: '\f61c';
+}
+
+.bi-wind::before {
+ content: '\f61d';
+}
+
+.bi-window-dock::before {
+ content: '\f61e';
+}
+
+.bi-window-sidebar::before {
+ content: '\f61f';
+}
+
+.bi-window::before {
+ content: '\f620';
+}
+
+.bi-wrench::before {
+ content: '\f621';
+}
+
+.bi-x-circle-fill::before {
+ content: '\f622';
+}
+
+.bi-x-circle::before {
+ content: '\f623';
+}
+
+.bi-x-diamond-fill::before {
+ content: '\f624';
+}
+
+.bi-x-diamond::before {
+ content: '\f625';
+}
+
+.bi-x-octagon-fill::before {
+ content: '\f626';
+}
+
+.bi-x-octagon::before {
+ content: '\f627';
+}
+
+.bi-x-square-fill::before {
+ content: '\f628';
+}
+
+.bi-x-square::before {
+ content: '\f629';
+}
+
+.bi-x::before {
+ content: '\f62a';
+}
+
+.bi-youtube::before {
+ content: '\f62b';
+}
+
+.bi-zoom-in::before {
+ content: '\f62c';
+}
+
+.bi-zoom-out::before {
+ content: '\f62d';
+}
+
+.bi-bank::before {
+ content: '\f62e';
+}
+
+.bi-bank2::before {
+ content: '\f62f';
+}
+
+.bi-bell-slash-fill::before {
+ content: '\f630';
+}
+
+.bi-bell-slash::before {
+ content: '\f631';
+}
+
+.bi-cash-coin::before {
+ content: '\f632';
+}
+
+.bi-check-lg::before {
+ content: '\f633';
+}
+
+.bi-coin::before {
+ content: '\f634';
+}
+
+.bi-currency-bitcoin::before {
+ content: '\f635';
+}
+
+.bi-currency-dollar::before {
+ content: '\f636';
+}
+
+.bi-currency-euro::before {
+ content: '\f637';
+}
+
+.bi-currency-exchange::before {
+ content: '\f638';
+}
+
+.bi-currency-pound::before {
+ content: '\f639';
+}
+
+.bi-currency-yen::before {
+ content: '\f63a';
+}
+
+.bi-dash-lg::before {
+ content: '\f63b';
+}
+
+.bi-exclamation-lg::before {
+ content: '\f63c';
+}
+
+.bi-file-earmark-pdf-fill::before {
+ content: '\f63d';
+}
+
+.bi-file-earmark-pdf::before {
+ content: '\f63e';
+}
+
+.bi-file-pdf-fill::before {
+ content: '\f63f';
+}
+
+.bi-file-pdf::before {
+ content: '\f640';
+}
+
+.bi-gender-ambiguous::before {
+ content: '\f641';
+}
+
+.bi-gender-female::before {
+ content: '\f642';
+}
+
+.bi-gender-male::before {
+ content: '\f643';
+}
+
+.bi-gender-trans::before {
+ content: '\f644';
+}
+
+.bi-headset-vr::before {
+ content: '\f645';
+}
+
+.bi-info-lg::before {
+ content: '\f646';
+}
+
+.bi-mastodon::before {
+ content: '\f647';
+}
+
+.bi-messenger::before {
+ content: '\f648';
+}
+
+.bi-piggy-bank-fill::before {
+ content: '\f649';
+}
+
+.bi-piggy-bank::before {
+ content: '\f64a';
+}
+
+.bi-pin-map-fill::before {
+ content: '\f64b';
+}
+
+.bi-pin-map::before {
+ content: '\f64c';
+}
+
+.bi-plus-lg::before {
+ content: '\f64d';
+}
+
+.bi-question-lg::before {
+ content: '\f64e';
+}
+
+.bi-recycle::before {
+ content: '\f64f';
+}
+
+.bi-reddit::before {
+ content: '\f650';
+}
+
+.bi-safe-fill::before {
+ content: '\f651';
+}
+
+.bi-safe2-fill::before {
+ content: '\f652';
+}
+
+.bi-safe2::before {
+ content: '\f653';
+}
+
+.bi-sd-card-fill::before {
+ content: '\f654';
+}
+
+.bi-sd-card::before {
+ content: '\f655';
+}
+
+.bi-skype::before {
+ content: '\f656';
+}
+
+.bi-slash-lg::before {
+ content: '\f657';
+}
+
+.bi-translate::before {
+ content: '\f658';
+}
+
+.bi-x-lg::before {
+ content: '\f659';
+}
+
+.bi-safe::before {
+ content: '\f65a';
+}
diff --git a/src/assets/css/bootstrap.css b/src/assets/css/bootstrap.css
new file mode 100644
index 0000000..4a94237
--- /dev/null
+++ b/src/assets/css/bootstrap.css
@@ -0,0 +1,11887 @@
+
+
+@charset "UTF-8";
+/*!
+ * Bootstrap v5.1.3 (https://getbootstrap.com/)
+ * Copyright 2011-2021 The Bootstrap Authors
+ * Copyright 2011-2021 Twitter, Inc.
+ * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
+ */
+:root {
+ --bs-blue: #0d6efd;
+ --bs-indigo: #6610f2;
+ --bs-purple: #6f42c1;
+ --bs-pink: #d63384;
+ --bs-red: #dc3545;
+ --bs-orange: #fd7e14;
+ --bs-yellow: #ffc107;
+ --bs-green: #198754;
+ --bs-teal: #20c997;
+ --bs-cyan: #0dcaf0;
+ --bs-white: #fff;
+ --bs-gray: #6c757d;
+ --bs-gray-dark: #343a40;
+ --bs-gray-100: #f8f9fa;
+ --bs-gray-200: #e9ecef;
+ --bs-gray-300: #dee2e6;
+ --bs-gray-400: #ced4da;
+ --bs-gray-500: #adb5bd;
+ --bs-gray-600: #6c757d;
+ --bs-gray-700: #495057;
+ --bs-gray-800: #343a40;
+ --bs-gray-900: #212529;
+ --bs-primary: #0d6efd;
+ --bs-secondary: #6c757d;
+ --bs-success: #198754;
+ --bs-info: #0dcaf0;
+ --bs-warning: #ffc107;
+ --bs-danger: #dc3545;
+ --bs-light: #f8f9fa;
+ --bs-dark: #212529;
+ --bs-primary-rgb: 13, 110, 253;
+ --bs-secondary-rgb: 108, 117, 125;
+ --bs-success-rgb: 25, 135, 84;
+ --bs-info-rgb: 13, 202, 240;
+ --bs-warning-rgb: 255, 193, 7;
+ --bs-danger-rgb: 220, 53, 69;
+ --bs-light-rgb: 248, 249, 250;
+ --bs-dark-rgb: 33, 37, 41;
+ --bs-white-rgb: 255, 255, 255;
+ --bs-black-rgb: 0, 0, 0;
+ --bs-body-color-rgb: 33, 37, 41;
+ --bs-body-bg-rgb: 255, 255, 255;
+ --bs-font-sans-serif: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "Liberation Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
+ --bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
+ --bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));
+ --bs-body-font-family: var(--bs-font-sans-serif);
+ --bs-body-font-size: 1rem;
+ --bs-body-font-weight: 400;
+ --bs-body-line-height: 1.5;
+ --bs-body-color: #212529;
+ --bs-body-bg: #fff;
+}
+
+*,
+*::before,
+*::after {
+ box-sizing: border-box;
+}
+
+@media (prefers-reduced-motion: no-preference) {
+ :root {
+ scroll-behavior: smooth;
+ }
+}
+
+body {
+ margin: 0;
+ font-family: var(--bs-body-font-family);
+ font-size: var(--bs-body-font-size);
+ font-weight: var(--bs-body-font-weight);
+ line-height: var(--bs-body-line-height);
+ color: var(--bs-body-color);
+ text-align: var(--bs-body-text-align);
+ background-color: var(--bs-body-bg);
+ -webkit-text-size-adjust: 100%;
+ -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
+}
+
+hr {
+ margin: 1rem 0;
+ color: inherit;
+ background-color: currentColor;
+ border: 0;
+ opacity: 0.25;
+}
+
+hr:not([size]) {
+ height: 1px;
+}
+
+h6, .h6, h5, .h5, h4, .h4, h3, .h3, h2, .h2, h1, .h1 {
+ margin-top: 0;
+ margin-bottom: 0.5rem;
+ font-weight: 500;
+ line-height: 1.2;
+}
+
+h1, .h1 {
+ font-size: calc(1.375rem + 1.5vw);
+}
+
+@media (min-width: 1200px) {
+ h1, .h1 {
+ font-size: 2.5rem;
+ }
+}
+
+h2, .h2 {
+ font-size: calc(1.325rem + 0.9vw);
+}
+
+@media (min-width: 1200px) {
+ h2, .h2 {
+ font-size: 2rem;
+ }
+}
+
+h3, .h3 {
+ font-size: calc(1.3rem + 0.6vw);
+}
+
+@media (min-width: 1200px) {
+ h3, .h3 {
+ font-size: 1.75rem;
+ }
+}
+
+h4, .h4 {
+ font-size: calc(1.275rem + 0.3vw);
+}
+
+@media (min-width: 1200px) {
+ h4, .h4 {
+ font-size: 1.5rem;
+ }
+}
+
+h5, .h5 {
+ font-size: 1.25rem;
+}
+
+h6, .h6 {
+ font-size: 1rem;
+}
+
+p {
+ margin-top: 0;
+ margin-bottom: 1rem;
+}
+
+abbr[title],
+abbr[data-bs-original-title] {
+ -webkit-text-decoration: underline dotted;
+ text-decoration: underline dotted;
+ cursor: help;
+ -webkit-text-decoration-skip-ink: none;
+ text-decoration-skip-ink: none;
+}
+
+address {
+ margin-bottom: 1rem;
+ font-style: normal;
+ line-height: inherit;
+}
+
+ol,
+ul {
+ padding-left: 2rem;
+}
+
+ol,
+ul,
+dl {
+ margin-top: 0;
+ margin-bottom: 1rem;
+}
+
+ol ol,
+ul ul,
+ol ul,
+ul ol {
+ margin-bottom: 0;
+}
+
+dt {
+ font-weight: 700;
+}
+
+dd {
+ margin-bottom: 0.5rem;
+ margin-left: 0;
+}
+
+blockquote {
+ margin: 0 0 1rem;
+}
+
+b,
+strong {
+ font-weight: bolder;
+}
+
+small, .small {
+ font-size: 0.875em;
+}
+
+mark, .mark {
+ padding: 0.2em;
+ background-color: #fcf8e3;
+}
+
+sub,
+sup {
+ position: relative;
+ font-size: 0.75em;
+ line-height: 0;
+ vertical-align: baseline;
+}
+
+sub {
+ bottom: -0.25em;
+}
+
+sup {
+ top: -0.5em;
+}
+
+a {
+ color: #0d6efd;
+ text-decoration: underline;
+}
+
+a:hover {
+ color: #0a58ca;
+}
+
+a:not([href]):not([class]), a:not([href]):not([class]):hover {
+ color: inherit;
+ text-decoration: none;
+}
+
+pre,
+code,
+kbd,
+samp {
+ font-family: var(--bs-font-monospace);
+ font-size: 1em;
+ direction: ltr /* rtl:ignore */;
+ unicode-bidi: bidi-override;
+}
+
+pre {
+ display: block;
+ margin-top: 0;
+ margin-bottom: 1rem;
+ overflow: auto;
+ font-size: 0.875em;
+}
+
+pre code {
+ font-size: inherit;
+ color: inherit;
+ word-break: normal;
+}
+
+code {
+ font-size: 0.875em;
+ color: #d63384;
+ word-wrap: break-word;
+}
+
+a > code {
+ color: inherit;
+}
+
+kbd {
+ padding: 0.2rem 0.4rem;
+ font-size: 0.875em;
+ color: #fff;
+ background-color: #212529;
+ border-radius: 0.2rem;
+}
+
+kbd kbd {
+ padding: 0;
+ font-size: 1em;
+ font-weight: 700;
+}
+
+figure {
+ margin: 0 0 1rem;
+}
+
+img,
+svg {
+ vertical-align: middle;
+}
+
+table {
+ caption-side: bottom;
+ border-collapse: collapse;
+}
+
+caption {
+ padding-top: 0.5rem;
+ padding-bottom: 0.5rem;
+ color: #6c757d;
+ text-align: left;
+}
+
+th {
+ text-align: inherit;
+ text-align: -webkit-match-parent;
+}
+
+thead,
+tbody,
+tfoot,
+tr,
+td,
+th {
+ border-color: inherit;
+ border-style: solid;
+ border-width: 0;
+}
+
+label {
+ display: inline-block;
+}
+
+button {
+ border-radius: 0;
+}
+
+button:focus:not(:focus-visible) {
+ outline: 0;
+}
+
+input,
+button,
+select,
+optgroup,
+textarea {
+ margin: 0;
+ font-family: inherit;
+ font-size: inherit;
+ line-height: inherit;
+}
+
+button,
+select {
+ text-transform: none;
+}
+
+[role=button] {
+ cursor: pointer;
+}
+
+select {
+ word-wrap: normal;
+}
+
+select:disabled {
+ opacity: 1;
+}
+
+[list]::-webkit-calendar-picker-indicator {
+ display: none;
+}
+
+button,
+[type=button],
+[type=reset],
+[type=submit] {
+ -webkit-appearance: button;
+}
+
+button:not(:disabled),
+[type=button]:not(:disabled),
+[type=reset]:not(:disabled),
+[type=submit]:not(:disabled) {
+ cursor: pointer;
+}
+
+::-moz-focus-inner {
+ padding: 0;
+ border-style: none;
+}
+
+textarea {
+ resize: vertical;
+}
+
+fieldset {
+ min-width: 0;
+ padding: 0;
+ margin: 0;
+ border: 0;
+}
+
+legend {
+ float: left;
+ width: 100%;
+ padding: 0;
+ margin-bottom: 0.5rem;
+ font-size: calc(1.275rem + 0.3vw);
+ line-height: inherit;
+}
+
+@media (min-width: 1200px) {
+ legend {
+ font-size: 1.5rem;
+ }
+}
+
+legend + * {
+ clear: left;
+}
+
+::-webkit-datetime-edit-fields-wrapper,
+::-webkit-datetime-edit-text,
+::-webkit-datetime-edit-minute,
+::-webkit-datetime-edit-hour-field,
+::-webkit-datetime-edit-day-field,
+::-webkit-datetime-edit-month-field,
+::-webkit-datetime-edit-year-field {
+ padding: 0;
+}
+
+::-webkit-inner-spin-button {
+ height: auto;
+}
+
+[type=search] {
+ outline-offset: -2px;
+ -webkit-appearance: textfield;
+}
+
+/* rtl:raw:
+[type="tel"],
+[type="url"],
+[type="email"],
+[type="number"] {
+ direction: ltr;
+}
+*/
+::-webkit-search-decoration {
+ -webkit-appearance: none;
+}
+
+::-webkit-color-swatch-wrapper {
+ padding: 0;
+}
+
+::-webkit-file-upload-button {
+ font: inherit;
+}
+
+::file-selector-button {
+ font: inherit;
+}
+
+::-webkit-file-upload-button {
+ font: inherit;
+ -webkit-appearance: button;
+}
+
+output {
+ display: inline-block;
+}
+
+iframe {
+ border: 0;
+}
+
+summary {
+ display: list-item;
+ cursor: pointer;
+}
+
+progress {
+ vertical-align: baseline;
+}
+
+[hidden] {
+ display: none !important;
+}
+
+.lead {
+ font-size: 1.25rem;
+ font-weight: 300;
+}
+
+.display-1 {
+ font-size: calc(1.625rem + 4.5vw);
+ font-weight: 300;
+ line-height: 1.2;
+}
+
+@media (min-width: 1200px) {
+ .display-1 {
+ font-size: 5rem;
+ }
+}
+
+.display-2 {
+ font-size: calc(1.575rem + 3.9vw);
+ font-weight: 300;
+ line-height: 1.2;
+}
+
+@media (min-width: 1200px) {
+ .display-2 {
+ font-size: 4.5rem;
+ }
+}
+
+.display-3 {
+ font-size: calc(1.525rem + 3.3vw);
+ font-weight: 300;
+ line-height: 1.2;
+}
+
+@media (min-width: 1200px) {
+ .display-3 {
+ font-size: 4rem;
+ }
+}
+
+.display-4 {
+ font-size: calc(1.475rem + 2.7vw);
+ font-weight: 300;
+ line-height: 1.2;
+}
+
+@media (min-width: 1200px) {
+ .display-4 {
+ font-size: 3.5rem;
+ }
+}
+
+.display-5 {
+ font-size: calc(1.425rem + 2.1vw);
+ font-weight: 300;
+ line-height: 1.2;
+}
+
+@media (min-width: 1200px) {
+ .display-5 {
+ font-size: 3rem;
+ }
+}
+
+.display-6 {
+ font-size: calc(1.375rem + 1.5vw);
+ font-weight: 300;
+ line-height: 1.2;
+}
+
+@media (min-width: 1200px) {
+ .display-6 {
+ font-size: 2.5rem;
+ }
+}
+
+.list-unstyled {
+ padding-left: 0;
+ list-style: none;
+}
+
+.list-inline {
+ padding-left: 0;
+ list-style: none;
+}
+
+.list-inline-item {
+ display: inline-block;
+}
+
+.list-inline-item:not(:last-child) {
+ margin-right: 0.5rem;
+}
+
+.initialism {
+ font-size: 0.875em;
+ text-transform: uppercase;
+}
+
+.blockquote {
+ margin-bottom: 1rem;
+ font-size: 1.25rem;
+}
+
+.blockquote > :last-child {
+ margin-bottom: 0;
+}
+
+.blockquote-footer {
+ margin-top: -1rem;
+ margin-bottom: 1rem;
+ font-size: 0.875em;
+ color: #6c757d;
+}
+
+.blockquote-footer::before {
+ content: "— ";
+}
+
+.img-fluid {
+ max-width: 100%;
+ height: auto;
+}
+
+.img-thumbnail {
+ padding: 0.25rem;
+ background-color: #fff;
+ border: 1px solid #dee2e6;
+ border-radius: 0.25rem;
+ max-width: 100%;
+ height: auto;
+}
+
+.figure {
+ display: inline-block;
+}
+
+.figure-img {
+ margin-bottom: 0.5rem;
+ line-height: 1;
+}
+
+.figure-caption {
+ font-size: 0.875em;
+ color: #6c757d;
+}
+
+.container,
+.container-fluid,
+.container-xxl,
+.container-xl,
+.container-lg,
+.container-md,
+.container-sm {
+ width: 100%;
+ padding-right: var(--bs-gutter-x, 0.75rem);
+ padding-left: var(--bs-gutter-x, 0.75rem);
+ margin-right: auto;
+ margin-left: auto;
+}
+
+@media (min-width: 576px) {
+ .container-sm, .container {
+ max-width: 540px;
+ }
+}
+
+@media (min-width: 768px) {
+ .container-md, .container-sm, .container {
+ max-width: 720px;
+ }
+}
+
+@media (min-width: 992px) {
+ .container-lg, .container-md, .container-sm, .container {
+ max-width: 960px;
+ }
+}
+
+@media (min-width: 1200px) {
+ .container-xl, .container-lg, .container-md, .container-sm, .container {
+ max-width: 1140px;
+ }
+}
+
+@media (min-width: 1400px) {
+ .container-xxl, .container-xl, .container-lg, .container-md, .container-sm, .container {
+ max-width: 1320px;
+ }
+}
+
+.row {
+ --bs-gutter-x: 1.5rem;
+ --bs-gutter-y: 0;
+ display: flex;
+ flex-wrap: wrap;
+ margin-top: calc(-1 * var(--bs-gutter-y));
+ margin-right: calc(-0.5 * var(--bs-gutter-x));
+ margin-left: calc(-0.5 * var(--bs-gutter-x));
+}
+
+.row > * {
+ flex-shrink: 0;
+ width: 100%;
+ max-width: 100%;
+ padding-right: calc(var(--bs-gutter-x) * 0.5);
+ padding-left: calc(var(--bs-gutter-x) * 0.5);
+ margin-top: var(--bs-gutter-y);
+}
+
+.col {
+ flex: 1 0 0%;
+}
+
+.row-cols-auto > * {
+ flex: 0 0 auto;
+ width: auto;
+}
+
+.row-cols-1 > * {
+ flex: 0 0 auto;
+ width: 100%;
+}
+
+.row-cols-2 > * {
+ flex: 0 0 auto;
+ width: 50%;
+}
+
+.row-cols-3 > * {
+ flex: 0 0 auto;
+ width: 33.3333333333%;
+}
+
+.row-cols-4 > * {
+ flex: 0 0 auto;
+ width: 25%;
+}
+
+.row-cols-5 > * {
+ flex: 0 0 auto;
+ width: 20%;
+}
+
+.row-cols-6 > * {
+ flex: 0 0 auto;
+ width: 16.6666666667%;
+}
+
+.col-auto {
+ flex: 0 0 auto;
+ width: auto;
+}
+
+.col-1 {
+ flex: 0 0 auto;
+ width: 8.33333333%;
+}
+
+.col-2 {
+ flex: 0 0 auto;
+ width: 16.66666667%;
+}
+
+.col-3 {
+ flex: 0 0 auto;
+ width: 25%;
+}
+
+.col-4 {
+ flex: 0 0 auto;
+ width: 33.33333333%;
+}
+
+.col-5 {
+ flex: 0 0 auto;
+ width: 41.66666667%;
+}
+
+.col-6 {
+ flex: 0 0 auto;
+ width: 50%;
+}
+
+.col-7 {
+ flex: 0 0 auto;
+ width: 58.33333333%;
+}
+
+.col-8 {
+ flex: 0 0 auto;
+ width: 66.66666667%;
+}
+
+.col-9 {
+ flex: 0 0 auto;
+ width: 75%;
+}
+
+.col-10 {
+ flex: 0 0 auto;
+ width: 83.33333333%;
+}
+
+.col-11 {
+ flex: 0 0 auto;
+ width: 91.66666667%;
+}
+
+.col-12 {
+ flex: 0 0 auto;
+ width: 100%;
+}
+
+.offset-1 {
+ margin-left: 8.33333333%;
+}
+
+.offset-2 {
+ margin-left: 16.66666667%;
+}
+
+.offset-3 {
+ margin-left: 25%;
+}
+
+.offset-4 {
+ margin-left: 33.33333333%;
+}
+
+.offset-5 {
+ margin-left: 41.66666667%;
+}
+
+.offset-6 {
+ margin-left: 50%;
+}
+
+.offset-7 {
+ margin-left: 58.33333333%;
+}
+
+.offset-8 {
+ margin-left: 66.66666667%;
+}
+
+.offset-9 {
+ margin-left: 75%;
+}
+
+.offset-10 {
+ margin-left: 83.33333333%;
+}
+
+.offset-11 {
+ margin-left: 91.66666667%;
+}
+
+.g-0,
+.gx-0 {
+ --bs-gutter-x: 0;
+}
+
+.g-0,
+.gy-0 {
+ --bs-gutter-y: 0;
+}
+
+.g-1,
+.gx-1 {
+ --bs-gutter-x: 0.25rem;
+}
+
+.g-1,
+.gy-1 {
+ --bs-gutter-y: 0.25rem;
+}
+
+.g-2,
+.gx-2 {
+ --bs-gutter-x: 0.5rem;
+}
+
+.g-2,
+.gy-2 {
+ --bs-gutter-y: 0.5rem;
+}
+
+.g-3,
+.gx-3 {
+ --bs-gutter-x: 1rem;
+}
+
+.g-3,
+.gy-3 {
+ --bs-gutter-y: 1rem;
+}
+
+.g-4,
+.gx-4 {
+ --bs-gutter-x: 1.5rem;
+}
+
+.g-4,
+.gy-4 {
+ --bs-gutter-y: 1.5rem;
+}
+
+.g-5,
+.gx-5 {
+ --bs-gutter-x: 3rem;
+}
+
+.g-5,
+.gy-5 {
+ --bs-gutter-y: 3rem;
+}
+
+@media (min-width: 576px) {
+ .col-sm {
+ flex: 1 0 0%;
+ }
+
+ .row-cols-sm-auto > * {
+ flex: 0 0 auto;
+ width: auto;
+ }
+
+ .row-cols-sm-1 > * {
+ flex: 0 0 auto;
+ width: 100%;
+ }
+
+ .row-cols-sm-2 > * {
+ flex: 0 0 auto;
+ width: 50%;
+ }
+
+ .row-cols-sm-3 > * {
+ flex: 0 0 auto;
+ width: 33.3333333333%;
+ }
+
+ .row-cols-sm-4 > * {
+ flex: 0 0 auto;
+ width: 25%;
+ }
+
+ .row-cols-sm-5 > * {
+ flex: 0 0 auto;
+ width: 20%;
+ }
+
+ .row-cols-sm-6 > * {
+ flex: 0 0 auto;
+ width: 16.6666666667%;
+ }
+
+ .col-sm-auto {
+ flex: 0 0 auto;
+ width: auto;
+ }
+
+ .col-sm-1 {
+ flex: 0 0 auto;
+ width: 8.33333333%;
+ }
+
+ .col-sm-2 {
+ flex: 0 0 auto;
+ width: 16.66666667%;
+ }
+
+ .col-sm-3 {
+ flex: 0 0 auto;
+ width: 25%;
+ }
+
+ .col-sm-4 {
+ flex: 0 0 auto;
+ width: 33.33333333%;
+ }
+
+ .col-sm-5 {
+ flex: 0 0 auto;
+ width: 41.66666667%;
+ }
+
+ .col-sm-6 {
+ flex: 0 0 auto;
+ width: 50%;
+ }
+
+ .col-sm-7 {
+ flex: 0 0 auto;
+ width: 58.33333333%;
+ }
+
+ .col-sm-8 {
+ flex: 0 0 auto;
+ width: 66.66666667%;
+ }
+
+ .col-sm-9 {
+ flex: 0 0 auto;
+ width: 75%;
+ }
+
+ .col-sm-10 {
+ flex: 0 0 auto;
+ width: 83.33333333%;
+ }
+
+ .col-sm-11 {
+ flex: 0 0 auto;
+ width: 91.66666667%;
+ }
+
+ .col-sm-12 {
+ flex: 0 0 auto;
+ width: 100%;
+ }
+
+ .offset-sm-0 {
+ margin-left: 0;
+ }
+
+ .offset-sm-1 {
+ margin-left: 8.33333333%;
+ }
+
+ .offset-sm-2 {
+ margin-left: 16.66666667%;
+ }
+
+ .offset-sm-3 {
+ margin-left: 25%;
+ }
+
+ .offset-sm-4 {
+ margin-left: 33.33333333%;
+ }
+
+ .offset-sm-5 {
+ margin-left: 41.66666667%;
+ }
+
+ .offset-sm-6 {
+ margin-left: 50%;
+ }
+
+ .offset-sm-7 {
+ margin-left: 58.33333333%;
+ }
+
+ .offset-sm-8 {
+ margin-left: 66.66666667%;
+ }
+
+ .offset-sm-9 {
+ margin-left: 75%;
+ }
+
+ .offset-sm-10 {
+ margin-left: 83.33333333%;
+ }
+
+ .offset-sm-11 {
+ margin-left: 91.66666667%;
+ }
+
+ .g-sm-0,
+ .gx-sm-0 {
+ --bs-gutter-x: 0;
+ }
+
+ .g-sm-0,
+ .gy-sm-0 {
+ --bs-gutter-y: 0;
+ }
+
+ .g-sm-1,
+ .gx-sm-1 {
+ --bs-gutter-x: 0.25rem;
+ }
+
+ .g-sm-1,
+ .gy-sm-1 {
+ --bs-gutter-y: 0.25rem;
+ }
+
+ .g-sm-2,
+ .gx-sm-2 {
+ --bs-gutter-x: 0.5rem;
+ }
+
+ .g-sm-2,
+ .gy-sm-2 {
+ --bs-gutter-y: 0.5rem;
+ }
+
+ .g-sm-3,
+ .gx-sm-3 {
+ --bs-gutter-x: 1rem;
+ }
+
+ .g-sm-3,
+ .gy-sm-3 {
+ --bs-gutter-y: 1rem;
+ }
+
+ .g-sm-4,
+ .gx-sm-4 {
+ --bs-gutter-x: 1.5rem;
+ }
+
+ .g-sm-4,
+ .gy-sm-4 {
+ --bs-gutter-y: 1.5rem;
+ }
+
+ .g-sm-5,
+ .gx-sm-5 {
+ --bs-gutter-x: 3rem;
+ }
+
+ .g-sm-5,
+ .gy-sm-5 {
+ --bs-gutter-y: 3rem;
+ }
+}
+
+@media (min-width: 768px) {
+ .col-md {
+ flex: 1 0 0%;
+ }
+
+ .row-cols-md-auto > * {
+ flex: 0 0 auto;
+ width: auto;
+ }
+
+ .row-cols-md-1 > * {
+ flex: 0 0 auto;
+ width: 100%;
+ }
+
+ .row-cols-md-2 > * {
+ flex: 0 0 auto;
+ width: 50%;
+ }
+
+ .row-cols-md-3 > * {
+ flex: 0 0 auto;
+ width: 33.3333333333%;
+ }
+
+ .row-cols-md-4 > * {
+ flex: 0 0 auto;
+ width: 25%;
+ }
+
+ .row-cols-md-5 > * {
+ flex: 0 0 auto;
+ width: 20%;
+ }
+
+ .row-cols-md-6 > * {
+ flex: 0 0 auto;
+ width: 16.6666666667%;
+ }
+
+ .col-md-auto {
+ flex: 0 0 auto;
+ width: auto;
+ }
+
+ .col-md-1 {
+ flex: 0 0 auto;
+ width: 8.33333333%;
+ }
+
+ .col-md-2 {
+ flex: 0 0 auto;
+ width: 16.66666667%;
+ }
+
+ .col-md-3 {
+ flex: 0 0 auto;
+ width: 25%;
+ }
+
+ .col-md-4 {
+ flex: 0 0 auto;
+ width: 33.33333333%;
+ }
+
+ .col-md-5 {
+ flex: 0 0 auto;
+ width: 41.66666667%;
+ }
+
+ .col-md-6 {
+ flex: 0 0 auto;
+ width: 50%;
+ }
+
+ .col-md-7 {
+ flex: 0 0 auto;
+ width: 58.33333333%;
+ }
+
+ .col-md-8 {
+ flex: 0 0 auto;
+ width: 66.66666667%;
+ }
+
+ .col-md-9 {
+ flex: 0 0 auto;
+ width: 75%;
+ }
+
+ .col-md-10 {
+ flex: 0 0 auto;
+ width: 83.33333333%;
+ }
+
+ .col-md-11 {
+ flex: 0 0 auto;
+ width: 91.66666667%;
+ }
+
+ .col-md-12 {
+ flex: 0 0 auto;
+ width: 100%;
+ }
+
+ .offset-md-0 {
+ margin-left: 0;
+ }
+
+ .offset-md-1 {
+ margin-left: 8.33333333%;
+ }
+
+ .offset-md-2 {
+ margin-left: 16.66666667%;
+ }
+
+ .offset-md-3 {
+ margin-left: 25%;
+ }
+
+ .offset-md-4 {
+ margin-left: 33.33333333%;
+ }
+
+ .offset-md-5 {
+ margin-left: 41.66666667%;
+ }
+
+ .offset-md-6 {
+ margin-left: 50%;
+ }
+
+ .offset-md-7 {
+ margin-left: 58.33333333%;
+ }
+
+ .offset-md-8 {
+ margin-left: 66.66666667%;
+ }
+
+ .offset-md-9 {
+ margin-left: 75%;
+ }
+
+ .offset-md-10 {
+ margin-left: 83.33333333%;
+ }
+
+ .offset-md-11 {
+ margin-left: 91.66666667%;
+ }
+
+ .g-md-0,
+ .gx-md-0 {
+ --bs-gutter-x: 0;
+ }
+
+ .g-md-0,
+ .gy-md-0 {
+ --bs-gutter-y: 0;
+ }
+
+ .g-md-1,
+ .gx-md-1 {
+ --bs-gutter-x: 0.25rem;
+ }
+
+ .g-md-1,
+ .gy-md-1 {
+ --bs-gutter-y: 0.25rem;
+ }
+
+ .g-md-2,
+ .gx-md-2 {
+ --bs-gutter-x: 0.5rem;
+ }
+
+ .g-md-2,
+ .gy-md-2 {
+ --bs-gutter-y: 0.5rem;
+ }
+
+ .g-md-3,
+ .gx-md-3 {
+ --bs-gutter-x: 1rem;
+ }
+
+ .g-md-3,
+ .gy-md-3 {
+ --bs-gutter-y: 1rem;
+ }
+
+ .g-md-4,
+ .gx-md-4 {
+ --bs-gutter-x: 1.5rem;
+ }
+
+ .g-md-4,
+ .gy-md-4 {
+ --bs-gutter-y: 1.5rem;
+ }
+
+ .g-md-5,
+ .gx-md-5 {
+ --bs-gutter-x: 3rem;
+ }
+
+ .g-md-5,
+ .gy-md-5 {
+ --bs-gutter-y: 3rem;
+ }
+}
+
+@media (min-width: 992px) {
+ .col-lg {
+ flex: 1 0 0%;
+ }
+
+ .row-cols-lg-auto > * {
+ flex: 0 0 auto;
+ width: auto;
+ }
+
+ .row-cols-lg-1 > * {
+ flex: 0 0 auto;
+ width: 100%;
+ }
+
+ .row-cols-lg-2 > * {
+ flex: 0 0 auto;
+ width: 50%;
+ }
+
+ .row-cols-lg-3 > * {
+ flex: 0 0 auto;
+ width: 33.3333333333%;
+ }
+
+ .row-cols-lg-4 > * {
+ flex: 0 0 auto;
+ width: 25%;
+ }
+
+ .row-cols-lg-5 > * {
+ flex: 0 0 auto;
+ width: 20%;
+ }
+
+ .row-cols-lg-6 > * {
+ flex: 0 0 auto;
+ width: 16.6666666667%;
+ }
+
+ .col-lg-auto {
+ flex: 0 0 auto;
+ width: auto;
+ }
+
+ .col-lg-1 {
+ flex: 0 0 auto;
+ width: 8.33333333%;
+ }
+
+ .col-lg-2 {
+ flex: 0 0 auto;
+ width: 16.66666667%;
+ }
+
+ .col-lg-3 {
+ flex: 0 0 auto;
+ width: 25%;
+ }
+
+ .col-lg-4 {
+ flex: 0 0 auto;
+ width: 33.33333333%;
+ }
+
+ .col-lg-5 {
+ flex: 0 0 auto;
+ width: 41.66666667%;
+ }
+
+ .col-lg-6 {
+ flex: 0 0 auto;
+ width: 50%;
+ }
+
+ .col-lg-7 {
+ flex: 0 0 auto;
+ width: 58.33333333%;
+ }
+
+ .col-lg-8 {
+ flex: 0 0 auto;
+ width: 66.66666667%;
+ }
+
+ .col-lg-9 {
+ flex: 0 0 auto;
+ width: 75%;
+ }
+
+ .col-lg-10 {
+ flex: 0 0 auto;
+ width: 83.33333333%;
+ }
+
+ .col-lg-11 {
+ flex: 0 0 auto;
+ width: 91.66666667%;
+ }
+
+ .col-lg-12 {
+ flex: 0 0 auto;
+ width: 100%;
+ }
+
+ .offset-lg-0 {
+ margin-left: 0;
+ }
+
+ .offset-lg-1 {
+ margin-left: 8.33333333%;
+ }
+
+ .offset-lg-2 {
+ margin-left: 16.66666667%;
+ }
+
+ .offset-lg-3 {
+ margin-left: 25%;
+ }
+
+ .offset-lg-4 {
+ margin-left: 33.33333333%;
+ }
+
+ .offset-lg-5 {
+ margin-left: 41.66666667%;
+ }
+
+ .offset-lg-6 {
+ margin-left: 50%;
+ }
+
+ .offset-lg-7 {
+ margin-left: 58.33333333%;
+ }
+
+ .offset-lg-8 {
+ margin-left: 66.66666667%;
+ }
+
+ .offset-lg-9 {
+ margin-left: 75%;
+ }
+
+ .offset-lg-10 {
+ margin-left: 83.33333333%;
+ }
+
+ .offset-lg-11 {
+ margin-left: 91.66666667%;
+ }
+
+ .g-lg-0,
+ .gx-lg-0 {
+ --bs-gutter-x: 0;
+ }
+
+ .g-lg-0,
+ .gy-lg-0 {
+ --bs-gutter-y: 0;
+ }
+
+ .g-lg-1,
+ .gx-lg-1 {
+ --bs-gutter-x: 0.25rem;
+ }
+
+ .g-lg-1,
+ .gy-lg-1 {
+ --bs-gutter-y: 0.25rem;
+ }
+
+ .g-lg-2,
+ .gx-lg-2 {
+ --bs-gutter-x: 0.5rem;
+ }
+
+ .g-lg-2,
+ .gy-lg-2 {
+ --bs-gutter-y: 0.5rem;
+ }
+
+ .g-lg-3,
+ .gx-lg-3 {
+ --bs-gutter-x: 1rem;
+ }
+
+ .g-lg-3,
+ .gy-lg-3 {
+ --bs-gutter-y: 1rem;
+ }
+
+ .g-lg-4,
+ .gx-lg-4 {
+ --bs-gutter-x: 1.5rem;
+ }
+
+ .g-lg-4,
+ .gy-lg-4 {
+ --bs-gutter-y: 1.5rem;
+ }
+
+ .g-lg-5,
+ .gx-lg-5 {
+ --bs-gutter-x: 3rem;
+ }
+
+ .g-lg-5,
+ .gy-lg-5 {
+ --bs-gutter-y: 3rem;
+ }
+}
+
+@media (min-width: 1200px) {
+ .col-xl {
+ flex: 1 0 0%;
+ }
+
+ .row-cols-xl-auto > * {
+ flex: 0 0 auto;
+ width: auto;
+ }
+
+ .row-cols-xl-1 > * {
+ flex: 0 0 auto;
+ width: 100%;
+ }
+
+ .row-cols-xl-2 > * {
+ flex: 0 0 auto;
+ width: 50%;
+ }
+
+ .row-cols-xl-3 > * {
+ flex: 0 0 auto;
+ width: 33.3333333333%;
+ }
+
+ .row-cols-xl-4 > * {
+ flex: 0 0 auto;
+ width: 25%;
+ }
+
+ .row-cols-xl-5 > * {
+ flex: 0 0 auto;
+ width: 20%;
+ }
+
+ .row-cols-xl-6 > * {
+ flex: 0 0 auto;
+ width: 16.6666666667%;
+ }
+
+ .col-xl-auto {
+ flex: 0 0 auto;
+ width: auto;
+ }
+
+ .col-xl-1 {
+ flex: 0 0 auto;
+ width: 8.33333333%;
+ }
+
+ .col-xl-2 {
+ flex: 0 0 auto;
+ width: 16.66666667%;
+ }
+
+ .col-xl-3 {
+ flex: 0 0 auto;
+ width: 25%;
+ }
+
+ .col-xl-4 {
+ flex: 0 0 auto;
+ width: 33.33333333%;
+ }
+
+ .col-xl-5 {
+ flex: 0 0 auto;
+ width: 41.66666667%;
+ }
+
+ .col-xl-6 {
+ flex: 0 0 auto;
+ width: 50%;
+ }
+
+ .col-xl-7 {
+ flex: 0 0 auto;
+ width: 58.33333333%;
+ }
+
+ .col-xl-8 {
+ flex: 0 0 auto;
+ width: 66.66666667%;
+ }
+
+ .col-xl-9 {
+ flex: 0 0 auto;
+ width: 75%;
+ }
+
+ .col-xl-10 {
+ flex: 0 0 auto;
+ width: 83.33333333%;
+ }
+
+ .col-xl-11 {
+ flex: 0 0 auto;
+ width: 91.66666667%;
+ }
+
+ .col-xl-12 {
+ flex: 0 0 auto;
+ width: 100%;
+ }
+
+ .offset-xl-0 {
+ margin-left: 0;
+ }
+
+ .offset-xl-1 {
+ margin-left: 8.33333333%;
+ }
+
+ .offset-xl-2 {
+ margin-left: 16.66666667%;
+ }
+
+ .offset-xl-3 {
+ margin-left: 25%;
+ }
+
+ .offset-xl-4 {
+ margin-left: 33.33333333%;
+ }
+
+ .offset-xl-5 {
+ margin-left: 41.66666667%;
+ }
+
+ .offset-xl-6 {
+ margin-left: 50%;
+ }
+
+ .offset-xl-7 {
+ margin-left: 58.33333333%;
+ }
+
+ .offset-xl-8 {
+ margin-left: 66.66666667%;
+ }
+
+ .offset-xl-9 {
+ margin-left: 75%;
+ }
+
+ .offset-xl-10 {
+ margin-left: 83.33333333%;
+ }
+
+ .offset-xl-11 {
+ margin-left: 91.66666667%;
+ }
+
+ .g-xl-0,
+ .gx-xl-0 {
+ --bs-gutter-x: 0;
+ }
+
+ .g-xl-0,
+ .gy-xl-0 {
+ --bs-gutter-y: 0;
+ }
+
+ .g-xl-1,
+ .gx-xl-1 {
+ --bs-gutter-x: 0.25rem;
+ }
+
+ .g-xl-1,
+ .gy-xl-1 {
+ --bs-gutter-y: 0.25rem;
+ }
+
+ .g-xl-2,
+ .gx-xl-2 {
+ --bs-gutter-x: 0.5rem;
+ }
+
+ .g-xl-2,
+ .gy-xl-2 {
+ --bs-gutter-y: 0.5rem;
+ }
+
+ .g-xl-3,
+ .gx-xl-3 {
+ --bs-gutter-x: 1rem;
+ }
+
+ .g-xl-3,
+ .gy-xl-3 {
+ --bs-gutter-y: 1rem;
+ }
+
+ .g-xl-4,
+ .gx-xl-4 {
+ --bs-gutter-x: 1.5rem;
+ }
+
+ .g-xl-4,
+ .gy-xl-4 {
+ --bs-gutter-y: 1.5rem;
+ }
+
+ .g-xl-5,
+ .gx-xl-5 {
+ --bs-gutter-x: 3rem;
+ }
+
+ .g-xl-5,
+ .gy-xl-5 {
+ --bs-gutter-y: 3rem;
+ }
+}
+
+@media (min-width: 1400px) {
+ .col-xxl {
+ flex: 1 0 0%;
+ }
+
+ .row-cols-xxl-auto > * {
+ flex: 0 0 auto;
+ width: auto;
+ }
+
+ .row-cols-xxl-1 > * {
+ flex: 0 0 auto;
+ width: 100%;
+ }
+
+ .row-cols-xxl-2 > * {
+ flex: 0 0 auto;
+ width: 50%;
+ }
+
+ .row-cols-xxl-3 > * {
+ flex: 0 0 auto;
+ width: 33.3333333333%;
+ }
+
+ .row-cols-xxl-4 > * {
+ flex: 0 0 auto;
+ width: 25%;
+ }
+
+ .row-cols-xxl-5 > * {
+ flex: 0 0 auto;
+ width: 20%;
+ }
+
+ .row-cols-xxl-6 > * {
+ flex: 0 0 auto;
+ width: 16.6666666667%;
+ }
+
+ .col-xxl-auto {
+ flex: 0 0 auto;
+ width: auto;
+ }
+
+ .col-xxl-1 {
+ flex: 0 0 auto;
+ width: 8.33333333%;
+ }
+
+ .col-xxl-2 {
+ flex: 0 0 auto;
+ width: 16.66666667%;
+ }
+
+ .col-xxl-3 {
+ flex: 0 0 auto;
+ width: 25%;
+ }
+
+ .col-xxl-4 {
+ flex: 0 0 auto;
+ width: 33.33333333%;
+ }
+
+ .col-xxl-5 {
+ flex: 0 0 auto;
+ width: 41.66666667%;
+ }
+
+ .col-xxl-6 {
+ flex: 0 0 auto;
+ width: 50%;
+ }
+
+ .col-xxl-7 {
+ flex: 0 0 auto;
+ width: 58.33333333%;
+ }
+
+ .col-xxl-8 {
+ flex: 0 0 auto;
+ width: 66.66666667%;
+ }
+
+ .col-xxl-9 {
+ flex: 0 0 auto;
+ width: 75%;
+ }
+
+ .col-xxl-10 {
+ flex: 0 0 auto;
+ width: 83.33333333%;
+ }
+
+ .col-xxl-11 {
+ flex: 0 0 auto;
+ width: 91.66666667%;
+ }
+
+ .col-xxl-12 {
+ flex: 0 0 auto;
+ width: 100%;
+ }
+
+ .offset-xxl-0 {
+ margin-left: 0;
+ }
+
+ .offset-xxl-1 {
+ margin-left: 8.33333333%;
+ }
+
+ .offset-xxl-2 {
+ margin-left: 16.66666667%;
+ }
+
+ .offset-xxl-3 {
+ margin-left: 25%;
+ }
+
+ .offset-xxl-4 {
+ margin-left: 33.33333333%;
+ }
+
+ .offset-xxl-5 {
+ margin-left: 41.66666667%;
+ }
+
+ .offset-xxl-6 {
+ margin-left: 50%;
+ }
+
+ .offset-xxl-7 {
+ margin-left: 58.33333333%;
+ }
+
+ .offset-xxl-8 {
+ margin-left: 66.66666667%;
+ }
+
+ .offset-xxl-9 {
+ margin-left: 75%;
+ }
+
+ .offset-xxl-10 {
+ margin-left: 83.33333333%;
+ }
+
+ .offset-xxl-11 {
+ margin-left: 91.66666667%;
+ }
+
+ .g-xxl-0,
+ .gx-xxl-0 {
+ --bs-gutter-x: 0;
+ }
+
+ .g-xxl-0,
+ .gy-xxl-0 {
+ --bs-gutter-y: 0;
+ }
+
+ .g-xxl-1,
+ .gx-xxl-1 {
+ --bs-gutter-x: 0.25rem;
+ }
+
+ .g-xxl-1,
+ .gy-xxl-1 {
+ --bs-gutter-y: 0.25rem;
+ }
+
+ .g-xxl-2,
+ .gx-xxl-2 {
+ --bs-gutter-x: 0.5rem;
+ }
+
+ .g-xxl-2,
+ .gy-xxl-2 {
+ --bs-gutter-y: 0.5rem;
+ }
+
+ .g-xxl-3,
+ .gx-xxl-3 {
+ --bs-gutter-x: 1rem;
+ }
+
+ .g-xxl-3,
+ .gy-xxl-3 {
+ --bs-gutter-y: 1rem;
+ }
+
+ .g-xxl-4,
+ .gx-xxl-4 {
+ --bs-gutter-x: 1.5rem;
+ }
+
+ .g-xxl-4,
+ .gy-xxl-4 {
+ --bs-gutter-y: 1.5rem;
+ }
+
+ .g-xxl-5,
+ .gx-xxl-5 {
+ --bs-gutter-x: 3rem;
+ }
+
+ .g-xxl-5,
+ .gy-xxl-5 {
+ --bs-gutter-y: 3rem;
+ }
+}
+
+.table {
+ --bs-table-bg: transparent;
+ --bs-table-accent-bg: transparent;
+ --bs-table-striped-color: #212529;
+ --bs-table-striped-bg: rgba(0, 0, 0, 0.05);
+ --bs-table-active-color: #212529;
+ --bs-table-active-bg: rgba(0, 0, 0, 0.1);
+ --bs-table-hover-color: #212529;
+ --bs-table-hover-bg: rgba(0, 0, 0, 0.075);
+ width: 100%;
+ margin-bottom: 1rem;
+ color: #212529;
+ vertical-align: top;
+ border-color: #dee2e6;
+}
+
+.table > :not(caption) > * > * {
+ padding: 0.5rem 0.5rem;
+ background-color: var(--bs-table-bg);
+ border-bottom-width: 1px;
+ box-shadow: inset 0 0 0 9999px var(--bs-table-accent-bg);
+}
+
+.table > tbody {
+ vertical-align: inherit;
+}
+
+.table > thead {
+ vertical-align: bottom;
+}
+
+.table > :not(:first-child) {
+ border-top: 2px solid currentColor;
+}
+
+.caption-top {
+ caption-side: top;
+}
+
+.table-sm > :not(caption) > * > * {
+ padding: 0.25rem 0.25rem;
+}
+
+.table-bordered > :not(caption) > * {
+ border-width: 1px 0;
+}
+
+.table-bordered > :not(caption) > * > * {
+ border-width: 0 1px;
+}
+
+.table-borderless > :not(caption) > * > * {
+ border-bottom-width: 0;
+}
+
+.table-borderless > :not(:first-child) {
+ border-top-width: 0;
+}
+
+.table-striped > tbody > tr:nth-of-type(odd) > * {
+ --bs-table-accent-bg: var(--bs-table-striped-bg);
+ color: var(--bs-table-striped-color);
+}
+
+.table-active {
+ --bs-table-accent-bg: var(--bs-table-active-bg);
+ color: var(--bs-table-active-color);
+}
+
+.table-hover > tbody > tr:hover > * {
+ --bs-table-accent-bg: var(--bs-table-hover-bg);
+ color: var(--bs-table-hover-color);
+}
+
+.table-primary {
+ --bs-table-bg: #cfe2ff;
+ --bs-table-striped-bg: #c5d7f2;
+ --bs-table-striped-color: #000;
+ --bs-table-active-bg: #bacbe6;
+ --bs-table-active-color: #000;
+ --bs-table-hover-bg: #bfd1ec;
+ --bs-table-hover-color: #000;
+ color: #000;
+ border-color: #bacbe6;
+}
+
+.table-secondary {
+ --bs-table-bg: #e2e3e5;
+ --bs-table-striped-bg: #d7d8da;
+ --bs-table-striped-color: #000;
+ --bs-table-active-bg: #cbccce;
+ --bs-table-active-color: #000;
+ --bs-table-hover-bg: #d1d2d4;
+ --bs-table-hover-color: #000;
+ color: #000;
+ border-color: #cbccce;
+}
+
+.table-success {
+ --bs-table-bg: #d1e7dd;
+ --bs-table-striped-bg: #c7dbd2;
+ --bs-table-striped-color: #000;
+ --bs-table-active-bg: #bcd0c7;
+ --bs-table-active-color: #000;
+ --bs-table-hover-bg: #c1d6cc;
+ --bs-table-hover-color: #000;
+ color: #000;
+ border-color: #bcd0c7;
+}
+
+.table-info {
+ --bs-table-bg: #cff4fc;
+ --bs-table-striped-bg: #c5e8ef;
+ --bs-table-striped-color: #000;
+ --bs-table-active-bg: #badce3;
+ --bs-table-active-color: #000;
+ --bs-table-hover-bg: #bfe2e9;
+ --bs-table-hover-color: #000;
+ color: #000;
+ border-color: #badce3;
+}
+
+.table-warning {
+ --bs-table-bg: #fff3cd;
+ --bs-table-striped-bg: #f2e7c3;
+ --bs-table-striped-color: #000;
+ --bs-table-active-bg: #e6dbb9;
+ --bs-table-active-color: #000;
+ --bs-table-hover-bg: #ece1be;
+ --bs-table-hover-color: #000;
+ color: #000;
+ border-color: #e6dbb9;
+}
+
+.table-danger {
+ --bs-table-bg: #f8d7da;
+ --bs-table-striped-bg: #eccccf;
+ --bs-table-striped-color: #000;
+ --bs-table-active-bg: #dfc2c4;
+ --bs-table-active-color: #000;
+ --bs-table-hover-bg: #e5c7ca;
+ --bs-table-hover-color: #000;
+ color: #000;
+ border-color: #dfc2c4;
+}
+
+.table-light {
+ --bs-table-bg: #f8f9fa;
+ --bs-table-striped-bg: #ecedee;
+ --bs-table-striped-color: #000;
+ --bs-table-active-bg: #dfe0e1;
+ --bs-table-active-color: #000;
+ --bs-table-hover-bg: #e5e6e7;
+ --bs-table-hover-color: #000;
+ color: #000;
+ border-color: #dfe0e1;
+}
+
+.table-dark {
+ --bs-table-bg: #212529;
+ --bs-table-striped-bg: #2c3034;
+ --bs-table-striped-color: #fff;
+ --bs-table-active-bg: #373b3e;
+ --bs-table-active-color: #fff;
+ --bs-table-hover-bg: #323539;
+ --bs-table-hover-color: #fff;
+ color: #fff;
+ border-color: #373b3e;
+}
+
+.table-responsive {
+ overflow-x: auto;
+ -webkit-overflow-scrolling: touch;
+}
+
+@media (max-width: 575.98px) {
+ .table-responsive-sm {
+ overflow-x: auto;
+ -webkit-overflow-scrolling: touch;
+ }
+}
+
+@media (max-width: 767.98px) {
+ .table-responsive-md {
+ overflow-x: auto;
+ -webkit-overflow-scrolling: touch;
+ }
+}
+
+@media (max-width: 991.98px) {
+ .table-responsive-lg {
+ overflow-x: auto;
+ -webkit-overflow-scrolling: touch;
+ }
+}
+
+@media (max-width: 1199.98px) {
+ .table-responsive-xl {
+ overflow-x: auto;
+ -webkit-overflow-scrolling: touch;
+ }
+}
+
+@media (max-width: 1399.98px) {
+ .table-responsive-xxl {
+ overflow-x: auto;
+ -webkit-overflow-scrolling: touch;
+ }
+}
+
+.form-label {
+ margin-bottom: 0.5rem;
+}
+
+.col-form-label {
+ padding-top: calc(0.375rem + 1px);
+ padding-bottom: calc(0.375rem + 1px);
+ margin-bottom: 0;
+ font-size: inherit;
+ line-height: 1.5;
+}
+
+.col-form-label-lg {
+ padding-top: calc(0.5rem + 1px);
+ padding-bottom: calc(0.5rem + 1px);
+ font-size: 1.25rem;
+}
+
+.col-form-label-sm {
+ padding-top: calc(0.25rem + 1px);
+ padding-bottom: calc(0.25rem + 1px);
+ font-size: 0.875rem;
+}
+
+.form-text {
+ margin-top: 0.25rem;
+ font-size: 0.875em;
+ color: #6c757d;
+}
+
+.form-control {
+ display: block;
+ width: 100%;
+ padding: 0.375rem 0.75rem;
+ font-size: 1rem;
+ font-weight: 400;
+ line-height: 1.5;
+ color: #212529;
+ background-color: #fff;
+ background-clip: padding-box;
+ border: 1px solid #ced4da;
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ appearance: none;
+ border-radius: 0.25rem;
+ transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .form-control {
+ transition: none;
+ }
+}
+
+.form-control[type=file] {
+ overflow: hidden;
+}
+
+.form-control[type=file]:not(:disabled):not([readonly]) {
+ cursor: pointer;
+}
+
+.form-control:focus {
+ color: #212529;
+ background-color: #fff;
+ border-color: #86b7fe;
+ outline: 0;
+ box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
+}
+
+.form-control::-webkit-date-and-time-value {
+ height: 1.5em;
+}
+
+.form-control::-moz-placeholder {
+ color: #6c757d;
+ opacity: 1;
+}
+
+.form-control::placeholder {
+ color: #6c757d;
+ opacity: 1;
+}
+
+.form-control:disabled, .form-control[readonly] {
+ background-color: #e9ecef;
+ opacity: 1;
+}
+
+.form-control::-webkit-file-upload-button {
+ padding: 0.375rem 0.75rem;
+ margin: -0.375rem -0.75rem;
+ -webkit-margin-end: 0.75rem;
+ margin-inline-end: 0.75rem;
+ color: #212529;
+ background-color: #e9ecef;
+ pointer-events: none;
+ border-color: inherit;
+ border-style: solid;
+ border-width: 0;
+ border-inline-end-width: 1px;
+ border-radius: 0;
+ -webkit-transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
+ transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
+}
+
+.form-control::file-selector-button {
+ padding: 0.375rem 0.75rem;
+ margin: -0.375rem -0.75rem;
+ -webkit-margin-end: 0.75rem;
+ margin-inline-end: 0.75rem;
+ color: #212529;
+ background-color: #e9ecef;
+ pointer-events: none;
+ border-color: inherit;
+ border-style: solid;
+ border-width: 0;
+ border-inline-end-width: 1px;
+ border-radius: 0;
+ transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .form-control::-webkit-file-upload-button {
+ -webkit-transition: none;
+ transition: none;
+ }
+
+ .form-control::file-selector-button {
+ transition: none;
+ }
+}
+
+.form-control:hover:not(:disabled):not([readonly])::-webkit-file-upload-button {
+ background-color: #dde0e3;
+}
+
+.form-control:hover:not(:disabled):not([readonly])::file-selector-button {
+ background-color: #dde0e3;
+}
+
+.form-control::-webkit-file-upload-button {
+ padding: 0.375rem 0.75rem;
+ margin: -0.375rem -0.75rem;
+ -webkit-margin-end: 0.75rem;
+ margin-inline-end: 0.75rem;
+ color: #212529;
+ background-color: #e9ecef;
+ pointer-events: none;
+ border-color: inherit;
+ border-style: solid;
+ border-width: 0;
+ border-inline-end-width: 1px;
+ border-radius: 0;
+ -webkit-transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
+ transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .form-control::-webkit-file-upload-button {
+ -webkit-transition: none;
+ transition: none;
+ }
+}
+
+.form-control:hover:not(:disabled):not([readonly])::-webkit-file-upload-button {
+ background-color: #dde0e3;
+}
+
+.form-control-plaintext {
+ display: block;
+ width: 100%;
+ padding: 0.375rem 0;
+ margin-bottom: 0;
+ line-height: 1.5;
+ color: #212529;
+ background-color: transparent;
+ border: solid transparent;
+ border-width: 1px 0;
+}
+
+.form-control-plaintext.form-control-sm, .form-control-plaintext.form-control-lg {
+ padding-right: 0;
+ padding-left: 0;
+}
+
+.form-control-sm {
+ min-height: calc(1.5em + 0.5rem + 2px);
+ padding: 0.25rem 0.5rem;
+ font-size: 0.875rem;
+ border-radius: 0.2rem;
+}
+
+.form-control-sm::-webkit-file-upload-button {
+ padding: 0.25rem 0.5rem;
+ margin: -0.25rem -0.5rem;
+ -webkit-margin-end: 0.5rem;
+ margin-inline-end: 0.5rem;
+}
+
+.form-control-sm::file-selector-button {
+ padding: 0.25rem 0.5rem;
+ margin: -0.25rem -0.5rem;
+ -webkit-margin-end: 0.5rem;
+ margin-inline-end: 0.5rem;
+}
+
+.form-control-sm::-webkit-file-upload-button {
+ padding: 0.25rem 0.5rem;
+ margin: -0.25rem -0.5rem;
+ -webkit-margin-end: 0.5rem;
+ margin-inline-end: 0.5rem;
+}
+
+.form-control-lg {
+ min-height: calc(1.5em + 1rem + 2px);
+ padding: 0.5rem 1rem;
+ font-size: 1.25rem;
+ border-radius: 0.3rem;
+}
+
+.form-control-lg::-webkit-file-upload-button {
+ padding: 0.5rem 1rem;
+ margin: -0.5rem -1rem;
+ -webkit-margin-end: 1rem;
+ margin-inline-end: 1rem;
+}
+
+.form-control-lg::file-selector-button {
+ padding: 0.5rem 1rem;
+ margin: -0.5rem -1rem;
+ -webkit-margin-end: 1rem;
+ margin-inline-end: 1rem;
+}
+
+.form-control-lg::-webkit-file-upload-button {
+ padding: 0.5rem 1rem;
+ margin: -0.5rem -1rem;
+ -webkit-margin-end: 1rem;
+ margin-inline-end: 1rem;
+}
+
+textarea.form-control {
+ min-height: calc(1.5em + 0.75rem + 2px);
+}
+
+textarea.form-control-sm {
+ min-height: calc(1.5em + 0.5rem + 2px);
+}
+
+textarea.form-control-lg {
+ min-height: calc(1.5em + 1rem + 2px);
+}
+
+.form-control-color {
+ width: 3rem;
+ height: auto;
+ padding: 0.375rem;
+}
+
+.form-control-color:not(:disabled):not([readonly]) {
+ cursor: pointer;
+}
+
+.form-control-color::-moz-color-swatch {
+ height: 1.5em;
+ border-radius: 0.25rem;
+}
+
+.form-control-color::-webkit-color-swatch {
+ height: 1.5em;
+ border-radius: 0.25rem;
+}
+
+.form-select {
+ display: block;
+ width: 100%;
+ padding: 0.375rem 2.25rem 0.375rem 0.75rem;
+ -moz-padding-start: calc(0.75rem - 3px);
+ font-size: 1rem;
+ font-weight: 400;
+ line-height: 1.5;
+ color: #212529;
+ background-color: #fff;
+ background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e");
+ background-repeat: no-repeat;
+ background-position: right 0.75rem center;
+ background-size: 16px 12px;
+ border: 1px solid #ced4da;
+ border-radius: 0.25rem;
+ transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ appearance: none;
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .form-select {
+ transition: none;
+ }
+}
+
+.form-select:focus {
+ border-color: #86b7fe;
+ outline: 0;
+ box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
+}
+
+.form-select[multiple], .form-select[size]:not([size="1"]) {
+ padding-right: 0.75rem;
+ background-image: none;
+}
+
+.form-select:disabled {
+ background-color: #e9ecef;
+}
+
+.form-select:-moz-focusring {
+ color: transparent;
+ text-shadow: 0 0 0 #212529;
+}
+
+.form-select-sm {
+ padding-top: 0.25rem;
+ padding-bottom: 0.25rem;
+ padding-left: 0.5rem;
+ font-size: 0.875rem;
+ border-radius: 0.2rem;
+}
+
+.form-select-lg {
+ padding-top: 0.5rem;
+ padding-bottom: 0.5rem;
+ padding-left: 1rem;
+ font-size: 1.25rem;
+ border-radius: 0.3rem;
+}
+
+.form-check {
+ display: block;
+ min-height: 1.5rem;
+ padding-left: 1.5em;
+ margin-bottom: 0.125rem;
+}
+
+.form-check .form-check-input {
+ float: left;
+ margin-left: -1.5em;
+}
+
+.form-check-input {
+ width: 1em;
+ height: 1em;
+ margin-top: 0.25em;
+ vertical-align: top;
+ background-color: #fff;
+ background-repeat: no-repeat;
+ background-position: center;
+ background-size: contain;
+ border: 1px solid rgba(0, 0, 0, 0.25);
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ appearance: none;
+ -webkit-print-color-adjust: exact;
+ color-adjust: exact;
+}
+
+.form-check-input[type=checkbox] {
+ border-radius: 0.25em;
+}
+
+.form-check-input[type=radio] {
+ border-radius: 50%;
+}
+
+.form-check-input:active {
+ filter: brightness(90%);
+}
+
+.form-check-input:focus {
+ border-color: #86b7fe;
+ outline: 0;
+ box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
+}
+
+.form-check-input:checked {
+ background-color: #0d6efd;
+ border-color: #0d6efd;
+}
+
+.form-check-input:checked[type=checkbox] {
+ background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10l3 3l6-6'/%3e%3c/svg%3e");
+}
+
+.form-check-input:checked[type=radio] {
+ background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='2' fill='%23fff'/%3e%3c/svg%3e");
+}
+
+.form-check-input[type=checkbox]:indeterminate {
+ background-color: #0d6efd;
+ border-color: #0d6efd;
+ background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10h8'/%3e%3c/svg%3e");
+}
+
+.form-check-input:disabled {
+ pointer-events: none;
+ filter: none;
+ opacity: 0.5;
+}
+
+.form-check-input[disabled] ~ .form-check-label, .form-check-input:disabled ~ .form-check-label {
+ opacity: 0.5;
+}
+
+.form-switch {
+ padding-left: 2.5em;
+}
+
+.form-switch .form-check-input {
+ width: 2em;
+ margin-left: -2.5em;
+ background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%280, 0, 0, 0.25%29'/%3e%3c/svg%3e");
+ background-position: left center;
+ border-radius: 2em;
+ transition: background-position 0.15s ease-in-out;
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .form-switch .form-check-input {
+ transition: none;
+ }
+}
+
+.form-switch .form-check-input:focus {
+ background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%2386b7fe'/%3e%3c/svg%3e");
+}
+
+.form-switch .form-check-input:checked {
+ background-position: right center;
+ background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e");
+}
+
+.form-check-inline {
+ display: inline-block;
+ margin-right: 1rem;
+}
+
+.btn-check {
+ position: absolute;
+ clip: rect(0, 0, 0, 0);
+ pointer-events: none;
+}
+
+.btn-check[disabled] + .btn, .btn-check:disabled + .btn {
+ pointer-events: none;
+ filter: none;
+ opacity: 0.65;
+}
+
+.form-range {
+ width: 100%;
+ height: 1.5rem;
+ padding: 0;
+ background-color: transparent;
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ appearance: none;
+}
+
+.form-range:focus {
+ outline: 0;
+}
+
+.form-range:focus::-webkit-slider-thumb {
+ box-shadow: 0 0 0 1px #fff, 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
+}
+
+.form-range:focus::-moz-range-thumb {
+ box-shadow: 0 0 0 1px #fff, 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
+}
+
+.form-range::-moz-focus-outer {
+ border: 0;
+}
+
+.form-range::-webkit-slider-thumb {
+ width: 1rem;
+ height: 1rem;
+ margin-top: -0.25rem;
+ background-color: #0d6efd;
+ border: 0;
+ border-radius: 1rem;
+ -webkit-transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
+ transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
+ -webkit-appearance: none;
+ appearance: none;
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .form-range::-webkit-slider-thumb {
+ -webkit-transition: none;
+ transition: none;
+ }
+}
+
+.form-range::-webkit-slider-thumb:active {
+ background-color: #b6d4fe;
+}
+
+.form-range::-webkit-slider-runnable-track {
+ width: 100%;
+ height: 0.5rem;
+ color: transparent;
+ cursor: pointer;
+ background-color: #dee2e6;
+ border-color: transparent;
+ border-radius: 1rem;
+}
+
+.form-range::-moz-range-thumb {
+ width: 1rem;
+ height: 1rem;
+ background-color: #0d6efd;
+ border: 0;
+ border-radius: 1rem;
+ -moz-transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
+ transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
+ -moz-appearance: none;
+ appearance: none;
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .form-range::-moz-range-thumb {
+ -moz-transition: none;
+ transition: none;
+ }
+}
+
+.form-range::-moz-range-thumb:active {
+ background-color: #b6d4fe;
+}
+
+.form-range::-moz-range-track {
+ width: 100%;
+ height: 0.5rem;
+ color: transparent;
+ cursor: pointer;
+ background-color: #dee2e6;
+ border-color: transparent;
+ border-radius: 1rem;
+}
+
+.form-range:disabled {
+ pointer-events: none;
+}
+
+.form-range:disabled::-webkit-slider-thumb {
+ background-color: #adb5bd;
+}
+
+.form-range:disabled::-moz-range-thumb {
+ background-color: #adb5bd;
+}
+
+.form-floating {
+ position: relative;
+}
+
+.form-floating > .form-control,
+.form-floating > .form-select {
+ height: calc(3.5rem + 2px);
+ line-height: 1.25;
+}
+
+.form-floating > label {
+ position: absolute;
+ top: 0;
+ left: 0;
+ height: 100%;
+ padding: 1rem 0.75rem;
+ pointer-events: none;
+ border: 1px solid transparent;
+ transform-origin: 0 0;
+ transition: opacity 0.1s ease-in-out, transform 0.1s ease-in-out;
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .form-floating > label {
+ transition: none;
+ }
+}
+
+.form-floating > .form-control {
+ padding: 1rem 0.75rem;
+}
+
+.form-floating > .form-control::-moz-placeholder {
+ color: transparent;
+}
+
+.form-floating > .form-control::placeholder {
+ color: transparent;
+}
+
+.form-floating > .form-control:not(:-moz-placeholder-shown) {
+ padding-top: 1.625rem;
+ padding-bottom: 0.625rem;
+}
+
+.form-floating > .form-control:focus, .form-floating > .form-control:not(:placeholder-shown) {
+ padding-top: 1.625rem;
+ padding-bottom: 0.625rem;
+}
+
+.form-floating > .form-control:-webkit-autofill {
+ padding-top: 1.625rem;
+ padding-bottom: 0.625rem;
+}
+
+.form-floating > .form-select {
+ padding-top: 1.625rem;
+ padding-bottom: 0.625rem;
+}
+
+.form-floating > .form-control:not(:-moz-placeholder-shown) ~ label {
+ opacity: 0.65;
+ transform: scale(0.85) translateY(-0.5rem) translateX(0.15rem);
+}
+
+.form-floating > .form-control:focus ~ label,
+.form-floating > .form-control:not(:placeholder-shown) ~ label,
+.form-floating > .form-select ~ label {
+ opacity: 0.65;
+ transform: scale(0.85) translateY(-0.5rem) translateX(0.15rem);
+}
+
+.form-floating > .form-control:-webkit-autofill ~ label {
+ opacity: 0.65;
+ transform: scale(0.85) translateY(-0.5rem) translateX(0.15rem);
+}
+
+.input-group {
+ position: relative;
+ display: flex;
+ flex-wrap: wrap;
+ align-items: stretch;
+ width: 100%;
+}
+
+.input-group > .form-control,
+.input-group > .form-select {
+ position: relative;
+ flex: 1 1 auto;
+ width: 1%;
+ min-width: 0;
+}
+
+.input-group > .form-control:focus,
+.input-group > .form-select:focus {
+ z-index: 3;
+}
+
+.input-group .btn {
+ position: relative;
+ z-index: 2;
+}
+
+.input-group .btn:focus {
+ z-index: 3;
+}
+
+.input-group-text {
+ display: flex;
+ align-items: center;
+ padding: 0.375rem 0.75rem;
+ font-size: 1rem;
+ font-weight: 400;
+ line-height: 1.5;
+ color: #212529;
+ text-align: center;
+ white-space: nowrap;
+ background-color: #e9ecef;
+ border: 1px solid #ced4da;
+ border-radius: 0.25rem;
+}
+
+.input-group-lg > .form-control,
+.input-group-lg > .form-select,
+.input-group-lg > .input-group-text,
+.input-group-lg > .btn {
+ padding: 0.5rem 1rem;
+ font-size: 1.25rem;
+ border-radius: 0.3rem;
+}
+
+.input-group-sm > .form-control,
+.input-group-sm > .form-select,
+.input-group-sm > .input-group-text,
+.input-group-sm > .btn {
+ padding: 0.25rem 0.5rem;
+ font-size: 0.875rem;
+ border-radius: 0.2rem;
+}
+
+.input-group-lg > .form-select,
+.input-group-sm > .form-select {
+ padding-right: 3rem;
+}
+
+.input-group:not(.has-validation) > :not(:last-child):not(.dropdown-toggle):not(.dropdown-menu),
+.input-group:not(.has-validation) > .dropdown-toggle:nth-last-child(n+3) {
+ border-top-right-radius: 0;
+ border-bottom-right-radius: 0;
+}
+
+.input-group.has-validation > :nth-last-child(n+3):not(.dropdown-toggle):not(.dropdown-menu),
+.input-group.has-validation > .dropdown-toggle:nth-last-child(n+4) {
+ border-top-right-radius: 0;
+ border-bottom-right-radius: 0;
+}
+
+.input-group > :not(:first-child):not(.dropdown-menu):not(.valid-tooltip):not(.valid-feedback):not(.invalid-tooltip):not(.invalid-feedback) {
+ margin-left: -1px;
+ border-top-left-radius: 0;
+ border-bottom-left-radius: 0;
+}
+
+.valid-feedback {
+ display: none;
+ width: 100%;
+ margin-top: 0.25rem;
+ font-size: 0.875em;
+ color: #198754;
+}
+
+.valid-tooltip {
+ position: absolute;
+ top: 100%;
+ z-index: 5;
+ display: none;
+ max-width: 100%;
+ padding: 0.25rem 0.5rem;
+ margin-top: 0.1rem;
+ font-size: 0.875rem;
+ color: #fff;
+ background-color: rgba(25, 135, 84, 0.9);
+ border-radius: 0.25rem;
+}
+
+.was-validated :valid ~ .valid-feedback,
+.was-validated :valid ~ .valid-tooltip,
+.is-valid ~ .valid-feedback,
+.is-valid ~ .valid-tooltip {
+ display: block;
+}
+
+.was-validated .form-control:valid, .form-control.is-valid {
+ border-color: #198754;
+ padding-right: calc(1.5em + 0.75rem);
+ background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");
+ background-repeat: no-repeat;
+ background-position: right calc(0.375em + 0.1875rem) center;
+ background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);
+}
+
+.was-validated .form-control:valid:focus, .form-control.is-valid:focus {
+ border-color: #198754;
+ box-shadow: 0 0 0 0.25rem rgba(25, 135, 84, 0.25);
+}
+
+.was-validated textarea.form-control:valid, textarea.form-control.is-valid {
+ padding-right: calc(1.5em + 0.75rem);
+ background-position: top calc(0.375em + 0.1875rem) right calc(0.375em + 0.1875rem);
+}
+
+.was-validated .form-select:valid, .form-select.is-valid {
+ border-color: #198754;
+}
+
+.was-validated .form-select:valid:not([multiple]):not([size]), .was-validated .form-select:valid:not([multiple])[size="1"], .form-select.is-valid:not([multiple]):not([size]), .form-select.is-valid:not([multiple])[size="1"] {
+ padding-right: 4.125rem;
+ background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e"), url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");
+ background-position: right 0.75rem center, center right 2.25rem;
+ background-size: 16px 12px, calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);
+}
+
+.was-validated .form-select:valid:focus, .form-select.is-valid:focus {
+ border-color: #198754;
+ box-shadow: 0 0 0 0.25rem rgba(25, 135, 84, 0.25);
+}
+
+.was-validated .form-check-input:valid, .form-check-input.is-valid {
+ border-color: #198754;
+}
+
+.was-validated .form-check-input:valid:checked, .form-check-input.is-valid:checked {
+ background-color: #198754;
+}
+
+.was-validated .form-check-input:valid:focus, .form-check-input.is-valid:focus {
+ box-shadow: 0 0 0 0.25rem rgba(25, 135, 84, 0.25);
+}
+
+.was-validated .form-check-input:valid ~ .form-check-label, .form-check-input.is-valid ~ .form-check-label {
+ color: #198754;
+}
+
+.form-check-inline .form-check-input ~ .valid-feedback {
+ margin-left: 0.5em;
+}
+
+.was-validated .input-group .form-control:valid, .input-group .form-control.is-valid,
+.was-validated .input-group .form-select:valid,
+.input-group .form-select.is-valid {
+ z-index: 1;
+}
+
+.was-validated .input-group .form-control:valid:focus, .input-group .form-control.is-valid:focus,
+.was-validated .input-group .form-select:valid:focus,
+.input-group .form-select.is-valid:focus {
+ z-index: 3;
+}
+
+.invalid-feedback {
+ display: none;
+ width: 100%;
+ margin-top: 0.25rem;
+ font-size: 0.875em;
+ color: #dc3545;
+}
+
+.invalid-tooltip {
+ position: absolute;
+ top: 100%;
+ z-index: 5;
+ display: none;
+ max-width: 100%;
+ padding: 0.25rem 0.5rem;
+ margin-top: 0.1rem;
+ font-size: 0.875rem;
+ color: #fff;
+ background-color: rgba(220, 53, 69, 0.9);
+ border-radius: 0.25rem;
+}
+
+.was-validated :invalid ~ .invalid-feedback,
+.was-validated :invalid ~ .invalid-tooltip,
+.is-invalid ~ .invalid-feedback,
+.is-invalid ~ .invalid-tooltip {
+ display: block;
+}
+
+.was-validated .form-control:invalid, .form-control.is-invalid {
+ border-color: #dc3545;
+ padding-right: calc(1.5em + 0.75rem);
+ background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");
+ background-repeat: no-repeat;
+ background-position: right calc(0.375em + 0.1875rem) center;
+ background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);
+}
+
+.was-validated .form-control:invalid:focus, .form-control.is-invalid:focus {
+ border-color: #dc3545;
+ box-shadow: 0 0 0 0.25rem rgba(220, 53, 69, 0.25);
+}
+
+.was-validated textarea.form-control:invalid, textarea.form-control.is-invalid {
+ padding-right: calc(1.5em + 0.75rem);
+ background-position: top calc(0.375em + 0.1875rem) right calc(0.375em + 0.1875rem);
+}
+
+.was-validated .form-select:invalid, .form-select.is-invalid {
+ border-color: #dc3545;
+}
+
+.was-validated .form-select:invalid:not([multiple]):not([size]), .was-validated .form-select:invalid:not([multiple])[size="1"], .form-select.is-invalid:not([multiple]):not([size]), .form-select.is-invalid:not([multiple])[size="1"] {
+ padding-right: 4.125rem;
+ background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e"), url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");
+ background-position: right 0.75rem center, center right 2.25rem;
+ background-size: 16px 12px, calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);
+}
+
+.was-validated .form-select:invalid:focus, .form-select.is-invalid:focus {
+ border-color: #dc3545;
+ box-shadow: 0 0 0 0.25rem rgba(220, 53, 69, 0.25);
+}
+
+.was-validated .form-check-input:invalid, .form-check-input.is-invalid {
+ border-color: #dc3545;
+}
+
+.was-validated .form-check-input:invalid:checked, .form-check-input.is-invalid:checked {
+ background-color: #dc3545;
+}
+
+.was-validated .form-check-input:invalid:focus, .form-check-input.is-invalid:focus {
+ box-shadow: 0 0 0 0.25rem rgba(220, 53, 69, 0.25);
+}
+
+.was-validated .form-check-input:invalid ~ .form-check-label, .form-check-input.is-invalid ~ .form-check-label {
+ color: #dc3545;
+}
+
+.form-check-inline .form-check-input ~ .invalid-feedback {
+ margin-left: 0.5em;
+}
+
+.was-validated .input-group .form-control:invalid, .input-group .form-control.is-invalid,
+.was-validated .input-group .form-select:invalid,
+.input-group .form-select.is-invalid {
+ z-index: 2;
+}
+
+.was-validated .input-group .form-control:invalid:focus, .input-group .form-control.is-invalid:focus,
+.was-validated .input-group .form-select:invalid:focus,
+.input-group .form-select.is-invalid:focus {
+ z-index: 3;
+}
+
+.btn {
+ display: inline-block;
+ font-weight: 400;
+ line-height: 1.5;
+ color: #212529;
+ text-align: center;
+ text-decoration: none;
+ vertical-align: middle;
+ cursor: pointer;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ user-select: none;
+ background-color: transparent;
+ border: 1px solid transparent;
+ padding: 0.375rem 0.75rem;
+ font-size: 1rem;
+ border-radius: 0.25rem;
+ transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .btn {
+ transition: none;
+ }
+}
+
+.btn:hover {
+ color: #212529;
+}
+
+.btn-check:focus + .btn, .btn:focus {
+ outline: 0;
+ box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
+}
+
+.btn:disabled, .btn.disabled, fieldset:disabled .btn {
+ pointer-events: none;
+ opacity: 0.65;
+}
+
+.btn-primary {
+ color: #fff;
+ background-color: #0d6efd;
+ border-color: #0d6efd;
+}
+
+.btn-primary:hover {
+ color: #fff;
+ background-color: #0b5ed7;
+ border-color: #0a58ca;
+}
+
+.btn-check:focus + .btn-primary, .btn-primary:focus {
+ color: #fff;
+ background-color: #0b5ed7;
+ border-color: #0a58ca;
+ box-shadow: 0 0 0 0.25rem rgba(49, 132, 253, 0.5);
+}
+
+.btn-check:checked + .btn-primary, .btn-check:active + .btn-primary, .btn-primary:active, .btn-primary.active, .show > .btn-primary.dropdown-toggle {
+ color: #fff;
+ background-color: #0a58ca;
+ border-color: #0a53be;
+}
+
+.btn-check:checked + .btn-primary:focus, .btn-check:active + .btn-primary:focus, .btn-primary:active:focus, .btn-primary.active:focus, .show > .btn-primary.dropdown-toggle:focus {
+ box-shadow: 0 0 0 0.25rem rgba(49, 132, 253, 0.5);
+}
+
+.btn-primary:disabled, .btn-primary.disabled {
+ color: #fff;
+ background-color: #0d6efd;
+ border-color: #0d6efd;
+}
+
+.btn-secondary {
+ color: #fff;
+ background-color: #6c757d;
+ border-color: #6c757d;
+}
+
+.btn-secondary:hover {
+ color: #fff;
+ background-color: #5c636a;
+ border-color: #565e64;
+}
+
+.btn-check:focus + .btn-secondary, .btn-secondary:focus {
+ color: #fff;
+ background-color: #5c636a;
+ border-color: #565e64;
+ box-shadow: 0 0 0 0.25rem rgba(130, 138, 145, 0.5);
+}
+
+.btn-check:checked + .btn-secondary, .btn-check:active + .btn-secondary, .btn-secondary:active, .btn-secondary.active, .show > .btn-secondary.dropdown-toggle {
+ color: #fff;
+ background-color: #565e64;
+ border-color: #51585e;
+}
+
+.btn-check:checked + .btn-secondary:focus, .btn-check:active + .btn-secondary:focus, .btn-secondary:active:focus, .btn-secondary.active:focus, .show > .btn-secondary.dropdown-toggle:focus {
+ box-shadow: 0 0 0 0.25rem rgba(130, 138, 145, 0.5);
+}
+
+.btn-secondary:disabled, .btn-secondary.disabled {
+ color: #fff;
+ background-color: #6c757d;
+ border-color: #6c757d;
+}
+
+.btn-success {
+ color: #fff;
+ background-color: #198754;
+ border-color: #198754;
+}
+
+.btn-success:hover {
+ color: #fff;
+ background-color: #157347;
+ border-color: #146c43;
+}
+
+.btn-check:focus + .btn-success, .btn-success:focus {
+ color: #fff;
+ background-color: #157347;
+ border-color: #146c43;
+ box-shadow: 0 0 0 0.25rem rgba(60, 153, 110, 0.5);
+}
+
+.btn-check:checked + .btn-success, .btn-check:active + .btn-success, .btn-success:active, .btn-success.active, .show > .btn-success.dropdown-toggle {
+ color: #fff;
+ background-color: #146c43;
+ border-color: #13653f;
+}
+
+.btn-check:checked + .btn-success:focus, .btn-check:active + .btn-success:focus, .btn-success:active:focus, .btn-success.active:focus, .show > .btn-success.dropdown-toggle:focus {
+ box-shadow: 0 0 0 0.25rem rgba(60, 153, 110, 0.5);
+}
+
+.btn-success:disabled, .btn-success.disabled {
+ color: #fff;
+ background-color: #198754;
+ border-color: #198754;
+}
+
+.btn-info {
+ color: #000;
+ background-color: #0dcaf0;
+ border-color: #0dcaf0;
+}
+
+.btn-info:hover {
+ color: #000;
+ background-color: #31d2f2;
+ border-color: #25cff2;
+}
+
+.btn-check:focus + .btn-info, .btn-info:focus {
+ color: #000;
+ background-color: #31d2f2;
+ border-color: #25cff2;
+ box-shadow: 0 0 0 0.25rem rgba(11, 172, 204, 0.5);
+}
+
+.btn-check:checked + .btn-info, .btn-check:active + .btn-info, .btn-info:active, .btn-info.active, .show > .btn-info.dropdown-toggle {
+ color: #000;
+ background-color: #3dd5f3;
+ border-color: #25cff2;
+}
+
+.btn-check:checked + .btn-info:focus, .btn-check:active + .btn-info:focus, .btn-info:active:focus, .btn-info.active:focus, .show > .btn-info.dropdown-toggle:focus {
+ box-shadow: 0 0 0 0.25rem rgba(11, 172, 204, 0.5);
+}
+
+.btn-info:disabled, .btn-info.disabled {
+ color: #000;
+ background-color: #0dcaf0;
+ border-color: #0dcaf0;
+}
+
+.btn-warning {
+ color: #000;
+ background-color: #ffc107;
+ border-color: #ffc107;
+}
+
+.btn-warning:hover {
+ color: #000;
+ background-color: #ffca2c;
+ border-color: #ffc720;
+}
+
+.btn-check:focus + .btn-warning, .btn-warning:focus {
+ color: #000;
+ background-color: #ffca2c;
+ border-color: #ffc720;
+ box-shadow: 0 0 0 0.25rem rgba(217, 164, 6, 0.5);
+}
+
+.btn-check:checked + .btn-warning, .btn-check:active + .btn-warning, .btn-warning:active, .btn-warning.active, .show > .btn-warning.dropdown-toggle {
+ color: #000;
+ background-color: #ffcd39;
+ border-color: #ffc720;
+}
+
+.btn-check:checked + .btn-warning:focus, .btn-check:active + .btn-warning:focus, .btn-warning:active:focus, .btn-warning.active:focus, .show > .btn-warning.dropdown-toggle:focus {
+ box-shadow: 0 0 0 0.25rem rgba(217, 164, 6, 0.5);
+}
+
+.btn-warning:disabled, .btn-warning.disabled {
+ color: #000;
+ background-color: #ffc107;
+ border-color: #ffc107;
+}
+
+.btn-danger {
+ color: #fff;
+ background-color: #dc3545;
+ border-color: #dc3545;
+}
+
+.btn-danger:hover {
+ color: #fff;
+ background-color: #bb2d3b;
+ border-color: #b02a37;
+}
+
+.btn-check:focus + .btn-danger, .btn-danger:focus {
+ color: #fff;
+ background-color: #bb2d3b;
+ border-color: #b02a37;
+ box-shadow: 0 0 0 0.25rem rgba(225, 83, 97, 0.5);
+}
+
+.btn-check:checked + .btn-danger, .btn-check:active + .btn-danger, .btn-danger:active, .btn-danger.active, .show > .btn-danger.dropdown-toggle {
+ color: #fff;
+ background-color: #b02a37;
+ border-color: #a52834;
+}
+
+.btn-check:checked + .btn-danger:focus, .btn-check:active + .btn-danger:focus, .btn-danger:active:focus, .btn-danger.active:focus, .show > .btn-danger.dropdown-toggle:focus {
+ box-shadow: 0 0 0 0.25rem rgba(225, 83, 97, 0.5);
+}
+
+.btn-danger:disabled, .btn-danger.disabled {
+ color: #fff;
+ background-color: #dc3545;
+ border-color: #dc3545;
+}
+
+.btn-light {
+ color: #000;
+ background-color: #f8f9fa;
+ border-color: #f8f9fa;
+}
+
+.btn-light:hover {
+ color: #000;
+ background-color: #f9fafb;
+ border-color: #f9fafb;
+}
+
+.btn-check:focus + .btn-light, .btn-light:focus {
+ color: #000;
+ background-color: #f9fafb;
+ border-color: #f9fafb;
+ box-shadow: 0 0 0 0.25rem rgba(211, 212, 213, 0.5);
+}
+
+.btn-check:checked + .btn-light, .btn-check:active + .btn-light, .btn-light:active, .btn-light.active, .show > .btn-light.dropdown-toggle {
+ color: #000;
+ background-color: #f9fafb;
+ border-color: #f9fafb;
+}
+
+.btn-check:checked + .btn-light:focus, .btn-check:active + .btn-light:focus, .btn-light:active:focus, .btn-light.active:focus, .show > .btn-light.dropdown-toggle:focus {
+ box-shadow: 0 0 0 0.25rem rgba(211, 212, 213, 0.5);
+}
+
+.btn-light:disabled, .btn-light.disabled {
+ color: #000;
+ background-color: #f8f9fa;
+ border-color: #f8f9fa;
+}
+
+.btn-dark {
+ color: #fff;
+ background-color: #212529;
+ border-color: #212529;
+}
+
+.btn-dark:hover {
+ color: #fff;
+ background-color: #1c1f23;
+ border-color: #1a1e21;
+}
+
+.btn-check:focus + .btn-dark, .btn-dark:focus {
+ color: #fff;
+ background-color: #1c1f23;
+ border-color: #1a1e21;
+ box-shadow: 0 0 0 0.25rem rgba(66, 70, 73, 0.5);
+}
+
+.btn-check:checked + .btn-dark, .btn-check:active + .btn-dark, .btn-dark:active, .btn-dark.active, .show > .btn-dark.dropdown-toggle {
+ color: #fff;
+ background-color: #1a1e21;
+ border-color: #191c1f;
+}
+
+.btn-check:checked + .btn-dark:focus, .btn-check:active + .btn-dark:focus, .btn-dark:active:focus, .btn-dark.active:focus, .show > .btn-dark.dropdown-toggle:focus {
+ box-shadow: 0 0 0 0.25rem rgba(66, 70, 73, 0.5);
+}
+
+.btn-dark:disabled, .btn-dark.disabled {
+ color: #fff;
+ background-color: #212529;
+ border-color: #212529;
+}
+
+.btn-outline-primary {
+ color: #0d6efd;
+ border-color: #0d6efd;
+}
+
+.btn-outline-primary:hover {
+ color: #fff;
+ background-color: #0d6efd;
+ border-color: #0d6efd;
+}
+
+.btn-check:focus + .btn-outline-primary, .btn-outline-primary:focus {
+ box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.5);
+}
+
+.btn-check:checked + .btn-outline-primary, .btn-check:active + .btn-outline-primary, .btn-outline-primary:active, .btn-outline-primary.active, .btn-outline-primary.dropdown-toggle.show {
+ color: #fff;
+ background-color: #0d6efd;
+ border-color: #0d6efd;
+}
+
+.btn-check:checked + .btn-outline-primary:focus, .btn-check:active + .btn-outline-primary:focus, .btn-outline-primary:active:focus, .btn-outline-primary.active:focus, .btn-outline-primary.dropdown-toggle.show:focus {
+ box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.5);
+}
+
+.btn-outline-primary:disabled, .btn-outline-primary.disabled {
+ color: #0d6efd;
+ background-color: transparent;
+}
+
+.btn-outline-secondary {
+ color: #6c757d;
+ border-color: #6c757d;
+}
+
+.btn-outline-secondary:hover {
+ color: #fff;
+ background-color: #6c757d;
+ border-color: #6c757d;
+}
+
+.btn-check:focus + .btn-outline-secondary, .btn-outline-secondary:focus {
+ box-shadow: 0 0 0 0.25rem rgba(108, 117, 125, 0.5);
+}
+
+.btn-check:checked + .btn-outline-secondary, .btn-check:active + .btn-outline-secondary, .btn-outline-secondary:active, .btn-outline-secondary.active, .btn-outline-secondary.dropdown-toggle.show {
+ color: #fff;
+ background-color: #6c757d;
+ border-color: #6c757d;
+}
+
+.btn-check:checked + .btn-outline-secondary:focus, .btn-check:active + .btn-outline-secondary:focus, .btn-outline-secondary:active:focus, .btn-outline-secondary.active:focus, .btn-outline-secondary.dropdown-toggle.show:focus {
+ box-shadow: 0 0 0 0.25rem rgba(108, 117, 125, 0.5);
+}
+
+.btn-outline-secondary:disabled, .btn-outline-secondary.disabled {
+ color: #6c757d;
+ background-color: transparent;
+}
+
+.btn-outline-success {
+ color: #198754;
+ border-color: #198754;
+}
+
+.btn-outline-success:hover {
+ color: #fff;
+ background-color: #198754;
+ border-color: #198754;
+}
+
+.btn-check:focus + .btn-outline-success, .btn-outline-success:focus {
+ box-shadow: 0 0 0 0.25rem rgba(25, 135, 84, 0.5);
+}
+
+.btn-check:checked + .btn-outline-success, .btn-check:active + .btn-outline-success, .btn-outline-success:active, .btn-outline-success.active, .btn-outline-success.dropdown-toggle.show {
+ color: #fff;
+ background-color: #198754;
+ border-color: #198754;
+}
+
+.btn-check:checked + .btn-outline-success:focus, .btn-check:active + .btn-outline-success:focus, .btn-outline-success:active:focus, .btn-outline-success.active:focus, .btn-outline-success.dropdown-toggle.show:focus {
+ box-shadow: 0 0 0 0.25rem rgba(25, 135, 84, 0.5);
+}
+
+.btn-outline-success:disabled, .btn-outline-success.disabled {
+ color: #198754;
+ background-color: transparent;
+}
+
+.btn-outline-info {
+ color: #0dcaf0;
+ border-color: #0dcaf0;
+}
+
+.btn-outline-info:hover {
+ color: #000;
+ background-color: #0dcaf0;
+ border-color: #0dcaf0;
+}
+
+.btn-check:focus + .btn-outline-info, .btn-outline-info:focus {
+ box-shadow: 0 0 0 0.25rem rgba(13, 202, 240, 0.5);
+}
+
+.btn-check:checked + .btn-outline-info, .btn-check:active + .btn-outline-info, .btn-outline-info:active, .btn-outline-info.active, .btn-outline-info.dropdown-toggle.show {
+ color: #000;
+ background-color: #0dcaf0;
+ border-color: #0dcaf0;
+}
+
+.btn-check:checked + .btn-outline-info:focus, .btn-check:active + .btn-outline-info:focus, .btn-outline-info:active:focus, .btn-outline-info.active:focus, .btn-outline-info.dropdown-toggle.show:focus {
+ box-shadow: 0 0 0 0.25rem rgba(13, 202, 240, 0.5);
+}
+
+.btn-outline-info:disabled, .btn-outline-info.disabled {
+ color: #0dcaf0;
+ background-color: transparent;
+}
+
+.btn-outline-warning {
+ color: #ffc107;
+ border-color: #ffc107;
+}
+
+.btn-outline-warning:hover {
+ color: #000;
+ background-color: #ffc107;
+ border-color: #ffc107;
+}
+
+.btn-check:focus + .btn-outline-warning, .btn-outline-warning:focus {
+ box-shadow: 0 0 0 0.25rem rgba(255, 193, 7, 0.5);
+}
+
+.btn-check:checked + .btn-outline-warning, .btn-check:active + .btn-outline-warning, .btn-outline-warning:active, .btn-outline-warning.active, .btn-outline-warning.dropdown-toggle.show {
+ color: #000;
+ background-color: #ffc107;
+ border-color: #ffc107;
+}
+
+.btn-check:checked + .btn-outline-warning:focus, .btn-check:active + .btn-outline-warning:focus, .btn-outline-warning:active:focus, .btn-outline-warning.active:focus, .btn-outline-warning.dropdown-toggle.show:focus {
+ box-shadow: 0 0 0 0.25rem rgba(255, 193, 7, 0.5);
+}
+
+.btn-outline-warning:disabled, .btn-outline-warning.disabled {
+ color: #ffc107;
+ background-color: transparent;
+}
+
+.btn-outline-danger {
+ color: #dc3545;
+ border-color: #dc3545;
+}
+
+.btn-outline-danger:hover {
+ color: #fff;
+ background-color: #dc3545;
+ border-color: #dc3545;
+}
+
+.btn-check:focus + .btn-outline-danger, .btn-outline-danger:focus {
+ box-shadow: 0 0 0 0.25rem rgba(220, 53, 69, 0.5);
+}
+
+.btn-check:checked + .btn-outline-danger, .btn-check:active + .btn-outline-danger, .btn-outline-danger:active, .btn-outline-danger.active, .btn-outline-danger.dropdown-toggle.show {
+ color: #fff;
+ background-color: #dc3545;
+ border-color: #dc3545;
+}
+
+.btn-check:checked + .btn-outline-danger:focus, .btn-check:active + .btn-outline-danger:focus, .btn-outline-danger:active:focus, .btn-outline-danger.active:focus, .btn-outline-danger.dropdown-toggle.show:focus {
+ box-shadow: 0 0 0 0.25rem rgba(220, 53, 69, 0.5);
+}
+
+.btn-outline-danger:disabled, .btn-outline-danger.disabled {
+ color: #dc3545;
+ background-color: transparent;
+}
+
+.btn-outline-light {
+ color: #f8f9fa;
+ border-color: #f8f9fa;
+}
+
+.btn-outline-light:hover {
+ color: #000;
+ background-color: #f8f9fa;
+ border-color: #f8f9fa;
+}
+
+.btn-check:focus + .btn-outline-light, .btn-outline-light:focus {
+ box-shadow: 0 0 0 0.25rem rgba(248, 249, 250, 0.5);
+}
+
+.btn-check:checked + .btn-outline-light, .btn-check:active + .btn-outline-light, .btn-outline-light:active, .btn-outline-light.active, .btn-outline-light.dropdown-toggle.show {
+ color: #000;
+ background-color: #f8f9fa;
+ border-color: #f8f9fa;
+}
+
+.btn-check:checked + .btn-outline-light:focus, .btn-check:active + .btn-outline-light:focus, .btn-outline-light:active:focus, .btn-outline-light.active:focus, .btn-outline-light.dropdown-toggle.show:focus {
+ box-shadow: 0 0 0 0.25rem rgba(248, 249, 250, 0.5);
+}
+
+.btn-outline-light:disabled, .btn-outline-light.disabled {
+ color: #f8f9fa;
+ background-color: transparent;
+}
+
+.btn-outline-dark {
+ color: #212529;
+ border-color: #212529;
+}
+
+.btn-outline-dark:hover {
+ color: #fff;
+ background-color: #212529;
+ border-color: #212529;
+}
+
+.btn-check:focus + .btn-outline-dark, .btn-outline-dark:focus {
+ box-shadow: 0 0 0 0.25rem rgba(33, 37, 41, 0.5);
+}
+
+.btn-check:checked + .btn-outline-dark, .btn-check:active + .btn-outline-dark, .btn-outline-dark:active, .btn-outline-dark.active, .btn-outline-dark.dropdown-toggle.show {
+ color: #fff;
+ background-color: #212529;
+ border-color: #212529;
+}
+
+.btn-check:checked + .btn-outline-dark:focus, .btn-check:active + .btn-outline-dark:focus, .btn-outline-dark:active:focus, .btn-outline-dark.active:focus, .btn-outline-dark.dropdown-toggle.show:focus {
+ box-shadow: 0 0 0 0.25rem rgba(33, 37, 41, 0.5);
+}
+
+.btn-outline-dark:disabled, .btn-outline-dark.disabled {
+ color: #212529;
+ background-color: transparent;
+}
+
+.btn-link {
+ font-weight: 400;
+ color: #0d6efd;
+ text-decoration: underline;
+}
+
+.btn-link:hover {
+ color: #0a58ca;
+}
+
+.btn-link:disabled, .btn-link.disabled {
+ color: #6c757d;
+}
+
+.btn-lg, .btn-group-lg > .btn {
+ padding: 0.5rem 1rem;
+ font-size: 1.25rem;
+ border-radius: 0.3rem;
+}
+
+.btn-sm, .btn-group-sm > .btn {
+ padding: 0.25rem 0.5rem;
+ font-size: 0.875rem;
+ border-radius: 0.2rem;
+}
+
+.fade {
+ transition: opacity 0.15s linear;
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .fade {
+ transition: none;
+ }
+}
+
+.fade:not(.show) {
+ opacity: 0;
+}
+
+.collapse:not(.show) {
+ display: none;
+}
+
+.collapsing {
+ height: 0;
+ overflow: hidden;
+ transition: height 0.35s ease;
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .collapsing {
+ transition: none;
+ }
+}
+
+.collapsing.collapse-horizontal {
+ width: 0;
+ height: auto;
+ transition: width 0.35s ease;
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .collapsing.collapse-horizontal {
+ transition: none;
+ }
+}
+
+.dropup,
+.dropend,
+.dropdown,
+.dropstart {
+ position: relative;
+}
+
+.dropdown-toggle {
+ white-space: nowrap;
+}
+
+.dropdown-toggle::after {
+ display: inline-block;
+ margin-left: 0.255em;
+ vertical-align: 0.255em;
+ content: "";
+ border-top: 0.3em solid;
+ border-right: 0.3em solid transparent;
+ border-bottom: 0;
+ border-left: 0.3em solid transparent;
+}
+
+.dropdown-toggle:empty::after {
+ margin-left: 0;
+}
+
+.dropdown-menu {
+ position: absolute;
+ z-index: 1000;
+ display: none;
+ min-width: 10rem;
+ padding: 0.5rem 0;
+ margin: 0;
+ font-size: 1rem;
+ color: #212529;
+ text-align: left;
+ list-style: none;
+ background-color: #fff;
+ background-clip: padding-box;
+ border: 1px solid rgba(0, 0, 0, 0.15);
+ border-radius: 0.25rem;
+}
+
+.dropdown-menu[data-bs-popper] {
+ top: 100%;
+ left: 0;
+ margin-top: 0.125rem;
+}
+
+.dropdown-menu-start {
+ --bs-position: start;
+}
+
+.dropdown-menu-start[data-bs-popper] {
+ right: auto;
+ left: 0;
+}
+
+.dropdown-menu-end {
+ --bs-position: end;
+}
+
+.dropdown-menu-end[data-bs-popper] {
+ right: 0;
+ left: auto;
+}
+
+@media (min-width: 576px) {
+ .dropdown-menu-sm-start {
+ --bs-position: start;
+ }
+
+ .dropdown-menu-sm-start[data-bs-popper] {
+ right: auto;
+ left: 0;
+ }
+
+ .dropdown-menu-sm-end {
+ --bs-position: end;
+ }
+
+ .dropdown-menu-sm-end[data-bs-popper] {
+ right: 0;
+ left: auto;
+ }
+}
+
+@media (min-width: 768px) {
+ .dropdown-menu-md-start {
+ --bs-position: start;
+ }
+
+ .dropdown-menu-md-start[data-bs-popper] {
+ right: auto;
+ left: 0;
+ }
+
+ .dropdown-menu-md-end {
+ --bs-position: end;
+ }
+
+ .dropdown-menu-md-end[data-bs-popper] {
+ right: 0;
+ left: auto;
+ }
+}
+
+@media (min-width: 992px) {
+ .dropdown-menu-lg-start {
+ --bs-position: start;
+ }
+
+ .dropdown-menu-lg-start[data-bs-popper] {
+ right: auto;
+ left: 0;
+ }
+
+ .dropdown-menu-lg-end {
+ --bs-position: end;
+ }
+
+ .dropdown-menu-lg-end[data-bs-popper] {
+ right: 0;
+ left: auto;
+ }
+}
+
+@media (min-width: 1200px) {
+ .dropdown-menu-xl-start {
+ --bs-position: start;
+ }
+
+ .dropdown-menu-xl-start[data-bs-popper] {
+ right: auto;
+ left: 0;
+ }
+
+ .dropdown-menu-xl-end {
+ --bs-position: end;
+ }
+
+ .dropdown-menu-xl-end[data-bs-popper] {
+ right: 0;
+ left: auto;
+ }
+}
+
+@media (min-width: 1400px) {
+ .dropdown-menu-xxl-start {
+ --bs-position: start;
+ }
+
+ .dropdown-menu-xxl-start[data-bs-popper] {
+ right: auto;
+ left: 0;
+ }
+
+ .dropdown-menu-xxl-end {
+ --bs-position: end;
+ }
+
+ .dropdown-menu-xxl-end[data-bs-popper] {
+ right: 0;
+ left: auto;
+ }
+}
+
+.dropup .dropdown-menu[data-bs-popper] {
+ top: auto;
+ bottom: 100%;
+ margin-top: 0;
+ margin-bottom: 0.125rem;
+}
+
+.dropup .dropdown-toggle::after {
+ display: inline-block;
+ margin-left: 0.255em;
+ vertical-align: 0.255em;
+ content: "";
+ border-top: 0;
+ border-right: 0.3em solid transparent;
+ border-bottom: 0.3em solid;
+ border-left: 0.3em solid transparent;
+}
+
+.dropup .dropdown-toggle:empty::after {
+ margin-left: 0;
+}
+
+.dropend .dropdown-menu[data-bs-popper] {
+ top: 0;
+ right: auto;
+ left: 100%;
+ margin-top: 0;
+ margin-left: 0.125rem;
+}
+
+.dropend .dropdown-toggle::after {
+ display: inline-block;
+ margin-left: 0.255em;
+ vertical-align: 0.255em;
+ content: "";
+ border-top: 0.3em solid transparent;
+ border-right: 0;
+ border-bottom: 0.3em solid transparent;
+ border-left: 0.3em solid;
+}
+
+.dropend .dropdown-toggle:empty::after {
+ margin-left: 0;
+}
+
+.dropend .dropdown-toggle::after {
+ vertical-align: 0;
+}
+
+.dropstart .dropdown-menu[data-bs-popper] {
+ top: 0;
+ right: 100%;
+ left: auto;
+ margin-top: 0;
+ margin-right: 0.125rem;
+}
+
+.dropstart .dropdown-toggle::after {
+ display: inline-block;
+ margin-left: 0.255em;
+ vertical-align: 0.255em;
+ content: "";
+}
+
+.dropstart .dropdown-toggle::after {
+ display: none;
+}
+
+.dropstart .dropdown-toggle::before {
+ display: inline-block;
+ margin-right: 0.255em;
+ vertical-align: 0.255em;
+ content: "";
+ border-top: 0.3em solid transparent;
+ border-right: 0.3em solid;
+ border-bottom: 0.3em solid transparent;
+}
+
+.dropstart .dropdown-toggle:empty::after {
+ margin-left: 0;
+}
+
+.dropstart .dropdown-toggle::before {
+ vertical-align: 0;
+}
+
+.dropdown-divider {
+ height: 0;
+ margin: 0.5rem 0;
+ overflow: hidden;
+ border-top: 1px solid rgba(0, 0, 0, 0.15);
+}
+
+.dropdown-item {
+ display: block;
+ width: 100%;
+ padding: 0.25rem 1rem;
+ clear: both;
+ font-weight: 400;
+ color: #212529;
+ text-align: inherit;
+ text-decoration: none;
+ white-space: nowrap;
+ background-color: transparent;
+ border: 0;
+}
+
+.dropdown-item:hover, .dropdown-item:focus {
+ color: #1e2125;
+ background-color: #e9ecef;
+}
+
+.dropdown-item.active, .dropdown-item:active {
+ color: #fff;
+ text-decoration: none;
+ background-color: #0d6efd;
+}
+
+.dropdown-item.disabled, .dropdown-item:disabled {
+ color: #adb5bd;
+ pointer-events: none;
+ background-color: transparent;
+}
+
+.dropdown-menu.show {
+ display: block;
+}
+
+.dropdown-header {
+ display: block;
+ padding: 0.5rem 1rem;
+ margin-bottom: 0;
+ font-size: 0.875rem;
+ color: #6c757d;
+ white-space: nowrap;
+}
+
+.dropdown-item-text {
+ display: block;
+ padding: 0.25rem 1rem;
+ color: #212529;
+}
+
+.dropdown-menu-dark {
+ color: #dee2e6;
+ background-color: #343a40;
+ border-color: rgba(0, 0, 0, 0.15);
+}
+
+.dropdown-menu-dark .dropdown-item {
+ color: #dee2e6;
+}
+
+.dropdown-menu-dark .dropdown-item:hover, .dropdown-menu-dark .dropdown-item:focus {
+ color: #fff;
+ background-color: rgba(255, 255, 255, 0.15);
+}
+
+.dropdown-menu-dark .dropdown-item.active, .dropdown-menu-dark .dropdown-item:active {
+ color: #fff;
+ background-color: #0d6efd;
+}
+
+.dropdown-menu-dark .dropdown-item.disabled, .dropdown-menu-dark .dropdown-item:disabled {
+ color: #adb5bd;
+}
+
+.dropdown-menu-dark .dropdown-divider {
+ border-color: rgba(0, 0, 0, 0.15);
+}
+
+.dropdown-menu-dark .dropdown-item-text {
+ color: #dee2e6;
+}
+
+.dropdown-menu-dark .dropdown-header {
+ color: #adb5bd;
+}
+
+.btn-group,
+.btn-group-vertical {
+ position: relative;
+ display: inline-flex;
+ vertical-align: middle;
+}
+
+.btn-group > .btn,
+.btn-group-vertical > .btn {
+ position: relative;
+ flex: 1 1 auto;
+}
+
+.btn-group > .btn-check:checked + .btn,
+.btn-group > .btn-check:focus + .btn,
+.btn-group > .btn:hover,
+.btn-group > .btn:focus,
+.btn-group > .btn:active,
+.btn-group > .btn.active,
+.btn-group-vertical > .btn-check:checked + .btn,
+.btn-group-vertical > .btn-check:focus + .btn,
+.btn-group-vertical > .btn:hover,
+.btn-group-vertical > .btn:focus,
+.btn-group-vertical > .btn:active,
+.btn-group-vertical > .btn.active {
+ z-index: 1;
+}
+
+.btn-toolbar {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: flex-start;
+}
+
+.btn-toolbar .input-group {
+ width: auto;
+}
+
+.btn-group > .btn:not(:first-child),
+.btn-group > .btn-group:not(:first-child) {
+ margin-left: -1px;
+}
+
+.btn-group > .btn:not(:last-child):not(.dropdown-toggle),
+.btn-group > .btn-group:not(:last-child) > .btn {
+ border-top-right-radius: 0;
+ border-bottom-right-radius: 0;
+}
+
+.btn-group > .btn:nth-child(n+3),
+.btn-group > :not(.btn-check) + .btn,
+.btn-group > .btn-group:not(:first-child) > .btn {
+ border-top-left-radius: 0;
+ border-bottom-left-radius: 0;
+}
+
+.dropdown-toggle-split {
+ padding-right: 0.5625rem;
+ padding-left: 0.5625rem;
+}
+
+.dropdown-toggle-split::after, .dropup .dropdown-toggle-split::after, .dropend .dropdown-toggle-split::after {
+ margin-left: 0;
+}
+
+.dropstart .dropdown-toggle-split::before {
+ margin-right: 0;
+}
+
+.btn-sm + .dropdown-toggle-split, .btn-group-sm > .btn + .dropdown-toggle-split {
+ padding-right: 0.375rem;
+ padding-left: 0.375rem;
+}
+
+.btn-lg + .dropdown-toggle-split, .btn-group-lg > .btn + .dropdown-toggle-split {
+ padding-right: 0.75rem;
+ padding-left: 0.75rem;
+}
+
+.btn-group-vertical {
+ flex-direction: column;
+ align-items: flex-start;
+ justify-content: center;
+}
+
+.btn-group-vertical > .btn,
+.btn-group-vertical > .btn-group {
+ width: 100%;
+}
+
+.btn-group-vertical > .btn:not(:first-child),
+.btn-group-vertical > .btn-group:not(:first-child) {
+ margin-top: -1px;
+}
+
+.btn-group-vertical > .btn:not(:last-child):not(.dropdown-toggle),
+.btn-group-vertical > .btn-group:not(:last-child) > .btn {
+ border-bottom-right-radius: 0;
+ border-bottom-left-radius: 0;
+}
+
+.btn-group-vertical > .btn ~ .btn,
+.btn-group-vertical > .btn-group:not(:first-child) > .btn {
+ border-top-left-radius: 0;
+ border-top-right-radius: 0;
+}
+
+.nav {
+ display: flex;
+ flex-wrap: wrap;
+ padding-left: 0;
+ margin-bottom: 0;
+ list-style: none;
+}
+
+.nav-link {
+ display: block;
+ padding: 0.5rem 1rem;
+ color: #0d6efd;
+ text-decoration: none;
+ transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out;
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .nav-link {
+ transition: none;
+ }
+}
+
+.nav-link:hover, .nav-link:focus {
+ color: #0a58ca;
+}
+
+.nav-link.disabled {
+ color: #6c757d;
+ pointer-events: none;
+ cursor: default;
+}
+
+.nav-tabs {
+ border-bottom: 1px solid #dee2e6;
+}
+
+.nav-tabs .nav-link {
+ margin-bottom: -1px;
+ background: none;
+ border: 1px solid transparent;
+ border-top-left-radius: 0.25rem;
+ border-top-right-radius: 0.25rem;
+}
+
+.nav-tabs .nav-link:hover, .nav-tabs .nav-link:focus {
+ border-color: #e9ecef #e9ecef #dee2e6;
+ isolation: isolate;
+}
+
+.nav-tabs .nav-link.disabled {
+ color: #6c757d;
+ background-color: transparent;
+ border-color: transparent;
+}
+
+.nav-tabs .nav-link.active,
+.nav-tabs .nav-item.show .nav-link {
+ color: #495057;
+ background-color: #fff;
+ border-color: #dee2e6 #dee2e6 #fff;
+}
+
+.nav-tabs .dropdown-menu {
+ margin-top: -1px;
+ border-top-left-radius: 0;
+ border-top-right-radius: 0;
+}
+
+.nav-pills .nav-link {
+ background: none;
+ border: 0;
+ border-radius: 0.25rem;
+}
+
+.nav-pills .nav-link.active,
+.nav-pills .show > .nav-link {
+ color: #fff;
+ background-color: #0d6efd;
+}
+
+.nav-fill > .nav-link,
+.nav-fill .nav-item {
+ flex: 1 1 auto;
+ text-align: center;
+}
+
+.nav-justified > .nav-link,
+.nav-justified .nav-item {
+ flex-basis: 0;
+ flex-grow: 1;
+ text-align: center;
+}
+
+.nav-fill .nav-item .nav-link,
+.nav-justified .nav-item .nav-link {
+ width: 100%;
+}
+
+.tab-content > .tab-pane {
+ display: none;
+}
+
+.tab-content > .active {
+ display: block;
+}
+
+.navbar {
+ position: relative;
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ justify-content: space-between;
+ padding-top: 0.5rem;
+ padding-bottom: 0.5rem;
+}
+
+.navbar > .container,
+.navbar > .container-fluid,
+.navbar > .container-sm,
+.navbar > .container-md,
+.navbar > .container-lg,
+.navbar > .container-xl,
+.navbar > .container-xxl {
+ display: flex;
+ flex-wrap: inherit;
+ align-items: center;
+ justify-content: space-between;
+}
+
+.navbar-brand {
+ padding-top: 0.3125rem;
+ padding-bottom: 0.3125rem;
+ margin-right: 1rem;
+ font-size: 1.25rem;
+ text-decoration: none;
+ white-space: nowrap;
+}
+
+.navbar-nav {
+ display: flex;
+ flex-direction: column;
+ padding-left: 0;
+ margin-bottom: 0;
+ list-style: none;
+}
+
+.navbar-nav .nav-link {
+ padding-right: 0;
+ padding-left: 0;
+}
+
+.navbar-nav .dropdown-menu {
+ position: static;
+}
+
+.navbar-text {
+ padding-top: 0.5rem;
+ padding-bottom: 0.5rem;
+}
+
+.navbar-collapse {
+ flex-basis: 100%;
+ flex-grow: 1;
+ align-items: center;
+}
+
+.navbar-toggler {
+ padding: 0.25rem 0.75rem;
+ font-size: 1.25rem;
+ line-height: 1;
+ background-color: transparent;
+ border: 1px solid transparent;
+ border-radius: 0.25rem;
+ transition: box-shadow 0.15s ease-in-out;
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .navbar-toggler {
+ transition: none;
+ }
+}
+
+.navbar-toggler:hover {
+ text-decoration: none;
+}
+
+.navbar-toggler:focus {
+ text-decoration: none;
+ outline: 0;
+ box-shadow: 0 0 0 0.25rem;
+}
+
+.navbar-toggler-icon {
+ display: inline-block;
+ width: 1.5em;
+ height: 1.5em;
+ vertical-align: middle;
+ background-repeat: no-repeat;
+ background-position: center;
+ background-size: 100%;
+}
+
+.navbar-nav-scroll {
+ max-height: var(--bs-scroll-height, 75vh);
+ overflow-y: auto;
+}
+
+@media (min-width: 576px) {
+ .navbar-expand-sm {
+ flex-wrap: nowrap;
+ justify-content: flex-start;
+ }
+
+ .navbar-expand-sm .navbar-nav {
+ flex-direction: row;
+ }
+
+ .navbar-expand-sm .navbar-nav .dropdown-menu {
+ position: absolute;
+ }
+
+ .navbar-expand-sm .navbar-nav .nav-link {
+ padding-right: 0.5rem;
+ padding-left: 0.5rem;
+ }
+
+ .navbar-expand-sm .navbar-nav-scroll {
+ overflow: visible;
+ }
+
+ .navbar-expand-sm .navbar-collapse {
+ display: flex !important;
+ flex-basis: auto;
+ }
+
+ .navbar-expand-sm .navbar-toggler {
+ display: none;
+ }
+
+ .navbar-expand-sm .offcanvas-header {
+ display: none;
+ }
+
+ .navbar-expand-sm .offcanvas {
+ position: inherit;
+ bottom: 0;
+ z-index: 1000;
+ flex-grow: 1;
+ visibility: visible !important;
+ background-color: transparent;
+ border-right: 0;
+ border-left: 0;
+ transition: none;
+ transform: none;
+ }
+
+ .navbar-expand-sm .offcanvas-top,
+ .navbar-expand-sm .offcanvas-bottom {
+ height: auto;
+ border-top: 0;
+ border-bottom: 0;
+ }
+
+ .navbar-expand-sm .offcanvas-body {
+ display: flex;
+ flex-grow: 0;
+ padding: 0;
+ overflow-y: visible;
+ }
+}
+
+@media (min-width: 768px) {
+ .navbar-expand-md {
+ flex-wrap: nowrap;
+ justify-content: flex-start;
+ }
+
+ .navbar-expand-md .navbar-nav {
+ flex-direction: row;
+ }
+
+ .navbar-expand-md .navbar-nav .dropdown-menu {
+ position: absolute;
+ }
+
+ .navbar-expand-md .navbar-nav .nav-link {
+ padding-right: 0.5rem;
+ padding-left: 0.5rem;
+ }
+
+ .navbar-expand-md .navbar-nav-scroll {
+ overflow: visible;
+ }
+
+ .navbar-expand-md .navbar-collapse {
+ display: flex !important;
+ flex-basis: auto;
+ }
+
+ .navbar-expand-md .navbar-toggler {
+ display: none;
+ }
+
+ .navbar-expand-md .offcanvas-header {
+ display: none;
+ }
+
+ .navbar-expand-md .offcanvas {
+ position: inherit;
+ bottom: 0;
+ z-index: 1000;
+ flex-grow: 1;
+ visibility: visible !important;
+ background-color: transparent;
+ border-right: 0;
+ border-left: 0;
+ transition: none;
+ transform: none;
+ }
+
+ .navbar-expand-md .offcanvas-top,
+ .navbar-expand-md .offcanvas-bottom {
+ height: auto;
+ border-top: 0;
+ border-bottom: 0;
+ }
+
+ .navbar-expand-md .offcanvas-body {
+ display: flex;
+ flex-grow: 0;
+ padding: 0;
+ overflow-y: visible;
+ }
+}
+
+@media (min-width: 992px) {
+ .navbar-expand-lg {
+ flex-wrap: nowrap;
+ justify-content: flex-start;
+ }
+
+ .navbar-expand-lg .navbar-nav {
+ flex-direction: row;
+ }
+
+ .navbar-expand-lg .navbar-nav .dropdown-menu {
+ position: absolute;
+ }
+
+ .navbar-expand-lg .navbar-nav .nav-link {
+ padding-right: 0.5rem;
+ padding-left: 0.5rem;
+ }
+
+ .navbar-expand-lg .navbar-nav-scroll {
+ overflow: visible;
+ }
+
+ .navbar-expand-lg .navbar-collapse {
+ display: flex !important;
+ flex-basis: auto;
+ }
+
+ .navbar-expand-lg .navbar-toggler {
+ display: none;
+ }
+
+ .navbar-expand-lg .offcanvas-header {
+ display: none;
+ }
+
+ .navbar-expand-lg .offcanvas {
+ position: inherit;
+ bottom: 0;
+ z-index: 1000;
+ flex-grow: 1;
+ visibility: visible !important;
+ background-color: transparent;
+ border-right: 0;
+ border-left: 0;
+ transition: none;
+ transform: none;
+ }
+
+ .navbar-expand-lg .offcanvas-top,
+ .navbar-expand-lg .offcanvas-bottom {
+ height: auto;
+ border-top: 0;
+ border-bottom: 0;
+ }
+
+ .navbar-expand-lg .offcanvas-body {
+ display: flex;
+ flex-grow: 0;
+ padding: 0;
+ overflow-y: visible;
+ }
+}
+
+@media (min-width: 1200px) {
+ .navbar-expand-xl {
+ flex-wrap: nowrap;
+ justify-content: flex-start;
+ }
+
+ .navbar-expand-xl .navbar-nav {
+ flex-direction: row;
+ }
+
+ .navbar-expand-xl .navbar-nav .dropdown-menu {
+ position: absolute;
+ }
+
+ .navbar-expand-xl .navbar-nav .nav-link {
+ padding-right: 0.5rem;
+ padding-left: 0.5rem;
+ }
+
+ .navbar-expand-xl .navbar-nav-scroll {
+ overflow: visible;
+ }
+
+ .navbar-expand-xl .navbar-collapse {
+ display: flex !important;
+ flex-basis: auto;
+ }
+
+ .navbar-expand-xl .navbar-toggler {
+ display: none;
+ }
+
+ .navbar-expand-xl .offcanvas-header {
+ display: none;
+ }
+
+ .navbar-expand-xl .offcanvas {
+ position: inherit;
+ bottom: 0;
+ z-index: 1000;
+ flex-grow: 1;
+ visibility: visible !important;
+ background-color: transparent;
+ border-right: 0;
+ border-left: 0;
+ transition: none;
+ transform: none;
+ }
+
+ .navbar-expand-xl .offcanvas-top,
+ .navbar-expand-xl .offcanvas-bottom {
+ height: auto;
+ border-top: 0;
+ border-bottom: 0;
+ }
+
+ .navbar-expand-xl .offcanvas-body {
+ display: flex;
+ flex-grow: 0;
+ padding: 0;
+ overflow-y: visible;
+ }
+}
+
+@media (min-width: 1400px) {
+ .navbar-expand-xxl {
+ flex-wrap: nowrap;
+ justify-content: flex-start;
+ }
+
+ .navbar-expand-xxl .navbar-nav {
+ flex-direction: row;
+ }
+
+ .navbar-expand-xxl .navbar-nav .dropdown-menu {
+ position: absolute;
+ }
+
+ .navbar-expand-xxl .navbar-nav .nav-link {
+ padding-right: 0.5rem;
+ padding-left: 0.5rem;
+ }
+
+ .navbar-expand-xxl .navbar-nav-scroll {
+ overflow: visible;
+ }
+
+ .navbar-expand-xxl .navbar-collapse {
+ display: flex !important;
+ flex-basis: auto;
+ }
+
+ .navbar-expand-xxl .navbar-toggler {
+ display: none;
+ }
+
+ .navbar-expand-xxl .offcanvas-header {
+ display: none;
+ }
+
+ .navbar-expand-xxl .offcanvas {
+ position: inherit;
+ bottom: 0;
+ z-index: 1000;
+ flex-grow: 1;
+ visibility: visible !important;
+ background-color: transparent;
+ border-right: 0;
+ border-left: 0;
+ transition: none;
+ transform: none;
+ }
+
+ .navbar-expand-xxl .offcanvas-top,
+ .navbar-expand-xxl .offcanvas-bottom {
+ height: auto;
+ border-top: 0;
+ border-bottom: 0;
+ }
+
+ .navbar-expand-xxl .offcanvas-body {
+ display: flex;
+ flex-grow: 0;
+ padding: 0;
+ overflow-y: visible;
+ }
+}
+
+.navbar-expand {
+ flex-wrap: nowrap;
+ justify-content: flex-start;
+}
+
+.navbar-expand .navbar-nav {
+ flex-direction: row;
+}
+
+.navbar-expand .navbar-nav .dropdown-menu {
+ position: absolute;
+}
+
+.navbar-expand .navbar-nav .nav-link {
+ padding-right: 0.5rem;
+ padding-left: 0.5rem;
+}
+
+.navbar-expand .navbar-nav-scroll {
+ overflow: visible;
+}
+
+.navbar-expand .navbar-collapse {
+ display: flex !important;
+ flex-basis: auto;
+}
+
+.navbar-expand .navbar-toggler {
+ display: none;
+}
+
+.navbar-expand .offcanvas-header {
+ display: none;
+}
+
+.navbar-expand .offcanvas {
+ position: inherit;
+ bottom: 0;
+ z-index: 1000;
+ flex-grow: 1;
+ visibility: visible !important;
+ background-color: transparent;
+ border-right: 0;
+ border-left: 0;
+ transition: none;
+ transform: none;
+}
+
+.navbar-expand .offcanvas-top,
+.navbar-expand .offcanvas-bottom {
+ height: auto;
+ border-top: 0;
+ border-bottom: 0;
+}
+
+.navbar-expand .offcanvas-body {
+ display: flex;
+ flex-grow: 0;
+ padding: 0;
+ overflow-y: visible;
+}
+
+.navbar-light .navbar-brand {
+ color: rgba(0, 0, 0, 0.9);
+}
+
+.navbar-light .navbar-brand:hover, .navbar-light .navbar-brand:focus {
+ color: rgba(0, 0, 0, 0.9);
+}
+
+.navbar-light .navbar-nav .nav-link {
+ color: rgba(0, 0, 0, 0.55);
+}
+
+.navbar-light .navbar-nav .nav-link:hover, .navbar-light .navbar-nav .nav-link:focus {
+ color: rgba(0, 0, 0, 0.7);
+}
+
+.navbar-light .navbar-nav .nav-link.disabled {
+ color: rgba(0, 0, 0, 0.3);
+}
+
+.navbar-light .navbar-nav .show > .nav-link,
+.navbar-light .navbar-nav .nav-link.active {
+ color: rgba(0, 0, 0, 0.9);
+}
+
+.navbar-light .navbar-toggler {
+ color: rgba(0, 0, 0, 0.55);
+ border-color: rgba(0, 0, 0, 0.1);
+}
+
+.navbar-light .navbar-toggler-icon {
+ background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%280, 0, 0, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e");
+}
+
+.navbar-light .navbar-text {
+ color: rgba(0, 0, 0, 0.55);
+}
+
+.navbar-light .navbar-text a,
+.navbar-light .navbar-text a:hover,
+.navbar-light .navbar-text a:focus {
+ color: rgba(0, 0, 0, 0.9);
+}
+
+.navbar-dark .navbar-brand {
+ color: #fff;
+}
+
+.navbar-dark .navbar-brand:hover, .navbar-dark .navbar-brand:focus {
+ color: #fff;
+}
+
+.navbar-dark .navbar-nav .nav-link {
+ color: rgba(255, 255, 255, 0.55);
+}
+
+.navbar-dark .navbar-nav .nav-link:hover, .navbar-dark .navbar-nav .nav-link:focus {
+ color: rgba(255, 255, 255, 0.75);
+}
+
+.navbar-dark .navbar-nav .nav-link.disabled {
+ color: rgba(255, 255, 255, 0.25);
+}
+
+.navbar-dark .navbar-nav .show > .nav-link,
+.navbar-dark .navbar-nav .nav-link.active {
+ color: #fff;
+}
+
+.navbar-dark .navbar-toggler {
+ color: rgba(255, 255, 255, 0.55);
+ border-color: rgba(255, 255, 255, 0.1);
+}
+
+.navbar-dark .navbar-toggler-icon {
+ background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e");
+}
+
+.navbar-dark .navbar-text {
+ color: rgba(255, 255, 255, 0.55);
+}
+
+.navbar-dark .navbar-text a,
+.navbar-dark .navbar-text a:hover,
+.navbar-dark .navbar-text a:focus {
+ color: #fff;
+}
+
+.card {
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ min-width: 0;
+ word-wrap: break-word;
+ background-color: #fff;
+ background-clip: border-box;
+ border: 1px solid rgba(0, 0, 0, 0.125);
+ border-radius: 0.25rem;
+}
+
+.card > hr {
+ margin-right: 0;
+ margin-left: 0;
+}
+
+.card > .list-group {
+ border-top: inherit;
+ border-bottom: inherit;
+}
+
+.card > .list-group:first-child {
+ border-top-width: 0;
+ border-top-left-radius: calc(0.25rem - 1px);
+ border-top-right-radius: calc(0.25rem - 1px);
+}
+
+.card > .list-group:last-child {
+ border-bottom-width: 0;
+ border-bottom-right-radius: calc(0.25rem - 1px);
+ border-bottom-left-radius: calc(0.25rem - 1px);
+}
+
+.card > .card-header + .list-group,
+.card > .list-group + .card-footer {
+ border-top: 0;
+}
+
+.card-body {
+ flex: 1 1 auto;
+ padding: 1rem 1rem;
+}
+
+.card-title {
+ margin-bottom: 0.5rem;
+}
+
+.card-subtitle {
+ margin-top: -0.25rem;
+ margin-bottom: 0;
+}
+
+.card-text:last-child {
+ margin-bottom: 0;
+}
+
+.card-link + .card-link {
+ margin-left: 1rem;
+}
+
+.card-header {
+ padding: 0.5rem 1rem;
+ margin-bottom: 0;
+ background-color: rgba(0, 0, 0, 0.03);
+ border-bottom: 1px solid rgba(0, 0, 0, 0.125);
+}
+
+.card-header:first-child {
+ border-radius: calc(0.25rem - 1px) calc(0.25rem - 1px) 0 0;
+}
+
+.card-footer {
+ padding: 0.5rem 1rem;
+ background-color: rgba(0, 0, 0, 0.03);
+ border-top: 1px solid rgba(0, 0, 0, 0.125);
+}
+
+.card-footer:last-child {
+ border-radius: 0 0 calc(0.25rem - 1px) calc(0.25rem - 1px);
+}
+
+.card-header-tabs {
+ margin-right: -0.5rem;
+ margin-bottom: -0.5rem;
+ margin-left: -0.5rem;
+ border-bottom: 0;
+}
+
+.card-header-pills {
+ margin-right: -0.5rem;
+ margin-left: -0.5rem;
+}
+
+.card-img-overlay {
+ position: absolute;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ padding: 1rem;
+ border-radius: calc(0.25rem - 1px);
+}
+
+.card-img,
+.card-img-top,
+.card-img-bottom {
+ width: 100%;
+}
+
+.card-img,
+.card-img-top {
+ border-top-left-radius: calc(0.25rem - 1px);
+ border-top-right-radius: calc(0.25rem - 1px);
+}
+
+.card-img,
+.card-img-bottom {
+ border-bottom-right-radius: calc(0.25rem - 1px);
+ border-bottom-left-radius: calc(0.25rem - 1px);
+}
+
+.card-group > .card {
+ margin-bottom: 0.75rem;
+}
+
+@media (min-width: 576px) {
+ .card-group {
+ display: flex;
+ flex-flow: row wrap;
+ }
+
+ .card-group > .card {
+ flex: 1 0 0%;
+ margin-bottom: 0;
+ }
+
+ .card-group > .card + .card {
+ margin-left: 0;
+ border-left: 0;
+ }
+
+ .card-group > .card:not(:last-child) {
+ border-top-right-radius: 0;
+ border-bottom-right-radius: 0;
+ }
+
+ .card-group > .card:not(:last-child) .card-img-top,
+ .card-group > .card:not(:last-child) .card-header {
+ border-top-right-radius: 0;
+ }
+
+ .card-group > .card:not(:last-child) .card-img-bottom,
+ .card-group > .card:not(:last-child) .card-footer {
+ border-bottom-right-radius: 0;
+ }
+
+ .card-group > .card:not(:first-child) {
+ border-top-left-radius: 0;
+ border-bottom-left-radius: 0;
+ }
+
+ .card-group > .card:not(:first-child) .card-img-top,
+ .card-group > .card:not(:first-child) .card-header {
+ border-top-left-radius: 0;
+ }
+
+ .card-group > .card:not(:first-child) .card-img-bottom,
+ .card-group > .card:not(:first-child) .card-footer {
+ border-bottom-left-radius: 0;
+ }
+}
+
+.accordion-button {
+ position: relative;
+ display: flex;
+ align-items: center;
+ width: 100%;
+ padding: 1rem 1.25rem;
+ font-size: 1rem;
+ color: #212529;
+ text-align: left;
+ background-color: #fff;
+ border: 0;
+ border-radius: 0;
+ overflow-anchor: none;
+ transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, border-radius 0.15s ease;
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .accordion-button {
+ transition: none;
+ }
+}
+
+.accordion-button:not(.collapsed) {
+ color: #0c63e4;
+ background-color: #e7f1ff;
+ box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.125);
+}
+
+.accordion-button:not(.collapsed)::after {
+ background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%230c63e4'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");
+ transform: rotate(-180deg);
+}
+
+.accordion-button::after {
+ flex-shrink: 0;
+ width: 1.25rem;
+ height: 1.25rem;
+ margin-left: auto;
+ content: "";
+ background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23212529'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");
+ background-repeat: no-repeat;
+ background-size: 1.25rem;
+ transition: transform 0.2s ease-in-out;
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .accordion-button::after {
+ transition: none;
+ }
+}
+
+.accordion-button:hover {
+ z-index: 2;
+}
+
+.accordion-button:focus {
+ z-index: 3;
+ border-color: #86b7fe;
+ outline: 0;
+ box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
+}
+
+.accordion-header {
+ margin-bottom: 0;
+}
+
+.accordion-item {
+ background-color: #fff;
+ border: 1px solid rgba(0, 0, 0, 0.125);
+}
+
+.accordion-item:first-of-type {
+ border-top-left-radius: 0.25rem;
+ border-top-right-radius: 0.25rem;
+}
+
+.accordion-item:first-of-type .accordion-button {
+ border-top-left-radius: calc(0.25rem - 1px);
+ border-top-right-radius: calc(0.25rem - 1px);
+}
+
+.accordion-item:not(:first-of-type) {
+ border-top: 0;
+}
+
+.accordion-item:last-of-type {
+ border-bottom-right-radius: 0.25rem;
+ border-bottom-left-radius: 0.25rem;
+}
+
+.accordion-item:last-of-type .accordion-button.collapsed {
+ border-bottom-right-radius: calc(0.25rem - 1px);
+ border-bottom-left-radius: calc(0.25rem - 1px);
+}
+
+.accordion-item:last-of-type .accordion-collapse {
+ border-bottom-right-radius: 0.25rem;
+ border-bottom-left-radius: 0.25rem;
+}
+
+.accordion-body {
+ padding: 1rem 1.25rem;
+}
+
+.accordion-flush .accordion-collapse {
+ border-width: 0;
+}
+
+.accordion-flush .accordion-item {
+ border-right: 0;
+ border-left: 0;
+ border-radius: 0;
+}
+
+.accordion-flush .accordion-item:first-child {
+ border-top: 0;
+}
+
+.accordion-flush .accordion-item:last-child {
+ border-bottom: 0;
+}
+
+.accordion-flush .accordion-item .accordion-button {
+ border-radius: 0;
+}
+
+.breadcrumb {
+ display: flex;
+ flex-wrap: wrap;
+ padding: 0 0;
+ margin-bottom: 1rem;
+ list-style: none;
+}
+
+.breadcrumb-item + .breadcrumb-item {
+ padding-left: 0.5rem;
+}
+
+.breadcrumb-item + .breadcrumb-item::before {
+ float: left;
+ padding-right: 0.5rem;
+ color: #6c757d;
+ content: var(--bs-breadcrumb-divider, "/") /* rtl: var(--bs-breadcrumb-divider, "/") */;
+}
+
+.breadcrumb-item.active {
+ color: #6c757d;
+}
+
+.pagination {
+ display: flex;
+ padding-left: 0;
+ list-style: none;
+}
+
+.page-link {
+ position: relative;
+ display: block;
+ color: #0d6efd;
+ text-decoration: none;
+ background-color: #fff;
+ border: 1px solid #dee2e6;
+ transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .page-link {
+ transition: none;
+ }
+}
+
+.page-link:hover {
+ z-index: 2;
+ color: #0a58ca;
+ background-color: #e9ecef;
+ border-color: #dee2e6;
+}
+
+.page-link:focus {
+ z-index: 3;
+ color: #0a58ca;
+ background-color: #e9ecef;
+ outline: 0;
+ box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
+}
+
+.page-item:not(:first-child) .page-link {
+ margin-left: -1px;
+}
+
+.page-item.active .page-link {
+ z-index: 3;
+ color: #fff;
+ background-color: #0d6efd;
+ border-color: #0d6efd;
+}
+
+.page-item.disabled .page-link {
+ color: #6c757d;
+ pointer-events: none;
+ background-color: #fff;
+ border-color: #dee2e6;
+}
+
+.page-link {
+ padding: 0.375rem 0.75rem;
+}
+
+.page-item:first-child .page-link {
+ border-top-left-radius: 0.25rem;
+ border-bottom-left-radius: 0.25rem;
+}
+
+.page-item:last-child .page-link {
+ border-top-right-radius: 0.25rem;
+ border-bottom-right-radius: 0.25rem;
+}
+
+.pagination-lg .page-link {
+ padding: 0.75rem 1.5rem;
+ font-size: 1.25rem;
+}
+
+.pagination-lg .page-item:first-child .page-link {
+ border-top-left-radius: 0.3rem;
+ border-bottom-left-radius: 0.3rem;
+}
+
+.pagination-lg .page-item:last-child .page-link {
+ border-top-right-radius: 0.3rem;
+ border-bottom-right-radius: 0.3rem;
+}
+
+.pagination-sm .page-link {
+ padding: 0.25rem 0.5rem;
+ font-size: 0.875rem;
+}
+
+.pagination-sm .page-item:first-child .page-link {
+ border-top-left-radius: 0.2rem;
+ border-bottom-left-radius: 0.2rem;
+}
+
+.pagination-sm .page-item:last-child .page-link {
+ border-top-right-radius: 0.2rem;
+ border-bottom-right-radius: 0.2rem;
+}
+
+.badge {
+ display: inline-block;
+ padding: 0.35em 0.65em;
+ font-size: 0.75em;
+ font-weight: 700;
+ line-height: 1;
+ color: #fff;
+ text-align: center;
+ white-space: nowrap;
+ vertical-align: baseline;
+ border-radius: 0.25rem;
+}
+
+.badge:empty {
+ display: none;
+}
+
+.btn .badge {
+ position: relative;
+ top: -1px;
+}
+
+.alert {
+ position: relative;
+ padding: 1rem 1rem;
+ margin-bottom: 1rem;
+ border: 1px solid transparent;
+ border-radius: 0.25rem;
+}
+
+.alert-heading {
+ color: inherit;
+}
+
+.alert-link {
+ font-weight: 700;
+}
+
+.alert-dismissible {
+ padding-right: 3rem;
+}
+
+.alert-dismissible .btn-close {
+ position: absolute;
+ top: 0;
+ right: 0;
+ z-index: 2;
+ padding: 1.25rem 1rem;
+}
+
+.alert-primary {
+ color: #084298;
+ background-color: #cfe2ff;
+ border-color: #b6d4fe;
+}
+
+.alert-primary .alert-link {
+ color: #06357a;
+}
+
+.alert-secondary {
+ color: #41464b;
+ background-color: #e2e3e5;
+ border-color: #d3d6d8;
+}
+
+.alert-secondary .alert-link {
+ color: #34383c;
+}
+
+.alert-success {
+ color: #0f5132;
+ background-color: #d1e7dd;
+ border-color: #badbcc;
+}
+
+.alert-success .alert-link {
+ color: #0c4128;
+}
+
+.alert-info {
+ color: #055160;
+ background-color: #cff4fc;
+ border-color: #b6effb;
+}
+
+.alert-info .alert-link {
+ color: #04414d;
+}
+
+.alert-warning {
+ color: #664d03;
+ background-color: #fff3cd;
+ border-color: #ffecb5;
+}
+
+.alert-warning .alert-link {
+ color: #523e02;
+}
+
+.alert-danger {
+ color: #842029;
+ background-color: #f8d7da;
+ border-color: #f5c2c7;
+}
+
+.alert-danger .alert-link {
+ color: #6a1a21;
+}
+
+.alert-light {
+ color: #636464;
+ background-color: #fefefe;
+ border-color: #fdfdfe;
+}
+
+.alert-light .alert-link {
+ color: #4f5050;
+}
+
+.alert-dark {
+ color: #141619;
+ background-color: #d3d3d4;
+ border-color: #bcbebf;
+}
+
+.alert-dark .alert-link {
+ color: #101214;
+}
+
+@-webkit-keyframes progress-bar-stripes {
+ 0% {
+ background-position-x: 1rem;
+ }
+}
+
+@keyframes progress-bar-stripes {
+ 0% {
+ background-position-x: 1rem;
+ }
+}
+
+.progress {
+ display: flex;
+ height: 1rem;
+ overflow: hidden;
+ font-size: 0.75rem;
+ background-color: #e9ecef;
+ border-radius: 0.25rem;
+}
+
+.progress-bar {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ overflow: hidden;
+ color: #fff;
+ text-align: center;
+ white-space: nowrap;
+ background-color: #0d6efd;
+ transition: width 0.6s ease;
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .progress-bar {
+ transition: none;
+ }
+}
+
+.progress-bar-striped {
+ background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
+ background-size: 1rem 1rem;
+}
+
+.progress-bar-animated {
+ -webkit-animation: 1s linear infinite progress-bar-stripes;
+ animation: 1s linear infinite progress-bar-stripes;
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .progress-bar-animated {
+ -webkit-animation: none;
+ animation: none;
+ }
+}
+
+.list-group {
+ display: flex;
+ flex-direction: column;
+ padding-left: 0;
+ margin-bottom: 0;
+ border-radius: 0.25rem;
+}
+
+.list-group-numbered {
+ list-style-type: none;
+ counter-reset: section;
+}
+
+.list-group-numbered > li::before {
+ content: counters(section, ".") ". ";
+ counter-increment: section;
+}
+
+.list-group-item-action {
+ width: 100%;
+ color: #495057;
+ text-align: inherit;
+}
+
+.list-group-item-action:hover, .list-group-item-action:focus {
+ z-index: 1;
+ color: #495057;
+ text-decoration: none;
+ background-color: #f8f9fa;
+}
+
+.list-group-item-action:active {
+ color: #212529;
+ background-color: #e9ecef;
+}
+
+.list-group-item {
+ position: relative;
+ display: block;
+ padding: 0.5rem 1rem;
+ color: #212529;
+ text-decoration: none;
+ background-color: #fff;
+ border: 1px solid rgba(0, 0, 0, 0.125);
+}
+
+.list-group-item:first-child {
+ border-top-left-radius: inherit;
+ border-top-right-radius: inherit;
+}
+
+.list-group-item:last-child {
+ border-bottom-right-radius: inherit;
+ border-bottom-left-radius: inherit;
+}
+
+.list-group-item.disabled, .list-group-item:disabled {
+ color: #6c757d;
+ pointer-events: none;
+ background-color: #fff;
+}
+
+.list-group-item.active {
+ z-index: 2;
+ color: #fff;
+ background-color: #0d6efd;
+ border-color: #0d6efd;
+}
+
+.list-group-item + .list-group-item {
+ border-top-width: 0;
+}
+
+.list-group-item + .list-group-item.active {
+ margin-top: -1px;
+ border-top-width: 1px;
+}
+
+.list-group-horizontal {
+ flex-direction: row;
+}
+
+.list-group-horizontal > .list-group-item:first-child {
+ border-bottom-left-radius: 0.25rem;
+ border-top-right-radius: 0;
+}
+
+.list-group-horizontal > .list-group-item:last-child {
+ border-top-right-radius: 0.25rem;
+ border-bottom-left-radius: 0;
+}
+
+.list-group-horizontal > .list-group-item.active {
+ margin-top: 0;
+}
+
+.list-group-horizontal > .list-group-item + .list-group-item {
+ border-top-width: 1px;
+ border-left-width: 0;
+}
+
+.list-group-horizontal > .list-group-item + .list-group-item.active {
+ margin-left: -1px;
+ border-left-width: 1px;
+}
+
+@media (min-width: 576px) {
+ .list-group-horizontal-sm {
+ flex-direction: row;
+ }
+
+ .list-group-horizontal-sm > .list-group-item:first-child {
+ border-bottom-left-radius: 0.25rem;
+ border-top-right-radius: 0;
+ }
+
+ .list-group-horizontal-sm > .list-group-item:last-child {
+ border-top-right-radius: 0.25rem;
+ border-bottom-left-radius: 0;
+ }
+
+ .list-group-horizontal-sm > .list-group-item.active {
+ margin-top: 0;
+ }
+
+ .list-group-horizontal-sm > .list-group-item + .list-group-item {
+ border-top-width: 1px;
+ border-left-width: 0;
+ }
+
+ .list-group-horizontal-sm > .list-group-item + .list-group-item.active {
+ margin-left: -1px;
+ border-left-width: 1px;
+ }
+}
+
+@media (min-width: 768px) {
+ .list-group-horizontal-md {
+ flex-direction: row;
+ }
+
+ .list-group-horizontal-md > .list-group-item:first-child {
+ border-bottom-left-radius: 0.25rem;
+ border-top-right-radius: 0;
+ }
+
+ .list-group-horizontal-md > .list-group-item:last-child {
+ border-top-right-radius: 0.25rem;
+ border-bottom-left-radius: 0;
+ }
+
+ .list-group-horizontal-md > .list-group-item.active {
+ margin-top: 0;
+ }
+
+ .list-group-horizontal-md > .list-group-item + .list-group-item {
+ border-top-width: 1px;
+ border-left-width: 0;
+ }
+
+ .list-group-horizontal-md > .list-group-item + .list-group-item.active {
+ margin-left: -1px;
+ border-left-width: 1px;
+ }
+}
+
+@media (min-width: 992px) {
+ .list-group-horizontal-lg {
+ flex-direction: row;
+ }
+
+ .list-group-horizontal-lg > .list-group-item:first-child {
+ border-bottom-left-radius: 0.25rem;
+ border-top-right-radius: 0;
+ }
+
+ .list-group-horizontal-lg > .list-group-item:last-child {
+ border-top-right-radius: 0.25rem;
+ border-bottom-left-radius: 0;
+ }
+
+ .list-group-horizontal-lg > .list-group-item.active {
+ margin-top: 0;
+ }
+
+ .list-group-horizontal-lg > .list-group-item + .list-group-item {
+ border-top-width: 1px;
+ border-left-width: 0;
+ }
+
+ .list-group-horizontal-lg > .list-group-item + .list-group-item.active {
+ margin-left: -1px;
+ border-left-width: 1px;
+ }
+}
+
+@media (min-width: 1200px) {
+ .list-group-horizontal-xl {
+ flex-direction: row;
+ }
+
+ .list-group-horizontal-xl > .list-group-item:first-child {
+ border-bottom-left-radius: 0.25rem;
+ border-top-right-radius: 0;
+ }
+
+ .list-group-horizontal-xl > .list-group-item:last-child {
+ border-top-right-radius: 0.25rem;
+ border-bottom-left-radius: 0;
+ }
+
+ .list-group-horizontal-xl > .list-group-item.active {
+ margin-top: 0;
+ }
+
+ .list-group-horizontal-xl > .list-group-item + .list-group-item {
+ border-top-width: 1px;
+ border-left-width: 0;
+ }
+
+ .list-group-horizontal-xl > .list-group-item + .list-group-item.active {
+ margin-left: -1px;
+ border-left-width: 1px;
+ }
+}
+
+@media (min-width: 1400px) {
+ .list-group-horizontal-xxl {
+ flex-direction: row;
+ }
+
+ .list-group-horizontal-xxl > .list-group-item:first-child {
+ border-bottom-left-radius: 0.25rem;
+ border-top-right-radius: 0;
+ }
+
+ .list-group-horizontal-xxl > .list-group-item:last-child {
+ border-top-right-radius: 0.25rem;
+ border-bottom-left-radius: 0;
+ }
+
+ .list-group-horizontal-xxl > .list-group-item.active {
+ margin-top: 0;
+ }
+
+ .list-group-horizontal-xxl > .list-group-item + .list-group-item {
+ border-top-width: 1px;
+ border-left-width: 0;
+ }
+
+ .list-group-horizontal-xxl > .list-group-item + .list-group-item.active {
+ margin-left: -1px;
+ border-left-width: 1px;
+ }
+}
+
+.list-group-flush {
+ border-radius: 0;
+}
+
+.list-group-flush > .list-group-item {
+ border-width: 0 0 1px;
+}
+
+.list-group-flush > .list-group-item:last-child {
+ border-bottom-width: 0;
+}
+
+.list-group-item-primary {
+ color: #084298;
+ background-color: #cfe2ff;
+}
+
+.list-group-item-primary.list-group-item-action:hover, .list-group-item-primary.list-group-item-action:focus {
+ color: #084298;
+ background-color: #bacbe6;
+}
+
+.list-group-item-primary.list-group-item-action.active {
+ color: #fff;
+ background-color: #084298;
+ border-color: #084298;
+}
+
+.list-group-item-secondary {
+ color: #41464b;
+ background-color: #e2e3e5;
+}
+
+.list-group-item-secondary.list-group-item-action:hover, .list-group-item-secondary.list-group-item-action:focus {
+ color: #41464b;
+ background-color: #cbccce;
+}
+
+.list-group-item-secondary.list-group-item-action.active {
+ color: #fff;
+ background-color: #41464b;
+ border-color: #41464b;
+}
+
+.list-group-item-success {
+ color: #0f5132;
+ background-color: #d1e7dd;
+}
+
+.list-group-item-success.list-group-item-action:hover, .list-group-item-success.list-group-item-action:focus {
+ color: #0f5132;
+ background-color: #bcd0c7;
+}
+
+.list-group-item-success.list-group-item-action.active {
+ color: #fff;
+ background-color: #0f5132;
+ border-color: #0f5132;
+}
+
+.list-group-item-info {
+ color: #055160;
+ background-color: #cff4fc;
+}
+
+.list-group-item-info.list-group-item-action:hover, .list-group-item-info.list-group-item-action:focus {
+ color: #055160;
+ background-color: #badce3;
+}
+
+.list-group-item-info.list-group-item-action.active {
+ color: #fff;
+ background-color: #055160;
+ border-color: #055160;
+}
+
+.list-group-item-warning {
+ color: #664d03;
+ background-color: #fff3cd;
+}
+
+.list-group-item-warning.list-group-item-action:hover, .list-group-item-warning.list-group-item-action:focus {
+ color: #664d03;
+ background-color: #e6dbb9;
+}
+
+.list-group-item-warning.list-group-item-action.active {
+ color: #fff;
+ background-color: #664d03;
+ border-color: #664d03;
+}
+
+.list-group-item-danger {
+ color: #842029;
+ background-color: #f8d7da;
+}
+
+.list-group-item-danger.list-group-item-action:hover, .list-group-item-danger.list-group-item-action:focus {
+ color: #842029;
+ background-color: #dfc2c4;
+}
+
+.list-group-item-danger.list-group-item-action.active {
+ color: #fff;
+ background-color: #842029;
+ border-color: #842029;
+}
+
+.list-group-item-light {
+ color: #636464;
+ background-color: #fefefe;
+}
+
+.list-group-item-light.list-group-item-action:hover, .list-group-item-light.list-group-item-action:focus {
+ color: #636464;
+ background-color: #e5e5e5;
+}
+
+.list-group-item-light.list-group-item-action.active {
+ color: #fff;
+ background-color: #636464;
+ border-color: #636464;
+}
+
+.list-group-item-dark {
+ color: #141619;
+ background-color: #d3d3d4;
+}
+
+.list-group-item-dark.list-group-item-action:hover, .list-group-item-dark.list-group-item-action:focus {
+ color: #141619;
+ background-color: #bebebf;
+}
+
+.list-group-item-dark.list-group-item-action.active {
+ color: #fff;
+ background-color: #141619;
+ border-color: #141619;
+}
+
+.btn-close {
+ box-sizing: content-box;
+ width: 1em;
+ height: 1em;
+ padding: 0.25em 0.25em;
+ color: #000;
+ background: transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") center/1em auto no-repeat;
+ border: 0;
+ border-radius: 0.25rem;
+ opacity: 0.5;
+}
+
+.btn-close:hover {
+ color: #000;
+ text-decoration: none;
+ opacity: 0.75;
+}
+
+.btn-close:focus {
+ outline: 0;
+ box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
+ opacity: 1;
+}
+
+.btn-close:disabled, .btn-close.disabled {
+ pointer-events: none;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ user-select: none;
+ opacity: 0.25;
+}
+
+.btn-close-white {
+ filter: invert(1) grayscale(100%) brightness(200%);
+}
+
+.toast {
+ width: 350px;
+ max-width: 100%;
+ font-size: 0.875rem;
+ pointer-events: auto;
+ background-color: rgba(255, 255, 255, 0.85);
+ background-clip: padding-box;
+ border: 1px solid rgba(0, 0, 0, 0.1);
+ box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
+ border-radius: 0.25rem;
+}
+
+.toast.showing {
+ opacity: 0;
+}
+
+.toast:not(.show) {
+ display: none;
+}
+
+.toast-container {
+ width: -webkit-max-content;
+ width: -moz-max-content;
+ width: max-content;
+ max-width: 100%;
+ pointer-events: none;
+}
+
+.toast-container > :not(:last-child) {
+ margin-bottom: 0.75rem;
+}
+
+.toast-header {
+ display: flex;
+ align-items: center;
+ padding: 0.5rem 0.75rem;
+ color: #6c757d;
+ background-color: rgba(255, 255, 255, 0.85);
+ background-clip: padding-box;
+ border-bottom: 1px solid rgba(0, 0, 0, 0.05);
+ border-top-left-radius: calc(0.25rem - 1px);
+ border-top-right-radius: calc(0.25rem - 1px);
+}
+
+.toast-header .btn-close {
+ margin-right: -0.375rem;
+ margin-left: 0.75rem;
+}
+
+.toast-body {
+ padding: 0.75rem;
+ word-wrap: break-word;
+}
+
+.modal {
+ position: fixed;
+ top: 0;
+ left: 0;
+ z-index: 1055;
+ display: none;
+ width: 100%;
+ height: 100%;
+ overflow-x: hidden;
+ overflow-y: auto;
+ outline: 0;
+}
+
+.modal-dialog {
+ position: relative;
+ width: auto;
+ margin: 0.5rem;
+ pointer-events: none;
+}
+
+.modal.fade .modal-dialog {
+ transition: transform 0.3s ease-out;
+ transform: translate(0, -50px);
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .modal.fade .modal-dialog {
+ transition: none;
+ }
+}
+
+.modal.show .modal-dialog {
+ transform: none;
+}
+
+.modal.modal-static .modal-dialog {
+ transform: scale(1.02);
+}
+
+.modal-dialog-scrollable {
+ height: calc(100% - 1rem);
+}
+
+.modal-dialog-scrollable .modal-content {
+ max-height: 100%;
+ overflow: hidden;
+}
+
+.modal-dialog-scrollable .modal-body {
+ overflow-y: auto;
+}
+
+.modal-dialog-centered {
+ display: flex;
+ align-items: center;
+ min-height: calc(100% - 1rem);
+}
+
+.modal-content {
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+ pointer-events: auto;
+ background-color: #fff;
+ background-clip: padding-box;
+ border: 1px solid rgba(0, 0, 0, 0.2);
+ border-radius: 0.3rem;
+ outline: 0;
+}
+
+.modal-backdrop {
+ position: fixed;
+ top: 0;
+ left: 0;
+ z-index: 1050;
+ width: 100vw;
+ height: 100vh;
+ background-color: #000;
+}
+
+.modal-backdrop.fade {
+ opacity: 0;
+}
+
+.modal-backdrop.show {
+ opacity: 0.5;
+}
+
+.modal-header {
+ display: flex;
+ flex-shrink: 0;
+ align-items: center;
+ justify-content: space-between;
+ padding: 1rem 1rem;
+ border-bottom: 1px solid #dee2e6;
+ border-top-left-radius: calc(0.3rem - 1px);
+ border-top-right-radius: calc(0.3rem - 1px);
+}
+
+.modal-header .btn-close {
+ padding: 0.5rem 0.5rem;
+ margin: -0.5rem -0.5rem -0.5rem auto;
+}
+
+.modal-title {
+ margin-bottom: 0;
+ line-height: 1.5;
+}
+
+.modal-body {
+ position: relative;
+ flex: 1 1 auto;
+ padding: 1rem;
+}
+
+.modal-footer {
+ display: flex;
+ flex-wrap: wrap;
+ flex-shrink: 0;
+ align-items: center;
+ justify-content: flex-end;
+ padding: 0.75rem;
+ border-top: 1px solid #dee2e6;
+ border-bottom-right-radius: calc(0.3rem - 1px);
+ border-bottom-left-radius: calc(0.3rem - 1px);
+}
+
+.modal-footer > * {
+ margin: 0.25rem;
+}
+
+@media (min-width: 576px) {
+ .modal-dialog {
+ max-width: 500px;
+ margin: 1.75rem auto;
+ }
+
+ .modal-dialog-scrollable {
+ height: calc(100% - 3.5rem);
+ }
+
+ .modal-dialog-centered {
+ min-height: calc(100% - 3.5rem);
+ }
+
+ .modal-sm {
+ max-width: 300px;
+ }
+}
+
+@media (min-width: 992px) {
+ .modal-lg,
+ .modal-xl {
+ max-width: 800px;
+ }
+}
+
+@media (min-width: 1200px) {
+ .modal-xl {
+ max-width: 1140px;
+ }
+}
+
+.modal-fullscreen {
+ width: 100vw;
+ max-width: none;
+ height: 100%;
+ margin: 0;
+}
+
+.modal-fullscreen .modal-content {
+ height: 100%;
+ border: 0;
+ border-radius: 0;
+}
+
+.modal-fullscreen .modal-header {
+ border-radius: 0;
+}
+
+.modal-fullscreen .modal-body {
+ overflow-y: auto;
+}
+
+.modal-fullscreen .modal-footer {
+ border-radius: 0;
+}
+
+@media (max-width: 575.98px) {
+ .modal-fullscreen-sm-down {
+ width: 100vw;
+ max-width: none;
+ height: 100%;
+ margin: 0;
+ }
+
+ .modal-fullscreen-sm-down .modal-content {
+ height: 100%;
+ border: 0;
+ border-radius: 0;
+ }
+
+ .modal-fullscreen-sm-down .modal-header {
+ border-radius: 0;
+ }
+
+ .modal-fullscreen-sm-down .modal-body {
+ overflow-y: auto;
+ }
+
+ .modal-fullscreen-sm-down .modal-footer {
+ border-radius: 0;
+ }
+}
+
+@media (max-width: 767.98px) {
+ .modal-fullscreen-md-down {
+ width: 100vw;
+ max-width: none;
+ height: 100%;
+ margin: 0;
+ }
+
+ .modal-fullscreen-md-down .modal-content {
+ height: 100%;
+ border: 0;
+ border-radius: 0;
+ }
+
+ .modal-fullscreen-md-down .modal-header {
+ border-radius: 0;
+ }
+
+ .modal-fullscreen-md-down .modal-body {
+ overflow-y: auto;
+ }
+
+ .modal-fullscreen-md-down .modal-footer {
+ border-radius: 0;
+ }
+}
+
+@media (max-width: 991.98px) {
+ .modal-fullscreen-lg-down {
+ width: 100vw;
+ max-width: none;
+ height: 100%;
+ margin: 0;
+ }
+
+ .modal-fullscreen-lg-down .modal-content {
+ height: 100%;
+ border: 0;
+ border-radius: 0;
+ }
+
+ .modal-fullscreen-lg-down .modal-header {
+ border-radius: 0;
+ }
+
+ .modal-fullscreen-lg-down .modal-body {
+ overflow-y: auto;
+ }
+
+ .modal-fullscreen-lg-down .modal-footer {
+ border-radius: 0;
+ }
+}
+
+@media (max-width: 1199.98px) {
+ .modal-fullscreen-xl-down {
+ width: 100vw;
+ max-width: none;
+ height: 100%;
+ margin: 0;
+ }
+
+ .modal-fullscreen-xl-down .modal-content {
+ height: 100%;
+ border: 0;
+ border-radius: 0;
+ }
+
+ .modal-fullscreen-xl-down .modal-header {
+ border-radius: 0;
+ }
+
+ .modal-fullscreen-xl-down .modal-body {
+ overflow-y: auto;
+ }
+
+ .modal-fullscreen-xl-down .modal-footer {
+ border-radius: 0;
+ }
+}
+
+@media (max-width: 1399.98px) {
+ .modal-fullscreen-xxl-down {
+ width: 100vw;
+ max-width: none;
+ height: 100%;
+ margin: 0;
+ }
+
+ .modal-fullscreen-xxl-down .modal-content {
+ height: 100%;
+ border: 0;
+ border-radius: 0;
+ }
+
+ .modal-fullscreen-xxl-down .modal-header {
+ border-radius: 0;
+ }
+
+ .modal-fullscreen-xxl-down .modal-body {
+ overflow-y: auto;
+ }
+
+ .modal-fullscreen-xxl-down .modal-footer {
+ border-radius: 0;
+ }
+}
+
+.tooltip {
+ position: absolute;
+ z-index: 1080;
+ display: block;
+ margin: 0;
+ font-family: var(--bs-font-sans-serif);
+ font-style: normal;
+ font-weight: 400;
+ line-height: 1.5;
+ text-align: left;
+ text-align: start;
+ text-decoration: none;
+ text-shadow: none;
+ text-transform: none;
+ letter-spacing: normal;
+ word-break: normal;
+ word-spacing: normal;
+ white-space: normal;
+ line-break: auto;
+ font-size: 0.875rem;
+ word-wrap: break-word;
+ opacity: 0;
+}
+
+.tooltip.show {
+ opacity: 0.9;
+}
+
+.tooltip .tooltip-arrow {
+ position: absolute;
+ display: block;
+ width: 0.8rem;
+ height: 0.4rem;
+}
+
+.tooltip .tooltip-arrow::before {
+ position: absolute;
+ content: "";
+ border-color: transparent;
+ border-style: solid;
+}
+
+.bs-tooltip-top, .bs-tooltip-auto[data-popper-placement^=top] {
+ padding: 0.4rem 0;
+}
+
+.bs-tooltip-top .tooltip-arrow, .bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow {
+ bottom: 0;
+}
+
+.bs-tooltip-top .tooltip-arrow::before, .bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow::before {
+ top: -1px;
+ border-width: 0.4rem 0.4rem 0;
+ border-top-color: #000;
+}
+
+.bs-tooltip-end, .bs-tooltip-auto[data-popper-placement^=right] {
+ padding: 0 0.4rem;
+}
+
+.bs-tooltip-end .tooltip-arrow, .bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow {
+ left: 0;
+ width: 0.4rem;
+ height: 0.8rem;
+}
+
+.bs-tooltip-end .tooltip-arrow::before, .bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow::before {
+ right: -1px;
+ border-width: 0.4rem 0.4rem 0.4rem 0;
+ border-right-color: #000;
+}
+
+.bs-tooltip-bottom, .bs-tooltip-auto[data-popper-placement^=bottom] {
+ padding: 0.4rem 0;
+}
+
+.bs-tooltip-bottom .tooltip-arrow, .bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow {
+ top: 0;
+}
+
+.bs-tooltip-bottom .tooltip-arrow::before, .bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow::before {
+ bottom: -1px;
+ border-width: 0 0.4rem 0.4rem;
+ border-bottom-color: #000;
+}
+
+.bs-tooltip-start, .bs-tooltip-auto[data-popper-placement^=left] {
+ padding: 0 0.4rem;
+}
+
+.bs-tooltip-start .tooltip-arrow, .bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow {
+ right: 0;
+ width: 0.4rem;
+ height: 0.8rem;
+}
+
+.bs-tooltip-start .tooltip-arrow::before, .bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow::before {
+ left: -1px;
+ border-width: 0.4rem 0 0.4rem 0.4rem;
+ border-left-color: #000;
+}
+
+.tooltip-inner {
+ max-width: 200px;
+ padding: 0.25rem 0.5rem;
+ color: #fff;
+ text-align: center;
+ background-color: #000;
+ border-radius: 0.25rem;
+}
+
+.popover {
+ position: absolute;
+ top: 0;
+ left: 0 /* rtl:ignore */;
+ z-index: 1070;
+ display: block;
+ max-width: 276px;
+ font-family: var(--bs-font-sans-serif);
+ font-style: normal;
+ font-weight: 400;
+ line-height: 1.5;
+ text-align: left;
+ text-align: start;
+ text-decoration: none;
+ text-shadow: none;
+ text-transform: none;
+ letter-spacing: normal;
+ word-break: normal;
+ word-spacing: normal;
+ white-space: normal;
+ line-break: auto;
+ font-size: 0.875rem;
+ word-wrap: break-word;
+ background-color: #fff;
+ background-clip: padding-box;
+ border: 1px solid rgba(0, 0, 0, 0.2);
+ border-radius: 0.3rem;
+}
+
+.popover .popover-arrow {
+ position: absolute;
+ display: block;
+ width: 1rem;
+ height: 0.5rem;
+}
+
+.popover .popover-arrow::before, .popover .popover-arrow::after {
+ position: absolute;
+ display: block;
+ content: "";
+ border-color: transparent;
+ border-style: solid;
+}
+
+.bs-popover-top > .popover-arrow, .bs-popover-auto[data-popper-placement^=top] > .popover-arrow {
+ bottom: calc(-0.5rem - 1px);
+}
+
+.bs-popover-top > .popover-arrow::before, .bs-popover-auto[data-popper-placement^=top] > .popover-arrow::before {
+ bottom: 0;
+ border-width: 0.5rem 0.5rem 0;
+ border-top-color: rgba(0, 0, 0, 0.25);
+}
+
+.bs-popover-top > .popover-arrow::after, .bs-popover-auto[data-popper-placement^=top] > .popover-arrow::after {
+ bottom: 1px;
+ border-width: 0.5rem 0.5rem 0;
+ border-top-color: #fff;
+}
+
+.bs-popover-end > .popover-arrow, .bs-popover-auto[data-popper-placement^=right] > .popover-arrow {
+ left: calc(-0.5rem - 1px);
+ width: 0.5rem;
+ height: 1rem;
+}
+
+.bs-popover-end > .popover-arrow::before, .bs-popover-auto[data-popper-placement^=right] > .popover-arrow::before {
+ left: 0;
+ border-width: 0.5rem 0.5rem 0.5rem 0;
+ border-right-color: rgba(0, 0, 0, 0.25);
+}
+
+.bs-popover-end > .popover-arrow::after, .bs-popover-auto[data-popper-placement^=right] > .popover-arrow::after {
+ left: 1px;
+ border-width: 0.5rem 0.5rem 0.5rem 0;
+ border-right-color: #fff;
+}
+
+.bs-popover-bottom > .popover-arrow, .bs-popover-auto[data-popper-placement^=bottom] > .popover-arrow {
+ top: calc(-0.5rem - 1px);
+}
+
+.bs-popover-bottom > .popover-arrow::before, .bs-popover-auto[data-popper-placement^=bottom] > .popover-arrow::before {
+ top: 0;
+ border-width: 0 0.5rem 0.5rem 0.5rem;
+ border-bottom-color: rgba(0, 0, 0, 0.25);
+}
+
+.bs-popover-bottom > .popover-arrow::after, .bs-popover-auto[data-popper-placement^=bottom] > .popover-arrow::after {
+ top: 1px;
+ border-width: 0 0.5rem 0.5rem 0.5rem;
+ border-bottom-color: #fff;
+}
+
+.bs-popover-bottom .popover-header::before, .bs-popover-auto[data-popper-placement^=bottom] .popover-header::before {
+ position: absolute;
+ top: 0;
+ left: 50%;
+ display: block;
+ width: 1rem;
+ margin-left: -0.5rem;
+ content: "";
+ border-bottom: 1px solid #f0f0f0;
+}
+
+.bs-popover-start > .popover-arrow, .bs-popover-auto[data-popper-placement^=left] > .popover-arrow {
+ right: calc(-0.5rem - 1px);
+ width: 0.5rem;
+ height: 1rem;
+}
+
+.bs-popover-start > .popover-arrow::before, .bs-popover-auto[data-popper-placement^=left] > .popover-arrow::before {
+ right: 0;
+ border-width: 0.5rem 0 0.5rem 0.5rem;
+ border-left-color: rgba(0, 0, 0, 0.25);
+}
+
+.bs-popover-start > .popover-arrow::after, .bs-popover-auto[data-popper-placement^=left] > .popover-arrow::after {
+ right: 1px;
+ border-width: 0.5rem 0 0.5rem 0.5rem;
+ border-left-color: #fff;
+}
+
+.popover-header {
+ padding: 0.5rem 1rem;
+ margin-bottom: 0;
+ font-size: 1rem;
+ background-color: #f0f0f0;
+ border-bottom: 1px solid rgba(0, 0, 0, 0.2);
+ border-top-left-radius: calc(0.3rem - 1px);
+ border-top-right-radius: calc(0.3rem - 1px);
+}
+
+.popover-header:empty {
+ display: none;
+}
+
+.popover-body {
+ padding: 1rem 1rem;
+ color: #212529;
+}
+
+.carousel {
+ position: relative;
+}
+
+.carousel.pointer-event {
+ touch-action: pan-y;
+}
+
+.carousel-inner {
+ position: relative;
+ width: 100%;
+ overflow: hidden;
+}
+
+.carousel-inner::after {
+ display: block;
+ clear: both;
+ content: "";
+}
+
+.carousel-item {
+ position: relative;
+ display: none;
+ float: left;
+ width: 100%;
+ margin-right: -100%;
+ -webkit-backface-visibility: hidden;
+ backface-visibility: hidden;
+ transition: transform 0.6s ease-in-out;
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .carousel-item {
+ transition: none;
+ }
+}
+
+.carousel-item.active,
+.carousel-item-next,
+.carousel-item-prev {
+ display: block;
+}
+
+/* rtl:begin:ignore */
+.carousel-item-next:not(.carousel-item-start),
+.active.carousel-item-end {
+ transform: translateX(100%);
+}
+
+.carousel-item-prev:not(.carousel-item-end),
+.active.carousel-item-start {
+ transform: translateX(-100%);
+}
+
+/* rtl:end:ignore */
+.carousel-fade .carousel-item {
+ opacity: 0;
+ transition-property: opacity;
+ transform: none;
+}
+
+.carousel-fade .carousel-item.active,
+.carousel-fade .carousel-item-next.carousel-item-start,
+.carousel-fade .carousel-item-prev.carousel-item-end {
+ z-index: 1;
+ opacity: 1;
+}
+
+.carousel-fade .active.carousel-item-start,
+.carousel-fade .active.carousel-item-end {
+ z-index: 0;
+ opacity: 0;
+ transition: opacity 0s 0.6s;
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .carousel-fade .active.carousel-item-start,
+ .carousel-fade .active.carousel-item-end {
+ transition: none;
+ }
+}
+
+.carousel-control-prev,
+.carousel-control-next {
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ z-index: 1;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 15%;
+ padding: 0;
+ color: #fff;
+ text-align: center;
+ background: none;
+ border: 0;
+ opacity: 0.5;
+ transition: opacity 0.15s ease;
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .carousel-control-prev,
+ .carousel-control-next {
+ transition: none;
+ }
+}
+
+.carousel-control-prev:hover, .carousel-control-prev:focus,
+.carousel-control-next:hover,
+.carousel-control-next:focus {
+ color: #fff;
+ text-decoration: none;
+ outline: 0;
+ opacity: 0.9;
+}
+
+.carousel-control-prev {
+ left: 0;
+}
+
+.carousel-control-next {
+ right: 0;
+}
+
+.carousel-control-prev-icon,
+.carousel-control-next-icon {
+ display: inline-block;
+ width: 2rem;
+ height: 2rem;
+ background-repeat: no-repeat;
+ background-position: 50%;
+ background-size: 100% 100%;
+}
+
+/* rtl:options: {
+ "autoRename": true,
+ "stringMap":[ {
+ "name" : "prev-next",
+ "search" : "prev",
+ "replace" : "next"
+ } ]
+} */
+.carousel-control-prev-icon {
+ background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z'/%3e%3c/svg%3e");
+}
+
+.carousel-control-next-icon {
+ background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");
+}
+
+.carousel-indicators {
+ position: absolute;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ z-index: 2;
+ display: flex;
+ justify-content: center;
+ padding: 0;
+ margin-right: 15%;
+ margin-bottom: 1rem;
+ margin-left: 15%;
+ list-style: none;
+}
+
+.carousel-indicators [data-bs-target] {
+ box-sizing: content-box;
+ flex: 0 1 auto;
+ width: 30px;
+ height: 3px;
+ padding: 0;
+ margin-right: 3px;
+ margin-left: 3px;
+ text-indent: -999px;
+ cursor: pointer;
+ background-color: #fff;
+ background-clip: padding-box;
+ border: 0;
+ border-top: 10px solid transparent;
+ border-bottom: 10px solid transparent;
+ opacity: 0.5;
+ transition: opacity 0.6s ease;
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .carousel-indicators [data-bs-target] {
+ transition: none;
+ }
+}
+
+.carousel-indicators .active {
+ opacity: 1;
+}
+
+.carousel-caption {
+ position: absolute;
+ right: 15%;
+ bottom: 1.25rem;
+ left: 15%;
+ padding-top: 1.25rem;
+ padding-bottom: 1.25rem;
+ color: #fff;
+ text-align: center;
+}
+
+.carousel-dark .carousel-control-prev-icon,
+.carousel-dark .carousel-control-next-icon {
+ filter: invert(1) grayscale(100);
+}
+
+.carousel-dark .carousel-indicators [data-bs-target] {
+ background-color: #000;
+}
+
+.carousel-dark .carousel-caption {
+ color: #000;
+}
+
+@-webkit-keyframes spinner-border {
+ to {
+ transform: rotate(360deg) /* rtl:ignore */;
+ }
+}
+
+@keyframes spinner-border {
+ to {
+ transform: rotate(360deg) /* rtl:ignore */;
+ }
+}
+
+.spinner-border {
+ display: inline-block;
+ width: 2rem;
+ height: 2rem;
+ vertical-align: -0.125em;
+ border: 0.25em solid currentColor;
+ border-right-color: transparent;
+ border-radius: 50%;
+ -webkit-animation: 0.75s linear infinite spinner-border;
+ animation: 0.75s linear infinite spinner-border;
+}
+
+.spinner-border-sm {
+ width: 1rem;
+ height: 1rem;
+ border-width: 0.2em;
+}
+
+@-webkit-keyframes spinner-grow {
+ 0% {
+ transform: scale(0);
+ }
+ 50% {
+ opacity: 1;
+ transform: none;
+ }
+}
+
+@keyframes spinner-grow {
+ 0% {
+ transform: scale(0);
+ }
+ 50% {
+ opacity: 1;
+ transform: none;
+ }
+}
+
+.spinner-grow {
+ display: inline-block;
+ width: 2rem;
+ height: 2rem;
+ vertical-align: -0.125em;
+ background-color: currentColor;
+ border-radius: 50%;
+ opacity: 0;
+ -webkit-animation: 0.75s linear infinite spinner-grow;
+ animation: 0.75s linear infinite spinner-grow;
+}
+
+.spinner-grow-sm {
+ width: 1rem;
+ height: 1rem;
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .spinner-border,
+ .spinner-grow {
+ -webkit-animation-duration: 1.5s;
+ animation-duration: 1.5s;
+ }
+}
+
+.offcanvas {
+ position: fixed;
+ bottom: 0;
+ z-index: 1045;
+ display: flex;
+ flex-direction: column;
+ max-width: 100%;
+ visibility: hidden;
+ background-color: #fff;
+ background-clip: padding-box;
+ outline: 0;
+ transition: transform 0.3s ease-in-out;
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .offcanvas {
+ transition: none;
+ }
+}
+
+.offcanvas-backdrop {
+ position: fixed;
+ top: 0;
+ left: 0;
+ z-index: 1040;
+ width: 100vw;
+ height: 100vh;
+ background-color: #000;
+}
+
+.offcanvas-backdrop.fade {
+ opacity: 0;
+}
+
+.offcanvas-backdrop.show {
+ opacity: 0.5;
+}
+
+.offcanvas-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 1rem 1rem;
+}
+
+.offcanvas-header .btn-close {
+ padding: 0.5rem 0.5rem;
+ margin-top: -0.5rem;
+ margin-right: -0.5rem;
+ margin-bottom: -0.5rem;
+}
+
+.offcanvas-title {
+ margin-bottom: 0;
+ line-height: 1.5;
+}
+
+.offcanvas-body {
+ flex-grow: 1;
+ padding: 1rem 1rem;
+ overflow-y: auto;
+}
+
+.offcanvas-start {
+ top: 0;
+ left: 0;
+ width: 400px;
+ border-right: 1px solid rgba(0, 0, 0, 0.2);
+ transform: translateX(-100%);
+}
+
+.offcanvas-end {
+ top: 0;
+ right: 0;
+ width: 400px;
+ border-left: 1px solid rgba(0, 0, 0, 0.2);
+ transform: translateX(100%);
+}
+
+.offcanvas-top {
+ top: 0;
+ right: 0;
+ left: 0;
+ height: 30vh;
+ max-height: 100%;
+ border-bottom: 1px solid rgba(0, 0, 0, 0.2);
+ transform: translateY(-100%);
+}
+
+.offcanvas-bottom {
+ right: 0;
+ left: 0;
+ height: 30vh;
+ max-height: 100%;
+ border-top: 1px solid rgba(0, 0, 0, 0.2);
+ transform: translateY(100%);
+}
+
+.offcanvas.show {
+ transform: none;
+}
+
+.placeholder {
+ display: inline-block;
+ min-height: 1em;
+ vertical-align: middle;
+ cursor: wait;
+ background-color: currentColor;
+ opacity: 0.5;
+}
+
+.placeholder.btn::before {
+ display: inline-block;
+ content: "";
+}
+
+.placeholder-xs {
+ min-height: 0.6em;
+}
+
+.placeholder-sm {
+ min-height: 0.8em;
+}
+
+.placeholder-lg {
+ min-height: 1.2em;
+}
+
+.placeholder-glow .placeholder {
+ -webkit-animation: placeholder-glow 2s ease-in-out infinite;
+ animation: placeholder-glow 2s ease-in-out infinite;
+}
+
+@-webkit-keyframes placeholder-glow {
+ 50% {
+ opacity: 0.2;
+ }
+}
+
+@keyframes placeholder-glow {
+ 50% {
+ opacity: 0.2;
+ }
+}
+
+.placeholder-wave {
+ -webkit-mask-image: linear-gradient(130deg, #000 55%, rgba(0, 0, 0, 0.8) 75%, #000 95%);
+ mask-image: linear-gradient(130deg, #000 55%, rgba(0, 0, 0, 0.8) 75%, #000 95%);
+ -webkit-mask-size: 200% 100%;
+ mask-size: 200% 100%;
+ -webkit-animation: placeholder-wave 2s linear infinite;
+ animation: placeholder-wave 2s linear infinite;
+}
+
+@-webkit-keyframes placeholder-wave {
+ 100% {
+ -webkit-mask-position: -200% 0%;
+ mask-position: -200% 0%;
+ }
+}
+
+@keyframes placeholder-wave {
+ 100% {
+ -webkit-mask-position: -200% 0%;
+ mask-position: -200% 0%;
+ }
+}
+
+.clearfix::after {
+ display: block;
+ clear: both;
+ content: "";
+}
+
+.link-primary {
+ color: #0d6efd;
+}
+
+.link-primary:hover, .link-primary:focus {
+ color: #0a58ca;
+}
+
+.link-secondary {
+ color: #6c757d;
+}
+
+.link-secondary:hover, .link-secondary:focus {
+ color: #565e64;
+}
+
+.link-success {
+ color: #198754;
+}
+
+.link-success:hover, .link-success:focus {
+ color: #146c43;
+}
+
+.link-info {
+ color: #0dcaf0;
+}
+
+.link-info:hover, .link-info:focus {
+ color: #3dd5f3;
+}
+
+.link-warning {
+ color: #ffc107;
+}
+
+.link-warning:hover, .link-warning:focus {
+ color: #ffcd39;
+}
+
+.link-danger {
+ color: #dc3545;
+}
+
+.link-danger:hover, .link-danger:focus {
+ color: #b02a37;
+}
+
+.link-light {
+ color: #f8f9fa;
+}
+
+.link-light:hover, .link-light:focus {
+ color: #f9fafb;
+}
+
+.link-dark {
+ color: #212529;
+}
+
+.link-dark:hover, .link-dark:focus {
+ color: #1a1e21;
+}
+
+.ratio {
+ position: relative;
+ width: 100%;
+}
+
+.ratio::before {
+ display: block;
+ padding-top: var(--bs-aspect-ratio);
+ content: "";
+}
+
+.ratio > * {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+}
+
+.ratio-1x1 {
+ --bs-aspect-ratio: 100%;
+}
+
+.ratio-4x3 {
+ --bs-aspect-ratio: 75%;
+}
+
+.ratio-16x9 {
+ --bs-aspect-ratio: 56.25%;
+}
+
+.ratio-21x9 {
+ --bs-aspect-ratio: 42.8571428571%;
+}
+
+.fixed-top {
+ position: fixed;
+ top: 0;
+ right: 0;
+ left: 0;
+ z-index: 1030;
+}
+
+.fixed-bottom {
+ position: fixed;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ z-index: 1030;
+}
+
+.sticky-top {
+ position: -webkit-sticky;
+ position: sticky;
+ top: 0;
+ z-index: 1020;
+}
+
+@media (min-width: 576px) {
+ .sticky-sm-top {
+ position: -webkit-sticky;
+ position: sticky;
+ top: 0;
+ z-index: 1020;
+ }
+}
+
+@media (min-width: 768px) {
+ .sticky-md-top {
+ position: -webkit-sticky;
+ position: sticky;
+ top: 0;
+ z-index: 1020;
+ }
+}
+
+@media (min-width: 992px) {
+ .sticky-lg-top {
+ position: -webkit-sticky;
+ position: sticky;
+ top: 0;
+ z-index: 1020;
+ }
+}
+
+@media (min-width: 1200px) {
+ .sticky-xl-top {
+ position: -webkit-sticky;
+ position: sticky;
+ top: 0;
+ z-index: 1020;
+ }
+}
+
+@media (min-width: 1400px) {
+ .sticky-xxl-top {
+ position: -webkit-sticky;
+ position: sticky;
+ top: 0;
+ z-index: 1020;
+ }
+}
+
+.hstack {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ align-self: stretch;
+}
+
+.vstack {
+ display: flex;
+ flex: 1 1 auto;
+ flex-direction: column;
+ align-self: stretch;
+}
+
+.visually-hidden,
+.visually-hidden-focusable:not(:focus):not(:focus-within) {
+ position: absolute !important;
+ width: 1px !important;
+ height: 1px !important;
+ padding: 0 !important;
+ margin: -1px !important;
+ overflow: hidden !important;
+ clip: rect(0, 0, 0, 0) !important;
+ white-space: nowrap !important;
+ border: 0 !important;
+}
+
+.stretched-link::after {
+ position: absolute;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ z-index: 1;
+ content: "";
+}
+
+.text-truncate {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.vr {
+ display: inline-block;
+ align-self: stretch;
+ width: 1px;
+ min-height: 1em;
+ background-color: currentColor;
+ opacity: 0.25;
+}
+
+.align-baseline {
+ vertical-align: baseline !important;
+}
+
+.align-top {
+ vertical-align: top !important;
+}
+
+.align-middle {
+ vertical-align: middle !important;
+}
+
+.align-bottom {
+ vertical-align: bottom !important;
+}
+
+.align-text-bottom {
+ vertical-align: text-bottom !important;
+}
+
+.align-text-top {
+ vertical-align: text-top !important;
+}
+
+.float-start {
+ float: left !important;
+}
+
+.float-end {
+ float: right !important;
+}
+
+.float-none {
+ float: none !important;
+}
+
+.opacity-0 {
+ opacity: 0 !important;
+}
+
+.opacity-25 {
+ opacity: 0.25 !important;
+}
+
+.opacity-50 {
+ opacity: 0.5 !important;
+}
+
+.opacity-75 {
+ opacity: 0.75 !important;
+}
+
+.opacity-100 {
+ opacity: 1 !important;
+}
+
+.overflow-auto {
+ overflow: auto !important;
+}
+
+.overflow-hidden {
+ overflow: hidden !important;
+}
+
+.overflow-visible {
+ overflow: visible !important;
+}
+
+.overflow-scroll {
+ overflow: scroll !important;
+}
+
+.d-inline {
+ display: inline !important;
+}
+
+.d-inline-block {
+ display: inline-block !important;
+}
+
+.d-block {
+ display: block !important;
+}
+
+.d-grid {
+ display: grid !important;
+}
+
+.d-table {
+ display: table !important;
+}
+
+.d-table-row {
+ display: table-row !important;
+}
+
+.d-table-cell {
+ display: table-cell !important;
+}
+
+.d-flex {
+ display: flex !important;
+}
+
+.d-inline-flex {
+ display: inline-flex !important;
+}
+
+.d-none {
+ display: none !important;
+}
+
+.shadow {
+ box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15) !important;
+}
+
+.shadow-sm {
+ box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075) !important;
+}
+
+.shadow-lg {
+ box-shadow: 0 1rem 3rem rgba(0, 0, 0, 0.175) !important;
+}
+
+.shadow-none {
+ box-shadow: none !important;
+}
+
+.position-static {
+ position: static !important;
+}
+
+.position-relative {
+ position: relative !important;
+}
+
+.position-absolute {
+ position: absolute !important;
+}
+
+.position-fixed {
+ position: fixed !important;
+}
+
+.position-sticky {
+ position: -webkit-sticky !important;
+ position: sticky !important;
+}
+
+.top-0 {
+ top: 0 !important;
+}
+
+.top-50 {
+ top: 50% !important;
+}
+
+.top-100 {
+ top: 100% !important;
+}
+
+.bottom-0 {
+ bottom: 0 !important;
+}
+
+.bottom-50 {
+ bottom: 50% !important;
+}
+
+.bottom-100 {
+ bottom: 100% !important;
+}
+
+.start-0 {
+ left: 0 !important;
+}
+
+.start-50 {
+ left: 50% !important;
+}
+
+.start-100 {
+ left: 100% !important;
+}
+
+.end-0 {
+ right: 0 !important;
+}
+
+.end-50 {
+ right: 50% !important;
+}
+
+.end-100 {
+ right: 100% !important;
+}
+
+.translate-middle {
+ transform: translate(-50%, -50%) !important;
+}
+
+.translate-middle-x {
+ transform: translateX(-50%) !important;
+}
+
+.translate-middle-y {
+ transform: translateY(-50%) !important;
+}
+
+.border {
+ border: 1px solid #dee2e6 !important;
+}
+
+.border-0 {
+ border: 0 !important;
+}
+
+.border-top {
+ border-top: 1px solid #dee2e6 !important;
+}
+
+.border-top-0 {
+ border-top: 0 !important;
+}
+
+.border-end {
+ border-right: 1px solid #dee2e6 !important;
+}
+
+.border-end-0 {
+ border-right: 0 !important;
+}
+
+.border-bottom {
+ border-bottom: 1px solid #dee2e6 !important;
+}
+
+.border-bottom-0 {
+ border-bottom: 0 !important;
+}
+
+.border-start {
+ border-left: 1px solid #dee2e6 !important;
+}
+
+.border-start-0 {
+ border-left: 0 !important;
+}
+
+.border-primary {
+ border-color: #0d6efd !important;
+}
+
+.border-secondary {
+ border-color: #6c757d !important;
+}
+
+.border-success {
+ border-color: #198754 !important;
+}
+
+.border-info {
+ border-color: #0dcaf0 !important;
+}
+
+.border-warning {
+ border-color: #ffc107 !important;
+}
+
+.border-danger {
+ border-color: #dc3545 !important;
+}
+
+.border-light {
+ border-color: #f8f9fa !important;
+}
+
+.border-dark {
+ border-color: #212529 !important;
+}
+
+.border-white {
+ border-color: #fff !important;
+}
+
+.border-1 {
+ border-width: 1px !important;
+}
+
+.border-2 {
+ border-width: 2px !important;
+}
+
+.border-3 {
+ border-width: 3px !important;
+}
+
+.border-4 {
+ border-width: 4px !important;
+}
+
+.border-5 {
+ border-width: 5px !important;
+}
+
+.w-25 {
+ width: 25% !important;
+}
+
+.w-50 {
+ width: 50% !important;
+}
+
+.w-75 {
+ width: 75% !important;
+}
+
+.w-100 {
+ width: 100% !important;
+}
+
+.w-auto {
+ width: auto !important;
+}
+
+.mw-100 {
+ max-width: 100% !important;
+}
+
+.vw-100 {
+ width: 100vw !important;
+}
+
+.min-vw-100 {
+ min-width: 100vw !important;
+}
+
+.h-25 {
+ height: 25% !important;
+}
+
+.h-50 {
+ height: 50% !important;
+}
+
+.h-75 {
+ height: 75% !important;
+}
+
+.h-100 {
+ height: 100% !important;
+}
+
+.h-auto {
+ height: auto !important;
+}
+
+.mh-100 {
+ max-height: 100% !important;
+}
+
+.vh-100 {
+ height: 100vh !important;
+}
+
+.min-vh-100 {
+ min-height: 100vh !important;
+}
+
+.flex-fill {
+ flex: 1 1 auto !important;
+}
+
+.flex-row {
+ flex-direction: row !important;
+}
+
+.flex-column {
+ flex-direction: column !important;
+}
+
+.flex-row-reverse {
+ flex-direction: row-reverse !important;
+}
+
+.flex-column-reverse {
+ flex-direction: column-reverse !important;
+}
+
+.flex-grow-0 {
+ flex-grow: 0 !important;
+}
+
+.flex-grow-1 {
+ flex-grow: 1 !important;
+}
+
+.flex-shrink-0 {
+ flex-shrink: 0 !important;
+}
+
+.flex-shrink-1 {
+ flex-shrink: 1 !important;
+}
+
+.flex-wrap {
+ flex-wrap: wrap !important;
+}
+
+.flex-nowrap {
+ flex-wrap: nowrap !important;
+}
+
+.flex-wrap-reverse {
+ flex-wrap: wrap-reverse !important;
+}
+
+.gap-0 {
+ gap: 0 !important;
+}
+
+.gap-1 {
+ gap: 0.25rem !important;
+}
+
+.gap-2 {
+ gap: 0.5rem !important;
+}
+
+.gap-3 {
+ gap: 1rem !important;
+}
+
+.gap-4 {
+ gap: 1.5rem !important;
+}
+
+.gap-5 {
+ gap: 3rem !important;
+}
+
+.justify-content-start {
+ justify-content: flex-start !important;
+}
+
+.justify-content-end {
+ justify-content: flex-end !important;
+}
+
+.justify-content-center {
+ justify-content: center !important;
+}
+
+.justify-content-between {
+ justify-content: space-between !important;
+}
+
+.justify-content-around {
+ justify-content: space-around !important;
+}
+
+.justify-content-evenly {
+ justify-content: space-evenly !important;
+}
+
+.align-items-start {
+ align-items: flex-start !important;
+}
+
+.align-items-end {
+ align-items: flex-end !important;
+}
+
+.align-items-center {
+ align-items: center !important;
+}
+
+.align-items-baseline {
+ align-items: baseline !important;
+}
+
+.align-items-stretch {
+ align-items: stretch !important;
+}
+
+.align-content-start {
+ align-content: flex-start !important;
+}
+
+.align-content-end {
+ align-content: flex-end !important;
+}
+
+.align-content-center {
+ align-content: center !important;
+}
+
+.align-content-between {
+ align-content: space-between !important;
+}
+
+.align-content-around {
+ align-content: space-around !important;
+}
+
+.align-content-stretch {
+ align-content: stretch !important;
+}
+
+.align-self-auto {
+ align-self: auto !important;
+}
+
+.align-self-start {
+ align-self: flex-start !important;
+}
+
+.align-self-end {
+ align-self: flex-end !important;
+}
+
+.align-self-center {
+ align-self: center !important;
+}
+
+.align-self-baseline {
+ align-self: baseline !important;
+}
+
+.align-self-stretch {
+ align-self: stretch !important;
+}
+
+.order-first {
+ order: -1 !important;
+}
+
+.order-0 {
+ order: 0 !important;
+}
+
+.order-1 {
+ order: 1 !important;
+}
+
+.order-2 {
+ order: 2 !important;
+}
+
+.order-3 {
+ order: 3 !important;
+}
+
+.order-4 {
+ order: 4 !important;
+}
+
+.order-5 {
+ order: 5 !important;
+}
+
+.order-last {
+ order: 6 !important;
+}
+
+.m-0 {
+ margin: 0 !important;
+}
+
+.m-1 {
+ margin: 0.25rem !important;
+}
+
+.m-2 {
+ margin: 0.5rem !important;
+}
+
+.m-3 {
+ margin: 1rem !important;
+}
+
+.m-4 {
+ margin: 1.5rem !important;
+}
+
+.m-5 {
+ margin: 3rem !important;
+}
+
+.m-auto {
+ margin: auto !important;
+}
+
+.mx-0 {
+ margin-right: 0 !important;
+ margin-left: 0 !important;
+}
+
+.mx-1 {
+ margin-right: 0.25rem !important;
+ margin-left: 0.25rem !important;
+}
+
+.mx-2 {
+ margin-right: 0.5rem !important;
+ margin-left: 0.5rem !important;
+}
+
+.mx-3 {
+ margin-right: 1rem !important;
+ margin-left: 1rem !important;
+}
+
+.mx-4 {
+ margin-right: 1.5rem !important;
+ margin-left: 1.5rem !important;
+}
+
+.mx-5 {
+ margin-right: 3rem !important;
+ margin-left: 3rem !important;
+}
+
+.mx-auto {
+ margin-right: auto !important;
+ margin-left: auto !important;
+}
+
+.my-0 {
+ margin-top: 0 !important;
+ margin-bottom: 0 !important;
+}
+
+.my-1 {
+ margin-top: 0.25rem !important;
+ margin-bottom: 0.25rem !important;
+}
+
+.my-2 {
+ margin-top: 0.5rem !important;
+ margin-bottom: 0.5rem !important;
+}
+
+.my-3 {
+ margin-top: 1rem !important;
+ margin-bottom: 1rem !important;
+}
+
+.my-4 {
+ margin-top: 1.5rem !important;
+ margin-bottom: 1.5rem !important;
+}
+
+.my-5 {
+ margin-top: 3rem !important;
+ margin-bottom: 3rem !important;
+}
+
+.my-auto {
+ margin-top: auto !important;
+ margin-bottom: auto !important;
+}
+
+.mt-0 {
+ margin-top: 0 !important;
+}
+
+.mt-1 {
+ margin-top: 0.25rem !important;
+}
+
+.mt-2 {
+ margin-top: 0.5rem !important;
+}
+
+.mt-3 {
+ margin-top: 1rem !important;
+}
+
+.mt-4 {
+ margin-top: 1.5rem !important;
+}
+
+.mt-5 {
+ margin-top: 3rem !important;
+}
+
+.mt-auto {
+ margin-top: auto !important;
+}
+
+.me-0 {
+ margin-right: 0 !important;
+}
+
+.me-1 {
+ margin-right: 0.25rem !important;
+}
+
+.me-2 {
+ margin-right: 0.5rem !important;
+}
+
+.me-3 {
+ margin-right: 1rem !important;
+}
+
+.me-4 {
+ margin-right: 1.5rem !important;
+}
+
+.me-5 {
+ margin-right: 3rem !important;
+}
+
+.me-auto {
+ margin-right: auto !important;
+}
+
+.mb-0 {
+ margin-bottom: 0 !important;
+}
+
+.mb-1 {
+ margin-bottom: 0.25rem !important;
+}
+
+.mb-2 {
+ margin-bottom: 0.5rem !important;
+}
+
+.mb-3 {
+ margin-bottom: 1rem !important;
+}
+
+.mb-4 {
+ margin-bottom: 1.5rem !important;
+}
+
+.mb-5 {
+ margin-bottom: 3rem !important;
+}
+
+.mb-auto {
+ margin-bottom: auto !important;
+}
+
+.ms-0 {
+ margin-left: 0 !important;
+}
+
+.ms-1 {
+ margin-left: 0.25rem !important;
+}
+
+.ms-2 {
+ margin-left: 0.5rem !important;
+}
+
+.ms-3 {
+ margin-left: 1rem !important;
+}
+
+.ms-4 {
+ margin-left: 1.5rem !important;
+}
+
+.ms-5 {
+ margin-left: 3rem !important;
+}
+
+.ms-auto {
+ margin-left: auto !important;
+}
+
+.p-0 {
+ padding: 0 !important;
+}
+
+.p-1 {
+ padding: 0.25rem !important;
+}
+
+.p-2 {
+ padding: 0.5rem !important;
+}
+
+.p-3 {
+ padding: 1rem !important;
+}
+
+.p-4 {
+ padding: 1.5rem !important;
+}
+
+.p-5 {
+ padding: 3rem !important;
+}
+
+.px-0 {
+ padding-right: 0 !important;
+ padding-left: 0 !important;
+}
+
+.px-1 {
+ padding-right: 0.25rem !important;
+ padding-left: 0.25rem !important;
+}
+
+.px-2 {
+ padding-right: 0.5rem !important;
+ padding-left: 0.5rem !important;
+}
+
+.px-3 {
+ padding-right: 1rem !important;
+ padding-left: 1rem !important;
+}
+
+.px-4 {
+ padding-right: 1.5rem !important;
+ padding-left: 1.5rem !important;
+}
+
+.px-5 {
+ padding-right: 3rem !important;
+ padding-left: 3rem !important;
+}
+
+.py-0 {
+ padding-top: 0 !important;
+ padding-bottom: 0 !important;
+}
+
+.py-1 {
+ padding-top: 0.25rem !important;
+ padding-bottom: 0.25rem !important;
+}
+
+.py-2 {
+ padding-top: 0.5rem !important;
+ padding-bottom: 0.5rem !important;
+}
+
+.py-3 {
+ padding-top: 1rem !important;
+ padding-bottom: 1rem !important;
+}
+
+.py-4 {
+ padding-top: 1.5rem !important;
+ padding-bottom: 1.5rem !important;
+}
+
+.py-5 {
+ padding-top: 3rem !important;
+ padding-bottom: 3rem !important;
+}
+
+.pt-0 {
+ padding-top: 0 !important;
+}
+
+.pt-1 {
+ padding-top: 0.25rem !important;
+}
+
+.pt-2 {
+ padding-top: 0.5rem !important;
+}
+
+.pt-3 {
+ padding-top: 1rem !important;
+}
+
+.pt-4 {
+ padding-top: 1.5rem !important;
+}
+
+.pt-5 {
+ padding-top: 3rem !important;
+}
+
+.pe-0 {
+ padding-right: 0 !important;
+}
+
+.pe-1 {
+ padding-right: 0.25rem !important;
+}
+
+.pe-2 {
+ padding-right: 0.5rem !important;
+}
+
+.pe-3 {
+ padding-right: 1rem !important;
+}
+
+.pe-4 {
+ padding-right: 1.5rem !important;
+}
+
+.pe-5 {
+ padding-right: 3rem !important;
+}
+
+.pb-0 {
+ padding-bottom: 0 !important;
+}
+
+.pb-1 {
+ padding-bottom: 0.25rem !important;
+}
+
+.pb-2 {
+ padding-bottom: 0.5rem !important;
+}
+
+.pb-3 {
+ padding-bottom: 1rem !important;
+}
+
+.pb-4 {
+ padding-bottom: 1.5rem !important;
+}
+
+.pb-5 {
+ padding-bottom: 3rem !important;
+}
+
+.ps-0 {
+ padding-left: 0 !important;
+}
+
+.ps-1 {
+ padding-left: 0.25rem !important;
+}
+
+.ps-2 {
+ padding-left: 0.5rem !important;
+}
+
+.ps-3 {
+ padding-left: 1rem !important;
+}
+
+.ps-4 {
+ padding-left: 1.5rem !important;
+}
+
+.ps-5 {
+ padding-left: 3rem !important;
+}
+
+.font-monospace {
+ font-family: var(--bs-font-monospace) !important;
+}
+
+.fs-1 {
+ font-size: calc(1.375rem + 1.5vw) !important;
+}
+
+.fs-2 {
+ font-size: calc(1.325rem + 0.9vw) !important;
+}
+
+.fs-3 {
+ font-size: calc(1.3rem + 0.6vw) !important;
+}
+
+.fs-4 {
+ font-size: calc(1.275rem + 0.3vw) !important;
+}
+
+.fs-5 {
+ font-size: 1.25rem !important;
+}
+
+.fs-6 {
+ font-size: 1rem !important;
+}
+
+.fst-italic {
+ font-style: italic !important;
+}
+
+.fst-normal {
+ font-style: normal !important;
+}
+
+.fw-light {
+ font-weight: 300 !important;
+}
+
+.fw-lighter {
+ font-weight: lighter !important;
+}
+
+.fw-normal {
+ font-weight: 400 !important;
+}
+
+.fw-bold {
+ font-weight: 700 !important;
+}
+
+.fw-bolder {
+ font-weight: bolder !important;
+}
+
+.lh-1 {
+ line-height: 1 !important;
+}
+
+.lh-sm {
+ line-height: 1.25 !important;
+}
+
+.lh-base {
+ line-height: 1.5 !important;
+}
+
+.lh-lg {
+ line-height: 2 !important;
+}
+
+.text-start {
+ text-align: left !important;
+}
+
+.text-end {
+ text-align: right !important;
+}
+
+.text-center {
+ text-align: center !important;
+}
+
+.text-decoration-none {
+ text-decoration: none !important;
+}
+
+.text-decoration-underline {
+ text-decoration: underline !important;
+}
+
+.text-decoration-line-through {
+ text-decoration: line-through !important;
+}
+
+.text-lowercase {
+ text-transform: lowercase !important;
+}
+
+.text-uppercase {
+ text-transform: uppercase !important;
+}
+
+.text-capitalize {
+ text-transform: capitalize !important;
+}
+
+.text-wrap {
+ white-space: normal !important;
+}
+
+.text-nowrap {
+ white-space: nowrap !important;
+}
+
+/* rtl:begin:remove */
+.text-break {
+ word-wrap: break-word !important;
+ word-break: break-word !important;
+}
+
+/* rtl:end:remove */
+.text-primary {
+ --bs-text-opacity: 1;
+ color: rgba(var(--bs-primary-rgb), var(--bs-text-opacity)) !important;
+}
+
+.text-secondary {
+ --bs-text-opacity: 1;
+ color: rgba(var(--bs-secondary-rgb), var(--bs-text-opacity)) !important;
+}
+
+.text-success {
+ --bs-text-opacity: 1;
+ color: rgba(var(--bs-success-rgb), var(--bs-text-opacity)) !important;
+}
+
+.text-info {
+ --bs-text-opacity: 1;
+ color: rgba(var(--bs-info-rgb), var(--bs-text-opacity)) !important;
+}
+
+.text-warning {
+ --bs-text-opacity: 1;
+ color: rgba(var(--bs-warning-rgb), var(--bs-text-opacity)) !important;
+}
+
+.text-danger {
+ --bs-text-opacity: 1;
+ color: rgba(var(--bs-danger-rgb), var(--bs-text-opacity)) !important;
+}
+
+.text-light {
+ --bs-text-opacity: 1;
+ color: rgba(var(--bs-light-rgb), var(--bs-text-opacity)) !important;
+}
+
+.text-dark {
+ --bs-text-opacity: 1;
+ color: rgba(var(--bs-dark-rgb), var(--bs-text-opacity)) !important;
+}
+
+.text-black {
+ --bs-text-opacity: 1;
+ color: rgba(var(--bs-black-rgb), var(--bs-text-opacity)) !important;
+}
+
+.text-white {
+ --bs-text-opacity: 1;
+ color: rgba(var(--bs-white-rgb), var(--bs-text-opacity)) !important;
+}
+
+.text-body {
+ --bs-text-opacity: 1;
+ color: rgba(var(--bs-body-color-rgb), var(--bs-text-opacity)) !important;
+}
+
+.text-muted {
+ --bs-text-opacity: 1;
+ color: #6c757d !important;
+}
+
+.text-black-50 {
+ --bs-text-opacity: 1;
+ color: rgba(0, 0, 0, 0.5) !important;
+}
+
+.text-white-50 {
+ --bs-text-opacity: 1;
+ color: rgba(255, 255, 255, 0.5) !important;
+}
+
+.text-reset {
+ --bs-text-opacity: 1;
+ color: inherit !important;
+}
+
+.text-opacity-25 {
+ --bs-text-opacity: 0.25;
+}
+
+.text-opacity-50 {
+ --bs-text-opacity: 0.5;
+}
+
+.text-opacity-75 {
+ --bs-text-opacity: 0.75;
+}
+
+.text-opacity-100 {
+ --bs-text-opacity: 1;
+}
+
+.bg-primary {
+ --bs-bg-opacity: 1;
+ background-color: rgba(var(--bs-primary-rgb), var(--bs-bg-opacity)) !important;
+}
+
+.bg-secondary {
+ --bs-bg-opacity: 1;
+ background-color: rgba(var(--bs-secondary-rgb), var(--bs-bg-opacity)) !important;
+}
+
+.bg-success {
+ --bs-bg-opacity: 1;
+ background-color: rgba(var(--bs-success-rgb), var(--bs-bg-opacity)) !important;
+}
+
+.bg-info {
+ --bs-bg-opacity: 1;
+ background-color: rgba(var(--bs-info-rgb), var(--bs-bg-opacity)) !important;
+}
+
+.bg-warning {
+ --bs-bg-opacity: 1;
+ background-color: rgba(var(--bs-warning-rgb), var(--bs-bg-opacity)) !important;
+}
+
+.bg-danger {
+ --bs-bg-opacity: 1;
+ background-color: rgba(var(--bs-danger-rgb), var(--bs-bg-opacity)) !important;
+}
+
+.bg-light {
+ --bs-bg-opacity: 1;
+ background-color: rgba(var(--bs-light-rgb), var(--bs-bg-opacity)) !important;
+}
+
+.bg-dark {
+ --bs-bg-opacity: 1;
+ background-color: rgba(var(--bs-dark-rgb), var(--bs-bg-opacity)) !important;
+}
+
+.bg-black {
+ --bs-bg-opacity: 1;
+ background-color: rgba(var(--bs-black-rgb), var(--bs-bg-opacity)) !important;
+}
+
+.bg-white {
+ --bs-bg-opacity: 1;
+ background-color: rgba(var(--bs-white-rgb), var(--bs-bg-opacity)) !important;
+}
+
+.bg-body {
+ --bs-bg-opacity: 1;
+ background-color: rgba(var(--bs-body-bg-rgb), var(--bs-bg-opacity)) !important;
+}
+
+.bg-transparent {
+ --bs-bg-opacity: 1;
+ background-color: transparent !important;
+}
+
+.bg-opacity-10 {
+ --bs-bg-opacity: 0.1;
+}
+
+.bg-opacity-25 {
+ --bs-bg-opacity: 0.25;
+}
+
+.bg-opacity-50 {
+ --bs-bg-opacity: 0.5;
+}
+
+.bg-opacity-75 {
+ --bs-bg-opacity: 0.75;
+}
+
+.bg-opacity-100 {
+ --bs-bg-opacity: 1;
+}
+
+.bg-gradient {
+ background-image: var(--bs-gradient) !important;
+}
+
+.user-select-all {
+ -webkit-user-select: all !important;
+ -moz-user-select: all !important;
+ user-select: all !important;
+}
+
+.user-select-auto {
+ -webkit-user-select: auto !important;
+ -moz-user-select: auto !important;
+ user-select: auto !important;
+}
+
+.user-select-none {
+ -webkit-user-select: none !important;
+ -moz-user-select: none !important;
+ user-select: none !important;
+}
+
+.pe-none {
+ pointer-events: none !important;
+}
+
+.pe-auto {
+ pointer-events: auto !important;
+}
+
+.rounded {
+ border-radius: 0.25rem !important;
+}
+
+.rounded-0 {
+ border-radius: 0 !important;
+}
+
+.rounded-1 {
+ border-radius: 0.2rem !important;
+}
+
+.rounded-2 {
+ border-radius: 0.25rem !important;
+}
+
+.rounded-3 {
+ border-radius: 0.3rem !important;
+}
+
+.rounded-circle {
+ border-radius: 50% !important;
+}
+
+.rounded-pill {
+ border-radius: 50rem !important;
+}
+
+.rounded-top {
+ border-top-left-radius: 0.25rem !important;
+ border-top-right-radius: 0.25rem !important;
+}
+
+.rounded-end {
+ border-top-right-radius: 0.25rem !important;
+ border-bottom-right-radius: 0.25rem !important;
+}
+
+.rounded-bottom {
+ border-bottom-right-radius: 0.25rem !important;
+ border-bottom-left-radius: 0.25rem !important;
+}
+
+.rounded-start {
+ border-bottom-left-radius: 0.25rem !important;
+ border-top-left-radius: 0.25rem !important;
+}
+
+.visible {
+ visibility: visible !important;
+}
+
+.invisible {
+ visibility: hidden !important;
+}
+
+@media (min-width: 576px) {
+ .float-sm-start {
+ float: left !important;
+ }
+
+ .float-sm-end {
+ float: right !important;
+ }
+
+ .float-sm-none {
+ float: none !important;
+ }
+
+ .d-sm-inline {
+ display: inline !important;
+ }
+
+ .d-sm-inline-block {
+ display: inline-block !important;
+ }
+
+ .d-sm-block {
+ display: block !important;
+ }
+
+ .d-sm-grid {
+ display: grid !important;
+ }
+
+ .d-sm-table {
+ display: table !important;
+ }
+
+ .d-sm-table-row {
+ display: table-row !important;
+ }
+
+ .d-sm-table-cell {
+ display: table-cell !important;
+ }
+
+ .d-sm-flex {
+ display: flex !important;
+ }
+
+ .d-sm-inline-flex {
+ display: inline-flex !important;
+ }
+
+ .d-sm-none {
+ display: none !important;
+ }
+
+ .flex-sm-fill {
+ flex: 1 1 auto !important;
+ }
+
+ .flex-sm-row {
+ flex-direction: row !important;
+ }
+
+ .flex-sm-column {
+ flex-direction: column !important;
+ }
+
+ .flex-sm-row-reverse {
+ flex-direction: row-reverse !important;
+ }
+
+ .flex-sm-column-reverse {
+ flex-direction: column-reverse !important;
+ }
+
+ .flex-sm-grow-0 {
+ flex-grow: 0 !important;
+ }
+
+ .flex-sm-grow-1 {
+ flex-grow: 1 !important;
+ }
+
+ .flex-sm-shrink-0 {
+ flex-shrink: 0 !important;
+ }
+
+ .flex-sm-shrink-1 {
+ flex-shrink: 1 !important;
+ }
+
+ .flex-sm-wrap {
+ flex-wrap: wrap !important;
+ }
+
+ .flex-sm-nowrap {
+ flex-wrap: nowrap !important;
+ }
+
+ .flex-sm-wrap-reverse {
+ flex-wrap: wrap-reverse !important;
+ }
+
+ .gap-sm-0 {
+ gap: 0 !important;
+ }
+
+ .gap-sm-1 {
+ gap: 0.25rem !important;
+ }
+
+ .gap-sm-2 {
+ gap: 0.5rem !important;
+ }
+
+ .gap-sm-3 {
+ gap: 1rem !important;
+ }
+
+ .gap-sm-4 {
+ gap: 1.5rem !important;
+ }
+
+ .gap-sm-5 {
+ gap: 3rem !important;
+ }
+
+ .justify-content-sm-start {
+ justify-content: flex-start !important;
+ }
+
+ .justify-content-sm-end {
+ justify-content: flex-end !important;
+ }
+
+ .justify-content-sm-center {
+ justify-content: center !important;
+ }
+
+ .justify-content-sm-between {
+ justify-content: space-between !important;
+ }
+
+ .justify-content-sm-around {
+ justify-content: space-around !important;
+ }
+
+ .justify-content-sm-evenly {
+ justify-content: space-evenly !important;
+ }
+
+ .align-items-sm-start {
+ align-items: flex-start !important;
+ }
+
+ .align-items-sm-end {
+ align-items: flex-end !important;
+ }
+
+ .align-items-sm-center {
+ align-items: center !important;
+ }
+
+ .align-items-sm-baseline {
+ align-items: baseline !important;
+ }
+
+ .align-items-sm-stretch {
+ align-items: stretch !important;
+ }
+
+ .align-content-sm-start {
+ align-content: flex-start !important;
+ }
+
+ .align-content-sm-end {
+ align-content: flex-end !important;
+ }
+
+ .align-content-sm-center {
+ align-content: center !important;
+ }
+
+ .align-content-sm-between {
+ align-content: space-between !important;
+ }
+
+ .align-content-sm-around {
+ align-content: space-around !important;
+ }
+
+ .align-content-sm-stretch {
+ align-content: stretch !important;
+ }
+
+ .align-self-sm-auto {
+ align-self: auto !important;
+ }
+
+ .align-self-sm-start {
+ align-self: flex-start !important;
+ }
+
+ .align-self-sm-end {
+ align-self: flex-end !important;
+ }
+
+ .align-self-sm-center {
+ align-self: center !important;
+ }
+
+ .align-self-sm-baseline {
+ align-self: baseline !important;
+ }
+
+ .align-self-sm-stretch {
+ align-self: stretch !important;
+ }
+
+ .order-sm-first {
+ order: -1 !important;
+ }
+
+ .order-sm-0 {
+ order: 0 !important;
+ }
+
+ .order-sm-1 {
+ order: 1 !important;
+ }
+
+ .order-sm-2 {
+ order: 2 !important;
+ }
+
+ .order-sm-3 {
+ order: 3 !important;
+ }
+
+ .order-sm-4 {
+ order: 4 !important;
+ }
+
+ .order-sm-5 {
+ order: 5 !important;
+ }
+
+ .order-sm-last {
+ order: 6 !important;
+ }
+
+ .m-sm-0 {
+ margin: 0 !important;
+ }
+
+ .m-sm-1 {
+ margin: 0.25rem !important;
+ }
+
+ .m-sm-2 {
+ margin: 0.5rem !important;
+ }
+
+ .m-sm-3 {
+ margin: 1rem !important;
+ }
+
+ .m-sm-4 {
+ margin: 1.5rem !important;
+ }
+
+ .m-sm-5 {
+ margin: 3rem !important;
+ }
+
+ .m-sm-auto {
+ margin: auto !important;
+ }
+
+ .mx-sm-0 {
+ margin-right: 0 !important;
+ margin-left: 0 !important;
+ }
+
+ .mx-sm-1 {
+ margin-right: 0.25rem !important;
+ margin-left: 0.25rem !important;
+ }
+
+ .mx-sm-2 {
+ margin-right: 0.5rem !important;
+ margin-left: 0.5rem !important;
+ }
+
+ .mx-sm-3 {
+ margin-right: 1rem !important;
+ margin-left: 1rem !important;
+ }
+
+ .mx-sm-4 {
+ margin-right: 1.5rem !important;
+ margin-left: 1.5rem !important;
+ }
+
+ .mx-sm-5 {
+ margin-right: 3rem !important;
+ margin-left: 3rem !important;
+ }
+
+ .mx-sm-auto {
+ margin-right: auto !important;
+ margin-left: auto !important;
+ }
+
+ .my-sm-0 {
+ margin-top: 0 !important;
+ margin-bottom: 0 !important;
+ }
+
+ .my-sm-1 {
+ margin-top: 0.25rem !important;
+ margin-bottom: 0.25rem !important;
+ }
+
+ .my-sm-2 {
+ margin-top: 0.5rem !important;
+ margin-bottom: 0.5rem !important;
+ }
+
+ .my-sm-3 {
+ margin-top: 1rem !important;
+ margin-bottom: 1rem !important;
+ }
+
+ .my-sm-4 {
+ margin-top: 1.5rem !important;
+ margin-bottom: 1.5rem !important;
+ }
+
+ .my-sm-5 {
+ margin-top: 3rem !important;
+ margin-bottom: 3rem !important;
+ }
+
+ .my-sm-auto {
+ margin-top: auto !important;
+ margin-bottom: auto !important;
+ }
+
+ .mt-sm-0 {
+ margin-top: 0 !important;
+ }
+
+ .mt-sm-1 {
+ margin-top: 0.25rem !important;
+ }
+
+ .mt-sm-2 {
+ margin-top: 0.5rem !important;
+ }
+
+ .mt-sm-3 {
+ margin-top: 1rem !important;
+ }
+
+ .mt-sm-4 {
+ margin-top: 1.5rem !important;
+ }
+
+ .mt-sm-5 {
+ margin-top: 3rem !important;
+ }
+
+ .mt-sm-auto {
+ margin-top: auto !important;
+ }
+
+ .me-sm-0 {
+ margin-right: 0 !important;
+ }
+
+ .me-sm-1 {
+ margin-right: 0.25rem !important;
+ }
+
+ .me-sm-2 {
+ margin-right: 0.5rem !important;
+ }
+
+ .me-sm-3 {
+ margin-right: 1rem !important;
+ }
+
+ .me-sm-4 {
+ margin-right: 1.5rem !important;
+ }
+
+ .me-sm-5 {
+ margin-right: 3rem !important;
+ }
+
+ .me-sm-auto {
+ margin-right: auto !important;
+ }
+
+ .mb-sm-0 {
+ margin-bottom: 0 !important;
+ }
+
+ .mb-sm-1 {
+ margin-bottom: 0.25rem !important;
+ }
+
+ .mb-sm-2 {
+ margin-bottom: 0.5rem !important;
+ }
+
+ .mb-sm-3 {
+ margin-bottom: 1rem !important;
+ }
+
+ .mb-sm-4 {
+ margin-bottom: 1.5rem !important;
+ }
+
+ .mb-sm-5 {
+ margin-bottom: 3rem !important;
+ }
+
+ .mb-sm-auto {
+ margin-bottom: auto !important;
+ }
+
+ .ms-sm-0 {
+ margin-left: 0 !important;
+ }
+
+ .ms-sm-1 {
+ margin-left: 0.25rem !important;
+ }
+
+ .ms-sm-2 {
+ margin-left: 0.5rem !important;
+ }
+
+ .ms-sm-3 {
+ margin-left: 1rem !important;
+ }
+
+ .ms-sm-4 {
+ margin-left: 1.5rem !important;
+ }
+
+ .ms-sm-5 {
+ margin-left: 3rem !important;
+ }
+
+ .ms-sm-auto {
+ margin-left: auto !important;
+ }
+
+ .p-sm-0 {
+ padding: 0 !important;
+ }
+
+ .p-sm-1 {
+ padding: 0.25rem !important;
+ }
+
+ .p-sm-2 {
+ padding: 0.5rem !important;
+ }
+
+ .p-sm-3 {
+ padding: 1rem !important;
+ }
+
+ .p-sm-4 {
+ padding: 1.5rem !important;
+ }
+
+ .p-sm-5 {
+ padding: 3rem !important;
+ }
+
+ .px-sm-0 {
+ padding-right: 0 !important;
+ padding-left: 0 !important;
+ }
+
+ .px-sm-1 {
+ padding-right: 0.25rem !important;
+ padding-left: 0.25rem !important;
+ }
+
+ .px-sm-2 {
+ padding-right: 0.5rem !important;
+ padding-left: 0.5rem !important;
+ }
+
+ .px-sm-3 {
+ padding-right: 1rem !important;
+ padding-left: 1rem !important;
+ }
+
+ .px-sm-4 {
+ padding-right: 1.5rem !important;
+ padding-left: 1.5rem !important;
+ }
+
+ .px-sm-5 {
+ padding-right: 3rem !important;
+ padding-left: 3rem !important;
+ }
+
+ .py-sm-0 {
+ padding-top: 0 !important;
+ padding-bottom: 0 !important;
+ }
+
+ .py-sm-1 {
+ padding-top: 0.25rem !important;
+ padding-bottom: 0.25rem !important;
+ }
+
+ .py-sm-2 {
+ padding-top: 0.5rem !important;
+ padding-bottom: 0.5rem !important;
+ }
+
+ .py-sm-3 {
+ padding-top: 1rem !important;
+ padding-bottom: 1rem !important;
+ }
+
+ .py-sm-4 {
+ padding-top: 1.5rem !important;
+ padding-bottom: 1.5rem !important;
+ }
+
+ .py-sm-5 {
+ padding-top: 3rem !important;
+ padding-bottom: 3rem !important;
+ }
+
+ .pt-sm-0 {
+ padding-top: 0 !important;
+ }
+
+ .pt-sm-1 {
+ padding-top: 0.25rem !important;
+ }
+
+ .pt-sm-2 {
+ padding-top: 0.5rem !important;
+ }
+
+ .pt-sm-3 {
+ padding-top: 1rem !important;
+ }
+
+ .pt-sm-4 {
+ padding-top: 1.5rem !important;
+ }
+
+ .pt-sm-5 {
+ padding-top: 3rem !important;
+ }
+
+ .pe-sm-0 {
+ padding-right: 0 !important;
+ }
+
+ .pe-sm-1 {
+ padding-right: 0.25rem !important;
+ }
+
+ .pe-sm-2 {
+ padding-right: 0.5rem !important;
+ }
+
+ .pe-sm-3 {
+ padding-right: 1rem !important;
+ }
+
+ .pe-sm-4 {
+ padding-right: 1.5rem !important;
+ }
+
+ .pe-sm-5 {
+ padding-right: 3rem !important;
+ }
+
+ .pb-sm-0 {
+ padding-bottom: 0 !important;
+ }
+
+ .pb-sm-1 {
+ padding-bottom: 0.25rem !important;
+ }
+
+ .pb-sm-2 {
+ padding-bottom: 0.5rem !important;
+ }
+
+ .pb-sm-3 {
+ padding-bottom: 1rem !important;
+ }
+
+ .pb-sm-4 {
+ padding-bottom: 1.5rem !important;
+ }
+
+ .pb-sm-5 {
+ padding-bottom: 3rem !important;
+ }
+
+ .ps-sm-0 {
+ padding-left: 0 !important;
+ }
+
+ .ps-sm-1 {
+ padding-left: 0.25rem !important;
+ }
+
+ .ps-sm-2 {
+ padding-left: 0.5rem !important;
+ }
+
+ .ps-sm-3 {
+ padding-left: 1rem !important;
+ }
+
+ .ps-sm-4 {
+ padding-left: 1.5rem !important;
+ }
+
+ .ps-sm-5 {
+ padding-left: 3rem !important;
+ }
+
+ .text-sm-start {
+ text-align: left !important;
+ }
+
+ .text-sm-end {
+ text-align: right !important;
+ }
+
+ .text-sm-center {
+ text-align: center !important;
+ }
+}
+
+@media (min-width: 768px) {
+ .float-md-start {
+ float: left !important;
+ }
+
+ .float-md-end {
+ float: right !important;
+ }
+
+ .float-md-none {
+ float: none !important;
+ }
+
+ .d-md-inline {
+ display: inline !important;
+ }
+
+ .d-md-inline-block {
+ display: inline-block !important;
+ }
+
+ .d-md-block {
+ display: block !important;
+ }
+
+ .d-md-grid {
+ display: grid !important;
+ }
+
+ .d-md-table {
+ display: table !important;
+ }
+
+ .d-md-table-row {
+ display: table-row !important;
+ }
+
+ .d-md-table-cell {
+ display: table-cell !important;
+ }
+
+ .d-md-flex {
+ display: flex !important;
+ }
+
+ .d-md-inline-flex {
+ display: inline-flex !important;
+ }
+
+ .d-md-none {
+ display: none !important;
+ }
+
+ .flex-md-fill {
+ flex: 1 1 auto !important;
+ }
+
+ .flex-md-row {
+ flex-direction: row !important;
+ }
+
+ .flex-md-column {
+ flex-direction: column !important;
+ }
+
+ .flex-md-row-reverse {
+ flex-direction: row-reverse !important;
+ }
+
+ .flex-md-column-reverse {
+ flex-direction: column-reverse !important;
+ }
+
+ .flex-md-grow-0 {
+ flex-grow: 0 !important;
+ }
+
+ .flex-md-grow-1 {
+ flex-grow: 1 !important;
+ }
+
+ .flex-md-shrink-0 {
+ flex-shrink: 0 !important;
+ }
+
+ .flex-md-shrink-1 {
+ flex-shrink: 1 !important;
+ }
+
+ .flex-md-wrap {
+ flex-wrap: wrap !important;
+ }
+
+ .flex-md-nowrap {
+ flex-wrap: nowrap !important;
+ }
+
+ .flex-md-wrap-reverse {
+ flex-wrap: wrap-reverse !important;
+ }
+
+ .gap-md-0 {
+ gap: 0 !important;
+ }
+
+ .gap-md-1 {
+ gap: 0.25rem !important;
+ }
+
+ .gap-md-2 {
+ gap: 0.5rem !important;
+ }
+
+ .gap-md-3 {
+ gap: 1rem !important;
+ }
+
+ .gap-md-4 {
+ gap: 1.5rem !important;
+ }
+
+ .gap-md-5 {
+ gap: 3rem !important;
+ }
+
+ .justify-content-md-start {
+ justify-content: flex-start !important;
+ }
+
+ .justify-content-md-end {
+ justify-content: flex-end !important;
+ }
+
+ .justify-content-md-center {
+ justify-content: center !important;
+ }
+
+ .justify-content-md-between {
+ justify-content: space-between !important;
+ }
+
+ .justify-content-md-around {
+ justify-content: space-around !important;
+ }
+
+ .justify-content-md-evenly {
+ justify-content: space-evenly !important;
+ }
+
+ .align-items-md-start {
+ align-items: flex-start !important;
+ }
+
+ .align-items-md-end {
+ align-items: flex-end !important;
+ }
+
+ .align-items-md-center {
+ align-items: center !important;
+ }
+
+ .align-items-md-baseline {
+ align-items: baseline !important;
+ }
+
+ .align-items-md-stretch {
+ align-items: stretch !important;
+ }
+
+ .align-content-md-start {
+ align-content: flex-start !important;
+ }
+
+ .align-content-md-end {
+ align-content: flex-end !important;
+ }
+
+ .align-content-md-center {
+ align-content: center !important;
+ }
+
+ .align-content-md-between {
+ align-content: space-between !important;
+ }
+
+ .align-content-md-around {
+ align-content: space-around !important;
+ }
+
+ .align-content-md-stretch {
+ align-content: stretch !important;
+ }
+
+ .align-self-md-auto {
+ align-self: auto !important;
+ }
+
+ .align-self-md-start {
+ align-self: flex-start !important;
+ }
+
+ .align-self-md-end {
+ align-self: flex-end !important;
+ }
+
+ .align-self-md-center {
+ align-self: center !important;
+ }
+
+ .align-self-md-baseline {
+ align-self: baseline !important;
+ }
+
+ .align-self-md-stretch {
+ align-self: stretch !important;
+ }
+
+ .order-md-first {
+ order: -1 !important;
+ }
+
+ .order-md-0 {
+ order: 0 !important;
+ }
+
+ .order-md-1 {
+ order: 1 !important;
+ }
+
+ .order-md-2 {
+ order: 2 !important;
+ }
+
+ .order-md-3 {
+ order: 3 !important;
+ }
+
+ .order-md-4 {
+ order: 4 !important;
+ }
+
+ .order-md-5 {
+ order: 5 !important;
+ }
+
+ .order-md-last {
+ order: 6 !important;
+ }
+
+ .m-md-0 {
+ margin: 0 !important;
+ }
+
+ .m-md-1 {
+ margin: 0.25rem !important;
+ }
+
+ .m-md-2 {
+ margin: 0.5rem !important;
+ }
+
+ .m-md-3 {
+ margin: 1rem !important;
+ }
+
+ .m-md-4 {
+ margin: 1.5rem !important;
+ }
+
+ .m-md-5 {
+ margin: 3rem !important;
+ }
+
+ .m-md-auto {
+ margin: auto !important;
+ }
+
+ .mx-md-0 {
+ margin-right: 0 !important;
+ margin-left: 0 !important;
+ }
+
+ .mx-md-1 {
+ margin-right: 0.25rem !important;
+ margin-left: 0.25rem !important;
+ }
+
+ .mx-md-2 {
+ margin-right: 0.5rem !important;
+ margin-left: 0.5rem !important;
+ }
+
+ .mx-md-3 {
+ margin-right: 1rem !important;
+ margin-left: 1rem !important;
+ }
+
+ .mx-md-4 {
+ margin-right: 1.5rem !important;
+ margin-left: 1.5rem !important;
+ }
+
+ .mx-md-5 {
+ margin-right: 3rem !important;
+ margin-left: 3rem !important;
+ }
+
+ .mx-md-auto {
+ margin-right: auto !important;
+ margin-left: auto !important;
+ }
+
+ .my-md-0 {
+ margin-top: 0 !important;
+ margin-bottom: 0 !important;
+ }
+
+ .my-md-1 {
+ margin-top: 0.25rem !important;
+ margin-bottom: 0.25rem !important;
+ }
+
+ .my-md-2 {
+ margin-top: 0.5rem !important;
+ margin-bottom: 0.5rem !important;
+ }
+
+ .my-md-3 {
+ margin-top: 1rem !important;
+ margin-bottom: 1rem !important;
+ }
+
+ .my-md-4 {
+ margin-top: 1.5rem !important;
+ margin-bottom: 1.5rem !important;
+ }
+
+ .my-md-5 {
+ margin-top: 3rem !important;
+ margin-bottom: 3rem !important;
+ }
+
+ .my-md-auto {
+ margin-top: auto !important;
+ margin-bottom: auto !important;
+ }
+
+ .mt-md-0 {
+ margin-top: 0 !important;
+ }
+
+ .mt-md-1 {
+ margin-top: 0.25rem !important;
+ }
+
+ .mt-md-2 {
+ margin-top: 0.5rem !important;
+ }
+
+ .mt-md-3 {
+ margin-top: 1rem !important;
+ }
+
+ .mt-md-4 {
+ margin-top: 1.5rem !important;
+ }
+
+ .mt-md-5 {
+ margin-top: 3rem !important;
+ }
+
+ .mt-md-auto {
+ margin-top: auto !important;
+ }
+
+ .me-md-0 {
+ margin-right: 0 !important;
+ }
+
+ .me-md-1 {
+ margin-right: 0.25rem !important;
+ }
+
+ .me-md-2 {
+ margin-right: 0.5rem !important;
+ }
+
+ .me-md-3 {
+ margin-right: 1rem !important;
+ }
+
+ .me-md-4 {
+ margin-right: 1.5rem !important;
+ }
+
+ .me-md-5 {
+ margin-right: 3rem !important;
+ }
+
+ .me-md-auto {
+ margin-right: auto !important;
+ }
+
+ .mb-md-0 {
+ margin-bottom: 0 !important;
+ }
+
+ .mb-md-1 {
+ margin-bottom: 0.25rem !important;
+ }
+
+ .mb-md-2 {
+ margin-bottom: 0.5rem !important;
+ }
+
+ .mb-md-3 {
+ margin-bottom: 1rem !important;
+ }
+
+ .mb-md-4 {
+ margin-bottom: 1.5rem !important;
+ }
+
+ .mb-md-5 {
+ margin-bottom: 3rem !important;
+ }
+
+ .mb-md-auto {
+ margin-bottom: auto !important;
+ }
+
+ .ms-md-0 {
+ margin-left: 0 !important;
+ }
+
+ .ms-md-1 {
+ margin-left: 0.25rem !important;
+ }
+
+ .ms-md-2 {
+ margin-left: 0.5rem !important;
+ }
+
+ .ms-md-3 {
+ margin-left: 1rem !important;
+ }
+
+ .ms-md-4 {
+ margin-left: 1.5rem !important;
+ }
+
+ .ms-md-5 {
+ margin-left: 3rem !important;
+ }
+
+ .ms-md-auto {
+ margin-left: auto !important;
+ }
+
+ .p-md-0 {
+ padding: 0 !important;
+ }
+
+ .p-md-1 {
+ padding: 0.25rem !important;
+ }
+
+ .p-md-2 {
+ padding: 0.5rem !important;
+ }
+
+ .p-md-3 {
+ padding: 1rem !important;
+ }
+
+ .p-md-4 {
+ padding: 1.5rem !important;
+ }
+
+ .p-md-5 {
+ padding: 3rem !important;
+ }
+
+ .px-md-0 {
+ padding-right: 0 !important;
+ padding-left: 0 !important;
+ }
+
+ .px-md-1 {
+ padding-right: 0.25rem !important;
+ padding-left: 0.25rem !important;
+ }
+
+ .px-md-2 {
+ padding-right: 0.5rem !important;
+ padding-left: 0.5rem !important;
+ }
+
+ .px-md-3 {
+ padding-right: 1rem !important;
+ padding-left: 1rem !important;
+ }
+
+ .px-md-4 {
+ padding-right: 1.5rem !important;
+ padding-left: 1.5rem !important;
+ }
+
+ .px-md-5 {
+ padding-right: 3rem !important;
+ padding-left: 3rem !important;
+ }
+
+ .py-md-0 {
+ padding-top: 0 !important;
+ padding-bottom: 0 !important;
+ }
+
+ .py-md-1 {
+ padding-top: 0.25rem !important;
+ padding-bottom: 0.25rem !important;
+ }
+
+ .py-md-2 {
+ padding-top: 0.5rem !important;
+ padding-bottom: 0.5rem !important;
+ }
+
+ .py-md-3 {
+ padding-top: 1rem !important;
+ padding-bottom: 1rem !important;
+ }
+
+ .py-md-4 {
+ padding-top: 1.5rem !important;
+ padding-bottom: 1.5rem !important;
+ }
+
+ .py-md-5 {
+ padding-top: 3rem !important;
+ padding-bottom: 3rem !important;
+ }
+
+ .pt-md-0 {
+ padding-top: 0 !important;
+ }
+
+ .pt-md-1 {
+ padding-top: 0.25rem !important;
+ }
+
+ .pt-md-2 {
+ padding-top: 0.5rem !important;
+ }
+
+ .pt-md-3 {
+ padding-top: 1rem !important;
+ }
+
+ .pt-md-4 {
+ padding-top: 1.5rem !important;
+ }
+
+ .pt-md-5 {
+ padding-top: 3rem !important;
+ }
+
+ .pe-md-0 {
+ padding-right: 0 !important;
+ }
+
+ .pe-md-1 {
+ padding-right: 0.25rem !important;
+ }
+
+ .pe-md-2 {
+ padding-right: 0.5rem !important;
+ }
+
+ .pe-md-3 {
+ padding-right: 1rem !important;
+ }
+
+ .pe-md-4 {
+ padding-right: 1.5rem !important;
+ }
+
+ .pe-md-5 {
+ padding-right: 3rem !important;
+ }
+
+ .pb-md-0 {
+ padding-bottom: 0 !important;
+ }
+
+ .pb-md-1 {
+ padding-bottom: 0.25rem !important;
+ }
+
+ .pb-md-2 {
+ padding-bottom: 0.5rem !important;
+ }
+
+ .pb-md-3 {
+ padding-bottom: 1rem !important;
+ }
+
+ .pb-md-4 {
+ padding-bottom: 1.5rem !important;
+ }
+
+ .pb-md-5 {
+ padding-bottom: 3rem !important;
+ }
+
+ .ps-md-0 {
+ padding-left: 0 !important;
+ }
+
+ .ps-md-1 {
+ padding-left: 0.25rem !important;
+ }
+
+ .ps-md-2 {
+ padding-left: 0.5rem !important;
+ }
+
+ .ps-md-3 {
+ padding-left: 1rem !important;
+ }
+
+ .ps-md-4 {
+ padding-left: 1.5rem !important;
+ }
+
+ .ps-md-5 {
+ padding-left: 3rem !important;
+ }
+
+ .text-md-start {
+ text-align: left !important;
+ }
+
+ .text-md-end {
+ text-align: right !important;
+ }
+
+ .text-md-center {
+ text-align: center !important;
+ }
+}
+
+@media (min-width: 992px) {
+ .float-lg-start {
+ float: left !important;
+ }
+
+ .float-lg-end {
+ float: right !important;
+ }
+
+ .float-lg-none {
+ float: none !important;
+ }
+
+ .d-lg-inline {
+ display: inline !important;
+ }
+
+ .d-lg-inline-block {
+ display: inline-block !important;
+ }
+
+ .d-lg-block {
+ display: block !important;
+ }
+
+ .d-lg-grid {
+ display: grid !important;
+ }
+
+ .d-lg-table {
+ display: table !important;
+ }
+
+ .d-lg-table-row {
+ display: table-row !important;
+ }
+
+ .d-lg-table-cell {
+ display: table-cell !important;
+ }
+
+ .d-lg-flex {
+ display: flex !important;
+ }
+
+ .d-lg-inline-flex {
+ display: inline-flex !important;
+ }
+
+ .d-lg-none {
+ display: none !important;
+ }
+
+ .flex-lg-fill {
+ flex: 1 1 auto !important;
+ }
+
+ .flex-lg-row {
+ flex-direction: row !important;
+ }
+
+ .flex-lg-column {
+ flex-direction: column !important;
+ }
+
+ .flex-lg-row-reverse {
+ flex-direction: row-reverse !important;
+ }
+
+ .flex-lg-column-reverse {
+ flex-direction: column-reverse !important;
+ }
+
+ .flex-lg-grow-0 {
+ flex-grow: 0 !important;
+ }
+
+ .flex-lg-grow-1 {
+ flex-grow: 1 !important;
+ }
+
+ .flex-lg-shrink-0 {
+ flex-shrink: 0 !important;
+ }
+
+ .flex-lg-shrink-1 {
+ flex-shrink: 1 !important;
+ }
+
+ .flex-lg-wrap {
+ flex-wrap: wrap !important;
+ }
+
+ .flex-lg-nowrap {
+ flex-wrap: nowrap !important;
+ }
+
+ .flex-lg-wrap-reverse {
+ flex-wrap: wrap-reverse !important;
+ }
+
+ .gap-lg-0 {
+ gap: 0 !important;
+ }
+
+ .gap-lg-1 {
+ gap: 0.25rem !important;
+ }
+
+ .gap-lg-2 {
+ gap: 0.5rem !important;
+ }
+
+ .gap-lg-3 {
+ gap: 1rem !important;
+ }
+
+ .gap-lg-4 {
+ gap: 1.5rem !important;
+ }
+
+ .gap-lg-5 {
+ gap: 3rem !important;
+ }
+
+ .justify-content-lg-start {
+ justify-content: flex-start !important;
+ }
+
+ .justify-content-lg-end {
+ justify-content: flex-end !important;
+ }
+
+ .justify-content-lg-center {
+ justify-content: center !important;
+ }
+
+ .justify-content-lg-between {
+ justify-content: space-between !important;
+ }
+
+ .justify-content-lg-around {
+ justify-content: space-around !important;
+ }
+
+ .justify-content-lg-evenly {
+ justify-content: space-evenly !important;
+ }
+
+ .align-items-lg-start {
+ align-items: flex-start !important;
+ }
+
+ .align-items-lg-end {
+ align-items: flex-end !important;
+ }
+
+ .align-items-lg-center {
+ align-items: center !important;
+ }
+
+ .align-items-lg-baseline {
+ align-items: baseline !important;
+ }
+
+ .align-items-lg-stretch {
+ align-items: stretch !important;
+ }
+
+ .align-content-lg-start {
+ align-content: flex-start !important;
+ }
+
+ .align-content-lg-end {
+ align-content: flex-end !important;
+ }
+
+ .align-content-lg-center {
+ align-content: center !important;
+ }
+
+ .align-content-lg-between {
+ align-content: space-between !important;
+ }
+
+ .align-content-lg-around {
+ align-content: space-around !important;
+ }
+
+ .align-content-lg-stretch {
+ align-content: stretch !important;
+ }
+
+ .align-self-lg-auto {
+ align-self: auto !important;
+ }
+
+ .align-self-lg-start {
+ align-self: flex-start !important;
+ }
+
+ .align-self-lg-end {
+ align-self: flex-end !important;
+ }
+
+ .align-self-lg-center {
+ align-self: center !important;
+ }
+
+ .align-self-lg-baseline {
+ align-self: baseline !important;
+ }
+
+ .align-self-lg-stretch {
+ align-self: stretch !important;
+ }
+
+ .order-lg-first {
+ order: -1 !important;
+ }
+
+ .order-lg-0 {
+ order: 0 !important;
+ }
+
+ .order-lg-1 {
+ order: 1 !important;
+ }
+
+ .order-lg-2 {
+ order: 2 !important;
+ }
+
+ .order-lg-3 {
+ order: 3 !important;
+ }
+
+ .order-lg-4 {
+ order: 4 !important;
+ }
+
+ .order-lg-5 {
+ order: 5 !important;
+ }
+
+ .order-lg-last {
+ order: 6 !important;
+ }
+
+ .m-lg-0 {
+ margin: 0 !important;
+ }
+
+ .m-lg-1 {
+ margin: 0.25rem !important;
+ }
+
+ .m-lg-2 {
+ margin: 0.5rem !important;
+ }
+
+ .m-lg-3 {
+ margin: 1rem !important;
+ }
+
+ .m-lg-4 {
+ margin: 1.5rem !important;
+ }
+
+ .m-lg-5 {
+ margin: 3rem !important;
+ }
+
+ .m-lg-auto {
+ margin: auto !important;
+ }
+
+ .mx-lg-0 {
+ margin-right: 0 !important;
+ margin-left: 0 !important;
+ }
+
+ .mx-lg-1 {
+ margin-right: 0.25rem !important;
+ margin-left: 0.25rem !important;
+ }
+
+ .mx-lg-2 {
+ margin-right: 0.5rem !important;
+ margin-left: 0.5rem !important;
+ }
+
+ .mx-lg-3 {
+ margin-right: 1rem !important;
+ margin-left: 1rem !important;
+ }
+
+ .mx-lg-4 {
+ margin-right: 1.5rem !important;
+ margin-left: 1.5rem !important;
+ }
+
+ .mx-lg-5 {
+ margin-right: 3rem !important;
+ margin-left: 3rem !important;
+ }
+
+ .mx-lg-auto {
+ margin-right: auto !important;
+ margin-left: auto !important;
+ }
+
+ .my-lg-0 {
+ margin-top: 0 !important;
+ margin-bottom: 0 !important;
+ }
+
+ .my-lg-1 {
+ margin-top: 0.25rem !important;
+ margin-bottom: 0.25rem !important;
+ }
+
+ .my-lg-2 {
+ margin-top: 0.5rem !important;
+ margin-bottom: 0.5rem !important;
+ }
+
+ .my-lg-3 {
+ margin-top: 1rem !important;
+ margin-bottom: 1rem !important;
+ }
+
+ .my-lg-4 {
+ margin-top: 1.5rem !important;
+ margin-bottom: 1.5rem !important;
+ }
+
+ .my-lg-5 {
+ margin-top: 3rem !important;
+ margin-bottom: 3rem !important;
+ }
+
+ .my-lg-auto {
+ margin-top: auto !important;
+ margin-bottom: auto !important;
+ }
+
+ .mt-lg-0 {
+ margin-top: 0 !important;
+ }
+
+ .mt-lg-1 {
+ margin-top: 0.25rem !important;
+ }
+
+ .mt-lg-2 {
+ margin-top: 0.5rem !important;
+ }
+
+ .mt-lg-3 {
+ margin-top: 1rem !important;
+ }
+
+ .mt-lg-4 {
+ margin-top: 1.5rem !important;
+ }
+
+ .mt-lg-5 {
+ margin-top: 3rem !important;
+ }
+
+ .mt-lg-auto {
+ margin-top: auto !important;
+ }
+
+ .me-lg-0 {
+ margin-right: 0 !important;
+ }
+
+ .me-lg-1 {
+ margin-right: 0.25rem !important;
+ }
+
+ .me-lg-2 {
+ margin-right: 0.5rem !important;
+ }
+
+ .me-lg-3 {
+ margin-right: 1rem !important;
+ }
+
+ .me-lg-4 {
+ margin-right: 1.5rem !important;
+ }
+
+ .me-lg-5 {
+ margin-right: 3rem !important;
+ }
+
+ .me-lg-auto {
+ margin-right: auto !important;
+ }
+
+ .mb-lg-0 {
+ margin-bottom: 0 !important;
+ }
+
+ .mb-lg-1 {
+ margin-bottom: 0.25rem !important;
+ }
+
+ .mb-lg-2 {
+ margin-bottom: 0.5rem !important;
+ }
+
+ .mb-lg-3 {
+ margin-bottom: 1rem !important;
+ }
+
+ .mb-lg-4 {
+ margin-bottom: 1.5rem !important;
+ }
+
+ .mb-lg-5 {
+ margin-bottom: 3rem !important;
+ }
+
+ .mb-lg-auto {
+ margin-bottom: auto !important;
+ }
+
+ .ms-lg-0 {
+ margin-left: 0 !important;
+ }
+
+ .ms-lg-1 {
+ margin-left: 0.25rem !important;
+ }
+
+ .ms-lg-2 {
+ margin-left: 0.5rem !important;
+ }
+
+ .ms-lg-3 {
+ margin-left: 1rem !important;
+ }
+
+ .ms-lg-4 {
+ margin-left: 1.5rem !important;
+ }
+
+ .ms-lg-5 {
+ margin-left: 3rem !important;
+ }
+
+ .ms-lg-auto {
+ margin-left: auto !important;
+ }
+
+ .p-lg-0 {
+ padding: 0 !important;
+ }
+
+ .p-lg-1 {
+ padding: 0.25rem !important;
+ }
+
+ .p-lg-2 {
+ padding: 0.5rem !important;
+ }
+
+ .p-lg-3 {
+ padding: 1rem !important;
+ }
+
+ .p-lg-4 {
+ padding: 1.5rem !important;
+ }
+
+ .p-lg-5 {
+ padding: 3rem !important;
+ }
+
+ .px-lg-0 {
+ padding-right: 0 !important;
+ padding-left: 0 !important;
+ }
+
+ .px-lg-1 {
+ padding-right: 0.25rem !important;
+ padding-left: 0.25rem !important;
+ }
+
+ .px-lg-2 {
+ padding-right: 0.5rem !important;
+ padding-left: 0.5rem !important;
+ }
+
+ .px-lg-3 {
+ padding-right: 1rem !important;
+ padding-left: 1rem !important;
+ }
+
+ .px-lg-4 {
+ padding-right: 1.5rem !important;
+ padding-left: 1.5rem !important;
+ }
+
+ .px-lg-5 {
+ padding-right: 3rem !important;
+ padding-left: 3rem !important;
+ }
+
+ .py-lg-0 {
+ padding-top: 0 !important;
+ padding-bottom: 0 !important;
+ }
+
+ .py-lg-1 {
+ padding-top: 0.25rem !important;
+ padding-bottom: 0.25rem !important;
+ }
+
+ .py-lg-2 {
+ padding-top: 0.5rem !important;
+ padding-bottom: 0.5rem !important;
+ }
+
+ .py-lg-3 {
+ padding-top: 1rem !important;
+ padding-bottom: 1rem !important;
+ }
+
+ .py-lg-4 {
+ padding-top: 1.5rem !important;
+ padding-bottom: 1.5rem !important;
+ }
+
+ .py-lg-5 {
+ padding-top: 3rem !important;
+ padding-bottom: 3rem !important;
+ }
+
+ .pt-lg-0 {
+ padding-top: 0 !important;
+ }
+
+ .pt-lg-1 {
+ padding-top: 0.25rem !important;
+ }
+
+ .pt-lg-2 {
+ padding-top: 0.5rem !important;
+ }
+
+ .pt-lg-3 {
+ padding-top: 1rem !important;
+ }
+
+ .pt-lg-4 {
+ padding-top: 1.5rem !important;
+ }
+
+ .pt-lg-5 {
+ padding-top: 3rem !important;
+ }
+
+ .pe-lg-0 {
+ padding-right: 0 !important;
+ }
+
+ .pe-lg-1 {
+ padding-right: 0.25rem !important;
+ }
+
+ .pe-lg-2 {
+ padding-right: 0.5rem !important;
+ }
+
+ .pe-lg-3 {
+ padding-right: 1rem !important;
+ }
+
+ .pe-lg-4 {
+ padding-right: 1.5rem !important;
+ }
+
+ .pe-lg-5 {
+ padding-right: 3rem !important;
+ }
+
+ .pb-lg-0 {
+ padding-bottom: 0 !important;
+ }
+
+ .pb-lg-1 {
+ padding-bottom: 0.25rem !important;
+ }
+
+ .pb-lg-2 {
+ padding-bottom: 0.5rem !important;
+ }
+
+ .pb-lg-3 {
+ padding-bottom: 1rem !important;
+ }
+
+ .pb-lg-4 {
+ padding-bottom: 1.5rem !important;
+ }
+
+ .pb-lg-5 {
+ padding-bottom: 3rem !important;
+ }
+
+ .ps-lg-0 {
+ padding-left: 0 !important;
+ }
+
+ .ps-lg-1 {
+ padding-left: 0.25rem !important;
+ }
+
+ .ps-lg-2 {
+ padding-left: 0.5rem !important;
+ }
+
+ .ps-lg-3 {
+ padding-left: 1rem !important;
+ }
+
+ .ps-lg-4 {
+ padding-left: 1.5rem !important;
+ }
+
+ .ps-lg-5 {
+ padding-left: 3rem !important;
+ }
+
+ .text-lg-start {
+ text-align: left !important;
+ }
+
+ .text-lg-end {
+ text-align: right !important;
+ }
+
+ .text-lg-center {
+ text-align: center !important;
+ }
+}
+
+@media (min-width: 1200px) {
+ .float-xl-start {
+ float: left !important;
+ }
+
+ .float-xl-end {
+ float: right !important;
+ }
+
+ .float-xl-none {
+ float: none !important;
+ }
+
+ .d-xl-inline {
+ display: inline !important;
+ }
+
+ .d-xl-inline-block {
+ display: inline-block !important;
+ }
+
+ .d-xl-block {
+ display: block !important;
+ }
+
+ .d-xl-grid {
+ display: grid !important;
+ }
+
+ .d-xl-table {
+ display: table !important;
+ }
+
+ .d-xl-table-row {
+ display: table-row !important;
+ }
+
+ .d-xl-table-cell {
+ display: table-cell !important;
+ }
+
+ .d-xl-flex {
+ display: flex !important;
+ }
+
+ .d-xl-inline-flex {
+ display: inline-flex !important;
+ }
+
+ .d-xl-none {
+ display: none !important;
+ }
+
+ .flex-xl-fill {
+ flex: 1 1 auto !important;
+ }
+
+ .flex-xl-row {
+ flex-direction: row !important;
+ }
+
+ .flex-xl-column {
+ flex-direction: column !important;
+ }
+
+ .flex-xl-row-reverse {
+ flex-direction: row-reverse !important;
+ }
+
+ .flex-xl-column-reverse {
+ flex-direction: column-reverse !important;
+ }
+
+ .flex-xl-grow-0 {
+ flex-grow: 0 !important;
+ }
+
+ .flex-xl-grow-1 {
+ flex-grow: 1 !important;
+ }
+
+ .flex-xl-shrink-0 {
+ flex-shrink: 0 !important;
+ }
+
+ .flex-xl-shrink-1 {
+ flex-shrink: 1 !important;
+ }
+
+ .flex-xl-wrap {
+ flex-wrap: wrap !important;
+ }
+
+ .flex-xl-nowrap {
+ flex-wrap: nowrap !important;
+ }
+
+ .flex-xl-wrap-reverse {
+ flex-wrap: wrap-reverse !important;
+ }
+
+ .gap-xl-0 {
+ gap: 0 !important;
+ }
+
+ .gap-xl-1 {
+ gap: 0.25rem !important;
+ }
+
+ .gap-xl-2 {
+ gap: 0.5rem !important;
+ }
+
+ .gap-xl-3 {
+ gap: 1rem !important;
+ }
+
+ .gap-xl-4 {
+ gap: 1.5rem !important;
+ }
+
+ .gap-xl-5 {
+ gap: 3rem !important;
+ }
+
+ .justify-content-xl-start {
+ justify-content: flex-start !important;
+ }
+
+ .justify-content-xl-end {
+ justify-content: flex-end !important;
+ }
+
+ .justify-content-xl-center {
+ justify-content: center !important;
+ }
+
+ .justify-content-xl-between {
+ justify-content: space-between !important;
+ }
+
+ .justify-content-xl-around {
+ justify-content: space-around !important;
+ }
+
+ .justify-content-xl-evenly {
+ justify-content: space-evenly !important;
+ }
+
+ .align-items-xl-start {
+ align-items: flex-start !important;
+ }
+
+ .align-items-xl-end {
+ align-items: flex-end !important;
+ }
+
+ .align-items-xl-center {
+ align-items: center !important;
+ }
+
+ .align-items-xl-baseline {
+ align-items: baseline !important;
+ }
+
+ .align-items-xl-stretch {
+ align-items: stretch !important;
+ }
+
+ .align-content-xl-start {
+ align-content: flex-start !important;
+ }
+
+ .align-content-xl-end {
+ align-content: flex-end !important;
+ }
+
+ .align-content-xl-center {
+ align-content: center !important;
+ }
+
+ .align-content-xl-between {
+ align-content: space-between !important;
+ }
+
+ .align-content-xl-around {
+ align-content: space-around !important;
+ }
+
+ .align-content-xl-stretch {
+ align-content: stretch !important;
+ }
+
+ .align-self-xl-auto {
+ align-self: auto !important;
+ }
+
+ .align-self-xl-start {
+ align-self: flex-start !important;
+ }
+
+ .align-self-xl-end {
+ align-self: flex-end !important;
+ }
+
+ .align-self-xl-center {
+ align-self: center !important;
+ }
+
+ .align-self-xl-baseline {
+ align-self: baseline !important;
+ }
+
+ .align-self-xl-stretch {
+ align-self: stretch !important;
+ }
+
+ .order-xl-first {
+ order: -1 !important;
+ }
+
+ .order-xl-0 {
+ order: 0 !important;
+ }
+
+ .order-xl-1 {
+ order: 1 !important;
+ }
+
+ .order-xl-2 {
+ order: 2 !important;
+ }
+
+ .order-xl-3 {
+ order: 3 !important;
+ }
+
+ .order-xl-4 {
+ order: 4 !important;
+ }
+
+ .order-xl-5 {
+ order: 5 !important;
+ }
+
+ .order-xl-last {
+ order: 6 !important;
+ }
+
+ .m-xl-0 {
+ margin: 0 !important;
+ }
+
+ .m-xl-1 {
+ margin: 0.25rem !important;
+ }
+
+ .m-xl-2 {
+ margin: 0.5rem !important;
+ }
+
+ .m-xl-3 {
+ margin: 1rem !important;
+ }
+
+ .m-xl-4 {
+ margin: 1.5rem !important;
+ }
+
+ .m-xl-5 {
+ margin: 3rem !important;
+ }
+
+ .m-xl-auto {
+ margin: auto !important;
+ }
+
+ .mx-xl-0 {
+ margin-right: 0 !important;
+ margin-left: 0 !important;
+ }
+
+ .mx-xl-1 {
+ margin-right: 0.25rem !important;
+ margin-left: 0.25rem !important;
+ }
+
+ .mx-xl-2 {
+ margin-right: 0.5rem !important;
+ margin-left: 0.5rem !important;
+ }
+
+ .mx-xl-3 {
+ margin-right: 1rem !important;
+ margin-left: 1rem !important;
+ }
+
+ .mx-xl-4 {
+ margin-right: 1.5rem !important;
+ margin-left: 1.5rem !important;
+ }
+
+ .mx-xl-5 {
+ margin-right: 3rem !important;
+ margin-left: 3rem !important;
+ }
+
+ .mx-xl-auto {
+ margin-right: auto !important;
+ margin-left: auto !important;
+ }
+
+ .my-xl-0 {
+ margin-top: 0 !important;
+ margin-bottom: 0 !important;
+ }
+
+ .my-xl-1 {
+ margin-top: 0.25rem !important;
+ margin-bottom: 0.25rem !important;
+ }
+
+ .my-xl-2 {
+ margin-top: 0.5rem !important;
+ margin-bottom: 0.5rem !important;
+ }
+
+ .my-xl-3 {
+ margin-top: 1rem !important;
+ margin-bottom: 1rem !important;
+ }
+
+ .my-xl-4 {
+ margin-top: 1.5rem !important;
+ margin-bottom: 1.5rem !important;
+ }
+
+ .my-xl-5 {
+ margin-top: 3rem !important;
+ margin-bottom: 3rem !important;
+ }
+
+ .my-xl-auto {
+ margin-top: auto !important;
+ margin-bottom: auto !important;
+ }
+
+ .mt-xl-0 {
+ margin-top: 0 !important;
+ }
+
+ .mt-xl-1 {
+ margin-top: 0.25rem !important;
+ }
+
+ .mt-xl-2 {
+ margin-top: 0.5rem !important;
+ }
+
+ .mt-xl-3 {
+ margin-top: 1rem !important;
+ }
+
+ .mt-xl-4 {
+ margin-top: 1.5rem !important;
+ }
+
+ .mt-xl-5 {
+ margin-top: 3rem !important;
+ }
+
+ .mt-xl-auto {
+ margin-top: auto !important;
+ }
+
+ .me-xl-0 {
+ margin-right: 0 !important;
+ }
+
+ .me-xl-1 {
+ margin-right: 0.25rem !important;
+ }
+
+ .me-xl-2 {
+ margin-right: 0.5rem !important;
+ }
+
+ .me-xl-3 {
+ margin-right: 1rem !important;
+ }
+
+ .me-xl-4 {
+ margin-right: 1.5rem !important;
+ }
+
+ .me-xl-5 {
+ margin-right: 3rem !important;
+ }
+
+ .me-xl-auto {
+ margin-right: auto !important;
+ }
+
+ .mb-xl-0 {
+ margin-bottom: 0 !important;
+ }
+
+ .mb-xl-1 {
+ margin-bottom: 0.25rem !important;
+ }
+
+ .mb-xl-2 {
+ margin-bottom: 0.5rem !important;
+ }
+
+ .mb-xl-3 {
+ margin-bottom: 1rem !important;
+ }
+
+ .mb-xl-4 {
+ margin-bottom: 1.5rem !important;
+ }
+
+ .mb-xl-5 {
+ margin-bottom: 3rem !important;
+ }
+
+ .mb-xl-auto {
+ margin-bottom: auto !important;
+ }
+
+ .ms-xl-0 {
+ margin-left: 0 !important;
+ }
+
+ .ms-xl-1 {
+ margin-left: 0.25rem !important;
+ }
+
+ .ms-xl-2 {
+ margin-left: 0.5rem !important;
+ }
+
+ .ms-xl-3 {
+ margin-left: 1rem !important;
+ }
+
+ .ms-xl-4 {
+ margin-left: 1.5rem !important;
+ }
+
+ .ms-xl-5 {
+ margin-left: 3rem !important;
+ }
+
+ .ms-xl-auto {
+ margin-left: auto !important;
+ }
+
+ .p-xl-0 {
+ padding: 0 !important;
+ }
+
+ .p-xl-1 {
+ padding: 0.25rem !important;
+ }
+
+ .p-xl-2 {
+ padding: 0.5rem !important;
+ }
+
+ .p-xl-3 {
+ padding: 1rem !important;
+ }
+
+ .p-xl-4 {
+ padding: 1.5rem !important;
+ }
+
+ .p-xl-5 {
+ padding: 3rem !important;
+ }
+
+ .px-xl-0 {
+ padding-right: 0 !important;
+ padding-left: 0 !important;
+ }
+
+ .px-xl-1 {
+ padding-right: 0.25rem !important;
+ padding-left: 0.25rem !important;
+ }
+
+ .px-xl-2 {
+ padding-right: 0.5rem !important;
+ padding-left: 0.5rem !important;
+ }
+
+ .px-xl-3 {
+ padding-right: 1rem !important;
+ padding-left: 1rem !important;
+ }
+
+ .px-xl-4 {
+ padding-right: 1.5rem !important;
+ padding-left: 1.5rem !important;
+ }
+
+ .px-xl-5 {
+ padding-right: 3rem !important;
+ padding-left: 3rem !important;
+ }
+
+ .py-xl-0 {
+ padding-top: 0 !important;
+ padding-bottom: 0 !important;
+ }
+
+ .py-xl-1 {
+ padding-top: 0.25rem !important;
+ padding-bottom: 0.25rem !important;
+ }
+
+ .py-xl-2 {
+ padding-top: 0.5rem !important;
+ padding-bottom: 0.5rem !important;
+ }
+
+ .py-xl-3 {
+ padding-top: 1rem !important;
+ padding-bottom: 1rem !important;
+ }
+
+ .py-xl-4 {
+ padding-top: 1.5rem !important;
+ padding-bottom: 1.5rem !important;
+ }
+
+ .py-xl-5 {
+ padding-top: 3rem !important;
+ padding-bottom: 3rem !important;
+ }
+
+ .pt-xl-0 {
+ padding-top: 0 !important;
+ }
+
+ .pt-xl-1 {
+ padding-top: 0.25rem !important;
+ }
+
+ .pt-xl-2 {
+ padding-top: 0.5rem !important;
+ }
+
+ .pt-xl-3 {
+ padding-top: 1rem !important;
+ }
+
+ .pt-xl-4 {
+ padding-top: 1.5rem !important;
+ }
+
+ .pt-xl-5 {
+ padding-top: 3rem !important;
+ }
+
+ .pe-xl-0 {
+ padding-right: 0 !important;
+ }
+
+ .pe-xl-1 {
+ padding-right: 0.25rem !important;
+ }
+
+ .pe-xl-2 {
+ padding-right: 0.5rem !important;
+ }
+
+ .pe-xl-3 {
+ padding-right: 1rem !important;
+ }
+
+ .pe-xl-4 {
+ padding-right: 1.5rem !important;
+ }
+
+ .pe-xl-5 {
+ padding-right: 3rem !important;
+ }
+
+ .pb-xl-0 {
+ padding-bottom: 0 !important;
+ }
+
+ .pb-xl-1 {
+ padding-bottom: 0.25rem !important;
+ }
+
+ .pb-xl-2 {
+ padding-bottom: 0.5rem !important;
+ }
+
+ .pb-xl-3 {
+ padding-bottom: 1rem !important;
+ }
+
+ .pb-xl-4 {
+ padding-bottom: 1.5rem !important;
+ }
+
+ .pb-xl-5 {
+ padding-bottom: 3rem !important;
+ }
+
+ .ps-xl-0 {
+ padding-left: 0 !important;
+ }
+
+ .ps-xl-1 {
+ padding-left: 0.25rem !important;
+ }
+
+ .ps-xl-2 {
+ padding-left: 0.5rem !important;
+ }
+
+ .ps-xl-3 {
+ padding-left: 1rem !important;
+ }
+
+ .ps-xl-4 {
+ padding-left: 1.5rem !important;
+ }
+
+ .ps-xl-5 {
+ padding-left: 3rem !important;
+ }
+
+ .text-xl-start {
+ text-align: left !important;
+ }
+
+ .text-xl-end {
+ text-align: right !important;
+ }
+
+ .text-xl-center {
+ text-align: center !important;
+ }
+}
+
+@media (min-width: 1400px) {
+ .float-xxl-start {
+ float: left !important;
+ }
+
+ .float-xxl-end {
+ float: right !important;
+ }
+
+ .float-xxl-none {
+ float: none !important;
+ }
+
+ .d-xxl-inline {
+ display: inline !important;
+ }
+
+ .d-xxl-inline-block {
+ display: inline-block !important;
+ }
+
+ .d-xxl-block {
+ display: block !important;
+ }
+
+ .d-xxl-grid {
+ display: grid !important;
+ }
+
+ .d-xxl-table {
+ display: table !important;
+ }
+
+ .d-xxl-table-row {
+ display: table-row !important;
+ }
+
+ .d-xxl-table-cell {
+ display: table-cell !important;
+ }
+
+ .d-xxl-flex {
+ display: flex !important;
+ }
+
+ .d-xxl-inline-flex {
+ display: inline-flex !important;
+ }
+
+ .d-xxl-none {
+ display: none !important;
+ }
+
+ .flex-xxl-fill {
+ flex: 1 1 auto !important;
+ }
+
+ .flex-xxl-row {
+ flex-direction: row !important;
+ }
+
+ .flex-xxl-column {
+ flex-direction: column !important;
+ }
+
+ .flex-xxl-row-reverse {
+ flex-direction: row-reverse !important;
+ }
+
+ .flex-xxl-column-reverse {
+ flex-direction: column-reverse !important;
+ }
+
+ .flex-xxl-grow-0 {
+ flex-grow: 0 !important;
+ }
+
+ .flex-xxl-grow-1 {
+ flex-grow: 1 !important;
+ }
+
+ .flex-xxl-shrink-0 {
+ flex-shrink: 0 !important;
+ }
+
+ .flex-xxl-shrink-1 {
+ flex-shrink: 1 !important;
+ }
+
+ .flex-xxl-wrap {
+ flex-wrap: wrap !important;
+ }
+
+ .flex-xxl-nowrap {
+ flex-wrap: nowrap !important;
+ }
+
+ .flex-xxl-wrap-reverse {
+ flex-wrap: wrap-reverse !important;
+ }
+
+ .gap-xxl-0 {
+ gap: 0 !important;
+ }
+
+ .gap-xxl-1 {
+ gap: 0.25rem !important;
+ }
+
+ .gap-xxl-2 {
+ gap: 0.5rem !important;
+ }
+
+ .gap-xxl-3 {
+ gap: 1rem !important;
+ }
+
+ .gap-xxl-4 {
+ gap: 1.5rem !important;
+ }
+
+ .gap-xxl-5 {
+ gap: 3rem !important;
+ }
+
+ .justify-content-xxl-start {
+ justify-content: flex-start !important;
+ }
+
+ .justify-content-xxl-end {
+ justify-content: flex-end !important;
+ }
+
+ .justify-content-xxl-center {
+ justify-content: center !important;
+ }
+
+ .justify-content-xxl-between {
+ justify-content: space-between !important;
+ }
+
+ .justify-content-xxl-around {
+ justify-content: space-around !important;
+ }
+
+ .justify-content-xxl-evenly {
+ justify-content: space-evenly !important;
+ }
+
+ .align-items-xxl-start {
+ align-items: flex-start !important;
+ }
+
+ .align-items-xxl-end {
+ align-items: flex-end !important;
+ }
+
+ .align-items-xxl-center {
+ align-items: center !important;
+ }
+
+ .align-items-xxl-baseline {
+ align-items: baseline !important;
+ }
+
+ .align-items-xxl-stretch {
+ align-items: stretch !important;
+ }
+
+ .align-content-xxl-start {
+ align-content: flex-start !important;
+ }
+
+ .align-content-xxl-end {
+ align-content: flex-end !important;
+ }
+
+ .align-content-xxl-center {
+ align-content: center !important;
+ }
+
+ .align-content-xxl-between {
+ align-content: space-between !important;
+ }
+
+ .align-content-xxl-around {
+ align-content: space-around !important;
+ }
+
+ .align-content-xxl-stretch {
+ align-content: stretch !important;
+ }
+
+ .align-self-xxl-auto {
+ align-self: auto !important;
+ }
+
+ .align-self-xxl-start {
+ align-self: flex-start !important;
+ }
+
+ .align-self-xxl-end {
+ align-self: flex-end !important;
+ }
+
+ .align-self-xxl-center {
+ align-self: center !important;
+ }
+
+ .align-self-xxl-baseline {
+ align-self: baseline !important;
+ }
+
+ .align-self-xxl-stretch {
+ align-self: stretch !important;
+ }
+
+ .order-xxl-first {
+ order: -1 !important;
+ }
+
+ .order-xxl-0 {
+ order: 0 !important;
+ }
+
+ .order-xxl-1 {
+ order: 1 !important;
+ }
+
+ .order-xxl-2 {
+ order: 2 !important;
+ }
+
+ .order-xxl-3 {
+ order: 3 !important;
+ }
+
+ .order-xxl-4 {
+ order: 4 !important;
+ }
+
+ .order-xxl-5 {
+ order: 5 !important;
+ }
+
+ .order-xxl-last {
+ order: 6 !important;
+ }
+
+ .m-xxl-0 {
+ margin: 0 !important;
+ }
+
+ .m-xxl-1 {
+ margin: 0.25rem !important;
+ }
+
+ .m-xxl-2 {
+ margin: 0.5rem !important;
+ }
+
+ .m-xxl-3 {
+ margin: 1rem !important;
+ }
+
+ .m-xxl-4 {
+ margin: 1.5rem !important;
+ }
+
+ .m-xxl-5 {
+ margin: 3rem !important;
+ }
+
+ .m-xxl-auto {
+ margin: auto !important;
+ }
+
+ .mx-xxl-0 {
+ margin-right: 0 !important;
+ margin-left: 0 !important;
+ }
+
+ .mx-xxl-1 {
+ margin-right: 0.25rem !important;
+ margin-left: 0.25rem !important;
+ }
+
+ .mx-xxl-2 {
+ margin-right: 0.5rem !important;
+ margin-left: 0.5rem !important;
+ }
+
+ .mx-xxl-3 {
+ margin-right: 1rem !important;
+ margin-left: 1rem !important;
+ }
+
+ .mx-xxl-4 {
+ margin-right: 1.5rem !important;
+ margin-left: 1.5rem !important;
+ }
+
+ .mx-xxl-5 {
+ margin-right: 3rem !important;
+ margin-left: 3rem !important;
+ }
+
+ .mx-xxl-auto {
+ margin-right: auto !important;
+ margin-left: auto !important;
+ }
+
+ .my-xxl-0 {
+ margin-top: 0 !important;
+ margin-bottom: 0 !important;
+ }
+
+ .my-xxl-1 {
+ margin-top: 0.25rem !important;
+ margin-bottom: 0.25rem !important;
+ }
+
+ .my-xxl-2 {
+ margin-top: 0.5rem !important;
+ margin-bottom: 0.5rem !important;
+ }
+
+ .my-xxl-3 {
+ margin-top: 1rem !important;
+ margin-bottom: 1rem !important;
+ }
+
+ .my-xxl-4 {
+ margin-top: 1.5rem !important;
+ margin-bottom: 1.5rem !important;
+ }
+
+ .my-xxl-5 {
+ margin-top: 3rem !important;
+ margin-bottom: 3rem !important;
+ }
+
+ .my-xxl-auto {
+ margin-top: auto !important;
+ margin-bottom: auto !important;
+ }
+
+ .mt-xxl-0 {
+ margin-top: 0 !important;
+ }
+
+ .mt-xxl-1 {
+ margin-top: 0.25rem !important;
+ }
+
+ .mt-xxl-2 {
+ margin-top: 0.5rem !important;
+ }
+
+ .mt-xxl-3 {
+ margin-top: 1rem !important;
+ }
+
+ .mt-xxl-4 {
+ margin-top: 1.5rem !important;
+ }
+
+ .mt-xxl-5 {
+ margin-top: 3rem !important;
+ }
+
+ .mt-xxl-auto {
+ margin-top: auto !important;
+ }
+
+ .me-xxl-0 {
+ margin-right: 0 !important;
+ }
+
+ .me-xxl-1 {
+ margin-right: 0.25rem !important;
+ }
+
+ .me-xxl-2 {
+ margin-right: 0.5rem !important;
+ }
+
+ .me-xxl-3 {
+ margin-right: 1rem !important;
+ }
+
+ .me-xxl-4 {
+ margin-right: 1.5rem !important;
+ }
+
+ .me-xxl-5 {
+ margin-right: 3rem !important;
+ }
+
+ .me-xxl-auto {
+ margin-right: auto !important;
+ }
+
+ .mb-xxl-0 {
+ margin-bottom: 0 !important;
+ }
+
+ .mb-xxl-1 {
+ margin-bottom: 0.25rem !important;
+ }
+
+ .mb-xxl-2 {
+ margin-bottom: 0.5rem !important;
+ }
+
+ .mb-xxl-3 {
+ margin-bottom: 1rem !important;
+ }
+
+ .mb-xxl-4 {
+ margin-bottom: 1.5rem !important;
+ }
+
+ .mb-xxl-5 {
+ margin-bottom: 3rem !important;
+ }
+
+ .mb-xxl-auto {
+ margin-bottom: auto !important;
+ }
+
+ .ms-xxl-0 {
+ margin-left: 0 !important;
+ }
+
+ .ms-xxl-1 {
+ margin-left: 0.25rem !important;
+ }
+
+ .ms-xxl-2 {
+ margin-left: 0.5rem !important;
+ }
+
+ .ms-xxl-3 {
+ margin-left: 1rem !important;
+ }
+
+ .ms-xxl-4 {
+ margin-left: 1.5rem !important;
+ }
+
+ .ms-xxl-5 {
+ margin-left: 3rem !important;
+ }
+
+ .ms-xxl-auto {
+ margin-left: auto !important;
+ }
+
+ .p-xxl-0 {
+ padding: 0 !important;
+ }
+
+ .p-xxl-1 {
+ padding: 0.25rem !important;
+ }
+
+ .p-xxl-2 {
+ padding: 0.5rem !important;
+ }
+
+ .p-xxl-3 {
+ padding: 1rem !important;
+ }
+
+ .p-xxl-4 {
+ padding: 1.5rem !important;
+ }
+
+ .p-xxl-5 {
+ padding: 3rem !important;
+ }
+
+ .px-xxl-0 {
+ padding-right: 0 !important;
+ padding-left: 0 !important;
+ }
+
+ .px-xxl-1 {
+ padding-right: 0.25rem !important;
+ padding-left: 0.25rem !important;
+ }
+
+ .px-xxl-2 {
+ padding-right: 0.5rem !important;
+ padding-left: 0.5rem !important;
+ }
+
+ .px-xxl-3 {
+ padding-right: 1rem !important;
+ padding-left: 1rem !important;
+ }
+
+ .px-xxl-4 {
+ padding-right: 1.5rem !important;
+ padding-left: 1.5rem !important;
+ }
+
+ .px-xxl-5 {
+ padding-right: 3rem !important;
+ padding-left: 3rem !important;
+ }
+
+ .py-xxl-0 {
+ padding-top: 0 !important;
+ padding-bottom: 0 !important;
+ }
+
+ .py-xxl-1 {
+ padding-top: 0.25rem !important;
+ padding-bottom: 0.25rem !important;
+ }
+
+ .py-xxl-2 {
+ padding-top: 0.5rem !important;
+ padding-bottom: 0.5rem !important;
+ }
+
+ .py-xxl-3 {
+ padding-top: 1rem !important;
+ padding-bottom: 1rem !important;
+ }
+
+ .py-xxl-4 {
+ padding-top: 1.5rem !important;
+ padding-bottom: 1.5rem !important;
+ }
+
+ .py-xxl-5 {
+ padding-top: 3rem !important;
+ padding-bottom: 3rem !important;
+ }
+
+ .pt-xxl-0 {
+ padding-top: 0 !important;
+ }
+
+ .pt-xxl-1 {
+ padding-top: 0.25rem !important;
+ }
+
+ .pt-xxl-2 {
+ padding-top: 0.5rem !important;
+ }
+
+ .pt-xxl-3 {
+ padding-top: 1rem !important;
+ }
+
+ .pt-xxl-4 {
+ padding-top: 1.5rem !important;
+ }
+
+ .pt-xxl-5 {
+ padding-top: 3rem !important;
+ }
+
+ .pe-xxl-0 {
+ padding-right: 0 !important;
+ }
+
+ .pe-xxl-1 {
+ padding-right: 0.25rem !important;
+ }
+
+ .pe-xxl-2 {
+ padding-right: 0.5rem !important;
+ }
+
+ .pe-xxl-3 {
+ padding-right: 1rem !important;
+ }
+
+ .pe-xxl-4 {
+ padding-right: 1.5rem !important;
+ }
+
+ .pe-xxl-5 {
+ padding-right: 3rem !important;
+ }
+
+ .pb-xxl-0 {
+ padding-bottom: 0 !important;
+ }
+
+ .pb-xxl-1 {
+ padding-bottom: 0.25rem !important;
+ }
+
+ .pb-xxl-2 {
+ padding-bottom: 0.5rem !important;
+ }
+
+ .pb-xxl-3 {
+ padding-bottom: 1rem !important;
+ }
+
+ .pb-xxl-4 {
+ padding-bottom: 1.5rem !important;
+ }
+
+ .pb-xxl-5 {
+ padding-bottom: 3rem !important;
+ }
+
+ .ps-xxl-0 {
+ padding-left: 0 !important;
+ }
+
+ .ps-xxl-1 {
+ padding-left: 0.25rem !important;
+ }
+
+ .ps-xxl-2 {
+ padding-left: 0.5rem !important;
+ }
+
+ .ps-xxl-3 {
+ padding-left: 1rem !important;
+ }
+
+ .ps-xxl-4 {
+ padding-left: 1.5rem !important;
+ }
+
+ .ps-xxl-5 {
+ padding-left: 3rem !important;
+ }
+
+ .text-xxl-start {
+ text-align: left !important;
+ }
+
+ .text-xxl-end {
+ text-align: right !important;
+ }
+
+ .text-xxl-center {
+ text-align: center !important;
+ }
+}
+
+@media (min-width: 1200px) {
+ .fs-1 {
+ font-size: 2.5rem !important;
+ }
+
+ .fs-2 {
+ font-size: 2rem !important;
+ }
+
+ .fs-3 {
+ font-size: 1.75rem !important;
+ }
+
+ .fs-4 {
+ font-size: 1.5rem !important;
+ }
+}
+
+@media print {
+ .d-print-inline {
+ display: inline !important;
+ }
+
+ .d-print-inline-block {
+ display: inline-block !important;
+ }
+
+ .d-print-block {
+ display: block !important;
+ }
+
+ .d-print-grid {
+ display: grid !important;
+ }
+
+ .d-print-table {
+ display: table !important;
+ }
+
+ .d-print-table-row {
+ display: table-row !important;
+ }
+
+ .d-print-table-cell {
+ display: table-cell !important;
+ }
+
+ .d-print-flex {
+ display: flex !important;
+ }
+
+ .d-print-inline-flex {
+ display: inline-flex !important;
+ }
+
+ .d-print-none {
+ display: none !important;
+ }
+}
+
+/*# sourceMappingURL=bootstrap.css.map */
diff --git a/src/assets/css/onap-styles.css b/src/assets/css/onap-styles.css
new file mode 100644
index 0000000..272eded
--- /dev/null
+++ b/src/assets/css/onap-styles.css
@@ -0,0 +1,10755 @@
+
+
+:root {
+ --blue: #07819b;
+ --indigo: #6610f2;
+ --purple: #6f42c1;
+ --pink: #e83e8c;
+ --red: #dc3545;
+ --orange: #fd7e14;
+ --yellow: #ffc107;
+ --green: #28a745;
+ --teal: #20c997;
+ --cyan: #17a2b8;
+ --white: #fff;
+ --gray: #313032;
+ --gray-dark: #343a40;
+ --primary: #07819b;
+ --secondary: #313032;
+ --success: #28a745;
+ --info: #17a2b8;
+ --warning: #ffc107;
+ --danger: #dc3545;
+ --light: #f8f9fa;
+ --dark: #343a40;
+ --breakpoint-xs: 0;
+ --breakpoint-sm: 576px;
+ --breakpoint-md: 768px;
+ --breakpoint-lg: 992px;
+ --breakpoint-xl: 1200px;
+ --font-family-sans-serif: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans',
+ sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
+ --font-family-monospace: SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;
+}
+
+*,
+*::before,
+*::after {
+ box-sizing: border-box;
+}
+
+html {
+ font-family: sans-serif;
+ line-height: 1.15;
+ -webkit-text-size-adjust: 100%;
+ -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
+}
+
+article,
+aside,
+figcaption,
+figure,
+footer,
+header,
+hgroup,
+main,
+nav,
+section {
+ display: block;
+}
+
+body {
+ margin: 0;
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif,
+ 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
+ font-size: 1rem;
+ font-weight: 400;
+ line-height: 1.5;
+ color: #212529;
+ text-align: left;
+ background-color: #fff;
+}
+
+[tabindex='-1']:focus:not(:focus-visible) {
+ outline: 0 !important;
+}
+
+hr {
+ box-sizing: content-box;
+ height: 0;
+ overflow: visible;
+}
+
+h1,
+h2,
+h3,
+h4,
+h5,
+h6 {
+ margin-top: 0;
+ margin-bottom: 0.5rem;
+}
+
+p {
+ margin-top: 0;
+ margin-bottom: 1rem;
+}
+
+abbr[title],
+abbr[data-original-title] {
+ text-decoration: underline;
+ text-decoration: underline dotted;
+ cursor: help;
+ border-bottom: 0;
+ text-decoration-skip-ink: none;
+}
+
+address {
+ margin-bottom: 1rem;
+ font-style: normal;
+ line-height: inherit;
+}
+
+ol,
+ul,
+dl {
+ margin-top: 0;
+ margin-bottom: 1rem;
+}
+
+ol ol,
+ul ul,
+ol ul,
+ul ol {
+ margin-bottom: 0;
+}
+
+dt {
+ font-weight: 700;
+}
+
+dd {
+ margin-bottom: 0.5rem;
+ margin-left: 0;
+}
+
+blockquote {
+ margin: 0 0 1rem;
+}
+
+b,
+strong {
+ font-weight: bolder;
+}
+
+small {
+ font-size: 80%;
+}
+
+sub,
+sup {
+ position: relative;
+ font-size: 75%;
+ line-height: 0;
+ vertical-align: baseline;
+}
+
+sub {
+ bottom: -0.25em;
+}
+
+sup {
+ top: -0.5em;
+}
+
+a {
+ color: #07819b;
+ text-decoration: none;
+ background-color: transparent;
+}
+
+a:hover {
+ color: #044452;
+ text-decoration: underline;
+}
+
+a:not([href]) {
+ color: inherit;
+ text-decoration: none;
+}
+
+a:not([href]):hover {
+ color: inherit;
+ text-decoration: none;
+}
+
+pre,
+code,
+kbd,
+samp {
+ font-family: SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;
+ font-size: 1em;
+}
+
+pre {
+ margin-top: 0;
+ margin-bottom: 1rem;
+ overflow: auto;
+ -ms-overflow-style: scrollbar;
+}
+
+figure {
+ margin: 0 0 1rem;
+}
+
+img {
+ vertical-align: middle;
+ border-style: none;
+}
+
+svg {
+ overflow: hidden;
+ vertical-align: middle;
+}
+
+table {
+ border-collapse: collapse;
+}
+
+caption {
+ padding-top: 0.75rem;
+ padding-bottom: 0.75rem;
+ color: #313032;
+ text-align: left;
+ caption-side: bottom;
+}
+
+th {
+ text-align: inherit;
+}
+
+label {
+ display: inline-block;
+ margin-bottom: 0.5rem;
+}
+
+button {
+ border-radius: 0;
+}
+
+button:focus {
+ outline: 1px dotted;
+ outline: 5px auto -webkit-focus-ring-color;
+}
+
+input,
+button,
+select,
+optgroup,
+textarea {
+ margin: 0;
+ font-family: inherit;
+ font-size: inherit;
+ line-height: inherit;
+}
+
+button,
+input {
+ overflow: visible;
+}
+
+button,
+select {
+ text-transform: none;
+}
+
+[role='button'] {
+ cursor: pointer;
+}
+
+select {
+ word-wrap: normal;
+}
+
+button,
+[type='button'],
+[type='reset'],
+[type='submit'] {
+ -webkit-appearance: button;
+}
+
+button:not(:disabled),
+[type='button']:not(:disabled),
+[type='reset']:not(:disabled),
+[type='submit']:not(:disabled) {
+ cursor: pointer;
+}
+
+button::-moz-focus-inner,
+[type='button']::-moz-focus-inner,
+[type='reset']::-moz-focus-inner,
+[type='submit']::-moz-focus-inner {
+ padding: 0;
+ border-style: none;
+}
+
+input[type='radio'],
+input[type='checkbox'] {
+ box-sizing: border-box;
+ padding: 0;
+}
+
+textarea {
+ overflow: auto;
+ resize: vertical;
+}
+
+fieldset {
+ min-width: 0;
+ padding: 0;
+ margin: 0;
+ border: 0;
+}
+
+legend {
+ display: block;
+ width: 100%;
+ max-width: 100%;
+ padding: 0;
+ margin-bottom: 0.5rem;
+ font-size: 1.5rem;
+ line-height: inherit;
+ color: inherit;
+ white-space: normal;
+}
+
+progress {
+ vertical-align: baseline;
+}
+
+[type='number']::-webkit-inner-spin-button,
+[type='number']::-webkit-outer-spin-button {
+ height: auto;
+}
+
+[type='search'] {
+ outline-offset: -2px;
+ -webkit-appearance: none;
+}
+
+[type='search']::-webkit-search-decoration {
+ -webkit-appearance: none;
+}
+
+::-webkit-file-upload-button {
+ font: inherit;
+ -webkit-appearance: button;
+}
+
+output {
+ display: inline-block;
+}
+
+summary {
+ display: list-item;
+ cursor: pointer;
+}
+
+template {
+ display: none;
+}
+
+[hidden] {
+ display: none !important;
+}
+
+h1,
+h2,
+h3,
+h4,
+h5,
+h6,
+.h1,
+.h2,
+.h3,
+.h4,
+.h5,
+.h6 {
+ margin-bottom: 0.5rem;
+ font-weight: 500;
+ line-height: 1.2;
+}
+
+h1,
+.h1 {
+ font-size: 2.5rem;
+}
+
+h2,
+.h2 {
+ font-size: 2rem;
+}
+
+h3,
+.h3 {
+ font-size: 1.75rem;
+}
+
+h4,
+.h4 {
+ font-size: 1.5rem;
+}
+
+h5,
+.h5 {
+ font-size: 1.25rem;
+}
+
+h6,
+.h6 {
+ font-size: 1rem;
+}
+
+.lead {
+ font-size: 1.25rem;
+ font-weight: 300;
+}
+
+.display-1 {
+ font-size: 6rem;
+ font-weight: 300;
+ line-height: 1.2;
+}
+
+.display-2 {
+ font-size: 5.5rem;
+ font-weight: 300;
+ line-height: 1.2;
+}
+
+.display-3 {
+ font-size: 4.5rem;
+ font-weight: 300;
+ line-height: 1.2;
+}
+
+.display-4 {
+ font-size: 3.5rem;
+ font-weight: 300;
+ line-height: 1.2;
+}
+
+hr {
+ margin-top: 1rem;
+ margin-bottom: 1rem;
+ border: 0;
+ border-top: 1px solid rgba(0, 0, 0, 0.1);
+}
+
+small,
+.small {
+ font-size: 80%;
+ font-weight: 400;
+}
+
+mark,
+.mark {
+ padding: 0.2em;
+ background-color: #fcf8e3;
+}
+
+.list-unstyled {
+ padding-left: 0;
+ list-style: none;
+}
+
+.list-inline {
+ padding-left: 0;
+ list-style: none;
+}
+
+.list-inline-item {
+ display: inline-block;
+}
+
+.list-inline-item:not(:last-child) {
+ margin-right: 0.5rem;
+}
+
+.initialism {
+ font-size: 90%;
+ text-transform: uppercase;
+}
+
+.blockquote {
+ margin-bottom: 1rem;
+ font-size: 1.25rem;
+}
+
+.blockquote-footer {
+ display: block;
+ font-size: 80%;
+ color: #313032;
+}
+
+.blockquote-footer::before {
+ content: '\2014\00A0';
+}
+
+.img-fluid {
+ max-width: 100%;
+ height: auto;
+}
+
+.img-thumbnail {
+ padding: 0.25rem;
+ background-color: #fff;
+ border: 1px solid #dee2e6;
+ border-radius: 0.25rem;
+ max-width: 100%;
+ height: auto;
+}
+
+.figure {
+ display: inline-block;
+}
+
+.figure-img {
+ margin-bottom: 0.5rem;
+ line-height: 1;
+}
+
+.figure-caption {
+ font-size: 90%;
+ color: #313032;
+}
+
+code {
+ font-size: 87.5%;
+ color: #e83e8c;
+ word-wrap: break-word;
+}
+
+a > code {
+ color: inherit;
+}
+
+kbd {
+ padding: 0.2rem 0.4rem;
+ font-size: 87.5%;
+ color: #fff;
+ background-color: #212529;
+ border-radius: 0.2rem;
+}
+
+kbd kbd {
+ padding: 0;
+ font-size: 100%;
+ font-weight: 700;
+}
+
+pre {
+ display: block;
+ font-size: 87.5%;
+ color: #212529;
+}
+
+pre code {
+ font-size: inherit;
+ color: inherit;
+ word-break: normal;
+}
+
+.pre-scrollable {
+ max-height: 340px;
+ overflow-y: scroll;
+}
+
+.container {
+ width: 100%;
+ padding-right: 15px;
+ padding-left: 15px;
+ margin-right: auto;
+ margin-left: auto;
+}
+
+@media (min-width: 576px) {
+ .container {
+ max-width: 540px;
+ }
+}
+
+@media (min-width: 768px) {
+ .container {
+ max-width: 720px;
+ }
+}
+
+@media (min-width: 992px) {
+ .container {
+ max-width: 960px;
+ }
+}
+
+@media (min-width: 1200px) {
+ .container {
+ max-width: 1140px;
+ }
+}
+
+.container-fluid,
+.container-sm,
+.container-md,
+.container-lg,
+.container-xl {
+ width: 100%;
+ padding-right: 15px;
+ padding-left: 15px;
+ margin-right: auto;
+ margin-left: auto;
+}
+
+@media (min-width: 576px) {
+ .container,
+ .container-sm {
+ max-width: 540px;
+ }
+}
+
+@media (min-width: 768px) {
+ .container,
+ .container-sm,
+ .container-md {
+ max-width: 720px;
+ }
+}
+
+@media (min-width: 992px) {
+ .container,
+ .container-sm,
+ .container-md,
+ .container-lg {
+ max-width: 960px;
+ }
+}
+
+@media (min-width: 1200px) {
+ .container,
+ .container-sm,
+ .container-md,
+ .container-lg,
+ .container-xl {
+ max-width: 1140px;
+ }
+}
+
+.row {
+ display: flex;
+ flex-wrap: wrap;
+ margin-right: -15px;
+ margin-left: -15px;
+}
+
+.no-gutters {
+ margin-right: 0;
+ margin-left: 0;
+}
+
+.no-gutters > .col,
+.no-gutters > [class*='col-'] {
+ padding-right: 0;
+ padding-left: 0;
+}
+
+.col-1,
+.col-2,
+.col-3,
+.col-4,
+.col-5,
+.col-6,
+.col-7,
+.col-8,
+.col-9,
+.col-10,
+.col-11,
+.col-12,
+.col,
+.col-auto,
+.col-sm-1,
+.col-sm-2,
+.col-sm-3,
+.col-sm-4,
+.col-sm-5,
+.col-sm-6,
+.col-sm-7,
+.col-sm-8,
+.col-sm-9,
+.col-sm-10,
+.col-sm-11,
+.col-sm-12,
+.col-sm,
+.col-sm-auto,
+.col-md-1,
+.col-md-2,
+.col-md-3,
+.col-md-4,
+.col-md-5,
+.col-md-6,
+.col-md-7,
+.col-md-8,
+.col-md-9,
+.col-md-10,
+.col-md-11,
+.col-md-12,
+.col-md,
+.col-md-auto,
+.col-lg-1,
+.col-lg-2,
+.col-lg-3,
+.col-lg-4,
+.col-lg-5,
+.col-lg-6,
+.col-lg-7,
+.col-lg-8,
+.col-lg-9,
+.col-lg-10,
+.col-lg-11,
+.col-lg-12,
+.col-lg,
+.col-lg-auto,
+.col-xl-1,
+.col-xl-2,
+.col-xl-3,
+.col-xl-4,
+.col-xl-5,
+.col-xl-6,
+.col-xl-7,
+.col-xl-8,
+.col-xl-9,
+.col-xl-10,
+.col-xl-11,
+.col-xl-12,
+.col-xl,
+.col-xl-auto {
+ position: relative;
+ width: 100%;
+ padding-right: 15px;
+ padding-left: 15px;
+}
+
+.col {
+ flex-basis: 0;
+ flex-grow: 1;
+ min-width: 0;
+ max-width: 100%;
+}
+
+.row-cols-1 > * {
+ flex: 0 0 100%;
+ max-width: 100%;
+}
+
+.row-cols-2 > * {
+ flex: 0 0 50%;
+ max-width: 50%;
+}
+
+.row-cols-3 > * {
+ flex: 0 0 33.33333%;
+ max-width: 33.33333%;
+}
+
+.row-cols-4 > * {
+ flex: 0 0 25%;
+ max-width: 25%;
+}
+
+.row-cols-5 > * {
+ flex: 0 0 20%;
+ max-width: 20%;
+}
+
+.row-cols-6 > * {
+ flex: 0 0 16.66667%;
+ max-width: 16.66667%;
+}
+
+.col-auto {
+ flex: 0 0 auto;
+ width: auto;
+ max-width: 100%;
+}
+
+.col-1 {
+ flex: 0 0 8.33333%;
+ max-width: 8.33333%;
+}
+
+.col-2 {
+ flex: 0 0 16.66667%;
+ max-width: 16.66667%;
+}
+
+.col-3 {
+ flex: 0 0 25%;
+ max-width: 25%;
+}
+
+.col-4 {
+ flex: 0 0 33.33333%;
+ max-width: 33.33333%;
+}
+
+.col-5 {
+ flex: 0 0 41.66667%;
+ max-width: 41.66667%;
+}
+
+.col-6 {
+ flex: 0 0 50%;
+ max-width: 50%;
+}
+
+.col-7 {
+ flex: 0 0 58.33333%;
+ max-width: 58.33333%;
+}
+
+.col-8 {
+ flex: 0 0 66.66667%;
+ max-width: 66.66667%;
+}
+
+.col-9 {
+ flex: 0 0 75%;
+ max-width: 75%;
+}
+
+.col-10 {
+ flex: 0 0 83.33333%;
+ max-width: 83.33333%;
+}
+
+.col-11 {
+ flex: 0 0 91.66667%;
+ max-width: 91.66667%;
+}
+
+.col-12 {
+ flex: 0 0 100%;
+ max-width: 100%;
+}
+
+.order-first {
+ order: -1;
+}
+
+.order-last {
+ order: 13;
+}
+
+.order-0 {
+ order: 0;
+}
+
+.order-1 {
+ order: 1;
+}
+
+.order-2 {
+ order: 2;
+}
+
+.order-3 {
+ order: 3;
+}
+
+.order-4 {
+ order: 4;
+}
+
+.order-5 {
+ order: 5;
+}
+
+.order-6 {
+ order: 6;
+}
+
+.order-7 {
+ order: 7;
+}
+
+.order-8 {
+ order: 8;
+}
+
+.order-9 {
+ order: 9;
+}
+
+.order-10 {
+ order: 10;
+}
+
+.order-11 {
+ order: 11;
+}
+
+.order-12 {
+ order: 12;
+}
+
+.offset-1 {
+ margin-left: 8.33333%;
+}
+
+.offset-2 {
+ margin-left: 16.66667%;
+}
+
+.offset-3 {
+ margin-left: 25%;
+}
+
+.offset-4 {
+ margin-left: 33.33333%;
+}
+
+.offset-5 {
+ margin-left: 41.66667%;
+}
+
+.offset-6 {
+ margin-left: 50%;
+}
+
+.offset-7 {
+ margin-left: 58.33333%;
+}
+
+.offset-8 {
+ margin-left: 66.66667%;
+}
+
+.offset-9 {
+ margin-left: 75%;
+}
+
+.offset-10 {
+ margin-left: 83.33333%;
+}
+
+.offset-11 {
+ margin-left: 91.66667%;
+}
+
+@media (min-width: 576px) {
+ .col-sm {
+ flex-basis: 0;
+ flex-grow: 1;
+ min-width: 0;
+ max-width: 100%;
+ }
+
+ .row-cols-sm-1 > * {
+ flex: 0 0 100%;
+ max-width: 100%;
+ }
+
+ .row-cols-sm-2 > * {
+ flex: 0 0 50%;
+ max-width: 50%;
+ }
+
+ .row-cols-sm-3 > * {
+ flex: 0 0 33.33333%;
+ max-width: 33.33333%;
+ }
+
+ .row-cols-sm-4 > * {
+ flex: 0 0 25%;
+ max-width: 25%;
+ }
+
+ .row-cols-sm-5 > * {
+ flex: 0 0 20%;
+ max-width: 20%;
+ }
+
+ .row-cols-sm-6 > * {
+ flex: 0 0 16.66667%;
+ max-width: 16.66667%;
+ }
+
+ .col-sm-auto {
+ flex: 0 0 auto;
+ width: auto;
+ max-width: 100%;
+ }
+
+ .col-sm-1 {
+ flex: 0 0 8.33333%;
+ max-width: 8.33333%;
+ }
+
+ .col-sm-2 {
+ flex: 0 0 16.66667%;
+ max-width: 16.66667%;
+ }
+
+ .col-sm-3 {
+ flex: 0 0 25%;
+ max-width: 25%;
+ }
+
+ .col-sm-4 {
+ flex: 0 0 33.33333%;
+ max-width: 33.33333%;
+ }
+
+ .col-sm-5 {
+ flex: 0 0 41.66667%;
+ max-width: 41.66667%;
+ }
+
+ .col-sm-6 {
+ flex: 0 0 50%;
+ max-width: 50%;
+ }
+
+ .col-sm-7 {
+ flex: 0 0 58.33333%;
+ max-width: 58.33333%;
+ }
+
+ .col-sm-8 {
+ flex: 0 0 66.66667%;
+ max-width: 66.66667%;
+ }
+
+ .col-sm-9 {
+ flex: 0 0 75%;
+ max-width: 75%;
+ }
+
+ .col-sm-10 {
+ flex: 0 0 83.33333%;
+ max-width: 83.33333%;
+ }
+
+ .col-sm-11 {
+ flex: 0 0 91.66667%;
+ max-width: 91.66667%;
+ }
+
+ .col-sm-12 {
+ flex: 0 0 100%;
+ max-width: 100%;
+ }
+
+ .order-sm-first {
+ order: -1;
+ }
+
+ .order-sm-last {
+ order: 13;
+ }
+
+ .order-sm-0 {
+ order: 0;
+ }
+
+ .order-sm-1 {
+ order: 1;
+ }
+
+ .order-sm-2 {
+ order: 2;
+ }
+
+ .order-sm-3 {
+ order: 3;
+ }
+
+ .order-sm-4 {
+ order: 4;
+ }
+
+ .order-sm-5 {
+ order: 5;
+ }
+
+ .order-sm-6 {
+ order: 6;
+ }
+
+ .order-sm-7 {
+ order: 7;
+ }
+
+ .order-sm-8 {
+ order: 8;
+ }
+
+ .order-sm-9 {
+ order: 9;
+ }
+
+ .order-sm-10 {
+ order: 10;
+ }
+
+ .order-sm-11 {
+ order: 11;
+ }
+
+ .order-sm-12 {
+ order: 12;
+ }
+
+ .offset-sm-0 {
+ margin-left: 0;
+ }
+
+ .offset-sm-1 {
+ margin-left: 8.33333%;
+ }
+
+ .offset-sm-2 {
+ margin-left: 16.66667%;
+ }
+
+ .offset-sm-3 {
+ margin-left: 25%;
+ }
+
+ .offset-sm-4 {
+ margin-left: 33.33333%;
+ }
+
+ .offset-sm-5 {
+ margin-left: 41.66667%;
+ }
+
+ .offset-sm-6 {
+ margin-left: 50%;
+ }
+
+ .offset-sm-7 {
+ margin-left: 58.33333%;
+ }
+
+ .offset-sm-8 {
+ margin-left: 66.66667%;
+ }
+
+ .offset-sm-9 {
+ margin-left: 75%;
+ }
+
+ .offset-sm-10 {
+ margin-left: 83.33333%;
+ }
+
+ .offset-sm-11 {
+ margin-left: 91.66667%;
+ }
+}
+
+@media (min-width: 768px) {
+ .col-md {
+ flex-basis: 0;
+ flex-grow: 1;
+ min-width: 0;
+ max-width: 100%;
+ }
+
+ .row-cols-md-1 > * {
+ flex: 0 0 100%;
+ max-width: 100%;
+ }
+
+ .row-cols-md-2 > * {
+ flex: 0 0 50%;
+ max-width: 50%;
+ }
+
+ .row-cols-md-3 > * {
+ flex: 0 0 33.33333%;
+ max-width: 33.33333%;
+ }
+
+ .row-cols-md-4 > * {
+ flex: 0 0 25%;
+ max-width: 25%;
+ }
+
+ .row-cols-md-5 > * {
+ flex: 0 0 20%;
+ max-width: 20%;
+ }
+
+ .row-cols-md-6 > * {
+ flex: 0 0 16.66667%;
+ max-width: 16.66667%;
+ }
+
+ .col-md-auto {
+ flex: 0 0 auto;
+ width: auto;
+ max-width: 100%;
+ }
+
+ .col-md-1 {
+ flex: 0 0 8.33333%;
+ max-width: 8.33333%;
+ }
+
+ .col-md-2 {
+ flex: 0 0 16.66667%;
+ max-width: 16.66667%;
+ }
+
+ .col-md-3 {
+ flex: 0 0 25%;
+ max-width: 25%;
+ }
+
+ .col-md-4 {
+ flex: 0 0 33.33333%;
+ max-width: 33.33333%;
+ }
+
+ .col-md-5 {
+ flex: 0 0 41.66667%;
+ max-width: 41.66667%;
+ }
+
+ .col-md-6 {
+ flex: 0 0 50%;
+ max-width: 50%;
+ }
+
+ .col-md-7 {
+ flex: 0 0 58.33333%;
+ max-width: 58.33333%;
+ }
+
+ .col-md-8 {
+ flex: 0 0 66.66667%;
+ max-width: 66.66667%;
+ }
+
+ .col-md-9 {
+ flex: 0 0 75%;
+ max-width: 75%;
+ }
+
+ .col-md-10 {
+ flex: 0 0 83.33333%;
+ max-width: 83.33333%;
+ }
+
+ .col-md-11 {
+ flex: 0 0 91.66667%;
+ max-width: 91.66667%;
+ }
+
+ .col-md-12 {
+ flex: 0 0 100%;
+ max-width: 100%;
+ }
+
+ .order-md-first {
+ order: -1;
+ }
+
+ .order-md-last {
+ order: 13;
+ }
+
+ .order-md-0 {
+ order: 0;
+ }
+
+ .order-md-1 {
+ order: 1;
+ }
+
+ .order-md-2 {
+ order: 2;
+ }
+
+ .order-md-3 {
+ order: 3;
+ }
+
+ .order-md-4 {
+ order: 4;
+ }
+
+ .order-md-5 {
+ order: 5;
+ }
+
+ .order-md-6 {
+ order: 6;
+ }
+
+ .order-md-7 {
+ order: 7;
+ }
+
+ .order-md-8 {
+ order: 8;
+ }
+
+ .order-md-9 {
+ order: 9;
+ }
+
+ .order-md-10 {
+ order: 10;
+ }
+
+ .order-md-11 {
+ order: 11;
+ }
+
+ .order-md-12 {
+ order: 12;
+ }
+
+ .offset-md-0 {
+ margin-left: 0;
+ }
+
+ .offset-md-1 {
+ margin-left: 8.33333%;
+ }
+
+ .offset-md-2 {
+ margin-left: 16.66667%;
+ }
+
+ .offset-md-3 {
+ margin-left: 25%;
+ }
+
+ .offset-md-4 {
+ margin-left: 33.33333%;
+ }
+
+ .offset-md-5 {
+ margin-left: 41.66667%;
+ }
+
+ .offset-md-6 {
+ margin-left: 50%;
+ }
+
+ .offset-md-7 {
+ margin-left: 58.33333%;
+ }
+
+ .offset-md-8 {
+ margin-left: 66.66667%;
+ }
+
+ .offset-md-9 {
+ margin-left: 75%;
+ }
+
+ .offset-md-10 {
+ margin-left: 83.33333%;
+ }
+
+ .offset-md-11 {
+ margin-left: 91.66667%;
+ }
+}
+
+@media (min-width: 992px) {
+ .col-lg {
+ flex-basis: 0;
+ flex-grow: 1;
+ min-width: 0;
+ max-width: 100%;
+ }
+
+ .row-cols-lg-1 > * {
+ flex: 0 0 100%;
+ max-width: 100%;
+ }
+
+ .row-cols-lg-2 > * {
+ flex: 0 0 50%;
+ max-width: 50%;
+ }
+
+ .row-cols-lg-3 > * {
+ flex: 0 0 33.33333%;
+ max-width: 33.33333%;
+ }
+
+ .row-cols-lg-4 > * {
+ flex: 0 0 25%;
+ max-width: 25%;
+ }
+
+ .row-cols-lg-5 > * {
+ flex: 0 0 20%;
+ max-width: 20%;
+ }
+
+ .row-cols-lg-6 > * {
+ flex: 0 0 16.66667%;
+ max-width: 16.66667%;
+ }
+
+ .col-lg-auto {
+ flex: 0 0 auto;
+ width: auto;
+ max-width: 100%;
+ }
+
+ .col-lg-1 {
+ flex: 0 0 8.33333%;
+ max-width: 8.33333%;
+ }
+
+ .col-lg-2 {
+ flex: 0 0 16.66667%;
+ max-width: 16.66667%;
+ }
+
+ .col-lg-3 {
+ flex: 0 0 25%;
+ max-width: 25%;
+ }
+
+ .col-lg-4 {
+ flex: 0 0 33.33333%;
+ max-width: 33.33333%;
+ }
+
+ .col-lg-5 {
+ flex: 0 0 41.66667%;
+ max-width: 41.66667%;
+ }
+
+ .col-lg-6 {
+ flex: 0 0 50%;
+ max-width: 50%;
+ }
+
+ .col-lg-7 {
+ flex: 0 0 58.33333%;
+ max-width: 58.33333%;
+ }
+
+ .col-lg-8 {
+ flex: 0 0 66.66667%;
+ max-width: 66.66667%;
+ }
+
+ .col-lg-9 {
+ flex: 0 0 75%;
+ max-width: 75%;
+ }
+
+ .col-lg-10 {
+ flex: 0 0 83.33333%;
+ max-width: 83.33333%;
+ }
+
+ .col-lg-11 {
+ flex: 0 0 91.66667%;
+ max-width: 91.66667%;
+ }
+
+ .col-lg-12 {
+ flex: 0 0 100%;
+ max-width: 100%;
+ }
+
+ .order-lg-first {
+ order: -1;
+ }
+
+ .order-lg-last {
+ order: 13;
+ }
+
+ .order-lg-0 {
+ order: 0;
+ }
+
+ .order-lg-1 {
+ order: 1;
+ }
+
+ .order-lg-2 {
+ order: 2;
+ }
+
+ .order-lg-3 {
+ order: 3;
+ }
+
+ .order-lg-4 {
+ order: 4;
+ }
+
+ .order-lg-5 {
+ order: 5;
+ }
+
+ .order-lg-6 {
+ order: 6;
+ }
+
+ .order-lg-7 {
+ order: 7;
+ }
+
+ .order-lg-8 {
+ order: 8;
+ }
+
+ .order-lg-9 {
+ order: 9;
+ }
+
+ .order-lg-10 {
+ order: 10;
+ }
+
+ .order-lg-11 {
+ order: 11;
+ }
+
+ .order-lg-12 {
+ order: 12;
+ }
+
+ .offset-lg-0 {
+ margin-left: 0;
+ }
+
+ .offset-lg-1 {
+ margin-left: 8.33333%;
+ }
+
+ .offset-lg-2 {
+ margin-left: 16.66667%;
+ }
+
+ .offset-lg-3 {
+ margin-left: 25%;
+ }
+
+ .offset-lg-4 {
+ margin-left: 33.33333%;
+ }
+
+ .offset-lg-5 {
+ margin-left: 41.66667%;
+ }
+
+ .offset-lg-6 {
+ margin-left: 50%;
+ }
+
+ .offset-lg-7 {
+ margin-left: 58.33333%;
+ }
+
+ .offset-lg-8 {
+ margin-left: 66.66667%;
+ }
+
+ .offset-lg-9 {
+ margin-left: 75%;
+ }
+
+ .offset-lg-10 {
+ margin-left: 83.33333%;
+ }
+
+ .offset-lg-11 {
+ margin-left: 91.66667%;
+ }
+}
+
+@media (min-width: 1200px) {
+ .col-xl {
+ flex-basis: 0;
+ flex-grow: 1;
+ min-width: 0;
+ max-width: 100%;
+ }
+
+ .row-cols-xl-1 > * {
+ flex: 0 0 100%;
+ max-width: 100%;
+ }
+
+ .row-cols-xl-2 > * {
+ flex: 0 0 50%;
+ max-width: 50%;
+ }
+
+ .row-cols-xl-3 > * {
+ flex: 0 0 33.33333%;
+ max-width: 33.33333%;
+ }
+
+ .row-cols-xl-4 > * {
+ flex: 0 0 25%;
+ max-width: 25%;
+ }
+
+ .row-cols-xl-5 > * {
+ flex: 0 0 20%;
+ max-width: 20%;
+ }
+
+ .row-cols-xl-6 > * {
+ flex: 0 0 16.66667%;
+ max-width: 16.66667%;
+ }
+
+ .col-xl-auto {
+ flex: 0 0 auto;
+ width: auto;
+ max-width: 100%;
+ }
+
+ .col-xl-1 {
+ flex: 0 0 8.33333%;
+ max-width: 8.33333%;
+ }
+
+ .col-xl-2 {
+ flex: 0 0 16.66667%;
+ max-width: 16.66667%;
+ }
+
+ .col-xl-3 {
+ flex: 0 0 25%;
+ max-width: 25%;
+ }
+
+ .col-xl-4 {
+ flex: 0 0 33.33333%;
+ max-width: 33.33333%;
+ }
+
+ .col-xl-5 {
+ flex: 0 0 41.66667%;
+ max-width: 41.66667%;
+ }
+
+ .col-xl-6 {
+ flex: 0 0 50%;
+ max-width: 50%;
+ }
+
+ .col-xl-7 {
+ flex: 0 0 58.33333%;
+ max-width: 58.33333%;
+ }
+
+ .col-xl-8 {
+ flex: 0 0 66.66667%;
+ max-width: 66.66667%;
+ }
+
+ .col-xl-9 {
+ flex: 0 0 75%;
+ max-width: 75%;
+ }
+
+ .col-xl-10 {
+ flex: 0 0 83.33333%;
+ max-width: 83.33333%;
+ }
+
+ .col-xl-11 {
+ flex: 0 0 91.66667%;
+ max-width: 91.66667%;
+ }
+
+ .col-xl-12 {
+ flex: 0 0 100%;
+ max-width: 100%;
+ }
+
+ .order-xl-first {
+ order: -1;
+ }
+
+ .order-xl-last {
+ order: 13;
+ }
+
+ .order-xl-0 {
+ order: 0;
+ }
+
+ .order-xl-1 {
+ order: 1;
+ }
+
+ .order-xl-2 {
+ order: 2;
+ }
+
+ .order-xl-3 {
+ order: 3;
+ }
+
+ .order-xl-4 {
+ order: 4;
+ }
+
+ .order-xl-5 {
+ order: 5;
+ }
+
+ .order-xl-6 {
+ order: 6;
+ }
+
+ .order-xl-7 {
+ order: 7;
+ }
+
+ .order-xl-8 {
+ order: 8;
+ }
+
+ .order-xl-9 {
+ order: 9;
+ }
+
+ .order-xl-10 {
+ order: 10;
+ }
+
+ .order-xl-11 {
+ order: 11;
+ }
+
+ .order-xl-12 {
+ order: 12;
+ }
+
+ .offset-xl-0 {
+ margin-left: 0;
+ }
+
+ .offset-xl-1 {
+ margin-left: 8.33333%;
+ }
+
+ .offset-xl-2 {
+ margin-left: 16.66667%;
+ }
+
+ .offset-xl-3 {
+ margin-left: 25%;
+ }
+
+ .offset-xl-4 {
+ margin-left: 33.33333%;
+ }
+
+ .offset-xl-5 {
+ margin-left: 41.66667%;
+ }
+
+ .offset-xl-6 {
+ margin-left: 50%;
+ }
+
+ .offset-xl-7 {
+ margin-left: 58.33333%;
+ }
+
+ .offset-xl-8 {
+ margin-left: 66.66667%;
+ }
+
+ .offset-xl-9 {
+ margin-left: 75%;
+ }
+
+ .offset-xl-10 {
+ margin-left: 83.33333%;
+ }
+
+ .offset-xl-11 {
+ margin-left: 91.66667%;
+ }
+}
+
+.table {
+ width: 100%;
+ margin-bottom: 1rem;
+ color: #212529;
+}
+
+.table th,
+.table td {
+ padding: 0.75rem;
+ vertical-align: top;
+ border-top: 1px solid #dee2e6;
+}
+
+.table thead th {
+ vertical-align: bottom;
+ border-bottom: 2px solid #dee2e6;
+}
+
+.table tbody + tbody {
+ border-top: 2px solid #dee2e6;
+}
+
+.table-sm th,
+.table-sm td {
+ padding: 0.3rem;
+}
+
+.table-bordered {
+ border: 1px solid #dee2e6;
+}
+
+.table-bordered th,
+.table-bordered td {
+ border: 1px solid #dee2e6;
+}
+
+.table-bordered thead th,
+.table-bordered thead td {
+ border-bottom-width: 2px;
+}
+
+.table-borderless th,
+.table-borderless td,
+.table-borderless thead th,
+.table-borderless tbody + tbody {
+ border: 0;
+}
+
+.table-striped tbody tr:nth-of-type(odd) {
+ background-color: rgba(0, 0, 0, 0.05);
+}
+
+.table-hover tbody tr:hover {
+ color: #212529;
+ background-color: rgba(0, 0, 0, 0.075);
+}
+
+.table-primary,
+.table-primary > th,
+.table-primary > td {
+ background-color: #badce3;
+}
+
+.table-primary th,
+.table-primary td,
+.table-primary thead th,
+.table-primary tbody + tbody {
+ border-color: #7ebdcb;
+}
+
+.table-hover .table-primary:hover {
+ background-color: #a8d3dc;
+}
+
+.table-hover .table-primary:hover > td,
+.table-hover .table-primary:hover > th {
+ background-color: #a8d3dc;
+}
+
+.table-secondary,
+.table-secondary > th,
+.table-secondary > td {
+ background-color: #c5c5c6;
+}
+
+.table-secondary th,
+.table-secondary td,
+.table-secondary thead th,
+.table-secondary tbody + tbody {
+ border-color: #949394;
+}
+
+.table-hover .table-secondary:hover {
+ background-color: #b8b8b9;
+}
+
+.table-hover .table-secondary:hover > td,
+.table-hover .table-secondary:hover > th {
+ background-color: #b8b8b9;
+}
+
+.table-success,
+.table-success > th,
+.table-success > td {
+ background-color: #c3e6cb;
+}
+
+.table-success th,
+.table-success td,
+.table-success thead th,
+.table-success tbody + tbody {
+ border-color: #8fd19e;
+}
+
+.table-hover .table-success:hover {
+ background-color: #b1dfbb;
+}
+
+.table-hover .table-success:hover > td,
+.table-hover .table-success:hover > th {
+ background-color: #b1dfbb;
+}
+
+.table-info,
+.table-info > th,
+.table-info > td {
+ background-color: #bee5eb;
+}
+
+.table-info th,
+.table-info td,
+.table-info thead th,
+.table-info tbody + tbody {
+ border-color: #86cfda;
+}
+
+.table-hover .table-info:hover {
+ background-color: #abdde5;
+}
+
+.table-hover .table-info:hover > td,
+.table-hover .table-info:hover > th {
+ background-color: #abdde5;
+}
+
+.table-warning,
+.table-warning > th,
+.table-warning > td {
+ background-color: #ffeeba;
+}
+
+.table-warning th,
+.table-warning td,
+.table-warning thead th,
+.table-warning tbody + tbody {
+ border-color: #ffdf7e;
+}
+
+.table-hover .table-warning:hover {
+ background-color: #ffe8a1;
+}
+
+.table-hover .table-warning:hover > td,
+.table-hover .table-warning:hover > th {
+ background-color: #ffe8a1;
+}
+
+.table-danger,
+.table-danger > th,
+.table-danger > td {
+ background-color: #f5c6cb;
+}
+
+.table-danger th,
+.table-danger td,
+.table-danger thead th,
+.table-danger tbody + tbody {
+ border-color: #ed969e;
+}
+
+.table-hover .table-danger:hover {
+ background-color: #f1b0b7;
+}
+
+.table-hover .table-danger:hover > td,
+.table-hover .table-danger:hover > th {
+ background-color: #f1b0b7;
+}
+
+.table-light,
+.table-light > th,
+.table-light > td {
+ background-color: #fdfdfe;
+}
+
+.table-light th,
+.table-light td,
+.table-light thead th,
+.table-light tbody + tbody {
+ border-color: #fbfcfc;
+}
+
+.table-hover .table-light:hover {
+ background-color: #ececf6;
+}
+
+.table-hover .table-light:hover > td,
+.table-hover .table-light:hover > th {
+ background-color: #ececf6;
+}
+
+.table-dark,
+.table-dark > th,
+.table-dark > td {
+ background-color: #c6c8ca;
+}
+
+.table-dark th,
+.table-dark td,
+.table-dark thead th,
+.table-dark tbody + tbody {
+ border-color: #95999c;
+}
+
+.table-hover .table-dark:hover {
+ background-color: #b9bbbe;
+}
+
+.table-hover .table-dark:hover > td,
+.table-hover .table-dark:hover > th {
+ background-color: #b9bbbe;
+}
+
+.table-active,
+.table-active > th,
+.table-active > td {
+ background-color: rgba(0, 0, 0, 0.075);
+}
+
+.table-hover .table-active:hover {
+ background-color: rgba(0, 0, 0, 0.075);
+}
+
+.table-hover .table-active:hover > td,
+.table-hover .table-active:hover > th {
+ background-color: rgba(0, 0, 0, 0.075);
+}
+
+.table .thead-dark th {
+ color: #fff;
+ background-color: #343a40;
+ border-color: #454d55;
+}
+
+.table .thead-light th {
+ color: #495057;
+ background-color: #e9ecef;
+ border-color: #dee2e6;
+}
+
+.table-dark {
+ color: #fff;
+ background-color: #343a40;
+}
+
+.table-dark th,
+.table-dark td,
+.table-dark thead th {
+ border-color: #454d55;
+}
+
+.table-dark.table-bordered {
+ border: 0;
+}
+
+.table-dark.table-striped tbody tr:nth-of-type(odd) {
+ background-color: rgba(255, 255, 255, 0.05);
+}
+
+.table-dark.table-hover tbody tr:hover {
+ color: #fff;
+ background-color: rgba(255, 255, 255, 0.075);
+}
+
+@media (max-width: 575.98px) {
+ .table-responsive-sm {
+ display: block;
+ width: 100%;
+ overflow-x: auto;
+ -webkit-overflow-scrolling: touch;
+ }
+
+ .table-responsive-sm > .table-bordered {
+ border: 0;
+ }
+}
+
+@media (max-width: 767.98px) {
+ .table-responsive-md {
+ display: block;
+ width: 100%;
+ overflow-x: auto;
+ -webkit-overflow-scrolling: touch;
+ }
+
+ .table-responsive-md > .table-bordered {
+ border: 0;
+ }
+}
+
+@media (max-width: 991.98px) {
+ .table-responsive-lg {
+ display: block;
+ width: 100%;
+ overflow-x: auto;
+ -webkit-overflow-scrolling: touch;
+ }
+
+ .table-responsive-lg > .table-bordered {
+ border: 0;
+ }
+}
+
+@media (max-width: 1199.98px) {
+ .table-responsive-xl {
+ display: block;
+ width: 100%;
+ overflow-x: auto;
+ -webkit-overflow-scrolling: touch;
+ }
+
+ .table-responsive-xl > .table-bordered {
+ border: 0;
+ }
+}
+
+.table-responsive {
+ display: block;
+ width: 100%;
+ overflow-x: auto;
+ -webkit-overflow-scrolling: touch;
+}
+
+.table-responsive > .table-bordered {
+ border: 0;
+}
+
+.form-control {
+ display: block;
+ width: 100%;
+ height: calc(1.5em + 0.75rem + 2px);
+ padding: 0.375rem 0.75rem;
+ font-size: 1rem;
+ font-weight: 400;
+ line-height: 1.5;
+ color: #495057;
+ background-color: #fff;
+ background-clip: padding-box;
+ border: 1px solid #ced4da;
+ border-radius: 0.25rem;
+ transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
+}
+
+.form-control::-ms-expand {
+ background-color: transparent;
+ border: 0;
+}
+
+.form-control:-moz-focusring {
+ color: transparent;
+ text-shadow: 0 0 0 #495057;
+}
+
+.form-control:focus {
+ color: #495057;
+ background-color: #fff;
+ border-color: #2cd2f5;
+ outline: 0;
+ box-shadow: 0 0 0 0.2rem rgba(7, 129, 155, 0.25);
+}
+
+.form-control::placeholder {
+ color: #313032;
+ opacity: 1;
+}
+
+.form-control:disabled,
+.form-control[readonly] {
+ background-color: #e9ecef;
+ opacity: 1;
+}
+
+input[type='date'].form-control,
+input[type='time'].form-control,
+input[type='datetime-local'].form-control,
+input[type='month'].form-control {
+ appearance: none;
+}
+
+select.form-control:focus::-ms-value {
+ color: #495057;
+ background-color: #fff;
+}
+
+.form-control-file,
+.form-control-range {
+ display: block;
+ width: 100%;
+}
+
+.col-form-label {
+ padding-top: calc(0.375rem + 1px);
+ padding-bottom: calc(0.375rem + 1px);
+ margin-bottom: 0;
+ font-size: inherit;
+ line-height: 1.5;
+}
+
+.col-form-label-lg {
+ padding-top: calc(0.5rem + 1px);
+ padding-bottom: calc(0.5rem + 1px);
+ font-size: 1.25rem;
+ line-height: 1.5;
+}
+
+.col-form-label-sm {
+ padding-top: calc(0.25rem + 1px);
+ padding-bottom: calc(0.25rem + 1px);
+ font-size: 0.875rem;
+ line-height: 1.5;
+}
+
+.form-control-plaintext {
+ display: block;
+ width: 100%;
+ padding: 0.375rem 0;
+ margin-bottom: 0;
+ font-size: 1rem;
+ line-height: 1.5;
+ color: #212529;
+ background-color: transparent;
+ border: solid transparent;
+ border-width: 1px 0;
+}
+
+.form-control-plaintext.form-control-sm,
+.form-control-plaintext.form-control-lg {
+ padding-right: 0;
+ padding-left: 0;
+}
+
+.form-control-sm {
+ height: calc(1.5em + 0.5rem + 2px);
+ padding: 0.25rem 0.5rem;
+ font-size: 0.875rem;
+ line-height: 1.5;
+ border-radius: 0.2rem;
+}
+
+.form-control-lg {
+ height: calc(1.5em + 1rem + 2px);
+ padding: 0.5rem 1rem;
+ font-size: 1.25rem;
+ line-height: 1.5;
+ border-radius: 0.3rem;
+}
+
+select.form-control[size],
+select.form-control[multiple] {
+ height: auto;
+}
+
+textarea.form-control {
+ height: auto;
+}
+
+.form-group {
+ margin-bottom: 1rem;
+}
+
+.form-text {
+ display: block;
+ margin-top: 0.25rem;
+}
+
+.form-row {
+ display: flex;
+ flex-wrap: wrap;
+ margin-right: -5px;
+ margin-left: -5px;
+}
+
+.form-row > .col,
+.form-row > [class*='col-'] {
+ padding-right: 5px;
+ padding-left: 5px;
+}
+
+.form-check {
+ position: relative;
+ display: block;
+ padding-left: 1.25rem;
+}
+
+.form-check-input {
+ position: absolute;
+ margin-top: 0.3rem;
+ margin-left: -1.25rem;
+}
+
+.form-check-input[disabled] ~ .form-check-label,
+.form-check-input:disabled ~ .form-check-label {
+ color: #313032;
+}
+
+.form-check-label {
+ margin-bottom: 0;
+}
+
+.form-check-inline {
+ display: inline-flex;
+ align-items: center;
+ padding-left: 0;
+ margin-right: 0.75rem;
+}
+
+.form-check-inline .form-check-input {
+ position: static;
+ margin-top: 0;
+ margin-right: 0.3125rem;
+ margin-left: 0;
+}
+
+.valid-feedback {
+ display: none;
+ width: 100%;
+ margin-top: 0.25rem;
+ font-size: 80%;
+ color: #28a745;
+}
+
+.valid-tooltip {
+ position: absolute;
+ top: 100%;
+ z-index: 5;
+ display: none;
+ max-width: 100%;
+ padding: 0.25rem 0.5rem;
+ margin-top: 0.1rem;
+ font-size: 0.875rem;
+ line-height: 1.5;
+ color: #fff;
+ background-color: rgba(40, 167, 69, 0.9);
+ border-radius: 0.25rem;
+}
+
+.was-validated :valid ~ .valid-feedback,
+.was-validated :valid ~ .valid-tooltip,
+.is-valid ~ .valid-feedback,
+.is-valid ~ .valid-tooltip {
+ display: block;
+}
+
+.was-validated .form-control:valid,
+.form-control.is-valid {
+ border-color: #28a745;
+ padding-right: calc(1.5em + 0.75rem);
+ background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath fill='%2328a745' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");
+ background-repeat: no-repeat;
+ background-position: right calc(0.375em + 0.1875rem) center;
+ background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);
+}
+
+.was-validated .form-control:valid:focus,
+.form-control.is-valid:focus {
+ border-color: #28a745;
+ box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.25);
+}
+
+.was-validated textarea.form-control:valid,
+textarea.form-control.is-valid {
+ padding-right: calc(1.5em + 0.75rem);
+ background-position: top calc(0.375em + 0.1875rem) right calc(0.375em + 0.1875rem);
+}
+
+.was-validated .custom-select:valid,
+.custom-select.is-valid {
+ border-color: #28a745;
+ padding-right: calc(0.75em + 2.3125rem);
+ background: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='5' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") no-repeat right 0.75rem center/8px 10px,
+ url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath fill='%2328a745' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e") #fff no-repeat center right 1.75rem / calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);
+}
+
+.was-validated .custom-select:valid:focus,
+.custom-select.is-valid:focus {
+ border-color: #28a745;
+ box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.25);
+}
+
+.was-validated .form-check-input:valid ~ .form-check-label,
+.form-check-input.is-valid ~ .form-check-label {
+ color: #28a745;
+}
+
+.was-validated .form-check-input:valid ~ .valid-feedback,
+.was-validated .form-check-input:valid ~ .valid-tooltip,
+.form-check-input.is-valid ~ .valid-feedback,
+.form-check-input.is-valid ~ .valid-tooltip {
+ display: block;
+}
+
+.was-validated .custom-control-input:valid ~ .custom-control-label,
+.custom-control-input.is-valid ~ .custom-control-label {
+ color: #28a745;
+}
+
+.was-validated .custom-control-input:valid ~ .custom-control-label::before,
+.custom-control-input.is-valid ~ .custom-control-label::before {
+ border-color: #28a745;
+}
+
+.was-validated .custom-control-input:valid:checked ~ .custom-control-label::before,
+.custom-control-input.is-valid:checked ~ .custom-control-label::before {
+ border-color: #34ce57;
+ background-color: #34ce57;
+}
+
+.was-validated .custom-control-input:valid:focus ~ .custom-control-label::before,
+.custom-control-input.is-valid:focus ~ .custom-control-label::before {
+ box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.25);
+}
+
+.was-validated .custom-control-input:valid:focus:not(:checked) ~ .custom-control-label::before,
+.custom-control-input.is-valid:focus:not(:checked) ~ .custom-control-label::before {
+ border-color: #28a745;
+}
+
+.was-validated .custom-file-input:valid ~ .custom-file-label,
+.custom-file-input.is-valid ~ .custom-file-label {
+ border-color: #28a745;
+}
+
+.was-validated .custom-file-input:valid:focus ~ .custom-file-label,
+.custom-file-input.is-valid:focus ~ .custom-file-label {
+ border-color: #28a745;
+ box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.25);
+}
+
+.invalid-feedback {
+ display: none;
+ width: 100%;
+ margin-top: 0.25rem;
+ font-size: 80%;
+ color: #dc3545;
+}
+
+.invalid-tooltip {
+ position: absolute;
+ top: 100%;
+ z-index: 5;
+ display: none;
+ max-width: 100%;
+ padding: 0.25rem 0.5rem;
+ margin-top: 0.1rem;
+ font-size: 0.875rem;
+ line-height: 1.5;
+ color: #fff;
+ background-color: rgba(220, 53, 69, 0.9);
+ border-radius: 0.25rem;
+}
+
+.was-validated :invalid ~ .invalid-feedback,
+.was-validated :invalid ~ .invalid-tooltip,
+.is-invalid ~ .invalid-feedback,
+.is-invalid ~ .invalid-tooltip {
+ display: block;
+}
+
+.was-validated .form-control:invalid,
+.form-control.is-invalid {
+ border-color: #dc3545;
+ padding-right: calc(1.5em + 0.75rem);
+ background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%23dc3545' viewBox='0 0 12 12'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");
+ background-repeat: no-repeat;
+ background-position: right calc(0.375em + 0.1875rem) center;
+ background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);
+}
+
+.was-validated .form-control:invalid:focus,
+.form-control.is-invalid:focus {
+ border-color: #dc3545;
+ box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25);
+}
+
+.was-validated textarea.form-control:invalid,
+textarea.form-control.is-invalid {
+ padding-right: calc(1.5em + 0.75rem);
+ background-position: top calc(0.375em + 0.1875rem) right calc(0.375em + 0.1875rem);
+}
+
+.was-validated .custom-select:invalid,
+.custom-select.is-invalid {
+ border-color: #dc3545;
+ padding-right: calc(0.75em + 2.3125rem);
+ background: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='5' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") no-repeat right 0.75rem center/8px 10px,
+ url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%23dc3545' viewBox='0 0 12 12'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e") #fff no-repeat center right 1.75rem / calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);
+}
+
+.was-validated .custom-select:invalid:focus,
+.custom-select.is-invalid:focus {
+ border-color: #dc3545;
+ box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25);
+}
+
+.was-validated .form-check-input:invalid ~ .form-check-label,
+.form-check-input.is-invalid ~ .form-check-label {
+ color: #dc3545;
+}
+
+.was-validated .form-check-input:invalid ~ .invalid-feedback,
+.was-validated .form-check-input:invalid ~ .invalid-tooltip,
+.form-check-input.is-invalid ~ .invalid-feedback,
+.form-check-input.is-invalid ~ .invalid-tooltip {
+ display: block;
+}
+
+.was-validated .custom-control-input:invalid ~ .custom-control-label,
+.custom-control-input.is-invalid ~ .custom-control-label {
+ color: #dc3545;
+}
+
+.was-validated .custom-control-input:invalid ~ .custom-control-label::before,
+.custom-control-input.is-invalid ~ .custom-control-label::before {
+ border-color: #dc3545;
+}
+
+.was-validated .custom-control-input:invalid:checked ~ .custom-control-label::before,
+.custom-control-input.is-invalid:checked ~ .custom-control-label::before {
+ border-color: #e4606d;
+ background-color: #e4606d;
+}
+
+.was-validated .custom-control-input:invalid:focus ~ .custom-control-label::before,
+.custom-control-input.is-invalid:focus ~ .custom-control-label::before {
+ box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25);
+}
+
+.was-validated .custom-control-input:invalid:focus:not(:checked) ~ .custom-control-label::before,
+.custom-control-input.is-invalid:focus:not(:checked) ~ .custom-control-label::before {
+ border-color: #dc3545;
+}
+
+.was-validated .custom-file-input:invalid ~ .custom-file-label,
+.custom-file-input.is-invalid ~ .custom-file-label {
+ border-color: #dc3545;
+}
+
+.was-validated .custom-file-input:invalid:focus ~ .custom-file-label,
+.custom-file-input.is-invalid:focus ~ .custom-file-label {
+ border-color: #dc3545;
+ box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25);
+}
+
+.form-inline {
+ display: flex;
+ flex-flow: row wrap;
+ align-items: center;
+}
+
+.form-inline .form-check {
+ width: 100%;
+}
+
+@media (min-width: 576px) {
+ .form-inline label {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ margin-bottom: 0;
+ }
+
+ .form-inline .form-group {
+ display: flex;
+ flex: 0 0 auto;
+ flex-flow: row wrap;
+ align-items: center;
+ margin-bottom: 0;
+ }
+
+ .form-inline .form-control {
+ display: inline-block;
+ width: auto;
+ vertical-align: middle;
+ }
+
+ .form-inline .form-control-plaintext {
+ display: inline-block;
+ }
+
+ .form-inline .input-group,
+ .form-inline .custom-select {
+ width: auto;
+ }
+
+ .form-inline .form-check {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: auto;
+ padding-left: 0;
+ }
+
+ .form-inline .form-check-input {
+ position: relative;
+ flex-shrink: 0;
+ margin-top: 0;
+ margin-right: 0.25rem;
+ margin-left: 0;
+ }
+
+ .form-inline .custom-control {
+ align-items: center;
+ justify-content: center;
+ }
+
+ .form-inline .custom-control-label {
+ margin-bottom: 0;
+ }
+}
+
+.btn {
+ display: inline-block;
+ font-weight: 400;
+ color: #212529;
+ text-align: center;
+ vertical-align: middle;
+ user-select: none;
+ background-color: transparent;
+ border: 1px solid transparent;
+ padding: 0.375rem 0.75rem;
+ font-size: 1rem;
+ line-height: 1.5;
+ border-radius: 0.25rem;
+ transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out,
+ box-shadow 0.15s ease-in-out;
+}
+
+.btn:hover {
+ color: #212529;
+ text-decoration: none;
+}
+
+.btn:focus,
+.btn.focus {
+ outline: 0;
+ box-shadow: 0 0 0 0.2rem rgba(7, 129, 155, 0.25);
+}
+
+.btn.disabled,
+.btn:disabled {
+ opacity: 0.65;
+}
+
+.btn:not(:disabled):not(.disabled) {
+ cursor: pointer;
+}
+
+a.btn.disabled,
+fieldset:disabled a.btn {
+ pointer-events: none;
+}
+
+.btn-primary {
+ color: #fff;
+ background-color: #07819b;
+ border-color: #07819b;
+}
+
+.btn-primary:hover {
+ color: #fff;
+ background-color: #056376;
+ border-color: #05586a;
+}
+
+.btn-primary:focus,
+.btn-primary.focus {
+ color: #fff;
+ background-color: #056376;
+ border-color: #05586a;
+ box-shadow: 0 0 0 0.2rem rgba(44, 148, 170, 0.5);
+}
+
+.btn-primary.disabled,
+.btn-primary:disabled {
+ color: #fff;
+ background-color: #07819b;
+ border-color: #07819b;
+}
+
+.btn-primary:not(:disabled):not(.disabled):active,
+.btn-primary:not(:disabled):not(.disabled).active,
+.show > .btn-primary.dropdown-toggle {
+ color: #fff;
+ background-color: #05586a;
+ border-color: #044e5e;
+}
+
+.btn-primary:not(:disabled):not(.disabled):active:focus,
+.btn-primary:not(:disabled):not(.disabled).active:focus,
+.show > .btn-primary.dropdown-toggle:focus {
+ box-shadow: 0 0 0 0.2rem rgba(44, 148, 170, 0.5);
+}
+
+.btn-secondary {
+ color: #fff;
+ background-color: #313032;
+ border-color: #313032;
+}
+
+.btn-secondary:hover {
+ color: #fff;
+ background-color: #1e1d1e;
+ border-color: #181718;
+}
+
+.btn-secondary:focus,
+.btn-secondary.focus {
+ color: #fff;
+ background-color: #1e1d1e;
+ border-color: #181718;
+ box-shadow: 0 0 0 0.2rem rgba(80, 79, 81, 0.5);
+}
+
+.btn-secondary.disabled,
+.btn-secondary:disabled {
+ color: #fff;
+ background-color: #313032;
+ border-color: #313032;
+}
+
+.btn-secondary:not(:disabled):not(.disabled):active,
+.btn-secondary:not(:disabled):not(.disabled).active,
+.show > .btn-secondary.dropdown-toggle {
+ color: #fff;
+ background-color: #181718;
+ border-color: #111111;
+}
+
+.btn-secondary:not(:disabled):not(.disabled):active:focus,
+.btn-secondary:not(:disabled):not(.disabled).active:focus,
+.show > .btn-secondary.dropdown-toggle:focus {
+ box-shadow: 0 0 0 0.2rem rgba(80, 79, 81, 0.5);
+}
+
+.btn-success {
+ color: #fff;
+ background-color: #28a745;
+ border-color: #28a745;
+}
+
+.btn-success:hover {
+ color: #fff;
+ background-color: #218838;
+ border-color: #1e7e34;
+}
+
+.btn-success:focus,
+.btn-success.focus {
+ color: #fff;
+ background-color: #218838;
+ border-color: #1e7e34;
+ box-shadow: 0 0 0 0.2rem rgba(72, 180, 97, 0.5);
+}
+
+.btn-success.disabled,
+.btn-success:disabled {
+ color: #fff;
+ background-color: #28a745;
+ border-color: #28a745;
+}
+
+.btn-success:not(:disabled):not(.disabled):active,
+.btn-success:not(:disabled):not(.disabled).active,
+.show > .btn-success.dropdown-toggle {
+ color: #fff;
+ background-color: #1e7e34;
+ border-color: #1c7430;
+}
+
+.btn-success:not(:disabled):not(.disabled):active:focus,
+.btn-success:not(:disabled):not(.disabled).active:focus,
+.show > .btn-success.dropdown-toggle:focus {
+ box-shadow: 0 0 0 0.2rem rgba(72, 180, 97, 0.5);
+}
+
+.btn-info {
+ color: #fff;
+ background-color: #17a2b8;
+ border-color: #17a2b8;
+}
+
+.btn-info:hover {
+ color: #fff;
+ background-color: #138496;
+ border-color: #117a8b;
+}
+
+.btn-info:focus,
+.btn-info.focus {
+ color: #fff;
+ background-color: #138496;
+ border-color: #117a8b;
+ box-shadow: 0 0 0 0.2rem rgba(58, 176, 195, 0.5);
+}
+
+.btn-info.disabled,
+.btn-info:disabled {
+ color: #fff;
+ background-color: #17a2b8;
+ border-color: #17a2b8;
+}
+
+.btn-info:not(:disabled):not(.disabled):active,
+.btn-info:not(:disabled):not(.disabled).active,
+.show > .btn-info.dropdown-toggle {
+ color: #fff;
+ background-color: #117a8b;
+ border-color: #10707f;
+}
+
+.btn-info:not(:disabled):not(.disabled):active:focus,
+.btn-info:not(:disabled):not(.disabled).active:focus,
+.show > .btn-info.dropdown-toggle:focus {
+ box-shadow: 0 0 0 0.2rem rgba(58, 176, 195, 0.5);
+}
+
+.btn-warning {
+ color: #212529;
+ background-color: #ffc107;
+ border-color: #ffc107;
+}
+
+.btn-warning:hover {
+ color: #212529;
+ background-color: #e0a800;
+ border-color: #d39e00;
+}
+
+.btn-warning:focus,
+.btn-warning.focus {
+ color: #212529;
+ background-color: #e0a800;
+ border-color: #d39e00;
+ box-shadow: 0 0 0 0.2rem rgba(222, 170, 12, 0.5);
+}
+
+.btn-warning.disabled,
+.btn-warning:disabled {
+ color: #212529;
+ background-color: #ffc107;
+ border-color: #ffc107;
+}
+
+.btn-warning:not(:disabled):not(.disabled):active,
+.btn-warning:not(:disabled):not(.disabled).active,
+.show > .btn-warning.dropdown-toggle {
+ color: #212529;
+ background-color: #d39e00;
+ border-color: #c69500;
+}
+
+.btn-warning:not(:disabled):not(.disabled):active:focus,
+.btn-warning:not(:disabled):not(.disabled).active:focus,
+.show > .btn-warning.dropdown-toggle:focus {
+ box-shadow: 0 0 0 0.2rem rgba(222, 170, 12, 0.5);
+}
+
+.btn-danger {
+ color: #fff;
+ background-color: #dc3545;
+ border-color: #dc3545;
+}
+
+.btn-danger:hover {
+ color: #fff;
+ background-color: #c82333;
+ border-color: #bd2130;
+}
+
+.btn-danger:focus,
+.btn-danger.focus {
+ color: #fff;
+ background-color: #c82333;
+ border-color: #bd2130;
+ box-shadow: 0 0 0 0.2rem rgba(225, 83, 97, 0.5);
+}
+
+.btn-danger.disabled,
+.btn-danger:disabled {
+ color: #fff;
+ background-color: #dc3545;
+ border-color: #dc3545;
+}
+
+.btn-danger:not(:disabled):not(.disabled):active,
+.btn-danger:not(:disabled):not(.disabled).active,
+.show > .btn-danger.dropdown-toggle {
+ color: #fff;
+ background-color: #bd2130;
+ border-color: #b21f2d;
+}
+
+.btn-danger:not(:disabled):not(.disabled):active:focus,
+.btn-danger:not(:disabled):not(.disabled).active:focus,
+.show > .btn-danger.dropdown-toggle:focus {
+ box-shadow: 0 0 0 0.2rem rgba(225, 83, 97, 0.5);
+}
+
+.btn-light {
+ color: #212529;
+ background-color: #f8f9fa;
+ border-color: #f8f9fa;
+}
+
+.btn-light:hover {
+ color: #212529;
+ background-color: #e2e6ea;
+ border-color: #dae0e5;
+}
+
+.btn-light:focus,
+.btn-light.focus {
+ color: #212529;
+ background-color: #e2e6ea;
+ border-color: #dae0e5;
+ box-shadow: 0 0 0 0.2rem rgba(216, 217, 219, 0.5);
+}
+
+.btn-light.disabled,
+.btn-light:disabled {
+ color: #212529;
+ background-color: #f8f9fa;
+ border-color: #f8f9fa;
+}
+
+.btn-light:not(:disabled):not(.disabled):active,
+.btn-light:not(:disabled):not(.disabled).active,
+.show > .btn-light.dropdown-toggle {
+ color: #212529;
+ background-color: #dae0e5;
+ border-color: #d3d9df;
+}
+
+.btn-light:not(:disabled):not(.disabled):active:focus,
+.btn-light:not(:disabled):not(.disabled).active:focus,
+.show > .btn-light.dropdown-toggle:focus {
+ box-shadow: 0 0 0 0.2rem rgba(216, 217, 219, 0.5);
+}
+
+.btn-dark {
+ color: #fff;
+ background-color: #343a40;
+ border-color: #343a40;
+}
+
+.btn-dark:hover {
+ color: #fff;
+ background-color: #23272b;
+ border-color: #1d2124;
+}
+
+.btn-dark:focus,
+.btn-dark.focus {
+ color: #fff;
+ background-color: #23272b;
+ border-color: #1d2124;
+ box-shadow: 0 0 0 0.2rem rgba(82, 88, 93, 0.5);
+}
+
+.btn-dark.disabled,
+.btn-dark:disabled {
+ color: #fff;
+ background-color: #343a40;
+ border-color: #343a40;
+}
+
+.btn-dark:not(:disabled):not(.disabled):active,
+.btn-dark:not(:disabled):not(.disabled).active,
+.show > .btn-dark.dropdown-toggle {
+ color: #fff;
+ background-color: #1d2124;
+ border-color: #171a1d;
+}
+
+.btn-dark:not(:disabled):not(.disabled):active:focus,
+.btn-dark:not(:disabled):not(.disabled).active:focus,
+.show > .btn-dark.dropdown-toggle:focus {
+ box-shadow: 0 0 0 0.2rem rgba(82, 88, 93, 0.5);
+}
+
+.btn-outline-primary {
+ color: #07819b;
+ border-color: #07819b;
+}
+
+.btn-outline-primary:hover {
+ color: #fff;
+ background-color: #07819b;
+ border-color: #07819b;
+}
+
+.btn-outline-primary:focus,
+.btn-outline-primary.focus {
+ box-shadow: 0 0 0 0.2rem rgba(7, 129, 155, 0.5);
+}
+
+.btn-outline-primary.disabled,
+.btn-outline-primary:disabled {
+ color: #07819b;
+ background-color: transparent;
+}
+
+.btn-outline-primary:not(:disabled):not(.disabled):active,
+.btn-outline-primary:not(:disabled):not(.disabled).active,
+.show > .btn-outline-primary.dropdown-toggle {
+ color: #fff;
+ background-color: #07819b;
+ border-color: #07819b;
+}
+
+.btn-outline-primary:not(:disabled):not(.disabled):active:focus,
+.btn-outline-primary:not(:disabled):not(.disabled).active:focus,
+.show > .btn-outline-primary.dropdown-toggle:focus {
+ box-shadow: 0 0 0 0.2rem rgba(7, 129, 155, 0.5);
+}
+
+.btn-outline-secondary {
+ color: #313032;
+ border-color: #313032;
+}
+
+.btn-outline-secondary:hover {
+ color: #fff;
+ background-color: #313032;
+ border-color: #313032;
+}
+
+.btn-outline-secondary:focus,
+.btn-outline-secondary.focus {
+ box-shadow: 0 0 0 0.2rem rgba(49, 48, 50, 0.5);
+}
+
+.btn-outline-secondary.disabled,
+.btn-outline-secondary:disabled {
+ color: #313032;
+ background-color: transparent;
+}
+
+.btn-outline-secondary:not(:disabled):not(.disabled):active,
+.btn-outline-secondary:not(:disabled):not(.disabled).active,
+.show > .btn-outline-secondary.dropdown-toggle {
+ color: #fff;
+ background-color: #313032;
+ border-color: #313032;
+}
+
+.btn-outline-secondary:not(:disabled):not(.disabled):active:focus,
+.btn-outline-secondary:not(:disabled):not(.disabled).active:focus,
+.show > .btn-outline-secondary.dropdown-toggle:focus {
+ box-shadow: 0 0 0 0.2rem rgba(49, 48, 50, 0.5);
+}
+
+.btn-outline-success {
+ color: #28a745;
+ border-color: #28a745;
+}
+
+.btn-outline-success:hover {
+ color: #fff;
+ background-color: #28a745;
+ border-color: #28a745;
+}
+
+.btn-outline-success:focus,
+.btn-outline-success.focus {
+ box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.5);
+}
+
+.btn-outline-success.disabled,
+.btn-outline-success:disabled {
+ color: #28a745;
+ background-color: transparent;
+}
+
+.btn-outline-success:not(:disabled):not(.disabled):active,
+.btn-outline-success:not(:disabled):not(.disabled).active,
+.show > .btn-outline-success.dropdown-toggle {
+ color: #fff;
+ background-color: #28a745;
+ border-color: #28a745;
+}
+
+.btn-outline-success:not(:disabled):not(.disabled):active:focus,
+.btn-outline-success:not(:disabled):not(.disabled).active:focus,
+.show > .btn-outline-success.dropdown-toggle:focus {
+ box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.5);
+}
+
+.btn-outline-info {
+ color: #17a2b8;
+ border-color: #17a2b8;
+}
+
+.btn-outline-info:hover {
+ color: #fff;
+ background-color: #17a2b8;
+ border-color: #17a2b8;
+}
+
+.btn-outline-info:focus,
+.btn-outline-info.focus {
+ box-shadow: 0 0 0 0.2rem rgba(23, 162, 184, 0.5);
+}
+
+.btn-outline-info.disabled,
+.btn-outline-info:disabled {
+ color: #17a2b8;
+ background-color: transparent;
+}
+
+.btn-outline-info:not(:disabled):not(.disabled):active,
+.btn-outline-info:not(:disabled):not(.disabled).active,
+.show > .btn-outline-info.dropdown-toggle {
+ color: #fff;
+ background-color: #17a2b8;
+ border-color: #17a2b8;
+}
+
+.btn-outline-info:not(:disabled):not(.disabled):active:focus,
+.btn-outline-info:not(:disabled):not(.disabled).active:focus,
+.show > .btn-outline-info.dropdown-toggle:focus {
+ box-shadow: 0 0 0 0.2rem rgba(23, 162, 184, 0.5);
+}
+
+.btn-outline-warning {
+ color: #ffc107;
+ border-color: #ffc107;
+}
+
+.btn-outline-warning:hover {
+ color: #212529;
+ background-color: #ffc107;
+ border-color: #ffc107;
+}
+
+.btn-outline-warning:focus,
+.btn-outline-warning.focus {
+ box-shadow: 0 0 0 0.2rem rgba(255, 193, 7, 0.5);
+}
+
+.btn-outline-warning.disabled,
+.btn-outline-warning:disabled {
+ color: #ffc107;
+ background-color: transparent;
+}
+
+.btn-outline-warning:not(:disabled):not(.disabled):active,
+.btn-outline-warning:not(:disabled):not(.disabled).active,
+.show > .btn-outline-warning.dropdown-toggle {
+ color: #212529;
+ background-color: #ffc107;
+ border-color: #ffc107;
+}
+
+.btn-outline-warning:not(:disabled):not(.disabled):active:focus,
+.btn-outline-warning:not(:disabled):not(.disabled).active:focus,
+.show > .btn-outline-warning.dropdown-toggle:focus {
+ box-shadow: 0 0 0 0.2rem rgba(255, 193, 7, 0.5);
+}
+
+.btn-outline-danger {
+ color: #dc3545;
+ border-color: #dc3545;
+}
+
+.btn-outline-danger:hover {
+ color: #fff;
+ background-color: #dc3545;
+ border-color: #dc3545;
+}
+
+.btn-outline-danger:focus,
+.btn-outline-danger.focus {
+ box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.5);
+}
+
+.btn-outline-danger.disabled,
+.btn-outline-danger:disabled {
+ color: #dc3545;
+ background-color: transparent;
+}
+
+.btn-outline-danger:not(:disabled):not(.disabled):active,
+.btn-outline-danger:not(:disabled):not(.disabled).active,
+.show > .btn-outline-danger.dropdown-toggle {
+ color: #fff;
+ background-color: #dc3545;
+ border-color: #dc3545;
+}
+
+.btn-outline-danger:not(:disabled):not(.disabled):active:focus,
+.btn-outline-danger:not(:disabled):not(.disabled).active:focus,
+.show > .btn-outline-danger.dropdown-toggle:focus {
+ box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.5);
+}
+
+.btn-outline-light {
+ color: #f8f9fa;
+ border-color: #f8f9fa;
+}
+
+.btn-outline-light:hover {
+ color: #212529;
+ background-color: #f8f9fa;
+ border-color: #f8f9fa;
+}
+
+.btn-outline-light:focus,
+.btn-outline-light.focus {
+ box-shadow: 0 0 0 0.2rem rgba(248, 249, 250, 0.5);
+}
+
+.btn-outline-light.disabled,
+.btn-outline-light:disabled {
+ color: #f8f9fa;
+ background-color: transparent;
+}
+
+.btn-outline-light:not(:disabled):not(.disabled):active,
+.btn-outline-light:not(:disabled):not(.disabled).active,
+.show > .btn-outline-light.dropdown-toggle {
+ color: #212529;
+ background-color: #f8f9fa;
+ border-color: #f8f9fa;
+}
+
+.btn-outline-light:not(:disabled):not(.disabled):active:focus,
+.btn-outline-light:not(:disabled):not(.disabled).active:focus,
+.show > .btn-outline-light.dropdown-toggle:focus {
+ box-shadow: 0 0 0 0.2rem rgba(248, 249, 250, 0.5);
+}
+
+.btn-outline-dark {
+ color: #343a40;
+ border-color: #343a40;
+}
+
+.btn-outline-dark:hover {
+ color: #fff;
+ background-color: #343a40;
+ border-color: #343a40;
+}
+
+.btn-outline-dark:focus,
+.btn-outline-dark.focus {
+ box-shadow: 0 0 0 0.2rem rgba(52, 58, 64, 0.5);
+}
+
+.btn-outline-dark.disabled,
+.btn-outline-dark:disabled {
+ color: #343a40;
+ background-color: transparent;
+}
+
+.btn-outline-dark:not(:disabled):not(.disabled):active,
+.btn-outline-dark:not(:disabled):not(.disabled).active,
+.show > .btn-outline-dark.dropdown-toggle {
+ color: #fff;
+ background-color: #343a40;
+ border-color: #343a40;
+}
+
+.btn-outline-dark:not(:disabled):not(.disabled):active:focus,
+.btn-outline-dark:not(:disabled):not(.disabled).active:focus,
+.show > .btn-outline-dark.dropdown-toggle:focus {
+ box-shadow: 0 0 0 0.2rem rgba(52, 58, 64, 0.5);
+}
+
+.btn-link {
+ font-weight: 400;
+ color: #07819b;
+ text-decoration: none;
+}
+
+.btn-link:hover {
+ color: #044452;
+ text-decoration: underline;
+}
+
+.btn-link:focus,
+.btn-link.focus {
+ text-decoration: underline;
+}
+
+.btn-link:disabled,
+.btn-link.disabled {
+ color: #313032;
+ pointer-events: none;
+}
+
+.btn-lg,
+.btn-group-lg > .btn {
+ padding: 0.5rem 1rem;
+ font-size: 1.25rem;
+ line-height: 1.5;
+ border-radius: 0.3rem;
+}
+
+.btn-sm,
+.btn-group-sm > .btn {
+ padding: 0.25rem 0.5rem;
+ font-size: 0.875rem;
+ line-height: 1.5;
+ border-radius: 0.2rem;
+}
+
+.btn-block {
+ display: block;
+ width: 100%;
+}
+
+.btn-block + .btn-block {
+ margin-top: 0.5rem;
+}
+
+input[type='submit'].btn-block,
+input[type='reset'].btn-block,
+input[type='button'].btn-block {
+ width: 100%;
+}
+
+.fade {
+ transition: opacity 0.15s linear;
+}
+
+.fade:not(.show) {
+ opacity: 0;
+}
+
+.collapse:not(.show) {
+ display: none;
+}
+
+.collapsing {
+ position: relative;
+ height: 0;
+ overflow: hidden;
+ transition: height 0.35s ease;
+}
+
+.dropup,
+.dropright,
+.dropdown,
+.dropleft {
+ position: relative;
+}
+
+.dropdown-toggle {
+ white-space: nowrap;
+}
+
+.dropdown-toggle::after {
+ display: inline-block;
+ margin-left: 0.255em;
+ vertical-align: 0.255em;
+ content: '';
+ border-top: 0.3em solid;
+ border-right: 0.3em solid transparent;
+ border-bottom: 0;
+ border-left: 0.3em solid transparent;
+}
+
+.dropdown-toggle:empty::after {
+ margin-left: 0;
+}
+
+.dropdown-menu {
+ position: absolute;
+ top: 100%;
+ left: 0;
+ z-index: 1000;
+ display: none;
+ float: left;
+ min-width: 10rem;
+ padding: 0.5rem 0;
+ margin: 0.125rem 0 0;
+ font-size: 1rem;
+ color: #212529;
+ text-align: left;
+ list-style: none;
+ background-color: #fff;
+ background-clip: padding-box;
+ border: 1px solid rgba(0, 0, 0, 0.15);
+ border-radius: 0.25rem;
+}
+
+.dropdown-menu-left {
+ right: auto;
+ left: 0;
+}
+
+.dropdown-menu-right {
+ right: 0;
+ left: auto;
+}
+
+@media (min-width: 576px) {
+ .dropdown-menu-sm-left {
+ right: auto;
+ left: 0;
+ }
+
+ .dropdown-menu-sm-right {
+ right: 0;
+ left: auto;
+ }
+}
+
+@media (min-width: 768px) {
+ .dropdown-menu-md-left {
+ right: auto;
+ left: 0;
+ }
+
+ .dropdown-menu-md-right {
+ right: 0;
+ left: auto;
+ }
+}
+
+@media (min-width: 992px) {
+ .dropdown-menu-lg-left {
+ right: auto;
+ left: 0;
+ }
+
+ .dropdown-menu-lg-right {
+ right: 0;
+ left: auto;
+ }
+}
+
+@media (min-width: 1200px) {
+ .dropdown-menu-xl-left {
+ right: auto;
+ left: 0;
+ }
+
+ .dropdown-menu-xl-right {
+ right: 0;
+ left: auto;
+ }
+}
+
+.dropup .dropdown-menu {
+ top: auto;
+ bottom: 100%;
+ margin-top: 0;
+ margin-bottom: 0.125rem;
+}
+
+.dropup .dropdown-toggle::after {
+ display: inline-block;
+ margin-left: 0.255em;
+ vertical-align: 0.255em;
+ content: '';
+ border-top: 0;
+ border-right: 0.3em solid transparent;
+ border-bottom: 0.3em solid;
+ border-left: 0.3em solid transparent;
+}
+
+.dropup .dropdown-toggle:empty::after {
+ margin-left: 0;
+}
+
+.dropright .dropdown-menu {
+ top: 0;
+ right: auto;
+ left: 100%;
+ margin-top: 0;
+ margin-left: 0.125rem;
+}
+
+.dropright .dropdown-toggle::after {
+ display: inline-block;
+ margin-left: 0.255em;
+ vertical-align: 0.255em;
+ content: '';
+ border-top: 0.3em solid transparent;
+ border-right: 0;
+ border-bottom: 0.3em solid transparent;
+ border-left: 0.3em solid;
+}
+
+.dropright .dropdown-toggle:empty::after {
+ margin-left: 0;
+}
+
+.dropright .dropdown-toggle::after {
+ vertical-align: 0;
+}
+
+.dropleft .dropdown-menu {
+ top: 0;
+ right: 100%;
+ left: auto;
+ margin-top: 0;
+ margin-right: 0.125rem;
+}
+
+.dropleft .dropdown-toggle::after {
+ display: inline-block;
+ margin-left: 0.255em;
+ vertical-align: 0.255em;
+ content: '';
+}
+
+.dropleft .dropdown-toggle::after {
+ display: none;
+}
+
+.dropleft .dropdown-toggle::before {
+ display: inline-block;
+ margin-right: 0.255em;
+ vertical-align: 0.255em;
+ content: '';
+ border-top: 0.3em solid transparent;
+ border-right: 0.3em solid;
+ border-bottom: 0.3em solid transparent;
+}
+
+.dropleft .dropdown-toggle:empty::after {
+ margin-left: 0;
+}
+
+.dropleft .dropdown-toggle::before {
+ vertical-align: 0;
+}
+
+.dropdown-menu[x-placement^='top'],
+.dropdown-menu[x-placement^='right'],
+.dropdown-menu[x-placement^='bottom'],
+.dropdown-menu[x-placement^='left'] {
+ right: auto;
+ bottom: auto;
+}
+
+.dropdown-divider {
+ height: 0;
+ margin: 0.5rem 0;
+ overflow: hidden;
+ border-top: 1px solid #e9ecef;
+}
+
+.dropdown-item {
+ display: block;
+ width: 100%;
+ padding: 0.25rem 1.5rem;
+ clear: both;
+ font-weight: 400;
+ color: #212529;
+ text-align: inherit;
+ white-space: nowrap;
+ background-color: transparent;
+ border: 0;
+}
+
+.dropdown-item:hover,
+.dropdown-item:focus {
+ color: #16181b;
+ text-decoration: none;
+ background-color: #f8f9fa;
+}
+
+.dropdown-item.active,
+.dropdown-item:active {
+ color: #fff;
+ text-decoration: none;
+ background-color: #07819b;
+}
+
+.dropdown-item.disabled,
+.dropdown-item:disabled {
+ color: #313032;
+ pointer-events: none;
+ background-color: transparent;
+}
+
+.dropdown-menu.show {
+ display: block;
+}
+
+.dropdown-header {
+ display: block;
+ padding: 0.5rem 1.5rem;
+ margin-bottom: 0;
+ font-size: 0.875rem;
+ color: #313032;
+ white-space: nowrap;
+}
+
+.dropdown-item-text {
+ display: block;
+ padding: 0.25rem 1.5rem;
+ color: #212529;
+}
+
+.btn-group,
+.btn-group-vertical {
+ position: relative;
+ display: inline-flex;
+ vertical-align: middle;
+}
+
+.btn-group > .btn,
+.btn-group-vertical > .btn {
+ position: relative;
+ flex: 1 1 auto;
+}
+
+.btn-group > .btn:hover,
+.btn-group-vertical > .btn:hover {
+ z-index: 1;
+}
+
+.btn-group > .btn:focus,
+.btn-group > .btn:active,
+.btn-group > .btn.active,
+.btn-group-vertical > .btn:focus,
+.btn-group-vertical > .btn:active,
+.btn-group-vertical > .btn.active {
+ z-index: 1;
+}
+
+.btn-toolbar {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: flex-start;
+}
+
+.btn-toolbar .input-group {
+ width: auto;
+}
+
+.btn-group > .btn:not(:first-child),
+.btn-group > .btn-group:not(:first-child) {
+ margin-left: -1px;
+}
+
+.btn-group > .btn:not(:last-child):not(.dropdown-toggle),
+.btn-group > .btn-group:not(:last-child) > .btn {
+ border-top-right-radius: 0;
+ border-bottom-right-radius: 0;
+}
+
+.btn-group > .btn:not(:first-child),
+.btn-group > .btn-group:not(:first-child) > .btn {
+ border-top-left-radius: 0;
+ border-bottom-left-radius: 0;
+}
+
+.dropdown-toggle-split {
+ padding-right: 0.5625rem;
+ padding-left: 0.5625rem;
+}
+
+.dropdown-toggle-split::after,
+.dropup .dropdown-toggle-split::after,
+.dropright .dropdown-toggle-split::after {
+ margin-left: 0;
+}
+
+.dropleft .dropdown-toggle-split::before {
+ margin-right: 0;
+}
+
+.btn-sm + .dropdown-toggle-split,
+.btn-group-sm > .btn + .dropdown-toggle-split {
+ padding-right: 0.375rem;
+ padding-left: 0.375rem;
+}
+
+.btn-lg + .dropdown-toggle-split,
+.btn-group-lg > .btn + .dropdown-toggle-split {
+ padding-right: 0.75rem;
+ padding-left: 0.75rem;
+}
+
+.btn-group-vertical {
+ flex-direction: column;
+ align-items: flex-start;
+ justify-content: center;
+}
+
+.btn-group-vertical > .btn,
+.btn-group-vertical > .btn-group {
+ width: 100%;
+}
+
+.btn-group-vertical > .btn:not(:first-child),
+.btn-group-vertical > .btn-group:not(:first-child) {
+ margin-top: -1px;
+}
+
+.btn-group-vertical > .btn:not(:last-child):not(.dropdown-toggle),
+.btn-group-vertical > .btn-group:not(:last-child) > .btn {
+ border-bottom-right-radius: 0;
+ border-bottom-left-radius: 0;
+}
+
+.btn-group-vertical > .btn:not(:first-child),
+.btn-group-vertical > .btn-group:not(:first-child) > .btn {
+ border-top-left-radius: 0;
+ border-top-right-radius: 0;
+}
+
+.btn-group-toggle > .btn,
+.btn-group-toggle > .btn-group > .btn {
+ margin-bottom: 0;
+}
+
+.btn-group-toggle > .btn input[type='radio'],
+.btn-group-toggle > .btn input[type='checkbox'],
+.btn-group-toggle > .btn-group > .btn input[type='radio'],
+.btn-group-toggle > .btn-group > .btn input[type='checkbox'] {
+ position: absolute;
+ clip: rect(0, 0, 0, 0);
+ pointer-events: none;
+}
+
+.input-group {
+ position: relative;
+ display: flex;
+ flex-wrap: wrap;
+ align-items: stretch;
+ width: 100%;
+}
+
+.input-group > .form-control,
+.input-group > .form-control-plaintext,
+.input-group > .custom-select,
+.input-group > .custom-file {
+ position: relative;
+ flex: 1 1 auto;
+ width: 1%;
+ min-width: 0;
+ margin-bottom: 0;
+}
+
+.input-group > .form-control + .form-control,
+.input-group > .form-control + .custom-select,
+.input-group > .form-control + .custom-file,
+.input-group > .form-control-plaintext + .form-control,
+.input-group > .form-control-plaintext + .custom-select,
+.input-group > .form-control-plaintext + .custom-file,
+.input-group > .custom-select + .form-control,
+.input-group > .custom-select + .custom-select,
+.input-group > .custom-select + .custom-file,
+.input-group > .custom-file + .form-control,
+.input-group > .custom-file + .custom-select,
+.input-group > .custom-file + .custom-file {
+ margin-left: -1px;
+}
+
+.input-group > .form-control:focus,
+.input-group > .custom-select:focus,
+.input-group > .custom-file .custom-file-input:focus ~ .custom-file-label {
+ z-index: 3;
+}
+
+.input-group > .custom-file .custom-file-input:focus {
+ z-index: 4;
+}
+
+.input-group > .form-control:not(:last-child),
+.input-group > .custom-select:not(:last-child) {
+ border-top-right-radius: 0;
+ border-bottom-right-radius: 0;
+}
+
+.input-group > .form-control:not(:first-child),
+.input-group > .custom-select:not(:first-child) {
+ border-top-left-radius: 0;
+ border-bottom-left-radius: 0;
+}
+
+.input-group > .custom-file {
+ display: flex;
+ align-items: center;
+}
+
+.input-group > .custom-file:not(:last-child) .custom-file-label,
+.input-group > .custom-file:not(:last-child) .custom-file-label::after {
+ border-top-right-radius: 0;
+ border-bottom-right-radius: 0;
+}
+
+.input-group > .custom-file:not(:first-child) .custom-file-label {
+ border-top-left-radius: 0;
+ border-bottom-left-radius: 0;
+}
+
+.input-group-prepend,
+.input-group-append {
+ display: flex;
+}
+
+.input-group-prepend .btn,
+.input-group-append .btn {
+ position: relative;
+ z-index: 2;
+}
+
+.input-group-prepend .btn:focus,
+.input-group-append .btn:focus {
+ z-index: 3;
+}
+
+.input-group-prepend .btn + .btn,
+.input-group-prepend .btn + .input-group-text,
+.input-group-prepend .input-group-text + .input-group-text,
+.input-group-prepend .input-group-text + .btn,
+.input-group-append .btn + .btn,
+.input-group-append .btn + .input-group-text,
+.input-group-append .input-group-text + .input-group-text,
+.input-group-append .input-group-text + .btn {
+ margin-left: -1px;
+}
+
+.input-group-prepend {
+ margin-right: -1px;
+}
+
+.input-group-append {
+ margin-left: -1px;
+}
+
+.input-group-text {
+ display: flex;
+ align-items: center;
+ padding: 0.375rem 0.75rem;
+ margin-bottom: 0;
+ font-size: 1rem;
+ font-weight: 400;
+ line-height: 1.5;
+ color: #495057;
+ text-align: center;
+ white-space: nowrap;
+ background-color: #e9ecef;
+ border: 1px solid #ced4da;
+ border-radius: 0.25rem;
+}
+
+.input-group-text input[type='radio'],
+.input-group-text input[type='checkbox'] {
+ margin-top: 0;
+}
+
+.input-group-lg > .form-control:not(textarea),
+.input-group-lg > .custom-select {
+ height: calc(1.5em + 1rem + 2px);
+}
+
+.input-group-lg > .form-control,
+.input-group-lg > .custom-select,
+.input-group-lg > .input-group-prepend > .input-group-text,
+.input-group-lg > .input-group-append > .input-group-text,
+.input-group-lg > .input-group-prepend > .btn,
+.input-group-lg > .input-group-append > .btn {
+ padding: 0.5rem 1rem;
+ font-size: 1.25rem;
+ line-height: 1.5;
+ border-radius: 0.3rem;
+}
+
+.input-group-sm > .form-control:not(textarea),
+.input-group-sm > .custom-select {
+ height: calc(1.5em + 0.5rem + 2px);
+}
+
+.input-group-sm > .form-control,
+.input-group-sm > .custom-select,
+.input-group-sm > .input-group-prepend > .input-group-text,
+.input-group-sm > .input-group-append > .input-group-text,
+.input-group-sm > .input-group-prepend > .btn,
+.input-group-sm > .input-group-append > .btn {
+ padding: 0.25rem 0.5rem;
+ font-size: 0.875rem;
+ line-height: 1.5;
+ border-radius: 0.2rem;
+}
+
+.input-group-lg > .custom-select,
+.input-group-sm > .custom-select {
+ padding-right: 1.75rem;
+}
+
+.input-group > .input-group-prepend > .btn,
+.input-group > .input-group-prepend > .input-group-text,
+.input-group > .input-group-append:not(:last-child) > .btn,
+.input-group > .input-group-append:not(:last-child) > .input-group-text,
+.input-group > .input-group-append:last-child > .btn:not(:last-child):not(.dropdown-toggle),
+.input-group > .input-group-append:last-child > .input-group-text:not(:last-child) {
+ border-top-right-radius: 0;
+ border-bottom-right-radius: 0;
+}
+
+.input-group > .input-group-append > .btn,
+.input-group > .input-group-append > .input-group-text,
+.input-group > .input-group-prepend:not(:first-child) > .btn,
+.input-group > .input-group-prepend:not(:first-child) > .input-group-text,
+.input-group > .input-group-prepend:first-child > .btn:not(:first-child),
+.input-group > .input-group-prepend:first-child > .input-group-text:not(:first-child) {
+ border-top-left-radius: 0;
+ border-bottom-left-radius: 0;
+}
+
+.custom-control {
+ position: relative;
+ display: block;
+ min-height: 1.5rem;
+ padding-left: 1.5rem;
+}
+
+.custom-control-inline {
+ display: inline-flex;
+ margin-right: 1rem;
+}
+
+.custom-control-input {
+ position: absolute;
+ left: 0;
+ z-index: -1;
+ width: 1rem;
+ height: 1.25rem;
+ opacity: 0;
+}
+
+.custom-control-input:checked ~ .custom-control-label::before {
+ color: #fff;
+ border-color: #07819b;
+ background-color: #07819b;
+}
+
+.custom-control-input:focus ~ .custom-control-label::before {
+ box-shadow: 0 0 0 0.2rem rgba(7, 129, 155, 0.25);
+}
+
+.custom-control-input:focus:not(:checked) ~ .custom-control-label::before {
+ border-color: #2cd2f5;
+}
+
+.custom-control-input:not(:disabled):active ~ .custom-control-label::before {
+ color: #fff;
+ background-color: #5ddcf8;
+ border-color: #5ddcf8;
+}
+
+.custom-control-input[disabled] ~ .custom-control-label,
+.custom-control-input:disabled ~ .custom-control-label {
+ color: #313032;
+}
+
+.custom-control-input[disabled] ~ .custom-control-label::before,
+.custom-control-input:disabled ~ .custom-control-label::before {
+ background-color: #e9ecef;
+}
+
+.custom-control-label {
+ position: relative;
+ margin-bottom: 0;
+ vertical-align: top;
+}
+
+.custom-control-label::before {
+ position: absolute;
+ top: 0.25rem;
+ left: -1.5rem;
+ display: block;
+ width: 1rem;
+ height: 1rem;
+ pointer-events: none;
+ content: '';
+ background-color: #fff;
+ border: #adb5bd solid 1px;
+}
+
+.custom-control-label::after {
+ position: absolute;
+ top: 0.25rem;
+ left: -1.5rem;
+ display: block;
+ width: 1rem;
+ height: 1rem;
+ content: '';
+ background: no-repeat 50% / 50% 50%;
+}
+
+.custom-checkbox .custom-control-label::before {
+ border-radius: 0.25rem;
+}
+
+.custom-checkbox .custom-control-input:checked ~ .custom-control-label::after {
+ background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26l2.974 2.99L8 2.193z'/%3e%3c/svg%3e");
+}
+
+.custom-checkbox .custom-control-input:indeterminate ~ .custom-control-label::before {
+ border-color: #07819b;
+ background-color: #07819b;
+}
+
+.custom-checkbox .custom-control-input:indeterminate ~ .custom-control-label::after {
+ background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='4' viewBox='0 0 4 4'%3e%3cpath stroke='%23fff' d='M0 2h4'/%3e%3c/svg%3e");
+}
+
+.custom-checkbox .custom-control-input:disabled:checked ~ .custom-control-label::before {
+ background-color: rgba(7, 129, 155, 0.5);
+}
+
+.custom-checkbox .custom-control-input:disabled:indeterminate ~ .custom-control-label::before {
+ background-color: rgba(7, 129, 155, 0.5);
+}
+
+.custom-radio .custom-control-label::before {
+ border-radius: 50%;
+}
+
+.custom-radio .custom-control-input:checked ~ .custom-control-label::after {
+ background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e");
+}
+
+.custom-radio .custom-control-input:disabled:checked ~ .custom-control-label::before {
+ background-color: rgba(7, 129, 155, 0.5);
+}
+
+.custom-switch {
+ padding-left: 2.25rem;
+}
+
+.custom-switch .custom-control-label::before {
+ left: -2.25rem;
+ width: 1.75rem;
+ pointer-events: all;
+ border-radius: 0.5rem;
+}
+
+.custom-switch .custom-control-label::after {
+ top: calc(0.25rem + 2px);
+ left: calc(-2.25rem + 2px);
+ width: calc(1rem - 4px);
+ height: calc(1rem - 4px);
+ background-color: #adb5bd;
+ border-radius: 0.5rem;
+ transition: transform 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out,
+ box-shadow 0.15s ease-in-out;
+}
+
+.custom-switch .custom-control-input:checked ~ .custom-control-label::after {
+ background-color: #fff;
+ transform: translateX(0.75rem);
+}
+
+.custom-switch .custom-control-input:disabled:checked ~ .custom-control-label::before {
+ background-color: rgba(7, 129, 155, 0.5);
+}
+
+.custom-select {
+ display: inline-block;
+ width: 100%;
+ height: calc(1.5em + 0.75rem + 2px);
+ padding: 0.375rem 1.75rem 0.375rem 0.75rem;
+ font-size: 1rem;
+ font-weight: 400;
+ line-height: 1.5;
+ color: #495057;
+ vertical-align: middle;
+ background: #fff url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='5' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") no-repeat right 0.75rem center/8px 10px;
+ border: 1px solid #ced4da;
+ border-radius: 0.25rem;
+ appearance: none;
+}
+
+.custom-select:focus {
+ border-color: #2cd2f5;
+ outline: 0;
+ box-shadow: 0 0 0 0.2rem rgba(7, 129, 155, 0.25);
+}
+
+.custom-select:focus::-ms-value {
+ color: #495057;
+ background-color: #fff;
+}
+
+.custom-select[multiple],
+.custom-select[size]:not([size='1']) {
+ height: auto;
+ padding-right: 0.75rem;
+ background-image: none;
+}
+
+.custom-select:disabled {
+ color: #313032;
+ background-color: #e9ecef;
+}
+
+.custom-select::-ms-expand {
+ display: none;
+}
+
+.custom-select:-moz-focusring {
+ color: transparent;
+ text-shadow: 0 0 0 #495057;
+}
+
+.custom-select-sm {
+ height: calc(1.5em + 0.5rem + 2px);
+ padding-top: 0.25rem;
+ padding-bottom: 0.25rem;
+ padding-left: 0.5rem;
+ font-size: 0.875rem;
+}
+
+.custom-select-lg {
+ height: calc(1.5em + 1rem + 2px);
+ padding-top: 0.5rem;
+ padding-bottom: 0.5rem;
+ padding-left: 1rem;
+ font-size: 1.25rem;
+}
+
+.custom-file {
+ position: relative;
+ display: inline-block;
+ width: 100%;
+ height: calc(1.5em + 0.75rem + 2px);
+ margin-bottom: 0;
+}
+
+.custom-file-input {
+ position: relative;
+ z-index: 2;
+ width: 100%;
+ height: calc(1.5em + 0.75rem + 2px);
+ margin: 0;
+ opacity: 0;
+}
+
+.custom-file-input:focus ~ .custom-file-label {
+ border-color: #2cd2f5;
+ box-shadow: 0 0 0 0.2rem rgba(7, 129, 155, 0.25);
+}
+
+.custom-file-input[disabled] ~ .custom-file-label,
+.custom-file-input:disabled ~ .custom-file-label {
+ background-color: #e9ecef;
+}
+
+.custom-file-input:lang(en) ~ .custom-file-label::after {
+ content: 'Browse';
+}
+
+.custom-file-input ~ .custom-file-label[data-browse]::after {
+ content: attr(data-browse);
+}
+
+.custom-file-label {
+ position: absolute;
+ top: 0;
+ right: 0;
+ left: 0;
+ z-index: 1;
+ height: calc(1.5em + 0.75rem + 2px);
+ padding: 0.375rem 0.75rem;
+ font-weight: 400;
+ line-height: 1.5;
+ color: #495057;
+ background-color: #fff;
+ border: 1px solid #ced4da;
+ border-radius: 0.25rem;
+}
+
+.custom-file-label::after {
+ position: absolute;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ z-index: 3;
+ display: block;
+ height: calc(1.5em + 0.75rem);
+ padding: 0.375rem 0.75rem;
+ line-height: 1.5;
+ color: #495057;
+ content: 'Browse';
+ background-color: #e9ecef;
+ border-left: inherit;
+ border-radius: 0 0.25rem 0.25rem 0;
+}
+
+.custom-range {
+ width: 100%;
+ height: 1.4rem;
+ padding: 0;
+ background-color: transparent;
+ appearance: none;
+}
+
+.custom-range:focus {
+ outline: none;
+}
+
+.custom-range:focus::-webkit-slider-thumb {
+ box-shadow: 0 0 0 1px #fff, 0 0 0 0.2rem rgba(7, 129, 155, 0.25);
+}
+
+.custom-range:focus::-moz-range-thumb {
+ box-shadow: 0 0 0 1px #fff, 0 0 0 0.2rem rgba(7, 129, 155, 0.25);
+}
+
+.custom-range:focus::-ms-thumb {
+ box-shadow: 0 0 0 1px #fff, 0 0 0 0.2rem rgba(7, 129, 155, 0.25);
+}
+
+.custom-range::-moz-focus-outer {
+ border: 0;
+}
+
+.custom-range::-webkit-slider-thumb {
+ width: 1rem;
+ height: 1rem;
+ margin-top: -0.25rem;
+ background-color: #07819b;
+ border: 0;
+ border-radius: 1rem;
+ transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
+ appearance: none;
+}
+
+.custom-range::-webkit-slider-thumb:active {
+ background-color: #5ddcf8;
+}
+
+.custom-range::-webkit-slider-runnable-track {
+ width: 100%;
+ height: 0.5rem;
+ color: transparent;
+ cursor: pointer;
+ background-color: #dee2e6;
+ border-color: transparent;
+ border-radius: 1rem;
+}
+
+.custom-range::-moz-range-thumb {
+ width: 1rem;
+ height: 1rem;
+ background-color: #07819b;
+ border: 0;
+ border-radius: 1rem;
+ transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
+ appearance: none;
+}
+
+.custom-range::-moz-range-thumb:active {
+ background-color: #5ddcf8;
+}
+
+.custom-range::-moz-range-track {
+ width: 100%;
+ height: 0.5rem;
+ color: transparent;
+ cursor: pointer;
+ background-color: #dee2e6;
+ border-color: transparent;
+ border-radius: 1rem;
+}
+
+.custom-range::-ms-thumb {
+ width: 1rem;
+ height: 1rem;
+ margin-top: 0;
+ margin-right: 0.2rem;
+ margin-left: 0.2rem;
+ background-color: #07819b;
+ border: 0;
+ border-radius: 1rem;
+ transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
+ appearance: none;
+}
+
+.custom-range::-ms-thumb:active {
+ background-color: #5ddcf8;
+}
+
+.custom-range::-ms-track {
+ width: 100%;
+ height: 0.5rem;
+ color: transparent;
+ cursor: pointer;
+ background-color: transparent;
+ border-color: transparent;
+ border-width: 0.5rem;
+}
+
+.custom-range::-ms-fill-lower {
+ background-color: #dee2e6;
+ border-radius: 1rem;
+}
+
+.custom-range::-ms-fill-upper {
+ margin-right: 15px;
+ background-color: #dee2e6;
+ border-radius: 1rem;
+}
+
+.custom-range:disabled::-webkit-slider-thumb {
+ background-color: #adb5bd;
+}
+
+.custom-range:disabled::-webkit-slider-runnable-track {
+ cursor: default;
+}
+
+.custom-range:disabled::-moz-range-thumb {
+ background-color: #adb5bd;
+}
+
+.custom-range:disabled::-moz-range-track {
+ cursor: default;
+}
+
+.custom-range:disabled::-ms-thumb {
+ background-color: #adb5bd;
+}
+
+.custom-control-label::before,
+.custom-file-label,
+.custom-select {
+ transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
+}
+
+.nav {
+ display: flex;
+ flex-wrap: wrap;
+ padding-left: 0;
+ margin-bottom: 0;
+ list-style: none;
+}
+
+.nav-link {
+ display: block;
+ padding: 0.5rem 1rem;
+}
+
+.nav-link:hover,
+.nav-link:focus {
+ text-decoration: none;
+}
+
+.nav-link.disabled {
+ color: #313032;
+ pointer-events: none;
+ cursor: default;
+}
+
+.nav-tabs {
+ border-bottom: 1px solid #dee2e6;
+}
+
+.nav-tabs .nav-item {
+ margin-bottom: -1px;
+}
+
+.nav-tabs .nav-link {
+ border: 1px solid transparent;
+ border-top-left-radius: 0.25rem;
+ border-top-right-radius: 0.25rem;
+}
+
+.nav-tabs .nav-link:hover,
+.nav-tabs .nav-link:focus {
+ border-color: #e9ecef #e9ecef #dee2e6;
+}
+
+.nav-tabs .nav-link.disabled {
+ color: #313032;
+ background-color: transparent;
+ border-color: transparent;
+}
+
+.nav-tabs .nav-link.active,
+.nav-tabs .nav-item.show .nav-link {
+ color: #495057;
+ background-color: #fff;
+ border-color: #dee2e6 #dee2e6 #fff;
+}
+
+.nav-tabs .dropdown-menu {
+ margin-top: -1px;
+ border-top-left-radius: 0;
+ border-top-right-radius: 0;
+}
+
+.nav-pills .nav-link {
+ border-radius: 0.25rem;
+}
+
+.nav-pills .nav-link.active,
+.nav-pills .show > .nav-link {
+ color: #fff;
+ background-color: #07819b;
+}
+
+.nav-fill .nav-item {
+ flex: 1 1 auto;
+ text-align: center;
+}
+
+.nav-justified .nav-item {
+ flex-basis: 0;
+ flex-grow: 1;
+ text-align: center;
+}
+
+.tab-content > .tab-pane {
+ display: none;
+}
+
+.tab-content > .active {
+ display: block;
+}
+
+.navbar {
+ position: relative;
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ justify-content: space-between;
+ padding: 0.5rem 1rem;
+}
+
+.navbar .container,
+.navbar .container-fluid,
+.navbar .container-sm,
+.navbar .container-md,
+.navbar .container-lg,
+.navbar .container-xl {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ justify-content: space-between;
+}
+
+.navbar-brand {
+ display: inline-block;
+ padding-top: 0.3125rem;
+ padding-bottom: 0.3125rem;
+ margin-right: 1rem;
+ font-size: 1.25rem;
+ line-height: inherit;
+ white-space: nowrap;
+}
+
+.navbar-brand:hover,
+.navbar-brand:focus {
+ text-decoration: none;
+}
+
+.navbar-nav {
+ display: flex;
+ flex-direction: column;
+ padding-left: 0;
+ margin-bottom: 0;
+ list-style: none;
+}
+
+.navbar-nav .nav-link {
+ padding-right: 0;
+ padding-left: 0;
+}
+
+.navbar-nav .dropdown-menu {
+ position: static;
+ float: none;
+}
+
+.navbar-text {
+ display: inline-block;
+ padding-top: 0.5rem;
+ padding-bottom: 0.5rem;
+}
+
+.navbar-collapse {
+ flex-basis: 100%;
+ flex-grow: 1;
+ align-items: center;
+}
+
+.navbar-toggler {
+ padding: 0.25rem 0.75rem;
+ font-size: 1.25rem;
+ line-height: 1;
+ background-color: transparent;
+ border: 1px solid transparent;
+ border-radius: 0.25rem;
+}
+
+.navbar-toggler:hover,
+.navbar-toggler:focus {
+ text-decoration: none;
+}
+
+.navbar-toggler-icon {
+ display: inline-block;
+ width: 1.5em;
+ height: 1.5em;
+ vertical-align: middle;
+ content: '';
+ background: no-repeat center center;
+ background-size: 100% 100%;
+}
+
+@media (max-width: 575.98px) {
+ .navbar-expand-sm > .container,
+ .navbar-expand-sm > .container-fluid,
+ .navbar-expand-sm > .container-sm,
+ .navbar-expand-sm > .container-md,
+ .navbar-expand-sm > .container-lg,
+ .navbar-expand-sm > .container-xl {
+ padding-right: 0;
+ padding-left: 0;
+ }
+}
+
+@media (min-width: 576px) {
+ .navbar-expand-sm {
+ flex-flow: row nowrap;
+ justify-content: flex-start;
+ }
+
+ .navbar-expand-sm .navbar-nav {
+ flex-direction: row;
+ }
+
+ .navbar-expand-sm .navbar-nav .dropdown-menu {
+ position: absolute;
+ }
+
+ .navbar-expand-sm .navbar-nav .nav-link {
+ padding-right: 0.5rem;
+ padding-left: 0.5rem;
+ }
+
+ .navbar-expand-sm > .container,
+ .navbar-expand-sm > .container-fluid,
+ .navbar-expand-sm > .container-sm,
+ .navbar-expand-sm > .container-md,
+ .navbar-expand-sm > .container-lg,
+ .navbar-expand-sm > .container-xl {
+ flex-wrap: nowrap;
+ }
+
+ .navbar-expand-sm .navbar-collapse {
+ display: flex !important;
+ flex-basis: auto;
+ }
+
+ .navbar-expand-sm .navbar-toggler {
+ display: none;
+ }
+}
+
+@media (max-width: 767.98px) {
+ .navbar-expand-md > .container,
+ .navbar-expand-md > .container-fluid,
+ .navbar-expand-md > .container-sm,
+ .navbar-expand-md > .container-md,
+ .navbar-expand-md > .container-lg,
+ .navbar-expand-md > .container-xl {
+ padding-right: 0;
+ padding-left: 0;
+ }
+}
+
+@media (min-width: 768px) {
+ .navbar-expand-md {
+ flex-flow: row nowrap;
+ justify-content: flex-start;
+ }
+
+ .navbar-expand-md .navbar-nav {
+ flex-direction: row;
+ }
+
+ .navbar-expand-md .navbar-nav .dropdown-menu {
+ position: absolute;
+ }
+
+ .navbar-expand-md .navbar-nav .nav-link {
+ padding-right: 0.5rem;
+ padding-left: 0.5rem;
+ }
+
+ .navbar-expand-md > .container,
+ .navbar-expand-md > .container-fluid,
+ .navbar-expand-md > .container-sm,
+ .navbar-expand-md > .container-md,
+ .navbar-expand-md > .container-lg,
+ .navbar-expand-md > .container-xl {
+ flex-wrap: nowrap;
+ }
+
+ .navbar-expand-md .navbar-collapse {
+ display: flex !important;
+ flex-basis: auto;
+ }
+
+ .navbar-expand-md .navbar-toggler {
+ display: none;
+ }
+}
+
+@media (max-width: 991.98px) {
+ .navbar-expand-lg > .container,
+ .navbar-expand-lg > .container-fluid,
+ .navbar-expand-lg > .container-sm,
+ .navbar-expand-lg > .container-md,
+ .navbar-expand-lg > .container-lg,
+ .navbar-expand-lg > .container-xl {
+ padding-right: 0;
+ padding-left: 0;
+ }
+}
+
+@media (min-width: 992px) {
+ .navbar-expand-lg {
+ flex-flow: row nowrap;
+ justify-content: flex-start;
+ }
+
+ .navbar-expand-lg .navbar-nav {
+ flex-direction: row;
+ }
+
+ .navbar-expand-lg .navbar-nav .dropdown-menu {
+ position: absolute;
+ }
+
+ .navbar-expand-lg .navbar-nav .nav-link {
+ padding-right: 0.5rem;
+ padding-left: 0.5rem;
+ }
+
+ .navbar-expand-lg > .container,
+ .navbar-expand-lg > .container-fluid,
+ .navbar-expand-lg > .container-sm,
+ .navbar-expand-lg > .container-md,
+ .navbar-expand-lg > .container-lg,
+ .navbar-expand-lg > .container-xl {
+ flex-wrap: nowrap;
+ }
+
+ .navbar-expand-lg .navbar-collapse {
+ display: flex !important;
+ flex-basis: auto;
+ }
+
+ .navbar-expand-lg .navbar-toggler {
+ display: none;
+ }
+}
+
+@media (max-width: 1199.98px) {
+ .navbar-expand-xl > .container,
+ .navbar-expand-xl > .container-fluid,
+ .navbar-expand-xl > .container-sm,
+ .navbar-expand-xl > .container-md,
+ .navbar-expand-xl > .container-lg,
+ .navbar-expand-xl > .container-xl {
+ padding-right: 0;
+ padding-left: 0;
+ }
+}
+
+@media (min-width: 1200px) {
+ .navbar-expand-xl {
+ flex-flow: row nowrap;
+ justify-content: flex-start;
+ }
+
+ .navbar-expand-xl .navbar-nav {
+ flex-direction: row;
+ }
+
+ .navbar-expand-xl .navbar-nav .dropdown-menu {
+ position: absolute;
+ }
+
+ .navbar-expand-xl .navbar-nav .nav-link {
+ padding-right: 0.5rem;
+ padding-left: 0.5rem;
+ }
+
+ .navbar-expand-xl > .container,
+ .navbar-expand-xl > .container-fluid,
+ .navbar-expand-xl > .container-sm,
+ .navbar-expand-xl > .container-md,
+ .navbar-expand-xl > .container-lg,
+ .navbar-expand-xl > .container-xl {
+ flex-wrap: nowrap;
+ }
+
+ .navbar-expand-xl .navbar-collapse {
+ display: flex !important;
+ flex-basis: auto;
+ }
+
+ .navbar-expand-xl .navbar-toggler {
+ display: none;
+ }
+}
+
+.navbar-expand {
+ flex-flow: row nowrap;
+ justify-content: flex-start;
+}
+
+.navbar-expand > .container,
+.navbar-expand > .container-fluid,
+.navbar-expand > .container-sm,
+.navbar-expand > .container-md,
+.navbar-expand > .container-lg,
+.navbar-expand > .container-xl {
+ padding-right: 0;
+ padding-left: 0;
+}
+
+.navbar-expand .navbar-nav {
+ flex-direction: row;
+}
+
+.navbar-expand .navbar-nav .dropdown-menu {
+ position: absolute;
+}
+
+.navbar-expand .navbar-nav .nav-link {
+ padding-right: 0.5rem;
+ padding-left: 0.5rem;
+}
+
+.navbar-expand > .container,
+.navbar-expand > .container-fluid,
+.navbar-expand > .container-sm,
+.navbar-expand > .container-md,
+.navbar-expand > .container-lg,
+.navbar-expand > .container-xl {
+ flex-wrap: nowrap;
+}
+
+.navbar-expand .navbar-collapse {
+ display: flex !important;
+ flex-basis: auto;
+}
+
+.navbar-expand .navbar-toggler {
+ display: none;
+}
+
+.navbar-light .navbar-brand {
+ color: rgba(0, 0, 0, 0.9);
+}
+
+.navbar-light .navbar-brand:hover,
+.navbar-light .navbar-brand:focus {
+ color: rgba(0, 0, 0, 0.9);
+}
+
+.navbar-light .navbar-nav .nav-link {
+ color: rgba(0, 0, 0, 0.5);
+}
+
+.navbar-light .navbar-nav .nav-link:hover,
+.navbar-light .navbar-nav .nav-link:focus {
+ color: rgba(0, 0, 0, 0.7);
+}
+
+.navbar-light .navbar-nav .nav-link.disabled {
+ color: rgba(0, 0, 0, 0.3);
+}
+
+.navbar-light .navbar-nav .show > .nav-link,
+.navbar-light .navbar-nav .active > .nav-link,
+.navbar-light .navbar-nav .nav-link.show,
+.navbar-light .navbar-nav .nav-link.active {
+ color: rgba(0, 0, 0, 0.9);
+}
+
+.navbar-light .navbar-toggler {
+ color: rgba(0, 0, 0, 0.5);
+ border-color: rgba(0, 0, 0, 0.1);
+}
+
+.navbar-light .navbar-toggler-icon {
+ background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='30' height='30' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%280, 0, 0, 0.5%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e");
+}
+
+.navbar-light .navbar-text {
+ color: rgba(0, 0, 0, 0.5);
+}
+
+.navbar-light .navbar-text a {
+ color: rgba(0, 0, 0, 0.9);
+}
+
+.navbar-light .navbar-text a:hover,
+.navbar-light .navbar-text a:focus {
+ color: rgba(0, 0, 0, 0.9);
+}
+
+.navbar-dark .navbar-brand {
+ color: #fff;
+}
+
+.navbar-dark .navbar-brand:hover,
+.navbar-dark .navbar-brand:focus {
+ color: #fff;
+}
+
+.navbar-dark .navbar-nav .nav-link {
+ color: rgba(255, 255, 255, 0.5);
+}
+
+.navbar-dark .navbar-nav .nav-link:hover,
+.navbar-dark .navbar-nav .nav-link:focus {
+ color: rgba(255, 255, 255, 0.75);
+}
+
+.navbar-dark .navbar-nav .nav-link.disabled {
+ color: rgba(255, 255, 255, 0.25);
+}
+
+.navbar-dark .navbar-nav .show > .nav-link,
+.navbar-dark .navbar-nav .active > .nav-link,
+.navbar-dark .navbar-nav .nav-link.show,
+.navbar-dark .navbar-nav .nav-link.active {
+ color: #fff;
+}
+
+.navbar-dark .navbar-toggler {
+ color: rgba(255, 255, 255, 0.5);
+ border-color: rgba(255, 255, 255, 0.1);
+}
+
+.navbar-dark .navbar-toggler-icon {
+ background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='30' height='30' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.5%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e");
+}
+
+.navbar-dark .navbar-text {
+ color: rgba(255, 255, 255, 0.5);
+}
+
+.navbar-dark .navbar-text a {
+ color: #fff;
+}
+
+.navbar-dark .navbar-text a:hover,
+.navbar-dark .navbar-text a:focus {
+ color: #fff;
+}
+
+.card {
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ min-width: 0;
+ word-wrap: break-word;
+ background-color: #fff;
+ background-clip: border-box;
+ border: 1px solid rgba(0, 0, 0, 0.125);
+ border-radius: 0.25rem;
+}
+
+.card > hr {
+ margin-right: 0;
+ margin-left: 0;
+}
+
+.card > .list-group {
+ border-top: inherit;
+ border-bottom: inherit;
+}
+
+.card > .list-group:first-child {
+ border-top-width: 0;
+ border-top-left-radius: calc(0.25rem - 1px);
+ border-top-right-radius: calc(0.25rem - 1px);
+}
+
+.card > .list-group:last-child {
+ border-bottom-width: 0;
+ border-bottom-right-radius: calc(0.25rem - 1px);
+ border-bottom-left-radius: calc(0.25rem - 1px);
+}
+
+.card-body {
+ flex: 1 1 auto;
+ min-height: 1px;
+ padding: 1.25rem;
+}
+
+.card-title {
+ margin-bottom: 0.75rem;
+}
+
+.card-subtitle {
+ margin-top: -0.375rem;
+ margin-bottom: 0;
+}
+
+.card-text:last-child {
+ margin-bottom: 0;
+}
+
+.card-link:hover {
+ text-decoration: none;
+}
+
+.card-link + .card-link {
+ margin-left: 1.25rem;
+}
+
+.card-header {
+ padding: 0.75rem 1.25rem;
+ margin-bottom: 0;
+ background-color: rgba(0, 0, 0, 0.03);
+ border-bottom: 1px solid rgba(0, 0, 0, 0.125);
+}
+
+.card-header:first-child {
+ border-radius: calc(0.25rem - 1px) calc(0.25rem - 1px) 0 0;
+}
+
+.card-header + .list-group .list-group-item:first-child {
+ border-top: 0;
+}
+
+.card-footer {
+ padding: 0.75rem 1.25rem;
+ background-color: rgba(0, 0, 0, 0.03);
+ border-top: 1px solid rgba(0, 0, 0, 0.125);
+}
+
+.card-footer:last-child {
+ border-radius: 0 0 calc(0.25rem - 1px) calc(0.25rem - 1px);
+}
+
+.card-header-tabs {
+ margin-right: -0.625rem;
+ margin-bottom: -0.75rem;
+ margin-left: -0.625rem;
+ border-bottom: 0;
+}
+
+.card-header-pills {
+ margin-right: -0.625rem;
+ margin-left: -0.625rem;
+}
+
+.card-img-overlay {
+ position: absolute;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ padding: 1.25rem;
+}
+
+.card-img,
+.card-img-top,
+.card-img-bottom {
+ flex-shrink: 0;
+ width: 100%;
+}
+
+.card-img,
+.card-img-top {
+ border-top-left-radius: calc(0.25rem - 1px);
+ border-top-right-radius: calc(0.25rem - 1px);
+}
+
+.card-img,
+.card-img-bottom {
+ border-bottom-right-radius: calc(0.25rem - 1px);
+ border-bottom-left-radius: calc(0.25rem - 1px);
+}
+
+.card-deck .card {
+ margin-bottom: 15px;
+}
+
+@media (min-width: 576px) {
+ .card-deck {
+ display: flex;
+ flex-flow: row wrap;
+ margin-right: -15px;
+ margin-left: -15px;
+ }
+
+ .card-deck .card {
+ flex: 1 0 0%;
+ margin-right: 15px;
+ margin-bottom: 0;
+ margin-left: 15px;
+ }
+}
+
+.card-group > .card {
+ margin-bottom: 15px;
+}
+
+@media (min-width: 576px) {
+ .card-group {
+ display: flex;
+ flex-flow: row wrap;
+ }
+
+ .card-group > .card {
+ flex: 1 0 0%;
+ margin-bottom: 0;
+ }
+
+ .card-group > .card + .card {
+ margin-left: 0;
+ border-left: 0;
+ }
+
+ .card-group > .card:not(:last-child) {
+ border-top-right-radius: 0;
+ border-bottom-right-radius: 0;
+ }
+
+ .card-group > .card:not(:last-child) .card-img-top,
+ .card-group > .card:not(:last-child) .card-header {
+ border-top-right-radius: 0;
+ }
+
+ .card-group > .card:not(:last-child) .card-img-bottom,
+ .card-group > .card:not(:last-child) .card-footer {
+ border-bottom-right-radius: 0;
+ }
+
+ .card-group > .card:not(:first-child) {
+ border-top-left-radius: 0;
+ border-bottom-left-radius: 0;
+ }
+
+ .card-group > .card:not(:first-child) .card-img-top,
+ .card-group > .card:not(:first-child) .card-header {
+ border-top-left-radius: 0;
+ }
+
+ .card-group > .card:not(:first-child) .card-img-bottom,
+ .card-group > .card:not(:first-child) .card-footer {
+ border-bottom-left-radius: 0;
+ }
+}
+
+.card-columns .card {
+ margin-bottom: 0.75rem;
+}
+
+@media (min-width: 576px) {
+ .card-columns {
+ column-count: 3;
+ column-gap: 1.25rem;
+ orphans: 1;
+ widows: 1;
+ }
+
+ .card-columns .card {
+ display: inline-block;
+ width: 100%;
+ }
+}
+
+.accordion > .card {
+ overflow: visible;
+}
+
+.accordion > .card:not(:last-of-type) {
+ border-bottom: 0;
+ border-bottom-right-radius: 0;
+ border-bottom-left-radius: 0;
+}
+
+.accordion > .card:not(:first-of-type) {
+ border-top-left-radius: 0;
+ border-top-right-radius: 0;
+}
+
+.accordion > .card > .card-header {
+ border-radius: 0;
+ margin-bottom: -1px;
+}
+
+.breadcrumb {
+ display: flex;
+ flex-wrap: wrap;
+ padding: 0.75rem 1rem;
+ margin-bottom: 1rem;
+ list-style: none;
+ background-color: #e9ecef;
+ border-radius: 0.25rem;
+}
+
+.breadcrumb-item {
+ display: flex;
+}
+
+.breadcrumb-item + .breadcrumb-item {
+ padding-left: 0.5rem;
+}
+
+.breadcrumb-item + .breadcrumb-item::before {
+ display: inline-block;
+ padding-right: 0.5rem;
+ color: #313032;
+ content: '/';
+}
+
+.breadcrumb-item + .breadcrumb-item:hover::before {
+ text-decoration: underline;
+}
+
+.breadcrumb-item + .breadcrumb-item:hover::before {
+ text-decoration: none;
+}
+
+.breadcrumb-item.active {
+ color: #313032;
+}
+
+.pagination {
+ display: flex;
+ padding-left: 0;
+ list-style: none;
+ border-radius: 0.25rem;
+}
+
+.page-link {
+ position: relative;
+ display: block;
+ padding: 0.5rem 0.75rem;
+ margin-left: -1px;
+ line-height: 1.25;
+ color: #07819b;
+ background-color: #fff;
+ border: 1px solid #dee2e6;
+}
+
+.page-link:hover {
+ z-index: 2;
+ color: #044452;
+ text-decoration: none;
+ background-color: #e9ecef;
+ border-color: #dee2e6;
+}
+
+.page-link:focus {
+ z-index: 3;
+ outline: 0;
+ box-shadow: 0 0 0 0.2rem rgba(7, 129, 155, 0.25);
+}
+
+.page-item:first-child .page-link {
+ margin-left: 0;
+ border-top-left-radius: 0.25rem;
+ border-bottom-left-radius: 0.25rem;
+}
+
+.page-item:last-child .page-link {
+ border-top-right-radius: 0.25rem;
+ border-bottom-right-radius: 0.25rem;
+}
+
+.page-item.active .page-link {
+ z-index: 3;
+ color: #fff;
+ background-color: #07819b;
+ border-color: #07819b;
+}
+
+.page-item.disabled .page-link {
+ color: #313032;
+ pointer-events: none;
+ cursor: auto;
+ background-color: #fff;
+ border-color: #dee2e6;
+}
+
+.pagination-lg .page-link {
+ padding: 0.75rem 1.5rem;
+ font-size: 1.25rem;
+ line-height: 1.5;
+}
+
+.pagination-lg .page-item:first-child .page-link {
+ border-top-left-radius: 0.3rem;
+ border-bottom-left-radius: 0.3rem;
+}
+
+.pagination-lg .page-item:last-child .page-link {
+ border-top-right-radius: 0.3rem;
+ border-bottom-right-radius: 0.3rem;
+}
+
+.pagination-sm .page-link {
+ padding: 0.25rem 0.5rem;
+ font-size: 0.875rem;
+ line-height: 1.5;
+}
+
+.pagination-sm .page-item:first-child .page-link {
+ border-top-left-radius: 0.2rem;
+ border-bottom-left-radius: 0.2rem;
+}
+
+.pagination-sm .page-item:last-child .page-link {
+ border-top-right-radius: 0.2rem;
+ border-bottom-right-radius: 0.2rem;
+}
+
+.badge {
+ display: inline-block;
+ padding: 0.25em 0.4em;
+ font-size: 75%;
+ font-weight: 700;
+ line-height: 1;
+ text-align: center;
+ white-space: nowrap;
+ vertical-align: baseline;
+ border-radius: 0.25rem;
+ transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out,
+ box-shadow 0.15s ease-in-out;
+}
+
+a.badge:hover,
+a.badge:focus {
+ text-decoration: none;
+}
+
+.badge:empty {
+ display: none;
+}
+
+.btn .badge {
+ position: relative;
+ top: -1px;
+}
+
+.badge-pill {
+ padding-right: 0.6em;
+ padding-left: 0.6em;
+ border-radius: 10rem;
+}
+
+.badge-primary {
+ color: #fff;
+ background-color: #07819b;
+}
+
+a.badge-primary:hover,
+a.badge-primary:focus {
+ color: #fff;
+ background-color: #05586a;
+}
+
+a.badge-primary:focus,
+a.badge-primary.focus {
+ outline: 0;
+ box-shadow: 0 0 0 0.2rem rgba(7, 129, 155, 0.5);
+}
+
+.badge-secondary {
+ color: #fff;
+ background-color: #313032;
+}
+
+a.badge-secondary:hover,
+a.badge-secondary:focus {
+ color: #fff;
+ background-color: #181718;
+}
+
+a.badge-secondary:focus,
+a.badge-secondary.focus {
+ outline: 0;
+ box-shadow: 0 0 0 0.2rem rgba(49, 48, 50, 0.5);
+}
+
+.badge-success {
+ color: #fff;
+ background-color: #28a745;
+}
+
+a.badge-success:hover,
+a.badge-success:focus {
+ color: #fff;
+ background-color: #1e7e34;
+}
+
+a.badge-success:focus,
+a.badge-success.focus {
+ outline: 0;
+ box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.5);
+}
+
+.badge-info {
+ color: #fff;
+ background-color: #17a2b8;
+}
+
+a.badge-info:hover,
+a.badge-info:focus {
+ color: #fff;
+ background-color: #117a8b;
+}
+
+a.badge-info:focus,
+a.badge-info.focus {
+ outline: 0;
+ box-shadow: 0 0 0 0.2rem rgba(23, 162, 184, 0.5);
+}
+
+.badge-warning {
+ color: #212529;
+ background-color: #ffc107;
+}
+
+a.badge-warning:hover,
+a.badge-warning:focus {
+ color: #212529;
+ background-color: #d39e00;
+}
+
+a.badge-warning:focus,
+a.badge-warning.focus {
+ outline: 0;
+ box-shadow: 0 0 0 0.2rem rgba(255, 193, 7, 0.5);
+}
+
+.badge-danger {
+ color: #fff;
+ background-color: #dc3545;
+}
+
+a.badge-danger:hover,
+a.badge-danger:focus {
+ color: #fff;
+ background-color: #bd2130;
+}
+
+a.badge-danger:focus,
+a.badge-danger.focus {
+ outline: 0;
+ box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.5);
+}
+
+.badge-light {
+ color: #212529;
+ background-color: #f8f9fa;
+}
+
+a.badge-light:hover,
+a.badge-light:focus {
+ color: #212529;
+ background-color: #dae0e5;
+}
+
+a.badge-light:focus,
+a.badge-light.focus {
+ outline: 0;
+ box-shadow: 0 0 0 0.2rem rgba(248, 249, 250, 0.5);
+}
+
+.badge-dark {
+ color: #fff;
+ background-color: #343a40;
+}
+
+a.badge-dark:hover,
+a.badge-dark:focus {
+ color: #fff;
+ background-color: #1d2124;
+}
+
+a.badge-dark:focus,
+a.badge-dark.focus {
+ outline: 0;
+ box-shadow: 0 0 0 0.2rem rgba(52, 58, 64, 0.5);
+}
+
+.jumbotron {
+ padding: 2rem 1rem;
+ margin-bottom: 2rem;
+ background-color: #e9ecef;
+ border-radius: 0.3rem;
+}
+
+@media (min-width: 576px) {
+ .jumbotron {
+ padding: 4rem 2rem;
+ }
+}
+
+.jumbotron-fluid {
+ padding-right: 0;
+ padding-left: 0;
+ border-radius: 0;
+}
+
+.alert {
+ position: relative;
+ padding: 0.75rem 1.25rem;
+ margin-bottom: 1rem;
+ border: 1px solid transparent;
+ border-radius: 0.25rem;
+}
+
+.alert-heading {
+ color: inherit;
+}
+
+.alert-link {
+ font-weight: 700;
+}
+
+.alert-dismissible {
+ padding-right: 4rem;
+}
+
+.alert-dismissible .close {
+ position: absolute;
+ top: 0;
+ right: 0;
+ padding: 0.75rem 1.25rem;
+ color: inherit;
+}
+
+.alert-primary {
+ color: #044351;
+ background-color: #cde6eb;
+ border-color: #badce3;
+}
+
+.alert-primary hr {
+ border-top-color: #a8d3dc;
+}
+
+.alert-primary .alert-link {
+ color: #021b20;
+}
+
+.alert-secondary {
+ color: #19191a;
+ background-color: #d6d6d6;
+ border-color: #c5c5c6;
+}
+
+.alert-secondary hr {
+ border-top-color: #b8b8b9;
+}
+
+.alert-secondary .alert-link {
+ color: black;
+}
+
+.alert-success {
+ color: #155724;
+ background-color: #d4edda;
+ border-color: #c3e6cb;
+}
+
+.alert-success hr {
+ border-top-color: #b1dfbb;
+}
+
+.alert-success .alert-link {
+ color: #0b2e13;
+}
+
+.alert-info {
+ color: #0c5460;
+ background-color: #d1ecf1;
+ border-color: #bee5eb;
+}
+
+.alert-info hr {
+ border-top-color: #abdde5;
+}
+
+.alert-info .alert-link {
+ color: #062c33;
+}
+
+.alert-warning {
+ color: #856404;
+ background-color: #fff3cd;
+ border-color: #ffeeba;
+}
+
+.alert-warning hr {
+ border-top-color: #ffe8a1;
+}
+
+.alert-warning .alert-link {
+ color: #533f03;
+}
+
+.alert-danger {
+ color: #721c24;
+ background-color: #f8d7da;
+ border-color: #f5c6cb;
+}
+
+.alert-danger hr {
+ border-top-color: #f1b0b7;
+}
+
+.alert-danger .alert-link {
+ color: #491217;
+}
+
+.alert-light {
+ color: #818182;
+ background-color: #fefefe;
+ border-color: #fdfdfe;
+}
+
+.alert-light hr {
+ border-top-color: #ececf6;
+}
+
+.alert-light .alert-link {
+ color: #686868;
+}
+
+.alert-dark {
+ color: #1b1e21;
+ background-color: #d6d8d9;
+ border-color: #c6c8ca;
+}
+
+.alert-dark hr {
+ border-top-color: #b9bbbe;
+}
+
+.alert-dark .alert-link {
+ color: #040505;
+}
+
+@keyframes progress-bar-stripes {
+ from {
+ background-position: 1rem 0;
+ }
+ to {
+ background-position: 0 0;
+ }
+}
+
+.progress {
+ display: flex;
+ height: 1rem;
+ overflow: hidden;
+ line-height: 0;
+ font-size: 0.75rem;
+ background-color: #e9ecef;
+ border-radius: 0.25rem;
+}
+
+.progress-bar {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ overflow: hidden;
+ color: #fff;
+ text-align: center;
+ white-space: nowrap;
+ background-color: #07819b;
+ transition: width 0.6s ease;
+}
+
+.progress-bar-striped {
+ background-image: linear-gradient(
+ 45deg,
+ rgba(255, 255, 255, 0.15) 25%,
+ transparent 25%,
+ transparent 50%,
+ rgba(255, 255, 255, 0.15) 50%,
+ rgba(255, 255, 255, 0.15) 75%,
+ transparent 75%,
+ transparent
+ );
+ background-size: 1rem 1rem;
+}
+
+.progress-bar-animated {
+ animation: progress-bar-stripes 1s linear infinite;
+}
+
+.media {
+ display: flex;
+ align-items: flex-start;
+}
+
+.media-body {
+ flex: 1;
+}
+
+.list-group {
+ display: flex;
+ flex-direction: column;
+ padding-left: 0;
+ margin-bottom: 0;
+ border-radius: 0.25rem;
+}
+
+.list-group-item-action {
+ width: 100%;
+ color: #495057;
+ text-align: inherit;
+}
+
+.list-group-item-action:hover,
+.list-group-item-action:focus {
+ z-index: 1;
+ color: #495057;
+ text-decoration: none;
+ background-color: #f8f9fa;
+}
+
+.list-group-item-action:active {
+ color: #212529;
+ background-color: #e9ecef;
+}
+
+.list-group-item {
+ position: relative;
+ display: block;
+ padding: 0.75rem 1.25rem;
+ background-color: #fff;
+ border: 1px solid rgba(0, 0, 0, 0.125);
+}
+
+.list-group-item:first-child {
+ border-top-left-radius: inherit;
+ border-top-right-radius: inherit;
+}
+
+.list-group-item:last-child {
+ border-bottom-right-radius: inherit;
+ border-bottom-left-radius: inherit;
+}
+
+.list-group-item.disabled,
+.list-group-item:disabled {
+ color: #313032;
+ pointer-events: none;
+ background-color: #fff;
+}
+
+.list-group-item.active {
+ z-index: 2;
+ color: #fff;
+ background-color: #07819b;
+ border-color: #07819b;
+}
+
+.list-group-item + .list-group-item {
+ border-top-width: 0;
+}
+
+.list-group-item + .list-group-item.active {
+ margin-top: -1px;
+ border-top-width: 1px;
+}
+
+.list-group-horizontal {
+ flex-direction: row;
+}
+
+.list-group-horizontal > .list-group-item:first-child {
+ border-bottom-left-radius: 0.25rem;
+ border-top-right-radius: 0;
+}
+
+.list-group-horizontal > .list-group-item:last-child {
+ border-top-right-radius: 0.25rem;
+ border-bottom-left-radius: 0;
+}
+
+.list-group-horizontal > .list-group-item.active {
+ margin-top: 0;
+}
+
+.list-group-horizontal > .list-group-item + .list-group-item {
+ border-top-width: 1px;
+ border-left-width: 0;
+}
+
+.list-group-horizontal > .list-group-item + .list-group-item.active {
+ margin-left: -1px;
+ border-left-width: 1px;
+}
+
+@media (min-width: 576px) {
+ .list-group-horizontal-sm {
+ flex-direction: row;
+ }
+
+ .list-group-horizontal-sm > .list-group-item:first-child {
+ border-bottom-left-radius: 0.25rem;
+ border-top-right-radius: 0;
+ }
+
+ .list-group-horizontal-sm > .list-group-item:last-child {
+ border-top-right-radius: 0.25rem;
+ border-bottom-left-radius: 0;
+ }
+
+ .list-group-horizontal-sm > .list-group-item.active {
+ margin-top: 0;
+ }
+
+ .list-group-horizontal-sm > .list-group-item + .list-group-item {
+ border-top-width: 1px;
+ border-left-width: 0;
+ }
+
+ .list-group-horizontal-sm > .list-group-item + .list-group-item.active {
+ margin-left: -1px;
+ border-left-width: 1px;
+ }
+}
+
+@media (min-width: 768px) {
+ .list-group-horizontal-md {
+ flex-direction: row;
+ }
+
+ .list-group-horizontal-md > .list-group-item:first-child {
+ border-bottom-left-radius: 0.25rem;
+ border-top-right-radius: 0;
+ }
+
+ .list-group-horizontal-md > .list-group-item:last-child {
+ border-top-right-radius: 0.25rem;
+ border-bottom-left-radius: 0;
+ }
+
+ .list-group-horizontal-md > .list-group-item.active {
+ margin-top: 0;
+ }
+
+ .list-group-horizontal-md > .list-group-item + .list-group-item {
+ border-top-width: 1px;
+ border-left-width: 0;
+ }
+
+ .list-group-horizontal-md > .list-group-item + .list-group-item.active {
+ margin-left: -1px;
+ border-left-width: 1px;
+ }
+}
+
+@media (min-width: 992px) {
+ .list-group-horizontal-lg {
+ flex-direction: row;
+ }
+
+ .list-group-horizontal-lg > .list-group-item:first-child {
+ border-bottom-left-radius: 0.25rem;
+ border-top-right-radius: 0;
+ }
+
+ .list-group-horizontal-lg > .list-group-item:last-child {
+ border-top-right-radius: 0.25rem;
+ border-bottom-left-radius: 0;
+ }
+
+ .list-group-horizontal-lg > .list-group-item.active {
+ margin-top: 0;
+ }
+
+ .list-group-horizontal-lg > .list-group-item + .list-group-item {
+ border-top-width: 1px;
+ border-left-width: 0;
+ }
+
+ .list-group-horizontal-lg > .list-group-item + .list-group-item.active {
+ margin-left: -1px;
+ border-left-width: 1px;
+ }
+}
+
+@media (min-width: 1200px) {
+ .list-group-horizontal-xl {
+ flex-direction: row;
+ }
+
+ .list-group-horizontal-xl > .list-group-item:first-child {
+ border-bottom-left-radius: 0.25rem;
+ border-top-right-radius: 0;
+ }
+
+ .list-group-horizontal-xl > .list-group-item:last-child {
+ border-top-right-radius: 0.25rem;
+ border-bottom-left-radius: 0;
+ }
+
+ .list-group-horizontal-xl > .list-group-item.active {
+ margin-top: 0;
+ }
+
+ .list-group-horizontal-xl > .list-group-item + .list-group-item {
+ border-top-width: 1px;
+ border-left-width: 0;
+ }
+
+ .list-group-horizontal-xl > .list-group-item + .list-group-item.active {
+ margin-left: -1px;
+ border-left-width: 1px;
+ }
+}
+
+.list-group-flush {
+ border-radius: 0;
+}
+
+.list-group-flush > .list-group-item {
+ border-width: 0 0 1px;
+}
+
+.list-group-flush > .list-group-item:last-child {
+ border-bottom-width: 0;
+}
+
+.list-group-item-primary {
+ color: #044351;
+ background-color: #badce3;
+}
+
+.list-group-item-primary.list-group-item-action:hover,
+.list-group-item-primary.list-group-item-action:focus {
+ color: #044351;
+ background-color: #a8d3dc;
+}
+
+.list-group-item-primary.list-group-item-action.active {
+ color: #fff;
+ background-color: #044351;
+ border-color: #044351;
+}
+
+.list-group-item-secondary {
+ color: #19191a;
+ background-color: #c5c5c6;
+}
+
+.list-group-item-secondary.list-group-item-action:hover,
+.list-group-item-secondary.list-group-item-action:focus {
+ color: #19191a;
+ background-color: #b8b8b9;
+}
+
+.list-group-item-secondary.list-group-item-action.active {
+ color: #fff;
+ background-color: #19191a;
+ border-color: #19191a;
+}
+
+.list-group-item-success {
+ color: #155724;
+ background-color: #c3e6cb;
+}
+
+.list-group-item-success.list-group-item-action:hover,
+.list-group-item-success.list-group-item-action:focus {
+ color: #155724;
+ background-color: #b1dfbb;
+}
+
+.list-group-item-success.list-group-item-action.active {
+ color: #fff;
+ background-color: #155724;
+ border-color: #155724;
+}
+
+.list-group-item-info {
+ color: #0c5460;
+ background-color: #bee5eb;
+}
+
+.list-group-item-info.list-group-item-action:hover,
+.list-group-item-info.list-group-item-action:focus {
+ color: #0c5460;
+ background-color: #abdde5;
+}
+
+.list-group-item-info.list-group-item-action.active {
+ color: #fff;
+ background-color: #0c5460;
+ border-color: #0c5460;
+}
+
+.list-group-item-warning {
+ color: #856404;
+ background-color: #ffeeba;
+}
+
+.list-group-item-warning.list-group-item-action:hover,
+.list-group-item-warning.list-group-item-action:focus {
+ color: #856404;
+ background-color: #ffe8a1;
+}
+
+.list-group-item-warning.list-group-item-action.active {
+ color: #fff;
+ background-color: #856404;
+ border-color: #856404;
+}
+
+.list-group-item-danger {
+ color: #721c24;
+ background-color: #f5c6cb;
+}
+
+.list-group-item-danger.list-group-item-action:hover,
+.list-group-item-danger.list-group-item-action:focus {
+ color: #721c24;
+ background-color: #f1b0b7;
+}
+
+.list-group-item-danger.list-group-item-action.active {
+ color: #fff;
+ background-color: #721c24;
+ border-color: #721c24;
+}
+
+.list-group-item-light {
+ color: #818182;
+ background-color: #fdfdfe;
+}
+
+.list-group-item-light.list-group-item-action:hover,
+.list-group-item-light.list-group-item-action:focus {
+ color: #818182;
+ background-color: #ececf6;
+}
+
+.list-group-item-light.list-group-item-action.active {
+ color: #fff;
+ background-color: #818182;
+ border-color: #818182;
+}
+
+.list-group-item-dark {
+ color: #1b1e21;
+ background-color: #c6c8ca;
+}
+
+.list-group-item-dark.list-group-item-action:hover,
+.list-group-item-dark.list-group-item-action:focus {
+ color: #1b1e21;
+ background-color: #b9bbbe;
+}
+
+.list-group-item-dark.list-group-item-action.active {
+ color: #fff;
+ background-color: #1b1e21;
+ border-color: #1b1e21;
+}
+
+.close {
+ float: right;
+ font-size: 1.5rem;
+ font-weight: 700;
+ line-height: 1;
+ color: #000;
+ text-shadow: 0 1px 0 #fff;
+ opacity: 0.5;
+}
+
+.close:hover {
+ color: #000;
+ text-decoration: none;
+}
+
+.close:not(:disabled):not(.disabled):hover,
+.close:not(:disabled):not(.disabled):focus {
+ opacity: 0.75;
+}
+
+button.close {
+ padding: 0;
+ background-color: transparent;
+ border: 0;
+}
+
+a.close.disabled {
+ pointer-events: none;
+}
+
+.toast {
+ max-width: 350px;
+ overflow: hidden;
+ font-size: 0.875rem;
+ background-color: rgba(255, 255, 255, 0.85);
+ background-clip: padding-box;
+ border: 1px solid rgba(0, 0, 0, 0.1);
+ box-shadow: 0 0.25rem 0.75rem rgba(0, 0, 0, 0.1);
+ backdrop-filter: blur(10px);
+ opacity: 0;
+ border-radius: 0.25rem;
+}
+
+.toast:not(:last-child) {
+ margin-bottom: 0.75rem;
+}
+
+.toast.showing {
+ opacity: 1;
+}
+
+.toast.show {
+ display: block;
+ opacity: 1;
+}
+
+.toast.hide {
+ display: none;
+}
+
+.toast-header {
+ display: flex;
+ align-items: center;
+ padding: 0.25rem 0.75rem;
+ color: #313032;
+ background-color: rgba(255, 255, 255, 0.85);
+ background-clip: padding-box;
+ border-bottom: 1px solid rgba(0, 0, 0, 0.05);
+}
+
+.toast-body {
+ padding: 0.75rem;
+}
+
+.modal-open {
+ overflow: hidden;
+}
+
+.modal-open .modal {
+ overflow-x: hidden;
+ overflow-y: auto;
+}
+
+.modal {
+ position: fixed;
+ top: 0;
+ left: 0;
+ z-index: 1050;
+ display: none;
+ width: 100%;
+ height: 100%;
+ overflow: hidden;
+ outline: 0;
+}
+
+.modal-dialog {
+ position: relative;
+ width: auto;
+ margin: 0.5rem;
+ pointer-events: none;
+}
+
+.modal.fade .modal-dialog {
+ transition: transform 0.3s ease-out;
+ transform: translate(0, -50px);
+}
+
+.modal.show .modal-dialog {
+ transform: none;
+}
+
+.modal.modal-static .modal-dialog {
+ transform: scale(1.02);
+}
+
+.modal-dialog-scrollable {
+ display: flex;
+ max-height: calc(100% - 1rem);
+}
+
+.modal-dialog-scrollable .modal-content {
+ max-height: calc(100vh - 1rem);
+ overflow: hidden;
+}
+
+.modal-dialog-scrollable .modal-header,
+.modal-dialog-scrollable .modal-footer {
+ flex-shrink: 0;
+}
+
+.modal-dialog-scrollable .modal-body {
+ overflow-y: auto;
+}
+
+.modal-dialog-centered {
+ display: flex;
+ align-items: center;
+ min-height: calc(100% - 1rem);
+}
+
+.modal-dialog-centered::before {
+ display: block;
+ height: calc(100vh - 1rem);
+ height: min-content;
+ content: '';
+}
+
+.modal-dialog-centered.modal-dialog-scrollable {
+ flex-direction: column;
+ justify-content: center;
+ height: 100%;
+}
+
+.modal-dialog-centered.modal-dialog-scrollable .modal-content {
+ max-height: none;
+}
+
+.modal-dialog-centered.modal-dialog-scrollable::before {
+ content: none;
+}
+
+.modal-content {
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+ pointer-events: auto;
+ background-color: #fff;
+ background-clip: padding-box;
+ border: 1px solid rgba(0, 0, 0, 0.2);
+ border-radius: 0.3rem;
+ outline: 0;
+}
+
+.modal-backdrop {
+ position: fixed;
+ top: 0;
+ left: 0;
+ z-index: 1040;
+ width: 100vw;
+ height: 100vh;
+ background-color: #000;
+}
+
+.modal-backdrop.fade {
+ opacity: 0;
+}
+
+.modal-backdrop.show {
+ opacity: 0.5;
+}
+
+.modal-header {
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+ padding: 1rem 1rem;
+ border-bottom: 1px solid #dee2e6;
+ border-top-left-radius: calc(0.3rem - 1px);
+ border-top-right-radius: calc(0.3rem - 1px);
+}
+
+.modal-header .close {
+ padding: 1rem 1rem;
+ margin: -1rem -1rem -1rem auto;
+}
+
+.modal-title {
+ margin-bottom: 0;
+ line-height: 1.5;
+}
+
+.modal-body {
+ position: relative;
+ flex: 1 1 auto;
+ padding: 1rem;
+}
+
+.modal-footer {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ justify-content: flex-end;
+ padding: 0.75rem;
+ border-top: 1px solid #dee2e6;
+ border-bottom-right-radius: calc(0.3rem - 1px);
+ border-bottom-left-radius: calc(0.3rem - 1px);
+}
+
+.modal-footer > * {
+ margin: 0.25rem;
+}
+
+.modal-scrollbar-measure {
+ position: absolute;
+ top: -9999px;
+ width: 50px;
+ height: 50px;
+ overflow: scroll;
+}
+
+@media (min-width: 576px) {
+ .modal-dialog {
+ max-width: 500px;
+ margin: 1.75rem auto;
+ }
+
+ .modal-dialog-scrollable {
+ max-height: calc(100% - 3.5rem);
+ }
+
+ .modal-dialog-scrollable .modal-content {
+ max-height: calc(100vh - 3.5rem);
+ }
+
+ .modal-dialog-centered {
+ min-height: calc(100% - 3.5rem);
+ }
+
+ .modal-dialog-centered::before {
+ height: calc(100vh - 3.5rem);
+ height: min-content;
+ }
+
+ .modal-sm {
+ max-width: 300px;
+ }
+}
+
+@media (min-width: 992px) {
+ .modal-lg,
+ .modal-xl {
+ max-width: 800px;
+ }
+}
+
+@media (min-width: 1200px) {
+ .modal-xl {
+ max-width: 1140px;
+ }
+}
+
+.tooltip {
+ position: absolute;
+ z-index: 1070;
+ display: block;
+ margin: 0;
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif,
+ 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
+ font-style: normal;
+ font-weight: 400;
+ line-height: 1.5;
+ text-align: left;
+ text-align: start;
+ text-decoration: none;
+ text-shadow: none;
+ text-transform: none;
+ letter-spacing: normal;
+ word-break: normal;
+ word-spacing: normal;
+ white-space: normal;
+ line-break: auto;
+ font-size: 0.875rem;
+ word-wrap: break-word;
+ opacity: 0;
+}
+
+.tooltip.show {
+ opacity: 0.9;
+}
+
+.tooltip .arrow {
+ position: absolute;
+ display: block;
+ width: 0.8rem;
+ height: 0.4rem;
+}
+
+.tooltip .arrow::before {
+ position: absolute;
+ content: '';
+ border-color: transparent;
+ border-style: solid;
+}
+
+.bs-tooltip-top,
+.bs-tooltip-auto[x-placement^='top'] {
+ padding: 0.4rem 0;
+}
+
+.bs-tooltip-top .arrow,
+.bs-tooltip-auto[x-placement^='top'] .arrow {
+ bottom: 0;
+}
+
+.bs-tooltip-top .arrow::before,
+.bs-tooltip-auto[x-placement^='top'] .arrow::before {
+ top: 0;
+ border-width: 0.4rem 0.4rem 0;
+ border-top-color: #000;
+}
+
+.bs-tooltip-right,
+.bs-tooltip-auto[x-placement^='right'] {
+ padding: 0 0.4rem;
+}
+
+.bs-tooltip-right .arrow,
+.bs-tooltip-auto[x-placement^='right'] .arrow {
+ left: 0;
+ width: 0.4rem;
+ height: 0.8rem;
+}
+
+.bs-tooltip-right .arrow::before,
+.bs-tooltip-auto[x-placement^='right'] .arrow::before {
+ right: 0;
+ border-width: 0.4rem 0.4rem 0.4rem 0;
+ border-right-color: #000;
+}
+
+.bs-tooltip-bottom,
+.bs-tooltip-auto[x-placement^='bottom'] {
+ padding: 0.4rem 0;
+}
+
+.bs-tooltip-bottom .arrow,
+.bs-tooltip-auto[x-placement^='bottom'] .arrow {
+ top: 0;
+}
+
+.bs-tooltip-bottom .arrow::before,
+.bs-tooltip-auto[x-placement^='bottom'] .arrow::before {
+ bottom: 0;
+ border-width: 0 0.4rem 0.4rem;
+ border-bottom-color: #000;
+}
+
+.bs-tooltip-left,
+.bs-tooltip-auto[x-placement^='left'] {
+ padding: 0 0.4rem;
+}
+
+.bs-tooltip-left .arrow,
+.bs-tooltip-auto[x-placement^='left'] .arrow {
+ right: 0;
+ width: 0.4rem;
+ height: 0.8rem;
+}
+
+.bs-tooltip-left .arrow::before,
+.bs-tooltip-auto[x-placement^='left'] .arrow::before {
+ left: 0;
+ border-width: 0.4rem 0 0.4rem 0.4rem;
+ border-left-color: #000;
+}
+
+.tooltip-inner {
+ max-width: 200px;
+ padding: 0.25rem 0.5rem;
+ color: #fff;
+ text-align: center;
+ background-color: #000;
+ border-radius: 0.25rem;
+}
+
+.popover {
+ position: absolute;
+ top: 0;
+ left: 0;
+ z-index: 1060;
+ display: block;
+ max-width: 276px;
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif,
+ 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
+ font-style: normal;
+ font-weight: 400;
+ line-height: 1.5;
+ text-align: left;
+ text-align: start;
+ text-decoration: none;
+ text-shadow: none;
+ text-transform: none;
+ letter-spacing: normal;
+ word-break: normal;
+ word-spacing: normal;
+ white-space: normal;
+ line-break: auto;
+ font-size: 0.875rem;
+ word-wrap: break-word;
+ background-color: #fff;
+ background-clip: padding-box;
+ border: 1px solid rgba(0, 0, 0, 0.2);
+ border-radius: 0.3rem;
+}
+
+.popover .arrow {
+ position: absolute;
+ display: block;
+ width: 1rem;
+ height: 0.5rem;
+ margin: 0 0.3rem;
+}
+
+.popover .arrow::before,
+.popover .arrow::after {
+ position: absolute;
+ display: block;
+ content: '';
+ border-color: transparent;
+ border-style: solid;
+}
+
+.bs-popover-top,
+.bs-popover-auto[x-placement^='top'] {
+ margin-bottom: 0.5rem;
+}
+
+.bs-popover-top > .arrow,
+.bs-popover-auto[x-placement^='top'] > .arrow {
+ bottom: calc(-0.5rem - 1px);
+}
+
+.bs-popover-top > .arrow::before,
+.bs-popover-auto[x-placement^='top'] > .arrow::before {
+ bottom: 0;
+ border-width: 0.5rem 0.5rem 0;
+ border-top-color: rgba(0, 0, 0, 0.25);
+}
+
+.bs-popover-top > .arrow::after,
+.bs-popover-auto[x-placement^='top'] > .arrow::after {
+ bottom: 1px;
+ border-width: 0.5rem 0.5rem 0;
+ border-top-color: #fff;
+}
+
+.bs-popover-right,
+.bs-popover-auto[x-placement^='right'] {
+ margin-left: 0.5rem;
+}
+
+.bs-popover-right > .arrow,
+.bs-popover-auto[x-placement^='right'] > .arrow {
+ left: calc(-0.5rem - 1px);
+ width: 0.5rem;
+ height: 1rem;
+ margin: 0.3rem 0;
+}
+
+.bs-popover-right > .arrow::before,
+.bs-popover-auto[x-placement^='right'] > .arrow::before {
+ left: 0;
+ border-width: 0.5rem 0.5rem 0.5rem 0;
+ border-right-color: rgba(0, 0, 0, 0.25);
+}
+
+.bs-popover-right > .arrow::after,
+.bs-popover-auto[x-placement^='right'] > .arrow::after {
+ left: 1px;
+ border-width: 0.5rem 0.5rem 0.5rem 0;
+ border-right-color: #fff;
+}
+
+.bs-popover-bottom,
+.bs-popover-auto[x-placement^='bottom'] {
+ margin-top: 0.5rem;
+}
+
+.bs-popover-bottom > .arrow,
+.bs-popover-auto[x-placement^='bottom'] > .arrow {
+ top: calc(-0.5rem - 1px);
+}
+
+.bs-popover-bottom > .arrow::before,
+.bs-popover-auto[x-placement^='bottom'] > .arrow::before {
+ top: 0;
+ border-width: 0 0.5rem 0.5rem 0.5rem;
+ border-bottom-color: rgba(0, 0, 0, 0.25);
+}
+
+.bs-popover-bottom > .arrow::after,
+.bs-popover-auto[x-placement^='bottom'] > .arrow::after {
+ top: 1px;
+ border-width: 0 0.5rem 0.5rem 0.5rem;
+ border-bottom-color: #fff;
+}
+
+.bs-popover-bottom .popover-header::before,
+.bs-popover-auto[x-placement^='bottom'] .popover-header::before {
+ position: absolute;
+ top: 0;
+ left: 50%;
+ display: block;
+ width: 1rem;
+ margin-left: -0.5rem;
+ content: '';
+ border-bottom: 1px solid #f7f7f7;
+}
+
+.bs-popover-left,
+.bs-popover-auto[x-placement^='left'] {
+ margin-right: 0.5rem;
+}
+
+.bs-popover-left > .arrow,
+.bs-popover-auto[x-placement^='left'] > .arrow {
+ right: calc(-0.5rem - 1px);
+ width: 0.5rem;
+ height: 1rem;
+ margin: 0.3rem 0;
+}
+
+.bs-popover-left > .arrow::before,
+.bs-popover-auto[x-placement^='left'] > .arrow::before {
+ right: 0;
+ border-width: 0.5rem 0 0.5rem 0.5rem;
+ border-left-color: rgba(0, 0, 0, 0.25);
+}
+
+.bs-popover-left > .arrow::after,
+.bs-popover-auto[x-placement^='left'] > .arrow::after {
+ right: 1px;
+ border-width: 0.5rem 0 0.5rem 0.5rem;
+ border-left-color: #fff;
+}
+
+.popover-header {
+ padding: 0.5rem 0.75rem;
+ margin-bottom: 0;
+ font-size: 1rem;
+ background-color: #f7f7f7;
+ border-bottom: 1px solid #ebebeb;
+ border-top-left-radius: calc(0.3rem - 1px);
+ border-top-right-radius: calc(0.3rem - 1px);
+}
+
+.popover-header:empty {
+ display: none;
+}
+
+.popover-body {
+ padding: 0.5rem 0.75rem;
+ color: #212529;
+}
+
+.carousel {
+ position: relative;
+}
+
+.carousel.pointer-event {
+ touch-action: pan-y;
+}
+
+.carousel-inner {
+ position: relative;
+ width: 100%;
+ overflow: hidden;
+}
+
+.carousel-inner::after {
+ display: block;
+ clear: both;
+ content: '';
+}
+
+.carousel-item {
+ position: relative;
+ display: none;
+ float: left;
+ width: 100%;
+ margin-right: -100%;
+ backface-visibility: hidden;
+ transition: transform 0.6s ease-in-out;
+}
+
+.carousel-item.active,
+.carousel-item-next,
+.carousel-item-prev {
+ display: block;
+}
+
+.carousel-item-next:not(.carousel-item-left),
+.active.carousel-item-right {
+ transform: translateX(100%);
+}
+
+.carousel-item-prev:not(.carousel-item-right),
+.active.carousel-item-left {
+ transform: translateX(-100%);
+}
+
+.carousel-fade .carousel-item {
+ opacity: 0;
+ transition-property: opacity;
+ transform: none;
+}
+
+.carousel-fade .carousel-item.active,
+.carousel-fade .carousel-item-next.carousel-item-left,
+.carousel-fade .carousel-item-prev.carousel-item-right {
+ z-index: 1;
+ opacity: 1;
+}
+
+.carousel-fade .active.carousel-item-left,
+.carousel-fade .active.carousel-item-right {
+ z-index: 0;
+ opacity: 0;
+ transition: opacity 0s 0.6s;
+}
+
+.carousel-control-prev,
+.carousel-control-next {
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ z-index: 1;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 15%;
+ color: #fff;
+ text-align: center;
+ opacity: 0.5;
+ transition: opacity 0.15s ease;
+}
+
+.carousel-control-prev:hover,
+.carousel-control-prev:focus,
+.carousel-control-next:hover,
+.carousel-control-next:focus {
+ color: #fff;
+ text-decoration: none;
+ outline: 0;
+ opacity: 0.9;
+}
+
+.carousel-control-prev {
+ left: 0;
+}
+
+.carousel-control-next {
+ right: 0;
+}
+
+.carousel-control-prev-icon,
+.carousel-control-next-icon {
+ display: inline-block;
+ width: 20px;
+ height: 20px;
+ background: no-repeat 50% / 100% 100%;
+}
+
+.carousel-control-prev-icon {
+ background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath d='M5.25 0l-4 4 4 4 1.5-1.5L4.25 4l2.5-2.5L5.25 0z'/%3e%3c/svg%3e");
+}
+
+.carousel-control-next-icon {
+ background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath d='M2.75 0l-1.5 1.5L3.75 4l-2.5 2.5L2.75 8l4-4-4-4z'/%3e%3c/svg%3e");
+}
+
+.carousel-indicators {
+ position: absolute;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ z-index: 15;
+ display: flex;
+ justify-content: center;
+ padding-left: 0;
+ margin-right: 15%;
+ margin-left: 15%;
+ list-style: none;
+}
+
+.carousel-indicators li {
+ box-sizing: content-box;
+ flex: 0 1 auto;
+ width: 30px;
+ height: 3px;
+ margin-right: 3px;
+ margin-left: 3px;
+ text-indent: -999px;
+ cursor: pointer;
+ background-color: #fff;
+ background-clip: padding-box;
+ border-top: 10px solid transparent;
+ border-bottom: 10px solid transparent;
+ opacity: 0.5;
+ transition: opacity 0.6s ease;
+}
+
+.carousel-indicators .active {
+ opacity: 1;
+}
+
+.carousel-caption {
+ position: absolute;
+ right: 15%;
+ bottom: 20px;
+ left: 15%;
+ z-index: 10;
+ padding-top: 20px;
+ padding-bottom: 20px;
+ color: #fff;
+ text-align: center;
+}
+
+@keyframes spinner-border {
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+.spinner-border {
+ display: inline-block;
+ width: 2rem;
+ height: 2rem;
+ vertical-align: text-bottom;
+ border: 0.25em solid currentColor;
+ border-right-color: transparent;
+ border-radius: 50%;
+ animation: spinner-border 0.75s linear infinite;
+}
+
+.spinner-border-sm {
+ width: 1rem;
+ height: 1rem;
+ border-width: 0.2em;
+}
+
+@keyframes spinner-grow {
+ 0% {
+ transform: scale(0);
+ }
+ 50% {
+ opacity: 1;
+ transform: none;
+ }
+}
+
+.spinner-grow {
+ display: inline-block;
+ width: 2rem;
+ height: 2rem;
+ vertical-align: text-bottom;
+ background-color: currentColor;
+ border-radius: 50%;
+ opacity: 0;
+ animation: spinner-grow 0.75s linear infinite;
+}
+
+.spinner-grow-sm {
+ width: 1rem;
+ height: 1rem;
+}
+
+.align-baseline {
+ vertical-align: baseline !important;
+}
+
+.align-top {
+ vertical-align: top !important;
+}
+
+.align-middle {
+ vertical-align: middle !important;
+}
+
+.align-bottom {
+ vertical-align: bottom !important;
+}
+
+.align-text-bottom {
+ vertical-align: text-bottom !important;
+}
+
+.align-text-top {
+ vertical-align: text-top !important;
+}
+
+.bg-primary {
+ background-color: #07819b !important;
+}
+
+a.bg-primary:hover,
+a.bg-primary:focus,
+button.bg-primary:hover,
+button.bg-primary:focus {
+ background-color: #05586a !important;
+}
+
+.bg-secondary {
+ background-color: #313032 !important;
+}
+
+a.bg-secondary:hover,
+a.bg-secondary:focus,
+button.bg-secondary:hover,
+button.bg-secondary:focus {
+ background-color: #181718 !important;
+}
+
+.bg-success {
+ background-color: #28a745 !important;
+}
+
+a.bg-success:hover,
+a.bg-success:focus,
+button.bg-success:hover,
+button.bg-success:focus {
+ background-color: #1e7e34 !important;
+}
+
+.bg-info {
+ background-color: #17a2b8 !important;
+}
+
+a.bg-info:hover,
+a.bg-info:focus,
+button.bg-info:hover,
+button.bg-info:focus {
+ background-color: #117a8b !important;
+}
+
+.bg-warning {
+ background-color: #ffc107 !important;
+}
+
+a.bg-warning:hover,
+a.bg-warning:focus,
+button.bg-warning:hover,
+button.bg-warning:focus {
+ background-color: #d39e00 !important;
+}
+
+.bg-danger {
+ background-color: #dc3545 !important;
+}
+
+a.bg-danger:hover,
+a.bg-danger:focus,
+button.bg-danger:hover,
+button.bg-danger:focus {
+ background-color: #bd2130 !important;
+}
+
+.bg-light {
+ background-color: #f8f9fa !important;
+}
+
+a.bg-light:hover,
+a.bg-light:focus,
+button.bg-light:hover,
+button.bg-light:focus {
+ background-color: #dae0e5 !important;
+}
+
+.bg-dark {
+ background-color: #343a40 !important;
+}
+
+a.bg-dark:hover,
+a.bg-dark:focus,
+button.bg-dark:hover,
+button.bg-dark:focus {
+ background-color: #1d2124 !important;
+}
+
+.bg-white {
+ background-color: #fff !important;
+}
+
+.bg-transparent {
+ background-color: transparent !important;
+}
+
+.border {
+ border: 1px solid #dee2e6 !important;
+}
+
+.border-top {
+ border-top: 1px solid #dee2e6 !important;
+}
+
+.border-right {
+ border-right: 1px solid #dee2e6 !important;
+}
+
+.border-bottom {
+ border-bottom: 1px solid #dee2e6 !important;
+}
+
+.border-left {
+ border-left: 1px solid #dee2e6 !important;
+}
+
+.border-0 {
+ border: 0 !important;
+}
+
+.border-top-0 {
+ border-top: 0 !important;
+}
+
+.border-right-0 {
+ border-right: 0 !important;
+}
+
+.border-bottom-0 {
+ border-bottom: 0 !important;
+}
+
+.border-left-0 {
+ border-left: 0 !important;
+}
+
+.border-primary {
+ border-color: #07819b !important;
+}
+
+.border-secondary {
+ border-color: #313032 !important;
+}
+
+.border-success {
+ border-color: #28a745 !important;
+}
+
+.border-info {
+ border-color: #17a2b8 !important;
+}
+
+.border-warning {
+ border-color: #ffc107 !important;
+}
+
+.border-danger {
+ border-color: #dc3545 !important;
+}
+
+.border-light {
+ border-color: #f8f9fa !important;
+}
+
+.border-dark {
+ border-color: #343a40 !important;
+}
+
+.border-white {
+ border-color: #fff !important;
+}
+
+.rounded-sm {
+ border-radius: 0.2rem !important;
+}
+
+.rounded {
+ border-radius: 0.25rem !important;
+}
+
+.rounded-top {
+ border-top-left-radius: 0.25rem !important;
+ border-top-right-radius: 0.25rem !important;
+}
+
+.rounded-right {
+ border-top-right-radius: 0.25rem !important;
+ border-bottom-right-radius: 0.25rem !important;
+}
+
+.rounded-bottom {
+ border-bottom-right-radius: 0.25rem !important;
+ border-bottom-left-radius: 0.25rem !important;
+}
+
+.rounded-left {
+ border-top-left-radius: 0.25rem !important;
+ border-bottom-left-radius: 0.25rem !important;
+}
+
+.rounded-lg {
+ border-radius: 0.3rem !important;
+}
+
+.rounded-circle {
+ border-radius: 50% !important;
+}
+
+.rounded-pill {
+ border-radius: 50rem !important;
+}
+
+.rounded-0 {
+ border-radius: 0 !important;
+}
+
+.clearfix::after {
+ display: block;
+ clear: both;
+ content: '';
+}
+
+.d-none {
+ display: none !important;
+}
+
+.d-inline {
+ display: inline !important;
+}
+
+.d-inline-block {
+ display: inline-block !important;
+}
+
+.d-block {
+ display: block !important;
+}
+
+.d-table {
+ display: table !important;
+}
+
+.d-table-row {
+ display: table-row !important;
+}
+
+.d-table-cell {
+ display: table-cell !important;
+}
+
+.d-flex {
+ display: flex !important;
+}
+
+.d-inline-flex {
+ display: inline-flex !important;
+}
+
+@media (min-width: 576px) {
+ .d-sm-none {
+ display: none !important;
+ }
+
+ .d-sm-inline {
+ display: inline !important;
+ }
+
+ .d-sm-inline-block {
+ display: inline-block !important;
+ }
+
+ .d-sm-block {
+ display: block !important;
+ }
+
+ .d-sm-table {
+ display: table !important;
+ }
+
+ .d-sm-table-row {
+ display: table-row !important;
+ }
+
+ .d-sm-table-cell {
+ display: table-cell !important;
+ }
+
+ .d-sm-flex {
+ display: flex !important;
+ }
+
+ .d-sm-inline-flex {
+ display: inline-flex !important;
+ }
+}
+
+@media (min-width: 768px) {
+ .d-md-none {
+ display: none !important;
+ }
+
+ .d-md-inline {
+ display: inline !important;
+ }
+
+ .d-md-inline-block {
+ display: inline-block !important;
+ }
+
+ .d-md-block {
+ display: block !important;
+ }
+
+ .d-md-table {
+ display: table !important;
+ }
+
+ .d-md-table-row {
+ display: table-row !important;
+ }
+
+ .d-md-table-cell {
+ display: table-cell !important;
+ }
+
+ .d-md-flex {
+ display: flex !important;
+ }
+
+ .d-md-inline-flex {
+ display: inline-flex !important;
+ }
+}
+
+@media (min-width: 992px) {
+ .d-lg-none {
+ display: none !important;
+ }
+
+ .d-lg-inline {
+ display: inline !important;
+ }
+
+ .d-lg-inline-block {
+ display: inline-block !important;
+ }
+
+ .d-lg-block {
+ display: block !important;
+ }
+
+ .d-lg-table {
+ display: table !important;
+ }
+
+ .d-lg-table-row {
+ display: table-row !important;
+ }
+
+ .d-lg-table-cell {
+ display: table-cell !important;
+ }
+
+ .d-lg-flex {
+ display: flex !important;
+ }
+
+ .d-lg-inline-flex {
+ display: inline-flex !important;
+ }
+}
+
+@media (min-width: 1200px) {
+ .d-xl-none {
+ display: none !important;
+ }
+
+ .d-xl-inline {
+ display: inline !important;
+ }
+
+ .d-xl-inline-block {
+ display: inline-block !important;
+ }
+
+ .d-xl-block {
+ display: block !important;
+ }
+
+ .d-xl-table {
+ display: table !important;
+ }
+
+ .d-xl-table-row {
+ display: table-row !important;
+ }
+
+ .d-xl-table-cell {
+ display: table-cell !important;
+ }
+
+ .d-xl-flex {
+ display: flex !important;
+ }
+
+ .d-xl-inline-flex {
+ display: inline-flex !important;
+ }
+}
+
+@media print {
+ .d-print-none {
+ display: none !important;
+ }
+
+ .d-print-inline {
+ display: inline !important;
+ }
+
+ .d-print-inline-block {
+ display: inline-block !important;
+ }
+
+ .d-print-block {
+ display: block !important;
+ }
+
+ .d-print-table {
+ display: table !important;
+ }
+
+ .d-print-table-row {
+ display: table-row !important;
+ }
+
+ .d-print-table-cell {
+ display: table-cell !important;
+ }
+
+ .d-print-flex {
+ display: flex !important;
+ }
+
+ .d-print-inline-flex {
+ display: inline-flex !important;
+ }
+}
+
+.embed-responsive {
+ position: relative;
+ display: block;
+ width: 100%;
+ padding: 0;
+ overflow: hidden;
+}
+
+.embed-responsive::before {
+ display: block;
+ content: '';
+}
+
+.embed-responsive .embed-responsive-item,
+.embed-responsive iframe,
+.embed-responsive embed,
+.embed-responsive object,
+.embed-responsive video {
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ border: 0;
+}
+
+.embed-responsive-21by9::before {
+ padding-top: 42.85714%;
+}
+
+.embed-responsive-16by9::before {
+ padding-top: 56.25%;
+}
+
+.embed-responsive-4by3::before {
+ padding-top: 75%;
+}
+
+.embed-responsive-1by1::before {
+ padding-top: 100%;
+}
+
+.flex-row {
+ flex-direction: row !important;
+}
+
+.flex-column {
+ flex-direction: column !important;
+}
+
+.flex-row-reverse {
+ flex-direction: row-reverse !important;
+}
+
+.flex-column-reverse {
+ flex-direction: column-reverse !important;
+}
+
+.flex-wrap {
+ flex-wrap: wrap !important;
+}
+
+.flex-nowrap {
+ flex-wrap: nowrap !important;
+}
+
+.flex-wrap-reverse {
+ flex-wrap: wrap-reverse !important;
+}
+
+.flex-fill {
+ flex: 1 1 auto !important;
+}
+
+.flex-grow-0 {
+ flex-grow: 0 !important;
+}
+
+.flex-grow-1 {
+ flex-grow: 1 !important;
+}
+
+.flex-shrink-0 {
+ flex-shrink: 0 !important;
+}
+
+.flex-shrink-1 {
+ flex-shrink: 1 !important;
+}
+
+.justify-content-start {
+ justify-content: flex-start !important;
+}
+
+.justify-content-end {
+ justify-content: flex-end !important;
+}
+
+.justify-content-center {
+ justify-content: center !important;
+}
+
+.justify-content-between {
+ justify-content: space-between !important;
+}
+
+.justify-content-around {
+ justify-content: space-around !important;
+}
+
+.align-items-start {
+ align-items: flex-start !important;
+}
+
+.align-items-end {
+ align-items: flex-end !important;
+}
+
+.align-items-center {
+ align-items: center !important;
+}
+
+.align-items-baseline {
+ align-items: baseline !important;
+}
+
+.align-items-stretch {
+ align-items: stretch !important;
+}
+
+.align-content-start {
+ align-content: flex-start !important;
+}
+
+.align-content-end {
+ align-content: flex-end !important;
+}
+
+.align-content-center {
+ align-content: center !important;
+}
+
+.align-content-between {
+ align-content: space-between !important;
+}
+
+.align-content-around {
+ align-content: space-around !important;
+}
+
+.align-content-stretch {
+ align-content: stretch !important;
+}
+
+.align-self-auto {
+ align-self: auto !important;
+}
+
+.align-self-start {
+ align-self: flex-start !important;
+}
+
+.align-self-end {
+ align-self: flex-end !important;
+}
+
+.align-self-center {
+ align-self: center !important;
+}
+
+.align-self-baseline {
+ align-self: baseline !important;
+}
+
+.align-self-stretch {
+ align-self: stretch !important;
+}
+
+@media (min-width: 576px) {
+ .flex-sm-row {
+ flex-direction: row !important;
+ }
+
+ .flex-sm-column {
+ flex-direction: column !important;
+ }
+
+ .flex-sm-row-reverse {
+ flex-direction: row-reverse !important;
+ }
+
+ .flex-sm-column-reverse {
+ flex-direction: column-reverse !important;
+ }
+
+ .flex-sm-wrap {
+ flex-wrap: wrap !important;
+ }
+
+ .flex-sm-nowrap {
+ flex-wrap: nowrap !important;
+ }
+
+ .flex-sm-wrap-reverse {
+ flex-wrap: wrap-reverse !important;
+ }
+
+ .flex-sm-fill {
+ flex: 1 1 auto !important;
+ }
+
+ .flex-sm-grow-0 {
+ flex-grow: 0 !important;
+ }
+
+ .flex-sm-grow-1 {
+ flex-grow: 1 !important;
+ }
+
+ .flex-sm-shrink-0 {
+ flex-shrink: 0 !important;
+ }
+
+ .flex-sm-shrink-1 {
+ flex-shrink: 1 !important;
+ }
+
+ .justify-content-sm-start {
+ justify-content: flex-start !important;
+ }
+
+ .justify-content-sm-end {
+ justify-content: flex-end !important;
+ }
+
+ .justify-content-sm-center {
+ justify-content: center !important;
+ }
+
+ .justify-content-sm-between {
+ justify-content: space-between !important;
+ }
+
+ .justify-content-sm-around {
+ justify-content: space-around !important;
+ }
+
+ .align-items-sm-start {
+ align-items: flex-start !important;
+ }
+
+ .align-items-sm-end {
+ align-items: flex-end !important;
+ }
+
+ .align-items-sm-center {
+ align-items: center !important;
+ }
+
+ .align-items-sm-baseline {
+ align-items: baseline !important;
+ }
+
+ .align-items-sm-stretch {
+ align-items: stretch !important;
+ }
+
+ .align-content-sm-start {
+ align-content: flex-start !important;
+ }
+
+ .align-content-sm-end {
+ align-content: flex-end !important;
+ }
+
+ .align-content-sm-center {
+ align-content: center !important;
+ }
+
+ .align-content-sm-between {
+ align-content: space-between !important;
+ }
+
+ .align-content-sm-around {
+ align-content: space-around !important;
+ }
+
+ .align-content-sm-stretch {
+ align-content: stretch !important;
+ }
+
+ .align-self-sm-auto {
+ align-self: auto !important;
+ }
+
+ .align-self-sm-start {
+ align-self: flex-start !important;
+ }
+
+ .align-self-sm-end {
+ align-self: flex-end !important;
+ }
+
+ .align-self-sm-center {
+ align-self: center !important;
+ }
+
+ .align-self-sm-baseline {
+ align-self: baseline !important;
+ }
+
+ .align-self-sm-stretch {
+ align-self: stretch !important;
+ }
+}
+
+@media (min-width: 768px) {
+ .flex-md-row {
+ flex-direction: row !important;
+ }
+
+ .flex-md-column {
+ flex-direction: column !important;
+ }
+
+ .flex-md-row-reverse {
+ flex-direction: row-reverse !important;
+ }
+
+ .flex-md-column-reverse {
+ flex-direction: column-reverse !important;
+ }
+
+ .flex-md-wrap {
+ flex-wrap: wrap !important;
+ }
+
+ .flex-md-nowrap {
+ flex-wrap: nowrap !important;
+ }
+
+ .flex-md-wrap-reverse {
+ flex-wrap: wrap-reverse !important;
+ }
+
+ .flex-md-fill {
+ flex: 1 1 auto !important;
+ }
+
+ .flex-md-grow-0 {
+ flex-grow: 0 !important;
+ }
+
+ .flex-md-grow-1 {
+ flex-grow: 1 !important;
+ }
+
+ .flex-md-shrink-0 {
+ flex-shrink: 0 !important;
+ }
+
+ .flex-md-shrink-1 {
+ flex-shrink: 1 !important;
+ }
+
+ .justify-content-md-start {
+ justify-content: flex-start !important;
+ }
+
+ .justify-content-md-end {
+ justify-content: flex-end !important;
+ }
+
+ .justify-content-md-center {
+ justify-content: center !important;
+ }
+
+ .justify-content-md-between {
+ justify-content: space-between !important;
+ }
+
+ .justify-content-md-around {
+ justify-content: space-around !important;
+ }
+
+ .align-items-md-start {
+ align-items: flex-start !important;
+ }
+
+ .align-items-md-end {
+ align-items: flex-end !important;
+ }
+
+ .align-items-md-center {
+ align-items: center !important;
+ }
+
+ .align-items-md-baseline {
+ align-items: baseline !important;
+ }
+
+ .align-items-md-stretch {
+ align-items: stretch !important;
+ }
+
+ .align-content-md-start {
+ align-content: flex-start !important;
+ }
+
+ .align-content-md-end {
+ align-content: flex-end !important;
+ }
+
+ .align-content-md-center {
+ align-content: center !important;
+ }
+
+ .align-content-md-between {
+ align-content: space-between !important;
+ }
+
+ .align-content-md-around {
+ align-content: space-around !important;
+ }
+
+ .align-content-md-stretch {
+ align-content: stretch !important;
+ }
+
+ .align-self-md-auto {
+ align-self: auto !important;
+ }
+
+ .align-self-md-start {
+ align-self: flex-start !important;
+ }
+
+ .align-self-md-end {
+ align-self: flex-end !important;
+ }
+
+ .align-self-md-center {
+ align-self: center !important;
+ }
+
+ .align-self-md-baseline {
+ align-self: baseline !important;
+ }
+
+ .align-self-md-stretch {
+ align-self: stretch !important;
+ }
+}
+
+@media (min-width: 992px) {
+ .flex-lg-row {
+ flex-direction: row !important;
+ }
+
+ .flex-lg-column {
+ flex-direction: column !important;
+ }
+
+ .flex-lg-row-reverse {
+ flex-direction: row-reverse !important;
+ }
+
+ .flex-lg-column-reverse {
+ flex-direction: column-reverse !important;
+ }
+
+ .flex-lg-wrap {
+ flex-wrap: wrap !important;
+ }
+
+ .flex-lg-nowrap {
+ flex-wrap: nowrap !important;
+ }
+
+ .flex-lg-wrap-reverse {
+ flex-wrap: wrap-reverse !important;
+ }
+
+ .flex-lg-fill {
+ flex: 1 1 auto !important;
+ }
+
+ .flex-lg-grow-0 {
+ flex-grow: 0 !important;
+ }
+
+ .flex-lg-grow-1 {
+ flex-grow: 1 !important;
+ }
+
+ .flex-lg-shrink-0 {
+ flex-shrink: 0 !important;
+ }
+
+ .flex-lg-shrink-1 {
+ flex-shrink: 1 !important;
+ }
+
+ .justify-content-lg-start {
+ justify-content: flex-start !important;
+ }
+
+ .justify-content-lg-end {
+ justify-content: flex-end !important;
+ }
+
+ .justify-content-lg-center {
+ justify-content: center !important;
+ }
+
+ .justify-content-lg-between {
+ justify-content: space-between !important;
+ }
+
+ .justify-content-lg-around {
+ justify-content: space-around !important;
+ }
+
+ .align-items-lg-start {
+ align-items: flex-start !important;
+ }
+
+ .align-items-lg-end {
+ align-items: flex-end !important;
+ }
+
+ .align-items-lg-center {
+ align-items: center !important;
+ }
+
+ .align-items-lg-baseline {
+ align-items: baseline !important;
+ }
+
+ .align-items-lg-stretch {
+ align-items: stretch !important;
+ }
+
+ .align-content-lg-start {
+ align-content: flex-start !important;
+ }
+
+ .align-content-lg-end {
+ align-content: flex-end !important;
+ }
+
+ .align-content-lg-center {
+ align-content: center !important;
+ }
+
+ .align-content-lg-between {
+ align-content: space-between !important;
+ }
+
+ .align-content-lg-around {
+ align-content: space-around !important;
+ }
+
+ .align-content-lg-stretch {
+ align-content: stretch !important;
+ }
+
+ .align-self-lg-auto {
+ align-self: auto !important;
+ }
+
+ .align-self-lg-start {
+ align-self: flex-start !important;
+ }
+
+ .align-self-lg-end {
+ align-self: flex-end !important;
+ }
+
+ .align-self-lg-center {
+ align-self: center !important;
+ }
+
+ .align-self-lg-baseline {
+ align-self: baseline !important;
+ }
+
+ .align-self-lg-stretch {
+ align-self: stretch !important;
+ }
+}
+
+@media (min-width: 1200px) {
+ .flex-xl-row {
+ flex-direction: row !important;
+ }
+
+ .flex-xl-column {
+ flex-direction: column !important;
+ }
+
+ .flex-xl-row-reverse {
+ flex-direction: row-reverse !important;
+ }
+
+ .flex-xl-column-reverse {
+ flex-direction: column-reverse !important;
+ }
+
+ .flex-xl-wrap {
+ flex-wrap: wrap !important;
+ }
+
+ .flex-xl-nowrap {
+ flex-wrap: nowrap !important;
+ }
+
+ .flex-xl-wrap-reverse {
+ flex-wrap: wrap-reverse !important;
+ }
+
+ .flex-xl-fill {
+ flex: 1 1 auto !important;
+ }
+
+ .flex-xl-grow-0 {
+ flex-grow: 0 !important;
+ }
+
+ .flex-xl-grow-1 {
+ flex-grow: 1 !important;
+ }
+
+ .flex-xl-shrink-0 {
+ flex-shrink: 0 !important;
+ }
+
+ .flex-xl-shrink-1 {
+ flex-shrink: 1 !important;
+ }
+
+ .justify-content-xl-start {
+ justify-content: flex-start !important;
+ }
+
+ .justify-content-xl-end {
+ justify-content: flex-end !important;
+ }
+
+ .justify-content-xl-center {
+ justify-content: center !important;
+ }
+
+ .justify-content-xl-between {
+ justify-content: space-between !important;
+ }
+
+ .justify-content-xl-around {
+ justify-content: space-around !important;
+ }
+
+ .align-items-xl-start {
+ align-items: flex-start !important;
+ }
+
+ .align-items-xl-end {
+ align-items: flex-end !important;
+ }
+
+ .align-items-xl-center {
+ align-items: center !important;
+ }
+
+ .align-items-xl-baseline {
+ align-items: baseline !important;
+ }
+
+ .align-items-xl-stretch {
+ align-items: stretch !important;
+ }
+
+ .align-content-xl-start {
+ align-content: flex-start !important;
+ }
+
+ .align-content-xl-end {
+ align-content: flex-end !important;
+ }
+
+ .align-content-xl-center {
+ align-content: center !important;
+ }
+
+ .align-content-xl-between {
+ align-content: space-between !important;
+ }
+
+ .align-content-xl-around {
+ align-content: space-around !important;
+ }
+
+ .align-content-xl-stretch {
+ align-content: stretch !important;
+ }
+
+ .align-self-xl-auto {
+ align-self: auto !important;
+ }
+
+ .align-self-xl-start {
+ align-self: flex-start !important;
+ }
+
+ .align-self-xl-end {
+ align-self: flex-end !important;
+ }
+
+ .align-self-xl-center {
+ align-self: center !important;
+ }
+
+ .align-self-xl-baseline {
+ align-self: baseline !important;
+ }
+
+ .align-self-xl-stretch {
+ align-self: stretch !important;
+ }
+}
+
+.float-left {
+ float: left !important;
+}
+
+.float-right {
+ float: right !important;
+}
+
+.float-none {
+ float: none !important;
+}
+
+@media (min-width: 576px) {
+ .float-sm-left {
+ float: left !important;
+ }
+
+ .float-sm-right {
+ float: right !important;
+ }
+
+ .float-sm-none {
+ float: none !important;
+ }
+}
+
+@media (min-width: 768px) {
+ .float-md-left {
+ float: left !important;
+ }
+
+ .float-md-right {
+ float: right !important;
+ }
+
+ .float-md-none {
+ float: none !important;
+ }
+}
+
+@media (min-width: 992px) {
+ .float-lg-left {
+ float: left !important;
+ }
+
+ .float-lg-right {
+ float: right !important;
+ }
+
+ .float-lg-none {
+ float: none !important;
+ }
+}
+
+@media (min-width: 1200px) {
+ .float-xl-left {
+ float: left !important;
+ }
+
+ .float-xl-right {
+ float: right !important;
+ }
+
+ .float-xl-none {
+ float: none !important;
+ }
+}
+
+.user-select-all {
+ user-select: all !important;
+}
+
+.user-select-auto {
+ user-select: auto !important;
+}
+
+.user-select-none {
+ user-select: none !important;
+}
+
+.overflow-auto {
+ overflow: auto !important;
+}
+
+.overflow-hidden {
+ overflow: hidden !important;
+}
+
+.position-static {
+ position: static !important;
+}
+
+.position-relative {
+ position: relative !important;
+}
+
+.position-absolute {
+ position: absolute !important;
+}
+
+.position-fixed {
+ position: fixed !important;
+}
+
+.position-sticky {
+ position: sticky !important;
+}
+
+.fixed-top {
+ position: fixed;
+ top: 0;
+ right: 0;
+ left: 0;
+ z-index: 1030;
+}
+
+.fixed-bottom {
+ position: fixed;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ z-index: 1030;
+}
+
+@supports (position: sticky) {
+ .sticky-top {
+ position: sticky;
+ top: 0;
+ z-index: 1020;
+ }
+}
+
+.sr-only {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ padding: 0;
+ margin: -1px;
+ overflow: hidden;
+ clip: rect(0, 0, 0, 0);
+ white-space: nowrap;
+ border: 0;
+}
+
+.sr-only-focusable:active,
+.sr-only-focusable:focus {
+ position: static;
+ width: auto;
+ height: auto;
+ overflow: visible;
+ clip: auto;
+ white-space: normal;
+}
+
+.shadow-sm {
+ box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075) !important;
+}
+
+.shadow {
+ box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15) !important;
+}
+
+.shadow-lg {
+ box-shadow: 0 1rem 3rem rgba(0, 0, 0, 0.175) !important;
+}
+
+.shadow-none {
+ box-shadow: none !important;
+}
+
+.w-25 {
+ width: 25% !important;
+}
+
+.w-50 {
+ width: 50% !important;
+}
+
+.w-75 {
+ width: 75% !important;
+}
+
+.w-100 {
+ width: 100% !important;
+}
+
+.w-auto {
+ width: auto !important;
+}
+
+.h-25 {
+ height: 25% !important;
+}
+
+.h-50 {
+ height: 50% !important;
+}
+
+.h-75 {
+ height: 75% !important;
+}
+
+.h-100 {
+ height: 100% !important;
+}
+
+.h-auto {
+ height: auto !important;
+}
+
+.mw-100 {
+ max-width: 100% !important;
+}
+
+.mh-100 {
+ max-height: 100% !important;
+}
+
+.min-vw-100 {
+ min-width: 100vw !important;
+}
+
+.min-vh-100 {
+ min-height: 100vh !important;
+}
+
+.vw-100 {
+ width: 100vw !important;
+}
+
+.vh-100 {
+ height: 100vh !important;
+}
+
+.m-0 {
+ margin: 0 !important;
+}
+
+.mt-0,
+.my-0 {
+ margin-top: 0 !important;
+}
+
+.mr-0,
+.mx-0 {
+ margin-right: 0 !important;
+}
+
+.mb-0,
+.my-0 {
+ margin-bottom: 0 !important;
+}
+
+.ml-0,
+.mx-0 {
+ margin-left: 0 !important;
+}
+
+.m-1 {
+ margin: 0.25rem !important;
+}
+
+.mt-1,
+.my-1 {
+ margin-top: 0.25rem !important;
+}
+
+.mr-1,
+.mx-1 {
+ margin-right: 0.25rem !important;
+}
+
+.mb-1,
+.my-1 {
+ margin-bottom: 0.25rem !important;
+}
+
+.ml-1,
+.mx-1 {
+ margin-left: 0.25rem !important;
+}
+
+.m-2 {
+ margin: 0.5rem !important;
+}
+
+.mt-2,
+.my-2 {
+ margin-top: 0.5rem !important;
+}
+
+.mr-2,
+.mx-2 {
+ margin-right: 0.5rem !important;
+}
+
+.mb-2,
+.my-2 {
+ margin-bottom: 0.5rem !important;
+}
+
+.ml-2,
+.mx-2 {
+ margin-left: 0.5rem !important;
+}
+
+.m-3 {
+ margin: 1rem !important;
+}
+
+.mt-3,
+.my-3 {
+ margin-top: 1rem !important;
+}
+
+.mr-3,
+.mx-3 {
+ margin-right: 1rem !important;
+}
+
+.mb-3,
+.my-3 {
+ margin-bottom: 1rem !important;
+}
+
+.ml-3,
+.mx-3 {
+ margin-left: 1rem !important;
+}
+
+.m-4 {
+ margin: 1.5rem !important;
+}
+
+.mt-4,
+.my-4 {
+ margin-top: 1.5rem !important;
+}
+
+.mr-4,
+.mx-4 {
+ margin-right: 1.5rem !important;
+}
+
+.mb-4,
+.my-4 {
+ margin-bottom: 1.5rem !important;
+}
+
+.ml-4,
+.mx-4 {
+ margin-left: 1.5rem !important;
+}
+
+.m-5 {
+ margin: 3rem !important;
+}
+
+.mt-5,
+.my-5 {
+ margin-top: 3rem !important;
+}
+
+.mr-5,
+.mx-5 {
+ margin-right: 3rem !important;
+}
+
+.mb-5,
+.my-5 {
+ margin-bottom: 3rem !important;
+}
+
+.ml-5,
+.mx-5 {
+ margin-left: 3rem !important;
+}
+
+.p-0 {
+ padding: 0 !important;
+}
+
+.pt-0,
+.py-0 {
+ padding-top: 0 !important;
+}
+
+.pr-0,
+.px-0 {
+ padding-right: 0 !important;
+}
+
+.pb-0,
+.py-0 {
+ padding-bottom: 0 !important;
+}
+
+.pl-0,
+.px-0 {
+ padding-left: 0 !important;
+}
+
+.p-1 {
+ padding: 0.25rem !important;
+}
+
+.pt-1,
+.py-1 {
+ padding-top: 0.25rem !important;
+}
+
+.pr-1,
+.px-1 {
+ padding-right: 0.25rem !important;
+}
+
+.pb-1,
+.py-1 {
+ padding-bottom: 0.25rem !important;
+}
+
+.pl-1,
+.px-1 {
+ padding-left: 0.25rem !important;
+}
+
+.p-2 {
+ padding: 0.5rem !important;
+}
+
+.pt-2,
+.py-2 {
+ padding-top: 0.5rem !important;
+}
+
+.pr-2,
+.px-2 {
+ padding-right: 0.5rem !important;
+}
+
+.pb-2,
+.py-2 {
+ padding-bottom: 0.5rem !important;
+}
+
+.pl-2,
+.px-2 {
+ padding-left: 0.5rem !important;
+}
+
+.p-3 {
+ padding: 1rem !important;
+}
+
+.pt-3,
+.py-3 {
+ padding-top: 1rem !important;
+}
+
+.pr-3,
+.px-3 {
+ padding-right: 1rem !important;
+}
+
+.pb-3,
+.py-3 {
+ padding-bottom: 1rem !important;
+}
+
+.pl-3,
+.px-3 {
+ padding-left: 1rem !important;
+}
+
+.p-4 {
+ padding: 1.5rem !important;
+}
+
+.pt-4,
+.py-4 {
+ padding-top: 1.5rem !important;
+}
+
+.pr-4,
+.px-4 {
+ padding-right: 1.5rem !important;
+}
+
+.pb-4,
+.py-4 {
+ padding-bottom: 1.5rem !important;
+}
+
+.pl-4,
+.px-4 {
+ padding-left: 1.5rem !important;
+}
+
+.p-5 {
+ padding: 3rem !important;
+}
+
+.pt-5,
+.py-5 {
+ padding-top: 3rem !important;
+}
+
+.pr-5,
+.px-5 {
+ padding-right: 3rem !important;
+}
+
+.pb-5,
+.py-5 {
+ padding-bottom: 3rem !important;
+}
+
+.pl-5,
+.px-5 {
+ padding-left: 3rem !important;
+}
+
+.m-n1 {
+ margin: -0.25rem !important;
+}
+
+.mt-n1,
+.my-n1 {
+ margin-top: -0.25rem !important;
+}
+
+.mr-n1,
+.mx-n1 {
+ margin-right: -0.25rem !important;
+}
+
+.mb-n1,
+.my-n1 {
+ margin-bottom: -0.25rem !important;
+}
+
+.ml-n1,
+.mx-n1 {
+ margin-left: -0.25rem !important;
+}
+
+.m-n2 {
+ margin: -0.5rem !important;
+}
+
+.mt-n2,
+.my-n2 {
+ margin-top: -0.5rem !important;
+}
+
+.mr-n2,
+.mx-n2 {
+ margin-right: -0.5rem !important;
+}
+
+.mb-n2,
+.my-n2 {
+ margin-bottom: -0.5rem !important;
+}
+
+.ml-n2,
+.mx-n2 {
+ margin-left: -0.5rem !important;
+}
+
+.m-n3 {
+ margin: -1rem !important;
+}
+
+.mt-n3,
+.my-n3 {
+ margin-top: -1rem !important;
+}
+
+.mr-n3,
+.mx-n3 {
+ margin-right: -1rem !important;
+}
+
+.mb-n3,
+.my-n3 {
+ margin-bottom: -1rem !important;
+}
+
+.ml-n3,
+.mx-n3 {
+ margin-left: -1rem !important;
+}
+
+.m-n4 {
+ margin: -1.5rem !important;
+}
+
+.mt-n4,
+.my-n4 {
+ margin-top: -1.5rem !important;
+}
+
+.mr-n4,
+.mx-n4 {
+ margin-right: -1.5rem !important;
+}
+
+.mb-n4,
+.my-n4 {
+ margin-bottom: -1.5rem !important;
+}
+
+.ml-n4,
+.mx-n4 {
+ margin-left: -1.5rem !important;
+}
+
+.m-n5 {
+ margin: -3rem !important;
+}
+
+.mt-n5,
+.my-n5 {
+ margin-top: -3rem !important;
+}
+
+.mr-n5,
+.mx-n5 {
+ margin-right: -3rem !important;
+}
+
+.mb-n5,
+.my-n5 {
+ margin-bottom: -3rem !important;
+}
+
+.ml-n5,
+.mx-n5 {
+ margin-left: -3rem !important;
+}
+
+.m-auto {
+ margin: auto !important;
+}
+
+.mt-auto,
+.my-auto {
+ margin-top: auto !important;
+}
+
+.mr-auto,
+.mx-auto {
+ margin-right: auto !important;
+}
+
+.mb-auto,
+.my-auto {
+ margin-bottom: auto !important;
+}
+
+.ml-auto,
+.mx-auto {
+ margin-left: auto !important;
+}
+
+@media (min-width: 576px) {
+ .m-sm-0 {
+ margin: 0 !important;
+ }
+
+ .mt-sm-0,
+ .my-sm-0 {
+ margin-top: 0 !important;
+ }
+
+ .mr-sm-0,
+ .mx-sm-0 {
+ margin-right: 0 !important;
+ }
+
+ .mb-sm-0,
+ .my-sm-0 {
+ margin-bottom: 0 !important;
+ }
+
+ .ml-sm-0,
+ .mx-sm-0 {
+ margin-left: 0 !important;
+ }
+
+ .m-sm-1 {
+ margin: 0.25rem !important;
+ }
+
+ .mt-sm-1,
+ .my-sm-1 {
+ margin-top: 0.25rem !important;
+ }
+
+ .mr-sm-1,
+ .mx-sm-1 {
+ margin-right: 0.25rem !important;
+ }
+
+ .mb-sm-1,
+ .my-sm-1 {
+ margin-bottom: 0.25rem !important;
+ }
+
+ .ml-sm-1,
+ .mx-sm-1 {
+ margin-left: 0.25rem !important;
+ }
+
+ .m-sm-2 {
+ margin: 0.5rem !important;
+ }
+
+ .mt-sm-2,
+ .my-sm-2 {
+ margin-top: 0.5rem !important;
+ }
+
+ .mr-sm-2,
+ .mx-sm-2 {
+ margin-right: 0.5rem !important;
+ }
+
+ .mb-sm-2,
+ .my-sm-2 {
+ margin-bottom: 0.5rem !important;
+ }
+
+ .ml-sm-2,
+ .mx-sm-2 {
+ margin-left: 0.5rem !important;
+ }
+
+ .m-sm-3 {
+ margin: 1rem !important;
+ }
+
+ .mt-sm-3,
+ .my-sm-3 {
+ margin-top: 1rem !important;
+ }
+
+ .mr-sm-3,
+ .mx-sm-3 {
+ margin-right: 1rem !important;
+ }
+
+ .mb-sm-3,
+ .my-sm-3 {
+ margin-bottom: 1rem !important;
+ }
+
+ .ml-sm-3,
+ .mx-sm-3 {
+ margin-left: 1rem !important;
+ }
+
+ .m-sm-4 {
+ margin: 1.5rem !important;
+ }
+
+ .mt-sm-4,
+ .my-sm-4 {
+ margin-top: 1.5rem !important;
+ }
+
+ .mr-sm-4,
+ .mx-sm-4 {
+ margin-right: 1.5rem !important;
+ }
+
+ .mb-sm-4,
+ .my-sm-4 {
+ margin-bottom: 1.5rem !important;
+ }
+
+ .ml-sm-4,
+ .mx-sm-4 {
+ margin-left: 1.5rem !important;
+ }
+
+ .m-sm-5 {
+ margin: 3rem !important;
+ }
+
+ .mt-sm-5,
+ .my-sm-5 {
+ margin-top: 3rem !important;
+ }
+
+ .mr-sm-5,
+ .mx-sm-5 {
+ margin-right: 3rem !important;
+ }
+
+ .mb-sm-5,
+ .my-sm-5 {
+ margin-bottom: 3rem !important;
+ }
+
+ .ml-sm-5,
+ .mx-sm-5 {
+ margin-left: 3rem !important;
+ }
+
+ .p-sm-0 {
+ padding: 0 !important;
+ }
+
+ .pt-sm-0,
+ .py-sm-0 {
+ padding-top: 0 !important;
+ }
+
+ .pr-sm-0,
+ .px-sm-0 {
+ padding-right: 0 !important;
+ }
+
+ .pb-sm-0,
+ .py-sm-0 {
+ padding-bottom: 0 !important;
+ }
+
+ .pl-sm-0,
+ .px-sm-0 {
+ padding-left: 0 !important;
+ }
+
+ .p-sm-1 {
+ padding: 0.25rem !important;
+ }
+
+ .pt-sm-1,
+ .py-sm-1 {
+ padding-top: 0.25rem !important;
+ }
+
+ .pr-sm-1,
+ .px-sm-1 {
+ padding-right: 0.25rem !important;
+ }
+
+ .pb-sm-1,
+ .py-sm-1 {
+ padding-bottom: 0.25rem !important;
+ }
+
+ .pl-sm-1,
+ .px-sm-1 {
+ padding-left: 0.25rem !important;
+ }
+
+ .p-sm-2 {
+ padding: 0.5rem !important;
+ }
+
+ .pt-sm-2,
+ .py-sm-2 {
+ padding-top: 0.5rem !important;
+ }
+
+ .pr-sm-2,
+ .px-sm-2 {
+ padding-right: 0.5rem !important;
+ }
+
+ .pb-sm-2,
+ .py-sm-2 {
+ padding-bottom: 0.5rem !important;
+ }
+
+ .pl-sm-2,
+ .px-sm-2 {
+ padding-left: 0.5rem !important;
+ }
+
+ .p-sm-3 {
+ padding: 1rem !important;
+ }
+
+ .pt-sm-3,
+ .py-sm-3 {
+ padding-top: 1rem !important;
+ }
+
+ .pr-sm-3,
+ .px-sm-3 {
+ padding-right: 1rem !important;
+ }
+
+ .pb-sm-3,
+ .py-sm-3 {
+ padding-bottom: 1rem !important;
+ }
+
+ .pl-sm-3,
+ .px-sm-3 {
+ padding-left: 1rem !important;
+ }
+
+ .p-sm-4 {
+ padding: 1.5rem !important;
+ }
+
+ .pt-sm-4,
+ .py-sm-4 {
+ padding-top: 1.5rem !important;
+ }
+
+ .pr-sm-4,
+ .px-sm-4 {
+ padding-right: 1.5rem !important;
+ }
+
+ .pb-sm-4,
+ .py-sm-4 {
+ padding-bottom: 1.5rem !important;
+ }
+
+ .pl-sm-4,
+ .px-sm-4 {
+ padding-left: 1.5rem !important;
+ }
+
+ .p-sm-5 {
+ padding: 3rem !important;
+ }
+
+ .pt-sm-5,
+ .py-sm-5 {
+ padding-top: 3rem !important;
+ }
+
+ .pr-sm-5,
+ .px-sm-5 {
+ padding-right: 3rem !important;
+ }
+
+ .pb-sm-5,
+ .py-sm-5 {
+ padding-bottom: 3rem !important;
+ }
+
+ .pl-sm-5,
+ .px-sm-5 {
+ padding-left: 3rem !important;
+ }
+
+ .m-sm-n1 {
+ margin: -0.25rem !important;
+ }
+
+ .mt-sm-n1,
+ .my-sm-n1 {
+ margin-top: -0.25rem !important;
+ }
+
+ .mr-sm-n1,
+ .mx-sm-n1 {
+ margin-right: -0.25rem !important;
+ }
+
+ .mb-sm-n1,
+ .my-sm-n1 {
+ margin-bottom: -0.25rem !important;
+ }
+
+ .ml-sm-n1,
+ .mx-sm-n1 {
+ margin-left: -0.25rem !important;
+ }
+
+ .m-sm-n2 {
+ margin: -0.5rem !important;
+ }
+
+ .mt-sm-n2,
+ .my-sm-n2 {
+ margin-top: -0.5rem !important;
+ }
+
+ .mr-sm-n2,
+ .mx-sm-n2 {
+ margin-right: -0.5rem !important;
+ }
+
+ .mb-sm-n2,
+ .my-sm-n2 {
+ margin-bottom: -0.5rem !important;
+ }
+
+ .ml-sm-n2,
+ .mx-sm-n2 {
+ margin-left: -0.5rem !important;
+ }
+
+ .m-sm-n3 {
+ margin: -1rem !important;
+ }
+
+ .mt-sm-n3,
+ .my-sm-n3 {
+ margin-top: -1rem !important;
+ }
+
+ .mr-sm-n3,
+ .mx-sm-n3 {
+ margin-right: -1rem !important;
+ }
+
+ .mb-sm-n3,
+ .my-sm-n3 {
+ margin-bottom: -1rem !important;
+ }
+
+ .ml-sm-n3,
+ .mx-sm-n3 {
+ margin-left: -1rem !important;
+ }
+
+ .m-sm-n4 {
+ margin: -1.5rem !important;
+ }
+
+ .mt-sm-n4,
+ .my-sm-n4 {
+ margin-top: -1.5rem !important;
+ }
+
+ .mr-sm-n4,
+ .mx-sm-n4 {
+ margin-right: -1.5rem !important;
+ }
+
+ .mb-sm-n4,
+ .my-sm-n4 {
+ margin-bottom: -1.5rem !important;
+ }
+
+ .ml-sm-n4,
+ .mx-sm-n4 {
+ margin-left: -1.5rem !important;
+ }
+
+ .m-sm-n5 {
+ margin: -3rem !important;
+ }
+
+ .mt-sm-n5,
+ .my-sm-n5 {
+ margin-top: -3rem !important;
+ }
+
+ .mr-sm-n5,
+ .mx-sm-n5 {
+ margin-right: -3rem !important;
+ }
+
+ .mb-sm-n5,
+ .my-sm-n5 {
+ margin-bottom: -3rem !important;
+ }
+
+ .ml-sm-n5,
+ .mx-sm-n5 {
+ margin-left: -3rem !important;
+ }
+
+ .m-sm-auto {
+ margin: auto !important;
+ }
+
+ .mt-sm-auto,
+ .my-sm-auto {
+ margin-top: auto !important;
+ }
+
+ .mr-sm-auto,
+ .mx-sm-auto {
+ margin-right: auto !important;
+ }
+
+ .mb-sm-auto,
+ .my-sm-auto {
+ margin-bottom: auto !important;
+ }
+
+ .ml-sm-auto,
+ .mx-sm-auto {
+ margin-left: auto !important;
+ }
+}
+
+@media (min-width: 768px) {
+ .m-md-0 {
+ margin: 0 !important;
+ }
+
+ .mt-md-0,
+ .my-md-0 {
+ margin-top: 0 !important;
+ }
+
+ .mr-md-0,
+ .mx-md-0 {
+ margin-right: 0 !important;
+ }
+
+ .mb-md-0,
+ .my-md-0 {
+ margin-bottom: 0 !important;
+ }
+
+ .ml-md-0,
+ .mx-md-0 {
+ margin-left: 0 !important;
+ }
+
+ .m-md-1 {
+ margin: 0.25rem !important;
+ }
+
+ .mt-md-1,
+ .my-md-1 {
+ margin-top: 0.25rem !important;
+ }
+
+ .mr-md-1,
+ .mx-md-1 {
+ margin-right: 0.25rem !important;
+ }
+
+ .mb-md-1,
+ .my-md-1 {
+ margin-bottom: 0.25rem !important;
+ }
+
+ .ml-md-1,
+ .mx-md-1 {
+ margin-left: 0.25rem !important;
+ }
+
+ .m-md-2 {
+ margin: 0.5rem !important;
+ }
+
+ .mt-md-2,
+ .my-md-2 {
+ margin-top: 0.5rem !important;
+ }
+
+ .mr-md-2,
+ .mx-md-2 {
+ margin-right: 0.5rem !important;
+ }
+
+ .mb-md-2,
+ .my-md-2 {
+ margin-bottom: 0.5rem !important;
+ }
+
+ .ml-md-2,
+ .mx-md-2 {
+ margin-left: 0.5rem !important;
+ }
+
+ .m-md-3 {
+ margin: 1rem !important;
+ }
+
+ .mt-md-3,
+ .my-md-3 {
+ margin-top: 1rem !important;
+ }
+
+ .mr-md-3,
+ .mx-md-3 {
+ margin-right: 1rem !important;
+ }
+
+ .mb-md-3,
+ .my-md-3 {
+ margin-bottom: 1rem !important;
+ }
+
+ .ml-md-3,
+ .mx-md-3 {
+ margin-left: 1rem !important;
+ }
+
+ .m-md-4 {
+ margin: 1.5rem !important;
+ }
+
+ .mt-md-4,
+ .my-md-4 {
+ margin-top: 1.5rem !important;
+ }
+
+ .mr-md-4,
+ .mx-md-4 {
+ margin-right: 1.5rem !important;
+ }
+
+ .mb-md-4,
+ .my-md-4 {
+ margin-bottom: 1.5rem !important;
+ }
+
+ .ml-md-4,
+ .mx-md-4 {
+ margin-left: 1.5rem !important;
+ }
+
+ .m-md-5 {
+ margin: 3rem !important;
+ }
+
+ .mt-md-5,
+ .my-md-5 {
+ margin-top: 3rem !important;
+ }
+
+ .mr-md-5,
+ .mx-md-5 {
+ margin-right: 3rem !important;
+ }
+
+ .mb-md-5,
+ .my-md-5 {
+ margin-bottom: 3rem !important;
+ }
+
+ .ml-md-5,
+ .mx-md-5 {
+ margin-left: 3rem !important;
+ }
+
+ .p-md-0 {
+ padding: 0 !important;
+ }
+
+ .pt-md-0,
+ .py-md-0 {
+ padding-top: 0 !important;
+ }
+
+ .pr-md-0,
+ .px-md-0 {
+ padding-right: 0 !important;
+ }
+
+ .pb-md-0,
+ .py-md-0 {
+ padding-bottom: 0 !important;
+ }
+
+ .pl-md-0,
+ .px-md-0 {
+ padding-left: 0 !important;
+ }
+
+ .p-md-1 {
+ padding: 0.25rem !important;
+ }
+
+ .pt-md-1,
+ .py-md-1 {
+ padding-top: 0.25rem !important;
+ }
+
+ .pr-md-1,
+ .px-md-1 {
+ padding-right: 0.25rem !important;
+ }
+
+ .pb-md-1,
+ .py-md-1 {
+ padding-bottom: 0.25rem !important;
+ }
+
+ .pl-md-1,
+ .px-md-1 {
+ padding-left: 0.25rem !important;
+ }
+
+ .p-md-2 {
+ padding: 0.5rem !important;
+ }
+
+ .pt-md-2,
+ .py-md-2 {
+ padding-top: 0.5rem !important;
+ }
+
+ .pr-md-2,
+ .px-md-2 {
+ padding-right: 0.5rem !important;
+ }
+
+ .pb-md-2,
+ .py-md-2 {
+ padding-bottom: 0.5rem !important;
+ }
+
+ .pl-md-2,
+ .px-md-2 {
+ padding-left: 0.5rem !important;
+ }
+
+ .p-md-3 {
+ padding: 1rem !important;
+ }
+
+ .pt-md-3,
+ .py-md-3 {
+ padding-top: 1rem !important;
+ }
+
+ .pr-md-3,
+ .px-md-3 {
+ padding-right: 1rem !important;
+ }
+
+ .pb-md-3,
+ .py-md-3 {
+ padding-bottom: 1rem !important;
+ }
+
+ .pl-md-3,
+ .px-md-3 {
+ padding-left: 1rem !important;
+ }
+
+ .p-md-4 {
+ padding: 1.5rem !important;
+ }
+
+ .pt-md-4,
+ .py-md-4 {
+ padding-top: 1.5rem !important;
+ }
+
+ .pr-md-4,
+ .px-md-4 {
+ padding-right: 1.5rem !important;
+ }
+
+ .pb-md-4,
+ .py-md-4 {
+ padding-bottom: 1.5rem !important;
+ }
+
+ .pl-md-4,
+ .px-md-4 {
+ padding-left: 1.5rem !important;
+ }
+
+ .p-md-5 {
+ padding: 3rem !important;
+ }
+
+ .pt-md-5,
+ .py-md-5 {
+ padding-top: 3rem !important;
+ }
+
+ .pr-md-5,
+ .px-md-5 {
+ padding-right: 3rem !important;
+ }
+
+ .pb-md-5,
+ .py-md-5 {
+ padding-bottom: 3rem !important;
+ }
+
+ .pl-md-5,
+ .px-md-5 {
+ padding-left: 3rem !important;
+ }
+
+ .m-md-n1 {
+ margin: -0.25rem !important;
+ }
+
+ .mt-md-n1,
+ .my-md-n1 {
+ margin-top: -0.25rem !important;
+ }
+
+ .mr-md-n1,
+ .mx-md-n1 {
+ margin-right: -0.25rem !important;
+ }
+
+ .mb-md-n1,
+ .my-md-n1 {
+ margin-bottom: -0.25rem !important;
+ }
+
+ .ml-md-n1,
+ .mx-md-n1 {
+ margin-left: -0.25rem !important;
+ }
+
+ .m-md-n2 {
+ margin: -0.5rem !important;
+ }
+
+ .mt-md-n2,
+ .my-md-n2 {
+ margin-top: -0.5rem !important;
+ }
+
+ .mr-md-n2,
+ .mx-md-n2 {
+ margin-right: -0.5rem !important;
+ }
+
+ .mb-md-n2,
+ .my-md-n2 {
+ margin-bottom: -0.5rem !important;
+ }
+
+ .ml-md-n2,
+ .mx-md-n2 {
+ margin-left: -0.5rem !important;
+ }
+
+ .m-md-n3 {
+ margin: -1rem !important;
+ }
+
+ .mt-md-n3,
+ .my-md-n3 {
+ margin-top: -1rem !important;
+ }
+
+ .mr-md-n3,
+ .mx-md-n3 {
+ margin-right: -1rem !important;
+ }
+
+ .mb-md-n3,
+ .my-md-n3 {
+ margin-bottom: -1rem !important;
+ }
+
+ .ml-md-n3,
+ .mx-md-n3 {
+ margin-left: -1rem !important;
+ }
+
+ .m-md-n4 {
+ margin: -1.5rem !important;
+ }
+
+ .mt-md-n4,
+ .my-md-n4 {
+ margin-top: -1.5rem !important;
+ }
+
+ .mr-md-n4,
+ .mx-md-n4 {
+ margin-right: -1.5rem !important;
+ }
+
+ .mb-md-n4,
+ .my-md-n4 {
+ margin-bottom: -1.5rem !important;
+ }
+
+ .ml-md-n4,
+ .mx-md-n4 {
+ margin-left: -1.5rem !important;
+ }
+
+ .m-md-n5 {
+ margin: -3rem !important;
+ }
+
+ .mt-md-n5,
+ .my-md-n5 {
+ margin-top: -3rem !important;
+ }
+
+ .mr-md-n5,
+ .mx-md-n5 {
+ margin-right: -3rem !important;
+ }
+
+ .mb-md-n5,
+ .my-md-n5 {
+ margin-bottom: -3rem !important;
+ }
+
+ .ml-md-n5,
+ .mx-md-n5 {
+ margin-left: -3rem !important;
+ }
+
+ .m-md-auto {
+ margin: auto !important;
+ }
+
+ .mt-md-auto,
+ .my-md-auto {
+ margin-top: auto !important;
+ }
+
+ .mr-md-auto,
+ .mx-md-auto {
+ margin-right: auto !important;
+ }
+
+ .mb-md-auto,
+ .my-md-auto {
+ margin-bottom: auto !important;
+ }
+
+ .ml-md-auto,
+ .mx-md-auto {
+ margin-left: auto !important;
+ }
+}
+
+@media (min-width: 992px) {
+ .m-lg-0 {
+ margin: 0 !important;
+ }
+
+ .mt-lg-0,
+ .my-lg-0 {
+ margin-top: 0 !important;
+ }
+
+ .mr-lg-0,
+ .mx-lg-0 {
+ margin-right: 0 !important;
+ }
+
+ .mb-lg-0,
+ .my-lg-0 {
+ margin-bottom: 0 !important;
+ }
+
+ .ml-lg-0,
+ .mx-lg-0 {
+ margin-left: 0 !important;
+ }
+
+ .m-lg-1 {
+ margin: 0.25rem !important;
+ }
+
+ .mt-lg-1,
+ .my-lg-1 {
+ margin-top: 0.25rem !important;
+ }
+
+ .mr-lg-1,
+ .mx-lg-1 {
+ margin-right: 0.25rem !important;
+ }
+
+ .mb-lg-1,
+ .my-lg-1 {
+ margin-bottom: 0.25rem !important;
+ }
+
+ .ml-lg-1,
+ .mx-lg-1 {
+ margin-left: 0.25rem !important;
+ }
+
+ .m-lg-2 {
+ margin: 0.5rem !important;
+ }
+
+ .mt-lg-2,
+ .my-lg-2 {
+ margin-top: 0.5rem !important;
+ }
+
+ .mr-lg-2,
+ .mx-lg-2 {
+ margin-right: 0.5rem !important;
+ }
+
+ .mb-lg-2,
+ .my-lg-2 {
+ margin-bottom: 0.5rem !important;
+ }
+
+ .ml-lg-2,
+ .mx-lg-2 {
+ margin-left: 0.5rem !important;
+ }
+
+ .m-lg-3 {
+ margin: 1rem !important;
+ }
+
+ .mt-lg-3,
+ .my-lg-3 {
+ margin-top: 1rem !important;
+ }
+
+ .mr-lg-3,
+ .mx-lg-3 {
+ margin-right: 1rem !important;
+ }
+
+ .mb-lg-3,
+ .my-lg-3 {
+ margin-bottom: 1rem !important;
+ }
+
+ .ml-lg-3,
+ .mx-lg-3 {
+ margin-left: 1rem !important;
+ }
+
+ .m-lg-4 {
+ margin: 1.5rem !important;
+ }
+
+ .mt-lg-4,
+ .my-lg-4 {
+ margin-top: 1.5rem !important;
+ }
+
+ .mr-lg-4,
+ .mx-lg-4 {
+ margin-right: 1.5rem !important;
+ }
+
+ .mb-lg-4,
+ .my-lg-4 {
+ margin-bottom: 1.5rem !important;
+ }
+
+ .ml-lg-4,
+ .mx-lg-4 {
+ margin-left: 1.5rem !important;
+ }
+
+ .m-lg-5 {
+ margin: 3rem !important;
+ }
+
+ .mt-lg-5,
+ .my-lg-5 {
+ margin-top: 3rem !important;
+ }
+
+ .mr-lg-5,
+ .mx-lg-5 {
+ margin-right: 3rem !important;
+ }
+
+ .mb-lg-5,
+ .my-lg-5 {
+ margin-bottom: 3rem !important;
+ }
+
+ .ml-lg-5,
+ .mx-lg-5 {
+ margin-left: 3rem !important;
+ }
+
+ .p-lg-0 {
+ padding: 0 !important;
+ }
+
+ .pt-lg-0,
+ .py-lg-0 {
+ padding-top: 0 !important;
+ }
+
+ .pr-lg-0,
+ .px-lg-0 {
+ padding-right: 0 !important;
+ }
+
+ .pb-lg-0,
+ .py-lg-0 {
+ padding-bottom: 0 !important;
+ }
+
+ .pl-lg-0,
+ .px-lg-0 {
+ padding-left: 0 !important;
+ }
+
+ .p-lg-1 {
+ padding: 0.25rem !important;
+ }
+
+ .pt-lg-1,
+ .py-lg-1 {
+ padding-top: 0.25rem !important;
+ }
+
+ .pr-lg-1,
+ .px-lg-1 {
+ padding-right: 0.25rem !important;
+ }
+
+ .pb-lg-1,
+ .py-lg-1 {
+ padding-bottom: 0.25rem !important;
+ }
+
+ .pl-lg-1,
+ .px-lg-1 {
+ padding-left: 0.25rem !important;
+ }
+
+ .p-lg-2 {
+ padding: 0.5rem !important;
+ }
+
+ .pt-lg-2,
+ .py-lg-2 {
+ padding-top: 0.5rem !important;
+ }
+
+ .pr-lg-2,
+ .px-lg-2 {
+ padding-right: 0.5rem !important;
+ }
+
+ .pb-lg-2,
+ .py-lg-2 {
+ padding-bottom: 0.5rem !important;
+ }
+
+ .pl-lg-2,
+ .px-lg-2 {
+ padding-left: 0.5rem !important;
+ }
+
+ .p-lg-3 {
+ padding: 1rem !important;
+ }
+
+ .pt-lg-3,
+ .py-lg-3 {
+ padding-top: 1rem !important;
+ }
+
+ .pr-lg-3,
+ .px-lg-3 {
+ padding-right: 1rem !important;
+ }
+
+ .pb-lg-3,
+ .py-lg-3 {
+ padding-bottom: 1rem !important;
+ }
+
+ .pl-lg-3,
+ .px-lg-3 {
+ padding-left: 1rem !important;
+ }
+
+ .p-lg-4 {
+ padding: 1.5rem !important;
+ }
+
+ .pt-lg-4,
+ .py-lg-4 {
+ padding-top: 1.5rem !important;
+ }
+
+ .pr-lg-4,
+ .px-lg-4 {
+ padding-right: 1.5rem !important;
+ }
+
+ .pb-lg-4,
+ .py-lg-4 {
+ padding-bottom: 1.5rem !important;
+ }
+
+ .pl-lg-4,
+ .px-lg-4 {
+ padding-left: 1.5rem !important;
+ }
+
+ .p-lg-5 {
+ padding: 3rem !important;
+ }
+
+ .pt-lg-5,
+ .py-lg-5 {
+ padding-top: 3rem !important;
+ }
+
+ .pr-lg-5,
+ .px-lg-5 {
+ padding-right: 3rem !important;
+ }
+
+ .pb-lg-5,
+ .py-lg-5 {
+ padding-bottom: 3rem !important;
+ }
+
+ .pl-lg-5,
+ .px-lg-5 {
+ padding-left: 3rem !important;
+ }
+
+ .m-lg-n1 {
+ margin: -0.25rem !important;
+ }
+
+ .mt-lg-n1,
+ .my-lg-n1 {
+ margin-top: -0.25rem !important;
+ }
+
+ .mr-lg-n1,
+ .mx-lg-n1 {
+ margin-right: -0.25rem !important;
+ }
+
+ .mb-lg-n1,
+ .my-lg-n1 {
+ margin-bottom: -0.25rem !important;
+ }
+
+ .ml-lg-n1,
+ .mx-lg-n1 {
+ margin-left: -0.25rem !important;
+ }
+
+ .m-lg-n2 {
+ margin: -0.5rem !important;
+ }
+
+ .mt-lg-n2,
+ .my-lg-n2 {
+ margin-top: -0.5rem !important;
+ }
+
+ .mr-lg-n2,
+ .mx-lg-n2 {
+ margin-right: -0.5rem !important;
+ }
+
+ .mb-lg-n2,
+ .my-lg-n2 {
+ margin-bottom: -0.5rem !important;
+ }
+
+ .ml-lg-n2,
+ .mx-lg-n2 {
+ margin-left: -0.5rem !important;
+ }
+
+ .m-lg-n3 {
+ margin: -1rem !important;
+ }
+
+ .mt-lg-n3,
+ .my-lg-n3 {
+ margin-top: -1rem !important;
+ }
+
+ .mr-lg-n3,
+ .mx-lg-n3 {
+ margin-right: -1rem !important;
+ }
+
+ .mb-lg-n3,
+ .my-lg-n3 {
+ margin-bottom: -1rem !important;
+ }
+
+ .ml-lg-n3,
+ .mx-lg-n3 {
+ margin-left: -1rem !important;
+ }
+
+ .m-lg-n4 {
+ margin: -1.5rem !important;
+ }
+
+ .mt-lg-n4,
+ .my-lg-n4 {
+ margin-top: -1.5rem !important;
+ }
+
+ .mr-lg-n4,
+ .mx-lg-n4 {
+ margin-right: -1.5rem !important;
+ }
+
+ .mb-lg-n4,
+ .my-lg-n4 {
+ margin-bottom: -1.5rem !important;
+ }
+
+ .ml-lg-n4,
+ .mx-lg-n4 {
+ margin-left: -1.5rem !important;
+ }
+
+ .m-lg-n5 {
+ margin: -3rem !important;
+ }
+
+ .mt-lg-n5,
+ .my-lg-n5 {
+ margin-top: -3rem !important;
+ }
+
+ .mr-lg-n5,
+ .mx-lg-n5 {
+ margin-right: -3rem !important;
+ }
+
+ .mb-lg-n5,
+ .my-lg-n5 {
+ margin-bottom: -3rem !important;
+ }
+
+ .ml-lg-n5,
+ .mx-lg-n5 {
+ margin-left: -3rem !important;
+ }
+
+ .m-lg-auto {
+ margin: auto !important;
+ }
+
+ .mt-lg-auto,
+ .my-lg-auto {
+ margin-top: auto !important;
+ }
+
+ .mr-lg-auto,
+ .mx-lg-auto {
+ margin-right: auto !important;
+ }
+
+ .mb-lg-auto,
+ .my-lg-auto {
+ margin-bottom: auto !important;
+ }
+
+ .ml-lg-auto,
+ .mx-lg-auto {
+ margin-left: auto !important;
+ }
+}
+
+@media (min-width: 1200px) {
+ .m-xl-0 {
+ margin: 0 !important;
+ }
+
+ .mt-xl-0,
+ .my-xl-0 {
+ margin-top: 0 !important;
+ }
+
+ .mr-xl-0,
+ .mx-xl-0 {
+ margin-right: 0 !important;
+ }
+
+ .mb-xl-0,
+ .my-xl-0 {
+ margin-bottom: 0 !important;
+ }
+
+ .ml-xl-0,
+ .mx-xl-0 {
+ margin-left: 0 !important;
+ }
+
+ .m-xl-1 {
+ margin: 0.25rem !important;
+ }
+
+ .mt-xl-1,
+ .my-xl-1 {
+ margin-top: 0.25rem !important;
+ }
+
+ .mr-xl-1,
+ .mx-xl-1 {
+ margin-right: 0.25rem !important;
+ }
+
+ .mb-xl-1,
+ .my-xl-1 {
+ margin-bottom: 0.25rem !important;
+ }
+
+ .ml-xl-1,
+ .mx-xl-1 {
+ margin-left: 0.25rem !important;
+ }
+
+ .m-xl-2 {
+ margin: 0.5rem !important;
+ }
+
+ .mt-xl-2,
+ .my-xl-2 {
+ margin-top: 0.5rem !important;
+ }
+
+ .mr-xl-2,
+ .mx-xl-2 {
+ margin-right: 0.5rem !important;
+ }
+
+ .mb-xl-2,
+ .my-xl-2 {
+ margin-bottom: 0.5rem !important;
+ }
+
+ .ml-xl-2,
+ .mx-xl-2 {
+ margin-left: 0.5rem !important;
+ }
+
+ .m-xl-3 {
+ margin: 1rem !important;
+ }
+
+ .mt-xl-3,
+ .my-xl-3 {
+ margin-top: 1rem !important;
+ }
+
+ .mr-xl-3,
+ .mx-xl-3 {
+ margin-right: 1rem !important;
+ }
+
+ .mb-xl-3,
+ .my-xl-3 {
+ margin-bottom: 1rem !important;
+ }
+
+ .ml-xl-3,
+ .mx-xl-3 {
+ margin-left: 1rem !important;
+ }
+
+ .m-xl-4 {
+ margin: 1.5rem !important;
+ }
+
+ .mt-xl-4,
+ .my-xl-4 {
+ margin-top: 1.5rem !important;
+ }
+
+ .mr-xl-4,
+ .mx-xl-4 {
+ margin-right: 1.5rem !important;
+ }
+
+ .mb-xl-4,
+ .my-xl-4 {
+ margin-bottom: 1.5rem !important;
+ }
+
+ .ml-xl-4,
+ .mx-xl-4 {
+ margin-left: 1.5rem !important;
+ }
+
+ .m-xl-5 {
+ margin: 3rem !important;
+ }
+
+ .mt-xl-5,
+ .my-xl-5 {
+ margin-top: 3rem !important;
+ }
+
+ .mr-xl-5,
+ .mx-xl-5 {
+ margin-right: 3rem !important;
+ }
+
+ .mb-xl-5,
+ .my-xl-5 {
+ margin-bottom: 3rem !important;
+ }
+
+ .ml-xl-5,
+ .mx-xl-5 {
+ margin-left: 3rem !important;
+ }
+
+ .p-xl-0 {
+ padding: 0 !important;
+ }
+
+ .pt-xl-0,
+ .py-xl-0 {
+ padding-top: 0 !important;
+ }
+
+ .pr-xl-0,
+ .px-xl-0 {
+ padding-right: 0 !important;
+ }
+
+ .pb-xl-0,
+ .py-xl-0 {
+ padding-bottom: 0 !important;
+ }
+
+ .pl-xl-0,
+ .px-xl-0 {
+ padding-left: 0 !important;
+ }
+
+ .p-xl-1 {
+ padding: 0.25rem !important;
+ }
+
+ .pt-xl-1,
+ .py-xl-1 {
+ padding-top: 0.25rem !important;
+ }
+
+ .pr-xl-1,
+ .px-xl-1 {
+ padding-right: 0.25rem !important;
+ }
+
+ .pb-xl-1,
+ .py-xl-1 {
+ padding-bottom: 0.25rem !important;
+ }
+
+ .pl-xl-1,
+ .px-xl-1 {
+ padding-left: 0.25rem !important;
+ }
+
+ .p-xl-2 {
+ padding: 0.5rem !important;
+ }
+
+ .pt-xl-2,
+ .py-xl-2 {
+ padding-top: 0.5rem !important;
+ }
+
+ .pr-xl-2,
+ .px-xl-2 {
+ padding-right: 0.5rem !important;
+ }
+
+ .pb-xl-2,
+ .py-xl-2 {
+ padding-bottom: 0.5rem !important;
+ }
+
+ .pl-xl-2,
+ .px-xl-2 {
+ padding-left: 0.5rem !important;
+ }
+
+ .p-xl-3 {
+ padding: 1rem !important;
+ }
+
+ .pt-xl-3,
+ .py-xl-3 {
+ padding-top: 1rem !important;
+ }
+
+ .pr-xl-3,
+ .px-xl-3 {
+ padding-right: 1rem !important;
+ }
+
+ .pb-xl-3,
+ .py-xl-3 {
+ padding-bottom: 1rem !important;
+ }
+
+ .pl-xl-3,
+ .px-xl-3 {
+ padding-left: 1rem !important;
+ }
+
+ .p-xl-4 {
+ padding: 1.5rem !important;
+ }
+
+ .pt-xl-4,
+ .py-xl-4 {
+ padding-top: 1.5rem !important;
+ }
+
+ .pr-xl-4,
+ .px-xl-4 {
+ padding-right: 1.5rem !important;
+ }
+
+ .pb-xl-4,
+ .py-xl-4 {
+ padding-bottom: 1.5rem !important;
+ }
+
+ .pl-xl-4,
+ .px-xl-4 {
+ padding-left: 1.5rem !important;
+ }
+
+ .p-xl-5 {
+ padding: 3rem !important;
+ }
+
+ .pt-xl-5,
+ .py-xl-5 {
+ padding-top: 3rem !important;
+ }
+
+ .pr-xl-5,
+ .px-xl-5 {
+ padding-right: 3rem !important;
+ }
+
+ .pb-xl-5,
+ .py-xl-5 {
+ padding-bottom: 3rem !important;
+ }
+
+ .pl-xl-5,
+ .px-xl-5 {
+ padding-left: 3rem !important;
+ }
+
+ .m-xl-n1 {
+ margin: -0.25rem !important;
+ }
+
+ .mt-xl-n1,
+ .my-xl-n1 {
+ margin-top: -0.25rem !important;
+ }
+
+ .mr-xl-n1,
+ .mx-xl-n1 {
+ margin-right: -0.25rem !important;
+ }
+
+ .mb-xl-n1,
+ .my-xl-n1 {
+ margin-bottom: -0.25rem !important;
+ }
+
+ .ml-xl-n1,
+ .mx-xl-n1 {
+ margin-left: -0.25rem !important;
+ }
+
+ .m-xl-n2 {
+ margin: -0.5rem !important;
+ }
+
+ .mt-xl-n2,
+ .my-xl-n2 {
+ margin-top: -0.5rem !important;
+ }
+
+ .mr-xl-n2,
+ .mx-xl-n2 {
+ margin-right: -0.5rem !important;
+ }
+
+ .mb-xl-n2,
+ .my-xl-n2 {
+ margin-bottom: -0.5rem !important;
+ }
+
+ .ml-xl-n2,
+ .mx-xl-n2 {
+ margin-left: -0.5rem !important;
+ }
+
+ .m-xl-n3 {
+ margin: -1rem !important;
+ }
+
+ .mt-xl-n3,
+ .my-xl-n3 {
+ margin-top: -1rem !important;
+ }
+
+ .mr-xl-n3,
+ .mx-xl-n3 {
+ margin-right: -1rem !important;
+ }
+
+ .mb-xl-n3,
+ .my-xl-n3 {
+ margin-bottom: -1rem !important;
+ }
+
+ .ml-xl-n3,
+ .mx-xl-n3 {
+ margin-left: -1rem !important;
+ }
+
+ .m-xl-n4 {
+ margin: -1.5rem !important;
+ }
+
+ .mt-xl-n4,
+ .my-xl-n4 {
+ margin-top: -1.5rem !important;
+ }
+
+ .mr-xl-n4,
+ .mx-xl-n4 {
+ margin-right: -1.5rem !important;
+ }
+
+ .mb-xl-n4,
+ .my-xl-n4 {
+ margin-bottom: -1.5rem !important;
+ }
+
+ .ml-xl-n4,
+ .mx-xl-n4 {
+ margin-left: -1.5rem !important;
+ }
+
+ .m-xl-n5 {
+ margin: -3rem !important;
+ }
+
+ .mt-xl-n5,
+ .my-xl-n5 {
+ margin-top: -3rem !important;
+ }
+
+ .mr-xl-n5,
+ .mx-xl-n5 {
+ margin-right: -3rem !important;
+ }
+
+ .mb-xl-n5,
+ .my-xl-n5 {
+ margin-bottom: -3rem !important;
+ }
+
+ .ml-xl-n5,
+ .mx-xl-n5 {
+ margin-left: -3rem !important;
+ }
+
+ .m-xl-auto {
+ margin: auto !important;
+ }
+
+ .mt-xl-auto,
+ .my-xl-auto {
+ margin-top: auto !important;
+ }
+
+ .mr-xl-auto,
+ .mx-xl-auto {
+ margin-right: auto !important;
+ }
+
+ .mb-xl-auto,
+ .my-xl-auto {
+ margin-bottom: auto !important;
+ }
+
+ .ml-xl-auto,
+ .mx-xl-auto {
+ margin-left: auto !important;
+ }
+}
+
+.stretched-link::after {
+ position: absolute;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ z-index: 1;
+ pointer-events: auto;
+ content: '';
+ background-color: rgba(0, 0, 0, 0);
+}
+
+.text-monospace {
+ font-family: SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace !important;
+}
+
+.text-justify {
+ text-align: justify !important;
+}
+
+.text-wrap {
+ white-space: normal !important;
+}
+
+.text-nowrap {
+ white-space: nowrap !important;
+}
+
+.text-truncate {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.text-left {
+ text-align: left !important;
+}
+
+.text-right {
+ text-align: right !important;
+}
+
+.text-center {
+ text-align: center !important;
+}
+
+@media (min-width: 576px) {
+ .text-sm-left {
+ text-align: left !important;
+ }
+
+ .text-sm-right {
+ text-align: right !important;
+ }
+
+ .text-sm-center {
+ text-align: center !important;
+ }
+}
+
+@media (min-width: 768px) {
+ .text-md-left {
+ text-align: left !important;
+ }
+
+ .text-md-right {
+ text-align: right !important;
+ }
+
+ .text-md-center {
+ text-align: center !important;
+ }
+}
+
+@media (min-width: 992px) {
+ .text-lg-left {
+ text-align: left !important;
+ }
+
+ .text-lg-right {
+ text-align: right !important;
+ }
+
+ .text-lg-center {
+ text-align: center !important;
+ }
+}
+
+@media (min-width: 1200px) {
+ .text-xl-left {
+ text-align: left !important;
+ }
+
+ .text-xl-right {
+ text-align: right !important;
+ }
+
+ .text-xl-center {
+ text-align: center !important;
+ }
+}
+
+.text-lowercase {
+ text-transform: lowercase !important;
+}
+
+.text-uppercase {
+ text-transform: uppercase !important;
+}
+
+.text-capitalize {
+ text-transform: capitalize !important;
+}
+
+.font-weight-light {
+ font-weight: 300 !important;
+}
+
+.font-weight-lighter {
+ font-weight: lighter !important;
+}
+
+.font-weight-normal {
+ font-weight: 400 !important;
+}
+
+.font-weight-bold {
+ font-weight: 700 !important;
+}
+
+.font-weight-bolder {
+ font-weight: bolder !important;
+}
+
+.font-italic {
+ font-style: italic !important;
+}
+
+.text-white {
+ color: #fff !important;
+}
+
+.text-primary {
+ color: #07819b !important;
+}
+
+a.text-primary:hover,
+a.text-primary:focus {
+ color: #044452 !important;
+}
+
+.text-secondary {
+ color: #313032 !important;
+}
+
+a.text-secondary:hover,
+a.text-secondary:focus {
+ color: #0b0b0b !important;
+}
+
+.text-success {
+ color: #28a745 !important;
+}
+
+a.text-success:hover,
+a.text-success:focus {
+ color: #19692c !important;
+}
+
+.text-info {
+ color: #17a2b8 !important;
+}
+
+a.text-info:hover,
+a.text-info:focus {
+ color: #0f6674 !important;
+}
+
+.text-warning {
+ color: #ffc107 !important;
+}
+
+a.text-warning:hover,
+a.text-warning:focus {
+ color: #ba8b00 !important;
+}
+
+.text-danger {
+ color: #dc3545 !important;
+}
+
+a.text-danger:hover,
+a.text-danger:focus {
+ color: #a71d2a !important;
+}
+
+.text-light {
+ color: #f8f9fa !important;
+}
+
+a.text-light:hover,
+a.text-light:focus {
+ color: #cbd3da !important;
+}
+
+.text-dark {
+ color: #343a40 !important;
+}
+
+a.text-dark:hover,
+a.text-dark:focus {
+ color: #121416 !important;
+}
+
+.text-body {
+ color: #212529 !important;
+}
+
+.text-muted {
+ color: #313032 !important;
+}
+
+.text-black-50 {
+ color: rgba(0, 0, 0, 0.5) !important;
+}
+
+.text-white-50 {
+ color: rgba(255, 255, 255, 0.5) !important;
+}
+
+.text-hide {
+ font: 0/0 a;
+ color: transparent;
+ text-shadow: none;
+ background-color: transparent;
+ border: 0;
+}
+
+.text-decoration-none {
+ text-decoration: none !important;
+}
+
+.text-break {
+ word-wrap: break-word !important;
+}
+
+.text-reset {
+ color: inherit !important;
+}
+
+.visible {
+ visibility: visible !important;
+}
+
+.invisible {
+ visibility: hidden !important;
+}
+
+@media print {
+ *,
+ *::before,
+ *::after {
+ text-shadow: none !important;
+ box-shadow: none !important;
+ }
+
+ a:not(.btn) {
+ text-decoration: underline;
+ }
+
+ abbr[title]::after {
+ content: ' (' attr(title) ')';
+ }
+
+ pre {
+ white-space: pre-wrap !important;
+ }
+
+ pre,
+ blockquote {
+ border: 1px solid #adb5bd;
+ page-break-inside: avoid;
+ }
+
+ thead {
+ display: table-header-group;
+ }
+
+ tr,
+ img {
+ page-break-inside: avoid;
+ }
+
+ p,
+ h2,
+ h3 {
+ orphans: 3;
+ widows: 3;
+ }
+
+ h2,
+ h3 {
+ page-break-after: avoid;
+ }
+
+ @page {
+ size: a3;
+ }
+
+ body {
+ min-width: 992px !important;
+ }
+
+ .container {
+ min-width: 992px !important;
+ }
+
+ .navbar {
+ display: none;
+ }
+
+ .badge {
+ border: 1px solid #000;
+ }
+
+ .table {
+ border-collapse: collapse !important;
+ }
+
+ .table td,
+ .table th {
+ background-color: #fff !important;
+ }
+
+ .table-bordered th,
+ .table-bordered td {
+ border: 1px solid #dee2e6 !important;
+ }
+
+ .table-dark {
+ color: inherit;
+ }
+
+ .table-dark th,
+ .table-dark td,
+ .table-dark thead th,
+ .table-dark tbody + tbody {
+ border-color: #dee2e6;
+ }
+
+ .table .thead-dark th {
+ color: inherit;
+ border-color: #dee2e6;
+ }
+}
+
+/**
+ * Styles for specific components of the portal
+ */
+.fixed-bottom {
+ background-color: #313032;
+}
diff --git a/src/assets/fonts/bootstrap-icons.woff b/src/assets/fonts/bootstrap-icons.woff
new file mode 100644
index 0000000..8cd8946
--- /dev/null
+++ b/src/assets/fonts/bootstrap-icons.woff
Binary files differ
diff --git a/src/assets/fonts/bootstrap-icons.woff2 b/src/assets/fonts/bootstrap-icons.woff2
new file mode 100644
index 0000000..3e587fd
--- /dev/null
+++ b/src/assets/fonts/bootstrap-icons.woff2
Binary files differ
diff --git a/src/assets/i18n/de.json b/src/assets/i18n/de.json
new file mode 100644
index 0000000..1b06ded
--- /dev/null
+++ b/src/assets/i18n/de.json
@@ -0,0 +1,355 @@
+{
+ "common": {
+ "loading": "Lädt...",
+ "required": "Erforderlich",
+ "buttons": {
+ "cancel": "Abbrechen",
+ "clear": "Clear",
+ "close": "Schließen",
+ "collapse": "Einklappen",
+ "delete": "Löschen",
+ "notPossibleDelete": "Es ist nicht möglich, den eigenen Account zu löschen.",
+ "edit": "Bearbeiten",
+ "expand": "Ausklappen",
+ "filter": "Filtern",
+ "info": "Info",
+ "next": "Weiter",
+ "previous": "Zurück",
+ "refresh": "Refresh",
+ "rightClick": "Öffne das Kontextmenü",
+ "save": "Speichern",
+ "search": "Suchen",
+ "set": "Speichern",
+ "reset": "Reset",
+ "submit": "Senden",
+ "openSupportLink": "Support link ausklappen",
+ "closeSupportLink": "Support link einklappen"
+ },
+ "dateFilter": {
+ "startDate": "Startdatum auswählen",
+ "endDate": "Enddatum auswählen"
+ },
+ "filters": {
+ "filterBy": "Filtern nach {{value}}",
+ "noResults": "Keine Daten verfügbar.",
+ "noResultsForCustomer": "Keine Daten für den Kunden verfügbar: {{customerId}}",
+ "noOptions": "Keine Aktion möglich.",
+ "searchHint": "Suchbegriffe eingeben",
+ "showCategories": "Menü zum filtern nach Kategorien anzeigen",
+ "noFilterResults": "Keine Ãœbereinstimmung mit den angegebenen Filterkriterien."
+ },
+ "no": "Nein",
+ "pagination": {
+ "itemsPerPage": "{{value}} Einträge pro Seite",
+ "totalCount": "Gesamtanzahl: {{value}}",
+ "selectItemsPerPage": "Wähle Anzahl der Elemente pro Seite"
+ },
+ "sorting": {
+ "ascending:": "Aufsteigend sortiert",
+ "descending": "Absteigend sortiert",
+ "none": "Keine Sortierung"
+ },
+ "select": {
+ "description": "Selektier die Anzahl der Items pro Seite",
+ "itemsPerPage": "{{value}} Einträge pro Seite",
+ "openOptions": "Optionen öffnen",
+ "closeOptions": "Optionen schließen",
+ "selectOption": "Option auswählen"
+ },
+ "hint": "Hinweis",
+ "warning": "Warnung",
+ "yes": "Ja",
+ "messages": {
+ "badRequest": "Bad request: Der Server kann ihre Anfrage nicht verarbeiten.",
+ "unauthorized": "Sie sind nicht authorisiert. Bitte loggen sie sich ein.",
+ "forbidden": "Sie haben keine oder nur unzureichende Berechtigungen.",
+ "notFound": "Die angeforderten Ressourcen konnten nicht gefunden werden.",
+ "conflict": "Ihre Anfrage steht im Konflikt zu einer anderen Anfrage.",
+ "internalServerError": "Internal Server Error: Bitte kontaktieren Sie den Support.",
+ "badGateway": "Überprüfen Sie ihre Proxy-Einstellungen.",
+ "serviceUnavailable": "Der Service ist zur Zeit nicht verfügbar. Bitte kontaktieren Sie die System-Administratoren.",
+ "checkRequest": "Überprüfen Sie den Request.",
+ "actionUnsuccessful": "Die Aktion konnte nicht durchgeführt werden, bitte probieren Sie es erneut.",
+ "contactSupport": "Bitte, kontaktieren Sie das Support Team.",
+ "timeout": "Ressource ist nicht erreichbar.",
+ "keycloakAccessTokenNotValid": "Die Gültigkeit Ihres Zugangstokens ist aufgrund von Verbindungsproblemen abgelaufen. Bitte laden sie erneut die Seite. Falls das Problem weiterhin besteht, wenden Sie sich an den Support."
+ },
+ "systems": {
+ "AAI": "AAI",
+ "CDS": "CDS",
+ "KUBERNETES": "Kubernetes",
+ "KEYCLOAK": "Keycloak",
+ "PORTAL_SERVICE": "Portal Service",
+ "SDC": "SDC",
+ "SO": "SO",
+ "SO_CATALOG": "SO Catalog",
+ "PORTAL_HISTORY": "Portal History",
+ "PORTAL_PREFS": "Portal Preferences"
+ },
+ "block": {
+ "userAdministration": {
+ "create": "Beim Anlegen des Benutzerkontos ist ein Fehler aufgetreten!",
+ "edit": "Beim Bearbeiten des Benutzerkontos ist ein Fehler aufgetreten!",
+ "list": "Die Benutzerlist kann wegen eines Fehlers nicht angezeigt werden!",
+ "delete": "Der Benutzer kann wegen eines Fehlers nicht gelöscht werden!",
+ "helpUserNameExists": "Versuchen Sie bitte den Benutzer mit einer anderen Benutzername anzulegen.",
+ "helpUserEmailExists": "Versuchen Sie bitte den Benutzer mit einer anderen E-mail Adresse anzulegen."
+ },
+ "defaultMessage": "Systemfehler aufgetreten!",
+ "logging": "Warnung, Logging ist fehlgeschlagen! Keine weitere Aktion notwendig! ",
+ "dashboard": "Systemfehler, Anzeigen des Dashboard nicht möglich!",
+ "appStarter": "Systemfehler, Anzeigen der Applikationen nicht möglich!",
+ "authorization": "Fehler bei der Autorisierung!",
+ "loadPreferences": "Beim Laden der Benutzereinstellungen ist ein Fehler aufgetreten!",
+ "savePreferences": "Beim Speichern der Benutzereinstellungen ist ein Fehler aufgetreten!",
+ "saveAction": "Beim Speichern der letzten Benutzeraktion ist ein Fehler aufgetreten!",
+ "loadAction": "Beim Laden der letzten Benutzeraktion ist ein Fehler aufgetreten!",
+ "timeout": "Timeout ist nach {{value}} Sekunden aufgetreten!"
+ },
+ "alert": {
+ "contactSupport": {
+ "part1": "Für Hilfe kontaktieren Sie bitte unseren ",
+ "part2": " und geben Sie alle Informationen einschließlich dieser ID an:"
+ },
+ "errorReporter": "Fehlermeldung von \"{{system}}\" System:",
+ "support": "Support"
+ },
+ "noPermissions": {
+ "noPermissions": "Sie haben nicht genügend Berechtigungen um diese Seite zu sehen!",
+ "support": "Wenn Sie glauben, dies ist ein Fehler, kontaktieren Sie bitte den Support."
+ },
+ "form": {
+ "feedback": {
+ "emailWrongFormat": "Falsches Email Format",
+ "invalidCharacters": "Ungültige Zeichen",
+ "invalidConfirmationPassword": "Passworte stimmen nicht überein",
+ "required": "Darf nicht leer sein",
+ "whiteSpaceNotAllowed": "Leerzeichen sind nicht erlaubt",
+ "specialCharactersNotAllowed": "Sonderzeichen sind nicht erlaubt."
+ }
+ }
+ },
+ "appStarter": {
+ "groups": {
+ "admin": "Admin",
+ "developer": "Developer",
+ "operator": "Operator"
+ },
+ "title": "App Starter",
+ "tiles": {
+ "tooltips": {
+ "enum": {
+ "1": "SDC ist das visuelle Modellierungs- und Designtool von ONAP. Es erstellt interne Metadaten, die Assets beschreiben, die von allen ONAP-Komponenten verwendet werden, sowohl zur Entwurfszeit als auch zur Laufzeit.",
+ "2": "VID bietet eine gut strukturierte Organisation von Infrastrukturbereitstellung, Instanziierung und Change Management, die von Operations verwendet werden, um Orchestrierungen und Change Management abzuleiten.",
+ "3": "Das ONAP Policy Framework legt die Architektur des Frameworks dar und zeigt die APIs, die anderen Komponenten bereitgestellt werden, die mit dem Framework zusammenarbeiten.",
+ "4": "Der ONAP Service Orchestrator bietet die höchste Ebene der Service-Orchestrierung in der ONAP-Architektur.",
+ "5": "CLAMP ist eine Plattform zum Entwerfen und Verwalten sogenannter Control Loops. Es wird verwendet, um geschlossene Schleifen (Closed Loops) zu entwerfen, diese mit spezifischen Parametern für einen bestimmten Netzwerkdienst zu konfigurieren und sie dann in Betrieb und außer Betrieb zu nehmen.",
+ "6": "Kibana Dashboard zur Visualisierung der von Control Loops ausgelösten Ereignisse (wird auch in ONAP für die Alarmvisualisierung verwendet).",
+ "7": "Die CDS Designer Benutzeroberfläche ist ein Framework zur Automatisierung der Auflösung von Ressourcen für die Instanziierung und aller Konfigurationsbereitstellung, wie z. B. die Konfiguration von Day0, Day1 oder Day2.",
+ "8": "CDT ermöglicht das Self-Service-Onboarding von VNFs durch das Erstellen von Templates und anderen Artefakten. Dies kann zum Beispiel für den Configure-Befehl im APPC verwendet werden um Konfigurationen nach der Instanziierung anzupassen.",
+ "9": "Das Holmes-Projekt bietet Alarmkorrelation und -analyse für Telekommunikations-Cloudinfrastruktur und -Dienste, einschließlich Hosts, Vims, VNFs und NSs.",
+ "10": "AAI ist eine Komponente der ONAP-Laufzeit (Echtzeitansichten von Ressourcen, Services, Produkten, Kundenabonnements und deren Beziehungen).",
+ "11": "DCAE ist der Obername für eine Reihe von Komponenten, die gemeinsam die Rolle der Datenerfassung, Analyse und Ereignisgenerierung für ONAP erfüllen. Die Architektur von DCAE zielt auf eine flexible, steckbare, Micros-Service-orientierte, modellbasierte Komponentenbereitstellung und Servicezusammensetzung ab. DCAE unterstützt auch Sammel- und Analysevorgänge an mehreren Standorten, die für große ONAP-Bereitstellungen unerlässlich sind.",
+ "12": "SDNC DG soll eine Ausführungsumgebung für schnell geschriebene und hochgradig angepasste Serviceabläufe bereitstellen.",
+ "13": "Das OpenDaylight Project ist ein kollaboratives Open-Source-Projekt, das von der Linux Foundation gehostet wird. Das Projekt dient als Plattform für Software-Defined Networking (SDN) zur offenen, zentralisierten Überwachung von Netzwerkgeräten."
+ }
+ }
+ }
+ },
+ "dashboard": {
+ "apps": {
+ "userLastAction": {
+ "filter": {
+ "label": "Letzte Aktivitäten filtern nach: ",
+ "type": {
+ "ALL": "Alle meine Aktivitäten",
+ "SEARCH": "Meine Suchen",
+ "ACTION": "Meine Handlungen"
+ },
+ "interval": {
+ "1H": "Letzte 1 Stunde",
+ "4H": "Letzten 4 Stunden",
+ "1D": "Letzten 1 Tage",
+ "ALL": "Alle"
+ }
+ },
+ "actionType": {
+ "SEARCH": "Suche",
+ "VIEW": "Ansehen",
+ "EDIT": "Bearbeiten",
+ "DEPLOY": "Deploy",
+ "DELETE": "Löschen",
+ "CREATE": "Erstellen",
+ "CLEAR": "Clear",
+ "ACK": "Acknowledge",
+ "UNACK": "Unacknowledge"
+ },
+ "entityType": {
+ "ALARM": "Alarm",
+ "USERADMINISTRATION": "Benutzeradministration",
+ "SERVICEMODEL": "Servicemodell",
+ "SERVICEINSTANCE": "Serviceinstanz",
+ "TOPOLOGY": "Topologie"
+ },
+ "tooltip": {
+ "again": "wieder",
+ "statusOf": "Status von",
+ "noAction": "Vorerst keine Aktion.",
+ "repeatAction": "Letzte Aktion wiederholen",
+ "viewAction": "Letzte Aktion anzeigen",
+ "deployment": "deployment"
+ },
+ "modal": {
+ "deploymentStatus": "Deployment Status",
+ "state": "State",
+ "progress": "Progress",
+ "flowStatus": "Flow Status",
+ "rollbackStatus": "Rollback Status",
+ "retryStatus": "Retry Status",
+ "taskInformation": "Task Information"
+ }
+ },
+ "KPI_GRAPH_TILE": "KPI Grafen",
+ "ALARM_COUNT_TILE": "Anzahl Alarme",
+ "USER_LAST_ACTION_TILE": "Letzte Benutzeraktionen"
+ },
+ "showSettings": "Konfigurieren Sie, welche Anwendungen im Dashboard sichtbar sein sollen",
+ "selectApplications": "Bitte wählen Sie Anwendungen aus, die Sie im Dashboard sehen möchten",
+ "tooltips": {
+ "settings": "Dashboard-Einstellungen anzeigen"
+ }
+ },
+ "layout": {
+ "header": {
+ "header": "Kopfleiste/Header",
+ "skipToMainContentLink": "Zum Hauptinhalt springen",
+ "button": {
+ "account": "Kontoeinstellungen",
+ "closeFullscreen": "Vollbildmodus schließen",
+ "logout": "Abmelden",
+ "openFullscreen": "Vollbildmodus öffnen",
+ "useraccount": "Mein Account"
+ },
+ "info": {
+ "mail": "Email:",
+ "name": "Name:"
+ },
+ "logo": {
+ "onap": "ONAP Logo"
+ },
+ "sidebarToggler": "Öffne/schliesse Seitenmenü",
+ "shortcuts": {
+ "details": "Informationen zu den Tastaturkürzeln",
+ "home": "Home (Kopfbereich)",
+ "main": "Setzt den Fokus auf den Inhaltsanteil der Seite",
+ "search": "Setz den Fokus auf das Suchfeld, wenn auf Seite vorhanden",
+ "menu": "Setzt Fokus auf das Hauptmenü links",
+ "heading": "Tastaturkurzbefehle",
+ "helpText": "Tastaturkurzbefehle werden vom Portal unterstützt. Die Mechanismen variieren zwischen den Browsern:",
+ "helpBrowser1": "<b>ALT</b> gedrückt halten und die <b>Taste</b> drücken für <b>Google Chrome, Microsoft, Safari</b>",
+ "helpBrowser2": "<b>ALT</b> und <b>Umschalten</b> gedrückt halten und die <b>Taste</b> drücken für <b>Firefox</b>",
+ "helpBroswer3": "<b>MACINTOSH Safari: CTRL</b> Taste gedrückt halten und die <b>Taste</b> drücken"
+ }
+ },
+ "main": {
+ "alerts": "Alarmmeldungen",
+ "mainContent": "Hauptinhalt",
+ "breadcrumb": "Breadcrumb/Brotkrumen"
+ },
+ "sidebar": "Seitenleiste",
+ "menu": {
+ "mainMenu": "Hauptmenü",
+ "menuItems": "Menü Items",
+ "items": {
+ "cellSiteMap": "Mobilfunk-Karte",
+ "home": "Home",
+ "appStarter": "App Starter",
+ "dashboard": "Dashboard",
+ "options": "Optionen",
+ "userAdministration": "Benutzerverwaltung",
+ "users": "Benutzer",
+ "userSettings": "Benutzereinstellungen"
+ }
+ }
+ },
+ "modal": {
+ "error": {
+ "accessDenied": "Keine Berechtigung!",
+ "details": "Details:"
+ }
+ },
+ "userAdministration": {
+ "buttons": {
+ "createUser": "Benutzer anlegen",
+ "editUser": "Benutzer bearbeiten"
+ },
+ "fields": {
+ "access": "Zugang",
+ "actions": "Aktionen",
+ "assignedRoles": "Zugewiesene Rollen",
+ "attributes": "Eigenschaften",
+ "confirmPassword": "Passwort bestätigen",
+ "createdAt": "Angelegt am",
+ "disableableCredentialTypes": "Berechtigungsnachweise deaktivieren",
+ "email": "Email",
+ "emailVerified": "Email geprüft",
+ "enabled": "Aktiviert",
+ "firstName": "Vorname",
+ "id": "ID",
+ "key": "Schlüssel",
+ "lastName": "Nachname",
+ "password": "Passwort",
+ "requiredActions": "Erforderliche Aktionen",
+ "temporaryPassword": "Temporäres Passwort",
+ "userName": "Benutzer",
+ "value": "Wert"
+ },
+ "form": {
+ "headings": {
+ "setNewPassword": "Neues Passwort setzen",
+ "setRoles": {
+ "assigned": "Zugewiesen",
+ "available": "Verfügbar",
+ "title": "Rollen zuweisen"
+ },
+ "setUserData": "Benutzerdaten einstellen"
+ },
+ "title": {
+ "changePassword": "Passwort ändern",
+ "changePasswordTooltip": "Um Ihr Passwort zu ändern, loggen Sie sich bitte aus und benutzen Sie den Link \"Passwort vergessen?\".",
+ "create": "Benutzer anlegen",
+ "edit": "Benutzer bearbeiten",
+ "roles": "Rollen"
+ }
+ },
+ "list": {
+ "modal": {
+ "delete": {
+ "title": "Lösche Benutzer",
+ "text": "Möchtest Du den Benutzer: {{userName}} wirklich löschen?"
+ }
+ },
+ "tableCaption": "Benutzertabelle",
+ "title": "Benutzerverwaltung"
+ },
+ "messages": {
+ "success": {
+ "created": "Benutzer erfolgreich angelegt.",
+ "deleted": "Benutzer erfolgreich gelöscht.",
+ "setPassword": "Password erfolgreich geändert.",
+ "updated": "Benutzer erfolgreich aktualisiert."
+ }
+ },
+ "warnings": {
+ "userDeleted": {
+ "header": "Benutzer existiert nicht mehr",
+ "message": "Eine Bearbeitung des Benutzers ist nicht möglich. Benutzer wurde bereits gelöscht."
+ }
+ }
+ },
+ "pageNotFound": {
+ "text": "Die gesuchte Seite konnte nicht gefunden werden.",
+ "button": "Zurück zur Hauptseite",
+ "imgAltText": "Bild einer fragenden Frau"
+ }
+}
diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json
new file mode 100644
index 0000000..350137d
--- /dev/null
+++ b/src/assets/i18n/en.json
@@ -0,0 +1,351 @@
+{
+ "common": {
+ "loading": "Loading...",
+ "required": "Required",
+ "buttons": {
+ "cancel": "Cancel",
+ "clear": "Clear",
+ "close": "Close",
+ "collapse": "Collapse",
+ "delete": "Delete",
+ "notPossibleDelete": "It is not possible to delete your own account.",
+ "edit": "Edit",
+ "expand": "Expand",
+ "filter": "Filter",
+ "info": "Info",
+ "next": "Next",
+ "previous": "Previous",
+ "refresh": "Refresh",
+ "rightClick": "Open the right-click context menu",
+ "save": "Save",
+ "search": "Search",
+ "set": "Set",
+ "reset": "Reset",
+ "submit": "Submit",
+ "openSupportLink": "Expand support link",
+ "closeSupportLink": "Collapse support link"
+ },
+ "dateFilter": {
+ "startDate": "Select start date",
+ "endDate": "Select end date"
+ },
+ "filters": {
+ "filterBy": "Filter by {{value}}",
+ "noResults": "No data available.",
+ "noResultsForCustomer": "No data available for customer: {{customerId}}",
+ "noOptions": "No action possible.",
+ "searchHint": "Enter Search Terms",
+ "showCategories": "Show menu to filter by categories",
+ "noFilterResults": "No match to the given filter criteria."
+ },
+ "no": "No",
+ "pagination": {
+ "itemsPerPage": "{{value}} items per page",
+ "totalCount": "Total Count: {{value}}",
+ "selectItemsPerPage": "Select items per page"
+ },
+ "yes": "Yes",
+ "sorting": {
+ "ascending": "Sorted Ascending",
+ "descending": "Sorted Descending",
+ "none": "No Sorting"
+ },
+ "select": {
+ "description": "Select number of items per page",
+ "itemsPerPage": "{{value}} items per page",
+ "openOptions": "Open Options",
+ "closeOptions": "Close Options",
+ "selectOption": "Select Option"
+ },
+ "hint": "Hint",
+ "warning": "Warning",
+ "messages": {
+ "badRequest": "Bad request: The server cannot process your request.",
+ "unauthorized": "You are not authorized. Please log in first.",
+ "forbidden": "You have no or limited permissions.",
+ "notFound": "Requested information not found.",
+ "conflict": "Your request has a conflict with resource.",
+ "internalServerError": "Internal Server Error: Please contact the support team.",
+ "badGateway": "Check your proxy settings.",
+ "serviceUnavailable": "Please, contact server administrators team.",
+ "checkRequest": "Check your requests.",
+ "actionUnsuccessful": "Action unsuccessful, try again later.",
+ "contactSupport": "Please, contact our support team.",
+ "timeout": "Resource is not reachable.",
+ "keycloakAccessTokenNotValid": "Your access token validity expired due to connection issues. Please try to reload a page and in case issue persists, contact support"
+ },
+ "systems": {
+ "AAI": "AAI",
+ "CDS": "CDS",
+ "KUBERNETES": "Kubernetes",
+ "KEYCLOAK": "Keycloak",
+ "PORTAL_SERVICE": "Portal Service",
+ "SDC": "SDC",
+ "SO": "SO",
+ "SO_CATALOG": "SO Catalog",
+ "PORTAL_HISTORY": "Portal History",
+ "PORTAL_PREFS": "Portal Preferences"
+ },
+ "block": {
+ "userAdministration": {
+ "create": "Error while creating user account!",
+ "edit": "Error, changing user account failed!",
+ "list": "System error, not able to display user accounts!",
+ "delete": "Error while trying to delete user account!",
+ "helpUserNameExists": "Please, try to create user with different username.",
+ "helpUserEmailExists": "Please, try to create user with different email address."
+ },
+ "defaultMessage": "A System error occurred!",
+ "logging": "Warning, internal logging failed! No further action required! ",
+ "dashboard": "System error while trying to load data for displaying tiles on the dashboard!",
+ "appStarter": "System error while trying to display app starter!",
+ "authorization": "Error during authorization!",
+ "loadPreferences": "System error while trying to load user preferences!",
+ "savePreferences": "System error while trying to save user preferences!",
+ "saveAction": "System error while trying to save user action!",
+ "loadAction": "System error while trying to load user actions!",
+ "timeout": "Timeout has occurred after {{value}} seconds!"
+ },
+ "alert": {
+ "contactSupport": {
+ "part1": "For help please contact our ",
+ "part2": " and provide all information including this ID:"
+ },
+ "errorReporter": "Error reported by \"{{system}}\" system:",
+ "support": "Support"
+ },
+ "noPermissions": {
+ "noPermissions": "You don't have enough permissions for viewing this page!",
+ "support": "If you think this is an error, please contact the support."
+ },
+ "form": {
+ "feedback": {
+ "emailWrongFormat": "Wrong email format",
+ "invalidCharacters": "Invalid character",
+ "invalidConfirmationPassword": "Password and Confirm Password are not matching",
+ "required": "Cannot be empty",
+ "whiteSpaceNotAllowed": "White and empty spaces are not allowed",
+ "specialCharactersNotAllowed": "Special characters are not allowed."
+ }
+ }
+ },
+ "appStarter": {
+ "groups": {
+ "admin": "Admin",
+ "developer": "Developer",
+ "operator": "Operator"
+ },
+ "title": "App Starter",
+ "tiles": {
+ "tooltips": {
+ "enum": {
+ "1": "SDC is the ONAP visual modeling and design tool. It creates internal metadata that describes assets used by all ONAP components, both at design time and run time.",
+ "2": "VID provides a well-structured organization of infrastructure deployment, instantiation and change-management operations used by Operations to derive orchestrations and change-management.",
+ "3": "ONAP Policy Framework lays out the architecture of the framework and shows the APIs provided to other components that interwork with the framework.",
+ "4": "The ONAP Service Orchestrator provides the highest level of service orchestration in the ONAP architecture.",
+ "5": "CLAMP is a platform for designing and managing control loops. It is used to design a closed loop, configure it with specific parameters for a particular network service, then deploying and undeploying it.",
+ "6": "Kibana Dashboard to visualize the events, triggered by control loops (used also in ONAP for Alarm Visualization).",
+ "7": "CDS Designer UI is a framework to automate the resolution of resources for instantiation and any config provisioning operation, such as day0, day1, or day2 configuration.",
+ "8": "CDT enables self-service onboarding of VNF's by creating templates and other artifacts. This may for instance be used in APPC's Configure command for applying a post-instantiation configuration.",
+ "9": "Holmes project provides alarm correlation and analysis for Telecom cloud infrastructure and services, including hosts, vims, VNFs and NSs.",
+ "10": "AAI is a component of ONAP runtime (Real-time views of Resources, Services, Products, Customer Subscriptions and their relationships).",
+ "11": "DCAE is the umbrella name for a number of components collectively fulfilling the role of Data Collection, Analytics, and Events generation for ONAP. The architecture of DCAE targets flexible, plug-able, micros-service oriented, model based component deployment and service composition. DCAE also support multi-site collection and analytics operations which are essential for large ONAP deployments.",
+ "12": "SDNC DG is to provide an execution environment for quickly written and highly customized service flows.",
+ "13": "The OpenDaylight Project is a collaborative open-source project hosted by The Linux Foundation. The project serves as a platform for software-defined networking (SDN) for open, centralized, network device monitoring."
+ }
+ }
+ }
+ },
+ "dashboard": {
+ "apps": {
+ "userLastAction": {
+ "filter": {
+ "label": "Filter the last actions by: ",
+ "type": {
+ "ALL": "All my activities",
+ "SEARCH": "My searches",
+ "ACTION": "My activities"
+ },
+ "interval": {
+ "1H": "Last 1 hour",
+ "4H": "Last 4 hours",
+ "1D": "Last 1 day",
+ "ALL": "All"
+ }
+ },
+ "actionType": {
+ "SEARCH": "Search",
+ "VIEW": "View",
+ "EDIT": "Edit",
+ "DEPLOY": "Deploy",
+ "DELETE": "Delete",
+ "CREATE": "Create",
+ "CLEAR": "Clear",
+ "ACK": "Acknowledge",
+ "UNACK": "Unacknowledge"
+ },
+ "entityType": {
+ "ALARM": "alarm",
+ "USERADMINISTRATION": "user",
+ "SERVICEMODEL": "service model",
+ "SERVICEINSTANCE": "service instance",
+ "TOPOLOGY": "topology"
+ },
+ "tooltip": {
+ "again": "again",
+ "statusOf": "status of",
+ "noAction": "No action for now.",
+ "repeatAction": "Repeat last action",
+ "viewAction": "View last action",
+ "deployment": "deployment"
+ },
+ "modal": {
+ "deploymentStatus": "Deployment Status",
+ "state": "State",
+ "progress": "Progress",
+ "flowStatus": "Flow Status",
+ "rollbackStatus": "Rollback Status",
+ "retryStatus": "Retry Status",
+ "taskInformation": "Task Information"
+ }
+ },
+ "USER_LAST_ACTION_TILE": "Last user actions"
+ },
+ "showSettings": "Configure which applications should be visible on dashboard",
+ "selectApplications": "Please select applications, that you would like to see in dashboard",
+ "tooltips": {
+ "settings": "Show dashboard settings"
+ }
+ },
+ "layout": {
+ "header": {
+ "header": "Header",
+ "skipToMainContentLink": "Skip to Main Content",
+ "button": {
+ "account": "Account settings",
+ "closeFullscreen": "Close Fullscreen",
+ "logout": "Logout",
+ "openFullscreen": "Open Fullscreen",
+ "useraccount": "My Account"
+ },
+ "info": {
+ "mail": "Email:",
+ "name": "Name:"
+ },
+ "logo": {
+ "onap": "ONAP Logo"
+ },
+ "sidebarToggler": "Toggle sidemenu",
+ "shortcuts": {
+ "details": "Details about keyboard shortcuts",
+ "home": "Home (top bar)",
+ "main": "Set focus to main content",
+ "search": "Set focus on search field if available on page",
+ "menu": "Set focus to main menu at the left",
+ "heading": "Keyboard Shortcuts",
+ "helpText": "Shortcut keys are supported by the portal, the mechanism differs across browsers:",
+ "helpBrowser1": "Hold down <b>ALT</b> and press key for <b>Google Chrome, Microsoft, Safari</b>",
+ "helpBrowser2": "Hold down <b>ALT + shift</b> and press the <b>key</b> for <b>Firefox</b>",
+ "helpBrowser3": "<b>MACINTOSH Safari:</b> hold down <b>CTRL</b> and press the <b>key</b>."
+ }
+ },
+ "main": {
+ "alerts": "Alerts",
+ "mainContent": "Main Content",
+ "breadcrumb": "Breadcrumb"
+ },
+ "sidebar": "Sidebar",
+ "menu": {
+ "mainMenu": "Main Menu",
+ "menuItems": "Menu Items",
+ "items": {
+ "home": "Home",
+ "appStarter": "App Starter",
+ "dashboard": "Dashboard",
+ "options": "Options",
+ "userAdministration": "User Administration",
+ "users": "Users",
+ "userSettings": "User Settings"
+ }
+ }
+ },
+ "modal": {
+ "error": {
+ "accessDenied": "Access denied!",
+ "details": "Details:"
+ }
+ },
+ "userAdministration": {
+ "buttons": {
+ "createUser": "Create User",
+ "editUser": "Edit User"
+ },
+ "fields": {
+ "access": "Access",
+ "actions": "Actions",
+ "assignedRoles": "Assigned Roles",
+ "attributes": "Attributes",
+ "confirmPassword": "Confirm Password",
+ "createdAt": "Created At",
+ "email": "Email",
+ "emailVerified": "Email Verified",
+ "enabled": "Enabled",
+ "firstName": "First Name",
+ "id": "ID",
+ "key": "Key",
+ "lastName": "Last Name",
+ "password": "Password",
+ "requiredActions": "Required Actions",
+ "temporaryPassword": "Temporary Password",
+ "userName": "Username",
+ "value": "Value"
+ },
+ "form": {
+ "headings": {
+ "setNewPassword": "Set New Password",
+ "setRoles": {
+ "assigned": "Assigned",
+ "available": "Available",
+ "title": "Set Roles"
+ },
+ "setUserData": "Set User Data"
+ },
+ "title": {
+ "changePassword": "Change Password",
+ "changePasswordTooltip": "To change your password, please log out and use the \"Forgot your password?\" link.",
+ "create": "Create User",
+ "edit": "Edit User",
+ "roles": "Roles"
+ }
+ },
+ "list": {
+ "modal": {
+ "delete": {
+ "title": "Delete User",
+ "text": "Are you sure, that you want to delete user: {{userName}} ?"
+ }
+ },
+ "tableCaption": "List of users",
+ "title": "User Administration"
+ },
+ "messages": {
+ "success": {
+ "created": "User successfully created.",
+ "deleted": "User successfully deleted.",
+ "setPassword": "Password was successfully changed.",
+ "updated": "User successfully updated."
+ },
+ "warnings": {
+ "userDeleted": {
+ "header": "User does not exist anymore",
+ "message": "Edit the user is not possible. User was already deleted."
+ }
+ }
+ }
+ },
+ "pageNotFound": {
+ "text": "The page that you are looking for could not be found.",
+ "button": "Back to Main page",
+ "imgAltText": "Image of wondering woman"
+ }
+}
diff --git a/src/assets/images/icons/Asterisk.svg b/src/assets/images/icons/Asterisk.svg
new file mode 100644
index 0000000..5480eea
--- /dev/null
+++ b/src/assets/images/icons/Asterisk.svg
@@ -0,0 +1,5 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="32.275" height="30.469">
+ <path
+ d="M0 14.355l2.246-6.933c5.176 1.823 8.936 3.402 11.28 4.736-.62-5.892-.945-9.944-.977-12.158h7.08c-.098 3.223-.472 7.26-1.123 12.11 3.353-1.693 7.194-3.256 11.523-4.688l2.246 6.933c-4.134 1.368-8.186 2.28-12.158 2.735 1.986 1.725 4.785 4.801 8.399 9.228l-5.86 4.15c-1.888-2.57-4.118-6.07-6.69-10.497-2.408 4.59-4.524 8.089-6.347 10.498l-5.762-4.15c3.776-4.656 6.478-7.732 8.106-9.23-4.2-.813-8.187-1.724-11.963-2.734"
+ font-size="100" font-family="arial" fill="red" />
+</svg> \ No newline at end of file
diff --git a/src/assets/images/icons/arrows-fullscreen-dark.svg b/src/assets/images/icons/arrows-fullscreen-dark.svg
new file mode 100644
index 0000000..0e7ae36
--- /dev/null
+++ b/src/assets/images/icons/arrows-fullscreen-dark.svg
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="18" height="20" fill="currentColor" class="bi bi-arrows-fullscreen" viewBox="0 0 16 16">
+ <path fill-rule="evenodd" d="M5.828 10.172a.5.5 0 0 0-.707 0l-4.096 4.096V11.5a.5.5 0 0 0-1 0v3.975a.5.5 0 0 0 .5.5H4.5a.5.5 0 0 0 0-1H1.732l4.096-4.096a.5.5 0 0 0 0-.707zm4.344 0a.5.5 0 0 1 .707 0l4.096 4.096V11.5a.5.5 0 1 1 1 0v3.975a.5.5 0 0 1-.5.5H11.5a.5.5 0 0 1 0-1h2.768l-4.096-4.096a.5.5 0 0 1 0-.707zm0-4.344a.5.5 0 0 0 .707 0l4.096-4.096V4.5a.5.5 0 1 0 1 0V.525a.5.5 0 0 0-.5-.5H11.5a.5.5 0 0 0 0 1h2.768l-4.096 4.096a.5.5 0 0 0 0 .707zm-4.344 0a.5.5 0 0 1-.707 0L1.025 1.732V4.5a.5.5 0 0 1-1 0V.525a.5.5 0 0 1 .5-.5H4.5a.5.5 0 0 1 0 1H1.732l4.096 4.096a.5.5 0 0 1 0 .707z"/>
+</svg>
diff --git a/src/assets/images/icons/brush_graphical.svg b/src/assets/images/icons/brush_graphical.svg
new file mode 100644
index 0000000..973e8c3
--- /dev/null
+++ b/src/assets/images/icons/brush_graphical.svg
@@ -0,0 +1,15 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="64.001" height="64">
+ <path fill="none" d="M0 0H64.001V64H0z"/>
+ <linearGradient id="a" gradientUnits="userSpaceOnUse" x1="196.4878" y1="-51.1158" x2="213.9881" y2="-68.6161" gradientTransform="scale(-1.05 1.05) rotate(-45 216.3452 261.4473)">
+ <stop offset="0" stop-color="#948983"/>
+ <stop offset="1" stop-color="#b9ada7"/>
+ </linearGradient>
+ <path fill="url(#a)" d="M18.537,31.208L37.329,50c-5.729,5.265-12.071,7.192-15.474,7.192c-1.095,0-2.756-0.21-3.588-1.101 l-16.01-16.01c-0.611-0.611-0.211-1.638,0.651-1.702C8.604,37.951,14.18,35.564,18.537,31.208z"/>
+ <linearGradient id="b" gradientUnits="userSpaceOnUse" x1="21.4183" y1="48.4644" x2="21.4183" y2="4.3958" gradientTransform="matrix(-1 0 0 1 64.003 0)">
+ <stop offset="0" stop-color="#1063ad"/>
+ <stop offset="1" stop-color="#529ad6"/>
+ </linearGradient>
+ <path fill="url(#b)" d="M23.132,26.612l18.794,18.794l0.828-0.829c1.089-1.111,1.246-2.795,0.562-4.271 c-1.004-2.24-2.78-5.832-0.383-8.23c0.226-0.225,16.369-14.21,16.632-14.473l0.688-0.688c2.314-2.314,2.375-6.019,0.197-8.415 c0,0-0.161-0.182-0.196-0.218l0,0c-0.035-0.035-0.218-0.197-0.218-0.197c-2.396-2.178-6.101-2.117-8.415,0.197l-0.688,0.688 C50.67,9.235,36.686,25.378,36.461,25.604c-2.397,2.397-5.99,0.621-8.23-0.382c-1.476-0.683-3.16-0.527-4.271,0.562L23.132,26.612 z M54.504,10.589c0.952-0.952,2.494-0.952,3.446,0s0.952,2.494,0,3.446s-2.494,0.952-3.446,0 C53.552,13.083,53.552,11.54,54.504,10.589z"/>
+ <path opacity=".7" fill="#948983" d="M18.537,31.208L37.329,50c-0.818,0.752-1.648,1.428-2.481,2.048L16.113,33.312 C16.952,32.666,17.768,31.976,18.537,31.208z"/>
+ <path transform="rotate(-135 30.2303 38.3063)" fill="#DADAE2" d="M16.942 35.057H43.518V41.555H16.942z"/>
+</svg>
diff --git a/src/assets/images/icons/caret-down-fill-dark.svg b/src/assets/images/icons/caret-down-fill-dark.svg
new file mode 100644
index 0000000..5ef3724
--- /dev/null
+++ b/src/assets/images/icons/caret-down-fill-dark.svg
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="21" height="22" fill="currentColor" class="bi bi-caret-down-fill" viewBox="0 0 16 16">
+ <path d="M7.247 11.14 2.451 5.658C1.885 5.013 2.345 4 3.204 4h9.592a1 1 0 0 1 .753 1.659l-4.796 5.48a1 1 0 0 1-1.506 0z"/>
+</svg>
diff --git a/src/assets/images/icons/crane_graphical.svg b/src/assets/images/icons/crane_graphical.svg
new file mode 100644
index 0000000..242517e
--- /dev/null
+++ b/src/assets/images/icons/crane_graphical.svg
@@ -0,0 +1,11 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="64.001" height="64">
+ <path fill="none" stroke="#6C6C6C" stroke-width="3" stroke-linecap="round" stroke-miterlimit="10" d="M48.41,25.586 c0,2.809,2.277,5.087,5.087,5.087c2.809,0,5.087-2.277,5.087-5.087c0-2.809-2.277-5.087-5.087-5.087V9.001"/>
+ <linearGradient id="a" gradientUnits="userSpaceOnUse" x1="33.4962" y1="31.1591" x2="33.4962" y2="-19.8768">
+ <stop offset="0" stop-color="#a6a6ae"/>
+ <stop offset="1" stop-color="#dadae2"/>
+ </linearGradient>
+ <path fill="url(#a)" d="M61.856,7.462c-0.16,0.375-0.466,0.67-0.848,0.814L21.361,23.327L18.033,9.412L58.74,1.96 c0.69-0.128,1.375,0.243,1.65,0.887l1.466,3.438C62.016,6.661,62.016,7.086,61.856,7.462z M5.016,23.537v31.462h12.982V23.537 H5.016z"/>
+ <path fill="#A6A6AE" d="M3.528,26.021h17.74c0.453,0,0.882-0.205,1.167-0.558c0.285-0.352,0.395-0.814,0.3-1.257L19.297,8.186 C19.149,7.494,18.538,7,17.83,7H3.528c-0.829,0-1.5,0.672-1.5,1.5v16.021C2.028,25.35,2.7,26.021,3.528,26.021z"/>
+ <path fill="#808083" d="M33.486,52.989H3.519c-0.829,0-1.5,0.672-1.5,1.5V62h32.966v-7.51 C34.986,53.661,34.314,52.989,33.486,52.989z"/>
+ <path opacity=".7" fill="#808083" d="M5.016 26.021H17.997999999999998V28.021H5.016z"/>
+</svg>
diff --git a/src/assets/images/icons/delete-icon.svg b/src/assets/images/icons/delete-icon.svg
new file mode 100644
index 0000000..8a8901a
--- /dev/null
+++ b/src/assets/images/icons/delete-icon.svg
@@ -0,0 +1,7 @@
+<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-trash" fill="currentColor"
+ xmlns="http://www.w3.org/2000/svg">
+ <path
+ d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6z"></path>
+ <path fill-rule="evenodd"
+ d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1zM4.118 4L4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118zM2.5 3V2h11v1h-11z"></path>
+</svg>
diff --git a/src/assets/images/icons/edit_graphical.svg b/src/assets/images/icons/edit_graphical.svg
new file mode 100644
index 0000000..14b0fd1
--- /dev/null
+++ b/src/assets/images/icons/edit_graphical.svg
@@ -0,0 +1,14 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="64.001" height="64">
+ <path fill="none" d="M0 0H64.001V64H0z"/>
+ <linearGradient id="a" gradientUnits="userSpaceOnUse" x1="-166.8658" y1="1526.6628" x2="-213.6318" y2="1573.4288" gradientTransform="scale(1 -1) rotate(-45 -1982.2689 489.3917)">
+ <stop offset="0" stop-color="#218076"/>
+ <stop offset="1" stop-color="#1bada2"/>
+ </linearGradient>
+ <path fill="url(#a)" d="M7.426,45.7l43-43c0.8-0.8,2.1-0.8,2.8,0l8.5,8.5c0.8,0.8,0.8,2.1,0,2.8l-43,43L7.426,45.7z"/>
+ <path fill="#B9ADA7" d="M7.426,45.7l-4.9,14c0,0-0.1,0.399-0.1,0.6s0,0.4,0.1,0.601c0.1,0.199,0.2,0.399,0.4,0.6 c0.2,0.2,0.4,0.3,0.6,0.4c0.2,0.1,0.4,0.1,0.6,0.1c0.2,0,0.6-0.1,0.6-0.1l14-4.9L7.426,45.7z"/>
+ <linearGradient id="b" gradientUnits="userSpaceOnUse" x1="2.4185" y1="58.2998" x2="9.8484" y2="58.2998">
+ <stop offset="0" stop-color="#4c4c4c"/>
+ <stop offset="1" stop-color="#7a7a7a"/>
+ </linearGradient>
+ <path fill="url(#b)" d="M2.542,59.679c-0.082,0.203-0.123,0.42-0.123,0.636c0,0.217,0.041,0.434,0.123,0.637 c0.083,0.204,0.207,0.396,0.373,0.561c0.165,0.165,0.356,0.289,0.559,0.372c0.204,0.082,0.421,0.124,0.637,0.124 c0.217,0,0.433-0.042,0.637-0.124l5.086-1.773l-5.518-5.519L2.542,59.679z"/>
+</svg>
diff --git a/src/assets/images/icons/eraser_graphical.svg b/src/assets/images/icons/eraser_graphical.svg
new file mode 100644
index 0000000..cbf4a9e
--- /dev/null
+++ b/src/assets/images/icons/eraser_graphical.svg
@@ -0,0 +1,7 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="64.001" height="64">
+ <path fill="none" d="M0 0H64.001V64H0z"/>
+ <path fill="#F05C51" d="M22.001,60c-1.1,0-2.636-0.636-3.414-1.414L2.915,42.914c-0.778-0.778-0.778-2.051,0-2.828L36.587,6.414 c0.778-0.778,2.051-0.778,2.828,0l20.172,20.172c0.778,0.778,0.778,2.051,0,2.828L30.415,58.586 C29.637,59.364,28.101,60,27.001,60H22.001z"/>
+ <path opacity=".6" fill="#D52B1E" d="M32.001,57l-1.586,1.586C29.637,59.364,28.101,60,27.001,60h-5 c-1.1,0-2.636-0.636-3.414-1.414L2.915,42.914c-0.778-0.778-0.778-2.051,0-2.828L9.001,34L32.001,57z"/>
+ <path fill="#427CAC" d="M49.001,40l12.086-12.086c0.778-0.778,0.778-2.051,0-2.828L40.915,4.914c-0.778-0.778-2.051-0.778-2.828,0 L26.001,17L49.001,40z"/>
+ <path fill="#EDEDED" d="M6.001,37l-1.5,1.5l21.5,21.5h1c0.498,0,1.084-0.135,1.65-0.35L6.001,37z"/>
+</svg>
diff --git a/src/assets/images/icons/install_graphical.svg b/src/assets/images/icons/install_graphical.svg
new file mode 100644
index 0000000..b8b4fb6
--- /dev/null
+++ b/src/assets/images/icons/install_graphical.svg
@@ -0,0 +1,15 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="64.001" height="64">
+ <style>
+ </style>
+ <path fill="none" d="M0 0H64.001V64H0z" id="Asset"/>
+ <g id="Icons">
+ <linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="32.0149" y1="37.2887" x2="32.0149" y2="62.0548">
+ <stop offset="0" stop-color="#ededed"/>
+ <stop offset="1" stop-color="#d0d0d0"/>
+ </linearGradient>
+ <path d="M58.015,44c0-1.1-0.9-2-2-2H8.015c-1.1,0-2,0.9-2,2v16.001c0,1.1,0.9,2,2,2h48.001c1.1,0,2-0.9,2-2V44z" fill="url(#SVGID_1_)"/>
+ <circle cx="49.002" cy="52.001" r="3" fill="#7ba84a"/>
+ <path d="M58.015,60.047c0,1.075-0.9,1.954-2,1.954H8.015c-1.1,0-2-0.88-2-1.955l0,0 C6.015,60.029,58.015,60.029,58.015,60.047L58.015,60.047z" opacity=".15"/>
+ <path d="M46.005,35h-10V6c0-2.209-1.791-4-4-4s-4,1.791-4,4v29h-10l14,14L46.005,35z" fill="#31c3f7"/>
+ </g>
+</svg>
diff --git a/src/assets/images/icons/list-dark.svg b/src/assets/images/icons/list-dark.svg
new file mode 100644
index 0000000..9191e81
--- /dev/null
+++ b/src/assets/images/icons/list-dark.svg
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="25" fill="currentColor" class="bi bi-list" viewBox="0 0 16 16">
+ <path fill-rule="evenodd" d="M2.5 12a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5z"/>
+</svg>
diff --git a/src/assets/images/icons/person-fill-dark.svg b/src/assets/images/icons/person-fill-dark.svg
new file mode 100644
index 0000000..aa6b09d
--- /dev/null
+++ b/src/assets/images/icons/person-fill-dark.svg
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="21" height="22" fill="currentColor" class="bi bi-person-fill" viewBox="0 0 16 16">
+ <path d="M3 14s-1 0-1-1 1-4 6-4 6 3 6 4-1 1-1 1H3zm5-6a3 3 0 1 0 0-6 3 3 0 0 0 0 6z"/>
+</svg>
diff --git a/src/assets/images/icons/question-circle.svg b/src/assets/images/icons/question-circle.svg
new file mode 100644
index 0000000..a7cf7d4
--- /dev/null
+++ b/src/assets/images/icons/question-circle.svg
@@ -0,0 +1,4 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="18" height="21" fill="currentColor" class="bi bi-question-circle" viewBox="0 0 16 16">
+ <path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"/>
+ <path d="M5.255 5.786a.237.237 0 0 0 .241.247h.825c.138 0 .248-.113.266-.25.09-.656.54-1.134 1.342-1.134.686 0 1.314.343 1.314 1.168 0 .635-.374.927-.965 1.371-.673.489-1.206 1.06-1.168 1.987l.003.217a.25.25 0 0 0 .25.246h.811a.25.25 0 0 0 .25-.25v-.105c0-.718.273-.927 1.01-1.486.609-.463 1.244-.977 1.244-2.056 0-1.511-1.276-2.241-2.673-2.241-1.267 0-2.655.59-2.75 2.286zm1.557 5.763c0 .533.425.927 1.01.927.609 0 1.028-.394 1.028-.927 0-.552-.42-.94-1.029-.94-.584 0-1.009.388-1.009.94z"/>
+</svg>
diff --git a/src/assets/images/icons/search_graphical.svg b/src/assets/images/icons/search_graphical.svg
new file mode 100644
index 0000000..89d2d6b
--- /dev/null
+++ b/src/assets/images/icons/search_graphical.svg
@@ -0,0 +1,16 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="64.001" height="64">
+ <path fill="none" d="M0 0H64.001V64H0z"/>
+ <linearGradient id="a" gradientUnits="userSpaceOnUse" x1="16.1731" y1="59.9902" x2="16.1731" y2="31.6028">
+ <stop offset="0" stop-color="#4c4c4c"/>
+ <stop offset="1" stop-color="#7a7a7a"/>
+ </linearGradient>
+ <path fill="url(#a)" d="M8.025,59.99c-1.024,0-2.047-0.391-2.829-1.172c-1.562-1.562-1.562-4.095,0-5.656l16.295-16.295 c1.563-1.562,4.095-1.562,5.657,0c1.562,1.562,1.562,4.095,0,5.656L10.854,58.818C10.073,59.6,9.049,59.99,8.025,59.99z"/>
+ <path opacity=".4" fill="#4B4B4B" d="M21.492,36.867l-5.001,5c1.624,2.148,3.461,4.084,5.717,5.597l4.94-4.941 c1.562-1.562,1.562-4.095,0-5.656C25.587,35.306,23.055,35.305,21.492,36.867z"/>
+ <path fill="#D4EEFC" d="M37.017,4c-12.7,0-23,10.3-23,23c0,5,1.6,9.7,4.3,13.4c1.5,2,3.2,3.8,5.3,5.199 c3.8,2.801,8.4,4.4,13.4,4.4c12.699,0,23-10.3,23-23S49.716,4,37.017,4z"/>
+ <path fill="#317CB3" d="M37.017,9.6c10.199,0,18.5,8.101,19,18.2c0-0.3,0-0.5,0-0.8c0-10.5-8.5-19-19-19s-19,8.5-19,19 c0,0.3,0,0.5,0,0.8C18.517,17.7,26.816,9.6,37.017,9.6z" opacity=".25"/>
+ <linearGradient id="b" gradientUnits="userSpaceOnUse" x1="12.0718" y1="-1489.1914" x2="12.0718" y2="-1442.2994" gradientTransform="matrix(1 0 0 -1 24.9448 -1439.1914)">
+ <stop offset="0" stop-color="#a6a6ae"/>
+ <stop offset="1" stop-color="#dadae2"/>
+ </linearGradient>
+ <path fill="url(#b)" d="M37.017,8c10.5,0,19,8.5,19,19s-8.5,19-19,19c-4,0-7.8-1.2-11.101-3.6 c-1.699-1.2-3.1-2.7-4.3-4.301c-2.399-3.3-3.6-7.1-3.6-11.1C18.017,16.5,26.517,8,37.017,8 M37.017,4c-12.7,0-23,10.3-23,23 c0,5,1.6,9.7,4.3,13.4c1.5,2,3.2,3.8,5.3,5.199c3.8,2.801,8.4,4.4,13.4,4.4c12.699,0,23-10.3,23-23S49.716,4,37.017,4L37.017,4z"/>
+</svg>
diff --git a/src/assets/images/icons/standing-woman.svg b/src/assets/images/icons/standing-woman.svg
new file mode 100644
index 0000000..dd87137
--- /dev/null
+++ b/src/assets/images/icons/standing-woman.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 53.6726 140.4517"><defs><style>.cls-1{isolation:isolate;}.cls-2{fill:#875a5a;}.cls-3{opacity:0.4;}.cls-3,.cls-5{mix-blend-mode:multiply;}.cls-4{fill:#efc3bd;}.cls-5{opacity:0.6;}.cls-6{fill:#d89891;}.cls-7{fill:#c67f79;}.cls-8{fill:#fbb03b;}.cls-9{fill:#363a46;}.cls-10{fill:#0a1045;}</style></defs><g class="cls-1"><g id="Layer_2" data-name="Layer 2"><g id="Layer_1-2" data-name="Layer 1"><path class="cls-2" d="M18.6217,6.3614A3.7477,3.7477,0,0,0,15.9363,0,9.2124,9.2124,0,0,0,12.72.68c-6.9959,2.6674.1452,18.5721.9794,22.0812L15.2856,12.2a8.9483,8.9483,0,0,1,2.4283-4.9037Z"/><g class="cls-3"><path d="M9.9764,9.97c.9013,5.1042,3.2072,10.6508,3.6593,12.5538L15.2215,11.963A8.9479,8.9479,0,0,1,17.55,7.1651l-1.5425-.4639S14.3527,5.266,12.9925,7.8329A5.2355,5.2355,0,0,1,9.9764,9.97Z"/></g><path class="cls-2" d="M15.74,14.7375c2.5945,6.7088,8.5439,3.0426,8.5439,3.0426l6.0725-8.5842.3231-.4571s.9249-1.3612-1.0017-4.0582c-.0148-.0207-.03-.0413-.0453-.062C27.6784,1.9719,22.9544,1.2223,20.2781,2.37,17.5812,3.5252,12.605,6.629,15.74,14.7375Z"/><polygon class="cls-4" points="19.965 14.514 19.347 23.754 19.965 37.372 26.93 37.372 27.928 24.546 26.536 23.754 25.346 14.589 19.965 14.514"/><g class="cls-5"><path class="cls-6" d="M20.9735,14.5278s-1.6261,6.0528,5.2259,6.6337l-.8532-6.5722Z"/></g><path class="cls-4" d="M19.9643,15.0855a4.4222,4.4222,0,0,0,2.5315,3.7637c2.432.9889,4.4384,2.1512,5.7061,1.6755s2.8654-6.1839,2.7-6.8153a10.5665,10.5665,0,0,1,0-3.4268l.0089-.0413a4.7336,4.7336,0,0,0,.0935-.6107c.0759-1.0087-.3181-2.4793-3.5282-4.8433,0,0,.0394.5408-.9644,1.3268s-5.3869,4.3005-5.4391,6.504c0,0-.6344-2.7108-2.4822-1.97S17.3255,14.61,19.9643,15.0855Z"/><ellipse cx="25.8379" cy="11.9603" rx="0.344" ry="0.6117"/><path class="cls-2" d="M24.1091,10.8287a2.7662,2.7662,0,0,1,1.4991-.75,4.7276,4.7276,0,0,1,1.4669-.0379l-.0056-.0282a.8974.8974,0,0,0-.8924-.7263l-.0311.0006a2.5546,2.5546,0,0,0-2.171,1.3308.1443.1443,0,0,0,.1341.2114Z"/><path class="cls-2" d="M28.8953,10.0571a4.7487,4.7487,0,0,1,1.4677.0384,2.67,2.67,0,0,1,.5477.1458,4.7336,4.7336,0,0,0,.0935-.6107,2.4844,2.4844,0,0,0-1.179-.3271c-.0109,0-.0217,0-.0315-.0009a.8992.8992,0,0,0-.8924.7269Z"/><path class="cls-7" d="M28.3879,15.2015l-.0129-.1966a1.4953,1.4953,0,0,0,1.1186-.3957.4278.4278,0,0,0,.103-.2885l-.6472-1.2073.1736-.0932.6663,1.253a.6044.6044,0,0,1-.1362.4518A1.7,1.7,0,0,1,28.3879,15.2015Z"/><path class="cls-7" d="M29.38,16.4694H27.0751A1.5666,1.5666,0,0,0,29.38,16.4694Z"/><path class="cls-2" d="M20.9735,6.5373v6.0805l1.7031-2.2912a5.5054,5.5054,0,0,0,1.0126-1.7887l.47.8323h1.1318L26.36,7.3224,27.048,9.37h.4029l.6678-2.8033.43,2.8033h1.2155l.4087-1.6952.8727,1.5209.3231-.4571s.4124-2.9526-1.0017-4.0582a4.8741,4.8741,0,0,0-2.6438-.4757C24.4929,4.2049,20.9735,6.5373,20.9735,6.5373Z"/><ellipse cx="30.0487" cy="11.9774" rx="0.344" ry="0.6117"/><path class="cls-8" d="M9.5837,42.0825l3.8135,15.9853h17.75l3.09-16.9732s1.3948-2.1641,1.251-6.4449c-.1291-3.8267,3.0281-6.9385-2.022-8.285-2.4556-.655-6.9432-3.609-6.9432-3.609H19.3346s-7.2023,2.9953-9.5052,3.609C4.78,27.7112,9.5837,42.0825,9.5837,42.0825Z"/><g class="cls-3"><path d="M14.4613,36.0278c5,15.23-1.9415,19.0447-1.9415,19.0447L8.68,38.9333S13.6866,33.6683,14.4613,36.0278Z"/></g><path class="cls-4" d="M26.45,22.6972A2.6066,2.6066,0,0,1,23.9756,26.38c-1.9515,0-4.641-1.6247-4.641-3.7188Z"/><path class="cls-9" d="M53.6693,140.4517a2.3285,2.3285,0,0,0-1.9354-2.3338c-2.1061-.6262-4.7814-.6831-6.0337-5.6922H39.6664l.3143,1.547a9.4594,9.4594,0,0,1-.0293,3.862,10.3279,10.3279,0,0,0-.285,2.617Z"/><path class="cls-9" d="M.0033,140.4517a2.3285,2.3285,0,0,1,1.9354-2.3338c2.1061-.6262,4.4-.2277,4.8054-5.6922h6.0338l.0333,2.3542a6.2545,6.2545,0,0,0,.4059,2.1267,9.9353,9.9353,0,0,1,.789,3.5451Z"/><path class="cls-10" d="M13.3972,58.0678S9.4405,65.7018,9.0223,72.633s-4.3351,61.3654-4.3351,61.3654h8.7835L23.44,78.8448l7.7074-20.777Z"/><path class="cls-10" d="M31.1472,58.0678s3.4862,11.6571,4.2277,14.4672c1.9849,7.5217,12.16,61.4634,12.16,61.4634H38.7514L22.5468,79.0392l-9.15-20.9714Z"/><path class="cls-8" d="M14.6133,36.7853a12.13,12.13,0,0,0-1.3366-6.9718c-1.0556-1.9911-2.529-3.8539-4.16-3.2092-3.2135,1.27-2.9953,3.9558-3.2043,9.6492S5.7416,49.694,7.1853,51.3312a3.5008,3.5008,0,0,0,6.0207-1.6879C13.4317,47.8,14.18,40.842,14.6133,36.7853Z"/><g class="cls-3"><path d="M27.6424,67.7272s2.886,23.3546,1.1082,32.3528L23.1791,81.6817l-.6323-2.6425Z"/></g><path class="cls-8" d="M38.4915,43.6064c.1375-4.8328-.0208-15.68-3.8178-16.82,0,0-4.0672.6077-3.8241,5.9062a106.0064,106.0064,0,0,0,.7322,10.8068,4.2384,4.2384,0,0,0,3.5866,3.3328A3.2807,3.2807,0,0,0,38.4915,43.6064Z"/><path class="cls-4" d="M32.2635,40.384,27.4821,40.23l-2.0379,2.16s-10.62,1.8463-14.11,2.2095a5.1033,5.1033,0,0,0-2.969,1.343,4.0946,4.0946,0,0,0-1.1286,4.4577,2.7891,2.7891,0,0,0,2.9729,1.949c2.2048-.2526,16.6034-6.924,16.6034-6.924l.351.0695a2.263,2.263,0,0,0,1.8507-.452l2.6177-1.1175Z"/><g class="cls-5"><polygon class="cls-6" points="27.482 40.23 31.632 43.926 32.264 40.384 27.482 40.23"/></g><path class="cls-4" d="M26.8085,20.5929l3.1633-1.5161,3.8208,3.5371,2.1336,4.8119,2.5966,16.571a3.7856,3.7856,0,0,1-1.5645,2.9824c-1.7856,1.0122-3.8239.8991-5.1776-2.0406-.3481-.756.6728-18.9251.6728-18.9251h0a2.7483,2.7483,0,0,1-2.4479-2.1011l-.349-1.5759-2.2727.0332Z"/><g class="cls-5"><polygon class="cls-6" points="29.657 22.337 29.972 19.077 26.808 20.593 27.384 22.37 29.657 22.337"/></g></g></g></g></svg> \ No newline at end of file
diff --git a/src/assets/images/icons/thumbs-down_graphical.svg b/src/assets/images/icons/thumbs-down_graphical.svg
new file mode 100644
index 0000000..20504e0
--- /dev/null
+++ b/src/assets/images/icons/thumbs-down_graphical.svg
@@ -0,0 +1,8 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="64.001" height="64">
+ <path fill="none" d="M0 0H64.001V64H0z"/>
+ <path fill="#FFD7A5" d="M12.375,33.829l11.883,4.913c0,0-1.543,2.56-4.077,6.824c-4.603,7.746-1.827,11.873,1.584,12.436 l5.718-9.249L43.926,36h14.072l0.029-21.001L43.911,15c0,0-3.626-1.499-8.25-3.218c-8.421-3.13-12.55-2.982-14.632-2.33 c-1.363,0.427-1.481,2.259-1.481,2.259l4.006,7.961L12.375,33.829z"/>
+ <path fill="#FFD7A5" d="M28.536,11.766l-5.367-2.207c-1.688-0.685-3.617,0.124-4.303,1.809c-0.685,1.69,0.123,3.616,1.811,4.305 l-3.705-1.515c-1.695-0.668-3.607,0.169-4.272,1.867c-0.67,1.695,0.169,3.609,1.862,4.271l-2.711-1.112 c-1.688-0.685-3.613,0.132-4.295,1.821c-0.688,1.686,0.131,3.611,1.822,4.294l0.598,0.246c-1.717-0.718-3.693,0.093-4.408,1.814 c-0.715,1.717,0.094,3.69,1.814,4.406l8.156,3.347c1.719,0.715,3.691-0.097,4.406-1.815c0.714-1.717-0.095-3.688-1.813-4.406 l2.43,0.997c1.688,0.68,3.609-0.132,4.293-1.819c0.686-1.69-0.131-3.611-1.82-4.296l1.469,0.603 c1.697,0.664,3.609-0.169,4.277-1.867c0.664-1.696-0.168-3.61-1.865-4.275l-0.866-0.355c1.684,0.684,3.609-0.127,4.296-1.813 C31.03,14.38,30.224,12.452,28.536,11.766z"/>
+ <path opacity=".4" fill="#D48936" d="M28.536,11.766l-1.928-0.793c1.288,0.867,1.839,2.611,1.236,4.091 c-0.657,1.613-2.446,2.41-4.075,1.878l1.518,0.623l-0.254-0.104c1.269,0.86,1.824,2.572,1.247,4.046 c-0.642,1.633-2.434,2.452-4.08,1.922l2.303,0.945c1.697,0.664,3.609-0.169,4.277-1.867c0.664-1.696-0.168-3.61-1.865-4.275 l-0.866-0.355c1.684,0.684,3.609-0.127,4.296-1.813C31.03,14.38,30.224,12.452,28.536,11.766z M23.034,23.771l-1.907-0.782 c1.277,0.869,1.827,2.6,1.227,4.078c-0.655,1.617-2.445,2.414-4.078,1.882l2.285,0.938c1.688,0.68,3.609-0.132,4.293-1.819 C25.54,26.377,24.724,24.456,23.034,23.771z M18.128,28.888l-2.375-0.974c1.701,0.726,2.402,2.673,1.691,4.382 c-0.682,1.639-2.507,2.437-4.167,1.887l2.26,0.928c1.719,0.715,3.691-0.097,4.406-1.815 C20.659,31.578,19.849,29.604,18.128,28.888z"/>
+ <path opacity=".25" fill="#D48936" d="M18.817 27.448L7.634 22.859c-.098-.039-.182-.097-.273-.144.165 1.119.891 2.127 2.018 2.582l8.899 3.651c1.633.532 3.423-.265 4.078-1.882.063-.156.1-.315.139-.473C21.59 27.591 20.136 27.979 18.817 27.448zM13.796 32.672l-8.158-3.347c-.1-.042-.185-.101-.279-.15.176 1.117.9 2.122 2.023 2.589l5.896 2.419c1.659.55 3.485-.248 4.167-1.887.069-.165.111-.333.152-.501C16.667 32.836 15.156 33.238 13.796 32.672zM22.761 21.935l-9.939-4.078c-.108-.042-.202-.103-.303-.154.166 1.132.905 2.147 2.045 2.593l7.637 3.133c1.646.529 3.438-.289 4.08-1.922.056-.142.086-.287.121-.431C25.507 22.06 24.072 22.449 22.761 21.935zM24.302 15.436l-5.367-2.203c-.094-.038-.175-.094-.264-.14.166 1.116.886 2.121 2.006 2.578l3.094 1.27c1.629.532 3.417-.265 4.075-1.878.064-.158.103-.32.142-.481C27.082 15.584 25.622 15.975 24.302 15.436zM34.438 25.045l.938-.347-.694-1.876-.938.348c-9.602 3.551-11.199 12.576-11.447 14.754l1.944.806.01-.243C24.256 38.39 24.772 28.62 34.438 25.045z"/>
+ <path opacity=".3" fill="#D48936" d="M27.482,48.753L43.926,36h14.072l0.003-2H43.222l-17.26,13.409 c0,0-3.994,6.594-5.108,10.339c0.296,0.111,0.599,0.203,0.911,0.254L27.482,48.753z"/>
+</svg>
diff --git a/src/assets/images/icons/thumbs-up_graphical.svg b/src/assets/images/icons/thumbs-up_graphical.svg
new file mode 100644
index 0000000..d0cde66
--- /dev/null
+++ b/src/assets/images/icons/thumbs-up_graphical.svg
@@ -0,0 +1,7 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="64.001" height="64">
+ <path fill="none" d="M0 0H64.001V64H0z"/>
+ <path fill="#FFD7A5" d="M56.619,31.145l-1.13-0.463l0.002-0.001l-15.715-6.448c0,0,1.512-2.535,4.046-6.8 c4.603-7.746,1.827-11.873-1.584-12.436l-5.718,9.249L20.077,27H6.001v21.001L20.092,48c0,0,3.626,1.497,8.25,3.216 c8.421,3.13,12.625,2.88,14.707,2.228c0.031-0.01,0.052-0.028,0.081-0.039c0.876-0.296,1.628-0.942,2.003-1.864 c0.688-1.69-0.123-3.616-1.811-4.305l-1.141-0.469l-0.002-0.004l4.848,1.987c1.697,0.668,3.609-0.169,4.273-1.867 c0.668-1.695-0.17-3.61-1.865-4.271l-5.469-2.244l0.001,0l8.181,3.357c1.688,0.685,3.615-0.132,4.297-1.821 c0.686-1.687-0.131-3.611-1.822-4.294l-4.957-2.034l0,0l4.359,1.788c1.715,0.718,3.691-0.093,4.408-1.813 C59.148,33.834,58.338,31.86,56.619,31.145z"/>
+ <path opacity=".4" fill="#D48936" d="M35.221,40.401c-0.664,1.696,0.166,3.61,1.865,4.275l2.257,0.925 c-1.539-0.746-2.255-2.584-1.623-4.2c0.64-1.633,2.433-2.451,4.078-1.923l-2.302-0.945C37.801,37.87,35.887,38.703,35.221,40.401z M39.354,45.607l-1.399-0.574c-1.686-0.688-3.611,0.124-4.299,1.812c-0.686,1.684,0.119,3.611,1.807,4.298l2.475,1.017 c-1.684-0.688-2.466-2.633-1.782-4.315c0.605-1.485,2.177-2.305,3.694-2.012l-0.284-0.116 C39.49,45.687,39.426,45.642,39.354,45.607z M39.145,34.841c-0.684,1.69,0.133,3.611,1.822,4.296l0.838,0.344 c0.063,0.021,0.128,0.029,0.191,0.053l1.021,0.419c-1.379-0.826-1.993-2.579-1.373-4.112c0.657-1.617,2.446-2.414,4.079-1.882 l-2.285-0.937C41.752,32.342,39.83,33.153,39.145,34.841z M50.723,28.726l-2.262-0.928c-1.717-0.715-3.689,0.097-4.406,1.814 c-0.713,1.718,0.096,3.691,1.818,4.408l2.509,1.029c-1.635-0.767-2.521-2.766-1.827-4.437 C47.239,28.973,49.065,28.175,50.723,28.726z"/>
+ <path opacity=".25" fill="#D48936" d="M48.382,35.05l-0.111-0.045c0,0,0,0,0,0l5.754,2.36c1.229,0.514,2.585,0.234,3.522-0.594 c-0.144-0.039-0.289-0.072-0.43-0.131l-8.152-3.344c-1.366-0.569-2.136-1.931-2.045-3.328c-0.141,0.198-0.267,0.41-0.365,0.645 C45.861,32.284,46.747,34.282,48.382,35.05z M38.557,50.418c-1.339-0.545-2.101-1.872-2.024-3.237 c-0.145,0.205-0.278,0.422-0.376,0.664c-0.684,1.68,0.097,3.622,1.776,4.312l2.907,1.195c1.21,0.488,2.537,0.199,3.446-0.621 c-0.121-0.034-0.243-0.058-0.362-0.106L38.557,50.418z M44.059,38.412c-1.343-0.544-2.112-1.871-2.034-3.242 c-0.147,0.206-0.28,0.425-0.38,0.67c-0.606,1.499-0.028,3.202,1.286,4.049c0.141,0.084,0.281,0.168,0.437,0.232l8.78,3.603 c1.21,0.491,2.537,0.2,3.446-0.622c-0.118-0.033-0.237-0.056-0.353-0.103L44.059,38.412z M40.178,43.952 c-1.36-0.533-2.142-1.869-2.066-3.251c-0.153,0.215-0.29,0.443-0.391,0.701c-0.609,1.556,0.04,3.306,1.46,4.101 c0.097,0.051,0.188,0.111,0.292,0.153l7.555,3.096c1.206,0.475,2.512,0.179,3.41-0.635c-0.106-0.03-0.212-0.048-0.317-0.089 L40.178,43.952z"/>
+ <path opacity=".3" fill="#D48936" d="M40.832,53.35l-4.666-1.918c-1.886-0.367-4.225-1.012-7.127-2.091 c-4.627-1.72-8.256-3.058-8.256-3.058L20.448,46L6.001,46.001v2L20.092,48c0,0,3.626,1.497,8.25,3.216 c8.421,3.13,12.625,2.88,14.707,2.228c0.037-0.012,0.065-0.031,0.1-0.045C42.419,53.651,41.602,53.662,40.832,53.35z M44.897,6.419c0.536,2.042-0.01,5.727-2.544,9.991c-2.521,4.242-3.252,5.453-3.256,5.46l-1.298,2.167l-0.018,0.017 c0.323,6.105-3.419,11.696-9.313,13.915l-0.936,0.352l0.705,1.872l0.935-0.353c6.617-2.49,10.844-8.731,10.61-15.591l-0.018-0.002 c0.05-0.083,1.567-2.621,4.059-6.815C47.076,11.955,46.636,8.296,44.897,6.419z"/>
+</svg>
diff --git a/src/assets/images/icons/triangular-warning-sign.svg b/src/assets/images/icons/triangular-warning-sign.svg
new file mode 100644
index 0000000..82ebf5c
--- /dev/null
+++ b/src/assets/images/icons/triangular-warning-sign.svg
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version='1.1' id='Capa_1' xmlns='http://www.w3.org/2000/svg' x='0px' y='0px'
+ width='458.096px' height='458.096px' viewBox='0 0 458.096 458.096'
+ style='enable-background:new 0 0 458.096 458.096;'
+ xml:space='preserve'>
+<g>
+ <path stroke="#a60000" fill="#a60000" d="M454.106,396.635L247.33,38.496c-3.783-6.555-10.775-10.592-18.344-10.592c-7.566,0-14.561,4.037-18.344,10.592
+ L2.837,398.414c-3.783,6.555-3.783,14.629,0,21.184c3.783,6.556,10.778,10.593,18.344,10.593h415.613c0.041,0,0.088,0.006,0.118,0
+ c11.709,0,21.184-9.481,21.184-21.185C458.096,404.384,456.612,400.116,454.106,396.635z M57.872,387.822L228.986,91.456
+ L400.1,387.828H57.872V387.822z M218.054,163.009h21.982c1.803,0,3.534,0.727,4.8,2.021c1.259,1.3,1.938,3.044,1.892,4.855
+ l-4.416,138.673c-0.095,3.641-3.073,6.537-6.703,6.537h-13.125c-3.635,0-6.614-2.902-6.7-6.537l-4.418-138.673
+ c-0.047-1.812,0.636-3.555,1.895-4.855C214.52,163.736,216.251,163.009,218.054,163.009z M246.449,333.502v25.104
+ c0,3.699-2.997,6.696-6.703,6.696h-21.394c-3.706,0-6.7-2.997-6.7-6.696v-25.104c0-3.7,2.994-6.703,6.7-6.703h21.394
+ C243.452,326.793,246.449,329.802,246.449,333.502z"/>
+</g>
+</svg>
diff --git a/src/assets/images/icons/visible_graphical.svg b/src/assets/images/icons/visible_graphical.svg
new file mode 100644
index 0000000..6875a28
--- /dev/null
+++ b/src/assets/images/icons/visible_graphical.svg
@@ -0,0 +1,18 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="64.001" height="64">
+ <path fill="none" d="M0 0H64.001V64H0z"/>
+ <linearGradient id="a" gradientUnits="userSpaceOnUse" x1="32" y1="54.6846" x2="32" y2="15.9954">
+ <stop offset="0" stop-color="#e9e4e2"/>
+ <stop offset=".6626" stop-color="#f5f3f2"/>
+ </linearGradient>
+ <path fill="url(#a)" d="M32,16.5c-11.962,0-22.851,6.319-28.5,15.528C9.149,41.237,20.038,47.556,32,47.556 c11.963,0,22.851-6.318,28.5-15.527C54.851,22.819,43.963,16.5,32,16.5z"/>
+ <linearGradient id="b" gradientUnits="userSpaceOnUse" x1="32" y1="55.8553" x2="32" y2="15.9035">
+ <stop offset="0" stop-color="#317cb3"/>
+ <stop offset="1" stop-color="#53baf2"/>
+ </linearGradient>
+ <path fill="url(#b)" d="M17.612,31.904c0-7.947,6.443-14.384,14.388-14.384c7.945,0,14.388,6.437,14.388,14.384 c0,7.95-6.442,14.388-14.388,14.388C24.056,46.292,17.612,39.854,17.612,31.904z"/>
+ <path fill="#235482" d="M25.204,31.904c0-3.752,3.044-6.793,6.796-6.793c3.753,0,6.796,3.041,6.796,6.793 c0,3.756-3.043,6.796-6.796,6.796C28.248,38.7,25.204,35.66,25.204,31.904z"/>
+ <path fill="#FFF" d="M24.096,27.278c0-1.809,1.468-3.276,3.278-3.276c1.812,0,3.277,1.468,3.277,3.276 c0,1.814-1.466,3.277-3.277,3.277C25.563,30.556,24.096,29.092,24.096,27.278z" opacity=".4"/>
+ <path fill="#C7BDB9" d="M32,48.556c-12.049,0-23.296-6.133-29.353-16.005l-0.321-0.522l0.321-0.522 C8.704,21.633,19.951,15.5,32,15.5c12.049,0,23.296,6.133,29.353,16.006l0.321,0.522l-0.321,0.522 C55.296,42.423,44.049,48.556,32,48.556z M4.681,32.028C10.378,40.872,21.028,46.556,32,46.556s21.622-5.684,27.319-14.527 C53.622,23.183,42.972,17.5,32,17.5S10.378,23.183,4.681,32.028z"/>
+ <path opacity=".12" d="M34.486,15.545c-0.825-0.046-1.653-0.072-2.486-0.072s-1.661,0.026-2.486,0.072 C18.336,16.312,8.353,22.187,3,30.496c0.48,0.746,1.011,1.464,1.563,2.168c0.322-0.164,0.617-0.393,0.851-0.699 C10.76,24.983,21.195,20.473,32,20.473s21.24,4.511,26.585,11.492c0.234,0.307,0.527,0.537,0.849,0.702 c0.553-0.706,1.084-1.425,1.565-2.171C55.647,22.187,45.664,16.312,34.486,15.545z"/>
+ <path fill="#99999C" d="M60.501,33.528c-0.504,0-0.997-0.254-1.28-0.716C53.701,23.815,43.016,18,32,18 S10.299,23.815,4.779,32.812c-0.434,0.707-1.358,0.927-2.063,0.494c-0.707-0.433-0.928-1.356-0.495-2.062 C8.368,21.225,19.779,15,32,15s23.632,6.225,29.779,16.244c0.433,0.706,0.212,1.63-0.495,2.062 C61.04,33.457,60.769,33.528,60.501,33.528z"/>
+</svg>
diff --git a/src/assets/images/onap-logo.png b/src/assets/images/onap-logo.png
new file mode 100644
index 0000000..41c41cc
--- /dev/null
+++ b/src/assets/images/onap-logo.png
Binary files differ
diff --git a/src/assets/images/tiles/cds.svg b/src/assets/images/tiles/cds.svg
new file mode 100644
index 0000000..fe9a6fe
--- /dev/null
+++ b/src/assets/images/tiles/cds.svg
@@ -0,0 +1,220 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:dc='http://purl.org/dc/elements/1.1/'
+ xmlns:cc='http://creativecommons.org/ns#'
+ xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#'
+ xmlns='http://www.w3.org/2000/svg'
+ xmlns:xlink='http://www.w3.org/1999/xlink'
+ xmlns:sodipodi='http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd'
+ xmlns:inkscape='http://www.inkscape.org/namespaces/inkscape'
+ width='512'
+ height='512'
+ viewBox='0 0 512 512'
+ version='1.1'
+ id='svg50'
+ sodipodi:docname='cds_512.svg'
+ inkscape:version='1.0.1 (3bc2e813f5, 2020-09-07)'>
+ <metadata
+ id="metadata54">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <sodipodi:namedview
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1"
+ objecttolerance="10"
+ gridtolerance="10"
+ guidetolerance="10"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:window-width="1920"
+ inkscape:window-height="1001"
+ id="namedview52"
+ showgrid="false"
+ inkscape:zoom="0.8203125"
+ inkscape:cx="-201.60994"
+ inkscape:cy="210.23007"
+ inkscape:window-x="4791"
+ inkscape:window-y="-9"
+ inkscape:window-maximized="1"
+ inkscape:current-layer="svg50"
+ inkscape:document-rotation="0" />
+ <!-- Generator: Sketch 59.1 (86144) - https://sketch.com -->
+ <title
+ id="title2">logo</title>
+ <desc
+ id="desc4">Created with Sketch.</desc>
+ <defs
+ id="defs10">
+ <polygon
+ id="path-1"
+ points="14.228769,9.2028846 0.065307692,9.2028846 0.065307692,0.30846154 14.228769,0.30846154 " />
+ <polygon
+ id="path-3"
+ points="14.824538,9.1926154 0,9.1926154 0,0.14338461 14.824538,0.14338461 " />
+ <polygon
+ id="path-5"
+ points="7.4436154,8.5450385 0,8.5450385 0,0.029269231 7.4436154,0.029269231 " />
+ <polygon
+ id="path-7"
+ points="14.824538,9.3957692 0,9.3957692 0,0.34653846 14.824538,0.34653846 " />
+ </defs>
+ <g
+ id="g938">
+ <rect
+ style="opacity:1;fill:#ffffff;stroke-width:0.295893"
+ id="rect868"
+ width="512"
+ height="512"
+ x="0"
+ y="0" />
+ <g
+ id="Symbols"
+ stroke="none"
+ stroke-width="1"
+ fill="none"
+ fill-rule="evenodd"
+ transform="matrix(17.195545,0,0,17.195545,0,18.563919)">
+ <g
+ id="Side-Nav"
+ transform="translate(-10,-16)">
+ <g
+ id="logo---icon">
+ <g
+ id="g45">
+ <g
+ id="logo"
+ transform="translate(10,16)">
+ <polygon
+ id="Fill-1"
+ fill="#312f31"
+ points="0.00015384615,18.160692 14.824385,27.307615 29.775154,17.719154 29.775154,9.4553077 14.824385,0.30838461 0.00015384615,9.6445385 " />
+ <g
+ id="Group-26">
+ <g
+ id="Group-4"
+ transform="translate(7.692308)">
+ <mask
+ id="mask-2"
+ fill="#ffffff">
+ <use
+ xlink:href="#path-1"
+ id="use13"
+ x="0"
+ y="0"
+ width="100%"
+ height="100%" />
+ </mask>
+ <g
+ id="Clip-3" />
+ <path
+ d="M 7.1322308,0.30826923 0.06530769,4.7586538 7.1322308,9.2028846 C 9.4976154,7.6855769 11.863385,6.1675 14.228769,4.6501923 11.863385,3.2025 9.4976154,1.7555769 7.1322308,0.30826923"
+ id="Fill-2"
+ fill="#5dbdba"
+ mask="url(#mask-2)" />
+ </g>
+ <path
+ d="m 29.774961,9.4552692 c -2.618076,-1.6015384 -5.235769,-3.2034615 -7.853846,-4.805 -2.365384,1.5173077 -4.731154,3.035 -7.096538,4.5526923 l 7.475,4.6049995 z"
+ id="Fill-5"
+ fill="#17a1a1" />
+ <g
+ id="Group-9"
+ transform="translate(0,4.615385)">
+ <mask
+ id="mask-4"
+ fill="#ffffff">
+ <use
+ xlink:href="#path-3"
+ id="use20"
+ x="0"
+ y="0"
+ width="100%"
+ height="100%" />
+ </mask>
+ <g
+ id="Clip-8" />
+ <polygon
+ id="Fill-7"
+ fill="#17a1a1"
+ mask="url(#mask-4)"
+ points="7.7576154,0.14338461 -7.6923077e-05,5.0291539 7.4437692,9.1926154 14.824538,4.5876154 " />
+ </g>
+ <polygon
+ id="Fill-10"
+ fill="#08809a"
+ points="29.774961,17.719115 29.143423,18.124115 22.299577,13.807961 29.774961,9.4552692 " />
+ <polygon
+ id="Fill-12"
+ fill="#08809a"
+ points="7.4437692,13.808269 14.824538,18.602115 22.299923,13.808269 14.824538,9.2028846 " />
+ <polygon
+ id="Fill-14"
+ fill="#0f699d"
+ points="22.299731,13.808 14.824731,18.602231 21.763577,22.857615 29.143577,18.124154 " />
+ <g
+ id="Group-18"
+ transform="translate(0,9.615385)">
+ <mask
+ id="mask-6"
+ fill="#ffffff">
+ <use
+ xlink:href="#path-5"
+ id="use29"
+ x="0"
+ y="0"
+ width="100%"
+ height="100%" />
+ </mask>
+ <g
+ id="Clip-17" />
+ <polygon
+ id="Fill-16"
+ fill="#08809a"
+ mask="url(#mask-6)"
+ points="-0.00023076923,8.5450385 7.4436154,4.1927308 -0.00023076923,0.029269231 " />
+ </g>
+ <g
+ id="Group-21"
+ transform="translate(0,13.461538)">
+ <mask
+ id="mask-8"
+ fill="#ffffff">
+ <use
+ xlink:href="#path-7"
+ id="use35"
+ x="0"
+ y="0"
+ width="100%"
+ height="100%" />
+ </mask>
+ <g
+ id="Clip-20" />
+ <polygon
+ id="Fill-19"
+ fill="#0f699d"
+ mask="url(#mask-8)"
+ points="14.824538,5.1407692 7.4437692,0.34653846 -7.6923077e-05,4.6992308 7.6118461,9.3957692 " />
+ </g>
+ <polygon
+ id="Fill-22"
+ fill="#1b3d6e"
+ points="14.824538,18.602462 7.6118461,22.857461 14.824538,27.307461 21.763385,22.857461 " />
+ <path
+ d="m 10.958115,20.1415 c -0.820384,0 -1.5015381,-0.07269 -2.0438458,-0.218077 -0.5430769,-0.145385 -0.9765384,-0.387308 -1.3003846,-0.725 -0.3242308,-0.337308 -0.5561538,-0.790385 -0.695,-1.359231 C 6.7804231,17.269961 6.7108077,16.561885 6.7108077,15.715346 v -4.803461 c 0,-0.846539 0.069615,-1.5546158 0.2080769,-2.1238465 0.1388462,-0.5684616 0.3707692,-1.0253846 0.695,-1.3692308 C 7.9377308,7.0745769 8.3711923,6.8330385 8.9142692,6.6941923 9.4565769,6.5553461 10.137731,6.4857308 10.958115,6.4857308 h 7.661539 c 0.503077,0 0.942692,0.069615 1.32,0.2084615 0.376923,0.1388462 0.697692,0.3111538 0.962307,0.5161539 0.265,0.2053846 0.476924,0.4234615 0.635385,0.6549999 0.158846,0.2319231 0.278077,0.4400001 0.357692,0.6250001 L 20.187731,10.237269 C 20.0685,9.9726539 19.876577,9.7311154 19.612346,9.5126539 c -0.265,-0.2184616 -0.655,-0.3273077 -1.171154,-0.3273077 h -7.463077 c -0.595384,0 -1.0123073,0.1257692 -1.2503842,0.376923 C 9.4896539,9.8138077 9.3704231,10.263808 9.3704231,10.911885 v 4.823077 c 0,0.291538 0.02,0.542692 0.059231,0.754615 0.04,0.211538 0.1157693,0.390384 0.2284616,0.535384 0.1123077,0.14577 0.2746154,0.255385 0.4865383,0.327693 0.211538,0.07269 0.489231,0.108846 0.833461,0.108846 h 7.562308 c 0.529231,0 0.919231,-0.108846 1.171154,-0.326923 0.251154,-0.218462 0.436538,-0.46 0.555384,-0.724615 l 1.707308,1.726538 c -0.07923,0.185769 -0.198461,0.394615 -0.357308,0.625385 -0.158846,0.231923 -0.370769,0.45 -0.635,0.655 -0.265,0.205384 -0.585769,0.377307 -0.962692,0.516153 -0.377308,0.138847 -0.817308,0.208462 -1.320384,0.208462 z"
+ id="Fill-24"
+ fill="#fefefe" />
+ </g>
+ </g>
+ </g>
+ </g>
+ </g>
+ </g>
+ </g>
+</svg>
diff --git a/src/assets/images/tiles/dcae-mod.svg b/src/assets/images/tiles/dcae-mod.svg
new file mode 100644
index 0000000..90efc07
--- /dev/null
+++ b/src/assets/images/tiles/dcae-mod.svg
@@ -0,0 +1,279 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ version="1.1"
+ id="svg226"
+ width="512"
+ height="512"
+ viewBox="0 0 512 512"
+ sodipodi:docname="dcae-mod1.svg"
+ inkscape:version="1.1.1 (c3084ef, 2021-09-22)"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:svg="http://www.w3.org/2000/svg">
+ <defs
+ id="defs230" />
+ <sodipodi:namedview
+ id="namedview228"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageshadow="2"
+ inkscape:pageopacity="0.0"
+ inkscape:pagecheckerboard="0"
+ showgrid="false"
+ inkscape:zoom="1.4511719"
+ inkscape:cx="256"
+ inkscape:cy="256"
+ inkscape:window-width="1312"
+ inkscape:window-height="969"
+ inkscape:window-x="0"
+ inkscape:window-y="25"
+ inkscape:window-maximized="0"
+ inkscape:current-layer="g232" />
+ <g
+ inkscape:groupmode="layer"
+ inkscape:label="Image"
+ id="g232">
+ <image
+ width="512"
+ height="512"
+ preserveAspectRatio="none"
+ xlink:href="data:image/png;base64,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
+"
+ id="image234" />
+ </g>
+</svg>
diff --git a/src/assets/images/tiles/kibana.svg b/src/assets/images/tiles/kibana.svg
new file mode 100644
index 0000000..072f2a6
--- /dev/null
+++ b/src/assets/images/tiles/kibana.svg
@@ -0,0 +1,84 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:dc='http://purl.org/dc/elements/1.1/'
+ xmlns:cc='http://creativecommons.org/ns#'
+ xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#'
+ xmlns='http://www.w3.org/2000/svg'
+ xmlns:sodipodi='http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd'
+ xmlns:inkscape='http://www.inkscape.org/namespaces/inkscape'
+ width='512'
+ height='512'
+ viewBox='0 0 512 512'
+ version='1.1'
+ id='svg12'
+ sodipodi:docname='kibana.svg'
+ inkscape:version='1.0.1 (3bc2e813f5, 2020-09-07)'>
+ <metadata
+ id="metadata18">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <defs
+ id="defs16" />
+ <sodipodi:namedview
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1"
+ objecttolerance="10"
+ gridtolerance="10"
+ guidetolerance="10"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:window-width="2400"
+ inkscape:window-height="1271"
+ id="namedview14"
+ showgrid="false"
+ inkscape:zoom="0.50754247"
+ inkscape:cx="-761.90676"
+ inkscape:cy="175.8502"
+ inkscape:window-x="2391"
+ inkscape:window-y="-9"
+ inkscape:window-maximized="1"
+ inkscape:current-layer="svg12" />
+ <!-- Generator: Sketch 63.1 (92452) - https://sketch.com -->
+ <title
+ id="title2">kibana-logo-color-64px</title>
+ <desc
+ id="desc4">Created with Sketch.</desc>
+ <g
+ id="kibana-logo-color-64px"
+ stroke="none"
+ stroke-width="1"
+ fill="none"
+ fill-rule="evenodd"
+ transform="scale(8)">
+ <rect
+ id="bounding-box"
+ x="0"
+ y="0"
+ width="64"
+ height="64" />
+ <g
+ id="group"
+ transform="translate(11,4.9999)">
+ <path
+ d="M 0,20.25 V 48.581 L 20.137,25.392 C 14.149,22.13 7.299,20.25 0,20.25"
+ id="Fill-1"
+ fill="#343741" />
+ <path
+ d="m 0,0 v 20.25 c 7.299,0 14.149,1.88 20.137,5.142 L 42.188,0 Z"
+ id="Fill-3"
+ fill="#f04e98" />
+ <path
+ d="m 24.4434,28.0591 -20.668,23.801 -1.859,2.14 h 39.425 c -2.163,-10.658 -8.349,-19.851 -16.898,-25.941"
+ id="Fill-6"
+ fill="#00bfb3" />
+ </g>
+ </g>
+</svg>
diff --git a/src/assets/images/tiles/onap.svg b/src/assets/images/tiles/onap.svg
new file mode 100644
index 0000000..f76048b
--- /dev/null
+++ b/src/assets/images/tiles/onap.svg
@@ -0,0 +1,157 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:dc='http://purl.org/dc/elements/1.1/'
+ xmlns:cc='http://creativecommons.org/ns#'
+ xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#'
+ xmlns='http://www.w3.org/2000/svg'
+ xmlns:sodipodi='http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd'
+ xmlns:inkscape='http://www.inkscape.org/namespaces/inkscape'
+ width='135.46666mm'
+ height='135.46667mm'
+ viewBox='0 0 135.46666 135.46667'
+ version='1.1'
+ id='svg8'
+ inkscape:version='1.0.1 (3bc2e813f5, 2020-09-07)'
+ sodipodi:docname='onap_512.svg'>
+ <defs
+ id="defs2" />
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0.0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="1"
+ inkscape:cx="254.99999"
+ inkscape:cy="382.49625"
+ inkscape:document-units="mm"
+ inkscape:current-layer="layer1"
+ inkscape:document-rotation="0"
+ showgrid="false"
+ showguides="true"
+ inkscape:guide-bbox="true"
+ inkscape:window-width="1920"
+ inkscape:window-height="1001"
+ inkscape:window-x="4791"
+ inkscape:window-y="-9"
+ inkscape:window-maximized="1" />
+ <metadata
+ id="metadata5">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title />
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1"
+ transform="translate(-38.364587,-46.964534)">
+ <g
+ id="g891">
+ <rect
+ style="fill:none;stroke-width:0.0828541"
+ id="rect931"
+ width="135.46645"
+ height="135.46667"
+ x="38.364693"
+ y="46.964535" />
+ <g
+ id="g929"
+ transform="matrix(0.71408631,0,0,0.71408744,30.33485,32.793562)">
+ <g
+ id="g909">
+ <path
+ style="fill:#252728;fill-opacity:1;stroke:#000000;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="m 11.386385,144.42823 -0.0093,53.56829 h 85.539418 z"
+ id="path7778" />
+ <path
+ style="fill:#252728;fill-opacity:1;stroke:#000000;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="m 200.80945,144.42823 0.009,53.56829 h -85.53941 z"
+ id="path7780" />
+ <path
+ style="fill:#252728;fill-opacity:1;stroke:#000000;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="m 200.80975,84.967496 0.009,-53.568284 h -85.53942 z"
+ id="path7782" />
+ <path
+ style="fill:#252728;fill-opacity:1;stroke:#000000;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="m 11.386085,84.967506 -0.009,-53.568294 h 85.539416 z"
+ id="path7784" />
+ </g>
+ <g
+ id="g918"
+ transform="translate(11.377085,34.307641)">
+ <path
+ id="path7720"
+ style="opacity:1;fill:#1f3d7c;fill-opacity:1;stroke:none;stroke-width:1.48358;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none"
+ d="M 96.13733,150.99858 58.341783,127.44042 c 8.715431,-10.97674 22.069544,-18.22157 37.795545,-23.55817 18.664302,6.41492 28.816872,14.84742 37.795552,23.55817 z"
+ sodipodi:nodetypes="ccccc" />
+ <g
+ id="g7923"
+ transform="translate(9.5787166e-5,5.9572513e-5)">
+ <path
+ id="path7722"
+ style="opacity:0.999;fill:#0081a4;fill-opacity:1;stroke:none;stroke-width:5.60724;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none"
+ d="m 77.654297,214.54883 -12.035156,7.50195 V 385.12305 L 77.654297,392.625 C 134.38081,367.89947 180.88136,337.61289 220.50391,303.58594 178.10009,266.29585 130.2023,237.02747 77.654297,214.54883 Z"
+ transform="scale(0.26458333)"
+ sodipodi:nodetypes="cccccc" />
+ <path
+ id="path7724"
+ style="opacity:0.999;fill:#0081a4;fill-opacity:1;stroke:none;stroke-width:2.8;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none"
+ transform="matrix(0.6711242,0,0,0.41831531,31.023878,52.065734)"
+ d="M 97.021318,123.86938 C 70.596563,109.56205 54.910314,88.989236 40.704548,67.552614 57.969944,45.682337 76.496459,26.404851 97.021315,11.235844 121.08206,26.256569 139.1231,45.547538 153.33809,67.552611 138.87483,91.09219 119.569,109.27412 97.021318,123.86938 Z"
+ sodipodi:nodetypes="ccccc" />
+ <path
+ id="path7726"
+ style="opacity:0.999;fill:#0081a4;fill-opacity:1;stroke:none;stroke-width:5.60724;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none"
+ d="m 649.05273,214.54883 c -50.75146,26.54411 -102.16252,52.42863 -142.84961,89.03711 37.05592,34.81884 82.35689,65.62536 142.84961,89.03906 l 12.03516,-7.50195 V 222.05078 Z"
+ transform="scale(0.26458333)"
+ sodipodi:nodetypes="cccccc" />
+ </g>
+ <g
+ id="g7746"
+ style="opacity:1;fill:#00b0aa;fill-opacity:1"
+ transform="translate(144.72404,-78.959013)">
+ <path
+ id="path7733"
+ style="opacity:0.999;fill:#00b0aa;fill-opacity:1;stroke:none;stroke-width:2.8;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none"
+ transform="matrix(0.6711242,0,0,0.41831531,-151.49571,107.46658)"
+ d="M 97.021318,123.86938 40.704548,67.552614 97.021315,11.235844 C 117.7448,26.909858 136.47987,45.741167 153.33809,67.552611 Z"
+ sodipodi:nodetypes="ccccc" />
+ <path
+ id="path7735"
+ style="opacity:1;fill:#00b0aa;fill-opacity:1;stroke:none;stroke-width:2.8;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none"
+ transform="matrix(0.6711242,0,0,0.41831531,-75.904619,107.46658)"
+ d="M 97.021318,123.86938 40.704548,67.552614 c 15.71848,-22.033132 35.252803,-39.99164 56.316767,-56.31677 l 56.316775,56.316767 z"
+ sodipodi:nodetypes="ccccc" />
+ </g>
+ <path
+ id="path7748"
+ style="opacity:1;fill:#69c7b9;fill-opacity:1;stroke:none;stroke-width:1.48358;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none"
+ d="M 96.13723,56.765859 58.341683,33.207694 96.137228,9.6495269 133.93278,33.207693 Z" />
+ <g
+ id="g7754"
+ style="opacity:1;fill:#006fa0;fill-opacity:1"
+ transform="translate(144.72414,-31.842621)">
+ <path
+ id="path7750"
+ style="opacity:0.999;fill:#006fa0;fill-opacity:1;stroke:none;stroke-width:2.8;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none"
+ transform="matrix(0.6711242,0,0,0.41831531,-151.49571,107.46658)"
+ d="M 97.021318,123.86938 40.704548,67.552614 97.021315,11.235844 153.33809,67.552611 Z" />
+ <path
+ id="path7752"
+ style="opacity:0.999;fill:#006fa0;fill-opacity:1;stroke:none;stroke-width:2.8;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none"
+ transform="matrix(0.6711242,0,0,0.41831531,-75.904619,107.46658)"
+ d="M 97.021318,123.86938 40.704548,67.552614 97.021315,11.235844 153.33809,67.552611 Z" />
+ </g>
+ </g>
+ </g>
+ </g>
+ </g>
+</svg>
diff --git a/src/assets/images/tiles/sdc.svg b/src/assets/images/tiles/sdc.svg
new file mode 100644
index 0000000..04cefab
--- /dev/null
+++ b/src/assets/images/tiles/sdc.svg
@@ -0,0 +1,96 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:dc='http://purl.org/dc/elements/1.1/'
+ xmlns:cc='http://creativecommons.org/ns#'
+ xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#'
+ xmlns='http://www.w3.org/2000/svg'
+ xmlns:xlink='http://www.w3.org/1999/xlink'
+ xmlns:sodipodi='http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd'
+ xmlns:inkscape='http://www.inkscape.org/namespaces/inkscape'
+ width='135.46666mm'
+ height='135.46666mm'
+ viewBox='0 0 135.46666 135.46666'
+ version='1.1'
+ id='svg1715'
+ inkscape:version='1.0.1 (3bc2e813f5, 2020-09-07)'
+ sodipodi:docname='sdc_512.svg'>
+ <defs
+ id="defs1709">
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient851"
+ id="linearGradient1672"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(2.3911867,0,0,2.3661441,-145.87097,-204.18732)"
+ x1="95.654984"
+ y1="137.89748"
+ x2="114.46542"
+ y2="148.75768" />
+ <linearGradient
+ inkscape:collect="always"
+ id="linearGradient851">
+ <stop
+ style="stop-color:#1ae5d9;stop-opacity:1"
+ offset="0"
+ id="stop847" />
+ <stop
+ style="stop-color:#4b7cc9;stop-opacity:1"
+ offset="1"
+ id="stop849" />
+ </linearGradient>
+ </defs>
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0.0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="4"
+ inkscape:cx="240.19182"
+ inkscape:cy="200.15801"
+ inkscape:document-units="mm"
+ inkscape:current-layer="layer1"
+ inkscape:document-rotation="0"
+ showgrid="false"
+ inkscape:window-width="2400"
+ inkscape:window-height="1271"
+ inkscape:window-x="2391"
+ inkscape:window-y="-9"
+ inkscape:window-maximized="1" />
+ <metadata
+ id="metadata1712">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title />
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1"
+ transform="translate(-37.266669,-80.76667)">
+ <rect
+ style="fill:#ffffff;stroke-width:0.1"
+ id="rect843"
+ width="47.247025"
+ height="19.654762"
+ x="81.376488"
+ y="167.51221" />
+ <path
+ id="rect1648"
+ style="fill:#2c333c;fill-opacity:1;stroke-width:0.1"
+ d="M 37.266668,80.76667 V 216.23333 H 172.73333 V 80.76667 Z m 68.051142,28.80444 c 0.008,-5e-5 0.0169,-5e-5 0.0253,0 0.37568,0.0253 0.737,0.1074 1.05627,0.27957 6.74476,4.33831 13.55385,8.09768 20.38635,11.64632 0.62356,0.39304 1.00213,1.10293 1.05161,1.81229 -0.45264,7.75818 -0.1237,15.51637 0,23.27455 -0.0243,0.71973 -0.51981,1.29816 -1.05161,1.81178 -7.06313,3.50751 -13.75925,7.52884 -20.38633,11.64683 -0.61604,0.2797 -1.4333,0.32478 -2.10323,0 -6.529496,-4.03003 -13.080116,-8.04832 -20.386356,-11.64683 -0.59609,-0.38772 -0.952697,-1.09256 -1.051615,-1.81178 0.194219,-7.80674 0.483868,-15.63733 0,-23.27455 0.05981,-0.71636 0.509685,-1.38156 1.051615,-1.81229 7.14441,-3.53315 13.856931,-7.49819 20.386356,-11.64632 0.31868,-0.16359 0.68515,-0.22987 1.02164,-0.27957 z m -0.032,2.06396 c -0.57462,0.003 -1.13829,0.15742 -1.63401,0.44803 -6.525104,3.41263 -12.716996,7.06327 -18.618539,10.9213 v 0.0129 c 6.670706,4.06704 13.326132,8.15445 20.256129,11.87576 7.09558,-3.8726 13.8416,-7.83842 20.26904,-11.88919 v -0.0134 c -6.04188,-3.7501 -11.96787,-7.58298 -18.60506,-10.90786 -0.49577,-0.29043 -1.05944,-0.4448 -1.63401,-0.44752 -0.0112,-6e-5 -0.0224,-6e-5 -0.0336,0 z m -20.552234,13.25139 -0.01188,0.006 c -0.166708,6.83653 -0.301451,13.68584 -0.03049,20.69744 -0.0013,0.89912 0.479559,1.72992 1.259872,2.17661 6.005006,3.92556 12.15492,7.27149 18.336352,10.49135 l 0.0124,-0.006 c 0.14844,-7.28293 0.3112,-14.56586 0.0263,-21.84879 -6.185763,-4.13466 -12.784737,-7.91514 -19.592554,-11.51661 z m 41.228494,0 c -6.65595,3.66352 -13.15809,7.54237 -19.59209,11.51661 -0.28213,7.39937 -0.1253,14.62315 0.0269,21.84879 l 0.0109,0.006 c 6.11212,-3.40849 12.22423,-6.59129 18.33635,-10.49135 1.07293,-0.3839 1.39195,-1.24945 1.26039,-2.17661 0.53235,-7.13183 -0.004,-13.80574 -0.0305,-20.69796 z m -35.28626,45.13471 c 0.980549,0 1.880378,0.0937 2.698544,0.28112 0.818166,0.18737 1.542193,0.41819 2.172994,0.69298 v 2.19211 h -0.130741 c -0.530874,-0.44968 -1.23045,-0.82112 -2.09858,-1.11466 -0.861886,-0.29978 -1.745917,-0.44958 -2.651519,-0.44958 -0.99304,0 -1.792486,0.20584 -2.398303,0.61805 -0.599572,0.41221 -0.89917,0.94313 -0.89917,1.59267 0,0.58083 0.149799,1.03686 0.449585,1.36787 0.299786,0.33102 0.827656,0.58425 1.583365,0.75913 0.399716,0.0874 0.967832,0.19341 1.704805,0.31833 0.736976,0.12491 1.361654,0.2528 1.87379,0.38395 1.036754,0.27481 1.817349,0.69007 2.341975,1.24592 0.524624,0.55585 0.787032,1.33339 0.787032,2.33267 0,0.54336 -0.127894,1.08092 -0.383955,1.61179 -0.249822,0.53088 -0.602927,0.98027 -1.058852,1.34876 -0.499642,0.39971 -1.083559,0.71205 -1.75183,0.93689 -0.662027,0.22484 -1.461479,0.33745 -2.398302,0.33745 -1.005533,0 -1.910956,-0.0938 -2.716631,-0.28112 -0.79943,-0.18737 -1.614673,-0.46557 -2.445329,-0.83406 v -2.32337 h 0.131257 c 0.705747,0.58708 1.520989,1.04004 2.445329,1.35857 0.924341,0.31852 1.792574,0.47801 2.604492,0.47801 1.149181,0 2.041861,-0.21553 2.678907,-0.64647 0.643292,-0.43095 0.965316,-1.0057 0.965316,-1.72393 0,-0.61831 -0.153374,-1.07433 -0.459404,-1.36787 -0.299786,-0.29355 -0.758867,-0.5213 -1.377177,-0.68368 -0.468416,-0.12492 -0.977429,-0.22783 -1.527038,-0.30903 -0.543361,-0.0812 -1.120648,-0.18463 -1.732711,-0.30954 -1.236617,-0.26231 -2.154782,-0.70865 -2.754355,-1.33945 -0.593325,-0.63705 -0.890384,-1.46452 -0.890384,-2.48254 0,-1.16792 0.493734,-2.12379 1.480529,-2.86701 0.986796,-0.74947 2.238693,-1.12396 3.756361,-1.12396 z m 29.23852,0 c 0.74947,0 1.49592,0.0907 2.23914,0.27182 0.74947,0.18112 1.57999,0.49957 2.49184,0.9555 v 2.20141 h -0.14004 c -0.7682,-0.64329 -1.53045,-1.11154 -2.28617,-1.40508 -0.7557,-0.29354 -1.56432,-0.44028 -2.4262,-0.44028 -0.70575,0 -1.34318,0.11514 -1.91152,0.34623 -0.5621,0.22484 -1.06448,0.57794 -1.50791,1.05885 -0.43095,0.46841 -0.76826,1.06149 -1.01183,1.77973 -0.23733,0.712 -0.35605,1.53641 -0.35605,2.47324 0,0.98055 0.13094,1.82381 0.39326,2.52956 0.26856,0.70575 0.61249,1.28049 1.03094,1.72393 0.43719,0.46216 0.94621,0.80558 1.52704,1.03042 0.58708,0.2186 1.20513,0.32815 1.85467,0.32815 0.89311,0 1.73026,-0.15337 2.51096,-0.45941 0.78068,-0.30602 1.51134,-0.76511 2.19211,-1.37717 h 0.13074 v 2.17351 c -0.3435,0.14989 -0.65533,0.29052 -0.93637,0.42168 -0.27481,0.13116 -0.63708,0.26873 -1.08676,0.41238 -0.38098,0.11866 -0.79625,0.21852 -1.24592,0.29972 -0.44343,0.0874 -0.93411,0.13126 -1.47123,0.13126 -1.01178,0 -1.933,-0.14064 -2.76365,-0.42168 -0.82442,-0.28729 -1.54233,-0.73415 -2.15439,-1.33997 -0.59958,-0.59333 -1.06834,-1.3459 -1.4056,-2.25775 -0.33726,-0.9181 -0.50592,-1.983 -0.50592,-3.19463 0,-1.14918 0.16255,-2.17638 0.48731,-3.08198 0.32478,-0.90561 0.79303,-1.6704 1.40509,-2.29495 0.59332,-0.60582 1.3087,-1.06847 2.1456,-1.387 0.84315,-0.31852 1.7766,-0.47749 2.80086,-0.47749 z m -20.825598,0.2527 h 3.485058 c 1.36153,0 2.44224,0.0999 3.24166,0.29972 0.80568,0.19361 1.4864,0.46265 2.04226,0.80615 0.94932,0.59333 1.68915,1.38308 2.22002,2.36988 0.53087,0.9868 0.79633,2.15769 0.79633,3.51297 0,1.26785 -0.27769,2.41734 -0.83354,3.44785 -0.54961,1.03051 -1.28384,1.82996 -2.20194,2.3983 -0.63704,0.39347 -1.34884,0.67779 -2.13578,0.85266 -0.78069,0.17488 -1.81095,0.262 -3.09128,0.262 h -3.522788 z m 1.855188,1.59266 v 10.76421 h 1.74253 c 0.89311,0 1.67064,-0.0652 2.33267,-0.19638 0.66827,-0.13115 1.28021,-0.37522 1.83606,-0.73122 0.69326,-0.44343 1.21145,-1.02735 1.55495,-1.75183 0.34975,-0.72448 0.52451,-1.62991 0.52451,-2.71663 0,-1.09297 -0.19004,-2.01419 -0.57102,-2.76365 -0.38098,-0.74947 -0.94962,-1.3395 -1.70532,-1.77044 -0.54961,-0.31228 -1.13352,-0.5278 -1.75184,-0.64647 -0.6183,-0.12491 -1.35813,-0.18759 -2.22001,-0.18759 z"
+ sodipodi:nodetypes="ccccccccccccccccccccccccccccsccccccccccccccccccscccccscsscscsscccsccccsscscccccscssccccsscccscccssccccccscccssccscsccsscccsccccscccssccsc" />
+ <path
+ id="path1670"
+ style="opacity:1;fill:url(#linearGradient1672);fill-opacity:1;stroke-width:3.09223;stroke-linecap:round"
+ d="m 105.34297,109.57121 c -0.36729,7.7e-4 -0.72808,0.097 -1.04701,0.27913 L 83.909519,121.4971 c -0.650209,0.37155 -1.051463,1.06302 -1.05142,1.8119 v 23.27462 c -5.1e-5,0.74888 0.401205,1.44037 1.05142,1.81192 l 20.386441,11.64676 c 0.65163,0.37228 1.45154,0.37228 2.10317,0 l 20.38641,-11.64676 c 0.65034,-0.37147 1.05173,-1.06297 1.05174,-1.81192 V 123.309 c -2e-5,-0.74894 -0.40141,-1.44044 -1.05174,-1.8119 l -20.38641,-11.64676 c -0.32162,-0.18371 -0.68577,-0.27995 -1.05616,-0.27913 z m -0.0571,2.06355 c 0.0111,-6e-5 0.0223,-6e-5 0.0334,0 0.57458,0.003 1.13822,0.15739 1.63392,0.44795 l 18.60497,10.90786 v 0.0132 l -20.26905,11.8892 -20.255751,-11.87561 v -0.0132 l 18.618581,-10.92118 c 0.49567,-0.29067 1.05931,-0.44529 1.63391,-0.44825 z m -20.552611,13.2517 19.592831,11.51667 -0.0266,21.84884 -0.0119,0.006 -18.336511,-10.49126 c -0.780478,-0.44664 -1.261492,-1.27753 -1.26017,-2.17677 l 0.0307,-20.69748 z m 41.228501,0 0.0119,0.006 0.0307,20.69748 c 10e-4,0.8993 -0.47988,1.7302 -1.26047,2.17677 l -18.33649,10.49126 -0.0107,-0.006 -0.0268,-21.84884 z"
+ sodipodi:nodetypes="ccccccccccccccccccccccccccccccccccccccccc" />
+ </g>
+</svg>
diff --git a/src/assets/images/tiles/sdnc-dg.svg b/src/assets/images/tiles/sdnc-dg.svg
new file mode 100644
index 0000000..6b93a58
--- /dev/null
+++ b/src/assets/images/tiles/sdnc-dg.svg
@@ -0,0 +1,534 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ version="1.1"
+ id="svg387"
+ width="1200"
+ height="1200"
+ viewBox="0 0 1200 1200"
+ sodipodi:docname="onap_lighty.jpg.svg"
+ inkscape:version="1.1.1 (c3084ef, 2021-09-22)"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:svg="http://www.w3.org/2000/svg">
+ <defs
+ id="defs391" />
+ <sodipodi:namedview
+ id="namedview389"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageshadow="2"
+ inkscape:pageopacity="0.0"
+ inkscape:pagecheckerboard="0"
+ showgrid="false"
+ inkscape:zoom="0.59916667"
+ inkscape:cx="630.04172"
+ inkscape:cy="469.81919"
+ inkscape:window-width="1306"
+ inkscape:window-height="969"
+ inkscape:window-x="0"
+ inkscape:window-y="25"
+ inkscape:window-maximized="0"
+ inkscape:current-layer="g393" />
+ <g
+ inkscape:groupmode="layer"
+ inkscape:label="Image"
+ id="g393">
+ <image
+ width="1200"
+ height="1200"
+ preserveAspectRatio="none"
+ xlink:href="data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAoHCAkIBgoJCAkMCwoMDxoRDw4ODx8WGBMaJSEnJiQh
+JCMpLjsyKSw4LCMkM0Y0OD0/QkNCKDFITUhATTtBQj//2wBDAQsMDA8NDx4RER4/KiQqPz8/Pz8/
+Pz8/Pz8/Pz8/Pz8/Pz8/Pz8/Pz8/Pz8/Pz8/Pz8/Pz8/Pz8/Pz8/Pz8/Pz//wgARCASwBLADAREA
+AhEBAxEB/8QAHAABAAICAwEAAAAAAAAAAAAAAAECBgcEBQgD/8QAGgEBAQEAAwEAAAAAAAAAAAAA
+AAECBAUGA//aAAwDAQACEAMQAAAA3MAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+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
+HEMXOGdiZMcgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+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
+pVapZTTsPrxNtdp5L7XIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAA4vz+mBdN6fqfhz66tKppSq1SztPvwtmdl5fmb+UgAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgrLjfB7bGOH3HV/HmU0odh9uJlXN6fKu
+X1H0sAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+HwzviY+3K38vvr5yAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+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
+sY+cmPOYU64s1Uon/hsP/9k=
+"
+ id="image395" />
+ </g>
+</svg>
diff --git a/src/assets/images/tiles/sdnc-odl.svg b/src/assets/images/tiles/sdnc-odl.svg
new file mode 100644
index 0000000..6b93a58
--- /dev/null
+++ b/src/assets/images/tiles/sdnc-odl.svg
@@ -0,0 +1,534 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ version="1.1"
+ id="svg387"
+ width="1200"
+ height="1200"
+ viewBox="0 0 1200 1200"
+ sodipodi:docname="onap_lighty.jpg.svg"
+ inkscape:version="1.1.1 (c3084ef, 2021-09-22)"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:svg="http://www.w3.org/2000/svg">
+ <defs
+ id="defs391" />
+ <sodipodi:namedview
+ id="namedview389"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageshadow="2"
+ inkscape:pageopacity="0.0"
+ inkscape:pagecheckerboard="0"
+ showgrid="false"
+ inkscape:zoom="0.59916667"
+ inkscape:cx="630.04172"
+ inkscape:cy="469.81919"
+ inkscape:window-width="1306"
+ inkscape:window-height="969"
+ inkscape:window-x="0"
+ inkscape:window-y="25"
+ inkscape:window-maximized="0"
+ inkscape:current-layer="g393" />
+ <g
+ inkscape:groupmode="layer"
+ inkscape:label="Image"
+ id="g393">
+ <image
+ width="1200"
+ height="1200"
+ preserveAspectRatio="none"
+ xlink:href="data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAoHCAkIBgoJCAkMCwoMDxoRDw4ODx8WGBMaJSEnJiQh
+JCMpLjsyKSw4LCMkM0Y0OD0/QkNCKDFITUhATTtBQj//2wBDAQsMDA8NDx4RER4/KiQqPz8/Pz8/
+Pz8/Pz8/Pz8/Pz8/Pz8/Pz8/Pz8/Pz8/Pz8/Pz8/Pz8/Pz8/Pz8/Pz8/Pz//wgARCASwBLADAREA
+AhEBAxEB/8QAHAABAAICAwEAAAAAAAAAAAAAAAECBgcEBQgD/8QAGgEBAQEAAwEAAAAAAAAAAAAA
+AAECBAUGA//aAAwDAQACEAMQAAAA3MAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+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
+HEMXOGdiZMcgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+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
+pVapZTTsPrxNtdp5L7XIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAA4vz+mBdN6fqfhz66tKppSq1SztPvwtmdl5fmb+UgAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgrLjfB7bGOH3HV/HmU0odh9uJlXN6fKu
+X1H0sAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+HwzviY+3K38vvr5yAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+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
+sY+cmPOYU64s1Uon/hsP/9k=
+"
+ id="image395" />
+ </g>
+</svg>
diff --git a/src/assets/js/bootstrap.min.js b/src/assets/js/bootstrap.min.js
new file mode 100644
index 0000000..633c722
--- /dev/null
+++ b/src/assets/js/bootstrap.min.js
@@ -0,0 +1,1345 @@
+/*!
+ * Bootstrap v3.3.1 (http://getbootstrap.com)
+ * Copyright 2011-2014 Twitter, Inc.
+ * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
+ */
+if ('undefined' == typeof jQuery) throw new Error("Bootstrap's JavaScript requires jQuery");
++(function (a) {
+ var b = a.fn.jquery.split(' ')[0].split('.');
+ if ((b[0] < 2 && b[1] < 9) || (1 == b[0] && 9 == b[1] && b[2] < 1))
+ throw new Error("Bootstrap's JavaScript requires jQuery version 1.9.1 or higher");
+})(jQuery),
+ +(function (a) {
+ 'use strict';
+ function b() {
+ var a = document.createElement('bootstrap'),
+ b = {
+ WebkitTransition: 'webkitTransitionEnd',
+ MozTransition: 'transitionend',
+ OTransition: 'oTransitionEnd otransitionend',
+ transition: 'transitionend',
+ };
+ for (var c in b) if (void 0 !== a.style[c]) return { end: b[c] };
+ return !1;
+ }
+ (a.fn.emulateTransitionEnd = function (b) {
+ var c = !1,
+ d = this;
+ a(this).one('bsTransitionEnd', function () {
+ c = !0;
+ });
+ var e = function () {
+ c || a(d).trigger(a.support.transition.end);
+ };
+ return setTimeout(e, b), this;
+ }),
+ a(function () {
+ (a.support.transition = b()),
+ a.support.transition &&
+ (a.event.special.bsTransitionEnd = {
+ bindType: a.support.transition.end,
+ delegateType: a.support.transition.end,
+ handle: function (b) {
+ return a(b.target).is(this) ? b.handleObj.handler.apply(this, arguments) : void 0;
+ },
+ });
+ });
+ })(jQuery),
+ +(function (a) {
+ 'use strict';
+ function b(b) {
+ return this.each(function () {
+ var c = a(this),
+ e = c.data('bs.alert');
+ e || c.data('bs.alert', (e = new d(this))), 'string' == typeof b && e[b].call(c);
+ });
+ }
+ var c = '[data-dismiss="alert"]',
+ d = function (b) {
+ a(b).on('click', c, this.close);
+ };
+ (d.VERSION = '3.3.1'),
+ (d.TRANSITION_DURATION = 150),
+ (d.prototype.close = function (b) {
+ function c() {
+ g.detach().trigger('closed.bs.alert').remove();
+ }
+ var e = a(this),
+ f = e.attr('data-target');
+ f || ((f = e.attr('href')), (f = f && f.replace(/.*(?=#[^\s]*$)/, '')));
+ var g = a(f);
+ b && b.preventDefault(),
+ g.length || (g = e.closest('.alert')),
+ g.trigger((b = a.Event('close.bs.alert'))),
+ b.isDefaultPrevented() ||
+ (g.removeClass('in'),
+ a.support.transition && g.hasClass('fade')
+ ? g.one('bsTransitionEnd', c).emulateTransitionEnd(d.TRANSITION_DURATION)
+ : c());
+ });
+ var e = a.fn.alert;
+ (a.fn.alert = b),
+ (a.fn.alert.Constructor = d),
+ (a.fn.alert.noConflict = function () {
+ return (a.fn.alert = e), this;
+ }),
+ a(document).on('click.bs.alert.data-api', c, d.prototype.close);
+ })(jQuery),
+ +(function (a) {
+ 'use strict';
+ function b(b) {
+ return this.each(function () {
+ var d = a(this),
+ e = d.data('bs.button'),
+ f = 'object' == typeof b && b;
+ e || d.data('bs.button', (e = new c(this, f))), 'toggle' == b ? e.toggle() : b && e.setState(b);
+ });
+ }
+ var c = function (b, d) {
+ (this.$element = a(b)), (this.options = a.extend({}, c.DEFAULTS, d)), (this.isLoading = !1);
+ };
+ (c.VERSION = '3.3.1'),
+ (c.DEFAULTS = { loadingText: 'loading...' }),
+ (c.prototype.setState = function (b) {
+ var c = 'disabled',
+ d = this.$element,
+ e = d.is('input') ? 'val' : 'html',
+ f = d.data();
+ (b += 'Text'),
+ null == f.resetText && d.data('resetText', d[e]()),
+ setTimeout(
+ a.proxy(function () {
+ d[e](null == f[b] ? this.options[b] : f[b]),
+ 'loadingText' == b
+ ? ((this.isLoading = !0), d.addClass(c).attr(c, c))
+ : this.isLoading && ((this.isLoading = !1), d.removeClass(c).removeAttr(c));
+ }, this),
+ 0,
+ );
+ }),
+ (c.prototype.toggle = function () {
+ var a = !0,
+ b = this.$element.closest('[data-toggle="buttons"]');
+ if (b.length) {
+ var c = this.$element.find('input');
+ 'radio' == c.prop('type') &&
+ (c.prop('checked') && this.$element.hasClass('active')
+ ? (a = !1)
+ : b.find('.active').removeClass('active')),
+ a && c.prop('checked', !this.$element.hasClass('active')).trigger('change');
+ } else this.$element.attr('aria-pressed', !this.$element.hasClass('active'));
+ a && this.$element.toggleClass('active');
+ });
+ var d = a.fn.button;
+ (a.fn.button = b),
+ (a.fn.button.Constructor = c),
+ (a.fn.button.noConflict = function () {
+ return (a.fn.button = d), this;
+ }),
+ a(document)
+ .on('click.bs.button.data-api', '[data-toggle^="button"]', function (c) {
+ var d = a(c.target);
+ d.hasClass('btn') || (d = d.closest('.btn')), b.call(d, 'toggle'), c.preventDefault();
+ })
+ .on('focus.bs.button.data-api blur.bs.button.data-api', '[data-toggle^="button"]', function (b) {
+ a(b.target)
+ .closest('.btn')
+ .toggleClass('focus', /^focus(in)?$/.test(b.type));
+ });
+ })(jQuery),
+ +(function (a) {
+ 'use strict';
+ function b(b) {
+ return this.each(function () {
+ var d = a(this),
+ e = d.data('bs.carousel'),
+ f = a.extend({}, c.DEFAULTS, d.data(), 'object' == typeof b && b),
+ g = 'string' == typeof b ? b : f.slide;
+ e || d.data('bs.carousel', (e = new c(this, f))),
+ 'number' == typeof b ? e.to(b) : g ? e[g]() : f.interval && e.pause().cycle();
+ });
+ }
+ var c = function (b, c) {
+ (this.$element = a(b)),
+ (this.$indicators = this.$element.find('.carousel-indicators')),
+ (this.options = c),
+ (this.paused = this.sliding = this.interval = this.$active = this.$items = null),
+ this.options.keyboard && this.$element.on('keydown.bs.carousel', a.proxy(this.keydown, this)),
+ 'hover' == this.options.pause &&
+ !('ontouchstart' in document.documentElement) &&
+ this.$element
+ .on('mouseenter.bs.carousel', a.proxy(this.pause, this))
+ .on('mouseleave.bs.carousel', a.proxy(this.cycle, this));
+ };
+ (c.VERSION = '3.3.1'),
+ (c.TRANSITION_DURATION = 600),
+ (c.DEFAULTS = { interval: 5e3, pause: 'hover', wrap: !0, keyboard: !0 }),
+ (c.prototype.keydown = function (a) {
+ if (!/input|textarea/i.test(a.target.tagName)) {
+ switch (a.which) {
+ case 37:
+ this.prev();
+ break;
+ case 39:
+ this.next();
+ break;
+ default:
+ return;
+ }
+ a.preventDefault();
+ }
+ }),
+ (c.prototype.cycle = function (b) {
+ return (
+ b || (this.paused = !1),
+ this.interval && clearInterval(this.interval),
+ this.options.interval &&
+ !this.paused &&
+ (this.interval = setInterval(a.proxy(this.next, this), this.options.interval)),
+ this
+ );
+ }),
+ (c.prototype.getItemIndex = function (a) {
+ return (this.$items = a.parent().children('.item')), this.$items.index(a || this.$active);
+ }),
+ (c.prototype.getItemForDirection = function (a, b) {
+ var c = 'prev' == a ? -1 : 1,
+ d = this.getItemIndex(b),
+ e = (d + c) % this.$items.length;
+ return this.$items.eq(e);
+ }),
+ (c.prototype.to = function (a) {
+ var b = this,
+ c = this.getItemIndex((this.$active = this.$element.find('.item.active')));
+ return a > this.$items.length - 1 || 0 > a
+ ? void 0
+ : this.sliding
+ ? this.$element.one('slid.bs.carousel', function () {
+ b.to(a);
+ })
+ : c == a
+ ? this.pause().cycle()
+ : this.slide(a > c ? 'next' : 'prev', this.$items.eq(a));
+ }),
+ (c.prototype.pause = function (b) {
+ return (
+ b || (this.paused = !0),
+ this.$element.find('.next, .prev').length &&
+ a.support.transition &&
+ (this.$element.trigger(a.support.transition.end), this.cycle(!0)),
+ (this.interval = clearInterval(this.interval)),
+ this
+ );
+ }),
+ (c.prototype.next = function () {
+ return this.sliding ? void 0 : this.slide('next');
+ }),
+ (c.prototype.prev = function () {
+ return this.sliding ? void 0 : this.slide('prev');
+ }),
+ (c.prototype.slide = function (b, d) {
+ var e = this.$element.find('.item.active'),
+ f = d || this.getItemForDirection(b, e),
+ g = this.interval,
+ h = 'next' == b ? 'left' : 'right',
+ i = 'next' == b ? 'first' : 'last',
+ j = this;
+ if (!f.length) {
+ if (!this.options.wrap) return;
+ f = this.$element.find('.item')[i]();
+ }
+ if (f.hasClass('active')) return (this.sliding = !1);
+ var k = f[0],
+ l = a.Event('slide.bs.carousel', { relatedTarget: k, direction: h });
+ if ((this.$element.trigger(l), !l.isDefaultPrevented())) {
+ if (((this.sliding = !0), g && this.pause(), this.$indicators.length)) {
+ this.$indicators.find('.active').removeClass('active');
+ var m = a(this.$indicators.children()[this.getItemIndex(f)]);
+ m && m.addClass('active');
+ }
+ var n = a.Event('slid.bs.carousel', { relatedTarget: k, direction: h });
+ return (
+ a.support.transition && this.$element.hasClass('slide')
+ ? (f.addClass(b),
+ f[0].offsetWidth,
+ e.addClass(h),
+ f.addClass(h),
+ e
+ .one('bsTransitionEnd', function () {
+ f.removeClass([b, h].join(' ')).addClass('active'),
+ e.removeClass(['active', h].join(' ')),
+ (j.sliding = !1),
+ setTimeout(function () {
+ j.$element.trigger(n);
+ }, 0);
+ })
+ .emulateTransitionEnd(c.TRANSITION_DURATION))
+ : (e.removeClass('active'), f.addClass('active'), (this.sliding = !1), this.$element.trigger(n)),
+ g && this.cycle(),
+ this
+ );
+ }
+ });
+ var d = a.fn.carousel;
+ (a.fn.carousel = b),
+ (a.fn.carousel.Constructor = c),
+ (a.fn.carousel.noConflict = function () {
+ return (a.fn.carousel = d), this;
+ });
+ var e = function (c) {
+ var d,
+ e = a(this),
+ f = a(e.attr('data-target') || ((d = e.attr('href')) && d.replace(/.*(?=#[^\s]+$)/, '')));
+ if (f.hasClass('carousel')) {
+ var g = a.extend({}, f.data(), e.data()),
+ h = e.attr('data-slide-to');
+ h && (g.interval = !1), b.call(f, g), h && f.data('bs.carousel').to(h), c.preventDefault();
+ }
+ };
+ a(document)
+ .on('click.bs.carousel.data-api', '[data-slide]', e)
+ .on('click.bs.carousel.data-api', '[data-slide-to]', e),
+ a(window).on('load', function () {
+ a('[data-ride="carousel"]').each(function () {
+ var c = a(this);
+ b.call(c, c.data());
+ });
+ });
+ })(jQuery),
+ +(function (a) {
+ 'use strict';
+ function b(b) {
+ var c,
+ d = b.attr('data-target') || ((c = b.attr('href')) && c.replace(/.*(?=#[^\s]+$)/, ''));
+ return a(d);
+ }
+ function c(b) {
+ return this.each(function () {
+ var c = a(this),
+ e = c.data('bs.collapse'),
+ f = a.extend({}, d.DEFAULTS, c.data(), 'object' == typeof b && b);
+ !e && f.toggle && 'show' == b && (f.toggle = !1),
+ e || c.data('bs.collapse', (e = new d(this, f))),
+ 'string' == typeof b && e[b]();
+ });
+ }
+ var d = function (b, c) {
+ (this.$element = a(b)),
+ (this.options = a.extend({}, d.DEFAULTS, c)),
+ (this.$trigger = a(this.options.trigger).filter('[href="#' + b.id + '"], [data-target="#' + b.id + '"]')),
+ (this.transitioning = null),
+ this.options.parent
+ ? (this.$parent = this.getParent())
+ : this.addAriaAndCollapsedClass(this.$element, this.$trigger),
+ this.options.toggle && this.toggle();
+ };
+ (d.VERSION = '3.3.1'),
+ (d.TRANSITION_DURATION = 350),
+ (d.DEFAULTS = { toggle: !0, trigger: '[data-toggle="collapse"]' }),
+ (d.prototype.dimension = function () {
+ var a = this.$element.hasClass('width');
+ return a ? 'width' : 'height';
+ }),
+ (d.prototype.show = function () {
+ if (!this.transitioning && !this.$element.hasClass('in')) {
+ var b,
+ e = this.$parent && this.$parent.find('> .panel').children('.in, .collapsing');
+ if (!(e && e.length && ((b = e.data('bs.collapse')), b && b.transitioning))) {
+ var f = a.Event('show.bs.collapse');
+ if ((this.$element.trigger(f), !f.isDefaultPrevented())) {
+ e && e.length && (c.call(e, 'hide'), b || e.data('bs.collapse', null));
+ var g = this.dimension();
+ this.$element.removeClass('collapse').addClass('collapsing')[g](0).attr('aria-expanded', !0),
+ this.$trigger.removeClass('collapsed').attr('aria-expanded', !0),
+ (this.transitioning = 1);
+ var h = function () {
+ this.$element.removeClass('collapsing').addClass('collapse in')[g](''),
+ (this.transitioning = 0),
+ this.$element.trigger('shown.bs.collapse');
+ };
+ if (!a.support.transition) return h.call(this);
+ var i = a.camelCase(['scroll', g].join('-'));
+ this.$element
+ .one('bsTransitionEnd', a.proxy(h, this))
+ .emulateTransitionEnd(d.TRANSITION_DURATION)
+ [g](this.$element[0][i]);
+ }
+ }
+ }
+ }),
+ (d.prototype.hide = function () {
+ if (!this.transitioning && this.$element.hasClass('in')) {
+ var b = a.Event('hide.bs.collapse');
+ if ((this.$element.trigger(b), !b.isDefaultPrevented())) {
+ var c = this.dimension();
+ this.$element[c](this.$element[c]())[0].offsetHeight,
+ this.$element.addClass('collapsing').removeClass('collapse in').attr('aria-expanded', !1),
+ this.$trigger.addClass('collapsed').attr('aria-expanded', !1),
+ (this.transitioning = 1);
+ var e = function () {
+ (this.transitioning = 0),
+ this.$element.removeClass('collapsing').addClass('collapse').trigger('hidden.bs.collapse');
+ };
+ return a.support.transition
+ ? void this.$element[c](0)
+ .one('bsTransitionEnd', a.proxy(e, this))
+ .emulateTransitionEnd(d.TRANSITION_DURATION)
+ : e.call(this);
+ }
+ }
+ }),
+ (d.prototype.toggle = function () {
+ this[this.$element.hasClass('in') ? 'hide' : 'show']();
+ }),
+ (d.prototype.getParent = function () {
+ return a(this.options.parent)
+ .find('[data-toggle="collapse"][data-parent="' + this.options.parent + '"]')
+ .each(
+ a.proxy(function (c, d) {
+ var e = a(d);
+ this.addAriaAndCollapsedClass(b(e), e);
+ }, this),
+ )
+ .end();
+ }),
+ (d.prototype.addAriaAndCollapsedClass = function (a, b) {
+ var c = a.hasClass('in');
+ a.attr('aria-expanded', c), b.toggleClass('collapsed', !c).attr('aria-expanded', c);
+ });
+ var e = a.fn.collapse;
+ (a.fn.collapse = c),
+ (a.fn.collapse.Constructor = d),
+ (a.fn.collapse.noConflict = function () {
+ return (a.fn.collapse = e), this;
+ }),
+ a(document).on('click.bs.collapse.data-api', '[data-toggle="collapse"]', function (d) {
+ var e = a(this);
+ e.attr('data-target') || d.preventDefault();
+ var f = b(e),
+ g = f.data('bs.collapse'),
+ h = g ? 'toggle' : a.extend({}, e.data(), { trigger: this });
+ c.call(f, h);
+ });
+ })(jQuery),
+ +(function (a) {
+ 'use strict';
+ function b(b) {
+ (b && 3 === b.which) ||
+ (a(e).remove(),
+ a(f).each(function () {
+ var d = a(this),
+ e = c(d),
+ f = { relatedTarget: this };
+ e.hasClass('open') &&
+ (e.trigger((b = a.Event('hide.bs.dropdown', f))),
+ b.isDefaultPrevented() ||
+ (d.attr('aria-expanded', 'false'), e.removeClass('open').trigger('hidden.bs.dropdown', f)));
+ }));
+ }
+ function c(b) {
+ var c = b.attr('data-target');
+ c || ((c = b.attr('href')), (c = c && /#[A-Za-z]/.test(c) && c.replace(/.*(?=#[^\s]*$)/, '')));
+ var d = c && a(c);
+ return d && d.length ? d : b.parent();
+ }
+ function d(b) {
+ return this.each(function () {
+ var c = a(this),
+ d = c.data('bs.dropdown');
+ d || c.data('bs.dropdown', (d = new g(this))), 'string' == typeof b && d[b].call(c);
+ });
+ }
+ var e = '.dropdown-backdrop',
+ f = '[data-toggle="dropdown"]',
+ g = function (b) {
+ a(b).on('click.bs.dropdown', this.toggle);
+ };
+ (g.VERSION = '3.3.1'),
+ (g.prototype.toggle = function (d) {
+ var e = a(this);
+ if (!e.is('.disabled, :disabled')) {
+ var f = c(e),
+ g = f.hasClass('open');
+ if ((b(), !g)) {
+ 'ontouchstart' in document.documentElement &&
+ !f.closest('.navbar-nav').length &&
+ a('<div class="dropdown-backdrop"/>').insertAfter(a(this)).on('click', b);
+ var h = { relatedTarget: this };
+ if ((f.trigger((d = a.Event('show.bs.dropdown', h))), d.isDefaultPrevented())) return;
+ e.trigger('focus').attr('aria-expanded', 'true'), f.toggleClass('open').trigger('shown.bs.dropdown', h);
+ }
+ return !1;
+ }
+ }),
+ (g.prototype.keydown = function (b) {
+ if (/(38|40|27|32)/.test(b.which) && !/input|textarea/i.test(b.target.tagName)) {
+ var d = a(this);
+ if ((b.preventDefault(), b.stopPropagation(), !d.is('.disabled, :disabled'))) {
+ var e = c(d),
+ g = e.hasClass('open');
+ if ((!g && 27 != b.which) || (g && 27 == b.which))
+ return 27 == b.which && e.find(f).trigger('focus'), d.trigger('click');
+ var h = ' li:not(.divider):visible a',
+ i = e.find('[role="menu"]' + h + ', [role="listbox"]' + h);
+ if (i.length) {
+ var j = i.index(b.target);
+ 38 == b.which && j > 0 && j--,
+ 40 == b.which && j < i.length - 1 && j++,
+ ~j || (j = 0),
+ i.eq(j).trigger('focus');
+ }
+ }
+ }
+ });
+ var h = a.fn.dropdown;
+ (a.fn.dropdown = d),
+ (a.fn.dropdown.Constructor = g),
+ (a.fn.dropdown.noConflict = function () {
+ return (a.fn.dropdown = h), this;
+ }),
+ a(document)
+ .on('click.bs.dropdown.data-api', b)
+ .on('click.bs.dropdown.data-api', '.dropdown form', function (a) {
+ a.stopPropagation();
+ })
+ .on('click.bs.dropdown.data-api', f, g.prototype.toggle)
+ .on('keydown.bs.dropdown.data-api', f, g.prototype.keydown)
+ .on('keydown.bs.dropdown.data-api', '[role="menu"]', g.prototype.keydown)
+ .on('keydown.bs.dropdown.data-api', '[role="listbox"]', g.prototype.keydown);
+ })(jQuery),
+ +(function (a) {
+ 'use strict';
+ function b(b, d) {
+ return this.each(function () {
+ var e = a(this),
+ f = e.data('bs.modal'),
+ g = a.extend({}, c.DEFAULTS, e.data(), 'object' == typeof b && b);
+ f || e.data('bs.modal', (f = new c(this, g))), 'string' == typeof b ? f[b](d) : g.show && f.show(d);
+ });
+ }
+ var c = function (b, c) {
+ (this.options = c),
+ (this.$body = a(document.body)),
+ (this.$element = a(b)),
+ (this.$backdrop = this.isShown = null),
+ (this.scrollbarWidth = 0),
+ this.options.remote &&
+ this.$element.find('.modal-content').load(
+ this.options.remote,
+ a.proxy(function () {
+ this.$element.trigger('loaded.bs.modal');
+ }, this),
+ );
+ };
+ (c.VERSION = '3.3.1'),
+ (c.TRANSITION_DURATION = 300),
+ (c.BACKDROP_TRANSITION_DURATION = 150),
+ (c.DEFAULTS = { backdrop: !0, keyboard: !0, show: !0 }),
+ (c.prototype.toggle = function (a) {
+ return this.isShown ? this.hide() : this.show(a);
+ }),
+ (c.prototype.show = function (b) {
+ var d = this,
+ e = a.Event('show.bs.modal', { relatedTarget: b });
+ this.$element.trigger(e),
+ this.isShown ||
+ e.isDefaultPrevented() ||
+ ((this.isShown = !0),
+ this.checkScrollbar(),
+ this.setScrollbar(),
+ this.$body.addClass('modal-open'),
+ this.escape(),
+ this.resize(),
+ this.$element.on('click.dismiss.bs.modal', '[data-dismiss="modal"]', a.proxy(this.hide, this)),
+ this.backdrop(function () {
+ var e = a.support.transition && d.$element.hasClass('fade');
+ d.$element.parent().length || d.$element.appendTo(d.$body),
+ d.$element.show().scrollTop(0),
+ d.options.backdrop && d.adjustBackdrop(),
+ d.adjustDialog(),
+ e && d.$element[0].offsetWidth,
+ d.$element.addClass('in').attr('aria-hidden', !1),
+ d.enforceFocus();
+ var f = a.Event('shown.bs.modal', { relatedTarget: b });
+ e
+ ? d.$element
+ .find('.modal-dialog')
+ .one('bsTransitionEnd', function () {
+ d.$element.trigger('focus').trigger(f);
+ })
+ .emulateTransitionEnd(c.TRANSITION_DURATION)
+ : d.$element.trigger('focus').trigger(f);
+ }));
+ }),
+ (c.prototype.hide = function (b) {
+ b && b.preventDefault(),
+ (b = a.Event('hide.bs.modal')),
+ this.$element.trigger(b),
+ this.isShown &&
+ !b.isDefaultPrevented() &&
+ ((this.isShown = !1),
+ this.escape(),
+ this.resize(),
+ a(document).off('focusin.bs.modal'),
+ this.$element.removeClass('in').attr('aria-hidden', !0).off('click.dismiss.bs.modal'),
+ a.support.transition && this.$element.hasClass('fade')
+ ? this.$element
+ .one('bsTransitionEnd', a.proxy(this.hideModal, this))
+ .emulateTransitionEnd(c.TRANSITION_DURATION)
+ : this.hideModal());
+ }),
+ (c.prototype.enforceFocus = function () {
+ a(document)
+ .off('focusin.bs.modal')
+ .on(
+ 'focusin.bs.modal',
+ a.proxy(function (a) {
+ this.$element[0] === a.target || this.$element.has(a.target).length || this.$element.trigger('focus');
+ }, this),
+ );
+ }),
+ (c.prototype.escape = function () {
+ this.isShown && this.options.keyboard
+ ? this.$element.on(
+ 'keydown.dismiss.bs.modal',
+ a.proxy(function (a) {
+ 27 == a.which && this.hide();
+ }, this),
+ )
+ : this.isShown || this.$element.off('keydown.dismiss.bs.modal');
+ }),
+ (c.prototype.resize = function () {
+ this.isShown
+ ? a(window).on('resize.bs.modal', a.proxy(this.handleUpdate, this))
+ : a(window).off('resize.bs.modal');
+ }),
+ (c.prototype.hideModal = function () {
+ var a = this;
+ this.$element.hide(),
+ this.backdrop(function () {
+ a.$body.removeClass('modal-open'),
+ a.resetAdjustments(),
+ a.resetScrollbar(),
+ a.$element.trigger('hidden.bs.modal');
+ });
+ }),
+ (c.prototype.removeBackdrop = function () {
+ this.$backdrop && this.$backdrop.remove(), (this.$backdrop = null);
+ }),
+ (c.prototype.backdrop = function (b) {
+ var d = this,
+ e = this.$element.hasClass('fade') ? 'fade' : '';
+ if (this.isShown && this.options.backdrop) {
+ var f = a.support.transition && e;
+ if (
+ ((this.$backdrop = a('<div class="modal-backdrop ' + e + '" />')
+ .prependTo(this.$element)
+ .on(
+ 'click.dismiss.bs.modal',
+ a.proxy(function (a) {
+ a.target === a.currentTarget &&
+ ('static' == this.options.backdrop
+ ? this.$element[0].focus.call(this.$element[0])
+ : this.hide.call(this));
+ }, this),
+ )),
+ f && this.$backdrop[0].offsetWidth,
+ this.$backdrop.addClass('in'),
+ !b)
+ )
+ return;
+ f ? this.$backdrop.one('bsTransitionEnd', b).emulateTransitionEnd(c.BACKDROP_TRANSITION_DURATION) : b();
+ } else if (!this.isShown && this.$backdrop) {
+ this.$backdrop.removeClass('in');
+ var g = function () {
+ d.removeBackdrop(), b && b();
+ };
+ a.support.transition && this.$element.hasClass('fade')
+ ? this.$backdrop.one('bsTransitionEnd', g).emulateTransitionEnd(c.BACKDROP_TRANSITION_DURATION)
+ : g();
+ } else b && b();
+ }),
+ (c.prototype.handleUpdate = function () {
+ this.options.backdrop && this.adjustBackdrop(), this.adjustDialog();
+ }),
+ (c.prototype.adjustBackdrop = function () {
+ this.$backdrop.css('height', 0).css('height', this.$element[0].scrollHeight);
+ }),
+ (c.prototype.adjustDialog = function () {
+ var a = this.$element[0].scrollHeight > document.documentElement.clientHeight;
+ this.$element.css({
+ paddingLeft: !this.bodyIsOverflowing && a ? this.scrollbarWidth : '',
+ paddingRight: this.bodyIsOverflowing && !a ? this.scrollbarWidth : '',
+ });
+ }),
+ (c.prototype.resetAdjustments = function () {
+ this.$element.css({ paddingLeft: '', paddingRight: '' });
+ }),
+ (c.prototype.checkScrollbar = function () {
+ (this.bodyIsOverflowing = document.body.scrollHeight > document.documentElement.clientHeight),
+ (this.scrollbarWidth = this.measureScrollbar());
+ }),
+ (c.prototype.setScrollbar = function () {
+ var a = parseInt(this.$body.css('padding-right') || 0, 10);
+ this.bodyIsOverflowing && this.$body.css('padding-right', a + this.scrollbarWidth);
+ }),
+ (c.prototype.resetScrollbar = function () {
+ this.$body.css('padding-right', '');
+ }),
+ (c.prototype.measureScrollbar = function () {
+ var a = document.createElement('div');
+ (a.className = 'modal-scrollbar-measure'), this.$body.append(a);
+ var b = a.offsetWidth - a.clientWidth;
+ return this.$body[0].removeChild(a), b;
+ });
+ var d = a.fn.modal;
+ (a.fn.modal = b),
+ (a.fn.modal.Constructor = c),
+ (a.fn.modal.noConflict = function () {
+ return (a.fn.modal = d), this;
+ }),
+ a(document).on('click.bs.modal.data-api', '[data-toggle="modal"]', function (c) {
+ var d = a(this),
+ e = d.attr('href'),
+ f = a(d.attr('data-target') || (e && e.replace(/.*(?=#[^\s]+$)/, ''))),
+ g = f.data('bs.modal') ? 'toggle' : a.extend({ remote: !/#/.test(e) && e }, f.data(), d.data());
+ d.is('a') && c.preventDefault(),
+ f.one('show.bs.modal', function (a) {
+ a.isDefaultPrevented() ||
+ f.one('hidden.bs.modal', function () {
+ d.is(':visible') && d.trigger('focus');
+ });
+ }),
+ b.call(f, g, this);
+ });
+ })(jQuery),
+ +(function (a) {
+ 'use strict';
+ function b(b) {
+ return this.each(function () {
+ var d = a(this),
+ e = d.data('bs.tooltip'),
+ f = 'object' == typeof b && b,
+ g = f && f.selector;
+ (e || 'destroy' != b) &&
+ (g
+ ? (e || d.data('bs.tooltip', (e = {})), e[g] || (e[g] = new c(this, f)))
+ : e || d.data('bs.tooltip', (e = new c(this, f))),
+ 'string' == typeof b && e[b]());
+ });
+ }
+ var c = function (a, b) {
+ (this.type = this.options = this.enabled = this.timeout = this.hoverState = this.$element = null),
+ this.init('tooltip', a, b);
+ };
+ (c.VERSION = '3.3.1'),
+ (c.TRANSITION_DURATION = 150),
+ (c.DEFAULTS = {
+ animation: !0,
+ placement: 'top',
+ selector: !1,
+ template:
+ '<div class="tooltip" role="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>',
+ trigger: 'hover focus',
+ title: '',
+ delay: 0,
+ html: !1,
+ container: !1,
+ viewport: { selector: 'body', padding: 0 },
+ }),
+ (c.prototype.init = function (b, c, d) {
+ (this.enabled = !0),
+ (this.type = b),
+ (this.$element = a(c)),
+ (this.options = this.getOptions(d)),
+ (this.$viewport = this.options.viewport && a(this.options.viewport.selector || this.options.viewport));
+ for (var e = this.options.trigger.split(' '), f = e.length; f--; ) {
+ var g = e[f];
+ if ('click' == g) this.$element.on('click.' + this.type, this.options.selector, a.proxy(this.toggle, this));
+ else if ('manual' != g) {
+ var h = 'hover' == g ? 'mouseenter' : 'focusin',
+ i = 'hover' == g ? 'mouseleave' : 'focusout';
+ this.$element.on(h + '.' + this.type, this.options.selector, a.proxy(this.enter, this)),
+ this.$element.on(i + '.' + this.type, this.options.selector, a.proxy(this.leave, this));
+ }
+ }
+ this.options.selector
+ ? (this._options = a.extend({}, this.options, { trigger: 'manual', selector: '' }))
+ : this.fixTitle();
+ }),
+ (c.prototype.getDefaults = function () {
+ return c.DEFAULTS;
+ }),
+ (c.prototype.getOptions = function (b) {
+ return (
+ (b = a.extend({}, this.getDefaults(), this.$element.data(), b)),
+ b.delay && 'number' == typeof b.delay && (b.delay = { show: b.delay, hide: b.delay }),
+ b
+ );
+ }),
+ (c.prototype.getDelegateOptions = function () {
+ var b = {},
+ c = this.getDefaults();
+ return (
+ this._options &&
+ a.each(this._options, function (a, d) {
+ c[a] != d && (b[a] = d);
+ }),
+ b
+ );
+ }),
+ (c.prototype.enter = function (b) {
+ var c = b instanceof this.constructor ? b : a(b.currentTarget).data('bs.' + this.type);
+ return c && c.$tip && c.$tip.is(':visible')
+ ? void (c.hoverState = 'in')
+ : (c ||
+ ((c = new this.constructor(b.currentTarget, this.getDelegateOptions())),
+ a(b.currentTarget).data('bs.' + this.type, c)),
+ clearTimeout(c.timeout),
+ (c.hoverState = 'in'),
+ c.options.delay && c.options.delay.show
+ ? void (c.timeout = setTimeout(function () {
+ 'in' == c.hoverState && c.show();
+ }, c.options.delay.show))
+ : c.show());
+ }),
+ (c.prototype.leave = function (b) {
+ var c = b instanceof this.constructor ? b : a(b.currentTarget).data('bs.' + this.type);
+ return (
+ c ||
+ ((c = new this.constructor(b.currentTarget, this.getDelegateOptions())),
+ a(b.currentTarget).data('bs.' + this.type, c)),
+ clearTimeout(c.timeout),
+ (c.hoverState = 'out'),
+ c.options.delay && c.options.delay.hide
+ ? void (c.timeout = setTimeout(function () {
+ 'out' == c.hoverState && c.hide();
+ }, c.options.delay.hide))
+ : c.hide()
+ );
+ }),
+ (c.prototype.show = function () {
+ var b = a.Event('show.bs.' + this.type);
+ if (this.hasContent() && this.enabled) {
+ this.$element.trigger(b);
+ var d = a.contains(this.$element[0].ownerDocument.documentElement, this.$element[0]);
+ if (b.isDefaultPrevented() || !d) return;
+ var e = this,
+ f = this.tip(),
+ g = this.getUID(this.type);
+ this.setContent(),
+ f.attr('id', g),
+ this.$element.attr('aria-describedby', g),
+ this.options.animation && f.addClass('fade');
+ var h =
+ 'function' == typeof this.options.placement
+ ? this.options.placement.call(this, f[0], this.$element[0])
+ : this.options.placement,
+ i = /\s?auto?\s?/i,
+ j = i.test(h);
+ j && (h = h.replace(i, '') || 'top'),
+ f
+ .detach()
+ .css({ top: 0, left: 0, display: 'block' })
+ .addClass(h)
+ .data('bs.' + this.type, this),
+ this.options.container ? f.appendTo(this.options.container) : f.insertAfter(this.$element);
+ var k = this.getPosition(),
+ l = f[0].offsetWidth,
+ m = f[0].offsetHeight;
+ if (j) {
+ var n = h,
+ o = this.options.container ? a(this.options.container) : this.$element.parent(),
+ p = this.getPosition(o);
+ (h =
+ 'bottom' == h && k.bottom + m > p.bottom
+ ? 'top'
+ : 'top' == h && k.top - m < p.top
+ ? 'bottom'
+ : 'right' == h && k.right + l > p.width
+ ? 'left'
+ : 'left' == h && k.left - l < p.left
+ ? 'right'
+ : h),
+ f.removeClass(n).addClass(h);
+ }
+ var q = this.getCalculatedOffset(h, k, l, m);
+ this.applyPlacement(q, h);
+ var r = function () {
+ var a = e.hoverState;
+ e.$element.trigger('shown.bs.' + e.type), (e.hoverState = null), 'out' == a && e.leave(e);
+ };
+ a.support.transition && this.$tip.hasClass('fade')
+ ? f.one('bsTransitionEnd', r).emulateTransitionEnd(c.TRANSITION_DURATION)
+ : r();
+ }
+ }),
+ (c.prototype.applyPlacement = function (b, c) {
+ var d = this.tip(),
+ e = d[0].offsetWidth,
+ f = d[0].offsetHeight,
+ g = parseInt(d.css('margin-top'), 10),
+ h = parseInt(d.css('margin-left'), 10);
+ isNaN(g) && (g = 0),
+ isNaN(h) && (h = 0),
+ (b.top = b.top + g),
+ (b.left = b.left + h),
+ a.offset.setOffset(
+ d[0],
+ a.extend(
+ {
+ using: function (a) {
+ d.css({ top: Math.round(a.top), left: Math.round(a.left) });
+ },
+ },
+ b,
+ ),
+ 0,
+ ),
+ d.addClass('in');
+ var i = d[0].offsetWidth,
+ j = d[0].offsetHeight;
+ 'top' == c && j != f && (b.top = b.top + f - j);
+ var k = this.getViewportAdjustedDelta(c, b, i, j);
+ k.left ? (b.left += k.left) : (b.top += k.top);
+ var l = /top|bottom/.test(c),
+ m = l ? 2 * k.left - e + i : 2 * k.top - f + j,
+ n = l ? 'offsetWidth' : 'offsetHeight';
+ d.offset(b), this.replaceArrow(m, d[0][n], l);
+ }),
+ (c.prototype.replaceArrow = function (a, b, c) {
+ this.arrow()
+ .css(c ? 'left' : 'top', 50 * (1 - a / b) + '%')
+ .css(c ? 'top' : 'left', '');
+ }),
+ (c.prototype.setContent = function () {
+ var a = this.tip(),
+ b = this.getTitle();
+ a.find('.tooltip-inner')[this.options.html ? 'html' : 'text'](b),
+ a.removeClass('fade in top bottom left right');
+ }),
+ (c.prototype.hide = function (b) {
+ function d() {
+ 'in' != e.hoverState && f.detach(),
+ e.$element.removeAttr('aria-describedby').trigger('hidden.bs.' + e.type),
+ b && b();
+ }
+ var e = this,
+ f = this.tip(),
+ g = a.Event('hide.bs.' + this.type);
+ return (
+ this.$element.trigger(g),
+ g.isDefaultPrevented()
+ ? void 0
+ : (f.removeClass('in'),
+ a.support.transition && this.$tip.hasClass('fade')
+ ? f.one('bsTransitionEnd', d).emulateTransitionEnd(c.TRANSITION_DURATION)
+ : d(),
+ (this.hoverState = null),
+ this)
+ );
+ }),
+ (c.prototype.fixTitle = function () {
+ var a = this.$element;
+ (a.attr('title') || 'string' != typeof a.attr('data-original-title')) &&
+ a.attr('data-original-title', a.attr('title') || '').attr('title', '');
+ }),
+ (c.prototype.hasContent = function () {
+ return this.getTitle();
+ }),
+ (c.prototype.getPosition = function (b) {
+ b = b || this.$element;
+ var c = b[0],
+ d = 'BODY' == c.tagName,
+ e = c.getBoundingClientRect();
+ null == e.width && (e = a.extend({}, e, { width: e.right - e.left, height: e.bottom - e.top }));
+ var f = d ? { top: 0, left: 0 } : b.offset(),
+ g = { scroll: d ? document.documentElement.scrollTop || document.body.scrollTop : b.scrollTop() },
+ h = d ? { width: a(window).width(), height: a(window).height() } : null;
+ return a.extend({}, e, g, h, f);
+ }),
+ (c.prototype.getCalculatedOffset = function (a, b, c, d) {
+ return 'bottom' == a
+ ? { top: b.top + b.height, left: b.left + b.width / 2 - c / 2 }
+ : 'top' == a
+ ? { top: b.top - d, left: b.left + b.width / 2 - c / 2 }
+ : 'left' == a
+ ? { top: b.top + b.height / 2 - d / 2, left: b.left - c }
+ : { top: b.top + b.height / 2 - d / 2, left: b.left + b.width };
+ }),
+ (c.prototype.getViewportAdjustedDelta = function (a, b, c, d) {
+ var e = { top: 0, left: 0 };
+ if (!this.$viewport) return e;
+ var f = (this.options.viewport && this.options.viewport.padding) || 0,
+ g = this.getPosition(this.$viewport);
+ if (/right|left/.test(a)) {
+ var h = b.top - f - g.scroll,
+ i = b.top + f - g.scroll + d;
+ h < g.top ? (e.top = g.top - h) : i > g.top + g.height && (e.top = g.top + g.height - i);
+ } else {
+ var j = b.left - f,
+ k = b.left + f + c;
+ j < g.left ? (e.left = g.left - j) : k > g.width && (e.left = g.left + g.width - k);
+ }
+ return e;
+ }),
+ (c.prototype.getTitle = function () {
+ var a,
+ b = this.$element,
+ c = this.options;
+ return (a = b.attr('data-original-title') || ('function' == typeof c.title ? c.title.call(b[0]) : c.title));
+ }),
+ (c.prototype.getUID = function (a) {
+ do a += ~~(1e6 * Math.random());
+ while (document.getElementById(a));
+ return a;
+ }),
+ (c.prototype.tip = function () {
+ return (this.$tip = this.$tip || a(this.options.template));
+ }),
+ (c.prototype.arrow = function () {
+ return (this.$arrow = this.$arrow || this.tip().find('.tooltip-arrow'));
+ }),
+ (c.prototype.enable = function () {
+ this.enabled = !0;
+ }),
+ (c.prototype.disable = function () {
+ this.enabled = !1;
+ }),
+ (c.prototype.toggleEnabled = function () {
+ this.enabled = !this.enabled;
+ }),
+ (c.prototype.toggle = function (b) {
+ var c = this;
+ b &&
+ ((c = a(b.currentTarget).data('bs.' + this.type)),
+ c ||
+ ((c = new this.constructor(b.currentTarget, this.getDelegateOptions())),
+ a(b.currentTarget).data('bs.' + this.type, c))),
+ c.tip().hasClass('in') ? c.leave(c) : c.enter(c);
+ }),
+ (c.prototype.destroy = function () {
+ var a = this;
+ clearTimeout(this.timeout),
+ this.hide(function () {
+ a.$element.off('.' + a.type).removeData('bs.' + a.type);
+ });
+ });
+ var d = a.fn.tooltip;
+ (a.fn.tooltip = b),
+ (a.fn.tooltip.Constructor = c),
+ (a.fn.tooltip.noConflict = function () {
+ return (a.fn.tooltip = d), this;
+ });
+ })(jQuery),
+ +(function (a) {
+ 'use strict';
+ function b(b) {
+ return this.each(function () {
+ var d = a(this),
+ e = d.data('bs.popover'),
+ f = 'object' == typeof b && b,
+ g = f && f.selector;
+ (e || 'destroy' != b) &&
+ (g
+ ? (e || d.data('bs.popover', (e = {})), e[g] || (e[g] = new c(this, f)))
+ : e || d.data('bs.popover', (e = new c(this, f))),
+ 'string' == typeof b && e[b]());
+ });
+ }
+ var c = function (a, b) {
+ this.init('popover', a, b);
+ };
+ if (!a.fn.tooltip) throw new Error('Popover requires tooltip.js');
+ (c.VERSION = '3.3.1'),
+ (c.DEFAULTS = a.extend({}, a.fn.tooltip.Constructor.DEFAULTS, {
+ placement: 'right',
+ trigger: 'click',
+ content: '',
+ template:
+ '<div class="popover" role="tooltip"><div class="arrow"></div><h3 class="popover-title"></h3><div class="popover-content"></div></div>',
+ })),
+ (c.prototype = a.extend({}, a.fn.tooltip.Constructor.prototype)),
+ (c.prototype.constructor = c),
+ (c.prototype.getDefaults = function () {
+ return c.DEFAULTS;
+ }),
+ (c.prototype.setContent = function () {
+ var a = this.tip(),
+ b = this.getTitle(),
+ c = this.getContent();
+ a.find('.popover-title')[this.options.html ? 'html' : 'text'](b),
+ a
+ .find('.popover-content')
+ .children()
+ .detach()
+ .end()
+ [this.options.html ? ('string' == typeof c ? 'html' : 'append') : 'text'](c),
+ a.removeClass('fade top bottom left right in'),
+ a.find('.popover-title').html() || a.find('.popover-title').hide();
+ }),
+ (c.prototype.hasContent = function () {
+ return this.getTitle() || this.getContent();
+ }),
+ (c.prototype.getContent = function () {
+ var a = this.$element,
+ b = this.options;
+ return a.attr('data-content') || ('function' == typeof b.content ? b.content.call(a[0]) : b.content);
+ }),
+ (c.prototype.arrow = function () {
+ return (this.$arrow = this.$arrow || this.tip().find('.arrow'));
+ }),
+ (c.prototype.tip = function () {
+ return this.$tip || (this.$tip = a(this.options.template)), this.$tip;
+ });
+ var d = a.fn.popover;
+ (a.fn.popover = b),
+ (a.fn.popover.Constructor = c),
+ (a.fn.popover.noConflict = function () {
+ return (a.fn.popover = d), this;
+ });
+ })(jQuery),
+ +(function (a) {
+ 'use strict';
+ function b(c, d) {
+ var e = a.proxy(this.process, this);
+ (this.$body = a('body')),
+ (this.$scrollElement = a(a(c).is('body') ? window : c)),
+ (this.options = a.extend({}, b.DEFAULTS, d)),
+ (this.selector = (this.options.target || '') + ' .nav li > a'),
+ (this.offsets = []),
+ (this.targets = []),
+ (this.activeTarget = null),
+ (this.scrollHeight = 0),
+ this.$scrollElement.on('scroll.bs.scrollspy', e),
+ this.refresh(),
+ this.process();
+ }
+ function c(c) {
+ return this.each(function () {
+ var d = a(this),
+ e = d.data('bs.scrollspy'),
+ f = 'object' == typeof c && c;
+ e || d.data('bs.scrollspy', (e = new b(this, f))), 'string' == typeof c && e[c]();
+ });
+ }
+ (b.VERSION = '3.3.1'),
+ (b.DEFAULTS = { offset: 10 }),
+ (b.prototype.getScrollHeight = function () {
+ return (
+ this.$scrollElement[0].scrollHeight ||
+ Math.max(this.$body[0].scrollHeight, document.documentElement.scrollHeight)
+ );
+ }),
+ (b.prototype.refresh = function () {
+ var b = 'offset',
+ c = 0;
+ a.isWindow(this.$scrollElement[0]) || ((b = 'position'), (c = this.$scrollElement.scrollTop())),
+ (this.offsets = []),
+ (this.targets = []),
+ (this.scrollHeight = this.getScrollHeight());
+ var d = this;
+ this.$body
+ .find(this.selector)
+ .map(function () {
+ var d = a(this),
+ e = d.data('target') || d.attr('href'),
+ f = /^#./.test(e) && a(e);
+ return (f && f.length && f.is(':visible') && [[f[b]().top + c, e]]) || null;
+ })
+ .sort(function (a, b) {
+ return a[0] - b[0];
+ })
+ .each(function () {
+ d.offsets.push(this[0]), d.targets.push(this[1]);
+ });
+ }),
+ (b.prototype.process = function () {
+ var a,
+ b = this.$scrollElement.scrollTop() + this.options.offset,
+ c = this.getScrollHeight(),
+ d = this.options.offset + c - this.$scrollElement.height(),
+ e = this.offsets,
+ f = this.targets,
+ g = this.activeTarget;
+ if ((this.scrollHeight != c && this.refresh(), b >= d)) return g != (a = f[f.length - 1]) && this.activate(a);
+ if (g && b < e[0]) return (this.activeTarget = null), this.clear();
+ for (a = e.length; a--; ) g != f[a] && b >= e[a] && (!e[a + 1] || b <= e[a + 1]) && this.activate(f[a]);
+ }),
+ (b.prototype.activate = function (b) {
+ (this.activeTarget = b), this.clear();
+ var c = this.selector + '[data-target="' + b + '"],' + this.selector + '[href="' + b + '"]',
+ d = a(c).parents('li').addClass('active');
+ d.parent('.dropdown-menu').length && (d = d.closest('li.dropdown').addClass('active')),
+ d.trigger('activate.bs.scrollspy');
+ }),
+ (b.prototype.clear = function () {
+ a(this.selector).parentsUntil(this.options.target, '.active').removeClass('active');
+ });
+ var d = a.fn.scrollspy;
+ (a.fn.scrollspy = c),
+ (a.fn.scrollspy.Constructor = b),
+ (a.fn.scrollspy.noConflict = function () {
+ return (a.fn.scrollspy = d), this;
+ }),
+ a(window).on('load.bs.scrollspy.data-api', function () {
+ a('[data-spy="scroll"]').each(function () {
+ var b = a(this);
+ c.call(b, b.data());
+ });
+ });
+ })(jQuery),
+ +(function (a) {
+ 'use strict';
+ function b(b) {
+ return this.each(function () {
+ var d = a(this),
+ e = d.data('bs.tab');
+ e || d.data('bs.tab', (e = new c(this))), 'string' == typeof b && e[b]();
+ });
+ }
+ var c = function (b) {
+ this.element = a(b);
+ };
+ (c.VERSION = '3.3.1'),
+ (c.TRANSITION_DURATION = 150),
+ (c.prototype.show = function () {
+ var b = this.element,
+ c = b.closest('ul:not(.dropdown-menu)'),
+ d = b.data('target');
+ if (
+ (d || ((d = b.attr('href')), (d = d && d.replace(/.*(?=#[^\s]*$)/, ''))), !b.parent('li').hasClass('active'))
+ ) {
+ var e = c.find('.active:last a'),
+ f = a.Event('hide.bs.tab', { relatedTarget: b[0] }),
+ g = a.Event('show.bs.tab', { relatedTarget: e[0] });
+ if ((e.trigger(f), b.trigger(g), !g.isDefaultPrevented() && !f.isDefaultPrevented())) {
+ var h = a(d);
+ this.activate(b.closest('li'), c),
+ this.activate(h, h.parent(), function () {
+ e.trigger({ type: 'hidden.bs.tab', relatedTarget: b[0] }),
+ b.trigger({ type: 'shown.bs.tab', relatedTarget: e[0] });
+ });
+ }
+ }
+ }),
+ (c.prototype.activate = function (b, d, e) {
+ function f() {
+ g
+ .removeClass('active')
+ .find('> .dropdown-menu > .active')
+ .removeClass('active')
+ .end()
+ .find('[data-toggle="tab"]')
+ .attr('aria-expanded', !1),
+ b.addClass('active').find('[data-toggle="tab"]').attr('aria-expanded', !0),
+ h ? (b[0].offsetWidth, b.addClass('in')) : b.removeClass('fade'),
+ b.parent('.dropdown-menu') &&
+ b.closest('li.dropdown').addClass('active').end().find('[data-toggle="tab"]').attr('aria-expanded', !0),
+ e && e();
+ }
+ var g = d.find('> .active'),
+ h = e && a.support.transition && ((g.length && g.hasClass('fade')) || !!d.find('> .fade').length);
+ g.length && h ? g.one('bsTransitionEnd', f).emulateTransitionEnd(c.TRANSITION_DURATION) : f(),
+ g.removeClass('in');
+ });
+ var d = a.fn.tab;
+ (a.fn.tab = b),
+ (a.fn.tab.Constructor = c),
+ (a.fn.tab.noConflict = function () {
+ return (a.fn.tab = d), this;
+ });
+ var e = function (c) {
+ c.preventDefault(), b.call(a(this), 'show');
+ };
+ a(document)
+ .on('click.bs.tab.data-api', '[data-toggle="tab"]', e)
+ .on('click.bs.tab.data-api', '[data-toggle="pill"]', e);
+ })(jQuery),
+ +(function (a) {
+ 'use strict';
+ function b(b) {
+ return this.each(function () {
+ var d = a(this),
+ e = d.data('bs.affix'),
+ f = 'object' == typeof b && b;
+ e || d.data('bs.affix', (e = new c(this, f))), 'string' == typeof b && e[b]();
+ });
+ }
+ var c = function (b, d) {
+ (this.options = a.extend({}, c.DEFAULTS, d)),
+ (this.$target = a(this.options.target)
+ .on('scroll.bs.affix.data-api', a.proxy(this.checkPosition, this))
+ .on('click.bs.affix.data-api', a.proxy(this.checkPositionWithEventLoop, this))),
+ (this.$element = a(b)),
+ (this.affixed = this.unpin = this.pinnedOffset = null),
+ this.checkPosition();
+ };
+ (c.VERSION = '3.3.1'),
+ (c.RESET = 'affix affix-top affix-bottom'),
+ (c.DEFAULTS = { offset: 0, target: window }),
+ (c.prototype.getState = function (a, b, c, d) {
+ var e = this.$target.scrollTop(),
+ f = this.$element.offset(),
+ g = this.$target.height();
+ if (null != c && 'top' == this.affixed) return c > e ? 'top' : !1;
+ if ('bottom' == this.affixed)
+ return null != c ? (e + this.unpin <= f.top ? !1 : 'bottom') : a - d >= e + g ? !1 : 'bottom';
+ var h = null == this.affixed,
+ i = h ? e : f.top,
+ j = h ? g : b;
+ return null != c && c >= i ? 'top' : null != d && i + j >= a - d ? 'bottom' : !1;
+ }),
+ (c.prototype.getPinnedOffset = function () {
+ if (this.pinnedOffset) return this.pinnedOffset;
+ this.$element.removeClass(c.RESET).addClass('affix');
+ var a = this.$target.scrollTop(),
+ b = this.$element.offset();
+ return (this.pinnedOffset = b.top - a);
+ }),
+ (c.prototype.checkPositionWithEventLoop = function () {
+ setTimeout(a.proxy(this.checkPosition, this), 1);
+ }),
+ (c.prototype.checkPosition = function () {
+ if (this.$element.is(':visible')) {
+ var b = this.$element.height(),
+ d = this.options.offset,
+ e = d.top,
+ f = d.bottom,
+ g = a('body').height();
+ 'object' != typeof d && (f = e = d),
+ 'function' == typeof e && (e = d.top(this.$element)),
+ 'function' == typeof f && (f = d.bottom(this.$element));
+ var h = this.getState(g, b, e, f);
+ if (this.affixed != h) {
+ null != this.unpin && this.$element.css('top', '');
+ var i = 'affix' + (h ? '-' + h : ''),
+ j = a.Event(i + '.bs.affix');
+ if ((this.$element.trigger(j), j.isDefaultPrevented())) return;
+ (this.affixed = h),
+ (this.unpin = 'bottom' == h ? this.getPinnedOffset() : null),
+ this.$element
+ .removeClass(c.RESET)
+ .addClass(i)
+ .trigger(i.replace('affix', 'affixed') + '.bs.affix');
+ }
+ 'bottom' == h && this.$element.offset({ top: g - b - f });
+ }
+ });
+ var d = a.fn.affix;
+ (a.fn.affix = b),
+ (a.fn.affix.Constructor = c),
+ (a.fn.affix.noConflict = function () {
+ return (a.fn.affix = d), this;
+ }),
+ a(window).on('load', function () {
+ a('[data-spy="affix"]').each(function () {
+ var c = a(this),
+ d = c.data();
+ (d.offset = d.offset || {}),
+ null != d.offsetBottom && (d.offset.bottom = d.offsetBottom),
+ null != d.offsetTop && (d.offset.top = d.offsetTop),
+ b.call(c, d);
+ });
+ });
+ })(jQuery);
diff --git a/src/assets/js/jquery-2.1.3.min.js b/src/assets/js/jquery-2.1.3.min.js
new file mode 100644
index 0000000..6c27cc7
--- /dev/null
+++ b/src/assets/js/jquery-2.1.3.min.js
@@ -0,0 +1,4462 @@
+/*! jQuery v2.1.3 | (c) 2005, 2014 jQuery Foundation, Inc. | jquery.org/license */
+!(function (a, b) {
+ 'object' == typeof module && 'object' == typeof module.exports
+ ? (module.exports = a.document
+ ? b(a, !0)
+ : function (a) {
+ if (!a.document) throw new Error('jQuery requires a window with a document');
+ return b(a);
+ })
+ : b(a);
+})('undefined' != typeof window ? window : this, function (a, b) {
+ var c = [],
+ d = c.slice,
+ e = c.concat,
+ f = c.push,
+ g = c.indexOf,
+ h = {},
+ i = h.toString,
+ j = h.hasOwnProperty,
+ k = {},
+ l = a.document,
+ m = '2.1.3',
+ n = function (a, b) {
+ return new n.fn.init(a, b);
+ },
+ o = /^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,
+ p = /^-ms-/,
+ q = /-([\da-z])/gi,
+ r = function (a, b) {
+ return b.toUpperCase();
+ };
+ (n.fn = n.prototype =
+ {
+ jquery: m,
+ constructor: n,
+ selector: '',
+ length: 0,
+ toArray: function () {
+ return d.call(this);
+ },
+ get: function (a) {
+ return null != a ? (0 > a ? this[a + this.length] : this[a]) : d.call(this);
+ },
+ pushStack: function (a) {
+ var b = n.merge(this.constructor(), a);
+ return (b.prevObject = this), (b.context = this.context), b;
+ },
+ each: function (a, b) {
+ return n.each(this, a, b);
+ },
+ map: function (a) {
+ return this.pushStack(
+ n.map(this, function (b, c) {
+ return a.call(b, c, b);
+ }),
+ );
+ },
+ slice: function () {
+ return this.pushStack(d.apply(this, arguments));
+ },
+ first: function () {
+ return this.eq(0);
+ },
+ last: function () {
+ return this.eq(-1);
+ },
+ eq: function (a) {
+ var b = this.length,
+ c = +a + (0 > a ? b : 0);
+ return this.pushStack(c >= 0 && b > c ? [this[c]] : []);
+ },
+ end: function () {
+ return this.prevObject || this.constructor(null);
+ },
+ push: f,
+ sort: c.sort,
+ splice: c.splice,
+ }),
+ (n.extend = n.fn.extend =
+ function () {
+ var a,
+ b,
+ c,
+ d,
+ e,
+ f,
+ g = arguments[0] || {},
+ h = 1,
+ i = arguments.length,
+ j = !1;
+ for (
+ 'boolean' == typeof g && ((j = g), (g = arguments[h] || {}), h++),
+ 'object' == typeof g || n.isFunction(g) || (g = {}),
+ h === i && ((g = this), h--);
+ i > h;
+ h++
+ )
+ if (null != (a = arguments[h]))
+ for (b in a)
+ (c = g[b]),
+ (d = a[b]),
+ g !== d &&
+ (j && d && (n.isPlainObject(d) || (e = n.isArray(d)))
+ ? (e ? ((e = !1), (f = c && n.isArray(c) ? c : [])) : (f = c && n.isPlainObject(c) ? c : {}),
+ (g[b] = n.extend(j, f, d)))
+ : void 0 !== d && (g[b] = d));
+ return g;
+ }),
+ n.extend({
+ expando: 'jQuery' + (m + Math.random()).replace(/\D/g, ''),
+ isReady: !0,
+ error: function (a) {
+ throw new Error(a);
+ },
+ noop: function () {},
+ isFunction: function (a) {
+ return 'function' === n.type(a);
+ },
+ isArray: Array.isArray,
+ isWindow: function (a) {
+ return null != a && a === a.window;
+ },
+ isNumeric: function (a) {
+ return !n.isArray(a) && a - parseFloat(a) + 1 >= 0;
+ },
+ isPlainObject: function (a) {
+ return 'object' !== n.type(a) || a.nodeType || n.isWindow(a)
+ ? !1
+ : a.constructor && !j.call(a.constructor.prototype, 'isPrototypeOf')
+ ? !1
+ : !0;
+ },
+ isEmptyObject: function (a) {
+ var b;
+ for (b in a) return !1;
+ return !0;
+ },
+ type: function (a) {
+ return null == a
+ ? a + ''
+ : 'object' == typeof a || 'function' == typeof a
+ ? h[i.call(a)] || 'object'
+ : typeof a;
+ },
+ globalEval: function (a) {
+ var b,
+ c = eval;
+ (a = n.trim(a)),
+ a &&
+ (1 === a.indexOf('use strict')
+ ? ((b = l.createElement('script')), (b.text = a), l.head.appendChild(b).parentNode.removeChild(b))
+ : c(a));
+ },
+ camelCase: function (a) {
+ return a.replace(p, 'ms-').replace(q, r);
+ },
+ nodeName: function (a, b) {
+ return a.nodeName && a.nodeName.toLowerCase() === b.toLowerCase();
+ },
+ each: function (a, b, c) {
+ var d,
+ e = 0,
+ f = a.length,
+ g = s(a);
+ if (c) {
+ if (g) {
+ for (; f > e; e++) if (((d = b.apply(a[e], c)), d === !1)) break;
+ } else for (e in a) if (((d = b.apply(a[e], c)), d === !1)) break;
+ } else if (g) {
+ for (; f > e; e++) if (((d = b.call(a[e], e, a[e])), d === !1)) break;
+ } else for (e in a) if (((d = b.call(a[e], e, a[e])), d === !1)) break;
+ return a;
+ },
+ trim: function (a) {
+ return null == a ? '' : (a + '').replace(o, '');
+ },
+ makeArray: function (a, b) {
+ var c = b || [];
+ return null != a && (s(Object(a)) ? n.merge(c, 'string' == typeof a ? [a] : a) : f.call(c, a)), c;
+ },
+ inArray: function (a, b, c) {
+ return null == b ? -1 : g.call(b, a, c);
+ },
+ merge: function (a, b) {
+ for (var c = +b.length, d = 0, e = a.length; c > d; d++) a[e++] = b[d];
+ return (a.length = e), a;
+ },
+ grep: function (a, b, c) {
+ for (var d, e = [], f = 0, g = a.length, h = !c; g > f; f++) (d = !b(a[f], f)), d !== h && e.push(a[f]);
+ return e;
+ },
+ map: function (a, b, c) {
+ var d,
+ f = 0,
+ g = a.length,
+ h = s(a),
+ i = [];
+ if (h) for (; g > f; f++) (d = b(a[f], f, c)), null != d && i.push(d);
+ else for (f in a) (d = b(a[f], f, c)), null != d && i.push(d);
+ return e.apply([], i);
+ },
+ guid: 1,
+ proxy: function (a, b) {
+ var c, e, f;
+ return (
+ 'string' == typeof b && ((c = a[b]), (b = a), (a = c)),
+ n.isFunction(a)
+ ? ((e = d.call(arguments, 2)),
+ (f = function () {
+ return a.apply(b || this, e.concat(d.call(arguments)));
+ }),
+ (f.guid = a.guid = a.guid || n.guid++),
+ f)
+ : void 0
+ );
+ },
+ now: Date.now,
+ support: k,
+ }),
+ n.each('Boolean Number String Function Array Date RegExp Object Error'.split(' '), function (a, b) {
+ h['[object ' + b + ']'] = b.toLowerCase();
+ });
+ function s(a) {
+ var b = a.length,
+ c = n.type(a);
+ return 'function' === c || n.isWindow(a)
+ ? !1
+ : 1 === a.nodeType && b
+ ? !0
+ : 'array' === c || 0 === b || ('number' == typeof b && b > 0 && b - 1 in a);
+ }
+ var t = (function (a) {
+ var b,
+ c,
+ d,
+ e,
+ f,
+ g,
+ h,
+ i,
+ j,
+ k,
+ l,
+ m,
+ n,
+ o,
+ p,
+ q,
+ r,
+ s,
+ t,
+ u = 'sizzle' + 1 * new Date(),
+ v = a.document,
+ w = 0,
+ x = 0,
+ y = hb(),
+ z = hb(),
+ A = hb(),
+ B = function (a, b) {
+ return a === b && (l = !0), 0;
+ },
+ C = 1 << 31,
+ D = {}.hasOwnProperty,
+ E = [],
+ F = E.pop,
+ G = E.push,
+ H = E.push,
+ I = E.slice,
+ J = function (a, b) {
+ for (var c = 0, d = a.length; d > c; c++) if (a[c] === b) return c;
+ return -1;
+ },
+ K =
+ 'checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped',
+ L = '[\\x20\\t\\r\\n\\f]',
+ M = '(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+',
+ N = M.replace('w', 'w#'),
+ O =
+ '\\[' +
+ L +
+ '*(' +
+ M +
+ ')(?:' +
+ L +
+ '*([*^$|!~]?=)' +
+ L +
+ '*(?:\'((?:\\\\.|[^\\\\\'])*)\'|"((?:\\\\.|[^\\\\"])*)"|(' +
+ N +
+ '))|)' +
+ L +
+ '*\\]',
+ P =
+ ':(' +
+ M +
+ ')(?:\\(((\'((?:\\\\.|[^\\\\\'])*)\'|"((?:\\\\.|[^\\\\"])*)")|((?:\\\\.|[^\\\\()[\\]]|' +
+ O +
+ ')*)|.*)\\)|)',
+ Q = new RegExp(L + '+', 'g'),
+ R = new RegExp('^' + L + '+|((?:^|[^\\\\])(?:\\\\.)*)' + L + '+$', 'g'),
+ S = new RegExp('^' + L + '*,' + L + '*'),
+ T = new RegExp('^' + L + '*([>+~]|' + L + ')' + L + '*'),
+ U = new RegExp('=' + L + '*([^\\]\'"]*?)' + L + '*\\]', 'g'),
+ V = new RegExp(P),
+ W = new RegExp('^' + N + '$'),
+ X = {
+ ID: new RegExp('^#(' + M + ')'),
+ CLASS: new RegExp('^\\.(' + M + ')'),
+ TAG: new RegExp('^(' + M.replace('w', 'w*') + ')'),
+ ATTR: new RegExp('^' + O),
+ PSEUDO: new RegExp('^' + P),
+ CHILD: new RegExp(
+ '^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\(' +
+ L +
+ '*(even|odd|(([+-]|)(\\d*)n|)' +
+ L +
+ '*(?:([+-]|)' +
+ L +
+ '*(\\d+)|))' +
+ L +
+ '*\\)|)',
+ 'i',
+ ),
+ bool: new RegExp('^(?:' + K + ')$', 'i'),
+ needsContext: new RegExp(
+ '^' +
+ L +
+ '*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\(' +
+ L +
+ '*((?:-\\d)?\\d*)' +
+ L +
+ '*\\)|)(?=[^-]|$)',
+ 'i',
+ ),
+ },
+ Y = /^(?:input|select|textarea|button)$/i,
+ Z = /^h\d$/i,
+ $ = /^[^{]+\{\s*\[native \w/,
+ _ = /^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,
+ ab = /[+~]/,
+ bb = /'|\\/g,
+ cb = new RegExp('\\\\([\\da-f]{1,6}' + L + '?|(' + L + ')|.)', 'ig'),
+ db = function (a, b, c) {
+ var d = '0x' + b - 65536;
+ return d !== d || c
+ ? b
+ : 0 > d
+ ? String.fromCharCode(d + 65536)
+ : String.fromCharCode((d >> 10) | 55296, (1023 & d) | 56320);
+ },
+ eb = function () {
+ m();
+ };
+ try {
+ H.apply((E = I.call(v.childNodes)), v.childNodes), E[v.childNodes.length].nodeType;
+ } catch (fb) {
+ H = {
+ apply: E.length
+ ? function (a, b) {
+ G.apply(a, I.call(b));
+ }
+ : function (a, b) {
+ var c = a.length,
+ d = 0;
+ while ((a[c++] = b[d++]));
+ a.length = c - 1;
+ },
+ };
+ }
+ function gb(a, b, d, e) {
+ var f, h, j, k, l, o, r, s, w, x;
+ if (
+ ((b ? b.ownerDocument || b : v) !== n && m(b),
+ (b = b || n),
+ (d = d || []),
+ (k = b.nodeType),
+ 'string' != typeof a || !a || (1 !== k && 9 !== k && 11 !== k))
+ )
+ return d;
+ if (!e && p) {
+ if (11 !== k && (f = _.exec(a)))
+ if ((j = f[1])) {
+ if (9 === k) {
+ if (((h = b.getElementById(j)), !h || !h.parentNode)) return d;
+ if (h.id === j) return d.push(h), d;
+ } else if (b.ownerDocument && (h = b.ownerDocument.getElementById(j)) && t(b, h) && h.id === j)
+ return d.push(h), d;
+ } else {
+ if (f[2]) return H.apply(d, b.getElementsByTagName(a)), d;
+ if ((j = f[3]) && c.getElementsByClassName) return H.apply(d, b.getElementsByClassName(j)), d;
+ }
+ if (c.qsa && (!q || !q.test(a))) {
+ if (((s = r = u), (w = b), (x = 1 !== k && a), 1 === k && 'object' !== b.nodeName.toLowerCase())) {
+ (o = g(a)),
+ (r = b.getAttribute('id')) ? (s = r.replace(bb, '\\$&')) : b.setAttribute('id', s),
+ (s = "[id='" + s + "'] "),
+ (l = o.length);
+ while (l--) o[l] = s + rb(o[l]);
+ (w = (ab.test(a) && pb(b.parentNode)) || b), (x = o.join(','));
+ }
+ if (x)
+ try {
+ return H.apply(d, w.querySelectorAll(x)), d;
+ } catch (y) {
+ } finally {
+ r || b.removeAttribute('id');
+ }
+ }
+ }
+ return i(a.replace(R, '$1'), b, d, e);
+ }
+ function hb() {
+ var a = [];
+ function b(c, e) {
+ return a.push(c + ' ') > d.cacheLength && delete b[a.shift()], (b[c + ' '] = e);
+ }
+ return b;
+ }
+ function ib(a) {
+ return (a[u] = !0), a;
+ }
+ function jb(a) {
+ var b = n.createElement('div');
+ try {
+ return !!a(b);
+ } catch (c) {
+ return !1;
+ } finally {
+ b.parentNode && b.parentNode.removeChild(b), (b = null);
+ }
+ }
+ function kb(a, b) {
+ var c = a.split('|'),
+ e = a.length;
+ while (e--) d.attrHandle[c[e]] = b;
+ }
+ function lb(a, b) {
+ var c = b && a,
+ d = c && 1 === a.nodeType && 1 === b.nodeType && (~b.sourceIndex || C) - (~a.sourceIndex || C);
+ if (d) return d;
+ if (c) while ((c = c.nextSibling)) if (c === b) return -1;
+ return a ? 1 : -1;
+ }
+ function mb(a) {
+ return function (b) {
+ var c = b.nodeName.toLowerCase();
+ return 'input' === c && b.type === a;
+ };
+ }
+ function nb(a) {
+ return function (b) {
+ var c = b.nodeName.toLowerCase();
+ return ('input' === c || 'button' === c) && b.type === a;
+ };
+ }
+ function ob(a) {
+ return ib(function (b) {
+ return (
+ (b = +b),
+ ib(function (c, d) {
+ var e,
+ f = a([], c.length, b),
+ g = f.length;
+ while (g--) c[(e = f[g])] && (c[e] = !(d[e] = c[e]));
+ })
+ );
+ });
+ }
+ function pb(a) {
+ return a && 'undefined' != typeof a.getElementsByTagName && a;
+ }
+ (c = gb.support = {}),
+ (f = gb.isXML =
+ function (a) {
+ var b = a && (a.ownerDocument || a).documentElement;
+ return b ? 'HTML' !== b.nodeName : !1;
+ }),
+ (m = gb.setDocument =
+ function (a) {
+ var b,
+ e,
+ g = a ? a.ownerDocument || a : v;
+ return g !== n && 9 === g.nodeType && g.documentElement
+ ? ((n = g),
+ (o = g.documentElement),
+ (e = g.defaultView),
+ e &&
+ e !== e.top &&
+ (e.addEventListener
+ ? e.addEventListener('unload', eb, !1)
+ : e.attachEvent && e.attachEvent('onunload', eb)),
+ (p = !f(g)),
+ (c.attributes = jb(function (a) {
+ return (a.className = 'i'), !a.getAttribute('className');
+ })),
+ (c.getElementsByTagName = jb(function (a) {
+ return a.appendChild(g.createComment('')), !a.getElementsByTagName('*').length;
+ })),
+ (c.getElementsByClassName = $.test(g.getElementsByClassName)),
+ (c.getById = jb(function (a) {
+ return (o.appendChild(a).id = u), !g.getElementsByName || !g.getElementsByName(u).length;
+ })),
+ c.getById
+ ? ((d.find.ID = function (a, b) {
+ if ('undefined' != typeof b.getElementById && p) {
+ var c = b.getElementById(a);
+ return c && c.parentNode ? [c] : [];
+ }
+ }),
+ (d.filter.ID = function (a) {
+ var b = a.replace(cb, db);
+ return function (a) {
+ return a.getAttribute('id') === b;
+ };
+ }))
+ : (delete d.find.ID,
+ (d.filter.ID = function (a) {
+ var b = a.replace(cb, db);
+ return function (a) {
+ var c = 'undefined' != typeof a.getAttributeNode && a.getAttributeNode('id');
+ return c && c.value === b;
+ };
+ })),
+ (d.find.TAG = c.getElementsByTagName
+ ? function (a, b) {
+ return 'undefined' != typeof b.getElementsByTagName
+ ? b.getElementsByTagName(a)
+ : c.qsa
+ ? b.querySelectorAll(a)
+ : void 0;
+ }
+ : function (a, b) {
+ var c,
+ d = [],
+ e = 0,
+ f = b.getElementsByTagName(a);
+ if ('*' === a) {
+ while ((c = f[e++])) 1 === c.nodeType && d.push(c);
+ return d;
+ }
+ return f;
+ }),
+ (d.find.CLASS =
+ c.getElementsByClassName &&
+ function (a, b) {
+ return p ? b.getElementsByClassName(a) : void 0;
+ }),
+ (r = []),
+ (q = []),
+ (c.qsa = $.test(g.querySelectorAll)) &&
+ (jb(function (a) {
+ (o.appendChild(a).innerHTML =
+ "<a id='" +
+ u +
+ "'></a><select id='" +
+ u +
+ "-\f]' msallowcapture=''><option selected=''></option></select>"),
+ a.querySelectorAll("[msallowcapture^='']").length && q.push('[*^$]=' + L + '*(?:\'\'|"")'),
+ a.querySelectorAll('[selected]').length || q.push('\\[' + L + '*(?:value|' + K + ')'),
+ a.querySelectorAll('[id~=' + u + '-]').length || q.push('~='),
+ a.querySelectorAll(':checked').length || q.push(':checked'),
+ a.querySelectorAll('a#' + u + '+*').length || q.push('.#.+[+~]');
+ }),
+ jb(function (a) {
+ var b = g.createElement('input');
+ b.setAttribute('type', 'hidden'),
+ a.appendChild(b).setAttribute('name', 'D'),
+ a.querySelectorAll('[name=d]').length && q.push('name' + L + '*[*^$|!~]?='),
+ a.querySelectorAll(':enabled').length || q.push(':enabled', ':disabled'),
+ a.querySelectorAll('*,:x'),
+ q.push(',.*:');
+ })),
+ (c.matchesSelector = $.test(
+ (s =
+ o.matches ||
+ o.webkitMatchesSelector ||
+ o.mozMatchesSelector ||
+ o.oMatchesSelector ||
+ o.msMatchesSelector),
+ )) &&
+ jb(function (a) {
+ (c.disconnectedMatch = s.call(a, 'div')), s.call(a, "[s!='']:x"), r.push('!=', P);
+ }),
+ (q = q.length && new RegExp(q.join('|'))),
+ (r = r.length && new RegExp(r.join('|'))),
+ (b = $.test(o.compareDocumentPosition)),
+ (t =
+ b || $.test(o.contains)
+ ? function (a, b) {
+ var c = 9 === a.nodeType ? a.documentElement : a,
+ d = b && b.parentNode;
+ return (
+ a === d ||
+ !(
+ !d ||
+ 1 !== d.nodeType ||
+ !(c.contains ? c.contains(d) : a.compareDocumentPosition && 16 & a.compareDocumentPosition(d))
+ )
+ );
+ }
+ : function (a, b) {
+ if (b) while ((b = b.parentNode)) if (b === a) return !0;
+ return !1;
+ }),
+ (B = b
+ ? function (a, b) {
+ if (a === b) return (l = !0), 0;
+ var d = !a.compareDocumentPosition - !b.compareDocumentPosition;
+ return d
+ ? d
+ : ((d = (a.ownerDocument || a) === (b.ownerDocument || b) ? a.compareDocumentPosition(b) : 1),
+ 1 & d || (!c.sortDetached && b.compareDocumentPosition(a) === d)
+ ? a === g || (a.ownerDocument === v && t(v, a))
+ ? -1
+ : b === g || (b.ownerDocument === v && t(v, b))
+ ? 1
+ : k
+ ? J(k, a) - J(k, b)
+ : 0
+ : 4 & d
+ ? -1
+ : 1);
+ }
+ : function (a, b) {
+ if (a === b) return (l = !0), 0;
+ var c,
+ d = 0,
+ e = a.parentNode,
+ f = b.parentNode,
+ h = [a],
+ i = [b];
+ if (!e || !f) return a === g ? -1 : b === g ? 1 : e ? -1 : f ? 1 : k ? J(k, a) - J(k, b) : 0;
+ if (e === f) return lb(a, b);
+ c = a;
+ while ((c = c.parentNode)) h.unshift(c);
+ c = b;
+ while ((c = c.parentNode)) i.unshift(c);
+ while (h[d] === i[d]) d++;
+ return d ? lb(h[d], i[d]) : h[d] === v ? -1 : i[d] === v ? 1 : 0;
+ }),
+ g)
+ : n;
+ }),
+ (gb.matches = function (a, b) {
+ return gb(a, null, null, b);
+ }),
+ (gb.matchesSelector = function (a, b) {
+ if (
+ ((a.ownerDocument || a) !== n && m(a),
+ (b = b.replace(U, "='$1']")),
+ !(!c.matchesSelector || !p || (r && r.test(b)) || (q && q.test(b))))
+ )
+ try {
+ var d = s.call(a, b);
+ if (d || c.disconnectedMatch || (a.document && 11 !== a.document.nodeType)) return d;
+ } catch (e) {}
+ return gb(b, n, null, [a]).length > 0;
+ }),
+ (gb.contains = function (a, b) {
+ return (a.ownerDocument || a) !== n && m(a), t(a, b);
+ }),
+ (gb.attr = function (a, b) {
+ (a.ownerDocument || a) !== n && m(a);
+ var e = d.attrHandle[b.toLowerCase()],
+ f = e && D.call(d.attrHandle, b.toLowerCase()) ? e(a, b, !p) : void 0;
+ return void 0 !== f
+ ? f
+ : c.attributes || !p
+ ? a.getAttribute(b)
+ : (f = a.getAttributeNode(b)) && f.specified
+ ? f.value
+ : null;
+ }),
+ (gb.error = function (a) {
+ throw new Error('Syntax error, unrecognized expression: ' + a);
+ }),
+ (gb.uniqueSort = function (a) {
+ var b,
+ d = [],
+ e = 0,
+ f = 0;
+ if (((l = !c.detectDuplicates), (k = !c.sortStable && a.slice(0)), a.sort(B), l)) {
+ while ((b = a[f++])) b === a[f] && (e = d.push(f));
+ while (e--) a.splice(d[e], 1);
+ }
+ return (k = null), a;
+ }),
+ (e = gb.getText =
+ function (a) {
+ var b,
+ c = '',
+ d = 0,
+ f = a.nodeType;
+ if (f) {
+ if (1 === f || 9 === f || 11 === f) {
+ if ('string' == typeof a.textContent) return a.textContent;
+ for (a = a.firstChild; a; a = a.nextSibling) c += e(a);
+ } else if (3 === f || 4 === f) return a.nodeValue;
+ } else while ((b = a[d++])) c += e(b);
+ return c;
+ }),
+ (d = gb.selectors =
+ {
+ cacheLength: 50,
+ createPseudo: ib,
+ match: X,
+ attrHandle: {},
+ find: {},
+ relative: {
+ '>': { dir: 'parentNode', first: !0 },
+ ' ': { dir: 'parentNode' },
+ '+': { dir: 'previousSibling', first: !0 },
+ '~': { dir: 'previousSibling' },
+ },
+ preFilter: {
+ ATTR: function (a) {
+ return (
+ (a[1] = a[1].replace(cb, db)),
+ (a[3] = (a[3] || a[4] || a[5] || '').replace(cb, db)),
+ '~=' === a[2] && (a[3] = ' ' + a[3] + ' '),
+ a.slice(0, 4)
+ );
+ },
+ CHILD: function (a) {
+ return (
+ (a[1] = a[1].toLowerCase()),
+ 'nth' === a[1].slice(0, 3)
+ ? (a[3] || gb.error(a[0]),
+ (a[4] = +(a[4] ? a[5] + (a[6] || 1) : 2 * ('even' === a[3] || 'odd' === a[3]))),
+ (a[5] = +(a[7] + a[8] || 'odd' === a[3])))
+ : a[3] && gb.error(a[0]),
+ a
+ );
+ },
+ PSEUDO: function (a) {
+ var b,
+ c = !a[6] && a[2];
+ return X.CHILD.test(a[0])
+ ? null
+ : (a[3]
+ ? (a[2] = a[4] || a[5] || '')
+ : c &&
+ V.test(c) &&
+ (b = g(c, !0)) &&
+ (b = c.indexOf(')', c.length - b) - c.length) &&
+ ((a[0] = a[0].slice(0, b)), (a[2] = c.slice(0, b))),
+ a.slice(0, 3));
+ },
+ },
+ filter: {
+ TAG: function (a) {
+ var b = a.replace(cb, db).toLowerCase();
+ return '*' === a
+ ? function () {
+ return !0;
+ }
+ : function (a) {
+ return a.nodeName && a.nodeName.toLowerCase() === b;
+ };
+ },
+ CLASS: function (a) {
+ var b = y[a + ' '];
+ return (
+ b ||
+ ((b = new RegExp('(^|' + L + ')' + a + '(' + L + '|$)')) &&
+ y(a, function (a) {
+ return b.test(
+ ('string' == typeof a.className && a.className) ||
+ ('undefined' != typeof a.getAttribute && a.getAttribute('class')) ||
+ '',
+ );
+ }))
+ );
+ },
+ ATTR: function (a, b, c) {
+ return function (d) {
+ var e = gb.attr(d, a);
+ return null == e
+ ? '!=' === b
+ : b
+ ? ((e += ''),
+ '=' === b
+ ? e === c
+ : '!=' === b
+ ? e !== c
+ : '^=' === b
+ ? c && 0 === e.indexOf(c)
+ : '*=' === b
+ ? c && e.indexOf(c) > -1
+ : '$=' === b
+ ? c && e.slice(-c.length) === c
+ : '~=' === b
+ ? (' ' + e.replace(Q, ' ') + ' ').indexOf(c) > -1
+ : '|=' === b
+ ? e === c || e.slice(0, c.length + 1) === c + '-'
+ : !1)
+ : !0;
+ };
+ },
+ CHILD: function (a, b, c, d, e) {
+ var f = 'nth' !== a.slice(0, 3),
+ g = 'last' !== a.slice(-4),
+ h = 'of-type' === b;
+ return 1 === d && 0 === e
+ ? function (a) {
+ return !!a.parentNode;
+ }
+ : function (b, c, i) {
+ var j,
+ k,
+ l,
+ m,
+ n,
+ o,
+ p = f !== g ? 'nextSibling' : 'previousSibling',
+ q = b.parentNode,
+ r = h && b.nodeName.toLowerCase(),
+ s = !i && !h;
+ if (q) {
+ if (f) {
+ while (p) {
+ l = b;
+ while ((l = l[p])) if (h ? l.nodeName.toLowerCase() === r : 1 === l.nodeType) return !1;
+ o = p = 'only' === a && !o && 'nextSibling';
+ }
+ return !0;
+ }
+ if (((o = [g ? q.firstChild : q.lastChild]), g && s)) {
+ (k = q[u] || (q[u] = {})),
+ (j = k[a] || []),
+ (n = j[0] === w && j[1]),
+ (m = j[0] === w && j[2]),
+ (l = n && q.childNodes[n]);
+ while ((l = (++n && l && l[p]) || (m = n = 0) || o.pop()))
+ if (1 === l.nodeType && ++m && l === b) {
+ k[a] = [w, n, m];
+ break;
+ }
+ } else if (s && (j = (b[u] || (b[u] = {}))[a]) && j[0] === w) m = j[1];
+ else
+ while ((l = (++n && l && l[p]) || (m = n = 0) || o.pop()))
+ if (
+ (h ? l.nodeName.toLowerCase() === r : 1 === l.nodeType) &&
+ ++m &&
+ (s && ((l[u] || (l[u] = {}))[a] = [w, m]), l === b)
+ )
+ break;
+ return (m -= e), m === d || (m % d === 0 && m / d >= 0);
+ }
+ };
+ },
+ PSEUDO: function (a, b) {
+ var c,
+ e = d.pseudos[a] || d.setFilters[a.toLowerCase()] || gb.error('unsupported pseudo: ' + a);
+ return e[u]
+ ? e(b)
+ : e.length > 1
+ ? ((c = [a, a, '', b]),
+ d.setFilters.hasOwnProperty(a.toLowerCase())
+ ? ib(function (a, c) {
+ var d,
+ f = e(a, b),
+ g = f.length;
+ while (g--) (d = J(a, f[g])), (a[d] = !(c[d] = f[g]));
+ })
+ : function (a) {
+ return e(a, 0, c);
+ })
+ : e;
+ },
+ },
+ pseudos: {
+ not: ib(function (a) {
+ var b = [],
+ c = [],
+ d = h(a.replace(R, '$1'));
+ return d[u]
+ ? ib(function (a, b, c, e) {
+ var f,
+ g = d(a, null, e, []),
+ h = a.length;
+ while (h--) (f = g[h]) && (a[h] = !(b[h] = f));
+ })
+ : function (a, e, f) {
+ return (b[0] = a), d(b, null, f, c), (b[0] = null), !c.pop();
+ };
+ }),
+ has: ib(function (a) {
+ return function (b) {
+ return gb(a, b).length > 0;
+ };
+ }),
+ contains: ib(function (a) {
+ return (
+ (a = a.replace(cb, db)),
+ function (b) {
+ return (b.textContent || b.innerText || e(b)).indexOf(a) > -1;
+ }
+ );
+ }),
+ lang: ib(function (a) {
+ return (
+ W.test(a || '') || gb.error('unsupported lang: ' + a),
+ (a = a.replace(cb, db).toLowerCase()),
+ function (b) {
+ var c;
+ do
+ if ((c = p ? b.lang : b.getAttribute('xml:lang') || b.getAttribute('lang')))
+ return (c = c.toLowerCase()), c === a || 0 === c.indexOf(a + '-');
+ while ((b = b.parentNode) && 1 === b.nodeType);
+ return !1;
+ }
+ );
+ }),
+ target: function (b) {
+ var c = a.location && a.location.hash;
+ return c && c.slice(1) === b.id;
+ },
+ root: function (a) {
+ return a === o;
+ },
+ focus: function (a) {
+ return a === n.activeElement && (!n.hasFocus || n.hasFocus()) && !!(a.type || a.href || ~a.tabIndex);
+ },
+ enabled: function (a) {
+ return a.disabled === !1;
+ },
+ disabled: function (a) {
+ return a.disabled === !0;
+ },
+ checked: function (a) {
+ var b = a.nodeName.toLowerCase();
+ return ('input' === b && !!a.checked) || ('option' === b && !!a.selected);
+ },
+ selected: function (a) {
+ return a.parentNode && a.parentNode.selectedIndex, a.selected === !0;
+ },
+ empty: function (a) {
+ for (a = a.firstChild; a; a = a.nextSibling) if (a.nodeType < 6) return !1;
+ return !0;
+ },
+ parent: function (a) {
+ return !d.pseudos.empty(a);
+ },
+ header: function (a) {
+ return Z.test(a.nodeName);
+ },
+ input: function (a) {
+ return Y.test(a.nodeName);
+ },
+ button: function (a) {
+ var b = a.nodeName.toLowerCase();
+ return ('input' === b && 'button' === a.type) || 'button' === b;
+ },
+ text: function (a) {
+ var b;
+ return (
+ 'input' === a.nodeName.toLowerCase() &&
+ 'text' === a.type &&
+ (null == (b = a.getAttribute('type')) || 'text' === b.toLowerCase())
+ );
+ },
+ first: ob(function () {
+ return [0];
+ }),
+ last: ob(function (a, b) {
+ return [b - 1];
+ }),
+ eq: ob(function (a, b, c) {
+ return [0 > c ? c + b : c];
+ }),
+ even: ob(function (a, b) {
+ for (var c = 0; b > c; c += 2) a.push(c);
+ return a;
+ }),
+ odd: ob(function (a, b) {
+ for (var c = 1; b > c; c += 2) a.push(c);
+ return a;
+ }),
+ lt: ob(function (a, b, c) {
+ for (var d = 0 > c ? c + b : c; --d >= 0; ) a.push(d);
+ return a;
+ }),
+ gt: ob(function (a, b, c) {
+ for (var d = 0 > c ? c + b : c; ++d < b; ) a.push(d);
+ return a;
+ }),
+ },
+ }),
+ (d.pseudos.nth = d.pseudos.eq);
+ for (b in { radio: !0, checkbox: !0, file: !0, password: !0, image: !0 }) d.pseudos[b] = mb(b);
+ for (b in { submit: !0, reset: !0 }) d.pseudos[b] = nb(b);
+ function qb() {}
+ (qb.prototype = d.filters = d.pseudos),
+ (d.setFilters = new qb()),
+ (g = gb.tokenize =
+ function (a, b) {
+ var c,
+ e,
+ f,
+ g,
+ h,
+ i,
+ j,
+ k = z[a + ' '];
+ if (k) return b ? 0 : k.slice(0);
+ (h = a), (i = []), (j = d.preFilter);
+ while (h) {
+ (!c || (e = S.exec(h))) && (e && (h = h.slice(e[0].length) || h), i.push((f = []))),
+ (c = !1),
+ (e = T.exec(h)) &&
+ ((c = e.shift()), f.push({ value: c, type: e[0].replace(R, ' ') }), (h = h.slice(c.length)));
+ for (g in d.filter)
+ !(e = X[g].exec(h)) ||
+ (j[g] && !(e = j[g](e))) ||
+ ((c = e.shift()), f.push({ value: c, type: g, matches: e }), (h = h.slice(c.length)));
+ if (!c) break;
+ }
+ return b ? h.length : h ? gb.error(a) : z(a, i).slice(0);
+ });
+ function rb(a) {
+ for (var b = 0, c = a.length, d = ''; c > b; b++) d += a[b].value;
+ return d;
+ }
+ function sb(a, b, c) {
+ var d = b.dir,
+ e = c && 'parentNode' === d,
+ f = x++;
+ return b.first
+ ? function (b, c, f) {
+ while ((b = b[d])) if (1 === b.nodeType || e) return a(b, c, f);
+ }
+ : function (b, c, g) {
+ var h,
+ i,
+ j = [w, f];
+ if (g) {
+ while ((b = b[d])) if ((1 === b.nodeType || e) && a(b, c, g)) return !0;
+ } else
+ while ((b = b[d]))
+ if (1 === b.nodeType || e) {
+ if (((i = b[u] || (b[u] = {})), (h = i[d]) && h[0] === w && h[1] === f)) return (j[2] = h[2]);
+ if (((i[d] = j), (j[2] = a(b, c, g)))) return !0;
+ }
+ };
+ }
+ function tb(a) {
+ return a.length > 1
+ ? function (b, c, d) {
+ var e = a.length;
+ while (e--) if (!a[e](b, c, d)) return !1;
+ return !0;
+ }
+ : a[0];
+ }
+ function ub(a, b, c) {
+ for (var d = 0, e = b.length; e > d; d++) gb(a, b[d], c);
+ return c;
+ }
+ function vb(a, b, c, d, e) {
+ for (var f, g = [], h = 0, i = a.length, j = null != b; i > h; h++)
+ (f = a[h]) && (!c || c(f, d, e)) && (g.push(f), j && b.push(h));
+ return g;
+ }
+ function wb(a, b, c, d, e, f) {
+ return (
+ d && !d[u] && (d = wb(d)),
+ e && !e[u] && (e = wb(e, f)),
+ ib(function (f, g, h, i) {
+ var j,
+ k,
+ l,
+ m = [],
+ n = [],
+ o = g.length,
+ p = f || ub(b || '*', h.nodeType ? [h] : h, []),
+ q = !a || (!f && b) ? p : vb(p, m, a, h, i),
+ r = c ? (e || (f ? a : o || d) ? [] : g) : q;
+ if ((c && c(q, r, h, i), d)) {
+ (j = vb(r, n)), d(j, [], h, i), (k = j.length);
+ while (k--) (l = j[k]) && (r[n[k]] = !(q[n[k]] = l));
+ }
+ if (f) {
+ if (e || a) {
+ if (e) {
+ (j = []), (k = r.length);
+ while (k--) (l = r[k]) && j.push((q[k] = l));
+ e(null, (r = []), j, i);
+ }
+ k = r.length;
+ while (k--) (l = r[k]) && (j = e ? J(f, l) : m[k]) > -1 && (f[j] = !(g[j] = l));
+ }
+ } else (r = vb(r === g ? r.splice(o, r.length) : r)), e ? e(null, g, r, i) : H.apply(g, r);
+ })
+ );
+ }
+ function xb(a) {
+ for (
+ var b,
+ c,
+ e,
+ f = a.length,
+ g = d.relative[a[0].type],
+ h = g || d.relative[' '],
+ i = g ? 1 : 0,
+ k = sb(
+ function (a) {
+ return a === b;
+ },
+ h,
+ !0,
+ ),
+ l = sb(
+ function (a) {
+ return J(b, a) > -1;
+ },
+ h,
+ !0,
+ ),
+ m = [
+ function (a, c, d) {
+ var e = (!g && (d || c !== j)) || ((b = c).nodeType ? k(a, c, d) : l(a, c, d));
+ return (b = null), e;
+ },
+ ];
+ f > i;
+ i++
+ )
+ if ((c = d.relative[a[i].type])) m = [sb(tb(m), c)];
+ else {
+ if (((c = d.filter[a[i].type].apply(null, a[i].matches)), c[u])) {
+ for (e = ++i; f > e; e++) if (d.relative[a[e].type]) break;
+ return wb(
+ i > 1 && tb(m),
+ i > 1 && rb(a.slice(0, i - 1).concat({ value: ' ' === a[i - 2].type ? '*' : '' })).replace(R, '$1'),
+ c,
+ e > i && xb(a.slice(i, e)),
+ f > e && xb((a = a.slice(e))),
+ f > e && rb(a),
+ );
+ }
+ m.push(c);
+ }
+ return tb(m);
+ }
+ function yb(a, b) {
+ var c = b.length > 0,
+ e = a.length > 0,
+ f = function (f, g, h, i, k) {
+ var l,
+ m,
+ o,
+ p = 0,
+ q = '0',
+ r = f && [],
+ s = [],
+ t = j,
+ u = f || (e && d.find.TAG('*', k)),
+ v = (w += null == t ? 1 : Math.random() || 0.1),
+ x = u.length;
+ for (k && (j = g !== n && g); q !== x && null != (l = u[q]); q++) {
+ if (e && l) {
+ m = 0;
+ while ((o = a[m++]))
+ if (o(l, g, h)) {
+ i.push(l);
+ break;
+ }
+ k && (w = v);
+ }
+ c && ((l = !o && l) && p--, f && r.push(l));
+ }
+ if (((p += q), c && q !== p)) {
+ m = 0;
+ while ((o = b[m++])) o(r, s, g, h);
+ if (f) {
+ if (p > 0) while (q--) r[q] || s[q] || (s[q] = F.call(i));
+ s = vb(s);
+ }
+ H.apply(i, s), k && !f && s.length > 0 && p + b.length > 1 && gb.uniqueSort(i);
+ }
+ return k && ((w = v), (j = t)), r;
+ };
+ return c ? ib(f) : f;
+ }
+ return (
+ (h = gb.compile =
+ function (a, b) {
+ var c,
+ d = [],
+ e = [],
+ f = A[a + ' '];
+ if (!f) {
+ b || (b = g(a)), (c = b.length);
+ while (c--) (f = xb(b[c])), f[u] ? d.push(f) : e.push(f);
+ (f = A(a, yb(e, d))), (f.selector = a);
+ }
+ return f;
+ }),
+ (i = gb.select =
+ function (a, b, e, f) {
+ var i,
+ j,
+ k,
+ l,
+ m,
+ n = 'function' == typeof a && a,
+ o = !f && g((a = n.selector || a));
+ if (((e = e || []), 1 === o.length)) {
+ if (
+ ((j = o[0] = o[0].slice(0)),
+ j.length > 2 && 'ID' === (k = j[0]).type && c.getById && 9 === b.nodeType && p && d.relative[j[1].type])
+ ) {
+ if (((b = (d.find.ID(k.matches[0].replace(cb, db), b) || [])[0]), !b)) return e;
+ n && (b = b.parentNode), (a = a.slice(j.shift().value.length));
+ }
+ i = X.needsContext.test(a) ? 0 : j.length;
+ while (i--) {
+ if (((k = j[i]), d.relative[(l = k.type)])) break;
+ if (
+ (m = d.find[l]) &&
+ (f = m(k.matches[0].replace(cb, db), (ab.test(j[0].type) && pb(b.parentNode)) || b))
+ ) {
+ if ((j.splice(i, 1), (a = f.length && rb(j)), !a)) return H.apply(e, f), e;
+ break;
+ }
+ }
+ }
+ return (n || h(a, o))(f, b, !p, e, (ab.test(a) && pb(b.parentNode)) || b), e;
+ }),
+ (c.sortStable = u.split('').sort(B).join('') === u),
+ (c.detectDuplicates = !!l),
+ m(),
+ (c.sortDetached = jb(function (a) {
+ return 1 & a.compareDocumentPosition(n.createElement('div'));
+ })),
+ jb(function (a) {
+ return (a.innerHTML = "<a href='#'></a>"), '#' === a.firstChild.getAttribute('href');
+ }) ||
+ kb('type|href|height|width', function (a, b, c) {
+ return c ? void 0 : a.getAttribute(b, 'type' === b.toLowerCase() ? 1 : 2);
+ }),
+ (c.attributes &&
+ jb(function (a) {
+ return (
+ (a.innerHTML = '<input/>'),
+ a.firstChild.setAttribute('value', ''),
+ '' === a.firstChild.getAttribute('value')
+ );
+ })) ||
+ kb('value', function (a, b, c) {
+ return c || 'input' !== a.nodeName.toLowerCase() ? void 0 : a.defaultValue;
+ }),
+ jb(function (a) {
+ return null == a.getAttribute('disabled');
+ }) ||
+ kb(K, function (a, b, c) {
+ var d;
+ return c
+ ? void 0
+ : a[b] === !0
+ ? b.toLowerCase()
+ : (d = a.getAttributeNode(b)) && d.specified
+ ? d.value
+ : null;
+ }),
+ gb
+ );
+ })(a);
+ (n.find = t),
+ (n.expr = t.selectors),
+ (n.expr[':'] = n.expr.pseudos),
+ (n.unique = t.uniqueSort),
+ (n.text = t.getText),
+ (n.isXMLDoc = t.isXML),
+ (n.contains = t.contains);
+ var u = n.expr.match.needsContext,
+ v = /^<(\w+)\s*\/?>(?:<\/\1>|)$/,
+ w = /^.[^:#\[\.,]*$/;
+ function x(a, b, c) {
+ if (n.isFunction(b))
+ return n.grep(a, function (a, d) {
+ return !!b.call(a, d, a) !== c;
+ });
+ if (b.nodeType)
+ return n.grep(a, function (a) {
+ return (a === b) !== c;
+ });
+ if ('string' == typeof b) {
+ if (w.test(b)) return n.filter(b, a, c);
+ b = n.filter(b, a);
+ }
+ return n.grep(a, function (a) {
+ return g.call(b, a) >= 0 !== c;
+ });
+ }
+ (n.filter = function (a, b, c) {
+ var d = b[0];
+ return (
+ c && (a = ':not(' + a + ')'),
+ 1 === b.length && 1 === d.nodeType
+ ? n.find.matchesSelector(d, a)
+ ? [d]
+ : []
+ : n.find.matches(
+ a,
+ n.grep(b, function (a) {
+ return 1 === a.nodeType;
+ }),
+ )
+ );
+ }),
+ n.fn.extend({
+ find: function (a) {
+ var b,
+ c = this.length,
+ d = [],
+ e = this;
+ if ('string' != typeof a)
+ return this.pushStack(
+ n(a).filter(function () {
+ for (b = 0; c > b; b++) if (n.contains(e[b], this)) return !0;
+ }),
+ );
+ for (b = 0; c > b; b++) n.find(a, e[b], d);
+ return (
+ (d = this.pushStack(c > 1 ? n.unique(d) : d)), (d.selector = this.selector ? this.selector + ' ' + a : a), d
+ );
+ },
+ filter: function (a) {
+ return this.pushStack(x(this, a || [], !1));
+ },
+ not: function (a) {
+ return this.pushStack(x(this, a || [], !0));
+ },
+ is: function (a) {
+ return !!x(this, 'string' == typeof a && u.test(a) ? n(a) : a || [], !1).length;
+ },
+ });
+ var y,
+ z = /^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,
+ A = (n.fn.init = function (a, b) {
+ var c, d;
+ if (!a) return this;
+ if ('string' == typeof a) {
+ if (
+ ((c = '<' === a[0] && '>' === a[a.length - 1] && a.length >= 3 ? [null, a, null] : z.exec(a)),
+ !c || (!c[1] && b))
+ )
+ return !b || b.jquery ? (b || y).find(a) : this.constructor(b).find(a);
+ if (c[1]) {
+ if (
+ ((b = b instanceof n ? b[0] : b),
+ n.merge(this, n.parseHTML(c[1], b && b.nodeType ? b.ownerDocument || b : l, !0)),
+ v.test(c[1]) && n.isPlainObject(b))
+ )
+ for (c in b) n.isFunction(this[c]) ? this[c](b[c]) : this.attr(c, b[c]);
+ return this;
+ }
+ return (
+ (d = l.getElementById(c[2])),
+ d && d.parentNode && ((this.length = 1), (this[0] = d)),
+ (this.context = l),
+ (this.selector = a),
+ this
+ );
+ }
+ return a.nodeType
+ ? ((this.context = this[0] = a), (this.length = 1), this)
+ : n.isFunction(a)
+ ? 'undefined' != typeof y.ready
+ ? y.ready(a)
+ : a(n)
+ : (void 0 !== a.selector && ((this.selector = a.selector), (this.context = a.context)), n.makeArray(a, this));
+ });
+ (A.prototype = n.fn), (y = n(l));
+ var B = /^(?:parents|prev(?:Until|All))/,
+ C = { children: !0, contents: !0, next: !0, prev: !0 };
+ n.extend({
+ dir: function (a, b, c) {
+ var d = [],
+ e = void 0 !== c;
+ while ((a = a[b]) && 9 !== a.nodeType)
+ if (1 === a.nodeType) {
+ if (e && n(a).is(c)) break;
+ d.push(a);
+ }
+ return d;
+ },
+ sibling: function (a, b) {
+ for (var c = []; a; a = a.nextSibling) 1 === a.nodeType && a !== b && c.push(a);
+ return c;
+ },
+ }),
+ n.fn.extend({
+ has: function (a) {
+ var b = n(a, this),
+ c = b.length;
+ return this.filter(function () {
+ for (var a = 0; c > a; a++) if (n.contains(this, b[a])) return !0;
+ });
+ },
+ closest: function (a, b) {
+ for (
+ var c, d = 0, e = this.length, f = [], g = u.test(a) || 'string' != typeof a ? n(a, b || this.context) : 0;
+ e > d;
+ d++
+ )
+ for (c = this[d]; c && c !== b; c = c.parentNode)
+ if (c.nodeType < 11 && (g ? g.index(c) > -1 : 1 === c.nodeType && n.find.matchesSelector(c, a))) {
+ f.push(c);
+ break;
+ }
+ return this.pushStack(f.length > 1 ? n.unique(f) : f);
+ },
+ index: function (a) {
+ return a
+ ? 'string' == typeof a
+ ? g.call(n(a), this[0])
+ : g.call(this, a.jquery ? a[0] : a)
+ : this[0] && this[0].parentNode
+ ? this.first().prevAll().length
+ : -1;
+ },
+ add: function (a, b) {
+ return this.pushStack(n.unique(n.merge(this.get(), n(a, b))));
+ },
+ addBack: function (a) {
+ return this.add(null == a ? this.prevObject : this.prevObject.filter(a));
+ },
+ });
+ function D(a, b) {
+ while ((a = a[b]) && 1 !== a.nodeType);
+ return a;
+ }
+ n.each(
+ {
+ parent: function (a) {
+ var b = a.parentNode;
+ return b && 11 !== b.nodeType ? b : null;
+ },
+ parents: function (a) {
+ return n.dir(a, 'parentNode');
+ },
+ parentsUntil: function (a, b, c) {
+ return n.dir(a, 'parentNode', c);
+ },
+ next: function (a) {
+ return D(a, 'nextSibling');
+ },
+ prev: function (a) {
+ return D(a, 'previousSibling');
+ },
+ nextAll: function (a) {
+ return n.dir(a, 'nextSibling');
+ },
+ prevAll: function (a) {
+ return n.dir(a, 'previousSibling');
+ },
+ nextUntil: function (a, b, c) {
+ return n.dir(a, 'nextSibling', c);
+ },
+ prevUntil: function (a, b, c) {
+ return n.dir(a, 'previousSibling', c);
+ },
+ siblings: function (a) {
+ return n.sibling((a.parentNode || {}).firstChild, a);
+ },
+ children: function (a) {
+ return n.sibling(a.firstChild);
+ },
+ contents: function (a) {
+ return a.contentDocument || n.merge([], a.childNodes);
+ },
+ },
+ function (a, b) {
+ n.fn[a] = function (c, d) {
+ var e = n.map(this, b, c);
+ return (
+ 'Until' !== a.slice(-5) && (d = c),
+ d && 'string' == typeof d && (e = n.filter(d, e)),
+ this.length > 1 && (C[a] || n.unique(e), B.test(a) && e.reverse()),
+ this.pushStack(e)
+ );
+ };
+ },
+ );
+ var E = /\S+/g,
+ F = {};
+ function G(a) {
+ var b = (F[a] = {});
+ return (
+ n.each(a.match(E) || [], function (a, c) {
+ b[c] = !0;
+ }),
+ b
+ );
+ }
+ (n.Callbacks = function (a) {
+ a = 'string' == typeof a ? F[a] || G(a) : n.extend({}, a);
+ var b,
+ c,
+ d,
+ e,
+ f,
+ g,
+ h = [],
+ i = !a.once && [],
+ j = function (l) {
+ for (b = a.memory && l, c = !0, g = e || 0, e = 0, f = h.length, d = !0; h && f > g; g++)
+ if (h[g].apply(l[0], l[1]) === !1 && a.stopOnFalse) {
+ b = !1;
+ break;
+ }
+ (d = !1), h && (i ? i.length && j(i.shift()) : b ? (h = []) : k.disable());
+ },
+ k = {
+ add: function () {
+ if (h) {
+ var c = h.length;
+ !(function g(b) {
+ n.each(b, function (b, c) {
+ var d = n.type(c);
+ 'function' === d ? (a.unique && k.has(c)) || h.push(c) : c && c.length && 'string' !== d && g(c);
+ });
+ })(arguments),
+ d ? (f = h.length) : b && ((e = c), j(b));
+ }
+ return this;
+ },
+ remove: function () {
+ return (
+ h &&
+ n.each(arguments, function (a, b) {
+ var c;
+ while ((c = n.inArray(b, h, c)) > -1) h.splice(c, 1), d && (f >= c && f--, g >= c && g--);
+ }),
+ this
+ );
+ },
+ has: function (a) {
+ return a ? n.inArray(a, h) > -1 : !(!h || !h.length);
+ },
+ empty: function () {
+ return (h = []), (f = 0), this;
+ },
+ disable: function () {
+ return (h = i = b = void 0), this;
+ },
+ disabled: function () {
+ return !h;
+ },
+ lock: function () {
+ return (i = void 0), b || k.disable(), this;
+ },
+ locked: function () {
+ return !i;
+ },
+ fireWith: function (a, b) {
+ return !h || (c && !i) || ((b = b || []), (b = [a, b.slice ? b.slice() : b]), d ? i.push(b) : j(b)), this;
+ },
+ fire: function () {
+ return k.fireWith(this, arguments), this;
+ },
+ fired: function () {
+ return !!c;
+ },
+ };
+ return k;
+ }),
+ n.extend({
+ Deferred: function (a) {
+ var b = [
+ ['resolve', 'done', n.Callbacks('once memory'), 'resolved'],
+ ['reject', 'fail', n.Callbacks('once memory'), 'rejected'],
+ ['notify', 'progress', n.Callbacks('memory')],
+ ],
+ c = 'pending',
+ d = {
+ state: function () {
+ return c;
+ },
+ always: function () {
+ return e.done(arguments).fail(arguments), this;
+ },
+ then: function () {
+ var a = arguments;
+ return n
+ .Deferred(function (c) {
+ n.each(b, function (b, f) {
+ var g = n.isFunction(a[b]) && a[b];
+ e[f[1]](function () {
+ var a = g && g.apply(this, arguments);
+ a && n.isFunction(a.promise)
+ ? a.promise().done(c.resolve).fail(c.reject).progress(c.notify)
+ : c[f[0] + 'With'](this === d ? c.promise() : this, g ? [a] : arguments);
+ });
+ }),
+ (a = null);
+ })
+ .promise();
+ },
+ promise: function (a) {
+ return null != a ? n.extend(a, d) : d;
+ },
+ },
+ e = {};
+ return (
+ (d.pipe = d.then),
+ n.each(b, function (a, f) {
+ var g = f[2],
+ h = f[3];
+ (d[f[1]] = g.add),
+ h &&
+ g.add(
+ function () {
+ c = h;
+ },
+ b[1 ^ a][2].disable,
+ b[2][2].lock,
+ ),
+ (e[f[0]] = function () {
+ return e[f[0] + 'With'](this === e ? d : this, arguments), this;
+ }),
+ (e[f[0] + 'With'] = g.fireWith);
+ }),
+ d.promise(e),
+ a && a.call(e, e),
+ e
+ );
+ },
+ when: function (a) {
+ var b = 0,
+ c = d.call(arguments),
+ e = c.length,
+ f = 1 !== e || (a && n.isFunction(a.promise)) ? e : 0,
+ g = 1 === f ? a : n.Deferred(),
+ h = function (a, b, c) {
+ return function (e) {
+ (b[a] = this),
+ (c[a] = arguments.length > 1 ? d.call(arguments) : e),
+ c === i ? g.notifyWith(b, c) : --f || g.resolveWith(b, c);
+ };
+ },
+ i,
+ j,
+ k;
+ if (e > 1)
+ for (i = new Array(e), j = new Array(e), k = new Array(e); e > b; b++)
+ c[b] && n.isFunction(c[b].promise)
+ ? c[b].promise().done(h(b, k, c)).fail(g.reject).progress(h(b, j, i))
+ : --f;
+ return f || g.resolveWith(k, c), g.promise();
+ },
+ });
+ var H;
+ (n.fn.ready = function (a) {
+ return n.ready.promise().done(a), this;
+ }),
+ n.extend({
+ isReady: !1,
+ readyWait: 1,
+ holdReady: function (a) {
+ a ? n.readyWait++ : n.ready(!0);
+ },
+ ready: function (a) {
+ (a === !0 ? --n.readyWait : n.isReady) ||
+ ((n.isReady = !0),
+ (a !== !0 && --n.readyWait > 0) ||
+ (H.resolveWith(l, [n]), n.fn.triggerHandler && (n(l).triggerHandler('ready'), n(l).off('ready'))));
+ },
+ });
+ function I() {
+ l.removeEventListener('DOMContentLoaded', I, !1), a.removeEventListener('load', I, !1), n.ready();
+ }
+ (n.ready.promise = function (b) {
+ return (
+ H ||
+ ((H = n.Deferred()),
+ 'complete' === l.readyState
+ ? setTimeout(n.ready)
+ : (l.addEventListener('DOMContentLoaded', I, !1), a.addEventListener('load', I, !1))),
+ H.promise(b)
+ );
+ }),
+ n.ready.promise();
+ var J = (n.access = function (a, b, c, d, e, f, g) {
+ var h = 0,
+ i = a.length,
+ j = null == c;
+ if ('object' === n.type(c)) {
+ e = !0;
+ for (h in c) n.access(a, b, h, c[h], !0, f, g);
+ } else if (
+ void 0 !== d &&
+ ((e = !0),
+ n.isFunction(d) || (g = !0),
+ j &&
+ (g
+ ? (b.call(a, d), (b = null))
+ : ((j = b),
+ (b = function (a, b, c) {
+ return j.call(n(a), c);
+ }))),
+ b)
+ )
+ for (; i > h; h++) b(a[h], c, g ? d : d.call(a[h], h, b(a[h], c)));
+ return e ? a : j ? b.call(a) : i ? b(a[0], c) : f;
+ });
+ n.acceptData = function (a) {
+ return 1 === a.nodeType || 9 === a.nodeType || !+a.nodeType;
+ };
+ function K() {
+ Object.defineProperty((this.cache = {}), 0, {
+ get: function () {
+ return {};
+ },
+ }),
+ (this.expando = n.expando + K.uid++);
+ }
+ (K.uid = 1),
+ (K.accepts = n.acceptData),
+ (K.prototype = {
+ key: function (a) {
+ if (!K.accepts(a)) return 0;
+ var b = {},
+ c = a[this.expando];
+ if (!c) {
+ c = K.uid++;
+ try {
+ (b[this.expando] = { value: c }), Object.defineProperties(a, b);
+ } catch (d) {
+ (b[this.expando] = c), n.extend(a, b);
+ }
+ }
+ return this.cache[c] || (this.cache[c] = {}), c;
+ },
+ set: function (a, b, c) {
+ var d,
+ e = this.key(a),
+ f = this.cache[e];
+ if ('string' == typeof b) f[b] = c;
+ else if (n.isEmptyObject(f)) n.extend(this.cache[e], b);
+ else for (d in b) f[d] = b[d];
+ return f;
+ },
+ get: function (a, b) {
+ var c = this.cache[this.key(a)];
+ return void 0 === b ? c : c[b];
+ },
+ access: function (a, b, c) {
+ var d;
+ return void 0 === b || (b && 'string' == typeof b && void 0 === c)
+ ? ((d = this.get(a, b)), void 0 !== d ? d : this.get(a, n.camelCase(b)))
+ : (this.set(a, b, c), void 0 !== c ? c : b);
+ },
+ remove: function (a, b) {
+ var c,
+ d,
+ e,
+ f = this.key(a),
+ g = this.cache[f];
+ if (void 0 === b) this.cache[f] = {};
+ else {
+ n.isArray(b)
+ ? (d = b.concat(b.map(n.camelCase)))
+ : ((e = n.camelCase(b)), b in g ? (d = [b, e]) : ((d = e), (d = d in g ? [d] : d.match(E) || []))),
+ (c = d.length);
+ while (c--) delete g[d[c]];
+ }
+ },
+ hasData: function (a) {
+ return !n.isEmptyObject(this.cache[a[this.expando]] || {});
+ },
+ discard: function (a) {
+ a[this.expando] && delete this.cache[a[this.expando]];
+ },
+ });
+ var L = new K(),
+ M = new K(),
+ N = /^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,
+ O = /([A-Z])/g;
+ function P(a, b, c) {
+ var d;
+ if (void 0 === c && 1 === a.nodeType)
+ if (((d = 'data-' + b.replace(O, '-$1').toLowerCase()), (c = a.getAttribute(d)), 'string' == typeof c)) {
+ try {
+ c =
+ 'true' === c
+ ? !0
+ : 'false' === c
+ ? !1
+ : 'null' === c
+ ? null
+ : +c + '' === c
+ ? +c
+ : N.test(c)
+ ? n.parseJSON(c)
+ : c;
+ } catch (e) {}
+ M.set(a, b, c);
+ } else c = void 0;
+ return c;
+ }
+ n.extend({
+ hasData: function (a) {
+ return M.hasData(a) || L.hasData(a);
+ },
+ data: function (a, b, c) {
+ return M.access(a, b, c);
+ },
+ removeData: function (a, b) {
+ M.remove(a, b);
+ },
+ _data: function (a, b, c) {
+ return L.access(a, b, c);
+ },
+ _removeData: function (a, b) {
+ L.remove(a, b);
+ },
+ }),
+ n.fn.extend({
+ data: function (a, b) {
+ var c,
+ d,
+ e,
+ f = this[0],
+ g = f && f.attributes;
+ if (void 0 === a) {
+ if (this.length && ((e = M.get(f)), 1 === f.nodeType && !L.get(f, 'hasDataAttrs'))) {
+ c = g.length;
+ while (c--)
+ g[c] && ((d = g[c].name), 0 === d.indexOf('data-') && ((d = n.camelCase(d.slice(5))), P(f, d, e[d])));
+ L.set(f, 'hasDataAttrs', !0);
+ }
+ return e;
+ }
+ return 'object' == typeof a
+ ? this.each(function () {
+ M.set(this, a);
+ })
+ : J(
+ this,
+ function (b) {
+ var c,
+ d = n.camelCase(a);
+ if (f && void 0 === b) {
+ if (((c = M.get(f, a)), void 0 !== c)) return c;
+ if (((c = M.get(f, d)), void 0 !== c)) return c;
+ if (((c = P(f, d, void 0)), void 0 !== c)) return c;
+ } else
+ this.each(function () {
+ var c = M.get(this, d);
+ M.set(this, d, b), -1 !== a.indexOf('-') && void 0 !== c && M.set(this, a, b);
+ });
+ },
+ null,
+ b,
+ arguments.length > 1,
+ null,
+ !0,
+ );
+ },
+ removeData: function (a) {
+ return this.each(function () {
+ M.remove(this, a);
+ });
+ },
+ }),
+ n.extend({
+ queue: function (a, b, c) {
+ var d;
+ return a
+ ? ((b = (b || 'fx') + 'queue'),
+ (d = L.get(a, b)),
+ c && (!d || n.isArray(c) ? (d = L.access(a, b, n.makeArray(c))) : d.push(c)),
+ d || [])
+ : void 0;
+ },
+ dequeue: function (a, b) {
+ b = b || 'fx';
+ var c = n.queue(a, b),
+ d = c.length,
+ e = c.shift(),
+ f = n._queueHooks(a, b),
+ g = function () {
+ n.dequeue(a, b);
+ };
+ 'inprogress' === e && ((e = c.shift()), d--),
+ e && ('fx' === b && c.unshift('inprogress'), delete f.stop, e.call(a, g, f)),
+ !d && f && f.empty.fire();
+ },
+ _queueHooks: function (a, b) {
+ var c = b + 'queueHooks';
+ return (
+ L.get(a, c) ||
+ L.access(a, c, {
+ empty: n.Callbacks('once memory').add(function () {
+ L.remove(a, [b + 'queue', c]);
+ }),
+ })
+ );
+ },
+ }),
+ n.fn.extend({
+ queue: function (a, b) {
+ var c = 2;
+ return (
+ 'string' != typeof a && ((b = a), (a = 'fx'), c--),
+ arguments.length < c
+ ? n.queue(this[0], a)
+ : void 0 === b
+ ? this
+ : this.each(function () {
+ var c = n.queue(this, a, b);
+ n._queueHooks(this, a), 'fx' === a && 'inprogress' !== c[0] && n.dequeue(this, a);
+ })
+ );
+ },
+ dequeue: function (a) {
+ return this.each(function () {
+ n.dequeue(this, a);
+ });
+ },
+ clearQueue: function (a) {
+ return this.queue(a || 'fx', []);
+ },
+ promise: function (a, b) {
+ var c,
+ d = 1,
+ e = n.Deferred(),
+ f = this,
+ g = this.length,
+ h = function () {
+ --d || e.resolveWith(f, [f]);
+ };
+ 'string' != typeof a && ((b = a), (a = void 0)), (a = a || 'fx');
+ while (g--) (c = L.get(f[g], a + 'queueHooks')), c && c.empty && (d++, c.empty.add(h));
+ return h(), e.promise(b);
+ },
+ });
+ var Q = /[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/.source,
+ R = ['Top', 'Right', 'Bottom', 'Left'],
+ S = function (a, b) {
+ return (a = b || a), 'none' === n.css(a, 'display') || !n.contains(a.ownerDocument, a);
+ },
+ T = /^(?:checkbox|radio)$/i;
+ !(function () {
+ var a = l.createDocumentFragment(),
+ b = a.appendChild(l.createElement('div')),
+ c = l.createElement('input');
+ c.setAttribute('type', 'radio'),
+ c.setAttribute('checked', 'checked'),
+ c.setAttribute('name', 't'),
+ b.appendChild(c),
+ (k.checkClone = b.cloneNode(!0).cloneNode(!0).lastChild.checked),
+ (b.innerHTML = '<textarea>x</textarea>'),
+ (k.noCloneChecked = !!b.cloneNode(!0).lastChild.defaultValue);
+ })();
+ var U = 'undefined';
+ k.focusinBubbles = 'onfocusin' in a;
+ var V = /^key/,
+ W = /^(?:mouse|pointer|contextmenu)|click/,
+ X = /^(?:focusinfocus|focusoutblur)$/,
+ Y = /^([^.]*)(?:\.(.+)|)$/;
+ function Z() {
+ return !0;
+ }
+ function $() {
+ return !1;
+ }
+ function _() {
+ try {
+ return l.activeElement;
+ } catch (a) {}
+ }
+ (n.event = {
+ global: {},
+ add: function (a, b, c, d, e) {
+ var f,
+ g,
+ h,
+ i,
+ j,
+ k,
+ l,
+ m,
+ o,
+ p,
+ q,
+ r = L.get(a);
+ if (r) {
+ c.handler && ((f = c), (c = f.handler), (e = f.selector)),
+ c.guid || (c.guid = n.guid++),
+ (i = r.events) || (i = r.events = {}),
+ (g = r.handle) ||
+ (g = r.handle =
+ function (b) {
+ return typeof n !== U && n.event.triggered !== b.type ? n.event.dispatch.apply(a, arguments) : void 0;
+ }),
+ (b = (b || '').match(E) || ['']),
+ (j = b.length);
+ while (j--)
+ (h = Y.exec(b[j]) || []),
+ (o = q = h[1]),
+ (p = (h[2] || '').split('.').sort()),
+ o &&
+ ((l = n.event.special[o] || {}),
+ (o = (e ? l.delegateType : l.bindType) || o),
+ (l = n.event.special[o] || {}),
+ (k = n.extend(
+ {
+ type: o,
+ origType: q,
+ data: d,
+ handler: c,
+ guid: c.guid,
+ selector: e,
+ needsContext: e && n.expr.match.needsContext.test(e),
+ namespace: p.join('.'),
+ },
+ f,
+ )),
+ (m = i[o]) ||
+ ((m = i[o] = []),
+ (m.delegateCount = 0),
+ (l.setup && l.setup.call(a, d, p, g) !== !1) || (a.addEventListener && a.addEventListener(o, g, !1))),
+ l.add && (l.add.call(a, k), k.handler.guid || (k.handler.guid = c.guid)),
+ e ? m.splice(m.delegateCount++, 0, k) : m.push(k),
+ (n.event.global[o] = !0));
+ }
+ },
+ remove: function (a, b, c, d, e) {
+ var f,
+ g,
+ h,
+ i,
+ j,
+ k,
+ l,
+ m,
+ o,
+ p,
+ q,
+ r = L.hasData(a) && L.get(a);
+ if (r && (i = r.events)) {
+ (b = (b || '').match(E) || ['']), (j = b.length);
+ while (j--)
+ if (((h = Y.exec(b[j]) || []), (o = q = h[1]), (p = (h[2] || '').split('.').sort()), o)) {
+ (l = n.event.special[o] || {}),
+ (o = (d ? l.delegateType : l.bindType) || o),
+ (m = i[o] || []),
+ (h = h[2] && new RegExp('(^|\\.)' + p.join('\\.(?:.*\\.|)') + '(\\.|$)')),
+ (g = f = m.length);
+ while (f--)
+ (k = m[f]),
+ (!e && q !== k.origType) ||
+ (c && c.guid !== k.guid) ||
+ (h && !h.test(k.namespace)) ||
+ (d && d !== k.selector && ('**' !== d || !k.selector)) ||
+ (m.splice(f, 1), k.selector && m.delegateCount--, l.remove && l.remove.call(a, k));
+ g &&
+ !m.length &&
+ ((l.teardown && l.teardown.call(a, p, r.handle) !== !1) || n.removeEvent(a, o, r.handle), delete i[o]);
+ } else for (o in i) n.event.remove(a, o + b[j], c, d, !0);
+ n.isEmptyObject(i) && (delete r.handle, L.remove(a, 'events'));
+ }
+ },
+ trigger: function (b, c, d, e) {
+ var f,
+ g,
+ h,
+ i,
+ k,
+ m,
+ o,
+ p = [d || l],
+ q = j.call(b, 'type') ? b.type : b,
+ r = j.call(b, 'namespace') ? b.namespace.split('.') : [];
+ if (
+ ((g = h = d = d || l),
+ 3 !== d.nodeType &&
+ 8 !== d.nodeType &&
+ !X.test(q + n.event.triggered) &&
+ (q.indexOf('.') >= 0 && ((r = q.split('.')), (q = r.shift()), r.sort()),
+ (k = q.indexOf(':') < 0 && 'on' + q),
+ (b = b[n.expando] ? b : new n.Event(q, 'object' == typeof b && b)),
+ (b.isTrigger = e ? 2 : 3),
+ (b.namespace = r.join('.')),
+ (b.namespace_re = b.namespace ? new RegExp('(^|\\.)' + r.join('\\.(?:.*\\.|)') + '(\\.|$)') : null),
+ (b.result = void 0),
+ b.target || (b.target = d),
+ (c = null == c ? [b] : n.makeArray(c, [b])),
+ (o = n.event.special[q] || {}),
+ e || !o.trigger || o.trigger.apply(d, c) !== !1))
+ ) {
+ if (!e && !o.noBubble && !n.isWindow(d)) {
+ for (i = o.delegateType || q, X.test(i + q) || (g = g.parentNode); g; g = g.parentNode) p.push(g), (h = g);
+ h === (d.ownerDocument || l) && p.push(h.defaultView || h.parentWindow || a);
+ }
+ f = 0;
+ while ((g = p[f++]) && !b.isPropagationStopped())
+ (b.type = f > 1 ? i : o.bindType || q),
+ (m = (L.get(g, 'events') || {})[b.type] && L.get(g, 'handle')),
+ m && m.apply(g, c),
+ (m = k && g[k]),
+ m && m.apply && n.acceptData(g) && ((b.result = m.apply(g, c)), b.result === !1 && b.preventDefault());
+ return (
+ (b.type = q),
+ e ||
+ b.isDefaultPrevented() ||
+ (o._default && o._default.apply(p.pop(), c) !== !1) ||
+ !n.acceptData(d) ||
+ (k &&
+ n.isFunction(d[q]) &&
+ !n.isWindow(d) &&
+ ((h = d[k]),
+ h && (d[k] = null),
+ (n.event.triggered = q),
+ d[q](),
+ (n.event.triggered = void 0),
+ h && (d[k] = h))),
+ b.result
+ );
+ }
+ },
+ dispatch: function (a) {
+ a = n.event.fix(a);
+ var b,
+ c,
+ e,
+ f,
+ g,
+ h = [],
+ i = d.call(arguments),
+ j = (L.get(this, 'events') || {})[a.type] || [],
+ k = n.event.special[a.type] || {};
+ if (((i[0] = a), (a.delegateTarget = this), !k.preDispatch || k.preDispatch.call(this, a) !== !1)) {
+ (h = n.event.handlers.call(this, a, j)), (b = 0);
+ while ((f = h[b++]) && !a.isPropagationStopped()) {
+ (a.currentTarget = f.elem), (c = 0);
+ while ((g = f.handlers[c++]) && !a.isImmediatePropagationStopped())
+ (!a.namespace_re || a.namespace_re.test(g.namespace)) &&
+ ((a.handleObj = g),
+ (a.data = g.data),
+ (e = ((n.event.special[g.origType] || {}).handle || g.handler).apply(f.elem, i)),
+ void 0 !== e && (a.result = e) === !1 && (a.preventDefault(), a.stopPropagation()));
+ }
+ return k.postDispatch && k.postDispatch.call(this, a), a.result;
+ }
+ },
+ handlers: function (a, b) {
+ var c,
+ d,
+ e,
+ f,
+ g = [],
+ h = b.delegateCount,
+ i = a.target;
+ if (h && i.nodeType && (!a.button || 'click' !== a.type))
+ for (; i !== this; i = i.parentNode || this)
+ if (i.disabled !== !0 || 'click' !== a.type) {
+ for (d = [], c = 0; h > c; c++)
+ (f = b[c]),
+ (e = f.selector + ' '),
+ void 0 === d[e] &&
+ (d[e] = f.needsContext ? n(e, this).index(i) >= 0 : n.find(e, this, null, [i]).length),
+ d[e] && d.push(f);
+ d.length && g.push({ elem: i, handlers: d });
+ }
+ return h < b.length && g.push({ elem: this, handlers: b.slice(h) }), g;
+ },
+ props:
+ 'altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which'.split(
+ ' ',
+ ),
+ fixHooks: {},
+ keyHooks: {
+ props: 'char charCode key keyCode'.split(' '),
+ filter: function (a, b) {
+ return null == a.which && (a.which = null != b.charCode ? b.charCode : b.keyCode), a;
+ },
+ },
+ mouseHooks: {
+ props: 'button buttons clientX clientY offsetX offsetY pageX pageY screenX screenY toElement'.split(' '),
+ filter: function (a, b) {
+ var c,
+ d,
+ e,
+ f = b.button;
+ return (
+ null == a.pageX &&
+ null != b.clientX &&
+ ((c = a.target.ownerDocument || l),
+ (d = c.documentElement),
+ (e = c.body),
+ (a.pageX =
+ b.clientX +
+ ((d && d.scrollLeft) || (e && e.scrollLeft) || 0) -
+ ((d && d.clientLeft) || (e && e.clientLeft) || 0)),
+ (a.pageY =
+ b.clientY +
+ ((d && d.scrollTop) || (e && e.scrollTop) || 0) -
+ ((d && d.clientTop) || (e && e.clientTop) || 0))),
+ a.which || void 0 === f || (a.which = 1 & f ? 1 : 2 & f ? 3 : 4 & f ? 2 : 0),
+ a
+ );
+ },
+ },
+ fix: function (a) {
+ if (a[n.expando]) return a;
+ var b,
+ c,
+ d,
+ e = a.type,
+ f = a,
+ g = this.fixHooks[e];
+ g || (this.fixHooks[e] = g = W.test(e) ? this.mouseHooks : V.test(e) ? this.keyHooks : {}),
+ (d = g.props ? this.props.concat(g.props) : this.props),
+ (a = new n.Event(f)),
+ (b = d.length);
+ while (b--) (c = d[b]), (a[c] = f[c]);
+ return (
+ a.target || (a.target = l),
+ 3 === a.target.nodeType && (a.target = a.target.parentNode),
+ g.filter ? g.filter(a, f) : a
+ );
+ },
+ special: {
+ load: { noBubble: !0 },
+ focus: {
+ trigger: function () {
+ return this !== _() && this.focus ? (this.focus(), !1) : void 0;
+ },
+ delegateType: 'focusin',
+ },
+ blur: {
+ trigger: function () {
+ return this === _() && this.blur ? (this.blur(), !1) : void 0;
+ },
+ delegateType: 'focusout',
+ },
+ click: {
+ trigger: function () {
+ return 'checkbox' === this.type && this.click && n.nodeName(this, 'input') ? (this.click(), !1) : void 0;
+ },
+ _default: function (a) {
+ return n.nodeName(a.target, 'a');
+ },
+ },
+ beforeunload: {
+ postDispatch: function (a) {
+ void 0 !== a.result && a.originalEvent && (a.originalEvent.returnValue = a.result);
+ },
+ },
+ },
+ simulate: function (a, b, c, d) {
+ var e = n.extend(new n.Event(), c, { type: a, isSimulated: !0, originalEvent: {} });
+ d ? n.event.trigger(e, null, b) : n.event.dispatch.call(b, e), e.isDefaultPrevented() && c.preventDefault();
+ },
+ }),
+ (n.removeEvent = function (a, b, c) {
+ a.removeEventListener && a.removeEventListener(b, c, !1);
+ }),
+ (n.Event = function (a, b) {
+ return this instanceof n.Event
+ ? (a && a.type
+ ? ((this.originalEvent = a),
+ (this.type = a.type),
+ (this.isDefaultPrevented =
+ a.defaultPrevented || (void 0 === a.defaultPrevented && a.returnValue === !1) ? Z : $))
+ : (this.type = a),
+ b && n.extend(this, b),
+ (this.timeStamp = (a && a.timeStamp) || n.now()),
+ void (this[n.expando] = !0))
+ : new n.Event(a, b);
+ }),
+ (n.Event.prototype = {
+ isDefaultPrevented: $,
+ isPropagationStopped: $,
+ isImmediatePropagationStopped: $,
+ preventDefault: function () {
+ var a = this.originalEvent;
+ (this.isDefaultPrevented = Z), a && a.preventDefault && a.preventDefault();
+ },
+ stopPropagation: function () {
+ var a = this.originalEvent;
+ (this.isPropagationStopped = Z), a && a.stopPropagation && a.stopPropagation();
+ },
+ stopImmediatePropagation: function () {
+ var a = this.originalEvent;
+ (this.isImmediatePropagationStopped = Z),
+ a && a.stopImmediatePropagation && a.stopImmediatePropagation(),
+ this.stopPropagation();
+ },
+ }),
+ n.each(
+ { mouseenter: 'mouseover', mouseleave: 'mouseout', pointerenter: 'pointerover', pointerleave: 'pointerout' },
+ function (a, b) {
+ n.event.special[a] = {
+ delegateType: b,
+ bindType: b,
+ handle: function (a) {
+ var c,
+ d = this,
+ e = a.relatedTarget,
+ f = a.handleObj;
+ return (
+ (!e || (e !== d && !n.contains(d, e))) &&
+ ((a.type = f.origType), (c = f.handler.apply(this, arguments)), (a.type = b)),
+ c
+ );
+ },
+ };
+ },
+ ),
+ k.focusinBubbles ||
+ n.each({ focus: 'focusin', blur: 'focusout' }, function (a, b) {
+ var c = function (a) {
+ n.event.simulate(b, a.target, n.event.fix(a), !0);
+ };
+ n.event.special[b] = {
+ setup: function () {
+ var d = this.ownerDocument || this,
+ e = L.access(d, b);
+ e || d.addEventListener(a, c, !0), L.access(d, b, (e || 0) + 1);
+ },
+ teardown: function () {
+ var d = this.ownerDocument || this,
+ e = L.access(d, b) - 1;
+ e ? L.access(d, b, e) : (d.removeEventListener(a, c, !0), L.remove(d, b));
+ },
+ };
+ }),
+ n.fn.extend({
+ on: function (a, b, c, d, e) {
+ var f, g;
+ if ('object' == typeof a) {
+ 'string' != typeof b && ((c = c || b), (b = void 0));
+ for (g in a) this.on(g, b, c, a[g], e);
+ return this;
+ }
+ if (
+ (null == c && null == d
+ ? ((d = b), (c = b = void 0))
+ : null == d && ('string' == typeof b ? ((d = c), (c = void 0)) : ((d = c), (c = b), (b = void 0))),
+ d === !1)
+ )
+ d = $;
+ else if (!d) return this;
+ return (
+ 1 === e &&
+ ((f = d),
+ (d = function (a) {
+ return n().off(a), f.apply(this, arguments);
+ }),
+ (d.guid = f.guid || (f.guid = n.guid++))),
+ this.each(function () {
+ n.event.add(this, a, d, c, b);
+ })
+ );
+ },
+ one: function (a, b, c, d) {
+ return this.on(a, b, c, d, 1);
+ },
+ off: function (a, b, c) {
+ var d, e;
+ if (a && a.preventDefault && a.handleObj)
+ return (
+ (d = a.handleObj),
+ n(a.delegateTarget).off(d.namespace ? d.origType + '.' + d.namespace : d.origType, d.selector, d.handler),
+ this
+ );
+ if ('object' == typeof a) {
+ for (e in a) this.off(e, b, a[e]);
+ return this;
+ }
+ return (
+ (b === !1 || 'function' == typeof b) && ((c = b), (b = void 0)),
+ c === !1 && (c = $),
+ this.each(function () {
+ n.event.remove(this, a, c, b);
+ })
+ );
+ },
+ trigger: function (a, b) {
+ return this.each(function () {
+ n.event.trigger(a, b, this);
+ });
+ },
+ triggerHandler: function (a, b) {
+ var c = this[0];
+ return c ? n.event.trigger(a, b, c, !0) : void 0;
+ },
+ });
+ var ab = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi,
+ bb = /<([\w:]+)/,
+ cb = /<|&#?\w+;/,
+ db = /<(?:script|style|link)/i,
+ eb = /checked\s*(?:[^=]|=\s*.checked.)/i,
+ fb = /^$|\/(?:java|ecma)script/i,
+ gb = /^true\/(.*)/,
+ hb = /^\s*<!(?:\[CDATA\[|--)|(?:\]\]|--)>\s*$/g,
+ ib = {
+ option: [1, "<select multiple='multiple'>", '</select>'],
+ thead: [1, '<table>', '</table>'],
+ col: [2, '<table><colgroup>', '</colgroup></table>'],
+ tr: [2, '<table><tbody>', '</tbody></table>'],
+ td: [3, '<table><tbody><tr>', '</tr></tbody></table>'],
+ _default: [0, '', ''],
+ };
+ (ib.optgroup = ib.option), (ib.tbody = ib.tfoot = ib.colgroup = ib.caption = ib.thead), (ib.th = ib.td);
+ function jb(a, b) {
+ return n.nodeName(a, 'table') && n.nodeName(11 !== b.nodeType ? b : b.firstChild, 'tr')
+ ? a.getElementsByTagName('tbody')[0] || a.appendChild(a.ownerDocument.createElement('tbody'))
+ : a;
+ }
+ function kb(a) {
+ return (a.type = (null !== a.getAttribute('type')) + '/' + a.type), a;
+ }
+ function lb(a) {
+ var b = gb.exec(a.type);
+ return b ? (a.type = b[1]) : a.removeAttribute('type'), a;
+ }
+ function mb(a, b) {
+ for (var c = 0, d = a.length; d > c; c++) L.set(a[c], 'globalEval', !b || L.get(b[c], 'globalEval'));
+ }
+ function nb(a, b) {
+ var c, d, e, f, g, h, i, j;
+ if (1 === b.nodeType) {
+ if (L.hasData(a) && ((f = L.access(a)), (g = L.set(b, f)), (j = f.events))) {
+ delete g.handle, (g.events = {});
+ for (e in j) for (c = 0, d = j[e].length; d > c; c++) n.event.add(b, e, j[e][c]);
+ }
+ M.hasData(a) && ((h = M.access(a)), (i = n.extend({}, h)), M.set(b, i));
+ }
+ }
+ function ob(a, b) {
+ var c = a.getElementsByTagName
+ ? a.getElementsByTagName(b || '*')
+ : a.querySelectorAll
+ ? a.querySelectorAll(b || '*')
+ : [];
+ return void 0 === b || (b && n.nodeName(a, b)) ? n.merge([a], c) : c;
+ }
+ function pb(a, b) {
+ var c = b.nodeName.toLowerCase();
+ 'input' === c && T.test(a.type)
+ ? (b.checked = a.checked)
+ : ('input' === c || 'textarea' === c) && (b.defaultValue = a.defaultValue);
+ }
+ n.extend({
+ clone: function (a, b, c) {
+ var d,
+ e,
+ f,
+ g,
+ h = a.cloneNode(!0),
+ i = n.contains(a.ownerDocument, a);
+ if (!(k.noCloneChecked || (1 !== a.nodeType && 11 !== a.nodeType) || n.isXMLDoc(a)))
+ for (g = ob(h), f = ob(a), d = 0, e = f.length; e > d; d++) pb(f[d], g[d]);
+ if (b)
+ if (c) for (f = f || ob(a), g = g || ob(h), d = 0, e = f.length; e > d; d++) nb(f[d], g[d]);
+ else nb(a, h);
+ return (g = ob(h, 'script')), g.length > 0 && mb(g, !i && ob(a, 'script')), h;
+ },
+ buildFragment: function (a, b, c, d) {
+ for (var e, f, g, h, i, j, k = b.createDocumentFragment(), l = [], m = 0, o = a.length; o > m; m++)
+ if (((e = a[m]), e || 0 === e))
+ if ('object' === n.type(e)) n.merge(l, e.nodeType ? [e] : e);
+ else if (cb.test(e)) {
+ (f = f || k.appendChild(b.createElement('div'))),
+ (g = (bb.exec(e) || ['', ''])[1].toLowerCase()),
+ (h = ib[g] || ib._default),
+ (f.innerHTML = h[1] + e.replace(ab, '<$1></$2>') + h[2]),
+ (j = h[0]);
+ while (j--) f = f.lastChild;
+ n.merge(l, f.childNodes), (f = k.firstChild), (f.textContent = '');
+ } else l.push(b.createTextNode(e));
+ (k.textContent = ''), (m = 0);
+ while ((e = l[m++]))
+ if (
+ (!d || -1 === n.inArray(e, d)) &&
+ ((i = n.contains(e.ownerDocument, e)), (f = ob(k.appendChild(e), 'script')), i && mb(f), c)
+ ) {
+ j = 0;
+ while ((e = f[j++])) fb.test(e.type || '') && c.push(e);
+ }
+ return k;
+ },
+ cleanData: function (a) {
+ for (var b, c, d, e, f = n.event.special, g = 0; void 0 !== (c = a[g]); g++) {
+ if (n.acceptData(c) && ((e = c[L.expando]), e && (b = L.cache[e]))) {
+ if (b.events) for (d in b.events) f[d] ? n.event.remove(c, d) : n.removeEvent(c, d, b.handle);
+ L.cache[e] && delete L.cache[e];
+ }
+ delete M.cache[c[M.expando]];
+ }
+ },
+ }),
+ n.fn.extend({
+ text: function (a) {
+ return J(
+ this,
+ function (a) {
+ return void 0 === a
+ ? n.text(this)
+ : this.empty().each(function () {
+ (1 === this.nodeType || 11 === this.nodeType || 9 === this.nodeType) && (this.textContent = a);
+ });
+ },
+ null,
+ a,
+ arguments.length,
+ );
+ },
+ append: function () {
+ return this.domManip(arguments, function (a) {
+ if (1 === this.nodeType || 11 === this.nodeType || 9 === this.nodeType) {
+ var b = jb(this, a);
+ b.appendChild(a);
+ }
+ });
+ },
+ prepend: function () {
+ return this.domManip(arguments, function (a) {
+ if (1 === this.nodeType || 11 === this.nodeType || 9 === this.nodeType) {
+ var b = jb(this, a);
+ b.insertBefore(a, b.firstChild);
+ }
+ });
+ },
+ before: function () {
+ return this.domManip(arguments, function (a) {
+ this.parentNode && this.parentNode.insertBefore(a, this);
+ });
+ },
+ after: function () {
+ return this.domManip(arguments, function (a) {
+ this.parentNode && this.parentNode.insertBefore(a, this.nextSibling);
+ });
+ },
+ remove: function (a, b) {
+ for (var c, d = a ? n.filter(a, this) : this, e = 0; null != (c = d[e]); e++)
+ b || 1 !== c.nodeType || n.cleanData(ob(c)),
+ c.parentNode && (b && n.contains(c.ownerDocument, c) && mb(ob(c, 'script')), c.parentNode.removeChild(c));
+ return this;
+ },
+ empty: function () {
+ for (var a, b = 0; null != (a = this[b]); b++)
+ 1 === a.nodeType && (n.cleanData(ob(a, !1)), (a.textContent = ''));
+ return this;
+ },
+ clone: function (a, b) {
+ return (
+ (a = null == a ? !1 : a),
+ (b = null == b ? a : b),
+ this.map(function () {
+ return n.clone(this, a, b);
+ })
+ );
+ },
+ html: function (a) {
+ return J(
+ this,
+ function (a) {
+ var b = this[0] || {},
+ c = 0,
+ d = this.length;
+ if (void 0 === a && 1 === b.nodeType) return b.innerHTML;
+ if ('string' == typeof a && !db.test(a) && !ib[(bb.exec(a) || ['', ''])[1].toLowerCase()]) {
+ a = a.replace(ab, '<$1></$2>');
+ try {
+ for (; d > c; c++) (b = this[c] || {}), 1 === b.nodeType && (n.cleanData(ob(b, !1)), (b.innerHTML = a));
+ b = 0;
+ } catch (e) {}
+ }
+ b && this.empty().append(a);
+ },
+ null,
+ a,
+ arguments.length,
+ );
+ },
+ replaceWith: function () {
+ var a = arguments[0];
+ return (
+ this.domManip(arguments, function (b) {
+ (a = this.parentNode), n.cleanData(ob(this)), a && a.replaceChild(b, this);
+ }),
+ a && (a.length || a.nodeType) ? this : this.remove()
+ );
+ },
+ detach: function (a) {
+ return this.remove(a, !0);
+ },
+ domManip: function (a, b) {
+ a = e.apply([], a);
+ var c,
+ d,
+ f,
+ g,
+ h,
+ i,
+ j = 0,
+ l = this.length,
+ m = this,
+ o = l - 1,
+ p = a[0],
+ q = n.isFunction(p);
+ if (q || (l > 1 && 'string' == typeof p && !k.checkClone && eb.test(p)))
+ return this.each(function (c) {
+ var d = m.eq(c);
+ q && (a[0] = p.call(this, c, d.html())), d.domManip(a, b);
+ });
+ if (
+ l &&
+ ((c = n.buildFragment(a, this[0].ownerDocument, !1, this)),
+ (d = c.firstChild),
+ 1 === c.childNodes.length && (c = d),
+ d)
+ ) {
+ for (f = n.map(ob(c, 'script'), kb), g = f.length; l > j; j++)
+ (h = c), j !== o && ((h = n.clone(h, !0, !0)), g && n.merge(f, ob(h, 'script'))), b.call(this[j], h, j);
+ if (g)
+ for (i = f[f.length - 1].ownerDocument, n.map(f, lb), j = 0; g > j; j++)
+ (h = f[j]),
+ fb.test(h.type || '') &&
+ !L.access(h, 'globalEval') &&
+ n.contains(i, h) &&
+ (h.src ? n._evalUrl && n._evalUrl(h.src) : n.globalEval(h.textContent.replace(hb, '')));
+ }
+ return this;
+ },
+ }),
+ n.each(
+ {
+ appendTo: 'append',
+ prependTo: 'prepend',
+ insertBefore: 'before',
+ insertAfter: 'after',
+ replaceAll: 'replaceWith',
+ },
+ function (a, b) {
+ n.fn[a] = function (a) {
+ for (var c, d = [], e = n(a), g = e.length - 1, h = 0; g >= h; h++)
+ (c = h === g ? this : this.clone(!0)), n(e[h])[b](c), f.apply(d, c.get());
+ return this.pushStack(d);
+ };
+ },
+ );
+ var qb,
+ rb = {};
+ function sb(b, c) {
+ var d,
+ e = n(c.createElement(b)).appendTo(c.body),
+ f = a.getDefaultComputedStyle && (d = a.getDefaultComputedStyle(e[0])) ? d.display : n.css(e[0], 'display');
+ return e.detach(), f;
+ }
+ function tb(a) {
+ var b = l,
+ c = rb[a];
+ return (
+ c ||
+ ((c = sb(a, b)),
+ ('none' !== c && c) ||
+ ((qb = (qb || n("<iframe frameborder='0' width='0' height='0'/>")).appendTo(b.documentElement)),
+ (b = qb[0].contentDocument),
+ b.write(),
+ b.close(),
+ (c = sb(a, b)),
+ qb.detach()),
+ (rb[a] = c)),
+ c
+ );
+ }
+ var ub = /^margin/,
+ vb = new RegExp('^(' + Q + ')(?!px)[a-z%]+$', 'i'),
+ wb = function (b) {
+ return b.ownerDocument.defaultView.opener
+ ? b.ownerDocument.defaultView.getComputedStyle(b, null)
+ : a.getComputedStyle(b, null);
+ };
+ function xb(a, b, c) {
+ var d,
+ e,
+ f,
+ g,
+ h = a.style;
+ return (
+ (c = c || wb(a)),
+ c && (g = c.getPropertyValue(b) || c[b]),
+ c &&
+ ('' !== g || n.contains(a.ownerDocument, a) || (g = n.style(a, b)),
+ vb.test(g) &&
+ ub.test(b) &&
+ ((d = h.width),
+ (e = h.minWidth),
+ (f = h.maxWidth),
+ (h.minWidth = h.maxWidth = h.width = g),
+ (g = c.width),
+ (h.width = d),
+ (h.minWidth = e),
+ (h.maxWidth = f))),
+ void 0 !== g ? g + '' : g
+ );
+ }
+ function yb(a, b) {
+ return {
+ get: function () {
+ return a() ? void delete this.get : (this.get = b).apply(this, arguments);
+ },
+ };
+ }
+ !(function () {
+ var b,
+ c,
+ d = l.documentElement,
+ e = l.createElement('div'),
+ f = l.createElement('div');
+ if (f.style) {
+ (f.style.backgroundClip = 'content-box'),
+ (f.cloneNode(!0).style.backgroundClip = ''),
+ (k.clearCloneStyle = 'content-box' === f.style.backgroundClip),
+ (e.style.cssText = 'border:0;width:0;height:0;top:0;left:-9999px;margin-top:1px;position:absolute'),
+ e.appendChild(f);
+ function g() {
+ (f.style.cssText =
+ '-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;display:block;margin-top:1%;top:1%;border:1px;padding:1px;width:4px;position:absolute'),
+ (f.innerHTML = ''),
+ d.appendChild(e);
+ var g = a.getComputedStyle(f, null);
+ (b = '1%' !== g.top), (c = '4px' === g.width), d.removeChild(e);
+ }
+ a.getComputedStyle &&
+ n.extend(k, {
+ pixelPosition: function () {
+ return g(), b;
+ },
+ boxSizingReliable: function () {
+ return null == c && g(), c;
+ },
+ reliableMarginRight: function () {
+ var b,
+ c = f.appendChild(l.createElement('div'));
+ return (
+ (c.style.cssText = f.style.cssText =
+ '-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;display:block;margin:0;border:0;padding:0'),
+ (c.style.marginRight = c.style.width = '0'),
+ (f.style.width = '1px'),
+ d.appendChild(e),
+ (b = !parseFloat(a.getComputedStyle(c, null).marginRight)),
+ d.removeChild(e),
+ f.removeChild(c),
+ b
+ );
+ },
+ });
+ }
+ })(),
+ (n.swap = function (a, b, c, d) {
+ var e,
+ f,
+ g = {};
+ for (f in b) (g[f] = a.style[f]), (a.style[f] = b[f]);
+ e = c.apply(a, d || []);
+ for (f in b) a.style[f] = g[f];
+ return e;
+ });
+ var zb = /^(none|table(?!-c[ea]).+)/,
+ Ab = new RegExp('^(' + Q + ')(.*)$', 'i'),
+ Bb = new RegExp('^([+-])=(' + Q + ')', 'i'),
+ Cb = { position: 'absolute', visibility: 'hidden', display: 'block' },
+ Db = { letterSpacing: '0', fontWeight: '400' },
+ Eb = ['Webkit', 'O', 'Moz', 'ms'];
+ function Fb(a, b) {
+ if (b in a) return b;
+ var c = b[0].toUpperCase() + b.slice(1),
+ d = b,
+ e = Eb.length;
+ while (e--) if (((b = Eb[e] + c), b in a)) return b;
+ return d;
+ }
+ function Gb(a, b, c) {
+ var d = Ab.exec(b);
+ return d ? Math.max(0, d[1] - (c || 0)) + (d[2] || 'px') : b;
+ }
+ function Hb(a, b, c, d, e) {
+ for (var f = c === (d ? 'border' : 'content') ? 4 : 'width' === b ? 1 : 0, g = 0; 4 > f; f += 2)
+ 'margin' === c && (g += n.css(a, c + R[f], !0, e)),
+ d
+ ? ('content' === c && (g -= n.css(a, 'padding' + R[f], !0, e)),
+ 'margin' !== c && (g -= n.css(a, 'border' + R[f] + 'Width', !0, e)))
+ : ((g += n.css(a, 'padding' + R[f], !0, e)),
+ 'padding' !== c && (g += n.css(a, 'border' + R[f] + 'Width', !0, e)));
+ return g;
+ }
+ function Ib(a, b, c) {
+ var d = !0,
+ e = 'width' === b ? a.offsetWidth : a.offsetHeight,
+ f = wb(a),
+ g = 'border-box' === n.css(a, 'boxSizing', !1, f);
+ if (0 >= e || null == e) {
+ if (((e = xb(a, b, f)), (0 > e || null == e) && (e = a.style[b]), vb.test(e))) return e;
+ (d = g && (k.boxSizingReliable() || e === a.style[b])), (e = parseFloat(e) || 0);
+ }
+ return e + Hb(a, b, c || (g ? 'border' : 'content'), d, f) + 'px';
+ }
+ function Jb(a, b) {
+ for (var c, d, e, f = [], g = 0, h = a.length; h > g; g++)
+ (d = a[g]),
+ d.style &&
+ ((f[g] = L.get(d, 'olddisplay')),
+ (c = d.style.display),
+ b
+ ? (f[g] || 'none' !== c || (d.style.display = ''),
+ '' === d.style.display && S(d) && (f[g] = L.access(d, 'olddisplay', tb(d.nodeName))))
+ : ((e = S(d)), ('none' === c && e) || L.set(d, 'olddisplay', e ? c : n.css(d, 'display'))));
+ for (g = 0; h > g; g++)
+ (d = a[g]),
+ d.style &&
+ ((b && 'none' !== d.style.display && '' !== d.style.display) || (d.style.display = b ? f[g] || '' : 'none'));
+ return a;
+ }
+ n.extend({
+ cssHooks: {
+ opacity: {
+ get: function (a, b) {
+ if (b) {
+ var c = xb(a, 'opacity');
+ return '' === c ? '1' : c;
+ }
+ },
+ },
+ },
+ cssNumber: {
+ columnCount: !0,
+ fillOpacity: !0,
+ flexGrow: !0,
+ flexShrink: !0,
+ fontWeight: !0,
+ lineHeight: !0,
+ opacity: !0,
+ order: !0,
+ orphans: !0,
+ widows: !0,
+ zIndex: !0,
+ zoom: !0,
+ },
+ cssProps: { float: 'cssFloat' },
+ style: function (a, b, c, d) {
+ if (a && 3 !== a.nodeType && 8 !== a.nodeType && a.style) {
+ var e,
+ f,
+ g,
+ h = n.camelCase(b),
+ i = a.style;
+ return (
+ (b = n.cssProps[h] || (n.cssProps[h] = Fb(i, h))),
+ (g = n.cssHooks[b] || n.cssHooks[h]),
+ void 0 === c
+ ? g && 'get' in g && void 0 !== (e = g.get(a, !1, d))
+ ? e
+ : i[b]
+ : ((f = typeof c),
+ 'string' === f && (e = Bb.exec(c)) && ((c = (e[1] + 1) * e[2] + parseFloat(n.css(a, b))), (f = 'number')),
+ null != c &&
+ c === c &&
+ ('number' !== f || n.cssNumber[h] || (c += 'px'),
+ k.clearCloneStyle || '' !== c || 0 !== b.indexOf('background') || (i[b] = 'inherit'),
+ (g && 'set' in g && void 0 === (c = g.set(a, c, d))) || (i[b] = c)),
+ void 0)
+ );
+ }
+ },
+ css: function (a, b, c, d) {
+ var e,
+ f,
+ g,
+ h = n.camelCase(b);
+ return (
+ (b = n.cssProps[h] || (n.cssProps[h] = Fb(a.style, h))),
+ (g = n.cssHooks[b] || n.cssHooks[h]),
+ g && 'get' in g && (e = g.get(a, !0, c)),
+ void 0 === e && (e = xb(a, b, d)),
+ 'normal' === e && b in Db && (e = Db[b]),
+ '' === c || c ? ((f = parseFloat(e)), c === !0 || n.isNumeric(f) ? f || 0 : e) : e
+ );
+ },
+ }),
+ n.each(['height', 'width'], function (a, b) {
+ n.cssHooks[b] = {
+ get: function (a, c, d) {
+ return c
+ ? zb.test(n.css(a, 'display')) && 0 === a.offsetWidth
+ ? n.swap(a, Cb, function () {
+ return Ib(a, b, d);
+ })
+ : Ib(a, b, d)
+ : void 0;
+ },
+ set: function (a, c, d) {
+ var e = d && wb(a);
+ return Gb(a, c, d ? Hb(a, b, d, 'border-box' === n.css(a, 'boxSizing', !1, e), e) : 0);
+ },
+ };
+ }),
+ (n.cssHooks.marginRight = yb(k.reliableMarginRight, function (a, b) {
+ return b ? n.swap(a, { display: 'inline-block' }, xb, [a, 'marginRight']) : void 0;
+ })),
+ n.each({ margin: '', padding: '', border: 'Width' }, function (a, b) {
+ (n.cssHooks[a + b] = {
+ expand: function (c) {
+ for (var d = 0, e = {}, f = 'string' == typeof c ? c.split(' ') : [c]; 4 > d; d++)
+ e[a + R[d] + b] = f[d] || f[d - 2] || f[0];
+ return e;
+ },
+ }),
+ ub.test(a) || (n.cssHooks[a + b].set = Gb);
+ }),
+ n.fn.extend({
+ css: function (a, b) {
+ return J(
+ this,
+ function (a, b, c) {
+ var d,
+ e,
+ f = {},
+ g = 0;
+ if (n.isArray(b)) {
+ for (d = wb(a), e = b.length; e > g; g++) f[b[g]] = n.css(a, b[g], !1, d);
+ return f;
+ }
+ return void 0 !== c ? n.style(a, b, c) : n.css(a, b);
+ },
+ a,
+ b,
+ arguments.length > 1,
+ );
+ },
+ show: function () {
+ return Jb(this, !0);
+ },
+ hide: function () {
+ return Jb(this);
+ },
+ toggle: function (a) {
+ return 'boolean' == typeof a
+ ? a
+ ? this.show()
+ : this.hide()
+ : this.each(function () {
+ S(this) ? n(this).show() : n(this).hide();
+ });
+ },
+ });
+ function Kb(a, b, c, d, e) {
+ return new Kb.prototype.init(a, b, c, d, e);
+ }
+ (n.Tween = Kb),
+ (Kb.prototype = {
+ constructor: Kb,
+ init: function (a, b, c, d, e, f) {
+ (this.elem = a),
+ (this.prop = c),
+ (this.easing = e || 'swing'),
+ (this.options = b),
+ (this.start = this.now = this.cur()),
+ (this.end = d),
+ (this.unit = f || (n.cssNumber[c] ? '' : 'px'));
+ },
+ cur: function () {
+ var a = Kb.propHooks[this.prop];
+ return a && a.get ? a.get(this) : Kb.propHooks._default.get(this);
+ },
+ run: function (a) {
+ var b,
+ c = Kb.propHooks[this.prop];
+ return (
+ (this.pos = b =
+ this.options.duration
+ ? n.easing[this.easing](a, this.options.duration * a, 0, 1, this.options.duration)
+ : a),
+ (this.now = (this.end - this.start) * b + this.start),
+ this.options.step && this.options.step.call(this.elem, this.now, this),
+ c && c.set ? c.set(this) : Kb.propHooks._default.set(this),
+ this
+ );
+ },
+ }),
+ (Kb.prototype.init.prototype = Kb.prototype),
+ (Kb.propHooks = {
+ _default: {
+ get: function (a) {
+ var b;
+ return null == a.elem[a.prop] || (a.elem.style && null != a.elem.style[a.prop])
+ ? ((b = n.css(a.elem, a.prop, '')), b && 'auto' !== b ? b : 0)
+ : a.elem[a.prop];
+ },
+ set: function (a) {
+ n.fx.step[a.prop]
+ ? n.fx.step[a.prop](a)
+ : a.elem.style && (null != a.elem.style[n.cssProps[a.prop]] || n.cssHooks[a.prop])
+ ? n.style(a.elem, a.prop, a.now + a.unit)
+ : (a.elem[a.prop] = a.now);
+ },
+ },
+ }),
+ (Kb.propHooks.scrollTop = Kb.propHooks.scrollLeft =
+ {
+ set: function (a) {
+ a.elem.nodeType && a.elem.parentNode && (a.elem[a.prop] = a.now);
+ },
+ }),
+ (n.easing = {
+ linear: function (a) {
+ return a;
+ },
+ swing: function (a) {
+ return 0.5 - Math.cos(a * Math.PI) / 2;
+ },
+ }),
+ (n.fx = Kb.prototype.init),
+ (n.fx.step = {});
+ var Lb,
+ Mb,
+ Nb = /^(?:toggle|show|hide)$/,
+ Ob = new RegExp('^(?:([+-])=|)(' + Q + ')([a-z%]*)$', 'i'),
+ Pb = /queueHooks$/,
+ Qb = [Vb],
+ Rb = {
+ '*': [
+ function (a, b) {
+ var c = this.createTween(a, b),
+ d = c.cur(),
+ e = Ob.exec(b),
+ f = (e && e[3]) || (n.cssNumber[a] ? '' : 'px'),
+ g = (n.cssNumber[a] || ('px' !== f && +d)) && Ob.exec(n.css(c.elem, a)),
+ h = 1,
+ i = 20;
+ if (g && g[3] !== f) {
+ (f = f || g[3]), (e = e || []), (g = +d || 1);
+ do (h = h || '.5'), (g /= h), n.style(c.elem, a, g + f);
+ while (h !== (h = c.cur() / d) && 1 !== h && --i);
+ }
+ return e && ((g = c.start = +g || +d || 0), (c.unit = f), (c.end = e[1] ? g + (e[1] + 1) * e[2] : +e[2])), c;
+ },
+ ],
+ };
+ function Sb() {
+ return (
+ setTimeout(function () {
+ Lb = void 0;
+ }),
+ (Lb = n.now())
+ );
+ }
+ function Tb(a, b) {
+ var c,
+ d = 0,
+ e = { height: a };
+ for (b = b ? 1 : 0; 4 > d; d += 2 - b) (c = R[d]), (e['margin' + c] = e['padding' + c] = a);
+ return b && (e.opacity = e.width = a), e;
+ }
+ function Ub(a, b, c) {
+ for (var d, e = (Rb[b] || []).concat(Rb['*']), f = 0, g = e.length; g > f; f++)
+ if ((d = e[f].call(c, b, a))) return d;
+ }
+ function Vb(a, b, c) {
+ var d,
+ e,
+ f,
+ g,
+ h,
+ i,
+ j,
+ k,
+ l = this,
+ m = {},
+ o = a.style,
+ p = a.nodeType && S(a),
+ q = L.get(a, 'fxshow');
+ c.queue ||
+ ((h = n._queueHooks(a, 'fx')),
+ null == h.unqueued &&
+ ((h.unqueued = 0),
+ (i = h.empty.fire),
+ (h.empty.fire = function () {
+ h.unqueued || i();
+ })),
+ h.unqueued++,
+ l.always(function () {
+ l.always(function () {
+ h.unqueued--, n.queue(a, 'fx').length || h.empty.fire();
+ });
+ })),
+ 1 === a.nodeType &&
+ ('height' in b || 'width' in b) &&
+ ((c.overflow = [o.overflow, o.overflowX, o.overflowY]),
+ (j = n.css(a, 'display')),
+ (k = 'none' === j ? L.get(a, 'olddisplay') || tb(a.nodeName) : j),
+ 'inline' === k && 'none' === n.css(a, 'float') && (o.display = 'inline-block')),
+ c.overflow &&
+ ((o.overflow = 'hidden'),
+ l.always(function () {
+ (o.overflow = c.overflow[0]), (o.overflowX = c.overflow[1]), (o.overflowY = c.overflow[2]);
+ }));
+ for (d in b)
+ if (((e = b[d]), Nb.exec(e))) {
+ if ((delete b[d], (f = f || 'toggle' === e), e === (p ? 'hide' : 'show'))) {
+ if ('show' !== e || !q || void 0 === q[d]) continue;
+ p = !0;
+ }
+ m[d] = (q && q[d]) || n.style(a, d);
+ } else j = void 0;
+ if (n.isEmptyObject(m)) 'inline' === ('none' === j ? tb(a.nodeName) : j) && (o.display = j);
+ else {
+ q ? 'hidden' in q && (p = q.hidden) : (q = L.access(a, 'fxshow', {})),
+ f && (q.hidden = !p),
+ p
+ ? n(a).show()
+ : l.done(function () {
+ n(a).hide();
+ }),
+ l.done(function () {
+ var b;
+ L.remove(a, 'fxshow');
+ for (b in m) n.style(a, b, m[b]);
+ });
+ for (d in m)
+ (g = Ub(p ? q[d] : 0, d, l)),
+ d in q || ((q[d] = g.start), p && ((g.end = g.start), (g.start = 'width' === d || 'height' === d ? 1 : 0)));
+ }
+ }
+ function Wb(a, b) {
+ var c, d, e, f, g;
+ for (c in a)
+ if (
+ ((d = n.camelCase(c)),
+ (e = b[d]),
+ (f = a[c]),
+ n.isArray(f) && ((e = f[1]), (f = a[c] = f[0])),
+ c !== d && ((a[d] = f), delete a[c]),
+ (g = n.cssHooks[d]),
+ g && 'expand' in g)
+ ) {
+ (f = g.expand(f)), delete a[d];
+ for (c in f) c in a || ((a[c] = f[c]), (b[c] = e));
+ } else b[d] = e;
+ }
+ function Xb(a, b, c) {
+ var d,
+ e,
+ f = 0,
+ g = Qb.length,
+ h = n.Deferred().always(function () {
+ delete i.elem;
+ }),
+ i = function () {
+ if (e) return !1;
+ for (
+ var b = Lb || Sb(),
+ c = Math.max(0, j.startTime + j.duration - b),
+ d = c / j.duration || 0,
+ f = 1 - d,
+ g = 0,
+ i = j.tweens.length;
+ i > g;
+ g++
+ )
+ j.tweens[g].run(f);
+ return h.notifyWith(a, [j, f, c]), 1 > f && i ? c : (h.resolveWith(a, [j]), !1);
+ },
+ j = h.promise({
+ elem: a,
+ props: n.extend({}, b),
+ opts: n.extend(!0, { specialEasing: {} }, c),
+ originalProperties: b,
+ originalOptions: c,
+ startTime: Lb || Sb(),
+ duration: c.duration,
+ tweens: [],
+ createTween: function (b, c) {
+ var d = n.Tween(a, j.opts, b, c, j.opts.specialEasing[b] || j.opts.easing);
+ return j.tweens.push(d), d;
+ },
+ stop: function (b) {
+ var c = 0,
+ d = b ? j.tweens.length : 0;
+ if (e) return this;
+ for (e = !0; d > c; c++) j.tweens[c].run(1);
+ return b ? h.resolveWith(a, [j, b]) : h.rejectWith(a, [j, b]), this;
+ },
+ }),
+ k = j.props;
+ for (Wb(k, j.opts.specialEasing); g > f; f++) if ((d = Qb[f].call(j, a, k, j.opts))) return d;
+ return (
+ n.map(k, Ub, j),
+ n.isFunction(j.opts.start) && j.opts.start.call(a, j),
+ n.fx.timer(n.extend(i, { elem: a, anim: j, queue: j.opts.queue })),
+ j.progress(j.opts.progress).done(j.opts.done, j.opts.complete).fail(j.opts.fail).always(j.opts.always)
+ );
+ }
+ (n.Animation = n.extend(Xb, {
+ tweener: function (a, b) {
+ n.isFunction(a) ? ((b = a), (a = ['*'])) : (a = a.split(' '));
+ for (var c, d = 0, e = a.length; e > d; d++) (c = a[d]), (Rb[c] = Rb[c] || []), Rb[c].unshift(b);
+ },
+ prefilter: function (a, b) {
+ b ? Qb.unshift(a) : Qb.push(a);
+ },
+ })),
+ (n.speed = function (a, b, c) {
+ var d =
+ a && 'object' == typeof a
+ ? n.extend({}, a)
+ : {
+ complete: c || (!c && b) || (n.isFunction(a) && a),
+ duration: a,
+ easing: (c && b) || (b && !n.isFunction(b) && b),
+ };
+ return (
+ (d.duration = n.fx.off
+ ? 0
+ : 'number' == typeof d.duration
+ ? d.duration
+ : d.duration in n.fx.speeds
+ ? n.fx.speeds[d.duration]
+ : n.fx.speeds._default),
+ (null == d.queue || d.queue === !0) && (d.queue = 'fx'),
+ (d.old = d.complete),
+ (d.complete = function () {
+ n.isFunction(d.old) && d.old.call(this), d.queue && n.dequeue(this, d.queue);
+ }),
+ d
+ );
+ }),
+ n.fn.extend({
+ fadeTo: function (a, b, c, d) {
+ return this.filter(S).css('opacity', 0).show().end().animate({ opacity: b }, a, c, d);
+ },
+ animate: function (a, b, c, d) {
+ var e = n.isEmptyObject(a),
+ f = n.speed(b, c, d),
+ g = function () {
+ var b = Xb(this, n.extend({}, a), f);
+ (e || L.get(this, 'finish')) && b.stop(!0);
+ };
+ return (g.finish = g), e || f.queue === !1 ? this.each(g) : this.queue(f.queue, g);
+ },
+ stop: function (a, b, c) {
+ var d = function (a) {
+ var b = a.stop;
+ delete a.stop, b(c);
+ };
+ return (
+ 'string' != typeof a && ((c = b), (b = a), (a = void 0)),
+ b && a !== !1 && this.queue(a || 'fx', []),
+ this.each(function () {
+ var b = !0,
+ e = null != a && a + 'queueHooks',
+ f = n.timers,
+ g = L.get(this);
+ if (e) g[e] && g[e].stop && d(g[e]);
+ else for (e in g) g[e] && g[e].stop && Pb.test(e) && d(g[e]);
+ for (e = f.length; e--; )
+ f[e].elem !== this || (null != a && f[e].queue !== a) || (f[e].anim.stop(c), (b = !1), f.splice(e, 1));
+ (b || !c) && n.dequeue(this, a);
+ })
+ );
+ },
+ finish: function (a) {
+ return (
+ a !== !1 && (a = a || 'fx'),
+ this.each(function () {
+ var b,
+ c = L.get(this),
+ d = c[a + 'queue'],
+ e = c[a + 'queueHooks'],
+ f = n.timers,
+ g = d ? d.length : 0;
+ for (c.finish = !0, n.queue(this, a, []), e && e.stop && e.stop.call(this, !0), b = f.length; b--; )
+ f[b].elem === this && f[b].queue === a && (f[b].anim.stop(!0), f.splice(b, 1));
+ for (b = 0; g > b; b++) d[b] && d[b].finish && d[b].finish.call(this);
+ delete c.finish;
+ })
+ );
+ },
+ }),
+ n.each(['toggle', 'show', 'hide'], function (a, b) {
+ var c = n.fn[b];
+ n.fn[b] = function (a, d, e) {
+ return null == a || 'boolean' == typeof a ? c.apply(this, arguments) : this.animate(Tb(b, !0), a, d, e);
+ };
+ }),
+ n.each(
+ {
+ slideDown: Tb('show'),
+ slideUp: Tb('hide'),
+ slideToggle: Tb('toggle'),
+ fadeIn: { opacity: 'show' },
+ fadeOut: { opacity: 'hide' },
+ fadeToggle: { opacity: 'toggle' },
+ },
+ function (a, b) {
+ n.fn[a] = function (a, c, d) {
+ return this.animate(b, a, c, d);
+ };
+ },
+ ),
+ (n.timers = []),
+ (n.fx.tick = function () {
+ var a,
+ b = 0,
+ c = n.timers;
+ for (Lb = n.now(); b < c.length; b++) (a = c[b]), a() || c[b] !== a || c.splice(b--, 1);
+ c.length || n.fx.stop(), (Lb = void 0);
+ }),
+ (n.fx.timer = function (a) {
+ n.timers.push(a), a() ? n.fx.start() : n.timers.pop();
+ }),
+ (n.fx.interval = 13),
+ (n.fx.start = function () {
+ Mb || (Mb = setInterval(n.fx.tick, n.fx.interval));
+ }),
+ (n.fx.stop = function () {
+ clearInterval(Mb), (Mb = null);
+ }),
+ (n.fx.speeds = { slow: 600, fast: 200, _default: 400 }),
+ (n.fn.delay = function (a, b) {
+ return (
+ (a = n.fx ? n.fx.speeds[a] || a : a),
+ (b = b || 'fx'),
+ this.queue(b, function (b, c) {
+ var d = setTimeout(b, a);
+ c.stop = function () {
+ clearTimeout(d);
+ };
+ })
+ );
+ }),
+ (function () {
+ var a = l.createElement('input'),
+ b = l.createElement('select'),
+ c = b.appendChild(l.createElement('option'));
+ (a.type = 'checkbox'),
+ (k.checkOn = '' !== a.value),
+ (k.optSelected = c.selected),
+ (b.disabled = !0),
+ (k.optDisabled = !c.disabled),
+ (a = l.createElement('input')),
+ (a.value = 't'),
+ (a.type = 'radio'),
+ (k.radioValue = 't' === a.value);
+ })();
+ var Yb,
+ Zb,
+ $b = n.expr.attrHandle;
+ n.fn.extend({
+ attr: function (a, b) {
+ return J(this, n.attr, a, b, arguments.length > 1);
+ },
+ removeAttr: function (a) {
+ return this.each(function () {
+ n.removeAttr(this, a);
+ });
+ },
+ }),
+ n.extend({
+ attr: function (a, b, c) {
+ var d,
+ e,
+ f = a.nodeType;
+ if (a && 3 !== f && 8 !== f && 2 !== f)
+ return typeof a.getAttribute === U
+ ? n.prop(a, b, c)
+ : ((1 === f && n.isXMLDoc(a)) ||
+ ((b = b.toLowerCase()), (d = n.attrHooks[b] || (n.expr.match.bool.test(b) ? Zb : Yb))),
+ void 0 === c
+ ? d && 'get' in d && null !== (e = d.get(a, b))
+ ? e
+ : ((e = n.find.attr(a, b)), null == e ? void 0 : e)
+ : null !== c
+ ? d && 'set' in d && void 0 !== (e = d.set(a, c, b))
+ ? e
+ : (a.setAttribute(b, c + ''), c)
+ : void n.removeAttr(a, b));
+ },
+ removeAttr: function (a, b) {
+ var c,
+ d,
+ e = 0,
+ f = b && b.match(E);
+ if (f && 1 === a.nodeType)
+ while ((c = f[e++])) (d = n.propFix[c] || c), n.expr.match.bool.test(c) && (a[d] = !1), a.removeAttribute(c);
+ },
+ attrHooks: {
+ type: {
+ set: function (a, b) {
+ if (!k.radioValue && 'radio' === b && n.nodeName(a, 'input')) {
+ var c = a.value;
+ return a.setAttribute('type', b), c && (a.value = c), b;
+ }
+ },
+ },
+ },
+ }),
+ (Zb = {
+ set: function (a, b, c) {
+ return b === !1 ? n.removeAttr(a, c) : a.setAttribute(c, c), c;
+ },
+ }),
+ n.each(n.expr.match.bool.source.match(/\w+/g), function (a, b) {
+ var c = $b[b] || n.find.attr;
+ $b[b] = function (a, b, d) {
+ var e, f;
+ return d || ((f = $b[b]), ($b[b] = e), (e = null != c(a, b, d) ? b.toLowerCase() : null), ($b[b] = f)), e;
+ };
+ });
+ var _b = /^(?:input|select|textarea|button)$/i;
+ n.fn.extend({
+ prop: function (a, b) {
+ return J(this, n.prop, a, b, arguments.length > 1);
+ },
+ removeProp: function (a) {
+ return this.each(function () {
+ delete this[n.propFix[a] || a];
+ });
+ },
+ }),
+ n.extend({
+ propFix: { for: 'htmlFor', class: 'className' },
+ prop: function (a, b, c) {
+ var d,
+ e,
+ f,
+ g = a.nodeType;
+ if (a && 3 !== g && 8 !== g && 2 !== g)
+ return (
+ (f = 1 !== g || !n.isXMLDoc(a)),
+ f && ((b = n.propFix[b] || b), (e = n.propHooks[b])),
+ void 0 !== c
+ ? e && 'set' in e && void 0 !== (d = e.set(a, c, b))
+ ? d
+ : (a[b] = c)
+ : e && 'get' in e && null !== (d = e.get(a, b))
+ ? d
+ : a[b]
+ );
+ },
+ propHooks: {
+ tabIndex: {
+ get: function (a) {
+ return a.hasAttribute('tabindex') || _b.test(a.nodeName) || a.href ? a.tabIndex : -1;
+ },
+ },
+ },
+ }),
+ k.optSelected ||
+ (n.propHooks.selected = {
+ get: function (a) {
+ var b = a.parentNode;
+ return b && b.parentNode && b.parentNode.selectedIndex, null;
+ },
+ }),
+ n.each(
+ [
+ 'tabIndex',
+ 'readOnly',
+ 'maxLength',
+ 'cellSpacing',
+ 'cellPadding',
+ 'rowSpan',
+ 'colSpan',
+ 'useMap',
+ 'frameBorder',
+ 'contentEditable',
+ ],
+ function () {
+ n.propFix[this.toLowerCase()] = this;
+ },
+ );
+ var ac = /[\t\r\n\f]/g;
+ n.fn.extend({
+ addClass: function (a) {
+ var b,
+ c,
+ d,
+ e,
+ f,
+ g,
+ h = 'string' == typeof a && a,
+ i = 0,
+ j = this.length;
+ if (n.isFunction(a))
+ return this.each(function (b) {
+ n(this).addClass(a.call(this, b, this.className));
+ });
+ if (h)
+ for (b = (a || '').match(E) || []; j > i; i++)
+ if (
+ ((c = this[i]), (d = 1 === c.nodeType && (c.className ? (' ' + c.className + ' ').replace(ac, ' ') : ' ')))
+ ) {
+ f = 0;
+ while ((e = b[f++])) d.indexOf(' ' + e + ' ') < 0 && (d += e + ' ');
+ (g = n.trim(d)), c.className !== g && (c.className = g);
+ }
+ return this;
+ },
+ removeClass: function (a) {
+ var b,
+ c,
+ d,
+ e,
+ f,
+ g,
+ h = 0 === arguments.length || ('string' == typeof a && a),
+ i = 0,
+ j = this.length;
+ if (n.isFunction(a))
+ return this.each(function (b) {
+ n(this).removeClass(a.call(this, b, this.className));
+ });
+ if (h)
+ for (b = (a || '').match(E) || []; j > i; i++)
+ if (
+ ((c = this[i]), (d = 1 === c.nodeType && (c.className ? (' ' + c.className + ' ').replace(ac, ' ') : '')))
+ ) {
+ f = 0;
+ while ((e = b[f++])) while (d.indexOf(' ' + e + ' ') >= 0) d = d.replace(' ' + e + ' ', ' ');
+ (g = a ? n.trim(d) : ''), c.className !== g && (c.className = g);
+ }
+ return this;
+ },
+ toggleClass: function (a, b) {
+ var c = typeof a;
+ return 'boolean' == typeof b && 'string' === c
+ ? b
+ ? this.addClass(a)
+ : this.removeClass(a)
+ : this.each(
+ n.isFunction(a)
+ ? function (c) {
+ n(this).toggleClass(a.call(this, c, this.className, b), b);
+ }
+ : function () {
+ if ('string' === c) {
+ var b,
+ d = 0,
+ e = n(this),
+ f = a.match(E) || [];
+ while ((b = f[d++])) e.hasClass(b) ? e.removeClass(b) : e.addClass(b);
+ } else
+ (c === U || 'boolean' === c) &&
+ (this.className && L.set(this, '__className__', this.className),
+ (this.className = this.className || a === !1 ? '' : L.get(this, '__className__') || ''));
+ },
+ );
+ },
+ hasClass: function (a) {
+ for (var b = ' ' + a + ' ', c = 0, d = this.length; d > c; c++)
+ if (1 === this[c].nodeType && (' ' + this[c].className + ' ').replace(ac, ' ').indexOf(b) >= 0) return !0;
+ return !1;
+ },
+ });
+ var bc = /\r/g;
+ n.fn.extend({
+ val: function (a) {
+ var b,
+ c,
+ d,
+ e = this[0];
+ {
+ if (arguments.length)
+ return (
+ (d = n.isFunction(a)),
+ this.each(function (c) {
+ var e;
+ 1 === this.nodeType &&
+ ((e = d ? a.call(this, c, n(this).val()) : a),
+ null == e
+ ? (e = '')
+ : 'number' == typeof e
+ ? (e += '')
+ : n.isArray(e) &&
+ (e = n.map(e, function (a) {
+ return null == a ? '' : a + '';
+ })),
+ (b = n.valHooks[this.type] || n.valHooks[this.nodeName.toLowerCase()]),
+ (b && 'set' in b && void 0 !== b.set(this, e, 'value')) || (this.value = e));
+ })
+ );
+ if (e)
+ return (
+ (b = n.valHooks[e.type] || n.valHooks[e.nodeName.toLowerCase()]),
+ b && 'get' in b && void 0 !== (c = b.get(e, 'value'))
+ ? c
+ : ((c = e.value), 'string' == typeof c ? c.replace(bc, '') : null == c ? '' : c)
+ );
+ }
+ },
+ }),
+ n.extend({
+ valHooks: {
+ option: {
+ get: function (a) {
+ var b = n.find.attr(a, 'value');
+ return null != b ? b : n.trim(n.text(a));
+ },
+ },
+ select: {
+ get: function (a) {
+ for (
+ var b,
+ c,
+ d = a.options,
+ e = a.selectedIndex,
+ f = 'select-one' === a.type || 0 > e,
+ g = f ? null : [],
+ h = f ? e + 1 : d.length,
+ i = 0 > e ? h : f ? e : 0;
+ h > i;
+ i++
+ )
+ if (
+ ((c = d[i]),
+ !(
+ (!c.selected && i !== e) ||
+ (k.optDisabled ? c.disabled : null !== c.getAttribute('disabled')) ||
+ (c.parentNode.disabled && n.nodeName(c.parentNode, 'optgroup'))
+ ))
+ ) {
+ if (((b = n(c).val()), f)) return b;
+ g.push(b);
+ }
+ return g;
+ },
+ set: function (a, b) {
+ var c,
+ d,
+ e = a.options,
+ f = n.makeArray(b),
+ g = e.length;
+ while (g--) (d = e[g]), (d.selected = n.inArray(d.value, f) >= 0) && (c = !0);
+ return c || (a.selectedIndex = -1), f;
+ },
+ },
+ },
+ }),
+ n.each(['radio', 'checkbox'], function () {
+ (n.valHooks[this] = {
+ set: function (a, b) {
+ return n.isArray(b) ? (a.checked = n.inArray(n(a).val(), b) >= 0) : void 0;
+ },
+ }),
+ k.checkOn ||
+ (n.valHooks[this].get = function (a) {
+ return null === a.getAttribute('value') ? 'on' : a.value;
+ });
+ }),
+ n.each(
+ 'blur focus focusin focusout load resize scroll unload click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup error contextmenu'.split(
+ ' ',
+ ),
+ function (a, b) {
+ n.fn[b] = function (a, c) {
+ return arguments.length > 0 ? this.on(b, null, a, c) : this.trigger(b);
+ };
+ },
+ ),
+ n.fn.extend({
+ hover: function (a, b) {
+ return this.mouseenter(a).mouseleave(b || a);
+ },
+ bind: function (a, b, c) {
+ return this.on(a, null, b, c);
+ },
+ unbind: function (a, b) {
+ return this.off(a, null, b);
+ },
+ delegate: function (a, b, c, d) {
+ return this.on(b, a, c, d);
+ },
+ undelegate: function (a, b, c) {
+ return 1 === arguments.length ? this.off(a, '**') : this.off(b, a || '**', c);
+ },
+ });
+ var cc = n.now(),
+ dc = /\?/;
+ (n.parseJSON = function (a) {
+ return JSON.parse(a + '');
+ }),
+ (n.parseXML = function (a) {
+ var b, c;
+ if (!a || 'string' != typeof a) return null;
+ try {
+ (c = new DOMParser()), (b = c.parseFromString(a, 'text/xml'));
+ } catch (d) {
+ b = void 0;
+ }
+ return (!b || b.getElementsByTagName('parsererror').length) && n.error('Invalid XML: ' + a), b;
+ });
+ var ec = /#.*$/,
+ fc = /([?&])_=[^&]*/,
+ gc = /^(.*?):[ \t]*([^\r\n]*)$/gm,
+ hc = /^(?:about|app|app-storage|.+-extension|file|res|widget):$/,
+ ic = /^(?:GET|HEAD)$/,
+ jc = /^\/\//,
+ kc = /^([\w.+-]+:)(?:\/\/(?:[^\/?#]*@|)([^\/?#:]*)(?::(\d+)|)|)/,
+ lc = {},
+ mc = {},
+ nc = '*/'.concat('*'),
+ oc = a.location.href,
+ pc = kc.exec(oc.toLowerCase()) || [];
+ function qc(a) {
+ return function (b, c) {
+ 'string' != typeof b && ((c = b), (b = '*'));
+ var d,
+ e = 0,
+ f = b.toLowerCase().match(E) || [];
+ if (n.isFunction(c))
+ while ((d = f[e++]))
+ '+' === d[0] ? ((d = d.slice(1) || '*'), (a[d] = a[d] || []).unshift(c)) : (a[d] = a[d] || []).push(c);
+ };
+ }
+ function rc(a, b, c, d) {
+ var e = {},
+ f = a === mc;
+ function g(h) {
+ var i;
+ return (
+ (e[h] = !0),
+ n.each(a[h] || [], function (a, h) {
+ var j = h(b, c, d);
+ return 'string' != typeof j || f || e[j] ? (f ? !(i = j) : void 0) : (b.dataTypes.unshift(j), g(j), !1);
+ }),
+ i
+ );
+ }
+ return g(b.dataTypes[0]) || (!e['*'] && g('*'));
+ }
+ function sc(a, b) {
+ var c,
+ d,
+ e = n.ajaxSettings.flatOptions || {};
+ for (c in b) void 0 !== b[c] && ((e[c] ? a : d || (d = {}))[c] = b[c]);
+ return d && n.extend(!0, a, d), a;
+ }
+ function tc(a, b, c) {
+ var d,
+ e,
+ f,
+ g,
+ h = a.contents,
+ i = a.dataTypes;
+ while ('*' === i[0]) i.shift(), void 0 === d && (d = a.mimeType || b.getResponseHeader('Content-Type'));
+ if (d)
+ for (e in h)
+ if (h[e] && h[e].test(d)) {
+ i.unshift(e);
+ break;
+ }
+ if (i[0] in c) f = i[0];
+ else {
+ for (e in c) {
+ if (!i[0] || a.converters[e + ' ' + i[0]]) {
+ f = e;
+ break;
+ }
+ g || (g = e);
+ }
+ f = f || g;
+ }
+ return f ? (f !== i[0] && i.unshift(f), c[f]) : void 0;
+ }
+ function uc(a, b, c, d) {
+ var e,
+ f,
+ g,
+ h,
+ i,
+ j = {},
+ k = a.dataTypes.slice();
+ if (k[1]) for (g in a.converters) j[g.toLowerCase()] = a.converters[g];
+ f = k.shift();
+ while (f)
+ if (
+ (a.responseFields[f] && (c[a.responseFields[f]] = b),
+ !i && d && a.dataFilter && (b = a.dataFilter(b, a.dataType)),
+ (i = f),
+ (f = k.shift()))
+ )
+ if ('*' === f) f = i;
+ else if ('*' !== i && i !== f) {
+ if (((g = j[i + ' ' + f] || j['* ' + f]), !g))
+ for (e in j)
+ if (((h = e.split(' ')), h[1] === f && (g = j[i + ' ' + h[0]] || j['* ' + h[0]]))) {
+ g === !0 ? (g = j[e]) : j[e] !== !0 && ((f = h[0]), k.unshift(h[1]));
+ break;
+ }
+ if (g !== !0)
+ if (g && a['throws']) b = g(b);
+ else
+ try {
+ b = g(b);
+ } catch (l) {
+ return { state: 'parsererror', error: g ? l : 'No conversion from ' + i + ' to ' + f };
+ }
+ }
+ return { state: 'success', data: b };
+ }
+ n.extend({
+ active: 0,
+ lastModified: {},
+ etag: {},
+ ajaxSettings: {
+ url: oc,
+ type: 'GET',
+ isLocal: hc.test(pc[1]),
+ global: !0,
+ processData: !0,
+ async: !0,
+ contentType: 'application/x-www-form-urlencoded; charset=UTF-8',
+ accepts: {
+ '*': nc,
+ text: 'text/plain',
+ html: 'text/html',
+ xml: 'application/xml, text/xml',
+ json: 'application/json, text/javascript',
+ },
+ contents: { xml: /xml/, html: /html/, json: /json/ },
+ responseFields: { xml: 'responseXML', text: 'responseText', json: 'responseJSON' },
+ converters: { '* text': String, 'text html': !0, 'text json': n.parseJSON, 'text xml': n.parseXML },
+ flatOptions: { url: !0, context: !0 },
+ },
+ ajaxSetup: function (a, b) {
+ return b ? sc(sc(a, n.ajaxSettings), b) : sc(n.ajaxSettings, a);
+ },
+ ajaxPrefilter: qc(lc),
+ ajaxTransport: qc(mc),
+ ajax: function (a, b) {
+ 'object' == typeof a && ((b = a), (a = void 0)), (b = b || {});
+ var c,
+ d,
+ e,
+ f,
+ g,
+ h,
+ i,
+ j,
+ k = n.ajaxSetup({}, b),
+ l = k.context || k,
+ m = k.context && (l.nodeType || l.jquery) ? n(l) : n.event,
+ o = n.Deferred(),
+ p = n.Callbacks('once memory'),
+ q = k.statusCode || {},
+ r = {},
+ s = {},
+ t = 0,
+ u = 'canceled',
+ v = {
+ readyState: 0,
+ getResponseHeader: function (a) {
+ var b;
+ if (2 === t) {
+ if (!f) {
+ f = {};
+ while ((b = gc.exec(e))) f[b[1].toLowerCase()] = b[2];
+ }
+ b = f[a.toLowerCase()];
+ }
+ return null == b ? null : b;
+ },
+ getAllResponseHeaders: function () {
+ return 2 === t ? e : null;
+ },
+ setRequestHeader: function (a, b) {
+ var c = a.toLowerCase();
+ return t || ((a = s[c] = s[c] || a), (r[a] = b)), this;
+ },
+ overrideMimeType: function (a) {
+ return t || (k.mimeType = a), this;
+ },
+ statusCode: function (a) {
+ var b;
+ if (a)
+ if (2 > t) for (b in a) q[b] = [q[b], a[b]];
+ else v.always(a[v.status]);
+ return this;
+ },
+ abort: function (a) {
+ var b = a || u;
+ return c && c.abort(b), x(0, b), this;
+ },
+ };
+ if (
+ ((o.promise(v).complete = p.add),
+ (v.success = v.done),
+ (v.error = v.fail),
+ (k.url = ((a || k.url || oc) + '').replace(ec, '').replace(jc, pc[1] + '//')),
+ (k.type = b.method || b.type || k.method || k.type),
+ (k.dataTypes = n
+ .trim(k.dataType || '*')
+ .toLowerCase()
+ .match(E) || ['']),
+ null == k.crossDomain &&
+ ((h = kc.exec(k.url.toLowerCase())),
+ (k.crossDomain = !(
+ !h ||
+ (h[1] === pc[1] &&
+ h[2] === pc[2] &&
+ (h[3] || ('http:' === h[1] ? '80' : '443')) === (pc[3] || ('http:' === pc[1] ? '80' : '443')))
+ ))),
+ k.data && k.processData && 'string' != typeof k.data && (k.data = n.param(k.data, k.traditional)),
+ rc(lc, k, b, v),
+ 2 === t)
+ )
+ return v;
+ (i = n.event && k.global),
+ i && 0 === n.active++ && n.event.trigger('ajaxStart'),
+ (k.type = k.type.toUpperCase()),
+ (k.hasContent = !ic.test(k.type)),
+ (d = k.url),
+ k.hasContent ||
+ (k.data && ((d = k.url += (dc.test(d) ? '&' : '?') + k.data), delete k.data),
+ k.cache === !1 &&
+ (k.url = fc.test(d) ? d.replace(fc, '$1_=' + cc++) : d + (dc.test(d) ? '&' : '?') + '_=' + cc++)),
+ k.ifModified &&
+ (n.lastModified[d] && v.setRequestHeader('If-Modified-Since', n.lastModified[d]),
+ n.etag[d] && v.setRequestHeader('If-None-Match', n.etag[d])),
+ ((k.data && k.hasContent && k.contentType !== !1) || b.contentType) &&
+ v.setRequestHeader('Content-Type', k.contentType),
+ v.setRequestHeader(
+ 'Accept',
+ k.dataTypes[0] && k.accepts[k.dataTypes[0]]
+ ? k.accepts[k.dataTypes[0]] + ('*' !== k.dataTypes[0] ? ', ' + nc + '; q=0.01' : '')
+ : k.accepts['*'],
+ );
+ for (j in k.headers) v.setRequestHeader(j, k.headers[j]);
+ if (k.beforeSend && (k.beforeSend.call(l, v, k) === !1 || 2 === t)) return v.abort();
+ u = 'abort';
+ for (j in { success: 1, error: 1, complete: 1 }) v[j](k[j]);
+ if ((c = rc(mc, k, b, v))) {
+ (v.readyState = 1),
+ i && m.trigger('ajaxSend', [v, k]),
+ k.async &&
+ k.timeout > 0 &&
+ (g = setTimeout(function () {
+ v.abort('timeout');
+ }, k.timeout));
+ try {
+ (t = 1), c.send(r, x);
+ } catch (w) {
+ if (!(2 > t)) throw w;
+ x(-1, w);
+ }
+ } else x(-1, 'No Transport');
+ function x(a, b, f, h) {
+ var j,
+ r,
+ s,
+ u,
+ w,
+ x = b;
+ 2 !== t &&
+ ((t = 2),
+ g && clearTimeout(g),
+ (c = void 0),
+ (e = h || ''),
+ (v.readyState = a > 0 ? 4 : 0),
+ (j = (a >= 200 && 300 > a) || 304 === a),
+ f && (u = tc(k, v, f)),
+ (u = uc(k, u, v, j)),
+ j
+ ? (k.ifModified &&
+ ((w = v.getResponseHeader('Last-Modified')),
+ w && (n.lastModified[d] = w),
+ (w = v.getResponseHeader('etag')),
+ w && (n.etag[d] = w)),
+ 204 === a || 'HEAD' === k.type
+ ? (x = 'nocontent')
+ : 304 === a
+ ? (x = 'notmodified')
+ : ((x = u.state), (r = u.data), (s = u.error), (j = !s)))
+ : ((s = x), (a || !x) && ((x = 'error'), 0 > a && (a = 0))),
+ (v.status = a),
+ (v.statusText = (b || x) + ''),
+ j ? o.resolveWith(l, [r, x, v]) : o.rejectWith(l, [v, x, s]),
+ v.statusCode(q),
+ (q = void 0),
+ i && m.trigger(j ? 'ajaxSuccess' : 'ajaxError', [v, k, j ? r : s]),
+ p.fireWith(l, [v, x]),
+ i && (m.trigger('ajaxComplete', [v, k]), --n.active || n.event.trigger('ajaxStop')));
+ }
+ return v;
+ },
+ getJSON: function (a, b, c) {
+ return n.get(a, b, c, 'json');
+ },
+ getScript: function (a, b) {
+ return n.get(a, void 0, b, 'script');
+ },
+ }),
+ n.each(['get', 'post'], function (a, b) {
+ n[b] = function (a, c, d, e) {
+ return (
+ n.isFunction(c) && ((e = e || d), (d = c), (c = void 0)),
+ n.ajax({ url: a, type: b, dataType: e, data: c, success: d })
+ );
+ };
+ }),
+ (n._evalUrl = function (a) {
+ return n.ajax({ url: a, type: 'GET', dataType: 'script', async: !1, global: !1, throws: !0 });
+ }),
+ n.fn.extend({
+ wrapAll: function (a) {
+ var b;
+ return n.isFunction(a)
+ ? this.each(function (b) {
+ n(this).wrapAll(a.call(this, b));
+ })
+ : (this[0] &&
+ ((b = n(a, this[0].ownerDocument).eq(0).clone(!0)),
+ this[0].parentNode && b.insertBefore(this[0]),
+ b
+ .map(function () {
+ var a = this;
+ while (a.firstElementChild) a = a.firstElementChild;
+ return a;
+ })
+ .append(this)),
+ this);
+ },
+ wrapInner: function (a) {
+ return this.each(
+ n.isFunction(a)
+ ? function (b) {
+ n(this).wrapInner(a.call(this, b));
+ }
+ : function () {
+ var b = n(this),
+ c = b.contents();
+ c.length ? c.wrapAll(a) : b.append(a);
+ },
+ );
+ },
+ wrap: function (a) {
+ var b = n.isFunction(a);
+ return this.each(function (c) {
+ n(this).wrapAll(b ? a.call(this, c) : a);
+ });
+ },
+ unwrap: function () {
+ return this.parent()
+ .each(function () {
+ n.nodeName(this, 'body') || n(this).replaceWith(this.childNodes);
+ })
+ .end();
+ },
+ }),
+ (n.expr.filters.hidden = function (a) {
+ return a.offsetWidth <= 0 && a.offsetHeight <= 0;
+ }),
+ (n.expr.filters.visible = function (a) {
+ return !n.expr.filters.hidden(a);
+ });
+ var vc = /%20/g,
+ wc = /\[\]$/,
+ xc = /\r?\n/g,
+ yc = /^(?:submit|button|image|reset|file)$/i,
+ zc = /^(?:input|select|textarea|keygen)/i;
+ function Ac(a, b, c, d) {
+ var e;
+ if (n.isArray(b))
+ n.each(b, function (b, e) {
+ c || wc.test(a) ? d(a, e) : Ac(a + '[' + ('object' == typeof e ? b : '') + ']', e, c, d);
+ });
+ else if (c || 'object' !== n.type(b)) d(a, b);
+ else for (e in b) Ac(a + '[' + e + ']', b[e], c, d);
+ }
+ (n.param = function (a, b) {
+ var c,
+ d = [],
+ e = function (a, b) {
+ (b = n.isFunction(b) ? b() : null == b ? '' : b),
+ (d[d.length] = encodeURIComponent(a) + '=' + encodeURIComponent(b));
+ };
+ if (
+ (void 0 === b && (b = n.ajaxSettings && n.ajaxSettings.traditional),
+ n.isArray(a) || (a.jquery && !n.isPlainObject(a)))
+ )
+ n.each(a, function () {
+ e(this.name, this.value);
+ });
+ else for (c in a) Ac(c, a[c], b, e);
+ return d.join('&').replace(vc, '+');
+ }),
+ n.fn.extend({
+ serialize: function () {
+ return n.param(this.serializeArray());
+ },
+ serializeArray: function () {
+ return this.map(function () {
+ var a = n.prop(this, 'elements');
+ return a ? n.makeArray(a) : this;
+ })
+ .filter(function () {
+ var a = this.type;
+ return (
+ this.name &&
+ !n(this).is(':disabled') &&
+ zc.test(this.nodeName) &&
+ !yc.test(a) &&
+ (this.checked || !T.test(a))
+ );
+ })
+ .map(function (a, b) {
+ var c = n(this).val();
+ return null == c
+ ? null
+ : n.isArray(c)
+ ? n.map(c, function (a) {
+ return { name: b.name, value: a.replace(xc, '\r\n') };
+ })
+ : { name: b.name, value: c.replace(xc, '\r\n') };
+ })
+ .get();
+ },
+ }),
+ (n.ajaxSettings.xhr = function () {
+ try {
+ return new XMLHttpRequest();
+ } catch (a) {}
+ });
+ var Bc = 0,
+ Cc = {},
+ Dc = { 0: 200, 1223: 204 },
+ Ec = n.ajaxSettings.xhr();
+ a.attachEvent &&
+ a.attachEvent('onunload', function () {
+ for (var a in Cc) Cc[a]();
+ }),
+ (k.cors = !!Ec && 'withCredentials' in Ec),
+ (k.ajax = Ec = !!Ec),
+ n.ajaxTransport(function (a) {
+ var b;
+ return k.cors || (Ec && !a.crossDomain)
+ ? {
+ send: function (c, d) {
+ var e,
+ f = a.xhr(),
+ g = ++Bc;
+ if ((f.open(a.type, a.url, a.async, a.username, a.password), a.xhrFields))
+ for (e in a.xhrFields) f[e] = a.xhrFields[e];
+ a.mimeType && f.overrideMimeType && f.overrideMimeType(a.mimeType),
+ a.crossDomain || c['X-Requested-With'] || (c['X-Requested-With'] = 'XMLHttpRequest');
+ for (e in c) f.setRequestHeader(e, c[e]);
+ (b = function (a) {
+ return function () {
+ b &&
+ (delete Cc[g],
+ (b = f.onload = f.onerror = null),
+ 'abort' === a
+ ? f.abort()
+ : 'error' === a
+ ? d(f.status, f.statusText)
+ : d(
+ Dc[f.status] || f.status,
+ f.statusText,
+ 'string' == typeof f.responseText ? { text: f.responseText } : void 0,
+ f.getAllResponseHeaders(),
+ ));
+ };
+ }),
+ (f.onload = b()),
+ (f.onerror = b('error')),
+ (b = Cc[g] = b('abort'));
+ try {
+ f.send((a.hasContent && a.data) || null);
+ } catch (h) {
+ if (b) throw h;
+ }
+ },
+ abort: function () {
+ b && b();
+ },
+ }
+ : void 0;
+ }),
+ n.ajaxSetup({
+ accepts: { script: 'text/javascript, application/javascript, application/ecmascript, application/x-ecmascript' },
+ contents: { script: /(?:java|ecma)script/ },
+ converters: {
+ 'text script': function (a) {
+ return n.globalEval(a), a;
+ },
+ },
+ }),
+ n.ajaxPrefilter('script', function (a) {
+ void 0 === a.cache && (a.cache = !1), a.crossDomain && (a.type = 'GET');
+ }),
+ n.ajaxTransport('script', function (a) {
+ if (a.crossDomain) {
+ var b, c;
+ return {
+ send: function (d, e) {
+ (b = n('<script>')
+ .prop({ async: !0, charset: a.scriptCharset, src: a.url })
+ .on(
+ 'load error',
+ (c = function (a) {
+ b.remove(), (c = null), a && e('error' === a.type ? 404 : 200, a.type);
+ }),
+ )),
+ l.head.appendChild(b[0]);
+ },
+ abort: function () {
+ c && c();
+ },
+ };
+ }
+ });
+ var Fc = [],
+ Gc = /(=)\?(?=&|$)|\?\?/;
+ n.ajaxSetup({
+ jsonp: 'callback',
+ jsonpCallback: function () {
+ var a = Fc.pop() || n.expando + '_' + cc++;
+ return (this[a] = !0), a;
+ },
+ }),
+ n.ajaxPrefilter('json jsonp', function (b, c, d) {
+ var e,
+ f,
+ g,
+ h =
+ b.jsonp !== !1 &&
+ (Gc.test(b.url)
+ ? 'url'
+ : 'string' == typeof b.data &&
+ !(b.contentType || '').indexOf('application/x-www-form-urlencoded') &&
+ Gc.test(b.data) &&
+ 'data');
+ return h || 'jsonp' === b.dataTypes[0]
+ ? ((e = b.jsonpCallback = n.isFunction(b.jsonpCallback) ? b.jsonpCallback() : b.jsonpCallback),
+ h
+ ? (b[h] = b[h].replace(Gc, '$1' + e))
+ : b.jsonp !== !1 && (b.url += (dc.test(b.url) ? '&' : '?') + b.jsonp + '=' + e),
+ (b.converters['script json'] = function () {
+ return g || n.error(e + ' was not called'), g[0];
+ }),
+ (b.dataTypes[0] = 'json'),
+ (f = a[e]),
+ (a[e] = function () {
+ g = arguments;
+ }),
+ d.always(function () {
+ (a[e] = f),
+ b[e] && ((b.jsonpCallback = c.jsonpCallback), Fc.push(e)),
+ g && n.isFunction(f) && f(g[0]),
+ (g = f = void 0);
+ }),
+ 'script')
+ : void 0;
+ }),
+ (n.parseHTML = function (a, b, c) {
+ if (!a || 'string' != typeof a) return null;
+ 'boolean' == typeof b && ((c = b), (b = !1)), (b = b || l);
+ var d = v.exec(a),
+ e = !c && [];
+ return d
+ ? [b.createElement(d[1])]
+ : ((d = n.buildFragment([a], b, e)), e && e.length && n(e).remove(), n.merge([], d.childNodes));
+ });
+ var Hc = n.fn.load;
+ (n.fn.load = function (a, b, c) {
+ if ('string' != typeof a && Hc) return Hc.apply(this, arguments);
+ var d,
+ e,
+ f,
+ g = this,
+ h = a.indexOf(' ');
+ return (
+ h >= 0 && ((d = n.trim(a.slice(h))), (a = a.slice(0, h))),
+ n.isFunction(b) ? ((c = b), (b = void 0)) : b && 'object' == typeof b && (e = 'POST'),
+ g.length > 0 &&
+ n
+ .ajax({ url: a, type: e, dataType: 'html', data: b })
+ .done(function (a) {
+ (f = arguments), g.html(d ? n('<div>').append(n.parseHTML(a)).find(d) : a);
+ })
+ .complete(
+ c &&
+ function (a, b) {
+ g.each(c, f || [a.responseText, b, a]);
+ },
+ ),
+ this
+ );
+ }),
+ n.each(['ajaxStart', 'ajaxStop', 'ajaxComplete', 'ajaxError', 'ajaxSuccess', 'ajaxSend'], function (a, b) {
+ n.fn[b] = function (a) {
+ return this.on(b, a);
+ };
+ }),
+ (n.expr.filters.animated = function (a) {
+ return n.grep(n.timers, function (b) {
+ return a === b.elem;
+ }).length;
+ });
+ var Ic = a.document.documentElement;
+ function Jc(a) {
+ return n.isWindow(a) ? a : 9 === a.nodeType && a.defaultView;
+ }
+ (n.offset = {
+ setOffset: function (a, b, c) {
+ var d,
+ e,
+ f,
+ g,
+ h,
+ i,
+ j,
+ k = n.css(a, 'position'),
+ l = n(a),
+ m = {};
+ 'static' === k && (a.style.position = 'relative'),
+ (h = l.offset()),
+ (f = n.css(a, 'top')),
+ (i = n.css(a, 'left')),
+ (j = ('absolute' === k || 'fixed' === k) && (f + i).indexOf('auto') > -1),
+ j ? ((d = l.position()), (g = d.top), (e = d.left)) : ((g = parseFloat(f) || 0), (e = parseFloat(i) || 0)),
+ n.isFunction(b) && (b = b.call(a, c, h)),
+ null != b.top && (m.top = b.top - h.top + g),
+ null != b.left && (m.left = b.left - h.left + e),
+ 'using' in b ? b.using.call(a, m) : l.css(m);
+ },
+ }),
+ n.fn.extend({
+ offset: function (a) {
+ if (arguments.length)
+ return void 0 === a
+ ? this
+ : this.each(function (b) {
+ n.offset.setOffset(this, a, b);
+ });
+ var b,
+ c,
+ d = this[0],
+ e = { top: 0, left: 0 },
+ f = d && d.ownerDocument;
+ if (f)
+ return (
+ (b = f.documentElement),
+ n.contains(b, d)
+ ? (typeof d.getBoundingClientRect !== U && (e = d.getBoundingClientRect()),
+ (c = Jc(f)),
+ { top: e.top + c.pageYOffset - b.clientTop, left: e.left + c.pageXOffset - b.clientLeft })
+ : e
+ );
+ },
+ position: function () {
+ if (this[0]) {
+ var a,
+ b,
+ c = this[0],
+ d = { top: 0, left: 0 };
+ return (
+ 'fixed' === n.css(c, 'position')
+ ? (b = c.getBoundingClientRect())
+ : ((a = this.offsetParent()),
+ (b = this.offset()),
+ n.nodeName(a[0], 'html') || (d = a.offset()),
+ (d.top += n.css(a[0], 'borderTopWidth', !0)),
+ (d.left += n.css(a[0], 'borderLeftWidth', !0))),
+ { top: b.top - d.top - n.css(c, 'marginTop', !0), left: b.left - d.left - n.css(c, 'marginLeft', !0) }
+ );
+ }
+ },
+ offsetParent: function () {
+ return this.map(function () {
+ var a = this.offsetParent || Ic;
+ while (a && !n.nodeName(a, 'html') && 'static' === n.css(a, 'position')) a = a.offsetParent;
+ return a || Ic;
+ });
+ },
+ }),
+ n.each({ scrollLeft: 'pageXOffset', scrollTop: 'pageYOffset' }, function (b, c) {
+ var d = 'pageYOffset' === c;
+ n.fn[b] = function (e) {
+ return J(
+ this,
+ function (b, e, f) {
+ var g = Jc(b);
+ return void 0 === f
+ ? g
+ ? g[c]
+ : b[e]
+ : void (g ? g.scrollTo(d ? a.pageXOffset : f, d ? f : a.pageYOffset) : (b[e] = f));
+ },
+ b,
+ e,
+ arguments.length,
+ null,
+ );
+ };
+ }),
+ n.each(['top', 'left'], function (a, b) {
+ n.cssHooks[b] = yb(k.pixelPosition, function (a, c) {
+ return c ? ((c = xb(a, b)), vb.test(c) ? n(a).position()[b] + 'px' : c) : void 0;
+ });
+ }),
+ n.each({ Height: 'height', Width: 'width' }, function (a, b) {
+ n.each({ padding: 'inner' + a, content: b, '': 'outer' + a }, function (c, d) {
+ n.fn[d] = function (d, e) {
+ var f = arguments.length && (c || 'boolean' != typeof d),
+ g = c || (d === !0 || e === !0 ? 'margin' : 'border');
+ return J(
+ this,
+ function (b, c, d) {
+ var e;
+ return n.isWindow(b)
+ ? b.document.documentElement['client' + a]
+ : 9 === b.nodeType
+ ? ((e = b.documentElement),
+ Math.max(
+ b.body['scroll' + a],
+ e['scroll' + a],
+ b.body['offset' + a],
+ e['offset' + a],
+ e['client' + a],
+ ))
+ : void 0 === d
+ ? n.css(b, c, g)
+ : n.style(b, c, d, g);
+ },
+ b,
+ f ? d : void 0,
+ f,
+ null,
+ );
+ };
+ });
+ }),
+ (n.fn.size = function () {
+ return this.length;
+ }),
+ (n.fn.andSelf = n.fn.addBack),
+ 'function' == typeof define &&
+ define.amd &&
+ define('jquery', [], function () {
+ return n;
+ });
+ var Kc = a.jQuery,
+ Lc = a.$;
+ return (
+ (n.noConflict = function (b) {
+ return a.$ === n && (a.$ = Lc), b && a.jQuery === n && (a.jQuery = Kc), n;
+ }),
+ typeof b === U && (a.jQuery = a.$ = n),
+ n
+ );
+});
diff --git a/src/assets/tiles/tiles.json b/src/assets/tiles/tiles.json
new file mode 100644
index 0000000..ab2db40
--- /dev/null
+++ b/src/assets/tiles/tiles.json
@@ -0,0 +1,160 @@
+{
+ "items": [
+ {
+ "id": 1,
+ "title": "Service Design and Creation (SDC)",
+ "description": "Open the SDC in a new tab",
+ "tooltipDE": "SDC ist das visuelle Modellierungs- und Designtool von ONAP. Es erstellt interne Metadaten, die Assets beschreiben, die von allen ONAP-Komponenten verwendet werden, sowohl zur Entwurfszeit als auch zur Laufzeit.",
+ "tooltipEN": "SDC is the ONAP visual modeling and design tool. It creates internal metadata that describes assets used by all ONAP components, both at design time and run time.",
+ "imageUrl": "sdc.svg",
+ "imageAltText": "SDC image",
+ "redirectUrl": "https://sdc-ui-HOSTNAME/sdc1/portal",
+ "groups": [],
+ "roles": [
+ "ONAP_ADMIN",
+ "ONAP_OPERATOR",
+ "ONAP_DESIGNER"
+ ]
+ },
+ {
+ "id": 3,
+ "title": "Policy Framework",
+ "description": "Open Policy GUI in a new tab",
+ "tooltipDE": "Das ONAP Policy Framework legt die Architektur des Frameworks dar und zeigt die APIs, die anderen Komponenten bereitgestellt werden, die mit dem Framework zusammenarbeiten.",
+ "tooltipEN": "ONAP Policy Framework lays out the architecture of the framework and shows the APIs provided to other components that interwork with the framework.",
+ "imageUrl": "onap.svg",
+ "imageAltText": "Policy image",
+ "redirectUrl": "https://policy-ui-HOSTNAME/",
+ "groups": [],
+ "roles": [
+ "ONAP_ADMIN"
+ ]
+ },
+ {
+ "id": 4,
+ "title": "Service Orchestration (SO) Monitoring",
+ "description": "Open SO-Monitoring in a new tab",
+ "tooltipDE": "Der ONAP Service Orchestrator bietet die höchste Ebene der Service-Orchestrierung in der ONAP-Architektur.",
+ "tooltipEN": "The ONAP Service Orchestrator provides the highest level of service orchestration in the ONAP architecture.",
+ "imageUrl": "onap.svg",
+ "imageAltText": "SO-Monitoring image",
+ "redirectUrl": "https://so-monitoring-ui-HOSTNAME/",
+ "groups": [],
+ "roles": [
+ "ONAP_OPERATOR"
+ ]
+ },
+ {
+ "id": 7,
+ "title": "Controller Design Studio (CDS)",
+ "description": "Open CDS in a new tab",
+ "tooltipDE": "Die CDS Designer Benutzeroberfläche ist ein Framework zur Automatisierung der Auflösung von Ressourcen für die Instanziierung und aller Konfigurationsbereitstellung, wie z. B. die Konfiguration von Day0, Day1 oder Day2.",
+ "tooltipEN": "CDS Designer UI is a framework to automate the resolution of resources for instantiation and any config provisioning operation, such as day0, day1, or day2 configuration.",
+ "imageUrl": "cds.svg",
+ "imageAltText": "CDS image",
+ "redirectUrl": "https://cds-ui-HOSTNAME/",
+ "groups": [],
+ "roles": [
+ "ONAP_ADMIN",
+ "ONAP_OPERATOR",
+ "ONAP_DESIGNER"
+ ]
+ },
+ {
+ "id": 9,
+ "title": "Holmes Rules",
+ "description": "Open Holmes in a new tab",
+ "tooltipDE": "Das Holmes-Projekt bietet Alarmkorrelation und -analyse für Telekommunikations-Cloudinfrastruktur und -Dienste, einschließlich Hosts, Vims, VNFs und NSs.",
+ "tooltipEN": "Holmes project provides alarm correlation and analysis for Telecom cloud infrastructure and services, including hosts, vims, VNFs and NSs.",
+ "imageUrl": "onap.svg",
+ "imageAltText": "Holmes image",
+ "redirectUrl": "https://holmes-ui-HOSTNAME/iui/holmes/default.html",
+ "groups": [],
+ "roles": [
+ "ONAP_ADMIN",
+ "ONAP_OPERATOR",
+ "ONAP_DESIGNER"
+ ]
+ },
+ {
+ "id": 10,
+ "title": "A&AI Browser",
+ "description": "Open the A&AI Browser in a new tab",
+ "tooltipDE": "AAI ist eine Komponente der ONAP-Laufzeit (Echtzeitansichten von Ressourcen, Services, Produkten, Kundenabonnements und deren Beziehungen).",
+ "tooltipEN": "AAI is a component of ONAP runtime (Real-time views of Resources, Services, Products, Customer Subscriptions and their relationships).",
+ "imageUrl": "onap.svg",
+ "imageAltText": "A&AI Browser image",
+ "redirectUrl": "https://aai-ui-HOSTNAME/services/aai/webapp/index.html#/browse",
+ "groups": [],
+ "roles": [
+ "ONAP_ADMIN",
+ "ONAP_OPERATOR",
+ "ONAP_DESIGNER"
+ ]
+ },
+ {
+ "id": 11,
+ "title": "DCAE Microservice Onboarding & Design (DCAE MOD)",
+ "description": "Open the DCAE MOD in a new tab",
+ "tooltipDE": "DCAE ist der Sammelbegriff für eine Reihe von Komponenten, die gemeinsam die Rolle der Datenerfassung, Analyse und Ereignisgenerierung für ONAP erfüllen. Die Architektur von DCAE zielt auf eine flexible, steckbare, Micros-Service-orientierte, modellbasierte Komponentenbereitstellung und Servicezusammensetzung ab. DCAE unterstützt auch Sammel- und Analysevorgänge an mehreren Standorten, die für große ONAP-Bereitstellungen unerlässlich sind.",
+ "tooltipEN": "DCAE is the umbrella name for a number of components collectively fulfilling the role of Data Collection, Analytics, and Events generation for ONAP. The architecture of DCAE targets flexible, plug-able, micros-service oriented, model based component deployment and service composition. DCAE also support multi-site collection and analytics operations which are essential for large ONAP deployments.",
+ "imageUrl": "dcae-mod.svg",
+ "imageAltText": "DCAE MOD image",
+ "redirectUrl": "https://dcaemod-ui-HOSTNAME/nifi/",
+ "groups": [],
+ "roles": [
+ "ONAP_ADMIN",
+ "ONAP_OPERATOR",
+ "ONAP_DESIGNER"
+ ]
+ },
+ {
+ "id": 12,
+ "title": "SDN-C Directed Graph Builder (SDC-C DGB)",
+ "description": "Open the SDN-C Directed Graph Builder in a new tab",
+ "tooltipDE": "SDNC DG soll eine Ausführungsumgebung für schnell geschriebene und hochgradig angepasste Serviceabläufe bereitstellen.",
+ "tooltipEN": "SDNC DG is to provide an execution environment for quickly written and highly customized service flows.",
+ "imageUrl": "sdnc-dg.svg",
+ "imageAltText": "SDN-C DG image",
+ "redirectUrl": "https://sdncdg-ui-HOSTNAME/",
+ "groups": [],
+ "roles": [
+ "ONAP_ADMIN",
+ "ONAP_OPERATOR",
+ "ONAP_DESIGNER"
+ ]
+ },
+ {
+ "id": 13,
+ "title": "SDN-C Open Daylight UI (SDN-C ODL)",
+ "description": "Open the SDN-C Open Daylight UI (SDN-C ODL) in a new tab",
+ "tooltipDE": "Das OpenDaylight Project ist ein kollaboratives Open-Source-Projekt, das von der Linux Foundation gehostet wird. Das Projekt dient als Plattform für Software-Defined Networking (SDN) zur offenen, zentralisierten Überwachung von Netzwerkgeräten.",
+ "tooltipEN": "The OpenDaylight Project is a collaborative open-source project hosted by The Linux Foundation. The project serves as a platform for software-defined networking (SDN) for open, centralized, network device monitoring.",
+ "imageUrl": "sdnc-odl.svg",
+ "imageAltText": "SDN-C ODL image",
+ "redirectUrl": "https://sdncodl-ui-HOSTNAME//odlux/index.html",
+ "groups": [],
+ "roles": [
+ "ONAP_ADMIN",
+ "ONAP_OPERATOR",
+ "ONAP_DESIGNER"
+ ]
+ },
+ {
+ "id": 14,
+ "title": "Non-RT RIC",
+ "description": "Open the Non-RT RIC in a new tab",
+ "tooltipDE": "Ein \"Non-RealTime RIC (RAN Intelligent Controller)\" Controller ist eine von der ORAN Allianz spezifizierte Orchestrierungs- und Automatisierungsfunktion für das Nicht-Echtzeit-Management von RAN Funktionen. ",
+ "tooltipEN": "A Non-RealTime RIC (RAN Intelligent Controller) is an Orchestration and Automation function specified by the O-RAN Alliance for non-real-time intelligent management of RAN (Radio Access Network) functions.",
+ "imageUrl": "onap.svg",
+ "imageAltText": "Non-RT RIC image",
+ "redirectUrl": "https://nonrtric-controlpanel-HOSTNAME/",
+ "groups": [],
+ "roles": [
+ "ONAP_ADMIN",
+ "ONAP_OPERATOR",
+ "ONAP_DESIGNER"
+ ]
+ }
+ ]
+} \ No newline at end of file
diff --git a/src/assets/version.json b/src/assets/version.json
new file mode 100644
index 0000000..cc29ff0
--- /dev/null
+++ b/src/assets/version.json
@@ -0,0 +1,3 @@
+{
+ "number": "VERSIONPLACEHOLDER"
+}
diff --git a/src/environments/environment.prod.ts b/src/environments/environment.prod.ts
new file mode 100644
index 0000000..a533520
--- /dev/null
+++ b/src/environments/environment.prod.ts
@@ -0,0 +1,45 @@
+/*
+ * Copyright (c) 2022. Deutsche Telekom AG
+ *
+ * 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
+ *
+ * http://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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+
+import { Environment } from '../app/model/environment.model';
+
+
+export const environment: Environment = {
+ customStyleEnabled: true,
+ backendServerUrl: window.location.origin + '/api',
+ hostname: window.location.hostname,
+ production: true,
+ keycloakEditProfile: `${window.location.origin}/auth/realms/ONAP/account`,
+ keycloak: {
+ issuer: `${window.location.origin}/auth/realms/ONAP`, // Url of the Identity Provider
+ redirectUri: window.location.origin, // URL of the SPA to redirect the user to after login
+ clientId: 'portal-app', // The Frontend is registered with this id at the auth-server
+ responseType: 'code',
+ scope: 'openid', // set the scope for the permissions the client should request
+ requireHttps: false, // Don't require https
+ showDebugInformation: false,
+ disableAtHashCheck: true, // if at_hash is not present in JWT token
+ skipIssuerCheck: false,
+ strictDiscoveryDocumentValidation: true,
+ },
+ dateTimeFormat: 'dd/MM/yyyy HH:mm:ss',
+ loggingUrl: window.location.origin + '/onap_logging',
+ supportUrlLink: 'https://wiki.onap.org/',
+ loggingEnabled: true
+};
diff --git a/src/environments/environment.ts b/src/environments/environment.ts
new file mode 100644
index 0000000..80d0005
--- /dev/null
+++ b/src/environments/environment.ts
@@ -0,0 +1,58 @@
+/*
+ * Copyright (c) 2022. Deutsche Telekom AG
+ *
+ * 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
+ *
+ * http://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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+
+import { Environment } from '../app/model/environment.model';
+
+/**
+ * The config that is used for local development.
+ *
+ * It can be replaced during build by using the `fileReplacements` array.
+ * `ng build --prod` replaces `environment.ts` with `environment.prod.ts`.
+ * The list of file replacements can be found in `angular.json`.
+ */
+export const environment: Environment = {
+ customStyleEnabled: true,
+ backendServerUrl: window.location.origin + '/api',
+ keycloakEditProfile: `${window.location.origin}/auth/realms/ONAP/account`,
+ hostname: window.location.hostname,
+ production: false,
+ keycloak: {
+ issuer: `${window.location.origin}/auth/realms/ONAP`, // Url of the Identity Provider
+ redirectUri: window.location.origin, // URL of the SPA to redirect the user to after login
+ clientId: 'portal-app', // The Frontend is registered with this id at the auth-server
+ responseType: 'code',
+ scope: 'openid', // set the scope for the permissions the client should request
+ requireHttps: false, // Don't require https
+ showDebugInformation: false,
+ disableAtHashCheck: true, // if at_hash is not present in JWT token
+ skipIssuerCheck: true,
+ strictDiscoveryDocumentValidation: false,
+ },
+ dateTimeFormat: 'dd/MM/yyyy HH:mm:ss',
+ loggingUrl: window.location.origin + '/onap_logging',
+ supportUrlLink: 'https://wiki.onap.org/',
+ loggingEnabled: false
+};
+/*
+ * For easier debugging in development mode, you can import the following file
+ * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`.
+ *
+ * This import should be commented out in production mode because it will have a negative impact
+ * on performance if an error is thrown.
+ */
diff --git a/src/favicon.ico b/src/favicon.ico
new file mode 100644
index 0000000..75268c5
--- /dev/null
+++ b/src/favicon.ico
Binary files differ
diff --git a/src/index.html b/src/index.html
new file mode 100644
index 0000000..9ff9ef5
--- /dev/null
+++ b/src/index.html
@@ -0,0 +1,34 @@
+<!--
+ ~ Copyright (c) 2022. Deutsche Telekom AG
+ ~
+ ~ 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
+ ~
+ ~ http://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.
+ ~
+ ~ SPDX-License-Identifier: Apache-2.0
+ -->
+
+
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8" />
+ <title>ONAP Portal</title>
+ <base href="/" />
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
+ <link rel="icon" type="image/x-icon" href="favicon.ico" />
+ <script src="assets/js/jquery-2.1.3.min.js"></script>
+ <script src="assets/js/bootstrap.min.js"></script>
+ </head>
+ <body>
+ <app-root></app-root>
+ </body>
+</html>
diff --git a/src/keycloak-error.html b/src/keycloak-error.html
new file mode 100644
index 0000000..745dd46
--- /dev/null
+++ b/src/keycloak-error.html
@@ -0,0 +1,196 @@
+<!--
+ ~ Copyright (c) 2022. Deutsche Telekom AG
+ ~
+ ~ 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
+ ~
+ ~ http://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.
+ ~
+ ~ SPDX-License-Identifier: Apache-2.0
+ -->
+
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="UTF-8" />
+ <title>ONAP Portal</title>
+ <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <link rel="icon" type="image/x-icon" href="favicon.ico" />
+ <link rel="stylesheet" href="assets/css/bootstrap.css" />
+ <link rel="stylesheet" href="styles.css" />
+ <script src="assets/js/jquery-2.1.3.min.js"></script>
+ <script src="assets/js/bootstrap.min.js"></script>
+ <style>
+ .navbar {
+ background-color: #e7e6e6;
+ height: 55px;
+ padding: 6px !important;
+ }
+
+ .sidebar-toggler {
+ background-color: transparent;
+ border-style: none;
+ margin-top: 2px;
+ }
+
+ .onap-logo {
+ margin-left: 0.75rem;
+ margin-bottom: 2px;
+ }
+
+ .margin-r-6-px {
+ margin-right: 6px;
+ }
+ .margin-l-auto {
+ margin-left: auto;
+ }
+
+ .margin-r-3-px {
+ margin-right: 3px;
+ }
+
+ .header-icon-styling {
+ color: var(--black);
+ width: 20px;
+ margin-bottom: 8px;
+ }
+
+ .btn-account {
+ background-color: transparent;
+ border-style: none;
+ font-size: 21px;
+ cursor: none;
+ margin-bottom: 7px;
+ height: 38.25px;
+ padding-top: 3px;
+ padding-bottom: 6.73px;
+ }
+
+ .nav-wrapper {
+ padding: 6px;
+ }
+
+ .sidebar-container {
+ margin-right: -1px;
+ flex: 0 0 230px;
+ border-right-style: solid;
+ border-right-width: 1px;
+ border-right-color: #b2b2b2;
+ }
+
+ .sidebar-expanded {
+ min-width: 250px;
+ max-width: 250px;
+ height: calc(100vh - 67px);
+ display: block;
+ border-right-style: solid;
+ border-right-width: 1px;
+ border-right-color: #c5c5c5;
+ }
+
+ .portal-version-wrapper {
+ padding: 2px;
+ }
+
+ .portal-version-title,
+ .portal-version-number {
+ font-size: 13px;
+ line-height: 15.6px;
+ margin-bottom: 10px;
+ }
+
+ .btn:focus {
+ outline: 0;
+ box-shadow: none;
+ }
+
+ .error-content-wrapper {
+ position: fixed;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ }
+
+ .error-text-wrapper {
+ font-weight: bold;
+ }
+
+ .error-text {
+ font-size: 25px;
+ }
+
+ img,
+ button,
+ button:not(:disabled),
+ [type='button']:not(:disabled),
+ [type='reset']:not(:disabled),
+ [type='submit']:not(:disabled) {
+ cursor: auto;
+ }
+ </style>
+ </head>
+
+ <body onload="errorInfo()">
+ <div>
+ <nav aria-label="Sidemenu" class="navbar navbar-light">
+ <button type="button" class="sidebar-toggler" [attr.aria-label]="'Toggle sidemenu'">
+ <img src="assets/images/icons/list-dark.svg" alt="List" />
+ </button>
+ <a class="onap-logo">
+ <img style="height: 38px" src="assets/images/onap-logo.png" alt="ONAP Logo" />
+ </a>
+ <div class="d-inline-block margin-r-6-px margin-l-auto">
+ <button class="btn btn-invisible header-icon-styling margin-r-3-px p-0" [attr.aria-label]="'Help'">
+ <img src="assets/images/icons/question-circle.svg" alt="Help" />
+ </button>
+ <button class="btn btn-invisible header-icon-styling p-0" [attr.aria-label]="'Fullscreen'">
+ <img src="assets/images/icons/arrows-fullscreen-dark.svg" alt="Fullscreen" /></button
+ ><!--
+ --><button class="btn btn-account px-3" [attr.aria-label]="'My Account'">
+ <img src="assets/images/icons/person-fill-dark.svg" alt="Person" /><!--
+ --><img src="assets/images/icons/caret-down-fill-dark.svg" alt="Caret down" />
+ </button>
+ </div>
+ </nav>
+ </div>
+ <div class="nav-wrapper">
+ <nav
+ class="sidebar-container sidebar-expanded overflow-auto d-flex flex-column justify-content-between"
+ [attr.aria-label]="'Main Menu'"
+ >
+ <div class="d-flex justify-content-center text-center mt-auto portal-version-wrapper">
+ <h5 class="portal-version-title mr-1">Portal Version:</h5>
+ <span class="portal-version-number">VERSIONPLACEHOLDER</span>
+ </div>
+ </nav>
+ </div>
+ <div>
+ <div class="container text-center error-content-wrapper">
+ <div class="row">
+ <div class="col">
+ <div class="error-text-wrapper">
+ <p class="error-text">
+ We are not able to authorize you due to a server issue that occured at: <span id="dateTime"></span>
+ </p>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <script>
+ function errorInfo() {
+ const dt = new Date();
+ document.getElementById('dateTime').innerHTML = dt.toLocaleString();
+ }
+ </script>
+ </body>
+</html>
diff --git a/src/main.ts b/src/main.ts
new file mode 100644
index 0000000..1e445f4
--- /dev/null
+++ b/src/main.ts
@@ -0,0 +1,45 @@
+/*
+ * Copyright (c) 2022. Deutsche Telekom AG
+ *
+ * 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
+ *
+ * http://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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+
+import { enableProdMode } from '@angular/core';
+import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
+
+import { AppModule } from './app/app.module';
+import { environment } from './environments/environment';
+import { ajax } from 'rxjs/ajax';
+import { catchError, switchMap } from 'rxjs/operators';
+import { EMPTY, from } from 'rxjs';
+import { ACL_CONFIG, AclConfig } from './app/modules/auth/injection-tokens';
+
+if (environment.production) {
+ enableProdMode();
+}
+
+const fetchAclConfig$ = ajax.getJSON<AclConfig>(`assets/acl.json`);
+
+fetchAclConfig$
+ .pipe(
+ switchMap(acl => from(platformBrowserDynamic([{ provide: ACL_CONFIG, useValue: acl }]).bootstrapModule(AppModule))),
+ catchError(() => {
+ return EMPTY;
+ }),
+ )
+ .subscribe(() => {
+ console.log('App bootstrapped successfully');
+ });
diff --git a/src/polyfills.ts b/src/polyfills.ts
new file mode 100644
index 0000000..d8ed2ca
--- /dev/null
+++ b/src/polyfills.ts
@@ -0,0 +1,78 @@
+/*
+ * Copyright (c) 2022. Deutsche Telekom AG
+ *
+ * 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
+ *
+ * http://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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+
+/***************************************************************************************************
+ * Load `$localize` onto the global scope - used if i18n tags appear in Angular templates.
+ */
+import '@angular/localize/init';
+/**
+ * This file includes polyfills needed by Angular and is loaded before the app.
+ * You can add your own extra polyfills to this file.
+ *
+ * This file is divided into 2 sections:
+ * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
+ * 2. Application imports. Files imported after ZoneJS that should be loaded before your main
+ * file.
+ *
+ * The current setup is for so-called "evergreen" browsers; the last versions of browsers that
+ * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
+ * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
+ *
+ * Learn more in https://angular.io/guide/browser-support
+ */
+/***************************************************************************************************
+ * BROWSER POLYFILLS
+ */
+/** IE10 and IE11 requires the following for NgClass support on SVG elements */
+/**
+ * Web Animations `@angular/platform-browser/animations`
+ * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari.
+ * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0).
+ */
+/**
+ * By default, zone.js will patch all possible macroTask and DomEvents
+ * user can disable parts of macroTask/DomEvents patch by setting following flags
+ * because those flags need to be set before `zone.js` being loaded, and webpack
+ * will put import in the top of bundle, so user need to create a separate file
+ * in this directory (for example: zone-flags.ts), and put the following flags
+ * into that file, and then add the following code before importing zone.js.
+ * import './zone-flags';
+ *
+ * The flags allowed in zone-flags.ts are listed here.
+ *
+ * The following flags will work for all browsers.
+ *
+ * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame
+ * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick
+ * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames
+ *
+ * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js
+ * with the following flag, it will bypass `zone.js` patch for IE/Edge
+ *
+ * (window as any).__Zone_enable_cross_context_check = true;
+ *
+ */
+/***************************************************************************************************
+ * Zone JS is required by default for Angular itself.
+ */
+import 'zone.js'; // Included with Angular CLI.
+
+/***************************************************************************************************
+ * APPLICATION IMPORTS
+ */
diff --git a/src/styles.css b/src/styles.css
new file mode 100644
index 0000000..2528e5d
--- /dev/null
+++ b/src/styles.css
@@ -0,0 +1,630 @@
+/*
+ * Copyright (c) 2022. Deutsche Telekom AG
+ *
+ * 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
+ *
+ * http://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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+
+/* https://www.w3schools.com/css/css3_variables.asp */
+:root {
+ --alert-transparency: 0.9;
+ --light-gray: #f5f5f5;
+ --dark-gray: #5a5858;
+ --active-gray: #d9d9d9;
+ --black: #000;
+ --white: #fff;
+ --danger: #d90000;
+ --secondary: #ededed;
+ --major: #fad4b8;
+ --minor: #fecb00;
+ --intense-blue: #00a0de;
+
+}
+
+/* ########################################
+ * Colors
+ * ########################################
+ */
+
+
+.blue {
+ color: var(--blue);
+}
+
+.orange {
+ color: #ffa500;
+}
+
+.red {
+ color: #ff0000;
+}
+
+.green {
+ color: #008000;
+}
+
+.dark-gray {
+ color: var(--dark-gray);
+}
+.black {
+ color: var(--black);
+}
+
+.max-w-100px {
+ max-width: 100px;
+}
+.max-w-120px {
+ max-width: 120px;
+}
+
+.right-10 {
+ right: 10px;
+}
+.opacity-1 {
+ opacity: 1 !important;
+}
+
+.alert {
+ z-index: 1;
+}
+
+.alert-primary {
+ background-color: #cce5ff !important;
+}
+.alert-secondary {
+ background-color: #e2e3e5 !important;
+}
+.alert-success {
+ background-color: #d4edda !important;
+}
+.alert-info {
+ background-color: #d1ecf1 !important;
+}
+.alert-warning {
+ background-color: #fff3cd !important;
+}
+.alert-danger {
+ color: var(--red) !important;
+ background-color: #f8d7da !important;
+}
+.alert-light {
+ background-color: #fefefe !important;
+}
+.alert-dark {
+ background-color: #d6d8d9 !important;
+}
+
+.navbar-collapse.in {
+ display: block !important;
+}
+
+.btn {
+ max-height: 40px;
+}
+.max-w-fit-content {
+ max-width: fit-content;
+}
+.btn-delete-bg {
+ background-position: center;
+ background-image: url('assets/images/icons/delete-icon.svg');
+}
+
+.opacity-1 {
+ opacity: 1 !important;
+}
+
+button[aria-label='Close']:focus {
+ outline: none;
+}
+
+.breadcrumb-item a {
+ color: var(--blue) !important;
+}
+.bg-lightGray {
+ background-color: var(--light-gray);
+}
+/* Make pagination responsive: https://github.com/twbs/bootstrap/issues/23504#issuecomment-592757850 */
+ul.pagination {
+ margin-bottom: 0;
+ flex-wrap: wrap;
+}
+
+/* Allow other elements to be in line with the pagination */
+ngb-pagination {
+ display: inline-block;
+}
+
+.form-control:disabled,
+.form-control[readonly],
+option:disabled {
+ background-color: #e9ecef !important;
+ opacity: 1 !important;
+}
+
+/*
+ Red asterisk that is not visible to screen readers
+ https://stackoverflow.com/questions/26634156/can-i-prevent-after-pseudo-element-from-being-read-by-screen-readers#26634352
+*/
+.required:after {
+ content: '';
+ display: inline-block;
+ width: 0.5em;
+ height: 0.5em;
+ background-image: url(src/assets/images/icons/Asterisk.svg);
+ background-size: 0.5em 0.5em;
+ vertical-align: top;
+ margin-left: 0.15em;
+ margin-top: 0.1em;
+}
+
+.breadcrumb-item::before {
+ margin-top: 4px;
+}
+.uuid-cell {
+ width: 350px;
+ min-width: 350px;
+ max-width: 350px;
+}
+.btn-invisible {
+ background-color: initial;
+ border: none;
+}
+.dropdown-toggle::after {
+ height: 6px !important;
+}
+
+#deployment-summary-accordion > .card {
+ border-radius: 0.25rem;
+}
+
+/* Override the (overly large) padding of the accordion header element */
+#deployment-summary-accordion > .card > .card-header {
+ padding-left: 17px;
+ padding-right: 17px;
+}
+
+/* add a scrollbar to the content area of the accordion */
+#deployment-summary-accordion > .card > div[id^='panel-'] {
+ max-height: 50vh;
+ overflow: auto;
+}
+
+/* telekom-styles is removing the bottom border on this element otherwise*/
+.accordion .card:first-of-type {
+ border: 1px solid #b2b2b2 !important;
+}
+.w-20px {
+ width: 20px !important;
+}
+
+/* override the telekom-styles behaviour: Pagination is too small */
+.pagination-sm .page-link {
+ line-height: 1.8 !important;
+}
+
+/* adding focus to buttons with class btn-default */
+.btn-default:focus {
+ outline: 5px auto var(--cyan) !important;
+ outline-offset: -2px !important;
+}
+
+.border-blue {
+ border-color: var(--light-blue) !important;
+}
+.ngx-charts-outer {
+ border: 1px solid rgba(0, 0, 0, 0.1);
+}
+
+.outline-blue {
+ border: 1px solid var(--light-blue) !important;
+ border-radius: 0.25rem;
+}
+.outline-grey {
+ border-color: 1px solid var(--secondary);
+ border-radius: 0.25rem;
+}
+
+.border-radius-top-none {
+ border-top-left-radius: 0 !important;
+ border-top-right-radius: 0 !important;
+}
+
+.accordion .card:last-of-type {
+ border-top-left-radius: 0.25rem !important;
+ border-top-right-radius: 0.25rem !important;
+}
+
+.text-align-start {
+ text-align: start;
+}
+
+.row-active {
+ background-color: var(--active-gray) !important;
+ border: 1px solid var(--light-blue) !important;
+ border-radius: 0.25rem;
+}
+
+.accordion > .card > .card-header {
+ padding-left: 0 !important;
+}
+
+.pointer {
+ cursor: pointer;
+}
+
+.draggable {
+ cursor: grab;
+}
+
+.draggable:active {
+ cursor: grabbing;
+}
+
+/* used for perceivedSeverity: major in the alarm list */
+.table-major {
+ background-color: var(--major) !important;
+}
+
+/* for ng-bootstraps nav tabs in the alarm details. Hovering over elements looks weird otherwise */
+.no-border-hover:hover > a {
+ border-bottom: none !important;
+}
+
+/* Prevent [sort icons] from breaking into a new line in table headers */
+th {
+ white-space: nowrap;
+}
+
+/* Override calc() to achieve same height of inputs and buttons */
+.form-control {
+ height: auto !important;
+}
+.active-badge {
+ box-shadow: 0 0 2pt 2pt var(--light-blue);
+}
+
+.badge {
+ font-size: 100% !important;
+}
+
+.badge-major {
+ color: var(--dark);
+ background-color: var(--major);
+}
+
+/* Context menu overrides */
+.ngx-contextmenu {
+ box-shadow: 0 0 17px 0 rgba(0, 0, 0, 0.59);
+}
+.ngx-contextmenu > .dropdown-menu > li > a:hover {
+ text-decoration: none !important;
+}
+
+.border-radius {
+ border-radius: 0.25rem !important;
+}
+
+.form-control.is-invalid,
+.was-validated .form-control:valid {
+ background-image: url('assets/images/icons/triangular-warning-sign.svg') !important;
+ background-position: right calc(0.75em + 0.1875rem) center !important;
+}
+
+.no-border {
+ border: none !important;
+}
+.border-gray {
+ border: 1px solid var(--gray);
+}
+
+.btn-outline-secondary {
+ color: var(--dark-gray) !important;
+ border-color: var(--dark-gray) !important;
+}
+.btn-outline-secondary:hover,
+.btn-outline-secondary:focus,
+.btn-outline-secondary:not(:focus),
+.btn-outline-secondary:active {
+ color: var(--gray-dark) !important;
+ background-color: var(--light-gray) !important;
+ border-color: var(--light-gray) !important;
+}
+
+.node-content-wrapper,
+.tree-children {
+ position: relative;
+}
+
+.node-content-wrapper::before,
+.tree-children::after {
+ content: '';
+ position: absolute;
+}
+
+.node-content-wrapper::before {
+ height: 28px;
+ top: -13px;
+ width: 20px;
+ left: -26px;
+}
+
+.tree-node-level-1 > tree-node-wrapper > .node-wrapper > .node-content-wrapper::before {
+ display: none;
+}
+
+.tree-node-leaf > .node-wrapper > .node-content-wrapper::before {
+ width: 25px;
+}
+
+.tree-children::after {
+ height: 100%;
+ top: -15px;
+ left: 6px;
+}
+
+.toggle-children-placeholder {
+ width: 13px !important;
+}
+
+tree-node:last-child > .tree-node > .tree-children::after {
+ border-left: none;
+}
+
+.toggle-children-wrapper {
+ padding: 2px 3px 5px 0;
+}
+
+.toggle-children {
+ z-index: 1;
+ width: 11px !important;
+ height: 11px !important;
+}
+
+.toggle-children-wrapper-expanded .toggle-children {
+ top: 1px;
+ left: 1px;
+}
+
+.attribute-pane-title {
+ color: var(--primary);
+ max-width: 60%;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ white-space: nowrap !important;
+ display: inline-block;
+ margin-top: 0 !important;
+ margin-bottom: 0 !important;
+}
+
+.mr-15-px {
+ margin-right: 15px;
+}
+
+.bg-color-blue {
+ background-color: var(--light-blue);
+}
+
+/* styling for switch that we are using in attribute pane for showing and hiding the attributes */
+/* ------------------------------------------------------------------------------------- */
+.attribute-pane-switch {
+ height: 0;
+ width: 0;
+ visibility: hidden;
+}
+
+.label-for-switch {
+ cursor: pointer;
+ text-align: -9999px;
+ width: 40px;
+ height: 20px;
+ background: var(--active-gray);
+ display: block;
+ border-radius: 100px;
+ position: relative;
+ border: 1px solid var(--primary);
+}
+
+.label-for-switch::after {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 1px;
+ width: 18px;
+ height: 18px;
+ background: var(--primary);
+ border-radius: 90px;
+ transition: 0.3s;
+}
+
+#switch:checked + .label-for-switch {
+ background: var(--primary);
+}
+
+#switch:checked + .label-for-switch:after {
+ right: calc(18px - 1px);
+ transform: translateX(100%);
+ background: var(--active-gray);
+}
+
+.label-for-switch:active:after {
+ width: 18px;
+}
+
+.text-for-switch {
+ font-size: 11px;
+}
+/* ------------------------------------------------------------------------------------- */
+
+.toggle-children-wrapper-collapsed .toggle-children,
+.toggle-children-wrapper-expanded .toggle-children {
+ top: 2.4px;
+}
+.node-custom-content-wrapper {
+ padding: 2px 5px !important;
+}
+.node-content-wrapper{
+ padding: 0 !important;
+}
+
+/* Display definition lists in a tabular style */
+/* https://stackoverflow.com/questions/1687733/what-html-markups-to-use-for-displaying-label-value-data#1687759 */
+dt { float: left; clear: left; width: 6em; font-weight: bold; }
+.ml-2px{
+ margin-left: 2px;
+}
+.overflow-y-scroll{
+ overflow-y: scroll;
+}
+
+.alarm-color-critical-outline {
+ color: var(--white);
+ background-color: var(--danger) !important;
+}
+.alarm-color-cleared-outline {
+ color: var(--black);
+ border: 1px solid var(--black);
+}
+
+.alarm-color-major-outline {
+ border: 1px solid var(--major);
+ color: var(--major);
+}
+
+.alarm-color-minor-outline {
+ border: 1px solid var(--minor);
+ color: var(--minor);
+}
+
+.alarm-color-warning-outline {
+ color: var(--intense-blue);
+ border: 1px solid var(--intense-blue);
+}
+
+.alarm-color-indeterminate-outline {
+ border: 1px solid var(--secondary);
+ color: var(--secondary);
+}
+
+.leaflet-div-icon-hide {
+ background: none;
+ border: none;
+}
+
+.alarm-severity-counter {
+ background-color: white;
+ border-radius: 4px;
+ margin: 0 2px;
+ padding: 0 0.25rem !important;
+}
+
+.bc-icon {
+ color: var(--primary);
+}
+
+.hover-bg-gray:hover {
+ background-color: rgb(237, 237, 237);
+ width: 100% !important;
+}
+
+.pulse{
+ z-index: 10!important;
+ height: 180px;
+ width: 180px;
+ background: linear-gradient(
+ var(--danger),
+ #d97e87
+ );
+ position: absolute;
+ margin: auto;
+ left: 0;
+ right: 0;
+ top: 0;
+ bottom: 0;
+ border-radius: 50%;
+ display: grid;
+ place-items: center;
+ font-size: 50px;
+ color: #ffffff;
+}
+.pulse:before,
+.pulse:after{
+ content: "";
+ position: absolute;
+ height: 100%;
+ width: 100%;
+ background-color: var(--danger);
+ border-radius: 50%;
+ z-index: -1;
+ opacity: 0.7;
+}
+.pulse:before{
+ animation: pulse 2s ease-out infinite;
+}
+.pulse:after{
+ animation: pulse 2s 1s ease-out infinite;
+}
+@keyframes pulse{
+ 100%{
+ transform: scale(2.5);
+ opacity: 0;
+ }
+}
+tree-viewport{
+ overflow-x: auto !important;
+ display: flex !important;
+}
+.node-content-wrapper{
+ border: 1px solid rgba(0,0,0,0);
+}
+.node-content-wrapper-focused, .node-content-wrapper-focused:hover{
+ border: 1px solid var(--primary);
+}
+
+.backdropClass{
+ z-index: 1050 !important;
+}
+
+.table > :not(:first-child) {
+ border-top: none !important;
+}
+
+.leaflet-top,
+.leaflet-bottom {
+ z-index: 998 !important;
+ }
+
+.btn-invisible:focus {
+ border: 2px solid black;
+ border-radius: 4px;
+ outline: 0;
+}
+
+/*override onap styles*/
+.btn-secondary:focus {
+ background-color: #5c636a !important;
+ border-color: #565e64 !important;
+}
+
+.close:not(:disabled):not(.disabled) {
+ cursor: pointer;
+}
+
+.close::before {
+ display: block;
+ width: 16px;
+ height: 16px;
+ content: " ";
+ background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M15.81,14.99l-6.99-7l6.99-7c0.24-0.24,0.2-0.63-0.04-0.83c-0.24-0.2-0.59-0.2-0.79,0l-6.99,7l-6.99-7 C0.75-0.08,0.36-0.04,0.16,0.2c-0.2,0.24-0.2,0.59,0,0.79l6.99,7l-6.99,7c-0.24,0.24-0.2,0.63,0.04,0.83c0.24,0.2,0.59,0.2,0.79,0 l6.99-7l6.99,7c0.24,0.24,0.59,0.24,0.83,0.04C16.04,15.66,16.08,15.26,15.81,14.99C15.85,15.03,15.81,15.03,15.81,14.99z' fill='%23262626'/%3E%3C/svg%3E");
+}
diff --git a/src/test.ts b/src/test.ts
new file mode 100644
index 0000000..36b9be6
--- /dev/null
+++ b/src/test.ts
@@ -0,0 +1,42 @@
+/*
+ * Copyright (c) 2022. Deutsche Telekom AG
+ *
+ * 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
+ *
+ * http://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.
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+
+// This file is required by karma.conf.js and loads recursively all the .spec and framework files
+
+import 'zone.js/testing';
+import { getTestBed } from '@angular/core/testing';
+import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing';
+
+declare const require: {
+ context(
+ path: string,
+ deep?: boolean,
+ filter?: RegExp,
+ ): {
+ keys(): string[];
+ <T>(id: string): T;
+ };
+};
+
+// First, initialize the Angular testing environment.
+getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting());
+// Then we find all the tests.
+const context = require.context('./', true, /\.spec\.ts$/);
+// And load the modules.
+context.keys().forEach(context);
diff --git a/staging.proxy.config.json.template b/staging.proxy.config.json.template
new file mode 100644
index 0000000..1fc85c0
--- /dev/null
+++ b/staging.proxy.config.json.template
@@ -0,0 +1,31 @@
+{
+ "/api": {
+ "target": "https://portal-ui.example.com/api",
+ "secure": false,
+ "changeOrigin": true,
+ "pathRewrite": {
+ "^/api": ""
+ }
+ },
+ "/mock-api": {
+ "target": "https://portal-ui.example.com/mock-api",
+ "secure": false,
+ "changeOrigin": true,
+ "pathRewrite": {
+ "^/mock-api": ""
+ }
+ },
+ "/keycloak": {
+ "target": "https://keycloak-ui.example.com",
+ "secure": false,
+ "changeOrigin": true,
+ "pathRewrite": {
+ "^/keycloak": ""
+ }
+ },
+ "/onap_logging": {
+ "target": "https://portal-ui.onap.example.com/onap_logging",
+ "secure": false,
+ "changeOrigin": true
+ }
+}
diff --git a/staging.sh b/staging.sh
new file mode 100755
index 0000000..0f0e259
--- /dev/null
+++ b/staging.sh
@@ -0,0 +1,24 @@
+#!/bin/sh
+#
+#
+# Copyright (c) 2022. Deutsche Telekom AG
+#
+# 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
+#
+# http://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.
+#
+# SPDX-License-Identifier: Apache-2.0
+#
+#
+#
+
+[ ! -d "./node_modules" ] && npm install
+npm start -- --proxy-config staging.proxy.config.json --port 80 --host 0.0.0.0 --disable-host-check
diff --git a/tsconfig.app.json b/tsconfig.app.json
new file mode 100644
index 0000000..29f5f58
--- /dev/null
+++ b/tsconfig.app.json
@@ -0,0 +1,9 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "./out-tsc/app",
+ "types": []
+ },
+ "files": ["src/main.ts", "src/polyfills.ts"],
+ "include": ["src/**/*.d.ts"]
+}
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..536748b
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,28 @@
+{
+ "compileOnSave": false,
+ "compilerOptions": {
+ "baseUrl": "./",
+ "outDir": "./dist/out-tsc",
+ "forceConsistentCasingInFileNames": true,
+ "strict": true,
+ "noImplicitReturns": true,
+ "noFallthroughCasesInSwitch": true,
+ "sourceMap": true,
+ "declaration": false,
+ "downlevelIteration": true,
+ "experimentalDecorators": true,
+ "module": "es2020",
+ "moduleResolution": "node",
+ "importHelpers": true,
+ "target": "es2017",
+ "lib": ["es2018", "dom"],
+ "resolveJsonModule": true,
+ "esModuleInterop": true
+ },
+ "angularCompilerOptions": {
+ "strictInjectionParameters": true,
+ "strictTemplates": true,
+ "fullTemplateTypeCheck": true,
+ "enableIvy": true
+ }
+}
diff --git a/tsconfig.spec.json b/tsconfig.spec.json
new file mode 100644
index 0000000..430cf75
--- /dev/null
+++ b/tsconfig.spec.json
@@ -0,0 +1,9 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "./out-tsc/spec",
+ "types": ["jasmine", "node"]
+ },
+ "files": ["src/test.ts", "src/polyfills.ts"],
+ "include": ["src/**/*.spec.ts", "src/**/*.d.ts"]
+}
diff --git a/version b/version
new file mode 100644
index 0000000..6e8bf73
--- /dev/null
+++ b/version
@@ -0,0 +1 @@
+0.1.0