Skip to content

Commit

Permalink
implemented mailer
Browse files Browse the repository at this point in the history
  • Loading branch information
vladyslavpavlenko committed May 18, 2024
1 parent 3f491a2 commit c4dd976
Show file tree
Hide file tree
Showing 17 changed files with 315 additions and 53 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,4 @@ go.work
.idea
.DS_Store
.env
/db-data/
/app/
5 changes: 4 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ FROM alpine:latest

RUN mkdir /app

COPY apiApp /app
COPY apiApp /app/apiApp
COPY .env /app/.env

WORKDIR /app

CMD ["/app/apiApp"]
28 changes: 28 additions & 0 deletions Makefile.windows
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
SHELL=cmd.exe
API_APP_BINARY=apiApp

## up: starts all containers in the background without forcing build
up:
@echo Starting Docker images...
docker-compose up -d
@echo Docker images started!

## up_build: stops docker-compose (if running), builds all projects and starts docker compose
up_build: build_app
@echo Stopping docker images (if running...)
docker-compose down
@echo Building (when required) and starting docker images...
docker-compose up --build -d
@echo Docker images built and started!

## down: stop docker compose
down:
@echo Stopping docker compose...
docker-compose down
@echo Done!

## build_app: builds the app binary as a linux executable
build_broker:
@echo Building app binary...
chdir . && set GOOS=linux&& set GOARCH=amd64&& set CGO_ENABLED=0 && go build -o ${API_APP_BINARY} ./cmd/api
@echo Done!
73 changes: 72 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,72 @@
# genesis-api-project
# Genesis API Project

This service implements the following API:

## API Endpoints

### `GET` /rate

This endpoint returns the current `USD to UAH` exchange rate using the Coinbase API.

#### Parameters

``No parameters``

#### Response Codes

```
200: Returns the actual USD to UAH exchange rate.
400: Invalid status value.
```

---

### `POST` /subscribe

This endpoint adds an email address to the database and automatically subscribes it to the USD to UAH exchange rate newsletter.

_The code includes the ability to subscribe to other rates for future development, but this functionality is not currently used to fulfill the requirements._

#### Parameters

``email`` **string** (formData): The email address to be added to the database and the mailing list.

#### Response Codes

```
200: The email address is added to the database and subscribed to the mailing list.
409: The email address already exists.
```

_Not mentioned in the task, but arose during the development process:_
```
400: The provided data (such as email address) is invalid.
500: Internal error status.
```

---

### `POST` /sendEmails

This endpoint sends the current `USD to UAH` exchange rate to subscribed email addresses using goroutines.

#### Parameters

``No parameters``

#### Response Codes

```
200: Emails were sent.
```

## Usage:

- Using Makefile
```
git clone https://github.com/vladyslavpavlenko/genesis-api-project.git
cd genesis-api-project
make up_build
```

