diff --git a/Project-Assignment.docx b/Project-Assignment.docx new file mode 100644 index 0000000..3512a33 Binary files /dev/null and b/Project-Assignment.docx differ diff --git a/backend/api/migrations/0004_user_can_edit_documents_user_can_manage_entities_and_more.py b/backend/api/migrations/0004_user_can_edit_documents_user_can_manage_entities_and_more.py new file mode 100644 index 0000000..856846d --- /dev/null +++ b/backend/api/migrations/0004_user_can_edit_documents_user_can_manage_entities_and_more.py @@ -0,0 +1,33 @@ +# Generated by Django 6.0.1 on 2026-02-22 19:43 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("api", "0003_documentcounter"), + ] + + operations = [ + migrations.AddField( + model_name="user", + name="can_edit_documents", + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name="user", + name="can_manage_entities", + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name="user", + name="can_manually_measure", + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name="user", + name="can_measure", + field=models.BooleanField(default=False), + ), + ] diff --git a/backend/api/models.py b/backend/api/models.py index 01922eb..94b1f9d 100644 --- a/backend/api/models.py +++ b/backend/api/models.py @@ -12,6 +12,10 @@ class User(AbstractUser): role = models.CharField(max_length=20, choices=ROLE_CHOICES, default='viewer') is_admin = models.BooleanField(default=False) + can_measure = models.BooleanField(default=False) + can_manually_measure = models.BooleanField(default=False) + can_manage_entities = models.BooleanField(default=False) + can_edit_documents = models.BooleanField(default=False) class Meta: db_table = 'api_user' diff --git a/backend/api/serializers.py b/backend/api/serializers.py index eea9deb..f5dd6c9 100644 --- a/backend/api/serializers.py +++ b/backend/api/serializers.py @@ -11,7 +11,8 @@ class UserSerializer(serializers.ModelSerializer): class Meta: model = User fields = ['id', 'username', 'email', 'first_name', 'last_name', - 'role', 'is_admin', 'is_active', 'date_joined', 'password'] + 'role', 'is_admin', 'is_active', 'date_joined', 'password', + 'can_measure', 'can_manually_measure', 'can_manage_entities', 'can_edit_documents'] read_only_fields = ['id', 'date_joined'] extra_kwargs = { 'password': {'write_only': True} @@ -40,7 +41,8 @@ class UserDetailSerializer(serializers.ModelSerializer): class Meta: model = User fields = ['id', 'username', 'email', 'first_name', 'last_name', - 'role', 'is_admin', 'is_active', 'date_joined'] + 'role', 'is_admin', 'is_active', 'date_joined', + 'can_measure', 'can_manually_measure', 'can_manage_entities', 'can_edit_documents'] read_only_fields = ['id', 'date_joined'] diff --git a/backend/api/views.py b/backend/api/views.py index 2dd3f4f..149d1b2 100644 --- a/backend/api/views.py +++ b/backend/api/views.py @@ -14,6 +14,11 @@ from nomenclatures.models import Nomenclature, NomenclatureEntry from scalesapp.sse import sse_broadcast_update +class IsAdminUser(IsAuthenticated): + def has_permission(self, request, view): + return bool(request.user and request.user.is_authenticated and request.user.is_admin) + + class UserViewSet(viewsets.ModelViewSet): """ API endpoint for user management. @@ -22,7 +27,7 @@ class UserViewSet(viewsets.ModelViewSet): create: Create a new user retrieve: Get a specific user update: Update a user - destroy: Delete a user + destroy: Soft-delete a user (sets is_active=False) me: Get current authenticated user change_password: Change password for current user """ @@ -31,13 +36,24 @@ class UserViewSet(viewsets.ModelViewSet): filterset_fields = ['role', 'is_admin', 'is_active'] ordering = ['username'] - @action(detail=False, methods=['get'], permission_classes=[IsAuthenticated]) + def get_permissions(self): + if self.action in ('me', 'change_password'): + return [IsAuthenticated()] + return [IsAuthenticated(), IsAdminUser()] + + def destroy(self, request, *args, **kwargs): + instance = self.get_object() + instance.is_active = False + instance.save() + return Response(status=status.HTTP_204_NO_CONTENT) + + @action(detail=False, methods=['get']) def me(self, request): """Get current authenticated user details""" serializer = UserDetailSerializer(request.user) return Response(serializer.data) - @action(detail=False, methods=['post'], permission_classes=[IsAuthenticated], + @action(detail=False, methods=['post'], url_path='change-password', url_name='change_password') def change_password(self, request): """Change password for current user""" diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 365d01e..101c83d 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -5,6 +5,7 @@ import { AuthProvider, useAuth } from './contexts/AuthContext'; import Login from './components/Users/Login'; import Main from './components/Main'; import ReportEditor from './components/ReportEditor/ReportEditor'; +import UserManager from './components/Users/UserManager'; import './App.css'; import { NomenclatureProvider } from './contexts/NomenclatureContext'; import { NomenclatureDataProvider } from './contexts/NomenclatureDataContext'; @@ -40,7 +41,7 @@ import { NomenclatureDataProvider } from './contexts/NomenclatureDataContext'; // } function AppContent() { - const { user, login, logout, loading, isAuthenticated } = useAuth(); + const { user, currentUser, login, logout, loading, isAuthenticated } = useAuth(); if (loading) { return ( @@ -59,7 +60,8 @@ function AppContent() { } /> - } /> + : } /> + : } /> diff --git a/frontend/src/components/Header.jsx b/frontend/src/components/Header.jsx index b36c08d..4e36dfc 100644 --- a/frontend/src/components/Header.jsx +++ b/frontend/src/components/Header.jsx @@ -27,20 +27,33 @@ function Header() {
- - + {currentUser?.can_edit_documents && ( + + )} + {currentUser?.can_manage_entities && ( + + )} + {currentUser?.is_admin && ( + + )}

