From 94697d536be3f1f5b90ccbd8eea1d40550746a42 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Sun, 13 Nov 2022 20:11:07 -0700 Subject: [PATCH] Add Server Directory integration --- .../Admin/AdminDirectoryController.php | 453 ++++++++++++++++++ app/Http/Controllers/AdminController.php | 2 + app/Http/Controllers/Api/ApiV1Controller.php | 4 +- .../PixelfedDirectoryController.php | 167 +++++++ app/Models/ConfigCache.php | 2 +- app/Services/ConfigCacheService.php | 9 + public/css/admin.css | Bin 366165 -> 366431 bytes public/js/admin.js | Bin 25625 -> 76790 bytes public/mix-manifest.json | Bin 3895 -> 3895 bytes .../views/admin/directory/home.blade.php | 12 + .../views/admin/partial/sidenav.blade.php | 7 + routes/api.php | 4 + routes/web.php | 11 + storage/app/public/headers/default.jpg | Bin 213518 -> 61845 bytes 14 files changed, 668 insertions(+), 3 deletions(-) create mode 100644 app/Http/Controllers/Admin/AdminDirectoryController.php create mode 100644 app/Http/Controllers/PixelfedDirectoryController.php create mode 100644 resources/views/admin/directory/home.blade.php diff --git a/app/Http/Controllers/Admin/AdminDirectoryController.php b/app/Http/Controllers/Admin/AdminDirectoryController.php new file mode 100644 index 000000000..1e4db7d2d --- /dev/null +++ b/app/Http/Controllers/Admin/AdminDirectoryController.php @@ -0,0 +1,453 @@ +all())->pluck('name'); + $res['admins'] = User::whereIsAdmin(true) + ->where('2fa_enabled', true) + ->get()->map(function($user) { + return [ + 'uid' => (string) $user->id, + 'pid' => (string) $user->profile_id, + 'username' => $user->username, + 'created_at' => $user->created_at + ]; + }); + $config = ConfigCache::whereK('pixelfed.directory')->first(); + if($config) { + $data = $config->v ? json_decode($config->v, true) : []; + $res = array_merge($res, $data); + } + + if(empty($res['summary'])) { + $summary = ConfigCache::whereK('app.short_description')->pluck('v'); + $res['summary'] = $summary ? $summary[0] : null; + } + + if(isset($res['banner_image']) && !empty($res['banner_image'])) { + $res['banner_image'] = url(Storage::url($res['banner_image'])); + } + + if(isset($res['favourite_posts'])) { + $res['favourite_posts'] = collect($res['favourite_posts'])->map(function($id) { + return StatusService::get($id); + }) + ->filter(function($post) { + return $post && isset($post['account']); + }) + ->values(); + } + + $res['community_guidelines'] = config_cache('app.rules') ? json_decode(config_cache('app.rules'), true) : []; + $res['open_registration'] = (bool) config_cache('pixelfed.open_registration'); + $res['oauth_enabled'] = (bool) config_cache('pixelfed.oauth_enabled') && file_exists(storage_path('oauth-public.key')) && file_exists(storage_path('oauth-private.key')); + + $res['activitypub_enabled'] = (bool) config_cache('federation.activitypub.enabled'); + + $res['feature_config'] = [ + 'media_types' => Str::of(config_cache('pixelfed.media_types'))->explode(','), + 'image_quality' => config_cache('pixelfed.image_quality'), + 'optimize_image' => config_cache('pixelfed.optimize_image'), + 'max_photo_size' => config_cache('pixelfed.max_photo_size'), + 'max_caption_length' => config_cache('pixelfed.max_caption_length'), + 'max_altext_length' => config_cache('pixelfed.max_altext_length'), + 'enforce_account_limit' => config_cache('pixelfed.enforce_account_limit'), + 'max_account_size' => config_cache('pixelfed.max_account_size'), + 'max_album_length' => config_cache('pixelfed.max_album_length'), + 'account_deletion' => config_cache('pixelfed.account_deletion'), + ]; + + if(config_cache('pixelfed.directory.testimonials')) { + $testimonials = collect(json_decode(config_cache('pixelfed.directory.testimonials'),true)) + ->map(function($t) { + return [ + 'profile' => AccountService::get($t['profile_id']), + 'body' => $t['body'] + ]; + }); + $res['testimonials'] = $testimonials; + } + + $validator = Validator::make($res['feature_config'], [ + 'media_types' => [ + 'required', + function ($attribute, $value, $fail) { + if (!in_array('image/jpeg', $value->toArray()) || !in_array('image/png', $value->toArray())) { + $fail('You must enable image/jpeg and image/png support.'); + } + }, + ], + 'image_quality' => 'required_if:optimize_image,true|integer|min:75|max:100', + 'max_altext_length' => 'required|integer|min:1000|max:5000', + 'max_photo_size' => 'required|integer|min:15000|max:100000', + 'max_account_size' => 'required_if:enforce_account_limit,true|integer|min:1000000', + 'max_album_length' => 'required|integer|min:4|max:20', + 'account_deletion' => 'required|accepted', + 'max_caption_length' => 'required|integer|min:500|max:10000' + ]); + + $res['requirements_validator'] = $validator->errors(); + + $res['is_eligible'] = $res['open_registration'] && + $res['oauth_enabled'] && + $res['activitypub_enabled'] && + count($res['requirements_validator']) === 0 && + $this->validVal($res, 'admin') && + $this->validVal($res, 'summary', null, 10) && + $this->validVal($res, 'favourite_posts', 3) && + $this->validVal($res, 'contact_email') && + $this->validVal($res, 'privacy_pledge') && + $this->validVal($res, 'location'); + + $res['has_submitted'] = config_cache('pixelfed.directory.has_submitted') ?? false; + $res['synced'] = config_cache('pixelfed.directory.is_synced') ?? false; + $res['latest_response'] = config_cache('pixelfed.directory.latest_response') ?? null; + + $path = base_path('resources/lang'); + $langs = collect([]); + + foreach (new \DirectoryIterator($path) as $io) { + $name = $io->getFilename(); + $skip = ['vendor']; + if($io->isDot() || in_array($name, $skip)) { + continue; + } + + if($io->isDir()) { + $langs->push(['code' => $name, 'name' => locale_get_display_name($name)]); + } + } + + $res['available_languages'] = $langs->sortBy('name')->values(); + $res['primary_locale'] = config('app.locale'); + + $submissionState = Http::withoutVerifying() + ->post('https://pixelfed.org/api/v1/directory/check-submission', [ + 'domain' => config('pixelfed.domain.app') + ]); + + $res['submission_state'] = $submissionState->json(); + return $res; + } + + protected function validVal($res, $val, $count = false, $minLen = false) + { + if(!isset($res[$val])) { + return false; + } + + if($count) { + return count($res[$val]) >= $count; + } + + if($minLen) { + return strlen($res[$val]) >= $minLen; + } + + return $res[$val]; + } + + public function directoryStore(Request $request) + { + $this->validate($request, [ + 'location' => 'string|min:1|max:53', + 'summary' => 'string|nullable|max:140', + 'admin_uid' => 'sometimes|nullable', + 'contact_email' => 'sometimes|nullable|email:rfc,dns', + 'favourite_posts' => 'array|max:12', + 'favourite_posts.*' => 'distinct', + 'privacy_pledge' => 'sometimes', + 'banner_image' => 'sometimes|mimes:jpg,png|dimensions:width=1920,height:1080|max:5000' + ]); + + $config = ConfigCache::firstOrNew([ + 'k' => 'pixelfed.directory' + ]); + + $res = $config->v ? json_decode($config->v, true) : []; + $res['summary'] = strip_tags($request->input('summary')); + $res['favourite_posts'] = $request->input('favourite_posts'); + $res['admin'] = (string) $request->input('admin_uid'); + $res['contact_email'] = $request->input('contact_email'); + $res['privacy_pledge'] = (bool) $request->input('privacy_pledge'); + + if($request->filled('location')) { + $exists = (new ISO3166)->name($request->location); + if($exists) { + $res['location'] = $request->input('location'); + } + } + + if($request->hasFile('banner_image')) { + collect(Storage::files('public/headers')) + ->filter(function($name) { + $protected = [ + 'public/headers/.gitignore', + 'public/headers/default.jpg', + 'public/headers/missing.png' + ]; + return !in_array($name, $protected); + }) + ->each(function($name) { + Storage::delete($name); + }); + $path = $request->file('banner_image')->store('public/headers'); + $res['banner_image'] = $path; + ConfigCacheService::put('app.banner_image', url(Storage::url($path))); + + Cache::forget('api:v1:instance-data-response-v1'); + } + + $config->v = json_encode($res); + $config->save(); + + ConfigCacheService::put('pixelfed.directory', $config->v); + $updated = json_decode($config->v, true); + if(isset($updated['banner_image'])) { + $updated['banner_image'] = url(Storage::url($updated['banner_image'])); + } + return $updated; + } + + public function directoryHandleServerSubmission(Request $request) + { + $reqs = []; + $reqs['feature_config'] = [ + 'open_registration' => config_cache('pixelfed.open_registration'), + 'activitypub_enabled' => config_cache('federation.activitypub.enabled'), + 'oauth_enabled' => config_cache('pixelfed.oauth_enabled'), + 'media_types' => Str::of(config_cache('pixelfed.media_types'))->explode(','), + 'image_quality' => config_cache('pixelfed.image_quality'), + 'optimize_image' => config_cache('pixelfed.optimize_image'), + 'max_photo_size' => config_cache('pixelfed.max_photo_size'), + 'max_caption_length' => config_cache('pixelfed.max_caption_length'), + 'max_altext_length' => config_cache('pixelfed.max_altext_length'), + 'enforce_account_limit' => config_cache('pixelfed.enforce_account_limit'), + 'max_account_size' => config_cache('pixelfed.max_account_size'), + 'max_album_length' => config_cache('pixelfed.max_album_length'), + 'account_deletion' => config_cache('pixelfed.account_deletion'), + ]; + + $validator = Validator::make($reqs['feature_config'], [ + 'open_registration' => 'required|accepted', + 'activitypub_enabled' => 'required|accepted', + 'oauth_enabled' => 'required|accepted', + 'media_types' => [ + 'required', + function ($attribute, $value, $fail) { + if (!in_array('image/jpeg', $value->toArray()) || !in_array('image/png', $value->toArray())) { + $fail('You must enable image/jpeg and image/png support.'); + } + }, + ], + 'image_quality' => 'required_if:optimize_image,true|integer|min:75|max:100', + 'max_altext_length' => 'required|integer|min:1000|max:5000', + 'max_photo_size' => 'required|integer|min:15000|max:100000', + 'max_account_size' => 'required_if:enforce_account_limit,true|integer|min:1000000', + 'max_album_length' => 'required|integer|min:4|max:20', + 'account_deletion' => 'required|accepted', + 'max_caption_length' => 'required|integer|min:500|max:10000' + ]); + + if(!$validator->validate()) { + return response()->json($validator->errors(), 422); + } + + ConfigCacheService::put('pixelfed.directory.submission-key', Str::random(random_int(40, 69))); + ConfigCacheService::put('pixelfed.directory.submission-ts', now()); + + $data = (new PixelfedDirectoryController())->buildListing(); + $res = Http::withoutVerifying()->post('https://pixelfed.org/api/v1/directory/submission', $data); + return 200; + } + + public function directoryDeleteBannerImage(Request $request) + { + $bannerImage = ConfigCache::whereK('app.banner_image')->first(); + $directory = ConfigCache::whereK('pixelfed.directory')->first(); + if(!$bannerImage && !$directory || empty($directory->v)) { + return; + } + $directoryArr = json_decode($directory->v, true); + $path = isset($directoryArr['banner_image']) ? $directoryArr['banner_image'] : false; + $protected = [ + 'public/headers/.gitignore', + 'public/headers/default.jpg', + 'public/headers/missing.png' + ]; + if(!$path || in_array($path, $protected)) { + return; + } + if(Storage::exists($directoryArr['banner_image'])) { + Storage::delete($directoryArr['banner_image']); + } + + $directoryArr['banner_image'] = 'public/headers/default.jpg'; + $directory->v = $directoryArr; + $directory->save(); + $bannerImage->v = url(Storage::url('public/headers/default.jpg')); + $bannerImage->save(); + Cache::forget('api:v1:instance-data-response-v1'); + ConfigCacheService::put('pixelfed.directory', $directory); + return $bannerImage->v; + } + + public function directoryGetPopularPosts(Request $request) + { + $ids = Cache::remember('admin:api:popular_posts', 86400, function() { + return Status::whereLocal(true) + ->whereScope('public') + ->whereType('photo') + ->whereNull(['in_reply_to_id', 'reblog_of_id']) + ->orderByDesc('likes_count') + ->take(50) + ->pluck('id'); + }); + + $res = $ids->map(function($id) { + return StatusService::get($id); + }) + ->filter(function($post) { + return $post && isset($post['account']); + }) + ->values(); + + return response()->json($res, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES); + } + + public function directoryGetAddPostByIdSearch(Request $request) + { + $this->validate($request, [ + 'q' => 'required|integer' + ]); + + $id = $request->input('q'); + + $status = Status::whereLocal(true) + ->whereType('photo') + ->whereNull(['in_reply_to_id', 'reblog_of_id']) + ->findOrFail($id); + + $res = StatusService::get($status->id); + + return $res; + } + + public function directoryDeleteTestimonial(Request $request) + { + $this->validate($request, [ + 'profile_id' => 'required', + ]); + $profile_id = $request->input('profile_id'); + $testimonials = ConfigCache::whereK('pixelfed.directory.testimonials')->firstOrFail(); + $existing = collect(json_decode($testimonials->v, true)) + ->filter(function($t) use($profile_id) { + return $t['profile_id'] !== $profile_id; + }) + ->values(); + ConfigCacheService::put('pixelfed.directory.testimonials', $existing); + return $existing; + } + + public function directorySaveTestimonial(Request $request) + { + $this->validate($request, [ + 'username' => 'required', + 'body' => 'required|string|min:5|max:500' + ]); + + $user = User::whereUsername($request->input('username'))->whereNull('status')->firstOrFail(); + + $configCache = ConfigCache::firstOrCreate([ + 'k' => 'pixelfed.directory.testimonials' + ]); + + $testimonials = $configCache->v ? collect(json_decode($configCache->v, true)) : collect([]); + + abort_if($testimonials->contains('profile_id', $user->profile_id), 422, 'Testimonial already exists'); + abort_if($testimonials->count() == 10, 422, 'You can only have 10 active testimonials'); + + $testimonials->push([ + 'profile_id' => (string) $user->profile_id, + 'username' => $request->input('username'), + 'body' => $request->input('body') + ]); + + $configCache->v = json_encode($testimonials->toArray()); + $configCache->save(); + ConfigCacheService::put('pixelfed.directory.testimonials', $configCache->v); + $res = [ + 'profile' => AccountService::get($user->profile_id), + 'body' => $request->input('body') + ]; + return $res; + } + + public function directoryUpdateTestimonial(Request $request) + { + $this->validate($request, [ + 'profile_id' => 'required', + 'body' => 'required|string|min:5|max:500' + ]); + + $profile_id = $request->input('profile_id'); + $body = $request->input('body'); + $user = User::whereProfileId($profile_id)->firstOrFail(); + + $configCache = ConfigCache::firstOrCreate([ + 'k' => 'pixelfed.directory.testimonials' + ]); + + $testimonials = $configCache->v ? collect(json_decode($configCache->v, true)) : collect([]); + + $updated = $testimonials->map(function($t) use($profile_id, $body) { + if($t['profile_id'] == $profile_id) { + $t['body'] = $body; + } + return $t; + }) + ->values(); + + $configCache->v = json_encode($updated); + $configCache->save(); + ConfigCacheService::put('pixelfed.directory.testimonials', $configCache->v); + + return $updated; + } +} diff --git a/app/Http/Controllers/AdminController.php b/app/Http/Controllers/AdminController.php index 37e3a7c0d..8a6f019ef 100644 --- a/app/Http/Controllers/AdminController.php +++ b/app/Http/Controllers/AdminController.php @@ -20,6 +20,7 @@ use Carbon\Carbon; use Illuminate\Http\Request; use Illuminate\Support\Facades\Redis; use App\Http\Controllers\Admin\{ + AdminDirectoryController, AdminDiscoverController, AdminInstanceController, AdminReportController, @@ -40,6 +41,7 @@ use App\Models\CustomEmoji; class AdminController extends Controller { use AdminReportController, + AdminDirectoryController, AdminDiscoverController, // AdminGroupsController, AdminMediaController, diff --git a/app/Http/Controllers/Api/ApiV1Controller.php b/app/Http/Controllers/Api/ApiV1Controller.php index 3387cd273..c2ab4ac1e 100644 --- a/app/Http/Controllers/Api/ApiV1Controller.php +++ b/app/Http/Controllers/Api/ApiV1Controller.php @@ -8,7 +8,7 @@ use Illuminate\Support\Str; use App\Util\ActivityPub\Helpers; use App\Util\Media\Filter; use Laravel\Passport\Passport; -use Auth, Cache, DB, URL; +use Auth, Cache, DB, Storage, URL; use App\{ Avatar, Bookmark, @@ -1375,7 +1375,7 @@ class ApiV1Controller extends Controller 'streaming_api' => 'wss://' . config('pixelfed.domain.app') ], 'stats' => $stats, - 'thumbnail' => url('img/pixelfed-icon-color.png'), + 'thumbnail' => config_cache('app.banner_image') ?? url(Storage::url('public/headers/default.jpg')), 'languages' => [config('app.locale')], 'registrations' => (bool) config_cache('pixelfed.open_registration'), 'approval_required' => false, diff --git a/app/Http/Controllers/PixelfedDirectoryController.php b/app/Http/Controllers/PixelfedDirectoryController.php new file mode 100644 index 000000000..6290cd398 --- /dev/null +++ b/app/Http/Controllers/PixelfedDirectoryController.php @@ -0,0 +1,167 @@ +filled('sk')) { + abort(404); + } + + if(!config_cache('pixelfed.directory.submission-key')) { + abort(404); + } + + if(!hash_equals(config_cache('pixelfed.directory.submission-key'), $request->input('sk'))) { + abort(403); + } + + $res = $this->buildListing(); + return response()->json($res, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES); + } + + public function buildListing() + { + $res = config_cache('pixelfed.directory'); + if($res) { + $res = is_string($res) ? json_decode($res, true) : $res; + } + + $res['_domain'] = config_cache('pixelfed.domain.app'); + $res['_sk'] = config_cache('pixelfed.directory.submission-key'); + $res['_ts'] = config_cache('pixelfed.directory.submission-ts'); + $res['version'] = config_cache('pixelfed.version'); + + if(empty($res['summary'])) { + $summary = ConfigCache::whereK('app.short_description')->pluck('v'); + $res['summary'] = $summary ? $summary[0] : null; + } + + if(isset($res['admin'])) { + $res['admin'] = AccountService::get($res['admin'], true); + } + + if(isset($res['banner_image']) && !empty($res['banner_image'])) { + $res['banner_image'] = url(Storage::url($res['banner_image'])); + } + + if(isset($res['favourite_posts'])) { + $res['favourite_posts'] = collect($res['favourite_posts'])->map(function($id) { + return StatusService::get($id); + }) + ->filter(function($post) { + return $post && isset($post['account']); + }) + ->map(function($post) { + return [ + 'avatar' => $post['account']['avatar'], + 'display_name' => $post['account']['display_name'], + 'username' => $post['account']['username'], + 'media' => $post['media_attachments'][0]['url'], + 'url' => $post['url'] + ]; + }) + ->values(); + } + + $guidelines = ConfigCache::whereK('app.rules')->first(); + if($guidelines) { + $res['community_guidelines'] = json_decode($guidelines->v, true); + } + + $openRegistration = ConfigCache::whereK('pixelfed.open_registration')->first(); + if($openRegistration) { + $res['open_registration'] = (bool) $openRegistration; + } + + $oauthEnabled = ConfigCache::whereK('pixelfed.oauth_enabled')->first(); + if($oauthEnabled) { + $keys = file_exists(storage_path('oauth-public.key')) && file_exists(storage_path('oauth-private.key')); + $res['oauth_enabled'] = (bool) $oauthEnabled && $keys; + } + + $activityPubEnabled = ConfigCache::whereK('federation.activitypub.enabled')->first(); + if($activityPubEnabled) { + $res['activitypub_enabled'] = (bool) $activityPubEnabled; + } + + $res['feature_config'] = [ + 'media_types' => Str::of(config_cache('pixelfed.media_types'))->explode(','), + 'image_quality' => config_cache('pixelfed.image_quality'), + 'optimize_image' => config_cache('pixelfed.optimize_image'), + 'max_photo_size' => config_cache('pixelfed.max_photo_size'), + 'max_caption_length' => config_cache('pixelfed.max_caption_length'), + 'max_altext_length' => config_cache('pixelfed.max_altext_length'), + 'enforce_account_limit' => config_cache('pixelfed.enforce_account_limit'), + 'max_account_size' => config_cache('pixelfed.max_account_size'), + 'max_album_length' => config_cache('pixelfed.max_album_length'), + 'account_deletion' => config_cache('pixelfed.account_deletion'), + ]; + + $res['is_eligible'] = $this->validVal($res, 'admin') && + $this->validVal($res, 'summary', null, 10) && + $this->validVal($res, 'favourite_posts', 3) && + $this->validVal($res, 'contact_email') && + $this->validVal($res, 'privacy_pledge') && + $this->validVal($res, 'location'); + + if(config_cache('pixelfed.directory.testimonials')) { + $res['testimonials'] = collect(json_decode(config_cache('pixelfed.directory.testimonials'), true)) + ->map(function($testimonial) { + $profile = AccountService::get($testimonial['profile_id']); + return [ + 'profile' => [ + 'username' => $profile['username'], + 'display_name' => $profile['display_name'], + 'avatar' => $profile['avatar'], + 'created_at' => $profile['created_at'] + ], + 'body' => $testimonial['body'] + ]; + }); + } + + $res['features_enabled'] = [ + 'stories' => (bool) config_cache('instance.stories.enabled') + ]; + + $res['stats'] = [ + 'user_count' => \App\User::count(), + 'post_count' => \App\Status::whereNull('uri')->count(), + ]; + + $res['primary_locale'] = config('app.locale'); + $hash = hash('sha256', json_encode($res)); + $res['_hash'] = $hash; + ksort($res); + + return $res; + } + + protected function validVal($res, $val, $count = false, $minLen = false) + { + if(!isset($res[$val])) { + return false; + } + + if($count) { + return count($res[$val]) >= $count; + } + + if($minLen) { + return strlen($res[$val]) >= $minLen; + } + + return $res[$val]; + } + +} diff --git a/app/Models/ConfigCache.php b/app/Models/ConfigCache.php index 4698b1c6b..1b4a18108 100644 --- a/app/Models/ConfigCache.php +++ b/app/Models/ConfigCache.php @@ -10,5 +10,5 @@ class ConfigCache extends Model use HasFactory; protected $table = 'config_cache'; - public $fillable = ['*']; + public $guarded = []; } diff --git a/app/Services/ConfigCacheService.php b/app/Services/ConfigCacheService.php index b334e9e33..0a9055287 100644 --- a/app/Services/ConfigCacheService.php +++ b/app/Services/ConfigCacheService.php @@ -55,6 +55,15 @@ class ConfigCacheService 'config.discover.features', 'instance.has_legal_notice', + + 'pixelfed.directory', + 'app.banner_image', + 'pixelfed.directory.submission-key', + 'pixelfed.directory.submission-ts', + 'pixelfed.directory.has_submitted', + 'pixelfed.directory.latest_response', + 'pixelfed.directory.is_synced', + 'pixelfed.directory.testimonials', // 'system.user_mode' ]; diff --git a/public/css/admin.css b/public/css/admin.css index 1f0afb047b6ff43d44b687458d9de2ce34482bc2..2df11cc5666c8a18e0397fa15bbb17e782752957 100644 GIT binary patch delta 190 zcmcb*M(qANv4$4L7N!>F7M3lnA=9Q$k!KN^zEqKgfBFS=CKjPuy`0SA65X`KlvLfM z{F0LV+{uZ|A_C}w(-N2^rmt#d6!0>%Fi1&F*U2c(Nz^dVF;LYpGyoC?nmWp9X=w_U z2CACY5Ob^ytdsNeN>cMmtdx|jGg33tGfJ!s4N`Nhb5hevfU=Vx&JdoyU>PIVbe{V_ X=R9C$Vd1f^(#_0ENv*IltmOg#uw*_# delta 21 ccmcb=PVDL$v4$4L7N!>F7M3lnA=4PS0ApDPg8%>k diff --git a/public/js/admin.js b/public/js/admin.js index a9626e9fc47711a78939e4959db6873a4f53306c..c84e6e2fe24ce1e484059524b52d3bd7980d8391 100644 GIT binary patch literal 76790 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 diff --git a/public/mix-manifest.json b/public/mix-manifest.json index 2f76f51b050a8bfc0b34abb83c876b0c626437bd..7cada2bace5be56900d38c65c223413fbcdd5a29 100644 GIT binary patch delta 82 zcmdlkw_R>SGpmB7siBdPk)@e|d2(7(iixp_VUoE)qNRaFibYzgNwWFo#jJfC3YMwK msmYcGhQ{WWX$Gc=24-mnCaIPtsb*=0=9Xs0Ns|Nlo&f;f-WT%# delta 82 zcmdlkw_R>SGpj;ciiJgzrJ0G5v9X~^l8Iq*vbmXYqJg=giK(ecqJhcg#jJfC3dZKf m21!Ylmgc6ZMu}!ADan?`hG}MIDaomZX~}6OmXibdo&f;V3>Xyv diff --git a/resources/views/admin/directory/home.blade.php b/resources/views/admin/directory/home.blade.php new file mode 100644 index 000000000..dcc5e050e --- /dev/null +++ b/resources/views/admin/directory/home.blade.php @@ -0,0 +1,12 @@ +@extends('admin.partial.template-full') + +@section('section') + + +@endsection + +@push('scripts') + +@endpush diff --git a/resources/views/admin/partial/sidenav.blade.php b/resources/views/admin/partial/sidenav.blade.php index 31a2a8a05..a34d572d2 100644 --- a/resources/views/admin/partial/sidenav.blade.php +++ b/resources/views/admin/partial/sidenav.blade.php @@ -62,6 +62,13 @@