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:
DaX
2025-06-20 00:11:36 +02:00
parent 1a96e5eef6
commit a2252fa923
31 changed files with 4089 additions and 42 deletions

281
terraform/api_gateway.tf Normal file
View 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
}

View 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
View 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
View 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"
}

View File

@@ -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

View File

@@ -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"
}

View File

@@ -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"

View File

@@ -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
}