Skip to main content

auth_password/
module.rs

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
127/// Build a [`JwtActorResolver`] if auth-password is configured with `token_strategy = "jwt"`.
128///
129/// Returns `Ok(None)` when the strategy is `"session"` (the default) or when
130/// the auth-password module is not enabled. Returns `Ok(Some(..))` with the
131/// resolver wrapping the given `fallback`.
132pub 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}