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: "created_at".to_owned(),
101                        label: "Created".to_owned(),
102                        field_type: FieldType::Timestamp,
103                        nullable: false,
104                    },
105                    FieldSchema {
106                        name: "expires_at".to_owned(),
107                        label: "Expires".to_owned(),
108                        field_type: FieldType::Timestamp,
109                        nullable: false,
110                    },
111                    FieldSchema {
112                        name: "revoked_at".to_owned(),
113                        label: "Revoked".to_owned(),
114                        field_type: FieldType::Timestamp,
115                        nullable: true,
116                    },
117                ],
118            },
119        ],
120    }
121}
122
123pub fn admin_surface() -> AdminDeclarativeSurface {
124    AdminDeclarativeSurface {
125        pages: vec![AdminDeclarativePage {
126            name: "sessions".to_owned(),
127            label: "Sessions".to_owned(),
128            sections: vec![AdminDeclarativeSection {
129                name: "sessions".to_owned(),
130                label: "Sessions".to_owned(),
131                component: AdminDeclarativeComponent::EntityTable {
132                    entity: "sessions".to_owned(),
133                },
134            }],
135        }],
136        actions: vec![
137            action_with_string_input(
138                "revoke_session",
139                "Revoke session",
140                "session_id",
141                "Session",
142                AdminActionDangerLevel::Medium,
143            ),
144            disable_user_action(),
145            action_with_string_input(
146                "enable_user",
147                "Enable user",
148                "user_id",
149                "User",
150                AdminActionDangerLevel::Low,
151            ),
152        ],
153        fallback_schema: Some(user_schema()),
154    }
155}
156
157fn action_with_string_input(
158    name: &str,
159    label: &str,
160    input_name: &str,
161    input_label: &str,
162    danger_level: AdminActionDangerLevel,
163) -> AdminAction {
164    AdminAction {
165        name: name.to_owned(),
166        label: label.to_owned(),
167        capability: AUTH_USERS_READ.to_owned(),
168        input_schema: Some(AdminActionInputSchema {
169            fields: vec![AdminActionInputField {
170                name: input_name.to_owned(),
171                label: input_label.to_owned(),
172                field_type: FieldType::String,
173                required: true,
174                description: None,
175            }],
176        }),
177        confirmation: None,
178        danger_level,
179    }
180}
181
182fn disable_user_action() -> AdminAction {
183    AdminAction {
184        name: "disable_user".to_owned(),
185        label: "Disable user".to_owned(),
186        capability: AUTH_USERS_READ.to_owned(),
187        input_schema: Some(AdminActionInputSchema {
188            fields: vec![
189                AdminActionInputField {
190                    name: "user_id".to_owned(),
191                    label: "User".to_owned(),
192                    field_type: FieldType::String,
193                    required: true,
194                    description: None,
195                },
196                AdminActionInputField {
197                    name: "reason".to_owned(),
198                    label: "Reason".to_owned(),
199                    field_type: FieldType::String,
200                    required: false,
201                    description: None,
202                },
203                AdminActionInputField {
204                    name: "disabled_until".to_owned(),
205                    label: "Until".to_owned(),
206                    field_type: FieldType::Timestamp,
207                    required: false,
208                    description: Some("RFC3339 timestamp; omit for permanent".to_owned()),
209                },
210            ],
211        }),
212        confirmation: None,
213        danger_level: AdminActionDangerLevel::Medium,
214    }
215}
216
217fn auth_workspace() -> ConsoleWorkspaceRef {
218    ConsoleWorkspaceRef {
219        id: "auth".to_owned(),
220        label: "Auth".to_owned(),
221        icon: Some("shield".to_owned()),
222    }
223}
224
225pub fn console_surfaces() -> Vec<ConsoleSurface> {
226    vec![
227        ConsoleSurface {
228            name: "sessions".to_owned(),
229            label: "Sessions".to_owned(),
230            area: ConsoleArea::Data,
231            route: "/data/auth/sessions".to_owned(),
232            package: ConsolePackage {
233                name: "@lenso/auth-console".to_owned(),
234                export: "authConsoleModule".to_owned(),
235            },
236            icon: Some("shield".to_owned()),
237            required_capabilities: vec![AUTH_USERS_READ.to_owned()],
238            navigation: Some(ConsoleNavigation {
239                workspace: auth_workspace(),
240                group: None,
241                order: Some(50),
242            }),
243        },
244        ConsoleSurface {
245            name: "users".to_owned(),
246            label: "Users".to_owned(),
247            area: ConsoleArea::Data,
248            route: "/data/auth/users".to_owned(),
249            package: ConsolePackage {
250                name: "@lenso/auth-console".to_owned(),
251                export: "authConsoleModule".to_owned(),
252            },
253            icon: Some("shield".to_owned()),
254            required_capabilities: vec![AUTH_USERS_READ.to_owned()],
255            navigation: Some(ConsoleNavigation {
256                workspace: auth_workspace(),
257                group: None,
258                order: Some(60),
259            }),
260        },
261    ]
262}
263
264pub fn manifest() -> ModuleManifest {
265    ModuleManifest::builder(MODULE_NAME)
266        .capabilities(vec![AUTH_USERS_READ.to_owned()])
267        .http_routes(http_routes())
268        .declarative_admin(admin_surface())
269        .console(console_surfaces())
270        .build()
271}
272
273pub fn merge_http(base: ApiOpenApiRouter) -> ApiOpenApiRouter {
274    base.merge(crate::routes::router())
275}
276
277pub fn binding() -> LinkedBinding {
278    LinkedBinding::builder()
279        .http(LinkedHttpContribution {
280            public_prefixes: &["/v1/auth/dev/", "/v1/auth/sessions/"],
281            merge: merge_http,
282        })
283        .build()
284}
285
286pub fn module(ctx: &AppContext) -> Module {
287    let repository = Arc::new(PostgresAuthUserRepository::new(ctx.db.clone()));
288    let admin = Arc::new(AuthAdminData::new(repository));
289    Module::linked(manifest(), binding())
290        .with_runtime_config(crate::config::RUNTIME_CONFIG.as_slice())
291        .with_admin_data(admin.clone())
292        .with_admin_actions(admin)
293}
294
295#[cfg(test)]
296mod tests {
297    use super::*;
298    use platform_module::{ModuleManifestLintSeverity, ModuleSource, lint_module_manifest};
299
300    #[test]
301    fn manifest_declares_auth_user_anchor() {
302        let manifest = manifest();
303
304        assert_eq!(manifest.name, MODULE_NAME);
305        assert_eq!(manifest.capabilities, vec![AUTH_USERS_READ]);
306        assert_eq!(manifest.http_routes, http_routes());
307        assert_eq!(
308            manifest.admin,
309            Some(platform_module::AdminSurface::DeclarativeCustom(
310                admin_surface()
311            ))
312        );
313        assert_eq!(manifest.console, console_surfaces());
314
315        let lints = lint_module_manifest(ModuleSource::Linked, &manifest);
316        assert!(
317            lints
318                .iter()
319                .all(|lint| lint.severity == ModuleManifestLintSeverity::Ok),
320            "auth manifest should not have warning/error lints: {lints:?}"
321        );
322    }
323}