Skip to content
Draft
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
118 changes: 118 additions & 0 deletions getUserById/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package main

import (
"context"
"encoding/json"
"log"
"net/http"

"feature-flag-backend/layer/database"
"feature-flag-backend/layer/jwt"
middleware "feature-flag-backend/layer/middlewares"
"feature-flag-backend/layer/models"
"feature-flag-backend/layer/utils"
"github.com/aws/aws-lambda-go/events"
"github.com/aws/aws-lambda-go/lambda"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/dynamodb"
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
)

func getUserById(ctx context.Context, db *dynamodb.Client, userId string) (*models.User, error) {
input := &dynamodb.GetItemInput{
TableName: aws.String(utils.USER_TABLE_NAME),
Key: map[string]types.AttributeValue{
"id": &types.AttributeValueMemberS{
Value: userId,
},
},
}

result, err := db.GetItem(ctx, input)
if err != nil {
return nil, err
}

if len(result.Item) == 0 {
return nil, nil
}

var user models.User
err = database.UnmarshalMap(result.Item, &user)
if err != nil {
return nil, err
}

return &user, nil
}

func handler(ctx context.Context, req events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
db := database.CreateDynamoDB()

utils.CheckRequestAllowed(ctx, db, utils.ConcurrencyDisablingLambda)

corsResponse, err, passed := middleware.HandleCORS(req)
if !passed {
return corsResponse, err
}

response, _, err := jwt.JWTMiddleware()(req)
if err != nil || response.StatusCode != http.StatusOK {
return response, err
}

corsHeaders := middleware.GetCORSHeadersV1(req.Headers)

userId := req.PathParameters["userId"]
if userId == "" {
log.Println("userId is required")
clientErrorResponse, _ := utils.ClientError(http.StatusBadRequest, "userId is required")
return clientErrorResponse, nil
}

// TODO: Add role-based access control in Week 3
// Users can only view their own profile unless they're ADMIN

user, err := getUserById(ctx, db, userId)
if err != nil {
log.Printf("Database error: %v", err)
serverErrorResponse, _ := utils.ServerError(err)
return serverErrorResponse, nil
}

if user == nil {
log.Println("User not found")
clientErrorResponse, _ := utils.ClientError(http.StatusNotFound, "User not found")
clientErrorResponse.Headers = corsHeaders
return clientErrorResponse, nil
}

// Don't return password hash
userResponse := utils.UserResponse{
Id: user.Id,
Email: user.Email,
Role: user.Role,
IsActive: user.IsActive,
CreatedAt: user.CreatedAt,
UpdatedAt: user.UpdatedAt,
}

jsonResponse, err := json.Marshal(userResponse)
if err != nil {
log.Printf("Error converting User to JSON: %v", err)
serverErrorResponse, _ := utils.ServerError(err)
return serverErrorResponse, nil
}

response = events.APIGatewayProxyResponse{
StatusCode: http.StatusOK,
Headers: corsHeaders,
Body: string(jsonResponse),
}
return response, nil
}

func main() {
lambda.Start(handler)
}

2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ require (
github.com/google/uuid v1.3.0
github.com/joho/godotenv v1.5.1
github.com/stretchr/testify v1.8.2
golang.org/x/crypto v0.7.0
)

