config loaded from .env

This commit is contained in:
Dmitri 2026-04-22 21:05:50 +02:00
parent af256b66b5
commit a5238921e3
Signed by: kanopo
GPG Key ID: 759ADD40E3132AC7
28 changed files with 51 additions and 1059 deletions

5
.gitignore vendored
View File

@ -1,3 +1,8 @@
target/ target/
.env .env
.data/ .data/
# Added by cargo
/target

16
Cargo.lock generated Normal file
View File

@ -0,0 +1,16 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "dotenvy"
version = "0.15.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
[[package]]
name = "rhythm-backend"
version = "0.1.0"
dependencies = [
"dotenvy",
]

7
Cargo.toml Normal file
View File

@ -0,0 +1,7 @@
[package]
name = "rhythm-backend"
version = "0.1.0"
edition = "2024"
[dependencies]
dotenvy = "0.15.7"

View File

@ -1,17 +0,0 @@
.PHONY: help run test fmt
help:
@echo "Targets:"
@echo " run - run API"
@echo " test - run tests"
@echo " fmt - format Go code"
run:
go run ./cmd/api
test:
go test ./...
fmt:
go fmt ./...

View File

@ -1,40 +0,0 @@
package main
import (
"git.kanopo.dev/rhythm/rhythm-backend/internal/config"
"git.kanopo.dev/rhythm/rhythm-backend/internal/db"
usersdb "git.kanopo.dev/rhythm/rhythm-backend/internal/db/users"
"git.kanopo.dev/rhythm/rhythm-backend/internal/http"
"git.kanopo.dev/rhythm/rhythm-backend/internal/http/api/auth"
"git.kanopo.dev/rhythm/rhythm-backend/internal/http/api/health"
"git.kanopo.dev/rhythm/rhythm-backend/internal/logger"
"git.kanopo.dev/rhythm/rhythm-backend/internal/service/users"
"go.uber.org/fx"
"go.uber.org/fx/fxevent"
"go.uber.org/zap"
"github.com/jackc/pgx/v5/pgxpool"
)
func main() {
fx.New(
fx.Provide(
config.Provide,
logger.ProvideLogger,
db.ProvidePool,
func(pool *pgxpool.Pool) usersdb.Querier {
return usersdb.New(pool)
},
users.NewService,
http.NewServer,
health.NewHandler,
auth.NewHandler,
),
fx.Invoke(
http.GlueRoutes,
),
fx.WithLogger(func(logger *zap.Logger) fxevent.Logger {
return &fxevent.ZapLogger{Logger: logger}
}),
).Run()
}

57
go.mod
View File

@ -1,57 +0,0 @@
module git.kanopo.dev/rhythm/rhythm-backend
go 1.26.2
require (
github.com/golang-jwt/jwt/v5 v5.3.1
github.com/google/uuid v1.6.0
github.com/jackc/pgx/v5 v5.9.2
github.com/joho/godotenv v1.5.1
github.com/pressly/goose/v3 v3.27.0
golang.org/x/crypto v0.48.0
)
require (
github.com/bytedance/gopkg v0.1.3 // indirect
github.com/bytedance/sonic v1.15.0 // indirect
github.com/bytedance/sonic/loader v0.5.0 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect
github.com/gabriel-vasile/mimetype v1.4.12 // indirect
github.com/gin-contrib/sse v1.1.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.30.1 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/goccy/go-yaml v1.19.2 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.3.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.4 // indirect
github.com/quic-go/qpack v0.6.0 // indirect
github.com/quic-go/quic-go v0.59.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.3.1 // indirect
go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect
go.uber.org/dig v1.19.0 // indirect
golang.org/x/arch v0.22.0 // indirect
golang.org/x/net v0.51.0 // indirect
golang.org/x/sys v0.41.0 // indirect
google.golang.org/protobuf v1.36.11 // indirect
)
require (
github.com/gin-gonic/gin v1.12.0
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/mfridman/interpolate v0.0.2 // indirect
github.com/sethvargo/go-retry v0.3.0 // indirect
go.uber.org/fx v1.24.0
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.1
golang.org/x/sync v0.19.0 // indirect
golang.org/x/text v0.34.0 // indirect
)

138
go.sum
View File

