├── database ├── .gitignore ├── seeders │ ├── DatabaseSeeder.php │ └── InstallSeeder.php ├── migrations │ ├── 2014_10_12_100000_create_password_resets_table.php │ ├── 2021_12_11_200033_create_configs_table.php │ ├── 2022_01_20_201231_create_group_strategy_table.php │ ├── 2019_08_19_000000_create_failed_jobs_table.php │ ├── 2019_12_14_000001_create_personal_access_tokens_table.php │ ├── 2021_12_11_184521_create_strategies_table.php │ ├── 2014_10_10_000000_create_groups_table.php │ ├── 2021_12_11_185759_create_albums_table.php │ ├── 2014_10_12_000000_create_users_table.php │ └── 2021_12_11_191158_create_images_table.php └── factories │ └── UserFactory.php ├── bootstrap ├── cache │ └── .gitignore └── app.php ├── storage ├── logs │ └── .gitignore ├── app │ ├── public │ │ └── .gitignore │ ├── uploads │ │ └── .gitignore │ └── .gitignore ├── debugbar │ └── .gitignore └── framework │ ├── testing │ └── .gitignore │ ├── views │ └── .gitignore │ ├── cache │ ├── data │ │ └── .gitignore │ └── .gitignore │ ├── sessions │ └── .gitignore │ └── .gitignore ├── public ├── robots.txt ├── thumbnails │ └── .gitignore ├── favicon.ico ├── fonts │ └── vendor │ │ └── @fortawesome │ │ └── fontawesome-free │ │ ├── webfa-brands-400.eot │ │ ├── webfa-brands-400.ttf │ │ ├── webfa-solid-900.eot │ │ ├── webfa-solid-900.ttf │ │ ├── webfa-solid-900.woff │ │ ├── webfa-brands-400.woff │ │ ├── webfa-brands-400.woff2 │ │ └── webfa-solid-900.woff2 ├── css │ ├── gallery.css │ └── justified-gallery │ │ └── justifiedGallery.min.css ├── .htaccess ├── js │ ├── app.js.LICENSE.txt │ ├── blueimp-file-upload │ │ └── jquery.iframe-transport.js │ └── clipboard │ │ └── index.browser.js ├── mix-manifest.json └── index.php ├── resources ├── css │ ├── app.css │ ├── fontawesome.less │ └── gallery.less ├── views │ ├── components │ │ ├── application-logo.blade.php │ │ ├── switch.blade.php │ │ ├── code.blade.php │ │ ├── label.blade.php │ │ ├── auth-session-status.blade.php │ │ ├── dropdown-link.blade.php │ │ ├── no-data.blade.php │ │ ├── button.blade.php │ │ ├── input.blade.php │ │ ├── box.blade.php │ │ ├── default-avatar.blade.php │ │ ├── container.blade.php │ │ ├── select.blade.php │ │ ├── textarea.blade.php │ │ ├── fieldset.blade.php │ │ ├── auth-card.blade.php │ │ ├── fieldset-radio.blade.php │ │ ├── fieldset-checkbox.blade.php │ │ ├── nav-link.blade.php │ │ ├── loading-spin.blade.php │ │ ├── auth-validation-errors.blade.php │ │ ├── table.blade.php │ │ └── dropdown.blade.php │ ├── emails │ │ └── test.blade.php │ ├── user │ │ └── upload.blade.php │ ├── admin │ │ ├── strategy │ │ │ └── tips.blade.php │ │ └── group │ │ │ └── tips.blade.php │ ├── layouts │ │ ├── notice.blade.php │ │ ├── header.blade.php │ │ ├── user-nav.blade.php │ │ ├── guest.blade.php │ │ ├── strategies.blade.php │ │ └── app.blade.php │ ├── common │ │ ├── notice.blade.php │ │ └── gallery.blade.php │ ├── auth │ │ ├── confirm-password.blade.php │ │ ├── forgot-password.blade.php │ │ ├── verify-email.blade.php │ │ ├── reset-password.blade.php │ │ └── register.blade.php │ └── welcome.blade.php └── js │ ├── stores │ ├── sidebar.js │ └── modal.js │ └── bootstrap.js ├── app ├── Exceptions │ ├── UploadException.php │ └── Handler.php ├── Enums │ ├── PastedAction.php │ ├── ImagePermission.php │ ├── Watermark │ │ ├── Mode.php │ │ ├── FontOption.php │ │ └── ImageOption.php │ ├── UserStatus.php │ ├── Strategy │ │ ├── LocalOption.php │ │ ├── UssOption.php │ │ ├── KodoOption.php │ │ ├── OssOption.php │ │ ├── CosOption.php │ │ ├── WebDavOption.php │ │ ├── S3Option.php │ │ ├── FtpOption.php │ │ ├── MinioOption.php │ │ └── SftpOption.php │ ├── Scan │ │ ├── NsfwJsOption.php │ │ ├── TencentOption.php │ │ └── AliyunOption.php │ ├── UserConfigKey.php │ ├── StrategyKey.php │ ├── Mail │ │ └── SmtpOption.php │ ├── ConfigKey.php │ └── GroupConfigKey.php ├── Http │ ├── Controllers │ │ ├── Common │ │ │ ├── ApiController.php │ │ │ └── GalleryController.php │ │ ├── Api │ │ │ └── V1 │ │ │ │ ├── UserController.php │ │ │ │ ├── StrategyController.php │ │ │ │ ├── AlbumController.php │ │ │ │ └── TokenController.php │ │ ├── Auth │ │ │ ├── EmailVerificationPromptController.php │ │ │ ├── EmailVerificationNotificationController.php │ │ │ ├── VerifyEmailController.php │ │ │ ├── ConfirmablePasswordController.php │ │ │ ├── AuthenticatedSessionController.php │ │ │ ├── PasswordResetLinkController.php │ │ │ ├── RegisteredUserController.php │ │ │ └── NewPasswordController.php │ │ ├── User │ │ │ ├── UserController.php │ │ │ └── AlbumController.php │ │ └── Admin │ │ │ ├── SettingController.php │ │ │ └── StrategyController.php │ ├── Middleware │ │ ├── EncryptCookies.php │ │ ├── VerifyCsrfToken.php │ │ ├── PreventRequestsDuringMaintenance.php │ │ ├── TrustHosts.php │ │ ├── TrimStrings.php │ │ ├── Authenticate.php │ │ ├── AuthenticateWithAdmin.php │ │ ├── CheckIsEnableApi.php │ │ ├── CheckIsEnableGuestUpload.php │ │ ├── CheckIsEnableGallery.php │ │ ├── CheckIsEnableRegistration.php │ │ ├── CheckIsInstalled.php │ │ ├── TrustProxies.php │ │ └── RedirectIfAuthenticated.php │ ├── Requests │ │ ├── FormRequest.php │ │ ├── AlbumRequest.php │ │ ├── Admin │ │ │ └── UserRequest.php │ │ ├── ImageRenameRequest.php │ │ ├── UserSettingRequest.php │ │ └── Auth │ │ │ └── LoginRequest.php │ ├── Result.php │ └── Kernel.php ├── Models │ ├── Model.php │ ├── Config.php │ └── Album.php ├── View │ └── Components │ │ ├── AppLayout.php │ │ └── GuestLayout.php ├── Providers │ ├── BroadcastServiceProvider.php │ ├── AuthServiceProvider.php │ ├── EventServiceProvider.php │ ├── AppServiceProvider.php │ └── RouteServiceProvider.php ├── Mail │ └── Test.php ├── Console │ ├── Kernel.php │ └── Commands │ │ ├── Upgrade.php │ │ └── MakeThumbnails.php └── Services │ └── UserService.php ├── .gitattributes ├── .styleci.yml ├── .editorconfig ├── tests ├── Unit │ └── ExampleTest.php ├── Feature │ ├── ExampleTest.php │ ├── Auth │ │ ├── RegistrationTest.php │ │ ├── AuthenticationTest.php │ │ ├── PasswordConfirmationTest.php │ │ ├── EmailVerificationTest.php │ │ └── PasswordResetTest.php │ └── UtilTest.php ├── CreatesApplication.php └── TestCase.php ├── .gitignore ├── lang ├── zh_CN │ ├── pagination.php │ ├── auth.php │ ├── passwords.php │ └── validation-attributes.php ├── en │ ├── pagination.php │ ├── auth.php │ └── passwords.php └── en.json ├── config ├── image.php ├── cors.php ├── services.php ├── view.php ├── hashing.php ├── filesystems.php ├── broadcasting.php ├── sanctum.php └── flare.php ├── routes ├── image.php ├── channels.php ├── console.php ├── api.php └── auth.php ├── .env.example ├── tailwind.config.js ├── phpunit.xml ├── package.json ├── artisan ├── composer.json └── webpack.mix.js /database/.gitignore: -------------------------------------------------------------------------------- 1 | *.sqlite* 2 | -------------------------------------------------------------------------------- /bootstrap/cache/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/logs/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: 3 | -------------------------------------------------------------------------------- /public/thumbnails/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/app/public/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/app/uploads/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/debugbar/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/testing/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/views/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/cache/data/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/sessions/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/cache/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !data/ 3 | !.gitignore 4 | -------------------------------------------------------------------------------- /storage/app/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !public/ 3 | !uploads/ 4 | !.gitignore 5 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lsky-org/lsky-pro/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /resources/css/app.css: -------------------------------------------------------------------------------- 1 | @import 'tailwindcss/base'; 2 | @import 'tailwindcss/components'; 3 | @import 'tailwindcss/utilities'; 4 | -------------------------------------------------------------------------------- /resources/views/components/application-logo.blade.php: -------------------------------------------------------------------------------- 1 | {{ \App\Utils::config(\App\Enums\ConfigKey::AppName) }} 2 | -------------------------------------------------------------------------------- /resources/js/stores/sidebar.js: -------------------------------------------------------------------------------- 1 | export default { 2 | open: false, 3 | 4 | toggle() { 5 | this.open = ! this.open; 6 | } 7 | }; 8 | -------------------------------------------------------------------------------- /resources/views/emails/test.blade.php: -------------------------------------------------------------------------------- 1 |

2 | 您好,这是一封来自 {{ \App\Utils::config(\App\Enums\ConfigKey::AppName) }} 的测试邮件,当您看到这封邮件后,说明邮件配置正确。如果不是您本人操作,请忽略。 3 |

4 | -------------------------------------------------------------------------------- /app/Exceptions/UploadException.php: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | -------------------------------------------------------------------------------- /resources/views/user/upload.blade.php: -------------------------------------------------------------------------------- 1 | @section('title', '上传图片') 2 | 3 | 4 |
5 | 6 |
7 |
8 | -------------------------------------------------------------------------------- /app/Enums/PastedAction.php: -------------------------------------------------------------------------------- 1 | merge(['class' => 'my-2 bg-white rounded-md p-4 text-sm bg-gray-500 text-white overflow-x-auto']) }}> 2 | {{ $slot ?? '' }} 3 | 4 | -------------------------------------------------------------------------------- /resources/views/components/label.blade.php: -------------------------------------------------------------------------------- 1 | @props(['value']) 2 | 3 | 6 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | 3 | *.blade.php diff=html 4 | *.css diff=css 5 | *.html diff=html 6 | *.md diff=markdown 7 | *.php diff=php 8 | 9 | /.github export-ignore 10 | CHANGELOG.md export-ignore 11 | -------------------------------------------------------------------------------- /app/Enums/UserStatus.php: -------------------------------------------------------------------------------- 1 | 2 | 一个策略可以关联多个角色组,一个角色组也可以关联多个策略,注意,如果某个组未设置储存策略,那么该角色组下的用户将无法上传图片。 3 |

4 | -------------------------------------------------------------------------------- /resources/views/components/auth-session-status.blade.php: -------------------------------------------------------------------------------- 1 | @props(['status']) 2 | 3 | @if ($status) 4 |
merge(['class' => 'font-medium text-sm text-green-600']) }}> 5 | {{ $status }} 6 |
7 | @endif 8 | -------------------------------------------------------------------------------- /.styleci.yml: -------------------------------------------------------------------------------- 1 | php: 2 | preset: laravel 3 | version: 8 4 | disabled: 5 | - no_unused_imports 6 | finder: 7 | not-name: 8 | - index.php 9 | js: 10 | finder: 11 | not-name: 12 | - webpack.mix.js 13 | css: true 14 | -------------------------------------------------------------------------------- /resources/views/components/dropdown-link.blade.php: -------------------------------------------------------------------------------- 1 | merge(['class' => 'block px-4 py-2 active:bg-gray-100 text-sm text-gray-700 truncate hover:bg-sky-500 hover:text-white', 'role' => 'menuitem', 'tabindex' => '-1']) }}>{{ $slot }} 2 | -------------------------------------------------------------------------------- /app/Enums/Strategy/LocalOption.php: -------------------------------------------------------------------------------- 1 | '暂无数据']) 2 | 3 |
4 | 5 |

{{ $message }}

6 |
7 | -------------------------------------------------------------------------------- /resources/views/components/button.blade.php: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /resources/views/components/input.blade.php: -------------------------------------------------------------------------------- 1 | @props(['disabled' => false]) 2 | 3 | merge(['class' => 'mt-1 block w-full rounded-md bg-gray-100 border-transparent focus:border-gray-500 focus:bg-white focus:ring-0']) !!}> 4 | -------------------------------------------------------------------------------- /resources/views/components/box.blade.php: -------------------------------------------------------------------------------- 1 |
2 |
{{ $title }}
3 |
4 | {{ $content }} 5 |
6 |
7 | -------------------------------------------------------------------------------- /resources/views/components/default-avatar.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /resources/views/components/container.blade.php: -------------------------------------------------------------------------------- 1 | @props(['full' => request()->routeIs('images', 'gallery', 'admin.images')]) 2 | 3 |
merge(['class' => $full ? 'h-full mx-auto sm:ml-64' : 'h-full mx-auto sm:ml-64 px-6 md:px-10 lg:px-10 xl:px-10 2xl:px-60']) }}> 4 | {{ $slot }} 5 |
6 | -------------------------------------------------------------------------------- /resources/views/components/select.blade.php: -------------------------------------------------------------------------------- 1 | @props(['disabled' => false]) 2 | 3 | 6 | -------------------------------------------------------------------------------- /resources/views/components/textarea.blade.php: -------------------------------------------------------------------------------- 1 | @props(['disabled' => false]) 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/Http/Controllers/Common/ApiController.php: -------------------------------------------------------------------------------- 1 | 2 | {{ $title }} 3 | @isset($faq) 4 |

{!! $faq !!}

