fix(dev): keep logins on the app's own address

The admin and login links pointed at VITE_API_URL (http://localhost:3000)
while the app runs at http://127.0.0.1:5173. Browsers keep cookies for the
two apart, so logging in there left the app logged out. In development these
links now go through Vite's proxy, as GraphQL requests already did.

Logging in through the proxy also failed Django's CSRF origin check, because
the proxy rewrote the Host header. Keep the app's Host for the local backend;
a remote VITE_DEV_API_PROXY still gets its own.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
leanderandClaude Opus 5.5 committed 2026-11-05 03:44:07 +01:00
1 parent 3a98d62226
commit 31eb71b354
5 files changed
+22 -6

No files matched your search

+3 -1
View File
@@ -1,7 +1,9 @@
import { ApolloClient, HttpLink, InMemoryCache } from "@apollo/client"; import { ApolloClient, HttpLink, InMemoryCache } from "@apollo/client";
import { apiUrl } from "./utils/api";
const client = new ApolloClient({ const client = new ApolloClient({
link: new HttpLink({ uri: import.meta.env.DEV ? "/graphql/" : `${import.meta.env.VITE_API_URL}/graphql/` }), link: new HttpLink({ uri: apiUrl("/graphql/") }),
cache: new InMemoryCache(), cache: new InMemoryCache(),
}); });
+2 -1
View File
@@ -3,8 +3,9 @@ import React from "react";
import { NavLink } from "react-router-dom"; import { NavLink } from "react-router-dom";
import styles from "./Header.module.css"; import styles from "./Header.module.css";
import { apiUrl } from "../utils/api";
const ADMIN_URL = `${import.meta.env.VITE_API_URL}/admin/`; const ADMIN_URL = apiUrl("/admin/");
const navLinkStyles = ({ isActive }: { isActive: boolean }) => clsx(styles.navLink, { [styles.active]: isActive }); const navLinkStyles = ({ isActive }: { isActive: boolean }) => clsx(styles.navLink, { [styles.active]: isActive });
@@ -6,8 +6,9 @@ import styles from "./Inventory.module.css";
import { GQLError } from "../../components/GQLError"; import { GQLError } from "../../components/GQLError";
import { useToast } from "../../hooks/useToast"; import { useToast } from "../../hooks/useToast";
import { CreateInventoryTransactionDocument, InventoryProductsDocument } from "../../types"; import { CreateInventoryTransactionDocument, InventoryProductsDocument } from "../../types";
import { loginUrl } from "../../utils/api";
const LOGIN_URL = `${import.meta.env.VITE_API_URL}/accounts/login/?next=/inventory/add`; const LOGIN_URL = loginUrl("/inventory/add");
const InventoryAdd = () => { const InventoryAdd = () => {
const navigate = useNavigate(); const navigate = useNavigate();
+6
View File
@@ -0,0 +1,6 @@
// URL of a backend path such as /graphql/. In development this goes through Vite's proxy on the app's
// own address (see vite.config.ts), so the login cookie Django sets belongs to the host the app runs on;
// browsers keep cookies for localhost and 127.0.0.1 apart. Builds use VITE_API_URL.
export const apiUrl = (path: string) => (import.meta.env.DEV ? path : `${import.meta.env.VITE_API_URL}${path}`);
export const loginUrl = (next: string) => apiUrl(`/accounts/login/?next=${encodeURIComponent(next)}`);
+9 -3
View File
@@ -13,9 +13,15 @@ export default defineConfig({
server: { server: {
host: "127.0.0.1", host: "127.0.0.1",
proxy: Object.fromEntries( proxy: Object.fromEntries(
["/admin", "/accounts", "/static", "/media", "/graphql"].map( ["/admin", "/accounts", "/static", "/media", "/graphql"].map((path) => [
(path) => [path, process.env.VITE_DEV_API_PROXY || "http://127.0.0.1:3000"], path,
), {
target: process.env.VITE_DEV_API_PROXY || "http://127.0.0.1:3000",
// Django only accepts a login form posted from the Host it was sent to (CSRF), so keep the
// app's own Host for the local backend. A remote backend needs its own Host to be reached.
changeOrigin: !!process.env.VITE_DEV_API_PROXY,
},
]),
), ),
}, },
build: { outDir: "../static", emptyOutDir: false }, build: { outDir: "../static", emptyOutDir: false },