@ -1,138 +0,0 @@
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE=
github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k=
github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE=
github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
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/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8=
github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc=
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.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=
github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
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/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
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.9.2 h1:3ZhOzMWnR4yJ+RW1XImIPsD1aNSz4T4fyP7zlQb56hw=
github.com/jackc/pgx/v5 v5.9.2/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
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/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
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.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
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/mfridman/interpolate v0.0.2 h1:pnuTK7MQIxxFz1Gr+rjSIx9u7qVjf5VOoM/u6BbAxPY=
github.com/mfridman/interpolate v0.0.2/go.mod h1:p+7uk6oE07mpE/Ik1b8EckO0O4ZXiGAfshKBWLUM9Xg=
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/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
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/pressly/goose/v3 v3.27.0 h1:/D30gVTuQhu0WsNZYbJi4DMOsx1lNq+6SkLe+Wp59BM=
github.com/pressly/goose/v3 v3.27.0/go.mod h1:3ZBeCXqzkgIRvrEMDkYh1guvtoJTU5oMMuDdkutoM78=
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw=
github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah2SE=
github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLyYaFyAZraas=
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.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
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.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE=
go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
go.uber.org/dig v1.19.0 h1:BACLhebsYdpQ7IROQ1AGPjrXcP5dF80U3gKoFzbaq/4=
go.uber.org/dig v1.19.0/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE=
go.uber.org/fx v1.24.0 h1:wE8mruvpg2kiiL1Vqd0CC+tr0/24XIB10Iwp2lLWzkg=
go.uber.org/fx v1.24.0/go.mod h1:AmDeGyS+ZARGKM4tlH4FY2Jr63VjbEDJHtqXTGP5hbo=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI=
golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa h1:Zt3DZoOFFYkKhDT3v7Lm9FDMEV06GpzjG2jrqW+QTE0=
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA=
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
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=
modernc.org/libc v1.68.0 h1:PJ5ikFOV5pwpW+VqCK1hKJuEWsonkIJhhIXyuF/91pQ=
modernc.org/libc v1.68.0/go.mod h1:NnKCYeoYgsEqnY3PgvNgAeaJnso968ygU8Z0DxjoEc0=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
modernc.org/sqlite v1.46.1 h1:eFJ2ShBLIEnUWlLy12raN0Z1plqmFX9Qe3rjQTKt6sU=
modernc.org/sqlite v1.46.1/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA=

View File

@ -1,65 +0,0 @@
package config
import (
"fmt"
_ "github.com/joho/godotenv/autoload"
"log"
"os"
"time"
)
type Config struct {
AppEnv string
DbUrl string
ServerPort string
JWTSecret string
JWTExpiry time.Duration
RefreshExpiry time.Duration
}
func Load() Config {
var dbUrl string
{
username := getEnv("DB_USERNAME")
password := getEnv("DB_PASSWORD")
name := getEnv("DB_NAME")
port := getEnv("DB_PORT")
host := getEnv("DB_HOST")
dbUrl = fmt.Sprintf("postgres://%v:%v@%v:%v/%v?sslmode=disable", username, password, host, port, name)
}
appEnv := os.Getenv("APP_ENV")
if appEnv == "" {
appEnv = "development"
}
jwtSecret := getEnv("JWT_SECRET")
if jwtSecret == "" {
log.Fatal("JWT_SECRET env variable is required")
}
cfg := Config{
AppEnv: appEnv,
DbUrl: dbUrl,
ServerPort: "8080",
JWTSecret: jwtSecret,
JWTExpiry: time.Minute * 15,
RefreshExpiry: time.Hour * 24 * 7,
}
return cfg
}
func getEnv(key string) string {
v := os.Getenv(key)
if v == "" {
log.Fatalf("The env variable %v is not defined and the application can not operate without\n", key)
}
return v
}
func Provide() *Config {
cfg := Load()
return &cfg
}

View File

@ -1,39 +0,0 @@
package db
import (
"context"
"time"
"git.kanopo.dev/rhythm/rhythm-backend/internal/config"
"github.com/jackc/pgx/v5/pgxpool"
"go.uber.org/fx"
"go.uber.org/zap"
)
func ProvidePool(lc fx.Lifecycle, cfg *config.Config, log *zap.SugaredLogger) (*pgxpool.Pool, error) {
ctx := context.Background()
pool, err := pgxpool.New(ctx, cfg.DbUrl)
if err != nil {
return nil, err
}
{
ctx, cancel := context.WithTimeout(ctx, time.Second*5)
defer cancel()
if err := pool.Ping(ctx); err != nil {
return nil, err
}
log.Info("successfully connected to database")
RunMigrations(cfg.DbUrl, log)
}
lc.Append(fx.Hook{
OnStop: func(ctx context.Context) error {
log.Info("Closing database pool")
pool.Close()
return nil
},
})
return pool, nil
}

