feat(next): add typesafe config

Because:

* Use Next.js standard environment variable procedure
* Provide typed config to be used throughout Payments Next.

This commit:

* Refactor config to use Next.js native environment loaders
* Remove .env.json and .env.production.json
* Add validator and transformer functions to provide
  validated and typesafe config.

Closes #FXA-9436
This commit is contained in:
Reino Muhl 2024-04-18 14:55:54 -04:00
Родитель 11a7356109
Коммит cb41181b4d
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: C86660FCF998897A
15 изменённых файлов: 191 добавлений и 76 удалений

Просмотреть файл

@ -0,0 +1,30 @@
#
# Set default variables for development environment
# e.g. yarn nx run payments-next:start
#
# Auth
AUTH__ISSUER_URL=http://localhost:3030
AUTH__WELL_KNOWN_URL=http://localhost:3030/.well-known/openid-configuration
AUTH__CLIENT_ID=32aaeb6f1c21316a
# NextAuth
NEXTAUTH_URL_INTERNAL=http://localhost:3035
AUTH_SECRET=replacewithsecret
# MySQLConfig
MYSQL_CONFIG__DATABASE=fxa
MYSQL_CONFIG__HOST=127.0.0.1
MYSQL_CONFIG__PORT=3306
MYSQL_CONFIG__USER=root
MYSQL_CONFIG__PASSWORD=
MYSQL_CONFIG__CONNECTION_LIMIT_MIN=
MYSQL_CONFIG__CONNECTION_LIMIT_MAX=20
MYSQL_CONFIG__ACQUIRE_TIMEOUT_MILLIS=
# GeoDBConfig
GEODB_CONFIG__DB_PATH=../../../libs/shared/geodb/db/cities-db.mmdb
# GeoDBManagerConfig
GEODB_MANAGER_CONFIG__LOCATION_OVERRIDE__COUNTRY_CODE=
GEODB_MANAGER_CONFIG__LOCATION_OVERRIDE__POSTAL_CODE=

Просмотреть файл

@ -1,12 +0,0 @@
# Auth
AUTH_ISSUER_URL=http://localhost:3030
AUTH_WELL_KNOWN_URL=http://localhost:3030/.well-known/openid-configuration
AUTH_CLIENT_ID=32aaeb6f1c21316a
# NextAuth
NEXTAUTH_URL_INTERNAL=http://localhost:3035
AUTH_SECRET=
# GeoDBManagerConfig
GEODB_MANAGER_CONFIG__LOCATION_OVERRIDE__COUNTRY_CODE=ZA
GEODB_MANAGER_CONFIG__LOCATION_OVERRIDE__POSTAL_CODE=11233

Просмотреть файл

@ -1,19 +0,0 @@
{
"mysqlConfig": {
"database": "fxa",
"port": 3306,
"host": "127.0.0.1",
"user": "root",
"password": "",
"connectionLimitMax": 20
},
"geodbConfig": {
"dbPath": "../../../libs/shared/geodb/db/cities-db.mmdb"
},
"geodbManagerConfig": {
"locationOverride": {
"countryCode": "",
"postalCode": ""
}
}
}

Просмотреть файл

@ -0,0 +1,26 @@
#
# Set default variables for production environment
# e.g. yarn nx run payments-next:build
#
# Auth
AUTH__ISSUER_URL=
AUTH__WELL_KNOWN_URL=
AUTH__CLIENT_ID=
# NextAuth
NEXTAUTH_URL_INTERNAL=
AUTH_SECRET=
# MySQLConfig
MYSQL_CONFIG__DATABASE=fxa
MYSQL_CONFIG__HOST=
MYSQL_CONFIG__PORT=3306
MYSQL_CONFIG__USER=
MYSQL_CONFIG__PASSWORD=
MYSQL_CONFIG__CONNECTION_LIMIT_MIN=
MYSQL_CONFIG__CONNECTION_LIMIT_MAX=20
MYSQL_CONFIG__ACQUIRE_TIMEOUT_MILLIS=
# GeoDBConfig
GEODB_CONFIG__DB_PATH=

Просмотреть файл

@ -1,19 +0,0 @@
{
"mysqlConfig": {
"database": "fxa",
"port": 3306,
"host": "",
"user": "",
"password": "",
"connectionLimitMax": 20
},
"geodbConfig": {
"dbPath": "../../../libs/shared/geodb/db/cities-db.mmdb"
},
"geodbManagerConfig": {
"locationOverride": {
"countryCode": "",
"postalCode": ""
}
}
}

