diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..16c3a25 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,40 @@ +# Changelog of kratos-admin-ui + +## 2.xx + +### 2.0.0 +#### BREAKING CHANGE +- Configure nginx to act as a reverse proxy for the kratos endpoints + - **You have to change your environment variables!** + +## 1.xx + +### 1.2.0 +- Support array schemas. see [#90](https://github.com/dfoxg/kratos-admin-ui/issues/90) +- notification popups +- rewrite identites overview to new fluentui DataGrid + +### 1.1.0 +- Support for ory/kratos v1.0.0 + +### 1.0.5 +- Update Node.js to v20 +- Support for ory/kratos v0.13.0 + +### 1.0.4 +- Support for ory/kratos v0.11.0 +- Support for viewing, deletion and extend sessions +- Update React -> 18; Upgrade packes by @dfoxg in [#66](https://github.com/dfoxg/kratos-admin-ui/pull/66) + +### 1.0.3 +- fix for [#63](https://github.com/dfoxg/kratos-admin-ui/issues/63) and [#64](https://github.com/dfoxg/kratos-admin-ui/issues/64) by @dfoxg in [65](https://github.com/dfoxg/kratos-admin-ui/pull/65) + +### 1.0.2 +- switched from @fluentui/react to @fluentui/react-components + +### 1.0.1 +- check undefined object by @rungthiwasrisaart in [#62](https://github.com/dfoxg/kratos-admin-ui/pull/62) +- Updated dependencies + +### 1.0.0 +- First stable release of kratos-admin-ui \ No newline at end of file diff --git a/README.md b/README.md index 9b06368..8a01993 100644 --- a/README.md +++ b/README.md @@ -4,23 +4,87 @@ A simple Admin-Interface for [ory/kratos](https://www.ory.sh/kratos/docs/). Made ## Run -On every commit on the main branch a new docker image is getting created on ghcr.io: ghcr.io/dfoxg/kratos-admin-ui. -To run the image, you have to provide two environemnt variables: +To run the image, you have to provide two environment variables: - `KRATOS_ADMIN_URL`: the admin url of your kratos instance - `KRATOS_PUBLIC_URL`: the public url of your kratos instance +You should follow the kratos best practices, [which recommends to never expore the admin-api to the internet, since there is no authentication](https://www.ory.sh/docs/kratos/guides/production#admin-api). + +To run the admin-ui, which of course needs access to the admin-api, you should run the admin-ui in the same network as kratos. + +In the following snipped the admin-ui gets deployed in the same docker network (`kratos_intranet`) as kratos - over the Docker-Compose-DNS resolution the nginx reverse proxy can call the admin + ``` docker run -it \ --rm -p 3000:80 \ --e KRATOS_ADMIN_URL=http://localhost:4435 \ --e KRATOS_PUBLIC_URL=http://localhost:4430 \ +-e KRATOS_ADMIN_URL=http://kratos:4434 \ +-e KRATOS_PUBLIC_URL=http://kratos:4433 \ +--network kratos_intranet \ ghcr.io/dfoxg/kratos-admin-ui ``` +or like here, include it in the docker compose: + +``` +version: '3.7' +services: + kratos-migrate: + image: oryd/kratos:v1.0.0 + environment: + - DSN=sqlite:///var/lib/sqlite/db.sqlite?_fk=true&mode=rwc + volumes: + - type: volume + source: kratos-sqlite + target: /var/lib/sqlite + read_only: false + - type: bind + source: ./contrib/quickstart/kratos/email-password + target: /etc/config/kratos + command: -c /etc/config/kratos/kratos.yml migrate sql -e --yes + restart: on-failure + networks: + - intranet + kratos: + image: oryd/kratos:v1.0.0 + depends_on: + - kratos-migrate + ports: + - '4433:4433' # public + # - '4434:4434' # admin, do not expose! + restart: unless-stopped + environment: + - DSN=sqlite:///var/lib/sqlite/db.sqlite?_fk=true + - LOG_LEVEL=trace + command: serve -c /etc/config/kratos/kratos.yml --dev --watch-courier + volumes: + - type: volume + source: kratos-sqlite + target: /var/lib/sqlite + read_only: false + - type: bind + source: ./contrib/quickstart/kratos/email-password + target: /etc/config/kratos + networks: + - intranet + admin_ui: + image: ghcr.io/dfoxg/kratos-admin-ui:2.0.0 + ports: + - '80:80' + restart: unless-stopped + environment: + - KRATOS_ADMIN_URL=http://kratos:4434 + - KRATOS_PUBLIC_URL=http://kratos:4433 + networks: + - intranet +networks: + intranet: +volumes: + kratos-sqlite: +``` ## Start local -It is required, that a local instance of ory kratos is running. the latest tested version is `v0.13.0`. +It is required, that a local instance of ory kratos is running. the latest tested version is `v1.0.0`. ``` cd kratos-admin-ui diff --git a/RELEASE.md b/RELEASE.md new file mode 100644 index 0000000..ba8fec3 --- /dev/null +++ b/RELEASE.md @@ -0,0 +1,7 @@ +# How to Release + +- set version in config.ts +- set version in package.json +- set version in README.md +- push to master +- git tag `git tag -a v1.4 -m "1.4"` \ No newline at end of file diff --git a/contrib/quickstart/kratos/email-password/identity.schema.json b/contrib/quickstart/kratos/email-password/identity.schema.json new file mode 100644 index 0000000..1a13787 --- /dev/null +++ b/contrib/quickstart/kratos/email-password/identity.schema.json @@ -0,0 +1,49 @@ +{ + "$id": "https://schemas.ory.sh/presets/kratos/quickstart/email-password/identity.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Person", + "type": "object", + "properties": { + "traits": { + "type": "object", + "properties": { + "email": { + "type": "string", + "format": "email", + "title": "E-Mail", + "minLength": 3, + "ory.sh/kratos": { + "credentials": { + "password": { + "identifier": true + } + }, + "verification": { + "via": "email" + }, + "recovery": { + "via": "email" + } + } + }, + "name": { + "type": "object", + "properties": { + "first": { + "title": "First Name", + "type": "string" + }, + "last": { + "title": "Last Name", + "type": "string" + } + } + } + }, + "required": [ + "email" + ], + "additionalProperties": false + } + } +} diff --git a/contrib/quickstart/kratos/email-password/kratos.yml b/contrib/quickstart/kratos/email-password/kratos.yml new file mode 100644 index 0000000..c3369d1 --- /dev/null +++ b/contrib/quickstart/kratos/email-password/kratos.yml @@ -0,0 +1,101 @@ +version: v0.13.0 + +dsn: memory + +serve: + public: + base_url: http://127.0.0.1:4433/ + cors: + enabled: true + admin: + base_url: http://kratos:4434/ + +selfservice: + default_browser_return_url: http://127.0.0.1:4455/ + allowed_return_urls: + - http://127.0.0.1:4455 + + methods: + password: + enabled: true + totp: + config: + issuer: Kratos + enabled: true + lookup_secret: + enabled: true + link: + enabled: true + code: + enabled: true + + flows: + error: + ui_url: http://127.0.0.1:4455/error + + settings: + ui_url: http://127.0.0.1:4455/settings + privileged_session_max_age: 15m + required_aal: highest_available + + recovery: + enabled: true + ui_url: http://127.0.0.1:4455/recovery + use: code + + verification: + enabled: true + ui_url: http://127.0.0.1:4455/verification + use: code + after: + default_browser_return_url: http://127.0.0.1:4455/ + + logout: + after: + default_browser_return_url: http://127.0.0.1:4455/login + + login: + ui_url: http://127.0.0.1:4455/login + lifespan: 10m + + registration: + lifespan: 10m + ui_url: http://127.0.0.1:4455/registration + after: + password: + hooks: + - hook: session + - hook: show_verification_ui + +log: + level: debug + format: text + leak_sensitive_values: true + +secrets: + cookie: + - PLEASE-CHANGE-ME-I-AM-VERY-INSECURE + cipher: + - 32-LONG-SECRET-NOT-SECURE-AT-ALL + +ciphers: + algorithm: xchacha20-poly1305 + +hashers: + algorithm: bcrypt + bcrypt: + cost: 8 + +identity: + default_schema_id: multi + schemas: + - id: default + url: file:///etc/config/kratos/identity.schema.json + - id: multi + url: file:///etc/config/kratos/multi.schema.json + - id: multi2 + url: file:///etc/config/kratos/multi2.schema.json + +courier: + smtp: + connection_uri: smtps://test:test@mailslurper:1025/?skip_ssl_verify=true diff --git a/contrib/quickstart/kratos/email-password/multi.schema.json b/contrib/quickstart/kratos/email-password/multi.schema.json new file mode 100644 index 0000000..5faa9d1 --- /dev/null +++ b/contrib/quickstart/kratos/email-password/multi.schema.json @@ -0,0 +1,44 @@ +{ + "$id": "https://schemas.ory.sh/presets/kratos/identity.email.schema.json", + "title": "Multi-Mail Person", + "type": "object", + "properties": { + "traits": { + "type": "object", + "properties": { + "emails": { + "type": "array", + "items": [ + { + "type": "string", + "format": "email", + "title": "E-Mail", + "ory.sh/kratos": { + "credentials": { + "password": { + "identifier": true + }, + "webauthn": { + "identifier": true + }, + "totp": { + "account_name": true + } + }, + "recovery": { + "via": "email" + }, + "verification": { + "via": "email" + } + }, + "maxLength": 320 + } + ] + } + }, + "required": ["emails"], + "additionalProperties": false + } + } +} diff --git a/contrib/quickstart/kratos/email-password/multi2.schema.json b/contrib/quickstart/kratos/email-password/multi2.schema.json new file mode 100644 index 0000000..a88d48b --- /dev/null +++ b/contrib/quickstart/kratos/email-password/multi2.schema.json @@ -0,0 +1,58 @@ +{ + "$id": "https://schemas.ory.sh/presets/kratos/identity.email.schema.json", + "title": "Multi-Mail Person", + "type": "object", + "properties": { + "traits": { + "type": "object", + "properties": { + "emails": { + "type": "array", + "items": [ + { + "type": "string", + "format": "email", + "title": "E-Mail", + "ory.sh/kratos": { + "credentials": { + "password": { + "identifier": true + }, + "webauthn": { + "identifier": true + }, + "totp": { + "account_name": true + } + }, + "recovery": { + "via": "email" + }, + "verification": { + "via": "email" + } + }, + "maxLength": 320 + } + ] + }, + "name": { + "type": "object", + "properties": { + "first": { + "title": "First Name", + "type": "string" + }, + "last": { + "title": "Last Name", + "type": "string" + } + }, + "required": ["first"] + } + }, + "required": ["emails", "name"], + "additionalProperties": false + } + } +} diff --git a/kratos-admin-ui/Dockerfile b/kratos-admin-ui/Dockerfile index 539225e..7efe3fa 100644 --- a/kratos-admin-ui/Dockerfile +++ b/kratos-admin-ui/Dockerfile @@ -19,14 +19,6 @@ LABEL org.opencontainers.image.source="https://github.com/dfoxg/kratos-admin-ui" WORKDIR /usr/share/nginx EXPOSE 80 -# copy sources and config -COPY --from=build /app/build html -COPY nginx.conf /etc/nginx/conf.d/default.conf - -#environment -COPY start.sh start.sh -COPY env2conf.sh env2conf.sh - #Timezone RUN apk upgrade --update \ && apk add -U tzdata \ @@ -35,4 +27,12 @@ RUN apk upgrade --update \ && rm -rf \ /var/cache/apk/* +#environment +COPY deploy/start.sh start.sh +COPY deploy/env2conf.sh env2conf.sh +COPY deploy/nginx.conf /etc/nginx/templates/default.conf.template + +# copy sources and config +COPY --from=build /app/build html + CMD ["sh","start.sh"] diff --git a/kratos-admin-ui/env2conf.sh b/kratos-admin-ui/deploy/env2conf.sh similarity index 56% rename from kratos-admin-ui/env2conf.sh rename to kratos-admin-ui/deploy/env2conf.sh index 54fe6e6..24021fd 100755 --- a/kratos-admin-ui/env2conf.sh +++ b/kratos-admin-ui/deploy/env2conf.sh @@ -6,6 +6,5 @@ mkdir -p "$(dirname "$1")" && touch "$1" echo '{' >"$1" -echo ' "kratosAdminURL":"'$KRATOS_ADMIN_URL'",' >> "$1" -echo ' "kratosPublicURL":"'$KRATOS_PUBLIC_URL'"' >> "$1" +echo ' "reverseProxy": true' >> "$1" echo '}' >> "$1" \ No newline at end of file diff --git a/kratos-admin-ui/deploy/nginx.conf b/kratos-admin-ui/deploy/nginx.conf new file mode 100644 index 0000000..0a824fc --- /dev/null +++ b/kratos-admin-ui/deploy/nginx.conf @@ -0,0 +1,54 @@ +map $host $kratos_admin_url { + default "$KRATOS_ADMIN_URL"; +} + +map $host $kratos_public_url { + default "$KRATOS_PUBLIC_URL"; +} + +# Expires map +map $sent_http_content_type $expires { + default off; + text/html epoch; + text/css max; + application/javascript max; + ~image/ max; + ~font/ max; +} + +server { + listen 80; + + location /api/admin/ { + # see https://www.emmanuelgautier.com/blog/nginx-docker-dns-resolution + resolver 127.0.0.11; + + rewrite /api/admin/(.*) /admin/$1 break; + + proxy_pass $kratos_admin_url; + proxy_redirect off; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + } + + location /api/public/ { + # https://www.emmanuelgautier.com/blog/nginx-docker-dns-resolution + resolver 127.0.0.11; + + rewrite /api/public/(.*) /$1 break; + + proxy_pass $kratos_public_url; + proxy_redirect off; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + } + + location / { + root /usr/share/nginx/html; + try_files $uri $uri/ /index.html =404; + } + + expires $expires; +} \ No newline at end of file diff --git a/kratos-admin-ui/deploy/start.sh b/kratos-admin-ui/deploy/start.sh new file mode 100644 index 0000000..19cb9a2 --- /dev/null +++ b/kratos-admin-ui/deploy/start.sh @@ -0,0 +1,30 @@ +checkFormat() { + regex="http?(s)://" + if [ "$1" == $regex* ]; then + echo "setting $2 to $1" + else + echo "$2 = $1" + echo "environment $2 doesnt start with http:// or https://. Reverse proxy could not be set" + exit 4 + fi +} + +if [ -z ${KRATOS_ADMIN_URL+x} ]; then + echo "KRATOS_ADMIN_URL is unset. Please set the environment variable 'KRATOS_ADMIN_URL'"; + exit 2 +#else + #checkFormat ${KRATOS_ADMIN_URL} "KRATOS_ADMIN_URL" +fi + +if [ -z ${KRATOS_PUBLIC_URL+x} ]; then + echo "KRATOS_PUBLIC_URL is unset. Please set the environment variable 'KRATOS_PUBLIC_URL'"; + exit 3 +#else + #checkFormat ${KRATOS_PUBLIC_URL} "KRATOS_PUBLIC_URL" +fi + +sh env2conf.sh /usr/share/nginx/html/config.json + + + +/docker-entrypoint.sh nginx -g "daemon off;" \ No newline at end of file diff --git a/kratos-admin-ui/nginx.conf b/kratos-admin-ui/nginx.conf deleted file mode 100644 index 6886006..0000000 --- a/kratos-admin-ui/nginx.conf +++ /dev/null @@ -1,19 +0,0 @@ -# Expires map -map $sent_http_content_type $expires { - default off; - text/html epoch; - text/css max; - application/javascript max; - ~image/ max; - ~font/ max; -} - -server { - listen 80; - location / { - root /usr/share/nginx/html; - try_files $uri $uri/ /index.html =404; - } - - expires $expires; -} \ No newline at end of file diff --git a/kratos-admin-ui/package-lock.json b/kratos-admin-ui/package-lock.json index e4aac41..277cdae 100644 --- a/kratos-admin-ui/package-lock.json +++ b/kratos-admin-ui/package-lock.json @@ -1,12 +1,12 @@ { "name": "kratos-admin-ui", - "version": "1.2.0", + "version": "2.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "kratos-admin-ui", - "version": "1.2.0", + "version": "2.0.0", "dependencies": { "@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@fluentui/react-components": "9.39.0", diff --git a/kratos-admin-ui/package.json b/kratos-admin-ui/package.json index 37cc379..0e0e90a 100644 --- a/kratos-admin-ui/package.json +++ b/kratos-admin-ui/package.json @@ -1,6 +1,6 @@ { "name": "kratos-admin-ui", - "version": "1.2.0", + "version": "2.0.0", "private": true, "dependencies": { "@babel/plugin-proposal-private-property-in-object": "^7.21.11", diff --git a/kratos-admin-ui/public/config.json b/kratos-admin-ui/public/config.json index 02f2ec3..1ceead1 100644 --- a/kratos-admin-ui/public/config.json +++ b/kratos-admin-ui/public/config.json @@ -1,4 +1,5 @@ { "kratosAdminURL": "http://localhost:4435", - "kratosPublicURL": "http://localhost:4430" + "kratosPublicURL": "http://localhost:4430", + "reverseProxy": false } \ No newline at end of file diff --git a/kratos-admin-ui/src/config.ts b/kratos-admin-ui/src/config.ts index eebf29f..6c8dce4 100644 --- a/kratos-admin-ui/src/config.ts +++ b/kratos-admin-ui/src/config.ts @@ -5,6 +5,7 @@ export interface KratosAdminConfig { kratosPublicURL: string; version: string; supportedVersion: string; + reverseProxy: boolean; } export interface KratosConfig { @@ -15,6 +16,7 @@ export interface KratosConfig { interface JSONConfig { kratosAdminURL?: string; kratosPublicURL?: string; + reverseProxy?: boolean; } let JSON_CONFIG: JSONConfig = {} @@ -23,24 +25,30 @@ async function loadConfig() { if (!JSON_CONFIG.kratosAdminURL) { const data = await fetch("/config.json") JSON_CONFIG = await data.json() as JSONConfig; + if (JSON_CONFIG.reverseProxy) { + // every admin-url starts with /admin, so there is no need to have /admin/admin. There is a url rewrite in the nginx config + JSON_CONFIG.kratosAdminURL = "/api"; + JSON_CONFIG.kratosPublicURL = "/api/public" + } } return JSON_CONFIG } export async function getKratosConfig() { - const urls = await loadConfig() + const configJSON = await loadConfig() return { - adminConfig: new Configuration({ basePath: urls.kratosAdminURL, baseOptions: { withCredentials: true } }), - publicConfig: new Configuration({ basePath: urls.kratosPublicURL, baseOptions: { withCredentials: true } }) + adminConfig: new Configuration({ basePath: configJSON.kratosAdminURL, baseOptions: { withCredentials: true } }), + publicConfig: new Configuration({ basePath: configJSON.kratosPublicURL, baseOptions: { withCredentials: true } }) } as KratosConfig } export async function getKratosAdminConfig() { - const urls = await loadConfig() + const configJSON = await loadConfig() return { - version: "1.1.0", + version: "2.0.0", supportedVersion: "v1.0.0", - kratosAdminURL: urls.kratosAdminURL, - kratosPublicURL: urls.kratosPublicURL + kratosAdminURL: configJSON.kratosAdminURL, + kratosPublicURL: configJSON.kratosPublicURL, + reverseProxy: configJSON.reverseProxy } as KratosAdminConfig } \ No newline at end of file diff --git a/kratos-admin-ui/src/sites/identities/identies.tsx b/kratos-admin-ui/src/sites/identities/identies.tsx index 19cec6b..2e1b358 100644 --- a/kratos-admin-ui/src/sites/identities/identies.tsx +++ b/kratos-admin-ui/src/sites/identities/identies.tsx @@ -145,6 +145,14 @@ class IdentitiesSite extends React.Component { private refreshData(showBanner: boolean) { this.refreshDataInternal(showBanner).then(() => { + }).catch(err => { + MessageService.Instance.dispatchMessage({ + message: { + intent: "error", + title: "failed to get identites" + }, + removeAfterSeconds: 4000 + }) }) } diff --git a/kratos-admin-ui/src/sites/overview.tsx b/kratos-admin-ui/src/sites/overview.tsx index 35cc951..ace9d01 100644 --- a/kratos-admin-ui/src/sites/overview.tsx +++ b/kratos-admin-ui/src/sites/overview.tsx @@ -3,6 +3,7 @@ import { MetadataApi } from "@ory/kratos-client"; import React from "react"; import { withRouter } from "react-router-dom"; import { KratosAdminConfig, getKratosAdminConfig, getKratosConfig, KratosConfig } from "../config"; +import { MessageService } from "../components/messages/messagebar"; interface OverviewState { version?: string; @@ -27,7 +28,10 @@ class OverviewSite extends React.Component { config: config }) const kratosConfig: KratosConfig = await getKratosConfig() - const metadataAPI = new MetadataApi(kratosConfig.adminConfig); + const copy = kratosConfig.adminConfig; + // admin api redirects to /admin/x. The API-Doc is wrong, at least in version 1.0.0 + copy.basePath = copy.basePath + "/admin"; + const metadataAPI = new MetadataApi(copy); const version = await metadataAPI.getVersion(); const ready = await metadataAPI.isReady(); this.setState({ @@ -39,6 +43,13 @@ class OverviewSite extends React.Component { this.setState({ version: "error", ready: "error" + }); + MessageService.Instance.dispatchMessage({ + message: { + intent: "error", + title: "failed to get kratos configuration" + }, + removeAfterSeconds: 4000 }) } } @@ -55,6 +66,10 @@ class OverviewSite extends React.Component { + + Reverse Proxy + {this.state.config?.reverseProxy ? "Yes" : "No"} + Public URI {this.state.config?.kratosPublicURL} diff --git a/kratos-admin-ui/start.sh b/kratos-admin-ui/start.sh deleted file mode 100644 index fdec137..0000000 --- a/kratos-admin-ui/start.sh +++ /dev/null @@ -1,11 +0,0 @@ -if [ -z ${KRATOS_ADMIN_URL+x} ]; then - echo "KRATOS_ADMIN_URL is unset. Please set the environment variable 'KRATOS_ADMIN_URL'"; - exit 2 -fi -if [ -z ${KRATOS_PUBLIC_URL+x} ]; then - echo "KRATOS_PUBLIC_URL is unset. Please set the environment variable 'KRATOS_PUBLIC_URL'"; - exit 3 -fi - -sh env2conf.sh /usr/share/nginx/html/config.json -nginx -g "daemon off;" \ No newline at end of file