feat(nginx): configure client max body size and update timeouts for API proxy
feat(clipboard): implement copyTextToClipboard utility function feat(desktop): add upload timeout for desktop client release API feat(k3s): add nginx config and update kustomization for ops-web
This commit is contained in:
@@ -8,6 +8,7 @@ real_ip_recursive on;
|
|||||||
server {
|
server {
|
||||||
listen 80;
|
listen 80;
|
||||||
server_name _;
|
server_name _;
|
||||||
|
client_max_body_size 600M;
|
||||||
|
|
||||||
root /usr/share/nginx/html;
|
root /usr/share/nginx/html;
|
||||||
index index.html;
|
index index.html;
|
||||||
@@ -30,8 +31,8 @@ server {
|
|||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
proxy_set_header X-Forwarded-For $remote_addr;
|
proxy_set_header X-Forwarded-For $remote_addr;
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
proxy_read_timeout 120s;
|
proxy_read_timeout 900s;
|
||||||
proxy_send_timeout 120s;
|
proxy_send_timeout 900s;
|
||||||
}
|
}
|
||||||
|
|
||||||
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2?)$ {
|
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2?)$ {
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
export async function copyTextToClipboard(text: string): Promise<void> {
|
||||||
|
if (!text) {
|
||||||
|
throw new Error('clipboard_empty_text')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (window.isSecureContext && navigator.clipboard?.writeText) {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(text)
|
||||||
|
return
|
||||||
|
} catch {
|
||||||
|
// Fall back to execCommand for browsers that expose clipboard but deny it.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (copyTextWithSelection(text)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('clipboard_write_failed')
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyTextWithSelection(text: string): boolean {
|
||||||
|
const textarea = document.createElement('textarea')
|
||||||
|
const activeElement = document.activeElement instanceof HTMLElement ? document.activeElement : null
|
||||||
|
|
||||||
|
textarea.value = text
|
||||||
|
textarea.setAttribute('readonly', '')
|
||||||
|
textarea.style.position = 'fixed'
|
||||||
|
textarea.style.top = '0'
|
||||||
|
textarea.style.left = '-9999px'
|
||||||
|
textarea.style.opacity = '0'
|
||||||
|
|
||||||
|
document.body.appendChild(textarea)
|
||||||
|
textarea.focus()
|
||||||
|
textarea.select()
|
||||||
|
textarea.setSelectionRange(0, text.length)
|
||||||
|
|
||||||
|
try {
|
||||||
|
return document.execCommand('copy')
|
||||||
|
} finally {
|
||||||
|
document.body.removeChild(textarea)
|
||||||
|
activeElement?.focus({ preventScroll: true })
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -75,6 +75,8 @@ export interface DesktopClientReleaseDownloadURLResult {
|
|||||||
expires_in?: number | null
|
expires_in?: number | null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const DESKTOP_CLIENT_RELEASE_UPLOAD_TIMEOUT_MS = 15 * 60 * 1000
|
||||||
|
|
||||||
export const platformOptions = [
|
export const platformOptions = [
|
||||||
{ label: 'macOS', value: 'darwin' },
|
{ label: 'macOS', value: 'darwin' },
|
||||||
{ label: 'Windows', value: 'win32' },
|
{ label: 'Windows', value: 'win32' },
|
||||||
@@ -138,7 +140,9 @@ export const desktopClientReleaseApi = {
|
|||||||
code: number
|
code: number
|
||||||
message: string
|
message: string
|
||||||
data: DesktopClientReleaseUploadResult
|
data: DesktopClientReleaseUploadResult
|
||||||
}>('/desktop-client/releases/upload', form)
|
}>('/desktop-client/releases/upload', form, {
|
||||||
|
timeout: DESKTOP_CLIENT_RELEASE_UPLOAD_TIMEOUT_MS,
|
||||||
|
})
|
||||||
return response.data.data
|
return response.data.data
|
||||||
},
|
},
|
||||||
resolveDownloadURL(id: number) {
|
resolveDownloadURL(id: number) {
|
||||||
|
|||||||
@@ -357,6 +357,7 @@ import {
|
|||||||
type DesktopReleasePlatform,
|
type DesktopReleasePlatform,
|
||||||
type DesktopReleaseSource,
|
type DesktopReleaseSource,
|
||||||
} from '@/lib/desktop-client-releases'
|
} from '@/lib/desktop-client-releases'
|
||||||
|
import { copyTextToClipboard } from '@/lib/clipboard'
|
||||||
import { showOpsError } from '@/lib/errors'
|
import { showOpsError } from '@/lib/errors'
|
||||||
|
|
||||||
const enabledOptions = [
|
const enabledOptions = [
|
||||||
@@ -763,19 +764,31 @@ async function deleteRelease(row: DesktopClientRelease) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function copyDownloadURL(row: DesktopClientRelease) {
|
async function copyDownloadURL(row: DesktopClientRelease) {
|
||||||
|
let downloadURL = ''
|
||||||
try {
|
try {
|
||||||
const result = await desktopClientReleaseApi.resolveDownloadURL(row.id)
|
const result = await desktopClientReleaseApi.resolveDownloadURL(row.id)
|
||||||
await navigator.clipboard.writeText(result.download_url)
|
downloadURL = result.download_url
|
||||||
message.success('下载地址已复制')
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showOpsError(error)
|
showOpsError(error)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await copyTextToClipboard(downloadURL)
|
||||||
|
message.success('下载地址已复制')
|
||||||
|
} catch {
|
||||||
|
message.error('复制失败,请在接口返回中手动复制下载地址')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function copyFormDownloadURL() {
|
async function copyFormDownloadURL() {
|
||||||
if (form.download_url) {
|
if (form.download_url) {
|
||||||
await navigator.clipboard.writeText(form.download_url)
|
try {
|
||||||
message.success('下载地址已复制')
|
await copyTextToClipboard(form.download_url)
|
||||||
|
message.success('下载地址已复制')
|
||||||
|
} catch {
|
||||||
|
message.error('复制失败,请手动复制下载地址')
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (!editingRow.value?.id) return
|
if (!editingRow.value?.id) return
|
||||||
|
|||||||
@@ -684,3 +684,12 @@ spec:
|
|||||||
limits:
|
limits:
|
||||||
cpu: 500m
|
cpu: 500m
|
||||||
memory: 256Mi
|
memory: 256Mi
|
||||||
|
volumeMounts:
|
||||||
|
- name: ops-web-nginx-config
|
||||||
|
mountPath: /etc/nginx/http.d/default.conf
|
||||||
|
subPath: default.conf
|
||||||
|
readOnly: true
|
||||||
|
volumes:
|
||||||
|
- name: ops-web-nginx-config
|
||||||
|
configMap:
|
||||||
|
name: ops-web-nginx-config
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
set_real_ip_from 127.0.0.1;
|
||||||
|
set_real_ip_from 10.42.0.0/16;
|
||||||
|
set_real_ip_from 10.43.0.0/16;
|
||||||
|
set_real_ip_from 172.16.0.0/12;
|
||||||
|
real_ip_header X-Forwarded-For;
|
||||||
|
real_ip_recursive on;
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name _;
|
||||||
|
client_max_body_size 600M;
|
||||||
|
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Never cache the SPA entrypoint — chunk hashes change every deploy and a
|
||||||
|
# stale index.html keeps users stuck on 404'd chunks even after reload.
|
||||||
|
location = /index.html {
|
||||||
|
add_header Cache-Control "no-cache, no-store, must-revalidate" always;
|
||||||
|
expires -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /api {
|
||||||
|
proxy_pass http://ops-api:8090;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_read_timeout 900s;
|
||||||
|
proxy_send_timeout 900s;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2?)$ {
|
||||||
|
expires 1y;
|
||||||
|
add_header Cache-Control "public, immutable";
|
||||||
|
}
|
||||||
|
|
||||||
|
gzip on;
|
||||||
|
gzip_static on;
|
||||||
|
gzip_vary on;
|
||||||
|
gzip_types text/plain text/css application/json application/javascript text/xml;
|
||||||
|
|
||||||
|
brotli_static on;
|
||||||
|
}
|
||||||
@@ -29,6 +29,9 @@ configMapGenerator:
|
|||||||
- config.yaml=config/config.yaml
|
- config.yaml=config/config.yaml
|
||||||
- prompts.yml=config/prompts.yml
|
- prompts.yml=config/prompts.yml
|
||||||
- ops-config.yaml=config/ops-config.yaml
|
- ops-config.yaml=config/ops-config.yaml
|
||||||
|
- name: ops-web-nginx-config
|
||||||
|
files:
|
||||||
|
- default.conf=config/ops-web-nginx.conf
|
||||||
|
|
||||||
images:
|
images:
|
||||||
- name: geo-rankly/migrate
|
- name: geo-rankly/migrate
|
||||||
|
|||||||
Reference in New Issue
Block a user