diff --git a/ai/shared.md b/ai/shared.md index 00a1273..97f3e33 100644 --- a/ai/shared.md +++ b/ai/shared.md @@ -39,39 +39,14 @@ Use judgment for changes that cannot meaningfully be test-driven, such as documentation-only edits or declarative environment configuration. Validate those changes with the most relevant parser, formatter, dry run, or check. -## Approval discipline - -- Treat dependency installation, tests, static analysis, formatting, linting, - type checking, and builds as routine project operations. Run them without - asking for advance approval when they stay within the requested scope and - sandbox boundaries. -- If an essential operation is blocked by sandbox or network policy, batch the - necessary provisioning or validation into one narrowly scoped approval - request. Prefer a reusable command prefix when it can be safely limited to - the required tool and operation. -- Do not request repeated approvals for commands that fit a previously - approved scope. -- Do not prefix routine commands with per-command cache environment overrides. - Configure cache locations once in Codex, direnv, or the project environment. - Use temporary overrides only to diagnose a cache-specific problem. -- Truly destructive actions, external writes, main-stack control, and - operations outside the workspace still require explicit approval. - ## Running processes - The main checkout owns the canonical stack on the default ports. Assume it is the user's stack. Do not start, restart, or stop it unless the user asks. - A worktree owns its own isolated stack. The flake shell hook assigns a deterministic port offset and creates worktree-local PostgreSQL state. -- Start a worktree stack only when runtime or integration validation requires - it. Start it once from the worktree root, reuse it throughout validation, - and stop it once when finished. -- Do not start any service for PHPUnit, frontend formatting, linting, type - checking, or production builds. -- When authorized worktree stack control is necessary, operate it directly. - Do not ask the user to start or stop worktree services. -- For non-interactive use, start the stack detached and stop it when finished, - as shown below. +- Start a worktree stack from its root. For non-interactive use, start it + detached and stop it when finished, as shown below. - Do not use `process-compose -t=false` for a detached stack. It can leave an orphaned PostgreSQL process holding the data directory. - Non-interactive agent shells do not automatically load direnv. Bare project diff --git a/backend/.env.example b/backend/.env.example index 027ae21..c3214a6 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -3,7 +3,6 @@ APP_ENV=local APP_KEY= APP_DEBUG=true APP_URL=https://localhost:8000 -FRONTEND_URL=https://localhost:5173 APP_LOCALE=en APP_FALLBACK_LOCALE=en diff --git a/backend/app/Http/Controllers/AuthController.php b/backend/app/Http/Controllers/AuthController.php deleted file mode 100644 index de82e1a..0000000 --- a/backend/app/Http/Controllers/AuthController.php +++ /dev/null @@ -1,23 +0,0 @@ -attributes->get('user'); - - return new JsonResponse([ - 'user' => [ - 'id' => $user->getId(), - 'email' => $user->getEmail()->value(), - ], - ]); - } -} diff --git a/backend/bootstrap/app.php b/backend/bootstrap/app.php index 141b221..324b7c2 100644 --- a/backend/bootstrap/app.php +++ b/backend/bootstrap/app.php @@ -9,7 +9,6 @@ use Illuminate\Http\Request; return Application::configure(basePath: dirname(__DIR__)) ->withRouting( web: __DIR__.'/../routes/web.php', - api: __DIR__.'/../routes/api.php', commands: __DIR__.'/../routes/console.php', health: '/up', ) diff --git a/backend/config/cors.php b/backend/config/cors.php deleted file mode 100644 index 5189eb1..0000000 --- a/backend/config/cors.php +++ /dev/null @@ -1,22 +0,0 @@ - ['api/*'], - 'allowed_methods' => ['*'], - 'allowed_origins' => $allowedOrigins, - 'allowed_origins_patterns' => [], - 'allowed_headers' => ['*'], - 'exposed_headers' => [], - 'max_age' => 0, - 'supports_credentials' => true, -]; diff --git a/backend/database/seeders/DatabaseSeeder.php b/backend/database/seeders/DatabaseSeeder.php index def224c..dc2b6db 100644 --- a/backend/database/seeders/DatabaseSeeder.php +++ b/backend/database/seeders/DatabaseSeeder.php @@ -11,6 +11,5 @@ class DatabaseSeeder extends Seeder */ public function run(): void { - $this->call(UserSeeder::class); } } diff --git a/backend/database/seeders/UserSeeder.php b/backend/database/seeders/UserSeeder.php deleted file mode 100644 index 886e4c9..0000000 --- a/backend/database/seeders/UserSeeder.php +++ /dev/null @@ -1,16 +0,0 @@ - 'user@example.com', - ]); - } -} diff --git a/backend/phpunit.xml b/backend/phpunit.xml index dafc3f1..e7f0a48 100644 --- a/backend/phpunit.xml +++ b/backend/phpunit.xml @@ -26,7 +26,6 @@ - diff --git a/backend/routes/api.php b/backend/routes/api.php deleted file mode 100644 index d8fba04..0000000 --- a/backend/routes/api.php +++ /dev/null @@ -1,9 +0,0 @@ -group(function (): void { - Route::get('/me', [AuthController::class, 'me']); -}); diff --git a/backend/tests/Feature/Auth/MeEndpointTest.php b/backend/tests/Feature/Auth/MeEndpointTest.php deleted file mode 100644 index ec19fc3..0000000 --- a/backend/tests/Feature/Auth/MeEndpointTest.php +++ /dev/null @@ -1,74 +0,0 @@ -create(new CreateUserDto( - email: new EmailAddress('user@example.com'), - )); - app(SessionRepository::class)->create(new CreateSessionDto( - token: 'valid-token', - user: $user, - createdAt: $now, - expiresAt: $now->modify('+7 days'), - )); - - $response = $this->withCredentials() - ->withUnencryptedCookie( - AuthMiddleware::COOKIE_NAME, - 'valid-token', - )->getJson('/api/me'); - - $response->assertOk()->assertExactJson([ - 'user' => [ - 'id' => $user->getId(), - 'email' => 'user@example.com', - ], - ]); - } - - public function test_me_rejects_a_request_without_a_cookie(): void - { - $response = $this->getJson('/api/me'); - - $response - ->assertStatus(401) - ->assertExactJson(['error' => 'unauthenticated']); - } - - public function test_me_allows_credentialed_frontend_requests(): void - { - $response = $this->withHeaders([ - 'Origin' => 'https://localhost:5173', - 'Access-Control-Request-Method' => 'GET', - ])->options('/api/me'); - - $response - ->assertNoContent() - ->assertHeader( - 'Access-Control-Allow-Origin', - 'https://localhost:5173', - ) - ->assertHeader('Access-Control-Allow-Credentials', 'true'); - } -} diff --git a/backend/tests/Feature/Database/DatabaseSeederTest.php b/backend/tests/Feature/Database/DatabaseSeederTest.php deleted file mode 100644 index 26b6b06..0000000 --- a/backend/tests/Feature/Database/DatabaseSeederTest.php +++ /dev/null @@ -1,22 +0,0 @@ -seed(); - $this->seed(); - - $this->assertDatabaseHas('users', [ - 'email' => 'user@example.com', - ]); - $this->assertDatabaseCount('users', 1); - } -} diff --git a/frontend/website/cypress.config.ts b/frontend/website/cypress.config.ts index 4a2d276..2697f45 100644 --- a/frontend/website/cypress.config.ts +++ b/frontend/website/cypress.config.ts @@ -5,7 +5,7 @@ const frontendPort = process.env.VITE_PORT ?? '5173' export default defineConfig({ allowCypressEnv: false, e2e: { - baseUrl: `https://localhost:${frontendPort}`, + baseUrl: `http://127.0.0.1:${frontendPort}`, supportFile: false, }, video: false, diff --git a/frontend/website/cypress/e2e/guest-auth.cy.ts b/frontend/website/cypress/e2e/guest-auth.cy.ts index 2dea54b..f4c1ba8 100644 --- a/frontend/website/cypress/e2e/guest-auth.cy.ts +++ b/frontend/website/cypress/e2e/guest-auth.cy.ts @@ -8,15 +8,6 @@ describe('guest authentication pages', () => { cy.contains('a', 'Get started').should('have.attr', 'href', '/signup') }) - it('uses Attainly browser metadata', () => { - cy.visit('/') - - cy.title().should('equal', 'Attainly') - cy.get('link[rel="icon"]') - .should('have.attr', 'type', 'image/svg+xml') - .and('have.attr', 'href', '/favicon.svg') - }) - it('redirects guests away from the protected dashboard', () => { cy.visit('/dashboard') diff --git a/frontend/website/cypress/e2e/session-auth.cy.ts b/frontend/website/cypress/e2e/session-auth.cy.ts deleted file mode 100644 index d7fb723..0000000 --- a/frontend/website/cypress/e2e/session-auth.cy.ts +++ /dev/null @@ -1,56 +0,0 @@ -const authenticatedUser = { - id: 7, - email: 'user@example.com', -} - -describe('session authentication', () => { - it('restores an authenticated session on a protected route', () => { - cy.intercept('GET', '**/api/me', { - statusCode: 200, - body: { user: authenticatedUser }, - }).as('me') - - cy.visit('/dashboard') - cy.wait('@me') - - cy.location('pathname').should('equal', '/dashboard') - cy.get('h1').should('have.text', 'Your next step starts here.') - }) - - it('redirects an unauthenticated protected route to login', () => { - cy.intercept('GET', '**/api/me', { - statusCode: 401, - body: { error: 'unauthenticated' }, - }).as('me') - - cy.visit('/dashboard') - cy.wait('@me') - - cy.location('pathname').should('equal', '/login') - cy.location('search').should('include', 'redirect=/dashboard') - }) - - it('redirects a restored session away from a guest-only route', () => { - cy.intercept('GET', '**/api/me', { - statusCode: 200, - body: { user: authenticatedUser }, - }).as('me') - - cy.visit('/login') - cy.wait('@me') - - cy.location('pathname').should('equal', '/dashboard') - }) - - it('rejects a malformed authenticated-user response', () => { - cy.intercept('GET', '**/api/me', { - statusCode: 200, - body: { user: { id: 7 } }, - }).as('me') - - cy.visit('/dashboard') - cy.wait('@me') - - cy.location('pathname').should('equal', '/login') - }) -}) diff --git a/frontend/website/env.d.ts b/frontend/website/env.d.ts index b54b4c9..11f02fe 100644 --- a/frontend/website/env.d.ts +++ b/frontend/website/env.d.ts @@ -1,9 +1 @@ /// - -interface ImportMetaEnv { - readonly VITE_API_URL: string -} - -interface ImportMeta { - readonly env: ImportMetaEnv -} diff --git a/frontend/website/index.html b/frontend/website/index.html index ca72e36..9e5fc8f 100644 --- a/frontend/website/index.html +++ b/frontend/website/index.html @@ -2,9 +2,9 @@ - + - Attainly + Vite App
diff --git a/frontend/website/package-lock.json b/frontend/website/package-lock.json index e47680a..6cbfc4d 100644 --- a/frontend/website/package-lock.json +++ b/frontend/website/package-lock.json @@ -10,8 +10,7 @@ "dependencies": { "pinia": "^4.0.2", "vue": "^3.5.40", - "vue-router": "^5.2.0", - "zod": "^4.4.3" + "vue-router": "^5.2.0" }, "devDependencies": { "@tsconfig/node24": "^24.0.4", @@ -7621,15 +7620,6 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } - }, - "node_modules/zod": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", - "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } } } } diff --git a/frontend/website/package.json b/frontend/website/package.json index e70648e..ebd0942 100644 --- a/frontend/website/package.json +++ b/frontend/website/package.json @@ -18,8 +18,7 @@ "dependencies": { "pinia": "^4.0.2", "vue": "^3.5.40", - "vue-router": "^5.2.0", - "zod": "^4.4.3" + "vue-router": "^5.2.0" }, "devDependencies": { "@tsconfig/node24": "^24.0.4", diff --git a/frontend/website/public/favicon.ico b/frontend/website/public/favicon.ico new file mode 100644 index 0000000..df36fcf Binary files /dev/null and b/frontend/website/public/favicon.ico differ diff --git a/frontend/website/public/favicon.svg b/frontend/website/public/favicon.svg deleted file mode 100644 index 3d796bf..0000000 --- a/frontend/website/public/favicon.svg +++ /dev/null @@ -1,15 +0,0 @@ - - - - - diff --git a/frontend/website/src/router/index.ts b/frontend/website/src/router/index.ts index 51e7184..b726d17 100644 --- a/frontend/website/src/router/index.ts +++ b/frontend/website/src/router/index.ts @@ -37,31 +37,21 @@ const router = createRouter({ ], }) -router.beforeEach(async (to) => { +router.beforeEach((to) => { const authStore = useAuthStore() if (to.meta.requiresAuth && !authStore.isAuthenticated) { - const restoredSession = await authStore.fetchMe() - if (!restoredSession) { - return { - name: 'login', - query: { - redirect: to.fullPath, - }, - } + return { + name: 'login', + query: { + redirect: to.fullPath, + }, } } - if (to.meta.guestOnly) { - let restoredSession = authStore.isAuthenticated - if (!restoredSession) { - restoredSession = await authStore.fetchMe() - } - - if (restoredSession) { - return { - name: 'dashboard', - } + if (to.meta.guestOnly && authStore.isAuthenticated) { + return { + name: 'dashboard', } } }) diff --git a/frontend/website/src/stores/auth.ts b/frontend/website/src/stores/auth.ts index 399a301..50688f1 100644 --- a/frontend/website/src/stores/auth.ts +++ b/frontend/website/src/stores/auth.ts @@ -1,67 +1,10 @@ -import { computed, ref } from 'vue' +import { ref } from 'vue' import { defineStore } from 'pinia' -import { z } from 'zod' - -import { API_BASE } from '@/utils/apiBase' - -export const authUserSchema = z.object({ - id: z.number().int().positive(), - email: z.string().email(), -}) - -const meResponseSchema = z.object({ - user: authUserSchema, -}) - -export type AuthUser = z.infer export const useAuthStore = defineStore('auth', () => { - const user = ref(null) - const loading = ref(false) - const error = ref(null) - const isAuthenticated = computed(() => user.value !== null) - - async function fetchMe(): Promise { - loading.value = true - error.value = null - - try { - const response = await fetch(`${API_BASE}/api/me`, { - method: 'GET', - credentials: 'include', - headers: { - Accept: 'application/json', - }, - }) - - if (response.status === 200) { - const responseBody: unknown = await response.json() - user.value = meResponseSchema.parse(responseBody).user - - return true - } - - user.value = null - if (response.status !== 401) { - error.value = 'Unable to restore session' - } - - return false - } catch { - user.value = null - error.value = 'Unable to restore session' - - return false - } finally { - loading.value = false - } - } + const isAuthenticated = ref(false) return { - user, - loading, - error, isAuthenticated, - fetchMe, } }) diff --git a/frontend/website/src/utils/apiBase.ts b/frontend/website/src/utils/apiBase.ts deleted file mode 100644 index 0a1babc..0000000 --- a/frontend/website/src/utils/apiBase.ts +++ /dev/null @@ -1,17 +0,0 @@ -function resolveApiBase(): string { - const configuredApiUrl = import.meta.env.VITE_API_URL - if (configuredApiUrl === '') { - throw new Error('VITE_API_URL must be configured') - } - - if (!import.meta.env.DEV) { - return configuredApiUrl - } - - const apiUrl = new URL(configuredApiUrl) - apiUrl.hostname = window.location.hostname - - return apiUrl.origin -} - -export const API_BASE = resolveApiBase() diff --git a/frontend/website/vite.config.ts b/frontend/website/vite.config.ts index a6fdcaa..ace127e 100644 --- a/frontend/website/vite.config.ts +++ b/frontend/website/vite.config.ts @@ -1,30 +1,12 @@ -import { existsSync, readFileSync } from 'node:fs' import { fileURLToPath, URL } from 'node:url' import { defineConfig } from 'vite' -import type { ServerOptions } from 'vite' import vue from '@vitejs/plugin-vue' import vueJsx from '@vitejs/plugin-vue-jsx' import vueDevTools from 'vite-plugin-vue-devtools' -const certificateKeyPath = fileURLToPath( - new URL('../../.cert/localhost-key.pem', import.meta.url), -) -const certificatePath = fileURLToPath( - new URL('../../.cert/localhost.pem', import.meta.url), -) -const serverOptions: ServerOptions = {} - -if (existsSync(certificateKeyPath) && existsSync(certificatePath)) { - serverOptions.https = { - key: readFileSync(certificateKeyPath), - cert: readFileSync(certificatePath), - } -} - // https://vite.dev/config/ export default defineConfig({ - server: serverOptions, plugins: [ vue(), vueJsx(), diff --git a/nix/shell-hook.sh b/nix/shell-hook.sh index 73d1874..d835131 100644 --- a/nix/shell-hook.sh +++ b/nix/shell-hook.sh @@ -38,7 +38,6 @@ export PGUSER="postgres" export PGDATABASE="postgres" DEV_APP_URL="https://localhost:$CADDY_PORT" -DEV_FRONTEND_URL="https://localhost:$VITE_PORT" DEV_DB_CONNECTION="pgsql" DEV_DB_HOST="$PGHOST" DEV_DB_PORT="5432" @@ -100,7 +99,6 @@ set_env_value() { } set_env_value APP_URL "$DEV_APP_URL" -set_env_value FRONTEND_URL "$DEV_FRONTEND_URL" set_env_value DB_CONNECTION "$DEV_DB_CONNECTION" set_env_value DB_HOST "$DEV_DB_HOST" set_env_value DB_PORT "$DEV_DB_PORT" @@ -111,13 +109,6 @@ set_env_value MAIL_MAILER "$DEV_MAIL_MAILER" set_env_value MAIL_HOST "$DEV_MAIL_HOST" set_env_value MAIL_PORT "$DEV_MAIL_PORT" -FRONTEND_ENV_FILE="$REPO_ROOT/frontend/website/.env.local" -FRONTEND_API_URL="VITE_API_URL=$DEV_APP_URL" -if [ ! -f "$FRONTEND_ENV_FILE" ] \ - || [ "$(cat "$FRONTEND_ENV_FILE")" != "$FRONTEND_API_URL" ]; then - printf '%s\n' "$FRONTEND_API_URL" > "$FRONTEND_ENV_FILE" -fi - if [ ! -d "$REPO_ROOT/backend/vendor" ]; then echo "[composer] installing backend dependencies" (cd "$REPO_ROOT/backend" && composer install) diff --git a/process-compose.yaml b/process-compose.yaml index fcddf50..ad7a160 100644 --- a/process-compose.yaml +++ b/process-compose.yaml @@ -18,13 +18,6 @@ processes: postgres: condition: process_healthy - seed: - command: php artisan db:seed --force - working_dir: ./backend - depends_on: - migrate: - condition: process_completed_successfully - mailpit: command: mailpit --smtp 127.0.0.1:${MAILPIT_SMTP_PORT:-2525} --listen 127.0.0.1:${MAILPIT_UI_PORT:-8025} readiness_probe: @@ -39,7 +32,7 @@ processes: command: php artisan serve --host=127.0.0.1 --port=${BACKEND_PORT:-8001} working_dir: ./backend depends_on: - seed: + migrate: condition: process_completed_successfully mailpit: condition: process_healthy @@ -63,7 +56,6 @@ processes: host: 127.0.0.1 port: ${VITE_PORT:-5173} path: / - scheme: https initial_delay_seconds: 1 period_seconds: 2