前端极简黑白重设计,并纳入客户端注册、站点设置与后台相关改动
- storefront 全站改为极简黑白配色:直角、细线分隔、卡片 hover 浅底、深色模式(跟随系统+手动切换) - 抽出 storefront/partials/card 复用卡片 - 一并提交此前暂存的客户端注册 API、站点设置、后台页面与测试 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,7 @@ namespace App\Http\Controllers\Api;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\DownloadLog;
|
||||
use App\Models\Package;
|
||||
use App\Models\StoreClient;
|
||||
use App\Services\RepoFormatter;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
@@ -166,7 +167,7 @@ class RepoController extends Controller
|
||||
'package' => [
|
||||
'size' => (int) $latest->package_size,
|
||||
'sha256' => $latest->sha256,
|
||||
'download_url' => $latest->package_url,
|
||||
'download_url' => $this->buildDownloadUrl($request, $type, $slug, $latest->version),
|
||||
],
|
||||
];
|
||||
}
|
||||
@@ -187,12 +188,14 @@ class RepoController extends Controller
|
||||
public function download(Request $request, string $type, string $slug, string $version): JsonResponse|BinaryFileResponse|RedirectResponse
|
||||
{
|
||||
$pluginToken = (string) config('store.plugin_access_token', '');
|
||||
$requestToken = (string) ($request->header('X-Store-Plugin-Token') ?: $request->query('access_token', ''));
|
||||
$requestToken = $this->extractRequestToken($request);
|
||||
$client = $this->resolveClientByToken($requestToken);
|
||||
$legacyTokenMatched = $pluginToken !== '' && $requestToken !== '' && hash_equals($pluginToken, $requestToken);
|
||||
|
||||
if ($pluginToken !== '' && $requestToken !== $pluginToken) {
|
||||
if (!$legacyTokenMatched && !$client) {
|
||||
return response()->json([
|
||||
'code' => 403,
|
||||
'message' => 'download forbidden',
|
||||
'message' => 'client token required',
|
||||
'data' => null,
|
||||
], 403);
|
||||
}
|
||||
@@ -228,12 +231,20 @@ class RepoController extends Controller
|
||||
$redirect = (string) $request->query('redirect', '0') === '1';
|
||||
|
||||
if ($redirect) {
|
||||
if ($client) {
|
||||
$client->status = 'online';
|
||||
$client->last_ip = (string) ($request->ip() ?? '');
|
||||
$client->last_user_agent = (string) ($request->userAgent() ?? '');
|
||||
$client->last_seen_at = now();
|
||||
$client->save();
|
||||
}
|
||||
|
||||
DownloadLog::create([
|
||||
'package_id' => $package->id,
|
||||
'version_id' => $versionModel->id,
|
||||
'site_url' => (string) $request->query('site_url', ''),
|
||||
'typecho_version' => (string) $request->query('typecho_version', ''),
|
||||
'php_version' => (string) $request->query('php_version', ''),
|
||||
'site_url' => $client?->site_url ?: (string) $request->query('site_url', ''),
|
||||
'typecho_version' => $client?->typecho_version ?: (string) $request->query('typecho_version', ''),
|
||||
'php_version' => $client?->php_version ?: (string) $request->query('php_version', ''),
|
||||
'ip' => (string) $request->ip(),
|
||||
'user_agent' => (string) $request->userAgent(),
|
||||
'created_at' => now(),
|
||||
@@ -306,12 +317,89 @@ class RepoController extends Controller
|
||||
private function buildDownloadUrl(Request $request, string $type, string $slug, string $version): string
|
||||
{
|
||||
$path = '/api/v1/repo/download/' . $type . '/' . $slug . '/' . $version . '?redirect=1';
|
||||
$appUrl = rtrim((string) config('app.url', ''), '/');
|
||||
$baseUrl = $this->resolveDownloadBaseUrl($request);
|
||||
|
||||
if ($appUrl !== '') {
|
||||
return $appUrl . $path;
|
||||
if ($baseUrl !== '') {
|
||||
return $baseUrl . $path;
|
||||
}
|
||||
|
||||
return $request->getSchemeAndHttpHost() . $path;
|
||||
return $path;
|
||||
}
|
||||
|
||||
private function resolveDownloadBaseUrl(Request $request): string
|
||||
{
|
||||
$appUrl = $this->normalizeBaseUrl((string) config('app.url', ''));
|
||||
if ($this->isPublicHttpsBaseUrl($appUrl)) {
|
||||
return $appUrl;
|
||||
}
|
||||
|
||||
$requestRoot = $this->normalizeBaseUrl($request->root());
|
||||
if ($requestRoot !== '') {
|
||||
return $requestRoot;
|
||||
}
|
||||
|
||||
return $appUrl;
|
||||
}
|
||||
|
||||
private function normalizeBaseUrl(string $url): string
|
||||
{
|
||||
return rtrim(trim($url), '/');
|
||||
}
|
||||
|
||||
private function isPublicHttpsBaseUrl(string $url): bool
|
||||
{
|
||||
if ($url === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
$parts = parse_url($url);
|
||||
if (!is_array($parts)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$scheme = strtolower((string) ($parts['scheme'] ?? ''));
|
||||
$host = strtolower((string) ($parts['host'] ?? ''));
|
||||
|
||||
if ($scheme !== 'https' || $host === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (filter_var($host, FILTER_VALIDATE_IP)) {
|
||||
return filter_var(
|
||||
$host,
|
||||
FILTER_VALIDATE_IP,
|
||||
FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE
|
||||
) !== false;
|
||||
}
|
||||
|
||||
if (in_array($host, ['localhost', 'host.docker.internal'], true)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (['.localhost', '.local', '.internal', '.test'] as $suffix) {
|
||||
if (str_ends_with($host, $suffix)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function extractRequestToken(Request $request): string
|
||||
{
|
||||
return trim((string) (
|
||||
$request->bearerToken()
|
||||
?: $request->header('X-Store-Plugin-Token')
|
||||
?: $request->query('access_token', '')
|
||||
));
|
||||
}
|
||||
|
||||
private function resolveClientByToken(string $token): ?StoreClient
|
||||
{
|
||||
if ($token === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return StoreClient::query()->where('access_token', $token)->first();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user