Browse Source

first admin sys push

parent
commit
ca8f0a2cb4
100 changed files with 15400 additions and 0 deletions
  1. 18
    0
      .editorconfig
  2. 65
    0
      .env.example
  3. 11
    0
      .gitattributes
  4. 23
    0
      .gitignore
  5. 27
    0
      app/Console/Kernel.php
  6. 30
    0
      app/Exceptions/Handler.php
  7. 107
    0
      app/Filament/Pages/Auth/Login.php
  8. 83
    0
      app/Filament/Resources/NewsCategoryResource.php
  9. 17
    0
      app/Filament/Resources/NewsCategoryResource/Pages/CreateNewsCategory.php
  10. 23
    0
      app/Filament/Resources/NewsCategoryResource/Pages/EditNewsCategory.php
  11. 19
    0
      app/Filament/Resources/NewsCategoryResource/Pages/ListNewsCategories.php
  12. 12
    0
      app/Filament/Resources/NewsCategoryResource/Pages/ViewNewsCategory.php
  13. 298
    0
      app/Filament/Resources/NewsResource.php
  14. 22
    0
      app/Filament/Resources/NewsResource/Pages/CreateNews.php
  15. 17
    0
      app/Filament/Resources/NewsResource/Pages/EditNews.php
  16. 52
    0
      app/Filament/Resources/NewsResource/Pages/HasNewsPreview.php
  17. 19
    0
      app/Filament/Resources/NewsResource/Pages/ListNews.php
  18. 11
    0
      app/Filament/Resources/NewsResource/Pages/ViewNews.php
  19. 84
    0
      app/Filament/Resources/UserResource.php
  20. 18
    0
      app/Filament/Resources/UserResource/Pages/CreateUser.php
  21. 19
    0
      app/Filament/Resources/UserResource/Pages/EditUser.php
  22. 19
    0
      app/Filament/Resources/UserResource/Pages/ListUsers.php
  23. 215
    0
      app/Http/Controllers/Api/NewsController.php
  24. 12
    0
      app/Http/Controllers/Controller.php
  25. 39
    0
      app/Http/Helper/Helper.php
  26. 69
    0
      app/Http/Kernel.php
  27. 31
    0
      app/Http/Middleware/AccessIpMiddleware.php
  28. 17
    0
      app/Http/Middleware/Authenticate.php
  29. 17
    0
      app/Http/Middleware/EncryptCookies.php
  30. 17
    0
      app/Http/Middleware/PreventRequestsDuringMaintenance.php
  31. 30
    0
      app/Http/Middleware/RedirectIfAuthenticated.php
  32. 19
    0
      app/Http/Middleware/TrimStrings.php
  33. 20
    0
      app/Http/Middleware/TrustHosts.php
  34. 28
    0
      app/Http/Middleware/TrustProxies.php
  35. 22
    0
      app/Http/Middleware/ValidateSignature.php
  36. 17
    0
      app/Http/Middleware/VerifyCsrfToken.php
  37. 58
    0
      app/Http/Requests/ContactRequest.php
  38. 48
    0
      app/Http/Requests/RegistEPaperRequest.php
  39. 77
    0
      app/Models/News.php
  40. 22
    0
      app/Models/NewsCategory.php
  41. 66
    0
      app/Models/NewsParagraph.php
  42. 31
    0
      app/Models/NewsPhoto.php
  43. 57
    0
      app/Models/User.php
  44. 108
    0
      app/Policies/RolePolicy.php
  45. 36
    0
      app/Providers/AppServiceProvider.php
  46. 26
    0
      app/Providers/AuthServiceProvider.php
  47. 19
    0
      app/Providers/BroadcastServiceProvider.php
  48. 38
    0
      app/Providers/EventServiceProvider.php
  49. 73
    0
      app/Providers/Filament/AdminPanelProvider.php
  50. 40
    0
      app/Providers/RouteServiceProvider.php
  51. 84
    0
      app/Service/CaptchaService.php
  52. 44
    0
      app/Supports/Response.php
  53. 18
    0
      artisan
  54. 18
    0
      bootstrap/app.php
  55. 2
    0
      bootstrap/cache/.gitignore
  56. 7
    0
      bootstrap/providers.php
  57. 82
    0
      composer.json
  58. 10206
    0
      composer.lock
  59. 126
    0
      config/app.php
  60. 115
    0
      config/auth.php
  61. 108
    0
      config/cache.php
  62. 174
    0
      config/database.php
  63. 92
    0
      config/filament-shield.php
  64. 101
    0
      config/filament.php
  65. 80
    0
      config/filesystems.php
  66. 132
    0
      config/logging.php
  67. 118
    0
      config/mail.php
  68. 202
    0
      config/permission.php
  69. 112
    0
      config/queue.php
  70. 38
    0
      config/services.php
  71. 217
    0
      config/session.php
  72. 1
    0
      database/.gitignore
  73. 44
    0
      database/factories/UserFactory.php
  74. 49
    0
      database/migrations/0001_01_01_000000_create_users_table.php
  75. 35
    0
      database/migrations/0001_01_01_000001_create_cache_table.php
  76. 57
    0
      database/migrations/0001_01_01_000002_create_jobs_table.php
  77. 34
    0
      database/migrations/2025_05_20_043731_create_news_categories_table.php
  78. 47
    0
      database/migrations/2025_05_20_043733_create_news_table.php
  79. 41
    0
      database/migrations/2025_05_20_043735_create_news_paragraphs_table.php
  80. 35
    0
      database/migrations/2025_05_20_043736_create_news_photos_table.php
  81. 136
    0
      database/migrations/2025_05_20_073544_create_permission_tables.php
  82. 31
    0
      database/migrations/2025_05_20_090304_create_notifications_table.php
  83. 23
    0
      database/seeders/DatabaseSeeder.php
  84. 12
    0
      lang/vendor/filament-panels/ar/global-search.php
  85. 63
    0
      lang/vendor/filament-panels/ar/layout.php
  86. 51
    0
      lang/vendor/filament-panels/ar/pages/auth/edit-profile.php
  87. 35
    0
      lang/vendor/filament-panels/ar/pages/auth/email-verification/email-verification-prompt.php
  88. 61
    0
      lang/vendor/filament-panels/ar/pages/auth/login.php
  89. 42
    0
      lang/vendor/filament-panels/ar/pages/auth/password-reset/request-password-reset.php
  90. 43
    0
      lang/vendor/filament-panels/ar/pages/auth/password-reset/reset-password.php
  91. 56
    0
      lang/vendor/filament-panels/ar/pages/auth/register.php
  92. 33
    0
      lang/vendor/filament-panels/ar/pages/dashboard.php
  93. 25
    0
      lang/vendor/filament-panels/ar/pages/tenancy/edit-tenant-profile.php
  94. 37
    0
      lang/vendor/filament-panels/ar/resources/pages/create-record.php
  95. 41
    0
      lang/vendor/filament-panels/ar/resources/pages/edit-record.php
  96. 7
    0
      lang/vendor/filament-panels/ar/resources/pages/list-records.php
  97. 17
    0
      lang/vendor/filament-panels/ar/resources/pages/view-record.php
  98. 7
    0
      lang/vendor/filament-panels/ar/unsaved-changes-alert.php
  99. 15
    0
      lang/vendor/filament-panels/ar/widgets/account-widget.php
  100. 0
    0
      lang/vendor/filament-panels/ar/widgets/filament-info-widget.php

+ 18
- 0
.editorconfig View File

@@ -0,0 +1,18 @@
1
+root = true
2
+
3
+[*]
4
+charset = utf-8
5
+end_of_line = lf
6
+indent_size = 4
7
+indent_style = space
8
+insert_final_newline = true
9
+trim_trailing_whitespace = true
10
+
11
+[*.md]
12
+trim_trailing_whitespace = false
13
+
14
+[*.{yml,yaml}]
15
+indent_size = 2
16
+
17
+[docker-compose.yml]
18
+indent_size = 4

+ 65
- 0
.env.example View File

@@ -0,0 +1,65 @@
1
+APP_NAME=Laravel
2
+APP_ENV=local
3
+APP_KEY=
4
+APP_DEBUG=true
5
+APP_URL=http://localhost
6
+
7
+APP_LOCALE=en
8
+APP_FALLBACK_LOCALE=en
9
+APP_FAKER_LOCALE=en_US
10
+
11
+APP_MAINTENANCE_DRIVER=file
12
+# APP_MAINTENANCE_STORE=database
13
+
14
+PHP_CLI_SERVER_WORKERS=4
15
+
16
+BCRYPT_ROUNDS=12
17
+
18
+LOG_CHANNEL=stack
19
+LOG_STACK=single
20
+LOG_DEPRECATIONS_CHANNEL=null
21
+LOG_LEVEL=debug
22
+
23
+DB_CONNECTION=sqlite
24
+# DB_HOST=127.0.0.1
25
+# DB_PORT=3306
26
+# DB_DATABASE=yicoadmin
27
+# DB_USERNAME=root
28
+# DB_PASSWORD=
29
+
30
+SESSION_DRIVER=database
31
+SESSION_LIFETIME=120
32
+SESSION_ENCRYPT=false
33
+SESSION_PATH=/
34
+SESSION_DOMAIN=null
35
+
36
+BROADCAST_CONNECTION=log
37
+FILESYSTEM_DISK=local
38
+QUEUE_CONNECTION=database
39
+
40
+CACHE_STORE=database
41
+# CACHE_PREFIX=
42
+
43
+MEMCACHED_HOST=127.0.0.1
44
+
45
+REDIS_CLIENT=phpredis
46
+REDIS_HOST=127.0.0.1
47
+REDIS_PASSWORD=null
48
+REDIS_PORT=6379
49
+
50
+MAIL_MAILER=log
51
+MAIL_SCHEME=null
52
+MAIL_HOST=127.0.0.1
53
+MAIL_PORT=2525
54
+MAIL_USERNAME=null
55
+MAIL_PASSWORD=null
56
+MAIL_FROM_ADDRESS="hello@example.com"
57
+MAIL_FROM_NAME="${APP_NAME}"
58
+
59
+AWS_ACCESS_KEY_ID=
60
+AWS_SECRET_ACCESS_KEY=
61
+AWS_DEFAULT_REGION=us-east-1
62
+AWS_BUCKET=
63
+AWS_USE_PATH_STYLE_ENDPOINT=false
64
+
65
+VITE_APP_NAME="${APP_NAME}"

+ 11
- 0
.gitattributes View File

@@ -0,0 +1,11 @@
1
+* text=auto eol=lf
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
+.styleci.yml export-ignore

+ 23
- 0
.gitignore View File

@@ -0,0 +1,23 @@
1
+/.phpunit.cache
2
+/node_modules
3
+/public/build
4
+/public/hot
5
+/public/storage
6
+/storage/*.key
7
+/storage/pail
8
+/vendor
9
+.env
10
+.env.backup
11
+.env.production
12
+.phpactor.json
13
+.phpunit.result.cache
14
+Homestead.json
15
+Homestead.yaml
16
+npm-debug.log
17
+yarn-error.log
18
+/auth.json
19
+/.fleet
20
+/.idea
21
+/.nova
22
+/.vscode
23
+/.zed

+ 27
- 0
app/Console/Kernel.php View File

@@ -0,0 +1,27 @@
1
+<?php
2
+
3
+namespace App\Console;
4
+
5
+use Illuminate\Console\Scheduling\Schedule;
6
+use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
7
+
8
+class Kernel extends ConsoleKernel
9
+{
10
+    /**
11
+     * Define the application's command schedule.
12
+     */
13
+    protected function schedule(Schedule $schedule): void
14
+    {
15
+        // $schedule->command('inspire')->hourly();
16
+    }
17
+
18
+    /**
19
+     * Register the commands for the application.
20
+     */
21
+    protected function commands(): void
22
+    {
23
+        $this->load(__DIR__.'/Commands');
24
+
25
+        require base_path('routes/console.php');
26
+    }
27
+}

+ 30
- 0
app/Exceptions/Handler.php View File

@@ -0,0 +1,30 @@
1
+<?php
2
+
3
+namespace App\Exceptions;
4
+
5
+use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
6
+use Throwable;
7
+
8
+class Handler extends ExceptionHandler
9
+{
10
+    /**
11
+     * The list of the inputs that are never flashed to the session on validation exceptions.
12
+     *
13
+     * @var array<int, string>
14
+     */
15
+    protected $dontFlash = [
16
+        'current_password',
17
+        'password',
18
+        'password_confirmation',
19
+    ];
20
+
21
+    /**
22
+     * Register the exception handling callbacks for the application.
23
+     */
24
+    public function register(): void
25
+    {
26
+        $this->reportable(function (Throwable $e) {
27
+            //
28
+        });
29
+    }
30
+}

+ 107
- 0
app/Filament/Pages/Auth/Login.php View File

@@ -0,0 +1,107 @@
1
+<?php
2
+
3
+namespace App\Filament\Pages\Auth;
4
+
5
+use Filament\Forms\Components\ViewField;
6
+use Filament\Http\Responses\Auth\LoginResponse;
7
+use Filament\Notifications\Notification;
8
+use Filament\Pages\Auth\Login as BaseLogin;
9
+use Filament\Actions\Action;
10
+use Filament\Actions\ActionGroup;
11
+use Filament\Forms\Components\TextInput;
12
+use Filament\Forms\Components\Component;
13
+use App\Service\CaptchaService;
14
+use Filament\Forms\Form;
15
+use Illuminate\Validation\ValidationException;
16
+use Livewire\Attributes\Computed;
17
+use Illuminate\Support\Facades\Cache;
18
+use Filament\Forms\Components\View;
19
+
20
+class Login extends BaseLogin
21
+{
22
+    protected static ?string $navigationIcon = 'heroicon-o-document-text';
23
+
24
+    protected static string $view = 'filament.pages.auth.login';
25
+
26
+    public ?string $verification_code = null;
27
+    public ?string $captchaImage = null;
28
+
29
+    public function mount(): void
30
+    {
31
+        parent::mount();
32
+        if (!$this->captchaImage) {
33
+            $this->refreshCaptcha();
34
+        }
35
+    }
36
+
37
+    public function refreshCaptcha(): void
38
+    {
39
+        $captchaService = app(CaptchaService::class);
40
+        $code = $captchaService->generateCode();
41
+
42
+        // Store code in cache with user's session ID as key
43
+        Cache::put(
44
+            'verification_code_' . session()->getId(),
45
+            $code,
46
+            now()->addMinutes(5)
47
+        );
48
+        $this->captchaImage = $captchaService->generateImage($code);
49
+        $this->dispatch('captcha-refreshed');
50
+    }
51
+
52
+    public function captchaImageUrl(): string
53
+    {
54
+        if(!$this->captchaImage)$this->refreshCaptcha();
55
+        return $this->captchaImage;
56
+    }
57
+
58
+    public function form(Form $form): Form
59
+    {
60
+        return $form
61
+            ->schema([
62
+                $this->getEmailFormComponent(),
63
+                $this->getPasswordFormComponent(),
64
+                $this->getVerificationCodeFormComponent(),
65
+                ViewField::make('captcha')
66
+                ->view('filament.pages.auth.captcha'),
67
+                $this->getRememberFormComponent(),
68
+            ])
69
+            ->statePath('data');
70
+    }
71
+
72
+    protected function getVerificationCodeFormComponent(): Component
73
+    {
74
+        return TextInput::make('verification_code')
75
+            ->label('Verification Code')
76
+            ->required()
77
+            ->length(6)
78
+            ->placeholder('Enter code shown above');
79
+    }
80
+
81
+    public function authenticate(): ?LoginResponse
82
+    {
83
+        $this->validate();
84
+        $formData = $this->form->getState();
85
+        if (! $this->verifyCode($formData['verification_code'])) {
86
+            throw ValidationException::withMessages([
87
+                'data.verification_code' => __('驗證碼錯誤'),
88
+            ]);
89
+        }
90
+
91
+        try {
92
+            return parent::authenticate();
93
+        } catch (ValidationException $e) {
94
+            $this->refreshCaptcha();
95
+            // throw $e;
96
+            throw ValidationException::withMessages([
97
+                'verification_code' => __('Invalid verification code.'),
98
+            ]);
99
+        }
100
+    }
101
+
102
+    protected function verifyCode(string $code): bool
103
+    {
104
+        $validCode = Cache::get('verification_code_' . session()->getId());
105
+        return $code === $validCode;
106
+    }
107
+}

+ 83
- 0
app/Filament/Resources/NewsCategoryResource.php View File

@@ -0,0 +1,83 @@
1
+<?php
2
+
3
+namespace App\Filament\Resources;
4
+
5
+use App\Filament\Resources\NewsCategoryResource\Pages;
6
+use App\Models\NewsCategory;
7
+use Filament\Forms\Components\Section;
8
+use Filament\Forms\Components\TextInput;
9
+use Filament\Forms\Form;
10
+use Filament\Resources\Concerns\Translatable;
11
+use Filament\Resources\Resource;
12
+use Filament\Tables;
13
+use Filament\Tables\Columns\TextColumn;
14
+use Filament\Tables\Table;
15
+use SolutionForest\FilamentTranslateField\Forms\Component\Translate;
16
+
17
+class NewsCategoryResource extends Resource
18
+{
19
+    protected static ?string $model = NewsCategory::class;
20
+    protected static ?string $modelLabel = "文章分類管理";
21
+    protected static ?string $navigationIcon = 'heroicon-o-rectangle-group';
22
+    protected static ?string $navigationGroup = '上稿內容管理';
23
+    protected static ?string $navigationLabel = "文章分類管理";
24
+
25
+    use Translatable;
26
+    public static function form(Form $form): Form
27
+    {
28
+        return $form
29
+            ->schema([
30
+                //
31
+                Section::make("")->schema([
32
+                    Translate::make()->schema(fn (string $locale) => [
33
+                        TextInput::make('name')->required($locale == 'zh_TW')->maxLength(40)->label("分類名稱")
34
+                    ])
35
+                    ->locales(["zh_TW", "en"])
36
+                    ->columnSpan(5),
37
+                    TextInput::make('order')->label("排序")->integer()->default(0)->columnSpan(1)
38
+                ])->columns(4)
39
+            ]);
40
+    }
41
+
42
+    public static function table(Table $table): Table
43
+    {
44
+        return $table
45
+            ->columns([
46
+                //
47
+                TextColumn::make('name')->label("分類名稱"),
48
+                TextColumn::make(name: 'created_at')->label("建立時間")->date(),
49
+                TextColumn::make('updated_at')->label("更新時間")->date()
50
+            ])
51
+            ->filters([
52
+                //
53
+            ])
54
+            ->actions([
55
+                // Tables\Actions\ViewAction::make(),
56
+                Tables\Actions\EditAction::make(),
57
+                Tables\Actions\DeleteAction::make()
58
+            ])
59
+            ->bulkActions([
60
+                Tables\Actions\BulkActionGroup::make([
61
+                    Tables\Actions\DeleteBulkAction::make(),
62
+                ]),
63
+            ])
64
+            ->defaultSort('order', 'desc')
65
+            ->defaultSort('created_at', 'desc');
66
+    }
67
+
68
+    public static function getRelations(): array
69
+    {
70
+        return [
71
+            //
72
+        ];
73
+    }
74
+
75
+    public static function getPages(): array
76
+    {
77
+        return [
78
+            'index' => Pages\ListNewsCategories::route('/'),
79
+            'create' => Pages\CreateNewsCategory::route('/create'),
80
+            'edit' => Pages\EditNewsCategory::route('/{record}/edit'),
81
+        ];
82
+    }
83
+}

+ 17
- 0
app/Filament/Resources/NewsCategoryResource/Pages/CreateNewsCategory.php View File

@@ -0,0 +1,17 @@
1
+<?php
2
+
3
+namespace App\Filament\Resources\NewsCategoryResource\Pages;
4
+
5
+use App\Filament\Resources\NewsCategoryResource;
6
+use Filament\Actions;
7
+use Filament\Resources\Pages\CreateRecord;
8
+
9
+class CreateNewsCategory extends CreateRecord
10
+{
11
+    protected static string $resource = NewsCategoryResource::class;
12
+    protected static bool $canCreateAnother = false;
13
+    protected function getRedirectUrl(): string
14
+    {
15
+        return $this->getResource()::getUrl('index');
16
+    }
17
+}

+ 23
- 0
app/Filament/Resources/NewsCategoryResource/Pages/EditNewsCategory.php View File

@@ -0,0 +1,23 @@
1
+<?php
2
+
3
+namespace App\Filament\Resources\NewsCategoryResource\Pages;
4
+
5
+use App\Filament\Resources\NewsCategoryResource;
6
+use Filament\Actions;
7
+use Filament\Resources\Pages\EditRecord;
8
+
9
+class EditNewsCategory extends EditRecord
10
+{
11
+    protected static string $resource = NewsCategoryResource::class;
12
+
13
+    protected function getHeaderActions(): array
14
+    {
15
+        return [
16
+            // Actions\DeleteAction::make(),
17
+        ];
18
+    }
19
+    protected function getRedirectUrl(): string
20
+    {
21
+        return $this->getResource()::getUrl('index');
22
+    }
23
+}

+ 19
- 0
app/Filament/Resources/NewsCategoryResource/Pages/ListNewsCategories.php View File

@@ -0,0 +1,19 @@
1
+<?php
2
+
3
+namespace App\Filament\Resources\NewsCategoryResource\Pages;
4
+
5
+use App\Filament\Resources\NewsCategoryResource;
6
+use Filament\Actions;
7
+use Filament\Resources\Pages\ListRecords;
8
+
9
+class ListNewsCategories extends ListRecords
10
+{
11
+    protected static string $resource = NewsCategoryResource::class;
12
+
13
+    protected function getHeaderActions(): array
14
+    {
15
+        return [
16
+            Actions\CreateAction::make()->label("新增"),
17
+        ];
18
+    }
19
+}

+ 12
- 0
app/Filament/Resources/NewsCategoryResource/Pages/ViewNewsCategory.php View File

@@ -0,0 +1,12 @@
1
+<?php
2
+
3
+namespace App\Filament\Resources\NewsCategoryResource\Pages;
4
+
5
+use App\Filament\Resources\NewsCategoryResource;
6
+use Filament\Actions;
7
+use Filament\Resources\Pages\ViewRecord;
8
+
9
+class ViewNewsCategory extends ViewRecord
10
+{
11
+    protected static string $resource = NewsCategoryResource::class;
12
+}

+ 298
- 0
app/Filament/Resources/NewsResource.php View File

