This commit is contained in:
2025-11-08 15:06:30 +08:00
parent 27614f695f
commit e28e885e66
14 changed files with 99 additions and 78 deletions

View File

@@ -11,7 +11,7 @@ jsonwebtoken = "9.2"
bcrypt = "0.15"
chrono = { version = "0.4", features = ["serde"] }
colored = "2.1.0"
sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "postgres", "chrono", "uuid", "migrate", "json"] }
sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "mysql", "chrono", "uuid", "migrate", "json"] }
anyhow = "1.0.86"
futures = "0.3.30"
macros-rs = "1.2.1"

View File

@@ -5,7 +5,7 @@ address = "127.0.0.1"
port = 8085
[server.database]
url = "postgresql://username:password@localhost:5432/domains"
url = "mysql://root:password@localhost:3306/dns"
# Maximum number of database connections
max_connections = 10

View File

@@ -1,5 +1,5 @@
CREATE TABLE IF NOT EXISTS users (
id SERIAL PRIMARY KEY,
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
registrations_remaining INTEGER DEFAULT 3,
@@ -14,7 +14,7 @@ CREATE TABLE IF NOT EXISTS invite_codes (
code VARCHAR(32) UNIQUE NOT NULL,
created_by INTEGER REFERENCES users(id),
used_by INTEGER REFERENCES users(id),
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
used_at TIMESTAMPTZ
);
@@ -57,7 +57,7 @@ CREATE TABLE IF NOT EXISTS dns_records (
value VARCHAR(1000) NOT NULL,
ttl INTEGER DEFAULT 3600,
priority INTEGER, -- For MX records
created_at TIMESTAMPTZ DEFAULT NOW()
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_dns_records_domain_type ON dns_records(domain_id, record_type);

View File

@@ -1,5 +1,5 @@
-- Make IP column optional for domains
ALTER TABLE domains ALTER COLUMN ip DROP NOT NULL;
ALTER TABLE domains MODIFY COLUMN ip VARCHAR(255) NULL;
-- Update DNS records constraint to only allow A, AAAA, CNAME, TXT
ALTER TABLE dns_records DROP CONSTRAINT IF EXISTS dns_records_record_type_check;

View File

@@ -1,10 +1,10 @@
-- Re-add NS record support and extend record types
ALTER TABLE dns_records DROP CONSTRAINT IF EXISTS dns_records_record_type_check;
ALTER TABLE dns_records DROP CONSTRAINT dns_records_record_type_check;
ALTER TABLE dns_records ADD CONSTRAINT dns_records_record_type_check
CHECK (record_type IN ('A', 'AAAA', 'CNAME', 'TXT', 'NS', 'MX'));
-- Add index for efficient NS record lookups during delegation
CREATE INDEX IF NOT EXISTS idx_dns_records_ns_lookup ON dns_records(record_type, name) WHERE record_type = 'NS';
CREATE INDEX idx_dns_records_ns_lookup ON dns_records(record_type, name) WHERE record_type = 'NS';
-- Add index for subdomain resolution optimization
CREATE INDEX IF NOT EXISTS idx_dns_records_subdomain_lookup ON dns_records(domain_id, name, record_type);
CREATE INDEX idx_dns_records_subdomain_lookup ON dns_records(domain_id, name, record_type);

View File

@@ -4,5 +4,5 @@ ALTER TABLE dns_records ADD CONSTRAINT dns_records_record_type_check
CHECK (record_type IN ('A', 'AAAA', 'CNAME', 'TXT', 'NS'));
-- Add indexes for efficient DNS lookups if they don't exist
CREATE INDEX IF NOT EXISTS idx_dns_records_ns_lookup ON dns_records(record_type, name) WHERE record_type = 'NS';
CREATE INDEX IF NOT EXISTS idx_dns_records_subdomain_lookup ON dns_records(domain_id, name, record_type);
CREATE INDEX idx_dns_records_ns_lookup ON dns_records(record_type, name);
CREATE INDEX idx_dns_records_subdomain_lookup ON dns_records(domain_id, name, record_type);

View File

@@ -1,18 +1,18 @@
-- Add certificate challenges table for CA functionality
CREATE TABLE IF NOT EXISTS certificate_challenges (
id SERIAL PRIMARY KEY,
id INT AUTO_INCREMENT PRIMARY KEY,
token VARCHAR(255) UNIQUE NOT NULL,
domain VARCHAR(255) NOT NULL,
challenge_type VARCHAR(20) NOT NULL CHECK (challenge_type IN ('dns')),
challenge_type VARCHAR(20) NOT NULL,
verification_data VARCHAR(500) NOT NULL,
status VARCHAR(20) DEFAULT 'pending' CHECK (status IN ('pending', 'valid', 'invalid', 'expired')),
created_at TIMESTAMPTZ DEFAULT NOW(),
status VARCHAR(20) DEFAULT 'pending',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
expires_at TIMESTAMPTZ NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_certificate_challenges_token ON certificate_challenges(token);
CREATE INDEX IF NOT EXISTS idx_certificate_challenges_domain ON certificate_challenges(domain);
CREATE INDEX IF NOT EXISTS idx_certificate_challenges_expires_at ON certificate_challenges(expires_at);
CREATE INDEX idx_certificate_challenges_token ON certificate_challenges(token);
CREATE INDEX idx_certificate_challenges_domain ON certificate_challenges(domain);
CREATE INDEX idx_certificate_challenges_expires_at ON certificate_challenges(expires_at);
-- Add table to store issued certificates
CREATE TABLE IF NOT EXISTS issued_certificates (
@@ -21,13 +21,13 @@ CREATE TABLE IF NOT EXISTS issued_certificates (
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
certificate_pem TEXT NOT NULL,
private_key_pem TEXT NOT NULL,
issued_at TIMESTAMPTZ DEFAULT NOW(),
issued_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
expires_at TIMESTAMPTZ NOT NULL,
revoked_at TIMESTAMPTZ,
serial_number VARCHAR(255) UNIQUE NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_issued_certificates_domain ON issued_certificates(domain);
CREATE INDEX IF NOT EXISTS idx_issued_certificates_user_id ON issued_certificates(user_id);
CREATE INDEX IF NOT EXISTS idx_issued_certificates_serial ON issued_certificates(serial_number);
CREATE INDEX IF NOT EXISTS idx_issued_certificates_expires_at ON issued_certificates(expires_at);
CREATE INDEX idx_issued_certificates_domain ON issued_certificates(domain);
CREATE INDEX idx_issued_certificates_user_id ON issued_certificates(user_id);
CREATE INDEX idx_issued_certificates_serial ON issued_certificates(serial_number);
CREATE INDEX idx_issued_certificates_expires_at ON issued_certificates(expires_at);

View File

@@ -2,6 +2,26 @@
DELETE FROM dns_records WHERE record_type NOT IN ('A', 'AAAA', 'CNAME', 'TXT');
-- Now apply the constraint
ALTER TABLE dns_records DROP CONSTRAINT IF EXISTS dns_records_record_type_check;
ALTER TABLE dns_records ADD CONSTRAINT dns_records_record_type_check
CHECK (record_type IN ('A', 'AAAA', 'CNAME', 'TXT'));
ALTER TABLE dns_records DROP CONSTRAINT dns_records_record_type_check;
-- MySQL doesn't support table-level CHECK constraints, using trigger instead
DELIMITER //
CREATE TRIGGER check_record_type_before_insert
BEFORE INSERT ON dns_records
FOR EACH ROW
BEGIN
IF NEW.record_type NOT IN ('A', 'AAAA', 'CNAME', 'TXT') THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Invalid record type';
END IF;
END;
//
CREATE TRIGGER check_record_type_before_update
BEFORE UPDATE ON dns_records
FOR EACH ROW
BEGIN
IF NEW.record_type NOT IN ('A', 'AAAA', 'CNAME', 'TXT') THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Invalid record type';
END IF;
END;
//
DELIMITER ;

View File

@@ -1,8 +1,8 @@
-- Add table to store CA certificate and key
CREATE TABLE IF NOT EXISTS ca_certificates (
id SERIAL PRIMARY KEY,
id INT AUTO_INCREMENT PRIMARY KEY,
ca_cert_pem TEXT NOT NULL,
ca_key_pem TEXT NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
is_active BOOLEAN DEFAULT TRUE
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
is_active TINYINT(1) DEFAULT 1
);

View File

@@ -1,2 +1,2 @@
-- Add CSR field to certificate challenges
ALTER TABLE certificate_challenges ADD COLUMN IF NOT EXISTS csr_pem TEXT;
ALTER TABLE certificate_challenges ADD COLUMN csr_pem TEXT;

View File

@@ -1,28 +1,25 @@
-- Search engine domain crawl status tracking
CREATE TABLE IF NOT EXISTS domain_crawl_status (
domain_id INTEGER PRIMARY KEY REFERENCES domains(id) ON DELETE CASCADE,
last_crawled_at TIMESTAMPTZ,
next_crawl_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
domain_id INT PRIMARY KEY, FOREIGN KEY (domain_id) REFERENCES domains(id) ON DELETE CASCADE,
last_crawled_at TIMESTAMP,
next_crawl_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
crawl_status VARCHAR(20) DEFAULT 'pending' CHECK (crawl_status IN ('pending', 'crawling', 'completed', 'failed', 'disabled')),
error_message TEXT,
pages_found INTEGER DEFAULT 0,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_domain_crawl_status_next_crawl ON domain_crawl_status(next_crawl_at);
CREATE INDEX IF NOT EXISTS idx_domain_crawl_status_status ON domain_crawl_status(crawl_status);
-- Function to update the updated_at column
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = CURRENT_TIMESTAMP;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- Trigger for updated_at
DROP TRIGGER IF EXISTS update_domain_crawl_status_updated_at ON domain_crawl_status;
-- MySQL trigger to update updated_at column
DELIMITER //
CREATE TRIGGER update_domain_crawl_status_updated_at
BEFORE UPDATE ON domain_crawl_status
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
FOR EACH ROW
BEGIN
SET NEW.updated_at = CURRENT_TIMESTAMP;
END;
//
DELIMITER ;

View File

@@ -3,7 +3,7 @@ mod structs;
use colored::Colorize;
use macros_rs::fmt::{crashln, string};
use sqlx::{PgPool, Error};
use sqlx::{MySqlPool, Error};
use std::fs::write;
use structs::{Auth, Database, Server, Settings};
@@ -22,7 +22,7 @@ impl Config {
address: "127.0.0.1".into(),
port: 8080,
database: Database {
url: "postgresql://username:password@localhost/domains".into(),
url: "mysql://root:password@localhost:3306/dns".into(),
max_connections: 10,
},
cert_path: "localhost+2.pem".into(),
@@ -64,13 +64,13 @@ impl Config {
return self;
}
pub async fn connect_to_db(&self) -> Result<PgPool, Error> {
let pool = PgPool::connect(&self.server.database.url).await?;
pub async fn connect_to_db(&self) -> Result<MySqlPool, Error> {
let pool = MySqlPool::connect(&self.server.database.url).await?;
// Run migrations
sqlx::migrate!("./migrations").run(&pool).await?;
log::info!("PostgreSQL database connected");
log::info!("MySQL database connected");
Ok(pool)
}
}

View File

@@ -200,8 +200,10 @@ pub(crate) async fn create_invite(_ctx: &ServerContext, app_state: AppState, cla
let mut tx = app_state.db.begin().await
.map_err(|_| GurtError::invalid_message("Database error"))?;
let affected_rows = sqlx::query("UPDATE users SET registrations_remaining = registrations_remaining - 1 WHERE id = $1 AND registrations_remaining > 0")
.bind(claims.user_id)
let affected_rows = sqlx::query(
"UPDATE users SET registrations_remaining = registrations_remaining - 1 WHERE id = ? AND registrations_remaining > 0"
)
.bind(claims.user_id)
.execute(&mut *tx)
.await
.map_err(|_| GurtError::invalid_message("Database error"))?
@@ -214,7 +216,7 @@ pub(crate) async fn create_invite(_ctx: &ServerContext, app_state: AppState, cla
})?);
}
sqlx::query("INSERT INTO invite_codes (code, created_by, created_at) VALUES ($1, $2, $3)")
sqlx::query("INSERT INTO invite_codes (code, created_by, created_at) VALUES (?, ?, ?)")
.bind(&invite_code)
.bind(claims.user_id)
.bind(Utc::now())
@@ -259,16 +261,16 @@ pub(crate) async fn redeem_invite(ctx: &ServerContext, app_state: AppState, clai
let mut tx = app_state.db.begin().await
.map_err(|_| GurtError::invalid_message("Database error"))?;
sqlx::query("UPDATE invite_codes SET used_by = $1, used_at = $2 WHERE id = $3")
.bind(claims.user_id)
.bind(Utc::now())
.bind(invite.id)
sqlx::query("UPDATE invite_codes SET used_by = ?, used_at = ? WHERE id = ?")
.bind(claims.user_id)
.bind(Utc::now())
.bind(invite.id)
.execute(&mut *tx)
.await
.map_err(|_| GurtError::invalid_message("Database error"))?;
sqlx::query("UPDATE users SET registrations_remaining = registrations_remaining + 1 WHERE id = $1")
.bind(claims.user_id)
sqlx::query("UPDATE users SET registrations_remaining = registrations_remaining + 1 WHERE id = ?")
.bind(claims.user_id)
.execute(&mut *tx)
.await
.map_err(|_| GurtError::invalid_message("Database error"))?;
@@ -313,8 +315,10 @@ pub(crate) async fn create_domain_invite(_ctx: &ServerContext, app_state: AppSta
let mut tx = app_state.db.begin().await
.map_err(|_| GurtError::invalid_message("Database error"))?;
let affected_rows = sqlx::query("UPDATE users SET domain_invite_codes = domain_invite_codes - 1 WHERE id = $1 AND domain_invite_codes > 0")
.bind(claims.user_id)
let affected_rows = sqlx::query(
"UPDATE users SET domain_invite_codes = domain_invite_codes - 1 WHERE id = ? AND domain_invite_codes > 0"
)
.bind(claims.user_id)
.execute(&mut *tx)
.await
.map_err(|_| GurtError::invalid_message("Database error"))?
@@ -367,16 +371,16 @@ pub(crate) async fn redeem_domain_invite(ctx: &ServerContext, app_state: AppStat
let mut tx = app_state.db.begin().await
.map_err(|_| GurtError::invalid_message("Database error"))?;
sqlx::query("UPDATE domain_invite_codes SET used_by = $1, used_at = $2 WHERE id = $3")
.bind(claims.user_id)
.bind(Utc::now())
.bind(invite.id)
sqlx::query("UPDATE domain_invite_codes SET used_by = ?, used_at = ? WHERE id = ?")
.bind(claims.user_id)
.bind(Utc::now())
.bind(invite.id)
.execute(&mut *tx)
.await
.map_err(|_| GurtError::invalid_message("Database error"))?;
sqlx::query("UPDATE users SET domain_invite_codes = domain_invite_codes + 1 WHERE id = $1")
.bind(claims.user_id)
sqlx::query("UPDATE users SET domain_invite_codes = domain_invite_codes + 1 WHERE id = ?")
.bind(claims.user_id)
.execute(&mut *tx)
.await
.map_err(|_| GurtError::invalid_message("Database error"))?;

View File

@@ -78,9 +78,9 @@ pub(crate) async fn create_logic(domain: Domain, user_id: i32, app: &AppState) -
}
let existing_count: i64 =
sqlx::query_scalar("SELECT COUNT(*) FROM domains WHERE name = $1 AND tld = $2")
.bind(&domain.name)
.bind(&domain.tld)
sqlx::query_scalar("SELECT COUNT(*) FROM domains WHERE name = ? AND tld = ?")
.bind(&domain.name)
.bind(&domain.tld)
.fetch_one(&app.db)
.await
.map_err(|_| GurtError::invalid_message("Database error"))?;
@@ -89,7 +89,7 @@ pub(crate) async fn create_logic(domain: Domain, user_id: i32, app: &AppState) -
return Err(GurtError::invalid_message("Domain already exists"));
}
let user: (String,) = sqlx::query_as("SELECT username FROM users WHERE id = $1")
let user: (String,) = sqlx::query_as("SELECT username FROM users WHERE id = ?")
.bind(user_id)
.fetch_one(&app.db)
.await
@@ -98,7 +98,7 @@ pub(crate) async fn create_logic(domain: Domain, user_id: i32, app: &AppState) -
let username = user.0;
let domain_row: (i32,) = sqlx::query_as(
"INSERT INTO domains (name, tld, user_id, status) VALUES ($1, $2, $3, 'pending') RETURNING id"
"INSERT INTO domains (name, tld, user_id, status) VALUES (?, ?, ?, 'pending') RETURNING id"
)
.bind(&domain.name)
.bind(&domain.tld)
@@ -110,7 +110,7 @@ pub(crate) async fn create_logic(domain: Domain, user_id: i32, app: &AppState) -
let domain_id = domain_row.0;
let affected_rows = sqlx::query(
"UPDATE users SET registrations_remaining = registrations_remaining - 1 WHERE id = $1 AND registrations_remaining > 0",
"UPDATE users SET registrations_remaining = registrations_remaining - 1 WHERE id = ? AND registrations_remaining > 0",
)
.bind(user_id)
.execute(&app.db)
@@ -119,8 +119,8 @@ pub(crate) async fn create_logic(domain: Domain, user_id: i32, app: &AppState) -
.rows_affected();
if affected_rows == 0 {
sqlx::query("DELETE FROM domains WHERE id = $1")
.bind(domain_id)
sqlx::query("DELETE FROM domains WHERE id = ?")
.bind(domain_id)
.execute(&app.db)
.await
.map_err(|_| GurtError::invalid_message("Database cleanup error"))?;
@@ -321,7 +321,7 @@ pub(crate) async fn delete_domain(
return Ok(GurtResponse::not_found().with_string_body("Domain not found or access denied"));
}
sqlx::query("DELETE FROM domains WHERE name = $1 AND tld = $2 AND user_id = $3")
sqlx::query("DELETE FROM domains WHERE name = ? AND tld = ? AND user_id = ?")
.bind(name)
.bind(tld)
.bind(claims.user_id)
@@ -620,7 +620,7 @@ pub(crate) async fn delete_domain_record(
}
};
let rows_affected = sqlx::query("DELETE FROM dns_records WHERE id = $1 AND domain_id = $2")
let rows_affected = sqlx::query("DELETE FROM dns_records WHERE id = ? AND domain_id = ?")
.bind(record_id)
.bind(domain.id.unwrap())
.execute(&app_state.db)
@@ -1066,7 +1066,7 @@ pub(crate) async fn get_certificate(
});
// Delete the challenge as it's completed
sqlx::query("DELETE FROM certificate_challenges WHERE token = $1")
sqlx::query("DELETE FROM certificate_challenges WHERE token = ?")
.bind(token)
.execute(&app_state.db)
.await