Made infrastructure to ensure private data is not leaked to client

This commit is contained in:
2023-08-11 13:52:10 +02:00
parent 8660345bf6
commit 22545e46bb
7 changed files with 232 additions and 29 deletions

View File

@@ -1 +0,0 @@
../static/config.yml

14
src/config.yml Normal file
View File

@@ -0,0 +1,14 @@
title: 'Hello World !!'
subtitle: 'actually, I am a new pilot.'
services:
- title: '/Cloud'
subtitle: 'Private Cloud Utilities'
icon: 'fas fa-cloud'
items:
- title: 'NAS'
subtitle: 'Network Attached Storage'
icon: 'fas fa-hard-drive'
target: '_blank'
url: '/NAS'
keywords: 'cloud storage files'

24
src/hooks.server.ts Normal file
View File

@@ -0,0 +1,24 @@
import { dev } from "$app/environment";
import { clientConfig, stripPrivateFields, type Config } from "$lib/config";
import type { Handle } from "@sveltejs/kit";
import { readFile } from 'fs';
import * as yml from 'js-yaml';
async function reloadConfig(): Promise<Config> {
try {
const dynamic = yml.load(await readFile('/dynamic/config.yml', 'utf8'));
return stripPrivateFields({ ...clientConfig, ...dynamic });
} catch (err) {
return clientConfig;
}
}
export const handle: Handle = async ({ event, resolve }) => {
if ( dev == false ) {
Object.assign(event.locals,{config: await reloadConfig()});
}
const response = await resolve(event);
return response;
}

58
src/lib/config.test.ts Normal file
View File

@@ -0,0 +1,58 @@
import { describe, expect, it } from 'vitest';
import { config, defaultConfig, mergeConfig, type Config, stripPrivateFields } from './config';
describe('Config', () => {
it('should be export a build time config', () => {
expect(config).toBeTruthy();
});
it('should be able to merge with POJO', () => {
const merged = mergeConfig(defaultConfig, {
subtitle: "Homer's favorite neighboor",
services: [
{
title: 'favorite occupations',
items: [
{
title: 'anoy homer',
customKey: 'all the time'
}
]
}
]
});
expect(merged.title).toEqual('Flanders');
expect(merged.services).toHaveLength(1);
expect(merged.services[0].title).toEqual('favorite occupations');
expect(merged.services[0].items).toHaveLength(1);
expect(merged.services[0].items[0].title).toEqual('anoy homer');
expect(merged.services[0].items[0].customKey).toEqual('all the time');
});
it('should be able to strip custom keys', () => {
const custom: Config = {
title: 'custom',
secret: {secret: 'some secret'},
services: [
{
title: 'top services',
secret: 'secret',
items: [
{
title: 'top service',
url: 'somewhere',
secret: 'secret'
}
]
}
]
};
const stripped: Config = stripPrivateFields(custom);
expect(stripped.secret).toBeUndefined();
expect(stripped.services[0].secret).toBeUndefined();
expect(stripped.services[0].items[0].secret).toBeUndefined();
});
});

View File

@@ -1,36 +1,109 @@
import configData from '../config.yml';
export type Brand = {
export interface Brand {
logo?: string;
icon?: string;
usemask?: boolean;
};
}
export type Section = {
export interface Section extends Brand {
title: string;
subtitle?: string;
} & Brand;
}
export type Service = {
export interface Service extends Section {
url: string;
target?: string;
type?: string;
[x: string]: unknown;
} & Section;
}
export type ServiceGroup = {
export interface ServiceGroup extends Section {
items: Service[];
} & Section;
export type Config = {
[x: string]: unknown;
}
export interface Config extends Section {
services: ServiceGroup[];
} & Section;
export const config: Config = {
...{
title: 'Flanders',
services: []
},
...configData
[x: string]: unknown;
}
type DeepRequired<T> = T extends object
? {
[P in keyof T]: DeepRequired<T[P]>;
}
: T;
const requiredConfig: DeepRequired<Config> = {
title: '',
subtitle: '',
services: [
{
title: '',
items: [
{
title: '',
url: '',
}
],
}
]
};
export function mergeConfig(a: Config, b: any): Config {
return { ...a, ...b };
}
export const defaultConfig: Config = {
title: 'Flanders',
services: []
};
type SPOJO = Record<string,unknown>
function strip<Type extends SPOJO> (toStrip: Type, reference: Type): Type {
const res: Type = {...toStrip}
const referenceNames = Object.entries(reference).map(([key,value]) => key)
for ( const [key,value] of Object.entries(res) ) {
if ( referenceNames.includes(key) == false ) {
// remove the object
delete res[key];
continue
}
// strips further arrays
if ( value instanceof Array ) {
const stripped : SPOJO = {};
const childRef = (reference[key] as Array<SPOJO>)[0];
stripped[key] = value.map((v: SPOJO) => strip(v,childRef));
Object.assign(res,stripped);
continue;
}
if ( typeof value != "object") {
continue
}
// it is a child object, we strip it further
const stripped : SPOJO = {};
stripped[key] = strip(value as SPOJO,reference[key] as SPOJO);
Object.assign(res,stripped);
}
return res;
}
export function stripPrivateFields(config: Config): Config {
return strip(config,requiredConfig);
}
export const config: Config = mergeConfig(defaultConfig, configData);
export const clientConfig :Config = stripPrivateFields(config)

View File

@@ -1,20 +1,12 @@
import { dev } from '$app/environment';
import { config, type Config } from '$lib/config';
import { clientConfig, type Config } from '$lib/config';
import type { PageServerLoad } from './$types';
import { readFileSync } from 'fs';
import * as yml from 'js-yaml';
export const load: PageServerLoad = () => {
export const load: PageServerLoad = ({locals}) => {
if (dev) {
return { config: config };
return { config: clientConfig };
}
try {
const dynamic = yml.load(readFileSync('/dynamic/config.yml', 'utf8'));
return { config: { ...config, ...dynamic } as Config };
} catch (err) {
console.debug("could not read '/dynamic/config.yml': " + err);
return { config: config };
}
return {config: (locals as Record<string,Config>).config || clientConfig};
};