diff --git a/_migrations/2022-09-17-173657_user_subscriptions/up.sql b/_migrations/2022-09-17-173657_user_subscriptions/up.sql index 01d9d2d5b5..0fa67b2956 100644 --- a/_migrations/2022-09-17-173657_user_subscriptions/up.sql +++ b/_migrations/2022-09-17-173657_user_subscriptions/up.sql @@ -16,11 +16,14 @@ CREATE TABLE user_subscriptions ( -- We want the latitude to correct mistakes in the future. (Hopefully this doesn't bite me later.) maybe_user_token VARCHAR(32) DEFAULT NULL, - -- If we host multiple sites with distinct categories, this will enable us to segregate subscriptions. + -- If we host multiple sites with distinct categories, this will enable us to segregate + -- subscriptions. This is effectively a subscriptions namespace. + -- TODO: Rename 'namespace' subscription_category VARCHAR(32) NOT NULL, -- This is the identifier for the actual type of subscription. -- These will not be defined in the database, but rather the source code. + -- TODO: Rename 'slug' subscription_product_key VARCHAR(32) NOT NULL, -- ========== STRIPE DATA ========== diff --git a/crates/component_subsystem/billing/src/stripe/traits/internal_user_lookup.rs b/crates/component_subsystem/billing/src/stripe/traits/internal_user_lookup.rs index bcda199963..4a507421a9 100644 --- a/crates/component_subsystem/billing/src/stripe/traits/internal_user_lookup.rs +++ b/crates/component_subsystem/billing/src/stripe/traits/internal_user_lookup.rs @@ -48,6 +48,20 @@ pub struct UserMetadata { /// Possible existing Stripe customer ID for the user. pub maybe_existing_stripe_customer_id: Option, + + /// Existing subscriptions that the user has. + pub existing_subscription_keys: Vec, +} + +pub struct SubscriptionKey { + /// The category or namespace for the product, eg "fakeyou" or "powerstream". + /// TODO: Rename "namespace" + pub internal_subscription_category: String, + + /// The key for the product in our internal system (not a stripe id), + /// eg. "fakeyou_en_pro" or "stream_package_plus". + /// TODO: Rename "slug" + pub internal_subscription_product_key: String, } /// Allows us to inject a user lookup from the HTTP request's session info and database backend, diff --git a/crates/component_subsystem/users/src/utils/session_cookie_manager.rs b/crates/component_subsystem/users/src/utils/session_cookie_manager.rs index 35e096e351..4c0da161ae 100644 --- a/crates/component_subsystem/users/src/utils/session_cookie_manager.rs +++ b/crates/component_subsystem/users/src/utils/session_cookie_manager.rs @@ -16,7 +16,13 @@ use sha2::Sha256; use std::collections::BTreeMap; use time::OffsetDateTime; -const COOKIE_VERSION : u32 = 1; +/** + * Cookie version history + * + * Version 1: Claims include "session_token" and "cookie_version" + * Version 2: The "user_token" is added to the claims, and the version is bumped to "2" + */ +const COOKIE_VERSION : u32 = 2; const SESSION_COOKIE_NAME : &'static str = "session"; @@ -29,6 +35,15 @@ pub struct SessionCookieManager { hmac_secret: String, } +#[derive(Clone)] +pub struct SessionCookiePayload { + /// The database primary key for the session instance. + pub session_token: String, + /// The primary key identifier of the user. + /// Version 1 cookies do not have a user token, hence it is optional. + pub maybe_user_token: Option, +} + impl SessionCookieManager { pub fn new(cookie_domain: &str, hmac_secret: &str) -> Self { Self { @@ -37,7 +52,7 @@ impl SessionCookieManager { } } - pub fn create_cookie(&self, session_token: &str) -> AnyhowResult { + pub fn create_cookie(&self, session_token: &str, user_token: &str) -> AnyhowResult { let key: Hmac = Hmac::new_varkey(self.hmac_secret.as_bytes()) .map_err(|e| anyhow!("invalid hmac: {:?}", e))?; @@ -45,6 +60,7 @@ impl SessionCookieManager { let mut claims = BTreeMap::new(); claims.insert("session_token", session_token); + claims.insert("user_token", user_token); claims.insert("cookie_version", &cookie_version); let jwt_string = claims.sign_with_key(&key)?; @@ -69,17 +85,30 @@ impl SessionCookieManager { .finish() } - pub fn decode_session_token(&self, session_cookie: &Cookie) -> AnyhowResult { + pub fn decode_session_cookie_payload(&self, session_cookie: &Cookie) + -> AnyhowResult + { let key: Hmac = Hmac::new_varkey(self.hmac_secret.as_bytes()) - .map_err(|e| anyhow!("invalid hmac: {:?}", e))?; + .map_err(|e| anyhow!("invalid hmac: {:?}", e))?; let cookie_contents = session_cookie.value().to_string(); let claims: BTreeMap = cookie_contents.verify_with_key(&key)?; let session_token = claims["session_token"].clone(); + let maybe_user_token = claims.get("user_token") + .map(|t| t.to_string()); - Ok(session_token) + Ok(SessionCookiePayload { + session_token, + maybe_user_token, + }) + } + + pub fn decode_session_token(&self, session_cookie: &Cookie) -> AnyhowResult { + let cookie_payload = + self.decode_session_cookie_payload(session_cookie)?; + Ok(cookie_payload.session_token) } pub fn decode_session_token_from_request(&self, request: &HttpRequest) diff --git a/crates/service/storyteller_web/src/billing/stripe_internal_user_lookup_impl.rs b/crates/service/storyteller_web/src/billing/stripe_internal_user_lookup_impl.rs index b1c0ee9631..31e3d64f80 100644 --- a/crates/service/storyteller_web/src/billing/stripe_internal_user_lookup_impl.rs +++ b/crates/service/storyteller_web/src/billing/stripe_internal_user_lookup_impl.rs @@ -48,6 +48,7 @@ impl InternalUserLookup for StripeInternalUserLookupImpl { username: Some(user_session.username), user_email: Some(user_session.email_address), maybe_existing_stripe_customer_id: user_session.maybe_stripe_customer_id, + existing_subscription_keys: vec![] })), } }