Skip to content

SyntaxError: 10 (at message-compiler.mjs:77:23) #2336

@NaysKutzu

Description

@NaysKutzu

Reporting a bug?

App works in development but not in production!

Expected behavior

To work both ways

Reproduction

<!--
MIT License

Copyright (c) 2025 MythicalSystems and Contributors
Copyright (c) 2025 Cassian Gherman (NaysKutzu)
Copyright (c) 2018 - 2021 Dane Everitt <dane@daneeveritt.com> and Contributors

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
-->

<template>
    <DashboardLayout :breadcrumbs="breadcrumbs">
        <div class="space-y-6 pb-8">
            <WidgetRenderer v-if="widgetsTopOfPage.length > 0" :widgets="widgetsTopOfPage" />

            <!-- Header Section -->
            <div class="flex flex-col gap-4">
                <div class="flex flex-col sm:flex-row sm:items-start sm:justify-between gap-4">
                    <div class="space-y-1">
                        <h1 class="text-2xl sm:text-3xl font-bold tracking-tight">
                            {{ t('serverProxy.title') }}
                        </h1>
                        <p class="text-sm text-muted-foreground">
                            {{ t('serverProxy.description') }}
                            <span v-if="proxyEnabled" class="font-medium">
                                ({{ proxies.length }}/{{ settingsStore.serverProxyMaxPerServer }})
                            </span>
                        </p>
                    </div>
                    <div class="flex gap-2">
                        <Button
                            variant="outline"
                            size="sm"
                            :disabled="loading"
                            class="flex items-center gap-2"
                            @click="refresh"
                        >
                            <RefreshCw :class="['h-4 w-4', loading && 'animate-spin']" />
                            <span>{{ t('common.refresh') }}</span>
                        </Button>
                        <Button
                            v-if="canManageProxy && proxyEnabled"
                            size="sm"
                            :disabled="loading || proxies.length >= settingsStore.serverProxyMaxPerServer"
                            class="flex items-center gap-2"
                            @click="openCreateDrawer"
                        >
                            <Plus class="h-4 w-4" />
                            <span>{{ t('serverProxy.createProxy') }}</span>
                        </Button>
                    </div>
                </div>

                <!-- Info Banner -->
                <div
                    class="flex items-start gap-3 p-4 rounded-lg bg-blue-50 border-2 border-blue-200 dark:bg-blue-950/30 dark:border-blue-800"
                >
                    <div class="h-10 w-10 rounded-lg bg-blue-500/20 flex items-center justify-center shrink-0">
                        <Info class="h-5 w-5 text-blue-600 dark:text-blue-400" />
                    </div>
                    <div class="flex-1 min-w-0 space-y-1">
                        <h3 class="font-semibold text-blue-800 dark:text-blue-200">
                            {{ t('serverProxy.infoTitle') }}
                        </h3>
                        <p class="text-sm text-blue-700 dark:text-blue-300">
                            {{ t('serverProxy.infoDescription') }}
                        </p>
                    </div>
                </div>
            </div>

            <WidgetRenderer v-if="widgetsAfterHeader.length > 0" :widgets="widgetsAfterHeader" />

            <!-- Feature Disabled State -->
            <Alert v-if="!proxyEnabled" variant="destructive" class="border-2">
                <AlertTitle>{{ t('serverProxy.featureDisabled') }}</AlertTitle>
                <AlertDescription>
                    {{ t('serverProxy.featureDisabledDescription') }}
                </AlertDescription>
            </Alert>

            <!-- Loading State -->
            <div v-else-if="loading && proxies.length === 0" class="flex flex-col items-center justify-center py-16">
                <div class="animate-spin h-10 w-10 border-3 border-primary border-t-transparent rounded-full"></div>
                <span class="mt-4 text-muted-foreground">{{ t('common.loading') }}</span>
            </div>

            <!-- Empty State -->
            <div
                v-else-if="!loading && proxies.length === 0"
                class="flex flex-col items-center justify-center py-16 px-4"
            >
                <div class="text-center max-w-md space-y-6">
                    <div class="flex justify-center">
                        <div class="relative">
                            <div class="absolute inset-0 animate-ping opacity-20">
                                <div class="w-32 h-32 rounded-full bg-primary/20"></div>
                            </div>
                            <div class="relative p-8 rounded-full bg-linear-to-br from-primary/20 to-primary/5">
                                <ArrowRightLeft class="h-16 w-16 text-primary" />
                            </div>
                        </div>
                    </div>
                    <div class="space-y-3">
                        <h3 class="text-2xl sm:text-3xl font-bold text-foreground">
                            {{ t('serverProxy.noProxiesTitle') }}
                        </h3>
                        <p class="text-sm sm:text-base text-muted-foreground">
                            {{ t('serverProxy.noProxiesDescription') }}
                        </p>
                    </div>
                    <Button
                        v-if="canManageProxy && proxyEnabled"
                        size="lg"
                        class="gap-2 shadow-lg"
                        :disabled="proxies.length >= settingsStore.serverProxyMaxPerServer"
                        @click="openCreateDrawer"
                    >
                        <Plus class="h-5 w-5" />
                        {{ t('serverProxy.createProxy') }}
                    </Button>
                </div>
            </div>

            <!-- Plugin Widgets: Before Proxies List -->
            <WidgetRenderer
                v-if="!loading && proxies.length > 0 && widgetsBeforeTable.length > 0"
                :widgets="widgetsBeforeTable"
            />

            <!-- Proxies List -->
            <Card v-if="!loading && proxies.length > 0" class="border-2 hover:border-primary/50 transition-colors">
                <CardHeader>
                    <div class="flex items-center gap-3">
                        <div class="h-10 w-10 rounded-lg bg-primary/10 flex items-center justify-center">
                            <ArrowRightLeft class="h-5 w-5 text-primary" />
                        </div>
                        <div class="flex-1">
                            <CardTitle class="text-lg">{{ t('serverProxy.proxiesTitle') }}</CardTitle>
                            <CardDescription class="text-sm">
                                {{ t('serverProxy.proxiesDescription') }}
                            </CardDescription>
                        </div>
                        <Badge variant="secondary" class="text-xs">
                            {{ proxies.length }}
                            {{ proxies.length === 1 ? 'proxy' : 'proxies' }}
                        </Badge>
                    </div>
                </CardHeader>
                <CardContent>
                    <div class="space-y-3">
                        <div
                            v-for="proxy in proxies"
                            :key="proxy.id"
                            class="group relative rounded-lg border-2 bg-card p-4 transition-all hover:border-primary/50 hover:shadow-md"
                        >
                            <div class="flex flex-col sm:flex-row sm:items-start sm:justify-between gap-3">
                                <div class="flex items-start gap-3 flex-1 min-w-0">
                                    <div
                                        class="h-10 w-10 rounded-lg flex items-center justify-center shrink-0"
                                        :class="proxy.ssl ? 'bg-green-500/10' : 'bg-muted'"
                                    >
                                        <component
                                            :is="proxy.ssl ? CheckCircle : ArrowRightLeft"
                                            class="h-5 w-5"
                                            :class="proxy.ssl ? 'text-green-500' : 'text-muted-foreground'"
                                        />
                                    </div>
                                    <div class="flex-1 min-w-0 space-y-1">
                                        <div class="flex flex-wrap items-center gap-2">
                                            <span class="font-mono font-semibold text-sm break-all">
                                                {{ proxy.domain }}
                                            </span>
                                            <Badge v-if="proxy.ssl" variant="default" class="text-xs">
                                                {{ getSslTypeLabel(proxy.use_lets_encrypt) }}
                                            </Badge>
                                            <Badge variant="secondary" class="text-xs font-mono">
                                                {{ proxy.ip }}:{{ proxy.port }}
                                            </Badge>
                                        </div>
                                        <div
                                            class="flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-muted-foreground"
                                        >
                                            <span>
                                                {{ t('serverProxy.createdAt') }}: {{ formatDate(proxy.created_at) }}
                                            </span>
                                            <span v-if="proxy.use_lets_encrypt && proxy.client_email">
                                                {{ t('serverProxy.email') }}: {{ proxy.client_email }}
                                            </span>
                                        </div>
                                    </div>
                                </div>
                                <div v-if="canManageProxy" class="flex flex-wrap items-center gap-2">
                                    <Button
                                        variant="destructive"
                                        size="sm"
                                        class="flex items-center gap-2"
                                        :disabled="deletingProxyId === proxy.id || loading"
                                        @click="deleteProxy(proxy)"
                                    >
                                        <Trash2 class="h-3.5 w-3.5" />
                                        <span class="hidden sm:inline">{{ t('common.delete') }}</span>
                                    </Button>
                                </div>
                            </div>
                        </div>
                    </div>
                </CardContent>
            </Card>

            <!-- Plugin Widgets: After Proxies List -->
            <WidgetRenderer
                v-if="!loading && proxies.length > 0 && widgetsAfterTable.length > 0"
                :widgets="widgetsAfterTable"
            />

            <WidgetRenderer v-if="widgetsBottomOfPage.length > 0" :widgets="widgetsBottomOfPage" />

            <!-- Create/Edit Proxy Drawer -->
            <Drawer
                :open="drawerOpen"
                @update:open="
                    (val) => {
                        if (!val) closeDrawer();
                    }
                "
            >
                <DrawerContent>
                    <DrawerHeader>
                        <DrawerTitle>{{ t('serverProxy.createProxy') }}</DrawerTitle>
                        <DrawerDescription>
                            {{ t('serverProxy.drawerDescription') }}
                        </DrawerDescription>
                    </DrawerHeader>
                    <div class="px-4 pb-4 space-y-4 overflow-y-auto max-h-[calc(100vh-200px)]">
                        <form class="space-y-4" @submit.prevent="saveProxy">
                            <!-- Domain -->
                            <div class="space-y-2">
                                <Label for="domain">{{ t('serverProxy.domain') }}</Label>
                                <Input
                                    id="domain"
                                    v-model="form.domain"
                                    :placeholder="t('serverProxy.domainPlaceholder')"
                                    :disabled="saving"
                                />
                                <p v-if="errors.domain" class="text-sm text-destructive">{{ errors.domain }}</p>
                                <p class="text-xs text-muted-foreground">{{ t('serverProxy.domainHelp') }}</p>
                            </div>

                            <!-- Port -->
                            <div class="space-y-2">
                                <Label for="port">{{ t('serverProxy.port') }}</Label>
                                <Select v-model="form.port" :disabled="saving || loadingAllocations">
                                    <SelectTrigger>
                                        <SelectValue :placeholder="t('serverProxy.portPlaceholder')" />
                                    </SelectTrigger>
                                    <SelectContent>
                                        <SelectItem
                                            v-for="allocation in allocations"
                                            :key="allocation.id"
                                            :value="String(allocation.port)"
                                        >
                                            {{ allocation.ip }}:{{ allocation.port }}
                                            <span
                                                v-if="allocation.is_primary"
                                                class="ml-2 text-xs text-muted-foreground"
                                            >
                                                ({{ t('serverAllocations.primary') }})
                                            </span>
                                        </SelectItem>
                                    </SelectContent>
                                </Select>
                                <p v-if="errors.port" class="text-sm text-destructive">{{ errors.port }}</p>
                                <p class="text-xs text-muted-foreground">{{ t('serverProxy.portHelp') }}</p>
                            </div>

                            <!-- SSL Toggle -->
                            <div class="space-y-2">
                                <div class="flex items-center justify-between">
                                    <Label for="ssl">{{ t('serverProxy.ssl') }}</Label>
                                    <div class="flex items-center gap-3">
                                        <Button
                                            type="button"
                                            :variant="sslButtonVariant"
                                            size="sm"
                                            :disabled="saving"
                                            class="min-w-[80px]"
                                            @click="toggleSsl"
                                        >
                                            {{ sslButtonText }}
                                        </Button>
                                    </div>
                                </div>
                                <p class="text-xs text-muted-foreground">{{ t('serverProxy.sslHelp') }}</p>
                            </div>

                            <!-- DNS Instructions (shown when SSL/Let's Encrypt is enabled) -->
                            <div v-if="form.ssl && form.use_lets_encrypt" class="space-y-4">
                                <Card
                                    class="border-2 border-blue-200 dark:border-blue-800 bg-blue-50/50 dark:bg-blue-950/20"
                                >
                                    <CardHeader class="pb-3">
                                        <div class="flex items-center gap-3">
                                            <div
                                                class="h-10 w-10 rounded-lg bg-blue-500/20 flex items-center justify-center shrink-0"
                                            >
                                                <Network class="h-5 w-5 text-blue-600 dark:text-blue-400" />
                                            </div>
                                            <div class="flex-1">
                                                <CardTitle class="text-base text-blue-900 dark:text-blue-100">
                                                    {{ t('serverProxy.dnsVerificationRequired') }}
                                                </CardTitle>
                                                <CardDescription class="text-blue-700 dark:text-blue-300 mt-1">
                                                    {{ t('serverProxy.dnsVerificationDescription') }}
                                                </CardDescription>
                                            </div>
                                        </div>
                                    </CardHeader>
                                    <CardContent class="space-y-4">
                                        <!-- DNS Records Display -->
                                        <div v-if="targetIp" class="space-y-3">
                                            <div class="text-sm font-medium text-foreground">
                                                {{ t('serverProxy.dnsRecordA') }}
                                            </div>
                                            <div class="rounded-lg border-2 bg-card p-4 space-y-3">
                                                <div class="grid grid-cols-1 md:grid-cols-3 gap-4">
                                                    <div class="space-y-1.5">
                                                        <Label class="text-xs font-medium text-muted-foreground">
                                                            {{ t('serverProxy.dnsRecordType') }}
                                                        </Label>
                                                        <div
                                                            class="flex items-center gap-2 px-3 py-2 rounded-md bg-muted font-mono text-sm"
                                                        >
                                                            <Badge variant="secondary" class="font-mono">A</Badge>
                                                        </div>
                                                    </div>
                                                    <div class="space-y-1.5">
                                                        <Label class="text-xs font-medium text-muted-foreground">
                                                            {{ t('serverProxy.dnsRecordName') }}
                                                        </Label>
                                                        <div
                                                            class="px-3 py-2 rounded-md bg-muted font-mono text-sm break-all"
                                                        >
                                                            <template v-if="form.domain.trim()">{{
                                                                form.domain.trim()
                                                            }}</template>
                                                            <template v-else>{{ getYourDomainPlaceholder() }}</template>
                                                        </div>
                                                    </div>
                                                    <div class="space-y-1.5">
                                                        <Label class="text-xs font-medium text-muted-foreground">
                                                            {{ t('serverProxy.dnsRecordValue') }}
                                                        </Label>
                                                        <div
                                                            class="px-3 py-2 rounded-md bg-muted font-mono text-sm break-all"
                                                        >
                                                            {{ targetIp }}
                                                        </div>
                                                    </div>
                                                </div>
                                            </div>
                                        </div>

                                        <!-- Verification Status -->
                                        <div class="flex flex-col sm:flex-row items-start sm:items-center gap-3 pt-2">
                                            <Button
                                                type="button"
                                                :disabled="!form.domain.trim() || !form.port || verifyingDns || saving"
                                                :variant="dnsVerified ? 'default' : 'default'"
                                                class="shrink-0"
                                                @click="verifyDns"
                                            >
                                                <Network v-if="!verifyingDns && !dnsVerified" class="h-4 w-4 mr-2" />
                                                <CheckCircle
                                                    v-else-if="dnsVerified"
                                                    class="h-4 w-4 mr-2 text-green-500"
                                                />
                                                <span v-if="verifyingDns" class="flex items-center gap-2">
                                                    <span
                                                        class="h-4 w-4 border-2 border-current border-t-transparent rounded-full animate-spin"
                                                    ></span>
                                                    {{ t('serverProxy.verifyingDns') }}
                                                </span>
                                                <span v-else-if="dnsVerified">
                                                    {{ t('serverProxy.dnsVerified') }}
                                                </span>
                                                <span v-else>
                                                    {{ t('serverProxy.verifyDns') }}
                                                </span>
                                            </Button>
                                            <div class="flex-1 min-w-0">
                                                <Alert
                                                    v-if="dnsVerificationError"
                                                    variant="destructive"
                                                    class="border-2"
                                                >
                                                    <AlertTriangle class="h-4 w-4" />
                                                    <AlertDescription class="text-sm">
                                                        {{ dnsVerificationError }}
                                                    </AlertDescription>
                                                </Alert>
                                                <Alert
                                                    v-else-if="dnsVerified"
                                                    class="border-2 border-green-200 dark:border-green-800 bg-green-50 dark:bg-green-950/30"
                                                >
                                                    <CheckCircle class="h-4 w-4 text-green-600 dark:text-green-400" />
                                                    <AlertDescription
                                                        class="text-sm text-green-800 dark:text-green-200"
                                                    >
                                                        {{ t('serverProxy.dnsVerificationSuccess') }}
                                                    </AlertDescription>
                                                </Alert>
                                            </div>
                                        </div>
                                    </CardContent>
                                </Card>
                            </div>

                            <!-- SSL Options (shown when SSL is enabled) -->
                            <div v-if="form.ssl" class="space-y-4 border-l-2 border-primary/20 pl-4">
                                <!-- Use Let's Encrypt -->
                                <div class="space-y-2">
                                    <div class="flex items-center justify-between">
                                        <Label for="use_lets_encrypt">{{ getUseLetsEncryptLabel() }}</Label>
                                        <div class="flex items-center gap-3">
                                            <Button
                                                type="button"
                                                :variant="letsEncryptButtonVariant"
                                                size="sm"
                                                :disabled="saving"
                                                class="min-w-[80px]"
                                                @click="toggleUseLetsEncrypt"
                                            >
                                                {{ letsEncryptButtonText }}
                                            </Button>
                                        </div>
                                    </div>
                                    <p class="text-xs text-muted-foreground">
                                        {{ getUseLetsEncryptHelp() }}
                                    </p>
                                </div>

                                <!-- Let's Encrypt Email (shown when using Let's Encrypt) -->
                                <div v-if="form.use_lets_encrypt" class="space-y-2">
                                    <Label for="client_email">{{ t('serverProxy.clientEmail') }}</Label>
                                    <Input
                                        id="client_email"
                                        v-model="form.client_email"
                                        type="email"
                                        :placeholder="t('serverProxy.clientEmailPlaceholder')"
                                        :disabled="saving"
                                    />
                                    <p v-if="errors.client_email" class="text-sm text-destructive">
                                        {{ errors.client_email }}
                                    </p>
                                    <p class="text-xs text-muted-foreground">{{ t('serverProxy.clientEmailHelp') }}</p>
                                </div>

                                <!-- Custom SSL Certificate (shown when NOT using Let's Encrypt) -->
                                <div v-if="!form.use_lets_encrypt" class="space-y-4">
                                    <div class="space-y-2">
                                        <Label for="ssl_cert">{{ t('serverProxy.sslCert') }}</Label>
                                        <Textarea
                                            id="ssl_cert"
                                            v-model="form.ssl_cert"
                                            :placeholder="t('serverProxy.sslCertPlaceholder')"
                                            :disabled="saving"
                                            rows="6"
                                            class="font-mono text-xs"
                                        />
                                        <p v-if="errors.ssl_cert" class="text-sm text-destructive">
                                            {{ errors.ssl_cert }}
                                        </p>
                                        <p class="text-xs text-muted-foreground">{{ t('serverProxy.sslCertHelp') }}</p>
                                    </div>

                                    <div class="space-y-2">
                                        <Label for="ssl_key">{{ t('serverProxy.sslKey') }}</Label>
                                        <Textarea
                                            id="ssl_key"
                                            v-model="form.ssl_key"
                                            :placeholder="t('serverProxy.sslKeyPlaceholder')"
                                            :disabled="saving"
                                            rows="6"
                                            class="font-mono text-xs"
                                        />
                                        <p v-if="errors.ssl_key" class="text-sm text-destructive">
                                            {{ errors.ssl_key }}
                                        </p>
                                        <p class="text-xs text-muted-foreground">{{ t('serverProxy.sslKeyHelp') }}</p>
                                    </div>
                                </div>
                            </div>

                            <!-- Error Message -->
                            <Alert v-if="formError" variant="destructive" class="border-2">
                                <AlertTitle>{{ t('serverProxy.errorTitle') }}</AlertTitle>
                                <AlertDescription>{{ formError }}</AlertDescription>
                            </Alert>
                        </form>
                    </div>
                    <DrawerFooter>
                        <Button variant="outline" :disabled="saving" @click="closeDrawer">
                            {{ t('common.cancel') }}
                        </Button>
                        <Button :disabled="saving || loadingAllocations" @click="saveProxy">
                            <span v-if="saving">{{ t('common.saving') }}</span>
                            <span v-else>{{ t('serverProxy.createProxy') }}</span>
                        </Button>
                    </DrawerFooter>
                </DrawerContent>
            </Drawer>
        </div>
    </DashboardLayout>
</template>

<script setup lang="ts">
import { ref, reactive, computed, onMounted, watch } from 'vue';
import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';
import axios from 'axios';
import DashboardLayout from '@/layouts/DashboardLayout.vue';
import type { BreadcrumbEntry } from '@/layouts/DashboardLayout.vue';
import { useToast } from 'vue-toastification';
import { useSessionStore } from '@/stores/session';
import { useSettingsStore } from '@/stores/settings';
import WidgetRenderer from '@/components/plugins/WidgetRenderer.vue';
import { usePluginWidgets, getWidgets } from '@/composables/usePluginWidgets';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Alert, AlertTitle, AlertDescription } from '@/components/ui/alert';
import {
    Drawer,
    DrawerContent,
    DrawerDescription,
    DrawerFooter,
    DrawerHeader,
    DrawerTitle,
} from '@/components/ui/drawer';
import { Info, ArrowRightLeft, Plus, Network, CheckCircle, AlertTriangle, Trash2, RefreshCw } from 'lucide-vue-next';

