diff --git a/automation/audit.json b/automation/audit.json index 940380d..bd39c78 100644 --- a/automation/audit.json +++ b/automation/audit.json @@ -148,6 +148,10 @@ "audit": true, "skip": null }, + "application_contact_map": { + "audit": true, + "skip": null + }, "domain_authInfo": { "audit": true, "skip": null @@ -156,6 +160,10 @@ "audit": true, "skip": null }, + "application_status": { + "audit": true, + "skip": null + }, "secdns": { "audit": true, "skip": null @@ -168,6 +176,10 @@ "audit": true, "skip": null }, + "application_host_map": { + "audit": true, + "skip": null + }, "host_addr": { "audit": true, "skip": null diff --git a/cp/app/Controllers/ApplicationsController.php b/cp/app/Controllers/ApplicationsController.php new file mode 100644 index 0000000..d38d440 --- /dev/null +++ b/cp/app/Controllers/ApplicationsController.php @@ -0,0 +1,1559 @@ +getMethod() === 'POST') { + // Retrieve POST data + $data = $request->getParsedBody(); + $db = $this->container->get('db'); + $domainName = $data['domainName'] ?? null; + $registrar_id = $data['registrar'] ?? null; + $registrars = $db->select("SELECT id, clid, name FROM registrar"); + if ($_SESSION["auth_roles"] != 0) { + $registrar = true; + } else { + $registrar = null; + } + + $result = $db->selectRow('SELECT registrar_id FROM registrar_users WHERE user_id = ?', [$_SESSION['auth_user_id']]); + + if ($_SESSION["auth_roles"] != 0) { + $clid = $result['registrar_id']; + } else { + $clid = $registrar_id; + } + + $contactRegistrant = $data['contactRegistrant'] ?? null; + $contactAdmin = $data['contactAdmin'] ?? null; + $contactTech = $data['contactTech'] ?? null; + $contactBilling = $data['contactBilling'] ?? null; + + $phaseType = $data['phaseType'] ?? null; + $phaseName = $data['phaseName'] ?? null; + $smd = $data['smd'] ?? null; + + $nameservers = !empty($data['nameserver']) ? $data['nameserver'] : null; + $nameserver_ipv4 = !empty($data['nameserver_ipv4']) ? $data['nameserver_ipv4'] : null; + $nameserver_ipv6 = !empty($data['nameserver_ipv6']) ? $data['nameserver_ipv6'] : null; + + $authInfo = $data['authInfo'] ?? null; + + $parts = extractDomainAndTLD($domainName); + $label = $parts['domain']; + $domain_extension = $parts['tld']; + $invalid_domain = validate_label($domainName, $db); + + if ($invalid_domain) { + return view($response, 'admin/domains/createApplication.twig', [ + 'domainName' => $domainName, + 'error' => 'Invalid domain name in application', + 'registrars' => $registrars, + 'registrar' => $registrar, + ]); + } + + $valid_tld = false; + $result = $db->select('SELECT id, tld FROM domain_tld'); + + foreach ($result as $row) { + if ('.' . strtoupper($domain_extension) === strtoupper($row['tld'])) { + $valid_tld = true; + $tld_id = $row['id']; + break; + } + } + + if (!$valid_tld) { + return view($response, 'admin/domains/createApplication.twig', [ + 'domainName' => $domainName, + 'error' => 'Invalid domain extension in application', + 'registrars' => $registrars, + 'registrar' => $registrar, + ]); + } + + $domain_already_exist = $db->selectValue( + 'SELECT id FROM application WHERE name = ? and clid = ? LIMIT 1', + [$domainName, $clid] + ); + + if ($domain_already_exist) { + return view($response, 'admin/domains/createApplication.twig', [ + 'domainName' => $domainName, + 'error' => 'Application already exists', + 'registrars' => $registrars, + 'registrar' => $registrar, + ]); + } + + $domain_already_reserved = $db->selectValue( + 'SELECT id FROM reserved_domain_names WHERE name = ? LIMIT 1', + [$label] + ); + + if ($domain_already_reserved) { + return view($response, 'admin/domains/createApplication.twig', [ + 'domainName' => $domainName, + 'error' => 'Domain name in application is reserved or restricted', + 'registrars' => $registrars, + 'registrar' => $registrar, + ]); + } + + $date_add = 12; + + $result = $db->selectRow('SELECT accountBalance, creditLimit FROM registrar WHERE id = ?', [$clid]); + + $registrar_balance = $result['accountBalance']; + $creditLimit = $result['creditLimit']; + + $returnValue = getDomainPrice($db, $domainName, $tld_id, $date_add, 'create'); + $price = $returnValue['price']; + + if (!$price) { + return view($response, 'admin/domains/createApplication.twig', [ + 'domainName' => $domainName, + 'error' => 'The price, period and currency for such TLD are not declared', + 'registrars' => $registrars, + 'registrar' => $registrar, + ]); + } + + if (($registrar_balance + $creditLimit) < $price) { + return view($response, 'admin/domains/createApplication.twig', [ + 'domainName' => $domainName, + 'error' => 'Low credit: minimum threshold reached', + 'registrars' => $registrars, + 'registrar' => $registrar, + ]); + } + + $nameservers = array_filter($data['nameserver'] ?? [], function($value) { + return !empty($value) && $value !== null; + }); + $nameserver_ipv4 = array_filter($data['nameserver_ipv4'] ?? [], function($value) { + return !empty($value) && $value !== null; + }); + $nameserver_ipv6 = array_filter($data['nameserver_ipv6'] ?? [], function($value) { + return !empty($value) && $value !== null; + }); + + if (!empty($nameservers)) { + if (count($nameservers) !== count(array_unique($nameservers))) { + return view($response, 'admin/domains/createApplication.twig', [ + 'domainName' => $domainName, + 'error' => 'Duplicate nameservers detected. Please provide unique nameservers.', + 'registrars' => $registrars, + 'registrar' => $registrar, + ]); + } + + foreach ($nameservers as $index => $nameserver) { + if (preg_match("/^-|^\.-|-\.$|^\.$/", $nameserver)) { + return view($response, 'admin/domains/createApplication.twig', [ + 'domainName' => $domainName, + 'error' => 'Invalid hostName', + 'registrars' => $registrars, + 'registrar' => $registrar, + ]); + } + + if (!preg_match('/^([A-Z0-9]([A-Z0-9-]{0,61}[A-Z0-9]){0,1}\.){1,125}[A-Z0-9]([A-Z0-9-]{0,61}[A-Z0-9])$/i', $nameserver) && strlen($nameserver) < 254) { + return view($response, 'admin/domains/createApplication.twig', [ + 'domainName' => $domainName, + 'error' => 'Invalid hostName', + 'registrars' => $registrars, + 'registrar' => $registrar, + ]); + } + } + } + + if ($contactRegistrant) { + $validRegistrant = validate_identifier($contactRegistrant); + $row = $db->selectRow('SELECT id, clid FROM contact WHERE identifier = ?', [$contactRegistrant]); + + if (!$row) { + return view($response, 'admin/domains/createApplication.twig', [ + 'domainName' => $domainName, + 'error' => 'Registrant does not exist', + 'registrars' => $registrars, + 'registrar' => $registrar, + ]); + } + + if ($clid != $row['clid']) { + return view($response, 'admin/domains/createApplication.twig', [ + 'domainName' => $domainName, + 'error' => 'The contact requested in the command does NOT belong to the current registrar', + 'registrars' => $registrars, + 'registrar' => $registrar, + ]); + } + } + + if ($contactAdmin) { + $validAdmin = validate_identifier($contactAdmin); + $row = $db->selectRow('SELECT id, clid FROM contact WHERE identifier = ?', [$contactAdmin]); + + if (!$row) { + return view($response, 'admin/domains/createApplication.twig', [ + 'domainName' => $domainName, + 'error' => 'Admin contact does not exist', + 'registrars' => $registrars, + 'registrar' => $registrar, + ]); + } + + if ($clid != $row['clid']) { + return view($response, 'admin/domains/createApplication.twig', [ + 'domainName' => $domainName, + 'error' => 'The contact requested in the command does NOT belong to the current registrar', + 'registrars' => $registrars, + 'registrar' => $registrar, + ]); + } + } + + if ($contactTech) { + $validTech = validate_identifier($contactTech); + $row = $db->selectRow('SELECT id, clid FROM contact WHERE identifier = ?', [$contactTech]); + + if (!$row) { + return view($response, 'admin/domains/createApplication.twig', [ + 'domainName' => $domainName, + 'error' => 'Tech contact does not exist', + 'registrars' => $registrars, + 'registrar' => $registrar, + ]); + } + + if ($clid != $row['clid']) { + return view($response, 'admin/domains/createApplication.twig', [ + 'domainName' => $domainName, + 'error' => 'The contact requested in the command does NOT belong to the current registrar', + 'registrars' => $registrars, + 'registrar' => $registrar, + ]); + } + } + + if ($contactBilling) { + $validBilling = validate_identifier($contactBilling); + $row = $db->selectRow('SELECT id, clid FROM contact WHERE identifier = ?', [$contactBilling]); + + if (!$row) { + return view($response, 'admin/domains/createApplication.twig', [ + 'domainName' => $domainName, + 'error' => 'Billing contact does not exist', + 'registrars' => $registrars, + 'registrar' => $registrar, + ]); + } + + if ($clid != $row['clid']) { + return view($response, 'admin/domains/createApplication.twig', [ + 'domainName' => $domainName, + 'error' => 'The contact requested in the command does NOT belong to the current registrar', + 'registrars' => $registrars, + 'registrar' => $registrar, + ]); + } + } + + if (!$authInfo) { + return view($response, 'admin/domains/createApplication.twig', [ + 'domainName' => $domainName, + 'error' => 'Missing application authinfo', + 'registrars' => $registrars, + 'registrar' => $registrar, + ]); + } + + if (strlen($authInfo) < 6 || strlen($authInfo) > 16) { + return view($response, 'admin/domains/createApplication.twig', [ + 'domainName' => $domainName, + 'error' => 'Password needs to be at least 6 and up to 16 characters long', + 'registrars' => $registrars, + 'registrar' => $registrar, + ]); + } + + if (!preg_match('/[A-Z]/', $authInfo)) { + return view($response, 'admin/domains/createApplication.twig', [ + 'domainName' => $domainName, + 'error' => 'Password should have both upper and lower case characters', + 'registrars' => $registrars, + 'registrar' => $registrar, + ]); + } + + $registrant_id = $db->selectValue( + 'SELECT id FROM contact WHERE identifier = ? LIMIT 1', + [$contactRegistrant] + ); + + try { + $db->beginTransaction(); + + $currentDateTime = new \DateTime(); + $crdate = $currentDateTime->format('Y-m-d H:i:s.v'); // Current timestamp + + $db->insert('application', [ + 'name' => $domainName, + 'tldid' => $tld_id, + 'registrant' => $registrant_id, + 'crdate' => $crdate, + 'exdate' => null, + 'lastupdate' => null, + 'clid' => $clid, + 'crid' => $clid, + 'upid' => null, + 'trdate' => null, + 'trstatus' => null, + 'reid' => null, + 'redate' => null, + 'acid' => null, + 'acdate' => null, + 'rgpstatus' => null, + 'addPeriod' => null, + 'authtype' => 'pw', + 'authinfo' => $authInfo, + 'phase_name' => $phaseName, + 'phase_type' => $phaseType, + 'smd' => $smd + ]); + $domain_id = $db->getlastInsertId(); + + $db->insert('application_status', [ + 'domain_id' => $domain_id, + 'status' => 'pendingValidation' + ]); + + $db->exec( + 'UPDATE registrar SET accountBalance = accountBalance - ? WHERE id = ?', + [$price, $clid] + ); + + $db->exec( + 'INSERT INTO payment_history (registrar_id, date, description, amount) VALUES (?, CURRENT_TIMESTAMP(3), ?, ?)', + [$clid, "create application for $domainName for period $date_add MONTH", "-$price"] + ); + + $row = $db->selectRow( + 'SELECT crdate FROM application WHERE name = ? LIMIT 1', + [$domainName] + ); + $from = $row['crdate']; + + $currentDateTime = new \DateTime(); + $stdate = $currentDateTime->format('Y-m-d H:i:s.v'); + $db->insert( + 'statement', + [ + 'registrar_id' => $clid, + 'date' => $stdate, + 'command' => 'create', + 'domain_name' => $domainName, + 'length_in_months' => $date_add, + 'fromS' => $from, + 'toS' => $from, + 'amount' => $price + ] + ); + + if (!empty($nameservers)) { + foreach ($nameservers as $index => $nameserver) { + + $internal_host = false; + + $result = $db->select('SELECT tld FROM domain_tld'); + + foreach ($result as $row) { + if ('.' . strtoupper($domain_extension) === strtoupper($row['tld'])) { + $internal_host = true; + break; + } + } + + $hostName_already_exist = $db->selectValue( + 'SELECT id FROM host WHERE name = ? LIMIT 1', + [$nameserver] + ); + + if ($hostName_already_exist) { + $domain_host_map_id = $db->selectValue( + 'SELECT domain_id FROM application_host_map WHERE domain_id = ? AND host_id = ? LIMIT 1', + [$domain_id, $hostName_already_exist] + ); + + if (!$domain_host_map_id) { + $db->insert( + 'application_host_map', + [ + 'domain_id' => $domain_id, + 'host_id' => $hostName_already_exist + ] + ); + } else { + $currentDateTime = new \DateTime(); + $logdate = $currentDateTime->format('Y-m-d H:i:s.v'); + $db->insert( + 'error_log', + [ + 'registrar_id' => $clid, + 'log' => "Domain : $domainName ; hostName : $nameserver - is duplicated", + 'date' => $logdate + ] + ); + } + } else { + $currentDateTime = new \DateTime(); + $host_date = $currentDateTime->format('Y-m-d H:i:s.v'); + + if ($internal_host) { + $db->insert( + 'host', + [ + 'name' => $nameserver, + 'domain_id' => $domain_id, + 'clid' => $clid, + 'crid' => $clid, + 'crdate' => $host_date + ] + ); + $host_id = $db->getlastInsertId(); + } else { + $db->insert( + 'host', + [ + 'name' => $nameserver, + 'clid' => $clid, + 'crid' => $clid, + 'crdate' => $host_date + ] + ); + $host_id = $db->getlastInsertId(); + } + + $db->insert( + 'application_host_map', + [ + 'domain_id' => $domain_id, + 'host_id' => $host_id + ] + ); + + $db->insert( + 'host_status', + [ + 'status' => 'ok', + 'host_id' => $host_id + ] + ); + + if ($internal_host) { + if (empty($nameserver_ipv4[$index]) && empty($nameserver_ipv6[$index])) { + return view($response, 'admin/domains/createApplication.twig', [ + 'domainName' => $domainName, + 'error' => 'Error: No IPv4 or IPv6 addresses provided for internal host', + 'registrars' => $registrars, + 'registrar' => $registrar, + ]); + } + + if (isset($nameserver_ipv4[$index]) && !empty($nameserver_ipv4[$index])) { + $ipv4 = normalize_v4_address($nameserver_ipv4[$index]); + + $db->insert( + 'host_addr', + [ + 'host_id' => $host_id, + 'addr' => $ipv4, + 'ip' => 'v4' + ] + ); + } + + if (isset($nameserver_ipv6[$index]) && !empty($nameserver_ipv6[$index])) { + $ipv6 = normalize_v6_address($nameserver_ipv6[$index]); + + $db->insert( + 'host_addr', + [ + 'host_id' => $host_id, + 'addr' => $ipv6, + 'ip' => 'v6' + ] + ); + } + } + + } + } + } + + $contacts = [ + 'admin' => $data['contactAdmin'] ?? null, + 'tech' => $data['contactTech'] ?? null, + 'billing' => $data['contactBilling'] ?? null + ]; + + foreach ($contacts as $type => $contact) { + if ($contact !== null) { + $contact_id = $db->selectValue( + 'SELECT id FROM contact WHERE identifier = ? LIMIT 1', + [$contact] + ); + + // Check if $contact_id is not null before insertion + if ($contact_id !== null) { + $db->insert( + 'application_contact_map', + [ + 'domain_id' => $domain_id, + 'contact_id' => $contact_id, + 'type' => $type + ] + ); + } + } + } + + $result = $db->selectRow( + 'SELECT crdate,exdate FROM domain WHERE name = ? LIMIT 1', + [$domainName] + ); + $crdate = $result['crdate']; + $exdate = $result['exdate']; + + $curdate_id = $db->selectValue( + 'SELECT id FROM statistics WHERE date = CURDATE()' + ); + + if (!$curdate_id) { + $db->exec( + 'INSERT IGNORE INTO statistics (date) VALUES(CURDATE())' + ); + } + + $db->exec( + 'UPDATE statistics SET created_domains = created_domains + 1 WHERE date = CURDATE()' + ); + + $db->commit(); + } catch (Exception $e) { + $db->rollBack(); + return view($response, 'admin/domains/createApplication.twig', [ + 'domainName' => $domainName, + 'error' => 'Database failure: ' . $e->getMessage(), + 'registrars' => $registrars, + 'registrar' => $registrar, + ]); + } catch (\Pinga\Db\Throwable\IntegrityConstraintViolationException $e) { + $db->rollBack(); + return view($response, 'admin/domains/createApplication.twig', [ + 'domainName' => $domainName, + 'error' => 'Database failure: ' . $e->getMessage(), + 'registrars' => $registrars, + 'registrar' => $registrar, + ]); + } + + $crdate = $db->selectValue( + "SELECT crdate FROM application WHERE id = ? LIMIT 1", + [$domain_id] + ); + + $this->container->get('flash')->addMessage('success', 'Application ' . $domainName . ' has been created successfully on ' . $crdate); + return $response->withHeader('Location', '/applications')->withStatus(302); + } + + $db = $this->container->get('db'); + $registrars = $db->select("SELECT id, clid, name FROM registrar"); + if ($_SESSION["auth_roles"] != 0) { + $registrar = true; + } else { + $registrar = null; + } + + // Default view for GET requests or if POST data is not set + return view($response,'admin/domains/createApplication.twig', [ + 'registrars' => $registrars, + 'registrar' => $registrar, + ]); + } + + public function viewApplication(Request $request, Response $response, $args) + { + $db = $this->container->get('db'); + // Get the current URI + $uri = $request->getUri()->getPath(); + + if ($args) { + $args = strtolower(trim($args)); + + if (!preg_match('/^([a-z0-9]([-a-z0-9]*[a-z0-9])?\.)*[a-z0-9]([-a-z0-9]*[a-z0-9])?$/', $args)) { + $this->container->get('flash')->addMessage('error', 'Invalid domain name format'); + return $response->withHeader('Location', '/applications')->withStatus(302); + } + + $domain = $db->selectRow('SELECT id, name, registrant, crdate, clid, idnlang, authinfo, authtype, phase_name, phase_type, smd FROM application WHERE name = ?', + [ $args ]); + + if ($domain) { + $registrars = $db->selectRow('SELECT id, clid, name FROM registrar WHERE id = ?', [$domain['clid']]); + + // Check if the user is not an admin (assuming role 0 is admin) + if ($_SESSION["auth_roles"] != 0) { + $userRegistrars = $db->select('SELECT registrar_id FROM registrar_users WHERE user_id = ?', [$_SESSION['auth_user_id']]); + + // Assuming $userRegistrars returns an array of arrays, each containing 'registrar_id' + $userRegistrarIds = array_column($userRegistrars, 'registrar_id'); + + // Check if the registrar's ID is in the user's list of registrar IDs + if (!in_array($registrars['id'], $userRegistrarIds)) { + // Redirect to the applications view if the user is not authorized for this contact + return $response->withHeader('Location', '/applications')->withStatus(302); + } + } + + $domainRegistrant = $db->selectRow('SELECT identifier FROM contact WHERE id = ?', + [ $domain['registrant'] ]); + $domainStatus = $db->select('SELECT status FROM application_status WHERE domain_id = ?', + [ $domain['id'] ]); + $domainHostsQuery = ' + SELECT dhm.id, dhm.domain_id, dhm.host_id, h.name + FROM application_host_map dhm + JOIN host h ON dhm.host_id = h.id + WHERE dhm.domain_id = ?'; + + $domainHosts = $db->select($domainHostsQuery, [$domain['id']]); + $domainContactsQuery = ' + SELECT dcm.id, dcm.domain_id, dcm.contact_id, dcm.type, c.identifier + FROM application_contact_map dcm + JOIN contact c ON dcm.contact_id = c.id + WHERE dcm.domain_id = ?'; + $domainContacts = $db->select($domainContactsQuery, [$domain['id']]); + + return view($response,'admin/domains/viewApplication.twig', [ + 'domain' => $domain, + 'domainStatus' => $domainStatus, + 'domainRegistrant' => $domainRegistrant, + 'domainHosts' => $domainHosts, + 'domainContacts' => $domainContacts, + 'registrars' => $registrars, + 'currentUri' => $uri + ]); + } else { + // Domain does not exist, redirect to the applications view + return $response->withHeader('Location', '/applications')->withStatus(302); + } + + } else { + // Redirect to the applications view + return $response->withHeader('Location', '/applications')->withStatus(302); + } + + } + + public function updateApplication(Request $request, Response $response, $args) + { + $db = $this->container->get('db'); + $registrars = $db->select("SELECT id, clid, name FROM registrar"); + if ($_SESSION["auth_roles"] != 0) { + $registrar = true; + } else { + $registrar = null; + } + + $uri = $request->getUri()->getPath(); + + if ($args) { + $args = strtolower(trim($args)); + + if (!preg_match('/^([a-z0-9]([-a-z0-9]*[a-z0-9])?\.)*[a-z0-9]([-a-z0-9]*[a-z0-9])?$/', $args)) { + $this->container->get('flash')->addMessage('error', 'Invalid domain name format'); + return $response->withHeader('Location', '/domains')->withStatus(302); + } + + $domain = $db->selectRow('SELECT id, name, registrant, crdate, exdate, lastupdate, clid, idnlang, rgpstatus FROM domain WHERE name = ?', + [ $args ]); + + if ($domain) { + $registrars = $db->selectRow('SELECT id, clid, name FROM registrar WHERE id = ?', [$domain['clid']]); + + // Check if the user is not an admin (assuming role 0 is admin) + if ($_SESSION["auth_roles"] != 0) { + $userRegistrars = $db->select('SELECT registrar_id FROM registrar_users WHERE user_id = ?', [$_SESSION['auth_user_id']]); + + // Assuming $userRegistrars returns an array of arrays, each containing 'registrar_id' + $userRegistrarIds = array_column($userRegistrars, 'registrar_id'); + + // Check if the registrar's ID is in the user's list of registrar IDs + if (!in_array($registrars['id'], $userRegistrarIds)) { + // Redirect to the domains view if the user is not authorized for this contact + return $response->withHeader('Location', '/domains')->withStatus(302); + } + } + + $domainRegistrant = $db->selectRow('SELECT identifier FROM contact WHERE id = ?', + [ $domain['registrant'] ]); + $domainStatus = $db->select('SELECT status FROM application_status WHERE domain_id = ?', + [ $domain['id'] ]); + $domainAuth = $db->selectRow('SELECT authinfo FROM domain_authInfo WHERE domain_id = ?', + [ $domain['id'] ]); + $domainSecdns = $db->select('SELECT * FROM secdns WHERE domain_id = ?', + [ $domain['id'] ]); + $domainHostsQuery = ' + SELECT dhm.id, dhm.domain_id, dhm.host_id, h.name + FROM application_host_map dhm + JOIN host h ON dhm.host_id = h.id + WHERE dhm.domain_id = ?'; + + $domainHosts = $db->select($domainHostsQuery, [$domain['id']]); + $domainContactsQuery = ' + SELECT dcm.id, dcm.domain_id, dcm.contact_id, dcm.type, c.identifier + FROM application_contact_map dcm + JOIN contact c ON dcm.contact_id = c.id + WHERE dcm.domain_id = ?'; + $domainContacts = $db->select($domainContactsQuery, [$domain['id']]); + + $csrfTokenName = $this->container->get('csrf')->getTokenName(); + $csrfTokenValue = $this->container->get('csrf')->getTokenValue(); + + + return view($response,'admin/domains/updateDomain.twig', [ + 'domain' => $domain, + 'domainStatus' => $domainStatus, + 'domainAuth' => $domainAuth, + 'domainRegistrant' => $domainRegistrant, + 'domainSecdns' => $domainSecdns, + 'domainHosts' => $domainHosts, + 'domainContacts' => $domainContacts, + 'registrar' => $registrars, + 'currentUri' => $uri, + 'csrfTokenName' => $csrfTokenName, + 'csrfTokenValue' => $csrfTokenValue + ]); + } else { + // Domain does not exist, redirect to the domains view + return $response->withHeader('Location', '/domains')->withStatus(302); + } + + } else { + // Redirect to the domains view + return $response->withHeader('Location', '/domains')->withStatus(302); + } + } + + public function updateApplicationProcess(Request $request, Response $response) + { + if ($request->getMethod() === 'POST') { + // Retrieve POST data + $data = $request->getParsedBody(); + $db = $this->container->get('db'); + $domainName = $data['domainName'] ?? null; + + $result = $db->selectRow('SELECT registrar_id FROM registrar_users WHERE user_id = ?', [$_SESSION['auth_user_id']]); + + if ($_SESSION["auth_roles"] != 0) { + $clid = $result['registrar_id']; + } else { + $clid = $db->selectValue('SELECT clid FROM domain WHERE name = ?', [$domainName]); + } + + $domain_id = $db->selectValue( + 'SELECT id FROM domain WHERE name = ?', + [$domainName] + ); + $results = $db->select( + 'SELECT status FROM application_status WHERE domain_id = ?', + [ $domain_id ] + ); + + foreach ($results as $row) { + $status = $row['status']; + if (preg_match('/.*(serverUpdateProhibited)$/', $status) || preg_match('/^pendingTransfer/', $status)) { + $this->container->get('flash')->addMessage('error', 'It has a status that does not allow renew, first change the status'); + return $response->withHeader('Location', '/domain/update/'.$domainName)->withStatus(302); + } + } + + $contactRegistrant = $data['contactRegistrant'] ?? null; + $contactAdmin = $data['contactAdmin'] ?? null; + $contactTech = $data['contactTech'] ?? null; + $contactBilling = $data['contactBilling'] ?? null; + + $nameservers = $data['nameserver'] ?? []; + + $dsKeyTag = isset($data['dsKeyTag']) ? (int)$data['dsKeyTag'] : null; + $dsAlg = $data['dsAlg'] ?? null; + $dsDigestType = isset($data['dsDigestType']) ? (int)$data['dsDigestType'] : null; + $dsDigest = $data['dsDigest'] ?? null; + + $dnskeyFlags = $data['dnskeyFlags'] ?? null; + $dnskeyProtocol = $data['dnskeyProtocol'] ?? null; + $dnskeyAlg = $data['dnskeyAlg'] ?? null; + $dnskeyPubKey = $data['dnskeyPubKey'] ?? null; + + $authInfo = $data['authInfo'] ?? null; + + if ($contactRegistrant) { + $validRegistrant = validate_identifier($contactRegistrant); + $row = $db->selectRow('SELECT id, clid FROM contact WHERE identifier = ?', [$contactRegistrant]); + + if (!$row) { + $this->container->get('flash')->addMessage('error', 'Registrant does not exist'); + return $response->withHeader('Location', '/domain/update/'.$domainName)->withStatus(302); + } + + if ($clid != $row['clid']) { + $this->container->get('flash')->addMessage('error', 'The contact requested in the command does NOT belong to the current registrar'); + return $response->withHeader('Location', '/domain/update/'.$domainName)->withStatus(302); + } + } else { + $this->container->get('flash')->addMessage('error', 'Please provide registrant identifier'); + return $response->withHeader('Location', '/domain/update/'.$domainName)->withStatus(302); + } + + if ($contactAdmin) { + $validAdmin = validate_identifier($contactAdmin); + $row = $db->selectRow('SELECT id, clid FROM contact WHERE identifier = ?', [$contactAdmin]); + + if (!$row) { + $this->container->get('flash')->addMessage('error', 'Admin contact does not exist'); + return $response->withHeader('Location', '/domain/update/'.$domainName)->withStatus(302); + } + + if ($clid != $row['clid']) { + $this->container->get('flash')->addMessage('error', 'The contact requested in the command does NOT belong to the current registrar'); + return $response->withHeader('Location', '/domain/update/'.$domainName)->withStatus(302); + } + } else { + $this->container->get('flash')->addMessage('error', 'Please provide admin contact identifier'); + return $response->withHeader('Location', '/domain/update/'.$domainName)->withStatus(302); + } + + if ($contactTech) { + $validTech = validate_identifier($contactTech); + $row = $db->selectRow('SELECT id, clid FROM contact WHERE identifier = ?', [$contactTech]); + + if (!$row) { + $this->container->get('flash')->addMessage('error', 'Tech contact does not exist'); + return $response->withHeader('Location', '/domain/update/'.$domainName)->withStatus(302); + } + + if ($clid != $row['clid']) { + $this->container->get('flash')->addMessage('error', 'The contact requested in the command does NOT belong to the current registrar'); + return $response->withHeader('Location', '/domain/update/'.$domainName)->withStatus(302); + } + } else { + $this->container->get('flash')->addMessage('error', 'Please provide tech contact identifier'); + return $response->withHeader('Location', '/domain/update/'.$domainName)->withStatus(302); + } + + if ($contactBilling) { + $validBilling = validate_identifier($contactBilling); + $row = $db->selectRow('SELECT id, clid FROM contact WHERE identifier = ?', [$contactBilling]); + + if (!$row) { + $this->container->get('flash')->addMessage('error', 'Billing contact does not exist'); + return $response->withHeader('Location', '/domain/update/'.$domainName)->withStatus(302); + } + + if ($clid != $row['clid']) { + $this->container->get('flash')->addMessage('error', 'The contact requested in the command does NOT belong to the current registrar'); + return $response->withHeader('Location', '/domain/update/'.$domainName)->withStatus(302); + } + } else { + $this->container->get('flash')->addMessage('error', 'Please provide billing contact identifier'); + return $response->withHeader('Location', '/domain/update/'.$domainName)->withStatus(302); + } + + if (!$authInfo) { + $this->container->get('flash')->addMessage('error', 'Domain ' . $domainName . ' can not be updated: Missing domain authinfo'); + return $response->withHeader('Location', '/domain/update/'.$domainName)->withStatus(302); + } + + if (strlen($authInfo) < 6 || strlen($authInfo) > 16) { + $this->container->get('flash')->addMessage('error', 'Password needs to be at least 6 and up to 16 characters long'); + return $response->withHeader('Location', '/domain/update/'.$domainName)->withStatus(302); + } + + if (!preg_match('/[A-Z]/', $authInfo)) { + $this->container->get('flash')->addMessage('error', 'Password should have both upper and lower case characters'); + return $response->withHeader('Location', '/domain/update/'.$domainName)->withStatus(302); + } + + $registrant_id = $db->selectValue( + 'SELECT id FROM contact WHERE identifier = ? LIMIT 1', + [$contactRegistrant] + ); + + try { + $db->beginTransaction(); + + $currentDateTime = new \DateTime(); + $update = $currentDateTime->format('Y-m-d H:i:s.v'); // Current timestamp + + $db->update('domain', [ + 'registrant' => $registrant_id, + 'lastupdate' => $update, + 'upid' => $clid + ], + [ + 'name' => $domainName + ] + ); + $domain_id = $db->selectValue( + 'SELECT id FROM domain WHERE name = ?', + [$domainName] + ); + + $db->update( + 'domain_authInfo', + [ + 'authinfo' => $authInfo + ], + [ + 'id' => $domain_id, + 'authtype' => 'pw' + ] + ); + + // Data sanity checks + // Validate keyTag + if (!empty($dsKeyTag)) { + if (!is_int($dsKeyTag)) { + $this->container->get('flash')->addMessage('error', 'Incomplete key tag provided'); + return $response->withHeader('Location', '/domain/update/'.$domainName)->withStatus(302); + } + + if ($dsKeyTag < 0 || $dsKeyTag > 65535) { + $this->container->get('flash')->addMessage('error', 'Incomplete key tag provided'); + return $response->withHeader('Location', '/domain/update/'.$domainName)->withStatus(302); + } + } + + // Validate alg + $validAlgorithms = [2, 3, 5, 6, 7, 8, 10, 13, 14, 15, 16]; + if (!empty($dsAlg) && !in_array($dsAlg, $validAlgorithms)) { + $this->container->get('flash')->addMessage('error', 'Incomplete algorithm provided'); + return $response->withHeader('Location', '/domain/update/'.$domainName)->withStatus(302); + } + + // Validate digestType and digest + if (!empty($dsDigestType) && !is_int($dsDigestType)) { + $this->container->get('flash')->addMessage('error', 'Incomplete digest type provided'); + return $response->withHeader('Location', '/domain/update/'.$domainName)->withStatus(302); + } + $validDigests = [ + 1 => 40, // SHA-1 + 2 => 64, // SHA-256 + 4 => 96 // SHA-384 + ]; + if (empty($validDigests[$dsDigestType])) { + $this->container->get('flash')->addMessage('error', 'Unsupported digest type'); + return $response->withHeader('Location', '/domain/update/'.$domainName)->withStatus(302); + } + if (!empty($dsDigest)) { + if (strlen($dsDigest) != $validDigests[$dsDigestType] || !ctype_xdigit($dsDigest)) { + $this->container->get('flash')->addMessage('error', 'Invalid digest length or format'); + return $response->withHeader('Location', '/domain/update/'.$domainName)->withStatus(302); + } + } + + // Data sanity checks for keyData + // Validate flags + $validFlags = [256, 257]; + if (!empty($dnskeyFlags) && !in_array($dnskeyFlags, $validFlags)) { + $this->container->get('flash')->addMessage('error', 'Invalid flags provided'); + return $response->withHeader('Location', '/domain/update/'.$domainName)->withStatus(302); + } + + // Validate protocol + if (!empty($dnskeyProtocol) && $dnskeyProtocol != 3) { + $this->container->get('flash')->addMessage('error', 'Invalid protocol provided'); + return $response->withHeader('Location', '/domain/update/'.$domainName)->withStatus(302); + } + + // Validate algKeyData + if (!empty($dnskeyAlg)) { + $this->container->get('flash')->addMessage('error', 'Invalid algorithm encoding'); + return $response->withHeader('Location', '/domain/update/'.$domainName)->withStatus(302); + } + + // Validate pubKey + if (!empty($dnskeyPubKey) && base64_encode(base64_decode($dnskeyPubKey, true)) !== $dnskeyPubKey) { + $this->container->get('flash')->addMessage('error', 'Invalid public key encoding'); + return $response->withHeader('Location', '/domain/update/'.$domainName)->withStatus(302); + } + + if (!empty($dsKeyTag)) { + // Base data for the insert + $insertData = [ + 'domain_id' => $domain_id, + 'maxsiglife' => $maxSigLife, + 'interface' => 'dsData', + 'keytag' => $dsKeyTag, + 'alg' => $dsAlg, + 'digesttype' => $dsDigestType, + 'digest' => $dsDigest, + 'flags' => null, + 'protocol' => null, + 'keydata_alg' => null, + 'pubkey' => null + ]; + + // Check additional conditions for dnskeyFlags + if (isset($dnskeyFlags) && $dnskeyFlags !== "") { + $insertData['flags'] = $dnskeyFlags; + $insertData['protocol'] = $dnskeyProtocol; + $insertData['keydata_alg'] = $dnskeyAlg; + $insertData['pubkey'] = $dnskeyPubKey; + } + + // Perform the insert + $db->insert('secdns', $insertData); + } + + foreach ($nameservers as $index => $nameserver) { + if (preg_match("/^-|^\.-|-\.$|^\.$/", $nameserver)) { + $this->container->get('flash')->addMessage('error', 'Invalid hostName'); + return $response->withHeader('Location', '/domain/update/'.$domainName)->withStatus(302); + } + + if (!preg_match('/^([A-Z0-9]([A-Z0-9-]{0,61}[A-Z0-9]){0,1}\.){1,125}[A-Z0-9]([A-Z0-9-]{0,61}[A-Z0-9])$/i', $nameserver) && strlen($nameserver) < 254) { + $this->container->get('flash')->addMessage('error', 'Invalid hostName'); + return $response->withHeader('Location', '/domain/update/'.$domainName)->withStatus(302); + } + + $hostName_already_exist = $db->selectValue( + 'SELECT id FROM host WHERE name = ? LIMIT 1', + [$nameserver] + ); + + if ($hostName_already_exist) { + $domain_host_map_id = $db->selectValue( + 'SELECT domain_id FROM application_host_map WHERE domain_id = ? AND host_id = ? LIMIT 1', + [$domain_id, $hostName_already_exist] + ); + + if (!$domain_host_map_id) { + $db->insert( + 'application_host_map', + [ + 'domain_id' => $domain_id, + 'host_id' => $hostName_already_exist + ] + ); + } else { + $host_map_id = $db->selectValue( + 'SELECT id FROM application_host_map WHERE domain_id = ? AND host_id = ? LIMIT 1', + [$domain_id, $hostName_already_exist] + ); + + $db->update( + 'application_host_map', + [ + 'host_id' => $hostName_already_exist + ], + [ + 'domain_id' => $domain_id, + 'id' => $host_map_id + ] + ); + } + } else { + $currentDateTime = new \DateTime(); + $host_date = $currentDateTime->format('Y-m-d H:i:s.v'); + $db->insert( + 'host', + [ + 'name' => $nameserver, + 'domain_id' => $domain_id, + 'clid' => $clid, + 'crid' => $clid, + 'crdate' => $host_date + ] + ); + $host_id = $db->getlastInsertId(); + + $db->insert( + 'application_host_map', + [ + 'domain_id' => $domain_id, + 'host_id' => $host_id + ] + ); + + $db->insert( + 'host_status', + [ + 'status' => 'ok', + 'host_id' => $host_id + ] + ); + + if (isset($nameserver_ipv4[$index]) && !empty($nameserver_ipv4[$index])) { + $ipv4 = normalize_v4_address($nameserver_ipv4[$index]); + + $db->insert( + 'host_addr', + [ + 'host_id' => $host_id, + 'addr' => $ipv4, + 'ip' => 'v4' + ] + ); + } + + if (isset($nameserver_ipv6[$index]) && !empty($nameserver_ipv6[$index])) { + $ipv6 = normalize_v6_address($nameserver_ipv6[$index]); + + $db->insert( + 'host_addr', + [ + 'host_id' => $host_id, + 'addr' => $ipv6, + 'ip' => 'v6' + ] + ); + } + + } + } + + $contacts = [ + 'admin' => $data['contactAdmin'] ?? null, + 'tech' => $data['contactTech'] ?? null, + 'billing' => $data['contactBilling'] ?? null + ]; + + foreach ($contacts as $type => $contact) { + if ($contact !== null) { + $contact_id = $db->selectValue( + 'SELECT id FROM contact WHERE identifier = ? LIMIT 1', + [$contact] + ); + + $contact_map_id = $db->selectRow( + 'SELECT * FROM application_contact_map WHERE domain_id = ? AND type = ?', + [$domain_id, $type] + ); + + // Check if $contact_id is not null before update + if ($contact_id !== null) { + $db->update( + 'application_contact_map', + [ + 'contact_id' => $contact_id, + ], + [ + 'id' => $contact_map_id['id'] + ] + ); + } + } + } + + $db->commit(); + } catch (Exception $e) { + $db->rollBack(); + $this->container->get('flash')->addMessage('error', 'Database failure during update: ' . $e->getMessage()); + return $response->withHeader('Location', '/domain/update/'.$domainName)->withStatus(302); + } catch (\Pinga\Db\Throwable\IntegrityConstraintViolationException $e) { + $db->rollBack(); + $this->container->get('flash')->addMessage('error', 'Database failure during update: ' . $e->getMessage()); + return $response->withHeader('Location', '/domain/update/'.$domainName)->withStatus(302); + } + + $this->container->get('flash')->addMessage('success', 'Domain ' . $domainName . ' has been updated successfully on ' . $update); + return $response->withHeader('Location', '/domain/update/'.$domainName)->withStatus(302); + } + } + + public function applicationDeleteHost(Request $request, Response $response) + { + $db = $this->container->get('db'); + $data = $request->getParsedBody(); + $uri = $request->getUri()->getPath(); + + if ($data['nameserver']) { + $host_id = $db->selectValue('SELECT id FROM host WHERE name = ?', + [ $data['nameserver'] ]); + $domain_id = $db->selectValue('SELECT domain_id FROM application_host_map WHERE host_id = ?', + [ $host_id ]); + $domainName = $db->selectValue('SELECT name FROM domain WHERE id = ?', + [ $domain_id ]); + $db->delete( + 'application_host_map', + [ + 'host_id' => $host_id, + 'domain_id' => $domain_id + ] + ); + + $this->container->get('flash')->addMessage('success', 'Host ' . $data['nameserver'] . ' has been removed from domain successfully'); + + $jsonData = json_encode([ + 'success' => true, + 'redirect' => '/domain/update/'.$domainName + ]); + + $response = new \Nyholm\Psr7\Response( + 200, // Status code + ['Content-Type' => 'application/json'], // Headers + $jsonData // Body + ); + + return $response; + } else { + $jsonData = json_encode([ + 'success' => false, + 'error' => 'An error occurred while processing your request.' + ]); + + return new \Nyholm\Psr7\Response( + 400, + ['Content-Type' => 'application/json'], + $jsonData + ); + } + } + + public function deleteApplication(Request $request, Response $response, $args) + { + // if ($request->getMethod() === 'POST') { + $db = $this->container->get('db'); + // Get the current URI + $uri = $request->getUri()->getPath(); + + if ($args) { + $args = strtolower(trim($args)); + + if (!preg_match('/^([a-z0-9]([-a-z0-9]*[a-z0-9])?\.)*[a-z0-9]([-a-z0-9]*[a-z0-9])?$/', $args)) { + $this->container->get('flash')->addMessage('error', 'Invalid domain name format'); + return $response->withHeader('Location', '/domains')->withStatus(302); + } + + $domain = $db->selectRow('SELECT id, name, tldid, registrant, crdate, exdate, clid, crid, upid, trdate, trstatus, reid, redate, acid, acdate, rgpstatus, addPeriod, autoRenewPeriod, renewPeriod, renewedDate, transferPeriod FROM domain WHERE name = ?', + [ $args ]); + + $domainName = $domain['name']; + $domain_id = $domain['id']; + $tldid = $domain['tldid']; + $registrant = $domain['registrant']; + $crdate = $domain['crdate']; + $exdate = $domain['exdate']; + $registrar_id_domain = $domain['clid']; + $crid = $domain['crid']; + $upid = $domain['upid']; + $trdate = $domain['trdate']; + $trstatus = $domain['trstatus']; + $reid = $domain['reid']; + $redate = $domain['redate']; + $acid = $domain['acid']; + $acdate = $domain['acdate']; + $rgpstatus = $domain['rgpstatus']; + $addPeriod = $domain['addPeriod']; + $autoRenewPeriod = $domain['autoRenewPeriod']; + $renewPeriod = $domain['renewPeriod']; + $renewedDate = $domain['renewedDate']; + $transferPeriod = $domain['transferPeriod']; + + $parts = extractDomainAndTLD($domainName); + $label = $parts['domain']; + $domain_extension = $parts['tld']; + + $result = $db->select('SELECT id, tld FROM domain_tld'); + foreach ($result as $row) { + if ('.' . strtoupper($domain_extension) === strtoupper($row['tld'])) { + $tld_id = $row['id']; + break; + } + } + + $result = $db->selectRow('SELECT registrar_id FROM registrar_users WHERE user_id = ?', [$_SESSION['auth_user_id']]); + + if ($_SESSION["auth_roles"] != 0) { + $clid = $result['registrar_id']; + } else { + $clid = $registrar_id_domain; + } + + $results = $db->select( + 'SELECT status FROM application_status WHERE domain_id = ?', + [ $domain_id ] + ); + + foreach ($results as $row) { + $status = $row['status']; + if (preg_match('/.*(UpdateProhibited|DeleteProhibited)$/', $status) || preg_match('/^pending/', $status)) { + $this->container->get('flash')->addMessage('error', 'It has a status that does not allow deletion, first change the status'); + return $response->withHeader('Location', '/domains')->withStatus(302); + } + } + + $grace_period = 30; + + $db->delete( + 'application_status', + [ + 'domain_id' => $domain_id + ] + ); + + $db->exec( + 'UPDATE domain SET rgpstatus = ?, delTime = DATE_ADD(CURRENT_TIMESTAMP(3), INTERVAL ? DAY) WHERE id = ?', + ['redemptionPeriod', $grace_period, $domain_id] + ); + + $db->insert( + 'application_status', + [ + 'domain_id' => $domain_id, + 'status' => 'pendingDelete' + ] + ); + + if ($rgpstatus) { + if ($rgpstatus === 'addPeriod') { + $addPeriod_id = $db->selectValue( + 'SELECT id FROM domain WHERE id = ? AND (CURRENT_TIMESTAMP(3) < DATE_ADD(crdate, INTERVAL 5 DAY)) LIMIT 1', + [ + $domain_id + ] + ); + if ($addPeriod_id) { + $returnValue = getDomainPrice($db, $domainName, $tld_id, $addPeriod, 'create'); + $price = $returnValue['price']; + + if (!$price) { + $this->container->get('flash')->addMessage('error', 'The price, period and currency for such TLD are not declared'); + return $response->withHeader('Location', '/domains')->withStatus(302); + } + + try { + $db->beginTransaction(); + + $db->exec( + 'UPDATE registrar SET accountBalance = accountBalance + ? WHERE id = ?', + [$price, $clid] + ); + + $description = "domain name is deleted by the registrar during grace addPeriod, the registry provides a credit for the cost of the registration domain $domainName for period $addPeriod MONTH"; + $db->exec( + 'INSERT INTO payment_history (registrar_id, date, description, amount) VALUES(?, CURRENT_TIMESTAMP(3), ?, ?)', + [$clid, $description, $price] + ); + + $hostIds = $db->select( + 'SELECT id FROM host WHERE domain_id = ?', + [$domain_id] + ); + + foreach ($hostIds as $host) { + $host_id = $host['id']; + + // Delete operations + $db->delete( + 'host_addr', + [ + 'host_id' => $host_id + ] + ); + $db->delete( + 'host_status', + [ + 'host_id' => $host_id + ] + ); + $db->delete( + 'application_host_map', + [ + 'host_id' => $host_id + ] + ); + } + + // Delete domain related records + $db->delete( + 'application_contact_map', + [ + 'domain_id' => $domain_id + ] + ); + $db->delete( + 'application_host_map', + [ + 'domain_id' => $domain_id + ] + ); + $db->delete( + 'domain_authInfo', + [ + 'domain_id' => $domain_id + ] + ); + $db->delete( + 'application_status', + [ + 'domain_id' => $domain_id + ] + ); + $db->delete( + 'host', + [ + 'domain_id' => $domain_id + ] + ); + $db->delete( + 'secdns', + [ + 'domain_id' => $domain_id + ] + ); + $db->delete( + 'domain', + [ + 'id' => $domain_id + ] + ); + + $curdate_id = $db->selectValue( + 'SELECT id FROM statistics WHERE date = CURDATE()' + ); + + if (!$curdate_id) { + $db->exec( + 'INSERT IGNORE INTO statistics (date) VALUES(CURDATE())' + ); + } + + $db->exec( + 'UPDATE statistics SET deleted_domains = deleted_domains + 1 WHERE date = CURDATE()' + ); + + $db->commit(); + } catch (Exception $e) { + $db->rollBack(); + $this->container->get('flash')->addMessage('error', 'Database failure: ' . $e->getMessage()); + return $response->withHeader('Location', '/domain/renew/'.$domainName)->withStatus(302); + } + $isImmediateDeletion = true; + } + } elseif ($rgpstatus === 'autoRenewPeriod') { + $autoRenewPeriod_id = $db->selectValue( + 'SELECT id FROM domain WHERE id = ? AND (CURRENT_TIMESTAMP(3) < DATE_ADD(renewedDate, INTERVAL 45 DAY)) LIMIT 1', + [ + $domain_id + ] + ); + if ($autoRenewPeriod_id) { + $returnValue = getDomainPrice($db, $domainName, $tld_id, $autoRenewPeriod, 'renew'); + $price = $returnValue['price']; + + if (!$price) { + $this->container->get('flash')->addMessage('error', 'The price, period and currency for such TLD are not declared'); + return $response->withHeader('Location', '/domains')->withStatus(302); + } + + $db->exec( + 'UPDATE registrar SET accountBalance = accountBalance + ? WHERE id = ?', + [$price, $clid] + ); + + $description = "domain name is deleted by the registrar during grace autoRenewPeriod, the registry provides a credit for the cost of the renewal domain $domainName for period $autoRenewPeriod MONTH"; + $db->exec( + 'INSERT INTO payment_history (registrar_id, date, description, amount) VALUES(?, CURRENT_TIMESTAMP(3), ?, ?)', + [$clid, $description, $price] + ); + } + } elseif ($rgpstatus === 'renewPeriod') { + $renewPeriod_id = $db->selectValue( + 'SELECT id FROM domain WHERE id = ? AND (CURRENT_TIMESTAMP(3) < DATE_ADD(renewedDate, INTERVAL 5 DAY)) LIMIT 1', + [ + $domain_id + ] + ); + if ($renewPeriod_id) { + $returnValue = getDomainPrice($db, $domainName, $tld_id, $renewPeriod, 'renew'); + $price = $returnValue['price']; + + if (!$price) { + $this->container->get('flash')->addMessage('error', 'The price, period and currency for such TLD are not declared'); + return $response->withHeader('Location', '/domains')->withStatus(302); + } + + $db->exec( + 'UPDATE registrar SET accountBalance = accountBalance + ? WHERE id = ?', + [$price, $clid] + ); + + $description = "domain name is deleted by the registrar during grace renewPeriod, the registry provides a credit for the cost of the renewal domain $domainName for period $renewPeriod MONTH"; + $db->exec( + 'INSERT INTO payment_history (registrar_id, date, description, amount) VALUES(?, CURRENT_TIMESTAMP(3), ?, ?)', + [$clid, $description, $price] + ); + } + } elseif ($rgpstatus === 'transferPeriod') { + $transferPeriod_id = $db->selectValue( + 'SELECT id FROM domain WHERE id = ? AND (CURRENT_TIMESTAMP(3) < DATE_ADD(trdate, INTERVAL 5 DAY)) LIMIT 1', + [ + $domain_id + ] + ); + if ($transferPeriod_id) { + $returnValue = getDomainPrice($db, $domainName, $tld_id, $transferPeriod, 'renew'); + $price = $returnValue['price']; + + if (!$price) { + $this->container->get('flash')->addMessage('error', 'The price, period and currency for such TLD are not declared'); + return $response->withHeader('Location', '/domains')->withStatus(302); + } + + $db->exec( + 'UPDATE registrar SET accountBalance = accountBalance + ? WHERE id = ?', + [$price, $clid] + ); + + $description = "domain name is deleted by the registrar during grace transferPeriod, the registry provides a credit for the cost of the transfer domain $domainName for period $transferPeriod MONTH"; + $db->exec( + 'INSERT INTO payment_history (registrar_id, date, description, amount) VALUES(?, CURRENT_TIMESTAMP(3), ?, ?)', + [$clid, $description, $price] + ); + } + } + } + + if ($isImmediateDeletion) { + $this->container->get('flash')->addMessage('success', 'Domain ' . $domainName . ' deleted successfully'); + } else { + $this->container->get('flash')->addMessage('info', 'Deletion process for domain ' . $domainName . ' has been initiated'); + } + return $response->withHeader('Location', '/domains')->withStatus(302); + } else { + // Redirect to the domains view + return $response->withHeader('Location', '/domains')->withStatus(302); + } + + //} + } + +} \ No newline at end of file diff --git a/cp/app/Controllers/DomainsController.php b/cp/app/Controllers/DomainsController.php index d2cb87f..c39b4a5 100644 --- a/cp/app/Controllers/DomainsController.php +++ b/cp/app/Controllers/DomainsController.php @@ -13,12 +13,7 @@ class DomainsController extends Controller { return view($response,'admin/domains/listDomains.twig'); } - - public function listApplications(Request $request, Response $response) - { - return view($response,'admin/domains/listApplications.twig'); - } - + public function checkDomain(Request $request, Response $response) { if ($request->getMethod() === 'POST') { diff --git a/cp/resources/views/admin/domains/createApplication.twig b/cp/resources/views/admin/domains/createApplication.twig new file mode 100644 index 0000000..a82f20e --- /dev/null +++ b/cp/resources/views/admin/domains/createApplication.twig @@ -0,0 +1,245 @@ +{% extends "layouts/app.twig" %} + +{% block title %}{{ __('Create Application') }}{% endblock %} + +{% block content %} +
+ + + +
+
+
+ {% if domainName is defined and crdate is defined %} + + {% elseif error is defined %} + + {% endif %} +
+
+
+ {{ csrf.field | raw }} +
+ + +
+ +
+ + +
+ +
+ + + The "Phase name" field is required only if the "Type" is set to "Custom". +
+ + {% if registrars and not registrar %} +
+ + +
+ {% endif %} + + +
+ + + + + +
+ + +
+ + +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+ + +
+ + +
+ +
+ + +
+ +
+ + +
+
+
+
+
+ + + +{% endblock %} \ No newline at end of file diff --git a/cp/resources/views/admin/domains/listApplications.twig b/cp/resources/views/admin/domains/listApplications.twig index f3c0b3a..27a8b82 100644 --- a/cp/resources/views/admin/domains/listApplications.twig +++ b/cp/resources/views/admin/domains/listApplications.twig @@ -25,11 +25,11 @@ {{ __('Check Domain') }} - + - {{ __('Create Domain') }} + {{ __('Create Application') }} - + diff --git a/cp/resources/views/admin/domains/viewApplication.twig b/cp/resources/views/admin/domains/viewApplication.twig new file mode 100644 index 0000000..a6eb1a8 --- /dev/null +++ b/cp/resources/views/admin/domains/viewApplication.twig @@ -0,0 +1,148 @@ +{% extends "layouts/app.twig" %} + +{% block title %}{{ __('Application Details') }}{% endblock %} + +{% block content %} +
+ + + +
+
+
+
+
+

+ Application for {{ domain.name }}  + {% if domain.status %} + {% if domain is iterable %} + {% for status in domainStatus %} + {{ status.status }}  + {% endfor %} + {% else %} + {% if domain.status %} + {{ domain.status }}  + {% endif %} + {% endif %} + {% else %} + ok + {% endif %} +

+
+
+
+
+
Registered On
+
{{ domain.crdate }}
+
+
+
Launch Phase
+
{{ domain.phase_type }}
+
+
+
Expiration Date
+
{{ domain.exdate }}
+
+
+
Registrar
+
{{ registrars.name }}
+
+
+
Registrant
+
{{ domainRegistrant.identifier }}
+
+ {% for contact in domainContacts %} +
+
{{ contact.type }} contact
+
{{ contact.identifier }}
+
+ {% endfor %} +
+
Auth Type
+
+ {% if domain.authtype == 'pw' %} + Regular + {% elseif domain.authtype == 'ext' %} + HSM + {% else %} + {{ domain.authtype }} {# Fallback in case there are other types #} + {% endif %} +
+
+
+
Auth Info
+
+ {{ domain.authinfo }} +
+
+
+
+
+
+
+
+
+
+
Nameservers
+
+ {% for host in domainHosts %} +
+
Nameserver {{ loop.index }}
+
+ {{ host.name }} +
+
+ {% endfor %} +
+
+
+
+
+
+
+
+ + + + +{% endblock %} \ No newline at end of file diff --git a/cp/resources/views/layouts/app.twig b/cp/resources/views/layouts/app.twig index a031026..02055b2 100644 --- a/cp/resources/views/layouts/app.twig +++ b/cp/resources/views/layouts/app.twig @@ -83,7 +83,7 @@ -
  • +
  • {{ __('List Applications') }} + + {{ __('Create Application') }} + {{ __('Transfers') }} diff --git a/cp/resources/views/partials/js-applications.twig b/cp/resources/views/partials/js-applications.twig index e8279af..188a1b9 100644 --- a/cp/resources/views/partials/js-applications.twig +++ b/cp/resources/views/partials/js-applications.twig @@ -9,32 +9,21 @@ document.addEventListener("DOMContentLoaded", function(){ function domainLinkFormatter(cell){ var value = cell.getValue(); - return `${value}`; + return `${value}`; } function actionsFormatter(cell, formatterParams, onRendered) { var rowData = cell.getRow().getData(); var actionButtons = ''; - var hasPendingDelete = rowData.domain_status.some(statusObj => statusObj.status && statusObj.status.includes('pendingDelete')); - var hasPendingRestore = rowData.rgpstatus ? rowData.rgpstatus.includes('pendingRestore') : false; - // Common action button for all statuses - actionButtons += ` `; - - if (hasPendingRestore) { - actionButtons += ``; - } else if (hasPendingDelete) { - actionButtons += ``; - } else { - actionButtons += ` `; - actionButtons += ``; - } + actionButtons += ` `; + actionButtons += ``; return actionButtons; } - function statusFormatter(cell) { + function phaseFormatter(cell) { var statusArray = cell.getValue(); var rowData = cell.getRow().getData(); // Get the entire row data @@ -44,15 +33,7 @@ } // Check if statusArray is empty or not - if (statusArray && Array.isArray(statusArray) && statusArray.length > 0) { - return statusArray.map(item => createBadge(item.status, 'azure')).join(' '); - } else if (rowData.rgpstatus) { - // Fallback to rgpstatus column if statusArray is empty - return createBadge(rowData.rgpstatus, 'lime'); - } else { - // Display 'ok' status with info badge if both statusArray and rgpstatus are empty - return createBadge('ok', 'info'); - } + return createBadge(statusArray, 'info'); } var searchTerm = ""; // global variable to hold the search term @@ -112,7 +93,7 @@ {title:"Name", field:"name", width:200, headerSort:false, formatter: domainLinkFormatter, responsive:0}, {title:"Applicant", width:200, field:"registrant.identifier", headerSort:false, responsive:2}, {title:"Creation Date", width:250, minWidth:150, field:"crdate", headerSort:false, responsive:2}, - {title:"Phase", width:250, minWidth:150, field:"tm_phase", headerSort:false, responsive:2}, + {title:"Phase", width:250, minWidth:150, field:"phase_type", formatter: phaseFormatter, headerSort:false, responsive:2}, {title: "Actions", formatter: actionsFormatter, headerSort: false, download:false, hozAlign: "center", responsive:0, cellClick:function(e, cell){ e.stopPropagation(); }}, ], placeholder:function(){ diff --git a/cp/routes/web.php b/cp/routes/web.php index 333e8b5..f74b937 100644 --- a/cp/routes/web.php +++ b/cp/routes/web.php @@ -3,6 +3,7 @@ use App\Controllers\Auth\AuthController; use App\Controllers\Auth\PasswordController; use App\Controllers\HomeController; use App\Controllers\DomainsController; +use App\Controllers\ApplicationsController; use App\Controllers\ContactsController; use App\Controllers\HostsController; use App\Controllers\LogsController; @@ -51,7 +52,13 @@ $app->group('', function ($route) { $route->map(['GET', 'POST'], '/domain/restore/{domain}', DomainsController::class . ':restoreDomain')->setName('restoreDomain'); $route->map(['GET', 'POST'], '/domain/report/{domain}', DomainsController::class . ':reportDomain')->setName('reportDomain'); - $route->get('/applications', DomainsController::class .':listApplications')->setName('listApplications'); + $route->get('/applications', ApplicationsController::class .':listApplications')->setName('listApplications'); + $route->map(['GET', 'POST'], '/application/create', ApplicationsController::class . ':createApplication')->setName('createApplication'); + $route->get('/application/view/{application}', ApplicationsController::class . ':viewApplication')->setName('viewApplication'); + $route->get('/application/update/{application}', ApplicationsController::class . ':updateApplication')->setName('updateApplication'); + $route->post('/application/update', ApplicationsController::class . ':updateApplicationProcess')->setName('updateApplicationProcess'); + $route->post('/application/deletehost', ApplicationsController::class . ':applicationDeleteHost')->setName('applicationDeleteHost'); + $route->map(['GET', 'POST'], '/application/delete/{application}', ApplicationsController::class . ':deleteApplication')->setName('deleteApplication'); $route->get('/transfers', DomainsController::class . ':listTransfers')->setName('listTransfers'); $route->map(['GET', 'POST'], '/transfer/request', DomainsController::class . ':requestTransfer')->setName('requestTransfer'); @@ -173,6 +180,7 @@ $app->any('/api[/{params:.*}]', function ( $columnMap = [ 'contact' => 'clid', 'domain' => 'clid', + 'application' => 'clid', 'host' => 'clid', 'poll' => 'registrar_id', 'registrar' => 'id', diff --git a/database/registry.mariadb.sql b/database/registry.mariadb.sql index 450b369..602307f 100644 --- a/database/registry.mariadb.sql +++ b/database/registry.mariadb.sql @@ -373,25 +373,11 @@ CREATE TABLE IF NOT EXISTS `registry`.`application` ( `transfer_exdate` datetime(3) default NULL, `idnlang` varchar(16) default NULL, `delTime` datetime(3) default NULL, - `resTime` datetime(3) default NULL, - `rgpstatus` enum('addPeriod','autoRenewPeriod','renewPeriod','transferPeriod','pendingDelete','pendingRestore','redemptionPeriod') default NULL, - `rgppostData` text default NULL, - `rgpdelTime` datetime(3) default NULL, - `rgpresTime` datetime(3) default NULL, - `rgpresReason` text default NULL, - `rgpstatement1` text default NULL, - `rgpstatement2` text default NULL, - `rgpother` text default NULL, - `addPeriod` tinyint(3) unsigned default NULL, - `autoRenewPeriod` tinyint(3) unsigned default NULL, - `renewPeriod` tinyint(3) unsigned default NULL, - `transferPeriod` tinyint(3) unsigned default NULL, - `renewedDate` datetime(3) default NULL, - `agp_exempted` tinyint(1) DEFAULT 0, - `agp_request` datetime(3) default NULL, - `agp_grant` datetime(3) default NULL, - `agp_reason` text default NULL, - `agp_status` varchar(30) default NULL, + `authtype` enum('pw','ext') NOT NULL default 'pw', + `authinfo` varchar(64) NOT NULL, + `phase_name` VARCHAR(75) DEFAULT NULL, + `phase_type` VARCHAR(50) NOT NULL, + `smd` text default NULL, `tm_notice_accepted` datetime(3) default NULL, `tm_notice_expires` datetime(3) default NULL, `tm_notice_id` varchar(150) default NULL, @@ -417,7 +403,18 @@ CREATE TABLE IF NOT EXISTS `registry`.`domain_contact_map` ( UNIQUE KEY `uniquekey` (`domain_id`,`contact_id`,`type`), CONSTRAINT `domain_contact_map_ibfk_1` FOREIGN KEY (`domain_id`) REFERENCES `domain` (`id`) ON DELETE RESTRICT, CONSTRAINT `domain_contact_map_ibfk_2` FOREIGN KEY (`contact_id`) REFERENCES `contact` (`id`) ON DELETE RESTRICT -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='contact map'; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='contact map for domains'; + +CREATE TABLE IF NOT EXISTS `registry`.`application_contact_map` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `domain_id` int(10) unsigned NOT NULL, + `contact_id` int(10) unsigned NOT NULL, + `type` enum('admin','billing','tech') NOT NULL default 'admin', + PRIMARY KEY (`id`), + UNIQUE KEY `uniquekey` (`domain_id`,`contact_id`,`type`), + CONSTRAINT `application_contact_map_ibfk_1` FOREIGN KEY (`domain_id`) REFERENCES `application` (`id`) ON DELETE RESTRICT, + CONSTRAINT `application_contact_map_ibfk_2` FOREIGN KEY (`contact_id`) REFERENCES `contact` (`id`) ON DELETE RESTRICT +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='contact map for applications'; CREATE TABLE IF NOT EXISTS `registry`.`domain_authInfo` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, @@ -438,6 +435,15 @@ CREATE TABLE IF NOT EXISTS `registry`.`domain_status` ( CONSTRAINT `domain_status_ibfk_1` FOREIGN KEY (`domain_id`) REFERENCES `domain` (`id`) ON DELETE RESTRICT ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='domain:status'; +CREATE TABLE IF NOT EXISTS `registry`.`application_status` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `domain_id` int(10) unsigned NOT NULL, + `status` enum('pendingValidation','validated','invalid','pendingAllocation','allocated','rejected','custom') NOT NULL default 'pendingValidation', + PRIMARY KEY (`id`), + UNIQUE KEY `uniquekey` (`domain_id`,`status`), + CONSTRAINT `application_status_ibfk_1` FOREIGN KEY (`domain_id`) REFERENCES `application` (`id`) ON DELETE RESTRICT +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='application:status'; + CREATE TABLE IF NOT EXISTS `registry`.`secdns` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `domain_id` int(10) unsigned NOT NULL, @@ -482,7 +488,17 @@ CREATE TABLE IF NOT EXISTS `registry`.`domain_host_map` ( UNIQUE KEY `domain_host_map_id` (`domain_id`,`host_id`), CONSTRAINT `domain_host_map_ibfk_1` FOREIGN KEY (`domain_id`) REFERENCES `domain` (`id`) ON DELETE RESTRICT, CONSTRAINT `domain_host_map_ibfk_2` FOREIGN KEY (`host_id`) REFERENCES `host` (`id`) ON DELETE RESTRICT -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='contact map'; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='host map for domains'; + +CREATE TABLE IF NOT EXISTS `registry`.`application_host_map` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `domain_id` int(10) unsigned NOT NULL, + `host_id` int(10) unsigned NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `application_host_map_id` (`domain_id`,`host_id`), + CONSTRAINT `application_host_map_ibfk_1` FOREIGN KEY (`domain_id`) REFERENCES `application` (`id`) ON DELETE RESTRICT, + CONSTRAINT `application_host_map_ibfk_2` FOREIGN KEY (`host_id`) REFERENCES `host` (`id`) ON DELETE RESTRICT +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='host map for applications'; CREATE TABLE IF NOT EXISTS `registry`.`host_addr` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, diff --git a/database/registry.postgres.sql b/database/registry.postgres.sql index 84ce676..193679b 100644 --- a/database/registry.postgres.sql +++ b/database/registry.postgres.sql @@ -371,25 +371,11 @@ CREATE TABLE registry.application ( "transfer_exdate" timestamp(3) without time zone default NULL, "idnlang" varchar(16) default NULL, "deltime" timestamp(3) without time zone default NULL, - "restime" timestamp(3) without time zone default NULL, - "rgpstatus" varchar CHECK ("rgpstatus" IN ( 'addPeriod','autoRenewPeriod','renewPeriod','transferPeriod','pendingDelete','pendingRestore','redemptionPeriod' )) default NULL, - "rgppostdata" text default NULL, - "rgpdeltime" timestamp(3) without time zone default NULL, - "rgprestime" timestamp(3) without time zone default NULL, - "rgpresreason" text default NULL, - "rgpstatement1" text default NULL, - "rgpstatement2" text default NULL, - "rgpother" text default NULL, - "addperiod" smallint CHECK ("addperiod" >= 0) default NULL, - "autorenewperiod" smallint CHECK ("autorenewperiod" >= 0) default NULL, - "renewperiod" smallint CHECK ("renewperiod" >= 0) default NULL, - "transferperiod" smallint CHECK ("transferperiod" >= 0) default NULL, - "reneweddate" timestamp(3) without time zone default NULL, - "agp_exempted" BOOLEAN DEFAULT FALSE, - "agp_request" TIMESTAMP(3) DEFAULT NULL, - "agp_grant" TIMESTAMP(3) DEFAULT NULL, - "agp_reason" TEXT DEFAULT NULL, - "agp_status" VARCHAR(30) DEFAULT NULL, + "authtype" varchar CHECK ("authtype" IN ( 'pw','ext' )) NOT NULL default 'pw', + "authinfo" varchar(64) NOT NULL, + "phase_name" VARCHAR(75) DEFAULT NULL, + "phase_type" VARCHAR(50) NOT NULL, + "smd" TEXT DEFAULT NULL, "tm_notice_accepted" TIMESTAMP(3) DEFAULT NULL, "tm_notice_expires" TIMESTAMP(3) DEFAULT NULL, "tm_notice_id" VARCHAR(150) DEFAULT NULL, @@ -408,6 +394,15 @@ CREATE TABLE registry.domain_contact_map ( unique ("domain_id", "contact_id", "type") ); +CREATE TABLE registry.application_contact_map ( + "id" serial8, + "domain_id" int CHECK ("domain_id" >= 0) NOT NULL, + "contact_id" int CHECK ("contact_id" >= 0) NOT NULL, + "type" varchar CHECK ("type" IN ( 'admin','billing','tech' )) NOT NULL default 'admin', + primary key ("id"), + unique ("domain_id", "contact_id", "type") +); + CREATE TABLE registry.domain_authinfo ( "id" serial8, "domain_id" int CHECK ("domain_id" >= 0) NOT NULL, @@ -425,6 +420,14 @@ CREATE TABLE registry.domain_status ( unique ("domain_id", "status") ); +CREATE TABLE registry.application_status ( + "id" serial8, + "domain_id" int CHECK ("domain_id" >= 0) NOT NULL, + "status" varchar CHECK ("status" IN ( 'pendingValidation','validated','invalid','pendingAllocation','allocated','rejected','custom' )) NOT NULL default 'pendingValidation', + primary key ("id"), + unique ("domain_id", "status") +); + CREATE TABLE registry.secdns ( "id" serial8, "domain_id" int CHECK ("domain_id" >= 0) NOT NULL, @@ -464,6 +467,14 @@ CREATE TABLE registry.domain_host_map ( unique ("domain_id", "host_id") ); +CREATE TABLE registry.application_host_map ( + "id" serial8, + "domain_id" int CHECK ("domain_id" >= 0) NOT NULL, + "host_id" int CHECK ("host_id" >= 0) NOT NULL, + primary key ("id"), + unique ("domain_id", "host_id") +); + CREATE TABLE registry.host_addr ( "id" serial8, "host_id" int CHECK ("host_id" >= 0) NOT NULL, @@ -836,8 +847,11 @@ ALTER TABLE registry.domain ADD FOREIGN KEY ("acid") REFERENCES registry.registr ALTER TABLE registry.domain ADD FOREIGN KEY ("tldid") REFERENCES registry.domain_tld ("id"); ALTER TABLE registry.domain_contact_map ADD FOREIGN KEY ("domain_id") REFERENCES registry.domain ("id"); ALTER TABLE registry.domain_contact_map ADD FOREIGN KEY ("contact_id") REFERENCES registry.contact ("id"); +ALTER TABLE registry.application_contact_map ADD FOREIGN KEY ("domain_id") REFERENCES registry.application ("id"); +ALTER TABLE registry.application_contact_map ADD FOREIGN KEY ("contact_id") REFERENCES registry.contact ("id"); ALTER TABLE registry.domain_authinfo ADD FOREIGN KEY ("domain_id") REFERENCES registry.domain ("id"); ALTER TABLE registry.domain_status ADD FOREIGN KEY ("domain_id") REFERENCES registry.domain ("id"); +ALTER TABLE registry.application_status ADD FOREIGN KEY ("domain_id") REFERENCES registry.application ("id"); ALTER TABLE registry.secdns ADD FOREIGN KEY ("domain_id") REFERENCES registry.domain ("id"); ALTER TABLE registry.host ADD FOREIGN KEY ("clid") REFERENCES registry.registrar ("id"); ALTER TABLE registry.host ADD FOREIGN KEY ("crid") REFERENCES registry.registrar ("id"); @@ -845,6 +859,8 @@ ALTER TABLE registry.host ADD FOREIGN KEY ("upid") REFERENCES registry.registrar ALTER TABLE registry.host ADD FOREIGN KEY ("domain_id") REFERENCES registry.domain ("id"); ALTER TABLE registry.domain_host_map ADD FOREIGN KEY ("domain_id") REFERENCES registry.domain ("id"); ALTER TABLE registry.domain_host_map ADD FOREIGN KEY ("host_id") REFERENCES registry.host ("id"); +ALTER TABLE registry.application_host_map ADD FOREIGN KEY ("domain_id") REFERENCES registry.application ("id"); +ALTER TABLE registry.application_host_map ADD FOREIGN KEY ("host_id") REFERENCES registry.host ("id"); ALTER TABLE registry.host_addr ADD FOREIGN KEY ("host_id") REFERENCES registry.host ("id"); ALTER TABLE registry.host_status ADD FOREIGN KEY ("host_id") REFERENCES registry.host ("id");