Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a966d0bc8e | ||
|
|
753339c28a |
@@ -143,6 +143,12 @@ def serve_menu_js() -> FileResponse:
|
||||
return FileResponse(static_path / "menu.js")
|
||||
|
||||
|
||||
@app.get("/auth-utils.js", include_in_schema=False)
|
||||
def serve_auth_utils_js() -> FileResponse:
|
||||
"""Serve the shared auth utilities JavaScript."""
|
||||
return FileResponse(static_path / "auth-utils.js")
|
||||
|
||||
|
||||
@app.get("/api/")
|
||||
def read_root(token: Annotated[str, Depends(verify_token)]) -> dict:
|
||||
"""API root endpoint (requires authentication)."""
|
||||
|
||||
@@ -211,12 +211,10 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/auth-utils.js"></script>
|
||||
<script>
|
||||
// Check if user is authenticated
|
||||
const token = localStorage.getItem("access_token");
|
||||
if (!token) {
|
||||
window.location.href = "/login.html";
|
||||
}
|
||||
const token = getToken();
|
||||
|
||||
// Get child ID from URL if editing
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
|
||||
@@ -424,17 +424,15 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/auth-utils.js"></script>
|
||||
<script>
|
||||
// Check if user is authenticated and is admin
|
||||
const token = localStorage.getItem("access_token");
|
||||
if (!token) {
|
||||
window.location.href = "/login.html";
|
||||
}
|
||||
const token = getToken();
|
||||
|
||||
let allUsers = [];
|
||||
let userToDelete = null;
|
||||
|
||||
// Verify user is admin and load data
|
||||
// Load data
|
||||
fetch("/api/me", {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* Redirect to login page with return URL
|
||||
* @param {string} returnUrl - The URL to return to after login (defaults to current page)
|
||||
*/
|
||||
function redirectToLogin(returnUrl) {
|
||||
const url = returnUrl || window.location.pathname + window.location.search;
|
||||
window.location.href = `/login.html?return=${encodeURIComponent(url)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user has valid token, redirect to login if not
|
||||
* @returns {string|null} - Returns token if valid, redirects to login if not
|
||||
*/
|
||||
function getToken() {
|
||||
const token = localStorage.getItem("access_token");
|
||||
if (!token) {
|
||||
redirectToLogin();
|
||||
return null;
|
||||
}
|
||||
return token;
|
||||
}
|
||||
@@ -266,12 +266,10 @@
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/chart.umd.min.js"></script>
|
||||
<script src="/menu.js"></script>
|
||||
<script src="/auth-utils.js"></script>
|
||||
<script>
|
||||
// Check if user is authenticated
|
||||
const token = localStorage.getItem("access_token");
|
||||
if (!token) {
|
||||
window.location.href = "/login.html";
|
||||
}
|
||||
const token = getToken();
|
||||
|
||||
// Initialize burger menu
|
||||
initBurgerMenu({ includeHome: true });
|
||||
@@ -281,9 +279,6 @@
|
||||
let currentDaysRange =
|
||||
parseInt(localStorage.getItem("lastDiaperTimeRange")) || 7;
|
||||
let selectedChildId = null; // null means show first/only child
|
||||
let barChartInstance = null; // Track chart instance for proper cleanup
|
||||
let poopPieChartInstance = null;
|
||||
let peePieChartInstance = null;
|
||||
|
||||
// Restore last selected child from localStorage
|
||||
const lastChildId = localStorage.getItem("lastDiaperChildFilter");
|
||||
@@ -785,12 +780,7 @@
|
||||
function renderBarChart(chartData) {
|
||||
const ctx = document.getElementById("diaperChart").getContext("2d");
|
||||
|
||||
// Destroy existing chart instance if it exists
|
||||
if (barChartInstance) {
|
||||
barChartInstance.destroy();
|
||||
}
|
||||
|
||||
barChartInstance = new Chart(ctx, {
|
||||
new Chart(ctx, {
|
||||
type: "bar",
|
||||
data: {
|
||||
labels: chartData.labels,
|
||||
@@ -883,11 +873,6 @@
|
||||
function renderPoopPieChart(chartData) {
|
||||
const ctx = document.getElementById("poopPieChart").getContext("2d");
|
||||
|
||||
// Destroy existing chart instance if it exists
|
||||
if (poopPieChartInstance) {
|
||||
poopPieChartInstance.destroy();
|
||||
}
|
||||
|
||||
const colors = {
|
||||
Light: {
|
||||
bg: "rgba(139, 69, 19, 0.5)",
|
||||
@@ -914,7 +899,7 @@
|
||||
(label) => colors[label]?.border || "rgba(200, 200, 200, 1)",
|
||||
);
|
||||
|
||||
poopPieChartInstance = new Chart(ctx, {
|
||||
new Chart(ctx, {
|
||||
type: "pie",
|
||||
data: {
|
||||
labels: chartData.labels,
|
||||
@@ -970,11 +955,6 @@
|
||||
function renderPeePieChart(chartData) {
|
||||
const ctx = document.getElementById("peePieChart").getContext("2d");
|
||||
|
||||
// Destroy existing chart instance if it exists
|
||||
if (peePieChartInstance) {
|
||||
peePieChartInstance.destroy();
|
||||
}
|
||||
|
||||
const colors = {
|
||||
Light: {
|
||||
bg: "rgba(255, 215, 0, 0.5)",
|
||||
@@ -1001,7 +981,7 @@
|
||||
(label) => colors[label]?.border || "rgba(200, 200, 200, 1)",
|
||||
);
|
||||
|
||||
peePieChartInstance = new Chart(ctx, {
|
||||
new Chart(ctx, {
|
||||
type: "pie",
|
||||
data: {
|
||||
labels: chartData.labels,
|
||||
|
||||
@@ -303,12 +303,10 @@
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/chart.umd.min.js"></script>
|
||||
<script src="/menu.js"></script>
|
||||
<script src="/auth-utils.js"></script>
|
||||
<script>
|
||||
// Check if user is authenticated
|
||||
const token = localStorage.getItem("access_token");
|
||||
if (!token) {
|
||||
window.location.href = "/login.html";
|
||||
}
|
||||
const token = getToken();
|
||||
|
||||
// Initialize burger menu
|
||||
initBurgerMenu({ includeHome: true });
|
||||
@@ -319,8 +317,6 @@
|
||||
parseInt(localStorage.getItem("lastFeedingTimeRange")) || 7;
|
||||
let selectedChildId = null; // null means show first/only child
|
||||
let barChartInstance = null;
|
||||
let pieChartInstance = null;
|
||||
let durationChartInstance = null;
|
||||
|
||||
// Restore last selected child from localStorage
|
||||
const lastChildId = localStorage.getItem("lastFeedingChildFilter");
|
||||
@@ -981,11 +977,6 @@
|
||||
function renderPieChart(chartData) {
|
||||
const ctx = document.getElementById("pieChart").getContext("2d");
|
||||
|
||||
// Destroy existing chart instance if it exists
|
||||
if (pieChartInstance) {
|
||||
pieChartInstance.destroy();
|
||||
}
|
||||
|
||||
// Color mapping for each type
|
||||
const colorMap = {
|
||||
"Left Breast": {
|
||||
@@ -1010,7 +1001,7 @@
|
||||
(label) => colorMap[label]?.border || "rgba(200, 200, 200, 1)",
|
||||
);
|
||||
|
||||
pieChartInstance = new Chart(ctx, {
|
||||
new Chart(ctx, {
|
||||
type: "pie",
|
||||
data: {
|
||||
labels: chartData.labels,
|
||||
@@ -1066,12 +1057,7 @@
|
||||
function renderDurationChart(chartData) {
|
||||
const ctx = document.getElementById("durationChart").getContext("2d");
|
||||
|
||||
// Destroy existing chart instance if it exists
|
||||
if (durationChartInstance) {
|
||||
durationChartInstance.destroy();
|
||||
}
|
||||
|
||||
durationChartInstance = new Chart(ctx, {
|
||||
new Chart(ctx, {
|
||||
type: "bar",
|
||||
data: {
|
||||
labels: chartData.labels,
|
||||
|
||||
@@ -167,52 +167,6 @@
|
||||
margin-top: 0.5rem;
|
||||
opacity: 0.9;
|
||||
}
|
||||
.recent-activity {
|
||||
margin-top: 2rem;
|
||||
padding: 1.5rem;
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
}
|
||||
.recent-activity h3 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
color: #333;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
.activity-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
.activity-card {
|
||||
background: white;
|
||||
padding: 1rem;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.activity-card .icon {
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.activity-card .type {
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
.activity-card .time {
|
||||
font-size: 0.85rem;
|
||||
color: #666;
|
||||
}
|
||||
.activity-card .details {
|
||||
font-size: 0.85rem;
|
||||
color: #888;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
.activity-card.empty {
|
||||
opacity: 0.5;
|
||||
font-style: italic;
|
||||
color: #999;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -282,32 +236,21 @@
|
||||
</div>
|
||||
|
||||
<script src="/menu.js"></script>
|
||||
<script src="/auth-utils.js"></script>
|
||||
<script>
|
||||
const token = localStorage.getItem("access_token");
|
||||
const token = getToken();
|
||||
const username = localStorage.getItem("username");
|
||||
|
||||
// Check if user is logged in
|
||||
if (!token) {
|
||||
window.location.href = "/login.html";
|
||||
} else {
|
||||
// Fetch current user info including admin status
|
||||
fetch("/api/me", {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.ok) {
|
||||
return response.json();
|
||||
} else {
|
||||
// Token invalid, redirect to login
|
||||
localStorage.removeItem("access_token");
|
||||
localStorage.removeItem("username");
|
||||
window.location.href = "/login.html";
|
||||
throw new Error("Authentication failed");
|
||||
}
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.then((user) => {
|
||||
if (!user) return; // Early exit if redirected
|
||||
|
||||
// Initialize burger menu
|
||||
initBurgerMenu({
|
||||
includeHome: true,
|
||||
@@ -334,7 +277,6 @@
|
||||
.catch((error) => {
|
||||
console.error("Error:", error);
|
||||
});
|
||||
}
|
||||
|
||||
function showNoChildrenMessage() {
|
||||
const content = document.getElementById("content");
|
||||
@@ -367,7 +309,6 @@
|
||||
let activeSleep = null;
|
||||
let lastSleep = null;
|
||||
let sleepUpdateTimerInterval = null;
|
||||
let lastDiaperChange = null;
|
||||
|
||||
async function loadFeedingStatus(children) {
|
||||
userChildren = children;
|
||||
@@ -423,20 +364,6 @@
|
||||
lastSleep = sleeps[0];
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch all diaper changes to get the last one
|
||||
const diaperResponse = await fetch("/api/diaper-changes", {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (diaperResponse.ok) {
|
||||
const diapers = await diaperResponse.json();
|
||||
if (diapers.length > 0) {
|
||||
lastDiaperChange = diapers[0];
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading feeding status:", error);
|
||||
}
|
||||
@@ -539,98 +466,9 @@
|
||||
</button>
|
||||
${feedingButtonHtml}
|
||||
${sleepButtonHtml}
|
||||
${buildRecentActivitySection()}
|
||||
`;
|
||||
}
|
||||
|
||||
function buildRecentActivitySection() {
|
||||
// Build diaper card
|
||||
const diaperCard = lastDiaperChange
|
||||
? `
|
||||
<div class="activity-card">
|
||||
<div class="icon">🧷</div>
|
||||
<div class="type">Last Diaper</div>
|
||||
<div class="time">${formatTime(lastDiaperChange.change_time)}</div>
|
||||
<div class="details">
|
||||
${lastDiaperChange.poop_amount ? `💩 ${lastDiaperChange.poop_amount}` : ""}
|
||||
${lastDiaperChange.poop_amount && lastDiaperChange.pee_amount ? " • " : ""}
|
||||
${lastDiaperChange.pee_amount ? `💧 ${lastDiaperChange.pee_amount}` : ""}
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
: `
|
||||
<div class="activity-card empty">
|
||||
<div class="icon">🧷</div>
|
||||
<div class="type">No diaper changes yet</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Build feeding card
|
||||
const feedingCard = lastFeeding
|
||||
? `
|
||||
<div class="activity-card">
|
||||
<div class="icon">🍼</div>
|
||||
<div class="type">Last Feeding</div>
|
||||
<div class="time">${formatTime(lastFeeding.start_time)}</div>
|
||||
<div class="details">
|
||||
${formatFeedingType(lastFeeding.feeding_type)}
|
||||
${lastFeeding.end_time ? ` • ${calculateDuration(lastFeeding.start_time, lastFeeding.end_time)}` : " (ongoing)"}
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
: `
|
||||
<div class="activity-card empty">
|
||||
<div class="icon">🍼</div>
|
||||
<div class="type">No feedings yet</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Build sleep card
|
||||
const sleepCard = lastSleep
|
||||
? `
|
||||
<div class="activity-card">
|
||||
<div class="icon">😴</div>
|
||||
<div class="type">Last Sleep</div>
|
||||
<div class="time">${formatTime(lastSleep.start_time)}</div>
|
||||
<div class="details">
|
||||
${lastSleep.end_time ? calculateDuration(lastSleep.start_time, lastSleep.end_time) : "ongoing"}
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
: `
|
||||
<div class="activity-card empty">
|
||||
<div class="icon">😴</div>
|
||||
<div class="type">No sleep sessions yet</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
return `
|
||||
<div class="recent-activity">
|
||||
<h3>📊 Recent Activity</h3>
|
||||
<div class="activity-grid">
|
||||
${diaperCard}
|
||||
${feedingCard}
|
||||
${sleepCard}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function calculateDuration(startTime, endTime) {
|
||||
const start = new Date(startTime);
|
||||
const end = new Date(endTime);
|
||||
const diffMs = end - start;
|
||||
const diffMins = Math.floor(diffMs / 60000);
|
||||
|
||||
if (diffMins < 60) {
|
||||
return `${diffMins} min`;
|
||||
}
|
||||
|
||||
const hours = Math.floor(diffMins / 60);
|
||||
const mins = diffMins % 60;
|
||||
return `${hours}h ${mins}m`;
|
||||
}
|
||||
|
||||
function formatFeedingType(type) {
|
||||
const types = {
|
||||
left_breast: "Left Breast",
|
||||
|
||||
@@ -242,12 +242,10 @@
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<script src="/auth-utils.js"></script>
|
||||
<script>
|
||||
// Check if user is authenticated
|
||||
const token = localStorage.getItem("access_token");
|
||||
if (!token) {
|
||||
window.location.href = "/login.html";
|
||||
}
|
||||
const token = getToken();
|
||||
|
||||
// Determine return URL based on referrer
|
||||
function getReturnUrl() {
|
||||
|
||||
@@ -199,12 +199,10 @@
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<script src="/auth-utils.js"></script>
|
||||
<script>
|
||||
// Check if user is authenticated
|
||||
const token = localStorage.getItem("access_token");
|
||||
if (!token) {
|
||||
window.location.href = "/login.html";
|
||||
}
|
||||
const token = getToken();
|
||||
|
||||
// Determine return URL based on referrer
|
||||
function getReturnUrl() {
|
||||
|
||||
@@ -189,12 +189,10 @@
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<script src="/auth-utils.js"></script>
|
||||
<script>
|
||||
// Check if user is authenticated
|
||||
const token = localStorage.getItem("access_token");
|
||||
if (!token) {
|
||||
window.location.href = "/login.html";
|
||||
}
|
||||
const token = getToken();
|
||||
|
||||
// Determine return URL based on referrer
|
||||
function getReturnUrl() {
|
||||
|
||||
@@ -82,10 +82,36 @@
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Check if redirected due to expired session
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const returnUrl = params.get("return");
|
||||
|
||||
const form = document.getElementById("loginForm");
|
||||
const messageDiv = document.getElementById("message");
|
||||
const loginButton = document.getElementById("loginButton");
|
||||
|
||||
// Show session expired message if redirected from another page
|
||||
if (returnUrl) {
|
||||
showMessage("Your session has expired. Please log in again.", "error");
|
||||
}
|
||||
|
||||
// Clear user-specific cache data on login
|
||||
function clearUserCache() {
|
||||
const keysToRemove = [
|
||||
'lastDiaperChildFilter',
|
||||
'lastFeedingChildFilter',
|
||||
'lastSleepChildFilter',
|
||||
'lastDiaperTimeRange',
|
||||
'lastFeedingTimeRange'
|
||||
];
|
||||
|
||||
keysToRemove.forEach(key => {
|
||||
localStorage.removeItem(key);
|
||||
});
|
||||
|
||||
console.log('Cleared user cache on login');
|
||||
}
|
||||
|
||||
form.addEventListener("submit", async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
@@ -109,13 +135,23 @@
|
||||
|
||||
if (response.ok) {
|
||||
showMessage(data.message, "success");
|
||||
|
||||
// Store token in localStorage
|
||||
localStorage.setItem("access_token", data.access_token);
|
||||
localStorage.setItem("username", data.username);
|
||||
|
||||
// Redirect based on is_admin from login response
|
||||
// Clear stale user preferences from previous sessions
|
||||
clearUserCache();
|
||||
|
||||
// Check for return URL parameter
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const returnUrl = params.get("return");
|
||||
|
||||
// Redirect based on return URL or is_admin from login response
|
||||
setTimeout(() => {
|
||||
if (data.is_admin === true) {
|
||||
if (returnUrl) {
|
||||
window.location.href = returnUrl;
|
||||
} else if (data.is_admin === true) {
|
||||
window.location.href = "/admin.html";
|
||||
} else {
|
||||
window.location.href = "/";
|
||||
|
||||
@@ -469,11 +469,9 @@
|
||||
</div>
|
||||
|
||||
<script src="/menu.js"></script>
|
||||
<script src="/auth-utils.js"></script>
|
||||
<script>
|
||||
const token = localStorage.getItem("access_token");
|
||||
if (!token) {
|
||||
window.location.href = "/login.html";
|
||||
}
|
||||
const token = getToken();
|
||||
|
||||
// Initialize burger menu
|
||||
initBurgerMenu({ includeHome: true });
|
||||
@@ -495,7 +493,7 @@
|
||||
currentUser = await userResponse.json();
|
||||
document.getElementById("username").value = currentUser.username;
|
||||
} else {
|
||||
window.location.href = "/login.html";
|
||||
redirectToLogin();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -852,8 +850,10 @@
|
||||
}
|
||||
});
|
||||
|
||||
// Load data on page load
|
||||
// Load data on page load (only if authenticated)
|
||||
if (token) {
|
||||
loadData();
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -313,13 +313,9 @@
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/chart.umd.min.js"></script>
|
||||
<script src="/menu.js"></script>
|
||||
<script src="/auth-utils.js"></script>
|
||||
<script>
|
||||
const token = localStorage.getItem("access_token");
|
||||
|
||||
// Check if user is logged in
|
||||
if (!token) {
|
||||
window.location.href = "/login.html";
|
||||
}
|
||||
const token = getToken();
|
||||
|
||||
// Initialize burger menu
|
||||
initBurgerMenu({ includeHome: true });
|
||||
|
||||
Reference in New Issue
Block a user