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

@ -8,51 +8,56 @@ const { sendError } = require("./utils/http");
const app = express();
app.use(requestIdMiddleware);
app.use(express.json());
// Serve static files from public directory
app.use('/test', express.static(path.join(__dirname, 'public')));
const allowedOrigins = process.env.ALLOWED_ORIGINS.split(",").map(origin => origin.trim());
// Serve static files from public directory
app.use('/test', express.static(path.join(__dirname, 'public')));
const allowedOrigins = (process.env.ALLOWED_ORIGINS || "")
.split(",")
.map((origin) => origin.trim())
.filter(Boolean);
app.use(
cors({
origin: function (origin, callback) {
if (!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 (/^https:\/\/192\.168\.\d+\.\d+/.test(origin)) return callback(null, true);
console.error(`🚫 CORS blocked origin: ${origin}`);
callback(new Error(`CORS blocked: ${origin}. Add this origin to ALLOWED_ORIGINS environment variable.`));
},
methods: ["GET", "POST", "PUT", "DELETE", "PATCH"],
})
);
origin: function (origin, callback) {
if (!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 (/^https:\/\/192\.168\.\d+\.\d+/.test(origin)) return callback(null, true);
console.error(`CORS blocked origin: ${origin}`);
callback(new Error(`CORS blocked: ${origin}. Add this origin to ALLOWED_ORIGINS environment variable.`));
},
methods: ["GET", "POST", "PUT", "DELETE", "PATCH"],
credentials: true,
exposedHeaders: ["X-Request-Id"],
})
);
app.get('/', async (req, res) => {
res.status(200).json({
message: "Grocery List API is running.",
roles: Object.values(User.ROLES),
});
});
const authRoutes = require("./routes/auth.routes");
app.use("/auth", authRoutes);
const listRoutes = require("./routes/list.routes");
app.use("/list", listRoutes);
const adminRoutes = require("./routes/admin.routes");
app.use("/admin", adminRoutes);
const usersRoutes = require("./routes/users.routes");
app.use("/users", usersRoutes);
const configRoutes = require("./routes/config.routes");
app.use("/config", configRoutes);
const householdsRoutes = require("./routes/households.routes");
app.use("/households", householdsRoutes);
const authRoutes = require("./routes/auth.routes");
app.use("/auth", authRoutes);
const listRoutes = require("./routes/list.routes");
app.use("/list", listRoutes);
const adminRoutes = require("./routes/admin.routes");
app.use("/admin", adminRoutes);
const usersRoutes = require("./routes/users.routes");
app.use("/users", usersRoutes);
const configRoutes = require("./routes/config.routes");
app.use("/config", configRoutes);
const householdsRoutes = require("./routes/households.routes");
app.use("/households", householdsRoutes);
const storesRoutes = require("./routes/stores.routes");
app.use("/stores", storesRoutes);

View File

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

View File

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

View File

@ -1,11 +1,12 @@
import axios from "axios";
import { API_BASE_URL } from "../config";
const api = axios.create({
baseURL: API_BASE_URL,
headers: {
"Content-Type": "application/json",
},
const api = axios.create({
baseURL: API_BASE_URL,
withCredentials: true,
headers: {
"Content-Type": "application/json",
},
});
api.interceptors.request.use((config => {
@ -40,8 +41,16 @@ api.interceptors.response.use(
payload.message = payload.error.message;
}
if (error.response?.status === 401 &&
normalizedMessage === "Invalid or expired token") {
if (
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");
window.location.href = "/login";
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 { Link } from "react-router-dom";
import { logoutRequest } from "../../api/auth";
import { AuthContext } from "../../context/AuthContext";
import HouseholdSwitcher from "../household/HouseholdSwitcher";
@ -15,6 +16,18 @@ export default function Navbar() {
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 (
<nav className="navbar">
{/* Left: Navigation Menu */}
@ -72,7 +85,7 @@ export default function Navbar() {
<span className="user-dropdown-username">{username}</span>
<span className="user-dropdown-role">{role}</span>
</div>
<button className="user-dropdown-logout" onClick={() => { logout(); closeMenus(); }}>
<button className="user-dropdown-logout" onClick={handleLogout}>
Logout
</button>
</div>
@ -81,4 +94,4 @@ export default function Navbar() {
</div>
</nav>
);
}
}

View File

@ -9,28 +9,35 @@ export const AuthContext = createContext({
logout: () => { },
});
export const AuthProvider = ({ children }) => {
const [token, setToken] = useState(localStorage.getItem('token') || null);
const [userId, setUserId] = useState(localStorage.getItem('userId') || null);
const [role, setRole] = useState(localStorage.getItem('role') || null);
const [username, setUsername] = useState(localStorage.getItem('username') || null);
export const AuthProvider = ({ children }) => {
const [token, setToken] = useState(localStorage.getItem('token') || null);
const [userId, setUserId] = useState(localStorage.getItem('userId') || null);
const [role, setRole] = useState(localStorage.getItem('role') || 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) => {
localStorage.setItem('token', data.token);
localStorage.setItem('userId', data.userId);
localStorage.setItem('role', data.role);
localStorage.setItem('username', data.username);
setToken(data.token);
setUserId(data.userId);
setRole(data.role);
setUsername(data.username);
};
const logout = () => {
localStorage.clear();
setToken(null);
setUserId(null);
const login = (data) => {
localStorage.setItem('token', data.token);
localStorage.setItem('userId', data.userId);
localStorage.setItem('role', data.role);
localStorage.setItem('username', data.username);
setToken(data.token);
setUserId(data.userId);
setRole(data.role);
setUsername(data.username);
};
const logout = () => {
clearAuthStorage();
setToken(null);
setUserId(null);
setRole(null);
setUsername(null);
};
@ -49,4 +56,4 @@ export const AuthProvider = ({ children }) => {
{children}
</AuthContext.Provider>
);
};
};