refactor: rename user-related files and modules for improved clarity
ZhenYi committed
Modified app/src/main.rs app/src/main.rs +13 −7 View app/src/main.rs
@@ -91,11 +91,14 @@
9191 let conn = queue::RocketMqConn::init(&cfg.rocketmq)
9292 .await
9393 .map_err(|err| anyhow::anyhow!("failed to start the rocketmq producer: {err}"))?;
94 let relay_task = tokio::spawn(queue::relay::run(
95 state.database.primary().clone(),
96 queue::Publisher::new(conn.clone()),
97 cancel.clone(),
98 ));
94 let identity_events = identity
95 .as_ref()
96 .and_then(|runtime| runtime.service().events().cloned());
97 let relay = events::outbox::OutboxRelay::start(
98 identity_events,
99 Some(queue::Publisher::new(conn.clone())),
100 vec![("apihost", state.database.primary().clone())],
101 );
99102 info!("event outbox relay started");
100103
101104 let mut push_tasks = Vec::new();
@@ -179,8 +182,11 @@
179182 _ = cancel.cancelled() => {},
180183 }
181184 };
182 let (api_result, identity_result, ()) = tokio::join!(api, identity_server, signal);
183 let _ = relay_task.await;
185 let (api_result, ()) = tokio::join!(api, signal);
186 if let Err(error) = relay.shutdown().await {
187 warn!(%error, "shutting down the event outbox relay failed");
188 }
189 let identity_result = identity_server.await;
184190 for task in push_tasks {
185191 let _ = task.await;
186192 }
Modified app/Cargo.toml app/Cargo.toml +1 −0 View app/Cargo.toml
@@ -15,6 +15,7 @@
1515clap = { workspace = true }
1616config = { workspace = true }
1717deletion = { workspace = true }
18events = { workspace = true }
1819export = { workspace = true }
1920jwt = { workspace = true }
2021models = { workspace = true }
Deleted lib/accounts/src/organization/mod.rs lib/accounts/src/organization/mod.rs +0 −128
@@ -1,128 +0,0 @@
1use utoipa::ToSchema;
2
3use sea_orm::entity::prelude::*;
4use serde::{Deserialize, Serialize};
5
6pub mod organization_invite_redemptions;
7pub mod organization_invites;
8pub mod organization_join_requests;
9pub mod organization_join_rules;
10pub mod organization_member_permissions;
11pub mod organization_member_roles;
12pub mod organization_members;
13pub mod organization_role_permissions;
14pub mod organization_roles;
15pub mod organizations;
16
17#[derive(
18 Clone, Copy, Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum, Serialize, Deserialize, ToSchema,
19)]
20#[serde(rename_all = "snake_case")]
21#[sea_orm(
22 rs_type = "String",
23 db_type = "String(StringLen::N(16))",
24 rename_all = "snake_case"
25)]
26pub enum OrganizationStatus {
27 Active,
28 Suspended,
29 Archived,
30}
31
32#[derive(
33 Clone, Copy, Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum, Serialize, Deserialize, ToSchema,
34)]
35#[serde(rename_all = "snake_case")]
36#[sea_orm(
37 rs_type = "String",
38 db_type = "String(StringLen::N(16))",
39 rename_all = "snake_case"
40)]
41pub enum OrgMemberRole {
42 Member,
43 Admin,
44 Owner,
45}
46
47#[derive(
48 Clone, Copy, Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum, Serialize, Deserialize, ToSchema,
49)]
50#[serde(rename_all = "snake_case")]
51#[sea_orm(
52 rs_type = "String",
53 db_type = "String(StringLen::N(16))",
54 rename_all = "snake_case"
55)]
56pub enum OrgJoinMethod {
57 Direct,
58 InviteLink,
59 InviteTargeted,
60 InviteEmail,
61}
62
63#[derive(
64 Clone, Copy, Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum, Serialize, Deserialize, ToSchema,
65)]
66#[serde(rename_all = "snake_case")]
67#[sea_orm(
68 rs_type = "String",
69 db_type = "String(StringLen::N(16))",
70 rename_all = "snake_case"
71)]
72pub enum PermissionEffect {
73 Allow,
74 Deny,
75}
76
77/// 会籍的通知偏好,对应旧 `space_members.message_notifications`。
78/// 旧表用 0/1/2 存,这里用字符串存,便于跨服务读写时直接透传。
79#[derive(
80 Clone, Copy, Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum, Serialize, Deserialize, ToSchema,
81)]
82#[serde(rename_all = "snake_case")]
83#[sea_orm(
84 rs_type = "String",
85 db_type = "String(StringLen::N(16))",
86 rename_all = "snake_case"
87)]
88pub enum MessageNotifications {
89 All,
90 Mentions,
91 Nothing,
92}
93
94#[derive(
95 Clone, Copy, Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum, Serialize, Deserialize, ToSchema,
96)]
97#[serde(rename_all = "snake_case")]
98#[sea_orm(
99 rs_type = "String",
100 db_type = "String(StringLen::N(16))",
101 rename_all = "snake_case"
102)]
103#[derive(Default)]
104pub enum OrganizationJoinPolicy {
105 Open,
106 Approval,
107 #[default]
108 InviteOnly,
109 Private,
110}
111
112#[derive(
113 Clone, Copy, Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum, Serialize, Deserialize, ToSchema,
114)]
115#[serde(rename_all = "snake_case")]
116#[sea_orm(
117 rs_type = "String",
118 db_type = "String(StringLen::N(16))",
119 rename_all = "snake_case"
120)]
121#[derive(Default)]
122pub enum OrganizationJoinRequestStatus {
123 #[default]
124 Pending,
125 Approved,
126 Rejected,
127 Withdrawn,
128}
Deleted lib/accounts/src/user/mod.rs lib/accounts/src/user/mod.rs +0 −15
@@ -1,15 +0,0 @@
1pub const DATABASE: &str = "user";
2
3pub mod oauth_clients;
4pub mod oauth_consents;
5pub mod oauth_refresh_tokens;
6pub mod service_token_revocations;
7pub mod user_access_tokens;
8pub mod user_events;
9pub mod user_mfa;
10pub mod user_mfa_backup_codes;
11pub mod user_sessions;
12pub mod user_ssh_keys;
13pub mod user_verification_tokens;
14pub mod user_webauthn_credentials;
15pub mod users;
Deleted lib/accounts/src/lib.rs lib/accounts/src/lib.rs +0 −4
@@ -1,4 +0,0 @@
1pub mod event_outbox;
2pub mod organization;
3mod proto_conv;
4pub mod user;
Deleted lib/accounts/Cargo.toml lib/accounts/Cargo.toml +0 −12
@@ -1,12 +0,0 @@
1[package]
2name = "accounts"
3version.workspace = true
4edition.workspace = true
5
6[dependencies]
7sea-orm = { workspace = true, features = ["with-chrono", "with-uuid", "with-json", "schema-sync", "entity-registry"] }
8chrono = { version = "0.4.45", features = ["serde"] }
9proto = { workspace = true }
10serde = { workspace = true, features = ["derive"] }
11utoipa = { version = "6.0.0", features = ["chrono", "uuid"] }
12uuid = { workspace = true }
Modified lib/config/src/identity/auth.rs lib/config/src/identity/auth.rs +2 −2 View lib/config/src/identity/auth.rs
@@ -94,7 +94,7 @@
9494 pub client_id: String,
9595 /// 同意页展示的应用名。
9696 pub name: String,
97 pub kind: accounts::user::oauth_clients::ClientKind,
97 pub kind: models::users::oauth_clients::ClientKind,
9898 /// 机密客户端的初始 secret;启动时会哈希后入库,明文不落库。
9999 pub secret: Option<String>,
100100 /// 令牌受众;为空时回落 `jwt::PLATFORM_AUDIENCE`。
@@ -115,7 +115,7 @@
115115 Self {
116116 client_id: String::new(),
117117 name: String::new(),
118 kind: accounts::user::oauth_clients::ClientKind::Public,
118 kind: models::users::oauth_clients::ClientKind::Public,
119119 secret: None,
120120 audience: None,
121121 enabled: true,
Modified lib/config/Cargo.toml lib/config/Cargo.toml +1 −1 View lib/config/Cargo.toml
@@ -8,7 +8,7 @@
88sea-orm = { workspace = true, features = ["sqlx-postgres", "runtime-tokio-rustls"] }
99base64 = { workspace = true }
1010figment = { workspace = true, features = ["json", "yaml"] }
11accounts = { workspace = true }
11models = { workspace = true }
1212chrono = { version = "0.4.45", features = ["serde"] }
1313serde_json = { workspace = true }
1414dotenvy = { workspace = true }
Modified lib/events/src/lib.rs lib/events/src/lib.rs +2 −0 View lib/events/src/lib.rs
@@ -1,3 +1,5 @@
1#![recursion_limit = "256"]
2
13pub mod event;
24pub mod handler;
35pub mod outbox;
Modified lib/events/src/outbox.rs lib/events/src/outbox.rs +112 −40 View lib/events/src/outbox.rs
@@ -1,12 +1,13 @@
11use std::sync::LazyLock;
22use std::time::{Duration, Instant};
33
4use accounts::event_outbox;
54use chrono::{DateTime, Duration as ChronoDuration, Utc};
65use futures_util::future;
6use models::event_outbox;
77use opentelemetry::metrics::Gauge;
88use opentelemetry::{KeyValue, global};
9use queue::identity::{DeliveryClass, Envelope, QueueError, SCHEMA_VERSION};
9use queue::identity::{DeliveryClass, Envelope, SCHEMA_VERSION};
10use queue::{Envelope as AppEnvelope, Publisher as AppPublisher};
1011use sea_orm::ActiveValue::Set;
1112use sea_orm::{
1213 ColumnTrait, ConnectionTrait, DatabaseConnection, DatabaseTransaction, DbBackend, DbErr,
@@ -34,7 +35,7 @@
3435const PURGE_INTERVAL: Duration = Duration::from_secs(600);
3536const PURGED_RETENTION_SECS: i64 = 7 * 24 * 60 * 60;
3637
37const OUTBOX_ENTITY_PREFIX: &str = concat!(env!("CARGO_CRATE_NAME"), "::models::event_outbox");
38const OUTBOX_ENTITY_PREFIX: &str = "models::event_outbox";
3839
3940static BACKLOG_AGE: LazyLock<Gauge<f64>> = LazyLock::new(|| {
4041 global::meter("events")
@@ -45,22 +46,22 @@
4546
4647const CLAIM: &str = "
4748WITH pending AS (
48 SELECT candidate.id FROM auth_event_outbox AS candidate
49 SELECT candidate.id FROM event_outbox AS candidate
4950 WHERE candidate.published_at IS NULL AND candidate.available_at <= CURRENT_TIMESTAMP
5051 AND (candidate.leased_until IS NULL OR candidate.leased_until <= CURRENT_TIMESTAMP)
5152 AND NOT EXISTS (
52 SELECT 1 FROM auth_event_outbox AS earlier
53 SELECT 1 FROM event_outbox AS earlier
5354 WHERE earlier.ordering_key = candidate.ordering_key AND earlier.published_at IS NULL
5455 AND (earlier.occurred_at, earlier.id) < (candidate.occurred_at, candidate.id)
5556 )
5657 ORDER BY candidate.occurred_at, candidate.id
5758 LIMIT $1 FOR UPDATE OF candidate SKIP LOCKED
5859)
59UPDATE auth_event_outbox AS events
60UPDATE event_outbox AS events
6061SET lease_id = $2, leased_until = CURRENT_TIMESTAMP + INTERVAL '60 seconds',
6162 attempts = LEAST(events.attempts, 2147483646) + 1
6263FROM pending WHERE events.id = pending.id
63RETURNING events.id, events.subject, events.envelope, events.attempts
64RETURNING events.id, events.subject, events.kind, events.envelope, events.attempts
6465";
6566
6667/// 建表与索引。走 `EntityFirst` 是为了复用启动迁移的 advisory lock:
@@ -68,9 +69,9 @@
6869pub async fn sync_schema(db: &DatabaseConnection) -> Result<(), DbErr> {
6970 db::migrate::EntityFirst::new(&[OUTBOX_ENTITY_PREFIX])
7071 .after([
71 "CREATE INDEX IF NOT EXISTS auth_event_outbox_pending_idx ON auth_event_outbox (available_at, occurred_at, id) WHERE published_at IS NULL",
72 "CREATE INDEX IF NOT EXISTS auth_event_outbox_ordering_idx ON auth_event_outbox (ordering_key, occurred_at, id) WHERE published_at IS NULL",
73 "CREATE INDEX IF NOT EXISTS auth_event_outbox_published_idx ON auth_event_outbox (published_at) WHERE published_at IS NOT NULL",
72 "CREATE INDEX IF NOT EXISTS idx_event_outbox_pending ON event_outbox (available_at, occurred_at, id) WHERE published_at IS NULL",
73 "CREATE INDEX IF NOT EXISTS idx_event_outbox_ordering ON event_outbox (ordering_key, occurred_at, id) WHERE published_at IS NULL",
74 "CREATE INDEX IF NOT EXISTS idx_event_outbox_published ON event_outbox (published_at) WHERE published_at IS NOT NULL",
7475 ])
7576 .run(db)
7677 .await
@@ -169,6 +170,7 @@
169170 event_outbox::Entity::insert(event_outbox::ActiveModel {
170171 id: Set(envelope.id),
171172 subject: Set(event.subject()),
173 kind: Set(envelope.kind.clone()),
172174 ordering_key: Set(ordering_key),
173175 envelope: Set(stored),
174176 occurred_at: Set(envelope.occurred_at),
@@ -237,6 +239,7 @@
237239struct PendingEvent {
238240 id: Uuid,
239241 subject: String,
242 kind: String,
240243 envelope: Value,
241244 attempts: i32,
242245}
@@ -245,9 +248,69 @@
245248 f64::from(2_u32.pow(attempts.clamp(1, 8) as u32))
246249}
247250
251/// identity 事件与 apihost 事件共用一张 outbox,按 kind 的域段决定投递管线。
252fn is_identity_kind(kind: &str) -> bool {
253 matches!(
254 kind.split('.').next().unwrap_or_default(),
255 "auth" | "organization" | "email"
256 )
257}
258
259async fn publish_identity(
260 handler: Option<&EventHandler>,
261 row: &PendingEvent,
262) -> Result<(), String> {
263 let handler = handler.ok_or_else(|| "identity event handler is not configured".to_owned())?;
264 let request_id = row
265 .envelope
266 .get("labels")
267 .and_then(|labels| labels.get("request_id"))
268 .or_else(|| {
269 row.envelope
270 .get("payload")
271 .and_then(|payload| payload.get("request_id"))
272 })
273 .and_then(Value::as_str)
274 .unwrap_or_default();
275 let envelope: Envelope<Value> = serde_json::from_value(row.envelope.clone())
276 .map_err(|error| format!("invalid outbox envelope: {error}"))?;
277 if envelope.id != row.id || envelope.schema_version != SCHEMA_VERSION {
278 return Err("outbox event identity or schema is invalid".to_owned());
279 }
280 handler
281 .publish_durable(&row.subject, &envelope, Some(request_id))
282 .await
283 .map_err(|error| error.to_string())
284}
285
286async fn publish_app(publisher: Option<&AppPublisher>, row: &PendingEvent) -> Result<(), String> {
287 let publisher = publisher.ok_or_else(|| "app event publisher is not configured".to_owned())?;
288 let envelope: AppEnvelope<Value> = serde_json::from_value(row.envelope.clone())
289 .map_err(|error| format!("invalid outbox envelope: {error}"))?;
290 if envelope.id != row.id || envelope.kind != row.kind {
291 return Err("outbox event identity is invalid".to_owned());
292 }
293 let route = queue::route::route(&envelope.kind).map_err(|error| error.to_string())?;
294 let topic = publisher.topic(&route.area);
295 let body = envelope
296 .summary_body(&route.tag)
297 .map_err(|error| error.to_string())?;
298 publisher
299 .publish(
300 &topic,
301 &route.tag,
302 vec![row.id.to_string()],
303 envelope.properties(),
304 body,
305 )
306 .await
307 .map_err(|error| error.to_string())
308}
309
248310async fn publish(
249311 db: &DatabaseConnection,
250 handler: &EventHandler,
312 identity: Option<&EventHandler>,
313 app: Option<&AppPublisher>,
251314 lease_id: Uuid,
252315 row: PendingEvent,
253316) -> Result<(), DbErr> {
@@ -267,51 +330,48 @@
267330 "outbox.publish",
268331 event_id = %row.id,
269332 subject = %row.subject,
333 kind = %row.kind,
270334 request_id = %request_id,
271335 );
272336 let result = async {
273 let envelope: Envelope<Value> = serde_json::from_value(row.envelope)
274 .map_err(|error| QueueError::payload(format!("invalid outbox envelope: {error}")))?;
275 if envelope.id != row.id || envelope.schema_version != SCHEMA_VERSION {
276 return Err(QueueError::payload(
277 "outbox event identity or schema is invalid",
278 ));
337 if is_identity_kind(&row.kind) {
338 publish_identity(identity, &row).await
339 } else {
340 publish_app(app, &row).await
279341 }
280 handler
281 .publish_durable(&row.subject, &envelope, Some(request_id.as_str()))
282 .await
283342 }
284343 .instrument(span);
285344 let result = tokio::time::timeout(PUBLISH_TIMEOUT, result)
286345 .await
287 .unwrap_or_else(|_| Err(QueueError::publish("outbox publish timed out")));
346 .unwrap_or_else(|_| Err("outbox publish timed out".to_owned()));
288347 let statement = match result {
289348 Ok(()) => Statement::from_sql_and_values(
290349 DbBackend::Postgres,
291 "UPDATE auth_event_outbox SET published_at = CURRENT_TIMESTAMP, lease_id = NULL, leased_until = NULL, last_error = NULL WHERE id = $1 AND lease_id = $2 AND published_at IS NULL",
350 "UPDATE event_outbox SET published_at = CURRENT_TIMESTAMP, lease_id = NULL, leased_until = NULL, last_error = NULL WHERE id = $1 AND lease_id = $2 AND published_at IS NULL",
292351 [row.id.into(), lease_id.into()],
293352 ),
294353 Err(error) => {
295 let error_text: String = error.to_string().chars().take(2048).collect();
354 let error_text: String = error.chars().take(2048).collect();
296355 if row.attempts >= MAX_PUBLISH_ATTEMPTS {
297356 tracing::error!(
298357 event_id = %row.id,
299358 subject = %row.subject,
359 kind = %row.kind,
300360 attempt = row.attempts,
301 %error,
361 error = %error_text,
302362 "outbox event exceeded the retry budget and was discarded"
303363 );
304364 Statement::from_sql_and_values(
305365 DbBackend::Postgres,
306 "UPDATE auth_event_outbox SET published_at = CURRENT_TIMESTAMP, lease_id = NULL, leased_until = NULL, last_error = $3 WHERE id = $1 AND lease_id = $2 AND published_at IS NULL",
366 "UPDATE event_outbox SET published_at = CURRENT_TIMESTAMP, lease_id = NULL, leased_until = NULL, last_error = $3 WHERE id = $1 AND lease_id = $2 AND published_at IS NULL",
307367 [row.id.into(), lease_id.into(), error_text.into()],
308368 )
309369 } else {
310 tracing::warn!(event_id = %row.id, subject = %row.subject, attempt = row.attempts, %error, "outbox publish will retry");
370 tracing::warn!(event_id = %row.id, subject = %row.subject, kind = %row.kind, attempt = row.attempts, error = %error_text, "outbox publish will retry");
311371 let delay = retry_delay(row.attempts) + f64::from(row.id.as_bytes()[15] % 5);
312372 Statement::from_sql_and_values(
313373 DbBackend::Postgres,
314 "UPDATE auth_event_outbox SET available_at = CURRENT_TIMESTAMP + ($3 * INTERVAL '1 second'), lease_id = NULL, leased_until = NULL, last_error = $4 WHERE id = $1 AND lease_id = $2 AND published_at IS NULL",
374 "UPDATE event_outbox SET available_at = CURRENT_TIMESTAMP + ($3 * INTERVAL '1 second'), lease_id = NULL, leased_until = NULL, last_error = $4 WHERE id = $1 AND lease_id = $2 AND published_at IS NULL",
315375 [
316376 row.id.into(),
317377 lease_id.into(),
@@ -332,7 +392,8 @@
332392#[tracing::instrument(skip_all, fields(request_id = %_request_id.unwrap_or_default()))]
333393async fn run_batch(
334394 db: &DatabaseConnection,
335 handler: &EventHandler,
395 identity: Option<&EventHandler>,
396 app: Option<&AppPublisher>,
336397 _request_id: Option<&str>,
337398) -> Result<usize, DbErr> {
338399 let lease_id = Uuid::now_v7();
@@ -348,6 +409,7 @@
348409 pending.push(PendingEvent {
349410 id: row.try_get("", "id")?,
350411 subject: row.try_get("", "subject")?,
412 kind: row.try_get("", "kind")?,
351413 envelope: row.try_get("", "envelope")?,
352414 attempts: row.try_get("", "attempts")?,
353415 });
@@ -356,7 +418,7 @@
356418 let results = future::join_all(
357419 pending
358420 .into_iter()
359 .map(|row| publish(db, handler, lease_id, row)),
421 .map(|row| publish(db, identity, app, lease_id, row)),
360422 )
361423 .await;
362424 for result in results {
@@ -368,7 +430,7 @@
368430async fn purge_published(db: &DatabaseConnection) {
369431 match db
370432 .execute_unprepared(&format!(
371 "DELETE FROM auth_event_outbox WHERE published_at < CURRENT_TIMESTAMP - INTERVAL '{PURGED_RETENTION_SECS} seconds'"
433 "DELETE FROM event_outbox WHERE published_at < CURRENT_TIMESTAMP - INTERVAL '{PURGED_RETENTION_SECS} seconds'"
372434 ))
373435 .await
374436 {
@@ -386,7 +448,8 @@
386448async fn run_database(
387449 domain: &'static str,
388450 db: DatabaseConnection,
389 handler: EventHandler,
451 identity: Option<EventHandler>,
452 app: Option<AppPublisher>,
390453 cancel: CancellationToken,
391454) {
392455 let mut sample_at = Instant::now();
@@ -403,7 +466,7 @@
403466 sample_at = Instant::now() + BACKLOG_SAMPLE_INTERVAL;
404467 let sample = db.query_one_raw(Statement::from_string(
405468 DbBackend::Postgres,
406 "SELECT COALESCE(EXTRACT(EPOCH FROM (CURRENT_TIMESTAMP - (SELECT available_at FROM auth_event_outbox WHERE published_at IS NULL AND available_at <= CURRENT_TIMESTAMP ORDER BY available_at, occurred_at, id LIMIT 1))), 0)::double precision AS age",
469 "SELECT COALESCE(EXTRACT(EPOCH FROM (CURRENT_TIMESTAMP - (SELECT available_at FROM event_outbox WHERE published_at IS NULL AND available_at <= CURRENT_TIMESTAMP ORDER BY available_at, occurred_at, id LIMIT 1))), 0)::double precision AS age",
407470 )).await;
408471 match sample {
409472 Ok(Some(row)) => match row.try_get::<f64>("", "age") {
@@ -416,7 +479,7 @@
416479 Err(error) => tracing::warn!(%domain, %error, "outbox backlog sample failed"),
417480 }
418481 }
419 match run_batch(&db, &handler, None).await {
482 match run_batch(&db, identity.as_ref(), app.as_ref(), None).await {
420483 Ok(count) if count > 0 => continue,
421484 Ok(_) => {}
422485 Err(error) => {
@@ -437,16 +500,23 @@
437500
438501impl OutboxRelay {
439502 pub fn start(
440 handler: Option<EventHandler>,
503 identity: Option<EventHandler>,
504 app: Option<AppPublisher>,
441505 databases: Vec<(&'static str, DatabaseConnection)>,
442506 ) -> Self {
443507 let cancel = CancellationToken::new();
444 let join = handler.map(|handler| {
508 let join = if identity.is_some() || app.is_some() {
445509 let cancel = cancel.clone();
446 tokio::spawn(async move {
510 Some(tokio::spawn(async move {
447511 let mut workers = JoinSet::new();
448512 for (domain, db) in databases {
449 workers.spawn(run_database(domain, db, handler.clone(), cancel.clone()));
513 workers.spawn(run_database(
514 domain,
515 db,
516 identity.clone(),
517 app.clone(),
518 cancel.clone(),
519 ));
450520 }
451521 while let Some(result) = workers.join_next().await {
452522 result.map_err(|error| {
@@ -459,11 +529,13 @@
459529 }
460530 }
461531 Ok(())
462 })
463 });
532 }))
533 } else {
534 None
535 };
464536 if join.is_none() {
465537 tracing::warn!(
466 "event forwarding is disabled; reliable events remain pending in the database outboxes"
538 "event forwarding is disabled; reliable events remain pending in the database outbox"
467539 );
468540 }
469541 Self { cancel, join }
Modified lib/events/Cargo.toml lib/events/Cargo.toml +1 −1 View lib/events/Cargo.toml
@@ -7,7 +7,7 @@
77config = { workspace = true }
88db = { workspace = true }
99envelope = { workspace = true }
10accounts = { workspace = true }
10models = { workspace = true }
1111queue = { workspace = true }
1212chrono = { version = "0.4.45", features = ["serde"] }
1313futures-util = "0.3.34"
Modified lib/export/src/events.rs lib/export/src/events.rs +7 −3 View lib/export/src/events.rs
@@ -1,9 +1,13 @@
1use models::system::event_outbox;
1use models::event_outbox;
22use queue::{Envelope, Labels};
33use sea_orm::{ActiveModelTrait, ConnectionTrait, DbErr, EntityTrait, Set};
44use serde::Serialize;
55use uuid::Uuid;
66
7fn timestamp(value: time::OffsetDateTime) -> chrono::DateTime<chrono::Utc> {
8 chrono::DateTime::from_timestamp(value.unix_timestamp(), value.nanosecond()).unwrap_or_default()
9}
10
711fn active_model<T: Serialize>(
812 kind: &str,
913 labels: Labels,
@@ -26,8 +30,8 @@
2630 kind: Set(kind.to_string()),
2731 ordering_key: Set(ordering_key.to_string()),
2832 envelope: Set(stored),
29 occurred_at: Set(envelope.occurred_at),
30 available_at: Set(envelope.occurred_at),
33 occurred_at: Set(timestamp(envelope.occurred_at)),
34 available_at: Set(timestamp(envelope.occurred_at)),
3135 ..Default::default()
3236 })
3337}
Modified lib/export/Cargo.toml lib/export/Cargo.toml +1 −0 View lib/export/Cargo.toml
@@ -29,6 +29,7 @@
2929utoipa = { workspace = true }
3030tower = { workspace = true }
3131serde_json = { workspace = true }
32chrono = { version = "0.4.45", features = ["serde"] }
3233uuid = { workspace = true }
3334bytes = { workspace = true }
3435thiserror = { workspace = true }
Modified lib/identity/src/auth/access_token/service.rs lib/identity/src/auth/access_token/service.rs +2 −2 View lib/identity/src/auth/access_token/service.rs
@@ -1,6 +1,6 @@
11use crate::events::{AuthEvent, Labels, RestOperate, RuntimeEvent};
2use crate::models::user::user_events::UserEventType;
3use crate::models::user::{user_access_tokens, users};
2use crate::models::users::user_events::UserEventType;
3use crate::models::users::{user_access_tokens, users};
44use crate::session::Session;
55use chrono::{DateTime, Duration, Utc};
66use sea_orm::entity::Set;
Modified lib/identity/src/auth/client/registry.rs lib/identity/src/auth/client/registry.rs +1 −1 View lib/identity/src/auth/client/registry.rs
@@ -12,7 +12,7 @@
1212use crate::auth::share::crypto;
1313use crate::config::auth::{OAuthClient, OAuthConfig};
1414use crate::db::DbConn;
15use crate::models::user::oauth_clients::{
15use crate::models::users::oauth_clients::{
1616 ActiveModel as ClientActiveModel, ClientKind, ClientSource, Column, Entity as ClientEntity,
1717 Model as ClientModel,
1818};
Modified lib/identity/src/auth/grpc/access_key.rs lib/identity/src/auth/grpc/access_key.rs +2 −2 View lib/identity/src/auth/grpc/access_key.rs
@@ -1,5 +1,5 @@
1use crate::models::user::user_access_tokens;
2use crate::models::user::user_events::UserEventType;
1use crate::models::users::user_access_tokens;
2use crate::models::users::user_events::UserEventType;
33use crate::protos::auth::common::v1::{AuthMethod, Code, PageResponse, TokenScope};
44use crate::protos::auth::v1::access_key_service_server::{
55 AccessKeyService, AccessKeyServiceServer,
Modified lib/identity/src/auth/grpc/audit.rs lib/identity/src/auth/grpc/audit.rs +1 −1 View lib/identity/src/auth/grpc/audit.rs
@@ -1,4 +1,4 @@
1use crate::models::user::user_events::{self, UserEventType};
1use crate::models::users::user_events::{self, UserEventType};
22use crate::protos::auth::internal::v1::audit_service_server::{AuditService, AuditServiceServer};
33use crate::protos::auth::internal::v1::*;
44use chrono::{Duration, Utc};
Modified lib/identity/src/auth/grpc/authority.rs lib/identity/src/auth/grpc/authority.rs +1 −1 View lib/identity/src/auth/grpc/authority.rs
@@ -1,6 +1,6 @@
11use crate::jwt::JwtError;
22use crate::jwt::authority::{AccessRecord, Authority, UserRecord};
3use crate::models::user::users;
3use crate::models::users::users;
44use crate::protos::auth::common::v1::{Code, PageResponse};
55use crate::protos::auth::internal::v1::authority_service_server::{
66 AuthorityService, AuthorityServiceServer,
Modified lib/identity/src/auth/grpc/authorization.rs lib/identity/src/auth/grpc/authorization.rs +1 −1 View lib/identity/src/auth/grpc/authorization.rs
@@ -1,6 +1,6 @@
11use std::time::Duration;
22
3use crate::models::user::users;
3use crate::models::users::users;
44use crate::protos::auth::common::v1::Code;
55use crate::protos::auth::v1::authorization_service_server::{
66 AuthorizationService, AuthorizationServiceServer,
Modified lib/identity/src/auth/grpc/client.rs lib/identity/src/auth/grpc/client.rs +2 −2 View lib/identity/src/auth/grpc/client.rs
@@ -9,7 +9,7 @@
99use crate::auth::client::{ClientRecord, ClientSpec};
1010use crate::auth::errors::AuthError;
1111use crate::auth::grpc::{context, convert, error};
12use crate::models::user::oauth_clients::{
12use crate::models::users::oauth_clients::{
1313 ClientKind as ModelKind, ClientSource as ModelSource, Entity as ClientEntity,
1414};
1515use crate::protos::auth::internal::v1::client_service_server::{
@@ -46,7 +46,7 @@
4646 }
4747}
4848
49fn record_of(row: &crate::models::user::oauth_clients::Model) -> OAuthClientRecord {
49fn record_of(row: &crate::models::users::oauth_clients::Model) -> OAuthClientRecord {
5050 OAuthClientRecord {
5151 client_id: row.client_id.clone(),
5252 name: row.name.clone(),
Modified lib/identity/src/auth/grpc/convert.rs lib/identity/src/auth/grpc/convert.rs +1 −1 View lib/identity/src/auth/grpc/convert.rs
@@ -1,4 +1,4 @@
1use crate::models::user::{user_access_tokens, user_sessions, user_ssh_keys};
1use crate::models::users::{user_access_tokens, user_sessions, user_ssh_keys};
22use crate::protos::auth::common::v1::{AccessKeyInfo, SessionInfo, SshKeyInfo};
33use chrono::{DateTime, NaiveDate, TimeZone, Utc};
44use prost_types::Timestamp;
Modified lib/identity/src/auth/grpc/identity.rs lib/identity/src/auth/grpc/identity.rs +10 −10 View lib/identity/src/auth/grpc/identity.rs
@@ -1,4 +1,4 @@
1use crate::models::user::{user_ssh_keys, users};
1use crate::models::users::{user_ssh_keys, users};
22use crate::protos::auth::common::v1::{
33 AuthMethod, Code, Credential, Principal, TokenScope, credential,
44};
@@ -333,23 +333,23 @@
333333 "" => None,
334334 value => uuid::Uuid::parse_str(value).ok(),
335335 };
336 let mut update = crate::models::user::user_sessions::Entity::update_many()
336 let mut update = crate::models::users::user_sessions::Entity::update_many()
337337 .col_expr(
338 crate::models::user::user_sessions::Column::RevokedAt,
338 crate::models::users::user_sessions::Column::RevokedAt,
339339 Expr::value(Some(now)),
340340 )
341341 .col_expr(
342 crate::models::user::user_sessions::Column::Version,
343 Expr::col(crate::models::user::user_sessions::Column::Version).add(1),
342 crate::models::users::user_sessions::Column::Version,
343 Expr::col(crate::models::users::user_sessions::Column::Version).add(1),
344344 )
345345 .col_expr(
346 crate::models::user::user_sessions::Column::UpdatedAt,
346 crate::models::users::user_sessions::Column::UpdatedAt,
347347 Expr::value(now),
348348 )
349 .filter(crate::models::user::user_sessions::Column::UserId.eq(user.id))
350 .filter(crate::models::user::user_sessions::Column::RevokedAt.is_null());
349 .filter(crate::models::users::user_sessions::Column::UserId.eq(user.id))
350 .filter(crate::models::users::user_sessions::Column::RevokedAt.is_null());
351351 if let Some(keep) = keep {
352 update = update.filter(crate::models::user::user_sessions::Column::Id.ne(keep));
352 update = update.filter(crate::models::users::user_sessions::Column::Id.ne(keep));
353353 }
354354 let revoked = update.exec(db.write()).await?;
355355
@@ -362,7 +362,7 @@
362362 db,
363363 user.id,
364364 Some(user.id),
365 crate::models::user::user_events::UserEventType::PasswordChange,
365 crate::models::users::user_events::UserEventType::PasswordChange,
366366 Some(serde_json::json!({ "action": "password_changed" })),
367367 request_id,
368368 match request.client_ip.trim() {
Modified lib/identity/src/auth/grpc/key_rotation.rs lib/identity/src/auth/grpc/key_rotation.rs +2 −2 View lib/identity/src/auth/grpc/key_rotation.rs
@@ -1,5 +1,5 @@
1use crate::models::user::user_mfa::MfaStatus;
2use crate::models::user::{user_access_tokens, user_mfa, user_sessions};
1use crate::models::users::user_mfa::MfaStatus;
2use crate::models::users::{user_access_tokens, user_mfa, user_sessions};
33use crate::protos::auth::internal::v1::get_key_inventory_response::KeyInfo;
44use crate::protos::auth::internal::v1::key_rotation_service_server::{
55 KeyRotationService, KeyRotationServiceServer,
Modified lib/identity/src/auth/grpc/mfa.rs lib/identity/src/auth/grpc/mfa.rs +3 −3 View lib/identity/src/auth/grpc/mfa.rs
@@ -1,6 +1,6 @@
1use crate::models::user::user_events::UserEventType;
2use crate::models::user::user_mfa::{self, MfaStatus, MfaType};
3use crate::models::user::user_mfa_backup_codes;
1use crate::models::users::user_events::UserEventType;
2use crate::models::users::user_mfa::{self, MfaStatus, MfaType};
3use crate::models::users::user_mfa_backup_codes;
44use crate::protos::auth::common::v1::{Code, MfaStatus as ProtoMfaStatus, MfaType as ProtoMfaType};
55use crate::protos::auth::v1::mfa_service_server::{MfaService, MfaServiceServer};
66use crate::protos::auth::v1::*;
Modified lib/identity/src/auth/grpc/organization.rs lib/identity/src/auth/grpc/organization.rs +4 −4 View lib/identity/src/auth/grpc/organization.rs
@@ -16,7 +16,7 @@
1616 UpdateBanBody, UpdateMemberBody, UpdateOrganizationBody,
1717};
1818use crate::auth::organization::{member, role, service};
19use crate::models::organization::{
19use crate::models::space::{
2020 MessageNotifications as DomainNotifications, OrgJoinMethod as DomainJoin,
2121 OrgMemberRole as DomainRole, OrganizationJoinPolicy as DomainJoinPolicy,
2222 OrganizationJoinRequestStatus as DomainRequestStatus, OrganizationStatus as DomainStatus,
@@ -2192,13 +2192,13 @@
21922192 let member = self
21932193 .svc
21942194 .member_summary(
2195 &crate::models::organization::organization_members::Entity::find()
2195 &crate::models::space::organization_members::Entity::find()
21962196 .filter(
2197 crate::models::organization::organization_members::Column::OrganizationId
2197 crate::models::space::organization_members::Column::OrganizationId
21982198 .eq(organization_id),
21992199 )
22002200 .filter(
2201 crate::models::organization::organization_members::Column::UserId
2201 crate::models::space::organization_members::Column::UserId
22022202 .eq(summary.user_id),
22032203 )
22042204 .one(self.svc.db().read())
Modified lib/identity/src/auth/grpc/session.rs lib/identity/src/auth/grpc/session.rs +2 −2 View lib/identity/src/auth/grpc/session.rs
@@ -1,5 +1,5 @@
1use crate::models::user::user_events::UserEventType;
2use crate::models::user::{user_sessions, users};
1use crate::models::users::user_events::UserEventType;
2use crate::models::users::{user_sessions, users};
33use crate::protos::auth::v1::session_service_server::{SessionService, SessionServiceServer};
44use crate::protos::auth::v1::*;
55use sea_orm::sea_query::{Expr, ExprTrait};
Modified lib/identity/src/auth/grpc/ssh_key.rs lib/identity/src/auth/grpc/ssh_key.rs +2 −2 View lib/identity/src/auth/grpc/ssh_key.rs
@@ -1,5 +1,5 @@
1use crate::models::user::user_events::UserEventType;
2use crate::models::user::{user_ssh_keys, users};
1use crate::models::users::user_events::UserEventType;
2use crate::models::users::{user_ssh_keys, users};
33use crate::protos::auth::common::v1::{AuthMethod, PageResponse};
44use crate::protos::auth::v1::ssh_key_service_server::{SshKeyService, SshKeyServiceServer};
55use crate::protos::auth::v1::*;
Modified lib/identity/src/auth/grpc/store.rs lib/identity/src/auth/grpc/store.rs +6 −6 View lib/identity/src/auth/grpc/store.rs
@@ -1,4 +1,4 @@
1use crate::models::user::users;
1use crate::models::users::users;
22use crate::protos::auth::common::v1::{
33 AuthMethod, MfaStatus, MfaType, Principal, SshKeyAlgorithm, TokenScope, UserPublic, UserRef,
44 UserStatus, user_ref,
@@ -191,7 +191,7 @@
191191 db: &crate::db::DbConn,
192192 user_id: i64,
193193 actor_user_id: Option<i64>,
194 event_type: crate::models::user::user_events::UserEventType,
194 event_type: crate::models::users::user_events::UserEventType,
195195 details: Option<serde_json::Value>,
196196 request_id: Option<&str>,
197197 client_ip: Option<String>,
@@ -230,11 +230,11 @@
230230 value as i32
231231}
232232
233pub fn ssh_algorithm_code(value: crate::models::user::user_ssh_keys::PublicKeyType) -> i32 {
233pub fn ssh_algorithm_code(value: crate::models::users::user_ssh_keys::PublicKeyType) -> i32 {
234234 let algorithm = match value {
235 crate::models::user::user_ssh_keys::PublicKeyType::RSA => SshKeyAlgorithm::Rsa,
236 crate::models::user::user_ssh_keys::PublicKeyType::ED25519 => SshKeyAlgorithm::Ed25519,
237 crate::models::user::user_ssh_keys::PublicKeyType::ECDSA => SshKeyAlgorithm::Ecdsa,
235 crate::models::users::user_ssh_keys::PublicKeyType::RSA => SshKeyAlgorithm::Rsa,
236 crate::models::users::user_ssh_keys::PublicKeyType::ED25519 => SshKeyAlgorithm::Ed25519,
237 crate::models::users::user_ssh_keys::PublicKeyType::ECDSA => SshKeyAlgorithm::Ecdsa,
238238 };
239239 algorithm as i32
240240}
Modified lib/identity/src/auth/grpc/token.rs lib/identity/src/auth/grpc/token.rs +1 −1 View lib/identity/src/auth/grpc/token.rs
@@ -1,6 +1,6 @@
11use std::time::Duration;
22
3use crate::models::user::{service_token_revocations, user_access_tokens};
3use crate::models::users::{service_token_revocations, user_access_tokens};
44use crate::protos::auth::common::v1::{Code, TokenAudience, TokenScope, credential};
55use crate::protos::auth::v1::issue_service_token_request::Constraint;
66use crate::protos::auth::v1::token_service_server::{TokenService, TokenServiceServer};
Modified lib/identity/src/auth/login/service.rs lib/identity/src/auth/login/service.rs +3 −3 View lib/identity/src/auth/login/service.rs
@@ -1,7 +1,7 @@
11use crate::events::{AuthEvent, Labels, RestOperate, RuntimeEvent};
2use crate::models::user::user_events::UserEventType;
3use crate::models::user::user_mfa::{self, MfaStatus, MfaType};
4use crate::models::user::users;
2use crate::models::users::user_events::UserEventType;
3use crate::models::users::user_mfa::{self, MfaStatus, MfaType};
4use crate::models::users::users;
55use crate::session::Session;
66use sea_orm::sea_query::{Expr, ExprTrait, Func};
77use sea_orm::{ColumnTrait, Condition, EntityTrait, QueryFilter};
Modified lib/identity/src/auth/logout/service.rs lib/identity/src/auth/logout/service.rs +2 −2 View lib/identity/src/auth/logout/service.rs
@@ -1,5 +1,5 @@
11use crate::events::{AuthEvent, Labels, RestOperate, RuntimeEvent};
2use crate::models::user::user_events::UserEventType;
2use crate::models::users::user_events::UserEventType;
33use crate::session::Session;
44use sea_orm::Set;
55
@@ -37,7 +37,7 @@
3737 session.purge();
3838
3939 if let Some(id) = stored_session_id {
40 use crate::models::user::user_sessions;
40 use crate::models::users::user_sessions;
4141 use sea_orm::{ColumnTrait, EntityTrait, QueryFilter};
4242 if let Err(error) = user_sessions::Entity::update_many()
4343 .set(user_sessions::ActiveModel {
Modified lib/identity/src/auth/me/service.rs lib/identity/src/auth/me/service.rs +1 −1 View lib/identity/src/auth/me/service.rs
@@ -1,7 +1,7 @@
11use std::sync::OnceLock;
22use std::time::Duration;
33
4use crate::models::user::users;
4use crate::models::users::users;
55use crate::session::Session;
66use sea_orm::{ColumnTrait, EntityTrait, QueryFilter};
77
Modified lib/identity/src/auth/me/types.rs lib/identity/src/auth/me/types.rs +1 −1 View lib/identity/src/auth/me/types.rs
@@ -1,4 +1,4 @@
1use crate::models::user::users;
1use crate::models::users::users;
22use chrono::{DateTime, Utc};
33use serde::{Deserialize, Serialize};
44use utoipa::ToSchema;
Modified lib/identity/src/auth/oauth/consent.rs lib/identity/src/auth/oauth/consent.rs +1 −1 View lib/identity/src/auth/oauth/consent.rs
@@ -11,7 +11,7 @@
1111
1212use crate::auth::AuthSvc;
1313use crate::auth::errors::AuthResult;
14use crate::models::user::oauth_consents as consents;
14use crate::models::users::oauth_consents as consents;
1515
1616async fn load(svc: &AuthSvc, user_id: i64, client_id: &str) -> AuthResult<Option<consents::Model>> {
1717 Ok(consents::Entity::find()
Modified lib/identity/src/auth/oauth/refresh.rs lib/identity/src/auth/oauth/refresh.rs +1 −1 View lib/identity/src/auth/oauth/refresh.rs
@@ -9,7 +9,7 @@
99use crate::auth::AuthSvc;
1010use crate::auth::errors::{AuthError, AuthResult};
1111use crate::auth::share::crypto::{random_b64url, sha256_hex};
12use crate::models::user::{oauth_refresh_tokens as tokens, user_sessions};
12use crate::models::users::{oauth_refresh_tokens as tokens, user_sessions};
1313
1414/// 明文令牌长度:48 字节随机 → 64 字符 base64url。
1515const TOKEN_BYTES: usize = 48;
Modified lib/identity/src/auth/organization/invite.rs lib/identity/src/auth/organization/invite.rs +5 −5 View lib/identity/src/auth/organization/invite.rs
@@ -16,7 +16,7 @@
1616};
1717use crate::auth::share::crypto;
1818use crate::events::{OrganizationEvent, RestOperate};
19use crate::models::organization::{
19use crate::models::space::{
2020 OrgJoinMethod, OrgMemberRole, OrganizationStatus, organization_invite_redemptions,
2121 organization_invites, organization_roles,
2222};
@@ -102,8 +102,8 @@
102102 .transpose()?;
103103 let method = match target_user_id {
104104 Some(user_id) => {
105 crate::models::user::users::Entity::find_by_id(user_id)
106 .filter(crate::models::user::users::Column::DeletedAt.is_null())
105 crate::models::users::users::Entity::find_by_id(user_id)
106 .filter(crate::models::users::users::Column::DeletedAt.is_null())
107107 .one(self.db().read())
108108 .await?
109109 .ok_or(AuthError::UserNotFound)?;
@@ -295,8 +295,8 @@
295295 if organization.status != OrganizationStatus::Active {
296296 return Err(AuthError::OrganizationInactive);
297297 }
298 let actor = crate::models::user::users::Entity::find_by_id(actor_user_id)
299 .filter(crate::models::user::users::Column::DeletedAt.is_null())
298 let actor = crate::models::users::users::Entity::find_by_id(actor_user_id)
299 .filter(crate::models::users::users::Column::DeletedAt.is_null())
300300 .one(self.db().read())
301301 .await?
302302 .ok_or(AuthError::UserNotFound)?;
Modified lib/identity/src/auth/organization/join.rs lib/identity/src/auth/organization/join.rs +9 −13 View lib/identity/src/auth/organization/join.rs
@@ -11,11 +11,11 @@
1111};
1212use crate::auth::organization::{member, service};
1313use crate::auth::{AuthError, AuthResult, AuthSvc};
14use crate::models::organization::{
14use crate::models::space::{
1515 OrgJoinMethod, OrgMemberRole, OrganizationJoinRequestStatus, organization_join_requests,
1616 organization_join_rules, organization_members, organization_roles, organizations,
1717};
18use crate::models::user::users;
18use crate::models::users::users;
1919
2020fn rule_summary(row: &organization_join_rules::Model) -> JoinRuleSummary {
2121 JoinRuleSummary {
@@ -205,7 +205,7 @@
205205 request_id: Option<&str>,
206206 ) -> AuthResult<JoinRequestSummary> {
207207 let organization = self.load_join_organization(organization_id).await?;
208 if organization.status != crate::models::organization::OrganizationStatus::Active {
208 if organization.status != crate::models::space::OrganizationStatus::Active {
209209 return Err(AuthError::OrganizationInactive);
210210 }
211211 if let Some(existing) =
@@ -403,8 +403,8 @@
403403 ) -> AuthResult<(crate::auth::organization::types::MemberSummary, bool)> {
404404 let txn = self.db().write().begin().await?;
405405 // 机器人成员的角色上限:OWNER 只能由人类走成员面转移,不能通过内部接口直接塞入。
406 let user = crate::models::user::users::Entity::find_by_id(user_id)
407 .filter(crate::models::user::users::Column::DeletedAt.is_null())
406 let user = crate::models::users::users::Entity::find_by_id(user_id)
407 .filter(crate::models::users::users::Column::DeletedAt.is_null())
408408 .one(&txn)
409409 .await?
410410 .ok_or(AuthError::UserNotFound)?;
@@ -451,19 +451,15 @@
451451 }
452452 }
453453 for role in roles {
454 let existing = crate::models::organization::organization_member_roles::Entity::find()
455 .filter(
456 crate::models::organization::organization_member_roles::Column::MemberId
457 .eq(row.id),
458 )
454 let existing = crate::models::space::organization_member_roles::Entity::find()
459455 .filter(
460 crate::models::organization::organization_member_roles::Column::RoleId
461 .eq(role.id),
456 crate::models::space::organization_member_roles::Column::MemberId.eq(row.id),
462457 )
458 .filter(crate::models::space::organization_member_roles::Column::RoleId.eq(role.id))
463459 .one(&txn)
464460 .await?;
465461 if existing.is_none() {
466 crate::models::organization::organization_member_roles::ActiveModel {
462 crate::models::space::organization_member_roles::ActiveModel {
467463 member_id: Set(row.id),
468464 role_id: Set(role.id),
469465 ..Default::default()
Modified lib/identity/src/auth/organization/member.rs lib/identity/src/auth/organization/member.rs +2 −2 View lib/identity/src/auth/organization/member.rs
@@ -16,10 +16,10 @@
1616 SetMemberRoleBody, UpdateBanBody, UpdateMemberBody, normalize_alias,
1717};
1818use crate::events::{OrganizationEvent, RestOperate};
19use crate::models::organization::{
19use crate::models::space::{
2020 OrgJoinMethod, OrgMemberRole, organization_member_permissions, organization_members,
2121};
22use crate::models::user::users;
22use crate::models::users::users;
2323
2424pub(crate) async fn find_any<C: ConnectionTrait>(
2525 db: &C,
Modified lib/identity/src/auth/organization/permission.rs lib/identity/src/auth/organization/permission.rs +2 −2 View lib/identity/src/auth/organization/permission.rs
@@ -11,7 +11,7 @@
1111 SYSTEM_ROLE_OWNER, is_admin_permission, is_known_permission,
1212};
1313use crate::auth::organization::{member, role, service};
14use crate::models::organization::{
14use crate::models::space::{
1515 OrgMemberRole, PermissionEffect, organization_member_permissions, organization_member_roles,
1616 organization_members, organization_role_permissions,
1717};
@@ -208,7 +208,7 @@
208208) -> AuthResult<Gate> {
209209 let organization = svc.load_organization(organization_id).await?;
210210 let version = organization.version;
211 if organization.status != crate::models::organization::OrganizationStatus::Active {
211 if organization.status != crate::models::space::OrganizationStatus::Active {
212212 return Ok(Gate::Denied {
213213 version,
214214 reason: "org_inactive",
Modified lib/identity/src/auth/organization/role.rs lib/identity/src/auth/organization/role.rs +8 −10 View lib/identity/src/auth/organization/role.rs
@@ -16,7 +16,7 @@
1616};
1717use crate::auth::organization::{error, member};
1818use crate::events::{OrganizationEvent, RestOperate};
19use crate::models::organization::{
19use crate::models::space::{
2020 PermissionEffect, organization_member_roles, organization_role_permissions, organization_roles,
2121};
2222
@@ -673,7 +673,7 @@
673673 member_id,
674674 )
675675 .await?;
676 if member.role == crate::models::organization::OrgMemberRole::Owner {
676 if member.role == crate::models::space::OrgMemberRole::Owner {
677677 return Err(AuthError::OrganizationOwnerRequired);
678678 }
679679 let grants = Self::validate_grants(&grants)?;
@@ -681,15 +681,15 @@
681681 .await?;
682682
683683 let txn = self.db().write().begin().await?;
684 crate::models::organization::organization_member_permissions::Entity::delete_many()
684 crate::models::space::organization_member_permissions::Entity::delete_many()
685685 .filter(
686 crate::models::organization::organization_member_permissions::Column::MemberId
686 crate::models::space::organization_member_permissions::Column::MemberId
687687 .eq(member_id),
688688 )
689689 .exec(&txn)
690690 .await?;
691691 for grant in &grants {
692 crate::models::organization::organization_member_permissions::ActiveModel {
692 crate::models::space::organization_member_permissions::ActiveModel {
693693 member_id: Set(member_id),
694694 permission: Set(grant.permission.clone()),
695695 effect: Set(grant.effect),
@@ -720,14 +720,12 @@
720720 }
721721
722722 pub(crate) async fn member_permissions(&self, member_id: Uuid) -> AuthResult<Vec<String>> {
723 let rows = crate::models::organization::organization_member_permissions::Entity::find()
723 let rows = crate::models::space::organization_member_permissions::Entity::find()
724724 .filter(
725 crate::models::organization::organization_member_permissions::Column::MemberId
725 crate::models::space::organization_member_permissions::Column::MemberId
726726 .eq(member_id),
727727 )
728 .order_by_asc(
729 crate::models::organization::organization_member_permissions::Column::Permission,
730 )
728 .order_by_asc(crate::models::space::organization_member_permissions::Column::Permission)
731729 .all(self.db().read())
732730 .await?;
733731 Ok(rows
Modified lib/identity/src/auth/organization/service.rs lib/identity/src/auth/organization/service.rs +4 −4 View lib/identity/src/auth/organization/service.rs
@@ -17,12 +17,12 @@
1717};
1818use crate::auth::organization::{error, member, permission, role};
1919use crate::events::{Labels, OrganizationEvent, RestOperate, RuntimeEvent};
20use crate::models::organization::{
20use crate::models::space::{
2121 OrgMemberRole, OrganizationStatus, PermissionEffect, organization_invite_redemptions,
2222 organization_invites, organization_member_permissions, organization_member_roles,
2323 organization_members, organization_role_permissions, organization_roles, organizations,
2424};
25use crate::models::user::users;
25use crate::models::users::users;
2626
2727pub(crate) fn perm_cache_prefix(organization_id: Uuid, user_id: i64) -> String {
2828 format!("auth:org:perm:{organization_id}:{user_id}")
@@ -208,7 +208,7 @@
208208 owner_user_id: Set(actor_user_id),
209209 join_policy: Set(body
210210 .join_policy
211 .unwrap_or(crate::models::organization::OrganizationJoinPolicy::InviteOnly)),
211 .unwrap_or(crate::models::space::OrganizationJoinPolicy::InviteOnly)),
212212 allow_join: Set(body.allow_join.unwrap_or(false)),
213213 require_email_verified: Set(body.require_email_verified.unwrap_or(true)),
214214 require_mfa: Set(body.require_mfa.unwrap_or(false)),
@@ -392,7 +392,7 @@
392392 .filter(organizations::Column::Status.eq(OrganizationStatus::Active))
393393 .filter(
394394 organizations::Column::JoinPolicy
395 .ne(crate::models::organization::OrganizationJoinPolicy::Private),
395 .ne(crate::models::space::OrganizationJoinPolicy::Private),
396396 );
397397 let needle = query.trim();
398398 if !needle.is_empty() {
Modified lib/identity/src/auth/organization/types.rs lib/identity/src/auth/organization/types.rs +1 −1 View lib/identity/src/auth/organization/types.rs
@@ -3,7 +3,7 @@
33use utoipa::ToSchema;
44use uuid::Uuid;
55
6use crate::models::organization::{
6use crate::models::space::{
77 MessageNotifications, OrgJoinMethod, OrgMemberRole, OrganizationJoinPolicy,
88 OrganizationJoinRequestStatus, OrganizationStatus, PermissionEffect,
99};
Modified lib/identity/src/auth/password_reset/service.rs lib/identity/src/auth/password_reset/service.rs +3 −3 View lib/identity/src/auth/password_reset/service.rs
@@ -15,9 +15,9 @@
1515use crate::auth::share::crypto::{random_b64url, sha256_hex};
1616use crate::auth::share::password::{hash as hash_password, validate_new_password};
1717use crate::auth::share::{rate, validate};
18use crate::models::user::user_events::UserEventType;
19use crate::models::user::user_verification_tokens::{self, VerificationPurpose};
20use crate::models::user::users;
18use crate::models::users::user_events::UserEventType;
19use crate::models::users::user_verification_tokens::{self, VerificationPurpose};
20use crate::models::users::users;
2121
2222const RESET_CODE_KEY_PREFIX: &str = "auth:code:reset:";
2323const RESET_RATE_KEY_PREFIX: &str = "auth:rate:reset:";
Modified lib/identity/src/auth/register/service.rs lib/identity/src/auth/register/service.rs +2 −2 View lib/identity/src/auth/register/service.rs
@@ -16,8 +16,8 @@
1616use crate::auth::share::password::{hash as hash_password, validate_new_password};
1717use crate::auth::share::validate::{validate_display_name, validate_email, validate_username};
1818use crate::auth::share::{crypto, rate};
19use crate::models::user::user_events::UserEventType;
20use crate::models::user::users;
19use crate::models::users::user_events::UserEventType;
20use crate::models::users::users;
2121
2222const REGISTER_CODE_KEY_PREFIX: &str = "auth:code:register:";
2323const REGISTER_RATE_KEY_PREFIX: &str = "auth:rate:register:";
Modified lib/identity/src/auth/share/audit.rs lib/identity/src/auth/share/audit.rs +1 −1 View lib/identity/src/auth/share/audit.rs
@@ -4,7 +4,7 @@
44use serde_json::Value;
55
66use crate::db::DbConn;
7use crate::models::user::user_events::{self, UserEventType};
7use crate::models::users::user_events::{self, UserEventType};
88
99use crate::auth::errors::AuthResult;
1010
Modified lib/identity/src/auth/share/mfa.rs lib/identity/src/auth/share/mfa.rs +1 −1 View lib/identity/src/auth/share/mfa.rs
@@ -1,4 +1,4 @@
1use crate::models::user::{user_mfa, user_mfa_backup_codes};
1use crate::models::users::{user_mfa, user_mfa_backup_codes};
22use sea_orm::sea_query::{Expr, ExprTrait};
33use sea_orm::{ColumnTrait, Condition, EntityTrait, QueryFilter};
44
Modified lib/identity/src/auth/share/user.rs lib/identity/src/auth/share/user.rs +2 −2 View lib/identity/src/auth/share/user.rs
@@ -3,8 +3,8 @@
33
44use crate::auth::errors::{AuthError, AuthResult};
55use crate::db::DbConn;
6use crate::models::user::user_mfa::MfaStatus as ModelMfaStatus;
7use crate::models::user::{user_mfa, users};
6use crate::models::users::user_mfa::MfaStatus as ModelMfaStatus;
7use crate::models::users::{user_mfa, users};
88use chrono::Utc;
99use sea_orm::{ColumnTrait, EntityTrait, QueryFilter};
1010
Modified lib/identity/src/auth/ssh_key/service.rs lib/identity/src/auth/ssh_key/service.rs +3 −3 View lib/identity/src/auth/ssh_key/service.rs
@@ -1,7 +1,7 @@
11use crate::events::{AuthEvent, Labels, RestOperate, RuntimeEvent};
2use crate::models::user::user_events::{self, UserEventType};
3use crate::models::user::user_ssh_keys::{self, PublicKeyType};
4use crate::models::user::users;
2use crate::models::users::user_events::{self, UserEventType};
3use crate::models::users::user_ssh_keys::{self, PublicKeyType};
4use crate::models::users::users;
55use crate::session::Session;
66use base64::Engine;
77use base64::engine::general_purpose::STANDARD_NO_PAD;
Modified lib/identity/src/auth/ssh_key/types.rs lib/identity/src/auth/ssh_key/types.rs +1 −1 View lib/identity/src/auth/ssh_key/types.rs
@@ -1,4 +1,4 @@
1use crate::models::user::user_ssh_keys::PublicKeyType;
1use crate::models::users::user_ssh_keys::PublicKeyType;
22use chrono::{DateTime, Utc};
33use serde::{Deserialize, Serialize};
44use utoipa::ToSchema;
Modified lib/identity/src/auth/account_lifecycle.rs lib/identity/src/auth/account_lifecycle.rs +2 −2 View lib/identity/src/auth/account_lifecycle.rs
@@ -6,8 +6,8 @@
66
77use crate::auth::AuthSvc;
88use crate::auth::errors::{AuthError, AuthResult};
9use crate::models::organization::{OrganizationStatus, organization_members, organizations};
10use crate::models::user::{
9use crate::models::space::{OrganizationStatus, organization_members, organizations};
10use crate::models::users::{
1111 user_access_tokens, user_mfa, user_mfa_backup_codes, user_sessions, user_ssh_keys, users,
1212};
1313
Modified lib/identity/src/auth/authority.rs lib/identity/src/auth/authority.rs +1 −1 View lib/identity/src/auth/authority.rs
@@ -1,6 +1,6 @@
11use crate::jwt::authority::{AccessRecord, Authority, UserRecord};
22use crate::jwt::{AppClaims, JwtError};
3use crate::models::user::{user_sessions, users};
3use crate::models::users::{user_sessions, users};
44use crate::session::Session;
55use async_trait::async_trait;
66use chrono::{DateTime, Duration, Utc};
Modified lib/identity/src/auth/migrate.rs lib/identity/src/auth/migrate.rs +3 −3 View lib/identity/src/auth/migrate.rs
@@ -1,10 +1,10 @@
11use crate::auth::errors::AuthResult;
22
3const USER_PREFIX: &str = "accounts::user::";
4const ORGANIZATION_PREFIX: &str = "accounts::organization::";
3const USER_PREFIX: &str = "models::users::";
4const SPACE_PREFIX: &str = "models::space::";
55
66pub async fn sync_schema(db: &crate::db::DbConn) -> AuthResult<()> {
7 crate::db::migrate::EntityFirst::new(&[USER_PREFIX, ORGANIZATION_PREFIX])
7 crate::db::migrate::EntityFirst::new(&[USER_PREFIX, SPACE_PREFIX])
88 .after([
99 // 邮箱查询一律走 lower(email),表达式唯一索引把大小写变体挡在数据库层。
1010 "CREATE UNIQUE INDEX IF NOT EXISTS auth_users_lower_email_idx ON auth_users (lower(email))",
Modified lib/identity/src/auth/protocol.rs lib/identity/src/auth/protocol.rs +4 −4 View lib/identity/src/auth/protocol.rs
@@ -64,7 +64,7 @@
6464 if duplicate
6565 || crate::auth::client::registry::validate_client_id(&client.client_id).is_err()
6666 // 仅跑 client_credentials 的机密客户端可以没有重定向地址。
67 || (client.kind == crate::models::user::oauth_clients::ClientKind::Public
67 || (client.kind == crate::models::users::oauth_clients::ClientKind::Public
6868 && client.redirect_uris.is_empty())
6969 || client
7070 .scopes
@@ -77,7 +77,7 @@
7777 crate::auth::client::registry::validate_uri(uri, config.allow_wildcard_redirect)
7878 .is_err()
7979 })
80 || (client.kind == crate::models::user::oauth_clients::ClientKind::Confidential
80 || (client.kind == crate::models::users::oauth_clients::ClientKind::Confidential
8181 && client
8282 .secret
8383 .as_deref()
@@ -970,7 +970,7 @@
970970) -> AuthResult<i32> {
971971 use sea_orm::EntityTrait;
972972 // security_version 判定走主库:副本延迟会让刚吊销的旧版本短暂放行。
973 let row = crate::models::user::user_sessions::Entity::find_by_id(session_id)
973 let row = crate::models::users::user_sessions::Entity::find_by_id(session_id)
974974 .one(svc.db().write())
975975 .await?
976976 .ok_or(AuthError::InvalidCredentials)?;
@@ -1312,7 +1312,7 @@
13121312mod tests {
13131313 use super::*;
13141314 use crate::auth::client::{ClientSpec, MIN_SECRET_LEN};
1315 use crate::models::user::oauth_clients::{ClientKind, ClientSource};
1315 use crate::models::users::oauth_clients::{ClientKind, ClientSource};
13161316
13171317 fn make_record(kind: ClientKind, secret: Option<&str>, scopes: &[&str]) -> ClientRecord {
13181318 ClientRecord::from_spec(
Modified lib/identity/src/oauth/mod.rs lib/identity/src/oauth/mod.rs +0 −11 View lib/identity/src/oauth/mod.rs
@@ -169,10 +169,6 @@
169169 .run();
170170
171171 let event_forwarding = service.verify_event_forwarding().await;
172 let relay = crate::events::outbox::OutboxRelay::start(
173 service.events().cloned(),
174 vec![("oauth", service.db().write().clone())],
175 );
176172 tracing::info!(
177173 app = APP,
178174 config = %config.path().display(),
@@ -190,7 +186,6 @@
190186 http,
191187 grpc,
192188 grpc_listener,
193 relay,
194189 })
195190}
196191
@@ -199,7 +194,6 @@
199194 http: actix_web::dev::Server,
200195 grpc: crate::auth::grpc::GrpcRouter,
201196 grpc_listener: tokio::net::TcpListener,
202 relay: crate::events::outbox::OutboxRelay,
203197}
204198
205199impl Runtime {
@@ -213,7 +207,6 @@
213207 http,
214208 grpc,
215209 grpc_listener,
216 mut relay,
217210 } = self;
218211 let handle = http.handle();
219212 let grpc_cancel = shutdown.clone();
@@ -227,7 +220,6 @@
227220 let result = tokio::select! {
228221 result = &mut http => { http_done = true; result.context("identity HTTP server stopped") },
229222 result = &mut grpc => { grpc_done = true; result.context("identity gRPC server stopped") },
230 result = relay.wait() => result.context("identity outbox relay stopped"),
231223 _ = shutdown.cancelled() => Ok(()),
232224 };
233225 shutdown.cancel();
@@ -259,9 +251,6 @@
259251 && let Err(error) = email.shutdown(EMAIL_SHUTDOWN_GRACE).await
260252 {
261253 tracing::warn!(%error, "draining the email queue failed");
262 }
263 if let Err(error) = relay.shutdown().await {
264 tracing::warn!(%error, "shutting down identity outbox failed");
265254 }
266255 if let Some(events) = service.events()
267256 && let Err(error) = events.shutdown().await
Modified lib/identity/src/lib.rs lib/identity/src/lib.rs +1 −1 View lib/identity/src/lib.rs
@@ -11,7 +11,6 @@
1111pub mod client;
1212pub mod oauth;
1313
14pub use accounts as models;
1514pub use config::identity as config;
1615pub use crypt;
1716pub use db;
@@ -19,6 +18,7 @@
1918pub use envelope;
2019pub use events;
2120pub use kvcache;
21pub use models;
2222pub use oltp::{self, shutdown};
2323pub use proto as protos;
2424pub use queue::identity as queue;
Modified lib/identity/Cargo.toml lib/identity/Cargo.toml +1 −1 View lib/identity/Cargo.toml
@@ -13,7 +13,7 @@
1313path = "src/lib.rs"
1414
1515[dependencies]
16accounts = { workspace = true }
16models = { workspace = true }
1717config = { workspace = true }
1818crypt = { workspace = true }
1919db = { workspace = true }
Modified lib/models/src/space/mod.rs lib/models/src/space/mod.rs +128 −0 View lib/models/src/space/mod.rs
@@ -1,3 +1,18 @@
1use sea_orm::entity::prelude::*;
2use serde::{Deserialize, Serialize};
3use utoipa::ToSchema;
4use utoipa6 as utoipa;
5
6pub mod organization_invite_redemptions;
7pub mod organization_invites;
8pub mod organization_join_requests;
9pub mod organization_join_rules;
10pub mod organization_member_permissions;
11pub mod organization_member_roles;
12pub mod organization_members;
13pub mod organization_role_permissions;
14pub mod organization_roles;
15pub mod organizations;
116pub mod space_bans;
217pub mod space_extras;
318pub mod space_group_permissions;
@@ -13,3 +28,116 @@
1328pub mod spaces;
1429
1530pub use spaces::*;
31
32#[derive(
33 Clone, Copy, Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum, Serialize, Deserialize, ToSchema,
34)]
35#[serde(rename_all = "snake_case")]
36#[sea_orm(
37 rs_type = "String",
38 db_type = "String(StringLen::N(16))",
39 rename_all = "snake_case"
40)]
41pub enum OrganizationStatus {
42 Active,
43 Suspended,
44 Archived,
45}
46
47#[derive(
48 Clone, Copy, Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum, Serialize, Deserialize, ToSchema,
49)]
50#[serde(rename_all = "snake_case")]
51#[sea_orm(
52 rs_type = "String",
53 db_type = "String(StringLen::N(16))",
54 rename_all = "snake_case"
55)]
56pub enum OrgMemberRole {
57 Member,
58 Admin,
59 Owner,
60}
61
62#[derive(
63 Clone, Copy, Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum, Serialize, Deserialize, ToSchema,
64)]
65#[serde(rename_all = "snake_case")]
66#[sea_orm(
67 rs_type = "String",
68 db_type = "String(StringLen::N(16))",
69 rename_all = "snake_case"
70)]
71pub enum OrgJoinMethod {
72 Direct,
73 InviteLink,
74 InviteTargeted,
75 InviteEmail,
76}
77
78#[derive(
79 Clone, Copy, Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum, Serialize, Deserialize, ToSchema,
80)]
81#[serde(rename_all = "snake_case")]
82#[sea_orm(
83 rs_type = "String",
84 db_type = "String(StringLen::N(16))",
85 rename_all = "snake_case"
86)]
87pub enum PermissionEffect {
88 Allow,
89 Deny,
90}
91
92/// 会籍的通知偏好,对应旧 `space_members.message_notifications`。
93/// 旧表用 0/1/2 存,这里用字符串存,便于跨服务读写时直接透传。
94#[derive(
95 Clone, Copy, Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum, Serialize, Deserialize, ToSchema,
96)]
97#[serde(rename_all = "snake_case")]
98#[sea_orm(
99 rs_type = "String",
100 db_type = "String(StringLen::N(16))",
101 rename_all = "snake_case"
102)]
103pub enum MessageNotifications {
104 All,
105 Mentions,
106 Nothing,
107}
108
109#[derive(
110 Clone, Copy, Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum, Serialize, Deserialize, ToSchema,
111)]
112#[serde(rename_all = "snake_case")]
113#[sea_orm(
114 rs_type = "String",
115 db_type = "String(StringLen::N(16))",
116 rename_all = "snake_case"
117)]
118#[derive(Default)]
119pub enum OrganizationJoinPolicy {
120 Open,
121 Approval,
122 #[default]
123 InviteOnly,
124 Private,
125}
126
127#[derive(
128 Clone, Copy, Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum, Serialize, Deserialize, ToSchema,
129)]
130#[serde(rename_all = "snake_case")]
131#[sea_orm(
132 rs_type = "String",
133 db_type = "String(StringLen::N(16))",
134 rename_all = "snake_case"
135)]
136#[derive(Default)]
137pub enum OrganizationJoinRequestStatus {
138 #[default]
139 Pending,
140 Approved,
141 Rejected,
142 Withdrawn,
143}
Renamed lib/accounts/src/organization/organization_invite_redemptions.rs → lib/models/src/space/organization_invite_redemptions.rs lib/models/src/space/organization_invite_redemptions.rs +0 −0 View lib/models/src/space/organization_invite_redemptions.rs
No content changes.
Renamed lib/accounts/src/organization/organization_invites.rs → lib/models/src/space/organization_invites.rs lib/models/src/space/organization_invites.rs +0 −0 View lib/models/src/space/organization_invites.rs
No content changes.
Renamed lib/accounts/src/organization/organization_join_requests.rs → lib/models/src/space/organization_join_requests.rs lib/models/src/space/organization_join_requests.rs +0 −0 View lib/models/src/space/organization_join_requests.rs
No content changes.
Renamed lib/accounts/src/organization/organization_join_rules.rs → lib/models/src/space/organization_join_rules.rs lib/models/src/space/organization_join_rules.rs +0 −0 View lib/models/src/space/organization_join_rules.rs
No content changes.
Renamed lib/accounts/src/organization/organization_member_permissions.rs → lib/models/src/space/organization_member_permissions.rs lib/models/src/space/organization_member_permissions.rs +0 −0 View lib/models/src/space/organization_member_permissions.rs
No content changes.
Renamed lib/accounts/src/organization/organization_member_roles.rs → lib/models/src/space/organization_member_roles.rs lib/models/src/space/organization_member_roles.rs +0 −0 View lib/models/src/space/organization_member_roles.rs
No content changes.
Renamed lib/accounts/src/organization/organization_members.rs → lib/models/src/space/organization_members.rs lib/models/src/space/organization_members.rs +0 −0 View lib/models/src/space/organization_members.rs
No content changes.
Renamed lib/accounts/src/organization/organization_role_permissions.rs → lib/models/src/space/organization_role_permissions.rs lib/models/src/space/organization_role_permissions.rs +0 −0 View lib/models/src/space/organization_role_permissions.rs
No content changes.
Renamed lib/accounts/src/organization/organization_roles.rs → lib/models/src/space/organization_roles.rs lib/models/src/space/organization_roles.rs +0 −0 View lib/models/src/space/organization_roles.rs
No content changes.
Renamed lib/accounts/src/organization/organizations.rs → lib/models/src/space/organizations.rs lib/models/src/space/organizations.rs +0 −0 View lib/models/src/space/organizations.rs
No content changes.
Deleted lib/models/src/system/event_outbox.rs lib/models/src/system/event_outbox.rs +0 −25
@@ -1,25 +0,0 @@
1use sea_orm::JsonValue;
2use sea_orm::entity::prelude::*;
3use time::OffsetDateTime;
4use uuid::Uuid;
5
6#[sea_orm::model]
7#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
8#[sea_orm(table_name = "event_outbox")]
9pub struct Model {
10 #[sea_orm(primary_key, auto_increment = false)]
11 pub id: Uuid,
12 pub subject: String,
13 pub kind: String,
14 pub ordering_key: String,
15 pub envelope: JsonValue,
16 pub occurred_at: OffsetDateTime,
17 pub available_at: OffsetDateTime,
18 #[sea_orm(default_value = 0)]
19 pub attempts: i32,
20 pub lease_id: Option<Uuid>,
21 pub leased_until: Option<OffsetDateTime>,
22 pub published_at: Option<OffsetDateTime>,
23}
24
25impl ActiveModelBehavior for ActiveModel {}
Modified lib/models/src/system/mod.rs lib/models/src/system/mod.rs +0 −1 View lib/models/src/system/mod.rs
@@ -1,5 +1,4 @@
11pub mod account_deletion;
2pub mod event_outbox;
32pub mod space_deletion;
43pub mod system_announcements;
54pub mod system_permissions;
Modified lib/models/src/users/mod.rs lib/models/src/users/mod.rs +15 −0 View lib/models/src/users/mod.rs
@@ -1,10 +1,20 @@
1pub const DATABASE: &str = "user";
2
13pub mod access_scope;
4pub mod oauth_clients;
5pub mod oauth_consents;
6pub mod oauth_refresh_tokens;
7pub mod service_token_revocations;
28pub mod user;
9pub mod user_access_tokens;
310pub mod user_agreement_acceptances;
411pub mod user_blocks;
12pub mod user_events;
513pub mod user_external_identity;
614pub mod user_favorite_memes;
715pub mod user_followers;
16pub mod user_mfa;
17pub mod user_mfa_backup_codes;
818pub mod user_notifications;
919pub mod user_preferences;
1020pub mod user_profiles;
@@ -12,6 +22,11 @@
1222pub mod user_recent_mentions;
1323pub mod user_reports;
1424pub mod user_saved_messages;
25pub mod user_sessions;
26pub mod user_ssh_keys;
1527pub mod user_suspensions;
28pub mod user_verification_tokens;
29pub mod user_webauthn_credentials;
30pub mod users;
1631
1732pub use user::*;
Renamed lib/accounts/src/user/oauth_clients.rs → lib/models/src/users/oauth_clients.rs lib/models/src/users/oauth_clients.rs +1 −0 View lib/models/src/users/oauth_clients.rs
@@ -7,6 +7,7 @@
77
88use serde::{Deserialize, Serialize};
99use utoipa::ToSchema;
10use utoipa6 as utoipa;
1011
1112use sea_orm::entity::Set;
1213use sea_orm::entity::prelude::*;
Renamed lib/accounts/src/user/oauth_consents.rs → lib/models/src/users/oauth_consents.rs lib/models/src/users/oauth_consents.rs +0 −0 View lib/models/src/users/oauth_consents.rs
No content changes.
Renamed lib/accounts/src/user/oauth_refresh_tokens.rs → lib/models/src/users/oauth_refresh_tokens.rs lib/models/src/users/oauth_refresh_tokens.rs +0 −0 View lib/models/src/users/oauth_refresh_tokens.rs
No content changes.
Renamed lib/accounts/src/user/service_token_revocations.rs → lib/models/src/users/service_token_revocations.rs lib/models/src/users/service_token_revocations.rs +0 −0 View lib/models/src/users/service_token_revocations.rs
No content changes.
Renamed lib/accounts/src/user/user_access_tokens.rs → lib/models/src/users/user_access_tokens.rs lib/models/src/users/user_access_tokens.rs +0 −0 View lib/models/src/users/user_access_tokens.rs
No content changes.
Renamed lib/accounts/src/user/user_events.rs → lib/models/src/users/user_events.rs lib/models/src/users/user_events.rs +1 −0 View lib/models/src/users/user_events.rs
@@ -1,4 +1,5 @@
11use utoipa::ToSchema;
2use utoipa6 as utoipa;
23
34use sea_orm::entity::Set;
45use sea_orm::entity::prelude::*;
Renamed lib/accounts/src/user/user_mfa.rs → lib/models/src/users/user_mfa.rs lib/models/src/users/user_mfa.rs +0 −0 View lib/models/src/users/user_mfa.rs
No content changes.
Renamed lib/accounts/src/user/user_mfa_backup_codes.rs → lib/models/src/users/user_mfa_backup_codes.rs lib/models/src/users/user_mfa_backup_codes.rs +0 −0 View lib/models/src/users/user_mfa_backup_codes.rs
No content changes.
Renamed lib/accounts/src/user/user_sessions.rs → lib/models/src/users/user_sessions.rs lib/models/src/users/user_sessions.rs +0 −0 View lib/models/src/users/user_sessions.rs
No content changes.
Renamed lib/accounts/src/user/user_ssh_keys.rs → lib/models/src/users/user_ssh_keys.rs lib/models/src/users/user_ssh_keys.rs +0 −0 View lib/models/src/users/user_ssh_keys.rs
No content changes.
Renamed lib/accounts/src/user/user_verification_tokens.rs → lib/models/src/users/user_verification_tokens.rs lib/models/src/users/user_verification_tokens.rs +0 −0 View lib/models/src/users/user_verification_tokens.rs
No content changes.
Renamed lib/accounts/src/user/user_webauthn_credentials.rs → lib/models/src/users/user_webauthn_credentials.rs lib/models/src/users/user_webauthn_credentials.rs +0 −0 View lib/models/src/users/user_webauthn_credentials.rs
No content changes.
Renamed lib/accounts/src/user/users.rs → lib/models/src/users/users.rs lib/models/src/users/users.rs +0 −0 View lib/models/src/users/users.rs
No content changes.
Renamed lib/accounts/src/event_outbox.rs → lib/models/src/event_outbox.rs lib/models/src/event_outbox.rs +4 −3 View lib/models/src/event_outbox.rs
@@ -1,15 +1,16 @@
1use sea_orm::JsonValue;
12use sea_orm::entity::prelude::*;
23
34#[sea_orm::model]
45#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
5#[sea_orm(table_name = "auth_event_outbox")]
6#[sea_orm(table_name = "event_outbox")]
67pub struct Model {
78 #[sea_orm(primary_key, auto_increment = false)]
89 pub id: Uuid,
910 pub subject: String,
11 pub kind: String,
1012 pub ordering_key: String,
11 #[sea_orm(column_type = "JsonBinary")]
12 pub envelope: Json,
13 pub envelope: JsonValue,
1314 pub occurred_at: DateTimeUtc,
1415 pub published_at: Option<DateTimeUtc>,
1516 #[sea_orm(default_expr = "Expr::current_timestamp()")]
Modified lib/models/src/lib.rs lib/models/src/lib.rs +2 −0 View lib/models/src/lib.rs
@@ -1,10 +1,12 @@
11pub mod chat;
22pub mod common;
33pub mod dir;
4pub mod event_outbox;
45pub mod git;
56pub mod issue;
67pub mod migrate;
78pub mod products;
9mod proto_conv;
810pub mod pull_request;
911pub mod repo;
1012pub mod room;
Renamed lib/accounts/src/proto_conv.rs → lib/models/src/proto_conv.rs lib/models/src/proto_conv.rs +1 −1 View lib/models/src/proto_conv.rs
@@ -3,7 +3,7 @@
33 OrganizationJoinMethod, OrganizationMemberRole, OrganizationStatus, PermissionEffect,
44};
55
6use crate::organization::{
6use crate::space::{
77 OrgJoinMethod, OrgMemberRole, OrganizationStatus as DomainStatus,
88 PermissionEffect as DomainEffect,
99};
Modified lib/models/Cargo.toml lib/models/Cargo.toml +4 −1 View lib/models/Cargo.toml
@@ -4,10 +4,13 @@
44edition.workspace = true
55
66[dependencies]
7sea-orm = { workspace = true, features = ["with-time", "with-uuid", "with-json", "schema-sync", "entity-registry"] }
7sea-orm = { workspace = true, features = ["with-time", "with-chrono", "with-uuid", "with-json", "schema-sync", "entity-registry"] }
8chrono = { version = "0.4.45", features = ["serde"] }
89time = { workspace = true }
910uuid = { workspace = true }
1011serde = { workspace = true, features = ["derive"] }
1112serde_json = { workspace = true }
13proto = { workspace = true }
1214utoipa = { workspace = true }
15utoipa6 = { package = "utoipa", version = "6.0.0", features = ["chrono", "uuid"] }
1316tokio = { workspace = true }
No newline at end of file
Modified lib/queue/src/lib.rs lib/queue/src/lib.rs +0 −1 View lib/queue/src/lib.rs
@@ -5,7 +5,6 @@
55pub mod error;
66pub mod identity;
77pub mod publisher;
8pub mod relay;
98pub mod route;
109pub mod subject;
1110
Deleted lib/queue/src/relay.rs lib/queue/src/relay.rs +0 −191
@@ -1,191 +0,0 @@
1use std::time::Duration;
2
3use sea_orm::{ConnectionTrait, DatabaseConnection, DbBackend, DbErr, Statement};
4use serde_json::Value;
5use tokio_util::sync::CancellationToken;
6use uuid::Uuid;
7
8use crate::envelope::Envelope;
9use crate::error::QueueError;
10use crate::publisher::Publisher;
11use crate::route;
12
13const BATCH_SIZE: i64 = 16;
14const POLL_INTERVAL: Duration = Duration::from_millis(500);
15const PUBLISH_TIMEOUT: Duration = Duration::from_secs(10);
16const MAX_PUBLISH_ATTEMPTS: i32 = 50;
17const PURGE_INTERVAL: Duration = Duration::from_secs(600);
18const PURGED_RETENTION_SECS: i64 = 7 * 24 * 60 * 60;
19
20const CLAIM: &str = "
21WITH pending AS (
22 SELECT candidate.id FROM event_outbox AS candidate
23 WHERE candidate.published_at IS NULL AND candidate.available_at <= CURRENT_TIMESTAMP
24 AND (candidate.leased_until IS NULL OR candidate.leased_until <= CURRENT_TIMESTAMP)
25 AND NOT EXISTS (
26 SELECT 1 FROM event_outbox AS earlier
27 WHERE earlier.ordering_key = candidate.ordering_key AND earlier.published_at IS NULL
28 AND (earlier.occurred_at, earlier.id) < (candidate.occurred_at, candidate.id)
29 )
30 ORDER BY candidate.occurred_at, candidate.id
31 LIMIT $1 FOR UPDATE OF candidate SKIP LOCKED
32)
33UPDATE event_outbox AS events
34SET lease_id = $2, leased_until = CURRENT_TIMESTAMP + INTERVAL '60 seconds',
35 attempts = LEAST(events.attempts, 2147483646) + 1
36FROM pending WHERE events.id = pending.id
37RETURNING events.id, events.kind, events.envelope, events.attempts
38";
39
40struct PendingEvent {
41 id: Uuid,
42 kind: String,
43 envelope: Value,
44 attempts: i32,
45}
46
47pub async fn run(db: DatabaseConnection, publisher: Publisher, cancel: CancellationToken) {
48 let mut purge_at = tokio::time::Instant::now() + PURGE_INTERVAL;
49 loop {
50 if cancel.is_cancelled() {
51 return;
52 }
53 if tokio::time::Instant::now() >= purge_at {
54 purge_at = tokio::time::Instant::now() + PURGE_INTERVAL;
55 purge_published(&db).await;
56 }
57 match run_batch(&db, &publisher).await {
58 Ok(count) if count > 0 => continue,
59 Ok(_) => {}
60 Err(error) => tracing::error!(%error, "event outbox relay failed"),
61 }
62 tokio::select! {
63 _ = cancel.cancelled() => return,
64 _ = tokio::time::sleep(POLL_INTERVAL) => {}
65 }
66 }
67}
68
69async fn run_batch(db: &DatabaseConnection, publisher: &Publisher) -> Result<usize, DbErr> {
70 let lease_id = Uuid::now_v7();
71 let claimed = db
72 .query_all_raw(Statement::from_sql_and_values(
73 DbBackend::Postgres,
74 CLAIM,
75 [BATCH_SIZE.into(), lease_id.into()],
76 ))
77 .await?;
78
79 let mut pending = Vec::with_capacity(claimed.len());
80 for row in claimed {
81 pending.push(PendingEvent {
82 id: row.try_get("", "id")?,
83 kind: row.try_get("", "kind")?,
84 envelope: row.try_get("", "envelope")?,
85 attempts: row.try_get("", "attempts")?,
86 });
87 }
88
89 let count = pending.len();
90 for row in pending {
91 publish(db, publisher, lease_id, row).await?;
92 }
93 Ok(count)
94}
95
96async fn publish(
97 db: &DatabaseConnection,
98 publisher: &Publisher,
99 lease_id: Uuid,
100 row: PendingEvent,
101) -> Result<(), DbErr> {
102 let result = tokio::time::timeout(PUBLISH_TIMEOUT, publish_body(publisher, &row))
103 .await
104 .unwrap_or_else(|_| Err(QueueError::publish("outbox publish timed out")));
105
106 let statement = match result {
107 Ok(()) => Statement::from_sql_and_values(
108 DbBackend::Postgres,
109 "UPDATE event_outbox SET published_at = CURRENT_TIMESTAMP, lease_id = NULL, leased_until = NULL WHERE id = $1 AND lease_id = $2 AND published_at IS NULL",
110 [row.id.into(), lease_id.into()],
111 ),
112 Err(error) => {
113 if row.attempts >= MAX_PUBLISH_ATTEMPTS {
114 tracing::error!(
115 event_id = %row.id,
116 kind = %row.kind,
117 attempt = row.attempts,
118 %error,
119 "outbox event exceeded the retry budget and was discarded"
120 );
121 Statement::from_sql_and_values(
122 DbBackend::Postgres,
123 "UPDATE event_outbox SET published_at = CURRENT_TIMESTAMP, lease_id = NULL, leased_until = NULL WHERE id = $1 AND lease_id = $2 AND published_at IS NULL",
124 [row.id.into(), lease_id.into()],
125 )
126 } else {
127 tracing::warn!(
128 event_id = %row.id,
129 kind = %row.kind,
130 attempt = row.attempts,
131 %error,
132 "outbox publish will retry"
133 );
134 let delay = retry_delay(row.attempts);
135 Statement::from_sql_and_values(
136 DbBackend::Postgres,
137 "UPDATE event_outbox SET available_at = CURRENT_TIMESTAMP + ($3 * INTERVAL '1 second'), lease_id = NULL, leased_until = NULL WHERE id = $1 AND lease_id = $2 AND published_at IS NULL",
138 [row.id.into(), lease_id.into(), delay.into()],
139 )
140 }
141 }
142 };
143
144 let updated = db.execute_raw(statement).await?;
145 if updated.rows_affected() == 0 {
146 tracing::warn!(event_id = %row.id, "outbox lease was reclaimed by another relay");
147 }
148 Ok(())
149}
150
151async fn publish_body(publisher: &Publisher, row: &PendingEvent) -> Result<(), QueueError> {
152 let envelope: Envelope<Value> = serde_json::from_value(row.envelope.clone())?;
153 if envelope.id != row.id || envelope.kind != row.kind {
154 return Err(QueueError::payload("outbox event identity is invalid"));
155 }
156 let route = route::route(&envelope.kind)?;
157 let topic = publisher.topic(&route.area);
158 let body = envelope.summary_body(&route.tag)?;
159 let properties = envelope.properties();
160 publisher
161 .publish(
162 &topic,
163 &route.tag,
164 vec![row.id.to_string()],
165 properties,
166 body,
167 )
168 .await
169}
170
171fn retry_delay(attempts: i32) -> f64 {
172 f64::from(2_u32.pow(attempts.clamp(1, 8) as u32))
173}
174
175async fn purge_published(db: &DatabaseConnection) {
176 match db
177 .execute_unprepared(&format!(
178 "DELETE FROM event_outbox WHERE published_at < CURRENT_TIMESTAMP - INTERVAL '{PURGED_RETENTION_SECS} seconds'"
179 ))
180 .await
181 {
182 Ok(result) if result.rows_affected() > 0 => {
183 tracing::info!(
184 rows = result.rows_affected(),
185 "purged published outbox events"
186 );
187 }
188 Ok(_) => {}
189 Err(error) => tracing::warn!(%error, "outbox purge failed"),
190 }
191}
Modified Cargo.lock Cargo.lock +8 −15 View Cargo.lock
@@ -3,18 +3,6 @@
33version = 4
44
55[[package]]
6name = "accounts"
7version = "1.0.0"
8dependencies = [
9 "chrono",
10 "proto",
11 "sea-orm",
12 "serde",
13 "utoipa 6.0.0",
14 "uuid",
15]
16
17[[package]]
186name = "actix-codec"
197version = "0.5.4"
208source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1466,11 +1454,11 @@
14661454name = "config"
14671455version = "1.0.0"
14681456dependencies = [
1469 "accounts",
14701457 "base64 0.23.1",
14711458 "chrono",
14721459 "dotenvy",
14731460 "figment",
1461 "models",
14741462 "sea-orm",
14751463 "serde",
14761464 "serde_json",
@@ -2373,12 +2361,12 @@
23732361name = "events"
23742362version = "1.0.0"
23752363dependencies = [
2376 "accounts",
23772364 "chrono",
23782365 "config 1.0.0",
23792366 "db",
23802367 "envelope",
23812368 "futures-util",
2369 "models",
23822370 "opentelemetry",
23832371 "queue",
23842372 "sea-orm",
@@ -2397,6 +2385,7 @@
23972385dependencies = [
23982386 "actix-web",
23992387 "bytes",
2388 "chrono",
24002389 "config 1.0.0",
24012390 "http 1.5.0",
24022391 "imaging",
@@ -3498,7 +3487,6 @@
34983487name = "identity"
34993488version = "1.0.0"
35003489dependencies = [
3501 "accounts",
35023490 "actix-web",
35033491 "aes-gcm 0.11.1",
35043492 "anyhow",
@@ -3523,6 +3511,7 @@
35233511 "kvcache",
35243512 "lettre",
35253513 "maud",
3514 "models",
35263515 "oltp",
35273516 "opentelemetry",
35283517 "opentelemetry-appender-tracing",
@@ -3975,6 +3964,7 @@
39753964 "clap",
39763965 "config 1.0.0",
39773966 "deletion",
3967 "events",
39783968 "export",
39793969 "identity",
39803970 "jwt",
@@ -4562,12 +4552,15 @@
45624552name = "models"
45634553version = "1.0.0"
45644554dependencies = [
4555 "chrono",
4556 "proto",
45654557 "sea-orm",
45664558 "serde",
45674559 "serde_json",
45684560 "time 0.3.55",
45694561 "tokio",
45704562 "utoipa 5.5.0",
4563 "utoipa 6.0.0",
45714564 "uuid",
45724565]
45734566
Modified Cargo.toml Cargo.toml +0 −1 View Cargo.toml
@@ -13,7 +13,6 @@
1313
1414
1515[workspace.dependencies]
16accounts = { path = "lib/accounts" }
1716auth = { path = "lib/auth" }
1817cache = { path = "lib/cache" }
1918chat = { path = "lib/chat" }