Compare commits
No commits in common. "410a09aec6760f1e8a01680e48424559babfa2ec" and "4cc7f0bbbb492fde059cf777b77511db5df0411c" have entirely different histories.
410a09aec6
...
4cc7f0bbbb
|
@ -67,9 +67,3 @@ If you want to add new pages, there are some things to be aware of.
|
|||
If the browser language is supported, it will be loaded automatically. Otherwise, English is loaded by default.
|
||||
|
||||
To add new languages, the corresponding file must be added under `static/login/resources/locales`. Then the language must be added to `SUPPORT_LOCALES` in `functions/i18n.ts`.
|
||||
|
||||
### Credits
|
||||
|
||||
Special thanks to the following projects, without which the development of keycloak-modern-login would not be possible:
|
||||
|
||||
- [Keycloakify](https://github.com/keycloakify/keycloakify), from which the [`ftl_object_to_js_code_declaring_an_object`](src/static/login/baselayout.ftl) function and [Keycloak context type definitions](src/types/context.ts) are derived.
|
|
@ -45,6 +45,7 @@
|
|||
"webpack-cli": "^4.9.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"moment": "^2.29.4",
|
||||
"rfc4648": "^1.5.2",
|
||||
"vue": "^3.2.26",
|
||||
"vue-i18n": "9"
|
||||
|
|
|
@ -1,39 +1,39 @@
|
|||
import { nextTick } from "vue";
|
||||
import { createI18n } from "vue-i18n";
|
||||
import { nextTick } from "vue"
|
||||
import { createI18n } from "vue-i18n"
|
||||
import { KcContextBase } from "~/types/context";
|
||||
|
||||
export const SUPPORT_LOCALES = ["en", "de"];
|
||||
export const SUPPORT_LOCALES = ["en", "de"]
|
||||
|
||||
export function setupI18n(options = {}) {
|
||||
const browserLocale = navigator.language.split("-")[0];
|
||||
let defaultLocale = "en";
|
||||
if (SUPPORT_LOCALES.includes(browserLocale)) {
|
||||
defaultLocale = browserLocale;
|
||||
let browserLocale = navigator.language.split("-")[0];
|
||||
let defaultLocale = "en";
|
||||
if (SUPPORT_LOCALES.includes(browserLocale)) {
|
||||
defaultLocale = browserLocale;
|
||||
}
|
||||
|
||||
const i18n = createI18n(options)
|
||||
setI18nLanguage(i18n, defaultLocale)
|
||||
return i18n
|
||||
}
|
||||
|
||||
const i18n = createI18n(options);
|
||||
setI18nLanguage(i18n, defaultLocale);
|
||||
return i18n;
|
||||
}
|
||||
|
||||
export function setI18nLanguage(i18n, locale) {
|
||||
loadLocaleMessages(i18n, locale);
|
||||
if (i18n.mode === "legacy") {
|
||||
i18n.global.locale = locale;
|
||||
} else {
|
||||
i18n.global.locale.value = locale;
|
||||
|
||||
export function setI18nLanguage(i18n, locale) {
|
||||
loadLocaleMessages(i18n, locale);
|
||||
if (i18n.mode === 'legacy') {
|
||||
i18n.global.locale = locale
|
||||
} else {
|
||||
i18n.global.locale.value = locale
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function loadLocaleMessages(i18n, locale) {
|
||||
// load locale messages with dynamic import
|
||||
const context = (window as any).kcContext as KcContextBase.Common;
|
||||
const messages = await import(
|
||||
/* webpackIgnore: true */ `${context.url.resourcesPath}/locales/${locale}.js`
|
||||
);
|
||||
|
||||
// set locale and locale message
|
||||
i18n.global.setLocaleMessage(locale, messages.content);
|
||||
|
||||
return nextTick();
|
||||
}
|
||||
|
||||
export async function loadLocaleMessages(i18n, locale) {
|
||||
// load locale messages with dynamic import
|
||||
let context = (window as any).kcContext as KcContextBase.Common;
|
||||
const messages = await import(
|
||||
/* webpackIgnore: true */`${context.url.resourcesPath}/locales/${locale}.js`
|
||||
)
|
||||
|
||||
// set locale and locale message
|
||||
i18n.global.setLocaleMessage(locale, messages.content)
|
||||
|
||||
return nextTick()
|
||||
}
|
|
@ -1,26 +1,3 @@
|
|||
<#--
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020 GitHub user u/garronej
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
-->
|
||||
<#function are_same_path path searchedPath>
|
||||
<#if path?size != searchedPath?size>
|
||||
<#return false>
|
||||
|
@ -92,12 +69,9 @@ SOFTWARE.
|
|||
key == "identityProviderBrokerCtx" &&
|
||||
are_same_path(path, []) &&
|
||||
["login-idp-link-confirm.ftl", "login-idp-link-email.ftl" ]?seq_contains(pageId)
|
||||
) || (
|
||||
) || (
|
||||
["masterAdminClient", "delegateForUpdate", "defaultRole"]?seq_contains(key) &&
|
||||
are_same_path(path, ["realm"])
|
||||
) || (
|
||||
["password"]?seq_contains(key) &&
|
||||
are_same_path(path, ["login"])
|
||||
)
|
||||
>
|
||||
<#continue>
|
||||
|
|
|
@ -1,40 +1,37 @@
|
|||
export const content = {
|
||||
login: {
|
||||
title: "Mit Ihrem Konto anmelden",
|
||||
noAccount: "Sie haben noch kein Konto?",
|
||||
signupLink: "Jetzt registrieren.",
|
||||
username: "Benutzername",
|
||||
usernameOrEmail: "Benutzername oder E-Mail-Adresse",
|
||||
password: "Passwort",
|
||||
forgotPassword: "Passwort vergessen?",
|
||||
rememberMe: "Angemeldet bleiben",
|
||||
login: "Anmelden",
|
||||
rety: "Erneut versuchen",
|
||||
welcome: "Willkommen!",
|
||||
},
|
||||
"2fa": {
|
||||
selectFactor: "Bitte wählen Sie einen zweiten Faktor aus:",
|
||||
hwSecKey: "Hardware Sicherheits-Schlüssel",
|
||||
hwSecKeyDesc: "Verwenden Sie ein WebAuthn kompatibles Gerät",
|
||||
otp: "Authenticator App",
|
||||
otpDesc: "Verwenden Sie einen Einmalcode aus ihrem Authenticator",
|
||||
recoveryCode: "Wiederherstellungsschlüssel",
|
||||
recoveryCodeDesc: "Verwenden Sie einen ihrer Wiederherstellungsschlüssel",
|
||||
tryAnotherWay: "Eine andere Option nutzen.",
|
||||
},
|
||||
webauthn: {
|
||||
title: "Bitte verwenden Sie eines ihrer Geräte, um fortzufahren:",
|
||||
registered: "Registriert: {date}",
|
||||
authenticate: "Fortfahren",
|
||||
noDevice: "Kein Gerät zur Hand?",
|
||||
noSupport:
|
||||
"Es scheint, als würde ihr Browser WebAuthn nicht unterstützen. Bitte nutzen Sie einen anderen Browser für die Anmeldung.",
|
||||
noSupportOtherMethod:
|
||||
"Es scheint, als würde ihr Browser WebAuthn nicht unterstützen. Bitte nutzen Sie einen anderen Browser für die Anmeldung, oder wählen Sie eine andere Anmeldeoption.",
|
||||
error:
|
||||
"Bei der Anmeldung mit ihrem Gerät ist etwas schief gelaufen. Bitte versuchen Sie es erneut, oder wählen Sie eine andere Anmeldeoption.",
|
||||
},
|
||||
redirect: {
|
||||
message: "Sie werden weitergeleitet...",
|
||||
},
|
||||
};
|
||||
"login": {
|
||||
"title": "Mit Ihrem Konto anmelden",
|
||||
"noAccount": "Sie haben noch kein Konto?",
|
||||
"signupLink": "Jetzt registrieren.",
|
||||
"username": "Benutzername",
|
||||
"usernameOrEmail": "Benutzername oder E-Mail-Adresse",
|
||||
"password": "Passwort",
|
||||
"forgotPassword": "Passwort vergessen?",
|
||||
"rememberMe": "Angemeldet bleiben",
|
||||
"login": "Anmelden",
|
||||
"rety": "Erneut versuchen",
|
||||
"welcome": "Willkommen!"
|
||||
},
|
||||
"2fa": {
|
||||
"selectFactor": "Bitte wählen Sie einen zweiten Faktor aus:",
|
||||
"hwSecKey": "Hardware Sicherheits-Schlüssel",
|
||||
"hwSecKeyDesc": "Verwenden Sie ein WebAuthn kompatibles Gerät",
|
||||
"otp": "Authenticator App",
|
||||
"otpDesc": "Verwenden Sie einen Einmalcode aus ihrem Authenticator",
|
||||
"recoveryCode": "Wiederherstellungsschlüssel",
|
||||
"recoveryCodeDesc": "Verwenden Sie einen ihrer Wiederherstellungsschlüssel",
|
||||
"tryAnotherWay": "Eine andere Option nutzen."
|
||||
},
|
||||
"webauthn": {
|
||||
"title": "Bitte verwenden Sie eines ihrer Geräte, um fortzufahren:",
|
||||
"registered": "Registriert: {date}",
|
||||
"authenticate": "Fortfahren",
|
||||
"noDevice": "Kein Gerät zur Hand?",
|
||||
"noSupport": "Es scheint, als würde ihr Browser WebAuthn nicht unterstützen. Bitte nutzen Sie einen anderen Browser für die Anmeldung.",
|
||||
"noSupportOtherMethod": "Es scheint, als würde ihr Browser WebAuthn nicht unterstützen. Bitte nutzen Sie einen anderen Browser für die Anmeldung, oder wählen Sie eine andere Anmeldeoption.",
|
||||
"error": "Bei der Anmeldung mit ihrem Gerät ist etwas schief gelaufen. Bitte versuchen Sie es erneut, oder wählen Sie eine andere Anmeldeoption."
|
||||
},
|
||||
"redirect": {
|
||||
"message": "Sie werden weitergeleitet..."
|
||||
}
|
||||
}
|
|
@ -1,40 +1,37 @@
|
|||
export const content = {
|
||||
login: {
|
||||
title: "Login to your account",
|
||||
noAccount: "Don't have an account yet?",
|
||||
signupLink: "Sign up.",
|
||||
username: "Username",
|
||||
usernameOrEmail: "Email or Username",
|
||||
password: "Password",
|
||||
forgotPassword: "Forgot Password?",
|
||||
rememberMe: "Remember me",
|
||||
login: "Login",
|
||||
rety: "Retry",
|
||||
welcome: "Welcome!",
|
||||
},
|
||||
"2fa": {
|
||||
selectFactor: "Please select a second factor you would like to use:",
|
||||
hwSecKey: "Hardware Security Key",
|
||||
hwSecKeyDesc: "Authenticate using a WebAuthn capable device",
|
||||
otp: "Authenticator App",
|
||||
otpDesc: "Authenticate using a one time code from your authenticator app",
|
||||
recoveryCode: "Recovery Code",
|
||||
recoveryCodeDesc: "Authenticate using one of your recovery codes",
|
||||
tryAnotherWay: "Try another method.",
|
||||
},
|
||||
webauthn: {
|
||||
title: "Please use one of your registered devices to continue:",
|
||||
registered: "Registered: {date}",
|
||||
authenticate: "Continue",
|
||||
noDevice: "Don't have your device at hand?",
|
||||
noSupport:
|
||||
"It seems that your browser doesn't support WebAuthn. Please try logging in with a different browser.",
|
||||
noSupportOtherMethod:
|
||||
"It seems that your browser doesn't support WebAuthn. Please try logging in with a different browser, or try a different login method using the button below.",
|
||||
error:
|
||||
"Something went wrong during authentication using your device. Please try again, or use a different login method.",
|
||||
},
|
||||
redirect: {
|
||||
message: "Redirecting...",
|
||||
},
|
||||
};
|
||||
"login": {
|
||||
"title": "Login to your account",
|
||||
"noAccount": "Don't have an account yet?",
|
||||
"signupLink": "Sign up.",
|
||||
"username": "Username",
|
||||
"usernameOrEmail": "Email or Username",
|
||||
"password": "Password",
|
||||
"forgotPassword": "Forgot Password?",
|
||||
"rememberMe": "Remember me",
|
||||
"login": "Login",
|
||||
"rety": "Retry",
|
||||
"welcome": "Welcome!"
|
||||
},
|
||||
"2fa": {
|
||||
"selectFactor": "Please select a second factor you would like to use:",
|
||||
"hwSecKey": "Hardware Security Key",
|
||||
"hwSecKeyDesc": "Authenticate using a WebAuthn capable device",
|
||||
"otp": "Authenticator App",
|
||||
"otpDesc": "Authenticate using a one time code from your authenticator app",
|
||||
"recoveryCode": "Recovery Code",
|
||||
"recoveryCodeDesc": "Authenticate using one of your recovery codes",
|
||||
"tryAnotherWay": "Try another method."
|
||||
},
|
||||
"webauthn": {
|
||||
"title": "Please use one of your registered devices to continue:",
|
||||
"registered": "Registered: {date}",
|
||||
"authenticate": "Continue",
|
||||
"noDevice": "Don't have your device at hand?",
|
||||
"noSupport": "It seems that your browser doesn't support WebAuthn. Please try logging in with a different browser.",
|
||||
"noSupportOtherMethod": "It seems that your browser doesn't support WebAuthn. Please try logging in with a different browser, or try a different login method using the button below.",
|
||||
"error": "Something went wrong during authentication using your device. Please try again, or use a different login method."
|
||||
},
|
||||
"redirect": {
|
||||
"message": "Redirecting..."
|
||||
}
|
||||
}
|
|
@ -1,29 +1,4 @@
|
|||
/* eslint-disable */
|
||||
|
||||
/**
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020 GitHub user u/garronej
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
/** Take theses type definition with a grain of salt.
|
||||
* Some values might be undefined on some pages.
|
||||
* (ex: url.loginAction is undefined on error.ftl)
|
||||
|
|
|
@ -4,6 +4,6 @@ import { setupI18n } from "~/functions/i18n";
|
|||
|
||||
if ((window as any).kcContext) {
|
||||
const app = createApp(index);
|
||||
app.use(setupI18n());
|
||||
app.use(setupI18n())
|
||||
app.mount("#app");
|
||||
}
|
||||
|
|
|
@ -22,14 +22,11 @@
|
|||
method="post">
|
||||
<div>
|
||||
<label for="username" class="block text-sm text-gray-800">{{
|
||||
context.realm.loginWithEmailAllowed
|
||||
? $t("login.usernameOrEmail")
|
||||
: $t("login.username")
|
||||
context.realm.loginWithEmailAllowed ? $t("login.usernameOrEmail") : $t("login.username")
|
||||
}}</label>
|
||||
<input
|
||||
name="username"
|
||||
type="text"
|
||||
ref="focus"
|
||||
:placeholder="
|
||||
context.realm.loginWithEmailAllowed
|
||||
? 'jane.doe@example.com'
|
||||
|
@ -40,9 +37,9 @@
|
|||
</div>
|
||||
<div class="mt-4">
|
||||
<div>
|
||||
<label for="password" class="block text-sm text-gray-800">{{
|
||||
$t("login.password")
|
||||
}}</label>
|
||||
<label for="password" class="block text-sm text-gray-800"
|
||||
>{{ $t("login.password") }}</label
|
||||
>
|
||||
<input
|
||||
type="password"
|
||||
name="password"
|
||||
|
@ -63,9 +60,9 @@
|
|||
type="checkbox"
|
||||
name="rememberMe"
|
||||
class="w-4 h-4 text-blue-500 bg-gray-100 border-gray-400 rounded dark:ring-offset-gray-800 dark:bg-gray-700 dark:border-gray-600" />
|
||||
<label for="rememberMe" class="block ml-3 text-sm text-gray-800">{{
|
||||
$t("login.rememberMe")
|
||||
}}</label>
|
||||
<label for="rememberMe" class="block ml-3 text-sm text-gray-800"
|
||||
>{{ $t("login.rememberMe") }}</label
|
||||
>
|
||||
</div>
|
||||
<input
|
||||
type="hidden"
|
||||
|
@ -81,7 +78,7 @@
|
|||
</layout>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, onMounted, nextTick } from "vue";
|
||||
import { defineComponent } from "vue";
|
||||
import Layout from "~/components/Layout.vue";
|
||||
import ErrorBox from "~/components/ErrorBox.vue";
|
||||
import type { KcContextBase } from "~/types/context";
|
||||
|
@ -97,19 +94,6 @@ export default defineComponent({
|
|||
context: (window as any).kcContext as KcContextBase.Login,
|
||||
};
|
||||
},
|
||||
setup() {
|
||||
const focus = ref(null);
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
focus.value.focus();
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
focus,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<style>
|
||||
|
|
|
@ -4,6 +4,6 @@ import { setupI18n } from "~/functions/i18n";
|
|||
|
||||
if ((window as any).kcContext) {
|
||||
const app = createApp(index);
|
||||
app.use(setupI18n());
|
||||
app.use(setupI18n())
|
||||
app.mount("#app");
|
||||
}
|
||||
|
|
|
@ -12,9 +12,7 @@
|
|||
stroke-dasharray="164 56" />
|
||||
</svg>
|
||||
</div>
|
||||
<h1 class="mt-4 text-xl text-center text-gray-700">
|
||||
{{ $t("redirect.message") }}
|
||||
</h1>
|
||||
<h1 class="mt-4 text-xl text-center text-gray-700">{{ $t("redirect.message") }}</h1>
|
||||
</layout>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
|
|
|
@ -4,6 +4,6 @@ import { setupI18n } from "~/functions/i18n";
|
|||
|
||||
if ((window as any).kcContext) {
|
||||
const app = createApp(index);
|
||||
app.use(setupI18n());
|
||||
app.use(setupI18n())
|
||||
app.mount("#app");
|
||||
}
|
||||
|
|
|
@ -24,7 +24,7 @@
|
|||
<div class="w-full ml-5 flex flex-col">
|
||||
<p>{{ authenticator.label }}</p>
|
||||
<p class="text-sm">
|
||||
{{ $t("webauthn.registered", { date: authenticator.createdAt }) }}
|
||||
{{ $t("webauthn.registered", {date: moment(authenticator.createdAt)}) }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -59,7 +59,9 @@
|
|||
</button>
|
||||
</div>
|
||||
<div v-if="error" class="mt-5 flex flex-col">
|
||||
<ErrorBox>{{ $t("webauthn.error") }}</ErrorBox>
|
||||
<ErrorBox
|
||||
>{{ $t("webauthn.error") }}</ErrorBox
|
||||
>
|
||||
<button
|
||||
:onclick="retryAuth"
|
||||
class="w-full mt-5 px-4 py-2 tracking-wide text-white transition-colors duration-200 transform bg-blue-500 rounded-md hover:bg-blue-400 focus:outline-none focus:bg-blue-400">
|
||||
|
@ -75,6 +77,7 @@ import ErrorBox from "~/components/ErrorBox.vue";
|
|||
import type { KcContextBase } from "~/types/context";
|
||||
import { base64url } from "rfc4648";
|
||||
import { formPost } from "~/functions/utils";
|
||||
import moment from "moment";
|
||||
|
||||
export default defineComponent({
|
||||
name: "WebAuthnAuthenticate",
|
||||
|
@ -95,6 +98,9 @@ export default defineComponent({
|
|||
}
|
||||
},
|
||||
methods: {
|
||||
moment(date: string) : string {
|
||||
return moment(date).toDate().toLocaleString();
|
||||
},
|
||||
tryAnotherWay(e: Event) {
|
||||
e.preventDefault();
|
||||
formPost(this.context.url.loginAction, {
|
||||
|
@ -103,7 +109,7 @@ export default defineComponent({
|
|||
},
|
||||
prepareAuthenticate() {
|
||||
if (this.context.isUserIdentified) {
|
||||
const allowedCredentials: PublicKeyCredentialDescriptor[] = [];
|
||||
const allowedCredentials = new Array<PublicKeyCredentialDescriptor>();
|
||||
this.context.authenticators.authenticators.forEach(authenticator => {
|
||||
allowedCredentials.push({
|
||||
id: base64url.parse(authenticator.credentialId, { loose: true }),
|
||||
|
@ -115,7 +121,9 @@ export default defineComponent({
|
|||
this.authenticate([]);
|
||||
}
|
||||
},
|
||||
async authenticate(allowedAuthenticators: PublicKeyCredentialDescriptor[]) {
|
||||
async authenticate(
|
||||
allowedAuthenticators: Array<PublicKeyCredentialDescriptor>
|
||||
) {
|
||||
const publicKey: PublicKeyCredentialRequestOptions = {
|
||||
rpId: this.context.rpId,
|
||||
challenge: base64url.parse(this.context.challenge, { loose: true }),
|
||||
|
|
Loading…
Reference in a new issue