Compare commits

..

3 Commits

Author SHA1 Message Date
d0f36e0bbd init 2025-10-06 12:46:23 +02:00
b802011d7f Update footer.html 2025-10-06 11:01:22 +02:00
ed20c841ab reverted to old web configuration on main branch 2025-10-06 10:56:21 +02:00
606 changed files with 354347 additions and 19276 deletions

View File

@@ -1,119 +0,0 @@
# Copilot Instructions for Vontor CZ
## Overview
This monorepo contains a Django backend and a Vite/React frontend, orchestrated via Docker Compose. The project is designed for a Czech e-marketplace, with custom payment integrations and real-time features.
## Architecture
- **backend/**: Django project (`vontor_cz`), custom `account` app, and third-party payment integrations (`thirdparty/`).
- Uses Django REST Framework, Channels (ASGI), Celery, and S3/static/media via `django-storages`.
- Custom user model: `account.CustomUser`.
- API docs: DRF Spectacular (`/api/schema/`).
- **frontend/**: Vite + React + TypeScript app.
- Organized by `src/api/`, `components/`, `features/`, `layouts/`, `pages/`, `routes/`.
- Uses React Router layouts and nested routes (see `src/layouts/`, `src/routes/`).
- Uses Tailwind CSS for styling (configured via `src/index.css` with `@import "tailwindcss";`). Prefer utility classes over custom CSS.
## Developer Workflows
- **Backend**
- Local dev: `python manage.py runserver` (or use Docker Compose)
- Migrations: `python manage.py makemigrations && python manage.py migrate`
- Celery: `celery -A vontor_cz worker -l info`
- Channels: Daphne/ASGI (see Docker Compose command)
- Env config: `.env` files in `backend/` (see `.gitignore` for secrets)
- **Frontend**
- Install: `npm install`
- Dev server: `npm run dev`
- Build: `npm run build`
- Preview: `npm run preview`
- Static assets: `src/assets/` (import in JS/CSS), `public/` (referenced in HTML)
## Conventions & Patterns
- **Backend**
- Use environment variables for secrets and config (see `settings.py`).
- Static/media files: S3 in production, local in dev (see `settings.py`).
- API versioning and docs: DRF Spectacular config in `settings.py`.
- Custom permissions, filters, and serializers in each app.
- **Frontend**
- Use React Router layouts for shared UI (see `src/layouts/`, `LAYOUTS.md`).
- API calls and JWT handling in `src/api/`.
- Route definitions and guards in `src/routes/` (`ROUTES.md`).
- Use TypeScript strict mode (see `tsconfig.*.json`).
- Linting: ESLint config in `eslint.config.js`.
- Styling: Tailwind CSS is present. Prefer utility classes; keep minimal component-scoped CSS. Global/base styles live in `src/index.css`. Avoid inline styles and CSS-in-JS unless necessary.
### Frontend API Client (required)
All frontend API calls must use the shared client at frontend/src/api/Client.ts.
- Client.public: no cookies, no Authorization header (for public Django endpoints).
- Client.auth: sends cookies and includes Bearer token; auto-refreshes on 401 (retries up to 2x).
- Centralized error handling: subscribe via Client.onError to show toasts/snackbars.
- Tokens are stored in cookies by Client.setTokens and cleared by Client.clearTokens.
Example usage (TypeScript)
```ts
import Client from "@/api/Client";
// Public request (no credentials)
async function listPublicItems() {
const res = await Client.public.get("/api/public/items/");
return res.data;
}
// Login (obtain tokens and persist to cookies)
async function login(username: string, password: string) {
// Default SimpleJWT endpoint (adjust if your backend differs)
const res = await Client.public.post("/api/token/", { username, password });
const { access, refresh } = res.data;
Client.setTokens(access, refresh);
}
// Authenticated requests (auto Bearer + refresh on 401)
async function fetchProfile() {
const res = await Client.auth.get("/api/users/me/");
return res.data;
}
function logout() {
Client.clearTokens();
window.location.assign("/login");
}
// Global error toasts
import { useEffect } from "react";
function useApiErrors(showToast: (msg: string) => void) {
useEffect(() => {
const unsubscribe = Client.onError((e) => {
const { message, status } = e.detail;
showToast(status ? `${status}: ${message}` : message);
});
return unsubscribe;
}, [showToast]);
}
```
Vite env used by the client:
- VITE_API_BASE_URL (default: http://localhost:8000)
- VITE_API_REFRESH_URL (default: /api/token/refresh/)
- VITE_LOGIN_PATH (default: /login)
Notes
- Public client never sends cookies or Authorization.
- Ensure Django CORS settings allow your frontend origin. See backend/vontor_cz/settings.py.
- Use React Router layouts and guards as documented in frontend/src/routes/ROUTES.md and frontend/src/layouts/LAYOUTS.md.
## Integration Points
- **Payments**: `thirdparty/` contains custom integrations for Stripe, GoPay, Trading212.
- **Real-time**: Django Channels (ASGI, Redis) for websockets.
- **Task queue**: Celery + Redis for async/background jobs.
- **API**: REST endpoints, JWT auth, API key support.
## References
- [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/routes/ROUTES.md](../frontend/src/routes/ROUTES.md): Routing conventions.
- [backend/vontor_cz/settings.py](../backend/vontor_cz/settings.py): All backend config, env, and integration details.
- [docker-compose.yml](../docker-compose.yml): Service orchestration and dev workflow.
---
**When in doubt, check the referenced markdown files and `settings.py` for project-specific logic and patterns.**

165
.gitignore copy Normal file
View File

@@ -0,0 +1,165 @@
#
*.mp4
backups/
collectedstaticfiles/
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
.pdm.toml
.pdm-python
.pdm-build/
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/

15
.vscode/launch.json vendored
View File

@@ -1,15 +0,0 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "chrome",
"request": "launch",
"name": "Launch Chrome against localhost",
"url": "http://localhost:8080",
"webRoot": "${workspaceFolder}"
}
]
}

View File

@@ -0,0 +1,23 @@
# vontor-cz
## venv
- windows
```
Set-ExecutionPolicy -Scope Process -ExecutionPolicy RemoteSigned
python -m venv venv
.\venv\Scripts\Activate
#start server
daphne -b localhost -p 8000 vontor_cz.asgi:application
```
## docker compose
spuštění dockeru pro lokální hosting, s instantníma změnami během editace ve vscodu.
```docker-compose up --build```
## dns reset windows
```ipconfig /flushdns```

37
X-Notes/admin(example).py Normal file
View File

@@ -0,0 +1,37 @@
from django.contrib import admin
from .models import MyModel # Importuj modely
# Příklad přizpůsobení zobrazení modelu v administrátorské sekci
class MyModelAdmin(admin.ModelAdmin):
# Určují se pole, která se zobrazí v seznamu (list view)
list_display = ('field1', 'field2', 'field3')
# Určuje, podle kterých polí lze vyhledávat
search_fields = ('field1', 'field2')
# Aktivuje filtrování podle hodnoty pole v pravém postranním panelu
list_filter = ('field1', 'field2')
# Určuje pole, která se zobrazí ve formuláři při detailním pohledu na model
fields = ('field1', 'field2', 'field3')
# Definuje rozložení polí ve formuláři
fieldsets = (
(None, {
'fields': ('field1', 'field2'),
}),
('Další informace', {
'classes': ('collapse',),
'fields': ('field3',),
}),
)
# Nastavuje výchozí řazení záznamů při jejich zobrazení
ordering = ('field1',)
# Určuje počet záznamů zobrazených na jedné stránce
list_per_page = 10
# Definuje akce dostupné pro vybrané objekty
actions = ['custom_action']
# Příklad vlastní akce
def custom_action(self, request, queryset):
# Vlastní logika pro akci
queryset.update(field1='Updated Value')
# Registrování modelu s vlastními nastaveními administrátorského rozhraní
admin.site.register(MyModel, MyModelAdmin)

View File

@@ -1,24 +0,0 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View File

@@ -1,11 +0,0 @@
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 5173
CMD ["npm", "run", "dev"]

View File

@@ -1,113 +0,0 @@
# Vontor CZ
Welcome to **Vontor CZ**!
## frontend Folder Overview
The `frontend` folder contains all code and assets for the client-side React application. Its structure typically includes:
- **src/**
Tady se ukladají věci, které uživateli se nepředavají přímo spíš takové stavební kostky.
- **api/**
TypeScript/JS soubory které se starají o API a o JWT tokeny.
Čistě pracují s backendem
- **context/**
Kontext si načte data které mu předáš a můžeš si je předávat mezi komponenty rychleji.
- **hooks/**
Pracuje s API a formátují to do výstupu který potřebujeme.
- **components/**
Konktrétní komponenty které se vykreslují na stránce.
Už využívají už hooky a contexty pro vykreslení informaci bez složite logiky (ať je komponenta hezky čistá a né moc obsáhla).
- **features/**
Nejsou to celé stránky, ale hotové komponenty.
Obsahuje všechny komponenty plus její hooky, API, state a utils potřebné pro jednu konkrétní funkcionalitu aplikace. (použijí se jenom jednou)
Features zajišťují modularitu a přehlednost aplikace.
Příklad: komponenta košík, která zahrnuje API volání, state management a UI komponenty.
---
- **layouts/**
Tohle je jenom komponenta, která načte další komponenty, ale používá se jako layout kde jsou například navigace footer a ostatní se opakující prvky stránky, ale hlavní obsah ještě není součastí! Ten se načte skrz <outlet/> v pages.
- **pages/**
Tady se jde do finále tady se vkládají samostatné komponenty a tvoří se už hlavní obsah stránky a vkládají se komponenty a tvoří se logika mezi ně.
- **routes/**
tady se ukládají routy které například zabraňuji načtení stránky nepříhlášeným uživatelům nebo jenom pro ty s určitou roli/oprávněním... tyhle route komponenty se pak využívají v
---
- **assets/**
Obrázky, fonty, a ostatní statické soubory.
---
- **App.tsx**
Root komponenta pro aplikaci, kde se nastavují routy pro jednotlivé stránky.
Pozor nemyslím ty routy ze složky routes/ ... to jsou jenom obaly pro konktrétní routy pro jednotlivé stránky.
- **main.tsx**
Vstupní bod pro načítaní aplikace (načíta se první komponenta App.jsx)
---
- **utils/**
Sběrné místo pro pomocné funkce, které nejsou přímo komponenty nebo hooky, ale jsou znovupoužitelné napříč aplikací.
---
- **index.css**
globální styly
- **App.css**
obsahuje stylovaní layoutu, navigace, footer, error okna atd. takové věci které zůstavjí vždy stejně
- **public/**
Složka public obsahuje statické soubory dostupné přímo přes URL (např. index.html, favicon, obrázky), které React přímo nereenderuje ani neoptimalizuje.
- **package.json**
něco jak requirements.txt v pythonu
- **vite.config.js / vite.config.ts**
Vite konfigurace pro building a serving frontend aplikace.
- **Dockerfile**
Konfigurace pro Docker
## Getting Started :3
1. **Instalace balíčku (bere z package.json):**
```sh
npm install
```
3. **Start dev server:**
```sh
npm run dev
```
4. **Build pro produkci(finále):**
```sh
npm run build
```
5. **Preview production build:**
```sh
npm run preview
```
## Creating a New React Project with Vite
If you want to start a new project:
```sh
npm create vite@latest
# Choose 'react' or 'react-ts' for TypeScript
cd your-project
npm install
npm run dev
```

View File

@@ -1,23 +0,0 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
import { globalIgnores } from 'eslint/config'
export default tseslint.config([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
js.configs.recommended,
tseslint.configs.recommended,
reactHooks.configs['recommended-latest'],
reactRefresh.configs.vite,
],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
},
])

View File

@@ -1,14 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<link rel="stylesheet" href="reset.css">
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@@ -1,36 +0,0 @@
{
"name": "frontend",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"@tailwindcss/vite": "^4.1.16",
"@types/react-router": "^5.1.20",
"axios": "^1.13.0",
"react": "^19.1.1",
"react-dom": "^19.1.1",
"react-icons": "^5.5.0",
"react-router-dom": "^7.8.1",
"tailwindcss": "^4.1.16"
},
"devDependencies": {
"@eslint/js": "^9.33.0",
"@types/axios": "^0.9.36",
"@types/react": "^19.1.10",
"@types/react-dom": "^19.1.7",
"@vitejs/plugin-react": "^5.0.0",
"eslint": "^9.33.0",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.20",
"globals": "^16.3.0",
"typescript": "~5.8.3",
"typescript-eslint": "^8.39.1",
"vite": "^7.1.2"
}
}

View File

@@ -1,4 +0,0 @@
# Anything you import in JS/CSS → src/assets/
# Anything you reference directly in HTML → public/ something like: <img src="/images/foo.png">

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

View File

@@ -1,42 +0,0 @@
#root {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}
.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
transition: filter 300ms;
}
.logo:hover {
filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.react:hover {
filter: drop-shadow(0 0 2em #61dafbaa);
}
@keyframes logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
@media (prefers-reduced-motion: no-preference) {
a:nth-of-type(2) .logo {
animation: logo-spin infinite 20s linear;
}
}
.card {
padding: 2em;
}
.read-the-docs {
color: #888;
}

View File

@@ -1,33 +0,0 @@
import { BrowserRouter as Router, Routes, Route, Link, Outlet } from "react-router-dom"
import Home from "./pages/home/home";
import HomeLayout from "./layouts/HomeLayout";
import Downloader from "./pages/downloader/Downloader";
import PrivateRoute from "./routes/PrivateRoute";
import { UserContextProvider } from "./context/UserContext";
export default function App() {
return (
<Router>
<UserContextProvider>
{/* Layout route */}
<Route path="/" element={<HomeLayout />}>
<Route index element={<Home />} />
<Route path="downloader" element={<Downloader />} />
</Route>
<Route element={<PrivateRoute />}>
{/* Protected routes go here */}
<Route path="/" element={<HomeLayout />} >
<Route path="protected-downloader" element={<Downloader />} />
</Route>
</Route>
</UserContextProvider>
</Router>
)
}

View File

