From 6ab8545c8483aa2f1bf5fcf4e64c0f4a5df6bf61 Mon Sep 17 00:00:00 2001 From: Utku Ufuk Date: Wed, 27 Apr 2022 01:40:21 +0300 Subject: [PATCH] Add HTTP authentication (#17) * improve config & add authentication * update .env.example --- .env.example | 7 +- cmd/habit-service/main.go | 24 +++++-- cmd/progress-report/main.go | 17 ++++- cmd/score-update/main.go | 5 +- internal/config/config.go | 96 +++++++++++++++----------- internal/habit/habit.go | 6 +- internal/habit/{auth.go => service.go} | 10 +-- internal/service/entrello_fetch.go | 7 +- internal/service/progress_report.go | 15 ++-- 9 files changed, 118 insertions(+), 69 deletions(-) rename internal/habit/{auth.go => service.go} (75%) diff --git a/.env.example b/.env.example index 9d81757..8820309 100644 --- a/.env.example +++ b/.env.example @@ -1,12 +1,13 @@ -APP_ENV=production +SECRET=xxxxxxxxxxxxxxxxxxxxxxxxx HTTP_PORT=XXXX -SPREADSHEET_ID=xxxxxxxxxxxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxxxx -TIMEZONE_LOCATION=Europe/Istanbul TELEGRAM_TOKEN=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx TELEGRAM_CHAT_ID=xxxxxxxxxx +TIMEZONE_LOCATION=Europe/Istanbul + GSHEETS_CLIENT_ID=xxxxxxxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.apps.googleusercontent.com GSHEETS_CLIENT_SECRET=xxxxxxxx-xxxxxxxxxxxxxxx GSHEETS_ACCESS_TOKEN=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx GSHEETS_REFRESH_TOKEN=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +SPREADSHEET_ID=xxxxxxxxxxxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxxxx diff --git a/cmd/habit-service/main.go b/cmd/habit-service/main.go index cdd5538..01cf44b 100644 --- a/cmd/habit-service/main.go +++ b/cmd/habit-service/main.go @@ -17,24 +17,40 @@ import ( ) var ( + cfg config.ServerConfig client habit.Client ) -func main() { +func init() { var err error - client, err = habit.GetClient(context.Background()) + cfg, err = config.ParseServerConfig() + if err != nil { + logger.Error("Failed to parse server config: %v", err) + os.Exit(1) + } + + client, err = habit.GetClient(context.Background(), cfg.GoogleSheets) if err != nil { logger.Error("Could not create gsheets client for Habit Service: %v", err) os.Exit(1) } +} +func main() { http.HandleFunc("/entrello", handleEntrelloRequest) - http.ListenAndServe(fmt.Sprintf(":%d", config.HttpPort), nil) + http.ListenAndServe(fmt.Sprintf(":%d", cfg.Port), nil) } func handleEntrelloRequest(w http.ResponseWriter, req *http.Request) { + if cfg.Secret != "" && req.Header.Get("X-Api-Key") != cfg.Secret { + w.WriteHeader(http.StatusUnauthorized) + return + } + if req.Method == http.MethodGet { - action := service.FetchHabitsAsTrelloCardsAction{} + action := service.FetchHabitsAsTrelloCardsAction{ + TimezoneLocation: cfg.TimezoneLocation, + } cards, err := action.Run(req.Context(), client) if err != nil { w.WriteHeader(http.StatusInternalServerError) diff --git a/cmd/progress-report/main.go b/cmd/progress-report/main.go index adf063e..4c3048a 100644 --- a/cmd/progress-report/main.go +++ b/cmd/progress-report/main.go @@ -4,20 +4,33 @@ import ( "context" "os" + "github.com/utkuufuk/habit-service/internal/config" "github.com/utkuufuk/habit-service/internal/habit" "github.com/utkuufuk/habit-service/internal/logger" "github.com/utkuufuk/habit-service/internal/service" ) func main() { + cfg, err := config.ParseProgressReportConfig() + if err != nil { + logger.Error("Failed to parse server config: %v", err) + os.Exit(1) + } + ctx := context.Background() - client, err := habit.GetClient(ctx) + client, err := habit.GetClient(ctx, cfg.GoogleSheets) if err != nil { logger.Error("Could not create gsheets client for Habit Service: %v", err) os.Exit(1) } - _, err = service.ReportProgressAction{}.Run(ctx, client) + action := service.ReportProgressAction{ + TelegramChatId: cfg.TelegramChatId, + TelegramToken: cfg.TelegramToken, + TimezoneLocation: cfg.TimezoneLocation, + } + + _, err = action.Run(ctx, client) if err != nil { logger.Error("Could not run Glados command: %v", err) os.Exit(1) diff --git a/cmd/score-update/main.go b/cmd/score-update/main.go index 36eb7fe..9a04040 100644 --- a/cmd/score-update/main.go +++ b/cmd/score-update/main.go @@ -11,13 +11,14 @@ import ( ) func main() { - client, err := habit.GetClient(context.Background()) + loc, cfg := config.ParseCommonConfig() + client, err := habit.GetClient(context.Background(), cfg) if err != nil { logger.Error("Could not create gsheets client for Habit Service: %v", err) os.Exit(1) } - now := time.Now().In(config.TimezoneLocation) + now := time.Now().In(loc) habits, err := client.FetchHabits(now) if err != nil { logger.Error("could not fetch habits: %v", err) diff --git a/internal/config/config.go b/internal/config/config.go index a27f256..4eaaef3 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -10,64 +10,78 @@ import ( "github.com/utkuufuk/habit-service/internal/logger" ) -var ( - AppEnv string - HttpPort int - TimezoneLocation *time.Location - SpreadsheetId string +type GoogleSheetsConfig struct { GoogleClientId string GoogleClientSecret string GoogleAccessToken string GoogleRefreshToken string - TelegramChatId int64 - TelegramToken string -) - -func init() { - godotenv.Load() + SpreadsheetId string +} - AppEnv = os.Getenv("APP_ENV") - TelegramToken = os.Getenv("TELEGRAM_TOKEN") - SpreadsheetId = os.Getenv("SPREADSHEET_ID") - GoogleClientId = os.Getenv("GSHEETS_CLIENT_ID") - GoogleClientSecret = os.Getenv("GSHEETS_CLIENT_SECRET") - GoogleAccessToken = os.Getenv("GSHEETS_ACCESS_TOKEN") - GoogleRefreshToken = os.Getenv("GSHEETS_REFRESH_TOKEN") +type ServerConfig struct { + TimezoneLocation *time.Location + GoogleSheets GoogleSheetsConfig + Port int + Secret string +} - httpPort := os.Getenv("PORT") - if httpPort == "" { - httpPort = os.Getenv("HTTP_PORT") - } +type ProgressReportConfig struct { + TimezoneLocation *time.Location + GoogleSheets GoogleSheetsConfig + TelegramChatId int64 + TelegramToken string +} - port, err := strconv.Atoi(httpPort) - if err != nil { - logger.Error("PORT or HTTP_PORT not set") +func ParseCommonConfig() (loc *time.Location, cfg GoogleSheetsConfig) { + godotenv.Load() - if AppEnv == "production" { - os.Exit(1) - } - } + cfg.SpreadsheetId = os.Getenv("SPREADSHEET_ID") + cfg.GoogleClientId = os.Getenv("GSHEETS_CLIENT_ID") + cfg.GoogleClientSecret = os.Getenv("GSHEETS_CLIENT_SECRET") + cfg.GoogleAccessToken = os.Getenv("GSHEETS_ACCESS_TOKEN") + cfg.GoogleRefreshToken = os.Getenv("GSHEETS_REFRESH_TOKEN") - location, err := time.LoadLocation(os.Getenv("TIMEZONE_LOCATION")) + loc, err := time.LoadLocation(os.Getenv("TIMEZONE_LOCATION")) if err != nil { - fmt.Printf( + logger.Warn( "Invalid timezone location: '%s', falling back to UTC: %v\n", - location, + os.Getenv("TIMEZONE_LOCATION"), err, ) - location, _ = time.LoadLocation("UTC") + loc, _ = time.LoadLocation("UTC") } - chatId, err := strconv.ParseInt(os.Getenv("TELEGRAM_CHAT_ID"), 10, 64) + return loc, cfg +} + +func ParseServerConfig() (cfg ServerConfig, err error) { + loc, common := ParseCommonConfig() + + port, err := strconv.Atoi(os.Getenv("PORT")) if err != nil { - logger.Error("Invalid Telegram Chat ID") + return cfg, fmt.Errorf("PORT not set") + } - if AppEnv == "production" { - os.Exit(1) - } + return ServerConfig{ + loc, + common, + port, + os.Getenv("SECRET"), + }, nil +} + +func ParseProgressReportConfig() (cfg ProgressReportConfig, err error) { + loc, common := ParseCommonConfig() + + chatId, err := strconv.ParseInt(os.Getenv("TELEGRAM_CHAT_ID"), 10, 64) + if err != nil { + return cfg, fmt.Errorf("Invalid Telegram Chat ID") } - HttpPort = port - TimezoneLocation = location - TelegramChatId = chatId + return ProgressReportConfig{ + loc, + common, + chatId, + os.Getenv("TELEGRAM_TOKEN"), + }, nil } diff --git a/internal/habit/habit.go b/internal/habit/habit.go index ce37eb1..b1bc22a 100644 --- a/internal/habit/habit.go +++ b/internal/habit/habit.go @@ -38,12 +38,12 @@ type cell struct { row int } -func GetClient(ctx context.Context) (client Client, err error) { - service, err := initializeService(ctx) +func GetClient(ctx context.Context, cfg config.GoogleSheetsConfig) (client Client, err error) { + service, err := initService(ctx, cfg) if err != nil { return client, fmt.Errorf("could not initialize gsheets service: %w", err) } - return Client{config.SpreadsheetId, service.Spreadsheets.Values}, nil + return Client{cfg.SpreadsheetId, service.Spreadsheets.Values}, nil } // FetchHabits retrieves the state of today's habits from the spreadsheet diff --git a/internal/habit/auth.go b/internal/habit/service.go similarity index 75% rename from internal/habit/auth.go rename to internal/habit/service.go index cc01325..87ab95f 100644 --- a/internal/habit/auth.go +++ b/internal/habit/service.go @@ -17,10 +17,10 @@ const ( tokenUrl = "https://oauth2.googleapis.com/token" ) -func initializeService(ctx context.Context) (service *sheets.Service, err error) { +func initService(ctx context.Context, cfg config.GoogleSheetsConfig) (service *sheets.Service, err error) { auth := &oauth2.Config{ - ClientID: config.GoogleClientId, - ClientSecret: config.GoogleClientSecret, + ClientID: cfg.GoogleClientId, + ClientSecret: cfg.GoogleClientSecret, Endpoint: oauth2.Endpoint{ AuthURL: authUrl, TokenURL: tokenUrl, @@ -30,9 +30,9 @@ func initializeService(ctx context.Context) (service *sheets.Service, err error) } token := &oauth2.Token{ - AccessToken: config.GoogleAccessToken, + AccessToken: cfg.GoogleAccessToken, TokenType: "Bearer", - RefreshToken: config.GoogleRefreshToken, + RefreshToken: cfg.GoogleRefreshToken, Expiry: time.Now(), } if err != nil { diff --git a/internal/service/entrello_fetch.go b/internal/service/entrello_fetch.go index 729c5f1..e9c8a31 100644 --- a/internal/service/entrello_fetch.go +++ b/internal/service/entrello_fetch.go @@ -6,15 +6,16 @@ import ( "time" "github.com/utkuufuk/entrello/pkg/trello" - "github.com/utkuufuk/habit-service/internal/config" "github.com/utkuufuk/habit-service/internal/entrello" "github.com/utkuufuk/habit-service/internal/habit" ) -type FetchHabitsAsTrelloCardsAction struct{} +type FetchHabitsAsTrelloCardsAction struct { + TimezoneLocation *time.Location +} func (a FetchHabitsAsTrelloCardsAction) Run(ctx context.Context, client habit.Client) ([]trello.Card, error) { - now := time.Now().In(config.TimezoneLocation) + now := time.Now().In(a.TimezoneLocation) habits, err := client.FetchHabits(now) if err != nil { diff --git a/internal/service/progress_report.go b/internal/service/progress_report.go index 9320537..36ae334 100644 --- a/internal/service/progress_report.go +++ b/internal/service/progress_report.go @@ -9,22 +9,25 @@ import ( "time" tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api" - "github.com/utkuufuk/habit-service/internal/config" "github.com/utkuufuk/habit-service/internal/habit" "github.com/utkuufuk/habit-service/internal/tableimage" ) -type ReportProgressAction struct{} +type ReportProgressAction struct { + TelegramChatId int64 + TelegramToken string + TimezoneLocation *time.Location +} func (a ReportProgressAction) Run(ctx context.Context, client habit.Client) (string, error) { - now := time.Now().In(config.TimezoneLocation) + now := time.Now().In(a.TimezoneLocation) currentHabits, err := client.FetchHabits(now) if err != nil { return "", fmt.Errorf("could not fetch this month's habits: %w\n", err) } year, month, _ := now.Date() - lastMonth := time.Date(year, month, 1, 0, 0, 0, 0, config.TimezoneLocation).Add(-time.Nanosecond) + lastMonth := time.Date(year, month, 1, 0, 0, 0, 0, a.TimezoneLocation).Add(-time.Nanosecond) previousHabits, err := client.FetchHabits(lastMonth) if err != nil { return "", fmt.Errorf("could not fetch habits from last month: %w\n", err) @@ -46,7 +49,7 @@ func (a ReportProgressAction) Run(ctx context.Context, client habit.Client) (str } func (a ReportProgressAction) sendProgressReport(path string) error { - bot, err := tgbotapi.NewBotAPI(config.TelegramToken) + bot, err := tgbotapi.NewBotAPI(a.TelegramToken) if err != nil { return fmt.Errorf("could not initialize Telegram bot client: %w", err) } @@ -56,7 +59,7 @@ func (a ReportProgressAction) sendProgressReport(path string) error { return fmt.Errorf("could not read progress report image '%s': %w", path, err) } - _, err = bot.Send(tgbotapi.NewPhotoUpload(config.TelegramChatId, tgbotapi.FileBytes{ + _, err = bot.Send(tgbotapi.NewPhotoUpload(a.TelegramChatId, tgbotapi.FileBytes{ Name: "picture", Bytes: photoBytes, }))