commit 7873968e4d1c0eb41fd12a7c90b1cabee13f6695 Author: JongYeob Sheen Date: Thu Oct 19 22:47:12 2023 +0900 first commit diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..b54d592 --- /dev/null +++ b/Makefile @@ -0,0 +1,158 @@ +FUNCTION_NAME =superrichquiz-octet +BUILD =$(CURDIR)/build +BIN =$(CURDIR)/bin +MAIN =$(CURDIR)/cmd/main.go +BUCKET =AWS_BUCKET_NAME +REGION =ap-northeast-2 +ARCH =amd64 +PROD_ZIPFILE =$(FUNCTION_NAME)-prod-linux-$(ARCH).zip +PROD_HANDLER =bootstrap +DEV_ZIPFILE =$(FUNCTION_NAME)-dev-linux-$(ARCH).zip +DEV_HANDLER =bootstrap + +default: help + +deps: ## install dependency + @echo "\033[32mDependency ...\033[0m" + @go install gorm.io/gorm +.PHONY: deps + +env.local: ## copy env for local + @cp configs/common.local configs/common.go + +env.dev: ## copy env for development + @cp configs/common.dev configs/common.go + +env.prod: ## copy env for production + @cp configs/common.prod configs/common.go + +run: env.local ## run local + @echo "\033[32mRunning ...\033[0m" + @go run $(MAIN) +.PHONY: run + +fmt: ## show formatting + @echo "\033[32mfmt ...\033[0m" + @gofmt -s -w . && go mod tidy +.PHONY: fmt + +lint: fmt ## linting + golangci-lint run ./... + +build: deps ## build + @echo "\033[32mBuilding ...\033[0m" + @go build -o $(BIN) $(MAIN) +.PHONY: build + +install: ## install + @echo "\033[32mInstalling ...\033[0m" + go install -v +.PHONY: install + +test: ## testing + @echo "\033[32mTesting...\033[0m" + @go test ./... -v + @echo "all tests passed" +.PHONY: test + +build.local: env.local ## build for local + @echo "\033[32mBuilding for Local running ...\033[0m" + @go build -o $(BUILD)/main $(MAIN) + +build.dev: env.dev ## build for development + @echo "\033[32mBuilding for AWS Lamdbda Development ...\033[0m" + @GOOS=linux GOARCH=$(ARCH) go build -o $(BUILD)/$(DEV_HANDLER) $(MAIN) + @cd $(BUILD) && \ + zip -9 $(DEV_ZIPFILE) $(DEV_HANDLER) + +build.prod: env.prod ## build for production + @echo "\033[32mBuilding for AWS Lamdbda Production ...\033[0m" + @GOOS=linux GOARCH=$(ARCH) go build -o $(BUILD)/$(PROD_HANDLER) $(MAIN) + @cd $(BUILD) && \ + zip -9 $(PROD_ZIPFILE) $(PROD_HANDLER) + +deploy.dev: build.dev lambda.deploy.dev ## deploy for development +deploy.prod: build.prod lambda.deploy.prod ## deploy for production + +lambda.deploy.dev: ## copy to S3 & update lambda function for development + @echo "\033[32mDistribution Development : copy to S3 & update lambda function ...\033[0m" + @aws s3 cp $(BUILD)/$(DEV_ZIPFILE) s3://$(BUCKET) + @aws lambda update-function-code \ + --function-name $(FUNCTION_NAME)-dev \ + --s3-bucket $(BUCKET) \ + --s3-key $(DEV_ZIPFILE) \ + --region $(REGION) \ + &2> /dev/null + +lambda.deploy.prod: ## copy to S3 & update lambda function for production + @echo "\033[32mDistribution Prodction : copy to S3 & update lambda function ...\033[0m" + @aws s3 cp $(BUILD)/$(PROD_ZIPFILE) s3://$(BUCKET) + @aws lambda update-function-code \ + --function-name $(FUNCTION_NAME) \ + --s3-bucket $(BUCKET) \ + --s3-key $(PROD_ZIPFILE) \ + --region $(REGION) \ + &2> /dev/null + + +docker.build.local: ## build docker for local running + @echo "\033[32mDocker for local ...\033[0m" + @docker compose -f docker/local/docker-compose.yml build --no-cache + @docker compose -f docker/local/docker-compose.yml create +.PHONY: docker.build.local + +docker.start.local: ## start docker for local + @echo "\033[32mDocker start ...\033[0m" + @docker compose -f docker/local/docker-compose.yml up -d +.PHONY: docker.start.local + +docker.stop.local: ## stop docker for local + @echo "\033[32mDocker stop ...\033[0m" + @docker compose -f docker/local/docker-compose.yml down +.PHONY: docker.stop.local + + +docker.build.dev: ## build docker for development + @echo "\033[32mDocker for development ...\033[0m" + @docker compose -f docker/dev/docker-compose.yml build --no-cache + @docker compose -f docker/dev/docker-compose.yml create +.PHONY: docker.build.dev + +docker.start.dev: ## start docker for development + @echo "\033[32mDocker start for development...\033[0m" + @docker compose -f docker/dev/docker-compose.yml up -d +.PHONY: docker.start.dev + +docker.stop.dev: ## stop docker for development + @echo "\033[32mDocker stop for development...\033[0m" + @docker compose -f docker/dev/docker-compose.yml down +.PHONY: docker.stop.dev + + +docker.build.prod: ## build docker for production + @echo "\033[32mDocker for production ...\033[0m" + @docker compose -f docker/prod/docker-compose.yml build --no-cache + @docker compose -f docker/prod/docker-compose.yml create +.PHONY: docker.build.prod + +docker.start.prod: ## start docker for production + @echo "\033[32mDocker start for production...\033[0m" + @docker compose -f docker/prod/docker-compose.yml up -d +.PHONY: docker.start.prod + +docker.stop.prod: ## stop docker for production + @echo "\033[32mDocker stop for production...\033[0m" + @docker compose -f docker/prod/docker-compose.yml down +.PHONY: docker.stop.prod + + +clean: ## clean + @echo "\033[32mCleaning...\033[0m" + @go clean + @rm -rf $(BIN)/* + @rm -rf $(BUILD)/* +.PHONY: clean + +help: ## Show help for each of the Makefile recipes. + @grep -E '^[a-zA-Z0-9 -.]+:.*#' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' +.PHONY: help \ No newline at end of file diff --git a/cmd/main.go b/cmd/main.go new file mode 100644 index 0000000..a93495a --- /dev/null +++ b/cmd/main.go @@ -0,0 +1,32 @@ +package main + +import ( + "log" + "net/http" + + configs "studioj/boilerplate_go/configs" + "studioj/boilerplate_go/internal/database" + "studioj/boilerplate_go/internal/helpers" + "studioj/boilerplate_go/internal/routers" + + "github.com/apex/gateway" +) + +func main() { + Init() + Run() +} + +func Init() { + database.Init() + routers.Init() + database.AutoMigrate() +} + +func Run() { + if helpers.InLambda() { + log.Fatal(gateway.ListenAndServe(configs.PORT, routers.Router)) + } else { + log.Fatal(http.ListenAndServe(configs.PORT, routers.Router)) + } +} diff --git a/configs/common.dev b/configs/common.dev new file mode 100644 index 0000000..3ff9f9c --- /dev/null +++ b/configs/common.dev @@ -0,0 +1,5 @@ +package config + +const PORT = ":3030" +const DATABASE_URL = "root:omHO7EEzHm52s9DlZD70P6KPKm2TbODC@tcp(db:3306)/boilerplate?charset=utf8&parseTime=True&loc=Local" +const SECRET_KEY = "5a14e06d-55a3-418c-9f3f-8fde328d6c49" \ No newline at end of file diff --git a/configs/common.go b/configs/common.go new file mode 100644 index 0000000..d2cb9d7 --- /dev/null +++ b/configs/common.go @@ -0,0 +1,7 @@ +package config + +const PORT = ":3030" +const DATABASE_URL = "root:sswha123@tcp(localhost:3306)/boilerplate?charset=utf8&parseTime=True&loc=Local" + +// const DATABASE_URL = "sqlite.db" +const SECRET_KEY = "5a14e06d-55a3-418c-9f3f-8fde328d6c49" diff --git a/configs/common.local b/configs/common.local new file mode 100644 index 0000000..51e3c29 --- /dev/null +++ b/configs/common.local @@ -0,0 +1,6 @@ +package config + +const PORT = ":3030" +const DATABASE_URL = "root:sswha123@tcp(localhost:3306)/boilerplate?charset=utf8&parseTime=True&loc=Local" +//const DATABASE_URL = "sqlite.db" +const SECRET_KEY = "5a14e06d-55a3-418c-9f3f-8fde328d6c49" \ No newline at end of file diff --git a/configs/common.prod b/configs/common.prod new file mode 100644 index 0000000..8719dab --- /dev/null +++ b/configs/common.prod @@ -0,0 +1,5 @@ +package config + +const PORT = ":3030" +const DATABASE_URL = "root:omHO7EEzHm52s9DlZD70P6KPKm2TbODC@tcp(localhost:3306)/boilerplate?charset=utf8&parseTime=True&loc=Local" +const SECRET_KEY = "5a14e06d-55a3-418c-9f3f-8fde328d6c49" \ No newline at end of file diff --git a/docker/dev/Dockerfile b/docker/dev/Dockerfile new file mode 100644 index 0000000..1babe6c --- /dev/null +++ b/docker/dev/Dockerfile @@ -0,0 +1,18 @@ +# syntax=docker/dockerfile:1 + +FROM alpine:latest AS deps +RUN apk add --no-cache mariadb-dev sqlite-dev build-base go + +FROM deps AS builder +WORKDIR /app +COPY . . +RUN cp configs/common.dev configs/common.go +RUN go mod download && go mod verify +RUN CGO_ENABLED=1 go build -v -o bootstrap cmd/main.go + +FROM alpine:latest AS runner +WORKDIR /app +RUN apk add --no-cache sqlite-libs mariadb-connector-c libgcc +COPY --from=builder /app/bootstrap . + +CMD ["sh", "-c", "./bootstrap"] \ No newline at end of file diff --git a/docker/dev/docker-compose.yml b/docker/dev/docker-compose.yml new file mode 100644 index 0000000..812d42b --- /dev/null +++ b/docker/dev/docker-compose.yml @@ -0,0 +1,29 @@ +version: "3" + +services: + db: + image: mysql:latest + environment: + MYSQL_ROOT_PASSWORD: omHO7EEzHm52s9DlZD70P6KPKm2TbODC + MYSQL_DATABASE: boilerplate + ports: + - "3306:3306" + volumes: + - db-data:/var/lib/mysql + app: + build: + context: ../../ + dockerfile: docker/dev/Dockerfile + image: studioj/boilerplate_app:dev + ports: + - "3030:3030" + environment: + DB_HOST: db + DB_PORT: 3306 + DB_USER: root + DB_PASSWORD: omHO7EEzHm52s9DlZD70P6KPKm2TbODC + DB_NAME: boilerplate + depends_on: + - db +volumes: + db-data: diff --git a/docker/local/Dockerfile b/docker/local/Dockerfile new file mode 100644 index 0000000..c04305e --- /dev/null +++ b/docker/local/Dockerfile @@ -0,0 +1,18 @@ +# syntax=docker/dockerfile:1 + +FROM alpine:latest AS deps +RUN apk add --no-cache mariadb-dev sqlite-dev build-base go + +FROM deps AS builder +WORKDIR /app +COPY . . +RUN cp configs/common.local configs/common.go +RUN go mod download && go mod verify +RUN CGO_ENABLED=1 go build -v -o bootstrap cmd/main.go + +FROM alpine:latest AS runner +WORKDIR /app +RUN apk add --no-cache sqlite-libs mariadb-connector-c libgcc +COPY --from=builder /app/bootstrap . + +CMD ["sh", "-c", "./bootstrap"] \ No newline at end of file diff --git a/docker/local/docker-compose.yml b/docker/local/docker-compose.yml new file mode 100644 index 0000000..ad61a63 --- /dev/null +++ b/docker/local/docker-compose.yml @@ -0,0 +1,30 @@ +version: "3" + +services: + db: + image: mysql:latest + environment: + MYSQL_ROOT_PASSWORD: omHO7EEzHm52s9DlZD70P6KPKm2TbODC + MYSQL_DATABASE: boilerplate + ports: + - "3306:3306" + volumes: + - db-data:/var/lib/mysql + app: + build: + context: ../../ + dockerfile: docker/local/Dockerfile + image: studioj/boilerplate_app:local + ports: + - "3030:3030" + environment: + DB_HOST: db + DB_PORT: 3306 + DB_USER: root + DB_PASSWORD: omHO7EEzHm52s9DlZD70P6KPKm2TbODC + DB_NAME: boilerplate + depends_on: + - db + +volumes: + db-data: diff --git a/docker/prod/Dockerfile b/docker/prod/Dockerfile new file mode 100644 index 0000000..26685d7 --- /dev/null +++ b/docker/prod/Dockerfile @@ -0,0 +1,18 @@ +# syntax=docker/dockerfile:1 + +FROM alpine:latest AS deps +RUN apk add --no-cache mariadb-dev sqlite-dev build-base go + +FROM deps AS builder +WORKDIR /app +COPY . . +RUN cp configs/common.prod configs/common.go +RUN go mod download && go mod verify +RUN CGO_ENABLED=1 go build -v -o bootstrap cmd/main.go + +FROM alpine:latest AS runner +WORKDIR /app +RUN apk add --no-cache sqlite-libs mariadb-connector-c libgcc +COPY --from=builder /app/bootstrap . + +CMD ["sh", "-c", "./bootstrap"] \ No newline at end of file diff --git a/docker/prod/docker-compose.yml b/docker/prod/docker-compose.yml new file mode 100644 index 0000000..469fdde --- /dev/null +++ b/docker/prod/docker-compose.yml @@ -0,0 +1,30 @@ +version: "3" + +services: + db: + image: mysql:latest + environment: + MYSQL_ROOT_PASSWORD: omHO7EEzHm52s9DlZD70P6KPKm2TbODC + MYSQL_DATABASE: boilerplate + ports: + - "3306:3306" + volumes: + - db-data:/var/lib/mysql + app: + build: + context: ../../ + dockerfile: docker/prod/Dockerfile + image: studioj/boilerplate_app + ports: + - "3030:3030" + environment: + DB_HOST: db + DB_PORT: 3306 + DB_USER: root + DB_PASSWORD: omHO7EEzHm52s9DlZD70P6KPKm2TbODC + DB_NAME: boilerplate + depends_on: + - db + +volumes: + db-data: diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..79a9066 --- /dev/null +++ b/go.mod @@ -0,0 +1,46 @@ +module studioj/boilerplate_go + +go 1.20 + +require github.com/gin-gonic/gin v1.9.1 + +require ( + github.com/aws/aws-lambda-go v1.17.0 // indirect + github.com/go-sql-driver/mysql v1.7.0 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect + github.com/pkg/errors v0.9.1 // indirect +) + +require ( + github.com/apex/gateway v1.1.2 + github.com/bytedance/sonic v1.9.1 // indirect + github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect + github.com/gabriel-vasile/mimetype v1.4.2 // indirect + github.com/gin-contrib/sse v0.1.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.14.0 // indirect + github.com/goccy/go-json v0.10.2 // indirect + github.com/golang-jwt/jwt/v5 v5.0.0 + github.com/google/uuid v1.3.1 + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.2.4 // indirect + github.com/leodido/go-urn v1.2.4 // indirect + github.com/mattn/go-isatty v0.0.19 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.0.8 // indirect + github.com/stretchr/testify v1.8.4 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.11 // indirect + golang.org/x/arch v0.3.0 // indirect + golang.org/x/crypto v0.9.0 + golang.org/x/net v0.10.0 // indirect + golang.org/x/sys v0.8.0 // indirect + golang.org/x/text v0.9.0 // indirect + google.golang.org/protobuf v1.30.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + gorm.io/driver/mysql v1.5.2 + gorm.io/gorm v1.25.5 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..aa292d7 --- /dev/null +++ b/go.sum @@ -0,0 +1,118 @@ +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/apex/gateway v1.1.2 h1:OWyLov8eaau8YhkYKkRuOAYqiUhpBJalBR1o+3FzX+8= +github.com/apex/gateway v1.1.2/go.mod h1:AMTkVbz5u5Hvd6QOGhhg0JUrNgCcLVu3XNJOGntdoB4= +github.com/aws/aws-lambda-go v1.17.0 h1:Ogihmi8BnpmCNktKAGpNwSiILNNING1MiosnKUfU8m0= +github.com/aws/aws-lambda-go v1.17.0/go.mod h1:FEwgPLE6+8wcGBTe5cJN3JWurd1Ztm9zN4jsXsjzKKw= +github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= +github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s= +github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= +github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= +github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= +github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +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/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= +github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= +github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js= +github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= +github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc= +github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/golang-jwt/jwt/v5 v5.0.0 h1:1n1XNM9hk7O9mnQoNBGolZvzebBQ7p93ULHRc28XJUE= +github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= +github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk= +github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= +github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= +github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= +github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= +github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +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/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/tj/assert v0.0.3 h1:Df/BlaZ20mq6kuai7f5z2TvPFiwC3xaWJSDQNiIS3Rk= +github.com/tj/assert v0.0.3/go.mod h1:Ne6X72Q+TB1AteidzQncjw9PabbMp4PBMZ1k+vd1Pvk= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= +github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/urfave/cli/v2 v2.1.1/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ= +golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= +golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g= +golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= +golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= +google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/driver/mysql v1.5.2 h1:QC2HRskSE75wBuOxe0+iCkyJZ+RqpudsQtqkp+IMuXs= +gorm.io/driver/mysql v1.5.2/go.mod h1:pQLhh1Ut/WUAySdTHwBpBv6+JKcj+ua4ZFx1QQTBzb8= +gorm.io/gorm v1.25.2-0.20230530020048-26663ab9bf55/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= +gorm.io/gorm v1.25.5 h1:zR9lOiiYf09VNh5Q1gphfyia1JpiClIWG9hQaxB/mls= +gorm.io/gorm v1.25.5/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/internal/controllers/auth.go b/internal/controllers/auth.go new file mode 100644 index 0000000..1e6f5e3 --- /dev/null +++ b/internal/controllers/auth.go @@ -0,0 +1,67 @@ +package controllers + +import ( + "net/http" + + "studioj/boilerplate_go/internal/models" + "studioj/boilerplate_go/internal/services" + + "github.com/gin-gonic/gin" +) + +type AuthController interface { + Register(*gin.Context) + Login(*gin.Context) +} + +type authController struct { + service services.AuthService +} + +func NewAuthController(service services.AuthService) AuthController { + return &authController{ + service: service, + } +} + +func (controller *authController) Register(c *gin.Context) { + var params models.RegisterRequest + if c.BindJSON(¶ms) != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "bad request"}) + return + } + + user, err := controller.service.Register(¶ms) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + token, err := controller.service.CreateToken(user.ID) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"user": user, "token": token.Token, "refresh_token": token.RefreshToken}) +} + +func (controller *authController) Login(c *gin.Context) { + var params models.LoginRequest + if c.BindJSON(¶ms) != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "bad request"}) + return + } + + user, err := controller.service.Login(¶ms) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + token, err := controller.service.CreateToken(user.ID) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"user": user, "token": token.Token, "refresh_token": token.RefreshToken}) +} diff --git a/internal/controllers/token.go b/internal/controllers/token.go new file mode 100644 index 0000000..37df098 --- /dev/null +++ b/internal/controllers/token.go @@ -0,0 +1,59 @@ +package controllers + +import ( + "net/http" + + "studioj/boilerplate_go/internal/services" + + "github.com/gin-gonic/gin" +) + +type TokenController interface { + Find(*gin.Context) +} + +type tokenController struct { + service services.TokenService +} + +func NewTokenController(service services.TokenService) TokenController { + return &tokenController{ + service: service, + } +} + +func (controller *tokenController) Find(c *gin.Context) { + id := c.Param("id") + user_id := c.GetString("user_id") + + if user_id != id { + c.JSON(http.StatusBadRequest, gin.H{"error": "Wrong user"}) + return + } + + result, err := controller.service.Find(id) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, result) +} + +func (controller *tokenController) List(c *gin.Context) { + id := c.Param("id") + user_id := c.GetString("user_id") + + if user_id != id { + c.JSON(http.StatusBadRequest, gin.H{"error": "Wrong user"}) + return + } + + result, err := controller.service.Find(id) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, result) +} diff --git a/internal/controllers/user.go b/internal/controllers/user.go new file mode 100644 index 0000000..1a645aa --- /dev/null +++ b/internal/controllers/user.go @@ -0,0 +1,46 @@ +package controllers + +import ( + "fmt" + "net/http" + + "studioj/boilerplate_go/internal/services" + + "github.com/gin-gonic/gin" +) + +type UserController interface { + Find(*gin.Context) +} + +type userController struct { + service services.UserService + tokenService services.TokenService +} + +func NewUserController(service services.UserService, tokenService services.TokenService) UserController { + return &userController{ + service: service, + tokenService: tokenService, + } +} + +func (controller *userController) Find(c *gin.Context) { + id := c.Param("id") + user_id := c.GetString("sub") + fmt.Println("id", id) + fmt.Println("user_id", user_id) + + if user_id != id { + c.JSON(http.StatusBadRequest, gin.H{"error": "Wrong user"}) + return + } + + result, err := controller.service.FindByID(id) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, result) +} diff --git a/internal/database/database.go b/internal/database/database.go new file mode 100644 index 0000000..a505f97 --- /dev/null +++ b/internal/database/database.go @@ -0,0 +1,40 @@ +package database + +import ( + config "studioj/boilerplate_go/configs" + "studioj/boilerplate_go/internal/models" + + "gorm.io/driver/mysql" + "gorm.io/gorm" + "gorm.io/gorm/schema" +) + +type Database struct { + *gorm.DB +} + +var DB *gorm.DB + +func Init() { + DB = ConnectDB((config.DATABASE_URL)) +} + +func ConnectDB(url string) *gorm.DB { + gorm_config := gorm.Config{NamingStrategy: schema.NamingStrategy{SingularTable: true}} + db, err := gorm.Open(mysql.Open(url), &gorm_config) + if err != nil { + panic(err) + } + return db +} + +func GetDB() *gorm.DB { + return DB +} + +func AutoMigrate() { + DB.AutoMigrate( + &models.User{}, + &models.Token{}, + ) +} diff --git a/internal/helpers/helpers.go b/internal/helpers/helpers.go new file mode 100644 index 0000000..e7de302 --- /dev/null +++ b/internal/helpers/helpers.go @@ -0,0 +1,29 @@ +package helpers + +import ( + "math/rand" + "os" + "strconv" + "time" +) + +func InLambda() bool { + if lambdaTaskRoot := os.Getenv("LAMBDA_TASK_ROOT"); lambdaTaskRoot != "" { + return true + } + return false +} + +func RandomPin() string { + min := 100000 + max := 999999 + r := rand.New(rand.NewSource(time.Now().UnixNano())) + pin := r.Intn(max-min) + min + return strconv.Itoa(pin) +} + +func Datetime(timeString string) (time.Time, error) { + layout := "2006-01-02T15:04:05.000Z" + result, err := time.Parse(layout, timeString) + return result, err +} diff --git a/internal/middleware/auth.go b/internal/middleware/auth.go new file mode 100644 index 0000000..480afa3 --- /dev/null +++ b/internal/middleware/auth.go @@ -0,0 +1,68 @@ +package middleware + +import ( + "errors" + "fmt" + "net/http" + "strings" + + config "studioj/boilerplate_go/configs" + + "github.com/gin-gonic/gin" + "github.com/golang-jwt/jwt/v5" +) + +func Auth() gin.HandlerFunc { + return func(c *gin.Context) { + sub, err := UserID(c.Request) + if err != nil { + c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"}) + c.Abort() + return + } + + fmt.Println("token", extract(c.Request)) + fmt.Println("sub", *sub) + + c.Set("token", extract(c.Request)) + c.Set("sub", *sub) + c.Next() + } +} + +func extract(r *http.Request) string { + authorization := r.Header.Get("Authorization") + strArr := strings.Split(authorization, " ") + if len(strArr) == 2 { + return strArr[1] + } + return "" +} + +func verify(r *http.Request) (*jwt.Token, error) { + tokenString := extract(r) + jwtToken, err := jwt.Parse(tokenString, func(jwtToken *jwt.Token) (interface{}, error) { + if _, ok := jwtToken.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, fmt.Errorf("unexpected signing method: %v", jwtToken.Header["alg"]) + } + return []byte(config.SECRET_KEY), nil + }) + + return jwtToken, err +} + +func UserID(r *http.Request) (*string, error) { + jwtToken, err := verify(r) + if err != nil { + return nil, err + } + + claims, ok := jwtToken.Claims.(jwt.MapClaims) + if !ok || !jwtToken.Valid { + return nil, errors.New("refresh token is invalid") + } + + sub := claims["sub"].(string) + + return &sub, nil +} diff --git a/internal/middleware/transaction.go b/internal/middleware/transaction.go new file mode 100644 index 0000000..87807ac --- /dev/null +++ b/internal/middleware/transaction.go @@ -0,0 +1,44 @@ +package middleware + +import ( + "log" + "net/http" + + "github.com/gin-gonic/gin" + "gorm.io/gorm" +) + +func StatusInList(status int, statusList []int) bool { + for _, i := range statusList { + for i == status { + return true + } + } + return false +} + +func Transaction(db *gorm.DB) gin.HandlerFunc { + return func(c *gin.Context) { + txHandle := db.Begin() + log.Print("begining database transaction") + + defer func() { + if r := recover(); r != nil { + txHandle.Rollback() + } + }() + + c.Set("db_trx", txHandle) + c.Next() + + if StatusInList(c.Writer.Status(), []int{http.StatusOK, http.StatusCreated}) { + log.Print("committing transactions") + if err := txHandle.Commit().Error; err != nil { + log.Print("transaction commit error: ", err) + } else { + log.Print("rollback transaction due to status code: ", c.Writer.Status()) + txHandle.Rollback() + } + } + } +} diff --git a/internal/models/auth.go b/internal/models/auth.go new file mode 100644 index 0000000..9be2fd1 --- /dev/null +++ b/internal/models/auth.go @@ -0,0 +1,11 @@ +package models + +type LoginRequest struct { + Username string `json:"username"` + Password string `json:"password"` +} + +type RegisterRequest struct { + Username string `json:"username"` + Password string `json:"password"` +} diff --git a/internal/models/token.go b/internal/models/token.go new file mode 100644 index 0000000..7f6a487 --- /dev/null +++ b/internal/models/token.go @@ -0,0 +1,25 @@ +package models + +import "time" + +type Token struct { + ID string `json:"id" db:"id" gorm:"column:id;size:255;primary_key;"` + UserID string `json:"user_id" db:"user_id" gorm:"column:user_id;size:255;index;"` + Token string `json:"token" gorm:"column:token;size:255;index;"` + RefreshToken string `json:"refresh_token" gorm:"column:token;size:255;index;"` + Status string `json:"status" gorm:"column:status;size:10;index;"` + ExpireAt time.Time `json:"expire_at" gorm:"column:expire_at;index;"` + UpdatedAt time.Time `json:"updated_at" gorm:"column:updated_at;index;"` + CreatedAt time.Time `json:"created_at" gorm:"column:created_at;index;"` +} + +type TokenResponse struct { + Token string `json:"token"` + TokenBody TokenBody `json:"tokenBody"` +} + +type TokenBody struct { + ExpireAt time.Time `json:"tokenExpiredDate"` + TokenIdx int `json:"tokenIdx"` + TokenType int `json:"tokenType"` +} diff --git a/internal/models/user.go b/internal/models/user.go new file mode 100644 index 0000000..b761187 --- /dev/null +++ b/internal/models/user.go @@ -0,0 +1,9 @@ +package models + +type User struct { + ID string `json:"id" db:"id" gorm:"column:id;size:255;primary_key;"` + Username string `json:"username" db:"username" gorm:"column:username;size:50;uniqueIndex;"` + Score int32 `json:"score" db:"score" gorm:"column:score;"` + Money int32 `json:"money" db:"money" gorm:"column:money;"` + Password string `json:"-" db:"password" gorm:"column:password;size:255;not null;"` +} diff --git a/internal/repositories/token.go b/internal/repositories/token.go new file mode 100644 index 0000000..4863085 --- /dev/null +++ b/internal/repositories/token.go @@ -0,0 +1,68 @@ +package repositories + +import ( + config "studioj/boilerplate_go/configs" + "studioj/boilerplate_go/internal/models" + + "github.com/golang-jwt/jwt/v5" + "gorm.io/gorm" +) + +type tokenRepository struct { + DB *gorm.DB +} + +func NewTokenRepository(db *gorm.DB) TokenRepository { + return &tokenRepository{ + DB: db, + } +} + +type TokenRepository interface { + Generate(string, int64) (string, error) + Find(string) (*models.Token, error) + Create(*models.Token) (*models.Token, error) + Update(*models.Token) (*models.Token, error) + Delete(string) error +} + +func (s *tokenRepository) Generate(sub string, expire_at int64) (string, error) { + claims := jwt.MapClaims{} + claims["authorized"] = true + claims["sub"] = sub + claims["exp"] = expire_at + at := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + token, err := at.SignedString([]byte(config.SECRET_KEY)) + + return token, err +} + +func (r *tokenRepository) Find(id string) (*models.Token, error) { + var token *models.Token + err := r.DB.Where("id = ?", id).First(&token).Error + return token, err +} + +func (r *tokenRepository) Create(token *models.Token) (*models.Token, error) { + err := r.DB.Create(&token).Error + return token, err +} + +func (r *tokenRepository) Update(token *models.Token) (*models.Token, error) { + var row *models.Token + if err := r.DB.Where("id=?", token.ID).First(&row).Error; err != nil { + return nil, err + } + + err := r.DB.Model(&row).Select("*").Updates(&token).Error + return row, err +} + +func (r *tokenRepository) Delete(id string) error { + var token *models.Token + if err := r.DB.Where("id=?", id).First(&token).Error; err != nil { + return err + } + err := r.DB.Delete(&token).Error + return err +} diff --git a/internal/repositories/user.go b/internal/repositories/user.go new file mode 100644 index 0000000..620a13e --- /dev/null +++ b/internal/repositories/user.go @@ -0,0 +1,68 @@ +package repositories + +import ( + "studioj/boilerplate_go/internal/models" + + "gorm.io/gorm" +) + +type userRepository struct { + DB *gorm.DB +} + +func NewUserRepository(db *gorm.DB) UserRepository { + return &userRepository{ + DB: db, + } +} + +type UserRepository interface { + List() (*[]models.User, error) + FindByID(string) (*models.User, error) + FindByUsername(string) (*models.User, error) + Create(*models.User) (*models.User, error) + Update(*models.User) (*models.User, error) + Delete(string) error +} + +func (r *userRepository) List() (*[]models.User, error) { + var users *[]models.User + err := r.DB.Find(&users).Error + return users, err +} + +func (r *userRepository) FindByID(id string) (*models.User, error) { + var user *models.User + err := r.DB.Where("id = ?", id).First(&user).Error + return user, err +} + +func (r *userRepository) FindByUsername(username string) (*models.User, error) { + var user *models.User + err := r.DB.Where("username = ?", username).First(&user).Error + return user, err +} + +func (r *userRepository) Create(user *models.User) (*models.User, error) { + err := r.DB.Create(&user).Error + return user, err +} + +func (r *userRepository) Update(user *models.User) (*models.User, error) { + var row *models.User + if err := r.DB.Where("id=?", user.ID).First(&row).Error; err != nil { + return nil, err + } + + err := r.DB.Model(&row).Select("*").Updates(&user).Error + return row, err +} + +func (r *userRepository) Delete(id string) error { + var user *models.User + if err := r.DB.Where("id=?", id).First(&user).Error; err != nil { + return err + } + err := r.DB.Delete(&user).Error + return err +} diff --git a/internal/routers/auth.go b/internal/routers/auth.go new file mode 100644 index 0000000..782563f --- /dev/null +++ b/internal/routers/auth.go @@ -0,0 +1,53 @@ +package routers + +import ( + "studioj/boilerplate_go/internal/controllers" + "studioj/boilerplate_go/internal/repositories" + "studioj/boilerplate_go/internal/services" + + "github.com/gin-gonic/gin" + "gorm.io/gorm" +) + +type AuthRouter interface { + SetRouter(db *gorm.DB, router *gin.Engine) +} + +type authRouter struct { + db *gorm.DB + userRepository repositories.UserRepository + tokenRepository repositories.TokenRepository + service services.AuthService + tokenService services.TokenService + controller controllers.AuthController + router *gin.Engine +} + +func InitAuthRouter(db *gorm.DB, router *gin.Engine) { + r := NewAuthRouter(db, router) + r.SetAuthRouter(db, router) +} + +func NewAuthRouter(db *gorm.DB, router *gin.Engine) *authRouter { + userRepository := repositories.NewUserRepository(db) + tokenRepository := repositories.NewTokenRepository(db) + service := services.NewAuthService(userRepository, tokenRepository) + tokenService := services.NewTokenService(tokenRepository) + controller := controllers.NewAuthController(service) + + return &authRouter{ + db: db, + userRepository: userRepository, + tokenRepository: tokenRepository, + service: service, + tokenService: tokenService, + controller: controller, + router: router, + } +} + +func (r *authRouter) SetAuthRouter(db *gorm.DB, router *gin.Engine) { + group := router.Group("/auth") + group.POST("login", r.controller.Login) + group.POST("register", r.controller.Register) +} diff --git a/internal/routers/router.go b/internal/routers/router.go new file mode 100644 index 0000000..7bb9faa --- /dev/null +++ b/internal/routers/router.go @@ -0,0 +1,19 @@ +package routers + +import ( + "github.com/gin-gonic/gin" + + "studioj/boilerplate_go/internal/database" +) + +var Router *gin.Engine + +func Init() { + gin.SetMode(gin.ReleaseMode) + Router = gin.Default() + maindb := database.GetDB() + + InitAuthRouter(maindb, Router) + InitTokenRouter(maindb, Router) + InitUserRouter(maindb, Router) +} diff --git a/internal/routers/token.go b/internal/routers/token.go new file mode 100644 index 0000000..2cc42db --- /dev/null +++ b/internal/routers/token.go @@ -0,0 +1,46 @@ +package routers + +import ( + "studioj/boilerplate_go/internal/controllers" + "studioj/boilerplate_go/internal/repositories" + "studioj/boilerplate_go/internal/services" + + "github.com/gin-gonic/gin" + "gorm.io/gorm" +) + +type TokenRouter interface { + SetRouter(db *gorm.DB, router *gin.Engine) +} + +type tokenRouter struct { + db *gorm.DB + repository repositories.TokenRepository + service services.TokenService + controller controllers.TokenController + router *gin.Engine +} + +func InitTokenRouter(db *gorm.DB, router *gin.Engine) { + r := NewTokenRouter(db, router) + r.SetTokenRouter(db, router) +} + +func NewTokenRouter(db *gorm.DB, router *gin.Engine) *tokenRouter { + repository := repositories.NewTokenRepository(db) + service := services.NewTokenService(repository) + controller := controllers.NewTokenController(service) + + return &tokenRouter{ + db: db, + repository: repository, + service: service, + controller: controller, + router: router, + } +} + +func (r *tokenRouter) SetTokenRouter(db *gorm.DB, router *gin.Engine) { + // group := router.Group("/token") + // group.GET("refresh", middleware.Auth(), r.controller.Refresh) +} diff --git a/internal/routers/user.go b/internal/routers/user.go new file mode 100644 index 0000000..4aeea5c --- /dev/null +++ b/internal/routers/user.go @@ -0,0 +1,50 @@ +package routers + +import ( + "studioj/boilerplate_go/internal/controllers" + "studioj/boilerplate_go/internal/middleware" + "studioj/boilerplate_go/internal/repositories" + "studioj/boilerplate_go/internal/services" + + "github.com/gin-gonic/gin" + "gorm.io/gorm" +) + +type UserRouter interface { + SetRouter(db *gorm.DB, router *gin.Engine) +} + +type userRouter struct { + db *gorm.DB + repository repositories.UserRepository + service services.UserService + controller controllers.UserController + router *gin.Engine +} + +func InitUserRouter(db *gorm.DB, router *gin.Engine) { + r := NewUserRouter(db, router) + r.SetUserRouter(db, router) +} + +func NewUserRouter(db *gorm.DB, router *gin.Engine) *userRouter { + repository := repositories.NewUserRepository(db) + tokenRepository := repositories.NewTokenRepository(db) + service := services.NewUserService(repository, tokenRepository) + + tokenService := services.NewTokenService(tokenRepository) + controller := controllers.NewUserController(service, tokenService) + + return &userRouter{ + db: db, + repository: repository, + service: service, + controller: controller, + router: router, + } +} + +func (r *userRouter) SetUserRouter(db *gorm.DB, router *gin.Engine) { + group := router.Group("/user") + group.GET("/:id", middleware.Auth(), r.controller.Find) +} diff --git a/internal/services/auth.go b/internal/services/auth.go new file mode 100644 index 0000000..5643b6a --- /dev/null +++ b/internal/services/auth.go @@ -0,0 +1,90 @@ +package services + +import ( + "errors" + "studioj/boilerplate_go/internal/models" + "studioj/boilerplate_go/internal/repositories" + "time" + + "github.com/google/uuid" + "golang.org/x/crypto/bcrypt" +) + +type authService struct { + userRepository repositories.UserRepository + tokenRepository repositories.TokenRepository +} + +type AuthService interface { + Register(*models.RegisterRequest) (*models.User, error) + Login(*models.LoginRequest) (*models.User, error) + CreateToken(string) (*models.Token, error) +} + +func NewAuthService(userRepository repositories.UserRepository, tokenRepository repositories.TokenRepository) AuthService { + return &authService{ + userRepository: userRepository, + tokenRepository: tokenRepository, + } +} + +func (s *authService) Register(request *models.RegisterRequest) (*models.User, error) { + hash, err := bcrypt.GenerateFromPassword([]byte(request.Password), 10) + if err != nil { + return nil, errors.New("fail to hash password") + } + + // Create the user + newUser := models.User{ + ID: uuid.NewString(), + Username: request.Username, + Password: string(hash), + } + + user, err := s.userRepository.Create(&newUser) + if err != nil { + return nil, errors.New("fail to create user") + } + + return user, err +} + +func (s *authService) Login(request *models.LoginRequest) (*models.User, error) { + user, err := s.userRepository.FindByUsername(request.Username) + if err != nil || user == nil { + return nil, errors.New("invalid user or password") + } + + err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(request.Password)) + if err != nil { + return nil, errors.New("invalid user or password") + } + return user, nil +} + +func (s *authService) CreateToken(user_id string) (*models.Token, error) { + tokenExpiredAt := time.Now().Add(time.Hour * 24 * 30) + accessToken, err := s.tokenRepository.Generate(user_id, tokenExpiredAt.Unix()) + if err != nil { + return nil, err + } + + refreshExpiredAt := time.Now().Add(time.Hour * 24 * 90) + refreshToken, err := s.tokenRepository.Generate(user_id, refreshExpiredAt.Unix()) + if err != nil { + return nil, err + } + + newToken := &models.Token{ + ID: uuid.NewString(), + UserID: user_id, + Token: accessToken, + RefreshToken: refreshToken, + Status: "valid", + ExpireAt: tokenExpiredAt, + } + + token, err := s.tokenRepository.Create(newToken) + + return token, err +} diff --git a/internal/services/token.go b/internal/services/token.go new file mode 100644 index 0000000..9c812c7 --- /dev/null +++ b/internal/services/token.go @@ -0,0 +1,115 @@ +package services + +import ( + "fmt" + "strings" + + config "studioj/boilerplate_go/configs" + "studioj/boilerplate_go/internal/models" + "studioj/boilerplate_go/internal/repositories" + + "github.com/golang-jwt/jwt/v5" +) + +type tokenService struct { + repository repositories.TokenRepository +} + +type TokenService interface { + Find(string) (*models.Token, error) + Create(*models.Token) (*models.Token, error) + Update(*models.Token) (*models.Token, error) + Delete(string) error + + Generate(string, int64) (string, error) + Verify(tokenString string) (*jwt.Token, error) + + GetJwtToken(string) (*jwt.Token, error) + ExtractTokenString(string) string + VerifyTokenString(string) (*jwt.Token, error) + ValidToken(*jwt.Token) (bool, error) +} + +func NewTokenService(repository repositories.TokenRepository) TokenService { + return &tokenService{ + repository: repository, + } +} + +func (s *tokenService) Find(id string) (*models.Token, error) { + return s.repository.Find(id) +} + +func (s *tokenService) Create(token *models.Token) (*models.Token, error) { + return s.repository.Create(token) +} + +func (s *tokenService) Update(token *models.Token) (*models.Token, error) { + return s.repository.Update(token) +} + +func (s *tokenService) Delete(id string) error { + return s.repository.Delete(id) +} + +func (s *tokenService) Verify(tokenString string) (*jwt.Token, error) { + jwtToken, err := jwt.Parse(tokenString, func(jwtToken *jwt.Token) (interface{}, error) { + if _, ok := jwtToken.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, fmt.Errorf("unexpected signing method: %v", jwtToken.Header["alg"]) + } + return []byte(config.SECRET_KEY), nil + }) + + return jwtToken, err +} + +func (s *tokenService) Generate(user_id string, expire_at int64) (string, error) { + claims := jwt.MapClaims{} + claims["authorized"] = true + claims["sub"] = user_id + claims["exp"] = expire_at + at := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + token, err := at.SignedString([]byte(config.SECRET_KEY)) + + return token, err +} + +func (s *tokenService) GetJwtToken(tokenString string) (*jwt.Token, error) { + jwtToken, err := jwt.Parse(tokenString, func(jwtToken *jwt.Token) (interface{}, error) { + if _, ok := jwtToken.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, fmt.Errorf("unexpected signing method: %v", jwtToken.Header["alg"]) + } + return []byte(config.SECRET_KEY), nil + }) + return jwtToken, err +} + +func (s *tokenService) ExtractTokenString(authorization string) string { + strArr := strings.Split(authorization, " ") + if len(strArr) == 2 { + return strArr[1] + } + return "" +} + +func (s *tokenService) VerifyTokenString(tokenString string) (*jwt.Token, error) { + jwtToken, err := jwt.Parse(tokenString, func(jwtToken *jwt.Token) (interface{}, error) { + if _, ok := jwtToken.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, fmt.Errorf("unexpected signing method: %v", jwtToken.Header["alg"]) + } + return []byte(config.SECRET_KEY), nil + }) + + if err != nil { + return nil, err + } + + return jwtToken, nil +} + +func (s *tokenService) ValidToken(jwtToken *jwt.Token) (bool, error) { + if jwtToken == nil { + return false, fmt.Errorf("no token") + } + return jwtToken.Valid, nil +} diff --git a/internal/services/user.go b/internal/services/user.go new file mode 100644 index 0000000..443b37c --- /dev/null +++ b/internal/services/user.go @@ -0,0 +1,38 @@ +package services + +import ( + "studioj/boilerplate_go/internal/models" + "studioj/boilerplate_go/internal/repositories" +) + +type userService struct { + repository repositories.UserRepository + tokenRepository repositories.TokenRepository +} + +type UserService interface { + FindByID(string) (*models.User, error) + FindByUsername(string) (*models.User, error) + Create(*models.User) (*models.User, error) +} + +func NewUserService(repository repositories.UserRepository, tokenRepository repositories.TokenRepository) UserService { + return &userService{ + repository: repository, + tokenRepository: tokenRepository, + } +} + +func (s *userService) FindByID(id string) (*models.User, error) { + return s.repository.FindByID(id) +} + +func (s *userService) FindByUsername(username string) (*models.User, error) { + return s.repository.FindByUsername(username) +} + +func (s *userService) Create(user *models.User) (*models.User, error) { + result, err := s.repository.Create(user) + + return result, err +} diff --git a/tests/test.db b/tests/test.db new file mode 100644 index 0000000..613ed74 Binary files /dev/null and b/tests/test.db differ diff --git a/tests/token_test.go b/tests/token_test.go new file mode 100644 index 0000000..149573a --- /dev/null +++ b/tests/token_test.go @@ -0,0 +1,245 @@ +package octet_test + +// type TokenTestSuite struct { +// suite.Suite +// db *gorm.DB +// repository repositories.TokenRepository +// service services.TokenService +// controller controllers.TokenController +// } + +// func (suite *TokenTestSuite) SetupSuite() { +// err := os.Remove("test.db") +// if err != nil { +// suite.Fail("Failed to remove the test database file") +// } + +// database.Init() +// gorm_config := gorm.Config{NamingStrategy: schema.NamingStrategy{SingularTable: true}} +// db, _ := gorm.Open(sqlite.Open("test.db"), &gorm_config) +// repository := repositories.NewTokenRepository(db) +// service := services.NewTokenService(repository) +// controller := controllers.NewTokenController(service) + +// suite.db = db +// suite.service = service +// suite.repository = repository +// suite.controller = controller + +// suite.CreateSampleData() +// } + +// func (suite *TokenTestSuite) CreateSampleData() { +// suite.db.AutoMigrate(&models.Token{}) +// var tokens []models.Token +// for i := 1; i < 100; i++ { +// expire_at := time.Now().Add(time.Hour * 24 * time.Duration(i+45)) +// tokenString, _ := suite.service.Generate("superrichquiz_octet", expire_at.Unix()) +// token := models.Token{ +// ID: uuid.NewString(), +// Token: tokenString, +// Status: "valid", +// ExpireAt: expire_at, +// UpdatedAt: time.Now(), +// CreatedAt: time.Now(), +// } +// tokens = append(tokens, token) +// } + +// for _, token := range tokens { +// suite.db.Create(&token) +// } +// } + +// func (suite *TokenTestSuite) TearDownSuite() { +// // suite.db.Migrator().DropTable(&models.Token{}) + +// // err := os.Remove("test.db") +// // if err != nil { +// // suite.Fail("Failed to remove the test database file") +// // } +// } +// func (suite *TokenTestSuite) SetupTest() { +// // suite.userRepository.DeleteByName("testUserName0001") +// } + +// func (suite *TokenTestSuite) TearDownTest() { + +// } + +// func TestTokenSuite(t *testing.T) { +// suite.Run(t, new(TokenTestSuite)) +// } + +// // 토큰 생성 테스트 +// func (suite *TokenTestSuite) TestGenerateTokenSuccess() { +// user_id := "testuser" +// expire_at := time.Now().Add(time.Hour * 24 * 365).Unix() +// token, err := suite.service.Generate(user_id, expire_at) +// assert.NoError(suite.T(), err) +// assert.NotNil(suite.T(), token) +// } + +// // 토큰 생성 테스트 +// func (suite *TokenTestSuite) TestGenerateTokenString() { +// user_id := "superrichquiz_octet" +// expire_at := time.Now().Add(time.Hour * 24 * 365 * 100).Unix() +// tokenString, err := suite.service.Generate(user_id, expire_at) +// assert.NoError(suite.T(), err) + +// jwtToken, err := suite.service.VerifyTokenString(tokenString) +// assert.NoError(suite.T(), err) +// assert.NotNil(suite.T(), jwtToken) +// } + +// // 토큰 검증 테스트 +// func (suite *TokenTestSuite) TestVerifyTokenString() { +// user_id := "testuser" +// expire_at := time.Now().Add(time.Hour * 24 * 365).Unix() +// tokenString, err := suite.service.Generate(user_id, expire_at) +// assert.NoError(suite.T(), err) + +// jwtToken, err := suite.service.VerifyTokenString(tokenString) +// assert.NoError(suite.T(), err) +// assert.NotNil(suite.T(), jwtToken) +// assert.Equal(suite.T(), tokenString, jwtToken.Raw) +// } + +// // 토큰 유효성 테스트 +// func (suite *TokenTestSuite) TestValidToken() { +// user_id := "testuser" +// expire_at := time.Now().Add(time.Hour * 24 * 365).Unix() +// tokenString, err := suite.service.Generate(user_id, expire_at) +// assert.NoError(suite.T(), err) + +// jwtToken, err := suite.service.VerifyTokenString(tokenString) +// assert.NoError(suite.T(), err) +// assert.NotNil(suite.T(), jwtToken) + +// valid, err := suite.service.ValidToken(jwtToken) +// assert.NoError(suite.T(), err) +// assert.True(suite.T(), valid) +// } + +// // 토큰 유효성 테스트 : Expire +// func (suite *TokenTestSuite) TestExpiredToken() { +// user_id := "testuser" +// expire_at := time.Now().Add(time.Hour * 1).Unix() +// tokenString, err := suite.service.Generate(user_id, expire_at) +// assert.NoError(suite.T(), err) + +// jwtToken, err := suite.service.GetJwtToken(tokenString) +// assert.NoError(suite.T(), err) +// // assert.Nil(suite.T(), jwtToken) + +// expirationTime, err := jwtToken.Claims.GetExpirationTime() +// assert.NoError(suite.T(), err) +// assert.Greater(suite.T(), expirationTime.Time, time.Now()) + +// expire_at = time.Now().Add(-time.Hour * 1).Unix() +// tokenString, err = suite.service.Generate(user_id, expire_at) +// assert.NoError(suite.T(), err) + +// jwtToken, err = suite.service.GetJwtToken(tokenString) +// assert.Error(suite.T(), err) +// // assert.Nil(suite.T(), jwtToken) + +// expirationTime, err = jwtToken.Claims.GetExpirationTime() +// assert.NoError(suite.T(), err) +// assert.Greater(suite.T(), time.Now(), expirationTime.Time) +// } + +// func (suite *TokenTestSuite) TestCreateToken() { +// dateString := "2023-08-15T01:50:08.706Z" +// layout := "2006-01-02T15:04:05.000Z" +// tokenString := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbklkeCI6MTc2NTQyLCJ0b2tlblR5cGUiOiJXQUxMRVQiLCJ0b2tlbkV4cGlyZWREYXRlIjoiMjAyMy0wOC0xNVQwMTo1MDowOC43MDZaIn0.skgP6ysLNx6KRDBYZy3miZW1Q95iV_Cw0ZVWXj1tJyw" +// expire_at, _ := time.Parse(layout, dateString) +// token := models.Token{ +// ID: uuid.NewString(), +// Token: tokenString, +// Status: "valid", +// ExpireAt: expire_at, +// } +// result, err := suite.repository.Create(&token) + +// assert.NoError(suite.T(), err) +// assert.Equal(suite.T(), tokenString, result.Token) +// } + +// // 리프레시할 토큰 목록 +// func (suite *TokenTestSuite) TestListOfTokensToRefresh() { +// tokens, err := suite.repository.ListRefresh() +// assert.NoError(suite.T(), err) +// assert.NotNil(suite.T(), tokens) +// assert.Greater(suite.T(), len(*tokens), 0) +// } + +// // 토큰 리프레시 +// func (suite *TokenTestSuite) TestOldTokenRefresh() { +// tokens, err := suite.repository.ListRefresh() +// assert.NoError(suite.T(), err) +// assert.NotNil(suite.T(), tokens) + +// for _, token := range *tokens { +// token.Status = "expired" +// result, err := suite.repository.Update(&token) +// assert.NoError(suite.T(), err) +// assert.Equal(suite.T(), "expired", result.Status) + +// user_id := "superrichquiz_octet" +// expire_at := result.ExpireAt.Add(time.Hour * 24 * 90) +// tokenString, err := suite.service.Generate(user_id, expire_at.Unix()) +// assert.NoError(suite.T(), err) + +// refreshedToken := models.Token{ +// ID: uuid.NewString(), +// Token: tokenString, +// Status: "valid", +// ExpireAt: expire_at, +// } + +// newToken, err := suite.repository.Create(&refreshedToken) +// assert.NoError(suite.T(), err) +// assert.Equal(suite.T(), tokenString, newToken.Token) +// } +// } + +// func (suite *TokenTestSuite) TestResponseToken() { +// respBody := []byte(`{ +// "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbklkeCI6MTc1MzU4LCJ0b2tlblR5cGUiOiJXQUxMRVQiLCJ0b2tlbkV4cGlyZWREYXRlIjoiMjAyMy0wOC0xNFQwNToxMTo1Ni41MzhaIn0.e1i4_y8ItC8Vje13Ew8NHwZTElOMBObIZjpGLgRCdyE", +// "tokenBody": { +// "tokenExpiredDate": "2023-08-13T06:06:12.065Z", +// "tokenIdx": 174025, +// "tokenType": "WALLET" +// } +// }`) + +// var response map[string]interface{} +// err := json.Unmarshal(respBody, &response) +// assert.NoError(suite.T(), err) +// assert.NotNil(suite.T(), response) +// assert.Equal(suite.T(), response["token"], "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbklkeCI6MTc1MzU4LCJ0b2tlblR5cGUiOiJXQUxMRVQiLCJ0b2tlbkV4cGlyZWREYXRlIjoiMjAyMy0wOC0xNFQwNToxMTo1Ni41MzhaIn0.e1i4_y8ItC8Vje13Ew8NHwZTElOMBObIZjpGLgRCdyE") + +// tokenBody := response["tokenBody"].(map[string]interface{}) +// tokenExpiredDate := tokenBody["tokenExpiredDate"].(string) +// assert.Equal(suite.T(), "2023-08-13T06:06:12.065Z", tokenExpiredDate) +// } + +// func (suite *TokenTestSuite) TestResponseError() { +// respBody := []byte(`{ +// "errorCode": "ERR_0105001", +// "message": "Invalid token" +// }`) + +// var response map[string]interface{} +// err := json.Unmarshal(respBody, &response) +// assert.NoError(suite.T(), err) +// assert.NotNil(suite.T(), response) + +// assert.NotNil(suite.T(), response["errorCode"]) +// assert.Equal(suite.T(), "ERR_0105001", response["errorCode"].(string)) + +// if response["errorCode"] != nil && response["errorCode"].(string) == "ERR_0105001" { +// assert.True(suite.T(), true) +// } +// }