@@ -0,0 +1,298 @@
1
+<?php
2
+
3
+namespace App\Filament\Resources;
4
+
5
+use App\Filament\Resources\NewsResource\Pages;
6
+use App\Models\News;
7
+use App\Models\NewsCategory;
8
+use Filament\Forms\Components\Actions\Action;
9
+use Filament\Forms\Components\Group;
10
+use Filament\Forms\Components\Repeater;
11
+use Filament\Forms\Components\RichEditor;
12
+use Filament\Forms\Components\TextInput;
13
+use Filament\Forms\Components\Textarea;
14
+use Filament\Forms\Components\Select;
15
+use Filament\Forms\Components\Radio;
16
+use Filament\Forms\Components\Section;
17
+use Filament\Forms\Components\Toggle;
18
+use Filament\Forms\Components\DatePicker;
19
+use Filament\Forms\Components\FileUpload;
20
+use Filament\Forms\Get;
21
+use Filament\Forms\Form;
22
+use Filament\Resources\Resource;
23
+use Filament\Tables;
24
+use Filament\Tables\Columns\IconColumn;
25
+use Filament\Tables\Columns\ImageColumn;
26
+use Filament\Tables\Columns\TextColumn;
27
+use Filament\Tables\Filters\SelectFilter;
28
+use Filament\Tables\Table;
29
+use Illuminate\Database\Eloquent\Builder;
30
+use Illuminate\Support\Facades\DB;
31
+use Illuminate\Support\Str;
32
+use SolutionForest\FilamentTranslateField\Forms\Component\Translate;
33
+use RalphJSmit\Filament\SEO\SEO;
34
+
35
+class NewsResource extends Resource
36
+{
37
+    protected static ?string $model = News::class;
38
+    protected static ?string $modelLabel = "文章管理";
39
+    protected static ?string $navigationIcon = 'heroicon-o-newspaper';
40
+    protected static ?string $navigationGroup = '上稿內容管理';
41
+    protected static ?string $navigationLabel = "文章管理";
42
+
43
+    public static function form(Form $form): Form
44
+    {
45
+        return $form
46
+            ->schema([
47
+                //
48
+                Section::make("新增 文章")->schema([
49
+                    Group::make()->schema([
50
+                        Select::make("news_category_id")
51
+                        ->options(NewsCategory::get()->pluck("name","id"))
52
+                        ->label("文章分類")
53
+                        ->columnSpan(1)
54
+                        ->native(false)
55
+                        ->Live(),
56
+                    ])->columnSpanFull()->columns(2),
57
+                    Group::make()->schema([
58
+                        DatePicker::make('post_date')
59
+                        ->label("發布日期")
60
+                        ->closeOnDateSelection(),
61
+                    ])->columnSpanFull()->columns(2),
62
+                    FileUpload::make('news_img_pc')->label("列表圖(desktop)")
63
+                    ->disk("public")
64
+                    // ->helperText('建議寬高限制為:1280*720px,檔案大小限制為1M以下')->maxSize('1024')
65
+                    ->directory("news"),
66
+                    FileUpload::make('news_img_mobile')->label("列表圖(mobile)")
67
+                    ->disk("public")
68
+                    // ->helperText('建議寬高限制為:600*896px,檔案大小限制為1M以下')->maxSize('1024')
69
+                    ->directory("news"),
70
+                    Translate::make()->schema(fn (string $locale) => [
71
+                        TextInput::make('title')
72
+                        ->label("標題")
73
+                        ->columnSpan(1),
74
+                        TextInput::make('written_by')
75
+                        ->label("發佈者")->columnSpan(1),
76
+                        Textarea::make("description")->rows(5)->columnSpanFull()->label("短文"),
77
+                    ])->locales(["zh_TW", "en"])
78
+                    ->id("main")
79
+                    ->columnSpanFull()->columns(3),
80
+                    TextInput::make('order')->label("排序")->integer()->default(0),
81
+                ])->columns(3),
82
+                Section::make("SEO")->schema([
83
+                    // SEO::make(),
84
+                    Translate::make()->schema(fn (string $locale) => [
85
+                        TextInput::make('meta_title')->label("SEO 標題")->columnSpan(1),
86
+                        Textarea::make("meta_keyword")->rows(5)->columnSpanFull()->label("SEO 關鍵字"),
87
+                        Textarea::make("meta_description")->rows(5)->columnSpanFull()->label("SEO 短文"),
88
+                    ])
89
+                    ->locales(["zh_TW", "en"])
90
+                    ->id("seo"),
91
+                    FileUpload::make('meta_img')->label("放大預覽圖")->disk("public")
92
+                    // ->helperText('建議寬高限制為:1200*630px,檔案大小限制為1M以下')->maxSize('1024')
93
+                    ->directory("seo/news"),
94
+                ])->columnSpanFull(),
95
+                Section::make("文章內容")->schema([
96
+                    Repeater::make("paragraphs")->schema([
97
+                        TextInput::make('item_key')
98
+                            ->default(fn () => Str::random())
99
+                            ->hidden()
100
+                            ->afterStateHydrated(function (TextInput $component, $state) {
101
+                                if (empty($state)) {
102
+                                    $component->state(Str::random());
103
+                                }
104
+                            }),
105
+                        Radio::make("paragraph_type")->options([
106
+                            1 => "圖片",
107
+                            2 => "文字",
108
+                            3 => "影音"
109
+                        ])->label("")->default(1)->Live(),
110
+                        Group::make()->schema([
111
+                            Translate::make()->schema(fn (string $locale) => [
112
+                                TextInput::make('image_alt')->label("圖片註文")->columnSpan(1),
113
+                            ])->locales(["zh_TW", "en"])
114
+                            ->id(fn ($get) => "para_img_" . $get('item_key')),
115
+                            FileUpload::make('image_url')->label("")->disk("public")
116
+                            // ->helperText('建議寬高限制為:1080*675px,檔案大小限制為1M以下')->maxSize('1024')
117
+                            ->directory("news/paragraph"),
118
+                            TextInput::make('image_link')->label("外連網址")->columnSpanFull()->url(),
119
+                        ])->visible(fn (Get $get):bool => $get("paragraph_type") == 1),
120
+                        Group::make()->schema([
121
+                            Translate::make()->schema(fn (string $locale) => [
122
+                                RichEditor::make('text_content')
123
+                                ->toolbarButtons([
124
+                                    'blockquote',
125
+                                    'bold',
126
+                                    'bulletList',
127
+                                    'h2',
128
+                                    'h3',
129
+                                    'italic',
130
+                                    'link',
131
+                                    'orderedList',
132
+                                    'redo',
133
+                                    'strike',
134
+                                    'underline',
135
+                                    'undo',
136
+                                ])
137
+                                ->disableToolbarButtons([
138
+                                    'blockquote',
139
+                                    'strike',
140
+                                    'attachFiles',
141
+                                ])
142
+                                ->fileAttachmentsDirectory('attachments')
143
+                                ->fileAttachmentsVisibility('private'),
144
+                            ])->locales(["zh_TW", "en"])
145
+                                ->id(fn ($get) => "para_text_" . $get('item_key')),
146
+                        ])->visible(fn (Get $get):bool => $get("paragraph_type") == 2),
147
+                            Group::make()->schema([
148
+                                Section::make("")->schema([
149
+                                    FileUpload::make('video_img')->label("影片底圖")
150
+                                    ->disk("public")
151
+                                    ->directory("news/paragraph/video"),
152
+                                    // ->helperText('建議寬高限制為:2000*720px,出血寬度720px,主要圖像範圍為:1280*720px,檔案大小限制為1M以下')->maxSize('1024'),
153
+                                    Radio::make("video_type")->label("")->options([
154
+                                        1 => "網址",
155
+                                        2 => "檔案"
156
+                                    ])->columnSpanFull()->default(1)->Live(),
157
+                                    Group::make()->schema([
158
+                                        TextInput::make('link')->label("網址")->nullable(),
159
+                                    ])->visible(fn (Get $get):bool => $get("video_type") == 1)->columnSpanFull(),
160
+                                    Group::make()->schema([
161
+                                        FileUpload::make('video_url')->label("")->disk("public")->directory("news/paragraph/video")
162
+                                        // ->helperText('建議影片寬高限制為:1920*1080px,出血寬度720px,大小限制為:100M以下')
163
+                                        // ->maxSize(102400)
164
+                                        ->nullable(),
165
+                                    ])->visible(fn (Get $get):bool => $get("video_type") == 2)->columnSpanFull(),
166
+                                ]),
167
+                            ])->visible(fn (Get $get):bool => $get("paragraph_type") == 3),
168
+                    ])
169
+                    ->relationship("paragraphs")
170
+                    ->label("段落")
171
+                    ->collapsible()
172
+                    ->reorderableWithButtons()
173
+                    ->orderColumn('order')
174
+                    ->cloneable()
175
+                ]),
176
+                Section::make("額外圖片")->schema([
177
+                    Repeater::make("extra_images")->schema([
178
+                        FileUpload::make('image_url')->label("")->disk("public")
179
+                        ->directory("news/extraPhoto")
180
+                        ->maxFiles(10),
181
+                        TextInput::make('image_link')->label("外連網址")->columnSpanFull()->url(),
182
+                    ])
183
+                    ->helperText('至少需上傳4張圖片才會出現圖片輯')
184
+                    ->relationship("photos")
185
+                    ->label("")
186
+                    ->collapsible()
187
+                    ->orderColumn('order')
188
+                    ->defaultItems(0)
189
+                ])->columns(5)
190
+            ]);
191
+    }
192
+
193
+    public static function table(Table $table): Table
194
+    {
195
+        return $table
196
+            ->columns([
197
+                //
198
+                TextColumn::make("newsCategory.name")->label("分類")->alignCenter(),
199
+                TextColumn::make("title")->label("標題")->alignCenter(),
200
+                TextColumn::make("written_by")->label("發佈者")->alignCenter(),
201
+                TextColumn::make("post_date")->label("發佈時間")->dateTime('Y/m/d')->alignCenter(),
202
+                ImageColumn::make("news_img_pc_url")->label("列表圖")->alignCenter(),
203
+                TextColumn::make("list_audit_state")->label("狀態")->badge()
204
+                ->color(fn (string $state): string => match ($state) {
205
+                    '暫存' => 'warning',
206
+                    '已發佈' => 'success',
207
+                }),
208
+                IconColumn::make("on_top")->label("置頂")
209
+                ->color(fn (string $state): string => match ($state) {
210
+                    1 => 'success',
211
+                    default => ''
212
+                })
213
+                ->icon(fn (string $state): string => match ($state) {
214
+                    1 => 'heroicon-o-check-circle',
215
+                    default => ''
216
+                })
217
+                ->action(function ($record): void {
218
+                    $record->on_top = !$record->on_top;
219
+                    $record->save();
220
+                }),
221
+                TextColumn::make("created_at")->label("建立時間")->dateTime('Y/m/d H:i:s')->alignCenter(),
222
+                TextColumn::make("updated_at")->label("更新時間")->dateTime()->alignCenter(),
223
+            ])
224
+            ->filters([
225
+                SelectFilter::make('news_category_id')->label("分類")
226
+                ->options(NewsCategory::orderBy("order")->pluck("name", "id"))
227
+                ->attribute('news_category_id'),
228
+                SelectFilter::make('post_date')->label("年份")
229
+                ->options(News::select(DB::raw("DATE_FORMAT(post_date, '%Y') as year"))->whereNotNull("post_date")->distinct()->pluck("year","year")->toArray())
230
+                ->query(
231
+                    fn (array $data, Builder $query): Builder =>
232
+                    $query->when(
233
+                        $data['value'],
234
+                        fn (Builder $query, $value): Builder => $query->where('post_date', 'like', $data['value']. "%")
235
+                    )
236
+                ),
237
+                SelectFilter::make('visible')->label("狀態")
238
+                ->options([
239
+                    0 => "暫存",
240
+                    1 => "已發佈",
241
+                ])
242
+                ->query(
243
+                    fn (array $data, Builder $query): Builder =>
244
+                    $query->when(
245
+                        $data['value'],
246
+                        fn (Builder $query, $value): Builder => $query->where('visible', $data['value'])
247
+                    )
248
+                ),
249
+            ])
250
+            ->actions([
251
+                Tables\Actions\EditAction::make(),
252
+                Tables\Actions\DeleteAction::make(),
253
+                \Filament\Tables\Actions\Action::make("audit")
254
+                ->label(fn ($record) => match ($record->visible) {
255
+                    0 => '發佈',
256
+                    1 => '下架',
257
+                })
258
+                ->color(fn ($record) => match ($record->visible) {
259
+                    0 => 'warning',
260
+                    1 => 'gray',
261
+                })
262
+                ->icon(fn ($record) => match ($record->visible) {
263
+                    0 => 'heroicon-m-chevron-double-up',
264
+                    1 => 'heroicon-m-chevron-double-down',
265
+                })
266
+                ->action(function ($record): void {
267
+                    $record->visible = !$record->visible;
268
+                    $record->save();
269
+                })
270
+                ->outlined()
271
+                ->requiresConfirmation(),
272
+            ])
273
+            ->bulkActions([
274
+                Tables\Actions\BulkActionGroup::make([
275
+                    Tables\Actions\DeleteBulkAction::make(),
276
+                ]),
277
+            ])
278
+            ->defaultSort('order', 'desc')
279
+            ->defaultSort('created_at', 'desc');
280
+    }
281
+
282
+    public static function getRelations(): array
283
+    {
284
+        return [
285
+            //
286
+        ];
287
+    }
288
+
289
+    public static function getPages(): array
290
+    {
291
+        return [
292
+            'index' => Pages\ListNews::route('/'),
293
+            'create' => Pages\CreateNews::route('/create'),
294
+            'edit' => Pages\EditNews::route('/{record}/edit'),
295
+            'view' => Pages\ViewNews::route('/{record}/view'),
296
+        ];
297
+    }
298
+}

+ 22
- 0
app/Filament/Resources/NewsResource/Pages/CreateNews.php View File

@@ -0,0 +1,22 @@
1
+<?php
2
+
3
+namespace App\Filament\Resources\NewsResource\Pages;
4
+
5
+use App\Filament\Resources\NewsResource;
6
+use Filament\Actions;
7
+use Filament\Resources\Pages\CreateRecord;
8
+use Illuminate\Database\Eloquent\Model;
9
+
10
+class CreateNews extends CreateRecord
11
+{
12
+    protected static string $resource = NewsResource::class;
13
+    protected static bool $canCreateAnother = false;
14
+    protected function getRedirectUrl(): string
15
+    {
16
+        return $this->getResource()::getUrl('index');
17
+    }
18
+    protected function handleRecordCreation(array $data): Model
19
+    {
20
+        return static::getModel()::create($data);
21
+    }
22
+}

+ 17
- 0
app/Filament/Resources/NewsResource/Pages/EditNews.php View File

@@ -0,0 +1,17 @@
1
+<?php
2
+
3
+namespace App\Filament\Resources\NewsResource\Pages;
4
+
5
+use App\Filament\Resources\NewsResource;
6
+use Filament\Actions;
7
+use Filament\Resources\Pages\EditRecord;
8
+
9
+class EditNews extends EditRecord
10
+{
11
+    protected static string $resource = NewsResource::class;
12
+
13
+    protected function getRedirectUrl(): string
14
+    {
15
+        return $this->getResource()::getUrl('index');
16
+    }
17
+}

+ 52
- 0
app/Filament/Resources/NewsResource/Pages/HasNewsPreview.php View File

@@ -0,0 +1,52 @@
1
+<?php
2
+
3
+namespace App\Filament\Resources\NewsResource\Pages;
4
+
5
+use Filament\Actions\ActionGroup;
6
+use Filament\Forms\Components\Select;
7
+use Pboivin\FilamentPeek\Pages\Actions\PreviewAction;
8
+use Pboivin\FilamentPeek\Pages\Concerns\HasPreviewModal;
9
+
10
+trait HasNewsPreview
11
+{
12
+    use HasPreviewModal;
13
+
14
+    protected function getActions(): array
15
+    {
16
+        return [
17
+            ActionGroup::make([
18
+                PreviewAction::make('tw')
19
+                    ->label('中文(台灣)')
20
+                    ->previewModalData(fn() => ['locale' => 'zh_TW']),
21
+                PreviewAction::make('en')
22
+                    ->label('English')
23
+                    ->previewModalData(fn() => ['locale' => 'en']),
24
+                PreviewAction::make('jp')
25
+                    ->label('jp')
26
+                    ->previewModalData(fn() => ['locale' => 'jp']),
27
+            ])
28
+                ->label('預覽')
29
+                ->icon('heroicon-m-chevron-down')
30
+                ->color('primary')
31
+                ->button()
32
+        ];
33
+    }
34
+
35
+    protected function getPreviewModalView(): ?string
36
+    {
37
+        return 'previews.news.show';
38
+    }
39
+
40
+    protected function getPreviewModalDataRecordKey(): ?string
41
+    {
42
+        return 'news';
43
+    }
44
+
45
+    protected function mutatePreviewModalData(array $data): array
46
+    {
47
+        // relations
48
+        $data['paragraphs'] = $this->data['paragraphs'];
49
+        $data['extraImages'] = $this->data['extra_images'];
50
+        return $data;
51
+    }
52
+}

+ 19
- 0
app/Filament/Resources/NewsResource/Pages/ListNews.php View File

@@ -0,0 +1,19 @@
1
+<?php
2
+
3
+namespace App\Filament\Resources\NewsResource\Pages;
4
+
5
+use App\Filament\Resources\NewsResource;
6
+use Filament\Actions;
7
+use Filament\Resources\Pages\ListRecords;
8
+
9
+class ListNews extends ListRecords
10
+{
11
+    protected static string $resource = NewsResource::class;
12
+
13
+    protected function getHeaderActions(): array
14
+    {
15
+        return [
16
+            Actions\CreateAction::make()->label("新增"),
17
+        ];
18
+    }
19
+}

+ 11
- 0
app/Filament/Resources/NewsResource/Pages/ViewNews.php View File

@@ -0,0 +1,11 @@
1
+<?php
2
+
3
+namespace App\Filament\Resources\NewsResource\Pages;
4
+
5
+use App\Filament\Resources\NewsResource;
6
+use Filament\Resources\Pages\ViewRecord;
7
+
8
+class ViewNews extends ViewRecord
9
+{
10
+    protected static string $resource = NewsResource::class;
11
+}

+ 84
- 0
app/Filament/Resources/UserResource.php View File

@@ -0,0 +1,84 @@
1
+<?php
2
+
3
+namespace App\Filament\Resources;
4
+
5
+use App\Filament\Resources\UserResource\Pages;
6
+use App\Models\User;
7
+use Filament\Forms\Components\Select;
8
+use Filament\Forms\Components\TextInput;
9
+use Filament\Forms\Form;
10
+use Filament\Resources\Resource;
11
+use Filament\Tables;
12
+use Filament\Tables\Columns\TextColumn;
13
+use Filament\Tables\Table;
14
+use Illuminate\Database\Eloquent\Builder;
15
+use Illuminate\Support\Facades\Hash;
16
+
17
+class UserResource extends Resource
18
+{
19
+    protected static ?string $model = User::class;
20
+    protected static ?string $modelLabel = "使用者管理";
21
+    protected static ?string $navigationIcon = 'heroicon-o-user-circle';
22
+    protected static ?string $navigationLabel = "使用者管理";
23
+
24
+    public static function form(Form $form): Form
25
+    {
26
+        return $form
27
+        ->schema([
28
+            //
29
+            TextInput::make("name")->required()->autocomplete(false),
30
+            TextInput::make("email")->required()->email()->unique(ignoreRecord: true)->autocomplete(false),
31
+            TextInput::make("password")
32
+            ->password()
33
+            ->dehydrateStateUsing(fn (string $state): string => Hash::make($state))
34
+            ->dehydrated(fn (?string $state): bool => filled($state))
35
+            ->required(fn (string $operation): bool => $operation === 'create'),
36
+            Select::make('roles')->label("權限選擇")
37
+                ->relationship(
38
+                name: 'roles',
39
+                titleAttribute: 'name',
40
+                modifyQueryUsing: fn (Builder $query) => $query->whereNotIn("id", [1,2]),)
41
+                ->multiple()
42
+                ->preload()
43
+        ]);
44
+    }
45
+
46
+    public static function table(Table $table): Table
47
+    {
48
+        return $table
49
+            ->columns([
50
+                TextColumn::make("name")->label("使用者名稱"),
51
+                TextColumn::make("email")->label("E-Mail"),
52
+                TextColumn::make('created_at')->label("建立時間")->date()
53
+            ])->query(function (User $query) {
54
+                return $query->where('email', '!=', env("SUPER_ADMIN_ACCOUNT", "admin@ogilvy.com"));
55
+            })
56
+            ->filters([
57
+                //
58
+            ])
59
+            ->actions([
60
+                Tables\Actions\EditAction::make(),
61
+            ])
62
+            ->bulkActions([
63
+                Tables\Actions\BulkActionGroup::make([
64
+                    Tables\Actions\DeleteBulkAction::make(),
65
+                ]),
66
+            ]);
67
+    }
68
+
69
+    public static function getRelations(): array
70
+    {
71
+        return [
72
+            //
73
+        ];
74
+    }
75
+
76
+    public static function getPages(): array
77
+    {
78
+        return [
79
+            'index' => Pages\ListUsers::route('/'),
80
+            'create' => Pages\CreateUser::route('/create'),
81
+            'edit' => Pages\EditUser::route('/{record}/edit'),
82
+        ];
83
+    }
84
+}

+ 18
- 0
app/Filament/Resources/UserResource/Pages/CreateUser.php View File

@@ -0,0 +1,18 @@
1
+<?php
2
+
3
+namespace App\Filament\Resources\UserResource\Pages;
4
+
5
+use App\Filament\Resources\UserResource;
6
+use Filament\Actions;
7
+use Filament\Resources\Pages\CreateRecord;
8
+
9
+class CreateUser extends CreateRecord
10
+{
11
+    protected static string $resource = UserResource::class;
12
+    protected static bool $canCreateAnother = false;
13
+
14
+    protected function getRedirectUrl(): string
15
+    {
16
+        return $this->getResource()::getUrl('index');
17
+    }
18
+}

+ 19
- 0
app/Filament/Resources/UserResource/Pages/EditUser.php View File

@@ -0,0 +1,19 @@
1
+<?php
2
+
3
+namespace App\Filament\Resources\UserResource\Pages;
4
+
5
+use App\Filament\Resources\UserResource;
6
+use Filament\Actions;
7
+use Filament\Resources\Pages\EditRecord;
8
+
9
+class EditUser extends EditRecord
10
+{
11
+    protected static string $resource = UserResource::class;
12
+
13
+    protected function getHeaderActions(): array
14
+    {
15
+        return [
16
+            Actions\DeleteAction::make(),
17
+        ];
18
+    }
19
+}

+ 19
- 0
app/Filament/Resources/UserResource/Pages/ListUsers.php View File

@@ -0,0 +1,19 @@
1
+<?php
2
+
3
+namespace App\Filament\Resources\UserResource\Pages;
4
+
5
+use App\Filament\Resources\UserResource;
6
+use Filament\Actions;
7
+use Filament\Resources\Pages\ListRecords;
8
+
9
+class ListUsers extends ListRecords
10
+{
11
+    protected static string $resource = UserResource::class;
12
+
13
+    protected function getHeaderActions(): array
14
+    {
15
+        return [
16
+            Actions\CreateAction::make()->label("新增"),
17
+        ];
18
+    }
19
+}

+ 215
- 0
app/Http/Controllers/Api/NewsController.php View File

