diff --git a/Cargo.lock b/Cargo.lock index 711e92d3..95b5c584 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2430,6 +2430,7 @@ dependencies = [ "tokio", "tower", "tracing", + "ulid", "url", ] @@ -2505,6 +2506,7 @@ dependencies = [ "thiserror", "tokio", "tracing", + "ulid", "url", ] @@ -2575,6 +2577,7 @@ dependencies = [ "tower", "tower-http", "tracing", + "ulid", "url", ] @@ -2744,6 +2747,7 @@ dependencies = [ "serde", "serde_urlencoded", "serde_with", + "ulid", "url", ] @@ -2781,7 +2785,9 @@ dependencies = [ "thiserror", "tokio", "tracing", + "ulid", "url", + "uuid", ] [[package]] @@ -2813,6 +2819,7 @@ dependencies = [ "thiserror", "tokio", "tracing", + "ulid", "url", ] @@ -4525,6 +4532,7 @@ dependencies = [ "thiserror", "tokio-stream", "url", + "uuid", "webpki-roots", "whoami", ] @@ -5153,6 +5161,17 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e79c4d996edb816c91e4308506774452e55e95c3c9de07b6729e17e15a5ef81" +[[package]] +name = "ulid" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13a3aaa69b04e5b66cc27309710a569ea23593612387d67daaf102e73aa974fd" +dependencies = [ + "rand", + "serde", + "uuid", +] + [[package]] name = "uncased" version = "0.9.7" diff --git a/Cargo.toml b/Cargo.toml index 5c1ac811..f621be0c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,3 +4,6 @@ members = ["crates/*"] [profile.dev.package.num-bigint-dig] opt-level = 3 + +[profile.dev.package.sqlx-macros] +opt-level = 3 diff --git a/crates/axum-utils/Cargo.toml b/crates/axum-utils/Cargo.toml index a84645c2..6a30f414 100644 --- a/crates/axum-utils/Cargo.toml +++ b/crates/axum-utils/Cargo.toml @@ -28,6 +28,7 @@ tokio = "1.21.2" tower = { version = "0.4.13", features = ["util"] } tracing = "0.1.37" url = "2.3.1" +ulid = { version = "1.0.0", features = ["serde"] } mas-data-model = { path = "../data-model" } mas-http = { path = "../http", features = ["client"] } diff --git a/crates/axum-utils/src/fancy_error.rs b/crates/axum-utils/src/fancy_error.rs index 5d078b70..3f238298 100644 --- a/crates/axum-utils/src/fancy_error.rs +++ b/crates/axum-utils/src/fancy_error.rs @@ -23,9 +23,11 @@ pub struct FancyError { context: ErrorContext, } -impl From for FancyError { +impl From for FancyError { fn from(err: E) -> Self { - let context = ErrorContext::new().with_description(err.to_string()); + let context = ErrorContext::new() + .with_description(format!("{err}")) + .with_details(format!("{err:?}")); FancyError { context } } } diff --git a/crates/axum-utils/src/session.rs b/crates/axum-utils/src/session.rs index 9e3af970..d0be4f8d 100644 --- a/crates/axum-utils/src/session.rs +++ b/crates/axum-utils/src/session.rs @@ -20,13 +20,14 @@ use mas_storage::{ }; use serde::{Deserialize, Serialize}; use sqlx::{Executor, Postgres}; +use ulid::Ulid; use crate::CookieExt; /// An encrypted cookie to save the session ID #[derive(Serialize, Deserialize, Debug, Default)] pub struct SessionInfo { - current: Option, + current: Option, } impl SessionInfo { diff --git a/crates/cli/src/commands/manage.rs b/crates/cli/src/commands/manage.rs index 3605bb2f..3cd5f38d 100644 --- a/crates/cli/src/commands/manage.rs +++ b/crates/cli/src/commands/manage.rs @@ -16,7 +16,7 @@ use argon2::Argon2; use clap::Parser; use mas_config::{DatabaseConfig, RootConfig}; use mas_storage::{ - oauth2::client::{insert_client_from_config, lookup_client_by_client_id, truncate_clients}, + oauth2::client::{insert_client_from_config, lookup_client, truncate_clients}, user::{ lookup_user_by_username, lookup_user_email, mark_user_email_as_verified, register_user, }, @@ -96,8 +96,8 @@ impl Options { } for client in config.clients.iter() { - let client_id = &client.client_id; - let res = lookup_client_by_client_id(&mut txn, client_id).await; + let client_id = client.client_id; + let res = lookup_client(&mut txn, client_id).await; match res { Ok(_) => { warn!(%client_id, "Skipping already imported client"); diff --git a/crates/config/Cargo.toml b/crates/config/Cargo.toml index 38e5862e..b78d401b 100644 --- a/crates/config/Cargo.toml +++ b/crates/config/Cargo.toml @@ -17,6 +17,7 @@ schemars = { version = "0.8.11", features = ["url", "chrono"] } figment = { version = "0.10.8", features = ["env", "yaml", "test"] } chrono = { version = "0.4.22", features = ["serde"] } url = { version = "2.3.1", features = ["serde"] } +ulid = { version = "1.0.0", features = ["serde"] } serde = { version = "1.0.147", features = ["derive"] } serde_with = { version = "2.0.1", features = ["hex", "chrono"] } diff --git a/crates/config/src/sections/clients.rs b/crates/config/src/sections/clients.rs index 313df21c..6cd051d0 100644 --- a/crates/config/src/sections/clients.rs +++ b/crates/config/src/sections/clients.rs @@ -21,6 +21,7 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use serde_with::skip_serializing_none; use thiserror::Error; +use ulid::Ulid; use url::Url; use super::ConfigurationSection; @@ -76,7 +77,8 @@ pub enum ClientAuthMethodConfig { #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] pub struct ClientConfig { /// The client ID - pub client_id: String, + #[schemars(with = "String")] + pub client_id: Ulid, /// Authentication method used for this client #[serde(flatten)] @@ -181,6 +183,8 @@ impl ConfigurationSection<'_> for ClientsConfig { #[cfg(test)] mod tests { + use std::str::FromStr; + use figment::Jail; use super::*; @@ -192,24 +196,24 @@ mod tests { "config.yaml", r#" clients: - - client_id: public + - client_id: 01GFWR28C4KNE04WG3HKXB7C9R client_auth_method: none redirect_uris: - https://exemple.fr/callback - - client_id: secret-basic + - client_id: 01GFWR32NCQ12B8Z0J8CPXRRB6 client_auth_method: client_secret_basic client_secret: hello - - client_id: secret-post + - client_id: 01GFWR3WHR93Y5HK389H28VHZ9 client_auth_method: client_secret_post client_secret: hello - - client_id: secret-jwk + - client_id: 01GFWR43R2ZZ8HX9CVBNW9TJWG client_auth_method: client_secret_jwt client_secret: hello - - client_id: jwks + - client_id: 01GFWR4BNFDCC4QDG6AMSP1VRR client_auth_method: private_key_jwt jwks: keys: @@ -233,13 +237,19 @@ mod tests { assert_eq!(config.0.len(), 5); - assert_eq!(config.0[0].client_id, "public"); + assert_eq!( + config.0[0].client_id, + Ulid::from_str("01GFWR28C4KNE04WG3HKXB7C9R").unwrap() + ); assert_eq!( config.0[0].redirect_uris, vec!["https://exemple.fr/callback".parse().unwrap()] ); - assert_eq!(config.0[1].client_id, "secret-basic"); + assert_eq!( + config.0[1].client_id, + Ulid::from_str("01GFWR32NCQ12B8Z0J8CPXRRB6").unwrap() + ); assert_eq!(config.0[1].redirect_uris, Vec::new()); Ok(()) diff --git a/crates/data-model/src/compat.rs b/crates/data-model/src/compat.rs index 07d90c14..42416d68 100644 --- a/crates/data-model/src/compat.rs +++ b/crates/data-model/src/compat.rs @@ -89,7 +89,7 @@ pub struct CompatSession { pub user: User, pub device: Device, pub created_at: DateTime, - pub deleted_at: Option>, + pub finished_at: Option>, } impl From> for CompatSession<()> { @@ -99,7 +99,7 @@ impl From> for CompatSession<()> { user: t.user.into(), device: t.device, created_at: t.created_at, - deleted_at: t.deleted_at, + finished_at: t.finished_at, } } } @@ -144,12 +144,12 @@ impl From> for CompatRefreshToken #[serde(bound = "T: StorageBackend")] pub enum CompatSsoLoginState { Pending, - Fullfilled { - fullfilled_at: DateTime, + Fulfilled { + fulfilled_at: DateTime, session: CompatSession, }, Exchanged { - fullfilled_at: DateTime, + fulfilled_at: DateTime, exchanged_at: DateTime, session: CompatSession, }, @@ -159,19 +159,19 @@ impl From> for CompatSsoLoginSta fn from(t: CompatSsoLoginState) -> Self { match t { CompatSsoLoginState::Pending => Self::Pending, - CompatSsoLoginState::Fullfilled { - fullfilled_at, + CompatSsoLoginState::Fulfilled { + fulfilled_at, session, - } => Self::Fullfilled { - fullfilled_at, + } => Self::Fulfilled { + fulfilled_at, session: session.into(), }, CompatSsoLoginState::Exchanged { - fullfilled_at, + fulfilled_at, exchanged_at, session, } => Self::Exchanged { - fullfilled_at, + fulfilled_at, exchanged_at, session: session.into(), }, @@ -185,7 +185,7 @@ pub struct CompatSsoLogin { #[serde(skip_serializing)] pub data: T::CompatSsoLoginData, pub redirect_uri: Url, - pub token: String, + pub login_token: String, pub created_at: DateTime, pub state: CompatSsoLoginState, } @@ -195,7 +195,7 @@ impl From> for CompatSsoLogin<()> { Self { data: (), redirect_uri: t.redirect_uri, - token: t.token, + login_token: t.login_token, created_at: t.created_at, state: t.state.into(), } diff --git a/crates/data-model/src/oauth2/authorization_grant.rs b/crates/data-model/src/oauth2/authorization_grant.rs index 4b1e7b3f..a183ac5d 100644 --- a/crates/data-model/src/oauth2/authorization_grant.rs +++ b/crates/data-model/src/oauth2/authorization_grant.rs @@ -171,7 +171,6 @@ pub struct AuthorizationGrant { pub state: Option, pub nonce: Option, pub max_age: Option, - pub acr_values: Option, pub response_mode: ResponseMode, pub response_type_id_token: bool, pub created_at: DateTime, @@ -190,7 +189,6 @@ impl From> for AuthorizationGrant state: g.state, nonce: g.nonce, max_age: g.max_age, - acr_values: g.acr_values, response_mode: g.response_mode, response_type_id_token: g.response_type_id_token, created_at: g.created_at, diff --git a/crates/data-model/src/tokens.rs b/crates/data-model/src/tokens.rs index 3bc3b3ca..f11ccb0a 100644 --- a/crates/data-model/src/tokens.rs +++ b/crates/data-model/src/tokens.rs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use chrono::{DateTime, Duration, Utc}; +use chrono::{DateTime, Utc}; use crc::{Crc, CRC_32_ISO_HDLC}; use mas_iana::oauth::OAuthTokenTypeHint; use rand::{distributions::Alphanumeric, Rng}; @@ -24,9 +24,9 @@ use crate::traits::{StorageBackend, StorageBackendMarker}; pub struct AccessToken { pub data: T::AccessTokenData, pub jti: String, - pub token: String, - pub expires_after: Duration, + pub access_token: String, pub created_at: DateTime, + pub expires_at: DateTime, } impl From> for AccessToken<()> { @@ -34,23 +34,24 @@ impl From> for AccessToken<()> { AccessToken { data: (), jti: t.jti, - token: t.token, - expires_after: t.expires_after, + access_token: t.access_token, + expires_at: t.expires_at, created_at: t.created_at, } } } impl AccessToken { + // XXX pub fn exp(&self) -> DateTime { - self.created_at + self.expires_after + self.expires_at } } #[derive(Debug, Clone, PartialEq)] pub struct RefreshToken { pub data: T::RefreshTokenData, - pub token: String, + pub refresh_token: String, pub created_at: DateTime, pub access_token: Option>, } @@ -59,7 +60,7 @@ impl From> for RefreshToken<()> { fn from(t: RefreshToken) -> Self { RefreshToken { data: (), - token: t.token, + refresh_token: t.refresh_token, created_at: t.created_at, access_token: t.access_token.map(Into::into), } diff --git a/crates/data-model/src/users.rs b/crates/data-model/src/users.rs index 4e10aaf6..90d034b1 100644 --- a/crates/data-model/src/users.rs +++ b/crates/data-model/src/users.rs @@ -164,7 +164,7 @@ where #[derive(Debug, Clone, PartialEq, Eq, Serialize)] pub enum UserEmailVerificationState { AlreadyUsed { when: DateTime }, - Expired, + Expired { when: DateTime }, Valid, } @@ -200,7 +200,9 @@ where UserEmailVerificationState::AlreadyUsed { when: Utc::now() - Duration::minutes(5), }, - UserEmailVerificationState::Expired, + UserEmailVerificationState::Expired { + when: Utc::now() - Duration::hours(5), + }, UserEmailVerificationState::Valid, ]; diff --git a/crates/handlers/Cargo.toml b/crates/handlers/Cargo.toml index c2fcd473..e64cd379 100644 --- a/crates/handlers/Cargo.toml +++ b/crates/handlers/Cargo.toml @@ -45,6 +45,7 @@ url = { version = "2.3.1", features = ["serde"] } mime = "0.3.16" rand = "0.8.5" headers = "0.3.8" +ulid = "1.0.0" oauth2-types = { path = "../oauth2-types" } mas-axum-utils = { path = "../axum-utils", default-features = false } diff --git a/crates/handlers/src/compat/login.rs b/crates/handlers/src/compat/login.rs index f456ab7a..490e097d 100644 --- a/crates/handlers/src/compat/login.rs +++ b/crates/handlers/src/compat/login.rs @@ -259,12 +259,15 @@ async fn token_login( match login.state { CompatSsoLoginState::Pending => { tracing::error!( - login.data, + compat_sso_login.id = %login.data, "Exchanged a token for a login that was not fullfilled yet" ); return Err(RouteError::InvalidLoginToken); } - CompatSsoLoginState::Fullfilled { fullfilled_at, .. } => { + CompatSsoLoginState::Fulfilled { + fulfilled_at: fullfilled_at, + .. + } => { if now > fullfilled_at + Duration::seconds(30) { return Err(RouteError::LoginTookTooLong); } @@ -273,7 +276,7 @@ async fn token_login( if now > exchanged_at + Duration::seconds(30) { // TODO: log that session out tracing::error!( - login.data, + compat_sso_login.id = %login.data, "Login token exchanged a second time more than 30s after" ); } diff --git a/crates/handlers/src/compat/login_sso_complete.rs b/crates/handlers/src/compat/login_sso_complete.rs index 3f38cc1a..6083d944 100644 --- a/crates/handlers/src/compat/login_sso_complete.rs +++ b/crates/handlers/src/compat/login_sso_complete.rs @@ -33,6 +33,7 @@ use mas_templates::{CompatSsoContext, ErrorContext, TemplateContext, Templates}; use rand::thread_rng; use serde::{Deserialize, Serialize}; use sqlx::PgPool; +use ulid::Ulid; #[derive(Serialize)] struct AllParams<'s> { @@ -52,7 +53,7 @@ pub async fn get( State(pool): State, State(templates): State, cookie_jar: PrivateCookieJar, - Path(id): Path, + Path(id): Path, Query(params): Query, ) -> Result { let mut conn = pool.acquire().await?; @@ -116,7 +117,7 @@ pub async fn post( State(pool): State, State(templates): State, cookie_jar: PrivateCookieJar, - Path(id): Path, + Path(id): Path, Query(params): Query, Form(form): Form>, ) -> Result { @@ -178,7 +179,7 @@ pub async fn post( let params = AllParams { existing_params, - login_token: &login.token, + login_token: &login.login_token, }; let query = serde_urlencoded::to_string(¶ms)?; redirect_uri.set_query(Some(&query)); diff --git a/crates/handlers/src/compat/refresh.rs b/crates/handlers/src/compat/refresh.rs index 3b2636a3..e07a5768 100644 --- a/crates/handlers/src/compat/refresh.rs +++ b/crates/handlers/src/compat/refresh.rs @@ -17,9 +17,8 @@ use chrono::Duration; use hyper::StatusCode; use mas_data_model::{TokenFormatError, TokenType}; use mas_storage::compat::{ - add_compat_access_token, add_compat_refresh_token, expire_compat_access_token, - lookup_active_compat_refresh_token, replace_compat_refresh_token, - CompatRefreshTokenLookupError, + add_compat_access_token, add_compat_refresh_token, consume_compat_refresh_token, + expire_compat_access_token, lookup_active_compat_refresh_token, CompatRefreshTokenLookupError, }; use rand::thread_rng; use serde::{Deserialize, Serialize}; @@ -125,7 +124,7 @@ pub(crate) async fn post( add_compat_refresh_token(&mut txn, &session, &new_access_token, new_refresh_token_str) .await?; - replace_compat_refresh_token(&mut txn, &refresh_token, &new_refresh_token).await?; + consume_compat_refresh_token(&mut txn, refresh_token).await?; expire_compat_access_token(&mut txn, access_token).await?; txn.commit().await?; diff --git a/crates/handlers/src/oauth2/authorization/complete.rs b/crates/handlers/src/oauth2/authorization/complete.rs index 644a8c7f..23709c2f 100644 --- a/crates/handlers/src/oauth2/authorization/complete.rs +++ b/crates/handlers/src/oauth2/authorization/complete.rs @@ -38,6 +38,7 @@ use mas_templates::Templates; use oauth2_types::requests::{AccessTokenResponse, AuthorizationResponse}; use sqlx::{PgPool, Postgres, Transaction}; use thiserror::Error; +use ulid::Ulid; use super::callback::{ CallbackDestination, CallbackDestinationError, IntoCallbackDestinationError, @@ -109,7 +110,7 @@ pub(crate) async fn get( State(templates): State, State(pool): State, cookie_jar: PrivateCookieJar, - Path(grant_id): Path, + Path(grant_id): Path, ) -> Result { let mut txn = pool.begin().await?; diff --git a/crates/handlers/src/oauth2/consent.rs b/crates/handlers/src/oauth2/consent.rs index e1e5b05b..1fa88ad6 100644 --- a/crates/handlers/src/oauth2/consent.rs +++ b/crates/handlers/src/oauth2/consent.rs @@ -36,6 +36,7 @@ use mas_storage::oauth2::{ use mas_templates::{ConsentContext, PolicyViolationContext, TemplateContext, Templates}; use sqlx::PgPool; use thiserror::Error; +use ulid::Ulid; #[derive(Debug, Error)] pub enum RouteError { @@ -54,7 +55,7 @@ pub(crate) async fn get( State(templates): State, State(pool): State, cookie_jar: PrivateCookieJar, - Path(grant_id): Path, + Path(grant_id): Path, ) -> Result { let mut conn = pool .acquire() @@ -115,7 +116,7 @@ pub(crate) async fn post( State(policy_factory): State>, State(pool): State, cookie_jar: PrivateCookieJar, - Path(grant_id): Path, + Path(grant_id): Path, Form(form): Form>, ) -> Result { let mut txn = pool diff --git a/crates/handlers/src/oauth2/registration.rs b/crates/handlers/src/oauth2/registration.rs index a2f42f00..c6226782 100644 --- a/crates/handlers/src/oauth2/registration.rs +++ b/crates/handlers/src/oauth2/registration.rs @@ -24,10 +24,10 @@ use oauth2_types::{ ClientMetadata, ClientMetadataVerificationError, ClientRegistrationResponse, Localized, }, }; -use rand::{distributions::Alphanumeric, thread_rng, Rng}; use sqlx::PgPool; use thiserror::Error; use tracing::info; +use ulid::Ulid; #[derive(Debug, Error)] pub(crate) enum RouteError { @@ -127,18 +127,14 @@ pub(crate) async fn post( let mut txn = pool.begin().await?; // Let's generate a random client ID - let client_id: String = thread_rng() - .sample_iter(&Alphanumeric) - .take(10) - .map(char::from) - .collect(); + let client_id = Ulid::new(); insert_client( &mut txn, - &client_id, + client_id, metadata.redirect_uris(), None, - &metadata.response_types(), + //&metadata.response_types(), metadata.grant_types(), contacts, metadata @@ -162,7 +158,7 @@ pub(crate) async fn post( txn.commit().await?; let response = ClientRegistrationResponse { - client_id, + client_id: client_id.to_string(), client_secret: None, client_id_issued_at: None, client_secret_expires_at: None, diff --git a/crates/handlers/src/oauth2/token.rs b/crates/handlers/src/oauth2/token.rs index 7209a61a..fb70e7fd 100644 --- a/crates/handlers/src/oauth2/token.rs +++ b/crates/handlers/src/oauth2/token.rs @@ -36,7 +36,7 @@ use mas_storage::{ client::ClientFetchError, end_oauth_session, refresh_token::{ - add_refresh_token, lookup_active_refresh_token, replace_refresh_token, + add_refresh_token, consume_refresh_token, lookup_active_refresh_token, RefreshTokenLookupError, }, }, @@ -311,10 +311,10 @@ async fn authorization_code_grant( ) }; - let access_token = add_access_token(&mut txn, session, &access_token_str, ttl).await?; + let access_token = add_access_token(&mut txn, session, access_token_str.clone(), ttl).await?; let _refresh_token = - add_refresh_token(&mut txn, session, access_token, &refresh_token_str).await?; + add_refresh_token(&mut txn, session, access_token, refresh_token_str.clone()).await?; let id_token = if session.scope.contains(&scope::OPENID) { let mut claims = HashMap::new(); @@ -391,20 +391,21 @@ async fn refresh_token_grant( ) }; - let new_access_token = add_access_token(&mut txn, &session, &access_token_str, ttl).await?; + let new_access_token = + add_access_token(&mut txn, &session, access_token_str.clone(), ttl).await?; let new_refresh_token = - add_refresh_token(&mut txn, &session, new_access_token, &refresh_token_str).await?; + add_refresh_token(&mut txn, &session, new_access_token, refresh_token_str).await?; - replace_refresh_token(&mut txn, &refresh_token, &new_refresh_token).await?; + consume_refresh_token(&mut txn, &refresh_token).await?; if let Some(access_token) = refresh_token.access_token { - revoke_access_token(&mut txn, &access_token).await?; + revoke_access_token(&mut txn, access_token).await?; } let params = AccessTokenResponse::new(access_token_str) .with_expires_in(ttl) - .with_refresh_token(refresh_token_str) + .with_refresh_token(new_refresh_token.refresh_token) .with_scope(session.scope); txn.commit().await?; diff --git a/crates/handlers/src/views/account/emails/add.rs b/crates/handlers/src/views/account/emails/add.rs index fe11ff3f..1165d02c 100644 --- a/crates/handlers/src/views/account/emails/add.rs +++ b/crates/handlers/src/views/account/emails/add.rs @@ -86,7 +86,7 @@ pub(crate) async fn post( return Ok((cookie_jar, login.go()).into_response()); }; - let user_email = add_user_email(&mut txn, &session.user, &form.email).await?; + let user_email = add_user_email(&mut txn, &session.user, form.email).await?; let next = mas_router::AccountVerifyEmail::new(user_email.data); let next = if let Some(action) = query.post_auth_action { next.and_then(action) diff --git a/crates/handlers/src/views/account/emails/mod.rs b/crates/handlers/src/views/account/emails/mod.rs index 5ac9d862..3ab0b950 100644 --- a/crates/handlers/src/views/account/emails/mod.rs +++ b/crates/handlers/src/views/account/emails/mod.rs @@ -17,6 +17,7 @@ use axum::{ response::{Html, IntoResponse, Response}, }; use axum_extra::extract::PrivateCookieJar; +use chrono::Duration; use lettre::{message::Mailbox, Address}; use mas_axum_utils::{ csrf::{CsrfExt, ProtectedForm}, @@ -101,7 +102,8 @@ async fn start_email_verification( let address: Address = user_email.email.parse()?; - let verification = add_user_email_verification_code(executor, user_email, code).await?; + let verification = + add_user_email_verification_code(executor, user_email, Duration::hours(8), code).await?; // And send the verification email let mailbox = Mailbox::new(Some(user.username.clone()), address); @@ -111,7 +113,7 @@ async fn start_email_verification( mailer.send_verification_email(mailbox, &context).await?; info!( - email.id = verification.email.data, + email.id = %verification.email.data, "Verification email sent" ); Ok(()) @@ -141,7 +143,7 @@ pub(crate) async fn post( match form { ManagementForm::Add { email } => { - let user_email = add_user_email(&mut txn, &session.user, &email).await?; + let user_email = add_user_email(&mut txn, &session.user, email).await?; let next = mas_router::AccountVerifyEmail::new(user_email.data); start_email_verification(&mailer, &mut txn, &session.user, user_email).await?; txn.commit().await?; diff --git a/crates/handlers/src/views/account/emails/verify.rs b/crates/handlers/src/views/account/emails/verify.rs index cebcc772..22e427d3 100644 --- a/crates/handlers/src/views/account/emails/verify.rs +++ b/crates/handlers/src/views/account/emails/verify.rs @@ -17,7 +17,6 @@ use axum::{ response::{Html, IntoResponse, Response}, }; use axum_extra::extract::PrivateCookieJar; -use chrono::Duration; use mas_axum_utils::{ csrf::{CsrfExt, ProtectedForm}, FancyError, SessionInfoExt, @@ -31,6 +30,7 @@ use mas_storage::user::{ use mas_templates::{EmailVerificationPageContext, TemplateContext, Templates}; use serde::Deserialize; use sqlx::PgPool; +use ulid::Ulid; use crate::views::shared::OptionalPostAuthAction; @@ -43,7 +43,7 @@ pub(crate) async fn get( State(templates): State, State(pool): State, Query(query): Query, - Path(id): Path, + Path(id): Path, cookie_jar: PrivateCookieJar, ) -> Result { let mut conn = pool.acquire().await?; @@ -81,7 +81,7 @@ pub(crate) async fn post( State(pool): State, cookie_jar: PrivateCookieJar, Query(query): Query, - Path(id): Path, + Path(id): Path, Form(form): Form>, ) -> Result { let mut txn = pool.begin().await?; @@ -105,9 +105,7 @@ pub(crate) async fn post( } // TODO: make those 8 hours configurable - let verification = - lookup_user_email_verification_code(&mut txn, email, &form.code, Duration::hours(8)) - .await?; + let verification = lookup_user_email_verification_code(&mut txn, email, &form.code).await?; // TODO: display nice errors if the code was already consumed or expired let verification = consume_email_verification(&mut txn, verification).await?; diff --git a/crates/handlers/src/views/register.rs b/crates/handlers/src/views/register.rs index 1417abc6..0b966620 100644 --- a/crates/handlers/src/views/register.rs +++ b/crates/handlers/src/views/register.rs @@ -22,6 +22,7 @@ use axum::{ response::{Html, IntoResponse, Response}, }; use axum_extra::extract::PrivateCookieJar; +use chrono::Duration; use lettre::{message::Mailbox, Address}; use mas_axum_utils::{ csrf::{CsrfExt, CsrfToken, ProtectedForm}, @@ -181,7 +182,7 @@ pub(crate) async fn post( let pfh = Argon2::default(); let user = register_user(&mut txn, pfh, &form.username, &form.password).await?; - let user_email = add_user_email(&mut txn, &user, &form.email).await?; + let user_email = add_user_email(&mut txn, &user, form.email).await?; // First, generate a code let range = Uniform::::from(0..1_000_000); @@ -189,7 +190,8 @@ pub(crate) async fn post( let address: Address = user_email.email.parse()?; - let verification = add_user_email_verification_code(&mut txn, user_email, code).await?; + let verification = + add_user_email_verification_code(&mut txn, user_email, Duration::hours(8), code).await?; // And send the verification email let mailbox = Mailbox::new(Some(user.username.clone()), address); diff --git a/crates/router/Cargo.toml b/crates/router/Cargo.toml index 3f5ee371..9b720e22 100644 --- a/crates/router/Cargo.toml +++ b/crates/router/Cargo.toml @@ -11,3 +11,4 @@ serde = { version = "1.0.147", features = ["derive"] } serde_urlencoded = "0.7.1" serde_with = "2.0.1" url = "2.3.1" +ulid = "1.0.0" diff --git a/crates/router/src/endpoints.rs b/crates/router/src/endpoints.rs index ffdf0b34..b12ba5b8 100644 --- a/crates/router/src/endpoints.rs +++ b/crates/router/src/endpoints.rs @@ -14,6 +14,7 @@ use serde::{Deserialize, Serialize}; use serde_with::{serde_as, DisplayFromStr}; +use ulid::Ulid; pub use crate::traits::*; @@ -23,23 +24,23 @@ pub use crate::traits::*; pub enum PostAuthAction { ContinueAuthorizationGrant { #[serde_as(as = "DisplayFromStr")] - data: i64, + data: Ulid, }, ContinueCompatSsoLogin { #[serde_as(as = "DisplayFromStr")] - data: i64, + data: Ulid, }, ChangePassword, } impl PostAuthAction { #[must_use] - pub fn continue_grant(data: i64) -> Self { + pub fn continue_grant(data: Ulid) -> Self { PostAuthAction::ContinueAuthorizationGrant { data } } #[must_use] - pub fn continue_compat_sso_login(data: i64) -> Self { + pub fn continue_compat_sso_login(data: Ulid) -> Self { PostAuthAction::ContinueCompatSsoLogin { data } } @@ -166,14 +167,14 @@ impl Login { } #[must_use] - pub fn and_continue_grant(data: i64) -> Self { + pub fn and_continue_grant(data: Ulid) -> Self { Self { post_auth_action: Some(PostAuthAction::continue_grant(data)), } } #[must_use] - pub fn and_continue_compat_sso_login(data: i64) -> Self { + pub fn and_continue_compat_sso_login(data: Ulid) -> Self { Self { post_auth_action: Some(PostAuthAction::continue_compat_sso_login(data)), } @@ -222,7 +223,7 @@ impl Reauth { } #[must_use] - pub fn and_continue_grant(data: i64) -> Self { + pub fn and_continue_grant(data: Ulid) -> Self { Self { post_auth_action: Some(PostAuthAction::continue_grant(data)), } @@ -275,14 +276,14 @@ impl Register { } #[must_use] - pub fn and_continue_grant(data: i64) -> Self { + pub fn and_continue_grant(data: Ulid) -> Self { Self { post_auth_action: Some(PostAuthAction::continue_grant(data)), } } #[must_use] - pub fn and_continue_compat_sso_login(data: i64) -> Self { + pub fn and_continue_compat_sso_login(data: Ulid) -> Self { Self { post_auth_action: Some(PostAuthAction::continue_compat_sso_login(data)), } @@ -323,13 +324,13 @@ impl From> for Register { /// `GET|POST /account/emails/verify/:id` #[derive(Debug, Clone)] pub struct AccountVerifyEmail { - id: i64, + id: Ulid, post_auth_action: Option, } impl AccountVerifyEmail { #[must_use] - pub fn new(id: i64) -> Self { + pub fn new(id: Ulid) -> Self { Self { id, post_auth_action: None, @@ -415,7 +416,7 @@ impl SimpleRoute for AccountEmails { /// `GET /authorize/:grant_id` #[derive(Debug, Clone)] -pub struct ContinueAuthorizationGrant(pub i64); +pub struct ContinueAuthorizationGrant(pub Ulid); impl Route for ContinueAuthorizationGrant { type Query = (); @@ -430,7 +431,7 @@ impl Route for ContinueAuthorizationGrant { /// `GET /consent/:grant_id` #[derive(Debug, Clone)] -pub struct Consent(pub i64); +pub struct Consent(pub Ulid); impl Route for Consent { type Query = (); @@ -493,13 +494,13 @@ pub struct CompatLoginSsoActionParams { /// `GET|POST /complete-compat-sso/:id` pub struct CompatLoginSsoComplete { - id: i64, + id: Ulid, query: Option, } impl CompatLoginSsoComplete { #[must_use] - pub fn new(id: i64, action: Option) -> Self { + pub fn new(id: Ulid, action: Option) -> Self { Self { id, query: action.map(|action| CompatLoginSsoActionParams { action }), diff --git a/crates/router/src/lib.rs b/crates/router/src/lib.rs index 753613fe..9e6bc468 100644 --- a/crates/router/src/lib.rs +++ b/crates/router/src/lib.rs @@ -30,6 +30,7 @@ pub use self::{endpoints::*, traits::Route, url_builder::UrlBuilder}; mod tests { use std::borrow::Cow; + use ulid::Ulid; use url::Url; use super::*; @@ -42,8 +43,10 @@ mod tests { ); assert_eq!(Index.relative_url(), Cow::Borrowed("/")); assert_eq!( - Login::and_continue_grant(42).relative_url(), - Cow::Borrowed("/login?next=continue_authorization_grant&data=42") + Login::and_continue_grant(Ulid::nil()).relative_url(), + Cow::Borrowed( + "/login?next=continue_authorization_grant&data=00000000000000000000000000" + ) ); } diff --git a/crates/storage/Cargo.toml b/crates/storage/Cargo.toml index d8fb6ff5..f3a154b8 100644 --- a/crates/storage/Cargo.toml +++ b/crates/storage/Cargo.toml @@ -7,7 +7,7 @@ license = "Apache-2.0" [dependencies] tokio = "1.21.2" -sqlx = { version = "0.6.2", features = ["runtime-tokio-rustls", "postgres", "migrate", "chrono", "offline", "json"] } +sqlx = { version = "0.6.2", features = ["runtime-tokio-rustls", "postgres", "migrate", "chrono", "offline", "json", "uuid"] } chrono = { version = "0.4.22", features = ["serde"] } serde = { version = "1.0.147", features = ["derive"] } serde_json = "1.0.87" @@ -20,6 +20,8 @@ argon2 = { version = "0.4.1", features = ["password-hash"] } password-hash = { version = "0.4.2", features = ["std"] } rand = "0.8.5" url = { version = "2.3.1", features = ["serde"] } +uuid = "1.2.1" +ulid = { version = "1.0.0", features = ["uuid", "serde"] } oauth2-types = { path = "../oauth2-types" } mas-data-model = { path = "../data-model" } diff --git a/crates/storage/migrations/20210709203828_timestamp_trigger.down.sql b/crates/storage/migrations/20210709203828_timestamp_trigger.down.sql deleted file mode 100644 index b98fff18..00000000 --- a/crates/storage/migrations/20210709203828_timestamp_trigger.down.sql +++ /dev/null @@ -1,15 +0,0 @@ --- Copyright 2021 The Matrix.org Foundation C.I.C. --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. - -DROP FUNCTION IF EXISTS trigger_set_timestamp(); diff --git a/crates/storage/migrations/20210709203828_timestamp_trigger.up.sql b/crates/storage/migrations/20210709203828_timestamp_trigger.up.sql deleted file mode 100644 index 47c5b45d..00000000 --- a/crates/storage/migrations/20210709203828_timestamp_trigger.up.sql +++ /dev/null @@ -1,21 +0,0 @@ --- Copyright 2021 The Matrix.org Foundation C.I.C. --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. - -CREATE OR REPLACE FUNCTION trigger_set_timestamp() -RETURNS TRIGGER AS $$ -BEGIN - NEW.updated_at = NOW(); - RETURN NEW; -END; -$$ LANGUAGE plpgsql; diff --git a/crates/storage/migrations/20210716213724_users.down.sql b/crates/storage/migrations/20210716213724_users.down.sql deleted file mode 100644 index fdb8145a..00000000 --- a/crates/storage/migrations/20210716213724_users.down.sql +++ /dev/null @@ -1,16 +0,0 @@ --- Copyright 2021 The Matrix.org Foundation C.I.C. --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. - -DROP TRIGGER set_timestamp ON users; -DROP TABLE users; diff --git a/crates/storage/migrations/20210716213724_users.up.sql b/crates/storage/migrations/20210716213724_users.up.sql deleted file mode 100644 index 6293e7aa..00000000 --- a/crates/storage/migrations/20210716213724_users.up.sql +++ /dev/null @@ -1,26 +0,0 @@ --- Copyright 2021 The Matrix.org Foundation C.I.C. --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. - -CREATE TABLE users ( - "id" BIGSERIAL PRIMARY KEY, - "username" TEXT NOT NULL UNIQUE, - "hashed_password" TEXT NOT NULL, - "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), - "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now() -); - -CREATE TRIGGER set_timestamp - BEFORE UPDATE ON users - FOR EACH ROW - EXECUTE PROCEDURE trigger_set_timestamp(); diff --git a/crates/storage/migrations/20210722072901_user_sessions.down.sql b/crates/storage/migrations/20210722072901_user_sessions.down.sql deleted file mode 100644 index 3a33ff4a..00000000 --- a/crates/storage/migrations/20210722072901_user_sessions.down.sql +++ /dev/null @@ -1,17 +0,0 @@ --- Copyright 2021 The Matrix.org Foundation C.I.C. --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. - -DROP TRIGGER set_timestamp ON user_sessions; -DROP TABLE user_session_authentications; -DROP TABLE user_sessions; diff --git a/crates/storage/migrations/20210722072901_user_sessions.up.sql b/crates/storage/migrations/20210722072901_user_sessions.up.sql deleted file mode 100644 index 31196c3c..00000000 --- a/crates/storage/migrations/20210722072901_user_sessions.up.sql +++ /dev/null @@ -1,35 +0,0 @@ --- Copyright 2021 The Matrix.org Foundation C.I.C. --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. - --- A logged in session -CREATE TABLE user_sessions ( - "id" BIGSERIAL PRIMARY KEY, - "user_id" BIGINT NOT NULL REFERENCES users (id) ON DELETE CASCADE, - "active" BOOLEAN NOT NULL DEFAULT TRUE, - - "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), - "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now() -); - -CREATE TRIGGER set_timestamp - BEFORE UPDATE ON user_sessions - FOR EACH ROW - EXECUTE PROCEDURE trigger_set_timestamp(); - --- An authentication within a session -CREATE TABLE user_session_authentications ( - "id" BIGSERIAL PRIMARY KEY, - "session_id" BIGINT NOT NULL REFERENCES user_sessions (id) ON DELETE CASCADE, - "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now() -); diff --git a/crates/storage/migrations/20210731130515_oauth2_sessions.down.sql b/crates/storage/migrations/20210731130515_oauth2_sessions.down.sql deleted file mode 100644 index ced0ee90..00000000 --- a/crates/storage/migrations/20210731130515_oauth2_sessions.down.sql +++ /dev/null @@ -1,17 +0,0 @@ --- Copyright 2021 The Matrix.org Foundation C.I.C. --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. - -DROP TRIGGER set_timestamp ON oauth2_sessions; -DROP TABLE oauth2_codes; -DROP TABLE oauth2_sessions; diff --git a/crates/storage/migrations/20210731130515_oauth2_sessions.up.sql b/crates/storage/migrations/20210731130515_oauth2_sessions.up.sql deleted file mode 100644 index 70e197b0..00000000 --- a/crates/storage/migrations/20210731130515_oauth2_sessions.up.sql +++ /dev/null @@ -1,45 +0,0 @@ --- Copyright 2021 The Matrix.org Foundation C.I.C. --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. - -CREATE TABLE oauth2_sessions ( - "id" BIGSERIAL PRIMARY KEY, - "user_session_id" BIGINT REFERENCES user_sessions (id) ON DELETE CASCADE, - "client_id" TEXT NOT NULL, - "redirect_uri" TEXT NOT NULL, - "scope" TEXT NOT NULL, - "state" TEXT, - "nonce" TEXT, - "max_age" INT, - "response_type" TEXT NOT NULL, - "response_mode" TEXT NOT NULL, - - "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), - "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now() -); - -CREATE TRIGGER set_timestamp - BEFORE UPDATE ON oauth2_sessions - FOR EACH ROW - EXECUTE PROCEDURE trigger_set_timestamp(); - -CREATE TABLE oauth2_codes ( - "id" BIGSERIAL PRIMARY KEY, - "oauth2_session_id" BIGINT NOT NULL REFERENCES oauth2_sessions (id) ON DELETE CASCADE, - "code" TEXT UNIQUE NOT NULL, - "code_challenge_method" SMALLINT, - "code_challenge" TEXT, - - CHECK (("code_challenge" IS NULL AND "code_challenge_method" IS NULL) - OR ("code_challenge" IS NOT NULL AND "code_challenge_method" IS NOT NULL)) -); diff --git a/crates/storage/migrations/20210813080413_oauth2_access_tokens.down.sql b/crates/storage/migrations/20210813080413_oauth2_access_tokens.down.sql deleted file mode 100644 index 60c1cb4d..00000000 --- a/crates/storage/migrations/20210813080413_oauth2_access_tokens.down.sql +++ /dev/null @@ -1,15 +0,0 @@ --- Copyright 2021 The Matrix.org Foundation C.I.C. --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. - -DROP TABLE oauth2_access_tokens; diff --git a/crates/storage/migrations/20210813080413_oauth2_access_tokens.up.sql b/crates/storage/migrations/20210813080413_oauth2_access_tokens.up.sql deleted file mode 100644 index 78d4dd24..00000000 --- a/crates/storage/migrations/20210813080413_oauth2_access_tokens.up.sql +++ /dev/null @@ -1,23 +0,0 @@ --- Copyright 2021 The Matrix.org Foundation C.I.C. --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. - -CREATE TABLE oauth2_access_tokens ( - "id" BIGSERIAL PRIMARY KEY, - "oauth2_session_id" BIGINT NOT NULL REFERENCES oauth2_sessions (id) ON DELETE CASCADE, - - "token" TEXT UNIQUE NOT NULL, - - "expires_after" INT NOT NULL, - "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now() -); diff --git a/crates/storage/migrations/20210827112032_oauth2_refresh_tokens.down.sql b/crates/storage/migrations/20210827112032_oauth2_refresh_tokens.down.sql deleted file mode 100644 index 54a821a7..00000000 --- a/crates/storage/migrations/20210827112032_oauth2_refresh_tokens.down.sql +++ /dev/null @@ -1,16 +0,0 @@ --- Copyright 2021 The Matrix.org Foundation C.I.C. --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. - -DROP TRIGGER set_timestamp ON oauth2_refresh_tokens; -DROP TABLE oauth2_refresh_tokens; diff --git a/crates/storage/migrations/20210827112032_oauth2_refresh_tokens.up.sql b/crates/storage/migrations/20210827112032_oauth2_refresh_tokens.up.sql deleted file mode 100644 index 74a4a9c8..00000000 --- a/crates/storage/migrations/20210827112032_oauth2_refresh_tokens.up.sql +++ /dev/null @@ -1,30 +0,0 @@ --- Copyright 2021 The Matrix.org Foundation C.I.C. --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. - -CREATE TABLE oauth2_refresh_tokens ( - "id" BIGSERIAL PRIMARY KEY, - "oauth2_session_id" BIGINT NOT NULL REFERENCES oauth2_sessions (id) ON DELETE CASCADE, - "oauth2_access_token_id" BIGINT REFERENCES oauth2_access_tokens (id) ON DELETE SET NULL, - - "token" TEXT UNIQUE NOT NULL, - "next_token_id" BIGINT REFERENCES oauth2_refresh_tokens (id), - - "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), - "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now() -); - -CREATE TRIGGER set_timestamp - BEFORE UPDATE ON oauth2_refresh_tokens - FOR EACH ROW - EXECUTE PROCEDURE trigger_set_timestamp(); diff --git a/crates/storage/migrations/20211021201500_oauth2_sessions.down.sql b/crates/storage/migrations/20211021201500_oauth2_sessions.down.sql deleted file mode 100644 index e69de29b..00000000 diff --git a/crates/storage/migrations/20211021201500_oauth2_sessions.up.sql b/crates/storage/migrations/20211021201500_oauth2_sessions.up.sql deleted file mode 100644 index 69ba4c62..00000000 --- a/crates/storage/migrations/20211021201500_oauth2_sessions.up.sql +++ /dev/null @@ -1,103 +0,0 @@ --- Copyright 2021 The Matrix.org Foundation C.I.C. --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. - - --- Replace the old "sessions" table -ALTER TABLE oauth2_sessions RENAME TO oauth2_sessions_old; - --- TODO: how do we handle temporary session upgrades (aka. sudo mode)? -CREATE TABLE oauth2_sessions ( - "id" BIGSERIAL PRIMARY KEY, - "user_session_id" BIGINT NOT NULL REFERENCES user_sessions (id) ON DELETE CASCADE, - "client_id" TEXT NOT NULL, -- The "authorization party" would be more accurate in that case - "scope" TEXT NOT NULL, - - "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now() -); - -TRUNCATE oauth2_access_tokens, oauth2_refresh_tokens; -ALTER TABLE oauth2_access_tokens - DROP CONSTRAINT oauth2_access_tokens_oauth2_session_id_fkey, - ADD CONSTRAINT oauth2_access_tokens_oauth2_session_id_fkey - FOREIGN KEY (oauth2_session_id) REFERENCES oauth2_sessions (id); -ALTER TABLE oauth2_refresh_tokens - DROP CONSTRAINT oauth2_refresh_tokens_oauth2_session_id_fkey, - ADD CONSTRAINT oauth2_refresh_tokens_oauth2_session_id_fkey - FOREIGN KEY (oauth2_session_id) REFERENCES oauth2_sessions (id); -DROP TABLE oauth2_codes, oauth2_sessions_old; - -CREATE TABLE oauth2_authorization_grants ( - "id" BIGSERIAL PRIMARY KEY, -- Saved as encrypted cookie - - -- All this comes from the authorization request - "client_id" TEXT NOT NULL, -- This should be verified before insertion - "redirect_uri" TEXT NOT NULL, -- This should be verified before insertion - "scope" TEXT NOT NULL, -- This should be verified before insertion - "state" TEXT, - "nonce" TEXT, - "max_age" INT CHECK ("max_age" IS NULL OR "max_age" > 0), - "acr_values" TEXT, -- This should be verified before insertion - "response_mode" TEXT NOT NULL, - "code_challenge_method" TEXT, - "code_challenge" TEXT, - - -- The "response_type" parameter broken down - "response_type_code" BOOLEAN NOT NULL, - "response_type_token" BOOLEAN NOT NULL, - "response_type_id_token" BOOLEAN NOT NULL, - - -- This one is created eagerly on grant creation if the response_type - -- includes "code" - -- When looking up codes, it should do "where fulfilled_at is not null" and - -- "inner join on oauth2_sessions". When doing that, it should check the - -- "exchanged_at" field: if it is not null and was exchanged more than 30s - -- ago, the session shold be considered as hijacked and fully invalidated - "code" TEXT UNIQUE, - - "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), - "fulfilled_at" TIMESTAMP WITH TIME ZONE, -- When we got back to the client - "cancelled_at" TIMESTAMP WITH TIME ZONE, -- When that grant was cancelled - "exchanged_at" TIMESTAMP WITH TIME ZONE, -- When the code was exchanged by the client - - "oauth2_session_id" BIGINT REFERENCES oauth2_sessions (id) ON DELETE CASCADE, - - -- Check a few invariants to keep a coherent state. - -- Even though the service should never violate those, it helps ensuring we're not doing anything wrong - - -- Code exchange can only happen after the grant was fulfilled - CONSTRAINT "oauth2_authorization_grants_exchanged_after_fullfill" - CHECK (("exchanged_at" IS NULL) - OR ("exchanged_at" IS NOT NULL AND - "fulfilled_at" IS NOT NULL AND - "exchanged_at" >= "fulfilled_at")), - - -- A grant can be either fulfilled or cancelled, but not both - CONSTRAINT "oauth2_authorization_grants_fulfilled_xor_cancelled" - CHECK ("fulfilled_at" IS NULL OR "cancelled_at" IS NULL), - - -- If it was fulfilled there is an oauth2_session_id attached to it - CONSTRAINT "oauth2_authorization_grants_fulfilled_and_session" - CHECK (("fulfilled_at" IS NULL AND "oauth2_session_id" IS NULL) - OR ("fulfilled_at" IS NOT NULL AND "oauth2_session_id" IS NOT NULL)), - - -- We should have a code if and only if the "code" response_type was asked - CONSTRAINT "oauth2_authorization_grants_code" - CHECK (("response_type_code" IS TRUE AND "code" IS NOT NULL) - OR ("response_type_code" IS FALSE AND "code" IS NULL)), - - -- If we have a challenge, we also have a challenge method and a code - CONSTRAINT "oauth2_authorization_grants_code_challenge" - CHECK (("code_challenge" IS NULL AND "code_challenge_method" IS NULL) - OR ("code_challenge" IS NOT NULL AND "code_challenge_method" IS NOT NULL AND "response_type_code" IS TRUE)) -); diff --git a/crates/storage/migrations/20211214161231_password_credentials.down.sql b/crates/storage/migrations/20211214161231_password_credentials.down.sql deleted file mode 100644 index 6b106b32..00000000 --- a/crates/storage/migrations/20211214161231_password_credentials.down.sql +++ /dev/null @@ -1,26 +0,0 @@ --- Copyright 2021 The Matrix.org Foundation C.I.C. --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. - -ALTER TABLE users ADD COLUMN hashed_password TEXT; - -UPDATE users u - SET hashed_password = up.hashed_password - FROM user_passwords up - WHERE up.user_id = u.id; - -ALTER TABLE users - ALTER COLUMN hashed_password - SET NOT NULL; - -DROP TABLE user_passwords; diff --git a/crates/storage/migrations/20211214161231_password_credentials.up.sql b/crates/storage/migrations/20211214161231_password_credentials.up.sql deleted file mode 100644 index 33642987..00000000 --- a/crates/storage/migrations/20211214161231_password_credentials.up.sql +++ /dev/null @@ -1,27 +0,0 @@ --- Copyright 2021 The Matrix.org Foundation C.I.C. --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. - -CREATE TABLE user_passwords ( - "id" BIGSERIAL PRIMARY KEY, - "user_id" BIGINT NOT NULL REFERENCES users (id) ON DELETE CASCADE, - "hashed_password" TEXT NOT NULL, - "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now() -); - -INSERT INTO - user_passwords (user_id, hashed_password, created_at) -SELECT id, hashed_password, updated_at -FROM users; - -ALTER TABLE users DROP COLUMN hashed_password; diff --git a/crates/storage/migrations/20220114112012_oauth_session_end.down.sql b/crates/storage/migrations/20220114112012_oauth_session_end.down.sql deleted file mode 100644 index d209e665..00000000 --- a/crates/storage/migrations/20220114112012_oauth_session_end.down.sql +++ /dev/null @@ -1,16 +0,0 @@ --- Copyright 2021 The Matrix.org Foundation C.I.C. --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. - -ALTER TABLE oauth2_sessions - DROP COLUMN "ended_at"; diff --git a/crates/storage/migrations/20220114112012_oauth_session_end.up.sql b/crates/storage/migrations/20220114112012_oauth_session_end.up.sql deleted file mode 100644 index 87f800d6..00000000 --- a/crates/storage/migrations/20220114112012_oauth_session_end.up.sql +++ /dev/null @@ -1,16 +0,0 @@ --- Copyright 2021 The Matrix.org Foundation C.I.C. --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. - -ALTER TABLE oauth2_sessions - ADD COLUMN "ended_at" TIMESTAMP WITH TIME ZONE DEFAULT NULL; diff --git a/crates/storage/migrations/20220114150141_user_emails.down.sql b/crates/storage/migrations/20220114150141_user_emails.down.sql deleted file mode 100644 index 8e8b097e..00000000 --- a/crates/storage/migrations/20220114150141_user_emails.down.sql +++ /dev/null @@ -1,15 +0,0 @@ --- Copyright 2021 The Matrix.org Foundation C.I.C. --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. - -DROP TABLE user_emails; diff --git a/crates/storage/migrations/20220114150141_user_emails.up.sql b/crates/storage/migrations/20220114150141_user_emails.up.sql deleted file mode 100644 index 102a8ed8..00000000 --- a/crates/storage/migrations/20220114150141_user_emails.up.sql +++ /dev/null @@ -1,24 +0,0 @@ --- Copyright 2021 The Matrix.org Foundation C.I.C. --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. - -CREATE TABLE user_emails ( - "id" BIGSERIAL PRIMARY KEY, - "user_id" BIGINT NOT NULL REFERENCES users (id) ON DELETE CASCADE, - "email" TEXT NOT NULL, - "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), - "confirmed_at" TIMESTAMP WITH TIME ZONE DEFAULT NULL -); - -ALTER TABLE users - ADD COLUMN "primary_email_id" BIGINT REFERENCES user_emails (id) ON DELETE SET NULL; diff --git a/crates/storage/migrations/20220121145350_email_verification.down.sql b/crates/storage/migrations/20220121145350_email_verification.down.sql deleted file mode 100644 index d948d06a..00000000 --- a/crates/storage/migrations/20220121145350_email_verification.down.sql +++ /dev/null @@ -1,15 +0,0 @@ --- Copyright 2021 The Matrix.org Foundation C.I.C. --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. - -DROP TABLE user_email_verifications; diff --git a/crates/storage/migrations/20220121145350_email_verification.up.sql b/crates/storage/migrations/20220121145350_email_verification.up.sql deleted file mode 100644 index 89acb821..00000000 --- a/crates/storage/migrations/20220121145350_email_verification.up.sql +++ /dev/null @@ -1,21 +0,0 @@ --- Copyright 2022 The Matrix.org Foundation C.I.C. --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. - -CREATE TABLE user_email_verifications ( - "id" BIGSERIAL PRIMARY KEY, - "user_email_id" BIGINT NOT NULL REFERENCES user_emails (id) ON DELETE CASCADE, - "code" TEXT UNIQUE NOT NULL, - "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), - "consumed_at" TIMESTAMP WITH TIME ZONE DEFAULT NULL -); diff --git a/crates/storage/migrations/20220228144045_oauth2_clients.down.sql b/crates/storage/migrations/20220228144045_oauth2_clients.down.sql deleted file mode 100644 index 844e3935..00000000 --- a/crates/storage/migrations/20220228144045_oauth2_clients.down.sql +++ /dev/null @@ -1,17 +0,0 @@ --- Copyright 2021 The Matrix.org Foundation C.I.C. --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. - -DROP TABLE oauth2_client_redirect_uris; -DROP TRIGGER set_timestamp ON oauth2_clients; -DROP TABLE oauth2_clients; diff --git a/crates/storage/migrations/20220228144045_oauth2_clients.up.sql b/crates/storage/migrations/20220228144045_oauth2_clients.up.sql deleted file mode 100644 index dcc2da78..00000000 --- a/crates/storage/migrations/20220228144045_oauth2_clients.up.sql +++ /dev/null @@ -1,51 +0,0 @@ --- Copyright 2022 The Matrix.org Foundation C.I.C. --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. - -CREATE TABLE oauth2_clients ( - "id" BIGSERIAL PRIMARY KEY, - "client_id" TEXT NOT NULL UNIQUE, - "encrypted_client_secret" TEXT, - "response_types" TEXT[] NOT NULL, - "grant_type_authorization_code" BOOL NOT NULL, - "grant_type_refresh_token" BOOL NOT NULL, - "contacts" TEXT[] NOT NULL, - "client_name" TEXT, - "logo_uri" TEXT, - "client_uri" TEXT, - "policy_uri" TEXT, - "tos_uri" TEXT, - "jwks_uri" TEXT, - "jwks" JSONB, - "id_token_signed_response_alg" TEXT, - "token_endpoint_auth_method" TEXT, - "token_endpoint_auth_signing_alg" TEXT, - "initiate_login_uri" TEXT, - - "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), - "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), - - -- jwks and jwks_uri can't be set at the same time - CHECK ("jwks" IS NULL OR "jwks_uri" IS NULL) -); - -CREATE TRIGGER set_timestamp - BEFORE UPDATE ON oauth2_clients - FOR EACH ROW - EXECUTE PROCEDURE trigger_set_timestamp(); - -CREATE TABLE oauth2_client_redirect_uris ( - "id" BIGSERIAL PRIMARY KEY, - "oauth2_client_id" BIGINT NOT NULL REFERENCES oauth2_clients (id) ON DELETE CASCADE, - "redirect_uri" TEXT NOT NULL -); diff --git a/crates/storage/migrations/20220421091049_oauth2_client_more_metadata.down.sql b/crates/storage/migrations/20220421091049_oauth2_client_more_metadata.down.sql deleted file mode 100644 index 95917ea3..00000000 --- a/crates/storage/migrations/20220421091049_oauth2_client_more_metadata.down.sql +++ /dev/null @@ -1,16 +0,0 @@ --- Copyright 2022 The Matrix.org Foundation C.I.C. --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. - -ALTER TABLE oauth2_clients - DROP COLUMN "userinfo_signed_response_alg" TEXT; diff --git a/crates/storage/migrations/20220421091049_oauth2_client_more_metadata.up.sql b/crates/storage/migrations/20220421091049_oauth2_client_more_metadata.up.sql deleted file mode 100644 index 1b30a733..00000000 --- a/crates/storage/migrations/20220421091049_oauth2_client_more_metadata.up.sql +++ /dev/null @@ -1,16 +0,0 @@ --- Copyright 2022 The Matrix.org Foundation C.I.C. --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. - -ALTER TABLE oauth2_clients - ADD COLUMN "userinfo_signed_response_alg" TEXT; diff --git a/crates/storage/migrations/20220428161108_oauth2_client_fk.down.sql b/crates/storage/migrations/20220428161108_oauth2_client_fk.down.sql deleted file mode 100644 index d070a591..00000000 --- a/crates/storage/migrations/20220428161108_oauth2_client_fk.down.sql +++ /dev/null @@ -1,23 +0,0 @@ --- Copyright 2022 The Matrix.org Foundation C.I.C. --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. - -TRUNCATE TABLE oauth2_sessions, oauth2_authorization_grants RESTART IDENTITY CASCADE; - -ALTER TABLE oauth2_sessions - DROP COLUMN "oauth2_client_id", - ADD COLUMN "client_id" TEXT NOT NULL; - -ALTER TABLE oauth2_authorization_grants - DROP COLUMN "oauth2_client_id", - ADD COLUMN "client_id" TEXT NOT NULL; diff --git a/crates/storage/migrations/20220428161108_oauth2_client_fk.up.sql b/crates/storage/migrations/20220428161108_oauth2_client_fk.up.sql deleted file mode 100644 index 56c461cd..00000000 --- a/crates/storage/migrations/20220428161108_oauth2_client_fk.up.sql +++ /dev/null @@ -1,27 +0,0 @@ --- Copyright 2022 The Matrix.org Foundation C.I.C. --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. - -TRUNCATE TABLE oauth2_sessions, oauth2_authorization_grants RESTART IDENTITY CASCADE; - -ALTER TABLE oauth2_sessions - DROP COLUMN "client_id", - ADD COLUMN "oauth2_client_id" BIGINT - NOT NULL - REFERENCES oauth2_clients (id) ON DELETE CASCADE; - -ALTER TABLE oauth2_authorization_grants - DROP COLUMN "client_id", - ADD COLUMN "oauth2_client_id" BIGINT - NOT NULL - REFERENCES oauth2_clients (id) ON DELETE CASCADE; diff --git a/crates/storage/migrations/20220429071609_oauth2_consent.down.sql b/crates/storage/migrations/20220429071609_oauth2_consent.down.sql deleted file mode 100644 index 16d939f0..00000000 --- a/crates/storage/migrations/20220429071609_oauth2_consent.down.sql +++ /dev/null @@ -1,17 +0,0 @@ --- Copyright 2022 The Matrix.org Foundation C.I.C. --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. - -DROP TRIGGER set_timestamp ON oauth2_consents; -DROP INDEX oauth2_consents_client_id_user_id_key; -DROP TABLE oauth2_consents; diff --git a/crates/storage/migrations/20220429071609_oauth2_consent.up.sql b/crates/storage/migrations/20220429071609_oauth2_consent.up.sql deleted file mode 100644 index d2f0cbc2..00000000 --- a/crates/storage/migrations/20220429071609_oauth2_consent.up.sql +++ /dev/null @@ -1,32 +0,0 @@ --- Copyright 2022 The Matrix.org Foundation C.I.C. --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. - -CREATE TABLE oauth2_consents ( - "id" BIGSERIAL PRIMARY KEY, - "oauth2_client_id" BIGINT NOT NULL REFERENCES oauth2_clients (id) ON DELETE CASCADE, - "user_id" BIGINT NOT NULL REFERENCES users (id) ON DELETE CASCADE, - "scope_token" TEXT NOT NULL, - "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), - "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), - - CONSTRAINT user_client_scope_tuple UNIQUE ("oauth2_client_id", "user_id", "scope_token") -); - -CREATE INDEX oauth2_consents_client_id_user_id_key - ON oauth2_consents ("oauth2_client_id", "user_id"); - -CREATE TRIGGER set_timestamp - BEFORE UPDATE ON oauth2_consents - FOR EACH ROW - EXECUTE PROCEDURE trigger_set_timestamp(); diff --git a/crates/storage/migrations/20220506134736_oauth2_authz_grant_require_consent.down.sql b/crates/storage/migrations/20220506134736_oauth2_authz_grant_require_consent.down.sql deleted file mode 100644 index c2ee11cb..00000000 --- a/crates/storage/migrations/20220506134736_oauth2_authz_grant_require_consent.down.sql +++ /dev/null @@ -1,16 +0,0 @@ --- Copyright 2022 The Matrix.org Foundation C.I.C. --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. - -ALTER TABLE oauth2_authorization_grants - DROP COLUMN requires_consent; diff --git a/crates/storage/migrations/20220506134736_oauth2_authz_grant_require_consent.up.sql b/crates/storage/migrations/20220506134736_oauth2_authz_grant_require_consent.up.sql deleted file mode 100644 index 77196c49..00000000 --- a/crates/storage/migrations/20220506134736_oauth2_authz_grant_require_consent.up.sql +++ /dev/null @@ -1,16 +0,0 @@ --- Copyright 2022 The Matrix.org Foundation C.I.C. --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. - -ALTER TABLE oauth2_authorization_grants - ADD COLUMN requires_consent BOOLEAN NOT NULL DEFAULT 'f'; diff --git a/crates/storage/migrations/20220512150806_compat_login.down.sql b/crates/storage/migrations/20220512150806_compat_login.down.sql deleted file mode 100644 index d269544f..00000000 --- a/crates/storage/migrations/20220512150806_compat_login.down.sql +++ /dev/null @@ -1,17 +0,0 @@ --- Copyright 2022 The Matrix.org Foundation C.I.C. --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. - -DROP TABLE compat_refresh_tokens; -DROP TABLE compat_access_tokens; -DROP TABLE compat_session; diff --git a/crates/storage/migrations/20220512150806_compat_login.up.sql b/crates/storage/migrations/20220512150806_compat_login.up.sql deleted file mode 100644 index 9b06e207..00000000 --- a/crates/storage/migrations/20220512150806_compat_login.up.sql +++ /dev/null @@ -1,42 +0,0 @@ --- Copyright 2022 The Matrix.org Foundation C.I.C. --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. - -CREATE TABLE compat_sessions ( - "id" BIGSERIAL PRIMARY KEY, - "user_id" BIGINT NOT NULL REFERENCES users (id) ON DELETE CASCADE, - "device_id" TEXT UNIQUE NOT NULL, - - "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), - "deleted_at" TIMESTAMP WITH TIME ZONE -); - -CREATE TABLE compat_access_tokens ( - "id" BIGSERIAL PRIMARY KEY, - "compat_session_id" BIGINT NOT NULL REFERENCES compat_sessions (id) ON DELETE CASCADE, - "token" TEXT UNIQUE NOT NULL, - - "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), - "expires_at" TIMESTAMP WITH TIME ZONE -); - -CREATE TABLE compat_refresh_tokens ( - "id" BIGSERIAL PRIMARY KEY, - "compat_session_id" BIGINT NOT NULL REFERENCES compat_sessions (id) ON DELETE CASCADE, - "compat_access_token_id" BIGINT REFERENCES compat_access_tokens (id) ON DELETE SET NULL, - - "token" TEXT UNIQUE NOT NULL, - "next_token_id" BIGINT REFERENCES compat_refresh_tokens (id), - - "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now() -); diff --git a/crates/storage/migrations/20220519113358_compat_sso_login.up.sql b/crates/storage/migrations/20220519113358_compat_sso_login.up.sql deleted file mode 100644 index 8ea0fe4f..00000000 --- a/crates/storage/migrations/20220519113358_compat_sso_login.up.sql +++ /dev/null @@ -1,25 +0,0 @@ --- Copyright 2022 The Matrix.org Foundation C.I.C. --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. - -CREATE TABLE compat_sso_logins ( - "id" BIGSERIAL PRIMARY KEY, - "redirect_uri" TEXT NOT NULL, - "token" TEXT UNIQUE NOT NULL, - - "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), - "fullfilled_at" TIMESTAMP WITH TIME ZONE, - "exchanged_at" TIMESTAMP WITH TIME ZONE, - - "compat_session_id" BIGINT REFERENCES compat_sessions (id) ON DELETE CASCADE -); diff --git a/crates/storage/migrations/20220902093231_oauth_remove_token_grant.down.sql b/crates/storage/migrations/20220902093231_oauth_remove_token_grant.down.sql deleted file mode 100644 index 3875f614..00000000 --- a/crates/storage/migrations/20220902093231_oauth_remove_token_grant.down.sql +++ /dev/null @@ -1,19 +0,0 @@ --- Copyright 2022 The Matrix.org Foundation C.I.C. --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. - -ALTER TABLE oauth2_authorization_grants - ADD COLUMN "response_type_token" BOOLEAN NOT NULL DEFAULT 'f'; - -ALTER TABLE oauth2_authorization_grants - ALTER COLUMN "response_type_token" DROP DEFAULT; diff --git a/crates/storage/migrations/20220902093231_oauth_remove_token_grant.up.sql b/crates/storage/migrations/20220902093231_oauth_remove_token_grant.up.sql deleted file mode 100644 index 933712bf..00000000 --- a/crates/storage/migrations/20220902093231_oauth_remove_token_grant.up.sql +++ /dev/null @@ -1,16 +0,0 @@ --- Copyright 2022 The Matrix.org Foundation C.I.C. --- --- Licensed under the Apache License, Version 2.0 (the "License"); --- you may not use this file except in compliance with the License. --- You may obtain a copy of the License at --- --- http://www.apache.org/licenses/LICENSE-2.0 --- --- Unless required by applicable law or agreed to in writing, software --- distributed under the License is distributed on an "AS IS" BASIS, --- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. --- See the License for the specific language governing permissions and --- limitations under the License. - -ALTER TABLE oauth2_authorization_grants - DROP COLUMN "response_type_token"; diff --git a/crates/storage/migrations/20220519113358_compat_sso_login.down.sql b/crates/storage/migrations/20221018142001_init.down.sql similarity index 95% rename from crates/storage/migrations/20220519113358_compat_sso_login.down.sql rename to crates/storage/migrations/20221018142001_init.down.sql index b998a2aa..2e5a72de 100644 --- a/crates/storage/migrations/20220519113358_compat_sso_login.down.sql +++ b/crates/storage/migrations/20221018142001_init.down.sql @@ -12,4 +12,3 @@ -- See the License for the specific language governing permissions and -- limitations under the License. -DROP TABLE compat_sso_logins; diff --git a/crates/storage/migrations/20221018142001_init.up.sql b/crates/storage/migrations/20221018142001_init.up.sql new file mode 100644 index 00000000..a5f1c0d9 --- /dev/null +++ b/crates/storage/migrations/20221018142001_init.up.sql @@ -0,0 +1,350 @@ +-- Copyright 2022 The Matrix.org Foundation C.I.C. +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. + +----------- +-- Users -- +----------- + +CREATE TABLE "users" ( + "user_id" UUID NOT NULL + CONSTRAINT "users_pkey" + PRIMARY KEY, + + "username" TEXT NOT NULL + CONSTRAINT "users_username_unique" + UNIQUE, + + "created_at" TIMESTAMP WITH TIME ZONE NOT NULL +); + +CREATE TABLE "user_passwords" ( + "user_password_id" UUID NOT NULL + CONSTRAINT "user_passwords_pkey" + PRIMARY KEY, + + "user_id" UUID NOT NULL + CONSTRAINT "user_passwords_user_id_fkey" + REFERENCES "users" ("user_id"), + + "hashed_password" TEXT NOT NULL, + "created_at" TIMESTAMP WITH TIME ZONE NOT NULL +); + +CREATE TABLE "user_emails" ( + "user_email_id" UUID NOT NULL + CONSTRAINT "user_emails_pkey" + PRIMARY KEY, + + "user_id" UUID NOT NULL + CONSTRAINT "user_emails_user_id_fkey" + REFERENCES "users" ("user_id") + ON DELETE CASCADE, + + "email" TEXT NOT NULL, + "created_at" TIMESTAMP WITH TIME ZONE NOT NULL, + "confirmed_at" TIMESTAMP WITH TIME ZONE +); + +ALTER TABLE "users" + ADD COLUMN "primary_user_email_id" UUID + CONSTRAINT "users_primary_user_email_id_fkey" + REFERENCES "user_emails" ("user_email_id") + ON DELETE SET NULL; + +CREATE TABLE "user_email_confirmation_codes" ( + "user_email_confirmation_code_id" UUID NOT NULL + CONSTRAINT "user_email_confirmation_codes_pkey" + PRIMARY KEY, + + "user_email_id" UUID NOT NULL + CONSTRAINT "user_email_confirmation_codes_user_email_id_fkey" + REFERENCES "user_emails" ("user_email_id"), + + "code" TEXT NOT NULL + CONSTRAINT "user_email_confirmation_codes_code_unique" + UNIQUE, + + "created_at" TIMESTAMP WITH TIME ZONE NOT NULL, + "expires_at" TIMESTAMP WITH TIME ZONE NOT NULL, + "consumed_at" TIMESTAMP WITH TIME ZONE +); + +CREATE TABLE "user_sessions" ( + "user_session_id" UUID NOT NULL + CONSTRAINT "user_sessions_pkey" + PRIMARY KEY, + + "user_id" UUID NOT NULL + CONSTRAINT "user_sessions_user_id_fkey" + REFERENCES "users" ("user_id"), + + "created_at" TIMESTAMP WITH TIME ZONE NOT NULL, + "finished_at" TIMESTAMP WITH TIME ZONE +); + +CREATE TABLE "user_session_authentications" ( + "user_session_authentication_id" UUID NOT NULL + CONSTRAINT "user_session_authentications_pkey" + PRIMARY KEY, + + "user_session_id" UUID NOT NULL + CONSTRAINT "user_session_authentications_user_session_id_fkey" + REFERENCES "user_sessions" ("user_session_id"), + + "created_at" TIMESTAMP WITH TIME ZONE NOT NULL +); + +--------------------- +-- Compat sessions -- +--------------------- + +CREATE TABLE "compat_sessions" ( + "compat_session_id" UUID NOT NULL + CONSTRAINT "compat_sessions_pkey" + PRIMARY KEY, + + "user_id" UUID NOT NULL + CONSTRAINT "compat_sessions_user_id_fkey" + REFERENCES "users" ("user_id"), + + "device_id" TEXT NOT NULL + CONSTRAINT "compat_sessions_device_id_unique" + UNIQUE, + + "created_at" TIMESTAMP WITH TIME ZONE NOT NULL, + "finished_at" TIMESTAMP WITH TIME ZONE +); + +CREATE TABLE "compat_sso_logins" ( + "compat_sso_login_id" UUID NOT NULL + CONSTRAINT "compat_sso_logins_pkey" + PRIMARY KEY, + + "redirect_uri" TEXT NOT NULL, + + "login_token" TEXT NOT NULL + CONSTRAINT "compat_sessions_login_token_unique" + UNIQUE, + + "compat_session_id" UUID + CONSTRAINT "compat_sso_logins_compat_session_id_fkey" + REFERENCES "compat_sessions" ("compat_session_id") + ON DELETE SET NULL, + + "created_at" TIMESTAMP WITH TIME ZONE NOT NULL, + "fulfilled_at" TIMESTAMP WITH TIME ZONE, + "exchanged_at" TIMESTAMP WITH TIME ZONE +); + +CREATE TABLE "compat_access_tokens" ( + "compat_access_token_id" UUID NOT NULL + CONSTRAINT "compat_access_tokens_pkey" + PRIMARY KEY, + + "compat_session_id" UUID NOT NULL + CONSTRAINT "compat_access_tokens_compat_session_id_fkey" + REFERENCES "compat_sessions" ("compat_session_id"), + + "access_token" TEXT NOT NULL + CONSTRAINT "compat_access_tokens_access_token_unique" + UNIQUE, + + "created_at" TIMESTAMP WITH TIME ZONE NOT NULL, + "expires_at" TIMESTAMP WITH TIME ZONE +); + +CREATE TABLE "compat_refresh_tokens" ( + "compat_refresh_token_id" UUID NOT NULL + CONSTRAINT "compat_refresh_tokens_pkey" + PRIMARY KEY, + + "compat_session_id" UUID NOT NULL + CONSTRAINT "compat_refresh_tokens_compat_session_id_fkey" + REFERENCES "compat_sessions" ("compat_session_id"), + + "compat_access_token_id" UUID NOT NULL + CONSTRAINT "compat_refresh_tokens_compat_access_token_id_fkey" + REFERENCES "compat_access_tokens" ("compat_access_token_id"), + + "refresh_token" TEXT NOT NULL + CONSTRAINT "compat_refresh_tokens_refresh_token_unique" + UNIQUE, + + "created_at" TIMESTAMP WITH TIME ZONE NOT NULL, + "consumed_at" TIMESTAMP WITH TIME ZONE +); + +---------------- +-- OAuth 2.0 --- +---------------- + +CREATE TABLE "oauth2_clients" ( + "oauth2_client_id" UUID NOT NULL + CONSTRAINT "oauth2_clients_pkey" + PRIMARY KEY, + + "encrypted_client_secret" TEXT, + + "grant_type_authorization_code" BOOLEAN NOT NULL, + "grant_type_refresh_token" BOOLEAN NOT NULL, + + "client_name" TEXT, + "logo_uri" TEXT, + "client_uri" TEXT, + "policy_uri" TEXT, + "tos_uri" TEXT, + + "jwks_uri" TEXT, + "jwks" JSONB, + + "id_token_signed_response_alg" TEXT, + "token_endpoint_auth_method" TEXT, + "token_endpoint_auth_signing_alg" TEXT, + "initiate_login_uri" TEXT, + "userinfo_signed_response_alg" TEXT, + + "created_at" TIMESTAMP WITH TIME ZONE NULL +); + +CREATE TABLE "oauth2_client_redirect_uris" ( + "oauth2_client_redirect_uri_id" UUID NOT NULL + CONSTRAINT "oauth2_client_redirect_uris_pkey" + PRIMARY KEY, + + "oauth2_client_id" UUID NOT NULL + CONSTRAINT "tbl_oauth2_client_id_fkey" + REFERENCES "oauth2_clients" ("oauth2_client_id"), + + "redirect_uri" TEXT NOT NULL +); + +CREATE TABLE "oauth2_sessions" ( + "oauth2_session_id" UUID NOT NULL + CONSTRAINT "oauth2_sessions_pkey" + PRIMARY KEY, + + "user_session_id" UUID NOT NULL + CONSTRAINT "oauth2_sessions_user_session_id_fkey" + REFERENCES "user_sessions" ("user_session_id"), + + "oauth2_client_id" UUID NOT NULL + CONSTRAINT "oauth2_sessions_oauth2_client_id_fkey" + REFERENCES "oauth2_clients" ("oauth2_client_id"), + + "scope" TEXT NOT NULL, + + "created_at" TIMESTAMP WITH TIME ZONE NOT NULL, + "finished_at" TIMESTAMP WITH TIME ZONE +); + +CREATE TABLE "oauth2_consents" ( + "oauth2_consent_id" UUID NOT NULL + CONSTRAINT "oauth2_consents_pkey" + PRIMARY KEY, + + "oauth2_client_id" UUID NOT NULL + CONSTRAINT "oauth2_consents_oauth2_client_id_fkey" + REFERENCES "oauth2_clients" ("oauth2_client_id"), + + "user_id" UUID NOT NULL + CONSTRAINT "oauth2_consents_user_id_fkey" + REFERENCES "users" ("user_id"), + + "scope_token" TEXT NOT NULL, + + "created_at" TIMESTAMP WITH TIME ZONE NOT NULL, + "refreshed_at" TIMESTAMP WITH TIME ZONE, + + CONSTRAINT "oauth2_consents_unique" + UNIQUE ("oauth2_client_id", "user_id", "scope_token") +); + +CREATE INDEX "oauth2_consents_oauth2_client_id_user_id" + ON "oauth2_consents" ("oauth2_client_id", "user_id"); + +CREATE TABLE "oauth2_access_tokens" ( + "oauth2_access_token_id" UUID NOT NULL + CONSTRAINT "oauth2_access_tokens_pkey" + PRIMARY KEY, + + "oauth2_session_id" UUID NOT NULL + CONSTRAINT "oauth2_access_tokens_oauth2_session_id_fkey" + REFERENCES "oauth2_sessions" ("oauth2_session_id"), + + "access_token" TEXT NOT NULL + CONSTRAINT "oauth2_access_tokens_unique" + UNIQUE, + + "created_at" TIMESTAMP WITH TIME ZONE NOT NULL, + "expires_at" TIMESTAMP WITH TIME ZONE NOT NULL, + "revoked_at" TIMESTAMP WITH TIME ZONE +); + +CREATE TABLE "oauth2_refresh_tokens" ( + "oauth2_refresh_token_id" UUID NOT NULL + CONSTRAINT "oauth2_refresh_tokens_pkey" + PRIMARY KEY, + + "oauth2_session_id" UUID NOT NULL + CONSTRAINT "oauth2_access_tokens_oauth2_session_id_fkey" + REFERENCES "oauth2_sessions" ("oauth2_session_id"), + + "oauth2_access_token_id" UUID NOT NULL + CONSTRAINT "oauth2_refresh_tokens_oauth2_access_token_id_fkey" + REFERENCES "oauth2_access_tokens" ("oauth2_access_token_id"), + + "refresh_token" TEXT NOT NULL + CONSTRAINT "oauth2_refresh_tokens_unique" + UNIQUE, + + "created_at" TIMESTAMP WITH TIME ZONE NOT NULL, + "consumed_at" TIMESTAMP WITH TIME ZONE, + "revoked_at" TIMESTAMP WITH TIME ZONE +); + +CREATE TABLE "oauth2_authorization_grants" ( + "oauth2_authorization_grant_id" UUID NOT NULL + CONSTRAINT "oauth2_authorization_grants_pkey" + PRIMARY KEY, + + "oauth2_client_id" UUID NOT NULL + CONSTRAINT "tbl_oauth2_client_fkey" + REFERENCES "oauth2_clients" ("oauth2_client_id"), + + "oauth2_session_id" UUID + CONSTRAINT "tbl_oauth2_session_fkey" + REFERENCES "oauth2_sessions" ("oauth2_session_id"), + + "authorization_code" TEXT + CONSTRAINT "oauth2_authorization_grants_authorization_code_unique" + UNIQUE, + + "redirect_uri" TEXT NOT NULL, + + "scope" TEXT NOT NULL, + "state" TEXT, + "nonce" TEXT, + "max_age" INTEGER, + "response_mode" TEXT NOT NULL, + "code_challenge_method" TEXT, + "code_challenge" TEXT, + "response_type_code" BOOLEAN NOT NULL, + "response_type_id_token" BOOLEAN NOT NULL, + "requires_consent" BOOLEAN NOT NULL, + + "created_at" TIMESTAMP WITH TIME ZONE NOT NULL, + "fulfilled_at" TIMESTAMP WITH TIME ZONE, + "cancelled_at" TIMESTAMP WITH TIME ZONE, + "exchanged_at" TIMESTAMP WITH TIME ZONE +); diff --git a/crates/storage/sqlx-data.json b/crates/storage/sqlx-data.json index 04dfd60e..5af83525 100644 --- a/crates/storage/sqlx-data.json +++ b/crates/storage/sqlx-data.json @@ -1,246 +1,12 @@ { "db": "PostgreSQL", - "02be1a7451e890cb0cc07b32c937881ac9bd1707eb498e20a3cf27737c95a949": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Left": [ - "Int8", - "Int8" - ] - } - }, - "query": "\n UPDATE compat_refresh_tokens\n SET next_token_id = $2\n WHERE id = $1\n " - }, - "096060f2be446fd77ee29308c673f9ba9210fb110444f4fccfeb976424ef4376": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Left": [ - "Int8" - ] - } - }, - "query": "\n UPDATE oauth2_authorization_grants AS og\n SET\n requires_consent = 'f'\n WHERE\n og.id = $1\n " - }, - "0c056fcc1a85d00db88034bcc582376cf220e1933d2932e520c44ed9931f5c9d": { - "describe": { - "columns": [ - { - "name": "id", - "ordinal": 0, - "type_info": "Int8" - }, - { - "name": "created_at", - "ordinal": 1, - "type_info": "Timestamptz" - } - ], - "nullable": [ - false, - false - ], - "parameters": { - "Left": [ - "Int8", - "Int8", - "Text" - ] - } - }, - "query": "\n INSERT INTO oauth2_refresh_tokens\n (oauth2_session_id, oauth2_access_token_id, token)\n VALUES\n ($1, $2, $3)\n RETURNING\n id, created_at\n " - }, - "11f29a7b467bef1cf483d91eede7849707e01847542e4fc3c1be702560bf36bf": { - "describe": { - "columns": [ - { - "name": "id", - "ordinal": 0, - "type_info": "Int8" - } - ], - "nullable": [ - false - ], - "parameters": { - "Left": [ - "Text", - "Text", - "TextArray", - "Bool", - "Bool", - "Text", - "Jsonb", - "Text" - ] - } - }, - "query": "\n INSERT INTO oauth2_clients\n (client_id,\n encrypted_client_secret,\n response_types,\n grant_type_authorization_code,\n grant_type_refresh_token,\n token_endpoint_auth_method,\n jwks,\n jwks_uri,\n contacts)\n VALUES\n ($1, $2, $3, $4, $5, $6, $7, $8, '{}')\n RETURNING id\n " - }, - "307fd9f71e7a94a0a0d9ce523ee9792e127485d0d12480c43f179dd9b75afbab": { - "describe": { - "columns": [ - { - "name": "id", - "ordinal": 0, - "type_info": "Int8" - }, - { - "name": "created_at", - "ordinal": 1, - "type_info": "Timestamptz" - } - ], - "nullable": [ - false, - false - ], - "parameters": { - "Left": [ - "Int8" - ] - } - }, - "query": "\n INSERT INTO user_sessions (user_id)\n VALUES ($1)\n RETURNING id, created_at\n " - }, - "32c94e013dc6cc0422dd8bc9ceaaf9100fee09df0cf52f84086a619de2fbbaaf": { - "describe": { - "columns": [ - { - "name": "compat_refresh_token_id", - "ordinal": 0, - "type_info": "Int8" - }, - { - "name": "compat_refresh_token", - "ordinal": 1, - "type_info": "Text" - }, - { - "name": "compat_refresh_token_created_at", - "ordinal": 2, - "type_info": "Timestamptz" - }, - { - "name": "compat_access_token_id", - "ordinal": 3, - "type_info": "Int8" - }, - { - "name": "compat_access_token", - "ordinal": 4, - "type_info": "Text" - }, - { - "name": "compat_access_token_created_at", - "ordinal": 5, - "type_info": "Timestamptz" - }, - { - "name": "compat_access_token_expires_at", - "ordinal": 6, - "type_info": "Timestamptz" - }, - { - "name": "compat_session_id", - "ordinal": 7, - "type_info": "Int8" - }, - { - "name": "compat_session_created_at", - "ordinal": 8, - "type_info": "Timestamptz" - }, - { - "name": "compat_session_deleted_at", - "ordinal": 9, - "type_info": "Timestamptz" - }, - { - "name": "compat_session_device_id", - "ordinal": 10, - "type_info": "Text" - }, - { - "name": "user_id!", - "ordinal": 11, - "type_info": "Int8" - }, - { - "name": "user_username!", - "ordinal": 12, - "type_info": "Text" - }, - { - "name": "user_email_id?", - "ordinal": 13, - "type_info": "Int8" - }, - { - "name": "user_email?", - "ordinal": 14, - "type_info": "Text" - }, - { - "name": "user_email_created_at?", - "ordinal": 15, - "type_info": "Timestamptz" - }, - { - "name": "user_email_confirmed_at?", - "ordinal": 16, - "type_info": "Timestamptz" - } - ], - "nullable": [ - false, - false, - false, - false, - false, - false, - true, - false, - false, - true, - false, - false, - false, - false, - false, - false, - true - ], - "parameters": { - "Left": [ - "Text" - ] - } - }, - "query": "\n SELECT\n cr.id AS \"compat_refresh_token_id\",\n cr.token AS \"compat_refresh_token\",\n cr.created_at AS \"compat_refresh_token_created_at\",\n ct.id AS \"compat_access_token_id\",\n ct.token AS \"compat_access_token\",\n ct.created_at AS \"compat_access_token_created_at\",\n ct.expires_at AS \"compat_access_token_expires_at\",\n cs.id AS \"compat_session_id\",\n cs.created_at AS \"compat_session_created_at\",\n cs.deleted_at AS \"compat_session_deleted_at\",\n cs.device_id AS \"compat_session_device_id\",\n u.id AS \"user_id!\",\n u.username AS \"user_username!\",\n ue.id AS \"user_email_id?\",\n ue.email AS \"user_email?\",\n ue.created_at AS \"user_email_created_at?\",\n ue.confirmed_at AS \"user_email_confirmed_at?\"\n\n FROM compat_refresh_tokens cr\n INNER JOIN compat_access_tokens ct\n ON ct.id = cr.compat_access_token_id\n INNER JOIN compat_sessions cs\n ON cs.id = cr.compat_session_id\n INNER JOIN users u\n ON u.id = cs.user_id\n LEFT JOIN user_emails ue\n ON ue.id = u.primary_email_id\n\n WHERE cr.token = $1\n AND cr.next_token_id IS NULL\n AND cs.deleted_at IS NULL\n " - }, - "366ea127c7b220960f17fd1b651600826ac10b8baf92f0e936fd07f34a7dc0fc": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Left": [ - "Int8" - ] - } - }, - "query": "\n UPDATE compat_access_tokens\n SET expires_at = NOW()\n WHERE id = $1\n " - }, - "41b5ecd6860791ac6f90417ac51eb977b8c69a3dd81af4672b2592efb65963eb": { + "028302788438b399be2eca6fb40c85b270c364a6ae58d256d52e6eafb4b8e6c3": { "describe": { "columns": [ { "name": "user_email_id", "ordinal": 0, - "type_info": "Int8" + "type_info": "Uuid" }, { "name": "user_email", @@ -266,67 +32,251 @@ ], "parameters": { "Left": [ - "Int8" + "Uuid" ] } }, - "query": "\n SELECT \n ue.id AS \"user_email_id\",\n ue.email AS \"user_email\",\n ue.created_at AS \"user_email_created_at\",\n ue.confirmed_at AS \"user_email_confirmed_at\"\n FROM user_emails ue\n\n WHERE ue.user_id = $1\n\n ORDER BY ue.email ASC\n " + "query": "\n SELECT \n ue.user_email_id,\n ue.email AS \"user_email\",\n ue.created_at AS \"user_email_created_at\",\n ue.confirmed_at AS \"user_email_confirmed_at\"\n FROM user_emails ue\n\n WHERE ue.user_id = $1\n\n ORDER BY ue.email ASC\n " }, - "494c17c20047e761b0dbdac0e13854af7955743afd970bbcae83ba944838c58e": { + "039a1c4c5234f381449de3d401dd5ebc79fb3794f614aa5012b8e28e6d26a847": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Uuid", + "Uuid", + "Text", + "Timestamptz" + ] + } + }, + "query": "\n INSERT INTO compat_sessions \n (compat_session_id, user_id, device_id, created_at)\n VALUES ($1, $2, $3, $4)\n " + }, + "1166343ad1563cb66ab387368f67320a53c34edf388bdb991359ebdf324497d5": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Uuid", + "Timestamptz" + ] + } + }, + "query": "\n UPDATE user_emails\n SET confirmed_at = $2\n WHERE user_email_id = $1\n " + }, + "1a10e6189300563e79684eb7ccc6c29b0418aadfdeea6f8bc5a700a411409c73": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Text", + "Timestamptz" + ] + } + }, + "query": "\n UPDATE compat_sessions cs\n SET finished_at = $2\n FROM compat_access_tokens ca\n WHERE ca.access_token = $1\n AND ca.compat_session_id = cs.compat_session_id\n AND cs.finished_at IS NULL\n " + }, + "1e6d4799268af538a8dd05ce95ab34d2e359ecba544d7c3022ba95c5dda44a7e": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Uuid", + "Uuid", + "Timestamptz" + ] + } + }, + "query": "\n INSERT INTO user_session_authentications \n (user_session_authentication_id, user_session_id, created_at)\n VALUES ($1, $2, $3)\n " + }, + "224f5b9acd93dbeae223ee94286fce25e6fe0f94926bea983916bc5facbc67b6": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Uuid", + "Uuid", + "Text", + "Timestamptz" + ] + } + }, + "query": "\n INSERT INTO user_email_confirmation_codes \n (user_email_confirmation_code_id, user_email_id, code, created_at)\n VALUES ($1, $2, $3, $4)\n " + }, + "24d6154b138a5e9105b996d6447e45a5c208e157f6583b4220cf58813d6f436c": { "describe": { "columns": [ { - "name": "id", + "name": "oauth2_authorization_grant_id", "ordinal": 0, - "type_info": "Int8" + "type_info": "Uuid" }, { - "name": "user_id", + "name": "oauth2_authorization_grant_created_at", "ordinal": 1, - "type_info": "Int8" + "type_info": "Timestamptz" }, { - "name": "username", + "name": "oauth2_authorization_grant_cancelled_at", "ordinal": 2, - "type_info": "Text" + "type_info": "Timestamptz" }, { - "name": "created_at", + "name": "oauth2_authorization_grant_fulfilled_at", "ordinal": 3, "type_info": "Timestamptz" }, { - "name": "last_authentication_id?", + "name": "oauth2_authorization_grant_exchanged_at", "ordinal": 4, - "type_info": "Int8" - }, - { - "name": "last_authd_at?", - "ordinal": 5, "type_info": "Timestamptz" }, { - "name": "user_email_id?", - "ordinal": 6, - "type_info": "Int8" + "name": "oauth2_authorization_grant_scope", + "ordinal": 5, + "type_info": "Text" }, { - "name": "user_email?", + "name": "oauth2_authorization_grant_state", + "ordinal": 6, + "type_info": "Text" + }, + { + "name": "oauth2_authorization_grant_redirect_uri", "ordinal": 7, "type_info": "Text" }, { - "name": "user_email_created_at?", + "name": "oauth2_authorization_grant_response_mode", "ordinal": 8, + "type_info": "Text" + }, + { + "name": "oauth2_authorization_grant_nonce", + "ordinal": 9, + "type_info": "Text" + }, + { + "name": "oauth2_authorization_grant_max_age", + "ordinal": 10, + "type_info": "Int4" + }, + { + "name": "oauth2_client_id", + "ordinal": 11, + "type_info": "Uuid" + }, + { + "name": "oauth2_authorization_grant_code", + "ordinal": 12, + "type_info": "Text" + }, + { + "name": "oauth2_authorization_grant_response_type_code", + "ordinal": 13, + "type_info": "Bool" + }, + { + "name": "oauth2_authorization_grant_response_type_id_token", + "ordinal": 14, + "type_info": "Bool" + }, + { + "name": "oauth2_authorization_grant_code_challenge", + "ordinal": 15, + "type_info": "Text" + }, + { + "name": "oauth2_authorization_grant_code_challenge_method", + "ordinal": 16, + "type_info": "Text" + }, + { + "name": "oauth2_authorization_grant_requires_consent", + "ordinal": 17, + "type_info": "Bool" + }, + { + "name": "oauth2_session_id?", + "ordinal": 18, + "type_info": "Uuid" + }, + { + "name": "user_session_id?", + "ordinal": 19, + "type_info": "Uuid" + }, + { + "name": "user_session_created_at?", + "ordinal": 20, + "type_info": "Timestamptz" + }, + { + "name": "user_id?", + "ordinal": 21, + "type_info": "Uuid" + }, + { + "name": "user_username?", + "ordinal": 22, + "type_info": "Text" + }, + { + "name": "user_session_last_authentication_id?", + "ordinal": 23, + "type_info": "Uuid" + }, + { + "name": "user_session_last_authentication_created_at?", + "ordinal": 24, + "type_info": "Timestamptz" + }, + { + "name": "user_email_id?", + "ordinal": 25, + "type_info": "Uuid" + }, + { + "name": "user_email?", + "ordinal": 26, + "type_info": "Text" + }, + { + "name": "user_email_created_at?", + "ordinal": 27, "type_info": "Timestamptz" }, { "name": "user_email_confirmed_at?", - "ordinal": 9, + "ordinal": 28, "type_info": "Timestamptz" } ], "nullable": [ + false, + false, + true, + true, + true, + false, + true, + false, + false, + true, + true, + false, + true, + false, + false, + true, + true, + false, + false, false, false, false, @@ -340,51 +290,531 @@ ], "parameters": { "Left": [ - "Int8" + "Text" ] } }, - "query": "\n SELECT\n s.id,\n u.id AS user_id,\n u.username,\n s.created_at,\n a.id AS \"last_authentication_id?\",\n a.created_at AS \"last_authd_at?\",\n ue.id AS \"user_email_id?\",\n ue.email AS \"user_email?\",\n ue.created_at AS \"user_email_created_at?\",\n ue.confirmed_at AS \"user_email_confirmed_at?\"\n FROM user_sessions s\n INNER JOIN users u \n ON s.user_id = u.id\n LEFT JOIN user_session_authentications a\n ON a.session_id = s.id\n LEFT JOIN user_emails ue\n ON ue.id = u.primary_email_id\n WHERE s.id = $1 AND s.active\n ORDER BY a.created_at DESC\n LIMIT 1\n " + "query": "\n SELECT\n og.oauth2_authorization_grant_id,\n og.created_at AS oauth2_authorization_grant_created_at,\n og.cancelled_at AS oauth2_authorization_grant_cancelled_at,\n og.fulfilled_at AS oauth2_authorization_grant_fulfilled_at,\n og.exchanged_at AS oauth2_authorization_grant_exchanged_at,\n og.scope AS oauth2_authorization_grant_scope,\n og.state AS oauth2_authorization_grant_state,\n og.redirect_uri AS oauth2_authorization_grant_redirect_uri,\n og.response_mode AS oauth2_authorization_grant_response_mode,\n og.nonce AS oauth2_authorization_grant_nonce,\n og.max_age AS oauth2_authorization_grant_max_age,\n og.oauth2_client_id AS oauth2_client_id,\n og.authorization_code AS oauth2_authorization_grant_code,\n og.response_type_code AS oauth2_authorization_grant_response_type_code,\n og.response_type_id_token AS oauth2_authorization_grant_response_type_id_token,\n og.code_challenge AS oauth2_authorization_grant_code_challenge,\n og.code_challenge_method AS oauth2_authorization_grant_code_challenge_method,\n og.requires_consent AS oauth2_authorization_grant_requires_consent,\n os.oauth2_session_id AS \"oauth2_session_id?\",\n us.user_session_id AS \"user_session_id?\",\n us.created_at AS \"user_session_created_at?\",\n u.user_id AS \"user_id?\",\n u.username AS \"user_username?\",\n usa.user_session_authentication_id AS \"user_session_last_authentication_id?\",\n usa.created_at AS \"user_session_last_authentication_created_at?\",\n ue.user_email_id AS \"user_email_id?\",\n ue.email AS \"user_email?\",\n ue.created_at AS \"user_email_created_at?\",\n ue.confirmed_at AS \"user_email_confirmed_at?\"\n FROM\n oauth2_authorization_grants og\n LEFT JOIN oauth2_sessions os\n USING (oauth2_session_id)\n LEFT JOIN user_sessions us\n USING (user_session_id)\n LEFT JOIN users u\n USING (user_id)\n LEFT JOIN user_session_authentications usa\n USING (user_session_id)\n LEFT JOIN user_emails ue\n ON ue.user_email_id = u.primary_user_email_id\n\n WHERE og.authorization_code = $1\n\n ORDER BY usa.created_at DESC\n LIMIT 1\n " }, - "4a6bee8775e2c614a28dc691e7e59d0e685859dc6cda07296326f2d9cfb09114": { - "describe": { - "columns": [ - { - "name": "id", - "ordinal": 0, - "type_info": "Int8" - }, - { - "name": "created_at", - "ordinal": 1, - "type_info": "Timestamptz" - } - ], - "nullable": [ - false, - false - ], - "parameters": { - "Left": [ - "Int8", - "Text", - "Interval" - ] - } - }, - "query": "\n INSERT INTO compat_access_tokens (compat_session_id, token, created_at, expires_at)\n VALUES ($1, $2, NOW(), NOW() + $3)\n RETURNING id, created_at\n " - }, - "4b9de6face2e21117c947b4f550cc747ad8397b6dfadb6bc6a84124763dc66e8": { + "262bee715889dc3e608639549600a131e641951ff979634e7c97afc74bbc1605": { "describe": { "columns": [], "nullable": [], "parameters": { "Left": [ - "Int8" + "Uuid", + "Timestamptz" ] } }, - "query": "\n UPDATE users\n SET primary_email_id = user_emails.id \n FROM user_emails\n WHERE user_emails.id = $1\n AND users.id = user_emails.user_id\n " + "query": "\n UPDATE oauth2_authorization_grants\n SET exchanged_at = $2\n WHERE oauth2_authorization_grant_id = $1\n " + }, + "26a9391df9f1128673cdaf431fe8c5e4a83b576ddf7b02d92abfab6deadd4fa2": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Uuid", + "Text", + "Bool", + "Bool", + "Text", + "Jsonb", + "Text" + ] + } + }, + "query": "\n INSERT INTO oauth2_clients\n (oauth2_client_id,\n encrypted_client_secret,\n grant_type_authorization_code,\n grant_type_refresh_token,\n token_endpoint_auth_method,\n jwks,\n jwks_uri)\n VALUES\n ($1, $2, $3, $4, $5, $6, $7)\n " + }, + "27a729b229491d179391b19b634f07291312bd238380c5a7ea0f60e9b71dfb14": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Uuid", + "Text", + "Timestamptz" + ] + } + }, + "query": "\n INSERT INTO users (user_id, username, created_at)\n VALUES ($1, $2, $3)\n " + }, + "2e756fe7be50128c0acc5f79df3a084230e9ca13cd45bd0858f97e59da20006e": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Uuid", + "Timestamptz" + ] + } + }, + "query": "\n UPDATE compat_sso_logins\n SET\n exchanged_at = $2\n WHERE\n compat_sso_login_id = $1\n " + }, + "2e885c30ae28a14f136b6529614bdbde9d6c3d48f3a24edf2373c28718f55f07": { + "describe": { + "columns": [ + { + "name": "user_email_confirmation_code_id", + "ordinal": 0, + "type_info": "Uuid" + }, + { + "name": "code", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "created_at", + "ordinal": 2, + "type_info": "Timestamptz" + }, + { + "name": "consumed_at", + "ordinal": 3, + "type_info": "Timestamptz" + } + ], + "nullable": [ + false, + false, + false, + true + ], + "parameters": { + "Left": [ + "Text", + "Uuid" + ] + } + }, + "query": "\n SELECT\n ec.user_email_confirmation_code_id,\n ec.code,\n ec.created_at,\n ec.consumed_at\n FROM user_email_confirmation_codes ec\n WHERE ec.code = $1\n AND ec.user_email_id = $2\n " + }, + "31d583a4e76e39f14f2ba300dc360ca44aea5f8d69d8675368513fbd0fc2d93c": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Uuid", + "Uuid", + "Uuid", + "TextArray", + "Timestamptz" + ] + } + }, + "query": "\n INSERT INTO oauth2_consents \n (oauth2_consent_id, user_id, oauth2_client_id, scope_token, created_at)\n SELECT $1, $2, $3, scope_token, $5 FROM UNNEST($4::text[]) scope_token\n ON CONFLICT (user_id, oauth2_client_id, scope_token) DO UPDATE SET refreshed_at = $5\n " + }, + "360466ff599c67c9af2ac75399c0b536a22c1178972a0172b707bcc81d47357b": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Uuid", + "Uuid", + "Uuid", + "Text", + "Timestamptz" + ] + } + }, + "query": "\n INSERT INTO compat_refresh_tokens\n (compat_refresh_token_id, compat_session_id,\n compat_access_token_id, refresh_token, created_at)\n VALUES ($1, $2, $3, $4, $5)\n " + }, + "3d94de9d292a91c091595215f898ef21c6ede2bb1bd1d59f6aed76429098909a": { + "describe": { + "columns": [ + { + "name": "compat_access_token_id", + "ordinal": 0, + "type_info": "Uuid" + }, + { + "name": "compat_access_token", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "compat_access_token_created_at", + "ordinal": 2, + "type_info": "Timestamptz" + }, + { + "name": "compat_access_token_expires_at", + "ordinal": 3, + "type_info": "Timestamptz" + }, + { + "name": "compat_session_id", + "ordinal": 4, + "type_info": "Uuid" + }, + { + "name": "compat_session_created_at", + "ordinal": 5, + "type_info": "Timestamptz" + }, + { + "name": "compat_session_finished_at", + "ordinal": 6, + "type_info": "Timestamptz" + }, + { + "name": "compat_session_device_id", + "ordinal": 7, + "type_info": "Text" + }, + { + "name": "user_id!", + "ordinal": 8, + "type_info": "Uuid" + }, + { + "name": "user_username!", + "ordinal": 9, + "type_info": "Text" + }, + { + "name": "user_email_id?", + "ordinal": 10, + "type_info": "Uuid" + }, + { + "name": "user_email?", + "ordinal": 11, + "type_info": "Text" + }, + { + "name": "user_email_created_at?", + "ordinal": 12, + "type_info": "Timestamptz" + }, + { + "name": "user_email_confirmed_at?", + "ordinal": 13, + "type_info": "Timestamptz" + } + ], + "nullable": [ + false, + false, + false, + true, + false, + false, + true, + false, + false, + false, + false, + false, + false, + true + ], + "parameters": { + "Left": [ + "Text" + ] + } + }, + "query": "\n SELECT\n ct.compat_access_token_id,\n ct.access_token AS \"compat_access_token\",\n ct.created_at AS \"compat_access_token_created_at\",\n ct.expires_at AS \"compat_access_token_expires_at\",\n cs.compat_session_id,\n cs.created_at AS \"compat_session_created_at\",\n cs.finished_at AS \"compat_session_finished_at\",\n cs.device_id AS \"compat_session_device_id\",\n u.user_id AS \"user_id!\",\n u.username AS \"user_username!\",\n ue.user_email_id AS \"user_email_id?\",\n ue.email AS \"user_email?\",\n ue.created_at AS \"user_email_created_at?\",\n ue.confirmed_at AS \"user_email_confirmed_at?\"\n\n FROM compat_access_tokens ct\n INNER JOIN compat_sessions cs\n USING (compat_session_id)\n INNER JOIN users u\n USING (user_id)\n LEFT JOIN user_emails ue\n ON ue.user_email_id = u.primary_user_email_id\n\n WHERE ct.access_token = $1\n AND (ct.expires_at IS NULL OR ct.expires_at > NOW())\n AND cs.finished_at IS NULL\n " + }, + "3e8f862ed05ce3e58c181ac6e0bd71e0a6a88419611af6f4117d14d9c36cb1ef": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Uuid", + "Uuid", + "Text", + "Timestamptz" + ] + } + }, + "query": "\n INSERT INTO user_emails (user_email_id, user_id, email, created_at)\n VALUES ($1, $2, $3, $4)\n " + }, + "42bfb0de5bbea2d580f1ff2322255731a4a5655ba80fc2dba0b55a0add8c55c0": { + "describe": { + "columns": [ + { + "name": "compat_sso_login_id", + "ordinal": 0, + "type_info": "Uuid" + }, + { + "name": "compat_sso_login_token", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "compat_sso_login_redirect_uri", + "ordinal": 2, + "type_info": "Text" + }, + { + "name": "compat_sso_login_created_at", + "ordinal": 3, + "type_info": "Timestamptz" + }, + { + "name": "compat_sso_login_fulfilled_at", + "ordinal": 4, + "type_info": "Timestamptz" + }, + { + "name": "compat_sso_login_exchanged_at", + "ordinal": 5, + "type_info": "Timestamptz" + }, + { + "name": "compat_session_id?", + "ordinal": 6, + "type_info": "Uuid" + }, + { + "name": "compat_session_created_at?", + "ordinal": 7, + "type_info": "Timestamptz" + }, + { + "name": "compat_session_finished_at?", + "ordinal": 8, + "type_info": "Timestamptz" + }, + { + "name": "compat_session_device_id?", + "ordinal": 9, + "type_info": "Text" + }, + { + "name": "user_id?", + "ordinal": 10, + "type_info": "Uuid" + }, + { + "name": "user_username?", + "ordinal": 11, + "type_info": "Text" + }, + { + "name": "user_email_id?", + "ordinal": 12, + "type_info": "Uuid" + }, + { + "name": "user_email?", + "ordinal": 13, + "type_info": "Text" + }, + { + "name": "user_email_created_at?", + "ordinal": 14, + "type_info": "Timestamptz" + }, + { + "name": "user_email_confirmed_at?", + "ordinal": 15, + "type_info": "Timestamptz" + } + ], + "nullable": [ + false, + false, + false, + false, + true, + true, + false, + false, + true, + false, + false, + false, + false, + false, + false, + true + ], + "parameters": { + "Left": [ + "Uuid" + ] + } + }, + "query": "\n SELECT\n cl.compat_sso_login_id,\n cl.login_token AS \"compat_sso_login_token\",\n cl.redirect_uri AS \"compat_sso_login_redirect_uri\",\n cl.created_at AS \"compat_sso_login_created_at\",\n cl.fulfilled_at AS \"compat_sso_login_fulfilled_at\",\n cl.exchanged_at AS \"compat_sso_login_exchanged_at\",\n cs.compat_session_id AS \"compat_session_id?\",\n cs.created_at AS \"compat_session_created_at?\",\n cs.finished_at AS \"compat_session_finished_at?\",\n cs.device_id AS \"compat_session_device_id?\",\n u.user_id AS \"user_id?\",\n u.username AS \"user_username?\",\n ue.user_email_id AS \"user_email_id?\",\n ue.email AS \"user_email?\",\n ue.created_at AS \"user_email_created_at?\",\n ue.confirmed_at AS \"user_email_confirmed_at?\"\n FROM compat_sso_logins cl\n LEFT JOIN compat_sessions cs\n USING (compat_session_id)\n LEFT JOIN users u\n USING (user_id)\n LEFT JOIN user_emails ue\n ON ue.user_email_id = u.primary_user_email_id\n WHERE cl.compat_sso_login_id = $1\n " + }, + "43a5cafbdc8037e9fb779812a0793cf0859902aa0dc8d25d4c33d231d3d1118b": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Uuid", + "Uuid", + "Text", + "Timestamptz", + "Timestamptz" + ] + } + }, + "query": "\n INSERT INTO oauth2_access_tokens\n (oauth2_access_token_id, oauth2_session_id, access_token, created_at, expires_at)\n VALUES\n ($1, $2, $3, $4, $5)\n " + }, + "46c5ae7052504bfd7b94f20e61b9cf92570779a794bccda23dd654fb8523f340": { + "describe": { + "columns": [ + { + "name": "fulfilled_at!: DateTime", + "ordinal": 0, + "type_info": "Timestamptz" + } + ], + "nullable": [ + true + ], + "parameters": { + "Left": [ + "Uuid", + "Uuid" + ] + } + }, + "query": "\n UPDATE oauth2_authorization_grants AS og\n SET\n oauth2_session_id = os.oauth2_session_id,\n fulfilled_at = os.created_at\n FROM oauth2_sessions os\n WHERE\n og.oauth2_authorization_grant_id = $1\n AND os.oauth2_session_id = $2\n RETURNING fulfilled_at AS \"fulfilled_at!: DateTime\"\n " + }, + "47fff42fd9871f73baf3e3ebb9e296fa65f7bc99f94639891f29d56d204b659a": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Uuid", + "Uuid", + "Text", + "Timestamptz" + ] + } + }, + "query": "\n INSERT INTO user_passwords (user_password_id, user_id, hashed_password, created_at)\n VALUES ($1, $2, $3, $4)\n " + }, + "4f8ec19f3f1bfe0268fe102a24e5a9fa542e77eccbebdce65e6deb1c197adf36": { + "describe": { + "columns": [ + { + "name": "oauth2_access_token_id", + "ordinal": 0, + "type_info": "Uuid" + }, + { + "name": "oauth2_access_token", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "oauth2_access_token_created_at", + "ordinal": 2, + "type_info": "Timestamptz" + }, + { + "name": "oauth2_access_token_expires_at", + "ordinal": 3, + "type_info": "Timestamptz" + }, + { + "name": "oauth2_session_id!", + "ordinal": 4, + "type_info": "Uuid" + }, + { + "name": "oauth2_client_id!", + "ordinal": 5, + "type_info": "Uuid" + }, + { + "name": "scope!", + "ordinal": 6, + "type_info": "Text" + }, + { + "name": "user_session_id!", + "ordinal": 7, + "type_info": "Uuid" + }, + { + "name": "user_session_created_at!", + "ordinal": 8, + "type_info": "Timestamptz" + }, + { + "name": "user_id!", + "ordinal": 9, + "type_info": "Uuid" + }, + { + "name": "user_username!", + "ordinal": 10, + "type_info": "Text" + }, + { + "name": "user_session_last_authentication_id?", + "ordinal": 11, + "type_info": "Uuid" + }, + { + "name": "user_session_last_authentication_created_at?", + "ordinal": 12, + "type_info": "Timestamptz" + }, + { + "name": "user_email_id?", + "ordinal": 13, + "type_info": "Uuid" + }, + { + "name": "user_email?", + "ordinal": 14, + "type_info": "Text" + }, + { + "name": "user_email_created_at?", + "ordinal": 15, + "type_info": "Timestamptz" + }, + { + "name": "user_email_confirmed_at?", + "ordinal": 16, + "type_info": "Timestamptz" + } + ], + "nullable": [ + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + true + ], + "parameters": { + "Left": [ + "Text" + ] + } + }, + "query": "\n SELECT\n at.oauth2_access_token_id,\n at.access_token AS \"oauth2_access_token\",\n at.created_at AS \"oauth2_access_token_created_at\",\n at.expires_at AS \"oauth2_access_token_expires_at\",\n os.oauth2_session_id AS \"oauth2_session_id!\",\n os.oauth2_client_id AS \"oauth2_client_id!\",\n os.scope AS \"scope!\",\n us.user_session_id AS \"user_session_id!\",\n us.created_at AS \"user_session_created_at!\",\n u.user_id AS \"user_id!\",\n u.username AS \"user_username!\",\n usa.user_session_authentication_id AS \"user_session_last_authentication_id?\",\n usa.created_at AS \"user_session_last_authentication_created_at?\",\n ue.user_email_id AS \"user_email_id?\",\n ue.email AS \"user_email?\",\n ue.created_at AS \"user_email_created_at?\",\n ue.confirmed_at AS \"user_email_confirmed_at?\"\n\n FROM oauth2_access_tokens at\n INNER JOIN oauth2_sessions os\n USING (oauth2_session_id)\n INNER JOIN user_sessions us\n USING (user_session_id)\n INNER JOIN users u\n USING (user_id)\n LEFT JOIN user_session_authentications usa\n USING (user_session_id)\n LEFT JOIN user_emails ue\n ON ue.user_email_id = u.primary_user_email_id\n\n WHERE at.access_token = $1\n AND at.revoked_at IS NULL\n AND os.finished_at IS NULL\n\n ORDER BY usa.created_at DESC\n LIMIT 1\n " }, "51158bfcaa1a8d8e051bffe7c5ba0369bf53fb162f7622626054e89e68fc07bd": { "describe": { @@ -400,195 +830,82 @@ ], "parameters": { "Left": [ - "Int8", - "Int8" + "Uuid", + "Uuid" ] } }, "query": "\n SELECT scope_token\n FROM oauth2_consents\n WHERE user_id = $1 AND oauth2_client_id = $2\n " }, - "51d148123a4a4254f3fc16574a7136ed015808d5e967f00431f1f9ed12f72c93": { - "describe": { - "columns": [ - { - "name": "id", - "ordinal": 0, - "type_info": "Int8" - }, - { - "name": "created_at", - "ordinal": 1, - "type_info": "Timestamptz" - } - ], - "nullable": [ - false, - false - ], - "parameters": { - "Left": [ - "Int8", - "Text" - ] - } - }, - "query": "\n INSERT INTO compat_sessions (user_id, device_id)\n VALUES ($1, $2)\n RETURNING id, created_at\n " - }, - "581243a7f0c033548cc9644e0c60855ecb8bfefe51779eb135dd7547b886de79": { + "60d039442cfa57e187602c0ff5e386e32fb774b5ad2d2f2c616040819b76873e": { "describe": { "columns": [], "nullable": [], "parameters": { "Left": [ - "Int8" + "Uuid", + "Uuid", + "Timestamptz" ] } }, - "query": "\n UPDATE oauth2_sessions\n SET ended_at = NOW()\n WHERE id = $1\n " + "query": "\n UPDATE compat_sso_logins\n SET\n compat_session_id = $2,\n fulfilled_at = $3\n WHERE\n compat_sso_login_id = $1\n " }, - "59e8a5de682642883a9b9fc1b522736fa4397f0a0c97074f2c8908e5956c0166": { + "613aaff56efa9081dcaabb72219ed6c067d4e38db92e472f40ddd21dc2bddf82": { "describe": { "columns": [ { - "name": "id", + "name": "user_session_id", "ordinal": 0, - "type_info": "Int8" + "type_info": "Uuid" + }, + { + "name": "user_id", + "ordinal": 1, + "type_info": "Uuid" + }, + { + "name": "username", + "ordinal": 2, + "type_info": "Text" }, { "name": "created_at", - "ordinal": 1, - "type_info": "Timestamptz" - } - ], - "nullable": [ - false, - false - ], - "parameters": { - "Left": [ - "Int8", - "Text", - "Int4" - ] - } - }, - "query": "\n INSERT INTO oauth2_access_tokens\n (oauth2_session_id, token, expires_after)\n VALUES\n ($1, $2, $3)\n RETURNING\n id, created_at\n " - }, - "5a85be31a95fb0028e9ebc465b543b8daf4a65e80a35dcf8c710068cd9633df8": { - "describe": { - "columns": [ - { - "name": "refresh_token_id", - "ordinal": 0, - "type_info": "Int8" - }, - { - "name": "refresh_token", - "ordinal": 1, - "type_info": "Text" - }, - { - "name": "refresh_token_created_at", - "ordinal": 2, - "type_info": "Timestamptz" - }, - { - "name": "access_token_id?", "ordinal": 3, - "type_info": "Int8" + "type_info": "Timestamptz" }, { - "name": "access_token?", + "name": "last_authentication_id?", "ordinal": 4, - "type_info": "Text" + "type_info": "Uuid" }, { - "name": "access_token_expires_after?", + "name": "last_authd_at?", "ordinal": 5, - "type_info": "Int4" - }, - { - "name": "access_token_created_at?", - "ordinal": 6, - "type_info": "Timestamptz" - }, - { - "name": "session_id!", - "ordinal": 7, - "type_info": "Int8" - }, - { - "name": "oauth2_client_id!", - "ordinal": 8, - "type_info": "Int8" - }, - { - "name": "scope!", - "ordinal": 9, - "type_info": "Text" - }, - { - "name": "user_session_id!", - "ordinal": 10, - "type_info": "Int8" - }, - { - "name": "user_session_created_at!", - "ordinal": 11, - "type_info": "Timestamptz" - }, - { - "name": "user_id!", - "ordinal": 12, - "type_info": "Int8" - }, - { - "name": "user_username!", - "ordinal": 13, - "type_info": "Text" - }, - { - "name": "user_session_last_authentication_id?", - "ordinal": 14, - "type_info": "Int8" - }, - { - "name": "user_session_last_authentication_created_at?", - "ordinal": 15, "type_info": "Timestamptz" }, { "name": "user_email_id?", - "ordinal": 16, - "type_info": "Int8" + "ordinal": 6, + "type_info": "Uuid" }, { "name": "user_email?", - "ordinal": 17, + "ordinal": 7, "type_info": "Text" }, { "name": "user_email_created_at?", - "ordinal": 18, + "ordinal": 8, "type_info": "Timestamptz" }, { "name": "user_email_confirmed_at?", - "ordinal": 19, + "ordinal": 9, "type_info": "Timestamptz" } ], "nullable": [ - false, - false, - false, - false, - false, - false, - false, - false, - false, - false, false, false, false, @@ -602,156 +919,11 @@ ], "parameters": { "Left": [ - "Text" + "Uuid" ] } }, - "query": "\n SELECT\n rt.id AS refresh_token_id,\n rt.token AS refresh_token,\n rt.created_at AS refresh_token_created_at,\n at.id AS \"access_token_id?\",\n at.token AS \"access_token?\",\n at.expires_after AS \"access_token_expires_after?\",\n at.created_at AS \"access_token_created_at?\",\n os.id AS \"session_id!\",\n os.oauth2_client_id AS \"oauth2_client_id!\",\n os.scope AS \"scope!\",\n us.id AS \"user_session_id!\",\n us.created_at AS \"user_session_created_at!\",\n u.id AS \"user_id!\",\n u.username AS \"user_username!\",\n usa.id AS \"user_session_last_authentication_id?\",\n usa.created_at AS \"user_session_last_authentication_created_at?\",\n ue.id AS \"user_email_id?\",\n ue.email AS \"user_email?\",\n ue.created_at AS \"user_email_created_at?\",\n ue.confirmed_at AS \"user_email_confirmed_at?\"\n FROM oauth2_refresh_tokens rt\n LEFT JOIN oauth2_access_tokens at\n ON at.id = rt.oauth2_access_token_id\n INNER JOIN oauth2_sessions os\n ON os.id = rt.oauth2_session_id\n INNER JOIN user_sessions us\n ON us.id = os.user_session_id\n INNER JOIN users u\n ON u.id = us.user_id\n LEFT JOIN user_session_authentications usa\n ON usa.session_id = us.id\n LEFT JOIN user_emails ue\n ON ue.id = u.primary_email_id\n\n WHERE rt.token = $1\n AND rt.next_token_id IS NULL\n AND us.active\n AND os.ended_at IS NULL\n\n ORDER BY usa.created_at DESC\n LIMIT 1\n " - }, - "5d1a17b2ad6153217551ae31549ad9d62cc39d2f9a4e62a7ccb60fd91e0ac685": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Left": [] - } - }, - "query": "\n DELETE FROM oauth2_access_tokens\n WHERE created_at + (expires_after * INTERVAL '1 second') + INTERVAL '15 minutes' < now()\n " - }, - "5e4a73693e45ab55b6c166621fa2d033775762f589e18b889b5e41dbfaed1ca7": { - "describe": { - "columns": [ - { - "name": "id", - "ordinal": 0, - "type_info": "Int8" - } - ], - "nullable": [ - false - ], - "parameters": { - "Left": [ - "Text", - "Text", - "TextArray", - "Bool", - "Bool", - "TextArray", - "Text", - "Text", - "Text", - "Text", - "Text", - "Text", - "Jsonb", - "Text", - "Text", - "Text", - "Text", - "Text" - ] - } - }, - "query": "\n INSERT INTO oauth2_clients\n (client_id,\n encrypted_client_secret,\n response_types,\n grant_type_authorization_code,\n grant_type_refresh_token,\n contacts,\n client_name,\n logo_uri,\n client_uri,\n policy_uri,\n tos_uri,\n jwks_uri,\n jwks,\n id_token_signed_response_alg,\n userinfo_signed_response_alg,\n token_endpoint_auth_method,\n token_endpoint_auth_signing_alg,\n initiate_login_uri)\n VALUES\n ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18)\n RETURNING id\n " - }, - "63522dddec4f2218fdb099473a80411d7f1c19b3750e27bb1f087357588e40c9": { - "describe": { - "columns": [ - { - "name": "compat_access_token_id", - "ordinal": 0, - "type_info": "Int8" - }, - { - "name": "compat_access_token", - "ordinal": 1, - "type_info": "Text" - }, - { - "name": "compat_access_token_created_at", - "ordinal": 2, - "type_info": "Timestamptz" - }, - { - "name": "compat_access_token_expires_at", - "ordinal": 3, - "type_info": "Timestamptz" - }, - { - "name": "compat_session_id", - "ordinal": 4, - "type_info": "Int8" - }, - { - "name": "compat_session_created_at", - "ordinal": 5, - "type_info": "Timestamptz" - }, - { - "name": "compat_session_deleted_at", - "ordinal": 6, - "type_info": "Timestamptz" - }, - { - "name": "compat_session_device_id", - "ordinal": 7, - "type_info": "Text" - }, - { - "name": "user_id!", - "ordinal": 8, - "type_info": "Int8" - }, - { - "name": "user_username!", - "ordinal": 9, - "type_info": "Text" - }, - { - "name": "user_email_id?", - "ordinal": 10, - "type_info": "Int8" - }, - { - "name": "user_email?", - "ordinal": 11, - "type_info": "Text" - }, - { - "name": "user_email_created_at?", - "ordinal": 12, - "type_info": "Timestamptz" - }, - { - "name": "user_email_confirmed_at?", - "ordinal": 13, - "type_info": "Timestamptz" - } - ], - "nullable": [ - false, - false, - false, - true, - false, - false, - true, - false, - false, - false, - false, - false, - false, - true - ], - "parameters": { - "Left": [ - "Text" - ] - } - }, - "query": "\n SELECT\n ct.id AS \"compat_access_token_id\",\n ct.token AS \"compat_access_token\",\n ct.created_at AS \"compat_access_token_created_at\",\n ct.expires_at AS \"compat_access_token_expires_at\",\n cs.id AS \"compat_session_id\",\n cs.created_at AS \"compat_session_created_at\",\n cs.deleted_at AS \"compat_session_deleted_at\",\n cs.device_id AS \"compat_session_device_id\",\n u.id AS \"user_id!\",\n u.username AS \"user_username!\",\n ue.id AS \"user_email_id?\",\n ue.email AS \"user_email?\",\n ue.created_at AS \"user_email_created_at?\",\n ue.confirmed_at AS \"user_email_confirmed_at?\"\n\n FROM compat_access_tokens ct\n INNER JOIN compat_sessions cs\n ON cs.id = ct.compat_session_id\n INNER JOIN users u\n ON u.id = cs.user_id\n LEFT JOIN user_emails ue\n ON ue.id = u.primary_email_id\n\n WHERE ct.token = $1\n AND (ct.expires_at IS NULL OR ct.expires_at > NOW())\n AND cs.deleted_at IS NULL\n " + "query": "\n SELECT\n s.user_session_id,\n u.user_id,\n u.username,\n s.created_at,\n a.user_session_authentication_id AS \"last_authentication_id?\",\n a.created_at AS \"last_authd_at?\",\n ue.user_email_id AS \"user_email_id?\",\n ue.email AS \"user_email?\",\n ue.created_at AS \"user_email_created_at?\",\n ue.confirmed_at AS \"user_email_confirmed_at?\"\n FROM user_sessions s\n INNER JOIN users u \n USING (user_id)\n LEFT JOIN user_session_authentications a\n USING (user_session_id)\n LEFT JOIN user_emails ue\n ON ue.user_email_id = u.primary_user_email_id\n WHERE s.user_session_id = $1 AND s.finished_at IS NULL\n ORDER BY a.created_at DESC\n LIMIT 1\n " }, "647a2a5bbde39d0ed3931d0287b468bc7dedf6171e1dc6171a5d9f079b9ed0fa": { "describe": { @@ -767,125 +939,455 @@ ], "parameters": { "Left": [ - "Int8" + "Uuid" ] } }, "query": "\n SELECT up.hashed_password\n FROM user_passwords up\n WHERE up.user_id = $1\n ORDER BY up.created_at DESC\n LIMIT 1\n " }, - "685ab6c4742944fc87b4c72316b02b02e40c94ba624e8e4aade4ecfc436e7f96": { + "64a56818dd16ac6368efe3e34196a77b7feda1eb87b696e0063a51bf50e499e5": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Timestamptz", + "Uuid" + ] + } + }, + "query": "\n UPDATE user_sessions\n SET finished_at = $1\n WHERE user_session_id = $2\n " + }, + "6bf0da5ba3dd07b499193a2e0ddeea6e712f9df8f7f28874ff56a952a9f10e54": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Uuid", + "Timestamptz" + ] + } + }, + "query": "\n UPDATE oauth2_access_tokens\n SET revoked_at = $2\n WHERE oauth2_access_token_id = $1\n " + }, + "75a16693cabdf57012f741e789b19d0a0f96fcd1e41bb2af92f2991b722cc9f1": { "describe": { "columns": [ { - "name": "id", + "name": "oauth2_authorization_grant_id", "ordinal": 0, - "type_info": "Int8" + "type_info": "Uuid" }, { - "name": "client_id", + "name": "oauth2_authorization_grant_created_at", "ordinal": 1, - "type_info": "Text" + "type_info": "Timestamptz" }, { - "name": "encrypted_client_secret", + "name": "oauth2_authorization_grant_cancelled_at", "ordinal": 2, + "type_info": "Timestamptz" + }, + { + "name": "oauth2_authorization_grant_fulfilled_at", + "ordinal": 3, + "type_info": "Timestamptz" + }, + { + "name": "oauth2_authorization_grant_exchanged_at", + "ordinal": 4, + "type_info": "Timestamptz" + }, + { + "name": "oauth2_authorization_grant_scope", + "ordinal": 5, "type_info": "Text" }, { - "name": "redirect_uris!", - "ordinal": 3, - "type_info": "TextArray" - }, - { - "name": "response_types", - "ordinal": 4, - "type_info": "TextArray" - }, - { - "name": "grant_type_authorization_code", - "ordinal": 5, - "type_info": "Bool" - }, - { - "name": "grant_type_refresh_token", + "name": "oauth2_authorization_grant_state", "ordinal": 6, - "type_info": "Bool" + "type_info": "Text" }, { - "name": "contacts", + "name": "oauth2_authorization_grant_redirect_uri", "ordinal": 7, - "type_info": "TextArray" + "type_info": "Text" }, { - "name": "client_name", + "name": "oauth2_authorization_grant_response_mode", "ordinal": 8, "type_info": "Text" }, { - "name": "logo_uri", + "name": "oauth2_authorization_grant_nonce", "ordinal": 9, "type_info": "Text" }, { - "name": "client_uri", + "name": "oauth2_authorization_grant_max_age", "ordinal": 10, - "type_info": "Text" + "type_info": "Int4" }, { - "name": "policy_uri", + "name": "oauth2_client_id", "ordinal": 11, - "type_info": "Text" + "type_info": "Uuid" }, { - "name": "tos_uri", + "name": "oauth2_authorization_grant_code", "ordinal": 12, "type_info": "Text" }, { - "name": "jwks_uri", + "name": "oauth2_authorization_grant_response_type_code", "ordinal": 13, - "type_info": "Text" + "type_info": "Bool" }, { - "name": "jwks", + "name": "oauth2_authorization_grant_response_type_id_token", "ordinal": 14, - "type_info": "Jsonb" + "type_info": "Bool" }, { - "name": "id_token_signed_response_alg", + "name": "oauth2_authorization_grant_code_challenge", "ordinal": 15, "type_info": "Text" }, { - "name": "userinfo_signed_response_alg", + "name": "oauth2_authorization_grant_code_challenge_method", "ordinal": 16, "type_info": "Text" }, { - "name": "token_endpoint_auth_method", + "name": "oauth2_authorization_grant_requires_consent", "ordinal": 17, - "type_info": "Text" + "type_info": "Bool" }, { - "name": "token_endpoint_auth_signing_alg", + "name": "oauth2_session_id?", "ordinal": 18, + "type_info": "Uuid" + }, + { + "name": "user_session_id?", + "ordinal": 19, + "type_info": "Uuid" + }, + { + "name": "user_session_created_at?", + "ordinal": 20, + "type_info": "Timestamptz" + }, + { + "name": "user_id?", + "ordinal": 21, + "type_info": "Uuid" + }, + { + "name": "user_username?", + "ordinal": 22, "type_info": "Text" }, { - "name": "initiate_login_uri", - "ordinal": 19, + "name": "user_session_last_authentication_id?", + "ordinal": 23, + "type_info": "Uuid" + }, + { + "name": "user_session_last_authentication_created_at?", + "ordinal": 24, + "type_info": "Timestamptz" + }, + { + "name": "user_email_id?", + "ordinal": 25, + "type_info": "Uuid" + }, + { + "name": "user_email?", + "ordinal": 26, "type_info": "Text" + }, + { + "name": "user_email_created_at?", + "ordinal": 27, + "type_info": "Timestamptz" + }, + { + "name": "user_email_confirmed_at?", + "ordinal": 28, + "type_info": "Timestamptz" } ], "nullable": [ false, + false, + true, + true, + true, + false, + true, + false, + false, + true, + true, + false, + true, + false, + false, + true, + true, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + true + ], + "parameters": { + "Left": [ + "Uuid" + ] + } + }, + "query": "\n SELECT\n og.oauth2_authorization_grant_id,\n og.created_at AS oauth2_authorization_grant_created_at,\n og.cancelled_at AS oauth2_authorization_grant_cancelled_at,\n og.fulfilled_at AS oauth2_authorization_grant_fulfilled_at,\n og.exchanged_at AS oauth2_authorization_grant_exchanged_at,\n og.scope AS oauth2_authorization_grant_scope,\n og.state AS oauth2_authorization_grant_state,\n og.redirect_uri AS oauth2_authorization_grant_redirect_uri,\n og.response_mode AS oauth2_authorization_grant_response_mode,\n og.nonce AS oauth2_authorization_grant_nonce,\n og.max_age AS oauth2_authorization_grant_max_age,\n og.oauth2_client_id AS oauth2_client_id,\n og.authorization_code AS oauth2_authorization_grant_code,\n og.response_type_code AS oauth2_authorization_grant_response_type_code,\n og.response_type_id_token AS oauth2_authorization_grant_response_type_id_token,\n og.code_challenge AS oauth2_authorization_grant_code_challenge,\n og.code_challenge_method AS oauth2_authorization_grant_code_challenge_method,\n og.requires_consent AS oauth2_authorization_grant_requires_consent,\n os.oauth2_session_id AS \"oauth2_session_id?\",\n us.user_session_id AS \"user_session_id?\",\n us.created_at AS \"user_session_created_at?\",\n u.user_id AS \"user_id?\",\n u.username AS \"user_username?\",\n usa.user_session_authentication_id AS \"user_session_last_authentication_id?\",\n usa.created_at AS \"user_session_last_authentication_created_at?\",\n ue.user_email_id AS \"user_email_id?\",\n ue.email AS \"user_email?\",\n ue.created_at AS \"user_email_created_at?\",\n ue.confirmed_at AS \"user_email_confirmed_at?\"\n FROM\n oauth2_authorization_grants og\n LEFT JOIN oauth2_sessions os\n USING (oauth2_session_id)\n LEFT JOIN user_sessions us\n USING (user_session_id)\n LEFT JOIN users u\n USING (user_id)\n LEFT JOIN user_session_authentications usa\n USING (user_session_id)\n LEFT JOIN user_emails ue\n ON ue.user_email_id = u.primary_user_email_id\n\n WHERE og.oauth2_authorization_grant_id = $1\n\n ORDER BY usa.created_at DESC\n LIMIT 1\n " + }, + "7cf5ae665b15ba78b01bb1dfa304150a89fd7203f4ee15b0753cb2143049a3dc": { + "describe": { + "columns": [ + { + "name": "oauth2_refresh_token_id", + "ordinal": 0, + "type_info": "Uuid" + }, + { + "name": "oauth2_refresh_token", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "oauth2_refresh_token_created_at", + "ordinal": 2, + "type_info": "Timestamptz" + }, + { + "name": "oauth2_access_token_id?", + "ordinal": 3, + "type_info": "Uuid" + }, + { + "name": "oauth2_access_token?", + "ordinal": 4, + "type_info": "Text" + }, + { + "name": "oauth2_access_token_created_at?", + "ordinal": 5, + "type_info": "Timestamptz" + }, + { + "name": "oauth2_access_token_expires_at?", + "ordinal": 6, + "type_info": "Timestamptz" + }, + { + "name": "oauth2_session_id!", + "ordinal": 7, + "type_info": "Uuid" + }, + { + "name": "oauth2_client_id!", + "ordinal": 8, + "type_info": "Uuid" + }, + { + "name": "oauth2_session_scope!", + "ordinal": 9, + "type_info": "Text" + }, + { + "name": "user_session_id!", + "ordinal": 10, + "type_info": "Uuid" + }, + { + "name": "user_session_created_at!", + "ordinal": 11, + "type_info": "Timestamptz" + }, + { + "name": "user_id!", + "ordinal": 12, + "type_info": "Uuid" + }, + { + "name": "user_username!", + "ordinal": 13, + "type_info": "Text" + }, + { + "name": "user_session_last_authentication_id?", + "ordinal": 14, + "type_info": "Uuid" + }, + { + "name": "user_session_last_authentication_created_at?", + "ordinal": 15, + "type_info": "Timestamptz" + }, + { + "name": "user_email_id?", + "ordinal": 16, + "type_info": "Uuid" + }, + { + "name": "user_email?", + "ordinal": 17, + "type_info": "Text" + }, + { + "name": "user_email_created_at?", + "ordinal": 18, + "type_info": "Timestamptz" + }, + { + "name": "user_email_confirmed_at?", + "ordinal": 19, + "type_info": "Timestamptz" + } + ], + "nullable": [ + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + true + ], + "parameters": { + "Left": [ + "Text" + ] + } + }, + "query": "\n SELECT\n rt.oauth2_refresh_token_id,\n rt.refresh_token AS oauth2_refresh_token,\n rt.created_at AS oauth2_refresh_token_created_at,\n at.oauth2_access_token_id AS \"oauth2_access_token_id?\",\n at.access_token AS \"oauth2_access_token?\",\n at.created_at AS \"oauth2_access_token_created_at?\",\n at.expires_at AS \"oauth2_access_token_expires_at?\",\n os.oauth2_session_id AS \"oauth2_session_id!\",\n os.oauth2_client_id AS \"oauth2_client_id!\",\n os.scope AS \"oauth2_session_scope!\",\n us.user_session_id AS \"user_session_id!\",\n us.created_at AS \"user_session_created_at!\",\n u.user_id AS \"user_id!\",\n u.username AS \"user_username!\",\n usa.user_session_authentication_id AS \"user_session_last_authentication_id?\",\n usa.created_at AS \"user_session_last_authentication_created_at?\",\n ue.user_email_id AS \"user_email_id?\",\n ue.email AS \"user_email?\",\n ue.created_at AS \"user_email_created_at?\",\n ue.confirmed_at AS \"user_email_confirmed_at?\"\n FROM oauth2_refresh_tokens rt\n INNER JOIN oauth2_sessions os\n USING (oauth2_session_id)\n LEFT JOIN oauth2_access_tokens at\n USING (oauth2_access_token_id)\n INNER JOIN user_sessions us\n USING (user_session_id)\n INNER JOIN users u\n USING (user_id)\n LEFT JOIN user_session_authentications usa\n USING (user_session_id)\n LEFT JOIN user_emails ue\n ON ue.user_email_id = u.primary_user_email_id\n\n WHERE rt.refresh_token = $1\n AND rt.consumed_at IS NULL\n AND rt.revoked_at IS NULL\n AND us.finished_at IS NULL\n AND os.finished_at IS NULL\n\n ORDER BY usa.created_at DESC\n LIMIT 1\n " + }, + "7d9fe18c9fd346d51dac4552a1fee5e6a5da8512c5640f5b68aa0af8301a9611": { + "describe": { + "columns": [ + { + "name": "oauth2_client_id", + "ordinal": 0, + "type_info": "Uuid" + }, + { + "name": "encrypted_client_secret", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "redirect_uris!", + "ordinal": 2, + "type_info": "TextArray" + }, + { + "name": "grant_type_authorization_code", + "ordinal": 3, + "type_info": "Bool" + }, + { + "name": "grant_type_refresh_token", + "ordinal": 4, + "type_info": "Bool" + }, + { + "name": "client_name", + "ordinal": 5, + "type_info": "Text" + }, + { + "name": "logo_uri", + "ordinal": 6, + "type_info": "Text" + }, + { + "name": "client_uri", + "ordinal": 7, + "type_info": "Text" + }, + { + "name": "policy_uri", + "ordinal": 8, + "type_info": "Text" + }, + { + "name": "tos_uri", + "ordinal": 9, + "type_info": "Text" + }, + { + "name": "jwks_uri", + "ordinal": 10, + "type_info": "Text" + }, + { + "name": "jwks", + "ordinal": 11, + "type_info": "Jsonb" + }, + { + "name": "id_token_signed_response_alg", + "ordinal": 12, + "type_info": "Text" + }, + { + "name": "userinfo_signed_response_alg", + "ordinal": 13, + "type_info": "Text" + }, + { + "name": "token_endpoint_auth_method", + "ordinal": 14, + "type_info": "Text" + }, + { + "name": "token_endpoint_auth_signing_alg", + "ordinal": 15, + "type_info": "Text" + }, + { + "name": "initiate_login_uri", + "ordinal": 16, + "type_info": "Text" + } + ], + "nullable": [ false, true, null, false, false, - false, - false, true, true, true, @@ -899,21 +1401,147 @@ true, true ], + "parameters": { + "Left": [ + "Uuid" + ] + } + }, + "query": "\n SELECT\n c.oauth2_client_id,\n c.encrypted_client_secret,\n ARRAY(\n SELECT redirect_uri \n FROM oauth2_client_redirect_uris r \n WHERE r.oauth2_client_id = c.oauth2_client_id\n ) AS \"redirect_uris!\",\n c.grant_type_authorization_code,\n c.grant_type_refresh_token,\n c.client_name,\n c.logo_uri,\n c.client_uri,\n c.policy_uri,\n c.tos_uri,\n c.jwks_uri,\n c.jwks,\n c.id_token_signed_response_alg,\n c.userinfo_signed_response_alg,\n c.token_endpoint_auth_method,\n c.token_endpoint_auth_signing_alg,\n c.initiate_login_uri\n FROM oauth2_clients c\n\n WHERE c.oauth2_client_id = $1\n " + }, + "7e3247e35ecf5335f0656c53bcde27264a9efb8dccb6246344950614f487dcaf": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Uuid", + "Timestamptz" + ] + } + }, + "query": "\n UPDATE compat_access_tokens\n SET expires_at = $2\n WHERE compat_access_token_id = $1\n " + }, + "819d6472e5bcbd83a83f3a7680e8dc88e77f3970d6beddcf54e8416c880bd496": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Uuid" + ] + } + }, + "query": "\n UPDATE users\n SET primary_user_email_id = user_emails.user_email_id\n FROM user_emails\n WHERE user_emails.user_email_id = $1\n AND users.user_id = user_emails.user_id\n " + }, + "874e677f82c221c5bb621c12f293bcef4e70c68c87ec003fcd475bcb994b5a4c": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Uuid", + "Timestamptz" + ] + } + }, + "query": "\n UPDATE oauth2_refresh_tokens\n SET consumed_at = $2\n WHERE oauth2_refresh_token_id = $1\n " + }, + "8bafcb5ab5a7cabccededbe60b1cc438fe3d7ba4c86e2d7ea26aea80d92a4af8": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Timestamptz" + ] + } + }, + "query": "\n DELETE FROM oauth2_access_tokens\n WHERE expires_at < $1 \n " + }, + "97a90983e07a1e18eeacd379484c9b5317e0d90c23bc5d12ba98269497a47e00": { + "describe": { + "columns": [ + { + "name": "user_id", + "ordinal": 0, + "type_info": "Uuid" + }, + { + "name": "user_username", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "user_email_id?", + "ordinal": 2, + "type_info": "Uuid" + }, + { + "name": "user_email?", + "ordinal": 3, + "type_info": "Text" + }, + { + "name": "user_email_created_at?", + "ordinal": 4, + "type_info": "Timestamptz" + }, + { + "name": "user_email_confirmed_at?", + "ordinal": 5, + "type_info": "Timestamptz" + } + ], + "nullable": [ + false, + false, + false, + false, + false, + true + ], "parameters": { "Left": [ "Text" ] } }, - "query": "\n SELECT\n c.id,\n c.client_id,\n c.encrypted_client_secret,\n ARRAY(SELECT redirect_uri FROM oauth2_client_redirect_uris r WHERE r.oauth2_client_id = c.id) AS \"redirect_uris!\",\n c.response_types,\n c.grant_type_authorization_code,\n c.grant_type_refresh_token,\n c.contacts,\n c.client_name,\n c.logo_uri,\n c.client_uri,\n c.policy_uri,\n c.tos_uri,\n c.jwks_uri,\n c.jwks,\n c.id_token_signed_response_alg,\n c.userinfo_signed_response_alg,\n c.token_endpoint_auth_method,\n c.token_endpoint_auth_signing_alg,\n c.initiate_login_uri\n FROM oauth2_clients c\n\n WHERE c.client_id = $1\n " + "query": "\n SELECT \n u.user_id,\n u.username AS user_username,\n ue.user_email_id AS \"user_email_id?\",\n ue.email AS \"user_email?\",\n ue.created_at AS \"user_email_created_at?\",\n ue.confirmed_at AS \"user_email_confirmed_at?\"\n FROM users u\n\n LEFT JOIN user_emails ue\n USING (user_id)\n\n WHERE u.username = $1\n " }, - "6da88febe6d8e45787cdd609dcea5f51dc601f4dffb07dd4c5d699c7d4c5b2d1": { + "99f5f9eb0adc5ec120ed8194cbf6a8545155bef09e6d94d92fb67fd1b14d4f28": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Uuid", + "Timestamptz" + ] + } + }, + "query": "\n UPDATE compat_refresh_tokens\n SET consumed_at = $2\n WHERE compat_refresh_token_id = $1\n " + }, + "9c1ef3114bfe22884d893bb11dc6054421c28cce4bd828cfe6a4ad46c062481a": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Uuid", + "Timestamptz" + ] + } + }, + "query": "\n UPDATE oauth2_sessions\n SET finished_at = $2\n WHERE oauth2_session_id = $1\n " + }, + "9dbaaf32ecf2ba8ff65d47dbe871c21ec80675431a890dfa21c64ccb0a664326": { "describe": { "columns": [ { "name": "user_email_id", "ordinal": 0, - "type_info": "Int8" + "type_info": "Uuid" }, { "name": "user_email", @@ -939,706 +1567,44 @@ ], "parameters": { "Left": [ - "Int8", - "Text" + "Uuid", + "Uuid" ] } }, - "query": "\n INSERT INTO user_emails (user_id, email)\n VALUES ($1, $2)\n RETURNING \n id AS user_email_id,\n email AS user_email,\n created_at AS user_email_created_at,\n confirmed_at AS user_email_confirmed_at\n " + "query": "\n SELECT \n ue.user_email_id,\n ue.email AS \"user_email\",\n ue.created_at AS \"user_email_created_at\",\n ue.confirmed_at AS \"user_email_confirmed_at\"\n FROM user_emails ue\n\n WHERE ue.user_id = $1\n AND ue.user_email_id = $2\n " }, - "703850ba4e001d53776d77a64cbc1ee6feb61485ce41aff1103251f9b3778128": { + "9edf5e8a3e00a7cdd8e55b97105df7831ee580096299df4bd6c1ed7c96b95e83": { "describe": { "columns": [ { - "name": "fulfilled_at!: DateTime", + "name": "count!", "ordinal": 0, - "type_info": "Timestamptz" + "type_info": "Int8" } ], "nullable": [ - true + null ], "parameters": { "Left": [ - "Int8", - "Int8" + "Uuid" ] } }, - "query": "\n UPDATE oauth2_authorization_grants AS og\n SET\n oauth2_session_id = os.id,\n fulfilled_at = os.created_at\n FROM oauth2_sessions os\n WHERE\n og.id = $1 AND os.id = $2\n RETURNING fulfilled_at AS \"fulfilled_at!: DateTime\"\n " + "query": "\n SELECT COUNT(*) as \"count!\"\n FROM user_sessions s\n WHERE s.user_id = $1 AND s.finished_at IS NULL\n " }, - "795ef686860689dd89ad7b23ea242fe7108bc1dc6db76e80b654fd5560f0c28f": { - "describe": { - "columns": [ - { - "name": "id", - "ordinal": 0, - "type_info": "Int8" - }, - { - "name": "client_id", - "ordinal": 1, - "type_info": "Text" - }, - { - "name": "encrypted_client_secret", - "ordinal": 2, - "type_info": "Text" - }, - { - "name": "redirect_uris!", - "ordinal": 3, - "type_info": "TextArray" - }, - { - "name": "response_types", - "ordinal": 4, - "type_info": "TextArray" - }, - { - "name": "grant_type_authorization_code", - "ordinal": 5, - "type_info": "Bool" - }, - { - "name": "grant_type_refresh_token", - "ordinal": 6, - "type_info": "Bool" - }, - { - "name": "contacts", - "ordinal": 7, - "type_info": "TextArray" - }, - { - "name": "client_name", - "ordinal": 8, - "type_info": "Text" - }, - { - "name": "logo_uri", - "ordinal": 9, - "type_info": "Text" - }, - { - "name": "client_uri", - "ordinal": 10, - "type_info": "Text" - }, - { - "name": "policy_uri", - "ordinal": 11, - "type_info": "Text" - }, - { - "name": "tos_uri", - "ordinal": 12, - "type_info": "Text" - }, - { - "name": "jwks_uri", - "ordinal": 13, - "type_info": "Text" - }, - { - "name": "jwks", - "ordinal": 14, - "type_info": "Jsonb" - }, - { - "name": "id_token_signed_response_alg", - "ordinal": 15, - "type_info": "Text" - }, - { - "name": "userinfo_signed_response_alg", - "ordinal": 16, - "type_info": "Text" - }, - { - "name": "token_endpoint_auth_method", - "ordinal": 17, - "type_info": "Text" - }, - { - "name": "token_endpoint_auth_signing_alg", - "ordinal": 18, - "type_info": "Text" - }, - { - "name": "initiate_login_uri", - "ordinal": 19, - "type_info": "Text" - } - ], - "nullable": [ - false, - false, - true, - null, - false, - false, - false, - false, - true, - true, - true, - true, - true, - true, - true, - true, - true, - true, - true, - true - ], - "parameters": { - "Left": [ - "Int8" - ] - } - }, - "query": "\n SELECT\n c.id,\n c.client_id,\n c.encrypted_client_secret,\n ARRAY(SELECT redirect_uri FROM oauth2_client_redirect_uris r WHERE r.oauth2_client_id = c.id) AS \"redirect_uris!\",\n c.response_types,\n c.grant_type_authorization_code,\n c.grant_type_refresh_token,\n c.contacts,\n c.client_name,\n c.logo_uri,\n c.client_uri,\n c.policy_uri,\n c.tos_uri,\n c.jwks_uri,\n c.jwks,\n c.id_token_signed_response_alg,\n c.userinfo_signed_response_alg,\n c.token_endpoint_auth_method,\n c.token_endpoint_auth_signing_alg,\n c.initiate_login_uri\n FROM oauth2_clients c\n\n WHERE c.id = $1\n " - }, - "79c5cb47e7074be1f8d4684ab175ab8c3972b2a83f0abd2a47141fbd23793175": { - "describe": { - "columns": [ - { - "name": "id", - "ordinal": 0, - "type_info": "Int8" - }, - { - "name": "created_at", - "ordinal": 1, - "type_info": "Timestamptz" - } - ], - "nullable": [ - false, - false - ], - "parameters": { - "Left": [ - "Int8", - "Int8" - ] - } - }, - "query": "\n INSERT INTO oauth2_sessions\n (user_session_id, oauth2_client_id, scope)\n SELECT\n $1,\n og.oauth2_client_id,\n og.scope\n FROM\n oauth2_authorization_grants og\n WHERE\n og.id = $2\n RETURNING id, created_at\n " - }, - "7de9cfa6e90ba20f5b298ea387cf13a7e40d0f5b3eb903a80d06fbe33074d596": { - "describe": { - "columns": [ - { - "name": "confirmed_at", - "ordinal": 0, - "type_info": "Timestamptz" - } - ], - "nullable": [ - true - ], - "parameters": { - "Left": [ - "Int8" - ] - } - }, - "query": "\n UPDATE user_emails\n SET confirmed_at = NOW()\n WHERE id = $1\n RETURNING confirmed_at\n " - }, - "81c673253d86035037695e0d2a3e24bfc8bbb5603c9291f9b5bcac64b43e1c04": { - "describe": { - "columns": [ - { - "name": "compat_sso_login_id", - "ordinal": 0, - "type_info": "Int8" - }, - { - "name": "compat_sso_login_token", - "ordinal": 1, - "type_info": "Text" - }, - { - "name": "compat_sso_login_redirect_uri", - "ordinal": 2, - "type_info": "Text" - }, - { - "name": "compat_sso_login_created_at", - "ordinal": 3, - "type_info": "Timestamptz" - }, - { - "name": "compat_sso_login_fullfilled_at", - "ordinal": 4, - "type_info": "Timestamptz" - }, - { - "name": "compat_sso_login_exchanged_at", - "ordinal": 5, - "type_info": "Timestamptz" - }, - { - "name": "compat_session_id?", - "ordinal": 6, - "type_info": "Int8" - }, - { - "name": "compat_session_created_at?", - "ordinal": 7, - "type_info": "Timestamptz" - }, - { - "name": "compat_session_deleted_at?", - "ordinal": 8, - "type_info": "Timestamptz" - }, - { - "name": "compat_session_device_id?", - "ordinal": 9, - "type_info": "Text" - }, - { - "name": "user_id?", - "ordinal": 10, - "type_info": "Int8" - }, - { - "name": "user_username?", - "ordinal": 11, - "type_info": "Text" - }, - { - "name": "user_email_id?", - "ordinal": 12, - "type_info": "Int8" - }, - { - "name": "user_email?", - "ordinal": 13, - "type_info": "Text" - }, - { - "name": "user_email_created_at?", - "ordinal": 14, - "type_info": "Timestamptz" - }, - { - "name": "user_email_confirmed_at?", - "ordinal": 15, - "type_info": "Timestamptz" - } - ], - "nullable": [ - false, - false, - false, - false, - true, - true, - false, - false, - true, - false, - false, - false, - false, - false, - false, - true - ], - "parameters": { - "Left": [ - "Int8" - ] - } - }, - "query": "\n SELECT\n cl.id AS \"compat_sso_login_id\",\n cl.token AS \"compat_sso_login_token\",\n cl.redirect_uri AS \"compat_sso_login_redirect_uri\",\n cl.created_at AS \"compat_sso_login_created_at\",\n cl.fullfilled_at AS \"compat_sso_login_fullfilled_at\",\n cl.exchanged_at AS \"compat_sso_login_exchanged_at\",\n cs.id AS \"compat_session_id?\",\n cs.created_at AS \"compat_session_created_at?\",\n cs.deleted_at AS \"compat_session_deleted_at?\",\n cs.device_id AS \"compat_session_device_id?\",\n u.id AS \"user_id?\",\n u.username AS \"user_username?\",\n ue.id AS \"user_email_id?\",\n ue.email AS \"user_email?\",\n ue.created_at AS \"user_email_created_at?\",\n ue.confirmed_at AS \"user_email_confirmed_at?\"\n FROM compat_sso_logins cl\n LEFT JOIN compat_sessions cs\n ON cs.id = cl.compat_session_id\n LEFT JOIN users u\n ON u.id = cs.user_id\n LEFT JOIN user_emails ue\n ON ue.id = u.primary_email_id\n WHERE cl.id = $1\n " - }, - "841760e75d0a3a5b4bad5988cf35757d0812389dd765c05c7487dad07160173a": { - "describe": { - "columns": [ - { - "name": "grant_id", - "ordinal": 0, - "type_info": "Int8" - }, - { - "name": "grant_created_at", - "ordinal": 1, - "type_info": "Timestamptz" - }, - { - "name": "grant_cancelled_at", - "ordinal": 2, - "type_info": "Timestamptz" - }, - { - "name": "grant_fulfilled_at", - "ordinal": 3, - "type_info": "Timestamptz" - }, - { - "name": "grant_exchanged_at", - "ordinal": 4, - "type_info": "Timestamptz" - }, - { - "name": "grant_scope", - "ordinal": 5, - "type_info": "Text" - }, - { - "name": "grant_state", - "ordinal": 6, - "type_info": "Text" - }, - { - "name": "grant_redirect_uri", - "ordinal": 7, - "type_info": "Text" - }, - { - "name": "grant_response_mode", - "ordinal": 8, - "type_info": "Text" - }, - { - "name": "grant_nonce", - "ordinal": 9, - "type_info": "Text" - }, - { - "name": "grant_max_age", - "ordinal": 10, - "type_info": "Int4" - }, - { - "name": "grant_acr_values", - "ordinal": 11, - "type_info": "Text" - }, - { - "name": "oauth2_client_id", - "ordinal": 12, - "type_info": "Int8" - }, - { - "name": "grant_code", - "ordinal": 13, - "type_info": "Text" - }, - { - "name": "grant_response_type_code", - "ordinal": 14, - "type_info": "Bool" - }, - { - "name": "grant_response_type_id_token", - "ordinal": 15, - "type_info": "Bool" - }, - { - "name": "grant_code_challenge", - "ordinal": 16, - "type_info": "Text" - }, - { - "name": "grant_code_challenge_method", - "ordinal": 17, - "type_info": "Text" - }, - { - "name": "grant_requires_consent", - "ordinal": 18, - "type_info": "Bool" - }, - { - "name": "session_id?", - "ordinal": 19, - "type_info": "Int8" - }, - { - "name": "user_session_id?", - "ordinal": 20, - "type_info": "Int8" - }, - { - "name": "user_session_created_at?", - "ordinal": 21, - "type_info": "Timestamptz" - }, - { - "name": "user_id?", - "ordinal": 22, - "type_info": "Int8" - }, - { - "name": "user_username?", - "ordinal": 23, - "type_info": "Text" - }, - { - "name": "user_session_last_authentication_id?", - "ordinal": 24, - "type_info": "Int8" - }, - { - "name": "user_session_last_authentication_created_at?", - "ordinal": 25, - "type_info": "Timestamptz" - }, - { - "name": "user_email_id?", - "ordinal": 26, - "type_info": "Int8" - }, - { - "name": "user_email?", - "ordinal": 27, - "type_info": "Text" - }, - { - "name": "user_email_created_at?", - "ordinal": 28, - "type_info": "Timestamptz" - }, - { - "name": "user_email_confirmed_at?", - "ordinal": 29, - "type_info": "Timestamptz" - } - ], - "nullable": [ - false, - false, - true, - true, - true, - false, - true, - false, - false, - true, - true, - true, - false, - true, - false, - false, - true, - true, - false, - false, - false, - false, - false, - false, - false, - false, - false, - false, - false, - true - ], - "parameters": { - "Left": [ - "Int8" - ] - } - }, - "query": "\n SELECT\n og.id AS grant_id,\n og.created_at AS grant_created_at,\n og.cancelled_at AS grant_cancelled_at,\n og.fulfilled_at AS grant_fulfilled_at,\n og.exchanged_at AS grant_exchanged_at,\n og.scope AS grant_scope,\n og.state AS grant_state,\n og.redirect_uri AS grant_redirect_uri,\n og.response_mode AS grant_response_mode,\n og.nonce AS grant_nonce,\n og.max_age AS grant_max_age,\n og.acr_values AS grant_acr_values,\n og.oauth2_client_id AS oauth2_client_id,\n og.code AS grant_code,\n og.response_type_code AS grant_response_type_code,\n og.response_type_id_token AS grant_response_type_id_token,\n og.code_challenge AS grant_code_challenge,\n og.code_challenge_method AS grant_code_challenge_method,\n og.requires_consent AS grant_requires_consent,\n os.id AS \"session_id?\",\n us.id AS \"user_session_id?\",\n us.created_at AS \"user_session_created_at?\",\n u.id AS \"user_id?\",\n u.username AS \"user_username?\",\n usa.id AS \"user_session_last_authentication_id?\",\n usa.created_at AS \"user_session_last_authentication_created_at?\",\n ue.id AS \"user_email_id?\",\n ue.email AS \"user_email?\",\n ue.created_at AS \"user_email_created_at?\",\n ue.confirmed_at AS \"user_email_confirmed_at?\"\n FROM\n oauth2_authorization_grants og\n LEFT JOIN oauth2_sessions os\n ON os.id = og.oauth2_session_id\n LEFT JOIN user_sessions us\n ON us.id = os.user_session_id\n LEFT JOIN users u\n ON u.id = us.user_id\n LEFT JOIN user_session_authentications usa\n ON usa.session_id = us.id\n LEFT JOIN user_emails ue\n ON ue.id = u.primary_email_id\n\n WHERE og.id = $1\n\n ORDER BY usa.created_at DESC\n LIMIT 1\n " - }, - "860722788c244caf722d1941e4b83aa421fd179586f9a1c2342c539fcb6c6361": { - "describe": { - "columns": [ - { - "name": "compat_sso_login_id", - "ordinal": 0, - "type_info": "Int8" - }, - { - "name": "compat_sso_login_token", - "ordinal": 1, - "type_info": "Text" - }, - { - "name": "compat_sso_login_redirect_uri", - "ordinal": 2, - "type_info": "Text" - }, - { - "name": "compat_sso_login_created_at", - "ordinal": 3, - "type_info": "Timestamptz" - }, - { - "name": "compat_sso_login_fullfilled_at", - "ordinal": 4, - "type_info": "Timestamptz" - }, - { - "name": "compat_sso_login_exchanged_at", - "ordinal": 5, - "type_info": "Timestamptz" - }, - { - "name": "compat_session_id?", - "ordinal": 6, - "type_info": "Int8" - }, - { - "name": "compat_session_created_at?", - "ordinal": 7, - "type_info": "Timestamptz" - }, - { - "name": "compat_session_deleted_at?", - "ordinal": 8, - "type_info": "Timestamptz" - }, - { - "name": "compat_session_device_id?", - "ordinal": 9, - "type_info": "Text" - }, - { - "name": "user_id?", - "ordinal": 10, - "type_info": "Int8" - }, - { - "name": "user_username?", - "ordinal": 11, - "type_info": "Text" - }, - { - "name": "user_email_id?", - "ordinal": 12, - "type_info": "Int8" - }, - { - "name": "user_email?", - "ordinal": 13, - "type_info": "Text" - }, - { - "name": "user_email_created_at?", - "ordinal": 14, - "type_info": "Timestamptz" - }, - { - "name": "user_email_confirmed_at?", - "ordinal": 15, - "type_info": "Timestamptz" - } - ], - "nullable": [ - false, - false, - false, - false, - true, - true, - false, - false, - true, - false, - false, - false, - false, - false, - false, - true - ], - "parameters": { - "Left": [ - "Text" - ] - } - }, - "query": "\n SELECT\n cl.id AS \"compat_sso_login_id\",\n cl.token AS \"compat_sso_login_token\",\n cl.redirect_uri AS \"compat_sso_login_redirect_uri\",\n cl.created_at AS \"compat_sso_login_created_at\",\n cl.fullfilled_at AS \"compat_sso_login_fullfilled_at\",\n cl.exchanged_at AS \"compat_sso_login_exchanged_at\",\n cs.id AS \"compat_session_id?\",\n cs.created_at AS \"compat_session_created_at?\",\n cs.deleted_at AS \"compat_session_deleted_at?\",\n cs.device_id AS \"compat_session_device_id?\",\n u.id AS \"user_id?\",\n u.username AS \"user_username?\",\n ue.id AS \"user_email_id?\",\n ue.email AS \"user_email?\",\n ue.created_at AS \"user_email_created_at?\",\n ue.confirmed_at AS \"user_email_confirmed_at?\"\n FROM compat_sso_logins cl\n LEFT JOIN compat_sessions cs\n ON cs.id = cl.compat_session_id\n LEFT JOIN users u\n ON u.id = cs.user_id\n LEFT JOIN user_emails ue\n ON ue.id = u.primary_email_id\n WHERE cl.token = $1\n " - }, - "88ac8783bd5881c42eafd9cf87a16fe6031f3153fd6a8618e689694584aeb2de": { + "a5a7dad633396e087239d5629092e4a305908ffce9c2610db07372f719070546": { "describe": { "columns": [], "nullable": [], "parameters": { "Left": [ - "Int8" + "Uuid" ] } }, - "query": "\n DELETE FROM oauth2_access_tokens\n WHERE id = $1\n " - }, - "893b23b2385594f6c878d000b336d3c897adcffed35ee51d7dfea650b75aa0cf": { - "describe": { - "columns": [ - { - "name": "id", - "ordinal": 0, - "type_info": "Int8" - }, - { - "name": "created_at", - "ordinal": 1, - "type_info": "Timestamptz" - } - ], - "nullable": [ - false, - false - ], - "parameters": { - "Left": [ - "Int8", - "Text" - ] - } - }, - "query": "\n INSERT INTO compat_sessions (user_id, device_id)\n VALUES ($1, $2)\n RETURNING id, created_at\n " - }, - "929605e8e86ab15a34721b8cbbe29f1bff90102e5641bc49ded86f6539810c73": { - "describe": { - "columns": [ - { - "name": "id", - "ordinal": 0, - "type_info": "Int8" - }, - { - "name": "created_at", - "ordinal": 1, - "type_info": "Timestamptz" - } - ], - "nullable": [ - false, - false - ], - "parameters": { - "Left": [ - "Text", - "Text" - ] - } - }, - "query": "\n INSERT INTO compat_sso_logins (token, redirect_uri)\n VALUES ($1, $2)\n RETURNING id, created_at\n " - }, - "a09dfe1019110f2ec6eba0d35bafa467ab4b7980dd8b556826f03863f8edb0ab": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Left": [ - "Int8" - ] - } - }, - "query": "UPDATE user_sessions SET active = FALSE WHERE id = $1" + "query": "\n UPDATE oauth2_authorization_grants AS og\n SET\n requires_consent = 'f'\n WHERE\n og.oauth2_authorization_grant_id = $1\n " }, "a80c14ba82cfc29493048d9e9578ec5ca482c9228efc7c7212dae4fed86b8367": { "describe": { @@ -1646,48 +1612,108 @@ "nullable": [], "parameters": { "Left": [ - "Int8", + "Uuid", "TextArray" ] } }, "query": "\n INSERT INTO oauth2_client_redirect_uris (oauth2_client_id, redirect_uri)\n SELECT $1, uri FROM UNNEST($2::text[]) uri\n " }, - "aea289a04e151da235825305a5085bc6aa100fce139dbf10a2c1bed4867fc52a": { + "a8117b4dd167167b477fb4ebda52789e376defbdc67f3d9093aa06308b2f856e": { "describe": { "columns": [ { - "name": "user_id", + "name": "compat_sso_login_id", "ordinal": 0, - "type_info": "Int8" + "type_info": "Uuid" }, { - "name": "user_username", + "name": "compat_sso_login_token", "ordinal": 1, "type_info": "Text" }, { - "name": "user_email_id?", + "name": "compat_sso_login_redirect_uri", "ordinal": 2, - "type_info": "Int8" - }, - { - "name": "user_email?", - "ordinal": 3, "type_info": "Text" }, { - "name": "user_email_created_at?", + "name": "compat_sso_login_created_at", + "ordinal": 3, + "type_info": "Timestamptz" + }, + { + "name": "compat_sso_login_fulfilled_at", "ordinal": 4, "type_info": "Timestamptz" }, { - "name": "user_email_confirmed_at?", + "name": "compat_sso_login_exchanged_at", "ordinal": 5, "type_info": "Timestamptz" + }, + { + "name": "compat_session_id?", + "ordinal": 6, + "type_info": "Uuid" + }, + { + "name": "compat_session_created_at?", + "ordinal": 7, + "type_info": "Timestamptz" + }, + { + "name": "compat_session_finished_at?", + "ordinal": 8, + "type_info": "Timestamptz" + }, + { + "name": "compat_session_device_id?", + "ordinal": 9, + "type_info": "Text" + }, + { + "name": "user_id?", + "ordinal": 10, + "type_info": "Uuid" + }, + { + "name": "user_username?", + "ordinal": 11, + "type_info": "Text" + }, + { + "name": "user_email_id?", + "ordinal": 12, + "type_info": "Uuid" + }, + { + "name": "user_email?", + "ordinal": 13, + "type_info": "Text" + }, + { + "name": "user_email_created_at?", + "ordinal": 14, + "type_info": "Timestamptz" + }, + { + "name": "user_email_confirmed_at?", + "ordinal": 15, + "type_info": "Timestamptz" } ], "nullable": [ + false, + false, + false, + false, + true, + true, + false, + false, + true, + false, false, false, false, @@ -1701,19 +1727,7 @@ ] } }, - "query": "\n SELECT \n u.id AS user_id, \n u.username AS user_username,\n ue.id AS \"user_email_id?\",\n ue.email AS \"user_email?\",\n ue.created_at AS \"user_email_created_at?\",\n ue.confirmed_at AS \"user_email_confirmed_at?\"\n FROM users u\n\n LEFT JOIN user_emails ue\n ON ue.id = u.primary_email_id\n\n WHERE u.username = $1\n " - }, - "af3d36161bc60593ba991a9652efca1f19a9c4b291966dd41c1883b9c303673a": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Left": [ - "Text" - ] - } - }, - "query": "\n UPDATE compat_sessions\n SET deleted_at = NOW()\n FROM compat_access_tokens\n WHERE compat_access_tokens.token = $1\n AND compat_sessions.id = compat_access_tokens.id \n AND compat_sessions.deleted_at IS NULL\n " + "query": "\n SELECT\n cl.compat_sso_login_id,\n cl.login_token AS \"compat_sso_login_token\",\n cl.redirect_uri AS \"compat_sso_login_redirect_uri\",\n cl.created_at AS \"compat_sso_login_created_at\",\n cl.fulfilled_at AS \"compat_sso_login_fulfilled_at\",\n cl.exchanged_at AS \"compat_sso_login_exchanged_at\",\n cs.compat_session_id AS \"compat_session_id?\",\n cs.created_at AS \"compat_session_created_at?\",\n cs.finished_at AS \"compat_session_finished_at?\",\n cs.device_id AS \"compat_session_device_id?\",\n u.user_id AS \"user_id?\",\n u.username AS \"user_username?\",\n ue.user_email_id AS \"user_email_id?\",\n ue.email AS \"user_email?\",\n ue.created_at AS \"user_email_created_at?\",\n ue.confirmed_at AS \"user_email_confirmed_at?\"\n FROM compat_sso_logins cl\n LEFT JOIN compat_sessions cs\n USING (compat_session_id)\n LEFT JOIN users u\n USING (user_id)\n LEFT JOIN user_emails ue\n ON ue.user_email_id = u.primary_user_email_id\n WHERE cl.login_token = $1\n " }, "af77bad7259175464c5ad57f9662571c17b29552ebb70e4b6022584b41bdff0d": { "describe": { @@ -1735,31 +1749,124 @@ }, "query": "\n SELECT EXISTS(\n SELECT 1 FROM users WHERE username = $1\n ) AS \"exists!\"\n " }, - "b0fec01072df856ba9cd8be0ecf7a58dd4709a0efca4035a2c6f99c43d5a12be": { + "bc768c63a7737818967bc28560de714bbbd262bdf3ab73d297263bb73dcd9f5e": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Uuid", + "Uuid", + "Timestamptz", + "Uuid" + ] + } + }, + "query": "\n INSERT INTO oauth2_sessions\n (oauth2_session_id, user_session_id, oauth2_client_id, scope, created_at)\n SELECT\n $1,\n $2,\n og.oauth2_client_id,\n og.scope,\n $3\n FROM\n oauth2_authorization_grants og\n WHERE\n og.oauth2_authorization_grant_id = $4\n " + }, + "c52c911bf39ada298bfdc4526028f1b29fdcb6f557b288bb7ea2472b160c8698": { "describe": { "columns": [ { - "name": "user_email_id", + "name": "compat_refresh_token_id", "ordinal": 0, - "type_info": "Int8" + "type_info": "Uuid" }, { - "name": "user_email", + "name": "compat_refresh_token", "ordinal": 1, "type_info": "Text" }, { - "name": "user_email_created_at", + "name": "compat_refresh_token_created_at", "ordinal": 2, "type_info": "Timestamptz" }, { - "name": "user_email_confirmed_at", + "name": "compat_access_token_id", "ordinal": 3, + "type_info": "Uuid" + }, + { + "name": "compat_access_token", + "ordinal": 4, + "type_info": "Text" + }, + { + "name": "compat_access_token_created_at", + "ordinal": 5, + "type_info": "Timestamptz" + }, + { + "name": "compat_access_token_expires_at", + "ordinal": 6, + "type_info": "Timestamptz" + }, + { + "name": "compat_session_id", + "ordinal": 7, + "type_info": "Uuid" + }, + { + "name": "compat_session_created_at", + "ordinal": 8, + "type_info": "Timestamptz" + }, + { + "name": "compat_session_finished_at", + "ordinal": 9, + "type_info": "Timestamptz" + }, + { + "name": "compat_session_device_id", + "ordinal": 10, + "type_info": "Text" + }, + { + "name": "user_id", + "ordinal": 11, + "type_info": "Uuid" + }, + { + "name": "user_username!", + "ordinal": 12, + "type_info": "Text" + }, + { + "name": "user_email_id?", + "ordinal": 13, + "type_info": "Uuid" + }, + { + "name": "user_email?", + "ordinal": 14, + "type_info": "Text" + }, + { + "name": "user_email_created_at?", + "ordinal": 15, + "type_info": "Timestamptz" + }, + { + "name": "user_email_confirmed_at?", + "ordinal": 16, "type_info": "Timestamptz" } ], "nullable": [ + false, + false, + false, + false, + false, + false, + true, + false, + false, + true, + false, + false, + false, false, false, false, @@ -1767,34 +1874,20 @@ ], "parameters": { "Left": [ - "Int8", - "Int8" + "Text" ] } }, - "query": "\n SELECT \n ue.id AS \"user_email_id\",\n ue.email AS \"user_email\",\n ue.created_at AS \"user_email_created_at\",\n ue.confirmed_at AS \"user_email_confirmed_at\"\n FROM user_emails ue\n\n WHERE ue.user_id = $1\n AND ue.id = $2\n " + "query": "\n SELECT\n cr.compat_refresh_token_id,\n cr.refresh_token AS \"compat_refresh_token\",\n cr.created_at AS \"compat_refresh_token_created_at\",\n ct.compat_access_token_id,\n ct.access_token AS \"compat_access_token\",\n ct.created_at AS \"compat_access_token_created_at\",\n ct.expires_at AS \"compat_access_token_expires_at\",\n cs.compat_session_id,\n cs.created_at AS \"compat_session_created_at\",\n cs.finished_at AS \"compat_session_finished_at\",\n cs.device_id AS \"compat_session_device_id\",\n u.user_id,\n u.username AS \"user_username!\",\n ue.user_email_id AS \"user_email_id?\",\n ue.email AS \"user_email?\",\n ue.created_at AS \"user_email_created_at?\",\n ue.confirmed_at AS \"user_email_confirmed_at?\"\n\n FROM compat_refresh_tokens cr\n INNER JOIN compat_sessions cs\n USING (compat_session_id)\n INNER JOIN compat_access_tokens ct\n USING (compat_access_token_id)\n INNER JOIN users u\n USING (user_id)\n LEFT JOIN user_emails ue\n ON ue.user_email_id = u.primary_user_email_id\n\n WHERE cr.refresh_token = $1\n AND cr.consumed_at IS NULL\n AND cs.finished_at IS NULL\n " }, - "b6849f9289c7559a3ea02bea4d231871107a7fdd4e9587e53ca9d54bdd483623": { + "c88376abdba124ff0487a9a69d2345c7d69d7394f355111ec369cfa6d45fb40f": { "describe": { - "columns": [ - { - "name": "id", - "ordinal": 0, - "type_info": "Int8" - }, - { - "name": "created_at", - "ordinal": 1, - "type_info": "Timestamptz" - } - ], - "nullable": [ - false, - false - ], + "columns": [], + "nullable": [], "parameters": { "Left": [ - "Int8", + "Uuid", + "Uuid", "Text", "Text", "Text", @@ -1803,241 +1896,161 @@ "Text", "Text", "Text", + "Bool", + "Bool", + "Text", + "Bool", + "Timestamptz" + ] + } + }, + "query": "\n INSERT INTO oauth2_authorization_grants (\n oauth2_authorization_grant_id,\n oauth2_client_id,\n redirect_uri,\n scope,\n state,\n nonce,\n max_age,\n response_mode,\n code_challenge,\n code_challenge_method,\n response_type_code,\n response_type_id_token,\n authorization_code,\n requires_consent,\n created_at\n )\n VALUES\n ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)\n " + }, + "caf54e4659306a746747aa61906bdb2cb8da51176e90435aa8b9754ebf3e4d60": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Uuid", + "Uuid", + "Text", + "Timestamptz" + ] + } + }, + "query": "\n INSERT INTO compat_sessions (compat_session_id, user_id, device_id, created_at)\n VALUES ($1, $2, $3, $4)\n " + }, + "cb8ba981330e58a6c8580f6e394a721df110e1f2206e080434aa821c44c0164b": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [] + } + }, + "query": "TRUNCATE oauth2_client_redirect_uris, oauth2_clients CASCADE" + }, + "cc9e30678d673546efca336ee8e550083eed71459611fa2db52264e51e175901": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Uuid", "Text", "Bool", "Bool", "Text", - "Bool" - ] - } - }, - "query": "\n INSERT INTO oauth2_authorization_grants\n (oauth2_client_id, redirect_uri, scope, state, nonce, max_age,\n acr_values, response_mode, code_challenge, code_challenge_method,\n response_type_code, response_type_id_token, code, requires_consent)\n VALUES\n ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)\n RETURNING id, created_at\n " - }, - "ba431a27a4b256ceacb5724bd746424ed1f059e59ae1aa818fdd5f44c01d70a0": { - "describe": { - "columns": [ - { - "name": "consumed_at!", - "ordinal": 0, - "type_info": "Timestamptz" - } - ], - "nullable": [ - true - ], - "parameters": { - "Left": [ - "Int8" - ] - } - }, - "query": "\n UPDATE user_email_verifications\n SET consumed_at = NOW()\n WHERE id = $1\n RETURNING consumed_at AS \"consumed_at!\"\n " - }, - "bb5c43feb0e2ac1b8b31ab18423176057e795618ac64527bfb04fee91d86be2a": { - "describe": { - "columns": [ - { - "name": "fullfilled_at!", - "ordinal": 0, - "type_info": "Timestamptz" - } - ], - "nullable": [ - true - ], - "parameters": { - "Left": [ - "Int8", - "Int8" - ] - } - }, - "query": "\n UPDATE compat_sso_logins\n SET\n fullfilled_at = NOW(),\n compat_session_id = $2\n WHERE\n id = $1\n RETURNING fullfilled_at AS \"fullfilled_at!\"\n " - }, - "c2c402cfe0adcafa615f14a499caba4c96ca71d9ffb163e1feb05e5d85f3462c": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Left": [ - "Int8", - "Int8" - ] - } - }, - "query": "\n UPDATE oauth2_refresh_tokens\n SET next_token_id = $2\n WHERE id = $1\n " - }, - "cd14bbd315bec758b846f619202fdfd26634dfdcc185d5117a394b556c019473": { - "describe": { - "columns": [ - { - "name": "exchanged_at!", - "ordinal": 0, - "type_info": "Timestamptz" - } - ], - "nullable": [ - true - ], - "parameters": { - "Left": [ - "Int8" - ] - } - }, - "query": "\n UPDATE compat_sso_logins\n SET\n exchanged_at = NOW()\n WHERE\n id = $1\n RETURNING exchanged_at AS \"exchanged_at!\"\n " - }, - "d144679fac4fb1a6903060e87b08538db68fe734905fcd4e121acf487d23bd13": { - "describe": { - "columns": [ - { - "name": "verification_id", - "ordinal": 0, - "type_info": "Int8" - }, - { - "name": "verification_code", - "ordinal": 1, - "type_info": "Text" - }, - { - "name": "verification_expired!", - "ordinal": 2, - "type_info": "Bool" - }, - { - "name": "verification_created_at", - "ordinal": 3, - "type_info": "Timestamptz" - }, - { - "name": "verification_consumed_at", - "ordinal": 4, - "type_info": "Timestamptz" - } - ], - "nullable": [ - false, - false, - null, - false, - true - ], - "parameters": { - "Left": [ "Text", - "Int8", - "Interval" + "Text", + "Text", + "Text", + "Text", + "Jsonb", + "Text", + "Text", + "Text", + "Text", + "Text" ] } }, - "query": "\n SELECT\n ev.id AS \"verification_id\",\n ev.code AS \"verification_code\",\n (ev.created_at + $3 < NOW()) AS \"verification_expired!\",\n ev.created_at AS \"verification_created_at\",\n ev.consumed_at AS \"verification_consumed_at\"\n FROM user_email_verifications ev\n WHERE ev.code = $1\n AND ev.user_email_id = $2\n " + "query": "\n INSERT INTO oauth2_clients\n (oauth2_client_id,\n encrypted_client_secret,\n grant_type_authorization_code,\n grant_type_refresh_token,\n client_name,\n logo_uri,\n client_uri,\n policy_uri,\n tos_uri,\n jwks_uri,\n jwks,\n id_token_signed_response_alg,\n userinfo_signed_response_alg,\n token_endpoint_auth_method,\n token_endpoint_auth_signing_alg,\n initiate_login_uri)\n VALUES\n ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16)\n " }, - "d2f767218ec2489058db9a0382ca0eea20379c30aeae9f492da4ba35b66f4dc7": { + "d2a5766f319bb006c0814d365938644b8d713955bc7e2fa2c14fa1066083be77": { "describe": { "columns": [], "nullable": [], "parameters": { "Left": [ - "Int8" + "Uuid", + "Uuid", + "Text", + "Timestamptz", + "Timestamptz" ] } }, - "query": "\n DELETE FROM user_emails\n WHERE user_emails.id = $1\n " + "query": "\n INSERT INTO compat_access_tokens \n (compat_access_token_id, compat_session_id, access_token, created_at, expires_at)\n VALUES ($1, $2, $3, $4, $5)\n " }, - "d604e13bdfb2ff3d354d995f0b68f04091847755db98bafea7c45bd7b5c4ab68": { - "describe": { - "columns": [ - { - "name": "exchanged_at!: DateTime", - "ordinal": 0, - "type_info": "Timestamptz" - } - ], - "nullable": [ - true - ], - "parameters": { - "Left": [ - "Int8" - ] - } - }, - "query": "\n UPDATE oauth2_authorization_grants\n SET\n exchanged_at = NOW()\n WHERE\n id = $1\n RETURNING exchanged_at AS \"exchanged_at!: DateTime\"\n " - }, - "d7200c0def0662fda4af259c7872e06b8208e36f320ca90ea781c13d2bf85a9f": { + "d55a321e8935f4effda29d9620a0f622125cb38472785049ee21c2616a6bd068": { "describe": { "columns": [], "nullable": [], "parameters": { "Left": [ - "Int8", - "Text" + "Uuid", + "Timestamptz" ] } }, - "query": "\n INSERT INTO user_passwords (user_id, hashed_password)\n VALUES ($1, $2)\n " + "query": "\n UPDATE user_email_confirmation_codes\n SET consumed_at = $2\n WHERE user_email_confirmation_code_id = $1\n " }, - "d9d27eb4a0c11818a636d407438c4bc567a39396e7e236b3e776504417988eab": { + "d8677b3b6ee594c230fad98c1aa1c6e3d983375bf5b701c7b52468e7f906abf9": { "describe": { - "columns": [ - { - "name": "id", - "ordinal": 0, - "type_info": "Int8" - }, - { - "name": "created_at", - "ordinal": 1, - "type_info": "Timestamptz" - } - ], - "nullable": [ - false, - false - ], + "columns": [], + "nullable": [], "parameters": { "Left": [ - "Int8" + "Uuid", + "Uuid", + "Uuid", + "Text", + "Timestamptz" ] } }, - "query": "\n INSERT INTO user_session_authentications (session_id)\n VALUES ($1)\n RETURNING id, created_at\n " + "query": "\n INSERT INTO oauth2_refresh_tokens\n (oauth2_refresh_token_id, oauth2_session_id, oauth2_access_token_id,\n refresh_token, created_at)\n VALUES\n ($1, $2, $3, $4, $5)\n " }, - "d9deefd13877e64c44fa7b60d07e97380ca6a9612b3f08fb8c341b32c3a63a27": { + "e16ac9f75be25ef6873f1851e916df3ea730422409decc0344f7f05ce3c3841f": { "describe": { - "columns": [ - { - "name": "id", - "ordinal": 0, - "type_info": "Int8" - }, - { - "name": "created_at", - "ordinal": 1, - "type_info": "Timestamptz" - } - ], - "nullable": [ - false, - false - ], + "columns": [], + "nullable": [], "parameters": { "Left": [ - "Int8", - "Text" + "Uuid" ] } }, - "query": "\n INSERT INTO compat_access_tokens (compat_session_id, token)\n VALUES ($1, $2)\n RETURNING id, created_at\n " + "query": "\n DELETE FROM user_emails\n WHERE user_emails.user_email_id = $1\n " }, - "db34b3d7fa5d824e63f388d660615d748e11c1406e8166da907e0a54a665e37a": { + "e446e37d48c8838ef2e0d0fd82f8f7b04893c84ad46747cdf193ebd83755ceb2": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Uuid", + "Uuid", + "Timestamptz" + ] + } + }, + "query": "\n INSERT INTO user_sessions (user_session_id, user_id, created_at)\n VALUES ($1, $2, $3)\n " + }, + "ea74353fdeca30a2aca252e54c07442c21ee96c3d1bda1049aa26085ec2d0ba2": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Uuid", + "Text", + "Text", + "Timestamptz" + ] + } + }, + "query": "\n INSERT INTO compat_sso_logins \n (compat_sso_login_id, login_token, redirect_uri, created_at)\n VALUES ($1, $2, $3, $4)\n " + }, + "f6c4742f613dafe79315c1e8e31b0662e5b1e9f8151e8c3751189f0222819094": { "describe": { "columns": [ { "name": "user_email_id", "ordinal": 0, - "type_info": "Int8" + "type_info": "Uuid" }, { "name": "user_email", @@ -2063,440 +2076,11 @@ ], "parameters": { "Left": [ - "Int8", + "Uuid", "Text" ] } }, - "query": "\n SELECT \n ue.id AS \"user_email_id\",\n ue.email AS \"user_email\",\n ue.created_at AS \"user_email_created_at\",\n ue.confirmed_at AS \"user_email_confirmed_at\"\n FROM user_emails ue\n\n WHERE ue.user_id = $1\n AND ue.email = $2\n " - }, - "dbeffa1433a96693b6c5dd501a834eef770946f8ad44fb846556729682a1bb9d": { - "describe": { - "columns": [ - { - "name": "id", - "ordinal": 0, - "type_info": "Int8" - }, - { - "name": "created_at", - "ordinal": 1, - "type_info": "Timestamptz" - } - ], - "nullable": [ - false, - false - ], - "parameters": { - "Left": [ - "Int8", - "Text" - ] - } - }, - "query": "\n INSERT INTO user_email_verifications (user_email_id, code)\n VALUES ($1, $2)\n RETURNING id, created_at\n " - }, - "dbf9d2ee583d4dec07d7948c7540ff39b3e1de0c6abd168f47c02401f8417eec": { - "describe": { - "columns": [ - { - "name": "id", - "ordinal": 0, - "type_info": "Int8" - }, - { - "name": "created_at", - "ordinal": 1, - "type_info": "Timestamptz" - } - ], - "nullable": [ - false, - false - ], - "parameters": { - "Left": [ - "Int8", - "Int8", - "Text" - ] - } - }, - "query": "\n INSERT INTO compat_refresh_tokens (compat_session_id, compat_access_token_id, token)\n VALUES ($1, $2, $3)\n RETURNING id, created_at\n " - }, - "dda03ba41249bff965cb8f129acc15f4e40807adb9b75dee0ac43edd7809de84": { - "describe": { - "columns": [ - { - "name": "id", - "ordinal": 0, - "type_info": "Int8" - } - ], - "nullable": [ - false - ], - "parameters": { - "Left": [ - "Text" - ] - } - }, - "query": "\n INSERT INTO users (username)\n VALUES ($1)\n RETURNING id\n " - }, - "df38de13e2f345175f9ef46b4ae2a4f6637dbf74bb28559da8f4d8969f411d14": { - "describe": { - "columns": [ - { - "name": "access_token_id", - "ordinal": 0, - "type_info": "Int8" - }, - { - "name": "access_token", - "ordinal": 1, - "type_info": "Text" - }, - { - "name": "access_token_expires_after", - "ordinal": 2, - "type_info": "Int4" - }, - { - "name": "access_token_created_at", - "ordinal": 3, - "type_info": "Timestamptz" - }, - { - "name": "session_id!", - "ordinal": 4, - "type_info": "Int8" - }, - { - "name": "oauth2_client_id!", - "ordinal": 5, - "type_info": "Int8" - }, - { - "name": "scope!", - "ordinal": 6, - "type_info": "Text" - }, - { - "name": "user_session_id!", - "ordinal": 7, - "type_info": "Int8" - }, - { - "name": "user_session_created_at!", - "ordinal": 8, - "type_info": "Timestamptz" - }, - { - "name": "user_id!", - "ordinal": 9, - "type_info": "Int8" - }, - { - "name": "user_username!", - "ordinal": 10, - "type_info": "Text" - }, - { - "name": "user_session_last_authentication_id?", - "ordinal": 11, - "type_info": "Int8" - }, - { - "name": "user_session_last_authentication_created_at?", - "ordinal": 12, - "type_info": "Timestamptz" - }, - { - "name": "user_email_id?", - "ordinal": 13, - "type_info": "Int8" - }, - { - "name": "user_email?", - "ordinal": 14, - "type_info": "Text" - }, - { - "name": "user_email_created_at?", - "ordinal": 15, - "type_info": "Timestamptz" - }, - { - "name": "user_email_confirmed_at?", - "ordinal": 16, - "type_info": "Timestamptz" - } - ], - "nullable": [ - false, - false, - false, - false, - false, - false, - false, - false, - false, - false, - false, - false, - false, - false, - false, - false, - true - ], - "parameters": { - "Left": [ - "Text" - ] - } - }, - "query": "\n SELECT\n at.id AS \"access_token_id\",\n at.token AS \"access_token\",\n at.expires_after AS \"access_token_expires_after\",\n at.created_at AS \"access_token_created_at\",\n os.id AS \"session_id!\",\n os.oauth2_client_id AS \"oauth2_client_id!\",\n os.scope AS \"scope!\",\n us.id AS \"user_session_id!\",\n us.created_at AS \"user_session_created_at!\",\n u.id AS \"user_id!\",\n u.username AS \"user_username!\",\n usa.id AS \"user_session_last_authentication_id?\",\n usa.created_at AS \"user_session_last_authentication_created_at?\",\n ue.id AS \"user_email_id?\",\n ue.email AS \"user_email?\",\n ue.created_at AS \"user_email_created_at?\",\n ue.confirmed_at AS \"user_email_confirmed_at?\"\n\n FROM oauth2_access_tokens at\n INNER JOIN oauth2_sessions os\n ON os.id = at.oauth2_session_id\n INNER JOIN user_sessions us\n ON us.id = os.user_session_id\n INNER JOIN users u\n ON u.id = us.user_id\n LEFT JOIN user_session_authentications usa\n ON usa.session_id = us.id\n LEFT JOIN user_emails ue\n ON ue.id = u.primary_email_id\n\n WHERE at.token = $1\n AND at.created_at + (at.expires_after * INTERVAL '1 second') >= now()\n AND us.active\n AND os.ended_at IS NULL\n\n ORDER BY usa.created_at DESC\n LIMIT 1\n " - }, - "e11a625fa2ca20f00cac0fac5b4548efad6dd2f2f4742087935345cbf5701db2": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Left": [ - "Int8", - "Int8", - "TextArray" - ] - } - }, - "query": "\n INSERT INTO oauth2_consents (user_id, oauth2_client_id, scope_token)\n SELECT $1, $2, scope_token FROM UNNEST($3::text[]) scope_token\n ON CONFLICT (user_id, oauth2_client_id, scope_token) DO UPDATE SET updated_at = NOW()\n " - }, - "e2854f442e1d85484c5e9aa150a1f8c1f0c68a2496d2b2e0e97f4a1f527e2895": { - "describe": { - "columns": [ - { - "name": "grant_id", - "ordinal": 0, - "type_info": "Int8" - }, - { - "name": "grant_created_at", - "ordinal": 1, - "type_info": "Timestamptz" - }, - { - "name": "grant_cancelled_at", - "ordinal": 2, - "type_info": "Timestamptz" - }, - { - "name": "grant_fulfilled_at", - "ordinal": 3, - "type_info": "Timestamptz" - }, - { - "name": "grant_exchanged_at", - "ordinal": 4, - "type_info": "Timestamptz" - }, - { - "name": "grant_scope", - "ordinal": 5, - "type_info": "Text" - }, - { - "name": "grant_state", - "ordinal": 6, - "type_info": "Text" - }, - { - "name": "grant_redirect_uri", - "ordinal": 7, - "type_info": "Text" - }, - { - "name": "grant_response_mode", - "ordinal": 8, - "type_info": "Text" - }, - { - "name": "grant_nonce", - "ordinal": 9, - "type_info": "Text" - }, - { - "name": "grant_max_age", - "ordinal": 10, - "type_info": "Int4" - }, - { - "name": "grant_acr_values", - "ordinal": 11, - "type_info": "Text" - }, - { - "name": "oauth2_client_id", - "ordinal": 12, - "type_info": "Int8" - }, - { - "name": "grant_code", - "ordinal": 13, - "type_info": "Text" - }, - { - "name": "grant_response_type_code", - "ordinal": 14, - "type_info": "Bool" - }, - { - "name": "grant_response_type_id_token", - "ordinal": 15, - "type_info": "Bool" - }, - { - "name": "grant_code_challenge", - "ordinal": 16, - "type_info": "Text" - }, - { - "name": "grant_code_challenge_method", - "ordinal": 17, - "type_info": "Text" - }, - { - "name": "grant_requires_consent", - "ordinal": 18, - "type_info": "Bool" - }, - { - "name": "session_id?", - "ordinal": 19, - "type_info": "Int8" - }, - { - "name": "user_session_id?", - "ordinal": 20, - "type_info": "Int8" - }, - { - "name": "user_session_created_at?", - "ordinal": 21, - "type_info": "Timestamptz" - }, - { - "name": "user_id?", - "ordinal": 22, - "type_info": "Int8" - }, - { - "name": "user_username?", - "ordinal": 23, - "type_info": "Text" - }, - { - "name": "user_session_last_authentication_id?", - "ordinal": 24, - "type_info": "Int8" - }, - { - "name": "user_session_last_authentication_created_at?", - "ordinal": 25, - "type_info": "Timestamptz" - }, - { - "name": "user_email_id?", - "ordinal": 26, - "type_info": "Int8" - }, - { - "name": "user_email?", - "ordinal": 27, - "type_info": "Text" - }, - { - "name": "user_email_created_at?", - "ordinal": 28, - "type_info": "Timestamptz" - }, - { - "name": "user_email_confirmed_at?", - "ordinal": 29, - "type_info": "Timestamptz" - } - ], - "nullable": [ - false, - false, - true, - true, - true, - false, - true, - false, - false, - true, - true, - true, - false, - true, - false, - false, - true, - true, - false, - false, - false, - false, - false, - false, - false, - false, - false, - false, - false, - true - ], - "parameters": { - "Left": [ - "Text" - ] - } - }, - "query": "\n SELECT\n og.id AS grant_id,\n og.created_at AS grant_created_at,\n og.cancelled_at AS grant_cancelled_at,\n og.fulfilled_at AS grant_fulfilled_at,\n og.exchanged_at AS grant_exchanged_at,\n og.scope AS grant_scope,\n og.state AS grant_state,\n og.redirect_uri AS grant_redirect_uri,\n og.response_mode AS grant_response_mode,\n og.nonce AS grant_nonce,\n og.max_age AS grant_max_age,\n og.acr_values AS grant_acr_values,\n og.oauth2_client_id AS oauth2_client_id,\n og.code AS grant_code,\n og.response_type_code AS grant_response_type_code,\n og.response_type_id_token AS grant_response_type_id_token,\n og.code_challenge AS grant_code_challenge,\n og.code_challenge_method AS grant_code_challenge_method,\n og.requires_consent AS grant_requires_consent,\n os.id AS \"session_id?\",\n us.id AS \"user_session_id?\",\n us.created_at AS \"user_session_created_at?\",\n u.id AS \"user_id?\",\n u.username AS \"user_username?\",\n usa.id AS \"user_session_last_authentication_id?\",\n usa.created_at AS \"user_session_last_authentication_created_at?\",\n ue.id AS \"user_email_id?\",\n ue.email AS \"user_email?\",\n ue.created_at AS \"user_email_created_at?\",\n ue.confirmed_at AS \"user_email_confirmed_at?\"\n FROM\n oauth2_authorization_grants og\n LEFT JOIN oauth2_sessions os\n ON os.id = og.oauth2_session_id\n LEFT JOIN user_sessions us\n ON us.id = os.user_session_id\n LEFT JOIN users u\n ON u.id = us.user_id\n LEFT JOIN user_session_authentications usa\n ON usa.session_id = us.id\n LEFT JOIN user_emails ue\n ON ue.id = u.primary_email_id\n\n WHERE og.code = $1\n\n ORDER BY usa.created_at DESC\n LIMIT 1\n " - }, - "e5cd99bdaf9c678fc659431fecc5d76b25bb08b781fd17e50eda82ea3aa8cea8": { - "describe": { - "columns": [ - { - "name": "count!", - "ordinal": 0, - "type_info": "Int8" - } - ], - "nullable": [ - null - ], - "parameters": { - "Left": [ - "Int8" - ] - } - }, - "query": "\n SELECT COUNT(*) as \"count!\"\n FROM user_sessions s\n WHERE s.user_id = $1 AND s.active\n " - }, - "ebf73a609e81830b16700d2c315fffa93fd85b2886e29f234d9953b18a9f72b5": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Left": [] - } - }, - "query": "TRUNCATE oauth2_client_redirect_uris, oauth2_clients RESTART IDENTITY CASCADE" + "query": "\n SELECT \n ue.user_email_id,\n ue.email AS \"user_email\",\n ue.created_at AS \"user_email_created_at\",\n ue.confirmed_at AS \"user_email_confirmed_at\"\n FROM user_emails ue\n\n WHERE ue.user_id = $1\n AND ue.email = $2\n " } } \ No newline at end of file diff --git a/crates/storage/src/compat.rs b/crates/storage/src/compat.rs index f315d34c..7ce20355 100644 --- a/crates/storage/src/compat.rs +++ b/crates/storage/src/compat.rs @@ -19,28 +19,28 @@ use mas_data_model::{ CompatAccessToken, CompatRefreshToken, CompatSession, CompatSsoLogin, CompatSsoLoginState, Device, User, UserEmail, }; -use sqlx::{postgres::types::PgInterval, Acquire, PgExecutor, Postgres}; +use sqlx::{Acquire, PgExecutor, Postgres}; use thiserror::Error; use tokio::task; use tracing::{info_span, Instrument}; +use ulid::Ulid; use url::Url; +use uuid::Uuid; -use crate::{ - user::lookup_user_by_username, DatabaseInconsistencyError, IdAndCreationTime, PostgresqlBackend, -}; +use crate::{user::lookup_user_by_username, DatabaseInconsistencyError, PostgresqlBackend}; struct CompatAccessTokenLookup { - compat_access_token_id: i64, + compat_access_token_id: Uuid, compat_access_token: String, compat_access_token_created_at: DateTime, compat_access_token_expires_at: Option>, - compat_session_id: i64, + compat_session_id: Uuid, compat_session_created_at: DateTime, - compat_session_deleted_at: Option>, + compat_session_finished_at: Option>, compat_session_device_id: String, - user_id: i64, + user_id: Uuid, user_username: String, - user_email_id: Option, + user_email_id: Option, user_email: Option, user_email_created_at: Option>, user_email_confirmed_at: Option>, @@ -49,6 +49,7 @@ struct CompatAccessTokenLookup { #[derive(Debug, Error)] #[error("failed to lookup compat access token")] pub enum CompatAccessTokenLookupError { + Expired { when: DateTime }, Database(#[from] sqlx::Error), Inconsistency(#[from] DatabaseInconsistencyError), } @@ -56,7 +57,10 @@ pub enum CompatAccessTokenLookupError { impl CompatAccessTokenLookupError { #[must_use] pub fn not_found(&self) -> bool { - matches!(self, Self::Database(sqlx::Error::RowNotFound)) + matches!( + self, + Self::Database(sqlx::Error::RowNotFound) | Self::Expired { .. } + ) } } @@ -75,41 +79,48 @@ pub async fn lookup_active_compat_access_token( CompatAccessTokenLookup, r#" SELECT - ct.id AS "compat_access_token_id", - ct.token AS "compat_access_token", + ct.compat_access_token_id, + ct.access_token AS "compat_access_token", ct.created_at AS "compat_access_token_created_at", ct.expires_at AS "compat_access_token_expires_at", - cs.id AS "compat_session_id", + cs.compat_session_id, cs.created_at AS "compat_session_created_at", - cs.deleted_at AS "compat_session_deleted_at", + cs.finished_at AS "compat_session_finished_at", cs.device_id AS "compat_session_device_id", - u.id AS "user_id!", + u.user_id AS "user_id!", u.username AS "user_username!", - ue.id AS "user_email_id?", + ue.user_email_id AS "user_email_id?", ue.email AS "user_email?", ue.created_at AS "user_email_created_at?", ue.confirmed_at AS "user_email_confirmed_at?" FROM compat_access_tokens ct INNER JOIN compat_sessions cs - ON cs.id = ct.compat_session_id + USING (compat_session_id) INNER JOIN users u - ON u.id = cs.user_id + USING (user_id) LEFT JOIN user_emails ue - ON ue.id = u.primary_email_id + ON ue.user_email_id = u.primary_user_email_id - WHERE ct.token = $1 + WHERE ct.access_token = $1 AND (ct.expires_at IS NULL OR ct.expires_at > NOW()) - AND cs.deleted_at IS NULL - "#, + AND cs.finished_at IS NULL + "#, token, ) .fetch_one(executor) .instrument(info_span!("Fetch compat access token")) .await?; + // Check for token expiration + if let Some(expires_at) = res.compat_access_token_expires_at { + if expires_at < Utc::now() { + return Err(CompatAccessTokenLookupError::Expired { when: expires_at }); + } + } + let token = CompatAccessToken { - data: res.compat_access_token_id, + data: res.compat_access_token_id.into(), token: res.compat_access_token, created_at: res.compat_access_token_created_at, expires_at: res.compat_access_token_expires_at, @@ -122,7 +133,7 @@ pub async fn lookup_active_compat_access_token( res.user_email_confirmed_at, ) { (Some(id), Some(email), Some(created_at), confirmed_at) => Some(UserEmail { - data: id, + data: id.into(), email, created_at, confirmed_at, @@ -131,41 +142,42 @@ pub async fn lookup_active_compat_access_token( _ => return Err(DatabaseInconsistencyError.into()), }; + let id = Ulid::from(res.user_id); let user = User { - data: res.user_id, + data: id, username: res.user_username, - sub: format!("fake-sub-{}", res.user_id), + sub: id.to_string(), primary_email, }; let device = Device::try_from(res.compat_session_device_id).unwrap(); let session = CompatSession { - data: res.compat_session_id, + data: res.compat_session_id.into(), user, device, created_at: res.compat_session_created_at, - deleted_at: res.compat_session_deleted_at, + finished_at: res.compat_session_finished_at, }; Ok((token, session)) } pub struct CompatRefreshTokenLookup { - compat_refresh_token_id: i64, + compat_refresh_token_id: Uuid, compat_refresh_token: String, compat_refresh_token_created_at: DateTime, - compat_access_token_id: i64, + compat_access_token_id: Uuid, compat_access_token: String, compat_access_token_created_at: DateTime, compat_access_token_expires_at: Option>, - compat_session_id: i64, + compat_session_id: Uuid, compat_session_created_at: DateTime, - compat_session_deleted_at: Option>, + compat_session_finished_at: Option>, compat_session_device_id: String, - user_id: i64, + user_id: Uuid, user_username: String, - user_email_id: Option, + user_email_id: Option, user_email: Option, user_email_created_at: Option>, user_email_confirmed_at: Option>, @@ -202,37 +214,37 @@ pub async fn lookup_active_compat_refresh_token( CompatRefreshTokenLookup, r#" SELECT - cr.id AS "compat_refresh_token_id", - cr.token AS "compat_refresh_token", + cr.compat_refresh_token_id, + cr.refresh_token AS "compat_refresh_token", cr.created_at AS "compat_refresh_token_created_at", - ct.id AS "compat_access_token_id", - ct.token AS "compat_access_token", + ct.compat_access_token_id, + ct.access_token AS "compat_access_token", ct.created_at AS "compat_access_token_created_at", ct.expires_at AS "compat_access_token_expires_at", - cs.id AS "compat_session_id", + cs.compat_session_id, cs.created_at AS "compat_session_created_at", - cs.deleted_at AS "compat_session_deleted_at", + cs.finished_at AS "compat_session_finished_at", cs.device_id AS "compat_session_device_id", - u.id AS "user_id!", + u.user_id, u.username AS "user_username!", - ue.id AS "user_email_id?", + ue.user_email_id AS "user_email_id?", ue.email AS "user_email?", ue.created_at AS "user_email_created_at?", ue.confirmed_at AS "user_email_confirmed_at?" FROM compat_refresh_tokens cr - INNER JOIN compat_access_tokens ct - ON ct.id = cr.compat_access_token_id INNER JOIN compat_sessions cs - ON cs.id = cr.compat_session_id + USING (compat_session_id) + INNER JOIN compat_access_tokens ct + USING (compat_access_token_id) INNER JOIN users u - ON u.id = cs.user_id + USING (user_id) LEFT JOIN user_emails ue - ON ue.id = u.primary_email_id + ON ue.user_email_id = u.primary_user_email_id - WHERE cr.token = $1 - AND cr.next_token_id IS NULL - AND cs.deleted_at IS NULL + WHERE cr.refresh_token = $1 + AND cr.consumed_at IS NULL + AND cs.finished_at IS NULL "#, token, ) @@ -241,13 +253,13 @@ pub async fn lookup_active_compat_refresh_token( .await?; let refresh_token = CompatRefreshToken { - data: res.compat_refresh_token_id, + data: res.compat_refresh_token_id.into(), token: res.compat_refresh_token, created_at: res.compat_refresh_token_created_at, }; let access_token = CompatAccessToken { - data: res.compat_access_token_id, + data: res.compat_access_token_id.into(), token: res.compat_access_token, created_at: res.compat_access_token_created_at, expires_at: res.compat_access_token_expires_at, @@ -260,7 +272,7 @@ pub async fn lookup_active_compat_refresh_token( res.user_email_confirmed_at, ) { (Some(id), Some(email), Some(created_at), confirmed_at) => Some(UserEmail { - data: id, + data: id.into(), email, created_at, confirmed_at, @@ -269,21 +281,22 @@ pub async fn lookup_active_compat_refresh_token( _ => return Err(DatabaseInconsistencyError.into()), }; + let id = Ulid::from(res.user_id); let user = User { - data: res.user_id, + data: id, username: res.user_username, - sub: format!("fake-sub-{}", res.user_id), + sub: id.to_string(), primary_email, }; let device = Device::try_from(res.compat_session_device_id).unwrap(); let session = CompatSession { - data: res.compat_session_id, + data: res.compat_session_id.into(), user, device, created_at: res.compat_session_created_at, - deleted_at: res.compat_session_deleted_at, + finished_at: res.compat_session_finished_at, }; Ok((refresh_token, access_token, session)) @@ -310,7 +323,7 @@ pub async fn compat_login( ORDER BY up.created_at DESC LIMIT 1 "#, - user.data, + Uuid::from(user.data), ) .fetch_one(&mut txn) .instrument(tracing::info_span!("Lookup hashed password")) @@ -327,27 +340,30 @@ pub async fn compat_login( .instrument(tracing::info_span!("Verify hashed password")) .await??; - let res = sqlx::query_as!( - IdAndCreationTime, + let created_at = Utc::now(); + let id = Ulid::from_datetime(created_at.into()); + sqlx::query!( r#" - INSERT INTO compat_sessions (user_id, device_id) - VALUES ($1, $2) - RETURNING id, created_at + INSERT INTO compat_sessions + (compat_session_id, user_id, device_id, created_at) + VALUES ($1, $2, $3, $4) "#, - user.data, + Uuid::from(id), + Uuid::from(user.data), device.as_str(), + created_at, ) - .fetch_one(&mut txn) + .execute(&mut txn) .instrument(tracing::info_span!("Insert compat session")) .await .context("could not insert compat session")?; let session = CompatSession { - data: res.id, + data: id, user, device, - created_at: res.created_at, - deleted_at: None, + created_at, + finished_at: None, }; txn.commit().await.context("could not commit transaction")?; @@ -361,70 +377,48 @@ pub async fn add_compat_access_token( token: String, expires_after: Option, ) -> Result, anyhow::Error> { - if let Some(expires_after) = expires_after { - // For some reason, we need to convert the type first - let pg_expires_after = PgInterval::try_from(expires_after) - // For some reason, this error type does not let me to just bubble up the error here - .map_err(|e| anyhow::anyhow!("failed to encode duration: {}", e))?; + let created_at = Utc::now(); + let id = Ulid::from_datetime(created_at.into()); + let expires_at = expires_after.map(|expires_after| created_at + expires_after); - let res = sqlx::query_as!( - IdAndCreationTime, - r#" - INSERT INTO compat_access_tokens (compat_session_id, token, created_at, expires_at) - VALUES ($1, $2, NOW(), NOW() + $3) - RETURNING id, created_at - "#, - session.data, - token, - pg_expires_after, - ) - .fetch_one(executor) - .instrument(tracing::info_span!("Insert compat access token")) - .await - .context("could not insert compat access token")?; + sqlx::query!( + r#" + INSERT INTO compat_access_tokens + (compat_access_token_id, compat_session_id, access_token, created_at, expires_at) + VALUES ($1, $2, $3, $4, $5) + "#, + Uuid::from(id), + Uuid::from(session.data), + token, + created_at, + expires_at, + ) + .execute(executor) + .instrument(tracing::info_span!("Insert compat access token")) + .await + .context("could not insert compat access token")?; - Ok(CompatAccessToken { - data: res.id, - token, - created_at: res.created_at, - expires_at: Some(res.created_at + expires_after), - }) - } else { - let res = sqlx::query_as!( - IdAndCreationTime, - r#" - INSERT INTO compat_access_tokens (compat_session_id, token) - VALUES ($1, $2) - RETURNING id, created_at - "#, - session.data, - token, - ) - .fetch_one(executor) - .instrument(tracing::info_span!("Insert compat access token")) - .await - .context("could not insert compat access token")?; - - Ok(CompatAccessToken { - data: res.id, - token, - created_at: res.created_at, - expires_at: None, - }) - } + Ok(CompatAccessToken { + data: id, + token, + created_at, + expires_at, + }) } pub async fn expire_compat_access_token( executor: impl PgExecutor<'_>, access_token: CompatAccessToken, ) -> anyhow::Result<()> { + let expires_at = Utc::now(); let res = sqlx::query!( r#" UPDATE compat_access_tokens - SET expires_at = NOW() - WHERE id = $1 + SET expires_at = $2 + WHERE compat_access_token_id = $1 "#, - access_token.data, + Uuid::from(access_token.data), + expires_at, ) .execute(executor) .await @@ -445,26 +439,30 @@ pub async fn add_compat_refresh_token( access_token: &CompatAccessToken, token: String, ) -> Result, anyhow::Error> { - let res = sqlx::query_as!( - IdAndCreationTime, + let created_at = Utc::now(); + let id = Ulid::from_datetime(created_at.into()); + sqlx::query!( r#" - INSERT INTO compat_refresh_tokens (compat_session_id, compat_access_token_id, token) - VALUES ($1, $2, $3) - RETURNING id, created_at + INSERT INTO compat_refresh_tokens + (compat_refresh_token_id, compat_session_id, + compat_access_token_id, refresh_token, created_at) + VALUES ($1, $2, $3, $4, $5) "#, - session.data, - access_token.data, + Uuid::from(id), + Uuid::from(session.data), + Uuid::from(access_token.data), token, + created_at, ) - .fetch_one(executor) + .execute(executor) .instrument(tracing::info_span!("Insert compat refresh token")) .await .context("could not insert compat refresh token")?; Ok(CompatRefreshToken { - data: res.id, + data: id, token, - created_at: res.created_at, + created_at, }) } @@ -473,16 +471,19 @@ pub async fn compat_logout( executor: impl PgExecutor<'_>, token: &str, ) -> Result<(), anyhow::Error> { + let finished_at = Utc::now(); + // TODO: this does not check for token expiration let res = sqlx::query!( r#" - UPDATE compat_sessions - SET deleted_at = NOW() - FROM compat_access_tokens - WHERE compat_access_tokens.token = $1 - AND compat_sessions.id = compat_access_tokens.id - AND compat_sessions.deleted_at IS NULL + UPDATE compat_sessions cs + SET finished_at = $2 + FROM compat_access_tokens ca + WHERE ca.access_token = $1 + AND ca.compat_session_id = cs.compat_session_id + AND cs.finished_at IS NULL "#, token, + finished_at, ) .execute(executor) .await @@ -495,19 +496,19 @@ pub async fn compat_logout( } } -pub async fn replace_compat_refresh_token( +pub async fn consume_compat_refresh_token( executor: impl PgExecutor<'_>, - refresh_token: &CompatRefreshToken, - next_refresh_token: &CompatRefreshToken, + refresh_token: CompatRefreshToken, ) -> anyhow::Result<()> { + let consumed_at = Utc::now(); let res = sqlx::query!( r#" UPDATE compat_refresh_tokens - SET next_token_id = $2 - WHERE id = $1 + SET consumed_at = $2 + WHERE compat_refresh_token_id = $1 "#, - refresh_token.data, - next_refresh_token.data + Uuid::from(refresh_token.data), + consumed_at, ) .execute(executor) .await @@ -524,47 +525,50 @@ pub async fn replace_compat_refresh_token( pub async fn insert_compat_sso_login( executor: impl PgExecutor<'_>, - token: String, + login_token: String, redirect_uri: Url, ) -> anyhow::Result> { - let res = sqlx::query_as!( - IdAndCreationTime, + let created_at = Utc::now(); + let id = Ulid::from_datetime(created_at.into()); + sqlx::query!( r#" - INSERT INTO compat_sso_logins (token, redirect_uri) - VALUES ($1, $2) - RETURNING id, created_at + INSERT INTO compat_sso_logins + (compat_sso_login_id, login_token, redirect_uri, created_at) + VALUES ($1, $2, $3, $4) "#, - &token, + Uuid::from(id), + &login_token, redirect_uri.as_str(), + created_at, ) - .fetch_one(executor) + .execute(executor) .instrument(tracing::info_span!("Insert compat SSO login")) .await .context("could not insert compat SSO login")?; Ok(CompatSsoLogin { - data: res.id, - token, + data: id, + login_token, redirect_uri, - created_at: res.created_at, + created_at, state: CompatSsoLoginState::Pending, }) } struct CompatSsoLoginLookup { - compat_sso_login_id: i64, + compat_sso_login_id: Uuid, compat_sso_login_token: String, compat_sso_login_redirect_uri: String, compat_sso_login_created_at: DateTime, - compat_sso_login_fullfilled_at: Option>, + compat_sso_login_fulfilled_at: Option>, compat_sso_login_exchanged_at: Option>, - compat_session_id: Option, + compat_session_id: Option, compat_session_created_at: Option>, - compat_session_deleted_at: Option>, + compat_session_finished_at: Option>, compat_session_device_id: Option, - user_id: Option, + user_id: Option, user_username: Option, - user_email_id: Option, + user_email_id: Option, user_email: Option, user_email_created_at: Option>, user_email_confirmed_at: Option>, @@ -584,7 +588,7 @@ impl TryFrom for CompatSsoLogin { res.user_email_confirmed_at, ) { (Some(id), Some(email), Some(created_at), confirmed_at) => Some(UserEmail { - data: id, + data: id.into(), email, created_at, confirmed_at, @@ -594,12 +598,16 @@ impl TryFrom for CompatSsoLogin { }; let user = match (res.user_id, res.user_username, primary_email) { - (Some(id), Some(username), primary_email) => Some(User { - data: id, - username, - sub: format!("fake-sub-{}", id), - primary_email, - }), + (Some(id), Some(username), primary_email) => { + let id = Ulid::from(id); + Some(User { + data: id, + username, + sub: id.to_string(), + primary_email, + }) + } + (None, None, None) => None, _ => return Err(DatabaseInconsistencyError), }; @@ -608,17 +616,17 @@ impl TryFrom for CompatSsoLogin { res.compat_session_id, res.compat_session_device_id, res.compat_session_created_at, - res.compat_session_deleted_at, + res.compat_session_finished_at, user, ) { - (Some(id), Some(device_id), Some(created_at), deleted_at, Some(user)) => { + (Some(id), Some(device_id), Some(created_at), finished_at, Some(user)) => { let device = Device::try_from(device_id).map_err(|_| DatabaseInconsistencyError)?; Some(CompatSession { - data: id, + data: id.into(), user, device, created_at, - deleted_at, + finished_at, }) } (None, None, None, None, None) => None, @@ -626,18 +634,18 @@ impl TryFrom for CompatSsoLogin { }; let state = match ( - res.compat_sso_login_fullfilled_at, + res.compat_sso_login_fulfilled_at, res.compat_sso_login_exchanged_at, session, ) { (None, None, None) => CompatSsoLoginState::Pending, - (Some(fullfilled_at), None, Some(session)) => CompatSsoLoginState::Fullfilled { - fullfilled_at, + (Some(fulfilled_at), None, Some(session)) => CompatSsoLoginState::Fulfilled { + fulfilled_at, session, }, - (Some(fullfilled_at), Some(exchanged_at), Some(session)) => { + (Some(fulfilled_at), Some(exchanged_at), Some(session)) => { CompatSsoLoginState::Exchanged { - fullfilled_at, + fulfilled_at, exchanged_at, session, } @@ -646,8 +654,8 @@ impl TryFrom for CompatSsoLogin { }; Ok(CompatSsoLogin { - data: res.compat_sso_login_id, - token: res.compat_sso_login_token, + data: res.compat_sso_login_id.into(), + login_token: res.compat_sso_login_token, redirect_uri, created_at: res.compat_sso_login_created_at, state, @@ -673,38 +681,38 @@ impl CompatSsoLoginLookupError { #[tracing::instrument(skip(executor), err)] pub async fn get_compat_sso_login_by_id( executor: impl PgExecutor<'_>, - id: i64, + id: Ulid, ) -> Result, CompatSsoLoginLookupError> { let res = sqlx::query_as!( CompatSsoLoginLookup, r#" SELECT - cl.id AS "compat_sso_login_id", - cl.token AS "compat_sso_login_token", + cl.compat_sso_login_id, + cl.login_token AS "compat_sso_login_token", cl.redirect_uri AS "compat_sso_login_redirect_uri", cl.created_at AS "compat_sso_login_created_at", - cl.fullfilled_at AS "compat_sso_login_fullfilled_at", + cl.fulfilled_at AS "compat_sso_login_fulfilled_at", cl.exchanged_at AS "compat_sso_login_exchanged_at", - cs.id AS "compat_session_id?", + cs.compat_session_id AS "compat_session_id?", cs.created_at AS "compat_session_created_at?", - cs.deleted_at AS "compat_session_deleted_at?", + cs.finished_at AS "compat_session_finished_at?", cs.device_id AS "compat_session_device_id?", - u.id AS "user_id?", + u.user_id AS "user_id?", u.username AS "user_username?", - ue.id AS "user_email_id?", + ue.user_email_id AS "user_email_id?", ue.email AS "user_email?", ue.created_at AS "user_email_created_at?", ue.confirmed_at AS "user_email_confirmed_at?" FROM compat_sso_logins cl LEFT JOIN compat_sessions cs - ON cs.id = cl.compat_session_id + USING (compat_session_id) LEFT JOIN users u - ON u.id = cs.user_id + USING (user_id) LEFT JOIN user_emails ue - ON ue.id = u.primary_email_id - WHERE cl.id = $1 + ON ue.user_email_id = u.primary_user_email_id + WHERE cl.compat_sso_login_id = $1 "#, - id, + Uuid::from(id), ) .fetch_one(executor) .instrument(tracing::info_span!("Lookup compat SSO login")) @@ -723,30 +731,30 @@ pub async fn get_compat_sso_login_by_token( CompatSsoLoginLookup, r#" SELECT - cl.id AS "compat_sso_login_id", - cl.token AS "compat_sso_login_token", + cl.compat_sso_login_id, + cl.login_token AS "compat_sso_login_token", cl.redirect_uri AS "compat_sso_login_redirect_uri", cl.created_at AS "compat_sso_login_created_at", - cl.fullfilled_at AS "compat_sso_login_fullfilled_at", + cl.fulfilled_at AS "compat_sso_login_fulfilled_at", cl.exchanged_at AS "compat_sso_login_exchanged_at", - cs.id AS "compat_session_id?", + cs.compat_session_id AS "compat_session_id?", cs.created_at AS "compat_session_created_at?", - cs.deleted_at AS "compat_session_deleted_at?", + cs.finished_at AS "compat_session_finished_at?", cs.device_id AS "compat_session_device_id?", - u.id AS "user_id?", + u.user_id AS "user_id?", u.username AS "user_username?", - ue.id AS "user_email_id?", + ue.user_email_id AS "user_email_id?", ue.email AS "user_email?", ue.created_at AS "user_email_created_at?", ue.confirmed_at AS "user_email_confirmed_at?" FROM compat_sso_logins cl LEFT JOIN compat_sessions cs - ON cs.id = cl.compat_session_id + USING (compat_session_id) LEFT JOIN users u - ON u.id = cs.user_id + USING (user_id) LEFT JOIN user_emails ue - ON ue.id = u.primary_email_id - WHERE cl.token = $1 + ON ue.user_email_id = u.primary_user_email_id + WHERE cl.login_token = $1 "#, token, ) @@ -769,49 +777,52 @@ pub async fn fullfill_compat_sso_login( let mut txn = conn.begin().await.context("could not start transaction")?; - let res = sqlx::query_as!( - IdAndCreationTime, + let created_at = Utc::now(); + let id = Ulid::from_datetime(created_at.into()); + sqlx::query!( r#" - INSERT INTO compat_sessions (user_id, device_id) - VALUES ($1, $2) - RETURNING id, created_at + INSERT INTO compat_sessions (compat_session_id, user_id, device_id, created_at) + VALUES ($1, $2, $3, $4) "#, - user.data, + Uuid::from(id), + Uuid::from(user.data), device.as_str(), + created_at, ) - .fetch_one(&mut txn) + .execute(&mut txn) .instrument(tracing::info_span!("Insert compat session")) .await .context("could not insert compat session")?; let session = CompatSession { - data: res.id, + data: id, user, device, - created_at: res.created_at, - deleted_at: None, + created_at, + finished_at: None, }; - let res = sqlx::query_scalar!( + let fulfilled_at = Utc::now(); + sqlx::query!( r#" UPDATE compat_sso_logins SET - fullfilled_at = NOW(), - compat_session_id = $2 + compat_session_id = $2, + fulfilled_at = $3 WHERE - id = $1 - RETURNING fullfilled_at AS "fullfilled_at!" + compat_sso_login_id = $1 "#, - login.data, - session.data, + Uuid::from(login.data), + Uuid::from(session.data), + fulfilled_at, ) - .fetch_one(&mut txn) + .execute(&mut txn) .instrument(tracing::info_span!("Update compat SSO login")) .await .context("could not update compat SSO login")?; - let state = CompatSsoLoginState::Fullfilled { - fullfilled_at: res, + let state = CompatSsoLoginState::Fulfilled { + fulfilled_at, session, }; @@ -826,33 +837,34 @@ pub async fn mark_compat_sso_login_as_exchanged( executor: impl PgExecutor<'_>, mut login: CompatSsoLogin, ) -> anyhow::Result> { - let (fullfilled_at, session) = match login.state { - CompatSsoLoginState::Fullfilled { - fullfilled_at, + let (fulfilled_at, session) = match login.state { + CompatSsoLoginState::Fulfilled { + fulfilled_at, session, - } => (fullfilled_at, session), + } => (fulfilled_at, session), _ => bail!("sso login in wrong state"), }; - let res = sqlx::query_scalar!( + let exchanged_at = Utc::now(); + sqlx::query!( r#" UPDATE compat_sso_logins SET - exchanged_at = NOW() + exchanged_at = $2 WHERE - id = $1 - RETURNING exchanged_at AS "exchanged_at!" + compat_sso_login_id = $1 "#, - login.data, + Uuid::from(login.data), + exchanged_at, ) - .fetch_one(executor) + .execute(executor) .instrument(tracing::info_span!("Update compat SSO login")) .await .context("could not update compat SSO login")?; let state = CompatSsoLoginState::Exchanged { - fullfilled_at, - exchanged_at: res, + fulfilled_at, + exchanged_at, session, }; login.state = state; diff --git a/crates/storage/src/lib.rs b/crates/storage/src/lib.rs index fcf6ea5c..6c5d8471 100644 --- a/crates/storage/src/lib.rs +++ b/crates/storage/src/lib.rs @@ -23,11 +23,11 @@ clippy::module_name_repetitions )] -use chrono::{DateTime, Utc}; use mas_data_model::{StorageBackend, StorageBackendMarker}; use serde::Serialize; use sqlx::migrate::Migrator; use thiserror::Error; +use ulid::Ulid; #[derive(Debug, Error)] #[error("database query returned an inconsistent state")] @@ -37,29 +37,24 @@ pub struct DatabaseInconsistencyError; pub struct PostgresqlBackend; impl StorageBackend for PostgresqlBackend { - type AccessTokenData = i64; - type AuthenticationData = i64; - type AuthorizationGrantData = i64; - type BrowserSessionData = i64; - type ClientData = i64; - type CompatAccessTokenData = i64; - type CompatRefreshTokenData = i64; - type CompatSessionData = i64; - type CompatSsoLoginData = i64; - type RefreshTokenData = i64; - type SessionData = i64; - type UserData = i64; - type UserEmailData = i64; - type UserEmailVerificationData = i64; + type AccessTokenData = Ulid; + type AuthenticationData = Ulid; + type AuthorizationGrantData = Ulid; + type BrowserSessionData = Ulid; + type ClientData = Ulid; + type CompatAccessTokenData = Ulid; + type CompatRefreshTokenData = Ulid; + type CompatSessionData = Ulid; + type CompatSsoLoginData = Ulid; + type RefreshTokenData = Ulid; + type SessionData = Ulid; + type UserData = Ulid; + type UserEmailData = Ulid; + type UserEmailVerificationData = Ulid; } impl StorageBackendMarker for PostgresqlBackend {} -struct IdAndCreationTime { - id: i64, - created_at: DateTime, -} - pub mod compat; pub mod oauth2; pub mod user; diff --git a/crates/storage/src/oauth2/access_token.rs b/crates/storage/src/oauth2/access_token.rs index 0b585823..dd49e737 100644 --- a/crates/storage/src/oauth2/access_token.rs +++ b/crates/storage/src/oauth2/access_token.rs @@ -17,62 +17,76 @@ use chrono::{DateTime, Duration, Utc}; use mas_data_model::{AccessToken, Authentication, BrowserSession, Session, User, UserEmail}; use sqlx::{Acquire, PgExecutor, Postgres}; use thiserror::Error; +use ulid::Ulid; +use uuid::Uuid; use super::client::{lookup_client, ClientFetchError}; -use crate::{DatabaseInconsistencyError, IdAndCreationTime, PostgresqlBackend}; +use crate::{DatabaseInconsistencyError, PostgresqlBackend}; +#[tracing::instrument( + skip_all, + fields( + session.id = %session.data, + client.id = %session.client.data, + user.id = %session.browser_session.user.data, + access_token.id, + ), + err(Debug), +)] pub async fn add_access_token( executor: impl PgExecutor<'_>, session: &Session, - token: &str, + access_token: String, expires_after: Duration, ) -> anyhow::Result> { - // Checked convertion of duration to i32, maxing at i32::MAX - let expires_after_seconds = i32::try_from(expires_after.num_seconds()).unwrap_or(i32::MAX); + let created_at = Utc::now(); + let expires_at = created_at + expires_after; + let id = Ulid::from_datetime(created_at.into()); - let res = sqlx::query_as!( - IdAndCreationTime, + tracing::Span::current().record("access_token.id", tracing::field::display(id)); + + sqlx::query!( r#" INSERT INTO oauth2_access_tokens - (oauth2_session_id, token, expires_after) + (oauth2_access_token_id, oauth2_session_id, access_token, created_at, expires_at) VALUES - ($1, $2, $3) - RETURNING - id, created_at + ($1, $2, $3, $4, $5) "#, - session.data, - token, - expires_after_seconds, + Uuid::from(id), + Uuid::from(session.data), + &access_token, + created_at, + expires_at, ) - .fetch_one(executor) + .execute(executor) .await .context("could not insert oauth2 access token")?; Ok(AccessToken { - data: res.id, - expires_after, - token: token.to_owned(), - jti: format!("{}", res.id), - created_at: res.created_at, + data: id, + access_token, + jti: id.to_string(), + created_at, + expires_at, }) } #[derive(Debug)] pub struct OAuth2AccessTokenLookup { - access_token_id: i64, - access_token: String, - access_token_expires_after: i32, - access_token_created_at: DateTime, - session_id: i64, - oauth2_client_id: i64, + oauth2_access_token_id: Uuid, + oauth2_access_token: String, + oauth2_access_token_created_at: DateTime, + oauth2_access_token_expires_at: DateTime, + oauth2_session_id: Uuid, + oauth2_client_id: Uuid, scope: String, - user_session_id: i64, + user_session_id: Uuid, user_session_created_at: DateTime, - user_id: i64, + user_id: Uuid, user_username: String, - user_session_last_authentication_id: Option, + user_session_last_authentication_id: Option, user_session_last_authentication_created_at: Option>, - user_email_id: Option, + user_email_id: Option, user_email: Option, user_email_created_at: Option>, user_email_confirmed_at: Option>, @@ -114,40 +128,39 @@ where OAuth2AccessTokenLookup, r#" SELECT - at.id AS "access_token_id", - at.token AS "access_token", - at.expires_after AS "access_token_expires_after", - at.created_at AS "access_token_created_at", - os.id AS "session_id!", + at.oauth2_access_token_id, + at.access_token AS "oauth2_access_token", + at.created_at AS "oauth2_access_token_created_at", + at.expires_at AS "oauth2_access_token_expires_at", + os.oauth2_session_id AS "oauth2_session_id!", os.oauth2_client_id AS "oauth2_client_id!", os.scope AS "scope!", - us.id AS "user_session_id!", + us.user_session_id AS "user_session_id!", us.created_at AS "user_session_created_at!", - u.id AS "user_id!", + u.user_id AS "user_id!", u.username AS "user_username!", - usa.id AS "user_session_last_authentication_id?", + usa.user_session_authentication_id AS "user_session_last_authentication_id?", usa.created_at AS "user_session_last_authentication_created_at?", - ue.id AS "user_email_id?", + ue.user_email_id AS "user_email_id?", ue.email AS "user_email?", ue.created_at AS "user_email_created_at?", ue.confirmed_at AS "user_email_confirmed_at?" FROM oauth2_access_tokens at INNER JOIN oauth2_sessions os - ON os.id = at.oauth2_session_id + USING (oauth2_session_id) INNER JOIN user_sessions us - ON us.id = os.user_session_id + USING (user_session_id) INNER JOIN users u - ON u.id = us.user_id + USING (user_id) LEFT JOIN user_session_authentications usa - ON usa.session_id = us.id + USING (user_session_id) LEFT JOIN user_emails ue - ON ue.id = u.primary_email_id + ON ue.user_email_id = u.primary_user_email_id - WHERE at.token = $1 - AND at.created_at + (at.expires_after * INTERVAL '1 second') >= now() - AND us.active - AND os.ended_at IS NULL + WHERE at.access_token = $1 + AND at.revoked_at IS NULL + AND os.finished_at IS NULL ORDER BY usa.created_at DESC LIMIT 1 @@ -158,14 +171,14 @@ where .await?; let access_token = AccessToken { - data: res.access_token_id, - jti: format!("{}", res.access_token_id), - token: res.access_token, - created_at: res.access_token_created_at, - expires_after: Duration::seconds(res.access_token_expires_after.into()), + data: res.oauth2_access_token_id.into(), + jti: res.oauth2_access_token_id.to_string(), + access_token: res.oauth2_access_token, + created_at: res.oauth2_access_token_created_at, + expires_at: res.oauth2_access_token_expires_at, }; - let client = lookup_client(&mut *conn, res.oauth2_client_id).await?; + let client = lookup_client(&mut *conn, res.oauth2_client_id.into()).await?; let primary_email = match ( res.user_email_id, @@ -174,7 +187,7 @@ where res.user_email_confirmed_at, ) { (Some(id), Some(email), Some(created_at), confirmed_at) => Some(UserEmail { - data: id, + data: id.into(), email, created_at, confirmed_at, @@ -183,10 +196,11 @@ where _ => return Err(DatabaseInconsistencyError.into()), }; + let id = Ulid::from(res.user_id); let user = User { - data: res.user_id, + data: id, username: res.user_username, - sub: format!("fake-sub-{}", res.user_id), + sub: id.to_string(), primary_email, }; @@ -196,14 +210,14 @@ where ) { (None, None) => None, (Some(id), Some(created_at)) => Some(Authentication { - data: id, + data: id.into(), created_at, }), _ => return Err(DatabaseInconsistencyError.into()), }; let browser_session = BrowserSession { - data: res.user_session_id, + data: res.user_session_id.into(), created_at: res.user_session_created_at, user, last_authentication, @@ -212,7 +226,7 @@ where let scope = res.scope.parse().map_err(|_e| DatabaseInconsistencyError)?; let session = Session { - data: res.session_id, + data: res.oauth2_session_id.into(), client, browser_session, scope, @@ -222,16 +236,24 @@ where } } +#[tracing::instrument( + skip_all, + fields(access_token.id = %access_token.data), + err(Debug), +)] pub async fn revoke_access_token( executor: impl PgExecutor<'_>, - access_token: &AccessToken, + access_token: AccessToken, ) -> anyhow::Result<()> { + let revoked_at = Utc::now(); let res = sqlx::query!( r#" - DELETE FROM oauth2_access_tokens - WHERE id = $1 + UPDATE oauth2_access_tokens + SET revoked_at = $2 + WHERE oauth2_access_token_id = $1 "#, - access_token.data, + Uuid::from(access_token.data), + revoked_at, ) .execute(executor) .await @@ -245,11 +267,14 @@ pub async fn revoke_access_token( } pub async fn cleanup_expired(executor: impl PgExecutor<'_>) -> anyhow::Result { + // Cleanup token which expired more than 15 minutes ago + let threshold = Utc::now() - Duration::minutes(15); let res = sqlx::query!( r#" DELETE FROM oauth2_access_tokens - WHERE created_at + (expires_after * INTERVAL '1 second') + INTERVAL '15 minutes' < now() + WHERE expires_at < $1 "#, + threshold, ) .execute(executor) .await diff --git a/crates/storage/src/oauth2/authorization_grant.rs b/crates/storage/src/oauth2/authorization_grant.rs index 8b788160..ef37f6aa 100644 --- a/crates/storage/src/oauth2/authorization_grant.rs +++ b/crates/storage/src/oauth2/authorization_grant.rs @@ -25,11 +25,21 @@ use mas_data_model::{ use mas_iana::oauth::PkceCodeChallengeMethod; use oauth2_types::{requests::ResponseMode, scope::Scope}; use sqlx::{PgConnection, PgExecutor}; +use ulid::Ulid; use url::Url; +use uuid::Uuid; use super::client::lookup_client; -use crate::{DatabaseInconsistencyError, IdAndCreationTime, PostgresqlBackend}; +use crate::{DatabaseInconsistencyError, PostgresqlBackend}; +#[tracing::instrument( + skip_all, + fields( + client.id = %client.data, + grant.id, + ), + err(Debug), +)] #[allow(clippy::too_many_arguments)] pub async fn new_authorization_grant( executor: impl PgExecutor<'_>, @@ -40,7 +50,7 @@ pub async fn new_authorization_grant( state: Option, nonce: Option, max_age: Option, - acr_values: Option, + _acr_values: Option, response_mode: ResponseMode, response_type_id_token: bool, requires_consent: bool, @@ -53,26 +63,43 @@ pub async fn new_authorization_grant( .as_ref() .and_then(|c| c.pkce.as_ref()) .map(|p| p.challenge_method.to_string()); + // TODO: this conversion is a bit ugly + let max_age_i32 = max_age.map(|x| i32::try_from(u32::from(x)).unwrap_or(i32::MAX)); let code_str = code.as_ref().map(|c| &c.code); - let res = sqlx::query_as!( - IdAndCreationTime, + + let created_at = Utc::now(); + let id = Ulid::from_datetime(created_at.into()); + tracing::Span::current().record("grant.id", tracing::field::display(id)); + + sqlx::query!( r#" - INSERT INTO oauth2_authorization_grants - (oauth2_client_id, redirect_uri, scope, state, nonce, max_age, - acr_values, response_mode, code_challenge, code_challenge_method, - response_type_code, response_type_id_token, code, requires_consent) + INSERT INTO oauth2_authorization_grants ( + oauth2_authorization_grant_id, + oauth2_client_id, + redirect_uri, + scope, + state, + nonce, + max_age, + response_mode, + code_challenge, + code_challenge_method, + response_type_code, + response_type_id_token, + authorization_code, + requires_consent, + created_at + ) VALUES - ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14) - RETURNING id, created_at + ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15) "#, - &client.data, + Uuid::from(id), + Uuid::from(client.data), redirect_uri.to_string(), scope.to_string(), state, nonce, - // TODO: this conversion is a bit ugly - max_age.map(|x| i32::try_from(u32::from(x)).unwrap_or(i32::MAX)), - acr_values, + max_age_i32, response_mode.to_string(), code_challenge, code_challenge_method, @@ -80,13 +107,14 @@ pub async fn new_authorization_grant( response_type_id_token, code_str, requires_consent, + created_at, ) - .fetch_one(executor) + .execute(executor) .await .context("could not insert oauth2 authorization grant")?; Ok(AuthorizationGrant { - data: res.id, + data: id, stage: AuthorizationGrantStage::Pending, code, redirect_uri, @@ -95,9 +123,8 @@ pub async fn new_authorization_grant( state, nonce, max_age, - acr_values, response_mode, - created_at: res.created_at, + created_at, response_type_id_token, requires_consent, }) @@ -105,33 +132,32 @@ pub async fn new_authorization_grant( #[allow(clippy::struct_excessive_bools)] struct GrantLookup { - grant_id: i64, - grant_created_at: DateTime, - grant_cancelled_at: Option>, - grant_fulfilled_at: Option>, - grant_exchanged_at: Option>, - grant_scope: String, - grant_state: Option, - grant_redirect_uri: String, - grant_response_mode: String, - grant_nonce: Option, - grant_max_age: Option, - grant_acr_values: Option, - grant_response_type_code: bool, - grant_response_type_id_token: bool, - grant_code: Option, - grant_code_challenge: Option, - grant_code_challenge_method: Option, - grant_requires_consent: bool, - oauth2_client_id: i64, - session_id: Option, - user_session_id: Option, + oauth2_authorization_grant_id: Uuid, + oauth2_authorization_grant_created_at: DateTime, + oauth2_authorization_grant_cancelled_at: Option>, + oauth2_authorization_grant_fulfilled_at: Option>, + oauth2_authorization_grant_exchanged_at: Option>, + oauth2_authorization_grant_scope: String, + oauth2_authorization_grant_state: Option, + oauth2_authorization_grant_nonce: Option, + oauth2_authorization_grant_redirect_uri: String, + oauth2_authorization_grant_response_mode: String, + oauth2_authorization_grant_max_age: Option, + oauth2_authorization_grant_response_type_code: bool, + oauth2_authorization_grant_response_type_id_token: bool, + oauth2_authorization_grant_code: Option, + oauth2_authorization_grant_code_challenge: Option, + oauth2_authorization_grant_code_challenge_method: Option, + oauth2_authorization_grant_requires_consent: bool, + oauth2_client_id: Uuid, + oauth2_session_id: Option, + user_session_id: Option, user_session_created_at: Option>, - user_id: Option, + user_id: Option, user_username: Option, - user_session_last_authentication_id: Option, + user_session_last_authentication_id: Option, user_session_last_authentication_created_at: Option>, - user_email_id: Option, + user_email_id: Option, user_email: Option, user_email_created_at: Option>, user_email_confirmed_at: Option>, @@ -144,12 +170,12 @@ impl GrantLookup { executor: impl PgExecutor<'_>, ) -> Result, DatabaseInconsistencyError> { let scope: Scope = self - .grant_scope + .oauth2_authorization_grant_scope .parse() .map_err(|_e| DatabaseInconsistencyError)?; // TODO: don't unwrap - let client = lookup_client(executor, self.oauth2_client_id) + let client = lookup_client(executor, self.oauth2_client_id.into()) .await .unwrap(); @@ -158,7 +184,7 @@ impl GrantLookup { self.user_session_last_authentication_created_at, ) { (Some(id), Some(created_at)) => Some(Authentication { - data: id, + data: id.into(), created_at, }), (None, None) => None, @@ -172,7 +198,7 @@ impl GrantLookup { self.user_email_confirmed_at, ) { (Some(id), Some(email), Some(created_at), confirmed_at) => Some(UserEmail { - data: id, + data: id.into(), email, created_at, confirmed_at, @@ -182,7 +208,7 @@ impl GrantLookup { }; let session = match ( - self.session_id, + self.oauth2_session_id, self.user_session_id, self.user_session_created_at, self.user_id, @@ -199,15 +225,16 @@ impl GrantLookup { last_authentication, primary_email, ) => { + let user_id = Ulid::from(user_id); let user = User { data: user_id, username: user_username, - sub: format!("fake-sub-{}", user_id), + sub: user_id.to_string(), primary_email, }; let browser_session = BrowserSession { - data: user_session_id, + data: user_session_id.into(), user, created_at: user_session_created_at, last_authentication, @@ -217,7 +244,7 @@ impl GrantLookup { let scope = scope.clone(); let session = Session { - data: session_id, + data: session_id.into(), client, browser_session, scope, @@ -230,9 +257,9 @@ impl GrantLookup { }; let stage = match ( - self.grant_fulfilled_at, - self.grant_exchanged_at, - self.grant_cancelled_at, + self.oauth2_authorization_grant_fulfilled_at, + self.oauth2_authorization_grant_exchanged_at, + self.oauth2_authorization_grant_cancelled_at, session, ) { (None, None, None, None) => AuthorizationGrantStage::Pending, @@ -255,7 +282,10 @@ impl GrantLookup { } }; - let pkce = match (self.grant_code_challenge, self.grant_code_challenge_method) { + let pkce = match ( + self.oauth2_authorization_grant_code_challenge, + self.oauth2_authorization_grant_code_challenge_method, + ) { (Some(challenge), Some(challenge_method)) if challenge_method == "plain" => { Some(Pkce { challenge_method: PkceCodeChallengeMethod::Plain, @@ -272,27 +302,30 @@ impl GrantLookup { } }; - let code: Option = - match (self.grant_response_type_code, self.grant_code, pkce) { - (false, None, None) => None, - (true, Some(code), pkce) => Some(AuthorizationCode { code, pkce }), - _ => { - return Err(DatabaseInconsistencyError); - } - }; + let code: Option = match ( + self.oauth2_authorization_grant_response_type_code, + self.oauth2_authorization_grant_code, + pkce, + ) { + (false, None, None) => None, + (true, Some(code), pkce) => Some(AuthorizationCode { code, pkce }), + _ => { + return Err(DatabaseInconsistencyError); + } + }; let redirect_uri = self - .grant_redirect_uri + .oauth2_authorization_grant_redirect_uri .parse() .map_err(|_e| DatabaseInconsistencyError)?; let response_mode = self - .grant_response_mode + .oauth2_authorization_grant_response_mode .parse() .map_err(|_e| DatabaseInconsistencyError)?; let max_age = self - .grant_max_age + .oauth2_authorization_grant_max_age .map(u32::try_from) .transpose() .map_err(|_e| DatabaseInconsistencyError)? @@ -301,82 +334,85 @@ impl GrantLookup { .map_err(|_e| DatabaseInconsistencyError)?; Ok(AuthorizationGrant { - data: self.grant_id, + data: self.oauth2_authorization_grant_id.into(), stage, client, code, - acr_values: self.grant_acr_values, scope, - state: self.grant_state, - nonce: self.grant_nonce, + state: self.oauth2_authorization_grant_state, + nonce: self.oauth2_authorization_grant_nonce, max_age, // TODO response_mode, redirect_uri, - created_at: self.grant_created_at, - response_type_id_token: self.grant_response_type_id_token, - requires_consent: self.grant_requires_consent, + created_at: self.oauth2_authorization_grant_created_at, + response_type_id_token: self.oauth2_authorization_grant_response_type_id_token, + requires_consent: self.oauth2_authorization_grant_requires_consent, }) } } +#[tracing::instrument( + skip_all, + fields(grant.id = %id), + err(Debug), +)] pub async fn get_grant_by_id( conn: &mut PgConnection, - id: i64, + id: Ulid, ) -> anyhow::Result> { // TODO: handle "not found" cases let res = sqlx::query_as!( GrantLookup, r#" SELECT - og.id AS grant_id, - og.created_at AS grant_created_at, - og.cancelled_at AS grant_cancelled_at, - og.fulfilled_at AS grant_fulfilled_at, - og.exchanged_at AS grant_exchanged_at, - og.scope AS grant_scope, - og.state AS grant_state, - og.redirect_uri AS grant_redirect_uri, - og.response_mode AS grant_response_mode, - og.nonce AS grant_nonce, - og.max_age AS grant_max_age, - og.acr_values AS grant_acr_values, - og.oauth2_client_id AS oauth2_client_id, - og.code AS grant_code, - og.response_type_code AS grant_response_type_code, - og.response_type_id_token AS grant_response_type_id_token, - og.code_challenge AS grant_code_challenge, - og.code_challenge_method AS grant_code_challenge_method, - og.requires_consent AS grant_requires_consent, - os.id AS "session_id?", - us.id AS "user_session_id?", - us.created_at AS "user_session_created_at?", - u.id AS "user_id?", - u.username AS "user_username?", - usa.id AS "user_session_last_authentication_id?", - usa.created_at AS "user_session_last_authentication_created_at?", - ue.id AS "user_email_id?", - ue.email AS "user_email?", - ue.created_at AS "user_email_created_at?", - ue.confirmed_at AS "user_email_confirmed_at?" + og.oauth2_authorization_grant_id, + og.created_at AS oauth2_authorization_grant_created_at, + og.cancelled_at AS oauth2_authorization_grant_cancelled_at, + og.fulfilled_at AS oauth2_authorization_grant_fulfilled_at, + og.exchanged_at AS oauth2_authorization_grant_exchanged_at, + og.scope AS oauth2_authorization_grant_scope, + og.state AS oauth2_authorization_grant_state, + og.redirect_uri AS oauth2_authorization_grant_redirect_uri, + og.response_mode AS oauth2_authorization_grant_response_mode, + og.nonce AS oauth2_authorization_grant_nonce, + og.max_age AS oauth2_authorization_grant_max_age, + og.oauth2_client_id AS oauth2_client_id, + og.authorization_code AS oauth2_authorization_grant_code, + og.response_type_code AS oauth2_authorization_grant_response_type_code, + og.response_type_id_token AS oauth2_authorization_grant_response_type_id_token, + og.code_challenge AS oauth2_authorization_grant_code_challenge, + og.code_challenge_method AS oauth2_authorization_grant_code_challenge_method, + og.requires_consent AS oauth2_authorization_grant_requires_consent, + os.oauth2_session_id AS "oauth2_session_id?", + us.user_session_id AS "user_session_id?", + us.created_at AS "user_session_created_at?", + u.user_id AS "user_id?", + u.username AS "user_username?", + usa.user_session_authentication_id AS "user_session_last_authentication_id?", + usa.created_at AS "user_session_last_authentication_created_at?", + ue.user_email_id AS "user_email_id?", + ue.email AS "user_email?", + ue.created_at AS "user_email_created_at?", + ue.confirmed_at AS "user_email_confirmed_at?" FROM oauth2_authorization_grants og LEFT JOIN oauth2_sessions os - ON os.id = og.oauth2_session_id + USING (oauth2_session_id) LEFT JOIN user_sessions us - ON us.id = os.user_session_id + USING (user_session_id) LEFT JOIN users u - ON u.id = us.user_id + USING (user_id) LEFT JOIN user_session_authentications usa - ON usa.session_id = us.id + USING (user_session_id) LEFT JOIN user_emails ue - ON ue.id = u.primary_email_id + ON ue.user_email_id = u.primary_user_email_id - WHERE og.id = $1 + WHERE og.oauth2_authorization_grant_id = $1 ORDER BY usa.created_at DESC LIMIT 1 "#, - id, + Uuid::from(id), ) .fetch_one(&mut *conn) .await @@ -387,6 +423,7 @@ pub async fn get_grant_by_id( Ok(grant) } +#[tracing::instrument(skip_all, err(Debug))] pub async fn lookup_grant_by_code( conn: &mut PgConnection, code: &str, @@ -396,50 +433,49 @@ pub async fn lookup_grant_by_code( GrantLookup, r#" SELECT - og.id AS grant_id, - og.created_at AS grant_created_at, - og.cancelled_at AS grant_cancelled_at, - og.fulfilled_at AS grant_fulfilled_at, - og.exchanged_at AS grant_exchanged_at, - og.scope AS grant_scope, - og.state AS grant_state, - og.redirect_uri AS grant_redirect_uri, - og.response_mode AS grant_response_mode, - og.nonce AS grant_nonce, - og.max_age AS grant_max_age, - og.acr_values AS grant_acr_values, - og.oauth2_client_id AS oauth2_client_id, - og.code AS grant_code, - og.response_type_code AS grant_response_type_code, - og.response_type_id_token AS grant_response_type_id_token, - og.code_challenge AS grant_code_challenge, - og.code_challenge_method AS grant_code_challenge_method, - og.requires_consent AS grant_requires_consent, - os.id AS "session_id?", - us.id AS "user_session_id?", - us.created_at AS "user_session_created_at?", - u.id AS "user_id?", - u.username AS "user_username?", - usa.id AS "user_session_last_authentication_id?", - usa.created_at AS "user_session_last_authentication_created_at?", - ue.id AS "user_email_id?", - ue.email AS "user_email?", - ue.created_at AS "user_email_created_at?", - ue.confirmed_at AS "user_email_confirmed_at?" + og.oauth2_authorization_grant_id, + og.created_at AS oauth2_authorization_grant_created_at, + og.cancelled_at AS oauth2_authorization_grant_cancelled_at, + og.fulfilled_at AS oauth2_authorization_grant_fulfilled_at, + og.exchanged_at AS oauth2_authorization_grant_exchanged_at, + og.scope AS oauth2_authorization_grant_scope, + og.state AS oauth2_authorization_grant_state, + og.redirect_uri AS oauth2_authorization_grant_redirect_uri, + og.response_mode AS oauth2_authorization_grant_response_mode, + og.nonce AS oauth2_authorization_grant_nonce, + og.max_age AS oauth2_authorization_grant_max_age, + og.oauth2_client_id AS oauth2_client_id, + og.authorization_code AS oauth2_authorization_grant_code, + og.response_type_code AS oauth2_authorization_grant_response_type_code, + og.response_type_id_token AS oauth2_authorization_grant_response_type_id_token, + og.code_challenge AS oauth2_authorization_grant_code_challenge, + og.code_challenge_method AS oauth2_authorization_grant_code_challenge_method, + og.requires_consent AS oauth2_authorization_grant_requires_consent, + os.oauth2_session_id AS "oauth2_session_id?", + us.user_session_id AS "user_session_id?", + us.created_at AS "user_session_created_at?", + u.user_id AS "user_id?", + u.username AS "user_username?", + usa.user_session_authentication_id AS "user_session_last_authentication_id?", + usa.created_at AS "user_session_last_authentication_created_at?", + ue.user_email_id AS "user_email_id?", + ue.email AS "user_email?", + ue.created_at AS "user_email_created_at?", + ue.confirmed_at AS "user_email_confirmed_at?" FROM oauth2_authorization_grants og LEFT JOIN oauth2_sessions os - ON os.id = og.oauth2_session_id + USING (oauth2_session_id) LEFT JOIN user_sessions us - ON us.id = os.user_session_id + USING (user_session_id) LEFT JOIN users u - ON u.id = us.user_id + USING (user_id) LEFT JOIN user_session_authentications usa - ON usa.session_id = us.id + USING (user_session_id) LEFT JOIN user_emails ue - ON ue.id = u.primary_email_id + ON ue.user_email_id = u.primary_user_email_id - WHERE og.code = $1 + WHERE og.authorization_code = $1 ORDER BY usa.created_at DESC LIMIT 1 @@ -455,41 +491,69 @@ pub async fn lookup_grant_by_code( Ok(grant) } +#[tracing::instrument( + skip_all, + fields( + grant.id = %grant.data, + client.id = %grant.client.data, + session.id, + user_session.id = %browser_session.data, + user.id = %browser_session.user.data, + ), + err(Debug), +)] pub async fn derive_session( executor: impl PgExecutor<'_>, grant: &AuthorizationGrant, browser_session: BrowserSession, ) -> anyhow::Result> { - let res = sqlx::query_as!( - IdAndCreationTime, + let created_at = Utc::now(); + let id = Ulid::from_datetime(created_at.into()); + tracing::Span::current().record("session.id", tracing::field::display(id)); + + sqlx::query!( r#" INSERT INTO oauth2_sessions - (user_session_id, oauth2_client_id, scope) + (oauth2_session_id, user_session_id, oauth2_client_id, scope, created_at) SELECT $1, + $2, og.oauth2_client_id, - og.scope + og.scope, + $3 FROM oauth2_authorization_grants og WHERE - og.id = $2 - RETURNING id, created_at + og.oauth2_authorization_grant_id = $4 "#, - browser_session.data, - grant.data, + Uuid::from(id), + Uuid::from(browser_session.data), + created_at, + Uuid::from(grant.data), ) - .fetch_one(executor) + .execute(executor) .await .context("could not insert oauth2 session")?; Ok(Session { - data: res.id, + data: id, browser_session, client: grant.client.clone(), scope: grant.scope.clone(), }) } +#[tracing::instrument( + skip_all, + fields( + grant.id = %grant.data, + client.id = %grant.client.data, + session.id = %session.data, + user_session.id = %session.browser_session.data, + user.id = %session.browser_session.user.data, + ), + err(Debug), +)] pub async fn fulfill_grant( executor: impl PgExecutor<'_>, mut grant: AuthorizationGrant, @@ -499,15 +563,16 @@ pub async fn fulfill_grant( r#" UPDATE oauth2_authorization_grants AS og SET - oauth2_session_id = os.id, + oauth2_session_id = os.oauth2_session_id, fulfilled_at = os.created_at FROM oauth2_sessions os WHERE - og.id = $1 AND os.id = $2 + og.oauth2_authorization_grant_id = $1 + AND os.oauth2_session_id = $2 RETURNING fulfilled_at AS "fulfilled_at!: DateTime" "#, - grant.data, - session.data, + Uuid::from(grant.data), + Uuid::from(session.data), ) .fetch_one(executor) .await @@ -518,6 +583,14 @@ pub async fn fulfill_grant( Ok(grant) } +#[tracing::instrument( + skip_all, + fields( + grant.id = %grant.data, + client.id = %grant.client.data, + ), + err(Debug), +)] pub async fn give_consent_to_grant( executor: impl PgExecutor<'_>, mut grant: AuthorizationGrant, @@ -528,9 +601,9 @@ pub async fn give_consent_to_grant( SET requires_consent = 'f' WHERE - og.id = $1 + og.oauth2_authorization_grant_id = $1 "#, - grant.data, + Uuid::from(grant.data), ) .execute(executor) .await?; @@ -540,22 +613,29 @@ pub async fn give_consent_to_grant( Ok(grant) } +#[tracing::instrument( + skip_all, + fields( + grant.id = %grant.data, + client.id = %grant.client.data, + ), + err(Debug), +)] pub async fn exchange_grant( executor: impl PgExecutor<'_>, mut grant: AuthorizationGrant, ) -> anyhow::Result> { - let exchanged_at = sqlx::query_scalar!( + let exchanged_at = Utc::now(); + sqlx::query!( r#" UPDATE oauth2_authorization_grants - SET - exchanged_at = NOW() - WHERE - id = $1 - RETURNING exchanged_at AS "exchanged_at!: DateTime" + SET exchanged_at = $2 + WHERE oauth2_authorization_grant_id = $1 "#, - grant.data, + Uuid::from(grant.data), + exchanged_at, ) - .fetch_one(executor) + .execute(executor) .await .context("could not mark grant as exchanged")?; diff --git a/crates/storage/src/oauth2/client.rs b/crates/storage/src/oauth2/client.rs index bafe461a..cc99f622 100644 --- a/crates/storage/src/oauth2/client.rs +++ b/crates/storage/src/oauth2/client.rs @@ -20,23 +20,25 @@ use mas_iana::{ oauth::{OAuthAuthorizationEndpointResponseType, OAuthClientAuthenticationMethod}, }; use mas_jose::jwk::PublicJsonWebKeySet; -use oauth2_types::{requests::GrantType, response_type::ResponseType}; +use oauth2_types::requests::GrantType; use sqlx::{PgConnection, PgExecutor}; use thiserror::Error; +use ulid::Ulid; use url::Url; +use uuid::Uuid; use crate::PostgresqlBackend; +// XXX: response_types & contacts #[derive(Debug)] pub struct OAuth2ClientLookup { - id: i64, - client_id: String, + oauth2_client_id: Uuid, encrypted_client_secret: Option, redirect_uris: Vec, - response_types: Vec, + // response_types: Vec, grant_type_authorization_code: bool, grant_type_refresh_token: bool, - contacts: Vec, + // contacts: Vec, client_name: Option, logo_uri: Option, client_uri: Option, @@ -53,6 +55,9 @@ pub struct OAuth2ClientLookup { #[derive(Debug, Error)] pub enum ClientFetchError { + #[error("invalid client ID")] + InvalidClientId(#[from] ulid::DecodeError), + #[error("malformed jwks column")] MalformedJwks(#[source] serde_json::Error), @@ -78,7 +83,10 @@ pub enum ClientFetchError { impl ClientFetchError { #[must_use] pub fn not_found(&self) -> bool { - matches!(self, Self::Database(sqlx::Error::RowNotFound)) + matches!( + self, + Self::Database(sqlx::Error::RowNotFound) | Self::InvalidClientId(_) + ) } } @@ -94,12 +102,19 @@ impl TryInto> for OAuth2ClientLookup { source, })?; + let response_types = vec![ + OAuthAuthorizationEndpointResponseType::Code, + OAuthAuthorizationEndpointResponseType::IdToken, + OAuthAuthorizationEndpointResponseType::None, + ]; + /* XXX let response_types: Result, _> = self.response_types.iter().map(|s| s.parse()).collect(); let response_types = response_types.map_err(|source| ClientFetchError::ParseField { field: "response_types", source, })?; + */ let mut grant_types = Vec::new(); if self.grant_type_authorization_code { @@ -210,13 +225,14 @@ impl TryInto> for OAuth2ClientLookup { }; Ok(Client { - data: self.id, - client_id: self.client_id, + data: self.oauth2_client_id.into(), + client_id: self.oauth2_client_id.to_string(), encrypted_client_secret: self.encrypted_client_secret, redirect_uris, response_types, grant_types, - contacts: self.contacts, + // contacts: self.contacts, + contacts: vec![], client_name: self.client_name, logo_uri, client_uri, @@ -234,20 +250,21 @@ impl TryInto> for OAuth2ClientLookup { pub async fn lookup_client( executor: impl PgExecutor<'_>, - id: i64, + id: Ulid, ) -> Result, ClientFetchError> { let res = sqlx::query_as!( OAuth2ClientLookup, r#" SELECT - c.id, - c.client_id, + c.oauth2_client_id, c.encrypted_client_secret, - ARRAY(SELECT redirect_uri FROM oauth2_client_redirect_uris r WHERE r.oauth2_client_id = c.id) AS "redirect_uris!", - c.response_types, + ARRAY( + SELECT redirect_uri + FROM oauth2_client_redirect_uris r + WHERE r.oauth2_client_id = c.oauth2_client_id + ) AS "redirect_uris!", c.grant_type_authorization_code, c.grant_type_refresh_token, - c.contacts, c.client_name, c.logo_uri, c.client_uri, @@ -262,9 +279,9 @@ pub async fn lookup_client( c.initiate_login_uri FROM oauth2_clients c - WHERE c.id = $1 + WHERE c.oauth2_client_id = $1 "#, - id, + Uuid::from(id), ) .fetch_one(executor) .await?; @@ -278,53 +295,18 @@ pub async fn lookup_client_by_client_id( executor: impl PgExecutor<'_>, client_id: &str, ) -> Result, ClientFetchError> { - let res = sqlx::query_as!( - OAuth2ClientLookup, - r#" - SELECT - c.id, - c.client_id, - c.encrypted_client_secret, - ARRAY(SELECT redirect_uri FROM oauth2_client_redirect_uris r WHERE r.oauth2_client_id = c.id) AS "redirect_uris!", - c.response_types, - c.grant_type_authorization_code, - c.grant_type_refresh_token, - c.contacts, - c.client_name, - c.logo_uri, - c.client_uri, - c.policy_uri, - c.tos_uri, - c.jwks_uri, - c.jwks, - c.id_token_signed_response_alg, - c.userinfo_signed_response_alg, - c.token_endpoint_auth_method, - c.token_endpoint_auth_signing_alg, - c.initiate_login_uri - FROM oauth2_clients c - - WHERE c.client_id = $1 - "#, - client_id, - ) - .fetch_one(executor) - .await?; - - let client = res.try_into()?; - - Ok(client) + let id: Ulid = client_id.parse()?; + lookup_client(executor, id).await } #[allow(clippy::too_many_arguments)] pub async fn insert_client( conn: &mut PgConnection, - client_id: &str, + client_id: Ulid, redirect_uris: &[Url], encrypted_client_secret: Option<&str>, - response_types: &[ResponseType], grant_types: &[GrantType], - contacts: &[String], + _contacts: &[String], client_name: Option<&str>, logo_uri: Option<&Url>, client_uri: Option<&Url>, @@ -338,7 +320,6 @@ pub async fn insert_client( token_endpoint_auth_signing_alg: Option<&JsonWebSignatureAlg>, initiate_login_uri: Option<&Url>, ) -> Result<(), sqlx::Error> { - let response_types: Vec = response_types.iter().map(ToString::to_string).collect(); let grant_type_authorization_code = grant_types.contains(&GrantType::AuthorizationCode); let grant_type_refresh_token = grant_types.contains(&GrantType::RefreshToken); let logo_uri = logo_uri.map(Url::as_str); @@ -353,15 +334,13 @@ pub async fn insert_client( let token_endpoint_auth_signing_alg = token_endpoint_auth_signing_alg.map(ToString::to_string); let initiate_login_uri = initiate_login_uri.map(Url::as_str); - let id = sqlx::query_scalar!( + sqlx::query!( r#" INSERT INTO oauth2_clients - (client_id, + (oauth2_client_id, encrypted_client_secret, - response_types, grant_type_authorization_code, grant_type_refresh_token, - contacts, client_name, logo_uri, client_uri, @@ -375,15 +354,12 @@ pub async fn insert_client( token_endpoint_auth_signing_alg, initiate_login_uri) VALUES - ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18) - RETURNING id + ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16) "#, - client_id, + Uuid::from(client_id), encrypted_client_secret, - &response_types, grant_type_authorization_code, grant_type_refresh_token, - contacts, client_name, logo_uri, client_uri, @@ -397,96 +373,87 @@ pub async fn insert_client( token_endpoint_auth_signing_alg, initiate_login_uri, ) - .fetch_one(&mut *conn) - .await?; - - let redirect_uris: Vec = redirect_uris.iter().map(ToString::to_string).collect(); - - sqlx::query!( - r#" - INSERT INTO oauth2_client_redirect_uris (oauth2_client_id, redirect_uri) - SELECT $1, uri FROM UNNEST($2::text[]) uri - "#, - id, - &redirect_uris, - ) .execute(&mut *conn) .await?; + for redirect_uri in redirect_uris { + let id = Ulid::new(); + sqlx::query!( + r#" + INSERT INTO oauth2_client_redirect_uris + (oauth2_client_redirect_uri_id, oauth2_client_id, redirect_uri) + VALUES ($1, $2, $3) + "#, + Uuid::from(id), + Uuid::from(client_id), + redirect_uri.as_str(), + ) + .execute(&mut *conn) + .await?; + } + Ok(()) } pub async fn insert_client_from_config( conn: &mut PgConnection, - client_id: &str, + client_id: Ulid, client_auth_method: OAuthClientAuthenticationMethod, encrypted_client_secret: Option<&str>, jwks: Option<&PublicJsonWebKeySet>, jwks_uri: Option<&Url>, redirect_uris: &[Url], ) -> anyhow::Result<()> { - let response_types = vec![ - OAuthAuthorizationEndpointResponseType::Code.to_string(), - OAuthAuthorizationEndpointResponseType::CodeIdToken.to_string(), - OAuthAuthorizationEndpointResponseType::CodeIdTokenToken.to_string(), - OAuthAuthorizationEndpointResponseType::CodeToken.to_string(), - OAuthAuthorizationEndpointResponseType::IdToken.to_string(), - OAuthAuthorizationEndpointResponseType::IdTokenToken.to_string(), - OAuthAuthorizationEndpointResponseType::None.to_string(), - OAuthAuthorizationEndpointResponseType::Token.to_string(), - ]; - let jwks = jwks.map(serde_json::to_value).transpose()?; let jwks_uri = jwks_uri.map(Url::as_str); let client_auth_method = client_auth_method.to_string(); - let id = sqlx::query_scalar!( + sqlx::query!( r#" INSERT INTO oauth2_clients - (client_id, + (oauth2_client_id, encrypted_client_secret, - response_types, grant_type_authorization_code, grant_type_refresh_token, token_endpoint_auth_method, jwks, - jwks_uri, - contacts) + jwks_uri) VALUES - ($1, $2, $3, $4, $5, $6, $7, $8, '{}') - RETURNING id + ($1, $2, $3, $4, $5, $6, $7) "#, - client_id, + Uuid::from(client_id), encrypted_client_secret, - &response_types, true, true, client_auth_method, jwks, jwks_uri, ) - .fetch_one(&mut *conn) - .await?; - - let redirect_uris: Vec = redirect_uris.iter().map(ToString::to_string).collect(); - - sqlx::query!( - r#" - INSERT INTO oauth2_client_redirect_uris (oauth2_client_id, redirect_uri) - SELECT $1, uri FROM UNNEST($2::text[]) uri - "#, - id, - &redirect_uris, - ) .execute(&mut *conn) .await?; + for redirect_uri in redirect_uris { + let id = Ulid::new(); + sqlx::query!( + r#" + INSERT INTO oauth2_client_redirect_uris + (oauth2_client_redirect_uri_id, oauth2_client_id, redirect_uri) + VALUES ($1, $2, $3) + "#, + Uuid::from(id), + Uuid::from(client_id), + redirect_uri.as_str(), + ) + .execute(&mut *conn) + .await?; + } + Ok(()) } pub async fn truncate_clients(executor: impl PgExecutor<'_>) -> anyhow::Result<()> { - sqlx::query!("TRUNCATE oauth2_client_redirect_uris, oauth2_clients RESTART IDENTITY CASCADE") + sqlx::query!("TRUNCATE oauth2_client_redirect_uris, oauth2_clients CASCADE") .execute(executor) .await?; Ok(()) diff --git a/crates/storage/src/oauth2/consent.rs b/crates/storage/src/oauth2/consent.rs index 1fd8bb45..2a5ca2c7 100644 --- a/crates/storage/src/oauth2/consent.rs +++ b/crates/storage/src/oauth2/consent.rs @@ -14,9 +14,12 @@ use std::str::FromStr; +use chrono::Utc; use mas_data_model::{Client, User}; use oauth2_types::scope::{Scope, ScopeToken}; use sqlx::PgExecutor; +use ulid::Ulid; +use uuid::Uuid; use crate::PostgresqlBackend; @@ -31,8 +34,8 @@ pub async fn fetch_client_consent( FROM oauth2_consents WHERE user_id = $1 AND oauth2_client_id = $2 "#, - user.data, - client.data, + Uuid::from(user.data), + Uuid::from(client.data), ) .fetch_all(executor) .await?; @@ -51,17 +54,29 @@ pub async fn insert_client_consent( client: &Client, scope: &Scope, ) -> anyhow::Result<()> { - let tokens: Vec = scope.iter().map(ToString::to_string).collect(); + let now = Utc::now(); + let (tokens, ids): (Vec, Vec) = scope + .iter() + .map(|token| { + ( + token.to_string(), + Uuid::from(Ulid::from_datetime(now.into())), + ) + }) + .unzip(); sqlx::query!( r#" - INSERT INTO oauth2_consents (user_id, oauth2_client_id, scope_token) - SELECT $1, $2, scope_token FROM UNNEST($3::text[]) scope_token - ON CONFLICT (user_id, oauth2_client_id, scope_token) DO UPDATE SET updated_at = NOW() + INSERT INTO oauth2_consents + (oauth2_consent_id, user_id, oauth2_client_id, scope_token, created_at) + SELECT id, $2, $3, scope_token, $5 FROM UNNEST($1::uuid[], $4::text[]) u(id, scope_token) + ON CONFLICT (user_id, oauth2_client_id, scope_token) DO UPDATE SET refreshed_at = $5 "#, - user.data, - client.data, + &ids, + Uuid::from(user.data), + Uuid::from(client.data), &tokens, + now, ) .execute(executor) .await?; diff --git a/crates/storage/src/oauth2/mod.rs b/crates/storage/src/oauth2/mod.rs index 5a1cfef4..d587011c 100644 --- a/crates/storage/src/oauth2/mod.rs +++ b/crates/storage/src/oauth2/mod.rs @@ -12,8 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. +use chrono::Utc; use mas_data_model::Session; use sqlx::PgExecutor; +use uuid::Uuid; use crate::PostgresqlBackend; @@ -27,13 +29,15 @@ pub async fn end_oauth_session( executor: impl PgExecutor<'_>, session: Session, ) -> anyhow::Result<()> { + let finished_at = Utc::now(); let res = sqlx::query!( r#" UPDATE oauth2_sessions - SET ended_at = NOW() - WHERE id = $1 + SET finished_at = $2 + WHERE oauth2_session_id = $1 "#, - session.data, + Uuid::from(session.data), + finished_at, ) .execute(executor) .await?; diff --git a/crates/storage/src/oauth2/refresh_token.rs b/crates/storage/src/oauth2/refresh_token.rs index 2452a87e..b78d9f21 100644 --- a/crates/storage/src/oauth2/refresh_token.rs +++ b/crates/storage/src/oauth2/refresh_token.rs @@ -13,66 +13,71 @@ // limitations under the License. use anyhow::Context; -use chrono::{DateTime, Duration, Utc}; +use chrono::{DateTime, Utc}; use mas_data_model::{ AccessToken, Authentication, BrowserSession, RefreshToken, Session, User, UserEmail, }; use sqlx::{PgConnection, PgExecutor}; use thiserror::Error; +use ulid::Ulid; +use uuid::Uuid; use super::client::{lookup_client, ClientFetchError}; -use crate::{DatabaseInconsistencyError, IdAndCreationTime, PostgresqlBackend}; +use crate::{DatabaseInconsistencyError, PostgresqlBackend}; pub async fn add_refresh_token( executor: impl PgExecutor<'_>, session: &Session, access_token: AccessToken, - token: &str, + refresh_token: String, ) -> anyhow::Result> { - let res = sqlx::query_as!( - IdAndCreationTime, + let created_at = Utc::now(); + let id = Ulid::from_datetime(created_at.into()); + + sqlx::query!( r#" INSERT INTO oauth2_refresh_tokens - (oauth2_session_id, oauth2_access_token_id, token) + (oauth2_refresh_token_id, oauth2_session_id, oauth2_access_token_id, + refresh_token, created_at) VALUES - ($1, $2, $3) - RETURNING - id, created_at + ($1, $2, $3, $4, $5) "#, - session.data, - access_token.data, - token, + Uuid::from(id), + Uuid::from(session.data), + Uuid::from(access_token.data), + refresh_token, + created_at, ) - .fetch_one(executor) + .execute(executor) .await .context("could not insert oauth2 refresh token")?; Ok(RefreshToken { - data: res.id, - token: token.to_owned(), + data: id, + refresh_token, access_token: Some(access_token), - created_at: res.created_at, + created_at, }) } struct OAuth2RefreshTokenLookup { - refresh_token_id: i64, - refresh_token: String, - refresh_token_created_at: DateTime, - access_token_id: Option, - access_token: Option, - access_token_expires_after: Option, - access_token_created_at: Option>, - session_id: i64, - oauth2_client_id: i64, - scope: String, - user_session_id: i64, + oauth2_refresh_token_id: Uuid, + oauth2_refresh_token: String, + oauth2_refresh_token_created_at: DateTime, + oauth2_access_token_id: Option, + oauth2_access_token: Option, + oauth2_access_token_created_at: Option>, + oauth2_access_token_expires_at: Option>, + oauth2_session_id: Uuid, + oauth2_client_id: Uuid, + oauth2_session_scope: String, + user_session_id: Uuid, user_session_created_at: DateTime, - user_id: i64, + user_id: Uuid, user_username: String, - user_session_last_authentication_id: Option, + user_session_last_authentication_id: Option, user_session_last_authentication_created_at: Option>, - user_email_id: Option, + user_email_id: Option, user_email: Option, user_email_created_at: Option>, user_email_confirmed_at: Option>, @@ -103,44 +108,45 @@ pub async fn lookup_active_refresh_token( OAuth2RefreshTokenLookup, r#" SELECT - rt.id AS refresh_token_id, - rt.token AS refresh_token, - rt.created_at AS refresh_token_created_at, - at.id AS "access_token_id?", - at.token AS "access_token?", - at.expires_after AS "access_token_expires_after?", - at.created_at AS "access_token_created_at?", - os.id AS "session_id!", - os.oauth2_client_id AS "oauth2_client_id!", - os.scope AS "scope!", - us.id AS "user_session_id!", - us.created_at AS "user_session_created_at!", - u.id AS "user_id!", - u.username AS "user_username!", - usa.id AS "user_session_last_authentication_id?", - usa.created_at AS "user_session_last_authentication_created_at?", - ue.id AS "user_email_id?", - ue.email AS "user_email?", - ue.created_at AS "user_email_created_at?", - ue.confirmed_at AS "user_email_confirmed_at?" + rt.oauth2_refresh_token_id, + rt.refresh_token AS oauth2_refresh_token, + rt.created_at AS oauth2_refresh_token_created_at, + at.oauth2_access_token_id AS "oauth2_access_token_id?", + at.access_token AS "oauth2_access_token?", + at.created_at AS "oauth2_access_token_created_at?", + at.expires_at AS "oauth2_access_token_expires_at?", + os.oauth2_session_id AS "oauth2_session_id!", + os.oauth2_client_id AS "oauth2_client_id!", + os.scope AS "oauth2_session_scope!", + us.user_session_id AS "user_session_id!", + us.created_at AS "user_session_created_at!", + u.user_id AS "user_id!", + u.username AS "user_username!", + usa.user_session_authentication_id AS "user_session_last_authentication_id?", + usa.created_at AS "user_session_last_authentication_created_at?", + ue.user_email_id AS "user_email_id?", + ue.email AS "user_email?", + ue.created_at AS "user_email_created_at?", + ue.confirmed_at AS "user_email_confirmed_at?" FROM oauth2_refresh_tokens rt - LEFT JOIN oauth2_access_tokens at - ON at.id = rt.oauth2_access_token_id INNER JOIN oauth2_sessions os - ON os.id = rt.oauth2_session_id + USING (oauth2_session_id) + LEFT JOIN oauth2_access_tokens at + USING (oauth2_access_token_id) INNER JOIN user_sessions us - ON us.id = os.user_session_id + USING (user_session_id) INNER JOIN users u - ON u.id = us.user_id + USING (user_id) LEFT JOIN user_session_authentications usa - ON usa.session_id = us.id + USING (user_session_id) LEFT JOIN user_emails ue - ON ue.id = u.primary_email_id + ON ue.user_email_id = u.primary_user_email_id - WHERE rt.token = $1 - AND rt.next_token_id IS NULL - AND us.active - AND os.ended_at IS NULL + WHERE rt.refresh_token = $1 + AND rt.consumed_at IS NULL + AND rt.revoked_at IS NULL + AND us.finished_at IS NULL + AND os.finished_at IS NULL ORDER BY usa.created_at DESC LIMIT 1 @@ -151,30 +157,31 @@ pub async fn lookup_active_refresh_token( .await?; let access_token = match ( - res.access_token_id, - res.access_token, - res.access_token_created_at, - res.access_token_expires_after, + res.oauth2_access_token_id, + res.oauth2_access_token, + res.oauth2_access_token_created_at, + res.oauth2_access_token_expires_at, ) { (None, None, None, None) => None, - (Some(id), Some(token), Some(created_at), Some(expires_after)) => Some(AccessToken { - data: id, - jti: format!("{}", id), - token, + (Some(id), Some(access_token), Some(created_at), Some(expires_at)) => Some(AccessToken { + data: id.into(), + // XXX: are we doing that everywhere? + jti: Ulid::from(id).to_string(), + access_token, created_at, - expires_after: Duration::seconds(expires_after.into()), + expires_at, }), _ => return Err(DatabaseInconsistencyError.into()), }; let refresh_token = RefreshToken { - data: res.refresh_token_id, - token: res.refresh_token, - created_at: res.refresh_token_created_at, + data: res.oauth2_refresh_token_id.into(), + refresh_token: res.oauth2_refresh_token, + created_at: res.oauth2_refresh_token_created_at, access_token, }; - let client = lookup_client(&mut *conn, res.oauth2_client_id).await?; + let client = lookup_client(&mut *conn, res.oauth2_client_id.into()).await?; let primary_email = match ( res.user_email_id, @@ -183,7 +190,7 @@ pub async fn lookup_active_refresh_token( res.user_email_confirmed_at, ) { (Some(id), Some(email), Some(created_at), confirmed_at) => Some(UserEmail { - data: id, + data: id.into(), email, created_at, confirmed_at, @@ -192,10 +199,11 @@ pub async fn lookup_active_refresh_token( _ => return Err(DatabaseInconsistencyError.into()), }; + let id = Ulid::from(res.user_id); let user = User { - data: res.user_id, + data: id, username: res.user_username, - sub: format!("fake-sub-{}", res.user_id), + sub: id.to_string(), primary_email, }; @@ -205,23 +213,26 @@ pub async fn lookup_active_refresh_token( ) { (None, None) => None, (Some(id), Some(created_at)) => Some(Authentication { - data: id, + data: id.into(), created_at, }), _ => return Err(DatabaseInconsistencyError.into()), }; let browser_session = BrowserSession { - data: res.user_session_id, + data: res.user_session_id.into(), created_at: res.user_session_created_at, user, last_authentication, }; - let scope = res.scope.parse().map_err(|_e| DatabaseInconsistencyError)?; + let scope = res + .oauth2_session_scope + .parse() + .map_err(|_e| DatabaseInconsistencyError)?; let session = Session { - data: res.session_id, + data: res.oauth2_session_id.into(), client, browser_session, scope, @@ -230,19 +241,19 @@ pub async fn lookup_active_refresh_token( Ok((refresh_token, session)) } -pub async fn replace_refresh_token( +pub async fn consume_refresh_token( executor: impl PgExecutor<'_>, refresh_token: &RefreshToken, - next_refresh_token: &RefreshToken, ) -> anyhow::Result<()> { + let consumed_at = Utc::now(); let res = sqlx::query!( r#" UPDATE oauth2_refresh_tokens - SET next_token_id = $2 - WHERE id = $1 + SET consumed_at = $2 + WHERE oauth2_refresh_token_id = $1 "#, - refresh_token.data, - next_refresh_token.data + Uuid::from(refresh_token.data), + consumed_at, ) .execute(executor) .await diff --git a/crates/storage/src/user.rs b/crates/storage/src/user.rs index 99c6d3f2..0b3ada39 100644 --- a/crates/storage/src/user.rs +++ b/crates/storage/src/user.rs @@ -22,20 +22,21 @@ use mas_data_model::{ UserEmailVerificationState, }; use password_hash::{PasswordHash, PasswordHasher, SaltString}; -use rand::rngs::OsRng; -use sqlx::{postgres::types::PgInterval, Acquire, PgExecutor, Postgres, Transaction}; +use rand::thread_rng; +use sqlx::{Acquire, PgExecutor, Postgres, Transaction}; use thiserror::Error; use tokio::task; use tracing::{info_span, Instrument}; +use ulid::Ulid; +use uuid::Uuid; use super::{DatabaseInconsistencyError, PostgresqlBackend}; -use crate::IdAndCreationTime; #[derive(Debug, Clone)] struct UserLookup { - user_id: i64, + user_id: Uuid, user_username: String, - user_email_id: Option, + user_email_id: Option, user_email: Option, user_email_created_at: Option>, user_email_confirmed_at: Option>, @@ -114,13 +115,13 @@ impl ActiveSessionLookupError { } struct SessionLookup { - id: i64, - user_id: i64, + user_session_id: Uuid, + user_id: Uuid, username: String, created_at: DateTime, - last_authentication_id: Option, + last_authentication_id: Option, last_authd_at: Option>, - user_email_id: Option, + user_email_id: Option, user_email: Option, user_email_created_at: Option>, user_email_confirmed_at: Option>, @@ -137,7 +138,7 @@ impl TryInto> for SessionLookup { self.user_email_confirmed_at, ) { (Some(id), Some(email), Some(created_at), confirmed_at) => Some(UserEmail { - data: id, + data: id.into(), email, created_at, confirmed_at, @@ -146,16 +147,17 @@ impl TryInto> for SessionLookup { _ => return Err(DatabaseInconsistencyError), }; + let id = Ulid::from(self.user_id); let user = User { - data: self.user_id, + data: id, username: self.username, - sub: format!("fake-sub-{}", self.user_id), + sub: id.to_string(), primary_email, }; let last_authentication = match (self.last_authentication_id, self.last_authd_at) { (Some(id), Some(created_at)) => Some(Authentication { - data: id, + data: id.into(), created_at, }), (None, None) => None, @@ -163,7 +165,7 @@ impl TryInto> for SessionLookup { }; Ok(BrowserSession { - data: self.id, + data: self.user_session_id.into(), user, created_at: self.created_at, last_authentication, @@ -171,37 +173,37 @@ impl TryInto> for SessionLookup { } } -#[tracing::instrument(skip_all, fields(session.id = id))] +#[tracing::instrument(skip_all, fields(session.id = %id))] pub async fn lookup_active_session( executor: impl PgExecutor<'_>, - id: i64, + id: Ulid, ) -> Result, ActiveSessionLookupError> { let res = sqlx::query_as!( SessionLookup, r#" SELECT - s.id, - u.id AS user_id, + s.user_session_id, + u.user_id, u.username, s.created_at, - a.id AS "last_authentication_id?", - a.created_at AS "last_authd_at?", - ue.id AS "user_email_id?", + a.user_session_authentication_id AS "last_authentication_id?", + a.created_at AS "last_authd_at?", + ue.user_email_id AS "user_email_id?", ue.email AS "user_email?", ue.created_at AS "user_email_created_at?", ue.confirmed_at AS "user_email_confirmed_at?" FROM user_sessions s INNER JOIN users u - ON s.user_id = u.id + USING (user_id) LEFT JOIN user_session_authentications a - ON a.session_id = s.id + USING (user_session_id) LEFT JOIN user_emails ue - ON ue.id = u.primary_email_id - WHERE s.id = $1 AND s.active + ON ue.user_email_id = u.primary_user_email_id + WHERE s.user_session_id = $1 AND s.finished_at IS NULL ORDER BY a.created_at DESC LIMIT 1 "#, - id, + Uuid::from(id), ) .fetch_one(executor) .await? @@ -210,35 +212,37 @@ pub async fn lookup_active_session( Ok(res) } -#[tracing::instrument(skip_all, fields(user.id = user.data))] +#[tracing::instrument(skip_all, fields(user.id = %user.data))] pub async fn start_session( executor: impl PgExecutor<'_>, user: User, ) -> anyhow::Result> { - let res = sqlx::query_as!( - IdAndCreationTime, + let created_at = Utc::now(); + let id = Ulid::from_datetime(created_at.into()); + sqlx::query!( r#" - INSERT INTO user_sessions (user_id) - VALUES ($1) - RETURNING id, created_at + INSERT INTO user_sessions (user_session_id, user_id, created_at) + VALUES ($1, $2, $3) "#, - user.data, + Uuid::from(id), + Uuid::from(user.data), + created_at, ) - .fetch_one(executor) + .execute(executor) .await .context("could not create session")?; let session = BrowserSession { - data: res.id, + data: id, user, - created_at: res.created_at, + created_at, last_authentication: None, }; Ok(session) } -#[tracing::instrument(skip_all, fields(user.id = user.data))] +#[tracing::instrument(skip_all, fields(user.id = %user.data))] pub async fn count_active_sessions( executor: impl PgExecutor<'_>, user: &User, @@ -247,9 +251,9 @@ pub async fn count_active_sessions( r#" SELECT COUNT(*) as "count!" FROM user_sessions s - WHERE s.user_id = $1 AND s.active + WHERE s.user_id = $1 AND s.finished_at IS NULL "#, - user.data, + Uuid::from(user.data), ) .fetch_one(executor) .await? @@ -273,7 +277,7 @@ pub enum AuthenticationError { Internal(#[from] tokio::task::JoinError), } -#[tracing::instrument(skip_all, fields(session.id = session.data, user.id = session.user.data))] +#[tracing::instrument(skip_all, fields(session.id = %session.data, user.id = %session.user.data))] pub async fn authenticate_session( txn: &mut Transaction<'_, Postgres>, session: &mut BrowserSession, @@ -288,7 +292,7 @@ pub async fn authenticate_session( ORDER BY up.created_at DESC LIMIT 1 "#, - session.user.data, + Uuid::from(session.user.data), ) .fetch_one(txn.borrow_mut()) .instrument(tracing::info_span!("Lookup hashed password")) @@ -309,44 +313,50 @@ pub async fn authenticate_session( .await??; // That went well, let's insert the auth info - let res = sqlx::query_as!( - IdAndCreationTime, + let created_at = Utc::now(); + let id = Ulid::from_datetime(created_at.into()); + sqlx::query!( r#" - INSERT INTO user_session_authentications (session_id) - VALUES ($1) - RETURNING id, created_at + INSERT INTO user_session_authentications + (user_session_authentication_id, user_session_id, created_at) + VALUES ($1, $2, $3) "#, - session.data, + Uuid::from(id), + Uuid::from(session.data), + created_at, ) - .fetch_one(txn.borrow_mut()) + .execute(txn.borrow_mut()) .instrument(tracing::info_span!("Save authentication")) .await .map_err(AuthenticationError::Save)?; session.last_authentication = Some(Authentication { - data: res.id, - created_at: res.created_at, + data: id, + created_at, }); Ok(()) } -#[tracing::instrument(skip(txn, phf, password))] +#[tracing::instrument(skip(txn, phf, password), err)] pub async fn register_user( txn: &mut Transaction<'_, Postgres>, phf: impl PasswordHasher, username: &str, password: &str, ) -> anyhow::Result> { - let id: i64 = sqlx::query_scalar!( + let created_at = Utc::now(); + let id = Ulid::from_datetime(created_at.into()); + sqlx::query!( r#" - INSERT INTO users (username) - VALUES ($1) - RETURNING id + INSERT INTO users (user_id, username, created_at) + VALUES ($1, $2, $3) "#, + Uuid::from(id), username, + created_at, ) - .fetch_one(txn.borrow_mut()) + .execute(txn.borrow_mut()) .instrument(info_span!("Register user")) .await .context("could not insert user")?; @@ -354,7 +364,7 @@ pub async fn register_user( let user = User { data: id, username: username.to_owned(), - sub: format!("fake-sub-{}", id), + sub: id.to_string(), primary_email: None, }; @@ -363,23 +373,28 @@ pub async fn register_user( Ok(user) } -#[tracing::instrument(skip_all, fields(user.id = user.data))] +#[tracing::instrument(skip_all, fields(user.id = %user.data))] pub async fn set_password( executor: impl PgExecutor<'_>, phf: impl PasswordHasher, user: &User, password: &str, ) -> anyhow::Result<()> { - let salt = SaltString::generate(&mut OsRng); + let created_at = Utc::now(); + let id = Ulid::from_datetime(created_at.into()); + + let salt = SaltString::generate(thread_rng()); let hashed_password = PasswordHash::generate(phf, password, salt.as_str())?; sqlx::query_scalar!( r#" - INSERT INTO user_passwords (user_id, hashed_password) - VALUES ($1, $2) + INSERT INTO user_passwords (user_password_id, user_id, hashed_password, created_at) + VALUES ($1, $2, $3, $4) "#, - user.data, + Uuid::from(id), + Uuid::from(user.data), hashed_password.to_string(), + created_at, ) .execute(executor) .instrument(info_span!("Save user credentials")) @@ -389,14 +404,20 @@ pub async fn set_password( Ok(()) } -#[tracing::instrument(skip_all, fields(session.id = session.data))] +#[tracing::instrument(skip_all, fields(session.id = %session.data))] pub async fn end_session( executor: impl PgExecutor<'_>, session: &BrowserSession, ) -> anyhow::Result<()> { + let now = Utc::now(); let res = sqlx::query!( - "UPDATE user_sessions SET active = FALSE WHERE id = $1", - session.data, + r#" + UPDATE user_sessions + SET finished_at = $1 + WHERE user_session_id = $2 + "#, + now, + Uuid::from(session.data), ) .execute(executor) .instrument(info_span!("End session")) @@ -433,16 +454,16 @@ pub async fn lookup_user_by_username( UserLookup, r#" SELECT - u.id AS user_id, - u.username AS user_username, - ue.id AS "user_email_id?", - ue.email AS "user_email?", - ue.created_at AS "user_email_created_at?", - ue.confirmed_at AS "user_email_confirmed_at?" + u.user_id, + u.username AS user_username, + ue.user_email_id AS "user_email_id?", + ue.email AS "user_email?", + ue.created_at AS "user_email_created_at?", + ue.confirmed_at AS "user_email_confirmed_at?" FROM users u LEFT JOIN user_emails ue - ON ue.id = u.primary_email_id + USING (user_id) WHERE u.username = $1 "#, @@ -459,7 +480,7 @@ pub async fn lookup_user_by_username( res.user_email_confirmed_at, ) { (Some(id), Some(email), Some(created_at), confirmed_at) => Some(UserEmail { - data: id, + data: id.into(), email, created_at, confirmed_at, @@ -468,10 +489,11 @@ pub async fn lookup_user_by_username( _ => return Err(DatabaseInconsistencyError.into()), }; + let id = Ulid::from(res.user_id); Ok(User { - data: res.user_id, + data: id, username: res.user_username, - sub: format!("fake-sub-{}", res.user_id), + sub: id.to_string(), primary_email, }) } @@ -494,7 +516,7 @@ pub async fn username_exists( #[derive(Debug, Clone)] struct UserEmailLookup { - user_email_id: i64, + user_email_id: Uuid, user_email: String, user_email_created_at: DateTime, user_email_confirmed_at: Option>, @@ -503,7 +525,7 @@ struct UserEmailLookup { impl From for UserEmail { fn from(e: UserEmailLookup) -> UserEmail { UserEmail { - data: e.user_email_id, + data: e.user_email_id.into(), email: e.user_email, created_at: e.user_email_created_at, confirmed_at: e.user_email_confirmed_at, @@ -511,7 +533,7 @@ impl From for UserEmail { } } -#[tracing::instrument(skip_all, fields(user.id = user.data, %user.username))] +#[tracing::instrument(skip_all, fields(user.id = %user.data, %user.username))] pub async fn get_user_emails( executor: impl PgExecutor<'_>, user: &User, @@ -520,7 +542,7 @@ pub async fn get_user_emails( UserEmailLookup, r#" SELECT - ue.id AS "user_email_id", + ue.user_email_id, ue.email AS "user_email", ue.created_at AS "user_email_created_at", ue.confirmed_at AS "user_email_confirmed_at" @@ -530,7 +552,7 @@ pub async fn get_user_emails( ORDER BY ue.email ASC "#, - user.data, + Uuid::from(user.data), ) .fetch_all(executor) .instrument(info_span!("Fetch user emails")) @@ -539,27 +561,27 @@ pub async fn get_user_emails( Ok(res.into_iter().map(Into::into).collect()) } -#[tracing::instrument(skip_all, fields(user.id = user.data, %user.username, email.id = id))] +#[tracing::instrument(skip_all, fields(user.id = %user.data, %user.username, email.id = %id))] pub async fn get_user_email( executor: impl PgExecutor<'_>, user: &User, - id: i64, + id: Ulid, ) -> Result, anyhow::Error> { let res = sqlx::query_as!( UserEmailLookup, r#" SELECT - ue.id AS "user_email_id", + ue.user_email_id, ue.email AS "user_email", ue.created_at AS "user_email_created_at", ue.confirmed_at AS "user_email_confirmed_at" FROM user_emails ue WHERE ue.user_id = $1 - AND ue.id = $2 + AND ue.user_email_id = $2 "#, - user.data, - id, + Uuid::from(user.data), + Uuid::from(id), ) .fetch_one(executor) .instrument(info_span!("Fetch user emails")) @@ -568,32 +590,35 @@ pub async fn get_user_email( Ok(res.into()) } -#[tracing::instrument(skip(executor, user), fields(user.id = user.data, %user.username))] +#[tracing::instrument(skip(executor, user), fields(user.id = %user.data, %user.username))] pub async fn add_user_email( executor: impl PgExecutor<'_>, user: &User, - email: &str, + email: String, ) -> anyhow::Result> { - let res = sqlx::query_as!( - UserEmailLookup, + let created_at = Utc::now(); + let id = Ulid::from_datetime(created_at.into()); + sqlx::query!( r#" - INSERT INTO user_emails (user_id, email) - VALUES ($1, $2) - RETURNING - id AS user_email_id, - email AS user_email, - created_at AS user_email_created_at, - confirmed_at AS user_email_confirmed_at + INSERT INTO user_emails (user_email_id, user_id, email, created_at) + VALUES ($1, $2, $3, $4) "#, - user.data, - email, + Uuid::from(id), + Uuid::from(user.data), + &email, + created_at, ) - .fetch_one(executor) + .execute(executor) .instrument(info_span!("Add user email")) .await .context("could not insert user email")?; - Ok(res.into()) + Ok(UserEmail { + data: id, + email, + created_at, + confirmed_at: None, + }) } #[tracing::instrument(skip(executor))] @@ -604,12 +629,12 @@ pub async fn set_user_email_as_primary( sqlx::query!( r#" UPDATE users - SET primary_email_id = user_emails.id + SET primary_user_email_id = user_emails.user_email_id FROM user_emails - WHERE user_emails.id = $1 - AND users.id = user_emails.user_id + WHERE user_emails.user_email_id = $1 + AND users.user_id = user_emails.user_id "#, - email.data, + Uuid::from(email.data), ) .execute(executor) .instrument(info_span!("Add user email")) @@ -627,9 +652,9 @@ pub async fn remove_user_email( sqlx::query!( r#" DELETE FROM user_emails - WHERE user_emails.id = $1 + WHERE user_emails.user_email_id = $1 "#, - email.data, + Uuid::from(email.data), ) .execute(executor) .instrument(info_span!("Remove user email")) @@ -649,7 +674,7 @@ pub async fn lookup_user_email( UserEmailLookup, r#" SELECT - ue.id AS "user_email_id", + ue.user_email_id, ue.email AS "user_email", ue.created_at AS "user_email_created_at", ue.confirmed_at AS "user_email_confirmed_at" @@ -658,7 +683,7 @@ pub async fn lookup_user_email( WHERE ue.user_id = $1 AND ue.email = $2 "#, - user.data, + Uuid::from(user.data), email, ) .fetch_one(executor) @@ -673,23 +698,23 @@ pub async fn lookup_user_email( pub async fn lookup_user_email_by_id( executor: impl PgExecutor<'_>, user: &User, - id: i64, + id: Ulid, ) -> anyhow::Result> { let res = sqlx::query_as!( UserEmailLookup, r#" SELECT - ue.id AS "user_email_id", + ue.user_email_id, ue.email AS "user_email", ue.created_at AS "user_email_created_at", ue.confirmed_at AS "user_email_confirmed_at" FROM user_emails ue WHERE ue.user_id = $1 - AND ue.id = $2 + AND ue.user_email_id = $2 "#, - user.data, - id, + Uuid::from(user.data), + Uuid::from(id), ) .fetch_one(executor) .instrument(info_span!("Lookup user email")) @@ -704,31 +729,32 @@ pub async fn mark_user_email_as_verified( executor: impl PgExecutor<'_>, mut email: UserEmail, ) -> anyhow::Result> { - let confirmed_at = sqlx::query_scalar!( + let confirmed_at = Utc::now(); + sqlx::query!( r#" UPDATE user_emails - SET confirmed_at = NOW() - WHERE id = $1 - RETURNING confirmed_at + SET confirmed_at = $2 + WHERE user_email_id = $1 "#, - email.data, + Uuid::from(email.data), + confirmed_at, ) - .fetch_one(executor) + .execute(executor) .instrument(info_span!("Confirm user email")) .await .context("could not update user email")?; - email.confirmed_at = confirmed_at; + email.confirmed_at = Some(confirmed_at); Ok(email) } -struct UserEmailVerificationLookup { - verification_id: i64, - verification_code: String, - verification_expired: bool, - verification_created_at: DateTime, - verification_consumed_at: Option>, +struct UserEmailConfirmationCodeLookup { + user_email_confirmation_code_id: Uuid, + code: String, + created_at: DateTime, + expires_at: DateTime, + consumed_at: Option>, } #[tracing::instrument(skip(executor))] @@ -736,49 +762,46 @@ pub async fn lookup_user_email_verification_code( executor: impl PgExecutor<'_>, email: UserEmail, code: &str, - max_age: chrono::Duration, ) -> anyhow::Result> { - // For some reason, we need to convert the type first - let max_age = PgInterval::try_from(max_age) - // For some reason, this error type does not let me to just bubble up the error here - .map_err(|e| anyhow::anyhow!("failed to encode duration: {}", e))?; + let now = Utc::now(); let res = sqlx::query_as!( - UserEmailVerificationLookup, + UserEmailConfirmationCodeLookup, r#" SELECT - ev.id AS "verification_id", - ev.code AS "verification_code", - (ev.created_at + $3 < NOW()) AS "verification_expired!", - ev.created_at AS "verification_created_at", - ev.consumed_at AS "verification_consumed_at" - FROM user_email_verifications ev - WHERE ev.code = $1 - AND ev.user_email_id = $2 + ec.user_email_confirmation_code_id, + ec.code, + ec.created_at, + ec.expires_at, + ec.consumed_at + FROM user_email_confirmation_codes ec + WHERE ec.code = $1 + AND ec.user_email_id = $2 "#, code, - email.data, - max_age, + Uuid::from(email.data), ) .fetch_one(executor) .instrument(info_span!("Lookup user email verification")) .await .context("could not lookup user email verification")?; - let state = if res.verification_expired { - UserEmailVerificationState::Expired - } else if let Some(when) = res.verification_consumed_at { + let state = if let Some(when) = res.consumed_at { UserEmailVerificationState::AlreadyUsed { when } + } else if res.expires_at < now { + UserEmailVerificationState::Expired { + when: res.expires_at, + } } else { UserEmailVerificationState::Valid }; Ok(UserEmailVerification { - data: res.verification_id, - code: res.verification_code, + data: res.user_email_confirmation_code_id.into(), + code: res.code, email, state, - created_at: res.verification_created_at, + created_at: res.created_at, }) } @@ -791,16 +814,18 @@ pub async fn consume_email_verification( bail!("user email verification in wrong state"); } - let consumed_at = sqlx::query_scalar!( + let consumed_at = Utc::now(); + + sqlx::query!( r#" - UPDATE user_email_verifications - SET consumed_at = NOW() - WHERE id = $1 - RETURNING consumed_at AS "consumed_at!" + UPDATE user_email_confirmation_codes + SET consumed_at = $2 + WHERE user_email_confirmation_code_id = $1 "#, - verification.data, + Uuid::from(verification.data), + consumed_at ) - .fetch_one(executor) + .execute(executor) .instrument(info_span!("Consume user email verification")) .await .context("could not update user email verification")?; @@ -810,32 +835,39 @@ pub async fn consume_email_verification( Ok(verification) } -#[tracing::instrument(skip(executor, email), fields(email.id = email.data, %email.email))] +#[tracing::instrument(skip(executor, email), fields(email.id = %email.data, %email.email))] pub async fn add_user_email_verification_code( executor: impl PgExecutor<'_>, email: UserEmail, + max_age: chrono::Duration, code: String, ) -> anyhow::Result> { - let res = sqlx::query_as!( - IdAndCreationTime, + let created_at = Utc::now(); + let id = Ulid::from_datetime(created_at.into()); + let expires_at = created_at + max_age; + + sqlx::query!( r#" - INSERT INTO user_email_verifications (user_email_id, code) - VALUES ($1, $2) - RETURNING id, created_at + INSERT INTO user_email_confirmation_codes + (user_email_confirmation_code_id, user_email_id, code, created_at, expires_at) + VALUES ($1, $2, $3, $4, $5) "#, - email.data, + Uuid::from(id), + Uuid::from(email.data), code, + created_at, + expires_at, ) - .fetch_one(executor) + .execute(executor) .instrument(info_span!("Add user email verification code")) .await .context("could not insert user email verification code")?; let verification = UserEmailVerification { - data: res.id, + data: id, email, code, - created_at: res.created_at, + created_at, state: UserEmailVerificationState::Valid, }; diff --git a/crates/templates/Cargo.toml b/crates/templates/Cargo.toml index 85e7b1d7..e3dbed97 100644 --- a/crates/templates/Cargo.toml +++ b/crates/templates/Cargo.toml @@ -22,6 +22,7 @@ serde_urlencoded = "0.7.1" chrono = "0.4.22" url = "2.3.1" +ulid = { version = "1.0.0", features = ["serde"] } oauth2-types = { path = "../oauth2-types" } mas-data-model = { path = "../data-model" } diff --git a/crates/templates/src/context.rs b/crates/templates/src/context.rs index a6e3cffa..9fd46fcc 100644 --- a/crates/templates/src/context.rs +++ b/crates/templates/src/context.rs @@ -23,6 +23,7 @@ use mas_data_model::{ }; use mas_router::PostAuthAction; use serde::{ser::SerializeStruct, Deserialize, Serialize}; +use ulid::Ulid; use url::Url; use crate::{FormField, FormState}; @@ -517,11 +518,11 @@ impl TemplateContext for CompatSsoContext { login: CompatSsoLogin { data: (), redirect_uri: Url::parse("https://app.element.io/").unwrap(), - token: "abcdefghijklmnopqrstuvwxyz012345".into(), + login_token: "abcdefghijklmnopqrstuvwxyz012345".into(), created_at: Utc::now(), state: CompatSsoLoginState::Pending, }, - action: PostAuthAction::ContinueCompatSsoLogin { data: 1 }, + action: PostAuthAction::ContinueCompatSsoLogin { data: Ulid::new() }, }] } }