const route = useRoute();
const { t } = useI18n();
const toast = useToast();
const sessionStore = useSessionStore();
const settingsStore = useSettingsStore();

const serverUuid = computed(() => route.params.uuidShort as string);

// Check if proxy management is enabled
const proxyEnabled = computed(() => settingsStore.serverAllowUserMadeProxy);

interface ServerInfo {
    id: number;
    name: string;
    uuid: string;
}

interface ServerAllocation {
    id: number;
    node_id: number;
    ip: string;
    port: number;
    ip_alias?: string;
    notes?: string;
    is_primary?: boolean;
}

const serverInfo = ref<ServerInfo | null>(null);
const allocations = ref<ServerAllocation[]>([]);
const loadingAllocations = ref<boolean>(false);
const proxies = ref<Proxy[]>([]);
const loading = ref<boolean>(false);
const deletingProxyId = ref<number | null>(null);

// Plugin widgets
const { fetchWidgets: fetchPluginWidgets } = usePluginWidgets('server-proxy');
const widgetsTopOfPage = computed(() => getWidgets('server-proxy', 'top-of-page'));
const widgetsAfterHeader = computed(() => getWidgets('server-proxy', 'after-header'));
const widgetsBeforeTable = computed(() => getWidgets('server-proxy', 'before-proxies-list'));
const widgetsAfterTable = computed(() => getWidgets('server-proxy', 'after-proxies-list'));
const widgetsBottomOfPage = computed(() => getWidgets('server-proxy', 'bottom-of-page'));

