diff --git a/.env b/.env new file mode 100644 index 0000000..5a95060 --- /dev/null +++ b/.env @@ -0,0 +1,12 @@ +# General application settings +APP_NAME=codeword +HTTP_HOST=localhost +HTTP_PORT=8000 + +# MongoDB settings +MONGO_HOST=mongo_host +MONGO_PORT=27017 +MONGO_USER=admin +MONGO_PASSWORD=admin +MONGO_DB=codeword_db +MONGO_AUTH=admin \ No newline at end of file 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..fa1faa4 --- /dev/null +++ b/deployment/local/docker-compose.yaml @@ -0,0 +1,15 @@ +version: '3.8' + +services: + mongo: + image: mongo + ports: + - "${MONGO_PORT}:27017" + environment: + - MONGO_INITDB_ROOT_USERNAME=${MONGO_USER} + - MONGO_INITDB_ROOT_PASSWORD=${MONGO_PASSWORD} + volumes: + - mongo_data:/data/db + +volumes: + mongo_data: \ No newline at end of file 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..85b4922 --- /dev/null +++ b/go.mod @@ -0,0 +1,31 @@ +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/golang/snappy v0.0.1 // 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/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe // 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 + github.com/xdg-go/pbkdf2 v1.0.0 // indirect + github.com/xdg-go/scram v1.1.2 // indirect + github.com/xdg-go/stringprep v1.0.4 // indirect + github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect + go.mongodb.org/mongo-driver v1.13.1 // indirect + go.uber.org/multierr v1.10.0 // indirect + go.uber.org/zap v1.26.0 // indirect + golang.org/x/crypto v0.7.0 // indirect + golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 // indirect + golang.org/x/sys v0.14.0 // indirect + golang.org/x/text v0.8.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..3981795 --- /dev/null +++ b/go.sum @@ -0,0 +1,85 @@ +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/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +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/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4= +github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +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.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= +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/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe h1:iruDEfMl2E6fbMZ9s0scYfZQ84/6SPL6zC8ACM2oIL0= +github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= +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= +github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= +github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= +github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= +github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= +github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8= +github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= +github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d h1:splanxYIlg+5LfHAM6xpdFEAYOk8iySO56hMFq6uLyA= +github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.mongodb.org/mongo-driver v1.13.1 h1:YIc7HTYsKndGK4RFzJ3covLz1byri52x0IoMB0Pt/vk= +go.mongodb.org/mongo-driver v1.13.1/go.mod h1:wcDf1JBCXy2mOW0bWHwO/IOYqdca1MPCwDtFu/Z9+eo= +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/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A= +golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 h1:uVc8UZUe6tr40fFVnUP5Oj+veunVezqYl9z7DYw9xzw= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +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= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68= +golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 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..c0e0a18 --- /dev/null +++ b/internal/app/app.go @@ -0,0 +1,59 @@ +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)) + + mdb, err := initialize.InitializeMongoDB(ctx, cfg) + if err != nil { + logger.Error("Failed to initialize MongoDB", zap.Error(err)) + return err + } + + userRepo := repository.NewUserRepository(mdb) + 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(cfg.HTTPHost + ":" + cfg.HTTPPort); 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/config.go b/internal/initialize/config.go new file mode 100644 index 0000000..dec81f5 --- /dev/null +++ b/internal/initialize/config.go @@ -0,0 +1,25 @@ +package initialize + +import ( + "github.com/caarlos0/env/v8" +) + +type Config struct { + AppName string `env:"APP_NAME" envDefault:"codeword"` + HTTPHost string `env:"HTTP_HOST" envDefault:"localhost"` + HTTPPort string `env:"HTTP_PORT" envDefault:"3000"` + MongoHost string `env:"MONGO_HOST" envDefault:"localhost"` + MongoPort string `env:"MONGO_PORT" envDefault:"27017"` + MongoUser string `env:"MONGO_USER" envDefault:"admin"` + MongoPassword string `env:"MONGO_PASSWORD" envDefault:"admin"` + MongoDatabase string `env:"MONGO_DB" envDefault:"codeword_db"` + MongoAuth string `env:"MONGO_AUTH" envDefault:"admin"` +} + +func LoadConfig() (*Config, error) { + var config Config + if err := env.Parse(&config); err != nil { + return nil, err + } + return &config, nil +} diff --git a/internal/initialize/mongo.go b/internal/initialize/mongo.go new file mode 100644 index 0000000..df49819 --- /dev/null +++ b/internal/initialize/mongo.go @@ -0,0 +1,36 @@ +package initialize + +import ( + mdb "codeword/pkg/mongo" + "context" + "go.mongodb.org/mongo-driver/mongo" + "time" +) + +func InitializeMongoDB(ctx context.Context, cfg Config) (*mongo.Database, error) { + dbConfig := &mdb.Configuration{ + MongoHost: cfg.MongoHost, + MongoPort: cfg.MongoPort, + MongoUser: cfg.MongoUser, + MongoPassword: cfg.MongoPassword, + MongoDatabase: cfg.MongoDatabase, + MongoAuth: cfg.MongoAuth, + } + + mongoDeps := &mdb.ConnectDeps{ + Configuration: dbConfig, + Timeout: 10 * time.Second, + } + + db, err := mdb.Connect(ctx, mongoDeps) + if err != nil { + return nil, err + } + + err = db.Client().Ping(ctx, nil) + if err != nil { + return nil, err + } + + return db, 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..4009049 --- /dev/null +++ b/internal/repository/user_repository.go @@ -0,0 +1,38 @@ +package repository + +import ( + "codeword/internal/models" + "context" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/readpref" + "time" +) + +type UserRepository struct { + db *mongo.Database +} + +func NewUserRepository(db *mongo.Database) *UserRepository { + return &UserRepository{db} +} + +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(ctx context.Context) error { + return r.db.Client().Ping(ctx, readpref.Primary()) +} diff --git a/internal/server/http/http_server.go b/internal/server/http/http_server.go new file mode 100644 index 0000000..56fa3bc --- /dev/null +++ b/internal/server/http/http_server.go @@ -0,0 +1,74 @@ +package http + +import ( + controller "codeword/internal/controller/recovery" + "codeword/internal/repository" + "context" + "fmt" + "github.com/gofiber/fiber/v2" + "go.uber.org/zap" + "time" +) + +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 { + startTime := time.Now() + if err := s.Repo.Ping(c.Context()); err != nil { + s.Logger.Error("Failed to ping the database", zap.Error(err)) + return c.Status(fiber.StatusServiceUnavailable).SendString("DB ping failed") + } + duration := time.Since(startTime) + + durationMillis := duration.Milliseconds() + responseMessage := fmt.Sprintf("DB ping success - Time taken: %d ms", durationMillis) + + return c.Status(fiber.StatusOK).SendString(responseMessage) +} + +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/pkg/mongo/config.go b/pkg/mongo/config.go new file mode 100644 index 0000000..34d845e --- /dev/null +++ b/pkg/mongo/config.go @@ -0,0 +1,22 @@ +package mongo + +import ( + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +type Configuration struct { + MongoHost string `env:"MONGO_HOST" envDefault:"localhost"` + MongoPort string `env:"MONGO_PORT" envDefault:"27017"` + MongoUser string `env:"MONGO_USER" envDefault:"admin"` + MongoPassword string `env:"MONGO_PASSWORD" envDefault:"admin"` + MongoDatabase string `env:"MONGO_DB" envDefault:"codeword_db"` + MongoAuth string `env:"MONGO_AUTH" envDefault:"admin"` +} + +type RequestSettings struct { + Driver *mongo.Collection + Options *options.FindOptions + Filter primitive.M +} diff --git a/pkg/mongo/connection.go b/pkg/mongo/connection.go new file mode 100644 index 0000000..80c22b1 --- /dev/null +++ b/pkg/mongo/connection.go @@ -0,0 +1,59 @@ +package mongo + +import ( + "context" + "fmt" + "log" + "net" + "net/url" + "time" + + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +type ConnectDeps struct { + Configuration *Configuration + Timeout time.Duration +} + +func Connect(ctx context.Context, deps *ConnectDeps) (*mongo.Database, error) { + if deps == nil { + return nil, ErrEmptyArgs + } + + mongoURI := &url.URL{ + Scheme: "mongodb", + Host: net.JoinHostPort(deps.Configuration.MongoHost, deps.Configuration.MongoPort), + } + + connectionOptions := options.Client(). + ApplyURI(mongoURI.String()). + SetAuth(options.Credential{ + AuthMechanism: "SCRAM-SHA-1", + AuthSource: deps.Configuration.MongoAuth, + Username: deps.Configuration.MongoUser, + Password: deps.Configuration.MongoPassword, + }) + + ticker := time.NewTicker(1 * time.Second) + timeoutExceeded := time.After(deps.Timeout) + + defer ticker.Stop() + + for { + select { + case <-ticker.C: + connection, err := mongo.Connect(ctx, connectionOptions) + if err == nil { + return connection.Database(deps.Configuration.MongoDatabase), nil + } + + log.Printf("failed to connect to db <%s>: %s", mongoURI.String(), err.Error()) + case <-timeoutExceeded: + return nil, fmt.Errorf("db connection <%s> failed after %d timeout", mongoURI.String(), deps.Timeout) + default: + time.Sleep(1 * time.Second) + } + } +} diff --git a/pkg/mongo/errors.go b/pkg/mongo/errors.go new file mode 100644 index 0000000..2e592f2 --- /dev/null +++ b/pkg/mongo/errors.go @@ -0,0 +1,7 @@ +package mongo + +import "errors" + +var ( + ErrEmptyArgs = errors.New("arguments are empty") +) 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 +}