Add domain verification feature - ready for integration into api.aethex.cloud

This commit is contained in:
Anderson 2026-01-10 03:30:15 +00:00 committed by GitHub
parent cef6bf1fea
commit 246a4ce5c1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
44 changed files with 12159 additions and 1 deletions

17
.env.example Normal file
View file

@ -0,0 +1,17 @@
# Database Configuration
DATABASE_URL=postgresql://user:password@localhost:5432/aethex_passport
# Server Configuration
PORT=3000
NODE_ENV=development
# Blockchain Configuration (for .aethex domain verification)
RPC_ENDPOINT=https://polygon-mainnet.infura.io/v3/YOUR_INFURA_KEY
FREENAME_REGISTRY_ADDRESS=0x... # Freename contract address
# JWT Secret (for authentication)
JWT_SECRET=your-secret-key-here
# Rate Limiting
RATE_LIMIT_WINDOW_MS=900000
RATE_LIMIT_MAX_REQUESTS=100

32
.env.supabase Normal file
View file

@ -0,0 +1,32 @@
# Supabase Configuration
# Get these values from your Supabase project settings: https://supabase.com/dashboard/project/_/settings/api
# Supabase Project URL
SUPABASE_URL=https://your-project-ref.supabase.co
# Supabase Anonymous Key (public)
SUPABASE_ANON_KEY=your-anon-key-here
# Supabase Service Role Key (secret - for backend only)
SUPABASE_SERVICE_ROLE_KEY=your-service-role-key-here
# Database Connection String (from Supabase Settings > Database)
DATABASE_URL=postgresql://postgres:[YOUR-PASSWORD]@db.your-project-ref.supabase.co:5432/postgres
# Server Configuration
PORT=3000
NODE_ENV=development
# JWT Secret (for custom auth)
JWT_SECRET=your-secret-key-here
# Blockchain Configuration (for .aethex domain verification)
RPC_ENDPOINT=https://polygon-mainnet.infura.io/v3/YOUR_INFURA_KEY
FREENAME_REGISTRY_ADDRESS=0x...
# Frontend URL (for CORS)
FRONTEND_URL=http://localhost:5173
# Rate Limiting
RATE_LIMIT_WINDOW_MS=900000
RATE_LIMIT_MAX_REQUESTS=100

36
.gitignore vendored Normal file
View file

@ -0,0 +1,36 @@
# Dependencies
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Environment variables
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
# Database
*.db
*.sqlite
# Build outputs
dist/
build/
*.tgz
# IDE
.vscode/
.idea/
*.swp
*.swo
*~
# OS
.DS_Store
Thumbs.db
# Logs
logs/
*.log

181
INTEGRATION.md Normal file
View file

@ -0,0 +1,181 @@
# Integration Guide: Adding Domain Verification to api.aethex.cloud
## Overview
This guide shows how to integrate the domain verification feature into your existing api.aethex.cloud backend.
## Files to Copy to Your API Backend
### 1. Core Utilities
Copy these to your api.aethex.cloud project:
```
📁 Your API Project/
├── utils/
│ └── domainVerification.js ← Copy from src/backend/utils/
└── middleware/
└── auth.js ← Update if needed (dev mode auth)
```
### 2. Routes
Copy and integrate:
```javascript
// In your api.aethex.cloud routes/index.js or app.js
const domainRoutes = require('./routes/domainRoutes');
// Mount the domain verification routes
app.use('/api/passport/domain', domainRoutes);
```
### 3. Database Migration
Run this SQL on your Supabase database:
```bash
# Option 1: Via Supabase Dashboard
# Go to https://supabase.com/dashboard/project/kmdeisowhtsalsekkzqd/editor
# Run the SQL from: src/backend/database/migrations/001_domain_verifications.sql
# Option 2: Via CLI (if you're in your API project with supabase linked)
supabase db push
```
## Integration Steps
### Step 1: Copy Files to Your API Project
```bash
# In your api.aethex.cloud project
mkdir -p utils routes
# Copy domain verification utilities
cp /path/to/AeThex-Connect/src/backend/utils/domainVerification.js utils/
# Copy domain routes
cp /path/to/AeThex-Connect/src/backend/routes/domainRoutes.js routes/
```
### Step 2: Install Dependencies (if not already installed)
```bash
npm install ethers --save
# (pg, crypto, dns are Node.js built-ins)
```
### Step 3: Add Routes to Your Express App
In your `app.js` or `server.js`:
```javascript
// Import the domain routes
const domainRoutes = require('./routes/domainRoutes');
// Mount at the correct path
app.use('/api/passport/domain', domainRoutes);
```
### Step 4: Environment Variables
Add to your api.aethex.cloud `.env`:
```env
# Blockchain Configuration (for .aethex domain verification)
RPC_ENDPOINT=https://polygon-mainnet.g.alchemy.com/v2/3-qjAZSq7DyEuJQKH3KPm
FREENAME_REGISTRY_ADDRESS=0x... # Add the actual contract address
```
### Step 5: Database Schema
The tables are already created on your Supabase database:
- ✅ `domain_verifications`
- ✅ `users` (extended with `verified_domain` and `domain_verified_at`)
### Step 6: Test the Integration
```bash
# Test the API endpoint
curl https://api.aethex.cloud/api/passport/domain/status \
-H "Authorization: Bearer YOUR_TOKEN"
# Expected: {"hasVerifiedDomain": false} or your verification status
```
## Frontend Integration
### Update Your aethex.tech Frontend
The frontend is already configured to call `api.aethex.cloud`:
```javascript
import DomainVerification from './components/DomainVerification';
// In your profile/settings page:
<DomainVerification />
// It will automatically call https://api.aethex.cloud/api/passport/domain/*
```
### Add the Components
Copy these to your aethex.tech frontend:
```
📁 Your Frontend (aethex.tech)/
├── components/
│ ├── DomainVerification.jsx
│ ├── DomainVerification.css
│ ├── VerifiedDomainBadge.jsx
│ └── VerifiedDomainBadge.css
```
## API Endpoints Added
Once integrated, these endpoints will be available at api.aethex.cloud:
| Method | Endpoint | Description |
|--------|----------|-------------|
| POST | `/api/passport/domain/request-verification` | Generate verification token |
| POST | `/api/passport/domain/verify` | Verify domain ownership |
| GET | `/api/passport/domain/status` | Get user's verification status |
## Authentication
The routes use your existing auth middleware. Make sure:
- JWT tokens are passed in `Authorization: Bearer <token>` header
- Middleware extracts `req.user.id` and `req.user.email`
If your auth is different, update `domainRoutes.js` accordingly.
## Quick Copy-Paste for Your API
### In your api.aethex.cloud app.js:
```javascript
// Add this line with your other route imports
const domainRoutes = require('./routes/domainRoutes');
// Add this line with your other route mounts
app.use('/api/passport/domain', domainRoutes);
```
### In your api.aethex.cloud database setup:
```sql
-- Run in Supabase SQL Editor
-- This creates the domain verification tables
-- (Already done if you ran the migration earlier)
```
## Testing Checklist
- [ ] Copy `domainVerification.js` to your API utils
- [ ] Copy `domainRoutes.js` to your API routes
- [ ] Add route mount in your Express app
- [ ] Verify database tables exist in Supabase
- [ ] Test endpoint: `GET /api/passport/domain/status`
- [ ] Copy frontend components to aethex.tech
- [ ] Test full flow: request → add DNS → verify
## Need Help?
The current standalone server at `localhost:3000` is just for testing. Once integrated into api.aethex.cloud, you can remove this standalone server.
All the backend code is modular and ready to plug into your existing API!

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021 Supabase, Inc. and contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

178
README.md
View file

