cz4ehs commited on
Commit
03cdf80
·
1 Parent(s): 942a4ce

Deploy Zone.ID Domain API 2025-12-07

Browse files
.gitattributes CHANGED
@@ -1,35 +1,2 @@
1
- *.7z filter=lfs diff=lfs merge=lfs -text
2
- *.arrow filter=lfs diff=lfs merge=lfs -text
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 Reg
3
- emoji: 🐨
4
- colorFrom: green
5
- colorTo: gray
6
  sdk: docker
 
7
  pinned: false
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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();