Coverage for src/lexigram/auth/admin/contributor.py: 82%

50 statements  

« prev     ^ index     » next       coverage.py v7.15.4, created at 2026-08-26 00:58 +0800

1"""Admin contributor for lexigram-auth — surfaces session, token, and login 

2widgets into the Lexigram admin dashboard. 

3""" 

4 

5from __future__ import annotations 

6 

7from collections.abc import Sequence 

8from typing import TYPE_CHECKING, Any, cast 

9 

10from lexigram.contracts import Result 

11from lexigram.contracts.admin.contributor import BaseAdminContributor 

12from lexigram.contracts.admin.errors import AdminError, WidgetNotFoundError 

13from lexigram.contracts.admin.types import ( 

14 AdminActionDefinition, 

15 AdminHealthDefinition, 

16 DashboardWidgetDefinition, 

17 ManagementPageDefinition, 

18 NavigationContribution, 

19 PageCategory, 

20 WidgetCategory, 

21 WidgetKind, 

22 WidgetParams, 

23 WidgetSize, 

24 WidgetViewModel, 

25) 

26from lexigram.contracts.admin.widget_protocols import WidgetHandlerProtocol 

27from lexigram.logging import get_logger 

28from lexigram.result import Err, Ok 

29 

30if TYPE_CHECKING: 

31 from lexigram.contracts.core.di import ContainerResolverProtocol 

32 

33logger = get_logger(__name__) 

34 

35_WIDGETS: tuple[DashboardWidgetDefinition, ...] = ( 

36 DashboardWidgetDefinition( 

37 name="active_sessions", 

38 title="Active Sessions", 

39 contributor="auth", 

40 render_endpoint="/admin/auth/widgets/active_sessions", 

41 size=WidgetSize.LARGE, 

42 category=WidgetCategory.ACTIVITY, 

43 view_kind=WidgetKind.STAT, 

44 description="Number of currently active authenticated sessions.", 

45 ), 

46 DashboardWidgetDefinition( 

47 name="token_refresh_rate", 

48 title="Token Refresh Rate", 

49 contributor="auth", 

50 render_endpoint="/admin/auth/widgets/token_refresh_rate", 

51 size=WidgetSize.SMALL, 

52 category=WidgetCategory.METRICS, 

53 view_kind=WidgetKind.STAT, 

54 description="JWT / OAuth2 token refresh requests per minute.", 

55 ), 

56 DashboardWidgetDefinition( 

57 name="failed_logins", 

58 title="Failed Logins", 

59 contributor="auth", 

60 render_endpoint="/admin/auth/widgets/failed_logins", 

61 size=WidgetSize.SMALL, 

62 category=WidgetCategory.HEALTH, 

63 view_kind=WidgetKind.STAT, 

64 description="Count of failed login attempts over the last hour.", 

65 ), 

66) 

67 

68_NAV_ITEMS: tuple[NavigationContribution, ...] = ( 

69 NavigationContribution( 

70 label="Auth", 

71 url="/admin/auth", 

72 icon="lock-closed", 

73 group="security", 

74 order=25, 

75 children=( 

76 NavigationContribution( 

77 label="Users", 

78 url="/admin/auth/users", 

79 icon="users", 

80 group="security", 

81 order=10, 

82 ), 

83 NavigationContribution( 

84 label="Sessions", 

85 url="/admin/auth/sessions", 

86 icon="activity", 

87 group="security", 

88 order=20, 

89 ), 

90 NavigationContribution( 

91 label="Tokens", 

92 url="/admin/auth/tokens", 

93 icon="key", 

94 group="security", 

95 order=30, 

96 ), 

97 ), 

98 ), 

99) 

100 

101_HEALTH_DEFS: tuple[AdminHealthDefinition, ...] = ( 

102 AdminHealthDefinition( 

103 name="auth.token_store", 

104 contributor="auth", 

105 component="Token Store", 

106 check_endpoint="/admin/auth/health/token_store", 

107 description="Verifies the token storage backend is available.", 

108 ), 

109) 

110 

111_ACTIONS: tuple[AdminActionDefinition, ...] = ( 

112 AdminActionDefinition( 

113 name="revoke_all_sessions", 

114 title="Revoke All Sessions", 

115 contributor="auth", 

116 handler="lexigram.auth.admin.actions:revoke_all_sessions", 

117 icon="shield-off", 

118 confirmation_message="This will immediately log out all active users. Are you sure?", 

119 destructive=True, 

120 category="security", 

121 ), 

122) 

123 

124 

125class AuthAdminContributor(BaseAdminContributor): 

126 """Admin contributor for the lexigram-auth package. 

127 

128 Provides 3 widgets: active sessions, token refresh rate, and failed logins. 

129 Each widget is handled by a dedicated handler class that returns 

130 structured WidgetContent directly. Dependencies are resolved from the 

131 container in ``on_admin_boot``. 

132 """ 

