├── 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 | merge(['class' => 'block font-medium text-sm text-gray-700']) }}>
4 | {{ $value ?? $slot }}
5 |
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 | merge(['type' => 'submit', 'class' => 'inline-flex justify-center py-2 px-4 text-sm font-medium rounded-md text-gray-600 bg-black/10 hover:bg-black/20 hover:text-gray-700']) }}>
2 | {{ $slot }}
3 |
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 | 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 | {{ $slot }}
5 |
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 | {{ $slot }}
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 | {{ $slot }}
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 |
10 | @foreach ($errors->all() as $error)
11 | {{ $error }}
12 | @endforeach
13 |
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 |
10 | {{ $column }}
11 |
12 | @endforeach
13 |
14 |
15 |
16 | {{ $slot }}
17 |
18 |
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 |
22 | {{ $content }}
23 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
39 |
40 |
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 |
--------------------------------------------------------------------------------