1
Fork 0
mirror of https://github.com/thegeneralist01/archivr synced 2026-07-21 18:55:36 +02:00

feat(auth): add initialize_auth_schema, open_auth_db, and auth record types

This commit is contained in:
TheGeneralist 2026-06-26 11:30:02 +02:00
parent 57fc48d73c
commit 696a5e1ac7
Signed by: thegeneralist01
SSH key fingerprint: SHA256:pp9qddbCNmVNoSjevdvQvM5z0DHN7LTa8qBMbcMq/R4

View file

@ -73,6 +73,31 @@ pub struct TagRecord {
pub full_path: String,
}
#[derive(Debug, Clone)]
pub struct AuthUserRecord {
pub id: i64,
pub user_uid: String,
pub username: String,
pub password_hash: String,
pub status: String,
}
#[derive(Debug, Clone)]
pub struct SessionRecord {
pub user_id: i64,
pub role_bits: u32,
pub last_seen_at: String,
pub session_uid: String,
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct ApiTokenRecord {
pub token_uid: String,
pub name: String,
pub created_at: String,
pub last_used_at: Option<String>,
}
pub fn database_path(archive_path: &Path) -> PathBuf {
archive_path.join(DATABASE_FILE_NAME)
}
@ -230,6 +255,101 @@ pub fn initialize_schema(conn: &Connection) -> Result<()> {
Ok(())
}
pub fn initialize_auth_schema(conn: &Connection) -> Result<()> {
conn.pragma_update(None, "journal_mode", "WAL")?;
conn.pragma_update(None, "foreign_keys", "ON")?;
conn.execute_batch(
r#"
CREATE TABLE IF NOT EXISTS roles (
id INTEGER PRIMARY KEY,
role_uid TEXT NOT NULL UNIQUE,
slug TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
level INTEGER NOT NULL,
bit_position INTEGER NOT NULL UNIQUE,
is_builtin INTEGER NOT NULL DEFAULT 0 CHECK (is_builtin IN (0, 1))
);
INSERT OR IGNORE INTO roles (role_uid, slug, name, level, bit_position, is_builtin) VALUES
('role-guest', 'guest', 'Guest', 0, 0, 1),
('role-user', 'user', 'User', 1, 1, 1),
('role-admin', 'admin', 'Admin', 3, 2, 1),
('role-owner', 'owner', 'Owner', 4, 3, 1);
CREATE TABLE IF NOT EXISTS user_roles (
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
role_id INTEGER NOT NULL REFERENCES roles(id),
assigned_at TEXT NOT NULL,
assigned_by_user_id INTEGER REFERENCES users(id),
PRIMARY KEY (user_id, role_id)
);
CREATE TABLE IF NOT EXISTS sessions (
id INTEGER PRIMARY KEY,
session_uid TEXT NOT NULL UNIQUE,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
role_bits INTEGER NOT NULL,
created_at TEXT NOT NULL,
last_seen_at TEXT NOT NULL,
expires_at TEXT NOT NULL,
user_agent TEXT
);
CREATE INDEX IF NOT EXISTS idx_sessions_user_id ON sessions(user_id);
CREATE TABLE IF NOT EXISTS api_tokens (
id INTEGER PRIMARY KEY,
token_uid TEXT NOT NULL UNIQUE,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
token_hash TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
created_at TEXT NOT NULL,
last_used_at TEXT,
expires_at TEXT
);
CREATE INDEX IF NOT EXISTS idx_api_tokens_user_id ON api_tokens(user_id);
CREATE TABLE IF NOT EXISTS instance_settings (
id INTEGER PRIMARY KEY CHECK (id = 1),
public_index_enabled INTEGER NOT NULL DEFAULT 0 CHECK (public_index_enabled IN (0, 1)),
public_entry_content_enabled INTEGER NOT NULL DEFAULT 0 CHECK (public_entry_content_enabled IN (0, 1)),
public_archive_submission_enabled INTEGER NOT NULL DEFAULT 0 CHECK (public_archive_submission_enabled IN (0, 1)),
default_entry_visibility INTEGER NOT NULL DEFAULT 2
);
INSERT OR IGNORE INTO instance_settings
(id, public_index_enabled, public_entry_content_enabled,
public_archive_submission_enabled, default_entry_visibility)
VALUES (1, 0, 0, 0, 2);
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY,
user_uid TEXT NOT NULL UNIQUE,
username TEXT NOT NULL UNIQUE,
email TEXT UNIQUE,
password_hash TEXT NOT NULL,
status TEXT NOT NULL CHECK (status IN ('active', 'disabled')),
role TEXT NOT NULL CHECK (role IN ('admin', 'user')),
created_at TEXT NOT NULL,
last_login_at TEXT
);
"#,
)?;
Ok(())
}
pub fn open_auth_db(auth_db_path: &Path) -> Result<Connection> {
if let Some(parent) = auth_db_path.parent() {
std::fs::create_dir_all(parent).with_context(|| {
format!("failed to create auth DB directory {}", parent.display())
})?;
}
let conn = Connection::open(auth_db_path).with_context(|| {
format!("failed to open auth database at {}", auth_db_path.display())
})?;
initialize_auth_schema(&conn)?;
Ok(conn)
}
pub fn ensure_default_user(conn: &Connection) -> Result<i64> {
if let Some(id) = conn
.query_row(
@ -1149,4 +1269,25 @@ mod tests {
let result = get_blob_by_sha256(&conn, "0000000000000000000000000000000000000000000000000000000000000000").unwrap();
assert!(result.is_none());
}
#[test]
fn auth_schema_seeds_builtin_roles() {
let conn = Connection::open_in_memory().unwrap();
initialize_auth_schema(&conn).unwrap();
let count: i64 = conn
.query_row("SELECT COUNT(*) FROM roles WHERE is_builtin = 1", [], |r| r.get(0))
.unwrap();
assert_eq!(count, 4);
let owner_bits: i64 = conn
.query_row("SELECT bit_position FROM roles WHERE slug = 'owner'", [], |r| r.get(0))
.unwrap();
assert_eq!(owner_bits, 3);
}
#[test]
fn auth_schema_is_idempotent() {
let conn = Connection::open_in_memory().unwrap();
initialize_auth_schema(&conn).unwrap();
initialize_auth_schema(&conn).unwrap();
}
}