feat: podpora více odkazů na podnik při objednávání

This commit is contained in:
mates committed 2026-08-25 11:28:57 +02:00
1 parent 93242b7c7e
commit ce051447e6
13 files changed
+338 -81

No files matched your search

+60 -20
View File
@@ -4,16 +4,68 @@ import getStorage from "./storage";
const storage = getStorage();
const STORES_KEY = 'stores';
/** Maximální počet URL na jeden obchod (ochrana proti nafouknutí dat). */
const MAX_URLS = 10;
/**
* Vrátí seznam povolených obchodů. Zachovává zpětnou kompatibilitu se starším
* formátem, kdy byly obchody uloženy jako pole řetězců (převede je na objekty).
* Podoby, v jakých mohou být obchody uloženy ve storage:
* - řetězec — nejstarší formát (jen název),
* - objekt s `url` — formát s jednou URL na nabídku,
* - objekt s `urls` — aktuální formát s více URL.
*/
type StoredStore = string | { name: string; url?: string; urls?: string[] };
/**
* Vrátí seznam povolených obchodů. Zachovává zpětnou kompatibilitu se staršími
* formáty uložených dat (pole řetězců, resp. jediná URL v poli `url`) — vše
* převede na aktuální podobu se seznamem `urls`.
*/
export async function getStores(): Promise<Store[]> {
const raw = await storage.getData<(string | Store)[]>(STORES_KEY);
const raw = await storage.getData<StoredStore[]>(STORES_KEY);
if (!raw) {
return [];
}
return raw.map(s => (typeof s === 'string' ? { name: s } : s));
return raw.map(s => {
if (typeof s === 'string') {
return { name: s };
}
const urls = s.urls ?? (s.url ? [s.url] : []);
return urls.length > 0 ? { name: s.name, urls } : { name: s.name };
});
}
/**
* Ověří a upraví seznam URL obchodu — ořeže mezery, zahodí prázdné hodnoty,
* odstraní duplicity a zkontroluje, že jde o platné http(s) odkazy.
*/
function normalizeUrls(urls?: string[]): string[] {
if (!urls) {
return [];
}
const result: string[] = [];
for (const raw of urls) {
const trimmed = raw?.trim();
if (!trimmed) {
continue;
}
// Povolíme pouze http(s), aby URL nemohla být zneužita (např. javascript: → XSS)
let parsed: URL;
try {
parsed = new URL(trimmed);
} catch {
throw new Error('Neplatná URL obchodu');
}
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
throw new Error('URL musí začínat http:// nebo https://');
}
if (!result.some(u => u.toLowerCase() === trimmed.toLowerCase())) {
result.push(trimmed);
}
}
if (result.length > MAX_URLS) {
throw new Error(`Obchod může mít nejvýše ${MAX_URLS} URL`);
}
return result;
}
/**
@@ -21,9 +73,9 @@ export async function getStores(): Promise<Store[]> {
*
* @param name název obchodu
* @param heslo admin heslo
* @param url volitelná URL na nabídku podniku
* @param urls volitelný seznam URL na nabídku podniku (různé dovozové služby)
*/
export async function addStore(name: string, heslo: string, url?: string): Promise<Store[]> {
export async function addStore(name: string, heslo: string, urls?: string[]): Promise<Store[]> {
const adminPassword = process.env.ADMIN_PASSWORD;
if (!adminPassword || heslo !== adminPassword) {
throw new Error('UNAUTHORIZED');
@@ -36,20 +88,8 @@ export async function addStore(name: string, heslo: string, url?: string): Promi
if (stores.some(s => s.name.toLowerCase() === trimmed.toLowerCase())) {
throw new Error('Obchod s tímto názvem již existuje');
}
const trimmedUrl = url?.trim();
if (trimmedUrl) {
// Povolíme pouze http(s), aby URL nemohla být zneužita (např. javascript: → XSS)
let parsed: URL;
try {
parsed = new URL(trimmedUrl);
} catch {
throw new Error('Neplatná URL obchodu');
}
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
throw new Error('URL musí začínat http:// nebo https://');
}
}
const store: Store = trimmedUrl ? { name: trimmed, url: trimmedUrl } : { name: trimmed };
const normalizedUrls = normalizeUrls(urls);
const store: Store = normalizedUrls.length > 0 ? { name: trimmed, urls: normalizedUrls } : { name: trimmed };
const updated = [...stores, store];
await storage.setData(STORES_KEY, updated);
return updated;