feat: add RBAC migrations, user modules, admin API updates, and utility scripts
This commit is contained in:
92
admin_api/migrations/0002_rbac_models.py
Normal file
92
admin_api/migrations/0002_rbac_models.py
Normal file
@@ -0,0 +1,92 @@
|
||||
# Generated by Django 4.2.21 on 2026-03-26 13:42
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('admin_api', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='CustomRole',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=100)),
|
||||
('slug', models.SlugField(unique=True)),
|
||||
('description', models.TextField(blank=True, default='')),
|
||||
('scopes', models.JSONField(default=list)),
|
||||
('is_system', models.BooleanField(default=False)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['name'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Department',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=100)),
|
||||
('slug', models.SlugField(unique=True)),
|
||||
('description', models.TextField(blank=True, default='')),
|
||||
('base_scopes', models.JSONField(default=list)),
|
||||
('color', models.CharField(default='#3B82F6', max_length=7)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['name'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Squad',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=100)),
|
||||
('extra_scopes', models.JSONField(default=list)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('department', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='squads', to='admin_api.department')),
|
||||
('manager', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='managed_squads', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['name'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='StaffProfile',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('staff_role', models.CharField(choices=[('SUPER_ADMIN', 'Super Admin'), ('MANAGER', 'Manager'), ('MEMBER', 'Member')], default='MEMBER', max_length=20)),
|
||||
('status', models.CharField(choices=[('active', 'Active'), ('invited', 'Invited'), ('deactivated', 'Deactivated')], default='active', max_length=20)),
|
||||
('joined_at', models.DateTimeField(auto_now_add=True)),
|
||||
('department', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='staff_members', to='admin_api.department')),
|
||||
('squad', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='members', to='admin_api.squad')),
|
||||
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='staff_profile', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['user__first_name'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='AuditLog',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('action', models.CharField(max_length=100)),
|
||||
('target_type', models.CharField(max_length=50)),
|
||||
('target_id', models.CharField(max_length=50)),
|
||||
('details', models.JSONField(default=dict)),
|
||||
('ip_address', models.GenericIPAddressField(blank=True, null=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='audit_logs', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['-created_at'],
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -63,3 +63,121 @@ class ReviewInteraction(models.Model):
|
||||
|
||||
def __str__(self):
|
||||
return f'{self.username} {self.interaction_type} on Review #{self.review_id}'
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# RBAC Models
|
||||
# ---------------------------------------------------------------------------
|
||||
from accounts.models import User
|
||||
|
||||
|
||||
class Department(models.Model):
|
||||
name = models.CharField(max_length=100)
|
||||
slug = models.SlugField(unique=True)
|
||||
description = models.TextField(blank=True, default='')
|
||||
base_scopes = models.JSONField(default=list)
|
||||
color = models.CharField(max_length=7, default='#3B82F6')
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class Squad(models.Model):
|
||||
name = models.CharField(max_length=100)
|
||||
department = models.ForeignKey(Department, on_delete=models.CASCADE, related_name='squads')
|
||||
manager = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name='managed_squads')
|
||||
extra_scopes = models.JSONField(default=list)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.department.name} > {self.name}"
|
||||
|
||||
|
||||
class StaffProfile(models.Model):
|
||||
ROLE_CHOICES = [('SUPER_ADMIN', 'Super Admin'), ('MANAGER', 'Manager'), ('MEMBER', 'Member')]
|
||||
STATUS_CHOICES = [('active', 'Active'), ('invited', 'Invited'), ('deactivated', 'Deactivated')]
|
||||
|
||||
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='staff_profile')
|
||||
department = models.ForeignKey(Department, on_delete=models.SET_NULL, null=True, blank=True, related_name='staff_members')
|
||||
squad = models.ForeignKey(Squad, on_delete=models.SET_NULL, null=True, blank=True, related_name='members')
|
||||
staff_role = models.CharField(max_length=20, choices=ROLE_CHOICES, default='MEMBER')
|
||||
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='active')
|
||||
joined_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ['user__first_name']
|
||||
|
||||
def get_effective_scopes(self):
|
||||
if self.staff_role == 'SUPER_ADMIN' or self.user.is_superuser:
|
||||
return ['*']
|
||||
scopes = set()
|
||||
if self.department:
|
||||
scopes.update(self.department.base_scopes or [])
|
||||
if self.squad:
|
||||
scopes.update(self.squad.extra_scopes or [])
|
||||
if self.staff_role == 'MANAGER':
|
||||
scopes.add('settings.staff')
|
||||
return list(scopes)
|
||||
|
||||
def get_allowed_modules(self):
|
||||
scopes = self.get_effective_scopes()
|
||||
if '*' in scopes:
|
||||
return ['dashboard', 'partners', 'events', 'ad-control', 'users', 'reviews', 'contributions', 'financials', 'settings']
|
||||
SCOPE_TO_MODULE = {
|
||||
'users': 'users',
|
||||
'events': 'events',
|
||||
'finance': 'financials',
|
||||
'partners': 'partners',
|
||||
'tickets': 'dashboard',
|
||||
'settings': 'settings',
|
||||
'ads': 'ad-control',
|
||||
'contributions': 'contributions',
|
||||
}
|
||||
modules = {'dashboard'}
|
||||
for scope in scopes:
|
||||
prefix = scope.split('.')[0]
|
||||
if prefix in SCOPE_TO_MODULE:
|
||||
modules.add(SCOPE_TO_MODULE[prefix])
|
||||
return list(modules)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.user.username} ({self.staff_role})"
|
||||
|
||||
|
||||
class CustomRole(models.Model):
|
||||
name = models.CharField(max_length=100)
|
||||
slug = models.SlugField(unique=True)
|
||||
description = models.TextField(blank=True, default='')
|
||||
scopes = models.JSONField(default=list)
|
||||
is_system = models.BooleanField(default=False)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class AuditLog(models.Model):
|
||||
user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, related_name='audit_logs')
|
||||
action = models.CharField(max_length=100)
|
||||
target_type = models.CharField(max_length=50)
|
||||
target_id = models.CharField(max_length=50)
|
||||
details = models.JSONField(default=dict)
|
||||
ip_address = models.GenericIPAddressField(null=True, blank=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ['-created_at']
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.action} by {self.user} at {self.created_at}"
|
||||
|
||||
@@ -18,6 +18,8 @@ urlpatterns = [
|
||||
path('partners/<int:pk>/', views.PartnerDetailView.as_view(), name='partner-detail'),
|
||||
path('partners/<int:pk>/status/', views.PartnerStatusView.as_view(), name='partner-status'),
|
||||
path('partners/<int:pk>/kyc/review/', views.PartnerKYCReviewView.as_view(), name='partner-kyc-review'),
|
||||
path('partners/onboard/', views.PartnerOnboardView.as_view(), name='partner-onboard'),
|
||||
path('partners/<int:partner_id>/staff/', views.PartnerStaffCreateView.as_view(), name='partner-staff-create'),
|
||||
path('users/metrics/', views.UserMetricsView.as_view(), name='user-metrics'),
|
||||
path('users/', views.UserListView.as_view(), name='user-list'),
|
||||
path('users/<int:pk>/', views.UserDetailView.as_view(), name='user-detail'),
|
||||
@@ -41,8 +43,34 @@ urlpatterns = [
|
||||
path('reviews/<int:pk>/moderate/', views.ReviewModerationView.as_view(), name='review-moderate'),
|
||||
path('reviews/<int:pk>/', views.ReviewDeleteView.as_view(), name='review-delete'),
|
||||
|
||||
path('gamification/submit-event/', views.GamificationSubmitEventView.as_view(), name='gamification-submit-event'),
|
||||
path('gamification/submit-event', views.GamificationSubmitEventView.as_view()),
|
||||
path('shop/items/', views.ShopItemsView.as_view(), name='shop-items'),
|
||||
path('shop/items', views.ShopItemsView.as_view()),
|
||||
path('shop/redeem/', views.ShopRedeemView.as_view(), name='shop-redeem'),
|
||||
path('shop/redeem', views.ShopRedeemView.as_view()),
|
||||
|
||||
path('gamification/dashboard/', views.GamificationDashboardView.as_view(), name='gamification-dashboard'),
|
||||
path('gamification/dashboard', views.GamificationDashboardView.as_view()),
|
||||
|
||||
# Payment gateway settings
|
||||
path('settings/payment-gateway/active/', views.ActivePaymentGatewayView.as_view(), name='active-payment-gateway'),
|
||||
path('settings/payment-gateways/', views.PaymentGatewaySettingsView.as_view(), name='payment-gateways'),
|
||||
path('settings/payment-gateways/<int:pk>/', views.PaymentGatewaySettingsView.as_view(), name='payment-gateway-detail'),
|
||||
|
||||
# RBAC
|
||||
path('rbac/departments/', views.DepartmentListCreateView.as_view(), name='rbac-department-list'),
|
||||
path('rbac/departments/<int:pk>/', views.DepartmentDetailView.as_view(), name='rbac-department-detail'),
|
||||
path('rbac/squads/', views.SquadListCreateView.as_view(), name='rbac-squad-list'),
|
||||
path('rbac/squads/<int:pk>/', views.SquadDetailView.as_view(), name='rbac-squad-detail'),
|
||||
path('rbac/staff/', views.StaffListView.as_view(), name='rbac-staff-list'),
|
||||
path('rbac/staff/invite/', views.StaffInviteView.as_view(), name='rbac-staff-invite'),
|
||||
path('rbac/staff/<int:pk>/', views.StaffUpdateView.as_view(), name='rbac-staff-update'),
|
||||
path('rbac/staff/<int:pk>/deactivate/', views.StaffDeactivateView.as_view(), name='rbac-staff-deactivate'),
|
||||
path('rbac/staff/<int:pk>/move/', views.StaffMoveView.as_view(), name='rbac-staff-move'),
|
||||
path('rbac/roles/', views.RoleListCreateView.as_view(), name='rbac-role-list'),
|
||||
path('rbac/roles/<int:pk>/', views.RoleDetailView.as_view(), name='rbac-role-detail'),
|
||||
path('rbac/scopes/', views.ScopeListView.as_view(), name='rbac-scope-list'),
|
||||
path('rbac/org-tree/', views.OrgTreeView.as_view(), name='rbac-org-tree'),
|
||||
path('rbac/audit-log/', views.AuditLogListView.as_view(), name='rbac-audit-log'),
|
||||
]
|
||||
@@ -682,6 +682,8 @@ def _serialize_event(e):
|
||||
'isFeatured': bool(e.is_featured),
|
||||
'isTopEvent': bool(e.is_top_event),
|
||||
'source': e.source or 'eventify',
|
||||
'eventTypeId': e.event_type_id,
|
||||
'eventTypeName': e.event_type.event_type if e.event_type_id and e.event_type else '',
|
||||
}
|
||||
|
||||
|
||||
@@ -740,11 +742,13 @@ class EventListView(APIView):
|
||||
def get(self, request):
|
||||
from events.models import Event
|
||||
from django.db.models import Q
|
||||
qs = Event.objects.select_related('partner').all()
|
||||
qs = Event.objects.select_related('partner', 'event_type').all()
|
||||
if s := request.GET.get('status'):
|
||||
reverse_map = {v: k for k, v in _EVENT_STATUS_MAP.items()}
|
||||
backend_status = reverse_map.get(s, s)
|
||||
qs = qs.filter(event_status=backend_status)
|
||||
if etid := request.GET.get('event_type'):
|
||||
qs = qs.filter(event_type_id=etid)
|
||||
if pid := request.GET.get('partner_id'):
|
||||
qs = qs.filter(partner_id=pid)
|
||||
if q := request.GET.get('search'):
|
||||
@@ -2238,3 +2242,91 @@ class PartnerStaffCreateView(APIView):
|
||||
},
|
||||
status=status.HTTP_201_CREATED,
|
||||
)
|
||||
|
||||
|
||||
# ─── Gamification Dashboard (stub) ───────────────────────────────────────────
|
||||
class GamificationDashboardView(APIView):
|
||||
permission_classes = [] # public for now; restrict when auth is wired up
|
||||
|
||||
def get(self, request):
|
||||
user_id = request.GET.get('user_id', '')
|
||||
return Response({
|
||||
'status': 'success',
|
||||
'profile': {
|
||||
'user_id': user_id,
|
||||
'current_tier': 'BRONZE',
|
||||
'current_ep': 0,
|
||||
'current_rp': 0,
|
||||
'lifetime_ep': 0,
|
||||
},
|
||||
'submissions': [],
|
||||
})
|
||||
|
||||
|
||||
# ─── Gamification: Event Submission (stub) ────────────────────────────────────
|
||||
class GamificationSubmitEventView(APIView):
|
||||
permission_classes = []
|
||||
|
||||
def post(self, request):
|
||||
data = request.data
|
||||
return Response({
|
||||
'status': 'success',
|
||||
'submission': {
|
||||
'id': 1,
|
||||
'event_name': data.get('event_name', ''),
|
||||
'status': 'PENDING',
|
||||
'total_ep_awarded': 0,
|
||||
'created_at': __import__('datetime').datetime.now().isoformat(),
|
||||
},
|
||||
'message': 'Event submitted for review. You will earn EP once approved!',
|
||||
})
|
||||
|
||||
|
||||
# ─── Reward Shop: List Items (stub) ──────────────────────────────────────────
|
||||
class ShopItemsView(APIView):
|
||||
permission_classes = []
|
||||
|
||||
def get(self, request):
|
||||
return Response({
|
||||
'status': 'success',
|
||||
'items': [
|
||||
{
|
||||
'id': 1,
|
||||
'name': 'BookMyShow Voucher',
|
||||
'description': 'Get a Rs.100 BookMyShow gift card',
|
||||
'rp_cost': 50,
|
||||
'stock_quantity': 10,
|
||||
},
|
||||
{
|
||||
'id': 2,
|
||||
'name': 'Event Priority Listing',
|
||||
'description': 'Feature your next event at the top for 7 days',
|
||||
'rp_cost': 30,
|
||||
'stock_quantity': 5,
|
||||
},
|
||||
{
|
||||
'id': 3,
|
||||
'name': 'Eventify Merch Pack',
|
||||
'description': 'Exclusive stickers, badge & notebook',
|
||||
'rp_cost': 100,
|
||||
'stock_quantity': 3,
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
|
||||
# ─── Reward Shop: Redeem (stub) ──────────────────────────────────────────────
|
||||
class ShopRedeemView(APIView):
|
||||
permission_classes = []
|
||||
|
||||
def post(self, request):
|
||||
import uuid
|
||||
item_id = request.data.get('item_id')
|
||||
return Response({
|
||||
'status': 'success',
|
||||
'voucher': {
|
||||
'item_id': item_id,
|
||||
'voucher_code_issued': 'EVF-' + uuid.uuid4().hex[:8].upper(),
|
||||
},
|
||||
'message': 'Reward redeemed successfully!',
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user