Add Paddle configuration foundation
This commit is contained in:
parent
efd8630aa0
commit
aa2438279b
@ -15,6 +15,14 @@ async function generate(): Promise<void> {
|
||||
'openapi-placeholder-account-access-secret';
|
||||
process.env.ACCOUNT_ACCESS_TOKEN_TTL_SECONDS ??= '900';
|
||||
process.env.MAX_UPLOAD_SIZE_BYTES ??= '524288000';
|
||||
process.env.PADDLE_ENVIRONMENT ??= 'sandbox';
|
||||
process.env.PADDLE_API_KEY ??= 'openapi-placeholder-paddle-api-key';
|
||||
process.env.PADDLE_WEBHOOK_SECRET ??=
|
||||
'openapi-placeholder-paddle-webhook-secret';
|
||||
process.env.PADDLE_PRO_MONTHLY_PRICE_ID ??=
|
||||
'openapi-placeholder-paddle-monthly-price-id';
|
||||
process.env.PADDLE_PRO_YEARLY_PRICE_ID ??=
|
||||
'openapi-placeholder-paddle-yearly-price-id';
|
||||
|
||||
const { createApp } = await import('../src/app.factory');
|
||||
const app = await createApp();
|
||||
|
||||
27
backend/src/modules/config/config.service.spec.ts
Normal file
27
backend/src/modules/config/config.service.spec.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { AppConfigService } from './config.service';
|
||||
|
||||
describe('AppConfigService Paddle configuration', () => {
|
||||
it('returns the validated Paddle configuration values', () => {
|
||||
const configService = new ConfigService({
|
||||
PADDLE_ENVIRONMENT: 'production',
|
||||
PADDLE_API_KEY: 'test-paddle-api-key',
|
||||
PADDLE_WEBHOOK_SECRET: 'test-paddle-webhook-secret',
|
||||
PADDLE_PRO_MONTHLY_PRICE_ID: 'test-monthly-price-id',
|
||||
PADDLE_PRO_YEARLY_PRICE_ID: 'test-yearly-price-id',
|
||||
});
|
||||
const appConfigService = new AppConfigService(configService);
|
||||
|
||||
expect(appConfigService.getPaddleEnvironment()).toBe('production');
|
||||
expect(appConfigService.getPaddleApiKey()).toBe('test-paddle-api-key');
|
||||
expect(appConfigService.getPaddleWebhookSecret()).toBe(
|
||||
'test-paddle-webhook-secret',
|
||||
);
|
||||
expect(appConfigService.getPaddleProMonthlyPriceId()).toBe(
|
||||
'test-monthly-price-id',
|
||||
);
|
||||
expect(appConfigService.getPaddleProYearlyPriceId()).toBe(
|
||||
'test-yearly-price-id',
|
||||
);
|
||||
});
|
||||
});
|
||||
@ -1,5 +1,6 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import type { PaddleEnvironment } from './environment';
|
||||
|
||||
@Injectable()
|
||||
export class AppConfigService {
|
||||
@ -41,6 +42,26 @@ export class AppConfigService {
|
||||
return Number(this.required('MAX_UPLOAD_SIZE_BYTES'));
|
||||
}
|
||||
|
||||
getPaddleEnvironment(): PaddleEnvironment {
|
||||
return this.required('PADDLE_ENVIRONMENT') as PaddleEnvironment;
|
||||
}
|
||||
|
||||
getPaddleApiKey(): string {
|
||||
return this.required('PADDLE_API_KEY');
|
||||
}
|
||||
|
||||
getPaddleWebhookSecret(): string {
|
||||
return this.required('PADDLE_WEBHOOK_SECRET');
|
||||
}
|
||||
|
||||
getPaddleProMonthlyPriceId(): string {
|
||||
return this.required('PADDLE_PRO_MONTHLY_PRICE_ID');
|
||||
}
|
||||
|
||||
getPaddleProYearlyPriceId(): string {
|
||||
return this.required('PADDLE_PRO_YEARLY_PRICE_ID');
|
||||
}
|
||||
|
||||
get appVersion(): string {
|
||||
return process.env.npm_package_version ?? '0.1.0';
|
||||
}
|
||||
|
||||
@ -1,18 +1,29 @@
|
||||
import { validateEnvironment } from './environment';
|
||||
|
||||
describe('validateEnvironment', () => {
|
||||
const validEnvironment = (
|
||||
overrides: Record<string, unknown> = {},
|
||||
): Record<string, unknown> => ({
|
||||
NODE_ENV: 'test',
|
||||
PORT: '3007',
|
||||
DATABASE_URL:
|
||||
'postgresql://velody:velody@localhost:5432/velody?schema=public',
|
||||
STORAGE_ROOT: '/tmp/velody',
|
||||
PUBLIC_BASE_URL: 'http://localhost:3007',
|
||||
DEVICE_BOOTSTRAP_SECRET: 'device-secret',
|
||||
ACCOUNT_ACCESS_TOKEN_SECRET: 'account-secret',
|
||||
ACCOUNT_ACCESS_TOKEN_TTL_SECONDS: '900',
|
||||
MAX_UPLOAD_SIZE_BYTES: '1024',
|
||||
PADDLE_ENVIRONMENT: 'sandbox',
|
||||
PADDLE_API_KEY: 'test-paddle-api-key',
|
||||
PADDLE_WEBHOOK_SECRET: 'test-paddle-webhook-secret',
|
||||
PADDLE_PRO_MONTHLY_PRICE_ID: 'test-monthly-price-id',
|
||||
PADDLE_PRO_YEARLY_PRICE_ID: 'test-yearly-price-id',
|
||||
...overrides,
|
||||
});
|
||||
|
||||
it('accepts a valid environment', () => {
|
||||
const result = validateEnvironment({
|
||||
NODE_ENV: 'test',
|
||||
PORT: '3007',
|
||||
DATABASE_URL: 'postgresql://velody:velody@localhost:5432/velody?schema=public',
|
||||
STORAGE_ROOT: '/tmp/velody',
|
||||
PUBLIC_BASE_URL: 'http://localhost:3007',
|
||||
DEVICE_BOOTSTRAP_SECRET: 'device-secret',
|
||||
ACCOUNT_ACCESS_TOKEN_SECRET: 'account-secret',
|
||||
ACCOUNT_ACCESS_TOKEN_TTL_SECONDS: '900',
|
||||
MAX_UPLOAD_SIZE_BYTES: '1024',
|
||||
});
|
||||
const result = validateEnvironment(validEnvironment());
|
||||
|
||||
expect(result.PORT).toBe(3007);
|
||||
expect(result.ACCOUNT_ACCESS_TOKEN_TTL_SECONDS).toBe(900);
|
||||
@ -29,18 +40,50 @@ describe('validateEnvironment', () => {
|
||||
|
||||
it('rejects reuse of the device-token secret for account tokens', () => {
|
||||
expect(() =>
|
||||
validateEnvironment({
|
||||
NODE_ENV: 'test',
|
||||
PORT: '3007',
|
||||
DATABASE_URL:
|
||||
'postgresql://velody:velody@localhost:5432/velody?schema=public',
|
||||
STORAGE_ROOT: '/tmp/velody',
|
||||
PUBLIC_BASE_URL: 'http://localhost:3007',
|
||||
DEVICE_BOOTSTRAP_SECRET: 'shared-secret',
|
||||
ACCOUNT_ACCESS_TOKEN_SECRET: 'shared-secret',
|
||||
ACCOUNT_ACCESS_TOKEN_TTL_SECONDS: '900',
|
||||
MAX_UPLOAD_SIZE_BYTES: '1024',
|
||||
}),
|
||||
validateEnvironment(
|
||||
validEnvironment({
|
||||
DEVICE_BOOTSTRAP_SECRET: 'shared-secret',
|
||||
ACCOUNT_ACCESS_TOKEN_SECRET: 'shared-secret',
|
||||
}),
|
||||
),
|
||||
).toThrow(/account and device token secrets must be different/);
|
||||
});
|
||||
|
||||
it('accepts the Paddle sandbox environment', () => {
|
||||
const result = validateEnvironment(
|
||||
validEnvironment({ PADDLE_ENVIRONMENT: 'sandbox' }),
|
||||
);
|
||||
|
||||
expect(result.PADDLE_ENVIRONMENT).toBe('sandbox');
|
||||
});
|
||||
|
||||
it('accepts the Paddle production environment', () => {
|
||||
const result = validateEnvironment(
|
||||
validEnvironment({ PADDLE_ENVIRONMENT: 'production' }),
|
||||
);
|
||||
|
||||
expect(result.PADDLE_ENVIRONMENT).toBe('production');
|
||||
});
|
||||
|
||||
it('rejects an invalid Paddle environment', () => {
|
||||
expect(() =>
|
||||
validateEnvironment(
|
||||
validEnvironment({ PADDLE_ENVIRONMENT: 'development' }),
|
||||
),
|
||||
).toThrow(/Invalid environment configuration/);
|
||||
});
|
||||
|
||||
it.each([
|
||||
'PADDLE_API_KEY',
|
||||
'PADDLE_WEBHOOK_SECRET',
|
||||
'PADDLE_PRO_MONTHLY_PRICE_ID',
|
||||
'PADDLE_PRO_YEARLY_PRICE_ID',
|
||||
])('rejects a missing %s', (key) => {
|
||||
const environment = validEnvironment();
|
||||
delete environment[key];
|
||||
|
||||
expect(() => validateEnvironment(environment)).toThrow(
|
||||
/Invalid environment configuration/,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { plainToInstance } from 'class-transformer';
|
||||
import {
|
||||
IsIn,
|
||||
IsInt,
|
||||
IsNotEmpty,
|
||||
IsString,
|
||||
@ -9,6 +10,8 @@ import {
|
||||
validateSync,
|
||||
} from 'class-validator';
|
||||
|
||||
export type PaddleEnvironment = 'sandbox' | 'production';
|
||||
|
||||
class EnvironmentVariables {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@ -48,6 +51,25 @@ class EnvironmentVariables {
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
MAX_UPLOAD_SIZE_BYTES!: number;
|
||||
|
||||
@IsIn(['sandbox', 'production'])
|
||||
PADDLE_ENVIRONMENT!: PaddleEnvironment;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
PADDLE_API_KEY!: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
PADDLE_WEBHOOK_SECRET!: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
PADDLE_PRO_MONTHLY_PRICE_ID!: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
PADDLE_PRO_YEARLY_PRICE_ID!: string;
|
||||
}
|
||||
|
||||
export function validateEnvironment(config: Record<string, unknown>) {
|
||||
|
||||
@ -17,3 +17,13 @@ process.env.ACCOUNT_ACCESS_TOKEN_TTL_SECONDS =
|
||||
process.env.ACCOUNT_ACCESS_TOKEN_TTL_SECONDS ?? '900';
|
||||
process.env.MAX_UPLOAD_SIZE_BYTES =
|
||||
process.env.MAX_UPLOAD_SIZE_BYTES ?? '1073741824';
|
||||
process.env.PADDLE_ENVIRONMENT =
|
||||
process.env.PADDLE_ENVIRONMENT ?? 'sandbox';
|
||||
process.env.PADDLE_API_KEY =
|
||||
process.env.PADDLE_API_KEY ?? 'test-paddle-api-key';
|
||||
process.env.PADDLE_WEBHOOK_SECRET =
|
||||
process.env.PADDLE_WEBHOOK_SECRET ?? 'test-paddle-webhook-secret';
|
||||
process.env.PADDLE_PRO_MONTHLY_PRICE_ID =
|
||||
process.env.PADDLE_PRO_MONTHLY_PRICE_ID ?? 'test-paddle-monthly-price-id';
|
||||
process.env.PADDLE_PRO_YEARLY_PRICE_ID =
|
||||
process.env.PADDLE_PRO_YEARLY_PRICE_ID ?? 'test-paddle-yearly-price-id';
|
||||
|
||||
5
infra/docker/env/backend.env.example
vendored
5
infra/docker/env/backend.env.example
vendored
@ -7,3 +7,8 @@ DEVICE_BOOTSTRAP_SECRET=replace-me
|
||||
ACCOUNT_ACCESS_TOKEN_SECRET=replace-with-a-distinct-secret
|
||||
ACCOUNT_ACCESS_TOKEN_TTL_SECONDS=900
|
||||
MAX_UPLOAD_SIZE_BYTES=524288000
|
||||
PADDLE_ENVIRONMENT=sandbox
|
||||
PADDLE_API_KEY=replace-with-paddle-api-key
|
||||
PADDLE_WEBHOOK_SECRET=replace-with-paddle-webhook-secret
|
||||
PADDLE_PRO_MONTHLY_PRICE_ID=replace-with-paddle-monthly-price-id
|
||||
PADDLE_PRO_YEARLY_PRICE_ID=replace-with-paddle-yearly-price-id
|
||||
|
||||
Loading…
Reference in New Issue
Block a user