chore: initialize standalone business plugin repository
Business Plugins CI / check (plugin-admin) (push) Successful in 3m13s
Business Plugins CI / check (subscription-admin) (push) Successful in 1m41s

This commit is contained in:
Qiufeng
2026-08-27 23:36:08 +08:00
commit 5feae3ad41
59 changed files with 8950 additions and 0 deletions
@@ -0,0 +1,43 @@
import { chromium } from 'playwright'
import fs from 'node:fs/promises'
import path from 'node:path'
const origin = process.env.PLUGIN_BROWSER_ORIGIN || 'http://127.0.0.1:18081'
const entry = `${origin}/extensions/qiu.subscription-admin/admin/`
const outputDir = process.env.PLUGIN_SCREENSHOT_DIR || '.playwright-cli/subscription-admin'
await fs.mkdir(path.resolve(outputDir), { recursive: true })
const browser = await chromium.launch({ headless: true })
try {
for (const width of [425, 900, 1440]) {
const page = await browser.newPage({ viewport: { width, height: 900 }, deviceScaleFactor: 1 })
const responseLeaks = []
page.on('response', async (response) => {
if (!response.headers()['content-type']?.includes('application/json')) return
try {
const body = await response.text()
if (/access_token|refresh_token|admin[_-]?api[_-]?key|password|client_secret/i.test(body)) responseLeaks.push(response.url())
} catch (_) {}
})
await page.goto(entry, { waitUntil: 'domcontentloaded' })
await page.getByLabel('管理员邮箱').fill('admin@example.com')
await page.getByLabel('密码').fill('password')
await page.getByRole('button', { name: '登录' }).click()
await page.locator('#app-view').waitFor({ state: 'visible' })
await page.waitForTimeout(50)
if (responseLeaks.length) throw new Error(`sensitive response field exposed at ${width}px: ${responseLeaks.join(', ')}`)
await page.getByRole('button', { name: '用户订阅' }).click()
await page.locator('#subscriptions-list table').waitFor()
await page.getByRole('button', { name: '下一页' }).click()
await page.getByRole('button', { name: '概览' }).click()
await page.reload({ waitUntil: 'domcontentloaded' })
await page.locator('#app-view').waitFor({ state: 'visible' })
const overflow = await page.evaluate(() => document.documentElement.scrollWidth > window.innerWidth)
if (overflow) throw new Error(`horizontal overflow at ${width}px`)
await page.screenshot({ path: path.join(outputDir, `subscription-admin-${width}.png`), fullPage: true })
await page.close()
}
} finally {
await browser.close()
}
@@ -0,0 +1,80 @@
import http from 'node:http'
const port = Number(process.env.MOCK_CORE_PORT || 18080)
const token = process.env.MOCK_ACCESS_TOKEN || 'MOCK-ACCESS'
const refresh = process.env.MOCK_REFRESH_TOKEN || 'MOCK-REFRESH'
const users = new Map([
['admin@example.com', { id: 1, role: 'admin', email: 'admin@example.com', username: 'admin' }],
['user@example.com', { id: 2, role: 'user', email: 'user@example.com', username: 'user' }]
])
function json(res, status, body) {
res.writeHead(status, { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' })
res.end(JSON.stringify(body))
}
async function body(req) {
const chunks = []
for await (const chunk of req) chunks.push(chunk)
return chunks.length ? JSON.parse(Buffer.concat(chunks).toString('utf8')) : {}
}
function authorized(req) {
return req.headers.authorization === `Bearer ${token}` || req.headers.authorization === `Bearer NEW-MOCK-ACCESS`
}
function subscriptions(page = 1, pageSize = 50) {
const all = Array.from({ length: 57 }, (_, index) => ({
id: index + 1,
user_id: 1,
plan_id: 10 + (index % 2),
plan_name: index % 2 ? '专业版' : '基础版',
status: 'active',
starts_at: '2026-08-01T00:00:00Z',
expires_at: '2026-09-01T00:00:00Z',
cycle_usage_usd: index / 100,
cycle_quota_usd: 100,
user: { id: 1, username: 'admin', email: 'admin@example.com', balance: 123.45 }
}))
const start = (page - 1) * pageSize
return { items: all.slice(start, start + pageSize), total: all.length, page, page_size: pageSize, pages: Math.ceil(all.length / pageSize) }
}
const server = http.createServer(async (req, res) => {
const url = new URL(req.url, `http://${req.headers.host}`)
const path = url.pathname
if (req.method === 'POST' && path === '/api/v1/auth/login') {
const input = await body(req)
const user = users.get(input.email)
if (!user || input.password !== 'password') return json(res, 401, { code: 401, message: 'invalid credentials' })
return json(res, 200, { code: 0, message: 'success', data: { access_token: token, refresh_token: refresh, user } })
}
if (req.method === 'POST' && path === '/api/v1/auth/refresh') {
const input = await body(req)
if (input.refresh_token !== refresh && input.refresh_token !== 'NEW-MOCK-REFRESH') return json(res, 401, { code: 401, message: 'expired' })
return json(res, 200, { code: 0, message: 'success', data: { access_token: 'NEW-MOCK-ACCESS', refresh_token: 'NEW-MOCK-REFRESH' } })
}
if (req.method === 'POST' && path === '/api/v1/auth/logout') return json(res, 200, { code: 0, message: 'success', data: {} })
if (req.method === 'GET' && path === '/api/v1/settings/public') return json(res, 200, { code: 0, message: 'success', data: { turnstile_enabled: false, geetest_captcha_enabled: false, tencent_captcha_enabled: false, aliyun_captcha_enabled: false } })
if (!authorized(req)) return json(res, 401, { code: 401, message: 'unauthorized' })
if (req.method === 'GET' && path === '/api/v1/auth/me') return json(res, 200, { code: 0, message: 'success', data: users.get('admin@example.com') })
if (req.method === 'GET' && path === '/api/v1/admin/payment/plans') {
return json(res, 200, { code: 0, message: 'success', data: [{ id: 10, name: '基础版', price: 9.9, currency: 'USD', validity_days: 30, validity_unit: 'day', for_sale: true, included_groups: [] }, { id: 11, name: '专业版', price: 19.9, currency: 'USD', validity_days: 30, validity_unit: 'day', for_sale: true, included_groups: [] }] })
}
if (req.method === 'GET' && path === '/api/v1/admin/subscriptions') {
const page = Number(url.searchParams.get('page') || 1)
const pageSize = Number(url.searchParams.get('page_size') || 50)
return json(res, 200, { code: 0, message: 'success', data: subscriptions(page, pageSize) })
}
const subscriptionMatch = path.match(/^\/api\/v1\/admin\/subscriptions\/(\d+)$/)
if (req.method === 'GET' && subscriptionMatch) return json(res, 200, { code: 0, message: 'success', data: subscriptions(1, 1).items[0] })
const userSubscriptionsMatch = path.match(/^\/api\/v1\/admin\/users\/(\d+)\/subscriptions$/)
if (req.method === 'GET' && userSubscriptionsMatch) return json(res, 200, { code: 0, message: 'success', data: subscriptions(1, 2).items })
const userMatch = path.match(/^\/api\/v1\/admin\/users\/(\d+)$/)
if (req.method === 'GET' && userMatch) return json(res, 200, { code: 0, message: 'success', data: { id: Number(userMatch[1]), username: 'admin', email: 'admin@example.com', balance: 123.45, frozen_balance: 0 } })
return json(res, 404, { code: 404, message: 'not found' })
})
server.listen(port, '127.0.0.1', () => {
process.stdout.write(`mock core listening on 127.0.0.1:${port}\n`)
})
@@ -0,0 +1,27 @@
import http from 'node:http'
const prefix = '/extensions/qiu.subscription-admin'
const targetPort = Number(process.env.PLUGIN_TARGET_PORT || 18082)
const port = Number(process.env.MOCK_PROXY_PORT || 18081)
const server = http.createServer((req, res) => {
if (!req.url.startsWith(prefix)) {
res.writeHead(404)
res.end('not found')
return
}
const forwardedPath = req.url.slice(prefix.length) || '/'
const proxy = http.request({ hostname: '127.0.0.1', port: targetPort, method: req.method, path: forwardedPath, headers: { ...req.headers, host: `127.0.0.1:${targetPort}` } }, (upstream) => {
const headers = { ...upstream.headers }
if (headers.location && headers.location.startsWith('/admin/')) headers.location = `${prefix}${headers.location}`
res.writeHead(upstream.statusCode || 502, headers)
upstream.pipe(res)
})
proxy.on('error', () => {
if (!res.headersSent) res.writeHead(502)
res.end('proxy error')
})
req.pipe(proxy)
})
server.listen(port, '127.0.0.1', () => process.stdout.write(`mock proxy listening on 127.0.0.1:${port}\n`))
+21
View File
@@ -0,0 +1,21 @@
#!/usr/bin/env sh
set -eu
ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
cleanup() {
kill "${PROXY_PID:-}" "${PLUGIN_PID:-}" "${CORE_PID:-}" 2>/dev/null || true
}
trap cleanup EXIT INT TERM
MOCK_CORE_PORT=18080 node "$ROOT/test/mock-core.mjs" >/tmp/subscription-admin-mock-core.log 2>&1 & CORE_PID=$!
CORE_BASE_URL=http://127.0.0.1:18080 \
PLUGIN_HOST=127.0.0.1 \
PLUGIN_PORT=18082 \
PLUGIN_PUBLIC_BASE_PATH=/extensions/qiu.subscription-admin \
PLUGIN_COOKIE_PATH=/extensions/qiu.subscription-admin/ \
PLUGIN_COOKIE_SECURE=false \
go run "$ROOT" >/tmp/subscription-admin-plugin.log 2>&1 & PLUGIN_PID=$!
PLUGIN_TARGET_PORT=18082 MOCK_PROXY_PORT=18081 node "$ROOT/test/mock-proxy.mjs" >/tmp/subscription-admin-mock-proxy.log 2>&1 & PROXY_PID=$!
sleep 2
PLUGIN_BROWSER_ORIGIN=http://127.0.0.1:18081 PLUGIN_SCREENSHOT_DIR="$ROOT/.screenshots/subscription-admin" node "$ROOT/test/browser-check.mjs"