websockets + chat app (django)
This commit is contained in:
@@ -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