Spaces:
Runtime error
Runtime error
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 +151 -73
- lib/autz-account.js +64 -13
- lib/job-worker.js +164 -0
- lib/zoneid-api.js +13 -8
- prisma/schema.prisma +19 -0
- src/routes/accounts.js +0 -111
- src/routes/domains.js +93 -71
- src/server.js +19 -0
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 {
|
| 7 |
|
| 8 |
const prisma = new PrismaClient();
|
| 9 |
|
|
@@ -52,11 +52,18 @@ async function extractOtpFromEmail(emailHtml) {
|
|
| 52 |
}
|
| 53 |
|
| 54 |
async function activateAccount(userTokenId, otp) {
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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,
|
|
|
|
| 232 |
|
| 233 |
-
await
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
if (
|
| 238 |
-
|
| 239 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 240 |
}
|
| 241 |
-
}
|
| 242 |
-
|
| 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 |
-
|
| 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 |
-
|
| 333 |
-
|
| 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('
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 369 |
where: {
|
| 370 |
status: 'active',
|
| 371 |
domainCount: { lt: 10 },
|
| 372 |
refreshToken: { not: null },
|
| 373 |
-
|
| 374 |
-
|
| 375 |
-
|
| 376 |
-
|
| 377 |
},
|
| 378 |
-
orderBy: {
|
| 379 |
});
|
| 380 |
|
| 381 |
-
if (
|
| 382 |
-
console.log(`
|
| 383 |
-
return
|
| 384 |
}
|
| 385 |
|
| 386 |
-
|
| 387 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 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:
|
| 132 |
email,
|
| 133 |
userTokenId,
|
| 134 |
-
|
| 135 |
-
|
| 136 |
};
|
| 137 |
|
| 138 |
-
}
|
| 139 |
await browser.close();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 140 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 141 |
}
|
| 142 |
|
| 143 |
-
module.exports = {
|
|
|
|
| 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 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 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.
|
| 100 |
-
const created = list.
|
| 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 {
|
| 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,
|
| 87 |
}
|
| 88 |
},
|
| 89 |
response: {
|
| 90 |
-
|
| 91 |
type: 'object',
|
| 92 |
properties: {
|
| 93 |
-
|
| 94 |
subdomain: { type: 'string' },
|
| 95 |
suffix: { type: 'string' },
|
| 96 |
fullDomain: { type: 'string' },
|
| 97 |
-
|
| 98 |
-
|
|
|
|
| 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 |
-
|
| 123 |
-
|
| 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 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
}
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 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
|
| 170 |
data: {
|
| 171 |
-
|
| 172 |
-
subdomain: `${subdomain}.${suffix}`,
|
| 173 |
suffix,
|
| 174 |
-
mode: 'dns_record',
|
| 175 |
usageType,
|
| 176 |
usageDescription,
|
| 177 |
-
accountId
|
|
|
|
|
|
|
| 178 |
}
|
| 179 |
});
|
| 180 |
|
| 181 |
-
|
| 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 |
-
|
| 192 |
-
subdomain
|
| 193 |
-
suffix
|
| 194 |
fullDomain: `${subdomain}.${suffix}`,
|
| 195 |
-
|
| 196 |
-
|
|
|
|
| 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);
|