Skip to main content

auth/
module.rs

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