diff --git a/.env.example b/.env.example index 6a2c14d..9d53215 100644 --- a/.env.example +++ b/.env.example @@ -7,7 +7,7 @@ TZ=UTC # Copy this file to .env and customize # Image version to run (set by build.sh, or use 'latest') -JAMA_VERSION=0.7.0 +JAMA_VERSION=0.7.1 # Default admin credentials (used on FIRST RUN only) ADMIN_NAME=Admin User diff --git a/backend/package.json b/backend/package.json index b0e17d4..45dd5d9 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,6 @@ { "name": "jama-backend", - "version": "0.7.0", + "version": "0.7.1", "description": "TeamChat backend server", "main": "src/index.js", "scripts": { diff --git a/backend/src/data/help.md b/backend/src/data/help.md index 58e1b4c..f257936 100644 --- a/backend/src/data/help.md +++ b/backend/src/data/help.md @@ -7,7 +7,7 @@ Welcome to **JAMA** — your private, self-hosted team messaging app. ## Security ### 🛡️ Your Privacy Assured -The only people that can read your direct messages (person 2 person or group) are the members of the message group. No one else, including admins, know which message groups exist or which you are part of, unless they are a member. +The only people that can read your direct messages (**person 2 person** or **group**) are the members of the message group. No one else, including admins, know which message groups exist or which you are part of, unless they are a member. **Every user**, at minimum, can read all public messages. @@ -50,11 +50,14 @@ Hover over any message and click the **emoji** button to react with an emoji. Two ways to start a private conversation with one person: +_**New Chat Button**_ 1. Click the **New Chat** icon in the sidebar 2. Select one user from the list 3. Click **Start Conversation** -4. Click the users avatar in a message to bring up the profile -5. Click **Direct Message** + +_**Message Window**_ +1. Click the users avatar in a message window to bring up the profile +2. Click **Direct Message** --- diff --git a/backend/src/index.js b/backend/src/index.js index 2b187ec..b3ea298 100644 --- a/backend/src/index.js +++ b/backend/src/index.js @@ -30,7 +30,7 @@ app.use(cookieParser()); app.use('/uploads', express.static('/app/uploads')); // API Routes -app.use('/api/auth', require('./routes/auth')); +app.use('/api/auth', require('./routes/auth')(io)); app.use('/api/users', require('./routes/users')); app.use('/api/groups', require('./routes/groups')(io)); app.use('/api/messages', require('./routes/messages')); diff --git a/backend/src/routes/auth.js b/backend/src/routes/auth.js index 8a1ace5..6bd2792 100644 --- a/backend/src/routes/auth.js +++ b/backend/src/routes/auth.js @@ -1,9 +1,11 @@ const express = require('express'); const bcrypt = require('bcryptjs'); -const router = express.Router(); const { getDb, getOrCreateSupportGroup } = require('../models/db'); const { generateToken, authMiddleware, setActiveSession, clearActiveSession } = require('../middleware/auth'); +module.exports = function(io) { +const router = express.Router(); + // Login router.post('/login', (req, res) => { const { email, password, rememberMe } = req.body; @@ -27,6 +29,10 @@ router.post('/login', (req, res) => { const token = generateToken(user.id); const ua = req.headers['user-agent'] || ''; const device = setActiveSession(user.id, token, ua); // displaces prior session on same device class + // Kick any live socket on the same device class — it now holds a stale token + if (io) { + io.to(`user:${user.id}`).emit('session:displaced', { device }); + } const { password: _, ...userSafe } = user; res.json({ @@ -102,4 +108,5 @@ ${message.trim()}`; res.json({ success: true }); }); -module.exports = router; + return router; +}; diff --git a/build.sh b/build.sh index 2e55e63..f4e211f 100644 --- a/build.sh +++ b/build.sh @@ -13,7 +13,7 @@ # ───────────────────────────────────────────────────────────── set -euo pipefail -VERSION="${1:-0.7.0}" +VERSION="${1:-0.7.1}" ACTION="${2:-}" REGISTRY="${REGISTRY:-}" IMAGE_NAME="jama" diff --git a/frontend/package.json b/frontend/package.json index 93a1f86..f41d118 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "jama-frontend", - "version": "0.7.0", + "version": "0.7.1", "private": true, "scripts": { "dev": "vite", diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index df4f2e9..d9a022c 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -20,11 +20,20 @@ function ProtectedRoute({ children }) { function AuthRoute({ children }) { const { user, loading, mustChangePassword } = useAuth(); + // Always show login in light mode regardless of user's saved theme preference + document.documentElement.setAttribute('data-theme', 'light'); if (loading) return null; if (user && !mustChangePassword) return ; return children; } +function RestoreTheme() { + // Called when entering a protected route — restore the user's saved theme + const saved = localStorage.getItem('jama-theme') || 'light'; + document.documentElement.setAttribute('data-theme', saved); + return null; +} + export default function App() { return ( @@ -34,7 +43,7 @@ export default function App() { } /> } /> - } /> + } /> } /> diff --git a/frontend/src/main.jsx b/frontend/src/main.jsx index 841ab1b..7e3b45e 100644 --- a/frontend/src/main.jsx +++ b/frontend/src/main.jsx @@ -3,10 +3,6 @@ import ReactDOM from 'react-dom/client'; import App from './App.jsx'; import './index.css'; -// Apply saved theme immediately to avoid flash of wrong theme -const savedTheme = localStorage.getItem('jama-theme') || 'light'; -document.documentElement.setAttribute('data-theme', savedTheme); - // Register service worker if ('serviceWorker' in navigator) { window.addEventListener('load', () => { diff --git a/frontend/src/pages/Chat.jsx b/frontend/src/pages/Chat.jsx index 4770d2a..2bb7536 100644 --- a/frontend/src/pages/Chat.jsx +++ b/frontend/src/pages/Chat.jsx @@ -194,9 +194,21 @@ export default function Chat() { }); }; + // Session displaced: another login on the same device type kicked us out + const handleSessionDisplaced = ({ device: displacedDevice }) => { + // Only act if it's our device slot that was taken over + // (The server emits to user room so all sockets of this user receive it; + // our socket's device is embedded in the socket but we can't read it here, + // so we force logout unconditionally — the new session will reconnect cleanly) + localStorage.removeItem('tc_token'); + sessionStorage.removeItem('tc_token'); + window.dispatchEvent(new CustomEvent('jama:session-displaced')); + }; + socket.on('group:new', handleGroupNew); socket.on('group:deleted', handleGroupDeleted); socket.on('group:updated', handleGroupUpdated); + socket.on('session:displaced', handleSessionDisplaced); // Bug B fix: on reconnect, reload groups to catch any messages missed while offline const handleReconnect = () => { loadGroups(); }; @@ -217,6 +229,7 @@ export default function Chat() { socket.off('group:deleted', handleGroupDeleted); socket.off('group:updated', handleGroupUpdated); socket.off('connect', handleReconnect); + socket.off('session:displaced', handleSessionDisplaced); document.removeEventListener('visibilitychange', handleVisibility); }; }, [socket, toast, activeGroupId, user, isMobile, loadGroups]);