@@ -1,275 +0,0 @@
import axios from "axios";
// --- ENV CONFIG ---
const API_BASE_URL =
import.meta.env.VITE_API_BASE_URL || "http://localhost:8000";
const LOGIN_PATH = import.meta.env.VITE_LOGIN_PATH || "/login";
// --- ERROR EVENT BUS ---
const ERROR_EVENT = "api:error";
type ApiErrorDetail = {
message: string;
status?: number;
url?: string;
data?: unknown;
};
// Use interface instead of arrow function types for readability
interface ApiErrorHandler {
(e: CustomEvent<ApiErrorDetail>): void;
}
function notifyError(detail: ApiErrorDetail) {
window.dispatchEvent(new CustomEvent<ApiErrorDetail>(ERROR_EVENT, { detail }));
// eslint-disable-next-line no-console
console.error("[API ERROR]", detail);
}
function onError(handler: ApiErrorHandler) {
const wrapped = handler as EventListener;
window.addEventListener(ERROR_EVENT, wrapped as EventListener);
return () => window.removeEventListener(ERROR_EVENT, wrapped);
}
// --- AXIOS INSTANCES ---
// Always send cookies. Django will set auth cookies; browser will include them automatically.
function createAxios(baseURL: string): any {
const instance = axios.create({
baseURL,
withCredentials: true, // cookies
headers: {
"Content-Type": "application/json",
},
timeout: 20000,
});
return instance;
}
// Use a single behavior for both: cookies are always sent
const apiPublic = createAxios(API_BASE_URL);
const apiAuth = createAxios(API_BASE_URL);
// --- REQUEST INTERCEPTOR (PUBLIC) ---
// Ensure no Authorization header is ever sent by the public client
apiPublic.interceptors.request.use(function (config: any) {
if (config?.headers && (config.headers as any).Authorization) {
delete (config.headers as any).Authorization;
}
return config;
});
// --- REQUEST INTERCEPTOR (AUTH) ---
// Do not attach Authorization header; rely on cookies set by Django.
apiAuth.interceptors.request.use(function (config: any) {
(config as any)._retryCount = (config as any)._retryCount || 0;
return config;
});
// --- RESPONSE INTERCEPTOR (AUTH) ---
// Simplified: on 401, redirect to login. Server manages refresh via cookies.
apiAuth.interceptors.response.use(
function (response: any) {
return response;
},
async function (error: any) {
if (!error.response) {
alert("Backend connection is unavailable. Please check your network.");
notifyError({
message: "Network error or backend unavailable",
url: error.config?.url,
});
return Promise.reject(error);
}
const status = error.response.status;
if (status === 401) {
ClearTokens();
window.location.assign(LOGIN_PATH);
return Promise.reject(error);
}
notifyError({
message:
(error.response.data as any)?.detail ||
(error.response.data as any)?.message ||
`Request failed with status ${status}`,
status,
url: error.config?.url,
data: error.response.data,
});
return Promise.reject(error);
}
);
// --- PUBLIC CLIENT: still emits errors and alerts on network failure ---
apiPublic.interceptors.response.use(
function (response: any) {
return response;
},
async function (error: any) {
if (!error.response) {
alert("Backend connection is unavailable. Please check your network.");
notifyError({
message: "Network error or backend unavailable",
url: error.config?.url,
});
return Promise.reject(error);
}
notifyError({
message:
(error.response.data as any)?.detail ||
(error.response.data as any)?.message ||
`Request failed with status ${error.response.status}`,
status: error.response.status,
url: error.config?.url,
data: error.response.data,
});
return Promise.reject(error);
}
);
function Logout() {
try {
const LogOutResponse = apiAuth.post("/api/logout/");
if (LogOutResponse.body.detail != "Logout successful") {
throw new Error("Logout failed");
}
ClearTokens();
} catch (error) {
console.error("Error during logout:", error);
}
}
function ClearTokens(){
document.cookie = "access_token=; Max-Age=0; path=/";
document.cookie = "refresh_token=; Max-Age=0; path=/";
}
// --- EXPORT DEFAULT API WRAPPER ---
const Client = {
// Axios instances
auth: apiAuth,
public: apiPublic,
Logout,
// Error subscription
onError,
};
export default Client;
/**
USAGE EXAMPLES (TypeScript/React)
Import the client
--------------------------------------------------
import Client from "@/api/Client";
Login: obtain tokens and persist to cookies
--------------------------------------------------
async function login(username: string, password: string) {
// SimpleJWT default login endpoint (adjust if your backend differs)
// Example backend endpoint: POST /api/token/ -> { access, refresh }
const res = await Client.public.post("/api/token/", { username, password });
const { access, refresh } = res.data;
Client.setTokens(access, refresh);
// After this, Client.auth will automatically attach Authorization header
// and refresh when receiving a 401 (up to 2 retries).
}
Public request (no cookies, no Authorization)
--------------------------------------------------
// The public client does NOT send cookies or Authorization.
async function listPublicItems() {
const res = await Client.public.get("/api/public/items/");
return res.data;
}
Authenticated requests (auto Bearer header + refresh on 401)
--------------------------------------------------
async function fetchProfile() {
const res = await Client.auth.get("/api/users/me/");
return res.data;
}
async function updateProfile(payload: { first_name?: string; last_name?: string }) {
const res = await Client.auth.patch("/api/users/me/", payload);
return res.data;
}
Global error handling (UI notifications)
--------------------------------------------------
import { useEffect } from "react";
function useApiErrors(showToast: (msg: string) => void) {
useEffect(function () {
const unsubscribe = Client.onError(function (e) {
const { message, status } = e.detail;
showToast(status ? String(status) + ": " + message : message);
});
return unsubscribe;
}, [showToast]);
}
// Note: Network connectivity issues trigger an alert and also dispatch api:error.
// All errors are logged to console for developers.
Logout
--------------------------------------------------
function logout() {
Client.clearTokens();
window.location.assign("/login");
}
Route protection (PrivateRoute)
--------------------------------------------------
// If you created src/routes/PrivateRoute.tsx, wrap your protected routes with it.
// PrivateRoute checks for "access_token" cookie presence and redirects to /login if missing.
// Example:
// <Routes>
// <Route element={<PrivateRoute />} >
// <Route element={<MainLayout />}>
// <Route path="/" element={<Dashboard />} />
// <Route path="/profile" element={<Profile />} />
// </Route>
// </Route>
// <Route path="/login" element={<Login />} />
// </Routes>
Refresh and retry flow (what happens on 401)
--------------------------------------------------
// 1) Client.auth request receives 401 from backend
// 2) Client tries to refresh access token using refresh_token cookie
// 3) If refresh succeeds, original request is retried (max 2 times)
// 4) If still 401 (or no refresh token), tokens are cleared and user is redirected to /login
Environment variables (optional overrides)
--------------------------------------------------
// VITE_API_BASE_URL default: "http://localhost:8000"
// VITE_API_REFRESH_URL default: "/api/token/refresh/"
// VITE_LOGIN_PATH default: "/login"
*/

View File

@@ -1,114 +0,0 @@
import Client from "../Client";
// Available output containers (must match backend)
export const FORMAT_EXTS = ["mp4", "mkv", "webm", "flv", "mov", "avi", "ogg"] as const;
export type FormatExt = (typeof FORMAT_EXTS)[number];
export type InfoResponse = {
title: string | null;
duration: number | null;
thumbnail: string | null;
video_resolutions: string[]; // e.g. ["2160p", "1440p", "1080p", ...]
audio_resolutions: string[]; // e.g. ["320kbps", "160kbps", ...]
};
// GET info for a URL
export async function fetchInfo(url: string): Promise<InfoResponse> {
const res = await Client.public.get("/api/downloader/download/", {
params: { url },
});
return res.data as InfoResponse;
}
// POST to stream binary immediately; returns { blob, filename }
export async function downloadImmediate(args: {
url: string;
ext: FormatExt;
videoResolution?: string | number; // "1080p" or 1080
audioResolution?: string | number; // "160kbps" or 160
}): Promise<{ blob: Blob; filename: string }> {
const video_quality = toHeight(args.videoResolution);
const audio_quality = toKbps(args.audioResolution);
if (video_quality == null || audio_quality == null) {
throw new Error("Please select both video and audio quality.");
}
const res = await Client.public.post(
"/api/downloader/download/",
{
url: args.url,
ext: args.ext,
video_quality,
audio_quality,
},
{ responseType: "blob" }
);
const cd = res.headers?.["content-disposition"] as string | undefined;
const xfn = res.headers?.["x-filename"] as string | undefined;
const filename =
parseContentDispositionFilename(cd) ||
(xfn && xfn.trim()) ||
inferFilenameFromUrl(args.url, res.headers?.["content-type"] as string | undefined) ||
`download.${args.ext}`;
return { blob: res.data as Blob, filename };
}
// Helpers
export function parseContentDispositionFilename(cd?: string): string | null {
if (!cd) return null;
const utf8Match = cd.match(/filename\*\s*=\s*UTF-8''([^;]+)/i);
if (utf8Match?.[1]) return decodeURIComponent(utf8Match[1]);
const plainMatch = cd.match(/filename\s*=\s*"([^"]+)"/i) || cd.match(/filename\s*=\s*([^;]+)/i);
return plainMatch?.[1]?.trim() || null;
}
function inferFilenameFromUrl(url: string, contentType?: string): string {
try {
const u = new URL(url);
const last = u.pathname.split("/").filter(Boolean).pop();
if (last) return last;
} catch {
// ignore
}
if (contentType) {
const ext = contentTypeToExt(contentType);
return `download${ext ? `.${ext}` : ""}`;
}
return "download.bin";
}
function contentTypeToExt(ct?: string): string | null {
if (!ct) return null;
const map: Record<string, string> = {
"video/mp4": "mp4",
"video/x-matroska": "mkv",
"video/webm": "webm",
"video/x-flv": "flv",
"video/quicktime": "mov",
"video/x-msvideo": "avi",
"video/ogg": "ogg",
"application/octet-stream": "bin",
};
return map[ct] || null;
}
function toHeight(v?: string | number): number | undefined {
if (typeof v === "number") return v || undefined;
if (!v) return undefined;
const m = /^(\d{2,4})p$/i.exec(v.trim());
if (m) return parseInt(m[1], 10);
const n = Number(v);
return Number.isFinite(n) ? (n as number) : undefined;
}
function toKbps(v?: string | number): number | undefined {
if (typeof v === "number") return v || undefined;
if (!v) return undefined;
const m = /^(\d{2,4})\s*kbps$/i.exec(v.trim());
if (m) return parseInt(m[1], 10);
const n = Number(v);
return Number.isFinite(n) ? (n as number) : undefined;
}

View File

@@ -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;
}
}

View File

@@ -1,82 +0,0 @@
// frontend/src/api/model/user.js
// User API model for searching users by username
// Structure matches other model files (see order.js for reference)
import Client from '../Client';
const API_BASE_URL = "/account/users";
const userAPI = {
/**
* Get current authenticated user
* @returns {Promise<User>}
*/
async getCurrentUser() {
const response = await Client.auth.get(`${API_BASE_URL}/me/`);
return response.data;
},
/**
* Get all users
* @returns {Promise<Array<User>>}
*/
async getUsers(params: Object) {
const response = await Client.auth.get(`${API_BASE_URL}/`, { params });
return response.data;
},
/**
* Get a single user by ID
* @param {number|string} id
* @returns {Promise<User>}
*/
async getUser(id: number) {
const response = await Client.auth.get(`${API_BASE_URL}/${id}/`);
return response.data;
},
/**
* Update a user by ID
* @param {number|string} id
* @param {Object} data
* @returns {Promise<User>}
*/
async updateUser(id: number, data: Object) {
const response = await Client.auth.patch(`${API_BASE_URL}/${id}/`, data);
return response.data;
},
/**
* Delete a user by ID
* @param {number|string} id
* @returns {Promise<void>}
*/
async deleteUser(id: number) {
const response = await Client.auth.delete(`${API_BASE_URL}/${id}/`);
return response.data;
},
/**
* Create a new user
* @param {Object} data
* @returns {Promise<User>}
*/
async createUser(data: Object) {
const response = await Client.auth.post(`${API_BASE_URL}/`, data);
return response.data;
},
/**
* Search users by username (partial match)
* @param {Object} params - { username: string }
* @returns {Promise<Array<User>>}
*/
async searchUsers(params: { username: string }) {
// Adjust the endpoint as needed for your backend
const response = await Client.auth.get(`${API_BASE_URL}/`, { params });
console.log("User search response:", response.data);
return response.data;
},
};
export default userAPI;

View File

@@ -1,19 +0,0 @@
const wsUri = "ws://127.0.0.1/";
const websocket = new WebSocket(wsUri);
websocket.onopen = function (event) {
console.log("WebSocket is open now.", event);
};
websocket.onmessage = function (event) {
console.log("WebSocket message received:", event.data);
};
websocket.onclose = function (event) {
console.log("WebSocket is closed now.", event.reason);
};
websocket.onerror = function (event) {
console.error("WebSocket error observed:", event);
};

View File

@@ -1,40 +0,0 @@
footer a{
color: var(--c-text);
text-decoration: none;
}
footer a i{
color: white;
text-decoration: none;
}
footer a:hover i{
color: var(--c-text);
text-decoration: none;
}
footer{
font-family: "Roboto Mono", monospace;
background-color: var(--c-boxes);
margin-top: 2em;
display: flex;
color: white;
align-items: center;
justify-content: space-evenly;
}
footer address{
padding: 1em;
font-style: normal;
}
footer .contacts{
font-size: 2em;
}
@media only screen and (max-width: 990px){
footer{
flex-direction: column;
padding-bottom: 1em;
padding-top: 1em;
}
}

View File

@@ -1,76 +0,0 @@
import styles from "./footer.module.css"
export default function Footer() {
return (
<footer id="contacts">
<div>
<h1>vontor.cz</h1>
</div>
<address>
Written by <b>David Bruno Vontor | © 2025</b>
<br />
<p>
Tel.:{" "}
<a href="tel:+420605512624">
<u>+420 605 512 624</u>
</a>
</p>
<p>
E-mail:{" "}
<a href="mailto:brunovontor@gmail.com">
<u>brunovontor@gmail.com</u>
</a>
</p>
<p>
IČO:{" "}
<a
href="https://www.rzp.cz/verejne-udaje/cs/udaje/vyber-subjektu;ico=21613109;"
target="_blank"
rel="noopener noreferrer"
>
<u>21613109</u>
</a>
</p>
</address>
<div className="contacts">
<a
href="https://github.com/Brunobrno"
target="_blank"
rel="noopener noreferrer"
>
<i className="fa fa-github"></i>
</a>
<a
href="https://www.instagram.com/brunovontor/"
target="_blank"
rel="noopener noreferrer"
>
<i className="fa fa-instagram"></i>
</a>
<a
href="https://twitter.com/BVontor"
target="_blank"
rel="noopener noreferrer"
>
<i className="fa-brands fa-x-twitter"></i>
</a>
<a
href="https://steamcommunity.com/id/Brunobrno/"
target="_blank"
rel="noopener noreferrer"
>
<i className="fa-brands fa-steam"></i>
</a>
<a
href="https://www.youtube.com/@brunovontor"
target="_blank"
rel="noopener noreferrer"
>
<i className="fa-brands fa-youtube"></i>
</a>
</div>
</footer>
)
}

View File

