1use crate::admin::AuthPasswordAdminActions;
2use crate::config::{AuthPasswordConfig, TokenStrategy};
3use auth::module::{AUTH_USERS_DETAIL_ACTIONS_SLOT, AUTH_USERS_DETAIL_ACTIONS_SLOT_VERSION};
4use platform_core::AppContext;
5use platform_http::ApiOpenApiRouter;
6use platform_module::{
7 AdminAction, AdminActionDangerLevel, AdminActionInputField, AdminActionInputSchema,
8 AdminDeclarativeSurface, ConsoleActionInputBinding, ConsoleActionInputValue,
9 ConsoleContribution, ConsoleContributionAction, FieldType, LinkedBinding,
10 LinkedHttpContribution, Module, ModuleHttpMethod, ModuleHttpRoute, ModuleManifest,
11};
12use std::sync::Arc;
13
14pub const MODULE_NAME: &str = "auth-password";
15pub const AUTH_MODULE_DEPENDENCY: &str = "auth";
16pub const AUTH_PASSWORD_CREDENTIALS_WRITE: &str = "auth_password.credentials.write";
17pub const RESET_PASSWORD_ACTION: &str = "reset_password";
18
19pub fn http_routes() -> Vec<ModuleHttpRoute> {
20 vec![
21 ModuleHttpRoute {
22 method: ModuleHttpMethod::Post,
23 path: "/v1/auth/password/register".to_owned(),
24 capability: None,
25 operation: None,
26 display_name: Some("Register With Password".to_owned()),
27 story_title: Some("Password Registration".to_owned()),
28 },
29 ModuleHttpRoute {
30 method: ModuleHttpMethod::Post,
31 path: "/v1/auth/password/login".to_owned(),
32 capability: None,
33 operation: None,
34 display_name: Some("Login With Password".to_owned()),
35 story_title: Some("Password Login".to_owned()),
36 },
37 ]
38}
39
40pub fn manifest() -> ModuleManifest {
41 ModuleManifest::builder(MODULE_NAME)
42 .capabilities(vec![AUTH_PASSWORD_CREDENTIALS_WRITE.to_owned()])
43 .dependencies(vec![AUTH_MODULE_DEPENDENCY.to_owned()])
44 .http_routes(http_routes())
45 .declarative_admin(admin_surface())
46 .console_contributions(console_contributions())
47 .build()
48}
49
50pub fn admin_surface() -> AdminDeclarativeSurface {
51 AdminDeclarativeSurface {
52 pages: Vec::new(),
53 actions: vec![reset_password_action()],
54 fallback_schema: None,
55 }
56}
57
58fn reset_password_action() -> AdminAction {
59 AdminAction {
60 name: RESET_PASSWORD_ACTION.to_owned(),
61 label: "Reset password".to_owned(),
62 capability: AUTH_PASSWORD_CREDENTIALS_WRITE.to_owned(),
63 input_schema: Some(AdminActionInputSchema {
64 fields: vec![
65 AdminActionInputField {
66 name: "user_id".to_owned(),
67 label: "User".to_owned(),
68 field_type: FieldType::String,
69 required: true,
70 description: None,
71 },
72 AdminActionInputField {
73 name: "new_password".to_owned(),
74 label: "New password".to_owned(),
75 field_type: FieldType::String,
76 required: true,
77 description: None,
78 },
79 ],
80 }),
81 confirmation: None,
82 operation: None,
83 danger_level: AdminActionDangerLevel::Medium,
84 }
85}
86
87pub fn console_contributions() -> Vec<ConsoleContribution> {
88 vec![ConsoleContribution {
89 target: AUTH_USERS_DETAIL_ACTIONS_SLOT.to_owned(),
90 target_version: AUTH_USERS_DETAIL_ACTIONS_SLOT_VERSION,
91 label: "Reset password".to_owned(),
92 action: ConsoleContributionAction::AdminAction {
93 module: MODULE_NAME.to_owned(),
94 name: RESET_PASSWORD_ACTION.to_owned(),
95 input_bindings: vec![ConsoleActionInputBinding {
96 input: "user_id".to_owned(),
97 value: ConsoleActionInputValue::SlotContext {
98 path: "selected_user.id".to_owned(),
99 },
100 }],
101 },
102 icon: Some("key-round".to_owned()),
103 required_capabilities: vec![AUTH_PASSWORD_CREDENTIALS_WRITE.to_owned()],
104 }]
105}
106
107pub fn merge_http(base: ApiOpenApiRouter) -> ApiOpenApiRouter {
108 base.merge(crate::routes::router())
109}
110
111pub fn binding() -> LinkedBinding {
112 LinkedBinding::builder()
113 .http(LinkedHttpContribution {
114 public_prefixes: &["/v1/auth/password/"],
115 merge: merge_http,
116 })
117 .build()
118}
119
120pub fn module(_ctx: &AppContext) -> Module {
121 Module::linked(manifest(), binding())
122 .with_runtime_config_groups(crate::config::RUNTIME_CONFIG_GROUPS.as_slice())
123 .with_runtime_config(crate::config::RUNTIME_CONFIG.as_slice())
124 .with_admin_actions(Arc::new(AuthPasswordAdminActions::new(_ctx.clone())))
125}
126
127pub fn jwt_actor_resolver(
133 ctx: &AppContext,
134 fallback: std::sync::Arc<dyn platform_core::ActorResolver>,
135) -> platform_core::AppResult<Option<std::sync::Arc<dyn platform_core::ActorResolver>>> {
136 let config = AuthPasswordConfig::from_context(ctx)?;
137 if config.token_strategy == TokenStrategy::Jwt && config.jwt_secret.is_none() {
138 return Ok(None);
139 }
140 let jwt_config = match config.jwt_config()? {
141 Some(cfg) => cfg,
142 None => return Ok(None),
143 };
144 Ok(Some(std::sync::Arc::new(
145 crate::resolver::JwtActorResolver::new(jwt_config, fallback),
146 )))
147}
148
149#[cfg(test)]
150mod tests {
151 use super::*;
152 use platform_module::{
153 AdminSurface, ConsoleActionInputBinding, ConsoleActionInputValue, ConsoleContribution,
154 ConsoleContributionAction, ModuleManifestLintSeverity, lint_module_manifest,
155 };
156
157 #[test]
158 fn manifest_declares_password_routes() {
159 let manifest = manifest();
160
161 assert_eq!(manifest.module_id, format!("lenso/{MODULE_NAME}"));
162 assert_eq!(manifest.http_routes, http_routes());
163 assert_eq!(
164 manifest.admin,
165 Some(AdminSurface::DeclarativeCustom(admin_surface()))
166 );
167 assert_eq!(manifest.capabilities, vec![AUTH_PASSWORD_CREDENTIALS_WRITE]);
168 assert_eq!(manifest.console_contributions, console_contributions());
169
170 let lints = lint_module_manifest(&manifest);
171 assert!(
172 lints
173 .iter()
174 .all(|lint| lint.severity == ModuleManifestLintSeverity::Ok),
175 "auth-password manifest should not have warning/error lints: {lints:?}"
176 );
177 }
178
179 #[test]
180 fn manifest_contributes_reset_password_to_auth_user_actions() {
181 assert_eq!(
182 console_contributions(),
183 vec![ConsoleContribution {
184 target: AUTH_USERS_DETAIL_ACTIONS_SLOT.to_owned(),
185 target_version: AUTH_USERS_DETAIL_ACTIONS_SLOT_VERSION,
186 label: "Reset password".to_owned(),
187 action: ConsoleContributionAction::AdminAction {
188 module: MODULE_NAME.to_owned(),
189 name: RESET_PASSWORD_ACTION.to_owned(),
190 input_bindings: vec![ConsoleActionInputBinding {
191 input: "user_id".to_owned(),
192 value: ConsoleActionInputValue::SlotContext {
193 path: "selected_user.id".to_owned(),
194 },
195 }],
196 },
197 icon: Some("key-round".to_owned()),
198 required_capabilities: vec![AUTH_PASSWORD_CREDENTIALS_WRITE.to_owned()],
199 }]
200 );
201 }
202}