From dddbbac74506ca0eff42b159ddac2b88607f0507 Mon Sep 17 00:00:00 2001 From: "a.tolmachev" Date: Mon, 13 Apr 2026 06:28:58 +0000 Subject: [PATCH] core: type platform api key timestamps --- ...1f8d7aa7b7624c64185fe0d19a3b2bd7d713.json} | 16 +++++----- ...35573e1d3f0111fe9f61195bcfab87684959.json} | 16 +++++----- apps/admin-api/src/service.rs | 4 +-- apps/mcp-server/src/app.rs | 4 +-- apps/mcp-server/src/main.rs | 3 +- crates/crank-core/src/access.rs | 29 ++++++++++++++++--- crates/crank-registry/src/postgres/api_key.rs | 16 +++++----- crates/crank-registry/src/postgres/mod.rs | 8 ++--- 8 files changed, 58 insertions(+), 38 deletions(-) rename .sqlx/{query-2b0b383bd209c6a94d6a422fbd52bf63338c048238c3ff63c44eddf6a56b8a0b.json => query-213432827436e3a17fb2c17e392c1f8d7aa7b7624c64185fe0d19a3b2bd7d713.json} (62%) rename .sqlx/{query-ed3c533baee2cdec1fabc3e3cece29939fee23fdf54dc4848a0951f3dd274f1f.json => query-eeab4c7f8abeb26554b340cd5ed935573e1d3f0111fe9f61195bcfab87684959.json} (62%) diff --git a/.sqlx/query-2b0b383bd209c6a94d6a422fbd52bf63338c048238c3ff63c44eddf6a56b8a0b.json b/.sqlx/query-213432827436e3a17fb2c17e392c1f8d7aa7b7624c64185fe0d19a3b2bd7d713.json similarity index 62% rename from .sqlx/query-2b0b383bd209c6a94d6a422fbd52bf63338c048238c3ff63c44eddf6a56b8a0b.json rename to .sqlx/query-213432827436e3a17fb2c17e392c1f8d7aa7b7624c64185fe0d19a3b2bd7d713.json index 6e246ae..03fe841 100644 --- a/.sqlx/query-2b0b383bd209c6a94d6a422fbd52bf63338c048238c3ff63c44eddf6a56b8a0b.json +++ b/.sqlx/query-213432827436e3a17fb2c17e392c1f8d7aa7b7624c64185fe0d19a3b2bd7d713.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "select\n k.id,\n k.workspace_id,\n k.name,\n k.prefix,\n k.scopes_json,\n k.status,\n to_char(k.created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as \"created_at!\",\n to_char(k.last_used_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as last_used_at\n from platform_api_keys k\n join workspaces w on w.id = k.workspace_id\n where w.slug = $1\n and k.secret_hash = $2\n and k.status = 'active'\n limit 1", + "query": "select\n k.id,\n k.workspace_id,\n k.name,\n k.prefix,\n k.scopes_json,\n k.status,\n k.created_at as \"created_at!: time::OffsetDateTime\",\n k.last_used_at as \"last_used_at: time::OffsetDateTime\"\n from platform_api_keys k\n join workspaces w on w.id = k.workspace_id\n where w.slug = $1\n and k.secret_hash = $2\n and k.status = 'active'\n limit 1", "describe": { "columns": [ { @@ -35,13 +35,13 @@ }, { "ordinal": 6, - "name": "created_at!", - "type_info": "Text" + "name": "created_at!: time::OffsetDateTime", + "type_info": "Timestamptz" }, { "ordinal": 7, - "name": "last_used_at", - "type_info": "Text" + "name": "last_used_at: time::OffsetDateTime", + "type_info": "Timestamptz" } ], "parameters": { @@ -57,9 +57,9 @@ false, false, false, - null, - null + false, + true ] }, - "hash": "2b0b383bd209c6a94d6a422fbd52bf63338c048238c3ff63c44eddf6a56b8a0b" + "hash": "213432827436e3a17fb2c17e392c1f8d7aa7b7624c64185fe0d19a3b2bd7d713" } diff --git a/.sqlx/query-ed3c533baee2cdec1fabc3e3cece29939fee23fdf54dc4848a0951f3dd274f1f.json b/.sqlx/query-eeab4c7f8abeb26554b340cd5ed935573e1d3f0111fe9f61195bcfab87684959.json similarity index 62% rename from .sqlx/query-ed3c533baee2cdec1fabc3e3cece29939fee23fdf54dc4848a0951f3dd274f1f.json rename to .sqlx/query-eeab4c7f8abeb26554b340cd5ed935573e1d3f0111fe9f61195bcfab87684959.json index 0165a56..a4bda08 100644 --- a/.sqlx/query-ed3c533baee2cdec1fabc3e3cece29939fee23fdf54dc4848a0951f3dd274f1f.json +++ b/.sqlx/query-eeab4c7f8abeb26554b340cd5ed935573e1d3f0111fe9f61195bcfab87684959.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "select\n id,\n workspace_id,\n name,\n prefix,\n scopes_json,\n status,\n to_char(created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as \"created_at!\",\n to_char(last_used_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as last_used_at\n from platform_api_keys\n where workspace_id = $1\n order by created_at desc", + "query": "select\n id,\n workspace_id,\n name,\n prefix,\n scopes_json,\n status,\n created_at as \"created_at!: time::OffsetDateTime\",\n last_used_at as \"last_used_at: time::OffsetDateTime\"\n from platform_api_keys\n where workspace_id = $1\n order by created_at desc", "describe": { "columns": [ { @@ -35,13 +35,13 @@ }, { "ordinal": 6, - "name": "created_at!", - "type_info": "Text" + "name": "created_at!: time::OffsetDateTime", + "type_info": "Timestamptz" }, { "ordinal": 7, - "name": "last_used_at", - "type_info": "Text" + "name": "last_used_at: time::OffsetDateTime", + "type_info": "Timestamptz" } ], "parameters": { @@ -56,9 +56,9 @@ false, false, false, - null, - null + false, + true ] }, - "hash": "ed3c533baee2cdec1fabc3e3cece29939fee23fdf54dc4848a0951f3dd274f1f" + "hash": "eeab4c7f8abeb26554b340cd5ed935573e1d3f0111fe9f61195bcfab87684959" } diff --git a/apps/admin-api/src/service.rs b/apps/admin-api/src/service.rs index c91efbf..fc0d959 100644 --- a/apps/admin-api/src/service.rs +++ b/apps/admin-api/src/service.rs @@ -1297,7 +1297,7 @@ impl AdminService { prefix: secret.chars().take(16).collect(), scopes: payload.scopes, status: PlatformApiKeyStatus::Active, - created_at: now_string()?, + created_at: OffsetDateTime::now_utc(), last_used_at: None, }, }; @@ -1319,7 +1319,7 @@ impl AdminService { key_id: &PlatformApiKeyId, ) -> Result<(), ApiError> { self.registry - .revoke_platform_api_key(workspace_id, key_id, &now_string()?) + .revoke_platform_api_key(workspace_id, key_id, &OffsetDateTime::now_utc()) .await?; Ok(()) } diff --git a/apps/mcp-server/src/app.rs b/apps/mcp-server/src/app.rs index 647cb6c..988a100 100644 --- a/apps/mcp-server/src/app.rs +++ b/apps/mcp-server/src/app.rs @@ -1584,9 +1584,7 @@ async fn require_platform_api_key( return Err(StatusCode::FORBIDDEN); } - let used_at = OffsetDateTime::now_utc() - .format(&Rfc3339) - .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + let used_at = OffsetDateTime::now_utc(); state .registry .touch_platform_api_key(&api_key.api_key.workspace_id, &api_key.api_key.id, &used_at) diff --git a/apps/mcp-server/src/main.rs b/apps/mcp-server/src/main.rs index abf2d4f..0b7ec1b 100644 --- a/apps/mcp-server/src/main.rs +++ b/apps/mcp-server/src/main.rs @@ -107,6 +107,7 @@ mod tests { use serde_json::{Value, json}; use sha2::{Digest, Sha256}; use sqlx::{Executor, postgres::PgPoolOptions}; + use time::{OffsetDateTime, format_description::well_known::Rfc3339}; use tokio::net::TcpListener; use tokio::time::sleep; @@ -1546,7 +1547,7 @@ mod tests { prefix: secret.chars().take(16).collect(), scopes: scopes.to_vec(), status: PlatformApiKeyStatus::Active, - created_at: "2026-03-26T10:00:00Z".to_owned(), + created_at: OffsetDateTime::parse("2026-03-26T10:00:00Z", &Rfc3339).unwrap(), last_used_at: None, }; diff --git a/crates/crank-core/src/access.rs b/crates/crank-core/src/access.rs index a7aab1b..31a9e9c 100644 --- a/crates/crank-core/src/access.rs +++ b/crates/crank-core/src/access.rs @@ -84,8 +84,10 @@ pub struct PlatformApiKey { pub prefix: String, pub scopes: Vec, pub status: PlatformApiKeyStatus, - pub created_at: String, - pub last_used_at: Option, + #[serde(with = "time::serde::rfc3339")] + pub created_at: OffsetDateTime, + #[serde(with = "time::serde::rfc3339::option")] + pub last_used_at: Option, } #[cfg(test)] @@ -93,8 +95,8 @@ mod tests { use serde_json::json; use time::{OffsetDateTime, format_description::well_known::Rfc3339}; - use super::{User, UserStatus}; - use crate::ids::UserId; + use super::{PlatformApiKey, PlatformApiKeyScope, PlatformApiKeyStatus, User, UserStatus}; + use crate::ids::{PlatformApiKeyId, UserId, WorkspaceId}; #[test] fn user_serializes_created_at_as_rfc3339() { @@ -127,4 +129,23 @@ mod tests { OffsetDateTime::parse("2026-03-25T12:00:00Z", &Rfc3339).unwrap() ); } + + #[test] + fn platform_api_key_serializes_timestamps_as_rfc3339() { + let api_key = PlatformApiKey { + id: PlatformApiKeyId::new("pk_01"), + workspace_id: WorkspaceId::new("ws_01"), + name: "Primary".to_owned(), + prefix: "crk_live".to_owned(), + scopes: vec![PlatformApiKeyScope::Read, PlatformApiKeyScope::Write], + status: PlatformApiKeyStatus::Active, + created_at: OffsetDateTime::parse("2026-03-25T12:01:00Z", &Rfc3339).unwrap(), + last_used_at: Some(OffsetDateTime::parse("2026-03-25T12:05:00Z", &Rfc3339).unwrap()), + }; + + let value = serde_json::to_value(&api_key).unwrap(); + + assert_eq!(value["created_at"], json!("2026-03-25T12:01:00Z")); + assert_eq!(value["last_used_at"], json!("2026-03-25T12:05:00Z")); + } } diff --git a/crates/crank-registry/src/postgres/api_key.rs b/crates/crank-registry/src/postgres/api_key.rs index 7e0eb80..0c9cfec 100644 --- a/crates/crank-registry/src/postgres/api_key.rs +++ b/crates/crank-registry/src/postgres/api_key.rs @@ -13,8 +13,8 @@ impl PostgresRegistry { prefix, scopes_json, status, - to_char(created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as \"created_at!\", - to_char(last_used_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as last_used_at + created_at as \"created_at!: time::OffsetDateTime\", + last_used_at as \"last_used_at: time::OffsetDateTime\" from platform_api_keys where workspace_id = $1 order by created_at desc", @@ -54,8 +54,8 @@ impl PostgresRegistry { k.prefix, k.scopes_json, k.status, - to_char(k.created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as \"created_at!\", - to_char(k.last_used_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as last_used_at + k.created_at as \"created_at!: time::OffsetDateTime\", + k.last_used_at as \"last_used_at: time::OffsetDateTime\" from platform_api_keys k join workspaces w on w.id = k.workspace_id where w.slug = $1 @@ -112,8 +112,8 @@ impl PostgresRegistry { .bind(request.secret_hash) .bind(Json(serialize_json_value(&request.api_key.scopes)?)) .bind(serialize_enum_text(&request.api_key.status, "status")?) - .bind(&request.api_key.created_at) - .bind(request.api_key.last_used_at.as_deref()) + .bind(request.api_key.created_at) + .bind(request.api_key.last_used_at) .bind(Option::<&str>::None) .execute(&self.pool) .await; @@ -133,7 +133,7 @@ impl PostgresRegistry { &self, workspace_id: &WorkspaceId, key_id: &PlatformApiKeyId, - revoked_at: &str, + revoked_at: &time::OffsetDateTime, ) -> Result<(), RegistryError> { let result = sqlx::query( "update platform_api_keys @@ -185,7 +185,7 @@ impl PostgresRegistry { &self, workspace_id: &WorkspaceId, key_id: &PlatformApiKeyId, - used_at: &str, + used_at: &time::OffsetDateTime, ) -> Result<(), RegistryError> { let result = sqlx::query( "update platform_api_keys diff --git a/crates/crank-registry/src/postgres/mod.rs b/crates/crank-registry/src/postgres/mod.rs index 7a0d364..85a0bd4 100644 --- a/crates/crank-registry/src/postgres/mod.rs +++ b/crates/crank-registry/src/postgres/mod.rs @@ -1676,7 +1676,7 @@ mod tests { prefix: "crk_live".to_owned(), scopes: vec![PlatformApiKeyScope::Read, PlatformApiKeyScope::Write], status: PlatformApiKeyStatus::Active, - created_at: "2026-03-25T12:01:00Z".to_owned(), + created_at: timestamp("2026-03-25T12:01:00Z"), last_used_at: None, }; let secret_hash = "secret_hash_01"; @@ -1717,7 +1717,7 @@ mod tests { .touch_platform_api_key( &workspace.id, &PlatformApiKeyId::new("key_01"), - "2026-03-25T12:05:00Z", + ×tamp("2026-03-25T12:05:00Z"), ) .await .unwrap(); @@ -1727,8 +1727,8 @@ mod tests { .await .unwrap(); assert_eq!( - touched[0].api_key.last_used_at.as_deref(), - Some("2026-03-25T12:05:00Z") + touched[0].api_key.last_used_at, + Some(timestamp("2026-03-25T12:05:00Z")) ); database.cleanup().await;