@@ -1,85 +0,0 @@
import React, { useState, useRef } from "react"
import styles from "./contact-me.module.css"
import { LuMousePointerClick } from "react-icons/lu";
export default function ContactMeForm() {
const [opened, setOpened] = useState(false)
const [contentMoveUp, setContentMoveUp] = useState(false)
const [openingBehind, setOpeningBehind] = useState(false)
const [success, setSuccess] = useState(false)
const openingRef = useRef<HTMLDivElement>(null)
function handleSubmit() {
// form submission logic here
}
const toggleOpen = () => {
if (!opened) {
setOpened(true)
setOpeningBehind(false)
setContentMoveUp(false)
// Wait for the rotate-opening animation to finish before moving content up
// The actual moveUp will be handled in onTransitionEnd
} else {
setContentMoveUp(false)
setOpeningBehind(false)
setTimeout(() => setOpened(false), 1000) // match transition duration
}
}
const handleTransitionEnd = (e: React.TransitionEvent<HTMLDivElement>) => {
if (opened && e.propertyName === "transform") {
setContentMoveUp(true)
// Move the opening behind after the animation
setTimeout(() => setOpeningBehind(true), 10)
}
if (!opened && e.propertyName === "transform") {
setOpeningBehind(false)
}
}
return (
<div className={styles["contact-me"]}>
<div
ref={openingRef}
className={
[
styles.opening,
opened ? styles["rotate-opening"] : "",
openingBehind ? styles["opening-behind"] : ""
].filter(Boolean).join(" ")
}
onClick={toggleOpen}
onTransitionEnd={handleTransitionEnd}
>
<LuMousePointerClick/>
</div>
<div
className={
contentMoveUp
? `${styles.content} ${styles["content-moveup"]}`
: styles.content
}
>
<form onSubmit={handleSubmit}>
<input
type="email"
name="email"
placeholder="Váš email"
required
/>
<textarea
name="message"
placeholder="Vaše zpráva"
required
/>
<input type="submit"/>
</form>
</div>
<div className={styles.cover}></div>
<div className={styles.triangle}></div>
</div>
)
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

View File

@@ -1,90 +0,0 @@
import React, { useEffect, useRef } from "react"
import styles from "./drone.module.css"
export default function Drone() {
const videoRef = useRef<HTMLVideoElement | null>(null)
const sourceRef = useRef<HTMLSourceElement | null>(null)
useEffect(() => {
function setVideoDroneQuality() {
if (!sourceRef.current || !videoRef.current) return
const videoSources = {
fullHD: "static/home/video/drone-background-video-1080p.mp4", // For desktops (1920x1080)
hd: "static/home/video/drone-background-video-720p.mp4", // For tablets/smaller screens (1280x720)
lowRes: "static/home/video/drone-background-video-480p.mp4" // For mobile devices or low performance (854x480)
}
const screenWidth = window.innerWidth
// Pick appropriate source
if (screenWidth >= 1920) {
sourceRef.current.src =
"https://vontor-cz.s3.eu-central-1.amazonaws.com/" + videoSources.fullHD
} else if (screenWidth >= 1280) {
sourceRef.current.src =
"https://vontor-cz.s3.eu-central-1.amazonaws.com/" + videoSources.hd
} else {
sourceRef.current.src =
"https://vontor-cz.s3.eu-central-1.amazonaws.com/" + videoSources.lowRes
}
// Reload video
videoRef.current.load()
console.log("Drone video set!")
}
// Run once on mount
setVideoDroneQuality()
// Optional: rerun on resize
window.addEventListener("resize", setVideoDroneQuality)
return () => {
window.removeEventListener("resize", setVideoDroneQuality)
}
}, [])
return (
<div className={`${styles.drone}`}>
<video
ref={videoRef}
id="drone-video"
className={styles.videoBackground}
autoPlay
muted
loop
playsInline
>
<source ref={sourceRef} id="video-source" type="video/mp4" />
Your browser does not support video.
</video>
<article>
<header>
<h1>Letecké záběry, co zaujmou</h1>
</header>
<main>
<section>
<h2>Oprávnění</h2>
<p>Oprávnění A1/A2/A3 + radiostanice. Bezpečný provoz i v omezených zónách, povolení zajistím.</p>
</section>
<section>
<h2>Cena</h2>
<p>Paušál 3000. Ostrava zdarma; mimo 10/km. Cena se může lišit dle povolení.</p>
</section>
<section>
<h2>Výstup</h2>
<p>Krátký sestřih nebo surové záběry podle potřeby.</p>
</section>
</main>
<div>
<a href="#contacts">Zájem?</a>
</div>
</article>
</div>
)
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

View File

@@ -1,86 +0,0 @@
import React, { useState } from "react"
import styles from "./Portfolio.module.css"
import { LuMousePointerClick } from "react-icons/lu";
interface PortfolioItem {
href: string
src: string
alt: string
// Optional per-item styling (prefer Tailwind utility classes in className/imgClassName)
className?: string
imgClassName?: string
style?: React.CSSProperties
imgStyle?: React.CSSProperties
}
const portfolioItems: PortfolioItem[] = [
{
href: "https://davo1.cz",
src: "/portfolio/davo1.png",
alt: "davo1.cz logo",
imgClassName: "bg-black rounded-lg p-4",
//className: "bg-white/5 rounded-lg p-4",
},
{
href: "https://perlica.cz",
src: "/portfolio/perlica.png",
alt: "Perlica logo",
imgClassName: "rounded-lg",
// imgClassName: "max-h-12",
},
{
href: "http://epinger2.cz",
src: "/portfolio/epinger.png",
alt: "Epinger2 logo",
imgClassName: "bg-white rounded-lg",
// imgClassName: "max-h-12",
},
]
export default function Portfolio() {
const [doorOpen, setDoorOpen] = useState(false)
const toggleDoor = () => setDoorOpen((prev) => !prev)
return (
<div className={styles.portfolio} id="portfolio">
<header>
<h1>Portfolio</h1>
</header>
<div>
<span
className={
doorOpen
? `${styles.door} ${styles["door-open"]}`
: styles.door
}
onClick={toggleDoor}
>
<LuMousePointerClick/>
</span>
{portfolioItems.map((item, index) => (
<article
key={index}
className={`${styles.article} ${item.className ?? ""}`}
style={item.style}
>
<header>
<a href={item.href} target="_blank" rel="noopener noreferrer">
<img
src={item.src}
alt={item.alt}
className={item.imgClassName}
style={item.imgStyle}
/>
</a>
</header>
<main></main>
</article>
))}
</div>
</div>
)
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

View File

@@ -1,304 +0,0 @@
nav{
padding: 1.1em;
font-family: "Roboto Mono", monospace;
position: -webkit-sticky;
position: sticky;
top: 0; /* required */
transition: top 1s ease-in-out, border-radius 1s ease-in-out;
z-index: 5;
padding-left: 2em;
padding-right: 2em;
width: max-content;
background: var(--c-boxes);
/*background: -moz-linear-gradient(117deg, rgba(34,34,34,1) 0%, rgba(59,54,54,1) 100%);
background: -webkit-linear-gradient(117deg, rgba(34,34,34,1) 0%, rgba(59,54,54,1) 100%);
background: linear-gradient(117deg, rgba(34,34,34,1) 0%, rgba(59,54,54,1) 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr="#222222",endColorstr="#3b3636",GradientType=1);*/
color: #fff;
text-align: center;
margin: auto;
border-radius: 2em;
}
nav.isSticky-nav{
border-top-left-radius: 0;
border-top-right-radius: 0;
}
nav ul #nav-logo{
border-right: 0.2em solid var(--c-lines);
}
/* Add class alias for logo used in TSX */
.logo {
border-right: 0.2em solid var(--c-lines);
}
nav ul #nav-logo span{
line-height: 0.75;
font-size: 1.5em;
}
nav a{
color: #fff;
transition: color 1s;
position: relative;
text-decoration: none;
}
nav a:hover{
color: #fff;
}
/* Unify link/summary layout to prevent distortion */
nav a,
nav summary {
color: #fff;
transition: color 1s;
position: relative;
text-decoration: none;
cursor: pointer;
display: inline-block; /* ensure consistent inline sizing */
vertical-align: middle; /* align with neighbors */
padding: 0; /* keep padding controlled by li */
}
nav a::before {
content: "";
position: absolute;
display: block;
width: 100%;
height: 2px;
bottom: 0;
left: 0;
background-color: #fff;
transform: scaleX(0);
transition: transform 0.3s ease;
}
nav a:hover::before {
transform: scaleX(1);
}
nav summary:hover {
color: #fff;
}
/* underline effect shared for links and summary */
nav a::before,
nav summary::before {
content: "";
position: absolute;
display: block;
width: 100%;
height: 2px;
bottom: 0;
left: 0;
background-color: #fff;
transform: scaleX(0);
transition: transform 0.3s ease;
}
nav a:hover::before,
nav summary:hover::before {
transform: scaleX(1);
}
/* Submenu support */
.hasSubmenu {
position: relative;
vertical-align: middle; /* align with other inline items */
}
/* Keep details inline to avoid breaking the first row flow */
.hasSubmenu details {
display: inline-block;
margin: 0;
padding: 0;
}
/* Ensure "Services" and caret stay on the same line */
.hasSubmenu details > summary {
display: inline-flex; /* horizontal layout */
align-items: center; /* vertical alignment */
gap: 0.5em; /* space between text and icon */
white-space: nowrap; /* prevent wrapping */
}
/* Hide native disclosure icon/marker on summary */
.hasSubmenu details > summary {
list-style: none;
outline: none;
}
.hasSubmenu details > summary::-webkit-details-marker {
display: none;
}
.hasSubmenu details > summary::marker {
content: "";
}
/* Reusable caret for submenu triggers */
.caret {
transition: transform 0.2s ease-in-out;
}
/* Rotate caret when submenu is open */
.hasSubmenu details[open] .caret {
transform: rotate(180deg);
}
/* Submenu box: place directly under nav with a tiny gap (no overlap) */
.submenu {
list-style: none;
margin: 1em 0;
padding: 0.5em 0;
position: absolute;
left: 0;
top: calc(100% + 0.25em);
display: none;
background: var(--c-background-light);
border: 1px solid var(--c-lines);
border-radius: 0.75em;
min-width: max-content;
text-align: left;
z-index: 10;
}
.submenu li {
display: block;
padding: 0;
}
.submenu a {
display: inline-block;
padding: 0; /* remove padding so underline equals text width */
margin: 0.35em 0; /* spacing without affecting underline width */
}
/* Show submenu when open */
.hasSubmenu details[open] .submenu {
display: flex;
flex-direction: column;
}
/* Hamburger toggle class (used by TSX) */
.toggle {
display: none;
transition: transform 0.5s ease;
}
.toggleRotated {
transform: rotate(180deg);
}
/* Bridge TSX classnames to existing rules */
.navList {
list-style: none;
padding: 0;
}
.navList li {
display: inline;
padding: 0 3em;
}
.navList li a {
text-decoration: none;
}
nav ul {
list-style: none;
padding: 0;
}
nav ul li {
display: inline;
padding: 0 3em;
}
nav ul li a {
text-decoration: none;
}
#toggle-nav{
display: none;
-webkit-transition: transform 0.5s ease;
-moz-transition: transform 0.5s ease;
-o-transition: transform 0.5s ease;
-ms-transition: transform 0.5s ease;
transition: transform 0.5s ease;
}
.toggle-nav-rotated {
transform: rotate(360deg);
}
.nav-open{
max-height: 20em;
}
@media only screen and (max-width: 990px){
#toggle-nav{
margin-top: 0.25em;
margin-left: 0.75em;
position: absolute;
left: 0;
display: block;
font-size: 2em;
}
nav{
width: 100%;
padding: 0;
border-top-right-radius: 0;
border-top-left-radius: 0;
border-bottom-left-radius: 1em;
border-bottom-right-radius: 1em;
overflow: hidden;
}
nav ul {
margin-top: 1em;
gap: 2em;
display: flex;
flex-direction: column;
-webkit-transition: max-height 1s ease;
-moz-transition: max-height 1s ease;
-o-transition: max-height 1s ease;
-ms-transition: max-height 1s ease;
transition: max-height 1s ease;
max-height: 2em;
}
/* When TSX adds styles.open to the UL, expand it */
.open {
max-height: 20em;
}
nav ul:last-child{
padding-bottom: 1em;
}
nav ul #nav-logo {
margin: auto;
padding-bottom: 0.5em;
margin-bottom: -1em;
border-bottom: 0.2em solid var(--c-lines);
border-right: none;
}
/* Show hamburger on mobile */
.toggle {
margin-top: 0.25em;
margin-left: 0.75em;
position: absolute;
left: 0;
display: block;
font-size: 2em;
}
/* Submenu stacks inline under the parent item on mobile */
.submenu {
position: static;
border: none;
border-radius: 0;
background: transparent;
padding: 0 0 0.5em 0.5em;
min-width: unset;
}
.submenu a {
display: inline-block;
padding: 0; /* keep no padding on mobile too */
margin: 0.25em 0.5em; /* spacing via margin */
}
}

View File

@@ -1,52 +0,0 @@
import React, { useState, useContext } from "react"
import styles from "./HomeNav.module.css"
import { FaBars, FaChevronDown } from "react-icons/fa";
import { UserContext } from "../../context/UserContext";
export default function HomeNav() {
const [navOpen, setNavOpen] = useState(false)
const toggleNav = () => setNavOpen((prev) => !prev)
const { user } = useContext(UserContext);
return (
<nav className={styles.nav}>
<FaBars
className={`${styles.toggle} ${navOpen ? styles.toggleRotated : ""}`}
onClick={toggleNav}
aria-label="Toggle navigation"
aria-expanded={navOpen}
/>
<ul className={`${styles.navList} ${navOpen ? styles.open : ""}`}>
<li id="nav-logo" className={styles.logo}>
<span>vontor.cz</span>
</li>
<li>
<a href="/">Home</a>
</li>
<li>
<a href="#portfolio">Portfolio</a>
</li>
<li className={styles.hasSubmenu}>
<details>
<summary>
Services
<FaChevronDown className={`${styles.caret} ml-2 inline-block`} aria-hidden="true" />
</summary>
<ul className={styles.submenu}>
<li><a href="#web">Web development</a></li>
<li><a href="#integration">Integrations</a></li>
<li><a href="#support">Support</a></li>
</ul>
</details>
</li>
<li>
<a href="#contactme-form">Contact me</a>
</li>
</ul>
</nav>
)
}

View File

@@ -1,30 +0,0 @@
/*
# EXAMPLE USAGE OF CONTEXT IN A COMPONENT:
## Wrap your app tree with the provider (e.g., in App.tsx)
```tsx
import { UserContextProvider } from "../context/UserContext";
function App() {
return (
<UserContextProvider>
<YourRoutes />
</UserContextProvider>
);
}
```
## Consume in any child component
```tsx
import React, { useContext } from "react"
import { UserContext } from '../context/UserContext';
export default function ExampleComponent() {
const { user, setUser } = useContext(UserContext);
return ...;
}
```

View File

@@ -1,75 +0,0 @@
import React, { createContext, useState, useContext, useEffect } from 'react';
import userAPI from '../api/models/User';
// definice uživatele
export interface User {
id: string;
email: string;
username: string;
}
// určíme typ kontextu
interface GlobalContextType {
user: User | null;
setUser: React.Dispatch<React.SetStateAction<User | null>>;
}
// vytvoříme a exportneme kontext !!!
export const UserContext = createContext<GlobalContextType | null>(null);
// hook pro použití kontextu
// zabal routy do téhle komponenty!!!
export const UserContextProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [user, setUser] = useState<User | null>(null);
useEffect(() => {
const fetchUser = async () => {
try {
const currentUser = await userAPI.getCurrentUser();
setUser(currentUser);
} catch (error) {
console.error('Failed to load user:', error);
setUser(null);
}
};
fetchUser();
}, []);
return (
<UserContext.Provider value={{ user, setUser }}>
{children}
</UserContext.Provider>
);
};
/*
EXAMPLE USAGE OF CONTEXT IN A COMPONENT:
// Wrap your app tree with the provider (e.g., in App.tsx)
// import { UserContextProvider } from "../context/UserContext";
// function App() {
// return (
// <UserContextProvider>
// <YourRoutes />
// </UserContextProvider>
// );
// }
// Consume in any child component
import React, { useContext } from "react"
import { UserContext } from '../context/UserContext';
export default function ExampleComponent() {
const { user, setUser } = useContext(UserContext);
return ...;
}
*/