View File

@ -1,33 +0,0 @@
package db
import (
"database/sql"
"log"
"git.kanopo.dev/rhythm/rhythm-backend/internal/logger"
"git.kanopo.dev/rhythm/rhythm-backend/migrations"
_ "github.com/jackc/pgx/v5/stdlib"
"github.com/pressly/goose/v3"
"go.uber.org/zap"
)
func RunMigrations(dbURL string, zLog *zap.SugaredLogger) {
db, err := sql.Open("pgx", dbURL)
if err != nil {
log.Fatalf("open db for migrations: %v", err.Error())
}
defer db.Close()
goose.SetLogger(&logger.GooseLogger{SugaredLogger: zLog})
// 2. Pass the exported FS to Goose!
goose.SetBaseFS(migrations.FS)
if err := goose.SetDialect("postgres"); err != nil {
log.Fatalf("set goose dialect: %v", err.Error())
}
// 3. Run the migrations using the current directory "." of the embed.FS
if err := goose.Up(db, "."); err != nil {
log.Fatalf("run goose up: %v", err.Error())
}
}

View File

@ -1,32 +0,0 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
package usersdb
import (
"context"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgconn"
)
type DBTX interface {
Exec(context.Context, string, ...interface{}) (pgconn.CommandTag, error)
Query(context.Context, string, ...interface{}) (pgx.Rows, error)
QueryRow(context.Context, string, ...interface{}) pgx.Row
}
func New(db DBTX) *Queries {
return &Queries{db: db}
}
type Queries struct {
db DBTX
}
func (q *Queries) WithTx(tx pgx.Tx) *Queries {
return &Queries{
db: tx,
}
}

View File

@ -1,19 +0,0 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
package usersdb
import (
"time"
"github.com/google/uuid"
)
type User struct {
ID uuid.UUID
Email string
Password string
CreatedAt time.Time
UpdatedAt time.Time
}

View File

@ -1,20 +0,0 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
package usersdb
import (
"context"
"github.com/google/uuid"
)
type Querier interface {
CreateUser(ctx context.Context, arg CreateUserParams) (User, error)
DeleteUser(ctx context.Context, id uuid.UUID) error
GetUser(ctx context.Context, id uuid.UUID) (User, error)
GetUserByEmail(ctx context.Context, email string) (User, error)
}
var _ Querier = (*Queries)(nil)

View File

@ -1,19 +0,0 @@
-- name: GetUser :one
select * from users
where id = $1 limit 1;
-- name: GetUserByEmail :one
select * from users
where email = $1 limit 1;
-- name: CreateUser :one
insert into users (
email, password
) values (
$1, $2
)
returning *;
-- name: DeleteUser :exec
DELETE FROM users
WHERE id = $1;

View File

