Compare commits

..

5 Commits

Author SHA1 Message Date
9246a54c68 Bump version 2023-04-24 22:36:54 +02:00
eaf9a5373a Merge pull request #332 from keycloakify/lordvlad/issue328
feat: honor npmrc settings for ssl ca, cert and strict ssh handling
2023-04-24 22:35:31 +02:00
5b261c9916 fix: collapse empty ca list to undefined 2023-04-24 22:31:27 +02:00
73d155b3bb feat: honor npmrc settings for ssl ca, cert and strict ssh handling 2023-04-24 12:00:13 +02:00
1f8b245499 Update README.md 2023-04-21 19:17:49 +02:00
19 changed files with 44 additions and 139 deletions

View File

@ -140,24 +140,6 @@
"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,15 +39,9 @@
<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://cloud-iam.com/?mtm_campaign=keycloakify-deal&mtm_source=keycloakify-github), a French company offering Keycloak as a service.
We are exclusively sponsored by [Cloud IAM](https://www.cloud-iam.com), 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:
@ -55,20 +49,12 @@ Their dedicated support helps us continue the development and maintenance of thi
- Simplify and secure your Keycloak Identity and Access Management. Keycloak as a Service.
- Custom theme building for your brand using Keycloakify.
<div align="center">
![Logo Dark](https://user-images.githubusercontent.com/6702424/234135797-c84d0a90-0526-43e5-a186-70cbebdeb278.png#gh-dark-mode-only)
</div>
<div align="center">
![Logo Light](https://user-images.githubusercontent.com/6702424/234135799-68684c33-4ec5-48d4-8763-0f3922c86643.png#gh-light-mode-only)
</div>
<p align="center">
<i>Checkout <a href="https://cloud-iam.com/?mtm_campaign=keycloakify-deal&mtm_source=keycloakify-github">Cloud IAM</a> and use promo code <code>keycloakify5</code></i>
<a href="https://cloud-iam.com/?mtm_campaign=keycloakify-deal&mtm_source=keycloakify-github">
<img src="https://user-images.githubusercontent.com/6702424/233476937-e37b1dc6-5a1c-4a0d-ba02-61c2ce62ffb6.png" alt="Cloud IAM Logo" width="350"/>
</a>
<br/>
<i>Use promo code <code>keycloakify5</code> </i>
<br/>
<i>5% of your annual subscription will be donated to us, and you'll get 5% off too.</i>
</p>
@ -104,8 +90,6 @@ 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>
@ -124,9 +108,10 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
You are now expected to have a `"prepare": "copy-keycloak-resources-to-public",` in your package.json scripts.
This script will create `public/keycloak-assets` when you run `yarn install` (If you are using another package manager
like `pnpm` makes sure that `"prepare"` is actually ran.)
[See the updated starter](https://github.com/keycloakify/keycloakify-starter/blob/94532fcf10bf8b19e0873be8575fd28a8958a806/package.json#L11). `public/keycloak-assets` shouldn't be tracked by GIT and is automatically ignored.
[See the updated starter](https://github.com/keycloakify/keycloakify-starter/blob/94532fcf10bf8b19e0873be8575fd28a8958a806/package.json#L11).
`public/keycloak-assets` shouldn't be tracked by GIT and is automatically ignored.
## 7.7
## 7.7
- Better storybook support, see [the starter project](https://github.com/keycloakify/keycloakify-starter).

View File

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

View File

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

View File

@ -7,6 +7,8 @@ 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>>[];
@ -85,7 +87,7 @@ export function createGetKcContext<KcContextExtension extends { pageId: string }
return { "kcContext": undefined as any };
}
if (realKcContext.themeType !== "account") {
if (id<readonly string[]>(accountThemePageIds).indexOf(realKcContext.pageId) < 0 && !("account" in realKcContext)) {
return { "kcContext": undefined as any };
}

View File

@ -8,7 +8,6 @@ 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,7 +121,6 @@
out["keycloakifyVersion"] = "KEYCLOAKIFY_VERSION_xEdKd3xEdr";
out["themeVersion"] = "KEYCLOAKIFY_THEME_VERSION_sIgKd3xEdr3dx";
out["themeType"] = "KEYCLOAKIFY_THEME_TYPE_dExKd3xEdr";
out["pageId"] = "${pageId}";
return out;
@ -337,17 +336,6 @@
</#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,9 +55,8 @@ export function generateFtlFilesCodeFactory(params: {
cssGlobalsToDefine: Record<string, string>;
buildOptions: BuildOptionsLike;
keycloakifyVersion: string;
themeType: ThemeType;
}) {
const { cssGlobalsToDefine, indexHtmlCode, buildOptions, keycloakifyVersion, themeType } = params;
const { cssGlobalsToDefine, indexHtmlCode, buildOptions, keycloakifyVersion } = params;
const $ = cheerio.load(indexHtmlCode);
@ -133,8 +132,7 @@ 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_TYPE_dExKd3xEdr", themeType),
.replace("KEYCLOAKIFY_THEME_VERSION_sIgKd3xEdr3dx", buildOptions.themeVersion),
"<!-- xIdLqMeOedErIdLsPdNdI9dSlxI -->": [
"<#if scripts??>",
" <#list scripts as script>",

View File

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

View File

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

View File

@ -1,4 +1,4 @@
import { dirname, relative, sep, join } from "path";
import { dirname, relative, sep } from "path";
import { createWriteStream } from "fs";
import walk from "./walk";
@ -48,14 +48,10 @@ 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) {
if (entry.fsPath.endsWith(sep)) {
zipFile.addEmptyDirectory(entry.zipPath);
} else {
} else if ("fsPath" in entry && !entry.fsPath.endsWith(sep)) {
zipFile.addFile(entry.fsPath, entry.zipPath);
}
}
}
zipFile.addBuffer(manifestData, manifestPath);
zipFile.addBuffer(pomPropsData, pomPropsPath);
@ -69,23 +65,15 @@ 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 {
const resourcesPath = join(rootPath, "src", "main", "resources");
for await (const fsPath of walk(resourcesPath)) {
const zipPath = relative(resourcesPath, fsPath).split(sep).join("/");
for await (const fsPath of walk(rootPath)) {
const zipPath = relative(rootPath, 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").slice(-1)[0]?.length ?? 0;
lastStringLineLength = strings[i].split("\n").at(-1)?.length ?? 0;
}
if (args[i]) {
// if the interpolation value has newlines, indent the interpolation values

View File

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

View File

@ -11,6 +11,7 @@ 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>>[];
@ -144,7 +145,7 @@ export function createGetKcContext<KcContextExtension extends { pageId: string }
return { "kcContext": undefined as any };
}
if (realKcContext.themeType !== "login") {
if (id<readonly string[]>(loginThemePageIds).indexOf(realKcContext.pageId) < 0 && !("login" in realKcContext)) {
return { "kcContext": undefined as any };
}

View File

@ -104,7 +104,6 @@ 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),
@ -527,7 +526,7 @@ export const kcContextMocks = [
...kcContextCommonMock,
pageId: "saml-post-form.ftl",
"samlPost": {
"url": ""
"url": "https://saml-post-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 [isFormSubmittable, setIsFormSubmittable] = useState(false);
const [isFomSubmittable, setIsFomSubmittable] = 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={setIsFormSubmittable}
onIsFormSubmittableValueChange={setIsFomSubmittable}
i18n={i18n}
getClassName={getClassName}
/>
@ -62,7 +62,7 @@ export default function RegisterUserProfile(props: PageProps<Extract<KcContext,
)}
type="submit"
value={msgStr("doRegister")}
disabled={!isFormSubmittable}
disabled={!isFomSubmittable}
/>
</div>
</div>

View File

@ -1,4 +1,3 @@
import { useEffect, useState } from "react";
import type { PageProps } from "keycloakify/login/pages/PageProps";
import type { KcContext } from "../kcContext";
import type { I18n } from "../i18n";
@ -10,28 +9,14 @@ 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} ref={setHtmlFormElement}>
<form name="saml-post-binding" method="post" action={samlPost.url}>
{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, writeFile } from "fs/promises";
import { mkdtemp, cp, mkdir, rm } from "fs/promises";
import path from "path";
import { createReadStream } from "fs";
import walk from "keycloakify/bin/tools/walk";
@ -98,17 +98,12 @@ 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, "root");
const resourcesPath = path.join(tmp, "root", "src", "main", "resources");
const rootPath = path.join(tmp, "src");
const targetPath = path.join(tmp, "jar.jar");
await mkdir(rootPath);
await mkdir(resourcesPath, { recursive: true });
await writeFile(path.join(rootPath, "pom.xml"), "foo", "utf-8");
await cp(path.dirname(__dirname), resourcesPath, { recursive: true });
await cp(path.dirname(__dirname), rootPath, { recursive: true });
await jar({ ...coords, rootPath, targetPath });
@ -119,12 +114,11 @@ 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(resourcesPath)) {
for await (const fsPath of walk(rootPath)) {
if (!fsPath.endsWith(path.sep)) {
const rel = path.relative(resourcesPath, fsPath).replace(path.sep === "/" ? /\//g : /\\/g, "/");
assert.isOk(zipPaths.includes(rel), `missing '${rel}' (${rel}, '${zipPaths.join("', '")}')`);
const rel = path.relative(rootPath, fsPath).replace(path.sep === "/" ? /\//g : /\\/g, "/");
assert.isOk(zipPaths.includes(rel), `missing ${rel} (${rel}, ${zipPaths.join(", ")})`);
}
}
});