diff --git a/backend/internal/db/users.go b/backend/internal/db/users.go
index 9ce15ab..3c7e40f 100644
--- a/backend/internal/db/users.go
+++ b/backend/internal/db/users.go
@@ -6,10 +6,10 @@ import (
)
// CreateUser inserts a new user record into the database
-func (db *DB) CreateUser(user *models.User) error {
+func (db *DB) CreateUser(user *models.User) (*models.User, error) {
tx, err := db.Begin()
if err != nil {
- return err
+ return nil, err
}
defer tx.Rollback()
@@ -18,15 +18,21 @@ func (db *DB) CreateUser(user *models.User) error {
VALUES (?, ?, ?, ?)`,
user.Email, user.DisplayName, user.PasswordHash, user.Role)
if err != nil {
- return err
+ return nil, err
}
userID, err := result.LastInsertId()
if err != nil {
- return err
+ return nil, err
}
user.ID = int(userID)
+ // Retrieve the created_at timestamp
+ err = tx.QueryRow("SELECT created_at FROM users WHERE id = ?", user.ID).Scan(&user.CreatedAt)
+ if err != nil {
+ return nil, err
+ }
+
// Create default workspace with default settings
defaultWorkspace := &models.Workspace{
UserID: user.ID,
@@ -37,22 +43,22 @@ func (db *DB) CreateUser(user *models.User) error {
// Create workspace with settings
err = db.createWorkspaceTx(tx, defaultWorkspace)
if err != nil {
- return err
+ return nil, err
}
// Update user's last workspace ID
_, err = tx.Exec("UPDATE users SET last_workspace_id = ? WHERE id = ?", defaultWorkspace.ID, user.ID)
if err != nil {
- return err
+ return nil, err
}
err = tx.Commit()
if err != nil {
- return err
+ return nil, err
}
user.LastWorkspaceID = defaultWorkspace.ID
- return nil
+ return user, nil
}
// Helper function to create a workspace in a transaction
diff --git a/backend/internal/handlers/admin_handlers.go b/backend/internal/handlers/admin_handlers.go
index 6b3c4a9..4c78ed1 100644
--- a/backend/internal/handlers/admin_handlers.go
+++ b/backend/internal/handlers/admin_handlers.go
@@ -75,18 +75,19 @@ func (h *Handler) AdminCreateUser() http.HandlerFunc {
Role: req.Role,
}
- if err := h.DB.CreateUser(user); err != nil {
+ insertedUser, err := h.DB.CreateUser(user)
+ if err != nil {
http.Error(w, "Failed to create user", http.StatusInternalServerError)
return
}
// Initialize user workspace
- if err := h.FS.InitializeUserWorkspace(user.ID, user.LastWorkspaceID); err != nil {
+ if err := h.FS.InitializeUserWorkspace(insertedUser.ID, insertedUser.LastWorkspaceID); err != nil {
http.Error(w, "Failed to initialize user workspace", http.StatusInternalServerError)
return
}
- respondJSON(w, user)
+ respondJSON(w, insertedUser)
}
}
@@ -197,7 +198,7 @@ func (h *Handler) AdminDeleteUser() http.HandlerFunc {
return
}
- w.WriteHeader(http.StatusOK)
+ w.WriteHeader(http.StatusNoContent)
}
}
diff --git a/backend/internal/user/user.go b/backend/internal/user/user.go
index dbfce37..fb83cc1 100644
--- a/backend/internal/user/user.go
+++ b/backend/internal/user/user.go
@@ -47,69 +47,18 @@ func (s *UserService) SetupAdminUser(adminEmail, adminPassword string) (*models.
Role: models.RoleAdmin,
}
- err = s.DB.CreateUser(adminUser)
+ createdUser, err := s.DB.CreateUser(adminUser)
if err != nil {
return nil, fmt.Errorf("failed to create admin user: %w", err)
}
// Initialize workspace directory
- err = s.FS.InitializeUserWorkspace(adminUser.ID, adminUser.LastWorkspaceID)
+ err = s.FS.InitializeUserWorkspace(createdUser.ID, createdUser.LastWorkspaceID)
if err != nil {
return nil, fmt.Errorf("failed to initialize admin workspace: %w", err)
}
- log.Printf("Created admin user with ID: %d and default workspace with ID: %d", adminUser.ID, adminUser.LastWorkspaceID)
+ log.Printf("Created admin user with ID: %d and default workspace with ID: %d", createdUser.ID, createdUser.LastWorkspaceID)
return adminUser, nil
}
-
-func (s *UserService) CreateUser(user *models.User) error {
- err := s.DB.CreateUser(user)
- if err != nil {
- return fmt.Errorf("failed to create user: %w", err)
- }
-
- err = s.FS.InitializeUserWorkspace(user.ID, user.LastWorkspaceID)
- if err != nil {
- return fmt.Errorf("failed to initialize user workspace: %w", err)
- }
-
- return nil
-}
-
-func (s *UserService) GetUserByID(id int) (*models.User, error) {
- return s.DB.GetUserByID(id)
-}
-
-func (s *UserService) GetUserByEmail(email string) (*models.User, error) {
- return s.DB.GetUserByEmail(email)
-}
-
-func (s *UserService) UpdateUser(user *models.User) error {
- return s.DB.UpdateUser(user)
-}
-
-func (s *UserService) DeleteUser(id int) error {
- // First, get the user to check if they exist
- user, err := s.DB.GetUserByID(id)
- if err != nil {
- return fmt.Errorf("failed to get user: %w", err)
- }
-
- // Get user's workspaces
- workspaces, err := s.DB.GetWorkspacesByUserID(id)
- if err != nil {
- return fmt.Errorf("failed to get user's workspaces: %w", err)
- }
-
- // Delete workspace directories
- for _, workspace := range workspaces {
- err = s.FS.DeleteUserWorkspace(user.ID, workspace.ID)
- if err != nil {
- return fmt.Errorf("failed to delete workspace files: %w", err)
- }
- }
-
- // Delete user from database (this will cascade delete workspaces)
- return s.DB.DeleteUser(id)
-}
diff --git a/frontend/src/components/modals/user/CreateUserModal.jsx b/frontend/src/components/modals/user/CreateUserModal.jsx
new file mode 100644
index 0000000..23d5b5d
--- /dev/null
+++ b/frontend/src/components/modals/user/CreateUserModal.jsx
@@ -0,0 +1,76 @@
+import React, { useState } from 'react';
+import {
+ Modal,
+ Stack,
+ TextInput,
+ PasswordInput,
+ Select,
+ Button,
+ Group,
+} from '@mantine/core';
+
+const CreateUserModal = ({ opened, onClose, onCreateUser, loading }) => {
+ const [email, setEmail] = useState('');
+ const [password, setPassword] = useState('');
+ const [displayName, setDisplayName] = useState('');
+ const [role, setRole] = useState('viewer');
+
+ const handleSubmit = async () => {
+ const result = await onCreateUser({ email, password, displayName, role });
+ if (result.success) {
+ setEmail('');
+ setPassword('');
+ setDisplayName('');
+ setRole('viewer');
+ onClose();
+ }
+ };
+
+ return (
+
+
+ setEmail(e.currentTarget.value)}
+ placeholder="user@example.com"
+ />
+ setDisplayName(e.currentTarget.value)}
+ placeholder="John Doe"
+ />
+ setPassword(e.currentTarget.value)}
+ placeholder="Enter password"
+ />
+
+
+
+
+
+
+
+ );
+};
+
+export default CreateUserModal;
diff --git a/frontend/src/components/modals/user/DeleteUserModal.jsx b/frontend/src/components/modals/user/DeleteUserModal.jsx
new file mode 100644
index 0000000..5763a0a
--- /dev/null
+++ b/frontend/src/components/modals/user/DeleteUserModal.jsx
@@ -0,0 +1,29 @@
+import React from 'react';
+import { Modal, Text, Button, Group, Stack } from '@mantine/core';
+
+const DeleteUserModal = ({ opened, onClose, onConfirm, user, loading }) => (
+
+
+
+ Are you sure you want to delete user "{user?.email}"? This action cannot
+ be undone and all associated data will be permanently deleted.
+
+
+
+
+
+
+
+);
+
+export default DeleteUserModal;
diff --git a/frontend/src/components/settings/admin/AdminDashboard.jsx b/frontend/src/components/settings/admin/AdminDashboard.jsx
index 3345ce2..e095dc5 100644
--- a/frontend/src/components/settings/admin/AdminDashboard.jsx
+++ b/frontend/src/components/settings/admin/AdminDashboard.jsx
@@ -1,194 +1,42 @@
import React, { useState } from 'react';
-import {
- Modal,
- Table,
- Button,
- Group,
- TextInput,
- PasswordInput,
- Select,
- Stack,
- Text,
- ActionIcon,
- Box,
- LoadingOverlay,
- Alert,
-} from '@mantine/core';
-import {
- IconTrash,
- IconEdit,
- IconPlus,
- IconAlertCircle,
-} from '@tabler/icons-react';
-import { useAdmin } from '../../../hooks/useAdmin';
+import { Modal, Tabs } from '@mantine/core';
+import { IconUsers, IconFolders, IconChartBar } from '@tabler/icons-react';
import { useAuth } from '../../../contexts/AuthContext';
-
-const CreateUserModal = ({ opened, onClose, onCreateUser, loading }) => {
- const [email, setEmail] = useState('');
- const [password, setPassword] = useState('');
- const [displayName, setDisplayName] = useState('');
- const [role, setRole] = useState('viewer');
-
- const handleSubmit = async () => {
- const result = await onCreateUser({ email, password, displayName, role });
- if (result.success) {
- setEmail('');
- setPassword('');
- setDisplayName('');
- setRole('viewer');
- onClose();
- }
- };
-
- return (
-
-
- setEmail(e.currentTarget.value)}
- placeholder="user@example.com"
- />
- setDisplayName(e.currentTarget.value)}
- placeholder="John Doe"
- />
- setPassword(e.currentTarget.value)}
- placeholder="Enter password"
- />
-
-
-
-
-
-
-
- );
-};
+import AdminUsersTab from './AdminUsersTab';
+import AdminWorkspacesTab from './AdminWorkspacesTab';
+import AdminStatsTab from './AdminStatsTab';
const AdminDashboard = ({ opened, onClose }) => {
- const {
- data: users,
- loading,
- error,
- create,
- delete: deleteUser,
- } = useAdmin('users');
- const [createModalOpened, setCreateModalOpened] = useState(false);
const { user: currentUser } = useAuth();
-
- const handleCreateUser = async (userData) => {
- return await create(userData);
- };
-
- const handleDeleteUser = async (userId) => {
- if (userId === currentUser.id) {
- notifications.show({
- title: 'Error',
- message: 'You cannot delete your own account',
- color: 'red',
- });
- return;
- }
-
- return await deleteUser(userId);
- };
-
- const rows = users.map((user) => (
-
- {user.email}
- {user.displayName}
-
- {user.role}
-
- {new Date(user.createdAt).toLocaleDateString()}
-
-
-
-
-
- handleDeleteUser(user.id)}
- disabled={user.id === currentUser.id}
- >
-
-
-
-
-
- ));
+ const [activeTab, setActiveTab] = useState('users');
return (
-
-
+
+
+ }>
+ Users
+
+ }>
+ Workspaces
+
+ }>
+ Statistics
+
+
- {error && (
- }
- title="Error"
- color="red"
- mb="md"
- >
- {error}
-
- )}
+
+
+
-
-
- User Management
-
- }
- onClick={() => setCreateModalOpened(true)}
- >
- Create User
-
-
+
+
+
-
-
-
- Email
- Display Name
- Role
- Created At
- Actions
-
-
- {rows}
-
-
- setCreateModalOpened(false)}
- onCreateUser={handleCreateUser}
- loading={loading}
- />
-
+
+
+
+
);
};
diff --git a/frontend/src/components/settings/admin/AdminStatsTab.jsx b/frontend/src/components/settings/admin/AdminStatsTab.jsx
new file mode 100644
index 0000000..31c13eb
--- /dev/null
+++ b/frontend/src/components/settings/admin/AdminStatsTab.jsx
@@ -0,0 +1,122 @@
+import React from 'react';
+import {
+ Grid,
+ Card,
+ Stack,
+ Text,
+ Title,
+ LoadingOverlay,
+ Alert,
+ RingProgress,
+} from '@mantine/core';
+import {
+ IconUsers,
+ IconFolders,
+ IconServer,
+ IconFiles,
+ IconAlertCircle,
+} from '@tabler/icons-react';
+import { useAdmin } from '../../../hooks/useAdmin';
+import StatCard from './StatCard';
+
+const formatBytes = (bytes) => {
+ const units = ['B', 'KB', 'MB', 'GB'];
+ let size = bytes;
+ let unitIndex = 0;
+ while (size >= 1024 && unitIndex < units.length - 1) {
+ size /= 1024;
+ unitIndex++;
+ }
+ return `${size.toFixed(1)} ${units[unitIndex]}`;
+};
+
+const AdminStatsTab = () => {
+ const { data: stats, loading, error } = useAdmin('stats');
+
+ if (loading) {
+ return ;
+ }
+
+ if (error) {
+ return (
+ } title="Error" color="red">
+ {error}
+
+ );
+ }
+
+ return (
+
+
+ System Statistics
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Active Users
+
+ {((stats.activeUsers / stats.totalUsers) * 100).toFixed(1)}%
+
+ }
+ />
+
+ {stats.activeUsers} out of {stats.totalUsers} users active in
+ last 30 days
+
+
+
+
+
+
+ );
+};
+
+export default AdminStatsTab;
diff --git a/frontend/src/components/settings/admin/AdminUsersTab.jsx b/frontend/src/components/settings/admin/AdminUsersTab.jsx
new file mode 100644
index 0000000..0960058
--- /dev/null
+++ b/frontend/src/components/settings/admin/AdminUsersTab.jsx
@@ -0,0 +1,142 @@
+import React, { useState } from 'react';
+import {
+ Table,
+ Button,
+ Group,
+ Text,
+ ActionIcon,
+ Box,
+ LoadingOverlay,
+ Alert,
+} from '@mantine/core';
+import {
+ IconTrash,
+ IconEdit,
+ IconPlus,
+ IconAlertCircle,
+} from '@tabler/icons-react';
+import { notifications } from '@mantine/notifications';
+import { useAdmin } from '../../../hooks/useAdmin';
+import CreateUserModal from '../../modals/user/CreateUserModal';
+import DeleteUserModal from '../../modals/user/DeleteUserModal';
+
+const AdminUsersTab = ({ currentUser }) => {
+ const {
+ data: users,
+ loading,
+ error,
+ create,
+ delete: deleteUser,
+ } = useAdmin('users');
+ const [createModalOpened, setCreateModalOpened] = useState(false);
+ const [deleteModalData, setDeleteModalData] = useState(null);
+
+ const handleCreateUser = async (userData) => {
+ return await create(userData);
+ };
+
+ const handleDeleteClick = (user) => {
+ if (user.id === currentUser.id) {
+ notifications.show({
+ title: 'Error',
+ message: 'You cannot delete your own account',
+ color: 'red',
+ });
+ return;
+ }
+ setDeleteModalData(user);
+ };
+
+ const handleDeleteConfirm = async () => {
+ if (!deleteModalData) return;
+ const result = await deleteUser(deleteModalData.id);
+ if (result.success) {
+ setDeleteModalData(null);
+ }
+ };
+
+ const rows = users.map((user) => (
+
+ {user.email}
+ {user.displayName}
+
+ {user.role}
+
+ {new Date(user.createdAt).toLocaleDateString()}
+
+
+
+
+
+ handleDeleteClick(user)}
+ disabled={user.id === currentUser.id}
+ >
+
+
+
+
+
+ ));
+
+ return (
+
+
+
+ {error && (
+ }
+ title="Error"
+ color="red"
+ mb="md"
+ >
+ {error}
+
+ )}
+
+
+
+ User Management
+
+ }
+ onClick={() => setCreateModalOpened(true)}
+ >
+ Create User
+
+
+
+
+
+
+ Email
+ Display Name
+ Role
+ Created At
+ Actions
+
+
+ {rows}
+
+
+ setCreateModalOpened(false)}
+ onCreateUser={handleCreateUser}
+ loading={loading}
+ />
+
+ setDeleteModalData(null)}
+ onConfirm={handleDeleteConfirm}
+ user={deleteModalData}
+ loading={loading}
+ />
+
+ );
+};
+
+export default AdminUsersTab;
diff --git a/frontend/src/components/settings/admin/AdminWorkspacesTab.jsx b/frontend/src/components/settings/admin/AdminWorkspacesTab.jsx
new file mode 100644
index 0000000..8de5f2f
--- /dev/null
+++ b/frontend/src/components/settings/admin/AdminWorkspacesTab.jsx
@@ -0,0 +1,73 @@
+import React from 'react';
+import {
+ Table,
+ Group,
+ Text,
+ ActionIcon,
+ Box,
+ LoadingOverlay,
+ Alert,
+} from '@mantine/core';
+import { IconTrash, IconEdit, IconAlertCircle } from '@tabler/icons-react';
+import { useAdmin } from '../../../hooks/useAdmin';
+
+const AdminWorkspacesTab = () => {
+ const { data: workspaces, loading, error } = useAdmin('workspaces');
+
+ const rows = workspaces.map((workspace) => (
+
+ {workspace.name}
+ {workspace.owner?.email}
+ {new Date(workspace.createdAt).toLocaleDateString()}
+ {workspace.gitEnabled ? 'Yes' : 'No'}
+
+
+
+
+
+
+
+
+
+
+
+ ));
+
+ return (
+
+
+
+ {error && (
+ }
+ title="Error"
+ color="red"
+ mb="md"
+ >
+ {error}
+
+ )}
+
+
+
+ Workspace Management
+
+
+
+
+
+
+ Name
+ Owner
+ Created At
+ Git Enabled
+ Actions
+
+
+ {rows}
+
+
+ );
+};
+
+export default AdminWorkspacesTab;
diff --git a/frontend/src/components/settings/admin/StatCard.jsx b/frontend/src/components/settings/admin/StatCard.jsx
new file mode 100644
index 0000000..9a2a379
--- /dev/null
+++ b/frontend/src/components/settings/admin/StatCard.jsx
@@ -0,0 +1,20 @@
+import React from 'react';
+import { Card, Group, Box, Text } from '@mantine/core';
+
+const StatCard = ({ title, value, icon: Icon, color = 'blue' }) => (
+
+
+
+
+ {title}
+
+
+ {value}
+
+
+
+
+
+);
+
+export default StatCard;
diff --git a/frontend/src/services/adminApi.js b/frontend/src/services/adminApi.js
index 3c6c4a0..3c3bec3 100644
--- a/frontend/src/services/adminApi.js
+++ b/frontend/src/services/adminApi.js
@@ -21,7 +21,11 @@ export const deleteUser = async (userId) => {
const response = await apiCall(`${ADMIN_BASE_URL}/users/${userId}`, {
method: 'DELETE',
});
- return response.json();
+ if (response.status === 204) {
+ return;
+ } else {
+ throw new Error('Failed to delete user with status: ', response.status);
+ }
};
export const updateUser = async (userId, userData) => {