View File

@@ -1,67 +0,0 @@
@import "tailwindcss";
:root {
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
--c-background: #031D44; /*background*/
--c-background-light: #04395E; /*background-highlight*/
--c-boxes: #24719f;; /*boxes*/
--c-lines: #87a9da; /*lines*/
--c-text: #CAF0F8; /*text*/
--c-other: #70A288; /*other*/
}
body {
margin: 0;
display: flex;
place-items: center;
min-width: 320px;
min-height: 100vh;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #1a1a1a;
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
}

View File

@@ -1,28 +0,0 @@
import Footer from "../components/Footer/footer";
import ContactMeForm from "../components/Forms/ContactMe/ContactMeForm";
import HomeNav from "../components/navbar/HomeNav";
import Drone from "../components/ads/Drone/Drone";
import Portfolio from "../components/ads/Portfolio/Portfolio";
import Home from "../pages/home/home";
import { Outlet } from "react-router";
export default function HomeLayout(){
return(
<>
{/* Example usage of imported components, adjust as needed */}
<HomeNav />
<Home /> {/*page*/}
<div style={{margin: "10em 0"}}>
<Drone />
</div>
<Outlet />
<Portfolio />
<div style={{ margin: "6em auto", marginTop: "15em", maxWidth: "80vw" }}>
<ContactMeForm />
</div>
<Footer />
</>
)
}

View File

@@ -1,28 +0,0 @@
import Footer from "../components/Footer/footer";
import ContactMeForm from "../components/Forms/ContactMe/ContactMeForm";
import HomeNav from "../components/navbar/HomeNav";
import Drone from "../components/ads/Drone/Drone";
import Portfolio from "../components/ads/Portfolio/Portfolio";
import Home from "../pages/home/home";
import { Outlet } from "react-router";
export default function HomeLayout(){
return(
<>
{/* Example usage of imported components, adjust as needed */}
<HomeNav />
<Home /> {/*page*/}
<div style={{margin: "10em 0"}}>
<Drone />
</div>
<Outlet />
<Portfolio />
<div style={{ margin: "6em auto", marginTop: "15em", maxWidth: "80vw" }}>
<ContactMeForm />
</div>
<Footer />
</>
)
}

View File

@@ -1,69 +0,0 @@
# Layouts in React Router
## 📌 What is a Layout?
A **layout** in React Router is just a **React component** that wraps multiple pages with shared structure or styling (e.g., header, footer, sidebar).
Layouts usually contain:
- Global UI elements (navigation, footer, etc.)
- An `<Outlet />` component where nested routes will render their content
---
## 📂 Folder Structure Example
src/
layouts/
├── MainLayout.jsx
└── AdminLayout.jsx
pages/
├── HomePage.jsx
├── AboutPage.jsx
└── DashboardPage.jsx
---
## 🛠 How Layouts Are Used in Routes
### 1. Layout as a Parent Route
Use the layout component as the `element` of a **parent route** and place **pages** inside as nested routes.
```jsx
import { BrowserRouter, Routes, Route } from "react-router-dom";
import MainLayout from "./layouts/MainLayout";
import HomePage from "./pages/HomePage";
import AboutPage from "./pages/AboutPage";
function App() {
return (
<BrowserRouter>
<Routes>
<Route element={<MainLayout />}>
<Route path="/" element={<HomePage />} />
<Route path="/about" element={<AboutPage />} />
</Route>
</Routes>
</BrowserRouter>
);
}
export default App;
```
### 2. Inside the MainLayout.jsx
```jsx
import { Outlet } from "react-router-dom";
export default function MainLayout() {
return (
<>
<header>Header</header>
<main>
<Outlet />
</main>
<footer>Footer</footer>
</>
);
}
```

View File

@@ -1,10 +0,0 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.tsx'
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
)

View File

@@ -1,206 +0,0 @@
import { useEffect, useMemo, useState } from "react";
import {
fetchInfo,
downloadImmediate,
FORMAT_EXTS,
type InfoResponse,
parseContentDispositionFilename,
} from "../../api/apps/Downloader";
export default function Downloader() {
const [url, setUrl] = useState("");
const [probing, setProbing] = useState(false);
const [downloading, setDownloading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [info, setInfo] = useState<InfoResponse | null>(null);
const [ext, setExt] = useState<typeof FORMAT_EXTS[number]>("mp4");
const [videoRes, setVideoRes] = useState<string | undefined>(undefined);
const [audioRes, setAudioRes] = useState<string | undefined>(undefined);
useEffect(() => {
if (info?.video_resolutions?.length && !videoRes) {
setVideoRes(info.video_resolutions[0]);
}
if (info?.audio_resolutions?.length && !audioRes) {
setAudioRes(info.audio_resolutions[0]);
}
}, [info]);
async function onProbe(e: React.FormEvent) {
e.preventDefault();
setError(null);
setInfo(null);
setProbing(true);
try {
const res = await fetchInfo(url);
setInfo(res);
// reset selections from fresh info
setVideoRes(res.video_resolutions?.[0]);
setAudioRes(res.audio_resolutions?.[0]);
} catch (e: any) {
setError(
e?.response?.data?.error ||
e?.response?.data?.detail ||
e?.message ||
"Failed to get info."
);
} finally {
setProbing(false);
}
}
async function onDownload() {
setError(null);
setDownloading(true);
try {
const { blob, filename } = await downloadImmediate({
url,
ext,
videoResolution: videoRes,
audioResolution: audioRes,
});
const name = filename || parseContentDispositionFilename("") || `download.${ext}`;
const href = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = href;
a.download = name;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(href);
} catch (e: any) {
setError(
e?.response?.data?.error ||
e?.response?.data?.detail ||
e?.message ||
"Download failed."
);
} finally {
setDownloading(false);
}
}
const canDownload = useMemo(
() => !!url && !!ext && !!videoRes && !!audioRes,
[url, ext, videoRes, audioRes]
);
return (
<div className="max-w-3xl mx-auto p-4 space-y-4">
<h1 className="text-2xl font-semibold">Downloader</h1>
{error && <div className="rounded border border-red-300 bg-red-50 text-red-700 p-2">{error}</div>}
<form onSubmit={onProbe} className="grid gap-3">
<label className="grid gap-1">
<span className="text-sm font-medium">URL</span>
<input
type="url"
required
placeholder="https://example.com/video"
value={url}
onChange={(e) => setUrl(e.target.value)}
className="w-full border rounded p-2"
/>
</label>
<div className="flex gap-2">
<button
type="submit"
disabled={!url || probing}
className="px-3 py-2 rounded bg-blue-600 text-white disabled:opacity-50"
>
{probing ? "Probing..." : "Get info"}
</button>
<button
type="button"
onClick={onDownload}
disabled={!canDownload || downloading}
className="px-3 py-2 rounded bg-emerald-600 text-white disabled:opacity-50"
>
{downloading ? "Downloading..." : "Download"}
</button>
</div>
</form>
{info && (
<div className="space-y-3">
<div className="flex items-start gap-3">
{info.thumbnail && (
<img
src={info.thumbnail}
alt={info.title || "thumbnail"}
className="w-40 h-24 object-cover rounded border"
/>
)}
<div className="text-sm text-gray-800 space-y-1">
<div>
<span className="font-medium">Title:</span> {info.title || "-"}
</div>
<div>
<span className="font-medium">Duration:</span>{" "}
{info.duration ? `${Math.round(info.duration)} s` : "-"}
</div>
</div>
</div>
<div className="grid md:grid-cols-3 gap-3">
<label className="grid gap-1">
<span className="text-sm font-medium">Container</span>
<select
value={ext}
onChange={(e) => setExt(e.target.value as any)}
className="border rounded p-2"
>
{FORMAT_EXTS.map((x) => (
<option key={x} value={x}>
{x.toUpperCase()}
</option>
))}
</select>
</label>
<label className="grid gap-1">
<span className="text-sm font-medium">Video resolution</span>
<select
value={videoRes || ""}
onChange={(e) => setVideoRes(e.target.value || undefined)}
className="border rounded p-2"
>
{info.video_resolutions?.length ? (
info.video_resolutions.map((r) => (
<option key={r} value={r}>
{r}
</option>
))
) : (
<option value="">-</option>
)}
</select>
</label>
<label className="grid gap-1">
<span className="text-sm font-medium">Audio bitrate</span>
<select
value={audioRes || ""}
onChange={(e) => setAudioRes(e.target.value || undefined)}
className="border rounded p-2"
>
{info.audio_resolutions?.length ? (
info.audio_resolutions.map((r) => (
<option key={r} value={r}>
{r}
</option>
))
) : (
<option value="">-</option>
)}
</select>
</label>
</div>
</div>
)}
</div>
);
}

View File

@@ -1,240 +0,0 @@
@import url('https://fonts.googleapis.com/css2?family=Roboto+Mono:ital,wght@0,100..700;1,100..700&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Doto:wght@100..900&family=Roboto+Mono:ital,wght@0,100..700;1,100..700&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Bebas+Neue&family=Doto:wght@300&family=Roboto+Mono:ital,wght@0,100..700;1,100..700&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Exo:ital,wght@0,100..900;1,100..900&display=swap');
:root{
--c-background: #031D44; /*background*/
--c-background-light: #04395E; /*background-highlight*/
--c-boxes: #24719f;; /*boxes*/
--c-lines: #87a9da; /*lines*/
--c-text: #CAF0F8; /*text*/
--c-other: #70A288; /*other*/
}
html{
scroll-behavior: smooth;
}
body{
font-family: "Exo", serif;
font-optical-sizing: auto;
font-weight: 700;
font-style: normal;
}
.wrapper {
position: relative;
width: 100%;
}
.doto-font{
font-family: "Doto", serif;
font-optical-sizing: auto;
font-weight: 300;
font-style: normal;
font-variation-settings: "ROND" 0;
}
.bebas-neue-regular {
font-family: "Bebas Neue", sans-serif;
font-weight: 400;
font-style: normal;
}
.introduction {
display: flex;
flex-direction: column;
color: var(--c-text);
padding-bottom: 10em;
margin-top: 6em;
width: 100%;
position: relative;
top:0;
/* gap: 4em;*/
}
.introduction h1{
font-size: 2em;
}
.introduction article {
/*background-color: cadetblue;*/
padding: 2em;
border-radius: 1em;
}
.introduction article header {}
.introduction article:nth-child(1) {
width: 100%;
/* transform: rotate(5deg); */
align-self: center;
text-align: center;
font-size: 2em;
}
/*
.introduction article:nth-child(2) {
width: 50%;
transform: rotate(3deg);
align-self: flex-end;
}
.introduction article:nth-child(3) {
width: 50%;
transform: rotate(-2deg);
align-self: flex-start;
}*/
.animation-introduction{
position: absolute;
width: 100%;
height: 100%;
top: 0;
z-index: -2;
}
.animation-introduction ul{
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
/*overflow: hidden; ZAPNOUT KDYŽ NECHCEŠ ANIMACI PŘECHÁZET DO OSTATNÍCH DIVŮ*/
}
.animation-introduction ul li{
position: absolute;
display: block;
list-style: none;
width: 20px;
height: 20px;
background: rgba(255, 255, 255, 35%);
animation: animate 4s linear infinite;
bottom: -150px;
}
.animation-introduction ul li:nth-child(1){
left: 25%;
width: 80px;
height: 80px;
animation-delay: 0s;
}
.animation-introduction ul li:nth-child(2){
left: 10%;
width: 20px;
height: 20px;
animation-delay: 2s;
animation-duration: 12s;
}
.animation-introduction ul li:nth-child(3){
left: 70%;
width: 20px;
height: 20px;
animation-delay: 4s;
}
.animation-introduction ul li:nth-child(4){
left: 40%;
width: 60px;
height: 60px;
animation-delay: 0s;
animation-duration: 18s;
}
.animation-introduction ul li:nth-child(5){
left: 65%;
width: 20px;
height: 20px;
animation-delay: 0s;
}
.animation-introduction ul li:nth-child(6){
left: 75%;
width: 110px;
height: 110px;
animation-delay: 3s;
}
.animation-introduction ul li:nth-child(7){
left: 35%;
width: 150px;
height: 150px;
animation-delay: 7s;
}
.animation-introduction ul li:nth-child(8){
left: 50%;
width: 25px;
height: 25px;
animation-delay: 15s;
animation-duration: 45s;
}
.animation-introduction ul li:nth-child(9){
left: 20%;
width: 15px;
height: 15px;
animation-delay: 2s;
animation-duration: 35s;
}
.animation-introduction ul li:nth-child(10){
left: 85%;
width: 150px;
height: 150px;
animation-delay: 0s;
animation-duration: 11s;
}
@keyframes animate {
0%{
transform: translateY(0) rotate(0deg);
opacity: 1;
border-radius: 0;
}
100%{
transform: translateY(-1000px) rotate(720deg);
opacity: 0;
border-radius: 50%;
}
}
@media only screen and (max-width: 990px) {
.animation-introduction ul li:nth-child(6){
left: 67%;
}
.animation-introduction ul li:nth-child(10) {
left: 60%;
}
.introduction {
margin: 0;
top: 0;
}
.introduction article {
width: auto !important;
transform: none !important;
align-self: none !important;
}
}

View File

@@ -1,49 +0,0 @@
import React, { useEffect } from "react"
import styles from "./Home.module.css"
export default function Home() {
useEffect(() => {
const handleClick = (event: MouseEvent) => {
const randomId = "spark-" + Math.floor(Math.random() * 100000)
const spark = document.createElement("div")
spark.className = "spark-cursor"
spark.id = randomId
document.body.appendChild(spark)
// pozice a barva
spark.style.top = `${event.pageY}px`
spark.style.left = `${event.pageX}px`
spark.style.filter = `hue-rotate(${Math.random() * 360}deg)`
for (let i = 0; i < 8; i++) {
const span = document.createElement("span")
span.style.transform = `rotate(${i * 45}deg)`
spark.appendChild(span)
}
setTimeout(() => {
spark.querySelectorAll("span").forEach((s) => {
(s as HTMLElement).classList.add("animate")
})
}, 10)
setTimeout(() => {
spark.remove()
}, 1000)
}
document.body.addEventListener("click", handleClick)
// cleanup když komponenta zmizí
return () => {
document.body.removeEventListener("click", handleClick)
}
}, [])
return (
<></>
)
}

View File

@@ -1,22 +0,0 @@
import { Navigate, Outlet, useLocation } from "react-router-dom";
function getCookie(name: string): string | null {
const nameEQ = name + "=";
const ca = document.cookie.split(";").map((c) => c.trim());
for (const c of ca) {
if (c.indexOf(nameEQ) === 0) return decodeURIComponent(c.substring(nameEQ.length));
}
return null;
}
const ACCESS_COOKIE = "access_token";
export default function PrivateRoute() {
const location = useLocation();
const isLoggedIn = !!getCookie(ACCESS_COOKIE);
if (!isLoggedIn) {
return <Navigate to="/login" replace state={{ from: location }} />;
}
return <Outlet />;
}

View File

@@ -1,71 +0,0 @@
# Routes Folder
This folder contains the route definitions and components used to manage routing in the React application. It includes public and private routes, as well as nested layouts.
## File Structure
routes/
├── PrivateRoute.jsx
├── AppRoutes.jsx
└── index.js
### `PrivateRoute.jsx`
`PrivateRoute` is a wrapper component that restricts access to certain routes based on the user's authentication status. Only logged-in users can access routes wrapped inside `PrivateRoute`.
#### Example Usage
```jsx
import { Navigate, Outlet } from "react-router-dom";
import { useAuth } from "../auth"; // custom hook to get auth status
const PrivateRoute = () => {
const { isLoggedIn } = useAuth();
return isLoggedIn ? <Outlet /> : <Navigate to="/login" />;
};
export default PrivateRoute;
```
` <Outlet /> ` allows nested routes to be rendered inside the PrivateRoute.
### AppRoutes.jsx
This file contains all the route definitions example of the app. It can use layouts from the layouts folder to wrap sections of the app.
Example Usage
```jsx
import { Routes, Route } from "react-router-dom";
import PrivateRoute from "./PrivateRoute";
// Layouts
import MainLayout from "../layouts/MainLayout";
import AuthLayout from "../layouts/AuthLayout";
// Pages
import Dashboard from "../pages/Dashboard";
import Profile from "../pages/Profile";
import Login from "../pages/Login";
const AppRoutes = () => {
return (
<Routes>
{/* Public Routes */}
<Route element={<AuthLayout />}>
<Route path="/login" element={<Login />} />
</Route>
{/* Private Routes */}
<Route element={<PrivateRoute />}>
<Route element={<MainLayout />}>
<Route path="/" element={<Dashboard />} />
<Route path="/profile" element={<Profile />} />
</Route>
</Route>
</Routes>
);
};
export default AppRoutes;
```

View File

@@ -1 +0,0 @@
/// <reference types="vite/client" />

View File

@@ -1,27 +0,0 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2022",
"useDefineForClassFields": true,
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["src"]
}

