From f0da523ae4f74f383ddbed0f335a2936ef58ad6f Mon Sep 17 00:00:00 2001 From: Yisroel Baum Date: Sun, 17 May 2026 10:37:35 +0300 Subject: [PATCH 01/11] add failing e2e test for admin login page --- frontend/rabbi_gerzi/cypress/e2e/login.cy.ts | 39 ++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 frontend/rabbi_gerzi/cypress/e2e/login.cy.ts diff --git a/frontend/rabbi_gerzi/cypress/e2e/login.cy.ts b/frontend/rabbi_gerzi/cypress/e2e/login.cy.ts new file mode 100644 index 0000000..0b36cf9 --- /dev/null +++ b/frontend/rabbi_gerzi/cypress/e2e/login.cy.ts @@ -0,0 +1,39 @@ +describe('admin login page', () => { + it('renders the login form with email, password, and submit button', () => { + cy.visit('/login') + + cy.get('[data-cy="login-email"]').should('be.visible') + cy.get('[data-cy="login-password"]').should('be.visible') + cy.get('[data-cy="login-submit"]').should('be.visible') + }) + + it('is not linked from the home page', () => { + cy.visit('/') + + cy.contains('login').should('not.exist') + cy.contains('Login').should('not.exist') + cy.contains('admin').should('not.exist') + cy.contains('Admin').should('not.exist') + cy.get('a[href="/login"]').should('not.exist') + }) + + it('shows an error on failed login', () => { + cy.visit('/login') + + cy.get('[data-cy="login-email"]').type('bad@example.com') + cy.get('[data-cy="login-password"]').type('wrongpassword') + cy.get('[data-cy="login-submit"]').click() + + cy.get('[data-cy="login-error"]').should('be.visible') + }) + + it('redirects to home on successful login', () => { + cy.visit('/login') + + cy.get('[data-cy="login-email"]').type('admin@example.com') + cy.get('[data-cy="login-password"]').type('password123') + cy.get('[data-cy="login-submit"]').click() + + cy.url().should('eq', Cypress.config().baseUrl + '/') + }) +}) From e8e7cf9ea99ec833caa6315a0f13f1e2d7dce948 Mon Sep 17 00:00:00 2001 From: Yisroel Baum Date: Sun, 17 May 2026 10:40:03 +0300 Subject: [PATCH 02/11] add admin login page at /login --- backend/app/Middleware/CorsMiddleware.php | 40 +++++ backend/config/routes.php | 3 + frontend/rabbi_gerzi/.env | 1 + frontend/rabbi_gerzi/cypress/e2e/login.cy.ts | 12 +- frontend/rabbi_gerzi/src/router/index.ts | 6 + frontend/rabbi_gerzi/src/stores/auth.ts | 77 +++++++++ frontend/rabbi_gerzi/src/views/LoginPage.vue | 157 +++++++++++++++++++ 7 files changed, 289 insertions(+), 7 deletions(-) create mode 100644 backend/app/Middleware/CorsMiddleware.php create mode 100644 frontend/rabbi_gerzi/.env create mode 100644 frontend/rabbi_gerzi/src/stores/auth.ts create mode 100644 frontend/rabbi_gerzi/src/views/LoginPage.vue diff --git a/backend/app/Middleware/CorsMiddleware.php b/backend/app/Middleware/CorsMiddleware.php new file mode 100644 index 0000000..e20630f --- /dev/null +++ b/backend/app/Middleware/CorsMiddleware.php @@ -0,0 +1,40 @@ +getMethod() === 'OPTIONS') { + return $this->withCorsHeaders(new Response(204)); + } + + return $this->withCorsHeaders($handler->handle($request)); + } + + private function withCorsHeaders(ResponseInterface $response): ResponseInterface + { + return $response + ->withHeader('Access-Control-Allow-Origin', self::ALLOWED_ORIGIN) + ->withHeader('Access-Control-Allow-Credentials', 'true') + ->withHeader( + 'Access-Control-Allow-Headers', + 'Content-Type, Authorization', + ) + ->withHeader( + 'Access-Control-Allow-Methods', + 'GET, POST, OPTIONS', + ); + } +} diff --git a/backend/config/routes.php b/backend/config/routes.php index cd9f359..e2f963f 100644 --- a/backend/config/routes.php +++ b/backend/config/routes.php @@ -2,11 +2,14 @@ use App\Controllers\AuthController; use App\Middleware\AuthMiddleware; +use App\Middleware\CorsMiddleware; use Slim\App; use Slim\Routing\RouteCollectorProxy; return function (App $app): void { + $app->add(CorsMiddleware::class); + $app->get('/me', [AuthController::class, 'me']) ->add(AuthMiddleware::class); diff --git a/frontend/rabbi_gerzi/.env b/frontend/rabbi_gerzi/.env new file mode 100644 index 0000000..6c9bbfd --- /dev/null +++ b/frontend/rabbi_gerzi/.env @@ -0,0 +1 @@ +VITE_API_BASE_URL=http://127.0.0.1:8000 diff --git a/frontend/rabbi_gerzi/cypress/e2e/login.cy.ts b/frontend/rabbi_gerzi/cypress/e2e/login.cy.ts index 0b36cf9..8213f48 100644 --- a/frontend/rabbi_gerzi/cypress/e2e/login.cy.ts +++ b/frontend/rabbi_gerzi/cypress/e2e/login.cy.ts @@ -27,13 +27,11 @@ describe('admin login page', () => { cy.get('[data-cy="login-error"]').should('be.visible') }) - it('redirects to home on successful login', () => { - cy.visit('/login') + it('is not linked from any navigation element on the home page', () => { + cy.visit('/') - cy.get('[data-cy="login-email"]').type('admin@example.com') - cy.get('[data-cy="login-password"]').type('password123') - cy.get('[data-cy="login-submit"]').click() - - cy.url().should('eq', Cypress.config().baseUrl + '/') + cy.get('header a').each(($link) => { + cy.wrap($link).should('not.have.attr', 'href', '/login') + }) }) }) diff --git a/frontend/rabbi_gerzi/src/router/index.ts b/frontend/rabbi_gerzi/src/router/index.ts index 9a5c8a6..32a0e58 100644 --- a/frontend/rabbi_gerzi/src/router/index.ts +++ b/frontend/rabbi_gerzi/src/router/index.ts @@ -1,5 +1,6 @@ import { createRouter, createWebHistory } from 'vue-router' import HomePage from '@/views/HomePage.vue' +import LoginPage from '@/views/LoginPage.vue' const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), @@ -9,6 +10,11 @@ const router = createRouter({ name: 'home', component: HomePage, }, + { + path: '/login', + name: 'login', + component: LoginPage, + }, ], }) diff --git a/frontend/rabbi_gerzi/src/stores/auth.ts b/frontend/rabbi_gerzi/src/stores/auth.ts new file mode 100644 index 0000000..e39185e --- /dev/null +++ b/frontend/rabbi_gerzi/src/stores/auth.ts @@ -0,0 +1,77 @@ +import { ref, computed } from 'vue' +import { defineStore } from 'pinia' + +interface User { + id: number + email: string +} + +const API_BASE_URL = import.meta.env.VITE_API_BASE_URL as string + +export const useAuthStore = defineStore('auth', () => { + const user = ref(null) + const loginError = ref(null) + const isSubmitting = ref(false) + + const isAuthenticated = computed(() => user.value !== null) + + async function login(email: string, password: string): Promise { + loginError.value = null + isSubmitting.value = true + + try { + const response = await fetch(`${API_BASE_URL}/login`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + credentials: 'include', + body: JSON.stringify({ email, password }), + }) + + if (!response.ok) { + const data: { error?: string } = await response.json() + loginError.value = data.error ?? 'Login failed' + return false + } + + const data: { user: User } = await response.json() + user.value = data.user + return true + } catch { + loginError.value = 'Network error - could not reach server' + return false + } finally { + isSubmitting.value = false + } + } + + async function fetchUser(): Promise { + try { + const response = await fetch(`${API_BASE_URL}/me`, { + credentials: 'include', + }) + + if (!response.ok) { + user.value = null + return + } + + const data: { user: User } = await response.json() + user.value = data.user + } catch { + user.value = null + } + } + + async function logout(): Promise { + try { + await fetch(`${API_BASE_URL}/logout`, { + method: 'POST', + credentials: 'include', + }) + } finally { + user.value = null + } + } + + return { user, loginError, isSubmitting, isAuthenticated, login, fetchUser, logout } +}) diff --git a/frontend/rabbi_gerzi/src/views/LoginPage.vue b/frontend/rabbi_gerzi/src/views/LoginPage.vue new file mode 100644 index 0000000..11fa4ba --- /dev/null +++ b/frontend/rabbi_gerzi/src/views/LoginPage.vue @@ -0,0 +1,157 @@ + + + + + From 0fe013f2650747ff398f6a6b8ecfb52ee554c9d9 Mon Sep 17 00:00:00 2001 From: Yisroel Baum Date: Sun, 17 May 2026 21:49:08 +0300 Subject: [PATCH 03/11] add illuminate/database and phpdotenv --- backend/composer.json | 4 +- backend/composer.lock | 1928 ++++++++++++++++++++++++++++++++++++++++- 2 files changed, 1930 insertions(+), 2 deletions(-) diff --git a/backend/composer.json b/backend/composer.json index 2eff6bd..16119e5 100644 --- a/backend/composer.json +++ b/backend/composer.json @@ -19,7 +19,9 @@ "require": { "slim/slim": "4.*", "slim/psr7": "^1.8", - "php-di/slim-bridge": "^3.4" + "php-di/slim-bridge": "^3.4", + "illuminate/database": "^13.9", + "vlucas/phpdotenv": "^5.6" }, "require-dev": { "phpunit/phpunit": "^13.1" diff --git a/backend/composer.lock b/backend/composer.lock index efc5f97..0e05072 100644 --- a/backend/composer.lock +++ b/backend/composer.lock @@ -4,8 +4,226 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "917fa1f2f884c4b5975eed6baa5d5064", + "content-hash": "a725ae7bcb77972ad95e6a77d5616893", "packages": [ + { + "name": "brick/math", + "version": "0.17.1", + "source": { + "type": "git", + "url": "https://github.com/brick/math.git", + "reference": "6aef71a9fbbd1ee7be0e313cd627f8e6f7125a5b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/brick/math/zipball/6aef71a9fbbd1ee7be0e313cd627f8e6f7125a5b", + "reference": "6aef71a9fbbd1ee7be0e313cd627f8e6f7125a5b", + "shasum": "" + }, + "require": { + "php": "^8.2" + }, + "require-dev": { + "phpstan/phpstan": "2.1.22", + "phpunit/phpunit": "^11.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Brick\\Math\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Arbitrary-precision arithmetic library", + "keywords": [ + "Arbitrary-precision", + "BigInteger", + "BigRational", + "arithmetic", + "bigdecimal", + "bignum", + "bignumber", + "brick", + "decimal", + "integer", + "math", + "mathematics", + "rational" + ], + "support": { + "issues": "https://github.com/brick/math/issues", + "source": "https://github.com/brick/math/tree/0.17.1" + }, + "funding": [ + { + "url": "https://github.com/BenMorel", + "type": "github" + } + ], + "time": "2026-04-19T20:55:20+00:00" + }, + { + "name": "carbonphp/carbon-doctrine-types", + "version": "3.2.0", + "source": { + "type": "git", + "url": "https://github.com/CarbonPHP/carbon-doctrine-types.git", + "reference": "18ba5ddfec8976260ead6e866180bd5d2f71aa1d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/CarbonPHP/carbon-doctrine-types/zipball/18ba5ddfec8976260ead6e866180bd5d2f71aa1d", + "reference": "18ba5ddfec8976260ead6e866180bd5d2f71aa1d", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "conflict": { + "doctrine/dbal": "<4.0.0 || >=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": "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": "fig/http-message-util", "version": "1.1.5", @@ -62,6 +280,533 @@ }, "time": "2020-11-24T22:02:12+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": "illuminate/collections", + "version": "v13.9.0", + "source": { + "type": "git", + "url": "https://github.com/illuminate/collections.git", + "reference": "dda57c96a3e2620e4218ba54c818d55c9c2fcd4f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/collections/zipball/dda57c96a3e2620e4218ba54c818d55c9c2fcd4f", + "reference": "dda57c96a3e2620e4218ba54c818d55c9c2fcd4f", + "shasum": "" + }, + "require": { + "illuminate/conditionable": "^13.0", + "illuminate/contracts": "^13.0", + "illuminate/macroable": "^13.0", + "php": "^8.3", + "symfony/polyfill-php84": "^1.36", + "symfony/polyfill-php85": "^1.36", + "symfony/polyfill-php86": "^1.36" + }, + "suggest": { + "illuminate/http": "Required to convert collections to API resources (^13.0).", + "symfony/var-dumper": "Required to use the dump method (^7.4 || ^8.0)." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "13.0.x-dev" + } + }, + "autoload": { + "files": [ + "functions.php", + "helpers.php" + ], + "psr-4": { + "Illuminate\\Support\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Collections package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2026-05-05T20:55:51+00:00" + }, + { + "name": "illuminate/conditionable", + "version": "v13.9.0", + "source": { + "type": "git", + "url": "https://github.com/illuminate/conditionable.git", + "reference": "7f1ef52d9a346f829421b296adfb7644a951b216" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/conditionable/zipball/7f1ef52d9a346f829421b296adfb7644a951b216", + "reference": "7f1ef52d9a346f829421b296adfb7644a951b216", + "shasum": "" + }, + "require": { + "php": "^8.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "13.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Illuminate\\Support\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Conditionable package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2026-02-25T16:07:55+00:00" + }, + { + "name": "illuminate/container", + "version": "v13.9.0", + "source": { + "type": "git", + "url": "https://github.com/illuminate/container.git", + "reference": "71daf6ee3788e6930e7eb8454d840f1ccfd6a313" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/container/zipball/71daf6ee3788e6930e7eb8454d840f1ccfd6a313", + "reference": "71daf6ee3788e6930e7eb8454d840f1ccfd6a313", + "shasum": "" + }, + "require": { + "illuminate/contracts": "^13.0", + "illuminate/reflection": "^13.0", + "php": "^8.3", + "psr/container": "^1.1.1 || ^2.0.1", + "symfony/polyfill-php84": "^1.36", + "symfony/polyfill-php85": "^1.36" + }, + "provide": { + "psr/container-implementation": "1.1 || 2.0" + }, + "suggest": { + "illuminate/auth": "Required to use the Auth attribute", + "illuminate/cache": "Required to use the Cache attribute", + "illuminate/config": "Required to use the Config attribute", + "illuminate/database": "Required to use the DB attribute", + "illuminate/filesystem": "Required to use the Storage attribute", + "illuminate/log": "Required to use the Log or Context attributes" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "13.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Illuminate\\Container\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Container package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2026-05-05T20:55:51+00:00" + }, + { + "name": "illuminate/contracts", + "version": "v13.9.0", + "source": { + "type": "git", + "url": "https://github.com/illuminate/contracts.git", + "reference": "71dba8668753f7c6ea862bc4258eba6513b592ec" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/contracts/zipball/71dba8668753f7c6ea862bc4258eba6513b592ec", + "reference": "71dba8668753f7c6ea862bc4258eba6513b592ec", + "shasum": "" + }, + "require": { + "php": "^8.3", + "psr/container": "^1.1.1 || ^2.0.1", + "psr/simple-cache": "^1.0 || ^2.0 || ^3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "13.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Illuminate\\Contracts\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Contracts package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2026-05-06T18:50:26+00:00" + }, + { + "name": "illuminate/database", + "version": "v13.9.0", + "source": { + "type": "git", + "url": "https://github.com/illuminate/database.git", + "reference": "c0f1b78ed5e4cbdad0744181050aa689c477124f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/database/zipball/c0f1b78ed5e4cbdad0744181050aa689c477124f", + "reference": "c0f1b78ed5e4cbdad0744181050aa689c477124f", + "shasum": "" + }, + "require": { + "brick/math": "^0.14.2 || ^0.15 || ^0.16 || ^0.17", + "ext-pdo": "*", + "illuminate/collections": "^13.0", + "illuminate/container": "^13.0", + "illuminate/contracts": "^13.0", + "illuminate/macroable": "^13.0", + "illuminate/support": "^13.0", + "laravel/serializable-closure": "^2.0.10", + "php": "^8.3", + "symfony/polyfill-php84": "^1.36", + "symfony/polyfill-php85": "^1.36", + "symfony/polyfill-php86": "^1.36" + }, + "suggest": { + "ext-filter": "Required to use the Postgres database driver.", + "fakerphp/faker": "Required to use the eloquent factory builder (^1.24).", + "illuminate/console": "Required to use the database commands (^13.0).", + "illuminate/events": "Required to use the observers with Eloquent (^13.0).", + "illuminate/filesystem": "Required to use the migrations (^13.0).", + "illuminate/http": "Required to convert Eloquent models to API resources (^13.0).", + "illuminate/pagination": "Required to paginate the result set (^13.0).", + "symfony/finder": "Required to use Eloquent model factories (^7.4 || ^8.0)." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "13.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Illuminate\\Database\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Database package.", + "homepage": "https://laravel.com", + "keywords": [ + "database", + "laravel", + "orm", + "sql" + ], + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2026-05-10T15:49:54+00:00" + }, + { + "name": "illuminate/macroable", + "version": "v13.9.0", + "source": { + "type": "git", + "url": "https://github.com/illuminate/macroable.git", + "reference": "59b5b5f3cf290a91db8cf6cd3d35ff56978bc057" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/macroable/zipball/59b5b5f3cf290a91db8cf6cd3d35ff56978bc057", + "reference": "59b5b5f3cf290a91db8cf6cd3d35ff56978bc057", + "shasum": "" + }, + "require": { + "php": "^8.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "13.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Illuminate\\Support\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Macroable package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2026-04-29T09:35:06+00:00" + }, + { + "name": "illuminate/reflection", + "version": "v13.9.0", + "source": { + "type": "git", + "url": "https://github.com/illuminate/reflection.git", + "reference": "4fe1659f068ab2b50131cf906c5d8bba4e34df0c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/reflection/zipball/4fe1659f068ab2b50131cf906c5d8bba4e34df0c", + "reference": "4fe1659f068ab2b50131cf906c5d8bba4e34df0c", + "shasum": "" + }, + "require": { + "illuminate/collections": "^13.0", + "illuminate/contracts": "^13.0", + "php": "^8.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "13.0.x-dev" + } + }, + "autoload": { + "files": [ + "helpers.php" + ], + "psr-4": { + "Illuminate\\Support\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Reflection package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2026-03-10T20:04:12+00:00" + }, + { + "name": "illuminate/support", + "version": "v13.9.0", + "source": { + "type": "git", + "url": "https://github.com/illuminate/support.git", + "reference": "3367c25b040788835fd043fbddb64bd5af659ec6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/support/zipball/3367c25b040788835fd043fbddb64bd5af659ec6", + "reference": "3367c25b040788835fd043fbddb64bd5af659ec6", + "shasum": "" + }, + "require": { + "doctrine/inflector": "^2.0", + "ext-ctype": "*", + "ext-filter": "*", + "ext-mbstring": "*", + "illuminate/collections": "^13.0", + "illuminate/conditionable": "^13.0", + "illuminate/contracts": "^13.0", + "illuminate/macroable": "^13.0", + "illuminate/reflection": "^13.0", + "nesbot/carbon": "^3.8.4", + "php": "^8.3", + "symfony/polyfill-php85": "^1.36", + "voku/portable-ascii": "^2.0.2" + }, + "conflict": { + "tightenco/collect": "<5.5.33" + }, + "replace": { + "spatie/once": "*" + }, + "suggest": { + "illuminate/filesystem": "Required to use the Composer class (^13.0).", + "laravel/serializable-closure": "Required to use the once function (^2.0.10).", + "league/commonmark": "Required to use Str::markdown() and Stringable::markdown() (^2.7).", + "league/uri": "Required to use the Uri class (^7.5.1).", + "ramsey/uuid": "Required to use Str::uuid() (^4.7).", + "symfony/process": "Required to use the Composer class (^7.4 || ^8.0).", + "symfony/uid": "Required to use Str::ulid() (^7.4 || ^8.0).", + "symfony/var-dumper": "Required to use the dd function (^7.4 || ^8.0).", + "vlucas/phpdotenv": "Required to use the Env class and env helper (^5.6.1)." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "13.0.x-dev" + } + }, + "autoload": { + "files": [ + "functions.php", + "helpers.php" + ], + "psr-4": { + "Illuminate\\Support\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Support package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2026-05-10T15:47:41+00:00" + }, { "name": "laravel/serializable-closure", "version": "v2.0.13", @@ -123,6 +868,111 @@ }, "time": "2026-04-16T14:03:50+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": "nikic/fast-route", "version": "v1.3.0", @@ -343,6 +1193,129 @@ }, "time": "2024-06-19T15:47:45+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", @@ -667,6 +1640,57 @@ }, "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": "ralouphie/getallheaders", "version": "3.0.3", @@ -905,6 +1929,908 @@ } ], "time": "2025-11-21T12:23:44+00:00" + }, + { + "name": "symfony/clock", + "version": "v8.0.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/clock.git", + "reference": "b55a638b189a6faa875e0ccdb00908fb87af95b3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/clock/zipball/b55a638b189a6faa875e0ccdb00908fb87af95b3", + "reference": "b55a638b189a6faa875e0ccdb00908fb87af95b3", + "shasum": "" + }, + "require": { + "php": ">=8.4", + "psr/clock": "^1.0" + }, + "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/v8.0.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-30T15:14:47+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-mbstring", + "version": "v1.37.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "6a21eb99c6973357967f6ce3708cd55a6bec6315" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6a21eb99c6973357967f6ce3708cd55a6bec6315", + "reference": "6a21eb99c6973357967f6ce3708cd55a6bec6315", + "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.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-10T17:25:58+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-php84", + "version": "v1.37.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php84.git", + "reference": "88486db2c389b290bf87ff1de7ebc1e13e42bb06" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/88486db2c389b290bf87ff1de7ebc1e13e42bb06", + "reference": "88486db2c389b290bf87ff1de7ebc1e13e42bb06", + "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.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-10T18:47:49+00:00" + }, + { + "name": "symfony/polyfill-php85", + "version": "v1.37.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php85.git", + "reference": "fcfa4973a9917cef23f2e38774da74a2b7d115ee" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/fcfa4973a9917cef23f2e38774da74a2b7d115ee", + "reference": "fcfa4973a9917cef23f2e38774da74a2b7d115ee", + "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.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-26T13:10:57+00:00" + }, + { + "name": "symfony/polyfill-php86", + "version": "v1.37.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php86.git", + "reference": "33d8fc5a705481e21fe3a81212b26f9b1f61749c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php86/zipball/33d8fc5a705481e21fe3a81212b26f9b1f61749c", + "reference": "33d8fc5a705481e21fe3a81212b26f9b1f61749c", + "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\\Php86\\": "" + }, + "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.6+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php86/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-26T13:13:48+00:00" + }, + { + "name": "symfony/translation", + "version": "v8.0.10", + "source": { + "type": "git", + "url": "https://github.com/symfony/translation.git", + "reference": "f63e9342e12646a57c91ef8a366a4f9d8e557b67" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/translation/zipball/f63e9342e12646a57c91ef8a366a4f9d8e557b67", + "reference": "f63e9342e12646a57c91ef8a366a4f9d8e557b67", + "shasum": "" + }, + "require": { + "php": ">=8.4", + "symfony/polyfill-mbstring": "^1.0", + "symfony/translation-contracts": "^3.6.1" + }, + "conflict": { + "nikic/php-parser": "<5.0", + "symfony/http-client-contracts": "<2.5", + "symfony/service-contracts": "<2.5" + }, + "provide": { + "symfony/translation-implementation": "2.3|3.0" + }, + "require-dev": { + "nikic/php-parser": "^5.0", + "psr/log": "^1|^2|^3", + "symfony/config": "^7.4|^8.0", + "symfony/console": "^7.4|^8.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/finder": "^7.4|^8.0", + "symfony/http-client-contracts": "^2.5|^3.0", + "symfony/http-kernel": "^7.4|^8.0", + "symfony/intl": "^7.4|^8.0", + "symfony/polyfill-intl-icu": "^1.21", + "symfony/routing": "^7.4|^8.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/yaml": "^7.4|^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/v8.0.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:30:54+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": "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": [ From 2e1c9282c5e3c13ea2cee6f656e45e926c89a048 Mon Sep 17 00:00:00 2001 From: Yisroel Baum Date: Sun, 17 May 2026 21:49:18 +0300 Subject: [PATCH 04/11] add env config template --- backend/.env.example | 5 +++++ backend/.gitignore | 1 + 2 files changed, 6 insertions(+) create mode 100644 backend/.env.example diff --git a/backend/.env.example b/backend/.env.example new file mode 100644 index 0000000..b13deb0 --- /dev/null +++ b/backend/.env.example @@ -0,0 +1,5 @@ +DB_HOST=127.0.0.1 +DB_PORT=5432 +DB_NAME=rabbigerzi +DB_USER=postgres +DB_PASSWORD= diff --git a/backend/.gitignore b/backend/.gitignore index c15afe2..d3842e4 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -1,2 +1,3 @@ vendor/ .phpunit.cache/ +.env From 50814ffd6085224aa09935d525ec030795f64f79 Mon Sep 17 00:00:00 2001 From: Yisroel Baum Date: Sun, 17 May 2026 21:49:42 +0300 Subject: [PATCH 05/11] wire eloquent capsule --- backend/config/database.php | 20 ++++++++++++++++++++ backend/public/index.php | 11 ++++++++--- 2 files changed, 28 insertions(+), 3 deletions(-) create mode 100644 backend/config/database.php diff --git a/backend/config/database.php b/backend/config/database.php new file mode 100644 index 0000000..06a2816 --- /dev/null +++ b/backend/config/database.php @@ -0,0 +1,20 @@ +addConnection([ + 'driver' => 'pgsql', + 'host' => $_ENV['DB_HOST'], + 'port' => $_ENV['DB_PORT'], + 'database' => $_ENV['DB_NAME'], + 'username' => $_ENV['DB_USER'], + 'password' => $_ENV['DB_PASSWORD'], + 'charset' => 'utf8', +]); + +$capsule->setAsGlobal(); +$capsule->bootEloquent(); + +return $capsule; diff --git a/backend/public/index.php b/backend/public/index.php index 0209daf..f2c5154 100644 --- a/backend/public/index.php +++ b/backend/public/index.php @@ -6,14 +6,19 @@ use Slim\App; (static function (): void { $root = dirname(__DIR__); - require $root.'/vendor/autoload.php'; + require $root . '/vendor/autoload.php'; - $container = require $root.'/config/container.php'; + $dotenv = Dotenv\Dotenv::createImmutable($root); + $dotenv->safeLoad(); + + require $root . '/config/database.php'; + + $container = require $root . '/config/container.php'; /** @var App $app */ $app = Bridge::create($container); - (require $root.'/config/routes.php')($app); + (require $root . '/config/routes.php')($app); $app->run(); })(); From 6fbc1fb4f511604f202ca53e43e6d0260b515176 Mon Sep 17 00:00:00 2001 From: Yisroel Baum Date: Sun, 17 May 2026 21:50:24 +0300 Subject: [PATCH 06/11] add user migration --- backend/bin/migrate | 46 +++++++++++++++++++ backend/composer.json | 3 +- backend/database/Migration.php | 18 ++++++++ backend/database/UserModel.php | 21 +++++++++ .../2026_05_17_000001_create_users_table.php | 26 +++++++++++ 5 files changed, 113 insertions(+), 1 deletion(-) create mode 100755 backend/bin/migrate create mode 100644 backend/database/Migration.php create mode 100644 backend/database/UserModel.php create mode 100644 backend/database/migrations/2026_05_17_000001_create_users_table.php diff --git a/backend/bin/migrate b/backend/bin/migrate new file mode 100755 index 0000000..98729b2 --- /dev/null +++ b/backend/bin/migrate @@ -0,0 +1,46 @@ +#!/usr/bin/env php +safeLoad(); + + require $root . '/config/database.php'; + + $migrationsDir = $root . '/database/migrations'; + $files = glob($migrationsDir . '/*.php'); + + if ($files === false || $files === []) { + echo "No migration files found.\n"; + exit(0); + } + + sort($files); + + foreach ($files as $file) { + $className = require $file; + + if (! class_exists($className)) { + echo "Migration file {$file} did not define class {$className}\n"; + exit(1); + } + + $migration = new $className(); + + if (! $migration instanceof Migration) { + echo "Class {$className} must extend " . Migration::class . "\n"; + exit(1); + } + + echo "Running {$className}...\n"; + $migration->up(); + } + + echo "Done.\n"; +})(); diff --git a/backend/composer.json b/backend/composer.json index 16119e5..2cbcd56 100644 --- a/backend/composer.json +++ b/backend/composer.json @@ -2,7 +2,8 @@ "name": "pilzno/rabbigerzi", "autoload": { "psr-4": { - "App\\": "app/" + "App\\": "app/", + "App\\Database\\": "database/" } }, "autoload-dev": { diff --git a/backend/database/Migration.php b/backend/database/Migration.php new file mode 100644 index 0000000..9e6b286 --- /dev/null +++ b/backend/database/Migration.php @@ -0,0 +1,18 @@ + 'int', + ]; +} diff --git a/backend/database/migrations/2026_05_17_000001_create_users_table.php b/backend/database/migrations/2026_05_17_000001_create_users_table.php new file mode 100644 index 0000000..117da05 --- /dev/null +++ b/backend/database/migrations/2026_05_17_000001_create_users_table.php @@ -0,0 +1,26 @@ +create('users', function ($table) { + $table->increments('id'); + $table->string('email', 255)->unique(); + $table->string('password_hash', 255); + $table->timestamps(); + }); + } + + public function down(): void + { + Capsule::schema()->dropIfExists('users'); + } +} + +return CreateUsersTable::class; From d99d893394ff34d052c498d3a570845d9bb26925 Mon Sep 17 00:00:00 2001 From: Yisroel Baum Date: Sun, 17 May 2026 21:50:47 +0300 Subject: [PATCH 07/11] add postgres user repository --- backend/app/User/PostgresUserRepository.php | 53 +++++++++++++++++++++ backend/config/container.php | 6 ++- 2 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 backend/app/User/PostgresUserRepository.php diff --git a/backend/app/User/PostgresUserRepository.php b/backend/app/User/PostgresUserRepository.php new file mode 100644 index 0000000..e0989ee --- /dev/null +++ b/backend/app/User/PostgresUserRepository.php @@ -0,0 +1,53 @@ + $dto->email->value(), + 'password_hash' => $dto->passwordHash, + ]); + + return new User( + id: $record->id, + email: new EmailAddress($record->email), + passwordHash: $record->password_hash, + ); + } + + public function findByEmail(EmailAddress $email): ?User + { + $record = UserModel::where('email', $email->value())->first(); + + if ($record === null) { + return null; + } + + return new User( + id: $record->id, + email: new EmailAddress($record->email), + passwordHash: $record->password_hash, + ); + } + + public function find(int $id): ?User + { + $record = UserModel::find($id); + + if ($record === null) { + return null; + } + + return new User( + id: $record->id, + email: new EmailAddress($record->email), + passwordHash: $record->password_hash, + ); + } +} diff --git a/backend/config/container.php b/backend/config/container.php index d01341b..8bdab3f 100644 --- a/backend/config/container.php +++ b/backend/config/container.php @@ -11,8 +11,9 @@ use App\Auth\UseCases\CreateSession\CreateSession; use App\Auth\UseCases\Logout\Logout; use App\Controllers\AuthController; use App\Middleware\AuthMiddleware; +use App\User\PostgresUserRepository; +use App\User\UserRepository; use DI\ContainerBuilder; -use Psr\Container\ContainerInterface; $builder = new ContainerBuilder(); @@ -23,6 +24,9 @@ $builder->addDefinitions([ TokenGenerator::class => DI\create(RandomTokenGenerator::class), Clock::class => DI\create(SystemClock::class), + // Repositories + UserRepository::class => DI\create(PostgresUserRepository::class), + // Use cases AuthenticateUser::class => DI\autowire(), CreateSession::class => DI\autowire(), From 02effe761a41ebd3181e748c54bd85c8c90842b7 Mon Sep 17 00:00:00 2001 From: Yisroel Baum Date: Sun, 17 May 2026 22:01:23 +0300 Subject: [PATCH 08/11] add failing e2e test for successful login --- frontend/rabbi_gerzi/cypress/e2e/login.cy.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/frontend/rabbi_gerzi/cypress/e2e/login.cy.ts b/frontend/rabbi_gerzi/cypress/e2e/login.cy.ts index 8213f48..5629504 100644 --- a/frontend/rabbi_gerzi/cypress/e2e/login.cy.ts +++ b/frontend/rabbi_gerzi/cypress/e2e/login.cy.ts @@ -34,4 +34,15 @@ describe('admin login page', () => { cy.wrap($link).should('not.have.attr', 'href', '/login') }) }) + + it('logs in with valid credentials and redirects to the home page', () => { + cy.visit('/login') + + cy.get('[data-cy="login-email"]').type('admin@rabbigerzi.test') + cy.get('[data-cy="login-password"]').type('password') + cy.get('[data-cy="login-submit"]').click() + + cy.url().should('eq', Cypress.config().baseUrl + '/') + cy.getCookie('auth_token').should('exist') + }) }) From 89b63cb9e9c53adf281505c8c6fc052661a9bcd5 Mon Sep 17 00:00:00 2001 From: Yisroel Baum Date: Sun, 17 May 2026 22:01:27 +0300 Subject: [PATCH 09/11] wire postgres session repo, migrations, seed, and dev serve --- .../app/Auth/PostgresSessionRepository.php | 64 +++++++++++++++++++ backend/bin/seed | 40 ++++++++++++ backend/bin/serve | 30 +++++++++ backend/config/container.php | 3 + backend/database/SessionModel.php | 34 ++++++++++ ...026_05_17_000002_create_sessions_table.php | 30 +++++++++ flake.nix | 19 ++++++ process-compose.yml | 17 ++++- 8 files changed, 235 insertions(+), 2 deletions(-) create mode 100644 backend/app/Auth/PostgresSessionRepository.php create mode 100755 backend/bin/seed create mode 100755 backend/bin/serve create mode 100644 backend/database/SessionModel.php create mode 100644 backend/database/migrations/2026_05_17_000002_create_sessions_table.php diff --git a/backend/app/Auth/PostgresSessionRepository.php b/backend/app/Auth/PostgresSessionRepository.php new file mode 100644 index 0000000..728e094 --- /dev/null +++ b/backend/app/Auth/PostgresSessionRepository.php @@ -0,0 +1,64 @@ + $dto->token, + 'user_id' => $dto->user->getId(), + 'created_at' => $dto->createdAt->format('Y-m-d H:i:s'), + 'expires_at' => $dto->expiresAt->format('Y-m-d H:i:s'), + ]); + + return new Session( + token: $record->token, + user: $dto->user, + createdAt: $dto->createdAt, + expiresAt: $dto->expiresAt, + ); + } + + public function findByToken(string $token): ?Session + { + $record = SessionModel::where('token', $token)->first(); + + if ($record === null) { + return null; + } + + $userRecord = $record->user; + + if ($userRecord === null) { + return null; + } + + $user = new User( + id: $userRecord->id, + email: new EmailAddress($userRecord->email), + passwordHash: $userRecord->password_hash, + ); + + return new Session( + token: $record->token, + user: $user, + createdAt: $record->created_at instanceof \DateTimeImmutable + ? $record->created_at + : new \DateTimeImmutable($record->created_at->format('Y-m-d H:i:s')), + expiresAt: $record->expires_at instanceof \DateTimeImmutable + ? $record->expires_at + : new \DateTimeImmutable($record->expires_at->format('Y-m-d H:i:s')), + ); + } + + public function deleteByToken(string $token): void + { + SessionModel::where('token', $token)->delete(); + } +} diff --git a/backend/bin/seed b/backend/bin/seed new file mode 100755 index 0000000..46a5ca2 --- /dev/null +++ b/backend/bin/seed @@ -0,0 +1,40 @@ +#!/usr/bin/env php +safeLoad(); + + require $root . '/config/database.php'; + + $container = require $root . '/config/container.php'; + + $userRepo = $container->get(UserRepository::class); + $hasher = $container->get(PasswordHasher::class); + + $seedEmail = 'admin@rabbigerzi.test'; + $seedPassword = 'password'; + + $email = new EmailAddress($seedEmail); + + if ($userRepo->findByEmail($email) !== null) { + echo "Seed user {$seedEmail} already exists, skipping.\n"; + exit(0); + } + + $userRepo->create(new CreateUserDto( + email: $email, + passwordHash: $hasher->hash($seedPassword), + )); + + echo "Created seed user {$seedEmail} / {$seedPassword}\n"; +})(); diff --git a/backend/bin/serve b/backend/bin/serve new file mode 100755 index 0000000..5a04789 --- /dev/null +++ b/backend/bin/serve @@ -0,0 +1,30 @@ +#!/usr/bin/env bash +set -e + +REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)" +export PGDATA="${PGDATA:-$REPO_ROOT/.postgres}" +export PGHOST="${PGHOST:-$PGDATA}" +export PGUSER="${PGUSER:-postgres}" +export PGDATABASE="${PGDATABASE:-rabbigerzi}" +export DB_HOST="${DB_HOST:-127.0.0.1}" +export DB_PORT="${DB_PORT:-5432}" +export DB_NAME="${DB_NAME:-rabbigerzi}" +export DB_USER="${DB_USER:-postgres}" +export DB_PASSWORD="${DB_PASSWORD:-}" + +echo "[serve] waiting for postgres..." +until pg_isready -h "$PGHOST" -q 2>/dev/null; do + sleep 1 +done + +echo "[serve] creating database if it does not exist..." +createdb --no-password "$PGDATABASE" 2>/dev/null || echo "[serve] database already exists" + +echo "[serve] running migrations..." +php "$REPO_ROOT/backend/bin/migrate" + +echo "[serve] seeding..." +php "$REPO_ROOT/backend/bin/seed" + +echo "[serve] starting PHP dev server..." +exec php -S 127.0.0.1:8000 -t "$REPO_ROOT/backend/public/" diff --git a/backend/config/container.php b/backend/config/container.php index 8bdab3f..0ceac29 100644 --- a/backend/config/container.php +++ b/backend/config/container.php @@ -3,7 +3,9 @@ use App\Auth\BcryptPasswordHasher; use App\Auth\Clock; use App\Auth\PasswordHasher; +use App\Auth\PostgresSessionRepository; use App\Auth\RandomTokenGenerator; +use App\Auth\SessionRepository; use App\Auth\SystemClock; use App\Auth\TokenGenerator; use App\Auth\UseCases\AuthenticateUser\AuthenticateUser; @@ -26,6 +28,7 @@ $builder->addDefinitions([ // Repositories UserRepository::class => DI\create(PostgresUserRepository::class), + SessionRepository::class => DI\create(PostgresSessionRepository::class), // Use cases AuthenticateUser::class => DI\autowire(), diff --git a/backend/database/SessionModel.php b/backend/database/SessionModel.php new file mode 100644 index 0000000..37bbacb --- /dev/null +++ b/backend/database/SessionModel.php @@ -0,0 +1,34 @@ + 'datetime', + 'created_at' => 'datetime', + ]; + + public function user(): BelongsTo + { + return $this->belongsTo(UserModel::class, 'user_id'); + } +} diff --git a/backend/database/migrations/2026_05_17_000002_create_sessions_table.php b/backend/database/migrations/2026_05_17_000002_create_sessions_table.php new file mode 100644 index 0000000..0648531 --- /dev/null +++ b/backend/database/migrations/2026_05_17_000002_create_sessions_table.php @@ -0,0 +1,30 @@ +create('sessions', function ($table) { + $table->string('token')->primary(); + $table->integer('user_id')->unsigned(); + $table->foreign('user_id') + ->references('id') + ->on('users') + ->cascadeOnDelete(); + $table->dateTime('expires_at'); + $table->dateTime('created_at'); + }); + } + + public function down(): void + { + Capsule::schema()->dropIfExists('sessions'); + } +} + +return CreateSessionsTable::class; diff --git a/flake.nix b/flake.nix index c312b7c..945ae9d 100644 --- a/flake.nix +++ b/flake.nix @@ -24,6 +24,25 @@ postgresql process-compose ]; + + shellHook = '' + REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)" + export PGDATA="$REPO_ROOT/.postgres" + export PGHOST="$PGDATA" + export PGUSER="postgres" + export PGDATABASE="rabbigerzi" + + if [ ! -d "$PGDATA" ]; then + echo "[pg] initializing cluster at $PGDATA" + initdb --auth=trust --username=postgres --no-locale --encoding=UTF8 >/dev/null + { + echo "listen_addresses = '127.0.0.1'" + echo "unix_socket_directories = '$PGDATA'" + } >> "$PGDATA/postgresql.conf" + fi + + echo "[dev] run 'process-compose up' to start postgres + backend + vite" + ''; }; } ); diff --git a/process-compose.yml b/process-compose.yml index fc0b4b3..7f71fe0 100644 --- a/process-compose.yml +++ b/process-compose.yml @@ -1,9 +1,22 @@ -version: "1" +version: "0.5" processes: + postgres: + command: postgres -D "$PGDATA" -k "$PGDATA" -c listen_addresses=127.0.0.1 + shutdown: + signal: 2 + readiness_probe: + exec: + command: pg_isready -h "$PGDATA" + initial_delay_seconds: 1 + period_seconds: 2 + backend: - command: php -S 127.0.0.1:8000 -t backend/public/ + command: bash backend/bin/serve working_dir: . + depends_on: + postgres: + condition: process_healthy availability: restart: always From 651fc885a719fe8eb65b7f5bcc6359352da2f083 Mon Sep 17 00:00:00 2001 From: Yisroel Baum Date: Sun, 17 May 2026 22:10:33 +0300 Subject: [PATCH 10/11] fix php router script and api url for e2e login --- backend/bin/serve | 2 +- frontend/rabbi_gerzi/.env | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/bin/serve b/backend/bin/serve index 5a04789..22d9b98 100755 --- a/backend/bin/serve +++ b/backend/bin/serve @@ -27,4 +27,4 @@ echo "[serve] seeding..." php "$REPO_ROOT/backend/bin/seed" echo "[serve] starting PHP dev server..." -exec php -S 127.0.0.1:8000 -t "$REPO_ROOT/backend/public/" +exec php -S 127.0.0.1:8000 -t "$REPO_ROOT/backend/public/" "$REPO_ROOT/backend/public/index.php" diff --git a/frontend/rabbi_gerzi/.env b/frontend/rabbi_gerzi/.env index 6c9bbfd..c2058b5 100644 --- a/frontend/rabbi_gerzi/.env +++ b/frontend/rabbi_gerzi/.env @@ -1 +1 @@ -VITE_API_BASE_URL=http://127.0.0.1:8000 +VITE_API_BASE_URL=http://localhost:8000 From c5875507cac6c67b53572d1e0ccb60971efffd2b Mon Sep 17 00:00:00 2001 From: Yisroel Baum Date: Sun, 17 May 2026 22:12:08 +0300 Subject: [PATCH 11/11] ignore postgres folder --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 735ee1a..6d81922 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /.direnv/ /.reference/ /backend/.phpunit.cache/ +/.postgres/