Skip to content
Clone

clear worksapce

ZhenYi committed
parent 91cbbf1 commit 8d7639e Browse files

136 files changed

+6913 −40

File tree 136
Added .zcode/plans/plan-sess_b7976664-052a-490f-8366-f95f0ea33b36.md View .zcode/plans/plan-sess_b7976664-052a-490f-8366-f95f0ea33b36.md
@@ -0,0 +1,79 @@
1# 计划:lib/session 增加用户标识快速访问 + 多用户切换
2
3## 背景(探索结论)
4
5- `lib/session` 是 `axum_session 0.21`(key-store)的 Redis 桥接层,类型别名 `Session`/`ReadOnlySession` 指向 `axum_session::*<SessionRedisPool>`,目前完全没有用户概念。
6- `axum_session::Session` 已有底层原语:`get::<T: DeserializeOwned>(&self, key) -> Option<T>`、`set(&self, key, value: impl Serialize)`、`remove`、`clear`,均无返回值(错误被内部吸收)。**无公开的 cycle_id**,session id 无法在会话层轮换。
7- 用户主键口径:`lib/models` 的 `users.uid` 是 `Uuid`(v7),models 用 `uuid = { version = "1", features = ["v7", "serde"] }`(未放进 workspace deps)。
8
9## 改动内容
10
11### 1. `lib/session/Cargo.toml`
12
13- 依赖新增 `uuid = { version = "1", features = ["v7", "serde"] }`(与 models 声明一致,让 uid 类型与数据库主键同口径,serde 特性保证 set/get 的 JSON 往返)。
14- dev-dependencies 新增 `axum = { workspace = true }`、`tower = { version = "0.5", features = ["util"] }`、`serde_json = { workspace = true }`(测试需要走 SessionLayer 才能构造 Session——`Session::new` 是 crate 私有)。
15
16### 2. 新文件 `lib/session/src/user.rs`
17
18常量:
19```rust
20pub const UID_KEY: &str = "uid";
21```
22
23两个 trait(本 crate 定义 trait,实现到 axum_session 的具体实例化类型上,孤儿规则合法):
24
25```rust
26/// 会话对用户标识的只读访问;Session 与 ReadOnlySession 均实现。
27pub trait SessionUser {
28 /// 当前会话绑定的用户(快速访问,等价于 session.get::<Uuid>("uid"))
29 fn uid(&self) -> Option<Uuid>;
30 fn is_authenticated(&self) -> bool { self.uid().is_some() }
31}
32
33/// 用户身份写入/切换;仅 Session 实现(ReadOnlySession 无写方法)。
34pub trait SessionUserMut: SessionUser {
35 /// 绑定用户(登录)。不动会话里其余数据。
36 fn set_uid(&self, uid: Uuid);
37
38 /// 切换用户:先 clear() 清空旧身份的全部遗留数据(偏好、草稿等),
39 /// 再写入新 uid,避免用户 A 的数据串场给用户 B。
40 fn switch_uid(&self, uid: Uuid);
41
42 /// 解除绑定(登出):仅移除 uid,保留其余会话数据。
43 fn clear_uid(&self);
44}
45```
46
47实现(一行转发,全部复用 axum_session 原语):
48- `Session` 和 `ReadOnlySession` 的 `SessionUser::uid` → `self.get::<Uuid>(UID_KEY)`。
49- `Session` 的 `set_uid` → `self.set(UID_KEY, uid)`;`clear_uid` → `self.remove(UID_KEY)`;`switch_uid` → `self.clear()` + `self.set(UID_KEY, uid)`。
50
51文档注释用英文(与 crate 现有风格一致),doc 示例标 `ignore`(无法在 doctest 里构造 Session)。
52
53### 3. `lib/session/src/lib.rs`
54
55```rust
56pub mod user;
57pub use user::{SessionUser, SessionUserMut, UID_KEY};
58```
59
60### 4. 测试 `lib/session/tests/user.rs`
61
62- 写一个内存版 `MockPool`(HashMap + async Mutex)实现 `axum_session::DatabasePool`(约 10 个平铺方法),key 用 `Key::try_from(&[7u8; 64])`。
63- 用 `Router::new().route("/", get(handler)).layer(SessionLayer::new(store, config))` + `tower::ServiceExt::oneshot` 走真实的请求/响应流程(`SessionMode::Persistent` 简化会话落库)。
64- 测试用例:
65 1. `set_uid` 后下一次请求 `uid()` 返回相同 Uuid、`is_authenticated()` 为 true;
66 2. `switch_uid` 会清掉旧用户的其他数据(先 set 一个 `"draft"` key,切换后读不到)且 uid 变为新值;
67 3. `clear_uid` 后 `uid()` 为 None,但其余 key(如 `"draft"`)仍在;
68 4. `ReadOnlySession` 的 `uid()` 可读。
69
70### 5. 验证
71
72- `cargo check -p session`、`cargo test -p session`。
73
74## 明确不做 / 已知限制
75
76- 不做 config 项:uid 的 key 名用常量 `"uid"`(与 models 列名一致),不值得配置化。
77- 不引入用户表校验:session 层只存不查,uid 是否存在于 `users` 表由上层(未来的 auth/app crate)负责。
78- 防会话固定:`axum_session 0.21` 没有公开的 id 轮换 API,`switch_uid` 用 `clear()` 兜底防数据串场但不换 session id;若未来要求切换即换 id,需在 app 层用 `destroy()` 销毁后引导客户端重建会话(本层无法单请求内完成)。
79- workspace 成员、config crate、models 均不动。
No newline at end of file
Added lib/auth/src/lib.rs View lib/auth/src/lib.rs

No content changes.