interface Proxy {
    id: number;
    server_id: number;
    domain: string;
    ip: string;
    port: number;
    ssl: boolean;
    use_lets_encrypt: boolean;
    client_email?: string | null;
    ssl_cert?: string | null;
    ssl_key?: string | null;
    created_at: string;
    updated_at: string;
}

const breadcrumbs = computed<BreadcrumbEntry[]>(() => [
    { text: t('common.dashboard'), href: '/dashboard' },
    { text: t('common.servers'), href: '/dashboard' },
    { text: serverInfo.value?.name || t('common.server'), href: `/server/${route.params.uuidShort}` },
    { text: t('serverProxy.title'), isCurrent: true, href: `/server/${route.params.uuidShort}/proxy` },
]);

function getAxiosErrorMessage(err: unknown, fallback: string): string {
    return axios.isAxiosError(err) && err.response?.data?.message ? err.response.data.message : fallback;
}

const drawerOpen = ref<boolean>(false);
const saving = ref<boolean>(false);
const formError = ref<string | null>(null);
const dnsVerified = ref<boolean>(false);
const verifyingDns = ref<boolean>(false);
const dnsVerificationError = ref<string | null>(null);
const targetIp = ref<string | null>(null);

const form = reactive<{
    domain: string;
    port: string;
    ssl: boolean;
    use_lets_encrypt: boolean;
    client_email: string;
    ssl_cert: string;
    ssl_key: string;
}>({
    domain: '',
    port: '',
    ssl: false,
    use_lets_encrypt: false,
    client_email: '',
    ssl_cert: '',
    ssl_key: '',
});

