feat: add RBAC migrations, user modules, admin API updates, and utility scripts

This commit is contained in:
2026-04-02 04:06:02 +00:00
parent 1b6185c758
commit 255519473b
10 changed files with 481 additions and 8 deletions

View File

@@ -0,0 +1,23 @@
# Generated by Django 4.2.21 on 2026-03-31 08:32
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('accounts', '0010_alter_user_id'),
]
operations = [
migrations.AddField(
model_name='user',
name='allowed_modules',
field=models.TextField(blank=True, help_text='Comma-separated module slugs this user can access', null=True),
),
migrations.AlterField(
model_name='user',
name='id',
field=models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
]

View File

@@ -37,6 +37,23 @@ class User(AbstractUser):
profile_picture = models.ImageField(upload_to='profile_pictures/', blank=True, null=True, default='default.png')
allowed_modules = models.TextField(
blank=True, null=True,
help_text="Comma-separated module slugs this user can access"
)
ALL_MODULES = ["dashboard", "partners", "events", "ad-control", "users", "reviews", "contributions", "financials", "settings"]
def get_allowed_modules(self):
ALL = ["dashboard", "partners", "events", "ad-control", "users", "reviews", "contributions", "financials", "settings"]
if self.is_superuser or self.role == "admin":
return ALL
if self.allowed_modules:
return [m.strip() for m in self.allowed_modules.split(",") if m.strip()]
if self.role == "manager":
return ALL
return []
objects = UserManager()
def __str__(self):

View 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'],
},
),
]

View File

@@ -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}"

View File

@@ -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'),
]

View File

@@ -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!',
})

25
create_temp_user.py Normal file
View File

@@ -0,0 +1,25 @@
import os
import django
import sys
sys.path.append('/var/www/myproject/eventify_prod')
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'eventify.settings')
django.setup()
from django.contrib.auth import get_user_model
User = get_user_model()
username = 'support_agent'
password = 'AgentPass123!'
email = 'agent@example.com'
if User.objects.filter(username=username).exists():
print(f"User {username} already exists. Resetting password.")
u = User.objects.get(username=username)
u.set_password(password)
u.save()
else:
print(f"Creating user {username}.")
User.objects.create_superuser(username, email, password)
print("Done.")

View File

@@ -16,13 +16,35 @@
*{box-sizing:border-box}
body{margin:0;font-family:Inter, ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial;background:var(--muted);color:#111}
.auth-wrapper{display:flex;min-height:100vh}
/* LEFT PANEL — video */
.auth-left{
position:relative;
width:40%;
min-width:320px;
background:linear-gradient(180deg,var(--blue1),var(--blue2));
color:#fff;padding:48px;display:flex;flex-direction:column;justify-content:center;gap:10px;
overflow:hidden;
color:#fff;
display:flex;flex-direction:column;justify-content:flex-end;
}
.brand{font-weight:700;font-size:28px}
.auth-left video{
position:absolute;inset:0;
width:100%;height:100%;
object-fit:cover;
z-index:0;
}
/* dark gradient overlay for text legibility */
.auth-left::after{
content:'';
position:absolute;inset:0;
background:linear-gradient(180deg,rgba(10,20,60,0.35) 0%,rgba(10,20,60,0.72) 100%);
z-index:1;
}
.auth-left-content{
position:relative;z-index:2;
padding:48px;
display:flex;flex-direction:column;gap:10px;
}
.brand{font-weight:700;font-size:28px;letter-spacing:-0.5px}
.auth-left h1{font-size:36px;margin:0}
.auth-left p{opacity:.92;margin:0;font-size:16px}
@@ -52,10 +74,12 @@
.message.warning{background:#fff7e6;color:#7a4b00}
.errorlist{color:#b00020;margin:6px 0 0 0;font-size:13px}
/* responsive */
/* responsive — mobile: hide video, show compact gradient header */
@media (max-width:900px){
.auth-wrapper{flex-direction:column}
.auth-left{width:100%;min-height:180px;padding:28px;text-align:center}
.auth-left{width:100%;min-height:180px;justify-content:flex-end;}
.auth-left video{display:block;}
.auth-left-content{padding:28px;text-align:center}
.auth-right{padding:20px}
.auth-card{border-radius:14px;padding:28px}
}
@@ -64,8 +88,14 @@
<body>
<div class="auth-wrapper">
<div class="auth-left">
<div class="brand">Eventify</div>
<p>{% block left_subtext %}Your events at your fingertips.{% endblock %}</p>
<video autoplay muted loop playsinline preload="auto" poster="https://images.pexels.com/videos/36761729/kerala-kerala-tourism-36761729.jpeg?auto=compress&cs=tinysrgb&w=750">
<source src="https://videos.pexels.com/video-files/36761729/15579487_1920_1080_30fps.mp4" type="video/mp4">
<source src="https://videos.pexels.com/video-files/36761729/15579486_1280_720_30fps.mp4" type="video/mp4">
</video>
<div class="auth-left-content">
<div class="brand">Eventify</div>
<p>{% block left_subtext %}Your events at your fingertips.{% endblock %}</p>
</div>
</div>
<div class="auth-right">

29
update_events.py Normal file
View File

@@ -0,0 +1,29 @@
import os
import django
import sys
import datetime
# Add the project directory to sys.path
sys.path.append('/var/www/myproject/eventify_prod')
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'eventify.settings')
django.setup()
from events.models import Event
start = datetime.date(2026, 1, 1)
end = datetime.date(2026, 12, 31)
print(f"Checking for events from {start} to {end}...")
events = Event.objects.filter(start_date=start, end_date=end)
count = events.count()
print(f"Found {count} events matching the criteria.")
if count > 0:
# Update matched events
updated_count = events.update(all_year_event=True)
print(f"Successfully updated {updated_count} events to be 'All Year'.")
else:
print("No events found to update.")

19
user.py Normal file
View File

@@ -0,0 +1,19 @@
import os
import django
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "eventify.settings")
django.setup()
from django.contrib.auth import get_user_model
User = get_user_model()
def make_all_users_admin():
users = User.objects.all()
for user in users:
user.role = "admin" # assuming role field exists
user.save()
print(f"Updated: {user.username} -> Admin")
if __name__ == "__main__":
make_all_users_admin()
print("All users updated to admin role!")