Made infrastructure to ensure private data is not leaked to client
This commit is contained in:
42
package-lock.json
generated
42
package-lock.json
generated
@@ -19,6 +19,7 @@
|
||||
"@sveltejs/kit": "^1.20.4",
|
||||
"@typescript-eslint/eslint-plugin": "^5.45.0",
|
||||
"@typescript-eslint/parser": "^5.45.0",
|
||||
"@vitest/ui": "^0.34.1",
|
||||
"eslint": "^8.28.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-svelte": "^2.30.0",
|
||||
@@ -1223,6 +1224,41 @@
|
||||
"url": "https://opencollective.com/vitest"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/ui": {
|
||||
"version": "0.34.1",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-0.34.1.tgz",
|
||||
"integrity": "sha512-bwmkgMjDcMr3pg0UXLwfwZ/WI1fq2N+5DUisqHkY9bvnNRnpT6QiewtSS/VhmN61ixgNpSKbEGVboml2GLuxfA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@vitest/utils": "0.34.1",
|
||||
"fast-glob": "^3.3.0",
|
||||
"fflate": "^0.8.0",
|
||||
"flatted": "^3.2.7",
|
||||
"pathe": "^1.1.1",
|
||||
"picocolors": "^1.0.0",
|
||||
"sirv": "^2.0.3"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vitest": ">=0.30.1 <1"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/ui/node_modules/@vitest/utils": {
|
||||
"version": "0.34.1",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-0.34.1.tgz",
|
||||
"integrity": "sha512-/ql9dsFi4iuEbiNcjNHQWXBum7aL8pyhxvfnD9gNtbjR9fUKAjxhj4AA3yfLXg6gJpMGGecvtF8Au2G9y3q47Q==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"diff-sequences": "^29.4.3",
|
||||
"loupe": "^2.3.6",
|
||||
"pretty-format": "^29.5.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/utils": {
|
||||
"version": "0.32.4",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-0.32.4.tgz",
|
||||
@@ -2086,6 +2122,12 @@
|
||||
"reusify": "^1.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/fflate": {
|
||||
"version": "0.8.0",
|
||||
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.0.tgz",
|
||||
"integrity": "sha512-FAdS4qMuFjsJj6XHbBaZeXOgaypXp8iw/Tpyuq/w3XA41jjLHT8NPA+n7czH/DDhdncq0nAyDZmPeWXh2qmdIg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/file-entry-cache": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
"@sveltejs/kit": "^1.20.4",
|
||||
"@typescript-eslint/eslint-plugin": "^5.45.0",
|
||||
"@typescript-eslint/parser": "^5.45.0",
|
||||
"@vitest/ui": "^0.34.1",
|
||||
"eslint": "^8.28.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-svelte": "^2.30.0",
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
../static/config.yml
|
||||
14
src/config.yml
Normal file
14
src/config.yml
Normal 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
24
src/hooks.server.ts
Normal 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
58
src/lib/config.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
@@ -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)
|
||||
|
||||
@@ -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};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user