diff --git a/Makefile b/Makefile index d58502e1..f8267a06 100644 --- a/Makefile +++ b/Makefile @@ -8,6 +8,7 @@ SERVICE_TAG=${tag} AWS_REGION=us-west-2 ECR=${AWS_ACCT}.dkr.ecr.us-west-2.amazonaws.com ECS_API_REPO=${ECR}/${API} +INTERNAL_REPO=${ECR}/internal all: build push deploy @@ -24,4 +25,16 @@ push: test-envvars docker push $(ECS_API_REPO):${SERVICE_TAG} deploy: test-envvars - aws ecs --region $(AWS_REGION) update-service --cluster $(ECS_CLUSTER) --service ${API} --force-new-deployment \ No newline at end of file + aws ecs --region $(AWS_REGION) update-service --cluster $(ECS_CLUSTER) --service ${API} --force-new-deployment + +build-internal:test-envvars + GOOS=linux GOARCH=amd64 go build -o ./cmd/internal/main ./cmd/internal/main.go + docker build --platform linux/amd64 -t $(INTERNAL_REPO):${SERVICE_TAG} cmd/internal/ + rm cmd/internal/main + +push-internal:test-envvars + aws ecr get-login-password --region $(AWS_REGION) | docker login --username AWS --password-stdin $(INTERNAL_REPO) + docker push $(INTERNAL_REPO):${SERVICE_TAG} + +deploy-internal: test-envvars + aws ecs --region $(AWS_REGION) update-service --cluster internal --service internal --force-new-deployment \ No newline at end of file diff --git a/api/api.go b/api/api.go index d0d1c8c9..b7ed8bd6 100644 --- a/api/api.go +++ b/api/api.go @@ -29,13 +29,22 @@ func Start(config APIConfig) { baseMiddleware(config.Logger, e) e.GET("/heartbeat", heartbeat) authService := authRoute(config, e) - platformRoute(config, e) + AuthAPIKey(config, e, true) transactRoute(config, authService, e) userRoute(config, authService, e) verificationRoute(config, e) e.Logger.Fatal(e.Start(":" + config.Port)) } +func StartInternal(config APIConfig) { + e := echo.New() + baseMiddleware(config.Logger, e) + e.GET("/heartbeat", heartbeat) + platformRoute(config, e) + AuthAPIKey(config, e, true) + e.Logger.Fatal(e.Start(":" + config.Port)) +} + func baseMiddleware(logger *zerolog.Logger, e *echo.Echo) { e.Use(middleware.CORS()) e.Use(middleware.RequestID()) @@ -64,6 +73,13 @@ func platformRoute(config APIConfig, e *echo.Echo) { handler.RegisterRoutes(e.Group("/platform"), middleware.BearerAuth()) } +func AuthAPIKey(config APIConfig, e *echo.Echo, internal bool) { + a := repository.NewAuth(config.Redis, config.DB) + service := service.NewAPIKeyStrategy(a) + handler := handler.NewAuthAPIKey(service, internal) + handler.RegisterRoutes(e.Group("/apikey")) +} + func transactRoute(config APIConfig, auth service.Auth, e *echo.Echo) { repos := service.TransactionRepos{ Asset: repository.NewAsset(config.DB), diff --git a/api/handler/auth_key.go b/api/handler/auth_key.go new file mode 100644 index 00000000..3aec8802 --- /dev/null +++ b/api/handler/auth_key.go @@ -0,0 +1,91 @@ +package handler + +import ( + "net/http" + + "github.com/String-xyz/string-api/pkg/service" + "github.com/labstack/echo/v4" + "github.com/rs/zerolog" +) + +type AuthAPIKey interface { + Create(c echo.Context) error + Approve(c echo.Context) error + List(c echo.Context) error + RegisterRoutes(g *echo.Group, ms ...echo.MiddlewareFunc) +} + +type authAPIKey struct { + isInternal bool + service service.APIKeyStrategy + logger *zerolog.Logger +} + +func NewAuthAPIKey(service service.APIKeyStrategy, internal bool) AuthAPIKey { + return &authAPIKey{service: service, isInternal: internal} +} + +func (o authAPIKey) Create(c echo.Context) error { + lg := c.Get("logger").(*zerolog.Logger) + key, err := o.service.Create() + if err != nil { + lg.Err(err).Stack().Msg("authKey approve:create") + return echo.NewHTTPError(http.StatusInternalServerError, "Unable to process request") + } + return c.JSON(http.StatusOK, map[string]string{"apiKey": key}) +} + +func (o authAPIKey) List(c echo.Context) error { + if !o.isInternal { + return c.String(http.StatusMethodNotAllowed, "Not Allowed") + } + lg := c.Get("logger").(*zerolog.Logger) + body := struct { + Status string `query:"status"` + Limit int `query:"limit"` + Offset int `query:"offset"` + }{} + err := c.Bind(&body) + if err != nil { + lg.Err(err).Stack().Msg("authKeys list: bind") + return echo.NewHTTPError(http.StatusBadRequest) + } + list, err := o.service.List(body.Limit, body.Offset, body.Status) + if err != nil { + lg.Err(err).Stack().Msg("authKeys list") + return echo.NewHTTPError(http.StatusInternalServerError, "ApiKey Service Failed") + } + return c.JSON(http.StatusCreated, list) +} + +func (o authAPIKey) Approve(c echo.Context) error { + if !o.isInternal { + return c.String(http.StatusMethodNotAllowed, "Not Allowed") + } + lg := c.Get("logger").(*zerolog.Logger) + params := struct { + ID string `param:"id"` + }{} + err := c.Bind(¶ms) + + if err != nil { + lg.Err(err).Stack().Msg("authKey approve:bind") + return echo.NewHTTPError(http.StatusInternalServerError, "Unable to process request") + } + err = o.service.Approve(params.ID) + if err != nil { + lg.Err(err).Stack().Msg("authKey approve:approve") + return echo.NewHTTPError(http.StatusInternalServerError, "Unable to process request") + } + return c.String(http.StatusOK, "Success") +} + +func (o authAPIKey) RegisterRoutes(g *echo.Group, ms ...echo.MiddlewareFunc) { + if g == nil { + panic("no group attached to the authKey handler") + } + g.Use(ms...) + g.POST("", o.Create) + g.GET("", o.List) + g.POST("/:id/approve", o.Approve) +} diff --git a/Dockerfile b/cmd/app/Dockerfile similarity index 82% rename from Dockerfile rename to cmd/app/Dockerfile index 3649a62f..22e11183 100644 --- a/Dockerfile +++ b/cmd/app/Dockerfile @@ -5,7 +5,6 @@ LABEL Description="core string api" EXPOSE 3000 # Copy over the app files -COPY ./tmp/app . +COPY ./main . - -CMD ["./app"] +CMD ["./main"] diff --git a/cmd/internal/Dockerfile b/cmd/internal/Dockerfile new file mode 100644 index 00000000..3736c857 --- /dev/null +++ b/cmd/internal/Dockerfile @@ -0,0 +1,11 @@ +FROM alpine:3.16.0 + +LABEL maintainer="Marlon Monroy" +LABEL Description="internal string api" + +EXPOSE 3000 +# Copy over the files +COPY ./main . + + +CMD ["./main"] diff --git a/cmd/internal/main.go b/cmd/internal/main.go new file mode 100644 index 00000000..68bfe190 --- /dev/null +++ b/cmd/internal/main.go @@ -0,0 +1,35 @@ +package main + +import ( + "os" + + "github.com/String-xyz/string-api/api" + "github.com/String-xyz/string-api/pkg/store" + "github.com/joho/godotenv" + "github.com/rs/zerolog" + "github.com/rs/zerolog/pkgerrors" + "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer" +) + +func main() { + // load .env file + godotenv.Load(".env") // removed the err since in cloud this wont be loaded + tracer.Start() + + defer tracer.Stop() + port := os.Getenv("PORT") + if port == "" { + panic("no port!") + } + + zerolog.ErrorStackMarshaler = pkgerrors.MarshalStack + db := store.MustNewPG() + lg := zerolog.New(os.Stdout) + // setup api + api.StartInternal(api.APIConfig{ + DB: db, + Redis: store.NewRedisStore(), + Port: port, + Logger: &lg, + }) +} diff --git a/infra/internal/dev/.terraform.lock.hcl b/infra/internal/dev/.terraform.lock.hcl new file mode 100644 index 00000000..afce80b2 --- /dev/null +++ b/infra/internal/dev/.terraform.lock.hcl @@ -0,0 +1,22 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/aws" { + version = "4.37.0" + constraints = "4.37.0" + hashes = [ + "h1:fLTymOb7xIdMkjQU1VDzPA5s+d2vNLZ2shpcFPF7KaY=", + "zh:12c2eb60cb1eb0a41d1afbca6fc6f0eed6ca31a12c51858f951a9e71651afbe0", + "zh:1e17482217c39a12e930e71fd2c9af8af577bec6736b184674476ebcaad28477", + "zh:1e8163c3d871bbd54c189bf2fe5e60e556d67fa399e4c88c8e6ee0834525dc33", + "zh:399c41a3e096fd75d487b98b1791f7cea5bd38567ac4e621c930cb67ec45977c", + "zh:40d4329eef2cc130e4cbed7a6345cb053dd258bf6f5f8eb0f8ce777ae42d5a01", + "zh:625db5fa75638d543b418be7d8046c4b76dc753d9d2184daa0faaaaebc02d207", + "zh:7785c8259f12b45d19fa5abdac6268f3b749fe5a35c8be762c27b7a634a4952b", + "zh:8a7611f33cc6422799c217ec2eeb79c779035ef05331d12505a6002bc48582f0", + "zh:9188178235a73c829872d2e82d88ac6d334d8bb01433e9be31615f1c1633e921", + "zh:994895b57bf225232a5fa7422e6ab87d8163a2f0605f54ff6a18cdd71f0aeadf", + "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", + "zh:b57de6903ef30c9f22d38d595d64b4f92a89ea717b65782e1f44f57020ce8b1f", + ] +} diff --git a/infra/internal/dev/Makefile b/infra/internal/dev/Makefile new file mode 100644 index 00000000..850cb8b0 --- /dev/null +++ b/infra/internal/dev/Makefile @@ -0,0 +1,12 @@ +export +AWS_PROFILE=dev-string + +init: + terraform init +plan: + terraform plan +apply: + terraform apply + +destroy: + terraform destroy diff --git a/infra/internal/dev/alb.tf b/infra/internal/dev/alb.tf new file mode 100644 index 00000000..4046fb7a --- /dev/null +++ b/infra/internal/dev/alb.tf @@ -0,0 +1,84 @@ +module "alb_acm" { + source = "../../acm" + domain_name = "admin.${local.root_domain}" + aws_region = "us-west-2" + zone_id = data.aws_route53_zone.root.zone_id + tags = { + Name = "admin-${local.root_domain}-alb" + } +} + +resource "aws_alb" "alb" { + name = "${local.service_name}-alb" + internal = true + drop_invalid_header_fields = true + security_groups = [aws_security_group.ecs_alb_https_sg.id] + subnets = data.terraform_remote_state.vpc.outputs.public_subnets + + tags = { + Name = "${local.service_name}-alb" + } + + lifecycle { + create_before_destroy = true + } +} + +resource "aws_alb_target_group" "ecs_task_target_group" { + name = "${local.service_name}-tg" + port = local.container_port + vpc_id = data.terraform_remote_state.vpc.outputs.id + target_type = "ip" + protocol = "HTTP" + + lifecycle { + create_before_destroy = true + } + + health_check { + path = "/heartbeat" + protocol = "HTTP" + matcher = "200" + interval = 60 + timeout = 30 + unhealthy_threshold = "3" + healthy_threshold = "3" + } + + tags = { + Name = "${local.service_name}-tg" + } +} + +resource "aws_alb_listener" "alb_https_listener" { + load_balancer_arn = aws_alb.alb.arn + port = 443 + protocol = "HTTPS" + ssl_policy = "ELBSecurityPolicy-TLS-1-2-2017-01" + certificate_arn = module.alb_acm.arn + + lifecycle { + create_before_destroy = true + } + + default_action { + type = "forward" + target_group_arn = aws_alb_target_group.ecs_task_target_group.arn + } +} + +resource "aws_alb_listener_rule" "ecs_alb_listener_rule" { + listener_arn = aws_alb_listener.alb_https_listener.arn + priority = 100 + action { + type = "forward" + target_group_arn = aws_alb_target_group.ecs_task_target_group.arn + } + + condition { + host_header { + values = ["admin.${local.root_domain}"] + } + } +} + diff --git a/infra/internal/dev/backend.tf b/infra/internal/dev/backend.tf new file mode 100644 index 00000000..a972abac --- /dev/null +++ b/infra/internal/dev/backend.tf @@ -0,0 +1,35 @@ +locals { + remote_state_bucket = "dev-string-terraform-state" + backend_region = "us-west-2" + vpc_remote_state_key = "vpc.tfstate" +} + +provider "aws" { + region = "us-west-2" +} + +terraform { + required_providers { + aws = { + source = "hashicorp/aws" + version = "4.37.0" + } + } + + backend "s3" { + encrypt = true + key = "internal.tfstate" + bucket = "dev-string-terraform-state" + dynamodb_table = "dev-string-terraform-state-lock" + region = "us-west-2" + } +} + +data "terraform_remote_state" "vpc" { + backend = "s3" + config = { + region = local.backend_region + bucket = local.remote_state_bucket + key = local.vpc_remote_state_key + } +} diff --git a/infra/internal/dev/domain.tf b/infra/internal/dev/domain.tf new file mode 100644 index 00000000..1698c6c2 --- /dev/null +++ b/infra/internal/dev/domain.tf @@ -0,0 +1,14 @@ +data "aws_route53_zone" "root" { + name = local.root_domain +} + +resource "aws_route53_record" "domain" { + name = "admin.${local.root_domain}" + type = "A" + zone_id = data.aws_route53_zone.root.zone_id + alias { + evaluate_target_health = false + name = aws_alb.alb.dns_name + zone_id = aws_alb.alb.zone_id + } +} diff --git a/infra/internal/dev/ecs.tf b/infra/internal/dev/ecs.tf new file mode 100644 index 00000000..8c71ef2a --- /dev/null +++ b/infra/internal/dev/ecs.tf @@ -0,0 +1,59 @@ +resource "aws_ecs_cluster" "cluster" { + name = local.cluster_name +} + +resource "aws_ecs_task_definition" "task_definition" { + container_definitions = local.task_definition + family = local.service_name + cpu = local.cpu + memory = local.memory + requires_compatibilities = ["FARGATE"] + network_mode = "awsvpc" + execution_role_arn = aws_iam_role.task_ecs_role.arn + task_role_arn = aws_iam_role.task_ecs_role.arn +} + +resource "aws_ecr_repository" "repo" { + name = local.service_name + image_tag_mutability = "MUTABLE" + + image_scanning_configuration { + scan_on_push = true + } + + tags = { + Environment = local.env + Name = local.service_name + } +} + +resource "aws_ecs_service" "ecs_service" { + name = local.service_name + task_definition = local.service_name + desired_count = local.desired_task_count + cluster = aws_ecs_cluster.cluster.name + launch_type = "FARGATE" + + network_configuration { + subnets = data.terraform_remote_state.vpc.outputs.public_subnets + security_groups = [aws_security_group.ecs_task_sg.id] + assign_public_ip = true + } + + load_balancer { + container_name = local.service_name + container_port = local.container_port + target_group_arn = aws_alb_target_group.ecs_task_target_group.arn + } + + depends_on = [ + aws_alb_listener_rule.ecs_alb_listener_rule, + aws_iam_role_policy.task_ecs_policy, + aws_ecs_task_definition.task_definition + ] + + tags = { + Environment = local.env + Name = local.service_name + } +} diff --git a/infra/internal/dev/iam_roles.tf b/infra/internal/dev/iam_roles.tf new file mode 100644 index 00000000..7cc7ee22 --- /dev/null +++ b/infra/internal/dev/iam_roles.tf @@ -0,0 +1,62 @@ +data "aws_iam_policy_document" "ecs_task_policy" { + statement { + sid = "AllowECSAndTaskAssumeRole" + actions = ["sts:AssumeRole"] + effect = "Allow" + principals { + type = "Service" + identifiers = ["ecs.amazonaws.com", "ecs-tasks.amazonaws.com"] + } + } +} + +resource "aws_iam_role" "task_ecs_role" { + name = "${local.service_name}-task-ecs-role" + assume_role_policy = data.aws_iam_policy_document.ecs_task_policy.json +} + +data "aws_iam_policy_document" "task_policy" { + statement { + sid = "AllowReadToResourcesInListToTask" + effect = "Allow" + actions = [ + "ecs:*", + "ecr:*" + ] + + resources = ["*"] + } + + statement { + sid = "AllowAccessToSSM" + effect = "Allow" + actions = [ + "ssm:GetParameters" + ] + resources = [ + data.aws_ssm_parameter.datadog.arn, + data.aws_ssm_parameter.db_password.arn, + data.aws_ssm_parameter.db_username.arn, + data.aws_ssm_parameter.db_name.arn, + data.aws_ssm_parameter.db_host.arn, + data.aws_ssm_parameter.redis_host_url.arn, + data.aws_ssm_parameter.redis_auth_token.arn + ] + } + + statement { + sid = "AllowDecrypt" + effect = "Allow" + actions = [ + "kms:Decrypt" + ] + resources = [data.aws_kms_key.kms_key.arn] + } + +} + +resource "aws_iam_role_policy" "task_ecs_policy" { + name = "${local.env}-${local.service_name}-task-ecs-policy" + role = aws_iam_role.task_ecs_role.id + policy = data.aws_iam_policy_document.task_policy.json +} diff --git a/infra/internal/dev/security_group.tf b/infra/internal/dev/security_group.tf new file mode 100644 index 00000000..2c294c1a --- /dev/null +++ b/infra/internal/dev/security_group.tf @@ -0,0 +1,84 @@ +resource "aws_security_group" "ecs_alb_https_sg" { + name = "${local.service_name}-alb-https-sg" + description = "Security group for ALB to cluster" + vpc_id = data.terraform_remote_state.vpc.outputs.id + + ingress { + from_port = 443 + to_port = 443 + protocol = "TCP" + cidr_blocks = ["0.0.0.0/0"] + } + + egress { + from_port = 0 + to_port = 0 + protocol = -1 + cidr_blocks = ["0.0.0.0/0"] + } + + lifecycle { + create_before_destroy = true + } + + tags = { + Name = "${local.service_name}-alb-https-sg" + Environment = local.env + } +} + +resource "aws_security_group" "ecs_task_sg" { + name = "${local.service_name}-task-sg" + vpc_id = data.terraform_remote_state.vpc.outputs.id + ingress { + from_port = local.container_port + to_port = local.container_port + protocol = "TCP" + cidr_blocks = [data.terraform_remote_state.vpc.outputs.cidr_block] + } + + egress { + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + } + + lifecycle { + create_before_destroy = true + } + + tags = { + Name = "${local.service_name}-task-sg" + environment = local.env + } +} + +# Give access to DB through Security group rule +data "aws_security_group" "rds" { + name = "${local.env}-string-write-master-client-rds" + vpc_id = data.terraform_remote_state.vpc.outputs.id +} + +data "aws_security_group" "redis" { + name = "redis-client-redis" + vpc_id = data.terraform_remote_state.vpc.outputs.id +} + +resource "aws_security_group_rule" "rds_to_ecs" { + type = "ingress" + protocol = "TCP" + from_port = local.db_port + to_port = local.db_port + source_security_group_id = aws_security_group.ecs_task_sg.id + security_group_id = data.aws_security_group.rds.id +} + +resource "aws_security_group_rule" "redis_to_ecs" { + type = "ingress" + protocol = "TCP" + from_port = local.redis_port + to_port = local.redis_port + source_security_group_id = aws_security_group.ecs_task_sg.id + security_group_id = data.aws_security_group.redis.id +} \ No newline at end of file diff --git a/infra/internal/dev/ssm.tf b/infra/internal/dev/ssm.tf new file mode 100644 index 00000000..e9e226bd --- /dev/null +++ b/infra/internal/dev/ssm.tf @@ -0,0 +1,31 @@ +data "aws_ssm_parameter" "datadog" { + name = "datadog-key" +} + +data "aws_ssm_parameter" "db_password" { + name = "string-rds-pg-db-password" +} + +data "aws_ssm_parameter" "db_username" { + name = "string-rds-pg-db-username" +} + +data "aws_ssm_parameter" "db_name" { + name = "string-rds-pg-db-name" +} + +data "aws_ssm_parameter" "db_host" { + name = "${local.env}-string-write-db-host-url" +} + +data "aws_ssm_parameter" "redis_auth_token" { + name = "redis-auth-token" +} + +data "aws_ssm_parameter" "redis_host_url" { + name = "redis-host-url" +} + +data "aws_kms_key" "kms_key" { + key_id = "alias/main-kms-key" +} diff --git a/infra/internal/dev/variables.tf b/infra/internal/dev/variables.tf new file mode 100644 index 00000000..79729db4 --- /dev/null +++ b/infra/internal/dev/variables.tf @@ -0,0 +1,165 @@ +locals { + cluster_name = "admin" + env = "dev" + service_name = "admin" + root_domain = "dev.string-api.xyz" + container_port = "3000" + origin_id = "admin-api" + desired_task_count = "1" + db_port = "5432" + redis_port = "6379" + memory = 512 + cpu = 256 + region = "us-west-2" +} + +variable "versioning" { + type = string + default = "latest" +} + +locals { + task_definition = jsonencode([ + { + name = local.service_name + image = "${aws_ecr_repository.repo.repository_url}:${var.versioning}" + essential = true, + dockerLabels = { + "com.datadoghq.ad.instances" : "[{\"host\":\"%%host%%\"}]", + "com.datadoghq.ad.check_names" : "[\"${local.service_name}\"]", + }, + portMappings = [ + { containerPort = 3000 } + ], + secrets = [ + { + name = "DB_USERNAME" + valueFrom = data.aws_ssm_parameter.db_username.arn + }, + { + name = "DB_PASSWORD" + valueFrom = data.aws_ssm_parameter.db_password.arn + }, + { + name = "DB_HOST" + valueFrom = data.aws_ssm_parameter.db_host.arn + }, + { + name = "DB_NAME" + valueFrom = data.aws_ssm_parameter.db_name.arn + }, + { + name = "REDIS_HOST", + valuefrom = data.aws_ssm_parameter.redis_host_url.arn + }, + { + name = "REDIS_PASSWORD", + valuefrom = data.aws_ssm_parameter.redis_auth_token.arn + } + ] + environment = [ + { + name = "PORT" + value = local.container_port + }, + { + name = "REDIS_PORT" + value = local.redis_port + }, + { + name = "DB_PORT", + value = local.db_port + }, + { + name = "ENV" + value = local.env + }, + { + name = "AWS_REGION" + value = local.region + }, + { + name = "AWS_KMS_KEY_ID" + value = data.aws_kms_key.kms_key.key_id + }, + { + name = "OWLRACLE_API_URL" + value = "https://api.owlracle.info/v3/" + }, + { + name = "COINGECKO_API_URL" + value = "https://api.coingecko.com/api/v3/" + } + ], + logConfiguration = { + logDriver = "awsfirelens" + secretOptions = [{ + name = "apiKey", + valueFrom = data.aws_ssm_parameter.datadog.arn + }] + options = { + Name = "datadog" + "dd_service" = "${local.service_name}" + "Host" = "http-intake.logs.datadoghq.com" + "dd_source" = "${local.service_name}" + "dd_message_key" = "log" + "dd_tags" = "project:${local.service_name}" + "TLS" = "on" + "provider" = "ecs" + } + } + }, + { + name = "datadog-agent" + image = "public.ecr.aws/datadog/agent:latest" + essential = true + secrets = [{ + name = "DD_API_KEY" + valueFrom = data.aws_ssm_parameter.datadog.arn + }], + environment = [ + { + name = "DD_APM_ENABLED" + value = "true" + }, + { + name = "DD_SITE" + value = "datadoghq.com" + }, + { + name = "DD_SERVICE" + value = local.service_name + }, + { + name = "DD_VERSION" + value = var.versioning + }, + { + name = "DD_ENV" + value = local.env + }, + { + name = "ECS_FARGATE" + value = "true" + }, + ] + portMappings = [{ + hostPort = 8126, + protocol = "tcp", + containerPort = 8126 + } + ] + }, + { + name = "log_router" + image = "public.ecr.aws/aws-observability/aws-for-fluent-bit:stable" + essential = true + firelensConfiguration = { + type = "fluentbit" + options = { + "enable-ecs-log-metadata" = "true" + } + } + } + ]) +} diff --git a/infra/internal/prod/Makefile b/infra/internal/prod/Makefile new file mode 100644 index 00000000..de9232d3 --- /dev/null +++ b/infra/internal/prod/Makefile @@ -0,0 +1,12 @@ +export +AWS_PROFILE=prod-string + +init: + terraform init +plan: + terraform plan +apply: + terraform apply + +destroy: + terraform destroy diff --git a/infra/internal/prod/alb.tf b/infra/internal/prod/alb.tf new file mode 100644 index 00000000..4046fb7a --- /dev/null +++ b/infra/internal/prod/alb.tf @@ -0,0 +1,84 @@ +module "alb_acm" { + source = "../../acm" + domain_name = "admin.${local.root_domain}" + aws_region = "us-west-2" + zone_id = data.aws_route53_zone.root.zone_id + tags = { + Name = "admin-${local.root_domain}-alb" + } +} + +resource "aws_alb" "alb" { + name = "${local.service_name}-alb" + internal = true + drop_invalid_header_fields = true + security_groups = [aws_security_group.ecs_alb_https_sg.id] + subnets = data.terraform_remote_state.vpc.outputs.public_subnets + + tags = { + Name = "${local.service_name}-alb" + } + + lifecycle { + create_before_destroy = true + } +} + +resource "aws_alb_target_group" "ecs_task_target_group" { + name = "${local.service_name}-tg" + port = local.container_port + vpc_id = data.terraform_remote_state.vpc.outputs.id + target_type = "ip" + protocol = "HTTP" + + lifecycle { + create_before_destroy = true + } + + health_check { + path = "/heartbeat" + protocol = "HTTP" + matcher = "200" + interval = 60 + timeout = 30 + unhealthy_threshold = "3" + healthy_threshold = "3" + } + + tags = { + Name = "${local.service_name}-tg" + } +} + +resource "aws_alb_listener" "alb_https_listener" { + load_balancer_arn = aws_alb.alb.arn + port = 443 + protocol = "HTTPS" + ssl_policy = "ELBSecurityPolicy-TLS-1-2-2017-01" + certificate_arn = module.alb_acm.arn + + lifecycle { + create_before_destroy = true + } + + default_action { + type = "forward" + target_group_arn = aws_alb_target_group.ecs_task_target_group.arn + } +} + +resource "aws_alb_listener_rule" "ecs_alb_listener_rule" { + listener_arn = aws_alb_listener.alb_https_listener.arn + priority = 100 + action { + type = "forward" + target_group_arn = aws_alb_target_group.ecs_task_target_group.arn + } + + condition { + host_header { + values = ["admin.${local.root_domain}"] + } + } +} + diff --git a/infra/internal/prod/backend.tf b/infra/internal/prod/backend.tf new file mode 100644 index 00000000..fd8cd561 --- /dev/null +++ b/infra/internal/prod/backend.tf @@ -0,0 +1,35 @@ +locals { + remote_state_bucket = "prod-string-terraform-state" + backend_region = "us-west-2" + vpc_remote_state_key = "vpc.tfstate" +} + +provider "aws" { + region = "us-west-2" +} + +terraform { + required_providers { + aws = { + source = "hashicorp/aws" + version = "4.37.0" + } + } + + backend "s3" { + encrypt = true + key = "admin-api.tfstate" + bucket = "prod-string-terraform-state" + dynamodb_table = "prod-string-terraform-state-lock" + region = "us-west-2" + } +} + +data "terraform_remote_state" "vpc" { + backend = "s3" + config = { + region = local.backend_region + bucket = local.remote_state_bucket + key = local.vpc_remote_state_key + } +} diff --git a/infra/internal/prod/domain.tf b/infra/internal/prod/domain.tf new file mode 100644 index 00000000..1698c6c2 --- /dev/null +++ b/infra/internal/prod/domain.tf @@ -0,0 +1,14 @@ +data "aws_route53_zone" "root" { + name = local.root_domain +} + +resource "aws_route53_record" "domain" { + name = "admin.${local.root_domain}" + type = "A" + zone_id = data.aws_route53_zone.root.zone_id + alias { + evaluate_target_health = false + name = aws_alb.alb.dns_name + zone_id = aws_alb.alb.zone_id + } +} diff --git a/infra/internal/prod/ecs.tf b/infra/internal/prod/ecs.tf new file mode 100644 index 00000000..8c71ef2a --- /dev/null +++ b/infra/internal/prod/ecs.tf @@ -0,0 +1,59 @@ +resource "aws_ecs_cluster" "cluster" { + name = local.cluster_name +} + +resource "aws_ecs_task_definition" "task_definition" { + container_definitions = local.task_definition + family = local.service_name + cpu = local.cpu + memory = local.memory + requires_compatibilities = ["FARGATE"] + network_mode = "awsvpc" + execution_role_arn = aws_iam_role.task_ecs_role.arn + task_role_arn = aws_iam_role.task_ecs_role.arn +} + +resource "aws_ecr_repository" "repo" { + name = local.service_name + image_tag_mutability = "MUTABLE" + + image_scanning_configuration { + scan_on_push = true + } + + tags = { + Environment = local.env + Name = local.service_name + } +} + +resource "aws_ecs_service" "ecs_service" { + name = local.service_name + task_definition = local.service_name + desired_count = local.desired_task_count + cluster = aws_ecs_cluster.cluster.name + launch_type = "FARGATE" + + network_configuration { + subnets = data.terraform_remote_state.vpc.outputs.public_subnets + security_groups = [aws_security_group.ecs_task_sg.id] + assign_public_ip = true + } + + load_balancer { + container_name = local.service_name + container_port = local.container_port + target_group_arn = aws_alb_target_group.ecs_task_target_group.arn + } + + depends_on = [ + aws_alb_listener_rule.ecs_alb_listener_rule, + aws_iam_role_policy.task_ecs_policy, + aws_ecs_task_definition.task_definition + ] + + tags = { + Environment = local.env + Name = local.service_name + } +} diff --git a/infra/internal/prod/iam_roles.tf b/infra/internal/prod/iam_roles.tf new file mode 100644 index 00000000..7cc7ee22 --- /dev/null +++ b/infra/internal/prod/iam_roles.tf @@ -0,0 +1,62 @@ +data "aws_iam_policy_document" "ecs_task_policy" { + statement { + sid = "AllowECSAndTaskAssumeRole" + actions = ["sts:AssumeRole"] + effect = "Allow" + principals { + type = "Service" + identifiers = ["ecs.amazonaws.com", "ecs-tasks.amazonaws.com"] + } + } +} + +resource "aws_iam_role" "task_ecs_role" { + name = "${local.service_name}-task-ecs-role" + assume_role_policy = data.aws_iam_policy_document.ecs_task_policy.json +} + +data "aws_iam_policy_document" "task_policy" { + statement { + sid = "AllowReadToResourcesInListToTask" + effect = "Allow" + actions = [ + "ecs:*", + "ecr:*" + ] + + resources = ["*"] + } + + statement { + sid = "AllowAccessToSSM" + effect = "Allow" + actions = [ + "ssm:GetParameters" + ] + resources = [ + data.aws_ssm_parameter.datadog.arn, + data.aws_ssm_parameter.db_password.arn, + data.aws_ssm_parameter.db_username.arn, + data.aws_ssm_parameter.db_name.arn, + data.aws_ssm_parameter.db_host.arn, + data.aws_ssm_parameter.redis_host_url.arn, + data.aws_ssm_parameter.redis_auth_token.arn + ] + } + + statement { + sid = "AllowDecrypt" + effect = "Allow" + actions = [ + "kms:Decrypt" + ] + resources = [data.aws_kms_key.kms_key.arn] + } + +} + +resource "aws_iam_role_policy" "task_ecs_policy" { + name = "${local.env}-${local.service_name}-task-ecs-policy" + role = aws_iam_role.task_ecs_role.id + policy = data.aws_iam_policy_document.task_policy.json +} diff --git a/infra/internal/prod/security_group.tf b/infra/internal/prod/security_group.tf new file mode 100644 index 00000000..2c294c1a --- /dev/null +++ b/infra/internal/prod/security_group.tf @@ -0,0 +1,84 @@ +resource "aws_security_group" "ecs_alb_https_sg" { + name = "${local.service_name}-alb-https-sg" + description = "Security group for ALB to cluster" + vpc_id = data.terraform_remote_state.vpc.outputs.id + + ingress { + from_port = 443 + to_port = 443 + protocol = "TCP" + cidr_blocks = ["0.0.0.0/0"] + } + + egress { + from_port = 0 + to_port = 0 + protocol = -1 + cidr_blocks = ["0.0.0.0/0"] + } + + lifecycle { + create_before_destroy = true + } + + tags = { + Name = "${local.service_name}-alb-https-sg" + Environment = local.env + } +} + +resource "aws_security_group" "ecs_task_sg" { + name = "${local.service_name}-task-sg" + vpc_id = data.terraform_remote_state.vpc.outputs.id + ingress { + from_port = local.container_port + to_port = local.container_port + protocol = "TCP" + cidr_blocks = [data.terraform_remote_state.vpc.outputs.cidr_block] + } + + egress { + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + } + + lifecycle { + create_before_destroy = true + } + + tags = { + Name = "${local.service_name}-task-sg" + environment = local.env + } +} + +# Give access to DB through Security group rule +data "aws_security_group" "rds" { + name = "${local.env}-string-write-master-client-rds" + vpc_id = data.terraform_remote_state.vpc.outputs.id +} + +data "aws_security_group" "redis" { + name = "redis-client-redis" + vpc_id = data.terraform_remote_state.vpc.outputs.id +} + +resource "aws_security_group_rule" "rds_to_ecs" { + type = "ingress" + protocol = "TCP" + from_port = local.db_port + to_port = local.db_port + source_security_group_id = aws_security_group.ecs_task_sg.id + security_group_id = data.aws_security_group.rds.id +} + +resource "aws_security_group_rule" "redis_to_ecs" { + type = "ingress" + protocol = "TCP" + from_port = local.redis_port + to_port = local.redis_port + source_security_group_id = aws_security_group.ecs_task_sg.id + security_group_id = data.aws_security_group.redis.id +} \ No newline at end of file diff --git a/infra/internal/prod/ssm.tf b/infra/internal/prod/ssm.tf new file mode 100644 index 00000000..e9e226bd --- /dev/null +++ b/infra/internal/prod/ssm.tf @@ -0,0 +1,31 @@ +data "aws_ssm_parameter" "datadog" { + name = "datadog-key" +} + +data "aws_ssm_parameter" "db_password" { + name = "string-rds-pg-db-password" +} + +data "aws_ssm_parameter" "db_username" { + name = "string-rds-pg-db-username" +} + +data "aws_ssm_parameter" "db_name" { + name = "string-rds-pg-db-name" +} + +data "aws_ssm_parameter" "db_host" { + name = "${local.env}-string-write-db-host-url" +} + +data "aws_ssm_parameter" "redis_auth_token" { + name = "redis-auth-token" +} + +data "aws_ssm_parameter" "redis_host_url" { + name = "redis-host-url" +} + +data "aws_kms_key" "kms_key" { + key_id = "alias/main-kms-key" +} diff --git a/infra/internal/prod/variables.tf b/infra/internal/prod/variables.tf new file mode 100644 index 00000000..90ecae90 --- /dev/null +++ b/infra/internal/prod/variables.tf @@ -0,0 +1,165 @@ +locals { + cluster_name = "admin" + env = "dev" + service_name = "admin" + root_domain = "string-api.xyz" + container_port = "3000" + origin_id = "admin-api" + desired_task_count = "1" + db_port = "5432" + redis_port = "6379" + memory = 512 + cpu = 256 + region = "us-west-2" +} + +variable "versioning" { + type = string + default = "latest" +} + +locals { + task_definition = jsonencode([ + { + name = local.service_name + image = "${aws_ecr_repository.repo.repository_url}:${var.versioning}" + essential = true, + dockerLabels = { + "com.datadoghq.ad.instances" : "[{\"host\":\"%%host%%\"}]", + "com.datadoghq.ad.check_names" : "[\"${local.service_name}\"]", + }, + portMappings = [ + { containerPort = 3000 } + ], + secrets = [ + { + name = "DB_USERNAME" + valueFrom = data.aws_ssm_parameter.db_username.arn + }, + { + name = "DB_PASSWORD" + valueFrom = data.aws_ssm_parameter.db_password.arn + }, + { + name = "DB_HOST" + valueFrom = data.aws_ssm_parameter.db_host.arn + }, + { + name = "DB_NAME" + valueFrom = data.aws_ssm_parameter.db_name.arn + }, + { + name = "REDIS_HOST", + valuefrom = data.aws_ssm_parameter.redis_host_url.arn + }, + { + name = "REDIS_PASSWORD", + valuefrom = data.aws_ssm_parameter.redis_auth_token.arn + } + ] + environment = [ + { + name = "PORT" + value = local.container_port + }, + { + name = "REDIS_PORT" + value = local.redis_port + }, + { + name = "DB_PORT", + value = local.db_port + }, + { + name = "ENV" + value = local.env + }, + { + name = "AWS_REGION" + value = local.region + }, + { + name = "AWS_KMS_KEY_ID" + value = data.aws_kms_key.kms_key.key_id + }, + { + name = "OWLRACLE_API_URL" + value = "https://api.owlracle.info/v3/" + }, + { + name = "COINGECKO_API_URL" + value = "https://api.coingecko.com/api/v3/" + } + ], + logConfiguration = { + logDriver = "awsfirelens" + secretOptions = [{ + name = "apiKey", + valueFrom = data.aws_ssm_parameter.datadog.arn + }] + options = { + Name = "datadog" + "dd_service" = "${local.service_name}" + "Host" = "http-intake.logs.datadoghq.com" + "dd_source" = "${local.service_name}" + "dd_message_key" = "log" + "dd_tags" = "project:${local.service_name}" + "TLS" = "on" + "provider" = "ecs" + } + } + }, + { + name = "datadog-agent" + image = "public.ecr.aws/datadog/agent:latest" + essential = true + secrets = [{ + name = "DD_API_KEY" + valueFrom = data.aws_ssm_parameter.datadog.arn + }], + environment = [ + { + name = "DD_APM_ENABLED" + value = "true" + }, + { + name = "DD_SITE" + value = "datadoghq.com" + }, + { + name = "DD_SERVICE" + value = local.service_name + }, + { + name = "DD_VERSION" + value = var.versioning + }, + { + name = "DD_ENV" + value = local.env + }, + { + name = "ECS_FARGATE" + value = "true" + }, + ] + portMappings = [{ + hostPort = 8126, + protocol = "tcp", + containerPort = 8126 + } + ] + }, + { + name = "log_router" + image = "public.ecr.aws/aws-observability/aws-for-fluent-bit:stable" + essential = true + firelensConfiguration = { + type = "fluentbit" + options = { + "enable-ecs-log-metadata" = "true" + } + } + } + ]) +} diff --git a/migrations/0004_auth_strategy.sql b/migrations/0004_auth_key.sql similarity index 64% rename from migrations/0004_auth_strategy.sql rename to migrations/0004_auth_key.sql index f6667bd9..40bfa569 100644 --- a/migrations/0004_auth_strategy.sql +++ b/migrations/0004_auth_key.sql @@ -1,12 +1,13 @@ -- +goose Up CREATE TABLE auth_strategy ( id UUID PRIMARY KEY NOT NULL DEFAULT UUID_GENERATE_V4(), + type TEXT NOT NULL DEFAULT '', -- this auth strat is email/password type + contact_id UUID REFERENCES contact(id) DEFAULT NULL, + status TEXT NOT NULL DEFAULT 'pending', -- temp use for API approval + data TEXT NOT NULL, -- the hashed password created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, - deactivated_at TIMESTAMP WITH TIME ZONE DEFAULT NULL, - type TEXT NOT NULL DEFAULT '', -- this auth strat is email/password type - contact_id UUID REFERENCES contact(id), - data TEXT NOT NULL -- the hashed password + deactivated_at TIMESTAMP WITH TIME ZONE DEFAULT NULL ); CREATE OR REPLACE TRIGGER update_auth_strategy_updated_at @@ -15,6 +16,8 @@ CREATE OR REPLACE TRIGGER update_auth_strategy_updated_at FOR EACH ROW EXECUTE PROCEDURE update_updated_at_column(); +CREATE INDEX auth_strategy_status_idx ON auth_strategy(status); -- +goose Down +DROP INDEX IF EXISTS auth_strategy_status_idx; DROP TRIGGER IF EXISTS update_auth_strategy_updated_at ON auth_strategy; DROP TABLE IF EXISTS auth_strategy; \ No newline at end of file diff --git a/pkg/model/custom.go b/pkg/model/custom.go index d18e5dba..f0a595b7 100644 --- a/pkg/model/custom.go +++ b/pkg/model/custom.go @@ -21,3 +21,28 @@ func (sm *StringMap) Scan(src interface{}) error { } return errors.New("unknown type") } + +type NullableString string + +const NullString NullableString = "\x00" + +// implements driver.Valuer, will be invoked automatically when written to the db +func (s NullableString) Value() (driver.Value, error) { + if s == NullString { + return nil, nil + } + return []byte(s), nil +} + +// implements sql.Scanner, will be invoked automatically when read from the db +func (s *NullableString) Scan(src interface{}) error { + switch v := src.(type) { + case string: + *s = NullableString(v) + case []byte: + *s = NullableString(v) + case nil: + *s = "" + } + return nil +} diff --git a/pkg/model/entity.go b/pkg/model/entity.go index 592b47ca..2a4457f0 100644 --- a/pkg/model/entity.go +++ b/pkg/model/entity.go @@ -181,15 +181,17 @@ type Transaction struct { } type AuthStrategy struct { - ID string `json:"id,omitempty" db:"id"` - EntityID string `json:"entityId" db:"id"` // for redis use only - CreatedAt time.Time `json:"createdAt,omitempty" db:"created"` - DeactivatedAt *time.Time `json:"deactivatedAt,omitempty" db:"deactivated_at"` - Type string `json:"authType" db:"type"` - EntityType string `json:"entityType,omitempty"` // for redis use only - ContactData string `json:"contactData"` // for redis use only - ContactID string `json:"contactId" db:"contact_id"` - Data string `json:"data" data:"data"` + ID string `json:"id,omitempty" db:"id"` + Status string `json:"status" db:"status"` + EntityID string `json:"entityId,omitempty"` // for redis use only + Type string `json:"authType" db:"type"` + EntityType string `json:"entityType,omitempty"` // for redis use only + ContactData string `json:"contactData,omitempty"` // for redis use only + ContactID NullableString `json:"contactId,omitempty" db:"contact_id"` + Data string `json:"data" data:"data"` + CreatedAt time.Time `json:"createdAt,omitempty" db:"created_at"` + UpdateddAt time.Time `json:"updatedAt,omitempty" db:"updated_at"` + DeactivatedAt *time.Time `json:"deactivatedAt,omitempty" db:"deactivated_at"` } func (a AuthStrategy) MarshalBinary() ([]byte, error) { diff --git a/pkg/repository/auth.go b/pkg/repository/auth.go index ce846c45..86cb67d1 100644 --- a/pkg/repository/auth.go +++ b/pkg/repository/auth.go @@ -1,7 +1,9 @@ package repository import ( + "database/sql" "encoding/json" + "fmt" "time" "github.com/String-xyz/string-api/pkg/internal/common" @@ -27,10 +29,13 @@ const ( type AuthStrategy interface { Create(authType AuthType, m model.AuthStrategy) error CreateAny(key string, val any, expire time.Duration) error - CreateAPIKey(entityID string, authType AuthType, apiKey string) error + CreateAPIKey(entityID string, authType AuthType, apiKey string, persistOnly bool) error CreateJWTRefresh(key string, val string) error Get(string) (model.AuthStrategy, error) GetKeyString(key string) (string, error) + List(limit, offset int) ([]model.AuthStrategy, error) + ListByStatus(limit, offset int, status string) ([]model.AuthStrategy, error) + UpdateStatus(ID, status string) (model.AuthStrategy, error) } type auth struct { @@ -59,7 +64,12 @@ func (a auth) CreateAny(key string, val any, expire time.Duration) error { } // CreateAPIKey creates and persists an API Key for a platform -func (a auth) CreateAPIKey(entityID string, authType AuthType, key string) error { +func (a auth) CreateAPIKey(entityID string, authType AuthType, key string, persistOnly bool) error { + // only insert to postgres and skip redis cache + if persistOnly { + _, err := a.store.Exec("INSERT INTO auth_strategy(type,data) VALUES($1, $2)", authType, key) + return err + } m := model.AuthStrategy{ EntityID: entityID, CreatedAt: time.Now(), @@ -106,3 +116,32 @@ func (a auth) GetKeyString(key string) (string, error) { } return string(m), nil } + +// List all the available auth_keys on the postgres db +func (a auth) List(limit, offset int) ([]model.AuthStrategy, error) { + list := []model.AuthStrategy{} + err := a.store.Select(&list, "SELECT * FROM auth_strategy LIMIT $1 OFFSET $2", limit, offset) + if err != nil && err == sql.ErrNoRows { + return list, nil + } + return list, err +} + +// ListByStatus lists all auth_keys with a given status on the postgres db +func (a auth) ListByStatus(limit, offset int, status string) ([]model.AuthStrategy, error) { + list := []model.AuthStrategy{} + err := a.store.Select(&list, "SELECT * FROM auth_strategy WHERE status = $1 LIMIT $2 OFFSET $3", status, limit, offset) + if err != nil && err == sql.ErrNoRows { + return list, nil + } + return list, err +} + +// UpdateStatus updates the status on postgres db and returns the updated row +func (a auth) UpdateStatus(ID, status string) (model.AuthStrategy, error) { + fmt.Println("Status and ID", status, ID) + row := a.store.QueryRowx("UPDATE auth_strategy SET status = $2 WHERE id = $1 RETURNING *", ID, status) + m := model.AuthStrategy{} + err := row.StructScan(&m) + return m, err +} diff --git a/pkg/repository/user_test.go b/pkg/repository/user_test.go index b84e53a5..578f48c8 100644 --- a/pkg/repository/user_test.go +++ b/pkg/repository/user_test.go @@ -8,6 +8,7 @@ import ( "github.com/String-xyz/string-api/pkg/model" "github.com/google/uuid" "github.com/jmoiron/sqlx" + "github.com/stretchr/testify/assert" ) func TestCreateUser(t *testing.T) { @@ -45,7 +46,9 @@ func TestGetUser(t *testing.T) { mock.ExpectQuery("SELECT * FROM string_user WHERE id = $1 AND 'deactivated_at' IS NOT NULL").WillReturnRows(rows).WithArgs(id) - NewUser(sqlxDB).GetById(id) + user, err := NewUser(sqlxDB).GetById(id) + assert.NoError(t, err) + assert.Equal(t, id, user.ID) if err := mock.ExpectationsWereMet(); err != nil { t.Errorf("error '%s' was not expected, getting user by id", err) } diff --git a/pkg/service/auth.go b/pkg/service/auth.go index 8abbe20c..d05bd23e 100644 --- a/pkg/service/auth.go +++ b/pkg/service/auth.go @@ -44,10 +44,8 @@ type Auth interface { GenerateJWT(model.User) (JWT, error) LoginPK(UserPKLogin) (JWT, error) Challenge(publicAddres string) (string, error) - GenerateAPIKey(model.Platform) error ValidateAPIKey(key string) bool RefreshToken(string) - LoginOTP() error } type auth struct { @@ -80,7 +78,7 @@ func (a auth) Register(m UserRegister) (JWT, error) { err = a.authRepo.Create(repository.AuthTypeEmail, model.AuthStrategy{ EntityID: user.ID, - ContactID: contact.ID, + ContactID: model.NullableString(contact.ID), CreatedAt: time.Now(), Type: string(repository.AuthTypeEmail), EntityType: string(repository.EntityTypeUser), @@ -199,14 +197,6 @@ func (a auth) ValidateAPIKey(key string) bool { return authKey.Data == hashed } -func (a auth) GenerateAPIKey(m model.Platform) error { - return nil -} - -func (a auth) LoginOTP() error { - return nil -} - func (a auth) RefreshToken(token string) { //stra, err := a.authRepo.Get(common.ToSha256(token)) } diff --git a/pkg/service/auth_key.go b/pkg/service/auth_key.go new file mode 100644 index 00000000..e4ebe8ab --- /dev/null +++ b/pkg/service/auth_key.go @@ -0,0 +1,50 @@ +package service + +import ( + "github.com/String-xyz/string-api/pkg/internal/common" + "github.com/String-xyz/string-api/pkg/model" + "github.com/String-xyz/string-api/pkg/repository" +) + +type APIKeyStrategy interface { + Create() (string, error) + List(limit, offset int, status string) ([]model.AuthStrategy, error) + Approve(ID string) error +} + +type aPIKeyStrategy struct { + repo repository.AuthStrategy +} + +func NewAPIKeyStrategy(repo repository.AuthStrategy) APIKeyStrategy { + return aPIKeyStrategy{repo} +} +func (g aPIKeyStrategy) Create() (string, error) { + uuiKey := "str." + uuidWithoutHyphens() + hashed := common.ToSha256(uuiKey) + err := g.repo.CreateAPIKey("", repository.AuthTypeAPIKey, hashed, true) + return uuiKey, err +} + +func (g aPIKeyStrategy) List(limit, offset int, status string) ([]model.AuthStrategy, error) { + if status != "" { + return g.ListByStatus(limit, offset, status) + } + return g.repo.List(limit, offset) +} + +func (g aPIKeyStrategy) ListByStatus(limit, offset int, status string) ([]model.AuthStrategy, error) { + if limit == 0 { + limit = 100 + } + return g.repo.ListByStatus(limit, offset, status) +} + +// Approve updates the APIKey status and creates an entry on redis +func (g aPIKeyStrategy) Approve(ID string) error { + m, err := g.repo.UpdateStatus(ID, "active") + if err != nil { + return err + } + return g.repo.CreateAPIKey(m.ID, repository.AuthTypeAPIKey, m.Data, false) +} diff --git a/pkg/service/platform.go b/pkg/service/platform.go index e153565e..b000fa62 100644 --- a/pkg/service/platform.go +++ b/pkg/service/platform.go @@ -2,7 +2,6 @@ package service import ( "github.com/String-xyz/string-api/pkg/internal/common" - stringCommon "github.com/String-xyz/string-api/pkg/internal/common" "github.com/String-xyz/string-api/pkg/model" "github.com/String-xyz/string-api/pkg/repository" ) @@ -30,19 +29,19 @@ func (a platform) Create(c CreatePlatform) (model.Platform, error) { Type: c.Type, Authentication: c.Authentication, ApiKey: hashed, - Status: "initialized", + Status: "pending", } plat, err := a.platRepo.Create(m) if err != nil { - return model.Platform{}, stringCommon.StringError(err) + return model.Platform{}, common.StringError(err) } - err = a.authRepo.CreateAPIKey(plat.ID, c.Authentication, hashed) + err = a.authRepo.CreateAPIKey(plat.ID, c.Authentication, hashed, false) pt := &plat pt.ApiKey = uuiKey if err != nil { - return *pt, stringCommon.StringError(err) + return *pt, common.StringError(err) } return plat, nil diff --git a/pkg/test/stubs/repository.go b/pkg/test/stubs/repository.go index 73a430eb..a132be47 100644 --- a/pkg/test/stubs/repository.go +++ b/pkg/test/stubs/repository.go @@ -70,7 +70,7 @@ func (AuthStrategyRepo) Create(authType repository.AuthType, m model.AuthStrateg return nil } -func (AuthStrategyRepo) CreateAPIKey(entityID string, authType model.AuthType, apiKey string) error { +func (AuthStrategyRepo) CreateAPIKey(entityID string, authType model.AuthType, apiKey string, persistOnly bool) error { return nil } @@ -88,3 +88,13 @@ func (AuthStrategyRepo) CreateAny(key string, val any, expire time.Duration) err func (AuthStrategyRepo) GetKeyString(key string) (string, error) { return "", nil } + +func (AuthStrategyRepo) List(limit, offset int) ([]model.AuthStrategy, error) { + return []model.AuthStrategy{}, nil +} +func (AuthStrategyRepo) ListByStatus(limit, offset int, status string) ([]model.AuthStrategy, error) { + return []model.AuthStrategy{}, nil +} +func (AuthStrategyRepo) UpdateStatus(ID, status string) (model.AuthStrategy, error) { + return model.AuthStrategy{}, nil +}