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_admin_data(admin.clone())
285 .with_admin_actions(admin)
286}
287
288#[cfg(test)]
289mod tests {
290 use super::*;
291 use platform_module::{ModuleManifestLintSeverity, ModuleSource, lint_module_manifest};
292
293 #[test]
294 fn manifest_declares_auth_user_anchor() {
295 let manifest = manifest();
296
297 assert_eq!(manifest.name, MODULE_NAME);
298 assert_eq!(manifest.capabilities, vec![AUTH_USERS_READ]);
299 assert_eq!(manifest.http_routes, http_routes());
300 assert_eq!(
301 manifest.admin,
302 Some(platform_module::AdminSurface::DeclarativeCustom(
303 admin_surface()
304 ))
305 );
306 assert_eq!(manifest.console, console_surfaces());
307
308 let lints = lint_module_manifest(ModuleSource::Linked, &manifest);
309 assert!(
310 lints
311 .iter()
312 .all(|lint| lint.severity == ModuleManifestLintSeverity::Ok),
313 "auth manifest should not have warning/error lints: {lints:?}"
314 );
315 }
316}