const crypto = require('crypto'); const dns = require('dns').promises; const { ethers } = require('ethers'); const db = require('../database/db'); /** * Generates a unique verification token for domain ownership * @param {string} userId - User's UUID * @param {string} domain - Domain to verify (e.g., dev.aethex.dev) * @returns {Promise} Verification instructions */ async function generateDomainVerificationToken(userId, domain) { // Generate random 32-byte token const token = crypto.randomBytes(32).toString('hex'); // Store in database await db.query( `INSERT INTO domain_verifications (user_id, domain, verification_token, verification_type) VALUES ($1, $2, $3, 'dns') ON CONFLICT (user_id, domain) DO UPDATE SET verification_token = $3, created_at = NOW(), expires_at = NOW() + INTERVAL '7 days', verified = false`, [userId, domain, token] ); return { domain: domain, recordType: 'TXT', recordName: '_aethex-verify', recordValue: `aethex-verification=${token}`, fullRecord: `_aethex-verify.${domain} TXT "aethex-verification=${token}"`, expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString() }; } /** * Verifies domain ownership by checking DNS TXT records * @param {string} userId - User's UUID * @param {string} domain - Domain to verify * @returns {Promise} Verification result */ async function verifyDomainOwnership(userId, domain) { try { // Get stored verification token const result = await db.query( 'SELECT verification_token, expires_at FROM domain_verifications WHERE user_id = $1 AND domain = $2', [userId, domain] ); if (result.rows.length === 0) { return { verified: false, error: 'No verification request found' }; } const { verification_token: expectedToken, expires_at: expiresAt } = result.rows[0]; // Check if verification request has expired if (new Date() > new Date(expiresAt)) { return { verified: false, error: 'Verification request has expired. Please request a new verification.' }; } // Query DNS for TXT records const txtRecords = await dns.resolveTxt(`_aethex-verify.${domain}`); // Flatten nested arrays that DNS returns const flatRecords = txtRecords.flat(); // Check if our token exists const verified = flatRecords.some(record => record.includes(`aethex-verification=${expectedToken}`) ); if (verified) { // Update database await db.query( `UPDATE domain_verifications SET verified = true, verified_at = NOW() WHERE user_id = $1 AND domain = $2`, [userId, domain] ); await db.query( `UPDATE users SET verified_domain = $2, domain_verified_at = NOW() WHERE id = $1`, [userId, domain] ); return { verified: true, domain: domain, verifiedAt: new Date() }; } return { verified: false, error: 'Verification token not found in DNS records. Please ensure the TXT record has been added and DNS has propagated (5-10 minutes).' }; } catch (error) { // DNS lookup failed (domain doesn't exist, no TXT records, etc.) if (error.code === 'ENOTFOUND' || error.code === 'ENODATA') { return { verified: false, error: 'DNS lookup failed: TXT record not found. Please ensure you\'ve added the record and wait 5-10 minutes for DNS propagation.' }; } return { verified: false, error: `DNS lookup failed: ${error.message}` }; } } /** * Verifies .aethex domain ownership via blockchain * @param {string} userId - User's UUID * @param {string} domain - Domain to verify (e.g., anderson.aethex) * @param {string} walletAddress - User's connected wallet address * @returns {Promise} Verification result */ async function verifyAethexDomain(userId, domain, walletAddress) { try { // Connect to blockchain (Polygon/Ethereum) const provider = new ethers.JsonRpcProvider(process.env.RPC_ENDPOINT); // Freename contract ABI (simplified - you'll need the actual ABI) // This is a minimal ERC721-like interface const FREENAME_ABI = [ "function ownerOf(string memory domain) view returns (address)", "function getDomainOwner(string memory domain) view returns (address)" ]; const contract = new ethers.Contract( process.env.FREENAME_REGISTRY_ADDRESS, FREENAME_ABI, provider ); // Query who owns this .aethex domain // Try both common method names let owner; try { owner = await contract.ownerOf(domain); } catch (e) { owner = await contract.getDomainOwner(domain); } // Check if connected wallet owns the domain const verified = owner.toLowerCase() === walletAddress.toLowerCase(); if (verified) { // Store verification await db.query( `INSERT INTO domain_verifications (user_id, domain, verification_token, verification_type, verified, verified_at) VALUES ($1, $2, $3, 'blockchain', true, NOW()) ON CONFLICT (user_id, domain) DO UPDATE SET verified = true, verified_at = NOW()`, [userId, domain, walletAddress] ); await db.query( `UPDATE users SET verified_domain = $2, domain_verified_at = NOW() WHERE id = $1`, [userId, domain] ); return { verified: true, domain: domain, verifiedAt: new Date() }; } return { verified: false, error: 'Wallet does not own this domain. The domain is owned by a different address.' }; } catch (error) { return { verified: false, error: `Blockchain verification failed: ${error.message}` }; } } /** * Get verification status for a user * @param {string} userId - User's UUID * @returns {Promise} Verification status */ async function getVerificationStatus(userId) { const result = await db.query( `SELECT verified_domain, domain_verified_at FROM users WHERE id = $1`, [userId] ); if (result.rows.length === 0) { return { hasVerifiedDomain: false }; } const { verified_domain, domain_verified_at } = result.rows[0]; if (!verified_domain) { return { hasVerifiedDomain: false }; } // Get verification type const verificationInfo = await db.query( `SELECT verification_type FROM domain_verifications WHERE user_id = $1 AND domain = $2 AND verified = true ORDER BY verified_at DESC LIMIT 1`, [userId, verified_domain] ); return { hasVerifiedDomain: true, domain: verified_domain, verifiedAt: domain_verified_at, verificationType: verificationInfo.rows[0]?.verification_type || 'dns' }; } /** * Clean up expired verification requests * Should be run periodically (e.g., daily cron job) */ async function cleanupExpiredVerifications() { const result = await db.query( `DELETE FROM domain_verifications WHERE verified = false AND expires_at < NOW() RETURNING id` ); console.log(`Cleaned up ${result.rowCount} expired verification requests`); return result.rowCount; } module.exports = { generateDomainVerificationToken, verifyDomainOwnership, verifyAethexDomain, getVerificationStatus, cleanupExpiredVerifications };