diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8354976..e31bf03 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,7 +8,7 @@ on: pull_request: branches: - main - - develop + - development jobs: frontend: @@ -48,15 +48,16 @@ jobs: working-directory: backend services: - mysql: - image: mysql:8.0 + postgres: + image: postgres:16 env: - MYSQL_ROOT_PASSWORD: root - MYSQL_DATABASE: testing + POSTGRES_DB: testing + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres ports: - - 3306:3306 + - 5432:5432 options: >- - --health-cmd="mysqladmin ping" + --health-cmd="pg_isready -U postgres" --health-interval=10s --health-timeout=5s --health-retries=3 @@ -69,42 +70,45 @@ jobs: uses: shivammathur/setup-php@v2 with: php-version: "8.3" - extensions: mbstring, bcmath, pdo, pdo_mysql + extensions: mbstring, bcmath, pdo, pdo_pgsql coverage: xdebug - name: Copy .env - run: cp .env.example .env.testing + run: cp .env.example .env - name: Install dependencies run: composer install --no-interaction --prefer-dist --optimize-autoloader - name: Generate app key - run: php artisan key:generate --env=testing + run: php artisan key:generate - name: Run migrations - run: php artisan migrate --env=testing --force + run: php artisan migrate --force env: - DB_CONNECTION: mysql + XDEBUG_MODE: off + DB_CONNECTION: pgsql DB_HOST: 127.0.0.1 - DB_PORT: 3306 + DB_PORT: 5432 DB_DATABASE: testing - DB_USERNAME: root - DB_PASSWORD: root + DB_USERNAME: postgres + DB_PASSWORD: postgres - name: Run tests - run: php artisan test --env=testing + run: php artisan test env: - DB_CONNECTION: mysql + XDEBUG_MODE: off + DB_CONNECTION: pgsql DB_HOST: 127.0.0.1 - DB_PORT: 3306 + DB_PORT: 5432 DB_DATABASE: testing - DB_USERNAME: root - DB_PASSWORD: root + DB_USERNAME: postgres + DB_PASSWORD: postgres docker-build: name: Docker Build Check runs-on: ubuntu-latest - needs: [frontend, backend] + needs: [frontend] +# needs: [frontend, backend] steps: - name: Checkout repository @@ -113,5 +117,5 @@ jobs: - name: Build frontend image run: docker build -t queuenova-frontend ./frontend - - name: Build backend image - run: docker build -t queuenova-backend ./backend \ No newline at end of file +# - name: Build backend image +# run: docker build -t queuenova-backend ./backend \ No newline at end of file diff --git a/backend/.editorconfig b/backend/.editorconfig new file mode 100644 index 0000000..a186cd2 --- /dev/null +++ b/backend/.editorconfig @@ -0,0 +1,18 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 4 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false + +[*.{yml,yaml}] +indent_size = 2 + +[compose.yaml] +indent_size = 4 diff --git a/backend/.env.example b/backend/.env.example new file mode 100644 index 0000000..9426f08 --- /dev/null +++ b/backend/.env.example @@ -0,0 +1,65 @@ +APP_NAME=Laravel +APP_ENV=local +APP_KEY= +APP_DEBUG=true +APP_URL=http://localhost + +APP_LOCALE=en +APP_FALLBACK_LOCALE=en +APP_FAKER_LOCALE=en_US + +APP_MAINTENANCE_DRIVER=file +# APP_MAINTENANCE_STORE=database + +# PHP_CLI_SERVER_WORKERS=4 + +BCRYPT_ROUNDS=12 + +LOG_CHANNEL=stack +LOG_STACK=single +LOG_DEPRECATIONS_CHANNEL=null +LOG_LEVEL=debug + +DB_CONNECTION=pgsql +DB_HOST=127.0.0.1 +DB_PORT=5432 +DB_DATABASE=laravel +DB_USERNAME=postgres +DB_PASSWORD= + +SESSION_DRIVER=database +SESSION_LIFETIME=120 +SESSION_ENCRYPT=false +SESSION_PATH=/ +SESSION_DOMAIN=null + +BROADCAST_CONNECTION=log +FILESYSTEM_DISK=local +QUEUE_CONNECTION=database + +CACHE_STORE=database +# CACHE_PREFIX= + +MEMCACHED_HOST=127.0.0.1 + +REDIS_CLIENT=phpredis +REDIS_HOST=127.0.0.1 +REDIS_PASSWORD=null +REDIS_PORT=6379 + +MAIL_MAILER=log +MAIL_SCHEME=null +MAIL_HOST=127.0.0.1 +MAIL_PORT=2525 +MAIL_USERNAME=null +MAIL_PASSWORD=null +MAIL_FROM_ADDRESS="hello@example.com" +MAIL_FROM_NAME="${APP_NAME}" + +AWS_ACCESS_KEY_ID= +AWS_SECRET_ACCESS_KEY= +AWS_DEFAULT_REGION=us-east-1 +AWS_BUCKET= +AWS_USE_PATH_STYLE_ENDPOINT=false + +VITE_APP_NAME="${APP_NAME}" diff --git a/backend/.gitattributes b/backend/.gitattributes new file mode 100644 index 0000000..fcb21d3 --- /dev/null +++ b/backend/.gitattributes @@ -0,0 +1,11 @@ +* text=auto eol=lf + +*.blade.php diff=html +*.css diff=css +*.html diff=html +*.md diff=markdown +*.php diff=php + +/.github export-ignore +CHANGELOG.md export-ignore +.styleci.yml export-ignore diff --git a/backend/.gitignore b/backend/.gitignore new file mode 100644 index 0000000..2f164e3 --- /dev/null +++ b/backend/.gitignore @@ -0,0 +1,25 @@ +*.log +.DS_Store +.env +.env.testing +.env.backup +.env.production +.phpactor.json +.phpunit.result.cache +/.fleet +/.idea +/.nova +/.phpunit.cache +/.vscode +/.zed +/auth.json +/node_modules +/public/build +/public/hot +/public/storage +/storage/*.key +/storage/pail +/vendor +Homestead.json +Homestead.yaml +Thumbs.db diff --git a/backend/README.md b/backend/README.md new file mode 100644 index 0000000..0165a77 --- /dev/null +++ b/backend/README.md @@ -0,0 +1,59 @@ +

Laravel Logo

+ +

+Build Status +Total Downloads +Latest Stable Version +License +

+ +## About Laravel + +Laravel is a web application framework with expressive, elegant syntax. We believe development must be an enjoyable and creative experience to be truly fulfilling. Laravel takes the pain out of development by easing common tasks used in many web projects, such as: + +- [Simple, fast routing engine](https://laravel.com/docs/routing). +- [Powerful dependency injection container](https://laravel.com/docs/container). +- Multiple back-ends for [session](https://laravel.com/docs/session) and [cache](https://laravel.com/docs/cache) storage. +- Expressive, intuitive [database ORM](https://laravel.com/docs/eloquent). +- Database agnostic [schema migrations](https://laravel.com/docs/migrations). +- [Robust background job processing](https://laravel.com/docs/queues). +- [Real-time event broadcasting](https://laravel.com/docs/broadcasting). + +Laravel is accessible, powerful, and provides tools required for large, robust applications. + +## Learning Laravel + +Laravel has the most extensive and thorough [documentation](https://laravel.com/docs) and video tutorial library of all modern web application frameworks, making it a breeze to get started with the framework. You can also check out [Laravel Learn](https://laravel.com/learn), where you will be guided through building a modern Laravel application. + +If you don't feel like reading, [Laracasts](https://laracasts.com) can help. Laracasts contains thousands of video tutorials on a range of topics including Laravel, modern PHP, unit testing, and JavaScript. Boost your skills by digging into our comprehensive video library. + +## Laravel Sponsors + +We would like to extend our thanks to the following sponsors for funding Laravel development. If you are interested in becoming a sponsor, please visit the [Laravel Partners program](https://partners.laravel.com). + +### Premium Partners + +- **[Vehikl](https://vehikl.com)** +- **[Tighten Co.](https://tighten.co)** +- **[Kirschbaum Development Group](https://kirschbaumdevelopment.com)** +- **[64 Robots](https://64robots.com)** +- **[Curotec](https://www.curotec.com/services/technologies/laravel)** +- **[DevSquad](https://devsquad.com/hire-laravel-developers)** +- **[Redberry](https://redberry.international/laravel-development)** +- **[Active Logic](https://activelogic.com)** + +## Contributing + +Thank you for considering contributing to the Laravel framework! The contribution guide can be found in the [Laravel documentation](https://laravel.com/docs/contributions). + +## Code of Conduct + +In order to ensure that the Laravel community is welcoming to all, please review and abide by the [Code of Conduct](https://laravel.com/docs/contributions#code-of-conduct). + +## Security Vulnerabilities + +If you discover a security vulnerability within Laravel, please send an e-mail to Taylor Otwell via [taylor@laravel.com](mailto:taylor@laravel.com). All security vulnerabilities will be promptly addressed. + +## License + +The Laravel framework is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT). diff --git a/backend/app/Http/Controllers/Auth/AuthController.php b/backend/app/Http/Controllers/Auth/AuthController.php new file mode 100644 index 0000000..87397d3 --- /dev/null +++ b/backend/app/Http/Controllers/Auth/AuthController.php @@ -0,0 +1,25 @@ +user(); + + $relation = match ($user->role) { + 'patient' => 'patient', + default => 'patient', // expand di EPIC 2 + }; + + return response()->json([ + 'data' => new UserResource($user->load($relation)), + ]); + } +} diff --git a/backend/app/Http/Controllers/Auth/AuthenticatedSessionController.php b/backend/app/Http/Controllers/Auth/AuthenticatedSessionController.php new file mode 100644 index 0000000..e8574a5 --- /dev/null +++ b/backend/app/Http/Controllers/Auth/AuthenticatedSessionController.php @@ -0,0 +1,54 @@ +authenticate(); + + $request->session()->regenerate(); + + $user = Auth::user(); + + $relation = match ($user->role) { + 'patient' => 'patient', + default => 'patient', // expand di EPIC 2 + }; + + return response()->json([ + 'message' => 'Login berhasil.', + 'data' => [ + 'user' => new UserResource($user->load($relation)), + ], + ]); + } + + /** + * Destroy an authenticated session. + */ + public function destroy(Request $request): JsonResponse + { + Auth::guard('web')->logout(); + + $request->session()->invalidate(); + + $request->session()->regenerateToken(); + + return response()->json([ + 'message' => 'Logout Successful.', + ]); + } +} diff --git a/backend/app/Http/Controllers/Auth/EmailVerificationNotificationController.php b/backend/app/Http/Controllers/Auth/EmailVerificationNotificationController.php new file mode 100644 index 0000000..0550fbd --- /dev/null +++ b/backend/app/Http/Controllers/Auth/EmailVerificationNotificationController.php @@ -0,0 +1,25 @@ +user()->hasVerifiedEmail()) { + return redirect()->intended('/dashboard'); + } + + $request->user()->sendEmailVerificationNotification(); + + return response()->json(['status' => 'verification-link-sent']); + } +} diff --git a/backend/app/Http/Controllers/Auth/NewPasswordController.php b/backend/app/Http/Controllers/Auth/NewPasswordController.php new file mode 100644 index 0000000..8c0959b --- /dev/null +++ b/backend/app/Http/Controllers/Auth/NewPasswordController.php @@ -0,0 +1,53 @@ +validate([ + 'token' => ['required'], + 'email' => ['required', 'email'], + 'password' => ['required', 'confirmed', Rules\Password::defaults()], + ]); + + // Here we will attempt to reset the user's password. If it is successful we + // will update the password on an actual user model and persist it to the + // database. Otherwise we will parse the error and return the response. + $status = Password::reset( + $request->only('email', 'password', 'password_confirmation', 'token'), + function ($user) use ($request) { + $user->forceFill([ + 'password' => Hash::make($request->string('password')), + 'remember_token' => Str::random(60), + ])->save(); + + event(new PasswordReset($user)); + } + ); + + if ($status != Password::PASSWORD_RESET) { + throw ValidationException::withMessages([ + 'email' => [__($status)], + ]); + } + + return response()->json(['status' => __($status)]); + } +} diff --git a/backend/app/Http/Controllers/Auth/PasswordResetLinkController.php b/backend/app/Http/Controllers/Auth/PasswordResetLinkController.php new file mode 100644 index 0000000..d555988 --- /dev/null +++ b/backend/app/Http/Controllers/Auth/PasswordResetLinkController.php @@ -0,0 +1,39 @@ +validate([ + 'email' => ['required', 'email'], + ]); + + // We will send the password reset link to this user. Once we have attempted + // to send the link, we will examine the response then see the message we + // need to show to the user. Finally, we'll send out a proper response. + $status = Password::sendResetLink( + $request->only('email') + ); + + if ($status != Password::RESET_LINK_SENT) { + throw ValidationException::withMessages([ + 'email' => [__($status)], + ]); + } + + return response()->json(['status' => __($status)]); + } +} diff --git a/backend/app/Http/Controllers/Auth/RegisteredUserController.php b/backend/app/Http/Controllers/Auth/RegisteredUserController.php new file mode 100644 index 0000000..bb939b7 --- /dev/null +++ b/backend/app/Http/Controllers/Auth/RegisteredUserController.php @@ -0,0 +1,32 @@ +authService->registerPatient($request->validated()); + + return response()->json([ + 'message' => 'Register Success', + 'data' => new UserResource($user), + ], 201); + } +} diff --git a/backend/app/Http/Controllers/Auth/VerifyEmailController.php b/backend/app/Http/Controllers/Auth/VerifyEmailController.php new file mode 100644 index 0000000..33cbed4 --- /dev/null +++ b/backend/app/Http/Controllers/Auth/VerifyEmailController.php @@ -0,0 +1,31 @@ +user()->hasVerifiedEmail()) { + return redirect()->intended( + config('app.frontend_url').'/dashboard?verified=1' + ); + } + + if ($request->user()->markEmailAsVerified()) { + event(new Verified($request->user())); + } + + return redirect()->intended( + config('app.frontend_url').'/dashboard?verified=1' + ); + } +} diff --git a/backend/app/Http/Controllers/Controller.php b/backend/app/Http/Controllers/Controller.php new file mode 100644 index 0000000..8677cd5 --- /dev/null +++ b/backend/app/Http/Controllers/Controller.php @@ -0,0 +1,8 @@ +user() || + ($request->user() instanceof MustVerifyEmail && + ! $request->user()->hasVerifiedEmail())) { + return response()->json(['message' => 'Your email address is not verified.'], 409); + } + + return $next($request); + } +} diff --git a/backend/app/Http/Middleware/RedirectIfAuthenticated.php b/backend/app/Http/Middleware/RedirectIfAuthenticated.php new file mode 100644 index 0000000..050f8eb --- /dev/null +++ b/backend/app/Http/Middleware/RedirectIfAuthenticated.php @@ -0,0 +1,35 @@ +check()) { + if ($request->expectsJson()) { + return response()->json([ + 'message' => 'Already authenticated.', + ], 409); + } + + return redirect('/'); + } + } + + return $next($request); + } +} diff --git a/backend/app/Http/Requests/Auth/LoginRequest.php b/backend/app/Http/Requests/Auth/LoginRequest.php new file mode 100644 index 0000000..9dc38aa --- /dev/null +++ b/backend/app/Http/Requests/Auth/LoginRequest.php @@ -0,0 +1,86 @@ +|string> + */ + public function rules(): array + { + return [ + 'email' => ['required', 'string', 'email'], + 'password' => ['required', 'string'], + ]; + } + + /** + * Attempt to authenticate the request's credentials. + * + * @throws ValidationException + */ + public function authenticate(): void + { + $this->ensureIsNotRateLimited(); + + if (! Auth::attempt($this->only('email', 'password'), $this->boolean('remember'))) { + RateLimiter::hit($this->throttleKey()); + + throw ValidationException::withMessages([ + 'email' => __('auth.failed'), + ]); + } + + RateLimiter::clear($this->throttleKey()); + } + + /** + * Ensure the login request is not rate limited. + * + * @throws ValidationException + */ + public function ensureIsNotRateLimited(): void + { + if (! RateLimiter::tooManyAttempts($this->throttleKey(), 5)) { + return; + } + + event(new Lockout($this)); + + $seconds = RateLimiter::availableIn($this->throttleKey()); + + throw ValidationException::withMessages([ + 'email' => trans('auth.throttle', [ + 'seconds' => $seconds, + 'minutes' => ceil($seconds / 60), + ]), + ]); + } + + /** + * Get the rate limiting throttle key for the request. + */ + public function throttleKey(): string + { + return Str::transliterate(Str::lower($this->input('email')).'|'.$this->ip()); + } +} diff --git a/backend/app/Http/Requests/Auth/RegisterRequest.php b/backend/app/Http/Requests/Auth/RegisterRequest.php new file mode 100644 index 0000000..42dbef5 --- /dev/null +++ b/backend/app/Http/Requests/Auth/RegisterRequest.php @@ -0,0 +1,32 @@ +|string> + */ + public function rules(): array + { + return [ + 'name' => ['required', 'string', 'max:255'], + 'email' => ['required', 'string', 'email', 'max:255', 'unique:users,email'], + 'password' => ['required', 'string', 'min:8', 'confirmed'], + 'phone' => ['required', 'string', 'max:15'], + ]; + } +} diff --git a/backend/app/Http/Resources/PatientProfileResource.php b/backend/app/Http/Resources/PatientProfileResource.php new file mode 100644 index 0000000..b6265e1 --- /dev/null +++ b/backend/app/Http/Resources/PatientProfileResource.php @@ -0,0 +1,28 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'patient_id' => $this->patient_id, + 'name' => $this->name, + 'phone' => $this->phone, + 'bpjs_number' => $this->bpjs_number, + 'birth_place' => $this->birth_place, + 'birth_date' => $this->birth_date?->format('Y-m-d'), + 'gender' => $this->gender, + 'user_id' => $this->user_id, + ]; + } +} diff --git a/backend/app/Http/Resources/UserResource.php b/backend/app/Http/Resources/UserResource.php new file mode 100644 index 0000000..582aec1 --- /dev/null +++ b/backend/app/Http/Resources/UserResource.php @@ -0,0 +1,35 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'id' => $this->id, + 'email' => $this->email, + 'role' => $this->role, + 'status' => $this->status, + 'profile' => $this->resolveProfile(), + ]; + } + + private function resolveProfile(): mixed + { + return match ($this->role) { + 'patient' => $this->patient + ? new PatientProfileResource($this->patient) + : null, + default => null, + }; + } +} diff --git a/backend/app/Models/Patient.php b/backend/app/Models/Patient.php new file mode 100644 index 0000000..5cb45a2 --- /dev/null +++ b/backend/app/Models/Patient.php @@ -0,0 +1,38 @@ + */ + use HasFactory; + + protected $primaryKey = 'id'; + + protected $fillable = [ + 'user_id', + 'name', + 'phone', + 'bpjs_number', + 'birth_place', + 'birth_date', + 'gender', + ]; + + protected function casts(): array + { + return [ + 'birth_date' => 'date', + ]; + } + + public function user(): BelongsTo + { + return $this->belongsTo(User::class, 'user_id', 'id'); + } +} diff --git a/backend/app/Models/User.php b/backend/app/Models/User.php new file mode 100644 index 0000000..0cf2a97 --- /dev/null +++ b/backend/app/Models/User.php @@ -0,0 +1,60 @@ + */ + use HasFactory, Notifiable, HasApiTokens; + + protected $primaryKey = 'id'; + + /** + * The attributes that are mass assignable. + * + * @var list + */ + protected $fillable = [ + 'name', + 'email', + 'password', + 'role', + 'status', + ]; + + /** + * The attributes that should be hidden for serialization. + * + * @var list + */ + protected $hidden = [ + 'password', + 'remember_token', + ]; + + /** + * Get the attributes that should be cast. + * + * @return array + */ + protected function casts(): array + { + return [ + 'email_verified_at' => 'datetime', + 'password' => 'hashed', + ]; + } + + public function patient(): HasOne + { + return $this->hasOne(Patient::class, 'user_id', 'id'); + } +} diff --git a/backend/app/Providers/AppServiceProvider.php b/backend/app/Providers/AppServiceProvider.php new file mode 100644 index 0000000..96ddc37 --- /dev/null +++ b/backend/app/Providers/AppServiceProvider.php @@ -0,0 +1,27 @@ +getEmailForPasswordReset()}"; + }); + } +} diff --git a/backend/app/Providers/AuthServiceProvider.php b/backend/app/Providers/AuthServiceProvider.php new file mode 100644 index 0000000..f5789a8 --- /dev/null +++ b/backend/app/Providers/AuthServiceProvider.php @@ -0,0 +1,29 @@ + \App\Services\impl\AuthService::class + ]; + + /** + * Register services. + */ + public function register(): void + { + // + } + + /** + * Bootstrap services. + */ + public function boot(): void + { + // + } +} diff --git a/backend/app/Services/AuthService.php b/backend/app/Services/AuthService.php new file mode 100644 index 0000000..c3c8b1e --- /dev/null +++ b/backend/app/Services/AuthService.php @@ -0,0 +1,11 @@ + $data['email'], + 'password' => $data['password'], + 'role' => 'patient', + 'status' => 'active', + ]); + + Patient::create([ + 'user_id' => $user->id, + 'name' => $data['name'], + 'phone' => $data['phone'], + ]); + + return $user->load('patient'); + } +} diff --git a/backend/artisan b/backend/artisan new file mode 100644 index 0000000..c35e31d --- /dev/null +++ b/backend/artisan @@ -0,0 +1,18 @@ +#!/usr/bin/env php +handleCommand(new ArgvInput); + +exit($status); diff --git a/backend/bootstrap/app.php b/backend/bootstrap/app.php new file mode 100644 index 0000000..b09060e --- /dev/null +++ b/backend/bootstrap/app.php @@ -0,0 +1,27 @@ +withRouting( + web: __DIR__.'/../routes/web.php', + api: __DIR__.'/../routes/api.php', + commands: __DIR__.'/../routes/console.php', + health: '/up', + ) + ->withMiddleware(function (Middleware $middleware): void { + $middleware->statefulApi(); + $middleware->alias([ + 'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class, + 'verified' => \App\Http\Middleware\EnsureEmailIsVerified::class, + ]); + $middleware->appendToGroup('api', [ + \Illuminate\Session\Middleware\StartSession::class, + ]); + + }) + ->withExceptions(function (Exceptions $exceptions): void { + // + })->create(); diff --git a/backend/bootstrap/cache/.gitignore b/backend/bootstrap/cache/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/backend/bootstrap/cache/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/backend/bootstrap/providers.php b/backend/bootstrap/providers.php new file mode 100644 index 0000000..8dc3a48 --- /dev/null +++ b/backend/bootstrap/providers.php @@ -0,0 +1,6 @@ +=5.0.0" + }, + "require-dev": { + "doctrine/dbal": "^4.0.0", + "nesbot/carbon": "^2.71.0 || ^3.0.0", + "phpunit/phpunit": "^10.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Carbon\\Doctrine\\": "src/Carbon/Doctrine/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "KyleKatarn", + "email": "kylekatarnls@gmail.com" + } + ], + "description": "Types to use Carbon in Doctrine", + "keywords": [ + "carbon", + "date", + "datetime", + "doctrine", + "time" + ], + "support": { + "issues": "https://github.com/CarbonPHP/carbon-doctrine-types/issues", + "source": "https://github.com/CarbonPHP/carbon-doctrine-types/tree/3.2.0" + }, + "funding": [ + { + "url": "https://github.com/kylekatarnls", + "type": "github" + }, + { + "url": "https://opencollective.com/Carbon", + "type": "open_collective" + }, + { + "url": "https://tidelift.com/funding/github/packagist/nesbot/carbon", + "type": "tidelift" + } + ], + "time": "2024-02-09T16:56:22+00:00" + }, + { + "name": "dflydev/dot-access-data", + "version": "v3.0.3", + "source": { + "type": "git", + "url": "https://github.com/dflydev/dflydev-dot-access-data.git", + "reference": "a23a2bf4f31d3518f3ecb38660c95715dfead60f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dflydev/dflydev-dot-access-data/zipball/a23a2bf4f31d3518f3ecb38660c95715dfead60f", + "reference": "a23a2bf4f31d3518f3ecb38660c95715dfead60f", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^0.12.42", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.3", + "scrutinizer/ocular": "1.6.0", + "squizlabs/php_codesniffer": "^3.5", + "vimeo/psalm": "^4.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Dflydev\\DotAccessData\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Dragonfly Development Inc.", + "email": "info@dflydev.com", + "homepage": "http://dflydev.com" + }, + { + "name": "Beau Simensen", + "email": "beau@dflydev.com", + "homepage": "http://beausimensen.com" + }, + { + "name": "Carlos Frutos", + "email": "carlos@kiwing.it", + "homepage": "https://github.com/cfrutos" + }, + { + "name": "Colin O'Dell", + "email": "colinodell@gmail.com", + "homepage": "https://www.colinodell.com" + } + ], + "description": "Given a deep data structure, access data by dot notation.", + "homepage": "https://github.com/dflydev/dflydev-dot-access-data", + "keywords": [ + "access", + "data", + "dot", + "notation" + ], + "support": { + "issues": "https://github.com/dflydev/dflydev-dot-access-data/issues", + "source": "https://github.com/dflydev/dflydev-dot-access-data/tree/v3.0.3" + }, + "time": "2024-07-08T12:26:09+00:00" + }, + { + "name": "doctrine/inflector", + "version": "2.1.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/inflector.git", + "reference": "6d6c96277ea252fc1304627204c3d5e6e15faa3b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/inflector/zipball/6d6c96277ea252fc1304627204c3d5e6e15faa3b", + "reference": "6d6c96277ea252fc1304627204c3d5e6e15faa3b", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^12.0 || ^13.0", + "phpstan/phpstan": "^1.12 || ^2.0", + "phpstan/phpstan-phpunit": "^1.4 || ^2.0", + "phpstan/phpstan-strict-rules": "^1.6 || ^2.0", + "phpunit/phpunit": "^8.5 || ^12.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Inflector\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Inflector is a small library that can perform string manipulations with regard to upper/lowercase and singular/plural forms of words.", + "homepage": "https://www.doctrine-project.org/projects/inflector.html", + "keywords": [ + "inflection", + "inflector", + "lowercase", + "manipulation", + "php", + "plural", + "singular", + "strings", + "uppercase", + "words" + ], + "support": { + "issues": "https://github.com/doctrine/inflector/issues", + "source": "https://github.com/doctrine/inflector/tree/2.1.0" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finflector", + "type": "tidelift" + } + ], + "time": "2025-08-10T19:31:58+00:00" + }, + { + "name": "doctrine/lexer", + "version": "3.0.1", + "source": { + "type": "git", + "url": "https://github.com/doctrine/lexer.git", + "reference": "31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/lexer/zipball/31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd", + "reference": "31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "doctrine/coding-standard": "^12", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^10.5", + "psalm/plugin-phpunit": "^0.18.3", + "vimeo/psalm": "^5.21" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Common\\Lexer\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Lexer parser library that can be used in Top-Down, Recursive Descent Parsers.", + "homepage": "https://www.doctrine-project.org/projects/lexer.html", + "keywords": [ + "annotations", + "docblock", + "lexer", + "parser", + "php" + ], + "support": { + "issues": "https://github.com/doctrine/lexer/issues", + "source": "https://github.com/doctrine/lexer/tree/3.0.1" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Flexer", + "type": "tidelift" + } + ], + "time": "2024-02-05T11:56:58+00:00" + }, + { + "name": "dragonmantank/cron-expression", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/dragonmantank/cron-expression.git", + "reference": "d61a8a9604ec1f8c3d150d09db6ce98b32675013" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dragonmantank/cron-expression/zipball/d61a8a9604ec1f8c3d150d09db6ce98b32675013", + "reference": "d61a8a9604ec1f8c3d150d09db6ce98b32675013", + "shasum": "" + }, + "require": { + "php": "^8.2|^8.3|^8.4|^8.5" + }, + "replace": { + "mtdowling/cron-expression": "^1.0" + }, + "require-dev": { + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^1.12.32|^2.1.31", + "phpunit/phpunit": "^8.5.48|^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Cron\\": "src/Cron/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Chris Tankersley", + "email": "chris@ctankersley.com", + "homepage": "https://github.com/dragonmantank" + } + ], + "description": "CRON for PHP: Calculate the next or previous run date and determine if a CRON expression is due", + "keywords": [ + "cron", + "schedule" + ], + "support": { + "issues": "https://github.com/dragonmantank/cron-expression/issues", + "source": "https://github.com/dragonmantank/cron-expression/tree/v3.6.0" + }, + "funding": [ + { + "url": "https://github.com/dragonmantank", + "type": "github" + } + ], + "time": "2025-10-31T18:51:33+00:00" + }, + { + "name": "egulias/email-validator", + "version": "4.0.4", + "source": { + "type": "git", + "url": "https://github.com/egulias/EmailValidator.git", + "reference": "d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa", + "reference": "d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa", + "shasum": "" + }, + "require": { + "doctrine/lexer": "^2.0 || ^3.0", + "php": ">=8.1", + "symfony/polyfill-intl-idn": "^1.26" + }, + "require-dev": { + "phpunit/phpunit": "^10.2", + "vimeo/psalm": "^5.12" + }, + "suggest": { + "ext-intl": "PHP Internationalization Libraries are required to use the SpoofChecking validation" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Egulias\\EmailValidator\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Eduardo Gulias Davis" + } + ], + "description": "A library for validating emails against several RFCs", + "homepage": "https://github.com/egulias/EmailValidator", + "keywords": [ + "email", + "emailvalidation", + "emailvalidator", + "validation", + "validator" + ], + "support": { + "issues": "https://github.com/egulias/EmailValidator/issues", + "source": "https://github.com/egulias/EmailValidator/tree/4.0.4" + }, + "funding": [ + { + "url": "https://github.com/egulias", + "type": "github" + } + ], + "time": "2025-03-06T22:45:56+00:00" + }, + { + "name": "fruitcake/php-cors", + "version": "v1.4.0", + "source": { + "type": "git", + "url": "https://github.com/fruitcake/php-cors.git", + "reference": "38aaa6c3fd4c157ffe2a4d10aa8b9b16ba8de379" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/fruitcake/php-cors/zipball/38aaa6c3fd4c157ffe2a4d10aa8b9b16ba8de379", + "reference": "38aaa6c3fd4c157ffe2a4d10aa8b9b16ba8de379", + "shasum": "" + }, + "require": { + "php": "^8.1", + "symfony/http-foundation": "^5.4|^6.4|^7.3|^8" + }, + "require-dev": { + "phpstan/phpstan": "^2", + "phpunit/phpunit": "^9", + "squizlabs/php_codesniffer": "^4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.3-dev" + } + }, + "autoload": { + "psr-4": { + "Fruitcake\\Cors\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fruitcake", + "homepage": "https://fruitcake.nl" + }, + { + "name": "Barryvdh", + "email": "barryvdh@gmail.com" + } + ], + "description": "Cross-origin resource sharing library for the Symfony HttpFoundation", + "homepage": "https://github.com/fruitcake/php-cors", + "keywords": [ + "cors", + "laravel", + "symfony" + ], + "support": { + "issues": "https://github.com/fruitcake/php-cors/issues", + "source": "https://github.com/fruitcake/php-cors/tree/v1.4.0" + }, + "funding": [ + { + "url": "https://fruitcake.nl", + "type": "custom" + }, + { + "url": "https://github.com/barryvdh", + "type": "github" + } + ], + "time": "2025-12-03T09:33:47+00:00" + }, + { + "name": "graham-campbell/result-type", + "version": "v1.1.4", + "source": { + "type": "git", + "url": "https://github.com/GrahamCampbell/Result-Type.git", + "reference": "e01f4a821471308ba86aa202fed6698b6b695e3b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/e01f4a821471308ba86aa202fed6698b6b695e3b", + "reference": "e01f4a821471308ba86aa202fed6698b6b695e3b", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "phpoption/phpoption": "^1.9.5" + }, + "require-dev": { + "phpunit/phpunit": "^8.5.41 || ^9.6.22 || ^10.5.45 || ^11.5.7" + }, + "type": "library", + "autoload": { + "psr-4": { + "GrahamCampbell\\ResultType\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + } + ], + "description": "An Implementation Of The Result Type", + "keywords": [ + "Graham Campbell", + "GrahamCampbell", + "Result Type", + "Result-Type", + "result" + ], + "support": { + "issues": "https://github.com/GrahamCampbell/Result-Type/issues", + "source": "https://github.com/GrahamCampbell/Result-Type/tree/v1.1.4" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/graham-campbell/result-type", + "type": "tidelift" + } + ], + "time": "2025-12-27T19:43:20+00:00" + }, + { + "name": "guzzlehttp/guzzle", + "version": "7.11.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/guzzle.git", + "reference": "c987f8ce84b8434fa430795eca0f3430663da72b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/c987f8ce84b8434fa430795eca0f3430663da72b", + "reference": "c987f8ce84b8434fa430795eca0f3430663da72b", + "shasum": "" + }, + "require": { + "ext-json": "*", + "guzzlehttp/promises": "^2.5", + "guzzlehttp/psr7": "^2.11", + "php": "^7.2.5 || ^8.0", + "psr/http-client": "^1.0", + "symfony/deprecation-contracts": "^2.5 || ^3.0", + "symfony/polyfill-php80": "^1.24" + }, + "provide": { + "psr/http-client-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "ext-curl": "*", + "guzzle/client-integration-tests": "3.0.2", + "guzzlehttp/test-server": "^0.4", + "php-http/message-factory": "^1.1", + "phpunit/phpunit": "^8.5.52 || ^9.6.34", + "psr/log": "^1.1 || ^2.0 || ^3.0" + }, + "suggest": { + "ext-curl": "Required for CURL handler support", + "ext-intl": "Required for Internationalized Domain Name (IDN) support", + "psr/log": "Required for using the Log middleware" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "GuzzleHttp\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Jeremy Lindblom", + "email": "jeremeamia@gmail.com", + "homepage": "https://github.com/jeremeamia" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle is a PHP HTTP client library", + "keywords": [ + "client", + "curl", + "framework", + "http", + "http client", + "psr-18", + "psr-7", + "rest", + "web service" + ], + "support": { + "issues": "https://github.com/guzzle/guzzle/issues", + "source": "https://github.com/guzzle/guzzle/tree/7.11.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/guzzle", + "type": "tidelift" + } + ], + "time": "2026-06-02T12:40:51+00:00" + }, + { + "name": "guzzlehttp/promises", + "version": "2.5.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/promises.git", + "reference": "4360e982f87f5f258bf872d094647791db2f4c8e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/promises/zipball/4360e982f87f5f258bf872d094647791db2f4c8e", + "reference": "4360e982f87f5f258bf872d094647791db2f4c8e", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "symfony/deprecation-contracts": "^2.5 || ^3.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.52 || ^9.6.34" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Promise\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle promises library", + "keywords": [ + "promise" + ], + "support": { + "issues": "https://github.com/guzzle/promises/issues", + "source": "https://github.com/guzzle/promises/tree/2.5.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/promises", + "type": "tidelift" + } + ], + "time": "2026-06-02T12:23:43+00:00" + }, + { + "name": "guzzlehttp/psr7", + "version": "2.11.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/psr7.git", + "reference": "bbb5e61349fa5cb822b3e87842b951088b76b81f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/bbb5e61349fa5cb822b3e87842b951088b76b81f", + "reference": "bbb5e61349fa5cb822b3e87842b951088b76b81f", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.1 || ^2.0", + "ralouphie/getallheaders": "^3.0", + "symfony/deprecation-contracts": "^2.5 || ^3.0", + "symfony/polyfill-php80": "^1.24" + }, + "provide": { + "psr/http-factory-implementation": "1.0", + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "http-interop/http-factory-tests": "1.1.0", + "jshttp/mime-db": "1.54.0.1", + "phpunit/phpunit": "^8.5.52 || ^9.6.34" + }, + "suggest": { + "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Psr7\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://sagikazarmark.hu" + } + ], + "description": "PSR-7 message implementation that also provides common utility methods", + "keywords": [ + "http", + "message", + "psr-7", + "request", + "response", + "stream", + "uri", + "url" + ], + "support": { + "issues": "https://github.com/guzzle/psr7/issues", + "source": "https://github.com/guzzle/psr7/tree/2.11.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7", + "type": "tidelift" + } + ], + "time": "2026-06-02T12:30:48+00:00" + }, + { + "name": "guzzlehttp/uri-template", + "version": "v1.0.6", + "source": { + "type": "git", + "url": "https://github.com/guzzle/uri-template.git", + "reference": "eef7f87bab6f204eba3c39224d8075c70c637946" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/uri-template/zipball/eef7f87bab6f204eba3c39224d8075c70c637946", + "reference": "eef7f87bab6f204eba3c39224d8075c70c637946", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "symfony/polyfill-php80": "^1.24" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.52 || ^9.6.34", + "uri-template/tests": "1.0.0" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\UriTemplate\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + } + ], + "description": "A polyfill class for uri_template of PHP", + "keywords": [ + "guzzlehttp", + "uri-template" + ], + "support": { + "issues": "https://github.com/guzzle/uri-template/issues", + "source": "https://github.com/guzzle/uri-template/tree/v1.0.6" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/uri-template", + "type": "tidelift" + } + ], + "time": "2026-05-23T22:00:21+00:00" + }, + { + "name": "laravel/framework", + "version": "v12.61.1", + "source": { + "type": "git", + "url": "https://github.com/laravel/framework.git", + "reference": "e8472ca9774452fe50841d9bdced060679f4d58d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/framework/zipball/e8472ca9774452fe50841d9bdced060679f4d58d", + "reference": "e8472ca9774452fe50841d9bdced060679f4d58d", + "shasum": "" + }, + "require": { + "brick/math": "^0.11|^0.12|^0.13|^0.14", + "composer-runtime-api": "^2.2", + "doctrine/inflector": "^2.0.5", + "dragonmantank/cron-expression": "^3.4", + "egulias/email-validator": "^3.2.1|^4.0", + "ext-ctype": "*", + "ext-filter": "*", + "ext-hash": "*", + "ext-mbstring": "*", + "ext-openssl": "*", + "ext-session": "*", + "ext-tokenizer": "*", + "fruitcake/php-cors": "^1.3", + "guzzlehttp/guzzle": "^7.8.2", + "guzzlehttp/uri-template": "^1.0", + "laravel/prompts": "^0.3.0", + "laravel/serializable-closure": "^1.3|^2.0", + "league/commonmark": "^2.8.1", + "league/flysystem": "^3.25.1", + "league/flysystem-local": "^3.25.1", + "league/uri": "^7.5.1", + "monolog/monolog": "^3.0", + "nesbot/carbon": "^3.8.4", + "nunomaduro/termwind": "^2.0", + "php": "^8.2", + "psr/container": "^1.1.1|^2.0.1", + "psr/log": "^1.0|^2.0|^3.0", + "psr/simple-cache": "^1.0|^2.0|^3.0", + "ramsey/uuid": "^4.7", + "symfony/console": "^7.2.0", + "symfony/error-handler": "^7.2.0", + "symfony/finder": "^7.2.0", + "symfony/http-foundation": "^7.2.0", + "symfony/http-kernel": "^7.2.0", + "symfony/mailer": "^7.2.0", + "symfony/mime": "^7.2.0", + "symfony/polyfill-php83": "^1.33", + "symfony/polyfill-php84": "^1.34", + "symfony/polyfill-php85": "^1.34", + "symfony/process": "^7.2.0", + "symfony/routing": "^7.2.0", + "symfony/uid": "^7.2.0", + "symfony/var-dumper": "^7.2.0", + "tijsverkoyen/css-to-inline-styles": "^2.2.5", + "vlucas/phpdotenv": "^5.6.1", + "voku/portable-ascii": "^2.0.2" + }, + "conflict": { + "tightenco/collect": "<5.5.33" + }, + "provide": { + "psr/container-implementation": "1.1|2.0", + "psr/log-implementation": "1.0|2.0|3.0", + "psr/simple-cache-implementation": "1.0|2.0|3.0" + }, + "replace": { + "illuminate/auth": "self.version", + "illuminate/broadcasting": "self.version", + "illuminate/bus": "self.version", + "illuminate/cache": "self.version", + "illuminate/collections": "self.version", + "illuminate/concurrency": "self.version", + "illuminate/conditionable": "self.version", + "illuminate/config": "self.version", + "illuminate/console": "self.version", + "illuminate/container": "self.version", + "illuminate/contracts": "self.version", + "illuminate/cookie": "self.version", + "illuminate/database": "self.version", + "illuminate/encryption": "self.version", + "illuminate/events": "self.version", + "illuminate/filesystem": "self.version", + "illuminate/hashing": "self.version", + "illuminate/http": "self.version", + "illuminate/json-schema": "self.version", + "illuminate/log": "self.version", + "illuminate/macroable": "self.version", + "illuminate/mail": "self.version", + "illuminate/notifications": "self.version", + "illuminate/pagination": "self.version", + "illuminate/pipeline": "self.version", + "illuminate/process": "self.version", + "illuminate/queue": "self.version", + "illuminate/redis": "self.version", + "illuminate/reflection": "self.version", + "illuminate/routing": "self.version", + "illuminate/session": "self.version", + "illuminate/support": "self.version", + "illuminate/testing": "self.version", + "illuminate/translation": "self.version", + "illuminate/validation": "self.version", + "illuminate/view": "self.version", + "spatie/once": "*" + }, + "require-dev": { + "ably/ably-php": "^1.0", + "aws/aws-sdk-php": "^3.322.9", + "ext-gmp": "*", + "fakerphp/faker": "^1.24", + "guzzlehttp/promises": "^2.0.3", + "guzzlehttp/psr7": "^2.4", + "laravel/pint": "^1.18", + "league/flysystem-aws-s3-v3": "^3.25.1", + "league/flysystem-ftp": "^3.25.1", + "league/flysystem-path-prefixing": "^3.25.1", + "league/flysystem-read-only": "^3.25.1", + "league/flysystem-sftp-v3": "^3.25.1", + "mockery/mockery": "^1.6.10", + "opis/json-schema": "^2.4.1", + "orchestra/testbench-core": "^10.9.0", + "pda/pheanstalk": "^5.0.6|^7.0.0", + "php-http/discovery": "^1.15", + "phpstan/phpstan": "^2.1.41", + "phpunit/phpunit": "^10.5.35|^11.5.3|^12.0.1", + "predis/predis": "^2.3|^3.0", + "resend/resend-php": "^0.10.0|^1.0", + "symfony/cache": "^7.2.0", + "symfony/http-client": "^7.2.0", + "symfony/psr-http-message-bridge": "^7.2.0", + "symfony/translation": "^7.2.0" + }, + "suggest": { + "ably/ably-php": "Required to use the Ably broadcast driver (^1.0).", + "aws/aws-sdk-php": "Required to use the SQS queue driver, DynamoDb failed job storage, and SES mail driver (^3.322.9).", + "brianium/paratest": "Required to run tests in parallel (^7.0|^8.0).", + "ext-apcu": "Required to use the APC cache driver.", + "ext-fileinfo": "Required to use the Filesystem class.", + "ext-ftp": "Required to use the Flysystem FTP driver.", + "ext-gd": "Required to use Illuminate\\Http\\Testing\\FileFactory::image().", + "ext-memcached": "Required to use the memcache cache driver.", + "ext-pcntl": "Required to use all features of the queue worker and console signal trapping.", + "ext-pdo": "Required to use all database features.", + "ext-posix": "Required to use all features of the queue worker.", + "ext-redis": "Required to use the Redis cache and queue drivers (^4.0|^5.0|^6.0).", + "fakerphp/faker": "Required to generate fake data using the fake() helper (^1.23).", + "filp/whoops": "Required for friendly error pages in development (^2.14.3).", + "laravel/tinker": "Required to use the tinker console command (^2.0).", + "league/flysystem-aws-s3-v3": "Required to use the Flysystem S3 driver (^3.25.1).", + "league/flysystem-ftp": "Required to use the Flysystem FTP driver (^3.25.1).", + "league/flysystem-path-prefixing": "Required to use the scoped driver (^3.25.1).", + "league/flysystem-read-only": "Required to use read-only disks (^3.25.1)", + "league/flysystem-sftp-v3": "Required to use the Flysystem SFTP driver (^3.25.1).", + "mockery/mockery": "Required to use mocking (^1.6).", + "pda/pheanstalk": "Required to use the beanstalk queue driver (^5.0).", + "php-http/discovery": "Required to use PSR-7 bridging features (^1.15).", + "phpunit/phpunit": "Required to use assertions and run tests (^10.5.35|^11.5.3|^12.0.1).", + "predis/predis": "Required to use the predis connector (^2.3|^3.0).", + "psr/http-message": "Required to allow Storage::put to accept a StreamInterface (^1.0).", + "pusher/pusher-php-server": "Required to use the Pusher broadcast driver (^6.0|^7.0).", + "resend/resend-php": "Required to enable support for the Resend mail transport (^0.10.0|^1.0).", + "symfony/cache": "Required to PSR-6 cache bridge (^7.2).", + "symfony/filesystem": "Required to enable support for relative symbolic links (^7.2).", + "symfony/http-client": "Required to enable support for the Symfony API mail transports (^7.2).", + "symfony/mailgun-mailer": "Required to enable support for the Mailgun mail transport (^7.2).", + "symfony/postmark-mailer": "Required to enable support for the Postmark mail transport (^7.2).", + "symfony/psr-http-message-bridge": "Required to use PSR-7 bridging features (^7.2)." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "12.x-dev" + } + }, + "autoload": { + "files": [ + "src/Illuminate/Collections/functions.php", + "src/Illuminate/Collections/helpers.php", + "src/Illuminate/Events/functions.php", + "src/Illuminate/Filesystem/functions.php", + "src/Illuminate/Foundation/helpers.php", + "src/Illuminate/Log/functions.php", + "src/Illuminate/Reflection/helpers.php", + "src/Illuminate/Support/functions.php", + "src/Illuminate/Support/helpers.php" + ], + "psr-4": { + "Illuminate\\": "src/Illuminate/", + "Illuminate\\Support\\": [ + "src/Illuminate/Macroable/", + "src/Illuminate/Collections/", + "src/Illuminate/Conditionable/", + "src/Illuminate/Reflection/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Laravel Framework.", + "homepage": "https://laravel.com", + "keywords": [ + "framework", + "laravel" + ], + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2026-06-04T14:22:52+00:00" + }, + { + "name": "laravel/prompts", + "version": "v0.3.18", + "source": { + "type": "git", + "url": "https://github.com/laravel/prompts.git", + "reference": "a19af51bb144bf87f08397921fa619f85c7d4e72" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/prompts/zipball/a19af51bb144bf87f08397921fa619f85c7d4e72", + "reference": "a19af51bb144bf87f08397921fa619f85c7d4e72", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2.2", + "ext-mbstring": "*", + "php": "^8.1", + "symfony/console": "^6.2|^7.0|^8.0" + }, + "conflict": { + "illuminate/console": ">=10.17.0 <10.25.0", + "laravel/framework": ">=10.17.0 <10.25.0" + }, + "require-dev": { + "illuminate/collections": "^10.0|^11.0|^12.0|^13.0", + "mockery/mockery": "^1.5", + "pestphp/pest": "^2.3|^3.4|^4.0", + "phpstan/phpstan": "^1.12.28", + "phpstan/phpstan-mockery": "^1.1.3" + }, + "suggest": { + "ext-pcntl": "Required for the spinner to be animated." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "0.3.x-dev" + } + }, + "autoload": { + "files": [ + "src/helpers.php" + ], + "psr-4": { + "Laravel\\Prompts\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Add beautiful and user-friendly forms to your command-line applications.", + "support": { + "issues": "https://github.com/laravel/prompts/issues", + "source": "https://github.com/laravel/prompts/tree/v0.3.18" + }, + "time": "2026-05-19T00:47:18+00:00" + }, + { + "name": "laravel/sanctum", + "version": "v4.3.2", + "source": { + "type": "git", + "url": "https://github.com/laravel/sanctum.git", + "reference": "2a9bccc18e9907808e0018dd15fa643937886b1e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/sanctum/zipball/2a9bccc18e9907808e0018dd15fa643937886b1e", + "reference": "2a9bccc18e9907808e0018dd15fa643937886b1e", + "shasum": "" + }, + "require": { + "ext-json": "*", + "illuminate/console": "^11.0|^12.0|^13.0", + "illuminate/contracts": "^11.0|^12.0|^13.0", + "illuminate/database": "^11.0|^12.0|^13.0", + "illuminate/support": "^11.0|^12.0|^13.0", + "php": "^8.2", + "symfony/console": "^7.0|^8.0" + }, + "require-dev": { + "mockery/mockery": "^1.6", + "orchestra/testbench": "^9.15|^10.8|^11.0", + "phpstan/phpstan": "^1.10" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Sanctum\\SanctumServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Laravel\\Sanctum\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Laravel Sanctum provides a featherweight authentication system for SPAs and simple APIs.", + "keywords": [ + "auth", + "laravel", + "sanctum" + ], + "support": { + "issues": "https://github.com/laravel/sanctum/issues", + "source": "https://github.com/laravel/sanctum" + }, + "time": "2026-04-30T11:46:25+00:00" + }, + { + "name": "laravel/serializable-closure", + "version": "v2.0.13", + "source": { + "type": "git", + "url": "https://github.com/laravel/serializable-closure.git", + "reference": "b566ee0dd251f3c4078bed003a7ce015f5ea6dce" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/b566ee0dd251f3c4078bed003a7ce015f5ea6dce", + "reference": "b566ee0dd251f3c4078bed003a7ce015f5ea6dce", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "illuminate/support": "^10.0|^11.0|^12.0|^13.0", + "nesbot/carbon": "^2.67|^3.0", + "pestphp/pest": "^2.36|^3.0|^4.0", + "phpstan/phpstan": "^2.0", + "symfony/var-dumper": "^6.2.0|^7.0.0|^8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Laravel\\SerializableClosure\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + }, + { + "name": "Nuno Maduro", + "email": "nuno@laravel.com" + } + ], + "description": "Laravel Serializable Closure provides an easy and secure way to serialize closures in PHP.", + "keywords": [ + "closure", + "laravel", + "serializable" + ], + "support": { + "issues": "https://github.com/laravel/serializable-closure/issues", + "source": "https://github.com/laravel/serializable-closure" + }, + "time": "2026-04-16T14:03:50+00:00" + }, + { + "name": "laravel/tinker", + "version": "v2.11.1", + "source": { + "type": "git", + "url": "https://github.com/laravel/tinker.git", + "reference": "c9f80cc835649b5c1842898fb043f8cc098dd741" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/tinker/zipball/c9f80cc835649b5c1842898fb043f8cc098dd741", + "reference": "c9f80cc835649b5c1842898fb043f8cc098dd741", + "shasum": "" + }, + "require": { + "illuminate/console": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", + "illuminate/contracts": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", + "illuminate/support": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", + "php": "^7.2.5|^8.0", + "psy/psysh": "^0.11.1|^0.12.0", + "symfony/var-dumper": "^4.3.4|^5.0|^6.0|^7.0|^8.0" + }, + "require-dev": { + "mockery/mockery": "~1.3.3|^1.4.2", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^8.5.8|^9.3.3|^10.0" + }, + "suggest": { + "illuminate/database": "The Illuminate Database package (^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0)." + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Tinker\\TinkerServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Laravel\\Tinker\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Powerful REPL for the Laravel framework.", + "keywords": [ + "REPL", + "Tinker", + "laravel", + "psysh" + ], + "support": { + "issues": "https://github.com/laravel/tinker/issues", + "source": "https://github.com/laravel/tinker/tree/v2.11.1" + }, + "time": "2026-02-06T14:12:35+00:00" + }, + { + "name": "league/commonmark", + "version": "2.8.2", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/commonmark.git", + "reference": "59fb075d2101740c337c7216e3f32b36c204218b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/59fb075d2101740c337c7216e3f32b36c204218b", + "reference": "59fb075d2101740c337c7216e3f32b36c204218b", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "league/config": "^1.1.1", + "php": "^7.4 || ^8.0", + "psr/event-dispatcher": "^1.0", + "symfony/deprecation-contracts": "^2.1 || ^3.0", + "symfony/polyfill-php80": "^1.16" + }, + "require-dev": { + "cebe/markdown": "^1.0", + "commonmark/cmark": "0.31.1", + "commonmark/commonmark.js": "0.31.1", + "composer/package-versions-deprecated": "^1.8", + "embed/embed": "^4.4", + "erusev/parsedown": "^1.0", + "ext-json": "*", + "github/gfm": "0.29.0", + "michelf/php-markdown": "^1.4 || ^2.0", + "nyholm/psr7": "^1.5", + "phpstan/phpstan": "^1.8.2", + "phpunit/phpunit": "^9.5.21 || ^10.5.9 || ^11.0.0", + "scrutinizer/ocular": "^1.8.1", + "symfony/finder": "^5.3 | ^6.0 | ^7.0 || ^8.0", + "symfony/process": "^5.4 | ^6.0 | ^7.0 || ^8.0", + "symfony/yaml": "^2.3 | ^3.0 | ^4.0 | ^5.0 | ^6.0 | ^7.0 || ^8.0", + "unleashedtech/php-coding-standard": "^3.1.1", + "vimeo/psalm": "^4.24.0 || ^5.0.0 || ^6.0.0" + }, + "suggest": { + "symfony/yaml": "v2.3+ required if using the Front Matter extension" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.9-dev" + } + }, + "autoload": { + "psr-4": { + "League\\CommonMark\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Colin O'Dell", + "email": "colinodell@gmail.com", + "homepage": "https://www.colinodell.com", + "role": "Lead Developer" + } + ], + "description": "Highly-extensible PHP Markdown parser which fully supports the CommonMark spec and GitHub-Flavored Markdown (GFM)", + "homepage": "https://commonmark.thephpleague.com", + "keywords": [ + "commonmark", + "flavored", + "gfm", + "github", + "github-flavored", + "markdown", + "md", + "parser" + ], + "support": { + "docs": "https://commonmark.thephpleague.com/", + "forum": "https://github.com/thephpleague/commonmark/discussions", + "issues": "https://github.com/thephpleague/commonmark/issues", + "rss": "https://github.com/thephpleague/commonmark/releases.atom", + "source": "https://github.com/thephpleague/commonmark" + }, + "funding": [ + { + "url": "https://www.colinodell.com/sponsor", + "type": "custom" + }, + { + "url": "https://www.paypal.me/colinpodell/10.00", + "type": "custom" + }, + { + "url": "https://github.com/colinodell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/league/commonmark", + "type": "tidelift" + } + ], + "time": "2026-03-19T13:16:38+00:00" + }, + { + "name": "league/config", + "version": "v1.2.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/config.git", + "reference": "754b3604fb2984c71f4af4a9cbe7b57f346ec1f3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/config/zipball/754b3604fb2984c71f4af4a9cbe7b57f346ec1f3", + "reference": "754b3604fb2984c71f4af4a9cbe7b57f346ec1f3", + "shasum": "" + }, + "require": { + "dflydev/dot-access-data": "^3.0.1", + "nette/schema": "^1.2", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.8.2", + "phpunit/phpunit": "^9.5.5", + "scrutinizer/ocular": "^1.8.1", + "unleashedtech/php-coding-standard": "^3.1", + "vimeo/psalm": "^4.7.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.2-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Config\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Colin O'Dell", + "email": "colinodell@gmail.com", + "homepage": "https://www.colinodell.com", + "role": "Lead Developer" + } + ], + "description": "Define configuration arrays with strict schemas and access values with dot notation", + "homepage": "https://config.thephpleague.com", + "keywords": [ + "array", + "config", + "configuration", + "dot", + "dot-access", + "nested", + "schema" + ], + "support": { + "docs": "https://config.thephpleague.com/", + "issues": "https://github.com/thephpleague/config/issues", + "rss": "https://github.com/thephpleague/config/releases.atom", + "source": "https://github.com/thephpleague/config" + }, + "funding": [ + { + "url": "https://www.colinodell.com/sponsor", + "type": "custom" + }, + { + "url": "https://www.paypal.me/colinpodell/10.00", + "type": "custom" + }, + { + "url": "https://github.com/colinodell", + "type": "github" + } + ], + "time": "2022-12-11T20:36:23+00:00" + }, + { + "name": "league/flysystem", + "version": "3.34.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/flysystem.git", + "reference": "2daaac3b0d4c83ea7ed5d8586e786f5d00f3540e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/2daaac3b0d4c83ea7ed5d8586e786f5d00f3540e", + "reference": "2daaac3b0d4c83ea7ed5d8586e786f5d00f3540e", + "shasum": "" + }, + "require": { + "league/flysystem-local": "^3.0.0", + "league/mime-type-detection": "^1.0.0", + "php": "^8.0.2" + }, + "conflict": { + "async-aws/core": "<1.19.0", + "async-aws/s3": "<1.14.0", + "aws/aws-sdk-php": "3.209.31 || 3.210.0", + "guzzlehttp/guzzle": "<7.0", + "guzzlehttp/ringphp": "<1.1.1", + "phpseclib/phpseclib": "3.0.15", + "symfony/http-client": "<5.2" + }, + "require-dev": { + "async-aws/s3": "^1.5 || ^2.0", + "async-aws/simple-s3": "^1.1 || ^2.0", + "aws/aws-sdk-php": "^3.295.10", + "composer/semver": "^3.0", + "ext-fileinfo": "*", + "ext-ftp": "*", + "ext-mongodb": "^1.3|^2", + "ext-zip": "*", + "friendsofphp/php-cs-fixer": "^3.5", + "google/cloud-storage": "^1.23", + "guzzlehttp/psr7": "^2.6", + "microsoft/azure-storage-blob": "^1.1", + "mongodb/mongodb": "^1.2|^2", + "phpseclib/phpseclib": "^3.0.36", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^9.5.11|^10.0", + "sabre/dav": "^4.6.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "League\\Flysystem\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frankdejonge.nl" + } + ], + "description": "File storage abstraction for PHP", + "keywords": [ + "WebDAV", + "aws", + "cloud", + "file", + "files", + "filesystem", + "filesystems", + "ftp", + "s3", + "sftp", + "storage" + ], + "support": { + "issues": "https://github.com/thephpleague/flysystem/issues", + "source": "https://github.com/thephpleague/flysystem/tree/3.34.0" + }, + "time": "2026-05-14T10:28:08+00:00" + }, + { + "name": "league/flysystem-local", + "version": "3.31.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/flysystem-local.git", + "reference": "2f669db18a4c20c755c2bb7d3a7b0b2340488079" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/flysystem-local/zipball/2f669db18a4c20c755c2bb7d3a7b0b2340488079", + "reference": "2f669db18a4c20c755c2bb7d3a7b0b2340488079", + "shasum": "" + }, + "require": { + "ext-fileinfo": "*", + "league/flysystem": "^3.0.0", + "league/mime-type-detection": "^1.0.0", + "php": "^8.0.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "League\\Flysystem\\Local\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frankdejonge.nl" + } + ], + "description": "Local filesystem adapter for Flysystem.", + "keywords": [ + "Flysystem", + "file", + "files", + "filesystem", + "local" + ], + "support": { + "source": "https://github.com/thephpleague/flysystem-local/tree/3.31.0" + }, + "time": "2026-01-23T15:30:45+00:00" + }, + { + "name": "league/mime-type-detection", + "version": "1.16.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/mime-type-detection.git", + "reference": "2d6702ff215bf922936ccc1ad31007edc76451b9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/mime-type-detection/zipball/2d6702ff215bf922936ccc1ad31007edc76451b9", + "reference": "2d6702ff215bf922936ccc1ad31007edc76451b9", + "shasum": "" + }, + "require": { + "ext-fileinfo": "*", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.2", + "phpstan/phpstan": "^0.12.68", + "phpunit/phpunit": "^8.5.8 || ^9.3 || ^10.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "League\\MimeTypeDetection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frankdejonge.nl" + } + ], + "description": "Mime-type detection for Flysystem", + "support": { + "issues": "https://github.com/thephpleague/mime-type-detection/issues", + "source": "https://github.com/thephpleague/mime-type-detection/tree/1.16.0" + }, + "funding": [ + { + "url": "https://github.com/frankdejonge", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/league/flysystem", + "type": "tidelift" + } + ], + "time": "2024-09-21T08:32:55+00:00" + }, + { + "name": "league/uri", + "version": "7.8.1", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/uri.git", + "reference": "08cf38e3924d4f56238125547b5720496fac8fd4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/uri/zipball/08cf38e3924d4f56238125547b5720496fac8fd4", + "reference": "08cf38e3924d4f56238125547b5720496fac8fd4", + "shasum": "" + }, + "require": { + "league/uri-interfaces": "^7.8.1", + "php": "^8.1", + "psr/http-factory": "^1" + }, + "conflict": { + "league/uri-schemes": "^1.0" + }, + "suggest": { + "ext-bcmath": "to improve IPV4 host parsing", + "ext-dom": "to convert the URI into an HTML anchor tag", + "ext-fileinfo": "to create Data URI from file contennts", + "ext-gmp": "to improve IPV4 host parsing", + "ext-intl": "to handle IDN host with the best performance", + "ext-uri": "to use the PHP native URI class", + "jeremykendall/php-domain-parser": "to further parse the URI host and resolve its Public Suffix and Top Level Domain", + "league/uri-components": "to provide additional tools to manipulate URI objects components", + "league/uri-polyfill": "to backport the PHP URI extension for older versions of PHP", + "php-64bit": "to improve IPV4 host parsing", + "rowbot/url": "to handle URLs using the WHATWG URL Living Standard specification", + "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "7.x-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Uri\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ignace Nyamagana Butera", + "email": "nyamsprod@gmail.com", + "homepage": "https://nyamsprod.com" + } + ], + "description": "URI manipulation library", + "homepage": "https://uri.thephpleague.com", + "keywords": [ + "URN", + "data-uri", + "file-uri", + "ftp", + "hostname", + "http", + "https", + "middleware", + "parse_str", + "parse_url", + "psr-7", + "query-string", + "querystring", + "rfc2141", + "rfc3986", + "rfc3987", + "rfc6570", + "rfc8141", + "uri", + "uri-template", + "url", + "ws" + ], + "support": { + "docs": "https://uri.thephpleague.com", + "forum": "https://thephpleague.slack.com", + "issues": "https://github.com/thephpleague/uri-src/issues", + "source": "https://github.com/thephpleague/uri/tree/7.8.1" + }, + "funding": [ + { + "url": "https://github.com/sponsors/nyamsprod", + "type": "github" + } + ], + "time": "2026-03-15T20:22:25+00:00" + }, + { + "name": "league/uri-interfaces", + "version": "7.8.1", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/uri-interfaces.git", + "reference": "85d5c77c5d6d3af6c54db4a78246364908f3c928" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/85d5c77c5d6d3af6c54db4a78246364908f3c928", + "reference": "85d5c77c5d6d3af6c54db4a78246364908f3c928", + "shasum": "" + }, + "require": { + "ext-filter": "*", + "php": "^8.1", + "psr/http-message": "^1.1 || ^2.0" + }, + "suggest": { + "ext-bcmath": "to improve IPV4 host parsing", + "ext-gmp": "to improve IPV4 host parsing", + "ext-intl": "to handle IDN host with the best performance", + "php-64bit": "to improve IPV4 host parsing", + "rowbot/url": "to handle URLs using the WHATWG URL Living Standard specification", + "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "7.x-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Uri\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ignace Nyamagana Butera", + "email": "nyamsprod@gmail.com", + "homepage": "https://nyamsprod.com" + } + ], + "description": "Common tools for parsing and resolving RFC3987/RFC3986 URI", + "homepage": "https://uri.thephpleague.com", + "keywords": [ + "data-uri", + "file-uri", + "ftp", + "hostname", + "http", + "https", + "parse_str", + "parse_url", + "psr-7", + "query-string", + "querystring", + "rfc3986", + "rfc3987", + "rfc6570", + "uri", + "url", + "ws" + ], + "support": { + "docs": "https://uri.thephpleague.com", + "forum": "https://thephpleague.slack.com", + "issues": "https://github.com/thephpleague/uri-src/issues", + "source": "https://github.com/thephpleague/uri-interfaces/tree/7.8.1" + }, + "funding": [ + { + "url": "https://github.com/sponsors/nyamsprod", + "type": "github" + } + ], + "time": "2026-03-08T20:05:35+00:00" + }, + { + "name": "monolog/monolog", + "version": "3.10.0", + "source": { + "type": "git", + "url": "https://github.com/Seldaek/monolog.git", + "reference": "b321dd6749f0bf7189444158a3ce785cc16d69b0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/b321dd6749f0bf7189444158a3ce785cc16d69b0", + "reference": "b321dd6749f0bf7189444158a3ce785cc16d69b0", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/log": "^2.0 || ^3.0" + }, + "provide": { + "psr/log-implementation": "3.0.0" + }, + "require-dev": { + "aws/aws-sdk-php": "^3.0", + "doctrine/couchdb": "~1.0@dev", + "elasticsearch/elasticsearch": "^7 || ^8", + "ext-json": "*", + "graylog2/gelf-php": "^1.4.2 || ^2.0", + "guzzlehttp/guzzle": "^7.4.5", + "guzzlehttp/psr7": "^2.2", + "mongodb/mongodb": "^1.8 || ^2.0", + "php-amqplib/php-amqplib": "~2.4 || ^3", + "php-console/php-console": "^3.1.8", + "phpstan/phpstan": "^2", + "phpstan/phpstan-deprecation-rules": "^2", + "phpstan/phpstan-strict-rules": "^2", + "phpunit/phpunit": "^10.5.17 || ^11.0.7", + "predis/predis": "^1.1 || ^2", + "rollbar/rollbar": "^4.0", + "ruflin/elastica": "^7 || ^8", + "symfony/mailer": "^5.4 || ^6", + "symfony/mime": "^5.4 || ^6" + }, + "suggest": { + "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB", + "doctrine/couchdb": "Allow sending log messages to a CouchDB server", + "elasticsearch/elasticsearch": "Allow sending log messages to an Elasticsearch server via official client", + "ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)", + "ext-curl": "Required to send log messages using the IFTTTHandler, the LogglyHandler, the SendGridHandler, the SlackWebhookHandler or the TelegramBotHandler", + "ext-mbstring": "Allow to work properly with unicode symbols", + "ext-mongodb": "Allow sending log messages to a MongoDB server (via driver)", + "ext-openssl": "Required to send log messages using SSL", + "ext-sockets": "Allow sending log messages to a Syslog server (via UDP driver)", + "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server", + "mongodb/mongodb": "Allow sending log messages to a MongoDB server (via library)", + "php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib", + "rollbar/rollbar": "Allow sending log messages to Rollbar", + "ruflin/elastica": "Allow sending log messages to an Elastic Search server" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Monolog\\": "src/Monolog" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "https://seld.be" + } + ], + "description": "Sends your logs to files, sockets, inboxes, databases and various web services", + "homepage": "https://github.com/Seldaek/monolog", + "keywords": [ + "log", + "logging", + "psr-3" + ], + "support": { + "issues": "https://github.com/Seldaek/monolog/issues", + "source": "https://github.com/Seldaek/monolog/tree/3.10.0" + }, + "funding": [ + { + "url": "https://github.com/Seldaek", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/monolog/monolog", + "type": "tidelift" + } + ], + "time": "2026-01-02T08:56:05+00:00" + }, + { + "name": "nesbot/carbon", + "version": "3.11.4", + "source": { + "type": "git", + "url": "https://github.com/CarbonPHP/carbon.git", + "reference": "e890471a3494740f7d9326d72ce6a8c559ffee60" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/e890471a3494740f7d9326d72ce6a8c559ffee60", + "reference": "e890471a3494740f7d9326d72ce6a8c559ffee60", + "shasum": "" + }, + "require": { + "carbonphp/carbon-doctrine-types": "<100.0", + "ext-json": "*", + "php": "^8.1", + "psr/clock": "^1.0", + "symfony/clock": "^6.3.12 || ^7.0 || ^8.0", + "symfony/polyfill-mbstring": "^1.0", + "symfony/translation": "^4.4.18 || ^5.2.1 || ^6.0 || ^7.0 || ^8.0" + }, + "provide": { + "psr/clock-implementation": "1.0" + }, + "require-dev": { + "doctrine/dbal": "^3.6.3 || ^4.0", + "doctrine/orm": "^2.15.2 || ^3.0", + "friendsofphp/php-cs-fixer": "^v3.87.1", + "kylekatarnls/multi-tester": "^2.5.3", + "phpmd/phpmd": "^2.15.0", + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^2.1.22", + "phpunit/phpunit": "^10.5.53", + "squizlabs/php_codesniffer": "^3.13.4 || ^4.0.0" + }, + "bin": [ + "bin/carbon" + ], + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Carbon\\Laravel\\ServiceProvider" + ] + }, + "phpstan": { + "includes": [ + "extension.neon" + ] + }, + "branch-alias": { + "dev-2.x": "2.x-dev", + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Carbon\\": "src/Carbon/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Brian Nesbitt", + "email": "brian@nesbot.com", + "homepage": "https://markido.com" + }, + { + "name": "kylekatarnls", + "homepage": "https://github.com/kylekatarnls" + } + ], + "description": "An API extension for DateTime that supports 281 different languages.", + "homepage": "https://carbonphp.github.io/carbon/", + "keywords": [ + "date", + "datetime", + "time" + ], + "support": { + "docs": "https://carbonphp.github.io/carbon/guide/getting-started/introduction.html", + "issues": "https://github.com/CarbonPHP/carbon/issues", + "source": "https://github.com/CarbonPHP/carbon" + }, + "funding": [ + { + "url": "https://github.com/sponsors/kylekatarnls", + "type": "github" + }, + { + "url": "https://opencollective.com/Carbon#sponsor", + "type": "opencollective" + }, + { + "url": "https://tidelift.com/subscription/pkg/packagist-nesbot-carbon?utm_source=packagist-nesbot-carbon&utm_medium=referral&utm_campaign=readme", + "type": "tidelift" + } + ], + "time": "2026-04-07T09:57:54+00:00" + }, + { + "name": "nette/schema", + "version": "v1.3.5", + "source": { + "type": "git", + "url": "https://github.com/nette/schema.git", + "reference": "f0ab1a3cda782dbc5da270d28545236aa80c4002" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/schema/zipball/f0ab1a3cda782dbc5da270d28545236aa80c4002", + "reference": "f0ab1a3cda782dbc5da270d28545236aa80c4002", + "shasum": "" + }, + "require": { + "nette/utils": "^4.0", + "php": "8.1 - 8.5" + }, + "require-dev": { + "nette/phpstan-rules": "^1.0", + "nette/tester": "^2.6", + "phpstan/extension-installer": "^1.4@stable", + "phpstan/phpstan": "^2.1.39@stable", + "tracy/tracy": "^2.8" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.3-dev" + } + }, + "autoload": { + "psr-4": { + "Nette\\": "src" + }, + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0-only", + "GPL-3.0-only" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" + } + ], + "description": "📐 Nette Schema: validating data structures against a given Schema.", + "homepage": "https://nette.org", + "keywords": [ + "config", + "nette" + ], + "support": { + "issues": "https://github.com/nette/schema/issues", + "source": "https://github.com/nette/schema/tree/v1.3.5" + }, + "time": "2026-02-23T03:47:12+00:00" + }, + { + "name": "nette/utils", + "version": "v4.1.4", + "source": { + "type": "git", + "url": "https://github.com/nette/utils.git", + "reference": "7da6c396d7ebe142bc857c20479d5e70a5e1aac7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/utils/zipball/7da6c396d7ebe142bc857c20479d5e70a5e1aac7", + "reference": "7da6c396d7ebe142bc857c20479d5e70a5e1aac7", + "shasum": "" + }, + "require": { + "php": "8.2 - 8.5" + }, + "conflict": { + "nette/finder": "<3", + "nette/schema": "<1.2.2" + }, + "require-dev": { + "jetbrains/phpstorm-attributes": "^1.2", + "nette/phpstan-rules": "^1.0", + "nette/tester": "^2.5", + "phpstan/extension-installer": "^1.4@stable", + "phpstan/phpstan": "^2.1@stable", + "tracy/tracy": "^2.9" + }, + "suggest": { + "ext-gd": "to use Image", + "ext-iconv": "to use Strings::webalize(), toAscii(), chr() and reverse()", + "ext-intl": "to use Strings::webalize(), toAscii(), normalize() and compare()", + "ext-json": "to use Nette\\Utils\\Json", + "ext-mbstring": "to use Strings::lower() etc...", + "ext-tokenizer": "to use Nette\\Utils\\Reflection::getUseStatements()" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.1-dev" + } + }, + "autoload": { + "psr-4": { + "Nette\\": "src" + }, + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0-only", + "GPL-3.0-only" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" + } + ], + "description": "🛠 Nette Utils: lightweight utilities for string & array manipulation, image handling, safe JSON encoding/decoding, validation, slug or strong password generating etc.", + "homepage": "https://nette.org", + "keywords": [ + "array", + "core", + "datetime", + "images", + "json", + "nette", + "paginator", + "password", + "slugify", + "string", + "unicode", + "utf-8", + "utility", + "validation" + ], + "support": { + "issues": "https://github.com/nette/utils/issues", + "source": "https://github.com/nette/utils/tree/v4.1.4" + }, + "time": "2026-05-11T20:49:54+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v5.7.0", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-json": "*", + "ext-tokenizer": "*", + "php": ">=7.4" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v5.7.0" + }, + "time": "2025-12-06T11:56:16+00:00" + }, + { + "name": "nunomaduro/termwind", + "version": "v2.4.0", + "source": { + "type": "git", + "url": "https://github.com/nunomaduro/termwind.git", + "reference": "712a31b768f5daea284c2169a7d227031001b9a8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nunomaduro/termwind/zipball/712a31b768f5daea284c2169a7d227031001b9a8", + "reference": "712a31b768f5daea284c2169a7d227031001b9a8", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": "^8.2", + "symfony/console": "^7.4.4 || ^8.0.4" + }, + "require-dev": { + "illuminate/console": "^11.47.0", + "laravel/pint": "^1.27.1", + "mockery/mockery": "^1.6.12", + "pestphp/pest": "^2.36.0 || ^3.8.4 || ^4.3.2", + "phpstan/phpstan": "^1.12.32", + "phpstan/phpstan-strict-rules": "^1.6.2", + "symfony/var-dumper": "^7.3.5 || ^8.0.4", + "thecodingmachine/phpstan-strict-rules": "^1.0.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Termwind\\Laravel\\TermwindServiceProvider" + ] + }, + "branch-alias": { + "dev-2.x": "2.x-dev" + } + }, + "autoload": { + "files": [ + "src/Functions.php" + ], + "psr-4": { + "Termwind\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "It's like Tailwind CSS, but for the console.", + "keywords": [ + "cli", + "console", + "css", + "package", + "php", + "style" + ], + "support": { + "issues": "https://github.com/nunomaduro/termwind/issues", + "source": "https://github.com/nunomaduro/termwind/tree/v2.4.0" + }, + "funding": [ + { + "url": "https://www.paypal.com/paypalme/enunomaduro", + "type": "custom" + }, + { + "url": "https://github.com/nunomaduro", + "type": "github" + }, + { + "url": "https://github.com/xiCO2k", + "type": "github" + } + ], + "time": "2026-02-16T23:10:27+00:00" + }, + { + "name": "phpoption/phpoption", + "version": "1.9.5", + "source": { + "type": "git", + "url": "https://github.com/schmittjoh/php-option.git", + "reference": "75365b91986c2405cf5e1e012c5595cd487a98be" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/75365b91986c2405cf5e1e012c5595cd487a98be", + "reference": "75365b91986c2405cf5e1e012c5595cd487a98be", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.44 || ^9.6.25 || ^10.5.53 || ^11.5.34" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + }, + "branch-alias": { + "dev-master": "1.9-dev" + } + }, + "autoload": { + "psr-4": { + "PhpOption\\": "src/PhpOption/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Johannes M. Schmitt", + "email": "schmittjoh@gmail.com", + "homepage": "https://github.com/schmittjoh" + }, + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + } + ], + "description": "Option Type for PHP", + "keywords": [ + "language", + "option", + "php", + "type" + ], + "support": { + "issues": "https://github.com/schmittjoh/php-option/issues", + "source": "https://github.com/schmittjoh/php-option/tree/1.9.5" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpoption/phpoption", + "type": "tidelift" + } + ], + "time": "2025-12-27T19:41:33+00:00" + }, + { + "name": "psr/clock", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/clock.git", + "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/clock/zipball/e41a24703d4560fd0acb709162f73b8adfc3aa0d", + "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Psr\\Clock\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for reading the clock.", + "homepage": "https://github.com/php-fig/clock", + "keywords": [ + "clock", + "now", + "psr", + "psr-20", + "time" + ], + "support": { + "issues": "https://github.com/php-fig/clock/issues", + "source": "https://github.com/php-fig/clock/tree/1.0.0" + }, + "time": "2022-11-25T14:36:26+00:00" + }, + { + "name": "psr/container", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "shasum": "" + }, + "require": { + "php": ">=7.4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/2.0.2" + }, + "time": "2021-11-05T16:47:00+00:00" + }, + { + "name": "psr/event-dispatcher", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/event-dispatcher.git", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0", + "shasum": "" + }, + "require": { + "php": ">=7.2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\EventDispatcher\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Standard interfaces for event handling.", + "keywords": [ + "events", + "psr", + "psr-14" + ], + "support": { + "issues": "https://github.com/php-fig/event-dispatcher/issues", + "source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0" + }, + "time": "2019-01-08T18:20:26+00:00" + }, + { + "name": "psr/http-client", + "version": "1.0.3", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-client.git", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP clients", + "homepage": "https://github.com/php-fig/http-client", + "keywords": [ + "http", + "http-client", + "psr", + "psr-18" + ], + "support": { + "source": "https://github.com/php-fig/http-client" + }, + "time": "2023-09-23T14:17:50+00:00" + }, + { + "name": "psr/http-factory", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-factory.git", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "shasum": "" + }, + "require": { + "php": ">=7.1", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories", + "keywords": [ + "factory", + "http", + "message", + "psr", + "psr-17", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-factory" + }, + "time": "2024-04-15T12:06:14+00:00" + }, + { + "name": "psr/http-message", + "version": "2.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-message/tree/2.0" + }, + "time": "2023-04-04T09:54:51+00:00" + }, + { + "name": "psr/log", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "support": { + "source": "https://github.com/php-fig/log/tree/3.0.2" + }, + "time": "2024-09-11T13:17:53+00:00" + }, + { + "name": "psr/simple-cache", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/simple-cache.git", + "reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/simple-cache/zipball/764e0b3939f5ca87cb904f570ef9be2d78a07865", + "reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\SimpleCache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interfaces for simple caching", + "keywords": [ + "cache", + "caching", + "psr", + "psr-16", + "simple-cache" + ], + "support": { + "source": "https://github.com/php-fig/simple-cache/tree/3.0.0" + }, + "time": "2021-10-29T13:26:27+00:00" + }, + { + "name": "psy/psysh", + "version": "v0.12.23", + "source": { + "type": "git", + "url": "https://github.com/bobthecow/psysh.git", + "reference": "4dcc0f08047d52bbde475eda481146fd8e27e1a4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/bobthecow/psysh/zipball/4dcc0f08047d52bbde475eda481146fd8e27e1a4", + "reference": "4dcc0f08047d52bbde475eda481146fd8e27e1a4", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-tokenizer": "*", + "nikic/php-parser": "^5.0 || ^4.0", + "php": "^8.0 || ^7.4", + "symfony/console": "^8.0 || ^7.0 || ^6.0 || ^5.0 || ^4.0 || ^3.4", + "symfony/var-dumper": "^8.0 || ^7.0 || ^6.0 || ^5.0 || ^4.0 || ^3.4" + }, + "conflict": { + "symfony/console": "4.4.37 || 5.3.14 || 5.3.15 || 5.4.3 || 5.4.4 || 6.0.3 || 6.0.4" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.2", + "composer/class-map-generator": "^1.6" + }, + "suggest": { + "composer/class-map-generator": "Improved tab completion performance with better class discovery.", + "ext-pcntl": "Enabling the PCNTL extension makes PsySH a lot happier :)", + "ext-posix": "If you have PCNTL, you'll want the POSIX extension as well." + }, + "bin": [ + "bin/psysh" + ], + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": false, + "forward-command": false + }, + "branch-alias": { + "dev-main": "0.12.x-dev" + } + }, + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Psy\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Justin Hileman", + "email": "justin@justinhileman.info" + } + ], + "description": "An interactive shell for modern PHP.", + "homepage": "https://psysh.org", + "keywords": [ + "REPL", + "console", + "interactive", + "shell" + ], + "support": { + "issues": "https://github.com/bobthecow/psysh/issues", + "source": "https://github.com/bobthecow/psysh/tree/v0.12.23" + }, + "time": "2026-05-23T13:41:31+00:00" + }, + { + "name": "ralouphie/getallheaders", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/ralouphie/getallheaders.git", + "reference": "120b605dfeb996808c31b6477290a714d356e822" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", + "reference": "120b605dfeb996808c31b6477290a714d356e822", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.1", + "phpunit/phpunit": "^5 || ^6.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/getallheaders.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ralph Khattar", + "email": "ralph.khattar@gmail.com" + } + ], + "description": "A polyfill for getallheaders.", + "support": { + "issues": "https://github.com/ralouphie/getallheaders/issues", + "source": "https://github.com/ralouphie/getallheaders/tree/develop" + }, + "time": "2019-03-08T08:55:37+00:00" + }, + { + "name": "ramsey/collection", + "version": "2.1.1", + "source": { + "type": "git", + "url": "https://github.com/ramsey/collection.git", + "reference": "344572933ad0181accbf4ba763e85a0306a8c5e2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ramsey/collection/zipball/344572933ad0181accbf4ba763e85a0306a8c5e2", + "reference": "344572933ad0181accbf4ba763e85a0306a8c5e2", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "captainhook/plugin-composer": "^5.3", + "ergebnis/composer-normalize": "^2.45", + "fakerphp/faker": "^1.24", + "hamcrest/hamcrest-php": "^2.0", + "jangregor/phpstan-prophecy": "^2.1", + "mockery/mockery": "^1.6", + "php-parallel-lint/php-console-highlighter": "^1.0", + "php-parallel-lint/php-parallel-lint": "^1.4", + "phpspec/prophecy-phpunit": "^2.3", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-mockery": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^10.5", + "ramsey/coding-standard": "^2.3", + "ramsey/conventional-commits": "^1.6", + "roave/security-advisories": "dev-latest" + }, + "type": "library", + "extra": { + "captainhook": { + "force-install": true + }, + "ramsey/conventional-commits": { + "configFile": "conventional-commits.json" + } + }, + "autoload": { + "psr-4": { + "Ramsey\\Collection\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ben Ramsey", + "email": "ben@benramsey.com", + "homepage": "https://benramsey.com" + } + ], + "description": "A PHP library for representing and manipulating collections.", + "keywords": [ + "array", + "collection", + "hash", + "map", + "queue", + "set" + ], + "support": { + "issues": "https://github.com/ramsey/collection/issues", + "source": "https://github.com/ramsey/collection/tree/2.1.1" + }, + "time": "2025-03-22T05:38:12+00:00" + }, + { + "name": "ramsey/uuid", + "version": "4.9.2", + "source": { + "type": "git", + "url": "https://github.com/ramsey/uuid.git", + "reference": "8429c78ca35a09f27565311b98101e2826affde0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ramsey/uuid/zipball/8429c78ca35a09f27565311b98101e2826affde0", + "reference": "8429c78ca35a09f27565311b98101e2826affde0", + "shasum": "" + }, + "require": { + "brick/math": "^0.8.16 || ^0.9 || ^0.10 || ^0.11 || ^0.12 || ^0.13 || ^0.14", + "php": "^8.0", + "ramsey/collection": "^1.2 || ^2.0" + }, + "replace": { + "rhumsaa/uuid": "self.version" + }, + "require-dev": { + "captainhook/captainhook": "^5.25", + "captainhook/plugin-composer": "^5.3", + "dealerdirect/phpcodesniffer-composer-installer": "^1.0", + "ergebnis/composer-normalize": "^2.47", + "mockery/mockery": "^1.6", + "paragonie/random-lib": "^2", + "php-mock/php-mock": "^2.6", + "php-mock/php-mock-mockery": "^1.5", + "php-parallel-lint/php-parallel-lint": "^1.4.0", + "phpbench/phpbench": "^1.2.14", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-mockery": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^9.6", + "slevomat/coding-standard": "^8.18", + "squizlabs/php_codesniffer": "^3.13" + }, + "suggest": { + "ext-bcmath": "Enables faster math with arbitrary-precision integers using BCMath.", + "ext-gmp": "Enables faster math with arbitrary-precision integers using GMP.", + "ext-uuid": "Enables the use of PeclUuidTimeGenerator and PeclUuidRandomGenerator.", + "paragonie/random-lib": "Provides RandomLib for use with the RandomLibAdapter", + "ramsey/uuid-doctrine": "Allows the use of Ramsey\\Uuid\\Uuid as Doctrine field type." + }, + "type": "library", + "extra": { + "captainhook": { + "force-install": true + } + }, + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Ramsey\\Uuid\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A PHP library for generating and working with universally unique identifiers (UUIDs).", + "keywords": [ + "guid", + "identifier", + "uuid" + ], + "support": { + "issues": "https://github.com/ramsey/uuid/issues", + "source": "https://github.com/ramsey/uuid/tree/4.9.2" + }, + "time": "2025-12-14T04:43:48+00:00" + }, + { + "name": "symfony/clock", + "version": "v7.4.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/clock.git", + "reference": "674fa3b98e21531dd040e613479f5f6fa8f32111" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/clock/zipball/674fa3b98e21531dd040e613479f5f6fa8f32111", + "reference": "674fa3b98e21531dd040e613479f5f6fa8f32111", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/clock": "^1.0", + "symfony/polyfill-php83": "^1.28" + }, + "provide": { + "psr/clock-implementation": "1.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/now.php" + ], + "psr-4": { + "Symfony\\Component\\Clock\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Decouples applications from the system clock", + "homepage": "https://symfony.com", + "keywords": [ + "clock", + "psr20", + "time" + ], + "support": { + "source": "https://github.com/symfony/clock/tree/v7.4.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-24T13:12:05+00:00" + }, + { + "name": "symfony/console", + "version": "v7.4.13", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "85095d2573eaefaf35e40b9513a9bf09f72cd217" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/85095d2573eaefaf35e40b9513a9bf09f72cd217", + "reference": "85095d2573eaefaf35e40b9513a9bf09f72cd217", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/string": "^7.2|^8.0" + }, + "conflict": { + "symfony/dependency-injection": "<6.4", + "symfony/dotenv": "<6.4", + "symfony/event-dispatcher": "<6.4", + "symfony/lock": "<6.4", + "symfony/process": "<6.4" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/event-dispatcher": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/lock": "^6.4|^7.0|^8.0", + "symfony/messenger": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/stopwatch": "^6.4|^7.0|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Eases the creation of beautiful and testable command line interfaces", + "homepage": "https://symfony.com", + "keywords": [ + "cli", + "command-line", + "console", + "terminal" + ], + "support": { + "source": "https://github.com/symfony/console/tree/v7.4.13" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-24T08:56:14+00:00" + }, + { + "name": "symfony/css-selector", + "version": "v7.4.9", + "source": { + "type": "git", + "url": "https://github.com/symfony/css-selector.git", + "reference": "b75663ed96cf4756e28e3105476f220f92886cc4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/b75663ed96cf4756e28e3105476f220f92886cc4", + "reference": "b75663ed96cf4756e28e3105476f220f92886cc4", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\CssSelector\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Jean-François Simon", + "email": "jeanfrancois.simon@sensiolabs.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Converts CSS selectors to XPath expressions", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/css-selector/tree/v7.4.9" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-04-18T13:18:21+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.7.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "50f59d1f3ca46d41ac911f97a78626b6756af35b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/50f59d1f3ca46d41ac911f97a78626b6756af35b", + "reference": "50f59d1f3ca46d41ac911f97a78626b6756af35b", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.7-dev" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.7.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-04-13T15:52:40+00:00" + }, + { + "name": "symfony/error-handler", + "version": "v7.4.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/error-handler.git", + "reference": "8dd79d8af777ee6cba2fd4d98da6ffb839f3c0fa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/error-handler/zipball/8dd79d8af777ee6cba2fd4d98da6ffb839f3c0fa", + "reference": "8dd79d8af777ee6cba2fd4d98da6ffb839f3c0fa", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/log": "^1|^2|^3", + "symfony/polyfill-php85": "^1.32", + "symfony/var-dumper": "^6.4|^7.0|^8.0" + }, + "conflict": { + "symfony/deprecation-contracts": "<2.5", + "symfony/http-kernel": "<6.4" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/serializer": "^6.4|^7.0|^8.0", + "symfony/webpack-encore-bundle": "^1.0|^2.0" + }, + "bin": [ + "Resources/bin/patch-type-declarations" + ], + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\ErrorHandler\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools to manage errors and ease debugging PHP code", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/error-handler/tree/v7.4.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-24T13:12:05+00:00" + }, + { + "name": "symfony/event-dispatcher", + "version": "v7.4.9", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher.git", + "reference": "e4a2e29753c7801f7a8340e066cfa788f3bc8101" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/e4a2e29753c7801f7a8340e066cfa788f3bc8101", + "reference": "e4a2e29753c7801f7a8340e066cfa788f3bc8101", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/event-dispatcher-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/dependency-injection": "<6.4", + "symfony/service-contracts": "<2.5" + }, + "provide": { + "psr/event-dispatcher-implementation": "1.0", + "symfony/event-dispatcher-implementation": "2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/error-handler": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/framework-bundle": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/stopwatch": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\EventDispatcher\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/event-dispatcher/tree/v7.4.9" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-04-18T13:18:21+00:00" + }, + { + "name": "symfony/event-dispatcher-contracts", + "version": "v3.7.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher-contracts.git", + "reference": "ccba7060602b7fed0b03c85bf025257f76d9ef32" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/ccba7060602b7fed0b03c85bf025257f76d9ef32", + "reference": "ccba7060602b7fed0b03c85bf025257f76d9ef32", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/event-dispatcher": "^1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.7-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\EventDispatcher\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to dispatching event", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.7.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-01-05T13:30:16+00:00" + }, + { + "name": "symfony/finder", + "version": "v7.4.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/finder.git", + "reference": "e0be088d22278583a82da281886e8c3592fbf149" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/finder/zipball/e0be088d22278583a82da281886e8c3592fbf149", + "reference": "e0be088d22278583a82da281886e8c3592fbf149", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "symfony/filesystem": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Finder\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Finds files and directories via an intuitive fluent interface", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/finder/tree/v7.4.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-24T13:12:05+00:00" + }, + { + "name": "symfony/http-foundation", + "version": "v7.4.13", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-foundation.git", + "reference": "bc354f47c62301e990b7874fa662326368508e2c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/bc354f47c62301e990b7874fa662326368508e2c", + "reference": "bc354f47c62301e990b7874fa662326368508e2c", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "^1.1" + }, + "conflict": { + "doctrine/dbal": "<3.6", + "symfony/cache": "<6.4.12|>=7.0,<7.1.5" + }, + "require-dev": { + "doctrine/dbal": "^3.6|^4", + "predis/predis": "^1.1|^2.0", + "symfony/cache": "^6.4.12|^7.1.5|^8.0", + "symfony/clock": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/mime": "^6.4|^7.0|^8.0", + "symfony/rate-limiter": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpFoundation\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Defines an object-oriented layer for the HTTP specification", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/http-foundation/tree/v7.4.13" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-24T11:20:33+00:00" + }, + { + "name": "symfony/http-kernel", + "version": "v7.4.13", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-kernel.git", + "reference": "9df847980c436451f4f51d1284491bb4356dd989" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/9df847980c436451f4f51d1284491bb4356dd989", + "reference": "9df847980c436451f4f51d1284491bb4356dd989", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/log": "^1|^2|^3", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/error-handler": "^6.4|^7.0|^8.0", + "symfony/event-dispatcher": "^7.3|^8.0", + "symfony/http-foundation": "^7.4|^8.0", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "symfony/browser-kit": "<6.4", + "symfony/cache": "<6.4", + "symfony/config": "<6.4", + "symfony/console": "<6.4", + "symfony/dependency-injection": "<6.4", + "symfony/doctrine-bridge": "<6.4", + "symfony/flex": "<2.10", + "symfony/form": "<6.4", + "symfony/http-client": "<6.4", + "symfony/http-client-contracts": "<2.5", + "symfony/mailer": "<6.4", + "symfony/messenger": "<6.4", + "symfony/translation": "<6.4", + "symfony/translation-contracts": "<2.5", + "symfony/twig-bridge": "<6.4", + "symfony/validator": "<6.4", + "symfony/var-dumper": "<6.4", + "twig/twig": "<3.12" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/cache": "^1.0|^2.0|^3.0", + "symfony/browser-kit": "^6.4|^7.0|^8.0", + "symfony/clock": "^6.4|^7.0|^8.0", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/css-selector": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4.1|^7.0.1|^8.0", + "symfony/dom-crawler": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/finder": "^6.4|^7.0|^8.0", + "symfony/http-client-contracts": "^2.5|^3", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/property-access": "^7.1|^8.0", + "symfony/routing": "^6.4|^7.0|^8.0", + "symfony/serializer": "^7.1|^8.0", + "symfony/stopwatch": "^6.4|^7.0|^8.0", + "symfony/translation": "^6.4|^7.0|^8.0", + "symfony/translation-contracts": "^2.5|^3", + "symfony/uid": "^6.4|^7.0|^8.0", + "symfony/validator": "^6.4|^7.0|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0", + "symfony/var-exporter": "^6.4|^7.0|^8.0", + "twig/twig": "^3.12" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpKernel\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides a structured process for converting a Request into a Response", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/http-kernel/tree/v7.4.13" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-27T08:31:43+00:00" + }, + { + "name": "symfony/mailer", + "version": "v7.4.12", + "source": { + "type": "git", + "url": "https://github.com/symfony/mailer.git", + "reference": "5cefb712a25f320579615ba9e1942abaeade7dff" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/mailer/zipball/5cefb712a25f320579615ba9e1942abaeade7dff", + "reference": "5cefb712a25f320579615ba9e1942abaeade7dff", + "shasum": "" + }, + "require": { + "egulias/email-validator": "^2.1.10|^3|^4", + "php": ">=8.2", + "psr/event-dispatcher": "^1", + "psr/log": "^1|^2|^3", + "symfony/event-dispatcher": "^6.4|^7.0|^8.0", + "symfony/mime": "^7.2|^8.0", + "symfony/service-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/http-client-contracts": "<2.5", + "symfony/http-kernel": "<6.4", + "symfony/messenger": "<6.4", + "symfony/mime": "<6.4", + "symfony/twig-bridge": "<6.4" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/http-client": "^6.4|^7.0|^8.0", + "symfony/messenger": "^6.4|^7.0|^8.0", + "symfony/twig-bridge": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Mailer\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Helps sending emails", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/mailer/tree/v7.4.12" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-20T07:20:23+00:00" + }, + { + "name": "symfony/mime", + "version": "v7.4.13", + "source": { + "type": "git", + "url": "https://github.com/symfony/mime.git", + "reference": "a845722765c4f6b2ce88beaf4f4479975b186770" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/mime/zipball/a845722765c4f6b2ce88beaf4f4479975b186770", + "reference": "a845722765c4f6b2ce88beaf4f4479975b186770", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-intl-idn": "^1.10", + "symfony/polyfill-mbstring": "^1.0" + }, + "conflict": { + "egulias/email-validator": "~3.0.0", + "phpdocumentor/reflection-docblock": "<5.2|>=7", + "phpdocumentor/type-resolver": "<1.5.1", + "symfony/mailer": "<6.4", + "symfony/serializer": "<6.4.3|>7.0,<7.0.3" + }, + "require-dev": { + "egulias/email-validator": "^2.1.10|^3.1|^4", + "league/html-to-markdown": "^5.0", + "phpdocumentor/reflection-docblock": "^5.2|^6.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/property-access": "^6.4|^7.0|^8.0", + "symfony/property-info": "^6.4|^7.0|^8.0", + "symfony/serializer": "^6.4.3|^7.0.3|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Mime\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Allows manipulating MIME messages", + "homepage": "https://symfony.com", + "keywords": [ + "mime", + "mime-type" + ], + "support": { + "source": "https://github.com/symfony/mime/tree/v7.4.13" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-23T16:22:37+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.37.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "141046a8f9477948ff284fa65be2095baafb94f2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/141046a8f9477948ff284fa65be2095baafb94f2", + "reference": "141046a8f9477948ff284fa65be2095baafb94f2", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "provide": { + "ext-ctype": "*" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.37.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-04-10T16:19:22+00:00" + }, + { + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.38.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "e9247d281d694a5120554d9afaf54e070e88a603" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/e9247d281d694a5120554d9afaf54e070e88a603", + "reference": "e9247d281d694a5120554d9afaf54e070e88a603", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's grapheme_* functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "grapheme", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.38.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-26T05:58:03+00:00" + }, + { + "name": "symfony/polyfill-intl-idn", + "version": "v1.38.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-idn.git", + "reference": "dc21118016c039a66235cf93d96b435ffb282412" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/dc21118016c039a66235cf93d96b435ffb282412", + "reference": "dc21118016c039a66235cf93d96b435ffb282412", + "shasum": "" + }, + "require": { + "php": ">=7.2", + "symfony/polyfill-intl-normalizer": "^1.10" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Idn\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Laurent Bassin", + "email": "laurent@bassin.info" + }, + { + "name": "Trevor Rowbotham", + "email": "trevor.rowbotham@pm.me" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's idn_to_ascii and idn_to_utf8 functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "idn", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.38.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-25T15:22:23+00:00" + }, + { + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.38.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "2d446c214bdbe5b71bde5011b060a05fece3ae6b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/2d446c214bdbe5b71bde5011b060a05fece3ae6b", + "reference": "2d446c214bdbe5b71bde5011b060a05fece3ae6b", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's Normalizer class and related functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "intl", + "normalizer", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.38.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-25T13:48:31+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.38.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "14c5439eec4ccff081ac14eca2dc57feb2a66d92" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/14c5439eec4ccff081ac14eca2dc57feb2a66d92", + "reference": "14c5439eec4ccff081ac14eca2dc57feb2a66d92", + "shasum": "" + }, + "require": { + "ext-iconv": "*", + "php": ">=7.2" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.38.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-26T12:51:13+00:00" + }, + { + "name": "symfony/polyfill-php80", + "version": "v1.37.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "dfb55726c3a76ea3b6459fcfda1ec2d80a682411" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/dfb55726c3a76ea3b6459fcfda1ec2d80a682411", + "reference": "dfb55726c3a76ea3b6459fcfda1ec2d80a682411", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php80\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php80/tree/v1.37.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-04-10T16:19:22+00:00" + }, + { + "name": "symfony/polyfill-php83", + "version": "v1.38.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php83.git", + "reference": "8339098cae28673c15cce00d80734af0453054e2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/8339098cae28673c15cce00d80734af0453054e2", + "reference": "8339098cae28673c15cce00d80734af0453054e2", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php83\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.3+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php83/tree/v1.38.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-26T12:51:13+00:00" + }, + { + "name": "symfony/polyfill-php84", + "version": "v1.38.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php84.git", + "reference": "f4e1dfaee5b74aba5964fe1fd4dfc7ba5e3085fa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/f4e1dfaee5b74aba5964fe1fd4dfc7ba5e3085fa", + "reference": "f4e1dfaee5b74aba5964fe1fd4dfc7ba5e3085fa", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php84\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.4+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php84/tree/v1.38.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-26T12:51:13+00:00" + }, + { + "name": "symfony/polyfill-php85", + "version": "v1.38.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php85.git", + "reference": "ba2ba04f3352cfa2dcbbcb90aee13ed967f505b1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/ba2ba04f3352cfa2dcbbcb90aee13ed967f505b1", + "reference": "ba2ba04f3352cfa2dcbbcb90aee13ed967f505b1", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php85\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.5+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php85/tree/v1.38.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-26T02:25:22+00:00" + }, + { + "name": "symfony/polyfill-uuid", + "version": "v1.37.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-uuid.git", + "reference": "26dfec253c4cf3e51b541b52ddf7e42cb0908e94" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-uuid/zipball/26dfec253c4cf3e51b541b52ddf7e42cb0908e94", + "reference": "26dfec253c4cf3e51b541b52ddf7e42cb0908e94", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "provide": { + "ext-uuid": "*" + }, + "suggest": { + "ext-uuid": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Uuid\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Grégoire Pineau", + "email": "lyrixx@lyrixx.info" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for uuid functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "uuid" + ], + "support": { + "source": "https://github.com/symfony/polyfill-uuid/tree/v1.37.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-04-10T16:19:22+00:00" + }, + { + "name": "symfony/process", + "version": "v7.4.13", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "f5804be144caceb570f6747519999636b664f24c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/f5804be144caceb570f6747519999636b664f24c", + "reference": "f5804be144caceb570f6747519999636b664f24c", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Process\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Executes commands in sub-processes", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/process/tree/v7.4.13" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-23T16:05:06+00:00" + }, + { + "name": "symfony/routing", + "version": "v7.4.13", + "source": { + "type": "git", + "url": "https://github.com/symfony/routing.git", + "reference": "3a162171bb008e5e0f15dce6581373a4c0e8390d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/routing/zipball/3a162171bb008e5e0f15dce6581373a4c0e8390d", + "reference": "3a162171bb008e5e0f15dce6581373a4c0e8390d", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/config": "<6.4", + "symfony/dependency-injection": "<6.4", + "symfony/yaml": "<6.4" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/yaml": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Routing\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Maps an HTTP request to a set of configuration variables", + "homepage": "https://symfony.com", + "keywords": [ + "router", + "routing", + "uri", + "url" + ], + "support": { + "source": "https://github.com/symfony/routing/tree/v7.4.13" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-24T11:20:33+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v3.7.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "d25d82433a80eba6aa0e6c24b61d7370d99e444a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/d25d82433a80eba6aa0e6c24b61d7370d99e444a", + "reference": "d25d82433a80eba6aa0e6c24b61d7370d99e444a", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "conflict": { + "ext-psr": "<1.1|>=2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.7-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to writing services", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/service-contracts/tree/v3.7.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-28T09:44:51+00:00" + }, + { + "name": "symfony/string", + "version": "v7.4.13", + "source": { + "type": "git", + "url": "https://github.com/symfony/string.git", + "reference": "961683010db3b27ec6ebcd7308e6e1ee8fa7ffde" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/string/zipball/961683010db3b27ec6ebcd7308e6e1ee8fa7ffde", + "reference": "961683010db3b27ec6ebcd7308e6e1ee8fa7ffde", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3.0", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-intl-grapheme": "~1.33", + "symfony/polyfill-intl-normalizer": "~1.0", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/translation-contracts": "<2.5" + }, + "require-dev": { + "symfony/emoji": "^7.1|^8.0", + "symfony/http-client": "^6.4|^7.0|^8.0", + "symfony/intl": "^6.4|^7.0|^8.0", + "symfony/translation-contracts": "^2.5|^3.0", + "symfony/var-exporter": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\String\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", + "homepage": "https://symfony.com", + "keywords": [ + "grapheme", + "i18n", + "string", + "unicode", + "utf-8", + "utf8" + ], + "support": { + "source": "https://github.com/symfony/string/tree/v7.4.13" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-23T15:23:29+00:00" + }, + { + "name": "symfony/translation", + "version": "v7.4.10", + "source": { + "type": "git", + "url": "https://github.com/symfony/translation.git", + "reference": "ada7578c30dd5feaa8259cff3e885069ea81ddde" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/translation/zipball/ada7578c30dd5feaa8259cff3e885069ea81ddde", + "reference": "ada7578c30dd5feaa8259cff3e885069ea81ddde", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/translation-contracts": "^2.5.3|^3.3" + }, + "conflict": { + "nikic/php-parser": "<5.0", + "symfony/config": "<6.4", + "symfony/console": "<6.4", + "symfony/dependency-injection": "<6.4", + "symfony/http-client-contracts": "<2.5", + "symfony/http-kernel": "<6.4", + "symfony/service-contracts": "<2.5", + "symfony/twig-bundle": "<6.4", + "symfony/yaml": "<6.4" + }, + "provide": { + "symfony/translation-implementation": "2.3|3.0" + }, + "require-dev": { + "nikic/php-parser": "^5.0", + "psr/log": "^1|^2|^3", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/finder": "^6.4|^7.0|^8.0", + "symfony/http-client-contracts": "^2.5|^3.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/intl": "^6.4|^7.0|^8.0", + "symfony/polyfill-intl-icu": "^1.21", + "symfony/routing": "^6.4|^7.0|^8.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/yaml": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\Translation\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools to internationalize your application", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/translation/tree/v7.4.10" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-06T11:19:24+00:00" + }, + { + "name": "symfony/translation-contracts", + "version": "v3.7.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/translation-contracts.git", + "reference": "0ab302977a952b42fd51475c4ebac81f8da0a95d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/0ab302977a952b42fd51475c4ebac81f8da0a95d", + "reference": "0ab302977a952b42fd51475c4ebac81f8da0a95d", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.7-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Translation\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to translation", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/translation-contracts/tree/v3.7.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-01-05T13:30:16+00:00" + }, + { + "name": "symfony/uid", + "version": "v7.4.9", + "source": { + "type": "git", + "url": "https://github.com/symfony/uid.git", + "reference": "2676b524340abcfe4d6151ec698463cebafee439" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/uid/zipball/2676b524340abcfe4d6151ec698463cebafee439", + "reference": "2676b524340abcfe4d6151ec698463cebafee439", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/polyfill-uuid": "^1.15" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Uid\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Grégoire Pineau", + "email": "lyrixx@lyrixx.info" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to generate and represent UIDs", + "homepage": "https://symfony.com", + "keywords": [ + "UID", + "ulid", + "uuid" + ], + "support": { + "source": "https://github.com/symfony/uid/tree/v7.4.9" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-04-30T15:19:22+00:00" + }, + { + "name": "symfony/var-dumper", + "version": "v7.4.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/var-dumper.git", + "reference": "9510c3966f749a1d1ff0059e1eabef6cc621e7fd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/9510c3966f749a1d1ff0059e1eabef6cc621e7fd", + "reference": "9510c3966f749a1d1ff0059e1eabef6cc621e7fd", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/console": "<6.4" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/uid": "^6.4|^7.0|^8.0", + "twig/twig": "^3.12" + }, + "bin": [ + "Resources/bin/var-dump-server" + ], + "type": "library", + "autoload": { + "files": [ + "Resources/functions/dump.php" + ], + "psr-4": { + "Symfony\\Component\\VarDumper\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides mechanisms for walking through any arbitrary PHP variable", + "homepage": "https://symfony.com", + "keywords": [ + "debug", + "dump" + ], + "support": { + "source": "https://github.com/symfony/var-dumper/tree/v7.4.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-30T13:44:50+00:00" + }, + { + "name": "tijsverkoyen/css-to-inline-styles", + "version": "v2.4.0", + "source": { + "type": "git", + "url": "https://github.com/tijsverkoyen/CssToInlineStyles.git", + "reference": "f0292ccf0ec75843d65027214426b6b163b48b41" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/tijsverkoyen/CssToInlineStyles/zipball/f0292ccf0ec75843d65027214426b6b163b48b41", + "reference": "f0292ccf0ec75843d65027214426b6b163b48b41", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "php": "^7.4 || ^8.0", + "symfony/css-selector": "^5.4 || ^6.0 || ^7.0 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^8.5.21 || ^9.5.10" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "TijsVerkoyen\\CssToInlineStyles\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Tijs Verkoyen", + "email": "css_to_inline_styles@verkoyen.eu", + "role": "Developer" + } + ], + "description": "CssToInlineStyles is a class that enables you to convert HTML-pages/files into HTML-pages/files with inline styles. This is very useful when you're sending emails.", + "homepage": "https://github.com/tijsverkoyen/CssToInlineStyles", + "support": { + "issues": "https://github.com/tijsverkoyen/CssToInlineStyles/issues", + "source": "https://github.com/tijsverkoyen/CssToInlineStyles/tree/v2.4.0" + }, + "time": "2025-12-02T11:56:42+00:00" + }, + { + "name": "vlucas/phpdotenv", + "version": "v5.6.3", + "source": { + "type": "git", + "url": "https://github.com/vlucas/phpdotenv.git", + "reference": "955e7815d677a3eaa7075231212f2110983adecc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/955e7815d677a3eaa7075231212f2110983adecc", + "reference": "955e7815d677a3eaa7075231212f2110983adecc", + "shasum": "" + }, + "require": { + "ext-pcre": "*", + "graham-campbell/result-type": "^1.1.4", + "php": "^7.2.5 || ^8.0", + "phpoption/phpoption": "^1.9.5", + "symfony/polyfill-ctype": "^1.26", + "symfony/polyfill-mbstring": "^1.26", + "symfony/polyfill-php80": "^1.26" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "ext-filter": "*", + "phpunit/phpunit": "^8.5.34 || ^9.6.13 || ^10.4.2" + }, + "suggest": { + "ext-filter": "Required to use the boolean validator." + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + }, + "branch-alias": { + "dev-master": "5.6-dev" + } + }, + "autoload": { + "psr-4": { + "Dotenv\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Vance Lucas", + "email": "vance@vancelucas.com", + "homepage": "https://github.com/vlucas" + } + ], + "description": "Loads environment variables from `.env` to `getenv()`, `$_ENV` and `$_SERVER` automagically.", + "keywords": [ + "dotenv", + "env", + "environment" + ], + "support": { + "issues": "https://github.com/vlucas/phpdotenv/issues", + "source": "https://github.com/vlucas/phpdotenv/tree/v5.6.3" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/vlucas/phpdotenv", + "type": "tidelift" + } + ], + "time": "2025-12-27T19:49:13+00:00" + }, + { + "name": "voku/portable-ascii", + "version": "2.1.1", + "source": { + "type": "git", + "url": "https://github.com/voku/portable-ascii.git", + "reference": "8e1051fe39379367aecf014f41744ce7539a856f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/voku/portable-ascii/zipball/8e1051fe39379367aecf014f41744ce7539a856f", + "reference": "8e1051fe39379367aecf014f41744ce7539a856f", + "shasum": "" + }, + "require": { + "php": ">=7.1.0" + }, + "require-dev": { + "phpunit/phpunit": "~8.5 || ~9.6 || ~10.5 || ~11.5" + }, + "suggest": { + "ext-intl": "Use Intl for transliterator_transliterate() support" + }, + "type": "library", + "autoload": { + "psr-4": { + "voku\\": "src/voku/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Lars Moelleken", + "homepage": "https://www.moelleken.org/" + } + ], + "description": "Portable ASCII library - performance optimized (ascii) string functions for php.", + "homepage": "https://github.com/voku/portable-ascii", + "keywords": [ + "ascii", + "clean", + "php" + ], + "support": { + "issues": "https://github.com/voku/portable-ascii/issues", + "source": "https://github.com/voku/portable-ascii/tree/2.1.1" + }, + "funding": [ + { + "url": "https://www.paypal.me/moelleken", + "type": "custom" + }, + { + "url": "https://github.com/voku", + "type": "github" + }, + { + "url": "https://opencollective.com/portable-ascii", + "type": "open_collective" + }, + { + "url": "https://www.patreon.com/voku", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/voku/portable-ascii", + "type": "tidelift" + } + ], + "time": "2026-04-26T05:33:54+00:00" + } + ], + "packages-dev": [ + { + "name": "fakerphp/faker", + "version": "v1.24.1", + "source": { + "type": "git", + "url": "https://github.com/FakerPHP/Faker.git", + "reference": "e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/FakerPHP/Faker/zipball/e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5", + "reference": "e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0", + "psr/container": "^1.0 || ^2.0", + "symfony/deprecation-contracts": "^2.2 || ^3.0" + }, + "conflict": { + "fzaninotto/faker": "*" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.4.1", + "doctrine/persistence": "^1.3 || ^2.0", + "ext-intl": "*", + "phpunit/phpunit": "^9.5.26", + "symfony/phpunit-bridge": "^5.4.16" + }, + "suggest": { + "doctrine/orm": "Required to use Faker\\ORM\\Doctrine", + "ext-curl": "Required by Faker\\Provider\\Image to download images.", + "ext-dom": "Required by Faker\\Provider\\HtmlLorem for generating random HTML.", + "ext-iconv": "Required by Faker\\Provider\\ru_RU\\Text::realText() for generating real Russian text.", + "ext-mbstring": "Required for multibyte Unicode string functionality." + }, + "type": "library", + "autoload": { + "psr-4": { + "Faker\\": "src/Faker/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "François Zaninotto" + } + ], + "description": "Faker is a PHP library that generates fake data for you.", + "keywords": [ + "data", + "faker", + "fixtures" + ], + "support": { + "issues": "https://github.com/FakerPHP/Faker/issues", + "source": "https://github.com/FakerPHP/Faker/tree/v1.24.1" + }, + "time": "2024-11-21T13:46:39+00:00" + }, + { + "name": "filp/whoops", + "version": "2.18.4", + "source": { + "type": "git", + "url": "https://github.com/filp/whoops.git", + "reference": "d2102955e48b9fd9ab24280a7ad12ed552752c4d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/filp/whoops/zipball/d2102955e48b9fd9ab24280a7ad12ed552752c4d", + "reference": "d2102955e48b9fd9ab24280a7ad12ed552752c4d", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0", + "psr/log": "^1.0.1 || ^2.0 || ^3.0" + }, + "require-dev": { + "mockery/mockery": "^1.0", + "phpunit/phpunit": "^7.5.20 || ^8.5.8 || ^9.3.3", + "symfony/var-dumper": "^4.0 || ^5.0" + }, + "suggest": { + "symfony/var-dumper": "Pretty print complex values better with var-dumper available", + "whoops/soap": "Formats errors as SOAP responses" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.7-dev" + } + }, + "autoload": { + "psr-4": { + "Whoops\\": "src/Whoops/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Filipe Dobreira", + "homepage": "https://github.com/filp", + "role": "Developer" + } + ], + "description": "php error handling for cool kids", + "homepage": "https://filp.github.io/whoops/", + "keywords": [ + "error", + "exception", + "handling", + "library", + "throwable", + "whoops" + ], + "support": { + "issues": "https://github.com/filp/whoops/issues", + "source": "https://github.com/filp/whoops/tree/2.18.4" + }, + "funding": [ + { + "url": "https://github.com/denis-sokolov", + "type": "github" + } + ], + "time": "2025-08-08T12:00:00+00:00" + }, + { + "name": "hamcrest/hamcrest-php", + "version": "v2.1.1", + "source": { + "type": "git", + "url": "https://github.com/hamcrest/hamcrest-php.git", + "reference": "f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/hamcrest/hamcrest-php/zipball/f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487", + "reference": "f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487", + "shasum": "" + }, + "require": { + "php": "^7.4|^8.0" + }, + "replace": { + "cordoval/hamcrest-php": "*", + "davedevelopment/hamcrest-php": "*", + "kodova/hamcrest-php": "*" + }, + "require-dev": { + "phpunit/php-file-iterator": "^1.4 || ^2.0 || ^3.0", + "phpunit/phpunit": "^4.8.36 || ^5.7 || ^6.5 || ^7.0 || ^8.0 || ^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.1-dev" + } + }, + "autoload": { + "classmap": [ + "hamcrest" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "This is the PHP port of Hamcrest Matchers", + "keywords": [ + "test" + ], + "support": { + "issues": "https://github.com/hamcrest/hamcrest-php/issues", + "source": "https://github.com/hamcrest/hamcrest-php/tree/v2.1.1" + }, + "time": "2025-04-30T06:54:44+00:00" + }, + { + "name": "laravel/breeze", + "version": "v2.4.2", + "source": { + "type": "git", + "url": "https://github.com/laravel/breeze.git", + "reference": "4f20e7b2cc8d25daa85d8647241a89c8e0930305" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/breeze/zipball/4f20e7b2cc8d25daa85d8647241a89c8e0930305", + "reference": "4f20e7b2cc8d25daa85d8647241a89c8e0930305", + "shasum": "" + }, + "require": { + "illuminate/console": "^11.0|^12.0|^13.0", + "illuminate/filesystem": "^11.0|^12.0|^13.0", + "illuminate/support": "^11.0|^12.0|^13.0", + "illuminate/validation": "^11.0|^12.0|^13.0", + "php": "^8.2.0", + "symfony/console": "^7.0|^8.0" + }, + "require-dev": { + "laravel/framework": "^11.0|^12.0|^13.0", + "orchestra/testbench-core": "^9.0|^10.0|^11.0", + "phpstan/phpstan": "^2.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Breeze\\BreezeServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Laravel\\Breeze\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Minimal Laravel authentication scaffolding with Blade and Tailwind.", + "keywords": [ + "auth", + "laravel" + ], + "support": { + "issues": "https://github.com/laravel/breeze/issues", + "source": "https://github.com/laravel/breeze" + }, + "time": "2026-05-14T16:54:25+00:00" + }, + { + "name": "laravel/pail", + "version": "v1.2.7", + "source": { + "type": "git", + "url": "https://github.com/laravel/pail.git", + "reference": "2f7d27dada8effc48b8c424445a69cca7007daaa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/pail/zipball/2f7d27dada8effc48b8c424445a69cca7007daaa", + "reference": "2f7d27dada8effc48b8c424445a69cca7007daaa", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "illuminate/console": "^10.24|^11.0|^12.0|^13.0", + "illuminate/contracts": "^10.24|^11.0|^12.0|^13.0", + "illuminate/log": "^10.24|^11.0|^12.0|^13.0", + "illuminate/process": "^10.24|^11.0|^12.0|^13.0", + "illuminate/support": "^10.24|^11.0|^12.0|^13.0", + "nunomaduro/termwind": "^1.15|^2.0", + "php": "^8.2", + "symfony/console": "^6.0|^7.0|^8.0" + }, + "require-dev": { + "laravel/framework": "^10.24|^11.0|^12.0|^13.0", + "laravel/pint": "^1.13", + "orchestra/testbench-core": "^8.13|^9.17|^10.8|^11.0", + "pestphp/pest": "^2.20|^3.0|^4.0", + "pestphp/pest-plugin-type-coverage": "^2.3|^3.0|^4.0", + "phpstan/phpstan": "^1.12.27", + "symfony/var-dumper": "^6.3|^7.0|^8.0", + "symfony/yaml": "^6.3|^7.0|^8.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Pail\\PailServiceProvider" + ] + }, + "branch-alias": { + "dev-main": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Laravel\\Pail\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + }, + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "Easily delve into your Laravel application's log files directly from the command line.", + "homepage": "https://github.com/laravel/pail", + "keywords": [ + "dev", + "laravel", + "logs", + "php", + "tail" + ], + "support": { + "issues": "https://github.com/laravel/pail/issues", + "source": "https://github.com/laravel/pail" + }, + "time": "2026-05-20T22:24:57+00:00" + }, + { + "name": "laravel/pint", + "version": "v1.29.1", + "source": { + "type": "git", + "url": "https://github.com/laravel/pint.git", + "reference": "0770e9b7fafd50d4586881d456d6eb41c9247a80" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/pint/zipball/0770e9b7fafd50d4586881d456d6eb41c9247a80", + "reference": "0770e9b7fafd50d4586881d456d6eb41c9247a80", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-mbstring": "*", + "ext-tokenizer": "*", + "ext-xml": "*", + "php": "^8.2.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.95.1", + "illuminate/view": "^12.56.0", + "larastan/larastan": "^3.9.6", + "laravel-zero/framework": "^12.1.0", + "mockery/mockery": "^1.6.12", + "nunomaduro/termwind": "^2.4.0", + "pestphp/pest": "^3.8.6", + "shipfastlabs/agent-detector": "^1.1.3" + }, + "bin": [ + "builds/pint" + ], + "type": "project", + "autoload": { + "psr-4": { + "App\\": "app/", + "Database\\Seeders\\": "database/seeders/", + "Database\\Factories\\": "database/factories/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "An opinionated code formatter for PHP.", + "homepage": "https://laravel.com", + "keywords": [ + "dev", + "format", + "formatter", + "lint", + "linter", + "php" + ], + "support": { + "issues": "https://github.com/laravel/pint/issues", + "source": "https://github.com/laravel/pint" + }, + "time": "2026-04-20T15:26:14+00:00" + }, + { + "name": "laravel/sail", + "version": "v1.62.0", + "source": { + "type": "git", + "url": "https://github.com/laravel/sail.git", + "reference": "3aaeefc979f8ba6586fbc5b6e0b1b3638058f98e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/sail/zipball/3aaeefc979f8ba6586fbc5b6e0b1b3638058f98e", + "reference": "3aaeefc979f8ba6586fbc5b6e0b1b3638058f98e", + "shasum": "" + }, + "require": { + "illuminate/console": "^9.52.16|^10.0|^11.0|^12.0|^13.0", + "illuminate/contracts": "^9.52.16|^10.0|^11.0|^12.0|^13.0", + "illuminate/support": "^9.52.16|^10.0|^11.0|^12.0|^13.0", + "php": "^8.0", + "symfony/console": "^6.0|^7.0|^8.0", + "symfony/yaml": "^6.0|^7.0|^8.0" + }, + "require-dev": { + "orchestra/testbench": "^7.0|^8.0|^9.0|^10.0|^11.0", + "phpstan/phpstan": "^2.0" + }, + "bin": [ + "bin/sail" + ], + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Sail\\SailServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Laravel\\Sail\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Docker files for running a basic Laravel application.", + "keywords": [ + "docker", + "laravel" + ], + "support": { + "issues": "https://github.com/laravel/sail/issues", + "source": "https://github.com/laravel/sail" + }, + "time": "2026-05-27T04:02:01+00:00" + }, + { + "name": "mockery/mockery", + "version": "1.6.12", + "source": { + "type": "git", + "url": "https://github.com/mockery/mockery.git", + "reference": "1f4efdd7d3beafe9807b08156dfcb176d18f1699" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/mockery/mockery/zipball/1f4efdd7d3beafe9807b08156dfcb176d18f1699", + "reference": "1f4efdd7d3beafe9807b08156dfcb176d18f1699", + "shasum": "" + }, + "require": { + "hamcrest/hamcrest-php": "^2.0.1", + "lib-pcre": ">=7.0", + "php": ">=7.3" + }, + "conflict": { + "phpunit/phpunit": "<8.0" + }, + "require-dev": { + "phpunit/phpunit": "^8.5 || ^9.6.17", + "symplify/easy-coding-standard": "^12.1.14" + }, + "type": "library", + "autoload": { + "files": [ + "library/helpers.php", + "library/Mockery.php" + ], + "psr-4": { + "Mockery\\": "library/Mockery" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Pádraic Brady", + "email": "padraic.brady@gmail.com", + "homepage": "https://github.com/padraic", + "role": "Author" + }, + { + "name": "Dave Marshall", + "email": "dave.marshall@atstsolutions.co.uk", + "homepage": "https://davedevelopment.co.uk", + "role": "Developer" + }, + { + "name": "Nathanael Esayeas", + "email": "nathanael.esayeas@protonmail.com", + "homepage": "https://github.com/ghostwriter", + "role": "Lead Developer" + } + ], + "description": "Mockery is a simple yet flexible PHP mock object framework", + "homepage": "https://github.com/mockery/mockery", + "keywords": [ + "BDD", + "TDD", + "library", + "mock", + "mock objects", + "mockery", + "stub", + "test", + "test double", + "testing" + ], + "support": { + "docs": "https://docs.mockery.io/", + "issues": "https://github.com/mockery/mockery/issues", + "rss": "https://github.com/mockery/mockery/releases.atom", + "security": "https://github.com/mockery/mockery/security/advisories", + "source": "https://github.com/mockery/mockery" + }, + "time": "2024-05-16T03:13:13+00:00" + }, + { + "name": "myclabs/deep-copy", + "version": "1.13.4", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3 <3.2.2" + }, + "require-dev": { + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpspec/prophecy": "^1.10", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" + }, + "type": "library", + "autoload": { + "files": [ + "src/DeepCopy/deep_copy.php" + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.4" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2025-08-01T08:46:24+00:00" + }, + { + "name": "nunomaduro/collision", + "version": "v8.9.4", + "source": { + "type": "git", + "url": "https://github.com/nunomaduro/collision.git", + "reference": "716af8f95a470e9094cfca09ed897b023be191a5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nunomaduro/collision/zipball/716af8f95a470e9094cfca09ed897b023be191a5", + "reference": "716af8f95a470e9094cfca09ed897b023be191a5", + "shasum": "" + }, + "require": { + "filp/whoops": "^2.18.4", + "nunomaduro/termwind": "^2.4.0", + "php": "^8.2.0", + "symfony/console": "^7.4.8 || ^8.0.8" + }, + "conflict": { + "laravel/framework": "<11.48.0 || >=14.0.0", + "phpunit/phpunit": "<11.5.50 || >=14.0.0" + }, + "require-dev": { + "brianium/paratest": "^7.8.5", + "larastan/larastan": "^3.9.6", + "laravel/framework": "^11.48.0 || ^12.56.0 || ^13.5.0", + "laravel/pint": "^1.29.1", + "orchestra/testbench-core": "^9.12.0 || ^10.12.1 || ^11.2.1", + "pestphp/pest": "^3.8.5 || ^4.4.3 || ^5.0.0", + "sebastian/environment": "^7.2.1 || ^8.0.4 || ^9.3.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "NunoMaduro\\Collision\\Adapters\\Laravel\\CollisionServiceProvider" + ] + }, + "branch-alias": { + "dev-8.x": "8.x-dev" + } + }, + "autoload": { + "files": [ + "./src/Adapters/Phpunit/Autoload.php" + ], + "psr-4": { + "NunoMaduro\\Collision\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "Cli error handling for console/command-line PHP applications.", + "keywords": [ + "artisan", + "cli", + "command-line", + "console", + "dev", + "error", + "handling", + "laravel", + "laravel-zero", + "php", + "symfony" + ], + "support": { + "issues": "https://github.com/nunomaduro/collision/issues", + "source": "https://github.com/nunomaduro/collision" + }, + "funding": [ + { + "url": "https://www.paypal.com/paypalme/enunomaduro", + "type": "custom" + }, + { + "url": "https://github.com/nunomaduro", + "type": "github" + }, + { + "url": "https://www.patreon.com/nunomaduro", + "type": "patreon" + } + ], + "time": "2026-04-21T14:04:20+00:00" + }, + { + "name": "phar-io/manifest", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "54750ef60c58e43759730615a392c31c80e23176" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", + "reference": "54750ef60c58e43759730615a392c31c80e23176", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-phar": "*", + "ext-xmlwriter": "*", + "phar-io/version": "^3.0.1", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "support": { + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:33:53+00:00" + }, + { + "name": "phar-io/version", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "support": { + "issues": "https://github.com/phar-io/version/issues", + "source": "https://github.com/phar-io/version/tree/3.2.1" + }, + "time": "2022-02-21T01:04:05+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "11.0.12", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "2c1ed04922802c15e1de5d7447b4856de949cf56" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/2c1ed04922802c15e1de5d7447b4856de949cf56", + "reference": "2c1ed04922802c15e1de5d7447b4856de949cf56", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-xmlwriter": "*", + "nikic/php-parser": "^5.7.0", + "php": ">=8.2", + "phpunit/php-file-iterator": "^5.1.0", + "phpunit/php-text-template": "^4.0.1", + "sebastian/code-unit-reverse-lookup": "^4.0.1", + "sebastian/complexity": "^4.0.1", + "sebastian/environment": "^7.2.1", + "sebastian/lines-of-code": "^3.0.1", + "sebastian/version": "^5.0.2", + "theseer/tokenizer": "^1.3.1" + }, + "require-dev": { + "phpunit/phpunit": "^11.5.46" + }, + "suggest": { + "ext-pcov": "PHP extension that provides line coverage", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "11.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", + "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/11.0.12" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-code-coverage", + "type": "tidelift" + } + ], + "time": "2025-12-24T07:01:01+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "5.1.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "2f3a64888c814fc235386b7387dd5b5ed92ad903" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/2f3a64888c814fc235386b7387dd5b5ed92ad903", + "reference": "2f3a64888c814fc235386b7387dd5b5ed92ad903", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", + "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/5.1.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-file-iterator", + "type": "tidelift" + } + ], + "time": "2026-02-02T13:52:54+00:00" + }, + { + "name": "phpunit/php-invoker", + "version": "5.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-invoker.git", + "reference": "c1ca3814734c07492b3d4c5f794f4b0995333da2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/c1ca3814734c07492b3d4c5f794f4b0995333da2", + "reference": "c1ca3814734c07492b3d4c5f794f4b0995333da2", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "ext-pcntl": "*", + "phpunit/phpunit": "^11.0" + }, + "suggest": { + "ext-pcntl": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Invoke callables with a timeout", + "homepage": "https://github.com/sebastianbergmann/php-invoker/", + "keywords": [ + "process" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-invoker/issues", + "security": "https://github.com/sebastianbergmann/php-invoker/security/policy", + "source": "https://github.com/sebastianbergmann/php-invoker/tree/5.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:07:44+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "3e0404dc6b300e6bf56415467ebcb3fe4f33e964" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/3e0404dc6b300e6bf56415467ebcb3fe4f33e964", + "reference": "3e0404dc6b300e6bf56415467ebcb3fe4f33e964", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-text-template/issues", + "security": "https://github.com/sebastianbergmann/php-text-template/security/policy", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:08:43+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "7.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "3b415def83fbcb41f991d9ebf16ae4ad8b7837b3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/3b415def83fbcb41f991d9ebf16ae4ad8b7837b3", + "reference": "3b415def83fbcb41f991d9ebf16ae4ad8b7837b3", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-timer/issues", + "security": "https://github.com/sebastianbergmann/php-timer/security/policy", + "source": "https://github.com/sebastianbergmann/php-timer/tree/7.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:09:35+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "11.5.55", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "adc7262fccc12de2b30f12a8aa0b33775d814f00" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/adc7262fccc12de2b30f12a8aa0b33775d814f00", + "reference": "adc7262fccc12de2b30f12a8aa0b33775d814f00", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xml": "*", + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.13.4", + "phar-io/manifest": "^2.0.4", + "phar-io/version": "^3.2.1", + "php": ">=8.2", + "phpunit/php-code-coverage": "^11.0.12", + "phpunit/php-file-iterator": "^5.1.1", + "phpunit/php-invoker": "^5.0.1", + "phpunit/php-text-template": "^4.0.1", + "phpunit/php-timer": "^7.0.1", + "sebastian/cli-parser": "^3.0.2", + "sebastian/code-unit": "^3.0.3", + "sebastian/comparator": "^6.3.3", + "sebastian/diff": "^6.0.2", + "sebastian/environment": "^7.2.1", + "sebastian/exporter": "^6.3.2", + "sebastian/global-state": "^7.0.2", + "sebastian/object-enumerator": "^6.0.1", + "sebastian/recursion-context": "^6.0.3", + "sebastian/type": "^5.1.3", + "sebastian/version": "^5.0.2", + "staabm/side-effects-detector": "^1.0.5" + }, + "suggest": { + "ext-soap": "To be able to generate mocks based on WSDL files" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "11.5-dev" + } + }, + "autoload": { + "files": [ + "src/Framework/Assert/Functions.php" + ], + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/phpunit/issues", + "security": "https://github.com/sebastianbergmann/phpunit/security/policy", + "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.55" + }, + "funding": [ + { + "url": "https://phpunit.de/sponsors.html", + "type": "custom" + }, + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", + "type": "tidelift" + } + ], + "time": "2026-02-18T12:37:06+00:00" + }, + { + "name": "sebastian/cli-parser", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "15c5dd40dc4f38794d383bb95465193f5e0ae180" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/15c5dd40dc4f38794d383bb95465193f5e0ae180", + "reference": "15c5dd40dc4f38794d383bb95465193f5e0ae180", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for parsing CLI options", + "homepage": "https://github.com/sebastianbergmann/cli-parser", + "support": { + "issues": "https://github.com/sebastianbergmann/cli-parser/issues", + "security": "https://github.com/sebastianbergmann/cli-parser/security/policy", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/3.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:41:36+00:00" + }, + { + "name": "sebastian/code-unit", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit.git", + "reference": "54391c61e4af8078e5b276ab082b6d3c54c9ad64" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/54391c61e4af8078e5b276ab082b6d3c54c9ad64", + "reference": "54391c61e4af8078e5b276ab082b6d3c54c9ad64", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the PHP code units", + "homepage": "https://github.com/sebastianbergmann/code-unit", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit/issues", + "security": "https://github.com/sebastianbergmann/code-unit/security/policy", + "source": "https://github.com/sebastianbergmann/code-unit/tree/3.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-03-19T07:56:08+00:00" + }, + { + "name": "sebastian/code-unit-reverse-lookup", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", + "reference": "183a9b2632194febd219bb9246eee421dad8d45e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/183a9b2632194febd219bb9246eee421dad8d45e", + "reference": "183a9b2632194febd219bb9246eee421dad8d45e", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Looks up which function or method a line of code belongs to", + "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", + "security": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/security/policy", + "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:45:54+00:00" + }, + { + "name": "sebastian/comparator", + "version": "6.3.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "2c95e1e86cb8dd41beb8d502057d1081ccc8eca9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/2c95e1e86cb8dd41beb8d502057d1081ccc8eca9", + "reference": "2c95e1e86cb8dd41beb8d502057d1081ccc8eca9", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-mbstring": "*", + "php": ">=8.2", + "sebastian/diff": "^6.0", + "sebastian/exporter": "^6.0" + }, + "require-dev": { + "phpunit/phpunit": "^11.4" + }, + "suggest": { + "ext-bcmath": "For comparing BcMath\\Number objects" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.3-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/comparator/issues", + "security": "https://github.com/sebastianbergmann/comparator/security/policy", + "source": "https://github.com/sebastianbergmann/comparator/tree/6.3.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/comparator", + "type": "tidelift" + } + ], + "time": "2026-01-24T09:26:40+00:00" + }, + { + "name": "sebastian/complexity", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/complexity.git", + "reference": "ee41d384ab1906c68852636b6de493846e13e5a0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/ee41d384ab1906c68852636b6de493846e13e5a0", + "reference": "ee41d384ab1906c68852636b6de493846e13e5a0", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^5.0", + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for calculating the complexity of PHP code units", + "homepage": "https://github.com/sebastianbergmann/complexity", + "support": { + "issues": "https://github.com/sebastianbergmann/complexity/issues", + "security": "https://github.com/sebastianbergmann/complexity/security/policy", + "source": "https://github.com/sebastianbergmann/complexity/tree/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:49:50+00:00" + }, + { + "name": "sebastian/diff", + "version": "6.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "b4ccd857127db5d41a5b676f24b51371d76d8544" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/b4ccd857127db5d41a5b676f24b51371d76d8544", + "reference": "b4ccd857127db5d41a5b676f24b51371d76d8544", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0", + "symfony/process": "^4.2 || ^5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "security": "https://github.com/sebastianbergmann/diff/security/policy", + "source": "https://github.com/sebastianbergmann/diff/tree/6.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:53:05+00:00" + }, + { + "name": "sebastian/environment", + "version": "7.2.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "a5c75038693ad2e8d4b6c15ba2403532647830c4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/a5c75038693ad2e8d4b6c15ba2403532647830c4", + "reference": "a5c75038693ad2e8d4b6c15ba2403532647830c4", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.3" + }, + "suggest": { + "ext-posix": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "https://github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/environment/issues", + "security": "https://github.com/sebastianbergmann/environment/security/policy", + "source": "https://github.com/sebastianbergmann/environment/tree/7.2.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/environment", + "type": "tidelift" + } + ], + "time": "2025-05-21T11:55:47+00:00" + }, + { + "name": "sebastian/exporter", + "version": "6.3.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "70a298763b40b213ec087c51c739efcaa90bcd74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/70a298763b40b213ec087c51c739efcaa90bcd74", + "reference": "70a298763b40b213ec087c51c739efcaa90bcd74", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": ">=8.2", + "sebastian/recursion-context": "^6.0" + }, + "require-dev": { + "phpunit/phpunit": "^11.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.3-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "https://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "security": "https://github.com/sebastianbergmann/exporter/security/policy", + "source": "https://github.com/sebastianbergmann/exporter/tree/6.3.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/exporter", + "type": "tidelift" + } + ], + "time": "2025-09-24T06:12:51+00:00" + }, + { + "name": "sebastian/global-state", + "version": "7.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "3be331570a721f9a4b5917f4209773de17f747d7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/3be331570a721f9a4b5917f4209773de17f747d7", + "reference": "3be331570a721f9a4b5917f4209773de17f747d7", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "sebastian/object-reflector": "^4.0", + "sebastian/recursion-context": "^6.0" + }, + "require-dev": { + "ext-dom": "*", + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "https://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/global-state/issues", + "security": "https://github.com/sebastianbergmann/global-state/security/policy", + "source": "https://github.com/sebastianbergmann/global-state/tree/7.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:57:36+00:00" + }, + { + "name": "sebastian/lines-of-code", + "version": "3.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "d36ad0d782e5756913e42ad87cb2890f4ffe467a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/d36ad0d782e5756913e42ad87cb2890f4ffe467a", + "reference": "d36ad0d782e5756913e42ad87cb2890f4ffe467a", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^5.0", + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for counting the lines of code in PHP source code", + "homepage": "https://github.com/sebastianbergmann/lines-of-code", + "support": { + "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", + "security": "https://github.com/sebastianbergmann/lines-of-code/security/policy", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/3.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:58:38+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "6.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "f5b498e631a74204185071eb41f33f38d64608aa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/f5b498e631a74204185071eb41f33f38d64608aa", + "reference": "f5b498e631a74204185071eb41f33f38d64608aa", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "sebastian/object-reflector": "^4.0", + "sebastian/recursion-context": "^6.0" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", + "security": "https://github.com/sebastianbergmann/object-enumerator/security/policy", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/6.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:00:13+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "6e1a43b411b2ad34146dee7524cb13a068bb35f9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/6e1a43b411b2ad34146dee7524cb13a068bb35f9", + "reference": "6e1a43b411b2ad34146dee7524cb13a068bb35f9", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-reflector/issues", + "security": "https://github.com/sebastianbergmann/object-reflector/security/policy", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:01:32+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "6.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "f6458abbf32a6c8174f8f26261475dc133b3d9dc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/f6458abbf32a6c8174f8f26261475dc133b3d9dc", + "reference": "f6458abbf32a6c8174f8f26261475dc133b3d9dc", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "https://github.com/sebastianbergmann/recursion-context", + "support": { + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "security": "https://github.com/sebastianbergmann/recursion-context/security/policy", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/6.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/recursion-context", + "type": "tidelift" + } + ], + "time": "2025-08-13T04:42:22+00:00" + }, + { + "name": "sebastian/type", + "version": "5.1.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "f77d2d4e78738c98d9a68d2596fe5e8fa380f449" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/f77d2d4e78738c98d9a68d2596fe5e8fa380f449", + "reference": "f77d2d4e78738c98d9a68d2596fe5e8fa380f449", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the types of the PHP type system", + "homepage": "https://github.com/sebastianbergmann/type", + "support": { + "issues": "https://github.com/sebastianbergmann/type/issues", + "security": "https://github.com/sebastianbergmann/type/security/policy", + "source": "https://github.com/sebastianbergmann/type/tree/5.1.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/type", + "type": "tidelift" + } + ], + "time": "2025-08-09T06:55:48+00:00" + }, + { + "name": "sebastian/version", + "version": "5.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "c687e3387b99f5b03b6caa64c74b63e2936ff874" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c687e3387b99f5b03b6caa64c74b63e2936ff874", + "reference": "c687e3387b99f5b03b6caa64c74b63e2936ff874", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "support": { + "issues": "https://github.com/sebastianbergmann/version/issues", + "security": "https://github.com/sebastianbergmann/version/security/policy", + "source": "https://github.com/sebastianbergmann/version/tree/5.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-10-09T05:16:32+00:00" + }, + { + "name": "staabm/side-effects-detector", + "version": "1.0.5", + "source": { + "type": "git", + "url": "https://github.com/staabm/side-effects-detector.git", + "reference": "d8334211a140ce329c13726d4a715adbddd0a163" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/staabm/side-effects-detector/zipball/d8334211a140ce329c13726d4a715adbddd0a163", + "reference": "d8334211a140ce329c13726d4a715adbddd0a163", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^1.12.6", + "phpunit/phpunit": "^9.6.21", + "symfony/var-dumper": "^5.4.43", + "tomasvotruba/type-coverage": "1.0.0", + "tomasvotruba/unused-public": "1.0.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "lib/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A static analysis tool to detect side effects in PHP code", + "keywords": [ + "static analysis" + ], + "support": { + "issues": "https://github.com/staabm/side-effects-detector/issues", + "source": "https://github.com/staabm/side-effects-detector/tree/1.0.5" + }, + "funding": [ + { + "url": "https://github.com/staabm", + "type": "github" + } + ], + "time": "2024-10-20T05:08:20+00:00" + }, + { + "name": "symfony/yaml", + "version": "v7.4.13", + "source": { + "type": "git", + "url": "https://github.com/symfony/yaml.git", + "reference": "a7ec3b1156faf8815db7683ec7c1e7338e6f977c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/yaml/zipball/a7ec3b1156faf8815db7683ec7c1e7338e6f977c", + "reference": "a7ec3b1156faf8815db7683ec7c1e7338e6f977c", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "symfony/console": "<6.4" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0|^8.0" + }, + "bin": [ + "Resources/bin/yaml-lint" + ], + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Yaml\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Loads and dumps YAML files", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/yaml/tree/v7.4.13" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-25T06:06:12+00:00" + }, + { + "name": "theseer/tokenizer", + "version": "1.3.1", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/b7489ce515e168639d17feec34b8847c326b0b3c", + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + } + ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "support": { + "issues": "https://github.com/theseer/tokenizer/issues", + "source": "https://github.com/theseer/tokenizer/tree/1.3.1" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2025-11-17T20:03:58+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": {}, + "prefer-stable": true, + "prefer-lowest": false, + "platform": { + "php": "^8.2" + }, + "platform-dev": {}, + "plugin-api-version": "2.6.0" +} diff --git a/backend/config/app.php b/backend/config/app.php new file mode 100644 index 0000000..423eed5 --- /dev/null +++ b/backend/config/app.php @@ -0,0 +1,126 @@ + env('APP_NAME', 'Laravel'), + + /* + |-------------------------------------------------------------------------- + | Application Environment + |-------------------------------------------------------------------------- + | + | This value determines the "environment" your application is currently + | running in. This may determine how you prefer to configure various + | services the application utilizes. Set this in your ".env" file. + | + */ + + 'env' => env('APP_ENV', 'production'), + + /* + |-------------------------------------------------------------------------- + | Application Debug Mode + |-------------------------------------------------------------------------- + | + | When your application is in debug mode, detailed error messages with + | stack traces will be shown on every error that occurs within your + | application. If disabled, a simple generic error page is shown. + | + */ + + 'debug' => (bool) env('APP_DEBUG', false), + + /* + |-------------------------------------------------------------------------- + | Application URL + |-------------------------------------------------------------------------- + | + | This URL is used by the console to properly generate URLs when using + | the Artisan command line tool. You should set this to the root of + | the application so that it's available within Artisan commands. + | + */ + + 'url' => env('APP_URL', 'http://localhost'), + + /* + |-------------------------------------------------------------------------- + | Application Timezone + |-------------------------------------------------------------------------- + | + | Here you may specify the default timezone for your application, which + | will be used by the PHP date and date-time functions. The timezone + | is set to "UTC" by default as it is suitable for most use cases. + | + */ + + 'timezone' => 'UTC', + + /* + |-------------------------------------------------------------------------- + | Application Locale Configuration + |-------------------------------------------------------------------------- + | + | The application locale determines the default locale that will be used + | by Laravel's translation / localization methods. This option can be + | set to any locale for which you plan to have translation strings. + | + */ + + 'locale' => env('APP_LOCALE', 'en'), + + 'fallback_locale' => env('APP_FALLBACK_LOCALE', 'en'), + + 'faker_locale' => env('APP_FAKER_LOCALE', 'en_US'), + + /* + |-------------------------------------------------------------------------- + | Encryption Key + |-------------------------------------------------------------------------- + | + | This key is utilized by Laravel's encryption services and should be set + | to a random, 32 character string to ensure that all encrypted values + | are secure. You should do this prior to deploying the application. + | + */ + + 'cipher' => 'AES-256-CBC', + + 'key' => env('APP_KEY'), + + 'previous_keys' => [ + ...array_filter( + explode(',', (string) env('APP_PREVIOUS_KEYS', '')) + ), + ], + + /* + |-------------------------------------------------------------------------- + | Maintenance Mode Driver + |-------------------------------------------------------------------------- + | + | These configuration options determine the driver used to determine and + | manage Laravel's "maintenance mode" status. The "cache" driver will + | allow maintenance mode to be controlled across multiple machines. + | + | Supported drivers: "file", "cache" + | + */ + + 'maintenance' => [ + 'driver' => env('APP_MAINTENANCE_DRIVER', 'file'), + 'store' => env('APP_MAINTENANCE_STORE', 'database'), + ], + +]; diff --git a/backend/config/auth.php b/backend/config/auth.php new file mode 100644 index 0000000..d7568ff --- /dev/null +++ b/backend/config/auth.php @@ -0,0 +1,117 @@ + [ + 'guard' => env('AUTH_GUARD', 'web'), + 'passwords' => env('AUTH_PASSWORD_BROKER', 'users'), + ], + + /* + |-------------------------------------------------------------------------- + | Authentication Guards + |-------------------------------------------------------------------------- + | + | Next, you may define every authentication guard for your application. + | Of course, a great default configuration has been defined for you + | which utilizes session storage plus the Eloquent user provider. + | + | All authentication guards have a user provider, which defines how the + | users are actually retrieved out of your database or other storage + | system used by the application. Typically, Eloquent is utilized. + | + | Supported: "session" + | + */ + + 'guards' => [ + 'web' => [ + 'driver' => 'session', + 'provider' => 'users', + ], + ], + + /* + |-------------------------------------------------------------------------- + | User Providers + |-------------------------------------------------------------------------- + | + | All authentication guards have a user provider, which defines how the + | users are actually retrieved out of your database or other storage + | system used by the application. Typically, Eloquent is utilized. + | + | If you have multiple user tables or models you may configure multiple + | providers to represent the model / table. These providers may then + | be assigned to any extra authentication guards you have defined. + | + | Supported: "database", "eloquent" + | + */ + + 'providers' => [ + 'users' => [ + 'driver' => 'eloquent', + 'model' => env('AUTH_MODEL', User::class), + ], + + // 'users' => [ + // 'driver' => 'database', + // 'table' => 'users', + // ], + ], + + /* + |-------------------------------------------------------------------------- + | Resetting Passwords + |-------------------------------------------------------------------------- + | + | These configuration options specify the behavior of Laravel's password + | reset functionality, including the table utilized for token storage + | and the user provider that is invoked to actually retrieve users. + | + | The expiry time is the number of minutes that each reset token will be + | considered valid. This security feature keeps tokens short-lived so + | they have less time to be guessed. You may change this as needed. + | + | The throttle setting is the number of seconds a user must wait before + | generating more password reset tokens. This prevents the user from + | quickly generating a very large amount of password reset tokens. + | + */ + + 'passwords' => [ + 'users' => [ + 'provider' => 'users', + 'table' => env('AUTH_PASSWORD_RESET_TOKEN_TABLE', 'password_reset_tokens'), + 'expire' => 60, + 'throttle' => 60, + ], + ], + + /* + |-------------------------------------------------------------------------- + | Password Confirmation Timeout + |-------------------------------------------------------------------------- + | + | Here you may define the number of seconds before a password confirmation + | window expires and users are asked to re-enter their password via the + | confirmation screen. By default, the timeout lasts for three hours. + | + */ + + 'password_timeout' => env('AUTH_PASSWORD_TIMEOUT', 10800), + +]; diff --git a/backend/config/cache.php b/backend/config/cache.php new file mode 100644 index 0000000..b32aead --- /dev/null +++ b/backend/config/cache.php @@ -0,0 +1,117 @@ + env('CACHE_STORE', 'database'), + + /* + |-------------------------------------------------------------------------- + | Cache Stores + |-------------------------------------------------------------------------- + | + | Here you may define all of the cache "stores" for your application as + | well as their drivers. You may even define multiple stores for the + | same cache driver to group types of items stored in your caches. + | + | Supported drivers: "array", "database", "file", "memcached", + | "redis", "dynamodb", "octane", + | "failover", "null" + | + */ + + 'stores' => [ + + 'array' => [ + 'driver' => 'array', + 'serialize' => false, + ], + + 'database' => [ + 'driver' => 'database', + 'connection' => env('DB_CACHE_CONNECTION'), + 'table' => env('DB_CACHE_TABLE', 'cache'), + 'lock_connection' => env('DB_CACHE_LOCK_CONNECTION'), + 'lock_table' => env('DB_CACHE_LOCK_TABLE'), + ], + + 'file' => [ + 'driver' => 'file', + 'path' => storage_path('framework/cache/data'), + 'lock_path' => storage_path('framework/cache/data'), + ], + + 'memcached' => [ + 'driver' => 'memcached', + 'persistent_id' => env('MEMCACHED_PERSISTENT_ID'), + 'sasl' => [ + env('MEMCACHED_USERNAME'), + env('MEMCACHED_PASSWORD'), + ], + 'options' => [ + // Memcached::OPT_CONNECT_TIMEOUT => 2000, + ], + 'servers' => [ + [ + 'host' => env('MEMCACHED_HOST', '127.0.0.1'), + 'port' => env('MEMCACHED_PORT', 11211), + 'weight' => 100, + ], + ], + ], + + 'redis' => [ + 'driver' => 'redis', + 'connection' => env('REDIS_CACHE_CONNECTION', 'cache'), + 'lock_connection' => env('REDIS_CACHE_LOCK_CONNECTION', 'default'), + ], + + 'dynamodb' => [ + 'driver' => 'dynamodb', + 'key' => env('AWS_ACCESS_KEY_ID'), + 'secret' => env('AWS_SECRET_ACCESS_KEY'), + 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), + 'table' => env('DYNAMODB_CACHE_TABLE', 'cache'), + 'endpoint' => env('DYNAMODB_ENDPOINT'), + ], + + 'octane' => [ + 'driver' => 'octane', + ], + + 'failover' => [ + 'driver' => 'failover', + 'stores' => [ + 'database', + 'array', + ], + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Cache Key Prefix + |-------------------------------------------------------------------------- + | + | When utilizing the APC, database, memcached, Redis, and DynamoDB cache + | stores, there might be other applications using the same cache. For + | that reason, you may prefix every cache key to avoid collisions. + | + */ + + 'prefix' => env('CACHE_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'-cache-'), + +]; diff --git a/backend/config/cors.php b/backend/config/cors.php new file mode 100644 index 0000000..6710393 --- /dev/null +++ b/backend/config/cors.php @@ -0,0 +1,36 @@ + ['*'], + + 'paths' => ['api/*', 'sanctum/csrf-cookie'], + + 'allowed_methods' => ['*'], + + 'allowed_origins' => [env('FRONTEND_URL', 'http://localhost:3000')], + + 'allowed_origins_patterns' => [], + + 'allowed_headers' => ['*'], + + 'exposed_headers' => [], + + 'max_age' => 0, + + 'supports_credentials' => true, + +]; diff --git a/backend/config/database.php b/backend/config/database.php new file mode 100644 index 0000000..64709ce --- /dev/null +++ b/backend/config/database.php @@ -0,0 +1,184 @@ + env('DB_CONNECTION', 'sqlite'), + + /* + |-------------------------------------------------------------------------- + | Database Connections + |-------------------------------------------------------------------------- + | + | Below are all of the database connections defined for your application. + | An example configuration is provided for each database system which + | is supported by Laravel. You're free to add / remove connections. + | + */ + + 'connections' => [ + + 'sqlite' => [ + 'driver' => 'sqlite', + 'url' => env('DB_URL'), + 'database' => env('DB_DATABASE', database_path('database.sqlite')), + 'prefix' => '', + 'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true), + 'busy_timeout' => null, + 'journal_mode' => null, + 'synchronous' => null, + 'transaction_mode' => 'DEFERRED', + ], + + 'mysql' => [ + 'driver' => 'mysql', + 'url' => env('DB_URL'), + 'host' => env('DB_HOST', '127.0.0.1'), + 'port' => env('DB_PORT', '3306'), + 'database' => env('DB_DATABASE', 'laravel'), + 'username' => env('DB_USERNAME', 'root'), + 'password' => env('DB_PASSWORD', ''), + 'unix_socket' => env('DB_SOCKET', ''), + 'charset' => env('DB_CHARSET', 'utf8mb4'), + 'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'), + 'prefix' => '', + 'prefix_indexes' => true, + 'strict' => true, + 'engine' => null, + 'options' => extension_loaded('pdo_mysql') ? array_filter([ + (PHP_VERSION_ID >= 80500 ? Mysql::ATTR_SSL_CA : PDO::MYSQL_ATTR_SSL_CA) => env('MYSQL_ATTR_SSL_CA'), + ]) : [], + ], + + 'mariadb' => [ + 'driver' => 'mariadb', + 'url' => env('DB_URL'), + 'host' => env('DB_HOST', '127.0.0.1'), + 'port' => env('DB_PORT', '3306'), + 'database' => env('DB_DATABASE', 'laravel'), + 'username' => env('DB_USERNAME', 'root'), + 'password' => env('DB_PASSWORD', ''), + 'unix_socket' => env('DB_SOCKET', ''), + 'charset' => env('DB_CHARSET', 'utf8mb4'), + 'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'), + 'prefix' => '', + 'prefix_indexes' => true, + 'strict' => true, + 'engine' => null, + 'options' => extension_loaded('pdo_mysql') ? array_filter([ + (PHP_VERSION_ID >= 80500 ? Mysql::ATTR_SSL_CA : PDO::MYSQL_ATTR_SSL_CA) => env('MYSQL_ATTR_SSL_CA'), + ]) : [], + ], + + 'pgsql' => [ + 'driver' => 'pgsql', + 'url' => env('DB_URL'), + 'host' => env('DB_HOST', '127.0.0.1'), + 'port' => env('DB_PORT', '5432'), + 'database' => env('DB_DATABASE', 'laravel'), + 'username' => env('DB_USERNAME', 'root'), + 'password' => env('DB_PASSWORD', ''), + 'charset' => env('DB_CHARSET', 'utf8'), + 'prefix' => '', + 'prefix_indexes' => true, + 'search_path' => 'public', + 'sslmode' => env('DB_SSLMODE', 'prefer'), + ], + + 'sqlsrv' => [ + 'driver' => 'sqlsrv', + 'url' => env('DB_URL'), + 'host' => env('DB_HOST', 'localhost'), + 'port' => env('DB_PORT', '1433'), + 'database' => env('DB_DATABASE', 'laravel'), + 'username' => env('DB_USERNAME', 'root'), + 'password' => env('DB_PASSWORD', ''), + 'charset' => env('DB_CHARSET', 'utf8'), + 'prefix' => '', + 'prefix_indexes' => true, + // 'encrypt' => env('DB_ENCRYPT', 'yes'), + // 'trust_server_certificate' => env('DB_TRUST_SERVER_CERTIFICATE', 'false'), + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Migration Repository Table + |-------------------------------------------------------------------------- + | + | This table keeps track of all the migrations that have already run for + | your application. Using this information, we can determine which of + | the migrations on disk haven't actually been run on the database. + | + */ + + 'migrations' => [ + 'table' => 'migrations', + 'update_date_on_publish' => true, + ], + + /* + |-------------------------------------------------------------------------- + | Redis Databases + |-------------------------------------------------------------------------- + | + | Redis is an open source, fast, and advanced key-value store that also + | provides a richer body of commands than a typical key-value system + | such as Memcached. You may define your connection settings here. + | + */ + + 'redis' => [ + + 'client' => env('REDIS_CLIENT', 'phpredis'), + + 'options' => [ + 'cluster' => env('REDIS_CLUSTER', 'redis'), + 'prefix' => env('REDIS_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'-database-'), + 'persistent' => env('REDIS_PERSISTENT', false), + ], + + 'default' => [ + 'url' => env('REDIS_URL'), + 'host' => env('REDIS_HOST', '127.0.0.1'), + 'username' => env('REDIS_USERNAME'), + 'password' => env('REDIS_PASSWORD'), + 'port' => env('REDIS_PORT', '6379'), + 'database' => env('REDIS_DB', '0'), + 'max_retries' => env('REDIS_MAX_RETRIES', 3), + 'backoff_algorithm' => env('REDIS_BACKOFF_ALGORITHM', 'decorrelated_jitter'), + 'backoff_base' => env('REDIS_BACKOFF_BASE', 100), + 'backoff_cap' => env('REDIS_BACKOFF_CAP', 1000), + ], + + 'cache' => [ + 'url' => env('REDIS_URL'), + 'host' => env('REDIS_HOST', '127.0.0.1'), + 'username' => env('REDIS_USERNAME'), + 'password' => env('REDIS_PASSWORD'), + 'port' => env('REDIS_PORT', '6379'), + 'database' => env('REDIS_CACHE_DB', '1'), + 'max_retries' => env('REDIS_MAX_RETRIES', 3), + 'backoff_algorithm' => env('REDIS_BACKOFF_ALGORITHM', 'decorrelated_jitter'), + 'backoff_base' => env('REDIS_BACKOFF_BASE', 100), + 'backoff_cap' => env('REDIS_BACKOFF_CAP', 1000), + ], + + ], + +]; diff --git a/backend/config/filesystems.php b/backend/config/filesystems.php new file mode 100644 index 0000000..37d8fca --- /dev/null +++ b/backend/config/filesystems.php @@ -0,0 +1,80 @@ + env('FILESYSTEM_DISK', 'local'), + + /* + |-------------------------------------------------------------------------- + | Filesystem Disks + |-------------------------------------------------------------------------- + | + | Below you may configure as many filesystem disks as necessary, and you + | may even configure multiple disks for the same driver. Examples for + | most supported storage drivers are configured here for reference. + | + | Supported drivers: "local", "ftp", "sftp", "s3" + | + */ + + 'disks' => [ + + 'local' => [ + 'driver' => 'local', + 'root' => storage_path('app/private'), + 'serve' => true, + 'throw' => false, + 'report' => false, + ], + + 'public' => [ + 'driver' => 'local', + 'root' => storage_path('app/public'), + 'url' => rtrim(env('APP_URL', 'http://localhost'), '/').'/storage', + 'visibility' => 'public', + 'throw' => false, + 'report' => false, + ], + + 's3' => [ + 'driver' => 's3', + 'key' => env('AWS_ACCESS_KEY_ID'), + 'secret' => env('AWS_SECRET_ACCESS_KEY'), + 'region' => env('AWS_DEFAULT_REGION'), + 'bucket' => env('AWS_BUCKET'), + 'url' => env('AWS_URL'), + 'endpoint' => env('AWS_ENDPOINT'), + 'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false), + 'throw' => false, + 'report' => false, + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Symbolic Links + |-------------------------------------------------------------------------- + | + | Here you may configure the symbolic links that will be created when the + | `storage:link` Artisan command is executed. The array keys should be + | the locations of the links and the values should be their targets. + | + */ + + 'links' => [ + public_path('storage') => storage_path('app/public'), + ], + +]; diff --git a/backend/config/logging.php b/backend/config/logging.php new file mode 100644 index 0000000..b09cb25 --- /dev/null +++ b/backend/config/logging.php @@ -0,0 +1,132 @@ + env('LOG_CHANNEL', 'stack'), + + /* + |-------------------------------------------------------------------------- + | Deprecations Log Channel + |-------------------------------------------------------------------------- + | + | This option controls the log channel that should be used to log warnings + | regarding deprecated PHP and library features. This allows you to get + | your application ready for upcoming major versions of dependencies. + | + */ + + 'deprecations' => [ + 'channel' => env('LOG_DEPRECATIONS_CHANNEL', 'null'), + 'trace' => env('LOG_DEPRECATIONS_TRACE', false), + ], + + /* + |-------------------------------------------------------------------------- + | Log Channels + |-------------------------------------------------------------------------- + | + | Here you may configure the log channels for your application. Laravel + | utilizes the Monolog PHP logging library, which includes a variety + | of powerful log handlers and formatters that you're free to use. + | + | Available drivers: "single", "daily", "slack", "syslog", + | "errorlog", "monolog", "custom", "stack" + | + */ + + 'channels' => [ + + 'stack' => [ + 'driver' => 'stack', + 'channels' => explode(',', (string) env('LOG_STACK', 'single')), + 'ignore_exceptions' => false, + ], + + 'single' => [ + 'driver' => 'single', + 'path' => storage_path('logs/laravel.log'), + 'level' => env('LOG_LEVEL', 'debug'), + 'replace_placeholders' => true, + ], + + 'daily' => [ + 'driver' => 'daily', + 'path' => storage_path('logs/laravel.log'), + 'level' => env('LOG_LEVEL', 'debug'), + 'days' => env('LOG_DAILY_DAYS', 14), + 'replace_placeholders' => true, + ], + + 'slack' => [ + 'driver' => 'slack', + 'url' => env('LOG_SLACK_WEBHOOK_URL'), + 'username' => env('LOG_SLACK_USERNAME', env('APP_NAME', 'Laravel')), + 'emoji' => env('LOG_SLACK_EMOJI', ':boom:'), + 'level' => env('LOG_LEVEL', 'critical'), + 'replace_placeholders' => true, + ], + + 'papertrail' => [ + 'driver' => 'monolog', + 'level' => env('LOG_LEVEL', 'debug'), + 'handler' => env('LOG_PAPERTRAIL_HANDLER', SyslogUdpHandler::class), + 'handler_with' => [ + 'host' => env('PAPERTRAIL_URL'), + 'port' => env('PAPERTRAIL_PORT'), + 'connectionString' => 'tls://'.env('PAPERTRAIL_URL').':'.env('PAPERTRAIL_PORT'), + ], + 'processors' => [PsrLogMessageProcessor::class], + ], + + 'stderr' => [ + 'driver' => 'monolog', + 'level' => env('LOG_LEVEL', 'debug'), + 'handler' => StreamHandler::class, + 'handler_with' => [ + 'stream' => 'php://stderr', + ], + 'formatter' => env('LOG_STDERR_FORMATTER'), + 'processors' => [PsrLogMessageProcessor::class], + ], + + 'syslog' => [ + 'driver' => 'syslog', + 'level' => env('LOG_LEVEL', 'debug'), + 'facility' => env('LOG_SYSLOG_FACILITY', LOG_USER), + 'replace_placeholders' => true, + ], + + 'errorlog' => [ + 'driver' => 'errorlog', + 'level' => env('LOG_LEVEL', 'debug'), + 'replace_placeholders' => true, + ], + + 'null' => [ + 'driver' => 'monolog', + 'handler' => NullHandler::class, + ], + + 'emergency' => [ + 'path' => storage_path('logs/laravel.log'), + ], + + ], + +]; diff --git a/backend/config/mail.php b/backend/config/mail.php new file mode 100644 index 0000000..e32e88d --- /dev/null +++ b/backend/config/mail.php @@ -0,0 +1,118 @@ + env('MAIL_MAILER', 'log'), + + /* + |-------------------------------------------------------------------------- + | Mailer Configurations + |-------------------------------------------------------------------------- + | + | Here you may configure all of the mailers used by your application plus + | their respective settings. Several examples have been configured for + | you and you are free to add your own as your application requires. + | + | Laravel supports a variety of mail "transport" drivers that can be used + | when delivering an email. You may specify which one you're using for + | your mailers below. You may also add additional mailers if needed. + | + | Supported: "smtp", "sendmail", "mailgun", "ses", "ses-v2", + | "postmark", "resend", "log", "array", + | "failover", "roundrobin" + | + */ + + 'mailers' => [ + + 'smtp' => [ + 'transport' => 'smtp', + 'scheme' => env('MAIL_SCHEME'), + 'url' => env('MAIL_URL'), + 'host' => env('MAIL_HOST', '127.0.0.1'), + 'port' => env('MAIL_PORT', 2525), + 'username' => env('MAIL_USERNAME'), + 'password' => env('MAIL_PASSWORD'), + 'timeout' => null, + 'local_domain' => env('MAIL_EHLO_DOMAIN', parse_url((string) env('APP_URL', 'http://localhost'), PHP_URL_HOST)), + ], + + 'ses' => [ + 'transport' => 'ses', + ], + + 'postmark' => [ + 'transport' => 'postmark', + // 'message_stream_id' => env('POSTMARK_MESSAGE_STREAM_ID'), + // 'client' => [ + // 'timeout' => 5, + // ], + ], + + 'resend' => [ + 'transport' => 'resend', + ], + + 'sendmail' => [ + 'transport' => 'sendmail', + 'path' => env('MAIL_SENDMAIL_PATH', '/usr/sbin/sendmail -bs -i'), + ], + + 'log' => [ + 'transport' => 'log', + 'channel' => env('MAIL_LOG_CHANNEL'), + ], + + 'array' => [ + 'transport' => 'array', + ], + + 'failover' => [ + 'transport' => 'failover', + 'mailers' => [ + 'smtp', + 'log', + ], + 'retry_after' => 60, + ], + + 'roundrobin' => [ + 'transport' => 'roundrobin', + 'mailers' => [ + 'ses', + 'postmark', + ], + 'retry_after' => 60, + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Global "From" Address + |-------------------------------------------------------------------------- + | + | You may wish for all emails sent by your application to be sent from + | the same address. Here you may specify a name and address that is + | used globally for all emails that are sent by your application. + | + */ + + 'from' => [ + 'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'), + 'name' => env('MAIL_FROM_NAME', env('APP_NAME', 'Laravel')), + ], + +]; diff --git a/backend/config/queue.php b/backend/config/queue.php new file mode 100644 index 0000000..79c2c0a --- /dev/null +++ b/backend/config/queue.php @@ -0,0 +1,129 @@ + env('QUEUE_CONNECTION', 'database'), + + /* + |-------------------------------------------------------------------------- + | Queue Connections + |-------------------------------------------------------------------------- + | + | Here you may configure the connection options for every queue backend + | used by your application. An example configuration is provided for + | each backend supported by Laravel. You're also free to add more. + | + | Drivers: "sync", "database", "beanstalkd", "sqs", "redis", + | "deferred", "background", "failover", "null" + | + */ + + 'connections' => [ + + 'sync' => [ + 'driver' => 'sync', + ], + + 'database' => [ + 'driver' => 'database', + 'connection' => env('DB_QUEUE_CONNECTION'), + 'table' => env('DB_QUEUE_TABLE', 'jobs'), + 'queue' => env('DB_QUEUE', 'default'), + 'retry_after' => (int) env('DB_QUEUE_RETRY_AFTER', 90), + 'after_commit' => false, + ], + + 'beanstalkd' => [ + 'driver' => 'beanstalkd', + 'host' => env('BEANSTALKD_QUEUE_HOST', 'localhost'), + 'queue' => env('BEANSTALKD_QUEUE', 'default'), + 'retry_after' => (int) env('BEANSTALKD_QUEUE_RETRY_AFTER', 90), + 'block_for' => 0, + 'after_commit' => false, + ], + + 'sqs' => [ + 'driver' => 'sqs', + 'key' => env('AWS_ACCESS_KEY_ID'), + 'secret' => env('AWS_SECRET_ACCESS_KEY'), + 'prefix' => env('SQS_PREFIX', 'https://sqs.us-east-1.amazonaws.com/your-account-id'), + 'queue' => env('SQS_QUEUE', 'default'), + 'suffix' => env('SQS_SUFFIX'), + 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), + 'after_commit' => false, + ], + + 'redis' => [ + 'driver' => 'redis', + 'connection' => env('REDIS_QUEUE_CONNECTION', 'default'), + 'queue' => env('REDIS_QUEUE', 'default'), + 'retry_after' => (int) env('REDIS_QUEUE_RETRY_AFTER', 90), + 'block_for' => null, + 'after_commit' => false, + ], + + 'deferred' => [ + 'driver' => 'deferred', + ], + + 'background' => [ + 'driver' => 'background', + ], + + 'failover' => [ + 'driver' => 'failover', + 'connections' => [ + 'database', + 'deferred', + ], + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Job Batching + |-------------------------------------------------------------------------- + | + | The following options configure the database and table that store job + | batching information. These options can be updated to any database + | connection and table which has been defined by your application. + | + */ + + 'batching' => [ + 'database' => env('DB_CONNECTION', 'sqlite'), + 'table' => 'job_batches', + ], + + /* + |-------------------------------------------------------------------------- + | Failed Queue Jobs + |-------------------------------------------------------------------------- + | + | These options configure the behavior of failed queue job logging so you + | can control how and where failed jobs are stored. Laravel ships with + | support for storing failed jobs in a simple file or in a database. + | + | Supported drivers: "database-uuids", "dynamodb", "file", "null" + | + */ + + 'failed' => [ + 'driver' => env('QUEUE_FAILED_DRIVER', 'database-uuids'), + 'database' => env('DB_CONNECTION', 'sqlite'), + 'table' => 'failed_jobs', + ], + +]; diff --git a/backend/config/sanctum.php b/backend/config/sanctum.php new file mode 100644 index 0000000..8ae01bf --- /dev/null +++ b/backend/config/sanctum.php @@ -0,0 +1,87 @@ + explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf( + '%s%s%s', + 'localhost,localhost:3000,127.0.0.1,127.0.0.1:3000,127.0.0.1:8000,::1', + Sanctum::currentApplicationUrlWithPort(), + env('FRONTEND_URL') ? ','.parse_url(env('FRONTEND_URL'), PHP_URL_HOST) : '' + ))), + + /* + |-------------------------------------------------------------------------- + | Sanctum Guards + |-------------------------------------------------------------------------- + | + | This array contains the authentication guards that will be checked when + | Sanctum is trying to authenticate a request. If none of these guards + | are able to authenticate the request, Sanctum will use the bearer + | token that's present on an incoming request for authentication. + | + */ + + 'guard' => ['web'], + + /* + |-------------------------------------------------------------------------- + | Expiration Minutes + |-------------------------------------------------------------------------- + | + | This value controls the number of minutes until an issued token will be + | considered expired. This will override any values set in the token's + | "expires_at" attribute, but first-party sessions are not affected. + | + */ + + 'expiration' => null, + + /* + |-------------------------------------------------------------------------- + | Token Prefix + |-------------------------------------------------------------------------- + | + | Sanctum can prefix new tokens in order to take advantage of numerous + | security scanning initiatives maintained by open source platforms + | that notify developers if they commit tokens into repositories. + | + | See: https://docs.github.com/en/code-security/secret-scanning/about-secret-scanning + | + */ + + 'token_prefix' => env('SANCTUM_TOKEN_PREFIX', ''), + + /* + |-------------------------------------------------------------------------- + | Sanctum Middleware + |-------------------------------------------------------------------------- + | + | When authenticating your first-party SPA with Sanctum you may need to + | customize some of the middleware Sanctum uses while processing the + | request. You may change the middleware listed below as required. + | + */ + + 'middleware' => [ + 'authenticate_session' => AuthenticateSession::class, + 'encrypt_cookies' => EncryptCookies::class, + 'validate_csrf_token' => ValidateCsrfToken::class, + ], + +]; diff --git a/backend/config/services.php b/backend/config/services.php new file mode 100644 index 0000000..6a90eb8 --- /dev/null +++ b/backend/config/services.php @@ -0,0 +1,38 @@ + [ + 'key' => env('POSTMARK_API_KEY'), + ], + + 'resend' => [ + 'key' => env('RESEND_API_KEY'), + ], + + 'ses' => [ + 'key' => env('AWS_ACCESS_KEY_ID'), + 'secret' => env('AWS_SECRET_ACCESS_KEY'), + 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), + ], + + 'slack' => [ + 'notifications' => [ + 'bot_user_oauth_token' => env('SLACK_BOT_USER_OAUTH_TOKEN'), + 'channel' => env('SLACK_BOT_USER_DEFAULT_CHANNEL'), + ], + ], + +]; diff --git a/backend/config/session.php b/backend/config/session.php new file mode 100644 index 0000000..5b541b7 --- /dev/null +++ b/backend/config/session.php @@ -0,0 +1,217 @@ + env('SESSION_DRIVER', 'database'), + + /* + |-------------------------------------------------------------------------- + | Session Lifetime + |-------------------------------------------------------------------------- + | + | Here you may specify the number of minutes that you wish the session + | to be allowed to remain idle before it expires. If you want them + | to expire immediately when the browser is closed then you may + | indicate that via the expire_on_close configuration option. + | + */ + + 'lifetime' => (int) env('SESSION_LIFETIME', 120), + + 'expire_on_close' => env('SESSION_EXPIRE_ON_CLOSE', false), + + /* + |-------------------------------------------------------------------------- + | Session Encryption + |-------------------------------------------------------------------------- + | + | This option allows you to easily specify that all of your session data + | should be encrypted before it's stored. All encryption is performed + | automatically by Laravel and you may use the session like normal. + | + */ + + 'encrypt' => env('SESSION_ENCRYPT', false), + + /* + |-------------------------------------------------------------------------- + | Session File Location + |-------------------------------------------------------------------------- + | + | When utilizing the "file" session driver, the session files are placed + | on disk. The default storage location is defined here; however, you + | are free to provide another location where they should be stored. + | + */ + + 'files' => storage_path('framework/sessions'), + + /* + |-------------------------------------------------------------------------- + | Session Database Connection + |-------------------------------------------------------------------------- + | + | When using the "database" or "redis" session drivers, you may specify a + | connection that should be used to manage these sessions. This should + | correspond to a connection in your database configuration options. + | + */ + + 'connection' => env('SESSION_CONNECTION'), + + /* + |-------------------------------------------------------------------------- + | Session Database Table + |-------------------------------------------------------------------------- + | + | When using the "database" session driver, you may specify the table to + | be used to store sessions. Of course, a sensible default is defined + | for you; however, you're welcome to change this to another table. + | + */ + + 'table' => env('SESSION_TABLE', 'sessions'), + + /* + |-------------------------------------------------------------------------- + | Session Cache Store + |-------------------------------------------------------------------------- + | + | When using one of the framework's cache driven session backends, you may + | define the cache store which should be used to store the session data + | between requests. This must match one of your defined cache stores. + | + | Affects: "dynamodb", "memcached", "redis" + | + */ + + 'store' => env('SESSION_STORE'), + + /* + |-------------------------------------------------------------------------- + | Session Sweeping Lottery + |-------------------------------------------------------------------------- + | + | Some session drivers must manually sweep their storage location to get + | rid of old sessions from storage. Here are the chances that it will + | happen on a given request. By default, the odds are 2 out of 100. + | + */ + + 'lottery' => [2, 100], + + /* + |-------------------------------------------------------------------------- + | Session Cookie Name + |-------------------------------------------------------------------------- + | + | Here you may change the name of the session cookie that is created by + | the framework. Typically, you should not need to change this value + | since doing so does not grant a meaningful security improvement. + | + */ + + 'cookie' => env( + 'SESSION_COOKIE', + Str::slug((string) env('APP_NAME', 'laravel')).'-session' + ), + + /* + |-------------------------------------------------------------------------- + | Session Cookie Path + |-------------------------------------------------------------------------- + | + | The session cookie path determines the path for which the cookie will + | be regarded as available. Typically, this will be the root path of + | your application, but you're free to change this when necessary. + | + */ + + 'path' => env('SESSION_PATH', '/'), + + /* + |-------------------------------------------------------------------------- + | Session Cookie Domain + |-------------------------------------------------------------------------- + | + | This value determines the domain and subdomains the session cookie is + | available to. By default, the cookie will be available to the root + | domain without subdomains. Typically, this shouldn't be changed. + | + */ + + 'domain' => env('SESSION_DOMAIN'), + + /* + |-------------------------------------------------------------------------- + | HTTPS Only Cookies + |-------------------------------------------------------------------------- + | + | By setting this option to true, session cookies will only be sent back + | to the server if the browser has a HTTPS connection. This will keep + | the cookie from being sent to you when it can't be done securely. + | + */ + + 'secure' => env('SESSION_SECURE_COOKIE'), + + /* + |-------------------------------------------------------------------------- + | HTTP Access Only + |-------------------------------------------------------------------------- + | + | Setting this value to true will prevent JavaScript from accessing the + | value of the cookie and the cookie will only be accessible through + | the HTTP protocol. It's unlikely you should disable this option. + | + */ + + 'http_only' => env('SESSION_HTTP_ONLY', true), + + /* + |-------------------------------------------------------------------------- + | Same-Site Cookies + |-------------------------------------------------------------------------- + | + | This option determines how your cookies behave when cross-site requests + | take place, and can be used to mitigate CSRF attacks. By default, we + | will set this value to "lax" to permit secure cross-site requests. + | + | See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#samesitesamesite-value + | + | Supported: "lax", "strict", "none", null + | + */ + + 'same_site' => env('SESSION_SAME_SITE', 'lax'), + + /* + |-------------------------------------------------------------------------- + | Partitioned Cookies + |-------------------------------------------------------------------------- + | + | Setting this value to true will tie the cookie to the top-level site for + | a cross-site context. Partitioned cookies are accepted by the browser + | when flagged "secure" and the Same-Site attribute is set to "none". + | + */ + + 'partitioned' => env('SESSION_PARTITIONED_COOKIE', false), + +]; diff --git a/backend/database/.gitignore b/backend/database/.gitignore new file mode 100644 index 0000000..9b19b93 --- /dev/null +++ b/backend/database/.gitignore @@ -0,0 +1 @@ +*.sqlite* diff --git a/backend/database/factories/PatientFactory.php b/backend/database/factories/PatientFactory.php new file mode 100644 index 0000000..ac6b320 --- /dev/null +++ b/backend/database/factories/PatientFactory.php @@ -0,0 +1,29 @@ + + */ +class PatientFactory extends Factory +{ + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + 'name' => $this->faker->name(), + 'phone' => $this->faker->numerify('08##########'), + 'bpjs_number' => null, + 'birth_place' => null, + 'birth_date' => null, + 'gender' => null, + ]; + } +} diff --git a/backend/database/factories/UserFactory.php b/backend/database/factories/UserFactory.php new file mode 100644 index 0000000..de48345 --- /dev/null +++ b/backend/database/factories/UserFactory.php @@ -0,0 +1,49 @@ + + */ +class UserFactory extends Factory +{ + /** + * The current password being used by the factory. + */ + protected static ?string $password; + + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + 'email' => fake()->unique()->safeEmail(), + 'email_verified_at' => now(), + 'password' => static::$password ??= Hash::make('password'), + 'role' => 'patient', + 'status' => 'active', + 'remember_token' => Str::random(10), + ]; + } + +// public function doctor(): static +// public function nurse(): static + + /** + * Indicate that the model's email address should be unverified. + */ + public function unverified(): static + { + return $this->state(fn (array $attributes) => [ + 'email_verified_at' => null, + ]); + } +} diff --git a/backend/database/migrations/0001_01_01_000000_create_users_table.php b/backend/database/migrations/0001_01_01_000000_create_users_table.php new file mode 100644 index 0000000..02839d6 --- /dev/null +++ b/backend/database/migrations/0001_01_01_000000_create_users_table.php @@ -0,0 +1,50 @@ +id(); + $table->string('email')->unique(); + $table->timestamp('email_verified_at')->nullable(); + $table->string('password'); + $table->enum('role', ['patient', 'doctor', 'nurse', 'admin']); + $table->enum('status', ['active', 'inactive'])->default('active'); + $table->rememberToken(); + $table->timestamps(); + }); + + Schema::create('password_reset_tokens', function (Blueprint $table) { + $table->string('email')->primary(); + $table->string('token'); + $table->timestamp('created_at')->nullable(); + }); + + Schema::create('sessions', function (Blueprint $table) { + $table->string('id')->primary(); + $table->foreignId('user_id')->nullable()->index(); + $table->string('ip_address', 45)->nullable(); + $table->text('user_agent')->nullable(); + $table->longText('payload'); + $table->integer('last_activity')->index(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('users'); + Schema::dropIfExists('password_reset_tokens'); + Schema::dropIfExists('sessions'); + } +}; diff --git a/backend/database/migrations/0001_01_01_000001_create_cache_table.php b/backend/database/migrations/0001_01_01_000001_create_cache_table.php new file mode 100644 index 0000000..ed758bd --- /dev/null +++ b/backend/database/migrations/0001_01_01_000001_create_cache_table.php @@ -0,0 +1,35 @@ +string('key')->primary(); + $table->mediumText('value'); + $table->integer('expiration')->index(); + }); + + Schema::create('cache_locks', function (Blueprint $table) { + $table->string('key')->primary(); + $table->string('owner'); + $table->integer('expiration')->index(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('cache'); + Schema::dropIfExists('cache_locks'); + } +}; diff --git a/backend/database/migrations/0001_01_01_000002_create_jobs_table.php b/backend/database/migrations/0001_01_01_000002_create_jobs_table.php new file mode 100644 index 0000000..425e705 --- /dev/null +++ b/backend/database/migrations/0001_01_01_000002_create_jobs_table.php @@ -0,0 +1,57 @@ +id(); + $table->string('queue')->index(); + $table->longText('payload'); + $table->unsignedTinyInteger('attempts'); + $table->unsignedInteger('reserved_at')->nullable(); + $table->unsignedInteger('available_at'); + $table->unsignedInteger('created_at'); + }); + + Schema::create('job_batches', function (Blueprint $table) { + $table->string('id')->primary(); + $table->string('name'); + $table->integer('total_jobs'); + $table->integer('pending_jobs'); + $table->integer('failed_jobs'); + $table->longText('failed_job_ids'); + $table->mediumText('options')->nullable(); + $table->integer('cancelled_at')->nullable(); + $table->integer('created_at'); + $table->integer('finished_at')->nullable(); + }); + + Schema::create('failed_jobs', function (Blueprint $table) { + $table->id(); + $table->string('uuid')->unique(); + $table->text('connection'); + $table->text('queue'); + $table->longText('payload'); + $table->longText('exception'); + $table->timestamp('failed_at')->useCurrent(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('jobs'); + Schema::dropIfExists('job_batches'); + Schema::dropIfExists('failed_jobs'); + } +}; diff --git a/backend/database/migrations/2026_06_07_065413_create_personal_access_tokens_table.php b/backend/database/migrations/2026_06_07_065413_create_personal_access_tokens_table.php new file mode 100644 index 0000000..40ff706 --- /dev/null +++ b/backend/database/migrations/2026_06_07_065413_create_personal_access_tokens_table.php @@ -0,0 +1,33 @@ +id(); + $table->morphs('tokenable'); + $table->text('name'); + $table->string('token', 64)->unique(); + $table->text('abilities')->nullable(); + $table->timestamp('last_used_at')->nullable(); + $table->timestamp('expires_at')->nullable()->index(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('personal_access_tokens'); + } +}; diff --git a/backend/database/migrations/2026_06_12_125654_create_patients_table.php b/backend/database/migrations/2026_06_12_125654_create_patients_table.php new file mode 100644 index 0000000..148bf7f --- /dev/null +++ b/backend/database/migrations/2026_06_12_125654_create_patients_table.php @@ -0,0 +1,34 @@ +id(); + $table->foreignId('user_id')->constrained('users', 'id')->cascadeOnDelete(); + $table->string('name'); + $table->string('phone', 15); + $table->string('bpjs_number', 50)->nullable(); + $table->string('birth_place', 100)->nullable(); + $table->date('birth_date')->nullable(); + $table->enum('gender', ['male', 'female'])->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('patients'); + } +}; diff --git a/backend/database/seeders/DatabaseSeeder.php b/backend/database/seeders/DatabaseSeeder.php new file mode 100644 index 0000000..6b901f8 --- /dev/null +++ b/backend/database/seeders/DatabaseSeeder.php @@ -0,0 +1,25 @@ +create(); + + User::factory()->create([ + 'name' => 'Test User', + 'email' => 'test@example.com', + ]); + } +} diff --git a/backend/database/seeders/PatientSeeder.php b/backend/database/seeders/PatientSeeder.php new file mode 100644 index 0000000..2ae1ea6 --- /dev/null +++ b/backend/database/seeders/PatientSeeder.php @@ -0,0 +1,17 @@ + + + + + tests/Unit + + + tests/Feature + + + + + app + + + + + + + + + + + + + + + + + + + + diff --git a/backend/public/.htaccess b/backend/public/.htaccess new file mode 100644 index 0000000..b574a59 --- /dev/null +++ b/backend/public/.htaccess @@ -0,0 +1,25 @@ + + + Options -MultiViews -Indexes + + + RewriteEngine On + + # Handle Authorization Header + RewriteCond %{HTTP:Authorization} . + RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] + + # Handle X-XSRF-Token Header + RewriteCond %{HTTP:x-xsrf-token} . + RewriteRule .* - [E=HTTP_X_XSRF_TOKEN:%{HTTP:X-XSRF-Token}] + + # Redirect Trailing Slashes If Not A Folder... + RewriteCond %{REQUEST_FILENAME} !-d + RewriteCond %{REQUEST_URI} (.+)/$ + RewriteRule ^ %1 [L,R=301] + + # Send Requests To Front Controller... + RewriteCond %{REQUEST_FILENAME} !-d + RewriteCond %{REQUEST_FILENAME} !-f + RewriteRule ^ index.php [L] + diff --git a/frontend/src/api/.gitkeep b/backend/public/favicon.ico similarity index 100% rename from frontend/src/api/.gitkeep rename to backend/public/favicon.ico diff --git a/backend/public/index.php b/backend/public/index.php new file mode 100644 index 0000000..ee8f07e --- /dev/null +++ b/backend/public/index.php @@ -0,0 +1,20 @@ +handleRequest(Request::capture()); diff --git a/backend/public/robots.txt b/backend/public/robots.txt new file mode 100644 index 0000000..eb05362 --- /dev/null +++ b/backend/public/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: diff --git a/backend/resources/views/.gitkeep b/backend/resources/views/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/backend/resources/views/.gitkeep @@ -0,0 +1 @@ + diff --git a/backend/routes/api.php b/backend/routes/api.php new file mode 100644 index 0000000..c8ff9bd --- /dev/null +++ b/backend/routes/api.php @@ -0,0 +1,12 @@ +get('/user', function (Request $request) { + return $request->user(); +}); + +Route::prefix('auth')->group(function () { + require __DIR__.'/auth.php'; +}); diff --git a/backend/routes/auth.php b/backend/routes/auth.php new file mode 100644 index 0000000..27d13f5 --- /dev/null +++ b/backend/routes/auth.php @@ -0,0 +1,45 @@ +middleware('guest') + ->name('register'); + +Route::post('/login', [AuthenticatedSessionController::class, 'store']) + ->middleware('guest') + ->name('login'); + +// Protected +Route::middleware('auth:sanctum')->group(function () { + Route::post('/logout', [AuthenticatedSessionController::class, 'destroy']) + ->name('logout'); + + Route::get('/me', [AuthController::class, 'me']) + ->name('me'); +}); + +Route::post('/forgot-password', [PasswordResetLinkController::class, 'store']) + ->middleware('guest') + ->name('password.email'); + +Route::post('/reset-password', [NewPasswordController::class, 'store']) + ->middleware('guest') + ->name('password.store'); + +Route::get('/verify-email/{id}/{hash}', VerifyEmailController::class) + ->middleware(['auth', 'signed', 'throttle:6,1']) + ->name('verification.verify'); + +Route::post('/email/verification-notification', [EmailVerificationNotificationController::class, 'store']) + ->middleware(['auth', 'throttle:6,1']) + ->name('verification.send'); diff --git a/backend/routes/console.php b/backend/routes/console.php new file mode 100644 index 0000000..3c9adf1 --- /dev/null +++ b/backend/routes/console.php @@ -0,0 +1,8 @@ +comment(Inspiring::quote()); +})->purpose('Display an inspiring quote'); diff --git a/backend/routes/web.php b/backend/routes/web.php new file mode 100644 index 0000000..cdd4027 --- /dev/null +++ b/backend/routes/web.php @@ -0,0 +1,9 @@ + app()->version()]; +}); + +require __DIR__.'/auth.php'; diff --git a/backend/storage/app/.gitignore b/backend/storage/app/.gitignore new file mode 100644 index 0000000..fedb287 --- /dev/null +++ b/backend/storage/app/.gitignore @@ -0,0 +1,4 @@ +* +!private/ +!public/ +!.gitignore diff --git a/backend/storage/app/private/.gitignore b/backend/storage/app/private/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/backend/storage/app/private/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/backend/storage/app/public/.gitignore b/backend/storage/app/public/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/backend/storage/app/public/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/backend/storage/framework/.gitignore b/backend/storage/framework/.gitignore new file mode 100644 index 0000000..05c4471 --- /dev/null +++ b/backend/storage/framework/.gitignore @@ -0,0 +1,9 @@ +compiled.php +config.php +down +events.scanned.php +maintenance.php +routes.php +routes.scanned.php +schedule-* +services.json diff --git a/backend/storage/framework/cache/.gitignore b/backend/storage/framework/cache/.gitignore new file mode 100644 index 0000000..01e4a6c --- /dev/null +++ b/backend/storage/framework/cache/.gitignore @@ -0,0 +1,3 @@ +* +!data/ +!.gitignore diff --git a/backend/storage/framework/cache/data/.gitignore b/backend/storage/framework/cache/data/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/backend/storage/framework/cache/data/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/backend/storage/framework/sessions/.gitignore b/backend/storage/framework/sessions/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/backend/storage/framework/sessions/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/backend/storage/framework/testing/.gitignore b/backend/storage/framework/testing/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/backend/storage/framework/testing/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/backend/storage/framework/views/.gitignore b/backend/storage/framework/views/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/backend/storage/framework/views/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/backend/storage/logs/.gitignore b/backend/storage/logs/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/backend/storage/logs/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/backend/test.http b/backend/test.http new file mode 100644 index 0000000..ae3807e --- /dev/null +++ b/backend/test.http @@ -0,0 +1,44 @@ +@baseUrl = http://localhost:8000 + +### User Data +@name = Handika Testing +@email = handika.testing@example.com +@password = Password123! +@phone = 081234567890 + +### Sanctum +@xsrfToken = eyJpdiI6IkV3S24vTEVqK3VLMWJyQ1hrV3hkOEE9PSIsInZhbHVlIjoiZFB3dGQ3azNUakZqWUxIZUEwdldUTjZIMFRJVW9LQUFwRlIxR2JxaWNDWHdtRzNwL1dRN1UyZlVBYW9JMGdtbHV5dVZHa0w0UGtRdUFaZDE0cTFQZEVDK2pyeGNtSWJRRUIzSk1lMndGdGtPNkpGY1M5ZWcvc3hDWVc2a2RKdUkiLCJtYWMiOiI4ZTkxZGYwNjNjOTg4MWIzZWJhYzk4N2ZiNzk3ZTE1MmM1OTQzYTMyMTVlZGE3M2Y0YzYwOThiNzM3N2Q2NWE4IiwidGFnIjoiIn0%3D +@sessionCookie = eyJpdiI6IlVLTkxwTVNqbFk3UGhXNjFTNk5vd3c9PSIsInZhbHVlIjoibi9qWGhPNkRkalpLZkNsS01tWGlnMFpOenhPQktES1RZQ01hdThUT3BRM0cxdDRNek96amRQZk5CbWtYamZ6YWEyTXVpQ3huWEpESTl6UVlGUDErSXJMeE5KM0t0bUdMdm1XMmMrcHlkaklHdTJnSXozNzlKNndrckVSdTgwNmciLCJtYWMiOiIxZTU3ODljOWFjMjIyYWZlOGRkMGZlMjNiY2I0ODg0YjNiYmFjZDkwY2EyOTc1MzI1ZmYxZDU3NmZmMWU4YzIzIiwidGFnIjoiIn0%3D + + +GET {{baseUrl}}/sanctum/csrf-cookie +Origin: http://localhost:3000 + +### Register User +POST {{baseUrl}}/api/auth/register +Content-Type: application/json +Origin: http://localhost:3000 +X-XSRF-TOKEN: {{xsrfToken}} +Cookie: XSRF-TOKEN={{xsrfToken}}; laravel-session={{sessionCookie}} + +{ + "name": "{{name}}", + "email": "{{email}}", + "password": "{{password}}", + "password_confirmation": "{{password}}", + "phone": "{{phone}}" +} + + +### Login +POST {{baseUrl}}/api/auth/login +Content-Type: application/json +Accept: application/json +Origin: http://localhost:3000 +X-XSRF-TOKEN: {{xsrfToken}} +Cookie: XSRF-TOKEN={{xsrfToken}}; laravel-session={{sessionCookie}} + +{ + "email": "{{email}}", + "password": "{{password}}" +} diff --git a/backend/tests/Feature/Api/Auth/LoginTest.php b/backend/tests/Feature/Api/Auth/LoginTest.php new file mode 100644 index 0000000..ec97d54 --- /dev/null +++ b/backend/tests/Feature/Api/Auth/LoginTest.php @@ -0,0 +1,185 @@ +create(array_merge([ + 'email' => 'handika@example.com', + 'password' => Hash::make('Password123!'), + 'role' => 'patient', + 'status' => 'active', + ], $overrides)); + + Patient::factory()->create([ + 'user_id' => $user->id, + 'name' => 'Handika Testing', + 'phone' => '081234567890', + ]); + + return $user; + } + + public function test_user_can_login_with_valid_credentials(): void + { + $this->createPatientUser(); + + $response = $this->postJson($this->endpoint, [ + 'email' => 'handika@example.com', + 'password' => 'Password123!', + ]); + + $response->assertStatus(200) + ->assertJsonStructure([ + 'message', + 'data' => [ + 'user' => [ + 'id', + 'email', + 'role', + 'status', + 'profile' => [ + 'patient_id', + 'name', + 'phone', + ], + ], + ], + ]) + ->assertJsonPath('data.user.email', 'handika@example.com') + ->assertJsonPath('data.user.role', 'patient'); + } + + public function test_login_response_does_not_contain_bearer_token(): void + { + $this->createPatientUser(); + + $response = $this->postJson($this->endpoint, [ + 'email' => 'handika@example.com', + 'password' => 'Password123!', + ]); + + $response->assertJsonMissingPath('data.token'); + $response->assertJsonMissingPath('data.token_type'); + } + + public function test_login_authenticates_user_in_session(): void + { + $user = $this->createPatientUser(); + + $this->postJson($this->endpoint, [ + 'email' => 'handika@example.com', + 'password' => 'Password123!', + ]); + + $this->assertAuthenticatedAs($user); + } + + public function test_login_fails_with_wrong_password(): void + { + $this->createPatientUser(); + + $response = $this->postJson($this->endpoint, [ + 'email' => 'handika@example.com', + 'password' => 'WrongPassword!', + ]); + + $response->assertStatus(422) + ->assertJsonValidationErrors(['email']); + + $this->assertGuest(); + } + + public function test_login_fails_when_email_not_registered(): void + { + $response = $this->postJson($this->endpoint, [ + 'email' => 'notfound@example.com', + 'password' => 'Password123!', + ]); + + $response->assertStatus(422) + ->assertJsonValidationErrors(['email']); + + $this->assertGuest(); + } + + public function test_login_fails_when_email_missing(): void + { + $response = $this->postJson($this->endpoint, [ + 'password' => 'Password123!', + ]); + + $response->assertStatus(422) + ->assertJsonValidationErrors(['email']); + } + + public function test_login_fails_when_password_missing(): void + { + $this->createPatientUser(); + + $response = $this->postJson($this->endpoint, [ + 'email' => 'handika@example.com', + ]); + + $response->assertStatus(422) + ->assertJsonValidationErrors(['password']); + } + + public function test_login_fails_when_email_format_invalid(): void + { + $response = $this->postJson($this->endpoint, [ + 'email' => 'not-an-email', + 'password' => 'Password123!', + ]); + + $response->assertStatus(422) + ->assertJsonValidationErrors(['email']); + } + + public function test_login_is_rate_limited_after_too_many_attempts(): void + { + $this->createPatientUser(); + + for ($i = 0; $i < 5; $i++) { + $this->postJson($this->endpoint, [ + 'email' => 'handika@example.com', + 'password' => 'WrongPassword!', + ]); + } + + $response = $this->postJson($this->endpoint, [ + 'email' => 'handika@example.com', + 'password' => 'Password123!', + ]); + + $response->assertStatus(422) + ->assertJsonValidationErrors(['email']); + + $this->assertGuest(); + } + + public function test_already_authenticated_user_cannot_hit_login_due_to_guest_middleware(): void + { + $user = $this->createPatientUser(); + + $response = $this->actingAs($user)->postJson($this->endpoint, [ + 'email' => 'handika@example.com', + 'password' => 'Password123!', + ]); + + $response->assertStatus(409); + } +} diff --git a/backend/tests/Feature/Api/Auth/LogoutTest.php b/backend/tests/Feature/Api/Auth/LogoutTest.php new file mode 100644 index 0000000..44eb6d6 --- /dev/null +++ b/backend/tests/Feature/Api/Auth/LogoutTest.php @@ -0,0 +1,76 @@ +create([ + 'role' => 'patient', + 'status' => 'active', + ]); + + Patient::factory()->create([ + 'user_id' => $user->id, + ]); + + return $user; + } + + public function test_authenticated_user_can_logout(): void + { + $user = $this->createPatientUser(); + + $response = $this->actingAs($user, 'web')->postJson($this->endpoint); + + $response->assertStatus(200) + ->assertJson([ + 'message' => 'Logout Successful.', + ]); + } + + public function test_logout_invalidates_session(): void + { + $user = $this->createPatientUser(); + + $this->actingAs($user, 'web')->postJson($this->endpoint); + + $this->app['auth']->forgetGuards(); + $this->flushSession(); + + $response = $this->postJson($this->endpoint); + $response->assertStatus(401); + } + + public function test_guest_cannot_logout(): void + { + $response = $this->postJson($this->endpoint); + + $response->assertStatus(401); + } + + public function test_logout_followed_by_protected_route_returns_unauthenticated(): void + { + $response = $this->getJson('/api/auth/me'); + $response->assertStatus(401); + } + + public function test_logout_clears_session_data(): void + { + $user = $this->createPatientUser(); + + $this->actingAs($user, 'web')->postJson($this->endpoint); + $this->assertGuest('web'); + } +} diff --git a/backend/tests/Feature/Api/Auth/MeTest.php b/backend/tests/Feature/Api/Auth/MeTest.php new file mode 100644 index 0000000..a77d312 --- /dev/null +++ b/backend/tests/Feature/Api/Auth/MeTest.php @@ -0,0 +1,102 @@ +create([ + 'role' => 'patient', + 'status' => 'active', + ]); + + Patient::factory()->create(array_merge([ + 'user_id' => $user->id, + 'name' => 'Handika Testing', + 'phone' => '081234567890', + ], $patientOverrides)); + + return $user; + } + + public function test_authenticated_user_can_get_own_data(): void + { + $user = $this->createPatientUser(); + + $response = $this->actingAs($user)->getJson($this->endpoint); + + $response->assertStatus(200) + ->assertJsonStructure([ + 'data' => [ + 'id', + 'email', + 'role', + 'status', + 'profile' => [ + 'patient_id', + 'name', + 'phone', + 'bpjs_number', + 'birth_place', + 'birth_date', + 'gender', + 'user_id', + ], + ], + ]) + ->assertJsonPath('data.id', $user->id) + ->assertJsonPath('data.email', $user->email) + ->assertJsonPath('data.role', 'patient') + ->assertJsonPath('data.profile.name', 'Handika Testing'); + } + + public function test_guest_cannot_access_me_endpoint(): void + { + $response = $this->getJson($this->endpoint); + + $response->assertStatus(401); + } + + public function test_me_returns_correct_user_when_multiple_users_exist(): void + { + $otherUser = $this->createPatientUser(); + $targetUser = $this->createPatientUser(); + + $response = $this->actingAs($targetUser)->getJson($this->endpoint); + + $response->assertStatus(200) + ->assertJsonPath('data.id', $targetUser->id) + ->assertJsonPath('data.email', $targetUser->email); + + $this->assertNotEquals($otherUser->id, $response->json('data.id')); + } + + public function test_me_profile_reflects_nullable_fields_as_null_when_not_filled(): void + { + $user = $this->createPatientUser([ + 'bpjs_number' => null, + 'birth_place' => null, + 'birth_date' => null, + 'gender' => null, + ]); + + $response = $this->actingAs($user)->getJson($this->endpoint); + + $response->assertStatus(200) + ->assertJsonPath('data.profile.bpjs_number', null) + ->assertJsonPath('data.profile.birth_place', null) + ->assertJsonPath('data.profile.birth_date', null) + ->assertJsonPath('data.profile.gender', null); + } +} diff --git a/backend/tests/Feature/Api/Auth/RegisterTest.php b/backend/tests/Feature/Api/Auth/RegisterTest.php new file mode 100644 index 0000000..36fc7e8 --- /dev/null +++ b/backend/tests/Feature/Api/Auth/RegisterTest.php @@ -0,0 +1,197 @@ + 'Handika Testing', + 'email' => 'handika@example.com', + 'password' => 'Password123!', + 'password_confirmation' => 'Password123!', + 'phone' => '081234567890', + ]; + + $response = $this->postJson($this->endpoint, $payload); + + $response->assertStatus(201) + ->assertJsonStructure([ + 'message', + 'data' => [ + 'id', + 'email', + 'role', + 'status', + 'profile' => [ + 'patient_id', + 'name', + 'phone', + 'bpjs_number', + 'birth_place', + 'birth_date', + 'gender', + 'user_id', + ], + ], + ]) + ->assertJsonPath('data.email', 'handika@example.com') + ->assertJsonPath('data.role', 'patient') + ->assertJsonPath('data.status', 'active') + ->assertJsonPath('data.profile.name', 'Handika Testing') + ->assertJsonPath('data.profile.phone', '081234567890'); + + $this->assertDatabaseHas('users', [ + 'email' => 'handika@example.com', + 'role' => 'patient', + ]); + + $user = User::where('email', 'handika@example.com')->first(); + + $this->assertDatabaseHas('patients', [ + 'user_id' => $user->id, + 'name' => 'Handika Testing', + 'phone' => '081234567890', + ]); + } + + public function test_register_hashes_password(): void + { + $payload = [ + 'name' => 'Handika Testing', + 'email' => 'handika@example.com', + 'password' => 'Password123!', + 'password_confirmation' => 'Password123!', + 'phone' => '081234567890', + ]; + + $this->postJson($this->endpoint, $payload); + + $user = User::where('email', 'handika@example.com')->first(); + + $this->assertNotEquals('Password123!', $user->password); + $this->assertTrue(\Illuminate\Support\Facades\Hash::check('Password123!', $user->password)); + } + + public function test_register_fails_when_email_already_taken(): void + { + User::factory()->create(['email' => 'handika@example.com']); + + $payload = [ + 'name' => 'Handika Testing', + 'email' => 'handika@example.com', + 'password' => 'Password123!', + 'password_confirmation' => 'Password123!', + 'phone' => '081234567890', + ]; + + $response = $this->postJson($this->endpoint, $payload); + + $response->assertStatus(422) + ->assertJsonValidationErrors(['email']); + } + + public function test_register_fails_when_password_confirmation_does_not_match(): void + { + $payload = [ + 'name' => 'Handika Testing', + 'email' => 'handika@example.com', + 'password' => 'Password123!', + 'password_confirmation' => 'WrongPassword!', + 'phone' => '081234567890', + ]; + + $response = $this->postJson($this->endpoint, $payload); + + $response->assertStatus(422) + ->assertJsonValidationErrors(['password']); + } + + public function test_register_fails_when_password_too_short(): void + { + $payload = [ + 'name' => 'Handika Testing', + 'email' => 'handika@example.com', + 'password' => 'short', + 'password_confirmation' => 'short', + 'phone' => '081234567890', + ]; + + $response = $this->postJson($this->endpoint, $payload); + + $response->assertStatus(422) + ->assertJsonValidationErrors(['password']); + } + + #[DataProvider('missingFieldProvider')] + public function test_register_fails_when_required_field_missing(string $field): void + { + $payload = [ + 'name' => 'Handika Testing', + 'email' => 'handika@example.com', + 'password' => 'Password123!', + 'password_confirmation' => 'Password123!', + 'phone' => '081234567890', + ]; + + unset($payload[$field]); + + $response = $this->postJson($this->endpoint, $payload); + + $response->assertStatus(422) + ->assertJsonValidationErrors([$field]); + } + + public static function missingFieldProvider(): array + { + return [ + 'missing name' => ['name'], + 'missing email' => ['email'], + 'missing password' => ['password'], + 'missing phone' => ['phone'], + ]; + } + + public function test_register_fails_when_email_format_invalid(): void + { + $payload = [ + 'name' => 'Handika Testing', + 'email' => 'not-an-email', + 'password' => 'Password123!', + 'password_confirmation' => 'Password123!', + 'phone' => '081234567890', + ]; + + $response = $this->postJson($this->endpoint, $payload); + + $response->assertStatus(422) + ->assertJsonValidationErrors(['email']); + } + + public function test_register_does_not_create_patient_record_if_validation_fails(): void + { + $payload = [ + 'name' => 'Handika Testing', + 'email' => 'not-an-email', + 'password' => 'Password123!', + 'password_confirmation' => 'Password123!', + 'phone' => '081234567890', + ]; + + $this->postJson($this->endpoint, $payload); + + $this->assertDatabaseCount('patients', 0); + $this->assertDatabaseCount('users', 0); + } +} diff --git a/backend/tests/TestCase.php b/backend/tests/TestCase.php new file mode 100644 index 0000000..fe1ffc2 --- /dev/null +++ b/backend/tests/TestCase.php @@ -0,0 +1,10 @@ +assertTrue(true); + } +} diff --git a/docs/decisions/ADR-001-monorepo.md b/docs/decisions/ADR-001-monorepo.md index e69de29..06cb866 100644 --- a/docs/decisions/ADR-001-monorepo.md +++ b/docs/decisions/ADR-001-monorepo.md @@ -0,0 +1,50 @@ +# ADR-001: Monorepo Structure + +## Status + +Accepted + +## Date + +2026-06-05 + +## Context + +QueueNova Health dikerjakan oleh tim 2 developer dengan pembagian frontend dan backend. +Kami membutuhkan strategi manajemen repository yang memungkinkan: + +- Koordinasi perubahan lintas frontend dan backend dalam satu PR jika diperlukan +- Satu pipeline CI yang dapat memverifikasi keseluruhan sistem +- Kemudahan onboarding karena semua kode ada di satu tempat +- Konsistensi dokumentasi, Docker, dan konfigurasi GitHub Actions + +Alternatif yang dipertimbangkan adalah polyrepo (dua repository terpisah untuk frontend dan backend). + +## Decision + +Kami menggunakan **monorepo** dengan struktur: +project/ +├── .github/ +├── backend/ +├── infrastructure/ +├── docs/ +├── frontend/ +├── README.md +└── docker-compose.yml +Setiap bagian memiliki scope yang jelas dan tidak saling mengintervensi, +namun tetap dalam satu repository untuk kemudahan koordinasi. + +## Consequences + +### Positif + +- Satu repository untuk di-clone, satu CI pipeline untuk dikelola +- Perubahan yang membutuhkan update frontend dan backend bisa masuk dalam satu PR +- Dokumentasi dan keputusan arsitektur terpusat di `docs/` +- Docker Compose root dapat mengorkestrasikan semua service sekaligus + +### Negatif + +- CI pipeline harus didesain dengan hati-hati agar job frontend dan backend tidak saling memblokir secara tidak perlu +- Akses kontrol per-folder tidak bisa dilakukan di level repository (hanya lewat konvensi tim) +- Ukuran repository akan lebih besar seiring waktu karena semua artifact ada di satu tempat diff --git a/docs/decisions/ADR-002-react-vite.md b/docs/decisions/ADR-002-react-vite.md index e69de29..5e7966c 100644 --- a/docs/decisions/ADR-002-react-vite.md +++ b/docs/decisions/ADR-002-react-vite.md @@ -0,0 +1,46 @@ +# ADR-002: React + Vite Frontend + +## Status + +Accepted + +## Date + +2026-06-05 + +## Context + +QueueNova Health membutuhkan frontend yang mampu mendukung pengembangan antarmuka pengguna secara cepat dan maintainable oleh tim yang terdiri dari 2 developer. + +Kebutuhan frontend meliputi: + +- Pengembangan SPA (Single Page Application) dengan pengalaman pengguna yang responsif +- Dukungan ekosistem library yang luas untuk kebutuhan UI, routing, state management, dan integrasi API +- Startup development server yang cepat untuk meningkatkan produktivitas pengembangan +- Konfigurasi yang sederhana dan mudah dipahami oleh seluruh anggota tim +- Dukungan TypeScript untuk meningkatkan maintainability dan type safety + +Alternatif yang dipertimbangkan adalah Vue + Vite. + +## Decision + +Kami menggunakan **React** sebagai framework frontend dan **Vite** sebagai build tool dengan **TypeScript** sebagai bahasa utama pengembangan. + +Arsitektur frontend akan berupa Single Page Application (SPA) yang berkomunikasi dengan backend melalui REST API. + +## Consequences + +### Positif + +- Tim telah familiar dengan React sehingga onboarding dan pengembangan lebih cepat +- Ekosistem React menyediakan banyak library pendukung yang matang +- Vite memberikan startup development server yang cepat dan build yang efisien +- TypeScript membantu mengurangi kesalahan pada tahap development +- Struktur SPA sederhana dan mudah diintegrasikan dengan backend Laravel API + +### Negatif + +- SEO tidak sebaik pendekatan SSR karena aplikasi menggunakan SPA +- Pengelolaan state dapat menjadi kompleks seiring bertambahnya fitur +- Bundle frontend berpotensi bertambah besar jika dependensi tidak dikelola dengan baik +- React memberikan fleksibilitas tinggi sehingga diperlukan konvensi tim yang konsisten diff --git a/docs/decisions/ADR-003-laravel-api.md b/docs/decisions/ADR-003-laravel-api.md new file mode 100644 index 0000000..6884a85 --- /dev/null +++ b/docs/decisions/ADR-003-laravel-api.md @@ -0,0 +1,48 @@ +# ADR-003: Laravel REST API Backend + +## Status + +Accepted + +## Date + +2026-06-05 + +## Context + +QueueNova Health membutuhkan backend yang dapat mendukung pengembangan fitur sistem antrean dan appointment booking dengan cepat serta mudah dipelihara oleh tim kecil. + +Kebutuhan backend meliputi: + +- Penyediaan REST API untuk frontend +- Struktur aplikasi yang produktif dan mudah dikembangkan +- Dukungan fitur bawaan yang mengurangi kebutuhan implementasi manual +- Kemampuan menjalankan proses asynchronous menggunakan queue +- Pemisahan frontend dan backend secara jelas meskipun berada dalam satu repository + +Alternatif yang dipertimbangkan adalah NestJS. + +## Decision + +Kami menggunakan **Laravel** sebagai framework backend dan mengimplementasikan komunikasi melalui **REST API**. + +Frontend dan backend dipisahkan secara arsitektural (decoupled architecture), namun tetap berada dalam satu monorepo untuk memudahkan koordinasi pengembangan. + +Fitur queue Laravel akan digunakan untuk mendukung kebutuhan proses asynchronous apabila diperlukan pada pengembangan berikutnya. + +## Consequences + +### Positif + +- Tim telah menguasai Laravel sehingga pengembangan lebih efisien +- Banyak fitur bawaan tersedia tanpa memerlukan library tambahan +- Struktur proyek dan praktik pengembangan Laravel sudah matang +- Queue system tersedia dan siap digunakan untuk kebutuhan background processing +- REST API mudah diintegrasikan dengan frontend React + +### Negatif + +- Konsumsi resource dapat lebih tinggi dibanding framework yang lebih minimalis +- Ketergantungan terhadap ekosistem Laravel cukup besar +- Pemisahan frontend dan backend memerlukan pengelolaan kontrak API yang disiplin +- Skalabilitas horizontal memerlukan perencanaan tambahan ketika sistem berkembang diff --git a/docs/decisions/ADR-004-postgre.md b/docs/decisions/ADR-004-postgre.md new file mode 100644 index 0000000..95edfd8 --- /dev/null +++ b/docs/decisions/ADR-004-postgre.md @@ -0,0 +1,48 @@ +# ADR-004: PostgreSQL Database + +## Status + +Accepted + +## Date + +2026-06-05 + +## Context + +QueueNova Health membutuhkan database yang mampu menyimpan data relasional untuk sistem antrean dan appointment booking secara konsisten dan andal. + +Kebutuhan utama meliputi: + +- Penyimpanan data relasional yang terstruktur +- Dukungan transaksi yang konsisten +- Kemampuan mendukung kebutuhan reporting +- Solusi open source yang dapat digunakan tanpa biaya lisensi +- Kemudahan integrasi dengan Laravel + +Alternatif yang dipertimbangkan adalah MySQL. + +## Decision + +Kami menggunakan **PostgreSQL** sebagai database utama aplikasi. + +PostgreSQL dipilih karena bersifat open source, memiliki kepatuhan ACID yang kuat, serta mampu mendukung kebutuhan data relasional dan reporting yang menjadi fokus aplikasi. + +Pada tahap awal, sistem menggunakan satu instance database. + +## Consequences + +### Positif + +- Open source dan bebas biaya lisensi +- Menyediakan implementasi ACID yang kuat untuk menjaga konsistensi data +- Cocok untuk data relasional yang menjadi kebutuhan utama aplikasi +- Mendukung kebutuhan reporting dengan baik +- Integrasi dengan Laravel sudah matang dan stabil + +### Negatif + +- Membutuhkan pemahaman administrasi database yang lebih baik dibanding solusi yang lebih sederhana +- Scaling database memerlukan perencanaan tambahan ketika beban meningkat +- Operasional backup dan recovery tetap perlu dikelola secara disiplin +- Satu instance database menjadi single point of failure apabila tidak disertai strategi redundansi diff --git a/docs/decisions/ADR-006-sanctum-auth-mode.md b/docs/decisions/ADR-006-sanctum-auth-mode.md new file mode 100644 index 0000000..158e799 --- /dev/null +++ b/docs/decisions/ADR-006-sanctum-auth-mode.md @@ -0,0 +1,82 @@ +# ADR-006: Sanctum Authentication Mode — Cookie-based Session (SPA) + +## Status +Accepted + +## Date +2025-06-22 + +## Context +API contract awal (OpenAPI v2.1) mendefinisikan autentikasi menggunakan +Sanctum Bearer Token (plain-text token dikembalikan di response body, +disimpan di frontend, dan dilampirkan manual via header +`Authorization: Bearer {token}`). + +Selama implementasi Sprint 9 (AUTH-01, AUTH-02), tim melakukan diskusi +teknis dan menyepakati perubahan ke Sanctum Cookie-based Session (SPA mode). + +Dua mode Sanctum yang dipertimbangkan: + +**Mode A — Bearer Token** +- Token dikembalikan di response body (`data.token`) +- Frontend menyimpan token (React state, sessionStorage, atau localStorage) +- Setiap request melampirkan `Authorization: Bearer {token}` secara manual +- Stateless dari sisi server per-request + +**Mode B — Cookie-based Session (SPA)** +- Tidak ada token di response body +- Frontend melakukan `GET /sanctum/csrf-cookie` sebelum login untuk + mendapatkan XSRF-TOKEN dan laravel_session cookie +- Laravel mengelola session server-side +- Browser mengirim cookie otomatis di setiap request (`withCredentials: true`) +- CSRF protection dihandle oleh Sanctum via XSRF-TOKEN cookie + +## Decision +Tim memilih **Mode B: Cookie-based Session (SPA)**. + +Alasan: +1. Tidak ada token yang perlu disimpan di frontend — menghilangkan seluruh + perdebatan localStorage vs sessionStorage vs React memory +2. CSRF protection built-in dari Sanctum tanpa implementasi tambahan di FE +3. Lebih sesuai dengan use case SPA + same-origin atau subdomain deployment +4. Backend Laravel lebih natural menggunakan session daripada token management + untuk aplikasi web (bukan mobile/third-party API) + +## Consequences + +### Positif +- Frontend tidak menyimpan credential dalam bentuk apapun +- Tidak perlu token refresh strategy +- Logout cukup invalidate session di server — tidak perlu tracking token + +### Negatif / Trade-off +- Requires `GET /sanctum/csrf-cookie` sebelum setiap login (satu extra request) +- `withCredentials: true` wajib di semua request — sudah terpenuhi di + `axiosInstance` +- CORS di BE harus dikonfigurasi dengan benar (`supports_credentials: true`, + `allowed_origins` tidak boleh wildcard `*`) +- Tidak cocok untuk mobile app atau third-party API consumer di masa depan + tanpa perubahan arsitektur auth + +### Dampak ke implementasi yang sudah ada +- `axiosInstance.ts`: interceptor yang attach `Authorization: Bearer` header + harus dihapus +- `AuthContext.tsx`: tidak perlu menyimpan token, cukup menyimpan `user` object +- `authService.ts`: login harus diawali `GET /sanctum/csrf-cookie`, response + tidak mengandung token +- `types/auth.ts`: `LoginResponseData` tidak perlu field `token` dan + `token_type` +- OpenAPI contract perlu diupdate oleh BE untuk mencerminkan perubahan ini + (in progress) + +## Implementation Note +Revisi implementasi FE (axiosInstance, AuthContext, authService, types) akan +dilakukan setelah OpenAPI contract diupdate oleh BE sebagai source of truth. +Kode yang sudah ada (AUTH-01, AUTH-02) masih menggunakan Bearer Token dan +belum mencerminkan keputusan ini. + +## References +- Laravel Sanctum SPA Authentication: + https://laravel.com/docs/sanctum#spa-authentication +- OpenAPI contract v2.1 (Bearer Token — akan diupdate ke v2.2) +- Sprint 9 AUTH-01, AUTH-02 implementation (feature branches) \ No newline at end of file diff --git a/docs/openapi_v2/openapi.yaml b/docs/openapi_v2/openapi.yaml new file mode 100644 index 0000000..59384d4 --- /dev/null +++ b/docs/openapi_v2/openapi.yaml @@ -0,0 +1,204 @@ +openapi: 3.0.3 + +# ============================================================================= +# Hospital Queue Management System — API Contract v2.1 (UI-First) +# ============================================================================= +# Stack : Laravel 11 (API) + React SPA +# Auth : Laravel Sanctum — plain-text Bearer Token +# Approach : UI-First — disesuaikan dengan Figma design +# ERD : v2 (schedule_templates + schedule_instances, field baru) +# +# PERUBAHAN UTAMA dari v2.0: +# --------------------------- +# 1. /schedules SPLIT menjadi: +# - /doctors/{id}/schedule-templates (recurring pattern) +# - /schedule-instances (tanggal aktual, auto-generated) +# 2. Patients: tambah bpjs_number, birth_place, birth_date, gender +# 3. Doctors : tambah sip_number, birth_date, gender +# 4. Nurses : tambah sip_number, birth_date, gender +# 5. Reservations: tambah complaint (keluhan pasien) +# 6. POST /reservations: FK ke instance_id (bukan schedule_id) +# 7. Endpoint baru: GET /patients/me (profil pasien sendiri) +# 8. Endpoint baru: PATCH /patients/me (update profil pasien) +# +# CARA PAKAI TOKEN SANCTUM +# ------------------------- +# 1. POST /auth/login → dapat data.token +# 2. Simpan token di React state/memory (BUKAN localStorage) +# 3. Sertakan di setiap request: Authorization: Bearer {token} +# 4. POST /auth/logout → token di-revoke +# +# ROLES +# ----- +# patient | doctor | nurse | admin +# +# QUEUE STATE MACHINE +# ------------------- +# booked → checked-in → waiting → called → in-progress → done +# ↘ no-show +# ↘ cancelled +# ============================================================================= + +info: + title: Hospital Queue Management System API + version: 2.1.0 + description: | + REST API untuk sistem manajemen antrian rumah sakit QueueNova Health. + Disesuaikan dengan UI Figma (UI-First approach). + + ### Perubahan dari v2.0 + - Schedule sekarang menggunakan sistem **template + instance** (Hybrid) + - Field baru di Patient, Doctor, Nurse (SIP, gender, tanggal lahir) + - Field `complaint` di Reservation (keluhan pasien) + - Endpoint profil pasien (`/patients/me`) + +servers: + - url: http://localhost:8000/api + description: Local Development Server + - url: https://api.queuenova.example.com/api + description: Production Server + +components: + securitySchemes: + sanctumToken: + type: http + scheme: bearer + bearerFormat: SanctumToken + description: | + Sanctum plain-text token dari POST /auth/login. + Header: `Authorization: Bearer {token}` + + schemas: + # Auth + RegisterRequest: { $ref: './schemas/auth.yaml#/RegisterRequest' } + LoginRequest: { $ref: './schemas/auth.yaml#/LoginRequest' } + LoginResponse: { $ref: './schemas/auth.yaml#/LoginResponse' } + UserResource: { $ref: './schemas/auth.yaml#/UserResource' } + + # Patient profile + PatientProfileResource: { $ref: './schemas/patient.yaml#/PatientProfileResource' } + PatientProfileRequest: { $ref: './schemas/patient.yaml#/PatientProfileRequest' } + + # Department + DepartmentResource: { $ref: './schemas/department.yaml#/DepartmentResource' } + DepartmentRequest: { $ref: './schemas/department.yaml#/DepartmentRequest' } + + # Doctor + DoctorResource: { $ref: './schemas/doctor.yaml#/DoctorResource' } + DoctorRequest: { $ref: './schemas/doctor.yaml#/DoctorRequest' } + DoctorUpdateRequest: { $ref: './schemas/doctor.yaml#/DoctorUpdateRequest' } + + # Nurse + NurseResource: { $ref: './schemas/nurse.yaml#/NurseResource' } + NurseRequest: { $ref: './schemas/nurse.yaml#/NurseRequest' } + NurseUpdateRequest: { $ref: './schemas/nurse.yaml#/NurseUpdateRequest' } + + # Schedule Template (recurring) + ScheduleTemplateResource: { $ref: './schemas/schedule.yaml#/ScheduleTemplateResource' } + ScheduleTemplateRequest: { $ref: './schemas/schedule.yaml#/ScheduleTemplateRequest' } + + # Schedule Instance (tanggal aktual) + ScheduleInstanceResource: { $ref: './schemas/schedule.yaml#/ScheduleInstanceResource' } + ScheduleInstanceRequest: { $ref: './schemas/schedule.yaml#/ScheduleInstanceRequest' } + + # Reservation + ReservationResource: { $ref: './schemas/reservation.yaml#/ReservationResource' } + ReservationRequest: { $ref: './schemas/reservation.yaml#/ReservationRequest' } + + # Queue + QueueResource: { $ref: './schemas/queue.yaml#/QueueResource' } + QueueStatusRequest: { $ref: './schemas/queue.yaml#/QueueStatusRequest' } + QueueReportResource: { $ref: './schemas/queue.yaml#/QueueReportResource' } + + # Common + SuccessResponse: { $ref: './schemas/common.yaml#/SuccessResponse' } + ErrorResponse: { $ref: './schemas/common.yaml#/ErrorResponse' } + ValidationErrorResponse: { $ref: './schemas/common.yaml#/ValidationErrorResponse' } + PaginationMeta: { $ref: './schemas/common.yaml#/PaginationMeta' } + +tags: + - name: Authentication + description: "**EPIC 1** — Register, login, logout" + - name: Patient Profile + description: "**EPIC 1/3** — Profil & data diri pasien" + - name: Departments + description: "**EPIC 2** — CRUD departemen (Admin)" + - name: Doctors + description: "**EPIC 2** — CRUD dokter (Admin)" + - name: Nurses + description: "**EPIC 2** — CRUD suster (Admin)" + - name: Schedule Templates + description: "**EPIC 2** — Template jadwal berulang per dokter (Admin)" + - name: Schedule Instances + description: "**EPIC 2/3** — Jadwal aktual per tanggal (auto-generated + override)" + - name: Booking + description: "**EPIC 3** — Reservasi pasien" + - name: Queue + description: "**EPIC 4** — Antrian (Nurse update, Doctor & Nurse view)" + - name: Reports + description: "**EPIC 4** — Laporan antrian (Admin)" + +paths: + # ---- EPIC 1: Auth ---- + /auth/register: + $ref: './paths/auth.yaml#/~1auth~1register' + /auth/login: + $ref: './paths/auth.yaml#/~1auth~1login' + /auth/logout: + $ref: './paths/auth.yaml#/~1auth~1logout' + /auth/me: + $ref: './paths/auth.yaml#/~1auth~1me' + + # ---- Patient Profile ---- + /patients/me: + $ref: './paths/patients.yaml#/~1patients~1me' + + # ---- EPIC 2: Departments ---- + /departments: + $ref: './paths/departments.yaml#/~1departments' + /departments/{id}: + $ref: './paths/departments.yaml#/~1departments~1{id}' + + # ---- EPIC 2: Doctors ---- + /doctors: + $ref: './paths/doctors.yaml#/~1doctors' + /doctors/{id}: + $ref: './paths/doctors.yaml#/~1doctors~1{id}' + + # ---- EPIC 2: Nurses ---- + /nurses: + $ref: './paths/nurses.yaml#/~1nurses' + /nurses/{id}: + $ref: './paths/nurses.yaml#/~1nurses~1{id}' + + # ---- EPIC 2: Schedule Templates (recurring) ---- + /doctors/{id}/schedule-templates: + $ref: './paths/schedule_templates.yaml#/~1doctors~1{id}~1schedule-templates' + /doctors/{id}/schedule-templates/{templateId}: + $ref: './paths/schedule_templates.yaml#/~1doctors~1{id}~1schedule-templates~1{templateId}' + + # ---- EPIC 2/3: Schedule Instances (tanggal aktual) ---- + # Admin: override/cancel per instance + # Patient: GET untuk melihat jadwal tersedia sebelum booking + /schedule-instances: + $ref: './paths/schedule_instances.yaml#/~1schedule-instances' + /schedule-instances/{instanceId}: + $ref: './paths/schedule_instances.yaml#/~1schedule-instances~1{instanceId}' + + # ---- EPIC 3: Reservations ---- + /reservations: + $ref: './paths/reservations.yaml#/~1reservations' + /reservations/{id}: + $ref: './paths/reservations.yaml#/~1reservations~1{id}' + /reservations/{id}/cancel: + $ref: './paths/reservations.yaml#/~1reservations~1{id}~1cancel' + + # ---- EPIC 4: Queue ---- + /queues: + $ref: './paths/queues.yaml#/~1queues' + /queues/{id}/status: + $ref: './paths/queues.yaml#/~1queues~1{id}~1status' + + # ---- EPIC 4: Reports ---- + /reports/queues: + $ref: './paths/reports.yaml#/~1reports~1queues' diff --git a/docs/openapi_v2/paths/auth.yaml b/docs/openapi_v2/paths/auth.yaml new file mode 100644 index 0000000..2064e43 --- /dev/null +++ b/docs/openapi_v2/paths/auth.yaml @@ -0,0 +1,174 @@ +# ============================================================================= +# Paths — EPIC 1: Authentication +# Dari Figma: app name = "QueueNova Health", brand = "QueueNova" +# Login screen: email + password + "Remember me" + "Forgot Password?" +# Register screen: nama, email, password, konfirmasi password +# ============================================================================= + +/auth/register: + post: + tags: [Authentication] + summary: Register pasien baru + description: | + Registrasi khusus **Pasien**. Doctor/Nurse/Admin didaftarkan oleh Admin. + Dari Figma register screen: field Name, Email, Password, Password Confirmation. + Data profil lengkap (BPJS, tempat lahir, dll) dilengkapi via `PATCH /patients/me`. + operationId: registerPatient + security: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '../schemas/auth.yaml#/RegisterRequest' + example: + name: "Ucok Sitorus" + email: "ucok@example.com" + password: "password123" + password_confirmation: "password123" + phone: "081234567890" + responses: + '201': + description: Registrasi berhasil + content: + application/json: + example: + message: "Registrasi berhasil. Silakan login." + data: + user_id: 10 + email: "ucok@example.com" + role: "patient" + status: "active" + profile: + patient_id: 5 + name: "Ucok Sitorus" + phone: "081234567890" + bpjs_number: null + birth_place: null + birth_date: null + gender: null + '422': + description: Validasi gagal + content: + application/json: + schema: + $ref: '../schemas/common.yaml#/ValidationErrorResponse' + example: + message: "The given data was invalid." + errors: + email: ["The email has already been taken."] + +/auth/login: + post: + tags: [Authentication] + summary: Login — semua role (Patient, Doctor, Nurse, Admin) + description: | + Dari Figma login screen (admin & patient): + - Field: Email (`example@gmail.com`), Password (`@#*%`) + - Checkbox "Remember me" — implementasi di frontend (tidak di API) + - Link "Forgot Password?" — belum dalam scope + + Mengembalikan Sanctum plain-text Bearer Token. + operationId: loginUser + security: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '../schemas/auth.yaml#/LoginRequest' + example: + email: "ucok@example.com" + password: "password123" + responses: + '200': + description: Login berhasil — token dikembalikan + content: + application/json: + schema: + $ref: '../schemas/auth.yaml#/LoginResponse' + example: + message: "Login berhasil." + data: + token: "3|aB1cUcokD2eF3gH4iJ5kL6" + token_type: "Bearer" + user: + user_id: 10 + email: "ucok@example.com" + role: "patient" + status: "active" + profile: + patient_id: 5 + name: "Ucok Sitorus" + phone: "081234567890" + bpjs_number: "0001234567890" + birth_place: "Medan" + birth_date: "1995-06-15" + gender: "laki-laki" + '401': + description: Email atau password salah + content: + application/json: + schema: { $ref: '../schemas/common.yaml#/ErrorResponse' } + example: { message: "Email atau password salah." } + '422': + description: Validasi gagal + content: + application/json: + schema: { $ref: '../schemas/common.yaml#/ValidationErrorResponse' } + +/auth/logout: + post: + tags: [Authentication] + summary: Logout — revoke token aktif + operationId: logoutUser + security: + - sanctumToken: [] + responses: + '200': + description: Logout berhasil + content: + application/json: + example: { message: "Logout berhasil." } + '401': + description: Token tidak valid + content: + application/json: + schema: { $ref: '../schemas/common.yaml#/ErrorResponse' } + example: { message: "Unauthenticated." } + +/auth/me: + get: + tags: [Authentication] + summary: Ambil data user yang sedang login + description: | + Berguna untuk inisialisasi React SPA setelah refresh. + Profile bervariasi per role — lihat masing-masing Resource schema. + operationId: getAuthenticatedUser + security: + - sanctumToken: [] + responses: + '200': + description: Data user + content: + application/json: + example: + data: + user_id: 10 + email: "ucok@example.com" + role: "nurse" + status: "active" + profile: + nurse_id: 2 + name: "Ucok Sihombing" + sip_number: "112345678" + gender: "laki-laki" + birth_date: "1990-07-10" + department: + department_id: 1 + name: "Poli Umum" + '401': + description: Token tidak valid + content: + application/json: + schema: { $ref: '../schemas/common.yaml#/ErrorResponse' } diff --git a/docs/openapi_v2/paths/departments.yaml b/docs/openapi_v2/paths/departments.yaml new file mode 100644 index 0000000..d44904b --- /dev/null +++ b/docs/openapi_v2/paths/departments.yaml @@ -0,0 +1,184 @@ +# ============================================================================= +# Paths — EPIC 2: Departments +# Dari Figma admin-department-dashboard: list card dengan nama + aksi Delete/Edit/Info +# Dari Figma patient-view-department: card poli dengan nama, deskripsi, jumlah dokter +# ============================================================================= + +/departments: + get: + tags: [Departments] + summary: Lihat daftar departemen / poli + description: | + Semua role yang login dapat melihat daftar poli. + Pasien menggunakan ini sebagai halaman awal booking. + Dari Figma: menampilkan nama, deskripsi singkat, jumlah dokter tersedia. + operationId: getDepartments + security: + - sanctumToken: [] + parameters: + - name: search + in: query + required: false + description: "Dari Figma: 'Cari dokter atau poli...'" + schema: { type: string, example: "Poli Umum" } + responses: + '200': + description: Daftar departemen + content: + application/json: + example: + data: + - department_id: 1 + name: "Poli Umum" + description: "Pemeriksaan Kesehatan Umum" + doctors_count: 3 + - department_id: 2 + name: "Poli Mata" + description: "Pemeriksaan Penglihatan dan Kesehatan Mata" + doctors_count: 2 + - department_id: 3 + name: "Poli Kebidanan dan Kandungan" + description: "Pemeriksaan Kesehatan Kandungan" + doctors_count: 2 + '401': + description: Tidak terautentikasi + content: + application/json: + schema: { $ref: '../schemas/common.yaml#/ErrorResponse' } + + post: + tags: [Departments] + summary: Buat departemen baru + description: "**Admin only**. Dari Figma form: Department Name + Description." + operationId: createDepartment + security: + - sanctumToken: [] + requestBody: + required: true + content: + application/json: + schema: { $ref: '../schemas/department.yaml#/DepartmentRequest' } + example: + name: "Poli Umum" + description: "Pusat layanan kesehatan primer untuk diagnosa awal, pengobatan umum, dan rujukan" + responses: + '201': + description: Departemen dibuat + content: + application/json: + example: + message: "Departemen berhasil dibuat." + data: + department_id: 4 + name: "Poli Umum" + description: "Pusat layanan kesehatan primer untuk diagnosa awal, pengobatan umum, dan rujukan" + doctors_count: 0 + '403': + description: Bukan Admin + content: + application/json: + schema: { $ref: '../schemas/common.yaml#/ErrorResponse' } + '422': + description: Validasi gagal + content: + application/json: + schema: { $ref: '../schemas/common.yaml#/ValidationErrorResponse' } + +/departments/{id}: + parameters: + - name: id + in: path + required: true + schema: { type: integer, example: 1 } + + get: + tags: [Departments] + summary: Detail departemen + list dokter + description: | + Dari Figma admin-department-detail: + - Info: Nama poli, deskripsi panjang + - "List Dokter Poli Umum": tabel No., Nama Dokter, No SIP, Jenis Kelamin, Aksi + operationId: getDepartmentById + security: + - sanctumToken: [] + responses: + '200': + description: Detail departemen + content: + application/json: + example: + data: + department_id: 1 + name: "Poli Umum" + description: "Pusat layanan kesehatan primer untuk diagnosa awal, pengobatan umum, dan rujukan" + doctors_count: 4 + doctors: + - doctor_id: 1 + name: "dr. Ucok Napitupulu" + sip_number: "12345" + gender: "laki-laki" + - doctor_id: 2 + name: "dr. Tirta Pengpeng" + sip_number: "321321" + gender: "laki-laki" + '404': + description: Tidak ditemukan + content: + application/json: + schema: { $ref: '../schemas/common.yaml#/ErrorResponse' } + + patch: + tags: [Departments] + summary: Update departemen + description: "**Admin only** — partial update. Dari Figma: field Department Name + Description." + operationId: updateDepartment + security: + - sanctumToken: [] + requestBody: + required: true + content: + application/json: + schema: { $ref: '../schemas/department.yaml#/DepartmentRequest' } + example: + name: "Poli Umum & Gawat Darurat" + responses: + '200': + description: Departemen diperbarui + content: + application/json: + example: + message: "Departemen berhasil diperbarui." + data: + department_id: 1 + name: "Poli Umum & Gawat Darurat" + description: "Pusat layanan kesehatan primer untuk diagnosa awal, pengobatan umum, dan rujukan" + doctors_count: 4 + '403': + description: Bukan Admin + content: + application/json: + schema: { $ref: '../schemas/common.yaml#/ErrorResponse' } + '404': + description: Tidak ditemukan + content: + application/json: + schema: { $ref: '../schemas/common.yaml#/ErrorResponse' } + + delete: + tags: [Departments] + summary: Hapus departemen + description: "**Admin only**. Gagal jika masih ada dokter aktif." + operationId: deleteDepartment + security: + - sanctumToken: [] + responses: + '200': + description: Dihapus + content: + application/json: + example: { message: "Departemen berhasil dihapus." } + '409': + description: Masih ada dokter aktif + content: + application/json: + example: { message: "Departemen tidak dapat dihapus karena masih memiliki dokter aktif." } diff --git a/docs/openapi_v2/paths/doctors.yaml b/docs/openapi_v2/paths/doctors.yaml new file mode 100644 index 0000000..5e5f5af --- /dev/null +++ b/docs/openapi_v2/paths/doctors.yaml @@ -0,0 +1,211 @@ +# ============================================================================= +# Paths — EPIC 2: Doctors +# Dari Figma admin-doctor-dashboard: list nama dokter + Delete/Edit/Info +# Dari Figma admin-doctor-form: Doctor Name, No SIP, Jenis Kelamin, +# Tanggal Lahir, Spesialisasi, Department, Email, Password +# Dari Figma admin-doctor-detail: Info Dokter + tabel Schedule (Hari, Pukul, Aksi) +# ============================================================================= + +/doctors: + get: + tags: [Doctors] + summary: Lihat daftar dokter + operationId: getDoctors + security: + - sanctumToken: [] + parameters: + - name: department_id + in: query + required: false + schema: { type: integer, example: 1 } + - name: search + in: query + required: false + schema: { type: string, example: "dr. Ucok" } + responses: + '200': + description: Daftar dokter + content: + application/json: + example: + data: + - doctor_id: 1 + name: "dr. Ucok Napitupulu, Sp.PD" + specialization: "Spesialis Penyakit Dalam" + sip_number: "112345678" + gender: "laki-laki" + birth_date: "1980-03-20" + doctor_status: "active" + department: + department_id: 1 + name: "Poli Umum" + user_id: 5 + + post: + tags: [Doctors] + summary: Daftarkan dokter baru + description: | + **Admin only**. Dari Figma form doctor: + Doctor Name, No SIP, Jenis Kelamin, Tanggal Lahir, Spesialisasi, Department, Email, Password. + operationId: createDoctor + security: + - sanctumToken: [] + requestBody: + required: true + content: + application/json: + schema: { $ref: '../schemas/doctor.yaml#/DoctorRequest' } + example: + name: "dr. Ucok Napitupulu, Sp.PD" + email: "ucok.dokter@hospital.com" + password: "password123" + specialization: "Spesialis Penyakit Dalam" + department_id: 1 + sip_number: "112345678" + birth_date: "1980-03-20" + gender: "laki-laki" + responses: + '201': + description: Dokter didaftarkan + content: + application/json: + example: + message: "Dokter berhasil didaftarkan." + data: + doctor_id: 1 + name: "dr. Ucok Napitupulu, Sp.PD" + specialization: "Spesialis Penyakit Dalam" + sip_number: "112345678" + gender: "laki-laki" + birth_date: "1980-03-20" + doctor_status: "active" + department: + department_id: 1 + name: "Poli Umum" + user_id: 5 + '403': + description: Bukan Admin + content: + application/json: + schema: { $ref: '../schemas/common.yaml#/ErrorResponse' } + '422': + description: Validasi gagal + content: + application/json: + schema: { $ref: '../schemas/common.yaml#/ValidationErrorResponse' } + +/doctors/{id}: + parameters: + - name: id + in: path + required: true + schema: { type: integer, example: 1 } + + get: + tags: [Doctors] + summary: Detail dokter + daftar template jadwal + description: | + Dari Figma admin-doctor-detail: + - Section "Informasi Dokter": Nama, No SIP, Jenis Kelamin, Spesialisasi, Email, Department + - Section "Schedule": tabel Hari + Jam + Aksi (Edit/Delete) + tombol "Add Schedule" + operationId: getDoctorById + security: + - sanctumToken: [] + responses: + '200': + description: Detail dokter + content: + application/json: + example: + data: + doctor_id: 1 + name: "dr. Ucok Napitupulu, Sp.PD" + specialization: "Spesialis Penyakit Dalam" + sip_number: "112345678" + gender: "laki-laki" + birth_date: "1980-03-20" + doctor_status: "active" + department: + department_id: 1 + name: "Poli Umum" + user_id: 5 + # Template jadwal ditampilkan di halaman detail dokter Figma + schedule_templates: + - template_id: 1 + day_of_week: "senin" + start_time: "09:00" + end_time: "15:00" + max_patients: 10 + is_active: true + - template_id: 2 + day_of_week: "selasa" + start_time: "09:00" + end_time: "15:00" + max_patients: 10 + is_active: true + '404': + description: Tidak ditemukan + content: + application/json: + schema: { $ref: '../schemas/common.yaml#/ErrorResponse' } + + patch: + tags: [Doctors] + summary: Update data dokter + description: "**Admin only** — partial update. Email/password tidak diubah di sini." + operationId: updateDoctor + security: + - sanctumToken: [] + requestBody: + required: true + content: + application/json: + schema: { $ref: '../schemas/doctor.yaml#/DoctorUpdateRequest' } + example: + specialization: "Spesialis Penyakit Dalam & Konsultan Ginjal" + sip_number: "112345679" + responses: + '200': + description: Data diperbarui + content: + application/json: + example: + message: "Data dokter berhasil diperbarui." + data: + doctor_id: 1 + name: "dr. Ucok Napitupulu, Sp.PD" + specialization: "Spesialis Penyakit Dalam & Konsultan Ginjal" + sip_number: "112345679" + doctor_status: "active" + department: + department_id: 1 + name: "Poli Umum" + '403': + description: Bukan Admin + content: + application/json: + schema: { $ref: '../schemas/common.yaml#/ErrorResponse' } + '404': + description: Tidak ditemukan + content: + application/json: + schema: { $ref: '../schemas/common.yaml#/ErrorResponse' } + + delete: + tags: [Doctors] + summary: Hapus dokter + description: "**Admin only**. Hapus users + doctors. Gagal jika ada reservasi aktif." + operationId: deleteDoctor + security: + - sanctumToken: [] + responses: + '200': + description: Dihapus + content: + application/json: + example: { message: "Dokter berhasil dihapus." } + '409': + description: Masih ada reservasi aktif + content: + application/json: + example: { message: "Dokter tidak dapat dihapus karena masih memiliki reservasi aktif." } diff --git a/docs/openapi_v2/paths/nurses.yaml b/docs/openapi_v2/paths/nurses.yaml new file mode 100644 index 0000000..550acd8 --- /dev/null +++ b/docs/openapi_v2/paths/nurses.yaml @@ -0,0 +1,197 @@ +# ============================================================================= +# Paths — EPIC 2: Nurses +# Dari Figma admin-nurse-form: Nurse Name, No SIP, Jenis Kelamin, +# Tanggal Lahir, Email, Password +# Dari Figma admin-nurse-detail: Informasi Nurse — Nama, No SIP, Jenis Kelamin, Email +# ============================================================================= + +/nurses: + get: + tags: [Nurses] + summary: Lihat daftar suster + description: "**Admin only**" + operationId: getNurses + security: + - sanctumToken: [] + parameters: + - name: search + in: query + required: false + schema: { type: string, example: "Ucok" } + - name: department_id + in: query + required: false + schema: { type: integer, example: 1 } + responses: + '200': + description: Daftar suster + content: + application/json: + example: + data: + - nurse_id: 1 + name: "Ucok Sihombing" + sip_number: "112345678" + gender: "laki-laki" + birth_date: "1990-07-10" + department: + department_id: 1 + name: "Poli Umum" + user_id: 8 + - nurse_id: 2 + name: "Bunda Rahma" + sip_number: "123456789" + gender: "perempuan" + birth_date: "1988-03-15" + department: null + user_id: 9 + '403': + description: Bukan Admin + content: + application/json: + schema: { $ref: '../schemas/common.yaml#/ErrorResponse' } + + post: + tags: [Nurses] + summary: Daftarkan suster baru + description: | + **Admin only**. Dari Figma form nurse: + Nurse Name, No SIP, Jenis Kelamin, Tanggal Lahir, Email, Password. + operationId: createNurse + security: + - sanctumToken: [] + requestBody: + required: true + content: + application/json: + schema: { $ref: '../schemas/nurse.yaml#/NurseRequest' } + example: + name: "Ucok Sihombing" + email: "ucok.nurse@hospital.com" + password: "password123" + department_id: 1 + sip_number: "112345678" + birth_date: "1990-07-10" + gender: "laki-laki" + responses: + '201': + description: Suster didaftarkan + content: + application/json: + example: + message: "Suster berhasil didaftarkan." + data: + nurse_id: 1 + name: "Ucok Sihombing" + sip_number: "112345678" + gender: "laki-laki" + birth_date: "1990-07-10" + department: + department_id: 1 + name: "Poli Umum" + user_id: 8 + '403': + description: Bukan Admin + content: + application/json: + schema: { $ref: '../schemas/common.yaml#/ErrorResponse' } + '422': + description: Validasi gagal + content: + application/json: + schema: { $ref: '../schemas/common.yaml#/ValidationErrorResponse' } + +/nurses/{id}: + parameters: + - name: id + in: path + required: true + schema: { type: integer, example: 1 } + + get: + tags: [Nurses] + summary: Detail suster + description: | + **Admin only**. Dari Figma admin-nurse-detail: + Informasi Nurse: No SIP, Jenis Kelamin, Email, Nama Nurse. + operationId: getNurseById + security: + - sanctumToken: [] + responses: + '200': + description: Detail suster + content: + application/json: + example: + data: + nurse_id: 2 + name: "Bunda Rahma" + sip_number: "123456789" + gender: "perempuan" + birth_date: "1988-03-15" + department: null + user_id: 9 + '404': + description: Tidak ditemukan + content: + application/json: + schema: { $ref: '../schemas/common.yaml#/ErrorResponse' } + + patch: + tags: [Nurses] + summary: Update data suster + description: "**Admin only** — partial update." + operationId: updateNurse + security: + - sanctumToken: [] + requestBody: + required: true + content: + application/json: + schema: { $ref: '../schemas/nurse.yaml#/NurseUpdateRequest' } + example: + name: "Bunda Rahma, S.Kep" + sip_number: "123456789" + responses: + '200': + description: Data diperbarui + content: + application/json: + example: + message: "Data suster berhasil diperbarui." + data: + nurse_id: 2 + name: "Bunda Rahma, S.Kep" + sip_number: "123456789" + gender: "perempuan" + department: null + user_id: 9 + '403': + description: Bukan Admin + content: + application/json: + schema: { $ref: '../schemas/common.yaml#/ErrorResponse' } + '404': + description: Tidak ditemukan + content: + application/json: + schema: { $ref: '../schemas/common.yaml#/ErrorResponse' } + + delete: + tags: [Nurses] + summary: Hapus suster + description: "**Admin only**. Hapus users + nurses." + operationId: deleteNurse + security: + - sanctumToken: [] + responses: + '200': + description: Dihapus + content: + application/json: + example: { message: "Suster berhasil dihapus." } + '404': + description: Tidak ditemukan + content: + application/json: + schema: { $ref: '../schemas/common.yaml#/ErrorResponse' } diff --git a/docs/openapi_v2/paths/patients.yaml b/docs/openapi_v2/paths/patients.yaml new file mode 100644 index 0000000..f0e76c2 --- /dev/null +++ b/docs/openapi_v2/paths/patients.yaml @@ -0,0 +1,104 @@ +# ============================================================================= +# Paths — Patient Profile +# [BARU v2.1] Dari Figma form Create Reservation: +# Section "Informasi Pasien" menampilkan: +# Nama, No. BPJS, Tempat lahir, Tanggal lahir +# Data ini perlu ada di profil pasien sebelum booking. +# Pasien bisa lihat dan update profil sendiri. +# ============================================================================= + +/patients/me: + get: + tags: [Patient Profile] + summary: Lihat profil pasien yang sedang login + description: | + Mengembalikan profil lengkap pasien yang sedang login. + Data ini ditampilkan di section "Informasi Pasien" pada form Create Reservation Figma: + Nama, No. BPJS, Tempat Lahir, Tanggal Lahir. + operationId: getMyPatientProfile + security: + - sanctumToken: [] + responses: + '200': + description: Profil pasien + content: + application/json: + schema: + type: object + properties: + data: + $ref: '../schemas/patient.yaml#/PatientProfileResource' + example: + data: + patient_id: 5 + name: "Ucok Sitorus" + phone: "081234567890" + bpjs_number: "0001234567890" + birth_place: "Medan" + birth_date: "1995-06-15" + gender: "laki-laki" + user_id: 10 + '401': + description: Tidak terautentikasi + content: + application/json: + schema: { $ref: '../schemas/common.yaml#/ErrorResponse' } + '403': + description: Bukan role patient + content: + application/json: + schema: { $ref: '../schemas/common.yaml#/ErrorResponse' } + example: { message: "Endpoint ini hanya untuk pasien." } + + patch: + tags: [Patient Profile] + summary: Update profil pasien sendiri + description: | + Pasien melengkapi data profil — BPJS, tempat lahir, tanggal lahir, gender. + Data ini muncul di form Create Reservation (pre-filled dari profil). + Partial update — kirim hanya field yang ingin diubah. + operationId: updateMyPatientProfile + security: + - sanctumToken: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '../schemas/patient.yaml#/PatientProfileRequest' + example: + bpjs_number: "0001234567890" + birth_place: "Medan" + birth_date: "1995-06-15" + gender: "laki-laki" + responses: + '200': + description: Profil berhasil diperbarui + content: + application/json: + example: + message: "Profil berhasil diperbarui." + data: + patient_id: 5 + name: "Ucok Sitorus" + phone: "081234567890" + bpjs_number: "0001234567890" + birth_place: "Medan" + birth_date: "1995-06-15" + gender: "laki-laki" + user_id: 10 + '401': + description: Tidak terautentikasi + content: + application/json: + schema: { $ref: '../schemas/common.yaml#/ErrorResponse' } + '403': + description: Bukan role patient + content: + application/json: + schema: { $ref: '../schemas/common.yaml#/ErrorResponse' } + '422': + description: Validasi gagal + content: + application/json: + schema: { $ref: '../schemas/common.yaml#/ValidationErrorResponse' } diff --git a/docs/openapi_v2/paths/queues.yaml b/docs/openapi_v2/paths/queues.yaml new file mode 100644 index 0000000..be709fa --- /dev/null +++ b/docs/openapi_v2/paths/queues.yaml @@ -0,0 +1,193 @@ +# ============================================================================= +# Paths — EPIC 4: Queue +# Dari Figma nurse-queue-list: +# - Header: "Poli Anda hari ini", "Lihat dan kelola semua bagian Anda" +# - Jadwal cards: Poli + Jam + Total Antrian + "Yang sudah check-in" +# - Filter tabs: Semua | Booked | Called | Done +# - Tabel: No. Antrian, Nama Pasien, Status, Estimasi Waktu Tunggu, Catatan +# Dari Figma nurse-chaos-mode: +# - Card alert: "TERLAMBAT 25 MENIT", nama dokter, tombol "BERITAHU PASIEN" +# - "Poli yang tersedia untuk perpindahan pasien" + slot antrian +# Dari Figma doctor-queue-list: +# - Kolom tambahan: Keluhan, Hasil Skrining, Jenis Kelamin +# ============================================================================= + +/queues: + get: + tags: [Queue] + summary: Lihat daftar antrian (Nurse & Doctor) + description: | + **Nurse**: melihat semua antrian hari ini, terkelompok per schedule instance. + **Doctor**: hanya antrian jadwal miliknya (filter by doctor_id dari token). + + Dari Figma nurse-queue-list: antrian dikelompokkan per jadwal + (misal "Poli Umum 08:00-09:45", "Poli Umum 10:00-11:45"). + + Response menyertakan `schedule_summary` per instance dan `queue_items` per antrian. + operationId: getQueue + security: + - sanctumToken: [] + parameters: + - name: date + in: query + description: "Filter by tanggal. Default: hari ini." + required: false + schema: { type: string, format: date, example: "2025-08-15" } + - name: status + in: query + required: false + schema: + type: string + enum: [booked, checked-in, waiting, called, in-progress, done, no-show, cancelled] + - name: instance_id + in: query + required: false + description: "Filter by jadwal tertentu" + schema: { type: integer, example: 1 } + responses: + '200': + description: Daftar antrian + content: + application/json: + example: + data: + # Grouped by schedule instance — dari Figma + - instance_id: 1 + schedule_summary: + department_name: "Poli Umum" + start_time: "08:00" + end_time: "09:45" + total_antrian: 20 + checked_in_count: 15 + queue_items: + - queue_id: 1 + queue_number: 1 + status: "called" + estimated_wait_minutes: 0 + nurse_notes: null + reservation: + reservation_id: 1 + complaint: "Batuk dan pilek" + patient: + patient_id: 1 + name: "Muhamad Rival Maulana" + gender: "laki-laki" + phone: "081234567890" + schedule: + instance_id: 1 + date: "2025-08-15" + start_time: "08:00" + end_time: "09:45" + - queue_id: 2 + queue_number: 2 + status: "waiting" + estimated_wait_minutes: 15 + nurse_notes: null + reservation: + reservation_id: 2 + complaint: "Demam" + patient: + patient_id: 2 + name: "Handika Chandra Pratama" + gender: "laki-laki" + phone: "082345678901" + schedule: + instance_id: 1 + date: "2025-08-15" + start_time: "08:00" + end_time: "09:45" + '403': + description: Bukan Nurse atau Doctor + content: + application/json: + example: { message: "Akses ditolak. Endpoint ini hanya untuk nurse dan doctor." } + +/queues/{id}/status: + parameters: + - name: id + in: path + required: true + schema: { type: integer, example: 2 } + + patch: + tags: [Queue] + summary: Update status antrian (Nurse only) + description: | + **Nurse only**. Dari Figma: nurse klik pasien di list → update status. + + Transisi valid: + ``` + booked → checked-in + checked-in → waiting + waiting → called (trigger notif ke pasien) + waiting → no-show + called → in-progress + in-progress → done + booked/checked-in → cancelled + ``` + + Nurse dapat menambahkan `nurse_notes` — tampil sebagai "Catatan" di nurse queue + dan "Hasil Skrining" di doctor queue (dari Figma). + + Setelah update, sistem otomatis: + 1. Sync `reservations.status` via QueueObserver + 2. Recalculate estimasi antrian semua pasien di instance yang sama + 3. Kirim notifikasi ke pasien jika status = `called` + operationId: updateQueueStatus + security: + - sanctumToken: [] + requestBody: + required: true + content: + application/json: + schema: { $ref: '../schemas/queue.yaml#/QueueStatusRequest' } + example: + status: "called" + nurse_notes: "Pasien sudah siap, tekanan darah normal" + responses: + '200': + description: Status diperbarui + content: + application/json: + example: + message: "Status antrian berhasil diperbarui." + data: + queue_id: 2 + queue_number: 2 + status: "called" + estimated_wait_minutes: null + nurse_notes: "Pasien sudah siap, tekanan darah normal" + updated_at: "2025-08-15T10:05:00Z" + reservation: + reservation_id: 2 + complaint: "Demam" + patient: + patient_id: 2 + name: "Handika Chandra Pratama" + gender: "laki-laki" + phone: "082345678901" + schedule: + instance_id: 1 + date: "2025-08-15" + start_time: "08:00" + end_time: "09:45" + '403': + description: Bukan Nurse + content: + application/json: + example: { message: "Akses ditolak. Endpoint ini hanya untuk nurse." } + '404': + description: Antrian tidak ditemukan + content: + application/json: + schema: { $ref: '../schemas/common.yaml#/ErrorResponse' } + '409': + description: Transisi status tidak valid + content: + application/json: + example: { message: "Transisi status tidak valid. Status 'done' tidak dapat diubah lagi." } + '422': + description: Nilai status tidak valid + content: + application/json: + schema: { $ref: '../schemas/common.yaml#/ValidationErrorResponse' } diff --git a/docs/openapi_v2/paths/reports.yaml b/docs/openapi_v2/paths/reports.yaml new file mode 100644 index 0000000..dd34383 --- /dev/null +++ b/docs/openapi_v2/paths/reports.yaml @@ -0,0 +1,110 @@ +# ============================================================================= +# Paths — EPIC 4: Reports +# Dari Figma admin-reports: +# - Filter tabs: Semua | Bulan Ini | Bulanan | Tahunan +# - "Cari berdasarkan tanggal..." +# - Tabel: # | Profesi | Hadir | Izin | Aktif | Selesai +# - Summary cards: "Total antrian hari ini: 240", "Total antrian aktif: 60", +# "Total antrian dibatalkan: 10" +# ============================================================================= + +/reports/queues: + get: + tags: [Reports] + summary: Laporan antrian berdasarkan periode (Admin) + description: | + **Admin only**. Dari Figma admin-reports: + - Filter: Semua | Bulan Ini | Bulanan | Tahunan + input tanggal + - Summary: Total antrian hari ini, Total aktif, Total dibatalkan + - Breakdown per departemen dan per dokter + operationId: getQueueReports + security: + - sanctumToken: [] + parameters: + - name: period + in: query + required: true + description: "Dari Figma tabs: Semua=all, Bulan Ini=current_month, Bulanan=monthly, Tahunan=yearly" + schema: + type: string + enum: [all, current_month, monthly, yearly] + example: "monthly" + - name: date + in: query + required: false + description: | + Tanggal referensi (YYYY-MM-DD): + - `monthly` → sistem ambil bulan dari tanggal ini + - `yearly` → sistem ambil tahun dari tanggal ini + - `all` / `current_month` → diabaikan + schema: + type: string + format: date + example: "2025-08-01" + - name: department_id + in: query + required: false + schema: { type: integer, example: 1 } + - name: doctor_id + in: query + required: false + schema: { type: integer, example: 1 } + responses: + '200': + description: Laporan antrian + content: + application/json: + schema: { $ref: '../schemas/queue.yaml#/QueueReportResource' } + example: + data: + period: "monthly" + period_label: "Agustus 2025" + date_range: + from: "2025-08-01" + to: "2025-08-31" + # Dari Figma summary cards + summary: + total_reservations: 240 + completed: 180 + cancelled: 10 + active: 50 + # "Total antrian hari ini" — khusus hari ini + today_total: 24 + by_department: + - department_id: 1 + department_name: "Poli Umum" + total: 100 + completed: 75 + cancelled: 5 + active: 20 + - department_id: 2 + department_name: "Poli Mata" + total: 80 + completed: 60 + cancelled: 3 + active: 17 + by_doctor: + - doctor_id: 1 + doctor_name: "dr. Ucok Napitupulu, Sp.PD" + department_name: "Poli Umum" + total: 60 + completed: 45 + cancelled: 3 + active: 12 + - doctor_id: 2 + doctor_name: "dr. Tirta Pengpeng" + department_name: "Poli Umum" + total: 40 + completed: 30 + cancelled: 2 + active: 8 + '403': + description: Bukan Admin + content: + application/json: + example: { message: "Akses ditolak. Endpoint ini hanya untuk admin." } + '422': + description: Parameter tidak valid + content: + application/json: + schema: { $ref: '../schemas/common.yaml#/ValidationErrorResponse' } diff --git a/docs/openapi_v2/paths/reservations.yaml b/docs/openapi_v2/paths/reservations.yaml new file mode 100644 index 0000000..16dfc04 --- /dev/null +++ b/docs/openapi_v2/paths/reservations.yaml @@ -0,0 +1,316 @@ +# ============================================================================= +# Paths — EPIC 3: Reservations +# Dari Figma patient-create-reservation: +# - "Informasi Pasien": Nama, No. BPJS, Tempat lahir, Tanggal lahir +# - "Keluhan Pasien" textarea +# - Info jadwal: Dokter, Estimasi nomor, Jam, Tanggal +# - "Dianjurkan datang pada 15:45 untuk check-in" +# - Checkbox "Checklist bila data sudah sesuai" +# - Tombol: Kirim | Kembali +# +# Dari Figma patient-reservation (history): +# - Tabs: Semua | Aktif | Selesai | Dibatalkan +# - Card: Poli, Dokter, "Estimasi nomor antrian: 16", Jam, Tanggal, Status badge +# +# Dari Figma patient-detail-reservation: +# - Panel kiri: list reservasi (history) +# - Panel kanan: "DETAIL RESERVATION" — info jadwal + info pasien + tombol Batalkan +# ============================================================================= + +/reservations: + get: + tags: [Booking] + summary: Riwayat reservasi pasien + description: | + **Patient only**. Dari Figma patient-reservation: + - Tab filter: Semua | Aktif | Selesai | Dibatalkan + - Card: Poli, Dokter, Estimasi nomor antrian, Jam, Tanggal, Status + operationId: getMyReservations + security: + - sanctumToken: [] + parameters: + - name: status + in: query + required: false + schema: + type: string + enum: [active, completed, cancelled] + example: "active" + - name: page + in: query + required: false + schema: { type: integer, default: 1 } + - name: per_page + in: query + required: false + schema: { type: integer, default: 10 } + responses: + '200': + description: Riwayat reservasi + content: + application/json: + example: + data: + - reservation_id: 1 + status: "active" + complaint: "Batuk dan pilek" + created_at: "2025-08-10T09:00:00Z" + doctor: + doctor_id: 1 + name: "dr. Ucok Napitupulu, Sp.PD" + specialization: "Spesialis Penyakit Dalam" + department: + department_id: 1 + name: "Poli Umum" + schedule: + instance_id: 1 + date: "2025-08-18" + start_time: "16:00" + end_time: "18:00" + recommended_checkin_time: "15:45" + queue: + queue_id: 1 + queue_number: 16 + status: "booked" + estimated_wait_minutes: null + estimated_queue_number: 16 + - reservation_id: 2 + status: "completed" + complaint: "Demam" + created_at: "2025-08-05T08:00:00Z" + doctor: + doctor_id: 1 + name: "dr. Ucok Napitupulu, Sp.PD" + specialization: "Spesialis Penyakit Dalam" + department: + department_id: 1 + name: "Poli Umum" + schedule: + instance_id: 2 + date: "2025-08-07" + start_time: "16:00" + end_time: "18:00" + recommended_checkin_time: "15:45" + queue: + queue_id: 2 + queue_number: 16 + status: "done" + estimated_wait_minutes: null + estimated_queue_number: 16 + - reservation_id: 3 + status: "cancelled" + complaint: null + created_at: "2025-08-01T07:00:00Z" + doctor: + doctor_id: 1 + name: "dr. Ucok Napitupulu, Sp.PD" + specialization: "Spesialis Penyakit Dalam" + department: + department_id: 1 + name: "Poli Umum" + schedule: + instance_id: 3 + date: "2025-08-03" + start_time: "16:00" + end_time: "18:00" + recommended_checkin_time: "15:45" + queue: + queue_id: 3 + queue_number: 16 + # Dari Figma: "Estimasi nomor antrian: Dibatalkan oleh pasien." + status: "cancelled" + estimated_wait_minutes: null + estimated_queue_number: null + meta: + current_page: 1 + per_page: 10 + total: 3 + last_page: 1 + from: 1 + to: 3 + '403': + description: Bukan Patient + content: + application/json: + schema: { $ref: '../schemas/common.yaml#/ErrorResponse' } + + post: + tags: [Booking] + summary: Buat reservasi baru + description: | + **Patient only**. Dari Figma patient-create-reservation: + - Input: instance_id (dari halaman pilih jadwal), complaint (keluhan pasien) + - Informasi Pasien ditampilkan dari `GET /patients/me` (pre-filled) + - Setelah submit: sistem generate queue_number otomatis dengan status `booked` + - [BARU] `instance_id` menggantikan `schedule_id` dari versi sebelumnya + operationId: createReservation + security: + - sanctumToken: [] + requestBody: + required: true + content: + application/json: + schema: { $ref: '../schemas/reservation.yaml#/ReservationRequest' } + example: + instance_id: 1 + complaint: "Batuk dan pilek sudah 3 hari, disertai demam ringan" + responses: + '201': + description: Reservasi berhasil — nomor antrian digenerate + content: + application/json: + example: + message: "Reservasi berhasil dibuat." + data: + reservation_id: 5 + status: "active" + complaint: "Batuk dan pilek sudah 3 hari, disertai demam ringan" + created_at: "2025-08-10T09:30:00Z" + doctor: + doctor_id: 1 + name: "dr. Ucok Napitupulu, Sp.PD" + specialization: "Spesialis Penyakit Dalam" + department: + department_id: 1 + name: "Poli Umum" + schedule: + instance_id: 1 + date: "2025-08-18" + start_time: "16:00" + end_time: "18:00" + # Dari Figma: "Dianjurkan datang pada 15:45 untuk check-in" + recommended_checkin_time: "15:45" + queue: + queue_id: 5 + queue_number: 16 + status: "booked" + estimated_wait_minutes: null + estimated_queue_number: 16 + '409': + description: Slot penuh atau double booking + content: + application/json: + example: + message: "Slot jadwal sudah penuh atau Anda sudah memiliki reservasi aktif pada jadwal ini." + '422': + description: Validasi gagal + content: + application/json: + schema: { $ref: '../schemas/common.yaml#/ValidationErrorResponse' } + example: + message: "The given data was invalid." + errors: + instance_id: ["Jadwal tidak tersedia atau sudah penuh."] + +/reservations/{id}: + parameters: + - name: id + in: path + required: true + schema: { type: integer, example: 1 } + + get: + tags: [Booking] + summary: Detail reservasi + description: | + **Patient only** — hanya reservasi milik pasien yang login. + Dari Figma patient-detail-reservation: + - "DETAIL RESERVATION" panel kanan + - Info jadwal: Dokter, Estimasi nomor, Jam, Tanggal + - "Dianjurkan datang pada 15:45 untuk check-in" + - Informasi Pasien: Nama, No.BPJS, Tempat lahir, Tanggal lahir, Keluhan + - Tombol: Batalkan | Kembali + - Queue status real-time (polling dari React SPA) + operationId: getReservationById + security: + - sanctumToken: [] + responses: + '200': + description: Detail reservasi + content: + application/json: + example: + data: + reservation_id: 1 + status: "active" + complaint: "Batuk dan pilek sudah 3 hari" + created_at: "2025-08-10T09:00:00Z" + patient: + patient_id: 5 + name: "Ucok Sitorus" + gender: "laki-laki" + bpjs_number: "0001234567890" + birth_place: "Medan" + birth_date: "1995-06-15" + doctor: + doctor_id: 1 + name: "dr. Ucok Napitupulu, Sp.PD" + specialization: "Spesialis Penyakit Dalam" + department: + department_id: 1 + name: "Poli Umum" + schedule: + instance_id: 1 + date: "2025-08-18" + start_time: "16:00" + end_time: "18:00" + recommended_checkin_time: "15:45" + queue: + queue_id: 1 + queue_number: 16 + status: "waiting" + estimated_wait_minutes: 25 + estimated_queue_number: 16 + '403': + description: Bukan milik pasien ini + content: + application/json: + example: { message: "Akses ditolak. Reservasi ini bukan milik Anda." } + '404': + description: Tidak ditemukan + content: + application/json: + schema: { $ref: '../schemas/common.yaml#/ErrorResponse' } + +/reservations/{id}/cancel: + parameters: + - name: id + in: path + required: true + schema: { type: integer, example: 1 } + + patch: + tags: [Booking] + summary: Batalkan reservasi + description: | + **Patient only** — hanya reservasi milik pasien yang login. + Dari Figma: tombol "Batalkan" di detail reservasi. + Tidak perlu request body. + Gagal jika antrian sudah `called/in-progress/done`. + operationId: cancelReservation + security: + - sanctumToken: [] + responses: + '200': + description: Reservasi dibatalkan + content: + application/json: + example: + message: "Reservasi berhasil dibatalkan." + data: + reservation_id: 1 + status: "cancelled" + queue: + queue_id: 1 + queue_number: 16 + status: "cancelled" + '403': + description: Bukan milik pasien ini + content: + application/json: + example: { message: "Akses ditolak. Reservasi ini bukan milik Anda." } + '409': + description: Tidak bisa dibatalkan + content: + application/json: + example: { message: "Reservasi tidak dapat dibatalkan karena pasien sudah dipanggil atau sedang konsultasi." } diff --git a/docs/openapi_v2/paths/schedule_instances.yaml b/docs/openapi_v2/paths/schedule_instances.yaml new file mode 100644 index 0000000..53838c6 --- /dev/null +++ b/docs/openapi_v2/paths/schedule_instances.yaml @@ -0,0 +1,207 @@ +# ============================================================================= +# Paths — EPIC 2/3: Schedule Instances (Tanggal Aktual) +# [BARU v2.1] +# +# Dari Figma patien-view-detail-department: +# - Card jadwal: "dr. Gia Pratama", "POLI UMUM", "Selasa - --/--/2026", +# "08:00", "Estimasi nomor antrian: 06", badge "Tersedia"/"Penuh" +# - Tombol "Pilih Jadwal" — mengarah ke Create Reservation +# +# Instances di-generate otomatis oleh Laravel Scheduler (2 minggu ke depan) +# Admin bisa override jam/kapasitas atau cancel instance tertentu +# ============================================================================= + +/schedule-instances: + get: + tags: [Schedule Instances] + summary: Lihat jadwal aktual tersedia + description: | + Mengembalikan daftar schedule instances (tanggal aktual). + Pasien menggunakan ini untuk melihat jadwal sebelum booking. + Dari Figma: menampilkan dokter, poli, tanggal, jam, estimasi antrian, status slot. + + Filter wajib minimal salah satu: `department_id` atau `doctor_id` atau `date`. + Default: hanya instances dengan `status = active` dan `date >= today`. + operationId: getScheduleInstances + security: + - sanctumToken: [] + parameters: + - name: department_id + in: query + required: false + description: "Filter by departemen — alur booking patient dari poli ke jadwal" + schema: { type: integer, example: 1 } + - name: doctor_id + in: query + required: false + description: "Filter by dokter tertentu" + schema: { type: integer, example: 1 } + - name: date + in: query + required: false + description: "Filter by tanggal spesifik (YYYY-MM-DD)" + schema: { type: string, format: date, example: "2025-08-18" } + - name: from_date + in: query + required: false + description: "Tanggal awal range. Default: hari ini." + schema: { type: string, format: date, example: "2025-08-18" } + - name: to_date + in: query + required: false + description: "Tanggal akhir range. Default: 2 minggu dari sekarang." + schema: { type: string, format: date, example: "2025-09-01" } + responses: + '200': + description: Daftar jadwal aktual + content: + application/json: + example: + data: + - instance_id: 1 + template_id: 1 + doctor_id: 1 + doctor: + name: "dr. Ucok Napitupulu, Sp.PD" + specialization: "Spesialis Penyakit Dalam" + department: + department_id: 1 + name: "Poli Umum" + date: "2025-08-18" + start_time: "08:00" + end_time: "12:00" + max_patients: 10 + booked_slots: 5 + available_slots: 5 + # Dari Figma: "Estimasi nomor antrian: 06" + estimated_queue_number: 6 + is_available: true + status: "active" + is_override: false + override_note: null + - instance_id: 2 + template_id: 1 + doctor_id: 1 + doctor: + name: "dr. Ucok Napitupulu, Sp.PD" + specialization: "Spesialis Penyakit Dalam" + department: + department_id: 1 + name: "Poli Umum" + date: "2025-08-18" + start_time: "10:00" + end_time: "14:00" + max_patients: 10 + booked_slots: 10 + available_slots: 0 + estimated_queue_number: 20 + # Dari Figma: badge "Penuh" + is_available: false + status: "active" + is_override: false + override_note: null + '401': + description: Tidak terautentikasi + content: + application/json: + schema: { $ref: '../schemas/common.yaml#/ErrorResponse' } + +/schedule-instances/{instanceId}: + parameters: + - name: instanceId + in: path + required: true + schema: { type: integer, example: 1 } + + get: + tags: [Schedule Instances] + summary: Detail satu schedule instance + operationId: getScheduleInstanceById + security: + - sanctumToken: [] + responses: + '200': + description: Detail instance + content: + application/json: + example: + data: + instance_id: 1 + template_id: 1 + doctor_id: 1 + doctor: + name: "dr. Ucok Napitupulu, Sp.PD" + specialization: "Spesialis Penyakit Dalam" + department: + department_id: 1 + name: "Poli Umum" + date: "2025-08-18" + start_time: "08:00" + end_time: "12:00" + max_patients: 10 + booked_slots: 5 + available_slots: 5 + estimated_queue_number: 6 + is_available: true + status: "active" + is_override: false + override_note: null + '404': + description: Tidak ditemukan + content: + application/json: + schema: { $ref: '../schemas/common.yaml#/ErrorResponse' } + + patch: + tags: [Schedule Instances] + summary: Override atau cancel satu instance + description: | + **Admin only**. Override jam/kapasitas atau cancel instance tertentu. + Jika `status = cancelled`: + - Reservasi dengan status `booked` di instance ini akan dibatalkan otomatis + - Notifikasi dikirim ke pasien terdampak + - Reservasi yang sudah `checked-in/waiting/called/in-progress` → 409 Conflict + operationId: updateScheduleInstance + security: + - sanctumToken: [] + requestBody: + required: true + content: + application/json: + schema: { $ref: '../schemas/schedule.yaml#/ScheduleInstanceRequest' } + example: + start_time: "09:00" + end_time: "13:00" + override_note: "Jam diubah karena rapat koordinasi dokter" + responses: + '200': + description: Instance diperbarui + content: + application/json: + example: + message: "Jadwal berhasil diperbarui." + data: + instance_id: 1 + date: "2025-08-18" + start_time: "09:00" + end_time: "13:00" + max_patients: 10 + status: "active" + is_override: true + override_note: "Jam diubah karena rapat koordinasi dokter" + '403': + description: Bukan Admin + content: + application/json: + schema: { $ref: '../schemas/common.yaml#/ErrorResponse' } + '404': + description: Instance tidak ditemukan + content: + application/json: + schema: { $ref: '../schemas/common.yaml#/ErrorResponse' } + '409': + description: Ada pasien aktif (checked-in/waiting/called/in-progress) + content: + application/json: + example: + message: "Jadwal tidak dapat diubah karena ada pasien yang sedang aktif di klinik." diff --git a/docs/openapi_v2/paths/schedule_templates.yaml b/docs/openapi_v2/paths/schedule_templates.yaml new file mode 100644 index 0000000..ae33a2d --- /dev/null +++ b/docs/openapi_v2/paths/schedule_templates.yaml @@ -0,0 +1,210 @@ +# ============================================================================= +# Paths — EPIC 2: Schedule Templates (Recurring) +# [BARU v2.1] Menggantikan /doctors/{id}/schedules untuk bagian recurring +# +# Dari Figma admin-doctor-detail (tabel Schedule): +# - Kolom: No., Hari (Senin/Selasa/dll), Pukul (09:00-15:00), Aksi (Delete/Edit) +# - Tombol "Add Schedule" di atas tabel +# Dari Figma admin-create/update-doctor-schedule: +# - Form: Doctor Name, No SIP, Hari Praktek (dropdown hari), Waktu +# ============================================================================= + +/doctors/{id}/schedule-templates: + parameters: + - name: id + in: path + required: true + description: "ID dokter" + schema: { type: integer, example: 1 } + + get: + tags: [Schedule Templates] + summary: Lihat template jadwal dokter + description: | + Menampilkan semua template jadwal recurring dokter. + Dari Figma doctor-detail: tabel berisi Hari + Jam praktik. + Dapat diakses semua role yang login. + operationId: getDoctorScheduleTemplates + security: + - sanctumToken: [] + responses: + '200': + description: Daftar template jadwal + content: + application/json: + example: + data: + - template_id: 1 + doctor_id: 1 + day_of_week: "senin" + start_time: "09:00" + end_time: "15:00" + max_patients: 10 + is_active: true + valid_from: "2025-08-01" + valid_until: null + - template_id: 2 + doctor_id: 1 + day_of_week: "selasa" + start_time: "09:00" + end_time: "15:00" + max_patients: 10 + is_active: true + valid_from: "2025-08-01" + valid_until: null + - template_id: 3 + doctor_id: 1 + day_of_week: "kamis" + start_time: "09:00" + end_time: "12:00" + max_patients: 8 + is_active: true + valid_from: "2025-08-01" + valid_until: null + '404': + description: Dokter tidak ditemukan + content: + application/json: + schema: { $ref: '../schemas/common.yaml#/ErrorResponse' } + + post: + tags: [Schedule Templates] + summary: Tambah template jadwal dokter + description: | + **Admin only**. Dari Figma "Add Doctor's Schedule" form: + Doctor Name (read-only), No SIP (read-only), Hari Praktek (dropdown), Waktu. + Setelah template dibuat, Laravel Scheduler akan auto-generate instances + untuk 2 minggu ke depan. + operationId: createDoctorScheduleTemplate + security: + - sanctumToken: [] + requestBody: + required: true + content: + application/json: + schema: { $ref: '../schemas/schedule.yaml#/ScheduleTemplateRequest' } + example: + day_of_week: "jumat" + start_time: "09:00" + end_time: "12:00" + max_patients: 10 + is_active: true + valid_from: "2025-08-01" + valid_until: null + responses: + '201': + description: Template dibuat — instances akan di-generate otomatis + content: + application/json: + example: + message: "Template jadwal berhasil dibuat. Instances akan di-generate otomatis." + data: + template_id: 4 + doctor_id: 1 + day_of_week: "jumat" + start_time: "09:00" + end_time: "12:00" + max_patients: 10 + is_active: true + valid_from: "2025-08-01" + valid_until: null + '403': + description: Bukan Admin + content: + application/json: + schema: { $ref: '../schemas/common.yaml#/ErrorResponse' } + '409': + description: Dokter sudah punya template aktif di hari yang sama + content: + application/json: + example: { message: "Dokter sudah memiliki jadwal aktif di hari Jumat." } + '422': + description: Validasi gagal + content: + application/json: + schema: { $ref: '../schemas/common.yaml#/ValidationErrorResponse' } + +/doctors/{id}/schedule-templates/{templateId}: + parameters: + - name: id + in: path + required: true + schema: { type: integer, example: 1 } + - name: templateId + in: path + required: true + schema: { type: integer, example: 1 } + + patch: + tags: [Schedule Templates] + summary: Update template jadwal + description: | + **Admin only**. Dari Figma "Update Doctor's Schedule" form. + Update template tidak mengubah instances yang sudah ada — + hanya mempengaruhi instances yang di-generate setelah update ini. + operationId: updateDoctorScheduleTemplate + security: + - sanctumToken: [] + requestBody: + required: true + content: + application/json: + schema: { $ref: '../schemas/schedule.yaml#/ScheduleTemplateRequest' } + example: + start_time: "10:00" + end_time: "14:00" + max_patients: 8 + responses: + '200': + description: Template diperbarui + content: + application/json: + example: + message: "Template jadwal berhasil diperbarui." + data: + template_id: 1 + doctor_id: 1 + day_of_week: "senin" + start_time: "10:00" + end_time: "14:00" + max_patients: 8 + is_active: true + valid_from: "2025-08-01" + valid_until: null + '403': + description: Bukan Admin + content: + application/json: + schema: { $ref: '../schemas/common.yaml#/ErrorResponse' } + '404': + description: Template tidak ditemukan + content: + application/json: + schema: { $ref: '../schemas/common.yaml#/ErrorResponse' } + + delete: + tags: [Schedule Templates] + summary: Hapus template jadwal + description: | + **Admin only**. Menonaktifkan template (is_active = false) atau menghapus. + Instances yang sudah ada dan belum completed tidak dihapus otomatis. + Gunakan PATCH /schedule-instances/{id} untuk cancel instance per instance. + operationId: deleteDoctorScheduleTemplate + security: + - sanctumToken: [] + responses: + '200': + description: Template dihapus + content: + application/json: + example: { message: "Template jadwal berhasil dihapus." } + '403': + description: Bukan Admin + content: + application/json: + schema: { $ref: '../schemas/common.yaml#/ErrorResponse' } + '404': + description: Template tidak ditemukan + content: + application/json: + schema: { $ref: '../schemas/common.yaml#/ErrorResponse' } diff --git a/docs/openapi_v2/schemas/auth.yaml b/docs/openapi_v2/schemas/auth.yaml new file mode 100644 index 0000000..222decd --- /dev/null +++ b/docs/openapi_v2/schemas/auth.yaml @@ -0,0 +1,98 @@ +# ============================================================================= +# Schemas — Authentication +# ============================================================================= + +RegisterRequest: + type: object + required: [name, email, password, password_confirmation, phone] + properties: + name: + type: string + maxLength: 255 + example: "Ucok Sitorus" + email: + type: string + format: email + example: "ucok@example.com" + password: + type: string + format: password + minLength: 8 + example: "password123" + password_confirmation: + type: string + format: password + example: "password123" + phone: + type: string + maxLength: 15 + example: "081234567890" + +LoginRequest: + type: object + required: [email, password] + properties: + email: + type: string + format: email + example: "ucok@example.com" + password: + type: string + format: password + example: "password123" + +# Response setelah login — mengembalikan Sanctum plain-text token +# Simpan di React state/memory, BUKAN localStorage +LoginResponse: + type: object + properties: + message: + type: string + example: "Login berhasil." + data: + type: object + properties: + token: + type: string + description: "Sanctum plain-text token. Gunakan: Authorization: Bearer {token}" + example: "3|aB1cUcokD2eF3gH4iJ5kL6" + token_type: + type: string + example: "Bearer" + user: + $ref: '#/UserResource' + +# Resource user — profile bervariasi per role: +# patient → { patient_id, name, phone, bpjs_number, birth_place, birth_date, gender } +# doctor → { doctor_id, name, specialization, sip_number, gender, birth_date, doctor_status, department } +# nurse → { nurse_id, name, sip_number, gender, birth_date, department } +# admin → { admin_id, name } +UserResource: + type: object + properties: + user_id: + type: integer + example: 10 + email: + type: string + format: email + example: "ucok@example.com" + role: + type: string + enum: [patient, doctor, nurse, admin] + example: "patient" + status: + type: string + enum: [active, inactive] + example: "active" + profile: + type: object + description: "Struktur berbeda per role. Lihat PatientProfileResource / DoctorResource / NurseResource." + example: + patient_id: 5 + name: "Ucok Sitorus" + phone: "081234567890" + bpjs_number: "0001234567890" + birth_place: "Medan" + birth_date: "1995-06-15" + gender: "laki-laki" diff --git a/docs/openapi_v2/schemas/common.yaml b/docs/openapi_v2/schemas/common.yaml new file mode 100644 index 0000000..14ed172 --- /dev/null +++ b/docs/openapi_v2/schemas/common.yaml @@ -0,0 +1,45 @@ +# ============================================================================= +# Schemas — Common / Shared +# ============================================================================= + +SuccessResponse: + type: object + properties: + message: + type: string + example: "Operasi berhasil." + +ErrorResponse: + type: object + properties: + message: + type: string + example: "Terjadi kesalahan." + +# Format default Laravel 422 validation error +ValidationErrorResponse: + type: object + properties: + message: + type: string + example: "The given data was invalid." + errors: + type: object + additionalProperties: + type: array + items: + type: string + example: + email: ["The email has already been taken."] + password: ["The password must be at least 8 characters."] + +# Format Laravel Resource Pagination +PaginationMeta: + type: object + properties: + current_page: { type: integer, example: 1 } + per_page: { type: integer, example: 10 } + total: { type: integer, example: 35 } + last_page: { type: integer, example: 4 } + from: { type: integer, example: 1 } + to: { type: integer, example: 10 } diff --git a/docs/openapi_v2/schemas/department.yaml b/docs/openapi_v2/schemas/department.yaml new file mode 100644 index 0000000..6246b75 --- /dev/null +++ b/docs/openapi_v2/schemas/department.yaml @@ -0,0 +1,46 @@ +# ============================================================================= +# Schemas — Department +# Dari Figma: card poli menampilkan nama, deskripsi, jumlah dokter tersedia +# Screen: patient-view-department — "3 dokter tersedia", "Poli Umum" +# ============================================================================= + +DepartmentResource: + type: object + properties: + department_id: + type: integer + example: 1 + name: + type: string + example: "Poli Umum" + description: + type: string + nullable: true + # Dari Figma: "Pemeriksaan Kesehatan Umum" — deskripsi singkat di card + example: "Pusat layanan kesehatan primer untuk diagnosa awal, pengobatan umum, dan rujukan" + # Dari Figma: card menampilkan "3 dokter tersedia" — computed field + doctors_count: + type: integer + description: "Jumlah dokter aktif di departemen ini." + example: 3 + created_at: + type: string + format: date-time + example: "2025-01-01T00:00:00Z" + updated_at: + type: string + format: date-time + example: "2025-08-01T10:00:00Z" + +# Digunakan untuk POST create dan PATCH update (partial) +DepartmentRequest: + type: object + properties: + name: + type: string + maxLength: 255 + example: "Poli Umum" + description: + type: string + nullable: true + example: "Pusat layanan kesehatan primer untuk diagnosa awal, pengobatan umum, dan rujukan" diff --git a/docs/openapi_v2/schemas/doctor.yaml b/docs/openapi_v2/schemas/doctor.yaml new file mode 100644 index 0000000..f1d29b3 --- /dev/null +++ b/docs/openapi_v2/schemas/doctor.yaml @@ -0,0 +1,128 @@ +# ============================================================================= +# Schemas — Doctor +# [BARU v2.1] Dari Figma form Doctor: +# - sip_number : field "No SIP" di form create/update doctor +# - birth_date : field "Tanggal Lahir" di form +# - gender : field "Jenis Kelamin" di form +# Dari Figma doctor-detail: tabel schedule menampilkan Hari + Jam +# Dari Figma doctor-queue-list: kolom tambahan Keluhan + Jenis Kelamin +# ============================================================================= + +DoctorResource: + type: object + properties: + doctor_id: + type: integer + example: 1 + name: + type: string + example: "dr. Ucok Napitupulu, Sp.PD" + specialization: + type: string + example: "Spesialis Penyakit Dalam" + # [BARU] dari Figma form doctor + sip_number: + type: string + nullable: true + description: "Nomor Surat Izin Praktik dokter." + example: "112345678" + birth_date: + type: string + format: date + nullable: true + example: "1980-03-20" + gender: + type: string + nullable: true + enum: ["laki-laki", "perempuan"] + example: "laki-laki" + # doctor_status: dikelola DoctorDelayDetectionService (auto) dan Nurse/Admin (manual) + doctor_status: + type: string + enum: [active, late, absent] + example: "active" + department: + type: object + properties: + department_id: { type: integer, example: 1 } + name: { type: string, example: "Poli Umum" } + user_id: + type: integer + example: 5 + created_at: + type: string + format: date-time + example: "2025-01-15T08:00:00Z" + updated_at: + type: string + format: date-time + example: "2025-08-01T10:00:00Z" + +# Request body POST — Register Doctor (Admin) +# Membuat users + doctors sekaligus dalam satu transaksi +DoctorRequest: + type: object + required: [name, email, password, specialization, department_id] + properties: + name: + type: string + maxLength: 255 + example: "dr. Ucok Napitupulu, Sp.PD" + email: + type: string + format: email + example: "ucok.dokter@hospital.com" + password: + type: string + format: password + minLength: 8 + example: "password123" + specialization: + type: string + example: "Spesialis Penyakit Dalam" + department_id: + type: integer + example: 1 + # [BARU] field dari Figma + sip_number: + type: string + nullable: true + example: "112345678" + birth_date: + type: string + format: date + nullable: true + example: "1980-03-20" + gender: + type: string + nullable: true + enum: ["laki-laki", "perempuan"] + example: "laki-laki" + +# Request body PATCH — Update Doctor (partial update) +DoctorUpdateRequest: + type: object + properties: + name: + type: string + example: "dr. Ucok Napitupulu, Sp.PD" + specialization: + type: string + example: "Spesialis Penyakit Dalam" + department_id: + type: integer + example: 1 + sip_number: + type: string + nullable: true + example: "112345678" + birth_date: + type: string + format: date + nullable: true + example: "1980-03-20" + gender: + type: string + nullable: true + enum: ["laki-laki", "perempuan"] + example: "laki-laki" diff --git a/docs/openapi_v2/schemas/nurse.yaml b/docs/openapi_v2/schemas/nurse.yaml new file mode 100644 index 0000000..43a81ba --- /dev/null +++ b/docs/openapi_v2/schemas/nurse.yaml @@ -0,0 +1,116 @@ +# ============================================================================= +# Schemas — Nurse +# [BARU v2.1] Dari Figma form Nurse: +# - sip_number : field "No SIP" di form +# - birth_date : field "Tanggal Lahir" +# - gender : field "Jenis Kelamin" +# ============================================================================= + +NurseResource: + type: object + properties: + nurse_id: + type: integer + example: 1 + name: + type: string + example: "Ucok Sihombing" + # [BARU] dari Figma form nurse + sip_number: + type: string + nullable: true + description: "Nomor Surat Izin Praktik suster." + example: "112345678" + birth_date: + type: string + format: date + nullable: true + example: "1990-07-10" + gender: + type: string + nullable: true + enum: ["laki-laki", "perempuan"] + example: "perempuan" + # department nullable — nurse bisa lintas departemen + department: + type: object + nullable: true + properties: + department_id: { type: integer, example: 1 } + name: { type: string, example: "Poli Umum" } + user_id: + type: integer + example: 8 + created_at: + type: string + format: date-time + example: "2025-01-15T08:00:00Z" + updated_at: + type: string + format: date-time + example: "2025-08-01T10:00:00Z" + +# Request body POST — Register Nurse (Admin) +NurseRequest: + type: object + required: [name, email, password] + properties: + name: + type: string + maxLength: 255 + example: "Ucok Sihombing" + email: + type: string + format: email + example: "ucok.nurse@hospital.com" + password: + type: string + format: password + minLength: 8 + example: "password123" + # department_id opsional — nurse bisa lintas departemen + department_id: + type: integer + nullable: true + example: 1 + # [BARU] field dari Figma + sip_number: + type: string + nullable: true + example: "112345678" + birth_date: + type: string + format: date + nullable: true + example: "1990-07-10" + gender: + type: string + nullable: true + enum: ["laki-laki", "perempuan"] + example: "perempuan" + +# Request body PATCH — Update Nurse (partial update) +NurseUpdateRequest: + type: object + properties: + name: + type: string + example: "Ucok Sihombing, S.Kep" + department_id: + type: integer + nullable: true + example: 2 + sip_number: + type: string + nullable: true + example: "112345678" + birth_date: + type: string + format: date + nullable: true + example: "1990-07-10" + gender: + type: string + nullable: true + enum: ["laki-laki", "perempuan"] + example: "perempuan" diff --git a/docs/openapi_v2/schemas/patient.yaml b/docs/openapi_v2/schemas/patient.yaml new file mode 100644 index 0000000..83a8def --- /dev/null +++ b/docs/openapi_v2/schemas/patient.yaml @@ -0,0 +1,80 @@ +# ============================================================================= +# Schemas — Patient Profile +# [BARU v2.1] Dari analisis Figma form Create Reservation: +# - bpjs_number, birth_place, birth_date, gender ditampilkan di form +# - Data ini perlu ada sebelum pasien bisa submit reservasi +# ============================================================================= + +# Resource profil pasien lengkap +PatientProfileResource: + type: object + properties: + patient_id: + type: integer + example: 5 + name: + type: string + example: "Ucok Sitorus" + phone: + type: string + example: "081234567890" + # Field dari Figma form Create Reservation + # "Informasi Pasien" section menampilkan semua field ini + bpjs_number: + type: string + nullable: true + description: "Nomor BPJS pasien. Nullable — bisa dilengkapi saat booking." + example: "0001234567890" + birth_place: + type: string + nullable: true + description: "Tempat lahir pasien." + example: "Medan" + birth_date: + type: string + format: date + nullable: true + description: "Tanggal lahir pasien (YYYY-MM-DD)." + example: "1995-06-15" + gender: + type: string + nullable: true + enum: ["laki-laki", "perempuan"] + example: "laki-laki" + user_id: + type: integer + example: 10 + +# Request body untuk PATCH /patients/me (update profil sendiri) +# Semua field opsional — partial update +PatientProfileRequest: + type: object + properties: + name: + type: string + maxLength: 255 + example: "Ucok Sitorus" + phone: + type: string + maxLength: 15 + example: "081234567890" + bpjs_number: + type: string + nullable: true + maxLength: 50 + example: "0001234567890" + birth_place: + type: string + nullable: true + maxLength: 100 + example: "Medan" + birth_date: + type: string + format: date + nullable: true + example: "1995-06-15" + gender: + type: string + nullable: true + enum: ["laki-laki", "perempuan"] + example: "laki-laki" diff --git a/docs/openapi_v2/schemas/queue.yaml b/docs/openapi_v2/schemas/queue.yaml new file mode 100644 index 0000000..4ac93c5 --- /dev/null +++ b/docs/openapi_v2/schemas/queue.yaml @@ -0,0 +1,148 @@ +# ============================================================================= +# Schemas — Queue +# Dari Figma nurse-queue-list: +# - Kolom: No. Antrian, Nama Pasien, Status, Estimasi Waktu Tunggu, Catatan +# - Status badge: CALLED, WAITING, BOOKED, DONE +# - Filter tabs: Semua | Booked | Called | Done +# Dari Figma doctor-queue-list: +# - Kolom tambahan: Keluhan, Jenis Kelamin (dari patients/reservations) +# - "Hasil Skrining" — field catatan nurse (belum di-spec sebelumnya) +# ============================================================================= + +QueueResource: + type: object + properties: + queue_id: + type: integer + example: 4 + queue_number: + type: integer + description: "Nomor urut antrian dalam satu instance. Dimulai dari 1." + example: 4 + # State machine: booked→checked-in→waiting→called→in-progress→done/no-show + status: + type: string + enum: [booked, checked-in, waiting, called, in-progress, done, no-show, cancelled] + example: "waiting" + estimated_wait_minutes: + type: integer + nullable: true + description: "Estimasi menit tunggu. null jika belum dikalkulasi (status masih booked)." + example: 25 + # [BARU dari Figma doctor-queue-list] catatan nurse saat check-in + # Figma menampilkan kolom "Catatan" di nurse queue dan "Hasil Skrining" di doctor queue + nurse_notes: + type: string + nullable: true + description: "Catatan dari nurse saat check-in atau update status. Terlihat oleh dokter." + example: "Pasien terlihat lemas, tekanan darah 130/90" + updated_at: + type: string + format: date-time + example: "2025-08-15T09:05:00Z" + # Nested reservation — berisi info pasien untuk tampilan nurse/doctor + reservation: + type: object + properties: + reservation_id: { type: integer, example: 4 } + # complaint dari reservations — ditampilkan di doctor-queue-list Figma + complaint: + type: string + nullable: true + example: "Batuk dan pilek sudah 3 hari" + patient: + type: object + properties: + patient_id: { type: integer, example: 4 } + name: { type: string, example: "Ucok Siahaan" } + # gender — ditampilkan di doctor-queue-list Figma (kolom "Jenis Kelamin") + gender: + type: string + nullable: true + example: "laki-laki" + phone: { type: string, example: "084567890123" } + schedule: + type: object + properties: + instance_id: { type: integer, example: 1 } + date: { type: string, format: date, example: "2025-08-15" } + start_time: { type: string, example: "08:00" } + end_time: { type: string, example: "12:00" } + +# Request body PATCH /queues/{id}/status (Nurse only) +QueueStatusRequest: + type: object + required: [status] + properties: + status: + type: string + description: | + Status antrian baru. Transisi valid: + booked → checked-in + checked-in → waiting + waiting → called (trigger notifikasi ke pasien) + waiting → no-show + called → in-progress + in-progress → done + booked → cancelled + checked-in → cancelled + enum: [checked-in, waiting, called, in-progress, done, no-show, cancelled] + example: "called" + # [BARU dari Figma] kolom "Catatan" di nurse queue list + # Nurse bisa tambah catatan saat update status + nurse_notes: + type: string + nullable: true + description: "Catatan nurse — ditampilkan sebagai 'Hasil Skrining' di queue list dokter." + example: "Pasien terlihat lemas, tekanan darah 130/90" + +# Resource laporan antrian untuk Admin +QueueReportResource: + type: object + properties: + data: + type: object + properties: + period: + type: string + enum: [daily, monthly, yearly] + example: "monthly" + period_label: + type: string + example: "Agustus 2025" + date_range: + type: object + properties: + from: { type: string, format: date, example: "2025-08-01" } + to: { type: string, format: date, example: "2025-08-31" } + # Dari Figma admin-reports: "Total antrian hari ini: 240", dll + summary: + type: object + properties: + total_reservations: { type: integer, example: 240 } + completed: { type: integer, example: 180 } + cancelled: { type: integer, example: 10 } + active: { type: integer, example: 50 } + by_department: + type: array + items: + type: object + properties: + department_id: { type: integer, example: 1 } + department_name: { type: string, example: "Poli Umum" } + total: { type: integer, example: 70 } + completed: { type: integer, example: 50 } + cancelled: { type: integer, example: 5 } + active: { type: integer, example: 15 } + by_doctor: + type: array + items: + type: object + properties: + doctor_id: { type: integer, example: 1 } + doctor_name: { type: string, example: "dr. Ucok Napitupulu, Sp.PD" } + department_name: { type: string, example: "Poli Umum" } + total: { type: integer, example: 40 } + completed: { type: integer, example: 30 } + cancelled: { type: integer, example: 3 } + active: { type: integer, example: 7 } diff --git a/docs/openapi_v2/schemas/reservation.yaml b/docs/openapi_v2/schemas/reservation.yaml new file mode 100644 index 0000000..b9f4518 --- /dev/null +++ b/docs/openapi_v2/schemas/reservation.yaml @@ -0,0 +1,123 @@ +# ============================================================================= +# Schemas — Reservation +# [BARU v2.1]: +# - instance_id menggantikan schedule_id (FK ke schedule_instances) +# - complaint (text, nullable) — dari Figma form Create Reservation +# Tampil di: form booking, detail reservasi, dan queue list dokter +# ============================================================================= + +ReservationResource: + type: object + properties: + reservation_id: + type: integer + example: 1 + # status: derived dari queues.status via QueueObserver + # active = queue belum done/no-show/cancelled + # completed = queue done + # cancelled = queue no-show atau cancelled + status: + type: string + enum: [active, completed, cancelled] + example: "active" + # [BARU] keluhan pasien — dari Figma "Keluhan Pasien" di form booking + complaint: + type: string + nullable: true + description: "Keluhan pasien yang diisi saat membuat reservasi. Ditampilkan di queue list dokter." + example: "Batuk dan pilek sudah 3 hari" + created_at: + type: string + format: date-time + example: "2025-08-10T09:00:00Z" + # Info pasien — muncul di detail reservasi dan di queue list dokter + patient: + type: object + properties: + patient_id: { type: integer, example: 5 } + name: { type: string, example: "Ucok Sitorus" } + gender: + type: string + nullable: true + example: "laki-laki" + bpjs_number: + type: string + nullable: true + example: "0001234567890" + birth_place: + type: string + nullable: true + example: "Medan" + birth_date: + type: string + format: date + nullable: true + example: "1995-06-15" + doctor: + type: object + properties: + doctor_id: { type: integer, example: 1 } + name: { type: string, example: "dr. Ucok Napitupulu, Sp.PD" } + specialization: { type: string, example: "Spesialis Penyakit Dalam" } + department: + type: object + properties: + department_id: { type: integer, example: 1 } + name: { type: string, example: "Poli Umum" } + # Schedule info — dari instance (tanggal aktual) + schedule: + type: object + properties: + instance_id: { type: integer, example: 1 } + date: { type: string, format: date, example: "2025-08-18" } + start_time: { type: string, example: "08:00" } + end_time: { type: string, example: "12:00" } + # Dari Figma: "Dianjurkan datang pada 15:45 untuk check-in" + recommended_checkin_time: + type: string + description: "15 menit sebelum start_time — ditampilkan di detail reservasi Figma" + example: "07:45" + # Queue — real-time, di-join ke reservations + queue: + type: object + properties: + queue_id: { type: integer, example: 1 } + queue_number: { type: integer, example: 4 } + status: + type: string + enum: [booked, checked-in, waiting, called, in-progress, done, no-show, cancelled] + example: "waiting" + estimated_wait_minutes: + type: integer + nullable: true + example: 25 + # Dari Figma: "Estimasi nomor antrian: 16" — ditampilkan di card history + estimated_queue_number: + type: integer + description: "Nomor antrian estimasi — sama dengan queue_number setelah booking" + example: 16 + +# Request body POST — Create Reservation (Patient only) +# [BARU v2.1]: instance_id menggantikan schedule_id +# complaint ditambahkan — wajib diisi saat booking +ReservationRequest: + type: object + required: [instance_id] + properties: + # FK ke schedule_instances — bukan schedule_templates + # Pasien booking ke tanggal aktual (instance), bukan template recurring + instance_id: + type: integer + description: | + ID jadwal aktual (schedule_instances.instance_id). + doctor_id diambil otomatis dari instance.doctor_id di backend. + Validasi: slot tersedia, tidak double booking, tanggal belum lampau, + instance.status = active. + example: 1 + # [BARU] dari Figma form Create Reservation + complaint: + type: string + nullable: true + description: "Keluhan pasien. Opsional tapi direkomendasikan untuk diisi." + maxLength: 1000 + example: "Batuk dan pilek sudah 3 hari, disertai demam ringan" diff --git a/docs/openapi_v2/schemas/schedule.yaml b/docs/openapi_v2/schemas/schedule.yaml new file mode 100644 index 0000000..cf1c1d6 --- /dev/null +++ b/docs/openapi_v2/schemas/schedule.yaml @@ -0,0 +1,199 @@ +# ============================================================================= +# Schemas — Schedule (Template + Instance) +# [BARU v2.1] Tabel schedules lama DIGANTI dengan sistem hybrid: +# +# ScheduleTemplate → recurring pattern (Hari + Jam, misal "Senin 08:00-12:00") +# ScheduleInstance → tanggal aktual (misal "2025-08-18 08:00-12:00") +# +# Dari Figma admin-doctor-detail: +# - Jadwal ditampilkan: Hari (Senin/Selasa/dll) + Jam (09:00-15:00) +# - Ada tombol Edit + Delete per jadwal +# - Tombol "Add Schedule" untuk tambah jadwal baru +# +# Dari Figma patien-view-detail-department: +# - Pasien melihat: "Selasa - --/--/2026", "08:00", "Estimasi nomor antrian: 06" +# - Instance yang ditampilkan ke pasien, bukan template +# ============================================================================= + +# ---- TEMPLATE ---- + +ScheduleTemplateResource: + type: object + properties: + template_id: + type: integer + example: 1 + doctor_id: + type: integer + example: 1 + # Dari Figma: jadwal disimpan per hari (Senin, Selasa, dst) + day_of_week: + type: string + enum: [senin, selasa, rabu, kamis, jumat, sabtu, minggu] + example: "senin" + start_time: + type: string + description: "Format HH:MM" + example: "08:00" + end_time: + type: string + description: "Format HH:MM" + example: "12:00" + max_patients: + type: integer + example: 10 + is_active: + type: boolean + description: "false = template tidak generate instance baru, tapi instance lama tetap valid" + example: true + valid_from: + type: string + format: date + description: "Tanggal template mulai berlaku" + example: "2025-08-01" + valid_until: + type: string + format: date + nullable: true + description: "Tanggal template berakhir. null = berlaku selamanya." + example: null + created_at: + type: string + format: date-time + example: "2025-07-01T00:00:00Z" + +# Request body POST/PATCH template +ScheduleTemplateRequest: + type: object + required: [day_of_week, start_time, end_time] + properties: + day_of_week: + type: string + enum: [senin, selasa, rabu, kamis, jumat, sabtu, minggu] + example: "senin" + start_time: + type: string + description: "Format HH:MM" + example: "08:00" + end_time: + type: string + description: "Format HH:MM, harus setelah start_time" + example: "12:00" + max_patients: + type: integer + minimum: 1 + maximum: 100 + default: 10 + example: 10 + is_active: + type: boolean + default: true + example: true + valid_from: + type: string + format: date + example: "2025-08-01" + valid_until: + type: string + format: date + nullable: true + example: null + +# ---- INSTANCE ---- + +ScheduleInstanceResource: + type: object + properties: + instance_id: + type: integer + example: 1 + template_id: + type: integer + nullable: true + description: "null jika instance manual tanpa template (jadwal insidental)" + example: 1 + doctor_id: + type: integer + example: 1 + # Info dokter untuk tampilan pasien (Figma: "dr. Gia Pratama", "POLI UMUM") + doctor: + type: object + properties: + name: { type: string, example: "dr. Ucok Napitupulu, Sp.PD" } + specialization: { type: string, example: "Spesialis Penyakit Dalam" } + department: + type: object + properties: + department_id: { type: integer, example: 1 } + name: { type: string, example: "Poli Umum" } + date: + type: string + format: date + description: "Tanggal aktual jadwal praktik" + example: "2025-08-18" + start_time: + type: string + example: "08:00" + end_time: + type: string + example: "12:00" + max_patients: + type: integer + example: 10 + # Computed — dihitung runtime dari jumlah reservasi aktif + booked_slots: + type: integer + example: 4 + available_slots: + type: integer + example: 6 + # Dari Figma: "Estimasi nomor antrian: 06" — ditampilkan di card jadwal pasien + estimated_queue_number: + type: integer + description: "Estimasi nomor antrian yang akan didapat pasien jika booking sekarang (booked_slots + 1)" + example: 5 + is_available: + type: boolean + description: "true jika available_slots > 0 dan tanggal belum lampau" + example: true + status: + type: string + enum: [active, cancelled, completed] + example: "active" + is_override: + type: boolean + description: "true jika admin mengubah data dari template asli" + example: false + override_note: + type: string + nullable: true + description: "Catatan alasan override oleh admin" + example: null + +# Request body untuk Admin override instance +# Digunakan PATCH /schedule-instances/{id} +ScheduleInstanceRequest: + type: object + properties: + start_time: + type: string + description: "Format HH:MM — override jam mulai" + example: "09:00" + end_time: + type: string + description: "Format HH:MM — override jam selesai" + example: "13:00" + max_patients: + type: integer + minimum: 1 + example: 8 + status: + type: string + enum: [active, cancelled] + description: "cancelled = batalkan instance ini (pasien booked akan dinotif)" + example: "active" + override_note: + type: string + nullable: true + description: "Alasan perubahan — ditampilkan di notifikasi pasien terdampak" + example: "Jam berubah karena acara rapat rumah sakit" diff --git a/frontend/.env.example b/frontend/.env.example new file mode 100644 index 0000000..0ea2969 --- /dev/null +++ b/frontend/.env.example @@ -0,0 +1,2 @@ +VITE_API_URL=http://localhost:8000/api +VITE_API_BASE_URL=http://localhost:8000 \ No newline at end of file diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 91d33b0..7f1b983 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -9,8 +9,10 @@ "version": "0.0.0", "dependencies": { "@tailwindcss/vite": "^4.3.0", + "axios": "^1.18.0", "react": "^19.2.6", "react-dom": "^19.2.6", + "react-router-dom": "^7.18.0", "tailwindcss": "^4.3.0" }, "devDependencies": { @@ -19,6 +21,7 @@ "@rolldown/plugin-babel": "^0.2.3", "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.2", + "@testing-library/user-event": "^14.6.1", "@types/babel__core": "^7.20.5", "@types/node": "^24.12.3", "@types/react": "^19.2.14", @@ -1463,6 +1466,20 @@ } } }, + "node_modules/@testing-library/user-event": { + "version": "14.6.1", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz", + "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "peerDependencies": { + "@testing-library/dom": ">=7.21.4" + } + }, "node_modules/@tybys/wasm-util": { "version": "0.10.2", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", @@ -2000,6 +2017,18 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/ajv": { "version": "6.15.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", @@ -2062,6 +2091,24 @@ "node": ">=12" } }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.18.0.tgz", + "integrity": "sha512-E32NzpYKp++W7XRe52rHiXV2ehxmh3wbdgO7MHeFM+vqxLBYHzt0ElkiImtOBxtOmyp0yoC8C6uESVV84Y2/hw==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.16.0", + "form-data": "^4.0.5", + "https-proxy-agent": "^5.0.1", + "proxy-from-env": "^2.1.0" + } + }, "node_modules/babel-plugin-react-compiler": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/babel-plugin-react-compiler/-/babel-plugin-react-compiler-1.0.0.tgz", @@ -2152,6 +2199,19 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001793", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001793.tgz", @@ -2183,6 +2243,18 @@ "node": ">=18" } }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -2190,6 +2262,19 @@ "dev": true, "license": "MIT" }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -2251,7 +2336,6 @@ "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -2279,6 +2363,15 @@ "dev": true, "license": "MIT" }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", @@ -2306,6 +2399,20 @@ "license": "MIT", "peer": true }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/electron-to-chromium": { "version": "1.5.368", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.368.tgz", @@ -2339,6 +2446,24 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es-module-lexer": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", @@ -2346,6 +2471,33 @@ "dev": true, "license": "MIT" }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -2660,6 +2812,42 @@ "dev": true, "license": "ISC" }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -2674,6 +2862,15 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -2684,6 +2881,43 @@ "node": ">=6.9.0" } }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -2710,12 +2944,63 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "license": "ISC" }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/hermes-estree": { "version": "0.25.1", "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", @@ -2746,6 +3031,19 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" } }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -3246,6 +3544,15 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/mdn-data": { "version": "2.27.1", "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", @@ -3253,6 +3560,27 @@ "dev": true, "license": "CC0-1.0" }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/min-indent": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", @@ -3283,7 +3611,6 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, "license": "MIT" }, "node_modules/nanoid": { @@ -3497,6 +3824,15 @@ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" } }, + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -3536,6 +3872,44 @@ "license": "MIT", "peer": true }, + "node_modules/react-router": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.0.tgz", + "integrity": "sha512-pTTGt8J+ji1NOmYnjzT+bAJy/1zD+Jp4ziO6cL7T3ZLvXKtusO7BpFqlRXitqpcPVqllsIXFHRMt+2/k3Xn6HQ==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.18.0.tgz", + "integrity": "sha512-Fi0yY6kgtKae/Th2xibdWK0KSdYZ4B53Gyf6wRtomOKWgpNm7H7+DyfDhncdz9FKbpS+1jmDhg3F4WoGJ+yFOA==", + "license": "MIT", + "dependencies": { + "react-router": "7.18.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, "node_modules/redent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", @@ -3624,6 +3998,12 @@ "semver": "bin/semver.js" } }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index 6bd9575..f73efd6 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -13,8 +13,10 @@ }, "dependencies": { "@tailwindcss/vite": "^4.3.0", + "axios": "^1.18.0", "react": "^19.2.6", "react-dom": "^19.2.6", + "react-router-dom": "^7.18.0", "tailwindcss": "^4.3.0" }, "devDependencies": { @@ -23,6 +25,7 @@ "@rolldown/plugin-babel": "^0.2.3", "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.2", + "@testing-library/user-event": "^14.6.1", "@types/babel__core": "^7.20.5", "@types/node": "^24.12.3", "@types/react": "^19.2.14", diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index b4f6543..08f4b04 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,5 +1,36 @@ +import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'; +import { AuthProvider } from './contexts/AuthContext'; +import { PublicRoute } from './routes/PublicRoute'; +import { ProtectedRoute } from './routes/ProtectedRoute'; +import RegisterPage from './pages/RegisterPage'; +import LoginPage from './pages/LoginPage'; +import DashboardPage from './pages/DashboardPage'; + function App() { - return <>; + return ( + + + + {/* Public routes — redirect to /dashboard if already authenticated */} + }> + } /> + } /> + + + {/* Protected routes — redirect to /login if not authenticated */} + }> + } + /> + + + {/* Fallback */} + } /> + + + + ); } export default App; diff --git a/frontend/src/api/axiosInstance.ts b/frontend/src/api/axiosInstance.ts new file mode 100644 index 0000000..bd70d7b --- /dev/null +++ b/frontend/src/api/axiosInstance.ts @@ -0,0 +1,37 @@ +import axios, { AxiosError } from 'axios'; + +function getCsrfToken(): string | null { + const match = document.cookie + .split('; ') + .find((row) => row.startsWith('XSRF-TOKEN=')); + return match ? decodeURIComponent(match.split('=')[1]) : null; +} + +const axiosInstance = axios.create({ + baseURL: import.meta.env.VITE_API_URL, + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + withCredentials: true, +}); + +axiosInstance.interceptors.request.use((config) => { + const token = getCsrfToken(); + if (token) { + config.headers['X-XSRF-TOKEN'] = token; + } + return config; +}); + +axiosInstance.interceptors.response.use( + (response) => response, + (error: AxiosError) => { + if (error.response?.status === 401) { + // Session expired — caller handles redirect + } + return Promise.reject(error); + } +); + +export default axiosInstance; \ No newline at end of file diff --git a/frontend/src/assets/images/canva-doctor-and-nurse-medical-consultation-MAHHY3lElT4.png b/frontend/src/assets/images/canva-doctor-and-nurse-medical-consultation-MAHHY3lElT4.png new file mode 100644 index 0000000..cb5d6ff Binary files /dev/null and b/frontend/src/assets/images/canva-doctor-and-nurse-medical-consultation-MAHHY3lElT4.png differ diff --git a/frontend/src/components/LogoutButton.tsx b/frontend/src/components/LogoutButton.tsx new file mode 100644 index 0000000..aadd343 --- /dev/null +++ b/frontend/src/components/LogoutButton.tsx @@ -0,0 +1,77 @@ +import { useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { AxiosError } from 'axios'; +import { authService } from '../services/authService'; +import { useAuth } from '../hooks/useAuth'; + +export function LogoutButton() { + const { clearAuth } = useAuth(); + const navigate = useNavigate(); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(''); + + async function handleLogout() { + setIsLoading(true); + setError(''); + + try { + await authService.logout(); + clearAuth(); + navigate('/login', { replace: true }); + } catch (err) { + const axiosError = err as AxiosError; + + if (axiosError.response?.status === 401) { + // Session already expired — clear state and redirect anyway + clearAuth(); + navigate('/login', { replace: true }); + } else { + setError('Logout failed. Please try again.'); + setIsLoading(false); + } + } + } + + return ( +
+ {error && ( + + {error} + + )} + +
+ ); +} + +const styles: Record = { + button: { + padding: '8px 16px', + backgroundColor: '#ef4444', + color: '#ffffff', + border: 'none', + borderRadius: '6px', + fontSize: '14px', + fontWeight: 600, + cursor: 'pointer', + }, + buttonDisabled: { + backgroundColor: '#fca5a5', + cursor: 'not-allowed', + }, + error: { + fontSize: '12px', + color: '#ef4444', + display: 'block', + marginBottom: '8px', + }, +}; \ No newline at end of file diff --git a/frontend/src/contexts/AuthContext.tsx b/frontend/src/contexts/AuthContext.tsx new file mode 100644 index 0000000..87f1793 --- /dev/null +++ b/frontend/src/contexts/AuthContext.tsx @@ -0,0 +1,58 @@ +import { + createContext, + useState, + useCallback, + useEffect, + type ReactNode, +} from 'react'; +import type { AuthUser, AuthState, LoginResponseData } from '../types/auth'; +import { authService } from '../services/authService'; + +interface AuthContextValue extends AuthState { + setAuth: (data: LoginResponseData) => void; + clearAuth: () => void; + isLoading: boolean; +} + +export const AuthContext = createContext(null); + +export function AuthProvider({ children }: { children: ReactNode }) { + const [user, setUser] = useState(null); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + authService + .getMe() + .then((response) => { + setUser(response.data); + }) + .catch(() => { + setUser(null); + }) + .finally(() => { + setIsLoading(false); + }); + }, []); + + const setAuth = useCallback((data: LoginResponseData) => { + setUser(data.user); + }, []); + + const clearAuth = useCallback(() => { + setUser(null); + }, []); + + return ( + + {children} + + ); +} \ No newline at end of file diff --git a/frontend/src/hooks/useAuth.ts b/frontend/src/hooks/useAuth.ts new file mode 100644 index 0000000..57304e2 --- /dev/null +++ b/frontend/src/hooks/useAuth.ts @@ -0,0 +1,10 @@ +import { useContext } from 'react'; +import { AuthContext } from '../contexts/AuthContext'; + +export function useAuth() { + const context = useContext(AuthContext); + if (!context) { + throw new Error('useAuth must be used within an AuthProvider'); + } + return context; +} \ No newline at end of file diff --git a/frontend/src/index.css b/frontend/src/index.css index 33a8327..7994525 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -1,4 +1,7 @@ @import "tailwindcss"; + +@custom-variant dark (&:where(.dark, .dark *)); + :root { --text: #6b6375; --text-h: #08060d; @@ -16,24 +19,14 @@ --heading: system-ui, 'Segoe UI', Roboto, sans-serif; --mono: ui-monospace, Consolas, monospace; - font: 18px/145% var(--sans); - letter-spacing: 0.18px; - color-scheme: light dark; - color: var(--text); - background: var(--bg); - font-synthesis: none; - text-rendering: optimizeLegibility; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - @media (max-width: 1024px) { font-size: 16px; } } -@media (prefers-color-scheme: dark) { - :root { - --text: #9ca3af; +.dark { + /* dark theme — prepared for future, not active yet */ + --text: #9ca3af; --text-h: #f3f4f6; --bg: #16171d; --border: #2e303a; @@ -44,8 +37,7 @@ --social-bg: rgba(47, 48, 58, 0.5); --shadow: rgba(0, 0, 0, 0.4) 0 10px 15px -3px, rgba(0, 0, 0, 0.25) 0 4px 6px -2px; - } - + #social .button-icon { filter: invert(1) brightness(2); } @@ -63,50 +55,64 @@ box-sizing: border-box; } -body { - margin: 0; -} +@layer base { + html { + font: 18px/145% var(--sans); + letter-spacing: 0.18px; + color-scheme: light; + color: var(--text); + background: var(--bg); + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; -h1, -h2 { - font-family: var(--heading); - font-weight: 500; - color: var(--text-h); -} - -h1 { - font-size: 56px; - letter-spacing: -1.68px; - margin: 32px 0; - @media (max-width: 1024px) { - font-size: 36px; - margin: 20px 0; } -} -h2 { - font-size: 24px; - line-height: 118%; - letter-spacing: -0.24px; - margin: 0 0 8px; - @media (max-width: 1024px) { - font-size: 20px; + body { + margin: 0; } -} -p { - margin: 0; -} - -code, -.counter { - font-family: var(--mono); - display: inline-flex; - border-radius: 4px; - color: var(--text-h); -} - -code { - font-size: 15px; - line-height: 135%; - padding: 4px 8px; - background: var(--code-bg); + + h1, + h2 { + font-family: var(--heading); + font-weight: 500; + color: var(--text-h); + } + + h1 { + font-size: 56px; + letter-spacing: -1.68px; + margin: 32px 0; + @media (max-width: 1024px) { + font-size: 36px; + margin: 20px 0; + } + } + h2 { + font-size: 24px; + line-height: 118%; + letter-spacing: -0.24px; + margin: 0 0 8px; + @media (max-width: 1024px) { + font-size: 20px; + } + } + p { + margin: 0; + } + + code, + .counter { + font-family: var(--mono); + display: inline-flex; + border-radius: 4px; + color: var(--text-h); + } + + code { + font-size: 15px; + line-height: 135%; + padding: 4px 8px; + background: var(--code-bg); + } } diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index bef5202..318fbf8 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -3,6 +3,9 @@ import { createRoot } from 'react-dom/client' import './index.css' import App from './App.tsx' +document.documentElement.classList.add('light'); +document.documentElement.dataset.theme = 'light'; + createRoot(document.getElementById('root')!).render( diff --git a/frontend/src/pages/DashboardPage.tsx b/frontend/src/pages/DashboardPage.tsx new file mode 100644 index 0000000..99e0209 --- /dev/null +++ b/frontend/src/pages/DashboardPage.tsx @@ -0,0 +1,11 @@ +import { LogoutButton } from '../components/LogoutButton'; + +export default function DashboardPage() { + return ( +
+

Dashboard

+ + +
+ ); +} \ No newline at end of file diff --git a/frontend/src/pages/LoginPage.tsx b/frontend/src/pages/LoginPage.tsx new file mode 100644 index 0000000..eea2750 --- /dev/null +++ b/frontend/src/pages/LoginPage.tsx @@ -0,0 +1,252 @@ +// frontend/src/pages/LoginPage.tsx +import { useState } from 'react'; +import { useNavigate, useLocation, Link } from 'react-router-dom'; +import { AxiosError } from 'axios'; +import { authService } from '../services/authService'; +import { useAuth } from '../hooks/useAuth'; +import type { LoginPayload, ApiValidationError, UserRole } from '../types/auth'; +import doctorsIllustration from '../assets/images/canva-doctor-and-nurse-medical-consultation-MAHHY3lElT4.png'; + +type FieldErrors = Partial>; + +const ROLE_REDIRECT: Record = { + patient: '/dashboard', + doctor: '/dashboard', + nurse: '/dashboard', + admin: '/dashboard', +}; + +export default function LoginPage() { + const navigate = useNavigate(); + const location = useLocation(); + const { setAuth } = useAuth(); + + const successMessage = (location.state as { successMessage?: string } | null) + ?.successMessage; + + const [form, setForm] = useState({ email: '', password: '' }); + const [rememberMe, setRememberMe] = useState(false); + const [showPassword, setShowPassword] = useState(false); + const [fieldErrors, setFieldErrors] = useState({}); + const [genericError, setGenericError] = useState(''); + const [isLoading, setIsLoading] = useState(false); + + function handleChange(e: React.ChangeEvent) { + const { name, value } = e.target; + setForm((prev) => ({ ...prev, [name]: value })); + setFieldErrors((prev) => ({ ...prev, [name]: undefined })); + setGenericError(''); + } + + async function handleSubmit(e: React.FormEvent) { + e.preventDefault(); + setIsLoading(true); + setFieldErrors({}); + setGenericError(''); + + try { + const response = await authService.login(form); + setAuth(response.data); + navigate(ROLE_REDIRECT[response.data.user.role], { replace: true }); + } catch (err) { + const error = err as AxiosError; + + if (error.response?.status === 422 && error.response.data.errors) { + const raw = error.response.data.errors; + const mapped: FieldErrors = {}; + for (const key of Object.keys(raw) as Array) { + mapped[key] = raw[key][0]; + } + setFieldErrors(mapped); + } else if (error.response?.status === 401) { + setGenericError('Email atau password salah.'); + } else { + setGenericError('Terjadi kesalahan. Silakan coba lagi.'); + } + } finally { + setIsLoading(false); + } + } + + return ( +
+ {/* LEFT PANEL */} +
+
+ + {successMessage && ( +
+ {successMessage} +
+ )} + + {genericError && ( +
+ {genericError} +
+ )} + +

Sign in

+ +

+ Don't have an account?{' '} + + Create now + +

+ +
+ {/* Email */} +
+ + + {fieldErrors.email && ( + + {fieldErrors.email} + + )} +
+ + {/* Password */} +
+ +
+ + +
+ {fieldErrors.password && ( + + {fieldErrors.password} + + )} +
+ + {/* Remember me + Forgot password */} +
+ + + Forgot Password? + +
+ + {/* Submit */} + +
+
+
+ + {/* RIGHT PANEL */} +
+ {/* Logo */} +
+
+ + + + + +
+ QueueNova +
+ + {/* Illustration */} +
+
+ {/* Grey cloud blob behind illustration */} +
+ Doctor and nurse illustration +
+ +

+ Welcome to QueueNova Health +

+
+ + {/* Carousel dots */} +
+ + + + + +
+
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/pages/RegisterPage.tsx b/frontend/src/pages/RegisterPage.tsx new file mode 100644 index 0000000..24c7944 --- /dev/null +++ b/frontend/src/pages/RegisterPage.tsx @@ -0,0 +1,297 @@ +// frontend/src/pages/RegisterPage.tsx +import { useState } from 'react'; +import { useNavigate, Link } from 'react-router-dom'; +import { AxiosError } from 'axios'; +import { authService } from '../services/authService'; +import type { RegisterPayload, ApiValidationError } from '../types/auth'; +import doctorsIllustration from '../assets/images/canva-doctor-and-nurse-medical-consultation-MAHHY3lElT4.png'; + +type FieldErrors = Partial>; + +export default function RegisterPage() { + const navigate = useNavigate(); + + const [form, setForm] = useState({ + name: '', + email: '', + password: '', + password_confirmation: '', + }); + + const [showPassword, setShowPassword] = useState(false); + const [showPasswordConfirmation, setShowPasswordConfirmation] = useState(false); + const [fieldErrors, setFieldErrors] = useState({}); + const [genericError, setGenericError] = useState(''); + const [isLoading, setIsLoading] = useState(false); + + function handleChange(e: React.ChangeEvent) { + const { name, value } = e.target; + setForm((prev) => ({ ...prev, [name]: value })); + setFieldErrors((prev) => ({ ...prev, [name]: undefined })); + setGenericError(''); + } + + async function handleSubmit(e: React.FormEvent) { + e.preventDefault(); + setIsLoading(true); + setFieldErrors({}); + setGenericError(''); + + try { + await authService.register(form); + navigate('/login', { + state: { successMessage: 'Registrasi berhasil. Silakan login.' }, + }); + } catch (err) { + const error = err as AxiosError; + + if (error.response?.status === 422 && error.response.data.errors) { + const raw = error.response.data.errors; + const mapped: FieldErrors = {}; + for (const key of Object.keys(raw) as Array) { + mapped[key] = raw[key][0]; + } + setFieldErrors(mapped); + } else { + setGenericError('Terjadi kesalahan. Silakan coba lagi.'); + } + } finally { + setIsLoading(false); + } + } + + return ( +
+ {/* LEFT PANEL */} +
+
+ + {genericError && ( +
+ {genericError} +
+ )} + +

Sign up

+ +

+ Already have an account?{' '} + + Sign in + +

+ +
+ + {/* Name */} +
+ + + {fieldErrors.name && ( + + {fieldErrors.name} + + )} +
+ + {/* Email */} +
+ + + {fieldErrors.email && ( + + {fieldErrors.email} + + )} +
+ + {/* Password */} +
+ +
+ + +
+ {fieldErrors.password && ( + + {fieldErrors.password} + + )} +
+ + {/* Confirm Password */} +
+ +
+ + +
+ {fieldErrors.password_confirmation && ( + + {fieldErrors.password_confirmation} + + )} +
+ + {/* Submit */} + +
+
+
+ + {/* RIGHT PANEL */} +
+ {/* Logo */} +
+
+ + + + + +
+ QueueNova +
+ + {/* Illustration */} +
+
+
+ Doctor and nurse illustration +
+ +

+ Welcome to QueueNova Health +

+
+ + {/* Carousel dots */} +
+ + + + + +
+
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/routes/ProtectedRoute.tsx b/frontend/src/routes/ProtectedRoute.tsx new file mode 100644 index 0000000..af8e2b3 --- /dev/null +++ b/frontend/src/routes/ProtectedRoute.tsx @@ -0,0 +1,16 @@ +import { Navigate, Outlet } from 'react-router-dom'; +import { useAuth } from '../hooks/useAuth'; + +export function ProtectedRoute() { + const { isAuthenticated, isLoading } = useAuth(); + + if (isLoading) { + return null; + } + + if (!isAuthenticated) { + return ; + } + + return ; +} \ No newline at end of file diff --git a/frontend/src/routes/PublicRoute.tsx b/frontend/src/routes/PublicRoute.tsx new file mode 100644 index 0000000..c14c02f --- /dev/null +++ b/frontend/src/routes/PublicRoute.tsx @@ -0,0 +1,16 @@ +import { Navigate, Outlet } from 'react-router-dom'; +import { useAuth } from '../hooks/useAuth'; + +export function PublicRoute() { + const { isAuthenticated, isLoading } = useAuth(); + + if (isLoading) { + return null; + } + + if (isAuthenticated) { + return ; + } + + return ; +} \ No newline at end of file diff --git a/frontend/src/services/authService.ts b/frontend/src/services/authService.ts new file mode 100644 index 0000000..79b888b --- /dev/null +++ b/frontend/src/services/authService.ts @@ -0,0 +1,48 @@ +import axiosInstance from '../api/axiosInstance'; +import type { + RegisterPayload, + LoginPayload, + LoginResponseData, + AuthUser, + ApiSuccessResponse, +} from '../types/auth'; + +const CSRF_COOKIE_URL = `${import.meta.env.VITE_API_BASE_URL}/sanctum/csrf-cookie`; + +async function initCsrf(): Promise { + await axiosInstance.get(CSRF_COOKIE_URL, { baseURL: '' }); +} + +export const authService = { + async register( + payload: RegisterPayload + ): Promise> { + await initCsrf(); + const response = await axiosInstance.post>( + '/auth/register', + payload + ); + return response.data; + }, + + async login( + payload: LoginPayload + ): Promise> { + await initCsrf(); + const response = await axiosInstance.post>( + '/auth/login', + payload + ); + return response.data; + }, + + async logout(): Promise { + await axiosInstance.post('/auth/logout'); + }, + + async getMe() : Promise> { + const response = + await axiosInstance.get>('/auth/me'); + return response.data; + }, +}; \ No newline at end of file diff --git a/frontend/src/tests/pages/LoginPage.test.tsx b/frontend/src/tests/pages/LoginPage.test.tsx new file mode 100644 index 0000000..af5f32f --- /dev/null +++ b/frontend/src/tests/pages/LoginPage.test.tsx @@ -0,0 +1,194 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import { MemoryRouter } from 'react-router-dom'; +import { AuthContext } from '../../contexts/AuthContext'; +import LoginPage from '../../pages/LoginPage'; +import { authService } from '../../services/authService'; +import type { AuthState, LoginResponseData } from '../../types/auth'; + +vi.mock('../../services/authService'); + +const mockNavigate = vi.fn(); +vi.mock('react-router-dom', async () => { + const actual = await vi.importActual('react-router-dom'); + return { ...actual, useNavigate: () => mockNavigate }; +}); + +const mockSetAuth = vi.fn<(data: LoginResponseData) => void>(); +const mockClearAuth = vi.fn<() => void>(); + +const mockAuthContextValue = { + user: null, + isAuthenticated: false, + isLoading: false, + setAuth: mockSetAuth, + clearAuth: mockClearAuth, +} satisfies AuthState & { + isLoading: boolean; + setAuth: (data: LoginResponseData) => void; + clearAuth: () => void; +}; + +function renderLoginPage() { + return render( + + + + + + ); +} + +const mockLoginResponse: { message: string; data: LoginResponseData } = { + message: 'Login berhasil.', + data: { + user: { + user_id: 10, + email: 'ucok@example.com', + role: 'patient', + status: 'active', + profile: { + patient_id: 5, + name: 'Ucok Sitorus', + phone: '081234567890', + bpjs_number: null, + birth_place: null, + birth_date: null, + gender: null, + }, + }, + }, +}; + +describe('LoginPage', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('renders email, password fields and submit button', () => { + renderLoginPage(); + expect(screen.getByLabelText('Email')).toBeInTheDocument(); + expect(screen.getByLabelText('Password')).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Login' })).toBeInTheDocument(); + }); + + it('calls authService.login with correct payload on submit', async () => { + vi.mocked(authService.login).mockResolvedValueOnce(mockLoginResponse); + + renderLoginPage(); + + fireEvent.change(screen.getByLabelText('Email'), { + target: { value: 'ucok@example.com' }, + }); + fireEvent.change(screen.getByLabelText('Password'), { + target: { value: 'password123' }, + }); + fireEvent.click(screen.getByRole('button', { name: 'Login' })); + + await waitFor(() => { + expect(authService.login).toHaveBeenCalledWith({ + email: 'ucok@example.com', + password: 'password123', + }); + }); + }); + + it('calls setAuth and navigates to /dashboard on successful login', async () => { + vi.mocked(authService.login).mockResolvedValueOnce(mockLoginResponse); + + renderLoginPage(); + fireEvent.change(screen.getByLabelText('Email'), { + target: { value: 'ucok@example.com' }, + }); + fireEvent.change(screen.getByLabelText('Password'), { + target: { value: 'password123' }, + }); + fireEvent.click(screen.getByRole('button', { name: 'Login' })); + + await waitFor(() => { + expect(mockSetAuth).toHaveBeenCalledWith(mockLoginResponse.data); + expect(mockNavigate).toHaveBeenCalledWith('/dashboard', { + replace: true, + }); + }); + }); + + it('displays "Email atau password salah" on 401', async () => { + const { AxiosError } = await import('axios'); + const error = new AxiosError('Unauthorized'); + error.response = { + status: 401, + data: { message: 'Email atau password salah.' }, + } as never; + + vi.mocked(authService.login).mockRejectedValueOnce(error); + + renderLoginPage(); + fireEvent.click(screen.getByRole('button', { name: 'Login' })); + + await waitFor(() => { + expect( + screen.getByText('Email atau password salah.') + ).toBeInTheDocument(); + }); + }); + + it('displays per-field errors on 422', async () => { + const { AxiosError } = await import('axios'); + const error = new AxiosError('Validation error'); + error.response = { + status: 422, + data: { + message: 'The given data was invalid.', + errors: { email: ['The email field is required.'] }, + }, + } as never; + + vi.mocked(authService.login).mockRejectedValueOnce(error); + + renderLoginPage(); + fireEvent.click(screen.getByRole('button', { name: 'Login' })); + + await waitFor(() => { + expect( + screen.getByText('The email field is required.') + ).toBeInTheDocument(); + }); + }); + + it('disables submit button while loading', async () => { + vi.mocked(authService.login).mockImplementation( + () => new Promise(() => {}) + ); + + renderLoginPage(); + fireEvent.click(screen.getByRole('button', { name: 'Login' })); + + await waitFor(() => { + expect( + screen.getByRole('button', { name: 'Logging in...' }) + ).toBeDisabled(); + }); + }); + + it('renders success message passed via location state', () => { + render( + + + + + + ); + + expect( + screen.getByText('Registrasi berhasil. Silakan login.') + ).toBeInTheDocument(); + }); +}); \ No newline at end of file diff --git a/frontend/src/tests/pages/LogoutButton.test.tsx b/frontend/src/tests/pages/LogoutButton.test.tsx new file mode 100644 index 0000000..9511eb4 --- /dev/null +++ b/frontend/src/tests/pages/LogoutButton.test.tsx @@ -0,0 +1,121 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import { MemoryRouter } from 'react-router-dom'; +import { AuthContext } from '../../contexts/AuthContext'; +import { LogoutButton } from '../../components/LogoutButton'; +import { authService } from '../../services/authService'; +import type { AuthState, LoginResponseData } from '../../types/auth'; + +vi.mock('../../services/authService'); + +const mockNavigate = vi.fn(); +vi.mock('react-router-dom', async () => { + const actual = await vi.importActual('react-router-dom'); + return { ...actual, useNavigate: () => mockNavigate }; +}); + +const mockClearAuth = vi.fn<() => void>(); + +const mockAuthContextValue = { + user: null, + isAuthenticated: false, + isLoading: false, + setAuth: vi.fn<(data: LoginResponseData) => void>(), + clearAuth: mockClearAuth, +} satisfies AuthState & { + isLoading: boolean; + setAuth: (data: LoginResponseData) => void; + clearAuth: () => void; +}; + +function renderLogoutButton() { + return render( + + + + + + ); +} + +describe('LogoutButton', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('renders logout button', () => { + renderLogoutButton(); + expect( + screen.getByRole('button', { name: 'Logout' }) + ).toBeInTheDocument(); + }); + + it('calls authService.logout, clears auth, and redirects to /login on success', async () => { + vi.mocked(authService.logout).mockResolvedValueOnce(undefined); + + renderLogoutButton(); + fireEvent.click(screen.getByRole('button', { name: 'Logout' })); + + await waitFor(() => { + expect(authService.logout).toHaveBeenCalledOnce(); + expect(mockClearAuth).toHaveBeenCalledOnce(); + expect(mockNavigate).toHaveBeenCalledWith('/login', { replace: true }); + }); + }); + + it('clears auth and redirects on 401 — session already expired', async () => { + const { AxiosError } = await import('axios'); + const error = new AxiosError('Unauthorized'); + error.response = { + status: 401, + data: { message: 'Unauthenticated.' }, + } as never; + + vi.mocked(authService.logout).mockRejectedValueOnce(error); + + renderLogoutButton(); + fireEvent.click(screen.getByRole('button', { name: 'Logout' })); + + await waitFor(() => { + expect(mockClearAuth).toHaveBeenCalledOnce(); + expect(mockNavigate).toHaveBeenCalledWith('/login', { replace: true }); + }); + }); + + it('shows generic error and does not clear auth on non-401 error', async () => { + const { AxiosError } = await import('axios'); + const error = new AxiosError('Server Error'); + error.response = { + status: 500, + data: { message: 'Server Error.' }, + } as never; + + vi.mocked(authService.logout).mockRejectedValueOnce(error); + + renderLogoutButton(); + fireEvent.click(screen.getByRole('button', { name: 'Logout' })); + + await waitFor(() => { + expect( + screen.getByText('Logout failed. Please try again.') + ).toBeInTheDocument(); + expect(mockClearAuth).not.toHaveBeenCalled(); + expect(mockNavigate).not.toHaveBeenCalled(); + }); + }); + + it('disables button while loading', async () => { + vi.mocked(authService.logout).mockImplementation( + () => new Promise(() => {}) + ); + + renderLogoutButton(); + fireEvent.click(screen.getByRole('button', { name: 'Logout' })); + + await waitFor(() => { + expect( + screen.getByRole('button', { name: 'Logging out...' }) + ).toBeDisabled(); + }); + }); +}); \ No newline at end of file diff --git a/frontend/src/tests/pages/RegisterPage.test.tsx b/frontend/src/tests/pages/RegisterPage.test.tsx new file mode 100644 index 0000000..dce6f49 --- /dev/null +++ b/frontend/src/tests/pages/RegisterPage.test.tsx @@ -0,0 +1,160 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import { MemoryRouter } from 'react-router-dom'; +import { AuthContext } from '../../contexts/AuthContext'; +import RegisterPage from '../../pages/RegisterPage'; +import { authService } from '../../services/authService'; +import type { AuthState, LoginResponseData } from '../../types/auth'; + +vi.mock('../../services/authService'); + +const mockNavigate = vi.fn(); +vi.mock('react-router-dom', async () => { + const actual = await vi.importActual('react-router-dom'); + return { ...actual, useNavigate: () => mockNavigate }; +}); + +const mockSetAuth = vi.fn<(data: LoginResponseData) => void>(); +const mockClearAuth = vi.fn<() => void>(); + +const mockAuthContextValue = { + user: null, + isAuthenticated: false, + isLoading: false, + setAuth: mockSetAuth, + clearAuth: mockClearAuth, +} satisfies AuthState & { + isLoading: boolean; + setAuth: (data: LoginResponseData) => void; + clearAuth: () => void; +}; + +function renderRegisterPage() { + return render( + + + + + + ); +} + +describe('RegisterPage', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('renders all form fields and submit button', () => { + renderRegisterPage(); + expect(screen.getByLabelText('Full Name')).toBeInTheDocument(); + expect(screen.getByLabelText('Email')).toBeInTheDocument(); + expect(screen.getByLabelText('Password')).toBeInTheDocument(); + expect(screen.getByLabelText('Confirm Password')).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Register' })).toBeInTheDocument(); + }); + + it('calls authService.register with form values on submit', async () => { + vi.mocked(authService.register).mockResolvedValueOnce({ + message: 'Registrasi berhasil.', + data: {} as never, + }); + + renderRegisterPage(); + + fireEvent.change(screen.getByLabelText('Full Name'), { + target: { value: 'Ucok Sitorus' }, + }); + fireEvent.change(screen.getByLabelText('Email'), { + target: { value: 'ucok@example.com' }, + }); + fireEvent.change(screen.getByLabelText('Password'), { + target: { value: 'password123' }, + }); + fireEvent.change(screen.getByLabelText('Confirm Password'), { + target: { value: 'password123' }, + }); + + fireEvent.click(screen.getByRole('button', { name: 'Register' })); + + await waitFor(() => { + expect(authService.register).toHaveBeenCalledWith({ + name: 'Ucok Sitorus', + email: 'ucok@example.com', + password: 'password123', + password_confirmation: 'password123', + }); + }); + }); + + it('redirects to /login with success message on 201', async () => { + vi.mocked(authService.register).mockResolvedValueOnce({ + message: 'Registrasi berhasil.', + data: {} as never, + }); + + renderRegisterPage(); + fireEvent.change(screen.getByLabelText('Email'), { + target: { value: 'ucok@example.com' }, + }); + fireEvent.click(screen.getByRole('button', { name: 'Register' })); + + await waitFor(() => { + expect(mockNavigate).toHaveBeenCalledWith('/login', { + state: { successMessage: 'Registrasi berhasil. Silakan login.' }, + }); + }); + }); + + it('displays per-field errors on 422', async () => { + const { AxiosError } = await import('axios'); + const error = new AxiosError('Validation error'); + error.response = { + status: 422, + data: { + message: 'The given data was invalid.', + errors: { email: ['The email has already been taken.'] }, + }, + } as never; + + vi.mocked(authService.register).mockRejectedValueOnce(error); + + renderRegisterPage(); + fireEvent.click(screen.getByRole('button', { name: 'Register' })); + + await waitFor(() => { + expect( + screen.getByText('The email has already been taken.') + ).toBeInTheDocument(); + }); + }); + + it('displays generic error on network failure', async () => { + vi.mocked(authService.register).mockRejectedValueOnce( + new Error('Network Error') + ); + + renderRegisterPage(); + fireEvent.click(screen.getByRole('button', { name: 'Register' })); + + await waitFor(() => { + expect( + screen.getByText('Terjadi kesalahan. Silakan coba lagi.') + ).toBeInTheDocument(); + }); + }); + + it('disables submit button while loading', async () => { + vi.mocked(authService.register).mockImplementation( + () => new Promise(() => {}) + ); + + renderRegisterPage(); + fireEvent.click(screen.getByRole('button', { name: 'Register' })); + + await waitFor(() => { + expect( + screen.getByRole('button', { name: 'Register...' }) + ).toBeDisabled(); + }); + }); +}); \ No newline at end of file diff --git a/frontend/src/types/auth.ts b/frontend/src/types/auth.ts new file mode 100644 index 0000000..031f2d4 --- /dev/null +++ b/frontend/src/types/auth.ts @@ -0,0 +1,50 @@ +export interface PatientProfile { + patient_id: number; + name: string; + phone: string; + bpjs_number: string | null; + birth_place: string | null; + birth_date: string | null; + gender: string | null; +} + +export type UserRole = 'patient' | 'doctor' | 'nurse' | 'admin'; + +export interface AuthUser { + user_id: number; + email: string; + role: UserRole; + status: string; + profile: PatientProfile; +} + +export interface AuthState { + user: AuthUser | null; + isAuthenticated: boolean; +} + +export interface RegisterPayload { + name: string; + email: string; + password: string; + password_confirmation: string; +} + +export interface LoginPayload { + email: string; + password: string; +} + +export interface LoginResponseData { + user: AuthUser; +} + +export interface ApiSuccessResponse { + message: string; + data: T; +} + +export interface ApiValidationError { + message: string; + errors: Record; +} \ No newline at end of file