add models / services / handlers of user / token / 2fa, add web server command
This commit is contained in:
+2
-1
@@ -29,7 +29,8 @@ func updateDatabaseStructure(c *cli.Context) error {
|
|||||||
|
|
||||||
log.BootInfof("[database.updateDatabaseStructure] starting maintaining")
|
log.BootInfof("[database.updateDatabaseStructure] starting maintaining")
|
||||||
|
|
||||||
_ = datastore.Container.UserStore.SyncStructs(new(models.User))
|
_ = datastore.Container.UserStore.SyncStructs(new(models.User), new(models.TwoFactor), new(models.TwoFactorRecoveryCode))
|
||||||
|
_ = datastore.Container.TokenStore.SyncStructs(new(models.TokenRecord))
|
||||||
|
|
||||||
log.BootInfof("[database.updateDatabaseStructure] maintained successfully")
|
log.BootInfof("[database.updateDatabaseStructure] maintained successfully")
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,176 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/gin-contrib/gzip"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/gin-gonic/gin/binding"
|
||||||
|
"github.com/go-playground/validator/v10"
|
||||||
|
"github.com/urfave/cli"
|
||||||
|
|
||||||
|
"github.com/mayswind/lab/pkg/api"
|
||||||
|
"github.com/mayswind/lab/pkg/core"
|
||||||
|
"github.com/mayswind/lab/pkg/errs"
|
||||||
|
"github.com/mayswind/lab/pkg/log"
|
||||||
|
"github.com/mayswind/lab/pkg/middlewares"
|
||||||
|
"github.com/mayswind/lab/pkg/requestid"
|
||||||
|
"github.com/mayswind/lab/pkg/settings"
|
||||||
|
"github.com/mayswind/lab/pkg/utils"
|
||||||
|
"github.com/mayswind/lab/pkg/validators"
|
||||||
|
)
|
||||||
|
|
||||||
|
var WebServer = cli.Command{
|
||||||
|
Name: "server",
|
||||||
|
Usage: "lab web server operation",
|
||||||
|
Subcommands: []cli.Command{
|
||||||
|
{
|
||||||
|
Name: "run",
|
||||||
|
Usage: "Run lab web server",
|
||||||
|
Action: startWebServer,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func startWebServer(c *cli.Context) error {
|
||||||
|
config, err := initializeSystem(c)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.BootInfof("[server.startWebServer] static root path is %s", config.StaticRootPath)
|
||||||
|
|
||||||
|
err = requestid.InitializeRequestIdGenerator(config)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.BootErrorf("[server.startWebServer] initializes requestid generator failed, because %s", err.Error())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
serverInfo := fmt.Sprintf("current server id is %d, current instance id is %d", requestid.Container.Current.GetCurrentServerUniqId(), requestid.Container.Current.GetCurrentInstanceUniqId())
|
||||||
|
uuidServerInfo := ""
|
||||||
|
if config.UuidGeneratorType == settings.UUID_GENERATOR_TYPE_INTERNAL {
|
||||||
|
uuidServerInfo = fmt.Sprintf(", current uuid server id is %d", config.UuidServerId)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.BootInfof("[server.startWebServer] %s%s", serverInfo, uuidServerInfo)
|
||||||
|
|
||||||
|
if config.Mode == settings.MODE_PRODUCTION {
|
||||||
|
gin.SetMode(gin.ReleaseMode)
|
||||||
|
}
|
||||||
|
|
||||||
|
router := gin.New()
|
||||||
|
router.Use(bindMiddleware(middlewares.Recovery))
|
||||||
|
|
||||||
|
if config.EnableGZip {
|
||||||
|
router.Use(gzip.Gzip(gzip.DefaultCompression))
|
||||||
|
}
|
||||||
|
|
||||||
|
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
|
||||||
|
_ = v.RegisterValidation("notBlank", validators.NotBlank)
|
||||||
|
_ = v.RegisterValidation("validUsername", validators.ValidUsername)
|
||||||
|
_ = v.RegisterValidation("validEmail", validators.ValidEmail)
|
||||||
|
}
|
||||||
|
|
||||||
|
router.NoRoute(bindApi(api.Default.ApiNotFound))
|
||||||
|
router.NoMethod(bindApi(api.Default.MethodNotAllowed))
|
||||||
|
|
||||||
|
router.StaticFile("/", filepath.Join(config.StaticRootPath, "index.html"))
|
||||||
|
router.StaticFile("login", filepath.Join(config.StaticRootPath, "login.html"))
|
||||||
|
|
||||||
|
if config.EnableUserRegister {
|
||||||
|
router.StaticFile("register", filepath.Join(config.StaticRootPath, "register.html"))
|
||||||
|
}
|
||||||
|
|
||||||
|
router.StaticFile("robots.txt", filepath.Join(config.StaticRootPath, "robots.txt"))
|
||||||
|
router.Static("/js", filepath.Join(config.StaticRootPath, "js"))
|
||||||
|
router.Static("/css", filepath.Join(config.StaticRootPath, "css"))
|
||||||
|
router.Static("/img", filepath.Join(config.StaticRootPath, "img"))
|
||||||
|
router.Static("/lang", filepath.Join(config.StaticRootPath, "lang"))
|
||||||
|
|
||||||
|
apiRoute := router.Group("/api")
|
||||||
|
|
||||||
|
apiRoute.Use(bindMiddleware(middlewares.RequestId(config)))
|
||||||
|
apiRoute.Use(bindMiddleware(middlewares.RequestLog))
|
||||||
|
{
|
||||||
|
apiRoute.POST("/authorize.json", bindApi(api.Authorizations.AuthorizeHandler))
|
||||||
|
|
||||||
|
if config.EnableTwoFactor {
|
||||||
|
twoFactorRoute := apiRoute.Group("/2fa")
|
||||||
|
twoFactorRoute.Use(bindMiddleware(middlewares.JWTTwoFactorAuthorization))
|
||||||
|
{
|
||||||
|
twoFactorRoute.POST("/authorize.json", bindApi(api.Authorizations.TwoFactorAuthorizeHandler))
|
||||||
|
twoFactorRoute.POST("/recovery.json", bindApi(api.Authorizations.TwoFactorAuthorizeByRecoveryCodeHandler))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.EnableUserRegister {
|
||||||
|
apiRoute.POST("/register.json", bindApi(api.Users.UserRegisterHandler))
|
||||||
|
}
|
||||||
|
|
||||||
|
apiV1Route := apiRoute.Group("/v1")
|
||||||
|
apiV1Route.Use(bindMiddleware(middlewares.JWTAuthorization))
|
||||||
|
{
|
||||||
|
// Tokens
|
||||||
|
apiV1Route.GET("/tokens/list.json", bindApi(api.Tokens.TokenListHandler))
|
||||||
|
apiV1Route.POST("/tokens/revoke.json", bindApi(api.Tokens.TokenRevokeHandler))
|
||||||
|
apiV1Route.POST("/tokens/refresh.json", bindApi(api.Tokens.TokenRefreshHandler))
|
||||||
|
|
||||||
|
// Users
|
||||||
|
apiV1Route.GET("/users/profile/get.json", bindApi(api.Users.UserProfileHandler))
|
||||||
|
apiV1Route.POST("/users/profile/update.json", bindApi(api.Users.UserUpdateProfileHandler))
|
||||||
|
|
||||||
|
// Two Factor Authorization
|
||||||
|
if config.EnableTwoFactor {
|
||||||
|
apiV1Route.GET("/users/2fa/status.json", bindApi(api.TwoFactorAuthorizations.TwoFactorStatusHandler))
|
||||||
|
apiV1Route.POST("/users/2fa/enable/request.json", bindApi(api.TwoFactorAuthorizations.TwoFactorEnableRequestHandler))
|
||||||
|
apiV1Route.POST("/users/2fa/enable/confirm.json", bindApi(api.TwoFactorAuthorizations.TwoFactorEnableConfirmHandler))
|
||||||
|
apiV1Route.POST("/users/2fa/disable.json", bindApi(api.TwoFactorAuthorizations.TwoFactorDisableHandler))
|
||||||
|
apiV1Route.POST("/users/2fa/recovery/regenerate.json", bindApi(api.TwoFactorAuthorizations.TwoFactorRecoveryCodeRegenerateHandler))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
listenAddr := fmt.Sprintf("%s:%d", config.HttpAddr, config.HttpPort)
|
||||||
|
|
||||||
|
if config.Protocol == settings.SCHEME_SOCKET {
|
||||||
|
log.BootInfof("[server.startWebServer] will run at socks:%s", config.UnixSocketPath)
|
||||||
|
err = router.RunUnix(config.UnixSocketPath)
|
||||||
|
} else if config.Protocol == settings.SCHEME_HTTP {
|
||||||
|
log.BootInfof("[server.startWebServer] will run at http://%s", listenAddr)
|
||||||
|
err = router.Run(listenAddr)
|
||||||
|
} else if config.Protocol == settings.SCHEME_HTTPS {
|
||||||
|
log.BootInfof("[server.startWebServer] will run at https://%s", listenAddr)
|
||||||
|
err = router.RunTLS(listenAddr, config.CertFile, config.CertKeyFile)
|
||||||
|
} else {
|
||||||
|
err = errs.ErrInvalidProtocol
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.BootErrorf("[server.startWebServer] cannot start, because %s", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func bindMiddleware(fn core.MiddlewareHandlerFunc) gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
fn(core.WrapContext(c))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func bindApi(fn core.ApiHandlerFunc) gin.HandlerFunc {
|
||||||
|
return func(ginCtx *gin.Context) {
|
||||||
|
c := core.WrapContext(ginCtx)
|
||||||
|
result, err := fn(c)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
utils.PrintErrorResult(c, err)
|
||||||
|
} else {
|
||||||
|
utils.PrintSuccessResult(c, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,14 +4,17 @@ go 1.14
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible
|
github.com/dgrijalva/jwt-go v3.2.0+incompatible
|
||||||
|
github.com/gin-contrib/gzip v0.0.3
|
||||||
github.com/gin-gonic/gin v1.6.3
|
github.com/gin-gonic/gin v1.6.3
|
||||||
github.com/go-playground/validator/v10 v10.2.0
|
github.com/go-playground/validator/v10 v10.2.0
|
||||||
github.com/go-sql-driver/mysql v1.5.0
|
github.com/go-sql-driver/mysql v1.5.0
|
||||||
github.com/lib/pq v1.8.0
|
github.com/lib/pq v1.8.0
|
||||||
github.com/mattn/go-sqlite3 v1.14.4
|
github.com/mattn/go-sqlite3 v1.14.4
|
||||||
|
github.com/pquerna/otp v1.2.0
|
||||||
github.com/sirupsen/logrus v1.7.0
|
github.com/sirupsen/logrus v1.7.0
|
||||||
github.com/smartystreets/goconvey v1.6.4 // indirect
|
github.com/smartystreets/goconvey v1.6.4 // indirect
|
||||||
github.com/stretchr/testify v1.4.0
|
github.com/stretchr/testify v1.4.0
|
||||||
|
github.com/urfave/cli v1.22.4
|
||||||
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897
|
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897
|
||||||
gopkg.in/ini.v1 v1.62.0
|
gopkg.in/ini.v1 v1.62.0
|
||||||
xorm.io/core v0.7.3
|
xorm.io/core v0.7.3
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a h1:lSA0F4e9A2NcQSqGqTOXqu2aRi/XEQxDCBwM8yJtE6s=
|
gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a h1:lSA0F4e9A2NcQSqGqTOXqu2aRi/XEQxDCBwM8yJtE6s=
|
||||||
gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a/go.mod h1:EXuID2Zs0pAQhH8yz+DNjUbjppKQzKFAn28TMYPB6IU=
|
gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a/go.mod h1:EXuID2Zs0pAQhH8yz+DNjUbjppKQzKFAn28TMYPB6IU=
|
||||||
|
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||||
github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
|
github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
|
||||||
github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
|
github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
|
||||||
|
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI=
|
||||||
|
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
|
||||||
|
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY=
|
||||||
|
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
@@ -10,6 +15,8 @@ github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumC
|
|||||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||||
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
|
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
|
||||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||||
|
github.com/gin-contrib/gzip v0.0.3 h1:etUaeesHhEORpZMp18zoOhepboiWnFtXrBZxszWUn4k=
|
||||||
|
github.com/gin-contrib/gzip v0.0.3/go.mod h1:YxxswVZIqOvcHEQpsSn+QF5guQtO1dCfy0shBPy4jFc=
|
||||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||||
github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14=
|
github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14=
|
||||||
@@ -62,6 +69,12 @@ github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU=
|
|||||||
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
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/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/pquerna/otp v1.2.0 h1:/A3+Jn+cagqayeR3iHs/L62m5ue7710D35zl1zJ1kok=
|
||||||
|
github.com/pquerna/otp v1.2.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
|
||||||
|
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
|
||||||
|
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
|
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
|
||||||
|
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||||
github.com/sirupsen/logrus v1.7.0 h1:ShrD1U9pZB12TX0cVy0DtePoCH97K8EtX+mg7ZARUtM=
|
github.com/sirupsen/logrus v1.7.0 h1:ShrD1U9pZB12TX0cVy0DtePoCH97K8EtX+mg7ZARUtM=
|
||||||
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
|
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
|
||||||
@@ -79,6 +92,8 @@ github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
|
|||||||
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
|
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
|
||||||
github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
|
github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
|
||||||
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
|
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
|
||||||
|
github.com/urfave/cli v1.22.4 h1:u7tSpNPPswAFymm8IehJhy4uJMlUuU/GmqSkvJ1InXA=
|
||||||
|
github.com/urfave/cli v1.22.4/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
|
||||||
github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0=
|
github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
|||||||
@@ -1 +1,30 @@
|
|||||||
package lab
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/urfave/cli"
|
||||||
|
|
||||||
|
"github.com/mayswind/lab/cmd"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
const LAB_VERSION = "0.1.0"
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
app := cli.NewApp()
|
||||||
|
app.Name = "lab"
|
||||||
|
app.Usage = "A lightweight account book app hosted by yourself."
|
||||||
|
app.Version = LAB_VERSION
|
||||||
|
app.Commands = []cli.Command{
|
||||||
|
cmd.WebServer,
|
||||||
|
cmd.Database,
|
||||||
|
}
|
||||||
|
app.Flags = []cli.Flag{
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "conf-path",
|
||||||
|
Usage: "Custom config `FILE` path",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
app.Run(os.Args)
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,182 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/pquerna/otp/totp"
|
||||||
|
|
||||||
|
"github.com/mayswind/lab/pkg/core"
|
||||||
|
"github.com/mayswind/lab/pkg/errs"
|
||||||
|
"github.com/mayswind/lab/pkg/log"
|
||||||
|
"github.com/mayswind/lab/pkg/models"
|
||||||
|
"github.com/mayswind/lab/pkg/services"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AuthorizationsApi struct {
|
||||||
|
users *services.UserService
|
||||||
|
tokens *services.TokenService
|
||||||
|
twoFactorAuthorizations *services.TwoFactorAuthorizationService
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
Authorizations = &AuthorizationsApi{
|
||||||
|
users: services.Users,
|
||||||
|
tokens: services.Tokens,
|
||||||
|
twoFactorAuthorizations: services.TwoFactorAuthorizations,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func (a *AuthorizationsApi) AuthorizeHandler(c *core.Context) (interface{}, *errs.Error) {
|
||||||
|
var credential models.UserLoginRequest
|
||||||
|
err := c.ShouldBindJSON(&credential)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.WarnfWithRequestId(c, "[authorizations.AuthorizeHandler] parse request failed, because %s", err.Error())
|
||||||
|
return nil, errs.ErrLoginNameOrPasswordInvalid
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := a.users.GetUserByUsernameOrEmailAndPassword(credential.LoginName, credential.Password)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.WarnfWithRequestId(c, "[authorizations.AuthorizeHandler] login failed for user \"%s\", because %s", credential.LoginName, err.Error())
|
||||||
|
return nil, errs.ErrLoginNameOrPasswordWrong
|
||||||
|
}
|
||||||
|
|
||||||
|
err = a.users.UpdateUserLastLoginTime(user.Uid)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.WarnfWithRequestId(c, "[authorizations.AuthorizeHandler] failed to update last login time for user \"uid:%d\", because %s", user.Uid, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
twoFactorEnable := a.tokens.CurrentConfig().EnableTwoFactor
|
||||||
|
|
||||||
|
if twoFactorEnable {
|
||||||
|
twoFactorEnable, err = a.twoFactorAuthorizations.ExistsTwoFactorSetting(user.Uid)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.ErrorfWithRequestId(c, "[authorizations.AuthorizeHandler] failed to check two factor setting for user \"uid:%d\", because %s", user.Uid, err.Error())
|
||||||
|
return nil, errs.Or(err, errs.ErrSystemError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var token string
|
||||||
|
var claims *core.UserTokenClaims
|
||||||
|
|
||||||
|
if twoFactorEnable {
|
||||||
|
token, claims, err = a.tokens.CreateRequire2FAToken(user, c)
|
||||||
|
} else {
|
||||||
|
token, claims, err = a.tokens.CreateToken(user, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.ErrorfWithRequestId(c, "[authorizations.AuthorizeHandler] failed to create token for user \"uid:%d\", because %s", user.Uid, err.Error())
|
||||||
|
return nil, errs.ErrTokenGenerating
|
||||||
|
}
|
||||||
|
|
||||||
|
c.SetTokenClaims(claims)
|
||||||
|
|
||||||
|
log.InfofWithRequestId(c, "[authorizations.AuthorizeHandler] user \"uid:%d\" has logined, token type is %d, token will be expired at %d", user.Uid, claims.Type, claims.ExpiresAt)
|
||||||
|
return token, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AuthorizationsApi) TwoFactorAuthorizeHandler(c *core.Context) (interface{}, *errs.Error) {
|
||||||
|
var credential models.TwoFactorLoginRequest
|
||||||
|
err := c.ShouldBindJSON(&credential)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.WarnfWithRequestId(c, "[authorizations.TwoFactorAuthorizeHandler] parse request failed, because %s", err.Error())
|
||||||
|
return nil, errs.ErrPasscodeInvalid
|
||||||
|
}
|
||||||
|
|
||||||
|
uid := c.GetCurrentUid()
|
||||||
|
twoFactorSetting, err := a.twoFactorAuthorizations.GetUserTwoFactorSettingByUid(uid)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.ErrorfWithRequestId(c, "[authorizations.TwoFactorAuthorizeHandler] failed to get two factor setting for user \"uid:%d\", because %s", uid, err.Error())
|
||||||
|
return nil, errs.Or(err, errs.ErrSystemError)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !totp.Validate(credential.Passcode, twoFactorSetting.Secret) {
|
||||||
|
log.WarnfWithRequestId(c, "[authorizations.TwoFactorAuthorizeHandler] passcode is invalid for user \"uid:%d\"", uid)
|
||||||
|
return nil, errs.ErrPasscodeInvalid
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := a.users.GetUserById(uid)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.ErrorfWithRequestId(c, "[authorizations.TwoFactorAuthorizeHandler] failed to get user \"uid:%d\" info, because %s", user.Uid, err.Error())
|
||||||
|
return nil, errs.ErrUserNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
oldTokenClaims := c.GetTokenClaims()
|
||||||
|
err = a.tokens.DeleteTokenByClaims(oldTokenClaims)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.WarnfWithRequestId(c, "[authorizations.TwoFactorAuthorizeHandler] failed to revoke temporary token \"utid:%s\" for user \"uid:%d\", because %s", oldTokenClaims.UserTokenId, user.Uid, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
token, claims, err := a.tokens.CreateToken(user, c)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.ErrorfWithRequestId(c, "[authorizations.TwoFactorAuthorizeHandler] failed to create token for user \"uid:%d\", because %s", user.Uid, err.Error())
|
||||||
|
return nil, errs.ErrTokenGenerating
|
||||||
|
}
|
||||||
|
|
||||||
|
c.SetTokenClaims(claims)
|
||||||
|
|
||||||
|
log.InfofWithRequestId(c, "[authorizations.TwoFactorAuthorizeHandler] user \"uid:%d\" has authorized two factor via passcode, token will be expired at %d", user.Uid, claims.ExpiresAt)
|
||||||
|
return token, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AuthorizationsApi) TwoFactorAuthorizeByRecoveryCodeHandler(c *core.Context) (interface{}, *errs.Error) {
|
||||||
|
var credential models.TwoFactorRecoveryCodeLoginRequest
|
||||||
|
err := c.ShouldBindJSON(&credential)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.WarnfWithRequestId(c, "[authorizations.TwoFactorAuthorizeByRecoveryCodeHandler] parse request failed, because %s", err.Error())
|
||||||
|
return nil, errs.ErrTwoFactorRecoveryCodeInvalid
|
||||||
|
}
|
||||||
|
|
||||||
|
uid := c.GetCurrentUid()
|
||||||
|
enableTwoFactor, err := a.twoFactorAuthorizations.ExistsTwoFactorSetting(uid)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.WarnfWithRequestId(c, "[authorizations.TwoFactorAuthorizeByRecoveryCodeHandler] failed to get two factor setting for user \"uid:%d\", because %s", uid, err.Error())
|
||||||
|
return nil, errs.Or(err, errs.ErrSystemError)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !enableTwoFactor {
|
||||||
|
return nil, errs.ErrTwoFactorKeyIsNotEnabled
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := a.users.GetUserById(uid)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.ErrorfWithRequestId(c, "[authorizations.TwoFactorAuthorizeByRecoveryCodeHandler] failed to get user \"uid:%d\" info, because %s", user.Uid, err.Error())
|
||||||
|
return nil, errs.ErrUserNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
err = a.twoFactorAuthorizations.GetAndUseUserTwoFactorRecoveryCode(uid, credential.RecoveryCode, user.Salt)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.WarnfWithRequestId(c, "[authorizations.TwoFactorAuthorizeByRecoveryCodeHandler] failed to get two factor recovery code for user \"uid:%d\", because %s", uid, err.Error())
|
||||||
|
return nil, errs.Or(err, errs.ErrTwoFactorRecoveryCodeNotExist)
|
||||||
|
}
|
||||||
|
|
||||||
|
oldTokenClaims := c.GetTokenClaims()
|
||||||
|
err = a.tokens.DeleteTokenByClaims(oldTokenClaims)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.WarnfWithRequestId(c, "[authorizations.TwoFactorAuthorizeByRecoveryCodeHandler] failed to revoke temporary token \"utid:%s\" for user \"uid:%d\", because %s", oldTokenClaims.UserTokenId, user.Uid, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
token, claims, err := a.tokens.CreateToken(user, c)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.ErrorfWithRequestId(c, "[authorizations.TwoFactorAuthorizeByRecoveryCodeHandler] failed to create token for user \"uid:%d\", because %s", user.Uid, err.Error())
|
||||||
|
return nil, errs.ErrTokenGenerating
|
||||||
|
}
|
||||||
|
|
||||||
|
c.SetTokenClaims(claims)
|
||||||
|
|
||||||
|
log.InfofWithRequestId(c, "[authorizations.TwoFactorAuthorizeByRecoveryCodeHandler] user \"uid:%d\" has authorized two factor via recovery code \"%s\", token will be expired at %d", user.Uid, credential.RecoveryCode, claims.ExpiresAt)
|
||||||
|
return token, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/mayswind/lab/pkg/core"
|
||||||
|
"github.com/mayswind/lab/pkg/errs"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DefaultApi struct {}
|
||||||
|
|
||||||
|
var (
|
||||||
|
Default = &DefaultApi{}
|
||||||
|
)
|
||||||
|
|
||||||
|
func (a *DefaultApi) ApiNotFound(c *core.Context) (interface{}, *errs.Error) {
|
||||||
|
return nil, errs.ErrApiNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *DefaultApi) MethodNotAllowed(c *core.Context) (interface{}, *errs.Error) {
|
||||||
|
return nil, errs.ErrMethodNotAllowed
|
||||||
|
}
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/mayswind/lab/pkg/core"
|
||||||
|
"github.com/mayswind/lab/pkg/errs"
|
||||||
|
"github.com/mayswind/lab/pkg/log"
|
||||||
|
"github.com/mayswind/lab/pkg/models"
|
||||||
|
"github.com/mayswind/lab/pkg/services"
|
||||||
|
"github.com/mayswind/lab/pkg/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TokensApi struct {
|
||||||
|
tokens *services.TokenService
|
||||||
|
users *services.UserService
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
Tokens = &TokensApi{
|
||||||
|
tokens: services.Tokens,
|
||||||
|
users: services.Users,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func (a *TokensApi) TokenListHandler(c *core.Context) (interface{}, *errs.Error) {
|
||||||
|
uid := c.GetCurrentUid()
|
||||||
|
tokens, err := a.tokens.GetAllTokensByUid(uid)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.ErrorfWithRequestId(c, "[tokens.TokenListHandler] failed to get all tokens for user \"uid:%d\", because %s", uid, err.Error())
|
||||||
|
return nil, errs.ErrOperationFailed
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenResps := make([]*models.TokenInfoResponse, len(tokens))
|
||||||
|
claims := c.GetTokenClaims()
|
||||||
|
|
||||||
|
for i := 0; i < len(tokens); i++ {
|
||||||
|
token := tokens[i]
|
||||||
|
tokenResp := &models.TokenInfoResponse{
|
||||||
|
TokenId: a.tokens.GenerateTokenId(token),
|
||||||
|
TokenType: token.TokenType,
|
||||||
|
UserAgent: token.UserAgent,
|
||||||
|
CreatedAt: token.CreatedUnixTime,
|
||||||
|
ExpiredAt: token.ExpiredUnixTime,
|
||||||
|
}
|
||||||
|
|
||||||
|
if utils.Int64ToString(token.Uid) == claims.Id && utils.Int64ToString(token.UserTokenId) == claims.UserTokenId && token.CreatedUnixTime == claims.IssuedAt {
|
||||||
|
tokenResp.IsCurrent = true
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenResps[i] = tokenResp
|
||||||
|
}
|
||||||
|
|
||||||
|
return tokenResps, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *TokensApi) TokenRevokeHandler(c *core.Context) (interface{}, *errs.Error) {
|
||||||
|
var tokenRevokeReq models.TokenRevokeRequest
|
||||||
|
err := c.ShouldBindJSON(&tokenRevokeReq)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.WarnfWithRequestId(c, "[tokens.TokenRevokeHandler] parse request failed, because %s", err.Error())
|
||||||
|
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenRecord, err := a.tokens.ParseFromTokenId(tokenRevokeReq.TokenId)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if !errs.IsCustomError(err) {
|
||||||
|
log.ErrorfWithRequestId(c, "[token.TokenRevokeHandler] failed to parse token \"id:%s\", because %s", tokenRevokeReq.TokenId, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, errs.Or(err, errs.ErrInvalidTokenId)
|
||||||
|
}
|
||||||
|
|
||||||
|
uid := c.GetCurrentUid()
|
||||||
|
|
||||||
|
if tokenRecord.Uid != uid {
|
||||||
|
log.WarnfWithRequestId(c, "[token.TokenRevokeHandler] token \"id:%s\" is not owned by user \"uid:%d\"", tokenRevokeReq.TokenId, uid)
|
||||||
|
return nil, errs.ErrInvalidTokenId
|
||||||
|
}
|
||||||
|
|
||||||
|
err = a.tokens.DeleteToken(tokenRecord)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.ErrorfWithRequestId(c, "[token.TokenRevokeHandler] failed to revoke token \"id:%s\" for user \"uid:%d\", because %s", tokenRevokeReq.TokenId, uid, err.Error())
|
||||||
|
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.InfofWithRequestId(c, "[token.TokenRevokeHandler] user \"uid:%d\" has revoked token \"id:%s\"", uid, tokenRevokeReq.TokenId)
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *TokensApi) TokenRefreshHandler(c *core.Context) (interface{}, *errs.Error) {
|
||||||
|
uid := c.GetCurrentUid()
|
||||||
|
user, err := a.users.GetUserById(uid)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.WarnfWithRequestId(c, "[token.TokenRefreshHandler] failed to get user \"uid:%d\" info, because %s", uid, err.Error())
|
||||||
|
return nil, errs.ErrUserNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
token, claims, err := a.tokens.CreateToken(user, c)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.ErrorfWithRequestId(c, "[token.TokenRefreshHandler] failed to create token for user \"uid:%d\", because %s", user.Uid, err.Error())
|
||||||
|
return nil, errs.Or(err, errs.ErrTokenGenerating)
|
||||||
|
}
|
||||||
|
|
||||||
|
oldTokenClaims := c.GetTokenClaims()
|
||||||
|
err = a.tokens.DeleteTokenByClaims(oldTokenClaims)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.WarnfWithRequestId(c, "[token.TokenRefreshHandler] failed to revoke token \"id:%s\" for user \"uid:%d\", because %s", oldTokenClaims.UserTokenId, user.Uid, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
c.SetTokenClaims(claims)
|
||||||
|
|
||||||
|
log.InfofWithRequestId(c, "[token.TokenRefreshHandler] user \"uid:%d\" token refreshed, new token will be expired at %d", user.Uid, claims.ExpiresAt)
|
||||||
|
return token, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,278 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/base64"
|
||||||
|
"image/png"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/pquerna/otp/totp"
|
||||||
|
|
||||||
|
"github.com/mayswind/lab/pkg/core"
|
||||||
|
"github.com/mayswind/lab/pkg/errs"
|
||||||
|
"github.com/mayswind/lab/pkg/log"
|
||||||
|
"github.com/mayswind/lab/pkg/models"
|
||||||
|
"github.com/mayswind/lab/pkg/services"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TwoFactorAuthorizationsApi struct {
|
||||||
|
twoFactorAuthorizations *services.TwoFactorAuthorizationService
|
||||||
|
users *services.UserService
|
||||||
|
tokens *services.TokenService
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
TwoFactorAuthorizations = &TwoFactorAuthorizationsApi{
|
||||||
|
twoFactorAuthorizations: services.TwoFactorAuthorizations,
|
||||||
|
users: services.Users,
|
||||||
|
tokens: services.Tokens,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func (a *TwoFactorAuthorizationsApi) TwoFactorStatusHandler(c *core.Context) (interface{}, *errs.Error) {
|
||||||
|
uid := c.GetCurrentUid()
|
||||||
|
twoFactorSetting, err := a.twoFactorAuthorizations.GetUserTwoFactorSettingByUid(uid)
|
||||||
|
|
||||||
|
if err == errs.ErrTwoFactorKeyIsNotEnabled {
|
||||||
|
statusResp := &models.TwoFactorStatusResponse{
|
||||||
|
Enable: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
return statusResp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorStatusHandler] failed to get two factor setting, because %s", err.Error())
|
||||||
|
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||||
|
}
|
||||||
|
|
||||||
|
statusResp := &models.TwoFactorStatusResponse{
|
||||||
|
Enable: true,
|
||||||
|
CreatedAt: twoFactorSetting.CreatedUnixTime,
|
||||||
|
}
|
||||||
|
|
||||||
|
return statusResp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *TwoFactorAuthorizationsApi) TwoFactorEnableRequestHandler(c *core.Context) (interface{}, *errs.Error) {
|
||||||
|
uid := c.GetCurrentUid()
|
||||||
|
enabled, err := a.twoFactorAuthorizations.ExistsTwoFactorSetting(uid)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorEnableRequestHandler] failed to check two factor setting, because %s", err.Error())
|
||||||
|
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||||
|
}
|
||||||
|
|
||||||
|
if enabled {
|
||||||
|
return nil, errs.ErrTwoFactorKeyAlreadyEnabled
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := a.users.GetUserById(uid)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if !errs.IsCustomError(err) {
|
||||||
|
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorEnableRequestHandler] failed to get user, because %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, errs.ErrUserNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
key, err := a.twoFactorAuthorizations.GenerateTwoFactorSecret(user)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorEnableRequestHandler] failed to generate two factor secret, because %s", err.Error())
|
||||||
|
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||||
|
}
|
||||||
|
|
||||||
|
img, err := key.Image(240, 240)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorEnableRequestHandler] failed to generate two factor qrcode, because %s", err.Error())
|
||||||
|
return nil, errs.ErrOperationFailed
|
||||||
|
}
|
||||||
|
|
||||||
|
imgData := &bytes.Buffer{}
|
||||||
|
|
||||||
|
if err = png.Encode(imgData, img); err != nil {
|
||||||
|
return nil, errs.ErrOperationFailed
|
||||||
|
}
|
||||||
|
|
||||||
|
enableResp := &models.TwoFactorEnableResponse{
|
||||||
|
Secret: key.Secret(),
|
||||||
|
QRCode: "data:image/png;base64," + base64.StdEncoding.EncodeToString(imgData.Bytes()),
|
||||||
|
}
|
||||||
|
|
||||||
|
return enableResp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *TwoFactorAuthorizationsApi) TwoFactorEnableConfirmHandler(c *core.Context) (interface{}, *errs.Error) {
|
||||||
|
var confirmReq models.TwoFactorEnableConfirmRequest
|
||||||
|
err := c.ShouldBindJSON(&confirmReq)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.WarnfWithRequestId(c, "[twofactor_authorizations.TwoFactorEnableConfirmHandler] parse request failed, because %s", err.Error())
|
||||||
|
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
uid := c.GetCurrentUid()
|
||||||
|
exists, err := a.twoFactorAuthorizations.ExistsTwoFactorSetting(uid)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorEnableConfirmHandler] failed to check two factor setting, because %s", err.Error())
|
||||||
|
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||||
|
}
|
||||||
|
|
||||||
|
if exists {
|
||||||
|
return nil, errs.ErrTwoFactorKeyAlreadyEnabled
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := a.users.GetUserById(uid)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if !errs.IsCustomError(err) {
|
||||||
|
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorEnableConfirmHandler] failed to get user, because %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, errs.ErrUserNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
twoFactorSetting := &models.TwoFactor{
|
||||||
|
Uid: uid,
|
||||||
|
Secret: confirmReq.Secret,
|
||||||
|
}
|
||||||
|
|
||||||
|
if !totp.Validate(confirmReq.Passcode, confirmReq.Secret) {
|
||||||
|
log.WarnfWithRequestId(c, "[twofactor_authorizations.TwoFactorEnableConfirmHandler] passcode is invalid")
|
||||||
|
return nil, errs.ErrPasscodeInvalid
|
||||||
|
}
|
||||||
|
|
||||||
|
recoveryCodes, err := a.twoFactorAuthorizations.GenerateTwoFactorRecoveryCodes()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorEnableConfirmHandler] failed to generate two factor recovery codes for user \"uid:%d\", because %s", uid, err.Error())
|
||||||
|
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = a.twoFactorAuthorizations.CreateTwoFactorRecoveryCodes(uid, recoveryCodes, user.Salt)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorEnableConfirmHandler] failed to create two factor recovery codes for user \"uid:%d\", because %s", uid, err.Error())
|
||||||
|
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = a.twoFactorAuthorizations.CreateTwoFactorSetting(twoFactorSetting)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorEnableConfirmHandler] failed to create two factor setting for user \"uid:%d\", because %s", uid, err.Error())
|
||||||
|
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.InfofWithRequestId(c, "[twofactor_authorizations.TwoFactorEnableConfirmHandler] user \"uid:%d\" has enabled two factor authorization", uid)
|
||||||
|
|
||||||
|
now := time.Now().Unix()
|
||||||
|
err = a.tokens.DeleteTokensBeforeTime(uid, now)
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
log.InfofWithRequestId(c, "[twofactor_authorizations.TwoFactorEnableConfirmHandler] revoke old tokens before unix time \"%d\" for user \"uid:%d\"", now, user.Uid)
|
||||||
|
} else {
|
||||||
|
log.WarnfWithRequestId(c, "[twofactor_authorizations.TwoFactorEnableConfirmHandler] failed to revoke old tokens for user \"uid:%d\", because %s", user.Uid, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
token, claims, err := a.tokens.CreateToken(user, c)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.WarnfWithRequestId(c, "[twofactor_authorizations.TwoFactorEnableConfirmHandler] failed to create token for user \"uid:%d\", because %s", user.Uid, err.Error())
|
||||||
|
|
||||||
|
confirmResp := &models.TwoFactorEnableConfirmResponse{
|
||||||
|
RecoveryCodes: recoveryCodes,
|
||||||
|
}
|
||||||
|
|
||||||
|
return confirmResp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
c.SetTokenClaims(claims)
|
||||||
|
|
||||||
|
log.InfofWithRequestId(c, "[twofactor_authorizations.TwoFactorEnableConfirmHandler] user \"uid:%d\" token refreshed, new token will be expired at %d", user.Uid, claims.ExpiresAt)
|
||||||
|
|
||||||
|
confirmResp := &models.TwoFactorEnableConfirmResponse{
|
||||||
|
Token: token,
|
||||||
|
RecoveryCodes: recoveryCodes,
|
||||||
|
}
|
||||||
|
|
||||||
|
return confirmResp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *TwoFactorAuthorizationsApi) TwoFactorDisableHandler(c *core.Context) (interface{}, *errs.Error) {
|
||||||
|
uid := c.GetCurrentUid()
|
||||||
|
enableTwoFactor, err := a.twoFactorAuthorizations.ExistsTwoFactorSetting(uid)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorDisableHandler] failed to check two factor setting, because %s", err.Error())
|
||||||
|
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !enableTwoFactor {
|
||||||
|
return nil, errs.ErrTwoFactorKeyIsNotEnabled
|
||||||
|
}
|
||||||
|
|
||||||
|
err = a.twoFactorAuthorizations.DeleteTwoFactorRecoveryCodes(uid)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorDisableHandler] failed to delete two factor recovery codes for user \"uid:%d\"", uid)
|
||||||
|
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = a.twoFactorAuthorizations.DeleteTwoFactorSetting(uid)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorDisableHandler] failed to delete two factor setting for user \"uid:%d\"", uid)
|
||||||
|
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.InfofWithRequestId(c, "[twofactor_authorizations.TwoFactorDisableHandler] user \"uid:%d\" has disabled two factor authorization", uid)
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *TwoFactorAuthorizationsApi) TwoFactorRecoveryCodeRegenerateHandler(c *core.Context) (interface{}, *errs.Error) {
|
||||||
|
uid := c.GetCurrentUid()
|
||||||
|
enableTwoFactor, err := a.twoFactorAuthorizations.ExistsTwoFactorSetting(uid)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorRecoveryCodeRegenerateHandler] failed to check two factor setting, because %s", err.Error())
|
||||||
|
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !enableTwoFactor {
|
||||||
|
return nil, errs.ErrTwoFactorKeyIsNotEnabled
|
||||||
|
}
|
||||||
|
|
||||||
|
recoveryCodes, err := a.twoFactorAuthorizations.GenerateTwoFactorRecoveryCodes()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorRecoveryCodeRegenerateHandler] failed to generate two factor recovery codes for user \"uid:%d\", because %s", uid, err.Error())
|
||||||
|
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := a.users.GetUserById(uid)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.WarnfWithRequestId(c, "[twofactor_authorizations.TwoFactorRecoveryCodeRegenerateHandler] failed to get user for user \"uid:%d\", because %s", uid, err.Error())
|
||||||
|
return nil, errs.ErrUserNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
err = a.twoFactorAuthorizations.CreateTwoFactorRecoveryCodes(uid, recoveryCodes, user.Salt)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.ErrorfWithRequestId(c, "[twofactor_authorizations.TwoFactorRecoveryCodeRegenerateHandler] failed to create two factor recovery codes for user \"uid:%d\", because %s", uid, err.Error())
|
||||||
|
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||||
|
}
|
||||||
|
|
||||||
|
recoveryCodesResp := &models.TwoFactorEnableConfirmResponse{
|
||||||
|
RecoveryCodes: recoveryCodes,
|
||||||
|
}
|
||||||
|
|
||||||
|
log.InfofWithRequestId(c, "[twofactor_authorizations.TwoFactorRecoveryCodeRegenerateHandler] user \"uid:%d\" has regenerated two factor recovery codes", uid)
|
||||||
|
|
||||||
|
return recoveryCodesResp, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,179 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/mayswind/lab/pkg/core"
|
||||||
|
"github.com/mayswind/lab/pkg/errs"
|
||||||
|
"github.com/mayswind/lab/pkg/log"
|
||||||
|
"github.com/mayswind/lab/pkg/models"
|
||||||
|
"github.com/mayswind/lab/pkg/services"
|
||||||
|
"github.com/mayswind/lab/pkg/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UsersApi struct {
|
||||||
|
users *services.UserService
|
||||||
|
tokens *services.TokenService
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
Users = &UsersApi{
|
||||||
|
users: services.Users,
|
||||||
|
tokens: services.Tokens,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func (a *UsersApi) UserRegisterHandler(c *core.Context) (interface{}, *errs.Error) {
|
||||||
|
var userRegisterReq models.UserRegisterRequest
|
||||||
|
err := c.ShouldBindJSON(&userRegisterReq)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.WarnfWithRequestId(c, "[users.UserRegisterHandler] parse request failed, because %s", err.Error())
|
||||||
|
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
userRegisterReq.Username = strings.TrimSpace(userRegisterReq.Username)
|
||||||
|
userRegisterReq.Email = strings.TrimSpace(userRegisterReq.Email)
|
||||||
|
userRegisterReq.Nickname = strings.TrimSpace(userRegisterReq.Nickname)
|
||||||
|
|
||||||
|
user := &models.User{
|
||||||
|
Username: userRegisterReq.Username,
|
||||||
|
Email: userRegisterReq.Email,
|
||||||
|
Nickname: userRegisterReq.Nickname,
|
||||||
|
Password: userRegisterReq.Password,
|
||||||
|
}
|
||||||
|
|
||||||
|
err = a.users.CreateUser(user)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.ErrorfWithRequestId(c, "[users.UserRegisterHandler] failed to create user \"%s\", because %s", user.Username, err.Error())
|
||||||
|
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.InfofWithRequestId(c, "[users.UserRegisterHandler] user \"%s\" has registered successfully, uid is %d", user.Username, user.Uid)
|
||||||
|
|
||||||
|
token, claims, err := a.tokens.CreateToken(user, c)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.WarnfWithRequestId(c, "[users.UserRegisterHandler] failed to create token for user \"uid:%d\", because %s", user.Uid, err.Error())
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
c.SetTokenClaims(claims)
|
||||||
|
|
||||||
|
log.InfofWithRequestId(c, "[users.UserRegisterHandler] user \"uid:%d\" has logined, token will be expired at %d", user.Uid, claims.ExpiresAt)
|
||||||
|
return token, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *UsersApi) UserProfileHandler(c *core.Context) (interface{}, *errs.Error) {
|
||||||
|
uid := c.GetCurrentUid()
|
||||||
|
user, err := a.users.GetUserById(uid)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if !errs.IsCustomError(err) {
|
||||||
|
log.ErrorfWithRequestId(c, "[users.UserRegisterHandler] failed to get user, because %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, errs.ErrUserNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
userResp := &models.UserProfileResponse{
|
||||||
|
Uid : utils.Int64ToString(user.Uid),
|
||||||
|
Username: user.Username,
|
||||||
|
Email: user.Email,
|
||||||
|
Nickname: user.Nickname,
|
||||||
|
Type: user.Type,
|
||||||
|
CreatedAt: user.CreatedUnixTime,
|
||||||
|
UpdatedAt: user.UpdatedUnixTime,
|
||||||
|
LastLoginAt: user.LastLoginUnixTime,
|
||||||
|
}
|
||||||
|
|
||||||
|
return userResp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *UsersApi) UserUpdateProfileHandler(c *core.Context) (interface{}, *errs.Error) {
|
||||||
|
var userUpdateReq models.UserProfileUpdateRequest
|
||||||
|
err := c.ShouldBindJSON(&userUpdateReq)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.WarnfWithRequestId(c, "[users.UserUpdateProfileHandler] parse request failed, because %s", err.Error())
|
||||||
|
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
uid := c.GetCurrentUid()
|
||||||
|
user, err := a.users.GetUserById(uid)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if !errs.IsCustomError(err) {
|
||||||
|
log.ErrorfWithRequestId(c, "[users.UserUpdateProfileHandler] failed to get user, because %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, errs.ErrUserNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
userUpdateReq.Email = strings.TrimSpace(userUpdateReq.Email)
|
||||||
|
userUpdateReq.Nickname = strings.TrimSpace(userUpdateReq.Nickname)
|
||||||
|
|
||||||
|
anythingUpdate := false
|
||||||
|
|
||||||
|
if userUpdateReq.Email != "" && userUpdateReq.Email != user.Email {
|
||||||
|
anythingUpdate = true
|
||||||
|
} else {
|
||||||
|
userUpdateReq.Email = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
if userUpdateReq.Password != "" && !a.users.IsPasswordEqualsUserPassword(userUpdateReq.Password, user) {
|
||||||
|
anythingUpdate = true
|
||||||
|
} else {
|
||||||
|
userUpdateReq.Password = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
if userUpdateReq.Nickname != "" && userUpdateReq.Nickname != user.Nickname {
|
||||||
|
anythingUpdate = true
|
||||||
|
} else {
|
||||||
|
userUpdateReq.Nickname = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
if !anythingUpdate {
|
||||||
|
return nil, errs.ErrNothingWillBeUpdated
|
||||||
|
}
|
||||||
|
|
||||||
|
user.Email = userUpdateReq.Email
|
||||||
|
user.Password = userUpdateReq.Password
|
||||||
|
user.Nickname = userUpdateReq.Nickname
|
||||||
|
|
||||||
|
keyProfileUpdated, err := a.users.UpdateUser(user)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.ErrorfWithRequestId(c, "[users.UserUpdateProfileHandler] failed to update user \"uid:%d\", because %s", user.Uid, err.Error())
|
||||||
|
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.InfofWithRequestId(c, "[users.UserUpdateProfileHandler] user \"uid:%d\" has updated successfully", user.Uid)
|
||||||
|
|
||||||
|
if keyProfileUpdated {
|
||||||
|
now := time.Now().Unix()
|
||||||
|
err = a.tokens.DeleteTokensBeforeTime(uid, now)
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
log.InfofWithRequestId(c, "[users.UserUpdateProfileHandler] revoke old tokens before unix time \"%d\" for user \"uid:%d\"", now, user.Uid)
|
||||||
|
} else {
|
||||||
|
log.WarnfWithRequestId(c, "[users.UserUpdateProfileHandler] failed to revoke old tokens for user \"uid:%d\", because %s", user.Uid, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
token, claims, err := a.tokens.CreateToken(user, c)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.WarnfWithRequestId(c, "[users.UserUpdateProfileHandler] failed to create token for user \"uid:%d\", because %s", user.Uid, err.Error())
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
c.SetTokenClaims(claims)
|
||||||
|
|
||||||
|
log.InfofWithRequestId(c, "[users.UserUpdateProfileHandler] user \"uid:%d\" token refreshed, new token will be expired at %d", user.Uid, claims.ExpiresAt)
|
||||||
|
return token, nil
|
||||||
|
} else {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -71,3 +71,8 @@ func Or(err error, defaultErr *Error) *Error {
|
|||||||
return defaultErr
|
return defaultErr
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func IsCustomError(err error) bool {
|
||||||
|
_, ok := err.(*Error);
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
package middlewares
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/mayswind/lab/pkg/core"
|
||||||
|
"github.com/mayswind/lab/pkg/errs"
|
||||||
|
"github.com/mayswind/lab/pkg/log"
|
||||||
|
"github.com/mayswind/lab/pkg/services"
|
||||||
|
"github.com/mayswind/lab/pkg/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
func JWTAuthorization(c *core.Context) {
|
||||||
|
claims, err := getTokenClaims(c)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
utils.PrintErrorResult(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if claims.Type == core.USER_TOKEN_TYPE_REQUIRE_2FA {
|
||||||
|
log.WarnfWithRequestId(c, "[authorization.JWTAuthorization] user \"uid:%s\" token requires 2fa", claims.Id)
|
||||||
|
utils.PrintErrorResult(c, errs.ErrTokenRequire2FA)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if claims.Type != core.USER_TOKEN_TYPE_NORMAL {
|
||||||
|
log.WarnfWithRequestId(c, "[authorization.JWTAuthorization] user \"uid:%s\" token type is invalid", claims.Id)
|
||||||
|
utils.PrintErrorResult(c, errs.ErrInvalidTokenType)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.SetTokenClaims(claims)
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
|
||||||
|
func JWTTwoFactorAuthorization(c *core.Context) {
|
||||||
|
claims, err := getTokenClaims(c)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
utils.PrintErrorResult(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if claims.Type != core.USER_TOKEN_TYPE_REQUIRE_2FA {
|
||||||
|
log.WarnfWithRequestId(c, "[authorization.JWTTwoFactorAuthorization] user \"uid:%s\" token is not need two factor authorization", claims.Id)
|
||||||
|
utils.PrintErrorResult(c, errs.ErrTokenNotRequire2FA)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.SetTokenClaims(claims)
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
|
||||||
|
func getTokenClaims(c *core.Context) (*core.UserTokenClaims, *errs.Error) {
|
||||||
|
token, claims, err := services.Tokens.ParseToken(c)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.WarnfWithRequestId(c, "[authorization.getTokenClaims] failed to parse token, because %s", err.Error())
|
||||||
|
return nil, errs.ErrUnauthorizedAccess
|
||||||
|
}
|
||||||
|
|
||||||
|
if !token.Valid {
|
||||||
|
log.WarnfWithRequestId(c, "[authorization.getTokenClaims] token is invalid")
|
||||||
|
return nil, errs.ErrInvalidToken
|
||||||
|
}
|
||||||
|
|
||||||
|
if !claims.VerifyExpiresAt(time.Now().Unix(), true) {
|
||||||
|
log.WarnfWithRequestId(c, "[authorization.getTokenClaims] token is expired")
|
||||||
|
return nil, errs.ErrTokenExpired
|
||||||
|
}
|
||||||
|
|
||||||
|
if claims.Id == "" {
|
||||||
|
log.WarnfWithRequestId(c, "[authorization.getTokenClaims] user id in token is empty")
|
||||||
|
return nil, errs.ErrInvalidToken
|
||||||
|
}
|
||||||
|
|
||||||
|
return claims, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import "github.com/mayswind/lab/pkg/core"
|
||||||
|
|
||||||
|
const TOKEN_USER_AGENT_MAX_LENGTH = 255
|
||||||
|
|
||||||
|
type TokenRecord struct {
|
||||||
|
Uid int64 `xorm:"PK"`
|
||||||
|
UserTokenId int64 `xorm:"PK"`
|
||||||
|
TokenType core.TokenType `xorm:"TINYINT NOT NULL"`
|
||||||
|
Secret string `xorm:"VARCHAR(10) NOT NULL"`
|
||||||
|
UserAgent string `xorm:"VARCHAR(255)"`
|
||||||
|
CreatedUnixTime int64 `xorm:"PK"`
|
||||||
|
ExpiredUnixTime int64
|
||||||
|
}
|
||||||
|
|
||||||
|
type TokenRevokeRequest struct {
|
||||||
|
TokenId string `json:"tokenId" binding:"required,notBlank"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TokenInfoResponse struct {
|
||||||
|
TokenId string `json:"tokenId"`
|
||||||
|
TokenType core.TokenType `json:"tokenType"`
|
||||||
|
UserAgent string `json:"userAgent"`
|
||||||
|
CreatedAt int64 `json:"createdAt"`
|
||||||
|
ExpiredAt int64 `json:"expiredAt"`
|
||||||
|
IsCurrent bool `json:"isCurrent"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
type TwoFactor struct {
|
||||||
|
Uid int64 `xorm:"PK"`
|
||||||
|
Secret string `xorm:"VARCHAR(80) NOT NULL"`
|
||||||
|
CreatedUnixTime int64
|
||||||
|
}
|
||||||
|
|
||||||
|
type TwoFactorLoginRequest struct {
|
||||||
|
Passcode string `json:"passcode" binding:"required,notBlank,len=6"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TwoFactorEnableConfirmRequest struct {
|
||||||
|
Secret string `json:"secret" binding:"required,notBlank,len=32"`
|
||||||
|
Passcode string `json:"passcode" binding:"required,notBlank,len=6"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TwoFactorEnableResponse struct {
|
||||||
|
Secret string `json:"secret"`
|
||||||
|
QRCode string `json:"qrcode"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TwoFactorEnableConfirmResponse struct {
|
||||||
|
Token string `json:"token,omitempty"`
|
||||||
|
RecoveryCodes []string `json:"recoveryCodes"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TwoFactorStatusResponse struct {
|
||||||
|
Enable bool `json:"enable"`
|
||||||
|
CreatedAt int64 `json:"createdAt,omitempty"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
type TwoFactorRecoveryCode struct {
|
||||||
|
Uid int64 `xorm:"PK"`
|
||||||
|
RecoveryCode string `xorm:"VARCHAR(64) PK"`
|
||||||
|
Used bool `xorm:"NOT NULL"`
|
||||||
|
CreatedUnixTime int64
|
||||||
|
UsedUnixTime int64
|
||||||
|
}
|
||||||
|
|
||||||
|
type TwoFactorRecoveryCodeLoginRequest struct {
|
||||||
|
RecoveryCode string `json:"recoveryCode" binding:"required,notBlank,len=11"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,282 @@
|
|||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/dgrijalva/jwt-go"
|
||||||
|
"github.com/dgrijalva/jwt-go/request"
|
||||||
|
"xorm.io/xorm"
|
||||||
|
|
||||||
|
"github.com/mayswind/lab/pkg/core"
|
||||||
|
"github.com/mayswind/lab/pkg/datastore"
|
||||||
|
"github.com/mayswind/lab/pkg/errs"
|
||||||
|
"github.com/mayswind/lab/pkg/log"
|
||||||
|
"github.com/mayswind/lab/pkg/models"
|
||||||
|
"github.com/mayswind/lab/pkg/settings"
|
||||||
|
"github.com/mayswind/lab/pkg/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TokenService struct {
|
||||||
|
ServiceUsingDB
|
||||||
|
ServiceUsingConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
Tokens = &TokenService{
|
||||||
|
ServiceUsingDB: ServiceUsingDB{
|
||||||
|
container: datastore.Container,
|
||||||
|
},
|
||||||
|
ServiceUsingConfig: ServiceUsingConfig{
|
||||||
|
container: settings.Container,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *TokenService) GetAllTokensByUid(uid int64) ([]*models.TokenRecord, error) {
|
||||||
|
if uid <= 0 {
|
||||||
|
return nil, errs.ErrUserIdInvalid
|
||||||
|
}
|
||||||
|
|
||||||
|
var tokenRecords []*models.TokenRecord
|
||||||
|
err := s.TokenDB(uid).Cols("uid", "user_token_id", "token_type", "user_agent", "created_unix_time", "expired_unix_time").Where("uid=?", uid).OrderBy("created_unix_time desc").Find(&tokenRecords)
|
||||||
|
|
||||||
|
return tokenRecords, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *TokenService) ParseToken(c *core.Context) (*jwt.Token, *core.UserTokenClaims, error) {
|
||||||
|
claims := &core.UserTokenClaims{}
|
||||||
|
|
||||||
|
token, err := request.ParseFromRequest(c.Request, request.AuthorizationHeaderExtractor,
|
||||||
|
func (token *jwt.Token) (interface{}, error) {
|
||||||
|
uid, err := utils.StringToInt64(claims.Id)
|
||||||
|
now := time.Now().Unix()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.WarnfWithRequestId(c, "[tokens.ParseToken] user \"uid:%s\" in token is invalid, because %s", claims.Id, err.Error())
|
||||||
|
return nil, errs.ErrInvalidToken
|
||||||
|
}
|
||||||
|
|
||||||
|
userTokenId, err := utils.StringToInt64(claims.UserTokenId)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.WarnfWithRequestId(c, "[tokens.ParseToken] token \"utid:%s\" in token of user \"uid:%s\" is invalid, because %s", claims.UserTokenId, claims.Id, err.Error())
|
||||||
|
return nil, errs.ErrInvalidUserTokenId
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenRecord, err := s.getTokenRecord(uid, userTokenId, claims.IssuedAt)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.WarnfWithRequestId(c, "[tokens.ParseToken] token \"utid:%s\" of user \"uid:%s\" record not found, because %s", claims.UserTokenId, claims.Id, err.Error())
|
||||||
|
return nil, errs.ErrTokenRecordNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
if tokenRecord.ExpiredUnixTime < now {
|
||||||
|
log.WarnfWithRequestId(c, "[tokens.ParseToken] token \"utid:%s\" of user \"uid:%s\" record is expired", claims.UserTokenId, claims.Id)
|
||||||
|
return nil, errs.ErrTokenExpired
|
||||||
|
}
|
||||||
|
|
||||||
|
return []byte(tokenRecord.Secret), nil
|
||||||
|
}, request.WithClaims(claims))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return token, claims, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *TokenService) CreateToken(user *models.User, ctx *core.Context) (string, *core.UserTokenClaims, error) {
|
||||||
|
return s.createToken(user, core.USER_TOKEN_TYPE_NORMAL, s.getUserAgent(ctx), s.CurrentConfig().TokenExpiredTimeDuration)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *TokenService) CreateRequire2FAToken(user *models.User, ctx *core.Context) (string, *core.UserTokenClaims, error) {
|
||||||
|
return s.createToken(user, core.USER_TOKEN_TYPE_REQUIRE_2FA, s.getUserAgent(ctx), s.CurrentConfig().TemporaryTokenExpiredTimeDuration)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *TokenService) DeleteToken(tokenRecord *models.TokenRecord) error {
|
||||||
|
if tokenRecord.Uid <= 0 {
|
||||||
|
return errs.ErrUserIdInvalid
|
||||||
|
}
|
||||||
|
|
||||||
|
if tokenRecord.UserTokenId <= 0 {
|
||||||
|
return errs.ErrInvalidUserTokenId
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.TokenDB(tokenRecord.Uid).DoTranscation(func(sess *xorm.Session) error {
|
||||||
|
deletedRows, err := sess.Where("uid=? AND user_token_id=? AND created_unix_time=?", tokenRecord.Uid, tokenRecord.UserTokenId, tokenRecord.CreatedUnixTime).Delete(&models.TokenRecord{})
|
||||||
|
|
||||||
|
if deletedRows < 1 {
|
||||||
|
return errs.ErrTokenRecordNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *TokenService) DeleteTokenByClaims(claims *core.UserTokenClaims) error {
|
||||||
|
uid, err := utils.StringToInt64(claims.Id)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return errs.ErrUserIdInvalid
|
||||||
|
}
|
||||||
|
|
||||||
|
userTokenId, err := utils.StringToInt64(claims.UserTokenId)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return errs.ErrInvalidUserTokenId
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.DeleteToken(&models.TokenRecord{Uid: uid, UserTokenId: userTokenId, CreatedUnixTime: claims.IssuedAt})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *TokenService) DeleteTokensBeforeTime(uid int64, expireTime int64) error {
|
||||||
|
if uid <= 0 {
|
||||||
|
return errs.ErrUserIdInvalid
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.TokenDB(uid).DoTranscation(func(sess *xorm.Session) error {
|
||||||
|
_, err := sess.Where("uid=? AND created_unix_time<?", uid, expireTime).Delete(&models.TokenRecord{})
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *TokenService) ParseFromTokenId(tokenId string) (*models.TokenRecord, error) {
|
||||||
|
pairs := strings.Split(tokenId, ":")
|
||||||
|
|
||||||
|
if len(pairs) != 3 {
|
||||||
|
return nil, errs.ErrInvalidTokenId
|
||||||
|
}
|
||||||
|
|
||||||
|
uid, err := utils.StringToInt64(pairs[0])
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, errs.ErrInvalidTokenId
|
||||||
|
}
|
||||||
|
|
||||||
|
createdUnixTime, err := utils.StringToInt64(pairs[1])
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, errs.ErrInvalidTokenId
|
||||||
|
}
|
||||||
|
|
||||||
|
userTokenId, err := utils.StringToInt64(pairs[2])
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, errs.ErrInvalidTokenId
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenRecord := &models.TokenRecord{
|
||||||
|
Uid: uid,
|
||||||
|
UserTokenId: userTokenId,
|
||||||
|
CreatedUnixTime: createdUnixTime,
|
||||||
|
}
|
||||||
|
|
||||||
|
return tokenRecord, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *TokenService) GenerateTokenId(tokenRecord *models.TokenRecord) string {
|
||||||
|
return fmt.Sprintf("%d:%d:%d", tokenRecord.Uid, tokenRecord.CreatedUnixTime, tokenRecord.UserTokenId)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *TokenService) createToken(user *models.User, tokenType core.TokenType, userAgent string, expiryDate time.Duration) (string, *core.UserTokenClaims, error) {
|
||||||
|
var err error
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
tokenRecord := &models.TokenRecord{
|
||||||
|
Uid: user.Uid,
|
||||||
|
UserTokenId: s.getUserTokenId(),
|
||||||
|
TokenType: tokenType,
|
||||||
|
UserAgent: userAgent,
|
||||||
|
CreatedUnixTime: now.Unix(),
|
||||||
|
ExpiredUnixTime: now.Add(expiryDate).Unix(),
|
||||||
|
}
|
||||||
|
|
||||||
|
if tokenRecord.Secret, err = utils.GetRandomString(10); err != nil {
|
||||||
|
return "", nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
claims := &core.UserTokenClaims{
|
||||||
|
UserTokenId: utils.Int64ToString(tokenRecord.UserTokenId),
|
||||||
|
Username: user.Username,
|
||||||
|
Type: tokenRecord.TokenType,
|
||||||
|
StandardClaims: jwt.StandardClaims{
|
||||||
|
Id: utils.Int64ToString(tokenRecord.Uid),
|
||||||
|
IssuedAt: tokenRecord.CreatedUnixTime,
|
||||||
|
ExpiresAt: tokenRecord.ExpiredUnixTime,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
jwtToken := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||||
|
tokenString, err := jwtToken.SignedString([]byte(tokenRecord.Secret))
|
||||||
|
|
||||||
|
err = s.createTokenRecord(tokenRecord)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return tokenString, claims, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *TokenService) getTokenRecord(uid int64, userTokenId int64, createUnixTime int64) (*models.TokenRecord, error) {
|
||||||
|
if uid <= 0 {
|
||||||
|
return nil, errs.ErrUserIdInvalid
|
||||||
|
}
|
||||||
|
|
||||||
|
if userTokenId <= 0 {
|
||||||
|
return nil, errs.ErrInvalidUserTokenId
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenRecord := &models.TokenRecord{}
|
||||||
|
has, err := s.TokenDB(uid).Where("uid=? AND user_token_id=? AND created_unix_time=?", uid, userTokenId, createUnixTime).Limit(1).Get(tokenRecord)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !has {
|
||||||
|
return nil, errs.ErrTokenRecordNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
return tokenRecord, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *TokenService) createTokenRecord(tokenRecord *models.TokenRecord) error {
|
||||||
|
if tokenRecord.Uid <= 0 {
|
||||||
|
return errs.ErrUserIdInvalid
|
||||||
|
}
|
||||||
|
|
||||||
|
if tokenRecord.UserTokenId <= 0 {
|
||||||
|
return errs.ErrInvalidUserTokenId
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.TokenDB(tokenRecord.Uid).DoTranscation(func(sess *xorm.Session) error {
|
||||||
|
_, err := sess.Insert(tokenRecord)
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *TokenService) getUserTokenId() int64 {
|
||||||
|
nanoSeconds := time.Now().Nanosecond()
|
||||||
|
randomNumber, _ := utils.GetRandomInteger(math.MaxInt32)
|
||||||
|
userTokenId := (int64(nanoSeconds) << 32) | int64(randomNumber)
|
||||||
|
|
||||||
|
return userTokenId
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *TokenService) getUserAgent(ctx *core.Context) string {
|
||||||
|
userAgent := ""
|
||||||
|
|
||||||
|
if ctx != nil && ctx.Request != nil {
|
||||||
|
userAgent = ctx.Request.UserAgent()
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(userAgent) > models.TOKEN_USER_AGENT_MAX_LENGTH {
|
||||||
|
userAgent = utils.SubString(userAgent, 0, models.TOKEN_USER_AGENT_MAX_LENGTH)
|
||||||
|
}
|
||||||
|
|
||||||
|
return userAgent
|
||||||
|
}
|
||||||
@@ -0,0 +1,204 @@
|
|||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
"xorm.io/xorm"
|
||||||
|
|
||||||
|
"github.com/pquerna/otp"
|
||||||
|
"github.com/pquerna/otp/totp"
|
||||||
|
|
||||||
|
"github.com/mayswind/lab/pkg/datastore"
|
||||||
|
"github.com/mayswind/lab/pkg/errs"
|
||||||
|
"github.com/mayswind/lab/pkg/models"
|
||||||
|
"github.com/mayswind/lab/pkg/settings"
|
||||||
|
"github.com/mayswind/lab/pkg/utils"
|
||||||
|
"github.com/mayswind/lab/pkg/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
TWOFACTOR_PERIOD uint = 30 // seconds
|
||||||
|
TWOFACTOR_SECRET_SIZE uint = 20 // bytes
|
||||||
|
TWOFACTOR_RECOVERY_CODE_COUNT int = 10
|
||||||
|
TWOFACTOR_RECOVERY_CODE_LENGTH int = 10 // bytes
|
||||||
|
)
|
||||||
|
|
||||||
|
type TwoFactorAuthorizationService struct {
|
||||||
|
ServiceUsingDB
|
||||||
|
ServiceUsingConfig
|
||||||
|
ServiceUsingUuid
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
TwoFactorAuthorizations = &TwoFactorAuthorizationService{
|
||||||
|
ServiceUsingDB: ServiceUsingDB{
|
||||||
|
container: datastore.Container,
|
||||||
|
},
|
||||||
|
ServiceUsingConfig: ServiceUsingConfig{
|
||||||
|
container: settings.Container,
|
||||||
|
},
|
||||||
|
ServiceUsingUuid: ServiceUsingUuid{
|
||||||
|
container: uuid.Container,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *TwoFactorAuthorizationService) GetUserTwoFactorSettingByUid(uid int64) (*models.TwoFactor, error) {
|
||||||
|
if uid <= 0 {
|
||||||
|
return nil, errs.ErrUserIdInvalid
|
||||||
|
}
|
||||||
|
|
||||||
|
twoFactor := &models.TwoFactor{}
|
||||||
|
has, err := s.UserDB().Where("uid=?", uid).Get(twoFactor)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if !has {
|
||||||
|
return nil, errs.ErrTwoFactorKeyIsNotEnabled
|
||||||
|
}
|
||||||
|
|
||||||
|
twoFactor.Secret, err = utils.DecryptSecret(twoFactor.Secret, s.CurrentConfig().SecretKey)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return twoFactor, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *TwoFactorAuthorizationService) GenerateTwoFactorSecret(user *models.User) (*otp.Key, error) {
|
||||||
|
if user == nil {
|
||||||
|
return nil, errs.ErrUserNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
key, err := totp.Generate(totp.GenerateOpts{
|
||||||
|
Issuer: s.CurrentConfig().AppName,
|
||||||
|
AccountName: user.Username,
|
||||||
|
Period: TWOFACTOR_PERIOD,
|
||||||
|
SecretSize: TWOFACTOR_SECRET_SIZE,
|
||||||
|
})
|
||||||
|
|
||||||
|
return key, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *TwoFactorAuthorizationService) CreateTwoFactorSetting(twoFactor *models.TwoFactor) error {
|
||||||
|
if twoFactor.Uid <= 0 {
|
||||||
|
return errs.ErrUserIdInvalid
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
twoFactor.Secret, err = utils.EncyptSecret(twoFactor.Secret, s.CurrentConfig().SecretKey)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
twoFactor.CreatedUnixTime = time.Now().Unix()
|
||||||
|
|
||||||
|
return s.UserDB().DoTranscation(func(sess *xorm.Session) error {
|
||||||
|
_, err := sess.Insert(twoFactor)
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *TwoFactorAuthorizationService) DeleteTwoFactorSetting(uid int64) error {
|
||||||
|
if uid <= 0 {
|
||||||
|
return errs.ErrUserIdInvalid
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.UserDB().DoTranscation(func(sess *xorm.Session) error {
|
||||||
|
deletedRows, err := sess.Where("uid=?", uid).Delete(&models.TwoFactor{})
|
||||||
|
|
||||||
|
if deletedRows < 1 {
|
||||||
|
return errs.ErrTwoFactorKeyIsNotEnabled
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *TwoFactorAuthorizationService) ExistsTwoFactorSetting(uid int64) (bool, error) {
|
||||||
|
if uid <= 0 {
|
||||||
|
return false, errs.ErrUserIdInvalid
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.UserDB().Cols("uid").Where("uid=?", uid).Exist(&models.TwoFactor{})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *TwoFactorAuthorizationService) GetAndUseUserTwoFactorRecoveryCode(uid int64, recoveryCode string, salt string) error {
|
||||||
|
if uid <= 0 {
|
||||||
|
return errs.ErrUserIdInvalid
|
||||||
|
}
|
||||||
|
|
||||||
|
recoveryCode = utils.EncodePassword(recoveryCode, salt)
|
||||||
|
exists, err := s.UserDB().Cols("uid", "recovery_code").Where("uid=? AND recovery_code=? AND used=?", uid, recoveryCode, false).Exist(&models.TwoFactorRecoveryCode{})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
} else if !exists {
|
||||||
|
return errs.ErrTwoFactorRecoveryCodeNotExist
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.UserDB().DoTranscation(func(sess *xorm.Session) error {
|
||||||
|
_, err := sess.Cols("used", "used_unix_time").Where("uid=? AND recovery_code=?", uid, recoveryCode).Update(&models.TwoFactorRecoveryCode{Used: true, UsedUnixTime: time.Now().Unix()})
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *TwoFactorAuthorizationService) GenerateTwoFactorRecoveryCodes() ([]string, error) {
|
||||||
|
recoveryCodes := make([]string, TWOFACTOR_RECOVERY_CODE_COUNT)
|
||||||
|
|
||||||
|
for i := 0; i < TWOFACTOR_RECOVERY_CODE_COUNT; i++ {
|
||||||
|
recoveryCode, err := utils.GetRandomNumberOrLetter(TWOFACTOR_RECOVERY_CODE_LENGTH)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
recoveryCodes[i] = recoveryCode[:5] + "-" + recoveryCode[5:]
|
||||||
|
}
|
||||||
|
|
||||||
|
return recoveryCodes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *TwoFactorAuthorizationService) CreateTwoFactorRecoveryCodes(uid int64, recoveryCodes []string, salt string) error {
|
||||||
|
twoFactorRecoveryCodes := make([]*models.TwoFactorRecoveryCode, len(recoveryCodes))
|
||||||
|
|
||||||
|
for i := 0; i < len(recoveryCodes); i++ {
|
||||||
|
twoFactorRecoveryCodes[i] = &models.TwoFactorRecoveryCode{
|
||||||
|
Uid: uid,
|
||||||
|
Used: false,
|
||||||
|
RecoveryCode: utils.EncodePassword(recoveryCodes[i], salt),
|
||||||
|
CreatedUnixTime: time.Now().Unix(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.UserDB().DoTranscation(func(sess *xorm.Session) error {
|
||||||
|
_, err := sess.Where("uid=?", uid).Delete(&models.TwoFactorRecoveryCode{})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < len(twoFactorRecoveryCodes); i++ {
|
||||||
|
twoFactorRecoveryCode := twoFactorRecoveryCodes[i]
|
||||||
|
_, err := sess.Insert(twoFactorRecoveryCode)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *TwoFactorAuthorizationService) DeleteTwoFactorRecoveryCodes(uid int64) error {
|
||||||
|
if uid <= 0 {
|
||||||
|
return errs.ErrUserIdInvalid
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.UserDB().DoTranscation(func(sess *xorm.Session) error {
|
||||||
|
_, err := sess.Where("uid=?", uid).Delete(&models.TwoFactorRecoveryCode{})
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
User-agent: *
|
||||||
|
Disallow: /
|
||||||
Reference in New Issue
Block a user