Просмотреть файл

@ -4,6 +4,7 @@
import NextAuth from 'next-auth';
import { authConfig } from './auth.config';
import { config } from './config';
export const {
handlers: { GET, POST },
@ -17,14 +18,14 @@ export const {
id: 'fxa',
name: 'Firefox Accounts',
type: 'oidc',
issuer: process.env.AUTH_ISSUER_URL,
wellKnown: process.env.AUTH_WELL_KNOWN_URL,
issuer: config.auth.issuerUrl,
wellKnown: config.auth.wellKnownUrl,
checks: ['pkce', 'state'],
client: {
token_endpoint_auth_method: 'none',
},
authorization: { params: { scope: 'openid email profile' } },
clientId: process.env.AUTH_CLIENT_ID,
clientId: config.auth.clientId,
},
],
callbacks: {

Просмотреть файл

@ -0,0 +1,33 @@
import 'reflect-metadata';
import { Type } from 'class-transformer';
import { IsString, ValidateNested, IsDefined } from 'class-validator';
import {
RootConfig as NestAppRootConfig,
validate,
} from '@fxa/payments/ui/server';
class AuthJSConfig {
@IsString()
issuerUrl!: string;
@IsString()
wellKnownUrl!: string;
@IsString()
clientId!: string;
}
export class PaymentsNextConfig extends NestAppRootConfig {
@Type(() => AuthJSConfig)
@ValidateNested()
@IsDefined()
auth!: AuthJSConfig;
@IsString()
nextauthUrlInternal!: string;
@IsString()
authSecret!: string;
}
export const config = validate(process.env, PaymentsNextConfig);

Просмотреть файл

@ -15,15 +15,6 @@
"configurations": {
"development": {
"outputPath": "apps/payments/next"
},
"production": {
"assets": [
{
"input": "apps/payments/next",
"glob": ".env.production.json",
"output": "./../"
}
]
}
},
"dependsOn": ["l10n-bundle"]
@ -98,6 +89,9 @@
"dependsOn": ["l10n-prime"],
"command": "node -r esbuild-register apps/payments/next/app/_lib/scripts/convert.ts"
},
"ztest": {
"command": "node -r esbuild-register apps/payments/next/config/index.ts"
},
"storybook": {
"executor": "@nx/storybook:storybook",
"options": {

Просмотреть файл

@ -0,0 +1,73 @@
import set from 'set-value';
import { plainToInstance, ClassConstructor } from 'class-transformer';
import { validateSync } from 'class-validator';
const SEPARATOR = '__';
/**
* Validate the config record against a config class with validation constraints
*
* More information: https://docs.nestjs.com/techniques/configuration#custom-validate-function
*
* @param config - any config record, typically process.env
* @param configClass - class-validator class with validation decorators
* @returns validated and formatted config object, typed according to input configClass
*/
export function validate<T extends object>(
config: Record<string, unknown>,
configClass: ClassConstructor<T>
) {
const dotEnv = transform(config);
const validatedConfig = plainToInstance(configClass, dotEnv, {
enableImplicitConversion: true,
});
const errors = validateSync(validatedConfig, {
skipMissingProperties: false,
});
if (errors.length > 0) {
throw new Error(errors.toString());
}
return validatedConfig;
}
/**
* Transform record with string keys, into a nested object using SEPARATOR
* e.g.
* {
* NODE_ENV=20
* AUTH__SECRET_KEY=verysecret
* }
*
* transforms into
*
* {
* nodeEnv: '20',
* auth: {
* secretKey: 'verysecret',
* }
* }
*
*/
function transform(config: Record<string, unknown>) {
const keyTransformer = (key: string) =>
key.toLowerCase().replace(/(?<!_)_([a-z])/g, (_, p1) => p1.toUpperCase());
config = Object.entries(config).reduce<Record<string, any>>(
(acc, [key, value]) => {
acc[keyTransformer(key)] = value;
return acc;
},
{}
);
const temp = {};
Object.entries(config).forEach(([key, value]) => {
set(temp, key.split(SEPARATOR), value);
});
config = temp;
return config;
}

Просмотреть файл

@ -2,7 +2,7 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import { dotenvLoader, fileLoader, TypedConfigModule } from 'nest-typed-config';
import { TypedConfigModule } from 'nest-typed-config';
import { CartManager, CartService } from '@fxa/payments/cart';
import { AccountDatabaseNestFactory } from '@fxa/shared/db/mysql/account';
@ -13,21 +13,16 @@ import { LocalizerRscFactoryProvider } from '@fxa/shared/l10n/server';
import { AccountCustomerManager } from '@fxa/payments/stripe';
import { NextJSActionsService } from './nextjs-actions.service';
import { GeoDBManager, GeoDBNestFactory } from '@fxa/shared/geodb';
import { validate } from '../config.utils';
@Module({
imports: [
TypedConfigModule.forRoot({
schema: RootConfig,
load: [
fileLoader(),
dotenvLoader({
separator: '__',
keyTransformer: (key) =>
key
.toLowerCase()
.replace(/(?<!_)_([a-z])/g, (_, p1) => p1.toUpperCase()),
}),
],
// Use the same validate function as apps/payments-next/config
// to ensure the same environment variables are loaded following
// the same process.
load: () => validate(process.env, RootConfig),
}),
],
controllers: [],

Просмотреть файл

@ -21,6 +21,5 @@ export class RootConfig {
@Type(() => GeoDBManagerConfig)
@ValidateNested()
@IsDefined()
public readonly geodbManagerConfig!: Partial<GeoDBManagerConfig>;
}

Просмотреть файл

@ -4,6 +4,8 @@
// Use this file to export React server components
export * from './lib/nestapp/app';
export * from './lib/nestapp/config';
export * from './lib/config.utils';
export * from './lib/server/purchase-details';
export * from './lib/server/subscription-title';
export * from './lib/server/terms-and-privacy';

Просмотреть файл

@ -2,7 +2,7 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import { Type } from 'class-transformer';
import { IsString, ValidateNested, IsDefined } from 'class-validator';
import { IsString, ValidateNested } from 'class-validator';
class LocationOverride {
@IsString()
@ -20,6 +20,5 @@ export class GeoDBConfig {
export class GeoDBManagerConfig {
@Type(() => LocationOverride)
@ValidateNested()
@IsDefined()
public readonly locationOverride!: Partial<LocationOverride>;
}

Просмотреть файл

@ -81,6 +81,7 @@
"class-transformer": "^0.5.1",
"class-validator": "^0.14.1",
"diffparser": "^2.0.1",
"dotenv": "^16.4.5",
"graphql": "^16.8.0",
"graphql-request": "^6.1.0",
"hot-shots": "^10.0.0",
@ -108,6 +109,7 @@
"react-ga4": "^2.1.0",
"rxjs": "^7.8.1",
"semver": "^7.6.0",
"set-value": "^4.1.0",
"tslib": "^2.6.2",
"uuid": "^9.0.0",
"winston": "^3.13.0"
@ -182,6 +184,7 @@
"@types/react": "^18",
"@types/react-dom": "^18",
"@types/react-test-renderer": "^18",
"@types/set-value": "^4",
"@types/uuid": "^8.3.0",
"@typescript-eslint/eslint-plugin": "^5.59.1",
"@typescript-eslint/parser": "^7.1.1",

Просмотреть файл

@ -22648,6 +22648,13 @@ __metadata:
languageName: node
linkType: hard
"@types/set-value@npm:^4":
version: 4.0.3
resolution: "@types/set-value@npm:4.0.3"
checksum: e7e45af27403d710d460ba7a1c4ba75fdeaa783ac7f8e1483f43273fd6feb27354d3dba92982a7f66021037e2f5383db6b799afb85b1cfc17c769648648843b5
languageName: node
linkType: hard
"@types/sharp@npm:^0":
version: 0.26.0
resolution: "@types/sharp@npm:0.26.0"
@ -38415,6 +38422,7 @@ fsevents@~2.1.1:
"@types/react": ^18
"@types/react-dom": ^18
"@types/react-test-renderer": ^18
"@types/set-value": ^4
"@types/uuid": ^8.3.0
"@typescript-eslint/eslint-plugin": ^5.59.1
"@typescript-eslint/parser": ^7.1.1
@ -38423,6 +38431,7 @@ fsevents@~2.1.1:
class-transformer: ^0.5.1
class-validator: ^0.14.1
diffparser: ^2.0.1
dotenv: ^16.4.5
esbuild: ^0.17.15
esbuild-register: ^3.5.0
eslint: ^7.32.0
@ -38478,6 +38487,7 @@ fsevents@~2.1.1:
rxjs: ^7.8.1
semver: ^7.6.0
server-only: ^0.0.1
set-value: ^4.1.0
stylelint: ^16.2.1
stylelint-config-prettier: ^9.0.3
stylelint-config-recommended-scss: ^14.0.0