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]);