@ -1 +1,177 @@
# AeThex-Connect
# Supabase CLI
[![Coverage Status](https://coveralls.io/repos/github/supabase/cli/badge.svg?branch=main)](https://coveralls.io/github/supabase/cli?branch=main) [![Bitbucket Pipelines](https://img.shields.io/bitbucket/pipelines/supabase-cli/setup-cli/master?style=flat-square&label=Bitbucket%20Canary)](https://bitbucket.org/supabase-cli/setup-cli/pipelines) [![Gitlab Pipeline Status](https://img.shields.io/gitlab/pipeline-status/sweatybridge%2Fsetup-cli?label=Gitlab%20Canary)
](https://gitlab.com/sweatybridge/setup-cli/-/pipelines)
[Supabase](https://supabase.io) is an open source Firebase alternative. We're building the features of Firebase using enterprise-grade open source tools.
This repository contains all the functionality for Supabase CLI.
- [x] Running Supabase locally
- [x] Managing database migrations
- [x] Creating and deploying Supabase Functions
- [x] Generating types directly from your database schema
- [x] Making authenticated HTTP requests to [Management API](https://supabase.com/docs/reference/api/introduction)
## Getting started
### Install the CLI
Available via [NPM](https://www.npmjs.com) as dev dependency. To install:
```bash
npm i supabase --save-dev
```
When installing with yarn 4, you need to disable experimental fetch with the following nodejs config.
```
NODE_OPTIONS=--no-experimental-fetch yarn add supabase
```
> **Note**
For Bun versions below v1.0.17, you must add `supabase` as a [trusted dependency](https://bun.sh/guides/install/trusted) before running `bun add -D supabase`.
<details>
<summary><b>macOS</b></summary>
Available via [Homebrew](https://brew.sh). To install:
```sh
brew install supabase/tap/supabase
```
To install the beta release channel:
```sh
brew install supabase/tap/supabase-beta
brew link --overwrite supabase-beta
```
To upgrade:
```sh
brew upgrade supabase
```
</details>
<details>
<summary><b>Windows</b></summary>
Available via [Scoop](https://scoop.sh). To install:
```powershell
scoop bucket add supabase https://github.com/supabase/scoop-bucket.git
scoop install supabase
```
To upgrade:
```powershell
scoop update supabase
```
</details>
<details>
<summary><b>Linux</b></summary>
Available via [Homebrew](https://brew.sh) and Linux packages.
#### via Homebrew
To install:
```sh
brew install supabase/tap/supabase
```
To upgrade:
```sh
brew upgrade supabase
```
#### via Linux packages
Linux packages are provided in [Releases](https://github.com/supabase/cli/releases). To install, download the `.apk`/`.deb`/`.rpm`/`.pkg.tar.zst` file depending on your package manager and run the respective commands.
```sh
sudo apk add --allow-untrusted <...>.apk
```
```sh
sudo dpkg -i <...>.deb
```
```sh
sudo rpm -i <...>.rpm
```
```sh
sudo pacman -U <...>.pkg.tar.zst
```
</details>
<details>
<summary><b>Other Platforms</b></summary>
You can also install the CLI via [go modules](https://go.dev/ref/mod#go-install) without the help of package managers.
```sh
go install github.com/supabase/cli@latest
```
Add a symlink to the binary in `$PATH` for easier access:
```sh
ln -s "$(go env GOPATH)/bin/cli" /usr/bin/supabase
```
This works on other non-standard Linux distros.
</details>
<details>
<summary><b>Community Maintained Packages</b></summary>
Available via [pkgx](https://pkgx.sh/). Package script [here](https://github.com/pkgxdev/pantry/blob/main/projects/supabase.com/cli/package.yml).
To install in your working directory:
```bash
pkgx install supabase
```
Available via [Nixpkgs](https://nixos.org/). Package script [here](https://github.com/NixOS/nixpkgs/blob/master/pkgs/development/tools/supabase-cli/default.nix).
</details>
### Run the CLI
```bash
supabase bootstrap
```
Or using npx:
```bash
npx supabase bootstrap
```
The bootstrap command will guide you through the process of setting up a Supabase project using one of the [starter](https://github.com/supabase-community/supabase-samples/blob/main/samples.json) templates.
## Docs
Command & config reference can be found [here](https://supabase.com/docs/reference/cli/about).
## Breaking changes
We follow semantic versioning for changes that directly impact CLI commands, flags, and configurations.
However, due to dependencies on other service images, we cannot guarantee that schema migrations, seed.sql, and generated types will always work for the same CLI major version. If you need such guarantees, we encourage you to pin a specific version of CLI in package.json.
## Developing
To run from source:
```sh
# Go >= 1.22
go run . help
```

333
SETUP.md Normal file
View file

@ -0,0 +1,333 @@
# AeThex Passport Domain Verification - Setup Guide
## Quick Start (5 Minutes)
### 1. Install Dependencies
```bash
# Root dependencies (backend)
npm install
# Frontend dependencies
cd src/frontend && npm install && cd ../..
```
### 2. Setup PostgreSQL Database
#### Option A: Local PostgreSQL
```bash
# Install PostgreSQL (Ubuntu/Debian)
sudo apt update
sudo apt install postgresql postgresql-contrib
# Start PostgreSQL
sudo systemctl start postgresql
sudo systemctl enable postgresql
# Create database and user
sudo -u postgres psql
```
In PostgreSQL shell:
```sql
CREATE DATABASE aethex_passport;
CREATE USER aethex_user WITH PASSWORD 'your_password';
GRANT ALL PRIVILEGES ON DATABASE aethex_passport TO aethex_user;
\q
```
#### Option B: Docker PostgreSQL
```bash
docker run --name aethex-postgres \
-e POSTGRES_DB=aethex_passport \
-e POSTGRES_USER=aethex_user \
-e POSTGRES_PASSWORD=your_password \
-p 5432:5432 \
-d postgres:14
```
### 3. Configure Environment
```bash
# Copy environment template
cp .env.example .env
# Edit configuration
nano .env
```
Update these values in `.env`:
```env
DATABASE_URL=postgresql://aethex_user:your_password@localhost:5432/aethex_passport
PORT=3000
JWT_SECRET=your-super-secret-key-change-this
NODE_ENV=development
```
### 4. Run Database Migrations
```bash
npm run migrate
```
You should see:
```
Starting database migrations...
Running migration: 001_domain_verifications.sql
✓ Completed: 001_domain_verifications.sql
✓ All migrations completed successfully
```
### 5. Start Development Servers
Open two terminal windows:
**Terminal 1 - Backend:**
```bash
npm run dev
```
**Terminal 2 - Frontend:**
```bash
cd src/frontend && npm run dev
```
### 6. Access the Application
- **Frontend**: http://localhost:5173
- **Backend API**: http://localhost:3000
- **Health Check**: http://localhost:3000/health
## Testing Domain Verification
### Test with Your Own Domain
1. **Request Verification**:
- Go to http://localhost:5173
- Enter your domain (e.g., `dev.aethex.dev`)
- Click "Request Verification"
2. **Add DNS Record**:
- Copy the TXT record value shown
- Go to your DNS provider (Cloudflare, Google Domains, etc.)
- Add new TXT record:
- **Name**: `_aethex-verify`
- **Type**: `TXT`
- **Value**: `aethex-verification=...` (the copied value)
3. **Verify**:
- Wait 5-10 minutes for DNS to propagate
- Click "Verify Domain" in the app
- Success! Your domain badge appears
### Check DNS Propagation
```bash
# Check if DNS record is live
dig _aethex-verify.yourdomain.com TXT +short
# Alternative method
nslookup -type=TXT _aethex-verify.yourdomain.com
```
Expected output:
```
"aethex-verification=abc123def456..."
```
## Common Issues & Solutions
### Issue: "Connection refused" to database
**Solution 1**: Check PostgreSQL is running
```bash
sudo systemctl status postgresql
```
**Solution 2**: Verify connection string in `.env`
```bash
# Test connection manually
psql postgresql://aethex_user:your_password@localhost:5432/aethex_passport
```
### Issue: "Port 3000 already in use"
**Solution**: Find and kill the process
```bash
# Find process using port 3000
lsof -i :3000
# Kill it
kill -9 <PID>
```
### Issue: DNS verification always fails
**Checklist**:
1. ✓ DNS record added correctly?
2. ✓ Waited 10+ minutes?
3. ✓ Record name is `_aethex-verify` (not `_aethex-verify.yourdomain.com`)?
4. ✓ Record value matches exactly (no extra quotes)?
**Debug DNS**:
```bash
# Check what DNS server sees
dig _aethex-verify.yourdomain.com TXT @8.8.8.8
# Flush local DNS cache
sudo systemd-resolve --flush-caches
```
## Environment Variables Reference
| Variable | Required | Default | Description |
|----------|----------|---------|-------------|
| `DATABASE_URL` | ✅ Yes | - | PostgreSQL connection string |
| `PORT` | No | 3000 | Backend server port |
| `NODE_ENV` | No | development | Environment mode |
| `JWT_SECRET` | ✅ Yes | - | Secret for JWT tokens |
| `RPC_ENDPOINT` | For .aethex | - | Blockchain RPC URL |
| `FREENAME_REGISTRY_ADDRESS` | For .aethex | - | Contract address |
| `FRONTEND_URL` | No | http://localhost:5173 | CORS origin |
| `RATE_LIMIT_WINDOW_MS` | No | 900000 | Rate limit window (15 min) |
| `RATE_LIMIT_MAX_REQUESTS` | No | 100 | Max requests per window |
## Production Deployment
### 1. Build Frontend
```bash
cd src/frontend
npm run build
cd ../..
```
### 2. Environment Configuration
```bash
# Create production .env
cp .env.example .env.production
# Edit with production values
nano .env.production
```
Set:
```env
NODE_ENV=production
DATABASE_URL=postgresql://user:pass@production-host:5432/aethex_passport
JWT_SECRET=<strong-random-secret>
FRONTEND_URL=https://yourdomain.com
```
### 3. Start Production Server
```bash
NODE_ENV=production node src/backend/server.js
```
### 4. Serve Frontend
Use nginx or serve built files:
```bash
# Using serve
npm install -g serve
serve -s src/frontend/dist -l 5173
```
Or configure nginx:
```nginx
server {
listen 80;
server_name yourdomain.com;
location / {
root /path/to/src/frontend/dist;
try_files $uri /index.html;
}
location /api {
proxy_pass http://localhost:3000;
}
}
```
## Database Backup
### Manual Backup
```bash
# Backup database
pg_dump -U aethex_user aethex_passport > backup_$(date +%Y%m%d).sql
# Restore from backup
psql -U aethex_user aethex_passport < backup_20260109.sql
```
### Automated Daily Backups
Create `/etc/cron.daily/aethex-backup`:
```bash
#!/bin/bash
pg_dump -U aethex_user aethex_passport | gzip > /backups/aethex_$(date +%Y%m%d).sql.gz
find /backups -name "aethex_*.sql.gz" -mtime +30 -delete
```
Make executable:
```bash
chmod +x /etc/cron.daily/aethex-backup
```
## Monitoring
### Health Check Endpoint
```bash
# Check if server is running
curl http://localhost:3000/health
```
Response:
```json
{"status":"ok","timestamp":"2026-01-09T12:00:00.000Z"}
```
### Database Connection Test
```bash
# Test query
psql -U aethex_user -d aethex_passport -c "SELECT COUNT(*) FROM domain_verifications;"
```
### Log Files
Backend logs to stdout. Capture with:
```bash
# Development
npm run dev 2>&1 | tee logs/app.log
# Production (with pm2)
npm install -g pm2
pm2 start src/backend/server.js --name aethex-passport
pm2 logs aethex-passport
```
## Next Steps
1. ✅ Setup complete? Test verification flow
2. 📱 Integrate into your main application
3. 🔐 Set up proper JWT authentication
4. 📊 Add analytics and monitoring
5. 🚀 Deploy to production
## Need Help?
- 📖 [Full Documentation](README.md)
- 🐛 [Report Issues](https://github.com/AeThex-Corporation/AeThex-Connect/issues)
- 💬 Email: support@aethex.dev
---
**Setup Guide v1.0** | Last Updated: January 10, 2026

242
SUPABASE.md Normal file
View file

@ -0,0 +1,242 @@
# Supabase Integration Guide
## ✅ Setup Complete
You've successfully:
- ✅ Logged into Supabase CLI
- ✅ Initialized Supabase in your project
- ✅ Installed Supabase JavaScript client
- ✅ Created migration files
## 🚀 Next Steps
### 1. Link to Your Supabase Project
If you have an existing project:
```bash
supabase link --project-ref your-project-ref
```
Or create a new project:
```bash
supabase projects create your-project-name
```
### 2. Configure Environment Variables
Get your Supabase credentials from: https://supabase.com/dashboard/project/_/settings/api
Then update `.env.supabase`:
```bash
cp .env.supabase .env
# Edit with your values:
SUPABASE_URL=https://your-project-ref.supabase.co
SUPABASE_ANON_KEY=your-anon-key
SUPABASE_SERVICE_ROLE_KEY=your-service-role-key
DATABASE_URL=postgresql://postgres:[PASSWORD]@db.your-project-ref.supabase.co:5432/postgres
```
### 3. Push Database Migrations
```bash
# Push your domain verification schema to Supabase
supabase db push
```
Or run migrations manually:
```bash
# Apply all migrations
supabase db push
# Or connect and run SQL directly
supabase db reset
```
### 4. Start Local Development
**Option A: Use Remote Supabase Database**
```bash
# Backend will connect to your Supabase project
npm run dev
```
**Option B: Local Supabase (Docker required)**
```bash
# Start local Supabase stack
supabase start
# This gives you:
# - Local PostgreSQL database
# - Local Auth server
# - Local API
# - Studio UI at http://localhost:54323
# Then start your backend
npm run dev
```
### 5. Frontend Configuration
Update your frontend to use Supabase auth (optional):
```javascript
// src/frontend/utils/supabase.js
import { createClient } from '@supabase/supabase-js'
const supabase = createClient(
process.env.VITE_SUPABASE_URL,
process.env.VITE_SUPABASE_ANON_KEY
)
export { supabase }
```
## 📋 Available Commands
```bash
# Project Management
supabase projects list # List all projects
supabase link # Link to a project
supabase status # Check connection status
# Database
supabase db push # Push migrations to remote
supabase db reset # Reset local database
supabase db diff # Show schema differences
supabase migration new <name> # Create new migration
# Local Development
supabase start # Start local Supabase
supabase stop # Stop local Supabase
supabase status # Check local services
# Functions (for future use)
supabase functions new <name> # Create Edge Function
supabase functions deploy <name> # Deploy Edge Function
```
## 🔄 Database Schema
Your domain verification tables are ready to deploy:
```
supabase/migrations/
└── 20260110025404_domain_verifications.sql
```
This includes:
- `domain_verifications` table
- `users` table extensions
- Indexes for performance
## 🎯 Integration Options
### Option 1: Use Supabase Auth (Recommended)
Replace custom JWT with Supabase Auth:
- Built-in authentication
- User management UI
- Social auth providers
- Row Level Security (RLS)
### Option 2: Keep Custom Auth
Continue using your custom JWT auth and just use Supabase as the database.
## 🔒 Security: Row Level Security (RLS)
Add RLS policies to your tables for extra security:
```sql
-- Enable RLS on domain_verifications
ALTER TABLE domain_verifications ENABLE ROW LEVEL SECURITY;
-- Users can only see their own verifications
CREATE POLICY "Users can view own verifications"
ON domain_verifications FOR SELECT
USING (auth.uid()::text = user_id);
-- Users can only insert their own verifications
CREATE POLICY "Users can insert own verifications"
ON domain_verifications FOR INSERT
WITH CHECK (auth.uid()::text = user_id);
```
## 📊 Database Access
### Via Supabase Studio
https://supabase.com/dashboard/project/_/editor
### Via CLI
```bash
supabase db remote
```
### Via Direct Connection
Use the DATABASE_URL from your `.env` file with any PostgreSQL client.
## 🧪 Testing
Test your Supabase connection:
```bash
# Check if you can connect
supabase db remote exec "SELECT version();"
# List tables
supabase db remote exec "SELECT tablename FROM pg_tables WHERE schemaname = 'public';"
```
## 📝 Migration Workflow
1. Make schema changes locally
2. Generate migration:
```bash
supabase db diff -f new_migration_name
```
3. Review the generated SQL
4. Push to production:
```bash
supabase db push
```
## 🚨 Troubleshooting
**Connection Failed**
```bash
# Check if linked
supabase status
# Relink if needed
supabase link --project-ref your-project-ref
```
**Migration Errors**
```bash
# View migration status
supabase migration list
# Reset if needed (⚠️ destructive)
supabase db reset
```
**Local Development Issues**
```bash
# Check Docker is running
docker ps
# Restart Supabase
supabase stop
supabase start
```
## 🔗 Useful Links
- **Dashboard**: https://supabase.com/dashboard
- **Docs**: https://supabase.com/docs
- **CLI Reference**: https://supabase.com/docs/reference/cli
- **API Docs**: https://supabase.com/docs/reference/javascript
---
**Ready to deploy?** Run `supabase db push` to apply your schema! 🚀

View file

@ -0,0 +1,171 @@
# Integration Instructions for api.aethex.cloud
## Quick Start
This package contains everything you need to add domain verification to api.aethex.cloud.
## Files Included
```
integration-package/
├── routes/
│ └── domainRoutes.js ← API routes
├── utils/
│ └── domainVerification.js ← Core logic
├── migrations/
│ └── 001_domain_verifications.sql ← Database schema
└── frontend/
└── components/ ← React components
├── DomainVerification.jsx
├── DomainVerification.css
├── VerifiedDomainBadge.jsx
└── VerifiedDomainBadge.css
```
## Step 1: Backend Integration (api.aethex.cloud)
### 1.1 Copy Backend Files
```bash
# In your api.aethex.cloud project root:
cp integration-package/routes/domainRoutes.js ./routes/
cp integration-package/utils/domainVerification.js ./utils/
```
### 1.2 Add Routes to Your Express App
In your `app.js`, `server.js`, or main API file:
```javascript
// Add this import with your other routes
const domainRoutes = require('./routes/domainRoutes');
// Add this route mount (after your other middleware)
app.use('/api/passport/domain', domainRoutes);
```
### 1.3 Install Dependencies (if needed)
```bash
npm install ethers --save
```
### 1.4 Add Environment Variables
Add to your `.env`:
```env
# Blockchain Configuration (for .aethex domain verification)
RPC_ENDPOINT=https://polygon-mainnet.g.alchemy.com/v2/3-qjAZSq7DyEuJQKH3KPm
FREENAME_REGISTRY_ADDRESS=0x... # Add actual contract address when available
```
### 1.5 Run Database Migration
The schema is already on your Supabase database! If you need to run it again:
```bash
# Via Supabase Dashboard:
# Go to: https://supabase.com/dashboard/project/kmdeisowhtsalsekkzqd/editor
# Copy and run SQL from: integration-package/migrations/001_domain_verifications.sql
```
### 1.6 Test the API
```bash
# Test the endpoint
curl https://api.aethex.cloud/api/passport/domain/status \
-H "Authorization: Bearer YOUR_JWT_TOKEN"
# Expected response:
# {"hasVerifiedDomain": false}
```
## Step 2: Frontend Integration (aethex.tech)
### 2.1 Copy Frontend Components
```bash
# In your aethex.tech frontend project:
cp -r integration-package/frontend/components/* ./src/components/
```
### 2.2 Add to Your Profile/Settings Page
```javascript
import DomainVerification from './components/DomainVerification';
import VerifiedDomainBadge from './components/VerifiedDomainBadge';
// In your profile settings:
<DomainVerification />
// To display verified domain on profile:
<VerifiedDomainBadge
verifiedDomain={user.verified_domain}
verifiedAt={user.domain_verified_at}
/>
```
## Step 3: Verify Everything Works
### Backend Check
```bash
# Should return 200
curl https://api.aethex.cloud/health
# Should return user's verification status
curl https://api.aethex.cloud/api/passport/domain/status \
-H "Authorization: Bearer YOUR_TOKEN"
```
### Frontend Check
1. Go to your profile page on aethex.tech
2. You should see "Verify Your Domain" section
3. Enter a domain you own
4. Follow DNS verification flow
## API Endpoints Added
| Method | Path | Description |
|--------|------|-------------|
| POST | `/api/passport/domain/request-verification` | Generate verification token |
| POST | `/api/passport/domain/verify` | Verify domain ownership (DNS or blockchain) |
| GET | `/api/passport/domain/status` | Get user's current verification status |
## Authentication
These routes use your existing authentication middleware. They expect:
- `Authorization: Bearer <JWT_TOKEN>` header
- Middleware that sets `req.user.id` and `req.user.email`
If your auth middleware works differently, update `domainRoutes.js` line 4.
## Database Schema
Tables created:
- `domain_verifications` - Stores verification requests
- `users` table extended with:
- `verified_domain` (VARCHAR)
- `domain_verified_at` (TIMESTAMP)
## Troubleshooting
### "Module not found" errors
- Make sure you copied files to the correct directories
- Check your require paths match your project structure
### "Database connection error"
- Verify DATABASE_URL in your api.aethex.cloud .env
- Should be the same Supabase connection string
### "401 Unauthorized"
- Check your auth middleware is running before domain routes
- Verify JWT tokens are being passed correctly
## Support
Questions? Check the main README.md or open an issue.
---
**Ready to integrate?** Start with Step 1.1! 🚀

View file

@ -0,0 +1,316 @@
.domain-verification {
max-width: 600px;
margin: 0 auto;
padding: 24px;
background: #ffffff;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.domain-verification h3 {
margin: 0 0 8px 0;
color: #1a1a1a;
font-size: 24px;
font-weight: 600;
}
.domain-verification .description {
margin: 0 0 24px 0;
color: #666;
font-size: 14px;
}
/* Error message */
.error-message {
padding: 12px;
margin-bottom: 16px;
background: #fee;
border: 1px solid #fcc;
border-radius: 6px;
color: #c33;
}
/* Input section */
.input-section {
display: flex;
flex-direction: column;
gap: 16px;
}
.form-group {
display: flex;
flex-direction: column;
gap: 8px;
}
.form-group label {
font-weight: 500;
color: #333;
font-size: 14px;
}
.domain-input,
.wallet-input {
padding: 12px;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 16px;
transition: border-color 0.2s;
}
.domain-input:focus,
.wallet-input:focus {
outline: none;
border-color: #4f46e5;
}
.domain-input:disabled,
.wallet-input:disabled {
background: #f5f5f5;
cursor: not-allowed;
}
.help-text {
font-size: 12px;
color: #666;
margin-top: -4px;
}
/* Buttons */
.primary-button,
.secondary-button {
padding: 12px 24px;
border: none;
border-radius: 8px;
font-size: 16px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
.primary-button {
background: #4f46e5;
color: white;
}
.primary-button:hover:not(:disabled) {
background: #4338ca;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(79, 70, 229, 0.3);
}
.primary-button:disabled {
background: #9ca3af;
cursor: not-allowed;
transform: none;
}
.secondary-button {
background: #f3f4f6;
color: #374151;
}
.secondary-button:hover:not(:disabled) {
background: #e5e7eb;
}
.cancel-button {
margin-top: 12px;
}
/* Instructions section */
.instructions-section {
display: flex;
flex-direction: column;
gap: 20px;
}
.instructions-section h4 {
margin: 0;
color: #1a1a1a;
font-size: 18px;
}
/* DNS record display */
.dns-record {
background: #f9fafb;
border: 1px solid #e5e7eb;
border-radius: 8px;
padding: 16px;
display: flex;
flex-direction: column;
gap: 12px;
}
.record-field {
display: flex;
flex-direction: column;
gap: 4px;
}
.record-field strong {
font-size: 12px;
text-transform: uppercase;
color: #6b7280;
letter-spacing: 0.5px;
}
.record-field span {
font-size: 14px;
color: #1f2937;
}
.value-container {
display: flex;
gap: 8px;
align-items: center;
}
.value-container code {
flex: 1;
padding: 8px;
background: #fff;
border: 1px solid #e5e7eb;
border-radius: 4px;
font-size: 13px;
word-break: break-all;
font-family: 'Courier New', monospace;
}
.copy-button {
padding: 8px 12px;
background: #fff;
border: 1px solid #e5e7eb;
border-radius: 4px;
font-size: 12px;
cursor: pointer;
white-space: nowrap;
transition: all 0.2s;
}
.copy-button:hover {
background: #f3f4f6;
border-color: #d1d5db;
}
/* Help section */
.help-section {
background: #eff6ff;
border: 1px solid #bfdbfe;
border-radius: 8px;
padding: 16px;
}
.help-section p {
margin: 0 0 8px 0;
color: #1e40af;
font-size: 14px;
}
.help-section ol {
margin: 8px 0;
padding-left: 24px;
color: #1e3a8a;
}
.help-section li {
margin: 4px 0;
font-size: 14px;
}
.expires-note {
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid #bfdbfe;
font-size: 13px;
color: #1e40af;
}
/* Status message */
.status-message {
padding: 12px;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
}
.status-message.success {
background: #d1fae5;
border: 1px solid #6ee7b7;
color: #065f46;
}
.status-message.error {
background: #fee2e2;
border: 1px solid #fca5a5;
color: #991b1b;
}
/* Blockchain verification */
.blockchain-verification {
display: flex;
flex-direction: column;
gap: 16px;
}
.blockchain-verification p {
margin: 0;
color: #374151;
}
/* Verified container */
.verified-container {
text-align: center;
}
.verified-container h3 {
color: #059669;
font-size: 28px;
}
.verified-domain-display {
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
margin: 20px 0;
padding: 16px;
background: #d1fae5;
border-radius: 8px;
}
.verified-domain-display strong {
font-size: 20px;
color: #065f46;
}
.verification-type {
padding: 4px 8px;
background: #059669;
color: white;
border-radius: 4px;
font-size: 12px;
text-transform: uppercase;
}
.verified-date {
color: #6b7280;
font-size: 14px;
margin-bottom: 20px;
}
/* Responsive */
@media (max-width: 640px) {
.domain-verification {
padding: 16px;
}
.value-container {
flex-direction: column;
align-items: stretch;
}
.copy-button {
width: 100%;
}
}

View file

@ -0,0 +1,313 @@
import React, { useState, useEffect } from 'react';
import './DomainVerification.css';
/**
* Domain verification UI component
* Allows users to verify domain ownership via DNS TXT records or blockchain
*/
export default function DomainVerification({ apiBaseUrl = 'https://api.aethex.cloud/api/passport/domain' }) {
const [domain, setDomain] = useState('');
const [walletAddress, setWalletAddress] = useState('');
const [verificationInstructions, setVerificationInstructions] = useState(null);
const [verificationStatus, setVerificationStatus] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const [currentStatus, setCurrentStatus] = useState(null);
// Load current verification status on mount
useEffect(() => {
loadCurrentStatus();
}, []);
/**
* Load current verification status
*/
async function loadCurrentStatus() {
try {
const token = localStorage.getItem('authToken');
const response = await fetch(`${apiBaseUrl}/status`, {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
const data = await response.json();
setCurrentStatus(data);
}
} catch (err) {
console.error('Failed to load status:', err);
}
}
/**
* Request verification token from backend
*/
async function requestVerification() {
if (!domain) {
setError('Please enter a domain');
return;
}
setLoading(true);
setError(null);
try {
const token = localStorage.getItem('authToken');
const response = await fetch(`${apiBaseUrl}/request-verification`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({ domain })
});
const data = await response.json();
if (data.success) {
setVerificationInstructions(data.verification);
} else {
setError(data.error || 'Failed to request verification');
}
} catch (err) {
setError('Network error. Please try again.');
console.error('Failed to request verification:', err);
} finally {
setLoading(false);
}
}
/**
* Verify domain ownership by checking DNS or blockchain
*/
async function verifyDomain() {
setLoading(true);
setError(null);
try {
const token = localStorage.getItem('authToken');
const body = { domain };
// Add wallet address if verifying .aethex domain
if (domain.endsWith('.aethex')) {
body.walletAddress = walletAddress;
}
const response = await fetch(`${apiBaseUrl}/verify`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify(body)
});
const data = await response.json();
setVerificationStatus(data);
if (data.verified) {
// Refresh status and reload after short delay
setTimeout(() => {
loadCurrentStatus();
window.location.reload();
}, 1500);
} else {
setError(data.error || 'Verification failed');
}
} catch (err) {
setError('Network error. Please try again.');
console.error('Verification failed:', err);
} finally {
setLoading(false);
}
}
/**
* Copy text to clipboard
*/
function copyToClipboard(text) {
navigator.clipboard.writeText(text);
// You could add a toast notification here
alert('Copied to clipboard!');
}
/**
* Reset form
*/
function resetForm() {
setVerificationInstructions(null);
setVerificationStatus(null);
setDomain('');
setWalletAddress('');
setError(null);
}
// Show current verified domain if exists
if (currentStatus?.hasVerifiedDomain) {
return (
<div className="domain-verification verified-container">
<h3> Domain Verified</h3>
<div className="verified-domain-display">
<strong>{currentStatus.domain}</strong>
<span className="verification-type">
{currentStatus.verificationType === 'blockchain' ? 'Blockchain' : 'DNS'}
</span>
</div>
<p className="verified-date">
Verified on {new Date(currentStatus.verifiedAt).toLocaleDateString()}
</p>
<button
className="secondary-button"
onClick={() => setCurrentStatus(null)}
>
Verify Another Domain
</button>
</div>
);
}
return (
<div className="domain-verification">
<h3>Verify Your Domain</h3>
<p className="description">
Prove ownership of a domain to display it on your profile
</p>
{error && (
<div className="error-message">
<span> {error}</span>
</div>
)}
{!verificationInstructions ? (
<div className="input-section">
<div className="form-group">
<label htmlFor="domain">Domain Name</label>
<input
id="domain"
type="text"
placeholder="yourdomain.com or anderson.aethex"
value={domain}
onChange={(e) => setDomain(e.target.value.toLowerCase().trim())}
disabled={loading}
className="domain-input"
/>
<small className="help-text">
Enter a traditional domain (e.g., dev.aethex.dev) or a .aethex blockchain domain
</small>
</div>
<button
onClick={requestVerification}
disabled={!domain || loading}
className="primary-button"
>
{loading ? 'Generating...' : 'Request Verification'}
</button>
</div>
) : (
<div className="instructions-section">
<h4>
{domain.endsWith('.aethex')
? 'Connect Your Wallet'
: `Add this DNS record to ${verificationInstructions.domain}:`
}
</h4>
{domain.endsWith('.aethex') ? (
// Blockchain verification
<div className="blockchain-verification">
<p>Connect the wallet that owns <strong>{domain}</strong></p>
<div className="form-group">
<label htmlFor="wallet">Wallet Address</label>
<input
id="wallet"
type="text"
placeholder="0x..."
value={walletAddress}
onChange={(e) => setWalletAddress(e.target.value.trim())}
disabled={loading}
className="wallet-input"
/>
</div>
<button
onClick={verifyDomain}
disabled={!walletAddress || loading}
className="primary-button verify-button"
>
{loading ? 'Verifying...' : 'Verify Ownership'}
</button>
</div>
) : (
// DNS verification
<div className="dns-verification">
<div className="dns-record">
<div className="record-field">
<strong>Type:</strong>
<span>{verificationInstructions.recordType}</span>
</div>
<div className="record-field">
<strong>Name:</strong>
<span>{verificationInstructions.recordName}</span>
</div>
<div className="record-field">
<strong>Value:</strong>
<div className="value-container">
<code>{verificationInstructions.recordValue}</code>
<button
onClick={() => copyToClipboard(verificationInstructions.recordValue)}
className="copy-button"
title="Copy to clipboard"
>
📋 Copy
</button>
</div>
</div>
</div>
<div className="help-section">
<p><strong>How to add DNS records:</strong></p>
<ol>
<li>Go to your domain's DNS settings (Google Domains, Cloudflare, etc.)</li>
<li>Add a new TXT record with the values above</li>
<li>Wait 5-10 minutes for DNS to propagate</li>
<li>Click "Verify Domain" below</li>
</ol>
<p className="expires-note">
This verification expires on {new Date(verificationInstructions.expiresAt).toLocaleDateString()}
</p>
</div>
<button
onClick={verifyDomain}
disabled={loading}
className="primary-button verify-button"
>
{loading ? 'Verifying...' : 'Verify Domain'}
</button>
{verificationStatus && (
<div className={`status-message ${verificationStatus.verified ? 'success' : 'error'}`}>
{verificationStatus.verified ? (
<span> Domain verified successfully!</span>
) : (
<span> {verificationStatus.error}</span>
)}
</div>
)}
</div>
)}
<button
onClick={resetForm}
className="secondary-button cancel-button"
disabled={loading}
>
Cancel
</button>
</div>
)}
</div>
);
}

View file

@ -0,0 +1,85 @@
.verified-domain-badge {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
background: linear-gradient(135deg, #d1fae5 0%, #a7f3d0 100%);
border: 2px solid #6ee7b7;
border-radius: 24px;
font-size: 14px;
font-weight: 500;
color: #065f46;
box-shadow: 0 2px 4px rgba(16, 185, 129, 0.2);
transition: all 0.2s;
}
.verified-domain-badge:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(16, 185, 129, 0.3);
}
.badge-content {
display: flex;
align-items: center;
gap: 8px;
}
.domain-text {
font-family: 'Courier New', monospace;
font-weight: 600;
color: #047857;
}
.checkmark {
display: inline-flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
background: #10b981;
color: white;
border-radius: 50%;
font-size: 12px;
font-weight: bold;
}
.blockchain-indicator {
font-size: 16px;
opacity: 0.8;
}
.verified-info {
font-size: 11px;
color: #047857;
opacity: 0.8;
margin-top: 4px;
}
/* Compact variant */
.verified-domain-badge.compact {
padding: 4px 12px;
font-size: 12px;
}
.verified-domain-badge.compact .checkmark {
width: 16px;
height: 16px;
font-size: 10px;
}
/* Dark mode support */
@media (prefers-color-scheme: dark) {
.verified-domain-badge {
background: linear-gradient(135deg, #064e3b 0%, #065f46 100%);
border-color: #047857;
color: #d1fae5;
}
.domain-text {
color: #a7f3d0;
}
.verified-info {
color: #a7f3d0;
}
}

View file

@ -0,0 +1,41 @@
import React from 'react';
import './VerifiedDomainBadge.css';
/**
* Displays verified domain badge on user profile
* @param {Object} props
* @param {string} props.verifiedDomain - The verified domain name
* @param {string} props.verificationType - Type of verification (dns or blockchain)
* @param {Date} props.verifiedAt - When the domain was verified
*/
export default function VerifiedDomainBadge({
verifiedDomain,
verificationType = 'dns',
verifiedAt
}) {
if (!verifiedDomain) return null;
return (
<div className="verified-domain-badge">
<div className="badge-content">
<span className="domain-text">{verifiedDomain}</span>
<span
className="checkmark"
title={`Verified via ${verificationType === 'blockchain' ? 'blockchain' : 'DNS'}`}
>
</span>
</div>
{verificationType === 'blockchain' && (
<span className="blockchain-indicator" title="Verified via blockchain">
</span>
)}
{verifiedAt && (
<div className="verified-info">
Verified {new Date(verifiedAt).toLocaleDateString()}
</div>
)}
</div>
);
}

View file

@ -0,0 +1,36 @@
-- Database Schema for Domain Verification Feature
-- Run this migration to create the necessary tables
-- Verification Requests Table
CREATE TABLE IF NOT EXISTS domain_verifications (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL,
domain VARCHAR(255) NOT NULL,
verification_token VARCHAR(64) NOT NULL,
verification_type VARCHAR(20) CHECK (verification_type IN ('dns', 'blockchain')) DEFAULT 'dns',
verified BOOLEAN DEFAULT false,
created_at TIMESTAMP DEFAULT NOW(),
verified_at TIMESTAMP,
expires_at TIMESTAMP DEFAULT NOW() + INTERVAL '7 days',
CONSTRAINT unique_user_domain UNIQUE(user_id, domain)
);
-- Create index for faster lookups
CREATE INDEX IF NOT EXISTS idx_domain_verifications_user_id ON domain_verifications(user_id);
CREATE INDEX IF NOT EXISTS idx_domain_verifications_domain ON domain_verifications(domain);
CREATE INDEX IF NOT EXISTS idx_domain_verifications_verified ON domain_verifications(verified);
-- Users Table Extensions
-- Assumes users table already exists
ALTER TABLE users
ADD COLUMN IF NOT EXISTS verified_domain VARCHAR(255),
ADD COLUMN IF NOT EXISTS domain_verified_at TIMESTAMP;
-- Create index for verified domains
CREATE INDEX IF NOT EXISTS idx_users_verified_domain ON users(verified_domain) WHERE verified_domain IS NOT NULL;
-- Comments for documentation
COMMENT ON TABLE domain_verifications IS 'Stores domain verification requests and their status';
COMMENT ON COLUMN domain_verifications.verification_token IS 'Unique token to be added to DNS TXT record or wallet address for blockchain verification';
COMMENT ON COLUMN domain_verifications.verification_type IS 'Type of verification: dns for traditional domains, blockchain for .aethex domains';
COMMENT ON COLUMN domain_verifications.expires_at IS 'Verification request expires after 7 days';

View file

@ -0,0 +1,132 @@
const express = require('express');
const router = express.Router();
const { authenticateUser, isValidDomain } = require('../middleware/auth');
const {
generateDomainVerificationToken,
verifyDomainOwnership,
verifyAethexDomain,
getVerificationStatus
} = require('../utils/domainVerification');
/**
* POST /api/passport/domain/request-verification
* Request domain verification token
*/
router.post('/request-verification', authenticateUser, async (req, res) => {
try {
const { domain } = req.body;
// Validate input
if (!domain) {
return res.status(400).json({
success: false,
error: 'Domain is required'
});
}
if (!isValidDomain(domain)) {
return res.status(400).json({
success: false,
error: 'Invalid domain format. Please enter a valid domain (e.g., example.com or sub.example.com)'
});
}
// Generate verification token
const verification = await generateDomainVerificationToken(req.user.id, domain);
res.json({
success: true,
verification: verification
});
} catch (error) {
console.error('Request verification error:', error);
res.status(500).json({
success: false,
error: 'Failed to generate verification token. Please try again.'
});
}
});
/**
* POST /api/passport/domain/verify
* Verify domain ownership via DNS or blockchain
*/
router.post('/verify', authenticateUser, async (req, res) => {
try {
const { domain, walletAddress } = req.body;
// Validate input
if (!domain) {
return res.status(400).json({
success: false,
error: 'Domain is required'
});
}
if (!isValidDomain(domain)) {
return res.status(400).json({
success: false,
error: 'Invalid domain format'
});
}
// Check if it's a .aethex domain (blockchain verification)
let result;
if (domain.endsWith('.aethex')) {
if (!walletAddress) {
return res.status(400).json({
success: false,
error: 'Wallet address is required for .aethex domain verification'
});
}
result = await verifyAethexDomain(req.user.id, domain, walletAddress);
} else {
// DNS verification for traditional domains
result = await verifyDomainOwnership(req.user.id, domain);
}
if (result.verified) {
res.json({
success: true,
verified: true,
domain: result.domain,
verifiedAt: result.verifiedAt
});
} else {
res.status(400).json({
success: false,
verified: false,
error: result.error
});
}
} catch (error) {
console.error('Verify domain error:', error);
res.status(500).json({
success: false,
verified: false,
error: 'Verification failed. Please try again.'
});
}
});
/**
* GET /api/passport/domain/status
* Get current verification status for user
*/
router.get('/status', authenticateUser, async (req, res) => {
try {
const status = await getVerificationStatus(req.user.id);
res.json(status);
} catch (error) {
console.error('Get status error:', error);
res.status(500).json({
success: false,
error: 'Failed to get verification status'
});
}
});
module.exports = router;

View file

@ -0,0 +1,249 @@
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<Object>} 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<Object>} 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<Object>} 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<Object>} 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
};

12
jest.config.js Normal file
View file

@ -0,0 +1,12 @@
module.exports = {
testEnvironment: 'node',
coverageDirectory: 'coverage',
collectCoverageFrom: [
'src/**/*.js',
'!src/**/*.test.js'
],
testMatch: [
'**/test/**/*.test.js',
'**/__tests__/**/*.js'
]
};

5547
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

38
package.json Normal file
View file

@ -0,0 +1,38 @@
{
"name": "aethex-passport-domain-verification",
"version": "1.0.0",
"description": "Domain verification feature for AeThex Passport",
"main": "src/backend/server.js",
"scripts": {
"start": "node src/backend/server.js",
"dev": "nodemon src/backend/server.js",
"migrate": "node src/backend/database/migrate.js",
"test": "jest",
"frontend:dev": "cd src/frontend && npm run dev",
"frontend:build": "cd src/frontend && npm run build"
},
"keywords": [
"domain-verification",
"dns",
"blockchain",
"passport"
],
"author": "AeThex Corporation",
"license": "MIT",
"dependencies": {
"@supabase/supabase-js": "^2.90.1",
"cors": "^2.8.5",
"dotenv": "^16.3.1",
"ethers": "^6.10.0",
"express": "^4.18.2",
"express-rate-limit": "^7.1.5",
"helmet": "^7.1.0",
"jsonwebtoken": "^9.0.3",
"pg": "^8.11.3"
},
"devDependencies": {
"jest": "^29.7.0",
"nodemon": "^3.0.2",
"supertest": "^6.3.3"
}
}

View file

@ -0,0 +1,39 @@
const { createClient } = require('@supabase/supabase-js');
const fs = require('fs');
require('dotenv').config();
async function applyMigration() {
const supabase = createClient(
process.env.SUPABASE_URL,
process.env.SUPABASE_SERVICE_ROLE_KEY
);
const sql = fs.readFileSync('src/backend/database/migrations/001_domain_verifications.sql', 'utf8');
console.log('Applying domain verification schema to Supabase...\n');
// Split by semicolons and execute each statement
const statements = sql
.split(';')
.map(s => s.trim())
.filter(s => s.length > 0 && !s.startsWith('--'));
for (const statement of statements) {
try {
console.log('Executing:', statement.substring(0, 80) + '...');
const { data, error } = await supabase.rpc('exec_sql', { sql: statement });
if (error) {
console.error('Error:', error.message);
} else {
console.log('✓ Success');
}
} catch (err) {
console.log('Note:', err.message);
}
}
console.log('\n✓ Migration complete! Domain verification tables are ready.');
process.exit(0);
}
applyMigration();

View file

@ -0,0 +1,55 @@
const { Pool } = require('pg');
require('dotenv').config();
/**
* PostgreSQL database connection pool
*/
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
max: 20,
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 2000,
});
// Handle pool errors
pool.on('error', (err) => {
console.error('Unexpected error on idle client', err);
process.exit(-1);
});
/**
* Execute a database query
* @param {string} text - SQL query
* @param {Array} params - Query parameters
* @returns {Promise<Object>} Query result
*/
async function query(text, params) {
const start = Date.now();
try {
const res = await pool.query(text, params);
const duration = Date.now() - start;
if (process.env.NODE_ENV === 'development') {
console.log('Executed query', { text, duration, rows: res.rowCount });
}
return res;
} catch (error) {
console.error('Database query error:', error);
throw error;
}
}
/**
* Get a client from the pool for transactions
* @returns {Promise<Object>} Database client
*/
async function getClient() {
return await pool.connect();
}
module.exports = {
query,
getClient,
pool
};

View file

@ -0,0 +1,49 @@
const { Pool } = require('pg');
const fs = require('fs');
const path = require('path');
require('dotenv').config();
/**
* Database migration runner
* Executes SQL migration files in order
*/
async function runMigrations() {
const pool = new Pool({
connectionString: process.env.DATABASE_URL
});
try {
console.log('Starting database migrations...');
// Get all migration files
const migrationsDir = path.join(__dirname, 'migrations');
const migrationFiles = fs.readdirSync(migrationsDir)
.filter(file => file.endsWith('.sql'))
.sort();
// Execute each migration
for (const file of migrationFiles) {
console.log(`Running migration: ${file}`);
const filePath = path.join(migrationsDir, file);
const sql = fs.readFileSync(filePath, 'utf8');
await pool.query(sql);
console.log(`✓ Completed: ${file}`);
}
console.log('\n✓ All migrations completed successfully');
} catch (error) {
console.error('Migration failed:', error);
process.exit(1);
} finally {
await pool.end();
}
}
// Run migrations if called directly
if (require.main === module) {
runMigrations();
}
module.exports = { runMigrations };

View file

@ -0,0 +1,36 @@
-- Database Schema for Domain Verification Feature
-- Run this migration to create the necessary tables
-- Verification Requests Table
CREATE TABLE IF NOT EXISTS domain_verifications (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL,
domain VARCHAR(255) NOT NULL,
verification_token VARCHAR(64) NOT NULL,
verification_type VARCHAR(20) CHECK (verification_type IN ('dns', 'blockchain')) DEFAULT 'dns',
verified BOOLEAN DEFAULT false,
created_at TIMESTAMP DEFAULT NOW(),
verified_at TIMESTAMP,
expires_at TIMESTAMP DEFAULT NOW() + INTERVAL '7 days',
CONSTRAINT unique_user_domain UNIQUE(user_id, domain)
);
-- Create index for faster lookups
CREATE INDEX IF NOT EXISTS idx_domain_verifications_user_id ON domain_verifications(user_id);
CREATE INDEX IF NOT EXISTS idx_domain_verifications_domain ON domain_verifications(domain);
CREATE INDEX IF NOT EXISTS idx_domain_verifications_verified ON domain_verifications(verified);
-- Users Table Extensions
-- Assumes users table already exists
ALTER TABLE users
ADD COLUMN IF NOT EXISTS verified_domain VARCHAR(255),
ADD COLUMN IF NOT EXISTS domain_verified_at TIMESTAMP;
-- Create index for verified domains
CREATE INDEX IF NOT EXISTS idx_users_verified_domain ON users(verified_domain) WHERE verified_domain IS NOT NULL;
-- Comments for documentation
COMMENT ON TABLE domain_verifications IS 'Stores domain verification requests and their status';
COMMENT ON COLUMN domain_verifications.verification_token IS 'Unique token to be added to DNS TXT record or wallet address for blockchain verification';
COMMENT ON COLUMN domain_verifications.verification_type IS 'Type of verification: dns for traditional domains, blockchain for .aethex domains';
COMMENT ON COLUMN domain_verifications.expires_at IS 'Verification request expires after 7 days';

View file

@ -0,0 +1,107 @@
const jwt = require('jsonwebtoken');
/**
* Authentication middleware
* Verifies JWT token and attaches user info to request
*/
function authenticateUser(req, res, next) {
try {
// In development mode, allow requests without auth for testing
if (process.env.NODE_ENV === 'development') {
// Check for token, but if not present, use demo user
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
// Use demo user for development
req.user = {
id: 'demo-user-123',
email: 'demo@aethex.dev'
};
return next();
}
const token = authHeader.substring(7);
// Try to verify, but don't fail if invalid
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = {
id: decoded.userId,
email: decoded.email
};
} catch {
// Use demo user if token invalid
req.user = {
id: 'demo-user-123',
email: 'demo@aethex.dev'
};
}
return next();
}
// Production: Strict auth required
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({
success: false,
error: 'Authentication required. Please provide a valid token.'
});
}
const token = authHeader.substring(7); // Remove 'Bearer ' prefix
// Verify token
const decoded = jwt.verify(token, process.env.JWT_SECRET);
// Attach user info to request
req.user = {
id: decoded.userId,
email: decoded.email
};
next();
} catch (error) {
if (error.name === 'JsonWebTokenError') {
return res.status(401).json({
success: false,
error: 'Invalid authentication token'
});
}
if (error.name === 'TokenExpiredError') {
return res.status(401).json({
success: false,
error: 'Authentication token has expired'
});
}
return res.status(500).json({
success: false,
error: 'Authentication failed'
});
}
}
/**
* Validate domain format
* @param {string} domain - Domain to validate
* @returns {boolean} Whether domain is valid
*/
function isValidDomain(domain) {
if (!domain || typeof domain !== 'string') {
return false;
}
// Basic domain validation regex
// Allows: example.com, sub.example.com, example.aethex
const domainRegex = /^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$/i;
return domainRegex.test(domain) && domain.length <= 253;
}
module.exports = {
authenticateUser,
isValidDomain
};

View file

@ -0,0 +1,132 @@
const express = require('express');
const router = express.Router();
const { authenticateUser, isValidDomain } = require('../middleware/auth');
const {
generateDomainVerificationToken,
verifyDomainOwnership,
verifyAethexDomain,
getVerificationStatus
} = require('../utils/domainVerification');
/**
* POST /api/passport/domain/request-verification
* Request domain verification token
*/
router.post('/request-verification', authenticateUser, async (req, res) => {
try {
const { domain } = req.body;
// Validate input
if (!domain) {
return res.status(400).json({
success: false,
error: 'Domain is required'
});
}
if (!isValidDomain(domain)) {
return res.status(400).json({
success: false,
error: 'Invalid domain format. Please enter a valid domain (e.g., example.com or sub.example.com)'
});
}
// Generate verification token
const verification = await generateDomainVerificationToken(req.user.id, domain);
res.json({
success: true,
verification: verification
});
} catch (error) {
console.error('Request verification error:', error);
res.status(500).json({
success: false,
error: 'Failed to generate verification token. Please try again.'
});
}
});
/**
* POST /api/passport/domain/verify
* Verify domain ownership via DNS or blockchain
*/
router.post('/verify', authenticateUser, async (req, res) => {
try {
const { domain, walletAddress } = req.body;
// Validate input
if (!domain) {
return res.status(400).json({
success: false,
error: 'Domain is required'
});
}
if (!isValidDomain(domain)) {
return res.status(400).json({
success: false,
error: 'Invalid domain format'
});
}
// Check if it's a .aethex domain (blockchain verification)
let result;
if (domain.endsWith('.aethex')) {
if (!walletAddress) {
return res.status(400).json({
success: false,
error: 'Wallet address is required for .aethex domain verification'
});
}
result = await verifyAethexDomain(req.user.id, domain, walletAddress);
} else {
// DNS verification for traditional domains
result = await verifyDomainOwnership(req.user.id, domain);
}
if (result.verified) {
res.json({
success: true,
verified: true,
domain: result.domain,
verifiedAt: result.verifiedAt
});
} else {
res.status(400).json({
success: false,
verified: false,
error: result.error
});
}
} catch (error) {
console.error('Verify domain error:', error);
res.status(500).json({
success: false,
verified: false,
error: 'Verification failed. Please try again.'
});
}
});
/**
* GET /api/passport/domain/status
* Get current verification status for user
*/
router.get('/status', authenticateUser, async (req, res) => {
try {
const status = await getVerificationStatus(req.user.id);
res.json(status);
} catch (error) {
console.error('Get status error:', error);
res.status(500).json({
success: false,
error: 'Failed to get verification status'
});
}
});
module.exports = router;

86
src/backend/server.js Normal file
View file

@ -0,0 +1,86 @@
const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
const rateLimit = require('express-rate-limit');
require('dotenv').config();
const domainRoutes = require('./routes/domainRoutes');
const app = express();
const PORT = process.env.PORT || 3000;
// Trust proxy for Codespaces/containers
app.set('trust proxy', 1);
// Security middleware
app.use(helmet());
app.use(cors({
origin: process.env.FRONTEND_URL || 'http://localhost:5173',
credentials: true
}));
// Rate limiting
const limiter = rateLimit({
windowMs: parseInt(process.env.RATE_LIMIT_WINDOW_MS) || 15 * 60 * 1000, // 15 minutes
max: parseInt(process.env.RATE_LIMIT_MAX_REQUESTS) || 100,
message: 'Too many requests from this IP, please try again later.',
standardHeaders: true,
legacyHeaders: false,
});
app.use('/api/', limiter);
// Body parsing middleware
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// Health check endpoint
app.get('/health', (req, res) => {
res.json({ status: 'ok', timestamp: new Date().toISOString() });
});
// API routes
app.use('/api/passport/domain', domainRoutes);
// 404 handler
app.use((req, res) => {
res.status(404).json({
success: false,
error: 'Endpoint not found'
});
});
// Error handler
app.use((err, req, res, next) => {
console.error('Server error:', err);
res.status(500).json({
success: false,
error: process.env.NODE_ENV === 'development' ? err.message : 'Internal server error'
});
});
// Start server
app.listen(PORT, () => {
console.log(`
AeThex Passport - Domain Verification API
Server running on port ${PORT}
Environment: ${process.env.NODE_ENV || 'development'}
`);
console.log(`Health check: http://localhost:${PORT}/health`);
console.log(`API Base URL: http://localhost:${PORT}/api/passport/domain`);
});
// Graceful shutdown
process.on('SIGTERM', () => {
console.log('SIGTERM received, shutting down gracefully...');
process.exit(0);
});
process.on('SIGINT', () => {
console.log('SIGINT received, shutting down gracefully...');
process.exit(0);
});
module.exports = app;

View file

@ -0,0 +1,249 @@
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<Object>} 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<Object>} 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<Object>} 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<Object>} 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
};

View file

@ -0,0 +1,55 @@
const { createClient } = require('@supabase/supabase-js');
require('dotenv').config();
/**
* Supabase client for backend operations
* Uses service role key for admin access
*/
const supabase = createClient(
process.env.SUPABASE_URL,
process.env.SUPABASE_SERVICE_ROLE_KEY,
{
auth: {
autoRefreshToken: false,
persistSession: false
}
}
);
/**
* Get Supabase client instance
* @returns {Object} Supabase client
*/
function getSupabaseClient() {
return supabase;
}
/**
* Execute a raw SQL query using Supabase
* @param {string} query - SQL query
* @param {Array} params - Query parameters
* @returns {Promise<Object>} Query result
*/
async function executeQuery(query, params = []) {
try {
const { data, error } = await supabase.rpc('execute_sql', {
query: query,
params: params
});
if (error) {
throw error;
}
return { rows: data, rowCount: data?.length || 0 };
} catch (error) {
console.error('Supabase query error:', error);
throw error;
}
}
module.exports = {
supabase,
getSupabaseClient,
executeQuery
};

176
src/frontend/App.css Normal file
View file

@ -0,0 +1,176 @@
supabase login --token YOUR_ACCESS_TOKENsupabase login --token YOUR_ACCESS_TOKEN* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}
.app {
min-height: 100vh;
display: flex;
flex-direction: column;
}
.app-header {
background: rgba(255, 255, 255, 0.95);
padding: 24px;
text-align: center;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.app-header h1 {
margin: 0 0 8px 0;
color: #1a1a1a;
font-size: 32px;
font-weight: 700;
}
.app-header p {
margin: 0;
color: #666;
font-size: 16px;
}
.app-main {
flex: 1;
padding: 40px 24px;
max-width: 1200px;
width: 100%;
margin: 0 auto;
}
.user-profile {
background: white;
border-radius: 16px;
padding: 32px;
margin-bottom: 32px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
}
.profile-header {
text-align: center;
padding-bottom: 24px;
border-bottom: 2px solid #f3f4f6;
margin-bottom: 24px;
}
.profile-header h2 {
margin: 0 0 8px 0;
color: #1a1a1a;
font-size: 28px;
}
.profile-header p {
margin: 0 0 16px 0;
color: #666;
font-size: 16px;
}
.profile-section {
display: flex;
flex-direction: column;
gap: 24px;
}
.toggle-button {
padding: 12px 24px;
background: #4f46e5;
color: white;
border: none;
border-radius: 8px;
font-size: 16px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
align-self: center;
}
.toggle-button:hover {
background: #4338ca;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(79, 70, 229, 0.3);
}
.verification-container {
margin-top: 8px;
}
.info-section {
background: rgba(255, 255, 255, 0.95);
border-radius: 16px;
padding: 32px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
}
.info-section h3 {
margin: 0 0 16px 0;
color: #1a1a1a;
font-size: 24px;
}
.info-section p {
margin: 0 0 16px 0;
color: #374151;
line-height: 1.6;
}
.info-section ul {
list-style: none;
padding: 0;
margin: 0;
}
.info-section li {
padding: 8px 0;
color: #374151;
font-size: 16px;
}
.app-footer {
background: rgba(0, 0, 0, 0.8);
color: white;
text-align: center;
padding: 24px;
margin-top: auto;
}
.app-footer p {
margin: 0;
font-size: 14px;
opacity: 0.8;
}
/* Responsive */
@media (max-width: 768px) {
.app-main {
padding: 24px 16px;
}
.user-profile,
.info-section {
padding: 24px;
}
.app-header h1 {
font-size: 24px;
}
.profile-header h2 {
font-size: 24px;
}
}

87
src/frontend/App.jsx Normal file
View file

@ -0,0 +1,87 @@
import React, { useState, useEffect } from 'react';
import DomainVerification from './components/DomainVerification';
import VerifiedDomainBadge from './components/VerifiedDomainBadge';
import './App.css';
/**
* Main application component
* Demo of domain verification feature
*/
function App() {
const [user, setUser] = useState(null);
const [showVerification, setShowVerification] = useState(false);
// Mock user data - in production, fetch from API
useEffect(() => {
// Simulate fetching user data
const mockUser = {
id: 'user-123',
name: 'Demo User',
email: 'demo@aethex.dev',
verifiedDomain: null, // Will be populated after verification
domainVerifiedAt: null
};
setUser(mockUser);
}, []);
return (
<div className="app">
<header className="app-header">
<h1>AeThex Passport</h1>
<p>Domain Verification Demo</p>
</header>
<main className="app-main">
{user && (
<div className="user-profile">
<div className="profile-header">
<h2>{user.name}</h2>
<p>{user.email}</p>
{user.verifiedDomain && (
<VerifiedDomainBadge
verifiedDomain={user.verifiedDomain}
verifiedAt={user.domainVerifiedAt}
/>
)}
</div>
<div className="profile-section">
<button
onClick={() => setShowVerification(!showVerification)}
className="toggle-button"
>
{showVerification ? 'Hide' : 'Show'} Domain Verification
</button>
{showVerification && (
<div className="verification-container">
<DomainVerification />
</div>
)}
</div>
</div>
)}
<div className="info-section">
<h3>About Domain Verification</h3>
<p>
Domain verification allows you to prove ownership of a domain by adding
a DNS TXT record or connecting a wallet that owns a .aethex blockchain domain.
</p>
<ul>
<li> Verify traditional domains via DNS TXT records</li>
<li> Verify .aethex domains via blockchain</li>
<li> Display verified domain on your profile</li>
<li> Prevent domain impersonation</li>
</ul>
</div>
</main>
<footer className="app-footer">
<p>&copy; 2026 AeThex Corporation. All rights reserved.</p>
</footer>
</div>
);
}
export default App;

View file

@ -0,0 +1,316 @@
.domain-verification {
max-width: 600px;
margin: 0 auto;
padding: 24px;
background: #ffffff;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.domain-verification h3 {
margin: 0 0 8px 0;
color: #1a1a1a;
font-size: 24px;
font-weight: 600;
}
.domain-verification .description {
margin: 0 0 24px 0;
color: #666;
font-size: 14px;
}
/* Error message */
.error-message {
padding: 12px;
margin-bottom: 16px;
background: #fee;
border: 1px solid #fcc;
border-radius: 6px;
color: #c33;
}
/* Input section */
.input-section {
display: flex;
flex-direction: column;
gap: 16px;
}
.form-group {
display: flex;
flex-direction: column;
gap: 8px;
}
.form-group label {
font-weight: 500;
color: #333;
font-size: 14px;
}
.domain-input,
.wallet-input {
padding: 12px;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 16px;
transition: border-color 0.2s;
}
.domain-input:focus,
.wallet-input:focus {
outline: none;
border-color: #4f46e5;
}
.domain-input:disabled,
.wallet-input:disabled {
background: #f5f5f5;
cursor: not-allowed;
}
.help-text {
font-size: 12px;
color: #666;
margin-top: -4px;
}
/* Buttons */
.primary-button,
.secondary-button {
padding: 12px 24px;
border: none;
border-radius: 8px;
font-size: 16px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
.primary-button {
background: #4f46e5;
color: white;
}
.primary-button:hover:not(:disabled) {
background: #4338ca;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(79, 70, 229, 0.3);
}
.primary-button:disabled {
background: #9ca3af;
cursor: not-allowed;
transform: none;
}
.secondary-button {
background: #f3f4f6;
color: #374151;
}
.secondary-button:hover:not(:disabled) {
background: #e5e7eb;
}
.cancel-button {
margin-top: 12px;
}
/* Instructions section */
.instructions-section {
display: flex;
flex-direction: column;
gap: 20px;
}
.instructions-section h4 {
margin: 0;
color: #1a1a1a;
font-size: 18px;
}
/* DNS record display */
.dns-record {
background: #f9fafb;
border: 1px solid #e5e7eb;
border-radius: 8px;
padding: 16px;
display: flex;
flex-direction: column;
gap: 12px;
}
.record-field {
display: flex;
flex-direction: column;
gap: 4px;
}
.record-field strong {
font-size: 12px;
text-transform: uppercase;
color: #6b7280;
letter-spacing: 0.5px;
}
.record-field span {
font-size: 14px;
color: #1f2937;
}
.value-container {
display: flex;
gap: 8px;
align-items: center;
}
.value-container code {
flex: 1;
padding: 8px;
background: #fff;
border: 1px solid #e5e7eb;
border-radius: 4px;
font-size: 13px;
word-break: break-all;
font-family: 'Courier New', monospace;
}
.copy-button {
padding: 8px 12px;
background: #fff;
border: 1px solid #e5e7eb;
border-radius: 4px;
font-size: 12px;
cursor: pointer;
white-space: nowrap;
transition: all 0.2s;
}
.copy-button:hover {
background: #f3f4f6;
border-color: #d1d5db;
}
/* Help section */
.help-section {
background: #eff6ff;
border: 1px solid #bfdbfe;
border-radius: 8px;
padding: 16px;
}
.help-section p {
margin: 0 0 8px 0;
color: #1e40af;
font-size: 14px;
}
.help-section ol {
margin: 8px 0;
padding-left: 24px;
color: #1e3a8a;
}
.help-section li {
margin: 4px 0;
font-size: 14px;
}
.expires-note {
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid #bfdbfe;
font-size: 13px;
color: #1e40af;
}
/* Status message */
.status-message {
padding: 12px;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
}
.status-message.success {
background: #d1fae5;
border: 1px solid #6ee7b7;
color: #065f46;
}
.status-message.error {
background: #fee2e2;
border: 1px solid #fca5a5;
color: #991b1b;
}
/* Blockchain verification */
.blockchain-verification {
display: flex;
flex-direction: column;
gap: 16px;
}
.blockchain-verification p {
margin: 0;
color: #374151;
}
/* Verified container */
.verified-container {
text-align: center;
}
.verified-container h3 {
color: #059669;
font-size: 28px;
}
.verified-domain-display {
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
margin: 20px 0;
padding: 16px;
background: #d1fae5;
border-radius: 8px;
}
.verified-domain-display strong {
font-size: 20px;
color: #065f46;
}
.verification-type {
padding: 4px 8px;
background: #059669;
color: white;
border-radius: 4px;
font-size: 12px;
text-transform: uppercase;
}
.verified-date {
color: #6b7280;
font-size: 14px;
margin-bottom: 20px;
}
/* Responsive */
@media (max-width: 640px) {
.domain-verification {
padding: 16px;
}
.value-container {
flex-direction: column;
align-items: stretch;
}
.copy-button {
width: 100%;
}
}

View file

@ -0,0 +1,313 @@
import React, { useState, useEffect } from 'react';
import './DomainVerification.css';
/**
* Domain verification UI component
* Allows users to verify domain ownership via DNS TXT records or blockchain
*/
export default function DomainVerification({ apiBaseUrl = 'https://api.aethex.cloud/api/passport/domain' }) {
const [domain, setDomain] = useState('');
const [walletAddress, setWalletAddress] = useState('');
const [verificationInstructions, setVerificationInstructions] = useState(null);
const [verificationStatus, setVerificationStatus] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const [currentStatus, setCurrentStatus] = useState(null);
// Load current verification status on mount
useEffect(() => {
loadCurrentStatus();
}, []);
/**
* Load current verification status
*/
async function loadCurrentStatus() {
try {
const token = localStorage.getItem('authToken');
const response = await fetch(`${apiBaseUrl}/status`, {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
const data = await response.json();
setCurrentStatus(data);
}
} catch (err) {
console.error('Failed to load status:', err);
}
}
/**
* Request verification token from backend
*/
async function requestVerification() {
if (!domain) {
setError('Please enter a domain');
return;
}
setLoading(true);
setError(null);
try {
const token = localStorage.getItem('authToken');
const response = await fetch(`${apiBaseUrl}/request-verification`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({ domain })
});
const data = await response.json();
if (data.success) {
setVerificationInstructions(data.verification);
} else {
setError(data.error || 'Failed to request verification');
}
} catch (err) {
setError('Network error. Please try again.');
console.error('Failed to request verification:', err);
} finally {
setLoading(false);
}
}
/**
* Verify domain ownership by checking DNS or blockchain
*/
async function verifyDomain() {
setLoading(true);
setError(null);
try {
const token = localStorage.getItem('authToken');
const body = { domain };
// Add wallet address if verifying .aethex domain
if (domain.endsWith('.aethex')) {
body.walletAddress = walletAddress;
}
const response = await fetch(`${apiBaseUrl}/verify`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify(body)
});
const data = await response.json();
setVerificationStatus(data);
if (data.verified) {
// Refresh status and reload after short delay
setTimeout(() => {
loadCurrentStatus();
window.location.reload();
}, 1500);
} else {
setError(data.error || 'Verification failed');
}
} catch (err) {
setError('Network error. Please try again.');
console.error('Verification failed:', err);
} finally {
setLoading(false);
}
}
/**
* Copy text to clipboard
*/
function copyToClipboard(text) {
navigator.clipboard.writeText(text);
// You could add a toast notification here
alert('Copied to clipboard!');
}
/**
* Reset form
*/
function resetForm() {
setVerificationInstructions(null);
setVerificationStatus(null);
setDomain('');
setWalletAddress('');
setError(null);
}
// Show current verified domain if exists
if (currentStatus?.hasVerifiedDomain) {
return (
<div className="domain-verification verified-container">
<h3> Domain Verified</h3>
<div className="verified-domain-display">
<strong>{currentStatus.domain}</strong>
<span className="verification-type">
{currentStatus.verificationType === 'blockchain' ? 'Blockchain' : 'DNS'}
</span>
</div>
<p className="verified-date">
Verified on {new Date(currentStatus.verifiedAt).toLocaleDateString()}
</p>
<button
className="secondary-button"
onClick={() => setCurrentStatus(null)}
>
Verify Another Domain
</button>
</div>
);
}
return (
<div className="domain-verification">
<h3>Verify Your Domain</h3>
<p className="description">
Prove ownership of a domain to display it on your profile
</p>
{error && (
<div className="error-message">
<span> {error}</span>
</div>
)}
{!verificationInstructions ? (
<div className="input-section">
<div className="form-group">
<label htmlFor="domain">Domain Name</label>
<input
id="domain"
type="text"
placeholder="yourdomain.com or anderson.aethex"
value={domain}
onChange={(e) => setDomain(e.target.value.toLowerCase().trim())}
disabled={loading}
className="domain-input"
/>
<small className="help-text">
Enter a traditional domain (e.g., dev.aethex.dev) or a .aethex blockchain domain
</small>
</div>
<button
onClick={requestVerification}
disabled={!domain || loading}
className="primary-button"
>
{loading ? 'Generating...' : 'Request Verification'}
</button>
</div>
) : (
<div className="instructions-section">
<h4>
{domain.endsWith('.aethex')
? 'Connect Your Wallet'
: `Add this DNS record to ${verificationInstructions.domain}:`
}
</h4>
{domain.endsWith('.aethex') ? (
// Blockchain verification
<div className="blockchain-verification">
<p>Connect the wallet that owns <strong>{domain}</strong></p>
<div className="form-group">
<label htmlFor="wallet">Wallet Address</label>
<input
id="wallet"
type="text"
placeholder="0x..."
value={walletAddress}
onChange={(e) => setWalletAddress(e.target.value.trim())}
disabled={loading}
className="wallet-input"
/>
</div>
<button
onClick={verifyDomain}
disabled={!walletAddress || loading}
className="primary-button verify-button"
>
{loading ? 'Verifying...' : 'Verify Ownership'}
</button>
</div>
) : (
// DNS verification
<div className="dns-verification">
<div className="dns-record">
<div className="record-field">
<strong>Type:</strong>
<span>{verificationInstructions.recordType}</span>
</div>
<div className="record-field">
<strong>Name:</strong>
<span>{verificationInstructions.recordName}</span>
</div>
<div className="record-field">
<strong>Value:</strong>
<div className="value-container">
<code>{verificationInstructions.recordValue}</code>
<button
onClick={() => copyToClipboard(verificationInstructions.recordValue)}
className="copy-button"
title="Copy to clipboard"
>
📋 Copy
</button>
</div>
</div>
</div>
<div className="help-section">
<p><strong>How to add DNS records:</strong></p>
<ol>
<li>Go to your domain's DNS settings (Google Domains, Cloudflare, etc.)</li>
<li>Add a new TXT record with the values above</li>
<li>Wait 5-10 minutes for DNS to propagate</li>
<li>Click "Verify Domain" below</li>
</ol>
<p className="expires-note">
This verification expires on {new Date(verificationInstructions.expiresAt).toLocaleDateString()}
</p>
</div>
<button
onClick={verifyDomain}
disabled={loading}
className="primary-button verify-button"
>
{loading ? 'Verifying...' : 'Verify Domain'}
</button>
{verificationStatus && (
<div className={`status-message ${verificationStatus.verified ? 'success' : 'error'}`}>
{verificationStatus.verified ? (
<span> Domain verified successfully!</span>
) : (
<span> {verificationStatus.error}</span>
)}
</div>
)}
</div>
)}
<button
onClick={resetForm}
className="secondary-button cancel-button"
disabled={loading}
>
Cancel
</button>
</div>
)}
</div>
);
}

View file

@ -0,0 +1,85 @@
.verified-domain-badge {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
background: linear-gradient(135deg, #d1fae5 0%, #a7f3d0 100%);
border: 2px solid #6ee7b7;
border-radius: 24px;
font-size: 14px;
font-weight: 500;
color: #065f46;
box-shadow: 0 2px 4px rgba(16, 185, 129, 0.2);
transition: all 0.2s;
}
.verified-domain-badge:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(16, 185, 129, 0.3);
}
.badge-content {
display: flex;
align-items: center;
gap: 8px;
}
.domain-text {
font-family: 'Courier New', monospace;
font-weight: 600;
color: #047857;
}
.checkmark {
display: inline-flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
background: #10b981;
color: white;
border-radius: 50%;
font-size: 12px;
font-weight: bold;
}
.blockchain-indicator {
font-size: 16px;
opacity: 0.8;
}
.verified-info {
font-size: 11px;
color: #047857;
opacity: 0.8;
margin-top: 4px;
}
/* Compact variant */
.verified-domain-badge.compact {
padding: 4px 12px;
font-size: 12px;
}
.verified-domain-badge.compact .checkmark {
width: 16px;
height: 16px;
font-size: 10px;
}
/* Dark mode support */
@media (prefers-color-scheme: dark) {
.verified-domain-badge {
background: linear-gradient(135deg, #064e3b 0%, #065f46 100%);
border-color: #047857;
color: #d1fae5;
}
.domain-text {
color: #a7f3d0;
}
.verified-info {
color: #a7f3d0;
}
}

View file

@ -0,0 +1,41 @@
import React from 'react';
import './VerifiedDomainBadge.css';
/**
* Displays verified domain badge on user profile
* @param {Object} props
* @param {string} props.verifiedDomain - The verified domain name
* @param {string} props.verificationType - Type of verification (dns or blockchain)
* @param {Date} props.verifiedAt - When the domain was verified
*/
export default function VerifiedDomainBadge({
verifiedDomain,
verificationType = 'dns',
verifiedAt
}) {
if (!verifiedDomain) return null;
return (
<div className="verified-domain-badge">
<div className="badge-content">
<span className="domain-text">{verifiedDomain}</span>
<span
className="checkmark"
title={`Verified via ${verificationType === 'blockchain' ? 'blockchain' : 'DNS'}`}
>
</span>
</div>
{verificationType === 'blockchain' && (
<span className="blockchain-indicator" title="Verified via blockchain">
</span>
)}
{verifiedAt && (
<div className="verified-info">
Verified {new Date(verifiedAt).toLocaleDateString()}
</div>
)}
</div>
);
}

5
src/frontend/index.css Normal file
View file

@ -0,0 +1,5 @@
body {
margin: 0;
padding: 0;
box-sizing: border-box;
}

13
src/frontend/index.html Normal file
View file

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>AeThex Passport - Domain Verification</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/main.jsx"></script>
</body>
</html>

10
src/frontend/main.jsx Normal file
View file

@ -0,0 +1,10 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import './index.css';
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<App />
</React.StrictMode>
);

1716
src/frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

21
src/frontend/package.json Normal file
View file

@ -0,0 +1,21 @@
{
"name": "aethex-passport-frontend",
"private": true,
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@types/react": "^18.2.43",
"@types/react-dom": "^18.2.17",
"@vitejs/plugin-react": "^4.2.1",
"vite": "^5.0.8"
}
}

View file

@ -0,0 +1,16 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
server: {
port: 5173,
proxy: {
'/api': {
target: 'http://localhost:3000',
changeOrigin: true
}
}
}
});

