Skip to content

Commit

Permalink
feat: add htmx support
Browse files Browse the repository at this point in the history
  • Loading branch information
vlad-tkachenko committed Nov 23, 2023
1 parent b7b5c63 commit 4f4f8e5
Show file tree
Hide file tree
Showing 15 changed files with 155 additions and 19 deletions.
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,8 +113,10 @@ Mappings format:
It is highly recommended to intercept 401 errors on the Web Application side and reload the page, so `MAPPINGS_PAGES` mapping flow is triggered and user gets redirected to the login page.

#### Redirects
- `REDIRECT_PAGE_REQUEST_ON_403` - [optional] URL to redirect when access is forbidden
- `REDIRECT_PAGE_REQUEST_ON_404` - [optional] URL to redirect when no mapping found for requested path
- `REDIRECT_PAGE_REQUEST_ON_403` - [optional] URL to redirect when no access is denied, as none of the JWT claims matching mappings
- `REDIRECT_PAGE_REQUEST_ON_500` - [optional] URL to redirect when unexpected error occurred
- `REDIRECT_PAGE_REQUEST_ON_503` - [optional] URL to redirect when connection to the upstream service cannot be established

#### Headers

Expand Down Expand Up @@ -165,3 +167,7 @@ More can be found in [LICENSE.md](https://github.com/FireBlinkLTD/prxi-openid-co
### Contact Information

To obtain a commercial license [click here](https://fireblink.com/#contact-us) to get in a contact.

## HTMX Support

Every time prxi-openid-connect needs to send a redirect it checks an incoming request to have the `Hx-Boosted` header. If header is found and its value is `true` then prxi-openid-connect will return `200` status code with [Hx-Redirect](https://htmx.org/reference/#response_headers) header instead of making a standard HTTP redirect.
2 changes: 2 additions & 0 deletions src/config/Config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ export interface Config {
pageRequest: {
e404?: string;
e403?: string;
e500?: string;
e503?: string;
}
},

Expand Down
2 changes: 2 additions & 0 deletions src/config/getConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ export const getConfig = () => {
pageRequest: {
e404: process.env.REDIRECT_PAGE_REQUEST_ON_404,
e403: process.env.REDIRECT_PAGE_REQUEST_ON_403,
e500: process.env.REDIRECT_PAGE_REQUEST_ON_500,
e503: process.env.REDIRECT_PAGE_REQUEST_ON_503,
}
},

Expand Down
4 changes: 2 additions & 2 deletions src/handlers/CallbackHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ export const CallbackHandler: RequestHandlerConfig = {
if (result.reject) {
logger.child({originalPath}).info('Webhook rejected the request');
if (getConfig().redirect.pageRequest.e403) {
sendRedirect(res, getConfig().redirect.pageRequest.e403);
sendRedirect(req, res, getConfig().redirect.pageRequest.e403);
} else {
sendErrorResponse(req, 403, result.reason || 'Forbidden', res);
}
Expand Down Expand Up @@ -83,6 +83,6 @@ export const CallbackHandler: RequestHandlerConfig = {
}

setAuthCookies(res, tokens, metaToken);
await sendRedirect(res, redirectTo);
await sendRedirect(req, res, redirectTo);
}
}
2 changes: 1 addition & 1 deletion src/handlers/E404Handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export const E404Handler: RequestHandlerConfig = {
logger.child({ method, path }).error('Request handler not found');

if (getConfig().redirect.pageRequest.e404) {
sendRedirect(res, getConfig().redirect.pageRequest.e404);
sendRedirect(req, res, getConfig().redirect.pageRequest.e404);
return;
}

Expand Down
21 changes: 19 additions & 2 deletions src/handlers/ErrorHandler.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,29 @@
import { IncomingMessage, ServerResponse } from "http";
import { ErrorHandler } from "prxi";
import { sendErrorResponse } from "../utils/ResponseUtils";
import { sendErrorResponse, sendRedirect } from "../utils/ResponseUtils";
import getLogger from "../Logger";
import { getConfig } from "../config/getConfig";

export const errorHandler: ErrorHandler = async (req: IncomingMessage, res: ServerResponse, err: Error) => {
console.log(err);
const logger = getLogger('ErrorHandler');
logger.child({ error: err.message }).error('Unexpected error occurred');
await sendErrorResponse(req, 500, 'Unexpected error occurred', res);

let code = 500;
let message = 'Unexpected error occurred';
let redirectTo = getConfig().redirect.pageRequest.e500;

if ((<any> err).code === 'ECONNREFUSED') {
code = 503;
message = 'Service Unavailable';
redirectTo = getConfig().redirect.pageRequest.e503;
}

if (redirectTo) {
sendRedirect(req, res, redirectTo);
return;
}

await sendErrorResponse(req, code, message, res);
}

2 changes: 1 addition & 1 deletion src/handlers/LoginHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,6 @@ export class LoginHandler implements RequestHandlerConfig {
invalidateAuthCookies(res);
}

await sendRedirect(res, OpenIDUtils.getAuthorizationUrl());
await sendRedirect(req, res, OpenIDUtils.getAuthorizationUrl());
}
}
2 changes: 1 addition & 1 deletion src/handlers/LogoutHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export class LogoutHandler implements RequestHandlerConfig {
invalidateAuthCookies(res);
await this.handleWebhook(req);

await sendRedirect(res, OpenIDUtils.getEndSessionUrl());
await sendRedirect(req, res, OpenIDUtils.getEndSessionUrl());
}

/**
Expand Down
6 changes: 3 additions & 3 deletions src/handlers/ProxyHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ export class ProxyHandler implements RequestHandlerConfig {

if (context.mapping.auth.required) {
if (context.page) {
await sendRedirect(res, OpenIDUtils.getAuthorizationUrl());
await sendRedirect(req, res, OpenIDUtils.getAuthorizationUrl());
} else {
await sendErrorResponse(req, 401, 'Unauthorized', res);
}
Expand All @@ -241,7 +241,7 @@ export class ProxyHandler implements RequestHandlerConfig {
invalidateAuthCookies(res);

if (context.page) {
await sendRedirect(res, OpenIDUtils.getAuthorizationUrl());
await sendRedirect(req, res, OpenIDUtils.getAuthorizationUrl());
} else {
sendErrorResponse(req, 401, 'Unauthorized', res);
}
Expand All @@ -266,7 +266,7 @@ export class ProxyHandler implements RequestHandlerConfig {

if (!claims) {
if (context.page && getConfig().redirect.pageRequest.e403) {
sendRedirect(res, getConfig().redirect.pageRequest.e403);
sendRedirect(req, res, getConfig().redirect.pageRequest.e403);
} else {
sendErrorResponse(req, 403, 'Forbidden', res);
}
Expand Down
20 changes: 14 additions & 6 deletions src/utils/ResponseUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,11 +129,19 @@ const setCookies = (resp: ServerResponse, cookies: Record<string, {value: string
* @param resp
* @param url
*/
export const sendRedirect = async (resp: ServerResponse, url: string): Promise<void> => {
getLogger('ResponseUtils').child({ url }).debug('Sending redirect');
resp.statusCode = 307;
resp.setHeader('Location', url);
resp.end();
export const sendRedirect = async (req: IncomingMessage, resp: ServerResponse, url: string): Promise<void> => {
if (req.headers['hx-boosted'] === 'true') {
getLogger('ResponseUtils').child({ url }).debug('HTMX boosted request detected, sending hx-redirect header');
resp.setHeader('hx-redirect', url);
await sendJsonResponse(200, {
redirectTo: url
}, resp);
} else {
getLogger('ResponseUtils').child({ url }).debug('Sending redirect');
resp.statusCode = 307;
resp.setHeader('Location', url);
resp.end();
}
}

/**
Expand Down Expand Up @@ -176,7 +184,7 @@ export const sendJsonResponse = async (statusCode: number, json: any, resp: Serv
* @param content
* @param resp
*/
export const sendResponse = async (statusCode: number, contentType: string, content: any, resp: ServerResponse): Promise<void> => {
const sendResponse = async (statusCode: number, contentType: string, content: any, resp: ServerResponse): Promise<void> => {
getLogger('ResponseUtils').debug('Setting response');
resp.statusCode = statusCode;
resp.setHeader('content-type', contentType);
Expand Down
8 changes: 7 additions & 1 deletion test/Base.suite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,11 @@ export class BaseSuite {
/**
* Open page, navigate and call the handler
* @param url
* @param handler
* @param extraHeaders
* @returns
*/
protected async withNewPage(url: string, handler: (page: Page) => Promise<void>): Promise<void> {
protected async withNewPage(url: string, handler: (page: Page) => Promise<void>, beforeNavigate?: (page: Page) => Promise<void>): Promise<void> {
console.log(`[puppeteer] -> Launching browser`);
const browser = await puppeteer.launch({
headless: 'new',
Expand All @@ -65,6 +67,10 @@ export class BaseSuite {
console.log(`[puppeteer] -> Opening new page`);
const page = await browser.newPage();
try {
if (beforeNavigate) {
await beforeNavigate(page);
}

await this.navigate(page, url);

console.log(`[puppeteer] -> Calling the handler`);
Expand Down
42 changes: 42 additions & 0 deletions test/ErrorHandler.suite.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { suite, test } from "@testdeck/mocha";
import { BaseSuite } from "./Base.suite";
import { getConfig } from "../src/config/getConfig";
import { strictEqual } from "assert";

@suite()
class ErrorHandlerSuite extends BaseSuite {
@test()
async e503() {
await this.reloadPrxiWith({
// set incorrect upstream
upstream: getConfig().upstream + 1,
})

await this.withNewPage(getConfig().hostURL + '/pages/test', async (page) => {
await this.loginOnKeycloak(page);

const text = await this.getTextFromPage(page);
strictEqual(text, '503: Service Unavailable')
});
}

@test()
async e503WithRedirect() {
await this.reloadPrxiWith({
// set incorrect upstream
upstream: getConfig().upstream + 1,
redirect: {
pageRequest: {
e503: getConfig().upstream + '/api/test'
}
}
})

await this.withNewPage(getConfig().hostURL + '/pages/test', async (page) => {
await this.loginOnKeycloak(page);

const json = await this.getJsonFromPage(page);
strictEqual(json.http.originalUrl, '/api/test');
});
}
}
2 changes: 1 addition & 1 deletion test/HealthzHandler.suite.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { suite, test } from "@testdeck/mocha";
import { BaseSuite } from "./Base.suite";
import { getConfig } from "../src/config/getConfig";
import { deepEqual, ok, strictEqual } from "assert";
import { strictEqual } from "assert";

@suite()
class HealthzHandlerSuite extends BaseSuite {
Expand Down
32 changes: 32 additions & 0 deletions test/LoginHandler.suite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { suite, test } from "@testdeck/mocha";
import { BaseSuite } from "./Base.suite";
import { getConfig } from "../src/config/getConfig";
import { strictEqual } from "assert";
import { OpenIDUtils } from "../src/utils/OpenIDUtils";

@suite()
export class LoginHandlerSuite extends BaseSuite {
Expand Down Expand Up @@ -30,4 +31,35 @@ export class LoginHandlerSuite extends BaseSuite {
strictEqual(json.http.originalUrl, uri);
});
}

@test()
async htmxRedirect() {
let headers: Record<string, string> = null;
let status: number = null;
let url: string = null;
await this.withNewPage(
getConfig().hostURL + '/pages/test',
// after navigate
async (page) => {
const json = await this.getJsonFromPage(page);

strictEqual(status, 200);
strictEqual(url, getConfig().hostURL + '/pages/test');
strictEqual(headers && headers['hx-redirect'], OpenIDUtils.getAuthorizationUrl());
strictEqual(json.redirectTo.replace(/&amp;/g, '&'), OpenIDUtils.getAuthorizationUrl());
},
// before navigate
async (page) => {
await page.setExtraHTTPHeaders({'Hx-Boosted': 'true'});

page.once('response', async(response) => {
if (!headers) {
headers = response.headers();
status = response.status();
url = response.url();
}
})
}
);
}
}
21 changes: 21 additions & 0 deletions test/WebhookHandler.suite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,27 @@ class PublicMappingSuite extends BaseSuite {
});
}

@test()
async testLoginFailureWithE500Redirect(): Promise<void> {
await this.reloadPrxiWith({
webhook: {
login: PublicMappingSuite.loginFailure,
},
redirect: {
pageRequest: {
e500: '/api/test'
}
}
});

await this.withNewPage(getConfig().hostURL + '/pages/test', async (page) => {
await this.loginOnKeycloak(page);

const url = page.url();
strictEqual(url, getConfig().hostURL + '/api/test');
});
}

@test()
async testMeta(): Promise<void> {
await this.reloadPrxiWith({
Expand Down

0 comments on commit 4f4f8e5

Please sign in to comment.