feat: add token-based API access (v0.6.0)
Some checks are pending
Release Binaries / Build and Release (.exe, amd64, windows, windows-amd64) (push) Waiting to run
Release Binaries / Build and Release (amd64, darwin, darwin-amd64) (push) Waiting to run
Release Binaries / Build and Release (amd64, linux, linux-amd64) (push) Waiting to run
Release Binaries / Build and Release (arm64, darwin, darwin-arm64) (push) Waiting to run
Release Binaries / Build and Release (arm64, linux, linux-arm64) (push) Waiting to run

- Add Token GORM model with UserID/Name/Token/LastUsedAt/ExpiresAt fields
- Add TokenManager controller: List/Create/Delete/Verify operations
- Add token HTTP handlers: list, create, revoke
- Update AuthVerify to support Bearer token auth; API tokens use "ust_" prefix to distinguish from session tokens
- Add one-step file upload endpoint: PUT /api/v1/upload/:filename (returns {"status":200,"data":{"code":"..."}})
- Add token management routes: GET/POST/DELETE /api/token
- Add /self page: personal center with account info, token management table, and curl usage guide
- Add "个人中心 / API Token" nav link for users with token_manage permission

🤖 Generated with [Qoder][https://qoder.com]
This commit is contained in:
loveuer
2026-02-28 01:32:08 -08:00
parent 6286332896
commit ef6347a8b4
11 changed files with 765 additions and 8 deletions

View File

@@ -0,0 +1,97 @@
package controller
import (
"strings"
"time"
"github.com/loveuer/ushare/internal/model"
"github.com/loveuer/ushare/internal/pkg/db"
"github.com/loveuer/ushare/internal/pkg/tool"
"github.com/pkg/errors"
)
type tokenManager struct{}
var TokenManager = &tokenManager{}
// List returns all tokens belonging to a user (token value is not exposed).
func (tm *tokenManager) List(userID uint) ([]model.Token, error) {
var tokens []model.Token
if err := db.Default.Session().Where("user_id = ?", userID).Order("created_at desc").Find(&tokens).Error; err != nil {
return nil, errors.Wrap(err, "list tokens failed")
}
return tokens, nil
}
// Create generates a new API token for the given user and returns the full token value (only shown once).
func (tm *tokenManager) Create(userID uint, name string) (*model.Token, string, error) {
name = strings.TrimSpace(name)
if name == "" {
return nil, "", errors.New("token 名称不能为空")
}
rawToken := model.TokenPrefix + tool.RandomString(32)
t := &model.Token{
UserID: userID,
Name: name,
Token: rawToken,
}
if err := db.Default.Session().Create(t).Error; err != nil {
return nil, "", errors.Wrap(err, "create token failed")
}
return t, rawToken, nil
}
// Delete removes a token by ID, only if it belongs to the given user.
func (tm *tokenManager) Delete(userID uint, tokenID uint) error {
result := db.Default.Session().
Where("id = ? AND user_id = ?", tokenID, userID).
Delete(&model.Token{})
if result.Error != nil {
return errors.Wrap(result.Error, "delete token failed")
}
if result.RowsAffected == 0 {
return errors.New("token 不存在或无权限删除")
}
return nil
}
// Verify looks up a DB API token and returns a Session if valid.
func (tm *tokenManager) Verify(rawToken string) (*model.Session, error) {
var t model.Token
err := db.Default.Session().
Where("token = ?", rawToken).
Preload("User").
Preload("User.Role").
First(&t).Error
if err != nil {
return nil, errors.New("无效的 API Token")
}
if t.ExpiresAt != nil && time.Now().After(*t.ExpiresAt) {
return nil, errors.New("API Token 已过期")
}
// Update last_used_at asynchronously
now := time.Now()
go db.Default.Session().Model(&t).Update("last_used_at", now) //nolint:errcheck
session := &model.Session{
UserID: t.User.ID,
Username: t.User.Username,
Role: t.User.Role.Name,
RoleLabel: t.User.Role.Label,
Permissions: t.User.Role.PermissionList(),
LoginAt: now.Unix(),
Token: rawToken,
}
return session, nil
}