View File

@@ -1,7 +0,0 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}

View File

@@ -1,25 +0,0 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "ES2023",
"lib": ["ES2023"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["vite.config.ts"]
}

View File

@@ -1,11 +0,0 @@
import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite'
import react from '@vitejs/plugin-react'
// https://vite.dev/config/
export default defineConfig({
plugins: [
react(),
tailwindcss()
],
})

View File

@@ -1,6 +1,6 @@
from django.apps import AppConfig
class GopayConfig(AppConfig):
class ApiConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'gopay'
name = 'api'

14
api/models.py Normal file
View File

@@ -0,0 +1,14 @@
from django.db import models
# Create your models here.
from django.contrib.auth.models import User
class UserProfile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
# Add custom fields here
bio = models.TextField(blank=True)
birthdate = models.DateField(null=True, blank=True)
profile_picture = models.ImageField(upload_to='profile_pics/', null=True, blank=True)
def __str__(self):
return f'{self.user.username} Profile'

7
api/permissions.py Normal file
View File

@@ -0,0 +1,7 @@
from rest_framework_api_key.permissions import HasAPIKey
class UserEditAPIKeyPermissions(HasAPIKey):
"""
Custom permision for restricting access using API key.
"""
pass

21
api/serializers.py Normal file
View File

@@ -0,0 +1,21 @@
from rest_framework import serializers
from .models import User
#Serializers are for what views can show fields of models
class PublicUserSerializers(serializers.ModelSerializer):
"""
Serializer for public User fields
"""
class Meta:
model = User
fields = ['id', 'username']
class SecureUserSerializers(serializers.ModelSerializer):
"""
Serializer for all User fields
Requires API key
"""
class Meta:
model = User
fields = ['id', 'username', 'email', 'first_name', 'last_name', 'bio']

10
api/urls.py Normal file
View File

@@ -0,0 +1,10 @@
from django.urls import path
from .views import PublicUserView, SecureUserUpdateView
urlpatterns = [
# URL for the public view to list users with public fields
path('users/', PublicUserView.as_view(), name='public-user-list'),
# URL for secure view to retrieve and update user with all fields
path('users/<int:pk>/', SecureUserUpdateView.as_view(), name='secure-user-update'),
]

20
api/views.py Normal file
View File

@@ -0,0 +1,20 @@
from django.shortcuts import render
# Create your views here.
from rest_framework import generics, permissions
from .models import User
from .serializers import PublicUserSerializers, SecureUserSerializers
from .permissions import UserEditAPIKeyPermissions
#Public view: List users with only public fields
class PublicUserView(generics.ListAPIView):
queryset = User.objects.all()
serializer_class = PublicUserSerializers
permission_classes = [permissions.AllowAny]
#Secure view for retrive/update user all fields (API key)
class SecureUserUpdateView(generics.RetrieveUpdateAPIView):
queryset = User.objects.all()
serializer_class = SecureUserSerializers
permission_classes = [UserEditAPIKeyPermissions]

View File

@@ -1,12 +0,0 @@
FROM python:3.12-slim
WORKDIR /app
RUN apt update && apt install ffmpeg -y
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8000

View File

@@ -1,23 +0,0 @@
from django.contrib import admin
from django.contrib.auth import get_user_model
from django.utils.translation import gettext_lazy as _
User = get_user_model()
@admin.register(User)
class UserAdmin(admin.ModelAdmin):
list_display = (
"id", "email", "role", "phone_number", "city", "street", "postal_code", "gdpr", "is_active", "email_verified", "create_time"
)
list_filter = ("role", "gdpr", "is_active", "email_verified", "city", "postal_code")
search_fields = ("email", "phone_number", "city", "street", "postal_code")
ordering = ("-create_time",)
fieldsets = (
(None, {"fields": ("email", "password", "role")}),
(_("Personal info"), {"fields": ("phone_number", "city", "street", "postal_code")}),
(_("Permissions"), {"fields": ("gdpr", "is_active", "email_verified")}),
(_("Important dates"), {"fields": ("create_time",)}),
)
readonly_fields = ("create_time",)
# Register your models here.

View File

@@ -1,24 +0,0 @@
import django_filters
from django.contrib.auth import get_user_model
User = get_user_model()
class UserFilter(django_filters.FilterSet):
role = django_filters.CharFilter(field_name="role", lookup_expr="exact")
email = django_filters.CharFilter(field_name="email", lookup_expr="icontains")
phone_number = django_filters.CharFilter(field_name="phone_number", lookup_expr="icontains")
city = django_filters.CharFilter(field_name="city", lookup_expr="icontains")
street = django_filters.CharFilter(field_name="street", lookup_expr="icontains")
postal_code = django_filters.CharFilter(field_name="postal_code", lookup_expr="exact")
gdpr = django_filters.BooleanFilter(field_name="gdpr")
is_active = django_filters.BooleanFilter(field_name="is_active")
email_verified = django_filters.BooleanFilter(field_name="email_verified")
create_time_after = django_filters.IsoDateTimeFilter(field_name="create_time", lookup_expr="gte")
create_time_before = django_filters.IsoDateTimeFilter(field_name="create_time", lookup_expr="lte")
class Meta:
model = User
fields = [
"role", "email", "phone_number", "city", "street", "postal_code", "gdpr", "is_active", "email_verified",
"create_time_after", "create_time_before"
]

View File

@@ -1,54 +0,0 @@
# Generated by Django 5.2.7 on 2025-10-28 22:28
import account.models
import django.contrib.auth.validators
import django.core.validators
import django.utils.timezone
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('auth', '0012_alter_user_first_name_max_length'),
]
operations = [
migrations.CreateModel(
name='CustomUser',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('password', models.CharField(max_length=128, verbose_name='password')),
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
('is_deleted', models.BooleanField(default=False)),
('deleted_at', models.DateTimeField(blank=True, null=True)),
('role', models.CharField(choices=[('admin', 'Admin'), ('mod', 'Moderator'), ('regular', 'Regular')], default='regular', max_length=20)),
('phone_number', models.CharField(blank=True, max_length=16, null=True, unique=True, validators=[django.core.validators.RegexValidator('^\\+?\\d{9,15}$', message='Zadejte platné telefonní číslo.')])),
('email_verified', models.BooleanField(default=False)),
('email', models.EmailField(db_index=True, max_length=254, unique=True)),
('gdpr', models.BooleanField(default=False)),
('is_active', models.BooleanField(default=False)),
('create_time', models.DateTimeField(auto_now_add=True)),
('city', models.CharField(blank=True, max_length=100, null=True)),
('street', models.CharField(blank=True, max_length=200, null=True)),
('postal_code', models.CharField(blank=True, max_length=5, null=True, validators=[django.core.validators.RegexValidator(code='invalid_postal_code', message='Postal code must contain exactly 5 digits.', regex='^\\d{5}$')])),
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to.', related_name='customuser_set', related_query_name='customuser', to='auth.group')),
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='customuser_set', related_query_name='customuser', to='auth.permission')),
],
options={
'abstract': False,
},
managers=[
('objects', account.models.CustomUserManager()),
('active', account.models.ActiveUserManager()),
],
),
]

View File

@@ -1,23 +0,0 @@
# Generated by Django 5.2.7 on 2025-10-31 07:36
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('account', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='customuser',
name='email_verification_sent_at',
field=models.DateTimeField(blank=True, null=True),
),
migrations.AddField(
model_name='customuser',
name='email_verification_token',
field=models.CharField(blank=True, db_index=True, max_length=128, null=True),
),
]

View File

@@ -1,160 +0,0 @@
import uuid
from django.db import models
from django.contrib.auth.models import AbstractUser, UserManager, Group, Permission
from django.core.validators import RegexValidator
from django.utils.crypto import get_random_string
from django.conf import settings
from django.db import models
from django.utils import timezone
from datetime import timedelta
from vontor_cz.models import SoftDeleteModel
from django.contrib.auth.models import UserManager
import logging
logger = logging.getLogger(__name__)
class CustomUserManager(UserManager):
# Inherit get_by_natural_key and all auth behaviors
use_in_migrations = True
class ActiveUserManager(CustomUserManager):
def get_queryset(self):
return super().get_queryset().filter(is_active=True)
class CustomUser(SoftDeleteModel, AbstractUser):
groups = models.ManyToManyField(
Group,
related_name="customuser_set", # <- přidáš related_name
blank=True,
help_text="The groups this user belongs to.",
related_query_name="customuser",
)
user_permissions = models.ManyToManyField(
Permission,
related_name="customuser_set", # <- přidáš related_name
blank=True,
help_text="Specific permissions for this user.",
related_query_name="customuser",
)
class Role(models.TextChoices):
ADMIN = "admin", "Admin"
MANAGER = "mod", "Moderator"
CUSTOMER = "regular", "Regular"
role = models.CharField(max_length=20, choices=Role.choices, default=Role.CUSTOMER)
phone_number = models.CharField(
null=True,
blank=True,
unique=True,
max_length=16,
validators=[RegexValidator(r'^\+?\d{9,15}$', message="Zadejte platné telefonní číslo.")]
)
email_verified = models.BooleanField(default=False)
email = models.EmailField(unique=True, db_index=True)
# + fields for email verification flow
email_verification_token = models.CharField(max_length=128, null=True, blank=True, db_index=True)
email_verification_sent_at = models.DateTimeField(null=True, blank=True)
gdpr = models.BooleanField(default=False)
is_active = models.BooleanField(default=False)
create_time = models.DateTimeField(auto_now_add=True)
city = models.CharField(null=True, blank=True, max_length=100)
street = models.CharField(null=True, blank=True, max_length=200)
postal_code = models.CharField(
blank=True,
null=True,
max_length=5,
validators=[
RegexValidator(
regex=r'^\d{5}$',
message="Postal code must contain exactly 5 digits.",
code='invalid_postal_code'
)
]
)
USERNAME_FIELD = "username"
REQUIRED_FIELDS = [
"email"
]
# Ensure default manager has get_by_natural_key
objects = CustomUserManager()
# Optional convenience manager for active users only
active = ActiveUserManager()
def delete(self, *args, **kwargs):
self.is_active = False
return super().delete(*args, **kwargs)
def save(self, *args, **kwargs):
is_new = self._state.adding # True if object hasn't been saved yet
# Pre-save flags for new users
if is_new:
if self.is_superuser or self.role == "admin":
# ensure admin flags are consistent
self.is_active = True
self.is_staff = True
self.is_superuser = True
self.role = "admin"
else:
self.is_staff = False
# First save to obtain a primary key
super().save(*args, **kwargs)
# Assign group after we have a PK
if is_new:
from django.contrib.auth.models import Group
group, _ = Group.objects.get_or_create(name=self.role)
# Use add/set now that PK exists
self.groups.set([group])
return super().save(*args, **kwargs)
def generate_email_verification_token(self, length: int = 48, save: bool = True) -> str:
token = get_random_string(length=length)
self.email_verification_token = token
self.email_verification_sent_at = timezone.now()
if save:
self.save(update_fields=["email_verification_token", "email_verification_sent_at"])
return token
def verify_email_token(self, token: str, max_age_hours: int = 48, save: bool = True) -> bool:
if not token or not self.email_verification_token:
return False
# optional expiry check
if self.email_verification_sent_at:
age = timezone.now() - self.email_verification_sent_at
if age > timedelta(hours=max_age_hours):
return False
if token != self.email_verification_token:
return False
if not self.email_verified:
self.email_verified = True
# clear token after success
self.email_verification_token = None
self.email_verification_sent_at = None
if save:
self.save(update_fields=["email_verified", "email_verification_token", "email_verification_sent_at"])
return True

View File

@@ -1,57 +0,0 @@
from urllib import request
from rest_framework.permissions import BasePermission, SAFE_METHODS
from rest_framework.permissions import IsAuthenticated
from rest_framework_api_key.permissions import HasAPIKey
#TOHLE POUŽÍT!!!
#Prostě stačí vložit: RoleAllowed('seller','cityClerk')
def RoleAllowed(*roles):
"""
Allows safe methods for any authenticated user.
Allows unsafe methods only for users with specific roles.
Allows access if a valid API key is provided.
Args:
RoleAllowed('admin', 'user')
"""
class SafeOrRolePermission(BasePermission):
def has_permission(self, request, view):
# Má API klíč?
has_api_key = HasAPIKey().has_permission(request, view)
# Allow safe methods for any authenticated user
if request.method in SAFE_METHODS:
return IsAuthenticated().has_permission(request, view)
# Otherwise, check the user's role
user = request.user
return user and user.is_authenticated and getattr(user, "role", None) in roles
return SafeOrRolePermission
def OnlyRolesAllowed(*roles):
class SafeOrRolePermission(BasePermission):
"""
Allows all methods only for users with specific roles.
"""
def has_permission(self, request, view):
# Otherwise, check the user's role
user = request.user
return user and user.is_authenticated and getattr(user, "role", None) in roles
return SafeOrRolePermission
# For Settings.py
class AdminOnly(BasePermission):
""" Allows access only to users with the 'admin' role.
Args:
BasePermission (rest_framework.permissions.BasePermission): Base class for permission classes.
"""
def has_permission(self, request, view):
return request.user and request.user.is_authenticated and getattr(request.user, 'role', None) == 'admin'

