diff --git a/migrations/V14__users.sql b/migrations/V14__users.sql index 008cee39..fe4a40fb 100644 --- a/migrations/V14__users.sql +++ b/migrations/V14__users.sql @@ -29,15 +29,3 @@ CREATE TABLE api_tokens ( ); CREATE INDEX idx_api_tokens_user ON api_tokens(user_id); CREATE INDEX idx_api_tokens_hash ON api_tokens(token_hash); - -CREATE TABLE invitations ( - id UUID PRIMARY KEY, - email TEXT, -- nullable for link-based invites - invite_token_hash BYTEA NOT NULL, -- SHA-256 hash of the invite link token - invited_by TEXT NOT NULL REFERENCES users(id), - status TEXT NOT NULL DEFAULT 'pending', -- pending | accepted | expired | revoked - expires_at TIMESTAMPTZ NOT NULL, - accepted_at TIMESTAMPTZ, - accepted_by TEXT REFERENCES users(id), - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() -); diff --git a/src/channels/web/handlers/invitations.rs b/src/channels/web/handlers/invitations.rs deleted file mode 100644 index 5e0e904e..00000000 --- a/src/channels/web/handlers/invitations.rs +++ /dev/null @@ -1,211 +0,0 @@ -//! Invitation management API handlers. - -use std::sync::Arc; - -use axum::{Json, extract::State, http::StatusCode}; -use rand::RngCore; -use rand::rngs::OsRng; -use uuid::Uuid; - -use crate::channels::web::auth::{AdminUser, AuthenticatedUser}; -use crate::channels::web::server::GatewayState; -use crate::db::{InvitationRecord, UserRecord}; - -/// POST /api/invitations — create an invitation (admin only). -pub async fn invitations_create_handler( - State(state): State>, - AdminUser(user): AdminUser, - Json(body): Json, -) -> Result, (StatusCode, String)> { - let store = state.store.as_ref().ok_or(( - StatusCode::SERVICE_UNAVAILABLE, - "Database not available".to_string(), - ))?; - - let email = body.get("email").and_then(|v| v.as_str()).map(String::from); - - let expires_in_days = body - .get("expires_in_days") - .and_then(|v| v.as_u64()) - .unwrap_or(7); - - let now = chrono::Utc::now(); - let expires_at = now + chrono::Duration::days(expires_in_days as i64); - - // Generate 32 random bytes for the invite token. - let mut token_bytes = [0u8; 32]; - OsRng.fill_bytes(&mut token_bytes); - let plaintext_token = hex::encode(token_bytes); - let hash = crate::channels::web::auth::hash_token(&plaintext_token); - - let invitation_id = Uuid::new_v4(); - let invitation = InvitationRecord { - id: invitation_id, - email: email.clone(), - invited_by: user.user_id.clone(), - status: "pending".to_string(), - expires_at, - accepted_at: None, - accepted_by: None, - created_at: now, - }; - - store - .create_invitation(&invitation, &hash) - .await - .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; - - // Return the plaintext token — this is the ONLY time it is shown. - Ok(Json(serde_json::json!({ - "invite_token": plaintext_token, - "id": invitation_id.to_string(), - "email": email, - "expires_at": expires_at.to_rfc3339(), - "created_at": now.to_rfc3339(), - }))) -} - -/// GET /api/invitations — list invitations created by the current user. -pub async fn invitations_list_handler( - State(state): State>, - AuthenticatedUser(user): AuthenticatedUser, -) -> Result, (StatusCode, String)> { - let store = state.store.as_ref().ok_or(( - StatusCode::SERVICE_UNAVAILABLE, - "Database not available".to_string(), - ))?; - - let invitations = store - .list_invitations(Some(&user.user_id)) - .await - .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; - - let invitations_json: Vec = invitations - .into_iter() - .map(|inv| { - serde_json::json!({ - "id": inv.id.to_string(), - "email": inv.email, - "invited_by": inv.invited_by, - "status": inv.status, - "expires_at": inv.expires_at.to_rfc3339(), - "accepted_at": inv.accepted_at.map(|dt| dt.to_rfc3339()), - "accepted_by": inv.accepted_by, - "created_at": inv.created_at.to_rfc3339(), - }) - }) - .collect(); - - Ok(Json(serde_json::json!({ "invitations": invitations_json }))) -} - -/// POST /api/invitations/accept — accept an invitation and create a user account. -pub async fn invitations_accept_handler( - State(state): State>, - Json(body): Json, -) -> Result, (StatusCode, String)> { - let store = state.store.as_ref().ok_or(( - StatusCode::SERVICE_UNAVAILABLE, - "Database not available".to_string(), - ))?; - - let invite_token = body.get("invite_token").and_then(|v| v.as_str()).ok_or(( - StatusCode::BAD_REQUEST, - "Missing required field 'invite_token'".to_string(), - ))?; - - let display_name = body - .get("display_name") - .and_then(|v| v.as_str()) - .ok_or(( - StatusCode::BAD_REQUEST, - "Missing required field 'display_name'".to_string(), - ))? - .to_string(); - - // Hash the provided token to look up the invitation. - // Hash the plaintext token string (not decoded bytes) — must match - // how it was hashed during invitation creation via hash_token(). - let hash = crate::channels::web::auth::hash_token(invite_token); - - // Look up the invitation by hash. - let invitation = store - .get_invitation_by_hash(&hash) - .await - .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))? - .ok_or(( - StatusCode::NOT_FOUND, - "Invitation not found or already used".to_string(), - ))?; - - // Verify the invitation is still pending. - if invitation.status != "pending" { - return Err(( - StatusCode::CONFLICT, - format!("Invitation is already '{}'", invitation.status), - )); - } - - // Verify the invitation has not expired. - if invitation.expires_at < chrono::Utc::now() { - return Err((StatusCode::GONE, "Invitation has expired".to_string())); - } - - let new_user_id = Uuid::new_v4().to_string(); - - let now = chrono::Utc::now(); - let user_record = UserRecord { - id: new_user_id.clone(), - email: invitation.email.clone(), - display_name: display_name.clone(), - status: "active".to_string(), - role: "member".to_string(), - created_at: now, - updated_at: now, - last_login_at: None, - created_by: Some(invitation.invited_by.clone()), - metadata: serde_json::json!({}), - }; - - store - .create_user(&user_record) - .await - .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; - - // Create a first API token for the new user. - let mut api_token_bytes = [0u8; 32]; - OsRng.fill_bytes(&mut api_token_bytes); - let plaintext_api_token = hex::encode(api_token_bytes); - let api_hash = crate::channels::web::auth::hash_token(&plaintext_api_token); - - let api_prefix = &plaintext_api_token[..8]; - - let api_token_record = store - .create_api_token(&new_user_id, "default", &api_hash, api_prefix, None) - .await - .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; - - // Mark the invitation as accepted. - store - .accept_invitation(invitation.id, &new_user_id) - .await - .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; - - Ok(Json(serde_json::json!({ - "user": { - "id": new_user_id, - "email": user_record.email, - "display_name": user_record.display_name, - "status": "active", - "created_at": now.to_rfc3339(), - }, - "api_token": { - "token": plaintext_api_token, - "id": api_token_record.id.to_string(), - "name": api_token_record.name, - "token_prefix": api_token_record.token_prefix, - "created_at": api_token_record.created_at.to_rfc3339(), - }, - "invitation_id": invitation.id.to_string(), - }))) -} diff --git a/src/channels/web/handlers/mod.rs b/src/channels/web/handlers/mod.rs index e67403e8..016cf6f7 100644 --- a/src/channels/web/handlers/mod.rs +++ b/src/channels/web/handlers/mod.rs @@ -2,7 +2,6 @@ //! //! Each module groups related endpoint handlers by domain. -pub mod invitations; pub mod jobs; pub mod memory; pub mod routines; diff --git a/src/channels/web/server.rs b/src/channels/web/server.rs index 6ca0d9ba..ab36fd05 100644 --- a/src/channels/web/server.rs +++ b/src/channels/web/server.rs @@ -417,11 +417,6 @@ pub async fn start_server( .route( "/api/webhooks/u/{user_id}/{path}", post(crate::channels::web::handlers::webhooks::webhook_trigger_user_scoped_handler), - ) - // Invitation accept (public — validated by invite token, not user auth) - .route( - "/api/invitations/accept", - post(super::handlers::invitations::invitations_accept_handler), ); // Protected routes (require auth) @@ -544,12 +539,6 @@ pub async fn start_server( "/api/tokens/{id}", axum::routing::delete(super::handlers::tokens::tokens_revoke_handler), ) - // Invitations - .route( - "/api/invitations", - get(super::handlers::invitations::invitations_list_handler) - .post(super::handlers::invitations::invitations_create_handler), - ) // Gateway control plane .route("/api/gateway/status", get(gateway_status_handler)) // OpenAI-compatible API diff --git a/src/db/libsql/users.rs b/src/db/libsql/users.rs index e3b1f56f..34928723 100644 --- a/src/db/libsql/users.rs +++ b/src/db/libsql/users.rs @@ -7,7 +7,7 @@ use uuid::Uuid; use super::{fmt_opt_ts, fmt_ts, get_opt_text, get_opt_ts, get_text, get_ts, opt_text}; use crate::db::libsql::LibSqlBackend; -use crate::db::{ApiTokenRecord, DatabaseError, InvitationRecord, UserRecord, UserStore}; +use crate::db::{ApiTokenRecord, DatabaseError, UserRecord, UserStore}; fn row_to_user(row: &libsql::Row) -> Result { let metadata_str = get_text(row, 9); @@ -44,23 +44,6 @@ fn row_to_api_token(row: &libsql::Row) -> Result }) } -fn row_to_invitation(row: &libsql::Row) -> Result { - 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), - expires_at: get_ts(row, 4), - accepted_at: get_opt_ts(row, 5), - accepted_by: get_opt_text(row, 6), - created_at: get_ts(row, 7), - }) -} - #[async_trait] impl UserStore for LibSqlBackend { async fn create_user(&self, user: &UserRecord) -> Result<(), DatabaseError> { @@ -388,119 +371,6 @@ impl UserStore for LibSqlBackend { Ok(()) } - async fn create_invitation( - &self, - invitation: &InvitationRecord, - invite_hash: &[u8; 32], - ) -> Result<(), DatabaseError> { - let conn = self.connect().await?; - conn.execute( - r#" - INSERT INTO invitations (id, email, invite_token_hash, invited_by, status, expires_at, accepted_at, accepted_by, created_at) - VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9) - "#, - params![ - invitation.id.to_string(), - opt_text(invitation.email.as_deref()), - libsql::Value::Blob(invite_hash.to_vec()), - invitation.invited_by.as_str(), - invitation.status.as_str(), - fmt_ts(&invitation.expires_at), - fmt_opt_ts(&invitation.accepted_at), - opt_text(invitation.accepted_by.as_deref()), - fmt_ts(&invitation.created_at), - ], - ) - .await - .map_err(|e| DatabaseError::Query(e.to_string()))?; - Ok(()) - } - - async fn get_invitation_by_hash( - &self, - invite_hash: &[u8; 32], - ) -> Result, DatabaseError> { - let conn = self.connect().await?; - let mut rows = conn - .query( - r#" - SELECT id, email, invited_by, status, expires_at, accepted_at, accepted_by, created_at - 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())], - ) - .await - .map_err(|e| DatabaseError::Query(e.to_string()))?; - - match rows - .next() - .await - .map_err(|e| DatabaseError::Query(e.to_string()))? - { - Some(row) => Ok(Some(row_to_invitation(&row)?)), - None => Ok(None), - } - } - - async fn accept_invitation(&self, id: Uuid, accepted_by: &str) -> Result<(), DatabaseError> { - let conn = self.connect().await?; - let now = fmt_ts(&Utc::now()); - conn.execute( - r#" - UPDATE invitations SET status = 'accepted', accepted_at = ?2, accepted_by = ?3 - WHERE id = ?1 - "#, - params![id.to_string(), now, accepted_by], - ) - .await - .map_err(|e| DatabaseError::Query(e.to_string()))?; - Ok(()) - } - - async fn list_invitations( - &self, - invited_by: Option<&str>, - ) -> Result, DatabaseError> { - let conn = self.connect().await?; - let mut invitations = Vec::new(); - - let mut rows = if let Some(invited_by) = invited_by { - conn.query( - r#" - SELECT id, email, invited_by, status, expires_at, accepted_at, accepted_by, created_at - FROM invitations WHERE invited_by = ?1 - ORDER BY created_at DESC - "#, - params![invited_by], - ) - .await - .map_err(|e| DatabaseError::Query(e.to_string()))? - } else { - conn.query( - r#" - SELECT id, email, invited_by, status, expires_at, accepted_at, accepted_by, created_at - FROM invitations - ORDER BY created_at DESC - "#, - (), - ) - .await - .map_err(|e| DatabaseError::Query(e.to_string()))? - }; - - while let Some(row) = rows - .next() - .await - .map_err(|e| DatabaseError::Query(e.to_string()))? - { - invitations.push(row_to_invitation(&row)?); - } - Ok(invitations) - } - async fn has_any_users(&self) -> Result { let conn = self.connect().await?; let mut rows = conn @@ -694,76 +564,6 @@ mod tests { assert!(db.revoke_api_token(record.id, "alice").await.unwrap()); } - #[tokio::test] - async fn test_invitation_lifecycle() { - let (db, _dir) = setup().await; - db.create_user(&test_user("alice")).await.unwrap(); - - // Create invitation - let invite_hash = hash("invite-token-xyz"); - let invitation = InvitationRecord { - id: Uuid::new_v4(), - email: Some("newuser@test.com".to_string()), - invited_by: "alice".to_string(), - status: "pending".to_string(), - expires_at: Utc::now() + chrono::Duration::days(7), - accepted_at: None, - accepted_by: None, - created_at: Utc::now(), - }; - db.create_invitation(&invitation, &invite_hash) - .await - .unwrap(); - - // Look up by hash - let found = db - .get_invitation_by_hash(&invite_hash) - .await - .unwrap() - .unwrap(); - assert_eq!(found.id, invitation.id); - assert_eq!(found.status, "pending"); - - // List invitations - let list = db.list_invitations(Some("alice")).await.unwrap(); - assert_eq!(list.len(), 1); - - // Accept - db.create_user(&UserRecord { - id: "newuser".to_string(), - email: Some("newuser@test.com".to_string()), - display_name: "New User".to_string(), - status: "active".to_string(), - role: "member".to_string(), - created_at: Utc::now(), - updated_at: Utc::now(), - last_login_at: None, - created_by: Some("alice".to_string()), - metadata: serde_json::json!({}), - }) - .await - .unwrap(); - db.accept_invitation(invitation.id, "newuser") - .await - .unwrap(); - - // 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] async fn test_record_login_and_token_usage() { let (db, _dir) = setup().await; diff --git a/src/db/libsql_migrations.rs b/src/db/libsql_migrations.rs index 39d8867f..b510a0b5 100644 --- a/src/db/libsql_migrations.rs +++ b/src/db/libsql_migrations.rs @@ -609,18 +609,6 @@ CREATE TABLE IF NOT EXISTS api_tokens ( CREATE INDEX IF NOT EXISTS idx_api_tokens_user ON api_tokens(user_id); CREATE INDEX IF NOT EXISTS idx_api_tokens_hash ON api_tokens(token_hash); -CREATE TABLE IF NOT EXISTS invitations ( - id TEXT PRIMARY KEY, - email TEXT, - invite_token_hash BLOB NOT NULL, - invited_by TEXT NOT NULL, - status TEXT NOT NULL DEFAULT 'pending', - expires_at TEXT NOT NULL, - accepted_at TEXT, - accepted_by TEXT, - created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')) -); - "#; /// Incremental migrations applied after the base schema. @@ -797,18 +785,6 @@ CREATE TABLE IF NOT EXISTS api_tokens ( ); CREATE INDEX IF NOT EXISTS idx_api_tokens_user ON api_tokens(user_id); CREATE INDEX IF NOT EXISTS idx_api_tokens_hash ON api_tokens(token_hash); - -CREATE TABLE IF NOT EXISTS invitations ( - id TEXT PRIMARY KEY, - email TEXT, - invite_token_hash BLOB NOT NULL, - invited_by TEXT NOT NULL, - status TEXT NOT NULL DEFAULT 'pending', - expires_at TEXT NOT NULL, - accepted_at TEXT, - accepted_by TEXT, - created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')) -); "#, ), ]; diff --git a/src/db/mod.rs b/src/db/mod.rs index 5aa94016..aad26c0e 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -346,20 +346,6 @@ pub struct ApiTokenRecord { pub revoked_at: Option>, } -/// A pending invitation to create an account. -#[derive(Debug, Clone)] -pub struct InvitationRecord { - pub id: Uuid, - pub email: Option, - pub invited_by: String, - /// `pending`, `accepted`, `expired`, or `revoked`. - pub status: String, - pub expires_at: DateTime, - pub accepted_at: Option>, - pub accepted_by: Option, - pub created_at: DateTime, -} - // ==================== Sub-traits ==================== // // Each sub-trait groups related persistence methods. The `Database` supertrait @@ -860,26 +846,6 @@ pub trait UserStore: Send + Sync { /// Update `last_used_at` for a token. async fn record_token_usage(&self, token_id: Uuid) -> Result<(), DatabaseError>; - // ---- Invitations ---- - - /// Create a new invitation. - async fn create_invitation( - &self, - invitation: &InvitationRecord, - invite_hash: &[u8; 32], - ) -> Result<(), DatabaseError>; - /// Look up a pending invitation by its hashed token. - async fn get_invitation_by_hash( - &self, - invite_hash: &[u8; 32], - ) -> Result, DatabaseError>; - /// Accept an invitation (sets status=accepted, accepted_by, accepted_at). - async fn accept_invitation(&self, id: Uuid, accepted_by: &str) -> Result<(), DatabaseError>; - /// List invitations, optionally filtered by inviter. - async fn list_invitations( - &self, - invited_by: Option<&str>, - ) -> Result, DatabaseError>; /// Check whether any user records exist (for first-run bootstrap detection). async fn has_any_users(&self) -> Result; } diff --git a/src/db/postgres.rs b/src/db/postgres.rs index 35059ef5..31ef0c8e 100644 --- a/src/db/postgres.rs +++ b/src/db/postgres.rs @@ -16,8 +16,8 @@ use crate::agent::routine::{Routine, RoutineRun, RunStatus}; use crate::config::DatabaseConfig; use crate::context::{ActionRecord, JobContext, JobState}; use crate::db::{ - ApiTokenRecord, ConversationStore, Database, InvitationRecord, JobStore, RoutineStore, - SandboxStore, SettingsStore, ToolFailureStore, UserRecord, UserStore, WorkspaceStore, + ApiTokenRecord, ConversationStore, Database, JobStore, RoutineStore, SandboxStore, + SettingsStore, ToolFailureStore, UserRecord, UserStore, WorkspaceStore, }; use crate::error::{DatabaseError, WorkspaceError}; use crate::history::{ @@ -858,32 +858,6 @@ impl UserStore for PgBackend { self.store.record_token_usage(token_id).await } - async fn create_invitation( - &self, - invitation: &InvitationRecord, - invite_hash: &[u8; 32], - ) -> Result<(), DatabaseError> { - self.store.create_invitation(invitation, invite_hash).await - } - - async fn get_invitation_by_hash( - &self, - invite_hash: &[u8; 32], - ) -> Result, DatabaseError> { - self.store.get_invitation_by_hash(invite_hash).await - } - - async fn accept_invitation(&self, id: Uuid, accepted_by: &str) -> Result<(), DatabaseError> { - self.store.accept_invitation(id, accepted_by).await - } - - async fn list_invitations( - &self, - invited_by: Option<&str>, - ) -> Result, DatabaseError> { - self.store.list_invitations(invited_by).await - } - async fn has_any_users(&self) -> Result { self.store.has_any_users().await } diff --git a/src/history/store.rs b/src/history/store.rs index a9401921..46a18d70 100644 --- a/src/history/store.rs +++ b/src/history/store.rs @@ -2282,7 +2282,7 @@ impl Store { // ==================== Users / API Tokens / Invitations ==================== #[cfg(feature = "postgres")] -use crate::db::{ApiTokenRecord, InvitationRecord, UserRecord}; +use crate::db::{ApiTokenRecord, UserRecord}; #[cfg(feature = "postgres")] impl Store { @@ -2525,102 +2525,6 @@ impl Store { Ok(()) } - /// Create a new invitation. - pub async fn create_invitation( - &self, - invitation: &InvitationRecord, - invite_hash: &[u8; 32], - ) -> Result<(), DatabaseError> { - let conn = self.conn().await?; - conn.execute( - r#" - INSERT INTO invitations (id, email, invite_token_hash, invited_by, status, expires_at, accepted_at, accepted_by, created_at) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) - "#, - &[ - &invitation.id, - &invitation.email, - &invite_hash.to_vec(), - &invitation.invited_by, - &invitation.status, - &invitation.expires_at, - &invitation.accepted_at, - &invitation.accepted_by, - &invitation.created_at, - ], - ) - .await?; - Ok(()) - } - - /// Look up a pending invitation by its hashed token. - pub async fn get_invitation_by_hash( - &self, - invite_hash: &[u8; 32], - ) -> Result, DatabaseError> { - let conn = self.conn().await?; - let row = conn - .query_opt( - r#" - SELECT id, email, invited_by, status, expires_at, accepted_at, accepted_by, created_at - FROM invitations - WHERE invite_token_hash = $1 AND status = 'pending' AND expires_at > NOW() - "#, - &[&invite_hash.to_vec()], - ) - .await?; - Ok(row.map(|r| row_to_invitation(&r))) - } - - /// Accept an invitation. - pub async fn accept_invitation( - &self, - id: Uuid, - accepted_by: &str, - ) -> Result<(), DatabaseError> { - let conn = self.conn().await?; - conn.execute( - "UPDATE invitations SET status = 'accepted', accepted_at = NOW(), accepted_by = $1 WHERE id = $2", - &[&accepted_by, &id], - ) - .await?; - Ok(()) - } - - /// List invitations, optionally filtered by inviter. - pub async fn list_invitations( - &self, - invited_by: Option<&str>, - ) -> Result, DatabaseError> { - let conn = self.conn().await?; - let rows = match invited_by { - Some(user) => { - conn.query( - r#" - SELECT id, email, invited_by, status, expires_at, accepted_at, accepted_by, created_at - FROM invitations - WHERE invited_by = $1 - ORDER BY created_at DESC - "#, - &[&user], - ) - .await? - } - None => { - conn.query( - r#" - SELECT id, email, invited_by, status, expires_at, accepted_at, accepted_by, created_at - FROM invitations - ORDER BY created_at DESC - "#, - &[], - ) - .await? - } - }; - Ok(rows.iter().map(row_to_invitation).collect()) - } - /// Check whether any user records exist. pub async fn has_any_users(&self) -> Result { let conn = self.conn().await?; @@ -2664,20 +2568,6 @@ fn row_to_api_token(row: &tokio_postgres::Row) -> ApiTokenRecord { } } -#[cfg(feature = "postgres")] -fn row_to_invitation(row: &tokio_postgres::Row) -> InvitationRecord { - InvitationRecord { - id: row.get("id"), - email: row.get("email"), - invited_by: row.get("invited_by"), - status: row.get("status"), - expires_at: row.get("expires_at"), - accepted_at: row.get("accepted_at"), - accepted_by: row.get("accepted_by"), - created_at: row.get("created_at"), - } -} - #[cfg(test)] mod tests { use super::*;