Add Fortify auth, Flux dark mode, demo seed, and Docker hot reload.

Protect recordings per user, seed a demo login on container start, and bind-mount the app with Vite HMR for local Compose development.
This commit is contained in:
ben
2026-08-12 18:38:53 +02:00
parent accb721811
commit bfcfc12f58
78 changed files with 3942 additions and 186 deletions
@@ -4,6 +4,7 @@ namespace App\Http\Controllers;
use App\Models\Recording;
use Illuminate\Http\RedirectResponse;
use Illuminate\Support\Facades\Gate;
class CancelTranscriptionController extends Controller
{
@@ -12,6 +13,8 @@ class CancelTranscriptionController extends Controller
*/
public function __invoke(Recording $recording): RedirectResponse
{
Gate::authorize('transcribe', $recording);
if (! $recording->isTranscribing()) {
return back()->with('error', 'No transcription is currently running.');
}
+18 -8
View File
@@ -8,6 +8,7 @@ use App\Services\Mp3MetadataService;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Storage;
use Illuminate\View\View;
@@ -18,12 +19,14 @@ class RecordingController extends Controller
*/
public function index(Request $request): View
{
Recording::query()
$user = $request->user();
$user->recordings()
->whereIn('transcription_status', ['pending', 'processing'])
->orderBy('id')
->each(fn (Recording $recording) => $recording->recoverOrphanedTranscription());
$query = Recording::query()->latest();
$query = $user->recordings()->latest();
if ($search = $request->string('q')->trim()->toString()) {
$query->search($search);
@@ -31,7 +34,7 @@ class RecordingController extends Controller
$recordings = $query->paginate(20)->withQueryString();
$pendingCount = Recording::query()
$pendingCount = $user->recordings()
->whereIn('transcription_status', ['pending', 'failed', 'cancelled'])
->get()
->filter(fn (Recording $recording) => ! $recording->hasActiveTranscriptionJob())
@@ -47,9 +50,9 @@ class RecordingController extends Controller
/**
* Show the upload form.
*/
public function create(): View
public function create(Request $request): View
{
$existingFingerprints = Recording::query()
$existingFingerprints = $request->user()->recordings()
->get(['original_filename', 'file_size_bytes'])
->map(fn (Recording $recording) => $this->uploadFingerprint(
$recording->original_filename,
@@ -75,6 +78,7 @@ class RecordingController extends Controller
fn ($file) => $file instanceof UploadedFile,
));
$user = $request->user();
$titleOverride = $request->string('title')->trim()->toString();
$recordings = [];
$skippedDuplicates = 0;
@@ -89,8 +93,8 @@ class RecordingController extends Controller
if (
isset($seenHashes[$hash])
|| Recording::query()->where('content_hash', $hash)->exists()
|| Recording::query()
|| $user->recordings()->where('content_hash', $hash)->exists()
|| $user->recordings()
->where('original_filename', $file->getClientOriginalName())
->where('file_size_bytes', $file->getSize() ?: 0)
->exists()
@@ -106,7 +110,7 @@ class RecordingController extends Controller
? $titleOverride
: null;
$recordings[] = $this->storeUploadedRecording($file, $metadata, $hash, $title);
$recordings[] = $this->storeUploadedRecording($request, $file, $metadata, $hash, $title);
}
if ($recordings === [] && $skippedDuplicates > 0) {
@@ -149,6 +153,8 @@ class RecordingController extends Controller
*/
public function show(Recording $recording): View
{
Gate::authorize('view', $recording);
$recording->recoverOrphanedTranscription();
$recording->refresh();
@@ -160,6 +166,8 @@ class RecordingController extends Controller
*/
public function destroy(Recording $recording): RedirectResponse
{
Gate::authorize('delete', $recording);
$recording->deleteFile();
$recording->delete();
@@ -172,6 +180,7 @@ class RecordingController extends Controller
* Persist a single uploaded audio file as a recording.
*/
private function storeUploadedRecording(
Request $request,
UploadedFile $file,
Mp3MetadataService $metadata,
string $contentHash,
@@ -185,6 +194,7 @@ class RecordingController extends Controller
?: ($tags['title'] ?? pathinfo($file->getClientOriginalName(), PATHINFO_FILENAME));
$recording = Recording::create([
'user_id' => $request->user()->id,
'title' => $title,
'original_filename' => $file->getClientOriginalName(),
'file_path' => $path,
@@ -5,6 +5,7 @@ namespace App\Http\Controllers;
use App\Http\Requests\TranscribeRecordingRequest;
use App\Models\Recording;
use Illuminate\Http\RedirectResponse;
use Illuminate\Support\Facades\Gate;
class TranscribeController extends Controller
{
@@ -15,6 +16,8 @@ class TranscribeController extends Controller
*/
public function __invoke(TranscribeRecordingRequest $request, Recording $recording): RedirectResponse
{
Gate::authorize('transcribe', $recording);
$recording->queueLocalTranscription();
return back()->with('success', 'Transcription started. Progress updates below.');
@@ -4,17 +4,18 @@ namespace App\Http\Controllers;
use App\Models\Recording;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
class TranscribePendingController extends Controller
{
/**
* Queue local transcription for recordings that still need a transcript.
*/
public function __invoke(): RedirectResponse
public function __invoke(Request $request): RedirectResponse
{
$queued = 0;
Recording::query()
$request->user()->recordings()
->whereIn('transcription_status', ['pending', 'failed', 'cancelled'])
->orderBy('id')
->each(function (Recording $recording) use (&$queued): void {
@@ -4,6 +4,7 @@ namespace App\Http\Controllers;
use App\Models\Recording;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Gate;
class TranscriptionStatusController extends Controller
{
@@ -12,6 +13,8 @@ class TranscriptionStatusController extends Controller
*/
public function __invoke(Recording $recording): JsonResponse
{
Gate::authorize('view', $recording);
$recording = $recording->fresh();
if ($recording->recoverOrphanedTranscription()) {
+1 -1
View File
@@ -37,7 +37,7 @@ class StoreRecordingRequest extends FormRequest
public function authorize(): bool
{
return true;
return $this->user() !== null;
}
/**
@@ -8,7 +8,9 @@ class TranscribeRecordingRequest extends FormRequest
{
public function authorize(): bool
{
return true;
$recording = $this->route('recording');
return $recording !== null && $this->user()?->can('transcribe', $recording) === true;
}
/**