Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
e3a8d15
refactor: enhance task status handling in dashboard logic and update …
AhmedIkram05 May 6, 2026
28085be
refactor: update task status handling in dashboard tests to include '…
AhmedIkram05 May 6, 2026
aad3939
refactor: update report and task services to include backlog metrics …
AhmedIkram05 May 6, 2026
173b257
refactor: enhance Team Lead permissions to include task updates and d…
AhmedIkram05 May 6, 2026
8e977be
refactor: integrate report service for managing saved reports; implem…
AhmedIkram05 May 6, 2026
d5116ed
feat: add Report model for storing generated reports with relationshi…
AhmedIkram05 May 6, 2026
526566a
feat: add report management functionality with routes, controller, an…
AhmedIkram05 May 6, 2026
f0867ff
refactor: add backend-rebuild target to Makefile for streamlined back…
AhmedIkram05 May 7, 2026
c23c7e3
refactor: enhance task management features and permissions; add tests…
AhmedIkram05 May 7, 2026
1a11507
feat: implement audit log management with API routes and frontend int…
AhmedIkram05 May 7, 2026
c51f99c
refactor: enhance GitHub integration routes with role-based access co…
AhmedIkram05 May 7, 2026
0526b84
refactor: enhance authentication validation and role management; add …
AhmedIkram05 May 7, 2026
7f3207c
refactor: remove role selection from Register component and update re…
AhmedIkram05 May 7, 2026
fc9a69c
refactor: update role requirements for project routes and integrate a…
AhmedIkram05 May 7, 2026
0b7fe49
refactor: register audit routes with the API blueprint
AhmedIkram05 May 7, 2026
0d52c75
feat: Enhance Admin Dashboard with Team Overview and User Management
AhmedIkram05 May 7, 2026
5bcf0ca
refactor: update authentication routes and enhance permission handlin…
AhmedIkram05 May 7, 2026
b8a140c
feat: Enhance admin dashboard access for Team Leads and add Team Over…
AhmedIkram05 May 7, 2026
2ecd75e
refactor: Clean up API routes and enhance permission handling in comm…
AhmedIkram05 May 7, 2026
85151e0
feat: Add audit logs and system settings models with migration
AhmedIkram05 May 7, 2026
f1eb784
feat: Implement user creation and deletion with audit logging
AhmedIkram05 May 7, 2026
d110c24
feat: Implement full RBAC with admin user management and enhanced per…
AhmedIkram05 May 7, 2026
da6973e
feat: Integrate recent audit logs into Admin Dashboard and update tests
AhmedIkram05 May 7, 2026
f4bfb16
fix: Update link text for tasks in Navbar component
AhmedIkram05 May 7, 2026
17b4b0b
feat: Enhance notification system with improved handling and new feat…
AhmedIkram05 May 7, 2026
cb0f613
feat: enhance Developer Dashboard
AhmedIkram05 May 7, 2026
a84579a
refactor: rename open_prs to total_prs across GitHub integration and …
AhmedIkram05 May 7, 2026
ef9dee4
feat: update BasicDashboard to enhance role-based views and remove te…
AhmedIkram05 May 7, 2026
a9a2e06
feat: enhance project and task serialization with additional fields; …
AhmedIkram05 May 7, 2026
7e1e5d4
feat: enhance dashboard functionality for team leads; update routing …
AhmedIkram05 May 7, 2026
d934d3c
refactor: clean up task status styling in TaskList component
AhmedIkram05 May 7, 2026
49a2d89
feat: enhance task fetching with support for query parameters and dee…
AhmedIkram05 May 7, 2026
b600eaf
feat: integrate authentication context and update dashboard navigatio…
AhmedIkram05 May 7, 2026
0a9c95d
feat: add delete notification functionality and enhance Notifications…
AhmedIkram05 May 7, 2026
6415153
feat: enhance audit log functionality with actor name resolution and …
AhmedIkram05 May 7, 2026
99b11a2
fix: increase maximum height of task panel in BasicDashboard
AhmedIkram05 May 7, 2026
d99e096
refactor: consolidate imports from githubService and taskService in G…
AhmedIkram05 May 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ DC_DB := docker compose -f $(COMPOSE_DB)
DC_ALL := docker compose -f $(COMPOSE_DB) -f $(COMPOSE_BACKEND)

