mirror of
https://github.com/azaion/admin.git
synced 2026-06-21 23:11:09 +00:00
[AZ-535] [AZ-533] Logout/revocation surface + UAV mission tokens
AZ-535: POST /logout (caller's session), /logout/all (all sessions for user),
admin POST /sessions/{sid}/revoke, and verifier-only GET /sessions/revoked
snapshot. New Service role gates the snapshot. Idempotent revoke; reason +
revoked_by_user_id audited per row.
AZ-533: POST /sessions/mission mints a long-lived no-refresh ES256 token bound
to one aircraft + one mission. Audience narrowed to satellite-provider, hard
12 h cap, persisted as class='mission' so the existing logout/revoke surface
covers it. Successful CompanionPC /login or /token/refresh auto-revokes that
aircraft's open mission session (post-flight reconnect).
Schema: 09_sessions_logout_and_mission.sql adds revoked_by_user_id, class,
aircraft_id; drops NOT NULL on refresh_hash for mission rows; adds two partial
indexes for the auto-revoke and snapshot hot paths.
Tests: 13 new e2e tests, all green; full suite 75/76 (1 pre-existing flake in
PasswordHashingTests AC5 timing assertion, unrelated to this batch).
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -193,6 +193,66 @@ public sealed class DbHelper
|
||||
await cmd.ExecuteNonQueryAsync(ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// AZ-535 — count active sessions for a user, optionally filtered to a session class.
|
||||
/// </summary>
|
||||
public async Task<int> CountActiveSessionsForUser(string email, string? sessionClass = null, CancellationToken ct = default)
|
||||
{
|
||||
await using var conn = await OpenAsync(ct);
|
||||
var sql = @"
|
||||
SELECT COUNT(*) FROM public.sessions
|
||||
WHERE user_id = (SELECT id FROM public.users WHERE email = @e)
|
||||
AND revoked_at IS NULL"
|
||||
+ (sessionClass != null ? " AND class = @c" : "");
|
||||
await using var cmd = new NpgsqlCommand(sql, conn);
|
||||
cmd.Parameters.AddWithValue("e", email);
|
||||
if (sessionClass != null) cmd.Parameters.AddWithValue("c", sessionClass);
|
||||
return Convert.ToInt32(await cmd.ExecuteScalarAsync(ct), CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// AZ-533 — count open mission sessions whose <c>aircraft_id</c> matches the given user.
|
||||
/// </summary>
|
||||
public async Task<int> CountOpenMissionsForAircraft(Guid aircraftId, CancellationToken ct = default)
|
||||
{
|
||||
await using var conn = await OpenAsync(ct);
|
||||
await using var cmd = new NpgsqlCommand(@"
|
||||
SELECT COUNT(*) FROM public.sessions
|
||||
WHERE aircraft_id = @a AND class = 'mission' AND revoked_at IS NULL", conn);
|
||||
cmd.Parameters.AddWithValue("a", aircraftId);
|
||||
return Convert.ToInt32(await cmd.ExecuteScalarAsync(ct), CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// AZ-535 — pluck the row's revocation columns for assertions on who/why/when.
|
||||
/// </summary>
|
||||
public async Task<(DateTime? RevokedAt, string? Reason, Guid? RevokedBy)> GetRevocationInfo(Guid sessionId, CancellationToken ct = default)
|
||||
{
|
||||
await using var conn = await OpenAsync(ct);
|
||||
await using var cmd = new NpgsqlCommand(
|
||||
"SELECT revoked_at, revoked_reason, revoked_by_user_id FROM public.sessions WHERE id = @s", conn);
|
||||
cmd.Parameters.AddWithValue("s", sessionId);
|
||||
await using var rd = await cmd.ExecuteReaderAsync(ct);
|
||||
if (!await rd.ReadAsync(ct))
|
||||
throw new InvalidOperationException($"Session {sessionId} not found.");
|
||||
return (
|
||||
rd.IsDBNull(0) ? null : DateTime.SpecifyKind(rd.GetDateTime(0), DateTimeKind.Utc),
|
||||
rd.IsDBNull(1) ? null : rd.GetString(1),
|
||||
rd.IsDBNull(2) ? null : rd.GetGuid(2));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// AZ-535 — promote a user to <c>Service</c> role so they can read /sessions/revoked.
|
||||
/// </summary>
|
||||
public async Task PromoteToService(string email, CancellationToken ct = default)
|
||||
{
|
||||
await using var conn = await OpenAsync(ct);
|
||||
await using var cmd = new NpgsqlCommand(
|
||||
"UPDATE public.users SET role = 'Service' WHERE email = @e", conn);
|
||||
cmd.Parameters.AddWithValue("e", email);
|
||||
await cmd.ExecuteNonQueryAsync(ct);
|
||||
}
|
||||
|
||||
public static string HashRefreshToken(string opaqueToken)
|
||||
{
|
||||
var bytes = System.Text.Encoding.ASCII.GetBytes(opaqueToken);
|
||||
|
||||
Reference in New Issue
Block a user