Spaces:
Runtime error
Runtime error
cz4ehs commited on
Commit ·
03cdf80
1
Parent(s): 942a4ce
Deploy Zone.ID Domain API 2025-12-07
Browse files- .gitattributes +2 -35
- Dockerfile +54 -0
- README.md +90 -5
- lib/account-creator.js +389 -0
- lib/autz-account.js +104 -0
- lib/remote-browser.js +53 -0
- lib/temp-email.js +64 -0
- lib/token-scheduler.js +149 -0
- lib/zoneid-api.js +180 -0
- package-lock.json +0 -0
- package.json +31 -0
- prisma/migrations/20251207094931_init/migration.sql +96 -0
- prisma/migrations/migration_lock.toml +3 -0
- prisma/schema.prisma +74 -0
- src/routes/accounts.js +310 -0
- src/routes/dns.js +421 -0
- src/routes/domains.js +283 -0
- src/server.js +126 -0
.gitattributes
CHANGED
|
@@ -1,35 +1,2 @@
|
|
| 1 |
-
*.
|
| 2 |
-
*.
|
| 3 |
-
*.bin filter=lfs diff=lfs merge=lfs -text
|
| 4 |
-
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
| 5 |
-
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
| 6 |
-
*.ftz filter=lfs diff=lfs merge=lfs -text
|
| 7 |
-
*.gz filter=lfs diff=lfs merge=lfs -text
|
| 8 |
-
*.h5 filter=lfs diff=lfs merge=lfs -text
|
| 9 |
-
*.joblib filter=lfs diff=lfs merge=lfs -text
|
| 10 |
-
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
| 11 |
-
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
| 12 |
-
*.model filter=lfs diff=lfs merge=lfs -text
|
| 13 |
-
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
| 14 |
-
*.npy filter=lfs diff=lfs merge=lfs -text
|
| 15 |
-
*.npz filter=lfs diff=lfs merge=lfs -text
|
| 16 |
-
*.onnx filter=lfs diff=lfs merge=lfs -text
|
| 17 |
-
*.ot filter=lfs diff=lfs merge=lfs -text
|
| 18 |
-
*.parquet filter=lfs diff=lfs merge=lfs -text
|
| 19 |
-
*.pb filter=lfs diff=lfs merge=lfs -text
|
| 20 |
-
*.pickle filter=lfs diff=lfs merge=lfs -text
|
| 21 |
-
*.pkl filter=lfs diff=lfs merge=lfs -text
|
| 22 |
-
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 23 |
-
*.pth filter=lfs diff=lfs merge=lfs -text
|
| 24 |
-
*.rar filter=lfs diff=lfs merge=lfs -text
|
| 25 |
-
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 26 |
-
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
| 27 |
-
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
| 28 |
-
*.tar filter=lfs diff=lfs merge=lfs -text
|
| 29 |
-
*.tflite filter=lfs diff=lfs merge=lfs -text
|
| 30 |
-
*.tgz filter=lfs diff=lfs merge=lfs -text
|
| 31 |
-
*.wasm filter=lfs diff=lfs merge=lfs -text
|
| 32 |
-
*.xz filter=lfs diff=lfs merge=lfs -text
|
| 33 |
-
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
-
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
-
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
| 1 |
+
*.js linguist-language=JavaScript
|
| 2 |
+
*.json linguist-language=JSON
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Dockerfile
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM node:20-slim
|
| 2 |
+
|
| 3 |
+
RUN apt-get update && apt-get install -y \
|
| 4 |
+
chromium \
|
| 5 |
+
fonts-liberation \
|
| 6 |
+
libasound2 \
|
| 7 |
+
libatk-bridge2.0-0 \
|
| 8 |
+
libatk1.0-0 \
|
| 9 |
+
libatspi2.0-0 \
|
| 10 |
+
libcups2 \
|
| 11 |
+
libdbus-1-3 \
|
| 12 |
+
libdrm2 \
|
| 13 |
+
libgbm1 \
|
| 14 |
+
libgtk-3-0 \
|
| 15 |
+
libnspr4 \
|
| 16 |
+
libnss3 \
|
| 17 |
+
libxcomposite1 \
|
| 18 |
+
libxdamage1 \
|
| 19 |
+
libxfixes3 \
|
| 20 |
+
libxkbcommon0 \
|
| 21 |
+
libxrandr2 \
|
| 22 |
+
xdg-utils \
|
| 23 |
+
openssl \
|
| 24 |
+
ca-certificates \
|
| 25 |
+
git \
|
| 26 |
+
--no-install-recommends \
|
| 27 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 28 |
+
|
| 29 |
+
RUN useradd -m -u 1000 user
|
| 30 |
+
USER user
|
| 31 |
+
|
| 32 |
+
ENV HOME=/home/user
|
| 33 |
+
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true
|
| 34 |
+
ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium
|
| 35 |
+
ENV CHROMIUM_PATH=/usr/bin/chromium
|
| 36 |
+
|
| 37 |
+
WORKDIR /home/user/app
|
| 38 |
+
|
| 39 |
+
COPY --chown=user package*.json ./
|
| 40 |
+
|
| 41 |
+
RUN npm ci --only=production
|
| 42 |
+
|
| 43 |
+
COPY --chown=user prisma ./prisma/
|
| 44 |
+
|
| 45 |
+
RUN npx prisma generate
|
| 46 |
+
|
| 47 |
+
COPY --chown=user . .
|
| 48 |
+
|
| 49 |
+
ENV PORT=7860
|
| 50 |
+
ENV HOST=0.0.0.0
|
| 51 |
+
|
| 52 |
+
EXPOSE 7860
|
| 53 |
+
|
| 54 |
+
CMD ["node", "src/server.js"]
|
README.md
CHANGED
|
@@ -1,10 +1,95 @@
|
|
| 1 |
---
|
| 2 |
-
title: Domain
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: docker
|
|
|
|
| 7 |
pinned: false
|
| 8 |
---
|
| 9 |
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: Zone.ID Domain Registration API
|
| 3 |
+
emoji: 🌐
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: purple
|
| 6 |
sdk: docker
|
| 7 |
+
app_port: 7860
|
| 8 |
pinned: false
|
| 9 |
---
|
| 10 |
|
| 11 |
+
# Zone.ID Domain Registration API
|
| 12 |
+
|
| 13 |
+
A REST API for automating zone.id and nett.to domain registration with automatic account management.
|
| 14 |
+
|
| 15 |
+
## Features
|
| 16 |
+
|
| 17 |
+
- **Domain Registration**: Create subdomains on zone.id and nett.to
|
| 18 |
+
- **DNS Management**: Create, update, and delete A and CNAME records
|
| 19 |
+
- **Auto-Account Creation**: Automatically creates new accounts when domain limits are reached
|
| 20 |
+
- **Token Auto-Refresh**: Automatically refreshes tokens every 6 hours to prevent expiration
|
| 21 |
+
|
| 22 |
+
## API Endpoints
|
| 23 |
+
|
| 24 |
+
All endpoints require `X-API-Key` header for authentication.
|
| 25 |
+
|
| 26 |
+
### Health Check
|
| 27 |
+
- `GET /health` - Check API status
|
| 28 |
+
- `GET /docs` - Swagger API documentation
|
| 29 |
+
|
| 30 |
+
### Accounts (`/api/accounts`)
|
| 31 |
+
- `GET /` - List all accounts
|
| 32 |
+
- `POST /` - Create a new account
|
| 33 |
+
- `GET /:id` - Get account details
|
| 34 |
+
- `PATCH /:id` - Update account
|
| 35 |
+
- `DELETE /:id` - Delete account
|
| 36 |
+
- `POST /:id/refresh-token` - Manually refresh account token
|
| 37 |
+
- `POST /refresh-all-tokens` - Refresh tokens for all accounts
|
| 38 |
+
|
| 39 |
+
### Domains (`/api/domains`)
|
| 40 |
+
- `GET /` - List all domains
|
| 41 |
+
- `POST /` - Register a new domain
|
| 42 |
+
- `GET /:id` - Get domain details
|
| 43 |
+
- `DELETE /:id` - Delete domain
|
| 44 |
+
|
| 45 |
+
### DNS Records (`/api/dns`)
|
| 46 |
+
- `GET /:domainId/records` - List DNS records
|
| 47 |
+
- `POST /:domainId/records` - Create DNS record (A/CNAME)
|
| 48 |
+
- `PUT /:domainId/records/:recordId` - Update record
|
| 49 |
+
- `DELETE /:domainId/records/:recordId` - Delete record
|
| 50 |
+
- `POST /:domainId/sync` - Sync records from Zone.ID
|
| 51 |
+
|
| 52 |
+
## Environment Variables
|
| 53 |
+
|
| 54 |
+
Required:
|
| 55 |
+
- `DATABASE_URL` - PostgreSQL connection string
|
| 56 |
+
|
| 57 |
+
Optional:
|
| 58 |
+
- `PORT` - Server port (default: 7860)
|
| 59 |
+
- `HOST` - Server host (default: 0.0.0.0)
|
| 60 |
+
|
| 61 |
+
## Usage Example
|
| 62 |
+
|
| 63 |
+
```bash
|
| 64 |
+
# Create a domain
|
| 65 |
+
curl -X POST https://your-space.hf.space/api/domains \
|
| 66 |
+
-H "X-API-Key: your-api-key" \
|
| 67 |
+
-H "Content-Type: application/json" \
|
| 68 |
+
-d '{"subdomain": "myapp", "suffix": "zone.id"}'
|
| 69 |
+
|
| 70 |
+
# Create an A record
|
| 71 |
+
curl -X POST https://your-space.hf.space/api/dns/{domainId}/records \
|
| 72 |
+
-H "X-API-Key: your-api-key" \
|
| 73 |
+
-H "Content-Type: application/json" \
|
| 74 |
+
-d '{"type": "A", "hostname": "@", "content": "1.2.3.4"}'
|
| 75 |
+
|
| 76 |
+
# Create a CNAME record
|
| 77 |
+
curl -X POST https://your-space.hf.space/api/dns/{domainId}/records \
|
| 78 |
+
-H "X-API-Key: your-api-key" \
|
| 79 |
+
-H "Content-Type: application/json" \
|
| 80 |
+
-d '{"type": "CNAME", "hostname": "www", "content": "example.com"}'
|
| 81 |
+
```
|
| 82 |
+
|
| 83 |
+
## Token Auto-Refresh
|
| 84 |
+
|
| 85 |
+
The API automatically refreshes Zone.ID tokens every 6 hours to prevent the 3-day expiration. You can also manually trigger a refresh:
|
| 86 |
+
|
| 87 |
+
```bash
|
| 88 |
+
# Refresh specific account token
|
| 89 |
+
curl -X POST https://your-space.hf.space/api/accounts/{accountId}/refresh-token \
|
| 90 |
+
-H "X-API-Key: your-api-key"
|
| 91 |
+
|
| 92 |
+
# Refresh all account tokens
|
| 93 |
+
curl -X POST https://your-space.hf.space/api/accounts/refresh-all-tokens \
|
| 94 |
+
-H "X-API-Key: your-api-key"
|
| 95 |
+
```
|
lib/account-creator.js
ADDED
|
@@ -0,0 +1,389 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const { PrismaClient } = require('@prisma/client');
|
| 2 |
+
const bcrypt = require('bcrypt');
|
| 3 |
+
const axios = require('axios');
|
| 4 |
+
const cheerio = require('cheerio');
|
| 5 |
+
const { generateEmail, waitForEmail, deleteAddress } = require('./temp-email');
|
| 6 |
+
const { createAutzAccount } = require('./autz-account');
|
| 7 |
+
|
| 8 |
+
const prisma = new PrismaClient();
|
| 9 |
+
|
| 10 |
+
const AUTZ_API = 'https://autz.org/api';
|
| 11 |
+
const ZONEID_API = 'https://my.zone.id/api';
|
| 12 |
+
|
| 13 |
+
function generatePassword(length = 16) {
|
| 14 |
+
const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz23456789!@#$%';
|
| 15 |
+
let password = '';
|
| 16 |
+
for (let i = 0; i < length; i++) {
|
| 17 |
+
password += chars.charAt(Math.floor(Math.random() * chars.length));
|
| 18 |
+
}
|
| 19 |
+
return password;
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
function generateName() {
|
| 23 |
+
const firstNames = ['Alex', 'Jordan', 'Taylor', 'Morgan', 'Casey', 'Riley', 'Quinn', 'Avery', 'Parker', 'Drew'];
|
| 24 |
+
const lastNames = ['Smith', 'Johnson', 'Williams', 'Brown', 'Jones', 'Garcia', 'Miller', 'Davis', 'Wilson', 'Moore'];
|
| 25 |
+
return `${firstNames[Math.floor(Math.random() * firstNames.length)]} ${lastNames[Math.floor(Math.random() * lastNames.length)]}`;
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
function generatePhone() {
|
| 29 |
+
const prefix = '+628';
|
| 30 |
+
let number = '';
|
| 31 |
+
for (let i = 0; i < 10; i++) {
|
| 32 |
+
number += Math.floor(Math.random() * 10);
|
| 33 |
+
}
|
| 34 |
+
return prefix + number;
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
async function extractOtpFromEmail(emailHtml) {
|
| 38 |
+
const $ = cheerio.load(emailHtml);
|
| 39 |
+
const text = $.text();
|
| 40 |
+
|
| 41 |
+
const otpMatch = text.match(/\b(\d{6})\b/);
|
| 42 |
+
if (otpMatch) {
|
| 43 |
+
return otpMatch[1];
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
const codeMatch = text.match(/code[:\s]+(\d{6})/i);
|
| 47 |
+
if (codeMatch) {
|
| 48 |
+
return codeMatch[1];
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
return null;
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
async function activateAccount(userTokenId, otp) {
|
| 55 |
+
const response = await axios.post(`${AUTZ_API}/activate`, {
|
| 56 |
+
user_token_id: userTokenId,
|
| 57 |
+
otp
|
| 58 |
+
});
|
| 59 |
+
return response.data;
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
async function loginToAutz(email, password) {
|
| 63 |
+
const response = await axios.post(`${AUTZ_API}/login`, {
|
| 64 |
+
email,
|
| 65 |
+
password
|
| 66 |
+
}, {
|
| 67 |
+
headers: {
|
| 68 |
+
'Content-Type': 'application/json',
|
| 69 |
+
'Origin': 'https://autz.org',
|
| 70 |
+
'Referer': 'https://autz.org/'
|
| 71 |
+
}
|
| 72 |
+
});
|
| 73 |
+
return response.data;
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
async function getZoneIdRefreshToken(email, password, retryCount = 0) {
|
| 77 |
+
const puppeteer = require('puppeteer-core');
|
| 78 |
+
const { solveTurnstile } = require('./autz-account');
|
| 79 |
+
|
| 80 |
+
const CHROMIUM_PATH = process.env.CHROMIUM_PATH || process.env.PUPPETEER_EXECUTABLE_PATH || '/nix/store/khk7xpgsm5insk81azy9d560yq4npf77-chromium-131.0.6778.204/bin/chromium';
|
| 81 |
+
const ONBOARDING_URL = 'https://autz.org/onboarding/qinw2ix?callback_url=https%3A%2F%2Fmy.zone.id%2F';
|
| 82 |
+
const MAX_RETRIES = 2;
|
| 83 |
+
|
| 84 |
+
console.log(`[OAuth] Starting Zone.ID OAuth flow for ${email} (attempt ${retryCount + 1}/${MAX_RETRIES + 1})`);
|
| 85 |
+
|
| 86 |
+
let turnstileToken;
|
| 87 |
+
try {
|
| 88 |
+
console.log('[OAuth] Solving CAPTCHA...');
|
| 89 |
+
turnstileToken = await solveTurnstile();
|
| 90 |
+
console.log('[OAuth] CAPTCHA solved successfully');
|
| 91 |
+
} catch (err) {
|
| 92 |
+
console.error('[OAuth] CAPTCHA solve failed:', err.message);
|
| 93 |
+
if (retryCount < MAX_RETRIES) {
|
| 94 |
+
console.log('[OAuth] Retrying after CAPTCHA failure...');
|
| 95 |
+
await new Promise(r => setTimeout(r, 5000));
|
| 96 |
+
return getZoneIdRefreshToken(email, password, retryCount + 1);
|
| 97 |
+
}
|
| 98 |
+
throw new Error('Failed to solve CAPTCHA after retries');
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
console.log('[OAuth] Launching browser...');
|
| 102 |
+
const browser = await puppeteer.launch({
|
| 103 |
+
executablePath: CHROMIUM_PATH,
|
| 104 |
+
headless: true,
|
| 105 |
+
args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage', '--disable-gpu']
|
| 106 |
+
});
|
| 107 |
+
|
| 108 |
+
let refreshToken = null;
|
| 109 |
+
let authorizeRequestSeen = false;
|
| 110 |
+
|
| 111 |
+
try {
|
| 112 |
+
const page = await browser.newPage();
|
| 113 |
+
await page.setViewport({ width: 1280, height: 800 });
|
| 114 |
+
|
| 115 |
+
page.on('response', async (response) => {
|
| 116 |
+
const url = response.url();
|
| 117 |
+
if (url.includes('my.zone.id/api/authorize')) {
|
| 118 |
+
authorizeRequestSeen = true;
|
| 119 |
+
console.log('[OAuth] Detected Zone.ID authorize request');
|
| 120 |
+
try {
|
| 121 |
+
const headers = response.headers();
|
| 122 |
+
const setCookie = headers['set-cookie'];
|
| 123 |
+
if (setCookie && setCookie.includes('rt_subzoneid=')) {
|
| 124 |
+
const match = setCookie.match(/rt_subzoneid=([^;]+)/);
|
| 125 |
+
if (match) {
|
| 126 |
+
refreshToken = `rt_subzoneid=${match[1]}`;
|
| 127 |
+
console.log('[OAuth] Captured refresh token from authorize response!');
|
| 128 |
+
}
|
| 129 |
+
}
|
| 130 |
+
} catch (e) {
|
| 131 |
+
console.log('[OAuth] Error extracting token from response:', e.message);
|
| 132 |
+
}
|
| 133 |
+
}
|
| 134 |
+
});
|
| 135 |
+
|
| 136 |
+
await page.evaluateOnNewDocument((token) => {
|
| 137 |
+
window.turnstile = {
|
| 138 |
+
render: (container, params) => {
|
| 139 |
+
if (params.callback) setTimeout(() => params.callback(token), 500);
|
| 140 |
+
return 'widget-id';
|
| 141 |
+
},
|
| 142 |
+
getResponse: () => token,
|
| 143 |
+
reset: () => {}
|
| 144 |
+
};
|
| 145 |
+
}, turnstileToken);
|
| 146 |
+
|
| 147 |
+
console.log('[OAuth] Navigating to onboarding URL...');
|
| 148 |
+
await page.goto(ONBOARDING_URL, { waitUntil: 'networkidle2', timeout: 60000 });
|
| 149 |
+
|
| 150 |
+
try {
|
| 151 |
+
await page.waitForSelector('#login-modal', { visible: true, timeout: 30000 });
|
| 152 |
+
console.log('[OAuth] Login modal visible');
|
| 153 |
+
} catch (err) {
|
| 154 |
+
console.log('[OAuth] Login modal not found, checking page state...');
|
| 155 |
+
const currentUrl = page.url();
|
| 156 |
+
console.log('[OAuth] Current URL:', currentUrl);
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
+
console.log('[OAuth] Entering email...');
|
| 160 |
+
const emailInput = await page.$('input[type="email"]');
|
| 161 |
+
if (!emailInput) {
|
| 162 |
+
throw new Error('Email input not found on page');
|
| 163 |
+
}
|
| 164 |
+
await emailInput.type(email, { delay: 30 });
|
| 165 |
+
|
| 166 |
+
await page.evaluate((token) => {
|
| 167 |
+
if (window.onVerifiedCaptcha) window.onVerifiedCaptcha(token);
|
| 168 |
+
const btn = document.querySelector('button.btn-primary');
|
| 169 |
+
if (btn) btn.disabled = false;
|
| 170 |
+
}, turnstileToken);
|
| 171 |
+
|
| 172 |
+
await new Promise(r => setTimeout(r, 1000));
|
| 173 |
+
await page.click('button.btn-primary');
|
| 174 |
+
console.log('[OAuth] Submitted email, waiting for password field...');
|
| 175 |
+
|
| 176 |
+
let passwordFieldFound = false;
|
| 177 |
+
for (let i = 0; i < 10; i++) {
|
| 178 |
+
await new Promise(r => setTimeout(r, 1000));
|
| 179 |
+
const passwordInput = await page.$('input[type="password"]');
|
| 180 |
+
if (passwordInput) {
|
| 181 |
+
passwordFieldFound = true;
|
| 182 |
+
console.log('[OAuth] Password field found');
|
| 183 |
+
break;
|
| 184 |
+
}
|
| 185 |
+
const pageText = await page.evaluate(() => document.body.innerText);
|
| 186 |
+
if (pageText.includes('Password') || pageText.includes('password')) {
|
| 187 |
+
passwordFieldFound = true;
|
| 188 |
+
break;
|
| 189 |
+
}
|
| 190 |
+
}
|
| 191 |
+
|
| 192 |
+
if (!passwordFieldFound) {
|
| 193 |
+
const pageText = await page.evaluate(() => document.body.innerText);
|
| 194 |
+
console.log('[OAuth] Page content after email submit:', pageText.substring(0, 500));
|
| 195 |
+
throw new Error('Password field did not appear - account may not be properly activated');
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
+
const passwordInput = await page.$('input[type="password"]');
|
| 199 |
+
if (passwordInput) {
|
| 200 |
+
console.log('[OAuth] Entering password...');
|
| 201 |
+
await passwordInput.type(password, { delay: 30 });
|
| 202 |
+
await new Promise(r => setTimeout(r, 500));
|
| 203 |
+
|
| 204 |
+
console.log('[OAuth] Clicking login button...');
|
| 205 |
+
await Promise.all([
|
| 206 |
+
page.waitForNavigation({ waitUntil: 'networkidle0', timeout: 45000 }).catch(err => {
|
| 207 |
+
console.log('[OAuth] Navigation timeout/error:', err.message);
|
| 208 |
+
}),
|
| 209 |
+
page.click('button.btn-primary')
|
| 210 |
+
]);
|
| 211 |
+
|
| 212 |
+
console.log('[OAuth] Login submitted, waiting for redirect...');
|
| 213 |
+
await new Promise(r => setTimeout(r, 5000));
|
| 214 |
+
}
|
| 215 |
+
|
| 216 |
+
if (!refreshToken) {
|
| 217 |
+
console.log('[OAuth] Checking cookies for refresh token...');
|
| 218 |
+
const cookies = await page.cookies('https://my.zone.id');
|
| 219 |
+
for (const cookie of cookies) {
|
| 220 |
+
if (cookie.name === 'rt_subzoneid') {
|
| 221 |
+
refreshToken = `rt_subzoneid=${cookie.value}`;
|
| 222 |
+
console.log('[OAuth] Captured refresh token from cookies!');
|
| 223 |
+
break;
|
| 224 |
+
}
|
| 225 |
+
}
|
| 226 |
+
}
|
| 227 |
+
|
| 228 |
+
if (!refreshToken) {
|
| 229 |
+
const pageText = await page.evaluate(() => document.body.innerText);
|
| 230 |
+
if (pageText.includes('Choose account') || pageText.includes('Select account')) {
|
| 231 |
+
console.log('[OAuth] Account selection page detected, selecting account...');
|
| 232 |
+
|
| 233 |
+
await page.evaluate((emailStr) => {
|
| 234 |
+
const emailPrefix = emailStr.split('@')[0];
|
| 235 |
+
const elements = document.querySelectorAll('div, button, a, li, span');
|
| 236 |
+
for (const el of elements) {
|
| 237 |
+
if (el.textContent && el.textContent.includes(emailPrefix)) {
|
| 238 |
+
el.click();
|
| 239 |
+
break;
|
| 240 |
+
}
|
| 241 |
+
}
|
| 242 |
+
}, email);
|
| 243 |
+
|
| 244 |
+
await new Promise(r => setTimeout(r, 5000));
|
| 245 |
+
|
| 246 |
+
const cookies = await page.cookies('https://my.zone.id');
|
| 247 |
+
for (const cookie of cookies) {
|
| 248 |
+
if (cookie.name === 'rt_subzoneid') {
|
| 249 |
+
refreshToken = `rt_subzoneid=${cookie.value}`;
|
| 250 |
+
console.log('[OAuth] Captured refresh token after account selection!');
|
| 251 |
+
break;
|
| 252 |
+
}
|
| 253 |
+
}
|
| 254 |
+
}
|
| 255 |
+
}
|
| 256 |
+
|
| 257 |
+
if (!refreshToken && !authorizeRequestSeen) {
|
| 258 |
+
console.log('[OAuth] Authorize request not seen, trying direct navigation to my.zone.id...');
|
| 259 |
+
await page.goto('https://my.zone.id/', { waitUntil: 'networkidle2', timeout: 30000 });
|
| 260 |
+
await new Promise(r => setTimeout(r, 3000));
|
| 261 |
+
|
| 262 |
+
const cookies = await page.cookies('https://my.zone.id');
|
| 263 |
+
for (const cookie of cookies) {
|
| 264 |
+
if (cookie.name === 'rt_subzoneid') {
|
| 265 |
+
refreshToken = `rt_subzoneid=${cookie.value}`;
|
| 266 |
+
console.log('[OAuth] Captured refresh token from my.zone.id navigation!');
|
| 267 |
+
break;
|
| 268 |
+
}
|
| 269 |
+
}
|
| 270 |
+
}
|
| 271 |
+
|
| 272 |
+
if (!refreshToken) {
|
| 273 |
+
const finalUrl = page.url();
|
| 274 |
+
console.log('[OAuth] Final URL:', finalUrl);
|
| 275 |
+
console.log('[OAuth] Authorize request seen:', authorizeRequestSeen);
|
| 276 |
+
}
|
| 277 |
+
|
| 278 |
+
} finally {
|
| 279 |
+
await browser.close();
|
| 280 |
+
}
|
| 281 |
+
|
| 282 |
+
if (!refreshToken && retryCount < MAX_RETRIES) {
|
| 283 |
+
console.log('[OAuth] Retrying OAuth flow...');
|
| 284 |
+
await new Promise(r => setTimeout(r, 3000));
|
| 285 |
+
return getZoneIdRefreshToken(email, password, retryCount + 1);
|
| 286 |
+
}
|
| 287 |
+
|
| 288 |
+
if (refreshToken) {
|
| 289 |
+
console.log('[OAuth] Successfully obtained refresh token');
|
| 290 |
+
} else {
|
| 291 |
+
console.log('[OAuth] Failed to obtain refresh token after all attempts');
|
| 292 |
+
}
|
| 293 |
+
|
| 294 |
+
return refreshToken;
|
| 295 |
+
}
|
| 296 |
+
|
| 297 |
+
async function createAndActivateAccount() {
|
| 298 |
+
console.log('Starting auto-account creation...');
|
| 299 |
+
|
| 300 |
+
const emailData = await generateEmail('zoneid');
|
| 301 |
+
const email = emailData.address;
|
| 302 |
+
console.log(`Generated temp email: ${email}`);
|
| 303 |
+
|
| 304 |
+
const password = generatePassword();
|
| 305 |
+
const name = generateName();
|
| 306 |
+
const phone = generatePhone();
|
| 307 |
+
|
| 308 |
+
console.log('Creating Autz.org account...');
|
| 309 |
+
const accountResult = await createAutzAccount(email, password, name, phone);
|
| 310 |
+
|
| 311 |
+
if (!accountResult.success || !accountResult.userTokenId) {
|
| 312 |
+
await deleteAddress(email);
|
| 313 |
+
throw new Error('Failed to create Autz.org account');
|
| 314 |
+
}
|
| 315 |
+
|
| 316 |
+
console.log('Waiting for verification email...');
|
| 317 |
+
const verificationEmail = await waitForEmail(email, 180000, 5000);
|
| 318 |
+
|
| 319 |
+
const otp = await extractOtpFromEmail(verificationEmail.html || verificationEmail.text);
|
| 320 |
+
if (!otp) {
|
| 321 |
+
await deleteAddress(email);
|
| 322 |
+
throw new Error('Could not extract OTP from email');
|
| 323 |
+
}
|
| 324 |
+
|
| 325 |
+
console.log(`Activating account with OTP: ${otp}`);
|
| 326 |
+
await activateAccount(accountResult.userTokenId, otp);
|
| 327 |
+
|
| 328 |
+
console.log('Getting Zone.ID refresh token via OAuth flow...');
|
| 329 |
+
let refreshToken = null;
|
| 330 |
+
try {
|
| 331 |
+
refreshToken = await getZoneIdRefreshToken(email, password);
|
| 332 |
+
if (refreshToken) {
|
| 333 |
+
console.log('Successfully obtained Zone.ID refresh token');
|
| 334 |
+
} else {
|
| 335 |
+
await deleteAddress(email);
|
| 336 |
+
throw new Error('Could not obtain Zone.ID refresh token - account created but not usable');
|
| 337 |
+
}
|
| 338 |
+
} catch (err) {
|
| 339 |
+
console.error('Error getting Zone.ID refresh token:', err.message);
|
| 340 |
+
await deleteAddress(email);
|
| 341 |
+
throw err;
|
| 342 |
+
}
|
| 343 |
+
|
| 344 |
+
const passwordHash = await bcrypt.hash(password, 10);
|
| 345 |
+
|
| 346 |
+
const account = await prisma.account.create({
|
| 347 |
+
data: {
|
| 348 |
+
email,
|
| 349 |
+
passwordHash,
|
| 350 |
+
name,
|
| 351 |
+
phone,
|
| 352 |
+
refreshToken,
|
| 353 |
+
status: 'active',
|
| 354 |
+
domainCount: 0
|
| 355 |
+
}
|
| 356 |
+
});
|
| 357 |
+
|
| 358 |
+
await deleteAddress(email);
|
| 359 |
+
|
| 360 |
+
console.log(`Account created successfully: ${account.id}`);
|
| 361 |
+
|
| 362 |
+
return account;
|
| 363 |
+
}
|
| 364 |
+
|
| 365 |
+
async function getOrCreateAvailableAccount() {
|
| 366 |
+
let account = await prisma.account.findFirst({
|
| 367 |
+
where: {
|
| 368 |
+
status: 'active',
|
| 369 |
+
domainCount: { lt: 10 },
|
| 370 |
+
refreshToken: { not: null }
|
| 371 |
+
},
|
| 372 |
+
orderBy: { domainCount: 'asc' }
|
| 373 |
+
});
|
| 374 |
+
|
| 375 |
+
if (account) {
|
| 376 |
+
return account;
|
| 377 |
+
}
|
| 378 |
+
|
| 379 |
+
console.log('No available account with domain slots. Creating new account...');
|
| 380 |
+
return await createAndActivateAccount();
|
| 381 |
+
}
|
| 382 |
+
|
| 383 |
+
module.exports = {
|
| 384 |
+
createAndActivateAccount,
|
| 385 |
+
getOrCreateAvailableAccount,
|
| 386 |
+
generatePassword,
|
| 387 |
+
generateName,
|
| 388 |
+
generatePhone
|
| 389 |
+
};
|
lib/autz-account.js
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const puppeteer = require('puppeteer-core');
|
| 2 |
+
const axios = require('axios');
|
| 3 |
+
|
| 4 |
+
const CHROMIUM_PATH = process.env.CHROMIUM_PATH || process.env.PUPPETEER_EXECUTABLE_PATH || '/nix/store/khk7xpgsm5insk81azy9d560yq4npf77-chromium-131.0.6778.204/bin/chromium';
|
| 5 |
+
const CF_SOLVER_URL = 'https://samleuma-cf-solver.hf.space/solver';
|
| 6 |
+
const TURNSTILE_SITEKEY = '0x4AAAAAAAfqMU3EtZs0r_nN';
|
| 7 |
+
const ONBOARDING_URL = 'https://autz.org/onboarding/qinw2ix?callback_url=https%3A%2F%2Fmy.zone.id%2F';
|
| 8 |
+
|
| 9 |
+
async function solveTurnstile() {
|
| 10 |
+
const response = await axios.post(CF_SOLVER_URL, {
|
| 11 |
+
mode: 'turnstile-max',
|
| 12 |
+
url: ONBOARDING_URL,
|
| 13 |
+
siteKey: TURNSTILE_SITEKEY
|
| 14 |
+
}, { timeout: 180000 });
|
| 15 |
+
|
| 16 |
+
if (response.data.code === 200 && response.data.token) {
|
| 17 |
+
return response.data.token;
|
| 18 |
+
}
|
| 19 |
+
throw new Error('Failed to solve Turnstile: ' + JSON.stringify(response.data));
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
async function createAutzAccount(email, password, name, phone) {
|
| 23 |
+
const turnstileToken = await solveTurnstile();
|
| 24 |
+
|
| 25 |
+
const browser = await puppeteer.launch({
|
| 26 |
+
executablePath: CHROMIUM_PATH,
|
| 27 |
+
headless: true,
|
| 28 |
+
args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage', '--disable-gpu']
|
| 29 |
+
});
|
| 30 |
+
|
| 31 |
+
let loginToken = null;
|
| 32 |
+
let userTokenId = null;
|
| 33 |
+
|
| 34 |
+
try {
|
| 35 |
+
const page = await browser.newPage();
|
| 36 |
+
await page.setViewport({ width: 1280, height: 800 });
|
| 37 |
+
|
| 38 |
+
page.on('response', async (response) => {
|
| 39 |
+
const url = response.url();
|
| 40 |
+
if (url.includes('/api/login') && response.status() === 201) {
|
| 41 |
+
try {
|
| 42 |
+
const data = await response.json();
|
| 43 |
+
loginToken = data.token;
|
| 44 |
+
} catch (e) {}
|
| 45 |
+
}
|
| 46 |
+
if (url.includes('/api/register') && response.status() === 200) {
|
| 47 |
+
try {
|
| 48 |
+
const data = await response.json();
|
| 49 |
+
userTokenId = data.user_token_id;
|
| 50 |
+
} catch (e) {}
|
| 51 |
+
}
|
| 52 |
+
});
|
| 53 |
+
|
| 54 |
+
await page.evaluateOnNewDocument((token) => {
|
| 55 |
+
window.turnstile = {
|
| 56 |
+
render: (container, params) => {
|
| 57 |
+
if (params.callback) setTimeout(() => params.callback(token), 500);
|
| 58 |
+
return 'widget-id';
|
| 59 |
+
},
|
| 60 |
+
getResponse: () => token,
|
| 61 |
+
reset: () => {}
|
| 62 |
+
};
|
| 63 |
+
}, turnstileToken);
|
| 64 |
+
|
| 65 |
+
await page.goto(ONBOARDING_URL, { waitUntil: 'networkidle2', timeout: 60000 });
|
| 66 |
+
await page.waitForSelector('#login-modal', { visible: true, timeout: 30000 });
|
| 67 |
+
|
| 68 |
+
await page.type('input[type="email"]', email, { delay: 20 });
|
| 69 |
+
|
| 70 |
+
await page.evaluate((token) => {
|
| 71 |
+
if (window.onVerifiedCaptcha) window.onVerifiedCaptcha(token);
|
| 72 |
+
const btn = document.querySelector('button.btn-primary');
|
| 73 |
+
if (btn) btn.disabled = false;
|
| 74 |
+
}, turnstileToken);
|
| 75 |
+
|
| 76 |
+
await new Promise(r => setTimeout(r, 1000));
|
| 77 |
+
await page.click('button.btn-primary');
|
| 78 |
+
await new Promise(r => setTimeout(r, 3000));
|
| 79 |
+
|
| 80 |
+
const inputs = await page.$$('#login-modal input.form-control');
|
| 81 |
+
|
| 82 |
+
if (inputs.length >= 4) {
|
| 83 |
+
await inputs[1].type(password, { delay: 15 });
|
| 84 |
+
await inputs[2].type(name, { delay: 15 });
|
| 85 |
+
await inputs[3].type(phone, { delay: 15 });
|
| 86 |
+
|
| 87 |
+
await page.click('button.btn-primary');
|
| 88 |
+
await new Promise(r => setTimeout(r, 5000));
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
return {
|
| 92 |
+
success: !!userTokenId,
|
| 93 |
+
email,
|
| 94 |
+
userTokenId,
|
| 95 |
+
loginToken,
|
| 96 |
+
needsActivation: true
|
| 97 |
+
};
|
| 98 |
+
|
| 99 |
+
} finally {
|
| 100 |
+
await browser.close();
|
| 101 |
+
}
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
module.exports = { createAutzAccount, solveTurnstile };
|
lib/remote-browser.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const puppeteer = require('puppeteer-core');
|
| 2 |
+
const axios = require('axios');
|
| 3 |
+
|
| 4 |
+
const BROWSER_API_URL = 'https://samleuma-browser.hf.space';
|
| 5 |
+
|
| 6 |
+
async function getRemoteBrowser() {
|
| 7 |
+
const response = await axios.get(`${BROWSER_API_URL}/api/ws-endpoint`, { timeout: 30000 });
|
| 8 |
+
|
| 9 |
+
if (!response.data.browserConnected || !response.data.wsEndpoint) {
|
| 10 |
+
throw new Error('Remote browser not ready');
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
const wsEndpoint = response.data.wsEndpoint.replace('ws://127.0.0.1', 'wss://samleuma-browser.hf.space');
|
| 14 |
+
|
| 15 |
+
const browser = await puppeteer.connect({
|
| 16 |
+
browserWSEndpoint: wsEndpoint,
|
| 17 |
+
defaultViewport: { width: 1280, height: 800 }
|
| 18 |
+
});
|
| 19 |
+
|
| 20 |
+
return browser;
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
async function executeOnRemoteBrowser(url, script, waitFor = null) {
|
| 24 |
+
const payload = { url, script };
|
| 25 |
+
if (waitFor) payload.waitFor = waitFor;
|
| 26 |
+
|
| 27 |
+
const response = await axios.post(`${BROWSER_API_URL}/api/execute`, payload, { timeout: 60000 });
|
| 28 |
+
return response.data;
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
async function takeRemoteScreenshot(url, fullPage = false) {
|
| 32 |
+
const response = await axios.post(`${BROWSER_API_URL}/api/screenshot`, { url, fullPage }, { timeout: 60000 });
|
| 33 |
+
return response.data;
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
async function createRemotePage() {
|
| 37 |
+
const response = await axios.post(`${BROWSER_API_URL}/api/page/new`, {}, { timeout: 30000 });
|
| 38 |
+
return response.data;
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
async function getBrowserStatus() {
|
| 42 |
+
const response = await axios.get(`${BROWSER_API_URL}/api/status`, { timeout: 10000 });
|
| 43 |
+
return response.data;
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
module.exports = {
|
| 47 |
+
getRemoteBrowser,
|
| 48 |
+
executeOnRemoteBrowser,
|
| 49 |
+
takeRemoteScreenshot,
|
| 50 |
+
createRemotePage,
|
| 51 |
+
getBrowserStatus,
|
| 52 |
+
BROWSER_API_URL
|
| 53 |
+
};
|
lib/temp-email.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const axios = require('axios');
|
| 2 |
+
|
| 3 |
+
const EMAIL_API_URL = 'https://email.agbala-itura.name.ng/api';
|
| 4 |
+
|
| 5 |
+
async function generateEmail(label = null, custom = null) {
|
| 6 |
+
const payload = {};
|
| 7 |
+
if (label) payload.label = label;
|
| 8 |
+
if (custom) payload.custom = custom;
|
| 9 |
+
|
| 10 |
+
const response = await axios.post(`${EMAIL_API_URL}/addresses`, payload);
|
| 11 |
+
|
| 12 |
+
if (response.data.success) {
|
| 13 |
+
return response.data.data;
|
| 14 |
+
}
|
| 15 |
+
throw new Error('Failed to generate email: ' + JSON.stringify(response.data));
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
async function listEmails(address) {
|
| 19 |
+
const response = await axios.get(`${EMAIL_API_URL}/emails`, {
|
| 20 |
+
params: { address }
|
| 21 |
+
});
|
| 22 |
+
|
| 23 |
+
if (response.data.success) {
|
| 24 |
+
return response.data.data.items;
|
| 25 |
+
}
|
| 26 |
+
return [];
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
async function getEmail(id) {
|
| 30 |
+
const response = await axios.get(`${EMAIL_API_URL}/emails/${id}`);
|
| 31 |
+
|
| 32 |
+
if (response.data.success) {
|
| 33 |
+
return response.data.data;
|
| 34 |
+
}
|
| 35 |
+
throw new Error('Email not found');
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
async function waitForEmail(address, timeout = 120000, interval = 5000) {
|
| 39 |
+
const startTime = Date.now();
|
| 40 |
+
|
| 41 |
+
while (Date.now() - startTime < timeout) {
|
| 42 |
+
const emails = await listEmails(address);
|
| 43 |
+
|
| 44 |
+
if (emails.length > 0) {
|
| 45 |
+
return emails[0];
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
await new Promise(r => setTimeout(r, interval));
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
throw new Error('Timeout waiting for email');
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
async function deleteAddress(address) {
|
| 55 |
+
await axios.delete(`${EMAIL_API_URL}/addresses/${encodeURIComponent(address)}`);
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
module.exports = {
|
| 59 |
+
generateEmail,
|
| 60 |
+
listEmails,
|
| 61 |
+
getEmail,
|
| 62 |
+
waitForEmail,
|
| 63 |
+
deleteAddress
|
| 64 |
+
};
|
lib/token-scheduler.js
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const { PrismaClient } = require('@prisma/client');
|
| 2 |
+
const { ZoneIdAPI } = require('./zoneid-api');
|
| 3 |
+
|
| 4 |
+
const prisma = new PrismaClient();
|
| 5 |
+
|
| 6 |
+
const REFRESH_INTERVAL_MS = 3 * 24 * 60 * 60 * 1000;
|
| 7 |
+
const REFRESH_CHECK_INTERVAL_MS = 6 * 60 * 60 * 1000;
|
| 8 |
+
|
| 9 |
+
async function refreshAccountToken(account) {
|
| 10 |
+
console.log(`[TokenScheduler] Refreshing token for account: ${account.email}`);
|
| 11 |
+
|
| 12 |
+
if (!account.refreshToken) {
|
| 13 |
+
console.log(`[TokenScheduler] Account ${account.email} has no refresh token, skipping`);
|
| 14 |
+
return { success: false, reason: 'no_token' };
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
const api = new ZoneIdAPI();
|
| 18 |
+
api.setRefreshTokenCookie(account.refreshToken);
|
| 19 |
+
|
| 20 |
+
try {
|
| 21 |
+
const response = await api.refreshAccessToken();
|
| 22 |
+
|
| 23 |
+
if (response && response.token) {
|
| 24 |
+
console.log(`[TokenScheduler] Successfully refreshed token for ${account.email}`);
|
| 25 |
+
|
| 26 |
+
await prisma.account.update({
|
| 27 |
+
where: { id: account.id },
|
| 28 |
+
data: { updatedAt: new Date() }
|
| 29 |
+
});
|
| 30 |
+
|
| 31 |
+
return { success: true };
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
return { success: true, note: 'Token validated' };
|
| 35 |
+
} catch (err) {
|
| 36 |
+
console.error(`[TokenScheduler] Failed to refresh token for ${account.email}:`, err.message);
|
| 37 |
+
|
| 38 |
+
if (err.response?.status === 401 || err.message.includes('expired')) {
|
| 39 |
+
console.log(`[TokenScheduler] Marking account ${account.email} as needing re-authentication`);
|
| 40 |
+
|
| 41 |
+
await prisma.account.update({
|
| 42 |
+
where: { id: account.id },
|
| 43 |
+
data: { status: 'token_expired' }
|
| 44 |
+
});
|
| 45 |
+
|
| 46 |
+
return { success: false, reason: 'token_expired' };
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
return { success: false, reason: err.message };
|
| 50 |
+
}
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
async function refreshAllAccountTokens() {
|
| 54 |
+
console.log('[TokenScheduler] Starting token refresh cycle for all accounts...');
|
| 55 |
+
|
| 56 |
+
const accounts = await prisma.account.findMany({
|
| 57 |
+
where: {
|
| 58 |
+
status: 'active',
|
| 59 |
+
refreshToken: { not: null }
|
| 60 |
+
}
|
| 61 |
+
});
|
| 62 |
+
|
| 63 |
+
console.log(`[TokenScheduler] Found ${accounts.length} active accounts with tokens`);
|
| 64 |
+
|
| 65 |
+
const results = {
|
| 66 |
+
total: accounts.length,
|
| 67 |
+
success: 0,
|
| 68 |
+
failed: 0,
|
| 69 |
+
skipped: 0,
|
| 70 |
+
details: []
|
| 71 |
+
};
|
| 72 |
+
|
| 73 |
+
for (const account of accounts) {
|
| 74 |
+
try {
|
| 75 |
+
await new Promise(r => setTimeout(r, 2000));
|
| 76 |
+
|
| 77 |
+
const result = await refreshAccountToken(account);
|
| 78 |
+
|
| 79 |
+
if (result.success) {
|
| 80 |
+
results.success++;
|
| 81 |
+
} else if (result.reason === 'no_token') {
|
| 82 |
+
results.skipped++;
|
| 83 |
+
} else {
|
| 84 |
+
results.failed++;
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
results.details.push({
|
| 88 |
+
accountId: account.id,
|
| 89 |
+
email: account.email,
|
| 90 |
+
...result
|
| 91 |
+
});
|
| 92 |
+
} catch (err) {
|
| 93 |
+
console.error(`[TokenScheduler] Unexpected error for ${account.email}:`, err.message);
|
| 94 |
+
results.failed++;
|
| 95 |
+
results.details.push({
|
| 96 |
+
accountId: account.id,
|
| 97 |
+
email: account.email,
|
| 98 |
+
success: false,
|
| 99 |
+
reason: err.message
|
| 100 |
+
});
|
| 101 |
+
}
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
console.log(`[TokenScheduler] Refresh cycle complete: ${results.success} success, ${results.failed} failed, ${results.skipped} skipped`);
|
| 105 |
+
|
| 106 |
+
return results;
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
let schedulerInterval = null;
|
| 110 |
+
|
| 111 |
+
function startTokenScheduler() {
|
| 112 |
+
if (schedulerInterval) {
|
| 113 |
+
console.log('[TokenScheduler] Scheduler already running');
|
| 114 |
+
return;
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
console.log(`[TokenScheduler] Starting scheduler (check interval: ${REFRESH_CHECK_INTERVAL_MS / 1000 / 60 / 60} hours)`);
|
| 118 |
+
|
| 119 |
+
setTimeout(() => {
|
| 120 |
+
refreshAllAccountTokens().catch(err => {
|
| 121 |
+
console.error('[TokenScheduler] Initial refresh failed:', err.message);
|
| 122 |
+
});
|
| 123 |
+
}, 60000);
|
| 124 |
+
|
| 125 |
+
schedulerInterval = setInterval(() => {
|
| 126 |
+
refreshAllAccountTokens().catch(err => {
|
| 127 |
+
console.error('[TokenScheduler] Scheduled refresh failed:', err.message);
|
| 128 |
+
});
|
| 129 |
+
}, REFRESH_CHECK_INTERVAL_MS);
|
| 130 |
+
|
| 131 |
+
console.log('[TokenScheduler] Scheduler started successfully');
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
function stopTokenScheduler() {
|
| 135 |
+
if (schedulerInterval) {
|
| 136 |
+
clearInterval(schedulerInterval);
|
| 137 |
+
schedulerInterval = null;
|
| 138 |
+
console.log('[TokenScheduler] Scheduler stopped');
|
| 139 |
+
}
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
module.exports = {
|
| 143 |
+
refreshAccountToken,
|
| 144 |
+
refreshAllAccountTokens,
|
| 145 |
+
startTokenScheduler,
|
| 146 |
+
stopTokenScheduler,
|
| 147 |
+
REFRESH_INTERVAL_MS,
|
| 148 |
+
REFRESH_CHECK_INTERVAL_MS
|
| 149 |
+
};
|
lib/zoneid-api.js
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const axios = require('axios');
|
| 2 |
+
|
| 3 |
+
const API_BASE = 'https://my.zone.id/api';
|
| 4 |
+
|
| 5 |
+
class ZoneIdAPI {
|
| 6 |
+
constructor() {
|
| 7 |
+
this.accessToken = null;
|
| 8 |
+
this.refreshTokenCookie = null;
|
| 9 |
+
this.client = axios.create({
|
| 10 |
+
baseURL: API_BASE,
|
| 11 |
+
headers: {
|
| 12 |
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
| 13 |
+
'Accept': 'application/json',
|
| 14 |
+
'Content-Type': 'application/json',
|
| 15 |
+
'Origin': 'https://my.zone.id',
|
| 16 |
+
'Referer': 'https://my.zone.id/'
|
| 17 |
+
}
|
| 18 |
+
});
|
| 19 |
+
|
| 20 |
+
this.client.interceptors.request.use((config) => {
|
| 21 |
+
if (this.accessToken) {
|
| 22 |
+
config.headers.Authorization = `Bearer ${this.accessToken}`;
|
| 23 |
+
}
|
| 24 |
+
if (this.refreshTokenCookie) {
|
| 25 |
+
config.headers.Cookie = this.refreshTokenCookie;
|
| 26 |
+
}
|
| 27 |
+
return config;
|
| 28 |
+
});
|
| 29 |
+
|
| 30 |
+
this.client.interceptors.response.use(
|
| 31 |
+
(response) => response,
|
| 32 |
+
(error) => {
|
| 33 |
+
if (error.response) {
|
| 34 |
+
console.log('API Error:', error.response.status, error.response.data);
|
| 35 |
+
}
|
| 36 |
+
return Promise.reject(error);
|
| 37 |
+
}
|
| 38 |
+
);
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
setAccessToken(token) {
|
| 42 |
+
this.accessToken = token;
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
setRefreshTokenCookie(cookie) {
|
| 46 |
+
this.refreshTokenCookie = cookie;
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
async refreshAccessToken() {
|
| 50 |
+
if (!this.refreshTokenCookie) {
|
| 51 |
+
throw new Error('No refresh token cookie set');
|
| 52 |
+
}
|
| 53 |
+
const response = await this.client.post('/refresh-token');
|
| 54 |
+
if (response.data && response.data.token) {
|
| 55 |
+
this.accessToken = response.data.token;
|
| 56 |
+
}
|
| 57 |
+
return response.data;
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
async getAuthUrl() {
|
| 61 |
+
const response = await this.client.get('/authurl');
|
| 62 |
+
return response.data;
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
async authorize(code) {
|
| 66 |
+
const response = await this.client.post('/authorize', { auth_code: code });
|
| 67 |
+
return response.data;
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
async refreshToken() {
|
| 71 |
+
const response = await this.client.post('/refresh-token');
|
| 72 |
+
return response.data;
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
async logout() {
|
| 76 |
+
const response = await this.client.post('/logout');
|
| 77 |
+
return response.data;
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
async listSubdomains() {
|
| 81 |
+
const response = await this.client.get('/subdomains');
|
| 82 |
+
return response.data;
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
async createSubdomain(subdomain, suffix = 'zone.id', mode = 'dns_record', usageType, usageDescription) {
|
| 86 |
+
const fullSubdomain = `${subdomain}.${suffix}`;
|
| 87 |
+
console.log('Creating subdomain:', fullSubdomain);
|
| 88 |
+
const response = await this.client.post('/subdomains', {
|
| 89 |
+
subdomain: fullSubdomain,
|
| 90 |
+
mode,
|
| 91 |
+
usage_type: usageType,
|
| 92 |
+
usage_description: usageDescription
|
| 93 |
+
});
|
| 94 |
+
console.log('Create response:', JSON.stringify(response.data));
|
| 95 |
+
|
| 96 |
+
if (!response.data || !response.data.id) {
|
| 97 |
+
console.log('No ID in response, fetching subdomain list...');
|
| 98 |
+
const list = await this.listSubdomains();
|
| 99 |
+
if (list && list.data) {
|
| 100 |
+
const created = list.data.find(d => d.subdomain === fullSubdomain);
|
| 101 |
+
if (created) {
|
| 102 |
+
console.log('Found created subdomain:', created.id);
|
| 103 |
+
return created;
|
| 104 |
+
}
|
| 105 |
+
}
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
return response.data;
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
async getSubdomain(id) {
|
| 112 |
+
const response = await this.client.get(`/subdomains/${id}`);
|
| 113 |
+
return response.data;
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
async updateSubdomain(id, data) {
|
| 117 |
+
const response = await this.client.patch(`/subdomains/${id}`, data);
|
| 118 |
+
return response.data;
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
async deleteSubdomain(id) {
|
| 122 |
+
const response = await this.client.delete(`/subdomains/${id}`);
|
| 123 |
+
return response.data;
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
async getDnsRecords(subdomainId) {
|
| 127 |
+
const response = await this.client.get(`/subdomains/${subdomainId}/dns`);
|
| 128 |
+
return response.data;
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
async createDnsRecord(subdomainId, record) {
|
| 132 |
+
const normalizedRecord = {
|
| 133 |
+
type: record.type,
|
| 134 |
+
hostname: record.hostname || record.name,
|
| 135 |
+
content: record.content || record.value
|
| 136 |
+
};
|
| 137 |
+
if (record.note) normalizedRecord.note = record.note;
|
| 138 |
+
|
| 139 |
+
const response = await this.client.post(`/subdomains/${subdomainId}/dns`, normalizedRecord);
|
| 140 |
+
return response.data;
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
async updateDnsRecord(subdomainId, recordId, record) {
|
| 144 |
+
const normalizedRecord = {};
|
| 145 |
+
if (record.type) normalizedRecord.type = record.type;
|
| 146 |
+
if (record.hostname || record.name) normalizedRecord.hostname = record.hostname || record.name;
|
| 147 |
+
if (record.content || record.value) normalizedRecord.content = record.content || record.value;
|
| 148 |
+
if (record.note) normalizedRecord.note = record.note;
|
| 149 |
+
|
| 150 |
+
const response = await this.client.patch(`/subdomains/${subdomainId}/dns/${recordId}`, normalizedRecord);
|
| 151 |
+
return response.data;
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
async deleteDnsRecord(subdomainId, recordId) {
|
| 155 |
+
const response = await this.client.delete(`/subdomains/${subdomainId}/dns/${recordId}`);
|
| 156 |
+
return response.data;
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
+
async getUrlForwarder(subdomainId) {
|
| 160 |
+
const response = await this.client.get(`/subdomains/${subdomainId}/url_forwarder`);
|
| 161 |
+
return response.data;
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
+
async createUrlForwarder(subdomainId, forwarder) {
|
| 165 |
+
const response = await this.client.post(`/subdomains/${subdomainId}/url_forwarder`, forwarder);
|
| 166 |
+
return response.data;
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
+
async getFeatured() {
|
| 170 |
+
const response = await this.client.get('/featured');
|
| 171 |
+
return response.data;
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
async getOfficial() {
|
| 175 |
+
const response = await this.client.get('/official');
|
| 176 |
+
return response.data;
|
| 177 |
+
}
|
| 178 |
+
}
|
| 179 |
+
|
| 180 |
+
module.exports = { ZoneIdAPI };
|
package-lock.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "zoneid-domain-api",
|
| 3 |
+
"version": "1.0.0",
|
| 4 |
+
"description": "Zone.ID Domain Registration API with auto-account creation",
|
| 5 |
+
"main": "src/server.js",
|
| 6 |
+
"scripts": {
|
| 7 |
+
"start": "node src/server.js",
|
| 8 |
+
"dev": "node src/server.js",
|
| 9 |
+
"db:migrate": "npx prisma migrate deploy",
|
| 10 |
+
"db:generate": "npx prisma generate",
|
| 11 |
+
"test": "echo \"Error: no test specified\" && exit 1"
|
| 12 |
+
},
|
| 13 |
+
"keywords": [],
|
| 14 |
+
"author": "",
|
| 15 |
+
"license": "ISC",
|
| 16 |
+
"dependencies": {
|
| 17 |
+
"@fastify/cors": "^11.1.0",
|
| 18 |
+
"@fastify/swagger": "^9.6.1",
|
| 19 |
+
"@fastify/swagger-ui": "^5.2.3",
|
| 20 |
+
"@prisma/client": "^5.22.0",
|
| 21 |
+
"@types/node": "^22.13.11",
|
| 22 |
+
"axios": "^1.13.2",
|
| 23 |
+
"bcrypt": "^6.0.0",
|
| 24 |
+
"cheerio": "^1.1.2",
|
| 25 |
+
"dotenv": "^17.2.3",
|
| 26 |
+
"fastify": "^5.6.2",
|
| 27 |
+
"prisma": "^5.22.0",
|
| 28 |
+
"puppeteer": "^24.32.0",
|
| 29 |
+
"puppeteer-core": "^24.32.0"
|
| 30 |
+
}
|
| 31 |
+
}
|
prisma/migrations/20251207094931_init/migration.sql
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
-- CreateTable
|
| 2 |
+
CREATE TABLE "Account" (
|
| 3 |
+
"id" TEXT NOT NULL,
|
| 4 |
+
"email" TEXT NOT NULL,
|
| 5 |
+
"passwordHash" TEXT NOT NULL,
|
| 6 |
+
"refreshToken" TEXT,
|
| 7 |
+
"name" TEXT,
|
| 8 |
+
"phone" TEXT,
|
| 9 |
+
"autzorgId" TEXT,
|
| 10 |
+
"zoneIdUserId" TEXT,
|
| 11 |
+
"status" TEXT NOT NULL DEFAULT 'active',
|
| 12 |
+
"domainCount" INTEGER NOT NULL DEFAULT 0,
|
| 13 |
+
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
| 14 |
+
"updatedAt" TIMESTAMP(3) NOT NULL,
|
| 15 |
+
|
| 16 |
+
CONSTRAINT "Account_pkey" PRIMARY KEY ("id")
|
| 17 |
+
);
|
| 18 |
+
|
| 19 |
+
-- CreateTable
|
| 20 |
+
CREATE TABLE "Domain" (
|
| 21 |
+
"id" TEXT NOT NULL,
|
| 22 |
+
"zoneIdDomainId" TEXT NOT NULL,
|
| 23 |
+
"subdomain" TEXT NOT NULL,
|
| 24 |
+
"suffix" TEXT NOT NULL,
|
| 25 |
+
"mode" TEXT NOT NULL DEFAULT 'dns_record',
|
| 26 |
+
"usageType" TEXT,
|
| 27 |
+
"usageDescription" TEXT,
|
| 28 |
+
"status" TEXT NOT NULL DEFAULT 'active',
|
| 29 |
+
"plan" TEXT NOT NULL DEFAULT 'free',
|
| 30 |
+
"expiresAt" TIMESTAMP(3),
|
| 31 |
+
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
| 32 |
+
"updatedAt" TIMESTAMP(3) NOT NULL,
|
| 33 |
+
"accountId" TEXT NOT NULL,
|
| 34 |
+
|
| 35 |
+
CONSTRAINT "Domain_pkey" PRIMARY KEY ("id")
|
| 36 |
+
);
|
| 37 |
+
|
| 38 |
+
-- CreateTable
|
| 39 |
+
CREATE TABLE "DnsRecord" (
|
| 40 |
+
"id" TEXT NOT NULL,
|
| 41 |
+
"zoneIdRecordId" TEXT NOT NULL,
|
| 42 |
+
"type" TEXT NOT NULL,
|
| 43 |
+
"hostname" TEXT NOT NULL,
|
| 44 |
+
"content" TEXT NOT NULL,
|
| 45 |
+
"note" TEXT,
|
| 46 |
+
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
| 47 |
+
"updatedAt" TIMESTAMP(3) NOT NULL,
|
| 48 |
+
"domainId" TEXT NOT NULL,
|
| 49 |
+
|
| 50 |
+
CONSTRAINT "DnsRecord_pkey" PRIMARY KEY ("id")
|
| 51 |
+
);
|
| 52 |
+
|
| 53 |
+
-- CreateTable
|
| 54 |
+
CREATE TABLE "ApiKey" (
|
| 55 |
+
"id" TEXT NOT NULL,
|
| 56 |
+
"key" TEXT NOT NULL,
|
| 57 |
+
"name" TEXT NOT NULL,
|
| 58 |
+
"isActive" BOOLEAN NOT NULL DEFAULT true,
|
| 59 |
+
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
| 60 |
+
"lastUsedAt" TIMESTAMP(3),
|
| 61 |
+
|
| 62 |
+
CONSTRAINT "ApiKey_pkey" PRIMARY KEY ("id")
|
| 63 |
+
);
|
| 64 |
+
|
| 65 |
+
-- CreateIndex
|
| 66 |
+
CREATE UNIQUE INDEX "Account_email_key" ON "Account"("email");
|
| 67 |
+
|
| 68 |
+
-- CreateIndex
|
| 69 |
+
CREATE UNIQUE INDEX "Domain_zoneIdDomainId_key" ON "Domain"("zoneIdDomainId");
|
| 70 |
+
|
| 71 |
+
-- CreateIndex
|
| 72 |
+
CREATE UNIQUE INDEX "Domain_subdomain_key" ON "Domain"("subdomain");
|
| 73 |
+
|
| 74 |
+
-- CreateIndex
|
| 75 |
+
CREATE INDEX "Domain_accountId_idx" ON "Domain"("accountId");
|
| 76 |
+
|
| 77 |
+
-- CreateIndex
|
| 78 |
+
CREATE INDEX "Domain_suffix_idx" ON "Domain"("suffix");
|
| 79 |
+
|
| 80 |
+
-- CreateIndex
|
| 81 |
+
CREATE UNIQUE INDEX "DnsRecord_zoneIdRecordId_key" ON "DnsRecord"("zoneIdRecordId");
|
| 82 |
+
|
| 83 |
+
-- CreateIndex
|
| 84 |
+
CREATE INDEX "DnsRecord_domainId_idx" ON "DnsRecord"("domainId");
|
| 85 |
+
|
| 86 |
+
-- CreateIndex
|
| 87 |
+
CREATE INDEX "DnsRecord_type_idx" ON "DnsRecord"("type");
|
| 88 |
+
|
| 89 |
+
-- CreateIndex
|
| 90 |
+
CREATE UNIQUE INDEX "ApiKey_key_key" ON "ApiKey"("key");
|
| 91 |
+
|
| 92 |
+
-- AddForeignKey
|
| 93 |
+
ALTER TABLE "Domain" ADD CONSTRAINT "Domain_accountId_fkey" FOREIGN KEY ("accountId") REFERENCES "Account"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
| 94 |
+
|
| 95 |
+
-- AddForeignKey
|
| 96 |
+
ALTER TABLE "DnsRecord" ADD CONSTRAINT "DnsRecord_domainId_fkey" FOREIGN KEY ("domainId") REFERENCES "Domain"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
prisma/migrations/migration_lock.toml
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Please do not edit this file manually
|
| 2 |
+
# It should be added in your version-control system (i.e. Git)
|
| 3 |
+
provider = "postgresql"
|
prisma/schema.prisma
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
generator client {
|
| 2 |
+
provider = "prisma-client-js"
|
| 3 |
+
}
|
| 4 |
+
|
| 5 |
+
datasource db {
|
| 6 |
+
provider = "postgresql"
|
| 7 |
+
url = env("DATABASE_URL")
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
model Account {
|
| 11 |
+
id String @id @default(cuid())
|
| 12 |
+
email String @unique
|
| 13 |
+
passwordHash String
|
| 14 |
+
refreshToken String?
|
| 15 |
+
name String?
|
| 16 |
+
phone String?
|
| 17 |
+
autzorgId String?
|
| 18 |
+
zoneIdUserId String?
|
| 19 |
+
status String @default("active")
|
| 20 |
+
domainCount Int @default(0)
|
| 21 |
+
createdAt DateTime @default(now())
|
| 22 |
+
updatedAt DateTime @updatedAt
|
| 23 |
+
|
| 24 |
+
domains Domain[]
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
model Domain {
|
| 28 |
+
id String @id @default(cuid())
|
| 29 |
+
zoneIdDomainId String @unique
|
| 30 |
+
subdomain String @unique
|
| 31 |
+
suffix String
|
| 32 |
+
mode String @default("dns_record")
|
| 33 |
+
usageType String?
|
| 34 |
+
usageDescription String?
|
| 35 |
+
status String @default("active")
|
| 36 |
+
plan String @default("free")
|
| 37 |
+
expiresAt DateTime?
|
| 38 |
+
createdAt DateTime @default(now())
|
| 39 |
+
updatedAt DateTime @updatedAt
|
| 40 |
+
|
| 41 |
+
accountId String
|
| 42 |
+
account Account @relation(fields: [accountId], references: [id])
|
| 43 |
+
|
| 44 |
+
dnsRecords DnsRecord[]
|
| 45 |
+
|
| 46 |
+
@@index([accountId])
|
| 47 |
+
@@index([suffix])
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
model DnsRecord {
|
| 51 |
+
id String @id @default(cuid())
|
| 52 |
+
zoneIdRecordId String @unique
|
| 53 |
+
type String
|
| 54 |
+
hostname String
|
| 55 |
+
content String
|
| 56 |
+
note String?
|
| 57 |
+
createdAt DateTime @default(now())
|
| 58 |
+
updatedAt DateTime @updatedAt
|
| 59 |
+
|
| 60 |
+
domainId String
|
| 61 |
+
domain Domain @relation(fields: [domainId], references: [id], onDelete: Cascade)
|
| 62 |
+
|
| 63 |
+
@@index([domainId])
|
| 64 |
+
@@index([type])
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
model ApiKey {
|
| 68 |
+
id String @id @default(cuid())
|
| 69 |
+
key String @unique
|
| 70 |
+
name String
|
| 71 |
+
isActive Boolean @default(true)
|
| 72 |
+
createdAt DateTime @default(now())
|
| 73 |
+
lastUsedAt DateTime?
|
| 74 |
+
}
|
src/routes/accounts.js
ADDED
|
@@ -0,0 +1,310 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const { PrismaClient } = require('@prisma/client');
|
| 2 |
+
const bcrypt = require('bcrypt');
|
| 3 |
+
const { refreshAccountToken, refreshAllAccountTokens } = require('../../lib/token-scheduler');
|
| 4 |
+
|
| 5 |
+
const prisma = new PrismaClient();
|
| 6 |
+
|
| 7 |
+
async function accountRoutes(fastify, options) {
|
| 8 |
+
fastify.get('/', {
|
| 9 |
+
schema: {
|
| 10 |
+
summary: 'List all accounts',
|
| 11 |
+
tags: ['Accounts'],
|
| 12 |
+
security: [{ apiKey: [] }],
|
| 13 |
+
response: {
|
| 14 |
+
200: {
|
| 15 |
+
type: 'object',
|
| 16 |
+
properties: {
|
| 17 |
+
accounts: {
|
| 18 |
+
type: 'array',
|
| 19 |
+
items: {
|
| 20 |
+
type: 'object',
|
| 21 |
+
properties: {
|
| 22 |
+
id: { type: 'string' },
|
| 23 |
+
email: { type: 'string' },
|
| 24 |
+
name: { type: 'string' },
|
| 25 |
+
status: { type: 'string' },
|
| 26 |
+
domainCount: { type: 'integer' },
|
| 27 |
+
createdAt: { type: 'string' }
|
| 28 |
+
}
|
| 29 |
+
}
|
| 30 |
+
}
|
| 31 |
+
}
|
| 32 |
+
}
|
| 33 |
+
}
|
| 34 |
+
}
|
| 35 |
+
}, async (request, reply) => {
|
| 36 |
+
const accounts = await prisma.account.findMany({
|
| 37 |
+
select: {
|
| 38 |
+
id: true,
|
| 39 |
+
email: true,
|
| 40 |
+
name: true,
|
| 41 |
+
status: true,
|
| 42 |
+
domainCount: true,
|
| 43 |
+
createdAt: true
|
| 44 |
+
}
|
| 45 |
+
});
|
| 46 |
+
return { accounts };
|
| 47 |
+
});
|
| 48 |
+
|
| 49 |
+
fastify.post('/', {
|
| 50 |
+
schema: {
|
| 51 |
+
summary: 'Create a new account',
|
| 52 |
+
description: 'Register a new account with Autz.org credentials. Note: Browser automation required for initial setup.',
|
| 53 |
+
tags: ['Accounts'],
|
| 54 |
+
security: [{ apiKey: [] }],
|
| 55 |
+
body: {
|
| 56 |
+
type: 'object',
|
| 57 |
+
required: ['email', 'password'],
|
| 58 |
+
properties: {
|
| 59 |
+
email: { type: 'string', format: 'email' },
|
| 60 |
+
password: { type: 'string', minLength: 8 },
|
| 61 |
+
name: { type: 'string' },
|
| 62 |
+
phone: { type: 'string' },
|
| 63 |
+
refreshToken: { type: 'string', description: 'Refresh token cookie from browser authentication' }
|
| 64 |
+
}
|
| 65 |
+
},
|
| 66 |
+
response: {
|
| 67 |
+
201: {
|
| 68 |
+
type: 'object',
|
| 69 |
+
properties: {
|
| 70 |
+
id: { type: 'string' },
|
| 71 |
+
email: { type: 'string' },
|
| 72 |
+
message: { type: 'string' }
|
| 73 |
+
}
|
| 74 |
+
}
|
| 75 |
+
}
|
| 76 |
+
}
|
| 77 |
+
}, async (request, reply) => {
|
| 78 |
+
const { email, password, name, phone, refreshToken } = request.body;
|
| 79 |
+
|
| 80 |
+
const existing = await prisma.account.findUnique({ where: { email } });
|
| 81 |
+
if (existing) {
|
| 82 |
+
return reply.code(409).send({ error: 'Account already exists' });
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
const passwordHash = await bcrypt.hash(password, 10);
|
| 86 |
+
|
| 87 |
+
const account = await prisma.account.create({
|
| 88 |
+
data: {
|
| 89 |
+
email,
|
| 90 |
+
passwordHash,
|
| 91 |
+
name,
|
| 92 |
+
phone,
|
| 93 |
+
refreshToken
|
| 94 |
+
}
|
| 95 |
+
});
|
| 96 |
+
|
| 97 |
+
reply.code(201);
|
| 98 |
+
return {
|
| 99 |
+
id: account.id,
|
| 100 |
+
email: account.email,
|
| 101 |
+
message: 'Account created successfully'
|
| 102 |
+
};
|
| 103 |
+
});
|
| 104 |
+
|
| 105 |
+
fastify.get('/:id', {
|
| 106 |
+
schema: {
|
| 107 |
+
summary: 'Get account details',
|
| 108 |
+
tags: ['Accounts'],
|
| 109 |
+
security: [{ apiKey: [] }],
|
| 110 |
+
params: {
|
| 111 |
+
type: 'object',
|
| 112 |
+
properties: {
|
| 113 |
+
id: { type: 'string' }
|
| 114 |
+
}
|
| 115 |
+
},
|
| 116 |
+
response: {
|
| 117 |
+
200: {
|
| 118 |
+
type: 'object',
|
| 119 |
+
properties: {
|
| 120 |
+
id: { type: 'string' },
|
| 121 |
+
email: { type: 'string' },
|
| 122 |
+
name: { type: 'string' },
|
| 123 |
+
status: { type: 'string' },
|
| 124 |
+
domainCount: { type: 'integer' },
|
| 125 |
+
createdAt: { type: 'string' },
|
| 126 |
+
domains: {
|
| 127 |
+
type: 'array',
|
| 128 |
+
items: {
|
| 129 |
+
type: 'object',
|
| 130 |
+
properties: {
|
| 131 |
+
id: { type: 'string' },
|
| 132 |
+
subdomain: { type: 'string' },
|
| 133 |
+
suffix: { type: 'string' },
|
| 134 |
+
status: { type: 'string' }
|
| 135 |
+
}
|
| 136 |
+
}
|
| 137 |
+
}
|
| 138 |
+
}
|
| 139 |
+
}
|
| 140 |
+
}
|
| 141 |
+
}
|
| 142 |
+
}, async (request, reply) => {
|
| 143 |
+
const { id } = request.params;
|
| 144 |
+
|
| 145 |
+
const account = await prisma.account.findUnique({
|
| 146 |
+
where: { id },
|
| 147 |
+
include: {
|
| 148 |
+
domains: {
|
| 149 |
+
select: {
|
| 150 |
+
id: true,
|
| 151 |
+
subdomain: true,
|
| 152 |
+
suffix: true,
|
| 153 |
+
status: true
|
| 154 |
+
}
|
| 155 |
+
}
|
| 156 |
+
}
|
| 157 |
+
});
|
| 158 |
+
|
| 159 |
+
if (!account) {
|
| 160 |
+
return reply.code(404).send({ error: 'Account not found' });
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
return {
|
| 164 |
+
id: account.id,
|
| 165 |
+
email: account.email,
|
| 166 |
+
name: account.name,
|
| 167 |
+
status: account.status,
|
| 168 |
+
domainCount: account.domainCount,
|
| 169 |
+
createdAt: account.createdAt,
|
| 170 |
+
domains: account.domains
|
| 171 |
+
};
|
| 172 |
+
});
|
| 173 |
+
|
| 174 |
+
fastify.patch('/:id', {
|
| 175 |
+
schema: {
|
| 176 |
+
summary: 'Update account',
|
| 177 |
+
tags: ['Accounts'],
|
| 178 |
+
security: [{ apiKey: [] }],
|
| 179 |
+
params: {
|
| 180 |
+
type: 'object',
|
| 181 |
+
properties: {
|
| 182 |
+
id: { type: 'string' }
|
| 183 |
+
}
|
| 184 |
+
},
|
| 185 |
+
body: {
|
| 186 |
+
type: 'object',
|
| 187 |
+
properties: {
|
| 188 |
+
refreshToken: { type: 'string' },
|
| 189 |
+
status: { type: 'string', enum: ['active', 'inactive'] }
|
| 190 |
+
}
|
| 191 |
+
}
|
| 192 |
+
}
|
| 193 |
+
}, async (request, reply) => {
|
| 194 |
+
const { id } = request.params;
|
| 195 |
+
const { refreshToken, status } = request.body;
|
| 196 |
+
|
| 197 |
+
const account = await prisma.account.update({
|
| 198 |
+
where: { id },
|
| 199 |
+
data: {
|
| 200 |
+
...(refreshToken && { refreshToken }),
|
| 201 |
+
...(status && { status })
|
| 202 |
+
}
|
| 203 |
+
});
|
| 204 |
+
|
| 205 |
+
return { id: account.id, message: 'Account updated' };
|
| 206 |
+
});
|
| 207 |
+
|
| 208 |
+
fastify.delete('/:id', {
|
| 209 |
+
schema: {
|
| 210 |
+
summary: 'Delete account',
|
| 211 |
+
tags: ['Accounts'],
|
| 212 |
+
security: [{ apiKey: [] }],
|
| 213 |
+
params: {
|
| 214 |
+
type: 'object',
|
| 215 |
+
properties: {
|
| 216 |
+
id: { type: 'string' }
|
| 217 |
+
}
|
| 218 |
+
}
|
| 219 |
+
}
|
| 220 |
+
}, async (request, reply) => {
|
| 221 |
+
const { id } = request.params;
|
| 222 |
+
|
| 223 |
+
await prisma.account.delete({ where: { id } });
|
| 224 |
+
|
| 225 |
+
return { message: 'Account deleted' };
|
| 226 |
+
});
|
| 227 |
+
|
| 228 |
+
fastify.post('/:id/refresh-token', {
|
| 229 |
+
schema: {
|
| 230 |
+
summary: 'Refresh account token',
|
| 231 |
+
description: 'Manually refresh the Zone.ID access token for a specific account',
|
| 232 |
+
tags: ['Accounts'],
|
| 233 |
+
security: [{ apiKey: [] }],
|
| 234 |
+
params: {
|
| 235 |
+
type: 'object',
|
| 236 |
+
properties: {
|
| 237 |
+
id: { type: 'string' }
|
| 238 |
+
}
|
| 239 |
+
},
|
| 240 |
+
response: {
|
| 241 |
+
200: {
|
| 242 |
+
type: 'object',
|
| 243 |
+
properties: {
|
| 244 |
+
success: { type: 'boolean' },
|
| 245 |
+
message: { type: 'string' },
|
| 246 |
+
accountId: { type: 'string' }
|
| 247 |
+
}
|
| 248 |
+
}
|
| 249 |
+
}
|
| 250 |
+
}
|
| 251 |
+
}, async (request, reply) => {
|
| 252 |
+
const { id } = request.params;
|
| 253 |
+
|
| 254 |
+
const account = await prisma.account.findUnique({ where: { id } });
|
| 255 |
+
if (!account) {
|
| 256 |
+
return reply.code(404).send({ error: 'Account not found' });
|
| 257 |
+
}
|
| 258 |
+
|
| 259 |
+
if (!account.refreshToken) {
|
| 260 |
+
return reply.code(400).send({ error: 'Account has no refresh token' });
|
| 261 |
+
}
|
| 262 |
+
|
| 263 |
+
const result = await refreshAccountToken(account);
|
| 264 |
+
|
| 265 |
+
if (result.success) {
|
| 266 |
+
return {
|
| 267 |
+
success: true,
|
| 268 |
+
message: 'Token refreshed successfully',
|
| 269 |
+
accountId: account.id
|
| 270 |
+
};
|
| 271 |
+
} else {
|
| 272 |
+
return reply.code(400).send({
|
| 273 |
+
success: false,
|
| 274 |
+
error: 'Failed to refresh token',
|
| 275 |
+
reason: result.reason
|
| 276 |
+
});
|
| 277 |
+
}
|
| 278 |
+
});
|
| 279 |
+
|
| 280 |
+
fastify.post('/refresh-all-tokens', {
|
| 281 |
+
schema: {
|
| 282 |
+
summary: 'Refresh all account tokens',
|
| 283 |
+
description: 'Manually trigger token refresh for all active accounts',
|
| 284 |
+
tags: ['Accounts'],
|
| 285 |
+
security: [{ apiKey: [] }],
|
| 286 |
+
response: {
|
| 287 |
+
200: {
|
| 288 |
+
type: 'object',
|
| 289 |
+
properties: {
|
| 290 |
+
total: { type: 'integer' },
|
| 291 |
+
success: { type: 'integer' },
|
| 292 |
+
failed: { type: 'integer' },
|
| 293 |
+
skipped: { type: 'integer' }
|
| 294 |
+
}
|
| 295 |
+
}
|
| 296 |
+
}
|
| 297 |
+
}
|
| 298 |
+
}, async (request, reply) => {
|
| 299 |
+
const results = await refreshAllAccountTokens();
|
| 300 |
+
return {
|
| 301 |
+
total: results.total,
|
| 302 |
+
success: results.success,
|
| 303 |
+
failed: results.failed,
|
| 304 |
+
skipped: results.skipped,
|
| 305 |
+
details: results.details
|
| 306 |
+
};
|
| 307 |
+
});
|
| 308 |
+
}
|
| 309 |
+
|
| 310 |
+
module.exports = accountRoutes;
|
src/routes/dns.js
ADDED
|
@@ -0,0 +1,421 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const { PrismaClient } = require('@prisma/client');
|
| 2 |
+
const { ZoneIdAPI } = require('../../lib/zoneid-api');
|
| 3 |
+
|
| 4 |
+
const prisma = new PrismaClient();
|
| 5 |
+
|
| 6 |
+
async function dnsRoutes(fastify, options) {
|
| 7 |
+
fastify.get('/:domainId/records', {
|
| 8 |
+
schema: {
|
| 9 |
+
summary: 'List DNS records for a domain',
|
| 10 |
+
tags: ['DNS Records'],
|
| 11 |
+
security: [{ apiKey: [] }],
|
| 12 |
+
params: {
|
| 13 |
+
type: 'object',
|
| 14 |
+
required: ['domainId'],
|
| 15 |
+
properties: {
|
| 16 |
+
domainId: { type: 'string', description: 'Domain ID' }
|
| 17 |
+
}
|
| 18 |
+
},
|
| 19 |
+
response: {
|
| 20 |
+
200: {
|
| 21 |
+
type: 'object',
|
| 22 |
+
properties: {
|
| 23 |
+
records: {
|
| 24 |
+
type: 'array',
|
| 25 |
+
items: {
|
| 26 |
+
type: 'object',
|
| 27 |
+
properties: {
|
| 28 |
+
id: { type: 'string' },
|
| 29 |
+
type: { type: 'string' },
|
| 30 |
+
hostname: { type: 'string' },
|
| 31 |
+
content: { type: 'string' },
|
| 32 |
+
note: { type: 'string' },
|
| 33 |
+
createdAt: { type: 'string' }
|
| 34 |
+
}
|
| 35 |
+
}
|
| 36 |
+
}
|
| 37 |
+
}
|
| 38 |
+
}
|
| 39 |
+
}
|
| 40 |
+
}
|
| 41 |
+
}, async (request, reply) => {
|
| 42 |
+
const { domainId } = request.params;
|
| 43 |
+
|
| 44 |
+
const domain = await prisma.domain.findUnique({
|
| 45 |
+
where: { id: domainId },
|
| 46 |
+
include: {
|
| 47 |
+
account: true,
|
| 48 |
+
dnsRecords: true
|
| 49 |
+
}
|
| 50 |
+
});
|
| 51 |
+
|
| 52 |
+
if (!domain) {
|
| 53 |
+
return reply.code(404).send({ error: 'Domain not found' });
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
return {
|
| 57 |
+
records: domain.dnsRecords.map(r => ({
|
| 58 |
+
id: r.id,
|
| 59 |
+
zoneIdRecordId: r.zoneIdRecordId,
|
| 60 |
+
type: r.type,
|
| 61 |
+
hostname: r.hostname,
|
| 62 |
+
content: r.content,
|
| 63 |
+
note: r.note,
|
| 64 |
+
createdAt: r.createdAt
|
| 65 |
+
}))
|
| 66 |
+
};
|
| 67 |
+
});
|
| 68 |
+
|
| 69 |
+
fastify.post('/:domainId/records', {
|
| 70 |
+
schema: {
|
| 71 |
+
summary: 'Create a DNS record',
|
| 72 |
+
description: 'Create an A or CNAME record for the domain',
|
| 73 |
+
tags: ['DNS Records'],
|
| 74 |
+
security: [{ apiKey: [] }],
|
| 75 |
+
params: {
|
| 76 |
+
type: 'object',
|
| 77 |
+
required: ['domainId'],
|
| 78 |
+
properties: {
|
| 79 |
+
domainId: { type: 'string', description: 'Domain ID' }
|
| 80 |
+
}
|
| 81 |
+
},
|
| 82 |
+
body: {
|
| 83 |
+
type: 'object',
|
| 84 |
+
required: ['type', 'hostname', 'content'],
|
| 85 |
+
properties: {
|
| 86 |
+
type: { type: 'string', enum: ['A', 'CNAME'], description: 'DNS record type' },
|
| 87 |
+
hostname: { type: 'string', description: 'Hostname (e.g., @ for root, www for subdomain)' },
|
| 88 |
+
content: { type: 'string', description: 'IP address for A records, target domain for CNAME' },
|
| 89 |
+
note: { type: 'string', description: 'Optional note for the record' }
|
| 90 |
+
}
|
| 91 |
+
},
|
| 92 |
+
response: {
|
| 93 |
+
201: {
|
| 94 |
+
type: 'object',
|
| 95 |
+
properties: {
|
| 96 |
+
id: { type: 'string' },
|
| 97 |
+
zoneIdRecordId: { type: 'string' },
|
| 98 |
+
type: { type: 'string' },
|
| 99 |
+
hostname: { type: 'string' },
|
| 100 |
+
content: { type: 'string' },
|
| 101 |
+
note: { type: 'string' },
|
| 102 |
+
domainId: { type: 'string' }
|
| 103 |
+
}
|
| 104 |
+
}
|
| 105 |
+
}
|
| 106 |
+
}
|
| 107 |
+
}, async (request, reply) => {
|
| 108 |
+
const { domainId } = request.params;
|
| 109 |
+
const { type, hostname, content, note } = request.body;
|
| 110 |
+
|
| 111 |
+
const domain = await prisma.domain.findUnique({
|
| 112 |
+
where: { id: domainId },
|
| 113 |
+
include: { account: true }
|
| 114 |
+
});
|
| 115 |
+
|
| 116 |
+
if (!domain) {
|
| 117 |
+
return reply.code(404).send({ error: 'Domain not found' });
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
if (!domain.account.refreshToken) {
|
| 121 |
+
return reply.code(400).send({ error: 'Account has no refresh token' });
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
const api = new ZoneIdAPI();
|
| 125 |
+
api.setRefreshTokenCookie(domain.account.refreshToken);
|
| 126 |
+
|
| 127 |
+
try {
|
| 128 |
+
await api.refreshAccessToken();
|
| 129 |
+
} catch (err) {
|
| 130 |
+
return reply.code(401).send({ error: 'Failed to authenticate with Zone.ID', details: err.message });
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
let zoneIdRecord;
|
| 134 |
+
try {
|
| 135 |
+
zoneIdRecord = await api.createDnsRecord(domain.zoneIdDomainId, {
|
| 136 |
+
type,
|
| 137 |
+
hostname,
|
| 138 |
+
content,
|
| 139 |
+
note
|
| 140 |
+
});
|
| 141 |
+
} catch (err) {
|
| 142 |
+
return reply.code(400).send({ error: 'Failed to create DNS record', details: err.response?.data || err.message });
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
const zoneIdRecordId = zoneIdRecord?.id || zoneIdRecord?.data?.id;
|
| 146 |
+
if (!zoneIdRecordId) {
|
| 147 |
+
return reply.code(500).send({ error: 'DNS record created but ID not returned' });
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
const record = await prisma.dnsRecord.create({
|
| 151 |
+
data: {
|
| 152 |
+
zoneIdRecordId,
|
| 153 |
+
type,
|
| 154 |
+
hostname,
|
| 155 |
+
content,
|
| 156 |
+
note,
|
| 157 |
+
domainId
|
| 158 |
+
}
|
| 159 |
+
});
|
| 160 |
+
|
| 161 |
+
reply.code(201);
|
| 162 |
+
return {
|
| 163 |
+
id: record.id,
|
| 164 |
+
zoneIdRecordId,
|
| 165 |
+
type: record.type,
|
| 166 |
+
hostname: record.hostname,
|
| 167 |
+
content: record.content,
|
| 168 |
+
note: record.note,
|
| 169 |
+
domainId: record.domainId
|
| 170 |
+
};
|
| 171 |
+
});
|
| 172 |
+
|
| 173 |
+
fastify.put('/:domainId/records/:recordId', {
|
| 174 |
+
schema: {
|
| 175 |
+
summary: 'Update a DNS record',
|
| 176 |
+
tags: ['DNS Records'],
|
| 177 |
+
security: [{ apiKey: [] }],
|
| 178 |
+
params: {
|
| 179 |
+
type: 'object',
|
| 180 |
+
required: ['domainId', 'recordId'],
|
| 181 |
+
properties: {
|
| 182 |
+
domainId: { type: 'string', description: 'Domain ID' },
|
| 183 |
+
recordId: { type: 'string', description: 'DNS Record ID' }
|
| 184 |
+
}
|
| 185 |
+
},
|
| 186 |
+
body: {
|
| 187 |
+
type: 'object',
|
| 188 |
+
properties: {
|
| 189 |
+
type: { type: 'string', enum: ['A', 'CNAME'], description: 'DNS record type' },
|
| 190 |
+
hostname: { type: 'string', description: 'Hostname' },
|
| 191 |
+
content: { type: 'string', description: 'Record content' },
|
| 192 |
+
note: { type: 'string', description: 'Optional note' }
|
| 193 |
+
}
|
| 194 |
+
},
|
| 195 |
+
response: {
|
| 196 |
+
200: {
|
| 197 |
+
type: 'object',
|
| 198 |
+
properties: {
|
| 199 |
+
id: { type: 'string' },
|
| 200 |
+
type: { type: 'string' },
|
| 201 |
+
hostname: { type: 'string' },
|
| 202 |
+
content: { type: 'string' },
|
| 203 |
+
note: { type: 'string' }
|
| 204 |
+
}
|
| 205 |
+
}
|
| 206 |
+
}
|
| 207 |
+
}
|
| 208 |
+
}, async (request, reply) => {
|
| 209 |
+
const { domainId, recordId } = request.params;
|
| 210 |
+
const { type, hostname, content, note } = request.body;
|
| 211 |
+
|
| 212 |
+
const record = await prisma.dnsRecord.findFirst({
|
| 213 |
+
where: { id: recordId, domainId },
|
| 214 |
+
include: {
|
| 215 |
+
domain: {
|
| 216 |
+
include: { account: true }
|
| 217 |
+
}
|
| 218 |
+
}
|
| 219 |
+
});
|
| 220 |
+
|
| 221 |
+
if (!record) {
|
| 222 |
+
return reply.code(404).send({ error: 'DNS record not found' });
|
| 223 |
+
}
|
| 224 |
+
|
| 225 |
+
if (!record.domain.account.refreshToken) {
|
| 226 |
+
return reply.code(400).send({ error: 'Account has no refresh token' });
|
| 227 |
+
}
|
| 228 |
+
|
| 229 |
+
const api = new ZoneIdAPI();
|
| 230 |
+
api.setRefreshTokenCookie(record.domain.account.refreshToken);
|
| 231 |
+
|
| 232 |
+
try {
|
| 233 |
+
await api.refreshAccessToken();
|
| 234 |
+
} catch (err) {
|
| 235 |
+
return reply.code(401).send({ error: 'Failed to authenticate with Zone.ID', details: err.message });
|
| 236 |
+
}
|
| 237 |
+
|
| 238 |
+
try {
|
| 239 |
+
await api.updateDnsRecord(record.domain.zoneIdDomainId, record.zoneIdRecordId, {
|
| 240 |
+
type,
|
| 241 |
+
hostname,
|
| 242 |
+
content,
|
| 243 |
+
note
|
| 244 |
+
});
|
| 245 |
+
} catch (err) {
|
| 246 |
+
return reply.code(400).send({ error: 'Failed to update DNS record', details: err.response?.data || err.message });
|
| 247 |
+
}
|
| 248 |
+
|
| 249 |
+
const updatedRecord = await prisma.dnsRecord.update({
|
| 250 |
+
where: { id: recordId },
|
| 251 |
+
data: {
|
| 252 |
+
...(type && { type }),
|
| 253 |
+
...(hostname && { hostname }),
|
| 254 |
+
...(content && { content }),
|
| 255 |
+
...(note !== undefined && { note })
|
| 256 |
+
}
|
| 257 |
+
});
|
| 258 |
+
|
| 259 |
+
return {
|
| 260 |
+
id: updatedRecord.id,
|
| 261 |
+
type: updatedRecord.type,
|
| 262 |
+
hostname: updatedRecord.hostname,
|
| 263 |
+
content: updatedRecord.content,
|
| 264 |
+
note: updatedRecord.note
|
| 265 |
+
};
|
| 266 |
+
});
|
| 267 |
+
|
| 268 |
+
fastify.delete('/:domainId/records/:recordId', {
|
| 269 |
+
schema: {
|
| 270 |
+
summary: 'Delete a DNS record',
|
| 271 |
+
tags: ['DNS Records'],
|
| 272 |
+
security: [{ apiKey: [] }],
|
| 273 |
+
params: {
|
| 274 |
+
type: 'object',
|
| 275 |
+
required: ['domainId', 'recordId'],
|
| 276 |
+
properties: {
|
| 277 |
+
domainId: { type: 'string', description: 'Domain ID' },
|
| 278 |
+
recordId: { type: 'string', description: 'DNS Record ID' }
|
| 279 |
+
}
|
| 280 |
+
},
|
| 281 |
+
response: {
|
| 282 |
+
200: {
|
| 283 |
+
type: 'object',
|
| 284 |
+
properties: {
|
| 285 |
+
message: { type: 'string' }
|
| 286 |
+
}
|
| 287 |
+
}
|
| 288 |
+
}
|
| 289 |
+
}
|
| 290 |
+
}, async (request, reply) => {
|
| 291 |
+
const { domainId, recordId } = request.params;
|
| 292 |
+
|
| 293 |
+
const record = await prisma.dnsRecord.findFirst({
|
| 294 |
+
where: { id: recordId, domainId },
|
| 295 |
+
include: {
|
| 296 |
+
domain: {
|
| 297 |
+
include: { account: true }
|
| 298 |
+
}
|
| 299 |
+
}
|
| 300 |
+
});
|
| 301 |
+
|
| 302 |
+
if (!record) {
|
| 303 |
+
return reply.code(404).send({ error: 'DNS record not found' });
|
| 304 |
+
}
|
| 305 |
+
|
| 306 |
+
const api = new ZoneIdAPI();
|
| 307 |
+
api.setRefreshTokenCookie(record.domain.account.refreshToken);
|
| 308 |
+
|
| 309 |
+
try {
|
| 310 |
+
await api.refreshAccessToken();
|
| 311 |
+
await api.deleteDnsRecord(record.domain.zoneIdDomainId, record.zoneIdRecordId);
|
| 312 |
+
} catch (err) {
|
| 313 |
+
fastify.log.error('Failed to delete from Zone.ID:', err.message);
|
| 314 |
+
}
|
| 315 |
+
|
| 316 |
+
await prisma.dnsRecord.delete({ where: { id: recordId } });
|
| 317 |
+
|
| 318 |
+
return { message: 'DNS record deleted' };
|
| 319 |
+
});
|
| 320 |
+
|
| 321 |
+
fastify.post('/:domainId/sync', {
|
| 322 |
+
schema: {
|
| 323 |
+
summary: 'Sync DNS records from Zone.ID',
|
| 324 |
+
description: 'Fetches DNS records from Zone.ID and syncs them to the local database',
|
| 325 |
+
tags: ['DNS Records'],
|
| 326 |
+
security: [{ apiKey: [] }],
|
| 327 |
+
params: {
|
| 328 |
+
type: 'object',
|
| 329 |
+
required: ['domainId'],
|
| 330 |
+
properties: {
|
| 331 |
+
domainId: { type: 'string', description: 'Domain ID' }
|
| 332 |
+
}
|
| 333 |
+
},
|
| 334 |
+
response: {
|
| 335 |
+
200: {
|
| 336 |
+
type: 'object',
|
| 337 |
+
properties: {
|
| 338 |
+
synced: { type: 'integer' },
|
| 339 |
+
records: {
|
| 340 |
+
type: 'array',
|
| 341 |
+
items: {
|
| 342 |
+
type: 'object',
|
| 343 |
+
properties: {
|
| 344 |
+
id: { type: 'string' },
|
| 345 |
+
type: { type: 'string' },
|
| 346 |
+
hostname: { type: 'string' },
|
| 347 |
+
content: { type: 'string' }
|
| 348 |
+
}
|
| 349 |
+
}
|
| 350 |
+
}
|
| 351 |
+
}
|
| 352 |
+
}
|
| 353 |
+
}
|
| 354 |
+
}
|
| 355 |
+
}, async (request, reply) => {
|
| 356 |
+
const { domainId } = request.params;
|
| 357 |
+
|
| 358 |
+
const domain = await prisma.domain.findUnique({
|
| 359 |
+
where: { id: domainId },
|
| 360 |
+
include: { account: true }
|
| 361 |
+
});
|
| 362 |
+
|
| 363 |
+
if (!domain) {
|
| 364 |
+
return reply.code(404).send({ error: 'Domain not found' });
|
| 365 |
+
}
|
| 366 |
+
|
| 367 |
+
if (!domain.account.refreshToken) {
|
| 368 |
+
return reply.code(400).send({ error: 'Account has no refresh token' });
|
| 369 |
+
}
|
| 370 |
+
|
| 371 |
+
const api = new ZoneIdAPI();
|
| 372 |
+
api.setRefreshTokenCookie(domain.account.refreshToken);
|
| 373 |
+
|
| 374 |
+
try {
|
| 375 |
+
await api.refreshAccessToken();
|
| 376 |
+
} catch (err) {
|
| 377 |
+
return reply.code(401).send({ error: 'Failed to authenticate with Zone.ID', details: err.message });
|
| 378 |
+
}
|
| 379 |
+
|
| 380 |
+
let zoneIdRecords;
|
| 381 |
+
try {
|
| 382 |
+
const response = await api.getDnsRecords(domain.zoneIdDomainId);
|
| 383 |
+
zoneIdRecords = response?.data || response || [];
|
| 384 |
+
} catch (err) {
|
| 385 |
+
return reply.code(400).send({ error: 'Failed to fetch DNS records from Zone.ID', details: err.response?.data || err.message });
|
| 386 |
+
}
|
| 387 |
+
|
| 388 |
+
const syncedRecords = [];
|
| 389 |
+
for (const zr of zoneIdRecords) {
|
| 390 |
+
const existing = await prisma.dnsRecord.findUnique({
|
| 391 |
+
where: { zoneIdRecordId: zr.id }
|
| 392 |
+
});
|
| 393 |
+
|
| 394 |
+
if (!existing) {
|
| 395 |
+
const record = await prisma.dnsRecord.create({
|
| 396 |
+
data: {
|
| 397 |
+
zoneIdRecordId: zr.id,
|
| 398 |
+
type: zr.type,
|
| 399 |
+
hostname: zr.hostname,
|
| 400 |
+
content: zr.content,
|
| 401 |
+
note: zr.note,
|
| 402 |
+
domainId
|
| 403 |
+
}
|
| 404 |
+
});
|
| 405 |
+
syncedRecords.push(record);
|
| 406 |
+
}
|
| 407 |
+
}
|
| 408 |
+
|
| 409 |
+
return {
|
| 410 |
+
synced: syncedRecords.length,
|
| 411 |
+
records: syncedRecords.map(r => ({
|
| 412 |
+
id: r.id,
|
| 413 |
+
type: r.type,
|
| 414 |
+
hostname: r.hostname,
|
| 415 |
+
content: r.content
|
| 416 |
+
}))
|
| 417 |
+
};
|
| 418 |
+
});
|
| 419 |
+
}
|
| 420 |
+
|
| 421 |
+
module.exports = dnsRoutes;
|
src/routes/domains.js
ADDED
|
@@ -0,0 +1,283 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const { PrismaClient } = require('@prisma/client');
|
| 2 |
+
const { ZoneIdAPI } = require('../../lib/zoneid-api');
|
| 3 |
+
const { getOrCreateAvailableAccount } = require('../../lib/account-creator');
|
| 4 |
+
|
| 5 |
+
const prisma = new PrismaClient();
|
| 6 |
+
|
| 7 |
+
async function domainRoutes(fastify, options) {
|
| 8 |
+
fastify.get('/', {
|
| 9 |
+
schema: {
|
| 10 |
+
summary: 'List all domains',
|
| 11 |
+
tags: ['Domains'],
|
| 12 |
+
security: [{ apiKey: [] }],
|
| 13 |
+
querystring: {
|
| 14 |
+
type: 'object',
|
| 15 |
+
properties: {
|
| 16 |
+
suffix: { type: 'string', enum: ['zone.id', 'nett.to'] },
|
| 17 |
+
accountId: { type: 'string' }
|
| 18 |
+
}
|
| 19 |
+
},
|
| 20 |
+
response: {
|
| 21 |
+
200: {
|
| 22 |
+
type: 'object',
|
| 23 |
+
properties: {
|
| 24 |
+
domains: {
|
| 25 |
+
type: 'array',
|
| 26 |
+
items: {
|
| 27 |
+
type: 'object',
|
| 28 |
+
properties: {
|
| 29 |
+
id: { type: 'string' },
|
| 30 |
+
subdomain: { type: 'string' },
|
| 31 |
+
suffix: { type: 'string' },
|
| 32 |
+
fullDomain: { type: 'string' },
|
| 33 |
+
status: { type: 'string' },
|
| 34 |
+
accountId: { type: 'string' },
|
| 35 |
+
createdAt: { type: 'string' }
|
| 36 |
+
}
|
| 37 |
+
}
|
| 38 |
+
}
|
| 39 |
+
}
|
| 40 |
+
}
|
| 41 |
+
}
|
| 42 |
+
}
|
| 43 |
+
}, async (request, reply) => {
|
| 44 |
+
const { suffix, accountId } = request.query;
|
| 45 |
+
|
| 46 |
+
const domains = await prisma.domain.findMany({
|
| 47 |
+
where: {
|
| 48 |
+
...(suffix && { suffix }),
|
| 49 |
+
...(accountId && { accountId })
|
| 50 |
+
},
|
| 51 |
+
include: {
|
| 52 |
+
account: {
|
| 53 |
+
select: { email: true }
|
| 54 |
+
}
|
| 55 |
+
}
|
| 56 |
+
});
|
| 57 |
+
|
| 58 |
+
return {
|
| 59 |
+
domains: domains.map(d => ({
|
| 60 |
+
id: d.id,
|
| 61 |
+
subdomain: d.subdomain,
|
| 62 |
+
suffix: d.suffix,
|
| 63 |
+
fullDomain: `${d.subdomain}.${d.suffix}`,
|
| 64 |
+
status: d.status,
|
| 65 |
+
accountId: d.accountId,
|
| 66 |
+
accountEmail: d.account.email,
|
| 67 |
+
createdAt: d.createdAt
|
| 68 |
+
}))
|
| 69 |
+
};
|
| 70 |
+
});
|
| 71 |
+
|
| 72 |
+
fastify.post('/', {
|
| 73 |
+
schema: {
|
| 74 |
+
summary: 'Register a new domain',
|
| 75 |
+
description: 'Create a new subdomain on zone.id or nett.to',
|
| 76 |
+
tags: ['Domains'],
|
| 77 |
+
security: [{ apiKey: [] }],
|
| 78 |
+
body: {
|
| 79 |
+
type: 'object',
|
| 80 |
+
required: ['subdomain', 'suffix'],
|
| 81 |
+
properties: {
|
| 82 |
+
subdomain: { type: 'string', minLength: 3, description: 'Subdomain name (without suffix)' },
|
| 83 |
+
suffix: { type: 'string', enum: ['zone.id', 'nett.to'], description: 'Domain suffix' },
|
| 84 |
+
usageType: { type: 'string', description: 'Usage type (e.g., Blog, Portfolio)' },
|
| 85 |
+
usageDescription: { type: 'string', description: 'Brief description of usage' },
|
| 86 |
+
accountId: { type: 'string', description: 'Account ID to use (optional, uses available account if not specified)' }
|
| 87 |
+
}
|
| 88 |
+
},
|
| 89 |
+
response: {
|
| 90 |
+
201: {
|
| 91 |
+
type: 'object',
|
| 92 |
+
properties: {
|
| 93 |
+
id: { type: 'string' },
|
| 94 |
+
subdomain: { type: 'string' },
|
| 95 |
+
suffix: { type: 'string' },
|
| 96 |
+
fullDomain: { type: 'string' },
|
| 97 |
+
zoneIdDomainId: { type: 'string' },
|
| 98 |
+
accountId: { type: 'string' }
|
| 99 |
+
}
|
| 100 |
+
}
|
| 101 |
+
}
|
| 102 |
+
}
|
| 103 |
+
}, async (request, reply) => {
|
| 104 |
+
const { subdomain, suffix, usageType, usageDescription, accountId } = request.body;
|
| 105 |
+
|
| 106 |
+
const existing = await prisma.domain.findUnique({
|
| 107 |
+
where: { subdomain: `${subdomain}.${suffix}` }
|
| 108 |
+
});
|
| 109 |
+
if (existing) {
|
| 110 |
+
return reply.code(409).send({ error: 'Domain already registered' });
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
let account;
|
| 114 |
+
if (accountId) {
|
| 115 |
+
account = await prisma.account.findUnique({ where: { id: accountId } });
|
| 116 |
+
if (!account) {
|
| 117 |
+
return reply.code(404).send({ error: 'Account not found' });
|
| 118 |
+
}
|
| 119 |
+
if (account.domainCount >= 10) {
|
| 120 |
+
return reply.code(400).send({ error: 'Account has reached 10 domain limit' });
|
| 121 |
+
}
|
| 122 |
+
} else {
|
| 123 |
+
try {
|
| 124 |
+
account = await getOrCreateAvailableAccount();
|
| 125 |
+
} catch (err) {
|
| 126 |
+
fastify.log.error('Auto-account creation failed:', err);
|
| 127 |
+
return reply.code(500).send({
|
| 128 |
+
error: 'No available account and failed to create new account',
|
| 129 |
+
details: err.message
|
| 130 |
+
});
|
| 131 |
+
}
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
if (!account) {
|
| 135 |
+
return reply.code(500).send({ error: 'Failed to get or create account' });
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
if (!account.refreshToken) {
|
| 139 |
+
return reply.code(400).send({ error: 'Account has no refresh token' });
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
const api = new ZoneIdAPI();
|
| 143 |
+
api.setRefreshTokenCookie(account.refreshToken);
|
| 144 |
+
|
| 145 |
+
try {
|
| 146 |
+
await api.refreshAccessToken();
|
| 147 |
+
} catch (err) {
|
| 148 |
+
return reply.code(401).send({ error: 'Failed to authenticate with Zone.ID', details: err.message });
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
let zoneIdDomain;
|
| 152 |
+
try {
|
| 153 |
+
zoneIdDomain = await api.createSubdomain(subdomain, suffix, 'dns_record', usageType, usageDescription);
|
| 154 |
+
} catch (err) {
|
| 155 |
+
return reply.code(400).send({ error: 'Failed to create domain', details: err.response?.data || err.message });
|
| 156 |
+
}
|
| 157 |
+
|
| 158 |
+
let zoneIdDomainId = zoneIdDomain?.id;
|
| 159 |
+
if (!zoneIdDomainId) {
|
| 160 |
+
const list = await api.listSubdomains();
|
| 161 |
+
const created = list?.subdomains?.find(d => d.subdomain === `${subdomain}.${suffix}`);
|
| 162 |
+
if (created) zoneIdDomainId = created.id;
|
| 163 |
+
}
|
| 164 |
+
|
| 165 |
+
if (!zoneIdDomainId) {
|
| 166 |
+
return reply.code(500).send({ error: 'Domain created but ID not returned' });
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
+
const domain = await prisma.domain.create({
|
| 170 |
+
data: {
|
| 171 |
+
zoneIdDomainId,
|
| 172 |
+
subdomain: `${subdomain}.${suffix}`,
|
| 173 |
+
suffix,
|
| 174 |
+
mode: 'dns_record',
|
| 175 |
+
usageType,
|
| 176 |
+
usageDescription,
|
| 177 |
+
accountId: account.id
|
| 178 |
+
}
|
| 179 |
+
});
|
| 180 |
+
|
| 181 |
+
await prisma.account.update({
|
| 182 |
+
where: { id: account.id },
|
| 183 |
+
data: { domainCount: { increment: 1 } }
|
| 184 |
+
});
|
| 185 |
+
|
| 186 |
+
reply.code(201);
|
| 187 |
+
return {
|
| 188 |
+
id: domain.id,
|
| 189 |
+
subdomain: domain.subdomain,
|
| 190 |
+
suffix: domain.suffix,
|
| 191 |
+
fullDomain: `${subdomain}.${suffix}`,
|
| 192 |
+
zoneIdDomainId,
|
| 193 |
+
accountId: account.id
|
| 194 |
+
};
|
| 195 |
+
});
|
| 196 |
+
|
| 197 |
+
fastify.get('/:id', {
|
| 198 |
+
schema: {
|
| 199 |
+
summary: 'Get domain details',
|
| 200 |
+
tags: ['Domains'],
|
| 201 |
+
security: [{ apiKey: [] }],
|
| 202 |
+
params: {
|
| 203 |
+
type: 'object',
|
| 204 |
+
properties: {
|
| 205 |
+
id: { type: 'string' }
|
| 206 |
+
}
|
| 207 |
+
}
|
| 208 |
+
}
|
| 209 |
+
}, async (request, reply) => {
|
| 210 |
+
const { id } = request.params;
|
| 211 |
+
|
| 212 |
+
const domain = await prisma.domain.findUnique({
|
| 213 |
+
where: { id },
|
| 214 |
+
include: {
|
| 215 |
+
account: { select: { email: true } },
|
| 216 |
+
dnsRecords: true
|
| 217 |
+
}
|
| 218 |
+
});
|
| 219 |
+
|
| 220 |
+
if (!domain) {
|
| 221 |
+
return reply.code(404).send({ error: 'Domain not found' });
|
| 222 |
+
}
|
| 223 |
+
|
| 224 |
+
return {
|
| 225 |
+
id: domain.id,
|
| 226 |
+
subdomain: domain.subdomain,
|
| 227 |
+
suffix: domain.suffix,
|
| 228 |
+
fullDomain: `${domain.subdomain}.${domain.suffix}`,
|
| 229 |
+
status: domain.status,
|
| 230 |
+
usageType: domain.usageType,
|
| 231 |
+
usageDescription: domain.usageDescription,
|
| 232 |
+
accountId: domain.accountId,
|
| 233 |
+
accountEmail: domain.account.email,
|
| 234 |
+
dnsRecords: domain.dnsRecords,
|
| 235 |
+
createdAt: domain.createdAt
|
| 236 |
+
};
|
| 237 |
+
});
|
| 238 |
+
|
| 239 |
+
fastify.delete('/:id', {
|
| 240 |
+
schema: {
|
| 241 |
+
summary: 'Delete domain',
|
| 242 |
+
tags: ['Domains'],
|
| 243 |
+
security: [{ apiKey: [] }],
|
| 244 |
+
params: {
|
| 245 |
+
type: 'object',
|
| 246 |
+
properties: {
|
| 247 |
+
id: { type: 'string' }
|
| 248 |
+
}
|
| 249 |
+
}
|
| 250 |
+
}
|
| 251 |
+
}, async (request, reply) => {
|
| 252 |
+
const { id } = request.params;
|
| 253 |
+
|
| 254 |
+
const domain = await prisma.domain.findUnique({
|
| 255 |
+
where: { id },
|
| 256 |
+
include: { account: true }
|
| 257 |
+
});
|
| 258 |
+
|
| 259 |
+
if (!domain) {
|
| 260 |
+
return reply.code(404).send({ error: 'Domain not found' });
|
| 261 |
+
}
|
| 262 |
+
|
| 263 |
+
const api = new ZoneIdAPI();
|
| 264 |
+
api.setRefreshTokenCookie(domain.account.refreshToken);
|
| 265 |
+
|
| 266 |
+
try {
|
| 267 |
+
await api.refreshAccessToken();
|
| 268 |
+
await api.deleteSubdomain(domain.zoneIdDomainId);
|
| 269 |
+
} catch (err) {
|
| 270 |
+
fastify.log.error('Failed to delete from Zone.ID:', err.message);
|
| 271 |
+
}
|
| 272 |
+
|
| 273 |
+
await prisma.domain.delete({ where: { id } });
|
| 274 |
+
await prisma.account.update({
|
| 275 |
+
where: { id: domain.accountId },
|
| 276 |
+
data: { domainCount: { decrement: 1 } }
|
| 277 |
+
});
|
| 278 |
+
|
| 279 |
+
return { message: 'Domain deleted' };
|
| 280 |
+
});
|
| 281 |
+
}
|
| 282 |
+
|
| 283 |
+
module.exports = domainRoutes;
|
src/server.js
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
require('dotenv').config();
|
| 2 |
+
const Fastify = require('fastify');
|
| 3 |
+
const cors = require('@fastify/cors');
|
| 4 |
+
const swagger = require('@fastify/swagger');
|
| 5 |
+
const swaggerUi = require('@fastify/swagger-ui');
|
| 6 |
+
|
| 7 |
+
const accountRoutes = require('./routes/accounts');
|
| 8 |
+
const domainRoutes = require('./routes/domains');
|
| 9 |
+
const dnsRoutes = require('./routes/dns');
|
| 10 |
+
const { startTokenScheduler } = require('../lib/token-scheduler');
|
| 11 |
+
|
| 12 |
+
const fastify = Fastify({
|
| 13 |
+
logger: true
|
| 14 |
+
});
|
| 15 |
+
|
| 16 |
+
async function build() {
|
| 17 |
+
await fastify.register(cors, {
|
| 18 |
+
origin: true
|
| 19 |
+
});
|
| 20 |
+
|
| 21 |
+
await fastify.register(swagger, {
|
| 22 |
+
openapi: {
|
| 23 |
+
info: {
|
| 24 |
+
title: 'Zone.ID Domain Registration API',
|
| 25 |
+
description: 'API for managing zone.id and nett.to domain registrations and DNS records',
|
| 26 |
+
version: '1.0.0'
|
| 27 |
+
},
|
| 28 |
+
servers: [
|
| 29 |
+
{ url: 'http://localhost:5000', description: 'Development server' }
|
| 30 |
+
],
|
| 31 |
+
components: {
|
| 32 |
+
securitySchemes: {
|
| 33 |
+
apiKey: {
|
| 34 |
+
type: 'apiKey',
|
| 35 |
+
name: 'X-API-Key',
|
| 36 |
+
in: 'header'
|
| 37 |
+
}
|
| 38 |
+
}
|
| 39 |
+
}
|
| 40 |
+
}
|
| 41 |
+
});
|
| 42 |
+
|
| 43 |
+
await fastify.register(swaggerUi, {
|
| 44 |
+
routePrefix: '/docs',
|
| 45 |
+
uiConfig: {
|
| 46 |
+
docExpansion: 'list',
|
| 47 |
+
deepLinking: false
|
| 48 |
+
}
|
| 49 |
+
});
|
| 50 |
+
|
| 51 |
+
fastify.addHook('onRequest', async (request, reply) => {
|
| 52 |
+
const publicRoutes = ['/docs', '/docs/', '/docs/json', '/docs/yaml', '/health', '/'];
|
| 53 |
+
const isPublic = publicRoutes.some(route => request.url.startsWith(route));
|
| 54 |
+
|
| 55 |
+
if (isPublic) return;
|
| 56 |
+
|
| 57 |
+
const apiKey = request.headers['x-api-key'];
|
| 58 |
+
if (!apiKey) {
|
| 59 |
+
reply.code(401).send({ error: 'API key required' });
|
| 60 |
+
return;
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
const { PrismaClient } = require('@prisma/client');
|
| 64 |
+
const prisma = new PrismaClient();
|
| 65 |
+
|
| 66 |
+
try {
|
| 67 |
+
const key = await prisma.apiKey.findUnique({ where: { key: apiKey } });
|
| 68 |
+
if (!key || !key.isActive) {
|
| 69 |
+
reply.code(401).send({ error: 'Invalid API key' });
|
| 70 |
+
return;
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
await prisma.apiKey.update({
|
| 74 |
+
where: { id: key.id },
|
| 75 |
+
data: { lastUsedAt: new Date() }
|
| 76 |
+
});
|
| 77 |
+
} finally {
|
| 78 |
+
await prisma.$disconnect();
|
| 79 |
+
}
|
| 80 |
+
});
|
| 81 |
+
|
| 82 |
+
fastify.get('/', {
|
| 83 |
+
schema: {
|
| 84 |
+
hide: true
|
| 85 |
+
}
|
| 86 |
+
}, async () => {
|
| 87 |
+
return {
|
| 88 |
+
name: 'Zone.ID Domain Registration API',
|
| 89 |
+
version: '1.0.0',
|
| 90 |
+
docs: '/docs'
|
| 91 |
+
};
|
| 92 |
+
});
|
| 93 |
+
|
| 94 |
+
fastify.get('/health', {
|
| 95 |
+
schema: {
|
| 96 |
+
hide: true
|
| 97 |
+
}
|
| 98 |
+
}, async () => {
|
| 99 |
+
return { status: 'ok' };
|
| 100 |
+
});
|
| 101 |
+
|
| 102 |
+
await fastify.register(accountRoutes, { prefix: '/api/accounts' });
|
| 103 |
+
await fastify.register(domainRoutes, { prefix: '/api/domains' });
|
| 104 |
+
await fastify.register(dnsRoutes, { prefix: '/api/dns' });
|
| 105 |
+
|
| 106 |
+
return fastify;
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
async function start() {
|
| 110 |
+
try {
|
| 111 |
+
const port = parseInt(process.env.PORT) || 5000;
|
| 112 |
+
const host = process.env.HOST || '0.0.0.0';
|
| 113 |
+
const server = await build();
|
| 114 |
+
await server.listen({ port, host });
|
| 115 |
+
console.log(`Server running at http://${host}:${port}`);
|
| 116 |
+
console.log(`API Documentation at http://${host}:${port}/docs`);
|
| 117 |
+
|
| 118 |
+
startTokenScheduler();
|
| 119 |
+
console.log('Token auto-refresh scheduler started (checks every 6 hours)');
|
| 120 |
+
} catch (err) {
|
| 121 |
+
console.error(err);
|
| 122 |
+
process.exit(1);
|
| 123 |
+
}
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
start();
|