nickserv: account SUSPEND / UNSUSPEND (oper-gated)

Freezes an account (data kept, login blocked) gated on the typed Priv::Suspend.
A typed Suspension{by,reason,ts,expires} on the account, folded through the
event log (persists, replays, federates) -- no stringly Extensible/Checker like
Anope. Expiry is evaluated lazily at check-time, so there is no expiry timer.
Blocks IDENTIFY and all SASL mechanisms (via one sasl_login gate); logs out any
live sessions on suspend; the login refusal names who/when/why and when it
lifts. Shown in INFO to owner/auspex. Data + full-flow tests.
This commit is contained in:
Jean Chevronnet 2026-07-13 04:01:30 +00:00
parent f1415bedb2
commit bbbe2c6cb8
No known key found for this signature in database
8 changed files with 273 additions and 9 deletions

View file

@ -1,4 +1,4 @@
use fedserv_api::Store;
use fedserv_api::{human_time, Store};
use fedserv_api::{Sender, ServiceCtx};
// IDENTIFY [account] <password>: log in. The account defaults to the current
@ -17,6 +17,27 @@ pub fn handle(me: &str, from: &Sender, args: &[&str], ctx: &mut ServiceCtx, db:
ctx.notice(me, from.uid, format!("\x02{account_name}\x02 isn't registered."));
return;
}
// A suspended account can't be logged into (checked before the password so it
// doesn't reveal whether the password was right). Tell them who, when and why,
// and when it lifts, so they know what happened and who to reach.
if let Some(acc) = db.resolve_account(account_name).map(str::to_string) {
if db.is_suspended(&acc) {
if let Some(s) = db.suspension(&acc) {
let mut msg = format!("\x02{acc}\x02 is suspended, so you can't log in to it right now. It was suspended by \x02{}\x02 on {}", s.by, human_time(s.ts));
if s.reason.trim().is_empty() {
msg.push('.');
} else {
msg.push_str(&format!(" — reason: {}", s.reason));
}
if let Some(exp) = s.expires {
msg.push_str(&format!(" The suspension is due to lift on {}.", human_time(exp)));
}
msg.push_str(" If you think this is a mistake, please contact the network staff.");
ctx.notice(me, from.uid, msg);
}
return;
}
}
// Refuse while throttled, so a password can't be brute-forced.
if let Some(secs) = db.auth_lockout(account_name) {
ctx.notice(me, from.uid, format!("Too many failed attempts. Please wait {secs}s and try again."));