Initial Ladill Hosting app with Gitea deploy pipeline.
Deploy Ladill Hosting / deploy (push) Failing after 17s
Deploy Ladill Hosting / deploy (push) Failing after 17s
Shared web hosting extracted from the platform monolith, with CI deploy to /var/www/ladill-hosting matching Bird/Domains/Email. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,369 @@
|
||||
<?php
|
||||
|
||||
namespace App\Support;
|
||||
|
||||
use App\Services\Domain\DomainPricingService;
|
||||
|
||||
class DomainConfig
|
||||
{
|
||||
private const PRIORITY_TLDS = [
|
||||
'com', 'org', 'net', 'ai', 'io', 'co', 'app', 'dev',
|
||||
'store', 'shop', 'online', 'site', 'tech', 'cloud', 'pro', 'me',
|
||||
'xyz', 'club', 'live', 'com.ng', 'com.gh', 'co.za', 'co.uk', 'info', 'biz',
|
||||
];
|
||||
|
||||
private const DEFAULT_PAGE_SIZE = 20;
|
||||
|
||||
/**
|
||||
* Get featured TLDs for initial display (lightweight).
|
||||
*
|
||||
* @return list<string>
|
||||
*/
|
||||
public static function featuredTlds(): array
|
||||
{
|
||||
$allTlds = self::allTlds();
|
||||
$featured = array_values(array_intersect(self::PRIORITY_TLDS, $allTlds));
|
||||
|
||||
if ($featured !== []) {
|
||||
return $featured;
|
||||
}
|
||||
|
||||
return array_slice($allTlds, 0, self::DEFAULT_PAGE_SIZE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all available TLDs sorted by priority.
|
||||
*
|
||||
* @return list<string>
|
||||
*/
|
||||
public static function allTlds(): array
|
||||
{
|
||||
$current = self::normalizeTlds(config('domain.search_tlds', []));
|
||||
$legacy = self::normalizeTlds(config('mailinfra.domain_search_tlds', []));
|
||||
|
||||
$tlds = $legacy !== [] ? $legacy : $current;
|
||||
$livePricedTlds = self::livePricedTlds();
|
||||
|
||||
if ($livePricedTlds === []) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$livePricedLookup = array_flip($livePricedTlds);
|
||||
$tlds = array_values(array_filter(
|
||||
$tlds,
|
||||
static fn (string $tld): bool => isset($livePricedLookup[$tld])
|
||||
));
|
||||
|
||||
return self::sortWithPriority($tlds);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get paginated TLDs (excluding featured ones).
|
||||
*
|
||||
* @return array{tlds: list<string>, hasMore: bool, total: int}
|
||||
*/
|
||||
public static function paginatedTlds(int $page = 1, int $perPage = self::DEFAULT_PAGE_SIZE): array
|
||||
{
|
||||
$allTlds = self::allTlds();
|
||||
$featured = self::featuredTlds();
|
||||
|
||||
$remaining = array_values(array_filter($allTlds, fn ($tld) => !in_array($tld, $featured, true)));
|
||||
$total = count($remaining);
|
||||
|
||||
$offset = ($page - 1) * $perPage;
|
||||
$tlds = array_slice($remaining, $offset, $perPage);
|
||||
|
||||
return [
|
||||
'tlds' => $tlds,
|
||||
'hasMore' => ($offset + $perPage) < $total,
|
||||
'total' => $total,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use featuredTlds() or allTlds() instead
|
||||
* @return list<string>
|
||||
*/
|
||||
public static function searchTlds(): array
|
||||
{
|
||||
return self::featuredTlds();
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the TLD list and query term for a search page request.
|
||||
*
|
||||
* @return array{query: string, tlds: list<string>, hasMore: bool, exact_domain: string|null}
|
||||
*/
|
||||
public static function searchPageConfig(string $query, int $page = 1, int $perPage = self::DEFAULT_PAGE_SIZE): array
|
||||
{
|
||||
$query = strtolower(trim($query));
|
||||
$allTlds = self::allTlds();
|
||||
|
||||
if ($allTlds === []) {
|
||||
return [
|
||||
'query' => $query,
|
||||
'tlds' => [],
|
||||
'hasMore' => false,
|
||||
'exact_domain' => null,
|
||||
];
|
||||
}
|
||||
|
||||
$exactDomain = self::normalizeExactDomain($query);
|
||||
|
||||
if ($exactDomain === null) {
|
||||
if ($page === 1) {
|
||||
$featured = self::featuredTlds();
|
||||
$paginatedData = self::paginatedTlds(1, $perPage);
|
||||
|
||||
return [
|
||||
'query' => $query,
|
||||
'tlds' => $featured,
|
||||
'hasMore' => $paginatedData['hasMore'] || count($paginatedData['tlds']) > 0,
|
||||
'exact_domain' => null,
|
||||
];
|
||||
}
|
||||
|
||||
$paginatedData = self::paginatedTlds($page - 1, $perPage);
|
||||
|
||||
return [
|
||||
'query' => $query,
|
||||
'tlds' => $paginatedData['tlds'],
|
||||
'hasMore' => $paginatedData['hasMore'],
|
||||
'exact_domain' => null,
|
||||
];
|
||||
}
|
||||
|
||||
$exactTld = self::extractTld($exactDomain);
|
||||
$keyword = explode('.', $exactDomain, 2)[0] ?? $exactDomain;
|
||||
$firstPageTlds = array_values(array_unique(array_filter([
|
||||
$exactTld,
|
||||
...self::featuredTlds(),
|
||||
])));
|
||||
$remainingTlds = array_values(array_filter(
|
||||
$allTlds,
|
||||
static fn (string $tld): bool => ! in_array($tld, $firstPageTlds, true)
|
||||
));
|
||||
|
||||
if ($page === 1) {
|
||||
return [
|
||||
'query' => $keyword,
|
||||
'tlds' => $firstPageTlds,
|
||||
'hasMore' => $remainingTlds !== [],
|
||||
'exact_domain' => $exactDomain,
|
||||
];
|
||||
}
|
||||
|
||||
$offset = max(0, ($page - 2) * $perPage);
|
||||
|
||||
return [
|
||||
'query' => $keyword,
|
||||
'tlds' => array_slice($remainingTlds, $offset, $perPage),
|
||||
'hasMore' => ($offset + $perPage) < count($remainingTlds),
|
||||
'exact_domain' => $exactDomain,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort domain search results by TLD priority.
|
||||
*
|
||||
* @param array<int, array{domain: string, ...}> $results
|
||||
* @return array<int, array{domain: string, ...}>
|
||||
*/
|
||||
public static function sortResultsByTldPriority(array $results, ?string $query = null): array
|
||||
{
|
||||
$priorityMap = array_flip(self::PRIORITY_TLDS);
|
||||
$exactDomain = self::normalizeExactDomain((string) $query);
|
||||
|
||||
usort($results, static function (array $a, array $b) use ($priorityMap, $exactDomain): int {
|
||||
$aDomain = strtolower((string) ($a['domain'] ?? ''));
|
||||
$bDomain = strtolower((string) ($b['domain'] ?? ''));
|
||||
|
||||
$aExact = $exactDomain !== null && $aDomain === $exactDomain ? 0 : 1;
|
||||
$bExact = $exactDomain !== null && $bDomain === $exactDomain ? 0 : 1;
|
||||
|
||||
if ($aExact !== $bExact) {
|
||||
return $aExact <=> $bExact;
|
||||
}
|
||||
|
||||
$aPremium = self::isPremiumResult($a) ? 0 : 1;
|
||||
$bPremium = self::isPremiumResult($b) ? 0 : 1;
|
||||
|
||||
if ($aPremium !== $bPremium) {
|
||||
return $aPremium <=> $bPremium;
|
||||
}
|
||||
|
||||
$aAvailable = ! empty($a['available']) ? 0 : 1;
|
||||
$bAvailable = ! empty($b['available']) ? 0 : 1;
|
||||
|
||||
if ($aAvailable !== $bAvailable) {
|
||||
return $aAvailable <=> $bAvailable;
|
||||
}
|
||||
|
||||
$aTld = self::extractTld($a['domain'] ?? '');
|
||||
$bTld = self::extractTld($b['domain'] ?? '');
|
||||
|
||||
$aPriority = $priorityMap[$aTld] ?? PHP_INT_MAX;
|
||||
$bPriority = $priorityMap[$bTld] ?? PHP_INT_MAX;
|
||||
|
||||
if ($aPriority !== $bPriority) {
|
||||
return $aPriority <=> $bPriority;
|
||||
}
|
||||
|
||||
return $aDomain <=> $bDomain;
|
||||
});
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract TLD from a domain name.
|
||||
*/
|
||||
private static function extractTld(string $domain): string
|
||||
{
|
||||
$parts = explode('.', strtolower($domain), 2);
|
||||
|
||||
return $parts[1] ?? '';
|
||||
}
|
||||
|
||||
private static function normalizeExactDomain(string $query): ?string
|
||||
{
|
||||
$query = strtolower(trim($query));
|
||||
|
||||
if ($query === '' || str_starts_with($query, '.') || str_ends_with($query, '.') || ! str_contains($query, '.')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return preg_match('/^(?=.{1,253}$)(?!-)(?:[a-z0-9-]{1,63}\.)+[a-z]{2,63}$/', $query) === 1
|
||||
? $query
|
||||
: null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $result
|
||||
*/
|
||||
private static function isPremiumResult(array $result): bool
|
||||
{
|
||||
$premium = $result['premium'] ?? false;
|
||||
|
||||
if (is_bool($premium)) {
|
||||
return $premium;
|
||||
}
|
||||
|
||||
return in_array(strtolower(trim((string) $premium)), ['1', 'yes', 'true', 'premium'], true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<string> $tlds
|
||||
* @return list<string>
|
||||
*/
|
||||
private static function sortWithPriority(array $tlds): array
|
||||
{
|
||||
$priorityMap = array_flip(self::PRIORITY_TLDS);
|
||||
$pricingMap = self::pricingMapForTlds($tlds);
|
||||
|
||||
usort($tlds, static function (string $a, string $b) use ($priorityMap, $pricingMap): int {
|
||||
$aPriority = $priorityMap[$a] ?? PHP_INT_MAX;
|
||||
$bPriority = $priorityMap[$b] ?? PHP_INT_MAX;
|
||||
|
||||
if ($aPriority !== $bPriority) {
|
||||
return $aPriority <=> $bPriority;
|
||||
}
|
||||
|
||||
$aPrice = (int) ($pricingMap[$a]['register'] ?? 0);
|
||||
$bPrice = (int) ($pricingMap[$b]['register'] ?? 0);
|
||||
|
||||
if ($aPrice !== $bPrice) {
|
||||
return $bPrice <=> $aPrice;
|
||||
}
|
||||
|
||||
return $a <=> $b;
|
||||
});
|
||||
|
||||
return $tlds;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public static function resellerClubTldProductKeys(): array
|
||||
{
|
||||
return array_replace(
|
||||
self::normalizeProductKeyMap(config('domain.resellerclub.tld_product_keys', [])),
|
||||
self::normalizeProductKeyMap(config('mailinfra.resellerclub_tld_product_keys', []))
|
||||
);
|
||||
}
|
||||
|
||||
public static function resellerClubProductKeyCacheTtlSeconds(): int
|
||||
{
|
||||
$ttl = (int) config('domain.resellerclub.product_key_cache_ttl_seconds', 86400);
|
||||
|
||||
return $ttl > 0 ? $ttl : 86400;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $value
|
||||
* @return list<string>
|
||||
*/
|
||||
private static function normalizeTlds(mixed $value): array
|
||||
{
|
||||
if (! is_array($value)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return array_values(array_unique(array_filter(array_map(
|
||||
static fn ($tld): string => ltrim(strtolower(trim((string) $tld)), '.'),
|
||||
$value
|
||||
))));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $value
|
||||
* @return array<string, string>
|
||||
*/
|
||||
private static function normalizeProductKeyMap(mixed $value): array
|
||||
{
|
||||
if (! is_array($value)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$map = [];
|
||||
|
||||
foreach ($value as $tld => $productKey) {
|
||||
$normalizedTld = ltrim(strtolower(trim((string) $tld)), '.');
|
||||
$normalizedProductKey = trim((string) $productKey);
|
||||
|
||||
if ($normalizedTld === '' || $normalizedProductKey === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$map[$normalizedTld] = $normalizedProductKey;
|
||||
}
|
||||
|
||||
return $map;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
private static function livePricedTlds(): array
|
||||
{
|
||||
try {
|
||||
return app(DomainPricingService::class)->getLivePricedTlds();
|
||||
} catch (\Throwable) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<string> $tlds
|
||||
* @return array<string, array{register: int, renew: int, transfer: int, currency: string}>
|
||||
*/
|
||||
private static function pricingMapForTlds(array $tlds): array
|
||||
{
|
||||
try {
|
||||
return app(DomainPricingService::class)->getPricingForTlds($tlds);
|
||||
} catch (\Throwable) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Support;
|
||||
|
||||
final class DomainGlobeIcon
|
||||
{
|
||||
public const VIEW_BOX = '0 0 14 14';
|
||||
|
||||
/** 14×14 globe for UI icons (inherits color via currentColor). */
|
||||
public const ICON_ASSET = 'images/ladill-icons/domain.svg';
|
||||
|
||||
public static function paths(): string
|
||||
{
|
||||
return '<path d="M7 13.5C10.5899 13.5 13.5 10.5899 13.5 7C13.5 3.41015 10.5899 0.5 7 0.5C3.41015 0.5 0.5 3.41015 0.5 7C0.5 10.5899 3.41015 13.5 7 13.5Z" stroke-linecap="round" stroke-linejoin="round"/>'
|
||||
.'<path d="M0.5 7H13.5" stroke-linecap="round" stroke-linejoin="round"/>'
|
||||
.'<path d="M9.5 7C9.3772 9.37699 8.50168 11.6533 7 13.5C5.49832 11.6533 4.6228 9.37699 4.5 7C4.6228 4.62301 5.49832 2.34665 7 0.5C8.50168 2.34665 9.3772 4.62301 9.5 7V7Z" stroke-linecap="round" stroke-linejoin="round"/>';
|
||||
}
|
||||
|
||||
public static function svg(string $class = 'h-5 w-5'): string
|
||||
{
|
||||
return sprintf(
|
||||
'<svg class="%s" viewBox="%s" fill="none" stroke="currentColor" stroke-width="1" aria-hidden="true">%s</svg>',
|
||||
e($class),
|
||||
self::VIEW_BOX,
|
||||
self::paths()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
namespace App\Support;
|
||||
|
||||
/** Storage-tier pricing for mailboxes (config/email.php quota_tiers). */
|
||||
class MailboxPricing
|
||||
{
|
||||
/** @return array<int,array{mb:int,price_minor:int,label:string}> */
|
||||
public static function tiers(): array
|
||||
{
|
||||
return array_map(fn ($t) => [
|
||||
'mb' => (int) $t['mb'],
|
||||
'price_minor' => (int) $t['price_minor'],
|
||||
'label' => self::label((int) $t['mb']),
|
||||
], (array) config('email.quota_tiers', []));
|
||||
}
|
||||
|
||||
public static function isValidQuota(int $mb): bool
|
||||
{
|
||||
foreach (self::tiers() as $t) {
|
||||
if ($t['mb'] === $mb) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static function priceMinorFor(int $mb): int
|
||||
{
|
||||
foreach (self::tiers() as $t) {
|
||||
if ($t['mb'] === $mb) {
|
||||
return $t['price_minor'];
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/** A tier is free when its monthly price is zero (the 1 GB plan). */
|
||||
public static function isFree(int $mb): bool
|
||||
{
|
||||
return self::priceMinorFor($mb) === 0;
|
||||
}
|
||||
|
||||
/** The smallest free tier's quota (the default for new mailboxes). */
|
||||
public static function freeQuotaMb(): int
|
||||
{
|
||||
foreach (self::tiers() as $t) {
|
||||
if ($t['price_minor'] === 0) {
|
||||
return $t['mb'];
|
||||
}
|
||||
}
|
||||
|
||||
return self::tiers()[0]['mb'] ?? 1024;
|
||||
}
|
||||
|
||||
public static function defaultQuotaMb(): int
|
||||
{
|
||||
$default = (int) config('email.default_quota_mb', 1024);
|
||||
|
||||
return self::isValidQuota($default) ? $default : self::freeQuotaMb();
|
||||
}
|
||||
|
||||
public static function label(int $mb): string
|
||||
{
|
||||
return $mb % 1024 === 0 ? ($mb / 1024).' GB' : $mb.' MB';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
<?php
|
||||
|
||||
namespace App\Support;
|
||||
|
||||
use App\Models\DomainOrder;
|
||||
use App\Models\RcServiceOrder;
|
||||
use Illuminate\Support\Collection;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* ResellerClub is legacy-only for new sales. Renewals and management remain available
|
||||
* for customers with existing ResellerClub services.
|
||||
*/
|
||||
final class ResellerClubLegacy
|
||||
{
|
||||
public static function integrationEnabled(): bool
|
||||
{
|
||||
return (bool) config('hosting.legacy.rc_enabled', true);
|
||||
}
|
||||
|
||||
public static function newOrdersEnabled(): bool
|
||||
{
|
||||
return self::integrationEnabled()
|
||||
&& (bool) config('hosting.legacy.rc_new_orders_enabled', false);
|
||||
}
|
||||
|
||||
public static function renewalsEnabled(): bool
|
||||
{
|
||||
return self::integrationEnabled()
|
||||
&& (bool) config('hosting.legacy.rc_renewals_enabled', true);
|
||||
}
|
||||
|
||||
public static function isRenewalRcServiceOrder(RcServiceOrder $order): bool
|
||||
{
|
||||
if ($order->order_type === RcServiceOrder::TYPE_RENEWAL) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return (bool) data_get($order->meta, 'is_renewal', false);
|
||||
}
|
||||
|
||||
public static function isRenewalDomainOrder(DomainOrder $order): bool
|
||||
{
|
||||
return $order->order_type === DomainOrder::TYPE_RENEWAL;
|
||||
}
|
||||
|
||||
public static function canAutomateFulfillment(RcServiceOrder $order): bool
|
||||
{
|
||||
if (! self::integrationEnabled()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (self::newOrdersEnabled()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return self::renewalsEnabled() && self::isRenewalRcServiceOrder($order);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Collection<int, RcServiceOrder>|iterable<int, RcServiceOrder> $items
|
||||
*/
|
||||
public static function assertCartCheckoutAllowed(iterable $items): void
|
||||
{
|
||||
$items = $items instanceof Collection ? $items : collect($items);
|
||||
|
||||
if ($items->isEmpty()) {
|
||||
throw new RuntimeException('Your cart is empty.');
|
||||
}
|
||||
|
||||
if (self::newOrdersEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (! self::renewalsEnabled()) {
|
||||
throw new RuntimeException(self::renewalsDisabledMessage());
|
||||
}
|
||||
|
||||
if ($items->contains(fn (RcServiceOrder $item) => ! self::isRenewalRcServiceOrder($item))) {
|
||||
throw new RuntimeException(
|
||||
'ResellerClub checkout is limited to renewals for existing services. '
|
||||
.'New product purchases use our current domain and hosting products instead.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public static function assertNewSaleAllowed(): void
|
||||
{
|
||||
if (self::newOrdersEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
throw new RuntimeException(
|
||||
'ResellerClub is no longer available for new product purchases. '
|
||||
.'Use our domain and hosting products instead, or renew an existing ResellerClub service from your dashboard.'
|
||||
);
|
||||
}
|
||||
|
||||
public static function assertRenewalsAllowed(): void
|
||||
{
|
||||
if (! self::renewalsEnabled()) {
|
||||
throw new RuntimeException(self::renewalsDisabledMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/** @deprecated Use assertNewSaleAllowed() */
|
||||
public static function assertNewOrdersAllowed(): void
|
||||
{
|
||||
self::assertNewSaleAllowed();
|
||||
}
|
||||
|
||||
public static function renewalsDisabledMessage(): string
|
||||
{
|
||||
return 'ResellerClub renewals are not available at this time. Please contact support if you need assistance.';
|
||||
}
|
||||
|
||||
public static function fulfillmentDisabledMessage(): string
|
||||
{
|
||||
return 'Automated ResellerClub fulfillment for new purchases is no longer available. '
|
||||
.'Open a support ticket if you need help with a legacy order.';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
use App\Models\User;
|
||||
|
||||
if (! function_exists('ladill_account')) {
|
||||
/**
|
||||
* The account the current request acts within — the owner User. Defaults to
|
||||
* the authenticated user; a team member who switched accounts gets the owner.
|
||||
* Set by the SetActingAccount middleware.
|
||||
*/
|
||||
function ladill_account(): ?User
|
||||
{
|
||||
$request = request();
|
||||
|
||||
return $request->attributes->get('actingAccount') ?? $request->user();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user