Skip to content

Commit ac22544

Browse files
committed
commit 9f8e7d6a2b3c4d5e6f7g8h9i0j1k2l3m4n5o6p
1 parent 48d4e46 commit ac22544

24 files changed

Lines changed: 3565 additions & 1 deletion

backend/config/settings.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@
8080
'support',
8181
'billing',
8282
'employment',
83+
'social_hub',
8384
]
8485

8586
MIDDLEWARE = [
@@ -278,6 +279,32 @@
278279
# Support system
279280
SUPPORT_ATTACHMENT_MAX_MB = config('SUPPORT_ATTACHMENT_MAX_MB', default=10, cast=int)
280281

282+
# ── Social Hub OAuth credentials ──────────────────────────────────────────
283+
# All values must be set in the environment (or .env file) — never hardcoded.
284+
SOCIAL_OAUTH = {
285+
'linkedin': {
286+
'client_id': config('LINKEDIN_CLIENT_ID', default=''),
287+
'client_secret': config('LINKEDIN_CLIENT_SECRET', default=''),
288+
},
289+
'facebook': {
290+
'client_id': config('FACEBOOK_CLIENT_ID', default=''),
291+
'client_secret': config('FACEBOOK_CLIENT_SECRET', default=''),
292+
},
293+
'twitter': {
294+
'client_id': config('TWITTER_CLIENT_ID', default=''),
295+
'client_secret': config('TWITTER_CLIENT_SECRET', default=''),
296+
},
297+
'tiktok': {
298+
'client_id': config('TIKTOK_CLIENT_ID', default=''),
299+
'client_secret': config('TIKTOK_CLIENT_SECRET', default=''),
300+
},
301+
'youtube': {
302+
'client_id': config('GOOGLE_CLIENT_ID', default=''),
303+
'client_secret': config('GOOGLE_CLIENT_SECRET', default=''),
304+
},
305+
}
306+
307+
281308
# JWT Settings
282309
SIMPLE_JWT = {
283310
'ACCESS_TOKEN_LIFETIME': timedelta(hours=1),

backend/config/urls.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
path('api/support/', include('support.urls')),
4747
path('api/billing/', include('billing.urls')),
4848
path('api/employment/', include('employment.urls')),
49+
path('api/social/', include('social_hub.urls')),
4950
]
5051

5152
# Serve media files in development

backend/social_hub/__init__.py

Whitespace-only changes.

backend/social_hub/admin.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
from django.contrib import admin
2+
from .models import (
3+
SocialAccount, SocialToken, MediaAsset, MediaBundle,
4+
SocialPost, SocialPostTarget, SocialAnalyticsSnapshot, SocialAuditLog,
5+
)
6+
7+
8+
@admin.register(SocialAccount)
9+
class SocialAccountAdmin(admin.ModelAdmin):
10+
list_display = ('account_name', 'platform', 'account_type', 'status', 'user', 'created_at')
11+
list_filter = ('platform', 'status')
12+
search_fields = ('account_name', 'account_handle', 'user__username')
13+
readonly_fields = ('id', 'created_at', 'updated_at')
14+
15+
16+
@admin.register(SocialToken)
17+
class SocialTokenAdmin(admin.ModelAdmin):
18+
list_display = ('social_account', 'expires_at', 'last_refresh_status', 'updated_at')
19+
readonly_fields = ('id', 'created_at', 'updated_at', 'access_token_enc', 'refresh_token_enc')
20+
21+
22+
@admin.register(MediaAsset)
23+
class MediaAssetAdmin(admin.ModelAdmin):
24+
list_display = ('original_filename', 'mime_type', 'size_bytes', 'user', 'created_at')
25+
search_fields = ('original_filename',)
26+
readonly_fields = ('id', 'hash', 'created_at', 'updated_at')
27+
28+
29+
@admin.register(SocialPost)
30+
class SocialPostAdmin(admin.ModelAdmin):
31+
list_display = ('__str__', 'status', 'user', 'scheduled_at', 'published_at', 'created_at')
32+
list_filter = ('status',)
33+
search_fields = ('title', 'body')
34+
readonly_fields = ('id', 'created_at', 'updated_at')
35+
36+
37+
@admin.register(SocialPostTarget)
38+
class SocialPostTargetAdmin(admin.ModelAdmin):
39+
list_display = ('social_post', 'platform', 'status', 'published_at')
40+
list_filter = ('platform', 'status')
41+
readonly_fields = ('id', 'created_at', 'updated_at')
42+
43+
44+
@admin.register(SocialAuditLog)
45+
class SocialAuditLogAdmin(admin.ModelAdmin):
46+
list_display = ('action', 'entity_type', 'entity_id', 'user', 'ip_address', 'created_at')
47+
list_filter = ('action', 'entity_type')
48+
readonly_fields = ('id', 'created_at')

backend/social_hub/apps.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
from django.apps import AppConfig
2+
3+
4+
class SocialHubConfig(AppConfig):
5+
default_auto_field = 'django.db.models.BigAutoField'
6+
name = 'social_hub'
7+
verbose_name = 'AtonixDev Social Hub'
Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
# Generated by Django 4.2.7 on 2026-03-11 01:39
2+
3+
from django.conf import settings
4+
from django.db import migrations, models
5+
import django.db.models.deletion
6+
import uuid
7+
8+
9+
class Migration(migrations.Migration):
10+
11+
initial = True
12+
13+
dependencies = [
14+
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
15+
]
16+
17+
operations = [
18+
migrations.CreateModel(
19+
name='MediaAsset',
20+
fields=[
21+
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
22+
('storage_path', models.CharField(max_length=1024)),
23+
('original_filename', models.CharField(blank=True, default='', max_length=512)),
24+
('mime_type', models.CharField(max_length=128)),
25+
('size_bytes', models.PositiveIntegerField(default=0)),
26+
('width', models.PositiveIntegerField(blank=True, null=True)),
27+
('height', models.PositiveIntegerField(blank=True, null=True)),
28+
('duration_seconds', models.PositiveIntegerField(blank=True, null=True)),
29+
('hash', models.CharField(blank=True, db_index=True, default='', max_length=64)),
30+
('created_at', models.DateTimeField(auto_now_add=True)),
31+
('updated_at', models.DateTimeField(auto_now=True)),
32+
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='social_media_assets', to=settings.AUTH_USER_MODEL)),
33+
],
34+
options={
35+
'verbose_name': 'Media Asset',
36+
'ordering': ['-created_at'],
37+
},
38+
),
39+
migrations.CreateModel(
40+
name='MediaBundle',
41+
fields=[
42+
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
43+
('name', models.CharField(blank=True, default='', max_length=255)),
44+
('created_at', models.DateTimeField(auto_now_add=True)),
45+
('updated_at', models.DateTimeField(auto_now=True)),
46+
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='social_media_bundles', to=settings.AUTH_USER_MODEL)),
47+
],
48+
options={
49+
'verbose_name': 'Media Bundle',
50+
'ordering': ['-created_at'],
51+
},
52+
),
53+
migrations.CreateModel(
54+
name='SocialAccount',
55+
fields=[
56+
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
57+
('platform', models.CharField(choices=[('linkedin', 'LinkedIn'), ('facebook', 'Facebook'), ('instagram', 'Instagram'), ('twitter', 'X (Twitter)'), ('tiktok', 'TikTok'), ('youtube', 'YouTube')], db_index=True, max_length=20)),
58+
('account_type', models.CharField(choices=[('personal', 'Personal'), ('page', 'Page'), ('business', 'Business'), ('channel', 'Channel')], default='personal', max_length=20)),
59+
('account_external_id', models.CharField(max_length=255)),
60+
('account_name', models.CharField(max_length=255)),
61+
('account_handle', models.CharField(blank=True, default='', max_length=255)),
62+
('avatar_url', models.URLField(blank=True, default='')),
63+
('status', models.CharField(choices=[('active', 'Active'), ('revoked', 'Revoked'), ('error', 'Error')], db_index=True, default='active', max_length=20)),
64+
('created_at', models.DateTimeField(auto_now_add=True)),
65+
('updated_at', models.DateTimeField(auto_now=True)),
66+
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='social_accounts', to=settings.AUTH_USER_MODEL)),
67+
],
68+
options={
69+
'verbose_name': 'Social Account',
70+
'ordering': ['-created_at'],
71+
},
72+
),
73+
migrations.CreateModel(
74+
name='SocialPost',
75+
fields=[
76+
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
77+
('title', models.CharField(blank=True, default='', max_length=255)),
78+
('body', models.TextField()),
79+
('status', models.CharField(choices=[('draft', 'Draft'), ('scheduled', 'Scheduled'), ('publishing', 'Publishing'), ('published', 'Published'), ('failed', 'Failed'), ('partial_published', 'Partial Published')], db_index=True, default='draft', max_length=30)),
80+
('scheduled_at', models.DateTimeField(blank=True, db_index=True, null=True)),
81+
('published_at', models.DateTimeField(blank=True, null=True)),
82+
('created_at', models.DateTimeField(auto_now_add=True)),
83+
('updated_at', models.DateTimeField(auto_now=True)),
84+
('media_bundle', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='posts', to='social_hub.mediabundle')),
85+
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='social_posts', to=settings.AUTH_USER_MODEL)),
86+
],
87+
options={
88+
'verbose_name': 'Social Post',
89+
'ordering': ['-created_at'],
90+
},
91+
),
92+
migrations.CreateModel(
93+
name='SocialToken',
94+
fields=[
95+
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
96+
('access_token_enc', models.TextField()),
97+
('refresh_token_enc', models.TextField(blank=True, default='')),
98+
('expires_at', models.DateTimeField(blank=True, null=True)),
99+
('scopes', models.JSONField(default=list)),
100+
('last_refresh_attempt_at', models.DateTimeField(blank=True, null=True)),
101+
('last_refresh_status', models.CharField(choices=[('success', 'Success'), ('failed', 'Failed'), ('pending', 'Pending')], default='pending', max_length=20)),
102+
('created_at', models.DateTimeField(auto_now_add=True)),
103+
('updated_at', models.DateTimeField(auto_now=True)),
104+
('social_account', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='token', to='social_hub.socialaccount')),
105+
],
106+
options={
107+
'verbose_name': 'Social Token',
108+
},
109+
),
110+
migrations.CreateModel(
111+
name='SocialPostTarget',
112+
fields=[
113+
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
114+
('platform', models.CharField(choices=[('linkedin', 'LinkedIn'), ('facebook', 'Facebook'), ('instagram', 'Instagram'), ('twitter', 'X (Twitter)'), ('tiktok', 'TikTok'), ('youtube', 'YouTube')], max_length=20)),
115+
('platform_post_id', models.CharField(blank=True, default='', max_length=512)),
116+
('status', models.CharField(choices=[('pending', 'Pending'), ('queued', 'Queued'), ('publishing', 'Publishing'), ('published', 'Published'), ('failed', 'Failed')], db_index=True, default='pending', max_length=20)),
117+
('error_code', models.CharField(blank=True, default='', max_length=64)),
118+
('error_message', models.TextField(blank=True, default='')),
119+
('published_at', models.DateTimeField(blank=True, null=True)),
120+
('created_at', models.DateTimeField(auto_now_add=True)),
121+
('updated_at', models.DateTimeField(auto_now=True)),
122+
('social_account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='post_targets', to='social_hub.socialaccount')),
123+
('social_post', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='targets', to='social_hub.socialpost')),
124+
],
125+
options={
126+
'verbose_name': 'Post Target',
127+
'ordering': ['-created_at'],
128+
},
129+
),
130+
migrations.CreateModel(
131+
name='SocialAuditLog',
132+
fields=[
133+
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
134+
('action', models.CharField(db_index=True, max_length=64)),
135+
('entity_type', models.CharField(blank=True, default='', max_length=64)),
136+
('entity_id', models.UUIDField(blank=True, null=True)),
137+
('metadata', models.JSONField(default=dict)),
138+
('ip_address', models.CharField(blank=True, default='', max_length=64)),
139+
('created_at', models.DateTimeField(auto_now_add=True, db_index=True)),
140+
('user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='social_audit_logs', to=settings.AUTH_USER_MODEL)),
141+
],
142+
options={
143+
'verbose_name': 'Social Audit Log',
144+
'ordering': ['-created_at'],
145+
},
146+
),
147+
migrations.CreateModel(
148+
name='SocialAnalyticsSnapshot',
149+
fields=[
150+
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
151+
('platform', models.CharField(choices=[('linkedin', 'LinkedIn'), ('facebook', 'Facebook'), ('instagram', 'Instagram'), ('twitter', 'X (Twitter)'), ('tiktok', 'TikTok'), ('youtube', 'YouTube')], max_length=20)),
152+
('snapshot_date', models.DateField(db_index=True)),
153+
('impressions', models.PositiveIntegerField(blank=True, null=True)),
154+
('reach', models.PositiveIntegerField(blank=True, null=True)),
155+
('clicks', models.PositiveIntegerField(blank=True, null=True)),
156+
('likes', models.PositiveIntegerField(blank=True, null=True)),
157+
('comments', models.PositiveIntegerField(blank=True, null=True)),
158+
('shares', models.PositiveIntegerField(blank=True, null=True)),
159+
('saves', models.PositiveIntegerField(blank=True, null=True)),
160+
('video_views', models.PositiveIntegerField(blank=True, null=True)),
161+
('raw_payload', models.JSONField(default=dict)),
162+
('created_at', models.DateTimeField(auto_now_add=True)),
163+
('post_target', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='analytics', to='social_hub.socialposttarget')),
164+
],
165+
options={
166+
'verbose_name': 'Analytics Snapshot',
167+
'ordering': ['-snapshot_date'],
168+
},
169+
),
170+
migrations.CreateModel(
171+
name='MediaBundleAsset',
172+
fields=[
173+
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
174+
('position', models.PositiveSmallIntegerField(default=0)),
175+
('asset', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='bundle_memberships', to='social_hub.mediaasset')),
176+
('bundle', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='bundle_assets', to='social_hub.mediabundle')),
177+
],
178+
options={
179+
'ordering': ['position'],
180+
},
181+
),
182+
migrations.AddIndex(
183+
model_name='socialpost',
184+
index=models.Index(fields=['user', 'status'], name='social_hub__user_id_70dfdb_idx'),
185+
),
186+
migrations.AddIndex(
187+
model_name='socialpost',
188+
index=models.Index(fields=['status', 'scheduled_at'], name='social_hub__status_7733b1_idx'),
189+
),
190+
migrations.AddIndex(
191+
model_name='socialauditlog',
192+
index=models.Index(fields=['user', 'action', 'created_at'], name='social_hub__user_id_66481c_idx'),
193+
),
194+
migrations.AddIndex(
195+
model_name='socialauditlog',
196+
index=models.Index(fields=['entity_type', 'entity_id'], name='social_hub__entity__bf9cb8_idx'),
197+
),
198+
migrations.AlterUniqueTogether(
199+
name='socialanalyticssnapshot',
200+
unique_together={('post_target', 'snapshot_date')},
201+
),
202+
migrations.AddIndex(
203+
model_name='socialaccount',
204+
index=models.Index(fields=['user', 'platform', 'status'], name='social_hub__user_id_cf925c_idx'),
205+
),
206+
migrations.AlterUniqueTogether(
207+
name='socialaccount',
208+
unique_together={('user', 'platform', 'account_external_id')},
209+
),
210+
migrations.AlterUniqueTogether(
211+
name='mediabundleasset',
212+
unique_together={('bundle', 'asset')},
213+
),
214+
]

backend/social_hub/migrations/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)