@@ -0,0 +1,215 @@
1
+<?php
2
+
3
+namespace App\Http\Controllers\Api;
4
+
5
+use App\Http\Controllers\Controller;
6
+use App\Models\News;
7
+use App\Models\NewsCategory;
8
+use App\Supports\Response;
9
+use Carbon\Carbon;
10
+use Illuminate\Http\Request;
11
+use Illuminate\Support\Facades\DB;
12
+use Illuminate\Support\Facades\Log;
13
+use App\Http\Helper\Helper;
14
+
15
+/**
16
+ * @group Lottery Prize
17
+ */
18
+class NewsController extends Controller
19
+{
20
+    public function __construct(
21
+    )
22
+    {
23
+    }
24
+
25
+    public function list(Request $request, $locate = 'tw')
26
+    {
27
+        $categoryId = $request->input("categoryId", "");
28
+        $page = $request->input("page", 1);
29
+        $per_page = $request->input("per_page", 15);
30
+        $locate = $locate == "tw" ? "zh_TW" : $locate;
31
+        $result = [];
32
+
33
+        //年份清單
34
+        $yearList = News::select(DB::raw("DATE_FORMAT(post_date, '%Y') as years"))->distinct()->orderBy("years", "desc")->pluck("years");
35
+        $result["yearList"] = $yearList;
36
+
37
+        //文章列表
38
+        $news = News::where("visible", 1);
39
+        if($categoryId){
40
+            $news->where("news_category_id", $categoryId);
41
+        }
42
+        $news = $news->orderByDesc("order")->orderByDesc("post_date");
43
+        $result["pagination"] = [
44
+            "page" => $page,
45
+            "per_page" => $per_page
46
+        ];
47
+        if($per_page == -1){
48
+            $news = $news->get();
49
+            $result["pagination"]["total"] = $news->count();
50
+            $result["pagination"]["total_page"] =1;
51
+        }else{
52
+            $news = $news->paginate($per_page, ['*'], 'page', $page);
53
+            $result["pagination"]["total"] = $news->total();
54
+            $result["pagination"]["total_page"] =$news->lastPage();
55
+
56
+        }
57
+        foreach($news as $item){
58
+            $result["list"][] = [
59
+                "id" => $item->id,
60
+                "categoryId" => $item->newsCategory->id,
61
+                "category" => $item->newsCategory->getTranslation("name", $locate),
62
+                "postDate" => Carbon::parse($item->post_date)->format("Y.m.d"),
63
+                "title" => $item->getTranslation("title", $locate),
64
+                "imgPC" => $item->news_img_pc_url,
65
+                "imgMobile" => $item->news_img_mobile_url,
66
+                "written" => $item->getTranslation("written_by", $locate),
67
+                "description" => $item->getTranslation("description", $locate),
68
+            ];
69
+        }
70
+        return response()->json(["data" => $result], 200);
71
+    }
72
+
73
+    public function getTop(Request $request, $locate = 'tw')
74
+    {
75
+        $categoryId = $request->input("categoryId", "");
76
+        $locate = $locate == "tw" ? "zh_TW" : $locate;
77
+        $result = [];
78
+
79
+        //年份清單
80
+        $yearList = News::select(DB::raw("DATE_FORMAT(post_date, '%Y') as years"))->distinct()->orderBy("years", "desc")->pluck("years");
81
+
82
+        //文章列表
83
+        $news = News::where("visible", 1);
84
+        if($categoryId){
85
+            $news->where("news_category_id", $categoryId);
86
+        }
87
+        $news = $news->where('on_top', 1)->first();
88
+        $result["data"] = [
89
+            "meta" => [
90
+                "meta_title" => $news->getTranslation("meta_title", $locate),
91
+                "meta_keyword" => $news->getTranslation("meta_keyword", $locate),
92
+                "meta_description" => $news->getTranslation("meta_description", $locate),
93
+                "imgPC" => $news->news_meta_img,
94
+            ],
95
+            "yearList" =>[
96
+                $yearList
97
+            ],
98
+            "top" => [
99
+                "id" => $news->id,
100
+                "categoryId" => $news->newsCategory->id,
101
+                "category" => $news->newsCategory->getTranslation("name", $locate),
102
+                "postDate" => Carbon::parse($news->post_date)->format("Y/m/d"),
103
+                "title" => $news->getTranslation("title", $locate),
104
+                "imgPC" => $news->news_img_pc_url,
105
+                "imgMobile" => $news->news_img_mobile_url,
106
+                "written" => $news->getTranslation("written_by", $locate),
107
+                "description" => $news->getTranslation("description", $locate),
108
+            ]
109
+        ];
110
+        return response()->json($result, 200);
111
+    }
112
+
113
+    public function detail($locale = 'tw', $id){
114
+        $locate = $locale == "tw" ? "zh_TW" : $locale;
115
+        $news = News::find($id);
116
+        $otherNewsList = [];
117
+
118
+        //取得前一篇
119
+        $previous = News::where(function ($query) use ($news) {
120
+            $query->where('order', $news->order)
121
+            ->where('post_date', $news->post_date)
122
+            ->where('id', '>', $news->id);
123
+        })
124
+        ->orWhere(function ($query) use ($news) {
125
+            $query->where('order', $news->order)
126
+            ->where('post_date', '>', $news->post_date);
127
+        })
128
+        ->orWhere(function ($query) use ($news) {
129
+            $query->where('order', '>', $news->order);
130
+        })
131
+        ->orderBy('order', 'asc')
132
+        ->orderBy('post_date', 'asc')
133
+        ->orderBy('id', 'asc')
134
+        ->first();
135
+        //取得下一篇
136
+        $next = News::where(function ($query) use ($news) {
137
+            $query->where('order', $news->order)
138
+            ->where('post_date', $news->post_date)
139
+            ->where('id', '<', $news->id);
140
+        })
141
+        ->orWhere(function ($query) use ($news) {
142
+            $query->where('order', $news->order)
143
+            ->where('post_date', '<', $news->post_date);
144
+        })
145
+        ->orWhere(function ($query) use ($news) {
146
+            $query->where('order', '<', $news->order);
147
+        })
148
+        ->orderBy('order', 'desc')
149
+        ->orderBy('post_date', 'desc')
150
+        ->orderBy('id', 'desc')
151
+        ->first();
152
+        if($previous)$otherNewsList["previous"] = [
153
+            "id" => $previous->id,
154
+            "categoryId" => $previous->newsCategory->id,
155
+            "category" => $previous->newsCategory->getTranslation("name", $locate),
156
+            "postDate" => Carbon::parse($previous->post_date)->format("Y/m/d"),
157
+            "title" => $previous->getTranslation("title", $locate),
158
+            "imgPC" => $previous->news_img_pc_url,
159
+            "imgMobile" => $previous->news_img_mobile_url,
160
+            "written" => $previous->getTranslation("written_by", $locate),
161
+            "description" => $previous->getTranslation("description", $locate),
162
+        ];
163
+        if($next)$otherNewsList["next"] = [
164
+            "id" => $next->id,
165
+            "categoryId" => $next->newsCategory->id,
166
+            "category" => $next->newsCategory->getTranslation("name", $locate),
167
+            "postDate" => Carbon::parse($previous->post_date)->format("Y/m/d"),
168
+            "title" => $next->getTranslation("title", $locate),
169
+            "imgPC" => $next->news_img_pc_url,
170
+            "imgMobile" => $next->news_img_mobile_url,
171
+            "written" => $next->getTranslation("written_by", $locate),
172
+            "description" => $next->getTranslation("description", $locate),
173
+        ];
174
+
175
+
176
+        $paragraphs = [];
177
+        foreach($news->paragraphs as $paragraph){
178
+            $paragraphs[] = [
179
+                "type" => $paragraph->contentType(),
180
+                "imgUrl" => $paragraph->paragraph_img,
181
+                "imgOutLink" => $paragraph->image_link,
182
+                "alt" => $paragraph->getTranslation("image_alt", $locate),
183
+                "content" => nl2br($paragraph->getTranslation("text_content", $locate)),
184
+                "videoImg" => $paragraph->paragraph_video_img,
185
+                "videoType" => $paragraph->paragraph_video_type,
186
+                "link" => $paragraph->paragraph_video_url,
187
+            ];
188
+        }
189
+        $extraPhotos = [];
190
+        foreach($news->photos as $photo){
191
+            $extraPhotos[] = [
192
+                "imgUrl" => $photo->news_photo_img,
193
+                "imgOutLink" => $photo->image_link,
194
+            ];
195
+        }
196
+        $result = [
197
+            "categoryId" => $news->newsCategory->id,
198
+            "category" => $news->newsCategory->getTranslation("name", $locate),
199
+            "postDate" => Carbon::parse($news->post_date)->format("Y/m/d"),
200
+            "title" => $news->getTranslation("title", $locate),
201
+            "description" => $news->getTranslation("description", $locate),
202
+            "written" => $news->getTranslation("written_by", $locate),
203
+            "imgPC" => $news->news_img_pc_url,
204
+            "imgMobile" => $news->news_img_mobile_url,
205
+            "metaTitle" => $news->getTranslation("meta_title", $locate),
206
+            "metaKeyword" => $news->getTranslation("meta_keyword", $locate),
207
+            "metaDesc" => $news->getTranslation("meta_description", $locate),
208
+            "metaImg" => $news->meta_img,
209
+            "paragraphs" => $paragraphs,
210
+            "photos" => $extraPhotos,
211
+            "otherNews" => $otherNewsList
212
+        ];
213
+        return response()->json(["data" => $result], 200);
214
+    }
215
+}

+ 12
- 0
app/Http/Controllers/Controller.php View File

@@ -0,0 +1,12 @@
1
+<?php
2
+
3
+namespace App\Http\Controllers;
4
+
5
+use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
6
+use Illuminate\Foundation\Validation\ValidatesRequests;
7
+use Illuminate\Routing\Controller as BaseController;
8
+
9
+class Controller extends BaseController
10
+{
11
+    use AuthorizesRequests, ValidatesRequests;
12
+}

+ 39
- 0
app/Http/Helper/Helper.php View File

@@ -0,0 +1,39 @@
1
+<?php
2
+
3
+namespace App\Http\Helper;
4
+
5
+class Helper {
6
+
7
+    /**
8
+     * Summary of findNextStepsIDs
9
+     * @param array $arr 目標陣列
10
+     * @param mixed $targetID 指定的value
11
+     * @param int $count 尋找組數
12
+     * @param string $direction 尋找方向 up down
13
+     * @return array
14
+     */
15
+    function findArrayTargetIndex(array $arr, $target, int $count, string $direction = "down") {
16
+        // 找到指定value 的Index
17
+        $targetIndex = array_search($target, $arr);
18
+
19
+        // 如果指定的value不存在陣列中,則回傳空陣列
20
+        if ($targetIndex === false) {
21
+            return [];
22
+        }
23
+
24
+        $result = [];
25
+        //計算陣列的數量
26
+        $arrayLength = count($arr);
27
+
28
+        // 根据方向确定增量
29
+        $increment = $direction === 'down' ? 1 : -1;
30
+
31
+        // 从目标索引开始找指定数量的元素
32
+        for ($i = 1; $i <= $count; $i++) {
33
+            $currentIndex = ($targetIndex + $increment * $i + $arrayLength) % $arrayLength;
34
+            $result[] = $arr[$currentIndex];
35
+        }
36
+
37
+        return $result;
38
+    }
39
+}

+ 69
- 0
app/Http/Kernel.php View File

@@ -0,0 +1,69 @@
1
+<?php
2
+
3
+namespace App\Http;
4
+
5
+use Illuminate\Foundation\Http\Kernel as HttpKernel;
6
+
7
+class Kernel extends HttpKernel
8
+{
9
+    /**
10
+     * The application's global HTTP middleware stack.
11
+     *
12
+     * These middleware are run during every request to your application.
13
+     *
14
+     * @var array<int, class-string|string>
15
+     */
16
+    protected $middleware = [
17
+        // \App\Http\Middleware\TrustHosts::class,
18
+        \App\Http\Middleware\TrustProxies::class,
19
+        \Illuminate\Http\Middleware\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<string, array<int, class-string|string>>
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\View\Middleware\ShareErrorsFromSession::class,
37
+            \App\Http\Middleware\VerifyCsrfToken::class,
38
+            \Illuminate\Routing\Middleware\SubstituteBindings::class,
39
+        ],
40
+
41
+        'api' => [
42
+            // \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
43
+            \Illuminate\Routing\Middleware\ThrottleRequests::class.':api',
44
+            \Illuminate\Routing\Middleware\SubstituteBindings::class,
45
+        ],
46
+    ];
47
+
48
+    /**
49
+     * The application's middleware aliases.
50
+     *
51
+     * Aliases may be used instead of class names to conveniently assign middleware to routes and groups.
52
+     *
53
+     * @var array<string, class-string|string>
54
+     */
55
+    protected $middlewareAliases = [
56
+        'auth' => \App\Http\Middleware\Authenticate::class,
57
+        'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
58
+        'auth.session' => \Illuminate\Session\Middleware\AuthenticateSession::class,
59
+        'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
60
+        'can' => \Illuminate\Auth\Middleware\Authorize::class,
61
+        'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
62
+        'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class,
63
+        'precognitive' => \Illuminate\Foundation\Http\Middleware\HandlePrecognitiveRequests::class,
64
+        'signed' => \App\Http\Middleware\ValidateSignature::class,
65
+        'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
66
+        'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
67
+        'accessIp' => \App\Http\Middleware\AccessIpMiddleware::class,
68
+    ];
69
+}

+ 31
- 0
app/Http/Middleware/AccessIpMiddleware.php View File

@@ -0,0 +1,31 @@
1
+<?php
2
+
3
+namespace App\Http\Middleware;
4
+
5
+use Closure;
6
+use Illuminate\Http\Request;
7
+use Symfony\Component\HttpFoundation\Response;
8
+
9
+class AccessIpMiddleware
10
+{
11
+    /**
12
+     * Handle an incoming request.
13
+     *
14
+     * @param  \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response)  $next
15
+     */
16
+    public function handle(Request $request, Closure $next): Response
17
+    {
18
+        \Log::info($request->ip());
19
+        $accessIps = config("ipWhiteList.access");
20
+        if($accessIps != "*") {
21
+            $accessIps = explode("|", $accessIps);
22
+            if (!in_array($request->ip(), $accessIps)) {
23
+                return response()->json([
24
+                  'message' => "You don't have permission to access this website."
25
+                ], 401);
26
+            }
27
+        }
28
+
29
+        return $next($request);
30
+    }
31
+}

+ 17
- 0
app/Http/Middleware/Authenticate.php View File

@@ -0,0 +1,17 @@
1
+<?php
2
+
3
+namespace App\Http\Middleware;
4
+
5
+use Illuminate\Auth\Middleware\Authenticate as Middleware;
6
+use Illuminate\Http\Request;
7
+
8
+class Authenticate extends Middleware
9
+{
10
+    /**
11
+     * Get the path the user should be redirected to when they are not authenticated.
12
+     */
13
+    protected function redirectTo(Request $request): ?string
14
+    {
15
+        return $request->expectsJson() ? null : route('login');
16
+    }
17
+}

+ 17
- 0
app/Http/Middleware/EncryptCookies.php View File

@@ -0,0 +1,17 @@
1
+<?php
2
+
3
+namespace App\Http\Middleware;
4
+
5
+use Illuminate\Cookie\Middleware\EncryptCookies as Middleware;
6
+
7
+class EncryptCookies extends Middleware
8
+{
9
+    /**
10
+     * The names of the cookies that should not be encrypted.
11
+     *
12
+     * @var array<int, string>
13
+     */
14
+    protected $except = [
15
+        //
16
+    ];
17
+}

+ 17
- 0
app/Http/Middleware/PreventRequestsDuringMaintenance.php View File

@@ -0,0 +1,17 @@
1
+<?php
2
+
3
+namespace App\Http\Middleware;
4
+
5
+use Illuminate\Foundation\Http\Middleware\PreventRequestsDuringMaintenance as Middleware;
6
+
7
+class PreventRequestsDuringMaintenance extends Middleware
8
+{
9
+    /**
10
+     * The URIs that should be reachable while maintenance mode is enabled.
11
+     *
12
+     * @var array<int, string>
13
+     */
14
+    protected $except = [
15
+        //
16
+    ];
17
+}

+ 30
- 0
app/Http/Middleware/RedirectIfAuthenticated.php View File

@@ -0,0 +1,30 @@
1
+<?php
2
+
3
+namespace App\Http\Middleware;
4
+
5
+use App\Providers\RouteServiceProvider;
6
+use Closure;
7
+use Illuminate\Http\Request;
8
+use Illuminate\Support\Facades\Auth;
9
+use Symfony\Component\HttpFoundation\Response;
10
+
11
+class RedirectIfAuthenticated
12
+{
13
+    /**
14
+     * Handle an incoming request.
15
+     *
16
+     * @param  \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response)  $next
17
+     */
18
+    public function handle(Request $request, Closure $next, string ...$guards): Response
19
+    {
20
+        $guards = empty($guards) ? [null] : $guards;
21
+
22
+        foreach ($guards as $guard) {
23
+            if (Auth::guard($guard)->check()) {
24
+                return redirect(RouteServiceProvider::HOME);
25
+            }
26
+        }
27
+
28
+        return $next($request);
29
+    }
30
+}

+ 19
- 0
app/Http/Middleware/TrimStrings.php View File

@@ -0,0 +1,19 @@
1
+<?php
2
+
3
+namespace App\Http\Middleware;
4
+
5
+use Illuminate\Foundation\Http\Middleware\TrimStrings as Middleware;
6
+
7
+class TrimStrings extends Middleware
8
+{
9
+    /**
10
+     * The names of the attributes that should not be trimmed.
11
+     *
12
+     * @var array<int, string>
13
+     */
14
+    protected $except = [
15
+        'current_password',
16
+        'password',
17
+        'password_confirmation',
18
+    ];
19
+}

+ 20
- 0
app/Http/Middleware/TrustHosts.php View File

@@ -0,0 +1,20 @@
1
+<?php
2
+
3
+namespace App\Http\Middleware;
4
+
5
+use Illuminate\Http\Middleware\TrustHosts as Middleware;
6
+
7
+class TrustHosts extends Middleware
8
+{
9
+    /**
10
+     * Get the host patterns that should be trusted.
11
+     *
12
+     * @return array<int, string|null>
13
+     */
14
+    public function hosts(): array
15
+    {
16
+        return [
17
+            $this->allSubdomainsOfApplicationUrl(),
18
+        ];
19
+    }
20
+}

+ 28
- 0
app/Http/Middleware/TrustProxies.php View File

@@ -0,0 +1,28 @@
1
+<?php
2
+
3
+namespace App\Http\Middleware;
4
+
5
+use Illuminate\Http\Middleware\TrustProxies as Middleware;
6
+use Illuminate\Http\Request;
7
+
8
+class TrustProxies extends Middleware
9
+{
10
+    /**
11
+     * The trusted proxies for this application.
12
+     *
13
+     * @var array<int, string>|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
+}

+ 22
- 0
app/Http/Middleware/ValidateSignature.php View File

@@ -0,0 +1,22 @@
1
+<?php
2
+
3
+namespace App\Http\Middleware;
4
+
5
+use Illuminate\Routing\Middleware\ValidateSignature as Middleware;
6
+
7
+class ValidateSignature extends Middleware
8
+{
9
+    /**
10
+     * The names of the query string parameters that should be ignored.
11
+     *
12
+     * @var array<int, string>
13
+     */
14
+    protected $except = [
15
+        // 'fbclid',
16
+        // 'utm_campaign',
17
+        // 'utm_content',
18
+        // 'utm_medium',
19
+        // 'utm_source',
20
+        // 'utm_term',
21
+    ];
22
+}

+ 17
- 0
app/Http/Middleware/VerifyCsrfToken.php View File

@@ -0,0 +1,17 @@
1
+<?php
2
+
3
+namespace App\Http\Middleware;
4
+
5
+use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as Middleware;
6
+
7
+class VerifyCsrfToken extends Middleware
8
+{
9
+    /**
10
+     * The URIs that should be excluded from CSRF verification.
11
+     *
12
+     * @var array<int, string>
13
+     */
14
+    protected $except = [
15
+        //
16
+    ];
17
+}

+ 58
- 0
app/Http/Requests/ContactRequest.php View File

@@ -0,0 +1,58 @@
1
+<?php
2
+
3
+namespace app\Http\Requests;
4
+
5
+use Illuminate\Contracts\Validation\Validator;
6
+use Illuminate\Foundation\Http\FormRequest;
7
+use Illuminate\Http\Exceptions\HttpResponseException;
8
+use Illuminate\Validation\Rule;
9
+
10
+class ContactRequest extends FormRequest
11
+{
12
+    public function authorize()
13
+    {
14
+        return true; // 確保這裡返回 true
15
+    }
16
+
17
+    /**
18
+     * Get the validation rules that apply to the request.
19
+     */
20
+    public function rules(): array
21
+    {
22
+        return [
23
+            'source' => ['required','regex:/^(official|commercial|mall)$/'],
24
+            'name' => 'required',
25
+            'company' => 'required',
26
+            'phone' => 'required',
27
+            'email' => 'required|email',
28
+            'description' => 'required|max:1000',
29
+        ];
30
+    }
31
+
32
+    /**
33
+     * Get custom messages for validator errors.
34
+     */
35
+    public function messages(): array
36
+    {
37
+        return [
38
+            'email.required' => 'E-Mail必填',
39
+            'name.required' => '名稱必填',
40
+            'company.required' => '公司必填',
41
+            'phone.required' => '電話必填',
42
+            'description.required' => '詢問內容必填',
43
+            'email.email' => 'E-Mail格式錯誤',
44
+            'source.regex' => '來源內容不符',
45
+        ];
46
+    }
47
+
48
+    /**
49
+     * 處理驗證失敗的情況
50
+     */
51
+    protected function failedValidation(Validator $validator)
52
+    {
53
+        throw new HttpResponseException(response()->json([
54
+            'result' => "failed",
55
+            'message' => $validator->errors(),
56
+        ], 400));
57
+    }
58
+}

+ 48
- 0
app/Http/Requests/RegistEPaperRequest.php View File

@@ -0,0 +1,48 @@
1
+<?php
2
+
3
+namespace app\Http\Requests;
4
+
5
+use Illuminate\Contracts\Validation\Validator;
6
+use Illuminate\Foundation\Http\FormRequest;
7
+use Illuminate\Http\Exceptions\HttpResponseException;
8
+use Illuminate\Validation\Rule;
9
+
10
+class RegistEPaperRequest extends FormRequest
11
+{
12
+    public function authorize()
13
+    {
14
+        return true; // 確保這裡返回 true
15
+    }
16
+
17
+    /**
18
+     * Get the validation rules that apply to the request.
19
+     */
20
+    public function rules(): array
21
+    {
22
+        return [
23
+            'email' => 'required|email'
24
+        ];
25
+    }
26
+
27
+    /**
28
+     * Get custom messages for validator errors.
29
+     */
30
+    public function messages(): array
31
+    {
32
+        return [
33
+            'email.required' => 'E-Mail必填',
34
+            'email.email' => 'E-Mail格式錯誤',
35
+        ];
36
+    }
37
+
38
+    /**
39
+     * 處理驗證失敗的情況
40
+     */
41
+    protected function failedValidation(Validator $validator)
42
+    {
43
+        throw new HttpResponseException(response()->json([
44
+            'result' => "failed",
45
+            'message' => $validator->errors(),
46
+        ], 422));
47
+    }
48
+}

+ 77
- 0
app/Models/News.php View File

@@ -0,0 +1,77 @@
1
+<?php
2
+
3
+namespace App\Models;
4
+
5
+use Illuminate\Database\Eloquent\Casts\Attribute;
6
+use Illuminate\Database\Eloquent\Factories\HasFactory;
7
+use Illuminate\Database\Eloquent\Model;
8
+use Illuminate\Database\Eloquent\SoftDeletes;
9
+use Illuminate\Support\Facades\Storage;
10
+use RalphJSmit\Laravel\SEO\Support\HasSEO;
11
+use App\Models\NewsCategory;
12
+use Spatie\Translatable\HasTranslations;
13
+
14
+class News extends Model
15
+{
16
+    use HasFactory, SoftDeletes, HasTranslations;
17
+
18
+    protected $casts = [
19
+        'on_top' => 'boolean',
20
+        "list_audit_state" => "string",
21
+    ];
22
+
23
+    protected $guarded = ['id'];
24
+    protected $appends = ["news_img_pc_url", "news_img_mobile_url", "news_meta_img"];
25
+
26
+    public $translatable = ['title', 'written_by', 'description', 'meta_title', 'meta_description', 'meta_keyword'];
27
+
28
+    public function newsCategory(){
29
+        return $this->belongsTo(NewsCategory::class);
30
+    }
31
+
32
+    public function paragraphs(){
33
+        return $this->hasMany(NewsParagraph::class)->orderBy('order');
34
+    }
35
+
36
+    public function photos(){
37
+        return $this->hasMany(NewsPhoto::class)->orderBy("order");
38
+    }
39
+    protected function newsImgPcUrl(): Attribute
40
+    {
41
+        return Attribute::make(
42
+            get: fn ($value) => is_null($this->news_img_pc) ? null :Storage::disk('public')->url($this->news_img_pc),
43
+        );
44
+    }
45
+    protected function newsImgMobileUrl(): Attribute
46
+    {
47
+        return Attribute::make(
48
+            get: fn ($value) => is_null($this->news_img_mobile) ? null :Storage::disk('public')->url($this->news_img_mobile),
49
+        );
50
+    }
51
+    protected function newsMetaImg(): Attribute
52
+    {
53
+        return Attribute::make(
54
+            get: fn ($value) => is_null($this->meta_img) ? null :Storage::disk('public')->url($this->meta_img),
55
+        );
56
+    }
57
+
58
+    protected function listAuditState(): Attribute
59
+    {
60
+        return Attribute::make(
61
+            get: fn ($value) => $this->attributes["visible"] == 1 ? "已發佈" : "暫存",
62
+        );
63
+    }
64
+    protected static function booted()
65
+    {
66
+        static::saving(function ($news) {
67
+            // 如果正在將此記錄設為置頂
68
+            if ($news->isDirty('on_top') && $news->on_top == 1) {
69
+                // 將其他所有記錄的置頂狀態取消
70
+                static::where('id', '!=', $news->id)
71
+                    ->where('news_category_id', $news->news_category_id)
72
+                    ->where('on_top', 1)
73
+                    ->update(['on_top' => 0]);
74
+            }
75
+        });
76
+    }
77
+}

