- The web pings the live connection, reconnects when it goes quiet and reloads what each screen shows after reconnecting. - Dropping a card on a column it cannot go to puts it back. - Owners and admins get a notice when a proposal arrives. - The proposal limit lives in Redis, so it holds across instances. - REST tells whether I voted; pasted images leave the attachment list. - Deleting a project removes its Git integration; no lint warnings left. - Reactions and the blocked badge are components of their own. - Docs: what is done and what is left (chapter 10), Word and PDF. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
25 lines
789 B
TypeScript
25 lines
789 B
TypeScript
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
|
|
import { RateLimitStore } from '@task-stream/api/tracker/application';
|
|
|
|
/** Proposals from one address within the window. */
|
|
const LIMIT = 5;
|
|
const WINDOW_MS = 15 * 60_000;
|
|
|
|
/**
|
|
* Keeps one address from flooding a project with proposals. Counted in
|
|
* Redis, so the limit holds across every instance of the API.
|
|
*/
|
|
@Injectable()
|
|
export class ProposalLimiter {
|
|
constructor(private readonly store: RateLimitStore) {}
|
|
|
|
async take(address: string): Promise<void> {
|
|
const count = await this.store.hit(`proposals:${address}`, WINDOW_MS);
|
|
if (count > LIMIT)
|
|
throw new HttpException(
|
|
'Too many proposals from here; try again in a few minutes',
|
|
HttpStatus.TOO_MANY_REQUESTS,
|
|
);
|
|
}
|
|
}
|