diff --git a/.github/workflows/cicd.yaml b/.github/workflows/cicd.yaml index 89763d1..2d16d17 100644 --- a/.github/workflows/cicd.yaml +++ b/.github/workflows/cicd.yaml @@ -120,6 +120,8 @@ jobs: test-and-build-api-users: name: Test and Build API Users runs-on: ubuntu-latest + env: + JWT_SECRET: ${{ secrets.JWT_SECRET }} steps: - name: Checkout code uses: actions/checkout@v4 @@ -170,6 +172,62 @@ jobs: name: api_users_lambda path: ./api-users/api_users_lambda.zip + test-and-build-authorizer: + name: Test and Build Authorizer + runs-on: ubuntu-latest + needs: test-and-build-api-users + env: + JWT_SECRET: ${{ secrets.JWT_SECRET }} + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v2 + with: + go-version: '1.22.4' + + - name: Cache dependencies + uses: actions/cache@v2 + with: + path: ~/go/pkg/mod + key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go- + + - name: Install dependencies + working-directory: ./authorizer + run: go mod download + + - name: Run golangci-lint + uses: golangci/golangci-lint-action@v6.0.1 + with: + working-directory: . + args: --out-format colored-line-number ./authorizer/... + + - name: Run tests + working-directory: ./authorizer + run: go test -coverprofile=coverage.out ./... + + - name: Upload coverage to codecov + uses: codecov/codecov-action@v2 + with: + token: ${{ secrets.CODECOV_TOKEN }} + + - name: Build Authorizer binary + working-directory: ./authorizer + run: GOOS=linux GOARCH=amd64 go build -o bootstrap main.go + + - name: Zip Authorizer binary + working-directory: ./authorizer + run: zip authorizer_lambda.zip bootstrap + + - name: Upload Authorizer artifact + uses: actions/upload-artifact@v2 + with: + name: authorizer_lambda + path: ./authorizer/authorizer_lambda.zip + deploy: name: Deploy runs-on: ubuntu-latest @@ -195,6 +253,11 @@ jobs: with: name: api_users_lambda path: ./terraform + - name: Download Authorizer artifact + uses: actions/download-artifact@v2 + with: + name: authorizer_lambda + path: ./terraform - name: Set up Terraform uses: hashicorp/setup-terraform@v1 diff --git a/api-users/controllers/user_controller.go b/api-users/controllers/user_controller.go index c2de0b4..913e874 100644 --- a/api-users/controllers/user_controller.go +++ b/api-users/controllers/user_controller.go @@ -6,4 +6,5 @@ type UserController interface { RegisterUser(c *gin.Context) GetAllUsers(c *gin.Context) GetUserByID(c *gin.Context) + LogInUser(c *gin.Context) } diff --git a/api-users/controllers/user_controller_impl.go b/api-users/controllers/user_controller_impl.go index 61e3e83..362e255 100644 --- a/api-users/controllers/user_controller_impl.go +++ b/api-users/controllers/user_controller_impl.go @@ -14,6 +14,49 @@ type UserControllerImpl struct { userService services.UserService } +// LogInUser implements UserController. +func (u *UserControllerImpl) LogInUser(c *gin.Context) { + loginRequest := request.LogInUserRequest{} + + err := c.ShouldBindJSON(&loginRequest) + if err != nil { + logrus.WithError(err).Error("[UserControllerImpl.LogInUser] Error binding JSON") + errorResponse := response.BaseResponse{ + Code: 400, + Status: "error", + Message: "Invalid request body", + Data: nil, + } + + c.JSON(400, errorResponse) + return + } + + loginResponse, err := u.userService.LogInUser(loginRequest) + if err != nil { + logrus.WithError(err).Error("[UserControllerImpl.LogInUser] Error logging in user") + errorResponse := response.BaseResponse{ + Code: 500, + Status: "error", + Message: "Error logging in user", + Data: nil, + } + + c.JSON(500, errorResponse) + return + } + + c.Header("Authorization", fmt.Sprintf("Bearer %s", loginResponse.Token)) + webResponse := response.BaseResponse{ + Code: 200, + Status: "success", + Message: "User logged in successfully", + Data: loginResponse, + } + + c.JSON(200, webResponse) +} + // GetAllUsers implements UserController. func (u *UserControllerImpl) GetAllUsers(c *gin.Context) { users, err := u.userService.GetAllUsers() diff --git a/api-users/go.mod b/api-users/go.mod index ae71f5a..be2e1e2 100644 --- a/api-users/go.mod +++ b/api-users/go.mod @@ -1,3 +1,5 @@ module github.com/dieg0code/api-users go 1.22.4 + +require github.com/golang-jwt/jwt/v5 v5.2.1 diff --git a/api-users/go.sum b/api-users/go.sum new file mode 100644 index 0000000..f56d3e6 --- /dev/null +++ b/api-users/go.sum @@ -0,0 +1,2 @@ +github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= +github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= diff --git a/api-users/main.go b/api-users/main.go index 8fa9013..b6f6a70 100644 --- a/api-users/main.go +++ b/api-users/main.go @@ -31,9 +31,10 @@ func init() { validator := validator.New() passwordHaher := utils.NewPasswordHasher() + jwtUtils := utils.NewJWTUtils() // Instance Service - userService := services.NewUserServiceImpl(userRepo, validator, passwordHaher) + userService := services.NewUserServiceImpl(userRepo, validator, passwordHaher, jwtUtils) // Instance controller userController := controllers.NewUserControllerImpl(userService) diff --git a/api-users/repository/user_repository.go b/api-users/repository/user_repository.go index de93c0b..c66003e 100644 --- a/api-users/repository/user_repository.go +++ b/api-users/repository/user_repository.go @@ -6,4 +6,5 @@ type UserRepository interface { GetAll() ([]models.User, error) GetByID(id string) (models.User, error) Create(user models.User) (models.User, error) + GetByEmail(email string) (models.User, error) } diff --git a/api-users/repository/user_repository_impl.go b/api-users/repository/user_repository_impl.go index 09c4743..4394c05 100644 --- a/api-users/repository/user_repository_impl.go +++ b/api-users/repository/user_repository_impl.go @@ -16,6 +16,39 @@ type UserRepositoryImpl struct { tableName string } +// GetByEmail implements UserRepository. +func (u *UserRepositoryImpl) GetByEmail(email string) (models.User, error) { + input := &dynamodb.QueryInput{ + TableName: &u.tableName, + IndexName: aws.String("EmailIndex"), + KeyConditionExpression: aws.String("Email = :email"), + ExpressionAttributeValues: map[string]*dynamodb.AttributeValue{ + ":email": { + S: aws.String(email), + }, + }, + } + + result, err := u.db.Query(input) + if err != nil { + logrus.WithError(err).Error("[UserRepositoryImpl.GetByEmail] error getting user") + return models.User{}, errors.New("error getting user") + } + + if len(result.Items) == 0 { + return models.User{}, errors.New("user not found") + } + + var user models.User + err = dynamodbattribute.UnmarshalMap(result.Items[0], &user) + if err != nil { + logrus.WithError(err).Error("[UserRepositoryImpl.GetByEmail] error unmarshalling user") + return models.User{}, errors.New("error getting user") + } + + return user, nil +} + // Create implements UserRepository. func (u *UserRepositoryImpl) Create(user models.User) (models.User, error) { input := &dynamodb.PutItemInput{ diff --git a/api-users/router/router.go b/api-users/router/router.go index 3561676..c1200d1 100644 --- a/api-users/router/router.go +++ b/api-users/router/router.go @@ -38,6 +38,7 @@ func (r *Router) InitRoutes() *Router { userRoute.GET("", r.UserController.GetAllUsers) userRoute.GET("/:userID", r.UserController.GetUserByID) userRoute.POST("", r.UserController.RegisterUser) + userRoute.POST("/login", r.UserController.LogInUser) } } diff --git a/api-users/services/user_service.go b/api-users/services/user_service.go index 6b8e3ec..eae82ad 100644 --- a/api-users/services/user_service.go +++ b/api-users/services/user_service.go @@ -10,4 +10,5 @@ type UserService interface { RegisterUser(createUserReq request.CreateUserRequest) (models.User, error) GetAllUsers() ([]response.UserResponse, error) GetUserByID(id string) (response.UserResponse, error) + LogInUser(logInUserReq request.LogInUserRequest) (response.LogInUserResponse, error) } diff --git a/api-users/services/user_service_impl.go b/api-users/services/user_service_impl.go index db025b7..f8fceff 100644 --- a/api-users/services/user_service_impl.go +++ b/api-users/services/user_service_impl.go @@ -17,6 +17,46 @@ type UserServiceImpl struct { UserRepository repository.UserRepository Validator *validator.Validate PasswordHasher utils.PasswordHasher + JWTUtils utils.JWTUtils +} + +// LogInUser implements UserService. +func (u *UserServiceImpl) LogInUser(logInUserReq request.LogInUserRequest) (response.LogInUserResponse, error) { + err := u.Validator.Struct(logInUserReq) + if err != nil { + logrus.WithError(err).Error("[UserServiceImpl.LogInUser] error validating login user request") + return response.LogInUserResponse{}, err + } + + user, err := u.UserRepository.GetByEmail(logInUserReq.Email) + if err != nil { + logrus.WithError(err).Error("[UserServiceImpl.LogInUser] error getting user by email") + return response.LogInUserResponse{}, err + } + + err = u.PasswordHasher.ComparePassword(user.Password, logInUserReq.Password) + if err != nil { + logrus.WithError(err).Error("[UserServiceImpl.LogInUser] error comparing password") + return response.LogInUserResponse{}, errors.New("invalid password") + } + + token, err := u.JWTUtils.GenerateToken(user.UserID) + if err != nil { + logrus.WithError(err).Error("[UserServiceImpl.LogInUser] error generating token") + return response.LogInUserResponse{}, err + } + + logInUserResponse := response.LogInUserResponse{ + Token: token, + } + + err = u.Validator.Struct(logInUserResponse) + if err != nil { + logrus.WithError(err).Error("[UserServiceImpl.LogInUser] error validating login user response") + return response.LogInUserResponse{}, err + } + + return logInUserResponse, nil } // GetAllUsers implements UserService. @@ -105,10 +145,11 @@ func (u *UserServiceImpl) RegisterUser(createUserReq request.CreateUserRequest) return user, nil } -func NewUserServiceImpl(userRepository repository.UserRepository, validator *validator.Validate, passwordHaher utils.PasswordHasher) UserService { +func NewUserServiceImpl(userRepository repository.UserRepository, validator *validator.Validate, passwordHaher utils.PasswordHasher, jwtUtils utils.JWTUtils) UserService { return &UserServiceImpl{ UserRepository: userRepository, Validator: validator, PasswordHasher: passwordHaher, + JWTUtils: jwtUtils, } } diff --git a/api-users/utils/jwt_utils.go b/api-users/utils/jwt_utils.go new file mode 100644 index 0000000..2b620aa --- /dev/null +++ b/api-users/utils/jwt_utils.go @@ -0,0 +1,5 @@ +package utils + +type JWTUtils interface { + GenerateToken(userID string) (string, error) +} diff --git a/api-users/utils/jwt_utils_impl.go b/api-users/utils/jwt_utils_impl.go new file mode 100644 index 0000000..2e7f8ee --- /dev/null +++ b/api-users/utils/jwt_utils_impl.go @@ -0,0 +1,33 @@ +package utils + +import ( + "os" + "time" + + "github.com/golang-jwt/jwt/v5" +) + +var jwtSecret = []byte(os.Getenv("JWT_SECRET")) + +type JWTUtilsImpl struct{} + +// GenerateToken implements JWTUtils. +func (j *JWTUtilsImpl) GenerateToken(userID string) (string, error) { + claims := jwt.MapClaims{ + "user_id": userID, + "exp": time.Now().Add(time.Hour * 72).Unix(), + } + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + + signedToken, err := token.SignedString(jwtSecret) + if err != nil { + return "", err + } + + return signedToken, nil +} + +func NewJWTUtils() JWTUtils { + return &JWTUtilsImpl{} +} diff --git a/api-users/utils/password_hasher.go b/api-users/utils/password_hasher.go index a8bafdf..08f0cd2 100644 --- a/api-users/utils/password_hasher.go +++ b/api-users/utils/password_hasher.go @@ -2,4 +2,5 @@ package utils type PasswordHasher interface { HashPassword(password string) (string, error) + ComparePassword(hashedPassword, password string) error } diff --git a/api-users/utils/password_hasher_impl.go b/api-users/utils/password_hasher_impl.go index 3b98932..2627cd1 100644 --- a/api-users/utils/password_hasher_impl.go +++ b/api-users/utils/password_hasher_impl.go @@ -7,6 +7,11 @@ import ( type PasswordHasherImpl struct{} +// ComparePassword implements PasswordHasher. +func (p *PasswordHasherImpl) ComparePassword(hashedPassword string, password string) error { + return bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password)) +} + // HashPassword implements PasswordHasher. func (p *PasswordHasherImpl) HashPassword(password string) (string, error) { hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) diff --git a/authorizer/auth/jwt.go b/authorizer/auth/jwt.go new file mode 100644 index 0000000..21d6a5b --- /dev/null +++ b/authorizer/auth/jwt.go @@ -0,0 +1,7 @@ +package auth + +import "github.com/golang-jwt/jwt/v5" + +type JWTValidator interface { + ValidateToken(tokenString string, secret []byte) (jwt.MapClaims, error) +} diff --git a/authorizer/auth/jwt_impl.go b/authorizer/auth/jwt_impl.go new file mode 100644 index 0000000..d724fa4 --- /dev/null +++ b/authorizer/auth/jwt_impl.go @@ -0,0 +1,38 @@ +package auth + +import ( + "errors" + + "github.com/golang-jwt/jwt/v5" +) + +type jwtValidator struct{} + +// ValidateToken implements JWTValidator. +func (v *jwtValidator) ValidateToken(tokenString string, secret []byte) (jwt.MapClaims, error) { + token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok || token.Method.Alg() != jwt.SigningMethodHS256.Alg() { + return nil, errors.New("unexpected signing method") + } + return secret, nil + }) + + if err != nil { + return nil, err + } + + if !token.Valid { + return nil, errors.New("invalid token") + } + + claims, ok := token.Claims.(jwt.MapClaims) + if !ok { + return nil, errors.New("invalid token claims") + } + + return claims, nil +} + +func NewJWTValidator() JWTValidator { + return &jwtValidator{} +} diff --git a/authorizer/aws/policy.go b/authorizer/aws/policy.go new file mode 100644 index 0000000..a2fbe47 --- /dev/null +++ b/authorizer/aws/policy.go @@ -0,0 +1,7 @@ +package aws + +import "github.com/aws/aws-lambda-go/events" + +type Policy interface { + GeneratePolicy(principalID, effect, resource string) events.APIGatewayCustomAuthorizerResponse +} diff --git a/authorizer/aws/policy_impl.go b/authorizer/aws/policy_impl.go new file mode 100644 index 0000000..fc8fb4e --- /dev/null +++ b/authorizer/aws/policy_impl.go @@ -0,0 +1,30 @@ +package aws + +import "github.com/aws/aws-lambda-go/events" + +type PolicyImpl struct{} + +// GeneratePolicy implements Policy. +func (p *PolicyImpl) GeneratePolicy(principalID string, effect string, resource string) events.APIGatewayCustomAuthorizerResponse { + authResponse := events.APIGatewayCustomAuthorizerResponse{PrincipalID: principalID} + if effect != "" && resource != "" { + authResponse.PolicyDocument = events.APIGatewayCustomAuthorizerPolicy{ + Version: "2012-10-17", + Statement: []events.IAMPolicyStatement{ + { + Action: []string{"execute-api:Invoke"}, + Effect: effect, + Resource: []string{resource}, + }, + }, + } + } + authResponse.Context = map[string]interface{}{ + "user_id": principalID, + } + return authResponse +} + +func NewPolicyImpl() Policy { + return &PolicyImpl{} +} diff --git a/authorizer/go.mod b/authorizer/go.mod new file mode 100644 index 0000000..af0caef --- /dev/null +++ b/authorizer/go.mod @@ -0,0 +1,3 @@ +module github.com/dieg0code/authorizer + +go 1.22.4 diff --git a/authorizer/handler/handler.go b/authorizer/handler/handler.go new file mode 100644 index 0000000..179d4f1 --- /dev/null +++ b/authorizer/handler/handler.go @@ -0,0 +1,11 @@ +package handler + +import ( + "context" + + "github.com/aws/aws-lambda-go/events" +) + +type AuthorizerHandler interface { + HandleAuthorizer(ctx context.Context, event events.APIGatewayCustomAuthorizerRequest) (events.APIGatewayCustomAuthorizerResponse, error) +} diff --git a/authorizer/handler/handler_impl.go b/authorizer/handler/handler_impl.go new file mode 100644 index 0000000..5e54a76 --- /dev/null +++ b/authorizer/handler/handler_impl.go @@ -0,0 +1,59 @@ +package handler + +import ( + "context" + "errors" + "os" + "strings" + "time" + + "github.com/aws/aws-lambda-go/events" + "github.com/dieg0code/authorizer/auth" + "github.com/dieg0code/authorizer/aws" + "github.com/sirupsen/logrus" +) + +var jwtSecret = []byte(os.Getenv("JWT_SECRET")) + +type AuthorizerHandlerImpl struct { + Policy aws.Policy + JWTValidator auth.JWTValidator +} + +// HandleAuthorizer implements AuthorizerHandler. +func (a *AuthorizerHandlerImpl) HandleAuthorizer(ctx context.Context, event events.APIGatewayCustomAuthorizerRequest) (events.APIGatewayCustomAuthorizerResponse, error) { + token := strings.TrimPrefix(event.AuthorizationToken, "Bearer ") + + claims, err := a.JWTValidator.ValidateToken(token, jwtSecret) + if err != nil { + logrus.WithError(err).Error("error validating token") + return events.APIGatewayCustomAuthorizerResponse{}, errors.New("invalid token") + } + + // Check if the token has expired + exp, ok := claims["exp"].(float64) + if !ok { + logrus.Error("invalid exp in token") + return events.APIGatewayCustomAuthorizerResponse{}, errors.New("invalid exp in token") + } + + if time.Now().Unix() > int64(exp) { + logrus.Error("token has expired") + return events.APIGatewayCustomAuthorizerResponse{}, errors.New("token has expired") + } + + userID, ok := claims["user_id"].(string) + if !ok { + logrus.Error("invalid user_id in token") + return events.APIGatewayCustomAuthorizerResponse{}, errors.New("invalid user_id in token") + } + + return a.Policy.GeneratePolicy(userID, "Allow", event.MethodArn), nil +} + +func NewAuthorizerHandler(policy aws.Policy, jwtValidator auth.JWTValidator) AuthorizerHandler { + return &AuthorizerHandlerImpl{ + Policy: policy, + JWTValidator: jwtValidator, + } +} diff --git a/authorizer/main.go b/authorizer/main.go new file mode 100644 index 0000000..0767700 --- /dev/null +++ b/authorizer/main.go @@ -0,0 +1,20 @@ +package main + +import ( + "github.com/aws/aws-lambda-go/lambda" + "github.com/dieg0code/authorizer/auth" + "github.com/dieg0code/authorizer/aws" + "github.com/dieg0code/authorizer/handler" + "github.com/sirupsen/logrus" +) + +func main() { + logrus.Info("Starting authorizer...") + jwtValidator := auth.NewJWTValidator() + policy := aws.NewPolicyImpl() + handler := handler.NewAuthorizerHandler(policy, jwtValidator) + + lambda.Start(handler.HandleAuthorizer) + + logrus.Info("Authorizer started") +} diff --git a/go.work b/go.work index c535b61..70d2b42 100644 --- a/go.work +++ b/go.work @@ -3,6 +3,7 @@ go 1.22.4 use ( ./api-products ./api-users + ./authorizer ./scraper ./shared ) diff --git a/go.work.sum b/go.work.sum index e166f00..8346ca1 100644 --- a/go.work.sum +++ b/go.work.sum @@ -12,6 +12,7 @@ github.com/go-chi/chi/v5 v5.0.8/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITL github.com/gofiber/fiber/v2 v2.52.1/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/gomarkdown/markdown v0.0.0-20231222211730-1d6d20845b47/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= diff --git a/shared/json/request/create_user.go b/shared/json/request/create_user.go index 8dbe515..6f1560f 100644 --- a/shared/json/request/create_user.go +++ b/shared/json/request/create_user.go @@ -3,6 +3,6 @@ package request type CreateUserRequest struct { Username string `json:"username" validate:"required,min=3,max=50"` Email string `json:"email" validate:"required,email"` - Password string `json:"password" validate:"required,min=6,max=50"` + Password string `json:"password" validate:"required,min=6,max=20"` Role string `json:"role" validate:"required,oneof=admin user"` } diff --git a/shared/json/request/login_user.go b/shared/json/request/login_user.go new file mode 100644 index 0000000..96bd0e9 --- /dev/null +++ b/shared/json/request/login_user.go @@ -0,0 +1,6 @@ +package request + +type LogInUserRequest struct { + Email string `json:"email" validate:"required,email"` + Password string `json:"password" validate:"required,min=6,max=20"` +} diff --git a/shared/json/response/login_response.go b/shared/json/response/login_response.go new file mode 100644 index 0000000..83b4b23 --- /dev/null +++ b/shared/json/response/login_response.go @@ -0,0 +1,5 @@ +package response + +type LogInUserResponse struct { + Token string `json:"token" validate:"required"` +} diff --git a/terraform/api_gateway.tf b/terraform/api_gateway.tf index 5706e88..2ef66ef 100644 --- a/terraform/api_gateway.tf +++ b/terraform/api_gateway.tf @@ -45,6 +45,13 @@ resource "aws_api_gateway_resource" "user" { path_part = "{userId}" } +# Resource for API Gateway /api/v1/users/login endpoint +resource "aws_api_gateway_resource" "user_login" { + rest_api_id = aws_api_gateway_rest_api.api.id + parent_id = aws_api_gateway_resource.users.id + path_part = "login" +} + # Method for GET /api/v1/products endpoint resource "aws_api_gateway_method" "get_products" { rest_api_id = aws_api_gateway_rest_api.api.id @@ -61,12 +68,22 @@ resource "aws_api_gateway_method" "get_product" { authorization = "NONE" } +# Authorizer for API Gateway +resource "aws_api_gateway_authorizer" "jwt_authorizer" { + rest_api_id = aws_api_gateway_rest_api.api.id + name = "jwt_authorizer" + type = "TOKEN" + authorizer_uri = aws_lambda_function.authorizer.invoke_arn + identity_source = "method.request.header.Authorization" +} + # Method for POST /api/v1/products endpoint resource "aws_api_gateway_method" "post_products" { rest_api_id = aws_api_gateway_rest_api.api.id resource_id = aws_api_gateway_resource.products.id http_method = "POST" - authorization = "NONE" + authorization = "CUSTOM" + authorizer_id = aws_api_gateway_authorizer.jwt_authorizer.id } # Method for GET /api/v1/users endpoint @@ -79,17 +96,25 @@ resource "aws_api_gateway_method" "get_users" { # Method for GET /api/v1/users/{userId} endpoint resource "aws_api_gateway_method" "get_user" { - rest_api_id = aws_api_gateway_rest_api.api.id - resource_id = aws_api_gateway_resource.user.id - http_method = "GET" + rest_api_id = aws_api_gateway_rest_api.api.id + resource_id = aws_api_gateway_resource.user.id + http_method = "GET" authorization = "NONE" } # Method for POST /api/v1/users endpoint resource "aws_api_gateway_method" "post_users" { - rest_api_id = aws_api_gateway_rest_api.api.id - resource_id = aws_api_gateway_resource.users.id - http_method = "POST" + rest_api_id = aws_api_gateway_rest_api.api.id + resource_id = aws_api_gateway_resource.users.id + http_method = "POST" + authorization = "NONE" +} + +# Method for POST /api/v1/users/login endpoint +resource "aws_api_gateway_method" "post_users_login" { + rest_api_id = aws_api_gateway_rest_api.api.id + resource_id = aws_api_gateway_resource.user_login.id + http_method = "POST" authorization = "NONE" } @@ -159,6 +184,17 @@ resource "aws_api_gateway_integration" "post_users_lambda_integration" { uri = aws_lambda_function.api_users.invoke_arn } +# Integration for POST /api/v1/users/login endpoint +resource "aws_api_gateway_integration" "post_users_login_lambda_integration" { + rest_api_id = aws_api_gateway_rest_api.api.id + resource_id = aws_api_gateway_resource.user_login.id + http_method = aws_api_gateway_method.post_users_login.http_method + + integration_http_method = "POST" + type = "AWS_PROXY" + uri = aws_lambda_function.api_users.invoke_arn +} + # Invoke permission for API Gateway to invoke Lambda - Products resource "aws_lambda_permission" "api_gateway" { statement_id = "AllowAPIGatewayInvoke" @@ -177,6 +213,15 @@ resource "aws_lambda_permission" "api_gateway_users" { source_arn = "${aws_api_gateway_rest_api.api.execution_arn}/*/*" } +# Invoke permission for API Gateway to invoke Lambda - Authorizer +resource "aws_lambda_permission" "api_gateway_authorizer" { + statement_id = "AllowAPIGatewayInvoke" + action = "lambda:InvokeFunction" + function_name = aws_lambda_function.authorizer.function_name + principal = "apigateway.amazonaws.com" + source_arn = "${aws_api_gateway_rest_api.api.execution_arn}/*/*" +} + resource "aws_api_gateway_deployment" "api_deployment" { depends_on = [ aws_api_gateway_integration.products_lambda_integration, @@ -184,10 +229,12 @@ resource "aws_api_gateway_deployment" "api_deployment" { aws_api_gateway_integration.post_products_lambda_integration, aws_api_gateway_integration.users_lambda_integration, aws_api_gateway_integration.user_lambda_integration, - aws_api_gateway_integration.post_users_lambda_integration + aws_api_gateway_integration.post_users_lambda_integration, + aws_api_gateway_integration.post_users_login_lambda_integration ] rest_api_id = aws_api_gateway_rest_api.api.id + description = "API Scraper Deployment 16/08/2024" } resource "aws_api_gateway_stage" "api_stage" { @@ -198,7 +245,7 @@ resource "aws_api_gateway_stage" "api_stage" { depends_on = [ aws_api_gateway_deployment.api_deployment ] - + } output "api_gateway_invoke_url" { diff --git a/terraform/dynamodb.tf b/terraform/dynamodb.tf index 6431428..0ada8bd 100644 --- a/terraform/dynamodb.tf +++ b/terraform/dynamodb.tf @@ -1,25 +1,40 @@ - -# DynamoDB table for products resource "aws_dynamodb_table" "products_table" { name = "Products" - billing_mode = "PAY_PER_REQUEST" + billing_mode = "PROVISIONED" hash_key = "ProductID" - attribute { name = "ProductID" type = "S" } + + write_capacity = 10 + read_capacity = 10 } resource "aws_dynamodb_table" "users_table" { name = "Users" - billing_mode = "PAY_PER_REQUEST" + billing_mode = "PROVISIONED" hash_key = "UserID" attribute { name = "UserID" type = "S" } -} + attribute { + name = "Email" + type = "S" + } + + global_secondary_index { + name = "EmailIndex" + hash_key = "Email" + projection_type = "ALL" + write_capacity = 10 + read_capacity = 10 + } + + write_capacity = 10 + read_capacity = 10 +} diff --git a/terraform/lambda.tf b/terraform/lambda.tf index 733847b..f2e659a 100644 --- a/terraform/lambda.tf +++ b/terraform/lambda.tf @@ -23,7 +23,7 @@ resource "aws_lambda_function" "scraper" { handler = "bootstrap" runtime = "provided.al2023" memory_size = 128 - timeout = 150 + timeout = 240 source_code_hash = filebase64sha256("scraper_lambda.zip") @@ -51,3 +51,16 @@ resource "aws_lambda_function" "api_users" { } } } + +resource "aws_lambda_function" "authorizer" { + filename = "authorizer_lambda.zip" + function_name = "authorizer" + role = aws_iam_role.lambda_role.arn + handler = "bootstrap" + runtime = "provided.al2023" + memory_size = 128 + timeout = 90 + + source_code_hash = filebase64sha256("authorizer_lambda.zip") + +}