@ -1,85 +0,0 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
// source: queries.sql
package usersdb
import (
"context"
"github.com/google/uuid"
)
const createUser = `-- name: CreateUser :one
insert into users (
email, password
) values (
$1, $2
)
returning id, email, password, created_at, updated_at
`
type CreateUserParams struct {
Email string
Password string
}
func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (User, error) {
row := q.db.QueryRow(ctx, createUser, arg.Email, arg.Password)
var i User
err := row.Scan(
&i.ID,
&i.Email,
&i.Password,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const deleteUser = `-- name: DeleteUser :exec
DELETE FROM users
WHERE id = $1
`
func (q *Queries) DeleteUser(ctx context.Context, id uuid.UUID) error {
_, err := q.db.Exec(ctx, deleteUser, id)
return err
}
const getUser = `-- name: GetUser :one
select id, email, password, created_at, updated_at from users
where id = $1 limit 1
`
func (q *Queries) GetUser(ctx context.Context, id uuid.UUID) (User, error) {
row := q.db.QueryRow(ctx, getUser, id)
var i User
err := row.Scan(
&i.ID,
&i.Email,
&i.Password,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const getUserByEmail = `-- name: GetUserByEmail :one
select id, email, password, created_at, updated_at from users
where email = $1 limit 1
`
func (q *Queries) GetUserByEmail(ctx context.Context, email string) (User, error) {
row := q.db.QueryRow(ctx, getUserByEmail, email)
var i User
err := row.Scan(
&i.ID,
&i.Email,
&i.Password,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}

View File

@ -1,91 +0,0 @@
package auth
import (
"errors"
"net/http"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
"git.kanopo.dev/rhythm/rhythm-backend/internal/config"
"git.kanopo.dev/rhythm/rhythm-backend/internal/service/users"
)
type Handler struct {
svc users.Service
log *zap.SugaredLogger
cfg *config.Config
}
func NewHandler(svc users.Service, log *zap.SugaredLogger, cfg *config.Config) *Handler {
return &Handler{
svc: svc,
log: log,
cfg: cfg,
}
}
func (h *Handler) RegisterRoutes(rg *gin.RouterGroup) {
rg.POST("/login", h.Login)
rg.POST("/register", h.Register)
}
func setRefreshTokenCookie(c *gin.Context, token string, maxAge int) {
c.SetCookie(
"refresh_token",
token,
maxAge,
"/",
"",
false,
true,
)
}
func (h *Handler) Register(c *gin.Context) {
var req struct {
Email string `json:"email" binding:"required,email"`
Password string `json:"password" binding:"required,min=8"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
result, err := h.svc.Register(c.Request.Context(), req.Email, req.Password)
if err != nil {
h.log.Error("registration error", "error", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "internal server error"})
return
}
setRefreshTokenCookie(c, result.RefreshToken, int(h.cfg.RefreshExpiry))
c.JSON(http.StatusCreated, gin.H{"accessToken": result.AccessToken})
}
func (h *Handler) Login(c *gin.Context) {
var req struct {
Email string `json:"email" binding:"required,email"`
Password string `json:"password" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
result, err := h.svc.Login(c.Request.Context(), req.Email, req.Password)
if err != nil {
if errors.Is(err, users.ErrInvalidCredentials) {
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid credentials"})
return
}
h.log.Error("login error", "error", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "internal server error"})
return
}
setRefreshTokenCookie(c, result.RefreshToken, int(h.cfg.RefreshExpiry))
c.JSON(http.StatusOK, gin.H{"accessToken": result.AccessToken})
}

View File

@ -1,42 +0,0 @@
package health
import (
"context"
"net/http"
"github.com/gin-gonic/gin"
"github.com/jackc/pgx/v5/pgxpool"
"go.uber.org/zap"
)
type Handler struct {
log *zap.SugaredLogger
pool *pgxpool.Pool
}
func NewHandler(log *zap.SugaredLogger, pool *pgxpool.Pool) *Handler {
return &Handler{
log: log,
pool: pool,
}
}
func (h *Handler) RegisterRoutes(rg *gin.RouterGroup) {
rg.GET("/live", h.Live)
rg.GET("/ready", h.Ready)
}
func (h *Handler) Live(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"status": "alive"})
}
func (h *Handler) Ready(c *gin.Context) {
ctx := context.Background()
if err := h.pool.Ping(ctx); err != nil {
h.log.Warnw("health check: db ping failed", "error", err)
c.JSON(http.StatusServiceUnavailable, gin.H{"status": "not ready", "error": "db unreachable"})
return
}
c.JSON(http.StatusOK, gin.H{"status": "ready"})
}

View File

@ -1,15 +0,0 @@
package http
import (
"git.kanopo.dev/rhythm/rhythm-backend/internal/http/api/auth"
"git.kanopo.dev/rhythm/rhythm-backend/internal/http/api/health"
"github.com/gin-gonic/gin"
)
func GlueRoutes(r *gin.Engine, healthHandler *health.Handler, authHandler *auth.Handler) {
api := r.Group("/api")
healthHandler.RegisterRoutes(api.Group("/health"))
v1 := api.Group("/v1")
authHandler.RegisterRoutes(v1.Group("/auth"))
}

View File

@ -1,43 +0,0 @@
package http
import (
"context"
"net/http"
"git.kanopo.dev/rhythm/rhythm-backend/internal/config"
"github.com/gin-gonic/gin"
"go.uber.org/fx"
"go.uber.org/zap"
)
func NewServer(lc fx.Lifecycle, cfg *config.Config, log *zap.SugaredLogger) *gin.Engine {
if cfg.AppEnv == "production" {
gin.SetMode(gin.ReleaseMode)
}
r := gin.New()
r.Use(gin.Recovery())
server := &http.Server{
Addr: ":" + cfg.ServerPort,
Handler: r,
}
lc.Append(fx.Hook{
OnStart: func(ctx context.Context) error {
log.Infof("Starting HTTP server on port %v", cfg.ServerPort)
go func() {
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("error starting http server %v", err.Error())
}
}()
return nil
},
OnStop: func(ctx context.Context) error {
log.Info("Shutting down HTTP server\n")
return server.Shutdown(ctx)
},
})
return r
}

View File

@ -1,68 +0,0 @@
package jwt
import (
"errors"
"time"
"git.kanopo.dev/rhythm/rhythm-backend/internal/config"
"github.com/golang-jwt/jwt/v5"
)
type Claims struct {
UserID string `json:"userId"`
jwt.RegisteredClaims
}
type TokenPair struct {
AccessToken string
RefreshToken string
}
func GenerateTokenPair(userID string, cfg *config.Config) (TokenPair, error) {
accessToken, err := generateToken(userID, cfg.JWTSecret, cfg.JWTExpiry)
if err != nil {
return TokenPair{}, err
}
refreshToken, err := generateToken(userID, cfg.JWTSecret, cfg.RefreshExpiry)
if err != nil {
return TokenPair{}, err
}
return TokenPair{
AccessToken: accessToken,
RefreshToken: refreshToken,
}, nil
}
func generateToken(userID, secret string, expiry time.Duration) (string, error) {
claims := &Claims{
UserID: userID,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(expiry)),
IssuedAt: jwt.NewNumericDate(time.Now()),
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString([]byte(secret))
}
func ValidateToken(tokenString, secret string) (*Claims, error) {
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, errors.New("unexpected signing method")
}
return []byte(secret), nil
})
if err != nil {
return nil, err
}
if claims, ok := token.Claims.(*Claims); ok && token.Valid {
return claims, nil
}
return nil, errors.New("invalid token")
}

