feat: allow apps to generate their own vapid key pair
This commit is contained in:
parent
3e9e7ce808
commit
4dd01eb31b
6 changed files with 112 additions and 9 deletions
|
@ -37,6 +37,7 @@ module.exports = {
|
|||
'no-underscore-dangle': 0,
|
||||
'arrow-body-style': 0,
|
||||
'class-methods-use-this': 0,
|
||||
'jsdoc/require-returns': 0,
|
||||
},
|
||||
globals: {
|
||||
JSX: true,
|
||||
|
|
|
@ -73,6 +73,7 @@
|
|||
"tslib": "^2.4.0",
|
||||
"uuid": "^9.0.0",
|
||||
"validator": "^13.7.0",
|
||||
"web-push": "^3.5.0",
|
||||
"winston": "^3.7.2",
|
||||
"zod": "^3.21.4",
|
||||
"zustand": "^4.3.6"
|
||||
|
@ -100,6 +101,7 @@
|
|||
"@types/testing-library__jest-dom": "^5.14.5",
|
||||
"@types/uuid": "^9.0.1",
|
||||
"@types/validator": "^13.7.14",
|
||||
"@types/web-push": "^3.3.2",
|
||||
"@typescript-eslint/eslint-plugin": "^5.55.0",
|
||||
"@typescript-eslint/parser": "^5.55.0",
|
||||
"dotenv-cli": "^7.1.0",
|
||||
|
|
|
@ -139,6 +139,9 @@ dependencies:
|
|||
validator:
|
||||
specifier: ^13.7.0
|
||||
version: 13.9.0
|
||||
web-push:
|
||||
specifier: ^3.5.0
|
||||
version: 3.5.0
|
||||
winston:
|
||||
specifier: ^3.7.2
|
||||
version: 3.8.2
|
||||
|
@ -216,6 +219,9 @@ devDependencies:
|
|||
'@types/validator':
|
||||
specifier: ^13.7.14
|
||||
version: 13.7.14
|
||||
'@types/web-push':
|
||||
specifier: ^3.3.2
|
||||
version: 3.3.2
|
||||
'@typescript-eslint/eslint-plugin':
|
||||
specifier: ^5.55.0
|
||||
version: 5.55.0(@typescript-eslint/parser@5.55.0)(eslint@8.36.0)(typescript@5.0.2)
|
||||
|
@ -2584,6 +2590,12 @@ packages:
|
|||
resolution: {integrity: sha512-J6OAed6rhN6zyqL9Of6ZMamhlsOEU/poBVvbHr/dKOYKTeuYYMlDkMv+b6UUV0o2i0tw73cgyv/97WTWaUl0/g==}
|
||||
dev: true
|
||||
|
||||
/@types/web-push@3.3.2:
|
||||
resolution: {integrity: sha512-JxWGVL/m7mWTIg4mRYO+A6s0jPmBkr4iJr39DqJpRJAc+jrPiEe1/asmkwerzRon8ZZDxaZJpsxpv0Z18Wo9gw==}
|
||||
dependencies:
|
||||
'@types/node': 18.15.3
|
||||
dev: true
|
||||
|
||||
/@types/yargs-parser@21.0.0:
|
||||
resolution: {integrity: sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==}
|
||||
dev: true
|
||||
|
@ -3055,6 +3067,15 @@ packages:
|
|||
get-intrinsic: 1.2.0
|
||||
dev: true
|
||||
|
||||
/asn1.js@5.4.1:
|
||||
resolution: {integrity: sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==}
|
||||
dependencies:
|
||||
bn.js: 4.12.0
|
||||
inherits: 2.0.4
|
||||
minimalistic-assert: 1.0.1
|
||||
safer-buffer: 2.1.2
|
||||
dev: false
|
||||
|
||||
/ast-types-flow@0.0.7:
|
||||
resolution: {integrity: sha512-eBvWn1lvIApYMhzQMsu9ciLfkBY499mFZlNqG+/9WR7PVlroQw0vG30cOQQbaKz3sCEc44TAOu2ykzqXSNnwag==}
|
||||
dev: true
|
||||
|
@ -3185,6 +3206,10 @@ packages:
|
|||
inherits: 2.0.4
|
||||
readable-stream: 3.6.0
|
||||
|
||||
/bn.js@4.12.0:
|
||||
resolution: {integrity: sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==}
|
||||
dev: false
|
||||
|
||||
/body-parser@1.20.1:
|
||||
resolution: {integrity: sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==}
|
||||
engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
|
||||
|
@ -5468,6 +5493,13 @@ packages:
|
|||
- supports-color
|
||||
dev: true
|
||||
|
||||
/http_ece@1.1.0:
|
||||
resolution: {integrity: sha512-bptAfCDdPJxOs5zYSe7Y3lpr772s1G346R4Td5LgRUeCwIGpCGDUTJxRrhTNcAXbx37spge0kWEIH7QAYWNTlA==}
|
||||
engines: {node: '>=4'}
|
||||
dependencies:
|
||||
urlsafe-base64: 1.0.0
|
||||
dev: false
|
||||
|
||||
/https-proxy-agent@5.0.1:
|
||||
resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==}
|
||||
engines: {node: '>= 6'}
|
||||
|
@ -6506,6 +6538,14 @@ packages:
|
|||
safe-buffer: 5.2.1
|
||||
dev: false
|
||||
|
||||
/jwa@2.0.0:
|
||||
resolution: {integrity: sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==}
|
||||
dependencies:
|
||||
buffer-equal-constant-time: 1.0.1
|
||||
ecdsa-sig-formatter: 1.0.11
|
||||
safe-buffer: 5.2.1
|
||||
dev: false
|
||||
|
||||
/jws@3.2.2:
|
||||
resolution: {integrity: sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==}
|
||||
dependencies:
|
||||
|
@ -6513,6 +6553,13 @@ packages:
|
|||
safe-buffer: 5.2.1
|
||||
dev: false
|
||||
|
||||
/jws@4.0.0:
|
||||
resolution: {integrity: sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==}
|
||||
dependencies:
|
||||
jwa: 2.0.0
|
||||
safe-buffer: 5.2.1
|
||||
dev: false
|
||||
|
||||
/kleur@3.0.3:
|
||||
resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==}
|
||||
engines: {node: '>=6'}
|
||||
|
@ -7120,6 +7167,10 @@ packages:
|
|||
engines: {node: '>=4'}
|
||||
dev: true
|
||||
|
||||
/minimalistic-assert@1.0.1:
|
||||
resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==}
|
||||
dev: false
|
||||
|
||||
/minimatch@3.1.2:
|
||||
resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
|
||||
dependencies:
|
||||
|
@ -9098,6 +9149,10 @@ packages:
|
|||
requires-port: 1.0.0
|
||||
dev: true
|
||||
|
||||
/urlsafe-base64@1.0.0:
|
||||
resolution: {integrity: sha512-RtuPeMy7c1UrHwproMZN9gN6kiZ0SvJwRaEzwZY0j9MypEkFqyBaKv176jvlPtg58Zh36bOkS0NFABXMHvvGCA==}
|
||||
dev: false
|
||||
|
||||
/use-callback-ref@1.3.0(@types/react@18.0.28)(react@18.2.0):
|
||||
resolution: {integrity: sha512-3FT9PRuRdbB9HfXhEq35u4oZkvpJ5kuYbpqhCfmiZyReuRgpnhDlbr2ZEnnuS0RrJAPn6l23xjFg9kpDM+Ms7w==}
|
||||
engines: {node: '>=10'}
|
||||
|
@ -9259,6 +9314,21 @@ packages:
|
|||
'@zxing/text-encoding': 0.9.0
|
||||
dev: true
|
||||
|
||||
/web-push@3.5.0:
|
||||
resolution: {integrity: sha512-JC0V9hzKTqlDYJ+LTZUXtW7B175qwwaqzbbMSWDxHWxZvd3xY0C2rcotMGDavub2nAAFw+sXTsqR65/KY2A5AQ==}
|
||||
engines: {node: '>= 6'}
|
||||
hasBin: true
|
||||
dependencies:
|
||||
asn1.js: 5.4.1
|
||||
http_ece: 1.1.0
|
||||
https-proxy-agent: 5.0.1
|
||||
jws: 4.0.0
|
||||
minimist: 1.2.8
|
||||
urlsafe-base64: 1.0.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: false
|
||||
|
||||
/web-streams-polyfill@3.2.1:
|
||||
resolution: {integrity: sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==}
|
||||
engines: {node: '>= 8'}
|
||||
|
|
|
@ -2,6 +2,7 @@ import crypto from 'crypto';
|
|||
import fs from 'fs-extra';
|
||||
import { z } from 'zod';
|
||||
import { App } from '@/server/db/schema';
|
||||
import { generateVapidKeys } from '@/server/utils/env-generation';
|
||||
import { deleteFolder, fileExists, getSeed, readdirSync, readFile, readJsonFile, writeFile } from '../../common/fs.helpers';
|
||||
import { APP_CATEGORIES, FIELD_TYPES } from './apps.types';
|
||||
import { getConfig } from '../../core/TipiConfig';
|
||||
|
@ -37,6 +38,7 @@ export const appInfoSchema = z.object({
|
|||
source: z.string(),
|
||||
website: z.string().optional(),
|
||||
force_expose: z.boolean().optional().default(false),
|
||||
generate_vapid_keys: z.boolean().optional().default(false),
|
||||
categories: z
|
||||
.nativeEnum(APP_CATEGORIES)
|
||||
.array()
|
||||
|
@ -64,7 +66,6 @@ export type FormField = z.infer<typeof formFieldSchema>;
|
|||
*
|
||||
* @param {string} appName - The name of the app.
|
||||
* @throws Will throw an error if the app has an invalid config.json file or if the current system architecture is not supported by the app.
|
||||
* @returns {AppInfo} - parsed app config data
|
||||
*/
|
||||
export const checkAppRequirements = (appName: string) => {
|
||||
const configFile = readJsonFile(`/runtipi/repos/${getConfig().appsRepoId}/apps/${appName}/config.json`);
|
||||
|
@ -86,7 +87,6 @@ export const checkAppRequirements = (appName: string) => {
|
|||
* It reads the file, splits it into individual environment variables, and stores them in a Map, with the environment variable name as the key and its value as the value.
|
||||
*
|
||||
* @param {string} appName - The name of the app.
|
||||
* @returns {Map<string, string>} - A Map containing the key-value pairs of the environment variables.
|
||||
*/
|
||||
export const getEnvMap = (appName: string) => {
|
||||
const envFile = readFile(`/app/storage/app-data/${appName}/app.env`).toString();
|
||||
|
@ -138,7 +138,6 @@ export const checkEnvFile = (appName: string) => {
|
|||
*
|
||||
* @param {string} name - A name used as input for the hash algorithm.
|
||||
* @param {number} length - The desired length of the random string.
|
||||
* @returns {string} - A random string of the provided length.
|
||||
*/
|
||||
const getEntropy = (name: string, length: number) => {
|
||||
const hash = crypto.createHash('sha256');
|
||||
|
@ -183,6 +182,17 @@ export const generateEnvFile = (app: App) => {
|
|||
let envFile = `${baseEnvFile}\nAPP_PORT=${parsedConfig.data.port}\n`;
|
||||
const envMap = getEnvMap(app.id);
|
||||
|
||||
if (parsedConfig.data.generate_vapid_keys) {
|
||||
if (envMap.has('VAPID_PUBLIC_KEY') && envMap.has('VAPID_PRIVATE_KEY')) {
|
||||
envFile += `VAPID_PUBLIC_KEY=${envMap.get('VAPID_PUBLIC_KEY')}\n`;
|
||||
envFile += `VAPID_PRIVATE_KEY=${envMap.get('VAPID_PRIVATE_KEY')}\n`;
|
||||
} else {
|
||||
const vapidKeys = generateVapidKeys();
|
||||
envFile += `VAPID_PUBLIC_KEY=${vapidKeys.publicKey}\n`;
|
||||
envFile += `VAPID_PRIVATE_KEY=${vapidKeys.privateKey}\n`;
|
||||
}
|
||||
}
|
||||
|
||||
parsedConfig.data.form_fields.forEach((field) => {
|
||||
const formValue = castAppConfig(app.config)[field.env_variable];
|
||||
const envVar = field.env_variable;
|
||||
|
@ -223,8 +233,6 @@ export const generateEnvFile = (app: App) => {
|
|||
This function reads the apps directory and skips certain system files, then reads the config.json and metadata/description.md files for each app,
|
||||
parses the config file, filters out any apps that are not available and returns an array of app information.
|
||||
If the config.json file is invalid, it logs an error message.
|
||||
|
||||
@returns {Promise<AppInfo[]>} - Returns a promise that resolves with an array of available apps' information.
|
||||
*/
|
||||
export const getAvailableApps = async () => {
|
||||
const appsDir = readdirSync(`/runtipi/repos/${getConfig().appsRepoId}/apps`);
|
||||
|
@ -259,7 +267,6 @@ export const getAvailableApps = async () => {
|
|||
* If the app is not found, it returns null.
|
||||
*
|
||||
* @param {string} id - The app id.
|
||||
* @returns {Promise<{current: number, latest: number, dockerVersion: string} | null>} - Returns an object containing information about the updates available for the app or null if the app is not found or has an invalid config.json file.
|
||||
*/
|
||||
export const getUpdateInfo = (id: string) => {
|
||||
const repoConfig = readJsonFile(`/runtipi/repos/${getConfig().appsRepoId}/apps/${id}/config.json`);
|
||||
|
@ -284,7 +291,6 @@ export const getUpdateInfo = (id: string) => {
|
|||
*
|
||||
* @param {string} id - The app id.
|
||||
* @param {App['status']} [status] - The app status.
|
||||
* @returns {AppInfo | null} - Returns an object with app information or null if the app is not found.
|
||||
*/
|
||||
export const getAppInfo = (id: string, status?: App['status']) => {
|
||||
try {
|
||||
|
@ -327,7 +333,7 @@ export const getAppInfo = (id: string, status?: App['status']) => {
|
|||
* @param {boolean} [cleanup=false] - A flag indicating whether to cleanup the app folder before ensuring its existence.
|
||||
* @throws Will throw an error if the app folder cannot be copied from the repository
|
||||
*/
|
||||
export const ensureAppFolder = (appName: string, cleanup = false) => {
|
||||
export const ensureAppFolder = (appName: string, cleanup = false): void => {
|
||||
if (cleanup && fileExists(`/runtipi/apps/${appName}`)) {
|
||||
deleteFolder(`/runtipi/apps/${appName}`);
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@ interface IProps {
|
|||
domain?: string;
|
||||
exposable?: boolean;
|
||||
forceExpose?: boolean;
|
||||
generateVapidKeys?: boolean;
|
||||
supportedArchitectures?: Architecture[];
|
||||
}
|
||||
|
||||
|
@ -34,7 +35,17 @@ const createAppConfig = (props?: Partial<AppInfo>) =>
|
|||
});
|
||||
|
||||
const createApp = async (props: IProps, database: TestDatabase) => {
|
||||
const { installed = false, status = 'running', randomField = false, exposed = false, domain = null, exposable = false, supportedArchitectures, forceExpose = false } = props;
|
||||
const {
|
||||
installed = false,
|
||||
status = 'running',
|
||||
randomField = false,
|
||||
exposed = false,
|
||||
domain = null,
|
||||
exposable = false,
|
||||
supportedArchitectures,
|
||||
forceExpose = false,
|
||||
generateVapidKeys = false,
|
||||
} = props;
|
||||
|
||||
const categories = Object.values(APP_CATEGORIES);
|
||||
|
||||
|
@ -65,6 +76,7 @@ const createApp = async (props: IProps, database: TestDatabase) => {
|
|||
version: String(faker.datatype.number({ min: 1, max: 10 })),
|
||||
https: false,
|
||||
no_gui: false,
|
||||
generate_vapid_keys: generateVapidKeys,
|
||||
};
|
||||
|
||||
if (randomField) {
|
||||
|
|
12
src/server/utils/env-generation.ts
Normal file
12
src/server/utils/env-generation.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
import webpush from 'web-push';
|
||||
|
||||
/**
|
||||
* Generate VAPID keys
|
||||
*/
|
||||
export const generateVapidKeys = () => {
|
||||
const vapidKeys = webpush.generateVAPIDKeys();
|
||||
return {
|
||||
publicKey: vapidKeys.publicKey,
|
||||
privateKey: vapidKeys.privateKey,
|
||||
};
|
||||
};
|
Loading…
Reference in a new issue