Skip to content

Commit be17296

Browse files
committed
Remove outdated documentation and prompts; add authentication and user management features
- Deleted the GitHub Actions workshop README and related prompts to streamline content. - Removed the "How GitHub uses GitHub" documentation to focus on more relevant materials. - Deleted various prompt files related to the Pets Workshop project. - Added new authentication API endpoints for login, logout, and user retrieval. - Implemented user model with password hashing and role management. - Created e2e tests for staff authentication and upload functionality. - Developed login and upload pages with form handling and user session management.
1 parent c180d23 commit be17296

76 files changed

Lines changed: 535 additions & 4002 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

app/client/e2e-tests/auth.spec.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { test, expect } from '@playwright/test';
2+
3+
test.describe('Staff Authentication', () => {
4+
test('guest browsing still works and shows login link', async ({ page }) => {
5+
await page.goto('/');
6+
7+
await expect(page.getByRole('heading', { name: 'Welcome to Tailspin Shelter' })).toBeVisible();
8+
await expect(page.getByRole('link', { name: 'Login' })).toBeVisible();
9+
});
10+
11+
test('guest visiting upload is sent to login', async ({ page }) => {
12+
await page.goto('/upload');
13+
14+
await expect(page).toHaveURL(/\/login\?next=\/upload$/);
15+
await expect(page.getByRole('heading', { name: 'Staff Login' })).toBeVisible();
16+
});
17+
18+
test('staff can log in and reach upload page', async ({ page }) => {
19+
await page.goto('/login');
20+
21+
await page.getByTestId('login-submit').click();
22+
23+
await expect(page).toHaveURL('/upload');
24+
await expect(page.getByTestId('upload-heading')).toHaveText('AI Listing Agent');
25+
await expect(page.getByRole('link', { name: 'Upload Listing' })).toBeVisible();
26+
});
27+
28+
test('staff can log out and return to guest navigation', async ({ page }) => {
29+
await page.goto('/login');
30+
await page.getByTestId('login-submit').click();
31+
await expect(page).toHaveURL('/upload');
32+
33+
await page.getByRole('button', { name: 'Logout' }).click();
34+
35+
await expect(page).toHaveURL('/');
36+
await expect(page.getByRole('link', { name: 'Login' })).toBeVisible();
37+
});
38+
});

app/client/playwright.config.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ import { fileURLToPath } from 'node:url';
55
const __dirname = path.dirname(fileURLToPath(import.meta.url));
66
const serverDir = path.resolve(__dirname, '..', 'server');
77
const testDbPath = path.join(serverDir, 'e2e_test_dogshelter.db');
8-
const flaskPort = 5100;
9-
const astroDevPort = 4321;
8+
const flaskPort = Number(process.env.FLASK_PORT || 5100);
9+
const astroDevPort = Number(process.env.ASTRO_DEV_PORT || 4321);
1010

