diff --git a/.env.example b/.env.example index faf6f07..402ca1a 100644 --- a/.env.example +++ b/.env.example @@ -10,7 +10,7 @@ PROJECT_NAME=jama # Image version to run (set by build.sh, or use 'latest') -JAMA_VERSION=0.9.43 +JAMA_VERSION=0.9.44 # App port — the host port Docker maps to the container PORT=3000 diff --git a/backend/package.json b/backend/package.json index a6dbc17..606ce80 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,6 @@ { "name": "jama-backend", - "version": "0.9.43", + "version": "0.9.44", "description": "TeamChat backend server", "main": "src/index.js", "scripts": { diff --git a/backend/src/middleware/auth.js b/backend/src/middleware/auth.js index 94a649c..7a7603f 100644 --- a/backend/src/middleware/auth.js +++ b/backend/src/middleware/auth.js @@ -43,15 +43,18 @@ function adminMiddleware(req, res, next) { next(); } -// Allows admins OR members of groups designated as Group Managers or Schedule Managers +// Allows admins OR members of groups designated as Tool Managers function teamManagerMiddleware(req, res, next) { if (req.user?.role === 'admin') return next(); const db = getDb(); - const gmSetting = db.prepare("SELECT value FROM settings WHERE key = 'team_group_managers'").get(); - const smSetting = db.prepare("SELECT value FROM settings WHERE key = 'team_schedule_managers'").get(); + // Prefer unified key, fall back to legacy keys for older installs + const tmSetting = db.prepare("SELECT value FROM settings WHERE key = 'team_tool_managers'").get(); + const gmSetting = db.prepare("SELECT value FROM settings WHERE key = 'team_group_managers'").get(); const allowedGroupIds = [ - ...JSON.parse(gmSetting?.value || '[]'), - ...JSON.parse(smSetting?.value || '[]'), + ...new Set([ + ...JSON.parse(tmSetting?.value || '[]'), + ...JSON.parse(gmSetting?.value || '[]'), + ]) ]; if (allowedGroupIds.length === 0) return res.status(403).json({ error: 'Access denied' }); const member = db.prepare(` diff --git a/backend/src/models/db.js b/backend/src/models/db.js index 1421885..d837965 100644 --- a/backend/src/models/db.js +++ b/backend/src/models/db.js @@ -220,6 +220,7 @@ function initDb() { insertSetting.run('app_type', 'JAMA-Chat'); insertSetting.run('team_group_managers', ''); insertSetting.run('team_schedule_managers', ''); + insertSetting.run('team_tool_managers', ''); // Migration: add hide_admin_tag if upgrading from older version try { diff --git a/backend/src/routes/settings.js b/backend/src/routes/settings.js index 66909c0..7dfd115 100644 --- a/backend/src/routes/settings.js +++ b/backend/src/routes/settings.js @@ -174,11 +174,16 @@ router.post('/register', authMiddleware, adminMiddleware, (req, res) => { // Save team management group assignments router.patch('/team', authMiddleware, adminMiddleware, (req, res) => { - const { groupManagers, scheduleManagers } = req.body; + const { toolManagers } = req.body; const db = getDb(); const upd = db.prepare("INSERT INTO settings (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = ?, updated_at = datetime('now')"); - if (groupManagers !== undefined) upd.run('team_group_managers', JSON.stringify(groupManagers || []), JSON.stringify(groupManagers || [])); - if (scheduleManagers !== undefined) upd.run('team_schedule_managers', JSON.stringify(scheduleManagers || []), JSON.stringify(scheduleManagers || [])); + if (toolManagers !== undefined) { + const val = JSON.stringify(toolManagers || []); + upd.run('team_tool_managers', val, val); + // Keep legacy keys in sync so existing teamManagerMiddleware still works + upd.run('team_group_managers', val, val); + upd.run('team_schedule_managers', val, val); + } res.json({ success: true }); }); diff --git a/build.sh b/build.sh index 839b749..47e4f84 100644 --- a/build.sh +++ b/build.sh @@ -13,7 +13,7 @@ # ───────────────────────────────────────────────────────────── set -euo pipefail -VERSION="${1:-0.9.43}" +VERSION="${1:-0.9.44}" ACTION="${2:-}" REGISTRY="${REGISTRY:-}" IMAGE_NAME="jama" diff --git a/frontend/package.json b/frontend/package.json index abc38f1..8b80c4f 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "jama-frontend", - "version": "0.9.43", + "version": "0.9.44", "private": true, "scripts": { "dev": "vite", diff --git a/frontend/src/components/NavDrawer.jsx b/frontend/src/components/NavDrawer.jsx index dd72fad..46f2599 100644 --- a/frontend/src/components/NavDrawer.jsx +++ b/frontend/src/components/NavDrawer.jsx @@ -17,11 +17,9 @@ export default function NavDrawer({ open, onClose, onMessages, onGroupManager, o const isAdmin = user?.role === 'admin'; const isMobile = window.matchMedia('(pointer: coarse)').matches || window.innerWidth < 768; - // Team-managed access: check if user is in any of the designated manager groups - // (frontend-only — no API enforcement yet) + // Tool Manager access: admin always passes; non-admins pass if in a designated tool manager group const userGroupIds = features.userGroupMemberships || []; - const canAccessGroupManager = isAdmin || (features.teamGroupManagers || []).some(gid => userGroupIds.includes(gid)); - const canAccessScheduleManager = isAdmin || (features.teamScheduleManagers || []).some(gid => userGroupIds.includes(gid)); + const canAccessTools = isAdmin || (features.teamToolManagers || []).some(gid => userGroupIds.includes(gid)); // Close on outside click useEffect(() => { @@ -74,22 +72,22 @@ export default function NavDrawer({ open, onClose, onMessages, onGroupManager, o {item(NAV_ICON.messages, 'Messages', onMessages)} {item(NAV_ICON.schedules, 'Schedules', () => {}, true)} - {/* Admin-only tools */} + {/* Admin-only: Branding + Settings */} {isAdmin && ( <>
Admin
- {item(NAV_ICON.users, 'User Manager', onUsers)} {features.branding && item(NAV_ICON.branding, 'Branding', onBranding)} {item(NAV_ICON.settings, 'Settings', onSettings)} )} - {/* Tools accessible to admins OR designated team groups */} - {(features.groupManager || features.scheduleManager) && !isMobile && (canAccessGroupManager || canAccessScheduleManager) && ( + {/* Tools: accessible to admins OR designated tool manager groups */} + {canAccessTools && ( <>
Tools
- {features.groupManager && canAccessGroupManager && item(NAV_ICON.groups, 'Group Manager', onGroupManager)} - {features.scheduleManager && canAccessScheduleManager && item(NAV_ICON.schedules, 'Schedule Manager', onScheduleManager || (() => {}))} + {item(NAV_ICON.users, 'User Manager', onUsers)} + {features.groupManager && !isMobile && item(NAV_ICON.groups, 'Group Manager', onGroupManager)} + {features.scheduleManager && !isMobile && item(NAV_ICON.schedules, 'Schedule Manager', onScheduleManager || (() => {}))} )} diff --git a/frontend/src/components/SettingsModal.jsx b/frontend/src/components/SettingsModal.jsx index 0f9a945..6422ba6 100644 --- a/frontend/src/components/SettingsModal.jsx +++ b/frontend/src/components/SettingsModal.jsx @@ -11,46 +11,47 @@ const APP_TYPES = { }; // ── Team Management Tab ─────────────────────────────────────────────────────── -function TeamManagementTab({ features }) { +function TeamManagementTab() { const toast = useToast(); const [userGroups, setUserGroups] = useState([]); - const [groupManagers, setGroupManagers] = useState([]); - const [scheduleManagers, setScheduleManagers] = useState([]); + const [toolManagers, setToolManagers] = useState([]); const [saving, setSaving] = useState(false); useEffect(() => { api.getUserGroups().then(({ groups }) => setUserGroups(groups || [])).catch(() => {}); api.getSettings().then(({ settings }) => { - setGroupManagers(JSON.parse(settings.team_group_managers || '[]')); - setScheduleManagers(JSON.parse(settings.team_schedule_managers || '[]')); + // Read from unified key, fall back to legacy key + setToolManagers(JSON.parse(settings.team_tool_managers || settings.team_group_managers || '[]')); }).catch(() => {}); }, []); - const toggle = (id, list, setList) => { - setList(prev => prev.includes(id) ? prev.filter(x => x !== id) : [...prev, id]); + const toggle = (id) => { + setToolManagers(prev => prev.includes(id) ? prev.filter(x => x !== id) : [...prev, id]); }; const handleSave = async () => { setSaving(true); try { - await api.updateTeamSettings({ groupManagers, scheduleManagers }); + await api.updateTeamSettings({ toolManagers }); toast('Team settings saved', 'success'); window.dispatchEvent(new Event('jama:settings-changed')); } catch (e) { toast(e.message, 'error'); } finally { setSaving(false); } }; - const GroupSelectList = ({ title, description, selected, onToggle }) => ( -
-
{title}
-

{description}

+ return ( +
+
Tool Managers
+

+ Members of selected groups can access Group Manager, Schedule Manager, and User Manager. Admin users always have access to all three tools. +

{userGroups.length === 0 ? ( -

No user groups created yet. Create groups in the Group Manager first.

+

No user groups created yet. Create groups in the Group Manager first.

) : ( -
+
{userGroups.map(g => (
)} - {selected.length === 0 && ( -

No groups selected — admins only.

+ {toolManagers.length === 0 && ( +

No groups selected — tools are admin-only.

)} -
- ); - - return ( -
- toggle(id, groupManagers, setGroupManagers)} - /> - toggle(id, scheduleManagers, setScheduleManagers)} - />
); @@ -324,7 +308,7 @@ export default function SettingsModal({ onClose, onFeaturesChanged }) { ))}
- {tab === 'team' && } + {tab === 'team' && } {tab === 'registration' && } {tab === 'webpush' && }
diff --git a/frontend/src/pages/Chat.jsx b/frontend/src/pages/Chat.jsx index 7a68d0a..1bb186f 100644 --- a/frontend/src/pages/Chat.jsx +++ b/frontend/src/pages/Chat.jsx @@ -38,7 +38,7 @@ export default function Chat() { const [unreadGroups, setUnreadGroups] = useState(new Map()); const [modal, setModal] = useState(null); // 'profile' | 'users' | 'settings' | 'newchat' | 'help' | 'groupmanager' const [drawerOpen, setDrawerOpen] = useState(false); - const [features, setFeatures] = useState({ branding: false, groupManager: false, scheduleManager: false, appType: 'JAMA-Chat', teamGroupManagers: [], teamScheduleManagers: [] }); + const [features, setFeatures] = useState({ branding: false, groupManager: false, scheduleManager: false, appType: 'JAMA-Chat', teamToolManagers: [] }); const [helpDismissed, setHelpDismissed] = useState(true); // true until status loaded const [isMobile, setIsMobile] = useState(window.innerWidth < 768); const [showSidebar, setShowSidebar] = useState(true); @@ -78,8 +78,7 @@ export default function Chat() { groupManager: settings.feature_group_manager === 'true', scheduleManager: settings.feature_schedule_manager === 'true', appType: settings.app_type || 'JAMA-Chat', - teamGroupManagers: JSON.parse(settings.team_group_managers || '[]'), - teamScheduleManagers: JSON.parse(settings.team_schedule_managers || '[]'), + teamToolManagers: JSON.parse(settings.team_tool_managers || settings.team_group_managers || '[]'), })); }).catch(() => {}); api.getMyUserGroups().then(({ groupIds }) => { diff --git a/frontend/src/utils/api.js b/frontend/src/utils/api.js index 7960394..6919765 100644 --- a/frontend/src/utils/api.js +++ b/frontend/src/utils/api.js @@ -102,7 +102,7 @@ export const api = { updateAppName: (name) => req('PATCH', '/settings/app-name', { name }), updateColors: (body) => req('PATCH', '/settings/colors', body), registerCode: (code) => req('POST', '/settings/register', { code }), - updateTeamSettings: (body) => req('PATCH', '/settings/team', body), + updateTeamSettings: (body) => req('PATCH', '/settings/team', body), // body: { toolManagers: [groupId,...] } // User groups (Group Manager) getMyUserGroups: () => req('GET', '/usergroups/me'),