Added lib/auth/Cargo.toml View lib/auth/Cargo.toml
@@ -0,0 +1,6 @@
1[package]
2name = "auth"
3version.workspace = true
4edition.workspace = true
5
6[dependencies]
Added lib/cache/src/error.rs View lib/cache/src/error.rs
@@ -0,0 +1,41 @@
1use std::fmt;
2
3use oxcache::error::OxCacheError;
4
5#[derive(Debug)]
6pub enum CacheError {
7 InvalidConfig(String),
8 Serialization(String),
9 Backend(OxCacheError),
10}
11
12impl fmt::Display for CacheError {
13 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
14 match self {
15 CacheError::InvalidConfig(msg) => write!(f, "invalid cache config: {msg}"),
16 CacheError::Serialization(msg) => write!(f, "cache serialization error: {msg}"),
17 CacheError::Backend(err) => write!(f, "cache backend error: {err}"),
18 }
19 }
20}
21
22impl std::error::Error for CacheError {
23 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
24 match self {
25 CacheError::Backend(err) => Some(err),
26 _ => None,
27 }
28 }
29}
30
31impl From<OxCacheError> for CacheError {
32 fn from(err: OxCacheError) -> Self {
33 CacheError::Backend(err)
34 }
35}
36
37impl From<serde_json::Error> for CacheError {
38 fn from(err: serde_json::Error) -> Self {
39 CacheError::Serialization(err.to_string())
40 }
41}
Added lib/cache/src/lib.rs View lib/cache/src/lib.rs
@@ -0,0 +1,5 @@
1pub mod error;
2pub mod tiered;
3
4pub use error::CacheError;
5pub use tiered::CacheClient;
Added lib/cache/src/tiered.rs View lib/cache/src/tiered.rs
@@ -0,0 +1,129 @@
1use std::sync::Arc;
2use std::time::Duration;
3
4use config::cache::CacheConfig;
5use config::redis::RedisMode;
6use oxcache::backend::{CacheReader, CacheWriter, MokaMemoryBackend, RedisBackend, RedisMode as OxRedisMode};
7use oxcache::cache::{ChainCache, ChainLink};
8use serde::de::DeserializeOwned;
9use serde::Serialize;
10
11use crate::error::CacheError;
12
13pub struct CacheClient {
14 chain: Arc<ChainCache>,
15}
16
17impl CacheClient {
18 pub async fn new(config: &CacheConfig) -> Result<Self, CacheError> {
19 config.validate().map_err(CacheError::InvalidConfig)?;
20
21 let l1 = build_l1(config)?;
22 let l2 = build_l2(config).await?;
23
24 let mut builder = ChainCache::builder()
25 .link(ChainLink::from_backend(l1))
26 .link(ChainLink::from_backend(l2));
27
28 if config.backfill_enabled {
29 builder = builder.enable_backfill();
30 }
31 if let Some(ttl) = config.default_ttl_secs {
32 builder = builder.default_time_to_live(Duration::from_secs(ttl));
33 }
34
35 Ok(Self {
36 chain: Arc::new(builder.build()),
37 })
38 }
39
40 pub fn raw(&self) -> &ChainCache {
41 &self.chain
42 }
43
44 pub async fn get<V>(&self, key: &str) -> Result<Option<V>, CacheError>
45 where
46 V: DeserializeOwned,
47 {
48 let raw = self.chain.get(key).await?;
49 match raw {
50 Some(bytes) => {
51 let value = serde_json::from_slice::<V>(&bytes)?;
52 Ok(Some(value))
53 }
54 None => Ok(None),
55 }
56 }
57
58 pub async fn set<V>(
59 &self,
60 key: &str,
61 value: &V,
62 ttl: Option<Duration>,
63 ) -> Result<(), CacheError>
64 where
65 V: Serialize,
66 {
67 let bytes = serde_json::to_vec(value)?;
68 self.chain.set(key, bytes, ttl).await?;
69 Ok(())
70 }
71
72 pub async fn delete(&self, key: &str) -> Result<(), CacheError> {
73 self.chain.delete(key).await?;
74 Ok(())
75 }
76
77 pub async fn exists(&self, key: &str) -> Result<bool, CacheError> {
78 Ok(self.chain.exists(key).await?)
79 }
80
81 pub async fn get_bytes(&self, key: &str) -> Result<Option<Vec<u8>>, CacheError> {
82 Ok(self.chain.get(key).await?)
83 }
84
85 pub async fn set_bytes(
86 &self,
87 key: &str,
88 value: Vec<u8>,
89 ttl: Option<Duration>,
90 ) -> Result<(), CacheError> {
91 self.chain.set(key, value, ttl).await?;
92 Ok(())
93 }
94}
95
96fn build_l1(config: &CacheConfig) -> Result<MokaMemoryBackend, CacheError> {
97 let mut builder = MokaMemoryBackend::builder().capacity(config.l1_capacity);
98 if let Some(ttl) = config.l1_ttl_secs {
99 builder = builder.ttl(Duration::from_secs(ttl));
100 }
101 if let Some(tti) = config.l1_tti_secs {
102 builder = builder.time_to_idle(Duration::from_secs(tti));
103 }
104 Ok(builder.build())
105}
106
107async fn build_l2(config: &CacheConfig) -> Result<RedisBackend, CacheError> {
108 let url = config.redis.validate().map_err(CacheError::InvalidConfig)?;
109 let mut builder = RedisBackend::builder()
110 .connection_string(url)
111 .mode(to_oxcache_mode(config.redis.mode()))
112 .pool_size(config.redis.pool_size())
113 .connection_timeout(config.redis.connection_timeout());
114
115 if let Some(db) = config.redis.database {
116 builder = builder.database(db);
117 }
118
119 Ok(builder.build().await?)
120}
121
122fn to_oxcache_mode(mode: RedisMode) -> OxRedisMode {
123 match mode {
124 RedisMode::Standalone => OxRedisMode::Standalone,
125 RedisMode::Sentinel => OxRedisMode::Sentinel,
126 RedisMode::Cluster => OxRedisMode::Cluster,
127 RedisMode::ValkeyStandalone => OxRedisMode::ValkeyStandalone,
128 }
129}
Added lib/config/src/database.rs View lib/config/src/database.rs
@@ -0,0 +1,283 @@
1use std::time::Duration;
2
3use sea_orm::ConnectOptions;
4use serde::{Deserialize, Serialize};
5
6#[derive(Debug, Clone, Default, Serialize, Deserialize)]
7#[serde(default)]
8pub struct DatabaseConfig {
9 pub url: Option<String>,
10 pub host: Option<String>,
11 pub port: Option<u16>,
12 pub username: Option<String>,
13 pub password: Option<String>,
14 pub database: Option<String>,
15 pub schema_search_path: Option<String>,
16 pub application_name: Option<String>,
17 pub ssl_mode: Option<SslMode>,
18 pub max_connections: Option<u32>,
19 pub min_connections: Option<u32>,
20 pub connect_timeout_secs: Option<u64>,
21 pub idle_timeout_secs: Option<u64>,
22 pub acquire_timeout_secs: Option<u64>,
23 pub max_lifetime_secs: Option<u64>,
24 pub sqlx_logging: Option<bool>,
25 pub replicas: Vec<DatabaseEndpoint>,
26}
27
28#[derive(Debug, Clone, Default, Serialize, Deserialize)]
29#[serde(default)]
30pub struct DatabaseEndpoint {
31 pub url: Option<String>,
32 pub host: Option<String>,
33 pub port: Option<u16>,
34 pub username: Option<String>,
35 pub password: Option<String>,
36 pub database: Option<String>,
37 pub application_name: Option<String>,
38 pub ssl_mode: Option<SslMode>,
39 pub max_connections: Option<u32>,
40 pub min_connections: Option<u32>,
41 pub connect_timeout_secs: Option<u64>,
42 pub idle_timeout_secs: Option<u64>,
43 pub acquire_timeout_secs: Option<u64>,
44 pub max_lifetime_secs: Option<u64>,
45}
46
47impl DatabaseEndpoint {
48 pub fn url(&self) -> String {
49 if let Some(url) = self.url.as_deref().filter(|s| !s.is_empty()) {
50 return url.to_string();
51 }
52
53 let host = self.host.as_deref().unwrap_or("localhost");
54 let port = self.port.unwrap_or(5432);
55 let user = self.username.as_deref().unwrap_or("postgres");
56 let db = self.database.as_deref().unwrap_or("postgres");
57 let password = match &self.password {
58 Some(p) if !p.is_empty() => format!(":{p}"),
59 _ => String::new(),
60 };
61
62 let mut url = format!("postgres://{user}{password}@{host}:{port}/{db}");
63
64 let mut params: Vec<String> = Vec::new();
65 if let Some(app) = self.application_name.as_deref().filter(|s| !s.is_empty()) {
66 params.push(format!("application_name={}", url_encode(app)));
67 }
68 if let Some(mode) = self.ssl_mode {
69 params.push(format!("sslmode={}", mode.as_query_value()));
70 }
71
72 if !params.is_empty() {
73 url.push('?');
74 url.push_str(&params.join("&"));
75 }
76
77 url
78 }
79
80 pub fn validate(&self) -> Result<(), String> {
81 if self.url.as_deref().is_some_and(|s| !s.is_empty()) {
82 return Ok(());
83 }
84 if self
85 .host
86 .as_deref()
87 .filter(|s| !s.is_empty())
88 .is_none()
89 {
90 return Err("endpoint.host must be set when endpoint.url is not provided".to_string());
91 }
92 if matches!(self.port, Some(0)) {
93 return Err("endpoint.port must be greater than 0".to_string());
94 }
95 Ok(())
96 }
97
98 pub fn apply_to(&self, opts: &mut ConnectOptions) -> Result<(), String> {
99 self.validate()?;
100 if let Some(v) = self.max_connections {
101 opts.max_connections(v);
102 }
103 if let Some(v) = self.min_connections {
104 opts.min_connections(v);
105 }
106 if let Some(v) = self.connect_timeout_secs {
107 opts.connect_timeout(Duration::from_secs(v));
108 }
109 if let Some(v) = self.idle_timeout_secs {
110 opts.idle_timeout(Duration::from_secs(v));
111 }
112 if let Some(v) = self.acquire_timeout_secs {
113 opts.acquire_timeout(Duration::from_secs(v));
114 }
115 if let Some(v) = self.max_lifetime_secs {
116 opts.max_lifetime(Duration::from_secs(v));
117 }
118 Ok(())
119 }
120}
121
122#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
123#[serde(rename_all = "snake_case")]
124pub enum SslMode {
125 #[default]
126 Disable,
127 Prefer,
128 Require,
129 VerifyCa,
130 VerifyFull,
131}
132
133impl SslMode {
134 pub fn as_query_value(&self) -> &'static str {
135 match self {
136 SslMode::Disable => "disable",
137 SslMode::Prefer => "prefer",
138 SslMode::Require => "require",
139 SslMode::VerifyCa => "verify-ca",
140 SslMode::VerifyFull => "verify-full",
141 }
142 }
143}
144
145#[derive(Debug, Clone, Copy, PartialEq, Eq)]
146pub enum DatabaseRole {
147 Primary,
148 Replica,
149}
150
151impl DatabaseConfig {
152 pub fn primary_url(&self) -> String {
153 let endpoint = DatabaseEndpoint {
154 url: self.url.clone(),
155 host: self.host.clone(),
156 port: self.port,
157 username: self.username.clone(),
158 password: self.password.clone(),
159 database: self.database.clone(),
160 application_name: self.application_name.clone(),
161 ssl_mode: self.ssl_mode,
162 max_connections: self.max_connections,
163 min_connections: self.min_connections,
164 connect_timeout_secs: self.connect_timeout_secs,
165 idle_timeout_secs: self.idle_timeout_secs,
166 acquire_timeout_secs: self.acquire_timeout_secs,
167 max_lifetime_secs: self.max_lifetime_secs,
168 };
169 endpoint.url()
170 }
171
172 pub fn replica_url(&self, index: usize) -> Option<String> {
173 self.replicas.get(index).map(|r| r.url())
174 }
175
176 pub fn replica_urls(&self) -> Vec<String> {
177 self.replicas.iter().map(|r| r.url()).collect()
178 }
179
180 pub fn replica_count(&self) -> usize {
181 self.replicas.len()
182 }
183
184 pub fn has_replicas(&self) -> bool {
185 !self.replicas.is_empty()
186 }
187
188 pub fn validate(&self) -> Result<(), String> {
189 if self.url.as_deref().is_some_and(|s| !s.is_empty()) {
190 return Ok(());
191 }
192 if self
193 .host
194 .as_deref()
195 .filter(|s| !s.is_empty())
196 .is_none()
197 {
198 return Err(
199 "database.host must be set when database.url is not provided".to_string(),
200 );
201 }
202 if matches!(self.port, Some(0)) {
203 return Err("database.port must be greater than 0".to_string());
204 }
205 for (i, replica) in self.replicas.iter().enumerate() {
206 replica.validate().map_err(|e| format!("replicas[{i}]: {e}"))?;
207 }
208 Ok(())
209 }
210
211 pub fn primary_connect_options(&self) -> Result<ConnectOptions, String> {
212 self.validate()?;
213 let url = self.primary_url();
214 let mut opts = ConnectOptions::new(url);
215 if let Some(v) = self.schema_search_path.as_deref().filter(|s| !s.is_empty()) {
216 opts.set_schema_search_path(v);
217 }
218 if let Some(v) = self.sqlx_logging {
219 opts.sqlx_logging(v);
220 }
221 let endpoint = DatabaseEndpoint {
222 url: self.url.clone(),
223 host: self.host.clone(),
224 port: self.port,
225 username: self.username.clone(),
226 password: self.password.clone(),
227 database: self.database.clone(),
228 application_name: self.application_name.clone(),
229 ssl_mode: self.ssl_mode,
230 max_connections: self.max_connections,
231 min_connections: self.min_connections,
232 connect_timeout_secs: self.connect_timeout_secs,
233 idle_timeout_secs: self.idle_timeout_secs,
234 acquire_timeout_secs: self.acquire_timeout_secs,
235 max_lifetime_secs: self.max_lifetime_secs,
236 };
237 endpoint.apply_to(&mut opts)?;
238 Ok(opts)
239 }
240
241 pub fn replica_connect_options(&self, index: usize) -> Result<ConnectOptions, String> {
242 let replica = self
243 .replicas
244 .get(index)
245 .ok_or_else(|| format!("replica index {index} out of range"))?;
246 replica.validate()?;
247 let url = replica.url();
248 let mut opts = ConnectOptions::new(url);
249 if let Some(v) = self.schema_search_path.as_deref().filter(|s| !s.is_empty()) {
250 opts.set_schema_search_path(v);
251 }
252 if let Some(v) = self.sqlx_logging {
253 opts.sqlx_logging(v);
254 }
255 replica.apply_to(&mut opts)?;
256 Ok(opts)
257 }
258
259 pub fn connect_options_for(&self, role: DatabaseRole) -> Result<ConnectOptions, String> {
260 match role {
261 DatabaseRole::Primary => self.primary_connect_options(),
262 DatabaseRole::Replica => {
263 if self.replicas.is_empty() {
264 return self.primary_connect_options();
265 }
266 self.replica_connect_options(0)
267 }
268 }
269 }
270}
271
272fn url_encode(value: &str) -> String {
273 let mut out = String::with_capacity(value.len());
274 for b in value.as_bytes() {
275 match b {
276 b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
277 out.push(*b as char);
278 }
279 _ => out.push_str(&format!("%{:02X}", b)),
280 }
281 }
282 out
283}
Modified lib/config/src/lib.rs View lib/config/src/lib.rs
@@ -1,5 +1,21 @@
1use crate::cache::CacheConfig;
2use crate::database::DatabaseConfig;
3use crate::redis::RedisConfig;
4use crate::s3::S3Config;
5use crate::session::SessionConfig;
6
17pub mod s3;
28pub mod redis;
39pub mod cache;
410pub mod database;
5pub mod session;
No newline at end of file
11pub mod session;
12pub mod mailbaby;
13
14#[derive(Clone,Debug)]
15pub struct AppConfig {
16 pub s3: S3Config,
17 pub redis: RedisConfig,
18 pub cache: CacheConfig,
19 pub database: DatabaseConfig,
20 pub session: SessionConfig,
21}
No newline at end of file
Added lib/config/src/mailbaby.rs View lib/config/src/mailbaby.rs
@@ -0,0 +1,212 @@
1use std::time::Duration;
2
3use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
6#[serde(rename_all = "snake_case")]
7pub enum MailbabyChannel {
8 #[default]
9 Rest,
10 Grpc,
11}
12
13impl MailbabyChannel {
14 pub fn parse(value: &str) -> Option<Self> {
15 if value.eq_ignore_ascii_case("rest") {
16 Some(Self::Rest)
17 } else if value.eq_ignore_ascii_case("grpc") {
18 Some(Self::Grpc)
19 } else {
20 None
21 }
22 }
23}
24
25#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
26#[serde(rename_all = "snake_case")]
27pub enum MailbabyAuthScheme {
28 #[default]
29 Header,
30 Bearer,
31 Query,
32}
33
34impl MailbabyAuthScheme {
35 pub fn parse(value: &str) -> Option<Self> {
36 if value.eq_ignore_ascii_case("header") {
37 Some(Self::Header)
38 } else if value.eq_ignore_ascii_case("bearer") {
39 Some(Self::Bearer)
40 } else if value.eq_ignore_ascii_case("query") {
41 Some(Self::Query)
42 } else {
43 None
44 }
45 }
46}
47
48#[derive(Debug, Clone, Default, Serialize, Deserialize)]
49#[serde(default)]
50pub struct MailbabyConfig {
51 pub enabled: Option<bool>,
52 pub channel: Option<MailbabyChannel>,
53 pub url: Option<String>,
54 pub api_key: Option<String>,
55 pub auth_scheme: Option<MailbabyAuthScheme>,
56 pub api_key_header: Option<String>,
57 pub account: Option<String>,
58 pub from_address: Option<String>,
59 pub from_name: Option<String>,
60 pub reply_to: Option<String>,
61 pub request_timeout_secs: Option<u64>,
62}
63
64impl MailbabyConfig {
65 pub fn new(url: impl Into<String>) -> Self {
66 Self {
67 url: Some(url.into()),
68 ..Self::default()
69 }
70 }
71
72 pub fn with_enabled(mut self, enabled: bool) -> Self {
73 self.enabled = Some(enabled);
74 self
75 }
76
77 pub fn with_channel(mut self, channel: MailbabyChannel) -> Self {
78 self.channel = Some(channel);
79 self
80 }
81
82 pub fn with_api_key(mut self, key: impl Into<String>) -> Self {
83 self.api_key = Some(key.into());
84 self
85 }
86
87 pub fn with_auth_scheme(mut self, scheme: MailbabyAuthScheme) -> Self {
88 self.auth_scheme = Some(scheme);
89 self
90 }
91
92 pub fn with_api_key_header(mut self, header: impl Into<String>) -> Self {
93 self.api_key_header = Some(header.into());
94 self
95 }
96
97 pub fn with_account(mut self, account: impl Into<String>) -> Self {
98 self.account = Some(account.into());
99 self
100 }
101
102 pub fn with_from_address(mut self, address: impl Into<String>) -> Self {
103 self.from_address = Some(address.into());
104 self
105 }
106
107 pub fn with_from_name(mut self, name: impl Into<String>) -> Self {
108 self.from_name = Some(name.into());
109 self
110 }
111
112 pub fn with_reply_to(mut self, address: impl Into<String>) -> Self {
113 self.reply_to = Some(address.into());
114 self
115 }
116
117 pub fn with_request_timeout_secs(mut self, secs: u64) -> Self {
118 self.request_timeout_secs = Some(secs);
119 self
120 }
121
122 pub fn enabled(&self) -> bool {
123 self.enabled.unwrap_or(true)
124 }
125
126 pub fn channel(&self) -> MailbabyChannel {
127 self.channel.unwrap_or_default()
128 }
129
130 pub fn url(&self) -> &str {
131 self.url.as_deref().unwrap_or("")
132 }
133
134 pub fn api_key(&self) -> Option<&str> {
135 self.api_key.as_deref().filter(|s| !s.is_empty())
136 }
137
138 pub fn auth_scheme(&self) -> MailbabyAuthScheme {
139 self.auth_scheme.unwrap_or_default()
140 }
141
142 pub fn api_key_header(&self) -> Option<&str> {
143 self.api_key_header.as_deref().filter(|s| !s.is_empty())
144 }
145
146 pub fn account(&self) -> Option<&str> {
147 self.account.as_deref().filter(|s| !s.is_empty())
148 }
149
150 pub fn from_address(&self) -> Option<&str> {
151 self.from_address.as_deref().filter(|s| !s.is_empty())
152 }
153
154 pub fn from_name(&self) -> Option<&str> {
155 self.from_name.as_deref().filter(|s| !s.is_empty())
156 }
157
158 pub fn reply_to(&self) -> Option<&str> {
159 self.reply_to.as_deref().filter(|s| !s.is_empty())
160 }
161
162 pub fn request_timeout(&self) -> Duration {
163 Duration::from_secs(self.request_timeout_secs.unwrap_or(30))
164 }
165
166 pub fn validate(&self) -> Result<(), String> {
167 if !self.enabled() {
168 return Ok(());
169 }
170
171 let url = self
172 .url
173 .as_deref()
174 .filter(|s| !s.is_empty())
175 .ok_or_else(|| "mailbaby.url must be set when enabled".to_string())?;
176 if !(url.starts_with("http://") || url.starts_with("https://")) {
177 return Err(format!(
178 "mailbaby.url must start with http:// or https://, got {url:?}"
179 ));
180 }
181
182 if self.channel() == MailbabyChannel::Grpc
183 && self.auth_scheme() == MailbabyAuthScheme::Query
184 {
185 return Err("mailbaby.auth_scheme = query is not supported over grpc".to_string());
186 }
187
188 if self.api_key.as_deref().is_some_and(|s| s.is_empty()) {
189 return Err("mailbaby.api_key must not be empty".to_string());
190 }
191 if self.api_key_header.as_deref().is_some_and(|s| s.is_empty()) {
192 return Err("mailbaby.api_key_header must not be empty".to_string());
193 }
194
195 for (name, value) in [
196 ("account", self.account.as_deref()),
197 ("from_address", self.from_address.as_deref()),
198 ("from_name", self.from_name.as_deref()),
199 ("reply_to", self.reply_to.as_deref()),
200 ] {
201 if value.is_some_and(|v| v.is_empty()) {
202 return Err(format!("mailbaby.{name} must not be empty"));
203 }
204 }
205
206 if matches!(self.request_timeout_secs, Some(0)) {
207 return Err("mailbaby.request_timeout_secs must be greater than 0".to_string());
208 }
209
210 Ok(())
211 }
212}
No newline at end of file
Added lib/config/src/redis.rs View lib/config/src/redis.rs
@@ -0,0 +1,117 @@
1use std::time::Duration;
2
3use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
6#[serde(rename_all = "snake_case")]
7pub enum RedisMode {
8 #[default]
9 Standalone,
10 Sentinel,
11 Cluster,
12 ValkeyStandalone,
13}
14
15impl RedisMode {
16 pub fn parse(value: &str) -> Option<Self> {
17 if value.eq_ignore_ascii_case("standalone") {
18 Some(Self::Standalone)
19 } else if value.eq_ignore_ascii_case("sentinel") {
20 Some(Self::Sentinel)
21 } else if value.eq_ignore_ascii_case("cluster") {
22 Some(Self::Cluster)
23 } else if value.eq_ignore_ascii_case("valkey_standalone") || value.eq_ignore_ascii_case("valkey") {
24 Some(Self::ValkeyStandalone)
25 } else {
26 None
27 }
28 }
29}
30
31#[derive(Debug, Clone, Default, Serialize, Deserialize)]
32#[serde(default)]
33pub struct RedisConfig {
34 pub url: Option<String>,
35 pub mode: Option<RedisMode>,
36 pub database: Option<u16>,
37 /// Number of multiplexed connections to open. Defaults to 8.
38 ///
39 /// In cluster mode every connection keeps its own links to the cluster
40 /// nodes it touches, so the total socket count scales with
41 /// `pool_size × nodes`; tune accordingly for large clusters.
42 pub pool_size: Option<usize>,
43 pub connection_timeout_secs: Option<u64>,
44 pub username: Option<String>,
45 pub password: Option<String>,
46}
47
48impl RedisConfig {
49 pub fn new(url: impl Into<String>) -> Self {
50 Self {
51 url: Some(url.into()),
52 ..Self::default()
53 }
54 }
55
56 pub fn url(&self) -> &str {
57 self.url.as_deref().unwrap_or("")
58 }
59
60 pub fn mode(&self) -> RedisMode {
61 self.mode.unwrap_or_default()
62 }
63
64 pub fn pool_size(&self) -> usize {
65 self.pool_size.unwrap_or(8)
66 }
67
68 pub fn connection_timeout(&self) -> Duration {
69 Duration::from_secs(self.connection_timeout_secs.unwrap_or(2))
70 }
71
72 pub fn validate(&self) -> Result<&str, String> {
73 if let Some(size) = self.pool_size {
74 if size == 0 {
75 return Err("redis.pool_size must be at least 1".to_string());
76 }
77 if size > 128 {
78 return Err(format!(
79 "redis.pool_size must not exceed 128, got {size}"
80 ));
81 }
82 }
83
84 let url = self
85 .url
86 .as_deref()
87 .filter(|s| !s.is_empty())
88 .ok_or_else(|| "redis.url must be set".to_string())?;
89 match self.mode() {
90 RedisMode::Standalone | RedisMode::ValkeyStandalone => {
91 if !(url.starts_with("redis://") || url.starts_with("rediss://")) {
92 return Err(format!(
93 "redis.url must start with redis:// or rediss://, got {url:?}"
94 ));
95 }
96 }
97 RedisMode::Cluster => {
98 if !(url.starts_with("redis://")
99 || url.starts_with("rediss://")
100 || url.contains(","))
101 {
102 return Err(format!(
103 "redis cluster url must start with redis:// or rediss://, or list multiple hosts comma-separated, got {url:?}"
104 ));
105 }
106 }
107 RedisMode::Sentinel => {
108 if !(url.starts_with("redis://") || url.starts_with("rediss://")) {
109 return Err(format!(
110 "redis sentinel url must start with redis:// or rediss://, got {url:?}"
111 ));
112 }
113 }
114 }
115 Ok(url)
116 }
117}
Added lib/config/src/s3.rs View lib/config/src/s3.rs
@@ -0,0 +1,60 @@
1use std::time::Duration;
2
3use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, Default, Serialize, Deserialize)]
6#[serde(default)]
7pub struct S3Config {
8 pub bucket: Option<String>,
9 pub region: Option<String>,
10 pub endpoint: Option<String>,
11 pub access_key_id: Option<String>,
12 pub secret_access_key: Option<String>,
13 pub path_style: Option<bool>,
14 pub request_timeout_secs: Option<u64>,
15}
16
17impl S3Config {
18 pub fn new(
19 bucket: impl Into<String>,
20 region: impl Into<String>,
21 endpoint: Option<String>,
22 ) -> Self {
23 Self {
24 bucket: Some(bucket.into()),
25 region: Some(region.into()),
26 endpoint,
27 ..Self::default()
28 }
29 }
30
31 pub fn bucket_name(&self) -> &str {
32 self.bucket.as_deref().unwrap_or("")
33 }
34
35 pub fn region(&self) -> &str {
36 self.region.as_deref().unwrap_or("us-east-1")
37 }
38
39 pub fn use_path_style(&self) -> bool {
40 self.path_style.unwrap_or(false)
41 }
42
43 pub fn request_timeout(&self) -> Option<Duration> {
44 self.request_timeout_secs.map(Duration::from_secs)
45 }
46
47 pub fn validate(&self) -> Result<(&str, &str), String> {
48 let bucket = self
49 .bucket
50 .as_deref()
51 .filter(|s| !s.is_empty())
52 .ok_or_else(|| "s3.bucket must be set".to_string())?;
53 let region = self
54 .region
55 .as_deref()
56 .filter(|s| !s.is_empty())
57 .ok_or_else(|| "s3.region must be set".to_string())?;
58 Ok((bucket, region))
59 }
60}
Added lib/config/src/session.rs View lib/config/src/session.rs
@@ -0,0 +1,68 @@
1use serde::{Deserialize, Serialize};
2
3#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
4#[serde(rename_all = "snake_case")]
5pub enum SessionMode {
6 Manual,
7 #[default]
8 OptIn,
9 Persistent,
10}
11
12#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
13#[serde(rename_all = "snake_case")]
14pub enum SameSite {
15 Strict,
16 #[default]
17 Lax,
18 None,
19}
20
21/// Session and session-cookie configuration. Every field is optional; unset
22/// fields keep the `axum_session` defaults.
23///
24/// Production notes:
25/// - Set `cookie_secure` to `true` when serving over TLS; it is also required
26/// for `cookie_same_site = none`.
27/// - `key` and `database_key` must be base64 encodings of at least 64 random
28/// bytes (32 signing + 32 encryption).
29/// - `table_name` namespaces the Redis keys (`{table_name}:{session_id}`); it
30/// must be non-empty and must not contain glob characters (`*?[]\`).
31#[derive(Debug, Clone, Default, Serialize, Deserialize)]
32#[serde(default)]
33pub struct SessionConfig {
34 pub mode: Option<SessionMode>,
35 pub cookie_name: Option<String>,
36 pub cookie_domain: Option<String>,
37 pub cookie_path: Option<String>,
38 pub cookie_http_only: Option<bool>,
39 /// Mark the cookie `Secure`. Must be `true` over TLS and when
40 /// `cookie_same_site` is `none`.
41 pub cookie_secure: Option<bool>,
42 pub cookie_same_site: Option<SameSite>,
43 /// Cookie max-age in seconds; must be positive (a zero max-age makes
44 /// browsers drop the cookie immediately).
45 pub cookie_max_age_secs: Option<i64>,
46 pub cookie_prefix_host: Option<bool>,
47 pub cookie_encode_ip_and_user_agent: Option<bool>,
48 /// Session validity in seconds; must be positive.
49 pub lifespan_secs: Option<i64>,
50 /// Long-term ("remember me") validity in seconds; must be positive and at
51 /// least as large as `lifespan_secs`.
52 pub max_lifespan_secs: Option<i64>,
53 /// How long an untouched session stays cached in memory; `0` keeps it in
54 /// memory indefinitely.
55 pub memory_lifespan_secs: Option<i64>,
56 /// Redis key namespace (`{table_name}:{session_id}`); must be non-empty
57 /// and free of glob characters.
58 pub table_name: Option<String>,
59 /// Cookie signing/encryption key, base64 of at least 64 random bytes.
60 pub key: Option<String>,
61 /// Key encrypting session data at rest in Redis, base64 of at least 64
62 /// random bytes.
63 pub database_key: Option<String>,
64 pub always_save: Option<bool>,
65 pub db_update_interval_secs: Option<i64>,
66 pub purge_database_update_secs: Option<i64>,
67 pub enable_bloom_filter: Option<bool>,
68}
No newline at end of file
Added lib/export/src/database.rs View lib/export/src/database.rs
@@ -0,0 +1,257 @@
1use std::sync::atomic::{AtomicUsize, Ordering};
2
3use config::database::{DatabaseConfig, DatabaseRole as ConfigRole};
4use sea_orm::{Database, DatabaseConnection, DbBackend, DbErr, ExecResult, QueryResult, Statement};
5use sea_orm::prelude::async_trait::async_trait;
6use crate::error::ExportError;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum DatabaseRole {
10 Primary,
11 Replica,
12}
13
14impl From<ConfigRole> for DatabaseRole {
15 fn from(role: ConfigRole) -> Self {
16 match role {
17 ConfigRole::Primary => DatabaseRole::Primary,
18 ConfigRole::Replica => DatabaseRole::Replica,
19 }
20 }
21}
22
23impl From<DatabaseRole> for ConfigRole {
24 fn from(role: DatabaseRole) -> Self {
25 match role {
26 DatabaseRole::Primary => ConfigRole::Primary,
27 DatabaseRole::Replica => ConfigRole::Replica,
28 }
29 }
30}
31
32pub struct AppDatabase {
33 config: DatabaseConfig,
34 primary: DatabaseConnection,
35 replicas: Vec<DatabaseConnection>,
36 round_robin: AtomicUsize,
37}
38
39impl AppDatabase {
40 pub async fn connect(config: DatabaseConfig) -> Result<Self, ExportError> {
41 config.validate().map_err(ExportError::InvalidConfig)?;
42
43 let primary_opts = config
44 .primary_connect_options()
45 .map_err(ExportError::InvalidConfig)?;
46 let primary = Database::connect(primary_opts).await?;
47
48 let mut replicas = Vec::with_capacity(config.replica_count());
49 for i in 0..config.replica_count() {
50 let opts = config
51 .replica_connect_options(i)
52 .map_err(ExportError::InvalidConfig)?;
53 let conn = Database::connect(opts).await?;
54 replicas.push(conn);
55 }
56
57 Ok(Self {
58 config,
59 primary,
60 replicas,
61 round_robin: AtomicUsize::new(0),
62 })
63 }
64
65 pub fn config(&self) -> &DatabaseConfig {
66 &self.config
67 }
68
69 pub fn primary(&self) -> &DatabaseConnection {
70 &self.primary
71 }
72
73 pub fn replicas(&self) -> &[DatabaseConnection] {
74 &self.replicas
75 }
76
77 pub fn replica(&self, index: usize) -> Option<&DatabaseConnection> {
78 self.replicas.get(index)
79 }
80
81 pub fn replica_count(&self) -> usize {
82 self.replicas.len()
83 }
84
85 pub fn has_replicas(&self) -> bool {
86 !self.replicas.is_empty()
87 }
88
89 pub fn write(&self) -> &DatabaseConnection {
90 &self.primary
91 }
92
93 pub fn read(&self) -> &DatabaseConnection {
94 self.pick_replica().unwrap_or(&self.primary)
95 }
96
97 pub fn for_role(&self, role: DatabaseRole) -> &DatabaseConnection {
98 match role {
99 DatabaseRole::Primary => self.write(),
100 DatabaseRole::Replica => self.read(),
101 }
102 }
103
104 pub fn route_for(&self, sql: &str) -> &DatabaseConnection {
105 if is_write_sql(sql) {
106 self.write()
107 } else {
108 self.read()
109 }
110 }
111
112 fn pick_replica(&self) -> Option<&DatabaseConnection> {
113 if self.replicas.is_empty() {
114 return None;
115 }
116 let idx = self.round_robin.fetch_add(1, Ordering::Relaxed) % self.replicas.len();
117 Some(&self.replicas[idx])
118 }
119}
120
121fn strip_sql_lead(sql: &str) -> &str {
122 let mut s = sql.trim_start();
123 loop {
124 if let Some(rest) = s.strip_prefix("--") {
125 let end = rest.find('\n').unwrap_or(rest.len());
126 s = rest[end..].trim_start();
127 continue;
128 }
129 if let Some(rest) = s.strip_prefix("/*") {
130 let end = rest.find("*/").map(|i| i + 2).unwrap_or(rest.len());
131 s = rest[end..].trim_start();
132 continue;
133 }
134 break;
135 }
136 s
137}
138
139fn first_keyword(sql: &str) -> Option<String> {
140 let lead = strip_sql_lead(sql);
141 let head: String = lead
142 .chars()
143 .take_while(|c| c.is_ascii_alphanumeric() || *c == '_')
144 .collect();
145 if head.is_empty() {
146 None
147 } else {
148 Some(head.to_uppercase())
149 }
150}
151
152fn is_write_sql(sql: &str) -> bool {
153 let Some(kw) = first_keyword(sql) else {
154 return true;
155 };
156 if kw == "WITH" {
157 return with_clause_is_write(sql);
158 }
159 matches!(
160 kw.as_str(),
161 "INSERT"
162 | "UPDATE"
163 | "DELETE"
164 | "MERGE"
165 | "UPSERT"
166 | "REPLACE"
167 | "TRUNCATE"
168 | "CREATE"
169 | "DROP"
170 | "ALTER"
171 | "RENAME"
172 | "GRANT"
173 | "REVOKE"
174 | "COMMIT"
175 | "ROLLBACK"
176 | "SAVEPOINT"
177 | "LOCK"
178 | "UNLOCK"
179 | "COPY"
180 | "CALL"
181 | "SET"
182 | "RESET"
183 | "DO"
184 )
185}
186
187fn with_clause_is_write(sql: &str) -> bool {
188 let upper = sql.to_uppercase();
189 contains_top_level(&upper, "INSERT")
190 || contains_top_level(&upper, "UPDATE")
191 || contains_top_level(&upper, "DELETE")
192 || contains_top_level(&upper, "MERGE")
193}
194
195fn contains_top_level(haystack: &str, needle: &str) -> bool {
196 let bytes = haystack.as_bytes();
197 let n = needle.as_bytes();
198 let mut depth: i32 = 0;
199 let mut in_string = false;
200 let mut i = 0;
201 while i < bytes.len() {
202 let c = bytes[i] as char;
203 if in_string {
204 if c == '\'' {
205 if i + 1 < bytes.len() && bytes[i + 1] == b'\'' {
206 i += 2;
207 continue;
208 }
209 in_string = false;
210 }
211 i += 1;
212 continue;
213 }
214 match c {
215 '\'' => in_string = true,
216 '(' => depth += 1,
217 ')' => depth -= 1,
218 _ if depth == 0 && i + n.len() <= bytes.len() && &bytes[i..i + n.len()] == n => {
219 let before_ok = i == 0
220 || !(bytes[i - 1] as char).is_ascii_alphanumeric()
221 && bytes[i - 1] != b'_';
222 let after_ok = i + n.len() == bytes.len()
223 || !(bytes[i + n.len()] as char).is_ascii_alphanumeric()
224 && bytes[i + n.len()] != b'_';
225 if before_ok && after_ok {
226 return true;
227 }
228 }
229 _ => {}
230 }
231 i += 1;
232 }
233 false
234}
235
236#[async_trait]
237impl sea_orm::ConnectionTrait for AppDatabase {
238 fn get_database_backend(&self) -> DbBackend {
239 self.primary.get_database_backend()
240 }
241
242 async fn execute_raw(&self, stmt: Statement) -> Result<ExecResult, DbErr> {
243 self.route_for(&stmt.sql).execute_raw(stmt).await
244 }
245
246 async fn execute_unprepared(&self, sql: &str) -> Result<ExecResult, DbErr> {
247 self.route_for(sql).execute_unprepared(sql).await
248 }
249
250 async fn query_one_raw(&self, stmt: Statement) -> Result<Option<QueryResult>, DbErr> {
251 self.route_for(&stmt.sql).query_one_raw(stmt).await
252 }
253
254 async fn query_all_raw(&self, stmt: Statement) -> Result<Vec<QueryResult>, DbErr> {
255 self.route_for(&stmt.sql).query_all_raw(stmt).await
256 }
257}
No newline at end of file
Added lib/export/src/email.rs View lib/export/src/email.rs
@@ -0,0 +1,208 @@
1use config::mailbaby::{MailbabyAuthScheme, MailbabyChannel, MailbabyConfig};
2use mailbaby::grpc;
3use mailbaby::rest;
4use mailbaby::{Auth, BatchResponse, Email, SendResponse};
5
6use crate::error::ExportError;
7
8#[derive(Clone)]
9pub enum EmailClient {
10 Rest(rest::MailBabyClient),
11 Grpc(grpc::GrpcClient),
12}
13
14#[derive(Clone)]
15pub struct AppEmail {
16 config: MailbabyConfig,
17 client: Option<EmailClient>,
18}
19
20impl AppEmail {
21 pub fn new(config: MailbabyConfig) -> Result<Self, ExportError> {
22 config.validate().map_err(ExportError::InvalidConfig)?;
23 if !config.enabled() {
24 return Ok(Self {
25 config,
26 client: None,
27 });
28 }
29 match config.channel() {
30 MailbabyChannel::Rest => {
31 let client = build_rest(&config)?;
32 Ok(Self {
33 config,
34 client: Some(EmailClient::Rest(client)),
35 })
36 }
37 MailbabyChannel::Grpc => Err(ExportError::InvalidConfig(
38 "grpc channel requires AppEmail::connect".to_string(),
39 )),
40 }
41 }
42
43 pub async fn connect(config: MailbabyConfig) -> Result<Self, ExportError> {
44 config.validate().map_err(ExportError::InvalidConfig)?;
45 if !config.enabled() {
46 return Ok(Self {
47 config,
48 client: None,
49 });
50 }
51 let client = match config.channel() {
52 MailbabyChannel::Rest => EmailClient::Rest(build_rest(&config)?),
53 MailbabyChannel::Grpc => EmailClient::Grpc(build_grpc(&config).await?),
54 };
55 Ok(Self {
56 config,
57 client: Some(client),
58 })
59 }
60
61 pub fn config(&self) -> &MailbabyConfig {
62 &self.config
63 }
64
65 pub fn enabled(&self) -> bool {
66 self.client.is_some()
67 }
68
69 pub fn channel(&self) -> MailbabyChannel {
70 self.config.channel()
71 }
72
73 pub fn client(&self) -> Result<&EmailClient, ExportError> {
74 self.client.as_ref().ok_or(ExportError::Disabled)
75 }
76
77 pub fn apply_defaults(&self, mut email: Email) -> Email {
78 if email.account.is_empty() {
79 email.account = self.config.account().unwrap_or_default().to_string();
80 }
81 if email.from.is_empty() {
82 email.from = self.config.from_address().unwrap_or_default().to_string();
83 }
84 if email.from_name.is_empty() {
85 email.from_name = self.config.from_name().unwrap_or_default().to_string();
86 }
87 if email.reply_to.is_empty() {
88 email.reply_to = self.config.reply_to().unwrap_or_default().to_string();
89 }
90 email
91 }
92
93 pub async fn send(&self, email: &Email) -> Result<SendResponse, ExportError> {
94 Ok(self.client()?.send(email).await?)
95 }
96
97 pub async fn send_queued(&self, email: &Email) -> Result<SendResponse, ExportError> {
98 Ok(self.client()?.send_queued(email).await?)
99 }
100
101 pub async fn send_batch(
102 &self,
103 emails: &[Email],
104 queue: bool,
105 ) -> Result<BatchResponse, ExportError> {
106 Ok(self.client()?.send_batch(emails, queue).await?)
107 }
108
109 pub async fn ping(&self) -> Result<String, ExportError> {
110 self.client()?.ping().await
111 }
112
113 pub async fn live(&self) -> Result<(), ExportError> {
114 self.client()?.live().await
115 }
116
117 pub async fn ready(&self) -> Result<(), ExportError> {
118 self.client()?.ready().await
119 }
120}
121
122impl EmailClient {
123 pub async fn send(&self, email: &Email) -> Result<SendResponse, ExportError> {
124 match self {
125 EmailClient::Rest(client) => Ok(client.send(email).await?),
126 EmailClient::Grpc(client) => Ok(client.send(email).await?),
127 }
128 }
129
130 pub async fn send_queued(&self, email: &Email) -> Result<SendResponse, ExportError> {
131 match self {
132 EmailClient::Rest(client) => Ok(client.send_async(email).await?),
133 EmailClient::Grpc(client) => Ok(client.send_async(email).await?),
134 }
135 }
136
137 pub async fn send_batch(
138 &self,
139 emails: &[Email],
140 queue: bool,
141 ) -> Result<BatchResponse, ExportError> {
142 match self {
143 EmailClient::Rest(client) => Ok(client.send_batch(emails, queue).await?),
144 EmailClient::Grpc(client) => Ok(client.send_batch(emails, queue).await?),
145 }
146 }
147
148 pub async fn ping(&self) -> Result<String, ExportError> {
149 match self {
150 EmailClient::Grpc(client) => Ok(client.ping().await?),
151 EmailClient::Rest(_) => Err(ExportError::Unsupported(
152 "ping is only available over the grpc channel".to_string(),
153 )),
154 }
155 }
156
157 pub async fn live(&self) -> Result<(), ExportError> {
158 match self {
159 EmailClient::Rest(client) => Ok(client.live().await?),
160 EmailClient::Grpc(_) => Err(ExportError::Unsupported(
161 "the live probe is only available over the rest channel".to_string(),
162 )),
163 }
164 }
165
166 pub async fn ready(&self) -> Result<(), ExportError> {
167 match self {
168 EmailClient::Rest(client) => Ok(client.ready().await?),
169 EmailClient::Grpc(_) => Err(ExportError::Unsupported(
170 "the ready probe is only available over the rest channel".to_string(),
171 )),
172 }
173 }
174}
175
176fn build_rest(config: &MailbabyConfig) -> Result<rest::MailBabyClient, ExportError> {
177 let mut builder = rest::MailBabyClient::builder(config.url()).timeout(config.request_timeout());
178 if let Some(auth) = build_auth(config)? {
179 builder = builder.auth(auth);
180 }
181 Ok(builder.build()?)
182}
183
184async fn build_grpc(config: &MailbabyConfig) -> Result<grpc::GrpcClient, ExportError> {
185 let mut builder = grpc::GrpcClient::builder(config.url()).timeout(config.request_timeout());
186 if let Some(auth) = build_auth(config)? {
187 builder = builder.auth(auth);
188 }
189 Ok(builder.build().await?)
190}
191
192fn build_auth(config: &MailbabyConfig) -> Result<Option<Auth>, ExportError> {
193 let Some(key) = config.api_key() else {
194 return Ok(None);
195 };
196 let auth = match config.auth_scheme() {
197 MailbabyAuthScheme::Header => {
198 let auth = Auth::api_key(key);
199 match config.api_key_header() {
200 Some(header) => auth.with_header(header),
201 None => auth,
202 }
203 }
204 MailbabyAuthScheme::Bearer => Auth::api_key(key).bearer(),
205 MailbabyAuthScheme::Query => Auth::api_key(key).query(),
206 };
207 Ok(Some(auth))
208}
No newline at end of file
Added lib/export/src/error.rs View lib/export/src/error.rs
@@ -0,0 +1,66 @@
1use std::fmt;
2
3use sea_orm::DbErr;
4
5#[derive(Debug)]
6pub enum ExportError {
7 Database(DbErr),
8 Redis(redis::RedisError),
9 Storage(storage::StorageError),
10 MailBaby(mailbaby::Error),
11 InvalidConfig(String),
12 Connection(String),
13 Disabled,
14 Unsupported(String),
15}
16
17impl fmt::Display for ExportError {
18 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
19 match self {
20 ExportError::Database(err) => write!(f, "database error: {err}"),
21 ExportError::Redis(err) => write!(f, "redis error: {err}"),
22 ExportError::Storage(err) => write!(f, "storage error: {err}"),
23 ExportError::MailBaby(err) => write!(f, "mailbaby error: {err}"),
24 ExportError::InvalidConfig(msg) => write!(f, "invalid config: {msg}"),
25 ExportError::Connection(msg) => write!(f, "connection error: {msg}"),
26 ExportError::Disabled => write!(f, "email service is disabled"),
27 ExportError::Unsupported(msg) => write!(f, "unsupported email operation: {msg}"),
28 }
29 }
30}
31
32impl std::error::Error for ExportError {
33 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
34 match self {
35 ExportError::Database(err) => Some(err),
36 ExportError::Redis(err) => Some(err),
37 ExportError::Storage(err) => Some(err),
38 ExportError::MailBaby(err) => Some(err),
39 _ => None,
40 }
41 }
42}
43
44impl From<DbErr> for ExportError {
45 fn from(err: DbErr) -> Self {
46 ExportError::Database(err)
47 }
48}
49
50impl From<redis::RedisError> for ExportError {
51 fn from(err: redis::RedisError) -> Self {
52 ExportError::Redis(err)
53 }
54}
55
56impl From<storage::StorageError> for ExportError {
57 fn from(err: storage::StorageError) -> Self {
58 ExportError::Storage(err)
59 }
60}
61
62impl From<mailbaby::Error> for ExportError {
63 fn from(err: mailbaby::Error) -> Self {
64 ExportError::MailBaby(err)
65 }
66}
No newline at end of file
Added lib/export/src/lib.rs View lib/export/src/lib.rs
@@ -0,0 +1,22 @@
1pub mod database;
2pub mod error;
3pub mod redis;
4pub mod storage;
5pub mod email;
6
7use std::sync::Arc;
8use config::AppConfig;
9pub use database::{AppDatabase, DatabaseRole};
10pub use email::{AppEmail, EmailClient};
11pub use error::ExportError;
12pub use redis::{AppRedis, RedisConnection};
13pub use storage::AppStorage;
14
15#[derive(Clone)]
16pub struct AppState {
17 pub database: Arc<AppDatabase>,
18 pub redis: AppRedis,
19 pub storage: Arc<AppStorage>,
20 pub email: AppEmail,
21 pub config: AppConfig,
22}
No newline at end of file
Added lib/export/src/redis.rs View lib/export/src/redis.rs
@@ -0,0 +1,221 @@
1use std::sync::atomic::{AtomicUsize, Ordering};
2use std::sync::Arc;
3
4use config::redis::{RedisConfig, RedisMode};
5use redis::aio::{ConnectionManager, MultiplexedConnection};
6use redis::cluster_async::ClusterConnection;
7use redis::{ConnectionAddr, ConnectionInfo, Value};
8
9use crate::error::ExportError;
10
11#[derive(Clone)]
12pub enum RedisConnection {
13 Standalone(ConnectionManager),
14 Cluster(ClusterConnection),
15}
16
17impl RedisConnection {
18 pub fn is_cluster(&self) -> bool {
19 matches!(self, RedisConnection::Cluster(_))
20 }
21}
22
23#[derive(Clone)]
24pub struct AppRedis {
25 config: RedisConfig,
26 /// One multiplexed connection per pool slot; every slot is an independent
27 /// connection (not a clone of another slot) so requests spread over
28 /// multiple sockets.
29 connections: Vec<RedisConnection>,
30 /// Round-robin cursor shared across clones of this handle.
31 cursor: Arc<AtomicUsize>,
32 /// Connection parameters (auth, tls) of the first configured node,
33 /// reused to open per-node connections for cluster-wide operations.
34 node_info: Option<ConnectionInfo>,
35}
36
37impl AppRedis {
38 pub async fn connect(config: RedisConfig) -> Result<Self, ExportError> {
39 let url = config.validate().map_err(ExportError::InvalidConfig)?;
40 let pool_size = config.pool_size().max(1);
41
42 let (connections, node_info) = match config.mode() {
43 RedisMode::Cluster => {
44 let nodes = url
45 .split(',')
46 .map(str::trim)
47 .filter(|node| !node.is_empty())
48 .map(String::from)
49 .collect::<Vec<_>>();
50
51 let client = redis::cluster::ClusterClient::new(nodes).map_err(ExportError::from)?;
52 let node_info = connection_info(url)?;
53
54 let mut connections = Vec::with_capacity(pool_size);
55 for _ in 0..pool_size {
56 let connection = client
57 .get_async_connection()
58 .await
59 .map_err(ExportError::from)?;
60
61 connections.push(RedisConnection::Cluster(connection));
62 }
63
64 (connections, Some(node_info))
65 }
66 RedisMode::Standalone | RedisMode::Sentinel | RedisMode::ValkeyStandalone => {
67 let mut connections = Vec::with_capacity(pool_size);
68 for _ in 0..pool_size {
69 let client = redis::Client::open(url).map_err(ExportError::from)?;
70 let manager = ConnectionManager::new(client).await.map_err(ExportError::from)?;
71
72 connections.push(RedisConnection::Standalone(manager));
73 }
74
75 let node_info = connection_info(url)?;
76
77 (connections, Some(node_info))
78 }
79 };
80
81 Ok(Self {
82 config,
83 connections,
84 cursor: Arc::new(AtomicUsize::new(0)),
85 node_info,
86 })
87 }
88
89 pub fn config(&self) -> &RedisConfig {
90 &self.config
91 }
92
93 pub fn mode(&self) -> RedisMode {
94 self.config.mode()
95 }
96
97 pub fn is_cluster(&self) -> bool {
98 self.config.mode() == RedisMode::Cluster
99 }
100
101 pub fn connection(&self) -> &RedisConnection {
102 &self.connections[0]
103 }
104
105 pub fn connection_clone(&self) -> RedisConnection {
106 let index = self.cursor.fetch_add(1, Ordering::Relaxed) % self.connections.len();
107
108 self.connections[index].clone()
109 }
110
111 /// Opens a multiplexed connection to every master node of the cluster.
112 ///
113 /// Masters are discovered at call time via `CLUSTER SLOTS`, so this keeps
114 /// working after topology changes. Returns an empty list for standalone
115 /// connections.
116 pub async fn cluster_master_connections(
117 &self,
118 ) -> Result<Vec<MultiplexedConnection>, ExportError> {
119 let mut conn = self.connection_clone();
120 let RedisConnection::Cluster(cluster) = &mut conn else {
121 return Ok(Vec::new());
122 };
123
124 let slots: Value = redis::cmd("CLUSTER")
125 .arg("SLOTS")
126 .query_async(cluster)
127 .await
128 .map_err(ExportError::from)?;
129
130 let node_info = self.node_info.clone().ok_or_else(|| {
131 ExportError::InvalidConfig("cluster connection info is unavailable".into())
132 })?;
133
134 let mut seen = std::collections::HashSet::new();
135 let mut connections = Vec::new();
136
137 for (host, port) in cluster_master_addrs(&slots) {
138 if !seen.insert((host.clone(), port)) {
139 continue;
140 }
141
142 let addr = match node_info.addr() {
143 ConnectionAddr::Tcp(_, _) => ConnectionAddr::Tcp(host.clone(), port),
144 ConnectionAddr::TcpTls { insecure, tls_params, .. } => ConnectionAddr::TcpTls {
145 host: host.clone(),
146 port,
147 insecure: *insecure,
148 tls_params: tls_params.clone(),
149 },
150 ConnectionAddr::Unix(_) => {
151 return Err(ExportError::InvalidConfig(
152 "unix sockets are not supported for cluster connections".into(),
153 ));
154 }
155 _ => {
156 return Err(ExportError::InvalidConfig(
157 "unsupported connection address type for cluster connections".into(),
158 ));
159 }
160 };
161
162 let mut info = node_info.clone();
163 info = info.set_addr(addr);
164
165 let client = redis::Client::open(info).map_err(ExportError::from)?;
166 let connection = client
167 .get_multiplexed_async_connection()
168 .await
169 .map_err(ExportError::from)?;
170
171 connections.push(connection);
172 }
173
174 Ok(connections)
175 }
176}
177
178/// Extracts the host/port of every master from a `CLUSTER SLOTS` response.
179///
180/// Each slot group is `[start, end, [ip, port, node_id, ...], replicas...]`,
181/// where the first entry of the node list is the master.
182fn cluster_master_addrs(slots: &Value) -> Vec<(String, u16)> {
183 let Value::Array(groups) = slots else {
184 return Vec::new();
185 };
186
187 let mut masters = Vec::new();
188 for group in groups {
189 let Value::Array(fields) = group else {
190 continue;
191 };
192
193 let Some(Value::Array(nodes)) = fields.get(2) else {
194 continue;
195 };
196
197 let (Some(Value::BulkString(host)), Some(Value::Int(port))) =
198 (nodes.first(), nodes.get(1))
199 else {
200 continue;
201 };
202
203 masters.push((String::from_utf8_lossy(host).into_owned(), *port as u16));
204 }
205
206 masters
207}
208
209/// Builds the connection info of the first configured node, used as a template
210/// (scheme, auth, tls settings) for connections to other cluster nodes.
211fn connection_info(urls: &str) -> Result<ConnectionInfo, ExportError> {
212 let first = urls
213 .split(',')
214 .map(str::trim)
215 .find(|node| !node.is_empty())
216 .ok_or_else(|| ExportError::InvalidConfig("redis url contains no nodes".into()))?;
217
218 Ok(redis::Client::open(first)?
219 .get_connection_info()
220 .clone())
221}
No newline at end of file
Added lib/export/src/storage.rs View lib/export/src/storage.rs
@@ -0,0 +1,24 @@
1use config::s3::S3Config;
2use storage::S3Client;
3
4use crate::error::ExportError;
5
6pub struct AppStorage {
7 config: S3Config,
8 client: S3Client,
9}
10
11impl AppStorage {
12 pub fn connect(config: S3Config) -> Result<Self, ExportError> {
13 let client = S3Client::new(&config).map_err(ExportError::from)?;
14 Ok(Self { config, client })
15 }
16
17 pub fn config(&self) -> &S3Config {
18 &self.config
19 }
20
21 pub fn client(&self) -> &S3Client {
22 &self.client
23 }
24}
No newline at end of file
Modified lib/export/Cargo.toml View lib/export/Cargo.toml
@@ -8,4 +8,5 @@
88storage = { path = "../storage" }
99sea-orm = { workspace = true }
1010redis = { workspace = true }
11tokio = { workspace = true }
No newline at end of file
11tokio = { workspace = true }
12mailbaby = { git = "https://github.com/mailbabys/mailbaby-rs", rev = "e408acf48a19e4fd554b020a865d144706f7c2de", features = ["rest", "grpc"] }
Added lib/models/src/common/mod.rs View lib/models/src/common/mod.rs
@@ -0,0 +1 @@
1pub mod watch_mode;
Added lib/models/src/common/watch_mode.rs View lib/models/src/common/watch_mode.rs
@@ -0,0 +1,18 @@
1use sea_orm::entity::prelude::*;
2use serde::{Deserialize, Serialize};
3
4#[derive(
5 Debug, Clone, Copy, PartialEq, Eq, Default, EnumIter, DeriveActiveEnum, Serialize, Deserialize,
6)]
7#[sea_orm(rs_type = "i16", db_type = "SmallInteger")]
8pub enum WatchMode {
9 #[default]
10 #[sea_orm(num_value = 0)]
11 Normal,
12 #[sea_orm(num_value = 1)]
13 NoWatch,
14 #[sea_orm(num_value = 2)]
15 Watch,
16 #[sea_orm(num_value = 3)]
17 Ignore,
18}
Added lib/models/src/dir/dir_overwrite_group.rs View lib/models/src/dir/dir_overwrite_group.rs
@@ -0,0 +1,22 @@
1use sea_orm::entity::prelude::*;
2use uuid::Uuid;
3
4#[sea_orm::model]
5#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
6#[sea_orm(table_name = "dir_overwrite_group")]
7pub struct Model {
8 #[sea_orm(primary_key, auto_increment = false)]
9 pub dir_id: Uuid,
10 #[sea_orm(primary_key, auto_increment = false)]
11 pub group_id: Uuid,
12 #[sea_orm(default_value = 0)]
13 pub allow_mask: i64,
14 #[sea_orm(default_value = 0)]
15 pub deny_mask: i64,
16 #[sea_orm(belongs_to, from = "dir_id", to = "id")]
17 pub dir: BelongsTo<super::dirs::Entity>,
18 #[sea_orm(belongs_to, from = "group_id", to = "id")]
19 pub group: BelongsTo<super::super::space::space_groups::Entity>,
20}
21
22impl ActiveModelBehavior for ActiveModel {}
Added lib/models/src/dir/dir_overwrite_users.rs View lib/models/src/dir/dir_overwrite_users.rs
@@ -0,0 +1,22 @@
1use sea_orm::entity::prelude::*;
2use uuid::Uuid;
3
4#[sea_orm::model]
5#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
6#[sea_orm(table_name = "dir_overwrite_user")]
7pub struct Model {
8 #[sea_orm(primary_key, auto_increment = false)]
9 pub dir_id: Uuid,
10 #[sea_orm(primary_key, auto_increment = false)]
11 pub user_id: Uuid,
12 #[sea_orm(default_value = 0)]
13 pub allow_mask: i64,
14 #[sea_orm(default_value = 0)]
15 pub deny_mask: i64,
16 #[sea_orm(belongs_to, from = "dir_id", to = "id")]
17 pub dir: BelongsTo<super::dirs::Entity>,
18 #[sea_orm(belongs_to, from = "user_id", to = "uid")]
19 pub user: BelongsTo<super::super::users::user::Entity>,
20}
21
22impl ActiveModelBehavior for ActiveModel {}
Added lib/models/src/dir/dirs.rs View lib/models/src/dir/dirs.rs
@@ -0,0 +1,71 @@
1use sea_orm::entity::prelude::*;
2use serde::{Deserialize, Serialize};
3use time::OffsetDateTime;
4use uuid::Uuid;
5
6#[derive(
7 Debug, Clone, Copy, PartialEq, Eq, Default, EnumIter, DeriveActiveEnum, Serialize, Deserialize,
8)]
9#[sea_orm(rs_type = "i16", db_type = "SmallInteger")]
10pub enum DirKind {
11 #[default]
12 #[sea_orm(num_value = 0)]
13 Dir,
14 #[sea_orm(num_value = 1)]
15 Object,
16}
17
18#[sea_orm::model]
19#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
20#[sea_orm(table_name = "dir")]
21pub struct Model {
22 #[sea_orm(primary_key, auto_increment = false)]
23 pub id: Uuid,
24 #[sea_orm(indexed)]
25 pub space_id: Option<Uuid>,
26 pub name: String,
27 #[sea_orm(default_value = 0)]
28 pub kind: DirKind,
29 #[sea_orm(indexed)]
30 pub parent_dir_id: Option<Uuid>,
31 pub color: Option<String>,
32 pub position: i64,
33 pub icon: Option<String>,
34 pub topic: Option<String>,
35 pub purpose: Option<String>,
36 #[sea_orm(default_value = "false")]
37 pub is_archived: bool,
38 pub archived_at: Option<OffsetDateTime>,
39 pub archived_by: Option<Uuid>,
40 #[sea_orm(indexed)]
41 pub created_at: OffsetDateTime,
42 pub updated_at: OffsetDateTime,
43 #[sea_orm(indexed)]
44 pub created_by_uid: Uuid,
45 #[sea_orm(belongs_to, from = "space_id", to = "id")]
46 pub space: BelongsTo<Option<super::super::space::spaces::Entity>>,
47 #[sea_orm(
48 self_ref,
49 relation_enum = "Parent",
50 relation_reverse = "Children",
51 from = "parent_dir_id",
52 to = "id"
53 )]
54 pub parent: HasOne<Entity>,
55 #[sea_orm(
56 belongs_to,
57 relation_enum = "Creator",
58 from = "created_by_uid",
59 to = "uid"
60 )]
61 pub creator: BelongsTo<super::super::users::user::Entity>,
62 #[sea_orm(
63 belongs_to,
64 relation_enum = "Archiver",
65 from = "archived_by",
66 to = "uid"
67 )]
68 pub archiver: BelongsTo<Option<super::super::users::user::Entity>>,
69}
70
71impl ActiveModelBehavior for ActiveModel {}
Added lib/models/src/dir/mod.rs View lib/models/src/dir/mod.rs
@@ -0,0 +1,5 @@
1pub mod dir_overwrite_group;
2pub mod dir_overwrite_users;
3pub mod dirs;
4
5pub use dirs::*;
Added lib/models/src/git/git_branches.rs View lib/models/src/git/git_branches.rs
@@ -0,0 +1,26 @@
1use sea_orm::entity::prelude::*;
2use time::OffsetDateTime;
3use uuid::Uuid;
4
5#[sea_orm::model]
6#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
7#[sea_orm(table_name = "git_branch")]
8pub struct Model {
9 #[sea_orm(primary_key, auto_increment = false)]
10 pub id: Uuid,
11 #[sea_orm(unique_key = "repo_name")]
12 pub repo_id: Uuid,
13 #[sea_orm(unique_key = "repo_name")]
14 pub name: String,
15 pub commit_sha: String,
16 pub is_deleted: bool,
17 // 由 repo_branch_protection_policy 是否有匹配策略派生,写策略时必须同步该缓存标志
18 pub is_protected: bool,
19 pub deleted_at: Option<OffsetDateTime>,
20 pub created_at: OffsetDateTime,
21 pub updated_at: OffsetDateTime,
22 #[sea_orm(belongs_to, from = "repo_id", to = "id")]
23 pub repo: BelongsTo<super::super::repo::repos::Entity>,
24}
25
26impl ActiveModelBehavior for ActiveModel {}
Added lib/models/src/git/git_commit_status.rs View lib/models/src/git/git_commit_status.rs
@@ -0,0 +1,46 @@
1use sea_orm::entity::prelude::*;
2use serde::{Deserialize, Serialize};
3use time::OffsetDateTime;
4use uuid::Uuid;
5
6#[derive(
7 Debug, Clone, Copy, PartialEq, Eq, Default, EnumIter, DeriveActiveEnum, Serialize, Deserialize,
8)]
9#[sea_orm(rs_type = "i16", db_type = "SmallInteger")]
10pub enum CommitStatusState {
11 #[default]
12 #[sea_orm(num_value = 0)]
13 Pending,
14 #[sea_orm(num_value = 1)]
15 Success,
16 #[sea_orm(num_value = 2)]
17 Failure,
18 #[sea_orm(num_value = 3)]
19 Error,
20}
21
22#[sea_orm::model]
23#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
24#[sea_orm(table_name = "git_commit_status")]
25pub struct Model {
26 #[sea_orm(primary_key, auto_increment = false)]
27 pub id: Uuid,
28 #[sea_orm(unique_key = "commit")]
29 pub repo_id: Uuid,
30 #[sea_orm(unique_key = "commit")]
31 pub sha: String,
32 pub state: CommitStatusState,
33 #[sea_orm(unique_key = "commit")]
34 pub context: String,
35 pub description: Option<String>,
36 pub target_url: Option<String>,
37 pub creator_id: Uuid,
38 pub created_at: OffsetDateTime,
39 pub updated_at: Option<OffsetDateTime>,
40 #[sea_orm(belongs_to, from = "(repo_id, sha)", to = "(repo_id, sha)")]
41 pub commit: BelongsTo<super::git_commits::Entity>,
42 #[sea_orm(belongs_to, from = "creator_id", to = "uid")]
43 pub creator: BelongsTo<super::super::users::user::Entity>,
44}
45
46impl ActiveModelBehavior for ActiveModel {}
Added lib/models/src/git/git_commits.rs View lib/models/src/git/git_commits.rs
@@ -0,0 +1,28 @@
1use sea_orm::entity::prelude::*;
2use time::OffsetDateTime;
3use uuid::Uuid;
4
5#[sea_orm::model]
6#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
7#[sea_orm(table_name = "git_commit")]
8pub struct Model {
9 #[sea_orm(primary_key, auto_increment = false)]
10 pub repo_id: Uuid,
11 #[sea_orm(primary_key, auto_increment = false)]
12 pub sha: String,
13 pub message: String,
14 pub author_id: Option<Uuid>,
15 pub author_name: String,
16 pub author_email: String,
17 pub authored_at: OffsetDateTime,
18 pub committer_name: String,
19 pub committer_email: String,
20 pub committed_at: OffsetDateTime,
21 pub created_at: OffsetDateTime,
22 #[sea_orm(belongs_to, from = "repo_id", to = "id")]
23 pub repo: BelongsTo<super::super::repo::repos::Entity>,
24 #[sea_orm(belongs_to, from = "author_id", to = "uid")]
25 pub author: BelongsTo<Option<super::super::users::user::Entity>>,
26}
27
28impl ActiveModelBehavior for ActiveModel {}
Added lib/models/src/git/git_languages.rs View lib/models/src/git/git_languages.rs
@@ -0,0 +1,23 @@
1use sea_orm::entity::prelude::*;
2use time::OffsetDateTime;
3use uuid::Uuid;
4
5#[sea_orm::model]
6#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
7#[sea_orm(table_name = "git_language")]
8pub struct Model {
9 #[sea_orm(primary_key, auto_increment = false)]
10 pub id: Uuid,
11 #[sea_orm(unique_key = "repo_language")]
12 pub repo_id: Uuid,
13 #[sea_orm(unique_key = "repo_language")]
14 pub language: String,
15 // 百分比以基点(basis points,0-10000)存储,避免浮点精度问题
16 pub percentage_bp: i32,
17 pub size: i64,
18 pub created_at: OffsetDateTime,
19 #[sea_orm(belongs_to, from = "repo_id", to = "id")]
20 pub repo: BelongsTo<super::super::repo::repos::Entity>,
21}
22
23impl ActiveModelBehavior for ActiveModel {}
Added lib/models/src/git/git_lfs.rs View lib/models/src/git/git_lfs.rs
@@ -0,0 +1,23 @@
1use sea_orm::entity::prelude::*;
2use time::OffsetDateTime;
3use uuid::Uuid;
4
5#[sea_orm::model]
6#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
7#[sea_orm(table_name = "git_lfs")]
8pub struct Model {
9 #[sea_orm(primary_key, auto_increment = false)]
10 pub id: Uuid,
11 #[sea_orm(unique_key = "repo_oid")]
12 pub repo_id: Uuid,
13 // 唯一约束按(仓库, oid)建立:同一对象可被多个仓库引用
14 #[sea_orm(unique_key = "repo_oid")]
15 pub oid: String,
16 pub size: i64,
17 pub content_type: Option<String>,
18 pub created_at: OffsetDateTime,
19 #[sea_orm(belongs_to, from = "repo_id", to = "id")]
20 pub repo: BelongsTo<super::super::repo::repos::Entity>,
21}
22
23impl ActiveModelBehavior for ActiveModel {}
Added lib/models/src/git/git_lfs_lock.rs View lib/models/src/git/git_lfs_lock.rs
@@ -0,0 +1,26 @@
1use sea_orm::entity::prelude::*;
2use time::OffsetDateTime;
3use uuid::Uuid;
4
5#[sea_orm::model]
6#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
7#[sea_orm(table_name = "git_lfs_lock")]
8pub struct Model {
9 #[sea_orm(primary_key, auto_increment = false)]
10 pub id: Uuid,
11 #[sea_orm(unique_key = "repo_path")]
12 pub repo_id: Uuid,
13 #[sea_orm(unique_key = "repo_path")]
14 pub path: String,
15 pub owner_id: Uuid,
16 pub oid: Option<String>,
17 pub created_at: OffsetDateTime,
18 #[sea_orm(belongs_to, from = "repo_id", to = "id")]
19 pub repo: BelongsTo<super::super::repo::repos::Entity>,
20 #[sea_orm(belongs_to, from = "(repo_id, oid)", to = "(repo_id, oid)")]
21 pub lfs: BelongsTo<Option<super::git_lfs::Entity>>,
22 #[sea_orm(belongs_to, from = "owner_id", to = "uid")]
23 pub owner: BelongsTo<super::super::users::user::Entity>,
24}
25
26impl ActiveModelBehavior for ActiveModel {}
Added lib/models/src/git/git_tags.rs View lib/models/src/git/git_tags.rs
@@ -0,0 +1,38 @@
1use sea_orm::entity::prelude::*;
2use serde::{Deserialize, Serialize};
3use time::OffsetDateTime;
4use uuid::Uuid;
5
6#[derive(
7 Debug, Clone, Copy, PartialEq, Eq, Default, EnumIter, DeriveActiveEnum, Serialize, Deserialize,
8)]
9#[sea_orm(rs_type = "i16", db_type = "SmallInteger")]
10pub enum TagType {
11 #[default]
12 #[sea_orm(num_value = 0)]
13 Lightweight,
14 #[sea_orm(num_value = 1)]
15 Annotated,
16}
17
18#[sea_orm::model]
19#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
20#[sea_orm(table_name = "git_tag")]
21pub struct Model {
22 #[sea_orm(primary_key, auto_increment = false)]
23 pub id: Uuid,
24 #[sea_orm(unique_key = "repo_name")]
25 pub repo_id: Uuid,
26 #[sea_orm(unique_key = "repo_name")]
27 pub name: String,
28 pub tag_type: TagType,
29 pub commit_sha: String,
30 pub message: Option<String>,
31 pub tagger_name: Option<String>,
32 pub tagger_email: Option<String>,
33 pub created_at: OffsetDateTime,
34 #[sea_orm(belongs_to, from = "repo_id", to = "id")]
35 pub repo: BelongsTo<super::super::repo::repos::Entity>,
36}
37
38impl ActiveModelBehavior for ActiveModel {}
Added lib/models/src/git/mod.rs View lib/models/src/git/mod.rs
@@ -0,0 +1,7 @@
1pub mod git_branches;
2pub mod git_commit_status;
3pub mod git_commits;
4pub mod git_languages;
5pub mod git_lfs;
6pub mod git_lfs_lock;
7pub mod git_tags;
Added lib/models/src/issue/comment.rs View lib/models/src/issue/comment.rs
@@ -0,0 +1,27 @@
1use sea_orm::entity::prelude::*;
2use time::OffsetDateTime;
3use uuid::Uuid;
4
5#[sea_orm::model]
6#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
7#[sea_orm(table_name = "comment")]
8pub struct Model {
9 #[sea_orm(primary_key, auto_increment = false)]
10 pub id: Uuid,
11 pub issue_id: Option<Uuid>,
12 // project 模块暂时移除后保留列,恢复时加回 belongs_to(同 issue_event_line/issue_pin/issue_reaction/issue_watch)
13 pub project_id: Option<Uuid>,
14 pub parent_id: Option<Uuid>,
15 pub poster_id: Uuid,
16 pub content: String,
17 pub created_at: OffsetDateTime,
18 pub updated_at: Option<OffsetDateTime>,
19 #[sea_orm(belongs_to, from = "issue_id", to = "id")]
20 pub issue: BelongsTo<Option<super::issue_item::Entity>>,
21 #[sea_orm(belongs_to, from = "parent_id", to = "id")]
22 pub parent: BelongsTo<Option<super::comment::Entity>>,
23 #[sea_orm(belongs_to, from = "poster_id", to = "uid")]
24 pub poster: BelongsTo<super::super::users::user::Entity>,
25}
26
27impl ActiveModelBehavior for ActiveModel {}
Added lib/models/src/issue/issue_content_history.rs View lib/models/src/issue/issue_content_history.rs
@@ -0,0 +1,26 @@
1use sea_orm::entity::prelude::*;
2use time::OffsetDateTime;
3use uuid::Uuid;
4
5#[sea_orm::model]
6#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
7#[sea_orm(table_name = "issue_content_history")]
8pub struct Model {
9 #[sea_orm(primary_key, auto_increment = false)]
10 pub id: Uuid,
11 pub issue_id: Uuid,
12 pub poster_id: Uuid,
13 pub comment_id: Option<Uuid>,
14 pub content_before: Option<String>,
15 pub content_after: Option<String>,
16 pub edited_at: OffsetDateTime,
17 pub created_at: OffsetDateTime,
18 #[sea_orm(belongs_to, from = "issue_id", to = "id")]
19 pub issue: BelongsTo<super::issue_item::Entity>,
20 #[sea_orm(belongs_to, from = "comment_id", to = "id")]
21 pub comment: BelongsTo<Option<super::comment::Entity>>,
22 #[sea_orm(belongs_to, from = "poster_id", to = "uid")]
23 pub poster: BelongsTo<super::super::users::user::Entity>,
24}
25
26impl ActiveModelBehavior for ActiveModel {}
Added lib/models/src/issue/issue_dependency.rs View lib/models/src/issue/issue_dependency.rs
@@ -0,0 +1,27 @@
1use sea_orm::entity::prelude::*;
2use time::OffsetDateTime;
3use uuid::Uuid;
4
5#[sea_orm::model]
6#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
7#[sea_orm(table_name = "issue_dependency")]
8pub struct Model {
9 #[sea_orm(primary_key, auto_increment = false)]
10 pub id: Uuid,
11 #[sea_orm(unique_key = "issue_dependency")]
12 pub issue_id: Uuid,
13 #[sea_orm(unique_key = "issue_dependency")]
14 pub dependency_id: Uuid,
15 pub created_at: OffsetDateTime,
16 #[sea_orm(belongs_to, from = "issue_id", to = "id")]
17 pub issue: BelongsTo<super::issue_item::Entity>,
18 #[sea_orm(
19 belongs_to,
20 relation_enum = "Dependency",
21 from = "dependency_id",
22 to = "id"
23 )]
24 pub dependency: BelongsTo<super::issue_item::Entity>,
25}
26
27impl ActiveModelBehavior for ActiveModel {}
Added lib/models/src/issue/issue_event_line.rs View lib/models/src/issue/issue_event_line.rs
@@ -0,0 +1,32 @@
1use sea_orm::entity::prelude::*;
2use time::OffsetDateTime;
3use uuid::Uuid;
4
5#[sea_orm::model]
6#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
7#[sea_orm(table_name = "issue_event_line")]
8pub struct Model {
9 #[sea_orm(primary_key, auto_increment = false)]
10 pub id: Uuid,
11 pub issue_id: Option<Uuid>,
12 // project 模块暂时移除后保留列,恢复时加回 belongs_to(同 comment/issue_pin/issue_reaction/issue_watch)
13 pub project_id: Option<Uuid>,
14 pub user_id: Uuid,
15 pub event_type: String,
16 pub comment_id: Option<Uuid>,
17 pub commit_sha: Option<String>,
18 pub old_title: Option<String>,
19 pub new_title: Option<String>,
20 pub old_ref: Option<String>,
21 pub new_ref: Option<String>,
22 pub content: Option<String>,
23 pub created_at: OffsetDateTime,
24 #[sea_orm(belongs_to, from = "issue_id", to = "id")]
25 pub issue: BelongsTo<Option<super::issue_item::Entity>>,
26 #[sea_orm(belongs_to, from = "comment_id", to = "id")]
27 pub comment: BelongsTo<Option<super::comment::Entity>>,
28 #[sea_orm(belongs_to, from = "user_id", to = "uid")]
29 pub user: BelongsTo<super::super::users::user::Entity>,
30}
31
32impl ActiveModelBehavior for ActiveModel {}
Added lib/models/src/issue/issue_item.rs View lib/models/src/issue/issue_item.rs
@@ -0,0 +1,30 @@
1use sea_orm::entity::prelude::*;
2use time::OffsetDateTime;
3use uuid::Uuid;
4
5#[sea_orm::model]
6#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
7#[sea_orm(table_name = "issue")]
8pub struct Model {
9 #[sea_orm(primary_key, auto_increment = false)]
10 pub id: Uuid,
11 #[sea_orm(unique_key = "collection_number")]
12 pub collection_id: Uuid,
13 #[sea_orm(unique_key = "collection_number")]
14 pub autoincrement_id: i32,
15 pub author_id: Uuid,
16 pub title: String,
17 pub body: String,
18 pub milestone_id: Option<Uuid>,
19 pub created_at: OffsetDateTime,
20 pub updated_at: Option<OffsetDateTime>,
21 pub closed_at: Option<OffsetDateTime>,
22 #[sea_orm(belongs_to, from = "collection_id", to = "id")]
23 pub collection: BelongsTo<super::issues::Entity>,
24 #[sea_orm(belongs_to, from = "author_id", to = "uid")]
25 pub author: BelongsTo<super::super::users::user::Entity>,
26 #[sea_orm(belongs_to, from = "milestone_id", to = "id")]
27 pub milestone: BelongsTo<Option<super::milestone::Entity>>,
28}
29
30impl ActiveModelBehavior for ActiveModel {}
Added lib/models/src/issue/issue_label.rs View lib/models/src/issue/issue_label.rs
@@ -0,0 +1,24 @@
1use sea_orm::entity::prelude::*;
2use time::OffsetDateTime;
3use uuid::Uuid;
4
5#[sea_orm::model]
6#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
7#[sea_orm(table_name = "issue_label")]
8pub struct Model {
9 #[sea_orm(primary_key, auto_increment = false)]
10 pub id: Uuid,
11 pub dir_id: Uuid,
12 pub name: String,
13 pub exclusive: bool,
14 pub exclusive_order: i32,
15 pub description: Option<String>,
16 pub color: String,
17 pub is_archived: bool,
18 pub created_at: OffsetDateTime,
19 pub updated_at: Option<OffsetDateTime>,
20 #[sea_orm(belongs_to, from = "dir_id", to = "id")]
21 pub dir: BelongsTo<super::super::dir::dirs::Entity>,
22}
23
24impl ActiveModelBehavior for ActiveModel {}
Added lib/models/src/issue/issue_member.rs View lib/models/src/issue/issue_member.rs
@@ -0,0 +1,37 @@
1use sea_orm::entity::prelude::*;
2use serde::{Deserialize, Serialize};
3use time::OffsetDateTime;
4use uuid::Uuid;
5
6#[derive(
7 Debug, Clone, Copy, PartialEq, Eq, Default, EnumIter, DeriveActiveEnum, Serialize, Deserialize,
8)]
9#[sea_orm(rs_type = "i16", db_type = "SmallInteger")]
10pub enum IssueMemberKind {
11 #[default]
12 #[sea_orm(num_value = 0)]
13 Assignee,
14 #[sea_orm(num_value = 1)]
15 Mentioned,
16 #[sea_orm(num_value = 2)]
17 Participant,
18}
19
20#[sea_orm::model]
21#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
22#[sea_orm(table_name = "issue_member")]
23pub struct Model {
24 #[sea_orm(primary_key, auto_increment = false)]
25 pub id: Uuid,
26 pub issue_id: Uuid,
27 pub user_id: Uuid,
28 pub kind: IssueMemberKind,
29 pub created_at: OffsetDateTime,
30 pub updated_at: Option<OffsetDateTime>,
31 #[sea_orm(belongs_to, from = "issue_id", to = "id")]
32 pub issue: BelongsTo<super::issue_item::Entity>,
33 #[sea_orm(belongs_to, from = "user_id", to = "uid")]
34 pub user: BelongsTo<super::super::users::user::Entity>,
35}
36
37impl ActiveModelBehavior for ActiveModel {}
Added lib/models/src/issue/issue_pin.rs View lib/models/src/issue/issue_pin.rs
@@ -0,0 +1,25 @@
1use sea_orm::entity::prelude::*;
2use time::OffsetDateTime;
3use uuid::Uuid;
4
5#[sea_orm::model]
6#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
7#[sea_orm(table_name = "issue_pin")]
8pub struct Model {
9 #[sea_orm(primary_key, auto_increment = false)]
10 pub id: Uuid,
11 #[sea_orm(unique_key = "collection_issue")]
12 pub collection_id: Uuid,
13 #[sea_orm(unique_key = "collection_issue")]
14 pub issue_id: Option<Uuid>,
15 // project 模块暂时移除后保留列,恢复时加回 belongs_to(同 comment/issue_event_line/issue_reaction/issue_watch)
16 pub project_id: Option<Uuid>,
17 pub pin_order: i32,
18 pub created_at: OffsetDateTime,
19 #[sea_orm(belongs_to, from = "collection_id", to = "id")]
20 pub collection: BelongsTo<super::issues::Entity>,
21 #[sea_orm(belongs_to, from = "issue_id", to = "id")]
22 pub issue: BelongsTo<Option<super::issue_item::Entity>>,
23}
24
25impl ActiveModelBehavior for ActiveModel {}
Added lib/models/src/issue/issue_reactions.rs View lib/models/src/issue/issue_reactions.rs
@@ -0,0 +1,32 @@
1use sea_orm::entity::prelude::*;
2use time::OffsetDateTime;
3use uuid::Uuid;
4
5#[sea_orm::model]
6#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
7#[sea_orm(table_name = "issue_reaction")]
8pub struct Model {
9 #[sea_orm(primary_key, auto_increment = false)]
10 pub id: Uuid,
11 #[sea_orm(unique_key = "issue_user_emoji")]
12 pub issue_id: Option<Uuid>,
13 #[sea_orm(unique_key = "comment_user_emoji")]
14 pub comment_id: Option<Uuid>,
15 // project 模块暂时移除后保留列,恢复时加回 belongs_to(同 comment/issue_event_line/issue_pin/issue_watch)
16 pub project_id: Option<Uuid>,
17 #[sea_orm(unique_key = "issue_user_emoji")]
18 #[sea_orm(unique_key = "comment_user_emoji")]
19 pub user_id: Uuid,
20 #[sea_orm(unique_key = "issue_user_emoji")]
21 #[sea_orm(unique_key = "comment_user_emoji")]
22 pub emoji: String,
23 pub created_at: OffsetDateTime,
24 #[sea_orm(belongs_to, from = "issue_id", to = "id")]
25 pub issue: BelongsTo<Option<super::issue_item::Entity>>,
26 #[sea_orm(belongs_to, from = "comment_id", to = "id")]
27 pub comment: BelongsTo<Option<super::comment::Entity>>,
28 #[sea_orm(belongs_to, from = "user_id", to = "uid")]
29 pub user: BelongsTo<super::super::users::user::Entity>,
30}
31
32impl ActiveModelBehavior for ActiveModel {}
Added lib/models/src/issue/issue_relation_label.rs View lib/models/src/issue/issue_relation_label.rs
@@ -0,0 +1,20 @@
1use sea_orm::entity::prelude::*;
2use time::OffsetDateTime;
3use uuid::Uuid;
4
5#[sea_orm::model]
6#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
7#[sea_orm(table_name = "issue_relation_label")]
8pub struct Model {
9 #[sea_orm(primary_key, auto_increment = false)]
10 pub id: Uuid,
11 pub issue_id: Uuid,
12 pub label_id: Uuid,
13 pub created_at: OffsetDateTime,
14 #[sea_orm(belongs_to, from = "issue_id", to = "id")]
15 pub issue: BelongsTo<super::issue_item::Entity>,
16 #[sea_orm(belongs_to, from = "label_id", to = "id")]
17 pub label: BelongsTo<super::issue_label::Entity>,
18}
19
20impl ActiveModelBehavior for ActiveModel {}
Added lib/models/src/issue/issue_relation_pull.rs View lib/models/src/issue/issue_relation_pull.rs
@@ -0,0 +1,20 @@
1use sea_orm::entity::prelude::*;
2use time::OffsetDateTime;
3use uuid::Uuid;
4
5#[sea_orm::model]
6#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
7#[sea_orm(table_name = "issue_relation_pull")]
8pub struct Model {
9 #[sea_orm(primary_key, auto_increment = false)]
10 pub id: Uuid,
11 pub issue_id: Uuid,
12 pub pull_request_id: Uuid,
13 pub created_at: OffsetDateTime,
14 #[sea_orm(belongs_to, from = "issue_id", to = "id")]
15 pub issue: BelongsTo<super::issue_item::Entity>,
16 #[sea_orm(belongs_to, from = "pull_request_id", to = "id")]
17 pub pull_request: BelongsTo<super::super::pull_request::pull_requests::Entity>,
18}
19
20impl ActiveModelBehavior for ActiveModel {}
Added lib/models/src/issue/issue_relation_repo.rs View lib/models/src/issue/issue_relation_repo.rs
@@ -0,0 +1,20 @@
1use sea_orm::entity::prelude::*;
2use time::OffsetDateTime;
3use uuid::Uuid;
4
5#[sea_orm::model]
6#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
7#[sea_orm(table_name = "issue_relation_repo")]
8pub struct Model {
9 #[sea_orm(primary_key, auto_increment = false)]
10 pub id: Uuid,
11 pub issue_id: Uuid,
12 pub repo_id: Uuid,
13 pub created_at: OffsetDateTime,
14 #[sea_orm(belongs_to, from = "issue_id", to = "id")]
15 pub issue: BelongsTo<super::issue_item::Entity>,
16 #[sea_orm(belongs_to, from = "repo_id", to = "id")]
17 pub repo: BelongsTo<super::super::repo::repos::Entity>,
18}
19
20impl ActiveModelBehavior for ActiveModel {}
Added lib/models/src/issue/issue_user.rs View lib/models/src/issue/issue_user.rs
@@ -0,0 +1,25 @@
1use sea_orm::entity::prelude::*;
2use time::OffsetDateTime;
3use uuid::Uuid;
4
5#[sea_orm::model]
6#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
7#[sea_orm(table_name = "issue_user")]
8pub struct Model {
9 #[sea_orm(primary_key, auto_increment = false)]
10 pub id: Uuid,
11 #[sea_orm(unique_key = "uid_issue")]
12 pub uid: Uuid,
13 #[sea_orm(unique_key = "uid_issue")]
14 pub issue_id: Uuid,
15 #[sea_orm(default_value = "false")]
16 pub is_read: bool,
17 // 提及状态统一由 issue_member(kind = Mentioned) 维护,见 issue_member 表
18 pub created_at: OffsetDateTime,
19 #[sea_orm(belongs_to, from = "uid", to = "uid")]
20 pub user: BelongsTo<super::super::users::user::Entity>,
21 #[sea_orm(belongs_to, from = "issue_id", to = "id")]
22 pub issue: BelongsTo<super::issue_item::Entity>,
23}
24
25impl ActiveModelBehavior for ActiveModel {}
Added lib/models/src/issue/issue_watch.rs View lib/models/src/issue/issue_watch.rs
@@ -0,0 +1,29 @@
1use crate::common::watch_mode::WatchMode;
2use sea_orm::entity::prelude::*;
3use time::OffsetDateTime;
4use uuid::Uuid;
5
6#[sea_orm::model]
7#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
8#[sea_orm(table_name = "issue_watch")]
9pub struct Model {
10 #[sea_orm(primary_key, auto_increment = false)]
11 pub id: Uuid,
12 #[sea_orm(unique_key = "issue_user")]
13 pub issue_id: Option<Uuid>,
14 // project 模块暂时移除后保留列,恢复时加回 belongs_to(同 comment/issue_event_line/issue_pin/issue_reaction)
15 #[sea_orm(unique_key = "project_user")]
16 pub project_id: Option<Uuid>,
17 #[sea_orm(unique_key = "issue_user")]
18 #[sea_orm(unique_key = "project_user")]
19 pub user_id: Uuid,
20 pub mode: WatchMode,
21 pub created_at: OffsetDateTime,
22 pub updated_at: Option<OffsetDateTime>,
23 #[sea_orm(belongs_to, from = "issue_id", to = "id")]
24 pub issue: BelongsTo<Option<super::issue_item::Entity>>,
25 #[sea_orm(belongs_to, from = "user_id", to = "uid")]
26 pub user: BelongsTo<super::super::users::user::Entity>,
27}
28
29impl ActiveModelBehavior for ActiveModel {}
Added lib/models/src/issue/issues.rs View lib/models/src/issue/issues.rs
@@ -0,0 +1,21 @@
1use sea_orm::entity::prelude::*;
2use time::OffsetDateTime;
3use uuid::Uuid;
4
5#[sea_orm::model]
6#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
7#[sea_orm(table_name = "issues")]
8pub struct Model {
9 #[sea_orm(primary_key, auto_increment = false)]
10 pub id: Uuid,
11 pub dir_id: Uuid,
12 pub name: String,
13 pub icon: Option<String>,
14 pub description: Option<String>,
15 pub created_at: OffsetDateTime,
16 pub updated_at: Option<OffsetDateTime>,
17 #[sea_orm(belongs_to, from = "dir_id", to = "id")]
18 pub dir: BelongsTo<super::super::dir::dirs::Entity>,
19}
20
21impl ActiveModelBehavior for ActiveModel {}
Added lib/models/src/issue/milestone.rs View lib/models/src/issue/milestone.rs
@@ -0,0 +1,27 @@
1use sea_orm::entity::prelude::*;
2use time::OffsetDateTime;
3use uuid::Uuid;
4
5#[sea_orm::model]
6#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
7#[sea_orm(table_name = "milestone")]
8pub struct Model {
9 #[sea_orm(primary_key, auto_increment = false)]
10 pub id: Uuid,
11 pub collection_id: Uuid,
12 pub name: String,
13 pub description: Option<String>,
14 #[sea_orm(default_value = "false")]
15 pub is_closed: bool,
16 pub due_at: Option<OffsetDateTime>,
17 #[sea_orm(default_value = 0)]
18 pub nums_issues: i32,
19 #[sea_orm(default_value = 0)]
20 pub nums_closed_issues: i32,
21 pub created_at: OffsetDateTime,
22 pub updated_at: Option<OffsetDateTime>,
23 #[sea_orm(belongs_to, from = "collection_id", to = "id")]
24 pub collection: BelongsTo<super::issues::Entity>,
25}
26
27impl ActiveModelBehavior for ActiveModel {}
Added lib/models/src/issue/mod.rs View lib/models/src/issue/mod.rs
@@ -0,0 +1,20 @@
1#![allow(clippy::module_inception)]
2
3pub mod comment;
4pub mod issue_content_history;
5pub mod issue_dependency;
6pub mod issue_event_line;
7pub mod issue_item;
8pub mod issue_label;
9pub mod issue_member;
10pub mod issue_pin;
11pub mod issue_reactions;
12pub mod issue_relation_label;
13pub mod issue_relation_pull;
14pub mod issue_relation_repo;
15pub mod issue_user;
16pub mod issue_watch;
17pub mod issues;
18pub mod milestone;
19pub mod stopwatch;
20pub mod tracked_time;
Added lib/models/src/issue/stopwatch.rs View lib/models/src/issue/stopwatch.rs
@@ -0,0 +1,23 @@
1use sea_orm::entity::prelude::*;
2use time::OffsetDateTime;
3use uuid::Uuid;
4
5#[sea_orm::model]
6#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
7#[sea_orm(table_name = "stopwatch")]
8pub struct Model {
9 #[sea_orm(primary_key, auto_increment = false)]
10 pub id: Uuid,
11 #[sea_orm(unique_key = "user_issue")]
12 pub issue_id: Uuid,
13 #[sea_orm(unique_key = "user_issue")]
14 pub user_id: Uuid,
15 pub started_at: OffsetDateTime,
16 pub created_at: OffsetDateTime,
17 #[sea_orm(belongs_to, from = "issue_id", to = "id")]
18 pub issue: BelongsTo<super::issue_item::Entity>,
19 #[sea_orm(belongs_to, from = "user_id", to = "uid")]
20 pub user: BelongsTo<super::super::users::user::Entity>,
21}
22
23impl ActiveModelBehavior for ActiveModel {}
Added lib/models/src/issue/tracked_time.rs View lib/models/src/issue/tracked_time.rs
@@ -0,0 +1,21 @@
1use sea_orm::entity::prelude::*;
2use time::OffsetDateTime;
3use uuid::Uuid;
4
5#[sea_orm::model]
6#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
7#[sea_orm(table_name = "tracked_time")]
8pub struct Model {
9 #[sea_orm(primary_key, auto_increment = false)]
10 pub id: Uuid,
11 pub issue_id: Uuid,
12 pub user_id: Uuid,
13 pub seconds: i64,
14 pub created_at: OffsetDateTime,
15 #[sea_orm(belongs_to, from = "issue_id", to = "id")]
16 pub issue: BelongsTo<super::issue_item::Entity>,
17 #[sea_orm(belongs_to, from = "user_id", to = "uid")]
18 pub user: BelongsTo<super::super::users::user::Entity>,
19}
20
21impl ActiveModelBehavior for ActiveModel {}
Added lib/models/src/products/mod.rs View lib/models/src/products/mod.rs
@@ -0,0 +1,6 @@
1#![allow(clippy::module_inception)]
2
3pub mod product_installs;
4pub mod product_reviews;
5pub mod product_versions;
6pub mod products;
Added lib/models/src/products/product_installs.rs View lib/models/src/products/product_installs.rs
@@ -0,0 +1,42 @@
1use sea_orm::entity::prelude::*;
2use serde::{Deserialize, Serialize};
3use time::OffsetDateTime;
4use uuid::Uuid;
5
6#[derive(
7 Debug, Clone, Copy, PartialEq, Eq, Default, EnumIter, DeriveActiveEnum, Serialize, Deserialize,
8)]
9#[sea_orm(rs_type = "i16", db_type = "SmallInteger")]
10pub enum InstallStatus {
11 #[default]
12 #[sea_orm(num_value = 0)]
13 Active,
14 #[sea_orm(num_value = 1)]
15 Disabled,
16 #[sea_orm(num_value = 2)]
17 Uninstalled,
18}
19
20#[sea_orm::model]
21#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
22#[sea_orm(table_name = "product_install")]
23pub struct Model {
24 #[sea_orm(primary_key, auto_increment = false)]
25 pub id: Uuid,
26 #[sea_orm(unique_key = "space_product")]
27 pub space_id: Uuid,
28 #[sea_orm(unique_key = "space_product")]
29 pub product_id: Uuid,
30 pub installed_by: Uuid,
31 pub status: InstallStatus,
32 pub installed_at: OffsetDateTime,
33 pub uninstalled_at: Option<OffsetDateTime>,
34 #[sea_orm(belongs_to, from = "space_id", to = "id")]
35 pub space: BelongsTo<super::super::space::spaces::Entity>,
36 #[sea_orm(belongs_to, from = "product_id", to = "id")]
37 pub product: BelongsTo<super::products::Entity>,
38 #[sea_orm(belongs_to, from = "installed_by", to = "uid")]
39 pub installer: BelongsTo<super::super::users::user::Entity>,
40}
41
42impl ActiveModelBehavior for ActiveModel {}
Added lib/models/src/products/product_reviews.rs View lib/models/src/products/product_reviews.rs
@@ -0,0 +1,24 @@
1use sea_orm::entity::prelude::*;
2use time::OffsetDateTime;
3use uuid::Uuid;
4
5#[sea_orm::model]
6#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
7#[sea_orm(table_name = "product_review")]
8pub struct Model {
9 #[sea_orm(primary_key, auto_increment = false)]
10 pub id: Uuid,
11 #[sea_orm(unique_key = "product_user")]
12 pub product_id: Uuid,
13 #[sea_orm(unique_key = "product_user")]
14 pub user_id: Uuid,
15 pub rating: i16,
16 pub comment: Option<String>,
17 pub created_at: OffsetDateTime,
18 #[sea_orm(belongs_to, from = "product_id", to = "id")]
19 pub product: BelongsTo<super::products::Entity>,
20 #[sea_orm(belongs_to, from = "user_id", to = "uid")]
21 pub user: BelongsTo<super::super::users::user::Entity>,
22}
23
24impl ActiveModelBehavior for ActiveModel {}
Added lib/models/src/products/product_versions.rs View lib/models/src/products/product_versions.rs
@@ -0,0 +1,24 @@
1use sea_orm::entity::prelude::*;
2use time::OffsetDateTime;
3use uuid::Uuid;
4
5#[sea_orm::model]
6#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
7#[sea_orm(table_name = "product_versions")]
8pub struct Model {
9 #[sea_orm(primary_key, auto_increment = false)]
10 pub id: Uuid,
11 #[sea_orm(unique_key = "product_version")]
12 pub product_id: Uuid,
13 #[sea_orm(unique_key = "product_version")]
14 pub version: String,
15 pub changelog: Option<String>,
16 pub artifact_url: Option<String>,
17 #[sea_orm(default_value = "false")]
18 pub is_active: bool,
19 pub created_at: OffsetDateTime,
20 #[sea_orm(belongs_to, from = "product_id", to = "id")]
21 pub product: BelongsTo<super::products::Entity>,
22}
23
24impl ActiveModelBehavior for ActiveModel {}
Added lib/models/src/products/products.rs View lib/models/src/products/products.rs
@@ -0,0 +1,51 @@
1use sea_orm::entity::prelude::*;
2use serde::{Deserialize, Serialize};
3use time::OffsetDateTime;
4use uuid::Uuid;
5
6#[derive(
7 Debug, Clone, Copy, PartialEq, Eq, Default, EnumIter, DeriveActiveEnum, Serialize, Deserialize,
8)]
9#[sea_orm(rs_type = "i16", db_type = "SmallInteger")]
10pub enum ProductKind {
11 #[default]
12 #[sea_orm(num_value = 0)]
13 App,
14 #[sea_orm(num_value = 1)]
15 Bot,
16 #[sea_orm(num_value = 2)]
17 Extension,
18}
19
20#[sea_orm::model]
21#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
22#[sea_orm(table_name = "products")]
23pub struct Model {
24 #[sea_orm(primary_key, auto_increment = false)]
25 pub id: Uuid,
26 pub space_id: Option<Uuid>,
27 pub owner_id: Uuid,
28 pub name: String,
29 #[sea_orm(unique)]
30 pub slug: String,
31 pub description: Option<String>,
32 pub icon: Option<String>,
33 pub kind: ProductKind,
34 pub latest_version: Option<String>,
35 #[sea_orm(default_value = "false")]
36 pub is_published: bool,
37 #[sea_orm(default_value = "true")]
38 pub is_active: bool,
39 #[sea_orm(default_value = 0)]
40 pub nums_installs: i64,
41 pub price: Option<i64>,
42 pub price_currency: Option<String>,
43 pub created_at: OffsetDateTime,
44 pub updated_at: OffsetDateTime,
45 #[sea_orm(belongs_to, from = "space_id", to = "id")]
46 pub space: BelongsTo<Option<super::super::space::spaces::Entity>>,
47 #[sea_orm(belongs_to, from = "owner_id", to = "uid")]
48 pub owner: BelongsTo<super::super::users::user::Entity>,
49}
50
51impl ActiveModelBehavior for ActiveModel {}
Added lib/models/src/pull_request/mod.rs View lib/models/src/pull_request/mod.rs
@@ -0,0 +1,4 @@
1pub mod pull_request_comment;
2pub mod pull_request_event_line;
3pub mod pull_request_review;
4pub mod pull_requests;
Added lib/models/src/pull_request/pull_request_comment.rs View lib/models/src/pull_request/pull_request_comment.rs
@@ -0,0 +1,28 @@
1use sea_orm::entity::prelude::*;
2use time::OffsetDateTime;
3use uuid::Uuid;
4
5#[sea_orm::model]
6#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
7#[sea_orm(table_name = "pull_request_comment")]
8pub struct Model {
9 #[sea_orm(primary_key, auto_increment = false)]
10 pub id: Uuid,
11 pub pull_request_id: Uuid,
12 pub user_id: Uuid,
13 pub review_id: Option<Uuid>,
14 pub commit_sha: String,
15 pub path: String,
16 pub line: Option<i32>,
17 pub body: String,
18 pub created_at: OffsetDateTime,
19 pub updated_at: Option<OffsetDateTime>,
20 #[sea_orm(belongs_to, from = "pull_request_id", to = "id")]
21 pub pull_request: BelongsTo<super::pull_requests::Entity>,
22 #[sea_orm(belongs_to, from = "user_id", to = "uid")]
23 pub user: BelongsTo<super::super::users::user::Entity>,
24 #[sea_orm(belongs_to, from = "review_id", to = "id")]
25 pub review: BelongsTo<Option<super::pull_request_review::Entity>>,
26}
27
28impl ActiveModelBehavior for ActiveModel {}
Added lib/models/src/pull_request/pull_request_event_line.rs View lib/models/src/pull_request/pull_request_event_line.rs
@@ -0,0 +1,23 @@
1use sea_orm::entity::prelude::*;
2use time::OffsetDateTime;
3use uuid::Uuid;
4
5#[sea_orm::model]
6#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
7#[sea_orm(table_name = "pull_request_event_line")]
8pub struct Model {
9 #[sea_orm(primary_key, auto_increment = false)]
10 pub id: Uuid,
11 pub pull_request_id: Uuid,
12 pub user_id: Uuid,
13 pub event_type: String,
14 pub commit_sha: Option<String>,
15 pub content: Option<String>,
16 pub created_at: OffsetDateTime,
17 #[sea_orm(belongs_to, from = "pull_request_id", to = "id")]
18 pub pull_request: BelongsTo<super::pull_requests::Entity>,
19 #[sea_orm(belongs_to, from = "user_id", to = "uid")]
20 pub user: BelongsTo<super::super::users::user::Entity>,
21}
22
23impl ActiveModelBehavior for ActiveModel {}
Added lib/models/src/pull_request/pull_request_review.rs View lib/models/src/pull_request/pull_request_review.rs
@@ -0,0 +1,40 @@
1use sea_orm::entity::prelude::*;
2use serde::{Deserialize, Serialize};
3use time::OffsetDateTime;
4use uuid::Uuid;
5
6#[derive(
7 Debug, Clone, Copy, PartialEq, Eq, Default, EnumIter, DeriveActiveEnum, Serialize, Deserialize,
8)]
9#[sea_orm(rs_type = "i16", db_type = "SmallInteger")]
10pub enum ReviewState {
11 #[default]
12 #[sea_orm(num_value = 0)]
13 Approved,
14 #[sea_orm(num_value = 1)]
15 ChangesRequested,
16 #[sea_orm(num_value = 2)]
17 Commented,
18}
19
20#[sea_orm::model]
21#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
22#[sea_orm(table_name = "pull_request_review")]
23pub struct Model {
24 #[sea_orm(primary_key, auto_increment = false)]
25 pub id: Uuid,
26 pub pull_request_id: Uuid,
27 pub user_id: Uuid,
28 pub commit_sha: String,
29 pub official: bool,
30 pub content: Option<String>,
31 pub state: ReviewState,
32 pub created_at: OffsetDateTime,
33 pub updated_at: Option<OffsetDateTime>,
34 #[sea_orm(belongs_to, from = "pull_request_id", to = "id")]
35 pub pull_request: BelongsTo<super::pull_requests::Entity>,
36 #[sea_orm(belongs_to, from = "user_id", to = "uid")]
37 pub user: BelongsTo<super::super::users::user::Entity>,
38}
39
40impl ActiveModelBehavior for ActiveModel {}
Added lib/models/src/pull_request/pull_requests.rs View lib/models/src/pull_request/pull_requests.rs
@@ -0,0 +1,44 @@
1use sea_orm::entity::prelude::*;
2use time::OffsetDateTime;
3use uuid::Uuid;
4
5#[sea_orm::model]
6#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
7#[sea_orm(table_name = "pull_request")]
8pub struct Model {
9 #[sea_orm(primary_key, auto_increment = false)]
10 pub id: Uuid,
11 #[sea_orm(unique_key = "repo_number")]
12 pub repo_id: Uuid,
13 pub issue_id: Uuid,
14 #[sea_orm(unique_key = "repo_number")]
15 pub autoincrement_id: i32,
16 pub author_id: Uuid,
17 pub title: String,
18 pub body: String,
19 pub head_branch: String,
20 pub head_sha: String,
21 pub base_branch: String,
22 pub base_sha: String,
23 pub has_merged: bool,
24 pub merged_commit_sha: Option<String>,
25 pub additions: i32,
26 pub deletions: i32,
27 pub files_changed: i32,
28 pub merger_id: Option<Uuid>,
29 pub merged_at: Option<OffsetDateTime>,
30 pub is_locked: bool,
31 pub created_at: OffsetDateTime,
32 pub updated_at: Option<OffsetDateTime>,
33 pub closed_at: Option<OffsetDateTime>,
34 #[sea_orm(belongs_to, from = "repo_id", to = "id")]
35 pub repo: BelongsTo<super::super::repo::repos::Entity>,
36 #[sea_orm(belongs_to, from = "issue_id", to = "id")]
37 pub issue: BelongsTo<super::super::issue::issue_item::Entity>,
38 #[sea_orm(belongs_to, relation_enum = "Author", from = "author_id", to = "uid")]
39 pub author: BelongsTo<super::super::users::user::Entity>,
40 #[sea_orm(belongs_to, relation_enum = "Merger", from = "merger_id", to = "uid")]
41 pub merger: BelongsTo<Option<super::super::users::user::Entity>>,
42}
43
44impl ActiveModelBehavior for ActiveModel {}
Added lib/models/src/repo/mod.rs View lib/models/src/repo/mod.rs
@@ -0,0 +1,13 @@
1pub mod repo_branch_protection_policy;
2pub mod repo_branch_protection_whitelists;
3pub mod repo_collaborations;
4pub mod repo_deploy_keys;
5pub mod repo_license;
6pub mod repo_relation_topic;
7pub mod repo_release;
8pub mod repo_star;
9pub mod repo_tag_protection_policy;
10pub mod repo_tag_protection_whitelists;
11pub mod repo_topic;
12pub mod repo_watch;
13pub mod repos;
Added lib/models/src/repo/repo_branch_protection_policy.rs View lib/models/src/repo/repo_branch_protection_policy.rs
@@ -0,0 +1,33 @@
1use sea_orm::entity::prelude::*;
2use time::OffsetDateTime;
3use uuid::Uuid;
4
5#[sea_orm::model]
6#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
7#[sea_orm(table_name = "repo_branch_protection_policy")]
8pub struct Model {
9 #[sea_orm(primary_key, auto_increment = false)]
10 pub id: Uuid,
11 pub repo_id: Uuid,
12 pub branch_pattern: String,
13 pub enable_push: bool,
14 pub enable_push_whitelist: bool,
15 pub push_whitelist_deploy_keys: bool,
16 pub enable_merge_whitelist: bool,
17 pub required_approvals: i32,
18 pub enable_approvals_whitelist: bool,
19 pub block_on_rejected_reviews: bool,
20 pub block_on_change_requested_reviews: bool,
21 pub block_on_outdated_branch: bool,
22 pub dismiss_stale_approvals: bool,
23 pub require_signed_commits: bool,
24 pub protected_file_patterns: String,
25 pub unprotected_file_patterns: String,
26 pub enable_force_push: bool,
27 pub created_at: OffsetDateTime,
28 pub updated_at: OffsetDateTime,
29 #[sea_orm(belongs_to, from = "repo_id", to = "id")]
30 pub repo: BelongsTo<super::repos::Entity>,
31}
32
33impl ActiveModelBehavior for ActiveModel {}
Added lib/models/src/repo/repo_branch_protection_whitelists.rs View lib/models/src/repo/repo_branch_protection_whitelists.rs
@@ -0,0 +1,41 @@
1use sea_orm::entity::prelude::*;
2use serde::{Deserialize, Serialize};
3use uuid::Uuid;
4
5#[derive(
6 Debug, Clone, Copy, PartialEq, Eq, Default, EnumIter, DeriveActiveEnum, Serialize, Deserialize,
7)]
8#[sea_orm(rs_type = "i16", db_type = "SmallInteger")]
9pub enum WhitelistKind {
10 #[default]
11 #[sea_orm(num_value = 0)]
12 Push,
13 #[sea_orm(num_value = 1)]
14 Merge,
15 #[sea_orm(num_value = 2)]
16 Approval,
17}
18
19#[sea_orm::model]
20#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
21#[sea_orm(table_name = "repo_branch_protection_whitelist")]
22pub struct Model {
23 #[sea_orm(primary_key, auto_increment = false)]
24 pub id: Uuid,
25 #[sea_orm(unique_key = "policy_principal")]
26 pub policy_id: Uuid,
27 #[sea_orm(unique_key = "policy_principal")]
28 pub kind: WhitelistKind,
29 #[sea_orm(unique_key = "policy_principal")]
30 pub user_id: Option<Uuid>,
31 #[sea_orm(unique_key = "policy_principal")]
32 pub group_id: Option<Uuid>,
33 #[sea_orm(belongs_to, from = "policy_id", to = "id")]
34 pub policy: BelongsTo<super::repo_branch_protection_policy::Entity>,
35 #[sea_orm(belongs_to, from = "user_id", to = "uid")]
36 pub user: BelongsTo<Option<super::super::users::user::Entity>>,
37 #[sea_orm(belongs_to, from = "group_id", to = "id")]
38 pub group: BelongsTo<Option<super::super::space::space_groups::Entity>>,
39}
40
41impl ActiveModelBehavior for ActiveModel {}
Added lib/models/src/repo/repo_collaborations.rs View lib/models/src/repo/repo_collaborations.rs
@@ -0,0 +1,39 @@
1use sea_orm::entity::prelude::*;
2use serde::{Deserialize, Serialize};
3use time::OffsetDateTime;
4use uuid::Uuid;
5
6#[derive(
7 Debug, Clone, Copy, PartialEq, Eq, Default, EnumIter, DeriveActiveEnum, Serialize, Deserialize,
8)]
9#[sea_orm(rs_type = "i16", db_type = "SmallInteger")]
10pub enum CollaborationMode {
11 #[default]
12 #[sea_orm(num_value = 0)]
13 Read,
14 #[sea_orm(num_value = 1)]
15 Write,
16 #[sea_orm(num_value = 2)]
17 Admin,
18}
19
20#[sea_orm::model]
21#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
22#[sea_orm(table_name = "repo_collaboration")]
23pub struct Model {
24 #[sea_orm(primary_key, auto_increment = false)]
25 pub id: Uuid,
26 #[sea_orm(unique_key = "repo_user")]
27 pub repo_id: Uuid,
28 #[sea_orm(unique_key = "repo_user")]
29 pub user_id: Uuid,
30 pub mode: CollaborationMode,
31 pub created_at: OffsetDateTime,
32 pub updated_at: OffsetDateTime,
33 #[sea_orm(belongs_to, from = "repo_id", to = "id")]
34 pub repo: BelongsTo<super::repos::Entity>,
35 #[sea_orm(belongs_to, from = "user_id", to = "uid")]
36 pub user: BelongsTo<super::super::users::user::Entity>,
37}
38
39impl ActiveModelBehavior for ActiveModel {}
Added lib/models/src/repo/repo_deploy_keys.rs View lib/models/src/repo/repo_deploy_keys.rs
@@ -0,0 +1,39 @@
1use sea_orm::entity::prelude::*;
2use serde::{Deserialize, Serialize};
3use time::OffsetDateTime;
4use uuid::Uuid;
5
6#[derive(
7 Debug, Clone, Copy, PartialEq, Eq, Default, EnumIter, DeriveActiveEnum, Serialize, Deserialize,
8)]
9#[sea_orm(rs_type = "i16", db_type = "SmallInteger")]
10pub enum DeployKeyType {
11 #[default]
12 #[sea_orm(num_value = 0)]
13 Rsa,
14 #[sea_orm(num_value = 1)]
15 Ed25519,
16 #[sea_orm(num_value = 2)]
17 Ecdsa,
18}
19
20#[sea_orm::model]
21#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
22#[sea_orm(table_name = "repo_deploy_key")]
23pub struct Model {
24 #[sea_orm(primary_key, auto_increment = false)]
25 pub id: Uuid,
26 pub repo_id: Uuid,
27 pub title: String,
28 pub key_type: DeployKeyType,
29 #[sea_orm(unique)]
30 pub fingerprint: String,
31 pub content: String,
32 pub is_read_only: bool,
33 pub last_used_at: Option<OffsetDateTime>,
34 pub created_at: OffsetDateTime,
35 #[sea_orm(belongs_to, from = "repo_id", to = "id")]
36 pub repo: BelongsTo<super::repos::Entity>,
37}
38
39impl ActiveModelBehavior for ActiveModel {}
Added lib/models/src/repo/repo_license.rs View lib/models/src/repo/repo_license.rs
@@ -0,0 +1,18 @@
1use sea_orm::entity::prelude::*;
2use time::OffsetDateTime;
3
4#[sea_orm::model]
5#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
6#[sea_orm(table_name = "repo_license")]
7pub struct Model {
8 #[sea_orm(primary_key, auto_increment = false)]
9 pub name: String,
10 pub display_name: String,
11 pub description: Option<String>,
12 pub body: String,
13 pub url: Option<String>,
14 pub created_at: OffsetDateTime,
15 pub updated_at: OffsetDateTime,
16}
17
18impl ActiveModelBehavior for ActiveModel {}
Added lib/models/src/repo/repo_relation_topic.rs View lib/models/src/repo/repo_relation_topic.rs
@@ -0,0 +1,20 @@
1use sea_orm::entity::prelude::*;
2use time::OffsetDateTime;
3use uuid::Uuid;
4
5#[sea_orm::model]
6#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
7#[sea_orm(table_name = "repo_relation_topic")]
8pub struct Model {
9 #[sea_orm(primary_key, auto_increment = false)]
10 pub id: Uuid,
11 pub repo_id: Uuid,
12 pub topic_id: Uuid,
13 pub created_at: OffsetDateTime,
14 #[sea_orm(belongs_to, from = "repo_id", to = "id")]
15 pub repo: BelongsTo<super::repos::Entity>,
16 #[sea_orm(belongs_to, from = "topic_id", to = "id")]
17 pub topic: BelongsTo<super::repo_topic::Entity>,
18}
19
20impl ActiveModelBehavior for ActiveModel {}
Added lib/models/src/repo/repo_release.rs View lib/models/src/repo/repo_release.rs
@@ -0,0 +1,34 @@
1use sea_orm::entity::prelude::*;
2use time::OffsetDateTime;
3use uuid::Uuid;
4
5#[sea_orm::model]
6#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
7#[sea_orm(table_name = "repo_release")]
8pub struct Model {
9 #[sea_orm(primary_key, auto_increment = false)]
10 pub id: Uuid,
11 #[sea_orm(unique_key = "repo_tag")]
12 pub repo_id: Uuid,
13 pub publisher_id: Uuid,
14 pub publisher_name: String,
15 pub tag_name: String,
16 #[sea_orm(unique_key = "repo_tag")]
17 pub lower_tag_name: String,
18 pub target: String,
19 pub title: String,
20 pub content: Option<String>,
21 pub sha: String,
22 pub num_commits: i32,
23 pub is_draft: bool,
24 pub is_prerelease: bool,
25 pub is_tag: bool,
26 pub created_at: OffsetDateTime,
27 pub updated_at: OffsetDateTime,
28 #[sea_orm(belongs_to, from = "repo_id", to = "id")]
29 pub repo: BelongsTo<super::repos::Entity>,
30 #[sea_orm(belongs_to, from = "publisher_id", to = "uid")]
31 pub publisher: BelongsTo<super::super::users::user::Entity>,
32}
33
34impl ActiveModelBehavior for ActiveModel {}
Added lib/models/src/repo/repo_star.rs View lib/models/src/repo/repo_star.rs
@@ -0,0 +1,23 @@
1use sea_orm::entity::prelude::*;
2use time::OffsetDateTime;
3use uuid::Uuid;
4
5#[sea_orm::model]
6#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
7#[sea_orm(table_name = "repo_star")]
8pub struct Model {
9 #[sea_orm(primary_key, auto_increment = false)]
10 pub id: Uuid,
11 #[sea_orm(unique_key = "user_repo")]
12 pub user_id: Uuid,
13 #[sea_orm(unique_key = "user_repo")]
14 pub repo_id: Uuid,
15 pub created_at: OffsetDateTime,
16 pub updated_at: OffsetDateTime,
17 #[sea_orm(belongs_to, from = "user_id", to = "uid")]
18 pub user: BelongsTo<super::super::users::user::Entity>,
19 #[sea_orm(belongs_to, from = "repo_id", to = "id")]
20 pub repo: BelongsTo<super::repos::Entity>,
21}
22
23impl ActiveModelBehavior for ActiveModel {}
Added lib/models/src/repo/repo_tag_protection_policy.rs View lib/models/src/repo/repo_tag_protection_policy.rs
@@ -0,0 +1,20 @@
1use sea_orm::entity::prelude::*;
2use time::OffsetDateTime;
3use uuid::Uuid;
4
5#[sea_orm::model]
6#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
7#[sea_orm(table_name = "repo_tag_protection_policy")]
8pub struct Model {
9 #[sea_orm(primary_key, auto_increment = false)]
10 pub id: Uuid,
11 pub repo_id: Uuid,
12 pub name_pattern: String,
13 // 白名单用户见 repo_tag_protection_whitelist 关联表
14 pub created_at: OffsetDateTime,
15 pub updated_at: OffsetDateTime,
16 #[sea_orm(belongs_to, from = "repo_id", to = "id")]
17 pub repo: BelongsTo<super::repos::Entity>,
18}
19
20impl ActiveModelBehavior for ActiveModel {}
Added lib/models/src/repo/repo_tag_protection_whitelists.rs View lib/models/src/repo/repo_tag_protection_whitelists.rs
@@ -0,0 +1,18 @@
1use sea_orm::entity::prelude::*;
2use uuid::Uuid;
3
4#[sea_orm::model]
5#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
6#[sea_orm(table_name = "repo_tag_protection_whitelist")]
7pub struct Model {
8 #[sea_orm(primary_key, auto_increment = false)]
9 pub policy_id: Uuid,
10 #[sea_orm(primary_key, auto_increment = false)]
11 pub user_id: Uuid,
12 #[sea_orm(belongs_to, from = "policy_id", to = "id")]
13 pub policy: BelongsTo<super::repo_tag_protection_policy::Entity>,
14 #[sea_orm(belongs_to, from = "user_id", to = "uid")]
15 pub user: BelongsTo<super::super::users::user::Entity>,
16}
17
18impl ActiveModelBehavior for ActiveModel {}
Added lib/models/src/repo/repo_topic.rs View lib/models/src/repo/repo_topic.rs
@@ -0,0 +1,16 @@
1use sea_orm::entity::prelude::*;
2use time::OffsetDateTime;
3use uuid::Uuid;
4
5#[sea_orm::model]
6#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
7#[sea_orm(table_name = "repo_topic")]
8pub struct Model {
9 #[sea_orm(primary_key, auto_increment = false)]
10 pub id: Uuid,
11 pub name: String,
12 pub created_at: OffsetDateTime,
13 pub updated_at: OffsetDateTime,
14}
15
16impl ActiveModelBehavior for ActiveModel {}
Added lib/models/src/repo/repo_watch.rs View lib/models/src/repo/repo_watch.rs
@@ -0,0 +1,25 @@
1use crate::common::watch_mode::WatchMode;
2use sea_orm::entity::prelude::*;
3use time::OffsetDateTime;
4use uuid::Uuid;
5
6#[sea_orm::model]
7#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
8#[sea_orm(table_name = "repo_watch")]
9pub struct Model {
10 #[sea_orm(primary_key, auto_increment = false)]
11 pub id: Uuid,
12 #[sea_orm(unique_key = "user_repo")]
13 pub user_id: Uuid,
14 #[sea_orm(unique_key = "user_repo")]
15 pub repo_id: Uuid,
16 pub mode: WatchMode,
17 pub created_at: OffsetDateTime,
18 pub updated_at: OffsetDateTime,
19 #[sea_orm(belongs_to, from = "user_id", to = "uid")]
20 pub user: BelongsTo<super::super::users::user::Entity>,
21 #[sea_orm(belongs_to, from = "repo_id", to = "id")]
22 pub repo: BelongsTo<super::repos::Entity>,
23}
24
25impl ActiveModelBehavior for ActiveModel {}
Added lib/models/src/repo/repos.rs View lib/models/src/repo/repos.rs
@@ -0,0 +1,57 @@
1use sea_orm::entity::prelude::*;
2use time::OffsetDateTime;
3use uuid::Uuid;
4
5#[sea_orm::model]
6#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
7#[sea_orm(table_name = "repos")]
8pub struct Model {
9 #[sea_orm(primary_key, auto_increment = false)]
10 pub id: Uuid,
11 pub dir_id: Uuid,
12 #[sea_orm(unique)]
13 pub lower_name: String,
14 pub name: String,
15 pub description: Option<String>,
16 pub website: Option<String>,
17 pub default_branch: String,
18 pub nums_watch: i32,
19 pub nums_star: i32,
20 pub nums_pr: i32,
21 pub nums_commits: i32,
22 pub nums_branches: i32,
23 pub nums_tags: i32,
24 pub enable_lfs: bool,
25 #[sea_orm(default_value = "false")]
26 pub is_private: bool,
27 #[sea_orm(default_value = "false")]
28 pub is_archived: bool,
29 pub storage_limit_size: Option<i64>,
30 pub size: i64,
31 pub git_size: i64,
32 pub lfs_size: i64,
33 pub avatar_url: Option<String>,
34 pub primary_language: Option<String>,
35 pub license_name: Option<String>,
36 pub created_at: OffsetDateTime,
37 pub updated_at: OffsetDateTime,
38 pub last_pushed_at: Option<OffsetDateTime>,
39 pub archived_at: Option<OffsetDateTime>,
40 pub readme_raw: Option<String>,
41 #[sea_orm(default_value = "true")]
42 pub allow_rebase: bool,
43 #[sea_orm(default_value = "false")]
44 pub allow_rebase_explicit: bool,
45 #[sea_orm(default_value = "true")]
46 pub allow_squash_merge: bool,
47 #[sea_orm(default_value = "true")]
48 pub allow_merge_commits: bool,
49 #[sea_orm(default_value = "sha1")]
50 pub object_format: String,
51 #[sea_orm(belongs_to, from = "dir_id", to = "id")]
52 pub dir: BelongsTo<super::super::dir::dirs::Entity>,
53 #[sea_orm(belongs_to, from = "license_name", to = "name")]
54 pub license: BelongsTo<Option<super::repo_license::Entity>>,
55}
56
57impl ActiveModelBehavior for ActiveModel {}
Added lib/models/src/space/mod.rs View lib/models/src/space/mod.rs
@@ -0,0 +1,11 @@
1pub mod space_bans;
2pub mod space_group_permissions;
3pub mod space_groups;
4pub mod space_invite;
5pub mod space_invite_groups;
6pub mod space_member_groups;
7pub mod space_members;
8pub mod space_warn;
9pub mod spaces;
10
11pub use spaces::*;
Added lib/models/src/space/space_bans.rs View lib/models/src/space/space_bans.rs
@@ -0,0 +1,30 @@
1use sea_orm::entity::prelude::*;
2use time::OffsetDateTime;
3use uuid::Uuid;
4
5#[sea_orm::model]
6#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
7#[sea_orm(table_name = "space_bans")]
8pub struct Model {
9 #[sea_orm(primary_key, auto_increment = false)]
10 pub space_id: Uuid,
11 #[sea_orm(primary_key, auto_increment = false)]
12 pub user_id: Uuid,
13 pub reason: Option<String>,
14 pub banned_by: Uuid,
15 pub created_at: OffsetDateTime,
16 pub updated_at: OffsetDateTime,
17 pub expires_at: Option<OffsetDateTime>,
18 pub allow_agent_join: bool,
19 pub allow_speak: bool,
20 pub allow_push_repo: bool,
21 pub allow_comment_any: bool,
22 #[sea_orm(belongs_to, from = "space_id", to = "id")]
23 pub space: BelongsTo<super::spaces::Entity>,
24 #[sea_orm(belongs_to, relation_enum = "BannedUser", from = "user_id", to = "uid")]
25 pub user: BelongsTo<super::super::users::user::Entity>,
26 #[sea_orm(belongs_to, relation_enum = "BannedBy", from = "banned_by", to = "uid")]
27 pub ban_actor: BelongsTo<super::super::users::user::Entity>,
28}
29
30impl ActiveModelBehavior for ActiveModel {}
Added lib/models/src/space/space_group_permissions.rs View lib/models/src/space/space_group_permissions.rs
@@ -0,0 +1,16 @@
1use sea_orm::entity::prelude::*;
2use uuid::Uuid;
3
4#[sea_orm::model]
5#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
6#[sea_orm(table_name = "space_group_permissions")]
7pub struct Model {
8 #[sea_orm(primary_key, auto_increment = false)]
9 pub group_id: Uuid,
10 #[sea_orm(primary_key, auto_increment = false)]
11 pub permission: String,
12 #[sea_orm(belongs_to, from = "group_id", to = "id")]
13 pub group: BelongsTo<super::space_groups::Entity>,
14}
15
16impl ActiveModelBehavior for ActiveModel {}
Added lib/models/src/space/space_groups.rs View lib/models/src/space/space_groups.rs
@@ -0,0 +1,29 @@
1use sea_orm::entity::prelude::*;
2use time::OffsetDateTime;
3use uuid::Uuid;
4
5#[sea_orm::model]
6#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
7#[sea_orm(table_name = "space_groups")]
8pub struct Model {
9 #[sea_orm(primary_key, auto_increment = false)]
10 pub id: Uuid,
11 #[sea_orm(unique_key = "space_name")]
12 pub space_id: Uuid,
13 #[sea_orm(unique_key = "space_name")]
14 pub name: String,
15 pub position: i64,
16 pub color: String,
17 pub unicode_emoji: Option<String>,
18 pub icon_hash: Option<String>,
19 pub version: i32,
20 pub created_at: OffsetDateTime,
21 pub updated_at: Option<OffsetDateTime>,
22 pub created_by: Uuid,
23 #[sea_orm(belongs_to, from = "space_id", to = "id")]
24 pub space: BelongsTo<super::spaces::Entity>,
25 #[sea_orm(belongs_to, from = "created_by", to = "uid")]
26 pub creator: BelongsTo<super::super::users::user::Entity>,
27}
28
29impl ActiveModelBehavior for ActiveModel {}
Added lib/models/src/space/space_invite.rs View lib/models/src/space/space_invite.rs
@@ -0,0 +1,26 @@
1use sea_orm::entity::prelude::*;
2use time::OffsetDateTime;
3use uuid::Uuid;
4
5#[sea_orm::model]
6#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
7#[sea_orm(table_name = "space_invite")]
8pub struct Model {
9 #[sea_orm(primary_key, auto_increment = false)]
10 pub id: Uuid,
11 pub space_id: Uuid,
12 pub user_id: Option<Uuid>,
13 pub email: String,
14 // 授予的角色组见 space_invite_groups 关联表
15 pub accept_code: String,
16 pub created_at: OffsetDateTime,
17 pub expires_at: Option<OffsetDateTime>,
18 pub accepted_at: Option<OffsetDateTime>,
19 pub revoked_at: Option<OffsetDateTime>,
20 #[sea_orm(belongs_to, from = "space_id", to = "id")]
21 pub space: BelongsTo<super::spaces::Entity>,
22 #[sea_orm(belongs_to, from = "user_id", to = "uid")]
23 pub user: BelongsTo<Option<super::super::users::user::Entity>>,
24}
25
26impl ActiveModelBehavior for ActiveModel {}
Added lib/models/src/space/space_invite_groups.rs View lib/models/src/space/space_invite_groups.rs
@@ -0,0 +1,18 @@
1use sea_orm::entity::prelude::*;
2use uuid::Uuid;
3
4#[sea_orm::model]
5#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
6#[sea_orm(table_name = "space_invite_groups")]
7pub struct Model {
8 #[sea_orm(primary_key, auto_increment = false)]
9 pub invite_id: Uuid,
10 #[sea_orm(primary_key, auto_increment = false)]
11 pub group_id: Uuid,
12 #[sea_orm(belongs_to, from = "invite_id", to = "id")]
13 pub invite: BelongsTo<super::space_invite::Entity>,
14 #[sea_orm(belongs_to, from = "group_id", to = "id")]
15 pub group: BelongsTo<super::space_groups::Entity>,
16}
17
18impl ActiveModelBehavior for ActiveModel {}
Added lib/models/src/space/space_member_groups.rs View lib/models/src/space/space_member_groups.rs
@@ -0,0 +1,24 @@
1use sea_orm::entity::prelude::*;
2use time::OffsetDateTime;
3use uuid::Uuid;
4
5#[sea_orm::model]
6#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
7#[sea_orm(table_name = "space_member_groups")]
8pub struct Model {
9 #[sea_orm(primary_key, auto_increment = false)]
10 pub space_id: Uuid,
11 #[sea_orm(primary_key, auto_increment = false)]
12 pub user_id: Uuid,
13 #[sea_orm(primary_key, auto_increment = false)]
14 pub group_id: Uuid,
15 pub created_at: OffsetDateTime,
16 #[sea_orm(belongs_to, from = "space_id", to = "id")]
17 pub space: BelongsTo<super::spaces::Entity>,
18 #[sea_orm(belongs_to, from = "user_id", to = "uid")]
19 pub user: BelongsTo<super::super::users::user::Entity>,
20 #[sea_orm(belongs_to, from = "group_id", to = "id")]
21 pub group: BelongsTo<super::space_groups::Entity>,
22}
23
24impl ActiveModelBehavior for ActiveModel {}
Added lib/models/src/space/space_members.rs View lib/models/src/space/space_members.rs
@@ -0,0 +1,75 @@
1use sea_orm::entity::prelude::*;
2use serde::{Deserialize, Serialize};
3use time::OffsetDateTime;
4use uuid::Uuid;
5
6#[derive(
7 Debug, Clone, Copy, PartialEq, Eq, Default, EnumIter, DeriveActiveEnum, Serialize, Deserialize,
8)]
9#[sea_orm(rs_type = "i16", db_type = "SmallInteger")]
10pub enum MemberStatus {
11 #[default]
12 #[sea_orm(num_value = 0)]
13 Active,
14 #[sea_orm(num_value = 1)]
15 Inactive,
16 #[sea_orm(num_value = 2)]
17 Left,
18 #[sea_orm(num_value = 3)]
19 Removed,
20}
21
22#[derive(
23 Debug, Clone, Copy, PartialEq, Eq, Default, EnumIter, DeriveActiveEnum, Serialize, Deserialize,
24)]
25#[sea_orm(rs_type = "i16", db_type = "SmallInteger")]
26pub enum MessageNotifications {
27 #[default]
28 #[sea_orm(num_value = 0)]
29 All,
30 #[sea_orm(num_value = 1)]
31 Mentions,
32 #[sea_orm(num_value = 2)]
33 Nothing,
34}
35
36#[sea_orm::model]
37#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
38#[sea_orm(table_name = "space_members")]
39pub struct Model {
40 #[sea_orm(primary_key, auto_increment = false)]
41 pub space_id: Uuid,
42 #[sea_orm(primary_key, auto_increment = false)]
43 pub user_id: Uuid,
44 pub alias_name: Option<String>,
45 pub alias_avatar_url: Option<String>,
46 pub status: MemberStatus,
47 pub last_active_at: Option<OffsetDateTime>,
48 pub left_at: Option<OffsetDateTime>,
49 pub invited_at: Option<OffsetDateTime>,
50 pub invited_by: Option<Uuid>,
51 pub joined_at: OffsetDateTime,
52 pub created_at: OffsetDateTime,
53 pub updated_at: Option<OffsetDateTime>,
54 #[sea_orm(default_value = 0)]
55 pub message_notifications: MessageNotifications,
56 #[sea_orm(default_value = "false")]
57 pub muted: bool,
58 pub mute_until: Option<OffsetDateTime>,
59 #[sea_orm(default_value = "false")]
60 pub mobile_push: bool,
61 #[sea_orm(default_value = "false")]
62 pub suppress_everyone: bool,
63 #[sea_orm(default_value = "false")]
64 pub suppress_roles: bool,
65 #[sea_orm(default_value = "false")]
66 pub hide_muted_channels: bool,
67 #[sea_orm(belongs_to, from = "space_id", to = "id")]
68 pub space: BelongsTo<super::spaces::Entity>,
69 #[sea_orm(belongs_to, relation_enum = "Member", from = "user_id", to = "uid")]
70 pub user: BelongsTo<super::super::users::user::Entity>,
71 #[sea_orm(belongs_to, relation_enum = "Inviter", from = "invited_by", to = "uid")]
72 pub inviter: BelongsTo<Option<super::super::users::user::Entity>>,
73}
74
75impl ActiveModelBehavior for ActiveModel {}
Added lib/models/src/space/space_warn.rs View lib/models/src/space/space_warn.rs
@@ -0,0 +1,23 @@
1use sea_orm::entity::prelude::*;
2use time::OffsetDateTime;
3use uuid::Uuid;
4
5#[sea_orm::model]
6#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
7#[sea_orm(table_name = "space_warn")]
8pub struct Model {
9 #[sea_orm(primary_key, auto_increment = false)]
10 pub id: Uuid,
11 pub space_id: Uuid,
12 pub user_id: Option<Uuid>,
13 pub reason: Option<String>,
14 // 指向聊天域的 channel,实体尚未迁入 models crate,暂保持裸列
15 pub channel_id: Option<Uuid>,
16 pub created_at: OffsetDateTime,
17 #[sea_orm(belongs_to, from = "space_id", to = "id")]
18 pub space: BelongsTo<super::spaces::Entity>,
19 #[sea_orm(belongs_to, from = "user_id", to = "uid")]
20 pub user: BelongsTo<Option<super::super::users::user::Entity>>,
21}
22
23impl ActiveModelBehavior for ActiveModel {}
Added lib/models/src/space/spaces.rs View lib/models/src/space/spaces.rs
@@ -0,0 +1,61 @@
1use sea_orm::entity::prelude::*;
2use serde::{Deserialize, Serialize};
3use time::OffsetDateTime;
4use uuid::Uuid;
5
6#[derive(
7 Debug, Clone, Copy, PartialEq, Eq, Default, EnumIter, DeriveActiveEnum, Serialize, Deserialize,
8)]
9#[sea_orm(rs_type = "i16", db_type = "SmallInteger")]
10pub enum AllowJoinLevel {
11 #[default]
12 #[sea_orm(num_value = 0)]
13 None,
14 #[sea_orm(num_value = 1)]
15 NeedMailActive,
16 #[sea_orm(num_value = 2)]
17 NeedMailActiveAndLastOneMid,
18 #[sea_orm(num_value = 3)]
19 NeedMailActiveAndLastOneHours,
20 #[sea_orm(num_value = 4)]
21 NeedMailActiveAndPhoneActive,
22}
23
24#[sea_orm::model]
25#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
26#[sea_orm(table_name = "spaces")]
27pub struct Model {
28 #[sea_orm(primary_key, auto_increment = false)]
29 pub id: Uuid,
30 #[sea_orm(unique)]
31 pub lower_name: String,
32 pub name: String,
33 pub description: Option<String>,
34 pub owner_id: Uuid,
35 pub avatar_url: Option<String>,
36 #[sea_orm(default_value = "false")]
37 pub is_private: bool,
38 pub is_active: bool,
39 pub allow_join: bool,
40 pub allow_invite: bool,
41 pub invite_sort_url_hash: Option<String>,
42 pub nums_members: i64,
43 pub banner_url: Option<String>,
44 pub banner_width: Option<i32>,
45 pub banner_height: Option<i32>,
46 pub invite_banner_url: Option<String>,
47 pub invite_banner_width: Option<i32>,
48 pub invite_banner_height: Option<i32>,
49 pub embed_splash_url: Option<String>,
50 pub embed_splash_width: Option<i32>,
51 pub embed_splash_height: Option<i32>,
52 pub welcome_message: Option<String>,
53 pub allow_join_level: AllowJoinLevel,
54 pub member_must_enable_2fa: bool,
55 pub created_at: OffsetDateTime,
56 pub updated_at: OffsetDateTime,
57 #[sea_orm(belongs_to, from = "owner_id", to = "uid")]
58 pub owner: BelongsTo<super::super::users::user::Entity>,
59}
60
61impl ActiveModelBehavior for ActiveModel {}
Added lib/models/src/system/mod.rs View lib/models/src/system/mod.rs
@@ -0,0 +1,8 @@
1pub mod system_announcements;
2pub mod system_permissions;
3pub mod system_rate_limits;
4pub mod system_roles;
5pub mod system_security_keys;
6pub mod system_settings;
7pub mod system_signing_keys;
8pub mod system_tasks;
Added lib/models/src/system/system_announcements.rs View lib/models/src/system/system_announcements.rs
@@ -0,0 +1,21 @@
1use sea_orm::entity::prelude::*;
2use time::OffsetDateTime;
3use uuid::Uuid;
4
5#[sea_orm::model]
6#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
7#[sea_orm(table_name = "system_announcements")]
8pub struct Model {
9 #[sea_orm(primary_key, auto_increment = false)]
10 pub id: Uuid,
11 pub title: String,
12 pub body: String,
13 pub author_id: Uuid,
14 pub is_active: bool,
15 pub created_at: OffsetDateTime,
16 pub updated_at: OffsetDateTime,
17 #[sea_orm(belongs_to, from = "author_id", to = "uid")]
18 pub author: BelongsTo<super::super::users::user::Entity>,
19}
20
21impl ActiveModelBehavior for ActiveModel {}
Added lib/models/src/system/system_permissions.rs View lib/models/src/system/system_permissions.rs
@@ -0,0 +1,18 @@
1use sea_orm::entity::prelude::*;
2use time::OffsetDateTime;
3use uuid::Uuid;
4
5#[sea_orm::model]
6#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
7#[sea_orm(table_name = "system_permissions")]
8pub struct Model {
9 #[sea_orm(primary_key, auto_increment = false)]
10 pub role_id: Uuid,
11 #[sea_orm(primary_key, auto_increment = false)]
12 pub permission: String,
13 pub created_at: OffsetDateTime,
14 #[sea_orm(belongs_to, from = "role_id", to = "id")]
15 pub role: BelongsTo<super::system_roles::Entity>,
16}
17
18impl ActiveModelBehavior for ActiveModel {}
Added lib/models/src/system/system_rate_limits.rs View lib/models/src/system/system_rate_limits.rs
@@ -0,0 +1,19 @@
1use sea_orm::entity::prelude::*;
2use time::OffsetDateTime;
3use uuid::Uuid;
4
5#[sea_orm::model]
6#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
7#[sea_orm(table_name = "system_rate_limits")]
8pub struct Model {
9 #[sea_orm(primary_key, auto_increment = false)]
10 pub id: Uuid,
11 #[sea_orm(unique_key = "key_window")]
12 pub key: String,
13 #[sea_orm(unique_key = "key_window")]
14 pub window_start: OffsetDateTime,
15 pub count: i64,
16 pub created_at: OffsetDateTime,
17}
18
19impl ActiveModelBehavior for ActiveModel {}
Added lib/models/src/system/system_roles.rs View lib/models/src/system/system_roles.rs
@@ -0,0 +1,18 @@
1use sea_orm::entity::prelude::*;
2use time::OffsetDateTime;
3use uuid::Uuid;
4
5#[sea_orm::model]
6#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
7#[sea_orm(table_name = "system_roles")]
8pub struct Model {
9 #[sea_orm(primary_key, auto_increment = false)]
10 pub id: Uuid,
11 pub name: String,
12 pub color: String,
13 pub position: i64,
14 pub created_at: OffsetDateTime,
15 pub updated_at: OffsetDateTime,
16}
17
18impl ActiveModelBehavior for ActiveModel {}
Added lib/models/src/system/system_security_keys.rs View lib/models/src/system/system_security_keys.rs
@@ -0,0 +1,19 @@
1use sea_orm::entity::prelude::*;
2use time::OffsetDateTime;
3use uuid::Uuid;
4
5#[sea_orm::model]
6#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
7#[sea_orm(table_name = "system_security_keys")]
8pub struct Model {
9 #[sea_orm(primary_key, auto_increment = false)]
10 pub id: Uuid,
11 pub name: String,
12 #[sea_orm(unique)]
13 pub key_hash: String,
14 pub is_active: bool,
15 pub created_at: OffsetDateTime,
16 pub updated_at: OffsetDateTime,
17}
18
19impl ActiveModelBehavior for ActiveModel {}
Added lib/models/src/system/system_settings.rs View lib/models/src/system/system_settings.rs
@@ -0,0 +1,14 @@
1use sea_orm::entity::prelude::*;
2use time::OffsetDateTime;
3
4#[sea_orm::model]
5#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
6#[sea_orm(table_name = "system_settings")]
7pub struct Model {
8 #[sea_orm(primary_key, auto_increment = false)]
9 pub key: String,
10 pub value: String,
11 pub updated_at: OffsetDateTime,
12}
13
14impl ActiveModelBehavior for ActiveModel {}
Added lib/models/src/system/system_signing_keys.rs View lib/models/src/system/system_signing_keys.rs
@@ -0,0 +1,53 @@
1use sea_orm::entity::prelude::*;
2use serde::{Deserialize, Serialize};
3use time::OffsetDateTime;
4use uuid::Uuid;
5
6#[derive(
7 Debug, Clone, Copy, PartialEq, Eq, Default, EnumIter, DeriveActiveEnum, Serialize, Deserialize,
8)]
9#[sea_orm(rs_type = "i16", db_type = "SmallInteger")]
10pub enum SigningAlgorithm {
11 #[default]
12 #[sea_orm(num_value = 0)]
13 Ed25519,
14 #[sea_orm(num_value = 1)]
15 ES256,
16 #[sea_orm(num_value = 2)]
17 RS256,
18}
19
20#[sea_orm::model]
21#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
22#[sea_orm(table_name = "system_signing_keys")]
23pub struct Model {
24 #[sea_orm(primary_key, auto_increment = false)]
25 pub kid: Uuid,
26 pub name: String,
27 pub algorithm: SigningAlgorithm,
28 pub public_key: Vec<u8>,
29 pub private_key_encrypted: Option<Vec<u8>>,
30 #[sea_orm(unique)]
31 pub key_hash: String,
32 #[sea_orm(default_value = "false")]
33 pub is_active: bool,
34 #[sea_orm(default_value = "false")]
35 pub is_signing: bool,
36 #[sea_orm(default_value = "false")]
37 pub is_rotating: bool,
38 pub grace_started_at: Option<OffsetDateTime>,
39 pub grace_ends_at: Option<OffsetDateTime>,
40 pub expires_at: Option<OffsetDateTime>,
41 pub created_at: OffsetDateTime,
42 pub created_by: Option<Uuid>,
43 pub rotated_by: Option<Uuid>,
44 pub revoked_at: Option<OffsetDateTime>,
45 pub revoked_reason: Option<String>,
46 pub updated_at: OffsetDateTime,
47 #[sea_orm(belongs_to, relation_enum = "Creator", from = "created_by", to = "uid")]
48 pub creator: BelongsTo<Option<super::super::users::user::Entity>>,
49 #[sea_orm(belongs_to, relation_enum = "Rotator", from = "rotated_by", to = "uid")]
50 pub rotator: BelongsTo<Option<super::super::users::user::Entity>>,
51}
52
53impl ActiveModelBehavior for ActiveModel {}
Added lib/models/src/system/system_tasks.rs View lib/models/src/system/system_tasks.rs
@@ -0,0 +1,42 @@
1use sea_orm::JsonValue;
2use sea_orm::entity::prelude::*;
3use serde::{Deserialize, Serialize};
4use time::OffsetDateTime;
5use uuid::Uuid;
6
7#[derive(
8 Debug, Clone, Copy, PartialEq, Eq, Default, EnumIter, DeriveActiveEnum, Serialize, Deserialize,
9)]
10#[sea_orm(rs_type = "i16", db_type = "SmallInteger")]
11pub enum TaskStatus {
12 #[default]
13 #[sea_orm(num_value = 0)]
14 Pending,
15 #[sea_orm(num_value = 1)]
16 Running,
17 #[sea_orm(num_value = 2)]
18 Success,
19 #[sea_orm(num_value = 3)]
20 Failed,
21}
22
23#[sea_orm::model]
24#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
25#[sea_orm(table_name = "system_tasks")]
26pub struct Model {
27 #[sea_orm(primary_key, auto_increment = false)]
28 pub id: Uuid,
29 pub task_type: String,
30 pub payload: JsonValue,
31 pub status: TaskStatus,
32 pub error: Option<String>,
33 #[sea_orm(default_value = 0)]
34 pub retry_count: i32,
35 pub created_at: OffsetDateTime,
36 pub started_at: Option<OffsetDateTime>,
37 pub finished_at: Option<OffsetDateTime>,
38 pub next_run_at: Option<OffsetDateTime>,
39 pub updated_at: Option<OffsetDateTime>,
40}
41
42impl ActiveModelBehavior for ActiveModel {}
Added lib/models/src/users/mod.rs View lib/models/src/users/mod.rs
@@ -0,0 +1,25 @@
1pub mod user;
2pub mod user_access_keys;
3pub mod user_agreement_acceptances;
4pub mod user_blocks;
5pub mod user_deletion_requests;
6pub mod user_devices;
7pub mod user_email_change_tickets;
8pub mod user_email_verification_codes;
9pub mod user_emails;
10pub mod user_favorite_memes;
11pub mod user_followers;
12pub mod user_notifications;
13pub mod user_preferences;
14pub mod user_profiles;
15pub mod user_push_subscriptions;
16pub mod user_recent_mentions;
17pub mod user_recovery_codes;
18pub mod user_reports;
19pub mod user_saved_messages;
20pub mod user_scheduled_messages;
21pub mod user_sessions;
22pub mod user_ssh_keys;
23pub mod user_suspensions;
24
25pub use user::*;
Added lib/models/src/users/user.rs View lib/models/src/users/user.rs
@@ -0,0 +1,125 @@
1use sea_orm::entity::prelude::*;
2use sea_orm::{EntityTrait, QueryFilter, Select};
3use serde::{Deserialize, Serialize};
4use time::OffsetDateTime;
5use uuid::Uuid;
6
7#[derive(
8 Debug, Clone, Copy, PartialEq, Eq, Default, EnumIter, DeriveActiveEnum, Serialize, Deserialize,
9)]
10#[sea_orm(rs_type = "i16", db_type = "SmallInteger")]
11pub enum ProfileVisibility {
12 #[default]
13 #[sea_orm(num_value = 0)]
14 Public,
15 #[sea_orm(num_value = 1)]
16 RegisteredOnly,
17 #[sea_orm(num_value = 2)]
18 MembersOnly,
19 #[sea_orm(num_value = 3)]
20 Private,
21}
22
23#[derive(
24 Debug, Clone, Copy, PartialEq, Eq, Default, EnumIter, DeriveActiveEnum, Serialize, Deserialize,
25)]
26#[sea_orm(rs_type = "i16", db_type = "SmallInteger")]
27pub enum Audience {
28 #[default]
29 #[sea_orm(num_value = 0)]
30 Everyone,
31 #[sea_orm(num_value = 1)]
32 FollowingOnly,
33 #[sea_orm(num_value = 2)]
34 Nobody,
35}
36
37#[sea_orm::model]
38#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
39#[sea_orm(table_name = "users")]
40pub struct Model {
41 #[sea_orm(primary_key, auto_increment = false)]
42 pub uid: Uuid,
43 pub username: String,
44 #[sea_orm(unique)]
45 pub lower_name: String,
46 pub display_name: String,
47 pub avatar_url: String,
48 pub is_active: bool,
49 pub is_restricted: bool,
50 pub prohibit_login: bool,
51 pub visibility: ProfileVisibility,
52 #[sea_orm(default_value = "false")]
53 pub bot: bool,
54 #[sea_orm(default_value = "false")]
55 pub system: bool,
56 pub last_login_at: Option<OffsetDateTime>,
57 pub last_login_ip_hash: Option<String>,
58 pub version: i32,
59
60 pub passwd_hash: Option<String>,
61 pub password_last_changed_at: Option<OffsetDateTime>,
62 pub passwd_version: Option<i64>,
63 pub totp_encrypted_secret: Option<String>,
64 #[sea_orm(default_value = "false")]
65 pub totp_enabled: bool,
66 pub totp_last_used_at: Option<OffsetDateTime>,
67 pub totp_last_counter: Option<i64>,
68
69 pub timezone: Option<String>,
70 pub locale: Option<String>,
71 pub language: Option<String>,
72
73 pub created_at: OffsetDateTime,
74 pub updated_at: OffsetDateTime,
75 #[sea_orm(has_many)]
76 pub sessions: HasMany<super::user_sessions::Entity>,
77 #[sea_orm(has_many)]
78 pub emails: HasMany<super::user_emails::Entity>,
79 #[sea_orm(has_many)]
80 pub devices: HasMany<super::user_devices::Entity>,
81 #[sea_orm(has_many, relation_enum = "User")]
82 pub notifications: HasMany<super::user_notifications::Entity>,
83 #[sea_orm(has_many)]
84 pub access_keys: HasMany<super::user_access_keys::Entity>,
85 #[sea_orm(has_many)]
86 pub ssh_keys: HasMany<super::user_ssh_keys::Entity>,
87 #[sea_orm(has_many)]
88 pub deletion_requests: HasMany<super::user_deletion_requests::Entity>,
89 #[sea_orm(has_many)]
90 pub email_verification_codes: HasMany<super::user_email_verification_codes::Entity>,
91 #[sea_orm(has_many)]
92 pub recovery_codes: HasMany<super::user_recovery_codes::Entity>,
93 #[sea_orm(has_one)]
94 pub profile: HasOne<super::user_profiles::Entity>,
95 #[sea_orm(has_one)]
96 pub preferences: HasOne<super::user_preferences::Entity>,
97 #[sea_orm(
98 self_ref,
99 has_many,
100 via = "user_followers",
101 from = "Followed",
102 to = "Follower"
103 )]
104 pub followers: HasMany<Entity>,
105 #[sea_orm(self_ref, has_many, via = "user_followers", reverse)]
106 pub following: HasMany<Entity>,
107 #[sea_orm(
108 self_ref,
109 has_many,
110 via = "user_blocks",
111 from = "Blocked",
112 to = "Blocker"
113 )]
114 pub blocked_by: HasMany<Entity>,
115 #[sea_orm(self_ref, has_many, via = "user_blocks", reverse)]
116 pub blocking: HasMany<Entity>,
117}
118
119impl Entity {
120 pub fn active() -> Select<Self> {
121 Self::find().filter(COLUMN.is_active.eq(true))
122 }
123}
124
125impl ActiveModelBehavior for ActiveModel {}
Added lib/models/src/users/user_access_keys.rs View lib/models/src/users/user_access_keys.rs
@@ -0,0 +1,51 @@
1use sea_orm::entity::prelude::*;
2use serde::{Deserialize, Serialize};
3use time::OffsetDateTime;
4use uuid::Uuid;
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq, EnumIter, DeriveActiveEnum, Serialize, Deserialize)]
7#[serde(rename_all = "lowercase")]
8#[sea_orm(rs_type = "String", db_type = "String(StringLen::N(8))")]
9pub enum AccessKeyScope {
10 #[sea_orm(string_value = "read")]
11 Read,
12 #[sea_orm(string_value = "write")]
13 Write,
14 #[sea_orm(string_value = "repo")]
15 Repo,
16 #[sea_orm(string_value = "all")]
17 All,
18}
19
20impl AccessKeyScope {
21 pub const fn allows(self, want_write: bool) -> bool {
22 matches!(
23 (self, want_write),
24 (Self::All | Self::Repo | Self::Write, _) | (Self::Read, false)
25 )
26 }
27}
28
29#[sea_orm::model]
30#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
31#[sea_orm(table_name = "user_access_key")]
32pub struct Model {
33 #[sea_orm(primary_key, auto_increment = false)]
34 pub id: Uuid,
35 #[sea_orm(unique_key = "user_name")]
36 pub user_id: Uuid,
37 #[sea_orm(belongs_to, from = "user_id", to = "uid")]
38 pub user: BelongsTo<super::user::Entity>,
39 #[sea_orm(unique_key = "user_name")]
40 pub name: String,
41 #[sea_orm(unique)]
42 pub token_hash: String,
43 pub token_salt: String,
44 pub token_last_eight: String,
45 pub scope: AccessKeyScope,
46 pub version: i32,
47 pub created_at: OffsetDateTime,
48 pub updated_at: OffsetDateTime,
49}
50
51impl ActiveModelBehavior for ActiveModel {}
Added lib/models/src/users/user_agreement_acceptances.rs View lib/models/src/users/user_agreement_acceptances.rs
@@ -0,0 +1,22 @@
1use sea_orm::entity::prelude::*;
2use time::OffsetDateTime;
3use uuid::Uuid;
4
5#[sea_orm::model]
6#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
7#[sea_orm(table_name = "user_agreement_acceptances")]
8pub struct Model {
9 #[sea_orm(primary_key, auto_increment = false)]
10 pub id: Uuid,
11 #[sea_orm(unique_key = "agreement_acceptance")]
12 pub user_id: Uuid,
13 #[sea_orm(belongs_to, from = "user_id", to = "uid")]
14 pub user: BelongsTo<super::user::Entity>,
15 #[sea_orm(unique_key = "agreement_acceptance")]
16 pub agreement_key: String,
17 #[sea_orm(unique_key = "agreement_acceptance")]
18 pub agreement_version: String,
19 pub accepted_at: OffsetDateTime,
20}
21
22impl ActiveModelBehavior for ActiveModel {}
Added lib/models/src/users/user_blocks.rs View lib/models/src/users/user_blocks.rs
@@ -0,0 +1,22 @@
1use sea_orm::entity::prelude::*;
2use time::OffsetDateTime;
3use uuid::Uuid;
4
5#[sea_orm::model]
6#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
7#[sea_orm(table_name = "user_blocks")]
8pub struct Model {
9 #[sea_orm(primary_key, auto_increment = false)]
10 pub id: Uuid,
11 #[sea_orm(unique_key = "block_pair")]
12 pub blocker_id: Uuid,
13 #[sea_orm(belongs_to, from = "blocker_id", to = "uid", relation_enum = "Blocker")]
14 pub blocker: BelongsTo<super::user::Entity>,
15 #[sea_orm(unique_key = "block_pair")]
16 pub blocked_id: Uuid,
17 #[sea_orm(belongs_to, from = "blocked_id", to = "uid", relation_enum = "Blocked")]
18 pub blocked: BelongsTo<super::user::Entity>,
19 pub created_at: OffsetDateTime,
20}
21
22impl ActiveModelBehavior for ActiveModel {}
Added lib/models/src/users/user_deletion_requests.rs View lib/models/src/users/user_deletion_requests.rs
@@ -0,0 +1,47 @@
1use sea_orm::entity::prelude::*;
2use time::OffsetDateTime;
3use uuid::Uuid;
4
5#[derive(
6 Debug,
7 Clone,
8 Copy,
9 PartialEq,
10 Eq,
11 EnumIter,
12 DeriveActiveEnum,
13 serde::Serialize,
14 serde::Deserialize,
15)]
16#[serde(rename_all = "snake_case")]
17#[sea_orm(rs_type = "String", db_type = "String(StringLen::N(16))")]
18pub enum UserDeletionRequestStatus {
19 #[sea_orm(string_value = "pending")]
20 Pending,
21 #[sea_orm(string_value = "scheduled")]
22 Scheduled,
23 #[sea_orm(string_value = "completed")]
24 Completed,
25 #[sea_orm(string_value = "cancelled")]
26 Cancelled,
27}
28
29#[sea_orm::model]
30#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
31#[sea_orm(table_name = "user_deletion_requests")]
32pub struct Model {
33 #[sea_orm(primary_key, auto_increment = false)]
34 pub id: Uuid,
35 pub user_id: Uuid,
36 #[sea_orm(belongs_to, from = "user_id", to = "uid")]
37 pub user: BelongsTo<super::user::Entity>,
38 pub reason: Option<String>,
39 pub status: UserDeletionRequestStatus,
40 pub scheduled_for: Option<OffsetDateTime>,
41 pub completed_at: Option<OffsetDateTime>,
42 pub cancelled_at: Option<OffsetDateTime>,
43 pub created_at: OffsetDateTime,
44 pub updated_at: OffsetDateTime,
45}
46
47impl ActiveModelBehavior for ActiveModel {}
Added lib/models/src/users/user_devices.rs View lib/models/src/users/user_devices.rs
@@ -0,0 +1,31 @@
1use sea_orm::entity::prelude::*;
2use time::OffsetDateTime;
3use uuid::Uuid;
4
5#[sea_orm::model]
6#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
7#[sea_orm(table_name = "user_devices")]
8pub struct Model {
9 #[sea_orm(primary_key, auto_increment = false)]
10 pub id: Uuid,
11 #[sea_orm(unique_key = "user_device")]
12 pub user_id: Uuid,
13 #[sea_orm(belongs_to, from = "user_id", to = "uid")]
14 pub user: BelongsTo<super::user::Entity>,
15 pub device_name: Option<String>,
16 pub device_type: Option<String>,
17 #[sea_orm(unique_key = "user_device")]
18 pub device_hash: String,
19 // 与 user_session.user_agent_hash 一致,UA 属于指纹数据,禁止明文落库
20 pub user_agent_hash: Option<String>,
21 pub os: Option<String>,
22
23 pub last_ip_hash: Option<String>,
24 pub last_seen_at: OffsetDateTime,
25 pub first_seen_at: OffsetDateTime,
26 pub is_trusted: bool,
27 pub created_at: OffsetDateTime,
28 pub updated_at: OffsetDateTime,
29}
30
31impl ActiveModelBehavior for ActiveModel {}
Added lib/models/src/users/user_email_change_tickets.rs View lib/models/src/users/user_email_change_tickets.rs
@@ -0,0 +1,41 @@
1use sea_orm::entity::prelude::*;
2use serde::{Deserialize, Serialize};
3use time::OffsetDateTime;
4use uuid::Uuid;
5
6#[derive(
7 Debug, Clone, Copy, PartialEq, Eq, Default, EnumIter, DeriveActiveEnum, Serialize, Deserialize,
8)]
9#[sea_orm(rs_type = "String", db_type = "String(StringLen::N(16))")]
10pub enum EmailChangeTicketStatus {
11 #[default]
12 #[sea_orm(string_value = "pending")]
13 Pending,
14 #[sea_orm(string_value = "verified")]
15 Verified,
16 #[sea_orm(string_value = "completed")]
17 Completed,
18 #[sea_orm(string_value = "cancelled")]
19 Cancelled,
20}
21
22#[sea_orm::model]
23#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
24#[sea_orm(table_name = "user_email_change_ticket")]
25pub struct Model {
26 #[sea_orm(primary_key, auto_increment = false)]
27 pub id: Uuid,
28 pub user_id: Uuid,
29 #[sea_orm(unique)]
30 pub token_hash: String,
31 pub new_email: String,
32 pub original_email: Option<String>,
33 pub status: EmailChangeTicketStatus,
34 pub created_at: OffsetDateTime,
35 pub expires_at: OffsetDateTime,
36 pub used_at: Option<OffsetDateTime>,
37 #[sea_orm(belongs_to, from = "user_id", to = "uid")]
38 pub user: BelongsTo<super::user::Entity>,
39}
40
41impl ActiveModelBehavior for ActiveModel {}
Added lib/models/src/users/user_email_verification_codes.rs View lib/models/src/users/user_email_verification_codes.rs
@@ -0,0 +1,45 @@
1use sea_orm::entity::prelude::*;
2use time::OffsetDateTime;
3use uuid::Uuid;
4
5#[derive(
6 Debug,
7 Clone,
8 Copy,
9 PartialEq,
10 Eq,
11 EnumIter,
12 DeriveActiveEnum,
13 serde::Serialize,
14 serde::Deserialize,
15)]
16#[serde(rename_all = "snake_case")]
17#[sea_orm(rs_type = "String", db_type = "String(StringLen::N(16))")]
18pub enum EmailVerificationPurpose {
19 #[sea_orm(string_value = "signup")]
20 Signup,
21 #[sea_orm(string_value = "password_reset")]
22 PasswordReset,
23}
24
25#[sea_orm::model]
26#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
27#[sea_orm(table_name = "user_email_verification_codes")]
28pub struct Model {
29 #[sea_orm(primary_key, auto_increment = false)]
30 pub id: Uuid,
31 pub user_id: Uuid,
32 #[sea_orm(belongs_to, from = "user_id", to = "uid")]
33 pub user: BelongsTo<super::user::Entity>,
34 pub email: String,
35
36 pub purpose: EmailVerificationPurpose,
37
38 #[sea_orm(unique)]
39 pub token_hash: String,
40 pub created_at: OffsetDateTime,
41 pub expires_at: OffsetDateTime,
42 pub used_at: Option<OffsetDateTime>,
43}
44
45impl ActiveModelBehavior for ActiveModel {}
Added lib/models/src/users/user_emails.rs View lib/models/src/users/user_emails.rs
@@ -0,0 +1,25 @@
1use sea_orm::entity::prelude::*;
2use time::OffsetDateTime;
3use uuid::Uuid;
4
5#[sea_orm::model]
6#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
7#[sea_orm(table_name = "user_email")]
8pub struct Model {
9 #[sea_orm(primary_key, auto_increment = false)]
10 pub id: Uuid,
11 pub user_id: Uuid,
12 #[sea_orm(belongs_to, from = "user_id", to = "uid")]
13 pub user: BelongsTo<super::user::Entity>,
14 pub email: String,
15 #[sea_orm(unique_key = "lower_email")]
16 pub lower_email: String,
17 pub is_activated: bool,
18 pub is_primary: bool,
19 pub created_at: OffsetDateTime,
20 pub updated_at: OffsetDateTime,
21 pub activated_at: Option<OffsetDateTime>,
22 pub deleted_at: Option<OffsetDateTime>,
23}
24
25impl ActiveModelBehavior for ActiveModel {}
Added lib/models/src/users/user_favorite_memes.rs View lib/models/src/users/user_favorite_memes.rs
@@ -0,0 +1,25 @@
1use sea_orm::entity::prelude::*;
2use time::OffsetDateTime;
3use uuid::Uuid;
4
5#[sea_orm::model]
6#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
7#[sea_orm(table_name = "user_favorite_meme")]
8pub struct Model {
9 #[sea_orm(primary_key, auto_increment = false)]
10 pub id: Uuid,
11 #[sea_orm(unique_key = "user_name")]
12 pub user_id: Uuid,
13 #[sea_orm(unique_key = "user_name")]
14 pub name: String,
15 pub content_type: String,
16 pub url: String,
17 pub size: i64,
18 pub width: Option<i32>,
19 pub height: Option<i32>,
20 pub created_at: OffsetDateTime,
21 #[sea_orm(belongs_to, from = "user_id", to = "uid")]
22 pub user: BelongsTo<super::user::Entity>,
23}
24
25impl ActiveModelBehavior for ActiveModel {}
Added lib/models/src/users/user_followers.rs View lib/models/src/users/user_followers.rs
@@ -0,0 +1,32 @@
1use sea_orm::entity::prelude::*;
2use time::OffsetDateTime;
3use uuid::Uuid;
4
5#[sea_orm::model]
6#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
7#[sea_orm(table_name = "user_followers")]
8pub struct Model {
9 #[sea_orm(primary_key, auto_increment = false)]
10 pub id: Uuid,
11 #[sea_orm(unique_key = "follow_pair")]
12 pub follower_id: Uuid,
13 #[sea_orm(
14 belongs_to,
15 from = "follower_id",
16 to = "uid",
17 relation_enum = "Follower"
18 )]
19 pub follower: BelongsTo<super::user::Entity>,
20 #[sea_orm(unique_key = "follow_pair")]
21 pub followed_id: Uuid,
22 #[sea_orm(
23 belongs_to,
24 from = "followed_id",
25 to = "uid",
26 relation_enum = "Followed"
27 )]
28 pub followed: BelongsTo<super::user::Entity>,
29 pub created_at: OffsetDateTime,
30}
31
32impl ActiveModelBehavior for ActiveModel {}
Added lib/models/src/users/user_notifications.rs View lib/models/src/users/user_notifications.rs
@@ -0,0 +1,61 @@
1use sea_orm::JsonValue;
2use sea_orm::entity::prelude::*;
3use time::OffsetDateTime;
4use uuid::Uuid;
5
6#[derive(
7 Debug,
8 Clone,
9 Copy,
10 PartialEq,
11 Eq,
12 EnumIter,
13 DeriveActiveEnum,
14 serde::Serialize,
15 serde::Deserialize,
16)]
17#[serde(rename_all = "snake_case")]
18#[sea_orm(rs_type = "String", db_type = "String(StringLen::N(16))")]
19pub enum NotificationType {
20 #[sea_orm(string_value = "mention")]
21 Mention,
22 #[sea_orm(string_value = "reply")]
23 Reply,
24 #[sea_orm(string_value = "follow")]
25 Follow,
26 #[sea_orm(string_value = "invite")]
27 Invite,
28 #[sea_orm(string_value = "assignment")]
29 Assignment,
30 #[sea_orm(string_value = "system")]
31 System,
32}
33
34#[sea_orm::model]
35#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
36#[sea_orm(table_name = "user_notifications")]
37pub struct Model {
38 #[sea_orm(primary_key, auto_increment = false)]
39 pub id: Uuid,
40 pub user_id: Uuid,
41 #[sea_orm(belongs_to, from = "user_id", to = "uid")]
42 pub user: BelongsTo<super::user::Entity>,
43 pub notification_type: NotificationType,
44 pub title: String,
45 pub body: Option<String>,
46 #[sea_orm(column_name = "data")]
47 pub data_json: Option<JsonValue>,
48 pub actor_uid: Option<Uuid>,
49 #[sea_orm(belongs_to, relation_enum = "Actor", from = "actor_uid", to = "uid")]
50 pub actor: BelongsTo<Option<super::user::Entity>>,
51 pub read_at: Option<OffsetDateTime>,
52 pub created_at: OffsetDateTime,
53}
54
55impl ActiveModelBehavior for ActiveModel {}
56
57impl Related<super::user::Entity> for Entity {
58 fn to() -> RelationDef {
59 Relation::User.def()
60 }
61}
Added lib/models/src/users/user_preferences.rs View lib/models/src/users/user_preferences.rs
@@ -0,0 +1,37 @@
1use sea_orm::entity::prelude::*;
2use time::OffsetDateTime;
3use uuid::Uuid;
4
5#[sea_orm::model]
6#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
7#[sea_orm(table_name = "user_preferences")]
8pub struct Model {
9 #[sea_orm(primary_key, auto_increment = false)]
10 pub user_id: Uuid,
11 #[sea_orm(default_value = "system")]
12 pub theme_mode: String,
13 pub theme_token: Option<String>,
14 pub theme_version: Option<i64>,
15 pub diff_view_style: Option<String>,
16 #[sea_orm(default_value = 0)]
17 pub in_app_flags: i32,
18 #[sea_orm(default_value = 0)]
19 pub email_flags: i32,
20 #[sea_orm(default_value = 0)]
21 pub push_flags: i32,
22 #[sea_orm(default_value = "false")]
23 pub email_digest: bool,
24 pub mute_until: Option<OffsetDateTime>,
25 #[sea_orm(default_value = 0)]
26 pub allow_dms_from: super::user::Audience,
27 #[sea_orm(default_value = 0)]
28 pub allow_mentions_from: super::user::Audience,
29 #[sea_orm(default_value = "false")]
30 pub show_online_status: bool,
31 pub created_at: OffsetDateTime,
32 pub updated_at: OffsetDateTime,
33 #[sea_orm(belongs_to, from = "user_id", to = "uid")]
34 pub user: BelongsTo<super::user::Entity>,
35}
36
37impl ActiveModelBehavior for ActiveModel {}
Added lib/models/src/users/user_profiles.rs View lib/models/src/users/user_profiles.rs
@@ -0,0 +1,42 @@
1use sea_orm::entity::prelude::*;
2use time::{Date, OffsetDateTime};
3use uuid::Uuid;
4
5#[sea_orm::model]
6#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
7#[sea_orm(table_name = "user_profile")]
8pub struct Model {
9 #[sea_orm(primary_key, auto_increment = false)]
10 pub user_id: Uuid,
11 pub real_name: Option<String>,
12 pub bio: Option<String>,
13 pub pronouns: Option<String>,
14 pub pronouns_privacy: Option<super::user::ProfileVisibility>,
15 pub website: Option<String>,
16 pub location: Option<String>,
17 pub company: Option<String>,
18 pub date_of_birth: Option<Date>,
19 pub banner_url: Option<String>,
20 pub banner_hash: Option<String>,
21 pub banner_color: Option<i32>,
22 pub accent_color: Option<String>,
23 pub timezone_privacy_flags: Option<i32>,
24 pub custom_status_emoji: Option<String>,
25 pub custom_status_text: Option<String>,
26 pub custom_status_expires_at: Option<OffsetDateTime>,
27 #[sea_orm(default_value = "false")]
28 pub show_email: bool,
29 #[sea_orm(default_value = "false")]
30 pub show_location: bool,
31 #[sea_orm(default_value = "false")]
32 pub show_real_name: bool,
33 #[sea_orm(default_value = "false")]
34 pub show_activity: bool,
35 pub privacy_agreed_at: Option<OffsetDateTime>,
36 pub created_at: OffsetDateTime,
37 pub updated_at: OffsetDateTime,
38 #[sea_orm(belongs_to, from = "user_id", to = "uid")]
39 pub user: BelongsTo<super::user::Entity>,
40}
41
42impl ActiveModelBehavior for ActiveModel {}
Added lib/models/src/users/user_push_subscriptions.rs View lib/models/src/users/user_push_subscriptions.rs
@@ -0,0 +1,38 @@
1use sea_orm::entity::prelude::*;
2use serde::{Deserialize, Serialize};
3use time::OffsetDateTime;
4use uuid::Uuid;
5
6#[derive(
7 Debug, Clone, Copy, PartialEq, Eq, Default, EnumIter, DeriveActiveEnum, Serialize, Deserialize,
8)]
9#[sea_orm(rs_type = "String", db_type = "String(StringLen::N(16))")]
10pub enum PushSubscriptionPlatform {
11 #[default]
12 #[sea_orm(string_value = "web_push")]
13 WebPush,
14 #[sea_orm(string_value = "android_fcm")]
15 AndroidFcm,
16 #[sea_orm(string_value = "ios_apns")]
17 IosApns,
18}
19
20#[sea_orm::model]
21#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
22#[sea_orm(table_name = "user_push_subscription")]
23pub struct Model {
24 #[sea_orm(primary_key, auto_increment = false)]
25 pub id: Uuid,
26 pub user_id: Uuid,
27 pub endpoint: String,
28 pub p256dh_key: String,
29 pub auth_key: String,
30 pub platform: PushSubscriptionPlatform,
31 pub user_agent: Option<String>,
32 pub created_at: OffsetDateTime,
33 pub updated_at: Option<OffsetDateTime>,
34 #[sea_orm(belongs_to, from = "user_id", to = "uid")]
35 pub user: BelongsTo<super::user::Entity>,
36}
37
38impl ActiveModelBehavior for ActiveModel {}
Added lib/models/src/users/user_recent_mentions.rs View lib/models/src/users/user_recent_mentions.rs
@@ -0,0 +1,20 @@
1use sea_orm::entity::prelude::*;
2use time::OffsetDateTime;
3use uuid::Uuid;
4
5#[sea_orm::model]
6#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
7#[sea_orm(table_name = "user_recent_mention")]
8pub struct Model {
9 #[sea_orm(primary_key, auto_increment = false)]
10 pub id: Uuid,
11 pub user_id: Uuid,
12 // 指向聊天域的 channel/message,实体尚未迁入 models crate,暂保持裸列
13 pub channel_id: Uuid,
14 pub message_id: Uuid,
15 pub created_at: OffsetDateTime,
16 #[sea_orm(belongs_to, from = "user_id", to = "uid")]
17 pub user: BelongsTo<super::user::Entity>,
18}
19
20impl ActiveModelBehavior for ActiveModel {}
Added lib/models/src/users/user_recovery_codes.rs View lib/models/src/users/user_recovery_codes.rs
@@ -0,0 +1,21 @@
1use sea_orm::entity::prelude::*;
2use time::OffsetDateTime;
3use uuid::Uuid;
4
5#[sea_orm::model]
6#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
7#[sea_orm(table_name = "user_recovery_codes")]
8pub struct Model {
9 #[sea_orm(primary_key, auto_increment = false)]
10 pub id: Uuid,
11 pub user_id: Uuid,
12 #[sea_orm(belongs_to, from = "user_id", to = "uid")]
13 pub user: BelongsTo<super::user::Entity>,
14 #[sea_orm(unique)]
15 pub code_hash: String,
16 pub used_at: Option<OffsetDateTime>,
17 pub expires_at: Option<OffsetDateTime>,
18 pub created_at: OffsetDateTime,
19}
20
21impl ActiveModelBehavior for ActiveModel {}
Added lib/models/src/users/user_reports.rs View lib/models/src/users/user_reports.rs
@@ -0,0 +1,69 @@
1use sea_orm::entity::prelude::*;
2use time::OffsetDateTime;
3use uuid::Uuid;
4
5#[derive(
6 Debug,
7 Clone,
8 Copy,
9 PartialEq,
10 Eq,
11 Default,
12 EnumIter,
13 DeriveActiveEnum,
14 serde::Serialize,
15 serde::Deserialize,
16)]
17#[serde(rename_all = "snake_case")]
18#[sea_orm(rs_type = "String", db_type = "String(StringLen::N(16))")]
19pub enum ReportStatus {
20 #[default]
21 #[sea_orm(string_value = "open")]
22 Open,
23 #[sea_orm(string_value = "under_review")]
24 UnderReview,
25 #[sea_orm(string_value = "resolved")]
26 Resolved,
27 #[sea_orm(string_value = "dismissed")]
28 Dismissed,
29}
30
31#[sea_orm::model]
32#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
33#[sea_orm(table_name = "user_reports")]
34pub struct Model {
35 #[sea_orm(primary_key, auto_increment = false)]
36 pub id: Uuid,
37 pub reporter_uid: Option<Uuid>,
38 #[sea_orm(
39 belongs_to,
40 relation_enum = "Reporter",
41 from = "reporter_uid",
42 to = "uid"
43 )]
44 pub reporter: BelongsTo<Option<super::user::Entity>>,
45 pub reported_uid: Uuid,
46 #[sea_orm(
47 belongs_to,
48 relation_enum = "ReportedUser",
49 from = "reported_uid",
50 to = "uid"
51 )]
52 pub reported_user: BelongsTo<super::user::Entity>,
53 pub reason: String,
54 pub evidence: Option<String>,
55 pub status: ReportStatus,
56 pub resolver_uid: Option<Uuid>,
57 #[sea_orm(
58 belongs_to,
59 relation_enum = "Resolver",
60 from = "resolver_uid",
61 to = "uid"
62 )]
63 pub resolver: BelongsTo<Option<super::user::Entity>>,
64 pub resolution_note: Option<String>,
65 pub created_at: OffsetDateTime,
66 pub updated_at: OffsetDateTime,
67}
68
69impl ActiveModelBehavior for ActiveModel {}
Added lib/models/src/users/user_saved_messages.rs View lib/models/src/users/user_saved_messages.rs
@@ -0,0 +1,21 @@
1use sea_orm::entity::prelude::*;
2use time::OffsetDateTime;
3use uuid::Uuid;
4
5#[sea_orm::model]
6#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
7#[sea_orm(table_name = "user_saved_message")]
8pub struct Model {
9 #[sea_orm(primary_key, auto_increment = false)]
10 pub id: Uuid,
11 #[sea_orm(unique_key = "user_message")]
12 pub user_id: Uuid,
13 pub channel_id: Uuid,
14 #[sea_orm(unique_key = "user_message")]
15 pub message_id: Uuid,
16 pub saved_at: OffsetDateTime,
17 #[sea_orm(belongs_to, from = "user_id", to = "uid")]
18 pub user: BelongsTo<super::user::Entity>,
19}
20
21impl ActiveModelBehavior for ActiveModel {}
Added lib/models/src/users/user_scheduled_messages.rs View lib/models/src/users/user_scheduled_messages.rs
@@ -0,0 +1,37 @@
1use sea_orm::JsonValue;
2use sea_orm::entity::prelude::*;
3use serde::{Deserialize, Serialize};
4use time::OffsetDateTime;
5use uuid::Uuid;
6
7#[derive(
8 Debug, Clone, Copy, PartialEq, Eq, Default, EnumIter, DeriveActiveEnum, Serialize, Deserialize,
9)]
10#[sea_orm(rs_type = "String", db_type = "String(StringLen::N(16))")]
11pub enum ScheduledMessageStatus {
12 #[default]
13 #[sea_orm(string_value = "pending")]
14 Pending,
15 #[sea_orm(string_value = "sent")]
16 Sent,
17 #[sea_orm(string_value = "cancelled")]
18 Cancelled,
19}
20
21#[sea_orm::model]
22#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
23#[sea_orm(table_name = "user_scheduled_message")]
24pub struct Model {
25 #[sea_orm(primary_key, auto_increment = false)]
26 pub id: Uuid,
27 pub user_id: Uuid,
28 pub target_channel_id: Uuid,
29 pub payload: JsonValue,
30 pub scheduled_at: OffsetDateTime,
31 pub status: ScheduledMessageStatus,
32 pub created_at: OffsetDateTime,
33 #[sea_orm(belongs_to, from = "user_id", to = "uid")]
34 pub user: BelongsTo<super::user::Entity>,
35}
36
37impl ActiveModelBehavior for ActiveModel {}
Added lib/models/src/users/user_sessions.rs View lib/models/src/users/user_sessions.rs
@@ -0,0 +1,59 @@
1use sea_orm::entity::prelude::*;
2use serde::{Deserialize, Serialize};
3use time::OffsetDateTime;
4use uuid::Uuid;
5
6#[derive(
7 Debug, Clone, Copy, PartialEq, Eq, Default, EnumIter, DeriveActiveEnum, Serialize, Deserialize,
8)]
9#[sea_orm(rs_type = "i16", db_type = "SmallInteger")]
10pub enum SessionKind {
11 #[default]
12 #[sea_orm(num_value = 0)]
13 WebLogin,
14
15 #[sea_orm(num_value = 1)]
16 BearerAccess,
17}
18
19#[sea_orm::model]
20#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
21#[sea_orm(table_name = "user_session")]
22pub struct Model {
23 #[sea_orm(primary_key, auto_increment = false)]
24 pub id: Uuid,
25 pub user_id: Uuid,
26 #[sea_orm(belongs_to, from = "user_id", to = "uid")]
27 pub user: BelongsTo<super::user::Entity>,
28
29 #[sea_orm(unique)]
30 pub token_hash: String,
31
32 pub user_agent_hash: Option<String>,
33 pub client_ip_hash: Option<String>,
34
35 #[sea_orm(default_value = "0")]
36 pub kind: SessionKind,
37
38 pub family_id: Uuid,
39
40 pub parent_refresh_id: Option<Uuid>,
41
42 #[sea_orm(default_value = "false")]
43 pub is_revoked: bool,
44
45 pub revoked_at: Option<OffsetDateTime>,
46 pub created_at: OffsetDateTime,
47 pub expires_at: OffsetDateTime,
48 pub replaced_at: Option<OffsetDateTime>,
49 pub grace_expires_at: Option<OffsetDateTime>,
50 pub updated_at: OffsetDateTime,
51
52 pub replaced_by: Option<Uuid>,
53 pub signing_key_id: Option<Uuid>,
54 pub app_id: Option<String>,
55 #[sea_orm(belongs_to, from = "signing_key_id", to = "kid")]
56 pub signing_key: BelongsTo<Option<super::super::system::system_signing_keys::Entity>>,
57}
58
59impl ActiveModelBehavior for ActiveModel {}
Added lib/models/src/users/user_ssh_keys.rs View lib/models/src/users/user_ssh_keys.rs
@@ -0,0 +1,37 @@
1use sea_orm::entity::prelude::*;
2use serde::{Deserialize, Serialize};
3use time::OffsetDateTime;
4use uuid::Uuid;
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq, EnumIter, DeriveActiveEnum, Serialize, Deserialize)]
7#[serde(rename_all = "snake_case")]
8#[sea_orm(rs_type = "String", db_type = "String(StringLen::N(32))")]
9pub enum SshKeyType {
10 #[sea_orm(string_value = "ssh_rsa")]
11 SshRsa,
12 #[sea_orm(string_value = "ssh_ed25519")]
13 SshEd25519,
14 #[sea_orm(string_value = "ecdsa_sha2_nistp256")]
15 EcdsaSha2Nistp256,
16}
17
18#[sea_orm::model]
19#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
20#[sea_orm(table_name = "user_ssh_key")]
21pub struct Model {
22 #[sea_orm(primary_key, auto_increment = false)]
23 pub id: Uuid,
24 pub user_id: Uuid,
25 #[sea_orm(belongs_to, from = "user_id", to = "uid")]
26 pub user: BelongsTo<super::user::Entity>,
27 pub key_type: SshKeyType,
28 #[sea_orm(unique)]
29 pub fingerprint: String,
30 pub name: String,
31 pub content: String,
32 pub created_at: OffsetDateTime,
33 pub updated_at: OffsetDateTime,
34 pub last_used_at: Option<OffsetDateTime>,
35}
36
37impl ActiveModelBehavior for ActiveModel {}
Added lib/models/src/users/user_suspensions.rs View lib/models/src/users/user_suspensions.rs
@@ -0,0 +1,64 @@
1use sea_orm::entity::prelude::*;
2use time::OffsetDateTime;
3use uuid::Uuid;
4
5#[derive(
6 Debug,
7 Clone,
8 Copy,
9 PartialEq,
10 Eq,
11 EnumIter,
12 DeriveActiveEnum,
13 serde::Serialize,
14 serde::Deserialize,
15)]
16#[serde(rename_all = "snake_case")]
17#[sea_orm(rs_type = "String", db_type = "String(StringLen::N(16))")]
18pub enum SuspensionType {
19 #[sea_orm(string_value = "temporary")]
20 Temporary,
21 #[sea_orm(string_value = "permanent")]
22 Permanent,
23}
24
25#[sea_orm::model]
26#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
27#[sea_orm(table_name = "user_suspensions")]
28pub struct Model {
29 #[sea_orm(primary_key, auto_increment = false)]
30 pub id: Uuid,
31 pub user_id: Uuid,
32 #[sea_orm(
33 belongs_to,
34 from = "user_id",
35 to = "uid",
36 relation_enum = "SuspendedUser"
37 )]
38 pub user: BelongsTo<super::user::Entity>,
39 pub suspension_type: SuspensionType,
40 pub reason: Option<String>,
41 pub suspended_by: Option<Uuid>,
42 #[sea_orm(
43 belongs_to,
44 from = "suspended_by",
45 to = "uid",
46 relation_enum = "SuspendedBy"
47 )]
48 pub suspender: BelongsTo<Option<super::user::Entity>>,
49 pub starts_at: OffsetDateTime,
50 pub ends_at: Option<OffsetDateTime>,
51 pub revoked_at: Option<OffsetDateTime>,
52 pub revoked_by: Option<Uuid>,
53 #[sea_orm(
54 belongs_to,
55 from = "revoked_by",
56 to = "uid",
57 relation_enum = "RevokedBy"
58 )]
59 pub revoker: BelongsTo<Option<super::user::Entity>>,
60 pub created_at: OffsetDateTime,
61 pub updated_at: OffsetDateTime,
62}
63
64impl ActiveModelBehavior for ActiveModel {}
Added lib/models/src/webhook/mod.rs View lib/models/src/webhook/mod.rs
@@ -0,0 +1,3 @@
1pub mod repo_webhooks;
2pub mod webhook_deliveries;
3pub mod webhooks;
Added lib/models/src/webhook/repo_webhooks.rs View lib/models/src/webhook/repo_webhooks.rs
@@ -0,0 +1,30 @@
1use sea_orm::JsonValue;
2use sea_orm::entity::prelude::*;
3use time::OffsetDateTime;
4use uuid::Uuid;
5
6use super::webhooks::WebhookStatus;
7
8#[sea_orm::model]
9#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
10#[sea_orm(table_name = "repo_webhook")]
11pub struct Model {
12 #[sea_orm(primary_key, auto_increment = false)]
13 pub id: Uuid,
14 pub repo_id: Uuid,
15 pub url: String,
16 // 敏感字段:必须在应用层加密后再落库,且响应中禁止回显明文
17 pub secret: String,
18 pub events: JsonValue,
19 #[sea_orm(default_value = 0)]
20 pub status: WebhookStatus,
21 pub created_by: Option<Uuid>,
22 pub created_at: OffsetDateTime,
23 pub updated_at: Option<OffsetDateTime>,
24 #[sea_orm(belongs_to, from = "repo_id", to = "id")]
25 pub repo: BelongsTo<super::super::repo::repos::Entity>,
26 #[sea_orm(belongs_to, from = "created_by", to = "uid")]
27 pub creator: BelongsTo<Option<super::super::users::user::Entity>>,
28}
29
30impl ActiveModelBehavior for ActiveModel {}
Added lib/models/src/webhook/webhook_deliveries.rs View lib/models/src/webhook/webhook_deliveries.rs
@@ -0,0 +1,38 @@
1use sea_orm::JsonValue;
2use sea_orm::entity::prelude::*;
3use serde::{Deserialize, Serialize};
4use time::OffsetDateTime;
5use uuid::Uuid;
6
7#[derive(
8 Debug, Clone, Copy, PartialEq, Eq, Default, EnumIter, DeriveActiveEnum, Serialize, Deserialize,
9)]
10#[sea_orm(rs_type = "i16", db_type = "SmallInteger")]
11pub enum WebhookDeliveryStatus {
12 #[default]
13 #[sea_orm(num_value = 0)]
14 Pending,
15 #[sea_orm(num_value = 1)]
16 Success,
17 #[sea_orm(num_value = 2)]
18 Failed,
19}
20
21#[sea_orm::model]
22#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
23#[sea_orm(table_name = "webhook_delivery")]
24pub struct Model {
25 #[sea_orm(primary_key, auto_increment = false)]
26 pub id: Uuid,
27 pub webhook_id: Uuid,
28 pub event_type: String,
29 pub payload: JsonValue,
30 pub status: WebhookDeliveryStatus,
31 pub response_status: Option<i32>,
32 pub response_body: Option<String>,
33 pub created_at: OffsetDateTime,
34 #[sea_orm(belongs_to, from = "webhook_id", to = "id")]
35 pub webhook: BelongsTo<super::webhooks::Entity>,
36}
37
38impl ActiveModelBehavior for ActiveModel {}
Added lib/models/src/webhook/webhooks.rs View lib/models/src/webhook/webhooks.rs
@@ -0,0 +1,40 @@
1use sea_orm::JsonValue;
2use sea_orm::entity::prelude::*;
3use serde::{Deserialize, Serialize};
4use time::OffsetDateTime;
5use uuid::Uuid;
6
7#[derive(
8 Debug, Clone, Copy, PartialEq, Eq, Default, EnumIter, DeriveActiveEnum, Serialize, Deserialize,
9)]
10#[sea_orm(rs_type = "i16", db_type = "SmallInteger")]
11pub enum WebhookStatus {
12 #[default]
13 #[sea_orm(num_value = 0)]
14 Active,
15 #[sea_orm(num_value = 1)]
16 Disabled,
17}
18
19#[sea_orm::model]
20#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
21#[sea_orm(table_name = "webhook")]
22pub struct Model {
23 #[sea_orm(primary_key, auto_increment = false)]
24 pub id: Uuid,
25 pub url: String,
26 // 敏感字段:必须在应用层加密后再落库,且响应中禁止回显明文
27 pub secret: String,
28 pub status: WebhookStatus,
29 pub events: JsonValue,
30 pub space_id: Option<Uuid>,
31 pub created_by: Uuid,
32 pub created_at: OffsetDateTime,
33 pub updated_at: Option<OffsetDateTime>,
34 #[sea_orm(belongs_to, from = "created_by", to = "uid")]
35 pub creator: BelongsTo<super::super::users::user::Entity>,
36 #[sea_orm(belongs_to, from = "space_id", to = "id")]
37 pub space: BelongsTo<Option<super::super::space::spaces::Entity>>,
38}
39
40impl ActiveModelBehavior for ActiveModel {}
Added lib/models/src/lib.rs View lib/models/src/lib.rs
@@ -0,0 +1,16 @@
1//! 全库 ORM 实体(entity-first 策略)。
2//!
3//! 实体属性无法表达的 DDL 约束(partial unique 索引、XOR CHECK、组合索引)
4//! 见同目录下的 `schema_extras.sql`,由 db 层的 schema sync 在每次迁移后执行。
5
6pub mod common;
7pub mod dir;
8pub mod git;
9pub mod issue;
10pub mod products;
11pub mod pull_request;
12pub mod repo;
13pub mod space;
14pub mod system;
15pub mod users;
16pub mod webhook;
Added lib/models/Cargo.toml View lib/models/Cargo.toml
@@ -0,0 +1,12 @@
1[package]
2name = "models"
3version.workspace = true
4edition.workspace = true
5
6[dependencies]
7sea-orm = { version = "2.0.1", features = ["sqlx-postgres", "runtime-tokio-rustls", "with-time", "with-uuid", "with-json"] }
8time = { version = "0.3", features = ["serde"] }
9uuid = { version = "1", features = ["v7", "serde"] }
10serde = { version = "1", features = ["derive"] }
11serde_json = { workspace = true }
12tokio = { version = "1.53.1", features = [] }
Added lib/models/schema_extras.sql View lib/models/schema_extras.sql
@@ -0,0 +1,114 @@
1-- schema_extras.sql
2-- Idempotent PostgreSQL DDL for constraints that SeaORM entity-first `sync`
3-- cannot express via entity attributes. Applies after every `sync` when
4-- auto_migrate is enabled (historical convention: lib/db/schema.rs; the db
5-- crate is currently removed and must apply this file when restored).
6-- Safe to re-run on every startup; every statement is guarded with
7-- IF NOT EXISTS / DO-block checks.
8
9-- ---------------------------------------------------------------------------
10-- Partial unique indexes (polymorphic targets, WHERE-clause filtered).
11-- Not expressible as #[sea_orm(unique_key)] — overlapping columns and partial
12-- predicates have no entity attribute. The plain unique indexes declared in
13-- the models (issue_watch.issue_user / project_user, issue_reaction.*) cover
14-- non-NULL rows; these partial variants additionally guard the NULL-tolerant
15-- lookup patterns the sync layer would otherwise drop.
16-- ---------------------------------------------------------------------------
17CREATE UNIQUE INDEX IF NOT EXISTS idx_issue_watch_issue_user
18 ON issue_watch (issue_id, user_id) WHERE issue_id IS NOT NULL;
19CREATE UNIQUE INDEX IF NOT EXISTS idx_issue_watch_project_user
20 ON issue_watch (project_id, user_id) WHERE project_id IS NOT NULL;
21CREATE UNIQUE INDEX IF NOT EXISTS idx_issue_reaction_issue
22 ON issue_reaction (issue_id, user_id, emoji) WHERE issue_id IS NOT NULL;
23CREATE UNIQUE INDEX IF NOT EXISTS idx_issue_reaction_comment
24 ON issue_reaction (comment_id, user_id, emoji) WHERE comment_id IS NOT NULL;
25CREATE UNIQUE INDEX IF NOT EXISTS idx_issue_reaction_project
26 ON issue_reaction (project_id, user_id, emoji) WHERE project_id IS NOT NULL;
27CREATE UNIQUE INDEX IF NOT EXISTS idx_branch_protection_whitelist_user
28 ON repo_branch_protection_whitelist (policy_id, kind, user_id) WHERE user_id IS NOT NULL;
29CREATE UNIQUE INDEX IF NOT EXISTS idx_branch_protection_whitelist_group
30 ON repo_branch_protection_whitelist (policy_id, kind, group_id) WHERE group_id IS NOT NULL;
31
32-- ---------------------------------------------------------------------------
33-- Composite non-unique indexes (entity attributes only express single-column
34-- indexed/unique; lookups below span two columns).
35-- ---------------------------------------------------------------------------
36CREATE INDEX IF NOT EXISTS idx_comment_parent_id ON comment (parent_id);
37CREATE INDEX IF NOT EXISTS idx_git_commit_status_sha ON git_commit_status (repo_id, sha);
38
39-- Single-column indexes for reserved project_id columns (parity with the
40-- historical project module; columns are kept until project is restored).
41CREATE INDEX IF NOT EXISTS idx_comment_project ON comment (project_id);
42CREATE INDEX IF NOT EXISTS idx_issue_reaction_uniq_proj ON issue_reaction (project_id);
43CREATE INDEX IF NOT EXISTS idx_issue_watch_project ON issue_watch (project_id);
44CREATE INDEX IF NOT EXISTS idx_issue_event_project ON issue_event_line (project_id);
45CREATE INDEX IF NOT EXISTS idx_issue_pin_project ON issue_pin (project_id);
46
47-- ---------------------------------------------------------------------------
48-- CHECK constraints (polymorphic XOR / exactly-one / range guards).
49-- ALTER TABLE ... ADD CONSTRAINT has no IF NOT EXISTS, so guard each with a
50-- DO block against pg_constraint.
51-- ---------------------------------------------------------------------------
52DO $$ BEGIN
53 IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'comment_target_xor') THEN
54 ALTER TABLE comment
55 ADD CONSTRAINT comment_target_xor
56 CHECK ((issue_id IS NOT NULL) <> (project_id IS NOT NULL));
57 END IF;
58END $$;
59
60DO $$ BEGIN
61 IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'issue_reaction_target_exactly_one') THEN
62 ALTER TABLE issue_reaction
63 ADD CONSTRAINT issue_reaction_target_exactly_one
64 CHECK (((issue_id IS NOT NULL)::int + (comment_id IS NOT NULL)::int + (project_id IS NOT NULL)::int) = 1);
65 END IF;
66END $$;
67
68DO $$ BEGIN
69 IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'issue_watch_target_xor') THEN
70 ALTER TABLE issue_watch
71 ADD CONSTRAINT issue_watch_target_xor
72 CHECK ((issue_id IS NOT NULL) <> (project_id IS NOT NULL));
73 END IF;
74END $$;
75
76DO $$ BEGIN
77 IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'issue_event_target_xor') THEN
78 ALTER TABLE issue_event_line
79 ADD CONSTRAINT issue_event_target_xor
80 CHECK ((issue_id IS NOT NULL) <> (project_id IS NOT NULL));
81 END IF;
82END $$;
83
84DO $$ BEGIN
85 IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'issue_pin_target_xor') THEN
86 ALTER TABLE issue_pin
87 ADD CONSTRAINT issue_pin_target_xor
88 CHECK ((issue_id IS NOT NULL) <> (project_id IS NOT NULL));
89 END IF;
90END $$;
91
92DO $$ BEGIN
93 IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'branch_protection_whitelist_principal_xor') THEN
94 ALTER TABLE repo_branch_protection_whitelist
95 ADD CONSTRAINT branch_protection_whitelist_principal_xor
96 CHECK ((user_id IS NOT NULL) <> (group_id IS NOT NULL));
97 END IF;
98END $$;
99
100DO $$ BEGIN
101 IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'product_review_rating_range') THEN
102 ALTER TABLE product_review
103 ADD CONSTRAINT product_review_rating_range
104 CHECK (rating BETWEEN 1 AND 5);
105 END IF;
106END $$;
107
108DO $$ BEGIN
109 IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'products_price_non_negative') THEN
110 ALTER TABLE products
111 ADD CONSTRAINT products_price_non_negative
112 CHECK (price IS NULL OR price >= 0);
113 END IF;
114END $$;
No newline at end of file
Added lib/session/src/config.rs View lib/session/src/config.rs
@@ -0,0 +1,190 @@
1use axum_session::{Key, SameSite as AxumSameSite, SessionConfig, SessionMode as AxumSessionMode};
2use base64::Engine as _;
3use chrono::TimeDelta;
4use config::session::{SameSite, SessionConfig as AppSessionConfig, SessionMode};
5
6/// `axum_session` keys contain a 32-byte signing key plus a 32-byte
7/// encryption key; anything shorter is rejected by the `cookie` crate.
8const MIN_KEY_LEN: usize = 64;
9
10pub fn build_session_config(config: &AppSessionConfig) -> Result<SessionConfig, String> {
11 validate(config)?;
12
13 let mut session = SessionConfig::default();
14
15 if let Some(mode) = config.mode {
16 session = session.with_mode(match mode {
17 SessionMode::Manual => AxumSessionMode::Manual,
18 SessionMode::OptIn => AxumSessionMode::OptIn,
19 SessionMode::Persistent => AxumSessionMode::Persistent,
20 });
21 }
22
23 if let Some(name) = &config.cookie_name {
24 session = session.with_session_name(name.clone());
25 }
26 if let Some(domain) = &config.cookie_domain {
27 session = session.with_cookie_domain(domain.clone());
28 }
29 if let Some(path) = &config.cookie_path {
30 session = session.with_cookie_path(path.clone());
31 }
32 if let Some(value) = config.cookie_http_only {
33 session = session.with_http_only(value);
34 }
35 if let Some(value) = config.cookie_secure {
36 session = session.with_secure(value);
37 }
38 if let Some(same_site) = config.cookie_same_site {
39 session = session.with_cookie_same_site(match same_site {
40 SameSite::Strict => AxumSameSite::Strict,
41 SameSite::Lax => AxumSameSite::Lax,
42 SameSite::None => AxumSameSite::None,
43 });
44 }
45 if let Some(secs) = config.cookie_max_age_secs {
46 // A zero max-age makes browsers delete the cookie on receipt, which
47 // breaks every session; validation rejects it before we get here.
48 session = session.with_max_age(Some(TimeDelta::seconds(secs)));
49 }
50 if let Some(value) = config.cookie_prefix_host {
51 session = session.with_prefix_with_host(value);
52 }
53 if let Some(value) = config.cookie_encode_ip_and_user_agent {
54 session = session.with_ip_and_user_agent(value);
55 }
56
57 if let Some(secs) = config.lifespan_secs {
58 session = session.with_lifetime(TimeDelta::seconds(secs));
59 }
60 if let Some(secs) = config.max_lifespan_secs {
61 session = session.with_max_lifetime(TimeDelta::seconds(secs));
62 }
63 if let Some(secs) = config.memory_lifespan_secs {
64 session = session.with_memory_lifetime(TimeDelta::seconds(secs));
65 }
66 if let Some(secs) = config.db_update_interval_secs {
67 session = session.with_db_update_interval(TimeDelta::seconds(secs));
68 }
69 if let Some(secs) = config.purge_database_update_secs {
70 session = session.with_purge_database_update(TimeDelta::seconds(secs));
71 }
72 if let Some(value) = config.always_save {
73 session = session.with_always_save(value);
74 }
75 if let Some(value) = config.enable_bloom_filter {
76 session = session.with_bloom_filter(value);
77 }
78 if let Some(name) = &config.table_name {
79 session = session.with_table_name(name.clone());
80 }
81
82 if let Some(key) = &config.key {
83 session = session.with_key(decode_key(key)?);
84 }
85 if let Some(key) = &config.database_key {
86 session = session.with_database_key(decode_key(key)?);
87 }
88
89 Ok(session)
90}
91
92fn validate(config: &AppSessionConfig) -> Result<(), String> {
93 if let Some(name) = &config.table_name {
94 if name.is_empty() {
95 return Err(
96 "table_name must not be empty: it prefixes session keys in Redis and an empty \
97 prefix would make scans/deletes target unrelated keys"
98 .into(),
99 );
100 }
101 for c in ['*', '?', '[', ']', '\\'] {
102 if name.contains(c) {
103 return Err(format!(
104 "table_name must not contain glob character '{c}': {name:?}"
105 ));
106 }
107 }
108 }
109
110 let secure = config.cookie_secure.unwrap_or(false);
111 if config.cookie_same_site == Some(SameSite::None) && !secure {
112 return Err(
113 "cookie_same_site = none requires cookie_secure = true: browsers reject non-Secure \
114 cookies with SameSite=None"
115 .into(),
116 );
117 }
118 if !secure {
119 tracing::warn!(
120 "session cookies are not marked Secure (cookie_secure is not true); enable it when \
121 serving over TLS in production"
122 );
123 }
124
125 check_duration("lifespan_secs", config.lifespan_secs, false)?;
126 check_duration("max_lifespan_secs", config.max_lifespan_secs, false)?;
127 check_duration("memory_lifespan_secs", config.memory_lifespan_secs, true)?;
128 check_duration("cookie_max_age_secs", config.cookie_max_age_secs, false)?;
129 check_duration("db_update_interval_secs", config.db_update_interval_secs, true)?;
130 check_duration("purge_database_update_secs", config.purge_database_update_secs, true)?;
131
132 if let (Some(lifespan), Some(max_lifespan)) = (config.lifespan_secs, config.max_lifespan_secs)
133 {
134 if max_lifespan < lifespan {
135 return Err(format!(
136 "max_lifespan_secs ({max_lifespan}) must be greater than or equal to \
137 lifespan_secs ({lifespan})"
138 ));
139 }
140 }
141
142 if let Some(key) = &config.key {
143 check_key("key", key)?;
144 }
145 if let Some(key) = &config.database_key {
146 check_key("database_key", key)?;
147 }
148
149 Ok(())
150}
151
152fn check_duration(name: &str, secs: Option<i64>, allow_zero: bool) -> Result<(), String> {
153 if let Some(secs) = secs {
154 if secs < 0 || (secs == 0 && !allow_zero) {
155 return Err(format!("{name} must be positive, got {secs}"));
156 }
157 }
158
159 Ok(())
160}
161
162fn check_key(name: &str, value: &str) -> Result<(), String> {
163 let bytes = base64::engine::general_purpose::STANDARD
164 .decode(value)
165 .map_err(|err| format!("invalid base64 session key for {name}: {err}"))?;
166
167 if bytes.len() < MIN_KEY_LEN {
168 return Err(format!(
169 "{name} is too short: expected at least {MIN_KEY_LEN} randomly generated bytes \
170 (base64-encoded), got {} bytes",
171 bytes.len()
172 ));
173 }
174 if bytes.len() > MIN_KEY_LEN {
175 tracing::warn!(
176 "{name} is longer than {MIN_KEY_LEN} bytes; only the first {MIN_KEY_LEN} bytes \
177 will be used"
178 );
179 }
180
181 Ok(())
182}
183
184fn decode_key(value: &str) -> Result<Key, String> {
185 let bytes = base64::engine::general_purpose::STANDARD
186 .decode(value)
187 .map_err(|err| format!("invalid base64 session key: {err}"))?;
188
189 Key::try_from(bytes.as_slice()).map_err(|err| err.to_string())
190}
No newline at end of file
Added lib/session/src/lib.rs View lib/session/src/lib.rs
@@ -0,0 +1,7 @@
1pub mod config;
2pub mod store;
3pub mod user;
4
5pub use config::build_session_config;
6pub use store::{Session, SessionLayer, SessionRedisPool, SessionStore, ReadOnlySession};
7pub use user::{AuthUser, SessionUser, SessionUserMut, UID_KEY, USERS_KEY};
No newline at end of file
Added lib/session/src/store.rs View lib/session/src/store.rs
@@ -0,0 +1,266 @@
1use async_trait::async_trait;
2use axum_session::{DatabaseError, DatabasePool, SessionError};
3use chrono::Utc;
4use config::session::SessionConfig;
5use export::{AppRedis, RedisConnection};
6
7use crate::config::build_session_config;
8
9pub type Session = axum_session::Session<SessionRedisPool>;
10pub type ReadOnlySession = axum_session::ReadOnlySession<SessionRedisPool>;
11pub type SessionStore = axum_session::SessionStore<SessionRedisPool>;
12pub type SessionLayer = axum_session::SessionLayer<SessionRedisPool>;
13
14/// Keys deleted by a single `UNLINK` call in standalone mode. Kept at one key
15/// per call in cluster mode, where multi-key commands would cross slots.
16const UNLINK_BATCH: usize = 100;
17
18/// Hint for `SCAN COUNT`, trading a few bigger replies for far fewer round trips.
19const SCAN_BATCH: usize = 1000;
20
21#[derive(Clone)]
22pub struct SessionRedisPool {
23 redis: AppRedis,
24}
25
26impl SessionRedisPool {
27 pub fn new(redis: AppRedis) -> Self {
28 Self { redis }
29 }
30
31 pub fn redis(&self) -> &AppRedis {
32 &self.redis
33 }
34}
35
36impl From<AppRedis> for SessionRedisPool {
37 fn from(redis: AppRedis) -> Self {
38 Self::new(redis)
39 }
40}
41
42impl std::fmt::Debug for SessionRedisPool {
43 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
44 f.debug_struct("SessionRedisPool").finish()
45 }
46}
47
48pub async fn connect(
49 redis: AppRedis,
50 config: &SessionConfig,
51) -> Result<SessionStore, SessionError> {
52 let config = build_session_config(config).map_err(|err| {
53 SessionError::GenericNotSupportedError(format!("invalid session configuration: {err}"))
54 })?;
55
56 SessionStore::new(Some(SessionRedisPool::new(redis)), config).await
57}
58
59macro_rules! dispatch {
60 ($conn:expr, |$c:ident| $body:expr) => {
61 match &mut $conn {
62 RedisConnection::Standalone($c) => $body,
63 RedisConnection::Cluster($c) => $body,
64 }
65 };
66}
67
68fn session_key(id: &str, table_name: &str) -> String {
69 if table_name.is_empty() {
70 id.to_string()
71 } else {
72 format!("{table_name}:{id}")
73 }
74}
75
76/// SCANs `pattern` across the whole store: a cursor loop on the standalone
77/// connection, or the same loop on every cluster master with results merged.
78async fn scan_keys(redis: &AppRedis, pattern: &str) -> Result<Vec<String>, DatabaseError> {
79 let mut keys = match redis.connection_clone() {
80 RedisConnection::Standalone(mut conn) => scan_conn(&mut conn, pattern).await?,
81 RedisConnection::Cluster(_) => {
82 let mut keys = Vec::new();
83
84 for mut conn in redis
85 .cluster_master_connections()
86 .await
87 .map_err(|err| DatabaseError::GenericSelectError(err.to_string()))?
88 {
89 keys.extend(scan_conn(&mut conn, pattern).await?);
90 }
91
92 keys
93 }
94 };
95
96 keys.sort();
97 keys.dedup();
98 Ok(keys)
99}
100
101async fn scan_conn<C>(conn: &mut C, pattern: &str) -> Result<Vec<String>, DatabaseError>
102where
103 C: redis::aio::ConnectionLike,
104{
105 let mut keys = Vec::new();
106 let mut cursor: u64 = 0;
107
108 loop {
109 let (next, batch): (u64, Vec<String>) = redis::cmd("SCAN")
110 .arg(cursor)
111 .arg("MATCH")
112 .arg(pattern)
113 .arg("COUNT")
114 .arg(SCAN_BATCH)
115 .query_async(conn)
116 .await
117 .map_err(|err| DatabaseError::GenericSelectError(err.to_string()))?;
118
119 keys.extend(batch);
120 cursor = next;
121
122 if cursor == 0 {
123 break;
124 }
125 }
126
127 Ok(keys)
128}
129
130#[async_trait]
131impl DatabasePool for SessionRedisPool {
132 async fn initiate(&self, _table_name: &str) -> Result<(), DatabaseError> {
133 Ok(())
134 }
135
136 async fn delete_by_expiry(&self, _table_name: &str) -> Result<Vec<String>, DatabaseError> {
137 Ok(Vec::new())
138 }
139
140 async fn count(&self, table_name: &str) -> Result<i64, DatabaseError> {
141 ensure_table(table_name)?;
142
143 let count = scan_keys(&self.redis, &format!("{table_name}:*")).await?.len() as i64;
144
145 Ok(count)
146 }
147
148 async fn store(
149 &self,
150 id: &str,
151 session: &str,
152 expires: i64,
153 table_name: &str,
154 ) -> Result<(), DatabaseError> {
155 // axum_session passes `expires` as a unix timestamp. Store a relative
156 // TTL instead of `EXAT` so Redis < 6.2 keeps working.
157 let ttl_ms = (expires - Utc::now().timestamp()) * 1000;
158 if ttl_ms <= 0 {
159 return Ok(());
160 }
161
162 let key = session_key(id, table_name);
163 let mut conn = self.redis.connection_clone();
164
165 dispatch!(conn, |c| {
166 redis::cmd("SET")
167 .arg(&key)
168 .arg(session)
169 .arg("PX")
170 .arg(ttl_ms)
171 .exec_async(c)
172 .await
173 })
174 .map_err(|err| DatabaseError::GenericInsertError(err.to_string()))
175 }
176
177 async fn load(&self, id: &str, table_name: &str) -> Result<Option<String>, DatabaseError> {
178 let key = session_key(id, table_name);
179 let mut conn = self.redis.connection_clone();
180
181 dispatch!(conn, |c| redis::cmd("GET").arg(&key).query_async(c).await)
182 .map_err(|err| DatabaseError::GenericSelectError(err.to_string()))
183 }
184
185 async fn delete_one_by_id(&self, id: &str, table_name: &str) -> Result<(), DatabaseError> {
186 let key = session_key(id, table_name);
187 let mut conn = self.redis.connection_clone();
188
189 dispatch!(conn, |c| redis::cmd("UNLINK").arg(&key).exec_async(c).await)
190 .map_err(|err| DatabaseError::GenericDeleteError(err.to_string()))
191 }
192
193 async fn exists(&self, id: &str, table_name: &str) -> Result<bool, DatabaseError> {
194 let key = session_key(id, table_name);
195 let mut conn = self.redis.connection_clone();
196
197 dispatch!(conn, |c| redis::cmd("EXISTS").arg(&key).query_async(c).await)
198 .map_err(|err| DatabaseError::GenericSelectError(err.to_string()))
199 }
200
201 async fn delete_all(&self, table_name: &str) -> Result<(), DatabaseError> {
202 ensure_table(table_name)?;
203
204 let keys = scan_keys(&self.redis, &format!("{table_name}:*")).await?;
205 if keys.is_empty() {
206 return Ok(());
207 }
208
209 let mut conn = self.redis.connection_clone();
210 match &mut conn {
211 // A single multi-key UNLINK must not cross slots, so batching is
212 // only safe on a standalone connection.
213 RedisConnection::Standalone(_) => {
214 for chunk in keys.chunks(UNLINK_BATCH) {
215 dispatch!(conn, |c| {
216 let mut cmd = redis::cmd("UNLINK");
217 for key in chunk {
218 cmd.arg(key);
219 }
220 cmd.exec_async(c).await
221 })
222 .map_err(|err| DatabaseError::GenericDeleteError(err.to_string()))?;
223 }
224 }
225 RedisConnection::Cluster(_) => {
226 for key in &keys {
227 dispatch!(conn, |c| redis::cmd("UNLINK").arg(key).exec_async(c).await)
228 .map_err(|err| DatabaseError::GenericDeleteError(err.to_string()))?;
229 }
230 }
231 }
232
233 Ok(())
234 }
235
236 async fn get_ids(&self, table_name: &str) -> Result<Vec<String>, DatabaseError> {
237 ensure_table(table_name)?;
238
239 let prefix = format!("{table_name}:");
240
241 let ids = scan_keys(&self.redis, &format!("{table_name}:*"))
242 .await?
243 .into_iter()
244 .filter_map(|key| key.strip_prefix(&prefix).map(str::to_string))
245 .collect();
246
247 Ok(ids)
248 }
249
250 fn auto_handles_expiry(&self) -> bool {
251 true
252 }
253}
254
255/// Session keys are managed under a `{table_name}:` prefix; an empty prefix
256/// would make scans, counts and deletes target unrelated keys (or the whole
257/// database). `connect` already rejects it, this is the defensive backstop.
258fn ensure_table(table_name: &str) -> Result<(), DatabaseError> {
259 if table_name.is_empty() {
260 return Err(DatabaseError::GenericNotSupportedError(
261 "session table_name must not be empty".into(),
262 ));
263 }
264
265 Ok(())
266}
No newline at end of file
Added lib/session/src/user.rs View lib/session/src/user.rs
@@ -0,0 +1,110 @@
1use std::fmt::Debug;
2
3use axum_session::{DatabasePool, ReadOnlySession as AxumReadOnlySession, Session as AxumSession};
4use serde::{Deserialize, Serialize};
5use uuid::Uuid;
6
7pub const UID_KEY: &str = "auth:uid";
8pub const USERS_KEY: &str = "auth:users";
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct AuthUser {
12 pub uid: Uuid,
13 pub username: String,
14 pub display_name: Option<String>,
15 pub avatar_url: Option<String>,
16 pub email: String,
17}
18
19pub trait SessionUser {
20 fn uid(&self) -> Option<Uuid>;
21
22 fn user(&self) -> Option<AuthUser> {
23 let uid = self.uid()?;
24 self.users().into_iter().find(|user| user.uid == uid)
25 }
26
27 fn users(&self) -> Vec<AuthUser>;
28
29 fn is_authenticated(&self) -> bool {
30 self.uid().is_some()
31 }
32}
33
34pub trait SessionUserMut: SessionUser {
35 fn login_user(&self, user: AuthUser);
36
37 fn switch_uid(&self, uid: Uuid) -> bool;
38
39 fn clear_uid(&self);
40
41 fn logout_user(&self, uid: Uuid) -> bool;
42}
43
44macro_rules! impl_session_user {
45 ($ty:ident) => {
46 impl<D> SessionUser for $ty<D>
47 where
48 D: DatabasePool + Clone + Debug + Sync + Send + 'static,
49 {
50 fn uid(&self) -> Option<Uuid> {
51 self.get::<Uuid>(UID_KEY)
52 }
53
54 fn users(&self) -> Vec<AuthUser> {
55 self.get::<Vec<AuthUser>>(USERS_KEY).unwrap_or_default()
56 }
57 }
58 };
59}
60
61impl_session_user!(AxumSession);
62impl_session_user!(AxumReadOnlySession);
63
64impl<D> SessionUserMut for AxumSession<D>
65where
66 D: DatabasePool + Clone + Debug + Sync + Send + 'static,
67{
68 fn login_user(&self, user: AuthUser) {
69 let uid = user.uid;
70 let mut users = self.users();
71 users.retain(|existing| existing.uid != uid);
72 users.push(user);
73 self.set(USERS_KEY, users);
74 self.set(UID_KEY, uid);
75 }
76
77 fn switch_uid(&self, uid: Uuid) -> bool {
78 if self.users().iter().any(|user| user.uid == uid) {
79 self.set(UID_KEY, uid);
80 true
81 } else {
82 false
83 }
84 }
85
86 fn clear_uid(&self) {
87 self.remove(UID_KEY);
88 }
89
90 fn logout_user(&self, uid: Uuid) -> bool {
91 let mut users = self.users();
92 let was_active = self.uid() == Some(uid);
93 let before = users.len();
94
95 users.retain(|user| user.uid != uid);
96 if users.len() == before {
97 return false;
98 }
99
100 if was_active {
101 match users.last() {
102 Some(remainder) => self.set(UID_KEY, remainder.uid),
103 None => self.remove(UID_KEY),
104 }
105 }
106
107 self.set(USERS_KEY, users);
108 true
109 }
110}
No newline at end of file
Added lib/session/Cargo.toml View lib/session/Cargo.toml
@@ -0,0 +1,16 @@
1[package]
2name = "session"
3version.workspace = true
4edition.workspace = true
5
6[dependencies]
7config = { path = "../config" }
8export = { path = "../export" }
9axum_session = { workspace = true }
10redis = { workspace = true }
11async-trait = { workspace = true }
12base64 = { workspace = true }
13chrono = { workspace = true }
14tracing = { workspace = true }
15serde = { workspace = true, features = ["derive"] }
16uuid = { version = "1", features = ["v7", "serde"] }
No newline at end of file
Added lib/storage/src/error.rs View lib/storage/src/error.rs
@@ -0,0 +1,47 @@
1use std::fmt;
2
3use s3::creds::error::CredentialsError;
4use s3::error::S3Error;
5
6#[derive(Debug)]
7pub enum StorageError {
8 InvalidConfig(String),
9 S3(S3Error),
10 UnexpectedStatus {
11 operation: &'static str,
12 status: u16,
13 },
14}
15
16impl fmt::Display for StorageError {
17 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
18 match self {
19 StorageError::InvalidConfig(msg) => write!(f, "invalid s3 config: {msg}"),
20 StorageError::S3(err) => write!(f, "s3 error: {err}"),
21 StorageError::UnexpectedStatus { operation, status } => {
22 write!(f, "{operation} returned unexpected status {status}")
23 }
24 }
25 }
26}
27
28impl std::error::Error for StorageError {
29 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
30 match self {
31 StorageError::S3(err) => Some(err),
32 _ => None,
33 }
34 }
35}
36
37impl From<S3Error> for StorageError {
38 fn from(err: S3Error) -> Self {
39 StorageError::S3(err)
40 }
41}
42
43impl From<CredentialsError> for StorageError {
44 fn from(err: CredentialsError) -> Self {
45 StorageError::S3(S3Error::Credentials(err))
46 }
47}
Added lib/storage/src/lib.rs View lib/storage/src/lib.rs
@@ -0,0 +1,5 @@
1pub mod error;
2pub mod s3;
3
4pub use error::StorageError;
5pub use s3::{ObjectInfo, ObjectPage, S3Client, MAX_PRESIGN_EXPIRY_SECS};
Added lib/storage/src/s3.rs View lib/storage/src/s3.rs
@@ -0,0 +1,288 @@
1use std::collections::HashMap;
2
3use config::s3::S3Config;
4use http::HeaderMap;
5use s3::creds::Credentials;
6use s3::region::Region;
7use s3::serde_types::{DateTime as S3DateTime, Object as S3Object};
8use s3::Bucket;
9
10use crate::error::StorageError;
11
12pub const MAX_PRESIGN_EXPIRY_SECS: u32 = 604_800;
13
14#[derive(Debug, Clone)]
15pub struct ObjectInfo {
16 pub key: String,
17 pub size: u64,
18 pub last_modified: Option<S3DateTime>,
19 pub etag: Option<String>,
20}
21
22impl From<S3Object> for ObjectInfo {
23 fn from(obj: S3Object) -> Self {
24 Self {
25 key: obj.key,
26 size: obj.size,
27 last_modified: Some(obj.last_modified),
28 etag: obj.e_tag,
29 }
30 }
31}
32
33#[derive(Debug, Clone)]
34pub struct ObjectPage {
35 pub objects: Vec<ObjectInfo>,
36 pub next_continuation_token: Option<String>,
37}
38
39#[derive(Clone)]
40pub struct S3Client {
41 bucket: Bucket,
42 bucket_name: String,
43}
44
45impl S3Client {
46 pub fn new(config: &S3Config) -> Result<Self, StorageError> {
47 let (bucket_name, region) = config.validate().map_err(StorageError::InvalidConfig)?;
48
49 let region = build_region(config, region)?;
50 let credentials = build_credentials(config)?;
51 let mut bucket = Bucket::new(bucket_name, region, credentials)?;
52
53 if config.use_path_style() {
54 bucket = bucket.with_path_style();
55 }
56 if let Some(timeout) = config.request_timeout() {
57 bucket = bucket.with_request_timeout(timeout);
58 }
59
60 Ok(Self {
61 bucket,
62 bucket_name: bucket_name.to_string(),
63 })
64 }
65
66 pub fn bucket_name(&self) -> &str {
67 &self.bucket_name
68 }
69
70 pub async fn put(&self, key: &str, body: &[u8]) -> Result<(), StorageError> {
71 self.put_with_content_type(key, body, "application/octet-stream")
72 .await
73 }
74
75 pub async fn put_with_content_type(
76 &self,
77 key: &str,
78 body: &[u8],
79 content_type: &str,
80 ) -> Result<(), StorageError> {
81 let response = self
82 .bucket
83 .put_object_with_content_type(key, body, content_type)
84 .await?;
85 let status = response.status_code();
86 if (200..300).contains(&status) {
87 Ok(())
88 } else {
89 Err(StorageError::UnexpectedStatus {
90 operation: "put_object",
91 status,
92 })
93 }
94 }
95
96 pub async fn get(&self, key: &str) -> Result<Vec<u8>, StorageError> {
97 let response = self.bucket.get_object(key).await?;
98 let status = response.status_code();
99 if !(200..300).contains(&status) {
100 return Err(StorageError::UnexpectedStatus {
101 operation: "get_object",
102 status,
103 });
104 }
105 Ok(response.as_slice().to_vec())
106 }
107
108 pub async fn delete(&self, key: &str) -> Result<(), StorageError> {
109 let response = self.bucket.delete_object(key).await?;
110 let status = response.status_code();
111 if (200..300).contains(&status) || status == 404 {
112 Ok(())
113 } else {
114 Err(StorageError::UnexpectedStatus {
115 operation: "delete_object",
116 status,
117 })
118 }
119 }
120
121 pub async fn exists(&self, key: &str) -> Result<bool, StorageError> {
122 let (_, status) = self.bucket.head_object(key).await?;
123 Ok((200..300).contains(&status))
124 }
125
126 pub async fn bucket_exists(&self) -> Result<bool, StorageError> {
127 Ok(self.bucket.exists().await?)
128 }
129
130 pub async fn list_page(
131 &self,
132 prefix: &str,
133 continuation_token: Option<&str>,
134 max_keys: Option<usize>,
135 ) -> Result<ObjectPage, StorageError> {
136 let (page, status) = self
137 .bucket
138 .list_page(
139 prefix.to_string(),
140 None,
141 continuation_token.map(str::to_owned),
142 None,
143 max_keys,
144 )
145 .await?;
146
147 if !(200..300).contains(&status) {
148 return Err(StorageError::UnexpectedStatus {
149 operation: "list",
150 status,
151 });
152 }
153
154 Ok(ObjectPage {
155 objects: page.contents.into_iter().map(ObjectInfo::from).collect(),
156 next_continuation_token: page.next_continuation_token,
157 })
158 }
159
160 pub async fn list(&self, prefix: &str) -> Result<Vec<ObjectInfo>, StorageError> {
161 let mut out = Vec::new();
162 let mut token: Option<String> = None;
163 loop {
164 let page = self.list_page(prefix, token.as_deref(), None).await?;
165 let more = page.next_continuation_token.is_some();
166 out.extend(page.objects);
167 match page.next_continuation_token {
168 Some(next) => token = Some(next),
169 None => {
170 if !more {
171 break;
172 }
173 }
174 }
175 if token.is_none() {
176 break;
177 }
178 }
179 Ok(out)
180 }
181
182 pub fn bucket(&self) -> &Bucket {
183 &self.bucket
184 }
185
186 pub fn presign_get(
187 &self,
188 key: &str,
189 expiry_secs: u32,
190 ) -> Result<String, StorageError> {
191 check_expiry(expiry_secs)?;
192 Ok(self.bucket.presign_get(key, expiry_secs, None)?)
193 }
194
195 pub fn presign_get_with_queries(
196 &self,
197 key: &str,
198 expiry_secs: u32,
199 custom_queries: HashMap<String, String>,
200 ) -> Result<String, StorageError> {
201 check_expiry(expiry_secs)?;
202 Ok(self
203 .bucket
204 .presign_get(key, expiry_secs, Some(custom_queries))?)
205 }
206
207 pub fn presign_put(
208 &self,
209 key: &str,
210 expiry_secs: u32,
211 ) -> Result<String, StorageError> {
212 check_expiry(expiry_secs)?;
213 Ok(self.bucket.presign_put(key, expiry_secs, None)?)
214 }
215
216 pub fn presign_put_with_headers(
217 &self,
218 key: &str,
219 expiry_secs: u32,
220 custom_headers: HeaderMap,
221 ) -> Result<String, StorageError> {
222 check_expiry(expiry_secs)?;
223 Ok(self
224 .bucket
225 .presign_put(key, expiry_secs, Some(custom_headers))?)
226 }
227
228 pub fn presign_delete(
229 &self,
230 key: &str,
231 expiry_secs: u32,
232 ) -> Result<String, StorageError> {
233 check_expiry(expiry_secs)?;
234 Ok(self.bucket.presign_delete(key, expiry_secs)?)
235 }
236
237 pub fn presign_post(
238 &self,
239 key: &str,
240 expiry_secs: u32,
241 post_policy: String,
242 ) -> Result<String, StorageError> {
243 check_expiry(expiry_secs)?;
244 Ok(self.bucket.presign_post(key, expiry_secs, post_policy)?)
245 }
246}
247
248fn build_region(config: &S3Config, region: &str) -> Result<Region, StorageError> {
249 match config.endpoint.as_deref().filter(|s| !s.is_empty()) {
250 Some(endpoint) => Ok(Region::Custom {
251 region: region.to_string(),
252 endpoint: endpoint.to_string(),
253 }),
254 None => region
255 .parse::<Region>()
256 .map_err(|err| StorageError::InvalidConfig(format!("region {region:?}: {err}"))),
257 }
258}
259
260fn build_credentials(config: &S3Config) -> Result<Credentials, StorageError> {
261 match (
262 config.access_key_id.as_deref().filter(|s| !s.is_empty()),
263 config
264 .secret_access_key
265 .as_deref()
266 .filter(|s| !s.is_empty()),
267 ) {
268 (Some(access), Some(secret)) => {
269 Ok(Credentials::new(Some(access), Some(secret), None, None, None)?)
270 }
271 (Some(_), None) => Err(StorageError::InvalidConfig(
272 "access_key_id provided without secret_access_key".into(),
273 )),
274 (None, Some(_)) => Err(StorageError::InvalidConfig(
275 "secret_access_key provided without access_key_id".into(),
276 )),
277 (None, None) => Ok(Credentials::default()?),
278 }
279}
280
281fn check_expiry(expiry_secs: u32) -> Result<(), StorageError> {
282 if expiry_secs > MAX_PRESIGN_EXPIRY_SECS {
283 return Err(StorageError::InvalidConfig(format!(
284 "presign expiry must be <= {MAX_PRESIGN_EXPIRY_SECS} seconds (got {expiry_secs})"
285 )));
286 }
287 Ok(())
288}
Modified Cargo.lock View Cargo.lock
@@ -134,6 +134,12 @@
134134checksum = "34fde25430d87a9388dadbe6e34d7f72a462c8b43ac8d309b42b0a8505d7e2a5"
135135
136136[[package]]
137name = "anyhow"
138version = "1.0.104"
139source = "registry+https://github.com/rust-lang/crates.io-index"
140checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
141
142[[package]]
137143name = "ar_archive_writer"
138144version = "0.5.3"
139145source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -396,6 +402,10 @@
396402]
397403
398404[[package]]
405name = "auth"
406version = "1.0.0"
407
408[[package]]
399409name = "autocfg"
400410version = "1.5.1"
401411source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -419,6 +429,29 @@
419429]
420430
421431[[package]]
432name = "aws-lc-rs"
433version = "1.18.0"
434source = "registry+https://github.com/rust-lang/crates.io-index"
435checksum = "ce2b2dcc879c3bae0d371e77c99f2238400ef24ec001394befa67b6e543add9e"
436dependencies = [
437 "aws-lc-sys",
438 "zeroize",
439]
440
441[[package]]
442name = "aws-lc-sys"
443version = "0.44.0"
444source = "registry+https://github.com/rust-lang/crates.io-index"
445checksum = "f09fae7be8bb3174e05c6afdb34199e6dc0c7c04ba9fa237b1967adfbde27483"
446dependencies = [
447 "cc",
448 "cmake",
449 "dunce",
450 "fs_extra",
451 "pkg-config",
452]
453
454[[package]]
422455name = "aws-region"
423456version = "0.25.5"
424457source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -493,6 +526,7 @@
493526 "chrono",
494527 "cookie",
495528 "dashmap",
529 "fastbloom-rs",
496530 "forwarded-header-value",
497531 "futures",
498532 "hmac 0.13.0",
@@ -692,6 +726,8 @@
692726checksum = "509591b7bcd67f4ef775afad7662703b4935daaa6ec0e5605cfb1090b32a2b6d"
693727dependencies = [
694728 "find-msvc-tools",
729 "jobserver",
730 "libc",
695731 "shlex",
696732]
697733
@@ -754,6 +790,15 @@
754790]
755791
756792[[package]]
793name = "cmake"
794version = "0.1.58"
795source = "registry+https://github.com/rust-lang/crates.io-index"
796checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678"
797dependencies = [
798 "cc",
799]
800
801[[package]]
757802name = "cmov"
758803version = "0.5.4"
759804source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -824,6 +869,16 @@
824869
825870[[package]]
826871name = "core-foundation"
872version = "0.9.4"
873source = "registry+https://github.com/rust-lang/crates.io-index"
874checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
875dependencies = [
876 "core-foundation-sys",
877 "libc",
878]
879
880[[package]]
881name = "core-foundation"
827882version = "0.10.1"
828883source = "registry+https://github.com/rust-lang/crates.io-index"
829884checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6"
@@ -981,6 +1036,17 @@
9811036]
9821037
9831038[[package]]
1039name = "cuckoofilter"
1040version = "0.5.0"
1041source = "registry+https://github.com/rust-lang/crates.io-index"
1042checksum = "b810a8449931679f64cd7eef1bbd0fa315801b6d5d9cdc1ace2804d6529eee18"
1043dependencies = [
1044 "byteorder",
1045 "fnv",
1046 "rand 0.7.3",
1047]
1048
1049[[package]]
9841050name = "darling"
9851051version = "0.20.11"
9861052source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1138,6 +1204,12 @@
11381204checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
11391205
11401206[[package]]
1207name = "dunce"
1208version = "1.0.5"
1209source = "registry+https://github.com/rust-lang/crates.io-index"
1210checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
1211
1212[[package]]
11411213name = "either"
11421214version = "1.17.0"
11431215source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1147,6 +1219,15 @@
11471219]
11481220
11491221[[package]]
1222name = "encoding_rs"
1223version = "0.8.35"
1224source = "registry+https://github.com/rust-lang/crates.io-index"
1225checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
1226dependencies = [
1227 "cfg-if",
1228]
1229
1230[[package]]
11501231name = "equivalent"
11511232version = "1.0.2"
11521233source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1197,6 +1278,7 @@
11971278version = "1.0.0"
11981279dependencies = [
11991280 "config",
1281 "mailbaby",
12001282 "redis",
12011283 "sea-orm",
12021284 "storage",
@@ -1204,6 +1286,24 @@
12041286]
12051287
12061288[[package]]
1289name = "fastbloom-rs"
1290version = "0.5.10"
1291source = "registry+https://github.com/rust-lang/crates.io-index"
1292checksum = "1cedd8cf9a34b304cfa127370e66b51f85c412cf1a9cc15ec39fc1fe2912403a"
1293dependencies = [
1294 "cuckoofilter",
1295 "fastmurmur3",
1296 "xorfilter-rs",
1297 "xxhash-rust",
1298]
1299
1300[[package]]
1301name = "fastmurmur3"
1302version = "0.2.0"
1303source = "registry+https://github.com/rust-lang/crates.io-index"
1304checksum = "2d7e9bc68be4cdabbb8938140b01a8b5bc1191937f2c7e7ecc2fcebbe2d749df"
1305
1306[[package]]
12071307name = "fastrand"
12081308version = "2.5.0"
12091309source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1227,6 +1327,12 @@
12271327]
12281328
12291329[[package]]
1330name = "fixedbitset"
1331version = "0.5.7"
1332source = "registry+https://github.com/rust-lang/crates.io-index"
1333checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99"
1334
1335[[package]]
12301336name = "flume"
12311337version = "0.12.0"
12321338source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1245,6 +1351,12 @@
12451351
12461352[[package]]
12471353name = "foldhash"
1354version = "0.1.5"
1355source = "registry+https://github.com/rust-lang/crates.io-index"
1356checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
1357
1358[[package]]
1359name = "foldhash"
12481360version = "0.2.0"
12491361source = "registry+https://github.com/rust-lang/crates.io-index"
12501362checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
@@ -1284,6 +1396,12 @@
12841396]
12851397
12861398[[package]]
1399name = "fs_extra"
1400version = "1.3.0"
1401source = "registry+https://github.com/rust-lang/crates.io-index"
1402checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
1403
1404[[package]]
12871405name = "funty"
12881406version = "2.0.0"
12891407source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1400,13 +1518,26 @@
14001518
14011519[[package]]
14021520name = "getrandom"
1521version = "0.1.16"
1522source = "registry+https://github.com/rust-lang/crates.io-index"
1523checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce"
1524dependencies = [
1525 "cfg-if",
1526 "libc",
1527 "wasi 0.9.0+wasi-snapshot-preview1",
1528]
1529
1530[[package]]
1531name = "getrandom"
14031532version = "0.2.17"
14041533source = "registry+https://github.com/rust-lang/crates.io-index"
14051534checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
14061535dependencies = [
14071536 "cfg-if",
1537 "js-sys",
14081538 "libc",
14091539 "wasi 0.11.1+wasi-snapshot-preview1",
1540 "wasm-bindgen",
14101541]
14111542
14121543[[package]]
@@ -1428,9 +1559,11 @@
14281559checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
14291560dependencies = [
14301561 "cfg-if",
1562 "js-sys",
14311563 "libc",
14321564 "r-efi 6.0.0",
14331565 "rand_core 0.10.1",
1566 "wasm-bindgen",
14341567]
14351568
14361569[[package]]
@@ -1472,6 +1605,25 @@
14721605]
14731606
14741607[[package]]
1608name = "h2"
1609version = "0.4.16"
1610source = "registry+https://github.com/rust-lang/crates.io-index"
1611checksum = "a9f37a958b41b3b19ee2707c06439c0e9e547e847223eb791ecb0cb821c65e27"
1612dependencies = [
1613 "atomic-waker",
1614 "bytes",
1615 "fnv",
1616 "futures-core",
1617 "futures-sink",
1618 "http 1.5.0",
1619 "indexmap",
1620 "slab",
1621 "tokio",
1622 "tokio-util",
1623 "tracing",
1624]
1625
1626[[package]]
14751627name = "half"
14761628version = "2.7.1"
14771629source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1500,13 +1652,22 @@
15001652
15011653[[package]]
15021654name = "hashbrown"
1655version = "0.15.5"
1656source = "registry+https://github.com/rust-lang/crates.io-index"
1657checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
1658dependencies = [
1659 "foldhash 0.1.5",
1660]
1661
1662[[package]]
1663name = "hashbrown"
15031664version = "0.16.1"
15041665source = "registry+https://github.com/rust-lang/crates.io-index"
15051666checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
15061667dependencies = [
15071668 "allocator-api2",
15081669 "equivalent",
1509 "foldhash",
1670 "foldhash 0.2.0",
15101671]
15111672
15121673[[package]]
@@ -1666,7 +1827,7 @@
16661827 "time 0.1.45",
16671828 "traitobject",
16681829 "typeable",
1669 "unicase",
1830 "unicase 1.4.2",
16701831 "url 1.7.2",
16711832]
16721833
@@ -1680,7 +1841,7 @@
16801841 "futures-channel",
16811842 "futures-core",
16821843 "futures-util",
1683 "h2",
1844 "h2 0.3.27",
16841845 "http 0.2.12",
16851846 "http-body 0.4.6",
16861847 "httparse",
@@ -1704,6 +1865,7 @@
17041865 "bytes",
17051866 "futures-channel",
17061867 "futures-core",
1868 "h2 0.4.16",
17071869 "http 1.5.0",
17081870 "http-body 1.1.0",
17091871 "httparse",
@@ -1712,6 +1874,7 @@
17121874 "pin-project-lite",
17131875 "smallvec",
17141876 "tokio",
1877 "want",
17151878]
17161879
17171880[[package]]
@@ -1726,6 +1889,34 @@
17261889]
17271890
17281891[[package]]
1892name = "hyper-rustls"
1893version = "0.27.9"
1894source = "registry+https://github.com/rust-lang/crates.io-index"
1895checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f"
1896dependencies = [
1897 "http 1.5.0",
1898 "hyper 1.11.0",
1899 "hyper-util",
1900 "rustls",
1901 "tokio",
1902 "tokio-rustls",
1903 "tower-service",
1904]
1905
1906[[package]]
1907name = "hyper-timeout"
1908version = "0.5.2"
1909source = "registry+https://github.com/rust-lang/crates.io-index"
1910checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0"
1911dependencies = [
1912 "hyper 1.11.0",
1913 "hyper-util",
1914 "pin-project-lite",
1915 "tokio",
1916 "tower-service",
1917]
1918
1919[[package]]
17291920name = "hyper-tls"
17301921version = "0.5.0"
17311922source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1744,13 +1935,23 @@
17441935source = "registry+https://github.com/rust-lang/crates.io-index"
17451936checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"
17461937dependencies = [
1938 "base64 0.22.1",
17471939 "bytes",
1940 "futures-channel",
1941 "futures-util",
17481942 "http 1.5.0",
17491943 "http-body 1.1.0",
17501944 "hyper 1.11.0",
1945 "ipnet",
1946 "libc",
1947 "percent-encoding 2.3.2",
17511948 "pin-project-lite",
1949 "socket2 0.5.10",
1950 "system-configuration",
17521951 "tokio",
17531952 "tower-service",
1953 "tracing",
1954 "windows-registry",
17541955]
17551956
17561957[[package]]
@@ -2266,6 +2467,12 @@
22662467]
22672468
22682469[[package]]
2470name = "ipnet"
2471version = "2.12.1"
2472source = "registry+https://github.com/rust-lang/crates.io-index"
2473checksum = "6a756c3fac73139e83f14c2d742155dd2b78d3ee56597b419a0579b7bdd6dd78"
2474
2475[[package]]
22692476name = "itertools"
22702477version = "0.14.0"
22712478source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2287,38 +2494,97 @@
22872494checksum = "4d3667095d64c3ecffc96463a21157b04bf3e252f6e8d5750b20c02e33c194e3"
22882495
22892496[[package]]
2290name = "js-sys"
2291version = "0.3.104"
2497name = "jni"
2498version = "0.22.4"
22922499source = "registry+https://github.com/rust-lang/crates.io-index"
2293checksum = "0e0c1080212aad755ea003d18543e8768dd432c48819efd73a7bf1e39b7a5a3a"
2500checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498"
22942501dependencies = [
22952502 "cfg-if",
2296 "futures-util",
2297 "wasm-bindgen",
2503 "combine",
2504 "jni-macros",
2505 "jni-sys",
2506 "log 0.4.33",
2507 "simd_cesu8",
2508 "thiserror 2.0.20",
2509 "walkdir",
2510 "windows-link",
22982511]
22992512
23002513[[package]]
2301name = "language-tags"
2302version = "0.2.2"
2514name = "jni-macros"
2515version = "0.22.4"
23032516source = "registry+https://github.com/rust-lang/crates.io-index"
2304checksum = "a91d884b6667cd606bb5a69aa0c99ba811a115fc68915e7056ec08a46e93199a"
2517checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3"
2518dependencies = [
2519 "proc-macro2",
2520 "quote",
2521 "rustc_version",
2522 "simd_cesu8",
2523 "syn 2.0.119",
2524]
23052525
23062526[[package]]
2307name = "lexical-core"
2308version = "1.0.6"
2527name = "jni-sys"
2528version = "0.4.1"
23092529source = "registry+https://github.com/rust-lang/crates.io-index"
2310checksum = "7d8d125a277f807e55a77304455eb7b1cb52f2b18c143b60e766c120bd64a594"
2530checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2"
23112531dependencies = [
2312 "lexical-parse-float",
2313 "lexical-parse-integer",
2314 "lexical-util",
2315 "lexical-write-float",
2316 "lexical-write-integer",
2532 "jni-sys-macros",
23172533]
23182534
23192535[[package]]
2320name = "lexical-parse-float"
2321version = "1.0.6"
2536name = "jni-sys-macros"
2537version = "0.4.1"
2538source = "registry+https://github.com/rust-lang/crates.io-index"
2539checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264"
2540dependencies = [
2541 "quote",
2542 "syn 2.0.119",
2543]
2544
2545[[package]]
2546name = "jobserver"
2547version = "0.1.35"
2548source = "registry+https://github.com/rust-lang/crates.io-index"
2549checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3"
2550dependencies = [
2551 "getrandom 0.4.3",
2552 "libc",
2553]
2554
2555[[package]]
2556name = "js-sys"
2557version = "0.3.104"
2558source = "registry+https://github.com/rust-lang/crates.io-index"
2559checksum = "0e0c1080212aad755ea003d18543e8768dd432c48819efd73a7bf1e39b7a5a3a"
2560dependencies = [
2561 "cfg-if",
2562 "futures-util",
2563 "wasm-bindgen",
2564]
2565
2566[[package]]
2567name = "language-tags"
2568version = "0.2.2"
2569source = "registry+https://github.com/rust-lang/crates.io-index"
2570checksum = "a91d884b6667cd606bb5a69aa0c99ba811a115fc68915e7056ec08a46e93199a"
2571
2572[[package]]
2573name = "lexical-core"
2574version = "1.0.6"
2575source = "registry+https://github.com/rust-lang/crates.io-index"
2576checksum = "7d8d125a277f807e55a77304455eb7b1cb52f2b18c143b60e766c120bd64a594"
2577dependencies = [
2578 "lexical-parse-float",
2579 "lexical-parse-integer",
2580 "lexical-util",
2581 "lexical-write-float",
2582 "lexical-write-integer",
2583]
2584
2585[[package]]
2586name = "lexical-parse-float"
2587version = "1.0.6"
23222588source = "registry+https://github.com/rust-lang/crates.io-index"
23232589checksum = "52a9f232fbd6f550bc0137dcb5f99ab674071ac2d690ac69704593cb4abbea56"
23242590dependencies = [
@@ -2428,6 +2694,12 @@
24282694checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
24292695
24302696[[package]]
2697name = "lru-slab"
2698version = "0.1.2"
2699source = "registry+https://github.com/rust-lang/crates.io-index"
2700checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
2701
2702[[package]]
24312703name = "mac_address"
24322704version = "1.1.8"
24332705source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2439,6 +2711,23 @@
24392711]
24402712
24412713[[package]]
2714name = "mailbaby"
2715version = "0.3.0"
2716source = "git+https://github.com/mailbabys/mailbaby-rs?rev=e408acf48a19e4fd554b020a865d144706f7c2de#e408acf48a19e4fd554b020a865d144706f7c2de"
2717dependencies = [
2718 "base64 0.23.1",
2719 "prost",
2720 "reqwest",
2721 "serde",
2722 "serde_json",
2723 "thiserror 2.0.20",
2724 "tokio",
2725 "tonic",
2726 "tonic-prost",
2727 "tonic-prost-build",
2728]
2729
2730[[package]]
24422731name = "matches"
24432732version = "0.1.10"
24442733source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2549,6 +2838,12 @@
25492838]
25502839
25512840[[package]]
2841name = "multimap"
2842version = "0.10.1"
2843source = "registry+https://github.com/rust-lang/crates.io-index"
2844checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084"
2845
2846[[package]]
25522847name = "native-tls"
25532848version = "0.2.18"
25542849source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2836,6 +3131,17 @@
28363131checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
28373132
28383133[[package]]
3134name = "petgraph"
3135version = "0.8.3"
3136source = "registry+https://github.com/rust-lang/crates.io-index"
3137checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455"
3138dependencies = [
3139 "fixedbitset",
3140 "hashbrown 0.15.5",
3141 "indexmap",
3142]
3143
3144[[package]]
28393145name = "pgvector"
28403146version = "0.4.2"
28413147source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2845,6 +3151,26 @@
28453151]
28463152
28473153[[package]]
3154name = "pin-project"
3155version = "1.1.13"
3156source = "registry+https://github.com/rust-lang/crates.io-index"
3157checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924"
3158dependencies = [
3159 "pin-project-internal",
3160]
3161
3162[[package]]
3163name = "pin-project-internal"
3164version = "1.1.13"
3165source = "registry+https://github.com/rust-lang/crates.io-index"
3166checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b"
3167dependencies = [
3168 "proc-macro2",
3169 "quote",
3170 "syn 2.0.119",
3171]
3172
3173[[package]]
28483174name = "pin-project-lite"
28493175version = "0.2.17"
28503176source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2922,6 +3248,16 @@
29223248]
29233249
29243250[[package]]
3251name = "prettyplease"
3252version = "0.2.37"
3253source = "registry+https://github.com/rust-lang/crates.io-index"
3254checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
3255dependencies = [
3256 "proc-macro2",
3257 "syn 2.0.119",
3258]
3259
3260[[package]]
29253261name = "proc-macro-crate"
29263262version = "3.5.0"
29273263source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2953,6 +3289,59 @@
29533289]
29543290
29553291[[package]]
3292name = "prost"
3293version = "0.14.4"
3294source = "registry+https://github.com/rust-lang/crates.io-index"
3295checksum = "528ac67416ff8646872a3c02cad9cc4ee5dc9f9540c9b10771855c95cb2e5ae1"
3296dependencies = [
3297 "bytes",
3298 "prost-derive",
3299]
3300
3301[[package]]
3302name = "prost-build"
3303version = "0.14.4"
3304source = "registry+https://github.com/rust-lang/crates.io-index"
3305checksum = "03da047801ff44bb6a4d407d4860c05fd70bb81714e6b2f3812603d5b145b042"
3306dependencies = [
3307 "heck 0.4.1",
3308 "itertools",
3309 "log 0.4.33",
3310 "multimap",
3311 "petgraph",
3312 "prettyplease",
3313 "prost",
3314 "prost-types",
3315 "pulldown-cmark",
3316 "pulldown-cmark-to-cmark",
3317 "regex",
3318 "syn 2.0.119",
3319 "tempfile",
3320]
3321
3322[[package]]
3323name = "prost-derive"
3324version = "0.14.4"
3325source = "registry+https://github.com/rust-lang/crates.io-index"
3326checksum = "b570b25f7617e43d59005d0990ccb79e950a423952cea19671b7a876da390adf"
3327dependencies = [
3328 "anyhow",
3329 "itertools",
3330 "proc-macro2",
3331 "quote",
3332 "syn 2.0.119",
3333]
3334
3335[[package]]
3336name = "prost-types"
3337version = "0.14.4"
3338source = "registry+https://github.com/rust-lang/crates.io-index"
3339checksum = "f94967dc7688f3054c7fac87473ffae4cc4c3904800e2d9f5b857246d8963b0a"
3340dependencies = [
3341 "prost",
3342]
3343
3344[[package]]
29563345name = "psm"
29573346version = "0.1.32"
29583347source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2983,6 +3372,26 @@
29833372]
29843373
29853374[[package]]
3375name = "pulldown-cmark"
3376version = "0.13.4"
3377source = "registry+https://github.com/rust-lang/crates.io-index"
3378checksum = "e9f068eba8e7071c5f9511831b44f32c740d5adf574e990f946ddb53db2f314e"
3379dependencies = [
3380 "bitflags",
3381 "memchr",
3382 "unicase 2.9.0",
3383]
3384
3385[[package]]
3386name = "pulldown-cmark-to-cmark"
3387version = "22.0.1"
3388source = "registry+https://github.com/rust-lang/crates.io-index"
3389checksum = "ab1ad36992cead65f02aa399a373a42730922f1525d988172634fdefdecb8a60"
3390dependencies = [
3391 "pulldown-cmark",
3392]
3393
3394[[package]]
29863395name = "quick-xml"
29873396version = "0.28.2"
29883397source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2993,6 +3402,63 @@
29933402]
29943403
29953404[[package]]
3405name = "quinn"
3406version = "0.11.11"
3407source = "registry+https://github.com/rust-lang/crates.io-index"
3408checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8"
3409dependencies = [
3410 "bytes",
3411 "cfg_aliases",
3412 "pin-project-lite",
3413 "quinn-proto",
3414 "quinn-udp",
3415 "rustc-hash",
3416 "rustls",
3417 "socket2 0.5.10",
3418 "thiserror 2.0.20",
3419 "tokio",
3420 "tracing",
3421 "web-time",
3422]
3423
3424[[package]]
3425name = "quinn-proto"
3426version = "0.11.16"
3427source = "registry+https://github.com/rust-lang/crates.io-index"
3428checksum = "2f4bfc015262b9df63c8845072ce59068853ff5872180c2ce2f13038b970e560"
3429dependencies = [
3430 "aws-lc-rs",
3431 "bytes",
3432 "getrandom 0.4.3",
3433 "lru-slab",
3434 "rand 0.10.2",
3435 "rand_pcg",
3436 "ring",
3437 "rustc-hash",
3438 "rustls",
3439 "rustls-pki-types",
3440 "slab",
3441 "thiserror 2.0.20",
3442 "tinyvec",
3443 "tracing",
3444 "web-time",
3445]
3446
3447[[package]]
3448name = "quinn-udp"
3449version = "0.5.15"
3450source = "registry+https://github.com/rust-lang/crates.io-index"
3451checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694"
3452dependencies = [
3453 "cfg_aliases",
3454 "libc",
3455 "once_cell",
3456 "socket2 0.5.10",
3457 "tracing",
3458 "windows-sys 0.52.0",
3459]
3460
3461[[package]]
29963462name = "quote"
29973463version = "1.0.47"
29983464source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3021,12 +3487,25 @@
30213487
30223488[[package]]
30233489name = "rand"
3490version = "0.7.3"
3491source = "registry+https://github.com/rust-lang/crates.io-index"
3492checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03"
3493dependencies = [
3494 "getrandom 0.1.16",
3495 "libc",
3496 "rand_chacha 0.2.2",
3497 "rand_core 0.5.1",
3498 "rand_hc",
3499]
3500
3501[[package]]
3502name = "rand"
30243503version = "0.8.7"
30253504source = "registry+https://github.com/rust-lang/crates.io-index"
30263505checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a"
30273506dependencies = [
30283507 "libc",
3029 "rand_chacha",
3508 "rand_chacha 0.3.1",
30303509 "rand_core 0.6.4",
30313510]
30323511
@@ -3043,6 +3522,16 @@
30433522
30443523[[package]]
30453524name = "rand_chacha"
3525version = "0.2.2"
3526source = "registry+https://github.com/rust-lang/crates.io-index"
3527checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402"
3528dependencies = [
3529 "ppv-lite86",
3530 "rand_core 0.5.1",
3531]
3532
3533[[package]]
3534name = "rand_chacha"
30463535version = "0.3.1"
30473536source = "registry+https://github.com/rust-lang/crates.io-index"
30483537checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
@@ -3053,6 +3542,15 @@
30533542
30543543[[package]]
30553544name = "rand_core"
3545version = "0.5.1"
3546source = "registry+https://github.com/rust-lang/crates.io-index"
3547checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19"
3548dependencies = [
3549 "getrandom 0.1.16",
3550]
3551
3552[[package]]
3553name = "rand_core"
30563554version = "0.6.4"
30573555source = "registry+https://github.com/rust-lang/crates.io-index"
30583556checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
@@ -3067,6 +3565,24 @@
30673565checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69"
30683566
30693567[[package]]
3568name = "rand_hc"
3569version = "0.2.0"
3570source = "registry+https://github.com/rust-lang/crates.io-index"
3571checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c"
3572dependencies = [
3573 "rand_core 0.5.1",
3574]
3575
3576[[package]]
3577name = "rand_pcg"
3578version = "0.10.2"
3579source = "registry+https://github.com/rust-lang/crates.io-index"
3580checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a"
3581dependencies = [
3582 "rand_core 0.10.1",
3583]
3584
3585[[package]]
30703586name = "redis"
30713587version = "1.6.0"
30723588source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3156,6 +3672,47 @@
31563672]
31573673
31583674[[package]]
3675name = "reqwest"
3676version = "0.13.4"
3677source = "registry+https://github.com/rust-lang/crates.io-index"
3678checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3"
3679dependencies = [
3680 "base64 0.22.1",
3681 "bytes",
3682 "encoding_rs",
3683 "futures-core",
3684 "h2 0.4.16",
3685 "http 1.5.0",
3686 "http-body 1.1.0",
3687 "http-body-util",
3688 "hyper 1.11.0",
3689 "hyper-rustls",
3690 "hyper-util",
3691 "js-sys",
3692 "log 0.4.33",
3693 "mime 0.3.17",
3694 "percent-encoding 2.3.2",
3695 "pin-project-lite",
3696 "quinn",
3697 "rustls",
3698 "rustls-pki-types",
3699 "rustls-platform-verifier",
3700 "serde",
3701 "serde_json",
3702 "serde_urlencoded",
3703 "sync_wrapper",
3704 "tokio",
3705 "tokio-rustls",
3706 "tower",
3707 "tower-http",
3708 "tower-service",
3709 "url 2.5.8",
3710 "wasm-bindgen",
3711 "wasm-bindgen-futures",
3712 "web-sys",
3713]
3714
3715[[package]]
31593716name = "ring"
31603717version = "0.17.14"
31613718source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3262,6 +3819,12 @@
32623819]
32633820
32643821[[package]]
3822name = "rustc-hash"
3823version = "2.1.3"
3824source = "registry+https://github.com/rust-lang/crates.io-index"
3825checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d"
3826
3827[[package]]
32653828name = "rustc_version"
32663829version = "0.4.1"
32673830source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3284,27 +3847,69 @@
32843847]
32853848
32863849[[package]]
3287name = "rustls"
3288version = "0.23.43"
3850name = "rustls"
3851version = "0.23.43"
3852source = "registry+https://github.com/rust-lang/crates.io-index"
3853checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06"
3854dependencies = [
3855 "aws-lc-rs",
3856 "log 0.4.33",
3857 "once_cell",
3858 "ring",
3859 "rustls-pki-types",
3860 "rustls-webpki",
3861 "subtle",
3862 "zeroize",
3863]
3864
3865[[package]]
3866name = "rustls-native-certs"
3867version = "0.8.4"
3868source = "registry+https://github.com/rust-lang/crates.io-index"
3869checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d"
3870dependencies = [
3871 "openssl-probe",
3872 "rustls-pki-types",
3873 "schannel",
3874 "security-framework",
3875]
3876
3877[[package]]
3878name = "rustls-pki-types"
3879version = "1.15.1"
3880source = "registry+https://github.com/rust-lang/crates.io-index"
3881checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96"
3882dependencies = [
3883 "web-time",
3884 "zeroize",
3885]
3886
3887[[package]]
3888name = "rustls-platform-verifier"
3889version = "0.7.0"
32893890source = "registry+https://github.com/rust-lang/crates.io-index"
3290checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06"
3891checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0"
32913892dependencies = [
3893 "core-foundation 0.10.1",
3894 "core-foundation-sys",
3895 "jni",
3896 "log 0.4.33",
32923897 "once_cell",
3293 "ring",
3294 "rustls-pki-types",
3898 "rustls",
3899 "rustls-native-certs",
3900 "rustls-platform-verifier-android",
32953901 "rustls-webpki",
3296 "subtle",
3297 "zeroize",
3902 "security-framework",
3903 "security-framework-sys",
3904 "webpki-root-certs",
3905 "windows-sys 0.52.0",
32983906]
32993907
33003908[[package]]
3301name = "rustls-pki-types"
3302version = "1.15.1"
3909name = "rustls-platform-verifier-android"
3910version = "0.1.1"
33033911source = "registry+https://github.com/rust-lang/crates.io-index"
3304checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96"
3305dependencies = [
3306 "zeroize",
3307]
3912checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f"
33083913
33093914[[package]]
33103915name = "rustls-webpki"
@@ -3312,6 +3917,7 @@
33123917source = "registry+https://github.com/rust-lang/crates.io-index"
33133918checksum = "0527518605e68109d875e248ea259b6758801cf165e4b2c2733ae3b51f12535a"
33143919dependencies = [
3920 "aws-lc-rs",
33153921 "ring",
33163922 "rustls-pki-types",
33173923 "untrusted",
@@ -3353,6 +3959,15 @@
33533959checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072"
33543960
33553961[[package]]
3962name = "same-file"
3963version = "1.0.6"
3964source = "registry+https://github.com/rust-lang/crates.io-index"
3965checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
3966dependencies = [
3967 "winapi-util",
3968]
3969
3970[[package]]
33563971name = "schannel"
33573972version = "0.1.29"
33583973source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3520,7 +4135,7 @@
35204135checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
35214136dependencies = [
35224137 "bitflags",
3523 "core-foundation",
4138 "core-foundation 0.10.1",
35244139 "core-foundation-sys",
35254140 "libc",
35264141 "security-framework-sys",
@@ -3630,6 +4245,9 @@
36304245 "config",
36314246 "export",
36324247 "redis",
4248 "serde",
4249 "tracing",
4250 "uuid",
36334251]
36344252
36354253[[package]]
@@ -3688,6 +4306,16 @@
36884306]
36894307
36904308[[package]]
4309name = "simd_cesu8"
4310version = "1.2.0"
4311source = "registry+https://github.com/rust-lang/crates.io-index"
4312checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520"
4313dependencies = [
4314 "rustc_version",
4315 "simdutf8",
4316]
4317
4318[[package]]
36914319name = "simdutf8"
36924320version = "0.1.5"
36934321source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4044,6 +4672,9 @@
40444672version = "1.0.2"
40454673source = "registry+https://github.com/rust-lang/crates.io-index"
40464674checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
4675dependencies = [
4676 "futures-core",
4677]
40474678
40484679[[package]]
40494680name = "synstructure"
@@ -4057,6 +4688,27 @@
40574688]
40584689
40594690[[package]]
4691name = "system-configuration"
4692version = "0.7.0"
4693source = "registry+https://github.com/rust-lang/crates.io-index"
4694checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b"
4695dependencies = [
4696 "bitflags",
4697 "core-foundation 0.9.4",
4698 "system-configuration-sys",
4699]
4700
4701[[package]]
4702name = "system-configuration-sys"
4703version = "0.6.0"
4704source = "registry+https://github.com/rust-lang/crates.io-index"
4705checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4"
4706dependencies = [
4707 "core-foundation-sys",
4708 "libc",
4709]
4710
4711[[package]]
40604712name = "tagptr"
40614713version = "0.2.0"
40624714source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4236,6 +4888,16 @@
42364888]
42374889
42384890[[package]]
4891name = "tokio-rustls"
4892version = "0.26.4"
4893source = "registry+https://github.com/rust-lang/crates.io-index"
4894checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61"
4895dependencies = [
4896 "rustls",
4897 "tokio",
4898]
4899
4900[[package]]
42394901name = "tokio-stream"
42404902version = "0.1.19"
42414903source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4291,6 +4953,75 @@
42914953]
42924954
42934955[[package]]
4956name = "tonic"
4957version = "0.14.6"
4958source = "registry+https://github.com/rust-lang/crates.io-index"
4959checksum = "ac2a5518c70fa84342385732db33fb3f44bc4cc748936eb5833d2df34d6445ef"
4960dependencies = [
4961 "async-trait",
4962 "axum",
4963 "base64 0.22.1",
4964 "bytes",
4965 "h2 0.4.16",
4966 "http 1.5.0",
4967 "http-body 1.1.0",
4968 "http-body-util",
4969 "hyper 1.11.0",
4970 "hyper-timeout",
4971 "hyper-util",
4972 "percent-encoding 2.3.2",
4973 "pin-project",
4974 "socket2 0.6.5",
4975 "sync_wrapper",
4976 "tokio",
4977 "tokio-rustls",
4978 "tokio-stream",
4979 "tower",
4980 "tower-layer",
4981 "tower-service",
4982 "tracing",
4983]
4984
4985[[package]]
4986name = "tonic-build"
4987version = "0.14.6"
4988source = "registry+https://github.com/rust-lang/crates.io-index"
4989checksum = "c68f61875ac5293cf72e6c8cf0158086428c82c37229e98c840878f1706b0322"
4990dependencies = [
4991 "prettyplease",
4992 "proc-macro2",
4993 "quote",
4994 "syn 2.0.119",
4995]
4996
4997[[package]]
4998name = "tonic-prost"
4999version = "0.14.6"
5000source = "registry+https://github.com/rust-lang/crates.io-index"
5001checksum = "50849f68853be452acf590cde0b146665b8d507b3b8af17261df47e02c209ea0"
5002dependencies = [
5003 "bytes",
5004 "prost",
5005 "tonic",
5006]
5007
5008[[package]]
5009name = "tonic-prost-build"
5010version = "0.14.6"
5011source = "registry+https://github.com/rust-lang/crates.io-index"
5012checksum = "654e5643eff75d7f8c99197ce1440ed19a3474eada74c12bbac488b2cafdae27"
5013dependencies = [
5014 "prettyplease",
5015 "proc-macro2",
5016 "prost-build",
5017 "prost-types",
5018 "quote",
5019 "syn 2.0.119",
5020 "tempfile",
5021 "tonic-build",
5022]
5023
5024[[package]]
42945025name = "tower"
42955026version = "0.5.3"
42965027source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4298,15 +5029,36 @@
42985029dependencies = [
42995030 "futures-core",
43005031 "futures-util",
5032 "indexmap",
43015033 "pin-project-lite",
5034 "slab",
43025035 "sync_wrapper",
43035036 "tokio",
5037 "tokio-util",
43045038 "tower-layer",
43055039 "tower-service",
43065040 "tracing",
43075041]
43085042
43095043[[package]]
5044name = "tower-http"
5045version = "0.6.11"
5046source = "registry+https://github.com/rust-lang/crates.io-index"
5047checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840"
5048dependencies = [
5049 "bitflags",
5050 "bytes",
5051 "futures-util",
5052 "http 1.5.0",
5053 "http-body 1.1.0",
5054 "pin-project-lite",
5055 "tower",
5056 "tower-layer",
5057 "tower-service",
5058 "url 2.5.8",
5059]
5060
5061[[package]]
43105062name = "tower-layer"
43115063version = "0.3.3"
43125064source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4384,6 +5136,12 @@
43845136]
43855137
43865138[[package]]
5139name = "unicase"
5140version = "2.9.0"
5141source = "registry+https://github.com/rust-lang/crates.io-index"
5142checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142"
5143
5144[[package]]
43875145name = "unicode-bidi"
43885146version = "0.3.18"
43895147source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4508,6 +5266,16 @@
45085266checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
45095267
45105268[[package]]
5269name = "walkdir"
5270version = "2.5.0"
5271source = "registry+https://github.com/rust-lang/crates.io-index"
5272checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
5273dependencies = [
5274 "same-file",
5275 "winapi-util",
5276]
5277
5278[[package]]
45115279name = "want"
45125280version = "0.3.1"
45135281source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4518,6 +5286,12 @@
45185286
45195287[[package]]
45205288name = "wasi"
5289version = "0.9.0+wasi-snapshot-preview1"
5290source = "registry+https://github.com/rust-lang/crates.io-index"
5291checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519"
5292
5293[[package]]
5294name = "wasi"
45215295version = "0.10.0+wasi-snapshot-preview1"
45225296source = "registry+https://github.com/rust-lang/crates.io-index"
45235297checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f"
@@ -4552,6 +5326,16 @@
45525326]
45535327
45545328[[package]]
5329name = "wasm-bindgen-futures"
5330version = "0.4.77"
5331source = "registry+https://github.com/rust-lang/crates.io-index"
5332checksum = "6b7777d5cc23d0e91404e53ce2d5e8ec7acae3026b16233dba62cd3246457950"
5333dependencies = [
5334 "js-sys",
5335 "wasm-bindgen",
5336]
5337
5338[[package]]
45555339name = "wasm-bindgen-macro"
45565340version = "0.2.127"
45575341source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4584,6 +5368,16 @@
45845368]
45855369
45865370[[package]]
5371name = "web-sys"
5372version = "0.3.104"
5373source = "registry+https://github.com/rust-lang/crates.io-index"
5374checksum = "c435338968042f4f59a557f690a253676d47ce13ceb55d70100e7facf6620a30"
5375dependencies = [
5376 "js-sys",
5377 "wasm-bindgen",
5378]
5379
5380[[package]]
45875381name = "web-time"
45885382version = "1.1.0"
45895383source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4594,6 +5388,15 @@
45945388]
45955389
45965390[[package]]
5391name = "webpki-root-certs"
5392version = "1.0.9"
5393source = "registry+https://github.com/rust-lang/crates.io-index"
5394checksum = "b96554aa2acc8ccdb7e1c9a58a7a68dd5d13bccc69cd124cb09406db612a1c9b"
5395dependencies = [
5396 "rustls-pki-types",
5397]
5398
5399[[package]]
45975400name = "webpki-roots"
45985401version = "1.0.9"
45995402source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4625,6 +5428,15 @@
46255428checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
46265429
46275430[[package]]
5431name = "winapi-util"
5432version = "0.1.11"
5433source = "registry+https://github.com/rust-lang/crates.io-index"
5434checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
5435dependencies = [
5436 "windows-sys 0.48.0",
5437]
5438
5439[[package]]
46285440name = "winapi-x86_64-pc-windows-gnu"
46295441version = "0.4.0"
46305442source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4672,6 +5484,17 @@
46725484checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
46735485
46745486[[package]]
5487name = "windows-registry"
5488version = "0.6.1"
5489source = "registry+https://github.com/rust-lang/crates.io-index"
5490checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720"
5491dependencies = [
5492 "windows-link",
5493 "windows-result",
5494 "windows-strings",
5495]
5496
5497[[package]]
46755498name = "windows-result"
46765499version = "0.4.1"
46775500source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4877,6 +5700,12 @@
48775700]
48785701
48795702[[package]]
5703name = "xorfilter-rs"
5704version = "0.5.1"
5705source = "registry+https://github.com/rust-lang/crates.io-index"
5706checksum = "47f9da296a88b6bc150b896d17770a62d4dc6f63ecf0ed10a9c08a1cb3d12f24"
5707
5708[[package]]
48805709name = "xxhash-rust"
48815710version = "0.8.18"
48825711source = "registry+https://github.com/rust-lang/crates.io-index"
Modified Cargo.toml View Cargo.toml
@@ -1,5 +1,5 @@
11[workspace]
2members = ["lib/cache",
2members = ["lib/auth","lib/cache",
33 "lib/config", "lib/export",
44 "lib/models",
55 "lib/session",
@@ -23,7 +23,8 @@
2323serde = "1.0.229"
2424serde_json = "1.0.151"
2525axum = "0.8.9"
26axum_session = "0.21.0"
26axum_session = { version = "0.21.0", features = ["key-store"] }
27tracing = "0.1"
2728rust-s3-async = "0.35.0"
2829redis = { version = "1.5.0", features = ["aio", "tokio-comp", "connection-manager", "cluster-async"] }
2930sea-orm = { version = "2.0.1", features = ["sqlx-postgres", "runtime-tokio-rustls"] }