feat: enable cookie auth flow and database url runtime config
This commit is contained in:
parent
119994b602
commit
aa9488755f
11
backend/.env.example
Normal file
11
backend/.env.example
Normal 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
|
||||||
@ -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"],
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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;
|
||||||
|
};
|
||||||
|
|||||||
@ -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.");
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user