.PHONY: db-up db-down db-inspect db-logs db-reset
.PHONY: backend-build backend-up backend-down backend-logs
.PHONY: backend-build backend-up backend-down backend-logs backend-rebuild
.PHONY: up down reset

# Database
Expand Down Expand Up @@ -38,6 +38,11 @@ backend-down:
backend-logs:
$(DC_ALL) logs -f backend

backend-rebuild:
$(DC_ALL) down
$(DC_ALL) build backend
$(DC_ALL) up -d --wait

# Combined
up:
$(DC_ALL) up -d --wait
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
"""Add audit logs and system settings

Revision ID: 93a1f8b3c4d5
Revises: e2f4g5h6i7j8
Create Date: 2026-05-07 00:39:00.000000

"""
from alembic import op
import sqlalchemy as sa
import json
from datetime import datetime, timezone

# revision identifiers, used by Alembic.
revision = '93a1f8b3c4d5'
down_revision = 'e2f4g5h6i7j8'
branch_labels = None
depends_on = None

def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('audit_logs',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('actor_user_id', sa.Integer(), nullable=True),
sa.Column('actor_role', sa.String(length=20), nullable=True),
sa.Column('action', sa.String(length=100), nullable=False),
sa.Column('resource_type', sa.String(length=50), nullable=True),
sa.Column('resource_id', sa.String(length=50), nullable=True),
sa.Column('ip', sa.String(length=45), nullable=True),
sa.Column('user_agent', sa.String(length=255), nullable=True),
sa.Column('metadata_info', sa.JSON(), nullable=True),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.ForeignKeyConstraint(['actor_user_id'], ['users.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_index('idx_audit_logs_actor_time', 'audit_logs', ['actor_user_id', 'created_at'], unique=False)
op.create_index('idx_audit_logs_action', 'audit_logs', ['action'], unique=False)
op.create_index('idx_audit_logs_resource', 'audit_logs', ['resource_type'], unique=False)

system_settings_table = op.create_table('system_settings',
sa.Column('key', sa.String(length=100), nullable=False),
sa.Column('value', sa.JSON(), nullable=False),
sa.Column('updated_by', sa.Integer(), nullable=True),
sa.Column('updated_at', sa.DateTime(), nullable=True),
sa.ForeignKeyConstraint(['updated_by'], ['users.id'], ),
sa.PrimaryKeyConstraint('key')
)

bind = op.get_bind()
if bind.dialect.name == 'postgresql':
op.create_check_constraint(
'check_valid_role',
'users',
"role IN ('developer', 'team_lead', 'admin')"
)

# Seed default system settings
op.bulk_insert(
system_settings_table,
[
{
'key': 'app_name',
'value': 'DevSync',
'updated_at': datetime.now(timezone.utc)
},
{
'key': 'allow_registration',
'value': True,
'updated_at': datetime.now(timezone.utc)
},
{
'key': 'default_user_role',
'value': 'developer',
'updated_at': datetime.now(timezone.utc)
},
{
'key': 'github_integration_enabled',
'value': True,
'updated_at': datetime.now(timezone.utc)
},
{
'key': 'notification_settings',
'value': {
'email_notifications': True,
'task_assignments': True,
'project_updates': True
},
'updated_at': datetime.now(timezone.utc)
}
]
)
# ### end Alembic commands ###

def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
bind = op.get_bind()
if bind.dialect.name == 'postgresql':
op.drop_constraint('check_valid_role', 'users', type_='check')

op.drop_table('system_settings')

op.drop_index('idx_audit_logs_resource', table_name='audit_logs')
op.drop_index('idx_audit_logs_action', table_name='audit_logs')
op.drop_index('idx_audit_logs_actor_time', table_name='audit_logs')
op.drop_table('audit_logs')
# ### end Alembic commands ###
46 changes: 46 additions & 0 deletions backend/migrations/versions/e2f4g5h6i7j8_add_reports_table.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
"""Add reports table for storing generated reports

Revision ID: e2f4g5h6i7j8
Revises: 3c8d9e2f1a4b
Create Date: 2026-05-06 15:30:00.000000

"""
from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision = 'e2f4g5h6i7j8'
down_revision = '3c8d9e2f1a4b'
branch_labels = None
depends_on = None


def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('reports',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('user_id', sa.Integer(), nullable=False),
sa.Column('report_type', sa.String(length=50), nullable=False),
sa.Column('date_range', sa.String(length=50), nullable=False),
sa.Column('summary', sa.JSON(), nullable=False),
sa.Column('details', sa.JSON(), nullable=False),
sa.Column('generated_at', sa.DateTime(), nullable=True),
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_index('idx_reports_generated_at', 'reports', ['generated_at'], unique=False)
op.create_index('idx_reports_type', 'reports', ['report_type'], unique=False)
op.create_index('idx_reports_user_generated', 'reports', ['user_id', 'generated_at'], unique=False)
op.create_index('idx_reports_user_id', 'reports', ['user_id'], unique=False)
# ### end Alembic commands ###


def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index('idx_reports_user_id', table_name='reports')
op.drop_index('idx_reports_user_generated', table_name='reports')
op.drop_index('idx_reports_type', table_name='reports')
op.drop_index('idx_reports_generated_at', table_name='reports')
op.drop_table('reports')
# ### end Alembic commands ###
5 changes: 5 additions & 0 deletions backend/src/api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@
notifications_routes.register_routes(api_bp)
dashboard_routes.register_routes(api_bp)
admin_routes.register_routes(api_bp)
from .routes import report_routes
from .routes import audit_routes

report_routes.register_routes(api_bp)
audit_routes.register_routes(api_bp)
github_routes.register_routes(api_bp) # Add GitHub routes

def init_app(app):
Expand Down
37 changes: 21 additions & 16 deletions backend/src/api/controllers/admin_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
from ...db.models import db, User, Project, Task
from ..validators.admin_validator import validate_system_settings, validate_user_role_update
from ...auth.rbac import Role
from flask_jwt_extended import get_jwt_identity
from ...services import audit_service, settings_service


def _safe_query_all(model):
Expand Down Expand Up @@ -53,20 +55,7 @@ def get_system_stats():

def get_system_settings():
"""Controller function to get system settings"""
# This would typically retrieve settings from a database
# For now, return placeholder settings
settings = {
'app_name': 'DevSync',
'allow_registration': True,
'default_user_role': Role.DEVELOPER.value,
'github_integration_enabled': True,
'notification_settings': {
'email_notifications': True,
'task_assignments': True,
'project_updates': True
}
}

settings = settings_service.get_settings()
return jsonify({'settings': settings})

def update_system_settings():
Expand All @@ -78,8 +67,16 @@ def update_system_settings():
if validation_result:
return validation_result

# This would typically update settings in a database
# For now, just return success response
current_user = get_jwt_identity()
user_id = current_user.get('user_id') if isinstance(current_user, dict) else current_user

settings_service.update_settings(data, user_id)

audit_service.record(
action='settings_updated',
resource_type='settings',
metadata={'settings_updated': list(data.keys())}
)

return jsonify({
'message': 'System settings updated successfully',
Expand All @@ -100,9 +97,17 @@ def update_user_role(user_id):
return validation_result

# Update user role
old_role = user.role
user.role = data['role']
db.session.commit()

audit_service.record(
action='user_role_changed',
resource_type='user',
resource_id=user.id,
metadata={'old_role': old_role, 'new_role': user.role}
)

return jsonify({
'message': 'User role updated successfully',
'user': {
Expand Down
82 changes: 82 additions & 0 deletions backend/src/api/controllers/audit_controller.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
"""Audit Log Controller"""

from flask import request, jsonify
from ...db.models import AuditLog, User


def _build_actor_name_map(logs):
actor_ids = {log.actor_user_id for log in logs if getattr(log, 'actor_user_id', None) is not None}
if not actor_ids:
return {}

users = User.query.filter(User.id.in_(actor_ids)).all()
return {user.id: user.name for user in users}


def _serialize_audit_log(log, actor_name_map=None):
actor_name_map = actor_name_map or {}
actor_user_id = log.actor_user_id

return {
'id': log.id,
'actor_user_id': actor_user_id,
'actor_name': actor_name_map.get(actor_user_id),
'actor_role': log.actor_role,
'action': log.action,
'resource_type': log.resource_type,
'resource_id': log.resource_id,
'ip': log.ip,
'user_agent': log.user_agent,
'metadata': log.metadata_info,
'created_at': log.created_at.isoformat() if log.created_at else None
}

def get_audit_logs():
"""Get paginated and filtered audit logs"""
action = request.args.get('action')
actor_id = request.args.get('actor')
from_date = request.args.get('from')
to_date = request.args.get('to')

page = request.args.get('page', 1, type=int)
per_page = request.args.get('per_page', 50, type=int)

query = AuditLog.query

if action:
query = query.filter(AuditLog.action.ilike(f'%{action}%'))
if actor_id:
query = query.filter_by(actor_user_id=actor_id)
if from_date:
query = query.filter(AuditLog.created_at >= from_date)
if to_date:
query = query.filter(AuditLog.created_at <= to_date)

pagination = query.order_by(AuditLog.created_at.desc()).paginate(
page=page, per_page=per_page, error_out=False
)

actor_name_map = _build_actor_name_map(pagination.items)
logs_data = [_serialize_audit_log(log, actor_name_map) for log in pagination.items]

return jsonify({
'logs': logs_data,
'total': pagination.total,
'pages': pagination.pages,
'current_page': page
})

def get_audit_log_by_id(log_id):
"""Get a specific audit log"""
log = AuditLog.query.get_or_404(log_id)
actor_name = None

if log.actor_user_id is not None:
actor = User.query.get(log.actor_user_id)
actor_name = actor.name if actor else None

return jsonify({
'log': {
**_serialize_audit_log(log, {log.actor_user_id: actor_name} if actor_name else {}),
}
})
31 changes: 31 additions & 0 deletions backend/src/api/controllers/comments_controller.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,23 @@
# Comment controller - business logic

import logging
from flask import request, jsonify
from flask_jwt_extended import get_jwt_identity, get_jwt
from ...db.models import db, Comment, Task, User # Changed to relative import
from ...auth.rbac import Role # Changed to relative import
from ..validators.comment_validator import validate_comment_data # Changed to relative import
from ...services.notification_service import NotificationService

logger = logging.getLogger(__name__)


def _run_notification(callback, *args, **kwargs):
"""Create notifications without making the primary comment mutation fail."""
try:
callback(*args, **kwargs)
except Exception:
db.session.rollback()
logger.exception("Failed to create comment notification")

def get_task_comments(task_id):
"""Controller function to get all comments for a task"""
Expand Down Expand Up @@ -57,6 +70,24 @@ def add_comment(task_id):

db.session.add(new_comment)
db.session.commit()

mentioned_user_ids = data.get('mentioned_user_ids') or data.get('mentioned_users') or []
recipient_user_ids = {
getattr(task, 'assigned_to', None),
getattr(task, 'created_by', None),
}
recipient_user_ids.discard(None)

_run_notification(
NotificationService.comment_added_notification,
task_id,
getattr(task, 'title', f'Task {task_id}'),
getattr(task, 'project_id', None),
new_comment.id,
user_id,
mentioned_user_ids,
recipient_user_ids
)

# Get user info for response
user = User.query.get(user_id)
Expand Down
Loading
Loading