1use crate::admin::AuthAdminData;
2use crate::repositories::PostgresAuthUserRepository;
3use platform_core::AppContext;
4use platform_http::ApiOpenApiRouter;
5use platform_module::{
6 AdminAction, AdminActionDangerLevel, AdminActionInputField, AdminActionInputSchema,
7 AdminDeclarativeComponent, AdminDeclarativePage, AdminDeclarativeSection,
8 AdminDeclarativeSurface, AdminSchema, ConsoleArea, ConsoleNavigation, ConsolePackage,
9 ConsoleSurface, ConsoleWorkspaceRef, EntitySchema, FieldSchema, FieldType, LinkedBinding,
10 LinkedHttpContribution, Module, ModuleHttpMethod, ModuleHttpRoute, ModuleManifest,
11};
12use std::sync::Arc;
13
14pub const MODULE_NAME: &str = "auth";
15pub const AUTH_USERS_READ: &str = "auth.users.read";
16
17pub fn http_routes() -> Vec<ModuleHttpRoute> {
18 vec![
19 ModuleHttpRoute {
20 method: ModuleHttpMethod::Post,
21 path: "/v1/auth/dev/sessions".to_owned(),
22 capability: None,
23 display_name: Some("Create Development Session".to_owned()),
24 story_title: Some("Development Auth Session".to_owned()),
25 },
26 ModuleHttpRoute {
27 method: ModuleHttpMethod::Post,
28 path: "/v1/auth/sessions/revoke".to_owned(),
29 capability: None,
30 display_name: Some("Revoke Session".to_owned()),
31 story_title: Some("Auth Session Revoked".to_owned()),
32 },
33 ]
34}
35
36pub fn user_schema() -> AdminSchema {
37 AdminSchema {
38 entities: vec![
39 EntitySchema {
40 name: "users".to_owned(),
41 label: "Users".to_owned(),
42 read_capability: AUTH_USERS_READ.to_owned(),
43 fields: vec![
44 FieldSchema {
45 name: "id".to_owned(),
46 label: "ID".to_owned(),
47 field_type: FieldType::String,
48 nullable: false,
49 },
50 FieldSchema {
51 name: "device_id".to_owned(),
52 label: "Device".to_owned(),
53 field_type: FieldType::String,
54 nullable: true,
55 },
56 FieldSchema {
57 name: "created_at".to_owned(),
58 label: "Created".to_owned(),
59 field_type: FieldType::Timestamp,
60 nullable: false,
61 },
62 FieldSchema {
63 name: "disabled_at".to_owned(),
64 label: "Disabled".to_owned(),
65 field_type: FieldType::Timestamp,
66 nullable: true,
67 },
68 FieldSchema {
69 name: "disabled_reason".to_owned(),
70 label: "Reason".to_owned(),
71 field_type: FieldType::String,
72 nullable: true,
73 },
74 FieldSchema {
75 name: "disabled_until".to_owned(),
76 label: "Until".to_owned(),
77 field_type: FieldType::Timestamp,
78 nullable: true,
79 },
80 ],
81 },
82 EntitySchema {
83 name: "sessions".to_owned(),
84 label: "Sessions".to_owned(),
85 read_capability: AUTH_USERS_READ.to_owned(),
86 fields: vec![
87 FieldSchema {
88 name: "id".to_owned(),
89 label: "ID".to_owned(),
90 field_type: FieldType::String,
91 nullable: false,
92 },
93 FieldSchema {
94 name: "user_id".to_owned(),
95 label: "User".to_owned(),
96 field_type: FieldType::String,
97 nullable: false,
98 },
99 FieldSchema {
100 name: "device_id".to_owned(),
101 label: "Device".to_owned(),
102 field_type: FieldType::String,
103 nullable: true,
104 },
105 FieldSchema {
106 name: "client_ip".to_owned(),
107 label: "IP".to_owned(),
108 field_type: FieldType::String,
109 nullable: true,
110 },
111 FieldSchema {
112 name: "user_agent".to_owned(),
113 label: "User agent".to_owned(),
114 field_type: FieldType::String,
115 nullable: true,
116 },
117 FieldSchema {
118 name: "created_at".to_owned(),
119 label: "Created".to_owned(),
120 field_type: FieldType::Timestamp,
121 nullable: false,
122 },
123 FieldSchema {
124 name: "expires_at".to_owned(),
125 label: "Expires".to_owned(),
126 field_type: FieldType::Timestamp,
127 nullable: false,
128 },
129 FieldSchema {
130 name: "revoked_at".to_owned(),
131 label: "Revoked".to_owned(),
132 field_type: FieldType::Timestamp,
133 nullable: true,
134 },
135 ],
136 },
137 ],
138 }
139}
140
141pub fn admin_surface() -> AdminDeclarativeSurface {
142 AdminDeclarativeSurface {
143 pages: vec![AdminDeclarativePage {
144 name: "sessions".to_owned(),
145 label: "Sessions".to_owned(),
146 sections: vec![AdminDeclarativeSection {
147 name: "sessions".to_owned(),
148 label: "Sessions".to_owned(),
149 component: AdminDeclarativeComponent::EntityTable {
150 entity: "sessions".to_owned(),
151 },
152 }],
153 }],
154 actions: vec![
155 action_with_string_input(
156 "revoke_session",
157 "Revoke session",
158 "session_id",
159 "Session",
160 AdminActionDangerLevel::Medium,
161 ),
162 disable_user_action(),
163 action_with_string_input(
164 "enable_user",
165 "Enable user",
166 "user_id",
167 "User",
168 AdminActionDangerLevel::Low,
169 ),
170 ],
171 fallback_schema: Some(user_schema()),
172 }
173}
174
175fn action_with_string_input(
176 name: &str,
177 label: &str,
178 input_name: &str,
179 input_label: &str,
180 danger_level: AdminActionDangerLevel,
181) -> AdminAction {
182 AdminAction {
183 name: name.to_owned(),
184 label: label.to_owned(),
185 capability: AUTH_USERS_READ.to_owned(),
186 input_schema: Some(AdminActionInputSchema {
187 fields: vec![AdminActionInputField {
188 name: input_name.to_owned(),
189 label: input_label.to_owned(),
190 field_type: FieldType::String,
191 required: true,
192 description: None,
193 }],
194 }),
195 confirmation: None,
196 danger_level,
197 }
198}
199
200fn disable_user_action() -> AdminAction {
201 AdminAction {
202 name: "disable_user".to_owned(),
203 label: "Disable user".to_owned(),
204 capability: AUTH_USERS_READ.to_owned(),
205 input_schema: Some(AdminActionInputSchema {
206 fields: vec![
207 AdminActionInputField {
208 name: "user_id".to_owned(),
209 label: "User".to_owned(),
210 field_type: FieldType::String,
211 required: true,
212 description: None,
213 },
214 AdminActionInputField {
215 name: "reason".to_owned(),
216 label: "Reason".to_owned(),
217 field_type: FieldType::String,
218 required: false,
219 description: None,
220 },
221 AdminActionInputField {
222 name: "disabled_until".to_owned(),
223 label: "Until".to_owned(),
224 field_type: FieldType::Timestamp,
225 required: false,
226 description: Some("RFC3339 timestamp; omit for permanent".to_owned()),
227 },
228 ],
229 }),
230 confirmation: None,
231 danger_level: AdminActionDangerLevel::Medium,
232 }
233}
234
235fn auth_workspace() -> ConsoleWorkspaceRef {
236 ConsoleWorkspaceRef {
237 id: "auth".to_owned(),
238 label: "Auth".to_owned(),
239 icon: Some("shield".to_owned()),
240 }
241}
242
243pub fn console_surfaces() -> Vec<ConsoleSurface> {
244 vec![
245 ConsoleSurface {
246 name: "sessions".to_owned(),
247 label: "Sessions".to_owned(),
248 area: ConsoleArea::Data,
249 route: "/data/auth/sessions".to_owned(),
250 package: ConsolePackage {
251 name: "@lenso/auth-console".to_owned(),
252 export: "authConsoleModule".to_owned(),
253 },
254 icon: Some("shield".to_owned()),
255 required_capabilities: vec![AUTH_USERS_READ.to_owned()],
256 navigation: Some(ConsoleNavigation {
257 workspace: auth_workspace(),
258 group: None,
259 order: Some(50),
260 }),
261 },
262 ConsoleSurface {
263 name: "users".to_owned(),
264 label: "Users".to_owned(),
265 area: ConsoleArea::Data,
266 route: "/data/auth/users".to_owned(),
267 package: ConsolePackage {
268 name: "@lenso/auth-console".to_owned(),
269 export: "authConsoleModule".to_owned(),
270 },
271 icon: Some("shield".to_owned()),
272 required_capabilities: vec![AUTH_USERS_READ.to_owned()],
273 navigation: Some(ConsoleNavigation {
274 workspace: auth_workspace(),
275 group: None,
276 order: Some(60),
277 }),
278 },
279 ]
280}
281
282pub fn manifest() -> ModuleManifest {
283 ModuleManifest::builder(MODULE_NAME)
284 .capabilities(vec![AUTH_USERS_READ.to_owned()])
285 .http_routes(http_routes())
286 .declarative_admin(admin_surface())
287 .console(console_surfaces())
288 .build()
289}
290
291pub fn merge_http(base: ApiOpenApiRouter) -> ApiOpenApiRouter {
292 base.merge(crate::routes::router())
293}
294
295pub fn binding() -> LinkedBinding {
296 LinkedBinding::builder()
297 .http(LinkedHttpContribution {
298 public_prefixes: &["/v1/auth/dev/", "/v1/auth/sessions/"],
299 merge: merge_http,
300 })
301 .build()
302}
303
304pub fn module(ctx: &AppContext) -> Module {
305 let repository = Arc::new(PostgresAuthUserRepository::new(ctx.db.clone()));
306 let admin = Arc::new(AuthAdminData::new(repository));
307 Module::linked(manifest(), binding())
308 .with_runtime_config(crate::config::RUNTIME_CONFIG.as_slice())
309 .with_admin_data(admin.clone())
310 .with_admin_actions(admin)
311}
312
313#[cfg(test)]
314mod tests {
315 use super::*;
316 use platform_module::{ModuleManifestLintSeverity, ModuleSource, lint_module_manifest};
317
318 #[test]
319 fn manifest_declares_auth_user_anchor() {
320 let manifest = manifest();
321
322 assert_eq!(manifest.name, MODULE_NAME);
323 assert_eq!(manifest.capabilities, vec![AUTH_USERS_READ]);
324 assert_eq!(manifest.http_routes, http_routes());
325 assert_eq!(
326 manifest.admin,
327 Some(platform_module::AdminSurface::DeclarativeCustom(
328 admin_surface()
329 ))
330 );
331 assert_eq!(manifest.console, console_surfaces());
332
333 let lints = lint_module_manifest(ModuleSource::Linked, &manifest);
334 assert!(
335 lints
336 .iter()
337 .all(|lint| lint.severity == ModuleManifestLintSeverity::Ok),
338 "auth manifest should not have warning/error lints: {lints:?}"
339 );
340 }
341}