+ 22
- 0
app/Models/NewsCategory.php View File

@@ -0,0 +1,22 @@
1
+<?php
2
+
3
+namespace App\Models;
4
+
5
+use Illuminate\Database\Eloquent\Factories\HasFactory;
6
+use Illuminate\Database\Eloquent\Model;
7
+use Illuminate\Database\Eloquent\SoftDeletes;
8
+use Spatie\Translatable\HasTranslations;
9
+use App\Models\News;
10
+
11
+class NewsCategory extends Model
12
+{
13
+    use HasFactory, SoftDeletes, HasTranslations;
14
+
15
+    protected $guarded = ['id'];
16
+
17
+    public $translatable = ['name'];
18
+
19
+    public function news(){
20
+        return $this->hasMany(News::class);
21
+    }
22
+}

+ 66
- 0
app/Models/NewsParagraph.php View File

@@ -0,0 +1,66 @@
1
+<?php
2
+
3
+namespace App\Models;
4
+
5
+use Illuminate\Database\Eloquent\Casts\Attribute;
6
+use Illuminate\Database\Eloquent\Factories\HasFactory;
7
+use Illuminate\Database\Eloquent\Model;
8
+use Illuminate\Database\Eloquent\SoftDeletes;
9
+use Illuminate\Support\Facades\Storage;
10
+use Spatie\Translatable\HasTranslations;
11
+use App\Models\News;
12
+
13
+class NewsParagraph extends Model
14
+{
15
+    use HasFactory, HasTranslations;
16
+    CONST IMAGE = 1;
17
+    CONST TEXT = 2;
18
+    CONST VIDEO = 3;
19
+
20
+    protected $guarded = ['id'];
21
+    public $timestamps = false;
22
+    protected $appends = ['paragraph_img', 'paragraph_video_img', 'paragraph_video_type', 'paragraph_video_url'];
23
+    public $translatable = ['text_content', 'image_alt'];
24
+
25
+    public function news(){
26
+        return $this->belongsTo(News::class);
27
+    }
28
+
29
+    public function contentType()
30
+    {
31
+        switch ($this->paragraph_type){
32
+            case 1:
33
+                return "image";
34
+            case 2:
35
+                return "text";
36
+            case 3:
37
+                return "video";
38
+            default:
39
+                return "";
40
+        }
41
+    }
42
+    protected function paragraphImg(): Attribute
43
+    {
44
+        return Attribute::make(
45
+            get: fn ($value) => is_null($this->image_url) ? null :Storage::disk('public')->url($this->image_url),
46
+        );
47
+    }
48
+    protected function paragraphVideoType(): Attribute
49
+    {
50
+        return Attribute::make(
51
+            get: fn ($value) => ($this->paragraph_type == 3) ? ($this->attributes["video_type"] == 1 ? "url" : "upload") : null,
52
+        );
53
+    }
54
+    protected function paragraphVideoImg(): Attribute
55
+    {
56
+        return Attribute::make(
57
+            get: fn ($value) => is_null($this->video_img) ? null :Storage::disk('public')->url($this->video_img),
58
+        );
59
+    }
60
+    protected function paragraphVideoUrl(): Attribute
61
+    {
62
+        return Attribute::make(
63
+            get: fn ($value) => ($this->attributes["video_type"] == 2) ? Storage::disk('public')->url($this->attributes["video_url"]) : $this->attributes["link"],
64
+        );
65
+    }
66
+}

+ 31
- 0
app/Models/NewsPhoto.php View File

@@ -0,0 +1,31 @@
1
+<?php
2
+
3
+namespace App\Models;
4
+
5
+use Illuminate\Database\Eloquent\Casts\Attribute;
6
+use Illuminate\Database\Eloquent\Factories\HasFactory;
7
+use Illuminate\Database\Eloquent\Model;
8
+use Illuminate\Database\Eloquent\SoftDeletes;
9
+use Illuminate\Support\Facades\Storage;
10
+use App\Models\News;
11
+use Spatie\Translatable\HasTranslations;
12
+
13
+class NewsPhoto extends Model
14
+{
15
+    use HasFactory, HasTranslations;
16
+
17
+    protected $guarded = ['id'];
18
+    public $timestamps = false;
19
+    protected $appends = ['news_photo_img'];
20
+    public $translatable = ['image_alt'];
21
+
22
+    public function news(){
23
+        return $this->belongsTo(News::class);
24
+    }
25
+    protected function newsPhotoImg(): Attribute
26
+    {
27
+        return Attribute::make(
28
+            get: fn ($value) => is_null($this->image_url) ? null :Storage::disk('public')->url($this->image_url),
29
+        );
30
+    }
31
+}

+ 57
- 0
app/Models/User.php View File

@@ -0,0 +1,57 @@
1
+<?php
2
+
3
+namespace App\Models;
4
+
5
+// use Illuminate\Contracts\Auth\MustVerifyEmail;
6
+use Filament\Models\Contracts\FilamentUser;
7
+use Filament\Panel;
8
+use Illuminate\Database\Eloquent\Factories\HasFactory;
9
+use Illuminate\Foundation\Auth\User as Authenticatable;
10
+use Illuminate\Notifications\Notifiable;
11
+use Spatie\Permission\Traits\HasRoles;
12
+
13
+class User extends Authenticatable implements FilamentUser
14
+{
15
+    /** @use HasFactory<\Database\Factories\UserFactory> */
16
+    use HasFactory, Notifiable, HasRoles;
17
+
18
+    /**
19
+     * The attributes that are mass assignable.
20
+     *
21
+     * @var list<string>
22
+     */
23
+    protected $fillable = [
24
+        'name',
25
+        'email',
26
+        'password',
27
+    ];
28
+
29
+    /**
30
+     * The attributes that should be hidden for serialization.
31
+     *
32
+     * @var list<string>
33
+     */
34
+    protected $hidden = [
35
+        'password',
36
+        'remember_token',
37
+    ];
38
+
39
+    /**
40
+     * Get the attributes that should be cast.
41
+     *
42
+     * @return array<string, string>
43
+     */
44
+    protected function casts(): array
45
+    {
46
+        return [
47
+            'email_verified_at' => 'datetime',
48
+            'password' => 'hashed',
49
+        ];
50
+    }
51
+
52
+    public function canAccessPanel(Panel $panel): bool
53
+    {
54
+        // return str_ends_with($this->email, '@yourdomain.com') && $this->hasVerifiedEmail();
55
+        return true;
56
+    }
57
+}

+ 108
- 0
app/Policies/RolePolicy.php View File

@@ -0,0 +1,108 @@
1
+<?php
2
+
3
+namespace App\Policies;
4
+
5
+use App\Models\User;
6
+use Spatie\Permission\Models\Role;
7
+use Illuminate\Auth\Access\HandlesAuthorization;
8
+
9
+class RolePolicy
10
+{
11
+    use HandlesAuthorization;
12
+
13
+    /**
14
+     * Determine whether the user can view any models.
15
+     */
16
+    public function viewAny(User $user): bool
17
+    {
18
+        return $user->can('view_any_role');
19
+    }
20
+
21
+    /**
22
+     * Determine whether the user can view the model.
23
+     */
24
+    public function view(User $user, Role $role): bool
25
+    {
26
+        return $user->can('view_role');
27
+    }
28
+
29
+    /**
30
+     * Determine whether the user can create models.
31
+     */
32
+    public function create(User $user): bool
33
+    {
34
+        return $user->can('create_role');
35
+    }
36
+
37
+    /**
38
+     * Determine whether the user can update the model.
39
+     */
40
+    public function update(User $user, Role $role): bool
41
+    {
42
+        return $user->can('update_role');
43
+    }
44
+
45
+    /**
46
+     * Determine whether the user can delete the model.
47
+     */
48
+    public function delete(User $user, Role $role): bool
49
+    {
50
+        return $user->can('delete_role');
51
+    }
52
+
53
+    /**
54
+     * Determine whether the user can bulk delete.
55
+     */
56
+    public function deleteAny(User $user): bool
57
+    {
58
+        return $user->can('delete_any_role');
59
+    }
60
+
61
+    /**
62
+     * Determine whether the user can permanently delete.
63
+     */
64
+    public function forceDelete(User $user, Role $role): bool
65
+    {
66
+        return $user->can('{{ ForceDelete }}');
67
+    }
68
+
69
+    /**
70
+     * Determine whether the user can permanently bulk delete.
71
+     */
72
+    public function forceDeleteAny(User $user): bool
73
+    {
74
+        return $user->can('{{ ForceDeleteAny }}');
75
+    }
76
+
77
+    /**
78
+     * Determine whether the user can restore.
79
+     */
80
+    public function restore(User $user, Role $role): bool
81
+    {
82
+        return $user->can('{{ Restore }}');
83
+    }
84
+
85
+    /**
86
+     * Determine whether the user can bulk restore.
87
+     */
88
+    public function restoreAny(User $user): bool
89
+    {
90
+        return $user->can('{{ RestoreAny }}');
91
+    }
92
+
93
+    /**
94
+     * Determine whether the user can replicate.
95
+     */
96
+    public function replicate(User $user, Role $role): bool
97
+    {
98
+        return $user->can('{{ Replicate }}');
99
+    }
100
+
101
+    /**
102
+     * Determine whether the user can reorder.
103
+     */
104
+    public function reorder(User $user): bool
105
+    {
106
+        return $user->can('{{ Reorder }}');
107
+    }
108
+}

+ 36
- 0
app/Providers/AppServiceProvider.php View File

@@ -0,0 +1,36 @@
1
+<?php
2
+
3
+namespace App\Providers;
4
+
5
+use Illuminate\Support\Facades\App;
6
+use Illuminate\Support\Facades\Gate;
7
+use Illuminate\Support\Facades\URL;
8
+use Illuminate\Support\ServiceProvider;
9
+
10
+class AppServiceProvider extends ServiceProvider
11
+{
12
+    /**
13
+     * Register any application services.
14
+     */
15
+    public function register(): void
16
+    {
17
+        //
18
+    }
19
+
20
+    /**
21
+     * Bootstrap any application services.
22
+     */
23
+    public function boot(): void
24
+    {
25
+        if (App::environment('production')) {
26
+            URL::forceScheme('https');
27
+        }
28
+        Gate::before(function ($user, $ability) {
29
+            if ($user->email == env("SUPER_ADMIN_ACCOUNT", "admin@ogilvy.com")) {
30
+                return true; // Super admin always passes
31
+            }
32
+            // Continue to policy logic for other users
33
+            return null; // Null lets the policy decide
34
+        });
35
+    }
36
+}

+ 26
- 0
app/Providers/AuthServiceProvider.php View File

@@ -0,0 +1,26 @@
1
+<?php
2
+
3
+namespace App\Providers;
4
+
5
+// use Illuminate\Support\Facades\Gate;
6
+use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
7
+
8
+class AuthServiceProvider extends ServiceProvider
9
+{
10
+    /**
11
+     * The model to policy mappings for the application.
12
+     *
13
+     * @var array<class-string, class-string>
14
+     */
15
+    protected $policies = [
16
+        //
17
+    ];
18
+
19
+    /**
20
+     * Register any authentication / authorization services.
21
+     */
22
+    public function boot(): void
23
+    {
24
+        //
25
+    }
26
+}

+ 19
- 0
app/Providers/BroadcastServiceProvider.php View File

@@ -0,0 +1,19 @@
1
+<?php
2
+
3
+namespace App\Providers;
4
+
5
+use Illuminate\Support\Facades\Broadcast;
6
+use Illuminate\Support\ServiceProvider;
7
+
8
+class BroadcastServiceProvider extends ServiceProvider
9
+{
10
+    /**
11
+     * Bootstrap any application services.
12
+     */
13
+    public function boot(): void
14
+    {
15
+        Broadcast::routes();
16
+
17
+        require base_path('routes/channels.php');
18
+    }
19
+}

+ 38
- 0
app/Providers/EventServiceProvider.php View File

@@ -0,0 +1,38 @@
1
+<?php
2
+
3
+namespace App\Providers;
4
+
5
+use Illuminate\Auth\Events\Registered;
6
+use Illuminate\Auth\Listeners\SendEmailVerificationNotification;
7
+use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
8
+use Illuminate\Support\Facades\Event;
9
+
10
+class EventServiceProvider extends ServiceProvider
11
+{
12
+    /**
13
+     * The event to listener mappings for the application.
14
+     *
15
+     * @var array<class-string, array<int, class-string>>
16
+     */
17
+    protected $listen = [
18
+        Registered::class => [
19
+            SendEmailVerificationNotification::class,
20
+        ],
21
+    ];
22
+
23
+    /**
24
+     * Register any events for your application.
25
+     */
26
+    public function boot(): void
27
+    {
28
+        //
29
+    }
30
+
31
+    /**
32
+     * Determine if events and listeners should be automatically discovered.
33
+     */
34
+    public function shouldDiscoverEvents(): bool
35
+    {
36
+        return false;
37
+    }
38
+}

+ 73
- 0
app/Providers/Filament/AdminPanelProvider.php View File

@@ -0,0 +1,73 @@
1
+<?php
2
+
3
+namespace App\Providers\Filament;
4
+
5
+use App\Filament\Pages\Auth\Login;
6
+use BezhanSalleh\FilamentShield\FilamentShieldPlugin;
7
+use Filament\Forms\Components\Radio;
8
+use Filament\Http\Middleware\Authenticate;
9
+use Filament\Http\Middleware\AuthenticateSession;
10
+use Filament\Http\Middleware\DisableBladeIconComponents;
11
+use Filament\Http\Middleware\DispatchServingFilamentEvent;
12
+use Filament\Pages;
13
+use Filament\Panel;
14
+use Filament\PanelProvider;
15
+use Filament\Support\Colors\Color;
16
+use Filament\Widgets;
17
+use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse;
18
+use Illuminate\Cookie\Middleware\EncryptCookies;
19
+use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken;
20
+use Illuminate\Routing\Middleware\SubstituteBindings;
21
+use Illuminate\Session\Middleware\StartSession;
22
+use Illuminate\View\Middleware\ShareErrorsFromSession;
23
+use Pboivin\FilamentPeek\FilamentPeekPlugin;
24
+use SolutionForest\FilamentTranslateField\FilamentTranslateFieldPlugin;
25
+
26
+class AdminPanelProvider extends PanelProvider
27
+{
28
+    public function panel(Panel $panel): Panel
29
+    {
30
+        Radio::configureUsing(function (Radio $radio): void {
31
+            $radio->inline()->inlineLabel(false);
32
+        });
33
+
34
+        return $panel
35
+            ->default()
36
+            ->id('admin')
37
+            ->path('admin')
38
+            ->login()
39
+            ->colors([
40
+                'primary' => Color::Amber,
41
+            ])
42
+            ->databaseNotifications()
43
+            ->discoverResources(in: app_path('Filament/Resources'), for: 'App\\Filament\\Resources')
44
+            ->discoverPages(in: app_path('Filament/Pages'), for: 'App\\Filament\\Pages')
45
+            ->pages([
46
+                Pages\Dashboard::class,
47
+            ])
48
+            ->discoverWidgets(in: app_path('Filament/Widgets'), for: 'App\\Filament\\Widgets')
49
+            ->widgets([
50
+                Widgets\AccountWidget::class,
51
+                // Widgets\FilamentInfoWidget::class,
52
+            ])
53
+            ->middleware([
54
+                EncryptCookies::class,
55
+                AddQueuedCookiesToResponse::class,
56
+                StartSession::class,
57
+                AuthenticateSession::class,
58
+                ShareErrorsFromSession::class,
59
+                VerifyCsrfToken::class,
60
+                SubstituteBindings::class,
61
+                DisableBladeIconComponents::class,
62
+                DispatchServingFilamentEvent::class,
63
+            ])
64
+            ->authMiddleware([
65
+                Authenticate::class,
66
+            ])->plugins([
67
+                FilamentShieldPlugin::make(),
68
+                FilamentTranslateFieldPlugin::make()
69
+                ->defaultLocales(['zh_TW', "en"]),
70
+                FilamentPeekPlugin::make(),
71
+            ]);
72
+    }
73
+}

+ 40
- 0
app/Providers/RouteServiceProvider.php View File

@@ -0,0 +1,40 @@
1
+<?php
2
+
3
+namespace App\Providers;
4
+
5
+use Illuminate\Cache\RateLimiting\Limit;
6
+use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
7
+use Illuminate\Http\Request;
8
+use Illuminate\Support\Facades\RateLimiter;
9
+use Illuminate\Support\Facades\Route;
10
+
11
+class RouteServiceProvider extends ServiceProvider
12
+{
13
+    /**
14
+     * The path to your application's "home" route.
15
+     *
16
+     * Typically, users are redirected here after authentication.
17
+     *
18
+     * @var string
19
+     */
20
+    public const HOME = '/home';
21
+
22
+    /**
23
+     * Define your route model bindings, pattern filters, and other route configuration.
24
+     */
25
+    public function boot(): void
26
+    {
27
+        RateLimiter::for('api', function (Request $request) {
28
+            return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
29
+        });
30
+
31
+        $this->routes(function () {
32
+            Route::middleware('api')
33
+                ->prefix('api')
34
+                ->group(base_path('routes/api.php'));
35
+
36
+            Route::middleware('web')
37
+                ->group(base_path('routes/web.php'));
38
+        });
39
+    }
40
+}

+ 84
- 0
app/Service/CaptchaService.php View File

@@ -0,0 +1,84 @@
1
+<?php
2
+
3
+namespace App\Service;
4
+
5
+class CaptchaService
6
+{
7
+    public function generateCode(): string
8
+    {
9
+        $characters = ['2', '3', '4', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'm', 'n', 'p', 'q', 'r', 's', 't', 'u', 'v','x', 'y', 'z'];
10
+
11
+        $code = '';
12
+        $max = count($characters) - 1;
13
+
14
+        for ($i = 0; $i < 6; $i++) {
15
+            $code .= $characters[random_int(0, $max)];
16
+        }
17
+
18
+        return $code;
19
+    }
20
+
21
+    public function generateImage(string $code): string
22
+    {
23
+        // Create image
24
+        $width = 200;
25
+        $height = 50;
26
+        $image = imagecreatetruecolor($width, $height);
27
+
28
+        // Colors
29
+        $background = imagecolorallocate($image, 255, 255, 255);
30
+        $textColor = imagecolorallocate($image, 33, 33, 33);
31
+        $noiseColor = imagecolorallocate($image, 123, 123, 123);
32
+
33
+        // Fill background
34
+        imagefilledrectangle($image, 0, 0, $width, $height, $background);
35
+
36
+        // Add random lines for noise
37
+        for ($i = 0; $i < 4; $i++) {
38
+            imageline(
39
+                $image,
40
+                mt_rand(0, $width),
41
+                mt_rand(0, $height),
42
+                mt_rand(0, $width),
43
+                mt_rand(0, $height),
44
+                $noiseColor
45
+            );
46
+        }
47
+
48
+        // Add dots for noise
49
+        for ($i = 0; $i < 100; $i++) {
50
+            imagesetpixel(
51
+                $image,
52
+                mt_rand(0, $width),
53
+                mt_rand(0, $height),
54
+                $noiseColor
55
+            );
56
+        }
57
+
58
+        // Split code into characters and space them out
59
+        $chars = str_split($code);
60
+        $spacing = 25; // Space between characters
61
+        $startX = 30;  // Starting X position
62
+
63
+        foreach ($chars as $i => $char) {
64
+            imagestring(
65
+                $image,
66
+                5, // font size (1-5)
67
+                $startX + ($i * $spacing),
68
+                15, // Y position
69
+                $char,
70
+                $textColor
71
+            );
72
+        }
73
+
74
+        // Output image as base64
75
+        ob_start();
76
+        imagepng($image);
77
+        $imageData = ob_get_clean();
78
+
79
+        // Clean up
80
+        imagedestroy($image);
81
+
82
+        return 'data:image/png;base64,' . base64_encode($imageData);
83
+    }
84
+}

+ 44
- 0
app/Supports/Response.php View File

@@ -0,0 +1,44 @@
1
+<?php
2
+
3
+namespace App\Supports;
4
+
5
+use Illuminate\Pagination\LengthAwarePaginator;
6
+
7
+class Response
8
+{
9
+    /**
10
+     *    response ok
11
+     *    @param  [type]   $result         [description]
12
+     *    @param  array    $custom         [description]
13
+     *    @param  integer  $responseCode   [description]
14
+     *    @return object                   [description]
15
+     */
16
+    public static function ok($result = [], int $responseCode = 200, array $custom = [])
17
+    {
18
+        $paginator = [];
19
+        if ($result instanceof LengthAwarePaginator) {
20
+            $paginator = collect($result->toArray());
21
+            $result = $paginator->pull('data');
22
+            $paginator = ['paginate' => $paginator];
23
+        }
24
+
25
+        $output = array_merge(['data' => $result], $paginator, $custom);
26
+
27
+        return response()->json($output, $responseCode);
28
+    }
29
+
30
+    /**
31
+     *    response fail
32
+     *    @param  string   $message        [description]
33
+     *    @param  array    $payload        [description]
34
+     *    @param  integer  $responseCode   [description]
35
+     *    @return object                   [description]
36
+     */
37
+    public static function fail($message = 'api error', int $responseCode = 400, array $payload = [])
38
+    {
39
+        return response()->json([
40
+            'message' => $message,
41
+            'errors'  => $payload,
42
+        ], $responseCode);
43
+    }
44
+}

+ 18
- 0
artisan View File

@@ -0,0 +1,18 @@
1
+#!/usr/bin/env php
2
+<?php
3
+
4
+use Illuminate\Foundation\Application;
5
+use Symfony\Component\Console\Input\ArgvInput;
6
+
7
+define('LARAVEL_START', microtime(true));
8
+
9
+// Register the Composer autoloader...
10
+require __DIR__.'/vendor/autoload.php';
11
+
12
+// Bootstrap Laravel and handle the command...
13
+/** @var Application $app */
14
+$app = require_once __DIR__.'/bootstrap/app.php';
15
+
16
+$status = $app->handleCommand(new ArgvInput);
17
+
18
+exit($status);

+ 18
- 0
bootstrap/app.php View File

@@ -0,0 +1,18 @@
1
+<?php
2
+
3
+use Illuminate\Foundation\Application;
4
+use Illuminate\Foundation\Configuration\Exceptions;
5
+use Illuminate\Foundation\Configuration\Middleware;
6
+
7
+return Application::configure(basePath: dirname(__DIR__))
8
+    ->withRouting(
9
+        web: __DIR__.'/../routes/web.php',
10
+        commands: __DIR__.'/../routes/console.php',
11
+        health: '/up',
12
+    )
13
+    ->withMiddleware(function (Middleware $middleware) {
14
+        //
15
+    })
16
+    ->withExceptions(function (Exceptions $exceptions) {
17
+        //
18
+    })->create();

+ 2
- 0
bootstrap/cache/.gitignore View File

@@ -0,0 +1,2 @@
1
+*
2
+!.gitignore

+ 7
- 0
bootstrap/providers.php View File

@@ -0,0 +1,7 @@
1
+<?php
2
+
3
+return [
4
+    App\Providers\AppServiceProvider::class,
5
+    App\Providers\Filament\AdminPanelProvider::class,
6
+    App\Providers\RouteServiceProvider::class,
7
+];

+ 82
- 0
composer.json View File