133 

134 name = "auth" 

135 display_name = "Auth" 

136 group = "security" 

137 icon = "lock-closed" 

138 priority = 25 

139 

140 def __init__(self) -> None: 

141 """Initialize the contributor with no arguments. 

142 

143 All DI-dependent attributes are resolved in ``on_admin_boot``. 

144 """ 

145 self._handlers: dict[str, WidgetHandlerProtocol] = {} 

146 

147 async def on_admin_boot(self, container: ContainerResolverProtocol) -> None: 

148 """Resolve auth DI dependencies from the container. 

149 

150 Args: 

151 container: The DI container resolver. 

152 """ 

153 from lexigram.auth.admin.handlers.active_sessions import ( 

154 ActiveSessionsWidgetHandler, 

155 ) 

156 from lexigram.auth.admin.handlers.failed_logins import ( 

157 FailedLoginsWidgetHandler, 

158 ) 

159 from lexigram.auth.admin.handlers.token_refresh_rate import ( 

160 TokenRefreshRateWidgetHandler, 

161 ) 

162 

163 try: 

164 self._handlers = { 

165 "active_sessions": await container.resolve(ActiveSessionsWidgetHandler), 

166 "token_refresh_rate": await container.resolve( 

167 TokenRefreshRateWidgetHandler 

168 ), 

169 "failed_logins": await container.resolve(FailedLoginsWidgetHandler), 

170 } 

171 except Exception as exc: # noqa: BLE001 

172 logger.warning("auth_contributor.handlers_unavailable", error=str(exc)) 

173 

174 def get_dashboard_widgets(self) -> Sequence[DashboardWidgetDefinition]: 

175 """Return the dashboard widget definitions for this contributor.""" 

176 return list(_WIDGETS) 

177 

178 def get_navigation_items(self) -> Sequence[NavigationContribution]: 

179 """Return the navigation items for this contributor.""" 

180 return list(_NAV_ITEMS) 

181 

182 def get_health_definitions(self) -> Sequence[AdminHealthDefinition]: 

183 """Return the health check definitions for this contributor.""" 

184 return list(_HEALTH_DEFS) 

185 

186 def get_actions(self) -> Sequence[AdminActionDefinition]: 

187 """Return the action definitions for this contributor.""" 

188 return list(_ACTIONS) 

189 

190 def get_management_pages(self) -> Sequence[ManagementPageDefinition]: 

191 """Return the management page definitions for this contributor.""" 

192 return [ 

193 ManagementPageDefinition( 

194 name="auth_overview", 

195 title="Auth Overview", 

196 contributor=self.name, 

197 route_path="/auth", 

198 handler="lexigram.auth.admin.pages.overview:AuthOverviewPage", 

199 icon="lock-closed", 

200 category=PageCategory.SECURITY, 

201 ), 

202 ManagementPageDefinition( 

203 name="auth_users", 

204 title="Auth Users", 

205 contributor=self.name, 

206 route_path="/auth/users", 

207 handler="lexigram.auth.admin.pages.users:AuthUsersPage", 

208 icon="users", 

209 category=PageCategory.SECURITY, 

210 ), 

211 ManagementPageDefinition( 

212 name="auth_sessions", 

213 title="Auth Sessions", 

214 contributor=self.name, 

215 route_path="/auth/sessions", 

216 handler="lexigram.auth.admin.pages.sessions:AuthSessionsPage", 

217 icon="activity", 

218 category=PageCategory.SECURITY, 

219 ), 

220 ManagementPageDefinition( 

221 name="auth_tokens", 

222 title="Auth Tokens", 

223 contributor=self.name, 

224 route_path="/auth/tokens", 

225 handler="lexigram.auth.admin.pages.tokens:AuthTokensPage", 

226 icon="key", 

227 category=PageCategory.SECURITY, 

228 ), 

229 ] 

230 

231 async def render_widget( 

232 self, 

233 widget_name: str, 

234 params: WidgetParams, 

235 resolver: ContainerResolverProtocol | None = None, 

236 ) -> Result[WidgetViewModel, AdminError]: 

237 """Render a widget by name using registry dispatch. 

238 

239 Registry dispatch — no if/elif. Infrastructure exceptions propagate. 

240 

241 Args: 

242 widget_name: Name of the widget to render. 

243 params: Widget parameters. 

244 

245 Returns: 

246 Result containing a WidgetViewModel with structured content, 

247 or WidgetNotFoundError if the widget is not registered. 

248 """ 

249 handler: Any = self._handlers.get(widget_name) 

250 if handler is None: 

251 return Err(cast("AdminError", WidgetNotFoundError(self.name, widget_name))) 

252 

253 result = await handler.get_data(params) 

254 if result.is_err(): 

255 return Err(result.unwrap_err()) 

256 return Ok(WidgetViewModel(content=result.unwrap())) 

257 

258 

259__all__ = ["AuthAdminContributor"]