const errors = reactive<{
    domain: string;
    port: string;
    client_email: string;
    ssl_cert: string;
    ssl_key: string;
}>({
    domain: '',
    port: '',
    client_email: '',
    ssl_cert: '',
    ssl_key: '',
});

const canManageProxy = computed<boolean>(() => {
    return sessionStore.hasPermission('proxy.manage');
});

const sslButtonText = computed<string>(() => {
    return form.ssl ? 'On' : 'Off';
});

const sslButtonVariant = computed<'default' | 'outline'>(() => {
    return form.ssl ? 'default' : 'outline';
});

const letsEncryptButtonText = computed<string>(() => {
    return form.use_lets_encrypt ? 'On' : 'Off';
});

const letsEncryptButtonVariant = computed<'default' | 'outline'>(() => {
    return form.use_lets_encrypt ? 'default' : 'outline';
});

async function fetchServerAllocations(): Promise<void> {
    if (!serverUuid.value) return;

    loadingAllocations.value = true;
    try {
        const { data } = await axios.get(`/api/user/servers/${serverUuid.value}/allocations`);

        if (!data.success) {
            toast.error(data.message || t('serverAllocations.failedToFetch'));
            return;
        }

        serverInfo.value = {
            id: data.data.server.id,
            name: data.data.server.name,
            uuid: data.data.server.uuid,
        };
        allocations.value = data.data.allocations ?? [];

        // Set default port from primary allocation
        const primary = allocations.value.find((a) => a.is_primary);
        if (primary) {
            form.port = String(primary.port);
        } else if (allocations.value.length > 0) {
            const firstAllocation = allocations.value[0];
            if (firstAllocation) {
                form.port = String(firstAllocation.port);
            }
        }
    } catch (error) {
        console.error('Failed to fetch server allocations for proxy:', error);
        toast.error(getAxiosErrorMessage(error, t('serverAllocations.failedToFetch')));
    } finally {
        loadingAllocations.value = false;
    }
}

