From f482707b75cce293b363014a1ccc2eda6045272d Mon Sep 17 00:00:00 2001 From: TheGeneralist <180094941+thegeneralist01@users.noreply.github.com> Date: Fri, 26 Jun 2026 11:31:20 +0200 Subject: [PATCH] feat(auth): user and role DB helpers (create_owner, compute_role_bits) --- crates/archivr-core/src/database.rs | 101 ++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) diff --git a/crates/archivr-core/src/database.rs b/crates/archivr-core/src/database.rs index fdd80a1..bbe6159 100644 --- a/crates/archivr-core/src/database.rs +++ b/crates/archivr-core/src/database.rs @@ -350,6 +350,74 @@ pub fn open_auth_db(auth_db_path: &Path) -> Result { Ok(conn) } +/// Returns true if an owner account exists. +pub fn ensure_owner_exists(conn: &Connection) -> Result { + let count: i64 = conn.query_row( + "SELECT COUNT(*) FROM user_roles ur + JOIN roles r ON r.id = ur.role_id + WHERE r.slug = 'owner'", + [], + |row| row.get(0), + )?; + Ok(count > 0) +} + +/// Creates a user and assigns all roles from `user` up to `owner` (cumulative). +/// `password_hash` must already be hashed by the caller. +pub fn create_owner(conn: &Connection, username: &str, password_hash: &str) -> Result { + let user_uid = public_id("usr"); + conn.execute( + "INSERT INTO users (user_uid, username, email, password_hash, status, role, created_at) + VALUES (?1, ?2, NULL, ?3, 'active', 'admin', ?4)", + params![user_uid, username, password_hash, now_timestamp()], + )?; + let user_id = conn.last_insert_rowid(); + for slug in &["user", "admin", "owner"] { + let role_id: i64 = conn.query_row( + "SELECT id FROM roles WHERE slug = ?1", + [slug], + |row| row.get(0), + )?; + conn.execute( + "INSERT OR IGNORE INTO user_roles (user_id, role_id, assigned_at) + VALUES (?1, ?2, ?3)", + params![user_id, role_id, now_timestamp()], + )?; + } + Ok(user_id) +} + +pub fn get_user_by_username(conn: &Connection, username: &str) -> Result> { + conn.query_row( + "SELECT id, user_uid, username, password_hash, status FROM users WHERE username = ?1", + [username], + |row| { + Ok(AuthUserRecord { + id: row.get(0)?, + user_uid: row.get(1)?, + username: row.get(2)?, + password_hash: row.get(3)?, + status: row.get(4)?, + }) + }, + ) + .optional() + .map_err(Into::into) +} + +/// Computes role_bits = ROLE_GUEST (1) | OR(assigned role bit values). +pub fn compute_role_bits(conn: &Connection, user_id: i64) -> Result { + let mut stmt = conn.prepare( + "SELECT (1 << r.bit_position) FROM user_roles ur + JOIN roles r ON r.id = ur.role_id + WHERE ur.user_id = ?1", + )?; + let bits: u32 = stmt + .query_map([user_id], |row| row.get::<_, i64>(0))? + .try_fold(1u32, |acc, val| val.map(|v| acc | v as u32))?; + Ok(bits) +} + pub fn ensure_default_user(conn: &Connection) -> Result { if let Some(id) = conn .query_row( @@ -1290,4 +1358,37 @@ mod tests { initialize_auth_schema(&conn).unwrap(); initialize_auth_schema(&conn).unwrap(); } + + fn make_auth_conn() -> Connection { + let conn = Connection::open_in_memory().unwrap(); + initialize_auth_schema(&conn).unwrap(); + conn + } + + #[test] + fn ensure_owner_exists_returns_false_when_no_owner() { + let conn = make_auth_conn(); + assert!(!ensure_owner_exists(&conn).unwrap()); + } + + #[test] + fn create_owner_then_ensure_returns_true() { + let conn = make_auth_conn(); + create_owner(&conn, "alice", "hashed_pw").unwrap(); + assert!(ensure_owner_exists(&conn).unwrap()); + } + + #[test] + fn create_owner_assigns_cumulative_roles() { + let conn = make_auth_conn(); + let user_id = create_owner(&conn, "alice", "hashed_pw").unwrap(); + let bits = compute_role_bits(&conn, user_id).unwrap(); + assert_eq!(bits, 15u32); + } + + #[test] + fn get_user_by_username_returns_none_for_unknown() { + let conn = make_auth_conn(); + assert!(get_user_by_username(&conn, "nobody").unwrap().is_none()); + } }