diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f36d5dd --- /dev/null +++ b/.gitignore @@ -0,0 +1,46 @@ +# If you prefer the allow list template instead of the deny list, see community template: +# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore +# +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# Go workspace file +go.work +go.work.sum + +# env file +.env + +# tmp files +tmp/ + +#React gitignore + +.idea/ +.vscode/ +node_modules/ +build +.DS_Store +*.tgz +my-app* +template/src/__tests__/__snapshots__/ +lerna-debug.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +/.changelog +.npm/ +yarn.lock \ No newline at end of file diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..33a9ec2 --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,10 @@ +FROM golang:1.24.1-alpine3.21 + +WORKDIR /app + +RUN go install github.com/air-verse/air@latest + +COPY . . +RUN go mod download + +CMD ["air", "run"] \ No newline at end of file diff --git a/backend/README.md b/backend/README.md new file mode 100644 index 0000000..44731e6 --- /dev/null +++ b/backend/README.md @@ -0,0 +1,265 @@ +# 🏭 Carbon Offset API + +![GitHub go.mod Go version](https://img.shields.io/github/go-mod/go-version/joaooliveira247/backend-test/dev/backend) + +## 💻 Requirements: + +### `Go >= 1.24.1` + +### [`Docker`](https://www.docker.com/) & [`Docker compose`](https://docs.docker.com/compose/) + +## 📜 Documentation: + +`/users/` + +
+POST /users/ + +- **Description**: Creates a new user. + +- **Headers**: + + ```plaintext + Content-Type: application/json + + ``` + +- **Request Body**: + + ```json + { + "name": "john", + "email": "john@gmail.com", + "phone": "+5519912345678" + } + ``` + +- **Success Response (201 Created)**: + + ```json + { + "affiliateCode": "1d47bbe5-c7d3-4580-ad2a-c4b192eeeb47" + } + ``` + +- **Errors**: + + - **400 Bad Request**: Invalid request body. + + - **500 Internal Server Error**: Failed to create the entity. + +- **Example Request with cURL**: + +```curl +curl -X POST localhost:8000/users/ \ +-H "Content-Type: application/json" \ +-d '{ + "name": "john", + "email": "john@gmail.com", + "phone": "+5519912345678" +}' +``` +
+ +### + +`/competitions/` + +
+POST /competitions/ + +- **Description**: Creates a new competition. + +- **Headers**: + + ```plaintext + Content-Type: application/json + + ``` + +- **Request Body**: + + ```json + ``` + +- **Success Response (201 Created)**: + + ```json + { + "id": "06ae5f86-46dd-42d3-8e6d-2abe26f6b07e" + } + ``` + +- **Errors**: + + - **409 Conflict**: competition already activated. + + - **500 Internal Server Error**: error create competition. + +- **Example Request with cURL**: + +```curl +curl -X POST localhost:8000/competitions/ \ +-H "Content-Type: application/json" \ +``` +
+ +
+GET /competitions/ + +- **Description**: Get competition activated. + +- **Headers**: + + ```plaintext + Content-Type: application/json + + ``` + +- **Success Response (200 OK / 204 No Content)**: + + ```json + { + "id": "9fbae8ae-3ba9-4582-a931-04d2e3c6aa93", + "createdAt": 1743096181, + "status": true + } + ``` + +- **Example Request with cURL**: + +```curl +curl -X GET localhost:8000/competitions/ \ +-H "Content-Type: application/json" \ +``` +
+ +
+PUT /competitions/ + +- **Description**: Close Competition. + +- **Headers**: + + ```plaintext + Content-Type: application/json + + ``` + +- **Query Parameters**: + + **ID** (required, UUID): Competiton ID. + +- **Request Body**: + + ```json + ``` + +- **Success Response (204 No Content)**: + + ```json + ``` + +- **Errors**: + + - **400 Bad Request**: invalid id. + + - **404 Not Found**: competition not found. + + - **409 Conflict**: competition already activated. + + - **500 Internal Server Error**: closed competition error. + +- **Example Request with cURL**: + +```curl +curl -X PUT localhost:8000/competitions/?ID=e4b5c0cc-2f47-4b29-9883-84f314600f71\ +-H "Content-Type: application/json" \ +``` +
+ +
+GET /competitions/reports/ + +- **Description**: Get competition reports. + +- **Headers**: + + ```plaintext + Content-Type: application/json + ``` + +- **Query Parameters**: + + **ID** (required, UUID): Competiton ID. + +- **Success Response (200 OK)**: + + ```json + [ + { + "name": "User 6", + "points": 10 + }, + { + "name": "User 5", + "points": 10 + }, + { + "name": "User 13", + "points": 9 + }, + { + "name": "User 8", + "points": 8 + }, + { + "name": "User 9", + "points": 8 + }, + { + "name": "User 7", + "points": 6 + }, + { + "name": "User 2", + "points": 4 + }, + { + "name": "User 11", + "points": 4 + }, + { + "name": "User 15", + "points": 4 + }, + { + "name": "User 1", + "points": 2 + } + ] + ``` + +- **Example Request with cURL**: + +```curl +curl -X GET localhost:8000/competitions/reports/?ID=d3dd9c62-5cc0-4b57-b341-ea1876dadac6 \ +-H "Content-Type: application/json" \ +``` +
+ +## 🗄️ Database Diagram + +![database_diagram](https://i.imgur.com/CaK8HUN.png) + +## 📦 Usage libraries: + +- [gin](github.com/gin-gonic/gin) + +- [gorm](https://gorm.io/) + +- [gorm/postgres](https://github.com/go-gorm/postgres) + +- [uuid](github.com/google/uuid) + +- [gomail](https://pkg.go.dev/gopkg.in/gomail.v2?utm_source=godoc) \ No newline at end of file diff --git a/backend/go.mod b/backend/go.mod new file mode 100644 index 0000000..0ad035c --- /dev/null +++ b/backend/go.mod @@ -0,0 +1,50 @@ +module github.com/joaooliveira247/backend-test + +go 1.24.1 + +require ( + github.com/gin-gonic/gin v1.10.0 + github.com/google/uuid v1.6.0 + gorm.io/driver/postgres v1.5.11 + gorm.io/gorm v1.25.12 +) + +require ( + github.com/bytedance/sonic v1.13.2 // indirect + github.com/bytedance/sonic/loader v0.2.4 // indirect + github.com/cloudwego/base64x v0.1.5 // indirect + github.com/gabriel-vasile/mimetype v1.4.8 // indirect + github.com/gin-contrib/sse v1.0.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.25.0 // indirect + github.com/goccy/go-json v0.10.5 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jackc/pgx/v5 v5.7.4 // indirect + github.com/jackc/puddle/v2 v2.2.2 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.2.10 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/mattn/go-isatty v0.0.20 // 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.2.3 // indirect + github.com/rogpeppe/go-internal v1.14.1 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.12 // indirect + golang.org/x/arch v0.15.0 // indirect + golang.org/x/crypto v0.36.0 // indirect + golang.org/x/net v0.37.0 // indirect + golang.org/x/sync v0.12.0 // indirect + golang.org/x/sys v0.31.0 // indirect + golang.org/x/text v0.23.0 // indirect + golang.org/x/time v0.11.0 // indirect + google.golang.org/protobuf v1.36.6 // indirect + gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect + gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/backend/go.sum b/backend/go.sum new file mode 100644 index 0000000..d638a93 --- /dev/null +++ b/backend/go.sum @@ -0,0 +1,120 @@ +github.com/bytedance/sonic v1.13.2 h1:8/H1FempDZqC4VqjptGo14QQlJx8VdZJegxs6wwfqpQ= +github.com/bytedance/sonic v1.13.2/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4= +github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY= +github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= +github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4= +github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= +github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +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.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= +github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= +github.com/gin-contrib/sse v1.0.0 h1:y3bT1mUWUxDpW4JLQg/HnTqV4rozuW4tC9eFKTxYI9E= +github.com/gin-contrib/sse v1.0.0/go.mod h1:zNuFdwarAygJBht0NTKiSi3jRf6RbqeILZ9Sp6Slhe0= +github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= +github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +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.25.0 h1:5Dh7cjvzR7BRZadnsVOzPhWsrwUr0nmsZJxEAnFLNO8= +github.com/go-playground/validator/v10 v10.25.0/go.mod h1:GGzBIJMuE98Ic/kJsBXbz1x/7cByt++cQ+YOuDM5wus= +github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= +github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +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.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.7.4 h1:9wKznZrhWa2QiHL+NjTSPP6yjl3451BX3imWDnokYlg= +github.com/jackc/pgx/v5 v5.7.4/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +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.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= +github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/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.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= +github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= +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/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +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/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +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.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +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.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= +github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +golang.org/x/arch v0.15.0 h1:QtOrQd0bTUnhNVNndMpLHNWrDmYzZ2KDqSrEymqInZw= +golang.org/x/arch v0.15.0/go.mod h1:JmwW7aLIoRUKgaTzhkiEFxvcEiQGyOg9BMonBJUS7EE= +golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= +golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= +golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= +golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= +golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= +golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= +golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +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.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= +google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= +gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk= +gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE= +gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/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/postgres v1.5.11 h1:ubBVAfbKEUld/twyKZ0IYn9rSQh448EdelLYk9Mv314= +gorm.io/driver/postgres v1.5.11/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI= +gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8= +gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ= +nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= diff --git a/backend/main.go b/backend/main.go new file mode 100644 index 0000000..35092b5 --- /dev/null +++ b/backend/main.go @@ -0,0 +1,37 @@ +package main + +import ( + "log" + + "github.com/gin-gonic/gin" + "github.com/joaooliveira247/backend-test/src/config" + "github.com/joaooliveira247/backend-test/src/db" + "github.com/joaooliveira247/backend-test/src/middlewares" + "github.com/joaooliveira247/backend-test/src/routes" +) + +func init() { + config.LoadEnv() +} + +func main() { + + gormDB, err := db.GetDBConnection() + + if err != nil { + log.Fatalf("DATABASE Connection: %v", err) + } + + if err := db.CreateTables(gormDB); err != nil { + log.Fatalf("DATABASE Tables Creation: %v", err) + return + } + + api := gin.Default() + api.Use(middlewares.RateLimiter) + routes.RegistryRoutes(api) + + if err := api.Run(":8000"); err != nil { + log.Fatalf("API RUN: err") + } +} diff --git a/backend/src/config/config.go b/backend/src/config/config.go new file mode 100644 index 0000000..9176f3b --- /dev/null +++ b/backend/src/config/config.go @@ -0,0 +1,15 @@ +package config + +import "os" + +var ( + DATABASE_URL = "" + SERVICE_EMAIL = "" + PASSWORD_SERVICE_EMAIL = "" +) + +func LoadEnv() { + DATABASE_URL = os.Getenv("DATABASE_URL") + SERVICE_EMAIL = os.Getenv("SERVICE_EMAIL") + PASSWORD_SERVICE_EMAIL = os.Getenv("PASSWORD_SERVICE_EMAIL") +} diff --git a/backend/src/controllers/competitions.go b/backend/src/controllers/competitions.go new file mode 100644 index 0000000..896def3 --- /dev/null +++ b/backend/src/controllers/competitions.go @@ -0,0 +1,173 @@ +package controllers + +import ( + "errors" + "fmt" + "log" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" + "github.com/joaooliveira247/backend-test/src/models" + "github.com/joaooliveira247/backend-test/src/repositories" + "github.com/joaooliveira247/backend-test/src/services" + "gorm.io/gorm" +) + +type CompetitionsController struct { + UserRepository repositories.UsersRepository + CompetitionRepository repositories.CompetitionsRepository + PointRepository repositories.PointsRepository +} + +func NewCompetitionsController( + userRepo repositories.UsersRepository, + compRepo repositories.CompetitionsRepository, + pointRepo repositories.PointsRepository, +) *CompetitionsController { + return &CompetitionsController{userRepo, compRepo, pointRepo} +} + +func (ctrl *CompetitionsController) Create(ctx *gin.Context) { + compCheck, _ := ctrl.CompetitionRepository.GetActiveCompetition() + + if compCheck.IsEmpty() { + id, err := ctrl.CompetitionRepository.Create( + &models.Competitions{Status: true}, + ) + + if err != nil { + ctx.JSON( + http.StatusInternalServerError, + gin.H{"error": "create competition", "details": err.Error()}, + ) + return + } + ctx.JSON(http.StatusCreated, gin.H{"id": id}) + return + } + + ctx.JSON( + http.StatusConflict, + gin.H{ + "error": "competition already activated", + "details": compCheck.ID, + }, + ) +} + +func (ctrl *CompetitionsController) GetCompetition(ctx *gin.Context) { + // main request for main screen + compCheck, _ := ctrl.CompetitionRepository.GetActiveCompetition() + + if !compCheck.IsEmpty() { + if code := ctx.Query("affiliateCode"); code != "" { + user, _ := ctrl.UserRepository.GetUserByAffiliateCode(code) + + if !user.IsEmpty() { + ctrl.PointRepository.AddPoint( + &models.Points{ + UserID: user.ID, + CompetitionID: compCheck.ID, + }, + ) + services.SendEmail( + user.Email, + "Carbon Offset Competition", + fmt.Sprintf( + "+1 point using your affiliate code: %s", + code, + ), + ) + } + } + ctx.JSON(http.StatusOK, compCheck) + return + } + ctx.JSON(http.StatusNoContent, nil) +} + +func (ctrl *CompetitionsController) CloseCompetition(ctx *gin.Context) { + compID, err := uuid.Parse(ctx.Query("ID")) + + if err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{ + "error": "invalid id format", "details": err.Error(), + }) + return + } + + if err := ctrl.CompetitionRepository.CloseCompetition(compID); err != nil { + + if errors.Is(err, gorm.ErrRecordNotFound) { + ctx.JSON( + http.StatusNotFound, + gin.H{ + "error": "competition not found", + "details": err.Error(), + }, + ) + return + } + + ctx.JSON( + http.StatusInternalServerError, + gin.H{"error": "closed competition error", "details": err.Error()}, + ) + return + } + + winners, err := ctrl.CompetitionRepository.GetCompetitionReport(compID) + + if err != nil { + log.Printf("error pick winners: %v", err) + } + + for i, report := range winners { + services.SendEmail( + report.Email, + "Carbon Offset Competition Ended", + fmt.Sprintf( + "Congratulations Carbon Offset Competition: %s ended, your position was %d", + compID, + i + 1, + ), + ) + } + + ctx.JSON(http.StatusNoContent, nil) +} + +func (ctrl *CompetitionsController) GetCompetitionReport(ctx *gin.Context) { + compID, err := uuid.Parse(ctx.Query("ID")) + + if err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{ + "error": "invalid id format", "details": err.Error(), + }) + return + } + + comp, _ := ctrl.CompetitionRepository.GetCompetitionByID(compID) + + if comp.Status { + ctx.JSON(http.StatusConflict, gin.H{"error": "competition not closed"}) + return + } + + reports, err := ctrl.CompetitionRepository.GetCompetitionReport(compID) + + if err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{ + "error": "gen competition report", "details": err.Error(), + }) + return + } + + if reports == nil { + ctx.JSON(http.StatusNotFound, gin.H{"error": "competiton not found"}) + return + } + + ctx.JSON(http.StatusOK, reports) +} diff --git a/backend/src/controllers/users.go b/backend/src/controllers/users.go new file mode 100644 index 0000000..be4ef9f --- /dev/null +++ b/backend/src/controllers/users.go @@ -0,0 +1,79 @@ +package controllers + +import ( + "net/http" + + "github.com/gin-gonic/gin" + "github.com/joaooliveira247/backend-test/src/models" + "github.com/joaooliveira247/backend-test/src/repositories" + "github.com/joaooliveira247/backend-test/src/utils" +) + +type UsersController struct { + userRepository repositories.UsersRepository + competitionRepository repositories.CompetitionsRepository + pointRepository repositories.PointsRepository +} + +func NewUsersController( + userRepo repositories.UsersRepository, + compRepo repositories.CompetitionsRepository, + pointRepo repositories.PointsRepository, +) *UsersController { + return &UsersController{userRepo, compRepo, pointRepo} +} + +func (ctrl *UsersController) CreateUser(ctx *gin.Context) { + var user models.Users + + if err := ctx.ShouldBindJSON(&user); err != nil { + ctx.JSON( + http.StatusBadRequest, + gin.H{"error": "invalid request body", "details": err.Error()}, + ) + return + } + + if err := user.Validate(); err != nil { + ctx.JSON( + http.StatusBadRequest, + gin.H{"error": "invalid request body", "details": err.Error()}, + ) + return + } + + code, err := utils.GenerateAffiliateCode() + + if err != nil { + ctx.JSON( + http.StatusInternalServerError, + gin.H{ + "error": "error when try create affiliate code", + "details": err.Error(), + }, + ) + return + } + + user.AffiliateCode = code + + affiliateCode, err := ctrl.userRepository.Create(&user) + + if err != nil { + ctx.JSON( + http.StatusInternalServerError, + gin.H{"error": "database error", "details": err.Error()}, + ) + return + } + + comp, _ := ctrl.competitionRepository.GetActiveCompetition() + + if !comp.IsEmpty() { + ctrl.pointRepository.AddPoint( + &models.Points{UserID: user.ID, CompetitionID: comp.ID}, + ) + } + + ctx.JSON(http.StatusCreated, gin.H{"affiliateCode": affiliateCode}) +} \ No newline at end of file diff --git a/backend/src/db/db.go b/backend/src/db/db.go new file mode 100644 index 0000000..c54f720 --- /dev/null +++ b/backend/src/db/db.go @@ -0,0 +1,28 @@ +package db + +import ( + "github.com/joaooliveira247/backend-test/src/config" + "github.com/joaooliveira247/backend-test/src/models" + "gorm.io/driver/postgres" + "gorm.io/gorm" + "gorm.io/gorm/logger" +) + +func GetDBConnection() (*gorm.DB, error) { + db, err := gorm.Open( + postgres.Open(config.DATABASE_URL), + &gorm.Config{Logger: logger.Default.LogMode(logger.Info)}, + ) + + if err != nil { + return nil, err + } + return db, nil +} + +func CreateTables(db *gorm.DB) error { + if err := db.AutoMigrate(&models.Users{}, &models.Competitions{}, &models.Points{}); err != nil { + return err + } + return nil +} diff --git a/backend/src/middlewares/limit.go b/backend/src/middlewares/limit.go new file mode 100644 index 0000000..364e486 --- /dev/null +++ b/backend/src/middlewares/limit.go @@ -0,0 +1,37 @@ +package middlewares + +import ( + "net/http" + "sync" + + "github.com/gin-gonic/gin" + "golang.org/x/time/rate" +) + +var limiters = sync.Map{} + +func getLimiter(ip string) *rate.Limiter { + limiter, ok := limiters.Load(ip) + + if !ok { + newLimiter := rate.NewLimiter(1, 5) + limiters.Store(ip, newLimiter) + return newLimiter + } + return limiter.(*rate.Limiter) +} + +func RateLimiter(ctx *gin.Context) { + ip := ctx.ClientIP() + limiter := getLimiter(ip) + + if !limiter.Allow() { + ctx.JSON( + http.StatusTooManyRequests, + gin.H{"error": "too many requests"}, + ) + ctx.Abort() + return + } + ctx.Next() +} diff --git a/backend/src/models/competiotions.go b/backend/src/models/competiotions.go new file mode 100644 index 0000000..6c061ef --- /dev/null +++ b/backend/src/models/competiotions.go @@ -0,0 +1,19 @@ +package models + +import "github.com/google/uuid" + +type Competitions struct { + BaseModel + Status bool `json:"status" gorm:"type:boolean;not null;column:status"` +} + +type CompetitionReport struct { + Name string `json:"name" gorm:"column:name"` + Points uint64 `json:"points" gorm:"column:points"` + Email string `json:"-" gorm:"column:email"` + Phone string `json:"-" gorm:"column:phone"` +} + +func (comp *Competitions) IsEmpty() bool { + return comp.ID == uuid.Nil +} diff --git a/backend/src/models/models.go b/backend/src/models/models.go new file mode 100644 index 0000000..8f7fc96 --- /dev/null +++ b/backend/src/models/models.go @@ -0,0 +1,8 @@ +package models + +import "github.com/google/uuid" + +type BaseModel struct { + ID uuid.UUID `json:"id" gorm:"primaryKey;type:uuid;default:gen_random_uuid()"` + CreatedAt int64 `json:"createdAt" gorm:"autoCreateTime"` +} diff --git a/backend/src/models/points.go b/backend/src/models/points.go new file mode 100644 index 0000000..fd7f0e9 --- /dev/null +++ b/backend/src/models/points.go @@ -0,0 +1,9 @@ +package models + +import "github.com/google/uuid" + +type Points struct { + BaseModel + CompetitionID uuid.UUID `gorm:"type:uuid;column:competition_id"` + UserID uuid.UUID `gorm:"type:uuid;column:user_id"` +} \ No newline at end of file diff --git a/backend/src/models/users.go b/backend/src/models/users.go new file mode 100644 index 0000000..f5120f3 --- /dev/null +++ b/backend/src/models/users.go @@ -0,0 +1,29 @@ +package models + +import ( + "errors" + + "github.com/google/uuid" + "github.com/joaooliveira247/backend-test/src/utils" +) + +type Users struct { + BaseModel + Name string `json:"name" binding:"required,gt=1,lt=256" gorm:"type:varchar(255);not null;column:name"` + Email string `json:"email" binding:"required,gt=4,lt=256" gorm:"type:varchar(255);unique;not null;column:email"` + Phone string `json:"phone" binding:"required,gt=7,lt=16" gorm:"type:varchar(16);not null;column:phone"` + AffiliateCode string `json:"-" gorm:"type:varchar(8);unique;not null;column:affiliate_code"` +} + +func (user *Users) Validate() error { + if !utils.EmailValidator(user.Email) { + return errors.New("invalid email format") + } else if !utils.IsPhoneNumber(user.Phone) { + return errors.New("invalid phone format") + } + return nil +} + +func (user *Users) IsEmpty() bool { + return user.ID == uuid.Nil +} diff --git a/backend/src/repositories/competitions.go b/backend/src/repositories/competitions.go new file mode 100644 index 0000000..476ca8d --- /dev/null +++ b/backend/src/repositories/competitions.go @@ -0,0 +1,91 @@ +package repositories + +import ( + "github.com/google/uuid" + "github.com/joaooliveira247/backend-test/src/models" + "gorm.io/gorm" +) + +type CompetitionsRepository struct { + db *gorm.DB +} + +func NewCompetiotionsRepository(db *gorm.DB) CompetitionsRepository { + return CompetitionsRepository{db} +} + +func (repository *CompetitionsRepository) Create( + competiotion *models.Competitions, +) (uuid.UUID, error) { + result := repository.db.Create(&competiotion) + + if err := result.Error; err != nil { + return uuid.UUID{}, err + } + + return competiotion.ID, nil +} + +func (repository *CompetitionsRepository) GetActiveCompetition() (models.Competitions, error) { + var competition models.Competitions + + if err := repository.db.First(&competition, "status = true").Error; err != nil { + return models.Competitions{}, err + } + + return competition, nil +} + +func (repository *CompetitionsRepository) GetCompetitionByID(competitionID uuid.UUID) (models.Competitions, error) { + var competition models.Competitions + + if err := repository.db.First(&competition, "id = ?", competitionID).Error; err != nil { + return models.Competitions{}, err + } + + return competition, nil +} + +func (repository *CompetitionsRepository) CloseCompetition( + competitionID uuid.UUID, +) error { + var competition models.Competitions + + if err := repository.db.First(&competition, "id = ?", competitionID).Error; err != nil { + return err + } + + competition.Status = false + + if err := repository.db.Save(&competition).Error; err != nil { + return err + } + + return nil +} + +func (repository *CompetitionsRepository) GetCompetitionReport( + competitionID uuid.UUID, +) ([]models.CompetitionReport, error) { + rawQuery := ` +SELECT + u.name, + u.email, + u.phone, + COUNT(*) AS points +FROM points p +INNER JOIN users u ON p.user_id = u.id +WHERE p.competition_id = ? +GROUP BY u.name, u.email, u.phone +ORDER BY points DESC +LIMIT 10; + ` + + var reports []models.CompetitionReport + + if err := repository.db.Raw(rawQuery, competitionID).Scan(&reports).Error; err != nil { + return nil, err + } + + return reports, nil +} diff --git a/backend/src/repositories/points.go b/backend/src/repositories/points.go new file mode 100644 index 0000000..1269b2e --- /dev/null +++ b/backend/src/repositories/points.go @@ -0,0 +1,22 @@ +package repositories + +import ( + "github.com/joaooliveira247/backend-test/src/models" + "gorm.io/gorm" +) + +type PointsRepository struct { + db *gorm.DB +} + +func NewPointsRepository(db *gorm.DB) PointsRepository { + return PointsRepository{db} +} + +func (repository *PointsRepository) AddPoint(point *models.Points) error { + if err := repository.db.Create(point).Error; err != nil { + return err + } + + return nil +} diff --git a/backend/src/repositories/users.go b/backend/src/repositories/users.go new file mode 100644 index 0000000..abece43 --- /dev/null +++ b/backend/src/repositories/users.go @@ -0,0 +1,36 @@ +package repositories + +import ( + "github.com/joaooliveira247/backend-test/src/models" + "gorm.io/gorm" +) + +type UsersRepository struct { + db *gorm.DB +} + +func NewUsersRepository(db *gorm.DB) UsersRepository { + return UsersRepository{db} +} + +func (repository *UsersRepository) Create(user *models.Users) (string, error) { + result := repository.db.Create(&user) + + if err := result.Error; err != nil { + return "", err + } + + return user.AffiliateCode, nil +} + +func (repository *UsersRepository) GetUserByAffiliateCode( + code string, +) (models.Users, error) { + var user models.Users + + if err := repository.db.First(&user, "affiliate_code = ?", code).Error; err != nil { + return models.Users{}, err + } + + return user, nil +} diff --git a/backend/src/routes/competitions.go b/backend/src/routes/competitions.go new file mode 100644 index 0000000..5166430 --- /dev/null +++ b/backend/src/routes/competitions.go @@ -0,0 +1,36 @@ +package routes + +import ( + "log" + + "github.com/gin-gonic/gin" + "github.com/joaooliveira247/backend-test/src/controllers" + "github.com/joaooliveira247/backend-test/src/db" + "github.com/joaooliveira247/backend-test/src/repositories" +) + +func CompetitionsRoute(eng *gin.Engine) { + gormDB, err := db.GetDBConnection() + + if err != nil { + log.Fatalf("Users Route: %v", err) + } + + usersReposiotry := repositories.NewUsersRepository(gormDB) + compsRepository := repositories.NewCompetiotionsRepository(gormDB) + pointsRepository := repositories.NewPointsRepository(gormDB) + + controller := controllers.NewCompetitionsController( + usersReposiotry, + compsRepository, + pointsRepository, + ) + + competitionsGroup := eng.Group("/competitions") + { + competitionsGroup.POST("/", controller.Create) + competitionsGroup.GET("/", controller.GetCompetition) + competitionsGroup.PUT("/", controller.CloseCompetition) + competitionsGroup.GET("/reports/", controller.GetCompetitionReport) + } +} \ No newline at end of file diff --git a/backend/src/routes/routes.go b/backend/src/routes/routes.go new file mode 100644 index 0000000..9049193 --- /dev/null +++ b/backend/src/routes/routes.go @@ -0,0 +1,8 @@ +package routes + +import "github.com/gin-gonic/gin" + +func RegistryRoutes(eng *gin.Engine) { + UsersRoute(eng) + CompetitionsRoute(eng) +} diff --git a/backend/src/routes/users.go b/backend/src/routes/users.go new file mode 100644 index 0000000..0c11df8 --- /dev/null +++ b/backend/src/routes/users.go @@ -0,0 +1,33 @@ +package routes + +import ( + "log" + + "github.com/gin-gonic/gin" + "github.com/joaooliveira247/backend-test/src/controllers" + "github.com/joaooliveira247/backend-test/src/db" + "github.com/joaooliveira247/backend-test/src/repositories" +) + +func UsersRoute(eng *gin.Engine) { + gormDB, err := db.GetDBConnection() + + if err != nil { + log.Fatalf("Users Route: %v", err) + } + + usersReposiotry := repositories.NewUsersRepository(gormDB) + compsRepository := repositories.NewCompetiotionsRepository(gormDB) + pointsRepository := repositories.NewPointsRepository(gormDB) + + controller := controllers.NewUsersController( + usersReposiotry, + compsRepository, + pointsRepository, + ) + + usersGroup := eng.Group("/users") + { + usersGroup.POST("/", controller.CreateUser) + } +} diff --git a/backend/src/services/email.go b/backend/src/services/email.go new file mode 100644 index 0000000..db38fa9 --- /dev/null +++ b/backend/src/services/email.go @@ -0,0 +1,42 @@ +package services + +import ( + "fmt" + "log" + + "github.com/joaooliveira247/backend-test/src/config" + "gopkg.in/gomail.v2" +) + +func SendEmail(to, subject, message string) { + smtpHost := "smtp.gmail.com" + smtpPort := 587 + + msg := gomail.NewMessage() + + headers := map[string]string{ + "From": config.SERVICE_EMAIL, + "To": to, + "Subject": subject, + } + + for header, value := range headers { + msg.SetHeader(header, value) + } + + msg.SetBody( + "text/html", + fmt.Sprintf("

