Clean working version: deployed TypeScript backend + JSX frontend

- Replace old JS backend with working TypeScript backend from deployed containers
- Backend: Fastify + better-sqlite3 + Yjs CRDT, 17 TS source files, 13 test files
- Frontend: React JSX with fabric.js CAD canvas, vite build, nginx serving
- Fix nginx proxy port 5000→3001 to match backend
- Fix docker-compose port mapping 5000→3001
- Fix vite dev proxy port 5000→3001
- Add backend healthcheck to docker-compose
- Update index.html title to 'Web CAD', lang to 'de'
- Add .a0/ to .gitignore
- Remove old JS backend files (models, routes, config, middleware, seed)
- Remove tracked build artifacts (backend/public/)
This commit is contained in:
Agent Zero
2026-06-28 01:24:31 +02:00
parent 249d5fbdb6
commit 8fa6f795c0
60 changed files with 8988 additions and 3708 deletions
+269
View File
@@ -0,0 +1,269 @@
/**
* Auth API Tests Register / Login / Logout / Me / Password Change
*/
import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest';
import type { FastifyInstance } from 'fastify';
import { SqliteAdapter } from '../src/database/SqliteAdapter.js';
import { createServer } from '../src/server.js';
describe('Auth API', () => {
let app: FastifyInstance;
let db: SqliteAdapter;
let authToken: string | null = null;
beforeAll(async () => {
db = new SqliteAdapter(':memory:');
await db.init();
app = await createServer({ db, port: 0 });
await app.ready();
});
afterAll(async () => {
await app.close();
db.close();
});
// ─── Register ────────────────────────────────────────
describe('POST /api/auth/register', () => {
it('should register a new user with 201', async () => {
const response = await app.inject({
method: 'POST',
url: '/api/auth/register',
payload: {
email: 'testuser@example.com',
password: 'TestPass123!',
name: 'Test User',
role: 'planer',
},
});
expect(response.statusCode).toBe(201);
const body = JSON.parse(response.body);
expect(body.user).toBeDefined();
expect(body.user.email).toBe('testuser@example.com');
expect(body.user.name).toBe('Test User');
expect(body.user.password_hash).toBeUndefined();
expect(body.session).toBeDefined();
expect(body.session.token).toBeDefined();
authToken = body.session.token;
});
it('should reject duplicate registration with 409', async () => {
const response = await app.inject({
method: 'POST',
url: '/api/auth/register',
payload: {
email: 'testuser@example.com',
password: 'TestPass123!',
name: 'Test User 2',
},
});
expect(response.statusCode).toBe(409);
const body = JSON.parse(response.body);
expect(body.error).toContain('already registered');
});
it('should reject missing fields with 400', async () => {
const response = await app.inject({
method: 'POST',
url: '/api/auth/register',
payload: { email: 'incomplete@example.com' },
});
expect(response.statusCode).toBe(400);
});
});
// ─── Login ──────────────────────────────────────────
describe('POST /api/auth/login', () => {
it('should login with correct credentials', async () => {
const response = await app.inject({
method: 'POST',
url: '/api/auth/login',
payload: {
email: 'testuser@example.com',
password: 'TestPass123!',
},
});
expect(response.statusCode).toBe(200);
const body = JSON.parse(response.body);
expect(body.user).toBeDefined();
expect(body.session.token).toBeDefined();
authToken = body.session.token;
});
it('should reject wrong password with 401', async () => {
const response = await app.inject({
method: 'POST',
url: '/api/auth/login',
payload: {
email: 'testuser@example.com',
password: 'WrongPassword!',
},
});
expect(response.statusCode).toBe(401);
});
it('should reject non-existent email with 401', async () => {
const response = await app.inject({
method: 'POST',
url: '/api/auth/login',
payload: {
email: 'nobody@example.com',
password: 'SomePassword!',
},
});
expect(response.statusCode).toBe(401);
});
it('should reject missing fields with 400', async () => {
const response = await app.inject({
method: 'POST',
url: '/api/auth/login',
payload: { email: 'testuser@example.com' },
});
expect(response.statusCode).toBe(400);
});
});
// ─── Me ─────────────────────────────────────────────
describe('GET /api/auth/me', () => {
it('should return current user profile with valid token', async () => {
const response = await app.inject({
method: 'GET',
url: '/api/auth/me',
headers: { authorization: `Bearer ${authToken}` },
});
expect(response.statusCode).toBe(200);
const body = JSON.parse(response.body);
expect(body.email).toBe('testuser@example.com');
expect(body.password_hash).toBeUndefined();
});
it('should reject without token with 401', async () => {
const response = await app.inject({
method: 'GET',
url: '/api/auth/me',
});
expect(response.statusCode).toBe(401);
});
it('should reject invalid token with 401', async () => {
const response = await app.inject({
method: 'GET',
url: '/api/auth/me',
headers: { authorization: 'Bearer invalid-token-xxx' },
});
expect(response.statusCode).toBe(401);
});
});
// ─── Password Change ───────────────────────────────
describe('PATCH /api/auth/password', () => {
it('should change password with correct old password', async () => {
const response = await app.inject({
method: 'PATCH',
url: '/api/auth/password',
headers: { authorization: `Bearer ${authToken}` },
payload: {
oldPassword: 'TestPass123!',
newPassword: 'NewPassword456!',
},
});
expect(response.statusCode).toBe(204);
});
it('should allow login with new password', async () => {
const response = await app.inject({
method: 'POST',
url: '/api/auth/login',
payload: {
email: 'testuser@example.com',
password: 'NewPassword456!',
},
});
expect(response.statusCode).toBe(200);
const body = JSON.parse(response.body);
expect(body.session.token).toBeDefined();
authToken = body.session.token;
});
it('should reject old password after change', async () => {
const response = await app.inject({
method: 'POST',
url: '/api/auth/login',
payload: {
email: 'testuser@example.com',
password: 'TestPass123!',
},
});
expect(response.statusCode).toBe(401);
});
it('should reject password change with wrong old password', async () => {
const response = await app.inject({
method: 'PATCH',
url: '/api/auth/password',
headers: { authorization: `Bearer ${authToken}` },
payload: {
oldPassword: 'WrongOldPassword!',
newPassword: 'AnotherNew789!',
},
});
expect(response.statusCode).toBe(400);
});
it('should reject password change without auth', async () => {
const response = await app.inject({
method: 'PATCH',
url: '/api/auth/password',
payload: {
oldPassword: 'NewPassword456!',
newPassword: 'AnotherNew789!',
},
});
expect(response.statusCode).toBe(401);
});
it('should reject missing password fields', async () => {
const response = await app.inject({
method: 'PATCH',
url: '/api/auth/password',
headers: { authorization: `Bearer ${authToken}` },
payload: { oldPassword: 'NewPassword456!' },
});
expect(response.statusCode).toBe(400);
});
});
// ─── Logout ─────────────────────────────────────────
describe('POST /api/auth/logout', () => {
it('should logout and invalidate token', async () => {
const response = await app.inject({
method: 'POST',
url: '/api/auth/logout',
headers: { authorization: `Bearer ${authToken}` },
});
expect(response.statusCode).toBe(204);
// Token should now be invalid
const meResponse = await app.inject({
method: 'GET',
url: '/api/auth/me',
headers: { authorization: `Bearer ${authToken}` },
});
expect(meResponse.statusCode).toBe(401);
});
it('should return 204 even without token', async () => {
const response = await app.inject({
method: 'POST',
url: '/api/auth/logout',
});
expect(response.statusCode).toBe(204);
});
});
});