@@ -0,0 +1,82 @@
1
+{
2
+    "$schema": "https://getcomposer.org/schema.json",
3
+    "name": "laravel/laravel",
4
+    "type": "project",
5
+    "description": "The skeleton application for the Laravel framework.",
6
+    "keywords": ["laravel", "framework"],
7
+    "license": "MIT",
8
+    "require": {
9
+        "php": "^8.2",
10
+        "bezhansalleh/filament-shield": "^3.3",
11
+        "filament/filament": "^3.3",
12
+        "filament/notifications": "^3.3",
13
+        "filament/spatie-laravel-translatable-plugin": "^3.2",
14
+        "laravel/framework": "^12.0",
15
+        "laravel/tinker": "^2.10.1",
16
+        "pboivin/filament-peek": "^2.0",
17
+        "solution-forest/filament-translate-field": "^1.4"
18
+    },
19
+    "require-dev": {
20
+        "fakerphp/faker": "^1.23",
21
+        "laravel/pail": "^1.2.2",
22
+        "laravel/pint": "^1.13",
23
+        "laravel/sail": "^1.41",
24
+        "mockery/mockery": "^1.6",
25
+        "nunomaduro/collision": "^8.6",
26
+        "phpunit/phpunit": "^11.5.3"
27
+    },
28
+    "autoload": {
29
+        "psr-4": {
30
+            "App\\": "app/",
31
+            "Database\\Factories\\": "database/factories/",
32
+            "Database\\Seeders\\": "database/seeders/"
33
+        }
34
+    },
35
+    "autoload-dev": {
36
+        "psr-4": {
37
+            "Tests\\": "tests/"
38
+        }
39
+    },
40
+    "scripts": {
41
+        "post-autoload-dump": [
42
+            "Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
43
+            "@php artisan package:discover --ansi",
44
+            "@php artisan filament:upgrade"
45
+        ],
46
+        "post-update-cmd": [
47
+            "@php artisan vendor:publish --tag=laravel-assets --ansi --force"
48
+        ],
49
+        "post-root-package-install": [
50
+            "@php -r \"file_exists('.env') || copy('.env.example', '.env');\""
51
+        ],
52
+        "post-create-project-cmd": [
53
+            "@php artisan key:generate --ansi",
54
+            "@php -r \"file_exists('database/database.sqlite') || touch('database/database.sqlite');\"",
55
+            "@php artisan migrate --graceful --ansi"
56
+        ],
57
+        "dev": [
58
+            "Composer\\Config::disableProcessTimeout",
59
+            "npx concurrently -c \"#93c5fd,#c4b5fd,#fb7185,#fdba74\" \"php artisan serve\" \"php artisan queue:listen --tries=1\" \"php artisan pail --timeout=0\" \"npm run dev\" --names=server,queue,logs,vite"
60
+        ],
61
+        "test": [
62
+            "@php artisan config:clear --ansi",
63
+            "@php artisan test"
64
+        ]
65
+    },
66
+    "extra": {
67
+        "laravel": {
68
+            "dont-discover": []
69
+        }
70
+    },
71
+    "config": {
72
+        "optimize-autoloader": true,
73
+        "preferred-install": "dist",
74
+        "sort-packages": true,
75
+        "allow-plugins": {
76
+            "pestphp/pest-plugin": true,
77
+            "php-http/discovery": true
78
+        }
79
+    },
80
+    "minimum-stability": "stable",
81
+    "prefer-stable": true
82
+}

+ 10206
- 0
composer.lock
File diff suppressed because it is too large
View File


+ 126
- 0
config/app.php View File

@@ -0,0 +1,126 @@
1
+<?php
2
+
3
+return [
4
+
5
+    /*
6
+    |--------------------------------------------------------------------------
7
+    | Application Name
8
+    |--------------------------------------------------------------------------
9
+    |
10
+    | This value is the name of your application, which will be used when the
11
+    | framework needs to place the application's name in a notification or
12
+    | other UI elements where an application name needs to be displayed.
13
+    |
14
+    */
15
+
16
+    'name' => env('APP_NAME', 'Laravel'),
17
+
18
+    /*
19
+    |--------------------------------------------------------------------------
20
+    | Application Environment
21
+    |--------------------------------------------------------------------------
22
+    |
23
+    | This value determines the "environment" your application is currently
24
+    | running in. This may determine how you prefer to configure various
25
+    | services the application utilizes. Set this in your ".env" file.
26
+    |
27
+    */
28
+
29
+    'env' => env('APP_ENV', 'production'),
30
+
31
+    /*
32
+    |--------------------------------------------------------------------------
33
+    | Application Debug Mode
34
+    |--------------------------------------------------------------------------
35
+    |
36
+    | When your application is in debug mode, detailed error messages with
37
+    | stack traces will be shown on every error that occurs within your
38
+    | application. If disabled, a simple generic error page is shown.
39
+    |
40
+    */
41
+
42
+    'debug' => (bool) env('APP_DEBUG', false),
43
+
44
+    /*
45
+    |--------------------------------------------------------------------------
46
+    | Application URL
47
+    |--------------------------------------------------------------------------
48
+    |
49
+    | This URL is used by the console to properly generate URLs when using
50
+    | the Artisan command line tool. You should set this to the root of
51
+    | the application so that it's available within Artisan commands.
52
+    |
53
+    */
54
+
55
+    'url' => env('APP_URL', 'http://localhost'),
56
+
57
+    /*
58
+    |--------------------------------------------------------------------------
59
+    | Application Timezone
60
+    |--------------------------------------------------------------------------
61
+    |
62
+    | Here you may specify the default timezone for your application, which
63
+    | will be used by the PHP date and date-time functions. The timezone
64
+    | is set to "UTC" by default as it is suitable for most use cases.
65
+    |
66
+    */
67
+
68
+    'timezone' => 'UTC',
69
+
70
+    /*
71
+    |--------------------------------------------------------------------------
72
+    | Application Locale Configuration
73
+    |--------------------------------------------------------------------------
74
+    |
75
+    | The application locale determines the default locale that will be used
76
+    | by Laravel's translation / localization methods. This option can be
77
+    | set to any locale for which you plan to have translation strings.
78
+    |
79
+    */
80
+
81
+    'locale' => env('APP_LOCALE', 'zh_TW'),
82
+
83
+    'fallback_locale' => env('APP_FALLBACK_LOCALE', 'zh_TW'),
84
+
85
+    'faker_locale' => env('APP_FAKER_LOCALE', 'en_US'),
86
+
87
+    /*
88
+    |--------------------------------------------------------------------------
89
+    | Encryption Key
90
+    |--------------------------------------------------------------------------
91
+    |
92
+    | This key is utilized by Laravel's encryption services and should be set
93
+    | to a random, 32 character string to ensure that all encrypted values
94
+    | are secure. You should do this prior to deploying the application.
95
+    |
96
+    */
97
+
98
+    'cipher' => 'AES-256-CBC',
99
+
100
+    'key' => env('APP_KEY'),
101
+
102
+    'previous_keys' => [
103
+        ...array_filter(
104
+            explode(',', env('APP_PREVIOUS_KEYS', ''))
105
+        ),
106
+    ],
107
+
108
+    /*
109
+    |--------------------------------------------------------------------------
110
+    | Maintenance Mode Driver
111
+    |--------------------------------------------------------------------------
112
+    |
113
+    | These configuration options determine the driver used to determine and
114
+    | manage Laravel's "maintenance mode" status. The "cache" driver will
115
+    | allow maintenance mode to be controlled across multiple machines.
116
+    |
117
+    | Supported drivers: "file", "cache"
118
+    |
119
+    */
120
+
121
+    'maintenance' => [
122
+        'driver' => env('APP_MAINTENANCE_DRIVER', 'file'),
123
+        'store' => env('APP_MAINTENANCE_STORE', 'database'),
124
+    ],
125
+
126
+];

+ 115
- 0
config/auth.php View File

@@ -0,0 +1,115 @@
1
+<?php
2
+
3
+return [
4
+
5
+    /*
6
+    |--------------------------------------------------------------------------
7
+    | Authentication Defaults
8
+    |--------------------------------------------------------------------------
9
+    |
10
+    | This option defines the default authentication "guard" and password
11
+    | reset "broker" for your application. You may change these values
12
+    | as required, but they're a perfect start for most applications.
13
+    |
14
+    */
15
+
16
+    'defaults' => [
17
+        'guard' => env('AUTH_GUARD', 'web'),
18
+        'passwords' => env('AUTH_PASSWORD_BROKER', 'users'),
19
+    ],
20
+
21
+    /*
22
+    |--------------------------------------------------------------------------
23
+    | Authentication Guards
24
+    |--------------------------------------------------------------------------
25
+    |
26
+    | Next, you may define every authentication guard for your application.
27
+    | Of course, a great default configuration has been defined for you
28
+    | which utilizes session storage plus the Eloquent user provider.
29
+    |
30
+    | All authentication guards have a user provider, which defines how the
31
+    | users are actually retrieved out of your database or other storage
32
+    | system used by the application. Typically, Eloquent is utilized.
33
+    |
34
+    | Supported: "session"
35
+    |
36
+    */
37
+
38
+    'guards' => [
39
+        'web' => [
40
+            'driver' => 'session',
41
+            'provider' => 'users',
42
+        ],
43
+    ],
44
+
45
+    /*
46
+    |--------------------------------------------------------------------------
47
+    | User Providers
48
+    |--------------------------------------------------------------------------
49
+    |
50
+    | All authentication guards have a user provider, which defines how the
51
+    | users are actually retrieved out of your database or other storage
52
+    | system used by the application. Typically, Eloquent is utilized.
53
+    |
54
+    | If you have multiple user tables or models you may configure multiple
55
+    | providers to represent the model / table. These providers may then
56
+    | be assigned to any extra authentication guards you have defined.
57
+    |
58
+    | Supported: "database", "eloquent"
59
+    |
60
+    */
61
+
62
+    'providers' => [
63
+        'users' => [
64
+            'driver' => 'eloquent',
65
+            'model' => env('AUTH_MODEL', App\Models\User::class),
66
+        ],
67
+
68
+        // 'users' => [
69
+        //     'driver' => 'database',
70
+        //     'table' => 'users',
71
+        // ],
72
+    ],
73
+
74
+    /*
75
+    |--------------------------------------------------------------------------
76
+    | Resetting Passwords
77
+    |--------------------------------------------------------------------------
78
+    |
79
+    | These configuration options specify the behavior of Laravel's password
80
+    | reset functionality, including the table utilized for token storage
81
+    | and the user provider that is invoked to actually retrieve users.
82
+    |
83
+    | The expiry time is the number of minutes that each reset token will be
84
+    | considered valid. This security feature keeps tokens short-lived so
85
+    | they have less time to be guessed. You may change this as needed.
86
+    |
87
+    | The throttle setting is the number of seconds a user must wait before
88
+    | generating more password reset tokens. This prevents the user from
89
+    | quickly generating a very large amount of password reset tokens.
90
+    |
91
+    */
92
+
93
+    'passwords' => [
94
+        'users' => [
95
+            'provider' => 'users',
96
+            'table' => env('AUTH_PASSWORD_RESET_TOKEN_TABLE', 'password_reset_tokens'),
97
+            'expire' => 60,
98
+            'throttle' => 60,
99
+        ],
100
+    ],
101
+
102
+    /*
103
+    |--------------------------------------------------------------------------
104
+    | Password Confirmation Timeout
105
+    |--------------------------------------------------------------------------
106
+    |
107
+    | Here you may define the amount of seconds before a password confirmation
108
+    | window expires and users are asked to re-enter their password via the
109
+    | confirmation screen. By default, the timeout lasts for three hours.
110
+    |
111
+    */
112
+
113
+    'password_timeout' => env('AUTH_PASSWORD_TIMEOUT', 10800),
114
+
115
+];

+ 108
- 0
config/cache.php View File

@@ -0,0 +1,108 @@
1
+<?php
2
+
3
+use Illuminate\Support\Str;
4
+
5
+return [
6
+
7
+    /*
8
+    |--------------------------------------------------------------------------
9
+    | Default Cache Store
10
+    |--------------------------------------------------------------------------
11
+    |
12
+    | This option controls the default cache store that will be used by the
13
+    | framework. This connection is utilized if another isn't explicitly
14
+    | specified when running a cache operation inside the application.
15
+    |
16
+    */
17
+
18
+    'default' => env('CACHE_STORE', 'database'),
19
+
20
+    /*
21
+    |--------------------------------------------------------------------------
22
+    | Cache Stores
23
+    |--------------------------------------------------------------------------
24
+    |
25
+    | Here you may define all of the cache "stores" for your application as
26
+    | well as their drivers. You may even define multiple stores for the
27
+    | same cache driver to group types of items stored in your caches.
28
+    |
29
+    | Supported drivers: "array", "database", "file", "memcached",
30
+    |                    "redis", "dynamodb", "octane", "null"
31
+    |
32
+    */
33
+
34
+    'stores' => [
35
+
36
+        'array' => [
37
+            'driver' => 'array',
38
+            'serialize' => false,
39
+        ],
40
+
41
+        'database' => [
42
+            'driver' => 'database',
43
+            'connection' => env('DB_CACHE_CONNECTION'),
44
+            'table' => env('DB_CACHE_TABLE', 'cache'),
45
+            'lock_connection' => env('DB_CACHE_LOCK_CONNECTION'),
46
+            'lock_table' => env('DB_CACHE_LOCK_TABLE'),
47
+        ],
48
+
49
+        'file' => [
50
+            'driver' => 'file',
51
+            'path' => storage_path('framework/cache/data'),
52
+            'lock_path' => storage_path('framework/cache/data'),
53
+        ],
54
+
55
+        'memcached' => [
56
+            'driver' => 'memcached',
57
+            'persistent_id' => env('MEMCACHED_PERSISTENT_ID'),
58
+            'sasl' => [
59
+                env('MEMCACHED_USERNAME'),
60
+                env('MEMCACHED_PASSWORD'),
61
+            ],
62
+            'options' => [
63
+                // Memcached::OPT_CONNECT_TIMEOUT => 2000,
64
+            ],
65
+            'servers' => [
66
+                [
67
+                    'host' => env('MEMCACHED_HOST', '127.0.0.1'),
68
+                    'port' => env('MEMCACHED_PORT', 11211),
69
+                    'weight' => 100,
70
+                ],
71
+            ],
72
+        ],
73
+
74
+        'redis' => [
75
+            'driver' => 'redis',
76
+            'connection' => env('REDIS_CACHE_CONNECTION', 'cache'),
77
+            'lock_connection' => env('REDIS_CACHE_LOCK_CONNECTION', 'default'),
78
+        ],
79
+
80
+        'dynamodb' => [
81
+            'driver' => 'dynamodb',
82
+            'key' => env('AWS_ACCESS_KEY_ID'),
83
+            'secret' => env('AWS_SECRET_ACCESS_KEY'),
84
+            'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
85
+            'table' => env('DYNAMODB_CACHE_TABLE', 'cache'),
86
+            'endpoint' => env('DYNAMODB_ENDPOINT'),
87
+        ],
88
+
89
+        'octane' => [
90
+            'driver' => 'octane',
91
+        ],
92
+
93
+    ],
94
+
95
+    /*
96
+    |--------------------------------------------------------------------------
97
+    | Cache Key Prefix
98
+    |--------------------------------------------------------------------------
99
+    |
100
+    | When utilizing the APC, database, memcached, Redis, and DynamoDB cache
101
+    | stores, there might be other applications using the same cache. For
102
+    | that reason, you may prefix every cache key to avoid collisions.
103
+    |
104
+    */
105
+
106
+    'prefix' => env('CACHE_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_cache_'),
107
+
108
+];

+ 174
- 0
config/database.php View File

@@ -0,0 +1,174 @@
1
+<?php
2
+
3
+use Illuminate\Support\Str;
4
+
5
+return [
6
+
7
+    /*
8
+    |--------------------------------------------------------------------------
9
+    | Default Database Connection Name
10
+    |--------------------------------------------------------------------------
11
+    |
12
+    | Here you may specify which of the database connections below you wish
13
+    | to use as your default connection for database operations. This is
14
+    | the connection which will be utilized unless another connection
15
+    | is explicitly specified when you execute a query / statement.
16
+    |
17
+    */
18
+
19
+    'default' => env('DB_CONNECTION', 'sqlite'),
20
+
21
+    /*
22
+    |--------------------------------------------------------------------------
23
+    | Database Connections
24
+    |--------------------------------------------------------------------------
25
+    |
26
+    | Below are all of the database connections defined for your application.
27
+    | An example configuration is provided for each database system which
28
+    | is supported by Laravel. You're free to add / remove connections.
29
+    |
30
+    */
31
+
32
+    'connections' => [
33
+
34
+        'sqlite' => [
35
+            'driver' => 'sqlite',
36
+            'url' => env('DB_URL'),
37
+            'database' => env('DB_DATABASE', database_path('database.sqlite')),
38
+            'prefix' => '',
39
+            'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true),
40
+            'busy_timeout' => null,
41
+            'journal_mode' => null,
42
+            'synchronous' => null,
43
+        ],
44
+
45
+        'mysql' => [
46
+            'driver' => 'mysql',
47
+            'url' => env('DB_URL'),
48
+            'host' => env('DB_HOST', '127.0.0.1'),
49
+            'port' => env('DB_PORT', '3306'),
50
+            'database' => env('DB_DATABASE', 'laravel'),
51
+            'username' => env('DB_USERNAME', 'root'),
52
+            'password' => env('DB_PASSWORD', ''),
53
+            'unix_socket' => env('DB_SOCKET', ''),
54
+            'charset' => env('DB_CHARSET', 'utf8mb4'),
55
+            'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'),
56
+            'prefix' => '',
57
+            'prefix_indexes' => true,
58
+            'strict' => true,
59
+            'engine' => null,
60
+            'options' => extension_loaded('pdo_mysql') ? array_filter([
61
+                PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
62
+            ]) : [],
63
+        ],
64
+
65
+        'mariadb' => [
66
+            'driver' => 'mariadb',
67
+            'url' => env('DB_URL'),
68
+            'host' => env('DB_HOST', '127.0.0.1'),
69
+            'port' => env('DB_PORT', '3306'),
70
+            'database' => env('DB_DATABASE', 'laravel'),
71
+            'username' => env('DB_USERNAME', 'root'),
72
+            'password' => env('DB_PASSWORD', ''),
73
+            'unix_socket' => env('DB_SOCKET', ''),
74
+            'charset' => env('DB_CHARSET', 'utf8mb4'),
75
+            'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'),
76
+            'prefix' => '',
77
+            'prefix_indexes' => true,
78
+            'strict' => true,
79
+            'engine' => null,
80
+            'options' => extension_loaded('pdo_mysql') ? array_filter([
81
+                PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
82
+            ]) : [],
83
+        ],
84
+
85
+        'pgsql' => [
86
+            'driver' => 'pgsql',
87
+            'url' => env('DB_URL'),
88
+            'host' => env('DB_HOST', '127.0.0.1'),
89
+            'port' => env('DB_PORT', '5432'),
90
+            'database' => env('DB_DATABASE', 'laravel'),
91
+            'username' => env('DB_USERNAME', 'root'),
92
+            'password' => env('DB_PASSWORD', ''),
93
+            'charset' => env('DB_CHARSET', 'utf8'),
94
+            'prefix' => '',
95
+            'prefix_indexes' => true,
96
+            'search_path' => 'public',
97
+            'sslmode' => 'prefer',
98
+        ],
99
+
100
+        'sqlsrv' => [
101
+            'driver' => 'sqlsrv',
102
+            'url' => env('DB_URL'),
103
+            'host' => env('DB_HOST', 'localhost'),
104
+            'port' => env('DB_PORT', '1433'),
105
+            'database' => env('DB_DATABASE', 'laravel'),
106
+            'username' => env('DB_USERNAME', 'root'),
107
+            'password' => env('DB_PASSWORD', ''),
108
+            'charset' => env('DB_CHARSET', 'utf8'),
109
+            'prefix' => '',
110
+            'prefix_indexes' => true,
111
+            // 'encrypt' => env('DB_ENCRYPT', 'yes'),
112
+            // 'trust_server_certificate' => env('DB_TRUST_SERVER_CERTIFICATE', 'false'),
113
+        ],
114
+
115
+    ],
116
+
117
+    /*
118
+    |--------------------------------------------------------------------------
119
+    | Migration Repository Table
120
+    |--------------------------------------------------------------------------
121
+    |
122
+    | This table keeps track of all the migrations that have already run for
123
+    | your application. Using this information, we can determine which of
124
+    | the migrations on disk haven't actually been run on the database.
125
+    |
126
+    */
127
+
128
+    'migrations' => [
129
+        'table' => 'migrations',
130
+        'update_date_on_publish' => true,
131
+    ],
132
+
133
+    /*
134
+    |--------------------------------------------------------------------------
135
+    | Redis Databases
136
+    |--------------------------------------------------------------------------
137
+    |
138
+    | Redis is an open source, fast, and advanced key-value store that also
139
+    | provides a richer body of commands than a typical key-value system
140
+    | such as Memcached. You may define your connection settings here.
141
+    |
142
+    */
143
+
144
+    'redis' => [
145
+
146
+        'client' => env('REDIS_CLIENT', 'phpredis'),
147
+
148
+        'options' => [
149
+            'cluster' => env('REDIS_CLUSTER', 'redis'),
150
+            'prefix' => env('REDIS_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_database_'),
151
+            'persistent' => env('REDIS_PERSISTENT', false),
152
+        ],
153
+
154
+        'default' => [
155
+            'url' => env('REDIS_URL'),
156
+            'host' => env('REDIS_HOST', '127.0.0.1'),
157
+            'username' => env('REDIS_USERNAME'),
158
+            'password' => env('REDIS_PASSWORD'),
159
+            'port' => env('REDIS_PORT', '6379'),
160
+            'database' => env('REDIS_DB', '0'),
161
+        ],
162
+
163
+        'cache' => [
164
+            'url' => env('REDIS_URL'),
165
+            'host' => env('REDIS_HOST', '127.0.0.1'),
166
+            'username' => env('REDIS_USERNAME'),
167
+            'password' => env('REDIS_PASSWORD'),
168
+            'port' => env('REDIS_PORT', '6379'),
169
+            'database' => env('REDIS_CACHE_DB', '1'),
170
+        ],
171
+
172
+    ],
173
+
174
+];

+ 92
- 0
config/filament-shield.php View File

@@ -0,0 +1,92 @@
1
+<?php
2
+
3
+return [
4
+    'shield_resource' => [
5
+        'should_register_navigation' => true,
6
+        'slug' => 'shield/roles',
7
+        'navigation_sort' => -1,
8
+        'navigation_badge' => true,
9
+        'navigation_group' => true,
10
+        'sub_navigation_position' => null,
11
+        'is_globally_searchable' => false,
12
+        'show_model_path' => true,
13
+        'is_scoped_to_tenant' => true,
14
+        'cluster' => null,
15
+    ],
16
+
17
+    'tenant_model' => null,
18
+
19
+    'auth_provider_model' => [
20
+        'fqcn' => 'App\\Models\\User',
21
+    ],
22
+
23
+    'super_admin' => [
24
+        'enabled' => true,
25
+        'name' => 'super_admin',
26
+        'define_via_gate' => false,
27
+        'intercept_gate' => 'before', // after
28
+    ],
29
+
30
+    'panel_user' => [
31
+        'enabled' => true,
32
+        'name' => 'panel_user',
33
+    ],
34
+
35
+    'permission_prefixes' => [
36
+        'resource' => [
37
+            'view',
38
+            'view_any',
39
+            'create',
40
+            'update',
41
+            'restore',
42
+            'restore_any',
43
+            'replicate',
44
+            'reorder',
45
+            'delete',
46
+            'delete_any',
47
+            'force_delete',
48
+            'force_delete_any',
49
+        ],
50
+
51
+        'page' => 'page',
52
+        'widget' => 'widget',
53
+    ],
54
+
55
+    'entities' => [
56
+        'pages' => true,
57
+        'widgets' => true,
58
+        'resources' => true,
59
+        'custom_permissions' => false,
60
+    ],
61
+
62
+    'generator' => [
63
+        'option' => 'policies_and_permissions',
64
+        'policy_directory' => 'Policies',
65
+        'policy_namespace' => 'Policies',
66
+    ],
67
+
68
+    'exclude' => [
69
+        'enabled' => true,
70
+
71
+        'pages' => [
72
+            'Dashboard',
73
+        ],
74
+
75
+        'widgets' => [
76
+            'AccountWidget', 'FilamentInfoWidget',
77
+        ],
78
+
79
+        'resources' => [],
80
+    ],
81
+
82
+    'discovery' => [
83
+        'discover_all_resources' => false,
84
+        'discover_all_widgets' => false,
85
+        'discover_all_pages' => false,
86
+    ],
87
+
88
+    'register_role_policy' => [
89
+        'enabled' => true,
90
+    ],
91
+
92
+];

