rustauth_plugins/anonymous/
hooks.rs1use http::header;
2use rustauth_core::api::{ApiRequest, ApiResponse};
3use rustauth_core::context::request_state::current_new_session;
4use rustauth_core::context::AuthContext;
5use rustauth_core::error::RustAuthError;
6use rustauth_core::plugin::{AuthPlugin, PluginAfterHookAction};
7use serde::Serialize;
8
9use super::model::{self, AnonymousSession, LinkedSession};
10use super::options::AnonymousOptions;
11
12#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
13pub struct AnonymousLinkAccount {
14 pub anonymous_user: AnonymousSession,
15 pub new_user: LinkedSession,
16}
17
18pub fn attach_link_hooks(mut plugin: AuthPlugin, options: AnonymousOptions) -> AuthPlugin {
19 for path in [
20 "/sign-in*",
21 "/sign-up*",
22 "/callback*",
23 "/oauth2/callback*",
24 "/magic-link/verify*",
25 "/email-otp/verify-email*",
26 "/one-tap/callback*",
27 "/passkey/verify-authentication*",
28 "/phone-number/verify*",
29 ] {
30 let options = options.clone();
31 plugin = plugin.with_async_after_hook(path, move |context, request, response| {
32 let options = options.clone();
33 Box::pin(async move { link_after_hook(context, request, response, options).await })
34 });
35 }
36 plugin
37}
38
39async fn link_after_hook(
40 context: &AuthContext,
41 request: &ApiRequest,
42 response: ApiResponse,
43 options: AnonymousOptions,
44) -> Result<PluginAfterHookAction, RustAuthError> {
45 let adapter = context.require_adapter()?;
46 let cookie_header = request
47 .headers()
48 .get(header::COOKIE)
49 .and_then(|value| value.to_str().ok())
50 .unwrap_or_default()
51 .to_owned();
52 let Some(anonymous_user) = model::current_anonymous_session(
53 adapter.as_ref(),
54 context,
55 options.storage_field_name(),
56 cookie_header,
57 )
58 .await?
59 else {
60 return Ok(PluginAfterHookAction::Continue(response));
61 };
62 if !anonymous_user.user.is_anonymous {
63 return Ok(PluginAfterHookAction::Continue(response));
64 }
65
66 let new_user = if let Some(new_user) =
67 linked_session_from_request_state(adapter.as_ref(), &options).await?
68 {
69 new_user
70 } else {
71 let Some(new_session_token) = new_session_token(context, &response)? else {
72 return Ok(PluginAfterHookAction::Continue(response));
73 };
74 let Some(new_user) = model::linked_session_from_token(
75 context,
76 adapter.as_ref(),
77 options.storage_field_name(),
78 &new_session_token,
79 )
80 .await?
81 else {
82 return Ok(PluginAfterHookAction::Continue(response));
83 };
84 new_user
85 };
86
87 finish_link(context, response, options, anonymous_user, new_user).await
88}
89
90async fn linked_session_from_request_state(
91 adapter: &dyn rustauth_core::db::DbAdapter,
92 options: &AnonymousOptions,
93) -> Result<Option<LinkedSession>, RustAuthError> {
94 let Some(new_session) = current_new_session_or_none()? else {
95 return Ok(None);
96 };
97 let Some(user) =
98 model::find_anonymous_user(adapter, options.storage_field_name(), &new_session.user.id)
99 .await?
100 else {
101 return Ok(None);
102 };
103 Ok(Some(LinkedSession {
104 session: new_session.session,
105 user,
106 }))
107}
108
109fn current_new_session_or_none(
110) -> Result<Option<rustauth_core::context::request_state::NewSession>, RustAuthError> {
111 match current_new_session() {
112 Ok(session) => Ok(session),
113 Err(RustAuthError::RequestStateMissing) => Ok(None),
114 Err(error) => Err(error),
115 }
116}
117
118async fn finish_link(
119 context: &AuthContext,
120 response: ApiResponse,
121 options: AnonymousOptions,
122 anonymous_user: AnonymousSession,
123 new_user: LinkedSession,
124) -> Result<PluginAfterHookAction, RustAuthError> {
125 if let Some(callback) = &options.on_link_account {
126 callback(AnonymousLinkAccount {
127 anonymous_user: anonymous_user.clone(),
128 new_user: new_user.clone(),
129 })
130 .await?;
131 }
132
133 if options.disable_delete_anonymous_user
134 || new_user.user.id == anonymous_user.user.id
135 || new_user.user.is_anonymous
136 {
137 return Ok(PluginAfterHookAction::Continue(response));
138 }
139
140 model::delete_anonymous_user_records(context, &anonymous_user.user.id).await?;
141
142 Ok(PluginAfterHookAction::Continue(response))
143}
144
145fn new_session_token(
146 context: &AuthContext,
147 response: &ApiResponse,
148) -> Result<Option<String>, RustAuthError> {
149 for value in response.headers().get_all(header::SET_COOKIE) {
150 let Ok(cookie) = value.to_str() else {
151 continue;
152 };
153 let Some(raw_value) = cookie_value(cookie, &context.auth_cookies.session_token.name) else {
154 continue;
155 };
156 if let Some(token) = model::verified_cookie_value(context, raw_value)? {
157 return Ok(Some(token));
158 }
159 }
160 Ok(None)
161}
162
163fn cookie_value<'a>(set_cookie: &'a str, name: &str) -> Option<&'a str> {
164 let (cookie_name, rest) = set_cookie.split_once('=')?;
165 if cookie_name.trim() != name {
166 return None;
167 }
168 Some(rest.split_once(';').map_or(rest, |(value, _)| value))
169}