diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..2b2e943 --- /dev/null +++ b/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2021 Harran Ali +Copyright (c) 2024 Zeni Kim + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/config/cache.go b/config/cache.go new file mode 100644 index 0000000..cc7549a --- /dev/null +++ b/config/cache.go @@ -0,0 +1,21 @@ +// Copyright 2023 Harran Ali . All rights reserved. +// Copyright (c) 2024 Zeni Kim +// Use of this source code is governed by MIT-style +// license that can be found in the LICENSE file. + +package config + +import "git.smarteching.com/goffee/core" + +// Retrieve the main config for the cache +func GetCacheConfig() core.CacheConfig { + //##################################### + //# Main configuration for cache ##### + //##################################### + + return core.CacheConfig{ + // For enabling and disabling the cache + // set to true to enable it, set to false to disable + EnableCache: true, + } +} diff --git a/config/dotenvfile.go b/config/dotenvfile.go new file mode 100644 index 0000000..cdd969f --- /dev/null +++ b/config/dotenvfile.go @@ -0,0 +1,23 @@ +// Copyright 2023 Harran Ali . All rights reserved. +// Copyright (c) 2024 Zeni Kim +// Use of this source code is governed by MIT-style +// license that can be found in the LICENSE file. + +package config + +import "git.smarteching.com/goffee/core" + +// Retrieve the main config for controlling the .env file +func GetEnvFileConfig() core.EnvFileConfig { + //######################################################### + //# Main configuration for controlling the .env file ##### + //######################################################### + + return core.EnvFileConfig{ + // Set to true to read the environment variables from the .env file and then + // inject them into the os environment, please keep in mind this will override any + // variables previously set in the os envrionment + // set to false to ignore the .env file + UseDotEnvFile: true, + } +} diff --git a/config/gorm.go b/config/gorm.go new file mode 100644 index 0000000..9a3e888 --- /dev/null +++ b/config/gorm.go @@ -0,0 +1,21 @@ +// Copyright 2023 Harran Ali . All rights reserved. +// Copyright (c) 2024 Zeni Kim +// Use of this source code is governed by MIT-style +// license that can be found in the LICENSE file. + +package config + +import "git.smarteching.com/goffee/core" + +// Retrieve the main config for the GORM +func GetGormConfig() core.GormConfig { + //##################################### + //# Main configuration for GORM ##### + //##################################### + + return core.GormConfig{ + // For enabling and disabling the GORM + // set to true to enable it, set to false to disable + EnableGorm: false, + } +} diff --git a/config/request.go b/config/request.go new file mode 100644 index 0000000..2b64151 --- /dev/null +++ b/config/request.go @@ -0,0 +1,20 @@ +// Copyright 2023 Harran Ali . All rights reserved. +// Copyright (c) 2024 Zeni Kim +// Use of this source code is governed by MIT-style +// license that can be found in the LICENSE file. + +package config + +import "git.smarteching.com/goffee/core" + +// Retrieve the main config for the HTTP request +func GetRequestConfig() core.RequestConfig { + //##################################### + //# Main configuration for gorm ##### + //##################################### + + return core.RequestConfig{ + // Set the max file upload size + MaxUploadFileSize: 40000000, // ~40MB + } +} diff --git a/events/.gitkeep b/events/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/events/event-names.go b/events/event-names.go new file mode 100644 index 0000000..0e5f431 --- /dev/null +++ b/events/event-names.go @@ -0,0 +1,6 @@ +package events + +// event names +const USER_REGISTERED = "user-registered" +const USER_PASSWORD_RESET_REQUESTED = "user-password-reset-requested" +const PASSWORD_CHANGED = "password-changed" diff --git a/events/eventjobs/.gitkeep b/events/eventjobs/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/events/eventjobs/send-password-changed-email.go b/events/eventjobs/send-password-changed-email.go new file mode 100644 index 0000000..1885180 --- /dev/null +++ b/events/eventjobs/send-password-changed-email.go @@ -0,0 +1,31 @@ +package eventjobs + +import ( + "fmt" + + "git.smarteching.com/goffee/core" + "git.smarteching.com/goffee/cup/models" +) + +var SendPasswordChangedEmail core.EventJob = func(event *core.Event, c *core.Context) { + go func() { + mailer := c.GetMailer() + logger := c.GetLogger() + + user, ok := event.Payload["user"].(models.User) + if !ok { + logger.Error("[SendPasswordChangedEmail job] invalid user") + return + } + mailer.SetFrom(core.EmailAddress{Name: "Goffee", Address: "mail@example.com"}) + mailer.SetTo([]core.EmailAddress{ + { + Name: user.Name, Address: user.Email, + }, + }) + mailer.SetSubject("Password Changed") + body := fmt.Sprintf("Hi %v, \nYour password have been changed. \nThanks.", user.Name) + mailer.SetPlainTextBody(body) + mailer.Send() + }() +} diff --git a/events/eventjobs/send-reset-password-email.go b/events/eventjobs/send-reset-password-email.go new file mode 100644 index 0000000..24c1b6e --- /dev/null +++ b/events/eventjobs/send-reset-password-email.go @@ -0,0 +1,38 @@ +package eventjobs + +import ( + "fmt" + "os" + + "git.smarteching.com/goffee/core" + "git.smarteching.com/goffee/cup/models" +) + +var SendResetPasswordEmail core.EventJob = func(event *core.Event, c *core.Context) { + go func() { + mailer := c.GetMailer() + logger := c.GetLogger() + + user, ok := event.Payload["user"].(models.User) + if !ok { + logger.Error("[SendResetPasswordEmail job] invalid user") + return + } + mailer.SetFrom(core.EmailAddress{Name: "Goffee", Address: "mail@example.com"}) + mailer.SetTo([]core.EmailAddress{ + { + Name: user.Name, Address: user.Email, + }, + }) + + mailer.SetSubject("Reset Password Link") + hostname, err := os.Hostname() + if err != nil { + c.GetLogger().Error(err.Error()) + } + resetPasswordLink := fmt.Sprintf("%v/reset-password/code/%v", hostname, c.CastToString(event.Payload["code"])) + body := fmt.Sprintf("Hi %v,
Click the link below to reset your password
Reset Password.
Thanks.", user.Name, resetPasswordLink) + mailer.SetHTMLBody(body) + mailer.Send() + }() +} diff --git a/events/eventjobs/send-welcome-email.go b/events/eventjobs/send-welcome-email.go new file mode 100644 index 0000000..a638dca --- /dev/null +++ b/events/eventjobs/send-welcome-email.go @@ -0,0 +1,31 @@ +package eventjobs + +import ( + "fmt" + + "git.smarteching.com/goffee/core" + "git.smarteching.com/goffee/cup/models" +) + +var SendWelcomeEmail core.EventJob = func(event *core.Event, c *core.Context) { + go func() { + mailer := c.GetMailer() + logger := c.GetLogger() + + user, ok := event.Payload["user"].(models.User) + if !ok { + logger.Error("[SenEmail job] invalid user") + return + } + mailer.SetFrom(core.EmailAddress{Name: "Goffee", Address: "mail@example.com"}) + mailer.SetTo([]core.EmailAddress{ + { + Name: user.Name, Address: user.Email, + }, + }) + mailer.SetSubject("Welcome To Goffee") + body := fmt.Sprintf("Hi %v, \nWelcome to Goffe \nYour account have been created successfully. \nThanks.", user.Name) + mailer.SetPlainTextBody(body) + mailer.Send() + }() +} diff --git a/events/eventjobs/test-job.go b/events/eventjobs/test-job.go new file mode 100644 index 0000000..e4d0d8c --- /dev/null +++ b/events/eventjobs/test-job.go @@ -0,0 +1,9 @@ +package eventjobs + +import ( + "git.smarteching.com/goffee/core" +) + +var TestEvent core.EventJob = func(event *core.Event, c *core.Context) { + c.GetLogger().Info("hello from event test job") +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..363db07 --- /dev/null +++ b/go.mod @@ -0,0 +1,52 @@ +module git.smarteching.com/goffee/cup + +replace ( + git.smarteching.com/goffee/cup/config => ./config + git.smarteching.com/goffee/cup/handlers => ./handlers + git.smarteching.com/goffee/cup/middlewares => ./middlewares + git.smarteching.com/goffee/cup/models => ./models +) + +go 1.20 + +require ( + git.smarteching.com/goffee/core v1.7.2 + github.com/google/uuid v1.5.0 + github.com/joho/godotenv v1.5.1 + github.com/julienschmidt/httprouter v1.3.0 + gorm.io/gorm v1.25.5 +) + +require ( + github.com/SparkPost/gosparkpost v0.2.0 // indirect + github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/go-chi/chi/v5 v5.0.11 // indirect + github.com/go-ozzo/ozzo-validation v3.6.0+incompatible // indirect + github.com/go-sql-driver/mysql v1.7.1 // indirect + github.com/golang-jwt/jwt/v5 v5.2.0 // indirect + github.com/harranali/mailing v1.2.0 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 // indirect + github.com/jackc/pgx/v5 v5.5.1 // indirect + github.com/jackc/puddle/v2 v2.2.1 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/mailgun/mailgun-go/v4 v4.12.0 // indirect + github.com/mattn/go-sqlite3 v1.14.19 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/redis/go-redis/v9 v9.3.1 // indirect + github.com/sendgrid/rest v2.6.9+incompatible // indirect + github.com/sendgrid/sendgrid-go v3.14.0+incompatible // indirect + golang.org/x/crypto v0.17.0 // indirect + golang.org/x/net v0.19.0 // indirect + golang.org/x/sync v0.1.0 // indirect + golang.org/x/text v0.14.0 // indirect + gorm.io/driver/mysql v1.5.2 // indirect + gorm.io/driver/postgres v1.5.4 // indirect + gorm.io/driver/sqlite v1.5.4 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..b25d26d --- /dev/null +++ b/go.sum @@ -0,0 +1,119 @@ +github.com/SparkPost/gosparkpost v0.2.0 h1:yzhHQT7cE+rqzd5tANNC74j+2x3lrPznqPJrxC1yR8s= +github.com/SparkPost/gosparkpost v0.2.0/go.mod h1:S9WKcGeou7cbPpx0kTIgo8Q69WZvUmVeVzbD+djalJ4= +github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= +github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= +github.com/brianvoe/gofakeit/v6 v6.21.0 h1:tNkm9yxEbpuPK8Bx39tT4sSc5i9SUGiciLdNix+VDQY= +github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= +github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= +github.com/buger/jsonparser v1.0.0/go.mod h1:tgcrVJ81GPSF0mz+0nu1Xaz0fazGPrmmJfJtxjbHhUQ= +github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a/go.mod h1:2GxOXOlEPAMFPfp014mK1SWq8G8BN8o7/dfYqJrVGn8= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +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/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/facebookgo/ensure v0.0.0-20160127193407-b4ab57deab51 h1:0JZ+dUmQeA8IIVUMzysrX4/AKuQwWhV2dYQuPZdvdSQ= +github.com/facebookgo/ensure v0.0.0-20160127193407-b4ab57deab51/go.mod h1:Yg+htXGokKKdzcwhuNDwVvN+uBxDGXJ7G/VN1d8fa64= +github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 h1:JWuenKqqX8nojtoVVWjGfOF9635RETekkoH6Cc9SX0A= +github.com/facebookgo/stack v0.0.0-20160209184415-751773369052/go.mod h1:UbMTZqLaRiH3MsBH8va0n7s1pQYcu3uTb8G4tygF4Zg= +github.com/facebookgo/subset v0.0.0-20150612182917-8dac2c3c4870 h1:E2s37DuLxFhQDg5gKsWoLBOB0n+ZW8s599zru8FJ2/Y= +github.com/facebookgo/subset v0.0.0-20150612182917-8dac2c3c4870/go.mod h1:5tD+neXqOorC30/tWg0LCSkrqj/AR6gu8yY8/fpw1q0= +github.com/go-chi/chi/v5 v5.0.8/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/go-chi/chi/v5 v5.0.11 h1:BnpYbFZ3T3S1WMpD79r7R5ThWX40TaFB7L31Y8xqSwA= +github.com/go-chi/chi/v5 v5.0.11/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/go-ozzo/ozzo-validation v3.6.0+incompatible h1:msy24VGS42fKO9K1vLz82/GeYW1cILu7Nuuj1N3BBkE= +github.com/go-ozzo/ozzo-validation v3.6.0+incompatible/go.mod h1:gsEKFIVnabGBt6mXmxK0MoFy+cZoTJY6mu5Ll3LVLBU= +github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= +github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI= +github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= +github.com/go-test/deep v1.0.2/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= +github.com/gocondor/core v1.7.2 h1:fL3UoR1k0zfrPpiOTK0rY2r/IrrEQ7elMPv9XsNOz3s= +github.com/gocondor/core v1.7.2/go.mod h1:BDg37tlLHYtjgW/5zgpRi/Ted8GLwE/jZesCRntbSiQ= +github.com/gogs/chardet v0.0.0-20150115103509-2404f7772561/go.mod h1:Pcatq5tYkCW2Q6yrR2VRHlbHpZ/R4/7qyL1TCF7vl14= +github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw= +github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= +github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/harranali/mailing v1.2.0 h1:ihIyJwB8hyRVcdk+v465wk1PHMrSrgJqo/kMd+gZClY= +github.com/harranali/mailing v1.2.0/go.mod h1:4a5N3yG98pZKluMpmcYlTtll7bisvOfGQEMIng3VQk4= +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-20231201235250-de7065d80cb9 h1:L0QtFUgDarD7Fpv9jeVMgy/+Ec0mtnmYuImjTz6dtDA= +github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.5.1 h1:5I9etrGkLrN+2XPCsi6XLlV5DITbSL/xBZdmAxFcXPI= +github.com/jackc/pgx/v5 v5.5.1/go.mod h1:Ig06C2Vu0t5qXC60W8sqIthScaEnFvojjj9dSljmHRA= +github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= +github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/jaytaylor/html2text v0.0.0-20190408195923-01ec452cbe43/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk= +github.com/jhillyerd/enmime v0.8.0/go.mod h1:MBHs3ugk03NGjMM6PuRynlKf+HA5eSillZ+TRCm73AE= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +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.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +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/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/mailgun/mailgun-go/v4 v4.12.0 h1:TtuQCgqSp4cB6swPxP5VF/u4JeeBIAjTdpuQ+4Usd/w= +github.com/mailgun/mailgun-go/v4 v4.12.0/go.mod h1:L9s941Lgk7iB3TgywTPz074pK2Ekkg4kgbnAaAyJ2z8= +github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/mattn/go-sqlite3 v1.14.19 h1:fhGleo2h1p8tVChob4I9HpmVFIAkKGpiukdrgQbWfGI= +github.com/mattn/go-sqlite3 v1.14.19/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= +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 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +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/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +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/redis/go-redis/v9 v9.3.1 h1:KqdY8U+3X6z+iACvumCNxnoluToB+9Me+TvyFa21Mds= +github.com/redis/go-redis/v9 v9.3.1/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M= +github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU= +github.com/sendgrid/rest v2.6.9+incompatible h1:1EyIcsNdn9KIisLW50MKwmSRSK+ekueiEMJ7NEoxJo0= +github.com/sendgrid/rest v2.6.9+incompatible/go.mod h1:kXX7q3jZtJXK5c5qK83bSGMdV6tsOE70KbHoqJls4lE= +github.com/sendgrid/sendgrid-go v3.14.0+incompatible h1:KDSasSTktAqMJCYClHVE94Fcif2i7P7wzISv1sU6DUA= +github.com/sendgrid/sendgrid-go v3.14.0+incompatible/go.mod h1:QRQt+LX/NmgVEvmdRw0VT/QgUn499+iza2FnDca9fg8= +github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02nZ62WenDCkgHFerpIOmW0iT7GKmXM= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +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.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= +golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= +golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= +golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +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= +gorm.io/driver/mysql v1.5.2 h1:QC2HRskSE75wBuOxe0+iCkyJZ+RqpudsQtqkp+IMuXs= +gorm.io/driver/mysql v1.5.2/go.mod h1:pQLhh1Ut/WUAySdTHwBpBv6+JKcj+ua4ZFx1QQTBzb8= +gorm.io/driver/postgres v1.5.4 h1:Iyrp9Meh3GmbSuyIAGyjkN+n9K+GHX9b9MqsTL4EJCo= +gorm.io/driver/postgres v1.5.4/go.mod h1:Bgo89+h0CRcdA33Y6frlaHHVuTdOf87pmyzwW9C/BH0= +gorm.io/driver/sqlite v1.5.4 h1:IqXwXi8M/ZlPzH/947tn5uik3aYQslP9BVveoax0nV0= +gorm.io/driver/sqlite v1.5.4/go.mod h1:qxAuCol+2r6PannQDpOP1FP6ag3mKi4esLnB/jHed+4= +gorm.io/gorm v1.25.2-0.20230530020048-26663ab9bf55/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= +gorm.io/gorm v1.25.5 h1:zR9lOiiYf09VNh5Q1gphfyia1JpiClIWG9hQaxB/mls= +gorm.io/gorm v1.25.5/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= diff --git a/handlers/authentication.go b/handlers/authentication.go new file mode 100644 index 0000000..5dcd00c --- /dev/null +++ b/handlers/authentication.go @@ -0,0 +1,391 @@ +// Copyright 2023 Harran Ali . All rights reserved. +// Copyright (c) 2024 Zeni Kim +// Use of this source code is governed by MIT-style +// license that can be found in the LICENSE file. + +package handlers + +import ( + "encoding/json" + "errors" + "net/http" + "strconv" + "strings" + "time" + + "git.smarteching.com/goffee/core" + "git.smarteching.com/goffee/cup/events" + "git.smarteching.com/goffee/cup/models" + "git.smarteching.com/goffee/cup/utils" + "github.com/google/uuid" + "gorm.io/gorm" +) + +func Signup(c *core.Context) *core.Response { + name := c.GetRequestParam("name") + email := c.GetRequestParam("email") + password := c.GetRequestParam("password") + // check if email exists + var user models.User + res := c.GetGorm().Where("email = ?", c.CastToString(email)).First(&user) + if res.Error != nil && !errors.Is(res.Error, gorm.ErrRecordNotFound) { + c.GetLogger().Error(res.Error.Error()) + return c.Response.SetStatusCode(http.StatusInternalServerError).Json(c.MapToJson(map[string]string{ + "message": "internal error", + })) + } + if res.Error == nil { + return c.Response.SetStatusCode(http.StatusUnprocessableEntity).Json(c.MapToJson(map[string]string{ + "message": "email already exists in the database", + })) + } + + // validation data + data := map[string]interface{}{ + "name": name, + "email": email, + "password": password, + } + // validation rules + rules := map[string]interface{}{ + "name": "required|alphaNumeric", + "email": "required|email", + "password": "required|length:6,10", + } + // validate + v := c.GetValidator().Validate(data, rules) + if v.Failed() { + c.GetLogger().Error(v.GetErrorMessagesJson()) + return c.Response.SetStatusCode(http.StatusUnprocessableEntity).Json(v.GetErrorMessagesJson()) + } + + //hash the password + passwordHashed, err := c.GetHashing().HashPassword(c.CastToString(password)) + if err != nil { + c.GetLogger().Error(err.Error()) + return c.Response.SetStatusCode(http.StatusUnprocessableEntity).Json(c.MapToJson(map[string]interface{}{ + "message": err.Error(), + })) + } + // store the record in db + user = models.User{ + Name: c.CastToString(name), + Email: c.CastToString(email), + Password: passwordHashed, + } + res = c.GetGorm().Create(&user) + if res.Error != nil { + c.GetLogger().Error(res.Error.Error()) + return c.Response.SetStatusCode(http.StatusInternalServerError).Json(c.MapToJson(map[string]string{ + "message": res.Error.Error(), + })) + } + + token, err := c.GetJWT().GenerateToken(map[string]interface{}{ + "userID": user.ID, + }) + + if err != nil { + c.GetLogger().Error(err.Error()) + return c.Response.SetStatusCode(http.StatusInternalServerError).Json(c.MapToJson(map[string]string{ + "message": "internal server error", + })) + } + + // cache the token + userAgent := c.GetUserAgent() + hashedCacheKey := utils.CreateAuthTokenHashedCacheKey(user.ID, userAgent) + err = c.GetCache().Set(hashedCacheKey, token) + if err != nil { + c.GetLogger().Error(err.Error()) + return c.Response.SetStatusCode(http.StatusInternalServerError).Json(c.MapToJson(map[string]interface{}{ + "message": "internal server error", + })) + } + + // fire user registered event + err = c.GetEventsManager().Fire(&core.Event{Name: events.USER_REGISTERED, Payload: map[string]interface{}{ + "user": user, + }}) + if err != nil { + c.GetLogger().Error(err.Error()) + return c.Response.SetStatusCode(http.StatusInternalServerError).Json(c.MapToJson(map[string]string{ + "message": "internal server error", + })) + } + + return c.Response.Json(c.MapToJson(map[string]string{ + "token": token, + })) +} + +func Signin(c *core.Context) *core.Response { + email := c.GetRequestParam("email") + password := c.GetRequestParam("password") + + data := map[string]interface{}{ + "email": email, + "password": password, + } + rules := map[string]interface{}{ + "email": "required|email", + "password": "required", + } + v := c.GetValidator().Validate(data, rules) + + if v.Failed() { + c.GetLogger().Error(v.GetErrorMessagesJson()) + return c.Response.SetStatusCode(http.StatusUnprocessableEntity).Json(v.GetErrorMessagesJson()) + } + + var user models.User + res := c.GetGorm().Where("email = ?", c.CastToString(email)).First(&user) + if res.Error != nil && !errors.Is(res.Error, gorm.ErrRecordNotFound) { + c.GetLogger().Error(res.Error.Error()) + return c.Response.SetStatusCode(http.StatusInternalServerError).Json(c.MapToJson(map[string]string{ + "message": "internal server error", + })) + } + + if res.Error != nil && errors.Is(res.Error, gorm.ErrRecordNotFound) { + return c.Response.SetStatusCode(http.StatusUnprocessableEntity).Json(c.MapToJson(map[string]string{ + "message": "invalid email or password", + })) + } + + ok, err := c.GetHashing().CheckPasswordHash(user.Password, c.CastToString(password)) + if err != nil { + c.GetLogger().Error(err.Error()) + return c.Response.SetStatusCode(http.StatusInternalServerError).Json(c.MapToJson(map[string]string{ + "message": err.Error(), + })) + } + + if !ok { + return c.Response.SetStatusCode(http.StatusUnprocessableEntity).Json(c.MapToJson(map[string]string{ + "message": "invalid email or password", + })) + } + + token, err := c.GetJWT().GenerateToken(map[string]interface{}{ + "userID": user.ID, + }) + + if err != nil { + c.GetLogger().Error(err.Error()) + return c.Response.SetStatusCode(http.StatusInternalServerError).Json(c.MapToJson(map[string]string{ + "message": "internal server error", + })) + } + // cache the token + userAgent := c.GetUserAgent() + hashedCacheKey := utils.CreateAuthTokenHashedCacheKey(user.ID, userAgent) + err = c.GetCache().Set(hashedCacheKey, token) + if err != nil { + c.GetLogger().Error(err.Error()) + return c.Response.SetStatusCode(http.StatusInternalServerError).Json(c.MapToJson(map[string]interface{}{ + "message": "internal server error", + })) + } + + return c.Response.Json(c.MapToJson(map[string]string{ + "token": token, + })) +} + +func ResetPasswordRequest(c *core.Context) *core.Response { + email := c.GetRequestParam("email") + + // validation data + data := map[string]interface{}{ + "email": email, + } + // validation rules + rules := map[string]interface{}{ + "email": "required|email", + } + // validate + v := c.GetValidator().Validate(data, rules) + if v.Failed() { + c.GetLogger().Error(v.GetErrorMessagesJson()) + return c.Response.SetStatusCode(http.StatusUnprocessableEntity).Json(v.GetErrorMessagesJson()) + } + + // check email in the database + var user models.User + res := c.GetGorm().Where("email = ?", c.CastToString(email)).First(&user) + if res.Error != nil && !errors.Is(res.Error, gorm.ErrRecordNotFound) { + c.GetLogger().Error(res.Error.Error()) + return c.Response.SetStatusCode(http.StatusInternalServerError).Json(c.MapToJson(map[string]string{ + "message": "internal server error", + })) + } + if res.Error != nil && errors.Is(res.Error, gorm.ErrRecordNotFound) { + return c.Response.SetStatusCode(http.StatusUnprocessableEntity).Json(c.MapToJson(map[string]string{ + "message": "email not found in our database", + })) + } + + // generate the link + expiresAt := time.Now().Add(time.Hour * 3).Unix() + linkCodeData := map[string]string{ + "userID": c.CastToString(user.ID), + "expiresAt": c.CastToString(expiresAt), + } + code := uuid.NewString() + c.GetCache().SetWithExpiration(code, c.MapToJson(linkCodeData), time.Hour*3) + err := c.GetEventsManager().Fire(&core.Event{Name: events.USER_PASSWORD_RESET_REQUESTED, Payload: map[string]interface{}{ + "user": user, + "code": code, + }}) + if err != nil { + c.GetLogger().Error(err.Error()) + return c.Response.SetStatusCode(http.StatusInternalServerError).Json(c.MapToJson(map[string]string{ + "message": "internal server error", + })) + } + + return c.Response.Json(c.MapToJson(map[string]string{ + "message": "reset password email sent successfully", + })) +} + +func SetNewPassword(c *core.Context) *core.Response { + urlCode := c.CastToString(c.GetPathParam("code")) + linkCodeDataStr, err := c.GetCache().Get(urlCode) + if err != nil { + c.GetLogger().Error(err.Error()) + return c.Response.SetStatusCode(http.StatusUnprocessableEntity).Json(c.MapToJson(map[string]string{ + "message": "invalid link", + })) + } + var linkCode map[string]interface{} + json.Unmarshal([]byte(linkCodeDataStr), &linkCode) + expiresAtUnix, err := strconv.ParseInt(c.CastToString(linkCode["expiresAt"]), 10, 64) + if err != nil { + c.GetLogger().Error(err.Error()) + return c.Response.SetStatusCode(http.StatusUnprocessableEntity).Json(c.MapToJson(map[string]string{ + "message": "invalid link", + })) + } + expiresAt := time.Unix(expiresAtUnix, 0) + if time.Now().After(expiresAt) { + return c.Response.SetStatusCode(http.StatusUnprocessableEntity).Json(c.MapToJson(map[string]string{ + "message": "invalid link", + })) + } + userID, err := strconv.ParseUint(c.CastToString(linkCode["userID"]), 10, 64) + if err != nil { + c.GetLogger().Error(err.Error()) + return c.Response.SetStatusCode(http.StatusUnprocessableEntity).Json(c.MapToJson(map[string]string{ + "message": "invalid link", + })) + } + + oldPassword := c.CastToString(c.GetRequestParam("old_password")) + newPassword := c.CastToString(c.GetRequestParam("new_password")) + newPasswordConfirm := c.CastToString(c.GetRequestParam("new_password_confirm")) + + // validation data + data := map[string]interface{}{ + "old_password": oldPassword, + "new_password": newPassword, + "new_password_confirm": newPasswordConfirm, + } + // validation rules + rules := map[string]interface{}{ + "old_password": "required|length:6,10", + "new_password": "required|length:6,10", + "new_password_confirm": "required|length:6,10", + } + // validate + v := c.GetValidator().Validate(data, rules) + if v.Failed() { + c.GetLogger().Error(v.GetErrorMessagesJson()) + return c.Response.SetStatusCode(http.StatusUnprocessableEntity).Json(v.GetErrorMessagesJson()) + } + + var user models.User + res := c.GetGorm().Where("id = ?", userID).First(&user) + if res.Error != nil { + c.GetLogger().Error(res.Error.Error()) + return c.Response.SetStatusCode(http.StatusUnprocessableEntity).Json(c.MapToJson(map[string]string{ + "message": "invalid link", + })) + } + + SamePassword, err := c.GetHashing().CheckPasswordHash(user.Password, oldPassword) + if err != nil { + c.GetLogger().Error(err.Error()) + return c.Response.SetStatusCode(http.StatusUnprocessableEntity).Json(c.MapToJson(map[string]string{ + "message": "invalid link", + })) + } + + if !SamePassword { + return c.Response.SetStatusCode(http.StatusUnprocessableEntity).Json(c.MapToJson(map[string]string{ + "message": "the old password is incorrect", + })) + } + + if newPassword != newPasswordConfirm { + return c.Response.SetStatusCode(http.StatusUnprocessableEntity).Json(c.MapToJson(map[string]string{ + "message": "new password does not match new password confirmation", + })) + } + + hashedNewPassword, err := c.GetHashing().HashPassword(newPassword) + if err != nil { + c.GetLogger().Error(err.Error()) + return c.Response.SetStatusCode(http.StatusUnprocessableEntity).Json(c.MapToJson(map[string]string{ + "message": "invalid password", + })) + } + + user.Password = hashedNewPassword + c.GetGorm().Save(&user) + + err = c.GetEventsManager().Fire(&core.Event{Name: events.PASSWORD_CHANGED, Payload: map[string]interface{}{ + "user": user, + }}) + + if err != nil { + c.GetLogger().Error(err.Error()) + return c.Response.SetStatusCode(http.StatusInternalServerError).Json(c.MapToJson(map[string]string{ + "message": "internal server error", + })) + } + + return c.Response.Json(c.MapToJson(map[string]string{ + "message": "password changed successfully", + })) +} + +func Signout(c *core.Context) *core.Response { + tokenRaw := c.GetHeader("Authorization") + token := strings.TrimSpace(strings.Replace(tokenRaw, "Bearer", "", 1)) + if token == "" { + return c.Response.SetStatusCode(http.StatusUnauthorized).Json(c.MapToJson(map[string]interface{}{ + "message": "unauthorized", + })) + } + payload, err := c.GetJWT().DecodeToken(token) + if err != nil { + return c.Response.SetStatusCode(http.StatusUnauthorized).Json(c.MapToJson(map[string]interface{}{ + "message": "unauthorized", + })) + } + userAgent := c.GetUserAgent() + hashedCacheKey := utils.CreateAuthTokenHashedCacheKey(uint(c.CastToInt(payload["userID"])), userAgent) + + err = c.GetCache().Delete(hashedCacheKey) + if err != nil { + return c.Response.SetStatusCode(http.StatusInternalServerError).Json(c.MapToJson(map[string]interface{}{ + "message": "internal error", + })) + } + + return c.Response.SetStatusCode(http.StatusOK).Json(c.MapToJson(map[string]interface{}{ + "message": "signed out successfully", + })) +} diff --git a/handlers/home.go b/handlers/home.go new file mode 100644 index 0000000..aac2ba8 --- /dev/null +++ b/handlers/home.go @@ -0,0 +1,22 @@ +// Copyright 2023 Harran Ali . All rights reserved. +// Copyright (c) 2024 Zeni Kim +// Use of this source code is governed by MIT-style +// license that can be found in the LICENSE file. + +package handlers + +import ( + "git.smarteching.com/goffee/core" +) + +// Show home page +func WelcomeHome(c *core.Context) *core.Response { + message := "{\"message\": \"Welcome to Goffee\"}" + return c.Response.Json(message) +} + +// Show dashboard +func WelcomeToDashboard(c *core.Context) *core.Response { + message := "{\"message\": \"Welcome to Dashboard\"}" + return c.Response.Json(message) +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..2d83130 --- /dev/null +++ b/main.go @@ -0,0 +1,53 @@ +// Copyright 2023 Harran Ali . All rights reserved. +// Copyright (c) 2024 Zeni Kim +// Use of this source code is governed by MIT-style +// license that can be found in the LICENSE file. + +package main + +import ( + "log" + "os" + "path" + + "git.smarteching.com/goffee/core" + "git.smarteching.com/goffee/core/env" + "git.smarteching.com/goffee/core/logger" + "git.smarteching.com/goffee/cup/config" + "github.com/joho/godotenv" + "github.com/julienschmidt/httprouter" +) + +// The main function +func main() { + app := core.New() + basePath, err := os.Getwd() + if err != nil { + log.Fatal("error getting current working dir") + } + app.SetBasePath(basePath) + app.MakeDirs("logs", "storage", "storage/sqlite", "tls") + // Handle the reading of the .env file + if config.GetEnvFileConfig().UseDotEnvFile { + envVars, err := godotenv.Read(".env") + if err != nil { + log.Fatal("Error loading .env file") + } + env.SetEnvVars(envVars) + } + // Handle the logs + app.SetLogsDriver(&logger.LogFileDriver{ + FilePath: path.Join(basePath, "logs/app.log"), + }) + app.SetRequestConfig(config.GetRequestConfig()) + app.SetGormConfig(config.GetGormConfig()) + app.SetCacheConfig(config.GetCacheConfig()) + app.Bootstrap() + registerGlobalMiddlewares() + registerRoutes() + registerEvents() + if config.GetGormConfig().EnableGorm == true { + RunAutoMigrations() + } + app.Run(httprouter.New()) +} diff --git a/middlewares/another-example-middleware.go.go b/middlewares/another-example-middleware.go.go new file mode 100644 index 0000000..8871f60 --- /dev/null +++ b/middlewares/another-example-middleware.go.go @@ -0,0 +1,18 @@ +// Copyright 2023 Harran Ali . All rights reserved. +// Copyright (c) 2024 Zeni Kim +// Use of this source code is governed by MIT-style +// license that can be found in the LICENSE file. + +package middlewares + +import ( + "fmt" + + "git.smarteching.com/goffee/core" +) + +// Another example middleware +var AnotherExampleMiddleware core.Middleware = func(c *core.Context) { + fmt.Println("another example middleware!") + c.Next() +} diff --git a/middlewares/auth-check.go b/middlewares/auth-check.go new file mode 100644 index 0000000..c2b16c5 --- /dev/null +++ b/middlewares/auth-check.go @@ -0,0 +1,69 @@ +package middlewares + +import ( + "errors" + "net/http" + "strings" + + "git.smarteching.com/goffee/core" + "git.smarteching.com/goffee/cup/models" + "git.smarteching.com/goffee/cup/utils" + "gorm.io/gorm" +) + +var AuthCheck core.Middleware = func(c *core.Context) { + tokenRaw := c.GetHeader("Authorization") + token := strings.TrimSpace(strings.Replace(tokenRaw, "Bearer", "", 1)) + if token == "" { + c.Response.SetStatusCode(http.StatusUnauthorized).Json(c.MapToJson(map[string]interface{}{ + "message": "unauthorized", + })).ForceSendResponse() + return + } + payload, err := c.GetJWT().DecodeToken(token) + if err != nil { + c.Response.SetStatusCode(http.StatusUnauthorized).Json(c.MapToJson(map[string]interface{}{ + "message": "unauthorized", + })).ForceSendResponse() + return + } + userAgent := c.GetUserAgent() + hashedCacheKey := utils.CreateAuthTokenHashedCacheKey(uint(c.CastToInt(payload["userID"])), userAgent) + + cachedToken, err := c.GetCache().Get(hashedCacheKey) + if err != nil { + // user signed out + c.Response.SetStatusCode(http.StatusUnauthorized).Json(c.MapToJson(map[string]interface{}{ + "message": "unauthorized", + })).ForceSendResponse() + return + } + if cachedToken != token { + // using old token replaced with new one after recent signin + c.Response.SetStatusCode(http.StatusUnauthorized).Json(c.MapToJson(map[string]interface{}{ + "message": "unauthorized", + })).ForceSendResponse() + return + } + + var user models.User + res := c.GetGorm().Where("id = ?", payload["userID"]).First(&user) + if res.Error != nil && !errors.Is(res.Error, gorm.ErrRecordNotFound) { + // error with the database + c.GetLogger().Error(res.Error.Error()) + c.Response.SetStatusCode(http.StatusInternalServerError).Json(c.MapToJson(map[string]interface{}{ + "message": "internal error", + })).ForceSendResponse() + return + } + + if res.Error != nil && errors.Is(res.Error, gorm.ErrRecordNotFound) { + // user record is not found (deleted) + c.Response.SetStatusCode(http.StatusUnauthorized).Json(c.MapToJson(map[string]interface{}{ + "message": "unauthorized", + })).ForceSendResponse() + return + } + + c.Next() +} diff --git a/middlewares/example-middleware.go b/middlewares/example-middleware.go new file mode 100644 index 0000000..d328b97 --- /dev/null +++ b/middlewares/example-middleware.go @@ -0,0 +1,18 @@ +// Copyright 2023 Harran Ali . All rights reserved. +// Copyright (c) 2024 Zeni Kim +// Use of this source code is governed by MIT-style +// license that can be found in the LICENSE file. + +package middlewares + +import ( + "fmt" + + "git.smarteching.com/goffee/core" +) + +// Example middleware +var ExampleMiddleware core.Middleware = func(c *core.Context) { + fmt.Println("example middleware!") + c.Next() +} diff --git a/models/user.go b/models/user.go new file mode 100644 index 0000000..c85a386 --- /dev/null +++ b/models/user.go @@ -0,0 +1,20 @@ +// Copyright 2023 Harran Ali . All rights reserved. +// Copyright (c) 2024 Zeni Kim +// Use of this source code is governed by MIT-style +// license that can be found in the LICENSE file. + +package models + +import "gorm.io/gorm" + +type User struct { + gorm.Model + Name string + Email string + Password string +} + +// Override the table name +func (User) TableName() string { + return "users" +} diff --git a/register-events.go b/register-events.go new file mode 100644 index 0000000..c74d45c --- /dev/null +++ b/register-events.go @@ -0,0 +1,26 @@ +// Copyright 2023 Harran Ali . All rights reserved. +// Copyright (c) 2024 Zeni Kim +// Use of this source code is governed by MIT-style +// license that can be found in the LICENSE file. + +package main + +import ( + "git.smarteching.com/goffee/core" + "git.smarteching.com/goffee/cup/events" + "git.smarteching.com/goffee/cup/events/eventjobs" +) + +// Register events +func registerEvents() { + eventsManager := core.ResolveEventsManager() + //######################################## + //# events registration ##### + //######################################## + + // register your event here ... + eventsManager.Register(events.USER_REGISTERED, eventjobs.SendWelcomeEmail) + eventsManager.Register(events.USER_REGISTERED, eventjobs.TestEvent) + eventsManager.Register(events.USER_PASSWORD_RESET_REQUESTED, eventjobs.SendResetPasswordEmail) + eventsManager.Register(events.PASSWORD_CHANGED, eventjobs.SendPasswordChangedEmail) +} diff --git a/register-global-middlewares.go b/register-global-middlewares.go new file mode 100644 index 0000000..6460798 --- /dev/null +++ b/register-global-middlewares.go @@ -0,0 +1,16 @@ +// Copyright 2023 Harran Ali . All rights reserved. +// Copyright (c) 2024 Zeni Kim +// Use of this source code is governed by MIT-style +// license that can be found in the LICENSE file. + +package main + +// Register middlewares globally +func registerGlobalMiddlewares() { + //######################################## + //# Global middlewares registration ##### + //######################################## + + // Register global middlewares here ... + // core.UseMiddleware(middlewares.AnotherExampleMiddleware) +} diff --git a/routes.go b/routes.go new file mode 100644 index 0000000..fd225d7 --- /dev/null +++ b/routes.go @@ -0,0 +1,29 @@ +// Copyright 2023 Harran Ali . All rights reserved. +// Copyright (c) 2024 Zeni Kim +// Use of this source code is governed by MIT-style +// license that can be found in the LICENSE file. + +package main + +import ( + "git.smarteching.com/goffee/core" + "git.smarteching.com/goffee/cup/handlers" +) + +// Register the app routes +func registerRoutes() { + router := core.ResolveRouter() + //############################# + //# App Routes ##### + //############################# + + // Define your routes here... + router.Get("/", handlers.WelcomeHome) + // Uncomment the lines below to enable authentication + // router.Post("/signup", handlers.Signup) + // router.Post("/signin", handlers.Signin) + // router.Post("/signout", handlers.Signout) + // router.Post("/reset-password", handlers.ResetPasswordRequest) + // router.Post("/reset-password/code/:code", handlers.SetNewPassword) + // router.Get("/dashboard", handlers.WelcomeToDashboard, middlewares.AuthCheck) +} diff --git a/run-auto-migrations.go b/run-auto-migrations.go new file mode 100644 index 0000000..8410637 --- /dev/null +++ b/run-auto-migrations.go @@ -0,0 +1,21 @@ +// Copyright 2023 Harran Ali . All rights reserved. +// Copyright (c) 2024 Zeni Kim +// Use of this source code is governed by MIT-style +// license that can be found in the LICENSE file. + +package main + +import ( + "git.smarteching.com/goffee/core" + "git.smarteching.com/goffee/cup/models" +) + +func RunAutoMigrations() { + db := core.ResolveGorm() + //############################## + //# Models auto migration ##### + //############################## + + // Add auto migrations for your models here... + db.AutoMigrate(&models.User{}) +} diff --git a/storage/.gitignore b/storage/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/storage/sqlite.db b/storage/sqlite.db new file mode 100644 index 0000000..b10cc17 Binary files /dev/null and b/storage/sqlite.db differ diff --git a/utils/helpers.go b/utils/helpers.go new file mode 100644 index 0000000..00525b7 --- /dev/null +++ b/utils/helpers.go @@ -0,0 +1,19 @@ +// Copyright 2023 Harran Ali . All rights reserved. +// Copyright (c) 2024 Zeni Kim +// Use of this source code is governed by MIT-style +// license that can be found in the LICENSE file. + +package utils + +import ( + "crypto/md5" + "fmt" +) + +// generate a hashed string to be used as key for caching auth jwt token +func CreateAuthTokenHashedCacheKey(userID uint, userAgent string) string { + cacheKey := fmt.Sprintf("userid:_%v_useragent:_%v_jwt_token", userID, userAgent) + hashedCacheKey := fmt.Sprintf("%v", fmt.Sprintf("%x", md5.Sum([]byte(cacheKey)))) + + return hashedCacheKey +}