+ 101
- 0
config/filament.php View File

@@ -0,0 +1,101 @@
1
+<?php
2
+
3
+return [
4
+
5
+    /*
6
+    |--------------------------------------------------------------------------
7
+    | Broadcasting
8
+    |--------------------------------------------------------------------------
9
+    |
10
+    | By uncommenting the Laravel Echo configuration, you may connect Filament
11
+    | to any Pusher-compatible websockets server.
12
+    |
13
+    | This will allow your users to receive real-time notifications.
14
+    |
15
+    */
16
+
17
+    'broadcasting' => [
18
+
19
+        // 'echo' => [
20
+        //     'broadcaster' => 'pusher',
21
+        //     'key' => env('VITE_PUSHER_APP_KEY'),
22
+        //     'cluster' => env('VITE_PUSHER_APP_CLUSTER'),
23
+        //     'wsHost' => env('VITE_PUSHER_HOST'),
24
+        //     'wsPort' => env('VITE_PUSHER_PORT'),
25
+        //     'wssPort' => env('VITE_PUSHER_PORT'),
26
+        //     'authEndpoint' => '/broadcasting/auth',
27
+        //     'disableStats' => true,
28
+        //     'encrypted' => true,
29
+        //     'forceTLS' => true,
30
+        // ],
31
+
32
+    ],
33
+
34
+    /*
35
+    |--------------------------------------------------------------------------
36
+    | Default Filesystem Disk
37
+    |--------------------------------------------------------------------------
38
+    |
39
+    | This is the storage disk Filament will use to store files. You may use
40
+    | any of the disks defined in the `config/filesystems.php`.
41
+    |
42
+    */
43
+
44
+    'default_filesystem_disk' => env('FILAMENT_FILESYSTEM_DISK', 'public'),
45
+
46
+    /*
47
+    |--------------------------------------------------------------------------
48
+    | Assets Path
49
+    |--------------------------------------------------------------------------
50
+    |
51
+    | This is the directory where Filament's assets will be published to. It
52
+    | is relative to the `public` directory of your Laravel application.
53
+    |
54
+    | After changing the path, you should run `php artisan filament:assets`.
55
+    |
56
+    */
57
+
58
+    'assets_path' => null,
59
+
60
+    /*
61
+    |--------------------------------------------------------------------------
62
+    | Cache Path
63
+    |--------------------------------------------------------------------------
64
+    |
65
+    | This is the directory that Filament will use to store cache files that
66
+    | are used to optimize the registration of components.
67
+    |
68
+    | After changing the path, you should run `php artisan filament:cache-components`.
69
+    |
70
+    */
71
+
72
+    'cache_path' => base_path('bootstrap/cache/filament'),
73
+
74
+    /*
75
+    |--------------------------------------------------------------------------
76
+    | Livewire Loading Delay
77
+    |--------------------------------------------------------------------------
78
+    |
79
+    | This sets the delay before loading indicators appear.
80
+    |
81
+    | Setting this to 'none' makes indicators appear immediately, which can be
82
+    | desirable for high-latency connections. Setting it to 'default' applies
83
+    | Livewire's standard 200ms delay.
84
+    |
85
+    */
86
+
87
+    'livewire_loading_delay' => 'default',
88
+
89
+    /*
90
+    |--------------------------------------------------------------------------
91
+    | System Route Prefix
92
+    |--------------------------------------------------------------------------
93
+    |
94
+    | This is the prefix used for the system routes that Filament registers,
95
+    | such as the routes for downloading exports and failed import rows.
96
+    |
97
+    */
98
+
99
+    'system_route_prefix' => 'filament',
100
+
101
+];

+ 80
- 0
config/filesystems.php View File

@@ -0,0 +1,80 @@
1
+<?php
2
+
3
+return [
4
+
5
+    /*
6
+    |--------------------------------------------------------------------------
7
+    | Default Filesystem Disk
8
+    |--------------------------------------------------------------------------
9
+    |
10
+    | Here you may specify the default filesystem disk that should be used
11
+    | by the framework. The "local" disk, as well as a variety of cloud
12
+    | based disks are available to your application for file storage.
13
+    |
14
+    */
15
+
16
+    'default' => env('FILESYSTEM_DISK', 'local'),
17
+
18
+    /*
19
+    |--------------------------------------------------------------------------
20
+    | Filesystem Disks
21
+    |--------------------------------------------------------------------------
22
+    |
23
+    | Below you may configure as many filesystem disks as necessary, and you
24
+    | may even configure multiple disks for the same driver. Examples for
25
+    | most supported storage drivers are configured here for reference.
26
+    |
27
+    | Supported drivers: "local", "ftp", "sftp", "s3"
28
+    |
29
+    */
30
+
31
+    'disks' => [
32
+
33
+        'local' => [
34
+            'driver' => 'local',
35
+            'root' => storage_path('app/private'),
36
+            'serve' => true,
37
+            'throw' => false,
38
+            'report' => false,
39
+        ],
40
+
41
+        'public' => [
42
+            'driver' => 'local',
43
+            'root' => storage_path('app/public'),
44
+            'url' => env('APP_URL').'/storage',
45
+            'visibility' => 'public',
46
+            'throw' => false,
47
+            'report' => false,
48
+        ],
49
+
50
+        's3' => [
51
+            'driver' => 's3',
52
+            'key' => env('AWS_ACCESS_KEY_ID'),
53
+            'secret' => env('AWS_SECRET_ACCESS_KEY'),
54
+            'region' => env('AWS_DEFAULT_REGION'),
55
+            'bucket' => env('AWS_BUCKET'),
56
+            'url' => env('AWS_URL'),
57
+            'endpoint' => env('AWS_ENDPOINT'),
58
+            'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false),
59
+            'throw' => false,
60
+            'report' => false,
61
+        ],
62
+
63
+    ],
64
+
65
+    /*
66
+    |--------------------------------------------------------------------------
67
+    | Symbolic Links
68
+    |--------------------------------------------------------------------------
69
+    |
70
+    | Here you may configure the symbolic links that will be created when the
71
+    | `storage:link` Artisan command is executed. The array keys should be
72
+    | the locations of the links and the values should be their targets.
73
+    |
74
+    */
75
+
76
+    'links' => [
77
+        public_path('storage') => storage_path('app/public'),
78
+    ],
79
+
80
+];

+ 132
- 0
config/logging.php View File

@@ -0,0 +1,132 @@
1
+<?php
2
+
3
+use Monolog\Handler\NullHandler;
4
+use Monolog\Handler\StreamHandler;
5
+use Monolog\Handler\SyslogUdpHandler;
6
+use Monolog\Processor\PsrLogMessageProcessor;
7
+
8
+return [
9
+
10
+    /*
11
+    |--------------------------------------------------------------------------
12
+    | Default Log Channel
13
+    |--------------------------------------------------------------------------
14
+    |
15
+    | This option defines the default log channel that is utilized to write
16
+    | messages to your logs. The value provided here should match one of
17
+    | the channels present in the list of "channels" configured below.
18
+    |
19
+    */
20
+
21
+    'default' => env('LOG_CHANNEL', 'stack'),
22
+
23
+    /*
24
+    |--------------------------------------------------------------------------
25
+    | Deprecations Log Channel
26
+    |--------------------------------------------------------------------------
27
+    |
28
+    | This option controls the log channel that should be used to log warnings
29
+    | regarding deprecated PHP and library features. This allows you to get
30
+    | your application ready for upcoming major versions of dependencies.
31
+    |
32
+    */
33
+
34
+    'deprecations' => [
35
+        'channel' => env('LOG_DEPRECATIONS_CHANNEL', 'null'),
36
+        'trace' => env('LOG_DEPRECATIONS_TRACE', false),
37
+    ],
38
+
39
+    /*
40
+    |--------------------------------------------------------------------------
41
+    | Log Channels
42
+    |--------------------------------------------------------------------------
43
+    |
44
+    | Here you may configure the log channels for your application. Laravel
45
+    | utilizes the Monolog PHP logging library, which includes a variety
46
+    | of powerful log handlers and formatters that you're free to use.
47
+    |
48
+    | Available drivers: "single", "daily", "slack", "syslog",
49
+    |                    "errorlog", "monolog", "custom", "stack"
50
+    |
51
+    */
52
+
53
+    'channels' => [
54
+
55
+        'stack' => [
56
+            'driver' => 'stack',
57
+            'channels' => explode(',', env('LOG_STACK', 'single')),
58
+            'ignore_exceptions' => false,
59
+        ],
60
+
61
+        'single' => [
62
+            'driver' => 'single',
63
+            'path' => storage_path('logs/laravel.log'),
64
+            'level' => env('LOG_LEVEL', 'debug'),
65
+            'replace_placeholders' => true,
66
+        ],
67
+
68
+        'daily' => [
69
+            'driver' => 'daily',
70
+            'path' => storage_path('logs/laravel.log'),
71
+            'level' => env('LOG_LEVEL', 'debug'),
72
+            'days' => env('LOG_DAILY_DAYS', 14),
73
+            'replace_placeholders' => true,
74
+        ],
75
+
76
+        'slack' => [
77
+            'driver' => 'slack',
78
+            'url' => env('LOG_SLACK_WEBHOOK_URL'),
79
+            'username' => env('LOG_SLACK_USERNAME', 'Laravel Log'),
80
+            'emoji' => env('LOG_SLACK_EMOJI', ':boom:'),
81
+            'level' => env('LOG_LEVEL', 'critical'),
82
+            'replace_placeholders' => true,
83
+        ],
84
+
85
+        'papertrail' => [
86
+            'driver' => 'monolog',
87
+            'level' => env('LOG_LEVEL', 'debug'),
88
+            'handler' => env('LOG_PAPERTRAIL_HANDLER', SyslogUdpHandler::class),
89
+            'handler_with' => [
90
+                'host' => env('PAPERTRAIL_URL'),
91
+                'port' => env('PAPERTRAIL_PORT'),
92
+                'connectionString' => 'tls://'.env('PAPERTRAIL_URL').':'.env('PAPERTRAIL_PORT'),
93
+            ],
94
+            'processors' => [PsrLogMessageProcessor::class],
95
+        ],
96
+
97
+        'stderr' => [
98
+            'driver' => 'monolog',
99
+            'level' => env('LOG_LEVEL', 'debug'),
100
+            'handler' => StreamHandler::class,
101
+            'handler_with' => [
102
+                'stream' => 'php://stderr',
103
+            ],
104
+            'formatter' => env('LOG_STDERR_FORMATTER'),
105
+            'processors' => [PsrLogMessageProcessor::class],
106
+        ],
107
+
108
+        'syslog' => [
109
+            'driver' => 'syslog',
110
+            'level' => env('LOG_LEVEL', 'debug'),
111
+            'facility' => env('LOG_SYSLOG_FACILITY', LOG_USER),
112
+            'replace_placeholders' => true,
113
+        ],
114
+
115
+        'errorlog' => [
116
+            'driver' => 'errorlog',
117
+            'level' => env('LOG_LEVEL', 'debug'),
118
+            'replace_placeholders' => true,
119
+        ],
120
+
121
+        'null' => [
122
+            'driver' => 'monolog',
123
+            'handler' => NullHandler::class,
124
+        ],
125
+
126
+        'emergency' => [
127
+            'path' => storage_path('logs/laravel.log'),
128
+        ],
129
+
130
+    ],
131
+
132
+];

+ 118
- 0
config/mail.php View File

@@ -0,0 +1,118 @@
1
+<?php
2
+
3
+return [
4
+
5
+    /*
6
+    |--------------------------------------------------------------------------
7
+    | Default Mailer
8
+    |--------------------------------------------------------------------------
9
+    |
10
+    | This option controls the default mailer that is used to send all email
11
+    | messages unless another mailer is explicitly specified when sending
12
+    | the message. All additional mailers can be configured within the
13
+    | "mailers" array. Examples of each type of mailer are provided.
14
+    |
15
+    */
16
+
17
+    'default' => env('MAIL_MAILER', 'log'),
18
+
19
+    /*
20
+    |--------------------------------------------------------------------------
21
+    | Mailer Configurations
22
+    |--------------------------------------------------------------------------
23
+    |
24
+    | Here you may configure all of the mailers used by your application plus
25
+    | their respective settings. Several examples have been configured for
26
+    | you and you are free to add your own as your application requires.
27
+    |
28
+    | Laravel supports a variety of mail "transport" drivers that can be used
29
+    | when delivering an email. You may specify which one you're using for
30
+    | your mailers below. You may also add additional mailers if needed.
31
+    |
32
+    | Supported: "smtp", "sendmail", "mailgun", "ses", "ses-v2",
33
+    |            "postmark", "resend", "log", "array",
34
+    |            "failover", "roundrobin"
35
+    |
36
+    */
37
+
38
+    'mailers' => [
39
+
40
+        'smtp' => [
41
+            'transport' => 'smtp',
42
+            'scheme' => env('MAIL_SCHEME'),
43
+            'url' => env('MAIL_URL'),
44
+            'host' => env('MAIL_HOST', '127.0.0.1'),
45
+            'port' => env('MAIL_PORT', 2525),
46
+            'username' => env('MAIL_USERNAME'),
47
+            'password' => env('MAIL_PASSWORD'),
48
+            'timeout' => null,
49
+            'local_domain' => env('MAIL_EHLO_DOMAIN', parse_url(env('APP_URL', 'http://localhost'), PHP_URL_HOST)),
50
+        ],
51
+
52
+        'ses' => [
53
+            'transport' => 'ses',
54
+        ],
55
+
56
+        'postmark' => [
57
+            'transport' => 'postmark',
58
+            // 'message_stream_id' => env('POSTMARK_MESSAGE_STREAM_ID'),
59
+            // 'client' => [
60
+            //     'timeout' => 5,
61
+            // ],
62
+        ],
63
+
64
+        'resend' => [
65
+            'transport' => 'resend',
66
+        ],
67
+
68
+        'sendmail' => [
69
+            'transport' => 'sendmail',
70
+            'path' => env('MAIL_SENDMAIL_PATH', '/usr/sbin/sendmail -bs -i'),
71
+        ],
72
+
73
+        'log' => [
74
+            'transport' => 'log',
75
+            'channel' => env('MAIL_LOG_CHANNEL'),
76
+        ],
77
+
78
+        'array' => [
79
+            'transport' => 'array',
80
+        ],
81
+
82
+        'failover' => [
83
+            'transport' => 'failover',
84
+            'mailers' => [
85
+                'smtp',
86
+                'log',
87
+            ],
88
+            'retry_after' => 60,
89
+        ],
90
+
91
+        'roundrobin' => [
92
+            'transport' => 'roundrobin',
93
+            'mailers' => [
94
+                'ses',
95
+                'postmark',
96
+            ],
97
+            'retry_after' => 60,
98
+        ],
99
+
100
+    ],
101
+
102
+    /*
103
+    |--------------------------------------------------------------------------
104
+    | Global "From" Address
105
+    |--------------------------------------------------------------------------
106
+    |
107
+    | You may wish for all emails sent by your application to be sent from
108
+    | the same address. Here you may specify a name and address that is
109
+    | used globally for all emails that are sent by your application.
110
+    |
111
+    */
112
+
113
+    'from' => [
114
+        'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'),
115
+        'name' => env('MAIL_FROM_NAME', 'Example'),
116
+    ],
117
+
118
+];

+ 202
- 0
config/permission.php View File

@@ -0,0 +1,202 @@
1
+<?php
2
+
3
+return [
4
+
5
+    'models' => [
6
+
7
+        /*
8
+         * When using the "HasPermissions" trait from this package, we need to know which
9
+         * Eloquent model should be used to retrieve your permissions. Of course, it
10
+         * is often just the "Permission" model but you may use whatever you like.
11
+         *
12
+         * The model you want to use as a Permission model needs to implement the
13
+         * `Spatie\Permission\Contracts\Permission` contract.
14
+         */
15
+
16
+        'permission' => Spatie\Permission\Models\Permission::class,
17
+
18
+        /*
19
+         * When using the "HasRoles" trait from this package, we need to know which
20
+         * Eloquent model should be used to retrieve your roles. Of course, it
21
+         * is often just the "Role" model but you may use whatever you like.
22
+         *
23
+         * The model you want to use as a Role model needs to implement the
24
+         * `Spatie\Permission\Contracts\Role` contract.
25
+         */
26
+
27
+        'role' => Spatie\Permission\Models\Role::class,
28
+
29
+    ],
30
+
31
+    'table_names' => [
32
+
33
+        /*
34
+         * When using the "HasRoles" trait from this package, we need to know which
35
+         * table should be used to retrieve your roles. We have chosen a basic
36
+         * default value but you may easily change it to any table you like.
37
+         */
38
+
39
+        'roles' => 'roles',
40
+
41
+        /*
42
+         * When using the "HasPermissions" trait from this package, we need to know which
43
+         * table should be used to retrieve your permissions. We have chosen a basic
44
+         * default value but you may easily change it to any table you like.
45
+         */
46
+
47
+        'permissions' => 'permissions',
48
+
49
+        /*
50
+         * When using the "HasPermissions" trait from this package, we need to know which
51
+         * table should be used to retrieve your models permissions. We have chosen a
52
+         * basic default value but you may easily change it to any table you like.
53
+         */
54
+
55
+        'model_has_permissions' => 'model_has_permissions',
56
+
57
+        /*
58
+         * When using the "HasRoles" trait from this package, we need to know which
59
+         * table should be used to retrieve your models roles. We have chosen a
60
+         * basic default value but you may easily change it to any table you like.
61
+         */
62
+
63
+        'model_has_roles' => 'model_has_roles',
64
+
65
+        /*
66
+         * When using the "HasRoles" trait from this package, we need to know which
67
+         * table should be used to retrieve your roles permissions. We have chosen a
68
+         * basic default value but you may easily change it to any table you like.
69
+         */
70
+
71
+        'role_has_permissions' => 'role_has_permissions',
72
+    ],
73
+
74
+    'column_names' => [
75
+        /*
76
+         * Change this if you want to name the related pivots other than defaults
77
+         */
78
+        'role_pivot_key' => null, // default 'role_id',
79
+        'permission_pivot_key' => null, // default 'permission_id',
80
+
81
+        /*
82
+         * Change this if you want to name the related model primary key other than
83
+         * `model_id`.
84
+         *
85
+         * For example, this would be nice if your primary keys are all UUIDs. In
86
+         * that case, name this `model_uuid`.
87
+         */
88
+
89
+        'model_morph_key' => 'model_id',
90
+
91
+        /*
92
+         * Change this if you want to use the teams feature and your related model's
93
+         * foreign key is other than `team_id`.
94
+         */
95
+
96
+        'team_foreign_key' => 'team_id',
97
+    ],
98
+
99
+    /*
100
+     * When set to true, the method for checking permissions will be registered on the gate.
101
+     * Set this to false if you want to implement custom logic for checking permissions.
102
+     */
103
+
104
+    'register_permission_check_method' => true,
105
+
106
+    /*
107
+     * When set to true, Laravel\Octane\Events\OperationTerminated event listener will be registered
108
+     * this will refresh permissions on every TickTerminated, TaskTerminated and RequestTerminated
109
+     * NOTE: This should not be needed in most cases, but an Octane/Vapor combination benefited from it.
110
+     */
111
+    'register_octane_reset_listener' => false,
112
+
113
+    /*
114
+     * Events will fire when a role or permission is assigned/unassigned:
115
+     * \Spatie\Permission\Events\RoleAttached
116
+     * \Spatie\Permission\Events\RoleDetached
117
+     * \Spatie\Permission\Events\PermissionAttached
118
+     * \Spatie\Permission\Events\PermissionDetached
119
+     *
120
+     * To enable, set to true, and then create listeners to watch these events.
121
+     */
122
+    'events_enabled' => false,
123
+
124
+    /*
125
+     * Teams Feature.
126
+     * When set to true the package implements teams using the 'team_foreign_key'.
127
+     * If you want the migrations to register the 'team_foreign_key', you must
128
+     * set this to true before doing the migration.
129
+     * If you already did the migration then you must make a new migration to also
130
+     * add 'team_foreign_key' to 'roles', 'model_has_roles', and 'model_has_permissions'
131
+     * (view the latest version of this package's migration file)
132
+     */
133
+
134
+    'teams' => false,
135
+
136
+    /*
137
+     * The class to use to resolve the permissions team id
138
+     */
139
+    'team_resolver' => \Spatie\Permission\DefaultTeamResolver::class,
140
+
141
+    /*
142
+     * Passport Client Credentials Grant
143
+     * When set to true the package will use Passports Client to check permissions
144
+     */
145
+
146
+    'use_passport_client_credentials' => false,
147
+
148
+    /*
149
+     * When set to true, the required permission names are added to exception messages.
150
+     * This could be considered an information leak in some contexts, so the default
151
+     * setting is false here for optimum safety.
152
+     */
153
+
154
+    'display_permission_in_exception' => false,
155
+
156
+    /*
157
+     * When set to true, the required role names are added to exception messages.
158
+     * This could be considered an information leak in some contexts, so the default
159
+     * setting is false here for optimum safety.
160
+     */
161
+
162
+    'display_role_in_exception' => false,
163
+
164
+    /*
165
+     * By default wildcard permission lookups are disabled.
166
+     * See documentation to understand supported syntax.
167
+     */
168
+
169
+    'enable_wildcard_permission' => false,
170
+
171
+    /*
172
+     * The class to use for interpreting wildcard permissions.
173
+     * If you need to modify delimiters, override the class and specify its name here.
174
+     */
175
+    // 'wildcard_permission' => Spatie\Permission\WildcardPermission::class,
176
+
177
+    /* Cache-specific settings */
178
+
179
+    'cache' => [
180
+
181
+        /*
182
+         * By default all permissions are cached for 24 hours to speed up performance.
183
+         * When permissions or roles are updated the cache is flushed automatically.
184
+         */
185
+
186
+        'expiration_time' => \DateInterval::createFromDateString('24 hours'),
187
+
188
+        /*
189
+         * The cache key used to store all permissions.
190
+         */
191
+
192
+        'key' => 'spatie.permission.cache',
193
+
194
+        /*
195
+         * You may optionally indicate a specific cache driver to use for permission and
196
+         * role caching using any of the `store` drivers listed in the cache.php config
197
+         * file. Using 'default' here means to use the `default` set in cache.php.
198
+         */
199
+
200
+        'store' => 'default',
201
+    ],
202
+];

+ 112
- 0
config/queue.php View File