8
supabase/.gitignore vendored Normal file
View file

@ -0,0 +1,8 @@
# Supabase
.branches
.temp
# dotenvx
.env.keys
.env.local
.env.*.local

382
supabase/config.toml Normal file
View file

@ -0,0 +1,382 @@
# For detailed configuration reference documentation, visit:
# https://supabase.com/docs/guides/local-development/cli/config
# A string used to distinguish different Supabase projects on the same host. Defaults to the
# working directory name when running `supabase init`.
project_id = "AeThex-Connect"
[api]
enabled = true
# Port to use for the API URL.
port = 54321
# Schemas to expose in your API. Tables, views and stored procedures in this schema will get API
# endpoints. `public` and `graphql_public` schemas are included by default.
schemas = ["public", "graphql_public"]
# Extra schemas to add to the search_path of every request.
extra_search_path = ["public", "extensions"]
# The maximum number of rows returns from a view, table, or stored procedure. Limits payload size
# for accidental or malicious requests.
max_rows = 1000
[api.tls]
# Enable HTTPS endpoints locally using a self-signed certificate.
enabled = false
# Paths to self-signed certificate pair.
# cert_path = "../certs/my-cert.pem"
# key_path = "../certs/my-key.pem"
[db]
# Port to use for the local database URL.
port = 54322
# Port used by db diff command to initialize the shadow database.
shadow_port = 54320
# The database major version to use. This has to be the same as your remote database's. Run `SHOW
# server_version;` on the remote database to check.
major_version = 17
[db.pooler]
enabled = false
# Port to use for the local connection pooler.
port = 54329
# Specifies when a server connection can be reused by other clients.
# Configure one of the supported pooler modes: `transaction`, `session`.
pool_mode = "transaction"
# How many server connections to allow per user/database pair.
default_pool_size = 20
# Maximum number of client connections allowed.
max_client_conn = 100
# [db.vault]
# secret_key = "env(SECRET_VALUE)"
[db.migrations]
# If disabled, migrations will be skipped during a db push or reset.
enabled = true
# Specifies an ordered list of schema files that describe your database.
# Supports glob patterns relative to supabase directory: "./schemas/*.sql"
schema_paths = []
[db.seed]
# If enabled, seeds the database after migrations during a db reset.
enabled = true
# Specifies an ordered list of seed files to load during db reset.
# Supports glob patterns relative to supabase directory: "./seeds/*.sql"
sql_paths = ["./seed.sql"]
[db.network_restrictions]
# Enable management of network restrictions.
enabled = false
# List of IPv4 CIDR blocks allowed to connect to the database.
# Defaults to allow all IPv4 connections. Set empty array to block all IPs.
allowed_cidrs = ["0.0.0.0/0"]
# List of IPv6 CIDR blocks allowed to connect to the database.
# Defaults to allow all IPv6 connections. Set empty array to block all IPs.
allowed_cidrs_v6 = ["::/0"]
[realtime]
enabled = true
# Bind realtime via either IPv4 or IPv6. (default: IPv4)
# ip_version = "IPv6"
# The maximum length in bytes of HTTP request headers. (default: 4096)
# max_header_length = 4096
[studio]
enabled = true
# Port to use for Supabase Studio.
port = 54323
# External URL of the API server that frontend connects to.
api_url = "http://127.0.0.1"
# OpenAI API Key to use for Supabase AI in the Supabase Studio.
openai_api_key = "env(OPENAI_API_KEY)"
# Email testing server. Emails sent with the local dev setup are not actually sent - rather, they
# are monitored, and you can view the emails that would have been sent from the web interface.
[inbucket]
enabled = true
# Port to use for the email testing server web interface.
port = 54324
# Uncomment to expose additional ports for testing user applications that send emails.
# smtp_port = 54325
# pop3_port = 54326
# admin_email = "admin@email.com"
# sender_name = "Admin"
[storage]
enabled = true
# The maximum file size allowed (e.g. "5MB", "500KB").
file_size_limit = "50MiB"
# Uncomment to configure local storage buckets
# [storage.buckets.images]
# public = false
# file_size_limit = "50MiB"
# allowed_mime_types = ["image/png", "image/jpeg"]
# objects_path = "./images"
# Allow connections via S3 compatible clients
[storage.s3_protocol]
enabled = true
# Image transformation API is available to Supabase Pro plan.
# [storage.image_transformation]
# enabled = true
# Store analytical data in S3 for running ETL jobs over Iceberg Catalog
# This feature is only available on the hosted platform.
[storage.analytics]
enabled = false
max_namespaces = 5
max_tables = 10
max_catalogs = 2
# Analytics Buckets is available to Supabase Pro plan.
# [storage.analytics.buckets.my-warehouse]
# Store vector embeddings in S3 for large and durable datasets
# This feature is only available on the hosted platform.
[storage.vector]
enabled = false
max_buckets = 10
max_indexes = 5
# Vector Buckets is available to Supabase Pro plan.
# [storage.vector.buckets.documents-openai]
[auth]
enabled = true
# The base URL of your website. Used as an allow-list for redirects and for constructing URLs used
# in emails.
site_url = "http://127.0.0.1:3000"
# A list of *exact* URLs that auth providers are permitted to redirect to post authentication.
additional_redirect_urls = ["https://127.0.0.1:3000"]
# How long tokens are valid for, in seconds. Defaults to 3600 (1 hour), maximum 604,800 (1 week).
jwt_expiry = 3600
# JWT issuer URL. If not set, defaults to the local API URL (http://127.0.0.1:<port>/auth/v1).
# jwt_issuer = ""
# Path to JWT signing key. DO NOT commit your signing keys file to git.
# signing_keys_path = "./signing_keys.json"
# If disabled, the refresh token will never expire.
enable_refresh_token_rotation = true
# Allows refresh tokens to be reused after expiry, up to the specified interval in seconds.
# Requires enable_refresh_token_rotation = true.
refresh_token_reuse_interval = 10
# Allow/disallow new user signups to your project.
enable_signup = true
# Allow/disallow anonymous sign-ins to your project.
enable_anonymous_sign_ins = false
# Allow/disallow testing manual linking of accounts
enable_manual_linking = false
# Passwords shorter than this value will be rejected as weak. Minimum 6, recommended 8 or more.
minimum_password_length = 6
# Passwords that do not meet the following requirements will be rejected as weak. Supported values
# are: `letters_digits`, `lower_upper_letters_digits`, `lower_upper_letters_digits_symbols`
password_requirements = ""
[auth.rate_limit]
# Number of emails that can be sent per hour. Requires auth.email.smtp to be enabled.
email_sent = 2
# Number of SMS messages that can be sent per hour. Requires auth.sms to be enabled.
sms_sent = 30
# Number of anonymous sign-ins that can be made per hour per IP address. Requires enable_anonymous_sign_ins = true.
anonymous_users = 30
# Number of sessions that can be refreshed in a 5 minute interval per IP address.
token_refresh = 150
# Number of sign up and sign-in requests that can be made in a 5 minute interval per IP address (excludes anonymous users).
sign_in_sign_ups = 30
# Number of OTP / Magic link verifications that can be made in a 5 minute interval per IP address.
token_verifications = 30
# Number of Web3 logins that can be made in a 5 minute interval per IP address.
web3 = 30
# Configure one of the supported captcha providers: `hcaptcha`, `turnstile`.
# [auth.captcha]
# enabled = true
# provider = "hcaptcha"
# secret = ""
[auth.email]
# Allow/disallow new user signups via email to your project.
enable_signup = true
# If enabled, a user will be required to confirm any email change on both the old, and new email
# addresses. If disabled, only the new email is required to confirm.
double_confirm_changes = true
# If enabled, users need to confirm their email address before signing in.
enable_confirmations = false
# If enabled, users will need to reauthenticate or have logged in recently to change their password.
secure_password_change = false
# Controls the minimum amount of time that must pass before sending another signup confirmation or password reset email.
max_frequency = "1s"
# Number of characters used in the email OTP.
otp_length = 6
# Number of seconds before the email OTP expires (defaults to 1 hour).
otp_expiry = 3600
# Use a production-ready SMTP server
# [auth.email.smtp]
# enabled = true
# host = "smtp.sendgrid.net"
# port = 587
# user = "apikey"
# pass = "env(SENDGRID_API_KEY)"
# admin_email = "admin@email.com"
# sender_name = "Admin"
# Uncomment to customize email template
# [auth.email.template.invite]
# subject = "You have been invited"
# content_path = "./supabase/templates/invite.html"
# Uncomment to customize notification email template
# [auth.email.notification.password_changed]
# enabled = true
# subject = "Your password has been changed"
# content_path = "./templates/password_changed_notification.html"
[auth.sms]
# Allow/disallow new user signups via SMS to your project.
enable_signup = false
# If enabled, users need to confirm their phone number before signing in.
enable_confirmations = false
# Template for sending OTP to users
template = "Your code is {{ .Code }}"
# Controls the minimum amount of time that must pass before sending another sms otp.
max_frequency = "5s"
# Use pre-defined map of phone number to OTP for testing.
# [auth.sms.test_otp]
# 4152127777 = "123456"
# Configure logged in session timeouts.
# [auth.sessions]
# Force log out after the specified duration.
# timebox = "24h"
# Force log out if the user has been inactive longer than the specified duration.
# inactivity_timeout = "8h"
# This hook runs before a new user is created and allows developers to reject the request based on the incoming user object.
# [auth.hook.before_user_created]
# enabled = true
# uri = "pg-functions://postgres/auth/before-user-created-hook"
# This hook runs before a token is issued and allows you to add additional claims based on the authentication method used.
# [auth.hook.custom_access_token]
# enabled = true
# uri = "pg-functions://<database>/<schema>/<hook_name>"
# Configure one of the supported SMS providers: `twilio`, `twilio_verify`, `messagebird`, `textlocal`, `vonage`.
[auth.sms.twilio]
enabled = false
account_sid = ""
message_service_sid = ""
# DO NOT commit your Twilio auth token to git. Use environment variable substitution instead:
auth_token = "env(SUPABASE_AUTH_SMS_TWILIO_AUTH_TOKEN)"
# Multi-factor-authentication is available to Supabase Pro plan.
[auth.mfa]
# Control how many MFA factors can be enrolled at once per user.
max_enrolled_factors = 10
# Control MFA via App Authenticator (TOTP)
[auth.mfa.totp]
enroll_enabled = false
verify_enabled = false
# Configure MFA via Phone Messaging
[auth.mfa.phone]
enroll_enabled = false
verify_enabled = false
otp_length = 6
template = "Your code is {{ .Code }}"
max_frequency = "5s"
# Configure MFA via WebAuthn
# [auth.mfa.web_authn]
# enroll_enabled = true
# verify_enabled = true
# Use an external OAuth provider. The full list of providers are: `apple`, `azure`, `bitbucket`,
# `discord`, `facebook`, `github`, `gitlab`, `google`, `keycloak`, `linkedin_oidc`, `notion`, `twitch`,
# `twitter`, `slack`, `spotify`, `workos`, `zoom`.
[auth.external.apple]
enabled = false
client_id = ""
# DO NOT commit your OAuth provider secret to git. Use environment variable substitution instead:
secret = "env(SUPABASE_AUTH_EXTERNAL_APPLE_SECRET)"
# Overrides the default auth redirectUrl.
redirect_uri = ""
# Overrides the default auth provider URL. Used to support self-hosted gitlab, single-tenant Azure,
# or any other third-party OIDC providers.
url = ""
# If enabled, the nonce check will be skipped. Required for local sign in with Google auth.
skip_nonce_check = false
# If enabled, it will allow the user to successfully authenticate when the provider does not return an email address.
email_optional = false
# Allow Solana wallet holders to sign in to your project via the Sign in with Solana (SIWS, EIP-4361) standard.
# You can configure "web3" rate limit in the [auth.rate_limit] section and set up [auth.captcha] if self-hosting.
[auth.web3.solana]
enabled = false
# Use Firebase Auth as a third-party provider alongside Supabase Auth.
[auth.third_party.firebase]
enabled = false
# project_id = "my-firebase-project"
# Use Auth0 as a third-party provider alongside Supabase Auth.
[auth.third_party.auth0]
enabled = false
# tenant = "my-auth0-tenant"
# tenant_region = "us"
# Use AWS Cognito (Amplify) as a third-party provider alongside Supabase Auth.
[auth.third_party.aws_cognito]
enabled = false
# user_pool_id = "my-user-pool-id"
# user_pool_region = "us-east-1"
# Use Clerk as a third-party provider alongside Supabase Auth.
[auth.third_party.clerk]
enabled = false
# Obtain from https://clerk.com/setup/supabase
# domain = "example.clerk.accounts.dev"
# OAuth server configuration
[auth.oauth_server]
# Enable OAuth server functionality
enabled = false
# Path for OAuth consent flow UI
authorization_url_path = "/oauth/consent"
# Allow dynamic client registration
allow_dynamic_registration = false
[edge_runtime]
enabled = true
# Supported request policies: `oneshot`, `per_worker`.
# `per_worker` (default) — enables hot reload during local development.
# `oneshot` — fallback mode if hot reload causes issues (e.g. in large repos or with symlinks).
policy = "per_worker"
# Port to attach the Chrome inspector for debugging edge functions.
inspector_port = 8083
# The Deno major version to use.
deno_version = 2
# [edge_runtime.secrets]
# secret_key = "env(SECRET_VALUE)"
[analytics]
enabled = true
port = 54327
# Configure one of the supported backends: `postgres`, `bigquery`.
backend = "postgres"
# Experimental features may be deprecated any time
[experimental]
# Configures Postgres storage engine to use OrioleDB (S3)
orioledb_version = ""
# Configures S3 bucket URL, eg. <bucket_name>.s3-<region>.amazonaws.com
s3_host = "env(S3_HOST)"
# Configures S3 bucket region, eg. us-east-1
s3_region = "env(S3_REGION)"
# Configures AWS_ACCESS_KEY_ID for S3 bucket
s3_access_key = "env(S3_ACCESS_KEY)"
# Configures AWS_SECRET_ACCESS_KEY for S3 bucket
s3_secret_key = "env(S3_SECRET_KEY)"

