Implement a mechanism to overload kcContext
This commit is contained in:
12
package.json
12
package.json
@ -12,7 +12,7 @@
|
|||||||
"clean": "rimraf dist/",
|
"clean": "rimraf dist/",
|
||||||
"build": "yarn clean && tsc && yarn grant-exec-perms && yarn copy-files",
|
"build": "yarn clean && tsc && yarn grant-exec-perms && yarn copy-files",
|
||||||
"grant-exec-perms": "node dist/bin/tools/grant-exec-perms.js",
|
"grant-exec-perms": "node dist/bin/tools/grant-exec-perms.js",
|
||||||
"test": "node dist/test",
|
"test": "node dist/test/bin && node dist/test/lib",
|
||||||
"copy-files": "copyfiles -u 1 src/**/*.ftl src/**/*.xml src/**/*.js dist/",
|
"copy-files": "copyfiles -u 1 src/**/*.ftl src/**/*.xml src/**/*.js dist/",
|
||||||
"generate-messages": "node dist/bin/generate-i18n-messages.js"
|
"generate-messages": "node dist/bin/generate-i18n-messages.js"
|
||||||
},
|
},
|
||||||
@ -46,16 +46,18 @@
|
|||||||
"properties-parser": "^0.3.1",
|
"properties-parser": "^0.3.1",
|
||||||
"react": "^17.0.1",
|
"react": "^17.0.1",
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
"typescript": "^4.2.3"
|
"typescript": "^4.2.3",
|
||||||
|
"ts-toolbelt": "^9.6.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cheerio": "^1.0.0-rc.5",
|
"cheerio": "^1.0.0-rc.5",
|
||||||
"evt": "2.0.0-beta.15",
|
"evt": "2.0.0-beta.20",
|
||||||
"minimal-polyfills": "^2.1.6",
|
"minimal-polyfills": "^2.1.6",
|
||||||
"path": "^0.12.7",
|
"path": "^0.12.7",
|
||||||
"powerhooks": "^0.1.0",
|
"powerhooks": "^0.1.6",
|
||||||
"react-markdown": "^5.0.3",
|
"react-markdown": "^5.0.3",
|
||||||
"scripting-tools": "^0.19.13",
|
"scripting-tools": "^0.19.13",
|
||||||
"tss-react": "^0.0.12"
|
"tss-react": "^0.0.12",
|
||||||
|
"tsafe": "^0.4.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,7 @@ import {
|
|||||||
} from "../replaceImportFromStatic";
|
} from "../replaceImportFromStatic";
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import { join as pathJoin } from "path";
|
import { join as pathJoin } from "path";
|
||||||
import { objectKeys } from "evt/tools/typeSafety/objectKeys";
|
import { objectKeys } from "tsafe/objectKeys";
|
||||||
import { ftlValuesGlobalName } from "../ftlValuesGlobalName";
|
import { ftlValuesGlobalName } from "../ftlValuesGlobalName";
|
||||||
|
|
||||||
export const pageIds = [
|
export const pageIds = [
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
import * as fs from "fs";
|
import * as fs from "fs";
|
||||||
import * as path from "path";
|
import * as path from "path";
|
||||||
import { crawl } from "./crawl";
|
import { crawl } from "./crawl";
|
||||||
import { id } from "evt/tools/typeSafety/id";
|
import { id } from "tsafe/id";
|
||||||
|
|
||||||
type TransformSourceCode =
|
type TransformSourceCode =
|
||||||
(params: {
|
(params: {
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
|
|
||||||
import { memo } from "react";
|
import { memo } from "react";
|
||||||
import { Template } from "./Template";
|
import { Template } from "./Template";
|
||||||
import type { KcProps } from "./KcProps";
|
import type { KcProps } from "./KcProps";
|
||||||
import { assert } from "../tools/assert";
|
|
||||||
import type { KcContextBase } from "../getKcContext/KcContextBase";
|
import type { KcContextBase } from "../getKcContext/KcContextBase";
|
||||||
import { useKcMessage } from "../i18n/useKcMessage";
|
import { useKcMessage } from "../i18n/useKcMessage";
|
||||||
|
|
||||||
@ -10,8 +8,6 @@ export const Error = memo(({ kcContext, ...props }: { kcContext: KcContextBase.E
|
|||||||
|
|
||||||
const { msg } = useKcMessage();
|
const { msg } = useKcMessage();
|
||||||
|
|
||||||
assert(kcContext.message !== undefined);
|
|
||||||
|
|
||||||
const { message, client } = kcContext;
|
const { message, client } = kcContext;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
|
|
||||||
import { allPropertiesValuesToUndefined } from "../tools/allPropertiesValuesToUndefined";
|
import { allPropertiesValuesToUndefined } from "../tools/allPropertiesValuesToUndefined";
|
||||||
import { doExtends } from "evt/tools/typeSafety/doExtends";
|
import { doExtends } from "tsafe/doExtends";
|
||||||
|
|
||||||
/** Class names can be provided as an array or separated by whitespace */
|
/** Class names can be provided as an array or separated by whitespace */
|
||||||
export type KcPropsGeneric<CssClasses extends string> = { [key in CssClasses]: readonly string[] | string | undefined; };
|
export type KcPropsGeneric<CssClasses extends string> = { [key in CssClasses]: readonly string[] | string | undefined; };
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
|
|
||||||
import type { PageId } from "../../bin/build-keycloak-theme/generateFtl";
|
import type { PageId } from "../../bin/build-keycloak-theme/generateFtl";
|
||||||
import type { KcLanguageTag } from "../i18n/KcLanguageTag";
|
import type { KcLanguageTag } from "../i18n/KcLanguageTag";
|
||||||
import { doExtends } from "evt/tools/typeSafety/doExtends";
|
import { doExtends } from "tsafe/doExtends";
|
||||||
import type { MessageKey } from "../i18n/useKcMessage";
|
import type { MessageKey } from "../i18n/useKcMessage";
|
||||||
import type { LanguageLabel } from "../i18n/KcLanguageTag";
|
import type { LanguageLabel } from "../i18n/KcLanguageTag";
|
||||||
|
|
||||||
@ -150,7 +150,8 @@ export declare namespace KcContextBase {
|
|||||||
pageId: "error.ftl";
|
pageId: "error.ftl";
|
||||||
client?: {
|
client?: {
|
||||||
baseUrl?: string;
|
baseUrl?: string;
|
||||||
}
|
},
|
||||||
|
message: NonNullable<Common["message"]>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type LoginResetPassword = Common & {
|
export type LoginResetPassword = Common & {
|
||||||
|
@ -1,28 +1,93 @@
|
|||||||
|
|
||||||
import type { KcContextBase } from "./KcContextBase";
|
import type { KcContextBase } from "./KcContextBase";
|
||||||
import { kcContextMocks, kcContextCommonMock } from "./kcContextMocks";
|
import { kcContextMocks, kcContextCommonMock } from "./kcContextMocks";
|
||||||
import { ftlValuesGlobalName } from "../../bin/build-keycloak-theme/ftlValuesGlobalName";
|
import { ftlValuesGlobalName } from "../../bin/build-keycloak-theme/ftlValuesGlobalName";
|
||||||
|
import type { AndByDiscriminatingKey } from "../tools/AndByDiscriminatingKey";
|
||||||
|
import type { DeepPartial } from "../tools/DeepPartial";
|
||||||
|
import { deepAssign } from "../tools/deepAssign";
|
||||||
|
|
||||||
export function getKcContext<KcContextExtended extends { pageId: string; } = never>(
|
|
||||||
|
export type ExtendsKcContextBase<
|
||||||
|
KcContextExtended extends ({ pageId: string; } | undefined)
|
||||||
|
> =
|
||||||
|
KcContextExtended extends undefined ?
|
||||||
|
KcContextBase :
|
||||||
|
AndByDiscriminatingKey<
|
||||||
|
"pageId",
|
||||||
|
KcContextExtended & KcContextBase.Common,
|
||||||
|
KcContextBase
|
||||||
|
>;
|
||||||
|
|
||||||
|
export function getKcContext<KcContextExtended extends ({ pageId: string; } | undefined) = undefined>(
|
||||||
params?: {
|
params?: {
|
||||||
mockPageId?: KcContextBase["pageId"] | KcContextExtended["pageId"];
|
mockPageId?: ExtendsKcContextBase<KcContextExtended>["pageId"];
|
||||||
kcContextExtendedMock?: KcContextExtended[];
|
mockData?: readonly DeepPartial<ExtendsKcContextBase<KcContextExtended>>[];
|
||||||
}
|
}
|
||||||
): { kcContext: (KcContextBase | KcContextExtended & KcContextBase.Common) | undefined; } {
|
): { kcContext: ExtendsKcContextBase<KcContextExtended> | undefined; } {
|
||||||
|
|
||||||
const { mockPageId, kcContextExtendedMock } = params ?? { "mockPageId": false };
|
const {
|
||||||
|
mockPageId,
|
||||||
|
mockData
|
||||||
|
} = params ?? {};
|
||||||
|
|
||||||
if (mockPageId !== undefined) {
|
if (mockPageId !== undefined) {
|
||||||
|
|
||||||
return {
|
//TODO maybe trow if no mock fo custom page
|
||||||
"pageId": mockPageId,
|
|
||||||
...(kcContextMocks.find(({ pageId }) => pageId === mockPageId) ?? kcContextCommonMock),
|
const kcContextDefaultMock = kcContextMocks.find(({ pageId }) => pageId === mockPageId);
|
||||||
...(kcContextExtendedMock?.find(({ pageId }) => pageId === mockPageId) ?? {})
|
|
||||||
} as any;
|
const partialKcContextCustomMock = mockData?.find(({ pageId }) => pageId === mockPageId);
|
||||||
|
|
||||||
|
if (
|
||||||
|
kcContextDefaultMock === undefined &&
|
||||||
|
partialKcContextCustomMock === undefined
|
||||||
|
) {
|
||||||
|
|
||||||
|
console.warn([
|
||||||
|
`WARNING: You declared the non build in page ${mockPageId} but you didn't `,
|
||||||
|
`provide mock data needed to debug the page outside of Keycloak as you are trying to do now.`,
|
||||||
|
`Please check the documentation of the getKcContext function`
|
||||||
|
].join("\n"));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const kcContext: any = { "pageId": mockPageId };
|
||||||
|
|
||||||
|
deepAssign({
|
||||||
|
"target": kcContext,
|
||||||
|
"source": kcContextCommonMock
|
||||||
|
});
|
||||||
|
|
||||||
|
if (kcContextDefaultMock !== undefined) {
|
||||||
|
|
||||||
|
deepAssign({
|
||||||
|
"target": kcContext,
|
||||||
|
"source": kcContextDefaultMock
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if (partialKcContextCustomMock !== undefined) {
|
||||||
|
|
||||||
|
deepAssign({
|
||||||
|
"target": kcContext,
|
||||||
|
"source": partialKcContextCustomMock
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return { kcContext };
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (window as any)[ftlValuesGlobalName];
|
return {
|
||||||
|
"kcContext":
|
||||||
|
typeof window === "undefined" ?
|
||||||
|
undefined :
|
||||||
|
(window as any)[ftlValuesGlobalName]
|
||||||
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -5,12 +5,15 @@ import { getKcLanguageTagLabel } from "../../i18n/KcLanguageTag";
|
|||||||
//NOTE: Aside because we want to be able to import them from node
|
//NOTE: Aside because we want to be able to import them from node
|
||||||
import { resourcesCommonPath, resourcesPath } from "./urlResourcesPath";
|
import { resourcesCommonPath, resourcesPath } from "./urlResourcesPath";
|
||||||
import { id } from "tsafe/id";
|
import { id } from "tsafe/id";
|
||||||
|
import { join as pathJoin } from "path";
|
||||||
|
|
||||||
|
const PUBLIC_URL = process.env["PUBLIC_URL"] ?? "/";
|
||||||
|
|
||||||
export const kcContextCommonMock: KcContextBase.Common = {
|
export const kcContextCommonMock: KcContextBase.Common = {
|
||||||
"url": {
|
"url": {
|
||||||
"loginAction": "#",
|
"loginAction": "#",
|
||||||
"resourcesPath": `${process.env["PUBLIC_URL"]}/${resourcesPath}`,
|
"resourcesPath": pathJoin(PUBLIC_URL, resourcesPath),
|
||||||
"resourcesCommonPath": `${process.env["PUBLIC_URL"]}/${resourcesCommonPath}`,
|
"resourcesCommonPath": pathJoin(PUBLIC_URL, resourcesCommonPath),
|
||||||
"loginRestartFlowUrl": "/auth/realms/myrealm/login-actions/restart?client_id=account&tab_id=HoAx28ja4xg",
|
"loginRestartFlowUrl": "/auth/realms/myrealm/login-actions/restart?client_id=account&tab_id=HoAx28ja4xg",
|
||||||
"loginUrl": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg",
|
"loginUrl": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg",
|
||||||
},
|
},
|
||||||
@ -95,7 +98,8 @@ export const kcContextCommonMock: KcContextBase.Common = {
|
|||||||
"languageTag": "tr"
|
"languageTag": "tr"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"current": null as any
|
//"current": null as any
|
||||||
|
"current": "English"
|
||||||
},
|
},
|
||||||
"auth": {
|
"auth": {
|
||||||
"showUsername": false,
|
"showUsername": false,
|
||||||
@ -110,6 +114,7 @@ export const kcContextCommonMock: KcContextBase.Common = {
|
|||||||
"isAppInitiatedAction": false,
|
"isAppInitiatedAction": false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
Object.defineProperty(
|
Object.defineProperty(
|
||||||
kcContextCommonMock.locale!,
|
kcContextCommonMock.locale!,
|
||||||
"current",
|
"current",
|
||||||
@ -188,6 +193,10 @@ export const kcContextMocks: KcContextBase[] = [
|
|||||||
"pageId": "error.ftl",
|
"pageId": "error.ftl",
|
||||||
"client": {
|
"client": {
|
||||||
"baseUrl": "#"
|
"baseUrl": "#"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"type": "error",
|
||||||
|
"summary": "This is the error message"
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
id<KcContextBase.LoginResetPassword>({
|
id<KcContextBase.LoginResetPassword>({
|
||||||
|
43
src/lib/getKcContext/typeHelper.ts
Normal file
43
src/lib/getKcContext/typeHelper.ts
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
import { KcContextBase } from "./KcContextBase";
|
||||||
|
import type { AndByDiscriminatingKey } from "../tools/AndByDiscriminatingKey";
|
||||||
|
|
||||||
|
|
||||||
|
export type ExtendsKcContextBase<KcContextExtended extends { pageId: string; }>=
|
||||||
|
AndByDiscriminatingKey<
|
||||||
|
"pageId",
|
||||||
|
KcContextExtended & KcContextBase.Common,
|
||||||
|
KcContextBase
|
||||||
|
>;
|
||||||
|
|
||||||
|
type KcContextExtended =
|
||||||
|
{ pageId: "register.ftl"; authorizedMailDomains: string[]; } |
|
||||||
|
{ pageId: "my-extra-page-1.ftl"; } |
|
||||||
|
{ pageId: "my-extra-page-2.ftl"; someCustomValue: string; };
|
||||||
|
|
||||||
|
const y: ExtendsKcContextBase<KcContextExtended> = null as any;
|
||||||
|
|
||||||
|
|
||||||
|
if (y.pageId === "register.ftl") {
|
||||||
|
|
||||||
|
y.authorizedMailDomains;
|
||||||
|
|
||||||
|
y.realm.displayName;
|
||||||
|
|
||||||
|
y.register
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if (y.pageId === "my-extra-page-1.ftl") {
|
||||||
|
y.realm.displayName;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (y.pageId === "my-extra-page-2.ftl") {
|
||||||
|
|
||||||
|
y.realm
|
||||||
|
y.someCustomValue
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,5 @@
|
|||||||
|
|
||||||
import { objectKeys } from "evt/tools/typeSafety/objectKeys";
|
import { objectKeys } from "tsafe/objectKeys";
|
||||||
import { kcMessages } from "./kcMessages/login";
|
import { kcMessages } from "./kcMessages/login";
|
||||||
|
|
||||||
export type KcLanguageTag = keyof typeof kcMessages;
|
export type KcLanguageTag = keyof typeof kcMessages;
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
|
|
||||||
import { kcMessages } from "../generated_kcMessages/login";
|
import { kcMessages } from "../generated_kcMessages/login";
|
||||||
import { Evt } from "evt";
|
import { Evt } from "evt";
|
||||||
import { objectKeys } from "evt/tools/typeSafety/objectKeys";
|
import { objectKeys } from "tsafe/objectKeys";
|
||||||
|
|
||||||
export const evtTermsUpdated = Evt.asNonPostable(Evt.create<void>());
|
export const evtTermsUpdated = Evt.asNonPostable(Evt.create<void>());
|
||||||
|
|
||||||
|
@ -2,22 +2,39 @@
|
|||||||
import { createUseGlobalState } from "powerhooks";
|
import { createUseGlobalState } from "powerhooks";
|
||||||
import { getKcContext } from "../getKcContext";
|
import { getKcContext } from "../getKcContext";
|
||||||
import { getBestMatchAmongKcLanguageTag } from "./KcLanguageTag";
|
import { getBestMatchAmongKcLanguageTag } from "./KcLanguageTag";
|
||||||
|
import type { StatefulEvt } from "powerhooks";
|
||||||
|
import { KcLanguageTag } from "./KcLanguageTag";
|
||||||
|
|
||||||
const { kcContext } = getKcContext();
|
|
||||||
|
|
||||||
//export const { useKcLanguageTag, evtKcLanguageTag } = createUseGlobalState(
|
//export const { useKcLanguageTag, evtKcLanguageTag } = createUseGlobalState(
|
||||||
const wrap = createUseGlobalState(
|
const wrap = createUseGlobalState(
|
||||||
"kcLanguageTag",
|
"kcLanguageTag",
|
||||||
() => getBestMatchAmongKcLanguageTag(
|
() => {
|
||||||
kcContext?.locale?.current ??
|
|
||||||
navigator.language
|
|
||||||
),
|
const { kcContext } = getKcContext();
|
||||||
|
|
||||||
|
const languageLike =
|
||||||
|
kcContext?.locale?.current ??
|
||||||
|
(
|
||||||
|
typeof navigator === "undefined" ?
|
||||||
|
undefined :
|
||||||
|
navigator.language
|
||||||
|
);
|
||||||
|
|
||||||
|
if (languageLike === undefined) {
|
||||||
|
return "en";
|
||||||
|
}
|
||||||
|
|
||||||
|
return getBestMatchAmongKcLanguageTag(languageLike);
|
||||||
|
|
||||||
|
},
|
||||||
{ "persistance": "localStorage" }
|
{ "persistance": "localStorage" }
|
||||||
);
|
);
|
||||||
|
|
||||||
export const { useKcLanguageTag } = wrap;
|
export const { useKcLanguageTag } = wrap;
|
||||||
|
|
||||||
export function getEvtKcLanguage() {
|
export function getEvtKcLanguage(): StatefulEvt<KcLanguageTag> {
|
||||||
return wrap.evtKcLanguageTag;
|
return wrap.evtKcLanguageTag;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
35
src/lib/tools/AndByDiscriminatingKey.d.ts
vendored
Normal file
35
src/lib/tools/AndByDiscriminatingKey.d.ts
vendored
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
|
||||||
|
export type AndByDiscriminatingKey<
|
||||||
|
DiscriminatingKey extends string,
|
||||||
|
U1 extends Record<DiscriminatingKey, string>,
|
||||||
|
U2 extends Record<DiscriminatingKey, string>
|
||||||
|
> =
|
||||||
|
AndByDiscriminatingKey.Tf1<DiscriminatingKey, U1, U1, U2>;
|
||||||
|
|
||||||
|
export declare namespace AndByDiscriminatingKey {
|
||||||
|
|
||||||
|
export type Tf1<
|
||||||
|
DiscriminatingKey extends string,
|
||||||
|
U1,
|
||||||
|
U1Again extends Record<DiscriminatingKey, string>,
|
||||||
|
U2 extends Record<DiscriminatingKey, string>
|
||||||
|
> =
|
||||||
|
U1 extends Pick<U2, DiscriminatingKey> ?
|
||||||
|
Tf2<DiscriminatingKey, U1, U2, U1Again> :
|
||||||
|
U1;
|
||||||
|
|
||||||
|
export type Tf2<
|
||||||
|
DiscriminatingKey extends string,
|
||||||
|
SingletonU1 extends Record<DiscriminatingKey, string>,
|
||||||
|
U2,
|
||||||
|
U1 extends Record<DiscriminatingKey, string>
|
||||||
|
> =
|
||||||
|
U2 extends Pick<SingletonU1, DiscriminatingKey> ?
|
||||||
|
U2 & SingletonU1 :
|
||||||
|
U2 extends Pick<U1, DiscriminatingKey> ?
|
||||||
|
never :
|
||||||
|
U2;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
4
src/lib/tools/DeepPartial.d.ts
vendored
Normal file
4
src/lib/tools/DeepPartial.d.ts
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
|
||||||
|
export type DeepPartial<T> = {
|
||||||
|
[P in keyof T]?: DeepPartial<T[P]>;
|
||||||
|
};
|
@ -1,2 +1,2 @@
|
|||||||
|
|
||||||
export { assert } from "evt/tools/typeSafety/assert";
|
export { assert } from "tsafe/assert";
|
56
src/lib/tools/deepAssign.ts
Normal file
56
src/lib/tools/deepAssign.ts
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
|
||||||
|
import { assert } from "tsafe/assert";
|
||||||
|
import { is } from "tsafe/is";
|
||||||
|
|
||||||
|
export function deepAssign(
|
||||||
|
params: {
|
||||||
|
target: Record<string, unknown>;
|
||||||
|
source: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
|
||||||
|
const { target, source } = params;
|
||||||
|
|
||||||
|
Object.keys(source).forEach(key => {
|
||||||
|
var dereferencedSource = source[key];
|
||||||
|
|
||||||
|
if (
|
||||||
|
target[key] === undefined ||
|
||||||
|
!(dereferencedSource instanceof Object)
|
||||||
|
) {
|
||||||
|
|
||||||
|
Object.defineProperty(
|
||||||
|
target,
|
||||||
|
key,
|
||||||
|
{
|
||||||
|
"enumerable": true,
|
||||||
|
"value": dereferencedSource
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dereferencedTarget = target[key];
|
||||||
|
|
||||||
|
if (dereferencedSource instanceof Array) {
|
||||||
|
|
||||||
|
assert(is<unknown[]>(dereferencedTarget));
|
||||||
|
assert(is<unknown[]>(dereferencedSource));
|
||||||
|
|
||||||
|
dereferencedSource.forEach(entry => dereferencedTarget.push(entry));
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(is<Record<string, unknown>>(dereferencedTarget));
|
||||||
|
assert(is<Record<string, unknown>>(dereferencedSource));
|
||||||
|
|
||||||
|
deepAssign({
|
||||||
|
"target": dereferencedTarget,
|
||||||
|
"source": dereferencedSource
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
4
src/lib/tools/deepClone.ts
Normal file
4
src/lib/tools/deepClone.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
|
||||||
|
export function deepClone<T>(arg: T): T {
|
||||||
|
return JSON.parse(JSON.stringify(arg));
|
||||||
|
}
|
@ -1,6 +1,6 @@
|
|||||||
|
|
||||||
import { join as pathJoin } from "path";
|
import { join as pathJoin } from "path";
|
||||||
import { generateKeycloakThemeResources } from "../bin/build-keycloak-theme/generateKeycloakThemeResources";
|
import { generateKeycloakThemeResources } from "../../bin/build-keycloak-theme/generateKeycloakThemeResources";
|
||||||
import {
|
import {
|
||||||
setupSampleReactProject,
|
setupSampleReactProject,
|
||||||
sampleReactProjectDirPath
|
sampleReactProjectDirPath
|
@ -6,7 +6,7 @@ import {
|
|||||||
} from "./setupSampleReactProject";
|
} from "./setupSampleReactProject";
|
||||||
import * as st from "scripting-tools";
|
import * as st from "scripting-tools";
|
||||||
import { join as pathJoin } from "path";
|
import { join as pathJoin } from "path";
|
||||||
import { getProjectRoot } from "../bin/tools/getProjectRoot";
|
import { getProjectRoot } from "../../bin/tools/getProjectRoot";
|
||||||
|
|
||||||
setupSampleReactProject();
|
setupSampleReactProject();
|
||||||
|
|
@ -3,7 +3,7 @@ import {
|
|||||||
replaceImportsFromStaticInJsCode,
|
replaceImportsFromStaticInJsCode,
|
||||||
replaceImportsInCssCode,
|
replaceImportsInCssCode,
|
||||||
generateCssCodeToDefineGlobals
|
generateCssCodeToDefineGlobals
|
||||||
} from "../bin/build-keycloak-theme/replaceImportFromStatic";
|
} from "../../bin/build-keycloak-theme/replaceImportFromStatic";
|
||||||
|
|
||||||
const { fixedJsCode } = replaceImportsFromStaticInJsCode({
|
const { fixedJsCode } = replaceImportsFromStaticInJsCode({
|
||||||
"jsCode": `
|
"jsCode": `
|
@ -1,7 +1,7 @@
|
|||||||
|
|
||||||
import { getProjectRoot } from "../bin/tools/getProjectRoot";
|
import { getProjectRoot } from "../../bin/tools/getProjectRoot";
|
||||||
import { join as pathJoin } from "path";
|
import { join as pathJoin } from "path";
|
||||||
import { downloadAndUnzip } from "../bin/tools/downloadAndUnzip";
|
import { downloadAndUnzip } from "../../bin/tools/downloadAndUnzip";
|
||||||
|
|
||||||
export const sampleReactProjectDirPath = pathJoin(getProjectRoot(), "sample_react_project");
|
export const sampleReactProjectDirPath = pathJoin(getProjectRoot(), "sample_react_project");
|
||||||
|
|
207
src/test/lib/getKcContext.ts
Normal file
207
src/test/lib/getKcContext.ts
Normal file
@ -0,0 +1,207 @@
|
|||||||
|
|
||||||
|
import { getKcContext } from "../../lib/getKcContext";
|
||||||
|
import type { KcContextBase } from "../../lib/getKcContext";
|
||||||
|
import type { ExtendsKcContextBase } from "../../lib/getKcContext/getKcContext";
|
||||||
|
import { same } from "evt/tools/inDepth";
|
||||||
|
import { doExtends } from "tsafe/doExtends";
|
||||||
|
import { assert } from "tsafe/assert";
|
||||||
|
import { kcContextMocks, kcContextCommonMock } from "../../lib/getKcContext/kcContextMocks";
|
||||||
|
import { deepClone } from "../../lib/tools/deepClone";
|
||||||
|
import type { Any } from "ts-toolbelt";
|
||||||
|
|
||||||
|
|
||||||
|
const authorizedMailDomains = [
|
||||||
|
"example.com",
|
||||||
|
"another-example.com",
|
||||||
|
"*.yet-another-example.com",
|
||||||
|
"*.example.com",
|
||||||
|
"hello-world.com"
|
||||||
|
];
|
||||||
|
|
||||||
|
const displayName = "this is an overwritten common value";
|
||||||
|
|
||||||
|
const aNonStandardValue = "a non standard value";
|
||||||
|
|
||||||
|
type KcContextExtended = {
|
||||||
|
pageId: "register.ftl";
|
||||||
|
authorizedMailDomains: string[];
|
||||||
|
} | {
|
||||||
|
pageId: "my-extra-page-1.ftl";
|
||||||
|
} | {
|
||||||
|
pageId: "my-extra-page-2.ftl";
|
||||||
|
aNonStandardValue: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function getKcContextProxy(
|
||||||
|
params: {
|
||||||
|
mockPageId: ExtendsKcContextBase<KcContextExtended>["pageId"];
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
|
||||||
|
const { mockPageId } = params;
|
||||||
|
|
||||||
|
const { kcContext } = getKcContext<KcContextExtended>({
|
||||||
|
mockPageId,
|
||||||
|
"mockData": [
|
||||||
|
{
|
||||||
|
"pageId": "login.ftl",
|
||||||
|
"realm": { displayName }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pageId": "register.ftl",
|
||||||
|
authorizedMailDomains
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pageId": "my-extra-page-2.ftl",
|
||||||
|
aNonStandardValue
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
return { kcContext };
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
|
||||||
|
const pageId= "login.ftl";
|
||||||
|
|
||||||
|
const { kcContext } = getKcContextProxy({ "mockPageId": pageId });
|
||||||
|
|
||||||
|
//@ts-expect-error
|
||||||
|
doExtends<Any.Equals<typeof kcContext, any>, 1>();
|
||||||
|
|
||||||
|
assert(kcContext?.pageId === pageId);
|
||||||
|
|
||||||
|
doExtends<typeof kcContext, KcContextBase.Login>();
|
||||||
|
|
||||||
|
assert(same(
|
||||||
|
//NOTE: deepClone for printIfExists or other functions...
|
||||||
|
deepClone(kcContext),
|
||||||
|
(() => {
|
||||||
|
|
||||||
|
const mock = deepClone(kcContextMocks.find(({ pageId: pageId_i }) => pageId_i === pageId)!);
|
||||||
|
|
||||||
|
mock.realm.displayName = displayName;
|
||||||
|
|
||||||
|
return mock;
|
||||||
|
|
||||||
|
})()
|
||||||
|
));
|
||||||
|
|
||||||
|
console.log(`PASS ${pageId}`);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const pageId = "register.ftl";
|
||||||
|
|
||||||
|
const { kcContext } = getKcContextProxy({ "mockPageId": pageId });
|
||||||
|
|
||||||
|
//@ts-expect-error
|
||||||
|
doExtends<Any.Equals<typeof kcContext, any>, 1>();
|
||||||
|
|
||||||
|
assert(kcContext?.pageId === pageId);
|
||||||
|
|
||||||
|
doExtends<typeof kcContext, KcContextBase.Register>();
|
||||||
|
|
||||||
|
assert(same(
|
||||||
|
deepClone(kcContext),
|
||||||
|
(() => {
|
||||||
|
|
||||||
|
const mock = deepClone(kcContextMocks.find(({ pageId: pageId_i }) => pageId_i === pageId)!);
|
||||||
|
|
||||||
|
Object.assign(mock, { authorizedMailDomains });
|
||||||
|
|
||||||
|
return mock;
|
||||||
|
|
||||||
|
})()
|
||||||
|
));
|
||||||
|
|
||||||
|
console.log(`PASS ${pageId}`);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
|
||||||
|
const pageId = "my-extra-page-2.ftl";
|
||||||
|
|
||||||
|
const { kcContext } = getKcContextProxy({ "mockPageId": pageId });
|
||||||
|
|
||||||
|
//@ts-expect-error
|
||||||
|
doExtends<Any.Equals<typeof kcContext, any>, 1>();
|
||||||
|
|
||||||
|
assert(kcContext?.pageId === pageId);
|
||||||
|
|
||||||
|
//@ts-expect-error
|
||||||
|
doExtends<typeof kcContext, KcContextBase>();
|
||||||
|
|
||||||
|
doExtends<typeof kcContext, KcContextBase.Common>();
|
||||||
|
|
||||||
|
assert(same(
|
||||||
|
deepClone(kcContext),
|
||||||
|
(() => {
|
||||||
|
|
||||||
|
const mock = deepClone(kcContextCommonMock);
|
||||||
|
|
||||||
|
Object.assign(mock, { pageId, aNonStandardValue });
|
||||||
|
|
||||||
|
return mock;
|
||||||
|
|
||||||
|
})()
|
||||||
|
));
|
||||||
|
|
||||||
|
console.log(`PASS ${pageId}`);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
|
||||||
|
const pageId = "my-extra-page-1.ftl";
|
||||||
|
|
||||||
|
console.log("We expect a warning here =>");
|
||||||
|
|
||||||
|
const { kcContext } = getKcContextProxy({ "mockPageId": pageId });
|
||||||
|
|
||||||
|
//@ts-expect-error
|
||||||
|
doExtends<Any.Equals<typeof kcContext, any>, 1>();
|
||||||
|
|
||||||
|
assert(kcContext?.pageId === pageId);
|
||||||
|
|
||||||
|
//@ts-expect-error
|
||||||
|
doExtends<typeof kcContext, KcContextBase>();
|
||||||
|
|
||||||
|
doExtends<typeof kcContext, KcContextBase.Common>();
|
||||||
|
|
||||||
|
assert(same(
|
||||||
|
deepClone(kcContext),
|
||||||
|
(() => {
|
||||||
|
|
||||||
|
const mock = deepClone(kcContextCommonMock);
|
||||||
|
|
||||||
|
Object.assign(mock, { pageId });
|
||||||
|
|
||||||
|
return mock;
|
||||||
|
|
||||||
|
})()
|
||||||
|
));
|
||||||
|
|
||||||
|
console.log(`PASS ${pageId}`);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const { kcContext } = getKcContext();
|
||||||
|
|
||||||
|
//@ts-expect-error
|
||||||
|
doExtends<Any.Equals<typeof kcContext, any>, 1>();
|
||||||
|
|
||||||
|
doExtends<typeof kcContext, KcContextBase | undefined>();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
2
src/test/lib/index.ts
Normal file
2
src/test/lib/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
|
||||||
|
import "./getKcContext";
|
91
src/test/lib/tools/AndByDiscriminatingKey.type.ts
Normal file
91
src/test/lib/tools/AndByDiscriminatingKey.type.ts
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
|
||||||
|
import { AndByDiscriminatingKey } from "../../../lib/tools/AndByDiscriminatingKey";
|
||||||
|
import { doExtends } from "tsafe/doExtends";
|
||||||
|
|
||||||
|
type Base =
|
||||||
|
{ pageId: "a"; onlyA: string; } |
|
||||||
|
{ pageId: "b"; onlyB: string; } |
|
||||||
|
{ pageId: "only base"; onlyBase: string; };
|
||||||
|
|
||||||
|
type Extension =
|
||||||
|
{ pageId: "a"; onlyExtA: string; } |
|
||||||
|
{ pageId: "b"; onlyExtB: string; } |
|
||||||
|
{ pageId: "only ext"; onlyExt: string; };
|
||||||
|
|
||||||
|
type Got = AndByDiscriminatingKey<"pageId", Extension, Base>;
|
||||||
|
|
||||||
|
type Expected =
|
||||||
|
{ pageId: "a"; onlyA: string; onlyExtA: string; } |
|
||||||
|
{ pageId: "b"; onlyB: string; onlyExtB: string; } |
|
||||||
|
{ pageId: "only base"; onlyBase: string; } |
|
||||||
|
{ pageId: "only ext"; onlyExt: string; };
|
||||||
|
|
||||||
|
doExtends<Got, Expected>();
|
||||||
|
doExtends<Expected, Got>();
|
||||||
|
|
||||||
|
const x: Got = null as any;
|
||||||
|
|
||||||
|
if (x.pageId === "a") {
|
||||||
|
|
||||||
|
x.onlyA;
|
||||||
|
x.onlyExtA;
|
||||||
|
|
||||||
|
//@ts-expect-error
|
||||||
|
x.onlyB;
|
||||||
|
|
||||||
|
//@ts-expect-error
|
||||||
|
x.onlyBase;
|
||||||
|
|
||||||
|
//@ts-expect-error
|
||||||
|
x.onlyExt;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (x.pageId === "b") {
|
||||||
|
|
||||||
|
x.onlyB;
|
||||||
|
x.onlyExtB;
|
||||||
|
|
||||||
|
//@ts-expect-error
|
||||||
|
x.onlyA;
|
||||||
|
|
||||||
|
//@ts-expect-error
|
||||||
|
x.onlyBase;
|
||||||
|
|
||||||
|
//@ts-expect-error
|
||||||
|
x.onlyExt;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if (x.pageId === "only base") {
|
||||||
|
|
||||||
|
x.onlyBase;
|
||||||
|
|
||||||
|
//@ts-expect-error
|
||||||
|
x.onlyA;
|
||||||
|
|
||||||
|
//@ts-expect-error
|
||||||
|
x.onlyB;
|
||||||
|
|
||||||
|
//@ts-expect-error
|
||||||
|
x.onlyExt;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if (x.pageId === "only ext") {
|
||||||
|
|
||||||
|
x.onlyExt;
|
||||||
|
|
||||||
|
|
||||||
|
//@ts-expect-error
|
||||||
|
x.onlyA;
|
||||||
|
|
||||||
|
//@ts-expect-error
|
||||||
|
x.onlyB;
|
||||||
|
|
||||||
|
//@ts-expect-error
|
||||||
|
x.onlyBase;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
34
yarn.lock
34
yarn.lock
@ -514,6 +514,15 @@ evt@2.0.0-beta.15:
|
|||||||
minimal-polyfills "^2.1.5"
|
minimal-polyfills "^2.1.5"
|
||||||
run-exclusive "^2.2.14"
|
run-exclusive "^2.2.14"
|
||||||
|
|
||||||
|
evt@2.0.0-beta.20:
|
||||||
|
version "2.0.0-beta.20"
|
||||||
|
resolved "https://registry.yarnpkg.com/evt/-/evt-2.0.0-beta.20.tgz#a018ff46a7dcb475c488e6508ac3aec7910e76fb"
|
||||||
|
integrity sha512-aIIqrDkQ5Hm4zzW7v9STZbO9RcwAI2vMQUkjQ7Bw4YkWOv2zoIl6ooJZ6942JU7UCYKWSi1u5BBtyoiXUSsnMA==
|
||||||
|
dependencies:
|
||||||
|
minimal-polyfills "^2.2.1"
|
||||||
|
run-exclusive "^2.2.14"
|
||||||
|
tsafe "^0.2.2"
|
||||||
|
|
||||||
ext@^1.1.2:
|
ext@^1.1.2:
|
||||||
version "1.4.0"
|
version "1.4.0"
|
||||||
resolved "https://registry.yarnpkg.com/ext/-/ext-1.4.0.tgz#89ae7a07158f79d35517882904324077e4379244"
|
resolved "https://registry.yarnpkg.com/ext/-/ext-1.4.0.tgz#89ae7a07158f79d35517882904324077e4379244"
|
||||||
@ -770,7 +779,7 @@ micromark@~2.11.0:
|
|||||||
debug "^4.0.0"
|
debug "^4.0.0"
|
||||||
parse-entities "^2.0.0"
|
parse-entities "^2.0.0"
|
||||||
|
|
||||||
minimal-polyfills@^2.1.5, minimal-polyfills@^2.1.6:
|
minimal-polyfills@^2.1.5, minimal-polyfills@^2.1.6, minimal-polyfills@^2.2.1:
|
||||||
version "2.2.1"
|
version "2.2.1"
|
||||||
resolved "https://registry.yarnpkg.com/minimal-polyfills/-/minimal-polyfills-2.2.1.tgz#7249d7ece666d3b4e1ec1c1b8f949eb9d44e2308"
|
resolved "https://registry.yarnpkg.com/minimal-polyfills/-/minimal-polyfills-2.2.1.tgz#7249d7ece666d3b4e1ec1c1b8f949eb9d44e2308"
|
||||||
integrity sha512-WLmHQrsZob4rVYf8yHapZPNJZ3sspGa/sN8abuSD59b0FifDEE7HMfLUi24z7mPZqTpBXy4Svp+iGvAmclCmXg==
|
integrity sha512-WLmHQrsZob4rVYf8yHapZPNJZ3sspGa/sN8abuSD59b0FifDEE7HMfLUi24z7mPZqTpBXy4Svp+iGvAmclCmXg==
|
||||||
@ -893,10 +902,10 @@ path@^0.12.7:
|
|||||||
process "^0.11.1"
|
process "^0.11.1"
|
||||||
util "^0.10.3"
|
util "^0.10.3"
|
||||||
|
|
||||||
powerhooks@^0.1.0:
|
powerhooks@^0.1.6:
|
||||||
version "0.1.4"
|
version "0.1.6"
|
||||||
resolved "https://registry.yarnpkg.com/powerhooks/-/powerhooks-0.1.4.tgz#82aae8dae5485a154d3ce4e89342a2d68cb2a413"
|
resolved "https://registry.yarnpkg.com/powerhooks/-/powerhooks-0.1.6.tgz#23dfb6c781bbfdd20ea37ff5a2a0fd5a9799d591"
|
||||||
integrity sha512-ig47hJIW/b75gCS3l2EtK8NVAduNVd9vem8IvaWkuPuZIP4mbDAy4Rc0/hvjXVUPs9OzK7jc/zIQika3tTacYg==
|
integrity sha512-S1b5awSAimyt9jXZsm+N8/GFBtzLPzmVIUyVLq/IADDe4aAcxFWBgXSWTOFc31PlWE9uz2K/NovsQNDf4Fr52A==
|
||||||
dependencies:
|
dependencies:
|
||||||
evt "2.0.0-beta.15"
|
evt "2.0.0-beta.15"
|
||||||
memoizee "^0.4.15"
|
memoizee "^0.4.15"
|
||||||
@ -1121,11 +1130,26 @@ trough@^1.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/trough/-/trough-1.0.5.tgz#b8b639cefad7d0bb2abd37d433ff8293efa5f406"
|
resolved "https://registry.yarnpkg.com/trough/-/trough-1.0.5.tgz#b8b639cefad7d0bb2abd37d433ff8293efa5f406"
|
||||||
integrity sha512-rvuRbTarPXmMb79SmzEp8aqXNKcK+y0XaB298IXueQ8I2PsrATcPBCSPyK/dDNa2iWOhKlfNnOjdAOTBU/nkFA==
|
integrity sha512-rvuRbTarPXmMb79SmzEp8aqXNKcK+y0XaB298IXueQ8I2PsrATcPBCSPyK/dDNa2iWOhKlfNnOjdAOTBU/nkFA==
|
||||||
|
|
||||||
|
ts-toolbelt@^9.6.0:
|
||||||
|
version "9.6.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/ts-toolbelt/-/ts-toolbelt-9.6.0.tgz#50a25426cfed500d4a09bd1b3afb6f28879edfd5"
|
||||||
|
integrity sha512-nsZd8ZeNUzukXPlJmTBwUAuABDe/9qtVDelJeT/qW0ow3ZS3BsQJtNkan1802aM9Uf68/Y8ljw86Hu0h5IUW3w==
|
||||||
|
|
||||||
tsafe@^0.1.0:
|
tsafe@^0.1.0:
|
||||||
version "0.1.15"
|
version "0.1.15"
|
||||||
resolved "https://registry.yarnpkg.com/tsafe/-/tsafe-0.1.15.tgz#9e2b6137fb5a49fc7c23cb0bc49ae09bab215f2a"
|
resolved "https://registry.yarnpkg.com/tsafe/-/tsafe-0.1.15.tgz#9e2b6137fb5a49fc7c23cb0bc49ae09bab215f2a"
|
||||||
integrity sha512-aAWMOACHXMmwE2zRcQpBv+whxYqB4zvAbs+dzwbnGaGK9NAOQ65m0+WyO+jw/41JzCX7orJU/ieFHTmgehTOKA==
|
integrity sha512-aAWMOACHXMmwE2zRcQpBv+whxYqB4zvAbs+dzwbnGaGK9NAOQ65m0+WyO+jw/41JzCX7orJU/ieFHTmgehTOKA==
|
||||||
|
|
||||||
|
tsafe@^0.2.2:
|
||||||
|
version "0.2.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/tsafe/-/tsafe-0.2.4.tgz#f2cffb9b33e4d20c6f9800aa1c45bee02d917aa1"
|
||||||
|
integrity sha512-oS/0wJKgEv/iDow/6U51xjAascJVtUQfZQMCDdoOS+VnMlWZFdXGMKuoH47JYs6hEWHpO+Z5pC5oAFFHiPfTLg==
|
||||||
|
|
||||||
|
tsafe@^0.4.1:
|
||||||
|
version "0.4.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/tsafe/-/tsafe-0.4.1.tgz#00af1be2db82abb4be531209b90232d7954e1a03"
|
||||||
|
integrity sha512-+OZ0gdgmwcru+MOSheCx+ymAvQz+1/ui+KFJRuaq0t2m8RNrlf7eSzEieptoPQXPY67Mdkqgkdjknn8azoD5sw==
|
||||||
|
|
||||||
tslib@^2.2.0:
|
tslib@^2.2.0:
|
||||||
version "2.3.0"
|
version "2.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.0.tgz#803b8cdab3e12ba581a4ca41c8839bbb0dacb09e"
|
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.0.tgz#803b8cdab3e12ba581a4ca41c8839bbb0dacb09e"
|
||||||
|
Reference in New Issue
Block a user