View File

@@ -1,205 +0,0 @@
import re
from django.utils.text import slugify
from django.core.validators import MinValueValidator, MaxValueValidator
from rest_framework import serializers
from rest_framework.exceptions import NotFound
from django.contrib.auth import get_user_model
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
from django.utils.translation import gettext_lazy as _
from django.utils.text import slugify
from .permissions import *
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
from rest_framework.exceptions import PermissionDenied
User = get_user_model()
class CustomUserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = [
"id",
"username",
"first_name",
"last_name",
"email",
"role",
"email_verified",
"phone_number",
"create_time",
"city",
"street",
"postal_code",
"gdpr",
"is_active",
]
read_only_fields = ["id", "create_time", "gdpr", "username"] # <-- removed "account_type"
def update(self, instance, validated_data):
user = self.context["request"].user
staff_only_fields = ["role", "email_verified", "var_symbol", "is_active"]
if user.role not in ["admin", "cityClerk"]:
unauthorized = [f for f in staff_only_fields if f in validated_data]
if unauthorized:
raise PermissionDenied(f"You are not allowed to modify: {', '.join(unauthorized)}")
return super().update(instance, validated_data)
# Token obtaining Default Serializer
class CustomTokenObtainPairSerializer(TokenObtainPairSerializer):
username_field = User.USERNAME_FIELD
def validate(self, attrs):
login = attrs.get("username")
password = attrs.get("password")
# Allow login by username or email
user = User.objects.filter(email__iexact=login).first() or \
User.objects.filter(username__iexact=login).first()
if user is None or not user.check_password(password):
raise serializers.ValidationError(_("No active account found with the given credentials"))
# Call the parent validation to create token
data = super().validate({
self.username_field: user.username,
"password": password
})
data["user_id"] = user.id
data["username"] = user.username
data["email"] = user.email
return data
# user creating section start ------------------------------------------
class UserRegistrationSerializer(serializers.ModelSerializer):
password = serializers.CharField(
write_only=True,
help_text="Heslo musí mít alespoň 8 znaků, obsahovat velká a malá písmena a číslici."
)
class Meta:
model = User
fields = [
'first_name', 'last_name', 'email', 'phone_number', 'password',
'city', 'street', 'postal_code', 'gdpr'
]
extra_kwargs = {
'first_name': {'required': True, 'help_text': 'Křestní jméno uživatele'},
'last_name': {'required': True, 'help_text': 'Příjmení uživatele'},
'email': {'required': True, 'help_text': 'Emailová adresa uživatele'},
'phone_number': {'required': True, 'help_text': 'Telefonní číslo uživatele'},
'city': {'required': True, 'help_text': 'Město uživatele'},
'street': {'required': True, 'help_text': 'Ulice uživatele'},
'postal_code': {'required': True, 'help_text': 'PSČ uživatele'},
'gdpr': {'required': True, 'help_text': 'Souhlas se zpracováním osobních údajů'},
}
def validate_password(self, value):
if len(value) < 8:
raise serializers.ValidationError("Password must be at least 8 characters long.")
if not re.search(r"[A-Z]", value):
raise serializers.ValidationError("Password must contain at least one uppercase letter.")
if not re.search(r"[a-z]", value):
raise serializers.ValidationError("Password must contain at least one lowercase letter.")
if not re.search(r"\d", value):
raise serializers.ValidationError("Password must contain at least one digit.")
return value
def validate(self, data):
email = data.get("email")
phone = data.get("phone_number")
dgpr = data.get("GDPR")
if not dgpr:
raise serializers.ValidationError({"GDPR": "You must agree to the GDPR to register."})
if User.objects.filter(email=email).exists():
raise serializers.ValidationError({"email": "Account with this email already exists."})
if phone and User.objects.filter(phone_number=phone).exists():
raise serializers.ValidationError({"phone_number": "Account with this phone number already exists."})
return data
def create(self, validated_data):
password = validated_data.pop("password")
username = validated_data.get("username", "")
user = User.objects.create(
username=username,
is_active=False, #uživatel je defaultně deaktivovaný
**validated_data
)
user.set_password(password)
user.save()
return user
class UserActivationSerializer(serializers.Serializer):
user_id = serializers.IntegerField()
var_symbol = serializers.IntegerField(validators=[MinValueValidator(0), MaxValueValidator(9999999999)])
def save(self, **kwargs):
try:
user = User.objects.get(pk=self.validated_data['user_id'])
except User.DoesNotExist:
raise NotFound("User with this ID does not exist.")
user.var_symbol = self.validated_data['var_symbol']
user.is_active = True
user.save()
return user
def to_representation(self, instance):
return {
"id": instance.id,
"email": instance.email,
"var_symbol": instance.var_symbol,
"is_active": instance.is_active,
}
class Meta:
model = User
fields = [
'user_id', 'var_symbol'
]
extra_kwargs = {
'user_id': {'required': True, 'help_text': 'ID uživatele'},
'var_symbol': {'required': True, 'help_text': 'Variablní symbol, zadán úředníkem'},
}
# user creating section end --------------------------------------------
class PasswordResetRequestSerializer(serializers.Serializer):
email = serializers.EmailField(
help_text="E-mail registrovaného a aktivního uživatele, na který bude zaslán reset hesla."
)
def validate_email(self, value):
if not User.objects.filter(email=value, is_active=True).exists():
raise serializers.ValidationError("Účet s tímto emailem neexistuje nebo není aktivní.")
return value
class PasswordResetConfirmSerializer(serializers.Serializer):
password = serializers.CharField(
write_only=True,
help_text="Nové heslo musí mít alespoň 8 znaků, obsahovat velká a malá písmena a číslici."
)
def validate_password(self, value):
import re
if len(value) < 8:
raise serializers.ValidationError("Heslo musí mít alespoň 8 znaků.")
if not re.search(r"[A-Z]", value):
raise serializers.ValidationError("Musí obsahovat velké písmeno.")
if not re.search(r"[a-z]", value):
raise serializers.ValidationError("Musí obsahovat malé písmeno.")
if not re.search(r"\d", value):
raise serializers.ValidationError("Musí obsahovat číslici.")
return value

View File

@@ -1,152 +0,0 @@
from celery import shared_task
from celery.utils.log import get_task_logger
from django.core.mail import send_mail
from django.conf import settings
from django.utils.http import urlsafe_base64_encode
from django.utils.encoding import force_bytes
from django.template.loader import render_to_string
from .tokens import *
from .models import CustomUser
logger = get_task_logger(__name__)
def send_email_with_context(recipients, subject, message=None, template_name=None, html_template_name=None, context=None):
"""
General function to send emails with a specific context.
Supports rendering plain text and HTML templates.
Converts `user` in context to a plain dict to avoid template access to the model.
"""
if isinstance(recipients, str):
recipients = [recipients]
html_message = None
if template_name or html_template_name:
# Best effort to resolve both templates if only one provided
if not template_name and html_template_name:
template_name = html_template_name.replace(".html", ".txt")
if not html_template_name and template_name:
html_template_name = template_name.replace(".txt", ".html")
ctx = dict(context or {})
# Sanitize user if someone passes the model by mistake
if "user" in ctx and not isinstance(ctx["user"], dict):
try:
ctx["user"] = _build_user_template_ctx(ctx["user"])
except Exception:
ctx["user"] = {}
message = render_to_string(template_name, ctx)
html_message = render_to_string(html_template_name, ctx)
try:
send_mail(
subject=subject,
message=message or "",
from_email=None,
recipient_list=recipients,
fail_silently=False,
html_message=html_message,
)
if settings.EMAIL_BACKEND == 'django.core.mail.backends.console.EmailBackend':
logger.debug(f"\nEMAIL OBSAH:\n{message}\nKONEC OBSAHU")
return True
except Exception as e:
logger.error(f"E-mail se neodeslal: {e}")
return False
def _build_user_template_ctx(user: CustomUser) -> dict:
"""
Return a plain dict for templates instead of passing the DB model.
Provides aliases to avoid template errors (firstname vs first_name).
Adds a backward-compatible key 'get_full_name' for templates using `user.get_full_name`.
"""
first_name = getattr(user, "first_name", "") or ""
last_name = getattr(user, "last_name", "") or ""
full_name = f"{first_name} {last_name}".strip()
return {
"id": user.pk,
"email": getattr(user, "email", "") or "",
"first_name": first_name,
"firstname": first_name, # alias for templates using `firstname`
"last_name": last_name,
"lastname": last_name, # alias for templates using `lastname`
"full_name": full_name,
"get_full_name": full_name, # compatibility for templates using method-style access
}
#----------------------------------------------------------------------------------------------------
# This function sends an email to the user for email verification after registration.
@shared_task
def send_email_verification_task(user_id):
try:
user = CustomUser.objects.get(pk=user_id)
except CustomUser.DoesNotExist:
logger.info(f"Task send_email_verification has failed. Invalid User ID was sent.")
return 0
uid = urlsafe_base64_encode(force_bytes(user.pk))
# {changed} generate and store a per-user token
token = user.generate_email_verification_token()
verify_url = f"{settings.FRONTEND_URL}/email-verification/?uidb64={uid}&token={token}"
context = {
"user": _build_user_template_ctx(user),
"action_url": verify_url,
"frontend_url": settings.FRONTEND_URL,
"cta_label": "Ověřit email",
}
send_email_with_context(
recipients=user.email,
subject="Ověření emailu",
template_name="email/email_verification.txt",
html_template_name="email/email_verification.html",
context=context,
)
@shared_task
def send_email_test_task(email):
context = {
"action_url": settings.FRONTEND_URL,
"frontend_url": settings.FRONTEND_URL,
"cta_label": "Otevřít aplikaci",
}
send_email_with_context(
recipients=email,
subject="Testovací email",
template_name="email/test.txt",
html_template_name="email/test.html",
context=context,
)
@shared_task
def send_password_reset_email_task(user_id):
try:
user = CustomUser.objects.get(pk=user_id)
except CustomUser.DoesNotExist:
logger.info(f"Task send_password_reset_email has failed. Invalid User ID was sent.")
return 0
uid = urlsafe_base64_encode(force_bytes(user.pk))
token = password_reset_token.make_token(user)
reset_url = f"{settings.FRONTEND_URL}/reset-password/{uid}/{token}"
context = {
"user": _build_user_template_ctx(user),
"action_url": reset_url,
"frontend_url": settings.FRONTEND_URL,
"cta_label": "Obnovit heslo",
}
send_email_with_context(
recipients=user.email,
subject="Obnova hesla",
template_name="email/password_reset.txt",
html_template_name="email/password_reset.html",
context=context,
)

View File

@@ -1,46 +0,0 @@
<!doctype html>
<html lang="cs">
<body style="margin:0; padding:0; background-color:#f5f7fb;">
<table role="presentation" cellpadding="0" cellspacing="0" border="0" width="100%" style="background-color:#f5f7fb;">
<tr>
<td align="center" style="padding:24px;">
<table role="presentation" cellpadding="0" cellspacing="0" border="0" width="600" style="width:600px; max-width:100%; background-color:#ffffff; border:1px solid #e5e7eb;">
<tr>
<td style="background-color:#111827; color:#ffffff; font-family:Arial, Helvetica, sans-serif; font-size:18px; font-weight:bold; padding:16px 20px;">
Ověření emailu
</td>
</tr>
<tr>
<td style="padding:20px; font-family:Arial, Helvetica, sans-serif; font-size:14px; color:#1f2937; line-height:1.6;">
{% with name=user.first_name|default:user.firstname|default:user.get_full_name %}
<p style="margin:0 0 12px 0;">Dobrý den{% if name %} {{ name }}{% endif %},</p>
{% endwith %}
<p style="margin:0 0 16px 0;">Děkujeme za registraci. Prosíme, ověřte svou emailovou adresu kliknutím na tlačítko níže.</p>
{% if action_url and cta_label %}
<table role="presentation" cellpadding="0" cellspacing="0" border="0" style="margin:16px 0;">
<tr>
<td bgcolor="#2563eb" style="border-radius:6px;">
<a href="{{ action_url }}" target="_blank" style="display:inline-block; padding:10px 16px; font-family:Arial, Helvetica, sans-serif; font-size:14px; color:#ffffff; text-decoration:none; font-weight:bold;">
{{ cta_label }}
</a>
</td>
</tr>
</table>
<p style="margin:0; color:#6b7280; font-size:12px;">Pokud tlačítko nefunguje, zkopírujte do prohlížeče tento odkaz:<br><span style="word-break:break-all;">{{ action_url }}</span></p>
{% endif %}
</td>
</tr>
</table>
<table role="presentation" cellpadding="0" cellspacing="0" border="0" width="600" style="width:600px; max-width:100%; margin-top:12px;">
<tr>
<td align="center" style="font-family:Arial, Helvetica, sans-serif; font-size:12px; color:#6b7280;">
Tento email byl odeslán z aplikace etržnice.
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>

View File

@@ -1,7 +0,0 @@
{% with name=user.first_name|default:user.firstname|default:user.get_full_name %}Dobrý den{% if name %} {{ name }}{% endif %},{% endwith %}
Děkujeme za registraci. Prosíme, ověřte svou emailovou adresu kliknutím na následující odkaz:
{{ action_url }}
Pokud jste účet nevytvořili vy, tento email ignorujte.

View File

@@ -1,46 +0,0 @@
<!doctype html>
<html lang="cs">
<body style="margin:0; padding:0; background-color:#f5f7fb;">
<table role="presentation" cellpadding="0" cellspacing="0" border="0" width="100%" style="background-color:#f5f7fb;">
<tr>
<td align="center" style="padding:24px;">
<table role="presentation" cellpadding="0" cellspacing="0" border="0" width="600" style="width:600px; max-width:100%; background-color:#ffffff; border:1px solid #e5e7eb;">
<tr>
<td style="background-color:#111827; color:#ffffff; font-family:Arial, Helvetica, sans-serif; font-size:18px; font-weight:bold; padding:16px 20px;">
Obnova hesla
</td>
</tr>
<tr>
<td style="padding:20px; font-family:Arial, Helvetica, sans-serif; font-size:14px; color:#1f2937; line-height:1.6;">
{% with name=user.first_name|default:user.firstname|default:user.get_full_name %}
<p style="margin:0 0 12px 0;">Dobrý den{% if name %} {{ name }}{% endif %},</p>
{% endwith %}
<p style="margin:0 0 12px 0;">Obdrželi jste tento email, protože byla požádána obnova hesla k vašemu účtu. Pokud jste o změnu nepožádali, tento email ignorujte.</p>
{% if action_url and cta_label %}
<table role="presentation" cellpadding="0" cellspacing="0" border="0" style="margin:16px 0;">
<tr>
<td bgcolor="#2563eb" style="border-radius:6px;">
<a href="{{ action_url }}" target="_blank" style="display:inline-block; padding:10px 16px; font-family:Arial, Helvetica, sans-serif; font-size:14px; color:#ffffff; text-decoration:none; font-weight:bold;">
{{ cta_label }}
</a>
</td>
</tr>
</table>
<p style="margin:0; color:#6b7280; font-size:12px;">Pokud tlačítko nefunguje, zkopírujte do prohlížeče tento odkaz:<br><span style="word-break:break-all;">{{ action_url }}</span></p>
{% endif %}
</td>
</tr>
</table>
<table role="presentation" cellpadding="0" cellspacing="0" border="0" width="600" style="width:600px; max-width:100%; margin-top:12px;">
<tr>
<td align="center" style="font-family:Arial, Helvetica, sans-serif; font-size:12px; color:#6b7280;">
Tento email byl odeslán z aplikace etržnice.
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>

