Compare commits

..

38 Commits

Author SHA1 Message Date
fc39e837ea Bump version 2023-05-25 07:40:41 +02:00
6df9f28c02 #277 fix storybook 2023-05-25 07:40:20 +02:00
f3d0947427 Bump version 2023-05-23 13:25:46 +02:00
3326a4cf2a Merge pull request #350 from abdurrahmanekr/patch-1
Change node.js 16.6.0 dependency that Array.prototype.at
2023-05-23 13:24:19 +02:00
9a6ea87b0c Change node.js 16.6.0 dependency that Array.prototype.at 2023-05-23 13:15:02 +03:00
12179d0ec0 Merge pull request #349 from keycloakify/all-contributors/add-kpoelhekke
docs: add kpoelhekke as a contributor for code
2023-05-15 16:56:38 +02:00
d4141fc51e Bump version 2023-05-15 16:43:16 +02:00
c32ab6181c docs: update .all-contributorsrc [skip ci] 2023-05-15 14:42:31 +00:00
3847882599 Merge pull request #348 from kpoelhekke/main
Parse datetime objects as iso strings
2023-05-15 16:42:31 +02:00
4db157f663 docs: update README.md [skip ci] 2023-05-15 14:42:30 +00:00
351b4e84c9 Parse datetime objects as iso strings 2023-05-15 16:09:15 +02:00
0c65561bcb Merge branch 'main' of https://github.com/keycloakify/keycloakify 2023-05-02 18:14:52 +02:00
00200f75a0 Fix cloud iam link 2023-05-02 18:14:41 +02:00
58614a74f5 Merge pull request #343 from keycloakify/all-contributors/add-satanshiro
docs: add satanshiro as a contributor for code
2023-05-02 16:22:00 +02:00
f3d64663a0 docs: update .all-contributorsrc [skip ci] 2023-05-02 14:21:20 +00:00
8be8c270f8 docs: update README.md [skip ci] 2023-05-02 14:21:19 +00:00
a56037f1c9 Bump version 2023-05-02 16:18:21 +02:00
2ff7955ec3 fmt 2023-05-02 16:17:53 +02:00
f2044c4d26 change name 2023-05-02 16:53:43 +03:00
4113f0faea fix-saml-post-form 2023-05-02 16:50:44 +03:00
bacd09484a Bump version 2023-05-02 04:51:36 +02:00
8253eb62bd Fix typo 2023-05-02 04:51:23 +02:00
70b659a0a0 Brag less 2023-05-02 04:36:20 +02:00
79ed74ab17 Somewhat dissociate from Keycloakify from React 2023-05-02 04:22:06 +02:00
93bb3ebd69 Bump version 2023-04-28 18:47:55 +02:00
e8e516159c Merge branch 'main' of https://github.com/keycloakify/keycloakify 2023-04-28 18:47:30 +02:00
1431c031a0 #340 2023-04-28 18:47:25 +02:00
209c2183e1 Bump version 2023-04-28 17:58:09 +02:00
0c98c282a0 Merge pull request #339 from keycloakify/fix/fix-broken-jar
fix: fix broken jar
2023-04-28 17:57:41 +02:00
58c10796a1 fix: fix broken jar
Many tools will handle zipfiles which lack directory entries
just fine, others will not. Looks like the JDKs JAR libs are
not  handling  it well. This commit will make sure to create
folder entries.
2023-04-28 16:59:06 +02:00
603e6a99f3 Update package.json 2023-04-27 18:00:14 +02:00
6622ebc04e Merge pull request #337 from keycloakify/fix/restore-missing-pom-xml-in-jar
Fix/restore missing pom xml in jar
2023-04-27 17:59:53 +02:00
465dbb4a8d fix formatting 2023-04-27 14:47:15 +02:00
08ae908453 fix: adjust test after adjusting jar.ts 2023-04-27 14:44:45 +02:00
c35a1e7c50 fix: fix paths after changing root path param meaning 2023-04-27 14:33:21 +02:00
ecb22c3829 fix: restore missing pom.xml in jar archive 2023-04-27 14:33:19 +02:00
eebf969f7e Bump version 2023-04-27 11:52:28 +02:00
5816f25c3e #334 2023-04-27 11:52:02 +02:00
19 changed files with 126 additions and 38 deletions

