Merge branch 'bruno' of https://git.vontor.cz/Brunobrno/vontor-cz into bruno
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
# 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),
|
||||
),
|
||||
]
|
||||
@@ -122,3 +122,31 @@ def send_email_test_task(email):
|
||||
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,
|
||||
)
|
||||
@@ -1,3 +1,28 @@
|
||||
from django.test import TestCase
|
||||
from django.contrib.auth import get_user_model
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
# Create your tests here.
|
||||
|
||||
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)}")
|
||||
|
||||
@@ -229,12 +229,16 @@ class UserView(viewsets.ModelViewSet):
|
||||
|
||||
# Only admin or the user themselves can update or delete
|
||||
elif self.action in ['update', 'partial_update', 'destroy']:
|
||||
if self.request.user.role == 'admin':
|
||||
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")()]
|
||||
elif self.kwargs.get('pk') and str(self.request.user.id) == self.kwargs['pk']:
|
||||
|
||||
# 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()]
|
||||
else:
|
||||
# fallback - deny access
|
||||
|
||||
# Fallback - deny access (prevents AttributeError for AnonymousUser)
|
||||
return [OnlyRolesAllowed("admin")()]
|
||||
|
||||
# Any authenticated user can retrieve (view) any user's profile
|
||||
|
||||
0
backend/social/chat/__init__.py
Normal file
0
backend/social/chat/__init__.py
Normal file
3
backend/social/chat/admin.py
Normal file
3
backend/social/chat/admin.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
8
backend/social/chat/apps.py
Normal file
8
backend/social/chat/apps.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class ChatConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'social.chat'
|
||||
|
||||
label = "chat"
|
||||
27
backend/social/chat/consumers.py
Normal file
27
backend/social/chat/consumers.py
Normal file
@@ -0,0 +1,27 @@
|
||||
# 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)
|
||||
0
backend/social/chat/migrations/__init__.py
Normal file
0
backend/social/chat/migrations/__init__.py
Normal file
3
backend/social/chat/models.py
Normal file
3
backend/social/chat/models.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.db import models
|
||||
|
||||
# Create your models here.
|
||||
8
backend/social/chat/routing.py
Normal file
8
backend/social/chat/routing.py
Normal file
@@ -0,0 +1,8 @@
|
||||
# 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()),
|
||||
]
|
||||
3
backend/social/chat/tests.py
Normal file
3
backend/social/chat/tests.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
3
backend/social/chat/views.py
Normal file
3
backend/social/chat/views.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.shortcuts import render
|
||||
|
||||
# Create your views here.
|
||||
@@ -327,6 +327,8 @@ MY_CREATED_APPS = [
|
||||
'account',
|
||||
'commerce',
|
||||
|
||||
'social.chat',
|
||||
|
||||
'thirdparty.downloader',
|
||||
'thirdparty.stripe', # register Stripe app so its models are recognized
|
||||
'thirdparty.trading212',
|
||||
|
||||
@@ -3,17 +3,31 @@ 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>
|
||||
<Routes>
|
||||
<UserContextProvider>
|
||||
|
||||
{/* Layout route */}
|
||||
<Route path="/" element={<HomeLayout />}>
|
||||
<Route index element={<Home />} />
|
||||
<Route path="downloader" element={<Downloader />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
|
||||
<Route element={<PrivateRoute />}>
|
||||
{/* Protected routes go here */}
|
||||
<Route path="/" element={<HomeLayout />} >
|
||||
<Route path="protected-downloader" element={<Downloader />} />
|
||||
</Route>
|
||||
|
||||
</Route>
|
||||
|
||||
</UserContextProvider>
|
||||
</Router>
|
||||
)
|
||||
}
|
||||
@@ -3,8 +3,7 @@ import axios from "axios";
|
||||
// --- ENV CONFIG ---
|
||||
const API_BASE_URL =
|
||||
import.meta.env.VITE_API_BASE_URL || "http://localhost:8000";
|
||||
const REFRESH_URL =
|
||||
import.meta.env.VITE_API_REFRESH_URL || "/api/token/refresh/";
|
||||
|
||||
const LOGIN_PATH = import.meta.env.VITE_LOGIN_PATH || "/login";
|
||||
|
||||
|
||||
@@ -30,6 +29,7 @@ function notifyError(detail: ApiErrorDetail) {
|
||||
function onError(handler: ApiErrorHandler) {
|
||||
const wrapped = handler as EventListener;
|
||||
window.addEventListener(ERROR_EVENT, wrapped as EventListener);
|
||||
|
||||
return () => window.removeEventListener(ERROR_EVENT, wrapped);
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ function onError(handler: ApiErrorHandler) {
|
||||
function createAxios(baseURL: string): any {
|
||||
const instance = axios.create({
|
||||
baseURL,
|
||||
withCredentials: true, // <-- always true
|
||||
withCredentials: true, // cookies
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
@@ -51,12 +51,14 @@ function createAxios(baseURL: string): any {
|
||||
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;
|
||||
});
|
||||
|
||||
@@ -64,6 +66,7 @@ apiPublic.interceptors.request.use(function (config: any) {
|
||||
// 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;
|
||||
});
|
||||
|
||||
@@ -76,16 +79,18 @@ apiAuth.interceptors.response.use(
|
||||
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(); // optional: clear cookies client-side
|
||||
ClearTokens();
|
||||
window.location.assign(LOGIN_PATH);
|
||||
return Promise.reject(error);
|
||||
}
|
||||
@@ -130,35 +135,37 @@ apiPublic.interceptors.response.use(
|
||||
}
|
||||
);
|
||||
|
||||
// --- TOKEN HELPERS (NO-OPS) ---
|
||||
// Django sets/rotates cookies server-side. Keep API surface to avoid breaking imports.
|
||||
function setTokens(_access?: string, _refresh?: string) {
|
||||
// no-op: cookies are managed by Django
|
||||
}
|
||||
function clearTokens() {
|
||||
// optional: try to clear auth cookies client-side; server should also clear on logout
|
||||
|
||||
|
||||
|
||||
function Logout() {
|
||||
try {
|
||||
document.cookie = "access_token=; Max-Age=0; path=/";
|
||||
document.cookie = "refresh_token=; Max-Age=0; path=/";
|
||||
} catch {
|
||||
// ignore
|
||||
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 getAccessToken(): string | null {
|
||||
// no Authorization header is used; rely purely on cookies
|
||||
return null;
|
||||
|
||||
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,
|
||||
|
||||
// Token helpers (kept for compatibility; now no-ops)
|
||||
setTokens,
|
||||
clearTokens,
|
||||
getAccessToken,
|
||||
Logout,
|
||||
|
||||
// Error subscription
|
||||
onError,
|
||||
|
||||
82
frontend/src/api/models/User.ts
Normal file
82
frontend/src/api/models/User.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
// 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;
|
||||
19
frontend/src/api/websockets/WebSocketClient.ts
Normal file
19
frontend/src/api/websockets/WebSocketClient.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
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);
|
||||
};
|
||||
@@ -1,12 +1,16 @@
|
||||
import React, { useState } from "react"
|
||||
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
|
||||
|
||||
30
frontend/src/context/Context.md
Normal file
30
frontend/src/context/Context.md
Normal file
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
# 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 ...;
|
||||
}
|
||||
```
|
||||
0
frontend/src/context/SettingsContext.tsx
Normal file
0
frontend/src/context/SettingsContext.tsx
Normal file
74
frontend/src/context/UserContext.tsx
Normal file
74
frontend/src/context/UserContext.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
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
|
||||
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 ...;
|
||||
}
|
||||
|
||||
*/
|
||||
Reference in New Issue
Block a user