Compare commits
69 Commits
v10.0.0-rc
...
v10.1.0-rc
Author | SHA1 | Date | |
---|---|---|---|
569e933f02 | |||
46c40d713a | |||
f3602219f3 | |||
c6b52acf2f | |||
7260589136 | |||
b2e9ddaa4f | |||
4338b3ecb7 | |||
0f81d9f146 | |||
9980b10a83 | |||
6bfd388827 | |||
8203ed687b | |||
f394e06e4d | |||
8db35a81da | |||
2e0ebfcf58 | |||
51d2ff85e0 | |||
8b54426b89 | |||
fa346c5b1f | |||
d87788980d | |||
1e4319498c | |||
48501407fc | |||
01cbdee2ca | |||
b70c0af0a9 | |||
dcaee9cb7f | |||
1d8b6c7792 | |||
c98dbe84c6 | |||
1785916d32 | |||
c6cf564842 | |||
380b739017 | |||
c3f3c55303 | |||
2c01018529 | |||
dd2edf3013 | |||
7f3cdf9fac | |||
f75a91fbc1 | |||
f151086bb1 | |||
7c833e6f10 | |||
885e8314e8 | |||
3bdd955ab6 | |||
9499587bad | |||
0879ddba7c | |||
106a1dd4c7 | |||
5580248bcd | |||
c9c10b8fba | |||
ed254922e9 | |||
4b7d1e2cec | |||
775ae57258 | |||
96e4cd79ee | |||
bb70f7df4f | |||
602de2e407 | |||
225ced989c | |||
ab53698f34 | |||
02f2124126 | |||
66623e3324 | |||
4cc886fd04 | |||
a10b490245 | |||
b947b8a00d | |||
60fa240a4d | |||
e05cd87b7c | |||
8e41c905ed | |||
e21f607ab0 | |||
34af5abb82 | |||
fc1cdb5dc9 | |||
069a0cc980 | |||
78363727e1 | |||
23b16746f6 | |||
6edf9c3d15 | |||
2e371d2078 | |||
b70b478e25 | |||
97ad132086 | |||
2c5c54bf46 |
@ -231,6 +231,34 @@
|
|||||||
"contributions": [
|
"contributions": [
|
||||||
"code"
|
"code"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "madmadson",
|
||||||
|
"name": "Tobias Matt",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/798831?v=4",
|
||||||
|
"profile": "https://github.com/madmadson",
|
||||||
|
"contributions": [
|
||||||
|
"code"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "oliviergoulet5",
|
||||||
|
"name": "Olivier Goulet",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/17685861?v=4",
|
||||||
|
"profile": "https://github.com/oliviergoulet5",
|
||||||
|
"contributions": [
|
||||||
|
"code"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "liamlows",
|
||||||
|
"name": "Liam Lowsley-Williams",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/1365914?v=4",
|
||||||
|
"profile": "https://github.com/liamlows",
|
||||||
|
"contributions": [
|
||||||
|
"code",
|
||||||
|
"doc"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"contributorsPerLine": 7,
|
"contributorsPerLine": 7,
|
||||||
|
@ -41,9 +41,7 @@
|
|||||||
<img width="400" src="https://github.com/user-attachments/assets/6bf3bef9-00b0-4460-97b9-0d2da8500798">
|
<img width="400" src="https://github.com/user-attachments/assets/6bf3bef9-00b0-4460-97b9-0d2da8500798">
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
Keycloakify is fully compatible with Keycloak 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, [~~22~~](https://github.com/keycloakify/keycloakify/issues/389#issuecomment-1822509763), 23, 24, 25...[and beyond](https://github.com/keycloakify/keycloakify/discussions/346#discussioncomment-5889791)
|
Keycloakify is fully compatible with Keycloak from version 11 to 25...[and beyond](https://github.com/keycloakify/keycloakify/discussions/346#discussioncomment-5889791)
|
||||||
|
|
||||||
> NOTE: Keycloakify 10 is still in release-candidate state. [Follow progress](https://github.com/keycloakify/keycloakify/pull/538).
|
|
||||||
|
|
||||||
## Sponsors
|
## Sponsors
|
||||||
|
|
||||||
@ -130,7 +128,9 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
|
|||||||
<td align="center" valign="top" width="14.28%"><a href="https://m-siemens.de/"><img src="https://avatars.githubusercontent.com/u/1873922?v=4?s=100" width="100px;" alt="Markus Siemens"/><br /><sub><b>Markus Siemens</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=msiemens" title="Code">💻</a></td>
|
<td align="center" valign="top" width="14.28%"><a href="https://m-siemens.de/"><img src="https://avatars.githubusercontent.com/u/1873922?v=4?s=100" width="100px;" alt="Markus Siemens"/><br /><sub><b>Markus Siemens</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=msiemens" title="Code">💻</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/law108000"><img src="https://avatars.githubusercontent.com/u/8112024?v=4?s=100" width="100px;" alt="Rlok"/><br /><sub><b>Rlok</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=law108000" title="Code">💻</a></td>
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/law108000"><img src="https://avatars.githubusercontent.com/u/8112024?v=4?s=100" width="100px;" alt="Rlok"/><br /><sub><b>Rlok</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=law108000" title="Code">💻</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Moulyy"><img src="https://avatars.githubusercontent.com/u/115405804?v=4?s=100" width="100px;" alt="Moulyy"/><br /><sub><b>Moulyy</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=Moulyy" title="Code">💻</a></td>
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Moulyy"><img src="https://avatars.githubusercontent.com/u/115405804?v=4?s=100" width="100px;" alt="Moulyy"/><br /><sub><b>Moulyy</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=Moulyy" title="Code">💻</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/giorgoslytos"><img src="https://avatars.githubusercontent.com/u/50946162?v=4?s=100" width="100px;" alt="giorgoslytos"/><br /><sub><b>giorgoslytos</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=giorgoslytos" title="Code">💻</a></td>
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/madmadson"><img src="https://avatars.githubusercontent.com/u/798831?v=4?s=100" width="100px;" alt="Tobias Matt"/><br /><sub><b>Tobias Matt</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=madmadson" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/oliviergoulet5"><img src="https://avatars.githubusercontent.com/u/17685861?v=4?s=100" width="100px;" alt="Olivier Goulet"/><br /><sub><b>Olivier Goulet</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=oliviergoulet5" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/liamlows"><img src="https://avatars.githubusercontent.com/u/1365914?v=4?s=100" width="100px;" alt="Liam Lowsley-Williams"/><br /><sub><b>Liam Lowsley-Williams</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=liamlows" title="Code">💻</a> <a href="https://github.com/keycloakify/keycloakify/commits?author=liamlows" title="Documentation">📖</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "keycloakify",
|
"name": "keycloakify",
|
||||||
"version": "10.0.0-rc.132",
|
"version": "10.1.0-rc.1",
|
||||||
"description": "Create Keycloak themes using React",
|
"description": "Create Keycloak themes using React",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@ -85,7 +85,7 @@
|
|||||||
"@types/yauzl": "^2.10.3",
|
"@types/yauzl": "^2.10.3",
|
||||||
"@vercel/ncc": "^0.38.1",
|
"@vercel/ncc": "^0.38.1",
|
||||||
"chalk": "^4.1.2",
|
"chalk": "^4.1.2",
|
||||||
"cheerio": "^1.0.0-rc.12",
|
"cheerio": "1.0.0-rc.12",
|
||||||
"chokidar-cli": "^3.0.0",
|
"chokidar-cli": "^3.0.0",
|
||||||
"cli-select": "^1.1.2",
|
"cli-select": "^1.1.2",
|
||||||
"husky": "^4.3.8",
|
"husky": "^4.3.8",
|
||||||
@ -103,7 +103,7 @@
|
|||||||
"termost": "^v0.12.1",
|
"termost": "^v0.12.1",
|
||||||
"tsc-alias": "^1.8.10",
|
"tsc-alias": "^1.8.10",
|
||||||
"tss-react": "^4.9.10",
|
"tss-react": "^4.9.10",
|
||||||
"typescript": "^4.9.1-beta",
|
"typescript": "^4.9.4",
|
||||||
"vite": "^5.2.11",
|
"vite": "^5.2.11",
|
||||||
"vitest": "^1.6.0",
|
"vitest": "^1.6.0",
|
||||||
"yauzl": "^2.10.0",
|
"yauzl": "^2.10.0",
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { join as pathJoin } from "path";
|
import { join as pathJoin } from "path";
|
||||||
import { copyKeycloakResourcesToPublic } from "../src/bin/shared/copyKeycloakResourcesToPublic";
|
import { copyKeycloakResourcesToPublic } from "../src/bin/shared/copyKeycloakResourcesToPublic";
|
||||||
import { getProxyFetchOptions } from "../src/bin/tools/fetchProxyOptions";
|
import { getProxyFetchOptions } from "../src/bin/tools/fetchProxyOptions";
|
||||||
import { LOGIN_THEME_RESOURCES_FROMkEYCLOAK_VERSION_DEFAULT } from "../src/bin/shared/constants";
|
import { LOGIN_THEME_RESOURCES_FROM_KEYCLOAK_VERSION_DEFAULT } from "../src/bin/shared/constants";
|
||||||
|
|
||||||
export async function copyKeycloakResourcesToStorybookStaticDir() {
|
export async function copyKeycloakResourcesToStorybookStaticDir() {
|
||||||
await copyKeycloakResourcesToPublic({
|
await copyKeycloakResourcesToPublic({
|
||||||
@ -11,7 +11,7 @@ export async function copyKeycloakResourcesToStorybookStaticDir() {
|
|||||||
npmConfigGetCwd: pathJoin(__dirname, "..")
|
npmConfigGetCwd: pathJoin(__dirname, "..")
|
||||||
}),
|
}),
|
||||||
loginThemeResourcesFromKeycloakVersion:
|
loginThemeResourcesFromKeycloakVersion:
|
||||||
LOGIN_THEME_RESOURCES_FROMkEYCLOAK_VERSION_DEFAULT,
|
LOGIN_THEME_RESOURCES_FROM_KEYCLOAK_VERSION_DEFAULT,
|
||||||
publicDirPath: pathJoin(__dirname, "..", ".storybook", "static")
|
publicDirPath: pathJoin(__dirname, "..", ".storybook", "static")
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -13,6 +13,10 @@ import { downloadKeycloakDefaultTheme } from "../src/bin/shared/downloadKeycloak
|
|||||||
import { getThisCodebaseRootDirPath } from "../src/bin/tools/getThisCodebaseRootDirPath";
|
import { getThisCodebaseRootDirPath } from "../src/bin/tools/getThisCodebaseRootDirPath";
|
||||||
import { deepAssign } from "../src/tools/deepAssign";
|
import { deepAssign } from "../src/tools/deepAssign";
|
||||||
import { getProxyFetchOptions } from "../src/bin/tools/fetchProxyOptions";
|
import { getProxyFetchOptions } from "../src/bin/tools/fetchProxyOptions";
|
||||||
|
import {
|
||||||
|
THEME_TYPES,
|
||||||
|
LAST_KEYCLOAK_VERSION_WITH_ACCOUNT_V1
|
||||||
|
} from "../src/bin/shared/constants";
|
||||||
|
|
||||||
// NOTE: To run without argument when we want to generate src/i18n/generated_kcMessages files,
|
// NOTE: To run without argument when we want to generate src/i18n/generated_kcMessages files,
|
||||||
// update the version array for generating for newer version.
|
// update the version array for generating for newer version.
|
||||||
@ -21,68 +25,77 @@ import { getProxyFetchOptions } from "../src/bin/tools/fetchProxyOptions";
|
|||||||
const propertiesParser = require("properties-parser");
|
const propertiesParser = require("properties-parser");
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
const keycloakVersion = "24.0.4";
|
|
||||||
|
|
||||||
const thisCodebaseRootDirPath = getThisCodebaseRootDirPath();
|
const thisCodebaseRootDirPath = getThisCodebaseRootDirPath();
|
||||||
|
|
||||||
const { defaultThemeDirPath } = await downloadKeycloakDefaultTheme({
|
|
||||||
keycloakVersion,
|
|
||||||
buildContext: {
|
|
||||||
cacheDirPath: pathJoin(
|
|
||||||
thisCodebaseRootDirPath,
|
|
||||||
"node_modules",
|
|
||||||
".cache",
|
|
||||||
"keycloakify"
|
|
||||||
),
|
|
||||||
fetchOptions: getProxyFetchOptions({
|
|
||||||
npmConfigGetCwd: thisCodebaseRootDirPath
|
|
||||||
})
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
type Dictionary = { [idiomId: string]: string };
|
type Dictionary = { [idiomId: string]: string };
|
||||||
|
|
||||||
const record: { [typeOfPage: string]: { [language: string]: Dictionary } } = {};
|
const record: { [themeType: string]: { [language: string]: Dictionary } } = {};
|
||||||
|
|
||||||
{
|
for (const themeType of THEME_TYPES) {
|
||||||
const baseThemeDirPath = pathJoin(defaultThemeDirPath, "base");
|
const { defaultThemeDirPath } = await downloadKeycloakDefaultTheme({
|
||||||
const re = new RegExp(
|
keycloakVersion: (() => {
|
||||||
`^([^\\${pathSep}]+)\\${pathSep}messages\\${pathSep}messages_([^.]+).properties$`
|
switch (themeType) {
|
||||||
);
|
case "login":
|
||||||
|
return "25.0.4";
|
||||||
crawl({
|
case "account":
|
||||||
dirPath: baseThemeDirPath,
|
return LAST_KEYCLOAK_VERSION_WITH_ACCOUNT_V1;
|
||||||
returnedPathsType: "relative to dirPath"
|
}
|
||||||
}).forEach(filePath => {
|
})(),
|
||||||
const match = filePath.match(re);
|
buildContext: {
|
||||||
|
cacheDirPath: pathJoin(
|
||||||
if (match === null) {
|
thisCodebaseRootDirPath,
|
||||||
return;
|
"node_modules",
|
||||||
|
".cache",
|
||||||
|
"keycloakify"
|
||||||
|
),
|
||||||
|
fetchOptions: getProxyFetchOptions({
|
||||||
|
npmConfigGetCwd: thisCodebaseRootDirPath
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const [, typeOfPage, language] = match;
|
|
||||||
|
|
||||||
(record[typeOfPage] ??= {})[language.replace(/_/g, "-")] = Object.fromEntries(
|
|
||||||
Object.entries(
|
|
||||||
propertiesParser.parse(
|
|
||||||
fs
|
|
||||||
.readFileSync(pathJoin(baseThemeDirPath, filePath))
|
|
||||||
.toString("utf8")
|
|
||||||
) as Record<string, string>
|
|
||||||
)
|
|
||||||
.map(([key, value]) => [key, value.replace(/''/g, "'")])
|
|
||||||
.map(([key, value]) => [
|
|
||||||
key === "locale_pt_BR" ? "locale_pt-BR" : key,
|
|
||||||
value
|
|
||||||
])
|
|
||||||
.map(([key, value]) => [key, key === "termsText" ? "" : value])
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
Object.keys(record).forEach(themeType => {
|
{
|
||||||
if (themeType !== "login" && themeType !== "account") {
|
const baseThemeDirPath = pathJoin(defaultThemeDirPath, "base");
|
||||||
return;
|
const re = new RegExp(
|
||||||
|
`^([^\\${pathSep}]+)\\${pathSep}messages\\${pathSep}messages_([^.]+).properties$`
|
||||||
|
);
|
||||||
|
|
||||||
|
crawl({
|
||||||
|
dirPath: baseThemeDirPath,
|
||||||
|
returnedPathsType: "relative to dirPath"
|
||||||
|
}).forEach(filePath => {
|
||||||
|
const match = filePath.match(re);
|
||||||
|
|
||||||
|
if (match === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [, themeType_here, language] = match;
|
||||||
|
|
||||||
|
if (themeType_here !== themeType) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
(record[themeType] ??= {})[language.replace(/_/g, "-")] =
|
||||||
|
Object.fromEntries(
|
||||||
|
Object.entries(
|
||||||
|
propertiesParser.parse(
|
||||||
|
fs
|
||||||
|
.readFileSync(pathJoin(baseThemeDirPath, filePath))
|
||||||
|
.toString("utf8")
|
||||||
|
) as Record<string, string>
|
||||||
|
)
|
||||||
|
.map(([key, value]) => [key, value.replace(/''/g, "'")])
|
||||||
|
.map(([key, value]) => [
|
||||||
|
key === "locale_pt_BR" ? "locale_pt-BR" : key,
|
||||||
|
value
|
||||||
|
])
|
||||||
|
.map(([key, value]) => [
|
||||||
|
key,
|
||||||
|
key === "termsText" ? "" : value
|
||||||
|
])
|
||||||
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const recordForThemeType = record[themeType];
|
const recordForThemeType = record[themeType];
|
||||||
@ -99,6 +112,29 @@ async function main() {
|
|||||||
assert(false);
|
assert(false);
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
/* Migration helper
|
||||||
|
|
||||||
|
console.log({ themeType });
|
||||||
|
|
||||||
|
{
|
||||||
|
|
||||||
|
const all = new Set<string>();
|
||||||
|
|
||||||
|
languages.forEach(languages => all.add(languages));
|
||||||
|
const currentlySupportedLanguages = Object.keys(keycloakifyExtraMessages);
|
||||||
|
currentlySupportedLanguages.forEach(languages => all.add(languages));
|
||||||
|
|
||||||
|
all.forEach(language => {
|
||||||
|
console.log([
|
||||||
|
`"${language}": `,
|
||||||
|
`isInLanguages: ${languages.includes(language)}`,
|
||||||
|
`isInKeycloakifyExtraMessages: ${currentlySupportedLanguages.includes(language)}`
|
||||||
|
].join(" "))
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
assert(
|
assert(
|
||||||
same(languages, Object.keys(keycloakifyExtraMessages), {
|
same(languages, Object.keys(keycloakifyExtraMessages), {
|
||||||
takeIntoAccountArraysOrdering: false
|
takeIntoAccountArraysOrdering: false
|
||||||
@ -180,7 +216,7 @@ async function main() {
|
|||||||
"utf8"
|
"utf8"
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const keycloakifyExtraMessages_login: Record<
|
const keycloakifyExtraMessages_login: Record<
|
||||||
@ -203,6 +239,7 @@ const keycloakifyExtraMessages_login: Record<
|
|||||||
| "nl"
|
| "nl"
|
||||||
| "no"
|
| "no"
|
||||||
| "pl"
|
| "pl"
|
||||||
|
| "pt"
|
||||||
| "pt-BR"
|
| "pt-BR"
|
||||||
| "ru"
|
| "ru"
|
||||||
| "sk"
|
| "sk"
|
||||||
@ -210,7 +247,9 @@ const keycloakifyExtraMessages_login: Record<
|
|||||||
| "th"
|
| "th"
|
||||||
| "tr"
|
| "tr"
|
||||||
| "uk"
|
| "uk"
|
||||||
| "zh-CN",
|
| "ka"
|
||||||
|
| "zh-CN"
|
||||||
|
| "zh-TW",
|
||||||
Record<
|
Record<
|
||||||
| "shouldBeEqual"
|
| "shouldBeEqual"
|
||||||
| "shouldBeDifferent"
|
| "shouldBeDifferent"
|
||||||
@ -434,6 +473,17 @@ const keycloakifyExtraMessages_login: Record<
|
|||||||
addValue: "Dodaj wartość",
|
addValue: "Dodaj wartość",
|
||||||
languages: "Języki"
|
languages: "Języki"
|
||||||
},
|
},
|
||||||
|
pt: {
|
||||||
|
shouldBeEqual: "{0} deve ser igual a {1}",
|
||||||
|
shouldBeDifferent: "{0} deve ser diferente de {1}",
|
||||||
|
shouldMatchPattern: "O padrão deve corresponder: `/{0}/`",
|
||||||
|
mustBeAnInteger: "Deve ser um número inteiro",
|
||||||
|
notAValidOption: "Não é uma opção válida",
|
||||||
|
selectAnOption: "Selecione uma opção",
|
||||||
|
remove: "Remover",
|
||||||
|
addValue: "Adicionar valor",
|
||||||
|
languages: "Idiomas"
|
||||||
|
},
|
||||||
"pt-BR": {
|
"pt-BR": {
|
||||||
shouldBeEqual: "{0} deve ser igual a {1}",
|
shouldBeEqual: "{0} deve ser igual a {1}",
|
||||||
shouldBeDifferent: "{0} deve ser diferente de {1}",
|
shouldBeDifferent: "{0} deve ser diferente de {1}",
|
||||||
@ -511,6 +561,17 @@ const keycloakifyExtraMessages_login: Record<
|
|||||||
addValue: "Додати значення",
|
addValue: "Додати значення",
|
||||||
languages: "Мови"
|
languages: "Мови"
|
||||||
},
|
},
|
||||||
|
ka: {
|
||||||
|
shouldBeEqual: "{0} უნდა იყოს ტოლი {1}-სთვის",
|
||||||
|
shouldBeDifferent: "{0} უნდა იყოს სხვა {1}-სთვის",
|
||||||
|
shouldMatchPattern: "შაბლონს უნდა ემთხვევა: `/{0}/`",
|
||||||
|
mustBeAnInteger: "უნდა იყოს მთელი რიცხვი",
|
||||||
|
notAValidOption: "არასწორი ვარიანტი",
|
||||||
|
selectAnOption: "აირჩიეთ ვარიანტი",
|
||||||
|
remove: "წაშალეთ",
|
||||||
|
addValue: "დაამატეთ მნიშვნელობა",
|
||||||
|
languages: "ენები"
|
||||||
|
},
|
||||||
"zh-CN": {
|
"zh-CN": {
|
||||||
shouldBeEqual: "{0} 应该等于 {1}",
|
shouldBeEqual: "{0} 应该等于 {1}",
|
||||||
shouldBeDifferent: "{0} 应该不同于 {1}",
|
shouldBeDifferent: "{0} 应该不同于 {1}",
|
||||||
@ -521,6 +582,17 @@ const keycloakifyExtraMessages_login: Record<
|
|||||||
remove: "移除",
|
remove: "移除",
|
||||||
addValue: "添加值",
|
addValue: "添加值",
|
||||||
languages: "语言"
|
languages: "语言"
|
||||||
|
},
|
||||||
|
"zh-TW": {
|
||||||
|
shouldBeEqual: "{0} 應該等於 {1}",
|
||||||
|
shouldBeDifferent: "{0} 應該不同於 {1}",
|
||||||
|
shouldMatchPattern: "模式應匹配: `/{0}/`",
|
||||||
|
mustBeAnInteger: "必須是整數",
|
||||||
|
notAValidOption: "不是有效選項",
|
||||||
|
selectAnOption: "選擇一個選項",
|
||||||
|
remove: "移除",
|
||||||
|
addValue: "添加值",
|
||||||
|
languages: "語言"
|
||||||
}
|
}
|
||||||
/* spell-checker: enable */
|
/* spell-checker: enable */
|
||||||
};
|
};
|
||||||
@ -532,9 +604,7 @@ const keycloakifyExtraMessages_account: Record<
|
|||||||
| "cs"
|
| "cs"
|
||||||
| "da"
|
| "da"
|
||||||
| "de"
|
| "de"
|
||||||
| "el"
|
|
||||||
| "es"
|
| "es"
|
||||||
| "fa"
|
|
||||||
| "fi"
|
| "fi"
|
||||||
| "fr"
|
| "fr"
|
||||||
| "hu"
|
| "hu"
|
||||||
@ -549,9 +619,7 @@ const keycloakifyExtraMessages_account: Record<
|
|||||||
| "ru"
|
| "ru"
|
||||||
| "sk"
|
| "sk"
|
||||||
| "sv"
|
| "sv"
|
||||||
| "th"
|
|
||||||
| "tr"
|
| "tr"
|
||||||
| "uk"
|
|
||||||
| "zh-CN",
|
| "zh-CN",
|
||||||
Record<"newPasswordSameAsOld" | "passwordConfirmNotMatch", string>
|
Record<"newPasswordSameAsOld" | "passwordConfirmNotMatch", string>
|
||||||
> = {
|
> = {
|
||||||
@ -580,18 +648,10 @@ const keycloakifyExtraMessages_account: Record<
|
|||||||
newPasswordSameAsOld: "Das neue Passwort muss sich vom alten unterscheiden",
|
newPasswordSameAsOld: "Das neue Passwort muss sich vom alten unterscheiden",
|
||||||
passwordConfirmNotMatch: "Passwortbestätigung stimmt nicht überein"
|
passwordConfirmNotMatch: "Passwortbestätigung stimmt nicht überein"
|
||||||
},
|
},
|
||||||
el: {
|
|
||||||
newPasswordSameAsOld: "Ο νέος κωδικός πρόσβασης πρέπει να διαφέρει από τον παλιό",
|
|
||||||
passwordConfirmNotMatch: "Η επιβεβαίωση του κωδικού πρόσβασης δεν ταιριάζει"
|
|
||||||
},
|
|
||||||
es: {
|
es: {
|
||||||
newPasswordSameAsOld: "La nueva contraseña debe ser diferente de la anterior",
|
newPasswordSameAsOld: "La nueva contraseña debe ser diferente de la anterior",
|
||||||
passwordConfirmNotMatch: "La confirmación de la contraseña no coincide"
|
passwordConfirmNotMatch: "La confirmación de la contraseña no coincide"
|
||||||
},
|
},
|
||||||
fa: {
|
|
||||||
newPasswordSameAsOld: "رمز عبور جدید باید با رمز عبور قبلی متفاوت باشد",
|
|
||||||
passwordConfirmNotMatch: "تأیید رمز عبور مطابقت ندارد"
|
|
||||||
},
|
|
||||||
fi: {
|
fi: {
|
||||||
newPasswordSameAsOld: "Uusi salasana on oltava erilainen kuin vanha",
|
newPasswordSameAsOld: "Uusi salasana on oltava erilainen kuin vanha",
|
||||||
passwordConfirmNotMatch: "Salasanan vahvistus ei täsmää"
|
passwordConfirmNotMatch: "Salasanan vahvistus ei täsmää"
|
||||||
@ -649,18 +709,10 @@ const keycloakifyExtraMessages_account: Record<
|
|||||||
newPasswordSameAsOld: "Det nya lösenordet måste skilja sig från det gamla",
|
newPasswordSameAsOld: "Det nya lösenordet måste skilja sig från det gamla",
|
||||||
passwordConfirmNotMatch: "Lösenordsbekräftelsen matchar inte"
|
passwordConfirmNotMatch: "Lösenordsbekräftelsen matchar inte"
|
||||||
},
|
},
|
||||||
th: {
|
|
||||||
newPasswordSameAsOld: "รหัสผ่านใหม่ต้องต่างจากรหัสผ่านเดิม",
|
|
||||||
passwordConfirmNotMatch: "การยืนยันรหัสผ่านไม่ตรงกัน"
|
|
||||||
},
|
|
||||||
tr: {
|
tr: {
|
||||||
newPasswordSameAsOld: "Yeni şifre eskisinden farklı olmalıdır",
|
newPasswordSameAsOld: "Yeni şifre eskisinden farklı olmalıdır",
|
||||||
passwordConfirmNotMatch: "Şifre doğrulama eşleşmiyor"
|
passwordConfirmNotMatch: "Şifre doğrulama eşleşmiyor"
|
||||||
},
|
},
|
||||||
uk: {
|
|
||||||
newPasswordSameAsOld: "Новий пароль повинен відрізнятися від старого",
|
|
||||||
passwordConfirmNotMatch: "Підтвердження пароля не співпадає"
|
|
||||||
},
|
|
||||||
"zh-CN": {
|
"zh-CN": {
|
||||||
newPasswordSameAsOld: "新密码必须与旧密码不同",
|
newPasswordSameAsOld: "新密码必须与旧密码不同",
|
||||||
passwordConfirmNotMatch: "密码确认不匹配"
|
passwordConfirmNotMatch: "密码确认不匹配"
|
||||||
|
@ -2,8 +2,33 @@ import * as child_process from "child_process";
|
|||||||
import * as fs from "fs";
|
import * as fs from "fs";
|
||||||
import { join } from "path";
|
import { join } from "path";
|
||||||
import { startRebuildOnSrcChange } from "./startRebuildOnSrcChange";
|
import { startRebuildOnSrcChange } from "./startRebuildOnSrcChange";
|
||||||
|
import { crawl } from "../src/bin/tools/crawl";
|
||||||
|
|
||||||
|
{
|
||||||
|
const dirPath = "node_modules";
|
||||||
|
|
||||||
|
try {
|
||||||
|
fs.rmSync(dirPath, { recursive: true, force: true });
|
||||||
|
} catch {
|
||||||
|
// NOTE: This is a workaround for windows
|
||||||
|
// we can't remove locked executables.
|
||||||
|
|
||||||
|
crawl({
|
||||||
|
dirPath,
|
||||||
|
returnedPathsType: "absolute"
|
||||||
|
}).forEach(filePath => {
|
||||||
|
try {
|
||||||
|
fs.rmSync(filePath, { force: true });
|
||||||
|
} catch (error) {
|
||||||
|
if (filePath.endsWith(".exe")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fs.rmSync("node_modules", { recursive: true, force: true });
|
|
||||||
fs.rmSync("dist", { recursive: true, force: true });
|
fs.rmSync("dist", { recursive: true, force: true });
|
||||||
fs.rmSync(".yarn_home", { recursive: true, force: true });
|
fs.rmSync(".yarn_home", { recursive: true, force: true });
|
||||||
|
|
||||||
|
@ -60,14 +60,14 @@ export function updateAccountThemeImplementationInConfig(params: {
|
|||||||
{
|
{
|
||||||
const parsedPackageJson = (() => {
|
const parsedPackageJson = (() => {
|
||||||
type ParsedPackageJson = {
|
type ParsedPackageJson = {
|
||||||
keycloakify: Record<string, string>;
|
keycloakify: Record<string, unknown>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const zParsedPackageJson = (() => {
|
const zParsedPackageJson = (() => {
|
||||||
type TargetType = ParsedPackageJson;
|
type TargetType = ParsedPackageJson;
|
||||||
|
|
||||||
const zTargetType = z.object({
|
const zTargetType = z.object({
|
||||||
keycloakify: z.record(z.string())
|
keycloakify: z.record(z.unknown())
|
||||||
});
|
});
|
||||||
|
|
||||||
assert<Equals<z.infer<typeof zTargetType>, TargetType>>();
|
assert<Equals<z.infer<typeof zTargetType>, TargetType>>();
|
||||||
@ -75,17 +75,22 @@ export function updateAccountThemeImplementationInConfig(params: {
|
|||||||
return id<z.ZodType<TargetType>>(zTargetType);
|
return id<z.ZodType<TargetType>>(zTargetType);
|
||||||
})();
|
})();
|
||||||
|
|
||||||
return zParsedPackageJson.parse(
|
const parsedPackageJson = JSON.parse(
|
||||||
JSON.parse(
|
fs.readFileSync(buildContext.packageJsonFilePath).toString("utf8")
|
||||||
fs
|
|
||||||
.readFileSync(buildContext.packageJsonFilePath)
|
|
||||||
.toString("utf8")
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
zParsedPackageJson.parse(parsedPackageJson);
|
||||||
|
|
||||||
|
return parsedPackageJson;
|
||||||
})();
|
})();
|
||||||
|
|
||||||
parsedPackageJson.keycloakify.accountThemeImplementation =
|
parsedPackageJson.keycloakify.accountThemeImplementation =
|
||||||
accountThemeType;
|
accountThemeType;
|
||||||
|
|
||||||
|
fs.writeFileSync(
|
||||||
|
buildContext.packageJsonFilePath,
|
||||||
|
Buffer.from(JSON.stringify(parsedPackageJson, undefined, 4), "utf8")
|
||||||
|
);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -30,6 +30,7 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
|
|||||||
// NOTE: This is arbitrary
|
// NOTE: This is arbitrary
|
||||||
startingFromMajor: 17,
|
startingFromMajor: 17,
|
||||||
excludeMajorVersions: [],
|
excludeMajorVersions: [],
|
||||||
|
doOmitPatch: false,
|
||||||
buildContext
|
buildContext
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -139,25 +139,6 @@ export async function buildJar(params: {
|
|||||||
break route_legacy_pages;
|
break route_legacy_pages;
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOTE: If there's no account theme there is no special target for keycloak 24 and up so we create
|
|
||||||
// the pages anyway. If there is an account pages, since we know that account-v1 is only support keycloak
|
|
||||||
// 24 in version 0.4 and up, we can safely break the route for legacy pages.
|
|
||||||
const doBreak: boolean = (() => {
|
|
||||||
switch (keycloakAccountV1Version) {
|
|
||||||
case null:
|
|
||||||
return false;
|
|
||||||
case "0.3":
|
|
||||||
return false;
|
|
||||||
default:
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
|
|
||||||
// TODO: Remove this optimization, it's a bit hacky.
|
|
||||||
if (doBreak) {
|
|
||||||
break route_legacy_pages;
|
|
||||||
}
|
|
||||||
|
|
||||||
(["register.ftl", "login-update-profile.ftl"] as const).forEach(pageId =>
|
(["register.ftl", "login-update-profile.ftl"] as const).forEach(pageId =>
|
||||||
buildContext.themeNames.map(themeName => {
|
buildContext.themeNames.map(themeName => {
|
||||||
const ftlFilePath = pathJoin(
|
const ftlFilePath = pathJoin(
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import cheerio from "cheerio";
|
import * as cheerio from "cheerio";
|
||||||
import {
|
import {
|
||||||
replaceImportsInJsCode,
|
replaceImportsInJsCode,
|
||||||
BuildContextLike as BuildContextLike_replaceImportsInJsCode
|
BuildContextLike as BuildContextLike_replaceImportsInJsCode
|
||||||
|
@ -78,7 +78,7 @@ program
|
|||||||
|
|
||||||
program
|
program
|
||||||
.command<{
|
.command<{
|
||||||
port: number;
|
port: number | undefined;
|
||||||
keycloakVersion: string | undefined;
|
keycloakVersion: string | undefined;
|
||||||
realmJsonFilePath: string | undefined;
|
realmJsonFilePath: string | undefined;
|
||||||
}>({
|
}>({
|
||||||
@ -96,7 +96,7 @@ program
|
|||||||
return name;
|
return name;
|
||||||
})(),
|
})(),
|
||||||
description: ["Keycloak server port.", "Example `--port 8085`"].join(" "),
|
description: ["Keycloak server port.", "Example `--port 8085`"].join(" "),
|
||||||
defaultValue: 8080
|
defaultValue: undefined
|
||||||
})
|
})
|
||||||
.option({
|
.option({
|
||||||
key: "keycloakVersion",
|
key: "keycloakVersion",
|
||||||
|
@ -15,7 +15,7 @@ import * as child_process from "child_process";
|
|||||||
import {
|
import {
|
||||||
VITE_PLUGIN_SUB_SCRIPTS_ENV_NAMES,
|
VITE_PLUGIN_SUB_SCRIPTS_ENV_NAMES,
|
||||||
BUILD_FOR_KEYCLOAK_MAJOR_VERSION_ENV_NAME,
|
BUILD_FOR_KEYCLOAK_MAJOR_VERSION_ENV_NAME,
|
||||||
LOGIN_THEME_RESOURCES_FROMkEYCLOAK_VERSION_DEFAULT
|
LOGIN_THEME_RESOURCES_FROM_KEYCLOAK_VERSION_DEFAULT
|
||||||
} from "./constants";
|
} from "./constants";
|
||||||
import type { KeycloakVersionRange } from "./KeycloakVersionRange";
|
import type { KeycloakVersionRange } from "./KeycloakVersionRange";
|
||||||
import { exclude } from "tsafe";
|
import { exclude } from "tsafe";
|
||||||
@ -61,6 +61,19 @@ export type BuildContext = {
|
|||||||
keycloakVersionRange: KeycloakVersionRange;
|
keycloakVersionRange: KeycloakVersionRange;
|
||||||
jarFileBasename: string;
|
jarFileBasename: string;
|
||||||
}[];
|
}[];
|
||||||
|
startKeycloakOptions: {
|
||||||
|
dockerImage:
|
||||||
|
| {
|
||||||
|
reference: string;
|
||||||
|
tag: string;
|
||||||
|
}
|
||||||
|
| undefined;
|
||||||
|
dockerExtraArgs: string[];
|
||||||
|
keycloakExtraArgs: string[];
|
||||||
|
extensionJars: ({ type: "path"; path: string } | { type: "url"; url: string })[];
|
||||||
|
realmJsonFilePath: string | undefined;
|
||||||
|
port: number | undefined;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
assert<Equals<keyof BuildContext["implementedThemeTypes"], ThemeType | "email">>();
|
assert<Equals<keyof BuildContext["implementedThemeTypes"], ThemeType | "email">>();
|
||||||
@ -75,6 +88,14 @@ export type BuildOptions = {
|
|||||||
loginThemeResourcesFromKeycloakVersion?: string;
|
loginThemeResourcesFromKeycloakVersion?: string;
|
||||||
keycloakifyBuildDirPath?: string;
|
keycloakifyBuildDirPath?: string;
|
||||||
kcContextExclusionsFtl?: string;
|
kcContextExclusionsFtl?: string;
|
||||||
|
startKeycloakOptions?: {
|
||||||
|
dockerImage?: string;
|
||||||
|
dockerExtraArgs?: string[];
|
||||||
|
keycloakExtraArgs?: string[];
|
||||||
|
extensionJars?: string[];
|
||||||
|
realmJsonFilePath?: string;
|
||||||
|
port?: number;
|
||||||
|
};
|
||||||
} & BuildOptions.AccountThemeImplAndKeycloakVersionTargets;
|
} & BuildOptions.AccountThemeImplAndKeycloakVersionTargets;
|
||||||
|
|
||||||
export namespace BuildOptions {
|
export namespace BuildOptions {
|
||||||
@ -301,6 +322,23 @@ export function getBuildContext(params: {
|
|||||||
return id<z.ZodType<TargetType>>(zTargetType);
|
return id<z.ZodType<TargetType>>(zTargetType);
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
const zStartKeycloakOptions = (() => {
|
||||||
|
type TargetType = NonNullable<BuildOptions["startKeycloakOptions"]>;
|
||||||
|
|
||||||
|
const zTargetType = z.object({
|
||||||
|
dockerImage: z.string().optional(),
|
||||||
|
extensionJars: z.array(z.string()).optional(),
|
||||||
|
realmJsonFilePath: z.string().optional(),
|
||||||
|
dockerExtraArgs: z.array(z.string()).optional(),
|
||||||
|
keycloakExtraArgs: z.array(z.string()).optional(),
|
||||||
|
port: z.number().optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
assert<Equals<z.infer<typeof zTargetType>, TargetType>>();
|
||||||
|
|
||||||
|
return id<z.ZodType<TargetType>>(zTargetType);
|
||||||
|
})();
|
||||||
|
|
||||||
const zBuildOptions = (() => {
|
const zBuildOptions = (() => {
|
||||||
type TargetType = BuildOptions;
|
type TargetType = BuildOptions;
|
||||||
|
|
||||||
@ -321,7 +359,8 @@ export function getBuildContext(params: {
|
|||||||
groupId: z.string().optional(),
|
groupId: z.string().optional(),
|
||||||
loginThemeResourcesFromKeycloakVersion: z.string().optional(),
|
loginThemeResourcesFromKeycloakVersion: z.string().optional(),
|
||||||
keycloakifyBuildDirPath: z.string().optional(),
|
keycloakifyBuildDirPath: z.string().optional(),
|
||||||
kcContextExclusionsFtl: z.string().optional()
|
kcContextExclusionsFtl: z.string().optional(),
|
||||||
|
startKeycloakOptions: zStartKeycloakOptions.optional()
|
||||||
}),
|
}),
|
||||||
zAccountThemeImplAndKeycloakVersionTargets
|
zAccountThemeImplAndKeycloakVersionTargets
|
||||||
);
|
);
|
||||||
@ -508,7 +547,7 @@ export function getBuildContext(params: {
|
|||||||
`${themeNames[0]}-keycloak-theme`,
|
`${themeNames[0]}-keycloak-theme`,
|
||||||
loginThemeResourcesFromKeycloakVersion:
|
loginThemeResourcesFromKeycloakVersion:
|
||||||
buildOptions.loginThemeResourcesFromKeycloakVersion ??
|
buildOptions.loginThemeResourcesFromKeycloakVersion ??
|
||||||
LOGIN_THEME_RESOURCES_FROMkEYCLOAK_VERSION_DEFAULT,
|
LOGIN_THEME_RESOURCES_FROM_KEYCLOAK_VERSION_DEFAULT,
|
||||||
projectDirPath,
|
projectDirPath,
|
||||||
projectBuildDirPath,
|
projectBuildDirPath,
|
||||||
keycloakifyBuildDirPath: (() => {
|
keycloakifyBuildDirPath: (() => {
|
||||||
@ -891,6 +930,48 @@ export function getBuildContext(params: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return jarTargets;
|
return jarTargets;
|
||||||
})()
|
})(),
|
||||||
|
startKeycloakOptions: {
|
||||||
|
dockerImage: (() => {
|
||||||
|
if (buildOptions.startKeycloakOptions?.dockerImage === undefined) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [reference, tag, ...rest] =
|
||||||
|
buildOptions.startKeycloakOptions.dockerImage.split(":");
|
||||||
|
|
||||||
|
assert(
|
||||||
|
reference !== undefined && tag !== undefined && rest.length === 0,
|
||||||
|
`Invalid docker image: ${buildOptions.startKeycloakOptions.dockerImage}`
|
||||||
|
);
|
||||||
|
|
||||||
|
return { reference, tag };
|
||||||
|
})(),
|
||||||
|
dockerExtraArgs: buildOptions.startKeycloakOptions?.dockerExtraArgs ?? [],
|
||||||
|
keycloakExtraArgs: buildOptions.startKeycloakOptions?.keycloakExtraArgs ?? [],
|
||||||
|
extensionJars: (buildOptions.startKeycloakOptions?.extensionJars ?? []).map(
|
||||||
|
urlOrPath => {
|
||||||
|
if (/^https?:\/\//.test(urlOrPath)) {
|
||||||
|
return { type: "url", url: urlOrPath };
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: "path",
|
||||||
|
path: getAbsoluteAndInOsFormatPath({
|
||||||
|
pathIsh: urlOrPath,
|
||||||
|
cwd: projectDirPath
|
||||||
|
})
|
||||||
|
};
|
||||||
|
}
|
||||||
|
),
|
||||||
|
realmJsonFilePath:
|
||||||
|
buildOptions.startKeycloakOptions?.realmJsonFilePath === undefined
|
||||||
|
? undefined
|
||||||
|
: getAbsoluteAndInOsFormatPath({
|
||||||
|
pathIsh: buildOptions.startKeycloakOptions.realmJsonFilePath,
|
||||||
|
cwd: projectDirPath
|
||||||
|
}),
|
||||||
|
port: buildOptions.startKeycloakOptions?.port
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -50,7 +50,9 @@ export const LOGIN_THEME_PAGE_IDS = [
|
|||||||
"login-recovery-authn-code-input.ftl",
|
"login-recovery-authn-code-input.ftl",
|
||||||
"login-reset-otp.ftl",
|
"login-reset-otp.ftl",
|
||||||
"login-x509-info.ftl",
|
"login-x509-info.ftl",
|
||||||
"webauthn-error.ftl"
|
"webauthn-error.ftl",
|
||||||
|
"login-passkeys-conditional-authenticate.ftl",
|
||||||
|
"login-idp-link-confirm-override.ftl"
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export const ACCOUNT_THEME_PAGE_IDS = [
|
export const ACCOUNT_THEME_PAGE_IDS = [
|
||||||
@ -70,4 +72,4 @@ export const CONTAINER_NAME = "keycloak-keycloakify";
|
|||||||
|
|
||||||
export const FALLBACK_LANGUAGE_TAG = "en";
|
export const FALLBACK_LANGUAGE_TAG = "en";
|
||||||
|
|
||||||
export const LOGIN_THEME_RESOURCES_FROMkEYCLOAK_VERSION_DEFAULT = "24.0.4";
|
export const LOGIN_THEME_RESOURCES_FROM_KEYCLOAK_VERSION_DEFAULT = "24.0.4";
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
import { join as pathJoin, relative as pathRelative } from "path";
|
import { join as pathJoin, relative as pathRelative } from "path";
|
||||||
import { type BuildContext } from "./buildContext";
|
import { type BuildContext } from "../buildContext";
|
||||||
import { assert } from "tsafe/assert";
|
import { assert } from "tsafe/assert";
|
||||||
import { LAST_KEYCLOAK_VERSION_WITH_ACCOUNT_V1 } from "./constants";
|
import { LAST_KEYCLOAK_VERSION_WITH_ACCOUNT_V1 } from "../constants";
|
||||||
import { downloadAndExtractArchive } from "../tools/downloadAndExtractArchive";
|
import { downloadAndExtractArchive } from "../../tools/downloadAndExtractArchive";
|
||||||
|
import { getThisCodebaseRootDirPath } from "../../tools/getThisCodebaseRootDirPath";
|
||||||
|
import * as fsPr from "fs/promises";
|
||||||
|
|
||||||
export type BuildContextLike = {
|
export type BuildContextLike = {
|
||||||
cacheDirPath: string;
|
cacheDirPath: string;
|
||||||
@ -20,6 +22,8 @@ export async function downloadKeycloakDefaultTheme(params: {
|
|||||||
let kcNodeModulesKeepFilePaths: Set<string> | undefined = undefined;
|
let kcNodeModulesKeepFilePaths: Set<string> | undefined = undefined;
|
||||||
let kcNodeModulesKeepFilePaths_lastAccountV1: Set<string> | undefined = undefined;
|
let kcNodeModulesKeepFilePaths_lastAccountV1: Set<string> | undefined = undefined;
|
||||||
|
|
||||||
|
let areExtraAssetsFor24Copied = false;
|
||||||
|
|
||||||
const { extractedDirPath } = await downloadAndExtractArchive({
|
const { extractedDirPath } = await downloadAndExtractArchive({
|
||||||
url: `https://repo1.maven.org/maven2/org/keycloak/keycloak-themes/${keycloakVersion}/keycloak-themes-${keycloakVersion}.jar`,
|
url: `https://repo1.maven.org/maven2/org/keycloak/keycloak-themes/${keycloakVersion}/keycloak-themes-${keycloakVersion}.jar`,
|
||||||
cacheDirPath: buildContext.cacheDirPath,
|
cacheDirPath: buildContext.cacheDirPath,
|
||||||
@ -32,8 +36,6 @@ export async function downloadKeycloakDefaultTheme(params: {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { readFile, writeFile } = params;
|
|
||||||
|
|
||||||
skip_keycloak_v2: {
|
skip_keycloak_v2: {
|
||||||
if (!fileRelativePath.startsWith(pathJoin("keycloak.v2"))) {
|
if (!fileRelativePath.startsWith(pathJoin("keycloak.v2"))) {
|
||||||
break skip_keycloak_v2;
|
break skip_keycloak_v2;
|
||||||
@ -42,6 +44,8 @@ export async function downloadKeycloakDefaultTheme(params: {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { readFile, writeFile } = params;
|
||||||
|
|
||||||
last_account_v1_transformations: {
|
last_account_v1_transformations: {
|
||||||
if (LAST_KEYCLOAK_VERSION_WITH_ACCOUNT_V1 !== keycloakVersion) {
|
if (LAST_KEYCLOAK_VERSION_WITH_ACCOUNT_V1 !== keycloakVersion) {
|
||||||
break last_account_v1_transformations;
|
break last_account_v1_transformations;
|
||||||
@ -168,6 +172,43 @@ export async function downloadKeycloakDefaultTheme(params: {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
copy_extra_assets: {
|
||||||
|
if (keycloakVersion !== "24.0.4") {
|
||||||
|
break copy_extra_assets;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (areExtraAssetsFor24Copied) {
|
||||||
|
break copy_extra_assets;
|
||||||
|
}
|
||||||
|
|
||||||
|
const extraAssetsDirPath = pathJoin(
|
||||||
|
getThisCodebaseRootDirPath(),
|
||||||
|
"src",
|
||||||
|
"bin",
|
||||||
|
"shared",
|
||||||
|
"downloadKeycloakDefaultTheme",
|
||||||
|
"extra-assets"
|
||||||
|
);
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
["webauthnAuthenticate.js", "passkeysConditionalAuth.js"].map(
|
||||||
|
async fileBasename =>
|
||||||
|
writeFile({
|
||||||
|
fileRelativePath: pathJoin(
|
||||||
|
"base",
|
||||||
|
"login",
|
||||||
|
"resources",
|
||||||
|
"js",
|
||||||
|
fileBasename
|
||||||
|
),
|
||||||
|
modifiedData: await fsPr.readFile(
|
||||||
|
pathJoin(extraAssetsDirPath, fileBasename)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
skip_unused_resources: {
|
skip_unused_resources: {
|
||||||
if (keycloakVersion !== "24.0.4") {
|
if (keycloakVersion !== "24.0.4") {
|
||||||
break skip_unused_resources;
|
break skip_unused_resources;
|
@ -0,0 +1,79 @@
|
|||||||
|
import { base64url } from "rfc4648";
|
||||||
|
import { returnSuccess, returnFailure } from "./webauthnAuthenticate.js";
|
||||||
|
|
||||||
|
export function initAuthenticate(input) {
|
||||||
|
// Check if WebAuthn is supported by this browser
|
||||||
|
if (!window.PublicKeyCredential) {
|
||||||
|
returnFailure(input.errmsg);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (input.isUserIdentified || typeof PublicKeyCredential.isConditionalMediationAvailable === "undefined") {
|
||||||
|
document.getElementById("kc-form-passkey-button").style.display = 'block';
|
||||||
|
} else {
|
||||||
|
tryAutoFillUI(input);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function doAuthenticate(input) {
|
||||||
|
// Check if WebAuthn is supported by this browser
|
||||||
|
if (!window.PublicKeyCredential) {
|
||||||
|
returnFailure(input.errmsg);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const publicKey = {
|
||||||
|
rpId : input.rpId,
|
||||||
|
challenge: base64url.parse(input.challenge, { loose: true })
|
||||||
|
};
|
||||||
|
|
||||||
|
publicKey.allowCredentials = !input.isUserIdentified ? [] : getAllowCredentials();
|
||||||
|
|
||||||
|
if (input.createTimeout !== 0) {
|
||||||
|
publicKey.timeout = input.createTimeout * 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.userVerification !== 'not specified') {
|
||||||
|
publicKey.userVerification = input.userVerification;
|
||||||
|
}
|
||||||
|
|
||||||
|
return navigator.credentials.get({
|
||||||
|
publicKey: publicKey,
|
||||||
|
...input.additionalOptions
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function tryAutoFillUI(input) {
|
||||||
|
const isConditionalMediationAvailable = await PublicKeyCredential.isConditionalMediationAvailable();
|
||||||
|
if (isConditionalMediationAvailable) {
|
||||||
|
document.getElementById("kc-form-login").style.display = "block";
|
||||||
|
input.additionalOptions = { mediation: 'conditional'};
|
||||||
|
try {
|
||||||
|
const result = await doAuthenticate(input);
|
||||||
|
returnSuccess(result);
|
||||||
|
} catch (error) {
|
||||||
|
returnFailure(error);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
document.getElementById("kc-form-passkey-button").style.display = 'block';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAllowCredentials() {
|
||||||
|
const allowCredentials = [];
|
||||||
|
const authnUse = document.forms['authn_select'].authn_use_chk;
|
||||||
|
if (authnUse !== undefined) {
|
||||||
|
if (authnUse.length === undefined) {
|
||||||
|
allowCredentials.push({
|
||||||
|
id: base64url.parse(authnUse.value, {loose: true}),
|
||||||
|
type: 'public-key',
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
authnUse.forEach((entry) =>
|
||||||
|
allowCredentials.push({
|
||||||
|
id: base64url.parse(entry.value, {loose: true}),
|
||||||
|
type: 'public-key',
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return allowCredentials;
|
||||||
|
}
|
@ -0,0 +1,82 @@
|
|||||||
|
import { base64url } from "rfc4648";
|
||||||
|
|
||||||
|
export async function authenticateByWebAuthn(input) {
|
||||||
|
if (!input.isUserIdentified) {
|
||||||
|
try {
|
||||||
|
const result = await doAuthenticate([], input.challenge, input.userVerification, input.rpId, input.createTimeout, input.errmsg);
|
||||||
|
returnSuccess(result);
|
||||||
|
} catch (error) {
|
||||||
|
returnFailure(error);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
checkAllowCredentials(input.challenge, input.userVerification, input.rpId, input.createTimeout, input.errmsg);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkAllowCredentials(challenge, userVerification, rpId, createTimeout, errmsg) {
|
||||||
|
const allowCredentials = [];
|
||||||
|
const authnUse = document.forms['authn_select'].authn_use_chk;
|
||||||
|
if (authnUse !== undefined) {
|
||||||
|
if (authnUse.length === undefined) {
|
||||||
|
allowCredentials.push({
|
||||||
|
id: base64url.parse(authnUse.value, {loose: true}),
|
||||||
|
type: 'public-key',
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
authnUse.forEach((entry) =>
|
||||||
|
allowCredentials.push({
|
||||||
|
id: base64url.parse(entry.value, {loose: true}),
|
||||||
|
type: 'public-key',
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const result = await doAuthenticate(allowCredentials, challenge, userVerification, rpId, createTimeout, errmsg);
|
||||||
|
returnSuccess(result);
|
||||||
|
} catch (error) {
|
||||||
|
returnFailure(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function doAuthenticate(allowCredentials, challenge, userVerification, rpId, createTimeout, errmsg) {
|
||||||
|
// Check if WebAuthn is supported by this browser
|
||||||
|
if (!window.PublicKeyCredential) {
|
||||||
|
returnFailure(errmsg);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const publicKey = {
|
||||||
|
rpId : rpId,
|
||||||
|
challenge: base64url.parse(challenge, { loose: true })
|
||||||
|
};
|
||||||
|
|
||||||
|
if (createTimeout !== 0) {
|
||||||
|
publicKey.timeout = createTimeout * 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allowCredentials.length) {
|
||||||
|
publicKey.allowCredentials = allowCredentials;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userVerification !== 'not specified') {
|
||||||
|
publicKey.userVerification = userVerification;
|
||||||
|
}
|
||||||
|
|
||||||
|
return navigator.credentials.get({publicKey});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function returnSuccess(result) {
|
||||||
|
document.getElementById("clientDataJSON").value = base64url.stringify(new Uint8Array(result.response.clientDataJSON), { pad: false });
|
||||||
|
document.getElementById("authenticatorData").value = base64url.stringify(new Uint8Array(result.response.authenticatorData), { pad: false });
|
||||||
|
document.getElementById("signature").value = base64url.stringify(new Uint8Array(result.response.signature), { pad: false });
|
||||||
|
document.getElementById("credentialId").value = result.id;
|
||||||
|
if (result.response.userHandle) {
|
||||||
|
document.getElementById("userHandle").value = base64url.stringify(new Uint8Array(result.response.userHandle), { pad: false });
|
||||||
|
}
|
||||||
|
document.getElementById("webauth").submit();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function returnFailure(err) {
|
||||||
|
document.getElementById("error").value = err;
|
||||||
|
document.getElementById("webauth").submit();
|
||||||
|
}
|
1
src/bin/shared/downloadKeycloakDefaultTheme/index.ts
Normal file
1
src/bin/shared/downloadKeycloakDefaultTheme/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from "./downloadKeycloakDefaultTheme";
|
@ -10,6 +10,7 @@ import { id } from "tsafe/id";
|
|||||||
import type { SemVer } from "../tools/SemVer";
|
import type { SemVer } from "../tools/SemVer";
|
||||||
import { same } from "evt/tools/inDepth/same";
|
import { same } from "evt/tools/inDepth/same";
|
||||||
import type { BuildContext } from "./buildContext";
|
import type { BuildContext } from "./buildContext";
|
||||||
|
import fetch from "make-fetch-happen";
|
||||||
|
|
||||||
type GetLatestsSemVersionedTag = ReturnType<
|
type GetLatestsSemVersionedTag = ReturnType<
|
||||||
typeof getLatestsSemVersionedTagFactory
|
typeof getLatestsSemVersionedTagFactory
|
||||||
@ -159,7 +160,13 @@ export async function getLatestsSemVersionedTag({
|
|||||||
|
|
||||||
const octokit = new Octokit({
|
const octokit = new Octokit({
|
||||||
...(githubToken === undefined ? {} : { auth: githubToken }),
|
...(githubToken === undefined ? {} : { auth: githubToken }),
|
||||||
request: buildContext.fetchOptions
|
request: {
|
||||||
|
fetch: (url: string, options?: any) =>
|
||||||
|
fetch(url, {
|
||||||
|
...options,
|
||||||
|
...buildContext.fetchOptions
|
||||||
|
})
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return octokit;
|
return octokit;
|
||||||
|
@ -14,9 +14,10 @@ assert<BuildContext extends BuildContextLike ? true : false>();
|
|||||||
export async function promptKeycloakVersion(params: {
|
export async function promptKeycloakVersion(params: {
|
||||||
startingFromMajor: number | undefined;
|
startingFromMajor: number | undefined;
|
||||||
excludeMajorVersions: number[];
|
excludeMajorVersions: number[];
|
||||||
|
doOmitPatch: boolean;
|
||||||
buildContext: BuildContextLike;
|
buildContext: BuildContextLike;
|
||||||
}) {
|
}) {
|
||||||
const { startingFromMajor, excludeMajorVersions, buildContext } = params;
|
const { startingFromMajor, excludeMajorVersions, doOmitPatch, buildContext } = params;
|
||||||
|
|
||||||
const semVersionedTagByMajor = new Map<number, { tag: string; version: SemVer }>();
|
const semVersionedTagByMajor = new Map<number, { tag: string; version: SemVer }>();
|
||||||
|
|
||||||
@ -55,7 +56,8 @@ export async function promptKeycloakVersion(params: {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const lastMajorVersions = Array.from(semVersionedTagByMajor.values()).map(
|
const lastMajorVersions = Array.from(semVersionedTagByMajor.values()).map(
|
||||||
({ tag }) => tag
|
({ version }) =>
|
||||||
|
`${version.major}.${version.minor}${doOmitPatch ? "" : `.${version.patch}`}`
|
||||||
);
|
);
|
||||||
|
|
||||||
const { value } = await cliSelect<string>({
|
const { value } = await cliSelect<string>({
|
||||||
|
@ -40,7 +40,7 @@ async function appBuild_vite(params: {
|
|||||||
|
|
||||||
const dIsSuccess = new Deferred<boolean>();
|
const dIsSuccess = new Deferred<boolean>();
|
||||||
|
|
||||||
console.log(chalk.blue("Running: 'npx vite build'"));
|
console.log(chalk.blue("$ npx vite build"));
|
||||||
|
|
||||||
const child = child_process.spawn("npx", ["vite", "build"], {
|
const child = child_process.spawn("npx", ["vite", "build"], {
|
||||||
cwd: buildContext.projectDirPath,
|
cwd: buildContext.projectDirPath,
|
||||||
@ -145,7 +145,7 @@ async function appBuild_webpack(params: {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(chalk.blue(`Running: '${subCommand}'`));
|
console.log(chalk.blue(`$ ${subCommand}`));
|
||||||
|
|
||||||
const child = child_process.spawn(command, args, {
|
const child = child_process.spawn(command, args, {
|
||||||
cwd: commandCwd,
|
cwd: commandCwd,
|
||||||
|
@ -20,7 +20,7 @@ export async function keycloakifyBuild(params: {
|
|||||||
|
|
||||||
const dResult = new Deferred<{ isSuccess: boolean }>();
|
const dResult = new Deferred<{ isSuccess: boolean }>();
|
||||||
|
|
||||||
console.log(chalk.blue("Running: 'npx keycloakify build'"));
|
console.log(chalk.blue("$ npx keycloakify build"));
|
||||||
|
|
||||||
const child = child_process.spawn("npx", ["keycloakify", "build"], {
|
const child = child_process.spawn("npx", ["keycloakify", "build"], {
|
||||||
cwd: buildContext.projectDirPath,
|
cwd: buildContext.projectDirPath,
|
||||||
|
@ -4,13 +4,14 @@ import type { CliCommandOptions as CliCommandOptions_common } from "../main";
|
|||||||
import { promptKeycloakVersion } from "../shared/promptKeycloakVersion";
|
import { promptKeycloakVersion } from "../shared/promptKeycloakVersion";
|
||||||
import { ACCOUNT_V1_THEME_NAME, CONTAINER_NAME } from "../shared/constants";
|
import { ACCOUNT_V1_THEME_NAME, CONTAINER_NAME } from "../shared/constants";
|
||||||
import { SemVer } from "../tools/SemVer";
|
import { SemVer } from "../tools/SemVer";
|
||||||
import { assert } from "tsafe/assert";
|
import { assert, type Equals } from "tsafe/assert";
|
||||||
import * as fs from "fs";
|
import * as fs from "fs";
|
||||||
import {
|
import {
|
||||||
join as pathJoin,
|
join as pathJoin,
|
||||||
relative as pathRelative,
|
relative as pathRelative,
|
||||||
sep as pathSep,
|
sep as pathSep,
|
||||||
basename as pathBasename
|
basename as pathBasename,
|
||||||
|
dirname as pathDirname
|
||||||
} from "path";
|
} from "path";
|
||||||
import * as child_process from "child_process";
|
import * as child_process from "child_process";
|
||||||
import chalk from "chalk";
|
import chalk from "chalk";
|
||||||
@ -26,9 +27,10 @@ import { keycloakifyBuild } from "./keycloakifyBuild";
|
|||||||
import { isInside } from "../tools/isInside";
|
import { isInside } from "../tools/isInside";
|
||||||
import { existsAsync } from "../tools/fs.existsAsync";
|
import { existsAsync } from "../tools/fs.existsAsync";
|
||||||
import { rm } from "../tools/fs.rm";
|
import { rm } from "../tools/fs.rm";
|
||||||
|
import { downloadAndExtractArchive } from "../tools/downloadAndExtractArchive";
|
||||||
|
|
||||||
export type CliCommandOptions = CliCommandOptions_common & {
|
export type CliCommandOptions = CliCommandOptions_common & {
|
||||||
port: number;
|
port: number | undefined;
|
||||||
keycloakVersion: string | undefined;
|
keycloakVersion: string | undefined;
|
||||||
realmJsonFilePath: string | undefined;
|
realmJsonFilePath: string | undefined;
|
||||||
};
|
};
|
||||||
@ -88,11 +90,14 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
|
|||||||
|
|
||||||
const buildContext = getBuildContext({ cliCommandOptions });
|
const buildContext = getBuildContext({ cliCommandOptions });
|
||||||
|
|
||||||
const { keycloakVersion } = await (async () => {
|
const { dockerImageTag } = await (async () => {
|
||||||
if (cliCommandOptions.keycloakVersion !== undefined) {
|
if (cliCommandOptions.keycloakVersion !== undefined) {
|
||||||
|
return { dockerImageTag: cliCommandOptions.keycloakVersion };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (buildContext.startKeycloakOptions.dockerImage !== undefined) {
|
||||||
return {
|
return {
|
||||||
keycloakVersion: cliCommandOptions.keycloakVersion,
|
dockerImageTag: buildContext.startKeycloakOptions.dockerImage.tag
|
||||||
keycloakMajorNumber: SemVer.parse(cliCommandOptions.keycloakVersion).major
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -110,15 +115,41 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
|
|||||||
const { keycloakVersion } = await promptKeycloakVersion({
|
const { keycloakVersion } = await promptKeycloakVersion({
|
||||||
startingFromMajor: 18,
|
startingFromMajor: 18,
|
||||||
excludeMajorVersions: [22],
|
excludeMajorVersions: [22],
|
||||||
|
doOmitPatch: true,
|
||||||
buildContext
|
buildContext
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(`→ ${keycloakVersion}`);
|
console.log(`→ ${keycloakVersion}`);
|
||||||
|
|
||||||
return { keycloakVersion };
|
return { dockerImageTag: keycloakVersion };
|
||||||
})();
|
})();
|
||||||
|
|
||||||
const keycloakMajorVersionNumber = SemVer.parse(keycloakVersion).major;
|
const keycloakMajorVersionNumber = (() => {
|
||||||
|
if (buildContext.startKeycloakOptions.dockerImage === undefined) {
|
||||||
|
return SemVer.parse(dockerImageTag).major;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { tag } = buildContext.startKeycloakOptions.dockerImage;
|
||||||
|
|
||||||
|
const [wrap] = [18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28]
|
||||||
|
.map(majorVersionNumber => ({
|
||||||
|
majorVersionNumber,
|
||||||
|
index: tag.indexOf(`${majorVersionNumber}`)
|
||||||
|
}))
|
||||||
|
.filter(({ index }) => index !== -1)
|
||||||
|
.sort((a, b) => a.index - b.index);
|
||||||
|
|
||||||
|
if (wrap === undefined) {
|
||||||
|
console.warn(
|
||||||
|
chalk.yellow(
|
||||||
|
`Could not determine the major Keycloak version number from the docker image tag ${tag}. Assuming 25`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
return 25;
|
||||||
|
}
|
||||||
|
|
||||||
|
return wrap.majorVersionNumber;
|
||||||
|
})();
|
||||||
|
|
||||||
{
|
{
|
||||||
const { isAppBuildSuccess } = await appBuild({
|
const { isAppBuildSuccess } = await appBuild({
|
||||||
@ -157,41 +188,68 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
|
|||||||
|
|
||||||
assert(jarFilePath !== undefined);
|
assert(jarFilePath !== undefined);
|
||||||
|
|
||||||
console.log(`Using ${chalk.bold(pathBasename(jarFilePath))}`);
|
const extensionJarFilePaths = await Promise.all(
|
||||||
|
buildContext.startKeycloakOptions.extensionJars.map(async extensionJar => {
|
||||||
|
switch (extensionJar.type) {
|
||||||
|
case "path": {
|
||||||
|
assert(
|
||||||
|
await existsAsync(extensionJar.path),
|
||||||
|
`${extensionJar.path} does not exist`
|
||||||
|
);
|
||||||
|
return extensionJar.path;
|
||||||
|
}
|
||||||
|
case "url": {
|
||||||
|
const { archiveFilePath } = await downloadAndExtractArchive({
|
||||||
|
cacheDirPath: buildContext.cacheDirPath,
|
||||||
|
fetchOptions: buildContext.fetchOptions,
|
||||||
|
url: extensionJar.url,
|
||||||
|
uniqueIdOfOnArchiveFile: "no extraction",
|
||||||
|
onArchiveFile: async () => {}
|
||||||
|
});
|
||||||
|
return archiveFilePath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert<Equals<typeof extensionJar, never>>(false);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const getRealmJsonFilePath_defaultForKeycloakMajor = (
|
||||||
|
keycloakMajorVersionNumber: number
|
||||||
|
) =>
|
||||||
|
pathJoin(
|
||||||
|
getThisCodebaseRootDirPath(),
|
||||||
|
"src",
|
||||||
|
"bin",
|
||||||
|
"start-keycloak",
|
||||||
|
`myrealm-realm-${keycloakMajorVersionNumber}.json`
|
||||||
|
);
|
||||||
|
|
||||||
const realmJsonFilePath = await (async () => {
|
const realmJsonFilePath = await (async () => {
|
||||||
if (cliCommandOptions.realmJsonFilePath !== undefined) {
|
if (cliCommandOptions.realmJsonFilePath !== undefined) {
|
||||||
if (cliCommandOptions.realmJsonFilePath === "none") {
|
if (cliCommandOptions.realmJsonFilePath === "none") {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(
|
|
||||||
chalk.green(
|
|
||||||
`Using realm json file: ${cliCommandOptions.realmJsonFilePath}`
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
return getAbsoluteAndInOsFormatPath({
|
return getAbsoluteAndInOsFormatPath({
|
||||||
pathIsh: cliCommandOptions.realmJsonFilePath,
|
pathIsh: cliCommandOptions.realmJsonFilePath,
|
||||||
cwd: process.cwd()
|
cwd: process.cwd()
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (buildContext.startKeycloakOptions.realmJsonFilePath !== undefined) {
|
||||||
|
assert(
|
||||||
|
await existsAsync(buildContext.startKeycloakOptions.realmJsonFilePath),
|
||||||
|
`${pathRelative(process.cwd(), buildContext.startKeycloakOptions.realmJsonFilePath)} does not exist`
|
||||||
|
);
|
||||||
|
return buildContext.startKeycloakOptions.realmJsonFilePath;
|
||||||
|
}
|
||||||
|
|
||||||
const internalFilePath = await (async () => {
|
const internalFilePath = await (async () => {
|
||||||
const dirPath = pathJoin(
|
const defaultFilePath = getRealmJsonFilePath_defaultForKeycloakMajor(
|
||||||
getThisCodebaseRootDirPath(),
|
keycloakMajorVersionNumber
|
||||||
"src",
|
|
||||||
"bin",
|
|
||||||
"start-keycloak"
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const filePath = pathJoin(
|
if (fs.existsSync(defaultFilePath)) {
|
||||||
dirPath,
|
return defaultFilePath;
|
||||||
`myrealm-realm-${keycloakMajorVersionNumber}.json`
|
|
||||||
);
|
|
||||||
|
|
||||||
if (fs.existsSync(filePath)) {
|
|
||||||
return filePath;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
@ -202,6 +260,8 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
|
|||||||
|
|
||||||
console.log(chalk.cyan("Select what configuration to use:"));
|
console.log(chalk.cyan("Select what configuration to use:"));
|
||||||
|
|
||||||
|
const dirPath = pathDirname(defaultFilePath);
|
||||||
|
|
||||||
const { value } = await cliSelect<string>({
|
const { value } = await cliSelect<string>({
|
||||||
values: [
|
values: [
|
||||||
...fs
|
...fs
|
||||||
@ -243,6 +303,40 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
|
|||||||
return filePath;
|
return filePath;
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
add_test_user_if_missing: {
|
||||||
|
if (realmJsonFilePath === undefined) {
|
||||||
|
break add_test_user_if_missing;
|
||||||
|
}
|
||||||
|
|
||||||
|
const realm: Record<string, unknown> = JSON.parse(
|
||||||
|
fs.readFileSync(realmJsonFilePath).toString("utf8")
|
||||||
|
);
|
||||||
|
|
||||||
|
if (realm.users !== undefined) {
|
||||||
|
break add_test_user_if_missing;
|
||||||
|
}
|
||||||
|
|
||||||
|
const realmJsonFilePath_internal = (() => {
|
||||||
|
const filePath = getRealmJsonFilePath_defaultForKeycloakMajor(
|
||||||
|
keycloakMajorVersionNumber
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!fs.existsSync(filePath)) {
|
||||||
|
return getRealmJsonFilePath_defaultForKeycloakMajor(25);
|
||||||
|
}
|
||||||
|
|
||||||
|
return filePath;
|
||||||
|
})();
|
||||||
|
|
||||||
|
const users = JSON.parse(
|
||||||
|
fs.readFileSync(realmJsonFilePath_internal).toString("utf8")
|
||||||
|
).users;
|
||||||
|
|
||||||
|
realm.users = users;
|
||||||
|
|
||||||
|
fs.writeFileSync(realmJsonFilePath, JSON.stringify(realm, null, 2), "utf8");
|
||||||
|
}
|
||||||
|
|
||||||
async function extractThemeResourcesFromJar() {
|
async function extractThemeResourcesFromJar() {
|
||||||
await extractArchive({
|
await extractArchive({
|
||||||
archiveFilePath: jarFilePath,
|
archiveFilePath: jarFilePath,
|
||||||
@ -281,77 +375,105 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
|
|||||||
});
|
});
|
||||||
} catch {}
|
} catch {}
|
||||||
|
|
||||||
const spawnArgs = [
|
const DEFAULT_PORT = 8080;
|
||||||
"docker",
|
const port =
|
||||||
[
|
cliCommandOptions.port ?? buildContext.startKeycloakOptions.port ?? DEFAULT_PORT;
|
||||||
"run",
|
|
||||||
...["-p", `${cliCommandOptions.port}:8080`],
|
|
||||||
...["--name", CONTAINER_NAME],
|
|
||||||
...["-e", "KEYCLOAK_ADMIN=admin"],
|
|
||||||
...["-e", "KEYCLOAK_ADMIN_PASSWORD=admin"],
|
|
||||||
...(realmJsonFilePath === undefined
|
|
||||||
? []
|
|
||||||
: [
|
|
||||||
"-v",
|
|
||||||
`"${realmJsonFilePath}":/opt/keycloak/data/import/myrealm-realm.json`
|
|
||||||
]),
|
|
||||||
...[
|
|
||||||
"-v",
|
|
||||||
`"${jarFilePath_cacheDir}":/opt/keycloak/providers/keycloak-theme.jar`
|
|
||||||
],
|
|
||||||
...(keycloakMajorVersionNumber <= 20
|
|
||||||
? ["-e", "JAVA_OPTS=-Dkeycloak.profile=preview"]
|
|
||||||
: []),
|
|
||||||
...[
|
|
||||||
...buildContext.themeNames,
|
|
||||||
...(fs.existsSync(
|
|
||||||
pathJoin(
|
|
||||||
buildContext.keycloakifyBuildDirPath,
|
|
||||||
"theme",
|
|
||||||
ACCOUNT_V1_THEME_NAME
|
|
||||||
)
|
|
||||||
)
|
|
||||||
? [ACCOUNT_V1_THEME_NAME]
|
|
||||||
: [])
|
|
||||||
]
|
|
||||||
.map(themeName => ({
|
|
||||||
localDirPath: pathJoin(
|
|
||||||
buildContext.keycloakifyBuildDirPath,
|
|
||||||
"theme",
|
|
||||||
themeName
|
|
||||||
),
|
|
||||||
containerDirPath: `/opt/keycloak/themes/${themeName}`
|
|
||||||
}))
|
|
||||||
.map(({ localDirPath, containerDirPath }) => [
|
|
||||||
"-v",
|
|
||||||
`"${localDirPath}":${containerDirPath}:rw`
|
|
||||||
])
|
|
||||||
.flat(),
|
|
||||||
...buildContext.environmentVariables
|
|
||||||
.map(({ name }) => ({ name, envValue: process.env[name] }))
|
|
||||||
.map(({ name, envValue }) =>
|
|
||||||
envValue === undefined ? undefined : { name, envValue }
|
|
||||||
)
|
|
||||||
.filter(exclude(undefined))
|
|
||||||
.map(({ name, envValue }) => [
|
|
||||||
"--env",
|
|
||||||
`${name}='${envValue.replace(/'/g, "'\\''")}'`
|
|
||||||
])
|
|
||||||
.flat(),
|
|
||||||
`quay.io/keycloak/keycloak:${keycloakVersion}`,
|
|
||||||
"start-dev",
|
|
||||||
...(21 <= keycloakMajorVersionNumber && keycloakMajorVersionNumber < 24
|
|
||||||
? ["--features=declarative-user-profile"]
|
|
||||||
: []),
|
|
||||||
...(realmJsonFilePath === undefined ? [] : ["--import-realm"])
|
|
||||||
],
|
|
||||||
{
|
|
||||||
cwd: buildContext.keycloakifyBuildDirPath,
|
|
||||||
shell: true
|
|
||||||
}
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
const child = child_process.spawn(...spawnArgs);
|
const SPACE_PLACEHOLDER = "SPACE_PLACEHOLDER_xKLmdPd";
|
||||||
|
|
||||||
|
const dockerRunArgs: string[] = [
|
||||||
|
`-p${SPACE_PLACEHOLDER}${port}:8080`,
|
||||||
|
`--name${SPACE_PLACEHOLDER}${CONTAINER_NAME}`,
|
||||||
|
`-e${SPACE_PLACEHOLDER}KEYCLOAK_ADMIN=admin`,
|
||||||
|
`-e${SPACE_PLACEHOLDER}KEYCLOAK_ADMIN_PASSWORD=admin`,
|
||||||
|
...(buildContext.startKeycloakOptions.dockerExtraArgs.length === 0
|
||||||
|
? []
|
||||||
|
: [
|
||||||
|
buildContext.startKeycloakOptions.dockerExtraArgs.join(
|
||||||
|
SPACE_PLACEHOLDER
|
||||||
|
)
|
||||||
|
]),
|
||||||
|
...(realmJsonFilePath === undefined
|
||||||
|
? []
|
||||||
|
: [
|
||||||
|
`-v${SPACE_PLACEHOLDER}".${pathSep}${pathRelative(process.cwd(), realmJsonFilePath)}":/opt/keycloak/data/import/myrealm-realm.json`
|
||||||
|
]),
|
||||||
|
`-v${SPACE_PLACEHOLDER}".${pathSep}${pathRelative(process.cwd(), jarFilePath_cacheDir)}":/opt/keycloak/providers/keycloak-theme.jar`,
|
||||||
|
...extensionJarFilePaths.map(
|
||||||
|
jarFilePath =>
|
||||||
|
`-v${SPACE_PLACEHOLDER}".${pathSep}${pathRelative(process.cwd(), jarFilePath)}":/opt/keycloak/providers/${pathBasename(jarFilePath)}`
|
||||||
|
),
|
||||||
|
...(keycloakMajorVersionNumber <= 20
|
||||||
|
? [`-e${SPACE_PLACEHOLDER}JAVA_OPTS=-Dkeycloak.profile=preview`]
|
||||||
|
: []),
|
||||||
|
...[
|
||||||
|
...buildContext.themeNames,
|
||||||
|
...(fs.existsSync(
|
||||||
|
pathJoin(
|
||||||
|
buildContext.keycloakifyBuildDirPath,
|
||||||
|
"theme",
|
||||||
|
ACCOUNT_V1_THEME_NAME
|
||||||
|
)
|
||||||
|
)
|
||||||
|
? [ACCOUNT_V1_THEME_NAME]
|
||||||
|
: [])
|
||||||
|
]
|
||||||
|
.map(themeName => ({
|
||||||
|
localDirPath: pathJoin(
|
||||||
|
buildContext.keycloakifyBuildDirPath,
|
||||||
|
"theme",
|
||||||
|
themeName
|
||||||
|
),
|
||||||
|
containerDirPath: `/opt/keycloak/themes/${themeName}`
|
||||||
|
}))
|
||||||
|
.map(
|
||||||
|
({ localDirPath, containerDirPath }) =>
|
||||||
|
`-v${SPACE_PLACEHOLDER}".${pathSep}${pathRelative(process.cwd(), localDirPath)}":${containerDirPath}:rw`
|
||||||
|
),
|
||||||
|
...buildContext.environmentVariables
|
||||||
|
.map(({ name }) => ({ name, envValue: process.env[name] }))
|
||||||
|
.map(({ name, envValue }) =>
|
||||||
|
envValue === undefined ? undefined : { name, envValue }
|
||||||
|
)
|
||||||
|
.filter(exclude(undefined))
|
||||||
|
.map(
|
||||||
|
({ name, envValue }) =>
|
||||||
|
`--env${SPACE_PLACEHOLDER}${name}='${envValue.replace(/'/g, "'\\''")}'`
|
||||||
|
),
|
||||||
|
`${buildContext.startKeycloakOptions.dockerImage?.reference ?? "quay.io/keycloak/keycloak"}:${dockerImageTag}`,
|
||||||
|
"start-dev",
|
||||||
|
...(21 <= keycloakMajorVersionNumber && keycloakMajorVersionNumber < 24
|
||||||
|
? ["--features=declarative-user-profile"]
|
||||||
|
: []),
|
||||||
|
...(realmJsonFilePath === undefined ? [] : ["--import-realm"]),
|
||||||
|
...(buildContext.startKeycloakOptions.keycloakExtraArgs.length === 0
|
||||||
|
? []
|
||||||
|
: [
|
||||||
|
buildContext.startKeycloakOptions.keycloakExtraArgs.join(
|
||||||
|
SPACE_PLACEHOLDER
|
||||||
|
)
|
||||||
|
])
|
||||||
|
];
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
chalk.blue(
|
||||||
|
[
|
||||||
|
`$ docker run \\`,
|
||||||
|
...dockerRunArgs
|
||||||
|
.map(arg => arg.replace(new RegExp(SPACE_PLACEHOLDER, "g"), " "))
|
||||||
|
.map(
|
||||||
|
(line, i, arr) =>
|
||||||
|
` ${line}${arr.length - 1 === i ? "" : " \\"}`
|
||||||
|
)
|
||||||
|
].join("\n")
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const child = child_process.spawn(
|
||||||
|
"docker",
|
||||||
|
["run", ...dockerRunArgs.map(line => line.split(SPACE_PLACEHOLDER)).flat()],
|
||||||
|
{ shell: true }
|
||||||
|
);
|
||||||
|
|
||||||
child.stdout.on("data", data => process.stdout.write(data));
|
child.stdout.on("data", data => process.stdout.write(data));
|
||||||
|
|
||||||
@ -362,6 +484,18 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
|
|||||||
const srcDirPath = pathJoin(buildContext.projectDirPath, "src");
|
const srcDirPath = pathJoin(buildContext.projectDirPath, "src");
|
||||||
|
|
||||||
{
|
{
|
||||||
|
const kcHttpRelativePath = (() => {
|
||||||
|
const match = buildContext.startKeycloakOptions.dockerExtraArgs
|
||||||
|
.join(" ")
|
||||||
|
.match(/KC_HTTP_RELATIVE_PATH=([^ ]+)/);
|
||||||
|
|
||||||
|
if (match === null) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return match[1];
|
||||||
|
})();
|
||||||
|
|
||||||
const handler = async (data: Buffer) => {
|
const handler = async (data: Buffer) => {
|
||||||
if (!data.toString("utf8").includes("Listening on: http://0.0.0.0:8080")) {
|
if (!data.toString("utf8").includes("Listening on: http://0.0.0.0:8080")) {
|
||||||
return;
|
return;
|
||||||
@ -379,7 +513,7 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
|
|||||||
)} are mounted in the Keycloak container.`,
|
)} are mounted in the Keycloak container.`,
|
||||||
"",
|
"",
|
||||||
`Keycloak Admin console: ${chalk.cyan.bold(
|
`Keycloak Admin console: ${chalk.cyan.bold(
|
||||||
`http://localhost:${cliCommandOptions.port}`
|
`http://localhost:${port}${kcHttpRelativePath ?? ""}`
|
||||||
)}`,
|
)}`,
|
||||||
`- user: ${chalk.cyan.bold("admin")}`,
|
`- user: ${chalk.cyan.bold("admin")}`,
|
||||||
`- password: ${chalk.cyan.bold("admin")}`,
|
`- password: ${chalk.cyan.bold("admin")}`,
|
||||||
@ -387,7 +521,21 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
|
|||||||
"",
|
"",
|
||||||
`${chalk.green("Your theme is accessible at:")}`,
|
`${chalk.green("Your theme is accessible at:")}`,
|
||||||
`${chalk.green("➜")} ${chalk.cyan.bold(
|
`${chalk.green("➜")} ${chalk.cyan.bold(
|
||||||
`https://my-theme.keycloakify.dev${cliCommandOptions.port === 8080 ? "" : `?port=${cliCommandOptions.port}`}`
|
(() => {
|
||||||
|
const url = new URL("https://my-theme.keycloakify.dev");
|
||||||
|
|
||||||
|
if (port !== DEFAULT_PORT) {
|
||||||
|
url.searchParams.set("port", `${port}`);
|
||||||
|
}
|
||||||
|
if (kcHttpRelativePath !== undefined) {
|
||||||
|
url.searchParams.set(
|
||||||
|
"kcHttpRelativePath",
|
||||||
|
kcHttpRelativePath
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return url.href;
|
||||||
|
})()
|
||||||
)}`,
|
)}`,
|
||||||
"",
|
"",
|
||||||
"You can login with the following credentials:",
|
"You can login with the following credentials:",
|
||||||
|
@ -4,7 +4,6 @@ import { dirname as pathDirname, join as pathJoin } from "path";
|
|||||||
import { assert } from "tsafe/assert";
|
import { assert } from "tsafe/assert";
|
||||||
import { extractArchive } from "./extractArchive";
|
import { extractArchive } from "./extractArchive";
|
||||||
import { existsAsync } from "./fs.existsAsync";
|
import { existsAsync } from "./fs.existsAsync";
|
||||||
|
|
||||||
import * as crypto from "crypto";
|
import * as crypto from "crypto";
|
||||||
import { rm } from "./fs.rm";
|
import { rm } from "./fs.rm";
|
||||||
|
|
||||||
@ -21,7 +20,7 @@ export async function downloadAndExtractArchive(params: {
|
|||||||
}) => Promise<void>;
|
}) => Promise<void>;
|
||||||
cacheDirPath: string;
|
cacheDirPath: string;
|
||||||
fetchOptions: FetchOptions | undefined;
|
fetchOptions: FetchOptions | undefined;
|
||||||
}): Promise<{ extractedDirPath: string }> {
|
}): Promise<{ extractedDirPath: string; archiveFilePath: string }> {
|
||||||
const { url, uniqueIdOfOnArchiveFile, onArchiveFile, cacheDirPath, fetchOptions } =
|
const { url, uniqueIdOfOnArchiveFile, onArchiveFile, cacheDirPath, fetchOptions } =
|
||||||
params;
|
params;
|
||||||
|
|
||||||
@ -30,6 +29,8 @@ export async function downloadAndExtractArchive(params: {
|
|||||||
const archiveFilePath = pathJoin(cacheDirPath, archiveFileBasename);
|
const archiveFilePath = pathJoin(cacheDirPath, archiveFileBasename);
|
||||||
|
|
||||||
download: {
|
download: {
|
||||||
|
await mkdir(pathDirname(archiveFilePath), { recursive: true });
|
||||||
|
|
||||||
if (await existsAsync(archiveFilePath)) {
|
if (await existsAsync(archiveFilePath)) {
|
||||||
const isDownloaded = await SuccessTracker.getIsDownloaded({
|
const isDownloaded = await SuccessTracker.getIsDownloaded({
|
||||||
cacheDirPath,
|
cacheDirPath,
|
||||||
@ -48,8 +49,6 @@ export async function downloadAndExtractArchive(params: {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
await mkdir(pathDirname(archiveFilePath), { recursive: true });
|
|
||||||
|
|
||||||
const response = await fetch(url, fetchOptions);
|
const response = await fetch(url, fetchOptions);
|
||||||
|
|
||||||
response.body?.setMaxListeners(Number.MAX_VALUE);
|
response.body?.setMaxListeners(Number.MAX_VALUE);
|
||||||
@ -136,7 +135,7 @@ export async function downloadAndExtractArchive(params: {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return { extractedDirPath };
|
return { extractedDirPath, archiveFilePath };
|
||||||
}
|
}
|
||||||
|
|
||||||
type SuccessTracker = {
|
type SuccessTracker = {
|
||||||
|
@ -40,6 +40,8 @@ const LoginRecoveryAuthnCodeInput = lazy(() => import("keycloakify/login/pages/L
|
|||||||
const LoginResetOtp = lazy(() => import("keycloakify/login/pages/LoginResetOtp"));
|
const LoginResetOtp = lazy(() => import("keycloakify/login/pages/LoginResetOtp"));
|
||||||
const LoginX509Info = lazy(() => import("keycloakify/login/pages/LoginX509Info"));
|
const LoginX509Info = lazy(() => import("keycloakify/login/pages/LoginX509Info"));
|
||||||
const WebauthnError = lazy(() => import("keycloakify/login/pages/WebauthnError"));
|
const WebauthnError = lazy(() => import("keycloakify/login/pages/WebauthnError"));
|
||||||
|
const LoginPasskeysConditionalAuthenticate = lazy(() => import("keycloakify/login/pages/LoginPasskeysConditionalAuthenticate"));
|
||||||
|
const LoginIdpLinkConfirmOverride = lazy(() => import("keycloakify/login/pages/LoginIdpLinkConfirmOverride"));
|
||||||
|
|
||||||
type DefaultPageProps = PageProps<KcContext, I18n> & {
|
type DefaultPageProps = PageProps<KcContext, I18n> & {
|
||||||
UserProfileFormFields: LazyOrNot<(props: UserProfileFormFieldsProps) => JSX.Element>;
|
UserProfileFormFields: LazyOrNot<(props: UserProfileFormFieldsProps) => JSX.Element>;
|
||||||
@ -121,6 +123,10 @@ export default function DefaultPage(props: DefaultPageProps) {
|
|||||||
return <LoginX509Info kcContext={kcContext} {...rest} />;
|
return <LoginX509Info kcContext={kcContext} {...rest} />;
|
||||||
case "webauthn-error.ftl":
|
case "webauthn-error.ftl":
|
||||||
return <WebauthnError kcContext={kcContext} {...rest} />;
|
return <WebauthnError kcContext={kcContext} {...rest} />;
|
||||||
|
case "login-passkeys-conditional-authenticate.ftl":
|
||||||
|
return <LoginPasskeysConditionalAuthenticate kcContext={kcContext} {...rest} />;
|
||||||
|
case "login-idp-link-confirm-override.ftl":
|
||||||
|
return <LoginIdpLinkConfirmOverride kcContext={kcContext} {...rest} />;
|
||||||
}
|
}
|
||||||
assert<Equals<typeof kcContext, never>>(false);
|
assert<Equals<typeof kcContext, never>>(false);
|
||||||
})()}
|
})()}
|
||||||
|
@ -59,7 +59,9 @@ export type KcContext =
|
|||||||
| KcContext.LoginRecoveryAuthnCodeInput
|
| KcContext.LoginRecoveryAuthnCodeInput
|
||||||
| KcContext.LoginResetOtp
|
| KcContext.LoginResetOtp
|
||||||
| KcContext.LoginX509Info
|
| KcContext.LoginX509Info
|
||||||
| KcContext.WebauthnError;
|
| KcContext.WebauthnError
|
||||||
|
| KcContext.LoginPasskeysConditionalAuthenticate
|
||||||
|
| KcContext.LoginIdpLinkConfirmOverride;
|
||||||
|
|
||||||
assert<KcContext["themeType"] extends ThemeType ? true : false>();
|
assert<KcContext["themeType"] extends ThemeType ? true : false>();
|
||||||
|
|
||||||
@ -79,8 +81,8 @@ export declare namespace KcContext {
|
|||||||
};
|
};
|
||||||
realm: {
|
realm: {
|
||||||
name: string;
|
name: string;
|
||||||
displayName?: string;
|
displayName: string;
|
||||||
displayNameHtml?: string;
|
displayNameHtml: string;
|
||||||
internationalizationEnabled: boolean;
|
internationalizationEnabled: boolean;
|
||||||
registrationEmailAsUsername: boolean;
|
registrationEmailAsUsername: boolean;
|
||||||
};
|
};
|
||||||
@ -577,6 +579,40 @@ export declare namespace KcContext {
|
|||||||
pageId: "webauthn-error.ftl";
|
pageId: "webauthn-error.ftl";
|
||||||
isAppInitiatedAction?: boolean;
|
isAppInitiatedAction?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type LoginPasskeysConditionalAuthenticate = Common & {
|
||||||
|
pageId: "login-passkeys-conditional-authenticate.ftl";
|
||||||
|
realm: {
|
||||||
|
registrationAllowed: boolean;
|
||||||
|
password: boolean;
|
||||||
|
};
|
||||||
|
url: {
|
||||||
|
registrationUrl: string;
|
||||||
|
};
|
||||||
|
registrationDisabled: boolean;
|
||||||
|
isUserIdentified: boolean | "true" | "false";
|
||||||
|
challenge: string;
|
||||||
|
userVerification: string;
|
||||||
|
rpId: string;
|
||||||
|
createTimeout: number | string;
|
||||||
|
|
||||||
|
authenticators?: {
|
||||||
|
authenticators: WebauthnAuthenticate.WebauthnAuthenticator[];
|
||||||
|
};
|
||||||
|
shouldDisplayAuthenticators?: boolean;
|
||||||
|
usernameHidden?: boolean;
|
||||||
|
login: {
|
||||||
|
username?: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type LoginIdpLinkConfirmOverride = Common & {
|
||||||
|
pageId: "login-idp-link-confirm-override.ftl";
|
||||||
|
url: {
|
||||||
|
loginRestartFlowUrl: string;
|
||||||
|
};
|
||||||
|
idpDisplayName: string;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UserProfile = {
|
export type UserProfile = {
|
||||||
|
@ -567,6 +567,39 @@ export const kcContextMocks = [
|
|||||||
pageId: "webauthn-error.ftl",
|
pageId: "webauthn-error.ftl",
|
||||||
...kcContextCommonMock,
|
...kcContextCommonMock,
|
||||||
isAppInitiatedAction: true
|
isAppInitiatedAction: true
|
||||||
|
}),
|
||||||
|
id<KcContext.LoginPasskeysConditionalAuthenticate>({
|
||||||
|
pageId: "login-passkeys-conditional-authenticate.ftl",
|
||||||
|
...kcContextCommonMock,
|
||||||
|
url: {
|
||||||
|
...kcContextCommonMock.url,
|
||||||
|
registrationUrl: "#"
|
||||||
|
},
|
||||||
|
realm: {
|
||||||
|
...kcContextCommonMock.realm,
|
||||||
|
password: true,
|
||||||
|
registrationAllowed: true
|
||||||
|
},
|
||||||
|
registrationDisabled: false,
|
||||||
|
isUserIdentified: "false",
|
||||||
|
challenge: "",
|
||||||
|
userVerification: "not specified",
|
||||||
|
rpId: "",
|
||||||
|
createTimeout: "0",
|
||||||
|
authenticators: {
|
||||||
|
authenticators: []
|
||||||
|
},
|
||||||
|
shouldDisplayAuthenticators: false,
|
||||||
|
login: {}
|
||||||
|
}),
|
||||||
|
id<KcContext.LoginIdpLinkConfirmOverride>({
|
||||||
|
pageId: "login-idp-link-confirm-override.ftl",
|
||||||
|
...kcContextCommonMock,
|
||||||
|
url: {
|
||||||
|
...kcContextCommonMock.url,
|
||||||
|
loginRestartFlowUrl: "#"
|
||||||
|
},
|
||||||
|
idpDisplayName: "Google"
|
||||||
})
|
})
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -224,19 +224,17 @@ export default function Template(props: TemplateProps<KcContext, I18n>) {
|
|||||||
{auth !== undefined && auth.showTryAnotherWayLink && (
|
{auth !== undefined && auth.showTryAnotherWayLink && (
|
||||||
<form id="kc-select-try-another-way-form" action={url.loginAction} method="post">
|
<form id="kc-select-try-another-way-form" action={url.loginAction} method="post">
|
||||||
<div className={kcClsx("kcFormGroupClass")}>
|
<div className={kcClsx("kcFormGroupClass")}>
|
||||||
<div className={kcClsx("kcFormGroupClass")}>
|
<input type="hidden" name="tryAnotherWay" value="on" />
|
||||||
<input type="hidden" name="tryAnotherWay" value="on" />
|
<a
|
||||||
<a
|
href="#"
|
||||||
href="#"
|
id="try-another-way"
|
||||||
id="try-another-way"
|
onClick={() => {
|
||||||
onClick={() => {
|
document.forms["kc-select-try-another-way-form" as never].submit();
|
||||||
document.forms["kc-select-try-another-way-form" as never].submit();
|
return false;
|
||||||
return false;
|
}}
|
||||||
}}
|
>
|
||||||
>
|
{msg("doTryAnotherWay")}
|
||||||
{msg("doTryAnotherWay")}
|
</a>
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
)}
|
)}
|
||||||
|
@ -58,7 +58,7 @@ export default function UserProfileFormFields(props: UserProfileFormFieldsProps<
|
|||||||
<label htmlFor={attribute.name} className={kcClsx("kcLabelClass")}>
|
<label htmlFor={attribute.name} className={kcClsx("kcLabelClass")}>
|
||||||
{advancedMsg(attribute.displayName ?? "")}
|
{advancedMsg(attribute.displayName ?? "")}
|
||||||
</label>
|
</label>
|
||||||
{attribute.required && <>*</>}
|
{attribute.required && <> *</>}
|
||||||
</div>
|
</div>
|
||||||
<div className={kcClsx("kcInputWrapperClass")}>
|
<div className={kcClsx("kcInputWrapperClass")}>
|
||||||
{attribute.annotations.inputHelperTextBefore !== undefined && (
|
{attribute.annotations.inputHelperTextBefore !== undefined && (
|
||||||
@ -580,7 +580,7 @@ function TextareaTag(props: InputFieldByTypeProps) {
|
|||||||
function SelectTag(props: InputFieldByTypeProps) {
|
function SelectTag(props: InputFieldByTypeProps) {
|
||||||
const { attribute, dispatchFormAction, kcClsx, displayableErrors, i18n, valueOrValues } = props;
|
const { attribute, dispatchFormAction, kcClsx, displayableErrors, i18n, valueOrValues } = props;
|
||||||
|
|
||||||
const { advancedMsg } = i18n;
|
const { advancedMsgStr } = i18n;
|
||||||
|
|
||||||
const isMultiple = attribute.annotations.inputType === "multiselect";
|
const isMultiple = attribute.annotations.inputType === "multiselect";
|
||||||
|
|
||||||
@ -649,11 +649,11 @@ function SelectTag(props: InputFieldByTypeProps) {
|
|||||||
if (attribute.annotations.inputOptionLabels !== undefined) {
|
if (attribute.annotations.inputOptionLabels !== undefined) {
|
||||||
const { inputOptionLabels } = attribute.annotations;
|
const { inputOptionLabels } = attribute.annotations;
|
||||||
|
|
||||||
return advancedMsg(inputOptionLabels[option] ?? option);
|
return advancedMsgStr(inputOptionLabels[option] ?? option);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (attribute.annotations.inputOptionLabelsI18nPrefix !== undefined) {
|
if (attribute.annotations.inputOptionLabelsI18nPrefix !== undefined) {
|
||||||
return advancedMsg(`${attribute.annotations.inputOptionLabelsI18nPrefix}.${option}`);
|
return advancedMsgStr(`${attribute.annotations.inputOptionLabelsI18nPrefix}.${option}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return option;
|
return option;
|
||||||
|
@ -217,7 +217,13 @@ function createI18nTranslationFunctionsFactory<MessageKey_themeDefined extends s
|
|||||||
|
|
||||||
messageWithArgsInjected = messageWithArgsInjected.replace(
|
messageWithArgsInjected = messageWithArgsInjected.replace(
|
||||||
new RegExp(`\\{${i + startIndex}\\}`, "g"),
|
new RegExp(`\\{${i + startIndex}\\}`, "g"),
|
||||||
arg.replace(/</g, "<").replace(/>/g, ">")
|
(() => {
|
||||||
|
if (key === "loginTitleHtml") {
|
||||||
|
return arg;
|
||||||
|
}
|
||||||
|
|
||||||
|
return arg.replace(/</g, "<").replace(/>/g, ">");
|
||||||
|
})()
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1394,14 +1394,10 @@ export function getButtonToDisplayForMultivaluedAttributeField(params: { attribu
|
|||||||
})();
|
})();
|
||||||
|
|
||||||
if (maxCount === undefined) {
|
if (maxCount === undefined) {
|
||||||
return false;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (values.length === maxCount) {
|
return values.length !== maxCount;
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
})();
|
})();
|
||||||
|
|
||||||
return { hasRemove, hasAdd };
|
return { hasRemove, hasAdd };
|
||||||
|
@ -43,7 +43,7 @@ export default function Login(props: PageProps<Extract<KcContext, { pageId: "log
|
|||||||
}
|
}
|
||||||
socialProvidersNode={
|
socialProvidersNode={
|
||||||
<>
|
<>
|
||||||
{realm.password && social.providers?.length && (
|
{realm.password && social.providers !== undefined && social.providers.length !== 0 && (
|
||||||
<div id="kc-social-providers" className={kcClsx("kcFormSocialAccountSectionClass")}>
|
<div id="kc-social-providers" className={kcClsx("kcFormSocialAccountSectionClass")}>
|
||||||
<hr />
|
<hr />
|
||||||
<h2>{msg("identity-provider-login-label")}</h2>
|
<h2>{msg("identity-provider-login-label")}</h2>
|
||||||
|
40
src/login/pages/LoginIdpLinkConfirmOverride.tsx
Normal file
40
src/login/pages/LoginIdpLinkConfirmOverride.tsx
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import { getKcClsx } from "keycloakify/login/lib/kcClsx";
|
||||||
|
import type { PageProps } from "keycloakify/login/pages/PageProps";
|
||||||
|
import type { KcContext } from "../KcContext";
|
||||||
|
import type { I18n } from "../i18n";
|
||||||
|
|
||||||
|
// NOTE: Added with Keycloak 25
|
||||||
|
export default function LoginIdpLinkConfirmOverride(props: PageProps<Extract<KcContext, { pageId: "login-idp-link-confirm-override.ftl" }>, I18n>) {
|
||||||
|
const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
|
||||||
|
|
||||||
|
const { kcClsx } = getKcClsx({
|
||||||
|
doUseDefaultCss,
|
||||||
|
classes
|
||||||
|
});
|
||||||
|
|
||||||
|
const { url, idpDisplayName } = kcContext;
|
||||||
|
|
||||||
|
const { msg } = i18n;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Template kcContext={kcContext} i18n={i18n} doUseDefaultCss={doUseDefaultCss} classes={classes} headerNode={msg("confirmOverrideIdpTitle")}>
|
||||||
|
<form id="kc-register-form" action={url.loginAction} method="post">
|
||||||
|
{msg("pageExpiredMsg1")}{" "}
|
||||||
|
<a id="loginRestartLink" href={url.loginRestartFlowUrl}>
|
||||||
|
{msg("doClickHere")}
|
||||||
|
</a>
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className={kcClsx("kcButtonClass", "kcButtonDefaultClass", "kcButtonBlockClass", "kcButtonLargeClass")}
|
||||||
|
name="submitAction"
|
||||||
|
id="confirmOverride"
|
||||||
|
value="confirmOverride"
|
||||||
|
>
|
||||||
|
{msg("confirmOverrideIdpContinue", idpDisplayName)}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</Template>
|
||||||
|
);
|
||||||
|
}
|
252
src/login/pages/LoginPasskeysConditionalAuthenticate.tsx
Normal file
252
src/login/pages/LoginPasskeysConditionalAuthenticate.tsx
Normal file
@ -0,0 +1,252 @@
|
|||||||
|
import { useEffect, Fragment } from "react";
|
||||||
|
import { clsx } from "keycloakify/tools/clsx";
|
||||||
|
import type { PageProps } from "keycloakify/login/pages/PageProps";
|
||||||
|
import { useInsertScriptTags } from "keycloakify/tools/useInsertScriptTags";
|
||||||
|
import { getKcClsx } from "keycloakify/login/lib/kcClsx";
|
||||||
|
import { assert } from "keycloakify/tools/assert";
|
||||||
|
import type { KcContext } from "../KcContext";
|
||||||
|
import type { I18n } from "../i18n";
|
||||||
|
|
||||||
|
// NOTE: From Keycloak 25.0.4
|
||||||
|
export default function LoginPasskeysConditionalAuthenticate(
|
||||||
|
props: PageProps<Extract<KcContext, { pageId: "login-passkeys-conditional-authenticate.ftl" }>, I18n>
|
||||||
|
) {
|
||||||
|
const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
messagesPerField,
|
||||||
|
login,
|
||||||
|
url,
|
||||||
|
usernameHidden,
|
||||||
|
shouldDisplayAuthenticators,
|
||||||
|
authenticators,
|
||||||
|
registrationDisabled,
|
||||||
|
realm,
|
||||||
|
isUserIdentified,
|
||||||
|
challenge,
|
||||||
|
userVerification,
|
||||||
|
rpId,
|
||||||
|
createTimeout
|
||||||
|
} = kcContext;
|
||||||
|
|
||||||
|
const { msg, msgStr, advancedMsg } = i18n;
|
||||||
|
|
||||||
|
const { kcClsx } = getKcClsx({
|
||||||
|
doUseDefaultCss,
|
||||||
|
classes
|
||||||
|
});
|
||||||
|
|
||||||
|
const { insertScriptTags } = useInsertScriptTags({
|
||||||
|
componentOrHookName: "LoginRecoveryAuthnCodeConfig",
|
||||||
|
scriptTags: [
|
||||||
|
{
|
||||||
|
type: "module",
|
||||||
|
textContent: `
|
||||||
|
import { authenticateByWebAuthn } from "${url.resourcesPath}/js/webauthnAuthenticate.js";
|
||||||
|
import { initAuthenticate } from "${url.resourcesPath}/js/passkeysConditionalAuth.js";
|
||||||
|
|
||||||
|
const authButton = document.getElementById('authenticateWebAuthnButton');
|
||||||
|
const input = {
|
||||||
|
isUserIdentified : ${isUserIdentified},
|
||||||
|
challenge : '${challenge}',
|
||||||
|
userVerification : '${userVerification}',
|
||||||
|
rpId : '${rpId}',
|
||||||
|
createTimeout : ${createTimeout},
|
||||||
|
errmsg : "${msgStr("webauthn-unsupported-browser-text")}"
|
||||||
|
};
|
||||||
|
authButton.addEventListener("click", () => {
|
||||||
|
authenticateByWebAuthn(input);
|
||||||
|
});
|
||||||
|
|
||||||
|
const args = {
|
||||||
|
isUserIdentified : ${isUserIdentified},
|
||||||
|
challenge : '${challenge}',
|
||||||
|
userVerification : '${userVerification}',
|
||||||
|
rpId : '${rpId}',
|
||||||
|
createTimeout : ${createTimeout},
|
||||||
|
errmsg : "${msgStr("passkey-unsupported-browser-text")}"
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener("DOMContentLoaded", (event) => initAuthenticate(args));
|
||||||
|
`
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
insertScriptTags();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Template
|
||||||
|
kcContext={kcContext}
|
||||||
|
i18n={i18n}
|
||||||
|
doUseDefaultCss={doUseDefaultCss}
|
||||||
|
classes={classes}
|
||||||
|
headerNode={msg("passkey-login-title")}
|
||||||
|
infoNode={
|
||||||
|
realm.registrationAllowed &&
|
||||||
|
!registrationDisabled && (
|
||||||
|
<div id="kc-registration">
|
||||||
|
<span>
|
||||||
|
${msg("noAccount")}{" "}
|
||||||
|
<a tabIndex={6} href={url.registrationUrl}>
|
||||||
|
{msg("doRegister")}
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<form id="webauth" action={url.loginAction} method="post">
|
||||||
|
<input type="hidden" id="clientDataJSON" name="clientDataJSON" />
|
||||||
|
<input type="hidden" id="authenticatorData" name="authenticatorData" />
|
||||||
|
<input type="hidden" id="signature" name="signature" />
|
||||||
|
<input type="hidden" id="credentialId" name="credentialId" />
|
||||||
|
<input type="hidden" id="userHandle" name="userHandle" />
|
||||||
|
<input type="hidden" id="error" name="error" />
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div className={kcClsx("kcFormGroupClass")} no-bottom-margin="true" style={{ marginBottom: 0 }}>
|
||||||
|
{authenticators !== undefined && Object.keys(authenticators).length !== 0 && (
|
||||||
|
<>
|
||||||
|
<form id="authn_select" className={kcClsx("kcFormClass")}>
|
||||||
|
{authenticators.authenticators.map((authenticator, i) => (
|
||||||
|
<input key={i} type="hidden" name="authn_use_chk" readOnly value={authenticator.credentialId} />
|
||||||
|
))}
|
||||||
|
</form>
|
||||||
|
{shouldDisplayAuthenticators && (
|
||||||
|
<>
|
||||||
|
{authenticators.authenticators.length > 1 && (
|
||||||
|
<p className={kcClsx("kcSelectAuthListItemTitle")}>{msg("passkey-available-authenticators")}</p>
|
||||||
|
)}
|
||||||
|
<div className={kcClsx("kcFormClass")}>
|
||||||
|
{authenticators.authenticators.map((authenticator, i) => (
|
||||||
|
<div key={i} id={`kc-webauthn-authenticator-item-${i}`} className={kcClsx("kcSelectAuthListItemClass")}>
|
||||||
|
<i
|
||||||
|
className={clsx(
|
||||||
|
(() => {
|
||||||
|
const className = kcClsx(authenticator.transports.iconClass as any);
|
||||||
|
if (className === authenticator.transports.iconClass) {
|
||||||
|
return kcClsx("kcWebAuthnDefaultIcon");
|
||||||
|
}
|
||||||
|
return className;
|
||||||
|
})(),
|
||||||
|
kcClsx("kcSelectAuthListItemIconPropertyClass")
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<div className={kcClsx("kcSelectAuthListItemBodyClass")}>
|
||||||
|
<div
|
||||||
|
id={`kc-webauthn-authenticator-label-${i}`}
|
||||||
|
className={kcClsx("kcSelectAuthListItemHeadingClass")}
|
||||||
|
>
|
||||||
|
{advancedMsg(authenticator.label)}
|
||||||
|
</div>
|
||||||
|
{authenticator.transports !== undefined &&
|
||||||
|
authenticator.transports.displayNameProperties !== undefined &&
|
||||||
|
authenticator.transports.displayNameProperties.length !== 0 && (
|
||||||
|
<div
|
||||||
|
id={`kc-webauthn-authenticator-transport-${i}`}
|
||||||
|
className={kcClsx("kcSelectAuthListItemDescriptionClass")}
|
||||||
|
>
|
||||||
|
{authenticator.transports.displayNameProperties.map((nameProperty, i, arr) => (
|
||||||
|
<Fragment key={i}>
|
||||||
|
<span key={i}> {advancedMsg(nameProperty)} </span>
|
||||||
|
{i !== arr.length - 1 && <span>, </span>}
|
||||||
|
</Fragment>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className={kcClsx("kcSelectAuthListItemDescriptionClass")}>
|
||||||
|
<span id={`kc-webauthn-authenticator-createdlabel-${i}`}>{msg("passkey-createdAt-label")}</span>
|
||||||
|
<span id={`kc-webauthn-authenticator-created-${i}`}>{authenticator.createdAt}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={kcClsx("kcSelectAuthListItemFillClass")} />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<div id="kc-form">
|
||||||
|
<div id="kc-form-wrapper">
|
||||||
|
{realm.password && (
|
||||||
|
<form
|
||||||
|
id="kc-form-passkey"
|
||||||
|
action={url.loginAction}
|
||||||
|
method="post"
|
||||||
|
style={{ display: "none" }}
|
||||||
|
onSubmit={event => {
|
||||||
|
try {
|
||||||
|
// @ts-expect-error
|
||||||
|
event.target.login.disabled = true;
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{!usernameHidden && (
|
||||||
|
<div className={kcClsx("kcFormGroupClass")}>
|
||||||
|
<label htmlFor="username" className={kcClsx("kcLabelClass")}>
|
||||||
|
{msg("passkey-autofill-select")}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
tabIndex={1}
|
||||||
|
id="username"
|
||||||
|
aria-invalid={messagesPerField.existsError("username")}
|
||||||
|
className={kcClsx("kcInputClass")}
|
||||||
|
name="username"
|
||||||
|
defaultValue={login.username ?? ""}
|
||||||
|
//autoComplete="username webauthn"
|
||||||
|
type="text"
|
||||||
|
autoFocus
|
||||||
|
autoComplete="off"
|
||||||
|
/>
|
||||||
|
{messagesPerField.existsError("username") && (
|
||||||
|
<span id="input-error-username" className={kcClsx("kcInputErrorMessageClass")} aria-live="polite">
|
||||||
|
{messagesPerField.get("username")}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
<div id="kc-form-passkey-button" className={kcClsx("kcFormButtonsClass")} style={{ display: "none" }}>
|
||||||
|
<input
|
||||||
|
id="authenticateWebAuthnButton"
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
assert("doAuthenticate" in window);
|
||||||
|
assert(typeof window.doAuthenticate === "function");
|
||||||
|
window.doAuthenticate(
|
||||||
|
[],
|
||||||
|
rpId,
|
||||||
|
challenge,
|
||||||
|
typeof isUserIdentified === "boolean" ? isUserIdentified : isUserIdentified === "true",
|
||||||
|
createTimeout,
|
||||||
|
userVerification,
|
||||||
|
msgStr("passkey-unsupported-browser-text")
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
autoFocus
|
||||||
|
value={msgStr("passkey-doAuthenticate")}
|
||||||
|
className={kcClsx("kcButtonClass", "kcButtonPrimaryClass", "kcButtonBlockClass", "kcButtonLargeClass")}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div id="kc-form-passkey-button" className={kcClsx("kcFormButtonsClass")} style={{ display: "none" }}>
|
||||||
|
<input
|
||||||
|
id="authenticateWebAuthnButton"
|
||||||
|
type="button"
|
||||||
|
autoFocus
|
||||||
|
value={msgStr("passkey-doAuthenticate")}
|
||||||
|
className={kcClsx("kcButtonClass", "kcButtonPrimaryClass", "kcButtonBlockClass", "kcButtonLargeClass")}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Template>
|
||||||
|
);
|
||||||
|
}
|
@ -40,7 +40,7 @@ export default function LoginUsername(props: PageProps<Extract<KcContext, { page
|
|||||||
headerNode={msg("doLogIn")}
|
headerNode={msg("doLogIn")}
|
||||||
socialProvidersNode={
|
socialProvidersNode={
|
||||||
<>
|
<>
|
||||||
{realm.password && social.providers?.length && (
|
{realm.password && social.providers !== undefined && social.providers.length !== 0 && (
|
||||||
<div id="kc-social-providers" className={kcClsx("kcFormSocialAccountSectionClass")}>
|
<div id="kc-social-providers" className={kcClsx("kcFormSocialAccountSectionClass")}>
|
||||||
<hr />
|
<hr />
|
||||||
<h2>{msg("identity-provider-login-label")}</h2>
|
<h2>{msg("identity-provider-login-label")}</h2>
|
||||||
|
@ -153,7 +153,7 @@ export default function WebauthnRegister(props: PageProps<Extract<KcContext, { p
|
|||||||
|
|
||||||
function getPubKeyCredParams(signatureAlgorithmsList) {
|
function getPubKeyCredParams(signatureAlgorithmsList) {
|
||||||
let pubKeyCredParams = [];
|
let pubKeyCredParams = [];
|
||||||
if (signatureAlgorithmsList === []) {
|
if (signatureAlgorithmsList.length === 0) {
|
||||||
pubKeyCredParams.push({type: "public-key", alg: -7});
|
pubKeyCredParams.push({type: "public-key", alg: -7});
|
||||||
return pubKeyCredParams;
|
return pubKeyCredParams;
|
||||||
}
|
}
|
||||||
@ -184,7 +184,7 @@ export default function WebauthnRegister(props: PageProps<Extract<KcContext, { p
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getTransportsAsString(transportsList) {
|
function getTransportsAsString(transportsList) {
|
||||||
if (transportsList === '' || transportsList.constructor !== Array) return "";
|
if (transportsList === '' || Array.isArray(transportsList)) return "";
|
||||||
|
|
||||||
let transportsString = "";
|
let transportsString = "";
|
||||||
|
|
||||||
|
@ -122,73 +122,85 @@ export const WithSocialProviders: Story = {
|
|||||||
loginUrl: "google",
|
loginUrl: "google",
|
||||||
alias: "google",
|
alias: "google",
|
||||||
providerId: "google",
|
providerId: "google",
|
||||||
displayName: "Google"
|
displayName: "Google",
|
||||||
|
iconClasses: "fa fa-google"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
loginUrl: "microsoft",
|
loginUrl: "microsoft",
|
||||||
alias: "microsoft",
|
alias: "microsoft",
|
||||||
providerId: "microsoft",
|
providerId: "microsoft",
|
||||||
displayName: "Microsoft"
|
displayName: "Microsoft",
|
||||||
|
iconClasses: "fa fa-windows"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
loginUrl: "facebook",
|
loginUrl: "facebook",
|
||||||
alias: "facebook",
|
alias: "facebook",
|
||||||
providerId: "facebook",
|
providerId: "facebook",
|
||||||
displayName: "Facebook"
|
displayName: "Facebook",
|
||||||
|
iconClasses: "fa fa-facebook"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
loginUrl: "instagram",
|
loginUrl: "instagram",
|
||||||
alias: "instagram",
|
alias: "instagram",
|
||||||
providerId: "instagram",
|
providerId: "instagram",
|
||||||
displayName: "Instagram"
|
displayName: "Instagram",
|
||||||
|
iconClasses: "fa fa-instagram"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
loginUrl: "twitter",
|
loginUrl: "twitter",
|
||||||
alias: "twitter",
|
alias: "twitter",
|
||||||
providerId: "twitter",
|
providerId: "twitter",
|
||||||
displayName: "Twitter"
|
displayName: "Twitter",
|
||||||
|
iconClasses: "fa fa-twitter"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
loginUrl: "linkedin",
|
loginUrl: "linkedin",
|
||||||
alias: "linkedin",
|
alias: "linkedin",
|
||||||
providerId: "linkedin",
|
providerId: "linkedin",
|
||||||
displayName: "LinkedIn"
|
displayName: "LinkedIn",
|
||||||
|
iconClasses: "fa fa-linkedin"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
loginUrl: "stackoverflow",
|
loginUrl: "stackoverflow",
|
||||||
alias: "stackoverflow",
|
alias: "stackoverflow",
|
||||||
providerId: "stackoverflow",
|
providerId: "stackoverflow",
|
||||||
displayName: "Stackoverflow"
|
displayName: "Stackoverflow",
|
||||||
|
iconClasses: "fa fa-stack-overflow"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
loginUrl: "github",
|
loginUrl: "github",
|
||||||
alias: "github",
|
alias: "github",
|
||||||
providerId: "github",
|
providerId: "github",
|
||||||
displayName: "Github"
|
displayName: "Github",
|
||||||
|
iconClasses: "fa fa-github"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
loginUrl: "gitlab",
|
loginUrl: "gitlab",
|
||||||
alias: "gitlab",
|
alias: "gitlab",
|
||||||
providerId: "gitlab",
|
providerId: "gitlab",
|
||||||
displayName: "Gitlab"
|
displayName: "Gitlab",
|
||||||
|
iconClasses: "fa fa-gitlab"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
loginUrl: "bitbucket",
|
loginUrl: "bitbucket",
|
||||||
alias: "bitbucket",
|
alias: "bitbucket",
|
||||||
providerId: "bitbucket",
|
providerId: "bitbucket",
|
||||||
displayName: "Bitbucket"
|
displayName: "Bitbucket",
|
||||||
|
iconClasses: "fa fa-bitbucket"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
loginUrl: "paypal",
|
loginUrl: "paypal",
|
||||||
alias: "paypal",
|
alias: "paypal",
|
||||||
providerId: "paypal",
|
providerId: "paypal",
|
||||||
displayName: "PayPal"
|
displayName: "PayPal",
|
||||||
|
iconClasses: "fa fa-paypal"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
loginUrl: "openshift",
|
loginUrl: "openshift",
|
||||||
alias: "openshift",
|
alias: "openshift",
|
||||||
providerId: "openshift",
|
providerId: "openshift",
|
||||||
displayName: "OpenShift"
|
displayName: "OpenShift",
|
||||||
|
iconClasses: "fa fa-cloud"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
18
stories/login/pages/LoginIdpLinkConfirmOverride.stories.tsx
Normal file
18
stories/login/pages/LoginIdpLinkConfirmOverride.stories.tsx
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import React from "react";
|
||||||
|
import type { Meta, StoryObj } from "@storybook/react";
|
||||||
|
import { createKcPageStory } from "../KcPageStory";
|
||||||
|
|
||||||
|
const { KcPageStory } = createKcPageStory({ pageId: "login-idp-link-confirm-override.ftl" });
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: "login/login-idp-link-confirm-override.ftl",
|
||||||
|
component: KcPageStory
|
||||||
|
} satisfies Meta<typeof KcPageStory>;
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
|
||||||
|
type Story = StoryObj<typeof meta>;
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
render: () => <KcPageStory />
|
||||||
|
};
|
@ -0,0 +1,18 @@
|
|||||||
|
import React from "react";
|
||||||
|
import type { Meta, StoryObj } from "@storybook/react";
|
||||||
|
import { createKcPageStory } from "../KcPageStory";
|
||||||
|
|
||||||
|
const { KcPageStory } = createKcPageStory({ pageId: "login-passkeys-conditional-authenticate.ftl" });
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: "login/login-passkeys-conditional-authenticate.ftl",
|
||||||
|
component: KcPageStory
|
||||||
|
} satisfies Meta<typeof KcPageStory>;
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
|
||||||
|
type Story = StoryObj<typeof meta>;
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
render: () => <KcPageStory />
|
||||||
|
};
|
@ -4518,7 +4518,7 @@ cheerio-select@^2.1.0:
|
|||||||
domhandler "^5.0.3"
|
domhandler "^5.0.3"
|
||||||
domutils "^3.0.1"
|
domutils "^3.0.1"
|
||||||
|
|
||||||
cheerio@^1.0.0-rc.12:
|
cheerio@1.0.0-rc.12:
|
||||||
version "1.0.0-rc.12"
|
version "1.0.0-rc.12"
|
||||||
resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-1.0.0-rc.12.tgz#788bf7466506b1c6bf5fae51d24a2c4d62e47683"
|
resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-1.0.0-rc.12.tgz#788bf7466506b1c6bf5fae51d24a2c4d62e47683"
|
||||||
integrity sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q==
|
integrity sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q==
|
||||||
@ -11332,7 +11332,7 @@ typedarray@^0.0.6:
|
|||||||
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
|
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
|
||||||
integrity sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==
|
integrity sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==
|
||||||
|
|
||||||
typescript@^4.9.1-beta:
|
typescript@^4.9.4:
|
||||||
version "4.9.5"
|
version "4.9.5"
|
||||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.5.tgz#095979f9bcc0d09da324d58d03ce8f8374cbe65a"
|
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.5.tgz#095979f9bcc0d09da324d58d03ce8f8374cbe65a"
|
||||||
integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==
|
integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==
|
||||||
|
Reference in New Issue
Block a user