feat(v3): migrate workspace to API, Tauri desktop, and v3 crates; remove legacy MCP stack
Some checks failed
Secrets v3 CI / 检查 (push) Has been cancelled
Some checks failed
Secrets v3 CI / 检查 (push) Has been cancelled
- Add apps/api, desktop Tauri shell, domain/application/crypto/device-auth/infrastructure-db - Replace desktop-daemon vault integration; drop secrets-core and secrets-mcp* - Ignore apps/desktop/dist and generated Tauri icons; document icon/dist steps in AGENTS.md - Apply rustfmt; fix clippy (collapsible_if, HTTP method as str)
This commit is contained in:
27
apps/api/Cargo.toml
Normal file
27
apps/api/Cargo.toml
Normal file
@@ -0,0 +1,27 @@
|
||||
[package]
|
||||
name = "secrets-api"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
|
||||
[[bin]]
|
||||
name = "secrets-api"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
axum.workspace = true
|
||||
dotenvy.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
sqlx.workspace = true
|
||||
tokio.workspace = true
|
||||
tracing.workspace = true
|
||||
tracing-subscriber.workspace = true
|
||||
uuid.workspace = true
|
||||
chrono.workspace = true
|
||||
reqwest.workspace = true
|
||||
|
||||
secrets-application = { path = "../../crates/application" }
|
||||
secrets-device-auth = { path = "../../crates/device-auth" }
|
||||
secrets-domain = { path = "../../crates/domain" }
|
||||
secrets-infrastructure-db = { path = "../../crates/infrastructure-db" }
|
||||
15
apps/api/src/bin/secrets-api-migrate.rs
Normal file
15
apps/api/src/bin/secrets-api-migrate.rs
Normal file
@@ -0,0 +1,15 @@
|
||||
use anyhow::{Context, Result};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
let _ = dotenvy::dotenv();
|
||||
|
||||
let database_url = secrets_infrastructure_db::load_database_url()?;
|
||||
let pool = secrets_infrastructure_db::create_pool(&database_url).await?;
|
||||
secrets_infrastructure_db::migrate_current_schema(&pool)
|
||||
.await
|
||||
.context("failed to initialize current database schema")?;
|
||||
|
||||
println!("current database schema initialized");
|
||||
Ok(())
|
||||
}
|
||||
568
apps/api/src/main.rs
Normal file
568
apps/api/src/main.rs
Normal file
@@ -0,0 +1,568 @@
|
||||
use anyhow::{Context, Result as AnyResult};
|
||||
use axum::{
|
||||
Json, Router,
|
||||
extract::{Path, State},
|
||||
http::{HeaderMap, StatusCode, header},
|
||||
routing::{get, post},
|
||||
};
|
||||
use chrono::{DateTime, Utc};
|
||||
use reqwest::Client;
|
||||
use secrets_application::sync::{fetch_object, sync_pull, sync_push};
|
||||
use secrets_device_auth::{
|
||||
hash_device_login_token, new_device_fingerprint, new_device_login_token,
|
||||
};
|
||||
use secrets_domain::{
|
||||
SyncPullRequest, SyncPullResponse, SyncPushRequest, SyncPushResponse, VaultObjectEnvelope,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use sqlx::PgPool;
|
||||
use tracing_subscriber::EnvFilter;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Clone)]
|
||||
struct AppState {
|
||||
pool: PgPool,
|
||||
http: Client,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct DemoLoginResponse {
|
||||
device_token: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct DesktopGoogleLoginRequest {
|
||||
access_token: String,
|
||||
device_name: String,
|
||||
platform: String,
|
||||
client_version: String,
|
||||
device_fingerprint: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct GoogleUserInfo {
|
||||
email: String,
|
||||
name: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct DeviceView {
|
||||
name: String,
|
||||
platform: String,
|
||||
client_version: String,
|
||||
last_seen: String,
|
||||
ip: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct UserProfileView {
|
||||
id: Uuid,
|
||||
name: String,
|
||||
email: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, sqlx::FromRow)]
|
||||
struct UserRow {
|
||||
id: Uuid,
|
||||
email: Option<String>,
|
||||
name: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, sqlx::FromRow)]
|
||||
struct DeviceRow {
|
||||
id: Uuid,
|
||||
display_name: String,
|
||||
platform: String,
|
||||
client_version: String,
|
||||
last_seen_at: DateTime<Utc>,
|
||||
last_ip: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct ObjectResponse {
|
||||
object: VaultObjectEnvelope,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> AnyResult<()> {
|
||||
let _ = dotenvy::dotenv();
|
||||
|
||||
tracing_subscriber::fmt()
|
||||
.with_env_filter(
|
||||
EnvFilter::try_from_default_env().unwrap_or_else(|_| "secrets_api=info".into()),
|
||||
)
|
||||
.init();
|
||||
|
||||
let database_url = secrets_infrastructure_db::load_database_url()?;
|
||||
let pool = secrets_infrastructure_db::create_pool(&database_url).await?;
|
||||
secrets_infrastructure_db::migrate_current_schema(&pool)
|
||||
.await
|
||||
.context("failed to initialize current database schema")?;
|
||||
|
||||
let bind = std::env::var("SECRETS_API_BIND").unwrap_or_else(|_| "127.0.0.1:9415".to_string());
|
||||
let app = Router::new()
|
||||
.route("/healthz", get(|| async { "ok" }))
|
||||
.route("/auth/demo-login", post(api_demo_login))
|
||||
.route("/auth/google/desktop-login", post(api_google_desktop_login))
|
||||
.route("/me", get(api_me))
|
||||
.route("/sync/pull", post(api_sync_pull))
|
||||
.route("/sync/push", post(api_sync_push))
|
||||
.route("/sync/objects/{id}", get(api_sync_object))
|
||||
.route("/devices", get(api_devices))
|
||||
.with_state(AppState {
|
||||
pool,
|
||||
http: Client::new(),
|
||||
});
|
||||
let listener = tokio::net::TcpListener::bind(&bind)
|
||||
.await
|
||||
.with_context(|| format!("failed to bind {}", bind))?;
|
||||
|
||||
tracing::info!(bind = %bind, "secrets-api listening");
|
||||
axum::serve(listener, app)
|
||||
.await
|
||||
.context("api server error")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn api_demo_login(
|
||||
State(state): State<AppState>,
|
||||
) -> std::result::Result<Json<DemoLoginResponse>, (StatusCode, Json<serde_json::Value>)> {
|
||||
let (user_id, device_id) = ensure_demo_user(&state.pool)
|
||||
.await
|
||||
.map_err(internal_error)?;
|
||||
let device_token = new_device_login_token();
|
||||
let token_hash = hash_device_login_token(&device_token);
|
||||
|
||||
sqlx::query("DELETE FROM device_login_tokens WHERE device_id = $1")
|
||||
.bind(device_id)
|
||||
.execute(&state.pool)
|
||||
.await
|
||||
.map_err(internal_error)?;
|
||||
|
||||
sqlx::query(
|
||||
r#"
|
||||
INSERT INTO device_login_tokens (device_id, token_hash)
|
||||
VALUES ($1, $2)
|
||||
"#,
|
||||
)
|
||||
.bind(device_id)
|
||||
.bind(token_hash)
|
||||
.execute(&state.pool)
|
||||
.await
|
||||
.map_err(internal_error)?;
|
||||
|
||||
sqlx::query(
|
||||
r#"
|
||||
INSERT INTO auth_events (
|
||||
user_id, device_id, device_name, platform, client_version, ip_addr, forwarded_ip, login_method, login_result
|
||||
)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, 'device_token', 'success')
|
||||
"#,
|
||||
)
|
||||
.bind(user_id)
|
||||
.bind(device_id)
|
||||
.bind("Voson 的 Mac mini")
|
||||
.bind("macOS")
|
||||
.bind(env!("CARGO_PKG_VERSION"))
|
||||
.bind::<Option<String>>(None)
|
||||
.bind::<Option<String>>(None)
|
||||
.execute(&state.pool)
|
||||
.await
|
||||
.map_err(internal_error)?;
|
||||
|
||||
Ok(Json(DemoLoginResponse { device_token }))
|
||||
}
|
||||
|
||||
async fn api_google_desktop_login(
|
||||
State(state): State<AppState>,
|
||||
Json(payload): Json<DesktopGoogleLoginRequest>,
|
||||
) -> std::result::Result<Json<DemoLoginResponse>, (StatusCode, Json<serde_json::Value>)> {
|
||||
let google_user = state
|
||||
.http
|
||||
.get("https://openidconnect.googleapis.com/v1/userinfo")
|
||||
.bearer_auth(&payload.access_token)
|
||||
.send()
|
||||
.await
|
||||
.map_err(internal_error)?
|
||||
.error_for_status()
|
||||
.map_err(internal_error)?
|
||||
.json::<GoogleUserInfo>()
|
||||
.await
|
||||
.map_err(internal_error)?;
|
||||
|
||||
let user_id = upsert_user_from_google(&state.pool, &google_user)
|
||||
.await
|
||||
.map_err(internal_error)?;
|
||||
let device_id = upsert_device_for_login(
|
||||
&state.pool,
|
||||
user_id,
|
||||
&payload.device_name,
|
||||
&payload.platform,
|
||||
&payload.client_version,
|
||||
&payload.device_fingerprint,
|
||||
)
|
||||
.await
|
||||
.map_err(internal_error)?;
|
||||
|
||||
let device_token = issue_device_login_token(
|
||||
&state.pool,
|
||||
user_id,
|
||||
device_id,
|
||||
&payload.device_name,
|
||||
&payload.platform,
|
||||
&payload.client_version,
|
||||
)
|
||||
.await
|
||||
.map_err(internal_error)?;
|
||||
|
||||
Ok(Json(DemoLoginResponse { device_token }))
|
||||
}
|
||||
|
||||
async fn api_sync_pull(
|
||||
State(state): State<AppState>,
|
||||
headers: HeaderMap,
|
||||
Json(payload): Json<SyncPullRequest>,
|
||||
) -> std::result::Result<Json<SyncPullResponse>, (StatusCode, Json<serde_json::Value>)> {
|
||||
let (user, _) = require_auth(&state.pool, &headers).await?;
|
||||
let response = sync_pull(&state.pool, user.id, payload)
|
||||
.await
|
||||
.map_err(internal_error)?;
|
||||
Ok(Json(response))
|
||||
}
|
||||
|
||||
async fn api_sync_push(
|
||||
State(state): State<AppState>,
|
||||
headers: HeaderMap,
|
||||
Json(payload): Json<SyncPushRequest>,
|
||||
) -> std::result::Result<Json<SyncPushResponse>, (StatusCode, Json<serde_json::Value>)> {
|
||||
let (user, _) = require_auth(&state.pool, &headers).await?;
|
||||
let response = sync_push(&state.pool, user.id, payload)
|
||||
.await
|
||||
.map_err(internal_error)?;
|
||||
Ok(Json(response))
|
||||
}
|
||||
|
||||
async fn api_sync_object(
|
||||
State(state): State<AppState>,
|
||||
headers: HeaderMap,
|
||||
Path(object_id): Path<Uuid>,
|
||||
) -> std::result::Result<Json<ObjectResponse>, (StatusCode, Json<serde_json::Value>)> {
|
||||
let (user, _) = require_auth(&state.pool, &headers).await?;
|
||||
let object = fetch_object(&state.pool, user.id, object_id)
|
||||
.await
|
||||
.map_err(internal_error)?
|
||||
.ok_or_else(|| unauthorized("object not found"))?;
|
||||
Ok(Json(ObjectResponse { object }))
|
||||
}
|
||||
|
||||
async fn api_devices(
|
||||
State(state): State<AppState>,
|
||||
headers: HeaderMap,
|
||||
) -> std::result::Result<Json<Vec<DeviceView>>, (StatusCode, Json<serde_json::Value>)> {
|
||||
let (user, _) = require_auth(&state.pool, &headers).await?;
|
||||
let rows = sqlx::query_as::<_, DeviceRow>(
|
||||
r#"
|
||||
SELECT
|
||||
d.id,
|
||||
d.display_name,
|
||||
d.platform,
|
||||
d.client_version,
|
||||
d.last_seen_at,
|
||||
COALESCE(NULLIF(a.forwarded_ip, ''), NULLIF(a.ip_addr, '')) AS last_ip
|
||||
FROM devices d
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT ip_addr, forwarded_ip
|
||||
FROM auth_events
|
||||
WHERE device_id = d.id
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 1
|
||||
) a ON TRUE
|
||||
WHERE d.user_id = $1
|
||||
ORDER BY last_seen_at DESC
|
||||
"#,
|
||||
)
|
||||
.bind(user.id)
|
||||
.fetch_all(&state.pool)
|
||||
.await
|
||||
.map_err(internal_error)?;
|
||||
|
||||
let devices = rows
|
||||
.into_iter()
|
||||
.map(|row| DeviceView {
|
||||
name: row.display_name,
|
||||
platform: row.platform,
|
||||
client_version: row.client_version,
|
||||
last_seen: row.last_seen_at.format("%Y-%m-%d %H:%M").to_string(),
|
||||
ip: row.last_ip,
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(Json(devices))
|
||||
}
|
||||
|
||||
async fn api_me(
|
||||
State(state): State<AppState>,
|
||||
headers: HeaderMap,
|
||||
) -> std::result::Result<Json<UserProfileView>, (StatusCode, Json<serde_json::Value>)> {
|
||||
let (user, _) = require_auth(&state.pool, &headers).await?;
|
||||
Ok(Json(UserProfileView {
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
email: user.email.unwrap_or_default(),
|
||||
}))
|
||||
}
|
||||
|
||||
async fn require_auth(
|
||||
pool: &PgPool,
|
||||
headers: &HeaderMap,
|
||||
) -> std::result::Result<(UserRow, DeviceRow), (StatusCode, Json<serde_json::Value>)> {
|
||||
let auth = headers
|
||||
.get(header::AUTHORIZATION)
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.and_then(|raw| raw.strip_prefix("Bearer "))
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.ok_or_else(|| unauthorized("missing bearer token"))?;
|
||||
let token_hash = hash_device_login_token(auth);
|
||||
|
||||
let row = sqlx::query_as::<_, DeviceRow>(
|
||||
r#"
|
||||
SELECT
|
||||
d.id,
|
||||
d.display_name,
|
||||
d.platform,
|
||||
d.client_version,
|
||||
d.last_seen_at,
|
||||
NULL::text AS last_ip
|
||||
FROM device_login_tokens t
|
||||
JOIN devices d ON d.id = t.device_id
|
||||
WHERE t.token_hash = $1
|
||||
"#,
|
||||
)
|
||||
.bind(&token_hash)
|
||||
.fetch_optional(pool)
|
||||
.await
|
||||
.map_err(internal_error)?
|
||||
.ok_or_else(|| unauthorized("invalid device token"))?;
|
||||
|
||||
sqlx::query("UPDATE device_login_tokens SET last_seen_at = NOW() WHERE token_hash = $1")
|
||||
.bind(&token_hash)
|
||||
.execute(pool)
|
||||
.await
|
||||
.map_err(internal_error)?;
|
||||
sqlx::query("UPDATE devices SET last_seen_at = NOW() WHERE id = $1")
|
||||
.bind(row.id)
|
||||
.execute(pool)
|
||||
.await
|
||||
.map_err(internal_error)?;
|
||||
|
||||
let user = sqlx::query_as::<_, UserRow>(
|
||||
r#"
|
||||
SELECT u.id, u.email, u.name
|
||||
FROM users u
|
||||
JOIN devices d ON d.user_id = u.id
|
||||
WHERE d.id = $1
|
||||
"#,
|
||||
)
|
||||
.bind(row.id)
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
.map_err(internal_error)?;
|
||||
|
||||
Ok((user, row))
|
||||
}
|
||||
|
||||
async fn ensure_demo_user(pool: &PgPool) -> AnyResult<(Uuid, Uuid)> {
|
||||
let existing =
|
||||
sqlx::query_as::<_, UserRow>("SELECT id, email, name FROM users WHERE email = $1 LIMIT 1")
|
||||
.bind("voson.wang.s@gmail.com")
|
||||
.fetch_optional(pool)
|
||||
.await?;
|
||||
|
||||
let user_id = if let Some(user) = existing {
|
||||
user.id
|
||||
} else {
|
||||
sqlx::query_scalar::<_, Uuid>(
|
||||
r#"
|
||||
INSERT INTO users (email, name)
|
||||
VALUES ($1, $2)
|
||||
RETURNING id
|
||||
"#,
|
||||
)
|
||||
.bind("voson.wang.s@gmail.com")
|
||||
.bind("Voson")
|
||||
.fetch_one(pool)
|
||||
.await?
|
||||
};
|
||||
|
||||
let existing_device = sqlx::query_scalar::<_, Uuid>(
|
||||
"SELECT id FROM devices WHERE user_id = $1 AND display_name = $2 LIMIT 1",
|
||||
)
|
||||
.bind(user_id)
|
||||
.bind("Voson 的 Mac mini")
|
||||
.fetch_optional(pool)
|
||||
.await?;
|
||||
|
||||
let device_id = if let Some(id) = existing_device {
|
||||
id
|
||||
} else {
|
||||
sqlx::query_scalar::<_, Uuid>(
|
||||
r#"
|
||||
INSERT INTO devices (user_id, display_name, platform, client_version, device_fingerprint)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
RETURNING id
|
||||
"#,
|
||||
)
|
||||
.bind(user_id)
|
||||
.bind("Voson 的 Mac mini")
|
||||
.bind("macOS")
|
||||
.bind(env!("CARGO_PKG_VERSION"))
|
||||
.bind(new_device_fingerprint())
|
||||
.fetch_one(pool)
|
||||
.await?
|
||||
};
|
||||
|
||||
Ok((user_id, device_id))
|
||||
}
|
||||
|
||||
async fn upsert_user_from_google(pool: &PgPool, google_user: &GoogleUserInfo) -> AnyResult<Uuid> {
|
||||
let existing = sqlx::query_scalar::<_, Uuid>("SELECT id FROM users WHERE email = $1 LIMIT 1")
|
||||
.bind(&google_user.email)
|
||||
.fetch_optional(pool)
|
||||
.await?;
|
||||
|
||||
if let Some(user_id) = existing {
|
||||
sqlx::query("UPDATE users SET name = $1, updated_at = NOW() WHERE id = $2")
|
||||
.bind(
|
||||
google_user
|
||||
.name
|
||||
.clone()
|
||||
.unwrap_or_else(|| google_user.email.clone()),
|
||||
)
|
||||
.bind(user_id)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
return Ok(user_id);
|
||||
}
|
||||
|
||||
sqlx::query_scalar::<_, Uuid>(
|
||||
r#"
|
||||
INSERT INTO users (email, name)
|
||||
VALUES ($1, $2)
|
||||
RETURNING id
|
||||
"#,
|
||||
)
|
||||
.bind(&google_user.email)
|
||||
.bind(
|
||||
google_user
|
||||
.name
|
||||
.clone()
|
||||
.unwrap_or_else(|| google_user.email.clone()),
|
||||
)
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
.context("failed to create user from google login")
|
||||
}
|
||||
|
||||
async fn upsert_device_for_login(
|
||||
pool: &PgPool,
|
||||
user_id: Uuid,
|
||||
device_name: &str,
|
||||
platform: &str,
|
||||
client_version: &str,
|
||||
device_fingerprint: &str,
|
||||
) -> AnyResult<Uuid> {
|
||||
let existing = sqlx::query_scalar::<_, Uuid>(
|
||||
"SELECT id FROM devices WHERE user_id = $1 AND device_fingerprint = $2 LIMIT 1",
|
||||
)
|
||||
.bind(user_id)
|
||||
.bind(device_fingerprint)
|
||||
.fetch_optional(pool)
|
||||
.await?;
|
||||
|
||||
if let Some(device_id) = existing {
|
||||
sqlx::query(
|
||||
r#"
|
||||
UPDATE devices
|
||||
SET display_name = $1, platform = $2, client_version = $3, last_seen_at = NOW()
|
||||
WHERE id = $4
|
||||
"#,
|
||||
)
|
||||
.bind(device_name)
|
||||
.bind(platform)
|
||||
.bind(client_version)
|
||||
.bind(device_id)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
return Ok(device_id);
|
||||
}
|
||||
|
||||
sqlx::query_scalar::<_, Uuid>(
|
||||
r#"
|
||||
INSERT INTO devices (user_id, display_name, platform, client_version, device_fingerprint)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
RETURNING id
|
||||
"#,
|
||||
)
|
||||
.bind(user_id)
|
||||
.bind(device_name)
|
||||
.bind(platform)
|
||||
.bind(client_version)
|
||||
.bind(device_fingerprint)
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
.context("failed to create device")
|
||||
}
|
||||
|
||||
async fn issue_device_login_token(
|
||||
pool: &PgPool,
|
||||
user_id: Uuid,
|
||||
device_id: Uuid,
|
||||
device_name: &str,
|
||||
platform: &str,
|
||||
client_version: &str,
|
||||
) -> AnyResult<String> {
|
||||
let device_token = new_device_login_token();
|
||||
let token_hash = hash_device_login_token(&device_token);
|
||||
|
||||
sqlx::query("DELETE FROM device_login_tokens WHERE device_id = $1")
|
||||
.bind(device_id)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
sqlx::query("INSERT INTO device_login_tokens (device_id, token_hash) VALUES ($1, $2)")
|
||||
.bind(device_id)
|
||||
.bind(token_hash)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
sqlx::query(
|
||||
r#"
|
||||
INSERT INTO auth_events (
|
||||
user_id, device_id, device_name, platform, client_version, ip_addr, forwarded_ip, login_method, login_result
|
||||
)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, 'google_desktop', 'success')
|
||||
"#,
|
||||
)
|
||||
.bind(user_id)
|
||||
.bind(device_id)
|
||||
.bind(device_name)
|
||||
.bind(platform)
|
||||
.bind(client_version)
|
||||
.bind::<Option<String>>(None)
|
||||
.bind::<Option<String>>(None)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
|
||||
Ok(device_token)
|
||||
}
|
||||
|
||||
fn internal_error<E: std::fmt::Display>(error: E) -> (StatusCode, Json<serde_json::Value>) {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(json!({ "error": error.to_string() })),
|
||||
)
|
||||
}
|
||||
|
||||
fn unauthorized(message: &str) -> (StatusCode, Json<serde_json::Value>) {
|
||||
(StatusCode::UNAUTHORIZED, Json(json!({ "error": message })))
|
||||
}
|
||||
Reference in New Issue
Block a user