Skip to content
Open
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
17 changes: 17 additions & 0 deletions assets/vue/components/Breadcrumb.vue
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,24 @@ function resolveSettingsSectionLabel(nsRaw) {
watchEffect(() => {
if ("/" === route.fullPath) return
itemList.value = []
// special-case accessurl routes
if (/^\/resources\/accessurl\/[^\/]+\/delete(?:\/|$)/.test(route.path)) {
itemList.value = []

itemList.value.push({
label: t("Administration"),
url: "/main/admin/index.php",
})

itemList.value.push({
label: t("Multiple access URL / Branding"),
url: "/main/admin/access_urls.php",
})

itemList.value.push({ label: t("Delete access") })

return
}
if (buildManualBreadcrumbIfNeeded()) return

// Static route categories
Expand Down
6 changes: 6 additions & 0 deletions assets/vue/router/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,12 @@ const router = createRouter({
showBreadcrumb: false,
},
},
{
path: '/resources/accessurl/:id/delete',
name: 'AccessUrlDelete',
component: () => import('../views/accessurl/DeleteAccessUrl.vue'),
props: route => ({ id: Number(route.params.id) })
},
{
path: "/home",
name: "Home",
Expand Down
45 changes: 45 additions & 0 deletions assets/vue/services/accessurlService.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,51 @@ export async function findUserActivePortals(userIri) {
return items
}

async function getUrl(id) {
const apiUrl = `/api/access_urls/${encodeURIComponent(id)}`;
try {
const resp = await fetch(apiUrl, {
credentials: "same-origin",
headers: { Accept: "application/json" },
});
if (resp.ok) {
const contentType = resp.headers.get("content-type") || "";
return contentType.includes("application/json") ? resp.json() : { url: await resp.text() };
}
} catch (err) {
}
}

async function deleteAccessUrl(id, confirmValue, secToken = "") {
const url = `/api/access_urls/${encodeURIComponent(id)}`
const resp = await fetch(url, {
method: "DELETE",
credentials: "same-origin",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
...(secToken ? { "X-CSRF-Token": secToken } : {}),
},
body: JSON.stringify({ confirm_value: String(confirmValue) }),
})

if (!resp.ok) {
const txt = await resp.text()
throw new Error(txt || "Delete failed")
}

const contentType = resp.headers.get("content-type") || ""
if (contentType.includes("application/json")) {
return resp.json()
}
return { message: await resp.text(), redirectUrl: "/main/admin/access_urls.php" }
}

export default {
deleteAccessUrl,
getUrl,
}

export async function findAll() {
const { items } = await baseService.getCollection("/api/access_urls")

Expand Down
104 changes: 104 additions & 0 deletions assets/vue/views/accessurl/DeleteAccessUrl.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
<template>
<div class="space-y-6">
<Message severity="warn" class="inline-block max-w-max">{{ t("Danger zone: deleting an access URL is permanent.")}}</Message>
<section class="p-0">
<h3 class="mb-2 text-sm font-semibold text-rose-900">{{ t("Confirm deletion") }}</h3>
<p class="mb-3 text-sm text-rose-800">
{{ t("Type the full URL to confirm. The URL and its relations will be permanently removed.") }}
</p>
<div class="grid grid-cols-1 gap-4 md:grid-cols-3 items-center">
<div class="md:col-span-2">
<label class="mb-1 block text-xs font-medium text-rose-900">{{ t("URL") }}</label>
<BaseInputText
v-model="confirmText"
class="w-full"
:placeholder="urlText || 'https://example.com'"
/>
<p v-if="confirmText && !canDelete" class="mt-1 text-xs text-rose-700">
{{ t("The value must match exactly:") }} <strong>{{ urlText }}</strong>
</p>
</div>
<div class="flex items-center md:justify-end md:col-span-1 md:pr-8">
<div class="hidden md:block">
<BaseButton
:label="t('Delete URL')"
icon="delete"
type="danger"
:disabled="loading || !canDelete"
:isLoading="loading"
@click="openConfirmDialog"
/>
</div>
</div>
</div>
</section>
<BaseDialogConfirmCancel
v-model:isVisible="confirmDialogVisible"
:title="t('Confirm deletion')"
:confirmLabel="t('Delete')"
:cancelLabel="t('Cancel')"
@confirmClicked="confirmSubmit"
/>
</div>
</template>

<script setup >
import { ref, computed, onMounted } from "vue"
import { useI18n } from "vue-i18n"
import { useRoute, useRouter } from "vue-router"
import svc from "../../services/accessurlService"
import Message from "primevue/message"

import BaseInputText from "../../components/basecomponents/BaseInputText.vue"
import BaseButton from "../../components/basecomponents/BaseButton.vue"
import BaseDialogConfirmCancel from "../../components/basecomponents/BaseDialogConfirmCancel.vue"

const { t } = useI18n()
const route = useRoute()
const router = useRouter()
const urlId = Number(route.params.id || 0)
const urlText = ref(route.query.url || "")
const confirmText = ref("")
const loading = ref(false)
const error = ref("")
const notice = ref("")
const confirmDialogVisible = ref(false)

const canDelete = computed(() => !!confirmText.value && confirmText.value === urlText.value)

onMounted(async () => {
if (!urlText.value && urlId) {
try {
const data = await svc.getUrl(urlId)
urlText.value = data.url || ""
} catch (e) {
error.value = t("Unable to load URL data.")
}
}
})

function openConfirmDialog() {
confirmDialogVisible.value = true
}

async function confirmSubmit() {
confirmDialogVisible.value = false
error.value = ""
notice.value = ""
try {
loading.value = true
const secToken = (window.SEC_TOKEN || route.query.sec_token || "")
const res = await svc.deleteAccessUrl(urlId, confirmText.value, secToken)
notice.value = res.message || t("URL deleted successfully.")
if (res.redirectUrl) {
window.location.href = res.redirectUrl
} else {
router.push({ name: "AccessUrlsList" })
}
} catch (e) {
error.value = e?.message || (e?.response?.data?.error) || t("Failed to delete URL.")
} finally {
loading.value = false
}
}
</script>
9 changes: 6 additions & 3 deletions public/main/admin/access_urls.php
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@
}

$parameters['sec_token'] = Security::get_token();

echo '<script>window.SEC_TOKEN = ' . json_encode($parameters['sec_token']) . ';</script>';
// Checking if the admin is registered in all sites
if (!api_is_admin_in_all_active_urls()) {
// Get the list of unregistered urls
Expand Down Expand Up @@ -213,8 +213,11 @@
);

if ($u->getId() !== 1) {
$rowActions .= '<a href="access_urls.php?action=delete_url&url_id=' . $u->getId() . '" ' .
'onclick="return confirm(\'' . addslashes(get_lang('Please confirm your choice')) . '\');">' .
// build a link to the Vue route that will open DeleteAccessUrl.vue
$urlEncoded = rawurlencode($u->getUrl());
$secTokenEncoded = rawurlencode($parameters['sec_token']);
$vueHref = api_get_path(WEB_PATH) . 'resources/accessurl/' . $u->getId() . '/delete?url=' . $urlEncoded . '&sec_token=' . $secTokenEncoded;
$rowActions .= '<a href="' . $vueHref . '">' .
Display::getMdiIcon('delete', 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Delete')) .
'</a>';
}
Expand Down
Loading