feat: add user login

This commit is contained in:
Paul Pan 2022-09-17 11:22:55 +08:00
parent 787fc39c29
commit f163768aae
15 changed files with 167 additions and 48 deletions

View File

@ -21,7 +21,8 @@ type createRequest struct {
// @Param nickname formData string true "nickname" // @Param nickname formData string true "nickname"
// @Param password formData string true "password" // @Param password formData string true "password"
// @Response 200 {object} e.Response "random string" // @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) { func (h *handler) Create(c *gin.Context) {
req := new(createRequest) req := new(createRequest)
@ -36,11 +37,6 @@ func (h *handler) Create(c *gin.Context) {
Password: req.Password, Password: req.Password,
} }
id, err := h.service.Create(createData) id, err := h.userService.Create(createData)
if err != nil { e.Pong(c, err, id)
e.Pong(c, e.DatabaseError, err.Error())
return
}
e.Pong(c, e.Success, id)
} }

View File

@ -2,6 +2,7 @@ package user
import ( import (
"github.com/WHUPRJ/woj-server/internal/global" "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/WHUPRJ/woj-server/internal/service/user"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"go.uber.org/zap" "go.uber.org/zap"
@ -11,20 +12,26 @@ var _ Handler = (*handler)(nil)
type Handler interface { type Handler interface {
Create(c *gin.Context) Create(c *gin.Context)
Login(c *gin.Context)
// List(c *gin.Context) // List(c *gin.Context)
tokenNext(c *gin.Context, user *model.User)
} }
type handler struct { type handler struct {
log *zap.Logger log *zap.Logger
service user.Service userService user.Service
jwtService global.JwtService
} }
func RouteRegister(g *global.Global, group *gin.RouterGroup) { func RouteRegister(g *global.Global, group *gin.RouterGroup) {
app := &handler{ app := &handler{
log: g.Log, log: g.Log,
service: user.NewUserService(g), 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) // group.GET("/", app.List)
} }

View File

@ -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)
}

View File

@ -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)
}

View File

@ -6,6 +6,7 @@ import (
"github.com/WHUPRJ/woj-server/internal/global" "github.com/WHUPRJ/woj-server/internal/global"
"github.com/WHUPRJ/woj-server/internal/repo/postgresql" "github.com/WHUPRJ/woj-server/internal/repo/postgresql"
"github.com/WHUPRJ/woj-server/internal/router" "github.com/WHUPRJ/woj-server/internal/router"
"github.com/WHUPRJ/woj-server/internal/service/jwt"
"go.uber.org/zap" "go.uber.org/zap"
"net/http" "net/http"
"os" "os"
@ -19,6 +20,9 @@ func Run(g *global.Global) error {
g.Db = new(postgresql.PgRepo) g.Db = new(postgresql.PgRepo)
g.Db.Setup(g) g.Db.Setup(g)
// Setup JWT
g.Jwt = jwt.NewJwtService(g)
// Prepare Router // Prepare Router
handler := router.InitRouters(g) handler := router.InitRouters(g)

View File

@ -16,6 +16,10 @@ const (
TokenInvalid Err = 204 TokenInvalid Err = 204
TokenSignError Err = 205 TokenSignError Err = 205
TokenRevoked Err = 206 TokenRevoked Err = 206
UserNotFound Err = 300
UserWrongPassword Err = 301
UserDuplicated Err = 302
) )
var msgText = map[Err]string{ var msgText = map[Err]string{
@ -34,4 +38,8 @@ var msgText = map[Err]string{
TokenInvalid: "Token Invalid", TokenInvalid: "Token Invalid",
TokenSignError: "Token Sign Error", TokenSignError: "Token Sign Error",
TokenRevoked: "Token Revoked", TokenRevoked: "Token Revoked",
UserNotFound: "User Not Found",
UserWrongPassword: "User Wrong Password",
UserDuplicated: "User Duplicated",
} }

View File

@ -10,4 +10,5 @@ type Global struct {
Conf *Config Conf *Config
Stat *metrics.Metrics Stat *metrics.Metrics
Db Repo Db Repo
Jwt JwtService
} }

23
internal/global/jwt.go Normal file
View File

@ -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
}

View File

