Implement backend API and database services in Docker setup
- Added a new `api` service for the NestJS backend, including health checks and dependencies on PostgreSQL, Redis, and MinIO. - Introduced PostgreSQL and Redis services with health checks and configurations for data persistence. - Added MinIO for S3-compatible object storage and a one-shot service to initialize required buckets. - Updated the Nginx configuration to proxy requests to the new backend API and MinIO storage. - Enhanced the Dockerfile to support the new API environment variables and configurations. - Updated the `package.json` and `package-lock.json` to include new dependencies for QR code generation and other utilities. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
29
backend/test/app.e2e-spec.ts
Normal file
29
backend/test/app.e2e-spec.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
// eslint-disable-next-line unicorn/prevent-abbreviations
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { INestApplication } from '@nestjs/common';
|
||||
import * as request from 'supertest';
|
||||
import { AppController } from './../src/app.controller';
|
||||
import { AppService } from './../src/app.service';
|
||||
|
||||
describe('AppController (e2e)', () => {
|
||||
let app: INestApplication;
|
||||
|
||||
beforeEach(async () => {
|
||||
const moduleFixture: TestingModule = await Test.createTestingModule({
|
||||
controllers: [AppController],
|
||||
providers: [AppService],
|
||||
}).compile();
|
||||
|
||||
app = moduleFixture.createNestApplication();
|
||||
await app.init();
|
||||
});
|
||||
|
||||
it('/ (GET)', () => {
|
||||
return request(app.getHttpServer())
|
||||
.get('/')
|
||||
.expect(200)
|
||||
.expect((response) =>
|
||||
expect(response.text).toMatch(/App has been running for/),
|
||||
);
|
||||
});
|
||||
});
|
||||
13
backend/test/jest-e2e.json
Normal file
13
backend/test/jest-e2e.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"moduleFileExtensions": ["js", "json", "ts"],
|
||||
"rootDir": "..",
|
||||
"testEnvironment": "node",
|
||||
"testRegex": ".e2e-spec.ts$",
|
||||
"moduleNameMapper": {
|
||||
"^src/(.*)$": "<rootDir>/src/$1"
|
||||
},
|
||||
"transform": {
|
||||
"^.+\\.(t|j)s$": "ts-jest"
|
||||
},
|
||||
"setupFilesAfterEnv": ["<rootDir>/test/setup.ts"]
|
||||
}
|
||||
54
backend/test/mocks/subscription.ts
Normal file
54
backend/test/mocks/subscription.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { Subscription } from 'src/subscriptions/entities/subscription.entity';
|
||||
import { SubscriptionPeriod } from 'src/subscriptions/enums/periods.enum';
|
||||
import { PaymentStatus } from 'src/subscriptions/enums/status.enum';
|
||||
import { SubscriptionType } from 'src/subscriptions/enums/types.enum';
|
||||
import { createUser } from './user';
|
||||
import { User } from 'src/users/entities/user.entity';
|
||||
|
||||
type SubscriptionParameters = {
|
||||
type?: SubscriptionType;
|
||||
period?: SubscriptionPeriod;
|
||||
status?: PaymentStatus;
|
||||
createdAt?: Date;
|
||||
user?: User;
|
||||
};
|
||||
|
||||
export const createSubscription = ({
|
||||
type = 'enthusiast',
|
||||
period = 'monthly',
|
||||
status = 'succeeded',
|
||||
createdAt = new Date(),
|
||||
user,
|
||||
}: SubscriptionParameters): Subscription => ({
|
||||
id: 'ab2b9b1b0-0b1b-4b1b-8b1b-2b1b3b1b4b1b5',
|
||||
stripeId: 'sub_123',
|
||||
status,
|
||||
type,
|
||||
period,
|
||||
userId: 'a2b9b1b0-0b1b-4b1b-8b1b-2b1b3b1b4b1b5',
|
||||
user,
|
||||
createdAt,
|
||||
flashEvents: [],
|
||||
});
|
||||
|
||||
export const audienceMonthlyStripe = createSubscription({
|
||||
type: 'enthusiast',
|
||||
period: 'monthly',
|
||||
user: createUser(),
|
||||
});
|
||||
|
||||
export const rssAddonMonthlyStripe = createSubscription({
|
||||
type: 'rss-addon',
|
||||
period: 'monthly',
|
||||
});
|
||||
|
||||
export const verificationAddonMonthlyStripe = createSubscription({
|
||||
type: 'verification-addon',
|
||||
period: 'monthly',
|
||||
});
|
||||
|
||||
export const audienceMonthlyRejectedStripe = createSubscription({
|
||||
type: 'enthusiast',
|
||||
period: 'monthly',
|
||||
status: 'rejected',
|
||||
});
|
||||
82
backend/test/mocks/user.ts
Normal file
82
backend/test/mocks/user.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import { User } from 'src/users/entities/user.entity';
|
||||
import {
|
||||
audienceMonthlyStripe,
|
||||
rssAddonMonthlyStripe,
|
||||
verificationAddonMonthlyStripe,
|
||||
} from './subscription';
|
||||
import { Subscription } from 'src/subscriptions/entities/subscription.entity';
|
||||
import { Filmmaker } from 'src/filmmakers/entities/filmmaker.entity';
|
||||
|
||||
interface UserParameters {
|
||||
id?: string;
|
||||
email?: string;
|
||||
legalName?: string;
|
||||
profilePictureUrl?: string;
|
||||
subscriptions?: Subscription[];
|
||||
filmmaker?: Filmmaker;
|
||||
}
|
||||
export const createUser = ({
|
||||
id = 'a2b9b1b0-0b1b-4b1b-8b1b-2b1b3b1b4b1b5',
|
||||
email = 'test@oneseventech.com',
|
||||
legalName = 'Test User',
|
||||
profilePictureUrl = 'https://example.com/profile.jpg',
|
||||
subscriptions = [],
|
||||
filmmaker,
|
||||
}: UserParameters = {}): User => {
|
||||
return {
|
||||
cognitoId: 'test-cognito-id',
|
||||
id,
|
||||
email,
|
||||
legalName,
|
||||
profilePictureUrl,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
subscriptions,
|
||||
filmmaker,
|
||||
seasonRents: [],
|
||||
redemptions: [],
|
||||
};
|
||||
};
|
||||
|
||||
const createFilmmaker = (
|
||||
id: string,
|
||||
userId: string,
|
||||
professionalName: string,
|
||||
bio: string,
|
||||
): any => {
|
||||
return {
|
||||
id,
|
||||
userId,
|
||||
professionalName,
|
||||
lightningAddress: '',
|
||||
bio,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
};
|
||||
|
||||
export const audienceUserStripe: User = createUser({
|
||||
subscriptions: [audienceMonthlyStripe],
|
||||
});
|
||||
|
||||
export const proPlusUserStripe: User = createUser({
|
||||
subscriptions: [rssAddonMonthlyStripe],
|
||||
filmmaker: createFilmmaker(
|
||||
'a2b9b1b0-0b1b-4b1b-8b1b-2b1b3b1b4b1b5',
|
||||
'a2b9b1b0-0b1b-4b1b-8b1b-2b1b3b1b4b1b5',
|
||||
'Test Filmmaker',
|
||||
'Test bio',
|
||||
),
|
||||
});
|
||||
|
||||
export const ultimateUserStripe: User = createUser({
|
||||
subscriptions: [verificationAddonMonthlyStripe],
|
||||
filmmaker: createFilmmaker(
|
||||
'a2b9b1b0-0b1b-4b1b-8b1b-2b1b3b1b4b1b5',
|
||||
'a2b9b1b0-0b1b-4b1b-8b1b-2b1b3b1b4b1b5',
|
||||
'Test Filmmaker',
|
||||
'Test bio',
|
||||
),
|
||||
});
|
||||
|
||||
export const audienceUserNoSubscription: User = createUser();
|
||||
97
backend/test/nostr-auth.e2e-spec.ts
Normal file
97
backend/test/nostr-auth.e2e-spec.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
/* eslint-disable unicorn/prevent-abbreviations */
|
||||
import { INestApplication } from '@nestjs/common';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import * as request from 'supertest';
|
||||
import { createHash } from 'node:crypto';
|
||||
import {
|
||||
finalizeEvent,
|
||||
generateSecretKey,
|
||||
getPublicKey,
|
||||
type UnsignedEvent,
|
||||
} from 'nostr-tools';
|
||||
import { NostrAuthModule } from '../src/nostr-auth/nostr-auth.module';
|
||||
|
||||
const hashPayload = (payload: string) =>
|
||||
createHash('sha256').update(payload).digest('hex');
|
||||
|
||||
describe('NostrAuth (e2e)', () => {
|
||||
let app: INestApplication;
|
||||
|
||||
const secretKey = generateSecretKey();
|
||||
const pubkey = getPublicKey(secretKey);
|
||||
const host = 'nostr.test';
|
||||
const path = '/nostr-auth/echo';
|
||||
const url = `http://${host}${path}`;
|
||||
|
||||
const buildAuthHeader = (unsignedEvent: UnsignedEvent): string => {
|
||||
const event = finalizeEvent(unsignedEvent, secretKey);
|
||||
return `Nostr ${Buffer.from(JSON.stringify(event)).toString('base64')}`;
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
const moduleFixture: TestingModule = await Test.createTestingModule({
|
||||
imports: [NostrAuthModule],
|
||||
}).compile();
|
||||
|
||||
app = moduleFixture.createNestApplication();
|
||||
await app.init();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await app.close();
|
||||
});
|
||||
|
||||
it('accepts a valid nostr-signed request', async () => {
|
||||
const body = { ping: 'pong' };
|
||||
const payload = JSON.stringify(body);
|
||||
|
||||
const authHeader = buildAuthHeader({
|
||||
pubkey,
|
||||
kind: 27_235,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
tags: [
|
||||
['u', url],
|
||||
['method', 'POST'],
|
||||
['payload', hashPayload(payload)],
|
||||
],
|
||||
content: '',
|
||||
});
|
||||
|
||||
const response = await request(app.getHttpServer())
|
||||
.post(path)
|
||||
.set('host', host)
|
||||
.set('x-forwarded-proto', 'http')
|
||||
.set('authorization', authHeader)
|
||||
.send(body)
|
||||
.expect(201);
|
||||
|
||||
expect(response.body.pubkey).toBe(pubkey);
|
||||
});
|
||||
|
||||
it('rejects a tampered payload hash', async () => {
|
||||
const body = { ping: 'pong' };
|
||||
const payload = JSON.stringify(body);
|
||||
|
||||
const authHeader = buildAuthHeader({
|
||||
pubkey,
|
||||
kind: 27_235,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
tags: [
|
||||
['u', url],
|
||||
['method', 'POST'],
|
||||
['payload', hashPayload(`${payload}tampered`)],
|
||||
],
|
||||
content: '',
|
||||
});
|
||||
|
||||
const response = await request(app.getHttpServer())
|
||||
.post(path)
|
||||
.set('host', host)
|
||||
.set('x-forwarded-proto', 'http')
|
||||
.set('authorization', authHeader)
|
||||
.send(body)
|
||||
.expect(401);
|
||||
|
||||
expect(response.body.code).toBe('PAYLOAD_MISMATCH');
|
||||
});
|
||||
});
|
||||
108
backend/test/nostr-session.e2e-spec.ts
Normal file
108
backend/test/nostr-session.e2e-spec.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
/* eslint-disable unicorn/no-useless-undefined */
|
||||
/* eslint-disable unicorn/prevent-abbreviations */
|
||||
import { INestApplication } from '@nestjs/common';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import * as request from 'supertest';
|
||||
import { createHash } from 'node:crypto';
|
||||
import {
|
||||
finalizeEvent,
|
||||
generateSecretKey,
|
||||
getPublicKey,
|
||||
type UnsignedEvent,
|
||||
} from 'nostr-tools';
|
||||
import { AuthController } from '../src/auth/auth.controller';
|
||||
import { AuthService } from '../src/auth/auth.service';
|
||||
import { UsersService } from '../src/users/users.service';
|
||||
import { FilmmakersService } from '../src/filmmakers/filmmakers.service';
|
||||
import { NostrAuthGuard } from '../src/nostr-auth/nostr-auth.guard';
|
||||
import { NostrAuthService } from '../src/nostr-auth/nostr-auth.service';
|
||||
import { NostrSessionService } from '../src/auth/nostr-session.service';
|
||||
import { NostrSessionJwtGuard } from '../src/auth/guards/nostr-session-jwt.guard';
|
||||
|
||||
const hashPayload = (payload: string) =>
|
||||
createHash('sha256').update(payload).digest('hex');
|
||||
|
||||
describe('Nostr session bridge (e2e)', () => {
|
||||
let app: INestApplication;
|
||||
|
||||
const secretKey = generateSecretKey();
|
||||
const pubkey = getPublicKey(secretKey);
|
||||
const host = 'nostr.test';
|
||||
const path = '/auth/nostr/session';
|
||||
const url = `http://${host}${path}`;
|
||||
|
||||
const buildAuthHeader = (unsignedEvent: UnsignedEvent): string => {
|
||||
const event = finalizeEvent(unsignedEvent, secretKey);
|
||||
return `Nostr ${Buffer.from(JSON.stringify(event)).toString('base64')}`;
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
const moduleFixture: TestingModule = await Test.createTestingModule({
|
||||
controllers: [AuthController],
|
||||
providers: [
|
||||
NostrSessionService,
|
||||
NostrAuthService,
|
||||
NostrAuthGuard,
|
||||
NostrSessionJwtGuard,
|
||||
{ provide: AuthService, useValue: {} },
|
||||
{
|
||||
provide: UsersService,
|
||||
useValue: {
|
||||
findUserByNostrPubkey: jest.fn().mockResolvedValue(undefined),
|
||||
findUsersById: jest.fn().mockResolvedValue(undefined),
|
||||
},
|
||||
},
|
||||
{ provide: FilmmakersService, useValue: {} },
|
||||
],
|
||||
}).compile();
|
||||
|
||||
app = moduleFixture.createNestApplication();
|
||||
await app.init();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await app.close();
|
||||
});
|
||||
|
||||
it('issues, refreshes, and accepts nostr session JWTs', async () => {
|
||||
const body = { ping: 'pong' };
|
||||
const payload = JSON.stringify(body);
|
||||
|
||||
const authHeader = buildAuthHeader({
|
||||
pubkey,
|
||||
kind: 27_235,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
tags: [
|
||||
['u', url],
|
||||
['method', 'POST'],
|
||||
['payload', hashPayload(payload)],
|
||||
],
|
||||
content: '',
|
||||
});
|
||||
|
||||
const createResponse = await request(app.getHttpServer())
|
||||
.post(path)
|
||||
.set('host', host)
|
||||
.set('x-forwarded-proto', 'http')
|
||||
.set('authorization', authHeader)
|
||||
.send(body)
|
||||
.expect(201);
|
||||
|
||||
const { accessToken, refreshToken } = createResponse.body;
|
||||
expect(accessToken).toBeDefined();
|
||||
expect(refreshToken).toBeDefined();
|
||||
|
||||
const refreshResponse = await request(app.getHttpServer())
|
||||
.post('/auth/nostr/refresh')
|
||||
.send({ refreshToken })
|
||||
.expect(201);
|
||||
|
||||
expect(refreshResponse.body.accessToken).toBeDefined();
|
||||
|
||||
await request(app.getHttpServer())
|
||||
.post('/auth/nostr/logout')
|
||||
.set('authorization', `Bearer ${accessToken}`)
|
||||
.expect(201)
|
||||
.expect((res) => expect(res.body.success).toBe(true));
|
||||
});
|
||||
});
|
||||
65
backend/test/setup.ts
Normal file
65
backend/test/setup.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
process.env.ENVIRONMENT = 'development';
|
||||
process.env.PORT = '4000';
|
||||
process.env.DOMAIN = 'localhost:4000';
|
||||
|
||||
process.env.DATABASE_HOST = 'rds.com';
|
||||
process.env.DATABASE_PORT = '5432';
|
||||
process.env.DATABASE_USER = 'postgres';
|
||||
process.env.DATABASE_PASSWORD = 'password';
|
||||
process.env.DATABASE_NAME = 'indeehub';
|
||||
|
||||
process.env.ZBD_API_KEY = 'api-key';
|
||||
process.env.STRIKE_API_KEY = 'api-key';
|
||||
process.env.STRIKE_WEBHOOK_SECRET = 'api-key';
|
||||
|
||||
process.env.COGNITO_USER_POOL_ID = 'pool-id';
|
||||
process.env.COGNITO_CLIENT_ID = 'client-id';
|
||||
|
||||
process.env.SENDGRID_API_KEY = 'sendgrid-api-key';
|
||||
process.env.SENDGRID_SENDER = 'hello@indeehub.studio';
|
||||
process.env.SENDGRID_WAITLIST = 'id';
|
||||
|
||||
process.env.AWS_ACCESS_KEY = 'access-key';
|
||||
process.env.AWS_SECRET_KEY = 'secret-key';
|
||||
process.env.AWS_REGION = 'us-west-1';
|
||||
|
||||
process.env.S3_PUBLIC_BUCKET_URL = 'https://public.cloudfront.net/';
|
||||
process.env.S3_PUBLIC_BUCKET_NAME = 'public';
|
||||
|
||||
process.env.S3_PRIVATE_BUCKET_URL = 'https://private.cloudfront.net/';
|
||||
process.env.S3_PRIVATE_BUCKET_NAME = 'private';
|
||||
|
||||
process.env.CLOUDFRONT_PRIVATE_KEY = 'key';
|
||||
process.env.CLOUDFRONT_KEY_PAIR_ID = 'key-pair-id';
|
||||
|
||||
process.env.STRIPE_SECRET_KEY = 'sk_test_key';
|
||||
|
||||
process.env.STRIPE_AUDIENCE_PRODUCT_ID = 'prod_id_audience';
|
||||
process.env.STRIPE_AUDIENCE_MONTHLY_PRICE_ID = 'price_id_monthly';
|
||||
process.env.STRIPE_AUDIENCE_YEARLY_PRICE_ID = 'price_id_yearly';
|
||||
process.env.STRIPE_PRO_PLUS_PRODUCT_ID = 'prod_id_pro_plus';
|
||||
process.env.STRIPE_PRO_PLUS_MONTHLY_PRICE_ID = 'price_id_monthly';
|
||||
process.env.STRIPE_PRO_PLUS_YEARLY_PRICE_ID = 'price_id_yearly';
|
||||
process.env.STRIPE_ULTIMATE_PRODUCT_ID = 'prod_id_ultimate';
|
||||
process.env.STRIPE_ULTIMATE_MONTHLY_PRICE_ID = 'price_id_monthly';
|
||||
process.env.STRIPE_ULTIMATE_YEARLY_PRICE_ID = 'price_id_yearly';
|
||||
|
||||
process.env.STRIPE_WEBHOOK_KEY = 'whsec_key';
|
||||
|
||||
process.env.FLASH_JWT_SECRET_AUDIENCE = 'flash_audience_secret';
|
||||
process.env.FLASH_JWT_SECRET_PRO_PLUS = 'flash_pro_plus_secret';
|
||||
process.env.FLASH_JWT_SECRET_ULTIMATE = 'flash_ultimate_secret';
|
||||
|
||||
process.env.FRONTEND_URL = 'http://localhost:3000';
|
||||
|
||||
process.env.TRANSCODING_API_KEY = 'api-key';
|
||||
process.env.NOSTR_JWT_SECRET = 'nostr-jwt-secret';
|
||||
process.env.NOSTR_JWT_REFRESH_SECRET = 'nostr-jwt-refresh-secret';
|
||||
|
||||
process.env.QUEUE_HOST = 'redis.com';
|
||||
process.env.QUEUE_PORT = '6379';
|
||||
process.env.QUEUE_PASSWORD = 'password';
|
||||
|
||||
process.env.PODPING_URL = 'https://podping.cloud/';
|
||||
process.env.PODPING_KEY = 'podping-key';
|
||||
process.env.PODPING_USER_AGENT = 'Indeehub';
|
||||
Reference in New Issue
Block a user