use crate::errors::CrabError; use crate::settings::Database as DbCfg; use chrono::Utc; use rand::RngCore; use base64ct::Encoding; use sea_orm::{ConnectionTrait, Database, DatabaseConnection, DbBackend, Statement}; use serde::{Deserialize, Serialize}; use serde_json::Value; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Client { pub client_id: String, pub client_secret: String, pub client_name: Option, pub redirect_uris: Vec, pub created_at: i64, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct NewClient { pub client_name: Option, pub redirect_uris: Vec, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AuthCode { pub code: String, pub client_id: String, pub redirect_uri: String, pub scope: String, pub subject: String, pub nonce: Option, pub code_challenge: String, pub code_challenge_method: String, pub created_at: i64, pub expires_at: i64, pub consumed: i64, pub auth_time: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AccessToken { pub token: String, pub client_id: String, pub subject: String, pub scope: String, pub created_at: i64, pub expires_at: i64, pub revoked: i64, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct User { pub subject: String, pub username: String, pub password_hash: String, pub email: Option, pub email_verified: i64, pub created_at: i64, pub enabled: i64, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Session { pub session_id: String, pub subject: String, pub auth_time: i64, pub created_at: i64, pub expires_at: i64, pub user_agent: Option, pub ip_address: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct RefreshToken { pub token: String, pub client_id: String, pub subject: String, pub scope: String, pub created_at: i64, pub expires_at: i64, pub revoked: i64, pub parent_token: Option, // For token rotation tracking } pub async fn init(cfg: &DbCfg) -> Result { let db = Database::connect(&cfg.url).await?; // bootstrap schema db.execute(Statement::from_string(DbBackend::Sqlite, "PRAGMA foreign_keys = ON")) .await?; db.execute(Statement::from_string( DbBackend::Sqlite, r#" CREATE TABLE IF NOT EXISTS clients ( client_id TEXT PRIMARY KEY, client_secret TEXT NOT NULL, client_name TEXT, redirect_uris TEXT NOT NULL, created_at INTEGER NOT NULL ) "# )) .await?; db.execute(Statement::from_string( DbBackend::Sqlite, r#" CREATE TABLE IF NOT EXISTS properties ( owner TEXT NOT NULL, key TEXT NOT NULL, value TEXT NOT NULL, updated_at INTEGER NOT NULL, PRIMARY KEY(owner, key) ) "# )) .await?; db.execute(Statement::from_string( DbBackend::Sqlite, r#" CREATE TABLE IF NOT EXISTS auth_codes ( code TEXT PRIMARY KEY, client_id TEXT NOT NULL, redirect_uri TEXT NOT NULL, scope TEXT NOT NULL, subject TEXT NOT NULL, nonce TEXT, code_challenge TEXT NOT NULL, code_challenge_method TEXT NOT NULL, created_at INTEGER NOT NULL, expires_at INTEGER NOT NULL, consumed INTEGER NOT NULL DEFAULT 0, auth_time INTEGER ) "# )) .await?; db.execute(Statement::from_string( DbBackend::Sqlite, r#" CREATE TABLE IF NOT EXISTS access_tokens ( token TEXT PRIMARY KEY, client_id TEXT NOT NULL, subject TEXT NOT NULL, scope TEXT NOT NULL, created_at INTEGER NOT NULL, expires_at INTEGER NOT NULL, revoked INTEGER NOT NULL DEFAULT 0 ) "# )) .await?; db.execute(Statement::from_string( DbBackend::Sqlite, r#" CREATE TABLE IF NOT EXISTS users ( subject TEXT PRIMARY KEY, username TEXT NOT NULL UNIQUE, password_hash TEXT NOT NULL, email TEXT, email_verified INTEGER NOT NULL DEFAULT 0, created_at INTEGER NOT NULL, enabled INTEGER NOT NULL DEFAULT 1 ) "# )) .await?; db.execute(Statement::from_string( DbBackend::Sqlite, r#" CREATE TABLE IF NOT EXISTS sessions ( session_id TEXT PRIMARY KEY, subject TEXT NOT NULL, auth_time INTEGER NOT NULL, created_at INTEGER NOT NULL, expires_at INTEGER NOT NULL, user_agent TEXT, ip_address TEXT ) "# )) .await?; db.execute(Statement::from_string( DbBackend::Sqlite, "CREATE INDEX IF NOT EXISTS idx_sessions_expires ON sessions(expires_at)" )) .await?; db.execute(Statement::from_string( DbBackend::Sqlite, r#" CREATE TABLE IF NOT EXISTS refresh_tokens ( token TEXT PRIMARY KEY, client_id TEXT NOT NULL, subject TEXT NOT NULL, scope TEXT NOT NULL, created_at INTEGER NOT NULL, expires_at INTEGER NOT NULL, revoked INTEGER NOT NULL DEFAULT 0, parent_token TEXT ) "# )) .await?; db.execute(Statement::from_string( DbBackend::Sqlite, "CREATE INDEX IF NOT EXISTS idx_refresh_tokens_expires ON refresh_tokens(expires_at)" )) .await?; Ok(db) } pub async fn create_client(db: &DatabaseConnection, input: NewClient) -> Result { let client_id = random_id(); let client_secret = random_id(); let created_at = Utc::now().timestamp(); let redirect_uris_json = serde_json::to_string(&input.redirect_uris)?; db.execute(Statement::from_sql_and_values( DbBackend::Sqlite, r#"INSERT INTO clients (client_id, client_secret, client_name, redirect_uris, created_at) VALUES (?, ?, ?, ?, ?)"#, [ client_id.clone().into(), client_secret.clone().into(), input.client_name.clone().into(), redirect_uris_json.into(), created_at.into(), ], )) .await?; Ok(Client { client_id, client_secret, client_name: input.client_name, redirect_uris: input.redirect_uris, created_at, }) } pub async fn get_property( db: &DatabaseConnection, owner: &str, key: &str, ) -> Result, CrabError> { if let Some(row) = db .query_one(Statement::from_sql_and_values( DbBackend::Sqlite, "SELECT value FROM properties WHERE owner = ? AND key = ?", [owner.into(), key.into()], )) .await? { let value_str: String = row.try_get("", "value").unwrap_or_default(); let json: Value = serde_json::from_str(&value_str)?; Ok(Some(json)) } else { Ok(None) } } pub async fn set_property( db: &DatabaseConnection, owner: &str, key: &str, value: &Value, ) -> Result<(), CrabError> { let now = Utc::now().timestamp(); let json = serde_json::to_string(value)?; db.execute(Statement::from_sql_and_values( DbBackend::Sqlite, r#"INSERT INTO properties (owner, key, value, updated_at) VALUES (?, ?, ?, ?) ON CONFLICT(owner, key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at"#, [owner.into(), key.into(), json.into(), now.into()], )) .await?; Ok(()) } pub async fn get_client(db: &DatabaseConnection, client_id: &str) -> Result, CrabError> { if let Some(row) = db .query_one(Statement::from_sql_and_values( DbBackend::Sqlite, r#"SELECT client_id, client_secret, client_name, redirect_uris, created_at FROM clients WHERE client_id = ?"#, [client_id.into()], )) .await? { let client_id: String = row.try_get("", "client_id").unwrap_or_default(); let client_secret: String = row.try_get("", "client_secret").unwrap_or_default(); let client_name: Option = row.try_get("", "client_name").ok(); let redirect_uris_json: String = row.try_get("", "redirect_uris").unwrap_or_default(); let redirect_uris: Vec = serde_json::from_str(&redirect_uris_json).unwrap_or_default(); let created_at: i64 = row.try_get("", "created_at").unwrap_or_default(); Ok(Some(Client { client_id, client_secret, client_name, redirect_uris, created_at })) } else { Ok(None) } } pub async fn issue_auth_code( db: &DatabaseConnection, client_id: &str, redirect_uri: &str, scope: &str, subject: &str, nonce: Option, code_challenge: &str, code_challenge_method: &str, ttl_secs: i64, auth_time: Option, ) -> Result { let code = random_id(); let now = Utc::now().timestamp(); let expires_at = now + ttl_secs; db.execute(Statement::from_sql_and_values( DbBackend::Sqlite, r#"INSERT INTO auth_codes (code, client_id, redirect_uri, scope, subject, nonce, code_challenge, code_challenge_method, created_at, expires_at, consumed, auth_time) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, ?)"#, [ code.clone().into(), client_id.into(), redirect_uri.into(), scope.into(), subject.into(), nonce.clone().into(), code_challenge.into(), code_challenge_method.into(), now.into(), expires_at.into(), auth_time.into(), ], )) .await?; Ok(AuthCode { code, client_id: client_id.to_string(), redirect_uri: redirect_uri.to_string(), scope: scope.to_string(), subject: subject.to_string(), nonce, code_challenge: code_challenge.to_string(), code_challenge_method: code_challenge_method.to_string(), created_at: now, expires_at, consumed: 0, auth_time, }) } pub async fn consume_auth_code( db: &DatabaseConnection, code: &str, ) -> Result, CrabError> { if let Some(row) = db .query_one(Statement::from_sql_and_values( DbBackend::Sqlite, r#"SELECT code, client_id, redirect_uri, scope, subject, nonce, code_challenge, code_challenge_method, created_at, expires_at, consumed, auth_time FROM auth_codes WHERE code = ?"#, [code.into()], )) .await? { let consumed: i64 = row.try_get("", "consumed").unwrap_or_default(); let expires_at: i64 = row.try_get("", "expires_at").unwrap_or_default(); let now = Utc::now().timestamp(); if consumed != 0 || now > expires_at { return Ok(None); } // Mark as consumed db.execute(Statement::from_sql_and_values( DbBackend::Sqlite, r#"UPDATE auth_codes SET consumed = ? WHERE code = ?"#, [1.into(), code.into()], )) .await?; let code_val: String = row.try_get("", "code").unwrap_or_default(); let client_id: String = row.try_get("", "client_id").unwrap_or_default(); let redirect_uri: String = row.try_get("", "redirect_uri").unwrap_or_default(); let scope: String = row.try_get("", "scope").unwrap_or_default(); let subject: String = row.try_get("", "subject").unwrap_or_default(); let nonce: Option = row.try_get("", "nonce").ok(); let code_challenge: String = row.try_get("", "code_challenge").unwrap_or_default(); let code_challenge_method: String = row.try_get("", "code_challenge_method").unwrap_or_default(); let created_at: i64 = row.try_get("", "created_at").unwrap_or_default(); let expires_at: i64 = row.try_get("", "expires_at").unwrap_or_default(); let auth_time: Option = row.try_get("", "auth_time").ok(); Ok(Some(AuthCode { code: code_val, client_id, redirect_uri, scope, subject, nonce, code_challenge, code_challenge_method, created_at, expires_at, consumed: 1, auth_time })) } else { Ok(None) } } pub async fn issue_access_token( db: &DatabaseConnection, client_id: &str, subject: &str, scope: &str, ttl_secs: i64, ) -> Result { let token = random_id(); let now = Utc::now().timestamp(); let expires_at = now + ttl_secs; db.execute(Statement::from_sql_and_values( DbBackend::Sqlite, r#"INSERT INTO access_tokens (token, client_id, subject, scope, created_at, expires_at, revoked) VALUES (?, ?, ?, ?, ?, ?, 0)"#, [token.clone().into(), client_id.into(), subject.into(), scope.into(), now.into(), expires_at.into()], )) .await?; Ok(AccessToken { token, client_id: client_id.to_string(), subject: subject.to_string(), scope: scope.to_string(), created_at: now, expires_at, revoked: 0 }) } pub async fn get_access_token(db: &DatabaseConnection, token: &str) -> Result, CrabError> { if let Some(row) = db .query_one(Statement::from_sql_and_values( DbBackend::Sqlite, r#"SELECT token, client_id, subject, scope, created_at, expires_at, revoked FROM access_tokens WHERE token = ?"#, [token.into()], )) .await? { let revoked: i64 = row.try_get("", "revoked").unwrap_or_default(); let expires_at: i64 = row.try_get("", "expires_at").unwrap_or_default(); let now = Utc::now().timestamp(); if revoked != 0 || now > expires_at { return Ok(None); } let token: String = row.try_get("", "token").unwrap_or_default(); let client_id: String = row.try_get("", "client_id").unwrap_or_default(); let subject: String = row.try_get("", "subject").unwrap_or_default(); let scope: String = row.try_get("", "scope").unwrap_or_default(); let created_at: i64 = row.try_get("", "created_at").unwrap_or_default(); Ok(Some(AccessToken { token, client_id, subject, scope, created_at, expires_at, revoked })) } else { Ok(None) } } fn random_id() -> String { let mut bytes = [0u8; 24]; rand::thread_rng().fill_bytes(&mut bytes); base64ct::Base64UrlUnpadded::encode_string(&bytes) } // User management functions pub async fn create_user( db: &DatabaseConnection, username: &str, password: &str, email: Option, ) -> Result { use argon2::{Argon2, PasswordHasher}; use argon2::password_hash::{SaltString, rand_core::OsRng}; let subject = random_id(); let created_at = Utc::now().timestamp(); // Hash password with Argon2id let salt = SaltString::generate(&mut OsRng); let argon2 = Argon2::default(); let password_hash = argon2 .hash_password(password.as_bytes(), &salt) .map_err(|e| CrabError::Other(format!("Password hashing failed: {}", e)))? .to_string(); db.execute(Statement::from_sql_and_values( DbBackend::Sqlite, r#"INSERT INTO users (subject, username, password_hash, email, email_verified, created_at, enabled) VALUES (?, ?, ?, ?, 0, ?, 1)"#, [ subject.clone().into(), username.into(), password_hash.clone().into(), email.clone().into(), created_at.into(), ], )) .await?; Ok(User { subject, username: username.to_string(), password_hash, email, email_verified: 0, created_at, enabled: 1, }) } pub async fn get_user_by_username( db: &DatabaseConnection, username: &str, ) -> Result, CrabError> { if let Some(row) = db .query_one(Statement::from_sql_and_values( DbBackend::Sqlite, r#"SELECT subject, username, password_hash, email, email_verified, created_at, enabled FROM users WHERE username = ?"#, [username.into()], )) .await? { let subject: String = row.try_get("", "subject").unwrap_or_default(); let username: String = row.try_get("", "username").unwrap_or_default(); let password_hash: String = row.try_get("", "password_hash").unwrap_or_default(); let email: Option = row.try_get("", "email").ok(); let email_verified: i64 = row.try_get("", "email_verified").unwrap_or_default(); let created_at: i64 = row.try_get("", "created_at").unwrap_or_default(); let enabled: i64 = row.try_get("", "enabled").unwrap_or_default(); Ok(Some(User { subject, username, password_hash, email, email_verified, created_at, enabled, })) } else { Ok(None) } } pub async fn verify_user_password( db: &DatabaseConnection, username: &str, password: &str, ) -> Result, CrabError> { use argon2::{Argon2, PasswordVerifier, PasswordHash}; let user = match get_user_by_username(db, username).await? { Some(u) if u.enabled == 1 => u, _ => return Ok(None), }; let parsed_hash = PasswordHash::new(&user.password_hash) .map_err(|e| CrabError::Other(format!("Invalid password hash: {}", e)))?; if Argon2::default() .verify_password(password.as_bytes(), &parsed_hash) .is_ok() { Ok(Some(user.subject)) } else { Ok(None) } } // Session management functions pub async fn create_session( db: &DatabaseConnection, subject: &str, ttl_secs: i64, user_agent: Option, ip_address: Option, ) -> Result { let session_id = random_id(); let now = Utc::now().timestamp(); let expires_at = now + ttl_secs; db.execute(Statement::from_sql_and_values( DbBackend::Sqlite, r#"INSERT INTO sessions (session_id, subject, auth_time, created_at, expires_at, user_agent, ip_address) VALUES (?, ?, ?, ?, ?, ?, ?)"#, [ session_id.clone().into(), subject.into(), now.into(), now.into(), expires_at.into(), user_agent.clone().into(), ip_address.clone().into(), ], )) .await?; Ok(Session { session_id, subject: subject.to_string(), auth_time: now, created_at: now, expires_at, user_agent, ip_address, }) } pub async fn get_session( db: &DatabaseConnection, session_id: &str, ) -> Result, CrabError> { if let Some(row) = db .query_one(Statement::from_sql_and_values( DbBackend::Sqlite, r#"SELECT session_id, subject, auth_time, created_at, expires_at, user_agent, ip_address FROM sessions WHERE session_id = ?"#, [session_id.into()], )) .await? { let session_id: String = row.try_get("", "session_id").unwrap_or_default(); let subject: String = row.try_get("", "subject").unwrap_or_default(); let auth_time: i64 = row.try_get("", "auth_time").unwrap_or_default(); let created_at: i64 = row.try_get("", "created_at").unwrap_or_default(); let expires_at: i64 = row.try_get("", "expires_at").unwrap_or_default(); let user_agent: Option = row.try_get("", "user_agent").ok(); let ip_address: Option = row.try_get("", "ip_address").ok(); // Check if session is expired let now = Utc::now().timestamp(); if now > expires_at { return Ok(None); } Ok(Some(Session { session_id, subject, auth_time, created_at, expires_at, user_agent, ip_address, })) } else { Ok(None) } } pub async fn delete_session( db: &DatabaseConnection, session_id: &str, ) -> Result<(), CrabError> { db.execute(Statement::from_sql_and_values( DbBackend::Sqlite, "DELETE FROM sessions WHERE session_id = ?", [session_id.into()], )) .await?; Ok(()) } pub async fn cleanup_expired_sessions(db: &DatabaseConnection) -> Result { let now = Utc::now().timestamp(); let result = db .execute(Statement::from_sql_and_values( DbBackend::Sqlite, "DELETE FROM sessions WHERE expires_at < ?", [now.into()], )) .await?; Ok(result.rows_affected()) } // Refresh Token Functions pub async fn issue_refresh_token( db: &DatabaseConnection, client_id: &str, subject: &str, scope: &str, ttl_secs: i64, parent_token: Option, ) -> Result { let token = random_id(); let now = Utc::now().timestamp(); let expires_at = now + ttl_secs; db.execute(Statement::from_sql_and_values( DbBackend::Sqlite, r#"INSERT INTO refresh_tokens (token, client_id, subject, scope, created_at, expires_at, revoked, parent_token) VALUES (?, ?, ?, ?, ?, ?, 0, ?)"#, [ token.clone().into(), client_id.into(), subject.into(), scope.into(), now.into(), expires_at.into(), parent_token.clone().into(), ], )) .await?; Ok(RefreshToken { token, client_id: client_id.to_string(), subject: subject.to_string(), scope: scope.to_string(), created_at: now, expires_at, revoked: 0, parent_token, }) } pub async fn get_refresh_token( db: &DatabaseConnection, token: &str, ) -> Result, CrabError> { let result = db .query_one(Statement::from_sql_and_values( DbBackend::Sqlite, r#"SELECT token, client_id, subject, scope, created_at, expires_at, revoked, parent_token FROM refresh_tokens WHERE token = ?"#, [token.into()], )) .await?; if let Some(row) = result { let token: String = row.try_get("", "token")?; let client_id: String = row.try_get("", "client_id")?; let subject: String = row.try_get("", "subject")?; let scope: String = row.try_get("", "scope")?; let created_at: i64 = row.try_get("", "created_at")?; let expires_at: i64 = row.try_get("", "expires_at")?; let revoked: i64 = row.try_get("", "revoked")?; let parent_token: Option = row.try_get("", "parent_token").ok(); // Check if token is expired or revoked let now = Utc::now().timestamp(); if revoked != 0 || now > expires_at { return Ok(None); } Ok(Some(RefreshToken { token, client_id, subject, scope, created_at, expires_at, revoked, parent_token, })) } else { Ok(None) } } pub async fn revoke_refresh_token( db: &DatabaseConnection, token: &str, ) -> Result<(), CrabError> { db.execute(Statement::from_sql_and_values( DbBackend::Sqlite, "UPDATE refresh_tokens SET revoked = 1 WHERE token = ?", [token.into()], )) .await?; Ok(()) } pub async fn rotate_refresh_token( db: &DatabaseConnection, old_token: &str, client_id: &str, subject: &str, scope: &str, ttl_secs: i64, ) -> Result { // Revoke the old token revoke_refresh_token(db, old_token).await?; // Issue a new token with the old token as parent issue_refresh_token(db, client_id, subject, scope, ttl_secs, Some(old_token.to_string())).await } pub async fn cleanup_expired_refresh_tokens(db: &DatabaseConnection) -> Result { let now = Utc::now().timestamp(); let result = db .execute(Statement::from_sql_and_values( DbBackend::Sqlite, "DELETE FROM refresh_tokens WHERE expires_at < ?", [now.into()], )) .await?; Ok(result.rows_affected()) }