function resetForm(): void {
    form.domain = '';
    form.port = '';
    form.ssl = false;
    form.use_lets_encrypt = false;
    form.client_email = '';
    form.ssl_cert = '';
    form.ssl_key = '';
    formError.value = null;
    errors.domain = '';
    errors.port = '';
    errors.client_email = '';
    errors.ssl_cert = '';
    errors.ssl_key = '';
    dnsVerified.value = false;
    dnsVerificationError.value = null;
    targetIp.value = null;
}

async function calculateTargetIp(): Promise<void> {
    if (!form.domain.trim() || !form.port) {
        targetIp.value = null;
        return;
    }

    // Get allocation to determine target IP
    const portNum = parseInt(form.port, 10);
    const allocation = allocations.value.find((a) => a.port === portNum);

    if (!allocation) {
        targetIp.value = null;
        return;
    }

    // For now, we'll get the target IP from the verify endpoint
    // But we can also calculate it here if we have node info
    // For simplicity, we'll fetch it during verification
    targetIp.value = allocation.ip; // Will be updated during verification if internal IP
}

async function verifyDns(): Promise<void> {
    if (!form.domain.trim() || !form.port) {
        dnsVerificationError.value = t('serverProxy.domainAndPortRequired');
        return;
    }

    verifyingDns.value = true;
    dnsVerificationError.value = null;
    dnsVerified.value = false;

    try {
        const { data } = await axios.post(`/api/user/servers/${serverUuid.value}/proxy/verify-dns`, {
            domain: form.domain.trim(),
            port: form.port.trim(),
        });

        if (data.success && data.data) {
            dnsVerified.value = data.data.verified === true;
            targetIp.value = data.data.expected_ip || null;

            if (data.data.verified) {
                dnsVerificationError.value = null;
                toast.success(data.data.message || t('serverProxy.dnsVerificationSuccess'));
            } else {
                dnsVerificationError.value = data.data.message || t('serverProxy.dnsVerificationFailed');
            }
        } else {
            dnsVerified.value = false;
            dnsVerificationError.value = data.message || t('serverProxy.dnsVerificationFailed');
        }
    } catch (error) {
        console.error('DNS verification failed:', error);
        dnsVerified.value = false;
        dnsVerificationError.value = getAxiosErrorMessage(error, t('serverProxy.dnsVerificationFailed'));
    } finally {
        verifyingDns.value = false;
    }
}

