All files / realm/blong-access/adapter/db accessSessionClose.ts

91.17% Statements 62/68
54.54% Branches 6/11
100% Functions 1/1
91.17% Lines 62/68

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 691x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 7x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x 1x 1x 1x 1x 1x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x             4x 4x 7x  
import {type IMeta, handler} from '@feasibleone/blong';
 
import * as model from './accessModel.ts';
 
type KnexQb = any;
 
/** The `access.session.close` action id as carried in `$meta.auth.actions`. */
const CLOSE_ACTION_ID = 'access.session.close'.toLowerCase().replaceAll('.', '');
 
/**
 * `access.session.close` — revoke a session.
 *
 * Custom action wired to the "Close Session" toolbar button on the session
 * browse page, and called by `login.token.revoke` (logout).  Sessions are
 * standalone rows (`access_session`), not resource-backed; closing marks the
 * row `isRevoked` + `revokedAt` and clears the restore-cookie hash.
 *
 * Closing the caller's OWN session (the id in `$meta.auth.sessionId`, from the
 * JWT `ses` claim) needs only a valid token.  When no explicit `sessionId` is
 * given the CURRENT session is closed (same as passing your own id).  Closing
 * any OTHER session — an arbitrary id, or by `userId` — requires the
 * `access.session.close` action; otherwise the operation is refused (403).
 *
 * Revocation does NOT immediately invalidate already-issued access tokens
 * (those are authorized by the JWT alone until they expire).  Renewal is
 * refused because `login.token.refresh` / `access.session.verify` reject
 * revoked sessions.
 */
export default handler(({errors}) => ({
    async accessSessionClose(
        params: {sessionId?: string; userId?: string},
        $meta: IMeta,
    ): Promise<{success: boolean}> {
        const auth = $meta?.auth;
        // Target: an explicit session id, else the caller's CURRENT session
        // (the JWT `ses` claim).  Closing your own session needs no permission
        // — a valid token suffices.
        const targetSessionId = params.sessionId ?? auth?.sessionId;
        const ownSession = Boolean(
            targetSessionId && auth?.sessionId && targetSessionId === auth.sessionId,
        );
        if (!ownSession) {
            const hasAction = (auth?.actions ?? []).some(
                action => action.toLowerCase().replaceAll('.', '') === CLOSE_ACTION_ID,
            );
            if (!hasAction) throw errors.sessionCloseForbidden();
        }
        const qb: KnexQb = this.config?.context?.queryBuilder;
        if (!qb) throw new Error('Database not available');
        const q = qb('access_session').update({
            isRevoked: 1,
            revokedAt: new Date(),
            cookieHash: null,
        });
        if (targetSessionId) {
            const hex = model.binHex(targetSessionId);
            if (!hex) return {success: false};
            await q.where('sessionId', Buffer.from(hex, 'hex'));
        } else if (params.userId) {
            const userHex = model.binHex(params.userId);
            if (!userHex) return {success: false};
            await q.where('userId', Buffer.from(userHex, 'hex'));
        } else {
            return {success: false};
        }
        return {success: true};
    },
}));