support for passworded files
This commit is contained in:
@@ -11,29 +11,6 @@ use Illuminate\Support\Facades\Validator;
|
|||||||
|
|
||||||
class MediaController extends Controller
|
class MediaController extends Controller
|
||||||
{
|
{
|
||||||
/**
|
|
||||||
* The disk to store public media.
|
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
private static $publicStorageDisk = 'public';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The disk to store temporary media.
|
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
private static $tempStorageDisk = 'temp';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Media preprocessors.
|
|
||||||
*
|
|
||||||
* @var array
|
|
||||||
*/
|
|
||||||
private static $preProcessors = [
|
|
||||||
\App\MediaServices\Converters\HEICToJPEG::class,
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Display a listing of the resource.
|
* Display a listing of the resource.
|
||||||
*/
|
*/
|
||||||
@@ -316,6 +293,18 @@ class MediaController extends Controller
|
|||||||
// $mediaData['hash'] = $hash;
|
// $mediaData['hash'] = $hash;
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
if($request->get('password_clear') === 'on') {
|
||||||
|
$mediaData['password'] = null;
|
||||||
|
} else {
|
||||||
|
$password = $request->get('password');
|
||||||
|
|
||||||
|
if($password !== null && $password !== '') {
|
||||||
|
$mediaData['password'] = password_hash($request->get('password'), PASSWORD_DEFAULT);
|
||||||
|
} else {
|
||||||
|
unset($mediaData['password']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$media->update($mediaData);
|
$media->update($mediaData);
|
||||||
|
|
||||||
// if($file) {
|
// if($file) {
|
||||||
@@ -421,6 +410,26 @@ class MediaController extends Controller
|
|||||||
abort(404, 'File not found');
|
abort(404, 'File not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if($media->password !== null) {
|
||||||
|
if(!$request->has('password')) {
|
||||||
|
return view('media-password');
|
||||||
|
} else {
|
||||||
|
$password = $request->get('password');
|
||||||
|
|
||||||
|
if($password === '' || $password === null) {
|
||||||
|
return view('media-password', [
|
||||||
|
'error' => 'Password is required',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!password_verify(base64_decode($password), $media->password)) {
|
||||||
|
return view('media-password', [
|
||||||
|
'error' => 'Password is incorrect',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$variant = '';
|
$variant = '';
|
||||||
$download = false;
|
$download = false;
|
||||||
$variants = array_keys($media->getVariantTypes());
|
$variants = array_keys($media->getVariantTypes());
|
||||||
|
|||||||
@@ -23,7 +23,8 @@ class Media extends Model
|
|||||||
'mime_type',
|
'mime_type',
|
||||||
'size',
|
'size',
|
||||||
'user_id',
|
'user_id',
|
||||||
'hash'
|
'hash',
|
||||||
|
'password',
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -53,7 +54,8 @@ class Media extends Model
|
|||||||
* @var array
|
* @var array
|
||||||
*/
|
*/
|
||||||
protected $casts = [
|
protected $casts = [
|
||||||
'variants' => 'array'
|
'variants' => 'array',
|
||||||
|
'password' => 'hashed'
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('media', function (Blueprint $table) {
|
||||||
|
$table->string('password')->nullable();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('media', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('password');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,3 +1,10 @@
|
|||||||
|
@php
|
||||||
|
$password = '';
|
||||||
|
if(isset($medium) && ($medium->password !== null && $medium->password !== '')) {
|
||||||
|
$password = 'yes';
|
||||||
|
}
|
||||||
|
@endphp
|
||||||
|
|
||||||
<x-layout>
|
<x-layout>
|
||||||
<x-mast backRoute="admin.media.index" backTitle="Media">{{ isset($medium) ? 'Edit' : 'Create' }} Media</x-mast>
|
<x-mast backRoute="admin.media.index" backTitle="Media">{{ isset($medium) ? 'Edit' : 'Create' }} Media</x-mast>
|
||||||
<x-container class="mt-4">
|
<x-container class="mt-4">
|
||||||
@@ -16,6 +23,10 @@
|
|||||||
</div>
|
</div>
|
||||||
@endisset
|
@endisset
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<x-ui.password label="Password" name="password" value="{{ $password }}"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<x-ui.file name="file" onchange="updateTitle" file-name="{{ $medium->name ?? '' }}" file-type="{{ $medium->mime_type ?? '' }}" file-size="{{ $medium->size ?? '' }}" file-url="{{ $medium?->thumbnail ?? '' }}" readonly="{{ isset($medium) }}" />
|
<x-ui.file name="file" onchange="updateTitle" file-name="{{ $medium->name ?? '' }}" file-type="{{ $medium->mime_type ?? '' }}" file-size="{{ $medium->size ?? '' }}" file-url="{{ $medium?->thumbnail ?? '' }}" readonly="{{ isset($medium) }}" />
|
||||||
|
|
||||||
<div class="flex justify-end gap-4 mt-8">
|
<div class="flex justify-end gap-4 mt-8">
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
'outline' => 'hover:bg-gray-500 focus-visible:outline-primary-color text-gray-800 border border-gray-400 bg-white hover:text-white',
|
'outline' => 'hover:bg-gray-500 focus-visible:outline-primary-color text-gray-800 border border-gray-400 bg-white hover:text-white',
|
||||||
'primary' => 'hover:bg-primary-color-dark focus-visible:outline-primary-color bg-primary-color text-white',
|
'primary' => 'hover:bg-primary-color-dark focus-visible:outline-primary-color bg-primary-color text-white',
|
||||||
'primary-outline' => 'hover:bg-primary-color-dark focus-visible:outline-primary-color text-primary-color border border-primary-color bg-white hover:text-white',
|
'primary-outline' => 'hover:bg-primary-color-dark focus-visible:outline-primary-color text-primary-color border border-primary-color bg-white hover:text-white',
|
||||||
|
'primary-outline-sm' => '!font-normal !text-xs !px-4 !py-1 hover:bg-primary-color-dark focus-visible:outline-primary-color text-primary-color border border-primary-color bg-white hover:text-white',
|
||||||
'danger' => 'hover:bg-danger-color-dark focus-visible:outline-danger-color bg-danger-color text-white',
|
'danger' => 'hover:bg-danger-color-dark focus-visible:outline-danger-color bg-danger-color text-white',
|
||||||
'success' => 'hover:bg-success-color-dark focus-visible:outline-success-color bg-success-color text-white'
|
'success' => 'hover:bg-success-color-dark focus-visible:outline-success-color bg-success-color text-white'
|
||||||
][$color];
|
][$color];
|
||||||
|
|||||||
@@ -1,4 +1,13 @@
|
|||||||
@props(['name', 'label', 'checked' => false])
|
@props(['name', 'label', 'checked' => false, 'small' => false])
|
||||||
|
|
||||||
|
@php
|
||||||
|
$checkBoxClasses = '';
|
||||||
|
$labelClasses = '';
|
||||||
|
if($small) {
|
||||||
|
$checkBoxClasses = 'h-6 w-6 rounded-md text-xs';
|
||||||
|
$labelClasses = 'pl-1 text-xs';
|
||||||
|
}
|
||||||
|
@endphp
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
input[type="checkbox"]:checked {
|
input[type="checkbox"]:checked {
|
||||||
@@ -9,15 +18,24 @@
|
|||||||
width: 0.6rem;
|
width: 0.6rem;
|
||||||
border-width: 0 0.25rem 0.25rem 0;
|
border-width: 0 0.25rem 0.25rem 0;
|
||||||
border-style: solid;
|
border-style: solid;
|
||||||
border-color: #000;
|
border-color: #0370A1;
|
||||||
transform: rotate(40deg) translateY(-0.2rem) translateX(0.65rem);
|
transform: rotate(40deg) translateY(-0.2rem) translateX(0.65rem);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input.text-xs[type="checkbox"]:checked {
|
||||||
|
&:after {
|
||||||
|
height: 1rem;
|
||||||
|
width: 0.55rem;
|
||||||
|
border-width: 0 0.225rem 0.225rem 0;
|
||||||
|
transform: rotate(40deg) translateY(-0.2rem) translateX(0.4rem);
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<div class="mb-4">
|
<div class="{{ twMerge(['mb-4'], $attributes->get('class')) }}">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<input class="{{ twMerge(['bg-white','mt-1','h-8','w-8','rounded-lg','border','border-gray-300','appearance-none','focus:outline-none','focus:ring-0','focus:border-blue-600','peer','focus:ring-indigo-300'], $classes ?? '') }}" type="checkbox" {{ $checked ? 'checked' : '' }} id="{{ $name }}" name="{{ $name }}" {{ $attributes }} />
|
<input class="{{ twMerge(['bg-white','mt-1','h-8','w-8','rounded-lg','border','border-gray-300','appearance-none','focus:outline-none','focus:ring-0','focus:border-blue-600','peer','focus:ring-indigo-300'], $checkBoxClasses ?? '') }}" type="checkbox" {{ $checked ? 'checked' : '' }} id="{{ $name }}" name="{{ $name }}" {{ $attributes }} />
|
||||||
<label for="{{ $name }}" class="text-sm pl-2 pt-1">{{ $label }}</label>
|
<label for="{{ $name }}" class="{{ twMerge(['text-sm','pl-2','pt-1'], $labelClasses ?? '') }}">{{ $label }}</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
37
resources/views/components/ui/password.blade.php
Normal file
37
resources/views/components/ui/password.blade.php
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
@props(['name', 'label' => '', 'value' => '', 'error' => null])
|
||||||
|
|
||||||
|
@php
|
||||||
|
if($error === null) {
|
||||||
|
$error = $errors->first($name);
|
||||||
|
}
|
||||||
|
|
||||||
|
$hasError = $error !== '';
|
||||||
|
$classes = 'disabled:bg-gray-100 bg-white block px-2.5 pb-2.5 w-full text-sm text-gray-900 rounded-lg border appearance-nonefocus:outline-none focus:ring-0 focus:border-blue-600 ' . ($hasError ? 'border-red-600 ring-red-600 focus:border-red-600 focus:ring-red-600' : 'border-gray-300 focus:border-indigo-300 focus:ring-indigo-300');
|
||||||
|
|
||||||
|
$label = ($value === '' ? '' : 'Change ') . $label;
|
||||||
|
@endphp
|
||||||
|
|
||||||
|
<div class="{{ twMerge(['mb-4'], $attributes->get('class')) }}">
|
||||||
|
<div>
|
||||||
|
<label for="{{ $name }}" class="block text-sm pl-1 pr-4">
|
||||||
|
{{ $label }}
|
||||||
|
@if($value !== '')
|
||||||
|
<span class="ml-3 text-xs text-gray-500">(Password has been set)</span>
|
||||||
|
@else
|
||||||
|
<span class="ml-3 text-xs text-gray-500">(No password has been set)</span>
|
||||||
|
@endif
|
||||||
|
</label>
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<input class="{{ twMerge(['pt-2.5','mt-1'], $classes) }}" autocomplete="off" placeholder=" " value="" type="password" id="{{ $name }}_password" name="{{ $name }}" {{ $attributes }} />
|
||||||
|
@if($value !== '')
|
||||||
|
<x-ui.checkbox label="Clear" id="{{ $name }}_clear" name="{{ $name }}_clear" class="mb-0" small="true" />
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@if(isset($info) && $info !== '')
|
||||||
|
<div class="text-xs text-gray-500 ml-2 mt-1">{{ $info }}</div>
|
||||||
|
@endif
|
||||||
|
@if ($hasError)
|
||||||
|
<div class="text-xs text-red-600 ml-2 mt-2">{{ $error }}</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
55
resources/views/media-password.blade.php
Normal file
55
resources/views/media-password.blade.php
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
<x-layout :bodyClass="'image-background'">
|
||||||
|
@props(['formmethod' => 'POST', 'formaction' => ''])
|
||||||
|
<div class="flex items-center justify-center flex-grow py-24" x-data>
|
||||||
|
<div x-show="$store.request.show" class="w-full mx-2 max-w-lg p-8 pb-6 bg-white rounded-md shadow-deep">
|
||||||
|
<h2 class="text-2xl font-bold mb-4 text-center">Password Required</h2>
|
||||||
|
<div class="flex justify-center gap-4 mb-4">
|
||||||
|
A password is required to access this file.
|
||||||
|
</div>
|
||||||
|
<form method="GET" action="" x-on:submit.prevent="submit">
|
||||||
|
<x-ui.input type="password" name="password" label="Password" floating autofocus error="{{ $error ?? '' }}"/>
|
||||||
|
<div class="flex flex-col items-center gap-4 justify-center">
|
||||||
|
<x-ui.button type="submit">Download</x-ui.button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div x-show="!$store.request.show" x-cloak class="w-full mx-2 max-w-lg p-8 pb-6 bg-white rounded-md shadow-deep">
|
||||||
|
<h2 class="text-2xl font-bold mb-4 text-center">Requesting file</h2>
|
||||||
|
<div class="flex justify-center mb-8">
|
||||||
|
The file has been requested from the server...
|
||||||
|
</div>
|
||||||
|
<div x-show="!$store.request.home" class="flex justify-center">
|
||||||
|
<img src="/loading.gif" class="h-32 w-32" alt="Please wait" />
|
||||||
|
</div>
|
||||||
|
<div x-show="$store.request.home" class="flex justify-between gap-4">
|
||||||
|
<x-ui.button type="link" color="primary-outline" href="#" x-on:click="$store.request.show=true">Try again</x-ui.button>
|
||||||
|
<x-ui.button type="link" href="{{ route('index') }}">Home</x-ui.button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</x-layout>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('alpine:init', () => {
|
||||||
|
Alpine.store('request', {
|
||||||
|
show: true,
|
||||||
|
home: false
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function submit(e) {
|
||||||
|
const password = document.querySelector('input[name="password"]').value;
|
||||||
|
const url = new URL(window.location.href);
|
||||||
|
url.searchParams.set('password', btoa(password));
|
||||||
|
window.location.href = url.href;
|
||||||
|
|
||||||
|
window.setTimeout(() => {
|
||||||
|
Alpine.store('request').show = false;
|
||||||
|
Alpine.store('request').home = false;
|
||||||
|
|
||||||
|
window.setTimeout(() => {
|
||||||
|
Alpine.store('request').home = true;
|
||||||
|
}, 4000);
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
Reference in New Issue
Block a user