Skip to content
Merged
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
14 changes: 9 additions & 5 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,16 @@ RUN npm run build



FROM dunglas/frankenphp:php8.2-bookworm

FROM dunglas/frankenphp:php8.2-bookworm

RUN apt-get update && \
apt-get install -y unzip libnss3-tools procps && \
rm -rf /var/lib/apt/lists/*
RUN apt-get update \
&& apt-get install -y curl ca-certificates gnupg2 \
&& curl -fsSL https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmor -o /usr/share/keyrings/postgresql-keyring.gpg \
&& echo "deb [signed-by=/usr/share/keyrings/postgresql-keyring.gpg] https://apt.postgresql.org/pub/repos/apt jammy-pgdg main" > /etc/apt/sources.list.d/pgdg.list \
&& apt-get update \
&& apt-get install -y unzip libnss3-tools procps postgresql-client-17 \
&& rm -rf /var/lib/apt/lists/*

RUN install-php-extensions \
intl \
Expand All @@ -30,7 +34,7 @@ RUN mkdir -p /app/public/build
COPY --from=frontend_builder /app/public/build/ /app/public/build/


COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
COPY . /app

RUN composer install --optimize-autoloader && php artisan optimize:clear
Expand Down
53 changes: 53 additions & 0 deletions app/Jobs/BackupDatabase.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?php

namespace App\Jobs;

use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use Illuminate\Process\Pipe;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Process;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use RuntimeException;

class BackupDatabase implements ShouldQueue
{
use Queueable;

public function __construct(public string $path) {}

public function handle(): void
{

$result = Process::pipe(function (Pipe $pipe) {
$backupCommand = Str::of('pg_dump')->append(' -U ')
->append(config('database.connections.pgsql.username'))
->append(' --host ')
->append(config('database.connections.pgsql.host'))
->append(' --port ')
->append(config('database.connections.pgsql.port'))
->append(' --format tar ')
->append(config('database.connections.pgsql.database'));

$pipe->as('pg_dump')->command($backupCommand)->env([
'PGPASSWORD' => config('database.connections.pgsql.password'),
]);
$pipe->as('gzip')->command('gzip');
});

if (($result->failed())) {

// Log the error output
Log::error('Database backup failed', ['stdEr' => $result->errorOutput()]);
// Raise an appropriate exception
throw new RuntimeException('Database backup failed: '.$result->errorOutput());
}

Storage::disk('local')->put($this->path, $result->output());

Log::info('Database backup completed successfully', [
'file' => $this->path,
]);
}
}
37 changes: 37 additions & 0 deletions app/Livewire/AdminIndexPage.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

namespace App\Livewire;

use App\Jobs\BackupDatabase;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Livewire\Component;
use RuntimeException;

class AdminIndexPage extends Component
{
public string $backupError = '';

public function backupDatabase()
{
$this->authorize('administrate');
$this->backupError = '';

$timestamp = Carbon::now()->format('Y-m-d-H-i-s');
$filename = Str::of('database-backup-')->append($timestamp)->append('.tar.gz');
$path = Str::of('backups/')->append($filename);
try {
BackupDatabase::dispatchSync($path);

return Storage::download($path, $filename);
} catch (RuntimeException $e) {
$this->backupError = $e->getMessage();
}
}

public function render()
{
return view('livewire.admin-index-page');
}
}
6 changes: 5 additions & 1 deletion app/Providers/AppServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

namespace App\Providers;

use App\Models\User;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
Expand All @@ -19,6 +21,8 @@ public function register(): void
*/
public function boot(): void
{
//
Gate::define('administrate', function (User $user) {
return $user->is_admin;
});
}
}
21 changes: 12 additions & 9 deletions composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions config/filesystems.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
'driver' => 'local',
'root' => storage_path('app'),
'throw' => false,
'serve' => true,
],

'public' => [
Expand Down
2 changes: 1 addition & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ x-laravel-config: &laravel-config

services:
database:
image: postgres:latest
image: postgres:17.2
secrets:
- DB_PASSWORD
environment:
Expand Down
10 changes: 10 additions & 0 deletions resources/views/components/layout/app.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,16 @@ class="link link-primary"
>
Scorecards
</a>

@can("administrate")
<a
href="{{ route("admin.index") }}"
wire:navigate
class="link link-primary"
>
Admin
</a>
@endcan
</div>
{{ $slot }}
</main>
Expand Down
15 changes: 15 additions & 0 deletions resources/views/livewire/admin-index-page.blade.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<div>
<x-type.page-title>Admin</x-type.page-title>

<ul class="list-disc">
<a class="list-item link link-primary" href="/admin">Filament Admin</a>

<a wire:click="backupDatabase" class="list-item link link-primary">
Backup database
</a>
</ul>

<div>
{{ $backupError }}
</div>
</div>
3 changes: 3 additions & 0 deletions routes/web.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

use App\Http\Controllers\ProfileController;
use App\Http\Controllers\ScorecardController;
use App\Livewire\AdminIndexPage;
use App\Livewire\Media\MediaPage;
use App\Livewire\Notes\NotesIndexPage;
use App\Livewire\Notes\ShowNotePage;
Expand All @@ -23,6 +24,8 @@
return view('welcome');
})->name('home');

Route::get('/backend', AdminIndexPage::class)->name('admin.index')->middleware('can:administrate');

Route::get('/dashboard', function () {
return view('dashboard');
})->middleware(['auth', 'verified'])->name('dashboard');
Expand Down
31 changes: 31 additions & 0 deletions tests/Feature/Http/Admin/AdminIndexTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

use App\Models\User;
use Tests\TestCase;

test('anonymous user not allowed', function () {
/** @var TestCase $this */
$response = $this->get('/backend');
$response->assertStatus(403);
$response->assertSeeText('Forbidden');
});

test('regular user not allowed', function () {
/** @var TestCase $this */
$user = User::factory()->create();
$response = $this->actingAs($user)->get('/backend');
$response->assertStatus(403);
$response->assertSeeText('Forbidden');
});

test('Visible to admin', function () {
/** @var TestCase $this */
$user = User::factory()->admin()->create();
$response = $this->actingAs($user)->get('/backend');
$response->assertStatus(200);
$response->assertSeeTextInOrder([
'Admin',
'Filament Admin',
'Backup database',
]);
});
38 changes: 38 additions & 0 deletions tests/Feature/Jobs/BackupDatabaseTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php

use App\Jobs\BackupDatabase;
use Illuminate\Support\Facades\Process;
use Illuminate\Support\Facades\Storage;
use Tests\TestCase;

test('happy path', function () {
/** @var TestCase $this */
Storage::fake('local');

Process::fake([
'pg_dump *' => Process::result(output: 'some tar file'),
'gzip' => Process::result(output: 'compressed tar file'),
]);

BackupDatabase::dispatchSync('backup');

Storage::assertExists('backup', "compressed tar file\n");
});

test('failure', function () {
/** @var TestCase $this */
Storage::fake('local');

Process::fake([
'pg_dump *' => Process::result(exitCode: 1, errorOutput: 'pg_dump: error'),
'gzip' => Process::result(output: 'compressed tar file'),
]);

$this->assertThrows(function () {
BackupDatabase::dispatchSync('backup');
}, RuntimeException::class);

Process::assertDidntRun('gzip');

$this->assertFalse(Storage::exists('backup'));
});
Loading
Loading