feat: 分类/变体体系 + 用户认证 + 管理后台
- 运行时分类体系:Shell/Python/JavaScript/Ruby/PHP 各含变体 - 用户注册/登录(JWT + bcrypt),首个注册用户为管理员 - 管理后台 /admin 动态管理分类和变体 - 脚本市场支持按分类筛选 - CodeMirror 语言模式根据分类名称自动切换 - 结果页展示该分类下所有变体的运行命令 - source 命令变体用于 Shell 类继承环境变量 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -8,14 +8,111 @@ import (
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"gitea.kmux.cn/zhilv/scriptforge/internal/auth"
|
||||
"gitea.kmux.cn/zhilv/scriptforge/internal/model"
|
||||
"gitea.kmux.cn/zhilv/scriptforge/internal/service"
|
||||
)
|
||||
|
||||
func getUserID(c *gin.Context) uint {
|
||||
val, exists := c.Get("user_id")
|
||||
if !exists {
|
||||
return 0
|
||||
}
|
||||
switch v := val.(type) {
|
||||
case uint:
|
||||
return v
|
||||
case float64:
|
||||
return uint(v)
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
// --- Auth handlers ---
|
||||
|
||||
type registerRequest struct {
|
||||
Username string `json:"username" binding:"required,min=3,max=32"`
|
||||
Password string `json:"password" binding:"required,min=6"`
|
||||
}
|
||||
|
||||
func Register(authSvc *auth.AuthService) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
var req registerRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
user, err := authSvc.Register(req.Username, req.Password)
|
||||
if errors.Is(err, auth.ErrUserExists) {
|
||||
c.JSON(http.StatusConflict, gin.H{"error": "username already exists"})
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "internal error"})
|
||||
return
|
||||
}
|
||||
token, _ := authSvc.GenerateToken(user.ID, user.Username, user.Role)
|
||||
c.JSON(http.StatusCreated, gin.H{
|
||||
"id": user.ID,
|
||||
"username": user.Username,
|
||||
"role": user.Role,
|
||||
"token": token,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Login(authSvc *auth.AuthService) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
var req registerRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
user, token, err := authSvc.Login(req.Username, req.Password)
|
||||
if errors.Is(err, auth.ErrInvalidCredentials) {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid credentials"})
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "internal error"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"id": user.ID,
|
||||
"username": user.Username,
|
||||
"role": user.Role,
|
||||
"token": token,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func GetMe(authSvc *auth.AuthService) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
userID := getUserID(c)
|
||||
if userID == 0 {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "not authenticated"})
|
||||
return
|
||||
}
|
||||
user, err := authSvc.GetUserByID(userID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "user not found"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"id": user.ID,
|
||||
"username": user.Username,
|
||||
"role": user.Role,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// --- Script handlers ---
|
||||
|
||||
type createRequest struct {
|
||||
Title string `json:"title" binding:"required,max=128"`
|
||||
Description string `json:"description" binding:"max=512"`
|
||||
Content string `json:"content" binding:"required,max=16384"`
|
||||
Runtime string `json:"runtime" binding:"required"`
|
||||
CategoryID uint `json:"category_id" binding:"required"`
|
||||
ExpiresIn string `json:"expires_in" binding:"required,oneof=1h 24h 7d 30d"`
|
||||
Publish bool `json:"publish"`
|
||||
}
|
||||
@@ -33,17 +130,20 @@ func CreateScript(svc *service.ScriptService) gin.HandlerFunc {
|
||||
scheme = "http"
|
||||
}
|
||||
|
||||
userID := getUserID(c)
|
||||
|
||||
result, err := svc.Create(service.CreateInput{
|
||||
Title: req.Title,
|
||||
Description: req.Description,
|
||||
Content: req.Content,
|
||||
Runtime: req.Runtime,
|
||||
CategoryID: req.CategoryID,
|
||||
ExpiresIn: req.ExpiresIn,
|
||||
Publish: req.Publish,
|
||||
UserID: userID,
|
||||
}, scheme, c.Request.Host)
|
||||
|
||||
if errors.Is(err, service.ErrInvalidRuntime) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid runtime"})
|
||||
if errors.Is(err, service.ErrInvalidCategory) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid category"})
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
@@ -52,16 +152,14 @@ func CreateScript(svc *service.ScriptService) gin.HandlerFunc {
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, gin.H{
|
||||
"id": result.Script.ID,
|
||||
"title": result.Script.Title,
|
||||
"description": result.Script.Description,
|
||||
"admin_token": result.AdminToken,
|
||||
"url": result.RawURL,
|
||||
"command": result.Command,
|
||||
"source_command": result.SourceCmd,
|
||||
"runtime": result.Script.Runtime,
|
||||
"status": result.Script.Status,
|
||||
"expires_at": result.Script.ExpiresAt,
|
||||
"id": result.Script.ID,
|
||||
"title": result.Script.Title,
|
||||
"description": result.Script.Description,
|
||||
"category_id": result.Script.CategoryID,
|
||||
"admin_token": result.AdminToken,
|
||||
"url": result.RawURL,
|
||||
"status": result.Script.Status,
|
||||
"expires_at": result.Script.ExpiresAt,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -80,15 +178,50 @@ func GetScript(svc *service.ScriptService) gin.HandlerFunc {
|
||||
return
|
||||
}
|
||||
|
||||
cat, _ := svc.GetCategoryByID(script.CategoryID)
|
||||
variants, _ := svc.GetVariants(script.CategoryID)
|
||||
|
||||
scheme := "https"
|
||||
if strings.HasPrefix(c.Request.Host, "localhost") || strings.HasPrefix(c.Request.Host, "127.0.0.1") {
|
||||
scheme = "http"
|
||||
}
|
||||
fullURL := scheme + "://" + c.Request.Host + "/raw/" + script.ID
|
||||
|
||||
var commands []gin.H
|
||||
for _, v := range variants {
|
||||
cmd := strings.ReplaceAll(v.CommandTemplate, "{url}", fullURL)
|
||||
entry := gin.H{
|
||||
"variant": v.Name,
|
||||
"label": v.Label,
|
||||
"command": cmd,
|
||||
}
|
||||
if v.SourceTemplate != "" {
|
||||
entry["source_command"] = strings.ReplaceAll(v.SourceTemplate, "{url}", fullURL)
|
||||
}
|
||||
commands = append(commands, entry)
|
||||
}
|
||||
|
||||
categoryName := ""
|
||||
categoryLabel := ""
|
||||
categoryIcon := ""
|
||||
if cat != nil {
|
||||
categoryName = cat.Name
|
||||
categoryLabel = cat.Label
|
||||
categoryIcon = cat.Icon
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"id": script.ID,
|
||||
"title": script.Title,
|
||||
"description": script.Description,
|
||||
"runtime": script.Runtime,
|
||||
"category_id": script.CategoryID,
|
||||
"category_name": categoryName,
|
||||
"category_label": categoryLabel,
|
||||
"category_icon": categoryIcon,
|
||||
"content": script.Content,
|
||||
"content_length": len(script.Content),
|
||||
"status": script.Status,
|
||||
"created_at": script.CreatedAt,
|
||||
"commands": commands,
|
||||
"expires_at": script.ExpiresAt,
|
||||
"published_at": script.PublishedAt,
|
||||
"expired": false,
|
||||
@@ -101,17 +234,17 @@ func PublishScript(svc *service.ScriptService) gin.HandlerFunc {
|
||||
id := c.Param("id")
|
||||
token := c.Query("token")
|
||||
if token == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "token query parameter is required"})
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "token required"})
|
||||
return
|
||||
}
|
||||
|
||||
script, err := svc.Publish(id, token)
|
||||
if errors.Is(err, service.ErrNotFound) {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "script not found or invalid token"})
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "not found or invalid token"})
|
||||
return
|
||||
}
|
||||
if errors.Is(err, service.ErrAlreadyPublished) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "script already published"})
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "already published"})
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
@@ -119,11 +252,27 @@ func PublishScript(svc *service.ScriptService) gin.HandlerFunc {
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"id": script.ID,
|
||||
"status": script.Status,
|
||||
"published_at": script.PublishedAt,
|
||||
})
|
||||
c.JSON(http.StatusOK, gin.H{"id": script.ID, "status": script.Status, "published_at": script.PublishedAt})
|
||||
}
|
||||
}
|
||||
|
||||
func DeleteScript(svc *service.ScriptService) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
token := c.Query("token")
|
||||
if token == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "token required"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := svc.Delete(id, token); errors.Is(err, service.ErrNotFound) {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
|
||||
return
|
||||
} else if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "internal error"})
|
||||
return
|
||||
}
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,14 +280,14 @@ func ListMarket(svc *service.ScriptService) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
perPage, _ := strconv.Atoi(c.DefaultQuery("per_page", "20"))
|
||||
runtime := c.Query("runtime")
|
||||
catID, _ := strconv.Atoi(c.DefaultQuery("category_id", "0"))
|
||||
search := c.Query("search")
|
||||
|
||||
result, err := svc.ListMarket(service.MarketQuery{
|
||||
Page: page,
|
||||
PerPage: perPage,
|
||||
Runtime: runtime,
|
||||
Search: search,
|
||||
Page: page,
|
||||
PerPage: perPage,
|
||||
CategoryID: uint(catID),
|
||||
Search: search,
|
||||
})
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "internal error"})
|
||||
@@ -147,12 +296,16 @@ func ListMarket(svc *service.ScriptService) gin.HandlerFunc {
|
||||
|
||||
items := make([]gin.H, len(result.Items))
|
||||
for i, s := range result.Items {
|
||||
cat, _ := svc.GetCategoryByID(s.CategoryID)
|
||||
items[i] = gin.H{
|
||||
"id": s.ID,
|
||||
"title": s.Title,
|
||||
"description": s.Description,
|
||||
"runtime": s.Runtime,
|
||||
"published_at": s.PublishedAt,
|
||||
"id": s.ID,
|
||||
"title": s.Title,
|
||||
"description": s.Description,
|
||||
"category_id": s.CategoryID,
|
||||
"category_name": cat.Name,
|
||||
"category_label": cat.Label,
|
||||
"category_icon": cat.Icon,
|
||||
"published_at": s.PublishedAt,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,23 +318,101 @@ func ListMarket(svc *service.ScriptService) gin.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
func DeleteScript(svc *service.ScriptService) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
token := c.Query("token")
|
||||
if token == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "token query parameter is required"})
|
||||
return
|
||||
}
|
||||
// --- Admin: Runtime management ---
|
||||
|
||||
if err := svc.Delete(id, token); errors.Is(err, service.ErrNotFound) {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "script not found or invalid token"})
|
||||
return
|
||||
} else if err != nil {
|
||||
func ListCategories(svc *service.ScriptService) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
cats, err := svc.ListCategories()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "internal error"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"categories": cats})
|
||||
}
|
||||
}
|
||||
|
||||
func CreateCategory(svc *service.ScriptService) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
var cat model.RuntimeCategory
|
||||
if err := c.ShouldBindJSON(&cat); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if err := svc.CreateCategory(&cat); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "internal error"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusCreated, gin.H{"category": cat})
|
||||
}
|
||||
}
|
||||
|
||||
func UpdateCategory(svc *service.ScriptService) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
var cat model.RuntimeCategory
|
||||
cat.ID = uint(id)
|
||||
if err := c.ShouldBindJSON(&cat); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if err := svc.UpdateCategory(&cat); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "internal error"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"category": cat})
|
||||
}
|
||||
}
|
||||
|
||||
func DeleteCategory(svc *service.ScriptService) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
if err := svc.DeleteCategory(uint(id)); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "internal error"})
|
||||
return
|
||||
}
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
}
|
||||
|
||||
func CreateVariant(svc *service.ScriptService) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
var v model.RuntimeVariant
|
||||
if err := c.ShouldBindJSON(&v); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if err := svc.CreateVariant(&v); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "internal error"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusCreated, gin.H{"variant": v})
|
||||
}
|
||||
}
|
||||
|
||||
func UpdateVariant(svc *service.ScriptService) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
var v model.RuntimeVariant
|
||||
v.ID = uint(id)
|
||||
if err := c.ShouldBindJSON(&v); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if err := svc.UpdateVariant(&v); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "internal error"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"variant": v})
|
||||
}
|
||||
}
|
||||
|
||||
func DeleteVariant(svc *service.ScriptService) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
if err := svc.DeleteVariant(uint(id)); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "internal error"})
|
||||
return
|
||||
}
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user