View file

@ -0,0 +1,36 @@
-- Database Schema for Domain Verification Feature
-- Run this migration to create the necessary tables
-- Verification Requests Table
CREATE TABLE IF NOT EXISTS domain_verifications (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL,
domain VARCHAR(255) NOT NULL,
verification_token VARCHAR(64) NOT NULL,
verification_type VARCHAR(20) CHECK (verification_type IN ('dns', 'blockchain')) DEFAULT 'dns',
verified BOOLEAN DEFAULT false,
created_at TIMESTAMP DEFAULT NOW(),
verified_at TIMESTAMP,
expires_at TIMESTAMP DEFAULT NOW() + INTERVAL '7 days',
CONSTRAINT unique_user_domain UNIQUE(user_id, domain)
);
-- Create index for faster lookups
CREATE INDEX IF NOT EXISTS idx_domain_verifications_user_id ON domain_verifications(user_id);
CREATE INDEX IF NOT EXISTS idx_domain_verifications_domain ON domain_verifications(domain);
CREATE INDEX IF NOT EXISTS idx_domain_verifications_verified ON domain_verifications(verified);
-- Users Table Extensions
-- Assumes users table already exists
ALTER TABLE users
ADD COLUMN IF NOT EXISTS verified_domain VARCHAR(255),
ADD COLUMN IF NOT EXISTS domain_verified_at TIMESTAMP;
-- Create index for verified domains
CREATE INDEX IF NOT EXISTS idx_users_verified_domain ON users(verified_domain) WHERE verified_domain IS NOT NULL;
-- Comments for documentation
COMMENT ON TABLE domain_verifications IS 'Stores domain verification requests and their status';
COMMENT ON COLUMN domain_verifications.verification_token IS 'Unique token to be added to DNS TXT record or wallet address for blockchain verification';
COMMENT ON COLUMN domain_verifications.verification_type IS 'Type of verification: dns for traditional domains, blockchain for .aethex domains';
COMMENT ON COLUMN domain_verifications.expires_at IS 'Verification request expires after 7 days';