function openCreateDrawer(): void {
    resetForm();
    // Set default port from allocations
    if (allocations.value.length > 0) {
        const primary = allocations.value.find((a) => a.is_primary);
        if (primary) {
            form.port = String(primary.port);
        } else {
            const firstAllocation = allocations.value[0];
            if (firstAllocation) {
                form.port = String(firstAllocation.port);
            }
        }
    }
    drawerOpen.value = true;
}

function closeDrawer(): void {
    drawerOpen.value = false;
}

function toggleSsl(): void {
    form.ssl = !form.ssl;
}

function toggleUseLetsEncrypt(): void {
    form.use_lets_encrypt = !form.use_lets_encrypt;
}

function getSslTypeLabel(useLetsEncrypt: boolean): string {
    return useLetsEncrypt ? t('serverProxy.useLetsEncrypt') : 'SSL';
}

function getUseLetsEncryptLabel(): string {
    return t('serverProxy.useLetsEncrypt');
}

function getUseLetsEncryptHelp(): string {
    return t('serverProxy.useLetsEncryptHelp');
}

function getYourDomainPlaceholder(): string {
    return t('serverProxy.yourDomain');
}

function validateForm(): boolean {
    let valid = true;
    errors.domain = '';
    errors.port = '';
    errors.client_email = '';
    errors.ssl_cert = '';
    errors.ssl_key = '';
    formError.value = null;

    // Validate domain - required and must be valid format
    const domainTrimmed = form.domain?.trim() || '';
    if (!domainTrimmed) {
        errors.domain = t('serverProxy.validation.domainRequired');
        valid = false;
    } else {
        // More comprehensive domain validation
        // Must have at least one dot (e.g., example.com)
        if (!domainTrimmed.includes('.')) {
            errors.domain = t('serverProxy.validation.domainInvalid');
            valid = false;
        } else {
            // Check domain length (max 253 chars total, each label max 63)
            if (domainTrimmed.length > 253) {
                errors.domain = t('serverProxy.validation.domainInvalid');
                valid = false;
            } else {
                // Validate domain format: letters, numbers, dots, hyphens
                // Must start and end with alphanumeric, labels separated by dots
                const domainRegex =
                    /^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+$/;
                if (!domainRegex.test(domainTrimmed)) {
                    errors.domain = t('serverProxy.validation.domainInvalid');
                    valid = false;
                } else {
                    // Check each label length (max 63 chars)
                    const labels = domainTrimmed.split('.');
                    for (const label of labels) {
                        if (label.length > 63 || label.length === 0) {
                            errors.domain = t('serverProxy.validation.domainInvalid');
                            valid = false;
                            break;
                        }
                    }
                }
            }
        }
    }

    // Validate port - required and must be valid
    const portTrimmed = form.port?.trim() || '';
    if (!portTrimmed) {
        errors.port = t('serverProxy.validation.portRequired');
        valid = false;
    } else {
        const portNum = parseInt(portTrimmed, 10);
        if (isNaN(portNum) || portNum < 1 || portNum > 65535) {
            errors.port = t('serverProxy.validation.portInvalid');
            valid = false;
        } else {
            // Ensure port belongs to an allocation
            const hasMatchingAllocation = allocations.value.some((a) => a.port === portNum);
            if (!hasMatchingAllocation) {
                errors.port = t('serverProxy.validation.portNotAllocated');
                valid = false;
            }
        }
    }

    // SSL validation
    if (form.ssl === true) {
        if (form.use_lets_encrypt === true) {
            // Let's Encrypt requires email
            const emailTrimmed = form.client_email?.trim() || '';
            if (!emailTrimmed) {
                errors.client_email = t('serverProxy.validation.emailRequired');
                valid = false;
            } else {
                // Validate email format
                const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
                if (!emailRegex.test(emailTrimmed)) {
                    errors.client_email = t('serverProxy.validation.emailInvalid');
                    valid = false;
                }
            }
        } else {
            // Custom SSL requires both cert and key
            const certTrimmed = form.ssl_cert?.trim() || '';
            const keyTrimmed = form.ssl_key?.trim() || '';
            if (!certTrimmed) {
                errors.ssl_cert = t('serverProxy.validation.sslCertRequired');
                valid = false;
            }
            if (!keyTrimmed) {
                errors.ssl_key = t('serverProxy.validation.sslKeyRequired');
                valid = false;
            }
        }
    }

    return valid;
}