require (
Expand All @@ -39,7 +40,6 @@ require (
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/leodido/go-urn v1.2.4 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
golang.org/x/crypto v0.7.0 // indirect
golang.org/x/net v0.8.0 // indirect
golang.org/x/sys v0.6.0 // indirect
golang.org/x/text v0.8.0 // indirect
Expand Down
61 changes: 60 additions & 1 deletion layer/jwt/jwt.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ var (
)

type JWTUtils struct {
publicKey *rsa.PublicKey
publicKey *rsa.PublicKey
privateKey *rsa.PrivateKey
}

type EnvConfig struct {
Expand Down Expand Up @@ -113,9 +114,67 @@ func (j *JWTUtils) initialize() error {

log.Printf("Successfully initialized JWT utils with public key")
j.publicKey = rsaPublicKey

privateKeyParameterName := ""
switch envConfig.Environment {
case utils.PROD:
privateKeyParameterName = utils.RDS_BACKEND_PRIVATE_KEY_NAME_PROD
case utils.DEV:
privateKeyParameterName = utils.RDS_BACKEND_PRIVATE_KEY_NAME_DEV
default:
privateKeyParameterName = utils.RDS_BACKEND_PRIVATE_KEY_NAME_LOCAL
}

privateKeyString, err := getPublicKeyFromParameterStore(privateKeyParameterName)
if err == nil {
privateKeyString = strings.TrimSpace(privateKeyString)
block, _ := pem.Decode([]byte(privateKeyString))
if block != nil {
privateKey, err := x509.ParsePKCS8PrivateKey(block.Bytes)
if err == nil {
if rsaPrivateKey, ok := privateKey.(*rsa.PrivateKey); ok {
j.privateKey = rsaPrivateKey
log.Printf("Successfully loaded private key for token generation")
}
}
}
} else {
log.Printf("Private key not available (this is OK if only validation is needed): %v", err)
}

return nil
}

func (j *JWTUtils) GenerateToken(userId string, role string) (string, error) {
if j == nil {
return "", errors.New("internal server error")
}

if j.privateKey == nil {
return "", errors.New("private key not available for token generation")
}

now := time.Now()
claims := jwt.MapClaims{
"userId": userId,
"role": role,
"iat": now.Unix(),
"exp": now.Add(24 * 365 * time.Hour).Unix(), // 1 year expiration
}

token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
tokenString, err := token.SignedString(j.privateKey)
if err != nil {
return "", fmt.Errorf("failed to sign token: %w", err)
}

return tokenString, nil
}

func (j *JWTUtils) SetPrivateKey(privateKey *rsa.PrivateKey) {
j.privateKey = privateKey
}

func getPublicKeyFromParameterStore(parameterName string) (string, error) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
Expand Down
14 changes: 14 additions & 0 deletions layer/models/user.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package models

type User struct {
Id string `json:"id" dynamodbav:"id"`
Email string `json:"email" dynamodbav:"email"`
PasswordHash string `json:"passwordHash" dynamodbav:"passwordHash"`
Role string `json:"role" dynamodbav:"role"` // ADMIN, DEVELOPER, VIEWER
CreatedAt int64 `json:"createdAt" dynamodbav:"createdAt"`
CreatedBy string `json:"createdBy" dynamodbav:"createdBy"`
UpdatedAt int64 `json:"updatedAt" dynamodbav:"updatedAt"`
UpdatedBy string `json:"updatedBy" dynamodbav:"updatedBy"`
IsActive bool `json:"isActive" dynamodbav:"isActive"`
}

7 changes: 7 additions & 0 deletions layer/utils/Constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,12 @@ const (
TEST = "TESTING"
FEATURE_FLAG_TABLE_NAME = "featureFlag"
FEATURE_FLAG_USER_MAPPING_TABLE_NAME = "featureFlagUserMapping"
USER_TABLE_NAME = "user"
ENABLED = "ENABLED"
DISABLED = "DISABLED"
ROLE_ADMIN = "ADMIN"
ROLE_DEVELOPER = "DEVELOPER"
ROLE_VIEWER = "VIEWER"
Id = "id"
Name = "name"
Description = "description"
Expand All @@ -26,4 +30,7 @@ const (
RDS_BACKEND_PUBLIC_KEY_NAME_DEV = "STAGING_RDS_BACKEND_PUBLIC_KEY"
RDS_BACKEND_PUBLIC_KEY_NAME_PROD = "PROD_RDS_BACKEND_PUBLIC_KEY"
RDS_BACKEND_PUBLIC_KEY_NAME_LOCAL = "publickey"
RDS_BACKEND_PRIVATE_KEY_NAME_DEV = "STAGING_RDS_BACKEND_PRIVATE_KEY"
RDS_BACKEND_PRIVATE_KEY_NAME_PROD = "PROD_RDS_BACKEND_PRIVATE_KEY"
RDS_BACKEND_PRIVATE_KEY_NAME_LOCAL = "privatekey"
)
8 changes: 8 additions & 0 deletions layer/utils/ErrorOutput.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,11 @@ func ValidateFeatureFlagStatus(status string) bool {
_, found := allowedStatuses[strings.ToUpper(status)]
return found
}

type UserExistsError struct {
Email string
}

func (e *UserExistsError) Error() string {
return "user with email " + e.Email + " already exists"
}
25 changes: 25 additions & 0 deletions layer/utils/RequestResponse.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,28 @@ type FeatureFlagUserMappingResponse struct {
UpdatedAt int64 `json:"updatedAt"`
UpdatedBy string `json:"updatedBy"`
}

type RegisterUserRequest struct {
Email string `json:"email" validate:"required,email"`
Password string `json:"password" validate:"required,min=8"`
Role string `json:"role" validate:"omitempty,oneof=ADMIN DEVELOPER VIEWER"`
}

type LoginUserRequest struct {
Email string `json:"email" validate:"required,email"`
Password string `json:"password" validate:"required"`
}

type UserResponse struct {
Id string `json:"id"`
Email string `json:"email"`
Role string `json:"role"`
IsActive bool `json:"isActive"`
CreatedAt int64 `json:"createdAt"`
UpdatedAt int64 `json:"updatedAt"`
}

type LoginResponse struct {
Token string `json:"token"`
User UserResponse `json:"user"`
}
19 changes: 19 additions & 0 deletions layer/utils/password.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package utils

import (
"golang.org/x/crypto/bcrypt"
)

func HashPassword(password string) (string, error) {
bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return "", err
}
return string(bytes), nil
}

func CheckPasswordHash(password, hash string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
return err == nil
}

Loading