ScalesApp - Real-time Data Monitor

diff --git a/frontend/src/components/NomenclatureManager/NomenclatureManager.jsx b/frontend/src/components/NomenclatureManager/NomenclatureManager.jsx index ae72339..334f08d 100644 --- a/frontend/src/components/NomenclatureManager/NomenclatureManager.jsx +++ b/frontend/src/components/NomenclatureManager/NomenclatureManager.jsx @@ -1,5 +1,6 @@ import React, { useState, useEffect, useCallback } from 'react'; import api from '../../services/api'; +import { useAuth } from '../../contexts/AuthContext'; import './NomenclatureManager.css'; const EMPTY_FORM = { @@ -13,6 +14,8 @@ const EMPTY_FORM = { }; function NomenclatureManager({ onClose }) { + const { currentUser } = useAuth(); + const canEdit = !!currentUser?.can_manage_entities; const [nomenclatures, setNomenclatures] = useState([]); const [selectedId, setSelectedId] = useState(null); const [form, setForm] = useState(null); // null = nothing selected @@ -179,7 +182,7 @@ function NomenclatureManager({ onClose }) {
{/* Left panel - List */}
- + {canEdit && } {isLoading &&
Loading...
} @@ -196,13 +199,15 @@ function NomenclatureManager({ onClose }) { {nom.kind === 'lookup' ? 'Lookup' : 'Field'} / {nom.applies_to}
- + {canEdit && ( + + )}
))} @@ -216,14 +221,14 @@ function NomenclatureManager({ onClose }) {
{!form ? (
- Select a nomenclature to edit or click "+ Add New" + {canEdit ? 'Select a nomenclature to edit or click "+ Add New"' : 'Select a nomenclature to view'}
) : (
{error &&
{error}
}
- {selectedId ? 'Edit Nomenclature' : 'New Nomenclature'} + {!canEdit ? 'View Nomenclature' : selectedId ? 'Edit Nomenclature' : 'New Nomenclature'}
{/* Basic fields */} @@ -235,7 +240,7 @@ function NomenclatureManager({ onClose }) { value={form.code} onChange={e => setForm(prev => ({ ...prev, code: e.target.value }))} placeholder="e.g. cargo_type" - disabled={isSaving} + disabled={isSaving || !canEdit} />
@@ -246,7 +251,7 @@ function NomenclatureManager({ onClose }) { value={form.name} onChange={e => setForm(prev => ({ ...prev, name: e.target.value }))} placeholder="e.g. Cargo Type" - disabled={isSaving} + disabled={isSaving || !canEdit} />
@@ -255,7 +260,7 @@ function NomenclatureManager({ onClose }) { setForm(prev => ({ ...prev, kind: e.target.value }))} - disabled={isSaving} + disabled={isSaving || !canEdit} > @@ -280,7 +285,7 @@ function NomenclatureManager({ onClose }) { updateField(idx, 'label', e.target.value)} placeholder="Label" - disabled={isSaving} + disabled={isSaving || !canEdit} /> setNewUser(prev => ({ ...prev, username: e.target.value }))} + required + autoFocus + /> +
+
+ + setNewUser(prev => ({ ...prev, password: e.target.value }))} + required + /> +
+ {PERMISSION_FIELDS.map(({ key, label }) => ( +
+ setNewUser(prev => ({ ...prev, [key]: e.target.checked }))} + /> + +
+ ))} +
+
+ + +
+ + )} + + + + + + + {PERMISSION_FIELDS.map(({ key, label }) => ( + + ))} + + + + + {users.map(u => ( + + + + {PERMISSION_FIELDS.map(({ key }) => ( + + ))} + + + ))} + {users.length === 0 && ( + + + + )} + +
UsernameStatus{label}Actions
{u.username} + + {u.is_active ? 'Active' : 'Inactive'} + + + handlePermissionChange(u.id, key, e.target.checked)} + style={{ width: '18px', height: '18px', cursor: 'pointer' }} + /> + + {u.is_active ? ( + + ) : ( + + )} +
+ No users found +
+ + + ); +}