From edf318ba67f7106e0cec45400260de338a04356d Mon Sep 17 00:00:00 2001 From: "a.tolmachev" Date: Sun, 12 Apr 2026 22:20:19 +0000 Subject: [PATCH] core: type auth profile timestamps --- ...1e19377582ec0ca245a162c2d2236077bb9b.json} | 16 ++++---- ...93eb1d1ad17505b1e1609a842750e3233ec2.json} | 16 ++++---- Cargo.lock | 1 + apps/admin-api/src/service.rs | 4 +- crates/crank-core/src/auth.rs | 40 ++++++++++++++++++- crates/crank-core/src/operation.rs | 9 ++++- crates/crank-registry/src/postgres/auth.rs | 12 +++--- crates/crank-registry/src/postgres/mod.rs | 12 +++--- crates/crank-runtime/Cargo.toml | 1 + crates/crank-runtime/src/auth.rs | 21 ++++++---- 10 files changed, 90 insertions(+), 42 deletions(-) rename .sqlx/{query-10d71ae9e9d2c39546ab0075809d0c17742dd5b6377f40704dfec1c76f51f839.json => query-b86a7406eeec83390458e35fb8611e19377582ec0ca245a162c2d2236077bb9b.json} (61%) rename .sqlx/{query-f51d113d5f026b0b11e086cc5a37578dcc6977ce6eb90a8fc3afe63023598c06.json => query-fadf243775a355c46ae97d26049793eb1d1ad17505b1e1609a842750e3233ec2.json} (62%) diff --git a/.sqlx/query-10d71ae9e9d2c39546ab0075809d0c17742dd5b6377f40704dfec1c76f51f839.json b/.sqlx/query-b86a7406eeec83390458e35fb8611e19377582ec0ca245a162c2d2236077bb9b.json similarity index 61% rename from .sqlx/query-10d71ae9e9d2c39546ab0075809d0c17742dd5b6377f40704dfec1c76f51f839.json rename to .sqlx/query-b86a7406eeec83390458e35fb8611e19377582ec0ca245a162c2d2236077bb9b.json index 8cf469b..e74aa00 100644 --- a/.sqlx/query-10d71ae9e9d2c39546ab0075809d0c17742dd5b6377f40704dfec1c76f51f839.json +++ b/.sqlx/query-b86a7406eeec83390458e35fb8611e19377582ec0ca245a162c2d2236077bb9b.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "select\n id,\n workspace_id,\n name,\n kind,\n config_json,\n to_char(created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as \"created_at!\",\n to_char(updated_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as \"updated_at!\"\n from auth_profiles\n where workspace_id = $1\n order by name asc", + "query": "select\n id,\n workspace_id,\n name,\n kind,\n config_json,\n created_at as \"created_at!: OffsetDateTime\",\n updated_at as \"updated_at!: OffsetDateTime\"\n from auth_profiles\n where workspace_id = $1\n order by name asc", "describe": { "columns": [ { @@ -30,13 +30,13 @@ }, { "ordinal": 5, - "name": "created_at!", - "type_info": "Text" + "name": "created_at!: OffsetDateTime", + "type_info": "Timestamptz" }, { "ordinal": 6, - "name": "updated_at!", - "type_info": "Text" + "name": "updated_at!: OffsetDateTime", + "type_info": "Timestamptz" } ], "parameters": { @@ -50,9 +50,9 @@ false, false, false, - null, - null + false, + false ] }, - "hash": "10d71ae9e9d2c39546ab0075809d0c17742dd5b6377f40704dfec1c76f51f839" + "hash": "b86a7406eeec83390458e35fb8611e19377582ec0ca245a162c2d2236077bb9b" } diff --git a/.sqlx/query-f51d113d5f026b0b11e086cc5a37578dcc6977ce6eb90a8fc3afe63023598c06.json b/.sqlx/query-fadf243775a355c46ae97d26049793eb1d1ad17505b1e1609a842750e3233ec2.json similarity index 62% rename from .sqlx/query-f51d113d5f026b0b11e086cc5a37578dcc6977ce6eb90a8fc3afe63023598c06.json rename to .sqlx/query-fadf243775a355c46ae97d26049793eb1d1ad17505b1e1609a842750e3233ec2.json index ca2fcf8..33bcc6e 100644 --- a/.sqlx/query-f51d113d5f026b0b11e086cc5a37578dcc6977ce6eb90a8fc3afe63023598c06.json +++ b/.sqlx/query-fadf243775a355c46ae97d26049793eb1d1ad17505b1e1609a842750e3233ec2.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "select\n id,\n workspace_id,\n name,\n kind,\n config_json,\n to_char(created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as \"created_at!\",\n to_char(updated_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as \"updated_at!\"\n from auth_profiles\n where workspace_id = $1 and id = $2", + "query": "select\n id,\n workspace_id,\n name,\n kind,\n config_json,\n created_at as \"created_at!: OffsetDateTime\",\n updated_at as \"updated_at!: OffsetDateTime\"\n from auth_profiles\n where workspace_id = $1 and id = $2", "describe": { "columns": [ { @@ -30,13 +30,13 @@ }, { "ordinal": 5, - "name": "created_at!", - "type_info": "Text" + "name": "created_at!: OffsetDateTime", + "type_info": "Timestamptz" }, { "ordinal": 6, - "name": "updated_at!", - "type_info": "Text" + "name": "updated_at!: OffsetDateTime", + "type_info": "Timestamptz" } ], "parameters": { @@ -51,9 +51,9 @@ false, false, false, - null, - null + false, + false ] }, - "hash": "f51d113d5f026b0b11e086cc5a37578dcc6977ce6eb90a8fc3afe63023598c06" + "hash": "fadf243775a355c46ae97d26049793eb1d1ad17505b1e1609a842750e3233ec2" } diff --git a/Cargo.lock b/Cargo.lock index 2ad2bd6..b662802 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -501,6 +501,7 @@ dependencies = [ "serde_json", "sha2", "thiserror", + "time", "tokio", "tokio-tungstenite", ] diff --git a/apps/admin-api/src/service.rs b/apps/admin-api/src/service.rs index a771c3a..2eb8ad2 100644 --- a/apps/admin-api/src/service.rs +++ b/apps/admin-api/src/service.rs @@ -3225,14 +3225,14 @@ impl AdminService { self.validate_auth_profile_secret_ids(workspace_id, &payload.config) .await?; - let now = now_string()?; + let now = OffsetDateTime::now_utc(); let profile = AuthProfile { id: AuthProfileId::new(new_prefixed_id("auth")), workspace_id: workspace_id.clone(), name: payload.name, kind: payload.kind, config: payload.config, - created_at: now.clone(), + created_at: now, updated_at: now, }; diff --git a/crates/crank-core/src/auth.rs b/crates/crank-core/src/auth.rs index 58f1d26..e9b1f8b 100644 --- a/crates/crank-core/src/auth.rs +++ b/crates/crank-core/src/auth.rs @@ -1,4 +1,5 @@ use serde::{Deserialize, Serialize}; +use time::OffsetDateTime; use crate::{ ids::{AuthProfileId, SecretId, WorkspaceId}, @@ -56,6 +57,41 @@ pub struct AuthProfile { pub name: String, pub kind: AuthKind, pub config: AuthConfig, - pub created_at: String, - pub updated_at: String, + #[serde(with = "time::serde::rfc3339")] + pub created_at: OffsetDateTime, + #[serde(with = "time::serde::rfc3339")] + pub updated_at: OffsetDateTime, +} + +#[cfg(test)] +mod tests { + use serde_json::json; + use time::{OffsetDateTime, format_description::well_known::Rfc3339}; + + use super::{ApiKeyHeaderAuthConfig, AuthConfig, AuthProfile}; + use crate::{ + ids::{AuthProfileId, SecretId, WorkspaceId}, + protocol::AuthKind, + }; + + #[test] + fn auth_profile_serializes_timestamps_as_rfc3339() { + let profile = AuthProfile { + id: AuthProfileId::new("auth_01"), + workspace_id: WorkspaceId::new("ws_default"), + name: "header".to_owned(), + kind: AuthKind::ApiKeyHeader, + config: AuthConfig::ApiKeyHeader(ApiKeyHeaderAuthConfig { + header_name: "X-Api-Key".to_owned(), + secret_id: SecretId::new("secret_01"), + }), + created_at: OffsetDateTime::parse("2026-03-25T12:00:00Z", &Rfc3339).unwrap(), + updated_at: OffsetDateTime::parse("2026-03-25T12:05:00Z", &Rfc3339).unwrap(), + }; + + let value = serde_json::to_value(&profile).unwrap(); + + assert_eq!(value["created_at"], json!("2026-03-25T12:00:00Z")); + assert_eq!(value["updated_at"], json!("2026-03-25T12:05:00Z")); + } } diff --git a/crates/crank-core/src/operation.rs b/crates/crank-core/src/operation.rs index ef01137..5154123 100644 --- a/crates/crank-core/src/operation.rs +++ b/crates/crank-core/src/operation.rs @@ -264,6 +264,7 @@ mod tests { use std::collections::BTreeMap; use serde_json::json; + use time::{OffsetDateTime, format_description::well_known::Rfc3339}; use crate::{ auth::{AuthConfig, AuthProfile, BearerAuthConfig}, @@ -280,6 +281,10 @@ mod tests { }, }; + fn timestamp(value: &str) -> OffsetDateTime { + OffsetDateTime::parse(value, &Rfc3339).unwrap() + } + #[test] fn rest_target_serializes_with_kind_tag() { let target = Target::Rest(RestTarget { @@ -440,8 +445,8 @@ mod tests { header_name: "Authorization".to_owned(), secret_id: crate::ids::SecretId::new("secret_crm_prod_token"), }), - created_at: "2026-03-25T08:00:00Z".to_owned(), - updated_at: "2026-03-25T08:00:00Z".to_owned(), + created_at: timestamp("2026-03-25T08:00:00Z"), + updated_at: timestamp("2026-03-25T08:00:00Z"), }; let value = serde_json::to_value(profile).unwrap(); diff --git a/crates/crank-registry/src/postgres/auth.rs b/crates/crank-registry/src/postgres/auth.rs index d3cee52..0d5e058 100644 --- a/crates/crank-registry/src/postgres/auth.rs +++ b/crates/crank-registry/src/postgres/auth.rs @@ -389,8 +389,8 @@ impl PostgresRegistry { .bind(&request.profile.name) .bind(serialize_enum_text(&request.profile.kind, "kind")?) .bind(Json(serialize_json_value(&request.profile.config)?)) - .bind(&request.profile.created_at) - .bind(&request.profile.updated_at) + .bind(request.profile.created_at) + .bind(request.profile.updated_at) .execute(&self.pool) .await?; @@ -409,8 +409,8 @@ impl PostgresRegistry { name, kind, config_json, - to_char(created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as \"created_at!\", - to_char(updated_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as \"updated_at!\" + created_at as \"created_at!: OffsetDateTime\", + updated_at as \"updated_at!: OffsetDateTime\" from auth_profiles where workspace_id = $1 and id = $2", workspace_id.as_str(), @@ -444,8 +444,8 @@ impl PostgresRegistry { name, kind, config_json, - to_char(created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as \"created_at!\", - to_char(updated_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as \"updated_at!\" + created_at as \"created_at!: OffsetDateTime\", + updated_at as \"updated_at!: OffsetDateTime\" from auth_profiles where workspace_id = $1 order by name asc", diff --git a/crates/crank-registry/src/postgres/mod.rs b/crates/crank-registry/src/postgres/mod.rs index 0975fdf..bed5922 100644 --- a/crates/crank-registry/src/postgres/mod.rs +++ b/crates/crank-registry/src/postgres/mod.rs @@ -750,8 +750,8 @@ fn build_auth_profile( name: String, kind: String, config_json: Value, - created_at: String, - updated_at: String, + created_at: OffsetDateTime, + updated_at: OffsetDateTime, ) -> Result { Ok(AuthProfile { id: crank_core::AuthProfileId::new(id), @@ -1315,8 +1315,8 @@ mod tests { header_name: "X-Api-Key".to_owned(), secret_id: SecretId::new("secret_crank_api_key"), }), - created_at: "2026-03-25T12:00:00Z".to_owned(), - updated_at: "2026-03-25T12:00:00Z".to_owned(), + created_at: timestamp("2026-03-25T12:00:00Z"), + updated_at: timestamp("2026-03-25T12:00:00Z"), }; registry @@ -1396,8 +1396,8 @@ mod tests { username_secret_id: primary_secret_id.clone(), password_secret_id: secondary_secret_id.clone(), }), - created_at: "2026-03-25T12:00:00Z".to_owned(), - updated_at: "2026-03-25T12:00:00Z".to_owned(), + created_at: timestamp("2026-03-25T12:00:00Z"), + updated_at: timestamp("2026-03-25T12:00:00Z"), }; registry diff --git a/crates/crank-runtime/Cargo.toml b/crates/crank-runtime/Cargo.toml index e140bf4..ed1bef6 100644 --- a/crates/crank-runtime/Cargo.toml +++ b/crates/crank-runtime/Cargo.toml @@ -26,5 +26,6 @@ thiserror.workspace = true axum.workspace = true crank-adapter-grpc = { path = "../crank-adapter-grpc", features = ["test-support"] } futures-util = "0.3" +time.workspace = true tokio.workspace = true tokio-tungstenite.workspace = true diff --git a/crates/crank-runtime/src/auth.rs b/crates/crank-runtime/src/auth.rs index 5dffbfa..e67f135 100644 --- a/crates/crank-runtime/src/auth.rs +++ b/crates/crank-runtime/src/auth.rs @@ -120,9 +120,14 @@ mod tests { BasicAuthConfig, BearerAuthConfig, SecretId, WorkspaceId, }; use serde_json::json; + use time::{OffsetDateTime, format_description::well_known::Rfc3339}; use crate::{PreparedRequest, auth::ResolvedAuth}; + fn timestamp(value: &str) -> OffsetDateTime { + OffsetDateTime::parse(value, &Rfc3339).unwrap() + } + #[test] fn applies_bearer_auth_to_headers() { let auth = ResolvedAuth::from_profile( @@ -135,8 +140,8 @@ mod tests { header_name: "Authorization".to_owned(), secret_id: SecretId::new("secret_token"), }), - created_at: "2026-04-07T00:00:00Z".to_owned(), - updated_at: "2026-04-07T00:00:00Z".to_owned(), + created_at: timestamp("2026-04-07T00:00:00Z"), + updated_at: timestamp("2026-04-07T00:00:00Z"), }, &BTreeMap::from([(SecretId::new("secret_token"), json!({"token":"abc"}))]), ) @@ -163,8 +168,8 @@ mod tests { username_secret_id: SecretId::new("secret_user"), password_secret_id: SecretId::new("secret_pass"), }), - created_at: "2026-04-07T00:00:00Z".to_owned(), - updated_at: "2026-04-07T00:00:00Z".to_owned(), + created_at: timestamp("2026-04-07T00:00:00Z"), + updated_at: timestamp("2026-04-07T00:00:00Z"), }, &BTreeMap::from([ (SecretId::new("secret_user"), json!({"username":"demo"})), @@ -194,8 +199,8 @@ mod tests { header_name: "X-Api-Key".to_owned(), secret_id: SecretId::new("secret_api_key"), }), - created_at: "2026-04-07T00:00:00Z".to_owned(), - updated_at: "2026-04-07T00:00:00Z".to_owned(), + created_at: timestamp("2026-04-07T00:00:00Z"), + updated_at: timestamp("2026-04-07T00:00:00Z"), }, &BTreeMap::from([(SecretId::new("secret_api_key"), json!("key-123"))]), ) @@ -222,8 +227,8 @@ mod tests { param_name: "api_key".to_owned(), secret_id: SecretId::new("secret_api_key"), }), - created_at: "2026-04-07T00:00:00Z".to_owned(), - updated_at: "2026-04-07T00:00:00Z".to_owned(), + created_at: timestamp("2026-04-07T00:00:00Z"), + updated_at: timestamp("2026-04-07T00:00:00Z"), }, &BTreeMap::from([(SecretId::new("secret_api_key"), json!({"value":"key-123"}))]), )