Fix production environment variables
- Remove old Confluence variables - Add NEXT_PUBLIC_API_URL for API access 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
281
terraform/api_gateway.tf
Normal file
281
terraform/api_gateway.tf
Normal file
@@ -0,0 +1,281 @@
|
||||
# API Gateway REST API
|
||||
resource "aws_api_gateway_rest_api" "api" {
|
||||
name = "${var.app_name}-api"
|
||||
description = "API for ${var.app_name}"
|
||||
}
|
||||
|
||||
# API Gateway Resources
|
||||
resource "aws_api_gateway_resource" "filaments" {
|
||||
rest_api_id = aws_api_gateway_rest_api.api.id
|
||||
parent_id = aws_api_gateway_rest_api.api.root_resource_id
|
||||
path_part = "filaments"
|
||||
}
|
||||
|
||||
resource "aws_api_gateway_resource" "filament" {
|
||||
rest_api_id = aws_api_gateway_rest_api.api.id
|
||||
parent_id = aws_api_gateway_resource.filaments.id
|
||||
path_part = "{id}"
|
||||
}
|
||||
|
||||
resource "aws_api_gateway_resource" "auth" {
|
||||
rest_api_id = aws_api_gateway_rest_api.api.id
|
||||
parent_id = aws_api_gateway_rest_api.api.root_resource_id
|
||||
path_part = "auth"
|
||||
}
|
||||
|
||||
resource "aws_api_gateway_resource" "login" {
|
||||
rest_api_id = aws_api_gateway_rest_api.api.id
|
||||
parent_id = aws_api_gateway_resource.auth.id
|
||||
path_part = "login"
|
||||
}
|
||||
|
||||
# Lambda Authorizer
|
||||
resource "aws_api_gateway_authorizer" "jwt_authorizer" {
|
||||
name = "${var.app_name}-jwt-authorizer"
|
||||
rest_api_id = aws_api_gateway_rest_api.api.id
|
||||
type = "TOKEN"
|
||||
authorizer_uri = aws_lambda_function.auth_api.invoke_arn
|
||||
authorizer_credentials = aws_iam_role.api_gateway_auth_invocation.arn
|
||||
identity_source = "method.request.header.Authorization"
|
||||
}
|
||||
|
||||
# IAM role for API Gateway to invoke Lambda authorizer
|
||||
resource "aws_iam_role" "api_gateway_auth_invocation" {
|
||||
name = "${var.app_name}-api-gateway-auth-invocation"
|
||||
|
||||
assume_role_policy = jsonencode({
|
||||
Version = "2012-10-17"
|
||||
Statement = [
|
||||
{
|
||||
Action = "sts:AssumeRole"
|
||||
Effect = "Allow"
|
||||
Principal = {
|
||||
Service = "apigateway.amazonaws.com"
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
resource "aws_iam_role_policy" "api_gateway_auth_invocation" {
|
||||
name = "${var.app_name}-api-gateway-auth-invocation"
|
||||
role = aws_iam_role.api_gateway_auth_invocation.id
|
||||
|
||||
policy = jsonencode({
|
||||
Version = "2012-10-17"
|
||||
Statement = [
|
||||
{
|
||||
Effect = "Allow"
|
||||
Action = "lambda:InvokeFunction"
|
||||
Resource = aws_lambda_function.auth_api.arn
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
# Methods for /filaments
|
||||
resource "aws_api_gateway_method" "get_filaments" {
|
||||
rest_api_id = aws_api_gateway_rest_api.api.id
|
||||
resource_id = aws_api_gateway_resource.filaments.id
|
||||
http_method = "GET"
|
||||
authorization = "NONE"
|
||||
}
|
||||
|
||||
resource "aws_api_gateway_method" "post_filament" {
|
||||
rest_api_id = aws_api_gateway_rest_api.api.id
|
||||
resource_id = aws_api_gateway_resource.filaments.id
|
||||
http_method = "POST"
|
||||
authorization = "CUSTOM"
|
||||
authorizer_id = aws_api_gateway_authorizer.jwt_authorizer.id
|
||||
}
|
||||
|
||||
# Methods for /filaments/{id}
|
||||
resource "aws_api_gateway_method" "get_filament" {
|
||||
rest_api_id = aws_api_gateway_rest_api.api.id
|
||||
resource_id = aws_api_gateway_resource.filament.id
|
||||
http_method = "GET"
|
||||
authorization = "NONE"
|
||||
}
|
||||
|
||||
resource "aws_api_gateway_method" "put_filament" {
|
||||
rest_api_id = aws_api_gateway_rest_api.api.id
|
||||
resource_id = aws_api_gateway_resource.filament.id
|
||||
http_method = "PUT"
|
||||
authorization = "CUSTOM"
|
||||
authorizer_id = aws_api_gateway_authorizer.jwt_authorizer.id
|
||||
}
|
||||
|
||||
resource "aws_api_gateway_method" "delete_filament" {
|
||||
rest_api_id = aws_api_gateway_rest_api.api.id
|
||||
resource_id = aws_api_gateway_resource.filament.id
|
||||
http_method = "DELETE"
|
||||
authorization = "CUSTOM"
|
||||
authorizer_id = aws_api_gateway_authorizer.jwt_authorizer.id
|
||||
}
|
||||
|
||||
# Method for /auth/login
|
||||
resource "aws_api_gateway_method" "login" {
|
||||
rest_api_id = aws_api_gateway_rest_api.api.id
|
||||
resource_id = aws_api_gateway_resource.login.id
|
||||
http_method = "POST"
|
||||
authorization = "NONE"
|
||||
}
|
||||
|
||||
# OPTIONS methods for CORS
|
||||
resource "aws_api_gateway_method" "options_filaments" {
|
||||
rest_api_id = aws_api_gateway_rest_api.api.id
|
||||
resource_id = aws_api_gateway_resource.filaments.id
|
||||
http_method = "OPTIONS"
|
||||
authorization = "NONE"
|
||||
}
|
||||
|
||||
resource "aws_api_gateway_method" "options_filament" {
|
||||
rest_api_id = aws_api_gateway_rest_api.api.id
|
||||
resource_id = aws_api_gateway_resource.filament.id
|
||||
http_method = "OPTIONS"
|
||||
authorization = "NONE"
|
||||
}
|
||||
|
||||
resource "aws_api_gateway_method" "options_login" {
|
||||
rest_api_id = aws_api_gateway_rest_api.api.id
|
||||
resource_id = aws_api_gateway_resource.login.id
|
||||
http_method = "OPTIONS"
|
||||
authorization = "NONE"
|
||||
}
|
||||
|
||||
# Lambda integrations
|
||||
resource "aws_api_gateway_integration" "filaments_get" {
|
||||
rest_api_id = aws_api_gateway_rest_api.api.id
|
||||
resource_id = aws_api_gateway_resource.filaments.id
|
||||
http_method = aws_api_gateway_method.get_filaments.http_method
|
||||
|
||||
integration_http_method = "POST"
|
||||
type = "AWS_PROXY"
|
||||
uri = aws_lambda_function.filaments_api.invoke_arn
|
||||
}
|
||||
|
||||
resource "aws_api_gateway_integration" "filaments_post" {
|
||||
rest_api_id = aws_api_gateway_rest_api.api.id
|
||||
resource_id = aws_api_gateway_resource.filaments.id
|
||||
http_method = aws_api_gateway_method.post_filament.http_method
|
||||
|
||||
integration_http_method = "POST"
|
||||
type = "AWS_PROXY"
|
||||
uri = aws_lambda_function.filaments_api.invoke_arn
|
||||
}
|
||||
|
||||
resource "aws_api_gateway_integration" "filament_get" {
|
||||
rest_api_id = aws_api_gateway_rest_api.api.id
|
||||
resource_id = aws_api_gateway_resource.filament.id
|
||||
http_method = aws_api_gateway_method.get_filament.http_method
|
||||
|
||||
integration_http_method = "POST"
|
||||
type = "AWS_PROXY"
|
||||
uri = aws_lambda_function.filaments_api.invoke_arn
|
||||
}
|
||||
|
||||
resource "aws_api_gateway_integration" "filament_put" {
|
||||
rest_api_id = aws_api_gateway_rest_api.api.id
|
||||
resource_id = aws_api_gateway_resource.filament.id
|
||||
http_method = aws_api_gateway_method.put_filament.http_method
|
||||
|
||||
integration_http_method = "POST"
|
||||
type = "AWS_PROXY"
|
||||
uri = aws_lambda_function.filaments_api.invoke_arn
|
||||
}
|
||||
|
||||
resource "aws_api_gateway_integration" "filament_delete" {
|
||||
rest_api_id = aws_api_gateway_rest_api.api.id
|
||||
resource_id = aws_api_gateway_resource.filament.id
|
||||
http_method = aws_api_gateway_method.delete_filament.http_method
|
||||
|
||||
integration_http_method = "POST"
|
||||
type = "AWS_PROXY"
|
||||
uri = aws_lambda_function.filaments_api.invoke_arn
|
||||
}
|
||||
|
||||
resource "aws_api_gateway_integration" "login" {
|
||||
rest_api_id = aws_api_gateway_rest_api.api.id
|
||||
resource_id = aws_api_gateway_resource.login.id
|
||||
http_method = aws_api_gateway_method.login.http_method
|
||||
|
||||
integration_http_method = "POST"
|
||||
type = "AWS_PROXY"
|
||||
uri = aws_lambda_function.auth_api.invoke_arn
|
||||
}
|
||||
|
||||
# OPTIONS integrations for CORS
|
||||
resource "aws_api_gateway_integration" "options_filaments" {
|
||||
rest_api_id = aws_api_gateway_rest_api.api.id
|
||||
resource_id = aws_api_gateway_resource.filaments.id
|
||||
http_method = aws_api_gateway_method.options_filaments.http_method
|
||||
|
||||
integration_http_method = "POST"
|
||||
type = "AWS_PROXY"
|
||||
uri = aws_lambda_function.filaments_api.invoke_arn
|
||||
}
|
||||
|
||||
resource "aws_api_gateway_integration" "options_filament" {
|
||||
rest_api_id = aws_api_gateway_rest_api.api.id
|
||||
resource_id = aws_api_gateway_resource.filament.id
|
||||
http_method = aws_api_gateway_method.options_filament.http_method
|
||||
|
||||
integration_http_method = "POST"
|
||||
type = "AWS_PROXY"
|
||||
uri = aws_lambda_function.filaments_api.invoke_arn
|
||||
}
|
||||
|
||||
resource "aws_api_gateway_integration" "options_login" {
|
||||
rest_api_id = aws_api_gateway_rest_api.api.id
|
||||
resource_id = aws_api_gateway_resource.login.id
|
||||
http_method = aws_api_gateway_method.options_login.http_method
|
||||
|
||||
integration_http_method = "POST"
|
||||
type = "AWS_PROXY"
|
||||
uri = aws_lambda_function.auth_api.invoke_arn
|
||||
}
|
||||
|
||||
# Lambda permissions for API Gateway
|
||||
resource "aws_lambda_permission" "api_gateway_filaments" {
|
||||
statement_id = "AllowAPIGatewayInvoke"
|
||||
action = "lambda:InvokeFunction"
|
||||
function_name = aws_lambda_function.filaments_api.function_name
|
||||
principal = "apigateway.amazonaws.com"
|
||||
source_arn = "${aws_api_gateway_rest_api.api.execution_arn}/*/*"
|
||||
}
|
||||
|
||||
resource "aws_lambda_permission" "api_gateway_auth" {
|
||||
statement_id = "AllowAPIGatewayInvoke"
|
||||
action = "lambda:InvokeFunction"
|
||||
function_name = aws_lambda_function.auth_api.function_name
|
||||
principal = "apigateway.amazonaws.com"
|
||||
source_arn = "${aws_api_gateway_rest_api.api.execution_arn}/*/*"
|
||||
}
|
||||
|
||||
# API Gateway Deployment
|
||||
resource "aws_api_gateway_deployment" "api" {
|
||||
rest_api_id = aws_api_gateway_rest_api.api.id
|
||||
|
||||
depends_on = [
|
||||
aws_api_gateway_integration.filaments_get,
|
||||
aws_api_gateway_integration.filaments_post,
|
||||
aws_api_gateway_integration.filament_get,
|
||||
aws_api_gateway_integration.filament_put,
|
||||
aws_api_gateway_integration.filament_delete,
|
||||
aws_api_gateway_integration.login,
|
||||
aws_api_gateway_integration.options_filaments,
|
||||
aws_api_gateway_integration.options_filament,
|
||||
aws_api_gateway_integration.options_login
|
||||
]
|
||||
|
||||
lifecycle {
|
||||
create_before_destroy = true
|
||||
}
|
||||
}
|
||||
|
||||
# API Gateway Stage
|
||||
resource "aws_api_gateway_stage" "api" {
|
||||
deployment_id = aws_api_gateway_deployment.api.id
|
||||
rest_api_id = aws_api_gateway_rest_api.api.id
|
||||
stage_name = var.environment
|
||||
}
|
||||
22
terraform/cloudflare-dns.tf
Normal file
22
terraform/cloudflare-dns.tf
Normal file
@@ -0,0 +1,22 @@
|
||||
# Cloudflare DNS configuration
|
||||
provider "cloudflare" {
|
||||
api_token = var.cloudflare_api_token
|
||||
}
|
||||
|
||||
# Data source to find the zone
|
||||
data "cloudflare_zone" "main" {
|
||||
count = var.domain_name != "" && var.cloudflare_api_token != "" ? 1 : 0
|
||||
name = var.domain_name
|
||||
}
|
||||
|
||||
# Create CNAME record for API subdomain
|
||||
resource "cloudflare_record" "api" {
|
||||
count = var.domain_name != "" && var.cloudflare_api_token != "" ? 1 : 0
|
||||
zone_id = data.cloudflare_zone.main[0].id
|
||||
name = "api"
|
||||
content = replace(replace(aws_api_gateway_stage.api.invoke_url, "https://", ""), "/production", "")
|
||||
type = "CNAME"
|
||||
ttl = 120
|
||||
proxied = false
|
||||
comment = "API Gateway endpoint"
|
||||
}
|
||||
52
terraform/dynamodb.tf
Normal file
52
terraform/dynamodb.tf
Normal file
@@ -0,0 +1,52 @@
|
||||
# DynamoDB table for storing filament data
|
||||
resource "aws_dynamodb_table" "filaments" {
|
||||
name = "${var.app_name}-filaments"
|
||||
billing_mode = "PAY_PER_REQUEST"
|
||||
hash_key = "id"
|
||||
|
||||
attribute {
|
||||
name = "id"
|
||||
type = "S"
|
||||
}
|
||||
|
||||
attribute {
|
||||
name = "brand"
|
||||
type = "S"
|
||||
}
|
||||
|
||||
attribute {
|
||||
name = "tip"
|
||||
type = "S"
|
||||
}
|
||||
|
||||
attribute {
|
||||
name = "status"
|
||||
type = "S"
|
||||
}
|
||||
|
||||
# Global secondary index for querying by brand
|
||||
global_secondary_index {
|
||||
name = "brand-index"
|
||||
hash_key = "brand"
|
||||
projection_type = "ALL"
|
||||
}
|
||||
|
||||
# Global secondary index for querying by type
|
||||
global_secondary_index {
|
||||
name = "tip-index"
|
||||
hash_key = "tip"
|
||||
projection_type = "ALL"
|
||||
}
|
||||
|
||||
# Global secondary index for querying by status
|
||||
global_secondary_index {
|
||||
name = "status-index"
|
||||
hash_key = "status"
|
||||
projection_type = "ALL"
|
||||
}
|
||||
|
||||
tags = {
|
||||
Name = "${var.app_name}-filaments"
|
||||
Environment = var.environment
|
||||
}
|
||||
}
|
||||
110
terraform/lambda.tf
Normal file
110
terraform/lambda.tf
Normal file
@@ -0,0 +1,110 @@
|
||||
# IAM role for Lambda functions
|
||||
resource "aws_iam_role" "lambda_role" {
|
||||
name = "${var.app_name}-lambda-role"
|
||||
|
||||
assume_role_policy = jsonencode({
|
||||
Version = "2012-10-17"
|
||||
Statement = [
|
||||
{
|
||||
Action = "sts:AssumeRole"
|
||||
Effect = "Allow"
|
||||
Principal = {
|
||||
Service = "lambda.amazonaws.com"
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
# IAM policy for Lambda to access DynamoDB
|
||||
resource "aws_iam_role_policy" "lambda_dynamodb_policy" {
|
||||
name = "${var.app_name}-lambda-dynamodb-policy"
|
||||
role = aws_iam_role.lambda_role.id
|
||||
|
||||
policy = jsonencode({
|
||||
Version = "2012-10-17"
|
||||
Statement = [
|
||||
{
|
||||
Effect = "Allow"
|
||||
Action = [
|
||||
"dynamodb:GetItem",
|
||||
"dynamodb:PutItem",
|
||||
"dynamodb:UpdateItem",
|
||||
"dynamodb:DeleteItem",
|
||||
"dynamodb:Scan",
|
||||
"dynamodb:Query"
|
||||
]
|
||||
Resource = [
|
||||
aws_dynamodb_table.filaments.arn,
|
||||
"${aws_dynamodb_table.filaments.arn}/index/*"
|
||||
]
|
||||
},
|
||||
{
|
||||
Effect = "Allow"
|
||||
Action = [
|
||||
"logs:CreateLogGroup",
|
||||
"logs:CreateLogStream",
|
||||
"logs:PutLogEvents"
|
||||
]
|
||||
Resource = "arn:aws:logs:*:*:*"
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
# Lambda function for filaments CRUD
|
||||
resource "aws_lambda_function" "filaments_api" {
|
||||
filename = data.archive_file.filaments_lambda_zip.output_path
|
||||
function_name = "${var.app_name}-filaments-api"
|
||||
role = aws_iam_role.lambda_role.arn
|
||||
handler = "index.handler"
|
||||
runtime = "nodejs18.x"
|
||||
timeout = 30
|
||||
memory_size = 256
|
||||
source_code_hash = data.archive_file.filaments_lambda_zip.output_base64sha256
|
||||
|
||||
environment {
|
||||
variables = {
|
||||
TABLE_NAME = aws_dynamodb_table.filaments.name
|
||||
CORS_ORIGIN = var.domain_name != "" ? "https://${var.domain_name}" : "*"
|
||||
}
|
||||
}
|
||||
|
||||
depends_on = [aws_iam_role_policy.lambda_dynamodb_policy]
|
||||
}
|
||||
|
||||
# Lambda function for authentication
|
||||
resource "aws_lambda_function" "auth_api" {
|
||||
filename = data.archive_file.auth_lambda_zip.output_path
|
||||
function_name = "${var.app_name}-auth-api"
|
||||
role = aws_iam_role.lambda_role.arn
|
||||
handler = "index.handler"
|
||||
runtime = "nodejs18.x"
|
||||
timeout = 10
|
||||
memory_size = 128
|
||||
source_code_hash = data.archive_file.auth_lambda_zip.output_base64sha256
|
||||
|
||||
environment {
|
||||
variables = {
|
||||
JWT_SECRET = var.jwt_secret
|
||||
ADMIN_USERNAME = var.admin_username
|
||||
ADMIN_PASSWORD_HASH = var.admin_password_hash
|
||||
CORS_ORIGIN = var.domain_name != "" ? "https://${var.domain_name}" : "*"
|
||||
}
|
||||
}
|
||||
|
||||
depends_on = [aws_iam_role_policy.lambda_dynamodb_policy]
|
||||
}
|
||||
|
||||
# Archive files for Lambda deployment
|
||||
data "archive_file" "filaments_lambda_zip" {
|
||||
type = "zip"
|
||||
source_dir = "${path.module}/../lambda/filaments"
|
||||
output_path = "${path.module}/../lambda/filaments.zip"
|
||||
}
|
||||
|
||||
data "archive_file" "auth_lambda_zip" {
|
||||
type = "zip"
|
||||
source_dir = "${path.module}/../lambda/auth"
|
||||
output_path = "${path.module}/../lambda/auth.zip"
|
||||
}
|
||||
@@ -4,6 +4,10 @@ terraform {
|
||||
source = "hashicorp/aws"
|
||||
version = "~> 5.0"
|
||||
}
|
||||
cloudflare = {
|
||||
source = "cloudflare/cloudflare"
|
||||
version = "~> 4.0"
|
||||
}
|
||||
}
|
||||
required_version = ">= 1.0"
|
||||
}
|
||||
@@ -17,8 +21,8 @@ resource "aws_amplify_app" "filamenteka" {
|
||||
repository = var.github_repository
|
||||
platform = "WEB"
|
||||
|
||||
# GitHub access token for private repos
|
||||
access_token = var.github_token
|
||||
# GitHub access token for private repos (optional for public repos)
|
||||
# access_token = var.github_token
|
||||
|
||||
# Build settings for Next.js
|
||||
build_spec = <<-EOT
|
||||
@@ -48,6 +52,7 @@ resource "aws_amplify_app" "filamenteka" {
|
||||
CONFLUENCE_API_URL = var.confluence_api_url
|
||||
CONFLUENCE_TOKEN = var.confluence_token
|
||||
CONFLUENCE_PAGE_ID = var.confluence_page_id
|
||||
NEXT_PUBLIC_API_URL = aws_api_gateway_stage.api.invoke_url
|
||||
}
|
||||
|
||||
# Custom rules for single-page app
|
||||
|
||||
@@ -18,3 +18,17 @@ output "custom_domain_url" {
|
||||
value = var.domain_name != "" ? "https://${var.domain_name}" : "Not configured"
|
||||
}
|
||||
|
||||
output "api_url" {
|
||||
description = "API Gateway URL"
|
||||
value = aws_api_gateway_stage.api.invoke_url
|
||||
}
|
||||
|
||||
output "dynamodb_table_name" {
|
||||
description = "DynamoDB table name"
|
||||
value = aws_dynamodb_table.filaments.name
|
||||
}
|
||||
|
||||
output "api_custom_url" {
|
||||
description = "Custom API URL via Cloudflare"
|
||||
value = var.domain_name != "" && var.cloudflare_api_token != "" ? "https://api.${var.domain_name}" : "Use api_url output instead"
|
||||
}
|
||||
|
||||
@@ -7,5 +7,10 @@ confluence_api_url = "https://your-domain.atlassian.net"
|
||||
confluence_token = "your_confluence_api_token"
|
||||
confluence_page_id = "your_confluence_page_id"
|
||||
|
||||
# Admin Authentication
|
||||
jwt_secret = "your-secret-key-at-least-32-characters-long"
|
||||
admin_username = "admin"
|
||||
admin_password_hash = "bcrypt-hash-generated-by-generate-password-hash.js"
|
||||
|
||||
# Optional: Custom domain
|
||||
# domain_name = "filamenteka.yourdomain.com"
|
||||
@@ -35,4 +35,35 @@ variable "environment" {
|
||||
description = "Environment name"
|
||||
type = string
|
||||
default = "production"
|
||||
}
|
||||
|
||||
variable "app_name" {
|
||||
description = "Application name"
|
||||
type = string
|
||||
default = "filamenteka"
|
||||
}
|
||||
|
||||
variable "jwt_secret" {
|
||||
description = "JWT secret for authentication"
|
||||
type = string
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
variable "admin_username" {
|
||||
description = "Admin username"
|
||||
type = string
|
||||
default = "admin"
|
||||
}
|
||||
|
||||
variable "admin_password_hash" {
|
||||
description = "BCrypt hash of admin password"
|
||||
type = string
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
variable "cloudflare_api_token" {
|
||||
description = "Cloudflare API token for DNS management"
|
||||
type = string
|
||||
default = ""
|
||||
sensitive = true
|
||||
}
|
||||
Reference in New Issue
Block a user