@@ -0,0 +1,112 @@
1
+<?php
2
+
3
+return [
4
+
5
+    /*
6
+    |--------------------------------------------------------------------------
7
+    | Default Queue Connection Name
8
+    |--------------------------------------------------------------------------
9
+    |
10
+    | Laravel's queue supports a variety of backends via a single, unified
11
+    | API, giving you convenient access to each backend using identical
12
+    | syntax for each. The default queue connection is defined below.
13
+    |
14
+    */
15
+
16
+    'default' => env('QUEUE_CONNECTION', 'database'),
17
+
18
+    /*
19
+    |--------------------------------------------------------------------------
20
+    | Queue Connections
21
+    |--------------------------------------------------------------------------
22
+    |
23
+    | Here you may configure the connection options for every queue backend
24
+    | used by your application. An example configuration is provided for
25
+    | each backend supported by Laravel. You're also free to add more.
26
+    |
27
+    | Drivers: "sync", "database", "beanstalkd", "sqs", "redis", "null"
28
+    |
29
+    */
30
+
31
+    'connections' => [
32
+
33
+        'sync' => [
34
+            'driver' => 'sync',
35
+        ],
36
+
37
+        'database' => [
38
+            'driver' => 'database',
39
+            'connection' => env('DB_QUEUE_CONNECTION'),
40
+            'table' => env('DB_QUEUE_TABLE', 'jobs'),
41
+            'queue' => env('DB_QUEUE', 'default'),
42
+            'retry_after' => (int) env('DB_QUEUE_RETRY_AFTER', 90),
43
+            'after_commit' => false,
44
+        ],
45
+
46
+        'beanstalkd' => [
47
+            'driver' => 'beanstalkd',
48
+            'host' => env('BEANSTALKD_QUEUE_HOST', 'localhost'),
49
+            'queue' => env('BEANSTALKD_QUEUE', 'default'),
50
+            'retry_after' => (int) env('BEANSTALKD_QUEUE_RETRY_AFTER', 90),
51
+            'block_for' => 0,
52
+            'after_commit' => false,
53
+        ],
54
+
55
+        'sqs' => [
56
+            'driver' => 'sqs',
57
+            'key' => env('AWS_ACCESS_KEY_ID'),
58
+            'secret' => env('AWS_SECRET_ACCESS_KEY'),
59
+            'prefix' => env('SQS_PREFIX', 'https://sqs.us-east-1.amazonaws.com/your-account-id'),
60
+            'queue' => env('SQS_QUEUE', 'default'),
61
+            'suffix' => env('SQS_SUFFIX'),
62
+            'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
63
+            'after_commit' => false,
64
+        ],
65
+
66
+        'redis' => [
67
+            'driver' => 'redis',
68
+            'connection' => env('REDIS_QUEUE_CONNECTION', 'default'),
69
+            'queue' => env('REDIS_QUEUE', 'default'),
70
+            'retry_after' => (int) env('REDIS_QUEUE_RETRY_AFTER', 90),
71
+            'block_for' => null,
72
+            'after_commit' => false,
73
+        ],
74
+
75
+    ],
76
+
77
+    /*
78
+    |--------------------------------------------------------------------------
79
+    | Job Batching
80
+    |--------------------------------------------------------------------------
81
+    |
82
+    | The following options configure the database and table that store job
83
+    | batching information. These options can be updated to any database
84
+    | connection and table which has been defined by your application.
85
+    |
86
+    */
87
+
88
+    'batching' => [
89
+        'database' => env('DB_CONNECTION', 'sqlite'),
90
+        'table' => 'job_batches',
91
+    ],
92
+
93
+    /*
94
+    |--------------------------------------------------------------------------
95
+    | Failed Queue Jobs
96
+    |--------------------------------------------------------------------------
97
+    |
98
+    | These options configure the behavior of failed queue job logging so you
99
+    | can control how and where failed jobs are stored. Laravel ships with
100
+    | support for storing failed jobs in a simple file or in a database.
101
+    |
102
+    | Supported drivers: "database-uuids", "dynamodb", "file", "null"
103
+    |
104
+    */
105
+
106
+    'failed' => [
107
+        'driver' => env('QUEUE_FAILED_DRIVER', 'database-uuids'),
108
+        'database' => env('DB_CONNECTION', 'sqlite'),
109
+        'table' => 'failed_jobs',
110
+    ],
111
+
112
+];

+ 38
- 0
config/services.php View File

@@ -0,0 +1,38 @@
1
+<?php
2
+
3
+return [
4
+
5
+    /*
6
+    |--------------------------------------------------------------------------
7
+    | Third Party Services
8
+    |--------------------------------------------------------------------------
9
+    |
10
+    | This file is for storing the credentials for third party services such
11
+    | as Mailgun, Postmark, AWS and more. This file provides the de facto
12
+    | location for this type of information, allowing packages to have
13
+    | a conventional file to locate the various service credentials.
14
+    |
15
+    */
16
+
17
+    'postmark' => [
18
+        'token' => env('POSTMARK_TOKEN'),
19
+    ],
20
+
21
+    'ses' => [
22
+        'key' => env('AWS_ACCESS_KEY_ID'),
23
+        'secret' => env('AWS_SECRET_ACCESS_KEY'),
24
+        'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
25
+    ],
26
+
27
+    'resend' => [
28
+        'key' => env('RESEND_KEY'),
29
+    ],
30
+
31
+    'slack' => [
32
+        'notifications' => [
33
+            'bot_user_oauth_token' => env('SLACK_BOT_USER_OAUTH_TOKEN'),
34
+            'channel' => env('SLACK_BOT_USER_DEFAULT_CHANNEL'),
35
+        ],
36
+    ],
37
+
38
+];

+ 217
- 0
config/session.php View File

@@ -0,0 +1,217 @@
1
+<?php
2
+
3
+use Illuminate\Support\Str;
4
+
5
+return [
6
+
7
+    /*
8
+    |--------------------------------------------------------------------------
9
+    | Default Session Driver
10
+    |--------------------------------------------------------------------------
11
+    |
12
+    | This option determines the default session driver that is utilized for
13
+    | incoming requests. Laravel supports a variety of storage options to
14
+    | persist session data. Database storage is a great default choice.
15
+    |
16
+    | Supported: "file", "cookie", "database", "memcached",
17
+    |            "redis", "dynamodb", "array"
18
+    |
19
+    */
20
+
21
+    'driver' => env('SESSION_DRIVER', 'database'),
22
+
23
+    /*
24
+    |--------------------------------------------------------------------------
25
+    | Session Lifetime
26
+    |--------------------------------------------------------------------------
27
+    |
28
+    | Here you may specify the number of minutes that you wish the session
29
+    | to be allowed to remain idle before it expires. If you want them
30
+    | to expire immediately when the browser is closed then you may
31
+    | indicate that via the expire_on_close configuration option.
32
+    |
33
+    */
34
+
35
+    'lifetime' => (int) env('SESSION_LIFETIME', 120),
36
+
37
+    'expire_on_close' => env('SESSION_EXPIRE_ON_CLOSE', false),
38
+
39
+    /*
40
+    |--------------------------------------------------------------------------
41
+    | Session Encryption
42
+    |--------------------------------------------------------------------------
43
+    |
44
+    | This option allows you to easily specify that all of your session data
45
+    | should be encrypted before it's stored. All encryption is performed
46
+    | automatically by Laravel and you may use the session like normal.
47
+    |
48
+    */
49
+
50
+    'encrypt' => env('SESSION_ENCRYPT', false),
51
+
52
+    /*
53
+    |--------------------------------------------------------------------------
54
+    | Session File Location
55
+    |--------------------------------------------------------------------------
56
+    |
57
+    | When utilizing the "file" session driver, the session files are placed
58
+    | on disk. The default storage location is defined here; however, you
59
+    | are free to provide another location where they should be stored.
60
+    |
61
+    */
62
+
63
+    'files' => storage_path('framework/sessions'),
64
+
65
+    /*
66
+    |--------------------------------------------------------------------------
67
+    | Session Database Connection
68
+    |--------------------------------------------------------------------------
69
+    |
70
+    | When using the "database" or "redis" session drivers, you may specify a
71
+    | connection that should be used to manage these sessions. This should
72
+    | correspond to a connection in your database configuration options.
73
+    |
74
+    */
75
+
76
+    'connection' => env('SESSION_CONNECTION'),
77
+
78
+    /*
79
+    |--------------------------------------------------------------------------
80
+    | Session Database Table
81
+    |--------------------------------------------------------------------------
82
+    |
83
+    | When using the "database" session driver, you may specify the table to
84
+    | be used to store sessions. Of course, a sensible default is defined
85
+    | for you; however, you're welcome to change this to another table.
86
+    |
87
+    */
88
+
89
+    'table' => env('SESSION_TABLE', 'sessions'),
90
+
91
+    /*
92
+    |--------------------------------------------------------------------------
93
+    | Session Cache Store
94
+    |--------------------------------------------------------------------------
95
+    |
96
+    | When using one of the framework's cache driven session backends, you may
97
+    | define the cache store which should be used to store the session data
98
+    | between requests. This must match one of your defined cache stores.
99
+    |
100
+    | Affects: "apc", "dynamodb", "memcached", "redis"
101
+    |
102
+    */
103
+
104
+    'store' => env('SESSION_STORE'),
105
+
106
+    /*
107
+    |--------------------------------------------------------------------------
108
+    | Session Sweeping Lottery
109
+    |--------------------------------------------------------------------------
110
+    |
111
+    | Some session drivers must manually sweep their storage location to get
112
+    | rid of old sessions from storage. Here are the chances that it will
113
+    | happen on a given request. By default, the odds are 2 out of 100.
114
+    |
115
+    */
116
+
117
+    'lottery' => [2, 100],
118
+
119
+    /*
120
+    |--------------------------------------------------------------------------
121
+    | Session Cookie Name
122
+    |--------------------------------------------------------------------------
123
+    |
124
+    | Here you may change the name of the session cookie that is created by
125
+    | the framework. Typically, you should not need to change this value
126
+    | since doing so does not grant a meaningful security improvement.
127
+    |
128
+    */
129
+
130
+    'cookie' => env(
131
+        'SESSION_COOKIE',
132
+        Str::slug(env('APP_NAME', 'laravel'), '_').'_session'
133
+    ),
134
+
135
+    /*
136
+    |--------------------------------------------------------------------------
137
+    | Session Cookie Path
138
+    |--------------------------------------------------------------------------
139
+    |
140
+    | The session cookie path determines the path for which the cookie will
141
+    | be regarded as available. Typically, this will be the root path of
142
+    | your application, but you're free to change this when necessary.
143
+    |
144
+    */
145
+
146
+    'path' => env('SESSION_PATH', '/'),
147
+
148
+    /*
149
+    |--------------------------------------------------------------------------
150
+    | Session Cookie Domain
151
+    |--------------------------------------------------------------------------
152
+    |
153
+    | This value determines the domain and subdomains the session cookie is
154
+    | available to. By default, the cookie will be available to the root
155
+    | domain and all subdomains. Typically, this shouldn't be changed.
156
+    |
157
+    */
158
+
159
+    'domain' => env('SESSION_DOMAIN'),
160
+
161
+    /*
162
+    |--------------------------------------------------------------------------
163
+    | HTTPS Only Cookies
164
+    |--------------------------------------------------------------------------
165
+    |
166
+    | By setting this option to true, session cookies will only be sent back
167
+    | to the server if the browser has a HTTPS connection. This will keep
168
+    | the cookie from being sent to you when it can't be done securely.
169
+    |
170
+    */
171
+
172
+    'secure' => env('SESSION_SECURE_COOKIE'),
173
+
174
+    /*
175
+    |--------------------------------------------------------------------------
176
+    | HTTP Access Only
177
+    |--------------------------------------------------------------------------
178
+    |
179
+    | Setting this value to true will prevent JavaScript from accessing the
180
+    | value of the cookie and the cookie will only be accessible through
181
+    | the HTTP protocol. It's unlikely you should disable this option.
182
+    |
183
+    */
184
+
185
+    'http_only' => env('SESSION_HTTP_ONLY', true),
186
+
187
+    /*
188
+    |--------------------------------------------------------------------------
189
+    | Same-Site Cookies
190
+    |--------------------------------------------------------------------------
191
+    |
192
+    | This option determines how your cookies behave when cross-site requests
193
+    | take place, and can be used to mitigate CSRF attacks. By default, we
194
+    | will set this value to "lax" to permit secure cross-site requests.
195
+    |
196
+    | See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#samesitesamesite-value
197
+    |
198
+    | Supported: "lax", "strict", "none", null
199
+    |
200
+    */
201
+
202
+    'same_site' => env('SESSION_SAME_SITE', 'lax'),
203
+
204
+    /*
205
+    |--------------------------------------------------------------------------
206
+    | Partitioned Cookies
207
+    |--------------------------------------------------------------------------
208
+    |
209
+    | Setting this value to true will tie the cookie to the top-level site for
210
+    | a cross-site context. Partitioned cookies are accepted by the browser
211
+    | when flagged "secure" and the Same-Site attribute is set to "none".
212
+    |
213
+    */
214
+
215
+    'partitioned' => env('SESSION_PARTITIONED_COOKIE', false),
216
+
217
+];

+ 1
- 0
database/.gitignore View File

@@ -0,0 +1 @@
1
+*.sqlite*

+ 44
- 0
database/factories/UserFactory.php View File

@@ -0,0 +1,44 @@
1
+<?php
2
+
3
+namespace Database\Factories;
4
+
5
+use Illuminate\Database\Eloquent\Factories\Factory;
6
+use Illuminate\Support\Facades\Hash;
7
+use Illuminate\Support\Str;
8
+
9
+/**
10
+ * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\User>
11
+ */
12
+class UserFactory extends Factory
13
+{
14
+    /**
15
+     * The current password being used by the factory.
16
+     */
17
+    protected static ?string $password;
18
+
19
+    /**
20
+     * Define the model's default state.
21
+     *
22
+     * @return array<string, mixed>
23
+     */
24
+    public function definition(): array
25
+    {
26
+        return [
27
+            'name' => fake()->name(),
28
+            'email' => fake()->unique()->safeEmail(),
29
+            'email_verified_at' => now(),
30
+            'password' => static::$password ??= Hash::make('password'),
31
+            'remember_token' => Str::random(10),
32
+        ];
33
+    }
34
+
35
+    /**
36
+     * Indicate that the model's email address should be unverified.
37
+     */
38
+    public function unverified(): static
39
+    {
40
+        return $this->state(fn (array $attributes) => [
41
+            'email_verified_at' => null,
42
+        ]);
43
+    }
44
+}

+ 49
- 0
database/migrations/0001_01_01_000000_create_users_table.php View File

@@ -0,0 +1,49 @@
1
+<?php
2
+
3
+use Illuminate\Database\Migrations\Migration;
4
+use Illuminate\Database\Schema\Blueprint;
5
+use Illuminate\Support\Facades\Schema;
6
+
7
+return new class extends Migration
8
+{
9
+    /**
10
+     * Run the migrations.
11
+     */
12
+    public function up(): void
13
+    {
14
+        Schema::create('users', function (Blueprint $table) {
15
+            $table->id();
16
+            $table->string('name');
17
+            $table->string('email')->unique();
18
+            $table->timestamp('email_verified_at')->nullable();
19
+            $table->string('password');
20
+            $table->rememberToken();
21
+            $table->timestamps();
22
+        });
23
+
24
+        Schema::create('password_reset_tokens', function (Blueprint $table) {
25
+            $table->string('email')->primary();
26
+            $table->string('token');
27
+            $table->timestamp('created_at')->nullable();
28
+        });
29
+
30
+        Schema::create('sessions', function (Blueprint $table) {
31
+            $table->string('id')->primary();
32
+            $table->foreignId('user_id')->nullable()->index();
33
+            $table->string('ip_address', 45)->nullable();
34
+            $table->text('user_agent')->nullable();
35
+            $table->longText('payload');
36
+            $table->integer('last_activity')->index();
37
+        });
38
+    }
39
+
40
+    /**
41
+     * Reverse the migrations.
42
+     */
43
+    public function down(): void
44
+    {
45
+        Schema::dropIfExists('users');
46
+        Schema::dropIfExists('password_reset_tokens');
47
+        Schema::dropIfExists('sessions');
48
+    }
49
+};

+ 35
- 0
database/migrations/0001_01_01_000001_create_cache_table.php View File

@@ -0,0 +1,35 @@
1
+<?php
2
+
3
+use Illuminate\Database\Migrations\Migration;
4
+use Illuminate\Database\Schema\Blueprint;
5
+use Illuminate\Support\Facades\Schema;
6
+
7
+return new class extends Migration
8
+{
9
+    /**
10
+     * Run the migrations.
11
+     */
12
+    public function up(): void
13
+    {
14
+        Schema::create('cache', function (Blueprint $table) {
15
+            $table->string('key')->primary();
16
+            $table->mediumText('value');
17
+            $table->integer('expiration');
18
+        });
19
+
20
+        Schema::create('cache_locks', function (Blueprint $table) {
21
+            $table->string('key')->primary();
22
+            $table->string('owner');
23
+            $table->integer('expiration');
24
+        });
25
+    }
26
+
27
+    /**
28
+     * Reverse the migrations.
29
+     */
30
+    public function down(): void
31
+    {
32
+        Schema::dropIfExists('cache');
33
+        Schema::dropIfExists('cache_locks');
34
+    }
35
+};

+ 57
- 0
database/migrations/0001_01_01_000002_create_jobs_table.php View File

@@ -0,0 +1,57 @@
1
+<?php
2
+
3
+use Illuminate\Database\Migrations\Migration;
4
+use Illuminate\Database\Schema\Blueprint;
5
+use Illuminate\Support\Facades\Schema;
6
+
7
+return new class extends Migration
8
+{
9
+    /**
10
+     * Run the migrations.
11
+     */
12
+    public function up(): void
13
+    {
14
+        Schema::create('jobs', function (Blueprint $table) {
15
+            $table->id();
16
+            $table->string('queue')->index();
17
+            $table->longText('payload');
18
+            $table->unsignedTinyInteger('attempts');
19
+            $table->unsignedInteger('reserved_at')->nullable();
20
+            $table->unsignedInteger('available_at');
21
+            $table->unsignedInteger('created_at');
22
+        });
23
+
24
+        Schema::create('job_batches', function (Blueprint $table) {
25
+            $table->string('id')->primary();
26
+            $table->string('name');
27
+            $table->integer('total_jobs');
28
+            $table->integer('pending_jobs');
29
+            $table->integer('failed_jobs');
30
+            $table->longText('failed_job_ids');
31
+            $table->mediumText('options')->nullable();
32
+            $table->integer('cancelled_at')->nullable();
33
+            $table->integer('created_at');
34
+            $table->integer('finished_at')->nullable();
35
+        });
36
+
37
+        Schema::create('failed_jobs', function (Blueprint $table) {
38
+            $table->id();
39
+            $table->string('uuid')->unique();
40
+            $table->text('connection');
41
+            $table->text('queue');
42
+            $table->longText('payload');
43
+            $table->longText('exception');
44
+            $table->timestamp('failed_at')->useCurrent();
45
+        });
46
+    }
47
+
48
+    /**
49
+     * Reverse the migrations.
50
+     */
51
+    public function down(): void
52
+    {
53
+        Schema::dropIfExists('jobs');
54
+        Schema::dropIfExists('job_batches');
55
+        Schema::dropIfExists('failed_jobs');
56
+    }
57
+};

+ 34
- 0
database/migrations/2025_05_20_043731_create_news_categories_table.php View File

@@ -0,0 +1,34 @@
1
+<?php
2
+
3
+use Illuminate\Database\Migrations\Migration;
4
+use Illuminate\Database\Schema\Blueprint;
5
+use Illuminate\Support\Facades\Schema;
6
+
7
+return new class extends Migration
8
+{
9
+    /**
10
+     * Run the migrations.
11
+     */
12
+    public function up(): void
13
+    {
14
+        Schema::disableForeignKeyConstraints();
15
+
16
+        Schema::create('news_categories', function (Blueprint $table) {
17
+            $table->id();
18
+            $table->json('name');
19
+            $table->unsignedInteger('order')->default(0)->comment("排序");
20
+            $table->timestamps();
21
+            $table->softDeletes();
22
+        });
23
+
24
+        Schema::enableForeignKeyConstraints();
25
+    }
26
+
27
+    /**
28
+     * Reverse the migrations.
29
+     */
30
+    public function down(): void
31
+    {
32
+        Schema::dropIfExists('news_categories');
33
+    }
34
+};

+ 47
- 0
database/migrations/2025_05_20_043733_create_news_table.php View File

@@ -0,0 +1,47 @@
1
+<?php
2
+
3
+use Illuminate\Database\Migrations\Migration;
4
+use Illuminate\Database\Schema\Blueprint;
5
+use Illuminate\Support\Facades\Schema;
6
+
7
+return new class extends Migration
8
+{
9
+    /**
10
+     * Run the migrations.
11
+     */
12
+    public function up(): void
13
+    {
14
+        Schema::disableForeignKeyConstraints();
15
+
16
+        Schema::create('news', function (Blueprint $table) {
17
+            $table->id();
18
+            $table->unsignedBigInteger('news_category_id')->index();
19
+            $table->foreign('news_category_id')->references('id')->on('news_categories')->comment('文章類別');
20
+            $table->string('news_img_pc')->nullable()->comment('列表圖 PC');
21
+            $table->string('news_img_mobile')->nullable()->comment('列表圖 MOBILE');
22
+            $table->json('title')->comment("標題");
23
+            $table->json('description')->comment("短文");
24
+            $table->json('written_by')->comment("編輯人");
25
+            $table->dateTime('post_date')->comment("文章發布日");
26
+            $table->string('meta_img')->nullable()->comment('seo image');
27
+            $table->json("meta_title")->nullable()->comment("seo title");
28
+            $table->json("meta_keyword")->nullable()->comment("seo keyword");
29
+            $table->json("meta_description")->nullable()->comment("seo description");
30
+            $table->boolean('on_top')->default(0)->comment("置頂");
31
+            $table->boolean('visible')->default(1)->comment("顯示/隱藏");
32
+            $table->unsignedInteger('order')->default(0)->comment("排序");
33
+            $table->timestamps();
34
+            $table->softDeletes();
35
+        });
36
+
37
+        Schema::enableForeignKeyConstraints();
38
+    }
39
+
40
+    /**
41
+     * Reverse the migrations.
42
+     */
43
+    public function down(): void
44
+    {
45
+        Schema::dropIfExists('news');
46
+    }
47
+};

+ 41
- 0
database/migrations/2025_05_20_043735_create_news_paragraphs_table.php View File

@@ -0,0 +1,41 @@
1
+<?php
2
+
3
+use Illuminate\Database\Migrations\Migration;
4
+use Illuminate\Database\Schema\Blueprint;
5
+use Illuminate\Support\Facades\Schema;
6
+
7
+return new class extends Migration
8
+{
9
+    /**
10
+     * Run the migrations.
11
+     */
12
+    public function up(): void
13
+    {
14
+        Schema::disableForeignKeyConstraints();
15
+
16
+        Schema::create('news_paragraphs', function (Blueprint $table) {
17
+            $table->id();
18
+            $table->unsignedBigInteger('news_id')->index();
19
+            $table->foreign('news_id')->references('id')->on('news')->comment("關聯文章");
20
+            $table->unsignedTinyInteger('paragraph_type')->comment('段落類型 1.image 2.text');
21
+            $table->string('image_url')->nullable()->comment("圖片網址");
22
+            $table->json('image_alt')->nullable()->comment("圖片註釋");
23
+            $table->json('text_content')->nullable()->comment("文字段落");
24
+            $table->integer("video_type")->nullable();
25
+            $table->string("video_img")->nullable();
26
+            $table->string("link")->nullable();
27
+            $table->string("video_url")->nullable();
28
+            $table->unsignedInteger('order')->default(0)->comment("排序");
29
+        });
30
+
31
+        Schema::enableForeignKeyConstraints();
32
+    }
33
+
34
+    /**
35
+     * Reverse the migrations.
36
+     */
37
+    public function down(): void
38
+    {
39
+        Schema::dropIfExists('news_paragraphs');
40
+    }
41
+};

+ 35
- 0
database/migrations/2025_05_20_043736_create_news_photos_table.php View File

@@ -0,0 +1,35 @@
1
+<?php
2
+
3
+use Illuminate\Database\Migrations\Migration;
4
+use Illuminate\Database\Schema\Blueprint;
5
+use Illuminate\Support\Facades\Schema;
6
+
7
+return new class extends Migration
8
+{
9
+    /**
10
+     * Run the migrations.
11
+     */
12
+    public function up(): void
13
+    {
14
+        Schema::disableForeignKeyConstraints();
15
+
16
+        Schema::create('news_photos', function (Blueprint $table) {
17
+            $table->id();
18
+            $table->unsignedBigInteger('news_id')->index();
19
+            $table->foreign('news_id')->references('id')->on('news')->comment("關聯文章");
20
+            $table->string('image_url')->nullable()->comment("圖片網址");
21
+            $table->json('image_alt')->nullable()->comment("圖片註釋");
22
+            $table->unsignedInteger('order')->default(0)->comment("排序");
23
+        });
24
+
25
+        Schema::enableForeignKeyConstraints();
26
+    }
27
+
28
+    /**
29
+     * Reverse the migrations.
30
+     */
31
+    public function down(): void
32
+    {
33
+        Schema::dropIfExists('news_photos');
34
+    }
35
+};

