Skip to main content

rustauth_stripe/
lib.rs

1//! Stripe integration for RustAuth.
2
3mod customers;
4pub mod errors;
5mod hooks;
6mod logging;
7mod metadata;
8pub mod models;
9pub mod options;
10mod organization;
11mod routes;
12mod schema;
13pub mod stripe_api;
14mod subscription_lookup;
15mod utils;
16
17use rustauth_core::plugin::{
18    AuthPlugin, PluginDatabaseAfterInput, PluginDatabaseHook, PluginDatabaseOperation,
19    PluginInitOutput,
20};
21
22pub use errors::{error_codes, StripeConfigError, StripeErrorCode};
23pub use options::{
24    AuthorizeReferenceAction, AuthorizeReferenceInput, CheckoutSessionParamsInput,
25    CustomerCreateContext, CustomerCreateInput, CustomerCreateParamsInput, FreeTrialOptions,
26    OrganizationCustomerCreateInput, OrganizationCustomerCreateParamsInput,
27    OrganizationStripeOptions, StripeOptions, StripePlan, SubscriptionLifecycleInput,
28    SubscriptionOptions, SubscriptionUpdateInput,
29};
30pub use stripe_api::{StripeClient, StripeTransport};
31
32/// Current crate version.
33pub const VERSION: &str = env!("CARGO_PKG_VERSION");
34
35pub const UPSTREAM_PLUGIN_ID: &str = "stripe";
36
37/// Build the Stripe [`AuthPlugin`] after validating configuration.
38pub fn stripe(options: StripeOptions) -> Result<AuthPlugin, StripeConfigError> {
39    validate_stripe_options(&options)?;
40    Ok(build_stripe_plugin(options))
41}
42
43fn validate_stripe_options(options: &StripeOptions) -> Result<(), StripeConfigError> {
44    if options.stripe_webhook_secret.is_empty() {
45        return Err(StripeConfigError::EmptyWebhookSecret);
46    }
47    Ok(())
48}
49
50fn build_stripe_plugin(options: StripeOptions) -> AuthPlugin {
51    let subscription_enabled = options.subscription.as_ref().is_some_and(|s| s.enabled);
52    let mut plugin = AuthPlugin::new(UPSTREAM_PLUGIN_ID)
53        .with_version(VERSION)
54        .with_options(options.to_metadata())
55        .with_init(|_| Ok(PluginInitOutput::default()))
56        .with_endpoint(routes::stripe_webhook(options.clone()))
57        .with_database_hook(sync_user_customer_email_hook(options.clone()));
58
59    if options.create_customer_on_sign_up {
60        plugin = plugin.with_database_hook(create_customer_on_sign_up_hook(options.clone()));
61    }
62
63    if options.organization.as_ref().is_some_and(|org| org.enabled) {
64        plugin = plugin.with_database_hook(organization::sync_customer_name_hook(options.clone()));
65    }
66
67    if subscription_enabled && options.organization.as_ref().is_some_and(|org| org.enabled) {
68        for hook in organization::subscription_database_hooks(options.clone()) {
69            plugin = plugin.with_database_hook(hook);
70        }
71    }
72
73    if subscription_enabled {
74        plugin = plugin
75            .with_endpoint(routes::upgrade_subscription(options.clone()))
76            .with_endpoint(routes::cancel_subscription(options.clone()))
77            .with_endpoint(routes::restore_subscription(options.clone()))
78            .with_endpoint(routes::list_active_subscriptions(options.clone()))
79            .with_endpoint(routes::subscription_success(options.clone()))
80            .with_endpoint(routes::create_billing_portal(options.clone()));
81    }
82
83    for contribution in schema::schema_contributions(&options) {
84        plugin = plugin.with_schema(contribution);
85    }
86    for error_code in errors::error_codes() {
87        plugin = plugin.with_error_code(error_code);
88    }
89    plugin
90}
91
92fn create_customer_on_sign_up_hook(options: StripeOptions) -> PluginDatabaseHook {
93    PluginDatabaseHook::after_async(
94        "stripe-create-customer-on-sign-up",
95        PluginDatabaseOperation::Create,
96        move |context, input| {
97            let options = options.clone();
98            Box::pin(async move {
99                let PluginDatabaseAfterInput::Create { query, result } = input else {
100                    return Ok(());
101                };
102                if query.model != "user" {
103                    return Ok(());
104                }
105                if let Err(error) = customers::ensure_user_customer_from_record(
106                    context.adapter,
107                    &options,
108                    options::CustomerCreateContext::database_hook(
109                        context.request_path.clone(),
110                        context.logger,
111                    ),
112                    &result,
113                )
114                .await
115                {
116                    logging::hook_error(
117                        &context,
118                        "Failed to create or link Stripe customer on sign-up",
119                        &error.to_string(),
120                    );
121                }
122                Ok(())
123            })
124        },
125    )
126}
127
128fn sync_user_customer_email_hook(options: StripeOptions) -> PluginDatabaseHook {
129    PluginDatabaseHook::after_async(
130        "stripe-sync-user-customer-email",
131        PluginDatabaseOperation::Update,
132        move |context, input| {
133            let options = options.clone();
134            Box::pin(async move {
135                let PluginDatabaseAfterInput::Update { query, result } = input else {
136                    return Ok(());
137                };
138                if query.model != "user" {
139                    return Ok(());
140                };
141                let Some(result) = result else {
142                    return Ok(());
143                };
144                if let Err(error) =
145                    customers::sync_user_customer_email_from_record(&options.stripe_client, &result)
146                        .await
147                {
148                    logging::hook_error(
149                        &context,
150                        "Failed to sync email to Stripe customer",
151                        &error.to_string(),
152                    );
153                }
154                Ok(())
155            })
156        },
157    )
158}