зеркало из https://github.com/mozilla/fxa.git
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:
Родитель
11a7356109
Коммит
cb41181b4d
|
@ -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",
|
||||
|
|
10
yarn.lock
10
yarn.lock
|
@ -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
|
||||
|
|
Загрузка…
Ссылка в новой задаче