chore: add main directories
Some checks failed
CI/CD Pipeline / Test Suite (push) Has been cancelled
CI/CD Pipeline / Security Audit (push) Has been cancelled
CI/CD Pipeline / Build Docker Image (push) Has been cancelled
CI/CD Pipeline / Deploy to Staging (push) Has been cancelled
CI/CD Pipeline / Deploy to Production (push) Has been cancelled
CI/CD Pipeline / Performance Benchmarks (push) Has been cancelled
CI/CD Pipeline / Cleanup (push) Has been cancelled
Some checks failed
CI/CD Pipeline / Test Suite (push) Has been cancelled
CI/CD Pipeline / Security Audit (push) Has been cancelled
CI/CD Pipeline / Build Docker Image (push) Has been cancelled
CI/CD Pipeline / Deploy to Staging (push) Has been cancelled
CI/CD Pipeline / Deploy to Production (push) Has been cancelled
CI/CD Pipeline / Performance Benchmarks (push) Has been cancelled
CI/CD Pipeline / Cleanup (push) Has been cancelled
This commit is contained in:
parent
76d374ea18
commit
31ab424d9d
504
migrations/001_initial_setup.sql
Normal file
504
migrations/001_initial_setup.sql
Normal file
@ -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("<h1>Hello, World!</h1>") }));
|
||||
|
||||
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';
|
100
migrations/001_initial_setup_postgres.sql
Normal file
100
migrations/001_initial_setup_postgres.sql
Normal file
@ -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;
|
96
migrations/001_initial_setup_sqlite.sql
Normal file
96
migrations/001_initial_setup_sqlite.sql
Normal file
@ -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;
|
101
migrations/002_add_2fa_support.sql
Normal file
101
migrations/002_add_2fa_support.sql
Normal file
@ -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';
|
131
migrations/002_add_2fa_support_postgres.sql
Normal file
131
migrations/002_add_2fa_support_postgres.sql
Normal file
@ -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';
|
117
migrations/002_add_2fa_support_sqlite.sql
Normal file
117
migrations/002_add_2fa_support_sqlite.sql
Normal file
@ -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;
|
320
migrations/003_rbac_system_postgres.sql
Normal file
320
migrations/003_rbac_system_postgres.sql
Normal file
@ -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;
|
316
migrations/README.md
Normal file
316
migrations/README.md
Normal file
@ -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
|
111
migrations/migration_files.md
Normal file
111
migrations/migration_files.md
Normal file
@ -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.
|
886
monitoring/grafana/dashboards/rustelo-overview.json
Normal file
886
monitoring/grafana/dashboards/rustelo-overview.json
Normal file
@ -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": ""
|
||||
}
|
42
monitoring/grafana/provisioning/dashboards/dashboards.yml
Normal file
42
monitoring/grafana/provisioning/dashboards/dashboards.yml
Normal file
@ -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
|
37
monitoring/grafana/provisioning/datasources/datasources.yml
Normal file
37
monitoring/grafana/provisioning/datasources/datasources.yml
Normal file
@ -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
|
67
monitoring/prometheus.yml
Normal file
67
monitoring/prometheus.yml
Normal file
@ -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
|
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
289
public/logos/rustelo-imag.svg
Normal file
289
public/logos/rustelo-imag.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 24 KiB |
307
public/logos/rustelo_dev-logo-b-h.svg
Normal file
307
public/logos/rustelo_dev-logo-b-h.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 24 KiB |
307
public/logos/rustelo_dev-logo-b-v.svg
Normal file
307
public/logos/rustelo_dev-logo-b-v.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 24 KiB |
307
public/logos/rustelo_dev-logo-h.svg
Normal file
307
public/logos/rustelo_dev-logo-h.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 24 KiB |
307
public/logos/rustelo_dev-logo-v.svg
Normal file
307
public/logos/rustelo_dev-logo-v.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 24 KiB |
2
public/website.css
Normal file
2
public/website.css
Normal file
@ -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: ;}
|
9
style/main.scss
Normal file
9
style/main.scss
Normal file
@ -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;}
|
Loading…
x
Reference in New Issue
Block a user