View File

@@ -1,7 +0,0 @@
{% with name=user.first_name|default:user.firstname|default:user.get_full_name %}Dobrý den{% if name %} {{ name }}{% endif %},{% endwith %}
Obdrželi jste tento email, protože byla požádána obnova hesla k vašemu účtu.
Pokud jste o změnu nepožádali, tento email ignorujte.
Pro nastavení nového hesla použijte tento odkaz:
{{ action_url }}

View File

@@ -1,44 +0,0 @@
<!doctype html>
<html lang="cs">
<body style="margin:0; padding:0; background-color:#f5f7fb;">
<table role="presentation" cellpadding="0" cellspacing="0" border="0" width="100%" style="background-color:#f5f7fb;">
<tr>
<td align="center" style="padding:24px;">
<table role="presentation" cellpadding="0" cellspacing="0" border="0" width="600" style="width:600px; max-width:100%; background-color:#ffffff; border:1px solid #e5e7eb;">
<tr>
<td style="background-color:#111827; color:#ffffff; font-family:Arial, Helvetica, sans-serif; font-size:18px; font-weight:bold; padding:16px 20px;">
Testovací email
</td>
</tr>
<tr>
<td style="padding:20px; font-family:Arial, Helvetica, sans-serif; font-size:14px; color:#1f2937; line-height:1.6;">
<p style="margin:0 0 12px 0;">Dobrý den,</p>
<p style="margin:0 0 16px 0;">Toto je testovací email z aplikace etržnice.</p>
{% if action_url and cta_label %}
<table role="presentation" cellpadding="0" cellspacing="0" border="0" style="margin:16px 0;">
<tr>
<td bgcolor="#2563eb" style="border-radius:6px;">
<a href="{{ action_url }}" target="_blank" style="display:inline-block; padding:10px 16px; font-family:Arial, Helvetica, sans-serif; font-size:14px; color:#ffffff; text-decoration:none; font-weight:bold;">
{{ cta_label }}
</a>
</td>
</tr>
</table>
<p style="margin:0; color:#6b7280; font-size:12px;">Pokud tlačítko nefunguje, zkopírujte do prohlížeče tento odkaz:<br><span style="word-break:break-all;">{{ action_url }}</span></p>
{% endif %}
</td>
</tr>
</table>
<table role="presentation" cellpadding="0" cellspacing="0" border="0" width="600" style="width:600px; max-width:100%; margin-top:12px;">
<tr>
<td align="center" style="font-family:Arial, Helvetica, sans-serif; font-size:12px; color:#6b7280;">
Tento email byl odeslán z aplikace etržnice.
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>

View File

@@ -1,6 +0,0 @@
Dobrý den,
Toto je testovací email z aplikace etržnice.
Odkaz na aplikaci:
{{ action_url }}

View File

@@ -1,28 +0,0 @@
from django.test import TestCase
from django.contrib.auth import get_user_model
from rest_framework.test import APIClient
class UserViewAnonymousTests(TestCase):
def setUp(self):
self.client = APIClient()
User = get_user_model()
self.target_user = User.objects.create_user(
username="target",
email="target@example.com",
password="pass1234",
is_active=True,
)
def test_anonymous_update_user_is_forbidden_and_does_not_crash(self):
url = f"/api/account/users/{self.target_user.id}/"
payload = {"username": "newname", "email": self.target_user.email}
resp = self.client.put(url, data=payload, format="json")
# Expect 403 Forbidden (permission denied), but most importantly no 500 error
self.assertEqual(resp.status_code, 403, msg=f"Unexpected status: {resp.status_code}, body={getattr(resp, 'data', resp.content)}")
def test_anonymous_retrieve_user_is_unauthorized(self):
url = f"/api/account/users/{self.target_user.id}/"
resp = self.client.get(url)
# Retrieve requires authentication per view; expect 401 Unauthorized
self.assertEqual(resp.status_code, 401, msg=f"Unexpected status: {resp.status_code}, body={getattr(resp, 'data', resp.content)}")

View File

@@ -1,33 +0,0 @@
from django.contrib.auth.tokens import PasswordResetTokenGenerator
# Subclass PasswordResetTokenGenerator to create a separate token generator
# for account activation. This allows future customization specific to activation tokens,
# even though it currently behaves exactly like the base class.
class AccountActivationTokenGenerator(PasswordResetTokenGenerator):
pass # No changes yet; inherits all behavior from PasswordResetTokenGenerator
# Create an instance of AccountActivationTokenGenerator to be used for generating
# and validating account activation tokens throughout the app.
account_activation_token = AccountActivationTokenGenerator()
# Create an instance of the base PasswordResetTokenGenerator to be used
# for password reset tokens.
password_reset_token = PasswordResetTokenGenerator()
from rest_framework_simplejwt.authentication import JWTAuthentication
#NEMĚNIT CUSTOM SBÍRANÍ COOKIE TOKENU
class CookieJWTAuthentication(JWTAuthentication):
def authenticate(self, request):
raw_token = request.COOKIES.get('access_token')
if not raw_token:
return None
validated_token = self.get_validated_token(raw_token)
return self.get_user(validated_token), validated_token

View File

@@ -1,27 +0,0 @@
from django.urls import path
from . import views
from django.urls import path, include
from rest_framework.routers import DefaultRouter
router = DefaultRouter()
router.register(r'users', views.UserView, basename='user')
urlpatterns = [
# Auth endpoints
path('login/', views.CookieTokenObtainPairView.as_view(), name='login'),
path('token/refresh/', views.CookieTokenRefreshView.as_view(), name='token-refresh'),
path('logout/', views.LogoutView.as_view(), name='logout'),
path('user/me/', views.CurrentUserView.as_view(), name='user-detail'),
# Registration & email endpoints
path('register/', views.UserRegistrationViewSet.as_view({'post': 'create'}), name='register'),
path('verify-email/<uidb64>/<token>/', views.EmailVerificationView.as_view(), name='verify-email'),
path('activate/', views.UserActivationViewSet.as_view(), name='activate-user'),
# Password reset endpoints
path('password-reset/', views.PasswordResetRequestView.as_view(), name='password-reset-request'),
path('password-reset-confirm/<uidb64>/<token>/', views.PasswordResetConfirmView.as_view(), name='password-reset-confirm'),
# User CRUD (list, retrieve, update, delete)
path('', include(router.urls)), #/users/
]

View File

@@ -1,425 +0,0 @@
from django.contrib.auth import get_user_model, authenticate
from django.utils.http import urlsafe_base64_encode, urlsafe_base64_decode
from django.utils.encoding import force_bytes, force_str
from .serializers import *
from .permissions import *
from .models import CustomUser
from .tokens import *
from .tasks import send_password_reset_email_task
from django.conf import settings
import logging
logger = logging.getLogger(__name__)
from .filters import UserFilter
from rest_framework import generics, permissions, status, viewsets
from rest_framework.response import Response
from rest_framework_simplejwt.tokens import RefreshToken
from rest_framework.views import APIView
from rest_framework.viewsets import ModelViewSet
from rest_framework.permissions import IsAuthenticated, AllowAny
from rest_framework_simplejwt.tokens import RefreshToken
from rest_framework_simplejwt.exceptions import TokenError, AuthenticationFailed
from django_filters.rest_framework import DjangoFilterBackend
from drf_spectacular.utils import extend_schema, OpenApiResponse, OpenApiExample, OpenApiParameter
User = get_user_model()
#general user view API
from rest_framework_simplejwt.views import TokenObtainPairView
#---------------------------------------------TOKENY------------------------------------------------
# Custom Token obtaining view
@extend_schema(
tags=["Authentication"],
summary="Obtain JWT access and refresh tokens (cookie-based)",
description="Authenticate user and obtain JWT access and refresh tokens. You can use either email or username.",
request=CustomTokenObtainPairSerializer,
responses={
200: OpenApiResponse(response=CustomTokenObtainPairSerializer, description="Tokens returned successfully."),
401: OpenApiResponse(description="Invalid credentials or inactive user."),
},
)
class CookieTokenObtainPairView(TokenObtainPairView):
serializer_class = CustomTokenObtainPairSerializer
def post(self, request, *args, **kwargs):
response = super().post(request, *args, **kwargs)
# Získáme tokeny z odpovědi
access = response.data.get("access")
refresh = response.data.get("refresh")
if not access or not refresh:
return response # Např. při chybě přihlášení
jwt_settings = settings.SIMPLE_JWT
# Access token cookie
response.set_cookie(
key=jwt_settings.get("AUTH_COOKIE", "access_token"),
value=access,
httponly=jwt_settings.get("AUTH_COOKIE_HTTP_ONLY", True),
secure=jwt_settings.get("AUTH_COOKIE_SECURE", not settings.DEBUG),
samesite=jwt_settings.get("AUTH_COOKIE_SAMESITE", "Lax"),
path=jwt_settings.get("AUTH_COOKIE_PATH", "/"),
max_age=5 * 60, # 5 minut
)
# Refresh token cookie
response.set_cookie(
key="refresh_token",
value=refresh,
httponly=True,
secure=not settings.DEBUG,
samesite="Lax",
path="/",
max_age=7 * 24 * 60 * 60, # 7 dní
)
return response
def validate(self, attrs):
username = attrs.get("username")
password = attrs.get("password")
# Přihlaš uživatele ručně
user = authenticate(request=self.context.get('request'), username=username, password=password)
if not user:
raise AuthenticationFailed("Špatné uživatelské jméno nebo heslo.")
if not user.is_active:
raise AuthenticationFailed("Uživatel je deaktivován.")
# Nastav validní uživatele (přebere další logiku ze SimpleJWT)
self.user = user
# Vrátí access a refresh token jako obvykle
return super().validate(attrs)
@extend_schema(
tags=["Authentication"],
summary="Refresh JWT token using cookie",
description="Refresh JWT access and refresh tokens using the refresh token stored in cookie.",
responses={
200: OpenApiResponse(description="Tokens refreshed successfully."),
400: OpenApiResponse(description="Refresh token cookie not found."),
401: OpenApiResponse(description="Invalid refresh token."),
},
)
class CookieTokenRefreshView(APIView):
def post(self, request):
refresh_token = request.COOKIES.get('refresh_token')
if not refresh_token:
return Response({"detail": "Refresh token cookie not found."}, status=status.HTTP_400_BAD_REQUEST)
try:
refresh = RefreshToken(refresh_token)
access_token = str(refresh.access_token)
new_refresh_token = str(refresh) # volitelně nový refresh token
response = Response({
"access": access_token,
"refresh": new_refresh_token,
})
# Nastav nové HttpOnly cookies
# Access token cookie (např. 5 minut platnost)
response.set_cookie(
"access_token",
access_token,
httponly=True,
secure=not settings.DEBUG,
samesite="Lax",
max_age=5 * 60,
path="/",
)
# Refresh token cookie (delší platnost, např. 7 dní)
response.set_cookie(
"refresh_token",
new_refresh_token,
httponly=True,
secure=not settings.DEBUG,
samesite="Lax",
max_age=7 * 24 * 60 * 60,
path="/",
)
return response
except TokenError:
return Response({"detail": "Invalid refresh token."}, status=status.HTTP_401_UNAUTHORIZED)
#---------------------------------------------LOGOUT------------------------------------------------
@extend_schema(
tags=["Authentication"],
summary="Logout user (delete access and refresh token cookies)",
description="Logs out the user by deleting access and refresh token cookies.",
responses={
200: OpenApiResponse(description="Logout successful."),
},
)
class LogoutView(APIView):
permission_classes = [AllowAny]
def post(self, request):
response = Response({"detail": "Logout successful"}, status=status.HTTP_200_OK)
# Smazání cookies
response.delete_cookie("access_token", path="/")
response.delete_cookie("refresh_token", path="/")
return response
#--------------------------------------------------------------------------------------------------------------
@extend_schema(
tags=["User"],
summary="List, retrieve, update, and delete users.",
description="Displays all users with filtering and ordering options. Requires authentication and appropriate role.",
responses={
200: OpenApiResponse(response=CustomUserSerializer, description="User(s) retrieved successfully."),
403: OpenApiResponse(description="Permission denied."),
},
)
class UserView(viewsets.ModelViewSet):
queryset = User.objects.all()
serializer_class = CustomUserSerializer
filter_backends = [DjangoFilterBackend]
filterset_class = UserFilter
# Require authentication and role permission
permission_classes = [IsAuthenticated]
class Meta:
model = CustomUser
extra_kwargs = {
"email": {"help_text": "Unikátní e-mailová adresa uživatele."},
"phone_number": {"help_text": "Telefonní číslo ve formátu +420123456789."},
"role": {"help_text": "Role uživatele určující jeho oprávnění v systému."},
"account_type": {"help_text": "Typ účtu firma nebo fyzická osoba."},
"email_verified": {"help_text": "Určuje, zda je e-mail ověřen."},
"create_time": {"help_text": "Datum a čas registrace uživatele (pouze pro čtení).", "read_only": True},
"var_symbol": {"help_text": "Variabilní symbol pro platby, pokud je vyžadován."},
"bank_account": {"help_text": "Číslo bankovního účtu uživatele."},
"ICO": {"help_text": "IČO firmy, pokud se jedná o firemní účet."},
"RC": {"help_text": "Rodné číslo pro fyzické osoby."},
"city": {"help_text": "Město trvalého pobytu / sídla."},
"street": {"help_text": "Ulice a číslo popisné."},
"PSC": {"help_text": "PSČ místa pobytu / sídla."},
"GDPR": {"help_text": "Souhlas se zpracováním osobních údajů."},
"is_active": {"help_text": "Stav aktivace uživatele."},
}
def get_permissions(self):
# Only admin can list or create users
if self.action in ['list', 'create']:
return [OnlyRolesAllowed("admin")()]
# Only admin or the user themselves can update or delete
elif self.action in ['update', 'partial_update', 'destroy']:
user = getattr(self, 'request', None) and getattr(self.request, 'user', None)
# Admins can modify any user
if user and getattr(user, 'is_authenticated', False) and getattr(user, 'role', None) == 'admin':
return [OnlyRolesAllowed("admin")()]
# Users can modify their own record
if user and getattr(user, 'is_authenticated', False) and self.kwargs.get('pk') and str(getattr(user, 'id', '')) == self.kwargs['pk']:
return [IsAuthenticated()]
# Fallback - deny access (prevents AttributeError for AnonymousUser)
return [OnlyRolesAllowed("admin")()]
# Any authenticated user can retrieve (view) any user's profile
elif self.action == 'retrieve':
return [IsAuthenticated()]
return super().get_permissions()
# Get current user data
@extend_schema(
tags=["User"],
summary="Get current authenticated user",
description="Returns details of the currently authenticated user based on JWT token or session.",
responses={
200: OpenApiResponse(response=CustomUserSerializer, description="Current user details."),
401: OpenApiResponse(description="Unauthorized, user is not authenticated."),
}
)
class CurrentUserView(APIView):
permission_classes = [IsAuthenticated]
def get(self, request):
serializer = CustomUserSerializer(request.user)
return Response(serializer.data)
#------------------------------------------------REGISTRACE--------------------------------------------------------------
#1. registration API
@extend_schema(
tags=["User Registration"],
summary="Register a new user (company or individual)",
description="Register a new user (company or individual). The user will receive an email with a verification link.",
request=UserRegistrationSerializer,
responses={
201: OpenApiResponse(response=UserRegistrationSerializer, description="User registered successfully."),
400: OpenApiResponse(description="Invalid registration data."),
},
)
class UserRegistrationViewSet(ModelViewSet):
queryset = CustomUser.objects.all()
serializer_class = UserRegistrationSerializer
http_method_names = ['post']
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
user = serializer.save()
try:
send_email_verification_task.delay(user.id) # posílaní emailu pro potvrzení registrace - CELERY TASK
except Exception as e:
logger.error(f"Celery not available, using fallback. Error: {e}")
send_email_verification_task(user.id) # posílaní emailu pro potvrzení registrace
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
#2. confirming email
@extend_schema(
tags=["User Registration"],
summary="Verify user email via link",
description="Verify user email using the link with uid and token.",
parameters=[
OpenApiParameter(name='uidb64', type=str, location=OpenApiParameter.PATH, description="User ID encoded in base64 from email link."),
OpenApiParameter(name='token', type=str, location=OpenApiParameter.PATH, description="User token from email link."),
],
responses={
200: OpenApiResponse(description="Email successfully verified."),
400: OpenApiResponse(description="Invalid or expired token."),
},
)
class EmailVerificationView(APIView):
def get(self, request, uidb64, token):
try:
uid = force_str(urlsafe_base64_decode(uidb64))
user = User.objects.get(pk=uid)
except (User.DoesNotExist, ValueError, TypeError):
return Response({"error": "Neplatný odkaz."}, status=400)
if account_activation_token.check_token(user, token):
user.email_verified = True
user.save()
return Response({"detail": "E-mail byl úspěšně ověřen. Účet čeká na schválení."})
else:
return Response({"error": "Token je neplatný nebo expirovaný."}, status=400)
#3. seller activation API (var_symbol)
@extend_schema(
tags=["User Registration"],
summary="Activate user and set variable symbol (admin/cityClerk only)",
description="Activate user and set variable symbol. Only accessible by admin or cityClerk.",
request=UserActivationSerializer,
responses={
200: OpenApiResponse(response=UserActivationSerializer, description="User activated successfully."),
400: OpenApiResponse(description="Invalid activation data."),
404: OpenApiResponse(description="User not found."),
},
)
class UserActivationViewSet(APIView):
permission_classes = [OnlyRolesAllowed('cityClerk', 'admin')]
def patch(self, request, *args, **kwargs):
serializer = UserActivationSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
user = serializer.save()
try:
send_email_clerk_accepted_task.delay(user.id) # posílaní emailu pro informování uživatele o dokončení registrace, uředník doplnil variabilní symbol - CELERY TASK
except Exception as e:
logger.error(f"Celery not available, using fallback. Error: {e}")
send_email_clerk_accepted_task(user.id) # posílaní emailu pro informování uživatele o dokončení registrace, uředník doplnil variabilní symbol
return Response(serializer.to_representation(user), status=status.HTTP_200_OK)
#-------------------------------------------------END REGISTRACE-------------------------------------------------------------
#1. PasswordReset + send Email
@extend_schema(
tags=["User password reset"],
summary="Request password reset (send email)",
description="Request password reset by providing registered email. An email with instructions will be sent.",
request=PasswordResetRequestSerializer,
responses={
200: OpenApiResponse(description="Email with instructions sent."),
400: OpenApiResponse(description="Invalid email or request data."),
},
)
class PasswordResetRequestView(APIView):
def post(self, request):
serializer = PasswordResetRequestSerializer(data=request.data)
if serializer.is_valid():
try:
user = User.objects.get(email=serializer.validated_data['email'])
except User.DoesNotExist:
# Always return 200 even if user doesn't exist to avoid user enumeration
return Response({"detail": "E-mail s odkazem byl odeslán."})
try:
send_password_reset_email_task.delay(user.id) # posílaní emailu pro obnovení hesla - CELERY TASK
except Exception as e:
logger.error(f"Celery not available, using fallback. Error: {e}")
send_password_reset_email_task(user.id) # posílaní emailu pro obnovení hesla registrace
return Response({"detail": "E-mail s odkazem byl odeslán."})
return Response(serializer.errors, status=400)
#2. Confirming reset
@extend_schema(
tags=["User password reset"],
summary="Confirm password reset via token",
description="Confirm password reset using token from email.",
request=PasswordResetConfirmSerializer,
parameters=[
OpenApiParameter(name='uidb64', type=str, location=OpenApiParameter.PATH, description="User ID encoded in base64 from email link."),
OpenApiParameter(name='token', type=str, location=OpenApiParameter.PATH, description="Password reset token from email link."),
],
responses={
200: OpenApiResponse(description="Password changed successfully."),
400: OpenApiResponse(description="Invalid token or request data."),
},
)
class PasswordResetConfirmView(APIView):
def post(self, request, uidb64, token):
try:
uid = force_str(urlsafe_base64_decode(uidb64))
user = User.objects.get(pk=uid)
except (TypeError, ValueError, OverflowError, User.DoesNotExist):
return Response({"error": "Neplatný odkaz."}, status=400)
if not password_reset_token.check_token(user, token):
return Response({"error": "Token je neplatný nebo expirovaný."}, status=400)
serializer = PasswordResetConfirmSerializer(data=request.data)
if serializer.is_valid():
user.set_password(serializer.validated_data['password'])
user.save()
return Response({"detail": "Heslo bylo úspěšně změněno."})
return Response(serializer.errors, status=400)