View file

@ -0,0 +1,147 @@
const request = require('supertest');
const app = require('../src/backend/server');
const db = require('../src/backend/database/db');
const {
generateDomainVerificationToken,
verifyDomainOwnership
} = require('../src/backend/utils/domainVerification');
describe('Domain Verification API', () => {
let authToken;
const mockUserId = 'test-user-123';
beforeAll(async () => {
// Setup: Create mock auth token
// In production, use actual JWT
authToken = 'mock-jwt-token';
});
afterAll(async () => {
// Cleanup database connections
await db.pool.end();
});
describe('POST /api/passport/domain/request-verification', () => {
it('should generate verification token for valid domain', async () => {
const response = await request(app)
.post('/api/passport/domain/request-verification')
.set('Authorization', `Bearer ${authToken}`)
.send({ domain: 'test.example.com' });
expect(response.status).toBe(200);
expect(response.body.success).toBe(true);
expect(response.body.verification).toHaveProperty('domain');
expect(response.body.verification).toHaveProperty('recordType', 'TXT');
expect(response.body.verification).toHaveProperty('recordName', '_aethex-verify');
expect(response.body.verification.recordValue).toMatch(/^aethex-verification=/);
});
it('should reject invalid domain format', async () => {
const response = await request(app)
.post('/api/passport/domain/request-verification')
.set('Authorization', `Bearer ${authToken}`)
.send({ domain: 'invalid domain!' });
expect(response.status).toBe(400);
expect(response.body.success).toBe(false);
});
it('should require authentication', async () => {
const response = await request(app)
.post('/api/passport/domain/request-verification')
.send({ domain: 'test.example.com' });
expect(response.status).toBe(401);
});
});
describe('POST /api/passport/domain/verify', () => {
it('should verify domain with correct DNS record', async () => {
// This test requires a real domain with TXT record
// Skip in CI/CD or use mock
const response = await request(app)
.post('/api/passport/domain/verify')
.set('Authorization', `Bearer ${authToken}`)
.send({ domain: 'test.example.com' });
expect(response.status).toBeOneOf([200, 400]);
expect(response.body).toHaveProperty('verified');
});
it('should require wallet address for .aethex domains', async () => {
const response = await request(app)
.post('/api/passport/domain/verify')
.set('Authorization', `Bearer ${authToken}`)
.send({ domain: 'test.aethex' });
expect(response.status).toBe(400);
expect(response.body.error).toMatch(/wallet address/i);
});
});
describe('GET /api/passport/domain/status', () => {
it('should return verification status', async () => {
const response = await request(app)
.get('/api/passport/domain/status')
.set('Authorization', `Bearer ${authToken}`);
expect(response.status).toBe(200);
expect(response.body).toHaveProperty('hasVerifiedDomain');
});
});
});
describe('Domain Verification Utils', () => {
const testUserId = 'test-user-456';
const testDomain = 'test.example.com';
describe('generateDomainVerificationToken', () => {
it('should generate unique tokens for same domain', async () => {
const token1 = await generateDomainVerificationToken(testUserId, testDomain);
const token2 = await generateDomainVerificationToken(testUserId, testDomain);
expect(token1.recordValue).not.toBe(token2.recordValue);
expect(token1.domain).toBe(testDomain);
expect(token1.recordType).toBe('TXT');
});
it('should generate proper DNS record format', async () => {
const result = await generateDomainVerificationToken(testUserId, testDomain);
expect(result.fullRecord).toMatch(
/^_aethex-verify\.test\.example\.com TXT "aethex-verification=.{64}"$/
);
});
});
describe('verifyDomainOwnership', () => {
it('should return error if no verification request exists', async () => {
const result = await verifyDomainOwnership('nonexistent-user', 'nonexistent.com');
expect(result.verified).toBe(false);
expect(result.error).toMatch(/no verification request/i);
});
it('should check DNS TXT records', async () => {
// Generate a verification token first
await generateDomainVerificationToken(testUserId, testDomain);
// Attempt verification (will fail unless DNS is actually set up)
const result = await verifyDomainOwnership(testUserId, testDomain);
expect(result).toHaveProperty('verified');
expect(result.verified).toBe(false); // Expected to fail without real DNS
});
});
});
// Custom Jest matcher
expect.extend({
toBeOneOf(received, expected) {
const pass = expected.includes(received);
return {
pass,
message: () => `expected ${received} to be one of ${expected.join(', ')}`
};
}
});