View File

@ -1,50 +0,0 @@
package logger
import (
"log"
"go.uber.org/zap"
"git.kanopo.dev/rhythm/rhythm-backend/internal/config"
)
func New(env string) (*zap.Logger, *zap.SugaredLogger) {
var zapLogger *zap.Logger
var err error
if env == "production" {
zapLogger, err = zap.NewProduction()
} else {
zapLogger, err = zap.NewDevelopment()
}
if err != nil {
log.Fatalf("failed to initialize zap logger: %v", err)
}
return zapLogger, zapLogger.Sugar()
}
func ProvideLogger(cfg *config.Config) (*zap.Logger, *zap.SugaredLogger) {
return New(cfg.AppEnv)
}
type GooseLogger struct {
*zap.SugaredLogger
}
func (l *GooseLogger) Fatal(v ...interface{}) {
l.SugaredLogger.Fatal(v...)
}
func (l *GooseLogger) Fatalf(format string, v ...interface{}) {
l.SugaredLogger.Fatalf(format, v...)
}
func (l *GooseLogger) Print(v ...interface{}) {
l.SugaredLogger.Info(v...)
}
func (l *GooseLogger) Printf(format string, v ...interface{}) {
l.SugaredLogger.Infof(format, v...)
}

View File

@ -1,15 +0,0 @@
package password
import (
"golang.org/x/crypto/bcrypt"
)
func HashPassword(password string) (string, error) {
bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
return string(bytes), err
}
func CheckPassword(password, hash string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
return err == nil
}

View File

