diff --git a/cmd/main.go b/cmd/main.go index b525019..5b86cc6 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -1,32 +1,52 @@ package main import ( - "go-gin/cmd/srv" + "context" + "go-gin/cmd/srv/controller" "go-gin/service/singleton" + "github.com/ory/graceful" flag "github.com/spf13/pflag" ) type CliParam struct { - ConfigName string // 配置文件名称 + ConfigName string // Config file name + Port uint // Server port } var ( - svrCliParam CliParam + cliParam CliParam ) func main() { flag.CommandLine.ParseErrorsWhitelist.UnknownFlags = true - flag.StringVarP(&svrCliParam.ConfigName, "config", "c", "config", "config file name") + flag.StringVarP(&cliParam.ConfigName, "config", "c", "config", "config file name") + flag.UintVarP(&cliParam.Port, "port", "p", 0, "server port") flag.Parse() flag.Lookup("config").NoOptDefVal = "config" - singleton.InitConfig(svrCliParam.ConfigName) - singleton.InitLog(singleton.Config) - // singleton.InitDBFromPath(singleton.Config.DB_Path) + singleton.InitConfig(cliParam.ConfigName) + singleton.InitLog(singleton.Conf) + singleton.InitDBFromPath(singleton.Conf.DBPath) initService() - srv.ServerWeb(singleton.Config) + port := singleton.Conf.Server.Port + if cliParam.Port != 0 { + port = cliParam.Port + } + + srv := controller.ServerWeb(port) + + if err := graceful.Graceful(func() error { + return srv.ListenAndServe() + }, func(c context.Context) error { + singleton.Log.Info().Msg("Graceful::START") + srv.Shutdown(c) + return nil + }); err != nil { + singleton.Log.Err(err).Msg("Graceful::Error") + } + } func initService() { diff --git a/cmd/srv/controller/api_v1.go b/cmd/srv/controller/api_v1.go new file mode 100644 index 0000000..446dccc --- /dev/null +++ b/cmd/srv/controller/api_v1.go @@ -0,0 +1,57 @@ +package controller + +import ( + "fmt" + "go-gin/internal/gogin" + "go-gin/service/singleton" + + "github.com/gin-gonic/gin" +) + +type apiV1 struct { + r gin.IRouter +} + +func (v *apiV1) serve() { + r := v.r.Group("") + // API + r.Use(gogin.Authorize(gogin.AuthorizeOption{ + User: true, + IsPage: false, + Msg: "Please log in first", + Btn: "Log in", + Redirect: fmt.Sprintf("%s/login", singleton.Conf.Site.BaseURL), + })) + r.PUT("/post", v.putPost) + + user := v.r.Group("user") + { + user.GET("/info", v.getUserInfo) + user.GET("/logout", v.logout) + user.GET("/refresh", v.refresh) + } +} + +func (v *apiV1) putPost(c *gin.Context) { + c.JSON(200, gin.H{ + "message": "post", + }) +} + +func (v *apiV1) getUserInfo(c *gin.Context) { + c.JSON(200, gin.H{ + "message": "user info", + }) +} + +func (v *apiV1) logout(c *gin.Context) { + c.JSON(200, gin.H{ + "message": "logout", + }) +} + +func (v *apiV1) refresh(c *gin.Context) { + c.JSON(200, gin.H{ + "message": "refresh", + }) +} diff --git a/cmd/srv/controller/auth.go b/cmd/srv/controller/auth.go deleted file mode 100644 index 676c42e..0000000 --- a/cmd/srv/controller/auth.go +++ /dev/null @@ -1,103 +0,0 @@ -package controller - -import ( - "net/http" - "time" - - "github.com/gin-gonic/gin" - "github.com/golang-jwt/jwt/v5" - - "go-gin/model" - "go-gin/service/singleton" - - api_utils "go-gin/internal/api" -) - -// Login authenticates the user -func Login(c *gin.Context) { - var creds model.Credentials - // get the body of the POST request - err := c.BindJSON(&creds) - if err != nil { - singleton.Log.Error().Msgf("Error binding JSON: %v", err) - api_utils.ResponseError(c, http.StatusBadRequest, "Invalid JSON") - return - } - - users := singleton.Config.Users - expectedPassword, ok := users[creds.Username] - - if !ok || expectedPassword != creds.Password { - singleton.Log.Error().Msgf("Invalid credentials: %v", creds) - api_utils.ResponseError(c, http.StatusUnauthorized, "Invalid credentials") - return - } - - expirationTime := time.Now().Add(time.Duration(singleton.Config.JWT.Expiration) * time.Minute) - claims := &model.Claims{ - Username: creds.Username, - RegisteredClaims: jwt.RegisteredClaims{ - // In JWT, the expiry time is expressed as unix milliseconds - ExpiresAt: jwt.NewNumericDate(expirationTime), - }, - } - - token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) - tokenString, err := token.SignedString([]byte(singleton.Config.JWT.Secret)) - if err != nil { - singleton.Log.Error().Msgf("Error signing token: %v", err) - api_utils.ResponseError(c, http.StatusInternalServerError, "Error signing token") - return - } - c.SetCookie("token", tokenString, int(expirationTime.Unix()), "/", "", false, true) - api_utils.Response(c, gin.H{"token": tokenString}) -} - -// Refresh refreshes the token -func Refresh(c *gin.Context) { - tokenString, err := api_utils.GetTokenString(c) - - if err != nil { - singleton.Log.Error().Msgf("Error getting token: %v", err) - c.JSON(http.StatusUnauthorized, &model.ErrorResponse{ - Code: http.StatusUnauthorized, - Message: "Unauthorized", - }) - return - } - - claims := &model.Claims{} - token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) { - return []byte(singleton.Config.JWT.Secret), nil - }) - if err != nil { - singleton.Log.Error().Msgf("Error parsing token: %v", err) - api_utils.ResponseError(c, http.StatusUnauthorized, "Unauthorized") - return - } - if !token.Valid { - singleton.Log.Error().Msgf("Invalid token") - c.JSON(http.StatusUnauthorized, &model.ErrorResponse{ - Code: http.StatusUnauthorized, - Message: "Invalid token", - }) - return - } - - expirationTime := time.Now().Add(time.Duration(singleton.Config.JWT.Expiration) * time.Minute) - claims.ExpiresAt = jwt.NewNumericDate(expirationTime) - token = jwt.NewWithClaims(jwt.SigningMethodHS256, claims) - tokenString, err = token.SignedString([]byte(singleton.Config.JWT.Secret)) - if err != nil { - singleton.Log.Error().Msgf("Error signing token: %v", err) - api_utils.ResponseError(c, http.StatusInternalServerError, "Error signing token") - return - } - c.SetCookie("token", tokenString, int(expirationTime.Unix()), "/", "", false, true) - api_utils.Response(c, gin.H{"token": tokenString}) -} - -func Logout(c *gin.Context) { - c.SetCookie("token", "", -1, "/", "", false, true) - api_utils.Response(c, "", "Logged out") -} diff --git a/cmd/srv/controller/common.go b/cmd/srv/controller/common.go deleted file mode 100644 index bce08a9..0000000 --- a/cmd/srv/controller/common.go +++ /dev/null @@ -1,26 +0,0 @@ -package controller - -import ( - "fmt" - "net/http" - - "go-gin/model" - - "github.com/gin-gonic/gin" -) - -func PageNotFound(c *gin.Context) { - c.JSON(http.StatusNotFound, - &model.ErrorResponse{ - Code: http.StatusNotFound, - Message: fmt.Sprintf("Resource not found: %s", c.Request.RequestURI), - }) -} - -func HealthCheck(c *gin.Context) { - c.JSON(http.StatusOK, gin.H{"status": "ok"}) -} - -func Home(c *gin.Context) { - c.HTML(http.StatusOK, "index", gin.H{}) -} diff --git a/cmd/srv/controller/common_page.go b/cmd/srv/controller/common_page.go new file mode 100644 index 0000000..c769675 --- /dev/null +++ b/cmd/srv/controller/common_page.go @@ -0,0 +1,28 @@ +package controller + +import ( + "go-gin/internal/gogin" + "net/http" + + "github.com/gin-gonic/gin" +) + +type commonPage struct { + r *gin.Engine +} + +func (cp *commonPage) serve() { + cr := cp.r.Group("") + cr.GET("/", cp.home) + cr.GET("/ping", cp.ping) +} + +func (p *commonPage) home(c *gin.Context) { + c.HTML(http.StatusOK, "index", gogin.CommonEnvironment(c, gin.H{})) +} + +func (p *commonPage) ping(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{ + "message": "pong", + }) +} diff --git a/cmd/srv/controller/controller.go b/cmd/srv/controller/controller.go new file mode 100644 index 0000000..91c7095 --- /dev/null +++ b/cmd/srv/controller/controller.go @@ -0,0 +1,107 @@ +package controller + +import ( + "fmt" + "html/template" + "io/fs" + "net/http" + "time" + + "github.com/gin-contrib/pprof" + "github.com/gin-gonic/gin" + + "go-gin/internal/gconfig" + "go-gin/internal/gogin" + "go-gin/pkg/mygin" + "go-gin/resource" + "go-gin/service/singleton" +) + +func ServerWeb(port uint) *http.Server { + gin.SetMode(gin.ReleaseMode) + r := gin.Default() + + loadTemplates(r) + + if singleton.Conf.Debug { + gin.SetMode(gin.DebugMode) + pprof.Register(r, gconfig.DefaultPprofRoutePath) + } + + r.Use(mygin.RecordPath) + + serveStatic(r) + + routers(r) + + srv := &http.Server{ + Addr: fmt.Sprintf(":%d", port), + ReadHeaderTimeout: time.Second * 5, + Handler: r, + } + return srv +} + +// Serve static files +func serveStatic(r *gin.Engine) { + staticFs, err := fs.Sub(resource.StaticFS, "static") + if err != nil { + singleton.Log.Fatal().Err(err).Msg("Error parsing static files") + panic(err) + } + r.StaticFS("/static", http.FS(staticFs)) + + // Serve uploaded files + r.Static("/upload", singleton.Conf.Upload.Dir) +} + +// Load templates +func loadTemplates(r *gin.Engine) { + new_tmpl := template.New("").Funcs(mygin.FuncMap) + var err error + new_tmpl, err = new_tmpl.ParseFS(resource.TemplateFS, "template/**/*.html", "template/*.html") + if err != nil { + singleton.Log.Fatal().Err(err).Msg("Error parsing templates") + panic(err) + } + r.SetHTMLTemplate(new_tmpl) +} + +func routers(r *gin.Engine) { + + r.Use(gogin.LoggingHandler()) + r.Use(mygin.GenerateContextIdHandler()) + r.Use(mygin.CORSHandler()) + r.Use(gogin.RateLimiterHandler(singleton.Conf.RateLimit.Max)) + + // Serve common pages, e.g. home, ping + cp := commonPage{r: r} + cp.serve() + + // Serve guest pages, e.g. register, login + gp := guestPage{r: r} + gp.serve() + + // Server show pages, e.g. post + sp := showPage{r: r} + sp.serve() + + // Serve API + api := r.Group("api") + { + ua := &userAPI{r: api} + ua.serve() + } + + page404 := func(c *gin.Context) { + gogin.ShowErrorPage(c, mygin.ErrInfo{ + Code: http.StatusNotFound, + Title: "Page not found", + Msg: "The page you are looking for is not found", + Link: singleton.Conf.Site.BaseURL, + Btn: "Back to home", + }, true) + } + r.NoRoute(page404) + r.NoMethod(page404) +} diff --git a/cmd/srv/controller/guest_page.go b/cmd/srv/controller/guest_page.go new file mode 100644 index 0000000..f88821d --- /dev/null +++ b/cmd/srv/controller/guest_page.go @@ -0,0 +1,34 @@ +package controller + +import ( + "go-gin/internal/gogin" + "go-gin/service/singleton" + "net/http" + + "github.com/gin-gonic/gin" +) + +type guestPage struct { + r *gin.Engine +} + +func (gp *guestPage) serve() { + gr := gp.r.Group("") + gr.Use(gogin.Authorize(gogin.AuthorizeOption{ + Guest: true, + IsPage: true, + Msg: "You are already logged in", + Btn: "Return to home", + Redirect: singleton.Conf.Site.BaseURL, + })) + gr.GET("/register", gp.register) + gr.GET("/login", gp.login) +} + +func (gp *guestPage) register(c *gin.Context) { + c.HTML(http.StatusOK, "register", gin.H{}) +} + +func (gp *guestPage) login(c *gin.Context) { + c.HTML(http.StatusOK, "login", gin.H{}) +} diff --git a/cmd/srv/controller/share.go b/cmd/srv/controller/share.go deleted file mode 100644 index f8d5bee..0000000 --- a/cmd/srv/controller/share.go +++ /dev/null @@ -1,31 +0,0 @@ -package controller - -import ( - "fmt" - "net/http" - "os" - - "github.com/gin-gonic/gin" - - api_utils "go-gin/internal/api" - "go-gin/service/singleton" -) - -func GetCreation(c *gin.Context) { - share_num := c.Param("share_num") - creation_file := fmt.Sprintf("%s/creation/%s.png", singleton.Config.Upload.Dir, share_num) - // Check if the file exists - if _, err := os.Stat(creation_file); os.IsNotExist(err) { - api_utils.ResponseError(c, http.StatusNotFound, "Creation not found") - return - } - - c.HTML( - http.StatusOK, - "creation/share", - gin.H{ - "share_num": share_num, - "config": singleton.Config, - }, - ) -} diff --git a/cmd/srv/controller/show_page.go b/cmd/srv/controller/show_page.go new file mode 100644 index 0000000..944e8c4 --- /dev/null +++ b/cmd/srv/controller/show_page.go @@ -0,0 +1,23 @@ +package controller + +import ( + "net/http" + + "github.com/gin-gonic/gin" +) + +type showPage struct { + r *gin.Engine +} + +func (sp *showPage) serve() { + gr := sp.r.Group("") + gr.GET("/post/:num", sp.postDetail) +} + +func (sp *showPage) postDetail(c *gin.Context) { + num := c.Param("num") + c.HTML(http.StatusOK, "post/detail", gin.H{ + "Num": num, + }) +} diff --git a/cmd/srv/controller/user.go b/cmd/srv/controller/user.go deleted file mode 100644 index 345d055..0000000 --- a/cmd/srv/controller/user.go +++ /dev/null @@ -1,33 +0,0 @@ -package controller - -import ( - "fmt" - "net/http" - "strings" - - "go-gin/pkg/utils" - "go-gin/service/singleton" - - "github.com/gin-gonic/gin" - - api_utils "go-gin/internal/api" -) - -func UploadCreation(c *gin.Context) { - file, err := c.FormFile("file") - if err != nil { - api_utils.ResponseError(c, http.StatusBadRequest, "File not found") - return - } - if !strings.Contains(file.Header.Get("Content-Type"), "image") { - api_utils.ResponseError(c, http.StatusBadRequest, "File is not an image") - return - } - new_id := utils.GenHexStr(32) - c.SaveUploadedFile(file, fmt.Sprintf("%s/creation/%s.png", singleton.Config.Upload.Dir, new_id)) - - api_utils.Response(c, gin.H{ - "creation_id": new_id, - "share_url": fmt.Sprintf("%s/share/creation/%s", singleton.Config.Server.BaseUrl, new_id), - }) -} diff --git a/cmd/srv/controller/user_api.go b/cmd/srv/controller/user_api.go new file mode 100644 index 0000000..cb9d2d0 --- /dev/null +++ b/cmd/srv/controller/user_api.go @@ -0,0 +1,33 @@ +package controller + +import ( + "github.com/gin-gonic/gin" +) + +type userAPI struct { + r gin.IRouter +} + +func (ua *userAPI) serve() { + ur := ua.r.Group("") + ur.POST("/login", ua.login) + ur.POST("/register", ua.register) + + v1 := ua.r.Group("v1") + { + apiv1 := &apiV1{r: v1} + apiv1.serve() + } +} + +func (ua *userAPI) login(c *gin.Context) { + c.JSON(200, gin.H{ + "message": "login", + }) +} + +func (ua *userAPI) register(c *gin.Context) { + c.JSON(200, gin.H{ + "message": "register", + }) +} diff --git a/cmd/srv/middleware/auth.go b/cmd/srv/middleware/auth.go deleted file mode 100644 index d7beee2..0000000 --- a/cmd/srv/middleware/auth.go +++ /dev/null @@ -1,41 +0,0 @@ -package middleware - -import ( - "net/http" - - "go-gin/model" - "go-gin/service/singleton" - - "github.com/gin-gonic/gin" - "github.com/golang-jwt/jwt/v5" - - api_utils "go-gin/internal/api" -) - -func AuthHanlder() gin.HandlerFunc { - return func(c *gin.Context) { - tokenString, err := api_utils.GetTokenString(c) - - if err != nil { - singleton.Log.Error().Msgf("Error getting token: %v", err) - api_utils.ResponseError(c, http.StatusUnauthorized, "Unauthorized") - return - } - claims := &model.Claims{} - token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) { - return []byte(singleton.Config.JWT.Secret), nil - }) - if err != nil { - singleton.Log.Error().Msgf("Error parsing token: %v", err) - api_utils.ResponseError(c, http.StatusUnauthorized, "Unauthorized") - return - } - if !token.Valid { - singleton.Log.Error().Msgf("Invalid token") - api_utils.ResponseError(c, http.StatusUnauthorized, "Invalid token") - return - } - - c.Next() - } -} diff --git a/cmd/srv/router.go b/cmd/srv/router.go deleted file mode 100644 index b74972b..0000000 --- a/cmd/srv/router.go +++ /dev/null @@ -1,77 +0,0 @@ -package srv - -import ( - "html/template" - "io/fs" - "net/http" - - "go-gin/cmd/srv/controller" - "go-gin/cmd/srv/middleware" - "go-gin/internal/config" - "go-gin/internal/tmpl" - "go-gin/resource" - "go-gin/service/singleton" - - "github.com/gin-gonic/gin" -) - -func NewRoute(config *config.Config) *gin.Engine { - r := gin.Default() - - loadTemplates(r) - - serveStatic(r) - // Serve uploaded files - r.Static("/upload", config.Upload.Dir) - - routers(r) - - return r -} - -// Serve static files -func serveStatic(r *gin.Engine) { - staticFs, err := fs.Sub(resource.StaticFS, "static") - if err != nil { - singleton.Log.Fatal().Err(err).Msg("Error parsing static files") - panic(err) - } - r.StaticFS("/static", http.FS(staticFs)) -} - -// Load templates -func loadTemplates(r *gin.Engine) { - new_tmpl := template.New("").Funcs(tmpl.FuncMap) - var err error - new_tmpl, err = new_tmpl.ParseFS(resource.TemplateFS, "template/**/*.html", "template/*.html") - if err != nil { - singleton.Log.Fatal().Err(err).Msg("Error parsing templates") - panic(err) - } - r.SetHTMLTemplate(new_tmpl) -} - -func routers(r *gin.Engine) { - // Logging middleware - r.Use(middleware.LoggingHandler()) - // Rate limit middleware - r.Use(middleware.RateLimiterHandler(singleton.Config.RateLimit.Max)) - - r.POST("/login", controller.Login) - userGroup := r.Group("/v1/user") - { - // Set auth middleware - userGroup.Use(middleware.AuthHanlder()) - userGroup.GET("/refresh", controller.Refresh) - userGroup.POST("/upload/creation", controller.UploadCreation) - } - - shareGroup := r.Group("/share") - { - shareGroup.GET("/creation/:share_num", controller.GetCreation) - } - - r.GET("/", controller.Home) - r.GET("/health", controller.HealthCheck) - r.NoRoute(controller.PageNotFound) -} diff --git a/cmd/srv/server.go b/cmd/srv/server.go deleted file mode 100644 index bf26b5e..0000000 --- a/cmd/srv/server.go +++ /dev/null @@ -1,21 +0,0 @@ -package srv - -import ( - "fmt" - - "go-gin/internal/config" - "go-gin/model" - - "github.com/gin-contrib/pprof" - "github.com/gin-gonic/gin" -) - -func ServerWeb(config *config.Config) { - gin.SetMode(gin.ReleaseMode) - r := NewRoute(config) - if config.Debug { - gin.SetMode(gin.DebugMode) - pprof.Register(r, model.DefaultPprofRoutePath) - } - r.Run(fmt.Sprintf(":%v", config.Server.Port)) -} diff --git a/config.yaml.example b/config.yaml.example index b4d6b70..385de0d 100644 --- a/config.yaml.example +++ b/config.yaml.example @@ -1,5 +1,9 @@ server: port: 8080 + +site: + brand: Go-Gin + description: A simple web application using Go and Gin. base_url: http://localhost:8080 debug: true @@ -18,8 +22,10 @@ upload: max_size: 10485760 # 10MB jwt: - secret: qhkxjrRmYcVYKSEobqsvhxhtPVeTWquu - expiration: 525600 # minutes + access_secret: qhkxjrRmYcVYKSEobqsvhxhtPVeTWquu + refresh_secret: qhkxjrRmYcVYKSEobqsvhxhtPV3TWquu + access_token_expiration: 60 # minutes + refresh_token_expiration: 720 # minutes users: user1: password1 diff --git a/go.mod b/go.mod index 27d54b5..b52aa6f 100644 --- a/go.mod +++ b/go.mod @@ -41,7 +41,9 @@ require ( github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/myesui/uuid v1.0.0 // indirect + github.com/ory/graceful v0.1.3 // indirect github.com/pelletier/go-toml/v2 v2.1.0 // indirect + github.com/pkg/errors v0.9.1 // indirect github.com/sagikazarmark/locafero v0.4.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect github.com/sourcegraph/conc v0.3.0 // indirect diff --git a/go.sum b/go.sum index 6fb65f1..dd344bd 100644 --- a/go.sum +++ b/go.sum @@ -100,10 +100,13 @@ github.com/onsi/ginkgo/v2 v2.9.2 h1:BA2GMJOtfGAfagzYtrAlufIP0lq6QERkFmHLMLPwFSU= github.com/onsi/ginkgo/v2 v2.9.2/go.mod h1:WHcJJG2dIlcCqVfBAwUCrJxSPFb6v4azBwgxeMeDuts= github.com/onsi/gomega v1.27.4 h1:Z2AnStgsdSayCMDiCU42qIz+HLqEPcgiOCXjAU/w+8E= github.com/onsi/gomega v1.27.4/go.mod h1:riYq/GJKh8hhoM01HN6Vmuy93AarCXCBGpvFDK3q3fQ= +github.com/ory/graceful v0.1.3 h1:FaeXcHZh168WzS+bqruqWEw/HgXWLdNv2nJ+fbhxbhc= +github.com/ory/graceful v0.1.3/go.mod h1:4zFz687IAF7oNHHiB586U4iL+/4aV09o/PYLE34t2bA= github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo= github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4= github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +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/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= @@ -133,6 +136,7 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ 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/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 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= @@ -202,6 +206,7 @@ gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= gopkg.in/stretchr/testify.v1 v1.2.2 h1:yhQC6Uy5CqibAIlk1wlusa/MJ3iAN49/BsR/dCCKz3M= gopkg.in/stretchr/testify.v1 v1.2.2/go.mod h1:QI5V/q6UbPmuhtm10CaFZxED9NreB8PnFYN9JcR6TxU= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/api/api.go b/internal/api/api.go deleted file mode 100644 index 4888f33..0000000 --- a/internal/api/api.go +++ /dev/null @@ -1,46 +0,0 @@ -package api - -import ( - "fmt" - - "go-gin/model" - - "github.com/gin-gonic/gin" -) - -func GetTokenString(c *gin.Context) (string, error) { - tokenString, err := c.Cookie("token") - if err == nil { - return tokenString, nil - } - // Get the token from the header X-JWT-Token - tokenString = c.GetHeader("X-JWT-Token") - if tokenString != "" { - return tokenString, nil - } - // Get the token from the query - tokenString = c.Query("token") - if tokenString != "" { - return tokenString, nil - } - return "", fmt.Errorf("no token found") -} - -func ResponseError(c *gin.Context, code int, message string) { - c.AbortWithStatusJSON(code, &model.ErrorResponse{ - Code: code, - Message: message, - }) -} - -func Response(c *gin.Context, data interface{}, messages ...string) { - var message string - - if len(messages) > 0 { - message = messages[0] - } - c.AbortWithStatusJSON(200, &model.Response{ - Message: message, - Data: data, - }) -} diff --git a/internal/config/config.go b/internal/config/config.go deleted file mode 100644 index 84b19cf..0000000 --- a/internal/config/config.go +++ /dev/null @@ -1,28 +0,0 @@ -package config - -type Config struct { - Server struct { - Port uint `mapstructure:"port"` - BaseUrl string `mapstructure:"base_url"` - } `mapstructure:"server"` - Debug bool `mapstructure:"debug"` - DB_Path string `mapstructure:"db_path"` - RateLimit struct { - Max int `mapstructure:"max"` - } `mapstructure:"rate_limit"` - Upload struct { - Dir string `mapstructure:"dir"` - MaxSize int `mapstructure:"max_size"` - } `mapstructure:"upload"` - Log struct { - Level string `mapstructure:"level"` - Path string `mapstructure:"path"` - } `mapstructure:"log"` - JWT struct { - Secret string `mapstructure:"secret"` - Expiration int `mapstructure:"expiration"` - } `mapstructure:"jwt"` - Users map[string]string `mapstructure:"users"` -} - -var Instance *Config = &Config{} diff --git a/internal/gconfig/config.go b/internal/gconfig/config.go new file mode 100644 index 0000000..ffb541a --- /dev/null +++ b/internal/gconfig/config.go @@ -0,0 +1,37 @@ +package gconfig + +const ( + DefaultLogPath = "logs/log.log" + DefaultPprofRoutePath = "/debug/pprof" +) + +type Config struct { + Server struct { + Port uint `mapstructure:"port"` + } `mapstructure:"server"` + Site struct { + Brand string `mapstructure:"brand"` + Description string `mapstructure:"description"` + BaseURL string `mapstructure:"base_url"` + } `mapstructure:"site"` + Debug bool `mapstructure:"debug"` + DBPath string `mapstructure:"db_path"` + RateLimit struct { + Max int `mapstructure:"max"` + } `mapstructure:"rate_limit"` + Upload struct { + Dir string `mapstructure:"dir"` + MaxSize int `mapstructure:"max_size"` + } `mapstructure:"upload"` + Log struct { + Level string `mapstructure:"level"` + Path string `mapstructure:"path"` + } `mapstructure:"log"` + JWT struct { + AccessSecret string `mapstructure:"access_secret"` + RefreshSecret string `mapstructure:"refresh_secret"` + TokenExpiration int `mapstructure:"token_expiration"` + RefreshTokenExpiration int `mapstructure:"refresh_token_expiration"` + } `mapstructure:"jwt"` + Users map[string]string `mapstructure:"users"` +} diff --git a/internal/gogin/authorize_handler.go b/internal/gogin/authorize_handler.go new file mode 100644 index 0000000..25ef258 --- /dev/null +++ b/internal/gogin/authorize_handler.go @@ -0,0 +1,71 @@ +package gogin + +import ( + "net/http" + + "go-gin/model" + "go-gin/pkg/mygin" + "go-gin/service/singleton" + + "github.com/gin-gonic/gin" +) + +type AuthorizeOption struct { + User bool // if true, only logged user can access + Guest bool // if true, only guest can access + IsPage bool + Msg string + Redirect string + Btn string +} + +var auth = model.Auth{} + +func Authorize(opt AuthorizeOption) gin.HandlerFunc { + return func(c *gin.Context) { + var code = http.StatusForbidden + + rltErr := mygin.ErrInfo{ + Title: "Unauthorized", + Code: code, + Msg: opt.Msg, + Link: opt.Redirect, + Btn: opt.Btn, + } + + token, err := auth.ExtractTokenMetadata(c.Request, singleton.Conf) + if err != nil { + singleton.Log.Err(err).Msgf("Error from ExtractTokenMetadata: %v", err) + } + + isLogin := token != nil + + if isLogin { + var u model.User + + singleton.ApiLock.RLock() + err = singleton.DB.Model(&model.User{}).Where("username = ?", token.UserName).First(&u).Error + singleton.ApiLock.RUnlock() + + if err != nil { + singleton.Log.Err(err).Msgf("User not found: %v", err) + isLogin = false + } else { + c.Set(model.CtxKeyAuthorizedUser, u) + + } + } + + if opt.Guest && isLogin { + ShowErrorPage(c, rltErr, opt.IsPage) + return + } + + if !isLogin && opt.User { + ShowErrorPage(c, rltErr, opt.IsPage) + return + } + + c.Next() + } +} diff --git a/internal/gogin/common.go b/internal/gogin/common.go new file mode 100644 index 0000000..ce61d98 --- /dev/null +++ b/internal/gogin/common.go @@ -0,0 +1,41 @@ +package gogin + +import ( + "fmt" + "net/http" + + "github.com/gin-gonic/gin" + + "go-gin/pkg/mygin" + "go-gin/service/singleton" +) + +func CommonEnvironment(c *gin.Context, data map[string]interface{}) gin.H { + data["MatchedPath"] = c.MustGet("MatchedPath") + data["Version"] = singleton.Version + data["Conf"] = singleton.Conf + if t, has := data["Title"]; !has { + data["Title"] = singleton.Conf.Site.Brand + } else { + data["Title"] = fmt.Sprintf("%s - %s", t, singleton.Conf.Site.Brand) + } + return data +} + +func ShowErrorPage(c *gin.Context, i mygin.ErrInfo, isPage bool) { + if isPage { + c.HTML(i.Code, "error", CommonEnvironment(c, gin.H{ + "Code": i.Code, + "Title": i.Title, + "Msg": i.Msg, + "Link": i.Link, + "Btn": i.Btn, + })) + } else { + c.JSON(http.StatusOK, mygin.Response{ + Code: i.Code, + Message: i.Msg, + }) + } + c.Abort() +} diff --git a/cmd/srv/middleware/logging.go b/internal/gogin/logging_handler.go similarity index 96% rename from cmd/srv/middleware/logging.go rename to internal/gogin/logging_handler.go index 5e9b439..0f15980 100644 --- a/cmd/srv/middleware/logging.go +++ b/internal/gogin/logging_handler.go @@ -1,4 +1,4 @@ -package middleware +package gogin import ( "go-gin/service/singleton" diff --git a/cmd/srv/middleware/rate_limit.go b/internal/gogin/ratelimit_handler.go similarity index 72% rename from cmd/srv/middleware/rate_limit.go rename to internal/gogin/ratelimit_handler.go index a05612a..9676b9a 100644 --- a/cmd/srv/middleware/rate_limit.go +++ b/internal/gogin/ratelimit_handler.go @@ -1,13 +1,12 @@ -package middleware +package gogin import ( + "go-gin/pkg/mygin" "net/http" "time" "github.com/gin-gonic/gin" "golang.org/x/time/rate" - - api_utils "go-gin/internal/api" ) func RateLimiterHandler(reqsPerMin int) gin.HandlerFunc { @@ -20,7 +19,10 @@ func RateLimiterHandler(reqsPerMin int) gin.HandlerFunc { return func(c *gin.Context) { if !limiter.Allow() { - api_utils.ResponseError(c, http.StatusTooManyRequests, "too many requests") + ShowErrorPage(c, mygin.ErrInfo{ + Code: http.StatusTooManyRequests, + Msg: "too many requests", + }, false) } c.Next() } diff --git a/mappers/auth.go b/mappers/auth.go new file mode 100644 index 0000000..5265834 --- /dev/null +++ b/mappers/auth.go @@ -0,0 +1,12 @@ +package mappers + +type LoginForm struct { + UserName string `form:"username" json:"username" binding:"required"` + Password string `form:"password" json:"password" binding:"required"` +} + +type RegisterForm struct { + UserName string `form:"username" json:"username" binding:"required,username"` + Password string `form:"password" json:"password" binding:"required"` + Email string `form:"email" json:"email" binding:"omitempty"` +} diff --git a/mappers/post.go b/mappers/post.go new file mode 100644 index 0000000..af676d0 --- /dev/null +++ b/mappers/post.go @@ -0,0 +1,8 @@ +package mappers + +type PostForm struct { + ID int `form:"id" json:"id" binding:"omitempty"` + Title string `form:"title" json:"title" binding:"required"` + Content string `form:"content" json:"content" binding:"required"` + Author string `form:"author" json:"author" binding:"required"` +} diff --git a/model/auth.go b/model/auth.go index 35b8b23..08025c8 100644 --- a/model/auth.go +++ b/model/auth.go @@ -1,17 +1,127 @@ package model -import "github.com/golang-jwt/jwt/v5" +import ( + "fmt" + "go-gin/internal/gconfig" + "net/http" + "strings" + "time" -type Credentials struct { - Password string `json:"password"` - Username string `json:"username"` + "github.com/golang-jwt/jwt/v5" + "github.com/twinj/uuid" +) + +type TokenDetails struct { + AccessToken string + RefreshToken string + AccessUUID string + RefreshUUID string + AtExpires int64 + RtExpires int64 +} + +type AccessDetails struct { + AccessUUID string + UserName string +} + +type Token struct { + AccessToken string `json:"access_token"` + RefreshToken string `json:"refresh_token"` +} + +type Auth struct{} + +// CreateToken by username +func (m Auth) CreateToken(username string, conf *gconfig.Config) (*TokenDetails, error) { + td := &TokenDetails{} + td.AtExpires = time.Now().Add(time.Minute * time.Duration(conf.JWT.TokenExpiration)).Unix() + td.AccessUUID = uuid.NewV4().String() + + td.RtExpires = time.Now().Add(time.Minute * time.Duration(conf.JWT.RefreshTokenExpiration)).Unix() + td.RefreshUUID = uuid.NewV4().String() + + var err error + atClaims := jwt.MapClaims{} + atClaims["authorized"] = true + atClaims["access_uuid"] = td.AccessUUID + atClaims["user_id"] = username + atClaims["exp"] = td.AtExpires + + at := jwt.NewWithClaims(jwt.SigningMethodHS256, atClaims) + td.AccessToken, err = at.SignedString([]byte(conf.JWT.AccessSecret)) + if err != nil { + return nil, err + } + rtClaims := jwt.MapClaims{} + rtClaims["refresh_uuid"] = td.RefreshUUID + rtClaims["user_id"] = username + rtClaims["exp"] = td.RtExpires + rt := jwt.NewWithClaims(jwt.SigningMethodHS256, rtClaims) + td.RefreshToken, err = rt.SignedString([]byte(conf.JWT.RefreshSecret)) + if err != nil { + return nil, err + } + return td, nil +} + +// Extract the token from the request +func (m Auth) ExtractTokenFromAuthorization(r *http.Request) string { + bearToken := r.Header.Get("Authorization") + strArr := strings.Split(bearToken, " ") + if len(strArr) == 2 { + return strArr[1] + } + return "" +} + +// Verify the token from the request +func (m Auth) VerifyToken(r *http.Request, conf *gconfig.Config) (*jwt.Token, error) { + tokenString := m.ExtractTokenFromAuthorization(r) + if tokenString == "" { + return nil, fmt.Errorf("Can't find token from request") + } + token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) + } + return []byte(conf.JWT.AccessSecret), nil + }) + if err != nil { + return nil, err + } + return token, nil } -type Claims struct { - Username string `json:"username"` - jwt.RegisteredClaims +// TokenValid from the request +func (m Auth) TokenValid(r *http.Request, conf *gconfig.Config) error { + token, err := m.VerifyToken(r, conf) + if err != nil { + return err + } + if _, ok := token.Claims.(jwt.Claims); !ok && !token.Valid { + return err + } + return nil } -func (c *Credentials) IsValid() bool { - return c.Username != "" && c.Password != "" +// Get the token metadata from the request +func (m Auth) ExtractTokenMetadata(r *http.Request, conf *gconfig.Config) (*AccessDetails, error) { + token, err := m.VerifyToken(r, conf) + if err != nil { + return nil, err + } + claims, ok := token.Claims.(jwt.MapClaims) + if ok && token.Valid { + accessUUID, ok := claims["access_uuid"].(string) + if !ok { + return nil, err + } + userName := claims["user_id"].(string) + return &AccessDetails{ + AccessUUID: accessUUID, + UserName: userName, + }, nil + } + return nil, err } diff --git a/model/common.go b/model/common.go index 335c683..86ff409 100644 --- a/model/common.go +++ b/model/common.go @@ -6,10 +6,7 @@ import ( "gorm.io/gorm" ) -const ( - DefaultLogPath = "logs/log.log" - DefaultPprofRoutePath = "/debug/pprof" -) +const CtxKeyAuthorizedUser = "ckau" type Common struct { ID uint64 `gorm:"primaryKey"` @@ -17,13 +14,3 @@ type Common struct { UpdatedAt time.Time `gorm:"autoUpdateTime"` DeletedAt gorm.DeletedAt `gorm:"index"` } - -type ErrorResponse struct { - Code int `json:"code,omitempty"` - Message string `json:"message,omitempty"` -} - -type Response struct { - Message string `json:"message,omitempty"` - Data interface{} `json:"data,omitempty"` -} diff --git a/model/post.go b/model/post.go new file mode 100644 index 0000000..b9efa2f --- /dev/null +++ b/model/post.go @@ -0,0 +1,33 @@ +package model + +import ( + "go-gin/mappers" + + "gorm.io/gorm" +) + +type Post struct { + Common + Title string `json:"title,omitempty"` + Content string `json:"content,omitempty"` + Author string `json:"author,omitempty"` +} + +func (p Post) Create(form mappers.PostForm, db *gorm.DB) (err error) { + p.Title = form.Title + p.Content = form.Content + p.Author = form.Author + err = db.Model(&Post{}).Create(&p).Error + return err +} + +func (p Post) Update(form mappers.PostForm, db *gorm.DB) (err error) { + if form.ID == 0 { + return err + } + p.Title = form.Title + p.Content = form.Content + p.Author = form.Author + err = db.Model(&Post{}).Where("id = ?", form.ID).Updates(&p).Error + return err +} diff --git a/model/user.go b/model/user.go new file mode 100644 index 0000000..f7e30ba --- /dev/null +++ b/model/user.go @@ -0,0 +1,79 @@ +package model + +import ( + "errors" + "go-gin/internal/gconfig" + "go-gin/mappers" + + "github.com/twinj/uuid" + "golang.org/x/crypto/bcrypt" + "gorm.io/gorm" +) + +type User struct { + Common + UserName string `json:"username,omitempty"` + Password string `json:"password,omitempty"` + ForgotPasswordCode string `json:"forgot_password_code,omitempty"` + VerificationCode string `json:"verification_code,omitempty"` + + // Optional + Email string `json:"email,omitempty"` + Locked bool `json:"locked,omitempty"` + Veryfied bool `json:"veryfied,omitempty"` + AvatarURL string `json:"avatar_url,omitempty"` + NickName string `json:"nickname,omitempty"` + Phone string `json:"phone,omitempty"` + Blog string `json:"blog,omitempty"` + Bio string `json:"bio,omitempty"` +} + +var auth = new(Auth) + +func (user User) Login(form mappers.LoginForm, db *gorm.DB, conf *gconfig.Config) (token Token, err error) { + + db.Model(&User{}).Where("username = ?", form.UserName).First(&user) + + bytePassword := []byte(form.Password) + byteHashedPassword := []byte(user.Password) + + err = bcrypt.CompareHashAndPassword(byteHashedPassword, bytePassword) + + if err != nil { + return token, errors.New("invalid password") + } + + tokenDetails, err := auth.CreateToken(user.UserName, conf) + + if err == nil { + token.AccessToken = tokenDetails.AccessToken + token.RefreshToken = tokenDetails.RefreshToken + } + + return token, nil +} + +func (u User) Register(form mappers.RegisterForm, db *gorm.DB, conf *gconfig.Config) (user User, err error) { + err = db.Model(&User{}).Where("username = ?", form.UserName).First(&u).Error + if err != nil { + return user, err + } + + bytePassword := []byte(form.Password) + hashedPassword, err := bcrypt.GenerateFromPassword(bytePassword, bcrypt.DefaultCost) + if err != nil { + panic(err) + } + + user.UserName = form.UserName + user.Email = form.Email + user.Password = string(hashedPassword) + user.VerificationCode = uuid.NewV4().String() + user.ForgotPasswordCode = uuid.NewV4().String() + err = db.Create(&user).Error + if err != nil { + return user, err + } + + return user, err +} diff --git a/pkg/mygin/gin.go b/pkg/mygin/gin.go new file mode 100644 index 0000000..3f39c9a --- /dev/null +++ b/pkg/mygin/gin.go @@ -0,0 +1,47 @@ +package mygin + +import ( + "fmt" + "strings" + + "github.com/gin-gonic/gin" +) + +// RetrieveToken retrieves token from cookie, header, query, or post form +func RetrieveToken(c *gin.Context, tokenName string) (string, error) { + tokenString, err := c.Cookie(tokenName) + if err == nil { + return tokenString, nil + } + tokenString = c.GetHeader(tokenName) + if tokenString != "" { + return tokenString, nil + } + tokenString = c.Query(tokenName) + if tokenString != "" { + return tokenString, nil + } + tokenString = c.PostForm(tokenName) + if tokenString != "" { + return tokenString, nil + } + // get token from json body + var jsonBody map[string]interface{} + if err := c.BindJSON(&jsonBody); err == nil { + if token, ok := jsonBody[tokenName]; ok { + if tokenString, ok := token.(string); ok { + return tokenString, nil + } + } + } + return "", fmt.Errorf("token not found") +} + +// MatchedPath returns the matched path of the request +func RecordPath(c *gin.Context) { + url := c.Request.URL.String() + for _, p := range c.Params { + url = strings.Replace(url, p.Value, ":"+p.Key, 1) + } + c.Set("MatchedPath", url) +} diff --git a/pkg/mygin/handler.go b/pkg/mygin/handler.go new file mode 100644 index 0000000..7ba4c07 --- /dev/null +++ b/pkg/mygin/handler.go @@ -0,0 +1,46 @@ +package mygin + +import ( + "github.com/gin-gonic/gin" + "github.com/twinj/uuid" +) + +func CORSHandler() gin.HandlerFunc { + return func(c *gin.Context) { + c.Writer.Header().Set("Access-Control-Allow-Origin", "*") + c.Writer.Header().Set("Access-Control-Max-Age", "86400") + c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE, UPDATE") + c.Writer.Header().Set("Access-Control-Allow-Headers", "X-Requested-With, Content-Type, Origin, Authorization, Accept, Client-Security-Token, Accept-Encoding, x-access-token") + c.Writer.Header().Set("Access-Control-Expose-Headers", "Content-Length") + c.Writer.Header().Set("Access-Control-Allow-Credentials", "true") + if c.Request.Method == "OPTIONS" { + c.AbortWithStatus(200) + } + c.Next() + } +} + +func NoCacheHandler() gin.HandlerFunc { + return func(c *gin.Context) { + c.Writer.Header().Set("Cache-Control", "no-cache, no-store, max-age=0, must-revalidate, value") + c.Writer.Header().Set("Expires", "Thu, 01 Jan 1970 00:00:00 GMT") + c.Writer.Header().Set("Last-Modified", "Thu, 01 Jan 1970 00:00:00 GMT") + c.Writer.Header().Set("Pragma", "no-cache") + c.Next() + } +} + +func SecureJSONHandler() gin.HandlerFunc { + return func(c *gin.Context) { + c.Writer.Header().Set("Content-Type", "application/json") + c.Next() + } +} + +func GenerateContextIdHandler() gin.HandlerFunc { + return func(c *gin.Context) { + contextId := uuid.NewV4() + c.Writer.Header().Set("X-Context-Id", contextId.String()) + c.Next() + } +} diff --git a/pkg/mygin/response.go b/pkg/mygin/response.go new file mode 100644 index 0000000..68d81a7 --- /dev/null +++ b/pkg/mygin/response.go @@ -0,0 +1,31 @@ +package mygin + +import "github.com/gin-gonic/gin" + +type Response struct { + Code int `json:"code,omitempty"` + Message string `json:"message,omitempty"` + Data interface{} `json:"data,omitempty"` +} + +type ErrInfo struct { + Code int `json:"code,omitempty"` + Title string `json:"title,omitempty"` + Msg string `json:"msg,omitempty"` + Link string `json:"link,omitempty"` + Btn string `json:"btn,omitempty"` +} + +func ResponseJSON(c *gin.Context, code int, data interface{}, messages ...string) { + rlt := &Response{} + if len(messages) > 0 { + rlt.Message = messages[0] + } + if code > 0 { + rlt.Code = code + } + if data != nil { + rlt.Data = data + } + c.AbortWithStatusJSON(code, rlt) +} diff --git a/internal/tmpl/tmpl.go b/pkg/mygin/template.go similarity index 99% rename from internal/tmpl/tmpl.go rename to pkg/mygin/template.go index a69073e..08c9078 100644 --- a/internal/tmpl/tmpl.go +++ b/pkg/mygin/template.go @@ -1,4 +1,4 @@ -package tmpl +package mygin import ( "fmt" diff --git a/pkg/config/viper.go b/pkg/utils/viper.go similarity index 96% rename from pkg/config/viper.go rename to pkg/utils/viper.go index bde78be..91fb8b7 100644 --- a/pkg/config/viper.go +++ b/pkg/utils/viper.go @@ -1,4 +1,4 @@ -package config +package utils import ( "github.com/spf13/viper" diff --git a/resource/static/asset/img/favicon.png b/resource/static/asset/img/favicon.png deleted file mode 100644 index 487163a..0000000 Binary files a/resource/static/asset/img/favicon.png and /dev/null differ diff --git a/resource/static/asset/img/logo.png b/resource/static/asset/img/logo.png new file mode 100644 index 0000000..9f21fad Binary files /dev/null and b/resource/static/asset/img/logo.png differ diff --git a/resource/static/asset/logo.svg b/resource/static/asset/logo.svg new file mode 100644 index 0000000..f26b8ce --- /dev/null +++ b/resource/static/asset/logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resource/static/asset/style/base.css b/resource/static/asset/style/base.css new file mode 100644 index 0000000..f96d0af --- /dev/null +++ b/resource/static/asset/style/base.css @@ -0,0 +1,102 @@ +:root { + --c-brand: #3eaf7c; + --c-brand-light: #4abf8a; + --c-bg: #ffffff; + --c-bg-light: #f3f4f5; + --c-bg-lighter: #eeeeee; + --c-bg-dark: #ebebec; + --c-bg-darker: #e6e6e6; + --c-bg-navbar: var(--c-bg); + --c-bg-sidebar: var(--c-bg); + --c-bg-arrow: #cccccc; + --c-text: #2c3e50; + --c-text-accent: var(--c-brand); + --c-text-light: #3a5169; + --c-text-lighter: #4e6e8e; + --c-text-lightest: #6a8bad; + --c-text-quote: #999999; + --c-border: #eaecef; + --c-border-dark: #dfe2e5; + --c-tip: #42b983; + --c-tip-bg: var(--c-bg-light); + --c-tip-title: var(--c-text); + --c-tip-text: var(--c-text); + --c-tip-text-accent: var(--c-text-accent); + --c-warning: #ffc310; + --c-warning-bg: #fffae3; + --c-warning-bg-light: #fff3ba; + --c-warning-bg-lighter: #fff0b0; + --c-warning-border-dark: #f7dc91; + --c-warning-details-bg: #fff5ca; + --c-warning-title: #f1b300; + --c-warning-text: #746000; + --c-warning-text-accent: #edb100; + --c-warning-text-light: #c1971c; + --c-warning-text-quote: #ccab49; + --c-danger: #f11e37; + --c-danger-bg: #ffe0e0; + --c-danger-bg-light: #ffcfde; + --c-danger-bg-lighter: #ffc9c9; + --c-danger-border-dark: #f1abab; + --c-danger-details-bg: #ffd4d4; + --c-danger-title: #ed1e2c; + --c-danger-text: #660000; + --c-danger-text-accent: #bd1a1a; + --c-danger-text-light: #b5474d; + --c-danger-text-quote: #c15b5b; + --c-details-bg: #eeeeee; + --c-badge-tip: var(--c-tip); + --c-badge-warning: #ecc808; + --c-badge-warning-text: var(--c-bg); + --c-badge-danger: #dc2626; + --c-badge-danger-text: var(--c-bg); + --t-color: .3s ease; + --t-transform: .3s ease; + --code-bg-color: #282c34; + --code-hl-bg-color: rgba(0, 0, 0, .66); + --code-ln-color: #9e9e9e; + --code-ln-wrapper-width: 3.5rem; + --font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; + --font-family-code: Consolas, Monaco, "Andale Mono", "Ubuntu Mono", monospace; + --navbar-height: 3.6rem; + --navbar-padding-v: .7rem; + --navbar-padding-h: 1.5rem; + --sidebar-width: 20rem; + --sidebar-width-mobile: calc(var(--sidebar-width) * .82); + --content-width: 740px; + --homepage-width: 960px +} + +html, body { + padding: 0; + margin: 0; + font-family: 'Roboto', sans-serif; + font-size: 16px; + line-height: 1.5; + color: var(--c-text); + background-color: var(--c-bg); +} + +h1, h2, h3, h4, h5, h6 { + margin: 0; + padding: 0; + font-weight: 400; +} + +a { + text-decoration: none; + color: var(--c-text-accent); +} + +ul { + list-style: none; + padding: 0; + margin: 0; +} + +img { + max-width: 100%; + height: auto; +} + + diff --git a/resource/template/403.html b/resource/template/403.html deleted file mode 100644 index 29c3539..0000000 --- a/resource/template/403.html +++ /dev/null @@ -1,43 +0,0 @@ -{{ define "403" }} - - - - Access Denied - - - -
-

403

-

Sorry, you are not authorized to access this page.

- Go Home -
- - -{{ end }} diff --git a/resource/template/404.html b/resource/template/404.html deleted file mode 100644 index 6c2c791..0000000 --- a/resource/template/404.html +++ /dev/null @@ -1,43 +0,0 @@ -{{ define "404" }} - - - - Page Not Found - - - -
-

404

-

Sorry, the page you are looking for could not be found.

- Go Home -
- - -{{ end }} diff --git a/resource/template/base/footer.html b/resource/template/base/footer.html index fe5444c..4dfdba1 100644 --- a/resource/template/base/footer.html +++ b/resource/template/base/footer.html @@ -1,5 +1,4 @@ {{define "base/footer"}} -
- This is the footer. -
+ + {{end}} diff --git a/resource/template/base/header.html b/resource/template/base/header.html new file mode 100644 index 0000000..3c8bcb6 --- /dev/null +++ b/resource/template/base/header.html @@ -0,0 +1,15 @@ +{{define "base/header"}} + + + + + + + + {{.Title}} + + + + + +{{end}} diff --git a/resource/template/creation/share.html b/resource/template/creation/share.html deleted file mode 100644 index c04392a..0000000 --- a/resource/template/creation/share.html +++ /dev/null @@ -1,16 +0,0 @@ -{{ define "creation/share" }} - - - - - - Shere Creation - - - - - {{ .share_num }} - - - -{{ end }} diff --git a/resource/template/error.html b/resource/template/error.html new file mode 100644 index 0000000..84a5e2a --- /dev/null +++ b/resource/template/error.html @@ -0,0 +1,16 @@ +{{ define "error" }} +{{template "base/header" .}} + +
+

{{ .Code }}

+

{{ .Msg }}

+ {{ .Btn }} +
+{{template "base/footer" .}} +{{ end }} diff --git a/resource/template/index.html b/resource/template/index.html index e9b3f3f..48b10cc 100644 --- a/resource/template/index.html +++ b/resource/template/index.html @@ -1,26 +1,22 @@ {{ define "index" }} - - - - GoGin - - - - - -
GO GIN
- - +{{template "base/header" .}} + +
+

{{ .Conf.Site.Brand }}

+

{{ .Conf.Site.Description }}

+
+{{template "base/footer" .}} {{ end }} diff --git a/resource/template/login.html b/resource/template/login.html new file mode 100644 index 0000000..fe91adb --- /dev/null +++ b/resource/template/login.html @@ -0,0 +1,5 @@ +{{ define "login" }} +{{template "base/header" .}} +登陆 +{{template "base/footer" .}} +{{ end }} diff --git a/resource/template/post/detail.html b/resource/template/post/detail.html new file mode 100644 index 0000000..5c2f014 --- /dev/null +++ b/resource/template/post/detail.html @@ -0,0 +1,5 @@ +{{ define "post/detail" }} +{{template "base/header" .}} +{{ .post.num }} +{{template "base/footer" .}} +{{ end }} diff --git a/resource/template/register.html b/resource/template/register.html new file mode 100644 index 0000000..0896202 --- /dev/null +++ b/resource/template/register.html @@ -0,0 +1,5 @@ +{{ define "login" }} +{{template "base/header" .}} +注册 +{{template "base/footer" .}} +{{ end }} diff --git a/service/singleton/api.go b/service/singleton/api.go new file mode 100644 index 0000000..97e26b4 --- /dev/null +++ b/service/singleton/api.go @@ -0,0 +1,7 @@ +package singleton + +import "sync" + +var ( + ApiLock sync.RWMutex +) diff --git a/service/singleton/singleton.go b/service/singleton/singleton.go index 2066674..ff06b58 100644 --- a/service/singleton/singleton.go +++ b/service/singleton/singleton.go @@ -2,23 +2,24 @@ package singleton import ( "fmt" - "go-gin/internal/config" - "go-gin/model" - - config_utils "go-gin/pkg/config" - logger "go-gin/pkg/logger" + "os" "github.com/rs/zerolog" "gorm.io/driver/sqlite" "gorm.io/gorm" + + "go-gin/internal/gconfig" + "go-gin/model" + "go-gin/pkg/logger" + "go-gin/pkg/utils" ) var Version = "0.0.1" var ( - Config *config.Config - Log *zerolog.Logger - DB *gorm.DB + Conf *gconfig.Config + Log *zerolog.Logger + DB *gorm.DB ) func InitSingleton() { @@ -26,38 +27,41 @@ func InitSingleton() { } func InitConfig(name string) { - _config, err := config_utils.ReadViperConfig(name, "yaml", []string{".", "./config", "../"}) + _config, err := utils.ReadViperConfig(name, "yaml", []string{".", "./config", "../"}) if err != nil { panic(fmt.Errorf("unable to read config: %s", err)) } - if err := _config.Unmarshal(&Config); err != nil { + if err := _config.Unmarshal(&Conf); err != nil { panic(fmt.Errorf("unable to unmarshal config: %s", err)) } } // Initialize the logger -func InitLog(config *config.Config) { - logPath := config.Log.Path +func InitLog(conf *gconfig.Config) { + logPath := conf.Log.Path if logPath == "" { - logPath = model.DefaultLogPath + logPath = Conf.DBPath } - Log = logger.NewLogger(config.Log.Level, logPath) + Log = logger.NewLogger(conf.Log.Level, logPath) } -// InitDBFromPath 从给出的文件路径中加载数据库 +// InitDBFromPath initialize the database from the given path func InitDBFromPath(path string) { var err error + if err = utils.MkdirAllIfNotExists(path, os.ModePerm); err != nil { + panic(err) + } DB, err = gorm.Open(sqlite.Open(path), &gorm.Config{ CreateBatchSize: 200, }) if err != nil { panic(err) } - if Config.Debug { + if Conf.Debug { DB = DB.Debug() } - // err = DB.AutoMigrate(&model.User{}, &model.Role{}, &model.Permission{}, &model.UserRole{}, &model.RolePermission{}) + err = DB.AutoMigrate(&model.User{}, &model.Post{}) if err != nil { panic(err) }