feat(session): add pairing_approved table and getPairingStore()
This commit is contained in:
@@ -62,4 +62,42 @@ describe('SessionStore', () => {
|
|||||||
expect(sessions).toContain('session-a');
|
expect(sessions).toContain('session-a');
|
||||||
expect(sessions).toContain('session-b');
|
expect(sessions).toContain('session-b');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('pairing persistence', () => {
|
||||||
|
it('getPairingStore returns a PairingStore', () => {
|
||||||
|
const pairingStore = store.getPairingStore();
|
||||||
|
expect(pairingStore).toBeDefined();
|
||||||
|
expect(pairingStore.loadApproved).toBeInstanceOf(Function);
|
||||||
|
expect(pairingStore.saveApproved).toBeInstanceOf(Function);
|
||||||
|
expect(pairingStore.removeApproved).toBeInstanceOf(Function);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('saveApproved and loadApproved round-trip', () => {
|
||||||
|
const ps = store.getPairingStore();
|
||||||
|
ps.saveApproved({ channel: 'telegram', senderId: '123', approvedAt: 1000, codeUsed: 'AABB11' });
|
||||||
|
ps.saveApproved({ channel: 'discord', senderId: '456', approvedAt: 2000, codeUsed: 'CC33DD' });
|
||||||
|
const loaded = ps.loadApproved();
|
||||||
|
expect(loaded).toHaveLength(2);
|
||||||
|
expect(loaded).toEqual(expect.arrayContaining([
|
||||||
|
expect.objectContaining({ channel: 'telegram', senderId: '123', codeUsed: 'AABB11' }),
|
||||||
|
expect.objectContaining({ channel: 'discord', senderId: '456', codeUsed: 'CC33DD' }),
|
||||||
|
]));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('removeApproved deletes a sender', () => {
|
||||||
|
const ps = store.getPairingStore();
|
||||||
|
ps.saveApproved({ channel: 'telegram', senderId: '123', approvedAt: 1000, codeUsed: 'AABB11' });
|
||||||
|
ps.removeApproved('telegram', '123');
|
||||||
|
expect(ps.loadApproved()).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('saveApproved upserts on duplicate channel+senderId', () => {
|
||||||
|
const ps = store.getPairingStore();
|
||||||
|
ps.saveApproved({ channel: 'telegram', senderId: '123', approvedAt: 1000, codeUsed: 'FIRST1' });
|
||||||
|
ps.saveApproved({ channel: 'telegram', senderId: '123', approvedAt: 2000, codeUsed: 'SECOND' });
|
||||||
|
const loaded = ps.loadApproved();
|
||||||
|
expect(loaded).toHaveLength(1);
|
||||||
|
expect(loaded[0].codeUsed).toBe('SECOND');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import Database from 'better-sqlite3';
|
import Database from 'better-sqlite3';
|
||||||
import type { Message } from '../models/types.js';
|
import type { Message } from '../models/types.js';
|
||||||
|
import type { PairingStore, ApprovedSender } from '../channels/pairing.js';
|
||||||
|
|
||||||
/** Parse a duration string like '30d', '7d', '12h' to milliseconds. Returns null if invalid or '0'. */
|
/** Parse a duration string like '30d', '7d', '12h' to milliseconds. Returns null if invalid or '0'. */
|
||||||
export function parseDuration(s: string): number | null {
|
export function parseDuration(s: string): number | null {
|
||||||
@@ -28,6 +29,13 @@ export class SessionStore {
|
|||||||
created_at INTEGER NOT NULL DEFAULT (unixepoch())
|
created_at INTEGER NOT NULL DEFAULT (unixepoch())
|
||||||
);
|
);
|
||||||
CREATE INDEX IF NOT EXISTS idx_messages_session ON messages(session_id);
|
CREATE INDEX IF NOT EXISTS idx_messages_session ON messages(session_id);
|
||||||
|
CREATE TABLE IF NOT EXISTS pairing_approved (
|
||||||
|
channel TEXT NOT NULL,
|
||||||
|
sender_id TEXT NOT NULL,
|
||||||
|
approved_at INTEGER NOT NULL,
|
||||||
|
code_used TEXT NOT NULL,
|
||||||
|
PRIMARY KEY (channel, sender_id)
|
||||||
|
);
|
||||||
`);
|
`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,6 +109,33 @@ export class SessionStore {
|
|||||||
return stale.map(r => r.session_id);
|
return stale.map(r => r.session_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getPairingStore(): PairingStore {
|
||||||
|
return {
|
||||||
|
loadApproved: (): ApprovedSender[] => {
|
||||||
|
const rows = this.db.prepare(
|
||||||
|
'SELECT channel, sender_id, approved_at, code_used FROM pairing_approved'
|
||||||
|
).all() as Array<{ channel: string; sender_id: string; approved_at: number; code_used: string }>;
|
||||||
|
return rows.map(r => ({
|
||||||
|
channel: r.channel,
|
||||||
|
senderId: r.sender_id,
|
||||||
|
approvedAt: r.approved_at,
|
||||||
|
codeUsed: r.code_used,
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
saveApproved: (sender: ApprovedSender): void => {
|
||||||
|
this.db.prepare(`
|
||||||
|
INSERT OR REPLACE INTO pairing_approved (channel, sender_id, approved_at, code_used)
|
||||||
|
VALUES (?, ?, ?, ?)
|
||||||
|
`).run(sender.channel, sender.senderId, sender.approvedAt, sender.codeUsed);
|
||||||
|
},
|
||||||
|
removeApproved: (channel: string, senderId: string): void => {
|
||||||
|
this.db.prepare(
|
||||||
|
'DELETE FROM pairing_approved WHERE channel = ? AND sender_id = ?'
|
||||||
|
).run(channel, senderId);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
close(): void {
|
close(): void {
|
||||||
this.db.close();
|
this.db.close();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user