@ -1,123 +0,0 @@
package users
import (
"context"
"errors"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
"go.uber.org/zap"
"git.kanopo.dev/rhythm/rhythm-backend/internal/config"
usersdb "git.kanopo.dev/rhythm/rhythm-backend/internal/db/users"
"git.kanopo.dev/rhythm/rhythm-backend/internal/jwt"
"git.kanopo.dev/rhythm/rhythm-backend/internal/password"
)
var ErrInvalidCredentials = errors.New("invalid credentials")
type AuthResult struct {
AccessToken string
RefreshToken string
}
type Service interface {
Login(ctx context.Context, email, password string) (*AuthResult, error)
Register(ctx context.Context, email, password string) (*AuthResult, error)
GetUserByEmail(ctx context.Context, email string) (usersdb.User, error)
}
type service struct {
pool *pgxpool.Pool
repo usersdb.Querier
cfg *config.Config
log *zap.SugaredLogger
}
func NewService(repo usersdb.Querier, cfg *config.Config, log *zap.SugaredLogger, pool *pgxpool.Pool) Service {
return &service{
repo: repo,
cfg: cfg,
log: log,
pool: pool,
}
}
func (s *service) Login(ctx context.Context, email, passwordPlain string) (*AuthResult, error) {
user, err := s.repo.GetUserByEmail(ctx, email)
if err != nil {
s.log.Infof("login failed: user not found with email %v", email)
return nil, ErrInvalidCredentials
}
if !password.CheckPassword(passwordPlain, user.Password) {
s.log.Infof("login failed: invalid password for email %v", email)
return nil, ErrInvalidCredentials
}
tokenPair, err := jwt.GenerateTokenPair(user.ID.String(), s.cfg)
if err != nil {
s.log.Errorf("failed to generate token pair %v", err)
return nil, err
}
s.log.Infof("user logged in successfully with email %v", email)
return &AuthResult{
AccessToken: tokenPair.AccessToken,
RefreshToken: tokenPair.RefreshToken,
}, nil
}
func (s *service) Register(ctx context.Context, email, passwordPlain string) (*AuthResult, error) {
tx, err := s.pool.BeginTx(ctx, pgx.TxOptions{})
if err != nil {
return nil, err
}
defer tx.Rollback(ctx)
txRepo := usersdb.New(tx)
_, err = txRepo.GetUserByEmail(ctx, email)
if err == nil {
s.log.Infof("registration failed: email already exists %v", email)
return nil, ErrInvalidCredentials
}
if !errors.Is(err, pgx.ErrNoRows) {
return nil, err
}
hash, err := password.HashPassword(passwordPlain)
if err != nil {
return nil, err
}
user, err := txRepo.CreateUser(ctx, usersdb.CreateUserParams{
Email: email,
Password: hash,
})
if err != nil {
return nil, err
}
if err := tx.Commit(ctx); err != nil {
return nil, err
}
tokenPair, err := jwt.GenerateTokenPair(user.ID.String(), s.cfg)
if err != nil {
s.log.Error("failed to generate token pair", "error", err)
return nil, err
}
s.log.Info("user registered successfully", "email", email)
return &AuthResult{
AccessToken: tokenPair.AccessToken,
RefreshToken: tokenPair.RefreshToken,
}, nil
}
func (s *service) GetUserByEmail(ctx context.Context, email string) (usersdb.User, error) {
return s.repo.GetUserByEmail(ctx, email)
}

View File

@ -1,11 +0,0 @@
-- +goose Up
create table users (
id uuid primary key default uuidv4(),
email varchar(255) not null unique,
password varchar(255) not null,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
-- +goose Down
drop table if exists users;

View File

@ -1,6 +0,0 @@
package migrations
import "embed"
//go:embed *.sql
var FS embed.FS

View File

@ -1,31 +0,0 @@
version: "2"
cloud:
organization: ""
project: ""
hostname: ""
servers: []
sql:
- engine: 'postgresql'
schema: 'migrations/*.sql'
queries: 'internal/db/users/queries.sql'
gen:
go:
package: "usersdb"
out: "internal/db/users"
sql_package: "pgx/v5"
emit_json_tags: false
emit_interface: true
overrides:
- db_type: "uuid"
go_type:
import: "github.com/google/uuid"
type: "UUID"
- db_type: "timestamptz"
go_type:
import: "time"
type: "Time"
overrides:
go: null
plugins: []
rules: []
options: {}

17
src/config.rs Normal file
View File

@ -0,0 +1,17 @@
use std::env;
use dotenvy::dotenv;
#[derive(Debug)]
pub struct Config {
pub db_url: String,
}
impl Config {
pub fn load() -> Self {
dotenv().ok();
Self {
db_url: env::var("DB_URL").expect("DB_URL is not configured"),
}
}
}

6
src/main.rs Normal file
View File

@ -0,0 +1,6 @@
mod config;
fn main() {
println!("Hello, world!");
let cfg = config::Config::load();
println!("{:?}", cfg)
}