Add multiple security and validation improvements across the app: - Prevent session fixation: regenerate session ID on login and after successful 2FA; tighten session cookie params (Secure, HttpOnly, SameSite=Lax). - Harden installer: add CSRF checks for install/update flows and use PDO::quote when injecting admin credentials into SQL migration to avoid injection; add csrf_field() to installer templates. - Template hardening: add safe_url and safe_mailto Twig filters, escape tag names for JS, and add rel="noopener noreferrer" to external links to mitigate XSS/opener risks. - Domain controller: validate referrer to avoid open redirects, enforce user isolation mode when finding/deleting/updating domains and when assigning notification groups (ensures users only affect their own resources). - Notification groups: verify channel belongs to group before deleting or toggling to prevent unauthorized access. - ErrorLog: whitelist allowed sort columns to avoid arbitrary column injection in ORDER BY. - Routes: move the debug whois route to protected/admin area. These changes collectively reduce attack surface (XSS, open redirect, session fixation, SQL injection) and enforce proper resource isolation and input validation.
260 lines
12 KiB
Twig
260 lines
12 KiB
Twig
{% extends 'layout/base.twig' %}
|
|
|
|
{% set title = 'Add New Domain' %}
|
|
{% set pageTitle = 'Add New Domain' %}
|
|
{% set pageDescription = 'Start monitoring a new domain' %}
|
|
{% set pageIcon = 'fas fa-plus-circle' %}
|
|
|
|
{% block content %}
|
|
|
|
<!-- Main Form -->
|
|
<div class="max-w-3xl mx-auto">
|
|
<div class="bg-white dark:bg-slate-800 rounded-lg border border-gray-200 dark:border-slate-700 overflow-hidden">
|
|
<div class="px-6 py-4 border-b border-gray-200 dark:border-slate-700">
|
|
<h2 class="text-lg font-semibold text-gray-900 dark:text-white flex items-center">
|
|
<i class="fas fa-globe text-gray-400 dark:text-slate-500 mr-2 text-sm"></i>
|
|
Domain Information
|
|
</h2>
|
|
</div>
|
|
|
|
<div class="p-6">
|
|
<form method="POST" action="/domains/store" class="space-y-5">
|
|
{{ csrf_field() }}
|
|
<!-- Domain Name -->
|
|
<div>
|
|
<label for="domain_name" class="block text-sm font-medium text-gray-700 dark:text-slate-300 mb-1.5">
|
|
Domain Name *
|
|
</label>
|
|
<input type="text"
|
|
id="domain_name"
|
|
name="domain_name"
|
|
class="w-full px-3 py-2.5 border border-gray-300 dark:border-slate-600 rounded-lg bg-white dark:bg-slate-900 text-gray-900 dark:text-white focus:ring-2 focus:ring-primary focus:border-primary transition-colors text-sm"
|
|
placeholder="example.com"
|
|
required
|
|
autofocus>
|
|
<p class="mt-1.5 text-xs text-gray-500 dark:text-slate-400">
|
|
Enter the domain name without http:// or https://
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Tags -->
|
|
<div>
|
|
<label for="tags-input" class="block text-sm font-medium text-gray-700 dark:text-slate-300 mb-1.5">
|
|
Tags
|
|
<span class="text-gray-400 dark:text-slate-500 font-normal">(Optional)</span>
|
|
</label>
|
|
|
|
<!-- Tag Display Area -->
|
|
<div id="tags-display" class="min-h-[40px] p-2 border border-gray-300 dark:border-slate-600 rounded-lg mb-2 flex flex-wrap gap-1.5 bg-gray-50 dark:bg-slate-900"></div>
|
|
|
|
<!-- Tag Input -->
|
|
<div class="relative">
|
|
<i class="fas fa-tag absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 dark:text-slate-500 text-sm"></i>
|
|
<input type="text"
|
|
id="tags-input"
|
|
class="w-full pl-10 pr-20 py-2.5 border border-gray-300 dark:border-slate-600 rounded-lg bg-white dark:bg-slate-900 text-gray-900 dark:text-white focus:ring-2 focus:ring-primary focus:border-primary transition-colors text-sm"
|
|
placeholder="Type any tag and press Enter or comma..."
|
|
onkeydown="handleTagInput(event)">
|
|
<button type="button" onclick="addTagFromInput()" class="absolute right-2 top-1/2 transform -translate-y-1/2 px-3 py-1 bg-primary text-white text-xs rounded hover:bg-primary-dark">
|
|
Add
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Hidden input to store tags for form submission -->
|
|
<input type="hidden" id="tags" name="tags" value="">
|
|
|
|
<p class="mt-1.5 text-xs text-gray-500 dark:text-slate-400">
|
|
<i class="fas fa-info-circle mr-1"></i>
|
|
Type any custom tag (letters, numbers, hyphens). Press <kbd class="px-1 py-0.5 bg-gray-200 dark:bg-slate-600 rounded text-xs">Enter</kbd> or <kbd class="px-1 py-0.5 bg-gray-200 dark:bg-slate-600 rounded text-xs">,</kbd> to add.
|
|
</p>
|
|
|
|
<!-- Available Tags -->
|
|
<div class="mt-2">
|
|
<p class="text-xs text-gray-600 dark:text-slate-400 mb-1.5">💡 Available Tags:</p>
|
|
<div class="flex flex-wrap gap-1.5">
|
|
{% for tag in availableTags %}
|
|
<button type="button" onclick="addTag('{{ tag.name|e('js') }}')"
|
|
class="inline-flex items-center px-2 py-1 rounded-md text-xs font-medium border {{ tag.color }} hover:opacity-80 transition-colors">
|
|
<i class="fas fa-plus mr-1" style="font-size: 8px;"></i>
|
|
{{ tag.name }}
|
|
</button>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Notification Group -->
|
|
<div>
|
|
<label for="notification_group_id" class="block text-sm font-medium text-gray-700 dark:text-slate-300 mb-1.5">
|
|
Notification Group
|
|
</label>
|
|
<select id="notification_group_id"
|
|
name="notification_group_id"
|
|
class="w-full px-3 py-2.5 border border-gray-300 dark:border-slate-600 rounded-lg bg-white dark:bg-slate-900 text-gray-900 dark:text-white focus:ring-2 focus:ring-primary focus:border-primary transition-colors text-sm">
|
|
<option value="">-- No Group (No notifications) --</option>
|
|
{% for group in groups %}
|
|
<option value="{{ group.id }}">{{ group.name }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
<p class="mt-1.5 text-xs text-gray-500 dark:text-slate-400">
|
|
Optional: Assign to a notification group to receive expiry alerts
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Action Buttons -->
|
|
<div class="flex flex-col sm:flex-row gap-3 pt-3">
|
|
<button type="submit"
|
|
class="inline-flex items-center justify-center px-5 py-2.5 bg-primary hover:bg-primary-dark text-white rounded-lg font-medium transition-colors text-sm">
|
|
<i class="fas fa-plus-circle mr-2"></i>
|
|
Add Domain
|
|
</button>
|
|
<a href="/domains"
|
|
class="inline-flex items-center justify-center px-5 py-2.5 border border-gray-300 dark:border-slate-600 text-gray-700 dark:text-slate-300 rounded-lg font-medium hover:bg-gray-50 dark:hover:bg-slate-700 transition-colors text-sm">
|
|
<i class="fas fa-times mr-2"></i>
|
|
Cancel
|
|
</a>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Info Cards -->
|
|
<div class="mt-4 grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<!-- How it works -->
|
|
<div class="bg-blue-50 dark:bg-blue-500/10 border border-blue-200 dark:border-blue-800 rounded-lg p-4">
|
|
<div class="flex items-start">
|
|
<div class="flex-shrink-0">
|
|
<div class="w-10 h-10 bg-blue-500 rounded-lg flex items-center justify-center">
|
|
<i class="fas fa-info-circle text-white"></i>
|
|
</div>
|
|
</div>
|
|
<div class="ml-3">
|
|
<h3 class="text-sm font-semibold text-gray-900 dark:text-white mb-1">How It Works</h3>
|
|
<p class="text-xs text-gray-600 dark:text-slate-400 leading-relaxed">
|
|
When you add a domain, we automatically fetch its WHOIS information including
|
|
expiration date, registrar, nameservers, and other important details. This may take a few seconds.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- What we track -->
|
|
<div class="bg-green-50 dark:bg-green-500/10 border border-green-200 dark:border-green-800 rounded-lg p-4">
|
|
<div class="flex items-start">
|
|
<div class="flex-shrink-0">
|
|
<div class="w-10 h-10 bg-green-500 rounded-lg flex items-center justify-center">
|
|
<i class="fas fa-check-circle text-white"></i>
|
|
</div>
|
|
</div>
|
|
<div class="ml-3">
|
|
<h3 class="text-sm font-semibold text-gray-900 dark:text-white mb-1">What We Track</h3>
|
|
<ul class="text-xs text-gray-600 dark:text-slate-400 space-y-1">
|
|
<li class="flex items-center">
|
|
<i class="fas fa-circle text-green-500" style="font-size: 6px;"></i>
|
|
<span class="ml-2">Domain expiration date</span>
|
|
</li>
|
|
<li class="flex items-center">
|
|
<i class="fas fa-circle text-green-500" style="font-size: 6px;"></i>
|
|
<span class="ml-2">Registrar information</span>
|
|
</li>
|
|
<li class="flex items-center">
|
|
<i class="fas fa-circle text-green-500" style="font-size: 6px;"></i>
|
|
<span class="ml-2">Nameservers</span>
|
|
</li>
|
|
<li class="flex items-center">
|
|
<i class="fas fa-circle text-green-500" style="font-size: 6px;"></i>
|
|
<span class="ml-2">Domain status</span>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{% endblock %}
|
|
|
|
{% block scripts %}
|
|
<script>
|
|
let tags = [];
|
|
|
|
const availableTags = {{ availableTags|json_encode|raw }};
|
|
const tagColors = {};
|
|
availableTags.forEach(tag => {
|
|
tagColors[tag.name] = tag.color;
|
|
});
|
|
|
|
function addTag(tagName) {
|
|
tagName = tagName.trim().toLowerCase();
|
|
|
|
if (!/^[a-z0-9-]+$/.test(tagName)) {
|
|
return;
|
|
}
|
|
|
|
if (tags.includes(tagName)) {
|
|
return;
|
|
}
|
|
|
|
tags.push(tagName);
|
|
updateTagsDisplay();
|
|
updateHiddenInput();
|
|
|
|
document.getElementById('tags-input').value = '';
|
|
}
|
|
|
|
function removeTag(tagName) {
|
|
tags = tags.filter(t => t !== tagName);
|
|
updateTagsDisplay();
|
|
updateHiddenInput();
|
|
}
|
|
|
|
function updateTagsDisplay() {
|
|
const display = document.getElementById('tags-display');
|
|
display.innerHTML = '';
|
|
|
|
if (tags.length === 0) {
|
|
display.innerHTML = '<span class="text-xs text-gray-400 dark:text-slate-500 italic">No tags added yet</span>';
|
|
return;
|
|
}
|
|
|
|
tags.forEach(tag => {
|
|
const colorClass = tagColors[tag] || 'bg-gray-100 text-gray-700 border-gray-300';
|
|
const tagElement = document.createElement('span');
|
|
tagElement.className = `inline-flex items-center px-2.5 py-1 rounded-md text-xs font-medium border ${colorClass}`;
|
|
tagElement.innerHTML = `
|
|
<i class="fas fa-tag mr-1" style="font-size: 9px;"></i>
|
|
${tag}
|
|
<button type="button" onclick="removeTag('${tag}')" class="ml-1.5 hover:text-red-600">
|
|
<i class="fas fa-times" style="font-size: 9px;"></i>
|
|
</button>
|
|
`;
|
|
display.appendChild(tagElement);
|
|
});
|
|
}
|
|
|
|
function updateHiddenInput() {
|
|
document.getElementById('tags').value = tags.join(',');
|
|
}
|
|
|
|
function handleTagInput(event) {
|
|
if (event.key === 'Enter' || event.key === ',') {
|
|
event.preventDefault();
|
|
addTagFromInput();
|
|
}
|
|
}
|
|
|
|
function addTagFromInput() {
|
|
const input = document.getElementById('tags-input');
|
|
const value = input.value.trim();
|
|
|
|
if (value) {
|
|
const newTags = value.split(',').map(t => t.trim().toLowerCase()).filter(t => t);
|
|
newTags.forEach(tag => addTag(tag));
|
|
input.value = '';
|
|
}
|
|
}
|
|
|
|
updateTagsDisplay();
|
|
</script>
|
|
{% endblock %}
|