feat(users): per-user signing-email override (login ≠ signing identity)
Adds nullable user_profiles.signing_email — the address representing a user in SIGNING contexts only (EOI developer/approver signer slot, in-CRM 'your turn to sign' notification, 'awaiting my signature' hub tab). Never a login credential; better-auth identity stays single-email. NULL → user signs as their own login email. Lets a person who logs in as e.g. abbie@ sign on behalf of a shared role mailbox (sales@) without changing their login. Additive + backward-compatible: every existing user (NULL override) is byte-for-byte unaffected. - migration 0100 + drizzle column (nullable, no unique constraint) - resolveCrmUser: signing_email ?? user.email - listDocuments hub filter: match the caller's owned email SET (login + override) for awaiting_me / awaiting_them (currentUserEmail -> currentUserEmails) - ctx.user.signingEmail surfaced from the already-loaded profile (no extra query) - updateUser persists/lowercases the override; '' clears it (edit-only) - admin Edit-User form: optional 'Signing email' field - TDD: validator unit test + 3 integration tests (resolver override+fallback, updateUser persist/clear, hub awaiting_me matches override) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L2qc3xZTfif7N4Wq3QDa8X
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
/**
|
||||
* The documents-hub "awaiting my signature" tab matches a document's pending
|
||||
* signer against the set of emails the caller owns — their login email AND
|
||||
* their signing_email override. So a user who logs in as `abbie@` but signs as
|
||||
* `sales@` still sees the `sales@`-signer documents in their personal tab.
|
||||
*/
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
import { db } from '@/lib/db';
|
||||
import { documents, documentSigners } from '@/lib/db/schema/documents';
|
||||
import { listDocuments } from '@/lib/services/documents.service';
|
||||
import { makePort, makeClient } from '../helpers/factories';
|
||||
|
||||
async function seedPendingDoc(portId: string, clientId: string, signerEmail: string) {
|
||||
const [doc] = await db
|
||||
.insert(documents)
|
||||
.values({
|
||||
portId,
|
||||
clientId,
|
||||
documentType: 'eoi',
|
||||
title: 'EOI awaiting signature',
|
||||
status: 'sent',
|
||||
createdBy: 'seed',
|
||||
})
|
||||
.returning();
|
||||
await db.insert(documentSigners).values({
|
||||
documentId: doc!.id,
|
||||
signerName: 'Approver',
|
||||
signerEmail,
|
||||
signerRole: 'approver',
|
||||
signingOrder: 1,
|
||||
status: 'pending',
|
||||
});
|
||||
return doc!.id;
|
||||
}
|
||||
|
||||
const baseQuery = {
|
||||
page: 1,
|
||||
limit: 50,
|
||||
sort: 'createdAt' as const,
|
||||
order: 'desc' as const,
|
||||
includeArchived: false,
|
||||
tab: 'awaiting_me' as const,
|
||||
};
|
||||
|
||||
describe('documents hub - awaiting_me matches owned email set', () => {
|
||||
it('includes a doc whose pending signer matches the caller signing_email (not their login)', async () => {
|
||||
const port = await makePort();
|
||||
const client = await makeClient({ portId: port.id });
|
||||
const docId = await seedPendingDoc(port.id, client.id, 'sales@portnimara.com');
|
||||
|
||||
const result = await listDocuments(port.id, baseQuery, {
|
||||
currentUserEmails: ['abbie@portnimara.com', 'sales@portnimara.com'],
|
||||
});
|
||||
|
||||
const ids = (result.data as Array<{ id: string }>).map((d) => d.id);
|
||||
expect(ids).toContain(docId);
|
||||
});
|
||||
|
||||
it('excludes the doc when the caller owns neither the signer email', async () => {
|
||||
const port = await makePort();
|
||||
const client = await makeClient({ portId: port.id });
|
||||
const docId = await seedPendingDoc(port.id, client.id, 'sales@portnimara.com');
|
||||
|
||||
const result = await listDocuments(port.id, baseQuery, {
|
||||
currentUserEmails: ['abbie@portnimara.com'],
|
||||
});
|
||||
|
||||
const ids = (result.data as Array<{ id: string }>).map((d) => d.id);
|
||||
expect(ids).not.toContain(docId);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user