5 | @endisset 6 |
7 | {{ $slot }} 8 |
9 | 10 | -------------------------------------------------------------------------------- /app/Enums/Scan/NsfwJsOption.php: -------------------------------------------------------------------------------- 1 | format(CarbonInterface::DEFAULT_TO_STRING_FORMAT); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /app/Models/Config.php: -------------------------------------------------------------------------------- 1 | 2 |
3 | {{ $logo }} 4 |
5 | 6 |
7 | {{ $slot }} 8 |
9 | 10 | -------------------------------------------------------------------------------- /tests/Unit/ExampleTest.php: -------------------------------------------------------------------------------- 1 | assertTrue(true); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /installed.lock 2 | /upgrading.lock 3 | /*.zip 4 | /node_modules 5 | /public/hot 6 | /public/storage 7 | /public/js/custom.js 8 | /storage/*.key 9 | /vendor 10 | .env 11 | .env.backup 12 | .phpunit.result.cache 13 | docker-compose.override.yml 14 | Homestead.json 15 | Homestead.yaml 16 | npm-debug.log 17 | yarn-error.log 18 | /.idea 19 | /.vscode 20 | /public/i 21 | -------------------------------------------------------------------------------- /resources/views/components/fieldset-radio.blade.php: -------------------------------------------------------------------------------- 1 |
2 | merge(['id' => $id, 'name' => $name, 'class' => 'focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300', 'value' => $value ?? 0]) }}> 3 | 4 |
5 | -------------------------------------------------------------------------------- /resources/views/components/fieldset-checkbox.blade.php: -------------------------------------------------------------------------------- 1 |
2 | merge(['name' => $name, 'class' => 'focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300 rounded']) }}/> 3 | 4 |
5 | -------------------------------------------------------------------------------- /resources/views/components/nav-link.blade.php: -------------------------------------------------------------------------------- 1 | @props(['active']) 2 | 3 | @php 4 | $classes = "space-x-3 px-4 h-10 w-full flex items-center hover:bg-gray-100 text-slate-600 text-sm rounded-md" . (($active ?? false) ? ' bg-gray-100' : ''); 5 | @endphp 6 | 7 | merge(['class' => $classes]) }}> 8 | {{ $icon }} 9 | {{ $name }} 10 | 11 | -------------------------------------------------------------------------------- /app/Enums/Strategy/UssOption.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | protected $except = [ 15 | // 16 | ]; 17 | } 18 | -------------------------------------------------------------------------------- /app/View/Components/AppLayout.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | protected $except = [ 15 | // 16 | ]; 17 | } 18 | -------------------------------------------------------------------------------- /app/Http/Middleware/PreventRequestsDuringMaintenance.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | protected $except = [ 15 | // 16 | ]; 17 | } 18 | -------------------------------------------------------------------------------- /app/Http/Middleware/TrustHosts.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | public function hosts() 15 | { 16 | return [ 17 | $this->allSubdomainsOfApplicationUrl(), 18 | ]; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /resources/views/components/loading-spin.blade.php: -------------------------------------------------------------------------------- 1 | merge(['class' => 'animate-spin -ml-1 mr-3 h-5 w-5 text-red-500']) }} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"> 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/Http/Middleware/TrimStrings.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | protected $except = [ 15 | 'current_password', 16 | 'password', 17 | 'password_confirmation', 18 | ]; 19 | } 20 | -------------------------------------------------------------------------------- /tests/Feature/ExampleTest.php: -------------------------------------------------------------------------------- 1 | get('/'); 18 | 19 | $response->assertStatus(302); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /tests/CreatesApplication.php: -------------------------------------------------------------------------------- 1 | make(Kernel::class)->bootstrap(); 19 | 20 | return $app; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/Enums/Scan/TencentOption.php: -------------------------------------------------------------------------------- 1 | any()) 4 |
5 |
6 | {{ __('Whoops! Something went wrong.') }} 7 |
8 | 9 | 14 |
15 | @endif 16 | -------------------------------------------------------------------------------- /app/Enums/Strategy/OssOption.php: -------------------------------------------------------------------------------- 1 | '下一页 »', 16 | 'previous' => '« 上一页', 17 | ]; 18 | -------------------------------------------------------------------------------- /app/Http/Middleware/Authenticate.php: -------------------------------------------------------------------------------- 1 | expectsJson()) { 18 | return route('login'); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/Enums/Strategy/S3Option.php: -------------------------------------------------------------------------------- 1 | fail($validator->errors()->first()))); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/Http/Middleware/AuthenticateWithAdmin.php: -------------------------------------------------------------------------------- 1 | authenticate($request, $guards); 14 | 15 | /** @var User $user */ 16 | $user = Auth::user(); 17 | 18 | if (! $user->is_adminer) { 19 | return abort(403); 20 | } 21 | 22 | return $next($request); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /config/image.php: -------------------------------------------------------------------------------- 1 | 'imagick' 19 | ]; 20 | -------------------------------------------------------------------------------- /routes/image.php: -------------------------------------------------------------------------------- 1 | group(function () use ($extensions) { 10 | $extensions = array_merge(array_map('strtoupper', $extensions), array_map('strtolower', $extensions)); 11 | Route::any('{key}.{extension}', [ 12 | Controller::class, 'output', 13 | ])->where('extension', implode('|', $extensions)); 14 | }); 15 | -------------------------------------------------------------------------------- /lang/zh_CN/auth.php: -------------------------------------------------------------------------------- 1 | '用户名或密码错误。', 16 | 'password' => '密码错误。', 17 | 'throttle' => '您尝试的登录次数过多,请 :seconds 秒后再试。', 18 | ]; 19 | -------------------------------------------------------------------------------- /lang/en/pagination.php: -------------------------------------------------------------------------------- 1 | '« Previous', 17 | 'next' => 'Next »', 18 | 19 | ]; 20 | -------------------------------------------------------------------------------- /routes/channels.php: -------------------------------------------------------------------------------- 1 | id === (int) $id; 18 | }); 19 | -------------------------------------------------------------------------------- /app/Http/Middleware/CheckIsEnableApi.php: -------------------------------------------------------------------------------- 1 | expectsJson()) { 19 | return $this->fail('管理员未启用 API')->setStatusCode(403); 20 | } 21 | abort(404); 22 | } 23 | 24 | return $next($request); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/Http/Middleware/CheckIsEnableGuestUpload.php: -------------------------------------------------------------------------------- 1 | $value) { 19 | $content = is_array($value) ? json_encode($value, JSON_UNESCAPED_UNICODE) : $value; 20 | Config::query()->firstOrCreate(['name' => $key], ['value' => $content]); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/Enums/Watermark/FontOption.php: -------------------------------------------------------------------------------- 1 | expectsJson()) { 19 | return $this->fail('管理员未启用画廊功能')->setStatusCode(403); 20 | } 21 | abort(404); 22 | } 23 | 24 | return $next($request); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /public/css/gallery.css: -------------------------------------------------------------------------------- 1 | .images-grid{margin:0 auto}.images-grid:after{clear:both;content:"";display:block}.grid-item,.grid-sizer{padding:8px;width:50%}.grid-item{float:left}.grid-item>div{transition:all .3s}.grid-item>div:hover{box-shadow:15.8px 21.3px 83.8px rgba(0,0,0,.07),102px 137px 196px rgba(0,0,0,.035);margin-top:-5px}@media screen and (min-width:640px){.grid-item,.grid-sizer{width:33.333%}}@media screen and (min-width:768px){.grid-item,.grid-sizer{width:25%}}@media screen and (min-width:1024px){.grid-item,.grid-sizer{width:20%}}@media screen and (min-width:1280px){.grid-item,.grid-sizer{width:12.5%}}@media screen and (min-width:1536px){.grid-item,.grid-sizer{width:10%}} 2 | -------------------------------------------------------------------------------- /app/Enums/Strategy/FtpOption.php: -------------------------------------------------------------------------------- 1 | expectsJson()) { 19 | return $this->fail('站点管理员关闭了注册功能')->setStatusCode(403); 20 | } 21 | abort(404); 22 | } 23 | 24 | return $next($request); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/Http/Middleware/CheckIsInstalled.php: -------------------------------------------------------------------------------- 1 | expectsJson()) { 18 | return redirect('install'); 19 | } else { 20 | return $this->fail('It has already been installed.'); 21 | } 22 | } 23 | 24 | return $next($request); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/Http/Result.php: -------------------------------------------------------------------------------- 1 | response(true, $message, $data); 12 | } 13 | 14 | public function fail(string $message = 'fail', $data = []): Response 15 | { 16 | return $this->response(false, $message, $data); 17 | } 18 | 19 | public function response(bool $status, string $message = '', $data = []): Response 20 | { 21 | $data = $data ?: new \stdClass; 22 | return response(compact('status', 'message', 'data')); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /routes/console.php: -------------------------------------------------------------------------------- 1 | comment(Inspiring::quote()); 19 | })->purpose('Display an inspiring quote'); 20 | -------------------------------------------------------------------------------- /lang/zh_CN/passwords.php: -------------------------------------------------------------------------------- 1 | '密码重置成功!', 16 | 'sent' => '密码重置邮件已发送!', 17 | 'throttled' => '请稍候再试。', 18 | 'token' => '密码重置令牌无效。', 19 | 'user' => '找不到该邮箱对应的用户。', 20 | ]; 21 | -------------------------------------------------------------------------------- /app/Mail/Test.php: -------------------------------------------------------------------------------- 1 | view('emails.test'); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /lang/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "The :attribute must contain at least one letter.": "The :attribute must contain at least one letter.", 3 | "The :attribute must contain at least one number.": "The :attribute must contain at least one number.", 4 | "The :attribute must contain at least one symbol.": "The :attribute must contain at least one symbol.", 5 | "The :attribute must contain at least one uppercase and one lowercase letter.": "The :attribute must contain at least one uppercase and one lowercase letter.", 6 | "The given :attribute has appeared in a data leak. Please choose a different :attribute.": "The given :attribute has appeared in a data leak. Please choose a different :attribute." 7 | } 8 | -------------------------------------------------------------------------------- /public/.htaccess: -------------------------------------------------------------------------------- 1 | 2 | 3 | Options -MultiViews -Indexes 4 | 5 | 6 | RewriteEngine On 7 | 8 | # Handle Authorization Header 9 | RewriteCond %{HTTP:Authorization} . 10 | RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] 11 | 12 | # Redirect Trailing Slashes If Not A Folder... 13 | RewriteCond %{REQUEST_FILENAME} !-d 14 | RewriteCond %{REQUEST_URI} (.+)/$ 15 | RewriteRule ^ %1 [L,R=301] 16 | 17 | # Send Requests To Front Controller... 18 | RewriteCond %{REQUEST_FILENAME} !-d 19 | RewriteCond %{REQUEST_FILENAME} !-f 20 | RewriteRule ^ index.php [L] 21 | 22 | -------------------------------------------------------------------------------- /app/Enums/StrategyKey.php: -------------------------------------------------------------------------------- 1 | with('user') 16 | ->whereNotNull('user_id') 17 | ->where('is_unhealthy', false) 18 | ->where('permission', ImagePermission::Public) 19 | ->latest() 20 | ->simplePaginate(40); 21 | return view('common.gallery', compact('images')); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/Http/Controllers/Api/V1/UserController.php: -------------------------------------------------------------------------------- 1 | used_capacity = $user->images()->sum('size') + 0; 17 | $user->setVisible([ 18 | 'name', 'avatar', 'email', 'capacity', 'used_capacity', 'url', 'image_num', 'album_num', 'registered_ip' 19 | ]); 20 | return $this->success('success', $user); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/Http/Controllers/Auth/EmailVerificationPromptController.php: -------------------------------------------------------------------------------- 1 | user()->hasVerifiedEmail() 20 | ? redirect()->intended(RouteServiceProvider::HOME) 21 | : view('auth.verify-email'); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/Enums/Mail/SmtpOption.php: -------------------------------------------------------------------------------- 1 | 3 |
4 | 5 |
6 | 7 | 8 | @push('scripts') 9 | 14 | @endpush 15 | @endif 16 | -------------------------------------------------------------------------------- /app/Http/Controllers/Api/V1/StrategyController.php: -------------------------------------------------------------------------------- 1 | group : Group::query()->where('is_guest', true)->first(); 17 | $strategies = $group->strategies()->get()->each(fn (Strategy $strategy) => $strategy->setVisible(['id', 'name'])); 18 | return $this->success('success', compact('strategies')); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /resources/views/admin/group/tips.blade.php: -------------------------------------------------------------------------------- 1 | @if(! ini_get('file_uploads')) 2 |

3 | 当前系统监测到运行环境关闭了 HTTP 上传文件权限(file_uploads=off),请更改 PHP 此项配置,否则无法上传文件。 4 |

5 | @endif 6 |

7 | 系统运行环境允许上传大小的最大值为 {{ ini_get('upload_max_filesize') }},最大 POST 数据大小为 {{ ini_get('post_max_size') }},上传文件大小不得超过这两项配置值。 8 |

9 |

10 | 原图保护以及水印功能,原理是使用 PHP 接管图片请求,动态处理后缓存之后通过载入缓存到内存中输出图片,对服务器有着较高的要求,请谨慎使用。如果你使用第三方储存,兰空图床更推荐你使用第三方储存的图片处理规则。 11 |

