better_auth_api/plugins/
username.rs1use 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#[derive(Clone, Default)]
26pub struct UsernamePlugin {
27 config: UsernameConfig,
28}
29
30impl UsernamePlugin {
31 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}