View File

@@ -1,6 +0,0 @@
from django.apps import AppConfig
class AdvertisementConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'advertisement'

View File

@@ -1,3 +0,0 @@
from django.db import models
# Create your models here.

View File

@@ -1,2 +0,0 @@
#udělat zasílaní reklamních emailů uživatelům.
#newletter --> když se vytvoří nový record s reklamou email se uloží pomocí zaškrtnutí tlačítka v záznamu

View File

@@ -1,3 +0,0 @@
from django.shortcuts import render
# Create your views here.

View File

@@ -1,14 +0,0 @@
from django.contrib import admin
from .models import Carrier, Product
# Register your models here.
@admin.register(Carrier)
class CarrierAdmin(admin.ModelAdmin):
list_display = ("name", "base_price", "is_active")
@admin.register(Product)
class ProductAdmin(admin.ModelAdmin):
list_display = ("name", "price", "currency", "stock", "is_active")
search_fields = ("name", "description")

View File

@@ -1,6 +0,0 @@
from django.apps import AppConfig
class CommerceConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'commerce'

View File

@@ -1,41 +0,0 @@
# Generated by Django 5.2.7 on 2025-10-28 22:28
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='Carrier',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100)),
('base_price', models.DecimalField(decimal_places=2, default=0, max_digits=10)),
('delivery_time', models.CharField(blank=True, max_length=100)),
('is_active', models.BooleanField(default=True)),
('logo', models.ImageField(blank=True, null=True, upload_to='carriers/')),
('external_id', models.CharField(blank=True, max_length=50, null=True)),
],
),
migrations.CreateModel(
name='Product',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=200)),
('description', models.TextField(blank=True)),
('price', models.DecimalField(decimal_places=2, max_digits=10)),
('currency', models.CharField(default='czk', max_length=10)),
('stock', models.PositiveIntegerField(default=0)),
('is_active', models.BooleanField(default=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('default_carrier', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='default_for_products', to='commerce.carrier')),
],
),
]

View File

@@ -1,40 +0,0 @@
from django.db import models
class Product(models.Model):
name = models.CharField(max_length=200)
description = models.TextField(blank=True)
price = models.DecimalField(max_digits=10, decimal_places=2)
currency = models.CharField(max_length=10, default="czk")
stock = models.PositiveIntegerField(default=0)
is_active = models.BooleanField(default=True)
default_carrier = models.ForeignKey(
"Carrier", on_delete=models.SET_NULL, null=True, blank=True, related_name="default_for_products"
)
created_at = models.DateTimeField(auto_now_add=True)
@property
def available(self):
return self.is_active and self.stock > 0
def __str__(self):
return f"{self.name} ({self.price} {self.currency.upper()})"
# Dopravci a způsoby dopravy
from django.db import models
class Carrier(models.Model):
name = models.CharField(max_length=100) # název dopravce (Zásilkovna, Česká pošta…)
base_price = models.DecimalField(max_digits=10, decimal_places=2, default=0) # základní cena dopravy
delivery_time = models.CharField(max_length=100, blank=True) # např. "23 pracovní dny"
is_active = models.BooleanField(default=True)
# pole pro logo
logo = models.ImageField(upload_to="carriers/", blank=True, null=True)
# pole pro propojení s externím API (např. ID služby u Zásilkovny)
external_id = models.CharField(max_length=50, blank=True, null=True)
def __str__(self):
return f"{self.name} ({self.base_price} Kč)"

View File

@@ -1,26 +0,0 @@
from rest_framework import serializers
from .models import Carrier
class CarrierSerializer(serializers.ModelSerializer):
class Meta:
model = Carrier
fields = [
"id", "name", "base_price", "delivery_time",
"is_active", "logo", "external_id"
]
from rest_framework import serializers
from .models import Product, Carrier, Order
class ProductSerializer(serializers.ModelSerializer):
class Meta:
model = Product
fields = "__all__"
class CarrierSerializer(serializers.ModelSerializer):
class Meta:
model = Carrier
fields = "__all__"

View File

@@ -1,3 +0,0 @@
from django.shortcuts import render
# Create your views here.

View File

@@ -1,91 +0,0 @@
# -- BASE --
requests
pip
python-dotenv # .env support
virtualenv #venv
Django
numpy # NumPy je knihovna programovacího jazyka Python, která poskytuje infrastrukturu pro práci s vektory, maticemi a obecně vícerozměrnými poli.
# -- DATABASE --
sqlparse #non-validating SQL parser for Python. It provides support for parsing, splitting and formatting SQL statements.
tzdata #timezone
psycopg[binary] #PostgreSQL database adapter for the Python
django-filter
django-constance #allows you to store and manage settings of page in the Django admin interface!!!!
# -- OBJECT STORAGE --
Pillow #adds image processing capabilities to your Python interpreter
whitenoise #pomáha se spuštěním serveru a načítaní static files
django-cleanup #odstraní zbytečné media soubory které nejsou v databázi/modelu
django-storages # potřeba k S3 bucket storage
boto3
# -- PROTOCOLS (asgi, websockets) --
redis
channels_redis
channels #django channels
#channels requried package
uvicorn[standard]
daphne
gunicorn
# -- REST API --
djangorestframework #REST Framework
djangorestframework-api-key #API key
djangorestframework-simplejwt #JWT authentication for Django REST Framework
PyJWT #JSON Web Token implementation in Python
asgiref #ASGI reference implementation, to be used with Django Channels
pytz
# pytz brings the Olson tz database into Python and allows
# accurate and cross platform timezone calculations.
# It also solves the issue of ambiguous times at the end of daylight saving time.
#documentation for frontend dev
drf-spectacular
# -- APPS --
django-tinymce
django-cors-headers #csfr
celery #slouží k vytvaření asynchoních úkolu (třeba každou hodinu vyčistit cache atd.)
django-celery-beat #slouží k plánování úkolů pro Celery
# -- EDITING photos, gifs, videos --
#aiofiles
#opencv-python #moviepy use this better instead of pillow
#moviepy
yt-dlp
weasyprint #tvoření PDFek z html dokumentu + css styly
## -- MISCELLANEOUS --
faker #generates fake data for testing purposes
## -- api --
stripe
gopay

View File

@@ -1,3 +0,0 @@
from django.contrib import admin
# Register your models here.

View File

@@ -1,8 +0,0 @@
from django.apps import AppConfig
class ChatConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'social.chat'
label = "chat"

View File

@@ -1,27 +0,0 @@
# chat/consumers.py
import json
from account.models import UserProfile
from channels.db import database_sync_to_async
from channels.generic.websocket import AsyncWebsocketConsumer
class ChatConsumer(AsyncWebsocketConsumer):
async def connect(self):
await self.accept()
async def disconnect(self, close_code):
pass
async def receive(self, text_data):
text_data_json = json.loads(text_data)
message = text_data_json["message"]
await self.send(text_data=json.dumps({"message": message}))
@database_sync_to_async
def get_user_profile(user_id):
return UserProfile.objects.get(pk=user_id)

View File

@@ -1,3 +0,0 @@
from django.db import models
# Create your models here.

View File

@@ -1,8 +0,0 @@
# chat/routing.py
from django.urls import re_path
from . import consumers
websocket_urlpatterns = [
re_path(r"ws/chat/(?P<room_name>\w+)/$", consumers.ChatConsumer.as_asgi()),
]

View File

@@ -1,3 +0,0 @@
from django.test import TestCase
# Create your tests here.

View File

@@ -1,3 +0,0 @@
from django.shortcuts import render
# Create your views here.

View File

@@ -1,10 +0,0 @@
from django.contrib import admin
from .models import DownloaderRecord
@admin.register(DownloaderRecord)
class DownloaderRecordAdmin(admin.ModelAdmin):
list_display = ("id", "url", "format", "length_of_media", "file_size", "download_time")
list_filter = ("format",)
search_fields = ("url",)
ordering = ("-download_time",)
readonly_fields = ("download_time",)

View File

@@ -1,10 +0,0 @@
from django.apps import AppConfig
class DownloaderConfig(AppConfig):
# Ensure stable default primary key type
default_auto_field = "django.db.models.BigAutoField"
# Must be the full dotted path of this app
name = "thirdparty.downloader"
# Keep a short, stable label (used in migrations/admin)
label = "downloader"

View File

@@ -1,30 +0,0 @@
# Generated by Django 5.2.7 on 2025-10-29 14:53
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='DownloaderRecord',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('is_deleted', models.BooleanField(default=False)),
('deleted_at', models.DateTimeField(blank=True, null=True)),
('url', models.URLField()),
('download_time', models.DateTimeField(auto_now_add=True)),
('format', models.CharField(max_length=50)),
('length_of_media', models.IntegerField(help_text='Length of media in seconds')),
('file_size', models.BigIntegerField(help_text='File size in bytes')),
],
options={
'abstract': False,
},
),
]

View File

@@ -1,15 +0,0 @@
from django.db import models
from django.conf import settings
from vontor_cz.models import SoftDeleteModel
# 7áznamy pro donwloader, co lidé nejvíc stahujou a v jakém formátu
class DownloaderRecord(SoftDeleteModel):
url = models.URLField()
download_time = models.DateTimeField(auto_now_add=True)
format = models.CharField(max_length=50)
length_of_media = models.IntegerField(help_text="Length of media in seconds")
file_size = models.BigIntegerField(help_text="File size in bytes")

Some files were not shown because too many files have changed in this diff Show More