core: type user and auth session timestamps

This commit is contained in:
a.tolmachev
2026-04-12 22:11:43 +00:00
parent 624224f089
commit e03d4da925
14 changed files with 156 additions and 44 deletions
+1
View File
@@ -9,6 +9,7 @@ version.workspace = true
serde.workspace = true
serde_json.workspace = true
thiserror.workspace = true
time.workspace = true
[dev-dependencies]
serde_yaml.workspace = true
+44 -1
View File
@@ -1,4 +1,5 @@
use serde::{Deserialize, Serialize};
use time::OffsetDateTime;
use crate::ids::{InvitationId, PlatformApiKeyId, UserId, WorkspaceId};
@@ -48,7 +49,8 @@ pub struct User {
pub email: String,
pub display_name: String,
pub status: UserStatus,
pub created_at: String,
#[serde(with = "time::serde::rfc3339")]
pub created_at: OffsetDateTime,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
@@ -82,3 +84,44 @@ pub struct PlatformApiKey {
pub created_at: String,
pub last_used_at: Option<String>,
}
#[cfg(test)]
mod tests {
use serde_json::json;
use time::{OffsetDateTime, format_description::well_known::Rfc3339};
use super::{User, UserStatus};
use crate::ids::UserId;
#[test]
fn user_serializes_created_at_as_rfc3339() {
let user = User {
id: UserId::new("user_01"),
email: "owner@example.com".to_owned(),
display_name: "Owner".to_owned(),
status: UserStatus::Active,
created_at: OffsetDateTime::parse("2026-03-25T12:00:00Z", &Rfc3339).unwrap(),
};
let value = serde_json::to_value(&user).unwrap();
assert_eq!(value["created_at"], json!("2026-03-25T12:00:00Z"));
}
#[test]
fn user_deserializes_created_at_from_rfc3339() {
let user: User = serde_json::from_value(json!({
"id": "user_01",
"email": "owner@example.com",
"display_name": "Owner",
"status": "active",
"created_at": "2026-03-25T12:00:00Z"
}))
.unwrap();
assert_eq!(
user.created_at,
OffsetDateTime::parse("2026-03-25T12:00:00Z", &Rfc3339).unwrap()
);
}
}
+1
View File
@@ -13,6 +13,7 @@ serde.workspace = true
serde_json.workspace = true
sqlx.workspace = true
thiserror.workspace = true
time.workspace = true
uuid.workspace = true
[dev-dependencies]
+7 -6
View File
@@ -1,4 +1,5 @@
use super::*;
use time::OffsetDateTime;
impl PostgresRegistry {
pub async fn upsert_bootstrap_user(
@@ -73,7 +74,7 @@ impl PostgresRegistry {
display_name,
password_hash as \"password_hash!\",
status,
to_char(created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as \"created_at!\"
created_at as \"created_at!: OffsetDateTime\"
from users
where email = $1
limit 1",
@@ -108,7 +109,7 @@ impl PostgresRegistry {
display_name,
password_hash as \"password_hash!\",
status,
to_char(created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as \"created_at!\"
created_at as \"created_at!: OffsetDateTime\"
from users
where id = $1
limit 1",
@@ -148,7 +149,7 @@ impl PostgresRegistry {
email,
display_name,
status,
to_char(created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as \"created_at!\"",
created_at as \"created_at!: OffsetDateTime\"",
user_id.as_str(),
email,
display_name,
@@ -196,7 +197,7 @@ impl PostgresRegistry {
user_id: &UserId,
current_workspace_id: Option<&WorkspaceId>,
secret_hash: &str,
expires_at: &str,
expires_at: &OffsetDateTime,
) -> Result<(), RegistryError> {
sqlx::query(
"insert into user_sessions (
@@ -223,7 +224,7 @@ impl PostgresRegistry {
.bind(user_id.as_str())
.bind(current_workspace_id.map(|id| id.as_str()))
.bind(secret_hash)
.bind(expires_at)
.bind(*expires_at)
.execute(&self.pool)
.await?;
@@ -243,7 +244,7 @@ impl PostgresRegistry {
u.email,
u.display_name,
u.status,
to_char(u.created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as \"created_at!\"
u.created_at as \"created_at!: OffsetDateTime\"
from user_sessions s
join users u on u.id = s.user_id
where s.id = $1
+65 -3
View File
@@ -25,6 +25,7 @@ use sqlx::{
};
use std::{collections::BTreeMap, env, time::Duration};
use thiserror::Error;
use time::OffsetDateTime;
use crate::{
error::RegistryError,
@@ -489,7 +490,7 @@ fn build_user(
email: String,
display_name: String,
status: String,
created_at: String,
created_at: OffsetDateTime,
) -> Result<User, RegistryError> {
Ok(User {
id: UserId::new(id),
@@ -1131,12 +1132,13 @@ mod tests {
MembershipRole, OperationId, OperationStatus, PlatformApiKey, PlatformApiKeyId,
PlatformApiKeyScope, PlatformApiKeyStatus, Protocol, RestTarget, RetryPolicy, Samples,
SecretId, StreamSession, StreamSessionId, StreamStatus, Target, ToolDescription,
ToolExample, User, UserId, Workspace, WorkspaceId,
ToolExample, User, UserId, UserSessionId, Workspace, WorkspaceId,
};
use crank_mapping::{MappingRule, MappingSet};
use crank_schema::{Schema, SchemaKind};
use serde_json::json;
use sqlx::{Executor, PgPool, postgres::PgPoolOptions};
use time::{OffsetDateTime, format_description::well_known::Rfc3339};
use crate::{
PostgresRegistry, RegistryError,
@@ -1156,6 +1158,10 @@ mod tests {
WorkspaceId::new("ws_default")
}
fn timestamp(value: &str) -> OffsetDateTime {
OffsetDateTime::parse(value, &Rfc3339).unwrap()
}
#[tokio::test]
async fn stores_versions_and_published_operations() {
let database = TestDatabase::new().await;
@@ -1480,7 +1486,7 @@ mod tests {
email: "owner@example.com".to_owned(),
display_name: "Owner".to_owned(),
status: crank_core::UserStatus::Active,
created_at: "2026-03-25T11:00:00Z".to_owned(),
created_at: timestamp("2026-03-25T11:00:00Z"),
};
registry
@@ -1590,6 +1596,62 @@ mod tests {
assert_eq!(updated.display_name, "Updated Owner");
assert!(has_access);
assert!(!lacks_access);
assert!(updated.created_at.unix_timestamp() > 0);
database.cleanup().await;
}
#[tokio::test]
async fn creates_and_loads_user_sessions_with_typed_expiration() {
let database = TestDatabase::new().await;
let registry = database.registry().await;
let workspace = Workspace {
id: WorkspaceId::new("ws_session_01"),
slug: "session".to_owned(),
display_name: "Session Workspace".to_owned(),
status: crank_core::WorkspaceStatus::Active,
settings: json!({}),
created_at: "2026-03-25T12:00:00Z".to_owned(),
updated_at: "2026-03-25T12:00:00Z".to_owned(),
};
let user_id = registry
.upsert_bootstrap_user("session@example.com", "Owner", "hashed-password")
.await
.unwrap();
let session_id = UserSessionId::new("sess_01");
let expires_at = timestamp("2030-04-06T12:05:00Z");
registry
.create_workspace(CreateWorkspaceRequest {
workspace: &workspace,
})
.await
.unwrap();
registry
.ensure_membership(&workspace.id, &user_id, MembershipRole::Owner)
.await
.unwrap();
registry
.create_user_session(
&session_id,
&user_id,
Some(&workspace.id),
"secret-hash-01",
&expires_at,
)
.await
.unwrap();
let session = registry
.get_user_session(&session_id, "secret-hash-01")
.await
.unwrap()
.unwrap();
assert_eq!(session.session_id, session_id);
assert_eq!(session.current_workspace_id, Some(workspace.id.clone()));
assert_eq!(session.user.id, user_id);
assert!(session.user.created_at.unix_timestamp() > 0);
database.cleanup().await;
}
@@ -1,4 +1,5 @@
use super::*;
use time::OffsetDateTime;
impl PostgresRegistry {
pub async fn list_workspaces(&self) -> Result<Vec<WorkspaceRecord>, RegistryError> {
@@ -88,7 +89,7 @@ impl PostgresRegistry {
u.email,
u.display_name,
u.status,
to_char(u.created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as \"user_created_at!\"
u.created_at as \"user_created_at!: OffsetDateTime\"
from memberships m
join users u on u.id = m.user_id
where m.workspace_id = $1