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