diff --git a/migrations/001_initial_setup.sql b/migrations/001_initial_setup.sql
new file mode 100644
index 0000000..2599926
--- /dev/null
+++ b/migrations/001_initial_setup.sql
@@ -0,0 +1,504 @@
+-- Migration 001: Initial Database Setup
+-- This migration creates all necessary tables for authentication and content management
+
+-- Enable UUID extension
+CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
+
+-- Users table - core user information
+CREATE TABLE users (
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
+ email VARCHAR(255) UNIQUE NOT NULL,
+ username VARCHAR(50) UNIQUE NOT NULL,
+ password_hash VARCHAR(255),
+ display_name VARCHAR(100),
+ avatar_url TEXT,
+ is_active BOOLEAN DEFAULT TRUE,
+ email_verified BOOLEAN DEFAULT FALSE,
+ created_at TIMESTAMPTZ DEFAULT NOW(),
+ updated_at TIMESTAMPTZ DEFAULT NOW(),
+ last_login TIMESTAMPTZ,
+
+ -- Profile information
+ first_name VARCHAR(100),
+ last_name VARCHAR(100),
+ bio TEXT,
+ timezone VARCHAR(50),
+ locale VARCHAR(10),
+ preferences JSONB DEFAULT '{}'::jsonb,
+
+ -- Constraints
+ CONSTRAINT users_email_format CHECK (email ~* '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$'),
+ CONSTRAINT users_username_format CHECK (username ~* '^[A-Za-z0-9_-]{3,50}$'),
+ CONSTRAINT users_display_name_length CHECK (char_length(display_name) >= 1)
+);
+
+-- User roles table - RBAC implementation
+CREATE TABLE user_roles (
+ user_id UUID REFERENCES users(id) ON DELETE CASCADE,
+ role VARCHAR(50) NOT NULL,
+ created_at TIMESTAMPTZ DEFAULT NOW(),
+ PRIMARY KEY (user_id, role),
+
+ -- Constraints
+ CONSTRAINT user_roles_valid_role CHECK (role IN ('admin', 'moderator', 'user', 'guest') OR role LIKE 'custom_%')
+);
+
+-- OAuth accounts table - external authentication providers
+CREATE TABLE oauth_accounts (
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
+ user_id UUID REFERENCES users(id) ON DELETE CASCADE,
+ provider VARCHAR(50) NOT NULL,
+ provider_id VARCHAR(255) NOT NULL,
+ provider_email VARCHAR(255) NOT NULL,
+ provider_data JSONB NOT NULL,
+ created_at TIMESTAMPTZ DEFAULT NOW(),
+ updated_at TIMESTAMPTZ DEFAULT NOW(),
+
+ -- Constraints
+ UNIQUE(provider, provider_id),
+ CONSTRAINT oauth_accounts_valid_provider CHECK (provider IN ('google', 'github', 'discord', 'microsoft') OR provider LIKE 'custom_%')
+);
+
+-- Sessions table - session management
+CREATE TABLE sessions (
+ id VARCHAR(255) PRIMARY KEY,
+ user_id UUID REFERENCES users(id) ON DELETE CASCADE,
+ created_at TIMESTAMPTZ DEFAULT NOW(),
+ expires_at TIMESTAMPTZ NOT NULL,
+ last_accessed TIMESTAMPTZ DEFAULT NOW(),
+ ip_address INET,
+ user_agent TEXT,
+ is_active BOOLEAN DEFAULT TRUE,
+
+ -- Constraints
+ CONSTRAINT sessions_valid_expiry CHECK (expires_at > created_at)
+);
+
+-- Tokens table - password reset, email verification, etc.
+CREATE TABLE tokens (
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
+ user_id UUID REFERENCES users(id) ON DELETE CASCADE,
+ token_type VARCHAR(50) NOT NULL,
+ token_hash VARCHAR(255) NOT NULL,
+ expires_at TIMESTAMPTZ NOT NULL,
+ created_at TIMESTAMPTZ DEFAULT NOW(),
+ used_at TIMESTAMPTZ,
+ is_active BOOLEAN DEFAULT TRUE,
+
+ -- Constraints
+ CONSTRAINT tokens_valid_type CHECK (token_type IN ('password_reset', 'email_verification', 'account_activation')),
+ CONSTRAINT tokens_valid_expiry CHECK (expires_at > created_at),
+ CONSTRAINT tokens_used_logic CHECK (used_at IS NULL OR used_at >= created_at)
+);
+
+-- Permissions table - for fine-grained access control
+CREATE TABLE permissions (
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
+ name VARCHAR(100) UNIQUE NOT NULL,
+ description TEXT,
+ resource VARCHAR(100) NOT NULL,
+ action VARCHAR(100) NOT NULL,
+ created_at TIMESTAMPTZ DEFAULT NOW(),
+
+ -- Constraints
+ CONSTRAINT permissions_name_format CHECK (name ~* '^[a-z][a-z0-9_]*[a-z0-9]$'),
+ CONSTRAINT permissions_resource_format CHECK (resource ~* '^[a-z][a-z0-9_]*[a-z0-9]$'),
+ CONSTRAINT permissions_action_format CHECK (action IN ('create', 'read', 'update', 'delete', 'manage', 'execute'))
+);
+
+-- Role permissions table - many-to-many relationship
+CREATE TABLE role_permissions (
+ role VARCHAR(50) NOT NULL,
+ permission_id UUID REFERENCES permissions(id) ON DELETE CASCADE,
+ created_at TIMESTAMPTZ DEFAULT NOW(),
+ PRIMARY KEY (role, permission_id),
+
+ -- Constraints
+ CONSTRAINT role_permissions_valid_role CHECK (role IN ('admin', 'moderator', 'user', 'guest') OR role LIKE 'custom_%')
+);
+
+-- User audit log - track important user actions
+CREATE TABLE user_audit_log (
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
+ user_id UUID REFERENCES users(id) ON DELETE SET NULL,
+ action VARCHAR(100) NOT NULL,
+ resource VARCHAR(100),
+ resource_id UUID,
+ old_values JSONB,
+ new_values JSONB,
+ ip_address INET,
+ user_agent TEXT,
+ created_at TIMESTAMPTZ DEFAULT NOW(),
+
+ -- Constraints
+ CONSTRAINT audit_log_valid_action CHECK (action IN ('login', 'logout', 'register', 'update_profile', 'change_password', 'oauth_login', 'password_reset', 'email_verify'))
+);
+
+-- Page contents table - main content management
+CREATE TABLE page_contents (
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
+ slug VARCHAR(255) NOT NULL UNIQUE,
+ title VARCHAR(500) NOT NULL,
+ name VARCHAR(255) NOT NULL,
+ author VARCHAR(255),
+ author_id UUID,
+ content_type VARCHAR(50) NOT NULL DEFAULT 'page',
+ content_format VARCHAR(20) NOT NULL DEFAULT 'markdown',
+ content TEXT NOT NULL,
+ container VARCHAR(255) NOT NULL DEFAULT 'page-container',
+ state VARCHAR(20) NOT NULL DEFAULT 'draft',
+ require_login BOOLEAN NOT NULL DEFAULT FALSE,
+ date_init TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+ date_end TIMESTAMPTZ,
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+ published_at TIMESTAMPTZ,
+ metadata JSONB DEFAULT '{}',
+ tags TEXT[] DEFAULT '{}',
+ category VARCHAR(255),
+ featured_image VARCHAR(500),
+ excerpt TEXT,
+ seo_title VARCHAR(500),
+ seo_description TEXT,
+ allow_comments BOOLEAN NOT NULL DEFAULT TRUE,
+ view_count BIGINT NOT NULL DEFAULT 0,
+ sort_order INTEGER NOT NULL DEFAULT 0,
+
+ CONSTRAINT fk_author FOREIGN KEY (author_id) REFERENCES users(id) ON DELETE SET NULL
+);
+
+-- Indexes for users table
+CREATE INDEX idx_users_email ON users(email);
+CREATE INDEX idx_users_username ON users(username);
+CREATE INDEX idx_users_is_active ON users(is_active);
+CREATE INDEX idx_users_created_at ON users(created_at);
+CREATE INDEX idx_users_email_verified ON users(email_verified);
+
+-- Indexes for user_roles table
+CREATE INDEX idx_user_roles_user_id ON user_roles(user_id);
+CREATE INDEX idx_user_roles_role ON user_roles(role);
+
+-- Indexes for oauth_accounts table
+CREATE INDEX idx_oauth_accounts_user_id ON oauth_accounts(user_id);
+CREATE INDEX idx_oauth_accounts_provider ON oauth_accounts(provider, provider_id);
+CREATE INDEX idx_oauth_accounts_provider_email ON oauth_accounts(provider_email);
+
+-- Indexes for sessions table
+CREATE INDEX idx_sessions_user_id ON sessions(user_id);
+CREATE INDEX idx_sessions_expires_at ON sessions(expires_at);
+CREATE INDEX idx_sessions_is_active ON sessions(is_active);
+CREATE INDEX idx_sessions_last_accessed ON sessions(last_accessed);
+
+-- Indexes for tokens table
+CREATE INDEX idx_tokens_user_id ON tokens(user_id);
+CREATE INDEX idx_tokens_type ON tokens(token_type);
+CREATE INDEX idx_tokens_expires_at ON tokens(expires_at);
+CREATE INDEX idx_tokens_is_active ON tokens(is_active);
+CREATE INDEX idx_tokens_hash ON tokens(token_hash);
+
+-- Indexes for permissions table
+CREATE INDEX idx_permissions_name ON permissions(name);
+CREATE INDEX idx_permissions_resource ON permissions(resource);
+CREATE INDEX idx_permissions_action ON permissions(action);
+
+-- Indexes for role_permissions table
+CREATE INDEX idx_role_permissions_role ON role_permissions(role);
+CREATE INDEX idx_role_permissions_permission ON role_permissions(permission_id);
+
+-- Indexes for user_audit_log table
+CREATE INDEX idx_user_audit_log_user_id ON user_audit_log(user_id);
+CREATE INDEX idx_user_audit_log_action ON user_audit_log(action);
+CREATE INDEX idx_user_audit_log_created_at ON user_audit_log(created_at);
+
+-- Indexes for page_contents table
+CREATE INDEX idx_page_contents_slug ON page_contents(slug);
+CREATE INDEX idx_page_contents_state ON page_contents(state);
+CREATE INDEX idx_page_contents_content_type ON page_contents(content_type);
+CREATE INDEX idx_page_contents_author_id ON page_contents(author_id);
+CREATE INDEX idx_page_contents_category ON page_contents(category);
+CREATE INDEX idx_page_contents_tags ON page_contents USING GIN(tags);
+CREATE INDEX idx_page_contents_published_at ON page_contents(published_at);
+CREATE INDEX idx_page_contents_created_at ON page_contents(created_at);
+CREATE INDEX idx_page_contents_view_count ON page_contents(view_count);
+CREATE INDEX idx_page_contents_sort_order ON page_contents(sort_order);
+CREATE INDEX idx_page_contents_metadata ON page_contents USING GIN(metadata);
+
+-- Full-text search index for page_contents
+CREATE INDEX idx_page_contents_search ON page_contents USING GIN(
+ to_tsvector('english', title || ' ' || COALESCE(content, '') || ' ' || COALESCE(excerpt, ''))
+);
+
+-- Partial indexes for published content
+CREATE INDEX idx_page_contents_published_public ON page_contents(created_at DESC)
+WHERE state = 'published' AND require_login = FALSE;
+
+CREATE INDEX idx_page_contents_published_by_type ON page_contents(content_type, created_at DESC)
+WHERE state = 'published';
+
+-- Function to update updated_at timestamp
+CREATE OR REPLACE FUNCTION update_updated_at_column()
+RETURNS TRIGGER AS $$
+BEGIN
+ NEW.updated_at = NOW();
+ RETURN NEW;
+END;
+$$ language 'plpgsql';
+
+-- Triggers to automatically update updated_at
+CREATE TRIGGER update_users_updated_at
+ BEFORE UPDATE ON users
+ FOR EACH ROW
+ EXECUTE FUNCTION update_updated_at_column();
+
+CREATE TRIGGER update_oauth_accounts_updated_at
+ BEFORE UPDATE ON oauth_accounts
+ FOR EACH ROW
+ EXECUTE FUNCTION update_updated_at_column();
+
+CREATE TRIGGER update_page_contents_updated_at
+ BEFORE UPDATE ON page_contents
+ FOR EACH ROW
+ EXECUTE FUNCTION update_updated_at_column();
+
+-- Function to automatically assign default role to new users
+CREATE OR REPLACE FUNCTION assign_default_role()
+RETURNS TRIGGER AS $$
+BEGIN
+ INSERT INTO user_roles (user_id, role) VALUES (NEW.id, 'user');
+ RETURN NEW;
+END;
+$$ language 'plpgsql';
+
+-- Trigger to assign default role
+CREATE TRIGGER assign_default_role_trigger
+ AFTER INSERT ON users
+ FOR EACH ROW
+ EXECUTE FUNCTION assign_default_role();
+
+-- Function to log user actions
+CREATE OR REPLACE FUNCTION log_user_action(
+ p_user_id UUID,
+ p_action VARCHAR(100),
+ p_resource VARCHAR(100) DEFAULT NULL,
+ p_resource_id UUID DEFAULT NULL,
+ p_old_values JSONB DEFAULT NULL,
+ p_new_values JSONB DEFAULT NULL,
+ p_ip_address INET DEFAULT NULL,
+ p_user_agent TEXT DEFAULT NULL
+)
+RETURNS UUID AS $$
+DECLARE
+ log_id UUID;
+BEGIN
+ INSERT INTO user_audit_log (
+ user_id, action, resource, resource_id,
+ old_values, new_values, ip_address, user_agent
+ ) VALUES (
+ p_user_id, p_action, p_resource, p_resource_id,
+ p_old_values, p_new_values, p_ip_address, p_user_agent
+ ) RETURNING id INTO log_id;
+
+ RETURN log_id;
+END;
+$$ LANGUAGE plpgsql;
+
+-- Function to clean up expired sessions and tokens
+CREATE OR REPLACE FUNCTION cleanup_expired_auth_data()
+RETURNS INTEGER AS $$
+DECLARE
+ deleted_count INTEGER := 0;
+ temp_count INTEGER;
+BEGIN
+ -- Delete expired sessions
+ DELETE FROM sessions WHERE expires_at < NOW();
+ GET DIAGNOSTICS temp_count = ROW_COUNT;
+ deleted_count := deleted_count + temp_count;
+
+ -- Delete expired tokens
+ DELETE FROM tokens WHERE expires_at < NOW();
+ GET DIAGNOSTICS temp_count = ROW_COUNT;
+ deleted_count := deleted_count + temp_count;
+
+ -- Delete old audit logs (older than 1 year)
+ DELETE FROM user_audit_log WHERE created_at < NOW() - INTERVAL '1 year';
+ GET DIAGNOSTICS temp_count = ROW_COUNT;
+ deleted_count := deleted_count + temp_count;
+
+ RETURN deleted_count;
+END;
+$$ LANGUAGE plpgsql;
+
+-- Default permissions
+INSERT INTO permissions (name, description, resource, action) VALUES
+('read_users', 'Read user information', 'users', 'read'),
+('write_users', 'Create and update users', 'users', 'create'),
+('delete_users', 'Delete users', 'users', 'delete'),
+('manage_users', 'Full user management', 'users', 'manage'),
+('read_content', 'Read content', 'content', 'read'),
+('write_content', 'Create and update content', 'content', 'create'),
+('delete_content', 'Delete content', 'content', 'delete'),
+('manage_content', 'Full content management', 'content', 'manage'),
+('manage_roles', 'Manage user roles and permissions', 'roles', 'manage'),
+('manage_system', 'System administration', 'system', 'manage'),
+('read_audit_log', 'Read audit logs', 'audit_log', 'read'),
+('execute_maintenance', 'Execute maintenance tasks', 'system', 'execute');
+
+-- Default role permissions
+INSERT INTO role_permissions (role, permission_id)
+SELECT 'admin', id FROM permissions;
+
+INSERT INTO role_permissions (role, permission_id)
+SELECT 'moderator', id FROM permissions
+WHERE name IN ('read_users', 'read_content', 'write_content', 'delete_content', 'read_audit_log');
+
+INSERT INTO role_permissions (role, permission_id)
+SELECT 'user', id FROM permissions
+WHERE name IN ('read_content', 'write_content');
+
+INSERT INTO role_permissions (role, permission_id)
+SELECT 'guest', id FROM permissions
+WHERE name IN ('read_content');
+
+-- Create a default admin user (password: 'admin123' - change this!)
+-- Password hash for 'admin123' with Argon2
+INSERT INTO users (email, username, password_hash, display_name, email_verified, is_active)
+VALUES (
+ 'admin@example.com',
+ 'admin',
+ '$argon2id$v=19$m=19456,t=2,p=1$4K5FCBeajDVi8smeWgce3w$y9zZkuvLE3H3GwTFgfl/ngjqlnjiuDRIPiBqu0yFICA',
+ 'System Administrator',
+ TRUE,
+ TRUE
+);
+
+-- Assign admin role to the default admin user
+INSERT INTO user_roles (user_id, role)
+SELECT id, 'admin' FROM users WHERE username = 'admin';
+
+-- Sample page content data
+INSERT INTO page_contents (
+ slug, title, name, content_type, content, container, state,
+ require_login, tags, category, excerpt, allow_comments
+) VALUES
+(
+ 'welcome',
+ 'Welcome to Our Site',
+ 'welcome-page',
+ 'page',
+ '# Welcome to Our Site
+
+Welcome to our amazing website! This is a sample page to demonstrate our content management system.
+
+## Features
+
+- Dynamic content loading
+- Markdown support
+- SEO optimization
+- Multi-format content support
+
+Feel free to explore and discover what we have to offer.',
+ 'page-container',
+ 'published',
+ false,
+ ARRAY['welcome', 'introduction'],
+ 'general',
+ 'Welcome to our amazing website! This is a sample page to demonstrate our content management system.',
+ false
+),
+(
+ 'about',
+ 'About Us',
+ 'about-page',
+ 'page',
+ '# About Us
+
+We are a team of passionate developers creating amazing web experiences.
+
+## Our Mission
+
+To build innovative solutions that make the web a better place.
+
+## Our Values
+
+- Quality
+- Innovation
+- User Experience
+- Open Source',
+ 'page-container',
+ 'published',
+ false,
+ ARRAY['about', 'company'],
+ 'general',
+ 'We are a team of passionate developers creating amazing web experiences.',
+ false
+),
+(
+ 'sample-blog-post',
+ 'Getting Started with Rust and Web Development',
+ 'sample-blog',
+ 'blog',
+ '# Getting Started with Rust and Web Development
+
+Rust is becoming increasingly popular for web development, and for good reason!
+
+## Why Rust for Web Development?
+
+1. **Performance**: Rust offers near C++ performance
+2. **Safety**: Memory safety without garbage collection
+3. **Concurrency**: Excellent support for concurrent programming
+4. **Ecosystem**: Growing ecosystem of web frameworks
+
+## Popular Rust Web Frameworks
+
+- **Axum**: A modern, async web framework
+- **Warp**: A super-easy, composable, web server framework
+- **Actix-web**: A powerful, pragmatic, and extremely fast web framework
+
+## Getting Started
+
+```rust
+use axum::{response::Html, routing::get, Router};
+
+#[tokio::main]
+async fn main() {
+ let app = Router::new().route("/", get(|| async { Html("
Hello, World!
") }));
+
+ axum::Server::bind(&"0.0.0.0:3000".parse().unwrap())
+ .serve(app.into_make_service())
+ .await
+ .unwrap();
+}
+```
+
+Happy coding!',
+ 'blog-container',
+ 'published',
+ false,
+ ARRAY['rust', 'web-development', 'programming', 'tutorial'],
+ 'technology',
+ 'Learn how to get started with Rust for web development. Discover popular frameworks and see practical examples.',
+ true
+);
+
+-- Comments on tables
+COMMENT ON TABLE users IS 'Core user accounts and profile information';
+COMMENT ON TABLE user_roles IS 'User role assignments for RBAC';
+COMMENT ON TABLE oauth_accounts IS 'External OAuth provider account links';
+COMMENT ON TABLE sessions IS 'User session management';
+COMMENT ON TABLE tokens IS 'Security tokens for password reset, email verification, etc.';
+COMMENT ON TABLE permissions IS 'System permissions for fine-grained access control';
+COMMENT ON TABLE role_permissions IS 'Role to permission mappings';
+COMMENT ON TABLE user_audit_log IS 'Audit trail for user actions';
+COMMENT ON TABLE page_contents IS 'Main content management table for pages, posts, and other content';
+
+-- Comments on important columns
+COMMENT ON COLUMN users.password_hash IS 'Argon2 hashed password, NULL for OAuth-only accounts';
+COMMENT ON COLUMN users.preferences IS 'JSON object for user preferences and settings';
+COMMENT ON COLUMN oauth_accounts.provider_data IS 'Raw user data from OAuth provider';
+COMMENT ON COLUMN sessions.id IS 'Session identifier, should be cryptographically secure';
+COMMENT ON COLUMN tokens.token_hash IS 'Hashed token value for security';
+COMMENT ON COLUMN user_audit_log.old_values IS 'Previous values before change (for updates)';
+COMMENT ON COLUMN user_audit_log.new_values IS 'New values after change (for updates)';
+COMMENT ON COLUMN page_contents.metadata IS 'JSON object for additional content metadata';
+COMMENT ON COLUMN page_contents.tags IS 'Array of tags for content categorization';
+COMMENT ON COLUMN page_contents.state IS 'Content state: draft, published, archived';
diff --git a/migrations/001_initial_setup_postgres.sql b/migrations/001_initial_setup_postgres.sql
new file mode 100644
index 0000000..1e058c9
--- /dev/null
+++ b/migrations/001_initial_setup_postgres.sql
@@ -0,0 +1,100 @@
+-- Initial database setup for PostgreSQL
+-- Migration: 001_initial_setup
+-- Database: PostgreSQL
+
+-- Enable UUID extension
+CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
+
+-- Users table for authentication
+CREATE TABLE IF NOT EXISTS users (
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
+ username VARCHAR(255) NOT NULL UNIQUE,
+ email VARCHAR(255) NOT NULL UNIQUE,
+ password_hash VARCHAR(255) NOT NULL,
+ is_active BOOLEAN NOT NULL DEFAULT true,
+ is_verified BOOLEAN NOT NULL DEFAULT false,
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
+);
+
+-- User sessions table
+CREATE TABLE IF NOT EXISTS user_sessions (
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
+ user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
+ session_token VARCHAR(255) NOT NULL UNIQUE,
+ expires_at TIMESTAMPTZ NOT NULL,
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
+);
+
+-- Content table for CMS functionality
+CREATE TABLE IF NOT EXISTS content (
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
+ title VARCHAR(255) NOT NULL,
+ slug VARCHAR(255) NOT NULL UNIQUE,
+ content_type VARCHAR(50) NOT NULL DEFAULT 'markdown',
+ body TEXT,
+ metadata JSONB,
+ is_published BOOLEAN NOT NULL DEFAULT false,
+ published_at TIMESTAMPTZ,
+ created_by UUID REFERENCES users(id),
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
+);
+
+-- User roles table
+CREATE TABLE IF NOT EXISTS user_roles (
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
+ name VARCHAR(100) NOT NULL UNIQUE,
+ description TEXT,
+ permissions JSONB,
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
+);
+
+-- User role assignments
+CREATE TABLE IF NOT EXISTS user_role_assignments (
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
+ user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
+ role_id UUID NOT NULL REFERENCES user_roles(id) ON DELETE CASCADE,
+ assigned_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+ assigned_by UUID REFERENCES users(id),
+ UNIQUE(user_id, role_id)
+);
+
+-- Create indexes for better performance
+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_users_active ON users(is_active);
+CREATE INDEX IF NOT EXISTS idx_user_sessions_token ON user_sessions(session_token);
+CREATE INDEX IF NOT EXISTS idx_user_sessions_user_id ON user_sessions(user_id);
+CREATE INDEX IF NOT EXISTS idx_user_sessions_expires ON user_sessions(expires_at);
+CREATE INDEX IF NOT EXISTS idx_content_slug ON content(slug);
+CREATE INDEX IF NOT EXISTS idx_content_published ON content(is_published);
+CREATE INDEX IF NOT EXISTS idx_content_created_by ON content(created_by);
+CREATE INDEX IF NOT EXISTS idx_content_type ON content(content_type);
+CREATE INDEX IF NOT EXISTS idx_user_role_assignments_user ON user_role_assignments(user_id);
+CREATE INDEX IF NOT EXISTS idx_user_role_assignments_role ON user_role_assignments(role_id);
+
+-- Update timestamp trigger function
+CREATE OR REPLACE FUNCTION update_updated_at_column()
+RETURNS TRIGGER AS $$
+BEGIN
+ NEW.updated_at = NOW();
+ RETURN NEW;
+END;
+$$ language 'plpgsql';
+
+-- Apply triggers to tables with updated_at columns
+CREATE TRIGGER update_users_updated_at
+ BEFORE UPDATE ON users
+ FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
+
+CREATE TRIGGER update_content_updated_at
+ BEFORE UPDATE ON content
+ FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
+
+-- Insert default roles
+INSERT INTO user_roles (name, description, permissions) VALUES
+('admin', 'Administrator with full access', '{"all": true}'),
+('editor', 'Content editor', '{"content": {"read": true, "write": true, "delete": true}}'),
+('user', 'Regular user', '{"content": {"read": true}}')
+ON CONFLICT (name) DO NOTHING;
diff --git a/migrations/001_initial_setup_sqlite.sql b/migrations/001_initial_setup_sqlite.sql
new file mode 100644
index 0000000..001ebc6
--- /dev/null
+++ b/migrations/001_initial_setup_sqlite.sql
@@ -0,0 +1,96 @@
+-- Initial database setup for SQLite
+-- Migration: 001_initial_setup
+-- Database: SQLite
+
+-- Users table for authentication
+CREATE TABLE IF NOT EXISTS users (
+ id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
+ username TEXT NOT NULL UNIQUE,
+ email TEXT NOT NULL UNIQUE,
+ password_hash TEXT NOT NULL,
+ is_active INTEGER NOT NULL DEFAULT 1,
+ is_verified INTEGER NOT NULL DEFAULT 0,
+ created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
+);
+
+-- User sessions table
+CREATE TABLE IF NOT EXISTS user_sessions (
+ id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
+ user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
+ session_token TEXT NOT NULL UNIQUE,
+ expires_at DATETIME NOT NULL,
+ created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
+);
+
+-- Content table for CMS functionality
+CREATE TABLE IF NOT EXISTS content (
+ id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
+ title TEXT NOT NULL,
+ slug TEXT NOT NULL UNIQUE,
+ content_type TEXT NOT NULL DEFAULT 'markdown',
+ body TEXT,
+ metadata TEXT, -- JSON as TEXT in SQLite
+ is_published INTEGER NOT NULL DEFAULT 0,
+ published_at DATETIME,
+ created_by TEXT REFERENCES users(id),
+ created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
+);
+
+-- User roles table
+CREATE TABLE IF NOT EXISTS user_roles (
+ id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
+ name TEXT NOT NULL UNIQUE,
+ description TEXT,
+ permissions TEXT, -- JSON as TEXT in SQLite
+ created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
+);
+
+-- User role assignments
+CREATE TABLE IF NOT EXISTS user_role_assignments (
+ id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
+ user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
+ role_id TEXT NOT NULL REFERENCES user_roles(id) ON DELETE CASCADE,
+ assigned_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ assigned_by TEXT REFERENCES users(id),
+ UNIQUE(user_id, role_id)
+);
+
+-- Create indexes for better performance
+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_users_active ON users(is_active);
+CREATE INDEX IF NOT EXISTS idx_user_sessions_token ON user_sessions(session_token);
+CREATE INDEX IF NOT EXISTS idx_user_sessions_user_id ON user_sessions(user_id);
+CREATE INDEX IF NOT EXISTS idx_user_sessions_expires ON user_sessions(expires_at);
+CREATE INDEX IF NOT EXISTS idx_content_slug ON content(slug);
+CREATE INDEX IF NOT EXISTS idx_content_published ON content(is_published);
+CREATE INDEX IF NOT EXISTS idx_content_created_by ON content(created_by);
+CREATE INDEX IF NOT EXISTS idx_content_type ON content(content_type);
+CREATE INDEX IF NOT EXISTS idx_user_role_assignments_user ON user_role_assignments(user_id);
+CREATE INDEX IF NOT EXISTS idx_user_role_assignments_role ON user_role_assignments(role_id);
+
+-- Triggers for updated_at timestamps (SQLite doesn't have automatic timestamp updates)
+CREATE TRIGGER IF NOT EXISTS update_users_updated_at
+ AFTER UPDATE ON users
+ FOR EACH ROW
+ WHEN NEW.updated_at = OLD.updated_at
+ BEGIN
+ UPDATE users SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id;
+ END;
+
+CREATE TRIGGER IF NOT EXISTS update_content_updated_at
+ AFTER UPDATE ON content
+ FOR EACH ROW
+ WHEN NEW.updated_at = OLD.updated_at
+ BEGIN
+ UPDATE content SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id;
+ END;
+
+-- Insert default roles
+INSERT INTO user_roles (name, description, permissions) VALUES
+('admin', 'Administrator with full access', '{"all": true}'),
+('editor', 'Content editor', '{"content": {"read": true, "write": true, "delete": true}}'),
+('user', 'Regular user', '{"content": {"read": true}}')
+ON CONFLICT (name) DO NOTHING;
diff --git a/migrations/002_add_2fa_support.sql b/migrations/002_add_2fa_support.sql
new file mode 100644
index 0000000..9a22c01
--- /dev/null
+++ b/migrations/002_add_2fa_support.sql
@@ -0,0 +1,101 @@
+-- Migration 002: Add Two-Factor Authentication Support
+-- This migration adds TOTP (Time-based One-Time Password) support for 2FA
+
+-- User 2FA settings table
+CREATE TABLE user_2fa (
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
+ user_id UUID REFERENCES users(id) ON DELETE CASCADE UNIQUE,
+ secret VARCHAR(32) NOT NULL, -- Base32 encoded TOTP secret
+ is_enabled BOOLEAN DEFAULT FALSE,
+ backup_codes TEXT[], -- Array of backup codes
+ created_at TIMESTAMPTZ DEFAULT NOW(),
+ updated_at TIMESTAMPTZ DEFAULT NOW(),
+ last_used TIMESTAMPTZ,
+
+ -- Constraints
+ CONSTRAINT user_2fa_secret_length CHECK (char_length(secret) = 32),
+ CONSTRAINT user_2fa_backup_codes_count CHECK (array_length(backup_codes, 1) <= 10)
+);
+
+-- 2FA recovery codes table (for better tracking)
+CREATE TABLE user_2fa_recovery_codes (
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
+ user_id UUID REFERENCES users(id) ON DELETE CASCADE,
+ code_hash VARCHAR(255) NOT NULL, -- Hashed recovery code
+ used_at TIMESTAMPTZ,
+ created_at TIMESTAMPTZ DEFAULT NOW(),
+
+ -- Constraints
+ CONSTRAINT user_2fa_recovery_codes_unique UNIQUE(user_id, code_hash)
+);
+
+-- 2FA authentication attempts table (for rate limiting and security)
+CREATE TABLE user_2fa_attempts (
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
+ user_id UUID REFERENCES users(id) ON DELETE CASCADE,
+ ip_address INET,
+ user_agent TEXT,
+ success BOOLEAN NOT NULL,
+ code_type VARCHAR(20) NOT NULL, -- 'totp' or 'backup'
+ created_at TIMESTAMPTZ DEFAULT NOW(),
+
+ -- Constraints
+ CONSTRAINT user_2fa_attempts_valid_code_type CHECK (code_type IN ('totp', 'backup'))
+);
+
+-- Add 2FA required flag to users table
+ALTER TABLE users ADD COLUMN two_factor_required BOOLEAN DEFAULT FALSE;
+
+-- Add 2FA verified flag to sessions table
+ALTER TABLE sessions ADD COLUMN two_factor_verified BOOLEAN DEFAULT FALSE;
+
+-- Update tokens table to support 2FA setup tokens
+ALTER TABLE tokens DROP CONSTRAINT tokens_valid_type;
+ALTER TABLE tokens ADD CONSTRAINT tokens_valid_type CHECK (
+ token_type IN ('password_reset', 'email_verification', 'account_activation', '2fa_setup')
+);
+
+-- Indexes for performance
+CREATE INDEX idx_user_2fa_user_id ON user_2fa(user_id);
+CREATE INDEX idx_user_2fa_recovery_codes_user_id ON user_2fa_recovery_codes(user_id);
+CREATE INDEX idx_user_2fa_attempts_user_id ON user_2fa_attempts(user_id);
+CREATE INDEX idx_user_2fa_attempts_created_at ON user_2fa_attempts(created_at);
+CREATE INDEX idx_sessions_2fa_verified ON sessions(two_factor_verified);
+
+-- Function to cleanup old 2FA attempts (for maintenance)
+CREATE OR REPLACE FUNCTION cleanup_old_2fa_attempts()
+RETURNS void AS $$
+BEGIN
+ DELETE FROM user_2fa_attempts
+ WHERE created_at < NOW() - INTERVAL '7 days';
+END;
+$$ LANGUAGE plpgsql;
+
+-- Trigger to update updated_at on user_2fa table
+CREATE OR REPLACE FUNCTION update_user_2fa_updated_at()
+RETURNS TRIGGER AS $$
+BEGIN
+ NEW.updated_at = NOW();
+ RETURN NEW;
+END;
+$$ LANGUAGE plpgsql;
+
+CREATE TRIGGER trigger_user_2fa_updated_at
+ BEFORE UPDATE ON user_2fa
+ FOR EACH ROW
+ EXECUTE FUNCTION update_user_2fa_updated_at();
+
+-- Comments for documentation
+COMMENT ON TABLE user_2fa IS 'Stores TOTP secrets and 2FA configuration for users';
+COMMENT ON COLUMN user_2fa.secret IS 'Base32 encoded TOTP secret key';
+COMMENT ON COLUMN user_2fa.backup_codes IS 'Array of hashed backup codes for account recovery';
+COMMENT ON COLUMN user_2fa.is_enabled IS 'Whether 2FA is currently enabled for the user';
+
+COMMENT ON TABLE user_2fa_recovery_codes IS 'Individual recovery codes for better tracking and management';
+COMMENT ON COLUMN user_2fa_recovery_codes.code_hash IS 'SHA256 hash of the recovery code';
+
+COMMENT ON TABLE user_2fa_attempts IS 'Tracks 2FA authentication attempts for security monitoring';
+COMMENT ON COLUMN user_2fa_attempts.code_type IS 'Type of 2FA code used: totp or backup';
+
+COMMENT ON COLUMN users.two_factor_required IS 'Whether 2FA is required for this user account';
+COMMENT ON COLUMN sessions.two_factor_verified IS 'Whether this session has completed 2FA verification';
diff --git a/migrations/002_add_2fa_support_postgres.sql b/migrations/002_add_2fa_support_postgres.sql
new file mode 100644
index 0000000..0dd0745
--- /dev/null
+++ b/migrations/002_add_2fa_support_postgres.sql
@@ -0,0 +1,131 @@
+-- Add 2FA support to users table
+-- Migration: 002_add_2fa_support
+-- Database: PostgreSQL
+
+-- Add 2FA columns to users table
+ALTER TABLE users
+ADD COLUMN IF NOT EXISTS two_factor_secret VARCHAR(255),
+ADD COLUMN IF NOT EXISTS two_factor_enabled BOOLEAN NOT NULL DEFAULT false,
+ADD COLUMN IF NOT EXISTS backup_codes TEXT[], -- Array of backup codes
+ADD COLUMN IF NOT EXISTS last_login_at TIMESTAMPTZ,
+ADD COLUMN IF NOT EXISTS failed_login_attempts INTEGER NOT NULL DEFAULT 0,
+ADD COLUMN IF NOT EXISTS locked_until TIMESTAMPTZ;
+
+-- Create 2FA recovery codes table
+CREATE TABLE IF NOT EXISTS two_factor_recovery_codes (
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
+ user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
+ code_hash VARCHAR(255) NOT NULL,
+ used_at TIMESTAMPTZ,
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
+);
+
+-- Create login attempts table for security tracking
+CREATE TABLE IF NOT EXISTS login_attempts (
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
+ user_id UUID REFERENCES users(id) ON DELETE CASCADE,
+ ip_address INET,
+ user_agent TEXT,
+ success BOOLEAN NOT NULL,
+ failure_reason VARCHAR(255),
+ attempted_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
+);
+
+-- Create password reset tokens table
+CREATE TABLE IF NOT EXISTS password_reset_tokens (
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
+ user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
+ token_hash VARCHAR(255) NOT NULL UNIQUE,
+ expires_at TIMESTAMPTZ NOT NULL,
+ used_at TIMESTAMPTZ,
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
+);
+
+-- Create email verification tokens table
+CREATE TABLE IF NOT EXISTS email_verification_tokens (
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
+ user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
+ token_hash VARCHAR(255) NOT NULL UNIQUE,
+ email VARCHAR(255) NOT NULL, -- Allow email change verification
+ expires_at TIMESTAMPTZ NOT NULL,
+ verified_at TIMESTAMPTZ,
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
+);
+
+-- Create indexes for 2FA and security tables
+CREATE INDEX IF NOT EXISTS idx_users_2fa_enabled ON users(two_factor_enabled);
+CREATE INDEX IF NOT EXISTS idx_users_locked_until ON users(locked_until);
+CREATE INDEX IF NOT EXISTS idx_users_last_login ON users(last_login_at);
+CREATE INDEX IF NOT EXISTS idx_2fa_recovery_codes_user_id ON two_factor_recovery_codes(user_id);
+CREATE INDEX IF NOT EXISTS idx_2fa_recovery_codes_hash ON two_factor_recovery_codes(code_hash);
+CREATE INDEX IF NOT EXISTS idx_login_attempts_user_id ON login_attempts(user_id);
+CREATE INDEX IF NOT EXISTS idx_login_attempts_ip ON login_attempts(ip_address);
+CREATE INDEX IF NOT EXISTS idx_login_attempts_time ON login_attempts(attempted_at);
+CREATE INDEX IF NOT EXISTS idx_password_reset_tokens_hash ON password_reset_tokens(token_hash);
+CREATE INDEX IF NOT EXISTS idx_password_reset_tokens_user ON password_reset_tokens(user_id);
+CREATE INDEX IF NOT EXISTS idx_email_verification_tokens_hash ON email_verification_tokens(token_hash);
+CREATE INDEX IF NOT EXISTS idx_email_verification_tokens_user ON email_verification_tokens(user_id);
+
+-- Add constraint to ensure backup codes are valid JSON array
+ALTER TABLE users ADD CONSTRAINT backup_codes_valid_json
+ CHECK (backup_codes IS NULL OR jsonb_typeof(backup_codes::jsonb) = 'array');
+
+-- Function to clean up expired tokens
+CREATE OR REPLACE FUNCTION cleanup_expired_tokens()
+RETURNS INTEGER AS $$
+DECLARE
+ deleted_count INTEGER := 0;
+BEGIN
+ -- Clean up expired password reset tokens
+ DELETE FROM password_reset_tokens
+ WHERE expires_at < NOW() AND used_at IS NULL;
+ GET DIAGNOSTICS deleted_count = ROW_COUNT;
+
+ -- Clean up expired email verification tokens
+ DELETE FROM email_verification_tokens
+ WHERE expires_at < NOW() AND verified_at IS NULL;
+ GET DIAGNOSTICS deleted_count = deleted_count + ROW_COUNT;
+
+ -- Clean up old login attempts (keep last 30 days)
+ DELETE FROM login_attempts
+ WHERE attempted_at < NOW() - INTERVAL '30 days';
+ GET DIAGNOSTICS deleted_count = deleted_count + ROW_COUNT;
+
+ -- Clean up expired user sessions
+ DELETE FROM user_sessions
+ WHERE expires_at < NOW();
+ GET DIAGNOSTICS deleted_count = deleted_count + ROW_COUNT;
+
+ RETURN deleted_count;
+END;
+$$ LANGUAGE plpgsql;
+
+-- Function to unlock user accounts after lock period
+CREATE OR REPLACE FUNCTION unlock_expired_accounts()
+RETURNS INTEGER AS $$
+DECLARE
+ unlocked_count INTEGER := 0;
+BEGIN
+ UPDATE users
+ SET locked_until = NULL,
+ failed_login_attempts = 0
+ WHERE locked_until IS NOT NULL
+ AND locked_until < NOW();
+
+ GET DIAGNOSTICS unlocked_count = ROW_COUNT;
+ RETURN unlocked_count;
+END;
+$$ LANGUAGE plpgsql;
+
+-- Add comments for documentation
+COMMENT ON COLUMN users.two_factor_secret IS 'Base32-encoded TOTP secret for 2FA';
+COMMENT ON COLUMN users.two_factor_enabled IS 'Whether 2FA is enabled for this user';
+COMMENT ON COLUMN users.backup_codes IS 'JSON array of hashed backup codes for 2FA recovery';
+COMMENT ON COLUMN users.last_login_at IS 'Timestamp of last successful login';
+COMMENT ON COLUMN users.failed_login_attempts IS 'Number of consecutive failed login attempts';
+COMMENT ON COLUMN users.locked_until IS 'Account locked until this timestamp due to failed attempts';
+
+COMMENT ON TABLE two_factor_recovery_codes IS 'Individual 2FA recovery codes for account recovery';
+COMMENT ON TABLE login_attempts IS 'Log of all login attempts for security monitoring';
+COMMENT ON TABLE password_reset_tokens IS 'Tokens for password reset functionality';
+COMMENT ON TABLE email_verification_tokens IS 'Tokens for email verification and changes';
diff --git a/migrations/002_add_2fa_support_sqlite.sql b/migrations/002_add_2fa_support_sqlite.sql
new file mode 100644
index 0000000..355dccf
--- /dev/null
+++ b/migrations/002_add_2fa_support_sqlite.sql
@@ -0,0 +1,117 @@
+-- Add 2FA support to users table
+-- Migration: 002_add_2fa_support
+-- Database: SQLite
+
+-- Add 2FA columns to users table (SQLite requires one column at a time)
+ALTER TABLE users ADD COLUMN two_factor_secret TEXT;
+ALTER TABLE users ADD COLUMN two_factor_enabled INTEGER NOT NULL DEFAULT 0;
+ALTER TABLE users ADD COLUMN backup_codes TEXT; -- JSON array as TEXT
+ALTER TABLE users ADD COLUMN last_login_at DATETIME;
+ALTER TABLE users ADD COLUMN failed_login_attempts INTEGER NOT NULL DEFAULT 0;
+ALTER TABLE users ADD COLUMN locked_until DATETIME;
+
+-- Create 2FA recovery codes table
+CREATE TABLE IF NOT EXISTS two_factor_recovery_codes (
+ id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
+ user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
+ code_hash TEXT NOT NULL,
+ used_at DATETIME,
+ created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
+);
+
+-- Create login attempts table for security tracking
+CREATE TABLE IF NOT EXISTS login_attempts (
+ id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
+ user_id TEXT REFERENCES users(id) ON DELETE CASCADE,
+ ip_address TEXT,
+ user_agent TEXT,
+ success INTEGER NOT NULL,
+ failure_reason TEXT,
+ attempted_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
+);
+
+-- Create password reset tokens table
+CREATE TABLE IF NOT EXISTS password_reset_tokens (
+ id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
+ user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
+ token_hash TEXT NOT NULL UNIQUE,
+ expires_at DATETIME NOT NULL,
+ used_at DATETIME,
+ created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
+);
+
+-- Create email verification tokens table
+CREATE TABLE IF NOT EXISTS email_verification_tokens (
+ id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
+ user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
+ token_hash TEXT NOT NULL UNIQUE,
+ email TEXT NOT NULL, -- Allow email change verification
+ expires_at DATETIME NOT NULL,
+ verified_at DATETIME,
+ created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
+);
+
+-- Create indexes for 2FA and security tables
+CREATE INDEX IF NOT EXISTS idx_users_2fa_enabled ON users(two_factor_enabled);
+CREATE INDEX IF NOT EXISTS idx_users_locked_until ON users(locked_until);
+CREATE INDEX IF NOT EXISTS idx_users_last_login ON users(last_login_at);
+CREATE INDEX IF NOT EXISTS idx_2fa_recovery_codes_user_id ON two_factor_recovery_codes(user_id);
+CREATE INDEX IF NOT EXISTS idx_2fa_recovery_codes_hash ON two_factor_recovery_codes(code_hash);
+CREATE INDEX IF NOT EXISTS idx_login_attempts_user_id ON login_attempts(user_id);
+CREATE INDEX IF NOT EXISTS idx_login_attempts_ip ON login_attempts(ip_address);
+CREATE INDEX IF NOT EXISTS idx_login_attempts_time ON login_attempts(attempted_at);
+CREATE INDEX IF NOT EXISTS idx_password_reset_tokens_hash ON password_reset_tokens(token_hash);
+CREATE INDEX IF NOT EXISTS idx_password_reset_tokens_user ON password_reset_tokens(user_id);
+CREATE INDEX IF NOT EXISTS idx_email_verification_tokens_hash ON email_verification_tokens(token_hash);
+CREATE INDEX IF NOT EXISTS idx_email_verification_tokens_user ON email_verification_tokens(user_id);
+
+-- SQLite doesn't support stored procedures, but we can create triggers for cleanup
+-- Trigger to automatically clean up expired password reset tokens on insert
+CREATE TRIGGER IF NOT EXISTS cleanup_expired_password_tokens
+ AFTER INSERT ON password_reset_tokens
+ BEGIN
+ DELETE FROM password_reset_tokens
+ WHERE expires_at < datetime('now') AND used_at IS NULL;
+ END;
+
+-- Trigger to automatically clean up expired email verification tokens on insert
+CREATE TRIGGER IF NOT EXISTS cleanup_expired_email_tokens
+ AFTER INSERT ON email_verification_tokens
+ BEGIN
+ DELETE FROM email_verification_tokens
+ WHERE expires_at < datetime('now') AND verified_at IS NULL;
+ END;
+
+-- Trigger to clean up old login attempts (keep last 1000 entries per user)
+CREATE TRIGGER IF NOT EXISTS cleanup_old_login_attempts
+ AFTER INSERT ON login_attempts
+ BEGIN
+ DELETE FROM login_attempts
+ WHERE id IN (
+ SELECT id FROM login_attempts
+ WHERE user_id = NEW.user_id
+ ORDER BY attempted_at DESC
+ LIMIT -1 OFFSET 1000
+ );
+ END;
+
+-- Trigger to clean up expired user sessions on new session creation
+CREATE TRIGGER IF NOT EXISTS cleanup_expired_sessions
+ AFTER INSERT ON user_sessions
+ BEGIN
+ DELETE FROM user_sessions
+ WHERE expires_at < datetime('now');
+ END;
+
+-- Trigger to automatically unlock accounts when lock period expires
+CREATE TRIGGER IF NOT EXISTS auto_unlock_accounts
+ BEFORE UPDATE ON users
+ FOR EACH ROW
+ WHEN NEW.locked_until IS NOT NULL
+ AND NEW.locked_until < datetime('now')
+ BEGIN
+ UPDATE users
+ SET locked_until = NULL,
+ failed_login_attempts = 0
+ WHERE id = NEW.id;
+ END;
diff --git a/migrations/003_rbac_system_postgres.sql b/migrations/003_rbac_system_postgres.sql
new file mode 100644
index 0000000..c25a84d
--- /dev/null
+++ b/migrations/003_rbac_system_postgres.sql
@@ -0,0 +1,320 @@
+-- RBAC System Migration for PostgreSQL
+-- Migration: 003_rbac_system
+-- Database: PostgreSQL
+
+-- User categories table
+CREATE TABLE IF NOT EXISTS user_categories (
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
+ name VARCHAR(100) NOT NULL UNIQUE,
+ description TEXT,
+ parent_id UUID REFERENCES user_categories(id) ON DELETE CASCADE,
+ metadata JSONB,
+ is_active BOOLEAN NOT NULL DEFAULT true,
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
+);
+
+-- User tags table
+CREATE TABLE IF NOT EXISTS user_tags (
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
+ name VARCHAR(100) NOT NULL UNIQUE,
+ description TEXT,
+ color VARCHAR(7), -- hex color code
+ metadata JSONB,
+ is_active BOOLEAN NOT NULL DEFAULT true,
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
+);
+
+-- User category assignments
+CREATE TABLE IF NOT EXISTS user_category_assignments (
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
+ user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
+ category_id UUID NOT NULL REFERENCES user_categories(id) ON DELETE CASCADE,
+ assigned_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+ assigned_by UUID REFERENCES users(id),
+ expires_at TIMESTAMPTZ,
+ UNIQUE(user_id, category_id)
+);
+
+-- User tag assignments
+CREATE TABLE IF NOT EXISTS user_tag_assignments (
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
+ user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
+ tag_id UUID NOT NULL REFERENCES user_tags(id) ON DELETE CASCADE,
+ assigned_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+ assigned_by UUID REFERENCES users(id),
+ expires_at TIMESTAMPTZ,
+ UNIQUE(user_id, tag_id)
+);
+
+-- Access rules table
+CREATE TABLE IF NOT EXISTS access_rules (
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
+ name VARCHAR(255) NOT NULL,
+ description TEXT,
+ resource_type VARCHAR(50) NOT NULL, -- 'database', 'file', 'directory', 'content', 'api'
+ resource_name VARCHAR(500) NOT NULL, -- supports wildcards
+ action VARCHAR(50) NOT NULL, -- 'read', 'write', 'delete', 'execute'
+ priority INTEGER NOT NULL DEFAULT 0, -- higher priority rules evaluated first
+ is_active BOOLEAN NOT NULL DEFAULT true,
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
+);
+
+-- Role requirements for access rules
+CREATE TABLE IF NOT EXISTS access_rule_roles (
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
+ rule_id UUID NOT NULL REFERENCES access_rules(id) ON DELETE CASCADE,
+ role_id UUID NOT NULL REFERENCES user_roles(id) ON DELETE CASCADE,
+ UNIQUE(rule_id, role_id)
+);
+
+-- Permission requirements for access rules
+CREATE TABLE IF NOT EXISTS access_rule_permissions (
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
+ rule_id UUID NOT NULL REFERENCES access_rules(id) ON DELETE CASCADE,
+ permission_name VARCHAR(100) NOT NULL,
+ resource_scope VARCHAR(255), -- optional scope for permission
+ UNIQUE(rule_id, permission_name, resource_scope)
+);
+
+-- Category requirements for access rules
+CREATE TABLE IF NOT EXISTS access_rule_categories (
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
+ rule_id UUID NOT NULL REFERENCES access_rules(id) ON DELETE CASCADE,
+ category_id UUID NOT NULL REFERENCES user_categories(id) ON DELETE CASCADE,
+ requirement_type VARCHAR(20) NOT NULL DEFAULT 'required', -- 'required', 'denied'
+ UNIQUE(rule_id, category_id, requirement_type)
+);
+
+-- Tag requirements for access rules
+CREATE TABLE IF NOT EXISTS access_rule_tags (
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
+ rule_id UUID NOT NULL REFERENCES access_rules(id) ON DELETE CASCADE,
+ tag_id UUID NOT NULL REFERENCES user_tags(id) ON DELETE CASCADE,
+ requirement_type VARCHAR(20) NOT NULL DEFAULT 'required', -- 'required', 'denied'
+ UNIQUE(rule_id, tag_id, requirement_type)
+);
+
+-- Permission cache for performance
+CREATE TABLE IF NOT EXISTS user_permission_cache (
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
+ user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
+ resource_type VARCHAR(50) NOT NULL,
+ resource_name VARCHAR(500) NOT NULL,
+ action VARCHAR(50) NOT NULL,
+ access_result VARCHAR(20) NOT NULL, -- 'allow', 'deny', 'require_additional_auth'
+ cache_key VARCHAR(255) NOT NULL,
+ expires_at TIMESTAMPTZ NOT NULL,
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+ UNIQUE(user_id, cache_key)
+);
+
+-- RBAC configuration table for storing TOML configs
+CREATE TABLE IF NOT EXISTS rbac_configs (
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
+ name VARCHAR(100) NOT NULL UNIQUE,
+ description TEXT,
+ config_data JSONB NOT NULL,
+ is_active BOOLEAN NOT NULL DEFAULT true,
+ version INTEGER NOT NULL DEFAULT 1,
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
+);
+
+-- Audit log for access attempts
+CREATE TABLE IF NOT EXISTS access_audit_log (
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
+ user_id UUID REFERENCES users(id) ON DELETE SET NULL,
+ resource_type VARCHAR(50) NOT NULL,
+ resource_name VARCHAR(500) NOT NULL,
+ action VARCHAR(50) NOT NULL,
+ access_result VARCHAR(20) NOT NULL,
+ rule_id UUID REFERENCES access_rules(id) ON DELETE SET NULL,
+ ip_address INET,
+ user_agent TEXT,
+ session_id VARCHAR(255),
+ additional_context JSONB,
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
+);
+
+-- Create indexes for performance
+CREATE INDEX IF NOT EXISTS idx_user_categories_name ON user_categories(name);
+CREATE INDEX IF NOT EXISTS idx_user_categories_parent ON user_categories(parent_id);
+CREATE INDEX IF NOT EXISTS idx_user_categories_active ON user_categories(is_active);
+
+CREATE INDEX IF NOT EXISTS idx_user_tags_name ON user_tags(name);
+CREATE INDEX IF NOT EXISTS idx_user_tags_active ON user_tags(is_active);
+
+CREATE INDEX IF NOT EXISTS idx_user_category_assignments_user ON user_category_assignments(user_id);
+CREATE INDEX IF NOT EXISTS idx_user_category_assignments_category ON user_category_assignments(category_id);
+CREATE INDEX IF NOT EXISTS idx_user_category_assignments_expires ON user_category_assignments(expires_at);
+
+CREATE INDEX IF NOT EXISTS idx_user_tag_assignments_user ON user_tag_assignments(user_id);
+CREATE INDEX IF NOT EXISTS idx_user_tag_assignments_tag ON user_tag_assignments(tag_id);
+CREATE INDEX IF NOT EXISTS idx_user_tag_assignments_expires ON user_tag_assignments(expires_at);
+
+CREATE INDEX IF NOT EXISTS idx_access_rules_resource ON access_rules(resource_type, resource_name);
+CREATE INDEX IF NOT EXISTS idx_access_rules_action ON access_rules(action);
+CREATE INDEX IF NOT EXISTS idx_access_rules_priority ON access_rules(priority DESC);
+CREATE INDEX IF NOT EXISTS idx_access_rules_active ON access_rules(is_active);
+
+CREATE INDEX IF NOT EXISTS idx_access_rule_roles_rule ON access_rule_roles(rule_id);
+CREATE INDEX IF NOT EXISTS idx_access_rule_roles_role ON access_rule_roles(role_id);
+
+CREATE INDEX IF NOT EXISTS idx_access_rule_permissions_rule ON access_rule_permissions(rule_id);
+CREATE INDEX IF NOT EXISTS idx_access_rule_permissions_name ON access_rule_permissions(permission_name);
+
+CREATE INDEX IF NOT EXISTS idx_access_rule_categories_rule ON access_rule_categories(rule_id);
+CREATE INDEX IF NOT EXISTS idx_access_rule_categories_category ON access_rule_categories(category_id);
+
+CREATE INDEX IF NOT EXISTS idx_access_rule_tags_rule ON access_rule_tags(rule_id);
+CREATE INDEX IF NOT EXISTS idx_access_rule_tags_tag ON access_rule_tags(tag_id);
+
+CREATE INDEX IF NOT EXISTS idx_user_permission_cache_user ON user_permission_cache(user_id);
+CREATE INDEX IF NOT EXISTS idx_user_permission_cache_key ON user_permission_cache(cache_key);
+CREATE INDEX IF NOT EXISTS idx_user_permission_cache_expires ON user_permission_cache(expires_at);
+
+CREATE INDEX IF NOT EXISTS idx_rbac_configs_name ON rbac_configs(name);
+CREATE INDEX IF NOT EXISTS idx_rbac_configs_active ON rbac_configs(is_active);
+
+CREATE INDEX IF NOT EXISTS idx_access_audit_log_user ON access_audit_log(user_id);
+CREATE INDEX IF NOT EXISTS idx_access_audit_log_resource ON access_audit_log(resource_type, resource_name);
+CREATE INDEX IF NOT EXISTS idx_access_audit_log_created ON access_audit_log(created_at);
+
+-- Add triggers for updated_at columns
+CREATE TRIGGER update_user_categories_updated_at
+ BEFORE UPDATE ON user_categories
+ FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
+
+CREATE TRIGGER update_user_tags_updated_at
+ BEFORE UPDATE ON user_tags
+ FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
+
+CREATE TRIGGER update_access_rules_updated_at
+ BEFORE UPDATE ON access_rules
+ FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
+
+CREATE TRIGGER update_rbac_configs_updated_at
+ BEFORE UPDATE ON rbac_configs
+ FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
+
+-- Function to clean up expired permission cache
+CREATE OR REPLACE FUNCTION cleanup_expired_permission_cache()
+RETURNS INTEGER AS $$
+DECLARE
+ deleted_count INTEGER;
+BEGIN
+ DELETE FROM user_permission_cache WHERE expires_at < NOW();
+ GET DIAGNOSTICS deleted_count = ROW_COUNT;
+ RETURN deleted_count;
+END;
+$$ LANGUAGE plpgsql;
+
+-- Function to get user categories (including inherited)
+CREATE OR REPLACE FUNCTION get_user_categories(user_uuid UUID)
+RETURNS TABLE(category_name VARCHAR(100)) AS $$
+BEGIN
+ RETURN QUERY
+ WITH RECURSIVE category_tree AS (
+ -- Direct categories
+ SELECT uc.name, uc.parent_id
+ FROM user_categories uc
+ JOIN user_category_assignments uca ON uc.id = uca.category_id
+ WHERE uca.user_id = user_uuid
+ AND uc.is_active = true
+ AND (uca.expires_at IS NULL OR uca.expires_at > NOW())
+
+ UNION ALL
+
+ -- Parent categories
+ SELECT uc.name, uc.parent_id
+ FROM user_categories uc
+ JOIN category_tree ct ON uc.id = ct.parent_id
+ WHERE uc.is_active = true
+ )
+ SELECT DISTINCT ct.name
+ FROM category_tree ct;
+END;
+$$ LANGUAGE plpgsql;
+
+-- Function to get user tags
+CREATE OR REPLACE FUNCTION get_user_tags(user_uuid UUID)
+RETURNS TABLE(tag_name VARCHAR(100)) AS $$
+BEGIN
+ RETURN QUERY
+ SELECT ut.name
+ FROM user_tags ut
+ JOIN user_tag_assignments uta ON ut.id = uta.tag_id
+ WHERE uta.user_id = user_uuid
+ AND ut.is_active = true
+ AND (uta.expires_at IS NULL OR uta.expires_at > NOW());
+END;
+$$ LANGUAGE plpgsql;
+
+-- Insert default categories
+INSERT INTO user_categories (name, description) VALUES
+('admin', 'Administrative access category'),
+('editor', 'Content editing access category'),
+('viewer', 'Read-only access category'),
+('finance', 'Financial data access category'),
+('hr', 'Human resources access category'),
+('it', 'Information technology access category')
+ON CONFLICT (name) DO NOTHING;
+
+-- Insert default tags
+INSERT INTO user_tags (name, description, color) VALUES
+('sensitive', 'Access to sensitive data', '#ff0000'),
+('public', 'Public data access', '#00ff00'),
+('internal', 'Internal data access', '#ffff00'),
+('confidential', 'Confidential data access', '#ff8800'),
+('restricted', 'Restricted access', '#8800ff'),
+('temporary', 'Temporary access', '#00ffff')
+ON CONFLICT (name) DO NOTHING;
+
+-- Insert default RBAC configuration
+INSERT INTO rbac_configs (name, description, config_data) VALUES
+('default', 'Default RBAC configuration', '{
+ "rules": [
+ {
+ "id": "admin_full_access",
+ "resource_type": "Database",
+ "resource_name": "*",
+ "allowed_roles": ["Admin"],
+ "allowed_permissions": [],
+ "required_categories": [],
+ "required_tags": [],
+ "deny_categories": [],
+ "deny_tags": [],
+ "is_active": true
+ },
+ {
+ "id": "editor_content_access",
+ "resource_type": "Database",
+ "resource_name": "content*",
+ "allowed_roles": ["Editor"],
+ "allowed_permissions": ["WriteContent"],
+ "required_categories": ["editor"],
+ "required_tags": [],
+ "deny_categories": [],
+ "deny_tags": ["restricted"],
+ "is_active": true
+ }
+ ],
+ "default_permissions": {
+ "Database": ["ReadContent"],
+ "File": ["ReadFile"]
+ },
+ "category_hierarchies": {
+ "admin": ["editor", "viewer"],
+ "editor": ["viewer"]
+ },
+ "tag_hierarchies": {
+ "public": ["internal"],
+ "internal": ["confidential"],
+ "confidential": ["restricted"]
+ },
+ "cache_ttl_seconds": 300
+}')
+ON CONFLICT (name) DO NOTHING;
diff --git a/migrations/README.md b/migrations/README.md
new file mode 100644
index 0000000..0e4cdcd
--- /dev/null
+++ b/migrations/README.md
@@ -0,0 +1,316 @@
+# Database Migrations
+
+This directory contains SQL migration files for the Rustelo application database setup.
+
+## Overview
+
+The migration system sets up a complete database with authentication, content management, and auditing capabilities. **Rustelo now supports both PostgreSQL and SQLite** through a database-agnostic architecture.
+
+## Migration Files
+
+Rustelo provides **database-specific migration files** to support both PostgreSQL and SQLite:
+
+### Database-Specific Files
+- `001_initial_setup_postgres.sql` - PostgreSQL-optimized schema
+- `001_initial_setup_sqlite.sql` - SQLite-optimized schema
+- `002_add_2fa_support_postgres.sql` - PostgreSQL 2FA tables
+- `002_add_2fa_support_sqlite.sql` - SQLite 2FA tables
+- `003_rbac_system_postgres.sql` - PostgreSQL RBAC system
+
+### Legacy Files
+- `001_initial_setup.sql` - Legacy unified file (deprecated)
+- `002_add_2fa_support.sql` - Legacy 2FA file (deprecated)
+
+The migration system **automatically detects your database type** from the connection URL and runs the appropriate migration files.
+
+### Schema Features
+
+#### Authentication Tables
+- **users** - Core user accounts and profile information
+- **user_roles** - User role assignments for RBAC (Role-Based Access Control)
+- **oauth_accounts** - External OAuth provider account links
+- **sessions** - User session management
+- **tokens** - Security tokens for password reset, email verification, etc.
+- **permissions** - System permissions for fine-grained access control
+- **role_permissions** - Role to permission mappings
+- **user_audit_log** - Audit trail for user actions
+
+#### Content Management Tables
+- **page_contents** - Main content management table for pages, posts, and other content
+
+#### Key Features
+- **UUID Primary Keys** - All tables use UUID primary keys for better security
+- **Comprehensive Indexing** - Optimized indexes for performance
+- **Full-Text Search** - PostgreSQL full-text search capabilities
+- **Audit Logging** - Complete audit trail for user actions
+- **Role-Based Access Control** - Flexible permission system
+- **Automatic Timestamps** - Automatic created_at/updated_at handling
+- **Data Validation** - Comprehensive constraints and validation rules
+
+## Database Schema
+
+### User Management
+```sql
+users (id, email, username, password_hash, display_name, ...)
+user_roles (user_id, role)
+oauth_accounts (id, user_id, provider, provider_id, ...)
+sessions (id, user_id, expires_at, ...)
+tokens (id, user_id, token_type, token_hash, ...)
+```
+
+### Authorization
+```sql
+permissions (id, name, description, resource, action)
+role_permissions (role, permission_id)
+user_audit_log (id, user_id, action, resource, ...)
+```
+
+### Content Management
+```sql
+page_contents (id, slug, title, content, author_id, ...)
+```
+
+## Default Data
+
+The migration includes default data:
+
+### User Roles
+- **admin** - Full system access
+- **moderator** - Content management and user oversight
+- **user** - Basic content creation
+- **guest** - Read-only access
+
+### Default Admin User
+- **Username**: admin
+- **Email**: admin@example.com
+- **Password**: admin123 (⚠️ **CHANGE THIS IMMEDIATELY IN PRODUCTION!**)
+
+### Sample Content
+- Welcome page
+- About page
+- Sample blog post
+
+## Functions and Triggers
+
+### Automatic Triggers
+- `update_updated_at_column()` - Updates timestamps automatically
+- `assign_default_role()` - Assigns default role to new users
+
+### Utility Functions
+- `log_user_action()` - Logs user actions for auditing
+- `cleanup_expired_auth_data()` - Cleans up expired sessions and tokens
+
+## Running Migrations
+
+### Using the Built-in Migration Runner (Recommended)
+```bash
+# The application automatically runs migrations on startup
+cargo run
+
+# Or use the database tool
+cargo run --bin db_tool -- migrate
+```
+
+### Database Type Detection
+The migration system automatically detects your database type from the `DATABASE_URL`:
+
+```bash
+# PostgreSQL - runs *_postgres.sql files
+DATABASE_URL=postgresql://user:pass@localhost/database_name
+
+# SQLite - runs *_sqlite.sql files
+DATABASE_URL=sqlite:data/app.db
+```
+
+### Using SQLx CLI
+```bash
+# Install sqlx-cli
+cargo install sqlx-cli
+
+# PostgreSQL migrations
+sqlx migrate run --database-url "postgresql://username:password@localhost/database_name"
+
+# SQLite migrations
+sqlx migrate run --database-url "sqlite:data/app.db"
+```
+
+### Manual Migration
+```bash
+# PostgreSQL
+psql -U username -d database_name -f 001_initial_setup_postgres.sql
+
+# SQLite
+sqlite3 data/app.db < 001_initial_setup_sqlite.sql
+```
+
+## Environment Setup
+
+Configure your database connection using the `DATABASE_URL` environment variable:
+
+### PostgreSQL
+```env
+DATABASE_URL=postgresql://username:password@localhost:5432/database_name
+```
+
+### SQLite
+```env
+DATABASE_URL=sqlite:data/app.db
+```
+
+### Additional Database Configuration
+```env
+# Connection pool settings (optional)
+DATABASE_MAX_CONNECTIONS=10
+DATABASE_MIN_CONNECTIONS=1
+DATABASE_CONNECT_TIMEOUT=30
+DATABASE_IDLE_TIMEOUT=600
+DATABASE_MAX_LIFETIME=3600
+```
+
+## Security Considerations
+
+1. **Change Default Admin Password** - The default admin password is `admin123`
+2. **Review Permissions** - Customize role permissions based on your needs
+3. **Configure OAuth** - Set up OAuth providers if using external authentication
+4. **Enable SSL** - Use SSL connections in production (PostgreSQL)
+5. **Backup Strategy** - Implement regular database backups
+6. **File Permissions** - Secure SQLite database files (chmod 600)
+7. **Database Location** - Store SQLite files outside web root
+
+## Schema Evolution
+
+For future schema changes:
+
+1. Create **database-specific** migration files:
+ - `003_new_feature_postgres.sql`
+ - `003_new_feature_sqlite.sql`
+2. Always use `CREATE TABLE IF NOT EXISTS` for safety
+3. Add proper indexes for performance
+4. Handle database-specific differences (UUID vs TEXT, etc.)
+5. Test migrations on both database types
+6. Include rollback scripts when possible
+7. Test migrations on a copy of production data
+
+### Database Differences to Consider
+- **UUIDs**: PostgreSQL native vs SQLite TEXT
+- **Timestamps**: PostgreSQL native vs SQLite TEXT (ISO 8601)
+- **JSON**: PostgreSQL JSONB vs SQLite TEXT
+- **Arrays**: PostgreSQL arrays vs SQLite JSON arrays
+- **Booleans**: PostgreSQL BOOLEAN vs SQLite INTEGER
+
+## Performance Notes
+
+### PostgreSQL
+- All tables have appropriate indexes for common queries
+- Full-text search is enabled for content
+- Partial indexes are used for filtered queries
+- GIN indexes are used for JSONB and array columns
+- Connection pooling for high concurrency
+
+### SQLite
+- Optimized indexes for single-user scenarios
+- WAL mode enabled for better concurrency
+- Foreign key constraints enabled
+- Query planner optimizations
+- Smaller memory footprint
+
+## Troubleshooting
+
+### Common Issues
+
+#### PostgreSQL Issues
+1. **Extension Error**: If you get an error about `uuid-ossp` extension, ensure your PostgreSQL user has superuser privileges or the extension is already installed.
+2. **Permission Denied**: Ensure your database user has CREATE privileges.
+3. **Connection Failed**: Check PostgreSQL service is running and connection details are correct.
+
+#### SQLite Issues
+1. **File Permission Error**: Ensure the SQLite file and directory have proper write permissions.
+2. **Database Locked**: Close other connections to the SQLite file.
+3. **Directory Not Found**: Ensure the directory for the SQLite file exists.
+
+#### General Issues
+1. **Constraint Violations**: Check that your data meets the constraint requirements (email format, username format, etc.).
+2. **Migration Version Mismatch**: Ensure all migration files are present and properly numbered.
+3. **Database Type Detection Failed**: Verify your `DATABASE_URL` format is correct.
+
+### Verification
+
+After running the migration, verify the setup:
+
+#### PostgreSQL Verification
+```sql
+-- Check tables were created
+SELECT table_name FROM information_schema.tables WHERE table_schema = 'public';
+
+-- Check default admin user
+SELECT username, email, is_active FROM users WHERE username = 'admin';
+
+-- Check permissions setup
+SELECT COUNT(*) FROM permissions;
+SELECT COUNT(*) FROM role_permissions;
+
+-- Check sample content
+SELECT slug, title, state FROM page_contents;
+```
+
+#### SQLite Verification
+```sql
+-- Check tables were created
+SELECT name FROM sqlite_master WHERE type='table';
+
+-- Check default admin user
+SELECT username, email, is_active FROM users WHERE username = 'admin';
+
+-- Check permissions setup
+SELECT COUNT(*) FROM permissions;
+SELECT COUNT(*) FROM role_permissions;
+
+-- Check sample content
+SELECT slug, title, state FROM page_contents;
+```
+
+## Backup and Restore
+
+### PostgreSQL
+```bash
+# Create backup
+pg_dump -U username -d database_name > backup.sql
+
+# Restore backup
+psql -U username -d database_name < backup.sql
+
+# Compressed backup
+pg_dump -U username -d database_name | gzip > backup.sql.gz
+```
+
+### SQLite
+```bash
+# Create backup (simple copy)
+cp data/app.db data/app_backup.db
+
+# Create SQL dump
+sqlite3 data/app.db .dump > backup.sql
+
+# Restore from SQL dump
+sqlite3 data/app_new.db < backup.sql
+
+# Online backup (while app is running)
+sqlite3 data/app.db ".backup data/app_backup.db"
+```
+
+### Cross-Database Migration
+```bash
+# SQLite to PostgreSQL
+sqlite3 data/app.db .dump | sed 's/INSERT INTO/INSERT INTO public./g' > backup.sql
+psql -U username -d database_name < backup.sql
+
+# Note: May require manual adjustments for data types
+```
+
+## Support
+
+For issues or questions:
+1. Check the application logs
+2. Verify database connectivity
+3. Review the migration file for any custom modifications
+4. Consult the PostgreSQL documentation for specific errors
\ No newline at end of file
diff --git a/migrations/migration_files.md b/migrations/migration_files.md
new file mode 100644
index 0000000..cada3bc
--- /dev/null
+++ b/migrations/migration_files.md
@@ -0,0 +1,111 @@
+📊 **Migration File Comparison**
+
+### **`001_initial_setup.sql` (Comprehensive)**
+This is the **full-featured, production-ready** migration that includes:
+
+**🔐 Authentication & Authorization Tables:**
+- `users` - Core user accounts with complete profile information
+- `user_roles` - Role-based access control (RBAC)
+- `oauth_accounts` - External OAuth provider integrations
+- `sessions` - Comprehensive session management
+- `tokens` - Password reset, email verification tokens
+- `permissions` - Fine-grained permission system
+- `role_permissions` - Role-to-permission mappings
+- `user_audit_log` - Complete audit trail
+
+**📝 Content Management:**
+- `page_contents` - Full-featured content management system
+
+**🎯 Advanced Features:**
+- Complete PostgreSQL functions and triggers
+- Full-text search capabilities
+- Comprehensive indexing strategy
+- Default admin user and permissions
+- Sample content data
+
+### **`001_initial_setup_postgres.sql` (Basic)**
+This is a **simplified, basic version** with only:
+
+**🔐 Basic Authentication:**
+- `users` - Basic user accounts
+- `user_sessions` - Simple session management
+- `user_roles` - Basic role system
+- `user_role_assignments` - User-role relationships
+
+**📝 Basic Content:**
+- `content` - Simple content management
+
+### **`001_initial_setup_sqlite.sql` (Basic)**
+Similar to PostgreSQL version but **adapted for SQLite:**
+- Same basic table structure
+- SQLite-specific data types (TEXT instead of UUID)
+- SQLite-specific syntax adaptations
+- No advanced PostgreSQL features
+
+## 🤔 **Why This Structure Exists?**
+
+### **1. Flexibility for Different Use Cases**
+- **Full Version**: Complete application with all features
+- **Basic Versions**: Minimal setup for simple projects or learning
+
+### **2. Database-Specific Optimizations**
+- **PostgreSQL**: Leverages advanced features (UUID, JSONB, functions)
+- **SQLite**: Optimized for embedded/lightweight usage
+
+### **3. Migration Strategy**
+The system likely uses:
+```rust
+// Pseudocode for migration selection
+match database_type {
+ PostgreSQL => {
+ if features.includes("full_auth") {
+ run_migration("001_initial_setup.sql")
+ } else {
+ run_migration("001_initial_setup_postgres.sql")
+ }
+ }
+ SQLite => run_migration("001_initial_setup_sqlite.sql")
+}
+```
+
+## 📋 **Table Count Comparison**
+
+### **Full Version (`001_initial_setup.sql`)**
+- ✅ `users` (comprehensive with profile fields)
+- ✅ `user_roles` (RBAC)
+- ✅ `oauth_accounts` (OAuth integration)
+- ✅ `sessions` (detailed session management)
+- ✅ `tokens` (password reset, verification)
+- ✅ `permissions` (fine-grained permissions)
+- ✅ `role_permissions` (role-permission mapping)
+- ✅ `user_audit_log` (audit trail)
+- ✅ `page_contents` (full CMS)
+
+**Total: 9 tables** + comprehensive functions, triggers, and sample data
+
+### **Basic Versions**
+- ✅ `users` (basic fields only)
+- ✅ `user_sessions` (simple sessions)
+- ✅ `content` (basic content)
+- ✅ `user_roles` (basic roles)
+- ✅ `user_role_assignments` (role assignments)
+
+**Total: 5 tables** + basic triggers
+
+## 💡 **Recommendation**
+
+For the Rustelo project, I recommend:
+
+1. **Use the full version** (`001_initial_setup.sql`) for production applications
+2. **Use basic versions** for prototyping or learning
+3. **Consider creating a configuration option** to choose migration complexity:
+
+```bash
+# Use full-featured migration
+./scripts/db.sh setup create --features full
+
+# Use basic migration
+./scripts/db.sh setup create --features basic
+```
+
+The full version provides enterprise-grade features like audit logging, OAuth integration, and comprehensive security, while the basic versions are perfect for getting started quickly.
diff --git a/monitoring/grafana/dashboards/rustelo-overview.json b/monitoring/grafana/dashboards/rustelo-overview.json
new file mode 100644
index 0000000..b4e5922
--- /dev/null
+++ b/monitoring/grafana/dashboards/rustelo-overview.json
@@ -0,0 +1,886 @@
+{
+ "annotations": {
+ "list": [
+ {
+ "builtIn": 1,
+ "datasource": {
+ "type": "grafana",
+ "uid": "-- Grafana --"
+ },
+ "enable": true,
+ "hide": true,
+ "iconColor": "rgba(0, 211, 255, 1)",
+ "name": "Annotations & Alerts",
+ "type": "dashboard"
+ }
+ ]
+ },
+ "editable": true,
+ "fiscalYearStartMonth": 0,
+ "graphTooltip": 0,
+ "id": null,
+ "links": [],
+ "liveNow": false,
+ "panels": [
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "thresholds"
+ },
+ "mappings": [],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green",
+ "value": null
+ },
+ {
+ "color": "red",
+ "value": 80
+ }
+ ]
+ }
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 8,
+ "w": 12,
+ "x": 0,
+ "y": 0
+ },
+ "id": 1,
+ "options": {
+ "colorMode": "value",
+ "graphMode": "area",
+ "justifyMode": "auto",
+ "orientation": "auto",
+ "reduceOptions": {
+ "calcs": [
+ "lastNotNull"
+ ],
+ "fields": "",
+ "values": false
+ },
+ "textMode": "auto"
+ },
+ "pluginVersion": "10.0.0",
+ "targets": [
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "expr": "rustelo_uptime_seconds",
+ "refId": "A"
+ }
+ ],
+ "title": "Application Uptime (seconds)",
+ "type": "stat"
+ },
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "palette-classic"
+ },
+ "custom": {
+ "axisLabel": "",
+ "axisPlacement": "auto",
+ "barAlignment": 0,
+ "drawStyle": "line",
+ "fillOpacity": 0,
+ "gradientMode": "none",
+ "hideFrom": {
+ "legend": false,
+ "tooltip": false,
+ "vis": false
+ },
+ "lineInterpolation": "linear",
+ "lineWidth": 1,
+ "pointSize": 5,
+ "scaleDistribution": {
+ "type": "linear"
+ },
+ "showPoints": "auto",
+ "spanNulls": false,
+ "stacking": {
+ "group": "A",
+ "mode": "none"
+ },
+ "thresholdsStyle": {
+ "mode": "off"
+ }
+ },
+ "mappings": [],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green",
+ "value": null
+ },
+ {
+ "color": "red",
+ "value": 80
+ }
+ ]
+ }
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 8,
+ "w": 12,
+ "x": 12,
+ "y": 0
+ },
+ "id": 2,
+ "options": {
+ "legend": {
+ "calcs": [],
+ "displayMode": "list",
+ "placement": "bottom"
+ },
+ "tooltip": {
+ "mode": "single",
+ "sort": "none"
+ }
+ },
+ "targets": [
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "expr": "rate(rustelo_http_requests_total[5m])",
+ "refId": "A"
+ }
+ ],
+ "title": "HTTP Request Rate",
+ "type": "timeseries"
+ },
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "palette-classic"
+ },
+ "custom": {
+ "axisLabel": "",
+ "axisPlacement": "auto",
+ "barAlignment": 0,
+ "drawStyle": "line",
+ "fillOpacity": 0,
+ "gradientMode": "none",
+ "hideFrom": {
+ "legend": false,
+ "tooltip": false,
+ "vis": false
+ },
+ "lineInterpolation": "linear",
+ "lineWidth": 1,
+ "pointSize": 5,
+ "scaleDistribution": {
+ "type": "linear"
+ },
+ "showPoints": "auto",
+ "spanNulls": false,
+ "stacking": {
+ "group": "A",
+ "mode": "none"
+ },
+ "thresholdsStyle": {
+ "mode": "off"
+ }
+ },
+ "mappings": [],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green",
+ "value": null
+ },
+ {
+ "color": "red",
+ "value": 80
+ }
+ ]
+ },
+ "unit": "s"
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 8,
+ "w": 12,
+ "x": 0,
+ "y": 8
+ },
+ "id": 3,
+ "options": {
+ "legend": {
+ "calcs": [],
+ "displayMode": "list",
+ "placement": "bottom"
+ },
+ "tooltip": {
+ "mode": "single",
+ "sort": "none"
+ }
+ },
+ "targets": [
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "expr": "histogram_quantile(0.95, rate(rustelo_http_request_duration_seconds_bucket[5m]))",
+ "refId": "A"
+ }
+ ],
+ "title": "HTTP Request Duration (95th percentile)",
+ "type": "timeseries"
+ },
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "thresholds"
+ },
+ "mappings": [],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green",
+ "value": null
+ },
+ {
+ "color": "red",
+ "value": 80
+ }
+ ]
+ }
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 8,
+ "w": 12,
+ "x": 12,
+ "y": 8
+ },
+ "id": 4,
+ "options": {
+ "colorMode": "value",
+ "graphMode": "area",
+ "justifyMode": "auto",
+ "orientation": "auto",
+ "reduceOptions": {
+ "calcs": [
+ "lastNotNull"
+ ],
+ "fields": "",
+ "values": false
+ },
+ "textMode": "auto"
+ },
+ "pluginVersion": "10.0.0",
+ "targets": [
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "expr": "rustelo_http_requests_in_flight",
+ "refId": "A"
+ }
+ ],
+ "title": "HTTP Requests In Flight",
+ "type": "stat"
+ },
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "palette-classic"
+ },
+ "custom": {
+ "axisLabel": "",
+ "axisPlacement": "auto",
+ "barAlignment": 0,
+ "drawStyle": "line",
+ "fillOpacity": 0,
+ "gradientMode": "none",
+ "hideFrom": {
+ "legend": false,
+ "tooltip": false,
+ "vis": false
+ },
+ "lineInterpolation": "linear",
+ "lineWidth": 1,
+ "pointSize": 5,
+ "scaleDistribution": {
+ "type": "linear"
+ },
+ "showPoints": "auto",
+ "spanNulls": false,
+ "stacking": {
+ "group": "A",
+ "mode": "none"
+ },
+ "thresholdsStyle": {
+ "mode": "off"
+ }
+ },
+ "mappings": [],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green",
+ "value": null
+ },
+ {
+ "color": "red",
+ "value": 80
+ }
+ ]
+ }
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 8,
+ "w": 12,
+ "x": 0,
+ "y": 16
+ },
+ "id": 5,
+ "options": {
+ "legend": {
+ "calcs": [],
+ "displayMode": "list",
+ "placement": "bottom"
+ },
+ "tooltip": {
+ "mode": "single",
+ "sort": "none"
+ }
+ },
+ "targets": [
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "expr": "rustelo_db_connections_active",
+ "refId": "A"
+ },
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "expr": "rustelo_db_connections_idle",
+ "refId": "B"
+ }
+ ],
+ "title": "Database Connections",
+ "type": "timeseries"
+ },
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "palette-classic"
+ },
+ "custom": {
+ "axisLabel": "",
+ "axisPlacement": "auto",
+ "barAlignment": 0,
+ "drawStyle": "line",
+ "fillOpacity": 0,
+ "gradientMode": "none",
+ "hideFrom": {
+ "legend": false,
+ "tooltip": false,
+ "vis": false
+ },
+ "lineInterpolation": "linear",
+ "lineWidth": 1,
+ "pointSize": 5,
+ "scaleDistribution": {
+ "type": "linear"
+ },
+ "showPoints": "auto",
+ "spanNulls": false,
+ "stacking": {
+ "group": "A",
+ "mode": "none"
+ },
+ "thresholdsStyle": {
+ "mode": "off"
+ }
+ },
+ "mappings": [],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green",
+ "value": null
+ },
+ {
+ "color": "red",
+ "value": 80
+ }
+ ]
+ },
+ "unit": "bytes"
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 8,
+ "w": 12,
+ "x": 12,
+ "y": 16
+ },
+ "id": 6,
+ "options": {
+ "legend": {
+ "calcs": [],
+ "displayMode": "list",
+ "placement": "bottom"
+ },
+ "tooltip": {
+ "mode": "single",
+ "sort": "none"
+ }
+ },
+ "targets": [
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "expr": "rustelo_memory_usage_bytes",
+ "refId": "A"
+ }
+ ],
+ "title": "Memory Usage",
+ "type": "timeseries"
+ },
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "palette-classic"
+ },
+ "custom": {
+ "axisLabel": "",
+ "axisPlacement": "auto",
+ "barAlignment": 0,
+ "drawStyle": "line",
+ "fillOpacity": 0,
+ "gradientMode": "none",
+ "hideFrom": {
+ "legend": false,
+ "tooltip": false,
+ "vis": false
+ },
+ "lineInterpolation": "linear",
+ "lineWidth": 1,
+ "pointSize": 5,
+ "scaleDistribution": {
+ "type": "linear"
+ },
+ "showPoints": "auto",
+ "spanNulls": false,
+ "stacking": {
+ "group": "A",
+ "mode": "none"
+ },
+ "thresholdsStyle": {
+ "mode": "off"
+ }
+ },
+ "mappings": [],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green",
+ "value": null
+ },
+ {
+ "color": "red",
+ "value": 80
+ }
+ ]
+ },
+ "unit": "percent"
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 8,
+ "w": 12,
+ "x": 0,
+ "y": 24
+ },
+ "id": 7,
+ "options": {
+ "legend": {
+ "calcs": [],
+ "displayMode": "list",
+ "placement": "bottom"
+ },
+ "tooltip": {
+ "mode": "single",
+ "sort": "none"
+ }
+ },
+ "targets": [
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "expr": "rustelo_cpu_usage_percent",
+ "refId": "A"
+ }
+ ],
+ "title": "CPU Usage",
+ "type": "timeseries"
+ },
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "palette-classic"
+ },
+ "custom": {
+ "axisLabel": "",
+ "axisPlacement": "auto",
+ "barAlignment": 0,
+ "drawStyle": "line",
+ "fillOpacity": 0,
+ "gradientMode": "none",
+ "hideFrom": {
+ "legend": false,
+ "tooltip": false,
+ "vis": false
+ },
+ "lineInterpolation": "linear",
+ "lineWidth": 1,
+ "pointSize": 5,
+ "scaleDistribution": {
+ "type": "linear"
+ },
+ "showPoints": "auto",
+ "spanNulls": false,
+ "stacking": {
+ "group": "A",
+ "mode": "none"
+ },
+ "thresholdsStyle": {
+ "mode": "off"
+ }
+ },
+ "mappings": [],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green",
+ "value": null
+ },
+ {
+ "color": "red",
+ "value": 80
+ }
+ ]
+ }
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 8,
+ "w": 12,
+ "x": 12,
+ "y": 24
+ },
+ "id": 8,
+ "options": {
+ "legend": {
+ "calcs": [],
+ "displayMode": "list",
+ "placement": "bottom"
+ },
+ "tooltip": {
+ "mode": "single",
+ "sort": "none"
+ }
+ },
+ "targets": [
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "expr": "rate(rustelo_auth_requests_total[5m])",
+ "refId": "A"
+ }
+ ],
+ "title": "Authentication Requests Rate",
+ "type": "timeseries"
+ },
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "palette-classic"
+ },
+ "custom": {
+ "axisLabel": "",
+ "axisPlacement": "auto",
+ "barAlignment": 0,
+ "drawStyle": "line",
+ "fillOpacity": 0,
+ "gradientMode": "none",
+ "hideFrom": {
+ "legend": false,
+ "tooltip": false,
+ "vis": false
+ },
+ "lineInterpolation": "linear",
+ "lineWidth": 1,
+ "pointSize": 5,
+ "scaleDistribution": {
+ "type": "linear"
+ },
+ "showPoints": "auto",
+ "spanNulls": false,
+ "stacking": {
+ "group": "A",
+ "mode": "none"
+ },
+ "thresholdsStyle": {
+ "mode": "off"
+ }
+ },
+ "mappings": [],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green",
+ "value": null
+ },
+ {
+ "color": "red",
+ "value": 80
+ }
+ ]
+ }
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 8,
+ "w": 12,
+ "x": 0,
+ "y": 32
+ },
+ "id": 9,
+ "options": {
+ "legend": {
+ "calcs": [],
+ "displayMode": "list",
+ "placement": "bottom"
+ },
+ "tooltip": {
+ "mode": "single",
+ "sort": "none"
+ }
+ },
+ "targets": [
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "expr": "rustelo_content_cache_hits_total",
+ "refId": "A"
+ },
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "expr": "rustelo_content_cache_misses_total",
+ "refId": "B"
+ }
+ ],
+ "title": "Content Cache Performance",
+ "type": "timeseries"
+ },
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "palette-classic"
+ },
+ "custom": {
+ "axisLabel": "",
+ "axisPlacement": "auto",
+ "barAlignment": 0,
+ "drawStyle": "line",
+ "fillOpacity": 0,
+ "gradientMode": "none",
+ "hideFrom": {
+ "legend": false,
+ "tooltip": false,
+ "vis": false
+ },
+ "lineInterpolation": "linear",
+ "lineWidth": 1,
+ "pointSize": 5,
+ "scaleDistribution": {
+ "type": "linear"
+ },
+ "showPoints": "auto",
+ "spanNulls": false,
+ "stacking": {
+ "group": "A",
+ "mode": "none"
+ },
+ "thresholdsStyle": {
+ "mode": "off"
+ }
+ },
+ "mappings": [],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green",
+ "value": null
+ },
+ {
+ "color": "red",
+ "value": 80
+ }
+ ]
+ }
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 8,
+ "w": 12,
+ "x": 12,
+ "y": 32
+ },
+ "id": 10,
+ "options": {
+ "legend": {
+ "calcs": [],
+ "displayMode": "list",
+ "placement": "bottom"
+ },
+ "tooltip": {
+ "mode": "single",
+ "sort": "none"
+ }
+ },
+ "targets": [
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "expr": "rate(rustelo_email_sent_total[5m])",
+ "refId": "A"
+ },
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "expr": "rate(rustelo_email_failures_total[5m])",
+ "refId": "B"
+ }
+ ],
+ "title": "Email Service Performance",
+ "type": "timeseries"
+ }
+ ],
+ "refresh": "5s",
+ "schemaVersion": 37,
+ "style": "dark",
+ "tags": [
+ "rustelo",
+ "application",
+ "overview"
+ ],
+ "templating": {
+ "list": []
+ },
+ "time": {
+ "from": "now-1h",
+ "to": "now"
+ },
+ "timepicker": {},
+ "timezone": "",
+ "title": "Rustelo Application Overview",
+ "uid": "rustelo-overview",
+ "version": 1,
+ "weekStart": ""
+}
diff --git a/monitoring/grafana/provisioning/dashboards/dashboards.yml b/monitoring/grafana/provisioning/dashboards/dashboards.yml
new file mode 100644
index 0000000..98533c2
--- /dev/null
+++ b/monitoring/grafana/provisioning/dashboards/dashboards.yml
@@ -0,0 +1,42 @@
+apiVersion: 1
+
+providers:
+ - name: 'default'
+ orgId: 1
+ folder: ''
+ type: file
+ disableDeletion: false
+ updateIntervalSeconds: 10
+ allowUiUpdates: true
+ options:
+ path: /var/lib/grafana/dashboards
+
+ - name: 'rustelo'
+ orgId: 1
+ folder: 'Rustelo'
+ type: file
+ disableDeletion: false
+ updateIntervalSeconds: 10
+ allowUiUpdates: true
+ options:
+ path: /var/lib/grafana/dashboards/rustelo
+
+ - name: 'system'
+ orgId: 1
+ folder: 'System'
+ type: file
+ disableDeletion: false
+ updateIntervalSeconds: 10
+ allowUiUpdates: true
+ options:
+ path: /var/lib/grafana/dashboards/system
+
+ - name: 'business'
+ orgId: 1
+ folder: 'Business Metrics'
+ type: file
+ disableDeletion: false
+ updateIntervalSeconds: 10
+ allowUiUpdates: true
+ options:
+ path: /var/lib/grafana/dashboards/business
diff --git a/monitoring/grafana/provisioning/datasources/datasources.yml b/monitoring/grafana/provisioning/datasources/datasources.yml
new file mode 100644
index 0000000..9169814
--- /dev/null
+++ b/monitoring/grafana/provisioning/datasources/datasources.yml
@@ -0,0 +1,37 @@
+apiVersion: 1
+
+datasources:
+ - name: Prometheus
+ type: prometheus
+ access: proxy
+ url: http://prometheus:9090
+ isDefault: true
+ editable: true
+ jsonData:
+ httpMethod: POST
+ queryTimeout: 60s
+ timeInterval: 15s
+ version: 1
+
+ - name: Loki
+ type: loki
+ access: proxy
+ url: http://loki:3100
+ isDefault: false
+ editable: true
+ jsonData:
+ maxLines: 1000
+ timeout: 60s
+ version: 1
+
+ - name: Rustelo Health
+ type: prometheus
+ access: proxy
+ url: http://app:3030/metrics/health
+ isDefault: false
+ editable: true
+ jsonData:
+ httpMethod: GET
+ queryTimeout: 30s
+ timeInterval: 30s
+ version: 1
diff --git a/monitoring/prometheus.yml b/monitoring/prometheus.yml
new file mode 100644
index 0000000..6cc77d0
--- /dev/null
+++ b/monitoring/prometheus.yml
@@ -0,0 +1,67 @@
+global:
+ scrape_interval: 15s
+ evaluation_interval: 15s
+
+rule_files:
+ - "rules/*.yml"
+
+scrape_configs:
+ # Rustelo application metrics
+ - job_name: 'rustelo-app'
+ static_configs:
+ - targets: ['app:3030']
+ scrape_interval: 15s
+ metrics_path: '/metrics'
+ scheme: http
+ scrape_timeout: 10s
+
+ # Prometheus self-monitoring
+ - job_name: 'prometheus'
+ static_configs:
+ - targets: ['localhost:9090']
+
+ # Node exporter for system metrics (optional)
+ - job_name: 'node-exporter'
+ static_configs:
+ - targets: ['node-exporter:9100']
+ scrape_interval: 15s
+
+ # PostgreSQL metrics (optional)
+ - job_name: 'postgres-exporter'
+ static_configs:
+ - targets: ['postgres-exporter:9187']
+ scrape_interval: 30s
+
+ # Redis metrics (optional)
+ - job_name: 'redis-exporter'
+ static_configs:
+ - targets: ['redis-exporter:9121']
+ scrape_interval: 30s
+
+ # Nginx metrics (optional)
+ - job_name: 'nginx-exporter'
+ static_configs:
+ - targets: ['nginx-exporter:9113']
+ scrape_interval: 30s
+
+ # Custom health check endpoint
+ - job_name: 'rustelo-health'
+ static_configs:
+ - targets: ['app:3030']
+ scrape_interval: 30s
+ metrics_path: '/health'
+ scheme: http
+ scrape_timeout: 10s
+
+alerting:
+ alertmanagers:
+ - static_configs:
+ - targets:
+ - alertmanager:9093
+
+# Storage configuration
+storage:
+ tsdb:
+ path: /prometheus
+ retention.time: 30d
+ retention.size: 10GB
diff --git a/public/favicon.ico b/public/favicon.ico
new file mode 100644
index 0000000..2ba8527
Binary files /dev/null and b/public/favicon.ico differ
diff --git a/public/logos/rustelo-imag.svg b/public/logos/rustelo-imag.svg
new file mode 100644
index 0000000..c22850d
--- /dev/null
+++ b/public/logos/rustelo-imag.svg
@@ -0,0 +1,289 @@
+
+
\ No newline at end of file
diff --git a/public/logos/rustelo_dev-logo-b-h.svg b/public/logos/rustelo_dev-logo-b-h.svg
new file mode 100644
index 0000000..13dc0d5
--- /dev/null
+++ b/public/logos/rustelo_dev-logo-b-h.svg
@@ -0,0 +1,307 @@
+
+
\ No newline at end of file
diff --git a/public/logos/rustelo_dev-logo-b-v.svg b/public/logos/rustelo_dev-logo-b-v.svg
new file mode 100644
index 0000000..070bca2
--- /dev/null
+++ b/public/logos/rustelo_dev-logo-b-v.svg
@@ -0,0 +1,307 @@
+
+
\ No newline at end of file
diff --git a/public/logos/rustelo_dev-logo-h.svg b/public/logos/rustelo_dev-logo-h.svg
new file mode 100644
index 0000000..5576400
--- /dev/null
+++ b/public/logos/rustelo_dev-logo-h.svg
@@ -0,0 +1,307 @@
+
+
\ No newline at end of file
diff --git a/public/logos/rustelo_dev-logo-v.svg b/public/logos/rustelo_dev-logo-v.svg
new file mode 100644
index 0000000..65c3822
--- /dev/null
+++ b/public/logos/rustelo_dev-logo-v.svg
@@ -0,0 +1,307 @@
+
+
\ No newline at end of file
diff --git a/public/website.css b/public/website.css
new file mode 100644
index 0000000..eb3d53e
--- /dev/null
+++ b/public/website.css
@@ -0,0 +1,2 @@
+/* layer: preflights */
+*,::before,::after{--un-rotate:0;--un-rotate-x:0;--un-rotate-y:0;--un-rotate-z:0;--un-scale-x:1;--un-scale-y:1;--un-scale-z:1;--un-skew-x:0;--un-skew-y:0;--un-translate-x:0;--un-translate-y:0;--un-translate-z:0;--un-pan-x: ;--un-pan-y: ;--un-pinch-zoom: ;--un-scroll-snap-strictness:proximity;--un-ordinal: ;--un-slashed-zero: ;--un-numeric-figure: ;--un-numeric-spacing: ;--un-numeric-fraction: ;--un-border-spacing-x:0;--un-border-spacing-y:0;--un-ring-offset-shadow:0 0 rgb(0 0 0 / 0);--un-ring-shadow:0 0 rgb(0 0 0 / 0);--un-shadow-inset: ;--un-shadow:0 0 rgb(0 0 0 / 0);--un-ring-inset: ;--un-ring-offset-width:0px;--un-ring-offset-color:#fff;--un-ring-width:0px;--un-ring-color:rgb(147 197 253 / 0.5);--un-blur: ;--un-brightness: ;--un-contrast: ;--un-drop-shadow: ;--un-grayscale: ;--un-hue-rotate: ;--un-invert: ;--un-saturate: ;--un-sepia: ;--un-backdrop-blur: ;--un-backdrop-brightness: ;--un-backdrop-contrast: ;--un-backdrop-grayscale: ;--un-backdrop-hue-rotate: ;--un-backdrop-invert: ;--un-backdrop-opacity: ;--un-backdrop-saturate: ;--un-backdrop-sepia: ;}::backdrop{--un-rotate:0;--un-rotate-x:0;--un-rotate-y:0;--un-rotate-z:0;--un-scale-x:1;--un-scale-y:1;--un-scale-z:1;--un-skew-x:0;--un-skew-y:0;--un-translate-x:0;--un-translate-y:0;--un-translate-z:0;--un-pan-x: ;--un-pan-y: ;--un-pinch-zoom: ;--un-scroll-snap-strictness:proximity;--un-ordinal: ;--un-slashed-zero: ;--un-numeric-figure: ;--un-numeric-spacing: ;--un-numeric-fraction: ;--un-border-spacing-x:0;--un-border-spacing-y:0;--un-ring-offset-shadow:0 0 rgb(0 0 0 / 0);--un-ring-shadow:0 0 rgb(0 0 0 / 0);--un-shadow-inset: ;--un-shadow:0 0 rgb(0 0 0 / 0);--un-ring-inset: ;--un-ring-offset-width:0px;--un-ring-offset-color:#fff;--un-ring-width:0px;--un-ring-color:rgb(147 197 253 / 0.5);--un-blur: ;--un-brightness: ;--un-contrast: ;--un-drop-shadow: ;--un-grayscale: ;--un-hue-rotate: ;--un-invert: ;--un-saturate: ;--un-sepia: ;--un-backdrop-blur: ;--un-backdrop-brightness: ;--un-backdrop-contrast: ;--un-backdrop-grayscale: ;--un-backdrop-hue-rotate: ;--un-backdrop-invert: ;--un-backdrop-opacity: ;--un-backdrop-saturate: ;--un-backdrop-sepia: ;}
\ No newline at end of file
diff --git a/style/main.scss b/style/main.scss
new file mode 100644
index 0000000..d60bf62
--- /dev/null
+++ b/style/main.scss
@@ -0,0 +1,9 @@
+/* layer: preflights */
+*,::before,::after{--un-rotate:0;--un-rotate-x:0;--un-rotate-y:0;--un-rotate-z:0;--un-scale-x:1;--un-scale-y:1;--un-scale-z:1;--un-skew-x:0;--un-skew-y:0;--un-translate-x:0;--un-translate-y:0;--un-translate-z:0;--un-pan-x: ;--un-pan-y: ;--un-pinch-zoom: ;--un-scroll-snap-strictness:proximity;--un-ordinal: ;--un-slashed-zero: ;--un-numeric-figure: ;--un-numeric-spacing: ;--un-numeric-fraction: ;--un-border-spacing-x:0;--un-border-spacing-y:0;--un-ring-offset-shadow:0 0 rgb(0 0 0 / 0);--un-ring-shadow:0 0 rgb(0 0 0 / 0);--un-shadow-inset: ;--un-shadow:0 0 rgb(0 0 0 / 0);--un-ring-inset: ;--un-ring-offset-width:0px;--un-ring-offset-color:#fff;--un-ring-width:0px;--un-ring-color:rgb(147 197 253 / 0.5);--un-blur: ;--un-brightness: ;--un-contrast: ;--un-drop-shadow: ;--un-grayscale: ;--un-hue-rotate: ;--un-invert: ;--un-saturate: ;--un-sepia: ;--un-backdrop-blur: ;--un-backdrop-brightness: ;--un-backdrop-contrast: ;--un-backdrop-grayscale: ;--un-backdrop-hue-rotate: ;--un-backdrop-invert: ;--un-backdrop-opacity: ;--un-backdrop-saturate: ;--un-backdrop-sepia: ;}::backdrop{--un-rotate:0;--un-rotate-x:0;--un-rotate-y:0;--un-rotate-z:0;--un-scale-x:1;--un-scale-y:1;--un-scale-z:1;--un-skew-x:0;--un-skew-y:0;--un-translate-x:0;--un-translate-y:0;--un-translate-z:0;--un-pan-x: ;--un-pan-y: ;--un-pinch-zoom: ;--un-scroll-snap-strictness:proximity;--un-ordinal: ;--un-slashed-zero: ;--un-numeric-figure: ;--un-numeric-spacing: ;--un-numeric-fraction: ;--un-border-spacing-x:0;--un-border-spacing-y:0;--un-ring-offset-shadow:0 0 rgb(0 0 0 / 0);--un-ring-shadow:0 0 rgb(0 0 0 / 0);--un-shadow-inset: ;--un-shadow:0 0 rgb(0 0 0 / 0);--un-ring-inset: ;--un-ring-offset-width:0px;--un-ring-offset-color:#fff;--un-ring-width:0px;--un-ring-color:rgb(147 197 253 / 0.5);--un-blur: ;--un-brightness: ;--un-contrast: ;--un-drop-shadow: ;--un-grayscale: ;--un-hue-rotate: ;--un-invert: ;--un-saturate: ;--un-sepia: ;--un-backdrop-blur: ;--un-backdrop-brightness: ;--un-backdrop-contrast: ;--un-backdrop-grayscale: ;--un-backdrop-hue-rotate: ;--un-backdrop-invert: ;--un-backdrop-opacity: ;--un-backdrop-saturate: ;--un-backdrop-sepia: ;}
+/* layer: default */
+@keyframes bounce-alt{from,20%,53%,80%,to{animation-timing-function:cubic-bezier(0.215,0.61,0.355,1);transform:translate3d(0,0,0)}40%,43%{animation-timing-function:cubic-bezier(0.755,0.05,0.855,0.06);transform:translate3d(0,-30px,0)}70%{animation-timing-function:cubic-bezier(0.755,0.05,0.855,0.06);transform:translate3d(0,-15px,0)}90%{transform:translate3d(0,-4px,0)}}
+.animate-bounce-alt{animation:bounce-alt 1s linear infinite;transform-origin:center bottom;}
+.animate-duration-1s{animation-duration:1s;}
+.animate-count-1{animation-iteration-count:1;}
+.text-5xl{font-size:3rem;line-height:1;}
+.font-thin{font-weight:100;}
\ No newline at end of file