mirror of
https://github.com/trailbaseio/trailbase.git
synced 2026-02-20 00:28:40 -06:00
Use UUIDv4 ids for _users to stop leaking creation times. We rely on _rowid_ for cursoring.
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
INSERT INTO _user
|
||||
(id, email, password_hash, verified, admin)
|
||||
VALUES
|
||||
(uuid_v7(), 'admin@localhost', (hash_password('secret')), TRUE, TRUE);
|
||||
(uuid_v4(), 'admin@localhost', (hash_password('secret')), TRUE, TRUE);
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
-- Add a a few non-admin users.
|
||||
INSERT INTO _user (id, email, password_hash, verified)
|
||||
VALUES
|
||||
(uuid_v7(), '0@localhost', (hash_password('secret')), TRUE),
|
||||
(uuid_v7(), '1@localhost', (hash_password('secret')), TRUE),
|
||||
(uuid_v7(), '2@localhost', (hash_password('secret')), TRUE),
|
||||
(uuid_v7(), '3@localhost', (hash_password('secret')), TRUE),
|
||||
(uuid_v7(), '4@localhost', (hash_password('secret')), TRUE),
|
||||
(uuid_v7(), '5@localhost', (hash_password('secret')), TRUE),
|
||||
(uuid_v7(), '6@localhost', (hash_password('secret')), TRUE),
|
||||
(uuid_v7(), '7@localhost', (hash_password('secret')), TRUE),
|
||||
(uuid_v7(), '8@localhost', (hash_password('secret')), TRUE),
|
||||
(uuid_v7(), '9@localhost', (hash_password('secret')), TRUE),
|
||||
(uuid_v7(), '10@localhost', (hash_password('secret')), TRUE),
|
||||
(uuid_v7(), '11@localhost', (hash_password('secret')), TRUE),
|
||||
(uuid_v7(), '12@localhost', (hash_password('secret')), TRUE);
|
||||
(uuid_v4(), '0@localhost', (hash_password('secret')), TRUE),
|
||||
(uuid_v4(), '1@localhost', (hash_password('secret')), TRUE),
|
||||
(uuid_v4(), '2@localhost', (hash_password('secret')), TRUE),
|
||||
(uuid_v4(), '3@localhost', (hash_password('secret')), TRUE),
|
||||
(uuid_v4(), '4@localhost', (hash_password('secret')), TRUE),
|
||||
(uuid_v4(), '5@localhost', (hash_password('secret')), TRUE),
|
||||
(uuid_v4(), '6@localhost', (hash_password('secret')), TRUE),
|
||||
(uuid_v4(), '7@localhost', (hash_password('secret')), TRUE),
|
||||
(uuid_v4(), '8@localhost', (hash_password('secret')), TRUE),
|
||||
(uuid_v4(), '9@localhost', (hash_password('secret')), TRUE),
|
||||
(uuid_v4(), '10@localhost', (hash_password('secret')), TRUE),
|
||||
(uuid_v4(), '11@localhost', (hash_password('secret')), TRUE),
|
||||
(uuid_v4(), '12@localhost', (hash_password('secret')), TRUE);
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
fn main() -> std::io::Result<()> {
|
||||
trailbase_build::init_env_logger();
|
||||
|
||||
println!("cargo::rerun-if-changed=migrations");
|
||||
|
||||
trailbase_assets::setup_version_info!();
|
||||
|
||||
trailbase_build::build_protos("./proto")?;
|
||||
|
||||
97
trailbase-core/migrations/main/U3__user_id.sql
Normal file
97
trailbase-core/migrations/main/U3__user_id.sql
Normal file
@@ -0,0 +1,97 @@
|
||||
-- Migrate from UUIDv7 ids to truly random UUIDv4 ids + INTEGER primary key.
|
||||
PRAGMA foreign_keys=off;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS _new_user (
|
||||
-- We only check `is_uuid` rather than `is_uuid_v4` to preserve user
|
||||
-- previously created as uuiv7.
|
||||
id BLOB PRIMARY KEY NOT NULL CHECK(is_uuid(id)) DEFAULT (uuid_v4()),
|
||||
email TEXT NOT NULL CHECK(is_email(email)),
|
||||
password_hash TEXT DEFAULT '' NOT NULL,
|
||||
verified INTEGER DEFAULT FALSE NOT NULL,
|
||||
admin INTEGER DEFAULT FALSE NOT NULL,
|
||||
|
||||
created INTEGER DEFAULT (UNIXEPOCH()) NOT NULL,
|
||||
updated INTEGER DEFAULT (UNIXEPOCH()) NOT NULL,
|
||||
|
||||
-- Ephemeral data for auth flows.
|
||||
--
|
||||
-- Email change/verification flow.
|
||||
email_verification_code TEXT,
|
||||
email_verification_code_sent_at INTEGER,
|
||||
-- Change email flow.
|
||||
pending_email TEXT CHECK(is_email(pending_email)),
|
||||
-- Reset forgotten password flow.
|
||||
password_reset_code TEXT,
|
||||
password_reset_code_sent_at INTEGER,
|
||||
-- Authorization Code Flow (optionally with PKCE proof key).
|
||||
authorization_code TEXT,
|
||||
authorization_code_sent_at INTEGER,
|
||||
pkce_code_challenge TEXT,
|
||||
|
||||
-- OAuth metadata
|
||||
--
|
||||
-- provider_id maps to proto.config.OAuthProviderId enum.
|
||||
provider_id INTEGER DEFAULT 0 NOT NULL,
|
||||
-- The external provider's id for the user.
|
||||
provider_user_id TEXT,
|
||||
-- Link to an external avatar image for oauth providers only.
|
||||
provider_avatar_url TEXT
|
||||
) STRICT;
|
||||
|
||||
INSERT INTO _new_user(
|
||||
id,
|
||||
email,
|
||||
password_hash,
|
||||
verified,
|
||||
admin,
|
||||
created,
|
||||
updated,
|
||||
email_verification_code,
|
||||
email_verification_code_sent_at,
|
||||
pending_email,
|
||||
password_reset_code,
|
||||
password_reset_code_sent_at,
|
||||
authorization_code,
|
||||
authorization_code_sent_at,
|
||||
pkce_code_challenge,
|
||||
provider_id,
|
||||
provider_user_id,
|
||||
provider_avatar_url
|
||||
)
|
||||
SELECT
|
||||
id,
|
||||
email,
|
||||
password_hash,
|
||||
verified,
|
||||
admin,
|
||||
created,
|
||||
updated,
|
||||
email_verification_code,
|
||||
email_verification_code_sent_at,
|
||||
pending_email,
|
||||
password_reset_code,
|
||||
password_reset_code_sent_at,
|
||||
authorization_code,
|
||||
authorization_code_sent_at,
|
||||
pkce_code_challenge,
|
||||
provider_id,
|
||||
provider_user_id,
|
||||
provider_avatar_url
|
||||
FROM _user;
|
||||
|
||||
DROP TABLE _user;
|
||||
|
||||
ALTER TABLE _new_user RENAME TO _user;
|
||||
|
||||
CREATE UNIQUE INDEX __user__email_index ON _user (email);
|
||||
CREATE UNIQUE INDEX __user__email_verification_code_index ON _user (email_verification_code);
|
||||
CREATE UNIQUE INDEX __user__password_reset_code_index ON _user (password_reset_code);
|
||||
CREATE UNIQUE INDEX __user__authorization_code_index ON _user (authorization_code);
|
||||
CREATE UNIQUE INDEX __user__provider_ids_index ON _user (provider_id, provider_user_id);
|
||||
|
||||
CREATE TRIGGER __user__updated_trigger AFTER UPDATE ON _user FOR EACH ROW
|
||||
BEGIN
|
||||
UPDATE _user SET updated = UNIXEPOCH() WHERE id = OLD.id;
|
||||
END;
|
||||
|
||||
PRAGMA foreign_keys=on;
|
||||
@@ -13,7 +13,7 @@ use uuid::Uuid;
|
||||
use crate::admin::AdminError as Error;
|
||||
use crate::app_state::AppState;
|
||||
use crate::auth::user::DbUser;
|
||||
use crate::constants::{USER_TABLE, USER_TABLE_ID_COLUMN};
|
||||
use crate::constants::USER_TABLE;
|
||||
use crate::listing::{WhereClause, build_filter_where_clause, cursor_to_value, limit_or_default};
|
||||
use crate::util::id_to_b64;
|
||||
|
||||
@@ -98,7 +98,7 @@ pub async fn list_users_handler(
|
||||
|
||||
lazy_static! {
|
||||
static ref DEFAULT_ORDERING: Order = Order {
|
||||
columns: vec![(USER_TABLE_ID_COLUMN.to_string(), OrderPrecedent::Descending)],
|
||||
columns: vec![("_rowid_".to_string(), OrderPrecedent::Descending)],
|
||||
};
|
||||
}
|
||||
let users = fetch_users(
|
||||
@@ -165,8 +165,7 @@ async fn fetch_users(
|
||||
let sql_query = format!(
|
||||
r#"
|
||||
SELECT _ROW_.*
|
||||
FROM
|
||||
(SELECT * FROM {USER_TABLE}) as _ROW_
|
||||
FROM {USER_TABLE} as _ROW_
|
||||
WHERE
|
||||
{where_clause}
|
||||
ORDER BY
|
||||
|
||||
@@ -10,7 +10,7 @@ use crate::constants::AVATAR_TABLE;
|
||||
use crate::extract::Either;
|
||||
use crate::records::params::{JsonRow, LazyParams};
|
||||
use crate::records::query_builder::QueryError;
|
||||
use crate::util::{assert_uuidv7_version, uuid_to_b64};
|
||||
use crate::util::uuid_to_b64;
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
@@ -24,8 +24,6 @@ pub async fn get_avatar_handler(
|
||||
let Ok(user_id) = crate::util::b64_to_uuid(&b64_user_id) else {
|
||||
return Err(AuthError::BadRequest("Invalid user id"));
|
||||
};
|
||||
assert_uuidv7_version(&user_id);
|
||||
|
||||
let Some(table) = state.schema_metadata().get_table(&table_name) else {
|
||||
return Err(AuthError::Internal("missing table".into()));
|
||||
};
|
||||
|
||||
@@ -44,7 +44,6 @@ pub(crate) struct DbUser {
|
||||
impl DbUser {
|
||||
pub(crate) fn uuid(&self) -> Uuid {
|
||||
let uuid = Uuid::from_bytes(self.id);
|
||||
assert_eq!(uuid.get_version_num(), 7);
|
||||
return uuid;
|
||||
}
|
||||
|
||||
@@ -108,7 +107,6 @@ impl User {
|
||||
pub(crate) fn from_token_claims(claims: TokenClaims) -> Result<Self, AuthError> {
|
||||
let uuid = b64_to_uuid(&claims.sub)
|
||||
.map_err(|_err| AuthError::UnauthorizedExt("invalid user id".into()))?;
|
||||
assert_eq!(uuid.get_version_num(), 7);
|
||||
|
||||
return Ok(Self {
|
||||
id: claims.sub,
|
||||
|
||||
@@ -2,7 +2,6 @@ use chrono::Duration;
|
||||
|
||||
pub const SQLITE_SCHEMA_TABLE: &str = "sqlite_schema";
|
||||
pub const USER_TABLE: &str = "_user";
|
||||
pub(crate) const USER_TABLE_ID_COLUMN: &str = "id";
|
||||
|
||||
pub(crate) const SESSION_TABLE: &str = "_session";
|
||||
pub(crate) const AVATAR_TABLE: &str = "_user_avatar";
|
||||
|
||||
@@ -213,14 +213,13 @@ mod test {
|
||||
|
||||
state
|
||||
.conn()
|
||||
.execute(
|
||||
.execute_batch(
|
||||
r#"
|
||||
CREATE TABLE simple (
|
||||
owner BLOB PRIMARY KEY CHECK(is_uuid(owner)) REFERENCES _user,
|
||||
value INTEGER
|
||||
) STRICT;
|
||||
"#,
|
||||
(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -291,7 +290,7 @@ mod test {
|
||||
User::from_auth_token(&state, &user_x_token.auth_token),
|
||||
Either::Json(
|
||||
json_row_from_value(json!({
|
||||
"owner": uuid_to_b64(&uuid::Uuid::now_v7()),
|
||||
"owner": uuid_to_b64(&uuid::Uuid::new_v4()),
|
||||
"value": 17,
|
||||
}))
|
||||
.unwrap()
|
||||
|
||||
@@ -103,12 +103,20 @@ pub fn sqlite3_extension_init(
|
||||
FunctionFlags::SQLITE_DETERMINISTIC | FunctionFlags::SQLITE_INNOCUOUS,
|
||||
uuid::is_uuid,
|
||||
)?;
|
||||
db.create_scalar_function(
|
||||
"is_uuid_v4",
|
||||
1,
|
||||
FunctionFlags::SQLITE_DETERMINISTIC | FunctionFlags::SQLITE_INNOCUOUS,
|
||||
uuid::is_uuid_v4,
|
||||
)?;
|
||||
db.create_scalar_function("uuid_v4", 0, FunctionFlags::SQLITE_INNOCUOUS, uuid::uuid_v4)?;
|
||||
db.create_scalar_function(
|
||||
"is_uuid_v7",
|
||||
1,
|
||||
FunctionFlags::SQLITE_DETERMINISTIC | FunctionFlags::SQLITE_INNOCUOUS,
|
||||
uuid::is_uuid_v7,
|
||||
)?;
|
||||
db.create_scalar_function("uuid_v7", 0, FunctionFlags::SQLITE_INNOCUOUS, uuid::uuid_v7)?;
|
||||
db.create_scalar_function(
|
||||
"uuid_text",
|
||||
1,
|
||||
@@ -116,7 +124,6 @@ pub fn sqlite3_extension_init(
|
||||
uuid::uuid_text,
|
||||
)?;
|
||||
|
||||
db.create_scalar_function("uuid_v7", 0, FunctionFlags::SQLITE_INNOCUOUS, uuid::uuid_v7)?;
|
||||
db.create_scalar_function(
|
||||
"uuid_parse",
|
||||
1,
|
||||
|
||||
@@ -9,6 +9,22 @@ pub(super) fn is_uuid(context: &Context) -> Result<bool, Error> {
|
||||
return Ok(unpack_uuid_or_null(context).is_ok());
|
||||
}
|
||||
|
||||
/// Checks that argument is a valid UUIDv4 blob or null.
|
||||
///
|
||||
/// Null is explicitly allowed to enable use as CHECK constraint in nullable columns.
|
||||
pub(super) fn is_uuid_v4(context: &Context) -> Result<bool, Error> {
|
||||
let Some(uuid) = unpack_uuid_or_null(context)? else {
|
||||
return Ok(true);
|
||||
};
|
||||
|
||||
return Ok(uuid.get_version_num() == 7);
|
||||
}
|
||||
|
||||
/// Creates a new UUIDv4 blob.
|
||||
pub(super) fn uuid_v4(_context: &Context) -> Result<[u8; 16], Error> {
|
||||
return Ok(Uuid::new_v4().into_bytes());
|
||||
}
|
||||
|
||||
/// Checks that argument is a valid UUIDv7 blob or null.
|
||||
///
|
||||
/// Null is explicitly allowed to enable use as CHECK constraint in nullable columns.
|
||||
@@ -21,8 +37,8 @@ pub(super) fn is_uuid_v7(context: &Context) -> Result<bool, Error> {
|
||||
}
|
||||
|
||||
/// Creates a new UUIDv7 blob.
|
||||
pub(super) fn uuid_v7(_context: &Context) -> Result<Vec<u8>, Error> {
|
||||
return Ok(Uuid::now_v7().as_bytes().to_vec());
|
||||
pub(super) fn uuid_v7(_context: &Context) -> Result<[u8; 16], Error> {
|
||||
return Ok(Uuid::now_v7().into_bytes());
|
||||
}
|
||||
|
||||
/// Format UUID blob as string-encoded UUID.
|
||||
@@ -38,7 +54,7 @@ pub(super) fn uuid_text(context: &Context) -> Result<String, Error> {
|
||||
}
|
||||
|
||||
/// Parse UUID from string-encoded UUID.
|
||||
pub(super) fn uuid_parse(context: &Context) -> Result<Vec<u8>, Error> {
|
||||
pub(super) fn uuid_parse(context: &Context) -> Result<[u8; 16], Error> {
|
||||
#[cfg(debug_assertions)]
|
||||
if context.len() != 1 {
|
||||
return Err(Error::InvalidParameterCount(context.len(), 1));
|
||||
@@ -46,7 +62,7 @@ pub(super) fn uuid_parse(context: &Context) -> Result<Vec<u8>, Error> {
|
||||
|
||||
let str = context.get_raw(0).as_str()?;
|
||||
let uuid = Uuid::try_parse(str).map_err(|err| Error::UserFunctionError(err.into()))?;
|
||||
return Ok(uuid.into_bytes().into());
|
||||
return Ok(uuid.into_bytes());
|
||||
}
|
||||
|
||||
#[inline]
|
||||
|
||||
Reference in New Issue
Block a user