77
88import json
99
10+ from django import forms
1011from django .contrib import admin , messages
1112from django .db .models import Avg
1213from django .utils import timezone
1617from unfold .admin import ModelAdmin
1718
1819from core .models import (
20+ BlueskyCredentials ,
1921 Content ,
2022 Entity ,
2123 IngestionRun ,
2931from core .plugins import get_plugin_for_source_config , validate_plugin_config
3032
3133
34+ class BlueskyCredentialsAdminForm (forms .ModelForm ):
35+ """Admin form that accepts a plaintext app password without exposing ciphertext."""
36+
37+ app_password = forms .CharField (
38+ required = False ,
39+ strip = False ,
40+ widget = forms .PasswordInput (render_value = False ),
41+ help_text = (
42+ "Leave blank to keep the existing app password. Stored encrypted at rest."
43+ ),
44+ label = "App password" ,
45+ )
46+
47+ class Meta :
48+ model = BlueskyCredentials
49+ fields = ["project" , "handle" , "app_password" , "pds_url" , "is_active" ]
50+
51+ def clean (self ):
52+ """Require an app password when creating credentials for the first time."""
53+
54+ cleaned_data = super ().clean ()
55+ app_password = cleaned_data .get ("app_password" , "" )
56+ if not self .instance .has_app_password () and not app_password :
57+ self .add_error ("app_password" , "App password is required." )
58+ return cleaned_data
59+
60+ def save (self , commit = True ):
61+ """Encrypt a new app password before saving the model instance."""
62+
63+ instance = super ().save (commit = False )
64+ app_password = self .cleaned_data .get ("app_password" , "" )
65+ if app_password :
66+ instance .set_app_password (app_password )
67+ if commit :
68+ instance .save ()
69+ return instance
70+
71+
3272@admin .register (Project )
3373class ProjectAdmin (ExportActionMixin , admin .ModelAdmin ):
3474 """Admin configuration for top-level project workspaces."""
@@ -49,6 +89,100 @@ class ProjectAdmin(ExportActionMixin, admin.ModelAdmin):
4989 list_editable = ("content_retention_days" ,)
5090
5191
92+ @admin .register (BlueskyCredentials )
93+ class BlueskyCredentialsAdmin (ModelAdmin ):
94+ """Admin view for project-scoped Bluesky authentication settings."""
95+
96+ form = BlueskyCredentialsAdminForm
97+ actions = ["verify_selected_credentials" ]
98+ list_display = (
99+ "project" ,
100+ "handle" ,
101+ "display_pds_host" ,
102+ "has_stored_app_password" ,
103+ "is_active" ,
104+ "last_verified_at" ,
105+ )
106+ list_filter = ("is_active" , ("project" , admin .RelatedOnlyFieldListFilter ))
107+ search_fields = ("project__name" , "handle" , "pds_url" )
108+ autocomplete_fields = ("project" ,)
109+ readonly_fields = (
110+ "has_stored_app_password" ,
111+ "last_verified_at" ,
112+ "last_error" ,
113+ "created_at" ,
114+ "updated_at" ,
115+ )
116+ fieldsets = (
117+ (
118+ "Account" ,
119+ {"fields" : ("project" , "handle" , "app_password" , "is_active" )},
120+ ),
121+ (
122+ "PDS Override" ,
123+ {
124+ "fields" : ("pds_url" ,),
125+ "description" : "Leave blank to use the default Bluesky-hosted account flow." ,
126+ },
127+ ),
128+ (
129+ "Verification" ,
130+ {
131+ "fields" : (
132+ "has_stored_app_password" ,
133+ "last_verified_at" ,
134+ "last_error" ,
135+ "created_at" ,
136+ "updated_at" ,
137+ )
138+ },
139+ ),
140+ )
141+
142+ @admin .display (description = "PDS" )
143+ def display_pds_host (self , obj ):
144+ """Show whether the credentials use the hosted default or a custom PDS."""
145+
146+ return obj .pds_url or "Bluesky hosted default"
147+
148+ @admin .display (boolean = True , description = "Stored App Password" )
149+ def has_stored_app_password (self , obj ):
150+ """Return whether an encrypted app password has been configured."""
151+
152+ return obj .has_app_password ()
153+
154+ @admin .action (description = "Verify Selected Credentials" )
155+ def verify_selected_credentials (self , request , queryset ):
156+ """Authenticate the selected Bluesky accounts and report the outcome."""
157+
158+ from core .plugins .bluesky import BlueskySourcePlugin
159+
160+ verified_credentials = []
161+ failed_credentials = []
162+
163+ for credentials in queryset .select_related ("project" ):
164+ try :
165+ BlueskySourcePlugin .verify_credentials (credentials )
166+ except Exception as exc :
167+ failed_credentials .append (f"{ credentials } : { exc } " )
168+ else :
169+ verified_credentials .append (str (credentials ))
170+
171+ if verified_credentials :
172+ self .message_user (
173+ request ,
174+ f"Credential verification passed for { len (verified_credentials )} account(s)." ,
175+ messages .SUCCESS ,
176+ )
177+
178+ if failed_credentials :
179+ self .message_user (
180+ request ,
181+ "Credential verification failed for: " + "; " .join (failed_credentials ),
182+ messages .ERROR ,
183+ )
184+
185+
52186@admin .register (ProjectConfig )
53187class ProjectConfigAdmin (admin .ModelAdmin ):
54188 """Admin configuration for per-project scoring settings."""
0 commit comments