From 7a1abe008c3d8b1fa6a125239d022d88c42680ab Mon Sep 17 00:00:00 2001 From: ryan Date: Wed, 11 Mar 2026 13:56:11 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=E8=B7=AF=E7=94=B1=EF=BC=8C=E6=94=AF=E6=8C=81=E8=8E=B7=E5=8F=96?= =?UTF-8?q?=E6=9C=80=E6=96=B0=E7=89=88=E6=9C=AC=E4=BF=A1=E6=81=AF=EF=BC=8C?= =?UTF-8?q?=E5=B9=B6=E5=AE=9E=E7=8E=B0=E7=9B=B8=E5=85=B3=E6=B5=8B=E8=AF=95?= =?UTF-8?q?=E7=94=A8=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- atsf_server/controller/update.go | 77 +++++++++++++ atsf_server/router/api-router.go | 5 + atsf_server/router/update_test.go | 104 ++++++++++++++++++ .../web/src/components/OtherSetting.js | 11 +- 4 files changed, 193 insertions(+), 4 deletions(-) create mode 100644 atsf_server/controller/update.go create mode 100644 atsf_server/router/update_test.go diff --git a/atsf_server/controller/update.go b/atsf_server/controller/update.go new file mode 100644 index 00000000..8999ce1e --- /dev/null +++ b/atsf_server/controller/update.go @@ -0,0 +1,77 @@ +package controller + +import ( + "encoding/json" + "fmt" + "net/http" + "time" + + "github.com/gin-gonic/gin" +) + +const latestReleaseURL = "https://api.github.com/repos/Rain-kl/ATSFlare/releases/latest" + +var updateHTTPClient = &http.Client{ + Timeout: 10 * time.Second, +} + +type latestReleaseResponse struct { + TagName string `json:"tag_name"` + Body string `json:"body"` + HTMLURL string `json:"html_url"` + PublishedAt string `json:"published_at"` +} + +func GetLatestRelease(c *gin.Context) { + req, err := http.NewRequestWithContext(c.Request.Context(), http.MethodGet, latestReleaseURL, nil) + if err != nil { + c.JSON(http.StatusOK, gin.H{ + "success": false, + "message": "创建更新请求失败", + }) + return + } + req.Header.Set("Accept", "application/vnd.github+json") + req.Header.Set("User-Agent", "ATSFlare-Server") + + resp, err := updateHTTPClient.Do(req) + if err != nil { + c.JSON(http.StatusOK, gin.H{ + "success": false, + "message": fmt.Sprintf("获取最新版本失败: %v", err), + }) + return + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + c.JSON(http.StatusOK, gin.H{ + "success": false, + "message": fmt.Sprintf("GitHub 返回异常状态: %s", resp.Status), + }) + return + } + + var release latestReleaseResponse + if err = json.NewDecoder(resp.Body).Decode(&release); err != nil { + c.JSON(http.StatusOK, gin.H{ + "success": false, + "message": "解析最新版本信息失败", + }) + return + } + + c.JSON(http.StatusOK, gin.H{ + "success": true, + "message": "", + "data": release, + }) +} + +func UpdateHTTPClientForTest() *http.Client { + return updateHTTPClient +} + +func SetUpdateHTTPClientForTest(client *http.Client) { + updateHTTPClient = client +} diff --git a/atsf_server/router/api-router.go b/atsf_server/router/api-router.go index 4a8d4f11..8e862c75 100644 --- a/atsf_server/router/api-router.go +++ b/atsf_server/router/api-router.go @@ -55,6 +55,11 @@ func SetApiRouter(router *gin.Engine) { optionRoute.GET("/", controller.GetOptions) optionRoute.PUT("/", controller.UpdateOption) } + updateRoute := apiRouter.Group("/update") + updateRoute.Use(middleware.RootAuth(), middleware.NoTokenAuth()) + { + updateRoute.GET("/latest-release", controller.GetLatestRelease) + } fileRoute := apiRouter.Group("/file") fileRoute.Use(middleware.AdminAuth()) { diff --git a/atsf_server/router/update_test.go b/atsf_server/router/update_test.go new file mode 100644 index 00000000..d59c16bb --- /dev/null +++ b/atsf_server/router/update_test.go @@ -0,0 +1,104 @@ +package router_test + +import ( + "atsflare/common" + "atsflare/controller" + "atsflare/router" + "bytes" + "encoding/json" + "io" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/gin-contrib/sessions" + "github.com/gin-contrib/sessions/cookie" + "github.com/gin-gonic/gin" +) + +type roundTripFunc func(req *http.Request) (*http.Response, error) + +func (f roundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) { + return f(req) +} + +func TestLatestReleaseProxy(t *testing.T) { + gin.SetMode(gin.TestMode) + common.RedisEnabled = false + setupTestDB(t) + + originalClient := controller.UpdateHTTPClientForTest() + controller.SetUpdateHTTPClientForTest(&http.Client{ + Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) { + if req.URL.String() != "https://api.github.com/repos/Rain-kl/ATSFlare/releases/latest" { + t.Fatalf("unexpected request url: %s", req.URL.String()) + } + if req.Header.Get("Accept") != "application/vnd.github+json" { + t.Fatalf("unexpected accept header: %s", req.Header.Get("Accept")) + } + if req.Header.Get("User-Agent") != "ATSFlare-Server" { + t.Fatalf("unexpected user-agent header: %s", req.Header.Get("User-Agent")) + } + return &http.Response{ + StatusCode: http.StatusOK, + Header: make(http.Header), + Body: io.NopCloser(strings.NewReader(`{ + "tag_name":"v1.2.3", + "body":"release notes", + "html_url":"https://github.com/Rain-kl/ATSFlare/releases/tag/v1.2.3", + "published_at":"2026-03-11T00:00:00Z" + }`)), + }, nil + }), + }) + t.Cleanup(func() { + controller.SetUpdateHTTPClientForTest(originalClient) + }) + + engine := gin.New() + engine.Use(sessions.Sessions("session", cookie.NewStore([]byte("test-secret")))) + router.SetApiRouter(engine) + + loginBody, err := json.Marshal(map[string]string{ + "username": "root", + "password": "123456", + }) + if err != nil { + t.Fatalf("failed to marshal login body: %v", err) + } + loginReq := httptest.NewRequest(http.MethodPost, "/api/user/login", bytes.NewReader(loginBody)) + loginReq.Header.Set("Content-Type", "application/json") + loginRecorder := httptest.NewRecorder() + engine.ServeHTTP(loginRecorder, loginReq) + if loginRecorder.Code != http.StatusOK { + t.Fatalf("unexpected login status code: %d", loginRecorder.Code) + } + + req := httptest.NewRequest(http.MethodGet, "/api/update/latest-release", nil) + for _, cookieValue := range loginRecorder.Result().Cookies() { + req.AddCookie(cookieValue) + } + + recorder := httptest.NewRecorder() + engine.ServeHTTP(recorder, req) + if recorder.Code != http.StatusOK { + t.Fatalf("unexpected status code: %d", recorder.Code) + } + + var resp apiResponse + if err := json.Unmarshal(recorder.Body.Bytes(), &resp); err != nil { + t.Fatalf("failed to decode response: %v", err) + } + if !resp.Success { + t.Fatalf("expected success response, got message: %s", resp.Message) + } + + var data map[string]any + if err := json.Unmarshal(resp.Data, &data); err != nil { + t.Fatalf("failed to decode response data: %v", err) + } + if data["tag_name"] != "v1.2.3" { + t.Fatalf("unexpected tag_name: %#v", data["tag_name"]) + } +} diff --git a/atsf_server/web/src/components/OtherSetting.js b/atsf_server/web/src/components/OtherSetting.js index 7babb4e4..3ccf3994 100644 --- a/atsf_server/web/src/components/OtherSetting.js +++ b/atsf_server/web/src/components/OtherSetting.js @@ -89,10 +89,13 @@ const OtherSetting = () => { }; const checkUpdate = async () => { - const res = await API.get( - 'https://api.github.com/repos/Rain-kl/ATSFlare/releases/latest' - ); - const { tag_name, body } = res.data; + const res = await API.get('/api/update/latest-release'); + const { success, message, data } = res.data; + if (!success) { + showError(message); + return; + } + const { tag_name, body } = data; if ( hasBuildVersion() && normalizeVersion(tag_name) === getBuildVersion()