1111
export default defineConfig({
1212
testDir: './e2e-tests',
Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,48 @@
1+
---
2+
const API_SERVER_URL = process.env.API_SERVER_URL || 'http://localhost:5100';
3+
const cookie = Astro.request.headers.get('cookie') || '';
4+
5+
let user = null;
6+
7+
try {
8+
const response = await fetch(`${API_SERVER_URL}/api/auth/me`, {
9+
headers: cookie ? { cookie } : {},
10+
});
11+
if (response.ok) {
12+
const data = await response.json();
13+
user = data.user;
14+
}
15+
} catch {
16+
user = null;
17+
}
18+
---
19+
120
<header class="bg-blue-700 text-white p-4 shadow-md">
221
<div class="container mx-auto flex justify-between items-center px-4">
322
<h1 class="text-xl font-bold"><a href="/" class="hover:underline">Tailspin Shelter</a></h1>
423
<nav>
5-
<ul class="flex gap-4">
24+
<ul class="flex gap-4 items-center">
625
<li><a href="/" class="hover:underline">Home</a></li>
726
<li><a href="/about" class="hover:underline">About</a></li>
27+
{user ? (
28+
<>
29+
<li><a href="/upload" class="hover:underline">Upload Listing</a></li>
30+
<li>
31+
<button id="logout-button" class="hover:underline" type="button">Logout</button>
32+
</li>
33+
</>
34+
) : (
35+
<li><a href="/login" class="hover:underline">Login</a></li>
36+
)}
837
</ul>
938
</nav>
1039
</div>
11-
</header>
40+
</header>
41+
42+
<script>
43+
const logoutButton = document.getElementById('logout-button');
44+
logoutButton?.addEventListener('click', async () => {
45+
await fetch('/api/auth/logout', { method: 'POST' });
46+
window.location.href = '/';
47+
});
48+
</script>
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import type { APIRoute } from 'astro';
2+
3+
const API_SERVER_URL = process.env.API_SERVER_URL || 'http://localhost:5100';
4+
5+
export const POST: APIRoute = async ({ request }) => {
6+
const response = await fetch(`${API_SERVER_URL}/api/auth/login`, {
7+
method: 'POST',
8+
headers: { 'content-type': 'application/json' },
9+
body: await request.text(),
10+
});
11+
12+
const body = await response.text();
13+
const headers = new Headers({ 'content-type': 'application/json' });
14+
const setCookie = response.headers.get('set-cookie');
15+
if (setCookie) {
16+
headers.set('set-cookie', setCookie);
17+
}
18+
19+
return new Response(body, { status: response.status, headers });
20+
};
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import type { APIRoute } from 'astro';
2+
3+
const API_SERVER_URL = process.env.API_SERVER_URL || 'http://localhost:5100';
4+
5+
export const POST: APIRoute = async ({ request }) => {
6+
const cookie = request.headers.get('cookie') || '';
7+
const response = await fetch(`${API_SERVER_URL}/api/auth/logout`, {
8+
method: 'POST',
9+
headers: cookie ? { cookie } : {},
10+
});
11+
12+
const body = await response.text();
13+
const headers = new Headers({ 'content-type': 'application/json' });
14+
const setCookie = response.headers.get('set-cookie');
15+
if (setCookie) {
16+
headers.set('set-cookie', setCookie);
17+
}
18+
19+
return new Response(body, { status: response.status, headers });
20+
};
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import type { APIRoute } from 'astro';
2+
3+
const API_SERVER_URL = process.env.API_SERVER_URL || 'http://localhost:5100';
4+
5+
export const GET: APIRoute = async ({ request }) => {
6+
const cookie = request.headers.get('cookie') || '';
7+
const response = await fetch(`${API_SERVER_URL}/api/auth/me`, {
8+
headers: cookie ? { cookie } : {},
9+
});
10+
11+
return new Response(await response.text(), {
12+
status: response.status,
13+
headers: { 'content-type': 'application/json' },
14+
});
15+
};

app/client/src/pages/login.astro

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
---
2+
import Layout from '../layouts/Layout.astro';
3+
4+
const next = Astro.url.searchParams.get('next') || '/upload';
5+
---
6+
7+
<Layout title="Staff Login - Tailspin Shelter">
8+
<div class="py-8 max-w-md">
9+
<h1 class="text-3xl font-bold text-slate-100 mb-3">Staff Login</h1>
10+
<p class="text-slate-300 mb-8">Sign in to upload and generate AI-assisted pet listings.</p>
11+
12+
<form id="login-form" class="bg-slate-800/70 border border-slate-700 rounded-xl p-6 space-y-5" data-testid="login-form">
13+
<input type="hidden" id="next-url" value={next} />
14+
15+
<div>
16+
<label for="email" class="block text-sm font-medium text-slate-200 mb-2">Email</label>
17+
<input
18+
id="email"
19+
name="email"
20+
type="email"
21+
autocomplete="username"
22+
required
23+
value="staff@tailspin.example"
24+
class="w-full rounded-lg border border-slate-600 bg-slate-900 px-3 py-2 text-slate-100 focus:border-blue-400 focus:outline-none"
25+
data-testid="login-email"
26+
/>
27+
</div>
28+
29+
<div>
30+
<label for="password" class="block text-sm font-medium text-slate-200 mb-2">Password</label>
31+
<input
32+
id="password"
33+
name="password"
34+
type="password"
35+
autocomplete="current-password"
36+
required
37+
value="TailspinDemo123!"
38+
class="w-full rounded-lg border border-slate-600 bg-slate-900 px-3 py-2 text-slate-100 focus:border-blue-400 focus:outline-none"
39+
data-testid="login-password"
40+
/>
41+
</div>
42+
43+
<p id="login-error" class="hidden text-sm text-red-400" data-testid="login-error"></p>
44+
45+
<button
46+
type="submit"
47+
class="w-full rounded-lg bg-blue-600 px-4 py-2 font-medium text-white hover:bg-blue-700 transition-colors"
48+
data-testid="login-submit"
49+
>
50+
Sign in
51+
</button>
52+
</form>
53+
</div>
54+
</Layout>
55+
56+
<script>
57+
const form = document.getElementById('login-form') as HTMLFormElement | null;
58+
const error = document.getElementById('login-error');
59+
const nextUrl = (document.getElementById('next-url') as HTMLInputElement | null)?.value || '/upload';
60+
61+
form?.addEventListener('submit', async (event) => {
62+
event.preventDefault();
63+
error?.classList.add('hidden');
64+
65+
const formData = new FormData(form);
66+
const response = await fetch('/api/auth/login', {
67+
method: 'POST',
68+
headers: { 'content-type': 'application/json' },
69+
body: JSON.stringify({
70+
email: formData.get('email'),
71+
password: formData.get('password'),
72+
}),
73+
});
74+
75+
if (response.ok) {
76+
window.location.href = nextUrl;
77+
return;
78+
}
79+
80+
const data = await response.json().catch(() => ({ error: 'Unable to sign in' }));
81+
if (error) {
82+
error.textContent = data.error || 'Unable to sign in';
83+
error.classList.remove('hidden');
84+
}
85+
});
86+
</script>

app/client/src/pages/upload.astro

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
---
2+
import Layout from '../layouts/Layout.astro';
3+
4+
const API_SERVER_URL = process.env.API_SERVER_URL || 'http://localhost:5100';
5+
const cookie = Astro.request.headers.get('cookie') || '';
6+
7+
let user = null;
8+
9+
try {
10+
const response = await fetch(`${API_SERVER_URL}/api/auth/me`, {
11+
headers: cookie ? { cookie } : {},
12+
});
13+
if (response.ok) {
14+
const data = await response.json();
15+
user = data.user;
16+
}
17+
} catch {
18+
user = null;
19+
}
20+
21+
if (!user) {
22+
return Astro.redirect('/login?next=/upload');
23+
}
24+
---
25+
26+
<Layout title="Upload AI Listing - Tailspin Shelter">
27+
<div class="py-8">
28+
<div class="max-w-3xl mb-8">
29+
<h1 class="text-3xl font-bold text-slate-100 mb-3" data-testid="upload-heading">AI Listing Agent</h1>
30+
<p class="text-slate-300">
31+
Upload pet images and notes to generate a searchable, SEO-ready adoption listing.
32+
</p>
33+
</div>
34+
35+
<section class="bg-slate-800/70 border border-slate-700 rounded-xl p-6" data-testid="upload-panel">
36+
<div class="grid gap-5">
37+
<div>
38+
<label for="pet-images" class="block text-sm font-medium text-slate-200 mb-2">Pet images</label>
39+
<input
40+
id="pet-images"
41+
type="file"
42+
accept="image/*"
43+
multiple
44+
class="block w-full text-sm text-slate-300 file:mr-4 file:rounded-lg file:border-0 file:bg-blue-600 file:px-4 file:py-2 file:text-white hover:file:bg-blue-700"
45+
data-testid="upload-images"
46+
/>
47+
</div>
48+
49+
<div>
50+
<label for="pet-notes" class="block text-sm font-medium text-slate-200 mb-2">Known notes</label>
51+
<textarea
52+
id="pet-notes"
53+
rows="5"
54+
class="w-full rounded-lg border border-slate-600 bg-slate-900 px-3 py-2 text-slate-100 focus:border-blue-400 focus:outline-none"
55+
placeholder="Name, temperament, medical notes, intake source, or adoption restrictions"
56+
data-testid="upload-notes"
57+
></textarea>
58+
</div>
59+
60+
<button
61+
type="button"
62+
class="w-fit rounded-lg bg-blue-600 px-4 py-2 font-medium text-white hover:bg-blue-700 transition-colors"
63+
data-testid="generate-listing"
64+
>
65+
Generate listing draft
66+
</button>
67+
</div>
68+
</section>
69+
</div>
70+
</Layout>

app/client/start-test-server.js

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
// Cross-platform script to seed the test database and start the Flask server
2-
const { execSync, spawn } = require('child_process');
3-
const path = require('path');
2+
import { execSync, spawn } from 'node:child_process';
3+
import path from 'node:path';
4+
import { fileURLToPath } from 'node:url';
45

6+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
57
const serverDir = path.resolve(__dirname, '..', 'server');
68
const testDbPath = path.join(serverDir, 'e2e_test_dogshelter.db');
79
const python = process.env.PYTHON || (process.platform === 'win32' ? 'py' : 'python3');
10+
const flaskPort = process.env.FLASK_PORT || '5100';
811

912
// Seed the test database
1013
execSync(`${python} utils/seed_test_database.py`, {
@@ -15,7 +18,7 @@ execSync(`${python} utils/seed_test_database.py`, {
1518
// Start Flask with the test database
1619
const server = spawn(python, ['app.py'], {
1720
cwd: serverDir,
18-
env: { ...process.env, DATABASE_PATH: testDbPath },
21+
env: { ...process.env, DATABASE_PATH: testDbPath, FLASK_PORT: flaskPort },
1922
stdio: 'inherit',
2023
});
2124

0 commit comments

Comments
 (0)