View File

@ -140,6 +140,24 @@
"contributions": [
"code"
]
},
{
"login": "satanshiro",
"name": "satanshiro",
"avatar_url": "https://avatars.githubusercontent.com/u/38865738?v=4",
"profile": "https://github.com/satanshiro",
"contributions": [
"code"
]
},
{
"login": "kpoelhekke",
"name": "Koen Poelhekke",
"avatar_url": "https://avatars.githubusercontent.com/u/1632377?v=4",
"profile": "https://poelhekke.dev",
"contributions": [
"code"
]
}
],
"contributorsPerLine": 7,

View File

@ -39,9 +39,15 @@
<img src="https://user-images.githubusercontent.com/6702424/110260457-a1c3d380-7fac-11eb-853a-80459b65626b.png">
</p>
> Whether or not React is your preferred framework, Keycloakify
> offers a solid option for building Keycloak themes.
> It's not just a convenient way to create a Keycloak theme
> when using React; it's a well-regarded solution that many
> developers appreciate.
## Sponsor 👼
We are exclusively sponsored by [Cloud IAM](https://www.cloud-iam.com), a French company offering Keycloak as a service.
We are exclusively sponsored by [Cloud IAM](https://cloud-iam.com/?mtm_campaign=keycloakify-deal&mtm_source=keycloakify-github), a French company offering Keycloak as a service.
Their dedicated support helps us continue the development and maintenance of this project.
[Cloud IAM](https://cloud-iam.com/?mtm_campaign=keycloakify-deal&mtm_source=keycloakify-github) provides the following services:
@ -98,6 +104,8 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://www.gravitysoftware.be"><img src="https://avatars.githubusercontent.com/u/1140574?v=4?s=100" width="100px;" alt="Thomas Silvestre"/><br /><sub><b>Thomas Silvestre</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=thosil" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/satanshiro"><img src="https://avatars.githubusercontent.com/u/38865738?v=4?s=100" width="100px;" alt="satanshiro"/><br /><sub><b>satanshiro</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=satanshiro" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://poelhekke.dev"><img src="https://avatars.githubusercontent.com/u/1632377?v=4?s=100" width="100px;" alt="Koen Poelhekke"/><br /><sub><b>Koen Poelhekke</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=kpoelhekke" title="Code">💻</a></td>
</tr>
</tbody>
</table>

View File

@ -1,6 +1,6 @@
{
"name": "keycloakify",
"version": "7.11.2",
"version": "7.11.11",
"description": "Create Keycloak themes using React",
"repository": {
"type": "git",

View File

@ -1,4 +1,4 @@
import type { AccountThemePageId } from "keycloakify/bin/keycloakify/generateFtl";
import type { AccountThemePageId, ThemeType } from "keycloakify/bin/keycloakify/generateFtl";
import { assert } from "tsafe/assert";
import type { Equals } from "tsafe";
@ -7,6 +7,7 @@ export type KcContext = KcContext.Password | KcContext.Account;
export declare namespace KcContext {
export type Common = {
keycloakifyVersion: string;
themeType: "account";
locale?: {
supported: {
url: string;
@ -84,4 +85,15 @@ export declare namespace KcContext {
};
}
assert<Equals<KcContext["pageId"], AccountThemePageId>>();
{
type Got = KcContext["pageId"];
type Expected = AccountThemePageId;
type OnlyInGot = Exclude<Got, Expected>;
type OnlyInExpected = Exclude<Expected, Got>;
assert<Equals<OnlyInGot, never>>();
assert<Equals<OnlyInExpected, never>>();
}
assert<KcContext["themeType"] extends ThemeType ? true : false>();

View File

@ -7,8 +7,6 @@ import { pathBasename } from "keycloakify/tools/pathBasename";
import { resourcesCommonDirPathRelativeToPublicDir } from "keycloakify/bin/mockTestingResourcesPath";
import { symToStr } from "tsafe/symToStr";
import { kcContextMocks, kcContextCommonMock } from "keycloakify/account/kcContext/kcContextMocks";
import { id } from "tsafe/id";
import { accountThemePageIds } from "keycloakify/bin/keycloakify/generateFtl/pageId";
export function createGetKcContext<KcContextExtension extends { pageId: string } = never>(params?: {
mockData?: readonly DeepPartial<ExtendKcContext<KcContextExtension>>[];
@ -87,7 +85,7 @@ export function createGetKcContext<KcContextExtension extends { pageId: string }
return { "kcContext": undefined as any };
}
if (id<readonly string[]>(accountThemePageIds).indexOf(realKcContext.pageId) < 0 && !("account" in realKcContext)) {
if (realKcContext.themeType !== "account") {
return { "kcContext": undefined as any };
}

View File

@ -8,6 +8,7 @@ const PUBLIC_URL = process.env["PUBLIC_URL"] ?? "/";
export const kcContextCommonMock: KcContext.Common = {
"keycloakifyVersion": "0.0.0",
"themeType": "account",
"url": {
"resourcesPath": pathJoin(PUBLIC_URL, resourcesDirPathRelativeToPublicDir),
"resourcesCommonPath": pathJoin(PUBLIC_URL, resourcesCommonDirPathRelativeToPublicDir),

View File

@ -121,6 +121,7 @@
out["keycloakifyVersion"] = "KEYCLOAKIFY_VERSION_xEdKd3xEdr";
out["themeVersion"] = "KEYCLOAKIFY_THEME_VERSION_sIgKd3xEdr3dx";
out["themeType"] = "KEYCLOAKIFY_THEME_TYPE_dExKd3xEdr";
out["pageId"] = "${pageId}";
return out;
@ -336,6 +337,17 @@
</#if>
<#local isDate = "">
<#attempt>
<#local isDate = object?is_date_like>
<#recover>
<#return "ABORT: Can't test if it's a date">
</#attempt>
<#if isDate>
<#return '"' + object?datetime?iso_utc + '"'>
</#if>
<#attempt>
<#return '"' + object?js_string + '"'>;
<#recover>

View File

@ -55,8 +55,9 @@ export function generateFtlFilesCodeFactory(params: {
cssGlobalsToDefine: Record<string, string>;
buildOptions: BuildOptionsLike;
keycloakifyVersion: string;
themeType: ThemeType;
}) {
const { cssGlobalsToDefine, indexHtmlCode, buildOptions, keycloakifyVersion } = params;
const { cssGlobalsToDefine, indexHtmlCode, buildOptions, keycloakifyVersion, themeType } = params;
const $ = cheerio.load(indexHtmlCode);
@ -132,7 +133,8 @@ export function generateFtlFilesCodeFactory(params: {
buildOptions.customUserAttributes.length === 0 ? "" : ", " + buildOptions.customUserAttributes.map(name => `"${name}"`).join(", ")
)
.replace("KEYCLOAKIFY_VERSION_xEdKd3xEdr", keycloakifyVersion)
.replace("KEYCLOAKIFY_THEME_VERSION_sIgKd3xEdr3dx", buildOptions.themeVersion),
.replace("KEYCLOAKIFY_THEME_VERSION_sIgKd3xEdr3dx", buildOptions.themeVersion)
.replace("KEYCLOAKIFY_THEME_TYPE_dExKd3xEdr", themeType),
"<!-- xIdLqMeOedErIdLsPdNdI9dSlxI -->": [
"<#if scripts??>",
" <#list scripts as script>",

View File

@ -141,7 +141,8 @@ export async function generateTheme(params: {
"indexHtmlCode": fs.readFileSync(pathJoin(reactAppBuildDirPath, "index.html")).toString("utf8"),
"cssGlobalsToDefine": allCssGlobalsToDefine,
buildOptions,
keycloakifyVersion
keycloakifyVersion,
themeType
});
return generateFtlFilesCode;

View File

@ -58,7 +58,7 @@ export async function main() {
case "keycloakify":
logger.log("🫶 Let keycloakify do its thang");
await jar({
"rootPath": pathJoin(buildOptions.keycloakifyBuildDirPath, "src", "main", "resources"),
"rootPath": buildOptions.keycloakifyBuildDirPath,
"version": buildOptions.themeVersion,
"groupId": buildOptions.groupId,
"artifactId": buildOptions.artifactId,

View File

@ -1,4 +1,4 @@
import { dirname, relative, sep } from "path";
import { dirname, relative, sep, join } from "path";
import { createWriteStream } from "fs";
import walk from "./walk";
@ -48,8 +48,12 @@ export async function jarStream({ groupId, artifactId, version, asyncPathGenerat
for await (const entry of asyncPathGeneratorFn()) {
if ("buffer" in entry) {
zipFile.addBuffer(entry.buffer, entry.zipPath);
} else if ("fsPath" in entry && !entry.fsPath.endsWith(sep)) {
zipFile.addFile(entry.fsPath, entry.zipPath);
} else if ("fsPath" in entry) {
if (entry.fsPath.endsWith(sep)) {
zipFile.addEmptyDirectory(entry.zipPath);
} else {
zipFile.addFile(entry.fsPath, entry.zipPath);
}
}
}
@ -65,15 +69,23 @@ export async function jarStream({ groupId, artifactId, version, asyncPathGenerat
* Create a jar archive, using the resources found at `rootPath` (a directory) and write the
* archive to `targetPath` (a file). Use `groupId`, `artifactId` and `version` to define
* the contents of the pom.properties file which is going to be added to the archive.
* The root directory is expectedto have a conventional maven/gradle folder structure with a
* single `pom.xml` file at the root and a `src/main/resources` directory containing all
* application resources.
*/
export default async function jar({ groupId, artifactId, version, rootPath, targetPath }: JarArgs) {
await mkdir(dirname(targetPath), { recursive: true });
const asyncPathGeneratorFn = async function* (): ZipEntryGenerator {
for await (const fsPath of walk(rootPath)) {
const zipPath = relative(rootPath, fsPath).split(sep).join("/");
const resourcesPath = join(rootPath, "src", "main", "resources");
for await (const fsPath of walk(resourcesPath)) {
const zipPath = relative(resourcesPath, fsPath).split(sep).join("/");
yield { fsPath, zipPath };
}
yield {
fsPath: join(rootPath, "pom.xml"),
zipPath: `META-INF/maven/${groupId}/${artifactId}/pom.xml`
};
};
const zipFile = await jarStream({ groupId, artifactId, version, asyncPathGeneratorFn });

View File

@ -9,7 +9,7 @@ function populateTemplate(strings: TemplateStringsArray, ...args: unknown[]) {
if (strings[i]) {
chunks.push(strings[i]);
// remember last indent of the string portion
lastStringLineLength = strings[i].split("\n").at(-1)?.length ?? 0;
lastStringLineLength = strings[i].split("\n").slice(-1)[0]?.length ?? 0;
}
if (args[i]) {
// if the interpolation value has newlines, indent the interpolation values

View File

@ -1,4 +1,4 @@
import type { LoginThemePageId } from "keycloakify/bin/keycloakify/generateFtl";
import type { LoginThemePageId, ThemeType } from "keycloakify/bin/keycloakify/generateFtl";
import { assert } from "tsafe/assert";
import type { Equals } from "tsafe";
import type { MessageKey } from "../i18n/i18n";
@ -38,6 +38,7 @@ export type KcContext =
export declare namespace KcContext {
export type Common = {
keycloakifyVersion: string;
themeType: "login";
url: {
loginAction: string;
resourcesPath: string;
@ -92,7 +93,7 @@ export declare namespace KcContext {
url: string;
SAMLRequest?: string;
SAMLResponse?: string;
RelayState?: string;
relayState?: string;
};
};
@ -564,3 +565,5 @@ export declare namespace Validators {
assert<Equals<OnlyInGot, never>>();
assert<Equals<OnlyInExpected, never>>();
}
assert<KcContext["themeType"] extends ThemeType ? true : false>();

View File

@ -11,7 +11,6 @@ import { pathJoin } from "keycloakify/bin/tools/pathJoin";
import { pathBasename } from "keycloakify/tools/pathBasename";
import { resourcesCommonDirPathRelativeToPublicDir } from "keycloakify/bin/mockTestingResourcesPath";
import { symToStr } from "tsafe/symToStr";
import { loginThemePageIds } from "keycloakify/bin/keycloakify/generateFtl/pageId";
export function createGetKcContext<KcContextExtension extends { pageId: string } = never>(params?: {
mockData?: readonly DeepPartial<ExtendKcContext<KcContextExtension>>[];
@ -145,7 +144,7 @@ export function createGetKcContext<KcContextExtension extends { pageId: string }
return { "kcContext": undefined as any };
}
if (id<readonly string[]>(loginThemePageIds).indexOf(realKcContext.pageId) < 0 && !("login" in realKcContext)) {
if (realKcContext.themeType !== "login") {
return { "kcContext": undefined as any };
}

View File

@ -104,6 +104,7 @@ const attributesByName = Object.fromEntries(attributes.map(attribute => [attribu
export const kcContextCommonMock: KcContext.Common = {
"keycloakifyVersion": "0.0.0",
"themeType": "login",
"url": {
"loginAction": "#",
"resourcesPath": pathJoin(PUBLIC_URL, resourcesDirPathRelativeToPublicDir),
@ -526,7 +527,7 @@ export const kcContextMocks = [
...kcContextCommonMock,
pageId: "saml-post-form.ftl",
"samlPost": {
"url": "https://saml-post-url"
"url": ""
}
}),
id<KcContext.LoginPageExpired>({

View File

@ -11,7 +11,7 @@ export default function LoginVerifyEmail(props: PageProps<Extract<KcContext, { p
return (
<Template {...{ kcContext, i18n, doUseDefaultCss, classes }} displayMessage={false} headerNode={msg("emailVerifyTitle")}>
<p className="instruction">{msg("emailVerifyInstruction1", user?.email)}</p>
<p className="instruction">{msg("emailVerifyInstruction1", user?.email ?? "")}</p>
<p className="instruction">
{msg("emailVerifyInstruction2")}
<br />

View File

@ -20,7 +20,7 @@ export default function RegisterUserProfile(props: PageProps<Extract<KcContext,
const { msg, msgStr } = i18n;
const [isFomSubmittable, setIsFomSubmittable] = useState(false);
const [isFormSubmittable, setIsFormSubmittable] = useState(false);
return (
<Template
@ -32,7 +32,7 @@ export default function RegisterUserProfile(props: PageProps<Extract<KcContext,
<form id="kc-register-form" className={getClassName("kcFormClass")} action={url.registrationAction} method="post">
<UserProfileFormFields
kcContext={kcContext}
onIsFormSubmittableValueChange={setIsFomSubmittable}
onIsFormSubmittableValueChange={setIsFormSubmittable}
i18n={i18n}
getClassName={getClassName}
/>
@ -62,7 +62,7 @@ export default function RegisterUserProfile(props: PageProps<Extract<KcContext,
)}
type="submit"
value={msgStr("doRegister")}
disabled={!isFomSubmittable}
disabled={!isFormSubmittable}
/>
</div>
</div>

View File

@ -1,3 +1,4 @@
import { useEffect, useState } from "react";
import type { PageProps } from "keycloakify/login/pages/PageProps";
import type { KcContext } from "../kcContext";
import type { I18n } from "../i18n";
@ -9,14 +10,28 @@ export default function SamlPostForm(props: PageProps<Extract<KcContext, { pageI
const { samlPost } = kcContext;
const [htmlFormElement, setHtmlFormElement] = useState<HTMLFormElement | null>(null);
useEffect(() => {
if (htmlFormElement === null) {
return;
}
// Storybook
if (samlPost.url === "") {
alert("In a real Keycloak the user would be redirected immediately");
return;
}
htmlFormElement.submit();
}, [htmlFormElement]);
return (
<Template {...{ kcContext, i18n, doUseDefaultCss, classes }} displayMessage={false} headerNode={msg("saml.post-form.title")}>
<script dangerouslySetInnerHTML={{ "__html": `window.onload = function() {document.forms[0].submit()};` }} />
<p>{msg("saml.post-form.message")}</p>
<form name="saml-post-binding" method="post" action={samlPost.url}>
<form name="saml-post-binding" method="post" action={samlPost.url} ref={setHtmlFormElement}>
{samlPost.SAMLRequest && <input type="hidden" name="SAMLRequest" value={samlPost.SAMLRequest} />}
{samlPost.SAMLResponse && <input type="hidden" name="SAMLResponse" value={samlPost.SAMLResponse} />}
{samlPost.RelayState && <input type="hidden" name="RelayState" value={samlPost.RelayState} />}
{samlPost.relayState && <input type="hidden" name="RelayState" value={samlPost.relayState} />}
<noscript>
<p>{msg("saml.post-form.js-disabled")}</p>
<input type="submit" value={msgStr("doContinue")} />

View File

@ -3,7 +3,7 @@ import { fromBuffer, Entry, ZipFile } from "yauzl";
import { it, describe, assert, afterAll } from "vitest";
import { Readable } from "stream";
import { tmpdir } from "os";
import { mkdtemp, cp, mkdir, rm } from "fs/promises";
import { mkdtemp, cp, mkdir, rm, writeFile } from "fs/promises";
import path from "path";
import { createReadStream } from "fs";
import walk from "keycloakify/bin/tools/walk";
@ -98,12 +98,17 @@ describe("jar", () => {
it("creates a jar from _real_ files without error", async () => {
const tmp = await mkdtemp(path.join(tmpdir(), "kc-jar-test-"));
tmpDirs.push(tmp);
const rootPath = path.join(tmp, "src");
const targetPath = path.join(tmp, "jar.jar");
await mkdir(rootPath);
await cp(path.dirname(__dirname), rootPath, { recursive: true });
tmpDirs.push(tmp);
const rootPath = path.join(tmp, "root");
const resourcesPath = path.join(tmp, "root", "src", "main", "resources");
const targetPath = path.join(tmp, "jar.jar");
await mkdir(resourcesPath, { recursive: true });
await writeFile(path.join(rootPath, "pom.xml"), "foo", "utf-8");
await cp(path.dirname(__dirname), resourcesPath, { recursive: true });
await jar({ ...coords, rootPath, targetPath });
@ -114,11 +119,12 @@ describe("jar", () => {
assert.isOk(entries.has("META-INF/MANIFEST.MF"));
assert.isOk(entries.has("META-INF/maven/someGroupId/someArtifactId/pom.properties"));
assert.isOk(entries.has("META-INF/maven/someGroupId/someArtifactId/pom.xml"));
for await (const fsPath of walk(rootPath)) {
for await (const fsPath of walk(resourcesPath)) {
if (!fsPath.endsWith(path.sep)) {
const rel = path.relative(rootPath, fsPath).replace(path.sep === "/" ? /\//g : /\\/g, "/");
assert.isOk(zipPaths.includes(rel), `missing ${rel} (${rel}, ${zipPaths.join(", ")})`);
const rel = path.relative(resourcesPath, fsPath).replace(path.sep === "/" ? /\//g : /\\/g, "/");
assert.isOk(zipPaths.includes(rel), `missing '${rel}' (${rel}, '${zipPaths.join("', '")}')`);
}
}
});