Carbon Offset Competition

%s

", message), + ) + + server := gomail.NewDialer( + smtpHost, + smtpPort, + config.SERVICE_EMAIL, + config.PASSWORD_SERVICE_EMAIL, + ) + + if err := server.DialAndSend(msg); err != nil { + log.Fatalf("Send Email: %v", err) + } +} diff --git a/backend/src/utils/utils.go b/backend/src/utils/utils.go new file mode 100644 index 0000000..e938522 --- /dev/null +++ b/backend/src/utils/utils.go @@ -0,0 +1,21 @@ +package utils + +import ( + "crypto/rand" + "math/big" +) + +const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" + +func GenerateAffiliateCode() (string, error) { + code := make([]byte, 8) + for i := range code { + n, err := rand.Int(rand.Reader, big.NewInt(int64(len(charset)))) + if err != nil { + return "", err + } + code[i] = charset[n.Int64()] + } + + return string(code), nil +} diff --git a/backend/src/utils/validators.go b/backend/src/utils/validators.go new file mode 100644 index 0000000..d90cb5d --- /dev/null +++ b/backend/src/utils/validators.go @@ -0,0 +1,28 @@ +package utils + +import ( + "net" + "regexp" + "strings" +) + +func EmailValidator(email string) bool { + regex := `^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$` + re := regexp.MustCompile(regex) + if !re.MatchString(email) { + return false + } + + parts := strings.Split(email, "@") + if len(parts) != 2 { + return false + } + mxRecords, err := net.LookupMX(parts[1]) + return err == nil && len(mxRecords) > 0 +} + +func IsPhoneNumber(phone string) bool { + re := regexp.MustCompile(`^\+55\d{11}$`) + + return re.MatchString(phone) +} diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..c010ff6 --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,32 @@ +version: "3.1" + +services: + carbon_offset_api: + build: + context: ./backend + dockerfile: Dockerfile + environment: + - DATABASE_URL= host=carbon_offset_db user=user password=passwd dbname=carbon_offset port=5432 sslmode=disable + - SERVICE_EMAIL=${SERVICE_EMAIL} + - PASSWORD_SERVICE_EMAIL=${PASSWORD_SERVICE_EMAIL} + networks: + - carbon_offset_network + ports: + - 8000:8000 + volumes: + - ./backend:/app + + carbon_offset_db: + image: postgres:alpine3.20 + restart: "no" + environment: + POSTGRES_USER: user + POSTGRES_PASSWORD: passwd + POSTGRES_DB: carbon_offset + networks: + - carbon_offset_network + ports: + - 5432:5432 + +networks: + carbon_offset_network: \ No newline at end of file