mirror of
https://github.com/outbackdingo/optimclaw.git
synced 2026-08-26 23:50:17 +00:00
refactor: remove GATEWAY_USER_TOKENS, fix review feedback
GATEWAY_USER_TOKENS never went to production — replaced entirely by DB-backed user management via /api/admin/users and /api/tokens. Removed: - UserTokenConfig struct and GATEWAY_USER_TOKENS env var parsing - user_tokens field from GatewayConfig - GatewayChannel::new_multi_auth() constructor - Env-var user migration block in main.rs (~90 lines) - multi_tenant auto-detection from GATEWAY_USER_TOKENS (now runtime via db.has_any_users() in app.rs) Review fixes (zmanian): - User ID generation: UUID instead of display-name derivation (#1) - Invitation accept moved to public router (no auth needed) (#3) - libSQL get_invitation_by_hash aligned with postgres: filters status='pending' AND expires_at > now (#4) - UUID parse: returns DatabaseError::Serialization instead of unwrap_or_default (#7) - PostgreSQL SELECT * replaced with explicit column lists (#8) - Sort order aligned (both backends use DESC) (#6) Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
This commit is contained in:
+43
-21
@@ -26,9 +26,13 @@ fn row_to_user(row: &libsql::Row) -> Result<UserRecord, DatabaseError> {
|
||||
})
|
||||
}
|
||||
|
||||
fn row_to_api_token(row: &libsql::Row) -> ApiTokenRecord {
|
||||
ApiTokenRecord {
|
||||
id: get_text(row, 0).parse().unwrap_or_default(),
|
||||
fn row_to_api_token(row: &libsql::Row) -> Result<ApiTokenRecord, DatabaseError> {
|
||||
let id_str = get_text(row, 0);
|
||||
let id: Uuid = id_str
|
||||
.parse()
|
||||
.map_err(|e| DatabaseError::Serialization(format!("invalid UUID: {e}")))?;
|
||||
Ok(ApiTokenRecord {
|
||||
id,
|
||||
user_id: get_text(row, 1),
|
||||
name: get_text(row, 2),
|
||||
token_prefix: get_text(row, 3),
|
||||
@@ -36,12 +40,16 @@ fn row_to_api_token(row: &libsql::Row) -> ApiTokenRecord {
|
||||
last_used_at: get_opt_ts(row, 5),
|
||||
created_at: get_ts(row, 6),
|
||||
revoked_at: get_opt_ts(row, 7),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn row_to_invitation(row: &libsql::Row) -> InvitationRecord {
|
||||
InvitationRecord {
|
||||
id: get_text(row, 0).parse().unwrap_or_default(),
|
||||
fn row_to_invitation(row: &libsql::Row) -> Result<InvitationRecord, DatabaseError> {
|
||||
let id_str = get_text(row, 0);
|
||||
let id: Uuid = id_str
|
||||
.parse()
|
||||
.map_err(|e| DatabaseError::Serialization(format!("invalid UUID: {e}")))?;
|
||||
Ok(InvitationRecord {
|
||||
id,
|
||||
email: get_opt_text(row, 1),
|
||||
invited_by: get_text(row, 2),
|
||||
status: get_text(row, 3),
|
||||
@@ -49,7 +57,7 @@ fn row_to_invitation(row: &libsql::Row) -> InvitationRecord {
|
||||
accepted_at: get_opt_ts(row, 5),
|
||||
accepted_by: get_opt_text(row, 6),
|
||||
created_at: get_ts(row, 7),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
@@ -274,7 +282,7 @@ impl UserStore for LibSqlBackend {
|
||||
.await
|
||||
.map_err(|e| DatabaseError::Query(e.to_string()))?
|
||||
{
|
||||
tokens.push(row_to_api_token(&row));
|
||||
tokens.push(row_to_api_token(&row)?);
|
||||
}
|
||||
Ok(tokens)
|
||||
}
|
||||
@@ -328,8 +336,12 @@ impl UserStore for LibSqlBackend {
|
||||
.map_err(|e| DatabaseError::Query(e.to_string()))?
|
||||
{
|
||||
Some(row) => {
|
||||
let id_str = get_text(&row, 0);
|
||||
let token_id: Uuid = id_str
|
||||
.parse()
|
||||
.map_err(|e| DatabaseError::Serialization(format!("invalid UUID: {e}")))?;
|
||||
let token = ApiTokenRecord {
|
||||
id: get_text(&row, 0).parse().unwrap_or_default(),
|
||||
id: token_id,
|
||||
user_id: get_text(&row, 1),
|
||||
name: get_text(&row, 2),
|
||||
token_prefix: get_text(&row, 3),
|
||||
@@ -410,7 +422,10 @@ impl UserStore for LibSqlBackend {
|
||||
.query(
|
||||
r#"
|
||||
SELECT id, email, invited_by, status, expires_at, accepted_at, accepted_by, created_at
|
||||
FROM invitations WHERE invite_token_hash = ?1
|
||||
FROM invitations
|
||||
WHERE invite_token_hash = ?1
|
||||
AND status = 'pending'
|
||||
AND expires_at > strftime('%Y-%m-%dT%H:%M:%S', 'now')
|
||||
"#,
|
||||
params![libsql::Value::Blob(invite_hash.to_vec())],
|
||||
)
|
||||
@@ -422,7 +437,7 @@ impl UserStore for LibSqlBackend {
|
||||
.await
|
||||
.map_err(|e| DatabaseError::Query(e.to_string()))?
|
||||
{
|
||||
Some(row) => Ok(Some(row_to_invitation(&row))),
|
||||
Some(row) => Ok(Some(row_to_invitation(&row)?)),
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
@@ -478,7 +493,7 @@ impl UserStore for LibSqlBackend {
|
||||
.await
|
||||
.map_err(|e| DatabaseError::Query(e.to_string()))?
|
||||
{
|
||||
invitations.push(row_to_invitation(&row));
|
||||
invitations.push(row_to_invitation(&row)?);
|
||||
}
|
||||
Ok(invitations)
|
||||
}
|
||||
@@ -727,14 +742,21 @@ mod tests {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Verify accepted
|
||||
let accepted = db
|
||||
.get_invitation_by_hash(&invite_hash)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(accepted.status, "accepted");
|
||||
assert_eq!(accepted.accepted_by, Some("newuser".to_string()));
|
||||
// After acceptance, the invitation should no longer be found via hash
|
||||
// lookup (which filters for status='pending')
|
||||
assert!(
|
||||
db.get_invitation_by_hash(&invite_hash)
|
||||
.await
|
||||
.unwrap()
|
||||
.is_none(),
|
||||
"Accepted invitation should not be returned by pending-only lookup"
|
||||
);
|
||||
|
||||
// Verify via list that it was accepted
|
||||
let all = db.list_invitations(Some("alice")).await.unwrap();
|
||||
assert_eq!(all.len(), 1);
|
||||
assert_eq!(all[0].status, "accepted");
|
||||
assert_eq!(all[0].accepted_by, Some("newuser".to_string()));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
Reference in New Issue
Block a user