start building dashboard
This commit is contained in:
parent
811629eda9
commit
7c205632cb
@ -59,4 +59,13 @@ argon2:
|
||||
logging:
|
||||
level: info
|
||||
pretty: false
|
||||
|
||||
color_themes:
|
||||
default:
|
||||
light: Minimal Light
|
||||
dark: Minimal Dark
|
||||
less_contrast:
|
||||
light: Minimal Light
|
||||
dark: Minimal Dark
|
||||
more_contrast:
|
||||
light: Minimal Light
|
||||
dark: Minimal Dark
|
||||
|
15
conf/01-local-test.yaml
Normal file
15
conf/01-local-test.yaml
Normal file
@ -0,0 +1,15 @@
|
||||
$schema: ../schemas/config.json
|
||||
http_web:
|
||||
exposed_url: http://me.local.jbrumond.me:8080
|
||||
oidc:
|
||||
server_url: https://sso.jbrumond.me/realms/public
|
||||
client_id: local-test-service
|
||||
client_secret: 7NwC0NEIHGuAa30Lp2V90KDOKOI3YlSt
|
||||
pkce_cookie:
|
||||
secure: false
|
||||
session_cookie:
|
||||
secure: false
|
||||
pepper: 4tWRICqVeGtOHaq66RA62aGOhIQ
|
||||
logging:
|
||||
level: debug
|
||||
pretty: true
|
42
package-lock.json
generated
42
package-lock.json
generated
@ -1,14 +1,15 @@
|
||||
{
|
||||
"name": "@templates/nodejs-typescript-service",
|
||||
"name": "@james/dashboard",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@templates/nodejs-typescript-service",
|
||||
"name": "@james/dashboard",
|
||||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@doc-utils/color-themes": "^0.1.15",
|
||||
"@fastify/compress": "^6.4.0",
|
||||
"@fastify/etag": "^4.2.0",
|
||||
"@fastify/formbody": "^7.4.0",
|
||||
@ -17,18 +18,25 @@
|
||||
"fastify": "^4.19.2",
|
||||
"generic-pool": "^3.9.0",
|
||||
"luxon": "^3.3.0",
|
||||
"mustache": "^4.2.0",
|
||||
"openid-client": "^5.4.3",
|
||||
"pino": "^8.14.1",
|
||||
"sqlite3": "^5.1.6",
|
||||
"yaml": "^2.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/mustache": "^4.2.2",
|
||||
"@types/node": "^20.4.2",
|
||||
"json-schema": "^0.4.0",
|
||||
"pino-pretty": "^10.1.0",
|
||||
"typescript": "^5.1.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@doc-utils/color-themes": {
|
||||
"version": "0.1.15",
|
||||
"resolved": "https://gitea.jbrumond.me/api/packages/doc-utils/npm/%40doc-utils%2Fcolor-themes/-/0.1.15/color-themes-0.1.15.tgz",
|
||||
"integrity": "sha512-P0oIlq4Z0cUOf7P4T2OUfhT/Cn/msl3oHTenVWHxKMDPlIYueR6AhFdRCHTI0A+DxyH+0EIi6CnMq3JYnILI0w=="
|
||||
},
|
||||
"node_modules/@fastify/accept-negotiator": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@fastify/accept-negotiator/-/accept-negotiator-1.1.0.tgz",
|
||||
@ -163,6 +171,12 @@
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/mustache": {
|
||||
"version": "4.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/mustache/-/mustache-4.2.2.tgz",
|
||||
"integrity": "sha512-MUSpfpW0yZbTgjekDbH0shMYBUD+X/uJJJMm9LXN1d5yjl5lCY1vN/eWKD6D1tOtjA6206K0zcIPnUaFMurdNA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "20.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.4.2.tgz",
|
||||
@ -1379,6 +1393,14 @@
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||
},
|
||||
"node_modules/mustache": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz",
|
||||
"integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==",
|
||||
"bin": {
|
||||
"mustache": "bin/mustache"
|
||||
}
|
||||
},
|
||||
"node_modules/negotiator": {
|
||||
"version": "0.6.3",
|
||||
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
|
||||
@ -2324,6 +2346,11 @@
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@doc-utils/color-themes": {
|
||||
"version": "0.1.15",
|
||||
"resolved": "https://gitea.jbrumond.me/api/packages/doc-utils/npm/%40doc-utils%2Fcolor-themes/-/0.1.15/color-themes-0.1.15.tgz",
|
||||
"integrity": "sha512-P0oIlq4Z0cUOf7P4T2OUfhT/Cn/msl3oHTenVWHxKMDPlIYueR6AhFdRCHTI0A+DxyH+0EIi6CnMq3JYnILI0w=="
|
||||
},
|
||||
"@fastify/accept-negotiator": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@fastify/accept-negotiator/-/accept-negotiator-1.1.0.tgz",
|
||||
@ -2442,6 +2469,12 @@
|
||||
"integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==",
|
||||
"optional": true
|
||||
},
|
||||
"@types/mustache": {
|
||||
"version": "4.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/mustache/-/mustache-4.2.2.tgz",
|
||||
"integrity": "sha512-MUSpfpW0yZbTgjekDbH0shMYBUD+X/uJJJMm9LXN1d5yjl5lCY1vN/eWKD6D1tOtjA6206K0zcIPnUaFMurdNA==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/node": {
|
||||
"version": "20.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.4.2.tgz",
|
||||
@ -3394,6 +3427,11 @@
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||
},
|
||||
"mustache": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz",
|
||||
"integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ=="
|
||||
},
|
||||
"negotiator": {
|
||||
"version": "0.6.3",
|
||||
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
|
||||
|
@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@templates/nodejs-typescript-service",
|
||||
"name": "@james/dashboard",
|
||||
"version": "1.0.0",
|
||||
"description": "Template project for creating new Node.js / TypeScript services",
|
||||
"description": "Configurable, self-hostable web dashboard",
|
||||
"main": "src/index.ts",
|
||||
"private": true,
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git@git.jbrumond.me:templates/nodejs-typescript-service.git"
|
||||
"url": "git@git.jbrumond.me:james/dashboard.git"
|
||||
},
|
||||
"scripts": {
|
||||
"tsc": "tsc --build",
|
||||
@ -16,12 +16,14 @@
|
||||
"author": "James Brumond <https://jbrumond.me>",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@types/mustache": "^4.2.2",
|
||||
"@types/node": "^20.4.2",
|
||||
"json-schema": "^0.4.0",
|
||||
"pino-pretty": "^10.1.0",
|
||||
"typescript": "^5.1.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@doc-utils/color-themes": "^0.1.15",
|
||||
"@fastify/compress": "^6.4.0",
|
||||
"@fastify/etag": "^4.2.0",
|
||||
"@fastify/formbody": "^7.4.0",
|
||||
@ -30,6 +32,7 @@
|
||||
"fastify": "^4.19.2",
|
||||
"generic-pool": "^3.9.0",
|
||||
"luxon": "^3.3.0",
|
||||
"mustache": "^4.2.0",
|
||||
"openid-client": "^5.4.3",
|
||||
"pino": "^8.14.1",
|
||||
"sqlite3": "^5.1.6",
|
||||
|
@ -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> {
|
||||
|
20
src/http-web/assets/color-themes.ts
Normal file
20
src/http-web/assets/color-themes.ts
Normal 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;
|
||||
});
|
||||
}
|
22
src/http-web/assets/typography.ts
Normal file
22
src/http-web/assets/typography.ts
Normal 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');
|
||||
});
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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,
|
||||
|
||||
|
@ -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');
|
||||
|
@ -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
|
||||
|
65
src/utilities/color-themes.ts
Normal file
65
src/utilities/color-themes.ts
Normal 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
32
src/utilities/mustache.ts
Normal 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];
|
||||
}
|
21
templates/login.html.mustache
Normal file
21
templates/login.html.mustache
Normal file
@ -0,0 +1,21 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Login</title>
|
||||
<link rel="stylesheet" type="text/css" href="/themes.css">
|
||||
<link rel="stylesheet" type="text/css" href="/typography.css">
|
||||
</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>
|
||||
{{/ error_code }}
|
||||
</body>
|
||||
</html>
|
22
templates/root.html.mustache
Normal file
22
templates/root.html.mustache
Normal file
@ -0,0 +1,22 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Dashboard</title>
|
||||
<link rel="stylesheet" type="text/css" href="/themes.css">
|
||||
<link rel="stylesheet" type="text/css" href="/typography.css">
|
||||
</head>
|
||||
<body>
|
||||
<h1>Dashboard</h1>
|
||||
|
||||
{{# user }}
|
||||
<p>Logged in as {{ user.name }} ({{ user.username }})</p>
|
||||
<form action="/logout" method="POST">
|
||||
<button type="submit">Logout</button>
|
||||
</form>
|
||||
{{/ user }}
|
||||
|
||||
{{^ user }}
|
||||
<a href="/login">Login Page</a>
|
||||
{{/ user }}
|
||||
</body>
|
||||
</html>
|
78
templates/themes.css.mustache
Normal file
78
templates/themes.css.mustache
Normal file
@ -0,0 +1,78 @@
|
||||
|
||||
:root {
|
||||
color-scheme: light dark;
|
||||
}
|
||||
|
||||
|
||||
|
||||
{{! ===== Default Themes ===== }}
|
||||
|
||||
body {
|
||||
{{> default_light }}
|
||||
}
|
||||
|
||||
body[data-color-scheme='dark'] {
|
||||
{{> default_dark }}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body {
|
||||
{{> default_dark }}
|
||||
}
|
||||
|
||||
body[data-color-scheme='light'] {
|
||||
{{> default_light }}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
{{! ===== High Contrast Themes ===== }}
|
||||
|
||||
{{# more_contrast }}
|
||||
@media (prefers-contrast: more) {
|
||||
body {
|
||||
{{> more_contrast_light }}
|
||||
}
|
||||
|
||||
body[data-color-scheme='dark'] {
|
||||
{{> more_contrast_dark }}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body {
|
||||
{{> more_contrast_dark }}
|
||||
}
|
||||
|
||||
body[data-color-scheme='light'] {
|
||||
{{> more_contrast_light }}
|
||||
}
|
||||
}
|
||||
}
|
||||
{{/ more_contrast }}
|
||||
|
||||
|
||||
|
||||
{{! ===== Low Contrast Themes ===== }}
|
||||
|
||||
{{# less_contrast }}
|
||||
@media (prefers-contrast: less) {
|
||||
body {
|
||||
{{> less_contrast_light }}
|
||||
}
|
||||
|
||||
body[data-color-scheme='dark'] {
|
||||
{{> less_contrast_dark }}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body {
|
||||
{{> less_contrast_dark }}
|
||||
}
|
||||
|
||||
body[data-color-scheme='light'] {
|
||||
{{> less_contrast_light }}
|
||||
}
|
||||
}
|
||||
}
|
||||
{{/ less_contrast }}
|
26
templates/typography.css
Normal file
26
templates/typography.css
Normal file
@ -0,0 +1,26 @@
|
||||
|
||||
:root {
|
||||
--font-heading: 'Open Sans', sans-serif;
|
||||
--font-body: 'Open Sans', sans-serif;
|
||||
--font-monospace: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
|
||||
font-family: var(--font-body);
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6,
|
||||
th, dt {
|
||||
font-family: var(--font-heading);
|
||||
}
|
||||
|
||||
p, td, dd, figcaption, li, blockquote {
|
||||
font-family: var(--font-body);
|
||||
}
|
||||
|
||||
pre, code, samp {
|
||||
font-family: var(--font-monospace);
|
||||
}
|
||||
|
||||
a, span,
|
||||
b, i, u, q,
|
||||
strong, em, mark, cite {
|
||||
font-family: inherit;
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user