diff --git a/.github/workflows/release-staging.yml b/.github/workflows/release-staging.yml index 905f629..691770c 100644 --- a/.github/workflows/release-staging.yml +++ b/.github/workflows/release-staging.yml @@ -1,3 +1,4 @@ + name: Release – Staging on: @@ -19,16 +20,16 @@ jobs: timeout-minutes: 15 environment: staging env: - TF_VERSION: 1.4.6 + TF_VERSION: 1.13.3 APP_NAME: 'stag-saas-template' SUPABASE_URL: ${{secrets.SUPABASE_URL}} SUPABASE_SECRET_KEY: ${{secrets.SUPABASE_SECRET_KEY}} GOOGLE_OAUTH_CLIENT_ID: ${{secrets.GOOGLE_OAUTH_CLIENT_ID}} GOOGLE_OAUTH_SECRET: ${{secrets.GOOGLE_OAUTH_SECRET}} - DB_URL: ${{secrets.DB_URL}} STRIPE_API_KEY: ${{secrets.STRIPE_API_KEY}} STRIPE_WEBHOOK_SIGNING_SECRET: ${{secrets.STRIPE_WEBHOOK_SIGNING_SECRET}} JWT_SECRET: ${{secrets.JWT_SECRET}} + LOGTAIL_SOURCE_TOKEN: ${{secrets.LOGTAIL_SOURCE_TOKEN}} API_BASE_URL: ${{vars.API_BASE_URL}} steps: @@ -43,7 +44,6 @@ jobs: SUPABASE_SECRET_KEY=${{ env.SUPABASE_SECRET_KEY }} GOOGLE_OAUTH_CLIENT_ID=${{ env.GOOGLE_OAUTH_CLIENT_ID }} GOOGLE_OAUTH_SECRET=${{ env.GOOGLE_OAUTH_SECRET }} - DB_URL=${{ env.DB_URL }} STRIPE_API_KEY=${{ env.STRIPE_API_KEY }} STRIPE_WEBHOOK_SIGNING_SECRET=${{ env.STRIPE_WEBHOOK_SIGNING_SECRET }} JWT_SECRET=${{ env.JWT_SECRET }} @@ -58,6 +58,7 @@ jobs: run: | sed -i "s/autoreplace_do_token/$DIGITAL_OCEAN_TOKEN/g" variables.tf sed -i "s/autoreplace_github_token/$GITHUB_TOKEN/g" variables.tf + sed -i "s/autoreplace_logtail_token/$LOGTAIL_SOURCE_TOKEN/g" variables.tf - name: Terraform init and apply working-directory: ./infra/environments/staging diff --git a/api/config/config.staging.sh b/api/config/config.staging.sh old mode 100644 new mode 100755 index 5cc869a..5c21bf3 --- a/api/config/config.staging.sh +++ b/api/config/config.staging.sh @@ -1,6 +1,35 @@ +# Database Configuration +# IMPORTANT: Only uncomment and configure these if create_database = false in your terraform.tfvars +# If using DigitalOcean managed database (create_database = true), these are automatically provided by Terraform +# DO NOT include these variables when using Terraform-managed database +#DB_HOST="YOUR_DB_HOST" +#DB_PORT="YOUR_DB_PORT" +#DB_USER="YOUR_DB_USER" +#DB_PASSWORD="YOUR_DB_PASSWORD" +#DB_NAME="YOUR_DB_NAME" +#DB_CA="" # Add CA certificate content if needed for SSL connections + +# Supabase Configuration +SUPABASE_URL=https://your-supabase-app-name.supabase.co +SUPABASE_SECRET_KEY=YOUR_SUPABASE_SECRET_KEY_HERE + +# Google OAuth Configuration +GOOGLE_OAUTH_CLIENT_ID=YOUR_GOOGLE_OAUTH_CLIENT_ID_HERE +GOOGLE_OAUTH_SECRET=YOUR_GOOGLE_OAUTH_SECRET_HERE GOOGLE_OAUTH_CALLBACK_URL=https://stag-saas-template-46ccu.ondigitalocean.app/api/auth/oauth/google-callback + +# Stripe Configuration +STRIPE_API_KEY=YOUR_STRIPE_API_KEY_HERE +STRIPE_WEBHOOK_SIGNING_SECRET=YOUR_STRIPE_WEBHOOK_SIGNING_SECRET_HERE + +# JWT Configuration +JWT_SECRET=YOUR_JWT_SECRET_HERE + +# Application Configuration PASSWORD_RESET_REDIRECT_URL=https://stag-saas-template-46ccu.ondigitalocean.app/reset CLIENT_AUTH_REDIRECT_URL=https://stag-saas-template-46ccu.ondigitalocean.app/sign-in PASSWORD_RECOVERY_TIME=900 BILLING_SUCCESS_REDIRECT_URL=https://stag-saas-template-46ccu.ondigitalocean.app/billing TRIAL_PERIOD_DURATION_DAYS=14 + +TEST=test \ No newline at end of file diff --git a/api/src/shared/application/models/app-config.model.ts b/api/src/shared/application/models/app-config.model.ts index e7c7170..2e1cd1c 100644 --- a/api/src/shared/application/models/app-config.model.ts +++ b/api/src/shared/application/models/app-config.model.ts @@ -1,11 +1,27 @@ -import { IsInt, IsPositive, IsString } from 'class-validator'; +import { IsInt, IsOptional, IsPositive, IsString } from 'class-validator'; export class AppConfigModel { @IsString() TEST: string; @IsString() - DB_URL: string; + DB_HOST: string; + + @IsString() + DB_PORT: string; + + @IsString() + DB_USER: string; + + @IsString() + DB_PASSWORD: string; + + @IsString() + DB_NAME: string; + + @IsOptional() + @IsString() + DB_CA?: string; @IsString() SUPABASE_URL: string; diff --git a/api/src/shared/infrastructure/database/database.module.ts b/api/src/shared/infrastructure/database/database.module.ts index 702197a..845cc57 100644 --- a/api/src/shared/infrastructure/database/database.module.ts +++ b/api/src/shared/infrastructure/database/database.module.ts @@ -9,10 +9,27 @@ import { DrizzleDbContext } from './drizzle/db-context/drizzle-db-context'; import { mergeDbdSchema } from './drizzle/schema/merged-schema'; export const drizzlePostgresModule = DrizzlePostgresModule.registerAsync({ - useFactory: (appConfig: IAppConfigService) => ({ - db: { config: { connectionString: appConfig.get('DB_URL') }, connection: 'pool' }, - schema: mergeDbdSchema, - }), + useFactory: (appConfig: IAppConfigService) => { + const databaseCa = appConfig.get('DB_CA'); + + return { + db: { + config: { + host: appConfig.get('DB_HOST'), + port: parseInt(appConfig.get('DB_PORT')), + user: appConfig.get('DB_USER'), + password: appConfig.get('DB_PASSWORD'), + database: appConfig.get('DB_NAME'), + ssl: databaseCa ? { + rejectUnauthorized: true, + ca: databaseCa, + } : false, + }, + connection: 'pool' + }, + schema: mergeDbdSchema, + }; + }, inject: [BaseToken.APP_CONFIG], }); diff --git a/infra/environments/staging/main.tf b/infra/environments/staging/main.tf index 3e129ca..b40316c 100644 --- a/infra/environments/staging/main.tf +++ b/infra/environments/staging/main.tf @@ -7,12 +7,17 @@ terraform { } backend "s3" { - endpoint = "fra1.digitaloceanspaces.com" + endpoints = { + s3 = "https://fra1.digitaloceanspaces.com" + } region = "us-west-1" bucket = "saas-template-space" key = "staging_terraform.tfstate" skip_credentials_validation = true skip_metadata_api_check = true + skip_region_validation = true + skip_s3_checksum = true + skip_requesting_account_id = true } } @@ -29,7 +34,7 @@ module "env_config" { module "saas_template_app" { source = "../../modules/app" name = "stag-saas-template" - region = "lon" + region = "fra" source_dir_static = "./web" source_dir_api = "./api" environment_slug = "node-js" @@ -52,4 +57,11 @@ module "saas_template_app" { logtail_source_name = "saas-staging-logtail" logtail_source_token = var.logtail_source_token + + # Database configuration + project_name = var.project_name + create_database = var.create_database + database_size = var.database_size + database_node_count = var.database_node_count + database_region = var.database_region } diff --git a/infra/environments/staging/outputs.tf b/infra/environments/staging/outputs.tf new file mode 100644 index 0000000..78625f5 --- /dev/null +++ b/infra/environments/staging/outputs.tf @@ -0,0 +1,12 @@ +# Database outputs +output "database_uri" { + value = module.saas_template_app.database_uri + sensitive = true + description = "Database connection URI (if database is created)" +} + +# Application outputs +output "app_url" { + value = module.saas_template_app.app_url + description = "The live URL of the deployed app" +} \ No newline at end of file diff --git a/infra/environments/staging/variables.tf b/infra/environments/staging/variables.tf index 0542dcf..450f81d 100644 --- a/infra/environments/staging/variables.tf +++ b/infra/environments/staging/variables.tf @@ -14,7 +14,7 @@ variable "do_token" { variable "logtail_source_token" { description = "Logtail Token for the application logs" type = string - default = "" + default = "autoreplace_logtail_token" } variable "github_token" { @@ -22,3 +22,33 @@ variable "github_token" { type = string default = "autoreplace_github_token" } + +variable "project_name" { + description = "Name of the DigitalOcean project" + type = string + default = "SaaS Template" +} + +variable "create_database" { + description = "Whether to create a managed database" + type = bool + default = true +} + +variable "database_size" { + description = "Size of the database cluster" + type = string + default = "db-s-1vcpu-1gb" +} + +variable "database_node_count" { + description = "Number of nodes in the database cluster" + type = number + default = 1 +} + +variable "database_region" { + description = "Region for the database cluster" + type = string + default = "fra1" +} diff --git a/infra/modules/app/database.tf b/infra/modules/app/database.tf new file mode 100644 index 0000000..2899e7e --- /dev/null +++ b/infra/modules/app/database.tf @@ -0,0 +1,29 @@ +# Database cluster and related resources +resource "digitalocean_database_cluster" "postgres" { + count = var.create_database ? 1 : 0 + name = "${var.name}-db" + engine = "pg" + version = "15" + size = var.database_size + region = var.database_region + node_count = var.database_node_count + + project_id = digitalocean_project.saas_template.id +} + +resource "digitalocean_database_db" "app_database" { + count = var.create_database ? 1 : 0 + cluster_id = digitalocean_database_cluster.postgres[0].id + name = "app" +} + +resource "digitalocean_database_user" "app_user" { + count = var.create_database ? 1 : 0 + cluster_id = digitalocean_database_cluster.postgres[0].id + name = "appuser" +} + +data "digitalocean_database_ca" "postgres" { + count = var.create_database ? 1 : 0 + cluster_id = digitalocean_database_cluster.postgres[0].id +} \ No newline at end of file diff --git a/infra/modules/app/main.tf b/infra/modules/app/main.tf index 6e54b4c..e6a48f6 100644 --- a/infra/modules/app/main.tf +++ b/infra/modules/app/main.tf @@ -14,14 +14,17 @@ provider "digitalocean" { resource "digitalocean_project" "saas_template" { - name = "SaaS Template" + name = var.project_name description = "A project for the SaaS Template application" purpose = "Web Application" environment = "Development" } resource "digitalocean_app" "saas_template" { - depends_on = [ digitalocean_project.saas_template ] + depends_on = [ + digitalocean_project.saas_template, + digitalocean_database_cluster.postgres + ] project_id = digitalocean_project.saas_template.id spec { name = var.name @@ -69,6 +72,60 @@ resource "digitalocean_app" "saas_template" { } } + dynamic "env" { + for_each = var.create_database ? [1] : [] + content { + key = "DB_HOST" + scope = "RUN_TIME" + value = digitalocean_database_cluster.postgres[0].host + } + } + + dynamic "env" { + for_each = var.create_database ? [1] : [] + content { + key = "DB_PORT" + scope = "RUN_TIME" + value = tostring(digitalocean_database_cluster.postgres[0].port) + } + } + + dynamic "env" { + for_each = var.create_database ? [1] : [] + content { + key = "DB_USER" + scope = "RUN_TIME" + value = digitalocean_database_user.app_user[0].name + } + } + + dynamic "env" { + for_each = var.create_database ? [1] : [] + content { + key = "DB_PASSWORD" + scope = "RUN_TIME" + value = digitalocean_database_user.app_user[0].password + } + } + + dynamic "env" { + for_each = var.create_database ? [1] : [] + content { + key = "DB_NAME" + scope = "RUN_TIME" + value = digitalocean_database_db.app_database[0].name + } + } + + dynamic "env" { + for_each = var.create_database ? [1] : [] + content { + key = "DB_CA" + scope = "RUN_TIME" + value = data.digitalocean_database_ca.postgres[0].certificate + } + } + github { repo = var.gh_repository deploy_on_push = var.deploy_on_push @@ -87,13 +144,13 @@ resource "digitalocean_app" "saas_template" { port = var.api_http_port } - # log_destination { - # name = var.logtail_source_name + log_destination { + name = var.logtail_source_name - # logtail { - # token = var.logtail_source_token - # } - # } + logtail { + token = var.logtail_source_token + } + } } ingress { diff --git a/infra/modules/app/outputs.tf b/infra/modules/app/outputs.tf new file mode 100644 index 0000000..e6f5e5f --- /dev/null +++ b/infra/modules/app/outputs.tf @@ -0,0 +1,54 @@ +# Application outputs +output "app_url" { + value = digitalocean_app.saas_template.live_url + description = "The live URL of the deployed app" +} + +# Database outputs +output "database_uri" { + value = var.create_database ? digitalocean_database_cluster.postgres[0].uri : "" + sensitive = true + description = "Database connection URI" +} + +output "database_private_uri" { + value = var.create_database ? digitalocean_database_cluster.postgres[0].private_uri : "" + sensitive = true + description = "Private database connection URI" +} + +output "database_host" { + value = var.create_database ? digitalocean_database_cluster.postgres[0].host : "" + sensitive = true + description = "Database host" +} + +output "database_port" { + value = var.create_database ? digitalocean_database_cluster.postgres[0].port : "" + sensitive = true + description = "Database port" +} + +output "database_user" { + value = var.create_database ? digitalocean_database_user.app_user[0].name : "" + sensitive = true + description = "Database user" +} + +output "database_password" { + value = var.create_database ? digitalocean_database_user.app_user[0].password : "" + sensitive = true + description = "Database password" +} + +output "database_name" { + value = var.create_database ? digitalocean_database_db.app_database[0].name : "" + sensitive = true + description = "Database name" +} + +output "database_ca_certificate" { + value = var.create_database ? data.digitalocean_database_ca.postgres[0].certificate : "" + sensitive = true + description = "Database CA certificate" +} \ No newline at end of file diff --git a/infra/modules/app/variables.tf b/infra/modules/app/variables.tf index 9e6cf16..7134a91 100644 --- a/infra/modules/app/variables.tf +++ b/infra/modules/app/variables.tf @@ -111,3 +111,33 @@ variable "env_vars" { value = string })) } + +variable "project_name" { + description = "Name of the DigitalOcean project" + type = string + default = "SaaS Template" +} + +variable "create_database" { + description = "Whether to create a managed database" + type = bool + default = true +} + +variable "database_size" { + description = "Size of the database cluster" + type = string + default = "db-s-1vcpu-1gb" +} + +variable "database_node_count" { + description = "Number of nodes in the database cluster" + type = number + default = 1 +} + +variable "database_region" { + description = "Region for the database cluster" + type = string + default = "fra1" +}