Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Create ChainedTemplateEngine for combining engines #887

Merged
merged 1 commit into from Aug 3, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
17 changes: 16 additions & 1 deletion config/identity/handler/default.json
Expand Up @@ -23,7 +23,22 @@
"providerFactory": { "@id": "urn:solid-server:default:IdentityProviderFactory" },
"templateHandler": {
"@type": "TemplateHandler",
"templateEngine": { "@type": "EjsTemplateEngine" }
"templateEngine": {
"comment": "Renders the specific page and embeds it into the main HTML body.",
"@type": "ChainedTemplateEngine",
"renderedName": "htmlBody",
"engines": [
{
"comment": "Will be called with specific interaction templates to generate HTML snippets.",
"@type": "EjsTemplateEngine"
},
{
"comment": "Will embed the result of the first engine into the main HTML template.",
"@type": "EjsTemplateEngine",
"template": "$PACKAGE_ROOT/templates/main.html.ejs",
}
]
}
},
"interactionCompleter": {
"comment": "Responsible for finishing OIDC interactions.",
Expand Down
Expand Up @@ -7,7 +7,7 @@
"@type": "InteractionRoute",
"route": "^/forgotpassword/?$",
"viewTemplate": "$PACKAGE_ROOT/templates/identity/email-password/forgot-password.html.ejs",
"responseTemplate": "$PACKAGE_ROOT/templates/identity/email-password/email-sent.html.ejs",
"responseTemplate": "$PACKAGE_ROOT/templates/identity/email-password/forgot-password-response.html.ejs",
"handler": {
"@type": "ForgotPasswordHandler",
"args_accountStore": { "@id": "urn:solid-server:auth:password:AccountStore" },
Expand Down
Expand Up @@ -8,7 +8,7 @@
"@type": "InteractionRoute",
"route": "^/resetpassword(/[^/]*)?$",
"viewTemplate": "$PACKAGE_ROOT/templates/identity/email-password/reset-password.html.ejs",
"responseTemplate": "$PACKAGE_ROOT/templates/identity/email-password/message.html.ejs",
"responseTemplate": "$PACKAGE_ROOT/templates/identity/email-password/reset-password-response.html.ejs",
"handler": {
"@type": "ResetPasswordHandler",
"accountStore": { "@id": "urn:solid-server:auth:password:AccountStore" }
Expand Down
Expand Up @@ -11,8 +11,8 @@
"@id": "urn:solid-server:default:MarkdownToHtmlConverter",
"@type": "MarkdownToHtmlConverter",
"templateEngine": {
"@type": "HandlebarsTemplateEngine",
"template": "$PACKAGE_ROOT/templates/main.html.hbs"
"@type": "EjsTemplateEngine",
"template": "$PACKAGE_ROOT/templates/main.html.ejs"
}
},
{
Expand Down
4 changes: 2 additions & 2 deletions src/identity/IdentityProviderHttpHandler.ts
Expand Up @@ -182,9 +182,9 @@ export class IdentityProviderHttpHandler extends HttpHandler {
throw new BadRequestHttpError(`Unsupported request: ${request.method} ${request.url}`);
}

private async handleTemplateResponse(response: HttpResponse, templateFile: string, contents: NodeJS.Dict<any>):
private async handleTemplateResponse(response: HttpResponse, templateFile: string, contents?: NodeJS.Dict<any>):
Promise<void> {
await this.templateHandler.handleSafe({ response, templateFile, contents });
await this.templateHandler.handleSafe({ response, templateFile, contents: contents ?? {}});
}

/**
Expand Down
Expand Up @@ -6,7 +6,7 @@ export type InteractionHandlerResult = InteractionResponseResult | InteractionCo

export interface InteractionResponseResult<T = NodeJS.Dict<any>> {
type: 'response';
details: T;
details?: T;
}

export interface InteractionCompleteResult {
Expand Down
Expand Up @@ -34,7 +34,7 @@ export class ResetPasswordHandler extends InteractionHandler {
assertPassword(password, confirmPassword);

await this.resetPassword(recordId, password);
return { type: 'response', details: { message: 'Your password was successfully reset.' }};
return { type: 'response' };
} catch (error: unknown) {
throwIdpInteractionError(error);
}
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Expand Up @@ -304,6 +304,7 @@ export * from './util/locking/SingleThreadedResourceLocker';
export * from './util/locking/WrappedExpiringReadWriteLocker';

// Util/Templates
export * from './util/templates/ChainedTemplateEngine';
export * from './util/templates/EjsTemplateEngine';
export * from './util/templates/HandlebarsTemplateEngine';
export * from './util/templates/TemplateEngine';
Expand Down
6 changes: 1 addition & 5 deletions src/storage/conversion/MarkdownToHtmlConverter.ts
Expand Up @@ -23,12 +23,8 @@ export class MarkdownToHtmlConverter extends TypedRepresentationConverter {

public async handle({ representation }: RepresentationConverterArgs): Promise<Representation> {
const markdown = await readableToString(representation.data);
// Try to extract the main title for use in the <title> tag
const title = /^#+\s*([^\n]+)\n/u.exec(markdown)?.[1];

// Place the rendered Markdown into the HTML template
const htmlBody = marked(markdown);
const html = await this.templateEngine.render({ htmlBody, title });
const html = await this.templateEngine.render({ htmlBody });

return new BasicRepresentation(html, representation.metadata, TEXT_HTML);
}
Expand Down
39 changes: 39 additions & 0 deletions src/util/templates/ChainedTemplateEngine.ts
@@ -0,0 +1,39 @@
import type { Template, TemplateEngine } from './TemplateEngine';
import Dict = NodeJS.Dict;

/**
* Calls the given array of {@link TemplateEngine}s in the order they appear,
* feeding the output of one into the input of the next.
*
* The first engine will be called with the provided contents and template parameters.
* All subsequent engines will be called with no template parameter.
* Contents will still be passed along and another entry will be added for the body of the previous output.
*/
export class ChainedTemplateEngine<T extends Dict<any> = Dict<any>> implements TemplateEngine<T> {
private readonly firstEngine: TemplateEngine<T>;
private readonly chainedEngines: TemplateEngine[];
private readonly renderedName: string;

/**
* @param engines - Engines will be executed in the same order as the array.
* @param renderedName - The name of the key used to pass the body of one engine to the next.
*/
public constructor(engines: TemplateEngine[], renderedName = 'body') {
if (engines.length === 0) {
throw new Error('At least 1 engine needs to be provided.');
}
this.firstEngine = engines[0];
this.chainedEngines = engines.slice(1);
this.renderedName = renderedName;
}

public async render(contents: T): Promise<string>;
public async render<TCustom = T>(contents: TCustom, template: Template): Promise<string>;
public async render<TCustom = T>(contents: TCustom, template?: Template): Promise<string> {
let body = await this.firstEngine.render(contents, template!);
for (const engine of this.chainedEngines) {
body = await engine.render({ ...contents, [this.renderedName]: body });
}
return body;
}
}
31 changes: 4 additions & 27 deletions templates/identity/email-password/confirm.html.ejs
@@ -1,27 +1,4 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<title>Authorize</title>
<link rel="stylesheet" href="/.well_known/css/styles/main.css" type="text/css">
</head>
<body>
<header>
<a href="/"><img src="/.well_known/css/images/solid.svg" alt="[Solid logo]" /></a>
<h1>Community Solid Server</h1>
</header>
<main>
<h1>Authorize</h1>
<form action="/idp/confirm" method="post">
<p class="actions"><button autofocus type="submit" name="submit" class="ids-link-filled">Continue</button></p>
</form>
</main>
<footer>
<p>
©2019–2021 <a href="https://inrupt.com/">Inrupt Inc.</a>
and <a href="https://www.imec-int.com/">imec</a>
</p>
</footer>
</body>
</html>
<h1>Authorize</h1>
<form action="/idp/confirm" method="post">
<p class="actions"><button autofocus type="submit" name="submit" class="ids-link-filled">Continue</button></p>
</form>
37 changes: 0 additions & 37 deletions templates/identity/email-password/email-sent.html.ejs

This file was deleted.

@@ -0,0 +1,13 @@
<h1>Email sent</h1>
<form action="/idp/forgotpassword" method="post">
<p>If your account exists, an email has been sent with a link to reset your password.</p>
<p>If you do not receive your email in a couple of minutes, check your spam folder or click the link below to send another email.</p>

<input type="hidden" name="email" value="<%= email %>" />

<p class="actions"><a href="/idp/login">Back to Log In</a></p>

<p class="actions">
<button type="submit" name="submit" class="link">Send Another Email</button>
</p>
</form>
55 changes: 16 additions & 39 deletions templates/identity/email-password/forgot-password.html.ejs
@@ -1,42 +1,19 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<title>Forgot password</title>
<link rel="stylesheet" href="/.well_known/css/styles/main.css" type="text/css">
</head>
<body>
<header>
<a href="/"><img src="/.well_known/css/images/solid.svg" alt="[Solid logo]" /></a>
<h1>Community Solid Server</h1>
</header>
<main>
<h1>Forgot password</h1>
<form action="/idp/forgotpassword" method="post">
<%if (errorMessage) { %>
<p class="error"><%= errorMessage %></p>
<% } %>
<h1>Forgot password</h1>
<form action="/idp/forgotpassword" method="post">
<%if (errorMessage) { %>
<p class="error"><%= errorMessage %></p>
<% } %>

<fieldset>
<ol>
<li>
<label for="email">Email</label>
<input id="email" type="email" name="email" autofocus>
</li>
</ol>
</fieldset>
<fieldset>
<ol>
<li>
<label for="email">Email</label>
<input id="email" type="email" name="email" autofocus>
</li>
</ol>
</fieldset>

<p class="actions"><button type="submit" name="submit">Send recovery email</button></p>
<p class="actions"><button type="submit" name="submit">Send recovery email</button></p>

<p class="actions"><a href="/idp/login" class="link">Log in</a></p>
</form>
</main>
<footer>
<p>
©2019–2021 <a href="https://inrupt.com/">Inrupt Inc.</a>
and <a href="https://www.imec-int.com/">imec</a>
</p>
</footer>
</body>
</html>
<p class="actions"><a href="/idp/login" class="link">Log in</a></p>
</form>
77 changes: 27 additions & 50 deletions templates/identity/email-password/login.html.ejs
@@ -1,53 +1,30 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<title>Log in</title>
<link rel="stylesheet" href="/.well_known/css/styles/main.css" type="text/css">
</head>
<body>
<header>
<a href="/"><img src="/.well_known/css/images/solid.svg" alt="[Solid logo]" /></a>
<h1>Community Solid Server</h1>
</header>
<main>
<h1>Log in</h1>
<form action="/idp/login" method="post">
<%if (errorMessage) { %>
<p class="error"><%= errorMessage %></p>
<% } %>
<h1>Log in</h1>
<form action="/idp/login" method="post">
<%if (errorMessage) { %>
<p class="error"><%= errorMessage %></p>
<% } %>

<fieldset>
<legend>Your account</legend>
<ol>
<li>
<label for="email">Email</label>
<input id="email" type="email" name="email" autofocus <% if (prefilled.email) { %> value="<%= prefilled.email %>" <% } %>>
</li>
<li>
<label for="password">Password</label>
<input id="password" type="password" name="password">
</li>
<li class="checkbox">
<label><input type="checkbox" name="remember" value="yes" checked>Stay logged in</label>
</li>
</ol>
</fieldset>
<fieldset>
<legend>Your account</legend>
<ol>
<li>
<label for="email">Email</label>
<input id="email" type="email" name="email" autofocus <% if (prefilled.email) { %> value="<%= prefilled.email %>" <% } %>>
</li>
<li>
<label for="password">Password</label>
<input id="password" type="password" name="password">
</li>
<li class="checkbox">
<label><input type="checkbox" name="remember" value="yes" checked>Stay logged in</label>
</li>
</ol>
</fieldset>

<p class="actions"><button type="submit" name="submit">Log in</button></p>
<p class="actions"><button type="submit" name="submit">Log in</button></p>

<ul class="actions">
<li><a href="/idp/register" class="link">Sign up</a></li>
<li><a href="/idp/forgotpassword" class="link">Forgot password</a></li>
</ul>
</form>
</main>
<footer>
<p>
©2019–2021 <a href="https://inrupt.com/">Inrupt Inc.</a>
and <a href="https://www.imec-int.com/">imec</a>
</p>
</footer>
</body>
</html>
<ul class="actions">
<li><a href="/idp/register" class="link">Sign up</a></li>
<li><a href="/idp/forgotpassword" class="link">Forgot password</a></li>
</ul>
</form>