Rewrite with Eloquent ORM JSON

This commit is contained in:
Axel 2023-05-22 17:28:26 +02:00
parent 52059e110d
commit 51a8831b98
Signed by: axel
GPG key ID: 73C0A5961B6BC740
35 changed files with 744 additions and 460 deletions

View file

@ -4,7 +4,6 @@ APP_KEY=
APP_DEBUG=false APP_DEBUG=false
APP_URL= APP_URL=
APP_TIMEZONE=Europe/Paris APP_TIMEZONE=Europe/Paris
APP_LOCALE=en
UPLOAD_MAX_FILESIZE=1G UPLOAD_MAX_FILESIZE=1G
UPLOAD_MAX_FILES=1000 UPLOAD_MAX_FILES=1000

View file

@ -2,6 +2,7 @@
namespace App\Console\Commands; namespace App\Console\Commands;
use App\Models\User;
use Exception; use Exception;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Hash;
@ -42,8 +43,8 @@ class CreateUser extends Command
goto login; goto login;
} }
// Checking login unicity $existing = User::find($login);
if (Storage::disk('users')->exists($login.'.json')) { if (! empty($existing) && $existing->count() > 0) {
$this->error('User "'.$login.'" already exists'); $this->error('User "'.$login.'" already exists');
unset($login); unset($login);
goto login; goto login;
@ -53,18 +54,17 @@ class CreateUser extends Command
// Asking for user's password // Asking for user's password
$password = $this->secret('Enter the user\'s password'); $password = $this->secret('Enter the user\'s password');
if (! preg_match('~^.{4,100}$i~', $password)) { if (! preg_match('~^[^\s]{5,100}$~', $password)) {
$this->error('Invalid password format. Must contains between 5 and 100 chars'); $this->error('Invalid password format. Must contains between 5 and 100 chars without space');
unset($password); unset($password);
goto password; goto password;
} }
try { try {
Storage::disk('users')->put($login.'.json', json_encode([ User::create([
'username' => $login, 'username' => $login,
'password' => Hash::make($password), 'password' => Hash::make($password)
'bundles' => [] ]);
]));
$this->info('User has been created'); $this->info('User has been created');
} }

View file

@ -5,8 +5,9 @@ namespace App\Helpers;
use Exception; use Exception;
use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
use App\Models\User;
class User { class Auth {
static function isLogged():Bool { static function isLogged():Bool {
// Checking credentials auth // Checking credentials auth
@ -27,7 +28,7 @@ class User {
$user = self::getUserDetails($username); $user = self::getUserDetails($username);
// Checking password // Checking password
if (true !== Hash::check($password, $user['password'])) { if (true !== Hash::check($password, $user->password)) {
throw new Exception('Invalid password'); throw new Exception('Invalid password');
} }
@ -41,30 +42,19 @@ class User {
} }
} }
static function getLoggedUserDetails():Array { static function getLoggedUserDetails():User {
if (self::isLogged()) { if (self::isLogged()) {
return self::getUserDetails(session()->get('username')); return self::getUserDetails(session()->get('username'));
} }
throw new UnauthenticatedUser('User is not logged in'); throw new UnauthenticatedUser('User is not logged in');
} }
static function getUserDetails(String $username):Array { static function getUserDetails(String $username):User {
$user = User::find($username);
// Checking user existence if (empty($user)) {
if (Storage::disk('users')->missing($username.'.json')) {
throw new Exception('No such user'); throw new Exception('No such user');
} }
// Getting user.json
if (! $json = Storage::disk('users')->get($username.'.json')) {
throw new Exception('Could not fetch user details');
}
// Decoding JSON
if (! $user = json_decode($json, true)) {
throw new Exception('Cannot decode JSON file');
}
return $user; return $user;
} }

View file

@ -70,7 +70,7 @@ class Upload {
return $metadata; return $metadata;
} }
public static function humanFilesize(Float $size, Int $precision = 2):Int { public static function humanFilesize(Float $size, Int $precision = 2):String {
if ($size > 0) { if ($size > 0) {
$size = (int) $size; $size = (int) $size;
$base = log($size) / log(1024); $base = log($size) / log(1024);

View file

@ -4,31 +4,20 @@ namespace App\Http\Controllers;
use ZipArchive; use ZipArchive;
use Exception; use Exception;
use Carbon\Carbon;
use App\Helpers\Upload; use App\Helpers\Upload;
use App\Models\Bundle;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use App\Http\Resources\BundleResource;
class BundleController extends Controller class BundleController extends Controller
{ {
// The bundle content preview // The bundle content preview
public function previewBundle(Request $request, $bundleId) { public function previewBundle(Request $request, Bundle $bundle) {
// Getting bundle metadata
abort_if(! $metadata = Upload::getMetadata($bundleId), 404);
// Handling dates as Carbon
Carbon::setLocale(config('app.locale'));
$metadata['created_at_carbon'] = Carbon::createFromTimestamp($metadata['created_at']);
$metadata['expires_at_carbon'] = Carbon::createFromTimestamp($metadata['expires_at']);
return view('download', [ return view('download', [
'bundleId' => $bundleId, 'bundle' => new BundleResource($bundle)
'metadata' => $metadata,
'auth' => $metadata['preview_token']
]); ]);
} }
@ -36,23 +25,18 @@ class BundleController extends Controller
// The download method // The download method
// - the bundle // - the bundle
// - or just one file // - or just one file
public function downloadZip(Request $request, $bundleId) { public function downloadZip(Request $request, Bundle $bundle) {
// Getting bundle metadata
abort_if(! $metadata = Upload::getMetadata($bundleId), 404);
try { try {
// Download of the full bundle // Download of the full bundle
// We must create a Zip archive // We must create a Zip archive
Upload::setMetadata($bundleId, [ $bundle->downloads ++;
'downloads' => $metadata['downloads'] + 1 $bundle->save();
]);
$filename = config('filesystems.disks.uploads.root').'/'.$metadata['bundle_id'].'/bundle.zip';
if (1 == 1 || ! file_exists($filename)) { $filename = Storage::disk('uploads')->path('').'/'.$bundle->slug.'/bundle.zip';
// Timestamped filename if (! file_exists($filename)) {
$bundlezip = fopen($filename, 'w'); $bundlezip = fopen($filename, 'w');
//chmod($filename, 0600);
// Creating the archive // Creating the archive
$zip = new ZipArchive; $zip = new ZipArchive;
@ -61,14 +45,14 @@ class BundleController extends Controller
} }
// Setting password when required // Setting password when required
if (! empty($metadata['password'])) { if (! empty($bundle->password)) {
$zip->setPassword($metadata['password']); $zip->setPassword($bundle->password);
} }
// Adding the files into the Zip with their real names // Adding the files into the Zip with their real names
foreach ($metadata['files'] as $k => $file) { foreach ($bundle->files as $k => $file) {
if (file_exists(config('filesystems.disks.uploads.root').'/'.$file['fullpath'])) { if (file_exists(config('filesystems.disks.uploads.root').'/'.$file->fullpath)) {
$name = $file['original']; $name = $file->original;
// If a file in the archive has the same name // If a file in the archive has the same name
if (false !== $zip->locateName($name)) { if (false !== $zip->locateName($name)) {
@ -89,9 +73,9 @@ class BundleController extends Controller
$name = $newname; $name = $newname;
} }
// Finally adding files // Finally adding files
$zip->addFile(config('filesystems.disks.uploads.root').'/'.$file['fullpath'], $name); $zip->addFile(config('filesystems.disks.uploads.root').'/'.$file->fullpath, $name);
if (! empty($metadata['password'])) { if (! empty($bundle->password)) {
$zip->setEncryptionIndex($k, ZipArchive::EM_AES_256); $zip->setEncryptionIndex($k, ZipArchive::EM_AES_256);
} }
} }
@ -116,16 +100,17 @@ class BundleController extends Controller
$limit_rate = $filesize; $limit_rate = $filesize;
} }
// Flushing everything
flush();
// Let's download now // Let's download now
header('Content-Type: application/octet-stream'); header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="'.Str::slug($metadata['title']).'-'.time().'.zip'.'"'); header('Content-Disposition: attachment; filename="'.Str::slug($bundle->title).'-'.time().'.zip'.'"');
header('Cache-Control: no-cache, must-revalidate'); header('Cache-Control: no-cache, must-revalidate');
header('Expires: Sat, 26 Jul 1997 05:00:00 GMT'); header('Expires: Sat, 26 Jul 1997 05:00:00 GMT');
header('Content-Length: '.$filesize); header('Content-Length: '.$filesize);
flush(); // Downloading
$fh = fopen($filename, 'rb'); $fh = fopen($filename, 'rb');
while (! feof($fh)) { while (! feof($fh)) {
echo fread($fh, round($limit_rate)); echo fread($fh, round($limit_rate));

View file

@ -8,44 +8,44 @@ use Illuminate\Http\Request;
use Illuminate\Support\Facades\Session; use Illuminate\Support\Facades\Session;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use App\Http\Resources\BundleResource;
use App\Http\Resources\FileResource;
use App\Models\Bundle;
use App\Models\File;
class UploadController extends Controller class UploadController extends Controller
{ {
public function createBundle(Request $request, String $bundleId = null) { public function createBundle(Request $request, Bundle $bundle) {
$metadata = Upload::getMetadata($bundleId);
abort_if(empty($metadata), 404);
return view('upload', [ return view('upload', [
'metadata' => $metadata ?? null, 'bundle' => $bundle->toArray(),
'baseUrl' => config('app.url') 'baseUrl' => config('app.url')
]); ]);
} }
public function getMetadata(Request $request, String $bundleId) {
return response()->json(Upload::getMetadata($bundleId));
}
// The upload form // The upload form
public function storeBundle(Request $request, String $bundleId) { public function storeBundle(Request $request, Bundle $bundle) {
$metadata = [ try {
'expiry' => $request->expiry ?? null, $bundle->update([
'password' => $request->password ?? null, 'expiry' => $request->expiry ?? null,
'title' => $request->title ?? null, 'password' => $request->password ?? null,
'description' => $request->description ?? null, 'title' => $request->title ?? null,
'max_downloads' => $request->max_downloads ?? 0 'description' => $request->description ?? null,
]; 'max_downloads' => $request->max_downloads ?? 0
]);
$metadata = Upload::setMetaData($bundleId, $metadata); return response()->json(new BundleResource($bundle));
}
// Creating the bundle folder catch (Exception $e) {
Storage::disk('uploads')->makeDirectory($bundleId); return response()->json([
'result' => false,
return response()->json($metadata); 'message' => $e->getMessage()
], 500);
}
} }
public function uploadFile(Request $request, String $bundleId) { public function uploadFile(Request $request, Bundle $bundle) {
// Validating form data // Validating form data
$request->validate([ $request->validate([
@ -59,22 +59,23 @@ class UploadController extends Controller
// Moving file to final destination // Moving file to final destination
try { try {
$fullpath = $request->file('file')->storeAs( $size = $request->file->getSize();
$bundleId, $filename, 'uploads'
);
$size = Storage::disk('uploads')->size($fullpath);
if (config('sharing.upload_prevent_duplicates', true) === true && $size < Upload::humanReadableToBytes(config('sharing.hash_maxfilesize', '1G'))) { if (config('sharing.upload_prevent_duplicates', true) === true && $size < Upload::humanReadableToBytes(config('sharing.hash_maxfilesize', '1G'))) {
$hash = sha1_file(Storage::disk('uploads')->path($fullpath)); $hash = sha1_file($request->file->getPathname());
if (Upload::isDuplicateFile($bundleId, $hash)) {
Storage::disk('uploads')->delete($fullpath); $existing = $bundle->files->whereNotNull('hash')->where('hash', $hash)->count();
if (! empty($existing) && $existing > 0) {
throw new Exception(__('app.duplicate-file')); throw new Exception(__('app.duplicate-file'));
} }
} }
$fullpath = $request->file('file')->storeAs(
$bundle->slug, $filename, 'uploads'
);
// Generating file metadata // Generating file metadata
$file = [ $file = new File([
'uuid' => $request->uuid, 'uuid' => $request->uuid,
'bundle_slug' => $bundle->slug,
'original' => $original, 'original' => $original,
'filesize' => $size, 'filesize' => $size,
'fullpath' => $fullpath, 'fullpath' => $fullpath,
@ -82,78 +83,72 @@ class UploadController extends Controller
'created_at' => time(), 'created_at' => time(),
'status' => true, 'status' => true,
'hash' => $hash ?? null 'hash' => $hash ?? null
];
$metadata = Upload::addFileMetaData($bundleId, $file);
return response()->json([
'result' => true,
'file' => $file
]); ]);
$file->save();
return response()->json(new FileResource($file));
} }
catch (Exception $e) { catch (Exception $e) {
return response()->json([ return response()->json([
'result' => false, 'result' => false,
'error' => $e->getMessage(), 'message' => $e->getMessage()
'file' => $e->getFile(),
'line' => $e->getLine()
], 500); ], 500);
} }
} }
public function deleteFile(Request $request, String $bundleId) { public function deleteFile(Request $request, Bundle $bundle) {
$request->validate([ $request->validate([
'uuid' => 'required|uuid' 'uuid' => 'required|uuid'
]); ]);
try { try {
$metadata = Upload::deleteFile($bundleId, $request->uuid); // Getting file model
return response()->json($metadata); $file = File::findOrFail($request->uuid);
// Physically deleting the file
if (! Storage::disk('uploads')->delete($file->fullpath)) {
throw new Exception('Cannot delete file from disk');
}
// Destroying the model
$file->delete();
return response()->json(new BundleResource($bundle));
} }
catch (Exception $e) { catch (Exception $e) {
return response()->json([ return response()->json([
'result' => false, 'result' => false,
'error' => $e->getMessage(), 'message' => $e->getMessage()
'file' => $e->getFile(),
'line' => $e->getLine()
], 500); ], 500);
} }
} }
public function completeBundle(Request $request, String $bundleId) { public function completeBundle(Request $request, Bundle $bundle) {
$metadata = Upload::getMetadata($bundleId);
// Processing size // Processing size
if (! empty($metadata['files'])) { $size = 0;
$size = 0; foreach ($bundle->files as $f) {
foreach ($metadata['files'] as $f) { $size += $f['filesize'];
$size += $f['filesize'];
}
} }
// Saving metadata // Saving metadata
try { try {
$preview_token = substr(sha1(uniqid('dbdl', true)), 0, rand(10, 15)); $bundle->completed = true;
$bundle->expires_at = time()+$bundle->expiry;
$bundle->fullsize = $size;
$bundle->preview_link = route('bundle.preview', ['bundle' => $bundle, 'auth' => $bundle->preview_token]);
$bundle->download_link = route('bundle.zip.download', ['bundle' => $bundle, 'auth' => $bundle->preview_token]);
$bundle->deletion_link = route('upload.bundle.delete', ['bundle' => $bundle]);
$bundle->save();
$metadata = Upload::setMetadata($bundleId, [ return response()->json(new BundleResource($bundle));
'completed' => true,
'expires_at' => time()+$metadata['expiry'],
'fullsize' => $size,
'preview_token' => $preview_token,
'preview_link' => route('bundle.preview', ['bundle' => $bundleId, 'auth' => $preview_token]),
'download_link' => route('bundle.zip.download', ['bundle' => $bundleId, 'auth' => $preview_token]),
'deletion_link' => route('upload.bundle.delete', ['bundle' => $bundleId])
]);
return response()->json($metadata);
} }
catch (\Exception $e) { catch (\Exception $e) {
return response()->json([ return response()->json([
'result' => false, 'result' => false,
'error' => $e->getMessage() 'message' => $e->getMessage()
], 500); ], 500);
} }
} }
@ -164,23 +159,25 @@ class UploadController extends Controller
* We invalidate the expiry date and let the CRON * We invalidate the expiry date and let the CRON
* task do the hard work * task do the hard work
*/ */
public function deleteBundle(Request $request, $bundleId) { public function deleteBundle(Request $request, Bundle $bundle) {
// Tries to get the metadata file
$metadata = Upload::getMetadata($bundleId);
// Forcing file to expire
$metadata['expires_at'] = time() - (3600 * 24 * 30);
// Rewriting the metadata file
try { try {
$metadata = Upload::setMetadata($bundleId, $metadata); // Forcing bundle to expire
return response()->json($metadata); $bundle->expires_at = time() - (3600 * 24 * 30);
$bundle->save();
// Then deleting file models
foreach ($bundle->files as $f) {
$f->forceDelete();
}
return response()->json(new BundleResource($bundle));
} }
catch (Exception $e) { catch (Exception $e) {
return response()->json([ return response()->json([
'success' => false 'success' => false,
]); 'message' => $e->getMessage()
], 500);
} }
} }

View file

@ -1,16 +1,26 @@
<?php <?php
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Helpers\Upload; use App\Helpers\Auth;
use App\Helpers\User;
use Exception; use Exception;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use App\Http\Resources\BundleResource;
use App\Models\Bundle;
class WebController extends Controller class WebController extends Controller
{ {
public function homepage() public function homepage()
{ {
return view('homepage'); // Getting user bundles
if (Auth::isLogged()) {
$bundles = Auth::getLoggedUserDetails()->bundles;
if (! empty($bundles) && $bundles->count() > 0) {
$bundles = BundleResource::collection($bundles) ;
}
}
return view('homepage', [
'bundles' => $bundles ?? []
]);
} }
public function login() { public function login() {
@ -26,7 +36,7 @@ class WebController extends Controller
]); ]);
try { try {
if (true === User::loginUser($request->login, $request->password)) { if (true === Auth::loginUser($request->login, $request->password)) {
return response()->json([ return response()->json([
'result' => true, 'result' => true,
]); ]);
@ -35,63 +45,55 @@ class WebController extends Controller
catch (Exception $e) { catch (Exception $e) {
return response()->json([ return response()->json([
'result' => false, 'result' => false,
'error' => 'Authentication failed, please try again.' 'message' => 'Authentication failed, please try again.'
], 403); ], 403);
} }
// This should never happen // This should never happen
return response()->json([ return response()->json([
'result' => false, 'result' => false,
'error' => 'Unexpected error' 'message' => 'Unexpected error'
]); ], 500);
} }
function newBundle(Request $request) { function newBundle(Request $request) {
// Aborting if request is not AJAX // Aborting if request is not AJAX
abort_if(! $request->ajax(), 403); abort_if(! $request->ajax(), 403);
$request->validate([ if (Auth::isLogged()) {
'bundle_id' => 'required', $user = Auth::getLoggedUserDetails();
'owner_token' => 'required'
]);
$owner = null;
if (User::isLogged()) {
$user = User::getLoggedUserDetails();
$owner = $user['username'];
// If bundle dimension is not initialized
if (empty($user['bundles']) || ! is_array($user['bundles'])) {
$user['bundles'] = [];
}
array_push($user['bundles'], $request->bundle_id);
User::setUserDetails($user['username'], $user);
} }
$metadata = [ try {
'owner' => $owner, $bundle = new Bundle([
'created_at' => time(), 'user_username' => $user->username ?? null,
'completed' => false, 'created_at' => time(),
'expiry' => config('sharing.default-expiry', 86400), 'completed' => false,
'expires_at' => null, 'expiry' => config('sharing.default-expiry', 86400),
'password' => null, 'expires_at' => null,
'bundle_id' => $request->bundle_id, 'password' => null,
'owner_token' => $request->owner_token, 'slug' => substr(sha1(uniqid('slug_', true)), 0, rand(35, 40)),
'preview_token' => null, 'owner_token' => substr(sha1(uniqid('preview_', true)), 0, 15),
'fullsize' => 0, 'preview_token' => substr(sha1(uniqid('preview_', true)), 0, 15),
'files' => [], 'fullsize' => 0,
'title' => null, 'title' => null,
'description' => null, 'description' => null,
'max_downloads' => 0, 'max_downloads' => 0,
'downloads' => 0 'downloads' => 0
]; ]);
$bundle->save();
if (Upload::setMetadata($metadata['bundle_id'], $metadata)) { return response()->json([
return response()->json($metadata); 'result' => true,
'redirect' => route('upload.create.show', ['bundle' => $bundle->slug]),
'bundle' => new BundleResource($bundle)
]);
} }
else { catch (Exception $e) {
abort(500); return response()->json([
'result' => false,
'message' => $e->getMessage()
], 500);
} }
} }
} }

View file

@ -21,6 +21,7 @@ class Kernel extends HttpKernel
\Illuminate\Foundation\Http\Middleware\ValidatePostSize::class, \Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
\App\Http\Middleware\TrimStrings::class, \App\Http\Middleware\TrimStrings::class,
\Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class, \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
\App\Http\Middleware\Localisation::class
]; ];
/** /**

View file

@ -6,6 +6,8 @@ use Closure;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use App\Helpers\Upload; use App\Helpers\Upload;
use App\Models\Bundle;
use Carbon\Carbon;
class GuestAccess class GuestAccess
{ {
@ -17,27 +19,23 @@ class GuestAccess
public function handle(Request $request, Closure $next): Response public function handle(Request $request, Closure $next): Response
{ {
// Aborting if Bundle ID is not present // Aborting if Bundle ID is not present
abort_if(empty($request->route()->parameter('bundle')), 403); abort_if(empty($request->route()->parameter('bundle')), 404);
$bundle = $request->route()->parameters()['bundle'];
abort_if(! is_a($bundle, Bundle::class), 404);
// Aborting if Auth token is not provided
abort_if(empty($request->auth), 403); abort_if(empty($request->auth), 403);
// Getting metadata
$metadata = Upload::getMetadata($request->route()->parameter('bundle'));
// Aborting if metadata are empty
abort_if(empty($metadata), 404);
// Aborting if auth_token is different from URL param // Aborting if auth_token is different from URL param
abort_if($metadata['preview_token'] !== $request->auth, 403); abort_if($bundle->preview_token !== $request->auth, 403);
// Checking bundle expiration // Aborting if bundle expired
abort_if($metadata['expires_at'] < time(), 404); abort_if($bundle->expires_at->isBefore(Carbon::now()), 404);
// If there is no file into the bundle (should never happen but ...) // Aborting if max download is reached
abort_if(count($metadata['files']) == 0, 404); abort_if( ($bundle->max_downloads ?? 0) > 0 && $bundle->downloads >= $bundle->max_downloads, 404);
abort_if(($metadata['max_downloads'] ?? 0) > 0 && $metadata['downloads'] >= $metadata['max_downloads'], 404);
// Else resuming
return $next($request); return $next($request);
} }
} }

View file

@ -0,0 +1,34 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
use Illuminate\Support\Facades\App;
class Localisation
{
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next): Response
{
$locales = $request->header('accept-language');
if (! empty($locales)) {
if (preg_match_all('~([a-z]{2})[-|_]?~', $locales, $matches) && ! empty($matches[1])) {
$locales = array_unique($matches[1]);
foreach ($locales as $l) {
if (in_array($l, config('app.supported_locales'))) {
App::setLocale($l);
break;
}
}
}
}
return $next($request);
}
}

View file

@ -6,6 +6,7 @@ use Closure;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use App\Helpers\Upload; use App\Helpers\Upload;
use App\Models\Bundle;
class OwnerAccess class OwnerAccess
{ {
@ -21,6 +22,8 @@ class OwnerAccess
// Aborting if Bundle ID is not present // Aborting if Bundle ID is not present
abort_if(empty($request->route()->parameter('bundle')), 403); abort_if(empty($request->route()->parameter('bundle')), 403);
$bundle = $request->route()->parameters()['bundle'];
abort_if(! is_a($bundle, Bundle::class), 404);
// Aborting if auth is not present // Aborting if auth is not present
$auth = null; $auth = null;
@ -30,16 +33,11 @@ class OwnerAccess
else if (! empty($request->auth)) { else if (! empty($request->auth)) {
$auth = $request->auth; $auth = $request->auth;
} }
// Aborting if no auth token provided
abort_if(empty($auth), 403); abort_if(empty($auth), 403);
// Getting metadata // Aborting if owner token is wrong
$metadata = Upload::getMetadata($request->route()->parameter('bundle')); abort_if($bundle->owner_token !== $auth, 403);
// Aborting if metadata are empty
abort_if(empty($metadata), 404);
// Aborting if auth_token is different from URL param
abort_if($metadata['owner_token'] !== $auth, 403);
return $next($request); return $next($request);
} }

View file

@ -6,7 +6,7 @@ use Closure;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use App\Helpers\Upload; use App\Helpers\Upload;
use App\Helpers\User; use App\Helpers\Auth;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
class UploadAccess class UploadAccess
@ -26,7 +26,7 @@ class UploadAccess
} }
// Checking credentials auth // Checking credentials auth
if (User::isLogged()) { if (Auth::isLogged()) {
return $next($request); return $next($request);
} }

View file

@ -0,0 +1,55 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Support\Facades\Route;
use App\Http\Resources\UserResource;
use App\Http\Resources\FileResource;
class BundleResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
/**
Do not return private data on the preview page
*/
$full = false;
$middleware = Route::current()->gatherMiddleware('access.guest');
foreach ($middleware as $m) {
if ($m === 'access.owner') {
$full = true;
}
}
$response = [
'created_at' => $this->created_at,
'completed' => (bool)$this->completed,
'expiry' => (int)$this->expiry,
'expires_at' => $this->expires_at,
'slug' => $this->slug,
'fullsize' => (int)$this->fullsize,
'title' => $this->title,
'description' => $this->description,
'max_downloads' => (int)$this->max_downloads,
'downloads' => (int)$this->downloads,
'files' => FileResource::collection($this->files),
'preview_link' => $this->preview_link,
'download_link' => $this->download_link,
'password' => $this->when($full === true, $this->password),
'owner_token' => $this->when($full === true, $this->owner_token),
'preview_token' => $this->when($full === true, $this->preview_token),
'deletion_link' => $this->when($full === true, $this->deletion_link),
'user' => $this->when($full === true, new UserResource($this->user))
];
return $response;
}
}

View file

@ -0,0 +1,29 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class FileResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return [
'uuid' => $this->uuid,
'bundle_slug' => $this->bundle_slug,
'original' => $this->original,
'filesize' => (int)$this->filesize,
'fullpath' => $this->fullpath,
'filename' => $this->filename,
'created_at' => $this->created_at,
'status' => $this->status,
'hash' => $this->hash
];
}
}

View file

@ -0,0 +1,21 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class UserResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return [
'username' => $this->username
];
}
}

73
app/Models/Bundle.php Normal file
View file

@ -0,0 +1,73 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use \Orbit\Concerns\Orbital;
use Illuminate\Database\Schema\Blueprint;
class Bundle extends Model
{
use Orbital;
public $incrementing = false;
public $fillable = [
'user_username',
'created_at',
'completed',
'expiry',
'expires_at',
'password' ,
'slug',
'owner_token',
'preview_token',
'fullsize',
'title',
'description',
'max_downloads',
'downloads',
'preview_link',
'download_link',
'deletion_link'
];
protected $casts = [
'expires_at' => 'datetime:Y-m-d',
];
public function getKeyName() {
return 'slug';
}
public function getIncrementing() {
return false;
}
public static function schema(Blueprint $table) {
$table->string('slug');
$table->string('title')->nullable();
$table->longText('description')->nullable();
$table->string('password')->nullable();
$table->string('owner_token');
$table->string('preview_token');
$table->integer('fullsize')->default(0);
$table->integer('max_downloads')->nullable();
$table->integer('downloads')->default(0);
$table->boolean('completed')->default(false);
$table->integer('expiry')->default(0);
$table->timestamp('expires_at')->nullable();
$table->string('preview_link')->nullable();
$table->string('download_link')->nullable();
$table->string('deletion_link')->nullable();
$table->string('user_username')->nullable();
}
public function files() {
return $this->hasMany(File::class);
}
public function user() {
return $this->belongsTo(User::class);
}
}

53
app/Models/File.php Normal file
View file

@ -0,0 +1,53 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use \Orbit\Concerns\Orbital;
use Illuminate\Database\Schema\Blueprint;
class File extends Model
{
use Orbital;
public $fillable = [
'uuid',
'bundle_slug',
'original',
'filesize',
'fullpath',
'filename',
'created_at',
'status',
'hash'
];
public $incrementing = false;
public function getKeyName()
{
return 'uuid';
}
public function getIncrementing()
{
return false;
}
public static function schema(Blueprint $table)
{
$table->string('uuid');
$table->string('original')->nullable();
$table->string('filename')->nullable();
$table->string('status')->nullable();
$table->string('hash')->nullable();
$table->longText('fullpath')->nullable();
$table->boolean('filesize')->nullable();
$table->string('bundle_slug');
}
public function bundle() {
return $this->belongsTo(Bundle::class);
}
}

View file

@ -2,15 +2,16 @@
namespace App\Models; namespace App\Models;
// use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable; use \Orbit\Concerns\Orbital;
use Laravel\Sanctum\HasApiTokens; use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Hash;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;
class User extends Authenticatable class User extends Authenticatable
{ {
use HasApiTokens, HasFactory, Notifiable; use Orbital;
/** /**
* The attributes that are mass assignable. * The attributes that are mass assignable.
@ -18,8 +19,7 @@ class User extends Authenticatable
* @var array<int, string> * @var array<int, string>
*/ */
protected $fillable = [ protected $fillable = [
'name', 'username',
'email',
'password', 'password',
]; ];
@ -30,15 +30,30 @@ class User extends Authenticatable
*/ */
protected $hidden = [ protected $hidden = [
'password', 'password',
'remember_token',
]; ];
/**
* The attributes that should be cast. public $incrementing = false;
*
* @var array<string, string>
*/ public function getKeyName()
protected $casts = [ {
'email_verified_at' => 'datetime', return 'username';
]; }
public function getIncrementing()
{
return false;
}
public static function schema(Blueprint $table)
{
$table->string('username');
$table->string('password');
}
public function bundles() {
return $this->hasMany(Bundle::class);
}
} }

View file

@ -19,6 +19,6 @@ class AppServiceProvider extends ServiceProvider
*/ */
public function boot(): void public function boot(): void
{ {
//
} }
} }

View file

@ -24,6 +24,7 @@ class RouteServiceProvider extends ServiceProvider
*/ */
public function boot(): void public function boot(): void
{ {
RateLimiter::for('api', function (Request $request) { RateLimiter::for('api', function (Request $request) {
return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip()); return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
}); });

View file

@ -98,6 +98,18 @@ return [
'fallback_locale' => 'en', 'fallback_locale' => 'en',
/*
|--------------------------------------------------------------------------
| Application supported locales
|--------------------------------------------------------------------------
|
| List of all the supported locales of this application
|
*/
'supported_locales' => [
'en', 'fr'
],
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
| Faker Locale | Faker Locale

18
config/orbit.php Normal file
View file

@ -0,0 +1,18 @@
<?php
return [
'default' => env('ORBIT_DEFAULT_DRIVER', 'json'),
'drivers' => [
'md' => \Orbit\Drivers\Markdown::class,
'json' => \Orbit\Drivers\Json::class,
'yaml' => \Orbit\Drivers\Yaml::class,
],
'paths' => [
'content' => base_path('content'),
'cache' => storage_path('framework/cache/orbit'),
],
];

View file

@ -61,6 +61,7 @@ return [
'created-at' => 'Created', 'created-at' => 'Created',
'fullsize' => 'Total', 'fullsize' => 'Total',
'max-downloads' => 'Max downloads', 'max-downloads' => 'Max downloads',
'current-downloads' => 'Downloads',
'create-new-upload' => 'Create a new upload bundle', 'create-new-upload' => 'Create a new upload bundle',
'page-not-found' => 'Page not found', 'page-not-found' => 'Page not found',
'permission-denied' => 'Permission denied', 'permission-denied' => 'Permission denied',
@ -81,5 +82,8 @@ return [
'password' => 'Password', 'password' => 'Password',
'do-login' => 'Login now', 'do-login' => 'Login now',
'pending' => 'Drafts', 'pending' => 'Drafts',
'duplicate-file' => 'This file already exists in the bundle' 'duplicate-file' => 'This file already exists in the bundle',
'unexpected-error' => 'An unexpected error has occurred',
'login-to-get-bundles' => 'to get your bundles',
'you-are-logged-in' => 'You are logged in as ":username"'
]; ];

View file

@ -61,6 +61,7 @@ return [
'created-at' => 'Créé', 'created-at' => 'Créé',
'fullsize' => 'Total', 'fullsize' => 'Total',
'max-downloads' => 'Téléchargements maximum', 'max-downloads' => 'Téléchargements maximum',
'current-downloads' => 'Téléchargements',
'create-new-upload' => 'Créer une nouvelle archive', 'create-new-upload' => 'Créer une nouvelle archive',
'page-not-found' => 'Page non trouvée', 'page-not-found' => 'Page non trouvée',
'permission-denied' => 'Permission refusée', 'permission-denied' => 'Permission refusée',
@ -79,7 +80,10 @@ return [
'authentication' => 'Authentification', 'authentication' => 'Authentification',
'login' => 'Identifiant', 'login' => 'Identifiant',
'password' => 'Mot de passe', 'password' => 'Mot de passe',
'do-login' => 'S\'authentifier', 'do-login' => 'Authentifiez-vous',
'pending' => 'Brouillons', 'pending' => 'Brouillons',
'duplicate-file' => 'Ce fichier existe déjà dans l\'archive' 'duplicate-file' => 'Ce fichier existe déjà dans l\'archive',
'unexpected-error' => 'Une erreur inattendue est survenue',
'to-get-bundles' => 'pour accéder à vos archives',
'you-are-logged-in' => 'Vous êtes connecté(e) en tant que ":username"'
]; ];

View file

@ -6,6 +6,8 @@ import axios from 'axios';
window.axios = axios; window.axios = axios;
import moment from 'moment'; import moment from 'moment';
import 'moment/locale/fr';
moment.locale('fr');
window.moment = moment; window.moment = moment;
moment().format(); moment().format();

View file

@ -4,14 +4,13 @@
@push('scripts') @push('scripts')
<script> <script>
let auth = @js($auth); let bundle = @js($bundle);
let bundleId = @js($bundleId);
let bundle_expires = '{{ __('app.warning-bundle-expiration') }}' let bundle_expires = '{{ __('app.warning-bundle-expiration') }}'
let bundle_expired = '{{ __('app.warning-bundle-expired') }}' let bundle_expired = '{{ __('app.warning-bundle-expired') }}'
document.addEventListener('alpine:init', () => { document.addEventListener('alpine:init', () => {
Alpine.data('download', () => ({ Alpine.data('download', () => ({
metadata: @js($metadata), metadata: @js($bundle),
created_at: null, created_at: null,
expires_at: null, expires_at: null,
expired: null, expired: null,
@ -25,13 +24,10 @@
}, },
updateTimes: function() { updateTimes: function() {
this.created_at = moment.unix(this.metadata.created_at).fromNow() this.created_at = moment(this.metadata.created_at).fromNow()
if (this.isExpired()) { if (! this.isExpired()) {
this.expires_at = bundle_expired this.expires_at =moment(this.metadata.expires_at).fromNow()
}
else {
this.expires_at = bundle_expires+' '+moment.unix(this.metadata.expires_at).fromNow()
} }
}, },
@ -78,26 +74,50 @@
@lang('app.preview-bundle') @lang('app.preview-bundle')
</h2> </h2>
<div class="flex flex-wrap items-center"> <div class="flex flex-wrap justify-between items-center text-xs">
<p class="w-6/12 px-1"> <p class="w-full px-1">
<span class="font-title text-xs text-primary uppercase mr-1"> <span class="font-title text-xs text-primary uppercase mr-1">
@lang('app.upload-title') @lang('app.upload-title')
</span> </span>
<span x-text="metadata.title"></span> <span x-text="metadata.title"></span>
</p> </p>
<p class="w-4/12 px-1"> <p class="w-1/2 px-1 mt-1">
<span class="font-title text-xs text-primary uppercase mr-1"> <span class="font-title text-xs text-primary uppercase mr-1">
@lang('app.created-at') @lang('app.created-at')
</span> </span>
<span x-text="created_at"></span> <span x-text="created_at"></span>
</p> </p>
<p class="w-2/12 px-1"> <p class="w-1/2 px-1 mt-1">
<span class="font-title text-xs text-primary uppercase mr-1">
@lang('app.upload-expiry')
</span>
<span x-text="expires_at"></span>
</p>
<p class="w-1/2 px-1 mt-1">
<span class="font-title text-xs text-primary uppercase mr-1"> <span class="font-title text-xs text-primary uppercase mr-1">
@lang('app.fullsize') @lang('app.fullsize')
</span> </span>
<span x-text="humanSize(metadata.fullsize)"></span> <span x-text="humanSize(metadata.fullsize)"></span>
</p> </p>
<p class="w-full px-1" x-show="metadata.description"> <p class="w-1/2 px-1 mt-1">
<span class="font-title text-xs text-primary uppercase mr-1">
@lang('app.max-downloads')
</span>
<span x-text="metadata.max_downloads > 0 ? metadata.max_download : '∞'"></span>
</p>
<p class="w-1/2 px-1 mt-1">
<span class="font-title text-xs text-primary uppercase mr-1">
@lang('app.current-downloads')
</span>
<span x-text="metadata.downloads"></span>
</p>
<p class="w-1/2 px-1 mt-1">
<span class="font-title text-xs text-primary uppercase mr-1">
@lang('app.password')
</span>
<span x-text="metadata.password ? 'yes': 'no'"></span>
</p>
<p class="w-full px-1 mt-1" x-show="metadata.description">
<span class="font-title text-xs text-primary uppercase mr-1"> <span class="font-title text-xs text-primary uppercase mr-1">
@lang('app.upload-description') @lang('app.upload-description')
</span> </span>
@ -121,12 +141,7 @@
<div class="grid grid-cols-2 gap-10 mt-10 text-center items-center"> <div class="grid grid-cols-2 gap-10 mt-10 text-center items-center">
<div> <div>
<p class="font-xs font-medium"> &nbsp;
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="inline w-4 h-4">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m9-.75a9 9 0 11-18 0 9 9 0 0118 0zm-9 3.75h.008v.008H12v-.008z" />
</svg>
<span x-text="expires_at"></span>
</p>
</div> </div>
<div> <div>
@include('partials.button', [ @include('partials.button', [

View file

@ -1,19 +1,10 @@
@extends('layout') @extends('layout')
@section('content') @section('content')
<div>
<div class="relative bg-white border border-primary rounded-lg overflow-hidden">
<div class="bg-gradient-to-r from-primary-light to-primary px-2 py-4 text-center">
<h1 class="relative font-title font-medium font-body text-4xl text-center text-white uppercase flex items-center">
<div class="grow text-center">{{ config('app.name') }}</div> <div class="my-10 text-center text-base font-title uppercase text-primary">
</h1> <h1 class="text-7xl mb-0 font-black">403</h1>
</div> @lang('app.permission-denied')
<div class="my-10 text-center text-base font-title uppercase text-primary">
<h1 class="text-7xl mb-0 font-black">403</h1>
@lang('app.permission-denied')
</div>
</div>
</div> </div>
@endsection @endsection

View file

@ -1,19 +1,10 @@
@extends('layout') @extends('layout')
@section('content') @section('content')
<div>
<div class="relative bg-white border border-primary rounded-lg overflow-hidden">
<div class="bg-gradient-to-r from-primary-light to-primary px-2 py-4 text-center">
<h1 class="relative font-title font-medium font-body text-4xl text-center text-white uppercase flex items-center">
<div class="grow text-center">{{ config('app.name') }}</div> <div class="my-10 text-center text-base font-title uppercase text-primary">
</h1> <h1 class="text-7xl mb-0 font-black">404</h1>
</div> @lang('app.page-not-found')
<div class="my-10 text-center text-base font-title uppercase text-primary">
<h1 class="text-7xl mb-0 font-black">404</h1>
@lang('app.page-not-found')
</div>
</div>
</div> </div>
@endsection @endsection

View file

@ -0,0 +1,10 @@
@extends('layout')
@section('content')
<div class="my-10 text-center text-base font-title uppercase text-primary">
<h1 class="text-7xl mb-0 font-black">500</h1>
@lang('app.unexpected-error')
</div>
@endsection

View file

@ -1,4 +1,11 @@
<footer class="relative mt-5 h-6"> <footer class="relative mt-5 h-6">
@if (App\Helpers\Auth::isLogged())
<span class="ml-3 text-xs text-slate-600">
@lang('app.you-are-logged-in', [
'username' => App\Helpers\Auth::getLoggedUserDetails()['username']
])
</span>
@endif
<div class="absolute right-0 top-0 text-[.6rem] text-slate-100 text-right px-2 py-1 italic bg-primary rounded-tl-lg"> <div class="absolute right-0 top-0 text-[.6rem] text-slate-100 text-right px-2 py-1 italic bg-primary rounded-tl-lg">
Made with Made with

View file

@ -1,7 +1,10 @@
<header class="bg-gradient-to-r from-primary-light to-primary px-2 py-4 text-center"> <header class="relative bg-gradient-to-r from-primary-light to-primary px-2 py-4 text-center">
<a href="/"> <h1 class="relative font-title font-medium font-body text-4xl text-center text-white uppercase">
<h1 class="relative font-title font-medium font-body text-4xl text-center text-white uppercase flex items-center"> <div class="grow text-center">
<div class="grow text-center">{{ config('app.name') }}</div> <a href="/">
</h1> {{ config('app.name') }}
</a> </a>
</div>
</h1>
</header> </header>

View file

@ -3,22 +3,29 @@
@push('scripts') @push('scripts')
<script> <script>
let bundles = @js($bundles)
document.addEventListener('alpine:init', () => { document.addEventListener('alpine:init', () => {
Alpine.data('bundle', () => ({ Alpine.data('bundle', () => ({
bundles: null, bundles: [],
pending: [], pending: [],
active: [], active: [],
expired: [], expired: [],
currentBundle: null, currentBundle: null,
ownerToken: null,
init: function() { init: function() {
// Getting bundles stored locally // Generating anonymous owner token
bundles = localStorage.getItem('bundles'); this.ownerToken = localStorage.getItem('owner_token')
// And JSON decoding it if (this.ownerToken === null) {
this.bundles = JSON.parse(bundles) this.ownerToken = this.generateStr(15)
localStorage.setItem('owner_token', this.ownerToken)
}
// Loading existing bundles
this.bundles = bundles
if (this.bundles != null && Object.keys(this.bundles).length > 0) { if (this.bundles != null && Object.keys(this.bundles).length > 0) {
this.bundles.forEach( (bundle) => { this.bundles.forEach( (bundle) => {
if (bundle.title == null || bundle.title == '') { if (bundle.title == null || bundle.title == '') {
bundle.label = 'untitled' bundle.label = 'untitled'
@ -27,7 +34,7 @@
bundle.label = bundle.title bundle.label = bundle.title
} }
if (bundle.expires_at != null && moment.unix(bundle.expires_at).isBefore(moment())) { if (bundle.expires_at != null && moment(bundle.expires_at).isBefore(moment())) {
this.expired.push(bundle) this.expired.push(bundle)
} }
else if (bundle.completed == true) { else if (bundle.completed == true) {
@ -36,38 +43,20 @@
else { else {
this.pending.push(bundle) this.pending.push(bundle)
} }
bundle.label += ' - {{ __('app.created-at') }} '+moment.unix(bundle.created_at).fromNow() bundle.label += ' - {{ __('app.created-at') }} '+moment(bundle.created_at).fromNow()
}) })
} }
// If bundle is empty, initializing it
if (this.bundles == null || this.bundles == '') {
this.bundles = []
}
}, },
newBundle: function() { newBundle: function() {
// Generating a new bundle key pair
const pair = {
bundle_id: this.generateStr(30),
owner_token: this.generateStr(15),
created_at: moment().unix()
}
this.bundles.unshift(pair)
// Storing them locally
localStorage.setItem('bundles', JSON.stringify(this.bundles))
axios({ axios({
url: '/new', url: '/new',
method: 'POST', method: 'POST'
data: {
bundle_id: pair.bundle_id,
owner_token: pair.owner_token
}
}) })
.then( (response) => { .then( (response) => {
window.location.href = '/upload/'+response.data.bundle_id if (response.data.result === true) {
window.location.href = response.data.redirect
}
}) })
.catch( (error) => { .catch( (error) => {
//TODO: do something here //TODO: do something here
@ -107,46 +96,59 @@
@section('content') @section('content')
<div x-data="bundle"> <div x-data="bundle">
<div class="p-5"> <div class="p-5">
<h2 class="font-title text-2xl mb-5 text-primary font-medium uppercase flex items-center">
<p>@lang('app.existing-bundles')</p>
<p class="text-sm bg-primary rounded-full ml-2 text-white px-3" x-text="Object.keys(bundles).length"></p>
</h2>
<span x-show="bundles == null || Object.keys(bundles).length == 0">@lang('app.no-existing-bundle')</span> <div>
<select <h2 class="font-title text-2xl mb-5 text-primary font-medium uppercase flex items-center">
class="w-full py-4 text-slate-700 bg-transparent h-8 p-0 py-1 border-b border-primary-superlight focus:ring-0 invalid:border-b-red-500 invalid:bg-red-50" <p>@lang('app.existing-bundles')</p>
name="expiry" <p class="text-sm bg-primary rounded-full ml-2 text-white px-3" x-text="Object.keys(bundles).length"></p>
id="upload-expiry" </h2>
x-model="currentBundle"
x-on:change="redirectToBundle()"
x-show="bundles != null && Object.keys(bundles).length > 0"
>
<option>-</option>
<template x-if="Object.keys(pending).length > 0"> @if (App\Helpers\Auth::isLogged())
<optgroup label="{{ __('app.pending') }}"> <p class="text-center">
<template x-for="bundle in pending"> <span x-show="bundles == null || Object.keys(bundles).length == 0">@lang('app.no-existing-bundle')</span>
<option :value="bundle.bundle_id" x-text="bundle.label"></option> </p>
<select
class="w-full py-4 text-slate-700 bg-transparent h-8 p-0 py-1 border-b border-primary-superlight focus:ring-0 invalid:border-b-red-500 invalid:bg-red-50"
name="expiry"
id="upload-expiry"
x-model="currentBundle"
x-on:change="redirectToBundle()"
x-show="bundles != null && Object.keys(bundles).length > 0"
>
<option>-</option>
<template x-if="Object.keys(pending).length > 0">
<optgroup label="{{ __('app.pending') }}">
<template x-for="bundle in pending">
<option :value="bundle.slug" x-text="bundle.label"></option>
</template>
</optgroup>
</template> </template>
</optgroup>
</template>
<template x-if="Object.keys(active).length > 0"> <template x-if="Object.keys(active).length > 0">
<optgroup label="{{ __('app.active') }}"> <optgroup label="{{ __('app.active') }}">
<template x-for="bundle in active"> <template x-for="bundle in active">
<option :value="bundle.bundle_id" x-text="bundle.label"></option> <option :value="bundle.slug" x-text="bundle.label"></option>
</template>
</optgroup>
</template> </template>
</optgroup>
</template>
<template x-if="Object.keys(expired).length > 0"> <template x-if="Object.keys(expired).length > 0">
<optgroup label="{{ __('app.expired') }}"> <optgroup label="{{ __('app.expired') }}">
<template x-for="bundle in expired"> <template x-for="bundle in expired">
<option :value="bundle.bundle_id" x-text="bundle.label"></option> <option :value="bundle.slug" x-text="bundle.label"></option>
</template>
</optgroup>
</template> </template>
</optgroup> </select>
</template> @else
</select> <p class="text-center">
<a href="/login" class="text-primary font-bold hover:underline">@lang('app.do-login')</a>
@lang('app.to-get-bundles')
</p>
@endif
</div>
<h2 class="mt-10 font-title text-2xl mb-5 text-primary font-medium uppercase">@lang('app.or-create')</h2> <h2 class="mt-10 font-title text-2xl mb-5 text-primary font-medium uppercase">@lang('app.or-create')</h2>

View file

@ -43,9 +43,9 @@
if (response.data.result == true) { if (response.data.result == true) {
window.location.href = '/' window.location.href = '/'
} }
else { })
this.error = response.data.error .catch( (error) => {
} this.error = error.response.data.message
}) })
} }
})) }))

View file

@ -3,20 +3,18 @@
@section('page_title', __('app.upload-files-title')) @section('page_title', __('app.upload-files-title'))
@push('scripts') @push('scripts')
<script> <script>
let baseUrl = @js($baseUrl); let baseUrl = @js($baseUrl);
let metadata = @js($metadata ?? []); let bundle = @js($bundle);
let maxFiles = @js(config('sharing.max_files')); let maxFiles = @js(config('sharing.max_files'));
let maxFileSize = @js(Upload::fileMaxSize()); let maxFileSize = @js(Upload::fileMaxSize());
document.addEventListener('alpine:init', () => { document.addEventListener('alpine:init', () => {
Alpine.data('upload', () => ({ Alpine.data('upload', () => ({
bundle: null, bundle: null,
bundleIndex: null,
bundles: null,
dropzone: null, dropzone: null,
uploadedFiles: [], uploadedFiles: [],
metadata: [],
completed: false, completed: false,
step: 0, step: 0,
maxFiles: maxFiles, maxFiles: maxFiles,
@ -43,14 +41,14 @@
], ],
init: function() { init: function() {
this.metadata = metadata this.bundle = bundle
if (this.getBundle()) { if (this.getBundle()) {
// Steps router // Steps router
if (this.metadata.completed == true) { if (this.bundle.completed == true) {
this.step = 3 this.step = 3
} }
else if (this.metadata.title) { else if (this.bundle.title) {
this.step = 2 this.step = 2
this.startDropzone() this.startDropzone()
} }
@ -61,39 +59,6 @@
}, },
getBundle: function() { getBundle: function() {
// Getting all bundles store in local storage
bundles = localStorage.getItem('bundles')
// If no bundle found, back to homepage
if (bundles == null || bundles == '') {
window.location.href = '/'
return false
}
this.bundles = JSON.parse(bundles)
// Looking for the current bundle
if (this.bundles != null && Object.keys(this.bundles).length > 0) {
this.bundles.forEach( (element, index) => {
if (element.bundle_id == this.metadata.bundle_id) {
//this.bundle = Object.assign(element)
//this.bundleIndex = index
this.bundle = index
}
})
}
// If current bundle not found, aborting
if (this.bundle == null) {
window.location.href = '/'
return false
}
if (this.bundles[this.bundle].owner_token != this.metadata.owner_token) {
window.location.href = '/'
return false
}
return true return true
}, },
@ -105,17 +70,17 @@
document.getElementById('upload-password').setCustomValidity('') document.getElementById('upload-password').setCustomValidity('')
document.getElementById('upload-max-downloads').setCustomValidity('') document.getElementById('upload-max-downloads').setCustomValidity('')
if (this.metadata.title == null || this.metadata.title == '') { if (this.bundle.title == null || this.bundle.title == '') {
document.getElementById('upload-title').setCustomValidity('Field is required') document.getElementById('upload-title').setCustomValidity('Field is required')
errors = true errors = true
} }
if (this.metadata.expiry == null || this.metadata.expiry == '') { if (this.bundle.expiry == null || this.bundle.expiry == '') {
document.getElementById('upload-expiry').setCustomValidity('Field is required') document.getElementById('upload-expiry').setCustomValidity('Field is required')
errors = true errors = true
} }
if (this.metadata.max_downloads < 0 || this.metadata.max_downloads > 999) { if (this.bundle.max_downloads < 0 || this.bundle.max_downloads > 999) {
document.getElementById('upload-max-downloads').setCustomValidity('Invalid number of max downloads') document.getElementById('upload-max-downloads').setCustomValidity('Invalid number of max downloads')
errors = true errors = true
} }
@ -125,20 +90,20 @@
} }
axios({ axios({
url: '/upload/'+this.metadata.bundle_id, url: '/upload/'+this.bundle.slug,
method: 'POST', method: 'POST',
data: { data: {
expiry: this.metadata.expiry, expiry: this.bundle.expiry,
title: this.metadata.title, title: this.bundle.title,
description: this.metadata.description, description: this.bundle.description,
max_downloads: this.metadata.max_downloads, max_downloads: this.bundle.max_downloads,
password: this.metadata.password, password: this.bundle.password,
auth: this.bundles[this.bundle].owner_token auth: this.bundle.owner_token
} }
}) })
.then( (response) => { .then( (response) => {
this.syncData(response.data) this.syncData(response.data)
window.history.pushState(null, null, baseUrl+'/upload/'+this.metadata.bundle_id); window.history.pushState(null, null, baseUrl+'/upload/'+this.bundle.slug);
this.step = 2 this.step = 2
this.startDropzone() this.startDropzone()
@ -149,16 +114,16 @@
}, },
completeStep: function() { completeStep: function() {
if (Object.keys(this.metadata.files).length == 0) { if (Object.keys(this.bundle.files).length == 0) {
return false; return false;
} }
this.showModal('{{ __('app.confirm-complete') }}', () => { this.showModal('{{ __('app.confirm-complete') }}', () => {
axios({ axios({
url: '/upload/'+this.metadata.bundle_id+'/complete', url: '/upload/'+this.bundle.slug+'/complete',
method: 'POST', method: 'POST',
data: { data: {
auth: this.bundles[this.bundle].owner_token auth: this.bundle.owner_token
} }
}) })
@ -183,10 +148,10 @@
this.maxFiles = this.maxFiles - this.countFilesOnServer() >= 0 ? this.maxFiles - this.countFilesOnServer() : 0 this.maxFiles = this.maxFiles - this.countFilesOnServer() >= 0 ? this.maxFiles - this.countFilesOnServer() : 0
this.dropzone = new Dropzone('#upload-frm', { this.dropzone = new Dropzone('#upload-frm', {
url: '/upload/'+this.metadata.bundle_id+'/file', url: '/upload/'+this.bundle.slug+'/file',
method: 'POST', method: 'POST',
headers: { headers: {
'X-Upload-Auth': this.bundles[this.bundle].owner_token 'X-Upload-Auth': this.bundle.owner_token
}, },
createImageThumbnails: false, createImageThumbnails: false,
disablePreviews: true, disablePreviews: true,
@ -204,7 +169,7 @@
this.dropzone.on('addedfile', (file) => { this.dropzone.on('addedfile', (file) => {
file.uuid = this.uuid() file.uuid = this.uuid()
this.metadata.files.unshift({ this.bundle.files.unshift({
uuid: file.uuid, uuid: file.uuid,
original: file.name, original: file.name,
filesize: file.size, filesize: file.size,
@ -224,29 +189,29 @@
let fileIndex = null let fileIndex = null
if (fileIndex = this.findFileIndex(file.uuid)) { if (fileIndex = this.findFileIndex(file.uuid)) {
this.metadata.files[fileIndex].progress = Math.round(progress) this.bundle.files[fileIndex].progress = Math.round(progress)
} }
}) })
this.dropzone.on('error', (file, message) => { this.dropzone.on('error', (file, message) => {
let fileIndex = this.findFileIndex(file.uuid) let fileIndex = this.findFileIndex(file.uuid)
this.metadata.files[fileIndex].status = false this.bundle.files[fileIndex].status = false
if (message.hasOwnProperty('error')) { if (message.hasOwnProperty('message')) {
this.metadata.files[fileIndex].message = message.error this.bundle.files[fileIndex].message = message.message
} }
else { else {
this.metadata.files[fileIndex].message = message this.bundle.files[fileIndex].message = message
} }
}) })
this.dropzone.on('complete', (file) => { this.dropzone.on('complete', (file) => {
let fileIndex = this.findFileIndex(file.uuid) let fileIndex = this.findFileIndex(file.uuid)
this.metadata.files[fileIndex].progress = 0 this.bundle.files[fileIndex].progress = 0
if (file.status == 'success') { if (file.status == 'success') {
this.maxFiles-- this.maxFiles--
this.metadata.files[fileIndex].status = true this.bundle.files[fileIndex].status = true
} }
}) })
} }
@ -259,11 +224,11 @@
let lfile = file let lfile = file
axios({ axios({
url: '/upload/'+this.metadata.bundle_id+'/file', url: '/upload/'+this.bundle.slug+'/file',
method: 'DELETE', method: 'DELETE',
data: { data: {
uuid: lfile.uuid, uuid: lfile.uuid,
auth: this.bundles[this.bundle].owner_token auth: this.bundle.owner_token
} }
}) })
.then( (response) => { .then( (response) => {
@ -277,7 +242,7 @@
// File not valid, no need to remove it from server, just locally // File not valid, no need to remove it from server, just locally
else if (file.status == false) { else if (file.status == false) {
let fileIndex = this.findFileIndex(file.uuid) let fileIndex = this.findFileIndex(file.uuid)
this.metadata.files.splice(fileIndex, 1) this.bundle.files.splice(fileIndex, 1)
} }
// File has not being uploaded, cannot delete file yet // File has not being uploaded, cannot delete file yet
else { else {
@ -288,16 +253,17 @@
deleteBundle: function() { deleteBundle: function() {
this.showModal('{{ __('app.confirm-delete-bundle') }}', () => { this.showModal('{{ __('app.confirm-delete-bundle') }}', () => {
axios({ axios({
url: '/upload/'+this.metadata.bundle_id+'/delete', url: '/upload/'+this.bundle.slug+'/delete',
method: 'DELETE', method: 'DELETE',
data: { data: {
auth: this.bundles[this.bundle].owner_token auth: this.bundle.owner_token
} }
}) })
.then( (response) => { .then( (response) => {
if (! response.data.success) { this.syncData(response.data)
this.syncData(response.data) })
} .catch( (error) => {
}) })
}) })
}, },
@ -305,25 +271,23 @@
findFile: function(uuid) { findFile: function(uuid) {
let index = this.findFileIndex(uuid) let index = this.findFileIndex(uuid)
if (index != null) { if (index != null) {
return this.metadata.files[index] return this.bundle.files[index]
} }
return null return null
}, },
findFileIndex: function (uuid) { findFileIndex: function (uuid) {
for (i in this.metadata.files) { for (i in this.bundle.files) {
if (this.metadata.files[i].uuid == uuid) { if (this.bundle.files[i].uuid == uuid) {
return i return i
} }
} }
return null return null
}, },
syncData: function(metadata) { syncData: function(bundle) {
if (Object.keys(metadata).length > 0) { if (Object.keys(bundle).length > 0) {
this.metadata = metadata this.bundle = bundle
this.bundles[this.bundle] = metadata
localStorage.setItem('bundles', JSON.stringify(this.bundles))
} }
}, },
@ -376,9 +340,9 @@
countFilesOnServer: function() { countFilesOnServer: function() {
count = 0 count = 0
if (this.metadata.hasOwnProperty('files') && Object.keys(this.metadata.files).length > 0) { if (this.bundle.hasOwnProperty('files') && Object.keys(this.bundle.files).length > 0) {
for (i in this.metadata.files) { for (i in this.bundle.files) {
if (this.metadata.files[i].status == true) { if (this.bundle.files[i].status == true) {
count ++ count ++
} }
} }
@ -387,11 +351,11 @@
}, },
isBundleExpired: function() { isBundleExpired: function() {
if (this.metadata.expires_at == null || this.metadata.expires_at == '') { if (this.bundle.expires_at == null || this.bundle.expires_at == '') {
return false; return false;
} }
return moment.unix(this.metadata.expires_at).isBefore(moment()) return moment.unix(this.bundle.expires_at).isBefore(moment())
} }
})) }))
}) })
@ -457,7 +421,7 @@
</p> </p>
<input <input
x-model="metadata.title" x-model="bundle.title"
class="w-full p-0 bg-transparent text-slate-700 h-8 py-1 rounded-none border-b border-purple-300 outline-none invalid:border-b-red-500 invalid:bg-red-50" class="w-full p-0 bg-transparent text-slate-700 h-8 py-1 rounded-none border-b border-purple-300 outline-none invalid:border-b-red-500 invalid:bg-red-50"
type="text" type="text"
name="title" name="title"
@ -471,7 +435,7 @@
<span class="font-title uppercase">@lang('app.upload-description')</span> <span class="font-title uppercase">@lang('app.upload-description')</span>
<textarea <textarea
x-model="metadata.description" x-model="bundle.description"
maxlength="300" maxlength="300"
class="w-full p-0 bg-transparent text-slate-700 h-18 py-1 rounded-none border-b border-purple-300 outline-none invalid:border-b-red-500 invalid:bg-red-50" class="w-full p-0 bg-transparent text-slate-700 h-18 py-1 rounded-none border-b border-purple-300 outline-none invalid:border-b-red-500 invalid:bg-red-50"
type="text" type="text"
@ -489,7 +453,7 @@
</p> </p>
<select <select
x-model="metadata.expiry"" x-model="bundle.expiry""
class="w-full text-slate-700 bg-transparent h-8 p-0 py-1 border-b border-primary-superlight focus:ring-0 invalid:border-b-red-500 invalid:bg-red-50" class="w-full text-slate-700 bg-transparent h-8 p-0 py-1 border-b border-primary-superlight focus:ring-0 invalid:border-b-red-500 invalid:bg-red-50"
name="expiry" name="expiry"
id="upload-expiry" id="upload-expiry"
@ -508,7 +472,7 @@
</p> </p>
<input <input
x-model="metadata.max_downloads" x-model="bundle.max_downloads"
class="w-full p-0 bg-transparent text-slate-700 h-8 py-1 rounded-none border-b border-purple-300 outline-none invalid:border-b-red-500 invalid:bg-red-50" class="w-full p-0 bg-transparent text-slate-700 h-8 py-1 rounded-none border-b border-purple-300 outline-none invalid:border-b-red-500 invalid:bg-red-50"
type="number" type="number"
name="max_downloads" name="max_downloads"
@ -523,7 +487,7 @@
<span class="font-title uppercase">@lang('app.bundle-password')</span> <span class="font-title uppercase">@lang('app.bundle-password')</span>
<input <input
x-model="metadata.password" x-model="bundle.password"
class="w-full bg-transparent text-slate-700 h-8 p-0 py-1 rounded-none border-b border-primary-superlight outline-none invalid:border-b-red-500 invalid:bg-red-50" class="w-full bg-transparent text-slate-700 h-8 p-0 py-1 rounded-none border-b border-primary-superlight outline-none invalid:border-b-red-500 invalid:bg-red-50"
placeholder="@lang('app.leave-empty')" placeholder="@lang('app.leave-empty')"
type="text" type="text"
@ -581,11 +545,11 @@
</div> </div>
</h3> </h3>
<span class="text-xs text-slate-400" x-show="Object.keys(metadata.files).length == 0">@lang('app.no-file')</span> <span class="text-xs text-slate-400" x-show="Object.keys(bundle.files).length == 0">@lang('app.no-file')</span>
{{-- Files list --}} {{-- Files list --}}
<ul id="output" class="text-xs max-h-32 overflow-y-scroll pb-3" x-show="Object.keys(metadata.files).length > 0"> <ul id="output" class="text-xs max-h-32 overflow-y-scroll pb-3" x-show="Object.keys(bundle.files).length > 0">
<template x-for="(f, k) in metadata.files" :key="k"> <template x-for="(f, k) in bundle.files" :key="k">
<li <li
title="{{ __('app.click-to-remove') }}" title="{{ __('app.click-to-remove') }}"
class="relative flex items-center leading-5 list-inside even:bg-gray-50 rounded px-2 cursor-pointer overflow-hidden" class="relative flex items-center leading-5 list-inside even:bg-gray-50 rounded px-2 cursor-pointer overflow-hidden"
@ -663,7 +627,7 @@
@lang('app.preview-link') @lang('app.preview-link')
</div> </div>
<div class="w-2/3 shadow"> <div class="w-2/3 shadow">
<input x-model="metadata.preview_link" class="w-full bg-transparent text-slate-700 h-8 px-2 py-1 rounded-none border border-primary-superlight outline-none" type="text" readonly x-on:click="selectCopy($el)" /> <input x-model="bundle.preview_link" class="w-full bg-transparent text-slate-700 h-8 px-2 py-1 rounded-none border border-primary-superlight outline-none" type="text" readonly x-on:click="selectCopy($el)" />
</div> </div>
</div> </div>
@ -673,7 +637,7 @@
@lang('app.direct-link') @lang('app.direct-link')
</div> </div>
<div class="w-2/3 shadow"> <div class="w-2/3 shadow">
<input x-model="metadata.download_link" class="w-full bg-transparent text-slate-700 h-8 px-2 py-1 rounded-none border border-primary-superlight outline-none" type="text" readonly x-on:click="selectCopy($el)" /> <input x-model="bundle.download_link" class="w-full bg-transparent text-slate-700 h-8 px-2 py-1 rounded-none border border-primary-superlight outline-none" type="text" readonly x-on:click="selectCopy($el)" />
</div> </div>
</div> </div>

View file

@ -7,6 +7,7 @@ use App\Http\Controllers\UploadController;
use App\Http\Controllers\BundleController; use App\Http\Controllers\BundleController;
use App\Http\Middleware\UploadAccess; use App\Http\Middleware\UploadAccess;
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
| Web Routes | Web Routes
@ -18,31 +19,40 @@ use App\Http\Middleware\UploadAccess;
| |
*/ */
/**
Public route for login
*/
Route::get('/login', [WebController::class, 'login']); Route::get('/login', [WebController::class, 'login']);
Route::post('/login', [WebController::class, 'doLogin']); Route::post('/login', [WebController::class, 'doLogin']);
/**
Upload routes
*/
Route::middleware(['can.upload'])->group(function() { Route::middleware(['can.upload'])->group(function() {
Route::get('/', [WebController::class, 'homepage'])->name('homepage'); Route::get('/', [WebController::class, 'homepage'])->name('homepage');
Route::post('/new', [WebController::class, 'newBundle'])->name('bundle.new'); Route::get('/new', [WebController::class, 'newBundle'])->name('bundle.new');
Route::prefix('/upload/{bundle}')->controller(UploadController::class)->name('upload.')->group(function() { Route::prefix('/upload/{bundle}')->controller(UploadController::class)->name('upload.')->group(function() {
Route::get('/', 'createBundle')->name('create.show'); Route::get('/', 'createBundle')->name('create.show');
Route::middleware(['access.owner'])->group(function() { Route::middleware(['access.owner'])->group(function() {
Route::post('/', 'storeBundle')->name('create.store'); Route::post('/', 'storeBundle')->name('create.store');
Route::get('/metadata', 'getMetadata')->name('metadata.get'); Route::get('/metadata', 'getMetadata')->name('metadata.get');
Route::post('/file', 'uploadFile')->name('file.store'); Route::post('/file', 'uploadFile')->name('file.store');
Route::delete('/file', 'deleteFile')->name('file.delete'); Route::delete('/file', 'deleteFile')->name('file.delete');
Route::post('/complete', 'completeBundle')->name('complete'); Route::post('/complete', 'completeBundle')->name('complete');
Route::delete('/delete', 'deleteBundle')->name('bundle.delete'); Route::delete('/delete', 'deleteBundle')->name('bundle.delete');
}); });
}); });
}); });
/**
Download routes
*/
Route::middleware(['access.guest'])->prefix('/bundle/{bundle}')->controller(BundleController::class)->name('bundle.')->group(function() { Route::middleware(['access.guest'])->prefix('/bundle/{bundle}')->controller(BundleController::class)->name('bundle.')->group(function() {
Route::get('/preview', 'previewBundle')->name('preview'); Route::get('/preview', 'previewBundle')->name('preview');
Route::post('/zip', 'prepareZip')->name('zip.make'); Route::post('/zip', 'prepareZip')->name('zip.make');
Route::get('/download', 'downloadZip')->name('zip.download'); Route::get('/download', 'downloadZip')->name('zip.download');
}); });