@ -10,6 +10,9 @@ import (
// @title OJ Server API Documentation // @title OJ Server API Documentation
// @version 1.0 // @version 1.0
// @BasePath /api // @BasePath /api
// @securityDefinitions.apikey Authentication
// @in header
// @name Authorization
func setupApi(g *global.Global, root *gin.RouterGroup) { func setupApi(g *global.Global, root *gin.RouterGroup) {
for _, v := range endpoints { for _, v := range endpoints {
group := root.Group(v.Version).Group(v.Path) group := root.Group(v.Version).Group(v.Path)

View File

@ -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
}

View File

@ -1,21 +1,11 @@
package jwt package jwt
import ( import (
"github.com/WHUPRJ/woj-server/internal/e"
"github.com/WHUPRJ/woj-server/internal/global" "github.com/WHUPRJ/woj-server/internal/global"
"github.com/gin-gonic/gin"
"go.uber.org/zap" "go.uber.org/zap"
) )
var _ Service = (*service)(nil) var _ global.JwtService = (*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
}
type service struct { type service struct {
log *zap.Logger log *zap.Logger
@ -23,7 +13,7 @@ type service struct {
ExpireHour int ExpireHour int
} }
func NewJwtService(g *global.Global) Service { func NewJwtService(g *global.Global) global.JwtService {
return &service{ return &service{
log: g.Log, log: g.Log,
SigningKey: []byte(g.Conf.WebServer.JwtSigningKey), SigningKey: []byte(g.Conf.WebServer.JwtSigningKey),

View File

@ -3,20 +3,21 @@ package jwt
import ( import (
"fmt" "fmt"
"github.com/WHUPRJ/woj-server/internal/e" "github.com/WHUPRJ/woj-server/internal/e"
"github.com/WHUPRJ/woj-server/internal/global"
"github.com/WHUPRJ/woj-server/pkg/utils" "github.com/WHUPRJ/woj-server/pkg/utils"
"github.com/golang-jwt/jwt/v4" "github.com/golang-jwt/jwt/v4"
"go.uber.org/zap" "go.uber.org/zap"
"time" "time"
) )
func (s *service) ParseToken(tokenText string) (*Claim, e.Err) { func (s *service) ParseToken(tokenText string) (*global.Claim, e.Err) {
if tokenText == "" { if tokenText == "" {
return nil, e.TokenEmpty return nil, e.TokenEmpty
} }
token, err := jwt.ParseWithClaims( token, err := jwt.ParseWithClaims(
tokenText, tokenText,
&Claim{}, &global.Claim{},
func(token *jwt.Token) (interface{}, error) { func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) 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 { if token.Valid {
c := token.Claims.(*Claim) c := token.Claims.(*global.Claim)
return c, e.Success return c, e.Success
} }
return nil, e.TokenInvalid 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() now := time.Now()
claim.IssuedAt = jwt.NewNumericDate(now) claim.IssuedAt = jwt.NewNumericDate(now)

View File

@ -1,10 +1,11 @@
package user package user
import ( import (
"github.com/WHUPRJ/woj-server/internal/e"
"github.com/WHUPRJ/woj-server/internal/repo/model" "github.com/WHUPRJ/woj-server/internal/repo/model"
"github.com/pkg/errors"
"go.uber.org/zap" "go.uber.org/zap"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
"strings"
) )
type CreateData struct { type CreateData struct {
@ -13,11 +14,11 @@ type CreateData struct {
Password string 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) hashed, err := bcrypt.GenerateFromPassword([]byte(data.Password), bcrypt.DefaultCost)
if err != nil { if err != nil {
s.log.Debug("bcrypt error", zap.Error(err), zap.String("password", data.Password)) 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{ 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 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)) 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
} }

View File

@ -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
}

View File

@ -1,14 +1,17 @@
package user package user
import ( import (
"github.com/WHUPRJ/woj-server/internal/e"
"github.com/WHUPRJ/woj-server/internal/global" "github.com/WHUPRJ/woj-server/internal/global"
"github.com/WHUPRJ/woj-server/internal/repo/model"
"go.uber.org/zap" "go.uber.org/zap"
) )
var _ Service = (*service)(nil) var _ Service = (*service)(nil)
type Service interface { 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 { type service struct {