websockets + chat app (django)

This commit is contained in:
David Bruno Vontor
2025-10-31 13:32:39 +01:00
parent 8dd4f6e731
commit 4791bbc92c
22 changed files with 398 additions and 31 deletions

View File

@@ -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,

View 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;

View 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);
};