This repository has been archived by the owner on Sep 18, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(config): improve setup and variable checking using zod
Removed pre-configured config values and use zod to parse the `.env` file and analyze for any missed/mistyped config values.
- Loading branch information
Showing
28 changed files
with
314 additions
and
188 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,46 +1,70 @@ | ||
API_PORT = 5000 | ||
####################### | ||
# Default config file # | ||
####################### | ||
|
||
#* If a parameter is noted as a LIST, it means that you can separate | ||
#* multiple values with a ';', DO NOT add a ';' at the end of the line, | ||
#* only between values | ||
|
||
#! ALL time value are expressed in milliseconds | ||
|
||
API_PORT = '4000' | ||
API_URL = 'http://localhost:4000' | ||
DEBUG = 'true' | ||
|
||
# Allowed URLs for CORS 'Access-Control-Allow-Origin' header | ||
CORS_ORIGIN_WHITELIST = 'https://ae.utbm.fr;' | ||
DEBUG = true | ||
# -> LIST | ||
# -> Accepted values: URLs or '*' | ||
CORS_ORIGIN_WHITELIST = 'https://ae.utbm.fr' | ||
|
||
# Postgres configuration | ||
# You can avoid user password when developing on local machine | ||
POSTGRES_DB = 'ae_test' | ||
POSTGRES_HOST = '127.0.0.1' | ||
POSTGRES_PORT = '5432' | ||
POSTGRES_USER = 'postgres' | ||
POSTGRES_PASSWORD = 'postgres' | ||
POSTGRES_PASSWORD = 'postgres' # optional | ||
|
||
# JWT configuration | ||
# Note: changing the key will invalidate all existing tokens | ||
JWT_KEY = 'secret_jwt_key' | ||
JWT_EXPIRATION_TIME = 604800000 # 1 week in milliseconds | ||
# Expiration time in milliseconds | ||
JWT_EXPIRATION_TIME = '604800000' # 1 week | ||
|
||
# Base directory for generic uploaded files | ||
FILES_BASE_DIR = './public' | ||
|
||
# Users configuration | ||
# Users files root directory | ||
USERS_BASE_PATH = './public/users' | ||
# Delay before allowing the user to update its profile picture | ||
USERS_PICTURES_DELAY = 604800000 # 1 week in milliseconds | ||
USERS_PICTURES_PATH = './public/users/pictures' | ||
USERS_BANNERS_PATH = './public/users/banners' | ||
USERS_PATH = './public/users' | ||
USERS_PICTURES_DELAY = '604800000' # 1 week | ||
# Delay before deleting unverified users | ||
USERS_VERIFICATION_DELAY = '604800000' # 1 week | ||
|
||
# Promotion configuration | ||
PROMOTION_LOGO_PATH = './public/promotions' | ||
# Promotion files root directory | ||
PROMOTION_BASE_PATH = './public/promotions' | ||
|
||
# Emailing configuration | ||
# Note: use your own UTBM email account when developing on local machine | ||
EMAIL_HOST = 'smtp.domain.com' | ||
EMAIL_PORT = 465 | ||
EMAIL_SECURE = true | ||
EMAIL_PORT = '465' | ||
EMAIL_SECURE = 'true' | ||
EMAIL_AUTH_USER = '[email protected]' | ||
EMAIL_AUTH_PASS = 'azertyuiop' | ||
EMAIL_ENABLED = true | ||
EMAIL_ENABLED = 'false' | ||
|
||
# Whitelisted emails/host can be used to register | ||
WHITELISTED_HOSTS = | ||
# Whitelisted hosts can be used to register | ||
# -> Bypass email validation | ||
# -> LIST | ||
WHITELISTED_HOSTS = '@gmail.com' | ||
# Whitelisted emails can be used to register, despite their host | ||
# being blacklisted | ||
# -> Bypass email validation | ||
# -> LIST | ||
WHITELISTED_EMAILS = '[email protected];[email protected]' | ||
# Blacklisted emails/host cannot be used to register | ||
BLACKLISTED_HOSTS = '@utbm.fr;' | ||
BLACKLISTED_EMAILS = | ||
|
||
# Blacklisted hosts/emails cannot be used to register, | ||
# unless they are whitelisted in WHITELISTED_HOSTS or WHITELISTED_EMAILS | ||
# -> LISTs | ||
BLACKLISTED_HOSTS = '@utbm.fr' | ||
BLACKLISTED_EMAILS = '[email protected]' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,47 +1,90 @@ | ||
/* istanbul ignore file */ | ||
import { join } from 'path'; | ||
|
||
export type Config = typeof config; | ||
|
||
const config = () => ({ | ||
production: process.env['DEBUG'] !== 'true', | ||
api_url: | ||
process.env['DEBUG'] === 'true' | ||
? `http://localhost:${parseInt(process.env['API_PORT'], 10) || 3000}` | ||
: 'https://ae.utbm.fr/api', | ||
port: parseInt(process.env['API_PORT'], 10) || 3000, | ||
cors: process.env['DEBUG'] === 'true' ? ['*'] : process.env['CORS_ORIGIN_WHITELIST']?.split(';'), | ||
auth: { | ||
jwtKey: process.env['JWT_KEY'], | ||
jwtExpirationTime: parseInt(process.env['JWT_EXPIRATION_TIME'], 10) || 60 * 60 * 24 * 7 * 1000, // 1 week | ||
}, | ||
files: { | ||
baseDir: join(process.cwd(), process.env['FILES_BASE_DIR'] || './public'), | ||
users: join(process.cwd(), process.env['USERS_PATH'] || './public/users'), | ||
promotions: join(process.cwd(), process.env['PROMOTIONS_LOGO_PATH'] || './public/promotions'), | ||
}, | ||
users: { | ||
verification_token_validity: 7, // number of days before the account being deleted | ||
picture_cooldown: parseInt(process.env['USERS_PICTURES_DELAY'], 10) || 60 * 60 * 24 * 7 * 1000, // 1 week | ||
}, | ||
email: { | ||
enabled: process.env['EMAIL_ENABLED'] === 'true', | ||
host: process.env['EMAIL_HOST'], | ||
port: parseInt(process.env['EMAIL_PORT'], 10) || 465, | ||
secure: process.env['EMAIL_SECURE'] === 'true', | ||
auth: { | ||
user: process.env['EMAIL_AUTH_USER'], | ||
pass: process.env['EMAIL_AUTH_PASS'], | ||
}, | ||
whitelist: { | ||
hosts: process.env['WHITELISTED_HOSTS']?.split(';') ?? [], | ||
emails: ['[email protected]', ...(process.env['WHITELISTED_EMAILS']?.split(';') ?? [])], | ||
}, | ||
blacklist: { | ||
hosts: ['@utbm.fr', ...(process.env['BLACKLISTED_HOSTS']?.split(';') ?? [])], | ||
emails: process.env['BLACKLISTED_EMAILS']?.split(';') ?? [], | ||
}, | ||
}, | ||
}); | ||
|
||
export default config; | ||
import { existsSync } from 'node:fs'; | ||
import { join } from 'node:path'; | ||
import 'dotenv/config'; | ||
|
||
import { Logger } from '@nestjs/common'; | ||
import { z } from 'zod'; | ||
|
||
/** | ||
* NodeJS environment variables + API specific environment variables, validated using zod | ||
* @see https://github.com/colinhacks/zod | ||
*/ | ||
export const env = (() => { | ||
// Throw if the .env file is missing | ||
if (!existsSync(join(process.cwd(), '.env'))) { | ||
Logger.error( | ||
"Cannot start the server, the '.env' file is missing, have you copied and edited the '.env.example' file?", | ||
'env.ts', | ||
); | ||
process.exit(1); | ||
} | ||
|
||
const refineAsEmail = (str: string) => { | ||
str.split(';').every((s) => { | ||
z.string().email().parse(s); | ||
}); | ||
return true; | ||
}; | ||
|
||
const refineAsHost = (str: string) => { | ||
str.split(';').every((s) => { | ||
z.string().startsWith('@').parse(s); | ||
}); | ||
return true; | ||
}; | ||
|
||
// Validate the environment variables using zod | ||
const schema = z.object({ | ||
API_PORT: z.coerce.number().min(0), | ||
API_URL: z.string().url(), | ||
|
||
CORS_ORIGIN_WHITELIST: z.string().refine((str) => str.split(';').every((s) => s.startsWith('http') || s === '*'), { | ||
message: 'The CORS_ORIGIN_WHITELIST environment variable must be a list of origins separated by a semicolon (;)', | ||
}), | ||
DEBUG: z.enum(['true', 'false']).transform((value) => value === 'true'), | ||
|
||
JWT_KEY: z.string().min(1), | ||
JWT_EXPIRATION_TIME: z.coerce.number().min(0), | ||
|
||
POSTGRES_DB: z.string(), | ||
POSTGRES_HOST: z.string().ip({ version: 'v4' }), | ||
POSTGRES_PASSWORD: z.string().optional(), | ||
POSTGRES_PORT: z.coerce.number().min(0), | ||
POSTGRES_USER: z.string(), | ||
|
||
FILES_BASE_DIR: z.string().startsWith('./'), | ||
|
||
PROMOTION_BASE_PATH: z.string().startsWith('./'), | ||
|
||
USERS_PICTURES_DELAY: z.coerce.number().min(0), | ||
USERS_VERIFICATION_DELAY: z.coerce.number().min(0), | ||
USERS_BASE_PATH: z.string().startsWith('./'), | ||
|
||
EMAIL_HOST: z.string(), | ||
EMAIL_PORT: z.coerce.number().min(0), | ||
EMAIL_SECURE: z.enum(['true', 'false']).transform((value) => value === 'true'), | ||
EMAIL_AUTH_USER: z.string().email(), | ||
EMAIL_AUTH_PASS: z.string(), | ||
EMAIL_ENABLED: z.enum(['true', 'false']).transform((value) => value === 'true'), | ||
|
||
WHITELISTED_HOSTS: z.string().refine(refineAsHost, { | ||
message: 'Should be a list of emails host, each starting with arobase (@) and separated with a semicolon (;)', | ||
}), | ||
WHITELISTED_EMAILS: z | ||
.string() | ||
.refine(refineAsEmail, { message: 'Should be a list of emails separated by a semicolon (;)' }), | ||
|
||
BLACKLISTED_HOSTS: z.string().refine(refineAsHost, { | ||
message: 'Should be a list of emails host, each starting with arobase (@) and separated with a semicolon (;)', | ||
}), | ||
BLACKLISTED_EMAILS: z | ||
.string() | ||
.refine(refineAsEmail, { message: 'Should be a list of emails separated by a semicolon (;)' }), | ||
}); | ||
|
||
return schema.parse(process.env) as NodeJS.ProcessEnv & | ||
Required<Omit<ReturnType<typeof schema.parse>, 'POSTGRES_PASSWORD'>> & { | ||
POSTGRES_PASSWORD?: string; | ||
}; | ||
})(); |
Oops, something went wrong.