1678 lines
64 KiB
Rust
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, ¶ms).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, ¶ms).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, ¶ms).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, ¶ms).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, ¶ms).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, ¶ms).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, ¶ms).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, ¶ms).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, ¶ms).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, ¶ms).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, ¶ms).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, ¶ms).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, ¶ms).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, ¶ms).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, ¶ms).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, ¶ms).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, ¶ms).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, ¶ms).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, ¶ms).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, ¶ms).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, ¶ms).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, ¶ms).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, ¶ms).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, ¶ms).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, ¶ms).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, ¶ms).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, ¶ms).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, ¶ms).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, ¶ms).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, ¶ms).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, ¶ms).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, ¶ms).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, ¶ms).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, ¶ms).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, ¶ms).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, ¶ms).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(),
|
|
}
|
|
}
|
|
}
|