Add rocket chat user id field and show messages from linked user id on non-admin accounts

This commit is contained in:
2025-09-01 18:46:59 -04:00
parent 5dcd19e7f3
commit 7938c50762
5 changed files with 256 additions and 49 deletions

View File

@@ -34,10 +34,12 @@ const authenticate = async (req, res, next) => {
// Get user from database // Get user from database
const result = await pool.query( const result = await pool.query(
'SELECT id, username, is_admin FROM users WHERE id = $1', 'SELECT id, username, email, is_admin, rocket_chat_user_id FROM users WHERE id = $1',
[decoded.userId] [decoded.userId]
); );
console.log('Database query result for user', decoded.userId, ':', result.rows[0]);
if (result.rows.length === 0) { if (result.rows.length === 0) {
return res.status(401).json({ error: 'User not found' }); return res.status(401).json({ error: 'User not found' });
} }
@@ -58,7 +60,7 @@ router.post('/login', async (req, res) => {
// Get user from database // Get user from database
const result = await pool.query( const result = await pool.query(
'SELECT id, username, password, is_admin, is_active FROM users WHERE username = $1', 'SELECT id, username, password, is_admin, is_active, rocket_chat_user_id FROM users WHERE username = $1',
[username] [username]
); );
@@ -101,6 +103,7 @@ router.post('/login', async (req, res) => {
id: user.id, id: user.id,
username: user.username, username: user.username,
is_admin: user.is_admin, is_admin: user.is_admin,
rocket_chat_user_id: user.rocket_chat_user_id,
permissions permissions
} }
}); });
@@ -119,8 +122,13 @@ router.get('/me', authenticate, async (req, res) => {
res.json({ res.json({
id: req.user.id, id: req.user.id,
username: req.user.username, username: req.user.username,
email: req.user.email,
is_admin: req.user.is_admin, is_admin: req.user.is_admin,
permissions rocket_chat_user_id: req.user.rocket_chat_user_id,
permissions,
// Debug info
_debug_raw_user: req.user,
_server_identifier: "INVENTORY_AUTH_SERVER_MODIFIED"
}); });
} catch (error) { } catch (error) {
console.error('Error getting current user:', error); console.error('Error getting current user:', error);
@@ -132,7 +140,7 @@ router.get('/me', authenticate, async (req, res) => {
router.get('/users', authenticate, requirePermission('view:users'), async (req, res) => { router.get('/users', authenticate, requirePermission('view:users'), async (req, res) => {
try { try {
const result = await pool.query(` const result = await pool.query(`
SELECT id, username, email, is_admin, is_active, created_at, last_login SELECT id, username, email, is_admin, is_active, rocket_chat_user_id, created_at, last_login
FROM users FROM users
ORDER BY username ORDER BY username
`); `);
@@ -151,7 +159,7 @@ router.get('/users/:id', authenticate, requirePermission('view:users'), async (r
// Get user details // Get user details
const userResult = await pool.query(` const userResult = await pool.query(`
SELECT id, username, email, is_admin, is_active, created_at, last_login SELECT id, username, email, is_admin, is_active, rocket_chat_user_id, created_at, last_login
FROM users FROM users
WHERE id = $1 WHERE id = $1
`, [userId]); `, [userId]);
@@ -187,13 +195,14 @@ router.post('/users', authenticate, requirePermission('create:users'), async (re
const client = await pool.connect(); const client = await pool.connect();
try { try {
const { username, email, password, is_admin, is_active, permissions } = req.body; const { username, email, password, is_admin, is_active, rocket_chat_user_id, permissions } = req.body;
console.log("Create user request:", { console.log("Create user request:", {
username, username,
email, email,
is_admin, is_admin,
is_active, is_active,
rocket_chat_user_id,
permissions: permissions || [] permissions: permissions || []
}); });
@@ -221,10 +230,10 @@ router.post('/users', authenticate, requirePermission('create:users'), async (re
// Insert new user // Insert new user
const userResult = await client.query(` const userResult = await client.query(`
INSERT INTO users (username, email, password, is_admin, is_active, created_at) INSERT INTO users (username, email, password, is_admin, is_active, rocket_chat_user_id, created_at)
VALUES ($1, $2, $3, $4, $5, CURRENT_TIMESTAMP) VALUES ($1, $2, $3, $4, $5, $6, CURRENT_TIMESTAMP)
RETURNING id RETURNING id
`, [username, email || null, hashedPassword, !!is_admin, is_active !== false]); `, [username, email || null, hashedPassword, !!is_admin, is_active !== false, rocket_chat_user_id || null]);
const userId = userResult.rows[0].id; const userId = userResult.rows[0].id;
@@ -299,7 +308,7 @@ router.put('/users/:id', authenticate, requirePermission('edit:users'), async (r
try { try {
const userId = req.params.id; const userId = req.params.id;
const { username, email, password, is_admin, is_active, permissions } = req.body; const { username, email, password, is_admin, is_active, rocket_chat_user_id, permissions } = req.body;
console.log("Update user request:", { console.log("Update user request:", {
userId, userId,
@@ -307,6 +316,7 @@ router.put('/users/:id', authenticate, requirePermission('edit:users'), async (r
email, email,
is_admin, is_admin,
is_active, is_active,
rocket_chat_user_id,
permissions: permissions || [] permissions: permissions || []
}); });
@@ -348,6 +358,11 @@ router.put('/users/:id', authenticate, requirePermission('edit:users'), async (r
updateValues.push(!!is_active); updateValues.push(!!is_active);
} }
if (rocket_chat_user_id !== undefined) {
updateFields.push(`rocket_chat_user_id = $${paramIndex++}`);
updateValues.push(rocket_chat_user_id || null);
}
// Update password if provided // Update password if provided
if (password) { if (password) {
const saltRounds = 10; const saltRounds = 10;

View File

@@ -108,7 +108,7 @@ app.get('/me', async (req, res) => {
// Get user details from database // Get user details from database
const userResult = await pool.query( const userResult = await pool.query(
'SELECT id, username, email, is_admin, is_active FROM users WHERE id = $1', 'SELECT id, username, email, is_admin, rocket_chat_user_id, is_active FROM users WHERE id = $1',
[decoded.userId] [decoded.userId]
); );
@@ -135,6 +135,7 @@ app.get('/me', async (req, res) => {
id: user.id, id: user.id,
username: user.username, username: user.username,
email: user.email, email: user.email,
rocket_chat_user_id: user.rocket_chat_user_id,
is_admin: user.is_admin, is_admin: user.is_admin,
permissions: permissions permissions: permissions
}); });

View File

@@ -14,8 +14,11 @@ import {
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Switch } from "@/components/ui/switch"; import { Switch } from "@/components/ui/switch";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { PermissionSelector } from "./PermissionSelector"; import { PermissionSelector } from "./PermissionSelector";
import { Alert, AlertDescription } from "@/components/ui/alert"; import { Alert, AlertDescription } from "@/components/ui/alert";
import config from "@/config";
interface Permission { interface Permission {
id: number; id: number;
@@ -25,12 +28,22 @@ interface Permission {
category?: string; category?: string;
} }
interface RocketChatUser {
id: number;
username: string;
name: string;
type: string;
active: boolean;
mongo_id?: string;
}
interface User { interface User {
id: number; id: number;
username: string; username: string;
email?: string; email?: string;
is_admin: boolean; is_admin: boolean;
is_active: boolean; is_active: boolean;
rocket_chat_user_id?: string;
permissions?: Permission[]; permissions?: Permission[];
} }
@@ -53,6 +66,7 @@ const userFormSchema = z.object({
password: z.string().min(6, { message: "Password must be at least 6 characters" }).optional().or(z.literal("")), password: z.string().min(6, { message: "Password must be at least 6 characters" }).optional().or(z.literal("")),
is_admin: z.boolean().default(false), is_admin: z.boolean().default(false),
is_active: z.boolean().default(true), is_active: z.boolean().default(true),
rocket_chat_user_id: z.string().optional(),
}); });
type FormValues = z.infer<typeof userFormSchema>; type FormValues = z.infer<typeof userFormSchema>;
@@ -80,12 +94,15 @@ interface UserSaveData {
password?: string; password?: string;
is_admin: boolean; is_admin: boolean;
is_active: boolean; is_active: boolean;
rocket_chat_user_id?: string;
permissions: Permission[]; permissions: Permission[];
} }
export function UserForm({ user, permissions, onSave, onCancel }: UserFormProps) { export function UserForm({ user, permissions, onSave, onCancel }: UserFormProps) {
const [selectedPermissions, setSelectedPermissions] = useState<number[]>([]); const [selectedPermissions, setSelectedPermissions] = useState<number[]>([]);
const [formError, setFormError] = useState<string | null>(null); const [formError, setFormError] = useState<string | null>(null);
const [rocketChatUsers, setRocketChatUsers] = useState<RocketChatUser[]>([]);
const [loadingRocketChatUsers, setLoadingRocketChatUsers] = useState(true);
// Initialize the form with React Hook Form // Initialize the form with React Hook Form
const form = useForm<FormValues>({ const form = useForm<FormValues>({
@@ -96,10 +113,33 @@ export function UserForm({ user, permissions, onSave, onCancel }: UserFormProps)
password: "", // Don't pre-fill password password: "", // Don't pre-fill password
is_admin: user?.is_admin || false, is_admin: user?.is_admin || false,
is_active: user?.is_active !== false, is_active: user?.is_active !== false,
rocket_chat_user_id: user?.rocket_chat_user_id || "none",
}, },
}); });
// Initialize selected permissions // Fetch Rocket Chat users
useEffect(() => {
const fetchRocketChatUsers = async () => {
try {
const response = await fetch(`${config.chatUrl}/users`);
const data = await response.json();
if (data.status === 'success') {
setRocketChatUsers(data.users);
} else {
console.error('Failed to fetch Rocket Chat users:', data.error);
}
} catch (error) {
console.error('Error fetching Rocket Chat users:', error);
} finally {
setLoadingRocketChatUsers(false);
}
};
fetchRocketChatUsers();
}, []);
// Initialize selected permissions and form values
useEffect(() => { useEffect(() => {
console.log("User permissions:", user?.permissions); console.log("User permissions:", user?.permissions);
@@ -112,7 +152,19 @@ export function UserForm({ user, permissions, onSave, onCancel }: UserFormProps)
console.log("No permissions found or empty permissions array"); console.log("No permissions found or empty permissions array");
setSelectedPermissions([]); setSelectedPermissions([]);
} }
}, [user]);
// Update form values when user data changes
if (user) {
form.reset({
username: user.username || "",
email: user.email || "",
password: "", // Don't pre-fill password
is_admin: user.is_admin || false,
is_active: user.is_active !== false,
rocket_chat_user_id: user.rocket_chat_user_id || "none",
});
}
}, [user, form]);
// Handle form submission // Handle form submission
const onSubmit = (data: FormValues) => { const onSubmit = (data: FormValues) => {
@@ -130,6 +182,7 @@ export function UserForm({ user, permissions, onSave, onCancel }: UserFormProps)
const userData: UserSaveData = { const userData: UserSaveData = {
...data, ...data,
id: user?.id, // Include ID if editing existing user id: user?.id, // Include ID if editing existing user
rocket_chat_user_id: data.is_admin ? undefined : (data.rocket_chat_user_id === "none" ? undefined : data.rocket_chat_user_id),
permissions: [] // Initialize with empty array permissions: [] // Initialize with empty array
}; };
@@ -220,6 +273,64 @@ export function UserForm({ user, permissions, onSave, onCancel }: UserFormProps)
)} )}
/> />
<FormField
control={form.control}
name="rocket_chat_user_id"
render={({ field }) => (
<FormItem>
<FormLabel>Rocket Chat User</FormLabel>
<FormControl>
{form.watch("is_admin") ? (
<div className="flex items-center gap-2 p-2 border rounded-md bg-muted">
<span className="text-muted-foreground">
Admin users have access to all chat rooms by default
</span>
</div>
) : (
<Select value={field.value} onValueChange={field.onChange}>
<SelectTrigger>
<SelectValue placeholder={loadingRocketChatUsers ? "Loading..." : "Select Rocket Chat user..."} />
</SelectTrigger>
<SelectContent>
{!loadingRocketChatUsers && (
<SelectItem value="none">
<span className="text-muted-foreground">None</span>
</SelectItem>
)}
{rocketChatUsers.map((rcUser) => (
<SelectItem key={rcUser.id} value={rcUser.id.toString()}>
<div className="flex items-center gap-2">
<Avatar className="h-6 w-6">
<AvatarImage
src={rcUser.mongo_id ? `${config.chatUrl}/avatar/${rcUser.mongo_id}` : undefined}
alt={rcUser.name || rcUser.username}
/>
<AvatarFallback className="text-xs">
{(rcUser.name || rcUser.username).charAt(0).toUpperCase()}
</AvatarFallback>
</Avatar>
<span className={rcUser.active ? '' : 'text-muted-foreground'}>
{rcUser.name || rcUser.username}
{!rcUser.active && <span className="text-xs ml-1">(inactive)</span>}
</span>
</div>
</SelectItem>
))}
</SelectContent>
</Select>
)}
</FormControl>
<FormDescription>
{form.watch("is_admin")
? "Admin users can view all chat rooms regardless of this setting"
: "Connect this user to a Rocket Chat account to control their chat access"
}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField <FormField
control={form.control} control={form.control}
name="password" name="password"

View File

@@ -14,6 +14,7 @@ export interface User {
username: string; username: string;
email?: string; email?: string;
is_admin: boolean; is_admin: boolean;
rocket_chat_user_id?: string;
permissions: string[]; permissions: string[];
} }
@@ -66,6 +67,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
const userData = await response.json(); const userData = await response.json();
console.log("Fetched current user data:", userData); console.log("Fetched current user data:", userData);
console.log("User permissions:", userData.permissions); console.log("User permissions:", userData.permissions);
console.log("User rocket_chat_user_id:", userData.rocket_chat_user_id);
setUser(userData); setUser(userData);
// Ensure we have the sessionStorage isLoggedIn flag set // Ensure we have the sessionStorage isLoggedIn flag set

View File

@@ -1,4 +1,4 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect, useContext } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
@@ -8,6 +8,7 @@ import { Loader2, Search } from 'lucide-react';
import { RoomList } from '@/components/chat/RoomList'; import { RoomList } from '@/components/chat/RoomList';
import { ChatRoom } from '@/components/chat/ChatRoom'; import { ChatRoom } from '@/components/chat/ChatRoom';
import { SearchResults } from '@/components/chat/SearchResults'; import { SearchResults } from '@/components/chat/SearchResults';
import { AuthContext } from '@/contexts/AuthContext';
import config from '@/config'; import config from '@/config';
interface User { interface User {
@@ -39,11 +40,13 @@ interface SearchResult {
} }
export function Chat() { export function Chat() {
const { user: currentUser, token } = useContext(AuthContext);
const [users, setUsers] = useState<User[]>([]); const [users, setUsers] = useState<User[]>([]);
const [selectedUserId, setSelectedUserId] = useState<string>(''); const [selectedUserId, setSelectedUserId] = useState<string>('');
const [selectedRoomId, setSelectedRoomId] = useState<string | null>(null); const [selectedRoomId, setSelectedRoomId] = useState<string | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [userRocketChatId, setUserRocketChatId] = useState<string | null>(null);
// Global search state // Global search state
const [globalSearchQuery, setGlobalSearchQuery] = useState(''); const [globalSearchQuery, setGlobalSearchQuery] = useState('');
@@ -51,6 +54,12 @@ export function Chat() {
const [showSearchResults, setShowSearchResults] = useState(false); const [showSearchResults, setShowSearchResults] = useState(false);
const [searching, setSearching] = useState(false); const [searching, setSearching] = useState(false);
useEffect(() => {
if (currentUser) {
setUserRocketChatId(currentUser.rocket_chat_user_id || null);
}
}, [currentUser]);
useEffect(() => { useEffect(() => {
const fetchUsers = async () => { const fetchUsers = async () => {
try { try {
@@ -59,6 +68,25 @@ export function Chat() {
if (data.status === 'success') { if (data.status === 'success') {
setUsers(data.users); setUsers(data.users);
// Auto-select user based on permissions
if (currentUser && !currentUser.is_admin && userRocketChatId) {
console.log('Attempting to auto-select user:', {
currentUser: currentUser.username,
userRocketChatId,
userRocketChatIdType: typeof userRocketChatId,
availableUsers: data.users.map((u: User) => ({ id: u.id, idType: typeof u.id }))
});
// Non-admin users should only see their connected rocket chat user
const userRocketChatUser = data.users.find((user: User) => user.id.toString() === userRocketChatId?.toString());
console.log('Found matching user:', userRocketChatUser);
if (userRocketChatUser) {
setSelectedUserId(userRocketChatUser.id.toString());
console.log('Auto-selected user ID:', userRocketChatUser.id.toString());
}
}
} else { } else {
throw new Error(data.error || 'Failed to fetch users'); throw new Error(data.error || 'Failed to fetch users');
} }
@@ -70,14 +98,19 @@ export function Chat() {
} }
}; };
if (currentUser) {
fetchUsers(); fetchUsers();
}, []); }
}, [currentUser, userRocketChatId]);
const handleUserChange = (userId: string) => { const handleUserChange = (userId: string) => {
// Only allow admins to change users, or if the user is selecting their own connected account
if (currentUser?.is_admin || userId === userRocketChatId) {
setSelectedUserId(userId); setSelectedUserId(userId);
setSelectedRoomId(null); // Reset room selection when user changes setSelectedRoomId(null); // Reset room selection when user changes
setGlobalSearchQuery(''); // Clear search when user changes setGlobalSearchQuery(''); // Clear search when user changes
setShowSearchResults(false); setShowSearchResults(false);
}
}; };
const handleRoomSelect = (roomId: string) => { const handleRoomSelect = (roomId: string) => {
@@ -181,6 +214,7 @@ export function Chat() {
</div> </div>
)} )}
{currentUser?.is_admin ? (
<Select value={selectedUserId} onValueChange={handleUserChange}> <Select value={selectedUserId} onValueChange={handleUserChange}>
<SelectTrigger className="w-64"> <SelectTrigger className="w-64">
<SelectValue placeholder="View as user..." /> <SelectValue placeholder="View as user..." />
@@ -207,6 +241,39 @@ export function Chat() {
))} ))}
</SelectContent> </SelectContent>
</Select> </Select>
) : (
<div className="flex items-center gap-2 p-2 border rounded-md bg-muted w-64">
{selectedUserId && users.length > 0 ? (
<>
{(() => {
const selectedUser = users.find(u => u.id.toString() === selectedUserId);
return selectedUser ? (
<>
<Avatar className="h-6 w-6">
<AvatarImage
src={selectedUser.mongo_id ? `${config.chatUrl}/avatar/${selectedUser.mongo_id}` : undefined}
alt={selectedUser.name || selectedUser.username}
/>
<AvatarFallback className="text-xs">
{(selectedUser.name || selectedUser.username).charAt(0).toUpperCase()}
</AvatarFallback>
</Avatar>
<span>
Viewing as: {selectedUser.name || selectedUser.username}
</span>
</>
) : (
<span className="text-muted-foreground">No connected Rocket Chat user</span>
);
})()}
</>
) : (
<span className="text-muted-foreground">
{userRocketChatId ? 'Loading...' : 'No connected Rocket Chat user'}
</span>
)}
</div>
)}
</div> </div>
</div> </div>
@@ -233,9 +300,20 @@ export function Chat() {
) : ( ) : (
<Card> <Card>
<CardContent className="flex items-center justify-center h-64"> <CardContent className="flex items-center justify-center h-64">
{currentUser?.is_admin ? (
<p className="text-muted-foreground"> <p className="text-muted-foreground">
Select a user to view their chat rooms and messages. Select a user to view their chat rooms and messages.
</p> </p>
) : (
<div className="text-center space-y-2">
<p className="text-muted-foreground">
No Rocket Chat user connected to your account.
</p>
<p className="text-sm text-muted-foreground">
Please contact your administrator to connect your account with a Rocket Chat user.
</p>
</div>
)}
</CardContent> </CardContent>
</Card> </Card>
)} )}