web: move global module register to service
continuous-integration/drone the build is pending Details

This commit is contained in:
Geno 2021-07-19 17:59:08 +02:00
parent ffd77c3aab
commit 6af94245b6
18 changed files with 214 additions and 205 deletions

View File

@ -34,19 +34,17 @@ type Status struct {
// @Failure 400 {object} web.HTTPError // @Failure 400 {object} web.HTTPError
// @Failure 404 {object} web.HTTPError // @Failure 404 {object} web.HTTPError
// @Router /api/status [get] // @Router /api/status [get]
func init() { func Register(r *gin.Engine, ws *web.Service) {
web.ModuleRegister(func(r *gin.Engine, ws *web.Service) { r.GET("/api/status", func(c *gin.Context) {
r.GET("/api/status", func(c *gin.Context) { status := &Status{
status := &Status{ Version: VERSION,
Version: VERSION, Up: UP(),
Up: UP(), Extras: EXTRAS,
Extras: EXTRAS, }
} if !status.Up {
if !status.Up { c.JSON(http.StatusInternalServerError, status)
c.JSON(http.StatusInternalServerError, status) return
return }
} c.JSON(http.StatusOK, status)
c.JSON(http.StatusOK, status)
})
}) })
} }

View File

@ -11,7 +11,7 @@ import (
func TestAPIStatus(t *testing.T) { func TestAPIStatus(t *testing.T) {
assert := assert.New(t) assert := assert.New(t)
s, err := webtest.New() s, err := webtest.New(Register)
assert.NoError(err) assert.NoError(err)
defer s.Close() defer s.Close()
assert.NotNil(s) assert.NotNil(s)

View File

@ -27,51 +27,49 @@ type login struct {
// @Failure 500 {object} web.HTTPError // @Failure 500 {object} web.HTTPError
// @Router /api/v1/auth/login [post] // @Router /api/v1/auth/login [post]
// @Param body body login false "login" // @Param body body login false "login"
func init() { func apiLogin(r *gin.Engine, ws *web.Service) {
web.ModuleRegister(func(r *gin.Engine, ws *web.Service) { r.POST("/api/v1/auth/login", func(c *gin.Context) {
r.POST("/api/v1/auth/login", func(c *gin.Context) { var data login
var data login if err := c.BindJSON(&data); err != nil {
if err := c.BindJSON(&data); err != nil { c.JSON(http.StatusBadRequest, web.HTTPError{
c.JSON(http.StatusBadRequest, web.HTTPError{ Message: web.APIErrorInvalidRequestFormat,
Message: web.APIErrorInvalidRequestFormat, Error: err.Error(),
Error: err.Error(), })
}) return
return }
}
d := &User{} d := &User{}
if err := ws.DB.Where(map[string]interface{}{"username": data.Username}).First(d).Error; err != nil { if err := ws.DB.Where(map[string]interface{}{"username": data.Username}).First(d).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
c.JSON(http.StatusUnauthorized, web.HTTPError{
Message: APIErrorUserNotFound,
Error: err.Error(),
})
return
}
c.JSON(http.StatusInternalServerError, web.HTTPError{
Message: web.APIErrorInternalDatabase,
Error: err.Error(),
})
return
}
if !d.ValidatePassword(data.Password) {
c.JSON(http.StatusUnauthorized, web.HTTPError{ c.JSON(http.StatusUnauthorized, web.HTTPError{
Message: APIErrorIncorrectPassword, Message: APIErrorUserNotFound,
})
return
}
session := sessions.Default(c)
session.Set("user_id", d.ID.String())
if err := session.Save(); err != nil {
c.JSON(http.StatusBadRequest, web.HTTPError{
Message: APIErrorCreateSession,
Error: err.Error(), Error: err.Error(),
}) })
return return
} }
c.JSON(http.StatusInternalServerError, web.HTTPError{
Message: web.APIErrorInternalDatabase,
Error: err.Error(),
})
return
}
if !d.ValidatePassword(data.Password) {
c.JSON(http.StatusUnauthorized, web.HTTPError{
Message: APIErrorIncorrectPassword,
})
return
}
c.JSON(http.StatusOK, d) session := sessions.Default(c)
}) session.Set("user_id", d.ID.String())
if err := session.Save(); err != nil {
c.JSON(http.StatusBadRequest, web.HTTPError{
Message: APIErrorCreateSession,
Error: err.Error(),
})
return
}
c.JSON(http.StatusOK, d)
}) })
} }

View File

@ -12,7 +12,7 @@ import (
func TestAPILogin(t *testing.T) { func TestAPILogin(t *testing.T) {
assert := assert.New(t) assert := assert.New(t)
s, err := webtest.NewWithDBSetup(SetupMigration) s, err := webtest.NewWithDBSetup(apiLogin, SetupMigration)
assert.NoError(err) assert.NoError(err)
defer s.Close() defer s.Close()
assert.NotNil(s) assert.NotNil(s)

View File

@ -17,21 +17,19 @@ import (
// @Failure 500 {object} web.HTTPError // @Failure 500 {object} web.HTTPError
// @Router /api/v1/my/profil [delete] // @Router /api/v1/my/profil [delete]
// @Security ApiKeyAuth // @Security ApiKeyAuth
func init() { func apiMyDelete(r *gin.Engine, ws *web.Service) {
web.ModuleRegister(func(r *gin.Engine, ws *web.Service) { r.DELETE("/api/v1/my/profil", func(c *gin.Context) {
r.DELETE("/api/v1/my/profil", func(c *gin.Context) { id, ok := GetCurrentUserID(c)
id, ok := GetCurrentUserID(c) if !ok {
if !ok { return
return }
} if err := ws.DB.Delete(&User{ID: id}).Error; err != nil {
if err := ws.DB.Delete(&User{ID: id}).Error; err != nil { c.JSON(http.StatusInternalServerError, web.HTTPError{
c.JSON(http.StatusInternalServerError, web.HTTPError{ Message: web.APIErrorInternalDatabase,
Message: web.APIErrorInternalDatabase, Error: err.Error(),
Error: err.Error(), })
}) return
return }
} c.JSON(http.StatusOK, true)
c.JSON(http.StatusOK, true)
})
}) })
} }

View File

@ -12,7 +12,7 @@ import (
func TestAPIDeleteMyProfil(t *testing.T) { func TestAPIDeleteMyProfil(t *testing.T) {
assert := assert.New(t) assert := assert.New(t)
s, err := webtest.NewWithDBSetup(SetupMigration) s, err := webtest.NewWithDBSetup(Register, SetupMigration)
assert.NoError(err) assert.NoError(err)
defer s.Close() defer s.Close()
assert.NotNil(s) assert.NotNil(s)

View File

@ -20,38 +20,36 @@ import (
// @Router /api/v1/my/auth/password [post] // @Router /api/v1/my/auth/password [post]
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Param body body string false "new password" // @Param body body string false "new password"
func init() { func apiMyPassword(r *gin.Engine, ws *web.Service) {
web.ModuleRegister(func(r *gin.Engine, ws *web.Service) { r.POST("/api/v1/my/auth/password", MiddlewareLogin(ws), func(c *gin.Context) {
r.POST("/api/v1/my/auth/password", MiddlewareLogin(ws), func(c *gin.Context) { d, ok := GetCurrentUser(c, ws)
d, ok := GetCurrentUser(c, ws) if !ok {
if !ok { return
return }
} var password string
var password string if err := c.BindJSON(&password); err != nil {
if err := c.BindJSON(&password); err != nil { c.JSON(http.StatusBadRequest, web.HTTPError{
c.JSON(http.StatusBadRequest, web.HTTPError{ Message: web.APIErrorInvalidRequestFormat,
Message: web.APIErrorInvalidRequestFormat, Error: err.Error(),
Error: err.Error(), })
}) return
return }
} if err := d.SetPassword(password); err != nil {
if err := d.SetPassword(password); err != nil { c.JSON(http.StatusInternalServerError, web.HTTPError{
c.JSON(http.StatusInternalServerError, web.HTTPError{ Message: APIErrroCreatePassword,
Message: APIErrroCreatePassword, Error: err.Error(),
Error: err.Error(), })
}) return
return }
}
if err := ws.DB.Save(&d).Error; err != nil { if err := ws.DB.Save(&d).Error; err != nil {
c.JSON(http.StatusInternalServerError, web.HTTPError{ c.JSON(http.StatusInternalServerError, web.HTTPError{
Message: web.APIErrorInternalDatabase, Message: web.APIErrorInternalDatabase,
Error: err.Error(), Error: err.Error(),
}) })
return return
} }
c.JSON(http.StatusOK, true) c.JSON(http.StatusOK, true)
})
}) })
} }

View File

@ -12,7 +12,7 @@ import (
func TestAPIPassword(t *testing.T) { func TestAPIPassword(t *testing.T) {
assert := assert.New(t) assert := assert.New(t)
s, err := webtest.NewWithDBSetup(SetupMigration) s, err := webtest.NewWithDBSetup(Register, SetupMigration)
assert.NoError(err) assert.NoError(err)
defer s.Close() defer s.Close()
assert.NotNil(s) assert.NotNil(s)

View File

@ -18,13 +18,11 @@ import (
// @Failure 500 {object} web.HTTPError // @Failure 500 {object} web.HTTPError
// @Router /api/v1/my/auth/status [get] // @Router /api/v1/my/auth/status [get]
// @Security ApiKeyAuth // @Security ApiKeyAuth
func init() { func apiMyStatus(r *gin.Engine, ws *web.Service) {
web.ModuleRegister(func(r *gin.Engine, ws *web.Service) { r.GET("/api/v1/my/auth/status", MiddlewareLogin(ws), func(c *gin.Context) {
r.GET("/api/v1/my/auth/status", MiddlewareLogin(ws), func(c *gin.Context) { d, ok := GetCurrentUser(c, ws)
d, ok := GetCurrentUser(c, ws) if ok {
if ok { c.JSON(http.StatusOK, d)
c.JSON(http.StatusOK, d) }
}
})
}) })
} }

View File

@ -10,9 +10,9 @@ import (
"dev.sum7.eu/genofire/golang-lib/web/webtest" "dev.sum7.eu/genofire/golang-lib/web/webtest"
) )
func TestAPIStatus(t *testing.T) { func TestAPIMyStatus(t *testing.T) {
assert := assert.New(t) assert := assert.New(t)
s, err := webtest.NewWithDBSetup(SetupMigration) s, err := webtest.NewWithDBSetup(Register, SetupMigration)
assert.NoError(err) assert.NoError(err)
defer s.Close() defer s.Close()
assert.NotNil(s) assert.NotNil(s)

View File

@ -28,49 +28,47 @@ type PasswordWithForgetCode struct {
// @Failure 500 {object} web.HTTPError // @Failure 500 {object} web.HTTPError
// @Router /api/v1/auth/password/code [post] // @Router /api/v1/auth/password/code [post]
// @Param body body PasswordWithForgetCode false "new password and forget code" // @Param body body PasswordWithForgetCode false "new password and forget code"
func init() { func apiPasswordCode(r *gin.Engine, ws *web.Service) {
web.ModuleRegister(func(r *gin.Engine, ws *web.Service) { r.POST("/api/v1/auth/password/code", func(c *gin.Context) {
r.POST("/api/v1/auth/password/code", func(c *gin.Context) { var req PasswordWithForgetCode
var req PasswordWithForgetCode if err := c.BindJSON(&req); err != nil {
if err := c.BindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, web.HTTPError{
Message: web.APIErrorInvalidRequestFormat,
Error: err.Error(),
})
return
}
d := User{}
if err := ws.DB.Where("forget_code", req.ForgetCode).First(&d).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
c.JSON(http.StatusBadRequest, web.HTTPError{ c.JSON(http.StatusBadRequest, web.HTTPError{
Message: web.APIErrorInvalidRequestFormat, Message: APIErrorUserNotFound,
Error: err.Error(), Error: err.Error(),
}) })
return return
} }
d := User{} c.JSON(http.StatusInternalServerError, web.HTTPError{
if err := ws.DB.Where("forget_code", req.ForgetCode).First(&d).Error; err != nil { Message: APIErrroCreatePassword,
if errors.Is(err, gorm.ErrRecordNotFound) { Error: err.Error(),
c.JSON(http.StatusBadRequest, web.HTTPError{ })
Message: APIErrorUserNotFound, return
Error: err.Error(), }
}) if err := d.SetPassword(req.Password); err != nil {
return c.JSON(http.StatusInternalServerError, web.HTTPError{
} Message: APIErrroCreatePassword,
c.JSON(http.StatusInternalServerError, web.HTTPError{ Error: err.Error(),
Message: APIErrroCreatePassword, })
Error: err.Error(), return
}) }
return d.ForgetCode = nil
}
if err := d.SetPassword(req.Password); err != nil {
c.JSON(http.StatusInternalServerError, web.HTTPError{
Message: APIErrroCreatePassword,
Error: err.Error(),
})
return
}
d.ForgetCode = nil
if err := ws.DB.Save(&d).Error; err != nil { if err := ws.DB.Save(&d).Error; err != nil {
c.JSON(http.StatusInternalServerError, web.HTTPError{ c.JSON(http.StatusInternalServerError, web.HTTPError{
Message: web.APIErrorInternalDatabase, Message: web.APIErrorInternalDatabase,
Error: err.Error(), Error: err.Error(),
}) })
return return
} }
c.JSON(http.StatusOK, d.Username) c.JSON(http.StatusOK, d.Username)
})
}) })
} }

View File

@ -13,7 +13,7 @@ import (
func TestAPIPasswordCode(t *testing.T) { func TestAPIPasswordCode(t *testing.T) {
assert := assert.New(t) assert := assert.New(t)
s, err := webtest.NewWithDBSetup(SetupMigration) s, err := webtest.NewWithDBSetup(apiPasswordCode, SetupMigration)
assert.NoError(err) assert.NoError(err)
defer s.Close() defer s.Close()
assert.NotNil(s) assert.NotNil(s)

15
web/auth/main.go Normal file
View File

@ -0,0 +1,15 @@
package auth
import (
"dev.sum7.eu/genofire/golang-lib/web"
"github.com/gin-gonic/gin"
)
// Register to WebService
func Register(r *gin.Engine, ws *web.Service) {
apiLogin(r, ws)
apiMyDelete(r, ws)
apiMyPassword(r, ws)
apiMyStatus(r, ws)
apiPasswordCode(r, ws)
}

View File

@ -3,9 +3,11 @@ package web
import ( import (
"github.com/bdlm/log" "github.com/bdlm/log"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
// acme // acme
"github.com/gin-gonic/autotls" "github.com/gin-gonic/autotls"
"golang.org/x/crypto/acme/autocert" "golang.org/x/crypto/acme/autocert"
// internal // internal
"dev.sum7.eu/genofire/golang-lib/mailer" "dev.sum7.eu/genofire/golang-lib/mailer"
"gorm.io/gorm" "gorm.io/gorm"
@ -30,6 +32,8 @@ type Service struct {
// internal // internal
DB *gorm.DB `toml:"-"` DB *gorm.DB `toml:"-"`
Mailer *mailer.Service `toml:"-"` Mailer *mailer.Service `toml:"-"`
modules []ModuleRegisterFunc
} }
// Run to startup all related web parts // Run to startup all related web parts

View File

@ -10,6 +10,7 @@ import (
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp" "github.com/prometheus/client_golang/prometheus/promhttp"
// db metrics // db metrics
gormPrometheus "gorm.io/plugin/prometheus" gormPrometheus "gorm.io/plugin/prometheus"
@ -27,41 +28,40 @@ var (
} }
) )
func init() { // Register to WebService
web.ModuleRegister(func(r *gin.Engine, ws *web.Service) { func Register(r *gin.Engine, ws *web.Service) {
r.Use(ginprom.PromMiddleware(&ginprom.PromOpts{ r.Use(ginprom.PromMiddleware(&ginprom.PromOpts{
EndpointLabelMappingFn: func(c *gin.Context) string { EndpointLabelMappingFn: func(c *gin.Context) string {
url := c.Request.URL.Path url := c.Request.URL.Path
for _, p := range c.Params { for _, p := range c.Params {
url = strings.Replace(url, p.Value, ":"+p.Key, 1) url = strings.Replace(url, p.Value, ":"+p.Key, 1)
} }
return url return url
}, },
}))
prometheus.MustRegister(prometheus.NewGaugeFunc(
prometheus.GaugeOpts{
Namespace: NAMESPACE,
Name: "up",
Help: "is current version of service running",
ConstLabels: prometheus.Labels{"version": VERSION},
},
func() float64 {
if UP() {
return 1
}
return 0
},
))
if ws.DB != nil {
ws.DB.Use(gormPrometheus.New(gormPrometheus.Config{
DBName: NAMESPACE,
RefreshInterval: 15,
})) }))
prometheus.MustRegister(prometheus.NewGaugeFunc( }
prometheus.GaugeOpts{
Namespace: NAMESPACE,
Name: "up",
Help: "is current version of service running",
ConstLabels: prometheus.Labels{"version": VERSION},
},
func() float64 {
if UP() {
return 1
}
return 0
},
))
if ws.DB != nil { r.GET("/metrics", ginprom.PromHandler(promhttp.Handler()))
ws.DB.Use(gormPrometheus.New(gormPrometheus.Config{
DBName: NAMESPACE,
RefreshInterval: 15,
}))
}
r.GET("/metrics", ginprom.PromHandler(promhttp.Handler()))
})
} }

View File

@ -11,7 +11,7 @@ import (
func TestMetricsLoaded(t *testing.T) { func TestMetricsLoaded(t *testing.T) {
assert := assert.New(t) assert := assert.New(t)
s, err := webtest.New() s, err := webtest.New(Register)
assert.NoError(err) assert.NoError(err)
defer s.Close() defer s.Close()
assert.NotNil(s) assert.NotNil(s)

View File

@ -6,24 +6,20 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
var (
modules []ModuleRegisterFunc
)
// ModuleRegisterFunc format of module which registered to WebService // ModuleRegisterFunc format of module which registered to WebService
type ModuleRegisterFunc func(*gin.Engine, *Service) type ModuleRegisterFunc func(*gin.Engine, *Service)
// ModuleRegister used on start of WebService // ModuleRegister used on start of WebService
func ModuleRegister(f ModuleRegisterFunc) { func (ws *Service) ModuleRegister(f ModuleRegisterFunc) {
modules = append(modules, f) ws.modules = append(ws.modules, f)
} }
// Bind WebService to gin.Engine // Bind WebService to gin.Engine
func (ws *Service) Bind(r *gin.Engine) { func (ws *Service) Bind(r *gin.Engine) {
for _, f := range modules { for _, f := range ws.modules {
f(r, ws) f(r, ws)
} }
log.Infof("loaded %d modules", len(modules)) log.Infof("loaded %d modules", len(ws.modules))
r.Use(static.Serve("/", static.LocalFile(ws.Webroot, false))) r.Use(static.Serve("/", static.LocalFile(ws.Webroot, false)))
} }

View File

@ -23,8 +23,9 @@ var (
// Option to configure TestServer // Option to configure TestServer
type Option struct { type Option struct {
ReRun bool ReRun bool
DBSetup func(db *database.Database) DBSetup func(db *database.Database)
ModuleLoader web.ModuleRegisterFunc
} }
type testServer struct { type testServer struct {
@ -32,7 +33,7 @@ type testServer struct {
Mails chan *mailer.TestingMail Mails chan *mailer.TestingMail
Close func() Close func()
gin *gin.Engine gin *gin.Engine
ws *web.Service WS *web.Service
lastCookies []*http.Cookie lastCookies []*http.Cookie
} }
@ -43,13 +44,17 @@ type Login struct {
} }
// New starts WebService for testing // New starts WebService for testing
func New() (*testServer, error) { func New(modules web.ModuleRegisterFunc) (*testServer, error) {
return NewWithOption(Option{}) return NewWithOption(Option{ModuleLoader: modules})
} }
// NewWithDBSetup allows to reconfigure before ReRun the database - e.g. for adding Migration-Steps // NewWithDBSetup allows to reconfigure before ReRun the database - e.g. for adding Migration-Steps
func NewWithDBSetup(dbCall func(db *database.Database)) (*testServer, error) { func NewWithDBSetup(modules web.ModuleRegisterFunc, dbCall func(db *database.Database)) (*testServer, error) {
return NewWithOption(Option{ReRun: true, DBSetup: dbCall}) return NewWithOption(Option{
ReRun: true,
DBSetup: dbCall,
ModuleLoader: modules,
})
} }
// NewWithOption allows to configure WebService for testing // NewWithOption allows to configure WebService for testing
@ -92,6 +97,7 @@ func NewWithOption(option Option) (*testServer, error) {
DB: dbConfig.DB, DB: dbConfig.DB,
Mailer: mail, Mailer: mail,
} }
ws.ModuleRegister(option.ModuleLoader)
ws.Session.Name = "mysession" ws.Session.Name = "mysession"
ws.Session.Secret = "hidden" ws.Session.Secret = "hidden"
@ -103,13 +109,13 @@ func NewWithOption(option Option) (*testServer, error) {
Mails: mock.Mails, Mails: mock.Mails,
Close: mock.Close, Close: mock.Close,
gin: r, gin: r,
ws: ws, WS: ws,
}, nil }, nil
} }
// DatabaseForget, to run a test without a database // DatabaseForget, to run a test without a database
func (s *testServer) DatabaseForget() { func (s *testServer) DatabaseForget() {
s.ws.DB = nil s.WS.DB = nil
s.DB = nil s.DB = nil
} }