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';
|
'openapi-placeholder-account-access-secret';
|
||||||
process.env.ACCOUNT_ACCESS_TOKEN_TTL_SECONDS ??= '900';
|
process.env.ACCOUNT_ACCESS_TOKEN_TTL_SECONDS ??= '900';
|
||||||
process.env.MAX_UPLOAD_SIZE_BYTES ??= '524288000';
|
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 { createApp } = await import('../src/app.factory');
|
||||||
const app = await createApp();
|
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 { Injectable } from '@nestjs/common';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import type { PaddleEnvironment } from './environment';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AppConfigService {
|
export class AppConfigService {
|
||||||
@ -41,6 +42,26 @@ export class AppConfigService {
|
|||||||
return Number(this.required('MAX_UPLOAD_SIZE_BYTES'));
|
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 {
|
get appVersion(): string {
|
||||||
return process.env.npm_package_version ?? '0.1.0';
|
return process.env.npm_package_version ?? '0.1.0';
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,19 +1,30 @@
|
|||||||
import { validateEnvironment } from './environment';
|
import { validateEnvironment } from './environment';
|
||||||
|
|
||||||
describe('validateEnvironment', () => {
|
describe('validateEnvironment', () => {
|
||||||
it('accepts a valid environment', () => {
|
const validEnvironment = (
|
||||||
const result = validateEnvironment({
|
overrides: Record<string, unknown> = {},
|
||||||
|
): Record<string, unknown> => ({
|
||||||
NODE_ENV: 'test',
|
NODE_ENV: 'test',
|
||||||
PORT: '3007',
|
PORT: '3007',
|
||||||
DATABASE_URL: 'postgresql://velody:velody@localhost:5432/velody?schema=public',
|
DATABASE_URL:
|
||||||
|
'postgresql://velody:velody@localhost:5432/velody?schema=public',
|
||||||
STORAGE_ROOT: '/tmp/velody',
|
STORAGE_ROOT: '/tmp/velody',
|
||||||
PUBLIC_BASE_URL: 'http://localhost:3007',
|
PUBLIC_BASE_URL: 'http://localhost:3007',
|
||||||
DEVICE_BOOTSTRAP_SECRET: 'device-secret',
|
DEVICE_BOOTSTRAP_SECRET: 'device-secret',
|
||||||
ACCOUNT_ACCESS_TOKEN_SECRET: 'account-secret',
|
ACCOUNT_ACCESS_TOKEN_SECRET: 'account-secret',
|
||||||
ACCOUNT_ACCESS_TOKEN_TTL_SECONDS: '900',
|
ACCOUNT_ACCESS_TOKEN_TTL_SECONDS: '900',
|
||||||
MAX_UPLOAD_SIZE_BYTES: '1024',
|
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(validEnvironment());
|
||||||
|
|
||||||
expect(result.PORT).toBe(3007);
|
expect(result.PORT).toBe(3007);
|
||||||
expect(result.ACCOUNT_ACCESS_TOKEN_TTL_SECONDS).toBe(900);
|
expect(result.ACCOUNT_ACCESS_TOKEN_TTL_SECONDS).toBe(900);
|
||||||
expect(result.MAX_UPLOAD_SIZE_BYTES).toBe(1024);
|
expect(result.MAX_UPLOAD_SIZE_BYTES).toBe(1024);
|
||||||
@ -29,18 +40,50 @@ describe('validateEnvironment', () => {
|
|||||||
|
|
||||||
it('rejects reuse of the device-token secret for account tokens', () => {
|
it('rejects reuse of the device-token secret for account tokens', () => {
|
||||||
expect(() =>
|
expect(() =>
|
||||||
validateEnvironment({
|
validateEnvironment(
|
||||||
NODE_ENV: 'test',
|
validEnvironment({
|
||||||
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',
|
DEVICE_BOOTSTRAP_SECRET: 'shared-secret',
|
||||||
ACCOUNT_ACCESS_TOKEN_SECRET: 'shared-secret',
|
ACCOUNT_ACCESS_TOKEN_SECRET: 'shared-secret',
|
||||||
ACCOUNT_ACCESS_TOKEN_TTL_SECONDS: '900',
|
|
||||||
MAX_UPLOAD_SIZE_BYTES: '1024',
|
|
||||||
}),
|
}),
|
||||||
|
),
|
||||||
).toThrow(/account and device token secrets must be different/);
|
).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 { plainToInstance } from 'class-transformer';
|
||||||
import {
|
import {
|
||||||
|
IsIn,
|
||||||
IsInt,
|
IsInt,
|
||||||
IsNotEmpty,
|
IsNotEmpty,
|
||||||
IsString,
|
IsString,
|
||||||
@ -9,6 +10,8 @@ import {
|
|||||||
validateSync,
|
validateSync,
|
||||||
} from 'class-validator';
|
} from 'class-validator';
|
||||||
|
|
||||||
|
export type PaddleEnvironment = 'sandbox' | 'production';
|
||||||
|
|
||||||
class EnvironmentVariables {
|
class EnvironmentVariables {
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
@ -48,6 +51,25 @@ class EnvironmentVariables {
|
|||||||
@IsInt()
|
@IsInt()
|
||||||
@Min(1)
|
@Min(1)
|
||||||
MAX_UPLOAD_SIZE_BYTES!: number;
|
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>) {
|
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.ACCOUNT_ACCESS_TOKEN_TTL_SECONDS ?? '900';
|
||||||
process.env.MAX_UPLOAD_SIZE_BYTES =
|
process.env.MAX_UPLOAD_SIZE_BYTES =
|
||||||
process.env.MAX_UPLOAD_SIZE_BYTES ?? '1073741824';
|
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_SECRET=replace-with-a-distinct-secret
|
||||||
ACCOUNT_ACCESS_TOKEN_TTL_SECONDS=900
|
ACCOUNT_ACCESS_TOKEN_TTL_SECONDS=900
|
||||||
MAX_UPLOAD_SIZE_BYTES=524288000
|
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