diff --git a/.env.example b/.env.example index 5e241f6..6335496 100644 --- a/.env.example +++ b/.env.example @@ -1,3 +1,10 @@ -DATASOURCE= +# pgx, postgresql, mysql +DRIVERNAME=pgx +# enable / disable migrations +MIGRATE= +# as example +DATASOURCE=postgresql://developer:secret@localhost:5432/db?sslmode=disable +# hex string format ASYMMETRICKEY= +# in minutes DURATION= diff --git a/app/app.go b/app/app.go new file mode 100644 index 0000000..437257f --- /dev/null +++ b/app/app.go @@ -0,0 +1,182 @@ +package app + +import ( + "bufio" + "database/sql" + "embed" + "errors" + "fmt" + "gopher-toolbox/utils" + "log/slog" + "os" + "strings" + "time" + + "aidanwoods.dev/go-paseto" + "github.com/golang-migrate/migrate/v4" + _ "github.com/golang-migrate/migrate/v4/database/postgres" + "github.com/golang-migrate/migrate/v4/source/iofs" + _ "github.com/jackc/pgx/v5/stdlib" +) + +const ( + // Handlers keys + InvalidRequest string = "invalid_request" + InternalError string = "internal_error" + RequestID string = "request_id" + NotFound string = "not_found" + Created string = "created" + Updated string = "updated" + Deleted string = "deleted" + Enabled string = "enabled" + Disabled string = "disabled" + Retrieved string = "retrieved" + ErrorCreating string = "error_creating" + ErrorUpdating string = "error_updating" + ErrorEnabling string = "error_enabling" + ErrorDisabling string = "error_disabling" + ErrorGetting string = "error_getting" + ErrorGettingAll string = "error_getting_all" + InvalidEntityID string = "invalid_entity_id" + NotImplemented string = "not_implemented" + + // User keys + UserUsernameKey string = "user_username_key" + UserEmailKey string = "user_email_key" + UsernameAlReadyExists string = "username_already_exists" + EmailAlreadyExists string = "email_already_exists" + IncorrectPassword string = "incorrect_password" + ErrorGeneratingToken string = "error_generating_token" + LoggedIn string = "logged_in" +) + +type App struct { + Database AppDatabase + Security AppSecurity + AppInfo AppInfo +} + +type AppDatabase struct { + DriverName string + DataSource string + Migrate bool +} + +type AppInfo struct { + Version string +} + +type AppSecurity struct { + AsymmetricKey paseto.V4AsymmetricSecretKey + PublicKey paseto.V4AsymmetricPublicKey + Duration time.Duration +} + +func New(version string) *App { + var err error + + err = loadEnvFile() + if err != nil { + slog.Error("error loading env file", "error", err) + panic(err) + } + + var durationTime time.Duration + var ak paseto.V4AsymmetricSecretKey + + ak, err = paseto.NewV4AsymmetricSecretKeyFromHex(os.Getenv("ASYMMETRICKEY")) + if err != nil { + ak = paseto.NewV4AsymmetricSecretKey() + } + pk := ak.Public() + + duration := os.Getenv("DURATION") + if duration != "" { + durationTime, err = time.ParseDuration(duration) + if err != nil { + durationTime = time.Hour * 24 * 7 + } + } + + return &App{ + Database: AppDatabase{ + Migrate: utils.GetBool(os.Getenv("MIGRATE")), + DriverName: os.Getenv("DRIVERNAME"), + DataSource: os.Getenv("DATASOURCE"), + }, + Security: AppSecurity{ + AsymmetricKey: ak, + PublicKey: pk, + Duration: durationTime, + }, + AppInfo: AppInfo{ + Version: version, + }, + } +} + +// MigrateDB migrates the database. The migrations must stored in the +// "database/migrations" directory inside cmd directory along with the main.go. +// +// cmd/main.go +// +// cmd/database/migrations/*.sql +func (a *App) Migrate(database embed.FS) { + if a.Database.Migrate == false { + slog.Info("migration disabled") + return + } + dbConn, err := sql.Open(a.Database.DriverName, a.Database.DataSource) + if err != nil { + fmt.Println(err) + return + } + defer dbConn.Close() + + d, err := iofs.New(database, "database/migrations") + if err != nil { + fmt.Println(err) + return + } + + m, err := migrate.NewWithSourceInstance("iofs", d, a.Database.DataSource) + if err != nil { + fmt.Println(err) + return + } + + err = m.Up() + if err != nil && !errors.Is(err, migrate.ErrNoChange) { + slog.Error("cannot migrate", "error", err) + panic(err) + } + if errors.Is(err, migrate.ErrNoChange) { + slog.Info("migration has no changes") + } + + slog.Info("migration done") +} + +func loadEnvFile() error { + file, err := os.Open(".env") + if err != nil { + return err + } + defer file.Close() + + scanner := bufio.NewScanner(file) + for scanner.Scan() { + line := scanner.Text() + if len(line) == 0 || strings.HasPrefix(line, "#") { + continue + } + parts := strings.SplitN(line, "=", 2) + if len(parts) != 2 { + continue + } + key := strings.TrimSpace(parts[0]) + value := strings.TrimSpace(parts[1]) + os.Setenv(key, value) + } + return scanner.Err() +} diff --git a/config/config.go b/config/config.go deleted file mode 100644 index bbedbab..0000000 --- a/config/config.go +++ /dev/null @@ -1,90 +0,0 @@ -package config - -import ( - "bufio" - "log/slog" - "os" - "strings" - "time" - - "aidanwoods.dev/go-paseto" -) - -type App struct { - DataSource string - Security Security - AppInfo AppInfo -} - -type AppInfo struct { - Version string -} - -type Security struct { - AsymmetricKey paseto.V4AsymmetricSecretKey - PublicKey paseto.V4AsymmetricPublicKey - Duration time.Duration -} - -func New(version string) *App { - var err error - - err = loadEnvFile() - if err != nil { - slog.Error("error loading env file", "error", err) - panic(err) - } - - var durationTime time.Duration - var ak paseto.V4AsymmetricSecretKey - - ak, err = paseto.NewV4AsymmetricSecretKeyFromHex(os.Getenv("ASYMMETRICKEY")) - if err != nil { - ak = paseto.NewV4AsymmetricSecretKey() - } - pk := ak.Public() - - duration := os.Getenv("DURATION") - if duration != "" { - durationTime, err = time.ParseDuration(duration) - if err != nil { - durationTime = time.Hour * 24 * 7 - } - } - - return &App{ - DataSource: os.Getenv("DATASOURCE"), - Security: Security{ - AsymmetricKey: ak, - PublicKey: pk, - Duration: durationTime, - }, - AppInfo: AppInfo{ - Version: version, - }, - } -} - -func loadEnvFile() error { - file, err := os.Open(".env") - if err != nil { - return err - } - defer file.Close() - - scanner := bufio.NewScanner(file) - for scanner.Scan() { - line := scanner.Text() - if len(line) == 0 || strings.HasPrefix(line, "#") { - continue - } - parts := strings.SplitN(line, "=", 2) - if len(parts) != 2 { - continue - } - key := strings.TrimSpace(parts[0]) - value := strings.TrimSpace(parts[1]) - os.Setenv(key, value) - } - return scanner.Err() -} diff --git a/go.mod b/go.mod index 2bbd629..76a1e9b 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module gopher-toolbox go 1.23.2 require ( + github.com/golang-migrate/migrate/v4 v4.18.1 github.com/jackc/pgconn v1.14.3 github.com/jackc/pgx/v5 v5.7.1 github.com/xuri/excelize/v2 v2.9.0 @@ -10,7 +11,10 @@ require ( require ( aidanwoods.dev/go-result v0.1.0 // indirect - github.com/stretchr/testify v1.9.0 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/lib/pq v1.10.9 // indirect + go.uber.org/atomic v1.7.0 // indirect ) require ( diff --git a/go.sum b/go.sum index a195734..84e1090 100644 --- a/go.sum +++ b/go.sum @@ -2,9 +2,38 @@ aidanwoods.dev/go-paseto v1.5.2 h1:9aKbCQQUeHCqis9Y6WPpJpM9MhEOEI5XBmfTkFMSF/o= aidanwoods.dev/go-paseto v1.5.2/go.mod h1:7eEJZ98h2wFi5mavCcbKfv9h86oQwut4fLVeL/UBFnw= aidanwoods.dev/go-result v0.1.0 h1:y/BMIRX6q3HwaorX1Wzrjo3WUdiYeyWbvGe18hKS3K8= aidanwoods.dev/go-result v0.1.0/go.mod h1:yridkWghM7AXSFA6wzx0IbsurIm1Lhuro3rYef8FBHM= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= 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/dhui/dktest v0.4.3 h1:wquqUxAFdcUgabAVLvSCOKOlag5cIZuaOjYIBOWdsR0= +github.com/dhui/dktest v0.4.3/go.mod h1:zNK8IwktWzQRm6I/l2Wjp7MakiyaFWv4G1hjmodmMTs= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/docker/docker v27.2.0+incompatible h1:Rk9nIVdfH3+Vz4cyI/uhbINhEZ/oLmc+CBXmH6fbNk4= +github.com/docker/docker v27.2.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= +github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-migrate/migrate/v4 v4.18.1 h1:JML/k+t4tpHCpQTCAD62Nu43NUFzHY4CV3uAuvHGC+Y= +github.com/golang-migrate/migrate/v4 v4.18.1/go.mod h1:HAX6m3sQgcdO81tdjn5exv20+3Kb13cmGli1hrD6hks= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8= github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= @@ -24,8 +53,22 @@ github.com/jackc/pgx/v5 v5.7.1 h1:x7SYsPBYDkHDksogeSmZZ5xzThcTgRz++I5E+ePFUcs= github.com/jackc/pgx/v5 v5.7.1/go.mod h1:e7O26IywZZ+naJtWWos6i6fvWK+29etgITqrqHLfoZA= 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/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= +github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= +github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 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/richardlehane/mscfb v1.0.4 h1:WULscsljNPConisD5hR0+OyZjwK46Pfyr6mPu5ZawpM= @@ -45,6 +88,16 @@ github.com/xuri/excelize/v2 v2.9.0 h1:1tgOaEq92IOEumR1/JfYS/eR0KHOCsRv/rYXXh6YJQ github.com/xuri/excelize/v2 v2.9.0/go.mod h1:uqey4QBZ9gdMeWApPLdhm9x+9o2lq4iVmjiLfBS5hdE= github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7 h1:hPVCafDV85blFTabnqKgNhDCkJX25eik94Si9cTER4A= github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8= +go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw= +go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8= +go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc= +go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8= +go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4= +go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ= +go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ=