some progress

This commit is contained in:
Doar Mihai
2026-01-17 16:42:43 +02:00
parent 030a105992
commit 3665a1d5a3
57 changed files with 5804 additions and 3 deletions
@@ -0,0 +1,71 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\ValidationException;
class AuthController extends Controller
{
/**
* Create a new API token for the authenticated user.
*/
public function createToken(Request $request): JsonResponse
{
$validated = $request->validate([
'token_name' => ['required', 'string', 'max:255'],
]);
$token = $request->user()->createToken($validated['token_name']);
return response()->json([
'token' => $token->plainTextToken,
'type' => 'Bearer',
], 201);
}
/**
* List all tokens for the authenticated user.
*/
public function tokens(Request $request): JsonResponse
{
$tokens = $request->user()->tokens()->get(['id', 'name', 'last_used_at', 'created_at']);
return response()->json([
'data' => $tokens,
]);
}
/**
* Revoke a specific token.
*/
public function revokeToken(Request $request, string $tokenId): JsonResponse
{
$deleted = $request->user()->tokens()->where('id', $tokenId)->delete();
if (!$deleted) {
return response()->json([
'message' => 'Token not found',
], 404);
}
return response()->json([
'message' => 'Token revoked successfully',
]);
}
/**
* Revoke all tokens for the authenticated user.
*/
public function revokeAllTokens(Request $request): JsonResponse
{
$request->user()->tokens()->delete();
return response()->json([
'message' => 'All tokens revoked successfully',
]);
}
}
@@ -0,0 +1,62 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Requests\Api\BrokenLinksIndexRequest;
use App\Models\Site;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class LinkController extends Controller
{
/**
* Get broken links list for a site.
*/
public function index(BrokenLinksIndexRequest $request, Site $site): JsonResponse
{
// Ensure the site belongs to the authenticated user
if ($site->user_id !== $request->user()->id) {
return response()->json([
'message' => 'Unauthorized',
], 403);
}
$perPage = $request->input('per_page', 50);
$perPage = min($perPage, 100); // Max 100 per page
$brokenLinks = $site->brokenLinks()
->orderBy('last_checked_at', 'desc')
->paginate($perPage);
return response()->json($brokenLinks);
}
/**
* Get broken links statistics for a site.
*/
public function stats(Request $request, Site $site): JsonResponse
{
// Ensure the site belongs to the authenticated user
if ($site->user_id !== $request->user()->id) {
return response()->json([
'message' => 'Unauthorized',
], 403);
}
$totalBrokenLinks = $site->brokenLinks()->count();
$statusCodeBreakdown = $site->brokenLinks()
->selectRaw('status_code, COUNT(*) as count')
->groupBy('status_code')
->orderBy('count', 'desc')
->get();
return response()->json([
'data' => [
'total_broken_links' => $totalBrokenLinks,
'status_code_breakdown' => $statusCodeBreakdown,
],
]);
}
}
@@ -0,0 +1,46 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Site;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class SiteController extends Controller
{
/**
* List all sites for the authenticated user.
*/
public function index(Request $request): JsonResponse
{
$sites = $request->user()
->sites()
->withCount(['uptimeChecks', 'sslCertificates', 'brokenLinks'])
->orderBy('created_at', 'desc')
->get();
return response()->json([
'data' => $sites,
]);
}
/**
* Get a specific site for the authenticated user.
*/
public function show(Request $request, Site $site): JsonResponse
{
// Ensure the site belongs to the authenticated user
if ($site->user_id !== $request->user()->id) {
return response()->json([
'message' => 'Unauthorized',
], 403);
}
$site->loadCount(['uptimeChecks', 'sslCertificates', 'brokenLinks']);
return response()->json([
'data' => $site,
]);
}
}
@@ -0,0 +1,76 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Requests\Api\SslHistoryRequest;
use App\Models\Site;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class SslController extends Controller
{
/**
* Get SSL certificate data for a site.
*/
public function show(Request $request, Site $site): JsonResponse
{
// Ensure the site belongs to the authenticated user
if ($site->user_id !== $request->user()->id) {
return response()->json([
'message' => 'Unauthorized',
], 403);
}
$latestCertificate = $site->sslCertificates()
->latest('checked_at')
->first();
if (!$latestCertificate) {
return response()->json([
'message' => 'No SSL certificate data found',
], 404);
}
$daysUntilExpiry = null;
$isExpiring = false;
if ($latestCertificate->expires_at) {
$daysUntilExpiry = now()->diffInDays($latestCertificate->expires_at, false);
$isExpiring = $daysUntilExpiry <= 30 && $daysUntilExpiry > 0;
}
return response()->json([
'data' => [
'issuer' => $latestCertificate->issuer,
'expires_at' => $latestCertificate->expires_at,
'checked_at' => $latestCertificate->checked_at,
'days_until_expiry' => $daysUntilExpiry ? (int) $daysUntilExpiry : null,
'is_expiring_soon' => $isExpiring,
'is_expired' => $daysUntilExpiry !== null && $daysUntilExpiry < 0,
],
]);
}
/**
* Get SSL certificate history for a site.
*/
public function history(SslHistoryRequest $request, Site $site): JsonResponse
{
// Ensure the site belongs to the authenticated user
if ($site->user_id !== $request->user()->id) {
return response()->json([
'message' => 'Unauthorized',
], 403);
}
$perPage = $request->input('per_page', 20);
$perPage = min($perPage, 100); // Max 100 per page
$certificates = $site->sslCertificates()
->orderBy('checked_at', 'desc')
->paginate($perPage);
return response()->json($certificates);
}
}
@@ -0,0 +1,73 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Requests\Api\UptimeIndexRequest;
use App\Models\Site;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class UptimeController extends Controller
{
/**
* Get uptime history for a site (paginated).
*/
public function index(UptimeIndexRequest $request, Site $site): JsonResponse
{
// Ensure the site belongs to the authenticated user
if ($site->user_id !== $request->user()->id) {
return response()->json([
'message' => 'Unauthorized',
], 403);
}
$perPage = $request->input('per_page', 50);
$perPage = min($perPage, 100); // Max 100 per page
$uptimeChecks = $site->uptimeChecks()
->orderBy('checked_at', 'desc')
->paginate($perPage);
return response()->json($uptimeChecks);
}
/**
* Get uptime statistics for a site.
*/
public function stats(Request $request, Site $site): JsonResponse
{
// Ensure the site belongs to the authenticated user
if ($site->user_id !== $request->user()->id) {
return response()->json([
'message' => 'Unauthorized',
], 403);
}
$totalChecks = $site->uptimeChecks()->count();
$successfulChecks = $site->uptimeChecks()
->whereBetween('status_code', [200, 299])
->count();
$uptimePercentage = $totalChecks > 0
? round(($successfulChecks / $totalChecks) * 100, 2)
: 0;
$averageResponseTime = $site->uptimeChecks()
->avg('response_time_ms');
$latestCheck = $site->uptimeChecks()
->latest('checked_at')
->first();
return response()->json([
'data' => [
'total_checks' => $totalChecks,
'successful_checks' => $successfulChecks,
'uptime_percentage' => $uptimePercentage,
'average_response_time_ms' => $averageResponseTime ? round($averageResponseTime, 2) : null,
'latest_check' => $latestCheck,
],
]);
}
}
@@ -0,0 +1,173 @@
<?php
namespace App\Http\Controllers;
use App\Models\DnsRecord;
use App\Models\Site;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\View\View;
class DnsRecordController extends Controller
{
/**
* Display a listing of the DNS records for a site.
*/
public function index(Site $site): View
{
// Ensure the user owns the site
if ($site->user_id !== Auth::id()) {
abort(403, 'Unauthorized access to this site.');
}
$dnsRecords = $site->dnsRecords()
->orderBy('created_at', 'desc')
->get();
return view('dns-records.index', compact('site', 'dnsRecords'));
}
/**
* Show the form for creating a new DNS record.
*/
public function create(Site $site): View
{
// Ensure the user owns the site
if ($site->user_id !== Auth::id()) {
abort(403, 'Unauthorized access to this site.');
}
$recordTypes = DnsRecord::getRecordTypes();
return view('dns-records.create', compact('site', 'recordTypes'));
}
/**
* Store a newly created DNS record in storage.
*/
public function store(Request $request, Site $site): RedirectResponse
{
// Ensure the user owns the site
if ($site->user_id !== Auth::id()) {
abort(403, 'Unauthorized access to this site.');
}
$validated = $request->validate([
'domain' => ['required', 'string', 'max:255'],
'record_type' => ['required', 'string', 'in:A,AAAA,CNAME,MX,TXT,NS,SOA,PTR,SRV'],
'expected_value' => ['nullable', 'string', 'max:1000'],
'is_active' => ['boolean'],
]);
// Check for duplicate record
$exists = $site->dnsRecords()
->where('domain', $validated['domain'])
->where('record_type', $validated['record_type'])
->exists();
if ($exists) {
return back()
->withInput()
->withErrors(['domain' => "A {$validated['record_type']} record for {$validated['domain']} already exists for this site."]);
}
$dnsRecord = $site->dnsRecords()->create([
'domain' => $validated['domain'],
'record_type' => $validated['record_type'],
'expected_value' => $validated['expected_value'] ?? null,
'is_active' => $validated['is_active'] ?? true,
]);
return redirect()
->route('dns-records.show', [$site, $dnsRecord])
->with('success', 'DNS record created successfully.');
}
/**
* Display the specified DNS record.
*/
public function show(Site $site, DnsRecord $dnsRecord): View
{
// Ensure the user owns the site and the DNS record belongs to the site
if ($site->user_id !== Auth::id() || $dnsRecord->site_id !== $site->id) {
abort(403, 'Unauthorized access to this DNS record.');
}
return view('dns-records.show', compact('site', 'dnsRecord'));
}
/**
* Show the form for editing the specified DNS record.
*/
public function edit(Site $site, DnsRecord $dnsRecord): View
{
// Ensure the user owns the site and the DNS record belongs to the site
if ($site->user_id !== Auth::id() || $dnsRecord->site_id !== $site->id) {
abort(403, 'Unauthorized access to this DNS record.');
}
$recordTypes = DnsRecord::getRecordTypes();
return view('dns-records.edit', compact('site', 'dnsRecord', 'recordTypes'));
}
/**
* Update the specified DNS record in storage.
*/
public function update(Request $request, Site $site, DnsRecord $dnsRecord): RedirectResponse
{
// Ensure the user owns the site and the DNS record belongs to the site
if ($site->user_id !== Auth::id() || $dnsRecord->site_id !== $site->id) {
abort(403, 'Unauthorized access to this DNS record.');
}
$validated = $request->validate([
'domain' => ['required', 'string', 'max:255'],
'record_type' => ['required', 'string', 'in:A,AAAA,CNAME,MX,TXT,NS,SOA,PTR,SRV'],
'expected_value' => ['nullable', 'string', 'max:1000'],
'is_active' => ['boolean'],
]);
// Check for duplicate record (excluding current record)
$exists = $site->dnsRecords()
->where('domain', $validated['domain'])
->where('record_type', $validated['record_type'])
->where('id', '!=', $dnsRecord->id)
->exists();
if ($exists) {
return back()
->withInput()
->withErrors(['domain' => "A {$validated['record_type']} record for {$validated['domain']} already exists for this site."]);
}
$dnsRecord->update([
'domain' => $validated['domain'],
'record_type' => $validated['record_type'],
'expected_value' => $validated['expected_value'] ?? null,
'is_active' => $validated['is_active'] ?? $dnsRecord->is_active,
]);
return redirect()
->route('dns-records.show', [$site, $dnsRecord])
->with('success', 'DNS record updated successfully.');
}
/**
* Remove the specified DNS record from storage.
*/
public function destroy(Site $site, DnsRecord $dnsRecord): RedirectResponse
{
// Ensure the user owns the site and the DNS record belongs to the site
if ($site->user_id !== Auth::id() || $dnsRecord->site_id !== $site->id) {
abort(403, 'Unauthorized access to this DNS record.');
}
$dnsRecord->delete();
return redirect()
->route('dns-records.index', $site)
->with('success', 'DNS record deleted successfully.');
}
}
@@ -0,0 +1,185 @@
<?php
namespace App\Http\Controllers;
use App\Models\HealthCheck;
use App\Models\Site;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\View\View;
class HealthCheckController extends Controller
{
/**
* Display a listing of the health checks for a site.
*/
public function index(Site $site): View
{
// Ensure the user owns the site
if ($site->user_id !== Auth::id()) {
abort(403, 'Unauthorized access to this site.');
}
$healthChecks = $site->healthChecks()
->orderBy('created_at', 'desc')
->get();
return view('health-checks.index', compact('site', 'healthChecks'));
}
/**
* Show the form for creating a new health check.
*/
public function create(Site $site): View
{
// Ensure the user owns the site
if ($site->user_id !== Auth::id()) {
abort(403, 'Unauthorized access to this site.');
}
return view('health-checks.create', compact('site'));
}
/**
* Store a newly created health check in storage.
*/
public function store(Request $request, Site $site): RedirectResponse
{
// Ensure the user owns the site
if ($site->user_id !== Auth::id()) {
abort(403, 'Unauthorized access to this site.');
}
$validated = $request->validate([
'endpoint' => ['required', 'string', 'max:255'],
'expected_json_keys' => ['nullable', 'string'],
'performance_threshold' => ['nullable', 'integer', 'min:1', 'max:60000'],
'is_active' => ['boolean'],
]);
// Convert comma-separated keys to array
$expectedKeys = null;
if (!empty($validated['expected_json_keys'])) {
$expectedKeys = array_map(
'trim',
explode(',', $validated['expected_json_keys'])
);
}
// Check for duplicate endpoint
$exists = $site->healthChecks()
->where('endpoint', $validated['endpoint'])
->exists();
if ($exists) {
return back()
->withInput()
->withErrors(['endpoint' => 'This endpoint is already being monitored for this site.']);
}
$healthCheck = $site->healthChecks()->create([
'endpoint' => $validated['endpoint'],
'expected_json_keys' => $expectedKeys,
'performance_threshold' => $validated['performance_threshold'] ?? null,
'is_active' => $validated['is_active'] ?? true,
]);
return redirect()
->route('health-checks.show', [$site, $healthCheck])
->with('success', 'Health check created successfully.');
}
/**
* Display the specified health check.
*/
public function show(Site $site, HealthCheck $healthCheck): View
{
// Ensure the user owns the site and the health check belongs to the site
if ($site->user_id !== Auth::id() || $healthCheck->site_id !== $site->id) {
abort(403, 'Unauthorized access to this health check.');
}
return view('health-checks.show', compact('site', 'healthCheck'));
}
/**
* Show the form for editing the specified health check.
*/
public function edit(Site $site, HealthCheck $healthCheck): View
{
// Ensure the user owns the site and the health check belongs to the site
if ($site->user_id !== Auth::id() || $healthCheck->site_id !== $site->id) {
abort(403, 'Unauthorized access to this health check.');
}
return view('health-checks.edit', compact('site', 'healthCheck'));
}
/**
* Update the specified health check in storage.
*/
public function update(Request $request, Site $site, HealthCheck $healthCheck): RedirectResponse
{
// Ensure the user owns the site and the health check belongs to the site
if ($site->user_id !== Auth::id() || $healthCheck->site_id !== $site->id) {
abort(403, 'Unauthorized access to this health check.');
}
$validated = $request->validate([
'endpoint' => ['required', 'string', 'max:255'],
'expected_json_keys' => ['nullable', 'string'],
'performance_threshold' => ['nullable', 'integer', 'min:1', 'max:60000'],
'is_active' => ['boolean'],
]);
// Convert comma-separated keys to array
$expectedKeys = null;
if (!empty($validated['expected_json_keys'])) {
$expectedKeys = array_map(
'trim',
explode(',', $validated['expected_json_keys'])
);
}
// Check for duplicate endpoint (excluding current health check)
$exists = $site->healthChecks()
->where('endpoint', $validated['endpoint'])
->where('id', '!=', $healthCheck->id)
->exists();
if ($exists) {
return back()
->withInput()
->withErrors(['endpoint' => 'This endpoint is already being monitored for this site.']);
}
$healthCheck->update([
'endpoint' => $validated['endpoint'],
'expected_json_keys' => $expectedKeys,
'performance_threshold' => $validated['performance_threshold'] ?? null,
'is_active' => $validated['is_active'] ?? $healthCheck->is_active,
]);
return redirect()
->route('health-checks.show', [$site, $healthCheck])
->with('success', 'Health check updated successfully.');
}
/**
* Remove the specified health check from storage.
*/
public function destroy(Site $site, HealthCheck $healthCheck): RedirectResponse
{
// Ensure the user owns the site and the health check belongs to the site
if ($site->user_id !== Auth::id() || $healthCheck->site_id !== $site->id) {
abort(403, 'Unauthorized access to this health check.');
}
$healthCheck->delete();
return redirect()
->route('health-checks.index', $site)
->with('success', 'Health check deleted successfully.');
}
}
+165
View File
@@ -0,0 +1,165 @@
<?php
namespace App\Http\Controllers;
use App\Models\Site;
use App\Models\TcpPort;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class TcpPortController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index(Request $request, Site $site)
{
// Ensure the site belongs to the authenticated user
if ($site->user_id !== Auth::id()) {
abort(403);
}
$tcpPorts = $site->tcpPorts()
->orderBy('port')
->paginate(20);
return view('tcp-ports.index', compact('site', 'tcpPorts'));
}
/**
* Show the form for creating a new resource.
*/
public function create(Site $site)
{
// Ensure the site belongs to the authenticated user
if ($site->user_id !== Auth::id()) {
abort(403);
}
$commonPorts = TcpPort::getCommonPorts();
return view('tcp-ports.create', compact('site', 'commonPorts'));
}
/**
* Store a newly created resource in storage.
*/
public function store(Request $request, Site $site)
{
// Ensure the site belongs to the authenticated user
if ($site->user_id !== Auth::id()) {
abort(403);
}
$validated = $request->validate([
'port' => ['required', 'integer', 'min:1', 'max:65535'],
'name' => ['nullable', 'string', 'max:255'],
'is_active' => ['boolean'],
]);
// Check if port already exists for this site
$exists = $site->tcpPorts()
->where('port', $validated['port'])
->exists();
if ($exists) {
return back()
->withInput()
->withErrors(['port' => 'This port is already being monitored for this site.']);
}
$site->tcpPorts()->create([
'port' => $validated['port'],
'name' => $validated['name'] ?? null,
'is_active' => $validated['is_active'] ?? true,
]);
return redirect()
->route('tcp-ports.index', $site)
->with('success', 'TCP port added successfully.');
}
/**
* Display the specified resource.
*/
public function show(Site $site, TcpPort $tcpPort)
{
// Ensure the port belongs to the site and user
if ($tcpPort->site_id !== $site->id || $site->user_id !== Auth::id()) {
abort(403);
}
return view('tcp-ports.show', compact('site', 'tcpPort'));
}
/**
* Show the form for editing the specified resource.
*/
public function edit(Site $site, TcpPort $tcpPort)
{
// Ensure the port belongs to the site and user
if ($tcpPort->site_id !== $site->id || $site->user_id !== Auth::id()) {
abort(403);
}
$commonPorts = TcpPort::getCommonPorts();
return view('tcp-ports.edit', compact('site', 'tcpPort', 'commonPorts'));
}
/**
* Update the specified resource in storage.
*/
public function update(Request $request, Site $site, TcpPort $tcpPort)
{
// Ensure the port belongs to the site and user
if ($tcpPort->site_id !== $site->id || $site->user_id !== Auth::id()) {
abort(403);
}
$validated = $request->validate([
'port' => ['required', 'integer', 'min:1', 'max:65535'],
'name' => ['nullable', 'string', 'max:255'],
'is_active' => ['boolean'],
]);
// Check if port already exists for this site (excluding current)
$exists = $site->tcpPorts()
->where('port', $validated['port'])
->where('id', '!=', $tcpPort->id)
->exists();
if ($exists) {
return back()
->withInput()
->withErrors(['port' => 'This port is already being monitored for this site.']);
}
$tcpPort->update([
'port' => $validated['port'],
'name' => $validated['name'] ?? null,
'is_active' => $validated['is_active'] ?? $tcpPort->is_active,
]);
return redirect()
->route('tcp-ports.index', $site)
->with('success', 'TCP port updated successfully.');
}
/**
* Remove the specified resource from storage.
*/
public function destroy(Site $site, TcpPort $tcpPort)
{
// Ensure the port belongs to the site and user
if ($tcpPort->site_id !== $site->id || $site->user_id !== Auth::id()) {
abort(403);
}
$tcpPort->delete();
return redirect()
->route('tcp-ports.index', $site)
->with('success', 'TCP port deleted successfully.');
}
}
@@ -0,0 +1,28 @@
<?php
namespace App\Http\Requests\Api;
use Illuminate\Foundation\Http\FormRequest;
class BrokenLinksIndexRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'per_page' => ['nullable', 'integer', 'min:1', 'max:100'],
];
}
}
@@ -0,0 +1,28 @@
<?php
namespace App\Http\Requests\Api;
use Illuminate\Foundation\Http\FormRequest;
class SslHistoryRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'per_page' => ['nullable', 'integer', 'min:1', 'max:100'],
];
}
}
@@ -0,0 +1,28 @@
<?php
namespace App\Http\Requests\Api;
use Illuminate\Foundation\Http\FormRequest;
class UptimeIndexRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'per_page' => ['nullable', 'integer', 'min:1', 'max:100'],
];
}
}