start building dashboard

This commit is contained in:
2023-08-04 17:39:04 -07:00
parent 811629eda9
commit 7c205632cb
18 changed files with 389 additions and 43 deletions

View File

@@ -11,6 +11,7 @@ import { LoggingConfig, validate_logging_conf } from './logger';
import { Argon2HashConfig } from './security/argon-hash';
import { SessionCookieConfig, validate_session_cookie_conf } from './security/session';
import { HttpConfig } from './http/server';
import { ColorThemeConfig } from './utilities/color-themes';
const conf_dir = process.env.CONF_PATH;
@@ -32,6 +33,7 @@ export interface Conf {
snowflake_uid: SnowflakeConfig;
storage: StorageConfig;
argon2: Argon2HashConfig;
color_themes: ColorThemeConfig;
}
export async function load_conf() : Promise<unknown> {

View File

@@ -0,0 +1,20 @@
import { Req } from '../../http/request';
import { HttpConfig } from '../../http/server';
import { HttpWebDependencies } from '../server';
import { csp_headers } from '../../http/content-security-policy';
import { FastifyInstance, RouteShorthandOptions } from 'fastify';
export function register_color_themes_endpoint(http_server: FastifyInstance, conf: HttpConfig, { color_themes }: HttpWebDependencies) {
const opts: RouteShorthandOptions = {
schema: { },
};
http_server.get('/themes.css', opts, async (req: Req, res) => {
res.status(200);
res.header('content-type', 'text/css; charset=utf-8');
res.header('cache-control', 'public, max-age=3600');
csp_headers(res, conf.exposed_url);
return color_themes.css;
});
}

View File

@@ -0,0 +1,22 @@
import { Req } from '../../http/request';
import { HttpConfig } from '../../http/server';
import { HttpWebDependencies } from '../server';
import { csp_headers } from '../../http/content-security-policy';
import { FastifyInstance, RouteShorthandOptions } from 'fastify';
import { load_template } from '../../utilities/mustache';
export function register_typography_endpoint(http_server: FastifyInstance, conf: HttpConfig, { }: HttpWebDependencies) {
const opts: RouteShorthandOptions = {
schema: { },
};
http_server.get('/typography.css', opts, async (req: Req, res) => {
res.status(200);
res.header('content-type', 'text/css; charset=utf-8');
res.header('cache-control', 'no-store');
// res.header('cache-control', 'public, max-age=3600');
csp_headers(res, conf.exposed_url);
return load_template('typography.css');
});
}

View File

@@ -5,6 +5,7 @@ import { ErrorCode, ErrorInfo } from '../../http/send-error';
import { redirect_303_see_other } from '../../http/redirects';
import { FastifyInstance, FastifyReply, RouteShorthandOptions } from 'fastify';
import { csp_headers } from '../../http/content-security-policy';
import { render_template } from '../../utilities/mustache';
export function register_login_page_endpoint(http_server: FastifyInstance, conf: HttpConfig, { pkce_cookie, session }: HttpWebDependencies) {
const opts: RouteShorthandOptions = {
@@ -32,22 +33,11 @@ export function register_login_page_endpoint(http_server: FastifyInstance, conf:
}
}
export const render_login_page = (error_code?: ErrorCode, error?: ErrorInfo) => `<!doctype html>
<html>
<head>
<title>Login</title>
</head>
<body>
<form action="/login" method="POST">
<button type="submit">Login with OpenID Connect</button>
</form>
${error_code ? `
<div>
<h4>Login failed</h4>
<b>Error Code:</b> ${error_code}<br />
<b>Error Message:</b> ${error.message}
</div>
` : ''}
</body>
</html>
`;
export function render_login_page(error_code?: ErrorCode, error?: ErrorInfo) {
const view = {
error_code,
error,
};
return render_template('login.html.mustache', view);
}

View File

@@ -3,9 +3,9 @@ import { Req } from '../http/request';
import { UserData } from '../storage';
import { HttpConfig } from '../http/server';
import { HttpWebDependencies } from './server';
import { ErrorCode, ErrorInfo } from '../http/send-error';
import { csp_headers } from '../http/content-security-policy';
import { FastifyInstance, RouteShorthandOptions } from 'fastify';
import { render_template } from '../utilities/mustache';
export function register_root_page_endpoint(http_server: FastifyInstance, conf: HttpConfig, { session, logger }: HttpWebDependencies) {
const opts: RouteShorthandOptions = {
@@ -28,19 +28,10 @@ export function register_root_page_endpoint(http_server: FastifyInstance, conf:
});
}
export const render_root_page = (user?: UserData, error_code?: ErrorCode, error?: ErrorInfo) => `<!doctype html>
<html>
<head>
<title>Node.js + TypeScript Service</title>
</head>
<body>
<h1>Node.js + TypeScript Service</h1>
${user
? `<p>Logged in as ${user.name} (${user.username})</p>
<form action="/logout" method="POST">
<button type="submit">Logout</button>
</form>`
: '<a href="/login">Login Page</a>'}
</body>
</html>
`;
function render_root_page(user: UserData) {
const view = {
user,
};
return render_template('root.html.mustache', view);
}

View File

@@ -14,6 +14,9 @@ import { register_login_page_endpoint } from './authentication/login-page';
import { register_submit_login_endpoint } from './authentication/submit-login';
import { register_login_callback_endpoint } from './authentication/login-callback';
import { register_logout_endpoint } from './authentication/logout';
import { ColorThemeProvider } from '../utilities/color-themes';
import { register_color_themes_endpoint } from './assets/color-themes';
import { register_typography_endpoint } from './assets/typography';
export interface HttpWebDependencies extends BaseHttpDependencies {
oidc: OIDCProvider;
@@ -22,6 +25,7 @@ export interface HttpWebDependencies extends BaseHttpDependencies {
argon2: Argon2HashProvider;
session: SessionProvider;
snowflake: SnowflakeProvider;
color_themes: ColorThemeProvider;
}
export function create_http_web_server(conf: HttpConfig, deps: HttpWebDependencies) {
@@ -29,6 +33,10 @@ export function create_http_web_server(conf: HttpConfig, deps: HttpWebDependenci
endpoints: [
register_csp_report_endpoint,
// Shared Assets
register_color_themes_endpoint,
register_typography_endpoint,
// Root page
register_root_page_endpoint,

View File

@@ -30,7 +30,7 @@ export function send_json_error(res: FastifyReply, error_code: ErrorCode) {
};
}
export function send_html_error(res: FastifyReply, error_code: ErrorCode, render_html: (code: ErrorCode, error: ErrorInfo) => string) {
export function send_html_error(res: FastifyReply, error_code: ErrorCode, render_html: (code: ErrorCode, error: ErrorInfo) => string | Promise<string>) {
const error = errors[error_code];
res.status(error.status);
res.header('content-type', 'text/html; charset=utf-8');

View File

@@ -11,6 +11,7 @@ import { create_session_provider } from './security/session';
import { create_http_metadata_server } from './http-metadata/server';
import { HttpWebDependencies, create_http_web_server } from './http-web/server';
import { create_color_theme_provider } from './utilities/color-themes';
main();
@@ -37,10 +38,12 @@ async function main() {
const snowflake = create_snowflake_provider(conf.snowflake_uid);
const argon2 = create_argon_hash_provider(conf.argon2);
const session = create_session_provider(conf.session_cookie, logger, argon2, storage);
const color_themes = create_color_theme_provider(conf.color_themes);
// Wait for any async init steps
await oidc.ready;
await storage.ready;
await color_themes.ready;
// Perform any cleanup steps before starting up
await storage.cleanup_old_sessions();
@@ -53,6 +56,7 @@ async function main() {
storage,
argon2,
session,
color_themes,
};
// Create the main web server

View File

@@ -0,0 +1,65 @@
import { ColorTheme, load_theme } from '@doc-utils/color-themes';
import { render_template } from './mustache';
export interface ColorThemeConfig {
default: LightAndDarkTheme;
less_contrast?: LightAndDarkTheme;
more_contrast?: LightAndDarkTheme;
}
export interface LightAndDarkTheme {
light: string;
dark: string;
}
export type ColorThemeProvider = ReturnType<typeof create_color_theme_provider>;
export function create_color_theme_provider(conf: ColorThemeConfig) {
let css: string;
const ready = setup();
return {
get ready() {
return ready;
},
get css() {
return css;
},
};
async function setup() {
const view = {
more_contrast: conf.more_contrast != null,
less_contrast: conf.less_contrast != null,
};
const partials = {
default_light: render_theme_css_properties(await load(conf.default.light)),
default_dark: render_theme_css_properties(await load(conf.default.dark)),
more_contrast_light: render_theme_css_properties(await load(conf.more_contrast?.light)),
more_contrast_dark: render_theme_css_properties(await load(conf.more_contrast?.dark)),
less_contrast_light: render_theme_css_properties(await load(conf.less_contrast?.light)),
less_contrast_dark: render_theme_css_properties(await load(conf.less_contrast?.dark)),
};
css = await render_template('themes.css.mustache', view, partials);
}
}
function load(theme: string) {
if (theme) {
return load_theme(theme);
}
return null;
}
function render_theme_css_properties(theme: ColorTheme) {
return Object
.entries(theme.colors)
.map(([ name, color ]) => {
return `--theme-${name.replace(/_/g, '-')}: ${color};`;
})
.join('\n') + '\n';
}

32
src/utilities/mustache.ts Normal file
View File

@@ -0,0 +1,32 @@
import { render } from 'mustache';
import { promises as fs } from 'fs';
import { resolve as path_resolve } from 'path';
const templates: Record<string, Promise<string>> = Object.create(null);
const template_path = path_resolve(__dirname, '../../templates');
export async function render_template(file: string, view: any, partials: Record<string, string> = Object.create(null), partial_files: Record<string, string> = Object.create(null)) {
const template = await load_template(file);
const partial_promises: Promise<void>[] = [ ];
for (const [key, file] of Object.entries(partial_files)) {
const promise = load_template(file).then((template) => {
partials[key] = template;
});
partial_promises.push(promise);
}
await Promise.all(partial_promises);
return render(template, view, partials);
}
export function load_template(file: string) {
if (! templates[file]) {
const path = path_resolve(template_path, file);
templates[file] = fs.readFile(path, 'utf8');
}
return templates[file];
}