1mod 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
32pub const VERSION: &str = env!("CARGO_PKG_VERSION");
34
35pub const UPSTREAM_PLUGIN_ID: &str = "stripe";
36
37pub 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}