or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

admin-interface.mdcore-authentication.mddevice-models.mddjango-integration.mdemail-devices.mdhotp-devices.mdindex.mdoath-algorithms.mdstatic-tokens.mdtotp-devices.md

admin-interface.mddocs/

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

```