Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 11 additions & 5 deletions app/error.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
<script setup lang="ts">
defineProps<{
error: {
statusCode: number;
statusMessage?: string;
message?: string;
};
}>();
</script>

<template>
<UError :error="{
statusCode: 404,
statusMessage: 'Page not found',
message: 'The page you are looking for does not exist.'
}" />
<UError :error="error" />
</template>
2 changes: 1 addition & 1 deletion app/pages/auth/forgot-password.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<script setup lang="ts">
import * as z from 'zod';
import type { FormSubmitEvent, AuthFormField, FormError } from '@nuxt/ui'
import type { FormSubmitEvent, AuthFormField } from '@nuxt/ui'

definePageMeta({
layout: 'auth'
Expand Down
18 changes: 17 additions & 1 deletion app/pages/auth/login.vue
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,23 @@ useSeoMeta({
});

const route = useRoute()
const redirectUrl = route.query.url || '/';

// Validate redirect URL to prevent open redirect attacks
const getValidRedirectUrl = (url: unknown): string => {
if (typeof url !== 'string' || !url) {
return '/';
}
// Remove any newlines or control characters to prevent header injection
const sanitized = url.replace(/[\r\n\t]/g, '');
// Only allow relative paths starting with /
// Prevent protocol-relative URLs (//evil.com) and absolute URLs
if (sanitized.startsWith('/') && !sanitized.startsWith('//')) {
return sanitized;
}
return '/';
};

const redirectUrl = getValidRedirectUrl(route.query.url);

const toast = useToast();

Expand Down
149 changes: 148 additions & 1 deletion app/pages/domains/[id]/danger-zone.vue
Original file line number Diff line number Diff line change
@@ -1,3 +1,150 @@
<script setup lang="ts">
import { inject, ref, type ComputedRef } from 'vue'
import type { GetDomainsDomainIdResponse } from '~/api-client'
import { getFullDomain } from '~/composables/getFullDomain'
import { DomainStore } from '~/utils/stores/domainStore'

type Domain = GetDomainsDomainIdResponse['data']

const domain = inject<Domain>('domain')
const domainId = inject<string>('domainId')
const isNewDomain = inject<ComputedRef<boolean>>('isNewDomain')

if (!domain || !domainId || !isNewDomain) {
throw new Error('Domain context is missing.')
}

// Redirect to domain list if this is a new domain
if (isNewDomain.value) {
navigateTo('/domains')
}

const router = useRouter()
const toast = useToast()

const deleteOpen = ref(false)
const deletingDomain = ref(false)
const confirmationText = ref('')

const fullDomain = computed(() => getFullDomain(domain.subdomain))

const canDelete = computed(() => confirmationText.value === domain.subdomain)

async function handleDeleteDomain() {
if (!canDelete.value || deletingDomain.value) {
return
}

deletingDomain.value = true
try {
const result = await useAPI().deleteDomainsDomainId({
path: { domainID: domainId },
ignoreResponseError: true
})

if (result.success) {
toast.add({
title: 'Domain deleted',
description: `${fullDomain.value} has been permanently deleted.`,
icon: 'i-lucide-check',
color: 'success'
})

await DomainStore.fetchAndSet()
router.push('/domains')
} else {
toast.add({
title: 'Unable to delete domain',
description: result.message || 'Please try again later.',
icon: 'i-lucide-alert-triangle',
color: 'error'
})
}
} catch (error) {
console.error(error)
toast.add({
title: 'Unexpected error',
description: 'Something went wrong while deleting the domain.',
icon: 'i-lucide-bug',
color: 'error'
})
} finally {
deletingDomain.value = false
deleteOpen.value = false
confirmationText.value = ''
}
}
</script>

<template>

<UCard class="border-error/30 bg-error/5">
<template #header>
<div class="flex items-center gap-2">
<UIcon name="i-lucide-alert-triangle" class="text-error h-6 w-6" />
<div>
<p class="text-lg font-semibold text-error">Danger Zone</p>
<p class="text-sm text-default-500">Irreversible and destructive actions</p>
</div>
</div>
</template>

<div class="space-y-4">
<div class="rounded-lg border border-error/30 p-4">
<div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div>
<p class="font-medium">Delete this domain</p>
<p class="text-sm text-default-500">
Once deleted, all DNS records and DDNS configurations will be permanently removed.
</p>
</div>
<UModal v-model:open="deleteOpen">
<UButton label="Delete domain" color="error" variant="soft" />

<template #content>
<UCard>
<template #header>
<div class="flex items-center gap-2">
<UIcon name="i-lucide-alert-triangle" class="text-error h-5 w-5" />
<h3 class="text-lg font-semibold">Delete Domain</h3>
</div>
</template>

<div class="space-y-4">
<p class="text-sm text-default-600">
Are you sure you want to delete <strong>{{ fullDomain }}</strong>?
This action is permanent and cannot be undone.
</p>

<div>
<label class="text-sm text-default-600">
Type <strong>{{ domain.subdomain }}</strong> to confirm:
</label>
<UInput v-model="confirmationText" class="mt-2" placeholder="Enter subdomain" />
</div>
</div>

<template #footer>
<div class="flex justify-end gap-2">
<UButton
label="Cancel"
color="neutral"
variant="ghost"
@click="deleteOpen = false; confirmationText = ''"
/>
<UButton
label="Delete permanently"
color="error"
:disabled="!canDelete"
:loading="deletingDomain"
@click="handleDeleteDomain"
/>
</div>
</template>
</UCard>
</template>
</UModal>
</div>
</div>
</div>
</UCard>
</template>
2 changes: 0 additions & 2 deletions app/pages/settings/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ import * as z from 'zod'
import type { FormSubmitEvent } from '@nuxt/ui'
import { UserStore } from '../../utils/stores/userStore';

const fileRef = ref<HTMLInputElement>()

const profileSchema = z.object({
username: z.string().trim()
.min(5, 'Must be at least 5 characters')
Expand Down
75 changes: 74 additions & 1 deletion app/pages/settings/security.vue
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,49 @@ async function onSubmit(event: FormSubmitEvent<PasswordSchema>) {
}
}

const deleteAccountOpen = ref(false);
const deletingAccount = ref(false);

async function handleDeleteAccount() {
deletingAccount.value = true;
try {
const result = await useAPI().deleteAccount({
ignoreResponseError: true
});

if (result.success) {
UserStore.clear();
useCookie("session_token").value = null;

toast.add({
title: 'Account deleted',
description: 'Your account has been permanently deleted.',
icon: 'i-lucide-check',
color: 'success'
});

navigateTo('/auth/login');
} else {
toast.add({
title: 'Error',
description: result.message || 'An error occurred while deleting your account.',
icon: 'i-lucide-alert-circle',
color: 'error'
});
}
} catch (error) {
toast.add({
title: 'Error',
description: 'An unexpected error occurred.',
icon: 'i-lucide-alert-circle',
color: 'error'
});
} finally {
deletingAccount.value = false;
deleteAccountOpen.value = false;
}
}

</script>

<template>
Expand All @@ -103,7 +146,37 @@ async function onSubmit(event: FormSubmitEvent<PasswordSchema>) {
description="No longer want to use our service? You can delete your account here. This action is not reversible. All information related to this account will be deleted permanently."
class="from-error/10 from-5% to-default">
<template #footer>
<UButton label="Delete account" color="error" />
<UModal v-model:open="deleteAccountOpen">
<UButton label="Delete account" color="error" />

<template #content>
<UCard>
<template #header>
<div class="flex items-center gap-2">
<UIcon name="i-lucide-alert-triangle" class="text-error h-5 w-5" />
<h3 class="text-lg font-semibold">Delete Account</h3>
</div>
</template>

<p class="text-sm text-default-600">
Are you sure you want to delete your account? This action is permanent and cannot be undone.
All your domains, DNS records, and settings will be permanently deleted.
</p>

<template #footer>
<div class="flex justify-end gap-2">
<UButton label="Cancel" color="neutral" variant="ghost" @click="deleteAccountOpen = false" />
<UButton
label="Delete permanently"
color="error"
:loading="deletingAccount"
@click="handleDeleteAccount"
/>
</div>
</template>
</UCard>
</template>
</UModal>
</template>
</UPageCard>
</template>
Loading