core: type platform api key timestamps

This commit is contained in:
a.tolmachev
2026-04-13 06:28:58 +00:00
parent c736b4f5a8
commit dddbbac745
8 changed files with 58 additions and 38 deletions
@@ -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"
}
@@ -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"
}
+2 -2
View File
@@ -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(())
}
+1 -3
View File
@@ -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)
+2 -1
View File
@@ -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,
};
+25 -4
View File
@@ -84,8 +84,10 @@ pub struct PlatformApiKey {
pub prefix: String,
pub scopes: Vec<PlatformApiKeyScope>,
pub status: PlatformApiKeyStatus,
pub created_at: String,
pub last_used_at: Option<String>,
#[serde(with = "time::serde::rfc3339")]
pub created_at: OffsetDateTime,
#[serde(with = "time::serde::rfc3339::option")]
pub last_used_at: Option<OffsetDateTime>,
}
#[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"));
}
}
@@ -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
+4 -4
View File
@@ -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",
&timestamp("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;