diff --git a/infrastructure/stack.ts b/infrastructure/stack.ts index ae6c1f0..0e768bb 100644 --- a/infrastructure/stack.ts +++ b/infrastructure/stack.ts @@ -1,12 +1,12 @@ import * as cdk from 'aws-cdk-lib'; +import * as acm from 'aws-cdk-lib/aws-certificatemanager'; import * as ec2 from 'aws-cdk-lib/aws-ec2'; +import * as ecr from 'aws-cdk-lib/aws-ecr'; import * as ecs from 'aws-cdk-lib/aws-ecs'; import * as ecsPatterns from 'aws-cdk-lib/aws-ecs-patterns'; -import * as rds from 'aws-cdk-lib/aws-rds'; -import * as ecr from 'aws-cdk-lib/aws-ecr'; -import * as logs from 'aws-cdk-lib/aws-logs'; import * as elbv2 from 'aws-cdk-lib/aws-elasticloadbalancingv2'; -import * as acm from 'aws-cdk-lib/aws-certificatemanager'; +import * as logs from 'aws-cdk-lib/aws-logs'; +import * as rds from 'aws-cdk-lib/aws-rds'; import * as route53 from 'aws-cdk-lib/aws-route53'; import { Construct } from 'constructs'; import * as dotenv from 'dotenv'; @@ -29,7 +29,7 @@ export class BackendStack extends cdk.Stack { dotenv.config({ path: '.env' }); // VPC - either use existing or create new - this.vpc = props?.vpcId + this.vpc = props?.vpcId ? ec2.Vpc.fromLookup(this, 'ExistingVpc', { vpcId: props.vpcId }) : new ec2.Vpc(this, 'SyncrowVpc', { maxAzs: 2, @@ -49,11 +49,15 @@ export class BackendStack extends cdk.Stack { }); // Security Groups - const dbSecurityGroup = new ec2.SecurityGroup(this, 'DatabaseSecurityGroup', { - vpc: this.vpc, - description: 'Security group for RDS PostgreSQL', - allowAllOutbound: false, - }); + const dbSecurityGroup = new ec2.SecurityGroup( + this, + 'DatabaseSecurityGroup', + { + vpc: this.vpc, + description: 'Security group for RDS PostgreSQL', + allowAllOutbound: false, + }, + ); const ecsSecurityGroup = new ec2.SecurityGroup(this, 'EcsSecurityGroup', { vpc: this.vpc, @@ -71,55 +75,70 @@ export class BackendStack extends cdk.Stack { ecsSecurityGroup.addIngressRule( albSecurityGroup, ec2.Port.tcp(3000), - 'Allow ALB to connect to ECS service' + 'Allow ALB to connect to ECS service', ); // Allow ECS to connect to RDS dbSecurityGroup.addIngressRule( ecsSecurityGroup, ec2.Port.tcp(5432), - 'Allow ECS to connect to PostgreSQL' + 'Allow ECS to connect to PostgreSQL', ); // Temporary access for admin IP dbSecurityGroup.addIngressRule( ec2.Peer.ipv4('216.126.231.231/32'), ec2.Port.tcp(5432), - 'Temporary access from admin IP' + 'Temporary access from admin IP', ); // Allow HTTP/HTTPS traffic to ALB albSecurityGroup.addIngressRule( ec2.Peer.anyIpv4(), ec2.Port.tcp(80), - 'Allow HTTP traffic' + 'Allow HTTP traffic', ); albSecurityGroup.addIngressRule( ec2.Peer.anyIpv4(), ec2.Port.tcp(443), - 'Allow HTTPS traffic' + 'Allow HTTPS traffic', ); - const dbCluster = rds.DatabaseCluster.fromDatabaseClusterAttributes(this, 'SyncrowDatabase', { - clusterIdentifier: 'syncrow-backend', - instanceIdentifiers: ['syncrowdatabase-instance-1'], - engine: rds.DatabaseClusterEngine.auroraPostgres({ - version: rds.AuroraPostgresEngineVersion.VER_16_6, - }), - port: 5432, - securityGroups: [ - ec2.SecurityGroup.fromSecurityGroupId(this, 'ImportedDbSecurityGroup', 'sg-07e163f588b2bac25') - ], - clusterEndpointAddress: 'syncrow-backend.cluster-criskv1sdkq4.me-central-1.rds.amazonaws.com', - }); + const dbCluster = rds.DatabaseCluster.fromDatabaseClusterAttributes( + this, + 'SyncrowDatabase', + { + clusterIdentifier: 'syncrow-backend', + instanceIdentifiers: ['syncrowdatabase-instance-1'], + engine: rds.DatabaseClusterEngine.auroraPostgres({ + version: rds.AuroraPostgresEngineVersion.VER_16_6, + }), + port: 5432, + securityGroups: [ + ec2.SecurityGroup.fromSecurityGroupId( + this, + 'ImportedDbSecurityGroup', + 'sg-07e163f588b2bac25', + ), + ], + clusterEndpointAddress: + 'syncrow-backend.cluster-criskv1sdkq4.me-central-1.rds.amazonaws.com', + }, + ); // Import the existing database secret separately - const dbSecret = rds.DatabaseSecret.fromSecretCompleteArn(this, 'ImportedDbSecret', - 'arn:aws:secretsmanager:me-central-1:482311766496:secret:rds!cluster-43ec14cd-9301-43e2-aa79-d330a429a126-v0JDQN' + const dbSecret = rds.DatabaseSecret.fromSecretCompleteArn( + this, + 'ImportedDbSecret', + 'arn:aws:secretsmanager:me-central-1:482311766496:secret:rds!cluster-43ec14cd-9301-43e2-aa79-d330a429a126-v0JDQN', ); // ECR Repository for Docker images - import existing repository - const ecrRepository = ecr.Repository.fromRepositoryName(this, 'SyncrowBackendRepo', 'syncrow-backend'); + const ecrRepository = ecr.Repository.fromRepositoryName( + this, + 'SyncrowBackendRepo', + 'syncrow-backend', + ); // Output the correct ECR URI for this region new cdk.CfnOutput(this, 'EcrRepositoryUriRegional', { @@ -142,129 +161,166 @@ export class BackendStack extends cdk.Stack { }); // Use existing wildcard certificate or create new one - const apiCertificate = props?.certificateArn - ? acm.Certificate.fromCertificateArn(this, 'ApiCertificate', props.certificateArn) + const apiCertificate = props?.certificateArn + ? acm.Certificate.fromCertificateArn( + this, + 'ApiCertificate', + props.certificateArn, + ) : new acm.Certificate(this, 'ApiCertificate', { domainName: 'api.syncrow.me', validation: acm.CertificateValidation.fromDns(), }); // ECS Fargate Service with Application Load Balancer - const fargateService = new ecsPatterns.ApplicationLoadBalancedFargateService(this, 'SyncrowBackendService', { - cluster, - memoryLimitMiB: 1024, - cpu: 512, - desiredCount: 1, - domainName: 'api.syncrow.me', - domainZone: route53.HostedZone.fromLookup(this, 'SyncrowZone', { - domainName: 'syncrow.me', - }), - certificate: apiCertificate, - protocol: elbv2.ApplicationProtocol.HTTPS, - redirectHTTP: true, - taskImageOptions: { - image: ecs.ContainerImage.fromEcrRepository(ecrRepository, 'latest'), - containerPort: 3000, - enableLogging: true, - environment: { - // App settings - NODE_ENV: process.env.NODE_ENV || 'production', - PORT: process.env.PORT || '3000', - BASE_URL: process.env.BASE_URL || '', - - // Database connection (CDK provides these automatically) - AZURE_POSTGRESQL_HOST: dbCluster.clusterEndpoint.hostname, - AZURE_POSTGRESQL_PORT: '5432', - AZURE_POSTGRESQL_DATABASE: props?.databaseName || 'syncrow', - AZURE_POSTGRESQL_USER: 'postgres', - AZURE_POSTGRESQL_SSL: process.env.AZURE_POSTGRESQL_SSL || 'false', - AZURE_POSTGRESQL_SYNC: process.env.AZURE_POSTGRESQL_SYNC || 'false', - - // JWT Configuration - CRITICAL: These must be set - JWT_SECRET: process.env.JWT_SECRET || 'syncrow-jwt-secret-key-2025-production-environment-very-secure-random-string', - JWT_SECRET_REFRESH: process.env.JWT_SECRET_REFRESH || 'syncrow-refresh-secret-key-2025-production-environment-different-secure-string', - JWT_EXPIRE_TIME: process.env.JWT_EXPIRE_TIME || '1h', - JWT_EXPIRE_TIME_REFRESH: process.env.JWT_EXPIRE_TIME_REFRESH || '7d', - - // Firebase Configuration - FIREBASE_API_KEY: process.env.FIREBASE_API_KEY || '', - FIREBASE_AUTH_DOMAIN: process.env.FIREBASE_AUTH_DOMAIN || '', - FIREBASE_PROJECT_ID: process.env.FIREBASE_PROJECT_ID || '', - FIREBASE_STORAGE_BUCKET: process.env.FIREBASE_STORAGE_BUCKET || '', - FIREBASE_MESSAGING_SENDER_ID: process.env.FIREBASE_MESSAGING_SENDER_ID || '', - FIREBASE_APP_ID: process.env.FIREBASE_APP_ID || '', - FIREBASE_MEASUREMENT_ID: process.env.FIREBASE_MEASUREMENT_ID || '', - FIREBASE_DATABASE_URL: process.env.FIREBASE_DATABASE_URL || '', - - // Tuya IoT Configuration - TUYA_EU_URL: process.env.TUYA_EU_URL || 'https://openapi.tuyaeu.com', - TUYA_ACCESS_ID: process.env.TUYA_ACCESS_ID || '', - TUYA_ACCESS_KEY: process.env.TUYA_ACCESS_KEY || '', - TRUN_ON_TUYA_SOCKET: process.env.TRUN_ON_TUYA_SOCKET || '', - - // Email Configuration - SMTP_HOST: process.env.SMTP_HOST || '', - SMTP_PORT: process.env.SMTP_PORT || '587', - SMTP_SECURE: process.env.SMTP_SECURE || 'true', - SMTP_USER: process.env.SMTP_USER || '', - SMTP_PASSWORD: process.env.SMTP_PASSWORD || '', - - // Mailtrap Configuration - MAILTRAP_API_TOKEN: process.env.MAILTRAP_API_TOKEN || '', - MAILTRAP_INVITATION_TEMPLATE_UUID: process.env.MAILTRAP_INVITATION_TEMPLATE_UUID || '', - MAILTRAP_EDIT_USER_TEMPLATE_UUID: process.env.MAILTRAP_EDIT_USER_TEMPLATE_UUID || '', - MAILTRAP_DISABLE_TEMPLATE_UUID: process.env.MAILTRAP_DISABLE_TEMPLATE_UUID || '', - MAILTRAP_ENABLE_TEMPLATE_UUID: process.env.MAILTRAP_ENABLE_TEMPLATE_UUID || '', - MAILTRAP_DELETE_USER_TEMPLATE_UUID: process.env.MAILTRAP_DELETE_USER_TEMPLATE_UUID || '', - - // OneSignal Push Notifications - ONESIGNAL_APP_ID: process.env.ONESIGNAL_APP_ID || '', - ONESIGNAL_API_KEY: process.env.ONESIGNAL_API_KEY || '', - - // Admin Configuration - SUPER_ADMIN_EMAIL: process.env.SUPER_ADMIN_EMAIL || 'admin@yourdomain.com', - SUPER_ADMIN_PASSWORD: process.env.SUPER_ADMIN_PASSWORD || 'YourSecureAdminPassword123!', - - // Google OAuth - GOOGLE_CLIENT_ID: process.env.GOOGLE_CLIENT_ID || '', - GOOGLE_CLIENT_SECRET: process.env.GOOGLE_CLIENT_SECRET || '', - - // Other Configuration - OTP_LIMITER: process.env.OTP_LIMITER || '5', - SECRET_KEY: process.env.SECRET_KEY || 'another-random-secret-key-for-general-encryption', - ACCESS_KEY: process.env.ACCESS_KEY || '', - DB_SYNC: process.env.DB_SYNC || 'txsrue', - - // Redis (used?) - AZURE_REDIS_CONNECTIONSTRING: process.env.AZURE_REDIS_CONNECTIONSTRING || '', - - // Docker Registry (for deployment) - DOCKER_REGISTRY_SERVER_URL: process.env.DOCKER_REGISTRY_SERVER_URL || '', - DOCKER_REGISTRY_SERVER_USERNAME: process.env.DOCKER_REGISTRY_SERVER_USERNAME || '', - DOCKER_REGISTRY_SERVER_PASSWORD: process.env.DOCKER_REGISTRY_SERVER_PASSWORD || '', - - // Doppler (if used for secrets management) - DOPPLER_PROJECT: process.env.DOPPLER_PROJECT || '', - DOPPLER_CONFIG: process.env.DOPPLER_CONFIG || '', - DOPPLER_ENVIRONMENT: process.env.DOPPLER_ENVIRONMENT || '', - - // Azure specific - WEBSITES_ENABLE_APP_SERVICE_STORAGE: process.env.WEBSITES_ENABLE_APP_SERVICE_STORAGE || 'false', + const fargateService = + new ecsPatterns.ApplicationLoadBalancedFargateService( + this, + 'SyncrowBackendService', + { + cluster, + memoryLimitMiB: 1024, + cpu: 512, + desiredCount: 1, + domainName: 'api.syncrow.me', + domainZone: route53.HostedZone.fromLookup(this, 'SyncrowZone', { + domainName: 'syncrow.me', + }), + certificate: apiCertificate, + protocol: elbv2.ApplicationProtocol.HTTPS, + redirectHTTP: true, + taskImageOptions: { + image: ecs.ContainerImage.fromEcrRepository( + ecrRepository, + 'latest', + ), + containerPort: 3000, + enableLogging: true, + environment: { + // App settings + NODE_ENV: process.env.NODE_ENV || 'production', + PORT: process.env.PORT || '3000', + BASE_URL: process.env.BASE_URL || '', + + // Database connection (CDK provides these automatically) + AZURE_POSTGRESQL_HOST: dbCluster.clusterEndpoint.hostname, + AZURE_POSTGRESQL_PORT: '5432', + AZURE_POSTGRESQL_DATABASE: props?.databaseName || 'postgres', + AZURE_POSTGRESQL_USER: 'postgres', + AZURE_POSTGRESQL_SSL: process.env.AZURE_POSTGRESQL_SSL || 'false', + AZURE_POSTGRESQL_SYNC: + process.env.AZURE_POSTGRESQL_SYNC || 'false', + + // JWT Configuration - CRITICAL: These must be set + JWT_SECRET: + process.env.JWT_SECRET || + 'syncrow-jwt-secret-key-2025-production-environment-very-secure-random-string', + JWT_SECRET_REFRESH: + process.env.JWT_SECRET_REFRESH || + 'syncrow-refresh-secret-key-2025-production-environment-different-secure-string', + JWT_EXPIRE_TIME: process.env.JWT_EXPIRE_TIME || '1h', + JWT_EXPIRE_TIME_REFRESH: + process.env.JWT_EXPIRE_TIME_REFRESH || '7d', + + // Firebase Configuration + FIREBASE_API_KEY: process.env.FIREBASE_API_KEY || '', + FIREBASE_AUTH_DOMAIN: process.env.FIREBASE_AUTH_DOMAIN || '', + FIREBASE_PROJECT_ID: process.env.FIREBASE_PROJECT_ID || '', + FIREBASE_STORAGE_BUCKET: + process.env.FIREBASE_STORAGE_BUCKET || '', + FIREBASE_MESSAGING_SENDER_ID: + process.env.FIREBASE_MESSAGING_SENDER_ID || '', + FIREBASE_APP_ID: process.env.FIREBASE_APP_ID || '', + FIREBASE_MEASUREMENT_ID: + process.env.FIREBASE_MEASUREMENT_ID || '', + FIREBASE_DATABASE_URL: process.env.FIREBASE_DATABASE_URL || '', + + // Tuya IoT Configuration + TUYA_EU_URL: + process.env.TUYA_EU_URL || 'https://openapi.tuyaeu.com', + TUYA_ACCESS_ID: process.env.TUYA_ACCESS_ID || '', + TUYA_ACCESS_KEY: process.env.TUYA_ACCESS_KEY || '', + TRUN_ON_TUYA_SOCKET: process.env.TRUN_ON_TUYA_SOCKET || '', + + // Email Configuration + SMTP_HOST: process.env.SMTP_HOST || '', + SMTP_PORT: process.env.SMTP_PORT || '587', + SMTP_SECURE: process.env.SMTP_SECURE || 'true', + SMTP_USER: process.env.SMTP_USER || '', + SMTP_PASSWORD: process.env.SMTP_PASSWORD || '', + + // Mailtrap Configuration + MAILTRAP_API_TOKEN: process.env.MAILTRAP_API_TOKEN || '', + MAILTRAP_INVITATION_TEMPLATE_UUID: + process.env.MAILTRAP_INVITATION_TEMPLATE_UUID || '', + MAILTRAP_EDIT_USER_TEMPLATE_UUID: + process.env.MAILTRAP_EDIT_USER_TEMPLATE_UUID || '', + MAILTRAP_DISABLE_TEMPLATE_UUID: + process.env.MAILTRAP_DISABLE_TEMPLATE_UUID || '', + MAILTRAP_ENABLE_TEMPLATE_UUID: + process.env.MAILTRAP_ENABLE_TEMPLATE_UUID || '', + MAILTRAP_DELETE_USER_TEMPLATE_UUID: + process.env.MAILTRAP_DELETE_USER_TEMPLATE_UUID || '', + + // OneSignal Push Notifications + ONESIGNAL_APP_ID: process.env.ONESIGNAL_APP_ID || '', + ONESIGNAL_API_KEY: process.env.ONESIGNAL_API_KEY || '', + + // Admin Configuration + SUPER_ADMIN_EMAIL: + process.env.SUPER_ADMIN_EMAIL || 'admin@yourdomain.com', + SUPER_ADMIN_PASSWORD: + process.env.SUPER_ADMIN_PASSWORD || + 'YourSecureAdminPassword123!', + + // Google OAuth + GOOGLE_CLIENT_ID: process.env.GOOGLE_CLIENT_ID || '', + GOOGLE_CLIENT_SECRET: process.env.GOOGLE_CLIENT_SECRET || '', + + // Other Configuration + OTP_LIMITER: process.env.OTP_LIMITER || '5', + SECRET_KEY: + process.env.SECRET_KEY || + 'another-random-secret-key-for-general-encryption', + ACCESS_KEY: process.env.ACCESS_KEY || '', + DB_SYNC: process.env.DB_SYNC || 'txsrue', + + // Redis (used?) + AZURE_REDIS_CONNECTIONSTRING: + process.env.AZURE_REDIS_CONNECTIONSTRING || '', + + // Docker Registry (for deployment) + DOCKER_REGISTRY_SERVER_URL: + process.env.DOCKER_REGISTRY_SERVER_URL || '', + DOCKER_REGISTRY_SERVER_USERNAME: + process.env.DOCKER_REGISTRY_SERVER_USERNAME || '', + DOCKER_REGISTRY_SERVER_PASSWORD: + process.env.DOCKER_REGISTRY_SERVER_PASSWORD || '', + + // Doppler (if used for secrets management) + DOPPLER_PROJECT: process.env.DOPPLER_PROJECT || '', + DOPPLER_CONFIG: process.env.DOPPLER_CONFIG || '', + DOPPLER_ENVIRONMENT: process.env.DOPPLER_ENVIRONMENT || '', + + // Azure specific + WEBSITES_ENABLE_APP_SERVICE_STORAGE: + process.env.WEBSITES_ENABLE_APP_SERVICE_STORAGE || 'false', + }, + secrets: { + AZURE_POSTGRESQL_PASSWORD: ecs.Secret.fromSecretsManager( + dbSecret, + 'password', + ), + }, + logDriver: ecs.LogDrivers.awsLogs({ + streamPrefix: 'syncrow-backend', + logGroup, + }), + }, + publicLoadBalancer: true, + securityGroups: [ecsSecurityGroup], }, - secrets: { - AZURE_POSTGRESQL_PASSWORD: ecs.Secret.fromSecretsManager( - dbSecret, - 'password' - ), - }, - logDriver: ecs.LogDrivers.awsLogs({ - streamPrefix: 'syncrow-backend', - logGroup, - }), - }, - publicLoadBalancer: true, - securityGroups: [ecsSecurityGroup], - }); + ); // Add security group to load balancer after creation fargateService.loadBalancer.addSecurityGroup(albSecurityGroup);