Files
Joshua OosthuizenandClaude Sonnet 5 4acbc03b35 Initial commit: multi-company financial management system
Root company app (src/, client/) — GL foundation, AR, and AP:
- Segmented Chart of Accounts (Sage 300-style), double-entry PostingService with
  three-layer balance enforcement (app validation, CHECK constraints, deferred
  DB trigger), gap-free document numbering, RBAC with branch restrictions.
- Accounts Receivable: Customers, Invoices, Credit Notes, Receipts with
  allocation, all posting through PostingService.
- Accounts Payable: Suppliers, Supplier Invoices, Debit Notes, Payments with
  allocation, mirroring AR's posting pattern.
- Tax Codes shared between AR and AP.
- Company-app login is a token handoff from the Account Manager (RS256,
  single-use, company-scoped) rather than a local password.
- Frontend reskinned to the approved mockup: navy sidebar, dashboard, tree-view
  Chart of Accounts, Customer/Supplier 360 pages.

Account Manager (manager/, manager/client/) — central login gateway:
- Password + TOTP 2FA + Google/Microsoft OAuth, company picker, and the RS256
  handoff token issuance that the company app verifies.

Covered by unit + integration tests against a real Postgres database (GL
balance invariants, AR/AP posting correctness and allocation limits, the full
handoff exchange including replay/cross-company/expiry/forgery rejection).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tq2DbEvzHzs13jDFhLaX44
2026-09-03 13:27:19 +02:00

83 lines
3.3 KiB
TypeScript

/**
* Requires the real, migrated, seeded company-app database (see SETUP.md) reachable via
* DATABASE_URL, and the Manager's real handoff keypair at ../manager/keys/ (see
* manager/SETUP.md — npm run generate-handoff-keys). Run with: npm run test:integration
*/
import fs from 'fs';
import path from 'path';
import jwt from 'jsonwebtoken';
import crypto from 'crypto';
import request from 'supertest';
import { createApp } from '../../src/app';
import { prisma } from '../../src/lib/prisma';
const app = createApp();
const managerPrivateKey = fs.readFileSync(path.resolve(__dirname, '../../manager/keys/handoff-private.pem'), 'utf8');
const ADMIN_EMAIL = 'joosth2002@icloud.com';
function mintHandoffToken(overrides: Partial<{ sub: string; companySlug: string; expiresIn: number }> = {}) {
const claims = { sub: overrides.sub ?? ADMIN_EMAIL, pid: 1, cid: 1, companySlug: overrides.companySlug ?? 'demo' };
return jwt.sign(claims, managerPrivateKey, {
algorithm: 'RS256',
expiresIn: overrides.expiresIn ?? 90,
jwtid: crypto.randomUUID(),
});
}
describe('Company app — handoff token exchange (real DB, real keypair)', () => {
afterAll(async () => {
await prisma.$disconnect();
});
it('exchanges a valid handoff token for a working session', async () => {
const token = mintHandoffToken();
const response = await request(app).post('/api/auth/handoff').send({ token });
expect(response.status).toBe(200);
expect(response.body.accessToken).toBeDefined();
expect(response.body.user.email).toBe(ADMIN_EMAIL);
const me = await request(app).get('/api/auth/me').set('Authorization', `Bearer ${response.body.accessToken}`);
expect(me.status).toBe(200);
expect(me.body.user.email).toBe(ADMIN_EMAIL);
});
it('rejects the same token twice (single-use / replay protection)', async () => {
const token = mintHandoffToken();
const first = await request(app).post('/api/auth/handoff').send({ token });
expect(first.status).toBe(200);
const second = await request(app).post('/api/auth/handoff').send({ token });
expect(second.status).toBe(401);
expect(second.body.error).toMatch(/already been used/i);
});
it('rejects a token minted for a different company slug', async () => {
const token = mintHandoffToken({ companySlug: 'some-other-company' });
const response = await request(app).post('/api/auth/handoff').send({ token });
expect(response.status).toBe(401);
expect(response.body.error).toMatch(/not issued for this company/i);
});
it('rejects an expired token', async () => {
const token = mintHandoffToken({ expiresIn: -10 });
const response = await request(app).post('/api/auth/handoff').send({ token });
expect(response.status).toBe(401);
});
it('rejects a well-formed but unsigned/forged token', async () => {
const forged = jwt.sign({ sub: ADMIN_EMAIL, pid: 1, cid: 1, companySlug: 'demo' }, 'not-the-real-key', {
algorithm: 'HS256',
expiresIn: 90,
jwtid: crypto.randomUUID(),
});
const response = await request(app).post('/api/auth/handoff').send({ token: forged });
expect(response.status).toBe(401);
});
it('the old POST /api/auth/login route no longer exists', async () => {
const response = await request(app).post('/api/auth/login').send({ email: 'x', password: 'y' });
expect(response.status).toBe(404);
});
});