CI - Test & Build / Lint & Type Check (push) Canceled after 0s
CI - Test & Build / Backend Unit Tests (push) Canceled after 0s
CI - Test & Build / Frontend Build (push) Canceled after 0s
CI - Test & Build / Security Scan (push) Canceled after 0s
CI - Test & Build / E2E Tests (Playwright) (push) Canceled after 0s
CI - Test & Build / CI Summary (push) Canceled after 0s
* feat: move login credentials to a DB-backed users table with an admin management page Replaces the hardcoded AUTH_USERNAME/AUTH_PASSWORD login check with a new auth_accounts table (bcrypt-hashed passwords, admin/user roles). Adds admin-only /api/users CRUD routes and a "Brugere" admin page in the frontend for managing logins without redeploying. Removes the unused, unmounted duplicate login route in src/routes/auth.js. * docs: add architecture codemaps with diagrams for the whole system Adds codemaps/architecture.md, backend.md, frontend.md, and data.md — Mermaid-diagrammed design documentation verified against the live codebase and database rather than assumed from CLAUDE.md. Covers the unified-server.js request flow (mounted routers + ~183 inline routes), 68 backend services grouped by domain, the frontend's state-driven view-switch (no React Router in practice despite BrowserRouter being present), and the full 122-table DB schema with the auth_accounts vs unrelated users table naming trap flagged explicitly. Links added from the root README. Co-Authored-By: Claude Sonnet 5 <[email protected]> * feat: ship canonical roof quote workflow * fix: keep migration dry-run idempotent * [verified] feat: complete Smart Pakker management * [verified] fix: ignore blank task dependencies * [verified] fix: align package duplication with schema * [verified] fix: enforce Discord status limits * [verified] fix: link Smart Pakke materials safely * [verified] fix: harden material link review * [verified] feat: improve material matching * fix: scope pitch validation to roof packages * fix: support canonical snapshots on production schema * [verified] fix: hide internal package metadata from PDF * [verified] feat: deliver sales-ready customer PDF * [verified] feat: ship sales-ready PDF with AI overview * [verified] fix: authenticate project list requests * [verified] fix: refresh project-list authentication * [verified] fix: open existing project details * [verified] fix: keep roof components searchable in builder * [verified] fix: expose all Smart Package categories * [verified] fix: authenticate project creation * [verified] feat: make Smart Pakker the universal project flow * [verified] feat: preview Smart Package contents * [verified] test: keep generic release isolated from downpipe work * feat: add first-class Smart Pakke rentals * [verified] feat: add gutter and downpipe smart packages * [verified] fix: prepare six-house gutter quote flow * [verified] fix: open generic quotes without roof geometry * [verified] fix: review generic quotes with authenticated APIs * [verified] fix: calculate generic Smart Package quotes * [verified] fix: return generic calculation breakdown * feat: checkpoint generic signed snapshot validation with red-green tests * feat: complete fail-closed generic quote approval and customer PDF flow * feat: use generic signed snapshot in final review * feat: redesign generic quote final review * fix: harden generic review summaries * feat: add auditable six-house package basis * [verified] feat: finish auditable Smart Pakke UI * [verified] fix: bind auditable quantity and price bases * [verified] fix: keep six-house basis across package versions * [verified] fix: complete smart package discovery management * [verified] fix: simplify composition and generic scope * [verified] test: keep explicit roof contracts fail closed * [verified] fix: harden generic quote snapshots * fix: make generic quote delivery customer safe * [verified] fix: secure package catalog reads * [verified] fix: close workspace provenance blockers * fix: harden customer document language boundary * [verified] fix: secure smart package internal reads * fix: version package child mutations atomically * feat: add generic customer quote text flow * [verified] fix: allow manual customer numbers * [verified] fix: expose optional roof geometry * [verified] fix: rebase hydrated packages after geometry edits * [verified] feat: add free editable site area map * [verified] fix: harden map recovery and geocoding gate * fix: bind map quantities to authoritative geometry * fix: release geocoder lock before dispatch * fix: separate roof and site geometry provenance * fix: revoke stale admin authorization * fix: migrate task geometry basis * fix: make backend CI dependency-complete * ci: seed isolated e2e login account * fix: allow clean database bootstrap * fix: skip indexes for optional tables * test: use canonical mansard geometry in e2e * [verified] fix(auth): enforce live operator boundary * fix: fail close Ordrestyring offer transport * fix(frontend): authenticate customer project requests * fix: align canonical roof type contract * [verified] fix: reconcile legacy package labor safely * [verified] fix: audit site geometry deletion * docs: add PR 31 reviewer guide * docs: synchronize Obsidian vault * docs: sync integrated reviewer guide to Obsidian * ci: seed isolated auth account explicitly * fix: close offer bootstrap and service readiness gaps * fix: authenticate protected package callers * fix: provision initial admin and disable generic send * [verified] fix: close final quote release blockers * [verified] fix: seed gutter packages before deployment --------- Co-authored-by: alexpolo1 <[email protected]> Co-authored-by: Claude Sonnet 5 <[email protected]>
93 lines
4.7 KiB
JavaScript
93 lines
4.7 KiB
JavaScript
jest.mock('mysql2/promise', () => ({ createPool: jest.fn(), createConnection: jest.fn() }));
|
|
const mysql = require('mysql2/promise');
|
|
const db = require('../src/services/databaseService');
|
|
afterEach(() => jest.restoreAllMocks());
|
|
test('normal clean startup creates auth schema idempotently without credential writes', async () => {
|
|
process.env.DB_PASSWORD = 'test-only';
|
|
const execute = jest.fn(async sql => {
|
|
if (/INFORMATION_SCHEMA\.TABLES/i.test(sql)) return [[{ ENGINE: 'InnoDB' }]];
|
|
if (/COUNT\(\*\).*auth_accounts/is.test(sql)) return [[{ account_count: 1 }]];
|
|
return [[]];
|
|
});
|
|
mysql.createPool.mockReturnValue({ execute, getConnection: async () => ({ query: async () => [[]], release() {} }) });
|
|
for (const method of ['ensureSystemSettingsTable', 'removeLegacySupportSecrets', 'syncSupportSettingsFromEnv', 'createTables', 'seedAiFeatureSettings']) jest.spyOn(db, method).mockResolvedValue();
|
|
await db.initialize();
|
|
await db.initialize();
|
|
expect(execute.mock.calls).toHaveLength(8);
|
|
for (const [sql] of execute.mock.calls) {
|
|
expect(sql).not.toMatch(/INSERT|UPDATE auth_accounts|REPLACE/i);
|
|
}
|
|
expect(execute.mock.calls.filter(([sql]) => /CREATE TABLE IF NOT EXISTS auth_accounts/i.test(sql))).toHaveLength(2);
|
|
expect(execute.mock.calls.filter(([sql]) => /CREATE TABLE IF NOT EXISTS ordrestyring_offer_operations/i.test(sql))).toHaveLength(2);
|
|
});
|
|
|
|
test('provisions the first admin once from configured credentials without overwriting accounts', async () => {
|
|
const execute = jest.fn()
|
|
.mockResolvedValueOnce([[{ account_count: 0 }]])
|
|
.mockResolvedValueOnce([{ affectedRows: 1 }]);
|
|
const hashPassword = jest.fn().mockResolvedValue('$2b$12$initial-admin-hash');
|
|
const { provisionInitialAdmin } = require('../src/services/authAccountSchema');
|
|
|
|
await expect(provisionInitialAdmin({ execute }, {
|
|
env: { AUTH_USERNAME: 'operator', AUTH_PASSWORD: 'configured-secret' },
|
|
hashPassword
|
|
})).resolves.toBe(true);
|
|
|
|
expect(hashPassword).toHaveBeenCalledWith('configured-secret', 12);
|
|
expect(execute.mock.calls[1]).toEqual([
|
|
expect.stringMatching(/INSERT IGNORE INTO auth_accounts/),
|
|
['operator', '$2b$12$initial-admin-hash', 'admin']
|
|
]);
|
|
});
|
|
|
|
test('fails closed when an empty account table has no initial admin credentials', async () => {
|
|
const { provisionInitialAdmin } = require('../src/services/authAccountSchema');
|
|
const execute = jest.fn().mockResolvedValueOnce([[{ account_count: 0 }]]);
|
|
|
|
await expect(provisionInitialAdmin({ execute }, { env: {}, hashPassword: jest.fn() }))
|
|
.rejects.toMatchObject({ code: 'INITIAL_ADMIN_REQUIRED' });
|
|
expect(execute).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
test('converts a legacy non-transactional auth table before account mutations can run', async () => {
|
|
const execute = jest.fn(async sql => (
|
|
/INFORMATION_SCHEMA\.TABLES/i.test(sql) ? [[{ ENGINE: 'MyISAM' }]] : [[]]
|
|
));
|
|
await require('../src/services/authAccountSchema').ensureAuthAccountsTable({ execute });
|
|
expect(execute.mock.calls.map(([sql]) => sql)).toEqual([
|
|
expect.stringMatching(/CREATE TABLE IF NOT EXISTS auth_accounts/i),
|
|
expect.stringMatching(/INFORMATION_SCHEMA\.TABLES/i),
|
|
expect.stringMatching(/ALTER TABLE auth_accounts ENGINE=InnoDB/i)
|
|
]);
|
|
});
|
|
test('migration has no credential seeding or hashing dependency', () => {
|
|
const source = require('fs').readFileSync(require('path').join(__dirname, '../../database/migrations/20260904_users_table.js'), 'utf8');
|
|
expect(source).not.toMatch(/bcrypt|AUTH_PASSWORD|INSERT|seedUsers/);
|
|
});
|
|
test('migration executes only idempotent DDL even when legacy credentials are configured', async () => {
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const vm = require('vm');
|
|
const filename = path.join(__dirname, '../../database/migrations/20260904_users_table.js');
|
|
const execute = jest.fn(async sql => (
|
|
/INFORMATION_SCHEMA\.TABLES/i.test(sql) ? [[{ ENGINE: 'InnoDB' }]] : [[]]
|
|
));
|
|
let finished;
|
|
const done = new Promise(resolve => { finished = resolve; });
|
|
const connection = { execute, end: jest.fn(async () => finished()) };
|
|
const localRequire = name => {
|
|
if (name === 'mysql2/promise') return { createConnection: async () => connection };
|
|
if (name === 'dotenv') return { config() {} };
|
|
if (name.includes('authAccountSchema')) return require('../src/services/authAccountSchema');
|
|
return require(name);
|
|
};
|
|
vm.runInNewContext(fs.readFileSync(filename, 'utf8'), {
|
|
require: localRequire, __dirname: path.dirname(filename), console,
|
|
process: { env: { AUTH_USERNAME: 'operator', AUTH_PASSWORD: 'must-not-be-written' }, exit: jest.fn() }
|
|
});
|
|
await done;
|
|
expect(execute).toHaveBeenCalledTimes(2);
|
|
expect(execute.mock.calls[0][0]).toMatch(/CREATE TABLE IF NOT EXISTS auth_accounts/);
|
|
expect(execute.mock.calls[0]).toHaveLength(1);
|
|
});
|