feat: enable cookie auth flow and database url runtime config

This commit is contained in:
Nico 2026-02-16 01:49:03 -08:00
parent 119994b602
commit aa9488755f
7 changed files with 139 additions and 79 deletions

11
backend/.env.example Normal file
View File

@ -0,0 +1,11 @@
DATABASE_URL=postgres://username:password@db-host:5432/database_name
DB_USER=
DB_PASS=
DB_HOST=
DB_PORT=5432
DB_NAME=
PORT=5000
JWT_SECRET=change-me
ALLOWED_ORIGINS=http://localhost:3000
SESSION_COOKIE_NAME=sid
SESSION_TTL_DAYS=30

View File

@ -12,7 +12,10 @@ app.use(express.json());
// Serve static files from public directory // Serve static files from public directory
app.use('/test', express.static(path.join(__dirname, 'public'))); app.use('/test', express.static(path.join(__dirname, 'public')));
const allowedOrigins = process.env.ALLOWED_ORIGINS.split(",").map(origin => origin.trim()); const allowedOrigins = (process.env.ALLOWED_ORIGINS || "")
.split(",")
.map((origin) => origin.trim())
.filter(Boolean);
app.use( app.use(
cors({ cors({
origin: function (origin, callback) { origin: function (origin, callback) {
@ -20,10 +23,12 @@ app.use(
if (allowedOrigins.includes(origin)) return callback(null, true); if (allowedOrigins.includes(origin)) return callback(null, true);
if (/^http:\/\/192\.168\.\d+\.\d+/.test(origin)) return callback(null, true); if (/^http:\/\/192\.168\.\d+\.\d+/.test(origin)) return callback(null, true);
if (/^https:\/\/192\.168\.\d+\.\d+/.test(origin)) return callback(null, true); if (/^https:\/\/192\.168\.\d+\.\d+/.test(origin)) return callback(null, true);
console.error(`🚫 CORS blocked origin: ${origin}`); console.error(`CORS blocked origin: ${origin}`);
callback(new Error(`CORS blocked: ${origin}. Add this origin to ALLOWED_ORIGINS environment variable.`)); callback(new Error(`CORS blocked: ${origin}. Add this origin to ALLOWED_ORIGINS environment variable.`));
}, },
methods: ["GET", "POST", "PUT", "DELETE", "PATCH"], methods: ["GET", "POST", "PUT", "DELETE", "PATCH"],
credentials: true,
exposedHeaders: ["X-Request-Id"],
}) })
); );

View File

@ -1,11 +1,21 @@
const { Pool } = require("pg"); const { Pool } = require("pg");
const pool = new Pool({ function buildPoolConfig() {
if (process.env.DATABASE_URL) {
return {
connectionString: process.env.DATABASE_URL,
};
}
return {
user: process.env.DB_USER, user: process.env.DB_USER,
password: process.env.DB_PASS, password: process.env.DB_PASS,
host: process.env.DB_HOST, host: process.env.DB_HOST,
database: process.env.DB_NAME, database: process.env.DB_NAME,
port: 5432, port: Number(process.env.DB_PORT || 5432),
}); };
}
const pool = new Pool(buildPoolConfig());
module.exports = pool; module.exports = pool;

View File

@ -9,3 +9,8 @@ export const registerRequest = async (username, password, name) => {
const res = await api.post("/auth/register", { username, password, name }); const res = await api.post("/auth/register", { username, password, name });
return res.data; return res.data;
}; };
export const logoutRequest = async () => {
const res = await api.post("/auth/logout");
return res.data;
};

View File

@ -3,6 +3,7 @@ import { API_BASE_URL } from "../config";
const api = axios.create({ const api = axios.create({
baseURL: API_BASE_URL, baseURL: API_BASE_URL,
withCredentials: true,
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
@ -40,8 +41,16 @@ api.interceptors.response.use(
payload.message = payload.error.message; payload.message = payload.error.message;
} }
if (error.response?.status === 401 && if (
normalizedMessage === "Invalid or expired token") { error.response?.status === 401 &&
window.location.pathname !== "/login" &&
window.location.pathname !== "/register" &&
[
"Invalid or expired token",
"Invalid or expired session",
"Missing authentication",
].includes(normalizedMessage)
) {
localStorage.removeItem("token"); localStorage.removeItem("token");
window.location.href = "/login"; window.location.href = "/login";
alert("Your session has expired. Please log in again."); alert("Your session has expired. Please log in again.");

View File

@ -2,6 +2,7 @@ import "../../styles/components/Navbar.css";
import { useContext, useState } from "react"; import { useContext, useState } from "react";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { logoutRequest } from "../../api/auth";
import { AuthContext } from "../../context/AuthContext"; import { AuthContext } from "../../context/AuthContext";
import HouseholdSwitcher from "../household/HouseholdSwitcher"; import HouseholdSwitcher from "../household/HouseholdSwitcher";
@ -15,6 +16,18 @@ export default function Navbar() {
setShowUserMenu(false); setShowUserMenu(false);
}; };
const handleLogout = async () => {
try {
await logoutRequest();
} catch (_) {
// Clear local auth state even if server logout fails.
} finally {
logout();
closeMenus();
window.location.href = "/login";
}
};
return ( return (
<nav className="navbar"> <nav className="navbar">
{/* Left: Navigation Menu */} {/* Left: Navigation Menu */}
@ -72,7 +85,7 @@ export default function Navbar() {
<span className="user-dropdown-username">{username}</span> <span className="user-dropdown-username">{username}</span>
<span className="user-dropdown-role">{role}</span> <span className="user-dropdown-role">{role}</span>
</div> </div>
<button className="user-dropdown-logout" onClick={() => { logout(); closeMenus(); }}> <button className="user-dropdown-logout" onClick={handleLogout}>
Logout Logout
</button> </button>
</div> </div>

View File

@ -15,6 +15,13 @@ export const AuthProvider = ({ children }) => {
const [role, setRole] = useState(localStorage.getItem('role') || null); const [role, setRole] = useState(localStorage.getItem('role') || null);
const [username, setUsername] = useState(localStorage.getItem('username') || null); const [username, setUsername] = useState(localStorage.getItem('username') || null);
const clearAuthStorage = () => {
localStorage.removeItem("token");
localStorage.removeItem("userId");
localStorage.removeItem("role");
localStorage.removeItem("username");
};
const login = (data) => { const login = (data) => {
localStorage.setItem('token', data.token); localStorage.setItem('token', data.token);
localStorage.setItem('userId', data.userId); localStorage.setItem('userId', data.userId);
@ -27,7 +34,7 @@ export const AuthProvider = ({ children }) => {
}; };
const logout = () => { const logout = () => {
localStorage.clear(); clearAuthStorage();
setToken(null); setToken(null);
setUserId(null); setUserId(null);