Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8c8486f9af | |||
| 7d0f4c8065 | |||
| 3d521973ab | |||
| cb8e4bddd8 |
@@ -45,7 +45,8 @@ jobs:
|
||||
env:
|
||||
RELEASE_KEYSTORE: ${{ secrets.RELEASE_KEYSTORE }}
|
||||
KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
|
||||
run: bash scripts/ci/compile-android.sh "${{ gitea.ref_name }}"
|
||||
REF_NAME: ${{ gitea.ref_name }}
|
||||
run: bash scripts/ci/compile-android.sh "$REF_NAME"
|
||||
|
||||
- name: Upload android artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
@@ -53,12 +54,30 @@ jobs:
|
||||
name: android
|
||||
path: dist/
|
||||
|
||||
# build-windows(runs-on: windows)本轮移除:pangolin 暂无 windows runner。
|
||||
# 待注册 windows runner 后从 git 历史(commit 2698116)恢复该 job + release-deploy
|
||||
# 的 needs/下载步骤 + compile-windows.sh 已就绪。
|
||||
build-windows:
|
||||
runs-on: windows
|
||||
env:
|
||||
GOPROXY: https://goproxy.cn,direct
|
||||
PUB_HOSTED_URL: https://pub.flutter-io.cn
|
||||
FLUTTER_STORAGE_BASE_URL: https://storage.flutter-io.cn
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Compile (Windows installer)
|
||||
shell: bash
|
||||
env:
|
||||
REF_NAME: ${{ gitea.ref_name }}
|
||||
run: bash scripts/ci/compile-windows.sh "$REF_NAME"
|
||||
|
||||
- name: Upload windows artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: windows
|
||||
path: dist/
|
||||
|
||||
release-deploy:
|
||||
needs: [build-android]
|
||||
needs: [build-android, build-windows]
|
||||
runs-on: mac
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -70,26 +89,36 @@ jobs:
|
||||
name: android
|
||||
path: dist/
|
||||
|
||||
- name: Download windows artifact
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: windows
|
||||
path: dist/
|
||||
|
||||
- name: Release → Forgejo
|
||||
env:
|
||||
FORGEJO_TOKEN: ${{ secrets.FORGEJO_TOKEN }}
|
||||
FORGEJO_URL: ${{ secrets.FORGEJO_URL }}
|
||||
run: bash scripts/ci/release-client.sh "${{ gitea.ref_name }}"
|
||||
REF_NAME: ${{ gitea.ref_name }}
|
||||
run: bash scripts/ci/release-client.sh "$REF_NAME"
|
||||
|
||||
- name: Deploy → pangolin1 (downloads/)
|
||||
env:
|
||||
DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
|
||||
run: bash scripts/ci/deploy-client.sh "${{ gitea.ref_name }}"
|
||||
REF_NAME: ${{ gitea.ref_name }}
|
||||
run: bash scripts/ci/deploy-client.sh "$REF_NAME"
|
||||
|
||||
- name: Notify
|
||||
if: always()
|
||||
env:
|
||||
TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }}
|
||||
TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }}
|
||||
REF_NAME: ${{ gitea.ref_name }}
|
||||
JOB_STATUS: ${{ job.status }}
|
||||
run: |
|
||||
. scripts/ci/notify.sh
|
||||
if [ "${{ job.status }}" = "success" ]; then
|
||||
notify_ok "client ${{ gitea.ref_name }} released + deployed"
|
||||
if [ "$JOB_STATUS" = "success" ]; then
|
||||
notify_ok "client $REF_NAME released + deployed"
|
||||
else
|
||||
notify_fail "client ${{ gitea.ref_name }} pipeline failed"
|
||||
notify_fail "client $REF_NAME pipeline failed"
|
||||
fi
|
||||
|
||||
@@ -123,6 +123,8 @@ func main() {
|
||||
r.Use(chimw.Logger)
|
||||
r.Use(chimw.Recoverer)
|
||||
r.Use(apierr.Middleware)
|
||||
// CORS:Web 用户中心(app.yanmeiai.com)跨域调 /v1/*;原生端不受影响。
|
||||
r.Use(httpapi.NewCORS())
|
||||
|
||||
r.Get("/healthz", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
package httpapi
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// NewCORS 返回一个 CORS 中间件:为白名单 Origin(精确匹配)补 CORS 响应头,并直接
|
||||
// 应答 OPTIONS 预检。Web 用户中心(app.yanmeiai.com,浏览器)跨域调用 /v1/*,浏览器
|
||||
// 会先发预检、并校验 Access-Control-Allow-Origin;原生移动/桌面客户端不是浏览器、
|
||||
// 不受 CORS 约束,故此前无需 CORS。
|
||||
//
|
||||
// Origin 白名单来自 CORS_ORIGINS(逗号分隔),缺省含 usercenter 的正式域与 pages.dev。
|
||||
// 认证走 Authorization: Bearer(非 cookie),所以不需要 Allow-Credentials。
|
||||
func NewCORS() func(http.Handler) http.Handler {
|
||||
raw := os.Getenv("CORS_ORIGINS")
|
||||
if raw == "" {
|
||||
raw = "https://app.yanmeiai.com,https://pangolin-usercenter.pages.dev"
|
||||
}
|
||||
allowed := map[string]bool{}
|
||||
for _, o := range strings.Split(raw, ",") {
|
||||
o = strings.TrimSpace(o)
|
||||
if o != "" {
|
||||
allowed[o] = true
|
||||
}
|
||||
}
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
origin := r.Header.Get("Origin")
|
||||
if origin != "" && allowed[origin] {
|
||||
h := w.Header()
|
||||
h.Set("Access-Control-Allow-Origin", origin)
|
||||
h.Add("Vary", "Origin")
|
||||
h.Set("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS")
|
||||
h.Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
|
||||
h.Set("Access-Control-Max-Age", "600")
|
||||
}
|
||||
// 预检请求直接 204(不落到业务路由,避免 /v1/... 的 OPTIONS 404)。
|
||||
if r.Method == http.MethodOptions {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package httpapi
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCORS(t *testing.T) {
|
||||
t.Setenv("CORS_ORIGINS", "https://app.yanmeiai.com")
|
||||
mw := NewCORS()
|
||||
next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(200) })
|
||||
h := mw(next)
|
||||
|
||||
// 允许的 Origin:补 Allow-Origin,普通请求继续。
|
||||
r := httptest.NewRequest("POST", "/v1/auth/login", nil)
|
||||
r.Header.Set("Origin", "https://app.yanmeiai.com")
|
||||
w := httptest.NewRecorder()
|
||||
h.ServeHTTP(w, r)
|
||||
if got := w.Header().Get("Access-Control-Allow-Origin"); got != "https://app.yanmeiai.com" {
|
||||
t.Fatalf("allowed origin: want header, got %q", got)
|
||||
}
|
||||
if w.Code != 200 {
|
||||
t.Fatalf("non-preflight should reach next, got %d", w.Code)
|
||||
}
|
||||
|
||||
// OPTIONS 预检:204,不落到业务。
|
||||
r = httptest.NewRequest("OPTIONS", "/v1/auth/login", nil)
|
||||
r.Header.Set("Origin", "https://app.yanmeiai.com")
|
||||
w = httptest.NewRecorder()
|
||||
h.ServeHTTP(w, r)
|
||||
if w.Code != http.StatusNoContent {
|
||||
t.Fatalf("preflight: want 204, got %d", w.Code)
|
||||
}
|
||||
if w.Header().Get("Access-Control-Allow-Methods") == "" {
|
||||
t.Fatalf("preflight missing Allow-Methods")
|
||||
}
|
||||
|
||||
// 未白名单 Origin:不补 Allow-Origin。
|
||||
r = httptest.NewRequest("POST", "/v1/auth/login", nil)
|
||||
r.Header.Set("Origin", "https://evil.example.com")
|
||||
w = httptest.NewRecorder()
|
||||
h.ServeHTTP(w, r)
|
||||
if got := w.Header().Get("Access-Control-Allow-Origin"); got != "" {
|
||||
t.Fatalf("disallowed origin should get no header, got %q", got)
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@
|
||||
*/
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Download, Menu } from 'lucide-react';
|
||||
import { SITE } from '../config/site';
|
||||
|
||||
function Mark() {
|
||||
return (
|
||||
@@ -60,8 +61,7 @@ export default function Header({ lang = 'zh', t = {} }) {
|
||||
<a data-lang="zh" class={lang === 'zh' ? 'on' : undefined} href="/">中文</a>
|
||||
<a data-lang="en" class={lang === 'en' ? 'on' : undefined} href="/en/">EN</a>
|
||||
</div>
|
||||
{/* 「登录」暂移除:Web 用户中心(web/usercenter)未部署、无 URL;待 #30 CI/CD
|
||||
部署后接上 usercenter 登录地址再恢复。 */}
|
||||
<a class="linklogin" href={SITE.usercenter}>{t.login}</a>
|
||||
<a class="btn btn-primary" href="#download">
|
||||
<Download />
|
||||
<span>{t.get}</span>
|
||||
|
||||
@@ -20,6 +20,9 @@ export const SITE = {
|
||||
/** 自助发卡商店 —— 占位待定(#24)。 */
|
||||
store: { label: 'shop.pangolin.vpn' },
|
||||
|
||||
/** Web 用户中心(web/usercenter,Next.js 静态导出 → CF Pages)。登录/订阅/设备管理入口。 */
|
||||
usercenter: 'https://app.yanmeiai.com',
|
||||
|
||||
/**
|
||||
* 客户端安装包直链。控制面 pangolin-server 通过 Cloudflare Tunnel 对外暴露
|
||||
* /downloads/<file>(origin = 127.0.0.1:8080),文件由
|
||||
|
||||
Reference in New Issue
Block a user