From f163768aae42ebc5c8439d98d390eafb792b262c Mon Sep 17 00:00:00 2001 From: Paul Pan Date: Sat, 17 Sep 2022 11:22:55 +0800 Subject: [PATCH] feat: add user login --- internal/api/user/create.go | 12 +++------ internal/api/user/handler.go | 17 ++++++++---- internal/api/user/login.go | 44 ++++++++++++++++++++++++++++++++ internal/api/user/token.go | 18 +++++++++++++ internal/app/app.go | 4 +++ internal/e/code.go | 8 ++++++ internal/global/global.go | 1 + internal/global/jwt.go | 23 +++++++++++++++++ internal/router/api.go | 7 +++-- internal/service/jwt/model.go | 11 -------- internal/service/jwt/service.go | 14 ++-------- internal/service/jwt/token.go | 9 ++++--- internal/service/user/create.go | 14 ++++++---- internal/service/user/login.go | 28 ++++++++++++++++++++ internal/service/user/service.go | 5 +++- 15 files changed, 167 insertions(+), 48 deletions(-) create mode 100644 internal/api/user/login.go create mode 100644 internal/api/user/token.go create mode 100644 internal/global/jwt.go delete mode 100644 internal/service/jwt/model.go create mode 100644 internal/service/user/login.go diff --git a/internal/api/user/create.go b/internal/api/user/create.go index 7be87a3..78c07a7 100644 --- a/internal/api/user/create.go +++ b/internal/api/user/create.go @@ -21,7 +21,8 @@ type createRequest struct { // @Param nickname formData string true "nickname" // @Param password formData string true "password" // @Response 200 {object} e.Response "random string" -// @Router /v1/user [post] +// @Security Authentication +// @Router /v1/user/create [post] func (h *handler) Create(c *gin.Context) { req := new(createRequest) @@ -36,11 +37,6 @@ func (h *handler) Create(c *gin.Context) { Password: req.Password, } - id, err := h.service.Create(createData) - if err != nil { - e.Pong(c, e.DatabaseError, err.Error()) - return - } - - e.Pong(c, e.Success, id) + id, err := h.userService.Create(createData) + e.Pong(c, err, id) } diff --git a/internal/api/user/handler.go b/internal/api/user/handler.go index 10bde4c..549c498 100644 --- a/internal/api/user/handler.go +++ b/internal/api/user/handler.go @@ -2,6 +2,7 @@ package user import ( "github.com/WHUPRJ/woj-server/internal/global" + "github.com/WHUPRJ/woj-server/internal/repo/model" "github.com/WHUPRJ/woj-server/internal/service/user" "github.com/gin-gonic/gin" "go.uber.org/zap" @@ -11,20 +12,26 @@ var _ Handler = (*handler)(nil) type Handler interface { Create(c *gin.Context) + Login(c *gin.Context) // List(c *gin.Context) + + tokenNext(c *gin.Context, user *model.User) } type handler struct { - log *zap.Logger - service user.Service + log *zap.Logger + userService user.Service + jwtService global.JwtService } func RouteRegister(g *global.Global, group *gin.RouterGroup) { app := &handler{ - log: g.Log, - service: user.NewUserService(g), + log: g.Log, + userService: user.NewUserService(g), + jwtService: g.Jwt, } - group.POST("/", app.Create) + group.POST("/login", app.Login) + group.POST("/create", app.jwtService.Handler(), app.Create) // group.GET("/", app.List) } diff --git a/internal/api/user/login.go b/internal/api/user/login.go new file mode 100644 index 0000000..7b7c0dc --- /dev/null +++ b/internal/api/user/login.go @@ -0,0 +1,44 @@ +package user + +import ( + "github.com/WHUPRJ/woj-server/internal/e" + "github.com/WHUPRJ/woj-server/internal/repo/model" + "github.com/gin-gonic/gin" +) + +type loginRequest struct { + Username string `form:"username" binding:"required"` + Password string `form:"password" binding:"required"` +} + +// Login +// @Summary login +// @Description login and return token +// @Accept application/x-www-form-urlencoded +// @Produce json +// @Param username formData string true "username" +// @Param password formData string true "password" +// @Response 200 {object} e.Response "random string" +// @Router /v1/user/login [post] +func (h *handler) Login(c *gin.Context) { + req := new(loginRequest) + + if err := c.ShouldBind(req); err != nil { + e.Pong(c, e.InvalidParameter, err.Error()) + return + } + + // check password + userData := &model.User{ + UserName: req.Username, + Password: []byte(req.Password), + } + user, err := h.userService.Login(userData) + if err != e.Success { + e.Pong(c, err, nil) + return + } + + // sign and return token + h.tokenNext(c, user) +} diff --git a/internal/api/user/token.go b/internal/api/user/token.go new file mode 100644 index 0000000..10a38fa --- /dev/null +++ b/internal/api/user/token.go @@ -0,0 +1,18 @@ +package user + +import ( + "github.com/WHUPRJ/woj-server/internal/e" + "github.com/WHUPRJ/woj-server/internal/global" + "github.com/WHUPRJ/woj-server/internal/repo/model" + "github.com/gin-gonic/gin" +) + +func (h *handler) tokenNext(c *gin.Context, user *model.User) { + claim := &global.Claim{ + UID: user.ID, + UserName: user.UserName, + NickName: user.NickName, + } + token, err := h.jwtService.SignClaim(claim) + e.Pong(c, err, token) +} diff --git a/internal/app/app.go b/internal/app/app.go index eb19eba..7a5c46c 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -6,6 +6,7 @@ import ( "github.com/WHUPRJ/woj-server/internal/global" "github.com/WHUPRJ/woj-server/internal/repo/postgresql" "github.com/WHUPRJ/woj-server/internal/router" + "github.com/WHUPRJ/woj-server/internal/service/jwt" "go.uber.org/zap" "net/http" "os" @@ -19,6 +20,9 @@ func Run(g *global.Global) error { g.Db = new(postgresql.PgRepo) g.Db.Setup(g) + // Setup JWT + g.Jwt = jwt.NewJwtService(g) + // Prepare Router handler := router.InitRouters(g) diff --git a/internal/e/code.go b/internal/e/code.go index e0858b4..4320e61 100644 --- a/internal/e/code.go +++ b/internal/e/code.go @@ -16,6 +16,10 @@ const ( TokenInvalid Err = 204 TokenSignError Err = 205 TokenRevoked Err = 206 + + UserNotFound Err = 300 + UserWrongPassword Err = 301 + UserDuplicated Err = 302 ) var msgText = map[Err]string{ @@ -34,4 +38,8 @@ var msgText = map[Err]string{ TokenInvalid: "Token Invalid", TokenSignError: "Token Sign Error", TokenRevoked: "Token Revoked", + + UserNotFound: "User Not Found", + UserWrongPassword: "User Wrong Password", + UserDuplicated: "User Duplicated", } diff --git a/internal/global/global.go b/internal/global/global.go index 725bca4..9d445ba 100644 --- a/internal/global/global.go +++ b/internal/global/global.go @@ -10,4 +10,5 @@ type Global struct { Conf *Config Stat *metrics.Metrics Db Repo + Jwt JwtService } diff --git a/internal/global/jwt.go b/internal/global/jwt.go new file mode 100644 index 0000000..6311ce5 --- /dev/null +++ b/internal/global/jwt.go @@ -0,0 +1,23 @@ +package global + +import ( + "github.com/WHUPRJ/woj-server/internal/e" + "github.com/gin-gonic/gin" + "github.com/golang-jwt/jwt/v4" +) + +type Claim struct { + UID uint `json:"id"` + UserName string `json:"user_name"` + NickName string `json:"nick_name"` + Version int `json:"version"` + jwt.RegisteredClaims +} + +type JwtService interface { + ParseToken(tokenText string) (*Claim, e.Err) + SignClaim(claim *Claim) (string, e.Err) + // TODO: Validate(claim *Claim) bool + + Handler() gin.HandlerFunc +} diff --git a/internal/router/api.go b/internal/router/api.go index 50e4a30..0687aae 100644 --- a/internal/router/api.go +++ b/internal/router/api.go @@ -8,8 +8,11 @@ import ( ) // @title OJ Server API Documentation -// @version 1.0 -// @BasePath /api +// @version 1.0 +// @BasePath /api +// @securityDefinitions.apikey Authentication +// @in header +// @name Authorization func setupApi(g *global.Global, root *gin.RouterGroup) { for _, v := range endpoints { group := root.Group(v.Version).Group(v.Path) diff --git a/internal/service/jwt/model.go b/internal/service/jwt/model.go deleted file mode 100644 index 44d8573..0000000 --- a/internal/service/jwt/model.go +++ /dev/null @@ -1,11 +0,0 @@ -package jwt - -import "github.com/golang-jwt/jwt/v4" - -type Claim struct { - UID uint `json:"id"` - UserName string `json:"user_name"` - NickName string `json:"nick_name"` - Version int `json:"version"` - jwt.RegisteredClaims -} diff --git a/internal/service/jwt/service.go b/internal/service/jwt/service.go index eb8f38a..25f7fb0 100644 --- a/internal/service/jwt/service.go +++ b/internal/service/jwt/service.go @@ -1,21 +1,11 @@ package jwt import ( - "github.com/WHUPRJ/woj-server/internal/e" "github.com/WHUPRJ/woj-server/internal/global" - "github.com/gin-gonic/gin" "go.uber.org/zap" ) -var _ Service = (*service)(nil) - -type Service interface { - ParseToken(tokenText string) (*Claim, e.Err) - SignClaim(claim *Claim) (string, e.Err) - // TODO: Validate(claim *Claim) bool - - Handler() gin.HandlerFunc -} +var _ global.JwtService = (*service)(nil) type service struct { log *zap.Logger @@ -23,7 +13,7 @@ type service struct { ExpireHour int } -func NewJwtService(g *global.Global) Service { +func NewJwtService(g *global.Global) global.JwtService { return &service{ log: g.Log, SigningKey: []byte(g.Conf.WebServer.JwtSigningKey), diff --git a/internal/service/jwt/token.go b/internal/service/jwt/token.go index b309a15..ddc2bbf 100644 --- a/internal/service/jwt/token.go +++ b/internal/service/jwt/token.go @@ -3,20 +3,21 @@ package jwt import ( "fmt" "github.com/WHUPRJ/woj-server/internal/e" + "github.com/WHUPRJ/woj-server/internal/global" "github.com/WHUPRJ/woj-server/pkg/utils" "github.com/golang-jwt/jwt/v4" "go.uber.org/zap" "time" ) -func (s *service) ParseToken(tokenText string) (*Claim, e.Err) { +func (s *service) ParseToken(tokenText string) (*global.Claim, e.Err) { if tokenText == "" { return nil, e.TokenEmpty } token, err := jwt.ParseWithClaims( tokenText, - &Claim{}, + &global.Claim{}, func(token *jwt.Token) (interface{}, error) { if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) @@ -39,14 +40,14 @@ func (s *service) ParseToken(tokenText string) (*Claim, e.Err) { } if token.Valid { - c := token.Claims.(*Claim) + c := token.Claims.(*global.Claim) return c, e.Success } return nil, e.TokenInvalid } -func (s *service) SignClaim(claim *Claim) (string, e.Err) { +func (s *service) SignClaim(claim *global.Claim) (string, e.Err) { now := time.Now() claim.IssuedAt = jwt.NewNumericDate(now) diff --git a/internal/service/user/create.go b/internal/service/user/create.go index 5fac43a..3c85674 100644 --- a/internal/service/user/create.go +++ b/internal/service/user/create.go @@ -1,10 +1,11 @@ package user import ( + "github.com/WHUPRJ/woj-server/internal/e" "github.com/WHUPRJ/woj-server/internal/repo/model" - "github.com/pkg/errors" "go.uber.org/zap" "golang.org/x/crypto/bcrypt" + "strings" ) type CreateData struct { @@ -13,11 +14,11 @@ type CreateData struct { Password string } -func (s *service) Create(data *CreateData) (id uint, err error) { +func (s *service) Create(data *CreateData) (uint, e.Err) { hashed, err := bcrypt.GenerateFromPassword([]byte(data.Password), bcrypt.DefaultCost) if err != nil { s.log.Debug("bcrypt error", zap.Error(err), zap.String("password", data.Password)) - return 0, errors.Wrap(err, "bcrypt error") + return 0, e.InternalError } user := &model.User{ @@ -28,8 +29,11 @@ func (s *service) Create(data *CreateData) (id uint, err error) { } if err := s.db.Get().Create(user).Error; err != nil { + if strings.Contains(err.Error(), "duplicate key") { + return 0, e.UserDuplicated + } s.log.Debug("create user error", zap.Error(err), zap.Any("data", data)) - return 0, errors.Wrap(err, "create error") + return 0, e.DatabaseError } - return user.ID, nil + return user.ID, e.Success } diff --git a/internal/service/user/login.go b/internal/service/user/login.go new file mode 100644 index 0000000..bc604a4 --- /dev/null +++ b/internal/service/user/login.go @@ -0,0 +1,28 @@ +package user + +import ( + "errors" + "github.com/WHUPRJ/woj-server/internal/e" + "github.com/WHUPRJ/woj-server/internal/repo/model" + "golang.org/x/crypto/bcrypt" + "gorm.io/gorm" +) + +func (s *service) Login(data *model.User) (*model.User, e.Err) { + user := &model.User{UserName: data.UserName} + + err := s.db.Get().Where(user).First(&user).Error + if errors.Is(err, gorm.ErrRecordNotFound) { + return user, e.UserNotFound + } + if err != nil { + return user, e.DatabaseError + } + + err = bcrypt.CompareHashAndPassword(user.Password, data.Password) + if err != nil { + return user, e.UserWrongPassword + } + + return user, e.Success +} diff --git a/internal/service/user/service.go b/internal/service/user/service.go index d43d399..5e2f1fa 100644 --- a/internal/service/user/service.go +++ b/internal/service/user/service.go @@ -1,14 +1,17 @@ package user import ( + "github.com/WHUPRJ/woj-server/internal/e" "github.com/WHUPRJ/woj-server/internal/global" + "github.com/WHUPRJ/woj-server/internal/repo/model" "go.uber.org/zap" ) var _ Service = (*service)(nil) type Service interface { - Create(data *CreateData) (id uint, err error) + Create(data *CreateData) (uint, e.Err) + Login(data *model.User) (*model.User, e.Err) } type service struct {