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 @@ + + + + +