mirror of
https://github.com/hanzoai/chat.git
synced 2026-09-30 20:51:17 +00:00
Chat was minting its own credentials on top of an identity IAM had already established: the browser ran the PKCE flow, posted the resulting IAM token to a bridge, and got back a Chat JWT, a refresh cookie and a Session row. Everything after login was authenticated by that second credential, and IAM's was filed away server-side for calls to cloud. The token IAM issues is now the only one. It rides every request as the bearer, is verified against IAM's JWKS on the spot, and is not stored — so there is one credential, and the principal that authenticates here is by construction the principal cloud bills. `resolveTenantBearer` loses the machinery that existed to keep a stored copy honest (principal binding, expiry checks, cookie fallback, the renewal middleware in front of the completion route); a token that IS the request cannot name a different user or outlive the request it arrived on. The audience is now checked. One issuer serves every Hanzo relying party, so a token addressed to another app would otherwise authenticate here on the strength of a shared signature. Removed, because IAM owns them: two-factor enrolment and verification, email verification, password reset, the admin panel's OAuth code exchange, and the server-initiated OpenID redirect. Sign-out now ends the session where it lives — revoking both tokens at the issuer — rather than dropping the local copy and leaving the browser recognised. A guest holds nothing at all. Anonymous chat used to mint a signed guest token, which proved nothing: minting was unauthenticated and the free-message quota keyed on the client address regardless. A request without a bearer is now the guest, identified by that same address, hashed so it stays out of job metadata and logs.
77 lines
2.6 KiB
JavaScript
77 lines
2.6 KiB
JavaScript
const { logger } = require('@hanzochat/data-schemas');
|
|
const { isEnabled, math } = require('@hanzochat/api');
|
|
const { ViolationTypes } = require('@hanzochat/data-provider');
|
|
const { removePorts } = require('~/server/utils');
|
|
const getLogStores = require('./getLogStores');
|
|
|
|
const { BAN_VIOLATIONS, BAN_INTERVAL } = process.env ?? {};
|
|
const interval = math(BAN_INTERVAL, 20);
|
|
|
|
/**
|
|
* Bans a user based on violation criteria.
|
|
*
|
|
* If the user's violation count is a multiple of the BAN_INTERVAL, the user will be banned.
|
|
* The duration of the ban is determined by the BAN_DURATION environment variable.
|
|
* If BAN_DURATION is not set or invalid, the user will not be banned.
|
|
* Sessions will be deleted and the refreshToken cookie will be cleared even with
|
|
* an invalid or nill duration, which is a "soft" ban; the user can remain active until
|
|
* access token expiry.
|
|
*
|
|
* @async
|
|
* @param {Object} req - Express request object containing user information.
|
|
* @param {Object} res - Express response object.
|
|
* @param {Object} errorMessage - Object containing user violation details.
|
|
* @param {string} errorMessage.type - Type of the violation.
|
|
* @param {string} errorMessage.user_id - ID of the user who committed the violation.
|
|
* @param {number} errorMessage.violation_count - Number of violations committed by the user.
|
|
*
|
|
* @returns {Promise<void>}
|
|
*
|
|
*/
|
|
const banViolation = async (req, res, errorMessage) => {
|
|
if (!isEnabled(BAN_VIOLATIONS)) {
|
|
return;
|
|
}
|
|
if (!errorMessage) {
|
|
return;
|
|
}
|
|
|
|
const { type, user_id, prev_count, violation_count } = errorMessage;
|
|
|
|
const prevThreshold = Math.floor(prev_count / interval);
|
|
const currentThreshold = Math.floor(violation_count / interval);
|
|
|
|
if (prevThreshold >= currentThreshold) {
|
|
return;
|
|
}
|
|
|
|
/* The ban itself is the enforcement: `checkBan` reads this store on every
|
|
request the banned account can make, so there is no session to tear down
|
|
and no cookie to clear. */
|
|
const banLogs = getLogStores(ViolationTypes.BAN);
|
|
const duration = errorMessage.duration || banLogs.opts.ttl;
|
|
if (duration <= 0) {
|
|
return;
|
|
}
|
|
|
|
req.ip = removePorts(req);
|
|
logger.info(
|
|
`[BAN] Banning user ${user_id} ${req.ip ? `@ ${req.ip} ` : ''}for ${
|
|
duration / 1000 / 60
|
|
} minutes`,
|
|
);
|
|
|
|
const expiresAt = Date.now() + duration;
|
|
await banLogs.set(user_id, { type, violation_count, duration, expiresAt });
|
|
if (req.ip) {
|
|
await banLogs.set(req.ip, { type, user_id, violation_count, duration, expiresAt });
|
|
}
|
|
|
|
errorMessage.ban = true;
|
|
errorMessage.ban_duration = duration;
|
|
|
|
return;
|
|
};
|
|
|
|
module.exports = banViolation;
|