change: security-check, memory-check [untest]
- fix high risk security issues - fix high memory usage - change adb to connection pool single instance Signed-off-by: pakintada@gmail.com <Pakin>
This commit is contained in:
parent
f0619c5a10
commit
4f464a8513
23 changed files with 413 additions and 73 deletions
|
|
@ -11,6 +11,7 @@
|
|||
import { auth } from '$lib/core/stores/auth';
|
||||
import { connectToWebsocket } from '$lib/core/stores/websocketStore';
|
||||
import * as adb from '$lib/core/adb/adb';
|
||||
import { adbConnectionStatus } from '$lib/core/stores/adbConnectionStore';
|
||||
import { addNotification } from '$lib/core/stores/noti';
|
||||
import { page } from '$app/stores';
|
||||
import {
|
||||
|
|
@ -117,7 +118,7 @@
|
|||
|
||||
if (adbReconnectTriedForUid !== currentUser.uid && !adb.getAdbInstance()) {
|
||||
adbReconnectTriedForUid = currentUser.uid;
|
||||
// void tryAutoConnect();
|
||||
void tryAutoConnect();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
import ArrowRight from '@lucide/svelte/icons/arrow-right';
|
||||
import { permission as currentPermissions } from '$lib/core/stores/permissions';
|
||||
import { get } from 'svelte/store';
|
||||
import { onMount } from 'svelte';
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import { referenceFromPage} from '$lib/core/stores/recipeStore';
|
||||
|
||||
let recipeModBtn = $state<HTMLElement | null>(null);
|
||||
|
|
@ -23,7 +23,11 @@
|
|||
|
||||
let perms = $state<string[]>([]);
|
||||
|
||||
setInterval(() => {
|
||||
// Wait for the goto-dashboard button to be mounted, then start a pulse
|
||||
// animation once. Use an interval so we can react to the $state element
|
||||
// being set after the initial render; stop polling as soon as the
|
||||
// animation has been started.
|
||||
const _pulseInterval = setInterval(() => {
|
||||
if (gotoDashboardBtn && !animationPulseGoto) {
|
||||
animationPulseGoto = animate(gotoDashboardBtn, {
|
||||
opacity: [0.8, 1], // Slight pulse in opacity
|
||||
|
|
@ -37,6 +41,10 @@
|
|||
}
|
||||
}, 1000);
|
||||
|
||||
onDestroy(() => {
|
||||
clearInterval(_pulseInterval);
|
||||
});
|
||||
|
||||
// let perms = get(currentPermissions);
|
||||
|
||||
onMount(() => {
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
import Checkbox from '$lib/components/ui/checkbox/checkbox.svelte';
|
||||
import Spinner from '$lib/components/ui/spinner/spinner.svelte';
|
||||
import * as adb from '$lib/core/adb/adb';
|
||||
import { adbConnectionStatus } from '$lib/core/stores/adbConnectionStore';
|
||||
import { addNotification } from '$lib/core/stores/noti';
|
||||
import { referenceFromPage } from '$lib/core/stores/recipeStore';
|
||||
import type { Material } from '$lib/models/material.model';
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
import Checkbox from '$lib/components/ui/checkbox/checkbox.svelte';
|
||||
import Spinner from '$lib/components/ui/spinner/spinner.svelte';
|
||||
import * as adb from '$lib/core/adb/adb';
|
||||
import { adbConnectionStatus } from '$lib/core/stores/adbConnectionStore';
|
||||
import { addNotification } from '$lib/core/stores/noti';
|
||||
import { referenceFromPage } from '$lib/core/stores/recipeStore';
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
import { onMount, onDestroy } from 'svelte';
|
||||
|
||||
import * as adb from '$lib/core/adb/adb';
|
||||
import { adbConnectionStatus } from '$lib/core/stores/adbConnectionStore';
|
||||
import { addNotification } from '$lib/core/stores/noti';
|
||||
import { columns, type RecipeOverview } from '../../recipe/overview/columns';
|
||||
import {
|
||||
|
|
@ -72,7 +73,7 @@
|
|||
|
||||
// clear out event
|
||||
|
||||
GlobalEventBus.on('recipe-event', (d: any) => {
|
||||
const unsubRecipeEvent = GlobalEventBus.on('recipe-event', (d: any) => {
|
||||
logger.info('[recipe-ev] get event: ', d);
|
||||
if (d?.type == 'load-recipe' && d?.status == 'end') {
|
||||
addNotification('INFO:Get data, waiting for reloading ...');
|
||||
|
|
@ -829,6 +830,7 @@
|
|||
});
|
||||
|
||||
onDestroy(() => {
|
||||
unsubRecipeEvent();
|
||||
clearOnMenuSavedCallback();
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
import { goto } from '$app/navigation';
|
||||
|
||||
import * as adb from '$lib/core/adb/adb';
|
||||
import { adbConnectionStatus } from '$lib/core/stores/adbConnectionStore';
|
||||
import { addNotification } from '$lib/core/stores/noti';
|
||||
import { referenceFromPage } from '$lib/core/stores/recipeStore';
|
||||
import { env } from '$env/dynamic/public';
|
||||
|
|
@ -295,21 +296,12 @@
|
|||
|
||||
async function connectAdb() {
|
||||
try {
|
||||
if (adb.getAdbInstance()) {
|
||||
const connected = await adb.ensureAdbConnection();
|
||||
if (connected) {
|
||||
if (!isAdbWriterAvailable()) {
|
||||
await adb.reconnectAndroidRecipeMenuServer();
|
||||
}
|
||||
await loadRecipeFromMachine();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!('usb' in navigator)) {
|
||||
throw new Error('WebUSB not supported');
|
||||
}
|
||||
|
||||
await adb.connectRecipeMenuViaWebUSB();
|
||||
if (adb.getAdbInstance()) {
|
||||
await loadRecipeFromMachine();
|
||||
}
|
||||
} catch (e: any) {
|
||||
addNotification(`ERROR:${e}`);
|
||||
|
|
|
|||
|
|
@ -2,12 +2,16 @@ import { logger } from '$lib/core/utils/logger';
|
|||
import { json, error } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
import { env } from '$env/dynamic/public';
|
||||
import { verifyAuthToken } from '$lib/server/auth';
|
||||
|
||||
// Method 2: forward a machine-generated sync_1.file to the adv FTP server.
|
||||
const ADV_API_BASE = env.PUBLIC_POST_IMAGE;
|
||||
|
||||
export const POST: RequestHandler = async ({ request }) => {
|
||||
try {
|
||||
// Verify authentication
|
||||
await verifyAuthToken(request);
|
||||
|
||||
const formData = await request.formData();
|
||||
|
||||
const country = formData.get('country') as string;
|
||||
|
|
|
|||
|
|
@ -2,12 +2,21 @@ import { logger } from '$lib/core/utils/logger';
|
|||
import { json, error } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
import { env } from '$env/dynamic/public';
|
||||
import { verifyAuthToken } from '$lib/server/auth';
|
||||
|
||||
// Adv videos are served by the same taobin_image service as menu images.
|
||||
const ADV_API_BASE = env.PUBLIC_POST_IMAGE;
|
||||
|
||||
const ALLOWED_MIME_TYPES = [
|
||||
'video/mp4', 'video/webm', 'video/ogg',
|
||||
'image/jpeg', 'image/png', 'image/gif', 'image/webp'
|
||||
];
|
||||
|
||||
export const POST: RequestHandler = async ({ request }) => {
|
||||
try {
|
||||
// Verify authentication
|
||||
await verifyAuthToken(request);
|
||||
|
||||
const formData = await request.formData();
|
||||
|
||||
const country = formData.get('country') as string;
|
||||
|
|
@ -22,6 +31,11 @@ export const POST: RequestHandler = async ({ request }) => {
|
|||
throw error(400, 'Missing required fields');
|
||||
}
|
||||
|
||||
// File type validation (size limit removed — upstream handles it)
|
||||
if (!ALLOWED_MIME_TYPES.includes(file.type)) {
|
||||
throw error(400, `File type "${file.type}" not allowed. Allowed: ${ALLOWED_MIME_TYPES.join(', ')}`);
|
||||
}
|
||||
|
||||
const endpoint =
|
||||
`${ADV_API_BASE}/adv/upload/${encodeURIComponent(country)}/${encodeURIComponent(uid)}/${encodeURIComponent(displayName)}/${encodeURIComponent(email)}` +
|
||||
`?regenerate=${encodeURIComponent(regenerate)}`;
|
||||
|
|
|
|||
|
|
@ -2,11 +2,17 @@ import { logger } from '$lib/core/utils/logger';
|
|||
import { json, error } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
import { env } from '$env/dynamic/public';
|
||||
import { verifyAuthToken } from '$lib/server/auth';
|
||||
|
||||
const IMAGE_API_BASE = env.PUBLIC_POST_IMAGE;
|
||||
|
||||
const ALLOWED_MIME_TYPES = ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/avif'];
|
||||
|
||||
export const POST: RequestHandler = async ({ request }) => {
|
||||
try {
|
||||
// Verify authentication
|
||||
await verifyAuthToken(request);
|
||||
|
||||
const formData = await request.formData();
|
||||
|
||||
const country = formData.get('country') as string;
|
||||
|
|
@ -19,6 +25,11 @@ export const POST: RequestHandler = async ({ request }) => {
|
|||
if (!folder || !uid || !displayName || !email || !file) {
|
||||
throw error(400, 'Missing required fields');
|
||||
}
|
||||
|
||||
// File type validation (size limit removed — upstream handles it)
|
||||
if (!ALLOWED_MIME_TYPES.includes(file.type)) {
|
||||
throw error(400, `File type "${file.type}" not allowed. Allowed: ${ALLOWED_MIME_TYPES.join(', ')}`);
|
||||
}
|
||||
|
||||
|
||||
// Build the upload endpoint
|
||||
|
|
|
|||
|
|
@ -1,21 +1,50 @@
|
|||
import { logger } from '$lib/core/utils/logger';
|
||||
import { json } from '@sveltejs/kit';
|
||||
import { error, json } from '@sveltejs/kit';
|
||||
import type { RequestEvent } from '@sveltejs/kit';
|
||||
import { verifyAuthToken } from '$lib/server/auth';
|
||||
|
||||
// In-memory store for streamed catalog data
|
||||
// Format: { batchId: { chunks: [...], status: 'collecting'|'complete'|'error' } }
|
||||
const streamCache = new Map<string, any>();
|
||||
const MAX_CACHE_SIZE = 200; // max number of batches in cache
|
||||
const MAX_CHUNKS_PER_BATCH = 500;
|
||||
|
||||
export async function POST({ request }) {
|
||||
export async function POST({ request }: RequestEvent) {
|
||||
try {
|
||||
await verifyAuthToken(request);
|
||||
|
||||
const data = await request.json();
|
||||
const { batch_id, msg, content, current_chunk, total_chunks } = data.payload;
|
||||
const { batch_id, msg, content, current_chunk, total_chunks } = data.payload ?? {};
|
||||
|
||||
// Input validation
|
||||
if (!batch_id || typeof batch_id !== 'string') {
|
||||
throw error(400, 'Missing or invalid batch_id');
|
||||
}
|
||||
if (!msg || typeof msg !== 'string') {
|
||||
throw error(400, 'Missing or invalid msg');
|
||||
}
|
||||
if (content !== undefined && content !== null && typeof content !== 'string') {
|
||||
throw error(400, 'content must be a string');
|
||||
}
|
||||
|
||||
// Enforce cache size limit — evict oldest batches when over capacity
|
||||
if (!streamCache.has(batch_id) && streamCache.size >= MAX_CACHE_SIZE) {
|
||||
const oldestKey = streamCache.keys().next().value;
|
||||
if (oldestKey !== undefined) {
|
||||
streamCache.delete(oldestKey);
|
||||
logger.warn(`[API] Evicted oldest batch "${oldestKey}" — cache full`);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize or update batch
|
||||
if (!streamCache.has(batch_id)) {
|
||||
const cappedTotal = typeof total_chunks === 'number' && total_chunks > 0
|
||||
? Math.min(total_chunks, MAX_CHUNKS_PER_BATCH)
|
||||
: MAX_CHUNKS_PER_BATCH;
|
||||
streamCache.set(batch_id, {
|
||||
chunks: [],
|
||||
status: 'collecting',
|
||||
total_chunks,
|
||||
total_chunks: cappedTotal,
|
||||
createdAt: Date.now()
|
||||
});
|
||||
}
|
||||
|
|
@ -26,6 +55,10 @@ export async function POST({ request }) {
|
|||
if (msg === 'start') {
|
||||
logger.info(`[API] Stream started for batch ${batch_id}`);
|
||||
} else if (msg === 'chunk') {
|
||||
if (batch.chunks.length >= MAX_CHUNKS_PER_BATCH) {
|
||||
logger.warn(`[API] Max chunks reached for batch ${batch_id}, ignoring chunk`);
|
||||
return json({ status: 'received', batch_id, warning: 'max_chunks_reached' });
|
||||
}
|
||||
batch.chunks.push(content);
|
||||
logger.info(`[API] Received chunk ${current_chunk}/${total_chunks} for batch ${batch_id}`);
|
||||
} else if (msg === 'end') {
|
||||
|
|
@ -44,7 +77,10 @@ export async function POST({ request }) {
|
|||
}
|
||||
}
|
||||
|
||||
export function GET({ url }) {
|
||||
export async function GET({ request, url }: RequestEvent) {
|
||||
// Verify authentication
|
||||
await verifyAuthToken(request);
|
||||
|
||||
const batchId = url.searchParams.get('batch_id');
|
||||
|
||||
// Clean up old cache entries (older than 5 minutes)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue