v0.7.1 minor bug fixes
This commit is contained in:
@@ -7,7 +7,7 @@ TZ=UTC
|
|||||||
# Copy this file to .env and customize
|
# Copy this file to .env and customize
|
||||||
|
|
||||||
# Image version to run (set by build.sh, or use 'latest')
|
# 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)
|
# Default admin credentials (used on FIRST RUN only)
|
||||||
ADMIN_NAME=Admin User
|
ADMIN_NAME=Admin User
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "jama-backend",
|
"name": "jama-backend",
|
||||||
"version": "0.7.0",
|
"version": "0.7.1",
|
||||||
"description": "TeamChat backend server",
|
"description": "TeamChat backend server",
|
||||||
"main": "src/index.js",
|
"main": "src/index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ Welcome to **JAMA** — your private, self-hosted team messaging app.
|
|||||||
## Security
|
## Security
|
||||||
|
|
||||||
### 🛡️ Your Privacy Assured
|
### 🛡️ 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.
|
**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:
|
Two ways to start a private conversation with one person:
|
||||||
|
|
||||||
|
_**New Chat Button**_
|
||||||
1. Click the **New Chat** icon in the sidebar
|
1. Click the **New Chat** icon in the sidebar
|
||||||
2. Select one user from the list
|
2. Select one user from the list
|
||||||
3. Click **Start Conversation**
|
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**
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ app.use(cookieParser());
|
|||||||
app.use('/uploads', express.static('/app/uploads'));
|
app.use('/uploads', express.static('/app/uploads'));
|
||||||
|
|
||||||
// API Routes
|
// 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/users', require('./routes/users'));
|
||||||
app.use('/api/groups', require('./routes/groups')(io));
|
app.use('/api/groups', require('./routes/groups')(io));
|
||||||
app.use('/api/messages', require('./routes/messages'));
|
app.use('/api/messages', require('./routes/messages'));
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
const express = require('express');
|
const express = require('express');
|
||||||
const bcrypt = require('bcryptjs');
|
const bcrypt = require('bcryptjs');
|
||||||
const router = express.Router();
|
|
||||||
const { getDb, getOrCreateSupportGroup } = require('../models/db');
|
const { getDb, getOrCreateSupportGroup } = require('../models/db');
|
||||||
const { generateToken, authMiddleware, setActiveSession, clearActiveSession } = require('../middleware/auth');
|
const { generateToken, authMiddleware, setActiveSession, clearActiveSession } = require('../middleware/auth');
|
||||||
|
|
||||||
|
module.exports = function(io) {
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
// Login
|
// Login
|
||||||
router.post('/login', (req, res) => {
|
router.post('/login', (req, res) => {
|
||||||
const { email, password, rememberMe } = req.body;
|
const { email, password, rememberMe } = req.body;
|
||||||
@@ -27,6 +29,10 @@ router.post('/login', (req, res) => {
|
|||||||
const token = generateToken(user.id);
|
const token = generateToken(user.id);
|
||||||
const ua = req.headers['user-agent'] || '';
|
const ua = req.headers['user-agent'] || '';
|
||||||
const device = setActiveSession(user.id, token, ua); // displaces prior session on same device class
|
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;
|
const { password: _, ...userSafe } = user;
|
||||||
res.json({
|
res.json({
|
||||||
@@ -102,4 +108,5 @@ ${message.trim()}`;
|
|||||||
res.json({ success: true });
|
res.json({ success: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports = router;
|
return router;
|
||||||
|
};
|
||||||
|
|||||||
2
build.sh
2
build.sh
@@ -13,7 +13,7 @@
|
|||||||
# ─────────────────────────────────────────────────────────────
|
# ─────────────────────────────────────────────────────────────
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
VERSION="${1:-0.7.0}"
|
VERSION="${1:-0.7.1}"
|
||||||
ACTION="${2:-}"
|
ACTION="${2:-}"
|
||||||
REGISTRY="${REGISTRY:-}"
|
REGISTRY="${REGISTRY:-}"
|
||||||
IMAGE_NAME="jama"
|
IMAGE_NAME="jama"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "jama-frontend",
|
"name": "jama-frontend",
|
||||||
"version": "0.7.0",
|
"version": "0.7.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -20,11 +20,20 @@ function ProtectedRoute({ children }) {
|
|||||||
|
|
||||||
function AuthRoute({ children }) {
|
function AuthRoute({ children }) {
|
||||||
const { user, loading, mustChangePassword } = useAuth();
|
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 (loading) return null;
|
||||||
if (user && !mustChangePassword) return <Navigate to="/" replace />;
|
if (user && !mustChangePassword) return <Navigate to="/" replace />;
|
||||||
return children;
|
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() {
|
export default function App() {
|
||||||
return (
|
return (
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
@@ -34,7 +43,7 @@ export default function App() {
|
|||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/login" element={<AuthRoute><Login /></AuthRoute>} />
|
<Route path="/login" element={<AuthRoute><Login /></AuthRoute>} />
|
||||||
<Route path="/change-password" element={<ChangePassword />} />
|
<Route path="/change-password" element={<ChangePassword />} />
|
||||||
<Route path="/" element={<ProtectedRoute><Chat /></ProtectedRoute>} />
|
<Route path="/" element={<ProtectedRoute><RestoreTheme /><Chat /></ProtectedRoute>} />
|
||||||
<Route path="*" element={<Navigate to="/" replace />} />
|
<Route path="*" element={<Navigate to="/" replace />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</SocketProvider>
|
</SocketProvider>
|
||||||
|
|||||||
@@ -3,10 +3,6 @@ import ReactDOM from 'react-dom/client';
|
|||||||
import App from './App.jsx';
|
import App from './App.jsx';
|
||||||
import './index.css';
|
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
|
// Register service worker
|
||||||
if ('serviceWorker' in navigator) {
|
if ('serviceWorker' in navigator) {
|
||||||
window.addEventListener('load', () => {
|
window.addEventListener('load', () => {
|
||||||
|
|||||||
@@ -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:new', handleGroupNew);
|
||||||
socket.on('group:deleted', handleGroupDeleted);
|
socket.on('group:deleted', handleGroupDeleted);
|
||||||
socket.on('group:updated', handleGroupUpdated);
|
socket.on('group:updated', handleGroupUpdated);
|
||||||
|
socket.on('session:displaced', handleSessionDisplaced);
|
||||||
|
|
||||||
// Bug B fix: on reconnect, reload groups to catch any messages missed while offline
|
// Bug B fix: on reconnect, reload groups to catch any messages missed while offline
|
||||||
const handleReconnect = () => { loadGroups(); };
|
const handleReconnect = () => { loadGroups(); };
|
||||||
@@ -217,6 +229,7 @@ export default function Chat() {
|
|||||||
socket.off('group:deleted', handleGroupDeleted);
|
socket.off('group:deleted', handleGroupDeleted);
|
||||||
socket.off('group:updated', handleGroupUpdated);
|
socket.off('group:updated', handleGroupUpdated);
|
||||||
socket.off('connect', handleReconnect);
|
socket.off('connect', handleReconnect);
|
||||||
|
socket.off('session:displaced', handleSessionDisplaced);
|
||||||
document.removeEventListener('visibilitychange', handleVisibility);
|
document.removeEventListener('visibilitychange', handleVisibility);
|
||||||
};
|
};
|
||||||
}, [socket, toast, activeGroupId, user, isMobile, loadGroups]);
|
}, [socket, toast, activeGroupId, user, isMobile, loadGroups]);
|
||||||
|
|||||||
Reference in New Issue
Block a user