feat api token

This commit is contained in:
coltea 2025-09-08 15:52:19 +08:00
parent 7360c054b5
commit e6faae6061
10 changed files with 169 additions and 22 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -22,4 +22,5 @@ var ProviderSet = wire.NewSet(
NewBlockWordRepo,
NewAuthRepo,
NewWechatRepository,
NewAPITokenRepo,
)

View File

@ -0,0 +1 @@
DROP TABLE IF EXISTS api_tokens;

View File

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