mirror of https://github.com/chaitin/PandaWiki.git
feat api token
This commit is contained in:
parent
7360c054b5
commit
e6faae6061
|
|
@ -55,7 +55,8 @@ func createApp() (*App, error) {
|
|||
return nil, err
|
||||
}
|
||||
userAccessRepository := pg2.NewUserAccessRepository(db, logger)
|
||||
authMiddleware, err := middleware.NewAuthMiddleware(configConfig, logger, userAccessRepository)
|
||||
apiTokenRepo := pg2.NewAPITokenRepo(db, logger)
|
||||
authMiddleware, err := middleware.NewAuthMiddleware(configConfig, logger, userAccessRepository, apiTokenRepo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,22 @@
|
|||
package domain
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/chaitin/panda-wiki/consts"
|
||||
)
|
||||
|
||||
type APIToken struct {
|
||||
ID string `json:"id" gorm:"primaryKey"`
|
||||
Name string `json:"name" gorm:"not null"`
|
||||
UserID string `json:"user_id" gorm:"not null"`
|
||||
Token string `json:"token" gorm:"uniqueIndex;not null"`
|
||||
KbId string `json:"kb_id" gorm:"not null"`
|
||||
Permission consts.UserKBPermission `json:"permission" gorm:"not null"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
func (APIToken) TableName() string {
|
||||
return "api_tokens"
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/chaitin/panda-wiki/domain"
|
||||
)
|
||||
|
||||
type APITokenRepository interface {
|
||||
GetByToken(ctx context.Context, token string) (*domain.APIToken, error)
|
||||
}
|
||||
|
|
@ -19,10 +19,10 @@ type AuthMiddleware interface {
|
|||
MustGetUserID(c echo.Context) (string, bool)
|
||||
}
|
||||
|
||||
func NewAuthMiddleware(config *config.Config, logger *log.Logger, userAccessRepo *pg.UserAccessRepository) (AuthMiddleware, error) {
|
||||
func NewAuthMiddleware(config *config.Config, logger *log.Logger, userAccessRepo *pg.UserAccessRepository, apiTokenRepo *pg.APITokenRepo) (AuthMiddleware, error) {
|
||||
switch config.Auth.Type {
|
||||
case "jwt":
|
||||
return NewJWTMiddleware(config, logger, userAccessRepo), nil
|
||||
return NewJWTMiddleware(config, logger, userAccessRepo, apiTokenRepo), nil
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid auth type: %s", config.Auth.Type)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,11 +23,16 @@ type JWTMiddleware struct {
|
|||
jwtMiddleware echo.MiddlewareFunc
|
||||
logger *log.Logger
|
||||
userAccessRepo *pg.UserAccessRepository
|
||||
apiTokenRepo *pg.APITokenRepo
|
||||
}
|
||||
|
||||
func NewJWTMiddleware(config *config.Config, logger *log.Logger, userAccessRepo *pg.UserAccessRepository) *JWTMiddleware {
|
||||
func NewJWTMiddleware(config *config.Config, logger *log.Logger, userAccessRepo *pg.UserAccessRepository, apiTokenRepo *pg.APITokenRepo) *JWTMiddleware {
|
||||
jwtMiddleware := echoMiddleware.WithConfig(echoMiddleware.Config{
|
||||
SigningKey: []byte(config.Auth.JWT.Secret),
|
||||
Skipper: func(c echo.Context) bool {
|
||||
authHeader := c.Request().Header.Get("Authorization")
|
||||
return strings.HasPrefix(authHeader, "Bearer ") && !strings.Contains(authHeader, ".")
|
||||
},
|
||||
ErrorHandler: func(c echo.Context, err error) error {
|
||||
logger.Error("jwt auth failed", log.Error(err))
|
||||
return c.JSON(http.StatusUnauthorized, domain.PWResponse{
|
||||
|
|
@ -41,18 +46,55 @@ func NewJWTMiddleware(config *config.Config, logger *log.Logger, userAccessRepo
|
|||
jwtMiddleware: jwtMiddleware,
|
||||
logger: logger.WithModule("middleware.jwt"),
|
||||
userAccessRepo: userAccessRepo,
|
||||
apiTokenRepo: apiTokenRepo,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *JWTMiddleware) Authorize(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return m.jwtMiddleware(func(c echo.Context) error {
|
||||
// JWT authentication was successful, update access time
|
||||
if userID, ok := m.MustGetUserID(c); ok {
|
||||
c.Set("user_id", userID)
|
||||
m.userAccessRepo.UpdateAccessTime(userID)
|
||||
return func(c echo.Context) error {
|
||||
authHeader := c.Request().Header.Get("Authorization")
|
||||
if strings.HasPrefix(authHeader, "Bearer ") {
|
||||
token := strings.TrimPrefix(authHeader, "Bearer ")
|
||||
|
||||
if !strings.Contains(token, ".") {
|
||||
return m.validateAPIToken(c, token, next)
|
||||
}
|
||||
}
|
||||
return next(c)
|
||||
})
|
||||
|
||||
return m.jwtMiddleware(func(c echo.Context) error {
|
||||
if userID, ok := m.MustGetUserID(c); ok {
|
||||
c.Set("user_id", userID)
|
||||
m.userAccessRepo.UpdateAccessTime(userID)
|
||||
}
|
||||
return next(c)
|
||||
})(c)
|
||||
}
|
||||
}
|
||||
|
||||
// validateAPIToken validates API token and sets user context
|
||||
func (m *JWTMiddleware) validateAPIToken(c echo.Context, token string, next echo.HandlerFunc) error {
|
||||
if m.apiTokenRepo == nil {
|
||||
m.logger.Debug("API token repository not available")
|
||||
return c.JSON(http.StatusUnauthorized, domain.PWResponse{
|
||||
Success: false,
|
||||
Message: "Unauthorized",
|
||||
})
|
||||
}
|
||||
|
||||
apiToken, err := m.apiTokenRepo.GetByToken(c.Request().Context(), token)
|
||||
if err != nil || apiToken == nil {
|
||||
m.logger.Error("failed to get API token", log.Error(err))
|
||||
return c.JSON(http.StatusUnauthorized, domain.PWResponse{
|
||||
Success: false,
|
||||
Message: "Unauthorized",
|
||||
})
|
||||
}
|
||||
|
||||
c.Set("user_id", apiToken.ID)
|
||||
c.Set("is_token", true)
|
||||
c.Set("permission", apiToken.Permission)
|
||||
|
||||
return next(c)
|
||||
}
|
||||
|
||||
func (m *JWTMiddleware) ValidateUserRole(role consts.UserRole) echo.MiddlewareFunc {
|
||||
|
|
@ -83,17 +125,29 @@ func (m *JWTMiddleware) ValidateKBUserPerm(perm consts.UserKBPermission) echo.Mi
|
|||
|
||||
kbId, _ := GetKbID(c)
|
||||
|
||||
valid, err := m.userAccessRepo.ValidateKBPerm(kbId, userID, perm)
|
||||
if err != nil || !valid {
|
||||
if err != nil {
|
||||
m.logger.Error("ValidateKBUserPerm ValidateKBPerm failed", log.Error(err))
|
||||
} else {
|
||||
m.logger.Info("ValidateKBUserPerm ValidateKBPerm failed", log.String("kb_id", kbId), log.String("user_id", userID))
|
||||
if m.IsUseToken(c) {
|
||||
// 使用token的情况
|
||||
tokenPermission := c.Get("permission").(consts.UserKBPermission)
|
||||
if tokenPermission != consts.UserKBPermissionFullControl && tokenPermission != perm {
|
||||
return c.JSON(http.StatusForbidden, domain.PWResponse{
|
||||
Success: false,
|
||||
Message: "Unauthorized ValidateTokenKBPerm",
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// 正常用户请求
|
||||
valid, err := m.userAccessRepo.ValidateKBPerm(kbId, userID, perm)
|
||||
if err != nil || !valid {
|
||||
if err != nil {
|
||||
m.logger.Error("ValidateKBUserPerm ValidateKBPerm failed", log.Error(err))
|
||||
} else {
|
||||
m.logger.Info("ValidateKBUserPerm ValidateKBPerm failed", log.String("kb_id", kbId), log.String("user_id", userID))
|
||||
}
|
||||
return c.JSON(http.StatusForbidden, domain.PWResponse{
|
||||
Success: false,
|
||||
Message: "Unauthorized ValidateKBPerm",
|
||||
})
|
||||
}
|
||||
return c.JSON(http.StatusForbidden, domain.PWResponse{
|
||||
Success: false,
|
||||
Message: "Unauthorized ValidateKBPerm",
|
||||
})
|
||||
}
|
||||
|
||||
return next(c)
|
||||
|
|
@ -180,3 +234,14 @@ func GetKbID(c echo.Context) (string, error) {
|
|||
return "", nil
|
||||
}
|
||||
}
|
||||
|
||||
func (m *JWTMiddleware) IsUseToken(c echo.Context) bool {
|
||||
v := c.Get("is_token")
|
||||
if v == nil {
|
||||
return false
|
||||
}
|
||||
if b, ok := v.(bool); ok {
|
||||
return b
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
Subproject commit 3144d3773ea161dba610b1b2a00d97900636611e
|
||||
Subproject commit cf3556f21810ef864c2b6c69475cc33c7c08029a
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
package pg
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/chaitin/panda-wiki/domain"
|
||||
"github.com/chaitin/panda-wiki/log"
|
||||
"github.com/chaitin/panda-wiki/store/pg"
|
||||
)
|
||||
|
||||
type APITokenRepo struct {
|
||||
db *pg.DB
|
||||
logger *log.Logger
|
||||
}
|
||||
|
||||
func NewAPITokenRepo(db *pg.DB, logger *log.Logger) *APITokenRepo {
|
||||
return &APITokenRepo{
|
||||
db: db,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *APITokenRepo) GetByToken(ctx context.Context, token string) (*domain.APIToken, error) {
|
||||
var apiToken domain.APIToken
|
||||
if err := r.db.WithContext(ctx).Where("token = ?", token).First(&apiToken).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, fmt.Errorf("get api token by token failed: %w", err)
|
||||
}
|
||||
return &apiToken, nil
|
||||
}
|
||||
|
|
@ -22,4 +22,5 @@ var ProviderSet = wire.NewSet(
|
|||
NewBlockWordRepo,
|
||||
NewAuthRepo,
|
||||
NewWechatRepository,
|
||||
NewAPITokenRepo,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
DROP TABLE IF EXISTS api_tokens;
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
CREATE TABLE IF NOT EXISTS api_tokens (
|
||||
id TEXT PRIMARY KEY,
|
||||
kb_id TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
user_id TEXT NOT NULL,
|
||||
token TEXT NOT NULL,
|
||||
permission TEXT NOT NULL,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
UNIQUE(token)
|
||||
);
|
||||
Loading…
Reference in New Issue