Hdhdjdjd dhjdbd hjdhd commited on
Commit
3d8743c
·
1 Parent(s): 6e196a9

Deploy Zone.ID Domain API 2025-12-07

Browse files
lib/account-creator.js CHANGED
@@ -3,7 +3,7 @@ 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
 
@@ -52,11 +52,18 @@ async function extractOtpFromEmail(emailHtml) {
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) {
@@ -75,12 +82,24 @@ async function loginToAutz(email, password) {
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;
@@ -146,6 +165,7 @@ async function getZoneIdRefreshToken(email, password, retryCount = 0) {
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 });
@@ -154,6 +174,7 @@ async function getZoneIdRefreshToken(email, password, retryCount = 0) {
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...');
@@ -172,6 +193,7 @@ async function getZoneIdRefreshToken(email, password, retryCount = 0) {
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++) {
@@ -190,6 +212,7 @@ async function getZoneIdRefreshToken(email, password, retryCount = 0) {
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');
@@ -211,6 +234,7 @@ async function getZoneIdRefreshToken(email, password, retryCount = 0) {
211
 
212
  console.log('[OAuth] Login submitted, waiting for redirect...');
213
  await new Promise(r => setTimeout(r, 5000));
 
214
  }
215
 
216
  if (!refreshToken) {
@@ -228,20 +252,29 @@ async function getZoneIdRefreshToken(email, password, retryCount = 0) {
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) {
@@ -305,86 +338,131 @@ async function createAndActivateAccount() {
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 (5 min timeout)...');
317
- const verificationEmail = await waitForEmail(email, 300000, 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
  const thirtyMinutesAgo = new Date(Date.now() - 30 * 60 * 1000);
367
 
368
- let account = await prisma.account.findFirst({
 
 
 
 
 
 
 
369
  where: {
370
  status: 'active',
371
  domainCount: { lt: 10 },
372
  refreshToken: { not: null },
373
- OR: [
374
- { lastDomainRegistration: null },
375
- { lastDomainRegistration: { lt: thirtyMinutesAgo } }
376
- ]
377
  },
378
- orderBy: { domainCount: 'asc' }
379
  });
380
 
381
- if (account) {
382
- console.log(`Using existing account ${account.email} (last registration: ${account.lastDomainRegistration || 'never'})`);
383
- return account;
384
  }
385
 
386
- console.log('No available account (all are rate-limited or full). Creating new account...');
387
- return await createAndActivateAccount();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
388
  }
389
 
390
  module.exports = {
 
3
  const axios = require('axios');
4
  const cheerio = require('cheerio');
5
  const { generateEmail, waitForEmail, deleteAddress } = require('./temp-email');
6
+ const { createAutzAccountWithBrowser, enterOtpAndActivate } = require('./autz-account');
7
 
8
  const prisma = new PrismaClient();
9
 
 
52
  }
53
 
54
  async function activateAccount(userTokenId, otp) {
55
+ try {
56
+ console.log(`[Activate] Calling API with userTokenId=${userTokenId}, otp=${otp}`);
57
+ const response = await axios.post(`${AUTZ_API}/activate`, {
58
+ user_token_id: userTokenId,
59
+ otp
60
+ });
61
+ console.log('[Activate] API response:', JSON.stringify(response.data));
62
+ return response.data;
63
+ } catch (err) {
64
+ console.error('[Activate] API error:', err.response?.data || err.message);
65
+ throw err;
66
+ }
67
  }
68
 
69
  async function loginToAutz(email, password) {
 
82
 
83
  async function getZoneIdRefreshToken(email, password, retryCount = 0) {
84
  const puppeteer = require('puppeteer-core');
85
+ const path = require('path');
86
  const { solveTurnstile } = require('./autz-account');
87
 
88
  const CHROMIUM_PATH = process.env.CHROMIUM_PATH || process.env.PUPPETEER_EXECUTABLE_PATH || '/nix/store/khk7xpgsm5insk81azy9d560yq4npf77-chromium-131.0.6778.204/bin/chromium';
89
  const ONBOARDING_URL = 'https://autz.org/onboarding/qinw2ix?callback_url=https%3A%2F%2Fmy.zone.id%2F';
90
+ const SCREENSHOTS_DIR = path.join(__dirname, '..', 'screenshots');
91
  const MAX_RETRIES = 2;
92
 
93
+ async function saveScreenshot(page, name) {
94
+ try {
95
+ const filePath = path.join(SCREENSHOTS_DIR, `oauth_${name}_${Date.now()}.png`);
96
+ await page.screenshot({ path: filePath, fullPage: true });
97
+ console.log(`[OAuth Screenshot] Saved: ${filePath}`);
98
+ } catch (err) {
99
+ console.error(`[OAuth Screenshot] Failed: ${err.message}`);
100
+ }
101
+ }
102
+
103
  console.log(`[OAuth] Starting Zone.ID OAuth flow for ${email} (attempt ${retryCount + 1}/${MAX_RETRIES + 1})`);
104
 
105
  let turnstileToken;
 
165
 
166
  console.log('[OAuth] Navigating to onboarding URL...');
167
  await page.goto(ONBOARDING_URL, { waitUntil: 'networkidle2', timeout: 60000 });
168
+ await saveScreenshot(page, '01_initial_page');
169
 
170
  try {
171
  await page.waitForSelector('#login-modal', { visible: true, timeout: 30000 });
 
174
  console.log('[OAuth] Login modal not found, checking page state...');
175
  const currentUrl = page.url();
176
  console.log('[OAuth] Current URL:', currentUrl);
177
+ await saveScreenshot(page, '01_no_modal');
178
  }
179
 
180
  console.log('[OAuth] Entering email...');
 
193
  await new Promise(r => setTimeout(r, 1000));
194
  await page.click('button.btn-primary');
195
  console.log('[OAuth] Submitted email, waiting for password field...');
196
+ await saveScreenshot(page, '02_after_email_submit');
197
 
198
  let passwordFieldFound = false;
199
  for (let i = 0; i < 10; i++) {
 
212
  }
213
 
214
  if (!passwordFieldFound) {
215
+ await saveScreenshot(page, '03_no_password_field');
216
  const pageText = await page.evaluate(() => document.body.innerText);
217
  console.log('[OAuth] Page content after email submit:', pageText.substring(0, 500));
218
  throw new Error('Password field did not appear - account may not be properly activated');
 
234
 
235
  console.log('[OAuth] Login submitted, waiting for redirect...');
236
  await new Promise(r => setTimeout(r, 5000));
237
+ await saveScreenshot(page, '04_after_login');
238
  }
239
 
240
  if (!refreshToken) {
 
252
  if (!refreshToken) {
253
  const pageText = await page.evaluate(() => document.body.innerText);
254
  if (pageText.includes('Choose account') || pageText.includes('Select account')) {
255
+ console.log('[OAuth] Account selection page detected, clicking first account...');
256
+ await saveScreenshot(page, '05_account_selection');
257
 
258
+ await Promise.all([
259
+ page.waitForNavigation({ waitUntil: 'networkidle2', timeout: 30000 }).catch(() => {}),
260
+ page.evaluate(() => {
261
+ const rows = document.querySelectorAll('[class*="account"], [class*="list-group-item"], div[role="button"], a[href*="authorize"]');
262
+ if (rows.length > 0) {
263
+ rows[0].click();
264
+ } else {
265
+ const allDivs = document.querySelectorAll('div');
266
+ for (const div of allDivs) {
267
+ if (div.textContent && div.textContent.includes('@') && div.querySelector) {
268
+ div.click();
269
+ break;
270
+ }
271
+ }
272
  }
273
+ })
274
+ ]);
275
 
276
  await new Promise(r => setTimeout(r, 5000));
277
+ await saveScreenshot(page, '06_after_account_select');
278
 
279
  const cookies = await page.cookies('https://my.zone.id');
280
  for (const cookie of cookies) {
 
338
  const name = generateName();
339
  const phone = generatePhone();
340
 
341
+ let browser = null;
 
 
 
 
 
 
 
 
 
342
 
 
 
 
 
 
 
 
 
 
 
 
343
  try {
344
+ console.log('Creating Autz.org account...');
345
+ const accountResult = await createAutzAccountWithBrowser(email, password, name, phone);
346
+
347
+ if (!accountResult.success) {
348
+ throw new Error('Failed to create Autz.org account');
349
+ }
350
+
351
+ browser = accountResult.browser;
352
+ const activePage = accountResult.activePage;
353
+
354
+ console.log('Waiting for verification email (5 min timeout)...');
355
+ const verificationEmail = await waitForEmail(email, 300000, 5000);
356
+
357
+ const otp = await extractOtpFromEmail(verificationEmail.body || verificationEmail.html || verificationEmail.text);
358
+ if (!otp) {
359
+ throw new Error('Could not extract OTP from email');
360
+ }
361
+
362
+ console.log(`Activating account with OTP: ${otp} via page form...`);
363
+ await enterOtpAndActivate(activePage, otp);
364
+ console.log('Account activated successfully via page form!');
365
+
366
+ await browser.close();
367
+ browser = null;
368
+
369
+ console.log('Getting Zone.ID refresh token via OAuth flow...');
370
+ let refreshToken = null;
371
  refreshToken = await getZoneIdRefreshToken(email, password);
372
+
373
+ if (!refreshToken) {
 
 
374
  throw new Error('Could not obtain Zone.ID refresh token - account created but not usable');
375
  }
376
+
377
+ console.log('Successfully obtained Zone.ID refresh token');
378
+
379
+ const passwordHash = await bcrypt.hash(password, 10);
380
+
381
+ const account = await prisma.account.create({
382
+ data: {
383
+ email,
384
+ passwordHash,
385
+ name,
386
+ phone,
387
+ refreshToken,
388
+ status: 'active',
389
+ domainCount: 0
390
+ }
391
+ });
392
+
393
+ await deleteAddress(email);
394
+
395
+ console.log(`Account created successfully: ${account.id}`);
396
+
397
+ return account;
398
+
399
  } catch (err) {
400
+ console.error('[CreateAccount] Error:', err.message);
401
+ if (browser) {
402
+ try { await browser.close(); } catch (e) {}
403
+ }
404
  await deleteAddress(email);
405
  throw err;
406
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
407
  }
408
 
409
  async function getOrCreateAvailableAccount() {
410
  const thirtyMinutesAgo = new Date(Date.now() - 30 * 60 * 1000);
411
 
412
+ // Strategy: Always create a fresh account first for immediate domain registration
413
+ // Only fall back to existing accounts if:
414
+ // 1. Account creation fails, OR
415
+ // 2. We want to reuse accounts whose rate limit has expired
416
+
417
+ // First, check if there's an OLD account whose rate limit has expired (30+ mins ago)
418
+ // These are accounts that have been used before and are now available again
419
+ const expiredRateLimitAccount = await prisma.account.findFirst({
420
  where: {
421
  status: 'active',
422
  domainCount: { lt: 10 },
423
  refreshToken: { not: null },
424
+ lastDomainRegistration: {
425
+ not: null,
426
+ lt: thirtyMinutesAgo
427
+ }
428
  },
429
+ orderBy: { lastDomainRegistration: 'asc' } // Use oldest first (longest since last use)
430
  });
431
 
432
+ if (expiredRateLimitAccount) {
433
+ console.log(`[Round-Robin] Reusing account ${expiredRateLimitAccount.email} - rate limit expired (last used: ${expiredRateLimitAccount.lastDomainRegistration})`);
434
+ return expiredRateLimitAccount;
435
  }
436
 
437
+ // No expired rate-limit accounts available, create a fresh new account
438
+ console.log('[Round-Robin] No accounts with expired rate limit. Creating fresh account for immediate registration...');
439
+
440
+ try {
441
+ const newAccount = await createAndActivateAccount();
442
+ console.log(`[Round-Robin] Fresh account created: ${newAccount.email}`);
443
+ return newAccount;
444
+ } catch (err) {
445
+ console.error('[Round-Robin] Failed to create new account:', err.message);
446
+
447
+ // Fallback: Check for any never-used account (lastDomainRegistration is null)
448
+ const unusedAccount = await prisma.account.findFirst({
449
+ where: {
450
+ status: 'active',
451
+ domainCount: { lt: 10 },
452
+ refreshToken: { not: null },
453
+ lastDomainRegistration: null
454
+ },
455
+ orderBy: { createdAt: 'asc' }
456
+ });
457
+
458
+ if (unusedAccount) {
459
+ console.log(`[Round-Robin] Fallback: Using unused account ${unusedAccount.email}`);
460
+ return unusedAccount;
461
+ }
462
+
463
+ // No options left
464
+ throw new Error('Failed to create new account and no available accounts exist');
465
+ }
466
  }
467
 
468
  module.exports = {
lib/autz-account.js CHANGED
@@ -1,10 +1,25 @@
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, {
@@ -19,7 +34,7 @@ async function solveTurnstile() {
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({
@@ -28,7 +43,6 @@ async function createAutzAccount(email, password, name, phone) {
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 {
@@ -37,16 +51,11 @@ async function createAutzAccount(email, password, name, phone) {
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
  });
@@ -64,6 +73,7 @@ async function createAutzAccount(email, password, name, phone) {
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
 
@@ -83,9 +93,11 @@ async function createAutzAccount(email, password, name, phone) {
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
  console.log('[Autz] Registration completed, clicking Activate Now...');
@@ -109,6 +121,7 @@ async function createAutzAccount(email, password, name, phone) {
109
 
110
  let pageText = await activePage.evaluate(() => document.body.innerText);
111
  console.log('[Autz] Current page:', pageText.substring(0, 120).replace(/\n/g, ' '));
 
112
 
113
  if (pageText.toLowerCase().includes('send otp')) {
114
  console.log('[Autz] Found Send OTP page - clicking Send OTP button...');
@@ -122,22 +135,60 @@ async function createAutzAccount(email, password, name, phone) {
122
  });
123
 
124
  await new Promise(r => setTimeout(r, 3000));
 
125
  console.log('[Autz] Send OTP clicked - email should be sent now');
126
  } else {
127
  console.log('[Autz] Send OTP page not found, checking for other activation methods...');
128
  }
129
 
130
  return {
131
- success: !!userTokenId,
132
  email,
133
  userTokenId,
134
- loginToken,
135
- needsActivation: true
136
  };
137
 
138
- } finally {
139
  await browser.close();
 
 
 
 
 
 
 
 
 
 
140
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
141
  }
142
 
143
- module.exports = { createAutzAccount, solveTurnstile };
 
1
  const puppeteer = require('puppeteer-core');
2
  const axios = require('axios');
3
+ const path = require('path');
4
 
5
  const CHROMIUM_PATH = process.env.CHROMIUM_PATH || process.env.PUPPETEER_EXECUTABLE_PATH || '/nix/store/khk7xpgsm5insk81azy9d560yq4npf77-chromium-131.0.6778.204/bin/chromium';
6
  const CF_SOLVER_URL = 'https://samleuma-cf-solver.hf.space/solver';
7
  const TURNSTILE_SITEKEY = '0x4AAAAAAAfqMU3EtZs0r_nN';
8
  const ONBOARDING_URL = 'https://autz.org/onboarding/qinw2ix?callback_url=https%3A%2F%2Fmy.zone.id%2F';
9
+ const SCREENSHOTS_DIR = path.join(__dirname, '..', 'screenshots');
10
+
11
+ async function saveScreenshot(page, name) {
12
+ try {
13
+ const timestamp = Date.now();
14
+ const filePath = path.join(SCREENSHOTS_DIR, `${name}_${timestamp}.png`);
15
+ await page.screenshot({ path: filePath, fullPage: true });
16
+ console.log(`[Screenshot] Saved: ${filePath}`);
17
+ return filePath;
18
+ } catch (err) {
19
+ console.error(`[Screenshot] Failed to save ${name}:`, err.message);
20
+ return null;
21
+ }
22
+ }
23
 
24
  async function solveTurnstile() {
25
  const response = await axios.post(CF_SOLVER_URL, {
 
34
  throw new Error('Failed to solve Turnstile: ' + JSON.stringify(response.data));
35
  }
36
 
37
+ async function createAutzAccountWithBrowser(email, password, name, phone) {
38
  const turnstileToken = await solveTurnstile();
39
 
40
  const browser = await puppeteer.launch({
 
43
  args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage', '--disable-gpu']
44
  });
45
 
 
46
  let userTokenId = null;
47
 
48
  try {
 
51
 
52
  page.on('response', async (response) => {
53
  const url = response.url();
 
 
 
 
 
 
54
  if (url.includes('/api/register') && response.status() === 200) {
55
  try {
56
  const data = await response.json();
57
  userTokenId = data.user_token_id;
58
+ console.log('[Autz] Captured userTokenId:', userTokenId);
59
  } catch (e) {}
60
  }
61
  });
 
73
 
74
  await page.goto(ONBOARDING_URL, { waitUntil: 'networkidle2', timeout: 60000 });
75
  await page.waitForSelector('#login-modal', { visible: true, timeout: 30000 });
76
+ await saveScreenshot(page, '01_login_modal');
77
 
78
  await page.type('input[type="email"]', email, { delay: 20 });
79
 
 
93
  await inputs[1].type(password, { delay: 15 });
94
  await inputs[2].type(name, { delay: 15 });
95
  await inputs[3].type(phone, { delay: 15 });
96
+ await saveScreenshot(page, '02_registration_form_filled');
97
 
98
  await page.click('button.btn-primary');
99
  await new Promise(r => setTimeout(r, 5000));
100
+ await saveScreenshot(page, '03_after_registration_submit');
101
  }
102
 
103
  console.log('[Autz] Registration completed, clicking Activate Now...');
 
121
 
122
  let pageText = await activePage.evaluate(() => document.body.innerText);
123
  console.log('[Autz] Current page:', pageText.substring(0, 120).replace(/\n/g, ' '));
124
+ await saveScreenshot(activePage, '04_after_activate_click');
125
 
126
  if (pageText.toLowerCase().includes('send otp')) {
127
  console.log('[Autz] Found Send OTP page - clicking Send OTP button...');
 
135
  });
136
 
137
  await new Promise(r => setTimeout(r, 3000));
138
+ await saveScreenshot(activePage, '05_after_send_otp_click');
139
  console.log('[Autz] Send OTP clicked - email should be sent now');
140
  } else {
141
  console.log('[Autz] Send OTP page not found, checking for other activation methods...');
142
  }
143
 
144
  return {
145
+ success: true,
146
  email,
147
  userTokenId,
148
+ browser,
149
+ activePage
150
  };
151
 
152
+ } catch (err) {
153
  await browser.close();
154
+ throw err;
155
+ }
156
+ }
157
+
158
+ async function enterOtpAndActivate(activePage, otp) {
159
+ console.log(`[Autz] Entering OTP: ${otp}`);
160
+
161
+ const otpInput = await activePage.$('input');
162
+ if (!otpInput) {
163
+ throw new Error('OTP input field not found');
164
  }
165
+
166
+ await otpInput.click({ clickCount: 3 });
167
+ await otpInput.type(otp, { delay: 50 });
168
+ await saveScreenshot(activePage, '06_otp_entered');
169
+
170
+ await activePage.evaluate(() => {
171
+ document.querySelectorAll('button').forEach(btn => {
172
+ if (btn.textContent?.toLowerCase().includes('submit') ||
173
+ btn.textContent?.toLowerCase().includes('verify') ||
174
+ btn.textContent?.toLowerCase().includes('activate')) {
175
+ btn.click();
176
+ }
177
+ });
178
+ });
179
+
180
+ console.log('[Autz] OTP submitted, waiting for activation...');
181
+ await new Promise(r => setTimeout(r, 5000));
182
+ await saveScreenshot(activePage, '07_after_otp_submit');
183
+
184
+ const pageText = await activePage.evaluate(() => document.body.innerText);
185
+ console.log('[Autz] Page after OTP submit:', pageText.substring(0, 200).replace(/\n/g, ' '));
186
+
187
+ if (pageText.toLowerCase().includes('error') || pageText.toLowerCase().includes('invalid')) {
188
+ throw new Error('OTP activation failed: ' + pageText.substring(0, 100));
189
+ }
190
+
191
+ return true;
192
  }
193
 
194
+ module.exports = { createAutzAccountWithBrowser, enterOtpAndActivate, solveTurnstile, saveScreenshot };
lib/job-worker.js ADDED
@@ -0,0 +1,164 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const { PrismaClient } = require('@prisma/client');
2
+ const { getOrCreateAvailableAccount } = require('./account-creator');
3
+ const { ZoneIdAPI } = require('./zoneid-api');
4
+
5
+ const prisma = new PrismaClient();
6
+
7
+ const JOB_STATUS = {
8
+ PENDING: 'pending',
9
+ FINDING_ACCOUNT: 'finding_account',
10
+ CREATING_ACCOUNT: 'creating_account',
11
+ REGISTERING_DOMAIN: 'registering_domain',
12
+ COMPLETED: 'completed',
13
+ FAILED: 'failed'
14
+ };
15
+
16
+ async function updateJobStatus(jobId, status, message = null, extra = {}) {
17
+ await prisma.domainJob.update({
18
+ where: { id: jobId },
19
+ data: {
20
+ status,
21
+ statusMessage: message,
22
+ updatedAt: new Date(),
23
+ ...extra
24
+ }
25
+ });
26
+ }
27
+
28
+ async function processJob(job) {
29
+ console.log(`[Worker] Processing job ${job.id}: ${job.subdomain}.${job.suffix}`);
30
+
31
+ try {
32
+ await updateJobStatus(job.id, JOB_STATUS.FINDING_ACCOUNT, 'Looking for available account...');
33
+
34
+ let account;
35
+ try {
36
+ if (job.accountId) {
37
+ account = await prisma.account.findUnique({ where: { id: job.accountId } });
38
+ if (!account) {
39
+ await updateJobStatus(job.id, JOB_STATUS.FAILED, 'Specified account not found', { error: 'Account not found' });
40
+ return;
41
+ }
42
+ console.log(`[Worker] Job ${job.id} using specified account: ${account.email}`);
43
+ } else {
44
+ account = await getOrCreateAvailableAccount();
45
+ console.log(`[Worker] Job ${job.id} auto-selected account: ${account.email}`);
46
+ }
47
+ await updateJobStatus(job.id, JOB_STATUS.FINDING_ACCOUNT, `Using account: ${account.email}`, { accountId: account.id });
48
+ } catch (err) {
49
+ console.error(`[Worker] Job ${job.id} failed to get account:`, err.message);
50
+ await updateJobStatus(job.id, JOB_STATUS.FAILED, 'Failed to get or create account', { error: err.message });
51
+ return;
52
+ }
53
+
54
+ if (!account.refreshToken) {
55
+ await updateJobStatus(job.id, JOB_STATUS.FAILED, 'Account has no refresh token', { error: 'No refresh token' });
56
+ return;
57
+ }
58
+
59
+ await updateJobStatus(job.id, JOB_STATUS.REGISTERING_DOMAIN, 'Registering domain on Zone.ID...');
60
+
61
+ const api = new ZoneIdAPI();
62
+ api.setRefreshTokenCookie(account.refreshToken);
63
+
64
+ try {
65
+ await api.refreshAccessToken();
66
+ } catch (err) {
67
+ console.error(`[Worker] Job ${job.id} failed to authenticate:`, err.message);
68
+ await updateJobStatus(job.id, JOB_STATUS.FAILED, 'Failed to authenticate with Zone.ID', { error: err.message });
69
+ return;
70
+ }
71
+
72
+ let zoneIdDomain;
73
+ try {
74
+ zoneIdDomain = await api.createSubdomain(job.subdomain, job.suffix, 'dns_record', job.usageType, job.usageDescription);
75
+ } catch (err) {
76
+ console.error(`[Worker] Job ${job.id} failed to create domain:`, err.message);
77
+ await updateJobStatus(job.id, JOB_STATUS.FAILED, 'Failed to create domain', { error: err.response?.data?.message || err.message });
78
+ return;
79
+ }
80
+
81
+ let zoneIdDomainId = zoneIdDomain?.id;
82
+ if (!zoneIdDomainId) {
83
+ const list = await api.listSubdomains();
84
+ const created = list?.subdomains?.find(d => d.subdomain === `${job.subdomain}.${job.suffix}`);
85
+ if (created) zoneIdDomainId = created.id;
86
+ }
87
+
88
+ if (!zoneIdDomainId) {
89
+ await updateJobStatus(job.id, JOB_STATUS.FAILED, 'Domain created but ID not returned', { error: 'Missing domain ID' });
90
+ return;
91
+ }
92
+
93
+ const domain = await prisma.domain.create({
94
+ data: {
95
+ zoneIdDomainId,
96
+ subdomain: `${job.subdomain}.${job.suffix}`,
97
+ suffix: job.suffix,
98
+ mode: 'dns_record',
99
+ usageType: job.usageType,
100
+ usageDescription: job.usageDescription,
101
+ accountId: account.id
102
+ }
103
+ });
104
+
105
+ await prisma.account.update({
106
+ where: { id: account.id },
107
+ data: {
108
+ domainCount: { increment: 1 },
109
+ lastDomainRegistration: new Date()
110
+ }
111
+ });
112
+
113
+ await updateJobStatus(job.id, JOB_STATUS.COMPLETED, 'Domain registered successfully', {
114
+ domainId: domain.id,
115
+ completedAt: new Date()
116
+ });
117
+
118
+ console.log(`[Worker] Job ${job.id} completed: ${domain.subdomain}`);
119
+
120
+ } catch (err) {
121
+ console.error(`[Worker] Job ${job.id} unexpected error:`, err);
122
+ await updateJobStatus(job.id, JOB_STATUS.FAILED, 'Unexpected error', { error: err.message });
123
+ }
124
+ }
125
+
126
+ async function runWorker() {
127
+ console.log('[Worker] Checking for pending jobs...');
128
+
129
+ const pendingJob = await prisma.domainJob.findFirst({
130
+ where: { status: JOB_STATUS.PENDING },
131
+ orderBy: { createdAt: 'asc' }
132
+ });
133
+
134
+ if (pendingJob) {
135
+ await processJob(pendingJob);
136
+ }
137
+ }
138
+
139
+ let workerInterval = null;
140
+
141
+ function startWorker(intervalMs = 5000) {
142
+ console.log(`[Worker] Starting job worker (polling every ${intervalMs/1000}s)`);
143
+
144
+ runWorker();
145
+
146
+ workerInterval = setInterval(runWorker, intervalMs);
147
+
148
+ return workerInterval;
149
+ }
150
+
151
+ function stopWorker() {
152
+ if (workerInterval) {
153
+ clearInterval(workerInterval);
154
+ workerInterval = null;
155
+ console.log('[Worker] Job worker stopped');
156
+ }
157
+ }
158
+
159
+ module.exports = {
160
+ startWorker,
161
+ stopWorker,
162
+ processJob,
163
+ JOB_STATUS
164
+ };
lib/zoneid-api.js CHANGED
@@ -85,19 +85,24 @@ class ZoneIdAPI {
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;
 
85
  async createSubdomain(subdomain, suffix = 'zone.id', mode = 'dns_record', usageType, usageDescription) {
86
  const fullSubdomain = `${subdomain}.${suffix}`;
87
  console.log('Creating subdomain:', fullSubdomain);
88
+
89
+ const payload = {
90
+ subdomain: subdomain,
91
+ suffix: suffix,
92
+ mode
93
+ };
94
+ if (usageType) payload.usage_type = usageType;
95
+ if (usageDescription) payload.usage_description = usageDescription;
96
+
97
+ console.log('Payload:', JSON.stringify(payload));
98
+ const response = await this.client.post('/subdomains', payload);
99
  console.log('Create response:', JSON.stringify(response.data));
100
 
101
  if (!response.data || !response.data.id) {
102
  console.log('No ID in response, fetching subdomain list...');
103
  const list = await this.listSubdomains();
104
+ if (list && list.subdomains) {
105
+ const created = list.subdomains.find(d => d.subdomain === fullSubdomain);
106
  if (created) {
107
  console.log('Found created subdomain:', created.id);
108
  return created;
prisma/schema.prisma CHANGED
@@ -73,3 +73,22 @@ model ApiKey {
73
  createdAt DateTime @default(now())
74
  lastUsedAt DateTime?
75
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73
  createdAt DateTime @default(now())
74
  lastUsedAt DateTime?
75
  }
76
+
77
+ model DomainJob {
78
+ id String @id @default(cuid())
79
+ subdomain String
80
+ suffix String
81
+ usageType String?
82
+ usageDescription String?
83
+ status String @default("pending")
84
+ statusMessage String?
85
+ error String?
86
+ domainId String?
87
+ accountId String?
88
+ createdAt DateTime @default(now())
89
+ updatedAt DateTime @updatedAt
90
+ completedAt DateTime?
91
+
92
+ @@index([status])
93
+ @@index([createdAt])
94
+ }
src/routes/accounts.js CHANGED
@@ -1,5 +1,4 @@
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();
@@ -46,62 +45,6 @@ async function accountRoutes(fastify, options) {
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',
@@ -171,60 +114,6 @@ async function accountRoutes(fastify, options) {
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',
 
1
  const { PrismaClient } = require('@prisma/client');
 
2
  const { refreshAccountToken, refreshAllAccountTokens } = require('../../lib/token-scheduler');
3
 
4
  const prisma = new PrismaClient();
 
45
  return { accounts };
46
  });
47
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
  fastify.get('/:id', {
49
  schema: {
50
  summary: 'Get account details',
 
114
  };
115
  });
116
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
117
  fastify.post('/:id/refresh-token', {
118
  schema: {
119
  summary: 'Refresh account token',
src/routes/domains.js CHANGED
@@ -1,10 +1,67 @@
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',
@@ -71,8 +128,8 @@ async function domainRoutes(fastify, options) {
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: {
@@ -83,19 +140,20 @@ async function domainRoutes(fastify, options) {
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
  }
@@ -110,90 +168,54 @@ async function domainRoutes(fastify, options) {
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: {
184
- domainCount: { increment: 1 },
185
- lastDomainRegistration: new Date()
186
- }
187
- });
188
-
189
- reply.code(201);
190
  return {
191
- id: domain.id,
192
- subdomain: domain.subdomain,
193
- suffix: domain.suffix,
194
  fullDomain: `${subdomain}.${suffix}`,
195
- zoneIdDomainId,
196
- accountId: account.id
 
197
  };
198
  });
199
 
 
1
  const { PrismaClient } = require('@prisma/client');
2
  const { ZoneIdAPI } = require('../../lib/zoneid-api');
3
+ const { JOB_STATUS } = require('../../lib/job-worker');
4
 
5
  const prisma = new PrismaClient();
6
 
7
  async function domainRoutes(fastify, options) {
8
+ fastify.get('/jobs/:jobId', {
9
+ schema: {
10
+ summary: 'Get job status',
11
+ description: 'Check the status of a domain creation job',
12
+ tags: ['Domains'],
13
+ security: [{ apiKey: [] }],
14
+ params: {
15
+ type: 'object',
16
+ properties: {
17
+ jobId: { type: 'string' }
18
+ }
19
+ },
20
+ response: {
21
+ 200: {
22
+ type: 'object',
23
+ properties: {
24
+ id: { type: 'string' },
25
+ subdomain: { type: 'string' },
26
+ suffix: { type: 'string' },
27
+ fullDomain: { type: 'string' },
28
+ status: { type: 'string' },
29
+ statusMessage: { type: 'string' },
30
+ error: { type: 'string' },
31
+ domainId: { type: 'string' },
32
+ accountId: { type: 'string' },
33
+ createdAt: { type: 'string' },
34
+ completedAt: { type: 'string' }
35
+ }
36
+ }
37
+ }
38
+ }
39
+ }, async (request, reply) => {
40
+ const { jobId } = request.params;
41
+
42
+ const job = await prisma.domainJob.findUnique({
43
+ where: { id: jobId }
44
+ });
45
+
46
+ if (!job) {
47
+ return reply.code(404).send({ error: 'Job not found' });
48
+ }
49
+
50
+ return {
51
+ id: job.id,
52
+ subdomain: job.subdomain,
53
+ suffix: job.suffix,
54
+ fullDomain: `${job.subdomain}.${job.suffix}`,
55
+ status: job.status,
56
+ statusMessage: job.statusMessage,
57
+ error: job.error,
58
+ domainId: job.domainId,
59
+ accountId: job.accountId,
60
+ createdAt: job.createdAt,
61
+ completedAt: job.completedAt
62
+ };
63
+ });
64
+
65
  fastify.get('/', {
66
  schema: {
67
  summary: 'List all domains',
 
128
 
129
  fastify.post('/', {
130
  schema: {
131
+ summary: 'Register a new domain (async)',
132
+ description: 'Create a new subdomain on zone.id or nett.to. Returns a job ID immediately - poll GET /jobs/:jobId for status.',
133
  tags: ['Domains'],
134
  security: [{ apiKey: [] }],
135
  body: {
 
140
  suffix: { type: 'string', enum: ['zone.id', 'nett.to'], description: 'Domain suffix' },
141
  usageType: { type: 'string', description: 'Usage type (e.g., Blog, Portfolio)' },
142
  usageDescription: { type: 'string', description: 'Brief description of usage' },
143
+ accountId: { type: 'string', description: 'Account ID to use (optional, auto-selects if not specified)' }
144
  }
145
  },
146
  response: {
147
+ 202: {
148
  type: 'object',
149
  properties: {
150
+ jobId: { type: 'string' },
151
  subdomain: { type: 'string' },
152
  suffix: { type: 'string' },
153
  fullDomain: { type: 'string' },
154
+ status: { type: 'string' },
155
+ message: { type: 'string' },
156
+ statusUrl: { type: 'string' }
157
  }
158
  }
159
  }
 
168
  return reply.code(409).send({ error: 'Domain already registered' });
169
  }
170
 
 
171
  if (accountId) {
172
+ const account = await prisma.account.findUnique({ where: { id: accountId } });
173
  if (!account) {
174
  return reply.code(404).send({ error: 'Account not found' });
175
  }
176
  if (account.domainCount >= 10) {
177
  return reply.code(400).send({ error: 'Account has reached 10 domain limit' });
178
  }
179
+ if (!account.refreshToken) {
180
+ return reply.code(400).send({ error: 'Account has no refresh token' });
 
 
 
 
 
 
 
181
  }
182
  }
183
 
184
+ const existingJob = await prisma.domainJob.findFirst({
185
+ where: {
186
+ subdomain,
187
+ suffix,
188
+ status: { in: ['pending', 'finding_account', 'creating_account', 'registering_domain'] }
189
+ }
190
+ });
191
+ if (existingJob) {
192
+ return reply.code(409).send({
193
+ error: 'Domain registration already in progress',
194
+ jobId: existingJob.id
195
+ });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
196
  }
197
 
198
+ const job = await prisma.domainJob.create({
199
  data: {
200
+ subdomain,
 
201
  suffix,
 
202
  usageType,
203
  usageDescription,
204
+ accountId,
205
+ status: JOB_STATUS.PENDING,
206
+ statusMessage: 'Job queued for processing'
207
  }
208
  });
209
 
210
+ reply.code(202);
 
 
 
 
 
 
 
 
211
  return {
212
+ jobId: job.id,
213
+ subdomain,
214
+ suffix,
215
  fullDomain: `${subdomain}.${suffix}`,
216
+ status: job.status,
217
+ message: 'Domain registration job created. Poll the status URL to track progress.',
218
+ statusUrl: `/api/domains/jobs/${job.id}`
219
  };
220
  });
221
 
src/server.js CHANGED
@@ -8,6 +8,20 @@ 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
@@ -108,6 +122,8 @@ async function build() {
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();
@@ -117,6 +133,9 @@ async function start() {
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);
 
8
  const domainRoutes = require('./routes/domains');
9
  const dnsRoutes = require('./routes/dns');
10
  const { startTokenScheduler } = require('../lib/token-scheduler');
11
+ const { startWorker } = require('../lib/job-worker');
12
+ const { execSync } = require('child_process');
13
+
14
+ async function setupDatabase() {
15
+ console.log('Setting up database...');
16
+ try {
17
+ execSync('npx prisma generate', { stdio: 'inherit' });
18
+ execSync('npx prisma db push --accept-data-loss', { stdio: 'inherit' });
19
+ console.log('Database setup complete');
20
+ } catch (err) {
21
+ console.error('Database setup failed:', err.message);
22
+ throw err;
23
+ }
24
+ }
25
 
26
  const fastify = Fastify({
27
  logger: true
 
122
 
123
  async function start() {
124
  try {
125
+ await setupDatabase();
126
+
127
  const port = parseInt(process.env.PORT) || 5000;
128
  const host = process.env.HOST || '0.0.0.0';
129
  const server = await build();
 
133
 
134
  startTokenScheduler();
135
  console.log('Token auto-refresh scheduler started (checks every 6 hours)');
136
+
137
+ startWorker();
138
+ console.log('Domain job worker started (polling every 5 seconds)');
139
  } catch (err) {
140
  console.error(err);
141
  process.exit(1);