12 | -------------------------------------------------------------------------------- /app/Http/Requests/AlbumRequest.php: -------------------------------------------------------------------------------- 1 | 'required|max:60|alpha_dash', 16 | 'intro' => 'max:600' 17 | ]; 18 | } 19 | 20 | public function messages() 21 | { 22 | return [ 23 | 'name.required' => '名称不能为空', 24 | 'name.max' => '名称字符过长,最大不能超过 60', 25 | 'name.alpha_dash' => '名称只能是字母、数字,短破折号(-)和下划线(_)', 26 | 'intro.max' => '简介字符过长,最大不能超过 600' 27 | ]; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/Enums/Strategy/SftpOption.php: -------------------------------------------------------------------------------- 1 | 'These credentials do not match our records.', 17 | 'password' => 'The provided password is incorrect.', 18 | 'throttle' => 'Too many login attempts. Please try again in :seconds seconds.', 19 | 20 | ]; 21 | -------------------------------------------------------------------------------- /app/Providers/AuthServiceProvider.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | protected $policies = [ 16 | // 'App\Models\Model' => 'App\Policies\ModelPolicy', 17 | ]; 18 | 19 | /** 20 | * Register any authentication / authorization services. 21 | * 22 | * @return void 23 | */ 24 | public function boot() 25 | { 26 | $this->registerPolicies(); 27 | 28 | // 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | APP_NAME="Lsky Pro" 2 | APP_ENV=prod 3 | APP_KEY= 4 | APP_DEBUG=false 5 | APP_URL=http://localhost 6 | 7 | LOG_CHANNEL=daily 8 | LOG_DEPRECATIONS_CHANNEL=null 9 | LOG_LEVEL=debug 10 | 11 | DB_CONNECTION= 12 | DB_HOST= 13 | DB_PORT= 14 | DB_DATABASE= 15 | DB_USERNAME= 16 | DB_PASSWORD= 17 | 18 | BROADCAST_DRIVER=log 19 | CACHE_DRIVER=file 20 | FILESYSTEM_DISK=public 21 | QUEUE_CONNECTION=sync 22 | SESSION_DRIVER=file 23 | SESSION_LIFETIME=120 24 | 25 | MEMCACHED_HOST=127.0.0.1 26 | 27 | REDIS_HOST=127.0.0.1 28 | REDIS_PASSWORD=null 29 | REDIS_PORT=6379 30 | 31 | PUSHER_APP_ID= 32 | PUSHER_APP_KEY= 33 | PUSHER_APP_SECRET= 34 | PUSHER_APP_CLUSTER=mt1 35 | 36 | MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}" 37 | MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}" 38 | 39 | IGNITION_SHARING_ENABLED=false 40 | -------------------------------------------------------------------------------- /app/Http/Middleware/TrustProxies.php: -------------------------------------------------------------------------------- 1 | |string|null 14 | */ 15 | protected $proxies = '*'; 16 | 17 | /** 18 | * The headers that should be used to detect proxies. 19 | * 20 | * @var int 21 | */ 22 | protected $headers = 23 | Request::HEADER_X_FORWARDED_FOR | 24 | Request::HEADER_X_FORWARDED_HOST | 25 | Request::HEADER_X_FORWARDED_PORT | 26 | Request::HEADER_X_FORWARDED_PROTO | 27 | Request::HEADER_X_FORWARDED_AWS_ELB; 28 | } 29 | -------------------------------------------------------------------------------- /app/Console/Kernel.php: -------------------------------------------------------------------------------- 1 | command('inspire')->hourly(); 19 | } 20 | 21 | /** 22 | * Register the commands for the application. 23 | * 24 | * @return void 25 | */ 26 | protected function commands() 27 | { 28 | $this->load(__DIR__.'/Commands'); 29 | 30 | require base_path('routes/console.php'); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /lang/en/passwords.php: -------------------------------------------------------------------------------- 1 | 'Your password has been reset!', 17 | 'sent' => 'We have emailed your password reset link!', 18 | 'throttled' => 'Please wait before retrying.', 19 | 'token' => 'This password reset token is invalid.', 20 | 'user' => "We can't find a user with that email address.", 21 | 22 | ]; 23 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | const defaultTheme = require('tailwindcss/defaultTheme'); 2 | 3 | module.exports = { 4 | darkMode: 'class', 5 | content: [ 6 | './vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php', 7 | './storage/framework/views/*.php', 8 | './resources/views/**/*.blade.php', 9 | ], 10 | 11 | theme: { 12 | extend: { 13 | fontFamily: { 14 | sans: ['Nunito', ...defaultTheme.fontFamily.sans], 15 | }, 16 | boxShadow: { 17 | custom: '0px 4px 6px -1px rgba(0, 0, 0, 0.04)', 18 | }, 19 | }, 20 | }, 21 | 22 | variants: { 23 | extend: { 24 | opacity: ['disabled'], 25 | }, 26 | }, 27 | 28 | plugins: [require('@tailwindcss/forms'), require('autoprefixer')], 29 | }; 30 | -------------------------------------------------------------------------------- /database/migrations/2014_10_12_100000_create_password_resets_table.php: -------------------------------------------------------------------------------- 1 | string('email')->index(); 18 | $table->string('token'); 19 | $table->timestamp('created_at')->nullable(); 20 | }); 21 | } 22 | 23 | /** 24 | * Reverse the migrations. 25 | * 26 | * @return void 27 | */ 28 | public function down() 29 | { 30 | Schema::dropIfExists('password_resets'); 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /database/migrations/2021_12_11_200033_create_configs_table.php: -------------------------------------------------------------------------------- 1 | string('name', 32)->comment('配置名')->unique(); 18 | $table->longText('value')->nullable()->comment('配置值'); 19 | $table->timestamps(); 20 | }); 21 | } 22 | 23 | /** 24 | * Reverse the migrations. 25 | * 26 | * @return void 27 | */ 28 | public function down() 29 | { 30 | Schema::dropIfExists('configs'); 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | links = \config('filesystems.links'); 19 | $this->seed(InstallSeeder::class); 20 | } 21 | 22 | protected function tearDown(): void 23 | { 24 | parent::tearDown(); 25 | 26 | foreach (array_flip($this->links) as $link) { 27 | @unlink($link); 28 | // 因 phpunit 运行时根目录和 env 同级,所以创建的符号链接被放到了根目录 29 | // 清理根目录生成的符号链接 30 | @unlink(str_replace('/public', '', $link)); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/Http/Controllers/Auth/EmailVerificationNotificationController.php: -------------------------------------------------------------------------------- 1 | user()->hasVerifiedEmail()) { 20 | return redirect()->intended(RouteServiceProvider::HOME); 21 | } 22 | 23 | $request->user()->sendEmailVerificationNotification(); 24 | 25 | return back()->with('status', 'verification-link-sent'); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/Providers/EventServiceProvider.php: -------------------------------------------------------------------------------- 1 | > 16 | */ 17 | protected $listen = [ 18 | Registered::class => [ 19 | SendEmailVerificationNotification::class, 20 | ], 21 | ]; 22 | 23 | /** 24 | * Register any events for your application. 25 | * 26 | * @return void 27 | */ 28 | public function boot() 29 | { 30 | // 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /database/migrations/2022_01_20_201231_create_group_strategy_table.php: -------------------------------------------------------------------------------- 1 | foreignId('group_id')->comment('角色组')->constrained('groups')->onDelete('cascade'); 18 | $table->foreignId('strategy_id')->comment('策略')->constrained('strategies')->onDelete('cascade'); 19 | }); 20 | } 21 | 22 | /** 23 | * Reverse the migrations. 24 | * 25 | * @return void 26 | */ 27 | public function down() 28 | { 29 | Schema::dropIfExists('group_strategy'); 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /tests/Feature/Auth/RegistrationTest.php: -------------------------------------------------------------------------------- 1 | get('/register'); 16 | 17 | $response->assertStatus(200); 18 | } 19 | 20 | public function test_new_users_can_register() 21 | { 22 | $response = $this->post('/register', [ 23 | 'name' => 'Test User', 24 | 'email' => 'test@example.com', 25 | 'password' => 'password', 26 | 'password_confirmation' => 'password', 27 | ]); 28 | 29 | $this->assertAuthenticated(); 30 | $response->assertRedirect(RouteServiceProvider::HOME); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/Http/Requests/Admin/UserRequest.php: -------------------------------------------------------------------------------- 1 | 'required', 19 | 'name' => 'required|between:2,30', 20 | 'capacity' => 'required|numeric', 21 | 'password' => ['nullable', Rules\Password::defaults()], 22 | 'status' => 'in:1,0' 23 | ]; 24 | } 25 | 26 | public function attributes() 27 | { 28 | return [ 29 | 'group_id' => '角色组', 30 | 'name' => '昵称', 31 | 'capacity' => '总容量', 32 | 'password' => '密码', 33 | 'status' => '状态', 34 | ]; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /public/js/app.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /*! 2 | * Sizzle CSS Selector Engine v2.3.6 3 | * https://sizzlejs.com/ 4 | * 5 | * Copyright JS Foundation and other contributors 6 | * Released under the MIT license 7 | * https://js.foundation/ 8 | * 9 | * Date: 2021-02-16 10 | */ 11 | 12 | /*! 13 | * jQuery JavaScript Library v3.6.0 14 | * https://jquery.com/ 15 | * 16 | * Includes Sizzle.js 17 | * https://sizzlejs.com/ 18 | * 19 | * Copyright OpenJS Foundation and other contributors 20 | * Released under the MIT license 21 | * https://jquery.org/license 22 | * 23 | * Date: 2021-03-02T17:08Z 24 | */ 25 | 26 | /** 27 | * @license 28 | * Lodash 29 | * Copyright OpenJS Foundation and other contributors 30 | * Released under MIT license 31 | * Based on Underscore.js 1.8.3 32 | * Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors 33 | */ 34 | -------------------------------------------------------------------------------- /config/cors.php: -------------------------------------------------------------------------------- 1 | ['api/*', 'sanctum/csrf-cookie'], 19 | 20 | 'allowed_methods' => ['*'], 21 | 22 | 'allowed_origins' => ['*'], 23 | 24 | 'allowed_origins_patterns' => [], 25 | 26 | 'allowed_headers' => ['*'], 27 | 28 | 'exposed_headers' => [], 29 | 30 | 'max_age' => 0, 31 | 32 | 'supports_credentials' => false, 33 | 34 | ]; 35 | -------------------------------------------------------------------------------- /app/Http/Requests/ImageRenameRequest.php: -------------------------------------------------------------------------------- 1 | 'required|numeric', 26 | 'name' => 'required|max:50|string', 27 | ]; 28 | } 29 | 30 | public function messages() 31 | { 32 | return [ 33 | 'id.required' => '请选择一张图片', 34 | 'id.numeric' => '图片选择异常', 35 | 'name.required' => '请输入名称', 36 | 'name.max' => '名称长度不能超过 50 个字符', 37 | 'name.string' => '名称格式不正确', 38 | ]; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /resources/js/stores/modal.js: -------------------------------------------------------------------------------- 1 | export default { 2 | state: {}, 3 | 4 | open(id) { 5 | this.setState(id, {open: true}); 6 | }, 7 | 8 | close(id) { 9 | this.setState(id, {open: false}); 10 | }, 11 | 12 | isOpen(id) { 13 | return this.getState(id).open; 14 | }, 15 | 16 | toggle(id) { 17 | let state = this.getState(id); 18 | return this.setState(id, {open: state.open = ! state.open}); 19 | }, 20 | 21 | isLoading(id) { 22 | return this.getState(id).loading ? true : false; 23 | }, 24 | 25 | setLoading(id, loading) { 26 | this.setState(id, loading); 27 | }, 28 | 29 | setState(id, data) { 30 | if (this.state[id] === undefined) { 31 | this.state[id] = {}; 32 | } 33 | for (let dataKey in data) { 34 | this.state[id][dataKey] = data[dataKey]; 35 | } 36 | }, 37 | 38 | getState(id) { 39 | return this.state[id] || {}; 40 | } 41 | }; 42 | -------------------------------------------------------------------------------- /database/migrations/2019_08_19_000000_create_failed_jobs_table.php: -------------------------------------------------------------------------------- 1 | id(); 18 | $table->string('uuid')->unique(); 19 | $table->text('connection'); 20 | $table->text('queue'); 21 | $table->longText('payload'); 22 | $table->longText('exception'); 23 | $table->timestamp('failed_at')->useCurrent(); 24 | }); 25 | } 26 | 27 | /** 28 | * Reverse the migrations. 29 | * 30 | * @return void 31 | */ 32 | public function down() 33 | { 34 | Schema::dropIfExists('failed_jobs'); 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /resources/views/layouts/header.blade.php: -------------------------------------------------------------------------------- 1 |
2 | 3 | 9 |
10 | @includeWhen($_is_notice, 'layouts.notice') 11 | @includeWhen($_group->strategies->isNotEmpty(), 'layouts.strategies') 12 | @include('layouts.user-nav') 13 |
14 |
15 |
16 | -------------------------------------------------------------------------------- /app/Console/Commands/Upgrade.php: -------------------------------------------------------------------------------- 1 | upgrade() ? 0 : 1; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /app/Http/Middleware/RedirectIfAuthenticated.php: -------------------------------------------------------------------------------- 1 | check()) { 26 | return redirect(RouteServiceProvider::HOME); 27 | } 28 | } 29 | 30 | return $next($request); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /resources/views/common/notice.blade.php: -------------------------------------------------------------------------------- 1 | @if($_is_notice) 2 | 3 | 4 |
5 | {!! (new Parsedown())->parse(\App\Utils::config(\App\Enums\ConfigKey::SiteNotice)) !!} 6 |
7 |
8 | OK 9 |
10 |
11 | 12 | @push('scripts') 13 | 27 | @endpush 28 | @endif 29 | -------------------------------------------------------------------------------- /app/Http/Controllers/Auth/VerifyEmailController.php: -------------------------------------------------------------------------------- 1 | user()->hasVerifiedEmail()) { 21 | return redirect()->intended(RouteServiceProvider::HOME.'?verified=1'); 22 | } 23 | 24 | if ($request->user()->markEmailAsVerified()) { 25 | event(new Verified($request->user())); 26 | } 27 | 28 | return redirect()->intended(RouteServiceProvider::HOME.'?verified=1'); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /database/migrations/2019_12_14_000001_create_personal_access_tokens_table.php: -------------------------------------------------------------------------------- 1 | id(); 18 | $table->morphs('tokenable'); 19 | $table->string('name'); 20 | $table->string('token', 64)->unique(); 21 | $table->text('abilities')->nullable(); 22 | $table->timestamp('last_used_at')->nullable(); 23 | $table->timestamps(); 24 | }); 25 | } 26 | 27 | /** 28 | * Reverse the migrations. 29 | * 30 | * @return void 31 | */ 32 | public function down() 33 | { 34 | Schema::dropIfExists('personal_access_tokens'); 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /app/Http/Controllers/Api/V1/AlbumController.php: -------------------------------------------------------------------------------- 1 | albums()->filter($request)->paginate(40); 19 | $albums->getCollection()->each(function (Album $album) { 20 | $album->setVisible(['id', 'name', 'intro', 'image_num']); 21 | }); 22 | return $this->success('success', $albums); 23 | } 24 | 25 | public function destroy(Request $request): Response 26 | { 27 | /** @var User $user */ 28 | $user = Auth::user(); 29 | $user->albums()->where('id', $request->route('id'))->delete(); 30 | return $this->success('删除成功'); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /resources/views/components/table.blade.php: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | 6 | 7 | 8 | @foreach($columns as $column) 9 | 12 | @endforeach 13 | 14 | 15 | 16 | {{ $slot }} 17 | 18 |
10 | {{ $column }} 11 |
19 |
20 |
21 |
22 |
23 | -------------------------------------------------------------------------------- /config/services.php: -------------------------------------------------------------------------------- 1 | [ 18 | 'domain' => env('MAILGUN_DOMAIN'), 19 | 'secret' => env('MAILGUN_SECRET'), 20 | 'endpoint' => env('MAILGUN_ENDPOINT', 'api.mailgun.net'), 21 | ], 22 | 23 | 'postmark' => [ 24 | 'token' => env('POSTMARK_TOKEN'), 25 | ], 26 | 27 | 'ses' => [ 28 | 'key' => env('AWS_ACCESS_KEY_ID'), 29 | 'secret' => env('AWS_SECRET_ACCESS_KEY'), 30 | 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), 31 | ], 32 | 33 | ]; 34 | -------------------------------------------------------------------------------- /database/migrations/2021_12_11_184521_create_strategies_table.php: -------------------------------------------------------------------------------- 1 | engine = 'InnoDB'; 18 | $table->charset = 'utf8mb4'; 19 | $table->collation = 'utf8mb4_unicode_ci'; 20 | 21 | $table->id(); 22 | $table->unsignedTinyInteger('key'); 23 | $table->string('name', 64)->comment('策略名称'); 24 | $table->string('intro', 255)->default('')->comment('简介'); 25 | $table->json('configs')->comment('策略配置'); 26 | $table->timestamps(); 27 | }); 28 | } 29 | 30 | /** 31 | * Reverse the migrations. 32 | * 33 | * @return void 34 | */ 35 | public function down() 36 | { 37 | Schema::dropIfExists('strategies'); 38 | } 39 | }; 40 | -------------------------------------------------------------------------------- /database/migrations/2014_10_10_000000_create_groups_table.php: -------------------------------------------------------------------------------- 1 | engine = 'InnoDB'; 18 | $table->charset = 'utf8mb4'; 19 | $table->collation = 'utf8mb4_unicode_ci'; 20 | 21 | $table->id(); 22 | $table->string('name', 64)->comment('角色组名称'); 23 | $table->boolean('is_default')->default(false)->comment('是否默认'); 24 | $table->boolean('is_guest')->default(false)->comment('是否为游客组'); 25 | $table->json('configs')->comment('组配置'); 26 | $table->timestamps(); 27 | }); 28 | } 29 | 30 | /** 31 | * Reverse the migrations. 32 | * 33 | * @return void 34 | */ 35 | public function down() 36 | { 37 | Schema::dropIfExists('groups'); 38 | } 39 | }; 40 | -------------------------------------------------------------------------------- /resources/views/components/dropdown.blade.php: -------------------------------------------------------------------------------- 1 | @props(['classes' => ['left' => 'origin-top-right right-0', 'right' => 'origin-top-left left-0'], 'direction' => 'left']) 2 | 3 |
4 |
5 | {{ $trigger }} 6 |
7 | 8 | 24 |
25 | -------------------------------------------------------------------------------- /config/view.php: -------------------------------------------------------------------------------- 1 | [ 17 | resource_path('views'), 18 | ], 19 | 20 | /* 21 | |-------------------------------------------------------------------------- 22 | | Compiled View Path 23 | |-------------------------------------------------------------------------- 24 | | 25 | | This option determines where all the compiled Blade templates will be 26 | | stored for your application. Typically, this is within the storage 27 | | directory. However, as usual, you are free to change this value. 28 | | 29 | */ 30 | 31 | 'compiled' => env( 32 | 'VIEW_COMPILED_PATH', 33 | realpath(storage_path('framework/views')) 34 | ), 35 | 36 | ]; 37 | -------------------------------------------------------------------------------- /database/migrations/2021_12_11_185759_create_albums_table.php: -------------------------------------------------------------------------------- 1 | engine = 'InnoDB'; 18 | $table->charset = 'utf8mb4'; 19 | $table->collation = 'utf8mb4_unicode_ci'; 20 | 21 | $table->id(); 22 | $table->foreignId('user_id')->comment('用户')->constrained('users')->onDelete('cascade'); 23 | $table->string('name', 64)->comment('名称'); 24 | $table->string('intro')->default('')->comment('简介'); 25 | $table->unsignedBigInteger('image_num')->default(0)->comment('图片数量'); 26 | $table->timestamps(); 27 | }); 28 | } 29 | 30 | /** 31 | * Reverse the migrations. 32 | * 33 | * @return void 34 | */ 35 | public function down() 36 | { 37 | Schema::dropIfExists('albums'); 38 | } 39 | }; 40 | -------------------------------------------------------------------------------- /resources/css/gallery.less: -------------------------------------------------------------------------------- 1 | .images-grid { 2 | margin: 0 auto; 3 | } 4 | 5 | .images-grid:after { 6 | content: ''; 7 | display: block; 8 | clear: both; 9 | } 10 | 11 | .grid-sizer, .grid-item { 12 | padding: 8px; 13 | width: calc(50%); 14 | } 15 | 16 | .grid-item { 17 | float: left; 18 | & > div { 19 | transition: all .3s; 20 | 21 | &:hover { 22 | margin-top: -5px; 23 | box-shadow: 15.8px 21.3px 83.8px rgba(0, 0, 0, 0.07), 102px 137px 196px rgba(0, 0, 0, 0.035); 24 | } 25 | } 26 | } 27 | 28 | @media screen and (min-width: 640px) { 29 | .grid-sizer, .grid-item { 30 | width: calc(33.333%); 31 | } 32 | } 33 | 34 | @media screen and (min-width: 768px) { 35 | .grid-sizer, .grid-item { 36 | width: calc(25%); 37 | } 38 | } 39 | 40 | @media screen and (min-width: 1024px) { 41 | .grid-sizer, .grid-item { 42 | width: calc(20%); 43 | } 44 | } 45 | 46 | @media screen and (min-width: 1280px) { 47 | .grid-sizer, .grid-item { 48 | width: calc(12.5%); 49 | } 50 | } 51 | 52 | @media screen and (min-width: 1536px) { 53 | .grid-sizer, .grid-item { 54 | width: calc(10%); 55 | } 56 | } 57 | 58 | -------------------------------------------------------------------------------- /database/factories/UserFactory.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | class UserFactory extends Factory 12 | { 13 | /** 14 | * Define the model's default state. 15 | * 16 | * @return array 17 | */ 18 | public function definition() 19 | { 20 | return [ 21 | 'name' => $this->faker->name(), 22 | 'email' => $this->faker->unique()->safeEmail(), 23 | 'email_verified_at' => now(), 24 | 'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password 25 | 'remember_token' => Str::random(10), 26 | ]; 27 | } 28 | 29 | /** 30 | * Indicate that the model's email address should be unverified. 31 | * 32 | * @return \Illuminate\Database\Eloquent\Factories\Factory 33 | */ 34 | public function unverified() 35 | { 36 | return $this->state(function (array $attributes) { 37 | return [ 38 | 'email_verified_at' => null, 39 | ]; 40 | }); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /tests/Feature/Auth/AuthenticationTest.php: -------------------------------------------------------------------------------- 1 | get('/login'); 17 | 18 | $response->assertStatus(200); 19 | } 20 | 21 | public function test_users_can_authenticate_using_the_login_screen() 22 | { 23 | $user = User::factory()->create(); 24 | 25 | $response = $this->post('/login', [ 26 | 'email' => $user->email, 27 | 'password' => 'password', 28 | ]); 29 | 30 | $this->assertAuthenticated(); 31 | $response->assertRedirect(RouteServiceProvider::HOME); 32 | } 33 | 34 | public function test_users_can_not_authenticate_with_invalid_password() 35 | { 36 | $user = User::factory()->create(); 37 | 38 | $this->post('/login', [ 39 | 'email' => $user->email, 40 | 'password' => 'wrong-password', 41 | ]); 42 | 43 | $this->assertGuest(); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /tests/Feature/Auth/PasswordConfirmationTest.php: -------------------------------------------------------------------------------- 1 | create(); 16 | 17 | $response = $this->actingAs($user)->get('/confirm-password'); 18 | 19 | $response->assertStatus(200); 20 | } 21 | 22 | public function test_password_can_be_confirmed() 23 | { 24 | $user = User::factory()->create(); 25 | 26 | $response = $this->actingAs($user)->post('/confirm-password', [ 27 | 'password' => 'password', 28 | ]); 29 | 30 | $response->assertRedirect(); 31 | $response->assertSessionHasNoErrors(); 32 | } 33 | 34 | public function test_password_is_not_confirmed_with_invalid_password() 35 | { 36 | $user = User::factory()->create(); 37 | 38 | $response = $this->actingAs($user)->post('/confirm-password', [ 39 | 'password' => 'wrong-password', 40 | ]); 41 | 42 | $response->assertSessionHasErrors(); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /app/Enums/ConfigKey.php: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | ./tests/Unit 10 | 11 | 12 | ./tests/Feature 13 | 14 | 15 | 16 | 17 | ./app 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /resources/views/layouts/user-nav.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | 11 | 12 | 13 |
14 | @csrf 15 | 我的图片 16 | 仪表盘 17 | 设置 18 | 19 | {{ __('Log Out') }} 20 | 21 |
22 |
23 |
24 | -------------------------------------------------------------------------------- /app/Http/Controllers/Auth/ConfirmablePasswordController.php: -------------------------------------------------------------------------------- 1 | validate([ 32 | 'email' => $request->user()->email, 33 | 'password' => $request->password, 34 | ])) { 35 | throw ValidationException::withMessages([ 36 | 'password' => __('auth.password'), 37 | ]); 38 | } 39 | 40 | $request->session()->put('auth.password_confirmed_at', time()); 41 | 42 | return redirect()->intended(RouteServiceProvider::HOME); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /resources/views/auth/confirm-password.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | {{ __('This is a secure area of the application. Please confirm your password before continuing.') }} 11 |
12 | 13 | 14 | 15 | 16 |
17 | @csrf 18 | 19 | 20 |
21 | 22 | 23 | 27 |
28 | 29 |
30 | 31 | {{ __('Confirm') }} 32 | 33 |
34 |
35 |
36 |
37 | -------------------------------------------------------------------------------- /lang/zh_CN/validation-attributes.php: -------------------------------------------------------------------------------- 1 | [ 5 | 'address' => '地址', 6 | 'age' => '年龄', 7 | 'available' => '可用的', 8 | 'city' => '城市', 9 | 'content' => '内容', 10 | 'country' => '国家', 11 | 'date' => '日期', 12 | 'day' => '天', 13 | 'description' => '描述', 14 | 'email' => '邮箱', 15 | 'excerpt' => '摘要', 16 | 'first_name' => '名', 17 | 'gender' => '性别', 18 | 'hour' => '时', 19 | 'last_name' => '姓', 20 | 'minute' => '分', 21 | 'mobile' => '手机', 22 | 'month' => '月', 23 | 'name' => '名称', 24 | 'password' => '密码', 25 | 'password_confirmation' => '确认密码', 26 | 'phone' => '电话', 27 | 'second' => '秒', 28 | 'sex' => '性别', 29 | 'size' => '大小', 30 | 'time' => '时间', 31 | 'title' => '标题', 32 | 'username' => '用户名', 33 | 'year' => '年', 34 | ], 35 | ]; 36 | -------------------------------------------------------------------------------- /resources/views/auth/forgot-password.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | {{ __('Forgot your password? No problem. Just let us know your email address and we will email you a password reset link that will allow you to choose a new one.') }} 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | @csrf 21 | 22 | 23 |
24 | 25 | 26 | 27 |
28 | 29 |
30 | 31 | {{ __('Email Password Reset Link') }} 32 | 33 |
34 |
35 |
36 |
37 | -------------------------------------------------------------------------------- /app/Http/Controllers/Api/V1/TokenController.php: -------------------------------------------------------------------------------- 1 | validate([ 19 | 'email' => 'required|email', 20 | 'password' => 'required', 21 | ]); 22 | } catch (ValidationException $e) { 23 | return $this->fail($e->validator->errors()->first()); 24 | } 25 | 26 | /** @var User|null $user */ 27 | $user = User::query()->where('email', $request->email)->first(); 28 | 29 | if (! $user || ! Hash::check($request->password, $user->password)) { 30 | return $this->fail('The email address or password is incorrect.'); 31 | } 32 | 33 | $token = $user->createToken($user->email)->plainTextToken; 34 | 35 | return $this->success('success', compact('token')); 36 | } 37 | 38 | public function clear(): Response 39 | { 40 | /** @var User $user */ 41 | $user = Auth::user(); 42 | $user->tokens()->delete(); 43 | return $this->success(); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /app/Http/Controllers/Auth/AuthenticatedSessionController.php: -------------------------------------------------------------------------------- 1 | authenticate(); 32 | 33 | $request->session()->regenerate(); 34 | 35 | return redirect()->intended(RouteServiceProvider::HOME); 36 | } 37 | 38 | /** 39 | * Destroy an authenticated session. 40 | * 41 | * @param \Illuminate\Http\Request $request 42 | * @return \Illuminate\Http\RedirectResponse 43 | */ 44 | public function destroy(Request $request) 45 | { 46 | Auth::guard('web')->logout(); 47 | 48 | $request->session()->invalidate(); 49 | 50 | $request->session()->regenerateToken(); 51 | 52 | return redirect('/'); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /app/Services/UserService.php: -------------------------------------------------------------------------------- 1 | when(! is_null($user), function (Builder $builder) use ($user) { 26 | $builder->where('user_id', $user->id); 27 | })->whereIn($field, $keys); 28 | 29 | DB::transaction(function () use ($model, $keys, &$count) { 30 | /** @var Image $image */ 31 | foreach ($model->cursor() as $image) { 32 | // 相册图片数量更新 33 | $image->album?->decrement('image_num'); 34 | // 更新相册图片数量 35 | $image->delete(); 36 | // 更新数量 37 | if ($image->user) { 38 | $image->user->image_num = $image->user->images()->count(); 39 | $image->user->save(); 40 | } 41 | 42 | $count++; 43 | } 44 | }); 45 | 46 | return $count; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /resources/views/layouts/guest.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | {{ \App\Utils::config(\App\Enums\ConfigKey::AppName) }} 11 | 12 | 13 | 14 | 15 | @stack('styles') 16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | {{ $slot }} 24 |
25 | 26 | 27 | 28 | @if(file_exists(public_path('js/custom.js'))) 29 | 30 | @endif 31 | @stack('scripts') 32 | 33 | -------------------------------------------------------------------------------- /app/Http/Controllers/Auth/PasswordResetLinkController.php: -------------------------------------------------------------------------------- 1 | validate([ 32 | 'email' => ['required', 'email'], 33 | ]); 34 | 35 | // We will send the password reset link to this user. Once we have attempted 36 | // to send the link, we will examine the response then see the message we 37 | // need to show to the user. Finally, we'll send out a proper response. 38 | $status = Password::sendResetLink( 39 | $request->only('email') 40 | ); 41 | 42 | return $status == Password::RESET_LINK_SENT 43 | ? back()->with('status', __($status)) 44 | : back()->withInput($request->only('email')) 45 | ->withErrors(['email' => __($status)]); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /resources/views/auth/verify-email.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | {{ __('Thanks for signing up! Before getting started, could you verify your email address by clicking on the link we just emailed to you? If you didn\'t receive the email, we will gladly send you another.') }} 11 |
12 | 13 | @if (session('status') == 'verification-link-sent') 14 |
15 | {{ __('A new verification link has been sent to the email address you provided during registration.') }} 16 |
17 | @endif 18 | 19 |
20 |
21 | @csrf 22 | 23 |
24 | 25 | {{ __('Resend Verification Email') }} 26 | 27 |
28 |
29 | 30 |
31 | @csrf 32 | 33 | 36 |
37 |
38 |
39 |
40 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "dev": "npm run development", 5 | "development": "mix", 6 | "watch": "mix watch", 7 | "watch-poll": "mix watch -- --watch-options-poll=1000", 8 | "hot": "mix watch --hot", 9 | "prod": "npm run production", 10 | "production": "mix --production" 11 | }, 12 | "devDependencies": { 13 | "@fortawesome/fontawesome-free": "^5.15.4", 14 | "@tailwindcss/forms": "^0.4.0", 15 | "alpinejs": "^3.4.2", 16 | "autoprefixer": "^10.1.0", 17 | "axios": "^1.8", 18 | "blueimp-canvas-to-blob": "^3.29.0", 19 | "blueimp-file-upload": "^10.32.0", 20 | "blueimp-load-image": "^5.16.0", 21 | "clipboard": "^2.0.8", 22 | "copy-image-clipboard": "^2.0.1", 23 | "deepmerge": "^4.2.2", 24 | "dragselect": "^2.3.0", 25 | "echarts": "^5.2.2", 26 | "github-markdown-css": "^5.1.0", 27 | "imagesloaded": "^4.1.4", 28 | "jquery": "^3.6.0", 29 | "jquery.photoswipe": "^1.1.1", 30 | "justifiedGallery": "^3.8.1", 31 | "laravel-mix": "^6.0.6", 32 | "less": "^4.1.2", 33 | "less-loader": "^10.2.0", 34 | "lodash": "^4.17.19", 35 | "masonry-layout": "^4.2.2", 36 | "postcss": "^8.4.31", 37 | "postcss-import": "^14.0.1", 38 | "resolve-url-loader": "^4.0.0", 39 | "sweetalert2": "^11.3.3", 40 | "tailwindcss": "^3.0.0", 41 | "toastr": "^2.1.4", 42 | "viewerjs": "^1.10.2" 43 | }, 44 | "dependencies": { 45 | "update": "^0.7.4" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /database/seeders/InstallSeeder.php: -------------------------------------------------------------------------------- 1 | format('Y-m-d H:i:s'); 21 | $array = collect(config('convention.app'))->transform(function ($value, $key) use ($date) { 22 | return [ 23 | 'name' => $key, 24 | 'value' => is_array($value) ? json_encode($value, JSON_UNESCAPED_UNICODE) : $value, 25 | 'updated_at' => $date, 26 | 'created_at' => $date, 27 | ]; 28 | })->values()->toArray(); 29 | DB::transaction(function () use ($array) { 30 | DB::table('configs')->insert($array); 31 | // 创建默认组和默认策略 32 | /** @var Group $group */ 33 | $group = Group::query()->create([ 34 | 'name' => '系统默认组&游客组', 35 | 'is_default' => true, 36 | 'is_guest' => true, 37 | 'configs' => config('convention.group'), 38 | ]); 39 | // 创建默认策略 40 | $group->strategies()->create([ 41 | 'key' => StrategyKey::Local, 42 | 'name' => '默认本地策略', 43 | 'intro' => '系统默认的本地策略', 44 | 'configs' => config('filesystems.disks.uploads'), 45 | ]); 46 | }); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /app/Console/Commands/MakeThumbnails.php: -------------------------------------------------------------------------------- 1 | output, Image::query()->count()); 35 | $progress->setMessage('获取图片处理中...'); 36 | $progress->start(); 37 | 38 | $service = new ImageService(); 39 | 40 | /** @var Image $image */ 41 | foreach (Image::query()->whereNotNull('strategy_id')->cursor() as $image) { 42 | try { 43 | $service->makeThumbnail( 44 | image: $image, 45 | data: $image->filesystem()->read($image->pathname), 46 | force: true, 47 | ); 48 | $progress->advance(); 49 | } catch (\Throwable $e) { 50 | $this->error("缩略图生成失败, {$e->getMessage()}"); 51 | } 52 | } 53 | 54 | $progress->finish(); 55 | 56 | return 0; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /app/Exceptions/Handler.php: -------------------------------------------------------------------------------- 1 | > 19 | */ 20 | protected $dontReport = [ 21 | // 22 | ]; 23 | 24 | /** 25 | * A list of the inputs that are never flashed for validation exceptions. 26 | * 27 | * @var array 28 | */ 29 | protected $dontFlash = [ 30 | 'current_password', 31 | 'password', 32 | 'password_confirmation', 33 | ]; 34 | 35 | /** 36 | * Register the exception handling callbacks for the application. 37 | * 38 | * @return void 39 | */ 40 | public function register() 41 | { 42 | $this->reportable(function (Throwable $e) { 43 | // 44 | }); 45 | 46 | $this->renderable(function (ThrottleRequestsException $e) { 47 | return $this->fail($e->getMessage())->setStatusCode(429); 48 | }); 49 | } 50 | 51 | protected function unauthenticated($request, AuthenticationException $exception) 52 | { 53 | return $this->shouldReturnJson($request, $exception) 54 | ? $this->fail($exception->getMessage())->setStatusCode(401) 55 | : redirect()->guest($exception->redirectTo() ?? route('login')); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /routes/api.php: -------------------------------------------------------------------------------- 1 | 'v1', 24 | 'middleware' => CheckIsEnableApi::class, 25 | ], function () { 26 | Route::get('strategies', [StrategyController::class, 'index']); 27 | Route::post('upload', [ImageController::class, 'upload']); 28 | Route::post('tokens', [TokenController::class, 'store'])->middleware('throttle:3,1'); 29 | 30 | Route::group([ 31 | 'middleware' => 'auth:sanctum', 32 | ], function () { 33 | Route::get('images', [ImageController::class, 'images']); 34 | Route::delete('images/{key}', [ImageController::class, 'destroy']); 35 | Route::get('albums', [AlbumController::class, 'index']); 36 | Route::delete('albums/{id}', [AlbumController::class, 'destroy']); 37 | Route::delete('tokens', [TokenController::class, 'clear']); 38 | Route::get('profile', [UserController::class, 'index']); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /app/Http/Requests/UserSettingRequest.php: -------------------------------------------------------------------------------- 1 | 'required|between:2,20', 16 | 'url' => 'nullable|url', 17 | 'password' => 'nullable|between:6,32', 18 | 'configs' => 'array', 19 | 'configs.default_album' => 'required|numeric', 20 | 'configs.default_strategy' => 'required|numeric', 21 | 'configs.default_permission' => 'required|in:1,0', 22 | 'configs.pasted_action' => 'required|in:1,2', 23 | 'configs.is_auto_clear_preview' => 'nullable|boolean' 24 | ]; 25 | } 26 | 27 | public function messages() 28 | { 29 | return [ 30 | 'name.required' => '昵称不能为空', 31 | 'name.between' => '昵称必须在 2-20 个字符之间', 32 | 'url.url' => '个人主页地址格式不正确', 33 | 'password.between' => '密码必须在 6-32 个字符之间', 34 | 'configs.array' => '配置值不正确', 35 | 'configs.default_album.required' => '默认相册选择错误', 36 | 'configs.default_album.numeric' => '默认相册选择错误', 37 | 'configs.default_strategy.required' => '默认策略选择错误', 38 | 'configs.default_strategy.numeric' => '默认策略选择错误', 39 | 'configs.default_permission.required' => '权限值选择错误', 40 | 'configs.default_permission.in' => '权限值不正确', 41 | 'configs.pasted_action.required' => '粘贴动作值选择错误', 42 | 'configs.pasted_action.in' => '粘贴动作值不正确', 43 | 'configs.is_auto_clear_preview.boolean' => '是否自动清除预览选择错误' 44 | ]; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /config/hashing.php: -------------------------------------------------------------------------------- 1 | 'bcrypt', 19 | 20 | /* 21 | |-------------------------------------------------------------------------- 22 | | Bcrypt Options 23 | |-------------------------------------------------------------------------- 24 | | 25 | | Here you may specify the configuration options that should be used when 26 | | passwords are hashed using the Bcrypt algorithm. This will allow you 27 | | to control the amount of time it takes to hash the given password. 28 | | 29 | */ 30 | 31 | 'bcrypt' => [ 32 | 'rounds' => env('BCRYPT_ROUNDS', 10), 33 | ], 34 | 35 | /* 36 | |-------------------------------------------------------------------------- 37 | | Argon Options 38 | |-------------------------------------------------------------------------- 39 | | 40 | | Here you may specify the configuration options that should be used when 41 | | passwords are hashed using the Argon algorithm. These will allow you 42 | | to control the amount of time it takes to hash the given password. 43 | | 44 | */ 45 | 46 | 'argon' => [ 47 | 'memory' => 65536, 48 | 'threads' => 1, 49 | 'time' => 4, 50 | ], 51 | 52 | ]; 53 | -------------------------------------------------------------------------------- /tests/Feature/UtilTest.php: -------------------------------------------------------------------------------- 1 | assertTrue(true); 22 | } 23 | 24 | if (is_string(Utils::config(ConfigKey::AppName))) { 25 | $this->assertTrue(true); 26 | } 27 | 28 | if (Utils::config(ConfigKey::Mail) instanceof Collection) { 29 | $this->assertTrue(true); 30 | } 31 | 32 | if (is_array(Utils::config(ConfigKey::Mail.'.mailers'))) { 33 | $this->assertTrue(true); 34 | } 35 | 36 | if (is_bool(Utils::config(ConfigKey::IsAllowGuestUpload))) { 37 | $this->assertTrue(true); 38 | } 39 | } 40 | 41 | public function test_array_filter_recursive() 42 | { 43 | 44 | $array = Utils::filter([ 45 | 'name' => 'Lsky', 46 | 'age' => null, 47 | 'configs' => [ 48 | 'one' => null, 49 | 'two' => 1 50 | ] 51 | ]); 52 | 53 | if (! array_key_exists('age', $array)) { 54 | $this->assertTrue(true); 55 | } 56 | 57 | if (! array_key_exists('one', $array['configs'])) { 58 | $this->assertTrue(true); 59 | } 60 | 61 | if (! array_key_exists('two', $array['configs'])) { 62 | $this->assertTrue(true); 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /app/Providers/AppServiceProvider.php: -------------------------------------------------------------------------------- 1 | toArray())); 45 | 46 | View::composer('*', function (\Illuminate\View\View $view) { 47 | /** @var Group $group */ 48 | $group = Auth::check() ? Auth::user()->group : Group::query()->where('is_guest', true)->first(); 49 | $view->with([ 50 | '_group' => $group, 51 | '_is_notice' => strip_tags(Utils::config(ConfigKey::SiteNotice)), 52 | ]); 53 | }); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /public/mix-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "/js/app.js": "/js/app.js", 3 | "/css/app.css": "/css/app.css", 4 | "/css/context-js/context-js.css": "/css/context-js/context-js.css", 5 | "/css/gallery.css": "/css/gallery.css", 6 | "/css/common.css": "/css/common.css", 7 | "/css/fontawesome.css": "/css/fontawesome.css", 8 | "/js/blueimp-file-upload/jquery.ui.widget.js": "/js/blueimp-file-upload/jquery.ui.widget.js", 9 | "/js/blueimp-file-upload/jquery.iframe-transport.js": "/js/blueimp-file-upload/jquery.iframe-transport.js", 10 | "/js/blueimp-file-upload/jquery.fileupload.js": "/js/blueimp-file-upload/jquery.fileupload.js", 11 | "/js/blueimp-load-image/load-image.all.min.js": "/js/blueimp-load-image/load-image.all.min.js", 12 | "/css/justified-gallery/justifiedGallery.min.css": "/css/justified-gallery/justifiedGallery.min.css", 13 | "/js/justified-gallery/jquery.justifiedGallery.min.js": "/js/justified-gallery/jquery.justifiedGallery.min.js", 14 | "/css/viewer-js/viewer.min.css": "/css/viewer-js/viewer.min.css", 15 | "/js/viewer-js/viewer.min.js": "/js/viewer-js/viewer.min.js", 16 | "/js/clipboard/clipboard.min.js": "/js/clipboard/clipboard.min.js", 17 | "/js/clipboard/index.browser.js": "/js/clipboard/index.browser.js", 18 | "/js/dragselect/ds.min.js": "/js/dragselect/ds.min.js", 19 | "/js/context-js/context-js.js": "/js/context-js/context-js.js", 20 | "/js/echarts/echarts.min.js": "/js/echarts/echarts.min.js", 21 | "/js/masonry/masonry.pkgd.min.js": "/js/masonry/masonry.pkgd.min.js", 22 | "/js/imagesloaded/imagesloaded.pkgd.min.js": "/js/imagesloaded/imagesloaded.pkgd.min.js", 23 | "/css/markdown-css/github-markdown.css": "/css/markdown-css/github-markdown.css", 24 | "/css/markdown-css/github-markdown-light.css": "/css/markdown-css/github-markdown-light.css" 25 | } 26 | -------------------------------------------------------------------------------- /bootstrap/app.php: -------------------------------------------------------------------------------- 1 | singleton( 30 | Illuminate\Contracts\Http\Kernel::class, 31 | App\Http\Kernel::class 32 | ); 33 | 34 | $app->singleton( 35 | Illuminate\Contracts\Console\Kernel::class, 36 | App\Console\Kernel::class 37 | ); 38 | 39 | $app->singleton( 40 | Illuminate\Contracts\Debug\ExceptionHandler::class, 41 | App\Exceptions\Handler::class 42 | ); 43 | 44 | /* 45 | |-------------------------------------------------------------------------- 46 | | Return The Application 47 | |-------------------------------------------------------------------------- 48 | | 49 | | This script returns the application instance. The instance is given to 50 | | the calling script so we can separate the building of the instances 51 | | from the actual running of the application and sending responses. 52 | | 53 | */ 54 | 55 | return $app; 56 | -------------------------------------------------------------------------------- /app/Http/Controllers/Auth/RegisteredUserController.php: -------------------------------------------------------------------------------- 1 | validate([ 39 | 'name' => ['required', 'string', 'max:255'], 40 | 'email' => ['required', 'string', 'email', 'max:255', 'unique:users'], 41 | 'password' => ['required', 'confirmed', Rules\Password::defaults()], 42 | ]); 43 | 44 | $user = User::create([ 45 | 'name' => $request->name, 46 | 'email' => $request->email, 47 | 'password' => Hash::make($request->password), 48 | 'registered_ip' => $request->ip(), 49 | ]); 50 | 51 | if (Utils::config(ConfigKey::IsUserNeedVerify)) { 52 | event(new Registered($user)); 53 | } 54 | 55 | Auth::login($user); 56 | 57 | return redirect(RouteServiceProvider::HOME); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /app/Enums/GroupConfigKey.php: -------------------------------------------------------------------------------- 1 | make(Illuminate\Contracts\Console\Kernel::class); 34 | 35 | $status = $kernel->handle( 36 | $input = new Symfony\Component\Console\Input\ArgvInput, 37 | new Symfony\Component\Console\Output\ConsoleOutput 38 | ); 39 | 40 | /* 41 | |-------------------------------------------------------------------------- 42 | | Shutdown The Application 43 | |-------------------------------------------------------------------------- 44 | | 45 | | Once Artisan has finished running, we will fire off the shutdown events 46 | | so that any final work may be done by the application before we shut 47 | | down the process. This is the last thing to happen to the request. 48 | | 49 | */ 50 | 51 | $kernel->terminate($input, $status); 52 | 53 | exit($status); 54 | -------------------------------------------------------------------------------- /database/migrations/2014_10_12_000000_create_users_table.php: -------------------------------------------------------------------------------- 1 | engine = 'InnoDB'; 18 | $table->charset = 'utf8mb4'; 19 | $table->collation = 'utf8mb4_unicode_ci'; 20 | 21 | $table->id(); 22 | $table->foreignId('group_id')->nullable()->comment('角色组')->constrained('groups')->onDelete('set null'); 23 | $table->string('name')->comment('姓名'); 24 | $table->string('email')->unique()->comment('邮箱'); 25 | $table->string('password')->comment('密码'); 26 | $table->rememberToken(); 27 | $table->boolean('is_adminer')->default(false)->comment('是否为管理员'); 28 | $table->decimal('capacity', 20)->default(0)->comment('总容量(kb)'); 29 | $table->string('url')->default('')->comment('个人主页'); 30 | $table->json('configs')->comment('配置'); 31 | $table->unsignedBigInteger('image_num')->default(0)->comment('图片数量'); 32 | $table->unsignedBigInteger('album_num')->default(0)->comment('相册数量'); 33 | $table->string('registered_ip')->default('')->comment('注册IP'); 34 | $table->unsignedTinyInteger('status')->default(1)->comment('状态'); 35 | $table->timestamp('email_verified_at')->nullable(); 36 | $table->timestamps(); 37 | }); 38 | } 39 | 40 | /** 41 | * Reverse the migrations. 42 | * 43 | * @return void 44 | */ 45 | public function down() 46 | { 47 | Schema::dropIfExists('users'); 48 | } 49 | }; 50 | -------------------------------------------------------------------------------- /resources/views/auth/reset-password.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | @csrf 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 |
24 | 25 | 26 |
27 | 28 | 29 | 30 |
31 | 32 | 33 |
34 | 35 | 36 | 39 |
40 | 41 |
42 | 43 | {{ __('Reset Password') }} 44 | 45 |
46 |
47 |
48 |
49 | -------------------------------------------------------------------------------- /public/index.php: -------------------------------------------------------------------------------- 1 | make(Kernel::class); 50 | 51 | $response = $kernel->handle( 52 | $request = Request::capture() 53 | )->send(); 54 | 55 | $kernel->terminate($request, $response); 56 | -------------------------------------------------------------------------------- /app/Providers/RouteServiceProvider.php: -------------------------------------------------------------------------------- 1 | configureRateLimiting(); 39 | 40 | $this->routes(function () { 41 | Route::prefix('api') 42 | ->middleware('api') 43 | ->namespace($this->namespace) 44 | ->group(base_path('routes/api.php')); 45 | 46 | Route::middleware('web') 47 | ->namespace($this->namespace) 48 | ->group(base_path('routes/web.php')); 49 | }); 50 | } 51 | 52 | /** 53 | * Configure the rate limiters for the application. 54 | * 55 | * @return void 56 | */ 57 | protected function configureRateLimiting() 58 | { 59 | RateLimiter::for('api', function (Request $request) { 60 | return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip()); 61 | }); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /resources/js/bootstrap.js: -------------------------------------------------------------------------------- 1 | window._ = require('lodash'); 2 | window.$ = window.jQuery = require('jquery'); 3 | window.toastr = require('toastr'); 4 | window.Swal = require('sweetalert2') 5 | window.Swal = window.Swal.mixin({ 6 | showCancelButton: true, 7 | confirmButtonText: '确认', 8 | cancelButtonText: '取消', 9 | }) 10 | 11 | toastr.options = { 12 | "closeButton": true, 13 | "debug": false, 14 | "newestOnTop": true, 15 | "progressBar": true, 16 | "positionClass": "toast-bottom-right", 17 | "preventDuplicates": false, 18 | } 19 | 20 | /** 21 | * We'll load the axios HTTP library which allows us to easily issue requests 22 | * to our Laravel back-end. This library automatically handles sending the 23 | * CSRF token as a header based on the value of the "XSRF" token cookie. 24 | */ 25 | 26 | window.axios = require('axios'); 27 | 28 | window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'; 29 | 30 | axios.interceptors.request.use(function (config) { 31 | return config; 32 | }, function (error) { 33 | return Promise.reject(error); 34 | }); 35 | 36 | axios.interceptors.response.use(function (response) { 37 | return response; 38 | }, function (error) { 39 | if (401 === error.response.status) { 40 | toastr.warning('状态失效,请先登录账号'); 41 | } 42 | if (500 === error.response.status) { 43 | toastr.warning('服务出现异常,请稍后再试'); 44 | } 45 | return Promise.reject(error); 46 | }); 47 | 48 | /** 49 | * Echo exposes an expressive API for subscribing to channels and listening 50 | * for events that are broadcast by Laravel. Echo and event broadcasting 51 | * allows your team to easily build robust real-time web applications. 52 | */ 53 | 54 | // import Echo from 'laravel-echo'; 55 | 56 | // window.Pusher = require('pusher-js'); 57 | 58 | // window.Echo = new Echo({ 59 | // broadcaster: 'pusher', 60 | // key: process.env.MIX_PUSHER_APP_KEY, 61 | // cluster: process.env.MIX_PUSHER_APP_CLUSTER, 62 | // forceTLS: true 63 | // }); 64 | -------------------------------------------------------------------------------- /tests/Feature/Auth/EmailVerificationTest.php: -------------------------------------------------------------------------------- 1 | create([ 20 | 'email_verified_at' => null, 21 | ]); 22 | 23 | $response = $this->actingAs($user)->get('/verify-email'); 24 | 25 | $response->assertStatus(200); 26 | } 27 | 28 | public function test_email_can_be_verified() 29 | { 30 | $user = User::factory()->create([ 31 | 'email_verified_at' => null, 32 | ]); 33 | 34 | Event::fake(); 35 | 36 | $verificationUrl = URL::temporarySignedRoute( 37 | 'verification.verify', 38 | now()->addMinutes(60), 39 | ['id' => $user->id, 'hash' => sha1($user->email)] 40 | ); 41 | 42 | $response = $this->actingAs($user)->get($verificationUrl); 43 | 44 | Event::assertDispatched(Verified::class); 45 | $this->assertTrue($user->fresh()->hasVerifiedEmail()); 46 | $response->assertRedirect(RouteServiceProvider::HOME.'?verified=1'); 47 | } 48 | 49 | public function test_email_is_not_verified_with_invalid_hash() 50 | { 51 | $user = User::factory()->create([ 52 | 'email_verified_at' => null, 53 | ]); 54 | 55 | $verificationUrl = URL::temporarySignedRoute( 56 | 'verification.verify', 57 | now()->addMinutes(60), 58 | ['id' => $user->id, 'hash' => sha1('wrong-email')] 59 | ); 60 | 61 | $this->actingAs($user)->get($verificationUrl); 62 | 63 | $this->assertFalse($user->fresh()->hasVerifiedEmail()); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /config/filesystems.php: -------------------------------------------------------------------------------- 1 | env('FILESYSTEM_DISK', 'local'), 17 | 18 | /* 19 | |-------------------------------------------------------------------------- 20 | | Filesystem Disks 21 | |-------------------------------------------------------------------------- 22 | | 23 | | Here you may configure as many filesystem "disks" as you wish, and you 24 | | may even configure multiple disks of the same driver. Defaults have 25 | | been setup for each driver as an example of the required options. 26 | | 27 | | Supported Drivers: "local", "ftp", "sftp", "s3" 28 | | 29 | */ 30 | 31 | 'disks' => [ 32 | 33 | 'local' => [ 34 | 'driver' => 'local', 35 | 'root' => storage_path('app'), 36 | ], 37 | 38 | 'uploads' => [ 39 | 'driver' => 'local', 40 | 'root' => storage_path('app/uploads'), 41 | 'url' => env('APP_URL').'/i', 42 | 'visibility' => 'public', 43 | ], 44 | ], 45 | 46 | /* 47 | |-------------------------------------------------------------------------- 48 | | Symbolic Links 49 | |-------------------------------------------------------------------------- 50 | | 51 | | Here you may configure the symbolic links that will be created when the 52 | | `storage:link` Artisan command is executed. The array keys should be 53 | | the locations of the links and the values should be their targets. 54 | | 55 | */ 56 | 57 | 'links' => [ 58 | public_path('i') => storage_path('app/uploads'), 59 | ], 60 | ]; 61 | -------------------------------------------------------------------------------- /config/broadcasting.php: -------------------------------------------------------------------------------- 1 | env('BROADCAST_DRIVER', 'null'), 19 | 20 | /* 21 | |-------------------------------------------------------------------------- 22 | | Broadcast Connections 23 | |-------------------------------------------------------------------------- 24 | | 25 | | Here you may define all of the broadcast connections that will be used 26 | | to broadcast events to other systems or over websockets. Samples of 27 | | each available type of connection are provided inside this array. 28 | | 29 | */ 30 | 31 | 'connections' => [ 32 | 33 | 'pusher' => [ 34 | 'driver' => 'pusher', 35 | 'key' => env('PUSHER_APP_KEY'), 36 | 'secret' => env('PUSHER_APP_SECRET'), 37 | 'app_id' => env('PUSHER_APP_ID'), 38 | 'options' => [ 39 | 'cluster' => env('PUSHER_APP_CLUSTER'), 40 | 'useTLS' => true, 41 | ], 42 | 'client_options' => [ 43 | // Guzzle client options: https://docs.guzzlephp.org/en/stable/request-options.html 44 | ], 45 | ], 46 | 47 | 'ably' => [ 48 | 'driver' => 'ably', 49 | 'key' => env('ABLY_KEY'), 50 | ], 51 | 52 | 'redis' => [ 53 | 'driver' => 'redis', 54 | 'connection' => 'default', 55 | ], 56 | 57 | 'log' => [ 58 | 'driver' => 'log', 59 | ], 60 | 61 | 'null' => [ 62 | 'driver' => 'null', 63 | ], 64 | 65 | ], 66 | 67 | ]; 68 | -------------------------------------------------------------------------------- /tests/Feature/Auth/PasswordResetTest.php: -------------------------------------------------------------------------------- 1 | get('/forgot-password'); 18 | 19 | $response->assertStatus(200); 20 | } 21 | 22 | public function test_reset_password_link_can_be_requested() 23 | { 24 | Notification::fake(); 25 | 26 | $user = User::factory()->create(); 27 | 28 | $this->post('/forgot-password', ['email' => $user->email]); 29 | 30 | Notification::assertSentTo($user, ResetPassword::class); 31 | } 32 | 33 | public function test_reset_password_screen_can_be_rendered() 34 | { 35 | Notification::fake(); 36 | 37 | $user = User::factory()->create(); 38 | 39 | $this->post('/forgot-password', ['email' => $user->email]); 40 | 41 | Notification::assertSentTo($user, ResetPassword::class, function ($notification) { 42 | $response = $this->get('/reset-password/'.$notification->token); 43 | 44 | $response->assertStatus(200); 45 | 46 | return true; 47 | }); 48 | } 49 | 50 | public function test_password_can_be_reset_with_valid_token() 51 | { 52 | Notification::fake(); 53 | 54 | $user = User::factory()->create(); 55 | 56 | $this->post('/forgot-password', ['email' => $user->email]); 57 | 58 | Notification::assertSentTo($user, ResetPassword::class, function ($notification) use ($user) { 59 | $response = $this->post('/reset-password', [ 60 | 'token' => $notification->token, 61 | 'email' => $user->email, 62 | 'password' => 'password', 63 | 'password_confirmation' => 'password', 64 | ]); 65 | 66 | $response->assertSessionHasNoErrors(); 67 | 68 | return true; 69 | }); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /app/Http/Controllers/User/UserController.php: -------------------------------------------------------------------------------- 1 | group->configs; 24 | $strategies = $user->group->strategies()->get(); 25 | return view('user.dashboard', compact('strategies', 'configs', 'user')); 26 | } 27 | 28 | public function settings(): View 29 | { 30 | return view('user.settings'); 31 | } 32 | 33 | public function update(UserSettingRequest $request): Response 34 | { 35 | /** @var User $user */ 36 | $user = Auth::user(); 37 | $user->name = $request->validated('name'); 38 | $user->url = $request->validated('url') ?: ''; 39 | $user->configs = $user->configs->merge(collect($request->validated('configs'))->transform(function ($value) { 40 | return (int)$value; 41 | })); 42 | if ($password = $request->validated('password')) { 43 | $user->forceFill([ 44 | 'password' => Hash::make($password), 45 | 'remember_token' => Str::random(60), 46 | ]); 47 | 48 | event(new PasswordReset($user)); 49 | } 50 | $user->save(); 51 | return $this->success('保存成功'); 52 | } 53 | 54 | public function setStrategy(Request $request): Response 55 | { 56 | /** @var User $user */ 57 | $user = Auth::user(); 58 | if (! $strategy = $user->group->strategies()->find($request->id)) { 59 | return $this->fail('没有找到该策略'); 60 | } 61 | $user->update(['configs->'.UserConfigKey::DefaultStrategy => $strategy->id]); 62 | return $this->success('设置成功'); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /app/Models/Album.php: -------------------------------------------------------------------------------- 1 | '', 37 | ]; 38 | 39 | protected $casts = [ 40 | 'id' => 'integer', 41 | 'user_id' => 'integer', 42 | 'image_num' => 'integer', 43 | ]; 44 | 45 | public function scopeFilter(Builder $builder, Request $request) 46 | { 47 | return $builder->when($request->query('order') ?: 'newest', function (Builder $builder, $order) { 48 | switch ($order) { 49 | case 'earliest': 50 | $builder->orderBy('created_at'); 51 | break; 52 | case 'most': 53 | $builder->orderByDesc('image_num'); 54 | break; 55 | case 'least': 56 | $builder->orderBy('image_num'); 57 | break; 58 | default: 59 | $builder->latest(); 60 | } 61 | })->when($request->query('keyword'), function (Builder $builder, $keyword) { 62 | $builder->where('name', 'like', "%{$keyword}%")->orWhere('intro', 'like', "%{$keyword}%"); 63 | }); 64 | } 65 | 66 | public function user(): BelongsTo 67 | { 68 | return $this->belongsTo(User::class, 'user_id', 'id'); 69 | } 70 | 71 | public function images(): HasMany 72 | { 73 | return $this->hasMany(Image::class, 'album_id', 'id'); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /public/css/justified-gallery/justifiedGallery.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * justifiedGallery - v3.8.1 3 | * http://miromannino.github.io/Justified-Gallery/ 4 | * Copyright (c) 2020 Miro Mannino 5 | * Licensed under the MIT license. 6 | */.justified-gallery{width:100%;position:relative;overflow:hidden}.justified-gallery>a,.justified-gallery>div,.justified-gallery>figure{position:absolute;display:inline-block;overflow:hidden;filter:"alpha(opacity=10)";opacity:.1;margin:0;padding:0}.justified-gallery>a>a>img,.justified-gallery>a>a>svg,.justified-gallery>a>img,.justified-gallery>a>svg,.justified-gallery>div>a>img,.justified-gallery>div>a>svg,.justified-gallery>div>img,.justified-gallery>div>svg,.justified-gallery>figure>a>img,.justified-gallery>figure>a>svg,.justified-gallery>figure>img,.justified-gallery>figure>svg{position:absolute;top:50%;left:50%;margin:0;padding:0;border:none;filter:"alpha(opacity=0)";opacity:0}.justified-gallery>a>.jg-caption,.justified-gallery>div>.jg-caption,.justified-gallery>figure>.jg-caption{display:none;position:absolute;bottom:0;padding:5px;background-color:#000;left:0;right:0;margin:0;color:#fff;font-size:12px;font-weight:300;font-family:sans-serif}.justified-gallery>a>.jg-caption.jg-caption-visible,.justified-gallery>div>.jg-caption.jg-caption-visible,.justified-gallery>figure>.jg-caption.jg-caption-visible{display:initial;filter:"alpha(opacity=70)";opacity:.7;-webkit-transition:opacity .5s ease-in;-moz-transition:opacity .5s ease-in;-o-transition:opacity .5s ease-in;transition:opacity .5s ease-in}.justified-gallery>.jg-entry-visible{filter:"alpha(opacity=100)";opacity:1;background:0 0}.justified-gallery>.jg-entry-visible>a>img,.justified-gallery>.jg-entry-visible>a>svg,.justified-gallery>.jg-entry-visible>img,.justified-gallery>.jg-entry-visible>svg{filter:"alpha(opacity=100)";opacity:1;-webkit-transition:opacity .5s ease-in;-moz-transition:opacity .5s ease-in;-o-transition:opacity .5s ease-in;transition:opacity .5s ease-in}.justified-gallery>.jg-filtered{display:none}.justified-gallery>.jg-spinner{position:absolute;bottom:0;margin-left:-24px;padding:10px 0 10px 0;left:50%;filter:"alpha(opacity=100)";opacity:1;overflow:initial}.justified-gallery>.jg-spinner>span{display:inline-block;filter:"alpha(opacity=0)";opacity:0;width:8px;height:8px;margin:0 4px 0 4px;background-color:#000;border-radius:6px} 7 | -------------------------------------------------------------------------------- /public/js/blueimp-file-upload/jquery.iframe-transport.js: -------------------------------------------------------------------------------- 1 | !function(e){"use strict";"function"==typeof define&&define.amd?define(["jquery"],e):"object"==typeof exports?e(require("jquery")):e(window.jQuery)}((function(e){"use strict";var t=0,r=e,n="parseJSON";"JSON"in window&&"parse"in JSON&&(r=JSON,n="parse"),e.ajaxTransport("iframe",(function(r){if(r.async){var n,a,o,i=r.initialIframeSrc||"javascript:false;";return{send:function(p,f){(n=e('
')).attr("accept-charset",r.formAcceptCharset),o=/\?/.test(r.url)?"&":"?","DELETE"===r.type?(r.url=r.url+o+"_method=DELETE",r.type="POST"):"PUT"===r.type?(r.url=r.url+o+"_method=PUT",r.type="POST"):"PATCH"===r.type&&(r.url=r.url+o+"_method=PATCH",r.type="POST"),a=e('').on("load",(function(){var t,o=e.isArray(r.paramName)?r.paramName:[r.paramName];a.off("load").on("load",(function(){var t;try{if(!(t=a.contents()).length||!t[0].firstChild)throw new Error}catch(e){t=void 0}f(200,"success",{iframe:t}),e('').appendTo(n),window.setTimeout((function(){n.remove()}),0)})),n.prop("target",a.prop("name")).prop("action",r.url).prop("method",r.type),r.formData&&e.each(r.formData,(function(t,r){e('').prop("name",r.name).val(r.value).appendTo(n)})),r.fileInput&&r.fileInput.length&&"POST"===r.type&&(t=r.fileInput.clone(),r.fileInput.after((function(e){return t[e]})),r.paramName&&r.fileInput.each((function(t){e(this).prop("name",o[t]||r.paramName)})),n.append(r.fileInput).prop("enctype","multipart/form-data").prop("encoding","multipart/form-data"),r.fileInput.removeAttr("form")),window.setTimeout((function(){n.submit(),t&&t.length&&r.fileInput.each((function(r,n){var a=e(t[r]);e(n).prop("name",a.prop("name")).attr("form",a.attr("form")),a.replaceWith(n)}))}),0)})),n.append(a).appendTo(document.body)},abort:function(){a&&a.off("load").prop("src",i),n&&n.remove()}}}})),e.ajaxSetup({converters:{"iframe text":function(t){return t&&e(t[0].body).text()},"iframe json":function(t){return t&&r[n](e(t[0].body).text())},"iframe html":function(t){return t&&e(t[0].body).html()},"iframe xml":function(t){var r=t&&t[0];return r&&e.isXMLDoc(r)?r:e.parseXML(r.XMLDocument&&r.XMLDocument.xml||e(r.body).html())},"iframe script":function(t){return t&&e.globalEval(e(t[0].body).text())}}})})); 2 | -------------------------------------------------------------------------------- /public/js/clipboard/index.browser.js: -------------------------------------------------------------------------------- 1 | var CopyImageClipboard=function(n){"use strict";function t(n,t,o,e){return new(o||(o=Promise))((function(i,r){function c(n){try{a(e.next(n))}catch(n){r(n)}}function u(n){try{a(e.throw(n))}catch(n){r(n)}}function a(n){var t;n.done?i(n.value):(t=n.value,t instanceof o?t:new o((function(n){n(t)}))).then(c,u)}a((e=e.apply(n,t||[])).next())}))}function o(n){return t(this,void 0,void 0,(function*(){const t=yield fetch(`${n}`);return yield t.blob()}))}function e(n){return n.type.includes("jpeg")}function i(n){return n.type.includes("png")}function r(n){return t(this,void 0,void 0,(function*(){return new Promise((function(t,o){const e=document.createElement("img");e.crossOrigin="anonymous",e.src=n,e.onload=function(n){const o=n.target;t(o)},e.onabort=o,e.onerror=o}))}))}function c(n){return t(this,void 0,void 0,(function*(){return new Promise((function(t,o){const e=document.createElement("canvas"),i=e.getContext("2d");if(i){const{width:r,height:c}=n;e.width=r,e.height=c,i.drawImage(n,0,0,r,c),e.toBlob((function(n){n?t(n):o("Cannot get blob from image element")}),"image/png",1)}}))}))}function u(n){return t(this,void 0,void 0,(function*(){const t=URL.createObjectURL(n),o=yield r(t);return yield c(o)}))}function a(n){return t(this,void 0,void 0,(function*(){const t={[n.type]:n},o=new ClipboardItem(t);yield navigator.clipboard.write([o])}))}return n.canCopyImagesToClipboard=function(){var n;const t="undefined"!=typeof fetch,o="undefined"!=typeof ClipboardItem,e=!!(null===(n=null===navigator||void 0===navigator?void 0:navigator.clipboard)||void 0===n?void 0:n.write);return t&&o&&e},n.convertBlobToPng=u,n.copyBlobToClipboard=a,n.copyImageToClipboard=function(n){return t(this,void 0,void 0,(function*(){const t=yield o(n);if(e(t)){const n=yield u(t);return yield a(n),t}if(i(t))return yield a(t),t;throw new Error("Cannot copy this type of image to clipboard")}))},n.createImageElement=r,n.getBlobFromImageElement=c,n.getBlobFromImageSource=o,n.isJpegBlob=e,n.isPngBlob=i,n.requestClipboardWritePermission=function(){var n;return t(this,void 0,void 0,(function*(){if(!(null===(n=null===navigator||void 0===navigator?void 0:navigator.permissions)||void 0===n?void 0:n.query))return!1;const{state:t}=yield navigator.permissions.query({name:"clipboard-write"});return"granted"===t}))},Object.defineProperty(n,"__esModule",{value:!0}),n}({}); 2 | -------------------------------------------------------------------------------- /resources/views/welcome.blade.php: -------------------------------------------------------------------------------- 1 | @push('styles') 2 | 3 | @endpush 4 | 5 | 6 |
7 |
8 |
9 | 12 |
13 | @includeWhen($_is_notice, 'layouts.notice') 14 | @includeWhen($_group->strategies->isNotEmpty(), 'layouts.strategies') 15 | 16 | @if(Auth::check()) 17 | @include('layouts.user-nav') 18 | @else 19 | 登录 20 | @if(\App\Utils::config(\App\Enums\ConfigKey::IsEnableRegistration)) 21 | 注册 22 | @endif 23 | @endif 24 |
25 |
26 |
27 |
28 | 29 |
30 | 35 |
36 | 37 | @include('common.notice') 38 | 39 |
40 | -------------------------------------------------------------------------------- /resources/views/auth/register.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | @csrf 14 | 15 | 16 |
17 | 18 | 19 | 20 |
21 | 22 | 23 |
24 | 25 | 26 | 27 |
28 | 29 | 30 |
31 | 32 | 33 | 37 |
38 | 39 | 40 |
41 | 42 | 43 | 46 |
47 | 48 |
49 | 50 | {{ __('Already registered?') }} 51 | 52 | 53 | 54 | {{ __('Register') }} 55 | 56 |
57 |
58 |
59 |
60 | -------------------------------------------------------------------------------- /app/Http/Controllers/User/AlbumController.php: -------------------------------------------------------------------------------- 1 | albums()->latest()->paginate(40); 21 | $albums->getCollection()->each(function (Album $album) { 22 | $album->setVisible(['id', 'name', 'intro', 'image_num']); 23 | }); 24 | return $this->success('success', compact('albums')); 25 | } 26 | 27 | public function create(AlbumRequest $request): Response 28 | { 29 | /** @var User $user */ 30 | $user = Auth::user(); 31 | DB::transaction(function () use ($user, $request) { 32 | $user->albums()->create(array_filter($request->validated())); 33 | $user->album_num = $user->albums()->count(); 34 | $user->save(); 35 | }); 36 | 37 | return $this->success('创建成功'); 38 | } 39 | 40 | public function update(AlbumRequest $request): Response 41 | { 42 | /** @var User $user */ 43 | $user = Auth::user(); 44 | $album = $user->albums()->find($request->route('id')); 45 | if (is_null($album)) { 46 | return $this->fail('不存在的相册'); 47 | } 48 | $album->update(array_filter($request->validated())); 49 | return $this->success('修改成功'); 50 | } 51 | 52 | public function delete(Request $request): Response 53 | { 54 | /** @var User $user */ 55 | $user = Auth::user(); 56 | /** @var Album|null $album */ 57 | $album = $user->albums()->find($request->route('id')); 58 | if (is_null($album)) { 59 | return $this->fail('不存在的相册'); 60 | } 61 | DB::transaction(function () use ($user, $album) { 62 | $album->images()->update(['album_id' => null]); 63 | $album->delete(); 64 | $user->album_num = $user->albums()->count(); 65 | $user->save(); 66 | }); 67 | return $this->success('删除成功'); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /database/migrations/2021_12_11_191158_create_images_table.php: -------------------------------------------------------------------------------- 1 | engine = 'InnoDB'; 18 | $table->charset = 'utf8mb4'; 19 | $table->collation = 'utf8mb4_unicode_ci'; 20 | 21 | $table->id(); 22 | $table->foreignId('user_id')->nullable()->comment('用户')->constrained('users')->onDelete('set null'); 23 | $table->foreignId('album_id')->nullable()->comment('相册')->constrained('albums')->onDelete('set null'); 24 | $table->foreignId('group_id')->nullable()->comment('角色组')->constrained('groups')->onDelete('set null'); 25 | $table->foreignId('strategy_id')->nullable()->comment('策略')->constrained('strategies')->onDelete('set null'); 26 | $table->string('key')->unique()->comment('key'); 27 | $table->string('path')->comment('保存路径'); 28 | $table->string('name')->comment('保存名称'); 29 | $table->string('origin_name')->default('')->comment('原始名称'); 30 | $table->string('alias_name')->default('')->comment('别名'); 31 | $table->decimal('size')->default(0)->comment('图片大小(kb)'); 32 | $table->string('mimetype', 32)->comment('文件类型'); 33 | $table->string('extension', 32)->comment('文件后缀'); 34 | $table->string('md5', 32)->comment('文件MD5'); 35 | $table->string('sha1')->comment('文件SHA1'); 36 | $table->unsignedInteger('width')->default(0)->comment('宽'); 37 | $table->unsignedInteger('height')->default(0)->comment('高'); 38 | $table->tinyInteger('permission')->default(0)->comment('访问权限'); 39 | $table->boolean('is_unhealthy')->default(false)->comment('是否为不健康的'); 40 | $table->string('uploaded_ip')->default('')->comment('上传IP'); 41 | $table->timestamps(); 42 | }); 43 | } 44 | 45 | /** 46 | * Reverse the migrations. 47 | * 48 | * @return void 49 | */ 50 | public function down() 51 | { 52 | Schema::dropIfExists('images'); 53 | } 54 | }; 55 | -------------------------------------------------------------------------------- /app/Http/Controllers/Admin/SettingController.php: -------------------------------------------------------------------------------- 1 | all() as $key => $value) { 28 | Config::query()->where('name', $key)->update(['value' => $value]); 29 | } 30 | Cache::flush(); 31 | return $this->success('保存成功'); 32 | } 33 | 34 | public function mailTest(Request $request): Response 35 | { 36 | try { 37 | Mail::to($request->post('email'))->send(new Test()); 38 | } catch (\Throwable $e) { 39 | return $this->fail($e->getMessage()); 40 | } 41 | return $this->success('发送成功'); 42 | } 43 | 44 | public function checkUpdate(): Response 45 | { 46 | $version = Utils::config(ConfigKey::AppVersion); 47 | $service = new UpgradeService($version); 48 | try { 49 | $data = [ 50 | 'is_update' => $service->check(), 51 | ]; 52 | if ($data['is_update']) { 53 | $data['version'] = $service->getVersions()->first(); 54 | } 55 | } catch (\Exception $e) { 56 | return $this->fail($e->getMessage()); 57 | } 58 | 59 | return $this->success('success', $data); 60 | } 61 | 62 | public function upgrade() 63 | { 64 | ignore_user_abort(true); 65 | set_time_limit(0); 66 | 67 | $version = Utils::config(ConfigKey::AppVersion); 68 | $service = new UpgradeService($version); 69 | $this->success()->send(); 70 | $service->upgrade(); 71 | flush(); 72 | } 73 | 74 | public function upgradeProgress(): Response 75 | { 76 | return $this->success('success', Cache::get('upgrade_progress')); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /resources/views/common/gallery.blade.php: -------------------------------------------------------------------------------- 1 | @section('title', '画廊') 2 | 3 | @push('styles') 4 | 5 | @endpush 6 | 7 | 8 |
9 | @if($images->isNotEmpty()) 10 |
11 |
12 | @foreach($images as $image) 13 |
14 |
15 | @if($image->extension === 'gif') 16 | Gif 17 | @endif 18 | 19 |
20 | 21 |
22 |
23 | 24 | 25 |

{{ $image->user->name }}

26 |
27 |
28 |
29 | @endforeach 30 |
31 | {{ $images->links() }} 32 | @else 33 | 34 | @endif 35 |
36 | 37 | @push('scripts') 38 | 39 | 40 | 54 | @endpush 55 |
56 | -------------------------------------------------------------------------------- /config/sanctum.php: -------------------------------------------------------------------------------- 1 | explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf( 17 | '%s%s', 18 | 'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1', 19 | env('APP_URL') ? ','.parse_url(env('APP_URL'), PHP_URL_HOST) : '' 20 | ))), 21 | 22 | /* 23 | |-------------------------------------------------------------------------- 24 | | Sanctum Guards 25 | |-------------------------------------------------------------------------- 26 | | 27 | | This array contains the authentication guards that will be checked when 28 | | Sanctum is trying to authenticate a request. If none of these guards 29 | | are able to authenticate the request, Sanctum will use the bearer 30 | | token that's present on an incoming request for authentication. 31 | | 32 | */ 33 | 34 | 'guard' => ['web'], 35 | 36 | /* 37 | |-------------------------------------------------------------------------- 38 | | Expiration Minutes 39 | |-------------------------------------------------------------------------- 40 | | 41 | | This value controls the number of minutes until an issued token will be 42 | | considered expired. If this value is null, personal access tokens do 43 | | not expire. This won't tweak the lifetime of first-party sessions. 44 | | 45 | */ 46 | 47 | 'expiration' => null, 48 | 49 | /* 50 | |-------------------------------------------------------------------------- 51 | | Sanctum Middleware 52 | |-------------------------------------------------------------------------- 53 | | 54 | | When authenticating your first-party SPA with Sanctum you may need to 55 | | customize some of the middleware Sanctum uses while processing the 56 | | request. You may change the middleware listed below as required. 57 | | 58 | */ 59 | 60 | 'middleware' => [ 61 | 'verify_csrf_token' => App\Http\Middleware\VerifyCsrfToken::class, 62 | 'encrypt_cookies' => App\Http\Middleware\EncryptCookies::class, 63 | ], 64 | 65 | ]; 66 | -------------------------------------------------------------------------------- /app/Http/Controllers/Auth/NewPasswordController.php: -------------------------------------------------------------------------------- 1 | $request]); 24 | } 25 | 26 | /** 27 | * Handle an incoming new password request. 28 | * 29 | * @param \Illuminate\Http\Request $request 30 | * @return \Illuminate\Http\RedirectResponse 31 | * 32 | * @throws \Illuminate\Validation\ValidationException 33 | */ 34 | public function store(Request $request) 35 | { 36 | $request->validate([ 37 | 'token' => ['required'], 38 | 'email' => ['required', 'email'], 39 | 'password' => ['required', 'confirmed', Rules\Password::defaults()], 40 | ]); 41 | 42 | // Here we will attempt to reset the user's password. If it is successful we 43 | // will update the password on an actual user model and persist it to the 44 | // database. Otherwise we will parse the error and return the response. 45 | $status = Password::reset( 46 | $request->only('email', 'password', 'password_confirmation', 'token'), 47 | function ($user) use ($request) { 48 | $user->forceFill([ 49 | 'password' => Hash::make($request->password), 50 | 'remember_token' => Str::random(60), 51 | ])->save(); 52 | 53 | event(new PasswordReset($user)); 54 | } 55 | ); 56 | 57 | // If the password was successfully reset, we will redirect the user back to 58 | // the application's home authenticated view. If there is an error we can 59 | // redirect them back to where they came from with their error message. 60 | return $status == Password::PASSWORD_RESET 61 | ? redirect()->route('login')->with('status', __($status)) 62 | : back()->withInput($request->only('email')) 63 | ->withErrors(['email' => __($status)]); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /app/Http/Requests/Auth/LoginRequest.php: -------------------------------------------------------------------------------- 1 | ['required', 'string', 'email'], 33 | 'password' => ['required', 'string'], 34 | ]; 35 | } 36 | 37 | /** 38 | * Attempt to authenticate the request's credentials. 39 | * 40 | * @return void 41 | * 42 | * @throws \Illuminate\Validation\ValidationException 43 | */ 44 | public function authenticate() 45 | { 46 | $this->ensureIsNotRateLimited(); 47 | 48 | if (! Auth::attempt($this->only('email', 'password'), $this->boolean('remember'))) { 49 | RateLimiter::hit($this->throttleKey()); 50 | 51 | throw ValidationException::withMessages([ 52 | 'email' => __('auth.failed'), 53 | ]); 54 | } 55 | 56 | RateLimiter::clear($this->throttleKey()); 57 | } 58 | 59 | /** 60 | * Ensure the login request is not rate limited. 61 | * 62 | * @return void 63 | * 64 | * @throws \Illuminate\Validation\ValidationException 65 | */ 66 | public function ensureIsNotRateLimited() 67 | { 68 | if (! RateLimiter::tooManyAttempts($this->throttleKey(), 5)) { 69 | return; 70 | } 71 | 72 | event(new Lockout($this)); 73 | 74 | $seconds = RateLimiter::availableIn($this->throttleKey()); 75 | 76 | throw ValidationException::withMessages([ 77 | 'email' => trans('auth.throttle', [ 78 | 'seconds' => $seconds, 79 | 'minutes' => ceil($seconds / 60), 80 | ]), 81 | ]); 82 | } 83 | 84 | /** 85 | * Get the rate limiting throttle key for the request. 86 | * 87 | * @return string 88 | */ 89 | public function throttleKey() 90 | { 91 | return Str::lower($this->input('email')).'|'.$this->ip(); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "laravel/laravel", 3 | "type": "project", 4 | "description": "The Laravel Framework.", 5 | "keywords": ["framework", "laravel"], 6 | "license": "GPL-3.0", 7 | "require": { 8 | "php": "^8.0", 9 | "alibabacloud/green": "^1.8", 10 | "doctrine/dbal": "^3.3", 11 | "erusev/parsedown": "^1.7", 12 | "fruitcake/laravel-cors": "^2.0.5", 13 | "guzzlehttp/guzzle": "^7.2", 14 | "intervention/image": "^2.7", 15 | "intervention/imagecache": "^2.5", 16 | "laravel/breeze": "^1.8", 17 | "laravel/framework": "^9.0", 18 | "laravel/octane": "^1.2", 19 | "laravel/sanctum": "^2.14", 20 | "laravel/tinker": "^2.7", 21 | "league/flysystem-aws-s3-v3": "^3.0", 22 | "league/flysystem-ftp": "^3.0", 23 | "league/flysystem-sftp-v3": "^3.0", 24 | "league/flysystem-webdav": "^3.0", 25 | "overtrue/flysystem-cos": "^5.0", 26 | "overtrue/flysystem-qiniu": "^3.0", 27 | "tencentcloud/ims": "^3.0", 28 | "wispx/flysystem-upyun": "^1.0", 29 | "zing/flysystem-oss": "^2.1" 30 | }, 31 | "require-dev": { 32 | "fakerphp/faker": "^1.9.1", 33 | "laravel/sail": "^1.0.1", 34 | "mockery/mockery": "^1.4.4", 35 | "nunomaduro/collision": "^6.1", 36 | "phpunit/phpunit": "^9.5.10", 37 | "spatie/laravel-ignition": "^1.0", 38 | "barryvdh/laravel-debugbar": "^3.6" 39 | }, 40 | "autoload": { 41 | "psr-4": { 42 | "App\\": "app/", 43 | "Database\\Factories\\": "database/factories/", 44 | "Database\\Seeders\\": "database/seeders/" 45 | } 46 | }, 47 | "autoload-dev": { 48 | "psr-4": { 49 | "Tests\\": "tests/" 50 | } 51 | }, 52 | "scripts": { 53 | "post-autoload-dump": [ 54 | "Illuminate\\Foundation\\ComposerScripts::postAutoloadDump", 55 | "@php artisan package:discover --ansi" 56 | ], 57 | "post-update-cmd": [ 58 | "@php artisan vendor:publish --tag=laravel-assets --ansi --force" 59 | ], 60 | "post-root-package-install": [ 61 | "@php -r \"file_exists('.env') || copy('.env.example', '.env');\"" 62 | ], 63 | "post-create-project-cmd": [ 64 | "@php artisan key:generate --ansi" 65 | ] 66 | }, 67 | "extra": { 68 | "laravel": { 69 | "dont-discover": [] 70 | } 71 | }, 72 | "config": { 73 | "optimize-autoloader": true, 74 | "preferred-install": "dist", 75 | "sort-packages": true 76 | }, 77 | "minimum-stability": "dev", 78 | "prefer-stable": true 79 | } 80 | -------------------------------------------------------------------------------- /resources/views/layouts/strategies.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | 13 | 14 |
15 | @foreach($_group->strategies as $strategy) 16 | {{ $strategy->name }} 17 | @endforeach 18 |
19 |
20 |
21 | 22 | @push('scripts') 23 | 57 | @endpush 58 | -------------------------------------------------------------------------------- /routes/auth.php: -------------------------------------------------------------------------------- 1 | middleware('guest')->middleware(CheckIsEnableRegistration::class)->name('register'); 17 | 18 | Route::post('/register', [ 19 | RegisteredUserController::class, 'store' 20 | ])->middleware('guest')->middleware(CheckIsEnableRegistration::class); 21 | 22 | Route::get('/login', [ 23 | AuthenticatedSessionController::class, 'create', 24 | ])->middleware('guest')->name('login'); 25 | 26 | Route::post('/login', [ 27 | AuthenticatedSessionController::class, 'store' 28 | ])->middleware('guest'); 29 | 30 | Route::get('/forgot-password', [ 31 | PasswordResetLinkController::class, 'create', 32 | ])->middleware('guest')->name('password.request'); 33 | 34 | Route::post('/forgot-password', [ 35 | PasswordResetLinkController::class, 'store', 36 | ])->middleware('guest')->name('password.email'); 37 | 38 | Route::get('/reset-password/{token}', [ 39 | NewPasswordController::class, 'create', 40 | ])->middleware('guest')->name('password.reset'); 41 | 42 | Route::post('/reset-password', [ 43 | NewPasswordController::class, 'store', 44 | ])->middleware('guest')->name('password.update'); 45 | 46 | Route::get('/verify-email', [ 47 | EmailVerificationPromptController::class, '__invoke', 48 | ])->middleware('auth')->name('verification.notice'); 49 | 50 | Route::get('/verify-email/{id}/{hash}', [ 51 | VerifyEmailController::class, '__invoke', 52 | ])->middleware(['auth', 'signed', 'throttle:6,1'])->name('verification.verify'); 53 | 54 | Route::post('/email/verification-notification', [ 55 | EmailVerificationNotificationController::class, 'store', 56 | ])->middleware(['auth', 'throttle:3,1'])->name('verification.send'); 57 | 58 | Route::get('/confirm-password', [ 59 | ConfirmablePasswordController::class, 'show', 60 | ])->middleware('auth')->name('password.confirm'); 61 | 62 | Route::post('/confirm-password', [ 63 | ConfirmablePasswordController::class, 'store', 64 | ])->middleware('auth'); 65 | 66 | Route::post('/logout', [ 67 | AuthenticatedSessionController::class, 'destroy', 68 | ])->middleware('auth')->name('logout'); 69 | -------------------------------------------------------------------------------- /webpack.mix.js: -------------------------------------------------------------------------------- 1 | const mix = require('laravel-mix'); 2 | 3 | /* 4 | |-------------------------------------------------------------------------- 5 | | Mix Asset Management 6 | |-------------------------------------------------------------------------- 7 | | 8 | | Mix provides a clean, fluent API for defining some Webpack build steps 9 | | for your Laravel applications. By default, we are compiling the CSS 10 | | file for the application as well as bundling up all the JS files. 11 | | 12 | */ 13 | 14 | mix.js('resources/js/app.js', 'public/js').postCss('resources/css/app.css', 'public/css', [ 15 | require('postcss-import'), 16 | require('tailwindcss'), 17 | require('autoprefixer'), 18 | ]); 19 | mix.less('resources/css/fontawesome.less', 'public/css'); 20 | mix.less('resources/css/common.less', 'public/css'); 21 | mix.less('resources/css/gallery.less', 'public/css'); 22 | 23 | mix.copy('node_modules/blueimp-file-upload/js/vendor/jquery.ui.widget.js', 'public/js/blueimp-file-upload'); 24 | mix.copy('node_modules/blueimp-file-upload/js/jquery.iframe-transport.js', 'public/js/blueimp-file-upload'); 25 | mix.copy('node_modules/blueimp-file-upload/js/jquery.fileupload.js', 'public/js/blueimp-file-upload'); 26 | mix.copy('node_modules/blueimp-load-image/js/load-image.all.min.js', 'public/js/blueimp-load-image') 27 | 28 | // justifiedGallery 29 | mix.copy('node_modules/justifiedGallery/dist/css/justifiedGallery.min.css', 'public/css/justified-gallery'); 30 | mix.copy('node_modules/justifiedGallery/dist/js/jquery.justifiedGallery.min.js', 'public/js/justified-gallery'); 31 | 32 | // viewer.js 33 | mix.copy('node_modules/viewerjs/dist/viewer.min.css', 'public/css/viewer-js') 34 | mix.copy('node_modules/viewerjs/dist/viewer.min.js', 'public/js/viewer-js') 35 | 36 | // clipboard 37 | mix.copy('node_modules/clipboard/dist/clipboard.min.js', 'public/js/clipboard') 38 | mix.copy('node_modules/copy-image-clipboard/dist/index.browser.js', 'public/js/clipboard') 39 | 40 | // dragselect 41 | mix.copy('node_modules/dragselect/dist/ds.min.js', 'public/js/dragselect') 42 | 43 | // context-menu.min.js 44 | mix.less('resources/css/context-js.less', 'public/css/context-js') 45 | mix.copy('resources/js/context-js.js', 'public/js/context-js') 46 | 47 | // apache echarts 48 | mix.copy('node_modules/echarts/dist/echarts.min.js', 'public/js/echarts') 49 | 50 | // masonry layout 51 | mix.copy('node_modules/masonry-layout/dist/masonry.pkgd.min.js', 'public/js/masonry') 52 | // imagesloaded 53 | mix.copy('node_modules/imagesloaded/imagesloaded.pkgd.min.js', 'public/js/imagesloaded') 54 | 55 | // markdown css 56 | mix.copy('node_modules/github-markdown-css/github-markdown.css', 'public/css/markdown-css') 57 | mix.copy('node_modules/github-markdown-css/github-markdown-light.css', 'public/css/markdown-css') 58 | -------------------------------------------------------------------------------- /app/Http/Controllers/Admin/StrategyController.php: -------------------------------------------------------------------------------- 1 | query('keywords'); 19 | $strategies = Strategy::query()->when($keywords, function (Builder $builder, $keywords) { 20 | $builder->where('name', 'like', "%{$keywords}%")->orWhere('intro', 'like', "%{$keywords}%"); 21 | })->withCount('images')->withSum('images', 'size')->latest()->paginate(); 22 | 23 | $strategies->appends(compact('keywords')); 24 | 25 | return view('admin.strategy.index', compact('strategies')); 26 | } 27 | 28 | public function add(): View 29 | { 30 | return view('admin.strategy.add'); 31 | } 32 | 33 | public function edit(Request $request): View 34 | { 35 | /** @var Strategy $strategy */ 36 | $strategy = Strategy::query()->findOrFail($request->route('id')); 37 | return view('admin.strategy.edit', compact('strategy')); 38 | } 39 | 40 | public function create(StrategyRequest $request): Response 41 | { 42 | $validated = $request->validated(); 43 | $strategy = new Strategy($validated); 44 | DB::transaction(function () use ($strategy, $validated) { 45 | $strategy->save(); 46 | $strategy->groups()->attach($validated['groups'] ?? []); 47 | }); 48 | return $this->success('创建成功'); 49 | } 50 | 51 | public function update(StrategyRequest $request): Response 52 | { 53 | $validated = $request->validated(); 54 | /** @var Strategy $strategy */ 55 | $strategy = Strategy::query()->findOrFail($request->route('id')); 56 | $strategy->fill($request->validated()); 57 | DB::transaction(function () use ($strategy, $validated) { 58 | $strategy->save(); 59 | $strategy->groups()->sync($validated['groups'] ?? []); 60 | }); 61 | return $this->success('保存成功'); 62 | } 63 | 64 | public function delete(Request $request): Response 65 | { 66 | /** @var Strategy $strategy */ 67 | if ($strategy = Strategy::query()->find($request->route('id'))) { 68 | DB::transaction(function () use ($strategy) { 69 | $strategy->images()->update(['strategy_id' => null]); 70 | $strategy->delete(); 71 | }); 72 | } 73 | return $this->success('删除成功'); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /config/flare.php: -------------------------------------------------------------------------------- 1 | env('FLARE_KEY'), 29 | 30 | /* 31 | |-------------------------------------------------------------------------- 32 | | Middleware 33 | |-------------------------------------------------------------------------- 34 | | 35 | | These middleware will modify the contents of the report sent to Flare. 36 | | 37 | */ 38 | 39 | 'flare_middleware' => [ 40 | RemoveRequestIp::class, 41 | AddGitInformation::class, 42 | AddNotifierName::class, 43 | AddEnvironmentInformation::class, 44 | AddExceptionInformation::class, 45 | AddDumps::class, 46 | AddLogs::class => [ 47 | 'maximum_number_of_collected_logs' => 200, 48 | ], 49 | AddQueries::class => [ 50 | 'maximum_number_of_collected_queries' => 200, 51 | 'report_query_bindings' => true, 52 | ], 53 | AddJobs::class => [ 54 | 'max_chained_job_reporting_depth' => 5, 55 | ], 56 | CensorRequestBodyFields::class => [ 57 | 'censor_fields' => [ 58 | 'password', 59 | ], 60 | ], 61 | CensorRequestHeaders::class => [ 62 | 'headers' => [ 63 | 'API-KEY' 64 | ] 65 | ] 66 | ], 67 | 68 | /* 69 | |-------------------------------------------------------------------------- 70 | | Reporting log statements 71 | |-------------------------------------------------------------------------- 72 | | 73 | | If this setting is `false` log statements won't be sent as events to Flare, 74 | | no matter which error level you specified in the Flare log channel. 75 | | 76 | */ 77 | 78 | 'send_logs_as_events' => true, 79 | ]; 80 | -------------------------------------------------------------------------------- /app/Http/Kernel.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | protected $middleware = [ 17 | // \App\Http\Middleware\TrustHosts::class, 18 | \App\Http\Middleware\TrustProxies::class, 19 | \Fruitcake\Cors\HandleCors::class, 20 | \App\Http\Middleware\PreventRequestsDuringMaintenance::class, 21 | \Illuminate\Foundation\Http\Middleware\ValidatePostSize::class, 22 | \App\Http\Middleware\TrimStrings::class, 23 | \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class, 24 | ]; 25 | 26 | /** 27 | * The application's route middleware groups. 28 | * 29 | * @var array> 30 | */ 31 | protected $middlewareGroups = [ 32 | 'web' => [ 33 | \App\Http\Middleware\EncryptCookies::class, 34 | \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class, 35 | \Illuminate\Session\Middleware\StartSession::class, 36 | // \Illuminate\Session\Middleware\AuthenticateSession::class, 37 | \Illuminate\View\Middleware\ShareErrorsFromSession::class, 38 | \App\Http\Middleware\VerifyCsrfToken::class, 39 | \Illuminate\Routing\Middleware\SubstituteBindings::class, 40 | ], 41 | 42 | 'api' => [ 43 | \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class, 44 | 'throttle:api', 45 | \Illuminate\Routing\Middleware\SubstituteBindings::class, 46 | ], 47 | ]; 48 | 49 | /** 50 | * The application's route middleware. 51 | * 52 | * These middleware may be assigned to groups or used individually. 53 | * 54 | * @var array 55 | */ 56 | protected $routeMiddleware = [ 57 | 'auth' => \App\Http\Middleware\Authenticate::class, 58 | 'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class, 59 | 'auth.admin' => \App\Http\Middleware\AuthenticateWithAdmin::class, 60 | 'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class, 61 | 'can' => \Illuminate\Auth\Middleware\Authorize::class, 62 | 'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class, 63 | 'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class, 64 | 'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class, 65 | 'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class, 66 | 'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class, 67 | ]; 68 | } 69 | -------------------------------------------------------------------------------- /resources/views/layouts/app.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | {{ \App\Utils::config(\App\Enums\ConfigKey::AppName) }} 11 | 12 | 13 | 14 | 15 | @stack('styles') 16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | @include('layouts.sidebar') 24 | @include('layouts.header') 25 | 41 | 42 | {{ $slot }} 43 | 44 |
45 | 46 | 47 | 48 | @include('common.notice') 49 | 64 | @if(file_exists(public_path('js/custom.js'))) 65 | 66 | @endif 67 | @stack('scripts') 68 | 69 | --------------------------------------------------------------------------------