function getErrorMessage(err: unknown): string {
    if (axios.isAxiosError(err)) {
        const responseData = err.response?.data;

        if (responseData) {
            // Try to extract the actual error message from nested structures
            if (responseData.error_message) {
                return responseData.error_message;
            }

            if (responseData.message) {
                // Check if message contains nested error info
                const message = responseData.message;

                // Try to extract error from Response: {...} pattern
                const responseMatch = message.match(/Response:\s*\{[^}]*"error":\s*"([^"]+)"/);
                if (responseMatch && responseMatch[1]) {
                    return responseMatch[1];
                }

                // Try to extract error from error field in nested structure
                if (message.includes('Failed to request certificate')) {
                    const certErrorMatch = message.match(/Failed to request certificate[^:]*:\s*(.+?)(?:\n|$)/);
                    if (certErrorMatch && certErrorMatch[1]) {
                        return certErrorMatch[1].trim();
                    }
                    return 'Failed to request certificate. Please check your domain DNS configuration and ensure port 80/443 is accessible.';
                }

                return message;
            }

            if (responseData.error) {
                return responseData.error;
            }

            // Check errors array
            if (Array.isArray(responseData.errors) && responseData.errors.length > 0) {
                const firstError = responseData.errors[0];
                if (firstError.detail) {
                    return firstError.detail;
                }
                if (firstError.message) {
                    return firstError.message;
                }
            }
        }

        return err.message || t('serverProxy.unknownError');
    }

    if (err instanceof Error) {
        return err.message;
    }

    return t('serverProxy.unknownError');
}

async function saveProxy(): Promise<void> {
    if (!serverUuid.value || !proxyEnabled.value || !validateForm()) {
        return;
    }

    // Require DNS verification for Let's Encrypt
    if (form.ssl && form.use_lets_encrypt && !dnsVerified.value) {
        toast.error(t('serverProxy.dnsVerificationRequiredError'));
        return;
    }

    saving.value = true;
    try {
        await axios.post(`/api/user/servers/${serverUuid.value}/proxy/create`, {
            domain: form.domain.trim(),
            port: form.port.trim(),
            ssl: form.ssl,
            use_lets_encrypt: form.use_lets_encrypt,
            client_email: form.use_lets_encrypt ? form.client_email.trim() : '',
            ssl_cert: form.use_lets_encrypt ? '' : form.ssl_cert.trim(),
            ssl_key: form.use_lets_encrypt ? '' : form.ssl_key.trim(),
        });

        toast.success(t('serverProxy.createSuccess'));

        closeDrawer();
        await fetchProxies();
    } catch (error) {
        console.error('Failed to create proxy:', error);
        formError.value = getErrorMessage(error);
        toast.error(formError.value);
    } finally {
        saving.value = false;
    }
}

async function fetchProxies(): Promise<void> {
    if (!serverUuid.value) return;

    loading.value = true;
    try {
        const { data } = await axios.get(`/api/user/servers/${serverUuid.value}/proxy`);

        if (!data.success) {
            toast.error(data.message || t('serverProxy.failedToFetch'));
            return;
        }

        proxies.value = data.data.proxies ?? [];
    } catch (error) {
        console.error('Failed to fetch proxies:', error);
        toast.error(getAxiosErrorMessage(error, t('serverProxy.failedToFetch')));
    } finally {
        loading.value = false;
    }
}

function refresh(): void {
    void fetchProxies();
}

