Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ main
.mcp.json
changes.md

deployments/glad/cdk
scripts/continue-refresh-token.sh

# Test binaries and coverage
Expand Down Expand Up @@ -88,3 +89,6 @@ yarn-error.log*

# Go module downloads (keep go.sum)
vendor/cmd/glad/testdata/api-testing/http-client.env.json

# Amplify configuration (generated from deployment)
amplify_outputs.json
80 changes: 80 additions & 0 deletions Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,83 @@ tasks:
dev:quick-test:
desc: 'Quick development test'
deps: [fmt, test]

# Frontend tasks
frontend:install:
desc: 'Install Angular dependencies'
dir: site/glad-ui
cmds:
- npm install

frontend:serve:
desc: 'Serve Angular locally (http://localhost:4200)'
dir: site/glad-ui
cmds:
- npm start

frontend:build:
desc: 'Build Angular for production'
dir: site/glad-ui
cmds:
- npm run build -- --configuration production

frontend:test:
desc: 'Run Angular unit tests'
dir: site/glad-ui
cmds:
- npm test

frontend:deploy:
desc: 'Deploy Angular to S3 and invalidate CloudFront cache'
deps: [frontend:build]
cmds:
- |
BUCKET_NAME=$(aws cloudformation describe-stacks \
--stack-name glad-frontend-stack-production \
--query "Stacks[0].Outputs[?OutputKey=='WebsiteBucketName'].OutputValue" \
--output text)
echo "Deploying to S3 bucket: $BUCKET_NAME"
aws s3 sync site/glad-ui/dist/glad-ui/browser s3://$BUCKET_NAME --delete
- |
DIST_ID=$(aws cloudformation describe-stacks \
--stack-name glad-frontend-stack-production \
--query "Stacks[0].Outputs[?OutputKey=='DistributionId'].OutputValue" \
--output text)
echo "Invalidating CloudFront distribution: $DIST_ID"
aws cloudfront create-invalidation --distribution-id $DIST_ID --paths "/*"

sync:config:
desc: 'Sync CDK outputs to Angular environment'
cmds:
- bash scripts/sync-cdk-outputs.sh

generate:amplify-config:
desc: 'Generate amplify_outputs.json from deployed CDK stacks'
cmds:
- go run ./cmd/generate-amplify-config/main.go --env production --output site/glad-ui/amplify_outputs.json

# Full deployment workflow
deploy:backend:
desc: 'Deploy backend infrastructure (CDK stacks)'
cmds:
# - task: glad:test
- task: glad:cdk:deploy

deploy:frontend:
desc: 'Deploy frontend (build + S3 upload + CloudFront invalidation)'
deps: [generate:amplify-config]
cmds:
- task: frontend:deploy

deploy:full:
desc: 'Deploy entire stack (backend + frontend)'
cmds:
- task: deploy:backend
- task: deploy:frontend
- echo '✅ Full stack deployment complete!'
- |
WEBSITE_URL=$(aws cloudformation describe-stacks \
--stack-name glad-frontend-stack-production \
--query "Stacks[0].Outputs[?OutputKey=='WebsiteURL'].OutputValue" \
--output text)
echo "🌐 Application URL: $WEBSITE_URL"
10 changes: 10 additions & 0 deletions cmd/cognito-triggers/post-confirmation/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
module github.com/hackmajoris/glad-stack/cmd/cognito-triggers/post-confirmation

go 1.24.0

require (
github.com/aws/aws-lambda-go v1.51.1
github.com/aws/aws-sdk-go v1.55.8
)

