commit 210c4159e7c96bd7e3c3d16ab8bb80fcc33ae6e2 Author: Pavel Date: Fri Dec 29 14:30:20 2023 +0300 init base struct diff --git a/.env b/.env new file mode 100644 index 0000000..2e417c9 --- /dev/null +++ b/.env @@ -0,0 +1 @@ +APP_NAME=codeword diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2b70277 --- /dev/null +++ b/.gitignore @@ -0,0 +1,161 @@ +# Created by https://www.toptal.com/developers/gitignore/api/visualstudiocode,goland,go +# Edit at https://www.toptal.com/developers/gitignore?templates=visualstudiocode,goland,go + +### Go ### +# 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 + +### GoLand ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf +.idea + +# AWS User-specific +.idea/**/aws.xml + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# SonarLint plugin +.idea/sonarlint/ + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### GoLand Patch ### +# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 + +# *.iml +# modules.xml +# .idea/misc.xml +# *.ipr + +# Sonarlint plugin +# https://plugins.jetbrains.com/plugin/7973-sonarlint +.idea/**/sonarlint/ + +# SonarQube Plugin +# https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin +.idea/**/sonarIssues.xml + +# Markdown Navigator plugin +# https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced +.idea/**/markdown-navigator.xml +.idea/**/markdown-navigator-enh.xml +.idea/**/markdown-navigator/ + +# Cache file creation bug +# See https://youtrack.jetbrains.com/issue/JBR-2257 +.idea/$CACHE_FILE$ + +# CodeStream plugin +# https://plugins.jetbrains.com/plugin/12206-codestream +.idea/codestream.xml + +# Azure Toolkit for IntelliJ plugin +# https://plugins.jetbrains.com/plugin/8053-azure-toolkit-for-intellij +.idea/**/azureSettings.xml + +### VisualStudioCode ### +.vscode +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +!.vscode/*.code-snippets + +# Local History for Visual Studio Code +.history/ + +# Built Visual Studio Code Extensions +*.vsix + +### VisualStudioCode Patch ### +# Ignore all local history of files +.history +.ionide + +# End of https://www.toptal.com/developers/gitignore/api/visualstudiocode,goland,go diff --git a/blueprint.yaml b/blueprint.yaml new file mode 100644 index 0000000..c86b65b --- /dev/null +++ b/blueprint.yaml @@ -0,0 +1,14 @@ +ProjectName: codeword +Description: Service for exchanging codewords + +Template: + path: "./" + +Modules: + logger: + name: zap + env: + vars: + - name: APP_NAME + type: string + default: "{{.ProjectName}}" diff --git a/cmd/codeword/main.go b/cmd/codeword/main.go new file mode 100644 index 0000000..3d84e25 --- /dev/null +++ b/cmd/codeword/main.go @@ -0,0 +1,32 @@ +package main + +import ( + "codeword/internal/app" + "codeword/internal/initialize" + "context" + "fmt" + "go.uber.org/zap" + "os" + "os/signal" + "syscall" +) + +func main() { + logger, err := zap.NewProduction() + if err != nil { + fmt.Printf("Failed to initialize logger: %v\n", err) + os.Exit(1) + } + + config, err := initialize.LoadConfig() + if err != nil { + logger.Fatal("Failed to load config", zap.Error(err)) + } + + ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) + defer stop() + + if err = app.Run(ctx, *config, logger); err != nil { + logger.Fatal("App exited with error", zap.Error(err)) + } +} diff --git a/deployment/local/docker-compose.yaml b/deployment/local/docker-compose.yaml new file mode 100644 index 0000000..e69de29 diff --git a/deployment/staging/docker-compose.yaml b/deployment/staging/docker-compose.yaml new file mode 100644 index 0000000..e69de29 diff --git a/deployment/test/docker-compose.yaml b/deployment/test/docker-compose.yaml new file mode 100644 index 0000000..e69de29 diff --git a/docs/openapi.yaml b/docs/openapi.yaml new file mode 100644 index 0000000..e69de29 diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..71dd7ac --- /dev/null +++ b/go.mod @@ -0,0 +1,21 @@ +module codeword + +go 1.21 + +require ( + github.com/andybalholm/brotli v1.0.5 // indirect + github.com/caarlos0/env/v8 v8.0.0 // indirect + github.com/gofiber/fiber/v2 v2.51.0 // indirect + github.com/google/uuid v1.4.0 // indirect + github.com/klauspost/compress v1.16.7 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/rivo/uniseg v0.2.0 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasthttp v1.50.0 // indirect + github.com/valyala/tcplisten v1.0.0 // indirect + go.uber.org/multierr v1.10.0 // indirect + go.uber.org/zap v1.26.0 // indirect + golang.org/x/sys v0.14.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..98f976a --- /dev/null +++ b/go.sum @@ -0,0 +1,33 @@ +github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs= +github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= +github.com/caarlos0/env/v8 v8.0.0 h1:POhxHhSpuxrLMIdvTGARuZqR4Jjm8AYmoi/JKlcScs0= +github.com/caarlos0/env/v8 v8.0.0/go.mod h1:7K4wMY9bH0esiXSSHlfHLX5xKGQMnkH5Fk4TDSSSzfo= +github.com/gofiber/fiber/v2 v2.51.0 h1:JNACcZy5e2tGApWB2QrRpenTWn0fq0hkFm6k0C86gKQ= +github.com/gofiber/fiber/v2 v2.51.0/go.mod h1:xaQRZQJGqnKOQnbQw+ltvku3/h8QxvNi8o6JiJ7Ll0U= +github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= +github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I= +github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +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/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= +github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasthttp v1.50.0 h1:H7fweIlBm0rXLs2q0XbalvJ6r0CUPFWK3/bB4N13e9M= +github.com/valyala/fasthttp v1.50.0/go.mod h1:k2zXd82h/7UZc3VOdJ2WaUqt1uZ/XpXAfE9i+HBC3lA= +github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= +github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= +go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= +go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= +go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q= +golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= diff --git a/internal/adapters/client/mail.go b/internal/adapters/client/mail.go new file mode 100644 index 0000000..ecf543b --- /dev/null +++ b/internal/adapters/client/mail.go @@ -0,0 +1,13 @@ +package client + +import "fmt" + +type RecoveryEmailSender struct{} + +// SendRecoveryEmail отправляет email с подписью для восстановления доступа +func (r *RecoveryEmailSender) SendRecoveryEmail(email, signature string) error { + + fmt.Printf("Отправляем письмо для восстановления доступа на: %s с подписью: %s\n", email, signature) + + return nil +} diff --git a/internal/app/app.go b/internal/app/app.go new file mode 100644 index 0000000..ea53893 --- /dev/null +++ b/internal/app/app.go @@ -0,0 +1,52 @@ +package app + +import ( + "codeword/internal/adapters/client" + controller "codeword/internal/controller/recovery" + "codeword/internal/initialize" + "codeword/internal/repository" + httpserver "codeword/internal/server/http" + "codeword/internal/services" + "context" + "go.uber.org/zap" + "time" +) + +func Run(ctx context.Context, cfg initialize.Config, logger *zap.Logger) error { + logger.Info("Запуск приложения", zap.String("AppName", cfg.AppName)) + + userRepo := repository.NewUserRepository() + recoveryEmailSender := &client.RecoveryEmailSender{} + recoveryService := services.NewRecoveryService(logger, userRepo, recoveryEmailSender) + recoveryController := controller.NewRecoveryController(logger, recoveryService) + server := httpserver.NewServer(httpserver.ServerConfig{ + Logger: logger, + Repo: userRepo, + RecoveryController: recoveryController, + }) + + go func() { + if err := server.Start(":3000"); err != nil { + logger.Error("Ошибка запуска сервера", zap.Error(err)) + } + }() + + <-ctx.Done() + + if err := shutdownApp(server, logger); err != nil { + return err + } + logger.Info("Приложение остановлено") + return nil +} + +func shutdownApp(server *httpserver.Server, logger *zap.Logger) error { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + if err := server.Shutdown(ctx); err != nil { + logger.Error("Ошибка при остановке сервера Fiber", zap.Error(err)) + return err + } + return nil +} diff --git a/internal/controller/recovery/recovery_controller.go b/internal/controller/recovery/recovery_controller.go new file mode 100644 index 0000000..6a0420c --- /dev/null +++ b/internal/controller/recovery/recovery_controller.go @@ -0,0 +1,81 @@ +package controller + +import ( + "codeword/internal/services" + "fmt" + "github.com/gofiber/fiber/v2" + "go.uber.org/zap" + "time" +) + +type RecoveryController struct { + Logger *zap.Logger + Service *services.RecoveryService +} + +func NewRecoveryController(logger *zap.Logger, service *services.RecoveryService) *RecoveryController { + return &RecoveryController{ + Logger: logger, + Service: service, + } +} + +// HandleRecoveryRequest обрабатывает запрос на восстановление пароля +func (r *RecoveryController) HandleRecoveryRequest(c *fiber.Ctx) error { + email := c.FormValue("email") + + key, err := r.Service.GenerateKey() + if err != nil { + r.Logger.Error("Failed to generate key", zap.Error(err)) + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Internal Server Error"}) + } + fmt.Println(key) + + user, err := r.Service.FindUserByEmail(email) + if err != nil { + r.Logger.Error("Failed to find user by email", zap.Error(err)) + return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "User not found"}) + } + fmt.Println(user) + // сохраняем в бд + signature, err := r.Service.StoreRecoveryRecord("user") + if err != nil { + r.Logger.Error("Failed to store recovery record", zap.Error(err)) + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Internal Server Error"}) + } + // тут что-то на подобии канала или что-то подобное так как отправка выполнятеся в воркере, + //это пока временное решение для написания структуры кода и проверки отправки, далее перепишу + // под горутины + err = r.Service.SendRecoveryEmail(email, signature) + if err != nil { + r.Logger.Error("Failed to send recovery email", zap.Error(err)) + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Internal Server Error"}) + } + + return c.Status(fiber.StatusOK).JSON(fiber.Map{"message": "Recovery email sent successfully"}) +} + +// HandleRecoveryLink обрабатывает ссылку восстановления и обменивает ее на токены +func (r *RecoveryController) HandleRecoveryLink(c *fiber.Ctx) error { + signature := c.Params("sign") + // тут получается + record, err := r.Service.GetRecoveryRecord(signature) + if err != nil { + r.Logger.Error("Failed to get recovery record", zap.Error(err)) + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Internal Server Error"}) + } + + // проверка на более чем 15 минут + if time.Since(record.CreatedAt) > 15*time.Minute { + r.Logger.Error("Recovery link expired", zap.String("signature", signature)) + return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Recovery link expired"}) + } + + tokens, err := r.Service.ExchangeForTokens(record.UserID) + if err != nil { + r.Logger.Error("Failed to exchange recovery link for tokens", zap.Error(err)) + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Internal Server Error"}) + } + + return c.Status(fiber.StatusOK).JSON(tokens) +} diff --git a/internal/errors/errors.go b/internal/errors/errors.go new file mode 100644 index 0000000..04b3218 --- /dev/null +++ b/internal/errors/errors.go @@ -0,0 +1 @@ +package errors diff --git a/internal/initialize/initialize.go b/internal/initialize/initialize.go new file mode 100644 index 0000000..0809dc1 --- /dev/null +++ b/internal/initialize/initialize.go @@ -0,0 +1,17 @@ +package initialize + +import ( + "github.com/caarlos0/env/v8" +) + +type Config struct { + AppName string `env:"APP_NAME" envDefault:"codeword"` +} + +func LoadConfig() (*Config, error) { + var config Config + if err := env.Parse(&config); err != nil { + return nil, err + } + return &config, nil +} diff --git a/internal/models/user.go b/internal/models/user.go new file mode 100644 index 0000000..2efc1fa --- /dev/null +++ b/internal/models/user.go @@ -0,0 +1,17 @@ +package models + +import "time" + +type User struct { + // получаем данные из другого сервиса +} + +type RestoreRequest struct { + ID string // xid или ObjectID + CreatedAt time.Time + Sign string // подпись + Email string // email из запроса + UserID string // айдишник юзера, которого нашли по email + Sent bool + SentAt time.Time +} diff --git a/internal/repository/user_repository.go b/internal/repository/user_repository.go new file mode 100644 index 0000000..c45fd92 --- /dev/null +++ b/internal/repository/user_repository.go @@ -0,0 +1,36 @@ +package repository + +import ( + "codeword/internal/models" + "time" +) + +type UserRepository struct { + //todo +} + +func NewUserRepository() *UserRepository { + //todo + return &UserRepository{} +} + +func (r *UserRepository) FindByEmail(email string) (*models.User, error) { + //todo + return &models.User{}, nil +} + +func (r *UserRepository) StoreRecoveryRecord(userID, signature string, createdAt time.Time) error { + //todo + + return nil +} + +func (r *UserRepository) GetRecoveryRecord(signature string) (*models.RestoreRequest, error) { + //todo + + return &models.RestoreRequest{UserID: "123", Sign: signature, CreatedAt: time.Now()}, nil +} + +func (r *UserRepository) Ping() error { + return nil +} diff --git a/internal/server/http/http_server.go b/internal/server/http/http_server.go new file mode 100644 index 0000000..3efb2a8 --- /dev/null +++ b/internal/server/http/http_server.go @@ -0,0 +1,66 @@ +package http + +import ( + controller "codeword/internal/controller/recovery" + "codeword/internal/repository" + "context" + "github.com/gofiber/fiber/v2" + "go.uber.org/zap" +) + +type ServerConfig struct { + Logger *zap.Logger + Repo *repository.UserRepository + RecoveryController *controller.RecoveryController +} + +type Server struct { + Logger *zap.Logger + Repo *repository.UserRepository + RecoveryController *controller.RecoveryController + app *fiber.App +} + +func NewServer(config ServerConfig) *Server { + app := fiber.New() + + s := &Server{ + Logger: config.Logger, + Repo: config.Repo, + RecoveryController: config.RecoveryController, + app: app, + } + + s.registerRoutes() + + return s +} + +func (s *Server) registerRoutes() { + s.app.Get("/liveness", s.handleLiveness) + s.app.Get("/readiness", s.handleReadiness) + + s.app.Post("/recover", s.RecoveryController.HandleRecoveryRequest) + s.app.Get("/recover/:sign", s.RecoveryController.HandleRecoveryLink) + //... other +} + +func (s *Server) handleLiveness(c *fiber.Ctx) error { + return c.SendStatus(fiber.StatusOK) +} + +func (s *Server) handleReadiness(c *fiber.Ctx) error { + if err := s.Repo.Ping(); err != nil { + s.Logger.Error("Failed to ping the database", zap.Error(err)) + return c.Status(fiber.StatusServiceUnavailable).SendString("DB ping failed") + } + return c.Status(fiber.StatusOK).SendString("42") +} + +func (s *Server) Start(addr string) error { + return s.app.Listen(addr) +} + +func (s *Server) Shutdown(ctx context.Context) error { + return s.app.Shutdown() +} diff --git a/internal/services/recovery_service.go b/internal/services/recovery_service.go new file mode 100644 index 0000000..679ffa3 --- /dev/null +++ b/internal/services/recovery_service.go @@ -0,0 +1,66 @@ +package services + +import ( + "codeword/internal/models" + "go.uber.org/zap" + "time" +) + +type UserRepository interface { + FindByEmail(email string) (*models.User, error) + StoreRecoveryRecord(userID string, signature string, createdAt time.Time) error + GetRecoveryRecord(signature string) (*models.RestoreRequest, error) +} + +type EmailSender interface { + SendRecoveryEmail(email, signature string) error +} + +type RecoveryService struct { + Logger *zap.Logger + Repository UserRepository + Email EmailSender +} + +func NewRecoveryService(logger *zap.Logger, repository UserRepository, email EmailSender) *RecoveryService { + return &RecoveryService{ + Logger: logger, + Repository: repository, + Email: email, + } +} + +// GenerateKey генерирует ключ, используя шифрование на основе эллиптической кривой +func (s *RecoveryService) GenerateKey() (string, error) { + // TODO + return "", nil +} + +// FindUserByEmail ищет пользователя по электронной почте +func (s *RecoveryService) FindUserByEmail(email string) (*models.User, error) { + return s.Repository.FindByEmail(email) +} + +// StoreRecoveryRecord сохраняет запись восстановления в базе данных +func (s *RecoveryService) StoreRecoveryRecord(userID string) (string, error) { + signature := "" + createdAt := time.Now() + err := s.Repository.StoreRecoveryRecord(userID, signature, createdAt) + return signature, err +} + +// GetRecoveryRecord получает запись восстановления из базы данных +func (s *RecoveryService) GetRecoveryRecord(signature string) (*models.RestoreRequest, error) { + return s.Repository.GetRecoveryRecord(signature) +} + +// SendRecoveryEmail посылает письмо для восстановления доступа пользователю +func (s *RecoveryService) SendRecoveryEmail(email string, signature string) error { + return s.Email.SendRecoveryEmail(email, signature) +} + +// ExchangeForTokens обменивает ссылку восстановления на токены используя сервис аутентификации. +func (s *RecoveryService) ExchangeForTokens(userID string) (map[string]string, error) { + // TODO + return nil, nil +} diff --git a/internal/worker/recovery_worker/recovery_worker.go b/internal/worker/recovery_worker/recovery_worker.go new file mode 100644 index 0000000..a1a2be2 --- /dev/null +++ b/internal/worker/recovery_worker/recovery_worker.go @@ -0,0 +1 @@ +package recovery_worker diff --git a/template/.gitignore.tmpl b/template/.gitignore.tmpl new file mode 100644 index 0000000..2b70277 --- /dev/null +++ b/template/.gitignore.tmpl @@ -0,0 +1,161 @@ +# Created by https://www.toptal.com/developers/gitignore/api/visualstudiocode,goland,go +# Edit at https://www.toptal.com/developers/gitignore?templates=visualstudiocode,goland,go + +### Go ### +# 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 + +### GoLand ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf +.idea + +# AWS User-specific +.idea/**/aws.xml + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# SonarLint plugin +.idea/sonarlint/ + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### GoLand Patch ### +# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 + +# *.iml +# modules.xml +# .idea/misc.xml +# *.ipr + +# Sonarlint plugin +# https://plugins.jetbrains.com/plugin/7973-sonarlint +.idea/**/sonarlint/ + +# SonarQube Plugin +# https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin +.idea/**/sonarIssues.xml + +# Markdown Navigator plugin +# https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced +.idea/**/markdown-navigator.xml +.idea/**/markdown-navigator-enh.xml +.idea/**/markdown-navigator/ + +# Cache file creation bug +# See https://youtrack.jetbrains.com/issue/JBR-2257 +.idea/$CACHE_FILE$ + +# CodeStream plugin +# https://plugins.jetbrains.com/plugin/12206-codestream +.idea/codestream.xml + +# Azure Toolkit for IntelliJ plugin +# https://plugins.jetbrains.com/plugin/8053-azure-toolkit-for-intellij +.idea/**/azureSettings.xml + +### VisualStudioCode ### +.vscode +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +!.vscode/*.code-snippets + +# Local History for Visual Studio Code +.history/ + +# Built Visual Studio Code Extensions +*.vsix + +### VisualStudioCode Patch ### +# Ignore all local history of files +.history +.ionide + +# End of https://www.toptal.com/developers/gitignore/api/visualstudiocode,goland,go diff --git a/template/cmd/{{.ProjectName}}/main.go.tmpl b/template/cmd/{{.ProjectName}}/main.go.tmpl new file mode 100644 index 0000000..00bcebb --- /dev/null +++ b/template/cmd/{{.ProjectName}}/main.go.tmpl @@ -0,0 +1,29 @@ +package main + +import ( + "context" + "os/signal" + "syscall" + + "{{.Vars.ProjectName}}/internal/app" + + {{.Modules.logger.Import}} + {{.Modules.logger.ImportCore}} + + {{.Modules.env.Import}} +) + +func main() { + {{.Modules.logger.Init}} + + ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) + defer cancel() + + var config app.Config + + {{.Modules.env.Declaration "config"}} + + if err := app.Run(ctx, config, logger); err != nil { + {{.Modules.logger.Message "Fatal" "Failed to run app" "Error" "err"}} + } +} diff --git a/template/internal/app/app.go.tmpl b/template/internal/app/app.go.tmpl new file mode 100644 index 0000000..1b72ed9 --- /dev/null +++ b/template/internal/app/app.go.tmpl @@ -0,0 +1,19 @@ +package app + +import ( + "context" + + {{.Modules.logger.Import}} +) + +{{.Modules.env.Struct}} + +func Run(ctx context.Context, config Config, logger {{.Modules.logger.Type}}) error { + {{.Modules.logger.Message "info" "App started" "config" "config"}} + + <-ctx.Done() + + logger.Info("App shutting down gracefully") + + return nil +}