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

@@ -3,6 +3,7 @@ package handler
import (
"fmt"
"net/http"
"strings"
"github.com/loveuer/nf"
"github.com/loveuer/ushare/internal/controller"
@@ -11,8 +12,12 @@ import (
func AuthVerify() nf.HandlerFunc {
tokenFn := func(c *nf.Ctx) (token string) {
if token = c.Get("Authorization"); token != "" {
return
if raw := c.Get("Authorization"); raw != "" {
// Strip "Bearer " prefix if present
if strings.HasPrefix(raw, "Bearer ") {
return strings.TrimPrefix(raw, "Bearer ")
}
return raw
}
token = c.Cookies("ushare")
return
@@ -24,7 +29,18 @@ func AuthVerify() nf.HandlerFunc {
return c.Status(http.StatusUnauthorized).JSON(map[string]string{"error": "unauthorized"})
}
session, err := controller.UserManager.Verify(token)
var (
session *model.Session
err error
)
// API tokens have the "ust_" prefix; session tokens do not.
if strings.HasPrefix(token, model.TokenPrefix) {
session, err = controller.TokenManager.Verify(token)
} else {
session, err = controller.UserManager.Verify(token)
}
if err != nil {
return c.Status(http.StatusUnauthorized).JSON(map[string]string{"error": "unauthorized", "msg": err.Error()})
}

View File

@@ -2,6 +2,11 @@ package handler
import (
"fmt"
"net/http"
"os"
"regexp"
"strings"
"github.com/loveuer/nf"
"github.com/loveuer/nf/nft/log"
"github.com/loveuer/ushare/internal/controller"
@@ -10,10 +15,6 @@ import (
"github.com/pkg/errors"
"github.com/spf13/cast"
"github.com/spf13/viper"
"net/http"
"os"
"regexp"
"strings"
)
func Fetch() nf.HandlerFunc {
@@ -116,3 +117,36 @@ func ShareUpload() nf.HandlerFunc {
return c.Status(http.StatusOK).JSON(map[string]any{"size": total, "cursor": cursor})
}
}
// ShareAPIUpload handles one-step file upload via API token.
// PUT /api/v1/upload/:filename
// Accepts the raw file body and Content-Length header, returns the download code.
func ShareAPIUpload() nf.HandlerFunc {
return func(c *nf.Ctx) error {
filename := strings.TrimSpace(c.Param("filename"))
if filename == "" {
return c.Status(http.StatusBadRequest).JSON(map[string]string{"msg": "filename required"})
}
size, err := cast.ToInt64E(c.Request.ContentLength)
if err != nil || size <= 0 {
return c.Status(http.StatusBadRequest).JSON(map[string]string{"msg": "Content-Length header required"})
}
code, err := controller.MetaManager.New(size, filename, c.IP())
if err != nil {
return c.Status(http.StatusInternalServerError).JSON(map[string]string{"msg": "create upload failed"})
}
_, _, err = controller.MetaManager.Write(code, 0, size-1, c.Request.Body)
if err != nil {
log.Error("handler.ShareAPIUpload: write error: %s", err)
return c.Status(http.StatusInternalServerError).JSON(map[string]string{"msg": "upload failed"})
}
return c.Status(http.StatusOK).JSON(map[string]any{
"status": 200,
"data": map[string]string{"code": code},
})
}
}

85
internal/handler/token.go Normal file
View File

@@ -0,0 +1,85 @@
package handler
import (
"net/http"
"github.com/loveuer/nf"
"github.com/loveuer/ushare/internal/controller"
"github.com/loveuer/ushare/internal/model"
)
func TokenList() nf.HandlerFunc {
return func(c *nf.Ctx) error {
session, ok := c.Locals("user").(*model.Session)
if !ok || session == nil {
return c.Status(http.StatusUnauthorized).JSON(map[string]string{"error": "unauthorized"})
}
tokens, err := controller.TokenManager.List(session.UserID)
if err != nil {
return c.Status(http.StatusInternalServerError).JSON(map[string]string{"msg": err.Error()})
}
return c.Status(http.StatusOK).JSON(map[string]any{"data": tokens})
}
}
func TokenCreate() nf.HandlerFunc {
return func(c *nf.Ctx) error {
session, ok := c.Locals("user").(*model.Session)
if !ok || session == nil {
return c.Status(http.StatusUnauthorized).JSON(map[string]string{"error": "unauthorized"})
}
type Req struct {
Name string `json:"name"`
}
var req Req
if err := c.BodyParser(&req); err != nil {
return c.Status(http.StatusBadRequest).JSON(map[string]string{"msg": "请求格式错误"})
}
t, rawToken, err := controller.TokenManager.Create(session.UserID, req.Name)
if err != nil {
return c.Status(http.StatusBadRequest).JSON(map[string]string{"msg": err.Error()})
}
return c.Status(http.StatusOK).JSON(map[string]any{
"data": map[string]any{
"id": t.ID,
"name": t.Name,
"token": rawToken,
"created_at": t.CreatedAt,
},
})
}
}
func TokenDelete() nf.HandlerFunc {
return func(c *nf.Ctx) error {
session, ok := c.Locals("user").(*model.Session)
if !ok || session == nil {
return c.Status(http.StatusUnauthorized).JSON(map[string]string{"error": "unauthorized"})
}
type Req struct {
ID uint `json:"id"`
}
var req Req
if err := c.BodyParser(&req); err != nil {
return c.Status(http.StatusBadRequest).JSON(map[string]string{"msg": "请求格式错误"})
}
if req.ID == 0 {
return c.Status(http.StatusBadRequest).JSON(map[string]string{"msg": "token id 不能为空"})
}
if err := controller.TokenManager.Delete(session.UserID, req.ID); err != nil {
return c.Status(http.StatusBadRequest).JSON(map[string]string{"msg": err.Error()})
}
return c.Status(http.StatusOK).JSON(map[string]any{"data": "ok"})
}
}