core: type platform api key timestamps
This commit is contained in:
+8
-8
@@ -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"
|
||||
}
|
||||
+8
-8
@@ -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"
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user