Initialize AutoLibrary website with Gitea deployment
This commit is contained in:
commit
18216953e7
101 files changed
+12581
No files matched your search
@@ -0,0 +1,9 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
_ "autolibrary.top/webserver/backend/handlers/errors_handler"
|
||||
_ "autolibrary.top/webserver/backend/handlers/health_handler"
|
||||
_ "autolibrary.top/webserver/backend/handlers/versions_handler"
|
||||
_ "autolibrary.top/webserver/backend/handlers/bulletins_handler"
|
||||
_ "autolibrary.top/webserver/backend/handlers/themes_handler"
|
||||
)
|
||||
@@ -0,0 +1,310 @@
|
||||
package bulletins_handler
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"autolibrary.top/webserver/utils"
|
||||
"autolibrary.top/webserver/backend/database/bulletins_database"
|
||||
"autolibrary.top/webserver/backend/models/bulletins_model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
/*
|
||||
These handlers are used to manage bulletins.
|
||||
*/
|
||||
|
||||
// GET /bulletins?date={yyyy-MM-dd}&time={HH:mm:ss}&range_hour={0<number}
|
||||
//
|
||||
// This handler regard the given 'date' and 'time' in Shanghai(+8:00) time zone.
|
||||
func GetBulletins(c *gin.Context) {
|
||||
dateStr := c.Query("date")
|
||||
timeStr := c.Query("time")
|
||||
rangeHourStr := c.Query("range_hour")
|
||||
|
||||
loc := utils.ShanghaiLocation
|
||||
|
||||
// Default to current date_time and range
|
||||
if dateStr == "" {
|
||||
dateStr = utils.NowInShanghai().Format("2006-01-02")
|
||||
}
|
||||
if timeStr == "" {
|
||||
timeStr = "00:00:00"
|
||||
}
|
||||
if rangeHourStr == "" {
|
||||
rangeHourStr = "24"
|
||||
}
|
||||
|
||||
dateTimeStr := dateStr + " " + timeStr
|
||||
dateTime, err := utils.ParseInShanghai("2006-01-02 15:04:05", dateTimeStr)
|
||||
if err != nil {
|
||||
log.Printf("[E] GET > try to get bulletins, but got invalid (date=%s, time=%s, tz=%s) parameters: %v", dateStr, timeStr, loc.String(), err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"code": http.StatusBadRequest,
|
||||
"msg": "GET > try to get bulletins, but got invalid parameters: 'date' or 'time'",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
rangeHours, err := strconv.Atoi(rangeHourStr)
|
||||
if err != nil {
|
||||
log.Printf("[E] GET > try to get bulletins, but got invalid (range_hour=%s) parameter: %v", rangeHourStr, err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"code": http.StatusBadRequest,
|
||||
"msg": "GET > try to get bulletins, but got invalid parameter: 'range_hour'",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
startOfDateTime := dateTime
|
||||
endOfDateTime := startOfDateTime.Add(time.Duration(rangeHours)*time.Hour)
|
||||
startOfDateTimeStr := startOfDateTime.Format(time.RFC3339)
|
||||
endOfDateTimeStr := endOfDateTime.Format(time.RFC3339)
|
||||
|
||||
var bulletins []bulletins_model.Bulletin
|
||||
result := bulletins_database.DB.Where(
|
||||
"date_time >= ? AND date_time < ? AND is_deleted = ?", startOfDateTime, endOfDateTime, false,
|
||||
).Find(&bulletins)
|
||||
if result.Error != nil {
|
||||
log.Printf("[E] GET > occur an error when get bulletins from %s to %s: %v", startOfDateTimeStr, endOfDateTimeStr, result.Error)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"code": http.StatusInternalServerError,
|
||||
"msg": "GET > occur an error when get bulletins",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var deleteIds []int64
|
||||
result = bulletins_database.DB.Model(bulletins_model.Bulletin{}).Where(
|
||||
"date_time >= ? AND date_time < ? AND is_deleted = ?", startOfDateTime, endOfDateTime, true,
|
||||
).Pluck("id", &deleteIds)
|
||||
if result.Error != nil {
|
||||
log.Printf("[E] GET > occur an error when get bulletins from %s to %s: %v", startOfDateTimeStr, endOfDateTimeStr, result.Error)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"code": http.StatusInternalServerError,
|
||||
"msg": "GET > occur an error when get bulletins",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
data := gin.H{
|
||||
"bulletins": bulletins,
|
||||
"delete_ids": deleteIds,
|
||||
}
|
||||
|
||||
log.Printf("[I] GET > get bulletins from %s to %s successfully", startOfDateTimeStr, endOfDateTimeStr)
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": http.StatusOK,
|
||||
"msg": "GET > get bulletins successful",
|
||||
"data": data,
|
||||
})
|
||||
}
|
||||
|
||||
// GET /bulletins/:id
|
||||
func GetBulletinByID(c *gin.Context) {
|
||||
id, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
log.Printf("[E] GET > try to get bulletin, but got invalid id parameter: %v", err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"code": http.StatusBadRequest,
|
||||
"msg": "GET > try to get bulletin, but got invalid id",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var bulletin bulletins_model.Bulletin
|
||||
result := bulletins_database.DB.Where("is_deleted = ?", false).First(&bulletin, id)
|
||||
if result.Error != nil {
|
||||
log.Printf("[E] GET > bulletin (id=%d) can not found: %v", id, result.Error)
|
||||
c.JSON(http.StatusNotFound, gin.H{
|
||||
"code": http.StatusNotFound,
|
||||
"msg": "GET > bulletin can not found",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("[I] GET > get bulletin (id=%d) successfully", id)
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": http.StatusOK,
|
||||
"msg": "GET > get bulletin successful",
|
||||
"data": bulletin,
|
||||
})
|
||||
}
|
||||
|
||||
// POST /bulletins
|
||||
func PostBulletin(c *gin.Context) {
|
||||
var bulletin bulletins_model.Bulletin
|
||||
if err := c.ShouldBindJSON(&bulletin); err != nil {
|
||||
log.Printf("[E] POST > try to create bulletin, but got invalid request data: %v", err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"code": http.StatusBadRequest,
|
||||
"msg": "POST > try to create bulletin, but got invalid request data",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
bulletin.CreateAt = utils.NowInShanghai()
|
||||
// set default value to make sure the bulletin is not edited and not deleted before
|
||||
// get into the database
|
||||
bulletin.IsEdited = false
|
||||
bulletin.IsDeleted = false
|
||||
result := bulletins_database.DB.Create(&bulletin)
|
||||
if result.Error != nil {
|
||||
log.Printf("[E] POST > occur an error when create bulletin: %v", result.Error)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"code": http.StatusInternalServerError,
|
||||
"msg": "POST > occur an error when create bulletin",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
dataTimeStr := bulletin.DateTime.Format(time.RFC3339)
|
||||
|
||||
id := strconv.FormatUint(uint64(bulletin.ID), 10)
|
||||
log.Printf("[I] POST > create bulletin (id=%s, date_time=%s) successful", id, dataTimeStr)
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": http.StatusOK,
|
||||
"msg": "POST > create bulletin successful",
|
||||
"data": bulletin,
|
||||
})
|
||||
}
|
||||
|
||||
// PUT /bulletins/:id
|
||||
func PutBulletinByID(c *gin.Context) {
|
||||
id, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
log.Printf("[E] PUT > try to update bulletin, but got invalid id parameter: %v", err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"code": http.StatusBadRequest,
|
||||
"msg": "PUT > try to update bulletin, but got invalid id",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var updateBulletin bulletins_model.Bulletin
|
||||
if err := c.ShouldBindJSON(&updateBulletin); err != nil {
|
||||
log.Printf("[E] PUT > try to update bulletin, but got invalid request data: %v", err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"code": http.StatusBadRequest,
|
||||
"msg": "PUT > try to update bulletin, but got invalid request data",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var existingBulletin bulletins_model.Bulletin
|
||||
result := bulletins_database.DB.Where("is_deleted = ?", false).First(&existingBulletin, id)
|
||||
if result.Error != nil {
|
||||
log.Printf("[E] PUT > bulletin (id=%d) can not found: %v", id, result.Error)
|
||||
c.JSON(http.StatusNotFound, gin.H{
|
||||
"code": http.StatusNotFound,
|
||||
"msg": "PUT > bulletin can not found",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// keep the original date time
|
||||
updateBulletin.DateTime = existingBulletin.DateTime
|
||||
// set last edit time to current time
|
||||
updateBulletin.LastEditAt = utils.NowInShanghai()
|
||||
// set is edited to true
|
||||
updateBulletin.IsEdited = true
|
||||
|
||||
result = bulletins_database.DB.Model(&existingBulletin).Where("id = ?", id).Updates(updateBulletin)
|
||||
if result.Error != nil {
|
||||
log.Printf("[E] PUT > occur an error when update bulletin (id=%d): %v", id, result.Error)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"code": http.StatusInternalServerError,
|
||||
"msg": "PUT > occur an error when update bulletin",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// post check after update
|
||||
var updatedBulletin bulletins_model.Bulletin
|
||||
if result = bulletins_database.DB.Where("is_deleted = ?", false).First(&updatedBulletin, id); result.Error == nil {
|
||||
log.Printf("[I] PUT > update bulletin (id=%d) successful", id)
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": http.StatusOK,
|
||||
"msg": "PUT > update bulletin successful",
|
||||
"data": updatedBulletin,
|
||||
})
|
||||
} else {
|
||||
log.Printf("[E] PUT > update successful but failed to retrieve updated bulletin (id=%d): %v", id, result.Error)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"code": http.StatusInternalServerError,
|
||||
"msg": "PUT > update successful but failed to retrieve updated bulletin",
|
||||
"data": nil,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// DELETE /bulletins/:id
|
||||
func DeleteBulletinByID(c *gin.Context) {
|
||||
id, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
log.Printf("[E] DELETE > try to delete bulletin, but got invalid id: %v", err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"code": http.StatusBadRequest,
|
||||
"msg": "DELETE > try to delete bulletin, but got invalid id",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// soft delete, actually is a update operation
|
||||
var existingBulletin bulletins_model.Bulletin
|
||||
result := bulletins_database.DB.Where("is_deleted = ?", false).First(&existingBulletin, id)
|
||||
if result.Error != nil {
|
||||
log.Printf("[E] DELETE > bulletin (id=%d) can not found: %v", id, result.Error)
|
||||
c.JSON(http.StatusNotFound, gin.H{
|
||||
"code": http.StatusNotFound,
|
||||
"msg": "DELETE > bulletin can not found",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
// set is_deleted to true
|
||||
existingBulletin.IsDeleted = true
|
||||
|
||||
result = bulletins_database.DB.Model(&existingBulletin).Where("id = ?", id).Updates(existingBulletin)
|
||||
if result.Error != nil {
|
||||
log.Printf("[E] DELETE > occur an error when soft delete bulletin (id=%d): %v", id, result.Error)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"code": http.StatusInternalServerError,
|
||||
"msg": "DELETE > occur an error when soft delete bulletin",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// post check after soft delete
|
||||
var deletedBulletin bulletins_model.Bulletin
|
||||
if result = bulletins_database.DB.Where("is_deleted = ?", true).First(&deletedBulletin, id); result.Error == nil {
|
||||
log.Printf("[I] DELETE > soft delete bulletin (id=%d) successful", id)
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": http.StatusOK,
|
||||
"msg": "DELETE > soft delete bulletin successful",
|
||||
"data": deletedBulletin,
|
||||
})
|
||||
} else {
|
||||
log.Printf("[E] DELETE > soft delete successful but failed to retrieve deleted bulletin (id=%d): %v", id, result.Error)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"code": http.StatusInternalServerError,
|
||||
"msg": "DELETE > soft delete successful but failed to retrieve deleted bulletin",
|
||||
"data": nil,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package errors_handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// 404 - Not Found
|
||||
func GetNotFoundError(c *gin.Context) {
|
||||
errorData := gin.H{
|
||||
"code": http.StatusNotFound,
|
||||
"msg": c.Request.Method + " > the requested API endpoint was not found",
|
||||
"data": gin.H{
|
||||
"path": c.Request.URL.Path,
|
||||
"method": c.Request.Method,
|
||||
"endpoints": gin.H{
|
||||
"health": "/health",
|
||||
"downloads": "/download/versions",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
acceptHeader := c.GetHeader("Accept")
|
||||
if strings.Contains(acceptHeader, "text/html") {
|
||||
// for browser requests, return HTML
|
||||
jsonBytes, err := json.Marshal(errorData)
|
||||
if err != nil {
|
||||
jsonBytes = []byte(`{"error": "json_serialization_failed"}`)
|
||||
}
|
||||
jsonStr := string(jsonBytes)
|
||||
c.HTML(http.StatusNotFound, "404.html", gin.H{
|
||||
"title": "404 - Not Found",
|
||||
"path": c.Request.URL.Path,
|
||||
"method": c.Request.Method,
|
||||
"baseURL": c.Request.Host,
|
||||
"jsonData": jsonStr,
|
||||
})
|
||||
return
|
||||
}
|
||||
// for General requests (no text/html in Accept header), return JSON
|
||||
c.JSON(http.StatusNotFound, errorData)
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package health_handler
|
||||
|
||||
import (
|
||||
"log"
|
||||
"time"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// GET /health
|
||||
func GetHealth(c *gin.Context) {
|
||||
log.Printf("[I] HEALTH > get backend server health")
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": http.StatusOK,
|
||||
"msg": "GET > backend server is running ok",
|
||||
"data": gin.H{
|
||||
"timestamp": time.Now().Unix(),
|
||||
"status": "healthy",
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
package themes_handler
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/dchest/captcha"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func (h *Handler) GetUserRole(c *gin.Context) {
|
||||
userID, _ := c.Get("user_id")
|
||||
userName, _ := c.Get("user_name")
|
||||
displayName, _ := c.Get("display_name")
|
||||
isAdmin, _ := c.Get("is_admin")
|
||||
userTag, _ := c.Get("user_tag")
|
||||
|
||||
isAdminBool := false
|
||||
if isAdmin != nil {
|
||||
isAdminBool = isAdmin.(bool)
|
||||
}
|
||||
displayNameStr := ""
|
||||
if displayName != nil {
|
||||
displayNameStr = displayName.(string)
|
||||
}
|
||||
userTagStr := ""
|
||||
if userTag != nil {
|
||||
userTagStr = userTag.(string)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": http.StatusOK,
|
||||
"msg": "GET > user role",
|
||||
"data": gin.H{
|
||||
"user_id": userID,
|
||||
"user_name": userName,
|
||||
"display_name": displayNameStr,
|
||||
"is_admin": isAdminBool,
|
||||
"is_verified": isAdminBool || userTagStr == "verified",
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// GET /api/user/downloads (stretch goal)
|
||||
|
||||
func (h *Handler) GetUserDownloads(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": http.StatusOK,
|
||||
"msg": "GET > download history not yet implemented",
|
||||
"data": []interface{}{},
|
||||
})
|
||||
}
|
||||
|
||||
// GET /api/marketplace/auth/captcha
|
||||
|
||||
func (h *Handler) GetCaptcha(c *gin.Context) {
|
||||
id := captcha.NewLen(4)
|
||||
var buf bytes.Buffer
|
||||
if err := captcha.WriteImage(&buf, id, 150, 50); err != nil {
|
||||
log.Printf("[E] CAPTCHA > failed to generate: %v", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": http.StatusInternalServerError, "msg": "captcha generation failed", "data": nil})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": http.StatusOK,
|
||||
"msg": "GET > captcha",
|
||||
"data": gin.H{
|
||||
"captchaId": id,
|
||||
"captchaImage": "data:image/png;base64," + base64.StdEncoding.EncodeToString(buf.Bytes()),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// GET /api/marketplace/auth/config
|
||||
|
||||
func (h *Handler) GetAuthConfig(c *gin.Context) {
|
||||
cfg := h.Config
|
||||
casdoorConfigured := cfg.Backend.Casdoor.Endpoint != "" && cfg.Backend.Casdoor.ClientID != ""
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": http.StatusOK,
|
||||
"msg": "GET > auth config",
|
||||
"data": gin.H{
|
||||
"casdoor_configured": casdoorConfigured,
|
||||
"casdoor_endpoint": cfg.Backend.Casdoor.Endpoint,
|
||||
"casdoor_client_id": cfg.Backend.Casdoor.ClientID,
|
||||
"casdoor_organization": cfg.Backend.Casdoor.Organization,
|
||||
"casdoor_application": cfg.Backend.Casdoor.Application,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// POST /api/marketplace/auth/login
|
||||
|
||||
func (h *Handler) LoginProxy(c *gin.Context) {
|
||||
cfg := h.Config
|
||||
|
||||
var req struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"code": http.StatusBadRequest, "msg": "PUT > invalid request body", "data": nil})
|
||||
return
|
||||
}
|
||||
|
||||
// Proxy to Casdoor OAuth token endpoint
|
||||
tokenURL := cfg.Backend.Casdoor.Endpoint + "/api/login/oauth/access_token"
|
||||
values := url.Values{
|
||||
"grant_type": {"password"},
|
||||
"client_id": {cfg.Backend.Casdoor.ClientID},
|
||||
"client_secret": {cfg.Backend.Casdoor.ClientSecret},
|
||||
"username": {req.Username},
|
||||
"password": {req.Password},
|
||||
"scope": {"read"},
|
||||
}
|
||||
reqHTTP, err := http.NewRequestWithContext(c.Request.Context(), http.MethodPost, tokenURL, strings.NewReader(values.Encode()))
|
||||
if err != nil {
|
||||
fail(c, http.StatusBadGateway, "Invalid authentication server configuration")
|
||||
return
|
||||
}
|
||||
reqHTTP.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
resp, err := h.HTTP.Do(reqHTTP)
|
||||
if err != nil {
|
||||
log.Printf("[E] AUTH > login proxy error: %v", err)
|
||||
c.JSON(http.StatusBadGateway, gin.H{"code": http.StatusBadGateway, "msg": "auth server unreachable", "data": nil})
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, readErr := io.ReadAll(io.LimitReader(resp.Body, 2<<20))
|
||||
if readErr != nil {
|
||||
fail(c, http.StatusBadGateway, "Unable to read authentication response")
|
||||
return
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"code": http.StatusUnauthorized, "msg": "invalid credentials", "data": nil})
|
||||
return
|
||||
}
|
||||
|
||||
c.Header("Content-Type", "application/json")
|
||||
c.String(http.StatusOK, string(body))
|
||||
}
|
||||
|
||||
// POST /api/marketplace/auth/signup
|
||||
|
||||
func (h *Handler) SignupProxy(c *gin.Context) {
|
||||
cfg := h.Config
|
||||
|
||||
var req struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Email string `json:"email"`
|
||||
Name string `json:"name"`
|
||||
CaptchaId string `json:"captchaId"`
|
||||
CaptchaCode string `json:"captchaCode"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"code": http.StatusBadRequest, "msg": "PUT > invalid request body", "data": nil})
|
||||
return
|
||||
}
|
||||
if req.Username == "" || req.Password == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"code": http.StatusBadRequest, "msg": "用户名和密码为必填项", "data": nil})
|
||||
return
|
||||
}
|
||||
|
||||
// Validate captcha
|
||||
if !captcha.VerifyString(req.CaptchaId, req.CaptchaCode) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"code": http.StatusBadRequest, "msg": "验证码错误或已过期", "data": nil})
|
||||
return
|
||||
}
|
||||
|
||||
// Proxy to Casdoor signup API
|
||||
signupData := map[string]interface{}{
|
||||
"organization": cfg.Backend.Casdoor.Organization,
|
||||
"username": req.Username,
|
||||
"password": req.Password,
|
||||
"application": cfg.Backend.Casdoor.Application,
|
||||
}
|
||||
if req.Email != "" {
|
||||
signupData["email"] = req.Email
|
||||
}
|
||||
if req.Name != "" {
|
||||
signupData["name"] = req.Name
|
||||
}
|
||||
signupURL := cfg.Backend.Casdoor.Endpoint + "/api/signup"
|
||||
signupBody, _ := json.Marshal(signupData)
|
||||
reqHTTP, err := http.NewRequestWithContext(c.Request.Context(), http.MethodPost, signupURL, bytes.NewReader(signupBody))
|
||||
if err != nil {
|
||||
fail(c, http.StatusBadGateway, "Invalid authentication server configuration")
|
||||
return
|
||||
}
|
||||
reqHTTP.Header.Set("Content-Type", "application/json")
|
||||
resp, err := h.HTTP.Do(reqHTTP)
|
||||
if err != nil {
|
||||
log.Printf("[E] AUTH > signup proxy error: %v", err)
|
||||
c.JSON(http.StatusBadGateway, gin.H{"code": http.StatusBadGateway, "msg": "auth server unreachable", "data": nil})
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
|
||||
var result map[string]interface{}
|
||||
json.Unmarshal(body, &result)
|
||||
|
||||
if status, ok := result["status"]; !ok || status != "ok" {
|
||||
msg := "注册失败"
|
||||
if m, ok := result["msg"]; ok {
|
||||
msg = fmt.Sprintf("%v", m)
|
||||
}
|
||||
c.JSON(http.StatusBadRequest, gin.H{"code": http.StatusBadRequest, "msg": msg, "data": nil})
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("[I] AUTH > signup successful for user: %s", req.Username)
|
||||
c.Header("Content-Type", "application/json")
|
||||
c.String(http.StatusOK, string(body))
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package themes_handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"autolibrary.top/webserver/backend/services/theme_files"
|
||||
"autolibrary.top/webserver/backend/services/themes_service"
|
||||
"autolibrary.top/webserver/backend/services/users_service"
|
||||
"autolibrary.top/webserver/config"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// Handler shares explicit dependencies across the marketplace HTTP handlers.
|
||||
// A separate instance can be constructed for each database and storage root.
|
||||
type Handler struct {
|
||||
DB *gorm.DB
|
||||
Config *config.Config
|
||||
Files *theme_files.Store
|
||||
Users *users_service.Cache
|
||||
HTTP *http.Client
|
||||
Themes *themes_service.Service
|
||||
}
|
||||
|
||||
func New(db *gorm.DB, cfg *config.Config, files *theme_files.Store, users *users_service.Cache) *Handler {
|
||||
return &Handler{DB: db, Config: cfg, Files: files, Users: users, Themes: &themes_service.Service{DB: db, Files: files}, HTTP: &http.Client{Timeout: 15 * time.Second}}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package themes_handler
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"mime"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"autolibrary.top/webserver/backend/models/themes_model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func (h *Handler) DownloadTheme(c *gin.Context) { h.serveTheme(c, true) }
|
||||
func (h *Handler) AdminDownloadTheme(c *gin.Context) { h.serveTheme(c, false) }
|
||||
|
||||
func (h *Handler) serveTheme(c *gin.Context, public bool) {
|
||||
id, err := parseThemeID(c)
|
||||
if err != nil {
|
||||
fail(c, http.StatusBadRequest, "Invalid theme ID")
|
||||
return
|
||||
}
|
||||
var theme themes_model.Theme
|
||||
query := h.DB
|
||||
if public {
|
||||
query = query.Where("status = ?", themes_model.ThemeApproved)
|
||||
}
|
||||
err = query.First(&theme, id).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
fail(c, 404, "Theme not found")
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
fail(c, 500, "Unable to load theme")
|
||||
return
|
||||
}
|
||||
path := h.Themes.ArchivePath(theme.ID)
|
||||
info, err := os.Stat(path)
|
||||
if err != nil || info.IsDir() {
|
||||
fail(c, 404, "Theme file not found")
|
||||
return
|
||||
}
|
||||
if public {
|
||||
h.DB.Model(&theme).UpdateColumn("download_count", gorm.Expr("download_count + 1"))
|
||||
}
|
||||
version := strings.TrimLeft(theme.Version, "vV")
|
||||
name := fmt.Sprintf("%s_v%s.altheme", theme.Name, version)
|
||||
c.Header("Content-Disposition", mime.FormatMediaType("attachment", map[string]string{"filename": name}))
|
||||
c.Header("Content-Type", "application/zip")
|
||||
c.File(path)
|
||||
}
|
||||
|
||||
func (h *Handler) ServePreview(c *gin.Context) {
|
||||
name := c.Param("filename")
|
||||
if name == "." || name == ".." || strings.ContainsAny(name, `/\`) || filepath.Base(name) != name {
|
||||
fail(c, 400, "Invalid preview filename")
|
||||
return
|
||||
}
|
||||
path := filepath.Join(h.Files.Root(), "previews", name)
|
||||
info, err := os.Stat(path)
|
||||
if err != nil || info.IsDir() {
|
||||
fail(c, 404, "Preview not found")
|
||||
return
|
||||
}
|
||||
c.Header("Cache-Control", "public, max-age=3600")
|
||||
c.File(path)
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
package themes_handler
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"autolibrary.top/webserver/backend/models/themes_model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func (h *Handler) GetFeaturedThemes(c *gin.Context) {
|
||||
limitStr := c.Query("limit")
|
||||
limit := 6
|
||||
if limitStr != "" {
|
||||
if v, err := strconv.Atoi(limitStr); err == nil && v > 0 && v <= 20 {
|
||||
limit = v
|
||||
}
|
||||
}
|
||||
|
||||
var themes []themes_model.Theme
|
||||
result := h.DB.
|
||||
Where("status = ?", themes_model.ThemeApproved).
|
||||
Order("download_count DESC").
|
||||
Limit(limit).
|
||||
Find(&themes)
|
||||
if result.Error != nil {
|
||||
log.Printf("[E] GET > occur an error when get featured themes: %v", result.Error)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"code": http.StatusInternalServerError,
|
||||
"msg": "GET > occur an error when get featured themes",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
for i := range themes {
|
||||
themes[i].Author = h.Users.ResolveAuthor(themes[i].UserID, themes[i].Author)
|
||||
themes[i].Verified = h.Users.IsVerified(themes[i].UserID)
|
||||
}
|
||||
|
||||
log.Printf("[I] GET > get featured themes (limit=%d) successful", limit)
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": http.StatusOK,
|
||||
"msg": "GET > get featured themes successful",
|
||||
"data": themes,
|
||||
})
|
||||
}
|
||||
|
||||
// GET /api/themes?page=1&per_page=9&need_theme=&tags=&search=&sort=popular
|
||||
|
||||
func (h *Handler) GetThemes(c *gin.Context) {
|
||||
pageStr := c.Query("page")
|
||||
perPageStr := c.Query("per_page")
|
||||
needTheme := c.Query("need_theme")
|
||||
tagsStr := c.Query("tags")
|
||||
search := c.Query("search")
|
||||
sort := c.Query("sort")
|
||||
|
||||
if pageStr == "" {
|
||||
pageStr = "1"
|
||||
}
|
||||
if perPageStr == "" {
|
||||
perPageStr = "9"
|
||||
}
|
||||
if sort == "" {
|
||||
sort = "popular"
|
||||
}
|
||||
|
||||
page, err := strconv.Atoi(pageStr)
|
||||
if err != nil || page <= 0 {
|
||||
page = 1
|
||||
}
|
||||
perPage, err := strconv.Atoi(perPageStr)
|
||||
if err != nil || perPage <= 0 || perPage > 50 {
|
||||
perPage = 9
|
||||
}
|
||||
|
||||
offset := (page - 1) * perPage
|
||||
|
||||
query := h.DB.Where("status = ?", themes_model.ThemeApproved)
|
||||
|
||||
if needTheme != "" {
|
||||
if needTheme == "both" {
|
||||
query = query.Where("need_theme = ?", "both")
|
||||
} else if needTheme == "dark" || needTheme == "light" {
|
||||
query = query.Where("need_theme = ? OR need_theme = ?", needTheme, "both")
|
||||
}
|
||||
}
|
||||
|
||||
if tagsStr != "" {
|
||||
tags := strings.Split(tagsStr, ",")
|
||||
for _, tag := range tags {
|
||||
tag = strings.TrimSpace(tag)
|
||||
if tag != "" {
|
||||
query = query.Where("tags LIKE ?", fmt.Sprintf("%%%s%%", tag))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if search != "" {
|
||||
like := fmt.Sprintf("%%%s%%", search)
|
||||
query = query.Where("name LIKE ? OR brief LIKE ? OR author LIKE ?", like, like, like)
|
||||
}
|
||||
|
||||
switch sort {
|
||||
case "newest":
|
||||
query = query.Order("created_at DESC")
|
||||
case "name":
|
||||
query = query.Order("name ASC")
|
||||
default: // popular
|
||||
query = query.Order("download_count DESC")
|
||||
}
|
||||
|
||||
var total int64
|
||||
if result := query.Model(&themes_model.Theme{}).Count(&total); result.Error != nil {
|
||||
log.Printf("[E] GET > occur an error when count themes: %v", result.Error)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"code": http.StatusInternalServerError,
|
||||
"msg": "GET > occur an error when get themes",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var themes []themes_model.Theme
|
||||
result := query.Limit(perPage).Offset(offset).Find(&themes)
|
||||
if result.Error != nil {
|
||||
log.Printf("[E] GET > occur an error when get themes (page=%d, per_page=%d): %v", page, perPage, result.Error)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"code": http.StatusInternalServerError,
|
||||
"msg": "GET > occur an error when get themes",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
for i := range themes {
|
||||
themes[i].Author = h.Users.ResolveAuthor(themes[i].UserID, themes[i].Author)
|
||||
themes[i].Verified = h.Users.IsVerified(themes[i].UserID)
|
||||
}
|
||||
|
||||
totalPages := int(total) / perPage
|
||||
if int(total)%perPage != 0 {
|
||||
totalPages++
|
||||
}
|
||||
|
||||
log.Printf("[I] GET > get themes (page=%d, per_page=%d, total=%d) successful", page, perPage, total)
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": http.StatusOK,
|
||||
"msg": "GET > get themes successful",
|
||||
"data": gin.H{
|
||||
"themes": themes,
|
||||
"meta": gin.H{
|
||||
"page": page,
|
||||
"per_page": perPage,
|
||||
"total": total,
|
||||
"total_pages": totalPages,
|
||||
"has_next": page < totalPages,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// GET /api/themes/:id
|
||||
|
||||
func (h *Handler) GetThemeByID(c *gin.Context) {
|
||||
id, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
log.Printf("[E] GET > try to get theme, but got invalid id: '%s'", c.Param("id"))
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"code": http.StatusBadRequest,
|
||||
"msg": "GET > invalid id",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var theme themes_model.Theme
|
||||
result := h.DB.Where("status = ?", themes_model.ThemeApproved).First(&theme, id)
|
||||
if result.Error != nil {
|
||||
log.Printf("[E] GET > theme (id=%d) can not found: %v", id, result.Error)
|
||||
c.JSON(http.StatusNotFound, gin.H{
|
||||
"code": http.StatusNotFound,
|
||||
"msg": "GET > theme not found",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
theme.Author = h.Users.ResolveAuthor(theme.UserID, theme.Author)
|
||||
theme.Verified = h.Users.IsVerified(theme.UserID)
|
||||
|
||||
log.Printf("[I] GET > get theme (id=%d) successful", id)
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": http.StatusOK,
|
||||
"msg": "GET > get theme successful",
|
||||
"data": theme,
|
||||
})
|
||||
}
|
||||
|
||||
// GET /api/themes/:id/download
|
||||
|
||||
func (h *Handler) GetUserThemes(c *gin.Context) {
|
||||
userID, _ := c.Get("user_id")
|
||||
|
||||
var themes []themes_model.Theme
|
||||
result := h.DB.
|
||||
Where("user_id = ?", userID.(string)).
|
||||
Order("created_at DESC").
|
||||
Find(&themes)
|
||||
if result.Error != nil {
|
||||
log.Printf("[E] GET > occur an error when get user themes: %v", result.Error)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": http.StatusInternalServerError, "msg": "获取用户主题失败", "data": nil})
|
||||
return
|
||||
}
|
||||
|
||||
for i := range themes {
|
||||
themes[i].Author = h.Users.ResolveAuthor(themes[i].UserID, themes[i].Author)
|
||||
themes[i].Verified = h.Users.IsVerified(themes[i].UserID)
|
||||
}
|
||||
|
||||
log.Printf("[I] GET > get user themes (user_id=%s, count=%d) successful", userID.(string), len(themes))
|
||||
c.JSON(http.StatusOK, gin.H{"code": http.StatusOK, "msg": "GET > get user themes successful", "data": themes})
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
package themes_handler
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"autolibrary.top/webserver/backend/models/themes_model"
|
||||
"autolibrary.top/webserver/utils"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func (h *Handler) GetPendingThemes(c *gin.Context) {
|
||||
var themes []themes_model.Theme
|
||||
result := h.DB.
|
||||
Where("status = ?", themes_model.ThemePending).
|
||||
Order("created_at ASC").
|
||||
Find(&themes)
|
||||
if result.Error != nil {
|
||||
log.Printf("[E] GET > occur an error when get pending themes: %v", result.Error)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"code": http.StatusInternalServerError,
|
||||
"msg": "GET > occur an error when get pending themes",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
for i := range themes {
|
||||
themes[i].Author = h.Users.ResolveAuthor(themes[i].UserID, themes[i].Author)
|
||||
themes[i].Verified = h.Users.IsVerified(themes[i].UserID)
|
||||
}
|
||||
|
||||
log.Printf("[I] GET > get pending themes (count=%d) successful", len(themes))
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": http.StatusOK,
|
||||
"msg": "GET > get pending themes successful",
|
||||
"data": themes,
|
||||
})
|
||||
}
|
||||
|
||||
// PUT /api/admin/themes/:id/review
|
||||
|
||||
func (h *Handler) ReviewTheme(c *gin.Context) {
|
||||
id, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
log.Printf("[E] PUT > try to review theme, but got invalid id: '%s'", c.Param("id"))
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"code": http.StatusBadRequest,
|
||||
"msg": "GET > invalid id",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var body struct {
|
||||
Action string `json:"action"`
|
||||
Comment string `json:"comment"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
log.Printf("[E] PUT > try to review theme (id=%d), but got invalid request data: %v", id, err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"code": http.StatusBadRequest,
|
||||
"msg": "PUT > invalid request body",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if body.Action != "approved" && body.Action != "rejected" {
|
||||
log.Printf("[E] PUT > try to review theme (id=%d), but got invalid action: '%s'", id, body.Action)
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"code": http.StatusBadRequest,
|
||||
"msg": "操作必须为通过或退回",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var theme themes_model.Theme
|
||||
result := h.DB.First(&theme, id)
|
||||
if result.Error != nil {
|
||||
log.Printf("[E] PUT > theme (id=%d) can not found for review: %v", id, result.Error)
|
||||
c.JSON(http.StatusNotFound, gin.H{
|
||||
"code": http.StatusNotFound,
|
||||
"msg": "GET > theme not found",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
theme.Status = themes_model.ThemeStatus(body.Action)
|
||||
theme.ReviewComment = body.Comment
|
||||
theme.ReviewedBy = c.GetString("user_name")
|
||||
if theme.ReviewedBy == "" {
|
||||
theme.ReviewedBy = "api-key"
|
||||
}
|
||||
theme.UpdatedAt = utils.NowInShanghai()
|
||||
|
||||
result = h.DB.Save(&theme)
|
||||
if result.Error != nil {
|
||||
log.Printf("[E] PUT > occur an error when review theme (id=%d): %v", id, result.Error)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"code": http.StatusInternalServerError,
|
||||
"msg": "审核操作失败",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
theme.Author = h.Users.ResolveAuthor(theme.UserID, theme.Author)
|
||||
theme.Verified = h.Users.IsVerified(theme.UserID)
|
||||
|
||||
log.Printf("[I] PUT > review theme (id=%d, action=%s) successful", id, body.Action)
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": http.StatusOK,
|
||||
"msg": "PUT > review theme successful",
|
||||
"data": theme,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package themes_handler
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"autolibrary.top/webserver/backend/models/themes_model"
|
||||
"autolibrary.top/webserver/utils"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func parseThemeID(c *gin.Context) (uint, error) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil || id == 0 {
|
||||
return 0, strconv.ErrSyntax
|
||||
}
|
||||
return uint(id), nil
|
||||
}
|
||||
|
||||
func (h *Handler) PutThemeByID(c *gin.Context) {
|
||||
theme, ok := h.ownTheme(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var update struct {
|
||||
Name string `json:"name"`
|
||||
Brief string `json:"brief"`
|
||||
Description string `json:"description"`
|
||||
NeedTheme string `json:"need_theme"`
|
||||
Tags []string `json:"tags"`
|
||||
Version string `json:"version"`
|
||||
CompatVersion string `json:"compat_version"`
|
||||
PreviewURL string `json:"preview_url"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&update); err != nil {
|
||||
fail(c, 400, "Invalid theme metadata")
|
||||
return
|
||||
}
|
||||
if update.Name != "" && strings.TrimSpace(update.Name) != theme.Name {
|
||||
var count int64
|
||||
if err := h.DB.Model(&themes_model.Theme{}).Where("user_id = ? AND name = ? AND id != ?", theme.UserID, strings.TrimSpace(update.Name), theme.ID).Count(&count).Error; err != nil {
|
||||
fail(c, 500, "Unable to check theme name")
|
||||
return
|
||||
}
|
||||
if count > 0 {
|
||||
fail(c, 409, "You already have a theme with this name")
|
||||
return
|
||||
}
|
||||
theme.Name = strings.TrimSpace(update.Name)
|
||||
}
|
||||
if update.NeedTheme != "" && update.NeedTheme != "dark" && update.NeedTheme != "light" && update.NeedTheme != "both" {
|
||||
fail(c, 400, "Invalid theme mode")
|
||||
return
|
||||
}
|
||||
if utf8.RuneCountInString(update.Brief) > 50 || utf8.RuneCountInString(update.Description) > 100 {
|
||||
fail(c, 400, "Theme description is too long")
|
||||
return
|
||||
}
|
||||
if strings.TrimSpace(update.Brief) != "" {
|
||||
theme.Brief = strings.TrimSpace(update.Brief)
|
||||
}
|
||||
theme.Description = update.Description
|
||||
if update.NeedTheme != "" {
|
||||
theme.NeedTheme = update.NeedTheme
|
||||
}
|
||||
if update.Tags != nil {
|
||||
theme.Tags = update.Tags
|
||||
}
|
||||
if update.Version != "" {
|
||||
theme.Version = update.Version
|
||||
}
|
||||
theme.CompatVersion = update.CompatVersion
|
||||
// Preview changes go through the authenticated image upload endpoint. Preserve
|
||||
// the legacy field only when it refers to the theme's existing preview.
|
||||
if update.PreviewURL != "" && update.PreviewURL != theme.PreviewURL {
|
||||
fail(c, 400, "Use the preview upload endpoint to replace an image")
|
||||
return
|
||||
}
|
||||
theme.Status = themes_model.ThemePending
|
||||
theme.UpdatedAt = utils.NowInShanghai()
|
||||
if err := h.Themes.Save(theme, nil); err != nil {
|
||||
fail(c, 500, "Unable to save theme and archive")
|
||||
return
|
||||
}
|
||||
success(c, 200, "Theme updated; review required", theme)
|
||||
}
|
||||
|
||||
func (h *Handler) DeleteThemeByID(c *gin.Context) {
|
||||
theme, ok := h.ownTheme(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if err := h.Themes.Delete(theme); err != nil {
|
||||
fail(c, 500, "Unable to delete theme")
|
||||
return
|
||||
}
|
||||
success(c, 200, "Theme deleted", nil)
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
package themes_handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"autolibrary.top/webserver/backend/models/themes_model"
|
||||
"autolibrary.top/webserver/backend/services/theme_files"
|
||||
"autolibrary.top/webserver/utils"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func fail(c *gin.Context, status int, message string) {
|
||||
c.JSON(status, gin.H{"code": status, "msg": message, "data": nil})
|
||||
}
|
||||
func success(c *gin.Context, status int, message string, data interface{}) {
|
||||
c.JSON(status, gin.H{"code": status, "msg": message, "data": data})
|
||||
}
|
||||
|
||||
func (h *Handler) ownTheme(c *gin.Context) (*themes_model.Theme, bool) {
|
||||
var theme themes_model.Theme
|
||||
id, err := parseThemeID(c)
|
||||
if err != nil {
|
||||
fail(c, http.StatusBadRequest, "Invalid theme ID")
|
||||
return nil, false
|
||||
}
|
||||
err = h.DB.First(&theme, id).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
fail(c, http.StatusNotFound, "Theme not found")
|
||||
return nil, false
|
||||
}
|
||||
if err != nil {
|
||||
fail(c, http.StatusInternalServerError, "Unable to load theme")
|
||||
return nil, false
|
||||
}
|
||||
if theme.UserID != c.GetString("user_id") {
|
||||
fail(c, http.StatusForbidden, "You can only edit your own themes")
|
||||
return nil, false
|
||||
}
|
||||
return &theme, true
|
||||
}
|
||||
|
||||
// readUpload enforces the same limits advertised by the web form.
|
||||
func readUpload(c *gin.Context, field string, extensions []string, max int64) ([]byte, error) {
|
||||
file, header, err := c.Request.FormFile(field)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer file.Close()
|
||||
ext := strings.ToLower(filepath.Ext(header.Filename))
|
||||
valid := false
|
||||
for _, candidate := range extensions {
|
||||
if ext == candidate {
|
||||
valid = true
|
||||
}
|
||||
}
|
||||
if !valid {
|
||||
return nil, fmt.Errorf("invalid %s file type", field)
|
||||
}
|
||||
if header.Size > max {
|
||||
return nil, fmt.Errorf("%s file is too large", field)
|
||||
}
|
||||
data, err := io.ReadAll(io.LimitReader(file, max+1))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if int64(len(data)) > max {
|
||||
return nil, fmt.Errorf("%s file is too large", field)
|
||||
}
|
||||
if len(data) == 0 {
|
||||
return nil, fmt.Errorf("%s file is empty", field)
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func (h *Handler) savePreview(data []byte) (string, error) {
|
||||
compressed, err := theme_files.CompressPreviewImage(data)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
name := fmt.Sprintf("preview_%d.jpg", utils.NowInShanghai().UnixNano())
|
||||
if err := h.Files.WriteFile(filepath.Join(h.Files.Root(), "previews", name), compressed, 0644); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return "/marketplace/themes/preview/" + name, nil
|
||||
}
|
||||
|
||||
func (h *Handler) PostTheme(c *gin.Context) {
|
||||
var metadata struct {
|
||||
Name string `json:"name"`
|
||||
Brief string `json:"brief"`
|
||||
Description string `json:"description"`
|
||||
NeedTheme string `json:"need_theme"`
|
||||
Tags []string `json:"tags"`
|
||||
Version string `json:"version"`
|
||||
CompatVersion string `json:"compat_version"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(c.PostForm("metadata")), &metadata); err != nil {
|
||||
fail(c, 400, "Invalid theme metadata")
|
||||
return
|
||||
}
|
||||
metadata.Name = strings.TrimSpace(metadata.Name)
|
||||
metadata.Brief = strings.TrimSpace(metadata.Brief)
|
||||
if metadata.Name == "" || metadata.Brief == "" || utf8.RuneCountInString(metadata.Brief) > 50 || utf8.RuneCountInString(metadata.Description) > 100 {
|
||||
fail(c, 400, "Invalid name, brief or description")
|
||||
return
|
||||
}
|
||||
if metadata.NeedTheme != "dark" && metadata.NeedTheme != "light" && metadata.NeedTheme != "both" {
|
||||
fail(c, 400, "Invalid theme mode")
|
||||
return
|
||||
}
|
||||
var count int64
|
||||
if err := h.DB.Model(&themes_model.Theme{}).Where("user_id = ? AND name = ?", c.GetString("user_id"), metadata.Name).Count(&count).Error; err != nil {
|
||||
fail(c, 500, "Unable to check theme name")
|
||||
return
|
||||
}
|
||||
if count > 0 {
|
||||
fail(c, 409, "You already have a theme with this name")
|
||||
return
|
||||
}
|
||||
qss, err := readUpload(c, "file", []string{".qss"}, 8<<20)
|
||||
if err != nil {
|
||||
fail(c, 400, err.Error())
|
||||
return
|
||||
}
|
||||
previewData, err := readUpload(c, "preview", []string{".png", ".jpg", ".jpeg"}, 4<<20)
|
||||
if err != nil && !errors.Is(err, http.ErrMissingFile) {
|
||||
fail(c, 400, err.Error())
|
||||
return
|
||||
}
|
||||
var preview string
|
||||
if len(previewData) > 0 {
|
||||
preview, err = h.savePreview(previewData)
|
||||
if err != nil {
|
||||
fail(c, 400, "Unable to process preview image")
|
||||
return
|
||||
}
|
||||
}
|
||||
author := c.GetString("display_name")
|
||||
if author == "" {
|
||||
author = c.GetString("user_name")
|
||||
}
|
||||
theme := themes_model.Theme{Name: metadata.Name, Brief: metadata.Brief, Description: metadata.Description, NeedTheme: metadata.NeedTheme, Tags: metadata.Tags, Version: metadata.Version, CompatVersion: metadata.CompatVersion, UserID: c.GetString("user_id"), Author: author, Status: themes_model.ThemePending, PreviewURL: preview, CreatedAt: utils.NowInShanghai(), UpdatedAt: utils.NowInShanghai()}
|
||||
if err := h.Themes.Create(&theme, qss); err != nil {
|
||||
if preview != "" {
|
||||
_ = h.Files.Remove(filepath.Join(h.Files.Root(), "previews", filepath.Base(preview)))
|
||||
}
|
||||
fail(c, 500, "Unable to save theme and archive")
|
||||
return
|
||||
}
|
||||
success(c, http.StatusCreated, "Theme submitted for review", theme)
|
||||
}
|
||||
|
||||
func (h *Handler) CheckThemeName(c *gin.Context) {
|
||||
name := strings.TrimSpace(c.Query("name"))
|
||||
if name == "" {
|
||||
fail(c, 400, "Name is required")
|
||||
return
|
||||
}
|
||||
var count int64
|
||||
if err := h.DB.Model(&themes_model.Theme{}).Where("user_id = ? AND name = ?", c.GetString("user_id"), name).Count(&count).Error; err != nil {
|
||||
fail(c, 500, "Unable to check theme name")
|
||||
return
|
||||
}
|
||||
success(c, 200, "Theme name checked", count > 0)
|
||||
}
|
||||
|
||||
func (h *Handler) UploadPreview(c *gin.Context) {
|
||||
theme, ok := h.ownTheme(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
data, err := readUpload(c, "preview", []string{".png", ".jpg", ".jpeg"}, 4<<20)
|
||||
if err != nil {
|
||||
fail(c, 400, err.Error())
|
||||
return
|
||||
}
|
||||
preview, err := h.savePreview(data)
|
||||
if err != nil {
|
||||
fail(c, 400, "Unable to process preview image")
|
||||
return
|
||||
}
|
||||
previous := theme.PreviewURL
|
||||
theme.PreviewURL = preview
|
||||
theme.Status = themes_model.ThemePending
|
||||
theme.UpdatedAt = utils.NowInShanghai()
|
||||
if err := h.Themes.Save(theme, nil); err != nil {
|
||||
_ = h.Files.Remove(filepath.Join(h.Files.Root(), "previews", filepath.Base(preview)))
|
||||
fail(c, 500, "Unable to save preview")
|
||||
return
|
||||
}
|
||||
if previous != "" {
|
||||
_ = h.Files.Remove(filepath.Join(h.Files.Root(), "previews", filepath.Base(previous)))
|
||||
}
|
||||
success(c, 200, "Preview updated; review required", gin.H{"preview_url": preview})
|
||||
}
|
||||
|
||||
func (h *Handler) ReplaceThemeFile(c *gin.Context) {
|
||||
theme, ok := h.ownTheme(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
qss, err := readUpload(c, "file", []string{".qss"}, 8<<20)
|
||||
if err != nil {
|
||||
fail(c, 400, err.Error())
|
||||
return
|
||||
}
|
||||
theme.Status = themes_model.ThemePending
|
||||
theme.UpdatedAt = utils.NowInShanghai()
|
||||
if err := h.Themes.Save(theme, qss); err != nil {
|
||||
fail(c, 500, "Unable to replace theme file")
|
||||
return
|
||||
}
|
||||
success(c, 200, "File replaced; review required", theme)
|
||||
}
|
||||
@@ -0,0 +1,301 @@
|
||||
package versions_handler
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"gorm.io/gorm"
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"autolibrary.top/webserver/backend/database/versions_database"
|
||||
"autolibrary.top/webserver/backend/models/versions_model"
|
||||
"autolibrary.top/webserver/utils"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
/*
|
||||
These handlers are used to manage download versions.
|
||||
*/
|
||||
|
||||
// GetLatestDownload returns the newest published stable release, or null.
|
||||
func GetLatestDownload(c *gin.Context) {
|
||||
var version versions_model.DownloadVersion
|
||||
err := versions_database.DB.Where("type = ? AND release_date <= ?", "stable", utils.NowInShanghai()).Order("release_date DESC, id DESC").First(&version).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
c.JSON(http.StatusOK, gin.H{"code": http.StatusOK, "msg": "No stable release available", "data": nil})
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": http.StatusInternalServerError, "msg": "Unable to load latest release", "data": nil})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"code": http.StatusOK, "msg": "Latest stable release", "data": version})
|
||||
}
|
||||
|
||||
// GET /download/versions?page={number}&per_page={number<=50}
|
||||
func GetDownloadVersions(c *gin.Context) {
|
||||
// Parse page and perPage parameters
|
||||
pageStr := c.Query("page")
|
||||
perPageStr := c.Query("per_page")
|
||||
|
||||
// Default values for page and perPage
|
||||
if pageStr == "" {
|
||||
pageStr = "1"
|
||||
}
|
||||
if perPageStr == "" {
|
||||
perPageStr = "9"
|
||||
}
|
||||
|
||||
page, err := strconv.Atoi(pageStr)
|
||||
if err != nil {
|
||||
log.Printf("[E] GET > try to get downloads, but got invalid parameter: 'page=%s'", pageStr)
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"code": http.StatusBadRequest,
|
||||
"msg": "GET > try to get downloads, but got invalid parameter: 'page'",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
perPage, err := strconv.Atoi(perPageStr)
|
||||
if err != nil {
|
||||
log.Printf("[E] GET > try to get downloads (page=%d), but got invalid parameter: 'per_page=%s'", page, perPageStr)
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"code": http.StatusBadRequest,
|
||||
"msg": "GET > try to get downloads, but got invalid parameter: 'per_page'",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// constrain page and perPage parameters
|
||||
if page <= 0 {
|
||||
page = 1
|
||||
}
|
||||
if perPage <= 0 || perPage > 50 {
|
||||
perPage = 9
|
||||
}
|
||||
|
||||
// Calculate offset
|
||||
offset := (page - 1) * perPage
|
||||
|
||||
var downloads []versions_model.DownloadVersion
|
||||
result := versions_database.DB.Order("id DESC").Limit(perPage).Offset(offset).Find(&downloads)
|
||||
if result.Error != nil {
|
||||
log.Printf("[E] GET > occur an error when get downloads (page=%d, per_page=%d): %v", page, perPage, result.Error)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"code": http.StatusInternalServerError,
|
||||
"msg": "GET > occur an error when get downloads",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var total int64
|
||||
result = versions_database.DB.Model(&versions_model.DownloadVersion{}).Count(&total)
|
||||
if result.Error != nil {
|
||||
log.Printf("[E] GET > occur an error when get downloads (page=%d, per_page=%d): %v", page, perPage, result.Error)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"code": http.StatusInternalServerError,
|
||||
"msg": "GET > occur an error when get downloads",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
totalPages := int(total) / perPage
|
||||
if int(total)%perPage != 0 {
|
||||
totalPages++
|
||||
}
|
||||
pageMeta := gin.H{
|
||||
"page": page,
|
||||
"per_page": perPage,
|
||||
"total": total,
|
||||
"total_pages": totalPages,
|
||||
"has_next": page < totalPages,
|
||||
}
|
||||
|
||||
log.Printf("[I] GET > get downloads (page=%d, per_page=%d) successful", page, perPage)
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": http.StatusOK,
|
||||
"msg": "GET > get downloads successful",
|
||||
"data": gin.H{
|
||||
"downloads": downloads,
|
||||
"meta": pageMeta,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// GET /download/versions/:id
|
||||
func GetDownloadVersionByID(c *gin.Context) {
|
||||
id, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
log.Printf("[E] GET > try to get download, but got invalid id: '%s'", c.Param("id"))
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"code": http.StatusBadRequest,
|
||||
"msg": "GET > try to get download, but got invalid id",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var download versions_model.DownloadVersion
|
||||
result := versions_database.DB.First(&download, id)
|
||||
if result.Error != nil {
|
||||
log.Printf("[E] GET > download (id=%d) can not found: %v", id, result.Error)
|
||||
c.JSON(http.StatusNotFound, gin.H{
|
||||
"code": http.StatusNotFound,
|
||||
"msg": "GET > download can not found",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("[I] GET > get download (id=%d) successful", id)
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": http.StatusOK,
|
||||
"msg": "GET > get download successful",
|
||||
"data": download,
|
||||
})
|
||||
}
|
||||
|
||||
// POST /download/versions
|
||||
func PostDownloadVersion(c *gin.Context) {
|
||||
var download versions_model.DownloadVersion
|
||||
if err := c.ShouldBindJSON(&download); err != nil {
|
||||
log.Printf("[E] POST > try to create download, but got invalid request data: %v", err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"code": http.StatusBadRequest,
|
||||
"msg": "POST > try to create download, but got invalid request data",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
download.CreatedAt = utils.NowInShanghai()
|
||||
result := versions_database.DB.Create(&download)
|
||||
if result.Error != nil {
|
||||
log.Printf("[E] POST > occur an error when create download: %v", result.Error)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"code": http.StatusInternalServerError,
|
||||
"msg": "POST > occur an error when create download",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
id := strconv.FormatUint(uint64(download.ID), 10)
|
||||
log.Printf("[I] POST > create download (id=%s) successful", id)
|
||||
c.JSON(http.StatusCreated, gin.H{
|
||||
"code": http.StatusCreated,
|
||||
"msg": "POST > create download successful",
|
||||
"data": download,
|
||||
})
|
||||
}
|
||||
|
||||
// PUT /download/versions/:id
|
||||
func PutDownloadVersionByID(c *gin.Context) {
|
||||
id, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
log.Printf("[E] PUT > try to update download, but got invalid id: '%s'", c.Param("id"))
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"code": http.StatusBadRequest,
|
||||
"msg": "PUT > try to update download, but got invalid id",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var updateDownload versions_model.DownloadVersion
|
||||
if err = c.ShouldBindJSON(&updateDownload); err != nil {
|
||||
log.Printf("[E] PUT > try to update download (id=%d), but got invalid request data: %v", id, err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"code": http.StatusBadRequest,
|
||||
"msg": "PUT > try to update download, but got invalid request data",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var existingDownload versions_model.DownloadVersion
|
||||
result := versions_database.DB.First(&existingDownload, id)
|
||||
if result.Error != nil {
|
||||
log.Printf("[E] PUT > download (id=%d) can not found: %v", id, result.Error)
|
||||
c.JSON(http.StatusNotFound, gin.H{
|
||||
"code": http.StatusNotFound,
|
||||
"msg": "PUT > download can not found",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
result = versions_database.DB.Model(&versions_model.DownloadVersion{}).Where("id = ?", id).Updates(updateDownload)
|
||||
if result.Error != nil {
|
||||
log.Printf("[E] PUT > occur an error when update download (id=%d): %v", id, result.Error)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"code": http.StatusInternalServerError,
|
||||
"msg": "PUT > occur an error when update download",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// post check after update
|
||||
var updatedDownload versions_model.DownloadVersion
|
||||
if result = versions_database.DB.First(&updatedDownload, id); result.Error == nil {
|
||||
log.Printf("[I] PUT > update download (id=%d) successful", id)
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": http.StatusOK,
|
||||
"msg": "PUT > update download successful",
|
||||
"data": updatedDownload,
|
||||
})
|
||||
} else {
|
||||
log.Printf("[W] PUT > update successful but failed to retrieve updated download (id=%d): %v", id, result.Error)
|
||||
c.JSON(http.StatusAccepted, gin.H{
|
||||
"code": http.StatusAccepted,
|
||||
"msg": "PUT > update successful but failed to retrieve updated download",
|
||||
"data": nil,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// DELETE /download/versions/:id
|
||||
func DeleteDownloadVersionByID(c *gin.Context) {
|
||||
id, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
log.Printf("[E] DELETE > try to delete download, but got invalid id: %s", c.Param("id"))
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"code": http.StatusBadRequest,
|
||||
"msg": "DELETE > try to delete download, but got invalid id",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
result := versions_database.DB.Delete(&versions_model.DownloadVersion{}, id)
|
||||
if result.Error != nil {
|
||||
log.Printf("[E] DELETE > occur an error when delete download (id=%d): %v", id, result.Error)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"code": http.StatusInternalServerError,
|
||||
"msg": "DELETE > occur an error when delete download",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if result.RowsAffected == 0 {
|
||||
log.Printf("[E] DELETE > try to delete download (id=%d), but RowsAffected is 0", id)
|
||||
c.JSON(http.StatusNotFound, gin.H{
|
||||
"code": http.StatusNotFound,
|
||||
"msg": "DELETE > delete download has no effect",
|
||||
"data": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("[I] DELETE > delete download (id=%d) successful", id)
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": http.StatusOK,
|
||||
"msg": "DELETE > delete download successful",
|
||||
"data": nil,
|
||||
})
|
||||
}
|
||||
Reference in new issue
Block a user