sofie-identity 0.1.0

Accounts, their area memberships, and signing up, in and out: the identity an app opts into.
Documentation
use sofie::core::Instant;
use sofie::http::{Area, Routes, Shipped};
use sofie::prelude::*;

use super::{AccountSession, Impersonations, ending_all, impersonations_in};
use crate::account_of;
use crate::signing_in::OwnSession;

/// Each principal sees and ends only its own account's sessions.
pub fn routes<A: Area>(routes: &mut Routes<A>) {
    routes
        .shipped(
            "/sessions",
            "sessions",
            (include_str!("screen.html"), None),
            own::<A>,
        )
        .name("list_sessions");
    routes
        .post("/sessions/{account_session}/end", end_one::<A>)
        .name("end_session")
        .then_screen("sessions", "sessions");
    routes
        .post("/sessions/others", end_others::<A>)
        .name("end_other_sessions")
        .then_screen("sessions", "sessions");
}

#[derive(ReadModel)]
#[read(from = AccountSession)]
struct Live {
    id: Id<AccountSession>,
    area: String,
    device: Option<String>,
    last_seen_at: Instant,
    created_at: Instant,
}

async fn own<A: Area>(principal: Principal<A>, read: Read) -> Result<Vec<Live>> {
    Live::query()
        .where_(AccountSession::ACCOUNT.eq(account_of(&principal)))
        .where_(AccountSession::ENDED_AT.is_null())
        .latest()
        .all(&read)
        .await
}

/// Checked here too, since a `Permitted` area may reach records it does not own.
async fn end_one<A: Area>(
    principal: Principal<A>,
    _own: OwnSession,
    mut record: AccountSession,
    read: Read,
    clock: Clock,
) -> Result<(AccountSession, Impersonations)> {
    if record.account != account_of(&principal) {
        return Err(Error::NotFound);
    }
    let now = clock.now();
    record.end(now);
    let impersonations = impersonations_in(&read, std::slice::from_ref(&record), now).await?;
    Ok((record, impersonations))
}

async fn end_others<A: Area>(
    principal: Principal<A>,
    _own: OwnSession,
    session: Session,
    read: Read,
    clock: Clock,
) -> Result<(Vec<AccountSession>, Impersonations)> {
    let now = clock.now();
    let ended = ending_all(&read, account_of(&principal), session.current(), now).await?;
    let impersonations = impersonations_in(&read, &ended, now).await?;
    Ok((ended, impersonations))
}