0
# Admin Interface
1
2
Admin classes and forms for managing OTP devices through Django's admin interface, including a two-factor authentication admin site.
3
4
## Capabilities
5
6
### OTP Admin Site
7
8
Admin site that requires two-factor authentication for access.
9
10
```python { .api }
11
class OTPAdminSite(admin.AdminSite):
12
"""Admin site requiring two-factor authentication."""
13
14
name = 'otpadmin' # Default instance name
15
login_form = OTPAdminAuthenticationForm
16
login_template = None # Auto-selected template
17
18
def has_permission(self, request) -> bool:
19
"""Check user permissions including OTP verification."""
20
```
21
22
### Admin Authentication Form
23
24
Authentication form for the OTP admin site.
25
26
```python { .api }
27
class OTPAdminAuthenticationForm(AdminAuthenticationForm, OTPAuthenticationFormMixin):
28
"""Admin authentication form with OTP support."""
29
30
otp_device = forms.CharField(widget=forms.Select)
31
otp_token = forms.CharField(required=False, widget=forms.TextInput)
32
otp_challenge = forms.CharField(required=False, widget=forms.TextInput)
33
```
34
35
### Utility Functions
36
37
#### user_model_search_fields
38
39
Generate search fields and help text for user model fields.
40
41
```python { .api }
42
def user_model_search_fields(field_names):
43
"""
44
Generate search fields and help text for user model fields.
45
46
Parameters:
47
- field_names: list - List of field names to check
48
49
Returns:
50
tuple - (search_fields, help_text)
51
"""
52
```
53
54
## Usage Examples
55
56
### Setting Up OTP Admin Site
57
58
```python
59
# admin.py
60
from django_otp.admin import OTPAdminSite
61
from django.contrib import admin
62
63
# Create OTP admin site instance
64
otp_admin_site = OTPAdminSite(name='otpadmin')
65
66
# Register models with OTP admin
67
from myapp.models import MyModel
68
otp_admin_site.register(MyModel)
69
70
# URLs.py
71
from django.urls import path, include
72
73
urlpatterns = [
74
path('admin/', admin.site.urls),
75
path('otpadmin/', otp_admin_site.urls), # OTP-protected admin
76
]
77
```
78
79
### Custom Device Admin
80
81
```python
82
from django.contrib import admin
83
from django_otp.plugins.otp_totp.models import TOTPDevice
84
from django_otp.admin import user_model_search_fields
85
86
@admin.register(TOTPDevice)
87
class CustomTOTPDeviceAdmin(admin.ModelAdmin):
88
"""Custom admin for TOTP devices."""
89
90
list_display = ['user', 'name', 'confirmed', 'created_at']
91
list_filter = ['confirmed', 'created_at']
92
search_fields = user_model_search_fields(['username', 'email'])
93
readonly_fields = ['key', 'created_at', 'last_used_at']
94
95
fieldsets = (
96
(None, {
97
'fields': ('user', 'name', 'confirmed')
98
}),
99
('Device Settings', {
100
'fields': ('digits', 'step', 'tolerance', 'drift'),
101
'classes': ('collapse',)
102
}),
103
('Security Info', {
104
'fields': ('key', 'last_t', 'throttling_failure_count'),
105
'classes': ('collapse',)
106
}),
107
('Timestamps', {
108
'fields': ('created_at', 'last_used_at'),
109
'classes': ('collapse',)
110
}),
111
)
112
113
def get_queryset(self, request):
114
"""Optimize queryset with select_related."""
115
return super().get_queryset(request).select_related('user')
116
```
117
118
### Admin Actions
119
120
```python
121
from django.contrib import admin
122
from django.contrib import messages
123
124
class DeviceAdminMixin:
125
"""Mixin providing common device admin actions."""
126
127
actions = ['reset_throttling', 'confirm_devices', 'reset_drift']
128
129
def reset_throttling(self, request, queryset):
130
"""Reset throttling for selected devices."""
131
count = 0
132
for device in queryset:
133
if hasattr(device, 'throttle_reset'):
134
device.throttle_reset()
135
count += 1
136
137
messages.success(request, f'Reset throttling for {count} devices.')
138
reset_throttling.short_description = "Reset throttling"
139
140
def confirm_devices(self, request, queryset):
141
"""Confirm selected devices."""
142
count = queryset.update(confirmed=True)
143
messages.success(request, f'Confirmed {count} devices.')
144
confirm_devices.short_description = "Confirm devices"
145
146
def reset_drift(self, request, queryset):
147
"""Reset drift for TOTP devices."""
148
count = 0
149
for device in queryset:
150
if hasattr(device, 'drift'):
151
device.drift = 0
152
device.save()
153
count += 1
154
155
messages.success(request, f'Reset drift for {count} devices.')
156
reset_drift.short_description = "Reset drift"
157
158
@admin.register(TOTPDevice)
159
class TOTPDeviceAdmin(DeviceAdminMixin, admin.ModelAdmin):
160
# ... admin configuration
161
```
162
163
### Inline Device Management
164
165
```python
166
from django.contrib import admin
167
from django.contrib.auth.models import User
168
from django.contrib.auth.admin import UserAdmin
169
from django_otp.plugins.otp_totp.models import TOTPDevice
170
171
class TOTPDeviceInline(admin.TabularInline):
172
"""Inline admin for TOTP devices."""
173
174
model = TOTPDevice
175
extra = 0
176
readonly_fields = ['key', 'last_t', 'drift']
177
fields = ['name', 'confirmed', 'digits', 'step', 'tolerance']
178
179
class CustomUserAdmin(UserAdmin):
180
"""User admin with OTP device inlines."""
181
182
inlines = UserAdmin.inlines + [TOTPDeviceInline]
183
184
# Unregister default user admin and register custom one
185
admin.site.unregister(User)
186
admin.site.register(User, CustomUserAdmin)
187
```
188
189
### Admin Templates
190
191
```html
192
<!-- admin/login.html override for OTP admin -->
193
{% extends "admin/login.html" %}
194
195
{% block content %}
196
<div id="content-main">
197
<form method="post" id="login-form">
198
{% csrf_token %}
199
200
<div class="form-row">
201
{{ form.username.label_tag }}
202
{{ form.username }}
203
</div>
204
205
<div class="form-row">
206
{{ form.password.label_tag }}
207
{{ form.password }}
208
</div>
209
210
{% if form.otp_device %}
211
<div class="form-row">
212
{{ form.otp_device.label_tag }}
213
{{ form.otp_device }}
214
</div>
215
216
<div class="form-row">
217
{{ form.otp_token.label_tag }}
218
{{ form.otp_token }}
219
</div>
220
221
{% if form.otp_challenge %}
222
<div class="form-row">
223
<input type="submit" name="otp_challenge" value="Send Challenge" class="default">
224
</div>
225
{% endif %}
226
{% endif %}
227
228
<div class="submit-row">
229
<input type="submit" value="Log in">
230
</div>
231
</form>
232
</div>
233
{% endblock %}
234
```
235
236
### Admin Dashboard Customization
237
238
```python
239
from django.contrib import admin
240
from django.template.response import TemplateResponse
241
from django.urls import path
242
from django_otp import devices_for_user
243
244
class CustomOTPAdminSite(OTPAdminSite):
245
"""Custom OTP admin site with dashboard."""
246
247
def get_urls(self):
248
"""Add custom URLs."""
249
urls = super().get_urls()
250
custom_urls = [
251
path('otp-dashboard/', self.admin_view(self.otp_dashboard), name='otp_dashboard'),
252
]
253
return custom_urls + urls
254
255
def otp_dashboard(self, request):
256
"""Custom OTP dashboard view."""
257
258
context = {
259
'title': 'OTP Dashboard',
260
'user_devices': list(devices_for_user(request.user)) if request.user.is_authenticated else [],
261
'total_users_with_otp': User.objects.filter(
262
id__in=Device.objects.values_list('user_id', flat=True).distinct()
263
).count(),
264
}
265
266
return TemplateResponse(request, 'admin/otp_dashboard.html', context)
267
268
def index(self, request, extra_context=None):
269
"""Customize admin index."""
270
extra_context = extra_context or {}
271
extra_context['otp_dashboard_url'] = 'admin:otp_dashboard'
272
273
return super().index(request, extra_context)
274
```
275
276
## Configuration
277
278
### Admin Settings
279
280
```python
281
# settings.py
282
283
# Hide sensitive data in admin interface
284
OTP_ADMIN_HIDE_SENSITIVE_DATA = True
285
```
286
287
### Admin Site Registration
288
289
```python
290
# Register with default admin
291
from django.contrib import admin
292
from django_otp.plugins.otp_totp.admin import TOTPDeviceAdmin
293
from django_otp.plugins.otp_totp.models import TOTPDevice
294
295
admin.site.register(TOTPDevice, TOTPDeviceAdmin)
296
297
# Or use OTP admin site
298
from django_otp.admin import OTPAdminSite
299
300
otp_admin = OTPAdminSite()
301
otp_admin.register(TOTPDevice, TOTPDeviceAdmin)
302
```