+ 136
- 0
database/migrations/2025_05_20_073544_create_permission_tables.php View File

@@ -0,0 +1,136 @@
1
+<?php
2
+
3
+use Illuminate\Database\Migrations\Migration;
4
+use Illuminate\Database\Schema\Blueprint;
5
+use Illuminate\Support\Facades\Schema;
6
+
7
+return new class extends Migration
8
+{
9
+    /**
10
+     * Run the migrations.
11
+     */
12
+    public function up(): void
13
+    {
14
+        $teams = config('permission.teams');
15
+        $tableNames = config('permission.table_names');
16
+        $columnNames = config('permission.column_names');
17
+        $pivotRole = $columnNames['role_pivot_key'] ?? 'role_id';
18
+        $pivotPermission = $columnNames['permission_pivot_key'] ?? 'permission_id';
19
+
20
+        throw_if(empty($tableNames), new Exception('Error: config/permission.php not loaded. Run [php artisan config:clear] and try again.'));
21
+        throw_if($teams && empty($columnNames['team_foreign_key'] ?? null), new Exception('Error: team_foreign_key on config/permission.php not loaded. Run [php artisan config:clear] and try again.'));
22
+
23
+        Schema::create($tableNames['permissions'], static function (Blueprint $table) {
24
+            // $table->engine('InnoDB');
25
+            $table->bigIncrements('id'); // permission id
26
+            $table->string('name');       // For MyISAM use string('name', 225); // (or 166 for InnoDB with Redundant/Compact row format)
27
+            $table->string('guard_name'); // For MyISAM use string('guard_name', 25);
28
+            $table->timestamps();
29
+
30
+            $table->unique(['name', 'guard_name']);
31
+        });
32
+
33
+        Schema::create($tableNames['roles'], static function (Blueprint $table) use ($teams, $columnNames) {
34
+            // $table->engine('InnoDB');
35
+            $table->bigIncrements('id'); // role id
36
+            if ($teams || config('permission.testing')) { // permission.testing is a fix for sqlite testing
37
+                $table->unsignedBigInteger($columnNames['team_foreign_key'])->nullable();
38
+                $table->index($columnNames['team_foreign_key'], 'roles_team_foreign_key_index');
39
+            }
40
+            $table->string('name');       // For MyISAM use string('name', 225); // (or 166 for InnoDB with Redundant/Compact row format)
41
+            $table->string('guard_name'); // For MyISAM use string('guard_name', 25);
42
+            $table->timestamps();
43
+            if ($teams || config('permission.testing')) {
44
+                $table->unique([$columnNames['team_foreign_key'], 'name', 'guard_name']);
45
+            } else {
46
+                $table->unique(['name', 'guard_name']);
47
+            }
48
+        });
49
+
50
+        Schema::create($tableNames['model_has_permissions'], static function (Blueprint $table) use ($tableNames, $columnNames, $pivotPermission, $teams) {
51
+            $table->unsignedBigInteger($pivotPermission);
52
+
53
+            $table->string('model_type');
54
+            $table->unsignedBigInteger($columnNames['model_morph_key']);
55
+            $table->index([$columnNames['model_morph_key'], 'model_type'], 'model_has_permissions_model_id_model_type_index');
56
+
57
+            $table->foreign($pivotPermission)
58
+                ->references('id') // permission id
59
+                ->on($tableNames['permissions'])
60
+                ->onDelete('cascade');
61
+            if ($teams) {
62
+                $table->unsignedBigInteger($columnNames['team_foreign_key']);
63
+                $table->index($columnNames['team_foreign_key'], 'model_has_permissions_team_foreign_key_index');
64
+
65
+                $table->primary([$columnNames['team_foreign_key'], $pivotPermission, $columnNames['model_morph_key'], 'model_type'],
66
+                    'model_has_permissions_permission_model_type_primary');
67
+            } else {
68
+                $table->primary([$pivotPermission, $columnNames['model_morph_key'], 'model_type'],
69
+                    'model_has_permissions_permission_model_type_primary');
70
+            }
71
+
72
+        });
73
+
74
+        Schema::create($tableNames['model_has_roles'], static function (Blueprint $table) use ($tableNames, $columnNames, $pivotRole, $teams) {
75
+            $table->unsignedBigInteger($pivotRole);
76
+
77
+            $table->string('model_type');
78
+            $table->unsignedBigInteger($columnNames['model_morph_key']);
79
+            $table->index([$columnNames['model_morph_key'], 'model_type'], 'model_has_roles_model_id_model_type_index');
80
+
81
+            $table->foreign($pivotRole)
82
+                ->references('id') // role id
83
+                ->on($tableNames['roles'])
84
+                ->onDelete('cascade');
85
+            if ($teams) {
86
+                $table->unsignedBigInteger($columnNames['team_foreign_key']);
87
+                $table->index($columnNames['team_foreign_key'], 'model_has_roles_team_foreign_key_index');
88
+
89
+                $table->primary([$columnNames['team_foreign_key'], $pivotRole, $columnNames['model_morph_key'], 'model_type'],
90
+                    'model_has_roles_role_model_type_primary');
91
+            } else {
92
+                $table->primary([$pivotRole, $columnNames['model_morph_key'], 'model_type'],
93
+                    'model_has_roles_role_model_type_primary');
94
+            }
95
+        });
96
+
97
+        Schema::create($tableNames['role_has_permissions'], static function (Blueprint $table) use ($tableNames, $pivotRole, $pivotPermission) {
98
+            $table->unsignedBigInteger($pivotPermission);
99
+            $table->unsignedBigInteger($pivotRole);
100
+
101
+            $table->foreign($pivotPermission)
102
+                ->references('id') // permission id
103
+                ->on($tableNames['permissions'])
104
+                ->onDelete('cascade');
105
+
106
+            $table->foreign($pivotRole)
107
+                ->references('id') // role id
108
+                ->on($tableNames['roles'])
109
+                ->onDelete('cascade');
110
+
111
+            $table->primary([$pivotPermission, $pivotRole], 'role_has_permissions_permission_id_role_id_primary');
112
+        });
113
+
114
+        app('cache')
115
+            ->store(config('permission.cache.store') != 'default' ? config('permission.cache.store') : null)
116
+            ->forget(config('permission.cache.key'));
117
+    }
118
+
119
+    /**
120
+     * Reverse the migrations.
121
+     */
122
+    public function down(): void
123
+    {
124
+        $tableNames = config('permission.table_names');
125
+
126
+        if (empty($tableNames)) {
127
+            throw new \Exception('Error: config/permission.php not found and defaults could not be merged. Please publish the package configuration before proceeding, or drop the tables manually.');
128
+        }
129
+
130
+        Schema::drop($tableNames['role_has_permissions']);
131
+        Schema::drop($tableNames['model_has_roles']);
132
+        Schema::drop($tableNames['model_has_permissions']);
133
+        Schema::drop($tableNames['roles']);
134
+        Schema::drop($tableNames['permissions']);
135
+    }
136
+};

+ 31
- 0
database/migrations/2025_05_20_090304_create_notifications_table.php View File

@@ -0,0 +1,31 @@
1
+<?php
2
+
3
+use Illuminate\Database\Migrations\Migration;
4
+use Illuminate\Database\Schema\Blueprint;
5
+use Illuminate\Support\Facades\Schema;
6
+
7
+return new class extends Migration
8
+{
9
+    /**
10
+     * Run the migrations.
11
+     */
12
+    public function up(): void
13
+    {
14
+        Schema::create('notifications', function (Blueprint $table) {
15
+            $table->uuid('id')->primary();
16
+            $table->string('type');
17
+            $table->morphs('notifiable');
18
+            $table->text('data');
19
+            $table->timestamp('read_at')->nullable();
20
+            $table->timestamps();
21
+        });
22
+    }
23
+
24
+    /**
25
+     * Reverse the migrations.
26
+     */
27
+    public function down(): void
28
+    {
29
+        Schema::dropIfExists('notifications');
30
+    }
31
+};

+ 23
- 0
database/seeders/DatabaseSeeder.php View File

@@ -0,0 +1,23 @@
1
+<?php
2
+
3
+namespace Database\Seeders;
4
+
5
+use App\Models\User;
6
+// use Illuminate\Database\Console\Seeds\WithoutModelEvents;
7
+use Illuminate\Database\Seeder;
8
+
9
+class DatabaseSeeder extends Seeder
10
+{
11
+    /**
12
+     * Seed the application's database.
13
+     */
14
+    public function run(): void
15
+    {
16
+        // User::factory(10)->create();
17
+
18
+        User::factory()->create([
19
+            'name' => 'Test User',
20
+            'email' => 'test@example.com',
21
+        ]);
22
+    }
23
+}

+ 12
- 0
lang/vendor/filament-panels/ar/global-search.php View File

@@ -0,0 +1,12 @@
1
+<?php
2
+
3
+return [
4
+
5
+    'field' => [
6
+        'label' => 'بحث عام',
7
+        'placeholder' => 'بحث',
8
+    ],
9
+
10
+    'no_results_message' => 'لم يتم العثور على نتائج عن البحث.',
11
+
12
+];

+ 63
- 0
lang/vendor/filament-panels/ar/layout.php View File

@@ -0,0 +1,63 @@
1
+<?php
2
+
3
+return [
4
+
5
+    'direction' => 'rtl',
6
+
7
+    'actions' => [
8
+
9
+        'billing' => [
10
+            'label' => 'إدارة الاشتراكات',
11
+        ],
12
+
13
+        'logout' => [
14
+            'label' => 'تسجيل الخروج',
15
+        ],
16
+
17
+        'open_database_notifications' => [
18
+            'label' => 'عرض التنبيهات',
19
+        ],
20
+
21
+        'open_user_menu' => [
22
+            'label' => 'قائمة المستخدم',
23
+        ],
24
+
25
+        'sidebar' => [
26
+
27
+            'collapse' => [
28
+                'label' => 'طيّ القائمة الجانبية',
29
+            ],
30
+
31
+            'expand' => [
32
+                'label' => 'توسيع القائمة الجانبية',
33
+            ],
34
+
35
+        ],
36
+
37
+        'theme_switcher' => [
38
+
39
+            'dark' => [
40
+                'label' => 'تفعيل الوضع الليلي',
41
+            ],
42
+
43
+            'light' => [
44
+                'label' => 'تفعيل الوضع النهاري',
45
+            ],
46
+
47
+            'system' => [
48
+                'label' => 'تفعيل سمة النظام',
49
+            ],
50
+
51
+        ],
52
+
53
+    ],
54
+
55
+    'avatar' => [
56
+        'alt' => 'صورة شخصية لـ :name',
57
+    ],
58
+
59
+    'logo' => [
60
+        'alt' => ':name شعار',
61
+    ],
62
+
63
+];

+ 51
- 0
lang/vendor/filament-panels/ar/pages/auth/edit-profile.php View File

@@ -0,0 +1,51 @@
1
+<?php
2
+
3
+return [
4
+
5
+    'label' => 'الملف الشخصي',
6
+
7
+    'form' => [
8
+
9
+        'email' => [
10
+            'label' => 'البريد الإلكتروني',
11
+        ],
12
+
13
+        'name' => [
14
+            'label' => 'الاسم',
15
+        ],
16
+
17
+        'password' => [
18
+            'label' => 'كلمة المرور الجديدة',
19
+        ],
20
+
21
+        'password_confirmation' => [
22
+            'label' => 'تأكيد كلمة المرور الجديدة',
23
+        ],
24
+
25
+        'actions' => [
26
+
27
+            'save' => [
28
+                'label' => 'حفظ التغييرات',
29
+            ],
30
+
31
+        ],
32
+
33
+    ],
34
+
35
+    'notifications' => [
36
+
37
+        'saved' => [
38
+            'title' => 'تم الحفظ',
39
+        ],
40
+
41
+    ],
42
+
43
+    'actions' => [
44
+
45
+        'cancel' => [
46
+            'label' => 'إلغاء',
47
+        ],
48
+
49
+    ],
50
+
51
+];

+ 35
- 0
lang/vendor/filament-panels/ar/pages/auth/email-verification/email-verification-prompt.php View File

@@ -0,0 +1,35 @@
1
+<?php
2
+
3
+return [
4
+
5
+    'title' => 'تحقق من عنوان بريدك الإلكتروني',
6
+
7
+    'heading' => 'تحقق من عنوان بريدك الإلكتروني',
8
+
9
+    'actions' => [
10
+
11
+        'resend_notification' => [
12
+            'label' => 'أعد الإرسال',
13
+        ],
14
+
15
+    ],
16
+
17
+    'messages' => [
18
+        'notification_not_received' => 'لم تستلم البريد الذي قمنا بإرساله؟',
19
+        'notification_sent' => 'لقد أرسلنا بريدًا إلكترونيًا إلى :email يحتوي على تعليمات حول كيفية التحقق من عنوان بريدك الإلكتروني.',
20
+    ],
21
+
22
+    'notifications' => [
23
+
24
+        'notification_resent' => [
25
+            'title' => 'لقد قمنا بإعادة إرسال البريد الإلكتروني.',
26
+        ],
27
+
28
+        'notification_resend_throttled' => [
29
+            'title' => 'لقد قمت بمحاولات إعادة إرسال كثيرة جداً',
30
+            'body' => 'يرجى المحاولة مرة أخرى بعد :seconds ثواني.',
31
+        ],
32
+
33
+    ],
34
+
35
+];

+ 61
- 0
lang/vendor/filament-panels/ar/pages/auth/login.php View File

@@ -0,0 +1,61 @@
1
+<?php
2
+
3
+return [
4
+
5
+    'title' => 'تسجيل الدخول',
6
+
7
+    'heading' => 'الدخول إلى حسابك',
8
+
9
+    'actions' => [
10
+
11
+        'register' => [
12
+            'before' => 'أو',
13
+            'label' => 'إنشاء حساب',
14
+        ],
15
+
16
+        'request_password_reset' => [
17
+            'label' => 'نسيت كلمة المرور؟',
18
+        ],
19
+
20
+    ],
21
+
22
+    'form' => [
23
+
24
+        'email' => [
25
+            'label' => 'البريد الإلكتروني',
26
+        ],
27
+
28
+        'password' => [
29
+            'label' => 'كلمة المرور',
30
+        ],
31
+
32
+        'remember' => [
33
+            'label' => 'تذكرني',
34
+        ],
35
+
36
+        'actions' => [
37
+
38
+            'authenticate' => [
39
+                'label' => 'تسجيل الدخول',
40
+            ],
41
+
42
+        ],
43
+
44
+    ],
45
+
46
+    'messages' => [
47
+
48
+        'failed' => 'بيانات الاعتماد هذه غير متطابقة مع البيانات المسجلة لدينا.',
49
+
50
+    ],
51
+
52
+    'notifications' => [
53
+
54
+        'throttled' => [
55
+            'title' => 'لقد قمت بمحاولات تسجيل دخول كثيرة جدًا',
56
+            'body' => 'يرجى المحاولة مرة أخرى بعد :seconds ثواني.',
57
+        ],
58
+
59
+    ],
60
+
61
+];

+ 42
- 0
lang/vendor/filament-panels/ar/pages/auth/password-reset/request-password-reset.php View File

@@ -0,0 +1,42 @@
1
+<?php
2
+
3
+return [
4
+
5
+    'title' => 'استعادة كلمة المرور',
6
+
7
+    'heading' => 'نسيت كلمة المرور؟',
8
+
9
+    'actions' => [
10
+
11
+        'login' => [
12
+            'label' => 'العودة لتسجيل الدخول',
13
+        ],
14
+
15
+    ],
16
+
17
+    'form' => [
18
+
19
+        'email' => [
20
+            'label' => 'البريد الإلكتروني',
21
+        ],
22
+
23
+        'actions' => [
24
+
25
+            'request' => [
26
+                'label' => 'أرسل البريد الإلكتروني',
27
+            ],
28
+
29
+        ],
30
+
31
+    ],
32
+
33
+    'notifications' => [
34
+
35
+        'throttled' => [
36
+            'title' => 'لقد قمت بمحاولات كثيرة جداً',
37
+            'body' => 'يرجى المحاولة مرة أخرى بعد :seconds ثواني.',
38
+        ],
39
+
40
+    ],
41
+
42
+];

+ 43
- 0
lang/vendor/filament-panels/ar/pages/auth/password-reset/reset-password.php View File

@@ -0,0 +1,43 @@
1
+<?php
2
+
3
+return [
4
+
5
+    'title' => 'إعادة تعيين كلمة المرور',
6
+
7
+    'heading' => 'إعادة تعيين كلمة المرور',
8
+
9
+    'form' => [
10
+
11
+        'email' => [
12
+            'label' => 'البريد الإلكتروني',
13
+        ],
14
+
15
+        'password' => [
16
+            'label' => 'كلمة المرور',
17
+            'validation_attribute' => 'كلمة المرور',
18
+        ],
19
+
20
+        'password_confirmation' => [
21
+            'label' => 'تأكيد كلمة المرور',
22
+        ],
23
+
24
+        'actions' => [
25
+
26
+            'reset' => [
27
+                'label' => 'إعادة تعيين كلمة المرور',
28
+            ],
29
+
30
+        ],
31
+
32
+    ],
33
+
34
+    'notifications' => [
35
+
36
+        'throttled' => [
37
+            'title' => 'لقد قمت بمحاولات كثيرة جداً لإعادة تعيين كلمة المرور',
38
+            'body' => 'يرجى المحاولة مرة أخرى بعد :seconds ثواني.',
39
+        ],
40
+
41
+    ],
42
+
43
+];

+ 56
- 0
lang/vendor/filament-panels/ar/pages/auth/register.php View File

@@ -0,0 +1,56 @@
1
+<?php
2
+
3
+return [
4
+
5
+    'title' => 'تسجيل',
6
+
7
+    'heading' => 'إنشاء حساب',
8
+
9
+    'actions' => [
10
+
11
+        'login' => [
12
+            'before' => 'أو',
13
+            'label' => 'سجل الدخول لحسابك',
14
+        ],
15
+
16
+    ],
17
+
18
+    'form' => [
19
+
20
+        'email' => [
21
+            'label' => 'البريد الإلكتروني',
22
+        ],
23
+
24
+        'name' => [
25
+            'label' => 'الاسم',
26
+        ],
27
+
28
+        'password' => [
29
+            'label' => 'كلمة المرور',
30
+            'validation_attribute' => 'كلمة المرور',
31
+        ],
32
+
33
+        'password_confirmation' => [
34
+            'label' => 'تأكيد كلمة المرور',
35
+        ],
36
+
37
+        'actions' => [
38
+
39
+            'register' => [
40
+                'label' => 'إنشاء حساب',
41
+            ],
42
+
43
+        ],
44
+
45
+    ],
46
+
47
+    'notifications' => [
48
+
49
+        'throttled' => [
50
+            'title' => 'لقد قمت بمحاولات تسجيل كثيرة جدًا',
51
+            'body' => 'يرجى المحاولة مرة أخرى بعد :seconds ثواني.',
52
+        ],
53
+
54
+    ],
55
+
56
+];

+ 33
- 0
lang/vendor/filament-panels/ar/pages/dashboard.php View File

@@ -0,0 +1,33 @@
1
+<?php
2
+
3
+return [
4
+
5
+    'title' => 'لوحة التحكم',
6
+
7
+    'actions' => [
8
+
9
+        'filter' => [
10
+
11
+            'label' => 'تصفية',
12
+
13
+            'modal' => [
14
+
15
+                'heading' => 'تصفية',
16
+
17
+                'actions' => [
18
+
19
+                    'apply' => [
20
+
21
+                        'label' => 'تطبيق',
22
+
23
+                    ],
24
+
25
+                ],
26
+
27
+            ],
28
+
29
+        ],
30
+
31
+    ],
32
+
33
+];

+ 25
- 0
lang/vendor/filament-panels/ar/pages/tenancy/edit-tenant-profile.php View File

@@ -0,0 +1,25 @@
1
+<?php
2
+
3
+return [
4
+
5
+    'form' => [
6
+
7
+        'actions' => [
8
+
9
+            'save' => [
10
+                'label' => 'حفظ التغييرات',
11
+            ],
12
+
13
+        ],
14
+
15
+    ],
16
+
17
+    'notifications' => [
18
+
19
+        'saved' => [
20
+            'title' => 'تم الحفظ',
21
+        ],
22
+
23
+    ],
24
+
25
+];

+ 37
- 0
lang/vendor/filament-panels/ar/resources/pages/create-record.php View File

@@ -0,0 +1,37 @@
1
+<?php
2
+
3
+return [
4
+
5
+    'title' => 'إضافة :label',
6
+
7
+    'breadcrumb' => 'إضافة',
8
+
9
+    'form' => [
10
+
11
+        'actions' => [
12
+
13
+            'cancel' => [
14
+                'label' => 'إلغاء',
15
+            ],
16
+
17
+            'create' => [
18
+                'label' => 'إضافة',
19
+            ],
20
+
21
+            'create_another' => [
22
+                'label' => 'إضافة وبدء إضافة المزيد',
23
+            ],
24
+
25
+        ],
26
+
27
+    ],
28
+
29
+    'notifications' => [
30
+
31
+        'created' => [
32
+            'title' => 'تمت الإضافة',
33
+        ],
34
+
35
+    ],
36
+
37
+];

+ 41
- 0
lang/vendor/filament-panels/ar/resources/pages/edit-record.php View File

@@ -0,0 +1,41 @@
1
+<?php
2
+
3
+return [
4
+
5
+    'title' => 'تعديل :label',
6
+
7
+    'breadcrumb' => 'تعديل',
8
+
9
+    'form' => [
10
+
11
+        'actions' => [
12
+
13
+            'cancel' => [
14
+                'label' => 'إلغاء',
15
+            ],
16
+
17
+            'save' => [
18
+                'label' => 'حفظ التغييرات',
19
+            ],
20
+
21
+        ],
22
+
23
+    ],
24
+
25
+    'content' => [
26
+
27
+        'tab' => [
28
+            'label' => 'تعديل',
29
+        ],
30
+
31
+    ],
32
+
33
+    'notifications' => [
34
+
35
+        'saved' => [
36
+            'title' => 'تم الحفظ',
37
+        ],
38
+
39
+    ],
40
+
41
+];

+ 7
- 0
lang/vendor/filament-panels/ar/resources/pages/list-records.php View File

@@ -0,0 +1,7 @@
1
+<?php
2
+
3
+return [
4
+
5
+    'breadcrumb' => 'القائمة',
6
+
7
+];

+ 17
- 0
lang/vendor/filament-panels/ar/resources/pages/view-record.php View File

@@ -0,0 +1,17 @@
1
+<?php
2
+
3
+return [
4
+
5
+    'title' => 'عرض :label',
6
+
7
+    'breadcrumb' => 'عرض',
8
+
9
+    'content' => [
10
+
11
+        'tab' => [
12
+            'label' => 'عرض',
13
+        ],
14
+
15
+    ],
16
+
17
+];

+ 7
- 0
lang/vendor/filament-panels/ar/unsaved-changes-alert.php View File

@@ -0,0 +1,7 @@
1
+<?php
2
+
3
+return [
4
+
5
+    'body' => 'توجد لديك تغييرات غير محفوظة. هل أنت متأكد من أنك تريد مغادرة هذه الصفحة؟',
6
+
7
+];

+ 15
- 0
lang/vendor/filament-panels/ar/widgets/account-widget.php View File

@@ -0,0 +1,15 @@
1
+<?php
2
+
3
+return [
4
+
5
+    'actions' => [
6
+
7
+        'logout' => [
8
+            'label' => 'تسجيل الخروج',
9
+        ],
10
+
11
+    ],
12
+
13
+    'welcome' => 'مرحبا',
14
+
15
+];

+ 0
- 0
lang/vendor/filament-panels/ar/widgets/filament-info-widget.php View File


Some files were not shown because too many files changed in this diff