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, 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}