From 833890e0ba50e43f46dfc7c4e16aeda7f02cafdf Mon Sep 17 00:00:00 2001 From: einhornimmond Date: Sat, 18 Jan 2025 10:57:52 +0100 Subject: [PATCH] more rules for frontend, all rules --- config/common.schema.js | 135 ++++++++++++++++++++++---------- deployment/bare_metal/.env.dist | 1 - frontend/.env.dist | 3 - frontend/.env.template | 3 - frontend/src/config/index.js | 29 +++++-- frontend/src/config/schema.js | 96 ++++++++++++++++++----- frontend/vite.config.js | 4 +- 7 files changed, 195 insertions(+), 76 deletions(-) diff --git a/config/common.schema.js b/config/common.schema.js index e39c62f65..601748849 100644 --- a/config/common.schema.js +++ b/config/common.schema.js @@ -1,13 +1,13 @@ const Joi = require('joi') -module.exports = Joi.object({ +module.exports = { BROWSER_PROTOCOL: Joi.string() .valid('http', 'https') .description( 'Protocol for all URLs in the browser, must be either http or https to prevent mixed content issues.', ) .default('http') - .require(), + .required(), COMMUNITY_URL: Joi.string() .uri({ scheme: ['http', 'https'] }) @@ -18,7 +18,7 @@ module.exports = Joi.object({ }) .description('The base URL of the community, should have the same scheme like frontend, admin and backend api to prevent mixed contend issues.') .default('http://0.0.0.0') - .require(), + .required(), GRAPHQL_URI: Joi.string() .uri({ scheme: ['http', 'https'] }) @@ -35,59 +35,61 @@ module.exports = Joi.object({ `, ) .default('http://0.0.0.0/graphql') - .require(), + .required(), COMMUNITY_NAME: Joi.string() .min(3) .max(100) .description('The name of the community') .default('Gradido Entwicklung') - .require(), + .required(), COMMUNITY_DESCRIPTION: Joi.string() .min(10) .max(300) .description('A short description of the community') .default('Die lokale Entwicklungsumgebung von Gradido.') - .require(), + .required(), COMMUNITY_SUPPORT_MAIL: Joi.string() .email() .description('The support email address for the community will be used in frontend and E-Mails') .default('support@supportmail.com') - .require(), + .required(), COMMUNITY_LOCATION: Joi.string() .pattern(/^[-+]?[0-9]{1,2}(\.[0-9]+)?,\s?[-+]?[0-9]{1,3}(\.[0-9]+)?$/) - // TODO: ask chatgpt for the correct way .when('GMS_ACTIVE', { is: true, - then: Joi.string().require(), + then: Joi.string().required(), otherwise: Joi.string().optional() }) .description('Geographical location of the community in "latitude, longitude" format') - .default('49.280377, 9.690151') - .require(), + .default('49.280377, 9.690151'), - GRAPHIQL: Joi.boolean() - .description('Flag for enabling graphql playground for debugging.') - .default(false) - .require(), // todo: only allow true in development mode + GRAPHIQL: Joi.boolean() + .description('Flag for enabling GraphQL playground for debugging.') + .default(false) + .when('NODE_ENV', { + is: 'development', + then: Joi.boolean().valid(true).required(), // only allow true in development mode + otherwise: Joi.boolean().valid(false).required() // false in any other mode + }), GMS_ACTIVE: Joi.boolean() .description('Flag to indicate if the GMS (Geographic Member Search) service is used.') .default(false) - .require(), + .required(), HUMHUB_ACTIVE: Joi.boolean() .description('Flag to indicate if the HumHub based Community Server is used.') .default(false) - .require(), + .required(), LOG_LEVEL: Joi.string() - .valid(['INFO', 'ERROR']) // TODO: lookup values + .valid('all', 'mark', 'trace', 'debug', 'info', 'warn', 'error', 'fatal', 'off') .description('set log level') - .default('INFO') + .default('info') .required(), DB_HOST: Joi.string() @@ -96,27 +98,80 @@ module.exports = Joi.object({ .default('localhost') .required(), -DB_PORT: Joi.number() - .integer() - .min(1024) - .max(49151) - .description('database port, default: 3306') - .default(3306) - .required(), + DB_PORT: Joi.number() + .integer() + .min(1024) + .max(49151) + .description('database port, default: 3306') + .default(3306) + .required(), -// TODO: check allowed users for mariadb -DB_USER: Joi.string() - .description('database user name like root (default) or gradido') - .default('root') - .required(), + + DB_USER: Joi.string() + .pattern(/^[A-Za-z0-9]([A-Za-z0-9-_\.]*[A-Za-z0-9])?$/) // Validates MariaDB username rules + .min(1) // Minimum length 1 + .max(16) // Maximum length 16 + .message( + 'Valid database username (letters, numbers, hyphens, underscores, dots allowed; no spaces, must not start or end with hyphen, dot, or underscore)' + ) + .description('database username for mariadb') + .default('root') + .required(), -DB_PASSWORD: Joi.string() - .description('database password') - .default('') - .required(), + DB_PASSWORD: Joi.string() + .when(Joi.ref('NODE_ENV'), { + is: 'development', + then: Joi.string().allow(''), + otherwise: Joi.string() + .min(8) + .max(32) + .pattern(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*(),.?":{}|<>]).+$/) + .message( + 'Password must be between 8 and 32 characters long, and contain at least one uppercase letter, one lowercase letter, one number, and one special character (e.g., !@#$%^&*).' + ) + }) + .description( + 'Password for the database user. In development mode, an empty password is allowed. In other environments, a complex password is required.' + ) + .default('') + .required(), -DB_DATABASE: Joi.string() - .description('database name like gradido_community') - .default('gradido_community') - .required(), -}) \ No newline at end of file + DB_DATABASE: Joi.string() + .pattern(/^[a-zA-Z][a-zA-Z0-9_-]{1,63}$/) + .description('Database name like gradido_community (must start with a letter, and can only contain letters, numbers, underscores, or dashes)') + .default('gradido_community') + .required(), + + APP_VERSION: Joi.string() + .pattern(/^\d+\.\d+\.\d+$/) + .message('Version must be in the format "major.minor.patch" (e.g., "2.4.1")') + .description('App Version from package.json, alle modules share one version') + .required(), + + BUILD_COMMIT: Joi.string() + .pattern(/^[0-9a-f]{40}$/) + .message('The commit hash must be a 40-character hexadecimal string.') + .description('The full git commit hash.') + .optional(), + + BUILD_COMMIT_SHORT: Joi.string() + .pattern(/^[0-9a-f]{7}$/) + .message('The first 7 hexadecimal character from git commit hash.') + .description('A short version from the git commit hash.') + .required(), + + NODE_ENV: Joi.string() + .valid('production', 'development') + .default('development') + .description('Specifies the environment in which the application is running, "production" or "development".'), + + DEBUG: Joi.boolean() + .description('Indicates whether the application is in debugging mode. Set to true when NODE_ENV is not "production".') + .default(false) + .required(), + + PRODUCTION: Joi.boolean() + .default(false) + .description('Indicates whether the application is running in production mode. Set to true when NODE_ENV is "production".') + .required(), +} \ No newline at end of file diff --git a/deployment/bare_metal/.env.dist b/deployment/bare_metal/.env.dist index a07d92dde..e88d0f1ff 100644 --- a/deployment/bare_metal/.env.dist +++ b/deployment/bare_metal/.env.dist @@ -117,7 +117,6 @@ NGINX_SSL_INCLUDE=/etc/letsencrypt/options-ssl-nginx.conf NGINX_REWRITE_LEGACY_URLS=false # LEGACY -DEFAULT_PUBLISHER_ID=2896 WEBHOOK_ELOPAGE_SECRET=secret # GMS diff --git a/frontend/.env.dist b/frontend/.env.dist index f11a70873..51a52b3a4 100644 --- a/frontend/.env.dist +++ b/frontend/.env.dist @@ -1,6 +1,3 @@ -# Environment -DEFAULT_PUBLISHER_ID=2896 - # Endpoints GRAPHQL_PATH=/graphql ADMIN_AUTH_PATH=/admin/authenticate?token={token} diff --git a/frontend/.env.template b/frontend/.env.template index 40d0e7ee1..76b53c784 100644 --- a/frontend/.env.template +++ b/frontend/.env.template @@ -1,8 +1,5 @@ CONFIG_VERSION=$FRONTEND_CONFIG_VERSION -# Environment -DEFAULT_PUBLISHER_ID=$DEFAULT_PUBLISHER_ID - # Endpoints GRAPHQL_PATH=$GRAPHQL_PATH ADMIN_AUTH_PATH=$ADMIN_AUTH_PATH diff --git a/frontend/src/config/index.js b/frontend/src/config/index.js index ade682bf3..a161f10e5 100644 --- a/frontend/src/config/index.js +++ b/frontend/src/config/index.js @@ -4,18 +4,19 @@ // Load Package Details for some default values const pkg = require('../../package') const schema = require('./schema') -const joi = require('joi') +// const joi = require('joi') const constants = { DECAY_START_TIME: new Date('2021-05-13 17:46:31-0000'), // GMT+0 } const version = { + BROWSER_PROTOCOL: process.env.BROWSER_PROTOCOL ?? 'http', FRONTEND_MODULE_PROTOCOL: process.env.FRONTEND_MODULE_PROTOCOL ?? 'http', FRONTEND_MODULE_HOST: process.env.FRONTEND_MODULE_HOST ?? '0.0.0.0', FRONTEND_MODULE_PORT: process.env.FRONTEND_MODULE_PORT ?? '3000', APP_VERSION: pkg.version, - BUILD_COMMIT: process.env.BUILD_COMMIT ?? null, + BUILD_COMMIT: process.env.BUILD_COMMIT ?? undefined, // self reference of `version.BUILD_COMMIT` is not possible at this point, hence the duplicate code BUILD_COMMIT_SHORT: (process.env.BUILD_COMMIT ?? '0000000').slice(0, 7), } @@ -47,7 +48,6 @@ const environment = { NODE_ENV: process.env.NODE_ENV, DEBUG: process.env.NODE_ENV !== 'production' ?? false, PRODUCTION: process.env.NODE_ENV === 'production' ?? false, - DEFAULT_PUBLISHER_ID: process.env.DEFAULT_PUBLISHER_ID ?? 2896, } // const COMMUNITY_HOST = process.env.COMMUNITY_HOST ?? 'localhost' @@ -91,7 +91,6 @@ const meta = { } const CONFIG = { - ...constants, ...version, ...features, ...environment, @@ -100,8 +99,24 @@ const CONFIG = { ...meta, } -// Check config +// Check config // TODO: use validate and construct error message including description -joi.attempt(CONFIG, schema) +// joi.attempt(CONFIG, schema) -module.exports = CONFIG +const { error } = schema.validate(CONFIG, { stack: true, debug: true }) +const schemaJson = schema.describe() +if (error) { + error.details.forEach((err) => { + const key = err.context.key + const description = schemaJson.keys[key] + ? schema.describe().keys[key].flags.description + : 'No description available' + if (CONFIG[key] === undefined) { + throw new Error(`Environment Variable '${key}' is missing. ${description}`) + } else { + throw new Error(`Error on Environment Variable '${key}': ${err.message}. ${description}`) + } + }) +} + +module.exports = { ...CONFIG, ...constants } diff --git a/frontend/src/config/schema.js b/frontend/src/config/schema.js index b3c20b956..f0326cc34 100644 --- a/frontend/src/config/schema.js +++ b/frontend/src/config/schema.js @@ -1,12 +1,67 @@ -const commonSchema = require('../../../config/common.schema') +import { + APP_VERSION, + BROWSER_PROTOCOL, + BUILD_COMMIT, + BUILD_COMMIT_SHORT, + COMMUNITY_DESCRIPTION, + COMMUNITY_NAME, + COMMUNITY_SUPPORT_MAIL, + COMMUNITY_LOCATION, + COMMUNITY_URL, + DEBUG, + GMS_ACTIVE, + GRAPHQL_URI, + HUMHUB_ACTIVE, + NODE_ENV, + PRODUCTION, +} from '../../../config/common.schema' const Joi = require('joi') -module.exports = commonSchema.keys({ +// console.log(commonSchema) + +module.exports = Joi.object({ + APP_VERSION, + BROWSER_PROTOCOL, + BUILD_COMMIT, + BUILD_COMMIT_SHORT, + COMMUNITY_DESCRIPTION, + COMMUNITY_NAME, + COMMUNITY_SUPPORT_MAIL, + COMMUNITY_LOCATION, + COMMUNITY_URL, + DEBUG, + GMS_ACTIVE, + GRAPHQL_URI, + HUMHUB_ACTIVE, + NODE_ENV, + PRODUCTION, + + ADMIN_AUTH_URL: Joi.string() + .uri({ scheme: ['http', 'https'] }) + .when('browser_protocol', { + is: 'https', + then: Joi.string().uri({ scheme: 'https' }), + otherwise: Joi.string().uri({ scheme: 'http' }), + }) + .description('Extern Url for admin-frontend') + .default('http://0.0.0.0/admin/authenticate?token=') + .required(), + + COMMUNITY_REGISTER_URL: Joi.string() + .uri({ scheme: ['http', 'https'] }) + .when('browser_protocol', { + is: 'https', + then: Joi.string().uri({ scheme: 'https' }), + otherwise: Joi.string().uri({ scheme: 'http' }), + }) + .description('URL for Register a new Account in frontend.') + .required(), + FRONTEND_MODULE_PROTOCOL: Joi.string() .valid('http', 'https') .when('BROWSER_PROTOCOL', { is: 'https', - then: Joi.string().uri({ scheme: 'https' }), + then: Joi.string().uri({ scheme: 'https' }), otherwise: Joi.string().uri({ scheme: 'http' }), }) .description( @@ -14,7 +69,7 @@ module.exports = commonSchema.keys({ ) .default('http') .required(), - + FRONTEND_HOSTING: Joi.string() .valid('nodejs') .description('set to `nodejs` if frontend is hosted by vite with a own nodejs instance') @@ -26,13 +81,22 @@ module.exports = commonSchema.keys({ Joi.string() .ip({ version: ['ipv4'] }) .messages({ 'string.ip': 'Must be a valid IPv4 address' }), - Joi.string().domain().messages({ 'string.domain': 'Must be a valid domain' }), + Joi.string().domain().messages({ 'string.domain': 'Must be a valid domain' }) ) + .when('FRONTEND_HOSTING', { + is: 'nodejs', + then: Joi.required(), + otherwise: Joi.optional(), + }) + .when('COMMUNITY_URL', { + is: null, + then: Joi.required(), + otherwise: Joi.optional(), + }) .description( - 'Host (domain, IPv4, or localhost) for the frontend, default is 0.0.0.0 for local hosting during develop', + 'Host (domain, IPv4, or localhost) for the frontend, default is 0.0.0.0 for local hosting during development.', ) - .default('0.0.0.0') - .required(), // required only if community_url isn't set or FRONTEND_HOSTING is nodejs + .default('0.0.0.0'), FRONTEND_MODULE_PORT: Joi.number() .integer() @@ -40,17 +104,11 @@ module.exports = commonSchema.keys({ .max(49151) .description('Port for hosting Frontend with Vite as a Node.js instance, default: 3000') .default(3000) - .required(), // required only if FRONTEND_HOSTING is nodejs - - ADMIN_AUTH_URL: Joi.string() - .uri({ scheme: ['http', 'https'] }) - .when('browser_protocol', { - is: 'https', - then: Joi.string().uri({ scheme: 'https' }), - otherwise: Joi.string().uri({ scheme: 'http' }), - }) - .description('Extern Url for admin-frontend') - .default('http://0.0.0.0/admin/authenticate?token='), + .when('FRONTEND_HOSTING', { + is: 'nodejs', + then: Joi.required(), + otherwise: Joi.optional(), + }), META_URL: Joi.string() .uri({ scheme: ['http', 'https'] }) diff --git a/frontend/vite.config.js b/frontend/vite.config.js index cb885dcec..33ee02239 100644 --- a/frontend/vite.config.js +++ b/frontend/vite.config.js @@ -15,8 +15,6 @@ dotenv.config() // load env vars from .env const CONFIG = require('./src/config') -console.log(CONFIG) - // https://vitejs.dev/config/ export default defineConfig({ server: { @@ -79,7 +77,7 @@ export default defineConfig({ URL_PROTOCOL: null, COMMUNITY_URL: null, GRAPHQL_PATH: null, - EXTERNAL_BACKEND_URL: CONFIG.EXTERNAL_BACKEND_URL, // null, + GRAPHQL_URI: CONFIG.GRAPHQL_URI, // null, ADMIN_AUTH_PATH: CONFIG.ADMIN_AUTH_PATH ?? null, // it is the only env without exported default ADMIN_AUTH_URL: CONFIG.ADMIN_AUTH_URL, // null, COMMUNITY_NAME: null,