async function deleteProxy(proxy: Proxy): Promise<void> {
    if (!serverUuid.value) return;

    deletingProxyId.value = proxy.id;
    try {
        await axios.post(`/api/user/servers/${serverUuid.value}/proxy/delete`, {
            id: proxy.id,
        });

        toast.success(t('serverProxy.deleteSuccess'));
        await fetchProxies();
    } catch (error) {
        console.error('Failed to delete proxy:', error);
        toast.error(getAxiosErrorMessage(error, t('serverProxy.deleteFailed')));
    } finally {
        deletingProxyId.value = null;
    }
}

function formatDate(value: string): string {
    const date = new Date(value);
    if (Number.isNaN(date.getTime())) {
        return value;
    }
    return new Intl.DateTimeFormat(undefined, {
        dateStyle: 'medium',
        timeStyle: 'short',
    }).format(date);
}

// Watch for domain/port changes to reset DNS verification
watch([() => form.domain, () => form.port], () => {
    if (form.ssl && form.use_lets_encrypt) {
        dnsVerified.value = false;
        dnsVerificationError.value = null;
        void calculateTargetIp();
    }
});

// Watch for SSL/Let's Encrypt changes
watch([() => form.ssl, () => form.use_lets_encrypt], () => {
    if (!form.ssl || !form.use_lets_encrypt) {
        dnsVerified.value = false;
        dnsVerificationError.value = null;
        targetIp.value = null;
    } else {
        void calculateTargetIp();
    }
});

onMounted(async () => {
    // Fetch plugin widgets
    await fetchPluginWidgets();

    // Fetch settings first to check if proxy is enabled
    await settingsStore.fetchSettings();

    // Only fetch data if the feature is enabled
    if (settingsStore.serverAllowUserMadeProxy) {
        await Promise.all([fetchServerAllocations(), fetchProxies()]);
    }
});
</script>

Than i get error ServerProxy.vue:817 SyntaxError: 10 (at message-compiler.mjs:77:23)
at ue (message-compiler.mjs:77:23)
at d (message-compiler.mjs:201:25)
at me (message-compiler.mjs:772:21)
at ve (message-compiler.mjs:786:20)
at Object.ye [as nextToken] (message-compiler.mjs:825:16)
at f (message-compiler.mjs:959:31)
at p (message-compiler.mjs:1059:36)
at h (message-compiler.mjs:1099:25)
at Object.g [as parse] (message-compiler.mjs:1114:21)
at Ie (message-compiler.mjs:1513:24)
Dr @ runtime-core.esm-bundler.js:275
Er @ runtime-core.esm-bundler.js:255
xa @ runtime-core.esm-bundler.js:4531
s @ runtime-core.esm-bundler.js:6112
run @ reactivity.esm-bundler.js:237
runIfDirty @ reactivity.esm-bundler.js:275
wr @ runtime-core.esm-bundler.js:199
Fr @ runtime-core.esm-bundler.js:408
Promise.then
jr @ runtime-core.esm-bundler.js:322
Ar @ runtime-core.esm-bundler.js:317
s.scheduler @ runtime-core.esm-bundler.js:4197
Sn.u.scheduler @ reactivity.esm-bundler.js:1900
trigger @ reactivity.esm-bundler.js:265
St @ reactivity.esm-bundler.js:323
Ft @ reactivity.esm-bundler.js:741
set @ reactivity.esm-bundler.js:1059
Tn @ ServerProxy.vue:817
wr @ runtime-core.esm-bundler.js:199
Tr @ runtime-core.esm-bundler.js:206
n @ runtime-dom.esm-bundler.js:730

When i build to production and i click the button for let's encrypt!

System Info

System:
    OS: Linux 6.17 Debian GNU/Linux 12 (bookworm) 12 (bookworm)
    CPU: (8) x64 AMD Ryzen 7 5700G with Radeon Graphics
    Memory: 19.40 GB / 24.00 GB
    Container: Yes
    Shell: 5.2.15 - /bin/bash
  Binaries:
    Node: 24.6.0 - /root/.nvm/versions/node/v24.6.0/bin/node
    Yarn: 1.22.22 - /root/.nvm/versions/node/v24.6.0/bin/yarn
    npm: 11.6.0 - /root/.nvm/versions/node/v24.6.0/bin/npm
    pnpm: 10.25.0 - /root/.local/share/pnpm/pnpm
    bun: 1.2.19 - /root/.bun/bin/bun
  npmPackages:
    @vitejs/plugin-vue: ^6.0.3 => 6.0.3 
    @vue/tsconfig: ^0.8.1 => 0.8.1 
    rolldown-vite:  7.2.11 
    vite-plugin-vue-devtools: ^8.0.5 => 8.0.5 
    vue: ^3.5.25 => 3.5.25 
    vue-animejs: ^2.1.1 => 2.1.1 
    vue-chartjs: ^5.3.3 => 5.3.3 
    vue-eslint-parser: ^10.2.0 => 10.2.0 
    vue-i18n: ^11.2.2 => 11.2.2 
    vue-qrcode: ^2.2.2 => 2.2.2 
    vue-router: ^4.6.4 => 4.6.4 
    vue-toastification: 2.0.0-rc.5 => 2.0.0-rc.5 
    vue-tsc: ^3.1.8 => 3.1.8 
    vue-turnstile: ^1.0.11 => 1.0.11 
    vue3-ace-editor: ^2.2.4 => 2.2.4 
    vuedraggable: ^4.1.0 => 4.1.0

Screenshot

Image

Additional context

No

Validations

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions