2025-07-07 23:05:19 +01:00

1678 lines
64 KiB
Rust

//! Database-agnostic authentication repository implementation
//!
//! This module provides a unified interface for user authentication operations
//! that works with both PostgreSQL and SQLite databases.
use crate::database::DatabaseType;
use crate::database::connection::{DatabaseConnection, DatabaseParam};
use anyhow::Result;
use async_trait::async_trait;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
/// Database-specific user representation
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DatabaseUser {
pub id: Uuid,
pub email: String,
pub username: Option<String>,
pub display_name: Option<String>,
pub password_hash: String,
pub avatar_url: Option<String>,
pub roles: Vec<String>,
pub is_active: bool,
pub is_verified: bool,
pub email_verified: bool,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub last_login: Option<DateTime<Utc>>,
pub two_factor_enabled: bool,
pub two_factor_secret: Option<String>,
pub backup_codes: Vec<String>,
}
/// Request struct for creating a new user
#[derive(Debug, Clone)]
pub struct CreateUserRequest {
pub email: String,
pub password_hash: String,
pub display_name: Option<String>,
pub username: Option<String>,
pub is_verified: bool,
pub is_active: bool,
}
/// Request struct for OAuth user creation
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub struct OAuthUserRequest {
pub email: String,
pub display_name: Option<String>,
pub username: Option<String>,
pub provider: String,
pub provider_id: String,
pub provider_data: serde_json::Value,
}
/// Request struct for creating a session
#[derive(Debug, Clone)]
pub struct CreateSessionRequest {
pub user_id: Uuid,
pub token: String,
pub expires_at: DateTime<Utc>,
pub user_agent: Option<String>,
pub ip_address: Option<String>,
}
/// User session representation
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UserSession {
pub id: Uuid,
pub user_id: Uuid,
pub token: String,
pub expires_at: DateTime<Utc>,
pub created_at: DateTime<Utc>,
pub last_used_at: Option<DateTime<Utc>>,
pub user_agent: Option<String>,
pub ip_address: Option<String>,
pub is_active: bool,
}
/// Database-agnostic authentication repository trait
#[async_trait]
#[allow(dead_code)]
pub trait AuthRepositoryTrait: Send + Sync + Clone + 'static {
/// Initialize database tables
async fn init_tables(&self) -> Result<()>;
/// Create a new user
async fn create_user(&self, user: &CreateUserRequest) -> Result<DatabaseUser>;
/// Find user by email
async fn find_user_by_email(&self, email: &str) -> Result<Option<DatabaseUser>>;
/// Find user by ID
async fn find_user_by_id(&self, id: &Uuid) -> Result<Option<DatabaseUser>>;
/// Update user information
async fn update_user(&self, user: &DatabaseUser) -> Result<()>;
/// Delete user
async fn delete_user(&self, id: &Uuid) -> Result<()>;
/// Verify user password
async fn verify_password(&self, email: &str, password_hash: &str) -> Result<bool>;
/// Update user password
async fn update_password(&self, id: &Uuid, password_hash: &str) -> Result<()>;
/// Get user roles
async fn get_user_roles(&self, id: &Uuid) -> Result<Vec<String>>;
/// Add role to user
async fn add_user_role(&self, id: &Uuid, role: &str) -> Result<()>;
/// Remove role from user
async fn remove_user_role(&self, id: &Uuid, role: &str) -> Result<()>;
/// Create OAuth user
async fn create_oauth_user(&self, user: &OAuthUserRequest) -> Result<DatabaseUser>;
/// Find user by OAuth provider
async fn find_user_by_oauth(
&self,
provider: &str,
provider_id: &str,
) -> Result<Option<DatabaseUser>>;
/// Get user profile
async fn get_user_profile(&self, id: &Uuid) -> Result<Option<DatabaseUser>>;
/// Update user profile
async fn update_user_profile(&self, user: &DatabaseUser) -> Result<()>;
/// Get user sessions
async fn get_user_sessions(&self, user_id: &Uuid) -> Result<Vec<UserSession>>;
/// Create session
async fn create_session(&self, session: &CreateSessionRequest) -> Result<UserSession>;
/// Get session by token
async fn get_session_by_token(&self, token: &str) -> Result<Option<UserSession>>;
/// Update session
async fn update_session(&self, session: &UserSession) -> Result<()>;
/// Delete session
async fn delete_session(&self, token: &str) -> Result<()>;
/// Cleanup expired sessions
async fn cleanup_expired_sessions(&self) -> Result<u64>;
/// Check if email exists
async fn email_exists(&self, email: &str) -> Result<bool>;
/// Check if username exists
async fn username_exists(&self, username: &str) -> Result<bool>;
/// Find session by ID
async fn find_session(&self, session_id: &str) -> Result<Option<UserSession>>;
/// Update session last accessed time
async fn update_session_accessed(&self, session_id: &str) -> Result<()>;
/// Update last login time
async fn update_last_login(&self, user_id: Uuid) -> Result<()>;
/// Invalidate all user sessions
async fn invalidate_all_user_sessions(&self, user_id: Uuid) -> Result<()>;
/// Create OAuth account link
async fn create_oauth_account(
&self,
user_id: Uuid,
provider: &str,
provider_id: &str,
) -> Result<()>;
/// Find user by OAuth account
async fn find_user_by_oauth_account(
&self,
provider: &str,
provider_id: &str,
) -> Result<Option<DatabaseUser>>;
/// Create token for password reset, verification, etc.
async fn create_token(
&self,
user_id: Uuid,
token_type: &str,
token_hash: &str,
expires_at: chrono::DateTime<chrono::Utc>,
) -> Result<()>;
/// Find token by hash and type
async fn find_token(
&self,
token_hash: &str,
token_type: &str,
) -> Result<Option<(Uuid, chrono::DateTime<chrono::Utc>)>>;
/// Mark token as used
async fn use_token(&self, token_hash: &str, token_type: &str) -> Result<()>;
/// Verify user email address
async fn verify_email(&self, user_id: Uuid) -> Result<()>;
/// Cleanup expired tokens
async fn cleanup_expired_tokens(&self) -> Result<u64>;
}
/// Database-agnostic authentication repository implementation
#[derive(Debug, Clone)]
pub struct AuthRepository {
database: DatabaseConnection,
}
impl AuthRepository {
/// Create a new auth repository with a database connection
pub fn new(database: DatabaseConnection) -> Self {
Self { database }
}
/// Get the database type
pub fn database_type(&self) -> DatabaseType {
self.database.database_type()
}
/// Create from database pool (convenience method)
pub fn from_pool(pool: &crate::database::DatabasePool) -> Self {
let connection = DatabaseConnection::from_pool(pool);
Self::new(connection)
}
/// Get a reference to the database connection (for compatibility)
pub fn pool(&self) -> &DatabaseConnection {
&self.database
}
}
#[async_trait]
impl AuthRepositoryTrait for AuthRepository {
async fn init_tables(&self) -> Result<()> {
match self.database.database_type() {
DatabaseType::PostgreSQL => self.init_postgres_tables().await,
DatabaseType::SQLite => self.init_sqlite_tables().await,
}
}
async fn create_user(&self, user: &CreateUserRequest) -> Result<DatabaseUser> {
match self.database.database_type() {
DatabaseType::PostgreSQL => self.create_user_postgres(user).await,
DatabaseType::SQLite => self.create_user_sqlite(user).await,
}
}
async fn find_user_by_email(&self, email: &str) -> Result<Option<DatabaseUser>> {
match self.database.database_type() {
DatabaseType::PostgreSQL => self.find_user_by_email_postgres(email).await,
DatabaseType::SQLite => self.find_user_by_email_sqlite(email).await,
}
}
async fn find_user_by_id(&self, id: &Uuid) -> Result<Option<DatabaseUser>> {
match self.database.database_type() {
DatabaseType::PostgreSQL => self.find_user_by_id_postgres(id).await,
DatabaseType::SQLite => self.find_user_by_id_sqlite(id).await,
}
}
async fn update_user(&self, user: &DatabaseUser) -> Result<()> {
match self.database.database_type() {
DatabaseType::PostgreSQL => self.update_user_postgres(user).await,
DatabaseType::SQLite => self.update_user_sqlite(user).await,
}
}
async fn delete_user(&self, id: &Uuid) -> Result<()> {
match self.database.database_type() {
DatabaseType::PostgreSQL => self.delete_user_postgres(id).await,
DatabaseType::SQLite => self.delete_user_sqlite(id).await,
}
}
async fn verify_password(&self, email: &str, password_hash: &str) -> Result<bool> {
match self.database.database_type() {
DatabaseType::PostgreSQL => self.verify_password_postgres(email, password_hash).await,
DatabaseType::SQLite => self.verify_password_sqlite(email, password_hash).await,
}
}
async fn update_password(&self, id: &Uuid, password_hash: &str) -> Result<()> {
match self.database.database_type() {
DatabaseType::PostgreSQL => self.update_password_postgres(id, password_hash).await,
DatabaseType::SQLite => self.update_password_sqlite(id, password_hash).await,
}
}
async fn get_user_roles(&self, id: &Uuid) -> Result<Vec<String>> {
match self.database.database_type() {
DatabaseType::PostgreSQL => self.get_user_roles_postgres(id).await,
DatabaseType::SQLite => self.get_user_roles_sqlite(id).await,
}
}
async fn add_user_role(&self, id: &Uuid, role: &str) -> Result<()> {
match self.database.database_type() {
DatabaseType::PostgreSQL => self.add_user_role_postgres(id, role).await,
DatabaseType::SQLite => self.add_user_role_sqlite(id, role).await,
}
}
async fn remove_user_role(&self, id: &Uuid, role: &str) -> Result<()> {
match self.database.database_type() {
DatabaseType::PostgreSQL => self.remove_user_role_postgres(id, role).await,
DatabaseType::SQLite => self.remove_user_role_sqlite(id, role).await,
}
}
async fn create_oauth_user(&self, user: &OAuthUserRequest) -> Result<DatabaseUser> {
match self.database.database_type() {
DatabaseType::PostgreSQL => self.create_oauth_user_postgres(user).await,
DatabaseType::SQLite => self.create_oauth_user_sqlite(user).await,
}
}
async fn find_user_by_oauth(
&self,
provider: &str,
provider_id: &str,
) -> Result<Option<DatabaseUser>> {
match self.database.database_type() {
DatabaseType::PostgreSQL => {
self.find_user_by_oauth_postgres(provider, provider_id)
.await
}
DatabaseType::SQLite => self.find_user_by_oauth_sqlite(provider, provider_id).await,
}
}
async fn get_user_profile(&self, id: &Uuid) -> Result<Option<DatabaseUser>> {
match self.database.database_type() {
DatabaseType::PostgreSQL => self.get_user_profile_postgres(id).await,
DatabaseType::SQLite => self.get_user_profile_sqlite(id).await,
}
}
async fn update_user_profile(&self, user: &DatabaseUser) -> Result<()> {
match self.database.database_type() {
DatabaseType::PostgreSQL => self.update_user_profile_postgres(user).await,
DatabaseType::SQLite => self.update_user_profile_sqlite(user).await,
}
}
async fn get_user_sessions(&self, user_id: &Uuid) -> Result<Vec<UserSession>> {
match self.database.database_type() {
DatabaseType::PostgreSQL => self.get_user_sessions_postgres(user_id).await,
DatabaseType::SQLite => self.get_user_sessions_sqlite(user_id).await,
}
}
async fn create_session(&self, session: &CreateSessionRequest) -> Result<UserSession> {
match self.database.database_type() {
DatabaseType::PostgreSQL => self.create_session_postgres(session).await,
DatabaseType::SQLite => self.create_session_sqlite(session).await,
}
}
async fn get_session_by_token(&self, token: &str) -> Result<Option<UserSession>> {
match self.database.database_type() {
DatabaseType::PostgreSQL => self.get_session_by_token_postgres(token).await,
DatabaseType::SQLite => self.get_session_by_token_sqlite(token).await,
}
}
async fn update_session(&self, session: &UserSession) -> Result<()> {
match self.database.database_type() {
DatabaseType::PostgreSQL => self.update_session_postgres(session).await,
DatabaseType::SQLite => self.update_session_sqlite(session).await,
}
}
async fn delete_session(&self, token: &str) -> Result<()> {
match self.database.database_type() {
DatabaseType::PostgreSQL => self.delete_session_postgres(token).await,
DatabaseType::SQLite => self.delete_session_sqlite(token).await,
}
}
async fn cleanup_expired_sessions(&self) -> Result<u64> {
match self.database.database_type() {
DatabaseType::PostgreSQL => self.cleanup_expired_sessions_postgres().await,
DatabaseType::SQLite => self.cleanup_expired_sessions_sqlite().await,
}
}
async fn email_exists(&self, email: &str) -> Result<bool> {
match self.find_user_by_email(email).await {
Ok(Some(_)) => Ok(true),
Ok(None) => Ok(false),
Err(e) => Err(e),
}
}
async fn username_exists(&self, username: &str) -> Result<bool> {
match self.database.database_type() {
DatabaseType::PostgreSQL => self.username_exists_postgres(username).await,
DatabaseType::SQLite => self.username_exists_sqlite(username).await,
}
}
async fn find_session(&self, session_id: &str) -> Result<Option<UserSession>> {
self.get_session_by_token(session_id).await
}
async fn update_session_accessed(&self, session_id: &str) -> Result<()> {
match self.database.database_type() {
DatabaseType::PostgreSQL => self.update_session_accessed_postgres(session_id).await,
DatabaseType::SQLite => self.update_session_accessed_sqlite(session_id).await,
}
}
async fn update_last_login(&self, user_id: Uuid) -> Result<()> {
match self.database.database_type() {
DatabaseType::PostgreSQL => self.update_last_login_postgres(user_id).await,
DatabaseType::SQLite => self.update_last_login_sqlite(user_id).await,
}
}
async fn invalidate_all_user_sessions(&self, user_id: Uuid) -> Result<()> {
match self.database.database_type() {
DatabaseType::PostgreSQL => self.invalidate_all_user_sessions_postgres(user_id).await,
DatabaseType::SQLite => self.invalidate_all_user_sessions_sqlite(user_id).await,
}
}
async fn create_oauth_account(
&self,
user_id: Uuid,
provider: &str,
provider_id: &str,
) -> Result<()> {
match self.database.database_type() {
DatabaseType::PostgreSQL => {
self.create_oauth_account_postgres(user_id, provider, provider_id)
.await
}
DatabaseType::SQLite => {
self.create_oauth_account_sqlite(user_id, provider, provider_id)
.await
}
}
}
async fn find_user_by_oauth_account(
&self,
provider: &str,
provider_id: &str,
) -> Result<Option<DatabaseUser>> {
self.find_user_by_oauth(provider, provider_id).await
}
async fn create_token(
&self,
user_id: Uuid,
token_type: &str,
token_hash: &str,
expires_at: chrono::DateTime<chrono::Utc>,
) -> Result<()> {
match self.database.database_type() {
DatabaseType::PostgreSQL => {
self.create_token_postgres(user_id, token_type, token_hash, expires_at)
.await
}
DatabaseType::SQLite => {
self.create_token_sqlite(user_id, token_type, token_hash, expires_at)
.await
}
}
}
async fn find_token(
&self,
token_hash: &str,
token_type: &str,
) -> Result<Option<(Uuid, chrono::DateTime<chrono::Utc>)>> {
match self.database.database_type() {
DatabaseType::PostgreSQL => self.find_token_postgres(token_hash, token_type).await,
DatabaseType::SQLite => self.find_token_sqlite(token_hash, token_type).await,
}
}
async fn use_token(&self, token_hash: &str, token_type: &str) -> Result<()> {
match self.database.database_type() {
DatabaseType::PostgreSQL => self.use_token_postgres(token_hash, token_type).await,
DatabaseType::SQLite => self.use_token_sqlite(token_hash, token_type).await,
}
}
async fn verify_email(&self, user_id: Uuid) -> Result<()> {
match self.database.database_type() {
DatabaseType::PostgreSQL => self.verify_email_postgres(user_id).await,
DatabaseType::SQLite => self.verify_email_sqlite(user_id).await,
}
}
async fn cleanup_expired_tokens(&self) -> Result<u64> {
match self.database.database_type() {
DatabaseType::PostgreSQL => self.cleanup_expired_tokens_postgres().await,
DatabaseType::SQLite => self.cleanup_expired_tokens_sqlite().await,
}
}
}
// PostgreSQL implementations
impl AuthRepository {
async fn init_postgres_tables(&self) -> Result<()> {
// Create users table
let create_users_table = r#"
CREATE TABLE IF NOT EXISTS users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email VARCHAR(255) UNIQUE NOT NULL,
username VARCHAR(255) UNIQUE,
display_name VARCHAR(255),
password_hash VARCHAR(255) NOT NULL,
avatar_url TEXT,
roles TEXT[] DEFAULT ARRAY[]::TEXT[],
is_active BOOLEAN DEFAULT TRUE,
is_verified BOOLEAN DEFAULT FALSE,
email_verified BOOLEAN DEFAULT FALSE,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
last_login TIMESTAMPTZ,
two_factor_enabled BOOLEAN DEFAULT FALSE,
two_factor_secret VARCHAR(255),
backup_codes TEXT[] DEFAULT ARRAY[]::TEXT[]
)
"#;
// Create sessions table
let create_sessions_table = r#"
CREATE TABLE IF NOT EXISTS user_sessions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
token VARCHAR(255) UNIQUE NOT NULL,
expires_at TIMESTAMPTZ NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW(),
last_used_at TIMESTAMPTZ,
user_agent TEXT,
ip_address INET,
is_active BOOLEAN DEFAULT TRUE
)
"#;
// Create OAuth providers table
let create_oauth_table = r#"
CREATE TABLE IF NOT EXISTS oauth_providers (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
provider VARCHAR(50) NOT NULL,
provider_id VARCHAR(255) NOT NULL,
provider_data JSONB,
created_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(provider, provider_id)
)
"#;
// Create indexes
let create_indexes = vec![
"CREATE INDEX IF NOT EXISTS idx_users_email ON users(email)",
"CREATE INDEX IF NOT EXISTS idx_users_username ON users(username)",
"CREATE INDEX IF NOT EXISTS idx_sessions_token ON user_sessions(token)",
"CREATE INDEX IF NOT EXISTS idx_sessions_user_id ON user_sessions(user_id)",
"CREATE INDEX IF NOT EXISTS idx_oauth_provider ON oauth_providers(provider, provider_id)",
];
self.database.execute(create_users_table, &[]).await?;
self.database.execute(create_sessions_table, &[]).await?;
self.database.execute(create_oauth_table, &[]).await?;
for index in create_indexes {
self.database.execute(index, &[]).await?;
}
Ok(())
}
async fn create_user_postgres(&self, user: &CreateUserRequest) -> Result<DatabaseUser> {
let now = Utc::now();
let id = Uuid::new_v4();
let roles = serde_json::to_string(&vec!["user".to_string()])?;
let query = r#"
INSERT INTO users (id, email, username, display_name, password_hash, roles, is_active, is_verified, email_verified, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
RETURNING id, email, username, display_name, password_hash, avatar_url, roles, is_active, is_verified, email_verified, created_at, updated_at, last_login, two_factor_enabled, two_factor_secret, backup_codes
"#;
let params = vec![
DatabaseParam::Uuid(id),
DatabaseParam::String(user.email.clone()),
DatabaseParam::OptionalString(user.username.clone()),
DatabaseParam::OptionalString(user.display_name.clone()),
DatabaseParam::String(user.password_hash.clone()),
DatabaseParam::String(roles),
DatabaseParam::Bool(user.is_active),
DatabaseParam::Bool(user.is_verified),
DatabaseParam::Bool(user.is_verified),
DatabaseParam::DateTime(now),
DatabaseParam::DateTime(now),
];
let row = self.database.fetch_one(query, &params).await?;
Ok(DatabaseUser {
id: row.get_uuid("id")?,
email: row.get_string("email")?,
username: row.get_optional_string("username")?,
display_name: row.get_optional_string("display_name")?,
password_hash: row.get_string("password_hash")?,
avatar_url: row.get_optional_string("avatar_url")?,
roles: serde_json::from_str(&row.get_string("roles")?).unwrap_or_default(),
is_active: row.get_bool("is_active")?,
is_verified: row.get_bool("is_verified")?,
email_verified: row.get_bool("email_verified")?,
created_at: row.get_datetime("created_at")?,
updated_at: row.get_datetime("updated_at")?,
last_login: row.get_optional_datetime("last_login")?,
two_factor_enabled: row.get_bool("two_factor_enabled")?,
two_factor_secret: row.get_optional_string("two_factor_secret")?,
backup_codes: serde_json::from_str(
&row.get_optional_string("backup_codes")?.unwrap_or_default(),
)
.unwrap_or_default(),
})
}
async fn find_user_by_email_postgres(&self, email: &str) -> Result<Option<DatabaseUser>> {
let query = r#"
SELECT id, email, username, display_name, password_hash, avatar_url, roles, is_active, is_verified, email_verified, created_at, updated_at, last_login, two_factor_enabled, two_factor_secret, backup_codes
FROM users WHERE email = $1
"#;
let params = vec![DatabaseParam::String(email.to_string())];
let row = self.database.fetch_optional(query, &params).await?;
Ok(row.map(|r| DatabaseUser {
id: r.get_uuid("id").unwrap(),
email: r.get_string("email").unwrap(),
username: r.get_optional_string("username").unwrap(),
display_name: r.get_optional_string("display_name").unwrap(),
password_hash: r.get_string("password_hash").unwrap(),
avatar_url: r.get_optional_string("avatar_url").unwrap(),
roles: serde_json::from_str(&r.get_string("roles").unwrap()).unwrap_or_default(),
is_active: r.get_bool("is_active").unwrap(),
is_verified: r.get_bool("is_verified").unwrap(),
email_verified: r.get_bool("email_verified").unwrap(),
created_at: r.get_datetime("created_at").unwrap(),
updated_at: r.get_datetime("updated_at").unwrap(),
last_login: r.get_optional_datetime("last_login").unwrap(),
two_factor_enabled: r.get_bool("two_factor_enabled").unwrap(),
two_factor_secret: r.get_optional_string("two_factor_secret").unwrap(),
backup_codes: serde_json::from_str(
&r.get_optional_string("backup_codes")
.unwrap()
.unwrap_or_default(),
)
.unwrap_or_default(),
}))
}
async fn find_user_by_id_postgres(&self, id: &Uuid) -> Result<Option<DatabaseUser>> {
let query = r#"
SELECT id, email, username, display_name, password_hash, avatar_url, roles, is_active, is_verified, email_verified, created_at, updated_at, last_login, two_factor_enabled, two_factor_secret, backup_codes
FROM users WHERE id = $1
"#;
let params = vec![DatabaseParam::Uuid(*id)];
let row = self.database.fetch_optional(query, &params).await?;
Ok(row.map(|r| DatabaseUser {
id: r.get_uuid("id").unwrap(),
email: r.get_string("email").unwrap(),
username: r.get_optional_string("username").unwrap(),
display_name: r.get_optional_string("display_name").unwrap(),
password_hash: r.get_string("password_hash").unwrap(),
avatar_url: r.get_optional_string("avatar_url").unwrap(),
roles: serde_json::from_str(&r.get_string("roles").unwrap()).unwrap_or_default(),
is_active: r.get_bool("is_active").unwrap(),
is_verified: r.get_bool("is_verified").unwrap(),
email_verified: r.get_bool("email_verified").unwrap(),
created_at: r.get_datetime("created_at").unwrap(),
updated_at: r.get_datetime("updated_at").unwrap(),
last_login: r.get_optional_datetime("last_login").unwrap(),
two_factor_enabled: r.get_bool("two_factor_enabled").unwrap(),
two_factor_secret: r.get_optional_string("two_factor_secret").unwrap(),
backup_codes: serde_json::from_str(
&r.get_optional_string("backup_codes")
.unwrap()
.unwrap_or_default(),
)
.unwrap_or_default(),
}))
}
// SQLite implementations
async fn init_sqlite_tables(&self) -> Result<()> {
// Create users table
let create_users_table = r#"
CREATE TABLE IF NOT EXISTS users (
id TEXT PRIMARY KEY,
email TEXT UNIQUE NOT NULL,
username TEXT UNIQUE,
display_name TEXT,
password_hash TEXT NOT NULL,
avatar_url TEXT,
roles TEXT DEFAULT '[]',
is_active BOOLEAN DEFAULT TRUE,
is_verified BOOLEAN DEFAULT FALSE,
email_verified BOOLEAN DEFAULT FALSE,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
last_login TEXT,
two_factor_enabled BOOLEAN DEFAULT FALSE,
two_factor_secret TEXT,
backup_codes TEXT DEFAULT '[]'
)
"#;
// Create sessions table
let create_sessions_table = r#"
CREATE TABLE IF NOT EXISTS user_sessions (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
token TEXT UNIQUE NOT NULL,
expires_at TEXT NOT NULL,
created_at TEXT NOT NULL,
last_used_at TEXT,
user_agent TEXT,
ip_address TEXT,
is_active BOOLEAN DEFAULT TRUE,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
)
"#;
// Create OAuth providers table
let create_oauth_table = r#"
CREATE TABLE IF NOT EXISTS oauth_providers (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
provider TEXT NOT NULL,
provider_id TEXT NOT NULL,
provider_data TEXT,
created_at TEXT NOT NULL,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
UNIQUE(provider, provider_id)
)
"#;
// Create indexes
let create_indexes = vec![
"CREATE INDEX IF NOT EXISTS idx_users_email ON users(email)",
"CREATE INDEX IF NOT EXISTS idx_users_username ON users(username)",
"CREATE INDEX IF NOT EXISTS idx_sessions_token ON user_sessions(token)",
"CREATE INDEX IF NOT EXISTS idx_sessions_user_id ON user_sessions(user_id)",
"CREATE INDEX IF NOT EXISTS idx_oauth_provider ON oauth_providers(provider, provider_id)",
];
self.database.execute(create_users_table, &[]).await?;
self.database.execute(create_sessions_table, &[]).await?;
self.database.execute(create_oauth_table, &[]).await?;
for index in create_indexes {
self.database.execute(index, &[]).await?;
}
Ok(())
}
async fn create_user_sqlite(&self, user: &CreateUserRequest) -> Result<DatabaseUser> {
let now = Utc::now();
let id = Uuid::new_v4();
let roles = serde_json::to_string(&vec!["user".to_string()])?;
let query = r#"
INSERT INTO users (id, email, username, display_name, password_hash, roles, is_active, is_verified, email_verified, created_at, updated_at)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11)
"#;
let params = vec![
DatabaseParam::String(id.to_string()),
DatabaseParam::String(user.email.clone()),
DatabaseParam::OptionalString(user.username.clone()),
DatabaseParam::OptionalString(user.display_name.clone()),
DatabaseParam::String(user.password_hash.clone()),
DatabaseParam::String(roles),
DatabaseParam::Bool(user.is_active),
DatabaseParam::Bool(user.is_verified),
DatabaseParam::Bool(user.is_verified),
DatabaseParam::String(now.to_rfc3339()),
DatabaseParam::String(now.to_rfc3339()),
];
self.database.execute(query, &params).await?;
Ok(DatabaseUser {
id,
email: user.email.clone(),
username: user.username.clone(),
display_name: user.display_name.clone(),
password_hash: user.password_hash.clone(),
avatar_url: None,
roles: vec!["user".to_string()],
is_active: user.is_active,
is_verified: user.is_verified,
email_verified: user.is_verified,
created_at: now,
updated_at: now,
last_login: None,
two_factor_enabled: false,
two_factor_secret: None,
backup_codes: Vec::new(),
})
}
async fn find_user_by_email_sqlite(&self, email: &str) -> Result<Option<DatabaseUser>> {
let query = r#"
SELECT id, email, username, display_name, password_hash, avatar_url, roles, is_active, is_verified, email_verified, created_at, updated_at, last_login, two_factor_enabled, two_factor_secret, backup_codes
FROM users WHERE email = ?1
"#;
let params = vec![DatabaseParam::String(email.to_string())];
let row = self.database.fetch_optional(query, &params).await?;
Ok(row.map(|r| DatabaseUser {
id: Uuid::parse_str(&r.get_string("id").unwrap()).unwrap(),
email: r.get_string("email").unwrap(),
username: r.get_optional_string("username").unwrap(),
display_name: r.get_optional_string("display_name").unwrap(),
password_hash: r.get_string("password_hash").unwrap(),
avatar_url: r.get_optional_string("avatar_url").unwrap(),
roles: serde_json::from_str(&r.get_string("roles").unwrap()).unwrap_or_default(),
is_active: r.get_bool("is_active").unwrap(),
is_verified: r.get_bool("is_verified").unwrap(),
email_verified: r.get_bool("email_verified").unwrap(),
created_at: DateTime::parse_from_rfc3339(&r.get_string("created_at").unwrap())
.unwrap()
.with_timezone(&Utc),
updated_at: DateTime::parse_from_rfc3339(&r.get_string("updated_at").unwrap())
.unwrap()
.with_timezone(&Utc),
last_login: r.get_optional_string("last_login").unwrap().map(|s| {
DateTime::parse_from_rfc3339(&s)
.unwrap()
.with_timezone(&Utc)
}),
two_factor_enabled: r.get_bool("two_factor_enabled").unwrap(),
two_factor_secret: r.get_optional_string("two_factor_secret").unwrap(),
backup_codes: serde_json::from_str(
&r.get_optional_string("backup_codes")
.unwrap()
.unwrap_or_default(),
)
.unwrap_or_default(),
}))
}
async fn find_user_by_id_sqlite(&self, id: &Uuid) -> Result<Option<DatabaseUser>> {
let query = r#"
SELECT id, email, username, display_name, password_hash, avatar_url, roles, is_active, is_verified, email_verified, created_at, updated_at, last_login, two_factor_enabled, two_factor_secret, backup_codes
FROM users WHERE id = ?1
"#;
let params = vec![DatabaseParam::String(id.to_string())];
let row = self.database.fetch_optional(query, &params).await?;
Ok(row.map(|r| DatabaseUser {
id: Uuid::parse_str(&r.get_string("id").unwrap()).unwrap(),
email: r.get_string("email").unwrap(),
username: r.get_optional_string("username").unwrap(),
display_name: r.get_optional_string("display_name").unwrap(),
password_hash: r.get_string("password_hash").unwrap(),
avatar_url: r.get_optional_string("avatar_url").unwrap(),
roles: serde_json::from_str(&r.get_string("roles").unwrap()).unwrap_or_default(),
is_active: r.get_bool("is_active").unwrap(),
is_verified: r.get_bool("is_verified").unwrap(),
email_verified: r.get_bool("email_verified").unwrap(),
created_at: DateTime::parse_from_rfc3339(&r.get_string("created_at").unwrap())
.unwrap()
.with_timezone(&Utc),
updated_at: DateTime::parse_from_rfc3339(&r.get_string("updated_at").unwrap())
.unwrap()
.with_timezone(&Utc),
last_login: r.get_optional_string("last_login").unwrap().map(|s| {
DateTime::parse_from_rfc3339(&s)
.unwrap()
.with_timezone(&Utc)
}),
two_factor_enabled: r.get_bool("two_factor_enabled").unwrap(),
two_factor_secret: r.get_optional_string("two_factor_secret").unwrap(),
backup_codes: serde_json::from_str(
&r.get_optional_string("backup_codes")
.unwrap()
.unwrap_or_default(),
)
.unwrap_or_default(),
}))
}
// Stub implementations for remaining methods
async fn update_user_postgres(&self, _user: &DatabaseUser) -> Result<()> {
// TODO: Implement user update for PostgreSQL
Ok(())
}
async fn update_user_sqlite(&self, _user: &DatabaseUser) -> Result<()> {
// TODO: Implement user update for SQLite
Ok(())
}
async fn delete_user_postgres(&self, _id: &Uuid) -> Result<()> {
// TODO: Implement user deletion for PostgreSQL
Ok(())
}
async fn delete_user_sqlite(&self, _id: &Uuid) -> Result<()> {
// TODO: Implement user deletion for SQLite
Ok(())
}
async fn verify_password_postgres(&self, email: &str, password_hash: &str) -> Result<bool> {
let query = "SELECT password_hash FROM users WHERE email = $1 AND is_active = true LIMIT 1";
let params = vec![DatabaseParam::String(email.to_string())];
match self.database.fetch_optional(query, &params).await? {
Some(row) => {
let stored_hash = row.get_string("password_hash")?;
Ok(stored_hash == password_hash)
}
None => Ok(false),
}
}
async fn verify_password_sqlite(&self, email: &str, password_hash: &str) -> Result<bool> {
let query = "SELECT password_hash FROM users WHERE email = ? AND is_active = 1 LIMIT 1";
let params = vec![DatabaseParam::String(email.to_string())];
match self.database.fetch_optional(query, &params).await? {
Some(row) => {
let stored_hash = row.get_string("password_hash")?;
Ok(stored_hash == password_hash)
}
None => Ok(false),
}
}
async fn update_password_postgres(&self, id: &Uuid, password_hash: &str) -> Result<()> {
let query = "UPDATE users SET password_hash = $1, updated_at = NOW() WHERE id = $2";
let params = vec![
DatabaseParam::String(password_hash.to_string()),
DatabaseParam::Uuid(*id),
];
self.database.execute(query, &params).await?;
Ok(())
}
async fn update_password_sqlite(&self, id: &Uuid, password_hash: &str) -> Result<()> {
let query = "UPDATE users SET password_hash = ?, updated_at = datetime('now') WHERE id = ?";
let params = vec![
DatabaseParam::String(password_hash.to_string()),
DatabaseParam::String(id.to_string()),
];
self.database.execute(query, &params).await?;
Ok(())
}
async fn get_user_roles_postgres(&self, _id: &Uuid) -> Result<Vec<String>> {
// TODO: Implement get user roles for PostgreSQL
Ok(vec![])
}
async fn get_user_roles_sqlite(&self, _id: &Uuid) -> Result<Vec<String>> {
// TODO: Implement get user roles for SQLite
Ok(vec![])
}
async fn add_user_role_postgres(&self, _id: &Uuid, _role: &str) -> Result<()> {
// TODO: Implement add user role for PostgreSQL
Ok(())
}
async fn add_user_role_sqlite(&self, _id: &Uuid, _role: &str) -> Result<()> {
// TODO: Implement add user role for SQLite
Ok(())
}
async fn remove_user_role_postgres(&self, _id: &Uuid, _role: &str) -> Result<()> {
// TODO: Implement remove user role for PostgreSQL
Ok(())
}
async fn remove_user_role_sqlite(&self, _id: &Uuid, _role: &str) -> Result<()> {
// TODO: Implement remove user role for SQLite
Ok(())
}
async fn create_oauth_user_postgres(&self, _user: &OAuthUserRequest) -> Result<DatabaseUser> {
// TODO: Implement OAuth user creation for PostgreSQL
Err(anyhow::anyhow!("Not implemented"))
}
async fn create_oauth_user_sqlite(&self, _user: &OAuthUserRequest) -> Result<DatabaseUser> {
// TODO: Implement OAuth user creation for SQLite
Err(anyhow::anyhow!("Not implemented"))
}
async fn find_user_by_oauth_postgres(
&self,
provider: &str,
provider_id: &str,
) -> Result<Option<DatabaseUser>> {
let query = r#"
SELECT u.id, u.email, u.username, u.display_name, u.password_hash, u.avatar_url,
u.roles, u.is_active, u.is_verified, u.email_verified, u.created_at,
u.updated_at, u.last_login, u.two_factor_enabled, u.two_factor_secret, u.backup_codes
FROM users u
JOIN oauth_providers o ON u.id = o.user_id
WHERE o.provider = $1 AND o.provider_id = $2 AND u.is_active = true
LIMIT 1
"#;
let params = vec![
DatabaseParam::String(provider.to_string()),
DatabaseParam::String(provider_id.to_string()),
];
match self.database.fetch_optional(query, &params).await? {
Some(row) => Ok(Some(self.row_to_database_user(row)?)),
None => Ok(None),
}
}
async fn find_user_by_oauth_sqlite(
&self,
provider: &str,
provider_id: &str,
) -> Result<Option<DatabaseUser>> {
let query = r#"
SELECT u.id, u.email, u.username, u.display_name, u.password_hash, u.avatar_url,
u.roles, u.is_active, u.is_verified, u.email_verified, u.created_at,
u.updated_at, u.last_login, u.two_factor_enabled, u.two_factor_secret, u.backup_codes
FROM users u
JOIN oauth_providers o ON u.id = o.user_id
WHERE o.provider = ? AND o.provider_id = ? AND u.is_active = 1
LIMIT 1
"#;
let params = vec![
DatabaseParam::String(provider.to_string()),
DatabaseParam::String(provider_id.to_string()),
];
match self.database.fetch_optional(query, &params).await? {
Some(row) => Ok(Some(self.row_to_database_user(row)?)),
None => Ok(None),
}
}
async fn get_user_profile_postgres(&self, _id: &Uuid) -> Result<Option<DatabaseUser>> {
// TODO: Implement get user profile for PostgreSQL
Ok(None)
}
async fn get_user_profile_sqlite(&self, _id: &Uuid) -> Result<Option<DatabaseUser>> {
// TODO: Implement get user profile for SQLite
Ok(None)
}
async fn update_user_profile_postgres(&self, _user: &DatabaseUser) -> Result<()> {
// TODO: Implement update user profile for PostgreSQL
Ok(())
}
async fn update_user_profile_sqlite(&self, _user: &DatabaseUser) -> Result<()> {
// TODO: Implement update user profile for SQLite
Ok(())
}
async fn get_user_sessions_postgres(&self, _user_id: &Uuid) -> Result<Vec<UserSession>> {
// TODO: Implement get user sessions for PostgreSQL
Ok(vec![])
}
async fn get_user_sessions_sqlite(&self, _user_id: &Uuid) -> Result<Vec<UserSession>> {
// TODO: Implement get user sessions for SQLite
Ok(vec![])
}
async fn create_session_postgres(&self, session: &CreateSessionRequest) -> Result<UserSession> {
let id = Uuid::new_v4();
let now = Utc::now();
let query = r#"
INSERT INTO user_sessions (id, user_id, token, expires_at, created_at, last_used_at, user_agent, ip_address, is_active)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
RETURNING id, user_id, token, expires_at, created_at, last_used_at, user_agent, ip_address, is_active
"#;
let params = vec![
DatabaseParam::Uuid(id),
DatabaseParam::Uuid(session.user_id),
DatabaseParam::String(session.token.clone()),
DatabaseParam::DateTime(session.expires_at),
DatabaseParam::DateTime(now),
DatabaseParam::DateTime(now),
DatabaseParam::OptionalString(session.user_agent.clone()),
DatabaseParam::OptionalString(session.ip_address.clone()),
DatabaseParam::Bool(true),
];
let row = self.database.fetch_one(query, &params).await?;
Ok(UserSession {
id: row.get_uuid("id")?,
user_id: row.get_uuid("user_id")?,
token: row.get_string("token")?,
expires_at: row.get_datetime("expires_at")?,
created_at: row.get_datetime("created_at")?,
last_used_at: row.get_optional_datetime("last_used_at")?,
user_agent: row.get_optional_string("user_agent")?,
ip_address: row.get_optional_string("ip_address")?,
is_active: row.get_bool("is_active")?,
})
}
async fn create_session_sqlite(&self, session: &CreateSessionRequest) -> Result<UserSession> {
let id = Uuid::new_v4();
let now = Utc::now();
let query = r#"
INSERT INTO user_sessions (id, user_id, token, expires_at, created_at, last_used_at, user_agent, ip_address, is_active)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
RETURNING id, user_id, token, expires_at, created_at, last_used_at, user_agent, ip_address, is_active
"#;
let params = vec![
DatabaseParam::String(id.to_string()),
DatabaseParam::String(session.user_id.to_string()),
DatabaseParam::String(session.token.clone()),
DatabaseParam::String(session.expires_at.to_rfc3339()),
DatabaseParam::String(now.to_rfc3339()),
DatabaseParam::String(now.to_rfc3339()),
DatabaseParam::OptionalString(session.user_agent.clone()),
DatabaseParam::OptionalString(session.ip_address.clone()),
DatabaseParam::Bool(true),
];
let row = self.database.fetch_one(query, &params).await?;
Ok(UserSession {
id: row.get_uuid("id")?,
user_id: row.get_uuid("user_id")?,
token: row.get_string("token")?,
expires_at: row.get_datetime("expires_at")?,
created_at: row.get_datetime("created_at")?,
last_used_at: row.get_optional_datetime("last_used_at")?,
user_agent: row.get_optional_string("user_agent")?,
ip_address: row.get_optional_string("ip_address")?,
is_active: row.get_bool("is_active")?,
})
}
async fn get_session_by_token_postgres(&self, token: &str) -> Result<Option<UserSession>> {
let query = r#"
SELECT id, user_id, token, expires_at, created_at, last_used_at, user_agent, ip_address, is_active
FROM user_sessions
WHERE token = $1 AND is_active = true AND expires_at > NOW()
LIMIT 1
"#;
let params = vec![DatabaseParam::String(token.to_string())];
match self.database.fetch_optional(query, &params).await? {
Some(row) => Ok(Some(UserSession {
id: row.get_uuid("id")?,
user_id: row.get_uuid("user_id")?,
token: row.get_string("token")?,
expires_at: row.get_datetime("expires_at")?,
created_at: row.get_datetime("created_at")?,
last_used_at: row.get_optional_datetime("last_used_at")?,
user_agent: row.get_optional_string("user_agent")?,
ip_address: row.get_optional_string("ip_address")?,
is_active: row.get_bool("is_active")?,
})),
None => Ok(None),
}
}
async fn get_session_by_token_sqlite(&self, token: &str) -> Result<Option<UserSession>> {
let query = r#"
SELECT id, user_id, token, expires_at, created_at, last_used_at, user_agent, ip_address, is_active
FROM user_sessions
WHERE token = ? AND is_active = 1 AND expires_at > datetime('now')
LIMIT 1
"#;
let params = vec![DatabaseParam::String(token.to_string())];
match self.database.fetch_optional(query, &params).await? {
Some(row) => Ok(Some(UserSession {
id: row.get_uuid("id")?,
user_id: row.get_uuid("user_id")?,
token: row.get_string("token")?,
expires_at: row.get_datetime("expires_at")?,
created_at: row.get_datetime("created_at")?,
last_used_at: row.get_optional_datetime("last_used_at")?,
user_agent: row.get_optional_string("user_agent")?,
ip_address: row.get_optional_string("ip_address")?,
is_active: row.get_bool("is_active")?,
})),
None => Ok(None),
}
}
async fn update_session_postgres(&self, _session: &UserSession) -> Result<()> {
// TODO: Implement update session for PostgreSQL
Ok(())
}
async fn update_session_sqlite(&self, _session: &UserSession) -> Result<()> {
// TODO: Implement update session for SQLite
Ok(())
}
async fn delete_session_postgres(&self, token: &str) -> Result<()> {
let query = "DELETE FROM user_sessions WHERE token = $1";
let params = vec![DatabaseParam::String(token.to_string())];
self.database.execute(query, &params).await?;
Ok(())
}
async fn delete_session_sqlite(&self, token: &str) -> Result<()> {
let query = "DELETE FROM user_sessions WHERE token = ?";
let params = vec![DatabaseParam::String(token.to_string())];
self.database.execute(query, &params).await?;
Ok(())
}
async fn cleanup_expired_sessions_postgres(&self) -> Result<u64> {
let query = "DELETE FROM user_sessions WHERE expires_at <= NOW() OR is_active = false";
self.database.execute(query, &[]).await?;
// Note: In a real implementation, you'd want to return the actual count
// This would require a different approach or counting before deletion
Ok(0)
}
async fn cleanup_expired_sessions_sqlite(&self) -> Result<u64> {
let query =
"DELETE FROM user_sessions WHERE expires_at <= datetime('now') OR is_active = 0";
self.database.execute(query, &[]).await?;
// Note: In a real implementation, you'd want to return the actual count
// This would require a different approach or counting before deletion
Ok(0)
}
async fn username_exists_postgres(&self, username: &str) -> Result<bool> {
let query = "SELECT 1 FROM users WHERE username = $1 LIMIT 1";
let params = vec![DatabaseParam::String(username.to_string())];
match self.database.fetch_optional(query, &params).await? {
Some(_) => Ok(true),
None => Ok(false),
}
}
async fn username_exists_sqlite(&self, username: &str) -> Result<bool> {
let query = "SELECT 1 FROM users WHERE username = ? LIMIT 1";
let params = vec![DatabaseParam::String(username.to_string())];
match self.database.fetch_optional(query, &params).await? {
Some(_) => Ok(true),
None => Ok(false),
}
}
async fn update_session_accessed_postgres(&self, session_id: &str) -> Result<()> {
let query =
"UPDATE user_sessions SET last_used_at = NOW() WHERE token = $1 AND is_active = true";
let params = vec![DatabaseParam::String(session_id.to_string())];
self.database.execute(query, &params).await?;
Ok(())
}
async fn update_session_accessed_sqlite(&self, session_id: &str) -> Result<()> {
let query = "UPDATE user_sessions SET last_used_at = datetime('now') WHERE token = ? AND is_active = 1";
let params = vec![DatabaseParam::String(session_id.to_string())];
self.database.execute(query, &params).await?;
Ok(())
}
async fn update_last_login_postgres(&self, user_id: Uuid) -> Result<()> {
let query = "UPDATE users SET last_login = NOW(), updated_at = NOW() WHERE id = $1";
let params = vec![DatabaseParam::Uuid(user_id)];
self.database.execute(query, &params).await?;
Ok(())
}
async fn update_last_login_sqlite(&self, user_id: Uuid) -> Result<()> {
let query = "UPDATE users SET last_login = datetime('now'), updated_at = datetime('now') WHERE id = ?";
let params = vec![DatabaseParam::String(user_id.to_string())];
self.database.execute(query, &params).await?;
Ok(())
}
async fn invalidate_all_user_sessions_postgres(&self, user_id: Uuid) -> Result<()> {
let query = "UPDATE user_sessions SET is_active = false WHERE user_id = $1";
let params = vec![DatabaseParam::Uuid(user_id)];
self.database.execute(query, &params).await?;
Ok(())
}
async fn invalidate_all_user_sessions_sqlite(&self, user_id: Uuid) -> Result<()> {
let query = "UPDATE user_sessions SET is_active = 0 WHERE user_id = ?";
let params = vec![DatabaseParam::String(user_id.to_string())];
self.database.execute(query, &params).await?;
Ok(())
}
async fn create_oauth_account_postgres(
&self,
user_id: Uuid,
provider: &str,
provider_id: &str,
) -> Result<()> {
let query = r#"
INSERT INTO oauth_providers (user_id, provider, provider_id, created_at)
VALUES ($1, $2, $3, NOW())
ON CONFLICT (provider, provider_id) DO NOTHING
"#;
let params = vec![
DatabaseParam::Uuid(user_id),
DatabaseParam::String(provider.to_string()),
DatabaseParam::String(provider_id.to_string()),
];
self.database.execute(query, &params).await?;
Ok(())
}
async fn create_oauth_account_sqlite(
&self,
user_id: Uuid,
provider: &str,
provider_id: &str,
) -> Result<()> {
let query = r#"
INSERT OR IGNORE INTO oauth_providers (user_id, provider, provider_id, created_at)
VALUES (?, ?, ?, datetime('now'))
"#;
let params = vec![
DatabaseParam::String(user_id.to_string()),
DatabaseParam::String(provider.to_string()),
DatabaseParam::String(provider_id.to_string()),
];
self.database.execute(query, &params).await?;
Ok(())
}
async fn create_token_postgres(
&self,
user_id: Uuid,
token_type: &str,
token_hash: &str,
expires_at: chrono::DateTime<chrono::Utc>,
) -> Result<()> {
// First, ensure tokens table exists
let create_table = r#"
CREATE TABLE IF NOT EXISTS user_tokens (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
token_hash VARCHAR(255) NOT NULL,
token_type VARCHAR(50) NOT NULL,
expires_at TIMESTAMPTZ NOT NULL,
used_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(token_hash, token_type)
)
"#;
self.database.execute(create_table, &[]).await?;
let query = r#"
INSERT INTO user_tokens (user_id, token_hash, token_type, expires_at)
VALUES ($1, $2, $3, $4)
ON CONFLICT (token_hash, token_type) DO UPDATE SET
expires_at = EXCLUDED.expires_at,
used_at = NULL
"#;
let params = vec![
DatabaseParam::Uuid(user_id),
DatabaseParam::String(token_hash.to_string()),
DatabaseParam::String(token_type.to_string()),
DatabaseParam::DateTime(expires_at),
];
self.database.execute(query, &params).await?;
Ok(())
}
async fn create_token_sqlite(
&self,
user_id: Uuid,
token_type: &str,
token_hash: &str,
expires_at: chrono::DateTime<chrono::Utc>,
) -> Result<()> {
// First, ensure tokens table exists
let create_table = r#"
CREATE TABLE IF NOT EXISTS user_tokens (
id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
user_id TEXT NOT NULL,
token_hash TEXT NOT NULL,
token_type TEXT NOT NULL,
expires_at TEXT NOT NULL,
used_at TEXT,
created_at TEXT DEFAULT (datetime('now')),
UNIQUE(token_hash, token_type),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
)
"#;
self.database.execute(create_table, &[]).await?;
let query = r#"
INSERT OR REPLACE INTO user_tokens (user_id, token_hash, token_type, expires_at)
VALUES (?, ?, ?, ?)
"#;
let params = vec![
DatabaseParam::String(user_id.to_string()),
DatabaseParam::String(token_hash.to_string()),
DatabaseParam::String(token_type.to_string()),
DatabaseParam::String(expires_at.to_rfc3339()),
];
self.database.execute(query, &params).await?;
Ok(())
}
async fn find_token_postgres(
&self,
token_hash: &str,
token_type: &str,
) -> Result<Option<(Uuid, chrono::DateTime<chrono::Utc>)>> {
let query = r#"
SELECT user_id, expires_at
FROM user_tokens
WHERE token_hash = $1 AND token_type = $2
AND expires_at > NOW() AND used_at IS NULL
LIMIT 1
"#;
let params = vec![
DatabaseParam::String(token_hash.to_string()),
DatabaseParam::String(token_type.to_string()),
];
match self.database.fetch_optional(query, &params).await? {
Some(row) => {
let user_id = row.get_uuid("user_id")?;
let expires_at = row.get_datetime("expires_at")?;
Ok(Some((user_id, expires_at)))
}
None => Ok(None),
}
}
async fn find_token_sqlite(
&self,
token_hash: &str,
token_type: &str,
) -> Result<Option<(Uuid, chrono::DateTime<chrono::Utc>)>> {
let query = r#"
SELECT user_id, expires_at
FROM user_tokens
WHERE token_hash = ? AND token_type = ?
AND expires_at > datetime('now') AND used_at IS NULL
LIMIT 1
"#;
let params = vec![
DatabaseParam::String(token_hash.to_string()),
DatabaseParam::String(token_type.to_string()),
];
match self.database.fetch_optional(query, &params).await? {
Some(row) => {
let user_id = row.get_uuid("user_id")?;
let expires_at = row.get_datetime("expires_at")?;
Ok(Some((user_id, expires_at)))
}
None => Ok(None),
}
}
async fn use_token_postgres(&self, token_hash: &str, token_type: &str) -> Result<()> {
let query = r#"
UPDATE user_tokens
SET used_at = NOW()
WHERE token_hash = $1 AND token_type = $2 AND used_at IS NULL
"#;
let params = vec![
DatabaseParam::String(token_hash.to_string()),
DatabaseParam::String(token_type.to_string()),
];
self.database.execute(query, &params).await?;
Ok(())
}
async fn use_token_sqlite(&self, token_hash: &str, token_type: &str) -> Result<()> {
let query = r#"
UPDATE user_tokens
SET used_at = datetime('now')
WHERE token_hash = ? AND token_type = ? AND used_at IS NULL
"#;
let params = vec![
DatabaseParam::String(token_hash.to_string()),
DatabaseParam::String(token_type.to_string()),
];
self.database.execute(query, &params).await?;
Ok(())
}
async fn verify_email_postgres(&self, user_id: Uuid) -> Result<()> {
let query = r#"
UPDATE users
SET email_verified = true, is_verified = true, updated_at = NOW()
WHERE id = $1
"#;
let params = vec![DatabaseParam::Uuid(user_id)];
self.database.execute(query, &params).await?;
Ok(())
}
async fn verify_email_sqlite(&self, user_id: Uuid) -> Result<()> {
let query = r#"
UPDATE users
SET email_verified = 1, is_verified = 1, updated_at = datetime('now')
WHERE id = ?
"#;
let params = vec![DatabaseParam::String(user_id.to_string())];
self.database.execute(query, &params).await?;
Ok(())
}
async fn cleanup_expired_tokens_postgres(&self) -> Result<u64> {
let query = "DELETE FROM user_tokens WHERE expires_at <= NOW()";
self.database.execute(query, &[]).await?;
// Note: In a real implementation, you'd want to return the actual count
// This would require a different approach or counting before deletion
Ok(0)
}
async fn cleanup_expired_tokens_sqlite(&self) -> Result<u64> {
let query = "DELETE FROM user_tokens WHERE expires_at <= datetime('now')";
self.database.execute(query, &[]).await?;
// Note: In a real implementation, you'd want to return the actual count
// This would require a different approach or counting before deletion
Ok(0)
}
// Helper method to convert database row to DatabaseUser
fn row_to_database_user(
&self,
row: crate::database::connection::DatabaseRow,
) -> Result<DatabaseUser> {
let roles_json = row.get_string("roles")?;
let roles: Vec<String> = serde_json::from_str(&roles_json)
.map_err(|e| anyhow::anyhow!("Failed to parse roles JSON: {}", e))?;
let backup_codes_json = row.get_optional_string("backup_codes")?;
let backup_codes: Vec<String> = backup_codes_json
.map(|json| serde_json::from_str(&json).unwrap_or_default())
.unwrap_or_default();
Ok(DatabaseUser {
id: row.get_uuid("id")?,
email: row.get_string("email")?,
username: row.get_optional_string("username")?,
display_name: row.get_optional_string("display_name")?,
password_hash: row.get_string("password_hash")?,
avatar_url: row.get_optional_string("avatar_url")?,
roles,
is_active: row.get_bool("is_active")?,
is_verified: row.get_bool("is_verified")?,
email_verified: row.get_bool("email_verified")?,
created_at: row.get_datetime("created_at")?,
updated_at: row.get_datetime("updated_at")?,
last_login: row.get_optional_datetime("last_login")?,
two_factor_enabled: row.get_bool("two_factor_enabled")?,
two_factor_secret: row.get_optional_string("two_factor_secret")?,
backup_codes,
})
}
}
impl From<DatabaseUser> for shared::auth::User {
fn from(db_user: DatabaseUser) -> Self {
Self {
id: db_user.id,
email: db_user.email,
username: db_user.username.unwrap_or_default(),
display_name: db_user.display_name,
avatar_url: db_user.avatar_url,
roles: db_user
.roles
.into_iter()
.map(|r| match r.as_str() {
"admin" => shared::auth::Role::Admin,
"moderator" => shared::auth::Role::Moderator,
"user" => shared::auth::Role::User,
"guest" => shared::auth::Role::Guest,
_ => shared::auth::Role::Custom(r),
})
.collect(),
is_active: db_user.is_active,
email_verified: db_user.email_verified,
created_at: db_user.created_at,
updated_at: db_user.updated_at,
last_login: db_user.last_login,
profile: shared::auth::UserProfile::default(),
two_factor_enabled: db_user.two_factor_enabled,
}
}
}
impl From<shared::auth::User> for DatabaseUser {
fn from(user: shared::auth::User) -> Self {
Self {
id: user.id,
email: user.email,
username: Some(user.username),
display_name: user.display_name,
password_hash: String::new(), // This should be handled separately
avatar_url: user.avatar_url,
roles: user
.roles
.into_iter()
.map(|r| match r {
shared::auth::Role::Admin => "admin".to_string(),
shared::auth::Role::Moderator => "moderator".to_string(),
shared::auth::Role::User => "user".to_string(),
shared::auth::Role::Guest => "guest".to_string(),
shared::auth::Role::Custom(name) => name,
})
.collect(),
is_active: user.is_active,
is_verified: user.email_verified,
email_verified: user.email_verified,
created_at: user.created_at,
updated_at: user.updated_at,
last_login: user.last_login,
two_factor_enabled: user.two_factor_enabled,
two_factor_secret: None,
backup_codes: Vec::new(),
}
}
}