Now you can reach an API using [`http://localhost:8080/`](http://localhost:8081/).
Binary file modified apiApp
Binary file not shown.
4 changes: 3 additions & 1 deletion cmd/api/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package main
import (
"fmt"
"github.com/vladyslavpavlenko/genesis-api-project/internal/config"
"github.com/vladyslavpavlenko/genesis-api-project/internal/mailer"
"log"
"net/http"
)
Expand All @@ -17,8 +18,9 @@ func main() {
log.Fatal()
}

log.Printf("Running on port %d", webPort)
mailer.ScheduleEmails(app.EmailConfig, app.DB)

log.Printf("Running on port %d", webPort)
srv := &http.Server{
Addr: fmt.Sprintf(":%d", webPort),
Handler: routes(&app),
Expand Down
17 changes: 16 additions & 1 deletion cmd/api/setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package main
import (
"errors"
"fmt"
"github.com/joho/godotenv"
"github.com/vladyslavpavlenko/genesis-api-project/internal/config"
"github.com/vladyslavpavlenko/genesis-api-project/internal/handlers"
"github.com/vladyslavpavlenko/genesis-api-project/internal/models"
Expand All @@ -16,7 +17,11 @@ import (
var counts int64

func setup(app *config.AppConfig) error {
// Connect to the database and run migrations
err := godotenv.Load()
if err != nil {
log.Fatalf("Error loading .env file: %v", err)
}

db, err := connectToDB()
if err != nil {
log.Fatal(err)
Expand All @@ -29,6 +34,16 @@ func setup(app *config.AppConfig) error {

app.DB = db
app.Models = models.New(db)

app.EmailConfig = config.EmailConfig{
Email: os.Getenv("GMAIL_EMAIL"),
Password: os.Getenv("GMAIL_PASSWORD"),
}

if app.EmailConfig.Email == "" || app.EmailConfig.Password == "" {
log.Fatal("Missing email configuration in environment variables")
}

repo := handlers.NewRepo(app)
handlers.NewHandlers(repo)

Expand Down
9 changes: 8 additions & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ services:
POSTGRES_PASSWORD: password
POSTGRES_DB: emails
volumes:
- ./db-data/postgres/:/var/lib/postgresql/data/
- ./app/db-data/postgres/:/var/lib/postgresql/data/
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro

api-app:
build:
Expand All @@ -21,8 +23,13 @@ services:
ports:
- "8080:8080"
restart: always
env_file:
- .env
deploy:
mode: replicated
replicas: 1
volumes:
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
environment:
DSN: "host=postgres port=5432 user=postgres password=password dbname=emails sslmode=disable timezone=UTC connect_timeout=5"
5 changes: 5 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,13 @@ require (
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/joho/godotenv v1.5.1 // indirect
github.com/robfig/cron/v3 v3.0.1 // indirect
github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208 // indirect
github.com/xhit/go-simple-mail/v2 v2.16.0 // indirect
golang.org/x/crypto v0.14.0 // indirect
golang.org/x/text v0.13.0 // indirect
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df // indirect
gorm.io/driver/postgres v1.5.7 // indirect
gorm.io/gorm v1.25.10 // indirect
)
10 changes: 10 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,24 @@ github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
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/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208 h1:PM5hJF7HVfNWmCjMdEfbuOBNXSVF2cMFGgQTPdKCbwM=
github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208/go.mod h1:BzWtXXrXzZUvMacR0oF/fbDDgUPO8L36tDMmRAf14ns=
github.com/xhit/go-simple-mail/v2 v2.16.0 h1:ouGy/Ww4kuaqu2E2UrDw7SvLaziWTB60ICLkIkNVccA=
github.com/xhit/go-simple-mail/v2 v2.16.0/go.mod h1:b7P5ygho6SYE+VIqpxA6QkYfv4teeyG4MKqB3utRu98=
golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
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/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=
gorm.io/driver/postgres v1.5.7 h1:8ptbNJTDbEmhdr62uReG5BGkdQyeasu/FZHxI0IMGnM=
gorm.io/driver/postgres v1.5.7/go.mod h1:3e019WlBaYI5o5LIdNV+LyxCMNtLOQETBXL2h4chKpA=
Expand Down
11 changes: 9 additions & 2 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@ import (

// AppConfig holds the application config.
type AppConfig struct {
DB *gorm.DB
Models models.Models
DB *gorm.DB
Models models.Models
EmailConfig EmailConfig
}

// EmailConfig holds the email configuration.
type EmailConfig struct {
Email string
Password string
}
10 changes: 6 additions & 4 deletions internal/handlers/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import (
"encoding/json"
"errors"
"fmt"
"github.com/vladyslavpavlenko/genesis-api-project/internal/mailer"
"github.com/vladyslavpavlenko/genesis-api-project/internal/models"
"github.com/vladyslavpavlenko/genesis-api-project/internal/rate"
"net/http"
"strings"
"time"
Expand All @@ -31,16 +33,16 @@ type subscriptionBody struct {

// GetRate gets the current USD to UAH exchange rate.
func (m *Repository) GetRate(w http.ResponseWriter, r *http.Request) {
rate, err := getRate("USD", "UAH")
price, err := rate.GetRate("USD", "UAH")
if err != nil {
_ = m.errorJSON(w, errors.New("error calling Coinbase API"), http.StatusServiceUnavailable)
_ = m.errorJSON(w, errors.New("error calling Coinbase API"), http.StatusBadRequest) // http.StatusServiceUnavailable
return
}

rateResp := rateResponse{
BaseCurrencyCode: "USD",
TargetCurrencyCode: "UAH",
Price: rate,
Price: price,
}

// Send response
Expand Down Expand Up @@ -140,5 +142,5 @@ func (m *Repository) Subscribe(w http.ResponseWriter, r *http.Request) {

// SendEmails handles sending emails to all the subscribed emails.
func (m *Repository) SendEmails(w http.ResponseWriter, r *http.Request) {

mailer.SendEmails(m.App.EmailConfig, m.App.DB)
}
33 changes: 0 additions & 33 deletions internal/handlers/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package handlers
import (
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/mail"
Expand Down Expand Up @@ -70,35 +69,3 @@ func validateEmail(email string) bool {
_, err := mail.ParseAddress(email)
return err == nil
}

// CoinbaseResponse is the Coinbase API response structure.
type CoinbaseResponse struct {
Data struct {
Amount string `json:"amount"`
Base string `json:"base"`
Currency string `json:"currency"`
} `json:"data"`
}

// getRate returns the exchange rate between the base currency and the target currency using Coinbase API.
func getRate(baseCurrencyCode string, targetCurrencyCode string) (string, error) {
url := fmt.Sprintf("https://api.coinbase.com/v2/prices/%s-%s/buy", baseCurrencyCode, targetCurrencyCode)
resp, err := http.Get(url)
if err != nil {
return "", fmt.Errorf("error making request: %v", err)
}
defer resp.Body.Close()

body, err := io.ReadAll(resp.Body)
if err != nil {
return "", fmt.Errorf("error reading the response body: %v", err)
}

var response CoinbaseResponse
err = json.Unmarshal(body, &response)
if err != nil {
return "", fmt.Errorf("error unmarshaling the response: %v", err)
}

return response.Data.Amount, nil
}
Loading

0 comments on commit c4dd976

Please sign in to comment.