require github.com/jmespath/go-jmespath v0.4.0 // indirect
21 changes: 21 additions & 0 deletions cmd/cognito-triggers/post-confirmation/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
github.com/aws/aws-lambda-go v1.51.1 h1:FpqpCK2WOSoq6hJvO9PhN44GzZHWCN3e9DUQgK0BOKo=
github.com/aws/aws-lambda-go v1.51.1/go.mod h1:dpMpZgvWx5vuQJfBt0zqBha60q7Dd7RfgJv23DymV8A=
github.com/aws/aws-sdk-go v1.55.8 h1:JRmEUbU52aJQZ2AjX4q4Wu7t4uZjOu71uyNmaWlUkJQ=
github.com/aws/aws-sdk-go v1.55.8/go.mod h1:ZkViS9AqA6otK+JBBNH2++sx1sgxrPKcSzPPvQkUtXk=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s=
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
97 changes: 97 additions & 0 deletions cmd/cognito-triggers/post-confirmation/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package main

import (
"context"
"os"
"time"

"github.com/aws/aws-lambda-go/events"
"github.com/aws/aws-lambda-go/lambda"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/dynamodb"
"github.com/aws/aws-sdk-go/service/dynamodb/dynamodbattribute"
)

// UserProfile represents the DynamoDB user profile structure
type UserProfile struct {
PK string `dynamodbav:"PK"`
SK string `dynamodbav:"entity_id"`
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛑 Logic Error: Incorrect DynamoDB attribute tag causes data insertion failure. The struct field SK has tag dynamodbav:"entity_id" but DynamoDB operations expect SK as the sort key attribute name.

Suggested change
SK string `dynamodbav:"entity_id"`
SK string `dynamodbav:"SK"`

EntityType string `dynamodbav:"EntityType"`
Username string `dynamodbav:"Username"`
Email string `dynamodbav:"Email"`
Name string `dynamodbav:"Name"`
PasswordHash string `dynamodbav:"PasswordHash"` // Empty for Cognito users
CreatedAt time.Time `dynamodbav:"CreatedAt"`
UpdatedAt time.Time `dynamodbav:"UpdatedAt"`
}

var (
dynamoClient *dynamodb.DynamoDB
tableName string
)

func init() {
// Initialize DynamoDB client
sess := session.Must(session.NewSession())
dynamoClient = dynamodb.New(sess)

// Get table name from environment variable
tableName = os.Getenv("DYNAMODB_TABLE")
if tableName == "" {
tableName = "entities-table" // Fallback default
}
}

// Handler is the Lambda function handler for Cognito Post Confirmation trigger
// This function is invoked automatically by Cognito after a user successfully confirms their email
func Handler(ctx context.Context, event events.CognitoEventUserPoolsPostConfirmation) (events.CognitoEventUserPoolsPostConfirmation, error) {
// Extract user attributes from Cognito event
username := event.UserName
email := event.Request.UserAttributes["email"]

// Create user profile in DynamoDB
now := time.Now()
userProfile := UserProfile{
PK: "User",
SK: "USER#" + username,
EntityType: "User",
Username: username,
Email: email,
Name: username, // Default to username, user can update later
PasswordHash: "", // Empty - Cognito manages passwords
CreatedAt: now,
UpdatedAt: now,
}

// Marshal to DynamoDB attribute values
item, err := dynamodbattribute.MarshalMap(userProfile)
if err != nil {
// Log error but don't fail the signup process
// Cognito will still confirm the user
return event, err
}

// Put item in DynamoDB
input := &dynamodb.PutItemInput{
TableName: aws.String(tableName),
Item: item,
// Use condition to prevent overwriting existing users
ConditionExpression: aws.String("attribute_not_exists(PK)"),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛑 Logic Error: Incorrect condition expression blocks user profile creation. The expression attribute_not_exists(PK) will fail when any user exists because all users share PK="User". This prevents new user registration after the first user.

Suggested change
ConditionExpression: aws.String("attribute_not_exists(PK)"),
ConditionExpression: aws.String("attribute_not_exists(PK) AND attribute_not_exists(SK)"),

}

_, err = dynamoClient.PutItem(input)
if err != nil {
// Check if it's a conditional check failure (user already exists)
// This can happen if user confirms multiple times
// Don't fail the process, just log and continue
return event, err
}

// Return the event unmodified - required by Cognito
return event, nil
}

func main() {
lambda.Start(Handler)
}
Loading
Loading