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:
parent
57fc48d73c
commit
696a5e1ac7
1 changed files with 141 additions and 0 deletions
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue