Skip to main content

better_auth_api/plugins/
username.rs

1//! Username login with endpoint, database-hook and adapter policies.
2use better_auth_core::user_fields::{FieldTransforms, UserFieldTransform};
3
4use std::sync::Arc;
5
6use async_trait::async_trait;
7use better_auth_core::user_fields::{UserConfig, UserFieldConfig};
8use better_auth_core::{
9    AuthContext, AuthInitContext, AuthPlugin, AuthRequest, AuthResponse, AuthResult, AuthRoute,
10    AuthSchema, AuthUser, BeforeRequestAction, FieldValue, HttpMethod,
11};
12use serde_json::Value;
13
14mod config;
15mod endpoint_hooks;
16mod endpoints;
17mod hooks;
18pub(crate) mod request;
19pub use config::{
20    UsernameConfig, UsernameNormalization, UsernameNormalizer, UsernameValidationOrder,
21    UsernameValidator,
22};
23
24/// Install username fields, validation, availability and password login endpoints.
25#[derive(Clone, Default)]
26pub struct UsernamePlugin {
27    config: UsernameConfig,
28}
29
30impl UsernamePlugin {
31    /// Construct the plugin with explicit options.
32    pub fn new(config: UsernameConfig) -> Self {
33        Self { config }
34    }
35
36    fn fields(&self) -> UserConfig {
37        let mut fields = UserConfig::default();
38        let config = self.config.clone();
39        let _ = fields.fields_mut().insert(
40            "username".into(),
41            UserFieldConfig {
42                required: Some(false),
43                field_name: config.username_field_name.clone(),
44                transform: Some(FieldTransforms {
45                    input: Some(UserFieldTransform::new(move |value| match value {
46                        FieldValue::String(value) => config.normalize(&value).map(Into::into),
47                        value => Ok(value),
48                    })),
49                    ..Default::default()
50                }),
51                ..Default::default()
52            },
53        );
54        if self.config.display_username {
55            let config = self.config.clone();
56            let _ = fields.fields_mut().insert(
57                "displayUsername".into(),
58                UserFieldConfig {
59                    required: Some(false),
60                    field_name: config.display_username_field_name.clone(),
61                    transform: Some(FieldTransforms {
62                        input: Some(UserFieldTransform::new(move |value| match value {
63                            FieldValue::String(value) => {
64                                config.normalize_display(&value).map(Into::into)
65                            }
66                            value => Ok(value),
67                        })),
68                        ..Default::default()
69                    }),
70                    ..Default::default()
71                },
72            );
73        }
74        fields
75    }
76}
77
78#[async_trait]
79impl<S: AuthSchema> AuthPlugin<S> for UsernamePlugin {
80    fn name(&self) -> &'static str {
81        "username"
82    }
83    fn routes(&self) -> Vec<AuthRoute> {
84        vec![
85            AuthRoute::post("/sign-in/username", "signInUsername")
86                .body_validator(request::sign_in_body),
87            AuthRoute::post("/is-username-available", "isUsernameAvailable")
88                .body_validator(request::availability_body),
89        ]
90    }
91    fn openapi(&self) -> AuthResult<better_auth_core::openapi::OpenApiPluginMetadata> {
92        let mut metadata = better_auth_core::openapi::OpenApiPluginMetadata::from_routes(
93            <Self as better_auth_core::AuthPlugin<S>>::name(self),
94            <Self as better_auth_core::AuthPlugin<S>>::routes(self),
95        )?
96        .model("user", &self.fields())?;
97        if !self.config.display_username {
98            metadata = metadata.remove_field("user", "displayUsername");
99        }
100        Ok(metadata)
101    }
102
103    async fn on_init(&self, context: &mut AuthInitContext<S>) -> AuthResult<()> {
104        let required = if self.config.display_username {
105            &["username", "display_username"][..]
106        } else {
107            &["username"][..]
108        };
109        S::User::require_plugin_fields("username", required)?;
110        context.set_metadata("username.enabled", Value::Bool(true));
111        context.set_metadata(
112            "username.display.enabled",
113            Value::Bool(self.config.display_username),
114        );
115        context.extensions.insert(self.config.clone());
116        context.register_user_fields(self.fields());
117        context.register_database_hook(Arc::new(hooks::UsernameHooks {
118            config: self.config.clone(),
119            runtime: context.runtime(),
120        }));
121        Ok(())
122    }
123    async fn before_request(
124        &self,
125        request: &AuthRequest,
126        context: &AuthContext<S>,
127    ) -> AuthResult<Option<BeforeRequestAction>> {
128        self.before_endpoint(request, context).await
129    }
130    async fn on_request(
131        &self,
132        request: &AuthRequest,
133        context: &AuthContext<S>,
134    ) -> AuthResult<Option<AuthResponse>> {
135        match (request.method(), request.path()) {
136            (HttpMethod::Post, "/sign-in/username") => {
137                self.sign_in(request, context).await.map(Some)
138            }
139            (HttpMethod::Post, "/is-username-available") => {
140                self.available(request, context).await.map(Some)
141            }
142            _ => Ok(None),
143        }
144    }
145}