Files
BetiX/resources/js/pages/Admin/GeoBlock.vue
Dolo 0280278978
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (8.4) (push) Has been cancelled
tests / ci (8.5) (push) Has been cancelled
Initialer Laravel Commit für BetiX
2026-04-04 18:01:50 +02:00

441 lines
24 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { Head, useForm } from '@inertiajs/vue3';
import { onMounted, nextTick, ref, computed } from 'vue';
import CasinoAdminLayout from '@/layouts/admin/CasinoAdminLayout.vue';
// Full country list with ISO codes and German names
const COUNTRIES: { code: string; name: string }[] = [
{ code: 'AF', name: 'Afghanistan' }, { code: 'AL', name: 'Albanien' }, { code: 'DZ', name: 'Algerien' },
{ code: 'AD', name: 'Andorra' }, { code: 'AO', name: 'Angola' }, { code: 'AR', name: 'Argentinien' },
{ code: 'AM', name: 'Armenien' }, { code: 'AU', name: 'Australien' }, { code: 'AT', name: 'Österreich' },
{ code: 'AZ', name: 'Aserbaidschan' }, { code: 'BS', name: 'Bahamas' }, { code: 'BH', name: 'Bahrain' },
{ code: 'BD', name: 'Bangladesch' }, { code: 'BY', name: 'Belarus' }, { code: 'BE', name: 'Belgien' },
{ code: 'BZ', name: 'Belize' }, { code: 'BO', name: 'Bolivien' }, { code: 'BA', name: 'Bosnien' },
{ code: 'BR', name: 'Brasilien' }, { code: 'BN', name: 'Brunei' }, { code: 'BG', name: 'Bulgarien' },
{ code: 'KH', name: 'Kambodscha' }, { code: 'CA', name: 'Kanada' }, { code: 'CL', name: 'Chile' },
{ code: 'CN', name: 'China' }, { code: 'CO', name: 'Kolumbien' }, { code: 'HR', name: 'Kroatien' },
{ code: 'CU', name: 'Kuba' }, { code: 'CY', name: 'Zypern' }, { code: 'CZ', name: 'Tschechien' },
{ code: 'DK', name: 'Dänemark' }, { code: 'EG', name: 'Ägypten' }, { code: 'EE', name: 'Estland' },
{ code: 'ET', name: 'Äthiopien' }, { code: 'FI', name: 'Finnland' }, { code: 'FR', name: 'Frankreich' },
{ code: 'GE', name: 'Georgien' }, { code: 'DE', name: 'Deutschland' }, { code: 'GH', name: 'Ghana' },
{ code: 'GR', name: 'Griechenland' }, { code: 'GT', name: 'Guatemala' }, { code: 'HU', name: 'Ungarn' },
{ code: 'IN', name: 'Indien' }, { code: 'ID', name: 'Indonesien' }, { code: 'IR', name: 'Iran' },
{ code: 'IQ', name: 'Irak' }, { code: 'IE', name: 'Irland' }, { code: 'IL', name: 'Israel' },
{ code: 'IT', name: 'Italien' }, { code: 'JP', name: 'Japan' }, { code: 'JO', name: 'Jordanien' },
{ code: 'KZ', name: 'Kasachstan' }, { code: 'KE', name: 'Kenia' }, { code: 'KW', name: 'Kuwait' },
{ code: 'LV', name: 'Lettland' }, { code: 'LB', name: 'Libanon' }, { code: 'LY', name: 'Libyen' },
{ code: 'LT', name: 'Litauen' }, { code: 'LU', name: 'Luxemburg' }, { code: 'MY', name: 'Malaysia' },
{ code: 'MT', name: 'Malta' }, { code: 'MX', name: 'Mexiko' }, { code: 'MD', name: 'Moldawien' },
{ code: 'MC', name: 'Monaco' }, { code: 'ME', name: 'Montenegro' }, { code: 'MA', name: 'Marokko' },
{ code: 'NL', name: 'Niederlande' }, { code: 'NZ', name: 'Neuseeland' }, { code: 'NG', name: 'Nigeria' },
{ code: 'NO', name: 'Norwegen' }, { code: 'OM', name: 'Oman' }, { code: 'PK', name: 'Pakistan' },
{ code: 'PE', name: 'Peru' }, { code: 'PH', name: 'Philippinen' }, { code: 'PL', name: 'Polen' },
{ code: 'PT', name: 'Portugal' }, { code: 'QA', name: 'Katar' }, { code: 'RO', name: 'Rumänien' },
{ code: 'RU', name: 'Russland' }, { code: 'SA', name: 'Saudi-Arabien' }, { code: 'RS', name: 'Serbien' },
{ code: 'SG', name: 'Singapur' }, { code: 'SK', name: 'Slowakei' }, { code: 'SI', name: 'Slowenien' },
{ code: 'ZA', name: 'Südafrika' }, { code: 'KR', name: 'Südkorea' }, { code: 'ES', name: 'Spanien' },
{ code: 'LK', name: 'Sri Lanka' }, { code: 'SE', name: 'Schweden' }, { code: 'CH', name: 'Schweiz' },
{ code: 'SY', name: 'Syrien' }, { code: 'TW', name: 'Taiwan' }, { code: 'TH', name: 'Thailand' },
{ code: 'TN', name: 'Tunesien' }, { code: 'TR', name: 'Türkei' }, { code: 'UA', name: 'Ukraine' },
{ code: 'AE', name: 'Vereinigte Arabische Emirate' }, { code: 'GB', name: 'Vereinigtes Königreich' },
{ code: 'US', name: 'USA' }, { code: 'UY', name: 'Uruguay' }, { code: 'UZ', name: 'Usbekistan' },
{ code: 'VE', name: 'Venezuela' }, { code: 'VN', name: 'Vietnam' }, { code: 'YE', name: 'Jemen' },
{ code: 'ZW', name: 'Simbabwe' },
];
const countryMap: Record<string, string> = Object.fromEntries(COUNTRIES.map(c => [c.code, c.name]));
function getCountryName(code: string): string { return countryMap[code] ?? code; }
const props = defineProps<{
settings: {
enabled: boolean;
mode: 'blacklist' | 'whitelist';
blocked_countries: string[];
allowed_countries: string[];
vpn_block: boolean;
vpn_provider: 'none' | 'ipqualityscore' | 'proxycheck';
vpn_api_key: string;
block_message: string;
redirect_url: string;
};
}>();
const form = useForm({
enabled: props.settings.enabled ?? false,
mode: props.settings.mode ?? 'blacklist',
blocked_countries: [...(props.settings.blocked_countries ?? [])],
allowed_countries: [...(props.settings.allowed_countries ?? [])],
vpn_block: props.settings.vpn_block ?? false,
vpn_provider: props.settings.vpn_provider ?? 'none',
vpn_api_key: props.settings.vpn_api_key ?? '',
block_message: props.settings.block_message ?? 'This service is not available in your region.',
redirect_url: props.settings.redirect_url ?? '',
});
const newBlocked = ref('');
const newAllowed = ref('');
const blockedSearch = ref('');
const allowedSearch = ref('');
const filteredBlockedCountries = computed(() => {
const q = blockedSearch.value.toLowerCase();
return COUNTRIES.filter(c =>
!form.blocked_countries.includes(c.code) &&
(c.name.toLowerCase().includes(q) || c.code.toLowerCase().includes(q))
);
});
const filteredAllowedCountries = computed(() => {
const q = allowedSearch.value.toLowerCase();
return COUNTRIES.filter(c =>
!form.allowed_countries.includes(c.code) &&
(c.name.toLowerCase().includes(q) || c.code.toLowerCase().includes(q))
);
});
function addCountry(list: 'blocked' | 'allowed') {
const code = (list === 'blocked' ? newBlocked.value : newAllowed.value).trim().toUpperCase();
if (!code || code.length !== 2) return;
const arr = list === 'blocked' ? form.blocked_countries : form.allowed_countries;
if (!arr.includes(code)) arr.push(code);
if (list === 'blocked') { newBlocked.value = ''; blockedSearch.value = ''; }
else { newAllowed.value = ''; allowedSearch.value = ''; }
}
function addCountryFromSelect(list: 'blocked' | 'allowed', code: string) {
if (!code) return;
const arr = list === 'blocked' ? form.blocked_countries : form.allowed_countries;
if (!arr.includes(code)) arr.push(code);
if (list === 'blocked') { newBlocked.value = ''; blockedSearch.value = ''; }
else { newAllowed.value = ''; allowedSearch.value = ''; }
}
function removeCountry(list: 'blocked' | 'allowed', code: string) {
if (list === 'blocked') {
form.blocked_countries = form.blocked_countries.filter(c => c !== code);
} else {
form.allowed_countries = form.allowed_countries.filter(c => c !== code);
}
}
function submit() {
form.post('/admin/settings/geo', { preserveScroll: true });
}
onMounted(() => {
nextTick(() => { if ((window as any).lucide) (window as any).lucide.createIcons(); });
});
</script>
<template>
<CasinoAdminLayout>
<Head title="Admin VPN & GeoBlock" />
<template #title>VPN & GeoBlock</template>
<div class="page-wrap">
<!-- Flash -->
<div v-if="($page.props as any).flash?.success" class="alert-success">
<i data-lucide="check-circle"></i>
{{ ($page.props as any).flash.success }}
</div>
<div class="card">
<div class="card-head">
<div class="card-title-group">
<h2>GeoBlock & VPN-Schutz</h2>
<p class="card-subtitle">Steuere den Zugang nach Land und blockiere VPN-Nutzer.</p>
</div>
<button class="btn-primary" @click="submit" :disabled="form.processing">
<i data-lucide="save"></i>
Speichern
</button>
</div>
<!-- Master Toggle -->
<div class="section">
<div class="toggle-row">
<div>
<div class="toggle-label">GeoBlock aktivieren</div>
<div class="toggle-desc">Wenn deaktiviert, haben alle Länder Zugang.</div>
</div>
<button class="toggle-btn" :class="{ active: form.enabled }" @click="form.enabled = !form.enabled">
<span class="toggle-knob"></span>
</button>
</div>
</div>
<div :class="{ 'dimmed': !form.enabled }">
<!-- Mode -->
<div class="section">
<label class="field-label">Modus</label>
<div class="mode-grid">
<label class="mode-card" :class="{ selected: form.mode === 'blacklist' }">
<input type="radio" v-model="form.mode" value="blacklist" class="hidden">
<i data-lucide="shield-x"></i>
<div>
<div class="mode-name">Blacklist</div>
<div class="mode-desc">Alle Länder erlaubt, <b>außer</b> den gesperrten.</div>
</div>
</label>
<label class="mode-card" :class="{ selected: form.mode === 'whitelist' }">
<input type="radio" v-model="form.mode" value="whitelist" class="hidden">
<i data-lucide="shield-check"></i>
<div>
<div class="mode-name">Whitelist</div>
<div class="mode-desc">Nur <b>erlaubte</b> Länder haben Zugang.</div>
</div>
</label>
</div>
</div>
<!-- Blocked Countries -->
<div class="section" v-if="form.mode === 'blacklist'">
<label class="field-label">Gesperrte Länder</label>
<div class="country-picker-row">
<div class="country-search-wrap">
<input type="text" v-model="blockedSearch" placeholder="Land suchen..." class="field-input country-search-input">
<div class="country-dropdown" v-if="blockedSearch.length >= 1">
<button
v-for="c in filteredBlockedCountries.slice(0, 8)"
:key="c.code"
class="country-option"
@click.prevent="addCountryFromSelect('blocked', c.code)"
>
<img :src="`https://flagcdn.com/20x15/${c.code.toLowerCase()}.png`" :alt="c.code" class="option-flag">
<span class="option-name">{{ c.name }}</span>
<span class="option-code">{{ c.code }}</span>
</button>
<p v-if="filteredBlockedCountries.length === 0" class="no-results">Kein Land gefunden.</p>
</div>
</div>
</div>
<div class="tags" v-if="form.blocked_countries.length">
<span v-for="c in form.blocked_countries" :key="c" class="tag tag-red">
<img :src="`https://flagcdn.com/16x12/${c.toLowerCase()}.png`" :alt="c" class="flag-img" @error="(e:any)=>e.target.style.display='none'">
<span class="tag-name">{{ getCountryName(c) }}</span>
<span class="tag-code">{{ c }}</span>
<button @click="removeCountry('blocked', c)"><i data-lucide="x"></i></button>
</span>
</div>
<p class="field-hint" v-else>Noch keine Länder gesperrt.</p>
</div>
<!-- Allowed Countries -->
<div class="section" v-if="form.mode === 'whitelist'">
<label class="field-label">Erlaubte Länder</label>
<div class="country-picker-row">
<div class="country-search-wrap">
<input type="text" v-model="allowedSearch" placeholder="Land suchen..." class="field-input country-search-input">
<div class="country-dropdown" v-if="allowedSearch.length >= 1">
<button
v-for="c in filteredAllowedCountries.slice(0, 8)"
:key="c.code"
class="country-option"
@click.prevent="addCountryFromSelect('allowed', c.code)"
>
<img :src="`https://flagcdn.com/20x15/${c.code.toLowerCase()}.png`" :alt="c.code" class="option-flag">
<span class="option-name">{{ c.name }}</span>
<span class="option-code">{{ c.code }}</span>
</button>
<p v-if="filteredAllowedCountries.length === 0" class="no-results">Kein Land gefunden.</p>
</div>
</div>
</div>
<div class="tags" v-if="form.allowed_countries.length">
<span v-for="c in form.allowed_countries" :key="c" class="tag tag-green">
<img :src="`https://flagcdn.com/16x12/${c.toLowerCase()}.png`" :alt="c" class="flag-img" @error="(e:any)=>e.target.style.display='none'">
<span class="tag-name">{{ getCountryName(c) }}</span>
<span class="tag-code">{{ c }}</span>
<button @click="removeCountry('allowed', c)"><i data-lucide="x"></i></button>
</span>
</div>
<p class="field-hint" v-else>Noch keine Länder erlaubt.</p>
</div>
<div class="divider"></div>
<!-- VPN Block -->
<div class="section">
<div class="toggle-row">
<div>
<div class="toggle-label">VPN/Proxy blockieren</div>
<div class="toggle-desc">Nutzer mit VPN oder Proxy werden gesperrt.</div>
</div>
<button class="toggle-btn" :class="{ active: form.vpn_block }" @click="form.vpn_block = !form.vpn_block">
<span class="toggle-knob"></span>
</button>
</div>
<div v-if="form.vpn_block" class="vpn-settings">
<div class="form-row">
<div class="form-col">
<label class="field-label">VPN-Anbieter</label>
<select v-model="form.vpn_provider" class="field-input">
<option value="none">Kein (deaktiviert)</option>
<option value="ipqualityscore">IPQualityScore</option>
<option value="proxycheck">ProxyCheck.io</option>
</select>
</div>
<div class="form-col" v-if="form.vpn_provider !== 'none'">
<label class="field-label">API-Schlüssel</label>
<input type="text" v-model="form.vpn_api_key" placeholder="API Key..." class="field-input">
</div>
</div>
</div>
</div>
<div class="divider"></div>
<!-- Block Message & Redirect -->
<div class="section">
<div class="form-row">
<div class="form-col full">
<label class="field-label">Sperr-Nachricht</label>
<textarea v-model="form.block_message" rows="3" class="field-input" placeholder="Diese Nachricht wird gesperrten Nutzern angezeigt."></textarea>
</div>
<div class="form-col full">
<label class="field-label">Weiterleitungs-URL <span class="optional">(optional)</span></label>
<input type="url" v-model="form.redirect_url" placeholder="https://example.com/gesperrt" class="field-input">
<p class="field-hint">Wenn angegeben, werden gesperrte Nutzer dorthin weitergeleitet statt eine Fehlermeldung zu sehen.</p>
</div>
</div>
</div>
</div>
<div class="card-foot">
<button class="btn-primary" @click="submit" :disabled="form.processing">
<i data-lucide="save"></i>
{{ form.processing ? 'Wird gespeichert...' : 'Einstellungen speichern' }}
</button>
</div>
</div>
</div>
</CasinoAdminLayout>
</template>
<style scoped>
.page-wrap { max-width: 860px; margin: 0 auto; display: flex; flex-direction: column; gap: 20px; }
.alert-success {
background: rgba(0, 200, 100, 0.1); border: 1px solid rgba(0, 200, 100, 0.3);
color: #00c864; padding: 12px 16px; border-radius: 10px;
display: flex; align-items: center; gap: 10px; font-weight: 600;
}
.alert-success i { width: 18px; height: 18px; flex-shrink: 0; }
.card { background: #0f0f10; border: 1px solid #18181b; border-radius: 14px; overflow: hidden; }
.card-head {
display: flex; align-items: flex-start; justify-content: space-between; gap: 16px;
padding: 24px; border-bottom: 1px solid #18181b;
}
.card-title-group h2 { font-size: 20px; font-weight: 700; color: #fff; margin: 0 0 4px; }
.card-subtitle { color: #71717a; font-size: 13px; margin: 0; }
.card-foot { padding: 20px 24px; border-top: 1px solid #18181b; display: flex; justify-content: flex-end; }
.section { padding: 20px 24px; }
.divider { height: 1px; background: #18181b; margin: 0; }
.dimmed { opacity: 0.4; pointer-events: none; }
/* Toggle */
.toggle-row { display: flex; align-items: center; justify-content: space-between; gap: 20px; }
.toggle-label { font-weight: 600; color: #e4e4e7; font-size: 14px; }
.toggle-desc { color: #71717a; font-size: 12px; margin-top: 2px; }
.toggle-btn {
width: 48px; height: 26px; border-radius: 99px; background: #27272a; border: none; cursor: pointer;
position: relative; transition: background 0.2s; flex-shrink: 0;
}
.toggle-btn.active { background: #df006a; }
.toggle-knob {
position: absolute; top: 3px; left: 3px; width: 20px; height: 20px;
background: #fff; border-radius: 50%; transition: transform 0.2s;
}
.toggle-btn.active .toggle-knob { transform: translateX(22px); }
/* Mode cards */
.mode-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-top: 10px; }
.mode-card {
display: flex; align-items: flex-start; gap: 14px; padding: 16px;
background: #18181b; border: 2px solid #27272a; border-radius: 10px;
cursor: pointer; transition: all 0.15s;
}
.mode-card:hover { border-color: #3f3f46; }
.mode-card.selected { border-color: #df006a; background: rgba(223,0,106,0.06); }
.mode-card i { width: 20px; height: 20px; color: #71717a; flex-shrink: 0; margin-top: 2px; }
.mode-card.selected i { color: #df006a; }
.mode-name { font-weight: 700; font-size: 14px; color: #e4e4e7; }
.mode-desc { font-size: 12px; color: #71717a; margin-top: 2px; }
/* Fields */
.field-label { display: block; font-size: 12px; font-weight: 700; color: #a1a1aa; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 8px; }
.field-input {
width: 100%; background: #18181b; border: 1px solid #27272a; border-radius: 8px;
color: #e4e4e7; padding: 10px 14px; font-size: 13px; transition: border-color 0.15s;
font-family: inherit;
}
.field-input:focus { outline: none; border-color: #df006a; }
.code-input { width: 100px; text-transform: uppercase; letter-spacing: 2px; font-weight: 700; }
.field-hint { font-size: 11px; color: #52525b; margin-top: 6px; }
.optional { font-weight: 400; color: #52525b; text-transform: none; letter-spacing: 0; }
/* Country picker */
.country-picker-row { margin-bottom: 12px; }
.country-search-wrap { position: relative; }
.country-search-input { width: 100%; }
.country-dropdown {
position: absolute; top: calc(100% + 4px); left: 0; right: 0; z-index: 100;
background: #18181b; border: 1px solid #3f3f46; border-radius: 10px;
overflow: hidden; box-shadow: 0 8px 24px rgba(0,0,0,0.5);
}
.country-option {
display: flex; align-items: center; gap: 10px; width: 100%;
padding: 9px 14px; background: transparent; border: none;
color: #e4e4e7; cursor: pointer; text-align: left; transition: background 0.1s;
}
.country-option:hover { background: rgba(223,0,106,0.1); }
.option-flag { width: 20px; height: 15px; object-fit: cover; border-radius: 2px; flex-shrink: 0; }
.option-name { flex: 1; font-size: 13px; }
.option-code { font-size: 11px; color: #71717a; font-weight: 700; letter-spacing: 1px; }
.no-results { color: #52525b; font-size: 12px; padding: 10px 14px; margin: 0; }
.tags { display: flex; flex-wrap: wrap; gap: 8px; }
.tag {
display: inline-flex; align-items: center; gap: 6px; padding: 5px 10px; border-radius: 6px;
font-size: 12px; font-weight: 600;
}
.tag-name { font-size: 12px; }
.tag-code { font-size: 10px; opacity: 0.6; letter-spacing: 1px; font-weight: 700; }
.tag button { background: transparent; border: none; cursor: pointer; display: flex; align-items: center; padding: 0; }
.tag button i { width: 12px; height: 12px; }
.flag-img { width: 16px; height: 12px; object-fit: cover; border-radius: 2px; flex-shrink: 0; }
.tag-red { background: rgba(255,62,62,0.1); color: #ff3e3e; border: 1px solid rgba(255,62,62,0.25); }
.tag-red button { color: #ff3e3e; }
.tag-green { background: rgba(0,200,100,0.1); color: #00c864; border: 1px solid rgba(0,200,100,0.25); }
.tag-green button { color: #00c864; }
/* VPN Settings */
.vpn-settings { margin-top: 16px; }
/* Form layout */
.form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin-top: 8px; }
.form-col.full { grid-column: 1 / -1; }
/* Primary button */
.btn-primary {
display: inline-flex; align-items: center; gap: 8px;
background: #df006a; border: none; color: #fff; padding: 10px 20px;
border-radius: 8px; font-weight: 700; font-size: 13px; cursor: pointer; transition: all 0.15s;
}
.btn-primary:hover { background: #b8005a; }
.btn-primary:disabled { opacity: 0.5; cursor: not-allowed; }
.btn-primary i { width: 16px; height: 16px; }
.hidden { display: none; }
@media (max-width: 640px) {
.mode-grid { grid-template-columns: 1fr; }
.form-row { grid-template-columns: 1fr; }
.card-head { flex-direction: column; }
}
</style>