Add choices API endpoint and OpenAPI client setup
Introduces a new /api/choices/ endpoint for fetching model choices with multilingual labels. Updates Django models to use 'cz#' prefix for Czech labels. Adds OpenAPI client generation via orval, refactors frontend API structure, and provides documentation and helper scripts for dynamic choices and OpenAPI usage.
This commit is contained in:
7
.github/copilot-instructions.md
vendored
7
.github/copilot-instructions.md
vendored
@@ -107,6 +107,13 @@ Notes
|
|||||||
- **Task queue**: Celery + Redis for async/background jobs.
|
- **Task queue**: Celery + Redis for async/background jobs.
|
||||||
- **API**: REST endpoints, JWT auth, API key support.
|
- **API**: REST endpoints, JWT auth, API key support.
|
||||||
|
|
||||||
|
### OpenAPI Client Generation
|
||||||
|
- Schema: `config = { schemaUrl: "/api/schema/", baseUrl: "/api/" }`
|
||||||
|
- Commands: `npm run api:update` (fetch schema + generate client)
|
||||||
|
- Output: `frontend/src/api/generated/` (TypeScript Axios client)
|
||||||
|
- Axios instance: `frontend/src/api/api.ts` with `withCredentials` and JWT auto-refresh via existing `Client.ts`.
|
||||||
|
- Choices helper: `frontend/src/api/get_choices.ts` → `getChoices(requests, lang)` returns `{ "Model.field": [{ value, label }] }`.
|
||||||
|
|
||||||
## References
|
## References
|
||||||
- [frontend/REACT.md](../frontend/REACT.md): Frontend structure, workflows, and conventions.
|
- [frontend/REACT.md](../frontend/REACT.md): Frontend structure, workflows, and conventions.
|
||||||
- [frontend/src/layouts/LAYOUTS.md](../frontend/src/layouts/LAYOUTS.md): Layout/component patterns.
|
- [frontend/src/layouts/LAYOUTS.md](../frontend/src/layouts/LAYOUTS.md): Layout/component patterns.
|
||||||
|
|||||||
@@ -42,9 +42,9 @@ class CustomUser(SoftDeleteModel, AbstractUser):
|
|||||||
)
|
)
|
||||||
|
|
||||||
class Role(models.TextChoices):
|
class Role(models.TextChoices):
|
||||||
ADMIN = "admin", "Admin"
|
ADMIN = "admin", "cz#Administrátor"
|
||||||
MANAGER = "mod", "Moderator"
|
MANAGER = "mod", "cz#Moderator"
|
||||||
CUSTOMER = "regular", "Regular"
|
CUSTOMER = "regular", "cz#Regular"
|
||||||
|
|
||||||
role = models.CharField(max_length=20, choices=Role.choices, default=Role.CUSTOMER)
|
role = models.CharField(max_length=20, choices=Role.choices, default=Role.CUSTOMER)
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ from rest_framework_simplejwt.authentication import JWTAuthentication
|
|||||||
#NEMĚNIT CUSTOM SBÍRANÍ COOKIE TOKENU
|
#NEMĚNIT CUSTOM SBÍRANÍ COOKIE TOKENU
|
||||||
class CookieJWTAuthentication(JWTAuthentication):
|
class CookieJWTAuthentication(JWTAuthentication):
|
||||||
def authenticate(self, request):
|
def authenticate(self, request):
|
||||||
|
|
||||||
raw_token = request.COOKIES.get('access_token')
|
raw_token = request.COOKIES.get('access_token')
|
||||||
|
|
||||||
if not raw_token:
|
if not raw_token:
|
||||||
|
|||||||
@@ -103,12 +103,12 @@ class ProductImage(models.Model):
|
|||||||
|
|
||||||
class Order(models.Model):
|
class Order(models.Model):
|
||||||
class Status(models.TextChoices):
|
class Status(models.TextChoices):
|
||||||
CREATED = "created", "Vytvořeno"
|
CREATED = "created", "cz#Vytvořeno"
|
||||||
CANCELLED = "cancelled", "Zrušeno"
|
CANCELLED = "cancelled", "cz#Zrušeno"
|
||||||
COMPLETED = "completed", "Dokončeno"
|
COMPLETED = "completed", "cz#Dokončeno"
|
||||||
|
|
||||||
REFUNDING = "refunding", "Vrácení v procesu"
|
REFUNDING = "refunding", "cz#Vrácení v procesu"
|
||||||
REFUNDED = "refunded", "Vráceno"
|
REFUNDED = "refunded", "cz#Vráceno"
|
||||||
|
|
||||||
status = models.CharField(
|
status = models.CharField(
|
||||||
max_length=20, choices=Status.choices, null=True, blank=True, default=Status.CREATED
|
max_length=20, choices=Status.choices, null=True, blank=True, default=Status.CREATED
|
||||||
@@ -205,15 +205,15 @@ class Order(models.Model):
|
|||||||
|
|
||||||
class Carrier(models.Model):
|
class Carrier(models.Model):
|
||||||
class SHIPPING(models.TextChoices):
|
class SHIPPING(models.TextChoices):
|
||||||
ZASILKOVNA = "packeta", "Zásilkovna"
|
ZASILKOVNA = "packeta", "cz#Zásilkovna"
|
||||||
STORE = "store", "Osobní odběr"
|
STORE = "store", "cz#Osobní odběr"
|
||||||
shipping_method = models.CharField(max_length=20, choices=SHIPPING.choices, default=SHIPPING.STORE)
|
shipping_method = models.CharField(max_length=20, choices=SHIPPING.choices, default=SHIPPING.STORE)
|
||||||
|
|
||||||
class STATE(models.TextChoices):
|
class STATE(models.TextChoices):
|
||||||
PREPARING = "ordered", "Objednávka se připravuje"
|
PREPARING = "ordered", "cz#Objednávka se připravuje"
|
||||||
SHIPPED = "shipped", "Odesláno"
|
SHIPPED = "shipped", "cz#Odesláno"
|
||||||
DELIVERED = "delivered", "Doručeno"
|
DELIVERED = "delivered", "cz#Doručeno"
|
||||||
READY_TO_PICKUP = "ready_to_pickup", "Připraveno k vyzvednutí"
|
READY_TO_PICKUP = "ready_to_pickup", "cz#Připraveno k vyzvednutí"
|
||||||
#RETURNING = "returning", "Vracení objednávky"
|
#RETURNING = "returning", "Vracení objednávky"
|
||||||
state = models.CharField(max_length=20, choices=STATE.choices, default=STATE.PREPARING)
|
state = models.CharField(max_length=20, choices=STATE.choices, default=STATE.PREPARING)
|
||||||
|
|
||||||
@@ -276,9 +276,9 @@ class Carrier(models.Model):
|
|||||||
|
|
||||||
class Payment(models.Model):
|
class Payment(models.Model):
|
||||||
class PAYMENT(models.TextChoices):
|
class PAYMENT(models.TextChoices):
|
||||||
SHOP = "shop", "Platba v obchodě"
|
SHOP = "shop", "cz#Platba v obchodě"
|
||||||
STRIPE = "stripe", "Bankovní převod"
|
STRIPE = "stripe", "cz#Bankovní převod"
|
||||||
CASH_ON_DELIVERY = "cash_on_delivery", "Dobírka"
|
CASH_ON_DELIVERY = "cash_on_delivery", "cz#Dobírka"
|
||||||
payment_method = models.CharField(max_length=30, choices=PAYMENT.choices, default=PAYMENT.SHOP)
|
payment_method = models.CharField(max_length=30, choices=PAYMENT.choices, default=PAYMENT.SHOP)
|
||||||
|
|
||||||
#FIXME: potvrdit že logika platby funguje správně
|
#FIXME: potvrdit že logika platby funguje správně
|
||||||
@@ -472,10 +472,10 @@ class Refund(models.Model):
|
|||||||
order = models.ForeignKey(Order, related_name="refunds", on_delete=models.CASCADE)
|
order = models.ForeignKey(Order, related_name="refunds", on_delete=models.CASCADE)
|
||||||
|
|
||||||
class Reason(models.TextChoices):
|
class Reason(models.TextChoices):
|
||||||
RETUNING_PERIOD = "retuning_before_fourteen_day_period", "Vrácení před uplynutím 14-ti denní lhůty"
|
RETUNING_PERIOD = "retuning_before_fourteen_day_period", "cz#Vrácení před uplynutím 14-ti denní lhůty"
|
||||||
DAMAGED_PRODUCT = "damaged_product", "Poškozený produkt"
|
DAMAGED_PRODUCT = "damaged_product", "cz#Poškozený produkt"
|
||||||
WRONG_ITEM = "wrong_item", "Špatná položka"
|
WRONG_ITEM = "wrong_item", "cz#Špatná položka"
|
||||||
OTHER = "other", "Jiný důvod"
|
OTHER = "other", "cz#Jiný důvod"
|
||||||
reason_choice = models.CharField(max_length=30, choices=Reason.choices)
|
reason_choice = models.CharField(max_length=30, choices=Reason.choices)
|
||||||
reason_text = models.TextField(blank=True)
|
reason_text = models.TextField(blank=True)
|
||||||
|
|
||||||
|
|||||||
@@ -30,8 +30,8 @@ class ShopConfiguration(models.Model):
|
|||||||
addition_of_coupons_amount = models.BooleanField(default=False, help_text="Sčítání slevových kupónů v objednávce (ano/ne), pokud ne tak se použije pouze nejvyšší slevový kupón")
|
addition_of_coupons_amount = models.BooleanField(default=False, help_text="Sčítání slevových kupónů v objednávce (ano/ne), pokud ne tak se použije pouze nejvyšší slevový kupón")
|
||||||
|
|
||||||
class CURRENCY(models.TextChoices):
|
class CURRENCY(models.TextChoices):
|
||||||
CZK = "CZK", "Czech Koruna"
|
CZK = "CZK", "cz#Czech Koruna"
|
||||||
EUR = "EUR", "Euro"
|
EUR = "EUR", "cz#Euro"
|
||||||
currency = models.CharField(max_length=10, default=CURRENCY.CZK, choices=CURRENCY.choices)
|
currency = models.CharField(max_length=10, default=CURRENCY.CZK, choices=CURRENCY.choices)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|||||||
12
backend/thirdparty/stripe/models.py
vendored
12
backend/thirdparty/stripe/models.py
vendored
@@ -7,12 +7,12 @@ from .client import StripeClient
|
|||||||
|
|
||||||
class StripeModel(models.Model):
|
class StripeModel(models.Model):
|
||||||
class STATUS_CHOICES(models.TextChoices):
|
class STATUS_CHOICES(models.TextChoices):
|
||||||
PENDING = "pending", "Čeká se na platbu"
|
PENDING = "pending", "cz#Čeká se na platbu"
|
||||||
PAID = "paid", "Zaplaceno"
|
PAID = "paid", "cz#Zaplaceno"
|
||||||
FAILED = "failed", "Neúspěšné"
|
FAILED = "failed", "cz#Neúspěšné"
|
||||||
CANCELLED = "cancelled", "Zrušeno"
|
CANCELLED = "cancelled", "cz#Zrušeno"
|
||||||
REFUNDING = "refunding", "Platba se vrací"
|
REFUNDING = "refunding", "cz#Platba se vrací"
|
||||||
REFUNDED = "refunded", "Platba úspěšně vrácena"
|
REFUNDED = "refunded", "cz#Platba úspěšně vrácena"
|
||||||
|
|
||||||
status = models.CharField(max_length=20, choices=STATUS_CHOICES.choices, default=STATUS_CHOICES.PENDING)
|
status = models.CharField(max_length=20, choices=STATUS_CHOICES.choices, default=STATUS_CHOICES.PENDING)
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ Including another URLconf
|
|||||||
"""
|
"""
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.urls import path, include
|
from django.urls import path, include
|
||||||
|
from .views import choices
|
||||||
|
|
||||||
from drf_spectacular.views import (
|
from drf_spectacular.views import (
|
||||||
SpectacularAPIView,
|
SpectacularAPIView,
|
||||||
@@ -31,6 +32,7 @@ urlpatterns = [
|
|||||||
|
|
||||||
|
|
||||||
path('admin/', admin.site.urls),
|
path('admin/', admin.site.urls),
|
||||||
|
path("api/choices/", choices, name="choices"),
|
||||||
path('api/account/', include('account.urls')),
|
path('api/account/', include('account.urls')),
|
||||||
path('api/commerce/', include('commerce.urls')),
|
path('api/commerce/', include('commerce.urls')),
|
||||||
#path('api/advertisments/', include('advertisements.urls')),
|
#path('api/advertisments/', include('advertisements.urls')),
|
||||||
|
|||||||
85
backend/vontor_cz/views.py
Normal file
85
backend/vontor_cz/views.py
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
import json
|
||||||
|
from django.apps import apps
|
||||||
|
from rest_framework.decorators import api_view
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from drf_spectacular.utils import extend_schema, OpenApiParameter
|
||||||
|
from rest_framework.permissions import AllowAny
|
||||||
|
|
||||||
|
@extend_schema(
|
||||||
|
description="Vrátí všechny možné hodnoty pro ChoiceField s podporou vícejazyčných labelů. "
|
||||||
|
"Umožňuje načíst více modelů a polí najednou.",
|
||||||
|
parameters=[
|
||||||
|
OpenApiParameter(
|
||||||
|
name="fields",
|
||||||
|
description=(
|
||||||
|
"JSON pole objektů {model: 'ModelName', field: 'field_name'} "
|
||||||
|
"např. '[{\"model\": \"User\", \"field\": \"role\"}, {\"model\": \"Carrier\", \"field\": \"shipping_method\"}]'"
|
||||||
|
),
|
||||||
|
required=True,
|
||||||
|
type=str,
|
||||||
|
),
|
||||||
|
OpenApiParameter(
|
||||||
|
name="lang",
|
||||||
|
description="Jazyk pro labely (např. 'cz', 'en')",
|
||||||
|
required=False,
|
||||||
|
type=str,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
responses={
|
||||||
|
200: {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"value": {"type": "string", "description": "Logická hodnota pro backend"},
|
||||||
|
"label": {"type": "string", "description": "Human-readable label podle zvoleného jazyka"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
@api_view(["GET"])
|
||||||
|
def choices(request):
|
||||||
|
"""
|
||||||
|
Endpoint pro získání choices pro dropdowny s podporou vícejazyčných labelů.
|
||||||
|
Formát labelu v modelu: 'lang#Label', např. 'cz#Administrátor'.
|
||||||
|
Podporuje více modelů a polí najednou.
|
||||||
|
"""
|
||||||
|
permission_classes = [AllowAny]
|
||||||
|
|
||||||
|
fields_param = request.query_params.get("fields")
|
||||||
|
lang = request.query_params.get("lang", None)
|
||||||
|
|
||||||
|
try:
|
||||||
|
fields_list = json.loads(fields_param)
|
||||||
|
except Exception:
|
||||||
|
return Response({"error": "Invalid 'fields' parameter, must be valid JSON"}, status=400)
|
||||||
|
|
||||||
|
result = {}
|
||||||
|
for item in fields_list:
|
||||||
|
model_name = item.get("model")
|
||||||
|
field_name = item.get("field")
|
||||||
|
if not model_name or not field_name:
|
||||||
|
continue
|
||||||
|
|
||||||
|
model = apps.get_model("app_name", model_name)
|
||||||
|
field = model._meta.get_field(field_name)
|
||||||
|
|
||||||
|
choices_data = []
|
||||||
|
for value, label in field.choices:
|
||||||
|
if "#" in label and lang:
|
||||||
|
label_parts = label.split("#", 1)
|
||||||
|
if label_parts[0] == lang:
|
||||||
|
label = label_parts[1]
|
||||||
|
else:
|
||||||
|
label = label_parts[1] # fallback na defaultní text (po #)
|
||||||
|
elif "#" in label:
|
||||||
|
label = label.split("#", 1)[1]
|
||||||
|
choices_data.append({"value": value, "label": label})
|
||||||
|
|
||||||
|
result[f"{model_name}.{field_name}"] = choices_data
|
||||||
|
|
||||||
|
return Response(result)
|
||||||
30
frontend/orval.config.js
Normal file
30
frontend/orval.config.js
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
module.exports = {
|
||||||
|
public: {
|
||||||
|
input: { target: "http://localhost:8000/api/schema/" },
|
||||||
|
output: {
|
||||||
|
target: "src/api/generated/public.ts",
|
||||||
|
schemas: "src/api/generated/models",
|
||||||
|
client: "axios",
|
||||||
|
override: {
|
||||||
|
mutator: {
|
||||||
|
path: "src/api/publicClient.ts",
|
||||||
|
name: "publicApi",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
private: {
|
||||||
|
input: { target: "http://localhost:8000/api/schema/" },
|
||||||
|
output: {
|
||||||
|
target: "src/api/generated/private.ts",
|
||||||
|
schemas: "src/api/generated/models",
|
||||||
|
client: "axios",
|
||||||
|
override: {
|
||||||
|
mutator: {
|
||||||
|
path: "src/api/privateClient.ts",
|
||||||
|
name: "privateApi",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -7,7 +7,8 @@
|
|||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "tsc -b && vite build",
|
"build": "tsc -b && vite build",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"preview": "vite preview"
|
"preview": "vite preview",
|
||||||
|
"api:gen": "orval --config orval.config.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tailwindcss/vite": "^4.1.16",
|
"@tailwindcss/vite": "^4.1.16",
|
||||||
@@ -33,6 +34,7 @@
|
|||||||
"globals": "^16.3.0",
|
"globals": "^16.3.0",
|
||||||
"typescript": "~5.8.3",
|
"typescript": "~5.8.3",
|
||||||
"typescript-eslint": "^8.39.1",
|
"typescript-eslint": "^8.39.1",
|
||||||
"vite": "^7.1.2"
|
"vite": "^7.1.2",
|
||||||
|
"openapi-generator-cli": "^2.9.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
25
frontend/scripts/fetch-openapi.js
Normal file
25
frontend/scripts/fetch-openapi.js
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import fs from "fs";
|
||||||
|
import path from "path";
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
// Single config point
|
||||||
|
const config = { schemaUrl: "/api/schema/", baseUrl: "/api/" };
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const outDir = path.resolve("./src/openapi");
|
||||||
|
const outFile = path.join(outDir, "schema.json");
|
||||||
|
const base = process.env.VITE_API_BASE_URL || "http://localhost:8000";
|
||||||
|
const url = new URL(config.schemaUrl, base).toString();
|
||||||
|
|
||||||
|
console.log(`[openapi] Fetching schema from ${url}`);
|
||||||
|
const res = await axios.get(url, { headers: { Accept: "application/json" } });
|
||||||
|
|
||||||
|
await fs.promises.mkdir(outDir, { recursive: true });
|
||||||
|
await fs.promises.writeFile(outFile, JSON.stringify(res.data, null, 2), "utf8");
|
||||||
|
console.log(`[openapi] Wrote ${outFile}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((err) => {
|
||||||
|
console.error("[openapi] Failed to fetch schema:", err?.message || err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
18
frontend/src/api/chocies.ts
Normal file
18
frontend/src/api/chocies.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { publicApi } from "./publicClient";
|
||||||
|
|
||||||
|
export async function getChoices(queries: {
|
||||||
|
model: string;
|
||||||
|
field: string;
|
||||||
|
lang?: string;
|
||||||
|
}[]) {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
|
||||||
|
queries.forEach((q) => {
|
||||||
|
params.append("model", q.model);
|
||||||
|
params.append("field", q.field);
|
||||||
|
if (q.lang) params.append("lang", q.lang);
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data } = await publicApi.get(`/choices/?${params.toString()}`);
|
||||||
|
return data; // typ: Array<{ value: string; label: string }>
|
||||||
|
}
|
||||||
9
frontend/src/api/docs/choices.md
Normal file
9
frontend/src/api/docs/choices.md
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# 🌈 Choices (dynamic enums)
|
||||||
|
|
||||||
|
Získaní možných hodnot pro pole s výběrem (ChoiceField) z backendu s podporou vícejazyčných labelů, definované v modelech Django.
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
const roles = await getChoices([
|
||||||
|
{ model: "User", field: "role", lang: "cz" },
|
||||||
|
]);
|
||||||
|
```
|
||||||
7
frontend/src/api/docs/example_of_getting_data.md
Normal file
7
frontend/src/api/docs/example_of_getting_data.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# Získání seznamu objektů (např. Orders)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { ordersList } from "@/api/generated/private";
|
||||||
|
|
||||||
|
const orders = await ordersList();
|
||||||
|
```
|
||||||
12
frontend/src/api/docs/login_logout.md
Normal file
12
frontend/src/api/docs/login_logout.md
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
|
||||||
|
# 🔐 Přihlášení (public)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
|
||||||
|
import { authLoginCreate } from "@/api/generated/public";
|
||||||
|
|
||||||
|
await authLoginCreate({
|
||||||
|
email: "test@test.com",
|
||||||
|
password: "secret",
|
||||||
|
});
|
||||||
|
```
|
||||||
15
frontend/src/api/docs/soubory.md
Normal file
15
frontend/src/api/docs/soubory.md
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# 🖼️ Podpora FileField / ImageField
|
||||||
|
|
||||||
|
Orval automaticky vytvoří endpointy s multipart/form-data.
|
||||||
|
|
||||||
|
Použití:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { productsUpdate } from "@/api/generated/private";
|
||||||
|
|
||||||
|
const form = new FormData();
|
||||||
|
form.append("name", values.name);
|
||||||
|
form.append("image", fileInput.files[0]);
|
||||||
|
|
||||||
|
await productsUpdate({ id: productId, data: form });
|
||||||
|
```
|
||||||
1
frontend/src/api/generated/filler
Normal file
1
frontend/src/api/generated/filler
Normal file
@@ -0,0 +1 @@
|
|||||||
|
v tehle složce se vygeneruje schema
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
import Client from "./Client";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Loads enum values from an OpenAPI schema for a given path, method, and field (e.g., category).
|
|
||||||
*
|
|
||||||
* @param path - API path, e.g., "/api/service-tickets/"
|
|
||||||
* @param method - HTTP method
|
|
||||||
* @param field - field name in parameters or request
|
|
||||||
* @param schemaUrl - URL of the JSON schema, default "/api/schema/?format=json"
|
|
||||||
* @returns Promise<Array<{ value: string; label: string }>>
|
|
||||||
*/
|
|
||||||
export async function fetchEnumFromSchemaJson(
|
|
||||||
path: string,
|
|
||||||
method: "get" | "post" | "patch" | "put",
|
|
||||||
field: string,
|
|
||||||
schemaUrl: string = "/schema/?format=json"
|
|
||||||
): Promise<Array<{ value: string; label: string }>> {
|
|
||||||
try {
|
|
||||||
const schema = await Client.public.get(schemaUrl);
|
|
||||||
|
|
||||||
const methodDef = schema.paths?.[path]?.[method];
|
|
||||||
if (!methodDef) {
|
|
||||||
throw new Error(`Method ${method.toUpperCase()} for ${path} not found in schema.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Search in "parameters" (e.g., GET query parameters)
|
|
||||||
const param = methodDef.parameters?.find((p: any) => p.name === field);
|
|
||||||
|
|
||||||
if (param?.schema?.enum) {
|
|
||||||
return param.schema.enum.map((val: string) => ({
|
|
||||||
value: val,
|
|
||||||
label: val,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error(`Field '${field}' does not contain enum`);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error loading enum values:", error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import Client from "../Client";
|
import Client from "./Client";
|
||||||
|
|
||||||
// Available output containers (must match backend)
|
// Available output containers (must match backend)
|
||||||
export const FORMAT_EXTS = ["mp4", "mkv", "webm", "flv", "mov", "avi", "ogg"] as const;
|
export const FORMAT_EXTS = ["mp4", "mkv", "webm", "flv", "mov", "avi", "ogg"] as const;
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
// User API model for searching users by username
|
// User API model for searching users by username
|
||||||
// Structure matches other model files (see order.js for reference)
|
// Structure matches other model files (see order.js for reference)
|
||||||
|
|
||||||
import Client from '../Client';
|
import Client from '../legacy/Client';
|
||||||
|
|
||||||
const API_BASE_URL = "/account/users";
|
const API_BASE_URL = "/account/users";
|
||||||
|
|
||||||
27
frontend/src/api/privateClient.ts
Normal file
27
frontend/src/api/privateClient.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
// použij tohle pro API vyžadující autentizaci
|
||||||
|
export const privateApi = axios.create({
|
||||||
|
baseURL: "/api/",
|
||||||
|
withCredentials: true, // potřebuje HttpOnly cookies
|
||||||
|
});
|
||||||
|
|
||||||
|
privateApi.interceptors.response.use(
|
||||||
|
(res) => res,
|
||||||
|
async (error) => {
|
||||||
|
const original = error.config;
|
||||||
|
|
||||||
|
if (error.response?.status === 401 && !original._retry) {
|
||||||
|
original._retry = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await privateApi.post("/auth/refresh/");
|
||||||
|
return privateApi(original);
|
||||||
|
} catch {
|
||||||
|
// optional: logout
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
);
|
||||||
7
frontend/src/api/publicClient.ts
Normal file
7
frontend/src/api/publicClient.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
// použij tohle pro veřejné API nevyžadující autentizaci
|
||||||
|
export const publicApi = axios.create({
|
||||||
|
baseURL: "/api/",
|
||||||
|
withCredentials: false, // veřejné API NEPOSÍLÁ cookies
|
||||||
|
});
|
||||||
@@ -5,7 +5,7 @@ import {
|
|||||||
FORMAT_EXTS,
|
FORMAT_EXTS,
|
||||||
type InfoResponse,
|
type InfoResponse,
|
||||||
parseContentDispositionFilename,
|
parseContentDispositionFilename,
|
||||||
} from "../../api/apps/Downloader";
|
} from "../../api/legacy/Downloader";
|
||||||
|
|
||||||
export default function Downloader() {
|
export default function Downloader() {
|
||||||
const [url, setUrl] = useState("");
|
const [url, setUrl] = useState("");
|
||||||
|
|||||||
Reference in New Issue
Block a user