Initial QSfera import
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
.pnpm-store/
|
||||
|
||||
# TypeScript cache
|
||||
*.tsbuildinfo
|
||||
@@ -0,0 +1,2 @@
|
||||
auto-install-peers=true
|
||||
public-hoist-pattern[]=*
|
||||
@@ -0,0 +1,45 @@
|
||||
SHELL := bash
|
||||
NAME := idp
|
||||
|
||||
ifneq (, $(shell command -v go 2> /dev/null)) # suppress `command not found warnings` for non go targets in CI
|
||||
include ../../.bingo/Variables.mk
|
||||
endif
|
||||
|
||||
include ../../.make/default.mk
|
||||
include ../../.make/go.mk
|
||||
include ../../.make/release.mk
|
||||
include ../../.make/docs.mk
|
||||
|
||||
.PHONY: node-generate-prod
|
||||
node-generate-prod: assets
|
||||
|
||||
.PHONY: assets
|
||||
assets: pnpm-build \
|
||||
assets/identifier/static/favicon.svg \
|
||||
assets/identifier/static/icon-lilac.svg
|
||||
|
||||
.PHONY: assets/identifier/static/favicon.svg # force overwrite
|
||||
assets/identifier/static/favicon.svg: pnpm-build
|
||||
cp src/images/favicon.svg assets/identifier/static/favicon.svg
|
||||
rm assets/identifier/static/favicon.ico
|
||||
|
||||
.PHONY: assets/identifier/static/icon-lilac.svg
|
||||
assets/identifier/static/icon-lilac.svg: pnpm-build
|
||||
cp src/images/icon-lilac.svg assets/identifier/static/icon-lilac.svg
|
||||
|
||||
.PHONY: pnpm-build
|
||||
pnpm-build: node_modules
|
||||
pnpm build
|
||||
|
||||
.PHONY: node_modules
|
||||
node_modules:
|
||||
pnpm install
|
||||
|
||||
.PHONY: ci-node-check-licenses
|
||||
ci-node-check-licenses: node_modules
|
||||
pnpm licenses:check
|
||||
|
||||
.PHONY: ci-node-save-licenses
|
||||
ci-node-save-licenses: node_modules
|
||||
pnpm licenses:csv
|
||||
pnpm licenses:save
|
||||
@@ -0,0 +1,82 @@
|
||||
# IDP
|
||||
|
||||
This service provides a builtin minimal OpenID Connect provider based on [LibreGraph Connect (lico)](https://github.com/libregraph/lico) for КуСфера.
|
||||
|
||||
It is mainly targeted at smaller installations. For larger setups it is recommended to replace IDP with an external OpenID Connect Provider.
|
||||
|
||||
By default, it is configured to use the КуСфера IDM service as its LDAP backend for looking up and authenticating users. Other backends like an external LDAP server can be configured via a set of [enviroment variables](https://docs.qsfera.eu/docs/dev/server/services/idp/environment-variables).
|
||||
|
||||
Note that translations provided by the IDP service are not maintained via КуСфера but part of the embedded [LibreGraph Connect Identifier](https://github.com/libregraph/lico/tree/master/identifier) package.
|
||||
|
||||
## Configuration
|
||||
|
||||
### Custom Clients
|
||||
|
||||
By default the `idp` service generates a OIDC client configuration suitable for
|
||||
using КуСфера with the standard client applications (Web, Desktop, iOS and
|
||||
Android). If you need to configure additional client it is possible to inject a
|
||||
custom configuration via `yaml`. This can be done by adding a section `clients`
|
||||
to the `idp` section of the main configuration file (`qsfera.yaml`). This section
|
||||
needs to contain configuration for all clients (including the standard clients).
|
||||
|
||||
For example if you want to add a (public) client for use with the oidc-agent you would
|
||||
need to add this snippet to the `idp` section in `qsfera.yaml`.
|
||||
|
||||
```yaml
|
||||
clients:
|
||||
- id: web
|
||||
name: КуСфера Web App
|
||||
trusted: true
|
||||
secret: ""
|
||||
redirect_uris:
|
||||
- https://qsfera.k8s:9200/
|
||||
- https://qsfera.k8s:9200/oidc-callback.html
|
||||
- https://qsfera.k8s:9200/oidc-silent-redirect.html
|
||||
post_logout_redirect_uris: []
|
||||
origins:
|
||||
- https://qsfera.k8s:9200
|
||||
application_type: ""
|
||||
- id: QsferaDesktop
|
||||
name: КуСфера Desktop Client
|
||||
trusted: false
|
||||
secret: ""
|
||||
redirect_uris:
|
||||
- http://127.0.0.1
|
||||
- http://localhost
|
||||
post_logout_redirect_uris: []
|
||||
origins: []
|
||||
application_type: native
|
||||
- id: QsferaAndroid
|
||||
name: КуСфера Android App
|
||||
trusted: false
|
||||
secret: ""
|
||||
redirect_uris:
|
||||
- oc://android.qsfera.eu
|
||||
post_logout_redirect_uris:
|
||||
- oc://android.qsfera.eu
|
||||
origins: []
|
||||
application_type: native
|
||||
- id: QsferaIOS
|
||||
name: КуСфера iOS App
|
||||
trusted: false
|
||||
secret: ""
|
||||
redirect_uris:
|
||||
- oc://ios.qsfera.eu
|
||||
post_logout_redirect_uris:
|
||||
- oc://ios.qsfera.eu
|
||||
origins: []
|
||||
application_type: native
|
||||
- id: oidc-agent
|
||||
name: OIDC Agent
|
||||
trusted: false
|
||||
secret: ""
|
||||
redirect_uris:
|
||||
- http://127.0.0.1
|
||||
- http://localhost
|
||||
post_logout_redirect_uris: []
|
||||
origins: []
|
||||
application_type: native
|
||||
```
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
*.mo
|
||||
dev-translation.json
|
||||
@@ -0,0 +1,57 @@
|
||||
# Tools
|
||||
|
||||
PNPM ?= pnpm
|
||||
MSGCAT ?= msgcat
|
||||
MSGMERGE ?= msgmerge
|
||||
MSGFMT ?= msgfmt
|
||||
|
||||
# Variables
|
||||
|
||||
POT = translation.pot
|
||||
POS ?= $(wildcard *.po)
|
||||
|
||||
# Translations
|
||||
|
||||
.PHONY: build
|
||||
build: json
|
||||
|
||||
.PYTHON: json
|
||||
json:
|
||||
@for po in $(POS); do \
|
||||
lang=$$(echo $$po | sed "s/\.po//"); \
|
||||
$(PNPM) i18next-conv -K --skipUntranslated -l $$lang -s i18n/$$po -t ../src/locales/$$lang/translation.json; \
|
||||
done
|
||||
$(PNPM) node i18n/build-json.js ../src/locales/locales.json $(POS)
|
||||
|
||||
dev-translation.json: FORCE
|
||||
$(PNPM) i18next --fail-on-warnings
|
||||
|
||||
.PHONY: extract
|
||||
extract: pot
|
||||
|
||||
.PHONY: pot
|
||||
pot: dev-translation.json
|
||||
@tmpfile1=$(shell mktemp).po; \
|
||||
tmpfile2=$(shell mktemp).po; \
|
||||
trap 'rm -f "$$tmpfile1" "$$tmpfile2"' EXIT; \
|
||||
$(PNPM) i18next-conv --project "LibreGraph Connect Identifier" -K -l en -s i18n/dev-translation.json -t $$tmpfile1; \
|
||||
$(PNPM) node i18n/build-pot.js $$tmpfile1 $$tmpfile2; \
|
||||
$(MSGCAT) --no-wrap -o $(POT) $$tmpfile2
|
||||
|
||||
.PHONY: merge
|
||||
merge: $(POS)
|
||||
|
||||
$(POS): FORCE $(POT)
|
||||
@echo -n "$@ " && \
|
||||
$(MSGMERGE) -U \
|
||||
--backup=none \
|
||||
--no-wrap \
|
||||
--sort-output \
|
||||
$@ $(POT)
|
||||
|
||||
.PHONY: stats
|
||||
stats:
|
||||
$(foreach po, $(POS), $(shell $(MSGFMT) -v --statistics $(po)))
|
||||
@- true
|
||||
|
||||
FORCE:
|
||||
@@ -0,0 +1,77 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const cldr = require('cldr');
|
||||
|
||||
if (process.argv.length < 4) {
|
||||
throw new Error('usage error: at least 2 arguments are required!');
|
||||
}
|
||||
|
||||
const output = process.argv[2];
|
||||
const pos = process.argv.slice(3);
|
||||
|
||||
const englishLanguageDisplayNames = cldr.extractLanguageDisplayNames('en');
|
||||
const englishTerritoryDisplayNames = cldr.extractTerritoryDisplayNames('en');
|
||||
|
||||
function localeCapitalize(s, locale) {
|
||||
return s.charAt(0).toLocaleUpperCase(locale) + s.slice(1)
|
||||
}
|
||||
|
||||
function Locale(locale, overrides={}) {
|
||||
let ietf = null;
|
||||
let [code, country] = locale.split('-', 2);
|
||||
switch(locale) {
|
||||
// Additional mapping.
|
||||
case 'zh-CN':
|
||||
code = 'zh_hans';
|
||||
ietf = code;
|
||||
country = null;
|
||||
break;
|
||||
case 'zh-TW':
|
||||
code = 'zh_hant';
|
||||
ietf = code;
|
||||
country = null;
|
||||
break;
|
||||
default:
|
||||
}
|
||||
overrides = ietf ? {
|
||||
ietf,
|
||||
...overrides,
|
||||
} : overrides;
|
||||
|
||||
const languageDisplayNames = cldr.extractLanguageDisplayNames(code);
|
||||
if (languageDisplayNames) {
|
||||
let name = localeCapitalize(englishLanguageDisplayNames[code], 'en');
|
||||
let nativeName = localeCapitalize(languageDisplayNames[code], locale);
|
||||
if (name && nativeName) {
|
||||
if (country) {
|
||||
let countryNative = localeCapitalize(cldr.extractTerritoryDisplayNames(code)[country], locale);
|
||||
nativeName = `${nativeName} (${countryNative})`;
|
||||
name = `${name} (${localeCapitalize(englishTerritoryDisplayNames[country], 'en')})`;
|
||||
}
|
||||
return {
|
||||
locale,
|
||||
name,
|
||||
nativeName,
|
||||
...cldr.extractLayout(code),
|
||||
...overrides,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var locales = [
|
||||
Locale('en-GB', { name: 'English', nativeName: 'English' }), // Always add en-GB as English.
|
||||
]
|
||||
|
||||
pos.map((po) => {
|
||||
const locale = Locale(po.replace(/\.[^/.]+$/, ''));
|
||||
if (locale) {
|
||||
locales.push(locale);
|
||||
}
|
||||
});
|
||||
|
||||
locales.sort((a, b) => {
|
||||
return a.locale > b.locale ? 1 : -1;
|
||||
})
|
||||
|
||||
require('fs').writeFileSync(output, JSON.stringify(locales, null, 2));
|
||||
@@ -0,0 +1,26 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
var gettextParser = require("gettext-parser");
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
var input = require('fs').readFileSync(args[0]);
|
||||
var po = gettextParser.po.parse(input);
|
||||
|
||||
Object.entries(po.translations[""]).map(([context, v]) => {
|
||||
if (v.msgid) {
|
||||
if (!v.comments) {
|
||||
v.comments = {};
|
||||
}
|
||||
v.comments.extracted = "From: " + (v.comments.reference || '');
|
||||
}
|
||||
});
|
||||
|
||||
delete po.headers["PO-Revision-Date"];
|
||||
delete po.headers["Language"];
|
||||
delete po.headers["Plural-Forms"];
|
||||
delete po.headers["mime-version"];
|
||||
po.headers["MIME-Version"] = "1.0";
|
||||
|
||||
var output = gettextParser.po.compile(po);
|
||||
require('fs').writeFileSync(args[1], output);
|
||||
@@ -0,0 +1,224 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: \n"
|
||||
"POT-Creation-Date: 2022-03-09T19:33:04.641Z\n"
|
||||
"PO-Revision-Date: 2022-03-10 09:40+0100\n"
|
||||
"Last-Translator: Simon Eisenmann <simon@longsleep.org>\n"
|
||||
"Language-Team: German <http://translate.kopano.io/projects/kopano/konnect/de/>\n"
|
||||
"Language: de\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
||||
"X-Generator: Poedit 2.3\n"
|
||||
|
||||
#. From: konnect##consent##message
|
||||
#: konnect##consent##message
|
||||
msgid "<0><0><0></0></0></0> wants to"
|
||||
msgstr "<0><0><0></0></0></0> möchte"
|
||||
|
||||
#. From: konnect##chooseaccount##useOther##persona##label
|
||||
#: konnect##chooseaccount##useOther##persona##label
|
||||
msgid "?"
|
||||
msgstr "?"
|
||||
|
||||
#. From: konnect##scopeDescription##aliasBasic
|
||||
#: konnect##scopeDescription##aliasBasic
|
||||
msgid "Access your basic account information"
|
||||
msgstr "Zugriff auf Ihre grundlegenden Benutzerinformationen"
|
||||
|
||||
#. From: konnect##consent##allowButton##label
|
||||
#: konnect##consent##allowButton##label
|
||||
msgid "Allow"
|
||||
msgstr "Einverstanden"
|
||||
|
||||
#. From: konnect##consent##question
|
||||
#: konnect##consent##question
|
||||
msgid "Allow <1><0></0></1> to do this?"
|
||||
msgstr "<1><0></0></1> den Zugriff gestatten?"
|
||||
|
||||
#. From: konnect##consent##consequence
|
||||
#: konnect##consent##consequence
|
||||
msgid "By clicking Allow, you allow this app to use your information."
|
||||
msgstr "Wenn Sie Einverstanden klicken, erhält die App Zugriff auf Ihre Informationen."
|
||||
|
||||
#. From: konnect##consent##cancelButton##label
|
||||
#: konnect##consent##cancelButton##label
|
||||
msgid "Cancel"
|
||||
msgstr "Abbrechen"
|
||||
|
||||
#. From: konnect##chooseaccount##headline
|
||||
#: konnect##chooseaccount##headline
|
||||
msgid "Choose an account"
|
||||
msgstr "Konto auswählen"
|
||||
|
||||
#. From: konnect##consent##tooltip##client
|
||||
#: konnect##consent##tooltip##client
|
||||
msgid "Clicking \"Allow\" will redirect you to: {{redirectURI}}"
|
||||
msgstr "Wenn Sie \"Einverstanden\" klicken werden Sie zu {{redirectURI}} weitergeleitet"
|
||||
|
||||
#. From: konnect##login##usernameField##placeholder##email
|
||||
#: konnect##login##usernameField##placeholder##email
|
||||
msgid "Email"
|
||||
msgstr "E-Mail"
|
||||
|
||||
#. From: konnect##error##login##validate##missingUsername
|
||||
#: konnect##error##login##validate##missingUsername
|
||||
msgid "Enter a valid value."
|
||||
msgstr "Geben Sie einen gültigen Wert ein."
|
||||
|
||||
#. From: konnect##error##login##validate##missingPassword
|
||||
#: konnect##error##login##validate##missingPassword
|
||||
msgid "Enter your password."
|
||||
msgstr "Geben Sie ein Passwort ein."
|
||||
|
||||
#. From: konnect##loading##error##headline
|
||||
#: konnect##loading##error##headline
|
||||
msgid "Failed to connect to server"
|
||||
msgstr "Verbindung zum Server fehlgeschlagen"
|
||||
|
||||
#. From: konnect##goodbye##headline
|
||||
#: konnect##goodbye##headline
|
||||
msgid "Goodbye"
|
||||
msgstr "Bis bald"
|
||||
|
||||
#. From: konnect##goodbye##confirm##headline
|
||||
#: konnect##goodbye##confirm##headline
|
||||
msgid "Hello {{displayName}}"
|
||||
msgstr "Hallo {{displayName}}"
|
||||
|
||||
#. From: konnect##consent##headline
|
||||
#: konnect##consent##headline
|
||||
msgid "Hi {{displayName}}"
|
||||
msgstr "Hallo {{displayName}}"
|
||||
|
||||
#. From: konnect##login##usernameField##placeholder##identity
|
||||
#: konnect##login##usernameField##placeholder##identity
|
||||
msgid "Identity"
|
||||
msgstr "Identität"
|
||||
|
||||
#. From: konnect##scopeDescription##offlineAccess
|
||||
#: konnect##scopeDescription##offlineAccess
|
||||
msgid "Keep the allowed access persistently and forever"
|
||||
msgstr "Dauerhaften Zugriff (läuft nicht ab)"
|
||||
|
||||
#. From: konnect##error##login##failed
|
||||
#: konnect##error##login##failed
|
||||
msgid "Logon failed. Please verify your credentials and try again."
|
||||
msgstr "Anmeldung fehlgeschlagen. Bitte überprüfen Sie Ihre Eingabe und versuchen Sie es noch einmal."
|
||||
|
||||
#. From: konnect##error##http##networkError
|
||||
#: konnect##error##http##networkError
|
||||
msgid "Network error. Please check your connection and try again."
|
||||
msgstr "Netzwerkfehler. Bitte prüfen Sie Ihre Verbindung und versuchen Sie es noch einmal."
|
||||
|
||||
#. From: konnect##login##nextButton##label
|
||||
#: konnect##login##nextButton##label
|
||||
msgid "Next"
|
||||
msgstr "Weiter"
|
||||
|
||||
#. From: konnect##login##passwordField##label
|
||||
#: konnect##login##passwordField##label
|
||||
msgid "Password"
|
||||
msgstr "Passwort"
|
||||
|
||||
#. From: konnect##goodbye##message##confirm
|
||||
#: konnect##goodbye##message##confirm
|
||||
msgid "Press the button below, to sign out from your account now."
|
||||
msgstr "Klicken Sie auf die Schaltfläche unten um sich aus Ihrem Konto abzumelden."
|
||||
|
||||
#. From: konnect##login##retryButton##label
|
||||
#: konnect##login##retryButton##label
|
||||
msgid "Retry"
|
||||
msgstr "Wiederholen"
|
||||
|
||||
#. From: konnect##scopeDescription##scope
|
||||
#: konnect##scopeDescription##scope
|
||||
msgid "Scope: {{scope}}"
|
||||
msgstr "Geltungsbereich: {{scope}}"
|
||||
|
||||
#. From: konnect##login##headline
|
||||
#: konnect##login##headline
|
||||
msgid "Sign in"
|
||||
msgstr "Anmelden"
|
||||
|
||||
#. From: konnect##goodbye##signoutButton##label
|
||||
#. konnect##welcome##signoutButton##label
|
||||
#: konnect##goodbye##signoutButton##label
|
||||
#: konnect##welcome##signoutButton##label
|
||||
msgid "Sign out"
|
||||
msgstr "Abmelden"
|
||||
|
||||
#. From: konnect##error##http##unexpectedResponseStatus
|
||||
#: konnect##error##http##unexpectedResponseStatus
|
||||
msgid "Unexpected HTTP response: {{status}}. Please check your connection and try again."
|
||||
msgstr "Unerwartete HTTP-Antwort: {{status}}. Bitte prüfen Sie Ihre Verbindung und versuchen Sie es noch einmal."
|
||||
|
||||
#. From: konnect##error##http##unexpectedResponseState
|
||||
#: konnect##error##http##unexpectedResponseState
|
||||
msgid "Unexpected response state: {{state}}"
|
||||
msgstr "Unerwarteter Antwort-Status: {{state}}"
|
||||
|
||||
#. From: konnect##chooseaccount##useOther##label
|
||||
#: konnect##chooseaccount##useOther##label
|
||||
msgid "Use another account"
|
||||
msgstr "Anderes Konto"
|
||||
|
||||
#. From: konnect##login##usernameField##placeholder##username
|
||||
#: konnect##login##usernameField##placeholder##username
|
||||
msgid "Username"
|
||||
msgstr "Benutzername"
|
||||
|
||||
#. From: konnect##welcome##headline
|
||||
#: konnect##welcome##headline
|
||||
msgid "Welcome {{displayName}}"
|
||||
msgstr "Willkommen {{displayName}}"
|
||||
|
||||
#. From: konnect##welcome##message
|
||||
#: konnect##welcome##message
|
||||
msgid "You are signed in - awesome!"
|
||||
msgstr "Sie sind angemeldet - super!"
|
||||
|
||||
#. From: konnect##goodbye##message##close
|
||||
#: konnect##goodbye##message##close
|
||||
msgid "You can close this window now."
|
||||
msgstr "Sie können dieses Fenster jetzt schließen."
|
||||
|
||||
#. From: konnect##goodbye##confirm##subHeader
|
||||
#: konnect##goodbye##confirm##subHeader
|
||||
msgid "please confirm sign out"
|
||||
msgstr "Bitte bestätigen Sie, dass Sie sich abmelden möchten"
|
||||
|
||||
#. From: konnect##chooseaccount##subHeader
|
||||
#: konnect##chooseaccount##subHeader
|
||||
msgid "to sign in"
|
||||
msgstr "das angemeldet werden soll"
|
||||
|
||||
#. From: konnect##goodbye##subHeader
|
||||
#: konnect##goodbye##subHeader
|
||||
msgid "you have been signed out from your account"
|
||||
msgstr "sie sind jetzt abgemeldet"
|
||||
|
||||
#~ msgctxt "konnect.consent.question"
|
||||
#~ msgid "Allow {clientDisplayName} to do this?"
|
||||
#~ msgstr "{clientDisplayName} den Zugriff gestatten?"
|
||||
|
||||
#~ msgctxt "konnect.error.login.validate.missingUsername"
|
||||
#~ msgid "Enter an username"
|
||||
#~ msgstr "Geben Sie einen Benutzername ein"
|
||||
|
||||
#~ msgctxt "konnect.welcome.signoutButton.label"
|
||||
#~ msgid "Sign out"
|
||||
#~ msgstr "Abmelden"
|
||||
|
||||
#~ msgctxt "konnect.chooseaccount.subHeader"
|
||||
#~ msgid "to sign in to Kopano"
|
||||
#~ msgstr "um sich bei Kopano anzumelden"
|
||||
|
||||
#~ msgctxt "konnect.login.subHeader"
|
||||
#~ msgid "with your Kopano account"
|
||||
#~ msgstr "mit Ihrem Kopano Konto"
|
||||
|
||||
#~ msgctxt "konnect.consent.message"
|
||||
#~ msgid "{clientDisplayName} wants to"
|
||||
#~ msgstr "{clientDisplayName} möchte"
|
||||
@@ -0,0 +1,199 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: LibreGraph Connect Identifier\n"
|
||||
"POT-Creation-Date: 2022-03-09T19:33:04.641Z\n"
|
||||
"PO-Revision-Date: 2022-03-09T10:29:32.094Z\n"
|
||||
"Last-Translator: Automatically generated\n"
|
||||
"Language-Team: none\n"
|
||||
"Language: es\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#. From: konnect##consent##message
|
||||
#: konnect##consent##message
|
||||
msgid "<0><0><0></0></0></0> wants to"
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##chooseaccount##useOther##persona##label
|
||||
#: konnect##chooseaccount##useOther##persona##label
|
||||
msgid "?"
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##scopeDescription##aliasBasic
|
||||
#: konnect##scopeDescription##aliasBasic
|
||||
msgid "Access your basic account information"
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##consent##allowButton##label
|
||||
#: konnect##consent##allowButton##label
|
||||
msgid "Allow"
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##consent##question
|
||||
#: konnect##consent##question
|
||||
msgid "Allow <1><0></0></1> to do this?"
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##consent##consequence
|
||||
#: konnect##consent##consequence
|
||||
msgid "By clicking Allow, you allow this app to use your information."
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##consent##cancelButton##label
|
||||
#: konnect##consent##cancelButton##label
|
||||
msgid "Cancel"
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##chooseaccount##headline
|
||||
#: konnect##chooseaccount##headline
|
||||
msgid "Choose an account"
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##consent##tooltip##client
|
||||
#: konnect##consent##tooltip##client
|
||||
msgid "Clicking \"Allow\" will redirect you to: {{redirectURI}}"
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##login##usernameField##placeholder##email
|
||||
#: konnect##login##usernameField##placeholder##email
|
||||
msgid "Email"
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##error##login##validate##missingUsername
|
||||
#: konnect##error##login##validate##missingUsername
|
||||
msgid "Enter a valid value."
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##error##login##validate##missingPassword
|
||||
#: konnect##error##login##validate##missingPassword
|
||||
msgid "Enter your password."
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##loading##error##headline
|
||||
#: konnect##loading##error##headline
|
||||
msgid "Failed to connect to server"
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##goodbye##headline
|
||||
#: konnect##goodbye##headline
|
||||
msgid "Goodbye"
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##goodbye##confirm##headline
|
||||
#: konnect##goodbye##confirm##headline
|
||||
msgid "Hello {{displayName}}"
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##consent##headline
|
||||
#: konnect##consent##headline
|
||||
msgid "Hi {{displayName}}"
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##login##usernameField##placeholder##identity
|
||||
#: konnect##login##usernameField##placeholder##identity
|
||||
msgid "Identity"
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##scopeDescription##offlineAccess
|
||||
#: konnect##scopeDescription##offlineAccess
|
||||
msgid "Keep the allowed access persistently and forever"
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##error##login##failed
|
||||
#: konnect##error##login##failed
|
||||
msgid "Logon failed. Please verify your credentials and try again."
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##error##http##networkError
|
||||
#: konnect##error##http##networkError
|
||||
msgid "Network error. Please check your connection and try again."
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##login##nextButton##label
|
||||
#: konnect##login##nextButton##label
|
||||
msgid "Next"
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##login##passwordField##label
|
||||
#: konnect##login##passwordField##label
|
||||
msgid "Password"
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##goodbye##message##confirm
|
||||
#: konnect##goodbye##message##confirm
|
||||
msgid "Press the button below, to sign out from your account now."
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##login##retryButton##label
|
||||
#: konnect##login##retryButton##label
|
||||
msgid "Retry"
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##scopeDescription##scope
|
||||
#: konnect##scopeDescription##scope
|
||||
msgid "Scope: {{scope}}"
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##login##headline
|
||||
#: konnect##login##headline
|
||||
msgid "Sign in"
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##goodbye##signoutButton##label
|
||||
#. konnect##welcome##signoutButton##label
|
||||
#: konnect##goodbye##signoutButton##label
|
||||
#: konnect##welcome##signoutButton##label
|
||||
msgid "Sign out"
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##error##http##unexpectedResponseStatus
|
||||
#: konnect##error##http##unexpectedResponseStatus
|
||||
msgid "Unexpected HTTP response: {{status}}. Please check your connection and try again."
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##error##http##unexpectedResponseState
|
||||
#: konnect##error##http##unexpectedResponseState
|
||||
msgid "Unexpected response state: {{state}}"
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##chooseaccount##useOther##label
|
||||
#: konnect##chooseaccount##useOther##label
|
||||
msgid "Use another account"
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##login##usernameField##placeholder##username
|
||||
#: konnect##login##usernameField##placeholder##username
|
||||
msgid "Username"
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##welcome##headline
|
||||
#: konnect##welcome##headline
|
||||
msgid "Welcome {{displayName}}"
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##welcome##message
|
||||
#: konnect##welcome##message
|
||||
msgid "You are signed in - awesome!"
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##goodbye##message##close
|
||||
#: konnect##goodbye##message##close
|
||||
msgid "You can close this window now."
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##goodbye##confirm##subHeader
|
||||
#: konnect##goodbye##confirm##subHeader
|
||||
msgid "please confirm sign out"
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##chooseaccount##subHeader
|
||||
#: konnect##chooseaccount##subHeader
|
||||
msgid "to sign in"
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##goodbye##subHeader
|
||||
#: konnect##goodbye##subHeader
|
||||
msgid "you have been signed out from your account"
|
||||
msgstr ""
|
||||
@@ -0,0 +1,224 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: \n"
|
||||
"POT-Creation-Date: 2022-03-09T19:33:04.641Z\n"
|
||||
"PO-Revision-Date: 2022-03-09 18:42+0100\n"
|
||||
"Last-Translator: Simon Eisenmann <simon@longsleep.org>\n"
|
||||
"Language-Team: French <http://translate.kopano.io/projects/kopano/konnect/fr/>\n"
|
||||
"Language: fr\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=n > 1;\n"
|
||||
"X-Generator: Poedit 2.3\n"
|
||||
|
||||
#. From: konnect##consent##message
|
||||
#: konnect##consent##message
|
||||
msgid "<0><0><0></0></0></0> wants to"
|
||||
msgstr "<0><0><0></0></0></0> souhaite"
|
||||
|
||||
#. From: konnect##chooseaccount##useOther##persona##label
|
||||
#: konnect##chooseaccount##useOther##persona##label
|
||||
msgid "?"
|
||||
msgstr "?"
|
||||
|
||||
#. From: konnect##scopeDescription##aliasBasic
|
||||
#: konnect##scopeDescription##aliasBasic
|
||||
msgid "Access your basic account information"
|
||||
msgstr "Consulter les informations de base de votre compte"
|
||||
|
||||
#. From: konnect##consent##allowButton##label
|
||||
#: konnect##consent##allowButton##label
|
||||
msgid "Allow"
|
||||
msgstr "Autoriser"
|
||||
|
||||
#. From: konnect##consent##question
|
||||
#: konnect##consent##question
|
||||
msgid "Allow <1><0></0></1> to do this?"
|
||||
msgstr "Autoriser <1><0></0></1> à faire cela?"
|
||||
|
||||
#. From: konnect##consent##consequence
|
||||
#: konnect##consent##consequence
|
||||
msgid "By clicking Allow, you allow this app to use your information."
|
||||
msgstr "En cliquant, vous autoriser l'app à accéder à vos informations."
|
||||
|
||||
#. From: konnect##consent##cancelButton##label
|
||||
#: konnect##consent##cancelButton##label
|
||||
msgid "Cancel"
|
||||
msgstr "Annuler"
|
||||
|
||||
#. From: konnect##chooseaccount##headline
|
||||
#: konnect##chooseaccount##headline
|
||||
msgid "Choose an account"
|
||||
msgstr "Choisir un compte"
|
||||
|
||||
#. From: konnect##consent##tooltip##client
|
||||
#: konnect##consent##tooltip##client
|
||||
msgid "Clicking \"Allow\" will redirect you to: {{redirectURI}}"
|
||||
msgstr "En cliquant \"Autoriser\" vous serez redirigé vers: {{redirectURI}}"
|
||||
|
||||
#. From: konnect##login##usernameField##placeholder##email
|
||||
#: konnect##login##usernameField##placeholder##email
|
||||
msgid "Email"
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##error##login##validate##missingUsername
|
||||
#: konnect##error##login##validate##missingUsername
|
||||
msgid "Enter a valid value."
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##error##login##validate##missingPassword
|
||||
#: konnect##error##login##validate##missingPassword
|
||||
msgid "Enter your password."
|
||||
msgstr "Saisir un mot de passe."
|
||||
|
||||
#. From: konnect##loading##error##headline
|
||||
#: konnect##loading##error##headline
|
||||
msgid "Failed to connect to server"
|
||||
msgstr "La connexion au serveur a échoué"
|
||||
|
||||
#. From: konnect##goodbye##headline
|
||||
#: konnect##goodbye##headline
|
||||
msgid "Goodbye"
|
||||
msgstr "Au revoir"
|
||||
|
||||
#. From: konnect##goodbye##confirm##headline
|
||||
#: konnect##goodbye##confirm##headline
|
||||
msgid "Hello {{displayName}}"
|
||||
msgstr "Bonjour {{displayName}}"
|
||||
|
||||
#. From: konnect##consent##headline
|
||||
#: konnect##consent##headline
|
||||
msgid "Hi {{displayName}}"
|
||||
msgstr "Bonjour {{displayName}}"
|
||||
|
||||
#. From: konnect##login##usernameField##placeholder##identity
|
||||
#: konnect##login##usernameField##placeholder##identity
|
||||
msgid "Identity"
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##scopeDescription##offlineAccess
|
||||
#: konnect##scopeDescription##offlineAccess
|
||||
msgid "Keep the allowed access persistently and forever"
|
||||
msgstr "Conserver les autorisations d'accès à l'avenir"
|
||||
|
||||
#. From: konnect##error##login##failed
|
||||
#: konnect##error##login##failed
|
||||
msgid "Logon failed. Please verify your credentials and try again."
|
||||
msgstr "Echec de connexion. Vérifier vos identifiants et essayer à nouveau."
|
||||
|
||||
#. From: konnect##error##http##networkError
|
||||
#: konnect##error##http##networkError
|
||||
msgid "Network error. Please check your connection and try again."
|
||||
msgstr "Erreur réseau. Vérifier votre connexion, et réessayer."
|
||||
|
||||
#. From: konnect##login##nextButton##label
|
||||
#: konnect##login##nextButton##label
|
||||
msgid "Next"
|
||||
msgstr "Suivant"
|
||||
|
||||
#. From: konnect##login##passwordField##label
|
||||
#: konnect##login##passwordField##label
|
||||
msgid "Password"
|
||||
msgstr "Mot de passe"
|
||||
|
||||
#. From: konnect##goodbye##message##confirm
|
||||
#: konnect##goodbye##message##confirm
|
||||
msgid "Press the button below, to sign out from your account now."
|
||||
msgstr "Cliquer le bouton ci-dessous, pour quitter."
|
||||
|
||||
#. From: konnect##login##retryButton##label
|
||||
#: konnect##login##retryButton##label
|
||||
msgid "Retry"
|
||||
msgstr "Réessayer"
|
||||
|
||||
#. From: konnect##scopeDescription##scope
|
||||
#: konnect##scopeDescription##scope
|
||||
msgid "Scope: {{scope}}"
|
||||
msgstr "Portée : {{scope}}"
|
||||
|
||||
#. From: konnect##login##headline
|
||||
#: konnect##login##headline
|
||||
msgid "Sign in"
|
||||
msgstr "Identification"
|
||||
|
||||
#. From: konnect##goodbye##signoutButton##label
|
||||
#. konnect##welcome##signoutButton##label
|
||||
#: konnect##goodbye##signoutButton##label
|
||||
#: konnect##welcome##signoutButton##label
|
||||
msgid "Sign out"
|
||||
msgstr "Quitter"
|
||||
|
||||
#. From: konnect##error##http##unexpectedResponseStatus
|
||||
#: konnect##error##http##unexpectedResponseStatus
|
||||
msgid "Unexpected HTTP response: {{status}}. Please check your connection and try again."
|
||||
msgstr "Erreur HTTP inattendue : {{status}}. Vérifier votre connexion et réessayer."
|
||||
|
||||
#. From: konnect##error##http##unexpectedResponseState
|
||||
#: konnect##error##http##unexpectedResponseState
|
||||
msgid "Unexpected response state: {{state}}"
|
||||
msgstr "Erreur d'état inattendue : {{state}}"
|
||||
|
||||
#. From: konnect##chooseaccount##useOther##label
|
||||
#: konnect##chooseaccount##useOther##label
|
||||
msgid "Use another account"
|
||||
msgstr "Utiliser un autre compte"
|
||||
|
||||
#. From: konnect##login##usernameField##placeholder##username
|
||||
#: konnect##login##usernameField##placeholder##username
|
||||
msgid "Username"
|
||||
msgstr "Utilisateur"
|
||||
|
||||
#. From: konnect##welcome##headline
|
||||
#: konnect##welcome##headline
|
||||
msgid "Welcome {{displayName}}"
|
||||
msgstr "Bienvenue {{displayName}}"
|
||||
|
||||
#. From: konnect##welcome##message
|
||||
#: konnect##welcome##message
|
||||
msgid "You are signed in - awesome!"
|
||||
msgstr "Magnifique - Vous êtes connecté!"
|
||||
|
||||
#. From: konnect##goodbye##message##close
|
||||
#: konnect##goodbye##message##close
|
||||
msgid "You can close this window now."
|
||||
msgstr "Vous pouvez fermer cette fenêtre à présent."
|
||||
|
||||
#. From: konnect##goodbye##confirm##subHeader
|
||||
#: konnect##goodbye##confirm##subHeader
|
||||
msgid "please confirm sign out"
|
||||
msgstr "confirmer votre déconnexion"
|
||||
|
||||
#. From: konnect##chooseaccount##subHeader
|
||||
#: konnect##chooseaccount##subHeader
|
||||
msgid "to sign in"
|
||||
msgstr "identification"
|
||||
|
||||
#. From: konnect##goodbye##subHeader
|
||||
#: konnect##goodbye##subHeader
|
||||
msgid "you have been signed out from your account"
|
||||
msgstr "vous avez été déconnecté"
|
||||
|
||||
#~ msgctxt "konnect.consent.question"
|
||||
#~ msgid "Allow {clientDisplayName} to do this?"
|
||||
#~ msgstr "Autoriser {clientDisplayName} à faire cela ?"
|
||||
|
||||
#~ msgctxt "konnect.error.login.validate.missingUsername"
|
||||
#~ msgid "Enter an username"
|
||||
#~ msgstr "Saisir un identifiant"
|
||||
|
||||
#~ msgctxt "konnect.welcome.signoutButton.label"
|
||||
#~ msgid "Sign out"
|
||||
#~ msgstr "Quitter"
|
||||
|
||||
#~ msgctxt "konnect.chooseaccount.subHeader"
|
||||
#~ msgid "to sign in to Kopano"
|
||||
#~ msgstr "pour vous authentifier dans Kopano"
|
||||
|
||||
#~ msgctxt "konnect.login.subHeader"
|
||||
#~ msgid "with your Kopano account"
|
||||
#~ msgstr "avec vos identifiants Kopano"
|
||||
|
||||
#~ msgctxt "konnect.consent.message"
|
||||
#~ msgid "{clientDisplayName} wants to"
|
||||
#~ msgstr "{clientDisplayName} souhaite"
|
||||
@@ -0,0 +1,200 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: LibreGraph Connect Identifier\n"
|
||||
"POT-Creation-Date: 2022-03-09T19:33:04.641Z\n"
|
||||
"PO-Revision-Date: 2022-03-09 11:22+0100\n"
|
||||
"Last-Translator: Simon Eisenmann <simon@longsleep.org>\n"
|
||||
"Language-Team: \n"
|
||||
"Language: it\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
"X-Generator: Poedit 2.3\n"
|
||||
|
||||
#. From: konnect##consent##message
|
||||
#: konnect##consent##message
|
||||
msgid "<0><0><0></0></0></0> wants to"
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##chooseaccount##useOther##persona##label
|
||||
#: konnect##chooseaccount##useOther##persona##label
|
||||
msgid "?"
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##scopeDescription##aliasBasic
|
||||
#: konnect##scopeDescription##aliasBasic
|
||||
msgid "Access your basic account information"
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##consent##allowButton##label
|
||||
#: konnect##consent##allowButton##label
|
||||
msgid "Allow"
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##consent##question
|
||||
#: konnect##consent##question
|
||||
msgid "Allow <1><0></0></1> to do this?"
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##consent##consequence
|
||||
#: konnect##consent##consequence
|
||||
msgid "By clicking Allow, you allow this app to use your information."
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##consent##cancelButton##label
|
||||
#: konnect##consent##cancelButton##label
|
||||
msgid "Cancel"
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##chooseaccount##headline
|
||||
#: konnect##chooseaccount##headline
|
||||
msgid "Choose an account"
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##consent##tooltip##client
|
||||
#: konnect##consent##tooltip##client
|
||||
msgid "Clicking \"Allow\" will redirect you to: {{redirectURI}}"
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##login##usernameField##placeholder##email
|
||||
#: konnect##login##usernameField##placeholder##email
|
||||
msgid "Email"
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##error##login##validate##missingUsername
|
||||
#: konnect##error##login##validate##missingUsername
|
||||
msgid "Enter a valid value."
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##error##login##validate##missingPassword
|
||||
#: konnect##error##login##validate##missingPassword
|
||||
msgid "Enter your password."
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##loading##error##headline
|
||||
#: konnect##loading##error##headline
|
||||
msgid "Failed to connect to server"
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##goodbye##headline
|
||||
#: konnect##goodbye##headline
|
||||
msgid "Goodbye"
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##goodbye##confirm##headline
|
||||
#: konnect##goodbye##confirm##headline
|
||||
msgid "Hello {{displayName}}"
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##consent##headline
|
||||
#: konnect##consent##headline
|
||||
msgid "Hi {{displayName}}"
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##login##usernameField##placeholder##identity
|
||||
#: konnect##login##usernameField##placeholder##identity
|
||||
msgid "Identity"
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##scopeDescription##offlineAccess
|
||||
#: konnect##scopeDescription##offlineAccess
|
||||
msgid "Keep the allowed access persistently and forever"
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##error##login##failed
|
||||
#: konnect##error##login##failed
|
||||
msgid "Logon failed. Please verify your credentials and try again."
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##error##http##networkError
|
||||
#: konnect##error##http##networkError
|
||||
msgid "Network error. Please check your connection and try again."
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##login##nextButton##label
|
||||
#: konnect##login##nextButton##label
|
||||
msgid "Next"
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##login##passwordField##label
|
||||
#: konnect##login##passwordField##label
|
||||
msgid "Password"
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##goodbye##message##confirm
|
||||
#: konnect##goodbye##message##confirm
|
||||
msgid "Press the button below, to sign out from your account now."
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##login##retryButton##label
|
||||
#: konnect##login##retryButton##label
|
||||
msgid "Retry"
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##scopeDescription##scope
|
||||
#: konnect##scopeDescription##scope
|
||||
msgid "Scope: {{scope}}"
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##login##headline
|
||||
#: konnect##login##headline
|
||||
msgid "Sign in"
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##goodbye##signoutButton##label
|
||||
#. konnect##welcome##signoutButton##label
|
||||
#: konnect##goodbye##signoutButton##label
|
||||
#: konnect##welcome##signoutButton##label
|
||||
msgid "Sign out"
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##error##http##unexpectedResponseStatus
|
||||
#: konnect##error##http##unexpectedResponseStatus
|
||||
msgid "Unexpected HTTP response: {{status}}. Please check your connection and try again."
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##error##http##unexpectedResponseState
|
||||
#: konnect##error##http##unexpectedResponseState
|
||||
msgid "Unexpected response state: {{state}}"
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##chooseaccount##useOther##label
|
||||
#: konnect##chooseaccount##useOther##label
|
||||
msgid "Use another account"
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##login##usernameField##placeholder##username
|
||||
#: konnect##login##usernameField##placeholder##username
|
||||
msgid "Username"
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##welcome##headline
|
||||
#: konnect##welcome##headline
|
||||
msgid "Welcome {{displayName}}"
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##welcome##message
|
||||
#: konnect##welcome##message
|
||||
msgid "You are signed in - awesome!"
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##goodbye##message##close
|
||||
#: konnect##goodbye##message##close
|
||||
msgid "You can close this window now."
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##goodbye##confirm##subHeader
|
||||
#: konnect##goodbye##confirm##subHeader
|
||||
msgid "please confirm sign out"
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##chooseaccount##subHeader
|
||||
#: konnect##chooseaccount##subHeader
|
||||
msgid "to sign in"
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##goodbye##subHeader
|
||||
#: konnect##goodbye##subHeader
|
||||
msgid "you have been signed out from your account"
|
||||
msgstr ""
|
||||
@@ -0,0 +1,224 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: \n"
|
||||
"POT-Creation-Date: 2022-03-09T19:33:04.641Z\n"
|
||||
"PO-Revision-Date: 2022-03-09 18:45+0100\n"
|
||||
"Last-Translator: Simon Eisenmann <simon@longsleep.org>\n"
|
||||
"Language-Team: Dutch <http://translate.kopano.io/projects/kopano/konnect/nl/>\n"
|
||||
"Language: nl\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
||||
"X-Generator: Poedit 2.3\n"
|
||||
|
||||
#. From: konnect##consent##message
|
||||
#: konnect##consent##message
|
||||
msgid "<0><0><0></0></0></0> wants to"
|
||||
msgstr "<0><0><0></0></0></0> wil"
|
||||
|
||||
#. From: konnect##chooseaccount##useOther##persona##label
|
||||
#: konnect##chooseaccount##useOther##persona##label
|
||||
msgid "?"
|
||||
msgstr "?"
|
||||
|
||||
#. From: konnect##scopeDescription##aliasBasic
|
||||
#: konnect##scopeDescription##aliasBasic
|
||||
msgid "Access your basic account information"
|
||||
msgstr "Basis accountgegevens weergeven"
|
||||
|
||||
#. From: konnect##consent##allowButton##label
|
||||
#: konnect##consent##allowButton##label
|
||||
msgid "Allow"
|
||||
msgstr "Toestaan"
|
||||
|
||||
#. From: konnect##consent##question
|
||||
#: konnect##consent##question
|
||||
msgid "Allow <1><0></0></1> to do this?"
|
||||
msgstr "<1><0></0></1> toestaan dit te doen?"
|
||||
|
||||
#. From: konnect##consent##consequence
|
||||
#: konnect##consent##consequence
|
||||
msgid "By clicking Allow, you allow this app to use your information."
|
||||
msgstr "Door op Toestaan te klikken, krijgt deze app toestemming je informatie te gebruiken."
|
||||
|
||||
#. From: konnect##consent##cancelButton##label
|
||||
#: konnect##consent##cancelButton##label
|
||||
msgid "Cancel"
|
||||
msgstr "Annuleren"
|
||||
|
||||
#. From: konnect##chooseaccount##headline
|
||||
#: konnect##chooseaccount##headline
|
||||
msgid "Choose an account"
|
||||
msgstr "Account kiezen"
|
||||
|
||||
#. From: konnect##consent##tooltip##client
|
||||
#: konnect##consent##tooltip##client
|
||||
msgid "Clicking \"Allow\" will redirect you to: {{redirectURI}}"
|
||||
msgstr "Door op \"Toestaan\" te klikken word je doorverwezen naar: {{redirectURI}}"
|
||||
|
||||
#. From: konnect##login##usernameField##placeholder##email
|
||||
#: konnect##login##usernameField##placeholder##email
|
||||
msgid "Email"
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##error##login##validate##missingUsername
|
||||
#: konnect##error##login##validate##missingUsername
|
||||
msgid "Enter a valid value."
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##error##login##validate##missingPassword
|
||||
#: konnect##error##login##validate##missingPassword
|
||||
msgid "Enter your password."
|
||||
msgstr "Voer een wachtwoord in."
|
||||
|
||||
#. From: konnect##loading##error##headline
|
||||
#: konnect##loading##error##headline
|
||||
msgid "Failed to connect to server"
|
||||
msgstr "Kon niet met server verbinden"
|
||||
|
||||
#. From: konnect##goodbye##headline
|
||||
#: konnect##goodbye##headline
|
||||
msgid "Goodbye"
|
||||
msgstr "Tot ziens"
|
||||
|
||||
#. From: konnect##goodbye##confirm##headline
|
||||
#: konnect##goodbye##confirm##headline
|
||||
msgid "Hello {{displayName}}"
|
||||
msgstr "Hallo {{displayName}}"
|
||||
|
||||
#. From: konnect##consent##headline
|
||||
#: konnect##consent##headline
|
||||
msgid "Hi {{displayName}}"
|
||||
msgstr "Hoi {{displayName}}"
|
||||
|
||||
#. From: konnect##login##usernameField##placeholder##identity
|
||||
#: konnect##login##usernameField##placeholder##identity
|
||||
msgid "Identity"
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##scopeDescription##offlineAccess
|
||||
#: konnect##scopeDescription##offlineAccess
|
||||
msgid "Keep the allowed access persistently and forever"
|
||||
msgstr "De toestemming voor altijd onthouden"
|
||||
|
||||
#. From: konnect##error##login##failed
|
||||
#: konnect##error##login##failed
|
||||
msgid "Logon failed. Please verify your credentials and try again."
|
||||
msgstr "Inloggen mislukt. Controleer logingegevens en probeer opnieuw."
|
||||
|
||||
#. From: konnect##error##http##networkError
|
||||
#: konnect##error##http##networkError
|
||||
msgid "Network error. Please check your connection and try again."
|
||||
msgstr "Netwerk probleem. Controleer je verbinding en probeer opnieuw."
|
||||
|
||||
#. From: konnect##login##nextButton##label
|
||||
#: konnect##login##nextButton##label
|
||||
msgid "Next"
|
||||
msgstr "Volgende"
|
||||
|
||||
#. From: konnect##login##passwordField##label
|
||||
#: konnect##login##passwordField##label
|
||||
msgid "Password"
|
||||
msgstr "Wachtwoord"
|
||||
|
||||
#. From: konnect##goodbye##message##confirm
|
||||
#: konnect##goodbye##message##confirm
|
||||
msgid "Press the button below, to sign out from your account now."
|
||||
msgstr "Klik op onderstaande knop om af te melden van je account."
|
||||
|
||||
#. From: konnect##login##retryButton##label
|
||||
#: konnect##login##retryButton##label
|
||||
msgid "Retry"
|
||||
msgstr "Opnieuw"
|
||||
|
||||
#. From: konnect##scopeDescription##scope
|
||||
#: konnect##scopeDescription##scope
|
||||
msgid "Scope: {{scope}}"
|
||||
msgstr "Scope: {{scope}}"
|
||||
|
||||
#. From: konnect##login##headline
|
||||
#: konnect##login##headline
|
||||
msgid "Sign in"
|
||||
msgstr "Aanmelden"
|
||||
|
||||
#. From: konnect##goodbye##signoutButton##label
|
||||
#. konnect##welcome##signoutButton##label
|
||||
#: konnect##goodbye##signoutButton##label
|
||||
#: konnect##welcome##signoutButton##label
|
||||
msgid "Sign out"
|
||||
msgstr "Afmelden"
|
||||
|
||||
#. From: konnect##error##http##unexpectedResponseStatus
|
||||
#: konnect##error##http##unexpectedResponseStatus
|
||||
msgid "Unexpected HTTP response: {{status}}. Please check your connection and try again."
|
||||
msgstr "Onverwachte HTTP respons: {{status}}. Controleer je verbinding en probeer opnieuw."
|
||||
|
||||
#. From: konnect##error##http##unexpectedResponseState
|
||||
#: konnect##error##http##unexpectedResponseState
|
||||
msgid "Unexpected response state: {{state}}"
|
||||
msgstr "Onverwachte respons status: {{state}}"
|
||||
|
||||
#. From: konnect##chooseaccount##useOther##label
|
||||
#: konnect##chooseaccount##useOther##label
|
||||
msgid "Use another account"
|
||||
msgstr "Gebruik een ander account"
|
||||
|
||||
#. From: konnect##login##usernameField##placeholder##username
|
||||
#: konnect##login##usernameField##placeholder##username
|
||||
msgid "Username"
|
||||
msgstr "Gebruikersnaam"
|
||||
|
||||
#. From: konnect##welcome##headline
|
||||
#: konnect##welcome##headline
|
||||
msgid "Welcome {{displayName}}"
|
||||
msgstr "Welkom {{displayName}}"
|
||||
|
||||
#. From: konnect##welcome##message
|
||||
#: konnect##welcome##message
|
||||
msgid "You are signed in - awesome!"
|
||||
msgstr "Je bent aangemeld - fantastisch!"
|
||||
|
||||
#. From: konnect##goodbye##message##close
|
||||
#: konnect##goodbye##message##close
|
||||
msgid "You can close this window now."
|
||||
msgstr "Dit venster kan nu worden gesloten."
|
||||
|
||||
#. From: konnect##goodbye##confirm##subHeader
|
||||
#: konnect##goodbye##confirm##subHeader
|
||||
msgid "please confirm sign out"
|
||||
msgstr "bevestig afmelden"
|
||||
|
||||
#. From: konnect##chooseaccount##subHeader
|
||||
#: konnect##chooseaccount##subHeader
|
||||
msgid "to sign in"
|
||||
msgstr "aanmelden"
|
||||
|
||||
#. From: konnect##goodbye##subHeader
|
||||
#: konnect##goodbye##subHeader
|
||||
msgid "you have been signed out from your account"
|
||||
msgstr "je bent afgemeld van je account"
|
||||
|
||||
#~ msgctxt "konnect.consent.question"
|
||||
#~ msgid "Allow {clientDisplayName} to do this?"
|
||||
#~ msgstr "{clientDisplayName} toestaan dit te doen?"
|
||||
|
||||
#~ msgctxt "konnect.error.login.validate.missingUsername"
|
||||
#~ msgid "Enter an username"
|
||||
#~ msgstr "Voer een gebruikersnaam in"
|
||||
|
||||
#~ msgctxt "konnect.welcome.signoutButton.label"
|
||||
#~ msgid "Sign out"
|
||||
#~ msgstr "Afmelden"
|
||||
|
||||
#~ msgctxt "konnect.chooseaccount.subHeader"
|
||||
#~ msgid "to sign in to Kopano"
|
||||
#~ msgstr "om aan te melden bij Kopano"
|
||||
|
||||
#~ msgctxt "konnect.login.subHeader"
|
||||
#~ msgid "with your Kopano account"
|
||||
#~ msgstr "met je Kopano account"
|
||||
|
||||
#~ msgctxt "konnect.consent.message"
|
||||
#~ msgid "{clientDisplayName} wants to"
|
||||
#~ msgstr "{clientDisplayName} wil"
|
||||
@@ -0,0 +1,194 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: LibreGraph Connect Identifier\n"
|
||||
"Content-Type: text/plain; charset=utf-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"POT-Creation-Date: 2022-03-09T19:33:04.641Z\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
|
||||
#. From: konnect##loading##error##headline
|
||||
#: konnect##loading##error##headline
|
||||
msgid "Failed to connect to server"
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##login##retryButton##label
|
||||
#: konnect##login##retryButton##label
|
||||
msgid "Retry"
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##login##usernameField##placeholder##email
|
||||
#: konnect##login##usernameField##placeholder##email
|
||||
msgid "Email"
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##login##usernameField##placeholder##identity
|
||||
#: konnect##login##usernameField##placeholder##identity
|
||||
msgid "Identity"
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##login##usernameField##placeholder##username
|
||||
#: konnect##login##usernameField##placeholder##username
|
||||
msgid "Username"
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##login##headline
|
||||
#: konnect##login##headline
|
||||
msgid "Sign in"
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##login##passwordField##label
|
||||
#: konnect##login##passwordField##label
|
||||
msgid "Password"
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##login##nextButton##label
|
||||
#: konnect##login##nextButton##label
|
||||
msgid "Next"
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##scopeDescription##aliasBasic
|
||||
#: konnect##scopeDescription##aliasBasic
|
||||
msgid "Access your basic account information"
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##scopeDescription##offlineAccess
|
||||
#: konnect##scopeDescription##offlineAccess
|
||||
msgid "Keep the allowed access persistently and forever"
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##scopeDescription##scope
|
||||
#: konnect##scopeDescription##scope
|
||||
msgid "Scope: {{scope}}"
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##goodbye##headline
|
||||
#: konnect##goodbye##headline
|
||||
msgid "Goodbye"
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##goodbye##subHeader
|
||||
#: konnect##goodbye##subHeader
|
||||
msgid "you have been signed out from your account"
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##goodbye##message##close
|
||||
#: konnect##goodbye##message##close
|
||||
msgid "You can close this window now."
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##goodbye##message##confirm
|
||||
#: konnect##goodbye##message##confirm
|
||||
msgid "Press the button below, to sign out from your account now."
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##goodbye##confirm##headline
|
||||
#: konnect##goodbye##confirm##headline
|
||||
msgid "Hello {{displayName}}"
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##goodbye##confirm##subHeader
|
||||
#: konnect##goodbye##confirm##subHeader
|
||||
msgid "please confirm sign out"
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##goodbye##signoutButton##label
|
||||
#. konnect##welcome##signoutButton##label
|
||||
#: konnect##goodbye##signoutButton##label
|
||||
#: konnect##welcome##signoutButton##label
|
||||
msgid "Sign out"
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##chooseaccount##headline
|
||||
#: konnect##chooseaccount##headline
|
||||
msgid "Choose an account"
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##chooseaccount##subHeader
|
||||
#: konnect##chooseaccount##subHeader
|
||||
msgid "to sign in"
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##chooseaccount##useOther##persona##label
|
||||
#: konnect##chooseaccount##useOther##persona##label
|
||||
msgid "?"
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##chooseaccount##useOther##label
|
||||
#: konnect##chooseaccount##useOther##label
|
||||
msgid "Use another account"
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##consent##headline
|
||||
#: konnect##consent##headline
|
||||
msgid "Hi {{displayName}}"
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##consent##message
|
||||
#: konnect##consent##message
|
||||
msgid "<0><0><0></0></0></0> wants to"
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##consent##tooltip##client
|
||||
#: konnect##consent##tooltip##client
|
||||
msgid "Clicking \"Allow\" will redirect you to: {{redirectURI}}"
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##consent##question
|
||||
#: konnect##consent##question
|
||||
msgid "Allow <1><0></0></1> to do this?"
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##consent##consequence
|
||||
#: konnect##consent##consequence
|
||||
msgid "By clicking Allow, you allow this app to use your information."
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##consent##cancelButton##label
|
||||
#: konnect##consent##cancelButton##label
|
||||
msgid "Cancel"
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##consent##allowButton##label
|
||||
#: konnect##consent##allowButton##label
|
||||
msgid "Allow"
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##welcome##headline
|
||||
#: konnect##welcome##headline
|
||||
msgid "Welcome {{displayName}}"
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##welcome##message
|
||||
#: konnect##welcome##message
|
||||
msgid "You are signed in - awesome!"
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##error##login##validate##missingUsername
|
||||
#: konnect##error##login##validate##missingUsername
|
||||
msgid "Enter a valid value."
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##error##login##validate##missingPassword
|
||||
#: konnect##error##login##validate##missingPassword
|
||||
msgid "Enter your password."
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##error##login##failed
|
||||
#: konnect##error##login##failed
|
||||
msgid "Logon failed. Please verify your credentials and try again."
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##error##http##networkError
|
||||
#: konnect##error##http##networkError
|
||||
msgid "Network error. Please check your connection and try again."
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##error##http##unexpectedResponseStatus
|
||||
#: konnect##error##http##unexpectedResponseStatus
|
||||
msgid "Unexpected HTTP response: {{status}}. Please check your connection and try again."
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##error##http##unexpectedResponseState
|
||||
#: konnect##error##http##unexpectedResponseState
|
||||
msgid "Unexpected response state: {{state}}"
|
||||
msgstr ""
|
||||
@@ -0,0 +1,198 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: LibreGraph Connect Identifier\n"
|
||||
"POT-Creation-Date: 2022-03-09T19:33:04.641Z\n"
|
||||
"PO-Revision-Date: 2022-03-09T10:29:32.094Z\n"
|
||||
"Last-Translator: Automatically generated\n"
|
||||
"Language-Team: none\n"
|
||||
"Language: zh-CN\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
#. From: konnect##consent##message
|
||||
#: konnect##consent##message
|
||||
msgid "<0><0><0></0></0></0> wants to"
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##chooseaccount##useOther##persona##label
|
||||
#: konnect##chooseaccount##useOther##persona##label
|
||||
msgid "?"
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##scopeDescription##aliasBasic
|
||||
#: konnect##scopeDescription##aliasBasic
|
||||
msgid "Access your basic account information"
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##consent##allowButton##label
|
||||
#: konnect##consent##allowButton##label
|
||||
msgid "Allow"
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##consent##question
|
||||
#: konnect##consent##question
|
||||
msgid "Allow <1><0></0></1> to do this?"
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##consent##consequence
|
||||
#: konnect##consent##consequence
|
||||
msgid "By clicking Allow, you allow this app to use your information."
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##consent##cancelButton##label
|
||||
#: konnect##consent##cancelButton##label
|
||||
msgid "Cancel"
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##chooseaccount##headline
|
||||
#: konnect##chooseaccount##headline
|
||||
msgid "Choose an account"
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##consent##tooltip##client
|
||||
#: konnect##consent##tooltip##client
|
||||
msgid "Clicking \"Allow\" will redirect you to: {{redirectURI}}"
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##login##usernameField##placeholder##email
|
||||
#: konnect##login##usernameField##placeholder##email
|
||||
msgid "Email"
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##error##login##validate##missingUsername
|
||||
#: konnect##error##login##validate##missingUsername
|
||||
msgid "Enter a valid value."
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##error##login##validate##missingPassword
|
||||
#: konnect##error##login##validate##missingPassword
|
||||
msgid "Enter your password."
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##loading##error##headline
|
||||
#: konnect##loading##error##headline
|
||||
msgid "Failed to connect to server"
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##goodbye##headline
|
||||
#: konnect##goodbye##headline
|
||||
msgid "Goodbye"
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##goodbye##confirm##headline
|
||||
#: konnect##goodbye##confirm##headline
|
||||
msgid "Hello {{displayName}}"
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##consent##headline
|
||||
#: konnect##consent##headline
|
||||
msgid "Hi {{displayName}}"
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##login##usernameField##placeholder##identity
|
||||
#: konnect##login##usernameField##placeholder##identity
|
||||
msgid "Identity"
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##scopeDescription##offlineAccess
|
||||
#: konnect##scopeDescription##offlineAccess
|
||||
msgid "Keep the allowed access persistently and forever"
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##error##login##failed
|
||||
#: konnect##error##login##failed
|
||||
msgid "Logon failed. Please verify your credentials and try again."
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##error##http##networkError
|
||||
#: konnect##error##http##networkError
|
||||
msgid "Network error. Please check your connection and try again."
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##login##nextButton##label
|
||||
#: konnect##login##nextButton##label
|
||||
msgid "Next"
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##login##passwordField##label
|
||||
#: konnect##login##passwordField##label
|
||||
msgid "Password"
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##goodbye##message##confirm
|
||||
#: konnect##goodbye##message##confirm
|
||||
msgid "Press the button below, to sign out from your account now."
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##login##retryButton##label
|
||||
#: konnect##login##retryButton##label
|
||||
msgid "Retry"
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##scopeDescription##scope
|
||||
#: konnect##scopeDescription##scope
|
||||
msgid "Scope: {{scope}}"
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##login##headline
|
||||
#: konnect##login##headline
|
||||
msgid "Sign in"
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##goodbye##signoutButton##label
|
||||
#. konnect##welcome##signoutButton##label
|
||||
#: konnect##goodbye##signoutButton##label
|
||||
#: konnect##welcome##signoutButton##label
|
||||
msgid "Sign out"
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##error##http##unexpectedResponseStatus
|
||||
#: konnect##error##http##unexpectedResponseStatus
|
||||
msgid "Unexpected HTTP response: {{status}}. Please check your connection and try again."
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##error##http##unexpectedResponseState
|
||||
#: konnect##error##http##unexpectedResponseState
|
||||
msgid "Unexpected response state: {{state}}"
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##chooseaccount##useOther##label
|
||||
#: konnect##chooseaccount##useOther##label
|
||||
msgid "Use another account"
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##login##usernameField##placeholder##username
|
||||
#: konnect##login##usernameField##placeholder##username
|
||||
msgid "Username"
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##welcome##headline
|
||||
#: konnect##welcome##headline
|
||||
msgid "Welcome {{displayName}}"
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##welcome##message
|
||||
#: konnect##welcome##message
|
||||
msgid "You are signed in - awesome!"
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##goodbye##message##close
|
||||
#: konnect##goodbye##message##close
|
||||
msgid "You can close this window now."
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##goodbye##confirm##subHeader
|
||||
#: konnect##goodbye##confirm##subHeader
|
||||
msgid "please confirm sign out"
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##chooseaccount##subHeader
|
||||
#: konnect##chooseaccount##subHeader
|
||||
msgid "to sign in"
|
||||
msgstr ""
|
||||
|
||||
#. From: konnect##goodbye##subHeader
|
||||
#: konnect##goodbye##subHeader
|
||||
msgid "you have been signed out from your account"
|
||||
msgstr ""
|
||||
@@ -0,0 +1,9 @@
|
||||
package idp
|
||||
|
||||
import (
|
||||
"embed"
|
||||
)
|
||||
|
||||
//go:generate make generate
|
||||
//go:embed assets/*
|
||||
var Assets embed.FS
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"i18next-conv@15.1.2": {
|
||||
"licenses": "MIT",
|
||||
"licenseFile": "LICENSE.md",
|
||||
"licenseStart": "# MIT License",
|
||||
"licenseEnd": "========="
|
||||
},
|
||||
"p-from-callback@2.0.0": {
|
||||
"licenses": "MIT",
|
||||
"licenseFile": "LICENSE.md",
|
||||
"licenseStart": "# MIT License",
|
||||
"licenseEnd": "========="
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
{
|
||||
"name": "identifier",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"homepage": ".",
|
||||
"scripts": {
|
||||
"analyze": "source-map-explorer 'build/static/js/*.js'",
|
||||
"build": "node --openssl-legacy-provider scripts/build.js && rm -f build/service-worker.js",
|
||||
"licenses": "NODE_PATH=./node_modules node ../scripts/js-license-ranger.js",
|
||||
"licenses:check": "license-checker-rseidelsohn --summary --relativeLicensePath --onlyAllow 'Python-2.0;Apache*;Apache License, Version 2.0;Apache-2.0;Apache 2.0;Artistic-2.0;BSD;BSD-3-Clause;CC-BY-3.0;CC-BY-4.0;CC0-1.0;ISC;MIT;MPL-2.0;Public Domain;Unicode-TOU;Unlicense;WTFPL;ODC-By-1.0;BlueOak-1.0.0;OFL-1.1' --excludePackages 'identifier;unicoderegexp' --clarificationsFile license-checker-clarifications.json",
|
||||
"licenses:csv": "license-checker-rseidelsohn --relativeLicensePath --csv --out ../../third-party-licenses/node/idp/third-party-licenses.csv",
|
||||
"licenses:save": "license-checker-rseidelsohn --relativeLicensePath --out /dev/null --files ../../third-party-licenses/node/idp/third-party-licenses"
|
||||
},
|
||||
"babel": {
|
||||
"presets": [
|
||||
"react-app"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@material-ui/core": "^4.12.4",
|
||||
"@types/node": "^25.7.0",
|
||||
"@types/react": "^17.0.91",
|
||||
"@types/react-dom": "^17.0.26",
|
||||
"@types/react-redux": "^7.1.34",
|
||||
"@types/redux-logger": "^3.0.13",
|
||||
"axios": "^1.16.0",
|
||||
"i18next": "^26.1.0",
|
||||
"i18next-browser-languagedetector": "^8.2.1",
|
||||
"i18next-resources-to-backend": "^1.2.1",
|
||||
"query-string": "^9.3.1",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-i18next": "^15.7.4",
|
||||
"react-redux": "^8.1.3",
|
||||
"react-router": "^5.3.4",
|
||||
"react-router-dom": "5.2.1",
|
||||
"redux": "^4.2.1",
|
||||
"redux-logger": "^3.0.6",
|
||||
"redux-thunk": "^2.4.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.26.10",
|
||||
"babel-loader": "10.0.0",
|
||||
"babel-plugin-named-asset-import": "^0.3.8",
|
||||
"babel-preset-react-app": "^10.1.0",
|
||||
"case-sensitive-paths-webpack-plugin": "2.4.0",
|
||||
"cldr": "^7.9.0",
|
||||
"css-loader": "^7.1.4",
|
||||
"css-minimizer-webpack-plugin": "^8.0.0",
|
||||
"dotenv": "17.4.2",
|
||||
"dotenv-expand": "^13.0.0",
|
||||
"html-webpack-plugin": "^5.6.7",
|
||||
"i18next-conv": "^15.1.2",
|
||||
"i18next-parser": "^9.4.0",
|
||||
"license-checker-rseidelsohn": "4.4.2",
|
||||
"mini-css-extract-plugin": "2.9.2",
|
||||
"pnp-webpack-plugin": "1.7.0",
|
||||
"postcss-flexbugs-fixes": "5.0.2",
|
||||
"postcss-loader": "4.3.0",
|
||||
"postcss-normalize": "13.0.1",
|
||||
"postcss-preset-env": "11.2.0",
|
||||
"react-dev-utils": "^12.0.1",
|
||||
"resolve": "^1.22.12",
|
||||
"resolve-url-loader": "^5.0.0",
|
||||
"sass-loader": "^16.0.8",
|
||||
"source-map-explorer": "^2.5.3",
|
||||
"typescript": "^5.9.3",
|
||||
"url-loader": "4.1.1",
|
||||
"webpack": "5.105.2",
|
||||
"webpack-manifest-plugin": "5.0.0",
|
||||
"workbox-webpack-plugin": "7.4.0"
|
||||
},
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
"fast-uri": ">=3.1.2",
|
||||
"serialize-javascript@<7.0.3": ">=7.0.3",
|
||||
"@xmldom/xmldom": ">=0.8.13"
|
||||
}
|
||||
},
|
||||
"packageManager": "pnpm@9.15.4"
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package assets
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/qsfera/server/pkg/log"
|
||||
"github.com/qsfera/server/pkg/x/io/fsx"
|
||||
"github.com/qsfera/server/services/idp"
|
||||
"github.com/qsfera/server/services/idp/pkg/config"
|
||||
)
|
||||
|
||||
// New returns a new http filesystem to serve assets.
|
||||
func New(opts ...Option) http.FileSystem {
|
||||
options := newOptions(opts...)
|
||||
|
||||
var assetFS fsx.FS = fsx.NewBasePathFs(fsx.FromIOFS(idp.Assets), "assets")
|
||||
|
||||
// only use a fsx.NewFallbackFS and fsx.OsFs if a path is set, use the embedded fs only otherwise
|
||||
if options.Config.Asset.Path != "" {
|
||||
assetFS = fsx.NewFallbackFS(fsx.NewBasePathFs(fsx.NewOsFs(), options.Config.Asset.Path), assetFS)
|
||||
}
|
||||
|
||||
return http.FS(assetFS.IOFS())
|
||||
}
|
||||
|
||||
// Option defines a single option function.
|
||||
type Option func(o *Options)
|
||||
|
||||
// Options define the available options for this package.
|
||||
type Options struct {
|
||||
Logger log.Logger
|
||||
Config *config.Config
|
||||
}
|
||||
|
||||
// newOptions initializes the available default options.
|
||||
func newOptions(opts ...Option) Options {
|
||||
opt := Options{}
|
||||
|
||||
for _, o := range opts {
|
||||
o(&opt)
|
||||
}
|
||||
|
||||
return opt
|
||||
}
|
||||
|
||||
// Logger provides a function to set the logger option.
|
||||
func Logger(val log.Logger) Option {
|
||||
return func(o *Options) {
|
||||
o.Logger = val
|
||||
}
|
||||
}
|
||||
|
||||
// Config provides a function to set the config option.
|
||||
func Config(val *config.Config) Option {
|
||||
return func(o *Options) {
|
||||
o.Config = val
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
/*
|
||||
* Copyright 2021 Kopano and its licensors
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
*/
|
||||
|
||||
package bootstrap
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/libregraph/lico/bootstrap"
|
||||
"github.com/libregraph/lico/identifier"
|
||||
"github.com/libregraph/lico/identity"
|
||||
"github.com/libregraph/lico/identity/managers"
|
||||
|
||||
cs3 "github.com/qsfera/server/services/idp/pkg/backends/cs3/identifier"
|
||||
)
|
||||
|
||||
// Identity managers.
|
||||
const (
|
||||
identityManagerName = "cs3"
|
||||
)
|
||||
|
||||
// Register adds the CS3 identity manager to the lico bootstrap
|
||||
func Register() error {
|
||||
return bootstrap.RegisterIdentityManager(identityManagerName, NewIdentityManager)
|
||||
}
|
||||
|
||||
// MustRegister adds the CS3 identity manager to the lico bootstrap or panics
|
||||
func MustRegister() {
|
||||
if err := Register(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
// NewIdentityManager produces a CS3 backed identity manager instance for the idp
|
||||
func NewIdentityManager(bs bootstrap.Bootstrap) (identity.Manager, error) {
|
||||
config := bs.Config()
|
||||
|
||||
logger := config.Config.Logger
|
||||
|
||||
if config.AuthorizationEndpointURI.String() != "" {
|
||||
return nil, fmt.Errorf("cs3 backend is incompatible with authorization-endpoint-uri parameter")
|
||||
}
|
||||
config.AuthorizationEndpointURI.Path = bs.MakeURIPath(bootstrap.APITypeSignin, "/identifier/_/authorize")
|
||||
|
||||
if config.EndSessionEndpointURI.String() != "" {
|
||||
return nil, fmt.Errorf("cs3 backend is incompatible with endsession-endpoint-uri parameter")
|
||||
}
|
||||
config.EndSessionEndpointURI.Path = bs.MakeURIPath(bootstrap.APITypeSignin, "/identifier/_/endsession")
|
||||
|
||||
if config.SignInFormURI.EscapedPath() == "" {
|
||||
config.SignInFormURI.Path = bs.MakeURIPath(bootstrap.APITypeSignin, "/identifier")
|
||||
}
|
||||
|
||||
if config.SignedOutURI.EscapedPath() == "" {
|
||||
config.SignedOutURI.Path = bs.MakeURIPath(bootstrap.APITypeSignin, "/goodbye")
|
||||
}
|
||||
|
||||
identifierBackend, identifierErr := cs3.NewCS3Backend(
|
||||
config.Config,
|
||||
config.TLSClientConfig,
|
||||
// FIXME add a map[string]interface{} property to the lico config.Config so backends can pass custom config parameters through the bootstrap process
|
||||
os.Getenv("CS3_GATEWAY"),
|
||||
os.Getenv("CS3_MACHINE_AUTH_API_KEY"),
|
||||
config.Settings.Insecure,
|
||||
)
|
||||
if identifierErr != nil {
|
||||
return nil, fmt.Errorf("failed to create identifier backend: %v", identifierErr)
|
||||
}
|
||||
|
||||
fullAuthorizationEndpointURL := bootstrap.WithSchemeAndHost(config.AuthorizationEndpointURI, config.IssuerIdentifierURI)
|
||||
fullSignInFormURL := bootstrap.WithSchemeAndHost(config.SignInFormURI, config.IssuerIdentifierURI)
|
||||
fullSignedOutEndpointURL := bootstrap.WithSchemeAndHost(config.SignedOutURI, config.IssuerIdentifierURI)
|
||||
|
||||
activeIdentifier, err := identifier.NewIdentifier(&identifier.Config{
|
||||
Config: config.Config,
|
||||
|
||||
BaseURI: config.IssuerIdentifierURI,
|
||||
PathPrefix: bs.MakeURIPath(bootstrap.APITypeSignin, ""),
|
||||
StaticFolder: config.IdentifierClientPath,
|
||||
ScopesConf: config.IdentifierScopesConf,
|
||||
WebAppDisabled: config.IdentifierClientDisabled,
|
||||
|
||||
LogonCookieName: "__Secure-KKT", // Kopano-Konnect-Token
|
||||
LogonCookieSameSite: config.CookieSameSite,
|
||||
|
||||
ConsentCookieSameSite: config.CookieSameSite,
|
||||
|
||||
StateCookieSameSite: config.CookieSameSite,
|
||||
|
||||
AuthorizationEndpointURI: fullAuthorizationEndpointURL,
|
||||
SignedOutEndpointURI: fullSignedOutEndpointURL,
|
||||
|
||||
DefaultBannerLogo: config.IdentifierDefaultBannerLogo,
|
||||
DefaultSignInPageText: config.IdentifierDefaultSignInPageText,
|
||||
DefaultUsernameHintText: config.IdentifierDefaultUsernameHintText,
|
||||
UILocales: config.IdentifierUILocales,
|
||||
|
||||
Backend: identifierBackend,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create identifier: %v", err)
|
||||
}
|
||||
err = activeIdentifier.SetKey(config.EncryptionSecret)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid --encryption-secret parameter value for identifier: %v", err)
|
||||
}
|
||||
|
||||
identityManagerConfig := &identity.Config{
|
||||
SignInFormURI: fullSignInFormURL,
|
||||
SignedOutURI: fullSignedOutEndpointURL,
|
||||
|
||||
Logger: logger,
|
||||
|
||||
ScopesSupported: config.Config.AllowedScopes,
|
||||
}
|
||||
|
||||
identifierIdentityManager := managers.NewIdentifierIdentityManager(identityManagerConfig, activeIdentifier)
|
||||
logger.Infoln("using identifier backed identity manager")
|
||||
|
||||
return identifierIdentityManager, nil
|
||||
}
|
||||
@@ -0,0 +1,230 @@
|
||||
package cs3
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
|
||||
cs3gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
|
||||
cs3rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
|
||||
"github.com/libregraph/lico"
|
||||
"github.com/libregraph/lico/config"
|
||||
"github.com/libregraph/lico/identifier/backends"
|
||||
"github.com/libregraph/lico/identifier/meta/scopes"
|
||||
"github.com/libregraph/lico/identity"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
|
||||
cmap "github.com/orcaman/concurrent-map"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const cs3BackendName = "identifier-cs3"
|
||||
|
||||
var cs3SpportedScopes = []string{
|
||||
"profile",
|
||||
"email",
|
||||
lico.ScopeUniqueUserID,
|
||||
lico.ScopeRawSubject,
|
||||
}
|
||||
|
||||
// CS3Backend holds the data for the CS3 identifier backend
|
||||
type CS3Backend struct {
|
||||
supportedScopes []string
|
||||
|
||||
logger logrus.FieldLogger
|
||||
tlsConfig *tls.Config
|
||||
gatewayAddr string
|
||||
machineAuthAPIKey string
|
||||
insecure bool
|
||||
|
||||
sessions cmap.ConcurrentMap
|
||||
}
|
||||
|
||||
// NewCS3Backend creates a new CS3 backend identifier backend
|
||||
func NewCS3Backend(
|
||||
c *config.Config,
|
||||
tlsConfig *tls.Config,
|
||||
gatewayAddr string,
|
||||
machineAuthAPIKey string,
|
||||
insecure bool,
|
||||
) (*CS3Backend, error) {
|
||||
|
||||
// Build supported scopes based on default scopes.
|
||||
supportedScopes := make([]string, len(cs3SpportedScopes))
|
||||
copy(supportedScopes, cs3SpportedScopes)
|
||||
|
||||
b := &CS3Backend{
|
||||
supportedScopes: supportedScopes,
|
||||
|
||||
logger: c.Logger,
|
||||
tlsConfig: tlsConfig,
|
||||
gatewayAddr: gatewayAddr,
|
||||
machineAuthAPIKey: machineAuthAPIKey,
|
||||
insecure: insecure,
|
||||
|
||||
sessions: cmap.New(),
|
||||
}
|
||||
|
||||
b.logger.Infoln("cs3 backend connection set up")
|
||||
|
||||
return b, nil
|
||||
}
|
||||
|
||||
// RunWithContext implements the Backend interface.
|
||||
func (b *CS3Backend) RunWithContext(_ context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Logon implements the Backend interface, enabling Logon with user name and
|
||||
// password as provided. Requests are bound to the provided context.
|
||||
func (b *CS3Backend) Logon(ctx context.Context, audience, username, password string) (bool, *string, *string, backends.UserFromBackend, error) {
|
||||
|
||||
client, err := pool.GetGatewayServiceClient(b.gatewayAddr)
|
||||
if err != nil {
|
||||
return false, nil, nil, nil, err
|
||||
}
|
||||
|
||||
res, err := client.Authenticate(ctx, &cs3gateway.AuthenticateRequest{
|
||||
Type: "basic",
|
||||
ClientId: username,
|
||||
ClientSecret: password,
|
||||
})
|
||||
if err != nil {
|
||||
return false, nil, nil, nil, fmt.Errorf("cs3 backend basic authenticate rpc error: %v", err)
|
||||
}
|
||||
if res.GetStatus().GetCode() != cs3rpc.Code_CODE_OK {
|
||||
return false, nil, nil, nil, fmt.Errorf("cs3 backend basic authenticate failed with code %s: %s", res.GetStatus().GetCode().String(), res.GetStatus().GetMessage())
|
||||
}
|
||||
|
||||
session := createSession(res.GetUser())
|
||||
|
||||
user, err := newCS3User(res.GetUser())
|
||||
if err != nil {
|
||||
return false, nil, nil, nil, fmt.Errorf("cs3 backend resolve entry data error: %v", err)
|
||||
}
|
||||
|
||||
// Use the users subject as user id.
|
||||
userID := user.Subject()
|
||||
|
||||
sessionRef := identity.GetSessionRef(b.Name(), audience, userID)
|
||||
b.sessions.Set(*sessionRef, session)
|
||||
b.logger.WithFields(logrus.Fields{
|
||||
"session": session,
|
||||
"ref": *sessionRef,
|
||||
"username": user.Username(),
|
||||
"id": userID,
|
||||
}).Debugln("cs3 backend logon")
|
||||
|
||||
return true, &userID, sessionRef, user, nil
|
||||
}
|
||||
|
||||
// GetUser implements the Backend interface, providing user meta data retrieval
|
||||
// for the user specified by the userID. Requests are bound to the provided
|
||||
// context.
|
||||
func (b *CS3Backend) GetUser(ctx context.Context, userEntryID string, sessionRef *string, _ map[string]bool) (backends.UserFromBackend, error) {
|
||||
|
||||
var session *cs3Session
|
||||
if s, ok := b.sessions.Get(*sessionRef); ok {
|
||||
// We have a cached session
|
||||
session = s.(*cs3Session)
|
||||
if session != nil {
|
||||
user, err := newCS3User(session.User())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cs3 backend get user failed to process user: %v", err)
|
||||
}
|
||||
return user, nil
|
||||
}
|
||||
}
|
||||
|
||||
// rebuild session
|
||||
|
||||
client, err := pool.GetGatewayServiceClient(b.gatewayAddr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res, err := client.Authenticate(ctx, &cs3gateway.AuthenticateRequest{
|
||||
Type: "machine",
|
||||
ClientId: "userid:" + userEntryID,
|
||||
ClientSecret: b.machineAuthAPIKey,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cs3 backend get user machine authenticate rpc error: %v", err)
|
||||
}
|
||||
if res.GetStatus().GetCode() != cs3rpc.Code_CODE_OK {
|
||||
return nil, fmt.Errorf("cs3 backend get user machine authenticate failed with code %s: %s", res.GetStatus().GetCode().String(), res.GetStatus().GetMessage())
|
||||
}
|
||||
|
||||
// cache session
|
||||
session = createSession(res.GetUser())
|
||||
b.sessions.Set(*sessionRef, session)
|
||||
|
||||
user, err := newCS3User(res.GetUser())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cs3 backend get user data error: %v", err)
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// ResolveUserByUsername implements the Backend interface, providing lookup for
|
||||
// user by providing the username. Requests are bound to the provided context.
|
||||
func (b *CS3Backend) ResolveUserByUsername(ctx context.Context, username string) (backends.UserFromBackend, error) {
|
||||
|
||||
client, err := pool.GetGatewayServiceClient(b.gatewayAddr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res, err := client.Authenticate(ctx, &cs3gateway.AuthenticateRequest{
|
||||
Type: "machine",
|
||||
ClientId: "username:" + username,
|
||||
ClientSecret: b.machineAuthAPIKey,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cs3 backend machine authenticate rpc error: %v", err)
|
||||
}
|
||||
if res.GetStatus().GetCode() != cs3rpc.Code_CODE_OK {
|
||||
return nil, fmt.Errorf("cs3 backend machine authenticate failed with code %s: %s", res.GetStatus().GetCode().String(), res.GetStatus().GetMessage())
|
||||
}
|
||||
|
||||
user, err := newCS3User(res.GetUser())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cs3 backend resolve username data error: %v", err)
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// RefreshSession implements the Backend interface.
|
||||
func (b *CS3Backend) RefreshSession(_ context.Context, _ string, _ *string, _ map[string]any) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// DestroySession implements the Backend interface providing destroy CS3 session.
|
||||
func (b *CS3Backend) DestroySession(_ context.Context, sessionRef *string) error {
|
||||
b.sessions.Remove(*sessionRef)
|
||||
return nil
|
||||
}
|
||||
|
||||
// UserClaims implements the Backend interface, providing user specific claims
|
||||
// for the user specified by the userID.
|
||||
func (b *CS3Backend) UserClaims(_ string, _ map[string]bool) map[string]any {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ScopesSupported implements the Backend interface, providing supported scopes
|
||||
// when running this backend.
|
||||
func (b *CS3Backend) ScopesSupported() []string {
|
||||
return b.supportedScopes
|
||||
}
|
||||
|
||||
// ScopesMeta implements the Backend interface, providing meta data for
|
||||
// supported scopes.
|
||||
func (b *CS3Backend) ScopesMeta() *scopes.Scopes {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Name implements the Backend interface.
|
||||
func (b *CS3Backend) Name() string {
|
||||
return cs3BackendName
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package cs3
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
cs3user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
||||
)
|
||||
|
||||
// createSession creates a new Session without the server using the provided
|
||||
// data.
|
||||
func createSession(u *cs3user.User) *cs3Session {
|
||||
s := &cs3Session{
|
||||
u: u,
|
||||
}
|
||||
|
||||
s.when = time.Now()
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
type cs3Session struct {
|
||||
u *cs3user.User
|
||||
when time.Time
|
||||
}
|
||||
|
||||
// User returns the cs3 user of the session
|
||||
func (s *cs3Session) User() *cs3user.User {
|
||||
return s.u
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package cs3
|
||||
|
||||
import (
|
||||
cs3user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
||||
konnect "github.com/libregraph/lico"
|
||||
)
|
||||
|
||||
type cs3User struct {
|
||||
u *cs3user.User
|
||||
}
|
||||
|
||||
func newCS3User(u *cs3user.User) (*cs3User, error) {
|
||||
return &cs3User{
|
||||
u: u,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Subject returns the cs3 users opaque id as sub
|
||||
func (u *cs3User) Subject() string {
|
||||
return u.u.GetId().GetOpaqueId()
|
||||
}
|
||||
|
||||
// Email returns the cs3 users email
|
||||
func (u *cs3User) Email() string {
|
||||
return u.u.GetMail()
|
||||
}
|
||||
|
||||
// EmailVerified returns the cs3 users email verified flag
|
||||
func (u *cs3User) EmailVerified() bool {
|
||||
return u.u.GetMailVerified()
|
||||
}
|
||||
|
||||
// Name returns the cs3 users displayname
|
||||
func (u *cs3User) Name() string {
|
||||
return u.u.GetDisplayName()
|
||||
}
|
||||
|
||||
// FamilyName always returns "" to fulfill the UserWithProfile interface
|
||||
func (u *cs3User) FamilyName() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// GivenName always returns "" to fulfill the UserWithProfile interface
|
||||
func (u *cs3User) GivenName() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Username returns the cs3 users username
|
||||
func (u *cs3User) Username() string {
|
||||
return u.u.GetUsername()
|
||||
}
|
||||
|
||||
// UniqueID returns the cs3 users opaque id
|
||||
func (u *cs3User) UniqueID() string {
|
||||
return u.u.GetId().GetOpaqueId()
|
||||
}
|
||||
|
||||
// BackendClaims returns additional claims the cs3 users provides
|
||||
func (u *cs3User) BackendClaims() map[string]any {
|
||||
claims := make(map[string]any)
|
||||
claims[konnect.IdentifiedUserIDClaim] = u.u.GetId().GetOpaqueId()
|
||||
|
||||
return claims
|
||||
}
|
||||
|
||||
// BackendScopes returns nil to fulfill the UserFromBackend interface
|
||||
func (u *cs3User) BackendScopes() []string {
|
||||
return nil
|
||||
}
|
||||
|
||||
// RequiredScopes returns nil to fulfill the UserFromBackend interface
|
||||
func (u *cs3User) RequiredScopes() []string {
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/qsfera/server/pkg/config/configlog"
|
||||
"github.com/qsfera/server/pkg/log"
|
||||
"github.com/qsfera/server/services/idp/pkg/config"
|
||||
"github.com/qsfera/server/services/idp/pkg/config/parser"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// Health is the entrypoint for the health command.
|
||||
func Health(cfg *config.Config) *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "health",
|
||||
Short: "check health status",
|
||||
PreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
return configlog.ReturnError(parser.ParseConfig(cfg))
|
||||
},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
logger := log.Configure(cfg.Service.Name, cfg.Commons, cfg.LogLevel)
|
||||
|
||||
resp, err := http.Get(
|
||||
fmt.Sprintf(
|
||||
"http://%s/healthz",
|
||||
cfg.Debug.Addr,
|
||||
),
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
logger.Fatal().
|
||||
Err(err).
|
||||
Msg("Failed to request health check")
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
logger.Fatal().
|
||||
Int("code", resp.StatusCode).
|
||||
Msg("Health seems to be in bad state")
|
||||
}
|
||||
|
||||
logger.Debug().
|
||||
Int("code", resp.StatusCode).
|
||||
Msg("Health got a good state")
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/qsfera/server/pkg/clihelper"
|
||||
"github.com/qsfera/server/services/idp/pkg/config"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// GetCommands provides all commands for this service
|
||||
func GetCommands(cfg *config.Config) []*cobra.Command {
|
||||
return []*cobra.Command{
|
||||
// start this service
|
||||
Server(cfg),
|
||||
|
||||
// interaction with this service
|
||||
|
||||
// infos about this service
|
||||
Health(cfg),
|
||||
Version(cfg),
|
||||
}
|
||||
}
|
||||
|
||||
// Execute is the entry point for the idp command.
|
||||
func Execute(cfg *config.Config) error {
|
||||
app := clihelper.DefaultApp(&cobra.Command{
|
||||
Use: "idp",
|
||||
Short: "Serve IDP API for КуСфера",
|
||||
})
|
||||
|
||||
app.AddCommand(GetCommands(cfg)...)
|
||||
app.SetArgs(os.Args[1:])
|
||||
|
||||
return app.ExecuteContext(cfg.Context)
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/qsfera/server/pkg/config/configlog"
|
||||
"github.com/qsfera/server/pkg/log"
|
||||
"github.com/qsfera/server/pkg/runner"
|
||||
"github.com/qsfera/server/pkg/tracing"
|
||||
"github.com/qsfera/server/pkg/version"
|
||||
"github.com/qsfera/server/services/idp/pkg/config"
|
||||
"github.com/qsfera/server/services/idp/pkg/config/parser"
|
||||
"github.com/qsfera/server/services/idp/pkg/metrics"
|
||||
"github.com/qsfera/server/services/idp/pkg/server/debug"
|
||||
"github.com/qsfera/server/services/idp/pkg/server/http"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
const _rsaKeySize = 4096
|
||||
|
||||
// Server is the entrypoint for the server command.
|
||||
func Server(cfg *config.Config) *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "server",
|
||||
Short: fmt.Sprintf("start the %s service without runtime (unsupervised mode)", cfg.Service.Name),
|
||||
PreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
err := configlog.ReturnFatal(parser.ParseConfig(cfg))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if cfg.IDP.EncryptionSecretFile != "" {
|
||||
if err := ensureEncryptionSecretExists(cfg.IDP.EncryptionSecretFile); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ensureSigningPrivateKeyExists(cfg.IDP.SigningPrivateKeyFiles); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
logger := log.Configure(cfg.Service.Name, cfg.Commons, cfg.LogLevel)
|
||||
traceProvider, err := tracing.GetTraceProvider(cmd.Context(), cfg.Commons.TracesExporter, cfg.Service.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var cancel context.CancelFunc
|
||||
if cfg.Context == nil {
|
||||
cfg.Context, cancel = signal.NotifyContext(context.Background(), runner.StopSignals...)
|
||||
defer cancel()
|
||||
}
|
||||
ctx := cfg.Context
|
||||
|
||||
metrics := metrics.New()
|
||||
metrics.BuildInfo.WithLabelValues(version.GetString()).Set(1)
|
||||
|
||||
gr := runner.NewGroup()
|
||||
{
|
||||
server, err := http.Server(
|
||||
http.Logger(logger),
|
||||
http.Context(ctx),
|
||||
http.Config(cfg),
|
||||
http.Metrics(metrics),
|
||||
http.TraceProvider(traceProvider),
|
||||
)
|
||||
if err != nil {
|
||||
logger.Info().
|
||||
Err(err).
|
||||
Str("transport", "http").
|
||||
Msg("Failed to initialize server")
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
gr.Add(runner.NewGoMicroHttpServerRunner(cfg.Service.Name+".http", server))
|
||||
}
|
||||
|
||||
{
|
||||
debugServer, err := debug.Server(
|
||||
debug.Logger(logger),
|
||||
debug.Context(ctx),
|
||||
debug.Config(cfg),
|
||||
)
|
||||
if err != nil {
|
||||
logger.Info().Err(err).Str("transport", "debug").Msg("Failed to initialize server")
|
||||
return err
|
||||
}
|
||||
|
||||
gr.Add(runner.NewGolangHttpServerRunner(cfg.Service.Name+".debug", debugServer))
|
||||
}
|
||||
|
||||
grResults := gr.Run(ctx)
|
||||
|
||||
// return the first non-nil error found in the results
|
||||
for _, grResult := range grResults {
|
||||
if grResult.RunnerError != nil {
|
||||
return grResult.RunnerError
|
||||
}
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func ensureEncryptionSecretExists(path string) error {
|
||||
_, err := os.Stat(path)
|
||||
if err == nil {
|
||||
// If the file exists we can just return
|
||||
return nil
|
||||
}
|
||||
if !errors.Is(err, fs.ErrNotExist) {
|
||||
return err
|
||||
}
|
||||
|
||||
dir := filepath.Dir(path)
|
||||
err = os.MkdirAll(dir, 0o700)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, 0o600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
secret := make([]byte, 32)
|
||||
_, err = rand.Read(secret)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = io.Copy(f, bytes.NewReader(secret))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func ensureSigningPrivateKeyExists(paths []string) error {
|
||||
for _, path := range paths {
|
||||
file, err := os.Stat(path)
|
||||
if err == nil && file.Size() > 0 {
|
||||
// If the file exists and is not empty we can just return
|
||||
return nil
|
||||
}
|
||||
if !errors.Is(err, fs.ErrNotExist) && file.Size() > 0 {
|
||||
return err
|
||||
}
|
||||
|
||||
dir := filepath.Dir(path)
|
||||
err = os.MkdirAll(dir, 0o700)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, 0o600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
pk, err := rsa.GenerateKey(rand.Reader, _rsaKeySize)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pb := &pem.Block{
|
||||
Type: "RSA PRIVATE KEY",
|
||||
Bytes: x509.MarshalPKCS1PrivateKey(pk),
|
||||
}
|
||||
if err := pem.Encode(f, pb); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/qsfera/server/pkg/registry"
|
||||
"github.com/qsfera/server/pkg/version"
|
||||
"github.com/qsfera/server/services/idp/pkg/config"
|
||||
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"github.com/olekukonko/tablewriter/tw"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// Version prints the service versions of all running instances.
|
||||
func Version(cfg *config.Config) *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "version",
|
||||
Short: "print the version of this binary and the running service instances",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
fmt.Println("Version: " + version.GetString())
|
||||
fmt.Printf("Compiled: %s\n", version.Compiled())
|
||||
fmt.Println("")
|
||||
|
||||
reg := registry.GetRegistry()
|
||||
services, err := reg.GetService(cfg.HTTP.Namespace + "." + cfg.Service.Name)
|
||||
if err != nil {
|
||||
fmt.Println(fmt.Errorf("could not get %s services from the registry: %v", cfg.Service.Name, err))
|
||||
return err
|
||||
}
|
||||
|
||||
if len(services) == 0 {
|
||||
fmt.Println("No running " + cfg.Service.Name + " service found.")
|
||||
return nil
|
||||
}
|
||||
|
||||
table := tablewriter.NewTable(os.Stdout, tablewriter.WithHeaderAutoFormat(tw.Off))
|
||||
table.Header([]string{"Version", "Address", "Id"})
|
||||
for _, s := range services {
|
||||
for _, n := range s.Nodes {
|
||||
table.Append([]string{s.Version, n.Address, n.Id})
|
||||
}
|
||||
}
|
||||
table.Render()
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/qsfera/server/pkg/shared"
|
||||
)
|
||||
|
||||
// Config combines all available configuration parts.
|
||||
type Config struct {
|
||||
Commons *shared.Commons `yaml:"-"` // don't use this directly as configuration for a service
|
||||
|
||||
Service Service `yaml:"-"`
|
||||
|
||||
LogLevel string `yaml:"loglevel" env:"OC_LOG_LEVEL;IDP_LOG_LEVEL" desc:"The log level. Valid values are: 'panic', 'fatal', 'error', 'warn', 'info', 'debug', 'trace'." introductionVersion:"1.0.0"`
|
||||
Debug Debug `yaml:"debug"`
|
||||
|
||||
HTTP HTTP `yaml:"http"`
|
||||
|
||||
Reva *shared.Reva `yaml:"reva"`
|
||||
|
||||
MachineAuthAPIKey string `yaml:"machine_auth_api_key" env:"OC_MACHINE_AUTH_API_KEY;IDP_MACHINE_AUTH_API_KEY" desc:"Machine auth API key used to validate internal requests necessary for the access to resources from other services." introductionVersion:"1.0.0"`
|
||||
|
||||
Asset Asset `yaml:"asset"`
|
||||
IDP Settings `yaml:"idp"`
|
||||
Clients []Client `yaml:"clients"`
|
||||
Ldap Ldap `yaml:"ldap"`
|
||||
|
||||
Context context.Context `yaml:"-"`
|
||||
}
|
||||
|
||||
// Ldap defines the available LDAP configuration.
|
||||
type Ldap struct {
|
||||
URI string `yaml:"uri" env:"OC_LDAP_URI;IDP_LDAP_URI" desc:"Url of the LDAP service to use as IDP." introductionVersion:"1.0.0"`
|
||||
TLSCACert string `yaml:"cacert" env:"OC_LDAP_CACERT;IDP_LDAP_TLS_CACERT" desc:"Path/File name for the root CA certificate (in PEM format) used to validate TLS server certificates of the LDAP service. If not defined, the root directory derives from $OC_BASE_DATA_PATH/idp." introductionVersion:"1.0.0"`
|
||||
|
||||
BindDN string `yaml:"bind_dn" env:"OC_LDAP_BIND_DN;IDP_LDAP_BIND_DN" desc:"LDAP DN to use for simple bind authentication with the target LDAP server." introductionVersion:"1.0.0"`
|
||||
BindPassword string `yaml:"bind_password" env:"OC_LDAP_BIND_PASSWORD;IDP_LDAP_BIND_PASSWORD" desc:"Password to use for authenticating the 'bind_dn'." introductionVersion:"1.0.0"`
|
||||
|
||||
BaseDN string `yaml:"base_dn" env:"OC_LDAP_USER_BASE_DN;IDP_LDAP_BASE_DN" desc:"Search base DN for looking up LDAP users." introductionVersion:"1.0.0"`
|
||||
Scope string `yaml:"scope" env:"OC_LDAP_USER_SCOPE;IDP_LDAP_SCOPE" desc:"LDAP search scope to use when looking up users. Supported scopes are 'base', 'one' and 'sub'." introductionVersion:"1.0.0"`
|
||||
|
||||
LoginAttribute string `yaml:"login_attribute" env:"IDP_LDAP_LOGIN_ATTRIBUTE" desc:"LDAP User attribute to use for login like 'uid'." introductionVersion:"1.0.0"`
|
||||
EmailAttribute string `yaml:"email_attribute" env:"OC_LDAP_USER_SCHEMA_MAIL;IDP_LDAP_EMAIL_ATTRIBUTE" desc:"LDAP User email attribute like 'mail'." introductionVersion:"1.0.0"`
|
||||
NameAttribute string `yaml:"name_attribute" env:"OC_LDAP_USER_SCHEMA_USERNAME;IDP_LDAP_NAME_ATTRIBUTE" desc:"LDAP User name attribute like 'displayName'." introductionVersion:"1.0.0"`
|
||||
UUIDAttribute string `yaml:"uuid_attribute" env:"OC_LDAP_USER_SCHEMA_ID;IDP_LDAP_UUID_ATTRIBUTE" desc:"LDAP User UUID attribute like 'uid'." introductionVersion:"1.0.0"`
|
||||
UUIDAttributeType string `yaml:"uuid_attribute_type" env:"IDP_LDAP_UUID_ATTRIBUTE_TYPE" desc:"LDAP User uuid attribute type like 'text'." introductionVersion:"1.0.0"`
|
||||
|
||||
UserEnabledAttribute string `yaml:"user_enabled_attribute" env:"OC_LDAP_USER_ENABLED_ATTRIBUTE;IDP_USER_ENABLED_ATTRIBUTE" desc:"LDAP Attribute to use as a flag telling if the user is enabled or disabled." introductionVersion:"1.0.0"`
|
||||
Filter string `yaml:"filter" env:"OC_LDAP_USER_FILTER;IDP_LDAP_FILTER" desc:"LDAP filter to add to the default filters for user search like '(objectclass=qsferaUser)'." introductionVersion:"1.0.0"`
|
||||
ObjectClass string `yaml:"objectclass" env:"OC_LDAP_USER_OBJECTCLASS;IDP_LDAP_OBJECTCLASS" desc:"LDAP User ObjectClass like 'inetOrgPerson'." introductionVersion:"1.0.0"`
|
||||
}
|
||||
|
||||
// Asset defines the available asset configuration.
|
||||
type Asset struct {
|
||||
Path string `yaml:"asset" env:"IDP_ASSET_PATH" desc:"Serve IDP assets from a path on the filesystem instead of the builtin assets." introductionVersion:"1.0.0"`
|
||||
LoginBackgroundUrl string `yaml:"login-background-url" env:"IDP_LOGIN_BACKGROUND_URL" desc:"Configure an alternative URL to the background image for the login page." introductionVersion:"1.0.0"`
|
||||
}
|
||||
|
||||
type Client struct {
|
||||
ID string `yaml:"id"`
|
||||
Name string `yaml:"name"`
|
||||
Trusted bool `yaml:"trusted"`
|
||||
Secret string `yaml:"secret"`
|
||||
RedirectURIs []string `yaml:"redirect_uris"`
|
||||
PostLogoutRedirectURIs []string `yaml:"post_logout_redirect_uris"`
|
||||
Origins []string `yaml:"origins"`
|
||||
ApplicationType string `yaml:"application_type"`
|
||||
}
|
||||
|
||||
type Settings struct {
|
||||
Iss string `yaml:"iss" env:"OC_URL;OC_OIDC_ISSUER;IDP_ISS" desc:"The OIDC issuer URL to use." introductionVersion:"1.0.0"`
|
||||
|
||||
IdentityManager string `yaml:"identity_manager" env:"IDP_IDENTITY_MANAGER" desc:"The identity manager implementation to use. Supported identity managers are 'ldap', 'cs3', 'libregraph' and 'guest'." introductionVersion:"1.0.0"`
|
||||
|
||||
URIBasePath string `yaml:"uri_base_path" env:"IDP_URI_BASE_PATH" desc:"IDP uri base path (defaults to '')." introductionVersion:"1.0.0"`
|
||||
|
||||
SignInURI string `yaml:"sign_in_uri" env:"IDP_SIGN_IN_URI" desc:"IDP sign-in url." introductionVersion:"1.0.0"`
|
||||
SignedOutURI string `yaml:"signed_out_uri" env:"IDP_SIGN_OUT_URI" desc:"IDP sign-out url." introductionVersion:"1.0.0"`
|
||||
|
||||
AuthorizationEndpointURI string `yaml:"authorization_endpoint_uri" env:"IDP_ENDPOINT_URI" desc:"URL of the IDP endpoint." introductionVersion:"1.0.0"`
|
||||
EndsessionEndpointURI string `yaml:"-"` // unused, not supported by lico-idp
|
||||
|
||||
Insecure bool `yaml:"ldap_insecure" env:"OC_LDAP_INSECURE;IDP_INSECURE" desc:"Disable TLS certificate validation for the LDAP connections. Do not set this in production environments." introductionVersion:"1.0.0"`
|
||||
|
||||
TrustedProxy []string `yaml:"trusted_proxy"` //TODO: how to configure this via env?
|
||||
|
||||
AllowScope []string `yaml:"allow_scope"` // TODO: is this even needed?
|
||||
AllowClientGuests bool `yaml:"allow_client_guests" env:"IDP_ALLOW_CLIENT_GUESTS" desc:"Allow guest clients to access КуСфера." introductionVersion:"1.0.0"`
|
||||
AllowDynamicClientRegistration bool `yaml:"allow_dynamic_client_registration" env:"IDP_ALLOW_DYNAMIC_CLIENT_REGISTRATION" desc:"Allow dynamic client registration." introductionVersion:"1.0.0"`
|
||||
|
||||
EncryptionSecretFile string `yaml:"encrypt_secret_file" env:"IDP_ENCRYPTION_SECRET_FILE" desc:"Path to the encryption secret file, if unset, a new certificate will be autogenerated upon each restart, thus invalidating all existing sessions. If not defined, the root directory derives from $OC_BASE_DATA_PATH/idp." introductionVersion:"1.0.0"`
|
||||
|
||||
Listen string
|
||||
|
||||
IdentifierClientDisabled bool `yaml:"-"` // unused
|
||||
IdentifierClientPath string `yaml:"-"`
|
||||
IdentifierRegistrationConf string `yaml:"-"`
|
||||
IdentifierScopesConf string `yaml:"-"` // unused
|
||||
IdentifierDefaultBannerLogo string
|
||||
IdentifierDefaultSignInPageText string `yaml:"default_sign_in_page_text" env:"IDP_DEFAULT_SIGNIN_PAGE_TEXT" desc:"" introductionVersion:"2.0.0"`
|
||||
IdentifierDefaultLogoTargetURI string `yaml:"default_logo_target_uri" env:"IDP_DEFAULT_LOGO_TARGET_URI" desc:"Default logo target URI." introductionVersion:"4.0.0"`
|
||||
IdentifierDefaultUsernameHintText string
|
||||
IdentifierUILocales []string
|
||||
|
||||
SigningKid string `yaml:"signing_kid" env:"IDP_SIGNING_KID" desc:"Value of the KID (Key ID) field which is used in created tokens to uniquely identify the signing-private-key." introductionVersion:"1.0.0"`
|
||||
SigningMethod string `yaml:"signing_method" env:"IDP_SIGNING_METHOD" desc:"Signing method of IDP requests like 'PS256'" introductionVersion:"1.0.0"`
|
||||
SigningPrivateKeyFiles []string `yaml:"signing_private_key_files" env:"IDP_SIGNING_PRIVATE_KEY_FILES" desc:"A list of private key files for signing IDP requests. If not defined, the root directory derives from $OC_BASE_DATA_PATH/idp. See the Environment Variable Types description for more details." introductionVersion:"1.0.0"`
|
||||
ValidationKeysPath string `yaml:"validation_keys_path" env:"IDP_VALIDATION_KEYS_PATH" desc:"Path to validation keys for IDP requests." introductionVersion:"1.0.0"`
|
||||
|
||||
CookieBackendURI string
|
||||
CookieNames []string
|
||||
CookieSameSite http.SameSite
|
||||
|
||||
AccessTokenDurationSeconds uint64 `yaml:"access_token_duration_seconds" env:"IDP_ACCESS_TOKEN_EXPIRATION" desc:"'Access token lifespan in seconds (time before an access token is expired).'" introductionVersion:"1.0.0"`
|
||||
IDTokenDurationSeconds uint64 `yaml:"id_token_duration_seconds" env:"IDP_ID_TOKEN_EXPIRATION" desc:"ID token lifespan in seconds (time before an ID token is expired)." introductionVersion:"1.0.0"`
|
||||
RefreshTokenDurationSeconds uint64 `yaml:"refresh_token_duration_seconds" env:"IDP_REFRESH_TOKEN_EXPIRATION" desc:"Refresh token lifespan in seconds (time before an refresh token is expired). This also limits the duration of an idle offline session." introductionVersion:"1.0.0"`
|
||||
DynamicClientSecretDurationSeconds uint64 `yaml:"dynamic_client_secret_duration_seconds" env:"IDP_DYNAMIC_CLIENT_SECRET_DURATION" desc:"Lifespan in seconds of a dynamically registered OIDC client." introductionVersion:"1.0.0"`
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package config
|
||||
|
||||
// Debug defines the available debug configuration.
|
||||
type Debug struct {
|
||||
Addr string `yaml:"addr" env:"IDP_DEBUG_ADDR" desc:"Bind address of the debug server, where metrics, health, config and debug endpoints will be exposed." introductionVersion:"1.0.0"`
|
||||
Token string `yaml:"token" env:"IDP_DEBUG_TOKEN" desc:"Token to secure the metrics endpoint." introductionVersion:"1.0.0"`
|
||||
Pprof bool `yaml:"pprof" env:"IDP_DEBUG_PPROF" desc:"Enables pprof, which can be used for profiling." introductionVersion:"1.0.0"`
|
||||
Zpages bool `yaml:"zpages" env:"IDP_DEBUG_ZPAGES" desc:"Enables zpages, which can be used for collecting and viewing in-memory traces." introductionVersion:"1.0.0"`
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
package defaults
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/qsfera/server/pkg/config/defaults"
|
||||
"github.com/qsfera/server/pkg/shared"
|
||||
"github.com/qsfera/server/pkg/structs"
|
||||
"github.com/qsfera/server/services/idp/pkg/config"
|
||||
)
|
||||
|
||||
// FullDefaultConfig returns a fully initialized default configuration
|
||||
func FullDefaultConfig() *config.Config {
|
||||
cfg := DefaultConfig()
|
||||
EnsureDefaults(cfg)
|
||||
Sanitize(cfg)
|
||||
return cfg
|
||||
}
|
||||
|
||||
// DefaultConfig returns a basic default configuration
|
||||
func DefaultConfig() *config.Config {
|
||||
return &config.Config{
|
||||
Debug: config.Debug{
|
||||
Addr: "127.0.0.1:9134",
|
||||
},
|
||||
HTTP: config.HTTP{
|
||||
Addr: "127.0.0.1:9130",
|
||||
Root: "/",
|
||||
Namespace: "qsfera.web",
|
||||
TLSCert: filepath.Join(defaults.BaseDataPath(), "idp", "server.crt"),
|
||||
TLSKey: filepath.Join(defaults.BaseDataPath(), "idp", "server.key"),
|
||||
TLS: false,
|
||||
},
|
||||
Reva: shared.DefaultRevaConfig(),
|
||||
Service: config.Service{
|
||||
Name: "idp",
|
||||
},
|
||||
IDP: config.Settings{
|
||||
Iss: "https://localhost:9200",
|
||||
IdentityManager: "ldap",
|
||||
URIBasePath: "",
|
||||
SignInURI: "",
|
||||
SignedOutURI: "",
|
||||
AuthorizationEndpointURI: "",
|
||||
EndsessionEndpointURI: "",
|
||||
Insecure: false,
|
||||
TrustedProxy: nil,
|
||||
AllowScope: nil,
|
||||
AllowClientGuests: false,
|
||||
AllowDynamicClientRegistration: false,
|
||||
EncryptionSecretFile: filepath.Join(defaults.BaseDataPath(), "idp", "encryption.key"),
|
||||
Listen: "",
|
||||
IdentifierClientDisabled: true,
|
||||
IdentifierClientPath: filepath.Join(defaults.BaseDataPath(), "idp"),
|
||||
IdentifierRegistrationConf: filepath.Join(defaults.BaseDataPath(), "idp", "tmp", "identifier-registration.yaml"),
|
||||
IdentifierScopesConf: "",
|
||||
IdentifierDefaultBannerLogo: "",
|
||||
IdentifierDefaultSignInPageText: "",
|
||||
IdentifierDefaultLogoTargetURI: "",
|
||||
IdentifierDefaultUsernameHintText: "",
|
||||
SigningKid: "private-key",
|
||||
SigningMethod: "PS256",
|
||||
SigningPrivateKeyFiles: []string{filepath.Join(defaults.BaseDataPath(), "idp", "private-key.pem")},
|
||||
ValidationKeysPath: "",
|
||||
CookieBackendURI: "",
|
||||
CookieNames: nil,
|
||||
CookieSameSite: http.SameSiteStrictMode,
|
||||
AccessTokenDurationSeconds: 60 * 5, // 5 minutes
|
||||
IDTokenDurationSeconds: 60 * 5, // 5 minutes
|
||||
RefreshTokenDurationSeconds: 60 * 60 * 24 * 30, // 30 days
|
||||
DynamicClientSecretDurationSeconds: 0,
|
||||
},
|
||||
Clients: []config.Client{
|
||||
{
|
||||
ID: "web",
|
||||
Name: "КуСфера Web App",
|
||||
Trusted: true,
|
||||
RedirectURIs: []string{
|
||||
"{{OC_URL}}/",
|
||||
"{{OC_URL}}/oidc-callback.html",
|
||||
"{{OC_URL}}/oidc-silent-redirect.html",
|
||||
},
|
||||
Origins: []string{
|
||||
"{{OC_URL}}",
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "QsferaDesktop",
|
||||
Name: "КуСфера Desktop Client",
|
||||
ApplicationType: "native",
|
||||
RedirectURIs: []string{
|
||||
"http://127.0.0.1",
|
||||
"http://localhost",
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "QsferaAndroid",
|
||||
Name: "КуСфера Android App",
|
||||
ApplicationType: "native",
|
||||
RedirectURIs: []string{
|
||||
"oc://android.qsfera.eu",
|
||||
},
|
||||
PostLogoutRedirectURIs: []string{
|
||||
"oc://android.qsfera.eu",
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "QsferaIOS",
|
||||
Name: "КуСфера iOS App",
|
||||
ApplicationType: "native",
|
||||
RedirectURIs: []string{
|
||||
"oc://ios.qsfera.eu",
|
||||
},
|
||||
PostLogoutRedirectURIs: []string{
|
||||
"oc://ios.qsfera.eu",
|
||||
},
|
||||
},
|
||||
},
|
||||
Ldap: config.Ldap{
|
||||
URI: "ldaps://localhost:9235",
|
||||
TLSCACert: filepath.Join(defaults.BaseDataPath(), "idm", "ldap.crt"),
|
||||
BindDN: "uid=idp,ou=sysusers,o=libregraph-idm",
|
||||
BaseDN: "ou=users,o=libregraph-idm",
|
||||
Scope: "sub",
|
||||
LoginAttribute: "uid",
|
||||
EmailAttribute: "mail",
|
||||
NameAttribute: "displayName",
|
||||
UUIDAttribute: "qsferaUUID",
|
||||
UUIDAttributeType: "text",
|
||||
Filter: "",
|
||||
ObjectClass: "inetOrgPerson",
|
||||
UserEnabledAttribute: "qsferaUserEnabled",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// EnsureDefaults adds default values to the configuration if they are not set yet
|
||||
func EnsureDefaults(cfg *config.Config) {
|
||||
if cfg.LogLevel == "" {
|
||||
cfg.LogLevel = "error"
|
||||
}
|
||||
|
||||
if cfg.Reva == nil && cfg.Commons != nil {
|
||||
cfg.Reva = structs.CopyOrZeroValue(cfg.Commons.Reva)
|
||||
}
|
||||
|
||||
if cfg.MachineAuthAPIKey == "" && cfg.Commons != nil && cfg.Commons.MachineAuthAPIKey != "" {
|
||||
cfg.MachineAuthAPIKey = cfg.Commons.MachineAuthAPIKey
|
||||
}
|
||||
}
|
||||
|
||||
// Sanitize sanitizes the configuration
|
||||
func Sanitize(cfg *config.Config) {
|
||||
// sanitize config
|
||||
if cfg.HTTP.Root != "/" {
|
||||
cfg.HTTP.Root = strings.TrimSuffix(cfg.HTTP.Root, "/")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package config
|
||||
|
||||
// HTTP defines the available http configuration.
|
||||
type HTTP struct {
|
||||
Addr string `yaml:"addr" env:"IDP_HTTP_ADDR" desc:"The bind address of the HTTP service." introductionVersion:"1.0.0"`
|
||||
Root string `yaml:"root" env:"IDP_HTTP_ROOT" desc:"Subdirectory that serves as the root for this HTTP service." introductionVersion:"1.0.0"`
|
||||
Namespace string `yaml:"-"`
|
||||
TLSCert string `yaml:"tls_cert" env:"IDP_TRANSPORT_TLS_CERT" desc:"Path/File name of the TLS server certificate (in PEM format) for the IDP service. If not defined, the root directory derives from $OC_BASE_DATA_PATH/idp." introductionVersion:"1.0.0"`
|
||||
TLSKey string `yaml:"tls_key" env:"IDP_TRANSPORT_TLS_KEY" desc:"Path/File name for the TLS certificate key (in PEM format) for the server certificate to use for the IDP service. If not defined, the root directory derives from $OC_BASE_DATA_PATH/idp." introductionVersion:"1.0.0"`
|
||||
TLS bool `yaml:"tls" env:"IDP_TLS" desc:"Disable or Enable HTTPS for the communication between the Proxy service and the IDP service. If set to 'true', the key and cert files need to be configured and present." introductionVersion:"1.0.0"`
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
occfg "github.com/qsfera/server/pkg/config"
|
||||
"github.com/qsfera/server/pkg/shared"
|
||||
"github.com/qsfera/server/services/idp/pkg/config"
|
||||
"github.com/qsfera/server/services/idp/pkg/config/defaults"
|
||||
|
||||
"github.com/qsfera/server/pkg/config/envdecode"
|
||||
)
|
||||
|
||||
// ParseConfig loads configuration from known paths.
|
||||
func ParseConfig(cfg *config.Config) error {
|
||||
err := occfg.BindSourcesToStructs(cfg.Service.Name, cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defaults.EnsureDefaults(cfg)
|
||||
|
||||
// load all env variables relevant to the config in the current context.
|
||||
if err := envdecode.Decode(cfg); err != nil {
|
||||
// no environment variable set for this config is an expected "error"
|
||||
if !errors.Is(err, envdecode.ErrNoTargetFieldsAreSet) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
defaults.Sanitize(cfg)
|
||||
|
||||
return Validate(cfg)
|
||||
}
|
||||
|
||||
func Validate(cfg *config.Config) error {
|
||||
switch cfg.IDP.IdentityManager {
|
||||
case "cs3":
|
||||
if cfg.MachineAuthAPIKey == "" {
|
||||
return shared.MissingMachineAuthApiKeyError(cfg.Service.Name)
|
||||
}
|
||||
case "ldap":
|
||||
if cfg.Ldap.BindPassword == "" {
|
||||
return shared.MissingLDAPBindPassword(cfg.Service.Name)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package config
|
||||
|
||||
// Service defines the available service configuration.
|
||||
type Service struct {
|
||||
Name string `yaml:"-"`
|
||||
PasswordResetURI string `yaml:"password_reset_uri" env:"IDP_PASSWORD_RESET_URI" desc:"The URI where a user can reset their password." introductionVersion:"1.0.0"`
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package metrics
|
||||
|
||||
import "github.com/prometheus/client_golang/prometheus"
|
||||
|
||||
var (
|
||||
// Namespace defines the namespace for the defines metrics.
|
||||
Namespace = "qsfera"
|
||||
|
||||
// Subsystem defines the subsystem for the defines metrics.
|
||||
Subsystem = "idp"
|
||||
)
|
||||
|
||||
// Metrics defines the available metrics of this service.
|
||||
type Metrics struct {
|
||||
// Counter *prometheus.CounterVec
|
||||
BuildInfo *prometheus.GaugeVec
|
||||
}
|
||||
|
||||
// New initializes the available metrics.
|
||||
func New() *Metrics {
|
||||
m := &Metrics{
|
||||
// Counter: prometheus.NewCounterVec(prometheus.CounterOpts{
|
||||
// Namespace: Namespace,
|
||||
// Subsystem: Subsystem,
|
||||
// Name: "greet_total",
|
||||
// Help: "How many greeting requests processed",
|
||||
// }, []string{}),
|
||||
BuildInfo: prometheus.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Namespace: Namespace,
|
||||
Subsystem: Subsystem,
|
||||
Name: "build_info",
|
||||
Help: "Build Information",
|
||||
}, []string{"version"}),
|
||||
}
|
||||
|
||||
// prometheus.Register(
|
||||
// m.Counter,
|
||||
// )
|
||||
|
||||
_ = prometheus.Register(
|
||||
m.BuildInfo,
|
||||
)
|
||||
|
||||
return m
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
// Package middleware provides middleware for the idp service.
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/codes"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
)
|
||||
|
||||
// Static is a middleware that serves static assets.
|
||||
func Static(root string, fs http.FileSystem, tp trace.TracerProvider) func(http.Handler) http.Handler {
|
||||
if !strings.HasSuffix(root, "/") {
|
||||
root = root + "/"
|
||||
}
|
||||
|
||||
static := http.StripPrefix(
|
||||
root,
|
||||
http.FileServer(
|
||||
fs,
|
||||
),
|
||||
)
|
||||
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
spanOpts := []trace.SpanStartOption{
|
||||
trace.WithSpanKind(trace.SpanKindServer),
|
||||
}
|
||||
ctx, span := tp.Tracer("idp").Start(r.Context(), "serve static asset", spanOpts...)
|
||||
defer span.End()
|
||||
r = r.WithContext(ctx)
|
||||
|
||||
// serve the static assets for the identifier web app
|
||||
if strings.HasPrefix(r.URL.Path, "/signin/v1/static/") {
|
||||
if strings.HasSuffix(r.URL.Path, "/") {
|
||||
// but no listing of folders
|
||||
span.SetAttributes(attribute.KeyValue{Key: "path", Value: attribute.StringValue(r.URL.Path)})
|
||||
span.SetStatus(codes.Error, "asset not found")
|
||||
http.NotFound(w, r)
|
||||
} else {
|
||||
r.URL.Path = strings.Replace(r.URL.Path, "/signin/v1/static/", "/signin/v1/identifier/static/", 1)
|
||||
span.SetAttributes(attribute.KeyValue{Key: "server", Value: attribute.StringValue(r.URL.Path)})
|
||||
static.ServeHTTP(w, r)
|
||||
}
|
||||
return
|
||||
}
|
||||
span.SetAttributes(attribute.KeyValue{Key: "path", Value: attribute.StringValue(r.URL.Path)})
|
||||
span.SetStatus(codes.Ok, "ok")
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package debug
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/qsfera/server/pkg/log"
|
||||
"github.com/qsfera/server/services/idp/pkg/config"
|
||||
)
|
||||
|
||||
// Option defines a single option function.
|
||||
type Option func(o *Options)
|
||||
|
||||
// Options defines the available options for this package.
|
||||
type Options struct {
|
||||
Logger log.Logger
|
||||
Context context.Context
|
||||
Config *config.Config
|
||||
}
|
||||
|
||||
// newOptions initializes the available default options.
|
||||
func newOptions(opts ...Option) Options {
|
||||
opt := Options{}
|
||||
|
||||
for _, o := range opts {
|
||||
o(&opt)
|
||||
}
|
||||
|
||||
return opt
|
||||
}
|
||||
|
||||
// Logger provides a function to set the logger option.
|
||||
func Logger(val log.Logger) Option {
|
||||
return func(o *Options) {
|
||||
o.Logger = val
|
||||
}
|
||||
}
|
||||
|
||||
// Context provides a function to set the context option.
|
||||
func Context(val context.Context) Option {
|
||||
return func(o *Options) {
|
||||
o.Context = val
|
||||
}
|
||||
}
|
||||
|
||||
// Config provides a function to set the config option.
|
||||
func Config(val *config.Config) Option {
|
||||
return func(o *Options) {
|
||||
o.Config = val
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package debug
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/qsfera/server/pkg/checks"
|
||||
"github.com/qsfera/server/pkg/handlers"
|
||||
"github.com/qsfera/server/pkg/service/debug"
|
||||
"github.com/qsfera/server/pkg/version"
|
||||
)
|
||||
|
||||
// Server initializes the debug service and server.
|
||||
func Server(opts ...Option) (*http.Server, error) {
|
||||
options := newOptions(opts...)
|
||||
|
||||
healthHandlerConfiguration := handlers.NewCheckHandlerConfiguration().
|
||||
WithLogger(options.Logger).
|
||||
WithCheck("http reachability", checks.NewHTTPCheck(options.Config.HTTP.Addr))
|
||||
|
||||
u, err := url.Parse(options.Config.Ldap.URI)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
readyHandlerConfiguration := healthHandlerConfiguration.
|
||||
WithCheck("ldap reachability", checks.NewTCPCheck(u.Host))
|
||||
|
||||
return debug.NewService(
|
||||
debug.Logger(options.Logger),
|
||||
debug.Name(options.Config.Service.Name),
|
||||
debug.Version(version.GetString()),
|
||||
debug.Address(options.Config.Debug.Addr),
|
||||
debug.Token(options.Config.Debug.Token),
|
||||
debug.Pprof(options.Config.Debug.Pprof),
|
||||
debug.Zpages(options.Config.Debug.Zpages),
|
||||
debug.Health(handlers.NewCheckHandler(healthHandlerConfiguration)),
|
||||
debug.Ready(handlers.NewCheckHandler(readyHandlerConfiguration)),
|
||||
), nil
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/qsfera/server/pkg/log"
|
||||
"github.com/qsfera/server/services/idp/pkg/config"
|
||||
"github.com/qsfera/server/services/idp/pkg/metrics"
|
||||
|
||||
"github.com/spf13/pflag"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
)
|
||||
|
||||
// Option defines a single option function.
|
||||
type Option func(o *Options)
|
||||
|
||||
// Options defines the available options for this package.
|
||||
type Options struct {
|
||||
Namespace string
|
||||
Logger log.Logger
|
||||
Context context.Context
|
||||
Config *config.Config
|
||||
Metrics *metrics.Metrics
|
||||
Flags []pflag.Flag
|
||||
TraceProvider trace.TracerProvider
|
||||
}
|
||||
|
||||
// newOptions initializes the available default options.
|
||||
func newOptions(opts ...Option) Options {
|
||||
opt := Options{}
|
||||
|
||||
for _, o := range opts {
|
||||
o(&opt)
|
||||
}
|
||||
|
||||
return opt
|
||||
}
|
||||
|
||||
// Logger provides a function to set the logger option.
|
||||
func Logger(val log.Logger) Option {
|
||||
return func(o *Options) {
|
||||
o.Logger = val
|
||||
}
|
||||
}
|
||||
|
||||
// Context provides a function to set the context option.
|
||||
func Context(val context.Context) Option {
|
||||
return func(o *Options) {
|
||||
o.Context = val
|
||||
}
|
||||
}
|
||||
|
||||
// Config provides a function to set the config option.
|
||||
func Config(val *config.Config) Option {
|
||||
return func(o *Options) {
|
||||
o.Config = val
|
||||
}
|
||||
}
|
||||
|
||||
// Metrics provides a function to set the metrics option.
|
||||
func Metrics(val *metrics.Metrics) Option {
|
||||
return func(o *Options) {
|
||||
o.Metrics = val
|
||||
}
|
||||
}
|
||||
|
||||
// Flags provides a function to set the flags option.
|
||||
func Flags(flags ...pflag.Flag) Option {
|
||||
return func(o *Options) {
|
||||
o.Flags = append(o.Flags, flags...)
|
||||
}
|
||||
}
|
||||
|
||||
// Namespace provides a function to set the namespace option.
|
||||
func Namespace(val string) Option {
|
||||
return func(o *Options) {
|
||||
o.Namespace = val
|
||||
}
|
||||
}
|
||||
|
||||
// TraceProvider provides a function to set the trace provider option.
|
||||
func TraceProvider(val trace.TracerProvider) Option {
|
||||
return func(o *Options) {
|
||||
o.TraceProvider = val
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
chimiddleware "github.com/go-chi/chi/v5/middleware"
|
||||
pkgcrypto "github.com/qsfera/server/pkg/crypto"
|
||||
"github.com/qsfera/server/pkg/middleware"
|
||||
"github.com/qsfera/server/pkg/service/http"
|
||||
"github.com/qsfera/server/pkg/shared"
|
||||
"github.com/qsfera/server/pkg/version"
|
||||
svc "github.com/qsfera/server/services/idp/pkg/service/v0"
|
||||
"go-micro.dev/v4"
|
||||
)
|
||||
|
||||
// Server initializes the http service and server.
|
||||
func Server(opts ...Option) (http.Service, error) {
|
||||
options := newOptions(opts...)
|
||||
|
||||
if options.Config.HTTP.TLS {
|
||||
_, certErr := os.Stat(options.Config.HTTP.TLSCert)
|
||||
_, keyErr := os.Stat(options.Config.HTTP.TLSKey)
|
||||
|
||||
if os.IsNotExist(certErr) || os.IsNotExist(keyErr) {
|
||||
options.Logger.Info().Msgf("Generating certs")
|
||||
if err := pkgcrypto.GenCert(options.Config.HTTP.TLSCert, options.Config.HTTP.TLSKey, options.Logger); err != nil {
|
||||
options.Logger.Fatal().Err(err).Msg("Could not setup TLS")
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
service, err := http.NewService(
|
||||
http.Logger(options.Logger),
|
||||
http.Namespace(options.Config.HTTP.Namespace),
|
||||
http.Name(options.Config.Service.Name),
|
||||
http.Version(version.GetString()),
|
||||
http.Address(options.Config.HTTP.Addr),
|
||||
http.Context(options.Context),
|
||||
http.Flags(options.Flags...),
|
||||
http.TLSConfig(shared.HTTPServiceTLS{
|
||||
Enabled: options.Config.HTTP.TLS,
|
||||
Cert: options.Config.HTTP.TLSCert,
|
||||
Key: options.Config.HTTP.TLSKey,
|
||||
}),
|
||||
http.TraceProvider(options.TraceProvider),
|
||||
)
|
||||
if err != nil {
|
||||
options.Logger.Error().
|
||||
Err(err).
|
||||
Msg("Error initializing http service")
|
||||
return http.Service{}, fmt.Errorf("could not initialize http service: %w", err)
|
||||
}
|
||||
|
||||
handle := svc.NewService(
|
||||
svc.Logger(options.Logger),
|
||||
svc.Config(options.Config),
|
||||
svc.Middleware(
|
||||
chimiddleware.RealIP,
|
||||
chimiddleware.RequestID,
|
||||
middleware.TraceContext,
|
||||
middleware.NoCache,
|
||||
middleware.Version(
|
||||
options.Config.Service.Name,
|
||||
version.GetString(),
|
||||
),
|
||||
middleware.Logger(
|
||||
options.Logger,
|
||||
),
|
||||
),
|
||||
svc.TraceProvider(options.TraceProvider),
|
||||
)
|
||||
|
||||
{
|
||||
handle = svc.NewInstrument(handle, options.Metrics)
|
||||
handle = svc.NewLoggingHandler(handle, options.Logger)
|
||||
}
|
||||
|
||||
if err := micro.RegisterHandler(service.Server(), handle); err != nil {
|
||||
return http.Service{}, err
|
||||
}
|
||||
|
||||
return service, nil
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package svc
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/qsfera/server/services/idp/pkg/metrics"
|
||||
)
|
||||
|
||||
// NewInstrument returns a service that instruments metrics.
|
||||
func NewInstrument(next Service, metrics *metrics.Metrics) Service {
|
||||
return instrument{
|
||||
next: next,
|
||||
metrics: metrics,
|
||||
}
|
||||
}
|
||||
|
||||
type instrument struct {
|
||||
next Service
|
||||
metrics *metrics.Metrics
|
||||
}
|
||||
|
||||
// ServeHTTP implements the Service interface.
|
||||
func (i instrument) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
i.next.ServeHTTP(w, r)
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package svc
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/qsfera/server/pkg/log"
|
||||
)
|
||||
|
||||
// NewLogging returns a service that logs messages.
|
||||
func NewLoggingHandler(next Service, logger log.Logger) Service {
|
||||
return loggingHandler{
|
||||
next: next,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
type loggingHandler struct {
|
||||
next Service
|
||||
logger log.Logger
|
||||
}
|
||||
|
||||
// ServeHTTP implements the Service interface.
|
||||
func (l loggingHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
l.next.ServeHTTP(w, r)
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package svc
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/qsfera/server/pkg/log"
|
||||
"github.com/qsfera/server/services/idp/pkg/config"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
)
|
||||
|
||||
// Option defines a single option function.
|
||||
type Option func(o *Options)
|
||||
|
||||
// Options defines the available options for this package.
|
||||
type Options struct {
|
||||
Logger log.Logger
|
||||
Config *config.Config
|
||||
Middleware []func(http.Handler) http.Handler
|
||||
TraceProvider trace.TracerProvider
|
||||
}
|
||||
|
||||
// newOptions initializes the available default options.
|
||||
func newOptions(opts ...Option) Options {
|
||||
opt := Options{}
|
||||
|
||||
for _, o := range opts {
|
||||
o(&opt)
|
||||
}
|
||||
|
||||
return opt
|
||||
}
|
||||
|
||||
// Logger provides a function to set the logger option.
|
||||
func Logger(val log.Logger) Option {
|
||||
return func(o *Options) {
|
||||
o.Logger = val
|
||||
}
|
||||
}
|
||||
|
||||
// Config provides a function to set the config option.
|
||||
func Config(val *config.Config) Option {
|
||||
return func(o *Options) {
|
||||
o.Config = val
|
||||
}
|
||||
}
|
||||
|
||||
// Middleware provides a function to set the middleware option.
|
||||
func Middleware(val ...func(http.Handler) http.Handler) Option {
|
||||
return func(o *Options) {
|
||||
o.Middleware = val
|
||||
}
|
||||
}
|
||||
|
||||
// TraceProvider provides a function to set the trace provider option.
|
||||
func TraceProvider(val trace.TracerProvider) Option {
|
||||
return func(o *Options) {
|
||||
o.TraceProvider = val
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,346 @@
|
||||
package svc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/libregraph/lico/bootstrap"
|
||||
guestBackendSupport "github.com/libregraph/lico/bootstrap/backends/guest"
|
||||
ldapBackendSupport "github.com/libregraph/lico/bootstrap/backends/ldap"
|
||||
libreGraphBackendSupport "github.com/libregraph/lico/bootstrap/backends/libregraph"
|
||||
licoconfig "github.com/libregraph/lico/config"
|
||||
"github.com/libregraph/lico/server"
|
||||
"github.com/qsfera/server/pkg/ldap"
|
||||
"github.com/qsfera/server/pkg/log"
|
||||
"github.com/qsfera/server/pkg/tracing"
|
||||
"github.com/qsfera/server/services/idp/pkg/assets"
|
||||
cs3BackendSupport "github.com/qsfera/server/services/idp/pkg/backends/cs3/bootstrap"
|
||||
"github.com/qsfera/server/services/idp/pkg/config"
|
||||
"github.com/qsfera/server/services/idp/pkg/middleware"
|
||||
"github.com/riandyrn/otelchi"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
"gopkg.in/yaml.v2"
|
||||
"stash.kopano.io/kgol/rndm"
|
||||
)
|
||||
|
||||
// Service defines the service handlers.
|
||||
type Service interface {
|
||||
ServeHTTP(http.ResponseWriter, *http.Request)
|
||||
}
|
||||
|
||||
// NewService returns a service implementation for Service.
|
||||
func NewService(opts ...Option) Service {
|
||||
ctx := context.Background()
|
||||
options := newOptions(opts...)
|
||||
logger := options.Logger.Logger
|
||||
assetVFS := assets.New(
|
||||
assets.Logger(options.Logger),
|
||||
assets.Config(options.Config),
|
||||
)
|
||||
|
||||
if err := createTemporaryClientsConfig(
|
||||
options.Config.IDP.IdentifierRegistrationConf,
|
||||
options.Config.Commons.QsferaURL,
|
||||
options.Config.Clients,
|
||||
); err != nil {
|
||||
logger.Fatal().Err(err).Msg("could not create default config")
|
||||
}
|
||||
|
||||
switch options.Config.IDP.IdentityManager {
|
||||
case "cs3":
|
||||
cs3BackendSupport.MustRegister()
|
||||
if err := initCS3EnvVars(options.Config.Reva.Address, options.Config.MachineAuthAPIKey); err != nil {
|
||||
logger.Fatal().Err(err).Msg("could not initialize cs3 backend env vars")
|
||||
}
|
||||
case "ldap":
|
||||
|
||||
if err := ldap.WaitForCA(options.Logger, options.Config.IDP.Insecure, options.Config.Ldap.TLSCACert); err != nil {
|
||||
logger.Fatal().Err(err).Msg("The configured LDAP CA cert does not exist")
|
||||
}
|
||||
if options.Config.IDP.Insecure {
|
||||
// force CACert to be empty to avoid lico try to load it
|
||||
options.Config.Ldap.TLSCACert = ""
|
||||
}
|
||||
|
||||
ldapBackendSupport.MustRegister()
|
||||
if err := initLicoInternalLDAPEnvVars(&options.Config.Ldap); err != nil {
|
||||
logger.Fatal().Err(err).Msg("could not initialize ldap env vars")
|
||||
}
|
||||
default:
|
||||
guestBackendSupport.MustRegister()
|
||||
libreGraphBackendSupport.MustRegister()
|
||||
}
|
||||
|
||||
idpSettings := bootstrap.Settings{
|
||||
Iss: options.Config.IDP.Iss,
|
||||
IdentityManager: options.Config.IDP.IdentityManager,
|
||||
URIBasePath: options.Config.IDP.URIBasePath,
|
||||
SignInURI: options.Config.IDP.SignInURI,
|
||||
SignedOutURI: options.Config.IDP.SignedOutURI,
|
||||
AuthorizationEndpointURI: options.Config.IDP.AuthorizationEndpointURI,
|
||||
EndsessionEndpointURI: options.Config.IDP.EndsessionEndpointURI,
|
||||
Insecure: options.Config.IDP.Insecure,
|
||||
TrustedProxy: options.Config.IDP.TrustedProxy,
|
||||
AllowScope: options.Config.IDP.AllowScope,
|
||||
AllowClientGuests: options.Config.IDP.AllowClientGuests,
|
||||
AllowDynamicClientRegistration: options.Config.IDP.AllowDynamicClientRegistration,
|
||||
EncryptionSecretFile: options.Config.IDP.EncryptionSecretFile,
|
||||
Listen: options.Config.IDP.Listen,
|
||||
IdentifierClientDisabled: options.Config.IDP.IdentifierClientDisabled,
|
||||
IdentifierClientPath: options.Config.IDP.IdentifierClientPath,
|
||||
IdentifierRegistrationConf: options.Config.IDP.IdentifierRegistrationConf,
|
||||
IdentifierScopesConf: options.Config.IDP.IdentifierScopesConf,
|
||||
IdentifierDefaultBannerLogo: options.Config.IDP.IdentifierDefaultBannerLogo,
|
||||
IdentifierDefaultSignInPageText: options.Config.IDP.IdentifierDefaultSignInPageText,
|
||||
IdentifierDefaultLogoTargetURI: options.Config.IDP.IdentifierDefaultLogoTargetURI,
|
||||
IdentifierDefaultUsernameHintText: options.Config.IDP.IdentifierDefaultUsernameHintText,
|
||||
IdentifierUILocales: options.Config.IDP.IdentifierUILocales,
|
||||
SigningKid: options.Config.IDP.SigningKid,
|
||||
SigningMethod: options.Config.IDP.SigningMethod,
|
||||
SigningPrivateKeyFiles: options.Config.IDP.SigningPrivateKeyFiles,
|
||||
ValidationKeysPath: options.Config.IDP.ValidationKeysPath,
|
||||
CookieBackendURI: options.Config.IDP.CookieBackendURI,
|
||||
CookieNames: options.Config.IDP.CookieNames,
|
||||
CookieSameSite: options.Config.IDP.CookieSameSite,
|
||||
AccessTokenDurationSeconds: options.Config.IDP.AccessTokenDurationSeconds,
|
||||
IDTokenDurationSeconds: options.Config.IDP.IDTokenDurationSeconds,
|
||||
RefreshTokenDurationSeconds: options.Config.IDP.RefreshTokenDurationSeconds,
|
||||
DyamicClientSecretDurationSeconds: options.Config.IDP.DynamicClientSecretDurationSeconds,
|
||||
}
|
||||
bs, err := bootstrap.Boot(ctx, &idpSettings, &licoconfig.Config{
|
||||
Logger: log.LogrusWrap(logger),
|
||||
})
|
||||
if err != nil {
|
||||
logger.Fatal().Err(err).Msg("could not bootstrap idp")
|
||||
}
|
||||
|
||||
managers := bs.Managers()
|
||||
routes := []server.WithRoutes{managers.Must("identity").(server.WithRoutes)}
|
||||
handlers := managers.Must("handler").(http.Handler)
|
||||
|
||||
svc := IDP{
|
||||
logger: options.Logger,
|
||||
config: options.Config,
|
||||
assets: assetVFS,
|
||||
tp: options.TraceProvider,
|
||||
}
|
||||
|
||||
svc.initMux(ctx, routes, handlers, options)
|
||||
|
||||
return svc
|
||||
}
|
||||
|
||||
type temporaryClientConfig struct {
|
||||
Clients []config.Client `yaml:"clients"`
|
||||
}
|
||||
|
||||
func createTemporaryClientsConfig(filePath, qsferaURL string, clients []config.Client) error {
|
||||
folder := path.Dir(filePath)
|
||||
if _, err := os.Stat(folder); os.IsNotExist(err) {
|
||||
if err := os.MkdirAll(folder, 0o700); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
for i, client := range clients {
|
||||
|
||||
for i, entry := range client.RedirectURIs {
|
||||
client.RedirectURIs[i] = strings.ReplaceAll(entry, "{{OC_URL}}", strings.TrimRight(qsferaURL, "/"))
|
||||
}
|
||||
for i, entry := range client.Origins {
|
||||
client.Origins[i] = strings.ReplaceAll(entry, "{{OC_URL}}", strings.TrimRight(qsferaURL, "/"))
|
||||
}
|
||||
clients[i] = client
|
||||
}
|
||||
|
||||
c := temporaryClientConfig{
|
||||
Clients: clients,
|
||||
}
|
||||
|
||||
conf, err := yaml.Marshal(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
confOnDisk, err := os.Create(filePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer confOnDisk.Close()
|
||||
|
||||
err = os.WriteFile(filePath, conf, 0o600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Init cs3 backend vars which are currently not accessible via idp api
|
||||
func initCS3EnvVars(cs3Addr, machineAuthAPIKey string) error {
|
||||
defaults := map[string]string{
|
||||
"CS3_GATEWAY": cs3Addr,
|
||||
"CS3_MACHINE_AUTH_API_KEY": machineAuthAPIKey,
|
||||
}
|
||||
|
||||
for k, v := range defaults {
|
||||
if err := os.Setenv(k, v); err != nil {
|
||||
return fmt.Errorf("could not set cs3 env var %s=%s", k, v)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Init ldap backend vars which are currently not accessible via idp api
|
||||
func initLicoInternalLDAPEnvVars(ldap *config.Ldap) error {
|
||||
filter := fmt.Sprintf("(objectclass=%s)", ldap.ObjectClass)
|
||||
|
||||
var needsAnd bool
|
||||
if ldap.Filter != "" {
|
||||
filter += ldap.Filter
|
||||
needsAnd = true
|
||||
}
|
||||
|
||||
if ldap.UserEnabledAttribute != "" {
|
||||
// Using a (!(enabled=FALSE)) filter here to allow user without
|
||||
// any value for the enable flag to log in
|
||||
filter += fmt.Sprintf("(!(%s=FALSE))", ldap.UserEnabledAttribute)
|
||||
needsAnd = true
|
||||
}
|
||||
|
||||
if needsAnd {
|
||||
filter = fmt.Sprintf("(&%s)", filter)
|
||||
}
|
||||
|
||||
defaults := map[string]string{
|
||||
"LDAP_URI": ldap.URI,
|
||||
"LDAP_BINDDN": ldap.BindDN,
|
||||
"LDAP_BINDPW": ldap.BindPassword,
|
||||
"LDAP_BASEDN": ldap.BaseDN,
|
||||
"LDAP_SCOPE": ldap.Scope,
|
||||
"LDAP_LOGIN_ATTRIBUTE": ldap.LoginAttribute,
|
||||
"LDAP_EMAIL_ATTRIBUTE": ldap.EmailAttribute,
|
||||
"LDAP_NAME_ATTRIBUTE": ldap.NameAttribute,
|
||||
"LDAP_UUID_ATTRIBUTE": ldap.UUIDAttribute,
|
||||
"LDAP_SUB_ATTRIBUTES": ldap.UUIDAttribute,
|
||||
"LDAP_UUID_ATTRIBUTE_TYPE": ldap.UUIDAttributeType,
|
||||
"LDAP_FILTER": filter,
|
||||
}
|
||||
|
||||
if ldap.TLSCACert != "" {
|
||||
defaults["LDAP_TLS_CACERT"] = ldap.TLSCACert
|
||||
}
|
||||
|
||||
for k, v := range defaults {
|
||||
if err := os.Setenv(k, v); err != nil {
|
||||
return fmt.Errorf("could not set ldap env var %s=%s", k, v)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// IDP defines implements the business logic for Service.
|
||||
type IDP struct {
|
||||
logger log.Logger
|
||||
config *config.Config
|
||||
mux *chi.Mux
|
||||
assets http.FileSystem
|
||||
tp trace.TracerProvider
|
||||
}
|
||||
|
||||
// initMux initializes the internal idp gorilla mux and mounts it in to an КуСфера chi-router
|
||||
func (idp *IDP) initMux(ctx context.Context, r []server.WithRoutes, h http.Handler, options Options) {
|
||||
gm := mux.NewRouter()
|
||||
for _, route := range r {
|
||||
route.AddRoutes(ctx, gm)
|
||||
}
|
||||
|
||||
// Delegate rest to provider which is also a handler.
|
||||
if h != nil {
|
||||
gm.NotFoundHandler = h
|
||||
}
|
||||
|
||||
idp.mux = chi.NewMux()
|
||||
idp.mux.Use(options.Middleware...)
|
||||
|
||||
idp.mux.Use(middleware.Static(
|
||||
"/signin/v1/",
|
||||
assets.New(
|
||||
assets.Logger(options.Logger),
|
||||
assets.Config(options.Config),
|
||||
),
|
||||
idp.tp,
|
||||
))
|
||||
|
||||
idp.mux.Use(
|
||||
otelchi.Middleware(
|
||||
"idp",
|
||||
otelchi.WithChiRoutes(idp.mux),
|
||||
otelchi.WithTracerProvider(idp.tp),
|
||||
otelchi.WithPropagators(tracing.GetPropagator()),
|
||||
),
|
||||
)
|
||||
|
||||
// handle / | index.html with a template that needs to have the BASE_PREFIX replaced
|
||||
idp.mux.Get("/signin/v1/identifier", idp.Index())
|
||||
idp.mux.Get("/signin/v1/identifier/", idp.Index())
|
||||
idp.mux.Get("/signin/v1/identifier/index.html", idp.Index())
|
||||
|
||||
idp.mux.Mount("/", gm)
|
||||
|
||||
_ = chi.Walk(idp.mux, func(method string, route string, handler http.Handler, middlewares ...func(http.Handler) http.Handler) error {
|
||||
options.Logger.Debug().Str("method", method).Str("route", route).Int("middlewares", len(middlewares)).Msg("serving endpoint")
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// ServeHTTP implements the Service interface.
|
||||
func (idp IDP) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
idp.mux.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
// Index renders the static html with templated variables.
|
||||
func (idp IDP) Index() http.HandlerFunc {
|
||||
f, err := idp.assets.Open("/identifier/index.html")
|
||||
if err != nil {
|
||||
idp.logger.Fatal().Err(err).Msg("Could not open index template")
|
||||
}
|
||||
|
||||
template, err := io.ReadAll(f)
|
||||
if err != nil {
|
||||
idp.logger.Fatal().Err(err).Msg("Could not read index template")
|
||||
}
|
||||
if err = f.Close(); err != nil {
|
||||
idp.logger.Fatal().Err(err).Msg("Could not close body")
|
||||
}
|
||||
|
||||
// TODO add environment variable to make the path prefix configurable
|
||||
pp := "/signin/v1"
|
||||
indexHTML := bytes.Replace(template, []byte("__PATH_PREFIX__"), []byte(pp), 1)
|
||||
|
||||
background := idp.config.Asset.LoginBackgroundUrl
|
||||
indexHTML = bytes.Replace(template, []byte("__BG_IMG_URL__"), []byte(background), 1)
|
||||
|
||||
nonce := rndm.GenerateRandomString(32)
|
||||
indexHTML = bytes.Replace(indexHTML, []byte("__CSP_NONCE__"), []byte(nonce), 1)
|
||||
|
||||
indexHTML = bytes.Replace(indexHTML, []byte("__PASSWORD_RESET_LINK__"), []byte(idp.config.Service.PasswordResetURI), 1)
|
||||
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
if _, err := w.Write(indexHTML); err != nil {
|
||||
idp.logger.Error().Err(err).Msg("could not write to response writer")
|
||||
}
|
||||
})
|
||||
}
|
||||
Generated
+10519
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,17 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head data-kopano-build="%REACT_APP_KOPANO_BUILD%">
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
<meta name="theme-color" content="#1b223d">
|
||||
<link rel="icon" href="%PUBLIC_URL%/static/favicon.svg" type="image/svg+xml">
|
||||
<meta property="csp-nonce" content="__CSP_NONCE__">
|
||||
<title>Sign in - КуСфера</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
You need to enable JavaScript to run this app.
|
||||
</noscript>
|
||||
<main id="root" data-path-prefix="__PATH_PREFIX__" passwort-reset-link="__PASSWORD_RESET_LINK__" data-bg-img="__BG_IMG_URL__"></main>
|
||||
</body>
|
||||
</html>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
@@ -0,0 +1,12 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 180 180" role="img" aria-label="КуСфера">
|
||||
<path fill="#061D2B" d="M34,12h112C158,12 168,22 168,34v112C168,158 158,168 146,168H34C22,168 12,158 12,146V34C12,22 22,12 34,12z"/>
|
||||
<path fill="#123E55" fill-opacity="0.74" d="M12,45C48,19 87,20 121,48C145,68 158,68 168,60V34C168,22 158,12 146,12H34C22,12 12,22 12,34z"/>
|
||||
<path fill="#19B8C9" fill-opacity="0.18" d="M12,126C46,105 82,104 114,124C139,139 156,139 168,128V146C168,158 158,168 146,168H34C22,168 12,158 12,146z"/>
|
||||
<path fill="none" stroke="#20D6E6" stroke-linecap="round" stroke-width="10" d="M75,47C100,29 138,39 153,70C167,100 151,135 121,147"/>
|
||||
<path fill="none" stroke="#9AF8FF" stroke-linecap="round" stroke-width="4" d="M81,45C101,35 130,39 146,62"/>
|
||||
<path fill="#21D1D6" d="M48,52h25v50h-25z"/>
|
||||
<path fill="#F8FCFF" d="M48,102h25v42h-25z"/>
|
||||
<path fill="#18BFD0" d="M77,94L111,55H143L99,103z"/>
|
||||
<path fill="#F8FCFF" d="M77,104L100,82L148,144H116z"/>
|
||||
<path fill="#9DF8FF" fill-opacity="0.45" d="M48,52h25v8h-25z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,211 @@
|
||||
'use strict';
|
||||
|
||||
// Do this as the first thing so that any code reading it knows the right env.
|
||||
process.env.BABEL_ENV = 'production';
|
||||
process.env.NODE_ENV = 'production';
|
||||
|
||||
// Makes the script crash on unhandled rejections instead of silently
|
||||
// ignoring them. In the future, promise rejections that are not handled will
|
||||
// terminate the Node.js process with a non-zero exit code.
|
||||
process.on('unhandledRejection', err => {
|
||||
throw err;
|
||||
});
|
||||
|
||||
// Ensure environment variables are read.
|
||||
require('../ui_config/env');
|
||||
|
||||
|
||||
const path = require('path');
|
||||
const chalk = require('react-dev-utils/chalk');
|
||||
const fs = require('fs-extra');
|
||||
const webpack = require('webpack');
|
||||
const configFactory = require('../ui_config/webpack.config');
|
||||
const paths = require('../ui_config/paths');
|
||||
const checkRequiredFiles = require('react-dev-utils/checkRequiredFiles');
|
||||
const formatWebpackMessages = require('react-dev-utils/formatWebpackMessages');
|
||||
const printHostingInstructions = require('react-dev-utils/printHostingInstructions');
|
||||
const FileSizeReporter = require('react-dev-utils/FileSizeReporter');
|
||||
const printBuildError = require('react-dev-utils/printBuildError');
|
||||
|
||||
const measureFileSizesBeforeBuild =
|
||||
FileSizeReporter.measureFileSizesBeforeBuild;
|
||||
const printFileSizesAfterBuild = FileSizeReporter.printFileSizesAfterBuild;
|
||||
const usePnpm = fs.existsSync(paths.pnpmLockFile);
|
||||
|
||||
// These sizes are pretty large. We'll warn for bundles exceeding them.
|
||||
const WARN_AFTER_BUNDLE_GZIP_SIZE = 512 * 1024;
|
||||
const WARN_AFTER_CHUNK_GZIP_SIZE = 1024 * 1024;
|
||||
|
||||
const isInteractive = process.stdout.isTTY;
|
||||
|
||||
// Warn and crash if required files are missing
|
||||
if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Generate configuration
|
||||
const config = configFactory('production');
|
||||
|
||||
// We require that you explicitly set browsers and do not fall back to
|
||||
// browserslist defaults.
|
||||
const { checkBrowsers } = require('react-dev-utils/browsersHelper');
|
||||
checkBrowsers(paths.appPath, isInteractive)
|
||||
.then(() => {
|
||||
// First, read the current file sizes in build directory.
|
||||
// This lets us display how much they changed later.
|
||||
return measureFileSizesBeforeBuild(paths.appBuild);
|
||||
})
|
||||
.then(previousFileSizes => {
|
||||
// Remove all content but keep the directory so that
|
||||
// if you're in it, you don't end up in Trash
|
||||
fs.emptyDirSync(paths.appBuild);
|
||||
// Merge with the public folder
|
||||
copyPublicFolder();
|
||||
// Start the webpack build
|
||||
return build(previousFileSizes);
|
||||
})
|
||||
.then(
|
||||
({ stats, previousFileSizes, warnings }) => {
|
||||
if (warnings.length) {
|
||||
console.log(chalk.yellow('Compiled with warnings.\n'));
|
||||
console.log(warnings.join('\n\n'));
|
||||
console.log(
|
||||
'\nSearch for the ' +
|
||||
chalk.underline(chalk.yellow('keywords')) +
|
||||
' to learn more about each warning.'
|
||||
);
|
||||
console.log(
|
||||
'To ignore, add ' +
|
||||
chalk.cyan('// eslint-disable-next-line') +
|
||||
' to the line before.\n'
|
||||
);
|
||||
} else {
|
||||
console.log(chalk.green('Compiled successfully.\n'));
|
||||
}
|
||||
|
||||
console.log('File sizes after gzip:\n');
|
||||
printFileSizesAfterBuild(
|
||||
stats,
|
||||
previousFileSizes,
|
||||
paths.appBuild,
|
||||
WARN_AFTER_BUNDLE_GZIP_SIZE,
|
||||
WARN_AFTER_CHUNK_GZIP_SIZE
|
||||
);
|
||||
console.log();
|
||||
|
||||
const appPackage = require(paths.appPackageJson);
|
||||
const publicUrl = paths.publicUrl;
|
||||
const publicPath = config.output.publicPath;
|
||||
const buildFolder = path.relative(process.cwd(), paths.appBuild);
|
||||
printHostingInstructions(
|
||||
appPackage,
|
||||
publicUrl,
|
||||
publicPath,
|
||||
buildFolder,
|
||||
usePnpm
|
||||
);
|
||||
},
|
||||
err => {
|
||||
const tscCompileOnError = process.env.TSC_COMPILE_ON_ERROR === 'true';
|
||||
if (tscCompileOnError) {
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
'Compiled with the following type errors (you may want to check these before deploying your app):\n'
|
||||
)
|
||||
);
|
||||
printBuildError(err);
|
||||
} else {
|
||||
console.log(chalk.red('Failed to compile.\n'));
|
||||
printBuildError(err);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
)
|
||||
.catch(err => {
|
||||
if (err && err.message) {
|
||||
console.log(err.message);
|
||||
}
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
// Create the production build and print the deployment instructions.
|
||||
function build(previousFileSizes) {
|
||||
// We used to support resolving modules according to `NODE_PATH`.
|
||||
// This now has been deprecated in favor of jsconfig/tsconfig.json
|
||||
// This lets you use absolute paths in imports inside large monorepos:
|
||||
if (process.env.NODE_PATH) {
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
'Setting NODE_PATH to resolve modules absolutely has been deprecated in favor of setting baseUrl in jsconfig.json (or tsconfig.json if you are using TypeScript) and will be removed in a future major release of create-react-app.'
|
||||
)
|
||||
);
|
||||
console.log();
|
||||
}
|
||||
|
||||
console.log('Creating an optimized production build...');
|
||||
|
||||
const compiler = webpack(config);
|
||||
return new Promise((resolve, reject) => {
|
||||
compiler.run((err, stats) => {
|
||||
let messages;
|
||||
if (err) {
|
||||
if (!err.message) {
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
let errMessage = err.message;
|
||||
|
||||
// Add additional information for postcss errors
|
||||
if (Object.prototype.hasOwnProperty.call(err, 'postcssNode')) {
|
||||
errMessage +=
|
||||
'\nCompileError: Begins at CSS selector ' +
|
||||
err['postcssNode'].selector;
|
||||
}
|
||||
|
||||
messages = formatWebpackMessages({
|
||||
errors: [errMessage],
|
||||
warnings: [],
|
||||
});
|
||||
} else {
|
||||
messages = formatWebpackMessages(
|
||||
stats.toJson({ all: false, warnings: true, errors: true })
|
||||
);
|
||||
}
|
||||
if (messages.errors.length) {
|
||||
// Only keep the first error. Others are often indicative
|
||||
// of the same problem, but confuse the reader with noise.
|
||||
if (messages.errors.length > 1) {
|
||||
messages.errors.length = 1;
|
||||
}
|
||||
return reject(new Error(messages.errors.join('\n\n')));
|
||||
}
|
||||
if (
|
||||
process.env.CI &&
|
||||
(typeof process.env.CI !== 'string' ||
|
||||
process.env.CI.toLowerCase() !== 'false') &&
|
||||
messages.warnings.length
|
||||
) {
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
'\nTreating warnings as errors because process.env.CI = true.\n' +
|
||||
'Most CI servers set it automatically.\n'
|
||||
)
|
||||
);
|
||||
return reject(new Error(messages.warnings.join('\n\n')));
|
||||
}
|
||||
|
||||
return resolve({
|
||||
stats,
|
||||
previousFileSizes,
|
||||
warnings: messages.warnings,
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function copyPublicFolder() {
|
||||
fs.copySync(paths.appPublic, paths.appBuild, {
|
||||
dereference: true,
|
||||
filter: file => file !== paths.appHtml,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
'use strict';
|
||||
|
||||
// Do this as the first thing so that any code reading it knows the right env.
|
||||
process.env.BABEL_ENV = 'development';
|
||||
process.env.NODE_ENV = 'development';
|
||||
|
||||
// Makes the script crash on unhandled rejections instead of silently
|
||||
// ignoring them. In the future, promise rejections that are not handled will
|
||||
// terminate the Node.js process with a non-zero exit code.
|
||||
process.on('unhandledRejection', err => {
|
||||
throw err;
|
||||
});
|
||||
|
||||
// Ensure environment variables are read.
|
||||
require('../ui_config/env');
|
||||
|
||||
|
||||
const fs = require('fs');
|
||||
const chalk = require('react-dev-utils/chalk');
|
||||
const webpack = require('webpack');
|
||||
const WebpackDevServer = require('webpack-dev-server');
|
||||
const clearConsole = require('react-dev-utils/clearConsole');
|
||||
const checkRequiredFiles = require('react-dev-utils/checkRequiredFiles');
|
||||
const {
|
||||
choosePort,
|
||||
createCompiler,
|
||||
prepareProxy,
|
||||
prepareUrls,
|
||||
} = require('react-dev-utils/WebpackDevServerUtils');
|
||||
const openBrowser = require('react-dev-utils/openBrowser');
|
||||
const paths = require('../ui_config/paths');
|
||||
const configFactory = require('../ui_config/webpack.config');
|
||||
const createDevServerConfig = require('../ui_config/webpackDevServer.config');
|
||||
|
||||
const usePnpm = fs.existsSync(paths.pnpmLockFile);
|
||||
const isInteractive = process.stdout.isTTY;
|
||||
|
||||
// Warn and crash if required files are missing
|
||||
if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Tools like Cloud9 rely on this.
|
||||
const DEFAULT_PORT = parseInt(process.env.PORT, 10) || 3000;
|
||||
const HOST = process.env.HOST || '0.0.0.0';
|
||||
|
||||
if (process.env.HOST) {
|
||||
console.log(
|
||||
chalk.cyan(
|
||||
`Attempting to bind to HOST environment variable: ${chalk.yellow(
|
||||
chalk.bold(process.env.HOST)
|
||||
)}`
|
||||
)
|
||||
);
|
||||
console.log(
|
||||
`If this was unintentional, check that you haven't mistakenly set it in your shell.`
|
||||
);
|
||||
console.log(
|
||||
`Learn more here: ${chalk.yellow('https://bit.ly/CRA-advanced-config')}`
|
||||
);
|
||||
console.log();
|
||||
}
|
||||
|
||||
// We require that you explicitly set browsers and do not fall back to
|
||||
// browserslist defaults.
|
||||
const { checkBrowsers } = require('react-dev-utils/browsersHelper');
|
||||
checkBrowsers(paths.appPath, isInteractive)
|
||||
.then(() => {
|
||||
// We attempt to use the default port but if it is busy, we offer the user to
|
||||
// run on a different port. `choosePort()` Promise resolves to the next free port.
|
||||
return choosePort(HOST, DEFAULT_PORT);
|
||||
})
|
||||
.then(port => {
|
||||
if (port == null) {
|
||||
// We have not found a port.
|
||||
return;
|
||||
}
|
||||
const config = configFactory('development');
|
||||
const protocol = process.env.HTTPS === 'true' ? 'https' : 'http';
|
||||
const appName = require(paths.appPackageJson).name;
|
||||
const useTypeScript = fs.existsSync(paths.appTsConfig);
|
||||
const tscCompileOnError = process.env.TSC_COMPILE_ON_ERROR === 'true';
|
||||
const urls = prepareUrls(protocol, HOST, port);
|
||||
const devSocket = {
|
||||
warnings: warnings =>
|
||||
devServer.sockWrite(devServer.sockets, 'warnings', warnings),
|
||||
errors: errors =>
|
||||
devServer.sockWrite(devServer.sockets, 'errors', errors),
|
||||
};
|
||||
// Create a webpack compiler that is configured with custom messages.
|
||||
const compiler = createCompiler({
|
||||
appName,
|
||||
config,
|
||||
devSocket,
|
||||
urls,
|
||||
usePnpm,
|
||||
useTypeScript,
|
||||
tscCompileOnError,
|
||||
webpack,
|
||||
});
|
||||
// Load proxy config
|
||||
const proxySetting = require(paths.appPackageJson).proxy;
|
||||
const proxyConfig = prepareProxy(proxySetting, paths.appPublic);
|
||||
// Serve webpack assets generated by the compiler over a web server.
|
||||
const serverConfig = createDevServerConfig(
|
||||
proxyConfig,
|
||||
urls.lanUrlForConfig
|
||||
);
|
||||
const devServer = new WebpackDevServer(compiler, serverConfig);
|
||||
// Launch WebpackDevServer.
|
||||
devServer.listen(port, HOST, err => {
|
||||
if (err) {
|
||||
return console.log(err);
|
||||
}
|
||||
if (isInteractive) {
|
||||
clearConsole();
|
||||
}
|
||||
|
||||
// We used to support resolving modules according to `NODE_PATH`.
|
||||
// This now has been deprecated in favor of jsconfig/tsconfig.json
|
||||
// This lets you use absolute paths in imports inside large monorepos:
|
||||
if (process.env.NODE_PATH) {
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
'Setting NODE_PATH to resolve modules absolutely has been deprecated in favor of setting baseUrl in jsconfig.json (or tsconfig.json if you are using TypeScript) and will be removed in a future major release of create-react-app.'
|
||||
)
|
||||
);
|
||||
console.log();
|
||||
}
|
||||
|
||||
console.log(chalk.cyan('Starting the development server...\n'));
|
||||
openBrowser(urls.localUrlForBrowser);
|
||||
});
|
||||
|
||||
['SIGINT', 'SIGTERM'].forEach(function(sig) {
|
||||
process.on(sig, function() {
|
||||
devServer.close();
|
||||
process.exit();
|
||||
});
|
||||
});
|
||||
})
|
||||
.catch(err => {
|
||||
if (err && err.message) {
|
||||
console.log(err.message);
|
||||
}
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,73 @@
|
||||
import React, {ReactElement, Suspense, lazy, useState, useEffect} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import {MuiThemeProvider} from '@material-ui/core/styles';
|
||||
import muiTheme from './theme';
|
||||
|
||||
import Spinner from './components/Spinner';
|
||||
import * as version from './version';
|
||||
import {QsferaContext} from './qsferaContext';
|
||||
|
||||
const LazyMain = lazy(() => import(/* webpackChunkName: "identifier-main" */ './Main'));
|
||||
|
||||
console.info(`Kopano Identifier build version: ${version.build}`); // eslint-disable-line no-console
|
||||
|
||||
const App = ({ bgImg }): ReactElement => {
|
||||
const [theme, setTheme] = useState(null);
|
||||
const [config, setConfig] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const configResponse = await fetch('/config.json');
|
||||
const configData = await configResponse.json();
|
||||
setConfig(configData);
|
||||
|
||||
const themeResponse = await fetch(configData.theme);
|
||||
const themeData = await themeResponse.json();
|
||||
setTheme(themeData);
|
||||
} catch (error) {
|
||||
console.error('Error fetching config/theme data:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
|
||||
if (loading) {
|
||||
return <Spinner/>;
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<QsferaContext.Provider value={{theme, config}}>
|
||||
<div
|
||||
className={`oc-login-bg ${bgImg ? 'oc-login-bg-image' : ''}`}
|
||||
style={{backgroundImage: bgImg ? `url(${bgImg})` : undefined}}
|
||||
>
|
||||
<MuiThemeProvider theme={muiTheme}>
|
||||
<Suspense fallback={<Spinner/>}>
|
||||
<LazyMain/>
|
||||
</Suspense>
|
||||
</MuiThemeProvider>
|
||||
{!bgImg &&
|
||||
<img
|
||||
src={`${process.env.PUBLIC_URL}/static/icon-lilac.svg`}
|
||||
className={'oc-login-bg-icon'}
|
||||
alt=''
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
</QsferaContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
App.propTypes = {
|
||||
bgImg: PropTypes.string
|
||||
};
|
||||
|
||||
export default App;
|
||||
@@ -0,0 +1,45 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
|
||||
import Routes from './Routes';
|
||||
|
||||
class Main extends PureComponent {
|
||||
render() {
|
||||
const { hello, pathPrefix } = this.props;
|
||||
|
||||
return (
|
||||
<BrowserRouter basename={pathPrefix}>
|
||||
<Routes hello={hello}/>
|
||||
</BrowserRouter>
|
||||
);
|
||||
}
|
||||
|
||||
reload(event) {
|
||||
event.preventDefault();
|
||||
|
||||
window.location.reload();
|
||||
}
|
||||
}
|
||||
|
||||
Main.propTypes = {
|
||||
classes: PropTypes.object.isRequired,
|
||||
|
||||
hello: PropTypes.object,
|
||||
updateAvailable: PropTypes.bool.isRequired,
|
||||
pathPrefix: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
const { hello, updateAvailable, pathPrefix } = state.common;
|
||||
|
||||
return {
|
||||
hello,
|
||||
updateAvailable,
|
||||
pathPrefix
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps)(Main);
|
||||
@@ -0,0 +1,8 @@
|
||||
all: images
|
||||
|
||||
.PHONY: images
|
||||
images:
|
||||
@$(MAKE) -C images
|
||||
|
||||
clean:
|
||||
@$(MAKE) -C images clean
|
||||
@@ -0,0 +1,39 @@
|
||||
import React, { ReactElement, lazy } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { Route, Switch } from 'react-router-dom';
|
||||
|
||||
import PrivateRoute from './components/PrivateRoute';
|
||||
|
||||
const AsyncLogin = lazy(() =>
|
||||
import(/* webpackChunkName: "containers-login" */ './containers/Login'));
|
||||
const AsyncWelcome = lazy(() =>
|
||||
import(/* webpackChunkName: "containers-welcome" */ './containers/Welcome'));
|
||||
const AsyncGoodbye = lazy(() =>
|
||||
import(/* webpackChunkName: "containers-goodbye" */ './containers/Goodbye'));
|
||||
|
||||
const Routes = ({ hello }: {hello: PropTypes.object}): ReactElement => (
|
||||
<Switch>
|
||||
<PrivateRoute
|
||||
path="/welcome"
|
||||
exact
|
||||
component={AsyncWelcome}
|
||||
hello={hello}
|
||||
/>
|
||||
<Route
|
||||
path="/goodbye"
|
||||
exact
|
||||
component={AsyncGoodbye}
|
||||
/>
|
||||
<Route
|
||||
path="/"
|
||||
component={AsyncLogin}
|
||||
/>
|
||||
</Switch>
|
||||
);
|
||||
|
||||
Routes.propTypes = {
|
||||
hello: PropTypes.object
|
||||
};
|
||||
|
||||
export default Routes;
|
||||
@@ -0,0 +1,133 @@
|
||||
import axios from 'axios';
|
||||
|
||||
import { newHelloRequest } from '../models/hello';
|
||||
import { withClientRequestState } from '../utils';
|
||||
import {
|
||||
ExtendedError,
|
||||
ERROR_HTTP_UNEXPECTED_RESPONSE_STATUS,
|
||||
ERROR_HTTP_UNEXPECTED_RESPONSE_STATE
|
||||
} from '../errors';
|
||||
|
||||
import { handleAxiosError } from './utils';
|
||||
import * as types from './types';
|
||||
|
||||
export function receiveError(error) {
|
||||
return {
|
||||
type: types.RECEIVE_ERROR,
|
||||
error
|
||||
};
|
||||
}
|
||||
|
||||
export function resetHello() {
|
||||
return {
|
||||
type: types.RESET_HELLO
|
||||
};
|
||||
}
|
||||
|
||||
export function receiveHello(hello) {
|
||||
const { success, username, displayName } = hello;
|
||||
|
||||
return {
|
||||
type: types.RECEIVE_HELLO,
|
||||
state: success === true,
|
||||
username,
|
||||
displayName,
|
||||
hello
|
||||
};
|
||||
}
|
||||
|
||||
export function executeHello() {
|
||||
return function(dispatch, getState) {
|
||||
dispatch(resetHello());
|
||||
|
||||
const { flow, query } = getState().common;
|
||||
|
||||
const r = withClientRequestState(newHelloRequest(flow, query));
|
||||
return axios.post('./identifier/_/hello', r, {
|
||||
headers: {
|
||||
'Kopano-Konnect-XSRF': '1'
|
||||
}
|
||||
}).then(response => {
|
||||
switch (response.status) {
|
||||
case 200:
|
||||
// success.
|
||||
return response.data;
|
||||
case 204:
|
||||
// not signed-in.
|
||||
return {
|
||||
success: false,
|
||||
state: response.headers['kopano-konnect-state']
|
||||
};
|
||||
default:
|
||||
// error.
|
||||
throw new ExtendedError(ERROR_HTTP_UNEXPECTED_RESPONSE_STATUS, response);
|
||||
}
|
||||
}).then(response => {
|
||||
if (response.state !== r.state) {
|
||||
throw new ExtendedError(ERROR_HTTP_UNEXPECTED_RESPONSE_STATE, response);
|
||||
}
|
||||
|
||||
dispatch(receiveHello(response));
|
||||
return Promise.resolve(response);
|
||||
}).catch(error => {
|
||||
error = handleAxiosError(error);
|
||||
|
||||
dispatch(receiveError(error));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function retryHello() {
|
||||
return function(dispatch) {
|
||||
dispatch(receiveError(null));
|
||||
|
||||
return dispatch(executeHello());
|
||||
};
|
||||
}
|
||||
|
||||
export function requestLogoff() {
|
||||
return {
|
||||
type: types.REQUEST_LOGOFF
|
||||
};
|
||||
}
|
||||
|
||||
export function receiveLogoff(state) {
|
||||
return {
|
||||
type: types.RECEIVE_LOGOFF,
|
||||
state
|
||||
};
|
||||
}
|
||||
|
||||
export function executeLogoff() {
|
||||
return function(dispatch) {
|
||||
dispatch(resetHello());
|
||||
dispatch(requestLogoff());
|
||||
|
||||
const r = withClientRequestState({});
|
||||
return axios.post('./identifier/_/logoff', r, {
|
||||
headers: {
|
||||
'Kopano-Konnect-XSRF': '1'
|
||||
}
|
||||
}).then(response => {
|
||||
switch (response.status) {
|
||||
case 200:
|
||||
// success.
|
||||
return response.data;
|
||||
default:
|
||||
// error.
|
||||
throw new ExtendedError(ERROR_HTTP_UNEXPECTED_RESPONSE_STATUS, response);
|
||||
}
|
||||
}).then(response => {
|
||||
if (response.state !== r.state) {
|
||||
throw new ExtendedError(ERROR_HTTP_UNEXPECTED_RESPONSE_STATE, response);
|
||||
}
|
||||
|
||||
dispatch(receiveLogoff(response.success === true));
|
||||
return Promise.resolve(response);
|
||||
}).catch(error => {
|
||||
error = handleAxiosError(error);
|
||||
|
||||
dispatch(receiveError(error));
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,293 @@
|
||||
import axios from 'axios';
|
||||
import queryString from 'query-string';
|
||||
|
||||
import { newHelloRequest } from '../models/hello';
|
||||
import { withClientRequestState } from '../utils';
|
||||
import {
|
||||
ExtendedError,
|
||||
ERROR_LOGIN_VALIDATE_MISSINGUSERNAME,
|
||||
ERROR_LOGIN_VALIDATE_MISSINGPASSWORD,
|
||||
ERROR_LOGIN_FAILED,
|
||||
ERROR_HTTP_UNEXPECTED_RESPONSE_STATUS,
|
||||
ERROR_HTTP_UNEXPECTED_RESPONSE_STATE
|
||||
} from '../errors';
|
||||
|
||||
import * as types from './types';
|
||||
import { receiveHello } from './common';
|
||||
import { handleAxiosError } from './utils';
|
||||
|
||||
// Modes for logon.
|
||||
export const ModeLogonUsernameEmptyPasswordCookie = '0';
|
||||
export const ModeLogonUsernamePassword = '1';
|
||||
|
||||
export function updateInput(name, value) {
|
||||
return {
|
||||
type: types.UPDATE_INPUT,
|
||||
name,
|
||||
value
|
||||
};
|
||||
}
|
||||
|
||||
export function receiveValidateLogon(errors) {
|
||||
return {
|
||||
type: types.RECEIVE_VALIDATE_LOGON,
|
||||
errors
|
||||
};
|
||||
}
|
||||
|
||||
export function requestLogon(username, password) {
|
||||
return {
|
||||
type: types.REQUEST_LOGON,
|
||||
username,
|
||||
password
|
||||
};
|
||||
}
|
||||
|
||||
export function receiveLogon(logon) {
|
||||
const { success, errors } = logon;
|
||||
|
||||
return {
|
||||
type: types.RECEIVE_LOGON,
|
||||
success,
|
||||
errors
|
||||
};
|
||||
}
|
||||
|
||||
export function requestConsent(allow=false) {
|
||||
return {
|
||||
type: allow ? types.REQUEST_CONSENT_ALLOW : types.REQUEST_CONSENT_CANCEL
|
||||
};
|
||||
}
|
||||
|
||||
export function receiveConsent(logon) {
|
||||
const { success, errors } = logon;
|
||||
|
||||
return {
|
||||
type: types.RECEIVE_CONSENT,
|
||||
success,
|
||||
errors
|
||||
};
|
||||
}
|
||||
|
||||
export function executeLogon(username, password, mode=ModeLogonUsernamePassword) {
|
||||
return function(dispatch, getState) {
|
||||
dispatch(requestLogon(username, password));
|
||||
dispatch(receiveHello({
|
||||
username
|
||||
})); // Reset any hello state on logon.
|
||||
|
||||
const { flow, query } = getState().common;
|
||||
|
||||
// Prepare params based on mode.
|
||||
const params = [];
|
||||
switch (mode) {
|
||||
case ModeLogonUsernamePassword:
|
||||
// Username with password.
|
||||
params.push(username, password, mode);
|
||||
break;
|
||||
|
||||
case ModeLogonUsernameEmptyPasswordCookie:
|
||||
// Username with empty password - this only works when the user is already signed in.
|
||||
params.push(username, '', mode);
|
||||
break;
|
||||
|
||||
// no default
|
||||
}
|
||||
|
||||
const r = withClientRequestState({
|
||||
params: params,
|
||||
hello: newHelloRequest(flow, query)
|
||||
});
|
||||
return axios.post('./identifier/_/logon', r, {
|
||||
headers: {
|
||||
'Kopano-Konnect-XSRF': '1'
|
||||
}
|
||||
}).then(response => {
|
||||
switch (response.status) {
|
||||
case 200:
|
||||
// success.
|
||||
return response.data;
|
||||
case 204:
|
||||
// login failed.
|
||||
return {
|
||||
success: false,
|
||||
state: response.headers['kopano-konnect-state'],
|
||||
errors: {
|
||||
http: new Error(ERROR_LOGIN_FAILED)
|
||||
}
|
||||
};
|
||||
default:
|
||||
// error.
|
||||
throw new ExtendedError(ERROR_HTTP_UNEXPECTED_RESPONSE_STATUS, response);
|
||||
}
|
||||
}).then(response => {
|
||||
if (response.state !== r.state) {
|
||||
throw new ExtendedError(ERROR_HTTP_UNEXPECTED_RESPONSE_STATE, response);
|
||||
}
|
||||
|
||||
let { hello } = response;
|
||||
if (!hello) {
|
||||
hello = {
|
||||
success: response.success,
|
||||
username
|
||||
};
|
||||
}
|
||||
dispatch(receiveHello(hello));
|
||||
dispatch(receiveLogon(response));
|
||||
return Promise.resolve(response);
|
||||
}).catch(error => {
|
||||
error = handleAxiosError(error);
|
||||
const errors = {
|
||||
http: error
|
||||
};
|
||||
|
||||
dispatch(receiveValidateLogon(errors));
|
||||
return {
|
||||
success: false,
|
||||
errors: errors
|
||||
};
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function executeConsent(allow=false, scope='') {
|
||||
return function(dispatch, getState) {
|
||||
dispatch(requestConsent(allow));
|
||||
|
||||
const { query } = getState().common;
|
||||
|
||||
const r = withClientRequestState({
|
||||
allow,
|
||||
scope,
|
||||
client_id: query.client_id || '', // eslint-disable-line camelcase
|
||||
redirect_uri: query.redirect_uri || '', // eslint-disable-line camelcase
|
||||
ref: query.state || '',
|
||||
flow_nonce: query.nonce || '' // eslint-disable-line camelcase
|
||||
});
|
||||
return axios.post('./identifier/_/consent', r, {
|
||||
headers: {
|
||||
'Kopano-Konnect-XSRF': '1'
|
||||
}
|
||||
}).then(response => {
|
||||
switch (response.status) {
|
||||
case 200:
|
||||
// success.
|
||||
return response.data;
|
||||
case 204:
|
||||
// cancel reply.
|
||||
return {
|
||||
success: true,
|
||||
state: response.headers['kopano-konnect-state']
|
||||
};
|
||||
default:
|
||||
// error.
|
||||
throw new ExtendedError(ERROR_HTTP_UNEXPECTED_RESPONSE_STATUS, response);
|
||||
}
|
||||
}).then(response => {
|
||||
if (response.state !== r.state) {
|
||||
throw new ExtendedError(ERROR_HTTP_UNEXPECTED_RESPONSE_STATE, response);
|
||||
}
|
||||
|
||||
dispatch(receiveConsent(response));
|
||||
return Promise.resolve(response);
|
||||
}).catch(error => {
|
||||
error = handleAxiosError(error);
|
||||
const errors = {
|
||||
http: error
|
||||
};
|
||||
|
||||
dispatch(receiveValidateLogon(errors));
|
||||
return {
|
||||
success: false,
|
||||
errors: errors
|
||||
};
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function validateUsernamePassword(username, password, isSignedIn) {
|
||||
return function(dispatch) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const errors = {};
|
||||
|
||||
if (!username) {
|
||||
errors.username = new Error(ERROR_LOGIN_VALIDATE_MISSINGUSERNAME);
|
||||
}
|
||||
if (!password && !isSignedIn) {
|
||||
errors.password = new Error(ERROR_LOGIN_VALIDATE_MISSINGPASSWORD);
|
||||
}
|
||||
|
||||
dispatch(receiveValidateLogon(errors));
|
||||
if (Object.keys(errors).length === 0) {
|
||||
resolve(errors);
|
||||
} else {
|
||||
reject(errors);
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function executeLogonIfFormValid(username, password, isSignedIn) {
|
||||
return (dispatch) => {
|
||||
return dispatch(
|
||||
validateUsernamePassword(username, password, isSignedIn)
|
||||
).then(() => {
|
||||
const mode = isSignedIn ? ModeLogonUsernameEmptyPasswordCookie : ModeLogonUsernamePassword;
|
||||
return dispatch(executeLogon(username, password, mode));
|
||||
}).catch((errors) => {
|
||||
return {
|
||||
success: false,
|
||||
errors: errors
|
||||
};
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function advanceLogonFlow(success, history, done=false, extraQuery={}) {
|
||||
return (dispatch, getState) => {
|
||||
if (!success) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { flow, query, hello } = getState().common;
|
||||
const q = Object.assign({}, query, extraQuery);
|
||||
|
||||
switch (flow) {
|
||||
case 'oauth':
|
||||
case 'consent':
|
||||
case 'oidc':
|
||||
if (hello.details.flow !== flow) {
|
||||
// Ignore requested flow if hello flow does not match.
|
||||
break;
|
||||
}
|
||||
|
||||
if (!done && hello.details.next === 'consent') {
|
||||
history.replace(`/consent${history.location.search}${history.location.hash}`);
|
||||
return;
|
||||
}
|
||||
if (hello.details.continue_uri) {
|
||||
q.prompt = 'none';
|
||||
window.location.replace(hello.details.continue_uri + '?' + queryString.stringify(q));
|
||||
return;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
// Legacy stupid modes.
|
||||
if (q.continue && q.continue.indexOf(document.location.origin) === 0) {
|
||||
window.location.replace(q.continue);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Default action.
|
||||
let target = '/welcome';
|
||||
if (history.action === 'REPLACE') {
|
||||
target = target + history.location.search + history.location.hash;
|
||||
}
|
||||
|
||||
dispatch(receiveValidateLogon({})); // XXX(longsleep): hack to reset loading and errors.
|
||||
history.push(target);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
export const RECEIVE_ERROR = 'RECEIVE_ERROR';
|
||||
|
||||
export const RESET_HELLO = 'RESET_HELLO';
|
||||
export const EXECUTE_HELLO = 'EXECUTE_HELLO';
|
||||
export const RECEIVE_HELLO = 'RECEIVE_HELLO';
|
||||
|
||||
export const RECEIVE_VALIDATE_LOGON = 'RECEIVE_VALIDATE_LOGON';
|
||||
export const REQUEST_LOGON = 'REQUEST_LOGON';
|
||||
export const EXECUTE_LOGON = 'EXECUTE_LOGON';
|
||||
export const RECEIVE_LOGON = 'RECEIVE_LOGON';
|
||||
export const UPDATE_INPUT = 'UPDATE_INPUT';
|
||||
|
||||
export const REQUEST_CONSENT_ALLOW = 'REQUEST_CONSENT_ALLOW';
|
||||
export const REQUEST_CONSENT_CANCEL = 'REQUEST_CONSENT_CANCEL';
|
||||
export const EXECUTE_CONSENT = 'EXECUTE_CONSENT';
|
||||
export const RECEIVE_CONSENT = 'RECEIVE_CONSENT';
|
||||
|
||||
export const REQUEST_LOGOFF = 'REQUEST_LOGOFF';
|
||||
export const EXECUTE_LOGOFF = 'EXECUTE_LOGOFF';
|
||||
export const RECEIVE_LOGOFF = 'RECEIVE_LOGOFF';
|
||||
|
||||
export const SERVICE_WORKER_NEW_CONTENT = 'SERVICE_WORKER_NEW_CONTENT';
|
||||
export const SERVICE_WORKER_READY = 'SERVICE_WORKER_READY';
|
||||
export const SERVICE_WORKER_ERROR = 'SERVICE_WORKER_ERROR';
|
||||
export const SERVICE_WORKER_OFFLINE = 'SERVICE_WORKER_OFFLINE';
|
||||
@@ -0,0 +1,18 @@
|
||||
import {
|
||||
ExtendedError,
|
||||
ERROR_HTTP_NETWORK_ERROR,
|
||||
ERROR_HTTP_UNEXPECTED_RESPONSE_STATUS
|
||||
} from '../errors';
|
||||
|
||||
export function handleAxiosError(error) {
|
||||
if (error.request) {
|
||||
// Axios errors.
|
||||
if (error.response) {
|
||||
error = new ExtendedError(ERROR_HTTP_UNEXPECTED_RESPONSE_STATUS, error.response);
|
||||
} else {
|
||||
error = new ExtendedError(ERROR_HTTP_NETWORK_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
return error;
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
@font-face {
|
||||
font-family: QSfera;
|
||||
src: url('./fonts/QSfera500-Regular.woff2') format('woff2');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: QSfera;
|
||||
src: url('./fonts/QSfera750-Bold.woff2') format('woff2');
|
||||
font-weight: bold;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
html {
|
||||
font-feature-settings: "cv11";
|
||||
color: #20434f !important;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: QSfera, sans-serif;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
strong {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
#root {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.oc-font-weight-light {
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.oc-login-bg {
|
||||
background: #20434F;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.oc-login-bg-icon {
|
||||
position: fixed;
|
||||
right: -3vw;
|
||||
bottom: -3vh;
|
||||
height: 50vh;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.oc-login-bg-image {
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
#loader {
|
||||
/* NOTE(longsleep): White here needed because of the background image */
|
||||
color: white;
|
||||
text-shadow: #000 0 0 1px;
|
||||
}
|
||||
|
||||
.oc-logo {
|
||||
height: 80px;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.oc-logo-container {
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.oc-progress {
|
||||
/* Needs to be important to overwrite material-ui */
|
||||
background-color: rgba(78, 133, 200, 0.8) !important;
|
||||
height: 4px;
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
.oc-progress > div {
|
||||
/* Needs to be important to overwrite material-ui */
|
||||
background-color: #4a76ac !important;
|
||||
}
|
||||
|
||||
.oc-input {
|
||||
border: 1px solid #20434f;
|
||||
border-radius: 4px;
|
||||
height: 45px;
|
||||
width: 100%;
|
||||
padding: 16px;
|
||||
box-sizing: border-box;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.oc-label {
|
||||
display: block;
|
||||
margin-bottom: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.oc-input.error {
|
||||
outline: none;
|
||||
border: 1px solid #fe4600;
|
||||
}
|
||||
|
||||
.MuiTypography-colorError {
|
||||
color: #fe4600 !important;
|
||||
}
|
||||
|
||||
.oc-input:focus {
|
||||
outline: 2px solid #e2baff;
|
||||
border: 1px solid #fff;
|
||||
}
|
||||
|
||||
.oc-input + .oc-input {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.MuiTouchRipple-root {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.oc-card {
|
||||
background: white;
|
||||
display: inline-flex;
|
||||
width: 500px;
|
||||
}
|
||||
|
||||
.oc-card-body {
|
||||
padding: 40px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.oc-button {
|
||||
/* Needs to be important to overwrite material-ui */
|
||||
font-size: 1.0625rem !important;
|
||||
border-radius: 100px !important;
|
||||
box-shadow: none !important;
|
||||
font-weight: 700 !important;
|
||||
}
|
||||
|
||||
.oc-button-primary {
|
||||
/* Needs to be important to overwrite material-ui */
|
||||
width: 100%;
|
||||
background: #e2baff !important;
|
||||
border-radius: 100px !important;
|
||||
color: #20434f !important;
|
||||
box-shadow: none !important;
|
||||
font-weight: 700 !important;
|
||||
}
|
||||
|
||||
.oc-button-secondary {
|
||||
/* Needs to be important to overwrite material-ui */
|
||||
width: 100%;
|
||||
border-radius: 100px !important;
|
||||
background: #F1F3F4 !important;
|
||||
color: #20434f !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.oc-footer-message {
|
||||
color: white;
|
||||
padding: 10px;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 768px) {
|
||||
.oc-logo {
|
||||
height: 60px;
|
||||
top: -90px;
|
||||
}
|
||||
|
||||
.oc-login-bg-icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.oc-card {
|
||||
min-width: 50vw !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 600px) {
|
||||
.oc-card {
|
||||
width: 100vw !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Helpers */
|
||||
.oc-mt-l {
|
||||
margin-top: 24px !important;
|
||||
}
|
||||
|
||||
.oc-mb-m {
|
||||
margin-bottom: 20px !important;
|
||||
}
|
||||
|
||||
.oc-login-form div:not(:last-of-type) {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Special SR classes
|
||||
* Used to hide an element visually, but keeping it accessible for accessibility tools.
|
||||
*/
|
||||
.oc-invisible-sr {
|
||||
border: 0 !important;
|
||||
clip: rect(1px, 1px, 1px, 1px) !important;
|
||||
height: 1px !important;
|
||||
overflow: hidden !important;
|
||||
padding: 0 !important;
|
||||
/* Need to make sure we override any existing styles. */
|
||||
position: absolute !important;
|
||||
top: 0;
|
||||
white-space: nowrap;
|
||||
width: 1px !important;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const ClientDisplayName = ({ client, ...rest }) => (
|
||||
<span {...rest}>{client.display_name ? client.display_name : client.id}</span>
|
||||
);
|
||||
|
||||
ClientDisplayName.propTypes = {
|
||||
client: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
export default ClientDisplayName;
|
||||
@@ -0,0 +1,70 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { withTranslation } from 'react-i18next';
|
||||
|
||||
import LinearProgress from '@material-ui/core/LinearProgress';
|
||||
import Grid from '@material-ui/core/Grid';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import Button from '@material-ui/core/Button';
|
||||
import { retryHello } from '../actions/common';
|
||||
import { ErrorMessage } from '../errors';
|
||||
|
||||
class Loading extends React.PureComponent {
|
||||
render() {
|
||||
const { error, t } = this.props;
|
||||
|
||||
return (
|
||||
<Grid item align="center">
|
||||
{error === null && (
|
||||
<LinearProgress className="oc-progress" />
|
||||
)}
|
||||
{error !== null && (
|
||||
<div>
|
||||
<Typography variant="h5" gutterBottom align="center">
|
||||
{t("konnect.loading.error.headline", "Failed to connect to server")}
|
||||
</Typography>
|
||||
<Typography align="center" color="error">
|
||||
<ErrorMessage error={error}></ErrorMessage>
|
||||
</Typography>
|
||||
<Button
|
||||
autoFocus
|
||||
color="primary"
|
||||
variant="outlined"
|
||||
className="oc-button-primary oc-mt-l"
|
||||
onClick={(event) => this.retry(event)}
|
||||
>
|
||||
{t("konnect.login.retryButton.label", "Retry")}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
|
||||
retry(event) {
|
||||
event.preventDefault();
|
||||
|
||||
this.props.dispatch(retryHello());
|
||||
}
|
||||
}
|
||||
|
||||
Loading.propTypes = {
|
||||
classes: PropTypes.object.isRequired,
|
||||
t: PropTypes.func.isRequired,
|
||||
|
||||
error: PropTypes.object,
|
||||
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
const { error } = state.common;
|
||||
|
||||
return {
|
||||
error
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps)(withTranslation()(Loading));
|
||||
@@ -0,0 +1,76 @@
|
||||
import React, { useCallback, useMemo, useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import MenuItem from '@material-ui/core/MenuItem';
|
||||
import Select from '@material-ui/core/Select';
|
||||
|
||||
import allLocales from '../locales';
|
||||
|
||||
function LocaleSelect({ locales: localesProp, ...other } = {}) {
|
||||
const { i18n, ready } = useTranslation();
|
||||
|
||||
const handleChange = useCallback((event) => {
|
||||
i18n.changeLanguage(event.target.value);
|
||||
}, [ i18n ])
|
||||
|
||||
const locales = useMemo(() => {
|
||||
if (!localesProp) {
|
||||
return allLocales;
|
||||
}
|
||||
const supported = allLocales.filter(locale => {
|
||||
return localesProp.includes(locale.locale);
|
||||
});
|
||||
return supported;
|
||||
}, [localesProp]);
|
||||
|
||||
useEffect(() => {
|
||||
if (locales) {
|
||||
const found = locales.find((locale) => {
|
||||
return locale.locale === i18n.language;
|
||||
});
|
||||
if (found) {
|
||||
// Have language -> is supported all good.
|
||||
return;
|
||||
}
|
||||
const wanted = i18n.modules.languageDetector.detectors.navigator.lookup();
|
||||
i18n.modules.languageDetector.services.languageUtils.options.supportedLngs = locales.map(locale => locale.locale);
|
||||
i18n.modules.languageDetector.services.languageUtils.options.fallbackLng = null;
|
||||
|
||||
let best = i18n.modules.languageDetector.services.languageUtils.getBestMatchFromCodes(wanted);
|
||||
if (!best) {
|
||||
best = locales[0].locale;
|
||||
}
|
||||
|
||||
// Auto change language to best one found if the current selected one is not enabled.
|
||||
if (i18n.language !== best) {
|
||||
i18n.changeLanguage(best);
|
||||
}
|
||||
}
|
||||
}, [i18n, locales]);
|
||||
|
||||
if (!ready || !locales || locales.length < 2) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <Select
|
||||
value={i18n.language}
|
||||
onChange={handleChange}
|
||||
{...other}
|
||||
>
|
||||
{locales.map(language => {
|
||||
return <MenuItem
|
||||
key={language.locale}
|
||||
value={language.locale}>
|
||||
{language.nativeName}
|
||||
</MenuItem>;
|
||||
})}
|
||||
</Select>;
|
||||
}
|
||||
|
||||
LocaleSelect.propTypes = {
|
||||
locales: PropTypes.arrayOf(PropTypes.string),
|
||||
};
|
||||
|
||||
export default LocaleSelect;
|
||||
@@ -0,0 +1,22 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Route } from 'react-router-dom';
|
||||
|
||||
import RedirectWithQuery from './RedirectWithQuery';
|
||||
|
||||
const PrivateRoute = ({ component: Target, hello, ...rest }) => (
|
||||
<Route {...rest} render={props => (
|
||||
hello ? (
|
||||
<Target {...props}/>
|
||||
) : (
|
||||
<RedirectWithQuery target='/identifier' />
|
||||
)
|
||||
)}/>
|
||||
);
|
||||
|
||||
PrivateRoute.propTypes = {
|
||||
component: PropTypes.elementType.isRequired,
|
||||
hello: PropTypes.object
|
||||
};
|
||||
|
||||
export default PrivateRoute;
|
||||
@@ -0,0 +1,23 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { withRouter } from 'react-router';
|
||||
import { Redirect } from 'react-router-dom';
|
||||
|
||||
const RedirectWithQuery = ({target, location, ...rest}) => {
|
||||
const to = {
|
||||
pathname: target,
|
||||
search: location.search,
|
||||
hash: location.hash
|
||||
};
|
||||
|
||||
return (
|
||||
<Redirect to={to} {...rest}></Redirect>
|
||||
);
|
||||
};
|
||||
|
||||
RedirectWithQuery.propTypes = {
|
||||
target: PropTypes.string.isRequired,
|
||||
location: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
export default withRouter(RedirectWithQuery);
|
||||
@@ -0,0 +1,15 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import Dialog from '@material-ui/core/Dialog';
|
||||
import withMobileDialog from '@material-ui/core/withMobileDialog';
|
||||
|
||||
const ResponsiveDialog = (props) => {
|
||||
return <Dialog {...props}/>;
|
||||
};
|
||||
|
||||
ResponsiveDialog.propTypes = {
|
||||
fullScreen: PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
export default withMobileDialog()(ResponsiveDialog);
|
||||
@@ -0,0 +1,114 @@
|
||||
import React, { useContext } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Trans } from 'react-i18next';
|
||||
|
||||
import { withStyles } from '@material-ui/core/styles';
|
||||
import Grid from '@material-ui/core/Grid';
|
||||
import DialogContent from '@material-ui/core/DialogContent';
|
||||
|
||||
import Loading from './Loading';
|
||||
import { QsferaContext } from "../qsferaContext";
|
||||
|
||||
const styles = theme => ({
|
||||
root: {
|
||||
display: 'flex',
|
||||
flex: 1,
|
||||
zIndex: 999
|
||||
},
|
||||
content: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center'
|
||||
},
|
||||
actions: {
|
||||
marginTop: -40,
|
||||
justifyContent: 'flex-start',
|
||||
paddingLeft: theme.spacing(3),
|
||||
paddingRight: theme.spacing(3)
|
||||
},
|
||||
wrapper: {
|
||||
display: 'flex',
|
||||
flex: 1,
|
||||
alignItems: 'center'
|
||||
}
|
||||
});
|
||||
|
||||
const ResponsiveScreen = (props) => {
|
||||
const {
|
||||
classes,
|
||||
withoutLogo,
|
||||
withoutPadding,
|
||||
loading,
|
||||
children,
|
||||
className,
|
||||
branding,
|
||||
...other
|
||||
} = props;
|
||||
const { theme } = useContext(QsferaContext);
|
||||
|
||||
const logo = (theme && !withoutLogo) ? (
|
||||
<img src={'/' + theme.common?.logo} className="oc-logo" alt="КуСфера logo"/>
|
||||
) : null;
|
||||
|
||||
const content = loading ? <Loading/> : (withoutPadding ? children : <DialogContent>{children}</DialogContent>);
|
||||
|
||||
return (
|
||||
<Grid
|
||||
container
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
direction="column"
|
||||
spacing={0}
|
||||
className={[classes.root, className].filter(Boolean).join(' ')}
|
||||
{...other}
|
||||
>
|
||||
<div className={classes.wrapper}>
|
||||
<div className={classes.content}>
|
||||
{branding?.signinPageLogoURI ? (
|
||||
<a
|
||||
href={branding.signinPageLogoURI}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className='oc-logo-container'
|
||||
>
|
||||
{logo}
|
||||
</a>
|
||||
) : (
|
||||
<div className='oc-logo-container'>
|
||||
{logo}
|
||||
</div>
|
||||
)}
|
||||
<div className={"oc-card"}>
|
||||
<div className={"oc-card-body"}>{content}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<footer className="oc-footer-message">
|
||||
<Trans i18nKey="konnect.footer.slogan">
|
||||
<strong>КуСфера</strong>
|
||||
</Trans>
|
||||
</footer>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
ResponsiveScreen.defaultProps = {
|
||||
withoutLogo: false,
|
||||
withoutPadding: false,
|
||||
loading: false
|
||||
};
|
||||
|
||||
ResponsiveScreen.propTypes = {
|
||||
classes: PropTypes.object.isRequired,
|
||||
withoutLogo: PropTypes.bool,
|
||||
withoutPadding: PropTypes.bool,
|
||||
loading: PropTypes.bool,
|
||||
branding: PropTypes.object,
|
||||
children: PropTypes.node.isRequired,
|
||||
className: PropTypes.string,
|
||||
PaperProps: PropTypes.object,
|
||||
DialogProps: PropTypes.object
|
||||
};
|
||||
|
||||
export default withStyles(styles)(ResponsiveScreen);
|
||||
@@ -0,0 +1,90 @@
|
||||
import React from 'react';
|
||||
import List from '@material-ui/core/List';
|
||||
import ListItem from '@material-ui/core/ListItem';
|
||||
import ListItemText from '@material-ui/core/ListItemText';
|
||||
import { withStyles } from '@material-ui/core/styles';
|
||||
import PropTypes from 'prop-types';
|
||||
import Checkbox from '@material-ui/core/Checkbox';
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const styles = () => ({
|
||||
row: {
|
||||
paddingTop: 0,
|
||||
paddingBottom: 0
|
||||
}
|
||||
});
|
||||
|
||||
const ScopesList = ({scopes, meta, classes, ...rest}) => {
|
||||
const { mapping, definitions } = meta;
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const rows = [];
|
||||
const known = {};
|
||||
|
||||
// TODO(longsleep): Sort scopes according to priority.
|
||||
for (let scope in scopes) {
|
||||
if (!scopes[scope]) {
|
||||
continue;
|
||||
}
|
||||
let id = mapping[scope];
|
||||
if (id) {
|
||||
if (known[id]) {
|
||||
continue;
|
||||
}
|
||||
known[id] = true;
|
||||
} else {
|
||||
id = scope;
|
||||
}
|
||||
let definition = definitions[id];
|
||||
let label;
|
||||
if (definition) {
|
||||
switch (definition.id) {
|
||||
case 'scope_alias_basic':
|
||||
label = t("konnect.scopeDescription.aliasBasic", "Access your basic account information");
|
||||
break;
|
||||
case 'scope_offline_access':
|
||||
label = t("konnect.scopeDescription.offlineAccess", "Keep the allowed access persistently and forever");
|
||||
break;
|
||||
default:
|
||||
}
|
||||
if (!label) {
|
||||
label = definition.description;
|
||||
}
|
||||
}
|
||||
if (!label) {
|
||||
label = t("konnect.scopeDescription.scope", "Scope: {{scope}}", { scope });
|
||||
}
|
||||
|
||||
rows.push(
|
||||
<ListItem
|
||||
disableGutters
|
||||
dense
|
||||
key={id}
|
||||
className={classes.row}
|
||||
><Checkbox
|
||||
checked
|
||||
disableRipple
|
||||
disabled
|
||||
/>
|
||||
<ListItemText primary={label} />
|
||||
</ListItem>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<List {...rest}>
|
||||
{rows}
|
||||
</List>
|
||||
);
|
||||
};
|
||||
|
||||
ScopesList.propTypes = {
|
||||
classes: PropTypes.object.isRequired,
|
||||
|
||||
scopes: PropTypes.object.isRequired,
|
||||
meta: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
export default withStyles(styles)(ScopesList);
|
||||
@@ -0,0 +1,34 @@
|
||||
import React from 'react';
|
||||
|
||||
import {
|
||||
Fade,
|
||||
CircularProgress,
|
||||
} from '@material-ui/core';
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
|
||||
const useStyles = makeStyles(() => ({
|
||||
root: {
|
||||
position: 'fixed',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
},
|
||||
}));
|
||||
|
||||
const Spinner = () => {
|
||||
const classes = useStyles();
|
||||
|
||||
return <div className={classes.root}>
|
||||
<Fade
|
||||
in
|
||||
style={{
|
||||
transitionDelay: '800ms',
|
||||
}}
|
||||
unmountOnExit
|
||||
>
|
||||
<CircularProgress size={70} thickness={1}/>
|
||||
</Fade>
|
||||
</div>;
|
||||
}
|
||||
|
||||
export default Spinner;
|
||||
@@ -0,0 +1,28 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
|
||||
const TextInput = (props) => {
|
||||
const label = props.label;
|
||||
const extraClassName = props.extraClassName;
|
||||
|
||||
delete props.label;
|
||||
delete props.extraClassName;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<label className="oc-label"
|
||||
htmlFor={props.id}>{label}</label>
|
||||
<input className={`oc-input ${extraClassName ? extraClassName : ''}`} {...props}
|
||||
placeholder={props.placeholder ? props.placeholder : null}/>
|
||||
</div>);
|
||||
};
|
||||
|
||||
TextInput.propTypes = {
|
||||
placeholder: PropTypes.object,
|
||||
label: PropTypes.object,
|
||||
id: PropTypes.string,
|
||||
extraClassName: PropTypes.string,
|
||||
}
|
||||
|
||||
export default TextInput;
|
||||
@@ -0,0 +1,115 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { withTranslation } from 'react-i18next';
|
||||
|
||||
import { withStyles } from '@material-ui/core/styles';
|
||||
import Button from '@material-ui/core/Button';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import DialogActions from '@material-ui/core/DialogActions';
|
||||
|
||||
import ResponsiveScreen from '../../components/ResponsiveScreen';
|
||||
import { executeHello, executeLogoff } from '../../actions/common';
|
||||
|
||||
const styles = theme => ({
|
||||
subHeader: {
|
||||
marginBottom: theme.spacing(5)
|
||||
},
|
||||
wrapper: {
|
||||
marginTop: theme.spacing(5),
|
||||
position: 'relative',
|
||||
display: 'inline-block'
|
||||
}
|
||||
});
|
||||
|
||||
class Goodbyescreen extends React.PureComponent {
|
||||
componentDidMount() {
|
||||
this.props.dispatch(executeHello());
|
||||
}
|
||||
|
||||
render() {
|
||||
const { classes, branding, hello, t } = this.props;
|
||||
|
||||
const loading = hello === null;
|
||||
return (
|
||||
<ResponsiveScreen loading={loading} branding={branding}>
|
||||
{hello !== null && !hello.state && (
|
||||
<div>
|
||||
<Typography variant="h5" component="h3">
|
||||
{t("konnect.goodbye.headline", "Goodbye")}
|
||||
</Typography>
|
||||
<Typography variant="subtitle1" className={classes.subHeader}>
|
||||
{t("konnect.goodbye.subHeader", "you have been signed out from your account")}
|
||||
</Typography>
|
||||
<Typography gutterBottom>
|
||||
{t("konnect.goodbye.message.close", "You can close this window now.")}
|
||||
</Typography>
|
||||
</div>
|
||||
)}
|
||||
{hello !== null && hello.state === true && (
|
||||
<div>
|
||||
<Typography variant="h5" component="h3">
|
||||
{t("konnect.goodbye.confirm.headline", "Hello {{displayName}}", { displayName: hello.displayName })}
|
||||
</Typography>
|
||||
<Typography variant="subtitle1" className={classes.subHeader}>
|
||||
{t("konnect.goodbye.confirm.subHeader", "please confirm sign out")}
|
||||
</Typography>
|
||||
|
||||
<Typography gutterBottom>
|
||||
{t("konnect.goodbye.message.confirm", "Press the button below, to sign out from your account now.")}
|
||||
</Typography>
|
||||
|
||||
<DialogActions>
|
||||
<div className={classes.wrapper}>
|
||||
<Button
|
||||
color="secondary"
|
||||
variant="outlined"
|
||||
className={classes.button}
|
||||
onClick={(event) => this.logoff(event)}
|
||||
>
|
||||
{t("konnect.goodbye.signoutButton.label", "Sign out")}
|
||||
</Button>
|
||||
</div>
|
||||
</DialogActions>
|
||||
</div>
|
||||
)}
|
||||
</ResponsiveScreen>
|
||||
);
|
||||
}
|
||||
|
||||
logoff(event) {
|
||||
event.preventDefault();
|
||||
|
||||
this.props.dispatch(executeLogoff()).then((response) => {
|
||||
const { history } = this.props;
|
||||
|
||||
if (response.success) {
|
||||
this.props.dispatch(executeHello());
|
||||
history.push('/goodbye');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Goodbyescreen.propTypes = {
|
||||
classes: PropTypes.object.isRequired,
|
||||
t: PropTypes.func.isRequired,
|
||||
|
||||
branding: PropTypes.object,
|
||||
hello: PropTypes.object,
|
||||
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
history: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
const { branding, hello } = state.common;
|
||||
|
||||
return {
|
||||
branding,
|
||||
hello
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps)(withStyles(styles)(withTranslation()(Goodbyescreen)));
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './Goodbyescreen';
|
||||
@@ -0,0 +1,150 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { withTranslation } from 'react-i18next';
|
||||
|
||||
import { withStyles } from '@material-ui/core/styles';
|
||||
import List from '@material-ui/core/List';
|
||||
import ListItem from '@material-ui/core/ListItem';
|
||||
import ListItemText from '@material-ui/core/ListItemText';
|
||||
import ListItemAvatar from '@material-ui/core/ListItemAvatar';
|
||||
import Avatar from '@material-ui/core/Avatar';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import DialogContent from '@material-ui/core/DialogContent';
|
||||
|
||||
import { executeLogonIfFormValid, advanceLogonFlow } from '../../actions/login';
|
||||
import { ErrorMessage } from '../../errors';
|
||||
|
||||
const styles = theme => ({
|
||||
content: {
|
||||
overflowY: 'visible',
|
||||
},
|
||||
subHeader: {
|
||||
marginBottom: theme.spacing(2)
|
||||
},
|
||||
message: {
|
||||
marginTop: theme.spacing(2)
|
||||
},
|
||||
accountList: {
|
||||
marginLeft: theme.spacing(-5),
|
||||
marginRight: theme.spacing(-5)
|
||||
},
|
||||
accountListItem: {
|
||||
paddingLeft: theme.spacing(5),
|
||||
paddingRight: theme.spacing(5)
|
||||
}
|
||||
});
|
||||
|
||||
class Chooseaccount extends React.PureComponent {
|
||||
componentDidMount() {
|
||||
const { hello, history } = this.props;
|
||||
if ((!hello || !hello.state) && history.action !== 'PUSH') {
|
||||
history.replace(`/identifier${history.location.search}${history.location.hash}`);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { loading, errors, classes, hello, t } = this.props;
|
||||
|
||||
let errorMessage = null;
|
||||
if (errors.http) {
|
||||
errorMessage = <Typography color="error" className={classes.message}>
|
||||
<ErrorMessage error={errors.http}></ErrorMessage>
|
||||
</Typography>;
|
||||
}
|
||||
|
||||
let username = '';
|
||||
if (hello && hello.state) {
|
||||
username = hello.username;
|
||||
}
|
||||
|
||||
return (
|
||||
<DialogContent className={classes.content}>
|
||||
<Typography variant="h5" component="h3">
|
||||
{t("konnect.chooseaccount.headline", "Choose an account")}
|
||||
</Typography>
|
||||
<Typography variant="subtitle1" className={classes.subHeader}>
|
||||
{t("konnect.chooseaccount.subHeader", "to sign in")}
|
||||
</Typography>
|
||||
|
||||
<form action="" onSubmit={(event) => this.logon(event)}>
|
||||
<List disablePadding className={classes.accountList}>
|
||||
<ListItem
|
||||
button
|
||||
disableGutters
|
||||
className={classes.accountListItem}
|
||||
disabled={!!loading}
|
||||
onClick={(event) => this.logon(event)}
|
||||
><ListItemAvatar><Avatar>{username.substr(0, 1)}</Avatar></ListItemAvatar>
|
||||
<ListItemText primary={username} />
|
||||
</ListItem>
|
||||
<ListItem
|
||||
button
|
||||
disableGutters
|
||||
className={classes.accountListItem}
|
||||
disabled={!!loading}
|
||||
onClick={(event) => this.logoff(event)}
|
||||
>
|
||||
<ListItemAvatar>
|
||||
<Avatar>
|
||||
{t("konnect.chooseaccount.useOther.persona.label", "?")}
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary={
|
||||
t("konnect.chooseaccount.useOther.label", "Use another account")
|
||||
}
|
||||
/>
|
||||
</ListItem>
|
||||
</List>
|
||||
|
||||
{errorMessage}
|
||||
</form>
|
||||
</DialogContent>
|
||||
);
|
||||
}
|
||||
|
||||
logon(event) {
|
||||
event.preventDefault();
|
||||
|
||||
const { hello, dispatch, history } = this.props;
|
||||
dispatch(executeLogonIfFormValid(hello.username, '', true)).then((response) => {
|
||||
if (response.success) {
|
||||
dispatch(advanceLogonFlow(response.success, history));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
logoff(event) {
|
||||
event.preventDefault();
|
||||
|
||||
const { history} = this.props;
|
||||
history.push(`/identifier${history.location.search}${history.location.hash}`);
|
||||
}
|
||||
}
|
||||
|
||||
Chooseaccount.propTypes = {
|
||||
classes: PropTypes.object.isRequired,
|
||||
t: PropTypes.func.isRequired,
|
||||
|
||||
loading: PropTypes.string.isRequired,
|
||||
errors: PropTypes.object.isRequired,
|
||||
hello: PropTypes.object,
|
||||
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
history: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
const { loading, errors } = state.login;
|
||||
const { hello } = state.common;
|
||||
|
||||
return {
|
||||
loading,
|
||||
errors,
|
||||
hello
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps)(withStyles(styles)(withTranslation()(Chooseaccount)));
|
||||
@@ -0,0 +1,202 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {withTranslation, Trans} from 'react-i18next';
|
||||
|
||||
import {withStyles} from '@material-ui/core/styles';
|
||||
import Button from '@material-ui/core/Button';
|
||||
import BaseTooltip from '@material-ui/core/Tooltip';
|
||||
import CircularProgress from '@material-ui/core/CircularProgress';
|
||||
import green from '@material-ui/core/colors/green';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import DialogActions from '@material-ui/core/DialogActions';
|
||||
import DialogContent from '@material-ui/core/DialogContent';
|
||||
|
||||
import {executeConsent, advanceLogonFlow, receiveValidateLogon} from '../../actions/login';
|
||||
import {ErrorMessage} from '../../errors';
|
||||
import {REQUEST_CONSENT_ALLOW} from '../../actions/types';
|
||||
import ClientDisplayName from '../../components/ClientDisplayName';
|
||||
import ScopesList from '../../components/ScopesList';
|
||||
|
||||
const styles = theme => ({
|
||||
button: {
|
||||
margin: theme.spacing(1),
|
||||
minWidth: 100
|
||||
},
|
||||
buttonProgress: {
|
||||
color: green[500],
|
||||
position: 'absolute',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
marginTop: -12,
|
||||
marginLeft: -12
|
||||
},
|
||||
header: {
|
||||
margin: 0,
|
||||
},
|
||||
subHeader: {
|
||||
marginBottom: theme.spacing(2)
|
||||
},
|
||||
scopesList: {
|
||||
marginBottom: theme.spacing(2)
|
||||
},
|
||||
wrapper: {
|
||||
marginTop: theme.spacing(2),
|
||||
position: 'relative',
|
||||
display: 'inline-block'
|
||||
},
|
||||
dialogActions: {
|
||||
gap: theme.spacing(1)
|
||||
},
|
||||
message: {
|
||||
marginTop: theme.spacing(2),
|
||||
marginBottom: theme.spacing(2)
|
||||
}
|
||||
});
|
||||
|
||||
const Tooltip = ({children, ...other} = {}) => {
|
||||
// Ensures that there is only a single child for the tooltip element to
|
||||
// make it compatible with the Trans component.
|
||||
return <BaseTooltip {...other}><span>{children}</span></BaseTooltip>;
|
||||
}
|
||||
|
||||
Tooltip.propTypes = {
|
||||
children: PropTypes.node,
|
||||
};
|
||||
|
||||
class Consent extends React.PureComponent {
|
||||
componentDidMount() {
|
||||
const {dispatch, hello, history, client} = this.props;
|
||||
if ((!hello || !hello.state || !client) && history.action !== 'PUSH') {
|
||||
history.replace(`/identifier${history.location.search}${history.location.hash}`);
|
||||
}
|
||||
|
||||
dispatch(receiveValidateLogon({})); // XXX(longsleep): hack to reset loading and errors.
|
||||
}
|
||||
|
||||
action = (allow = false, scopes = {}) => (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
if (allow === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Convert all scopes which are true to a scope value.
|
||||
const scope = Object.keys(scopes).filter(scope => {
|
||||
return !!scopes[scope];
|
||||
}).join(' ');
|
||||
|
||||
const {dispatch, history} = this.props;
|
||||
dispatch(executeConsent(allow, scope)).then((response) => {
|
||||
if (response.success) {
|
||||
dispatch(advanceLogonFlow(response.success, history, true, {konnect: response.state}));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const {classes, loading, hello, errors, client, t} = this.props;
|
||||
|
||||
const scopes = hello.details.scopes || {};
|
||||
const meta = hello.details.meta || {};
|
||||
|
||||
return (
|
||||
<DialogContent>
|
||||
<h1 className={classes.header}>
|
||||
{t("konnect.consent.headline", "Hi {{displayName}}", {displayName: hello.displayName})}
|
||||
</h1>
|
||||
<Typography variant="subtitle1" className={classes.subHeader + " oc-mb-m"}>
|
||||
{hello.username}
|
||||
</Typography>
|
||||
|
||||
<Typography variant="subtitle1" gutterBottom>
|
||||
<Trans t={t} i18nKey="konnect.consent.message">
|
||||
<Tooltip
|
||||
placement="bottom"
|
||||
title={t("konnect.consent.tooltip.client", 'Clicking "Allow" will redirect you to: {{redirectURI}}', {redirectURI: client.redirect_uri})}
|
||||
>
|
||||
<span className={'oc-font-weight-light'}><ClientDisplayName client={client}/></span>
|
||||
</Tooltip> wants to
|
||||
</Trans>
|
||||
</Typography>
|
||||
<ScopesList dense disablePadding className={classes.scopesList} scopes={scopes}
|
||||
meta={meta.scopes}></ScopesList>
|
||||
|
||||
<Typography variant="subtitle1" gutterBottom>
|
||||
<Trans t={t} i18nKey="konnect.consent.question">
|
||||
Allow <span className={'oc-font-weight-light'}><ClientDisplayName client={client}/></span> to do
|
||||
this?
|
||||
</Trans>
|
||||
</Typography>
|
||||
<Typography>
|
||||
{t("konnect.consent.consequence", "By clicking Allow, you allow this app to use your information.")}
|
||||
</Typography>
|
||||
|
||||
<form action="" onSubmit={this.action(undefined, scopes)}>
|
||||
<DialogActions className={classes.dialogActions}>
|
||||
<div className={classes.wrapper}>
|
||||
<Button
|
||||
color="secondary"
|
||||
className={classes.button + ' oc-button-secondary'}
|
||||
disabled={!!loading}
|
||||
onClick={this.action(false, scopes)}
|
||||
>
|
||||
{t("konnect.consent.cancelButton.label", "Cancel")}
|
||||
</Button>
|
||||
{(loading && loading !== REQUEST_CONSENT_ALLOW) &&
|
||||
<CircularProgress size={24} className={classes.buttonProgress}/>}
|
||||
</div>
|
||||
<div className={classes.wrapper}>
|
||||
<Button
|
||||
type="submit"
|
||||
color="primary"
|
||||
variant="contained"
|
||||
className="oc-button-primary"
|
||||
disabled={!!loading}
|
||||
onClick={this.action(true, scopes)}
|
||||
>
|
||||
{t("konnect.consent.allowButton.label", "Allow")}
|
||||
</Button>
|
||||
{loading === REQUEST_CONSENT_ALLOW &&
|
||||
<CircularProgress size={24} className={classes.buttonProgress}/>}
|
||||
</div>
|
||||
</DialogActions>
|
||||
|
||||
{errors.http && (
|
||||
<Typography variant="subtitle2" color="error" className={classes.message}>
|
||||
<ErrorMessage error={errors.http}></ErrorMessage>
|
||||
</Typography>
|
||||
)}
|
||||
</form>
|
||||
</DialogContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Consent.propTypes = {
|
||||
classes: PropTypes.object.isRequired,
|
||||
t: PropTypes.func.isRequired,
|
||||
|
||||
loading: PropTypes.string.isRequired,
|
||||
errors: PropTypes.object.isRequired,
|
||||
hello: PropTypes.object,
|
||||
client: PropTypes.object.isRequired,
|
||||
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
history: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
const {hello} = state.common;
|
||||
const {loading, errors} = state.login;
|
||||
|
||||
return {
|
||||
loading: loading,
|
||||
errors,
|
||||
hello,
|
||||
client: hello.details.client || {}
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps)(withStyles(styles)(withTranslation()(Consent)));
|
||||
@@ -0,0 +1,238 @@
|
||||
import React, { useEffect, useMemo } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import { connect } from "react-redux";
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { withStyles } from "@material-ui/core/styles";
|
||||
import Button from "@material-ui/core/Button";
|
||||
import CircularProgress from "@material-ui/core/CircularProgress";
|
||||
import green from "@material-ui/core/colors/green";
|
||||
import Typography from "@material-ui/core/Typography";
|
||||
import Link from "@material-ui/core/Link";
|
||||
|
||||
import TextInput from "../../components/TextInput";
|
||||
|
||||
import {
|
||||
updateInput,
|
||||
executeLogonIfFormValid,
|
||||
advanceLogonFlow,
|
||||
} from "../../actions/login";
|
||||
import { ErrorMessage } from "../../errors";
|
||||
|
||||
const styles = (theme) => ({
|
||||
buttonProgress: {
|
||||
color: green[500],
|
||||
position: "absolute",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
marginTop: -12,
|
||||
marginLeft: -12,
|
||||
},
|
||||
main: {
|
||||
width: "100%",
|
||||
},
|
||||
header: {
|
||||
textAlign: "center",
|
||||
marginTop: 0,
|
||||
marginBottom: theme.spacing(6),
|
||||
},
|
||||
wrapper: {
|
||||
position: "relative",
|
||||
width: "100%",
|
||||
textAlign: "center",
|
||||
},
|
||||
message: {
|
||||
marginTop: 5,
|
||||
marginBottom: 5,
|
||||
},
|
||||
signinPageText: {
|
||||
marginBottom: 12
|
||||
}
|
||||
});
|
||||
|
||||
function Login(props) {
|
||||
const {
|
||||
hello,
|
||||
query,
|
||||
dispatch,
|
||||
history,
|
||||
loading,
|
||||
errors,
|
||||
classes,
|
||||
username,
|
||||
password,
|
||||
passwordResetLink,
|
||||
branding,
|
||||
} = props;
|
||||
|
||||
const { t } = useTranslation();
|
||||
const loginFailed = errors.http;
|
||||
const hasError = errors.http || errors.username || errors.password;
|
||||
const errorMessage = errors.http ? (
|
||||
<ErrorMessage error={errors.http}></ErrorMessage>
|
||||
) : errors.username ? (
|
||||
<ErrorMessage error={errors.username}></ErrorMessage>
|
||||
) : (
|
||||
<ErrorMessage error={errors.password}></ErrorMessage>
|
||||
);
|
||||
const extraPropsUsername = {
|
||||
"aria-invalid": errors.username || errors.http ? "true" : "false",
|
||||
};
|
||||
const extraPropsPassword = {
|
||||
"aria-invalid": errors.password || errors.http ? "true" : "false",
|
||||
};
|
||||
|
||||
if (errors.username || errors.http) {
|
||||
extraPropsUsername["extraClassName"] = "error";
|
||||
extraPropsUsername["aria-describedby"] = "oc-login-error-message";
|
||||
}
|
||||
|
||||
if (errors.password || errors.http) {
|
||||
extraPropsPassword["extraClassName"] = "error";
|
||||
extraPropsPassword["aria-describedby"] = "oc-login-error-message";
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (hello && hello.state && history.action !== "PUSH") {
|
||||
if (!query.prompt || query.prompt.indexOf("select_account") === -1) {
|
||||
dispatch(advanceLogonFlow(true, history));
|
||||
return;
|
||||
}
|
||||
|
||||
history.replace(
|
||||
`/chooseaccount${history.location.search}${history.location.hash}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
const handleChange = (name) => (event) => {
|
||||
dispatch(updateInput(name, event.target.value));
|
||||
};
|
||||
|
||||
const handleNextClick = (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
dispatch(executeLogonIfFormValid(username, password, false)).then(
|
||||
(response) => {
|
||||
if (response.success) {
|
||||
dispatch(advanceLogonFlow(response.success, history));
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={classes.main}>
|
||||
<h1 className={classes.header}>
|
||||
{" "}
|
||||
{t("konnect.login.headline", "Sign in")}
|
||||
</h1>
|
||||
{branding?.signinPageText && (
|
||||
<Typography
|
||||
className={classes.signinPageText}
|
||||
variant="body2"
|
||||
dangerouslySetInnerHTML={{ __html: branding.signinPageText }}
|
||||
/>
|
||||
)}
|
||||
<form
|
||||
action=""
|
||||
className="oc-login-form"
|
||||
onSubmit={(event) => handleNextClick(event)}
|
||||
>
|
||||
<TextInput
|
||||
autoFocus
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
value={username}
|
||||
onChange={handleChange("username")}
|
||||
autoComplete="username"
|
||||
placeholder={t("konnect.login.usernameField.label", "Username")}
|
||||
label={t("konnect.login.usernameField.label", "Username")}
|
||||
id="oc-login-username"
|
||||
{...extraPropsUsername}
|
||||
/>
|
||||
<TextInput
|
||||
type="password"
|
||||
margin="normal"
|
||||
onChange={handleChange("password")}
|
||||
autoComplete="current-password"
|
||||
placeholder={t("konnect.login.passwordField.label", "Password")}
|
||||
label={t("konnect.login.passwordField.label", "Password")}
|
||||
id="oc-login-password"
|
||||
{...extraPropsPassword}
|
||||
/>
|
||||
{hasError && (
|
||||
<Typography
|
||||
id="oc-login-error-message"
|
||||
variant="subtitle2"
|
||||
component="span"
|
||||
color="error"
|
||||
className={classes.message}
|
||||
>
|
||||
{errorMessage}
|
||||
</Typography>
|
||||
)}
|
||||
<div className={classes.wrapper}>
|
||||
{loginFailed && passwordResetLink && (
|
||||
<Link
|
||||
id="oc-login-password-reset"
|
||||
href={passwordResetLink}
|
||||
variant="subtitle2"
|
||||
>
|
||||
{"Reset password?"}
|
||||
</Link>
|
||||
)}
|
||||
<Button
|
||||
type="submit"
|
||||
color="primary"
|
||||
variant="contained"
|
||||
className="oc-button-primary oc-mt-l"
|
||||
disabled={!!loading}
|
||||
onClick={handleNextClick}
|
||||
>
|
||||
{t("konnect.login.nextButton.label", "Log in")}
|
||||
</Button>
|
||||
{loading && (
|
||||
<CircularProgress size={24} className={classes.buttonProgress} />
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Login.propTypes = {
|
||||
classes: PropTypes.object.isRequired,
|
||||
|
||||
loading: PropTypes.string.isRequired,
|
||||
username: PropTypes.string.isRequired,
|
||||
password: PropTypes.string.isRequired,
|
||||
passwordResetLink: PropTypes.string.isRequired,
|
||||
errors: PropTypes.object.isRequired,
|
||||
branding: PropTypes.object,
|
||||
hello: PropTypes.object,
|
||||
query: PropTypes.object.isRequired,
|
||||
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
history: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
const { loading, username, password, errors } = state.login;
|
||||
const { branding, hello, query, passwordResetLink } = state.common;
|
||||
|
||||
return {
|
||||
loading,
|
||||
username,
|
||||
password,
|
||||
errors,
|
||||
branding,
|
||||
hello,
|
||||
query,
|
||||
passwordResetLink,
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps)(withStyles(styles)(Login));
|
||||
@@ -0,0 +1,60 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { Route, Switch } from 'react-router-dom';
|
||||
|
||||
import { withStyles } from '@material-ui/core/styles';
|
||||
|
||||
import ResponsiveScreen from '../../components/ResponsiveScreen';
|
||||
import RedirectWithQuery from '../../components/RedirectWithQuery';
|
||||
import { executeHello } from '../../actions/common';
|
||||
|
||||
import Login from './Login';
|
||||
import Chooseaccount from './Chooseaccount';
|
||||
import Consent from './Consent';
|
||||
|
||||
const styles = () => ({
|
||||
});
|
||||
|
||||
class Loginscreen extends React.PureComponent {
|
||||
componentDidMount() {
|
||||
this.props.dispatch(executeHello());
|
||||
}
|
||||
|
||||
render() {
|
||||
const { branding, hello } = this.props;
|
||||
|
||||
const loading = hello === null;
|
||||
return (
|
||||
<ResponsiveScreen loading={loading} branding={branding} withoutPadding>
|
||||
<Switch>
|
||||
<Route path="/identifier" exact component={Login}></Route>
|
||||
<Route path="/chooseaccount" exact component={Chooseaccount}></Route>
|
||||
<Route path="/consent" exact component={Consent}></Route>
|
||||
<RedirectWithQuery target="/identifier"/>
|
||||
</Switch>
|
||||
</ResponsiveScreen>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Loginscreen.propTypes = {
|
||||
classes: PropTypes.object.isRequired,
|
||||
|
||||
branding: PropTypes.object,
|
||||
hello: PropTypes.object,
|
||||
|
||||
dispatch: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
const { branding, hello } = state.common;
|
||||
|
||||
return {
|
||||
branding,
|
||||
hello
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps)(withStyles(styles)(Loginscreen));
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './Loginscreen';
|
||||
@@ -0,0 +1,90 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { withTranslation } from 'react-i18next';
|
||||
|
||||
import { withStyles } from '@material-ui/core/styles';
|
||||
import Button from '@material-ui/core/Button';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import DialogActions from '@material-ui/core/DialogActions';
|
||||
|
||||
import ResponsiveScreen from '../../components/ResponsiveScreen';
|
||||
import { executeLogoff } from '../../actions/common';
|
||||
|
||||
const styles = theme => ({
|
||||
button: {
|
||||
margin: theme.spacing(1),
|
||||
minWidth: 100
|
||||
},
|
||||
subHeader: {
|
||||
marginBottom: theme.spacing(5)
|
||||
}
|
||||
});
|
||||
|
||||
class Welcomescreen extends React.PureComponent {
|
||||
render() {
|
||||
const { classes, branding, hello, t } = this.props;
|
||||
|
||||
const loading = hello === null;
|
||||
return (
|
||||
<ResponsiveScreen loading={loading} branding={branding}>
|
||||
<Typography variant="h5" component="h3">
|
||||
{t("konnect.welcome.headline", "Welcome {{displayName}}", {displayName: hello.displayName})}
|
||||
</Typography>
|
||||
<Typography variant="subtitle1" className={classes.subHeader}>
|
||||
{hello.username}
|
||||
</Typography>
|
||||
|
||||
<Typography gutterBottom>
|
||||
{t("konnect.welcome.message", "You are signed in - awesome!")}
|
||||
</Typography>
|
||||
|
||||
<DialogActions>
|
||||
<Button
|
||||
color="secondary"
|
||||
className={classes.button}
|
||||
variant="contained"
|
||||
onClick={(event) => this.logoff(event)}
|
||||
>
|
||||
{t("konnect.welcome.signoutButton.label", "Sign out")}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</ResponsiveScreen>
|
||||
);
|
||||
}
|
||||
|
||||
logoff(event) {
|
||||
event.preventDefault();
|
||||
|
||||
this.props.dispatch(executeLogoff()).then((response) => {
|
||||
const { history } = this.props;
|
||||
|
||||
if (response.success) {
|
||||
history.push('/identifier');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Welcomescreen.propTypes = {
|
||||
classes: PropTypes.object.isRequired,
|
||||
t: PropTypes.func.isRequired,
|
||||
|
||||
branding: PropTypes.object,
|
||||
hello: PropTypes.object,
|
||||
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
history: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
const { branding, hello } = state.common;
|
||||
|
||||
return {
|
||||
branding,
|
||||
hello
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps)(withStyles(styles)(withTranslation()(Welcomescreen)));
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './Welcomescreen';
|
||||
@@ -0,0 +1,67 @@
|
||||
import { withTranslation } from 'react-i18next';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export const ERROR_LOGIN_VALIDATE_MISSINGUSERNAME = 'konnect.error.login.validate.missingUsername';
|
||||
export const ERROR_LOGIN_VALIDATE_MISSINGPASSWORD = 'konnect.error.login.validate.missingPassword';
|
||||
export const ERROR_LOGIN_FAILED = 'konnect.error.login.failed';
|
||||
export const ERROR_HTTP_NETWORK_ERROR = 'konnect.error.http.networkError';
|
||||
export const ERROR_HTTP_UNEXPECTED_RESPONSE_STATUS = 'konnect.error.http.unexpectedResponseStatus';
|
||||
export const ERROR_HTTP_UNEXPECTED_RESPONSE_STATE = 'konnect.error.http.unexpectedResponseState';
|
||||
|
||||
// Error with values.
|
||||
export class ExtendedError extends Error {
|
||||
values = undefined;
|
||||
|
||||
constructor(message, values) {
|
||||
super(message);
|
||||
if (Error.captureStackTrace !== undefined) {
|
||||
Error.captureStackTrace(this, ExtendedError);
|
||||
}
|
||||
this.values = values;
|
||||
}
|
||||
}
|
||||
|
||||
// Component to translate error text with values.
|
||||
function ErrorMessageComponent(props) {
|
||||
const { error, t, values } = props;
|
||||
|
||||
if (!error) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const id = error.id ? error.id : error.message;
|
||||
const messageDescriptor = Object.assign({}, {
|
||||
id,
|
||||
defaultMessage: error.id ? error.message : undefined,
|
||||
values: {
|
||||
...error.values,
|
||||
...values,
|
||||
},
|
||||
});
|
||||
|
||||
switch (messageDescriptor.id) {
|
||||
case ERROR_LOGIN_VALIDATE_MISSINGUSERNAME:
|
||||
return t("konnect.error.login.validate.missingUsername", "Enter a valid value.", messageDescriptor.values);
|
||||
case ERROR_LOGIN_VALIDATE_MISSINGPASSWORD:
|
||||
return t("konnect.error.login.validate.missingPassword", "Enter your password.");
|
||||
case ERROR_LOGIN_FAILED:
|
||||
return t("konnect.error.login.failed", "Logon failed. Please verify your credentials and try again.");
|
||||
case ERROR_HTTP_NETWORK_ERROR:
|
||||
return t("konnect.error.http.networkError", "Network error. Please check your connection and try again.");
|
||||
case ERROR_HTTP_UNEXPECTED_RESPONSE_STATUS:
|
||||
return t("konnect.error.http.unexpectedResponseStatus", "Unexpected HTTP response: {{status}}. Please check your connection and try again.", messageDescriptor.values);
|
||||
case ERROR_HTTP_UNEXPECTED_RESPONSE_STATE:
|
||||
return t("konnect.error.http.unexpectedResponseState", "Unexpected response state: {{state}}", messageDescriptor.values);
|
||||
default:
|
||||
}
|
||||
|
||||
const f = t;
|
||||
return f(messageDescriptor.defaultMessage, messageDescriptor.values);
|
||||
}
|
||||
|
||||
ErrorMessageComponent.propTypes = {
|
||||
error: PropTypes.object,
|
||||
t: PropTypes.func,
|
||||
values: PropTypes.any,
|
||||
};
|
||||
export const ErrorMessage = withTranslation()(ErrorMessageComponent);
|
||||
@@ -0,0 +1,17 @@
|
||||
/* not so fancy background */
|
||||
|
||||
#bg {
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
#bg > div {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
background-image: url(./images/background.png);
|
||||
z-index: 0;
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,80 @@
|
||||
import i18n from 'i18next';
|
||||
import { initReactI18next } from 'react-i18next';
|
||||
|
||||
import resourcesToBackend from 'i18next-resources-to-backend';
|
||||
import LanguageDetector, { CustomDetector } from 'i18next-browser-languagedetector';
|
||||
|
||||
import queryString from 'query-string';
|
||||
|
||||
import locales from './locales';
|
||||
|
||||
const config = {
|
||||
uiLocalesQueryName: 'ui_locales', // Same as OIDC uses.
|
||||
uiLocaleCookieName: 'ui_locale', // For domain wide syncing, not set here.
|
||||
uiLocaleLocalStorageName: 'lico.identifier_ui_locale', // Sufficiently unique, set here.
|
||||
}
|
||||
|
||||
const supportedLanguages = ['en-GB', 'en', ...locales.map((locale) => {
|
||||
return locale.locale;
|
||||
})];
|
||||
|
||||
const queryUiLocalesDetector: CustomDetector = {
|
||||
name: 'queryUiLocales',
|
||||
lookup: (options): string | string[] | undefined => {
|
||||
const query = queryString.parse(document.location.search);
|
||||
const ui_locales = query[config.uiLocalesQueryName];
|
||||
if (!ui_locales) {
|
||||
return;
|
||||
}
|
||||
if (Array.isArray(ui_locales)) {
|
||||
return ui_locales as string[];
|
||||
} else {
|
||||
return ui_locales.split(' ');
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
const languageDetector = new LanguageDetector();
|
||||
languageDetector.addDetector(queryUiLocalesDetector);
|
||||
|
||||
i18n
|
||||
.use(resourcesToBackend((language, namespace, callback) => {
|
||||
import(
|
||||
/* webpackMode: "lazy-once" */
|
||||
/* webpackInclude: /\.json$/ */
|
||||
/* webpackChunkName: "all-i18n-data" */
|
||||
/* webpackPrefetch: true */
|
||||
`./locales/${language}/${namespace}.json`
|
||||
)
|
||||
.then((resources) => {
|
||||
callback(null, resources)
|
||||
})
|
||||
.catch((error) => {
|
||||
callback(error, null)
|
||||
})
|
||||
}))
|
||||
.use(languageDetector)
|
||||
.use(initReactI18next)
|
||||
// init i18next
|
||||
// for all options read: https://www.i18next.com/overview/configuration-options
|
||||
.init({
|
||||
fallbackLng: 'en-GB',
|
||||
supportedLngs: [...supportedLanguages],
|
||||
cleanCode: true,
|
||||
returnEmptyString: false,
|
||||
debug: false,
|
||||
|
||||
detection: {
|
||||
/* https://github.com/i18next/i18next-browser-languageDetector */
|
||||
order: [queryUiLocalesDetector.name, 'cookie', 'localStorage', 'navigator'],
|
||||
lookupCookie: config.uiLocaleCookieName,
|
||||
lookupLocalStorage: config.uiLocaleLocalStorageName,
|
||||
caches: ['localStorage'],
|
||||
},
|
||||
|
||||
interpolation: {
|
||||
escapeValue: false,
|
||||
},
|
||||
});
|
||||
|
||||
export default i18n;
|
||||
@@ -0,0 +1,2 @@
|
||||
app-icon.svg
|
||||
app-icon-*.png
|
||||
@@ -0,0 +1,42 @@
|
||||
# Tools
|
||||
|
||||
CONVERT ?= convert
|
||||
IDENTIFY ?= identify
|
||||
BASE64 ?= base64
|
||||
ENVSUBST ?= envsubst
|
||||
SCOUR ?= scour
|
||||
INKSCAPE ?= inkscape
|
||||
|
||||
# Variables
|
||||
|
||||
STATIC ?= ../../public/static
|
||||
ICON ?= lico-icon.svg
|
||||
|
||||
# Build
|
||||
|
||||
all: app-icon.svg
|
||||
|
||||
.PHONY: icons
|
||||
icons: $(STATIC)/favicon.ico
|
||||
|
||||
.PHONY: $(STATIC)/favicon.ico
|
||||
$(STATIC)/favicon.ico: app-icon-rounded-256x256.png
|
||||
$(CONVERT) -background transparent $< -define icon:auto-resize=16,32,48,64,128,256 $@
|
||||
|
||||
app-icon.svg: $(ICON)
|
||||
cp -vaf $< $@
|
||||
|
||||
app-icon-whitebox-256x256.png: app-icon.svg
|
||||
$(INKSCAPE) -z -e $@.tmp -w 204.8 -h 204.8 -b white -y 1.0 $<
|
||||
$(CONVERT) $@.tmp -background white -gravity center -extent 256x256 $@
|
||||
@$(RM) $@.tmp
|
||||
|
||||
app-icon-rounded-256x256.png: app-icon-whitebox-256x256.png
|
||||
$(CONVERT) -size 256x256 xc:none -draw "roundrectangle 2,2,252,252,126,126" $@.tmp.png
|
||||
$(CONVERT) $< -matte $@.tmp.png -compose DstIn -composite $@
|
||||
@$(RM) $@.tmp.png
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
$(RM) app-icon-*.png || true
|
||||
$(RM) app-icon.svg || true
|
||||
@@ -0,0 +1,12 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 180 180" role="img" aria-label="КуСфера">
|
||||
<path fill="#061D2B" d="M34,12h112C158,12 168,22 168,34v112C168,158 158,168 146,168H34C22,168 12,158 12,146V34C12,22 22,12 34,12z"/>
|
||||
<path fill="#123E55" fill-opacity="0.74" d="M12,45C48,19 87,20 121,48C145,68 158,68 168,60V34C168,22 158,12 146,12H34C22,12 12,22 12,34z"/>
|
||||
<path fill="#19B8C9" fill-opacity="0.18" d="M12,126C46,105 82,104 114,124C139,139 156,139 168,128V146C168,158 158,168 146,168H34C22,168 12,158 12,146z"/>
|
||||
<path fill="none" stroke="#20D6E6" stroke-linecap="round" stroke-width="10" d="M75,47C100,29 138,39 153,70C167,100 151,135 121,147"/>
|
||||
<path fill="none" stroke="#9AF8FF" stroke-linecap="round" stroke-width="4" d="M81,45C101,35 130,39 146,62"/>
|
||||
<path fill="#21D1D6" d="M48,52h25v50h-25z"/>
|
||||
<path fill="#F8FCFF" d="M48,102h25v42h-25z"/>
|
||||
<path fill="#18BFD0" d="M77,94L111,55H143L99,103z"/>
|
||||
<path fill="#F8FCFF" d="M77,104L100,82L148,144H116z"/>
|
||||
<path fill="#9DF8FF" fill-opacity="0.45" d="M48,52h25v8h-25z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,12 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 180 180" role="img" aria-label="КуСфера">
|
||||
<path fill="#061D2B" d="M34,12h112C158,12 168,22 168,34v112C168,158 158,168 146,168H34C22,168 12,158 12,146V34C12,22 22,12 34,12z"/>
|
||||
<path fill="#123E55" fill-opacity="0.74" d="M12,45C48,19 87,20 121,48C145,68 158,68 168,60V34C168,22 158,12 146,12H34C22,12 12,22 12,34z"/>
|
||||
<path fill="#19B8C9" fill-opacity="0.18" d="M12,126C46,105 82,104 114,124C139,139 156,139 168,128V146C168,158 158,168 146,168H34C22,168 12,158 12,146z"/>
|
||||
<path fill="none" stroke="#20D6E6" stroke-linecap="round" stroke-width="10" d="M75,47C100,29 138,39 153,70C167,100 151,135 121,147"/>
|
||||
<path fill="none" stroke="#9AF8FF" stroke-linecap="round" stroke-width="4" d="M81,45C101,35 130,39 146,62"/>
|
||||
<path fill="#21D1D6" d="M48,52h25v50h-25z"/>
|
||||
<path fill="#F8FCFF" d="M48,102h25v42h-25z"/>
|
||||
<path fill="#18BFD0" d="M77,94L111,55H143L99,103z"/>
|
||||
<path fill="#F8FCFF" d="M77,104L100,82L148,144H116z"/>
|
||||
<path fill="#9DF8FF" fill-opacity="0.45" d="M48,52h25v8h-25z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
@@ -0,0 +1,12 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 180 180" role="img" aria-label="КуСфера">
|
||||
<path fill="#061D2B" d="M34,12h112C158,12 168,22 168,34v112C168,158 158,168 146,168H34C22,168 12,158 12,146V34C12,22 22,12 34,12z"/>
|
||||
<path fill="#123E55" fill-opacity="0.74" d="M12,45C48,19 87,20 121,48C145,68 158,68 168,60V34C168,22 158,12 146,12H34C22,12 12,22 12,34z"/>
|
||||
<path fill="#19B8C9" fill-opacity="0.18" d="M12,126C46,105 82,104 114,124C139,139 156,139 168,128V146C168,158 158,168 146,168H34C22,168 12,158 12,146z"/>
|
||||
<path fill="none" stroke="#20D6E6" stroke-linecap="round" stroke-width="10" d="M75,47C100,29 138,39 153,70C167,100 151,135 121,147"/>
|
||||
<path fill="none" stroke="#9AF8FF" stroke-linecap="round" stroke-width="4" d="M81,45C101,35 130,39 146,62"/>
|
||||
<path fill="#21D1D6" d="M48,52h25v50h-25z"/>
|
||||
<path fill="#F8FCFF" d="M48,102h25v42h-25z"/>
|
||||
<path fill="#18BFD0" d="M77,94L111,55H143L99,103z"/>
|
||||
<path fill="#F8FCFF" d="M77,104L100,82L148,144H116z"/>
|
||||
<path fill="#9DF8FF" fill-opacity="0.45" d="M48,52h25v8h-25z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
@@ -0,0 +1,24 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { Provider } from 'react-redux';
|
||||
|
||||
import './i18n';
|
||||
|
||||
import App from './App';
|
||||
import store from './store';
|
||||
|
||||
import './app.css';
|
||||
|
||||
const root = document.getElementById('root')
|
||||
|
||||
// if a custom background image has been configured, make use of it
|
||||
const bgImg = root.getAttribute('data-bg-img')
|
||||
|
||||
ReactDOM.render(
|
||||
<React.StrictMode>
|
||||
<Provider store={store as any}>
|
||||
<App bgImg={bgImg}/>
|
||||
</Provider>
|
||||
</React.StrictMode>,
|
||||
root
|
||||
);
|
||||
@@ -0,0 +1,95 @@
|
||||
{
|
||||
"konnect": {
|
||||
"consent": {
|
||||
"message": "<0><0><0></0></0></0> möchte",
|
||||
"allowButton": {
|
||||
"label": "Einverstanden"
|
||||
},
|
||||
"question": "<1><0></0></1> den Zugriff gestatten?",
|
||||
"consequence": "Wenn Sie Einverstanden klicken, erhält die App Zugriff auf Ihre Informationen.",
|
||||
"cancelButton": {
|
||||
"label": "Abbrechen"
|
||||
},
|
||||
"tooltip": {
|
||||
"client": "Wenn Sie \"Einverstanden\" klicken werden Sie zu {{redirectURI}} weitergeleitet"
|
||||
},
|
||||
"headline": "Hallo {{displayName}}"
|
||||
},
|
||||
"chooseaccount": {
|
||||
"useOther": {
|
||||
"persona": {
|
||||
"label": "?"
|
||||
},
|
||||
"label": "Anderes Konto"
|
||||
},
|
||||
"headline": "Konto auswählen",
|
||||
"subHeader": "das angemeldet werden soll"
|
||||
},
|
||||
"scopeDescription": {
|
||||
"aliasBasic": "Zugriff auf Ihre grundlegenden Benutzerinformationen",
|
||||
"offlineAccess": "Dauerhaften Zugriff (läuft nicht ab)",
|
||||
"scope": "Geltungsbereich: {{scope}}"
|
||||
},
|
||||
"login": {
|
||||
"usernameField": {
|
||||
"label": "Benutzername",
|
||||
"placeholder": {
|
||||
"email": "E-Mail",
|
||||
"identity": "Identität",
|
||||
"username": "Benutzername"
|
||||
}
|
||||
},
|
||||
"nextButton": {
|
||||
"label": "Weiter"
|
||||
},
|
||||
"passwordField": {
|
||||
"label": "Passwort"
|
||||
},
|
||||
"retryButton": {
|
||||
"label": "Wiederholen"
|
||||
},
|
||||
"headline": "Anmelden"
|
||||
},
|
||||
"error": {
|
||||
"login": {
|
||||
"validate": {
|
||||
"missingUsername": "Geben Sie einen gültigen Wert ein.",
|
||||
"missingPassword": "Geben Sie ein Passwort ein."
|
||||
},
|
||||
"failed": "Anmeldung fehlgeschlagen. Bitte überprüfen Sie Ihre Eingabe und versuchen Sie es noch einmal."
|
||||
},
|
||||
"http": {
|
||||
"networkError": "Netzwerkfehler. Bitte prüfen Sie Ihre Verbindung und versuchen Sie es noch einmal.",
|
||||
"unexpectedResponseStatus": "Unerwartete HTTP-Antwort: {{status}}. Bitte prüfen Sie Ihre Verbindung und versuchen Sie es noch einmal.",
|
||||
"unexpectedResponseState": "Unerwarteter Antwort-Status: {{state}}"
|
||||
}
|
||||
},
|
||||
"loading": {
|
||||
"error": {
|
||||
"headline": "Verbindung zum Server fehlgeschlagen"
|
||||
}
|
||||
},
|
||||
"goodbye": {
|
||||
"headline": "Bis bald",
|
||||
"confirm": {
|
||||
"headline": "Hallo {{displayName}}",
|
||||
"subHeader": "Bitte bestätigen Sie, dass Sie sich abmelden möchten"
|
||||
},
|
||||
"message": {
|
||||
"confirm": "Klicken Sie auf die Schaltfläche unten um sich aus Ihrem Konto abzumelden.",
|
||||
"close": "Sie können dieses Fenster jetzt schließen."
|
||||
},
|
||||
"signoutButton": {
|
||||
"label": "Abmelden"
|
||||
},
|
||||
"subHeader": "sie sind jetzt abgemeldet"
|
||||
},
|
||||
"welcome": {
|
||||
"signoutButton": {
|
||||
"label": "Abmelden"
|
||||
},
|
||||
"headline": "Willkommen {{displayName}}",
|
||||
"message": "Sie sind angemeldet - super!"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"konnect": {
|
||||
"consent": {
|
||||
"allowButton": {},
|
||||
"cancelButton": {},
|
||||
"tooltip": {}
|
||||
},
|
||||
"chooseaccount": {
|
||||
"useOther": {
|
||||
"persona": {}
|
||||
}
|
||||
},
|
||||
"scopeDescription": {},
|
||||
"login": {
|
||||
"usernameField": {
|
||||
"placeholder": {}
|
||||
},
|
||||
"nextButton": {},
|
||||
"passwordField": {},
|
||||
"retryButton": {}
|
||||
},
|
||||
"error": {
|
||||
"login": {
|
||||
"validate": {}
|
||||
},
|
||||
"http": {}
|
||||
},
|
||||
"loading": {
|
||||
"error": {}
|
||||
},
|
||||
"goodbye": {
|
||||
"confirm": {},
|
||||
"message": {},
|
||||
"signoutButton": {}
|
||||
},
|
||||
"welcome": {
|
||||
"signoutButton": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
{
|
||||
"konnect": {
|
||||
"consent": {
|
||||
"message": "<0><0><0></0></0></0> souhaite",
|
||||
"allowButton": {
|
||||
"label": "Autoriser"
|
||||
},
|
||||
"question": "Autoriser <1><0></0></1> à faire cela?",
|
||||
"consequence": "En cliquant, vous autoriser l'app à accéder à vos informations.",
|
||||
"cancelButton": {
|
||||
"label": "Annuler"
|
||||
},
|
||||
"tooltip": {
|
||||
"client": "En cliquant \"Autoriser\" vous serez redirigé vers: {{redirectURI}}"
|
||||
},
|
||||
"headline": "Bonjour {{displayName}}"
|
||||
},
|
||||
"chooseaccount": {
|
||||
"useOther": {
|
||||
"persona": {
|
||||
"label": "?"
|
||||
},
|
||||
"label": "Utiliser un autre compte"
|
||||
},
|
||||
"headline": "Choisir un compte",
|
||||
"subHeader": "identification"
|
||||
},
|
||||
"scopeDescription": {
|
||||
"aliasBasic": "Consulter les informations de base de votre compte",
|
||||
"offlineAccess": "Conserver les autorisations d'accès à l'avenir",
|
||||
"scope": "Portée : {{scope}}"
|
||||
},
|
||||
"login": {
|
||||
"usernameField": {
|
||||
"placeholder": {
|
||||
"username": "Utilisateur"
|
||||
}
|
||||
},
|
||||
"nextButton": {
|
||||
"label": "Suivant"
|
||||
},
|
||||
"passwordField": {
|
||||
"label": "Mot de passe"
|
||||
},
|
||||
"retryButton": {
|
||||
"label": "Réessayer"
|
||||
},
|
||||
"headline": "Identification"
|
||||
},
|
||||
"error": {
|
||||
"login": {
|
||||
"validate": {
|
||||
"missingPassword": "Saisir un mot de passe."
|
||||
},
|
||||
"failed": "Echec de connexion. Vérifier vos identifiants et essayer à nouveau."
|
||||
},
|
||||
"http": {
|
||||
"networkError": "Erreur réseau. Vérifier votre connexion, et réessayer.",
|
||||
"unexpectedResponseStatus": "Erreur HTTP inattendue : {{status}}. Vérifier votre connexion et réessayer.",
|
||||
"unexpectedResponseState": "Erreur d'état inattendue : {{state}}"
|
||||
}
|
||||
},
|
||||
"loading": {
|
||||
"error": {
|
||||
"headline": "La connexion au serveur a échoué"
|
||||
}
|
||||
},
|
||||
"goodbye": {
|
||||
"headline": "Au revoir",
|
||||
"confirm": {
|
||||
"headline": "Bonjour {{displayName}}",
|
||||
"subHeader": "confirmer votre déconnexion"
|
||||
},
|
||||
"message": {
|
||||
"confirm": "Cliquer le bouton ci-dessous, pour quitter.",
|
||||
"close": "Vous pouvez fermer cette fenêtre à présent."
|
||||
},
|
||||
"signoutButton": {
|
||||
"label": "Quitter"
|
||||
},
|
||||
"subHeader": "vous avez été déconnecté"
|
||||
},
|
||||
"welcome": {
|
||||
"signoutButton": {
|
||||
"label": "Quitter"
|
||||
},
|
||||
"headline": "Bienvenue {{displayName}}",
|
||||
"message": "Magnifique - Vous êtes connecté!"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import allLocales from './locales.json';
|
||||
|
||||
interface Locale {
|
||||
locale: string,
|
||||
name: string,
|
||||
nativeName: string,
|
||||
}
|
||||
|
||||
function enableLocales(locales: Locale[]): Locale[] {
|
||||
return locales;
|
||||
}
|
||||
|
||||
export const locales = enableLocales(allLocales);
|
||||
|
||||
export default locales;
|
||||
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"konnect": {
|
||||
"consent": {
|
||||
"allowButton": {},
|
||||
"cancelButton": {},
|
||||
"tooltip": {}
|
||||
},
|
||||
"chooseaccount": {
|
||||
"useOther": {
|
||||
"persona": {}
|
||||
}
|
||||
},
|
||||
"scopeDescription": {},
|
||||
"login": {
|
||||
"usernameField": {
|
||||
"placeholder": {}
|
||||
},
|
||||
"nextButton": {},
|
||||
"passwordField": {},
|
||||
"retryButton": {}
|
||||
},
|
||||
"error": {
|
||||
"login": {
|
||||
"validate": {}
|
||||
},
|
||||
"http": {}
|
||||
},
|
||||
"loading": {
|
||||
"error": {}
|
||||
},
|
||||
"goodbye": {
|
||||
"confirm": {},
|
||||
"message": {},
|
||||
"signoutButton": {}
|
||||
},
|
||||
"welcome": {
|
||||
"signoutButton": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
[
|
||||
{
|
||||
"locale": "de",
|
||||
"name": "German",
|
||||
"nativeName": "Deutsch",
|
||||
"orientation": {
|
||||
"characterOrder": "left-to-right",
|
||||
"lineOrder": "top-to-bottom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"locale": "en-GB",
|
||||
"name": "English",
|
||||
"nativeName": "English",
|
||||
"orientation": {
|
||||
"characterOrder": "left-to-right",
|
||||
"lineOrder": "top-to-bottom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"locale": "es",
|
||||
"name": "Spanish",
|
||||
"nativeName": "Español",
|
||||
"orientation": {
|
||||
"characterOrder": "left-to-right",
|
||||
"lineOrder": "top-to-bottom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"locale": "fr",
|
||||
"name": "French",
|
||||
"nativeName": "Français",
|
||||
"orientation": {
|
||||
"characterOrder": "left-to-right",
|
||||
"lineOrder": "top-to-bottom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"locale": "it",
|
||||
"name": "Italian",
|
||||
"nativeName": "Italiano",
|
||||
"orientation": {
|
||||
"characterOrder": "left-to-right",
|
||||
"lineOrder": "top-to-bottom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"locale": "nl",
|
||||
"name": "Dutch",
|
||||
"nativeName": "Nederlands",
|
||||
"orientation": {
|
||||
"characterOrder": "left-to-right",
|
||||
"lineOrder": "top-to-bottom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"locale": "zh-CN",
|
||||
"name": "Simplified Chinese",
|
||||
"nativeName": "简体中文",
|
||||
"orientation": {
|
||||
"characterOrder": "left-to-right",
|
||||
"lineOrder": "top-to-bottom"
|
||||
},
|
||||
"ietf": "zh_hans"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,91 @@
|
||||
{
|
||||
"konnect": {
|
||||
"consent": {
|
||||
"message": "<0><0><0></0></0></0> wil",
|
||||
"allowButton": {
|
||||
"label": "Toestaan"
|
||||
},
|
||||
"question": "<1><0></0></1> toestaan dit te doen?",
|
||||
"consequence": "Door op Toestaan te klikken, krijgt deze app toestemming je informatie te gebruiken.",
|
||||
"cancelButton": {
|
||||
"label": "Annuleren"
|
||||
},
|
||||
"tooltip": {
|
||||
"client": "Door op \"Toestaan\" te klikken word je doorverwezen naar: {{redirectURI}}"
|
||||
},
|
||||
"headline": "Hoi {{displayName}}"
|
||||
},
|
||||
"chooseaccount": {
|
||||
"useOther": {
|
||||
"persona": {
|
||||
"label": "?"
|
||||
},
|
||||
"label": "Gebruik een ander account"
|
||||
},
|
||||
"headline": "Account kiezen",
|
||||
"subHeader": "aanmelden"
|
||||
},
|
||||
"scopeDescription": {
|
||||
"aliasBasic": "Basis accountgegevens weergeven",
|
||||
"offlineAccess": "De toestemming voor altijd onthouden",
|
||||
"scope": "Scope: {{scope}}"
|
||||
},
|
||||
"login": {
|
||||
"usernameField": {
|
||||
"placeholder": {
|
||||
"username": "Gebruikersnaam"
|
||||
}
|
||||
},
|
||||
"nextButton": {
|
||||
"label": "Volgende"
|
||||
},
|
||||
"passwordField": {
|
||||
"label": "Wachtwoord"
|
||||
},
|
||||
"retryButton": {
|
||||
"label": "Opnieuw"
|
||||
},
|
||||
"headline": "Aanmelden"
|
||||
},
|
||||
"error": {
|
||||
"login": {
|
||||
"validate": {
|
||||
"missingPassword": "Voer een wachtwoord in."
|
||||
},
|
||||
"failed": "Inloggen mislukt. Controleer logingegevens en probeer opnieuw."
|
||||
},
|
||||
"http": {
|
||||
"networkError": "Netwerk probleem. Controleer je verbinding en probeer opnieuw.",
|
||||
"unexpectedResponseStatus": "Onverwachte HTTP respons: {{status}}. Controleer je verbinding en probeer opnieuw.",
|
||||
"unexpectedResponseState": "Onverwachte respons status: {{state}}"
|
||||
}
|
||||
},
|
||||
"loading": {
|
||||
"error": {
|
||||
"headline": "Kon niet met server verbinden"
|
||||
}
|
||||
},
|
||||
"goodbye": {
|
||||
"headline": "Tot ziens",
|
||||
"confirm": {
|
||||
"headline": "Hallo {{displayName}}",
|
||||
"subHeader": "bevestig afmelden"
|
||||
},
|
||||
"message": {
|
||||
"confirm": "Klik op onderstaande knop om af te melden van je account.",
|
||||
"close": "Dit venster kan nu worden gesloten."
|
||||
},
|
||||
"signoutButton": {
|
||||
"label": "Afmelden"
|
||||
},
|
||||
"subHeader": "je bent afgemeld van je account"
|
||||
},
|
||||
"welcome": {
|
||||
"signoutButton": {
|
||||
"label": "Afmelden"
|
||||
},
|
||||
"headline": "Welkom {{displayName}}",
|
||||
"message": "Je bent aangemeld - fantastisch!"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"konnect": {
|
||||
"consent": {
|
||||
"allowButton": {},
|
||||
"cancelButton": {},
|
||||
"tooltip": {}
|
||||
},
|
||||
"chooseaccount": {
|
||||
"useOther": {
|
||||
"persona": {}
|
||||
}
|
||||
},
|
||||
"scopeDescription": {},
|
||||
"login": {
|
||||
"usernameField": {
|
||||
"placeholder": {}
|
||||
},
|
||||
"nextButton": {},
|
||||
"passwordField": {},
|
||||
"retryButton": {}
|
||||
},
|
||||
"error": {
|
||||
"login": {
|
||||
"validate": {}
|
||||
},
|
||||
"http": {}
|
||||
},
|
||||
"loading": {
|
||||
"error": {}
|
||||
},
|
||||
"goodbye": {
|
||||
"confirm": {},
|
||||
"message": {},
|
||||
"signoutButton": {}
|
||||
},
|
||||
"welcome": {
|
||||
"signoutButton": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
export function newHelloRequest(flow, query) {
|
||||
const r = {};
|
||||
|
||||
if (query.prompt) {
|
||||
// TODO(longsleep): Validate prompt values?
|
||||
r.prompt = query.prompt;
|
||||
}
|
||||
|
||||
let selectedFlow = flow;
|
||||
switch (flow) {
|
||||
case 'oauth':
|
||||
case 'consent':
|
||||
case 'oidc':
|
||||
r.scope = query.scope || '';
|
||||
r.client_id = query.client_id || ''; // eslint-disable-line camelcase
|
||||
r.redirect_uri = query.redirect_uri || ''; // eslint-disable-line camelcase
|
||||
if (query.id_token_hint) {
|
||||
r.id_token_hint = query.id_token_hint; // eslint-disable-line camelcase
|
||||
}
|
||||
if (query.max_age) {
|
||||
r.max_age = query.max_age; // eslint-disable-line camelcase
|
||||
}
|
||||
if (query.claims_scope) {
|
||||
// Add additional scopes from claims request if given.
|
||||
r.scope += ' ' + query.claims_scope;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
selectedFlow = null;
|
||||
}
|
||||
|
||||
if (selectedFlow) {
|
||||
r.flow = selectedFlow;
|
||||
}
|
||||
|
||||
return r;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { createContext } from 'react';
|
||||
|
||||
export const QsferaContext = createContext({
|
||||
theme: null,
|
||||
config: null,
|
||||
});
|
||||
+1
@@ -0,0 +1 @@
|
||||
/// <reference types="react-scripts" />
|
||||
@@ -0,0 +1,78 @@
|
||||
import {
|
||||
RECEIVE_ERROR,
|
||||
RESET_HELLO,
|
||||
RECEIVE_HELLO,
|
||||
SERVICE_WORKER_NEW_CONTENT
|
||||
} from '../actions/types';
|
||||
import queryString from 'query-string';
|
||||
|
||||
const query = queryString.parse(document.location.search);
|
||||
const flow = query.flow || '';
|
||||
delete query.flow;
|
||||
|
||||
const defaultPathPrefix = (() => {
|
||||
const root = document.getElementById('root');
|
||||
let pathPrefix = root ? root.getAttribute('data-path-prefix') : null;
|
||||
if (!pathPrefix || pathPrefix === '__PATH_PREFIX__') {
|
||||
// Not replaced, probably we are running in debug mode or whatever. Use sane default.
|
||||
pathPrefix = '/signin/v1';
|
||||
}
|
||||
return pathPrefix;
|
||||
})();
|
||||
|
||||
const defaultPasswordResetLink = (() => {
|
||||
const root = document.getElementById('root');
|
||||
let link = root ? root.getAttribute('passwort-reset-link') : null;
|
||||
if (!link || link === '__PASSWORD_RESET_LINK__') {
|
||||
// Not replaced, probably we are running in debug mode or whatever. Use sane default.
|
||||
link = '';
|
||||
}
|
||||
return link;
|
||||
})();
|
||||
|
||||
const defaultState = {
|
||||
hello: null,
|
||||
branding: null,
|
||||
error: null,
|
||||
flow: flow,
|
||||
query: query,
|
||||
updateAvailable: false,
|
||||
pathPrefix: defaultPathPrefix,
|
||||
passwordResetLink: defaultPasswordResetLink
|
||||
};
|
||||
|
||||
function commonReducer(state = defaultState, action) {
|
||||
switch (action.type) {
|
||||
case RECEIVE_ERROR:
|
||||
return Object.assign({}, state, {
|
||||
error: action.error
|
||||
});
|
||||
|
||||
case RESET_HELLO:
|
||||
return Object.assign({}, state, {
|
||||
hello: null,
|
||||
branding: null
|
||||
});
|
||||
|
||||
case RECEIVE_HELLO:
|
||||
return Object.assign({}, state, {
|
||||
hello: {
|
||||
state: action.state,
|
||||
username: action.username,
|
||||
displayName: action.displayName,
|
||||
details: action.hello
|
||||
},
|
||||
branding: action.hello.branding ? action.hello.branding : state.branding
|
||||
});
|
||||
|
||||
case SERVICE_WORKER_NEW_CONTENT:
|
||||
return Object.assign({}, state, {
|
||||
updateAvailable: true
|
||||
});
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
export default commonReducer;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user