feat: RepoDto add http_clone_url and ssh url feat: Avatar let it use s3 url
ZhenYi committed
This change is very large, so some diffs are not shown. Clone the repository to see the full change.
Modified config/default.toml config/default.toml +4 −0 View config/default.toml
@@ -186,6 +186,10 @@
186186git_command_timeout_secs = 300
187187git_rpc_timeout_secs = 3600
188188base_url = "http://localhost:3000"
189http_clone_base_url = ""
190ssh_clone_host = ""
191ssh_clone_port = 0
192ssh_clone_user = "git"
189193graphql_complexity_limit = 1000
190194graphql_introspection = true
191195
Modified lib/auth/src/services/email.rs lib/auth/src/services/email.rs +17 −2 View lib/auth/src/services/email.rs
@@ -291,6 +291,8 @@
291291 &self.config().web.web_base_url,
292292 &lower_email,
293293 &artifacts.temp_id.to_string(),
294 &artifacts.code,
295 cmd.purpose,
294296 );
295297 let html =
296298 crate::templates::mail_verify_code(&artifacts.code, minutes, subject, link.as_deref());
@@ -349,6 +351,8 @@
349351 &self.config().web.web_base_url,
350352 &lower_email,
351353 &artifacts.temp_id.to_string(),
354 &artifacts.code,
355 EmailVerificationPurpose::Reclaim,
352356 );
353357 let html = crate::templates::mail_verify_code(
354358 &artifacts.code,
@@ -597,15 +601,26 @@
597601 }
598602}
599603
600pub(super) fn build_verify_link(base_url: &str, email: &str, temp_id: &str) -> Option<String> {
604/// Verification links must carry the one-time `code` and the `purpose` slug:
605/// the web page confirms by submitting both, and defaults `purpose` to
606/// `signup` when it is absent, which would reject non-signup flows.
607pub(super) fn build_verify_link(
608 base_url: &str,
609 email: &str,
610 temp_id: &str,
611 code: &str,
612 purpose: EmailVerificationPurpose,
613) -> Option<String> {
601614 if base_url.is_empty() {
602615 return None;
603616 }
604617 let base = base_url.trim_end_matches('/');
605618 Some(format!(
606 "{base}/auth/verify-email?email={}&temp_id={}",
619 "{base}/auth/verify-email?email={}&temp_id={}&code={}&purpose={}",
607620 percent_encode(email),
608621 percent_encode(temp_id),
622 percent_encode(code),
623 purpose_str(purpose),
609624 ))
610625}
611626
Modified lib/auth/src/services/register.rs lib/auth/src/services/register.rs +2 −0 View lib/auth/src/services/register.rs
@@ -157,6 +157,8 @@
157157 &self.config().web.web_base_url,
158158 &lower_email,
159159 &artifacts.temp_id.to_string(),
160 &artifacts.code,
161 models::users::user_email_verification_codes::EmailVerificationPurpose::Signup,
160162 );
161163 let html = crate::templates::mail_verify_code(
162164 &artifacts.code,
Modified lib/config/src/git.rs lib/config/src/git.rs +8 −0 View lib/config/src/git.rs
@@ -36,6 +36,10 @@
3636 pub sync: SyncSettings,
3737 pub ssh: GitSshConfig,
3838 pub base_url: String,
39 pub http_clone_base_url: String,
40 pub ssh_clone_host: String,
41 pub ssh_clone_port: u16,
42 pub ssh_clone_user: String,
3943 pub graphql_complexity_limit: u64,
4044 pub graphql_introspection: bool,
4145}
@@ -51,6 +55,10 @@
5155 sync: SyncSettings::default(),
5256 ssh: GitSshConfig::default(),
5357 base_url: "http://localhost:3000".to_string(),
58 http_clone_base_url: String::new(),
59 ssh_clone_host: String::new(),
60 ssh_clone_port: 0,
61 ssh_clone_user: "git".to_string(),
5462 graphql_complexity_limit: 1000,
5563 graphql_introspection: false,
5664 }
Modified lib/email/src/templates/verify_code.rs lib/email/src/templates/verify_code.rs +10 −4 View lib/email/src/templates/verify_code.rs
@@ -43,8 +43,14 @@
4343 let base_url = vars.get("base_url").and_then(|v| v.as_str())?;
4444 let email = vars.get("email").and_then(|v| v.as_str())?;
4545 let temp_id = vars.get("temp_id").and_then(|v| v.as_str())?;
46 Some(append_query(
47 &url_join(base_url, "/auth/verify-email"),
48 &[("email", email), ("temp_id", temp_id)],
49 ))
46 let code = vars.get("code").and_then(|v| v.as_str()).unwrap_or("");
47 let mut query = vec![("email", email), ("temp_id", temp_id), ("code", code)];
48 // Optional slug (signup / password_reset / ...); the human-readable
49 // `purpose` var is a heading and must not leak into the URL.
50 let purpose_slug;
51 if let Some(slug) = vars.get("purpose_slug").and_then(|v| v.as_str()) {
52 purpose_slug = slug;
53 query.push(("purpose", purpose_slug));
54 }
55 Some(append_query(&url_join(base_url, "/auth/verify-email"), &query))
5056}
Modified lib/git/src/handler/html/mod.rs lib/git/src/handler/html/mod.rs +6 −27 View lib/git/src/handler/html/mod.rs
@@ -312,38 +312,17 @@
312312}
313313
314314pub fn clone_https_url(service: &GitService, space: &str, repo: &str) -> (String, bool) {
315 let base = &service.state.config.git.base_url;
316 if base.is_empty() {
317 return (String::new(), true);
315 match service.http_clone_url(space, repo) {
316 Some(url) => (url, false),
317 None => (String::new(), true),
318318 }
319 let mut url = base.trim_end_matches('/').to_string();
320 url.push('/');
321 url.push_str(space);
322 url.push('/');
323 url.push_str(repo);
324 url.push_str(".git");
325 (url, false)
326319}
327320
328321pub fn clone_ssh_url(service: &GitService, space: &str, repo: &str) -> (String, bool) {
329 let base = &service.state.config.git.base_url;
330 if base.is_empty() {
331 return (String::new(), true);
332 }
333 let scheme_stripped = base
334 .trim_start_matches("https://")
335 .trim_start_matches("http://");
336 let host = scheme_stripped.split('/').next().unwrap_or("");
337 let port = service.state.config.git.ssh.port;
338 if host.is_empty() {
339 return (String::new(), true);
322 match service.ssh_clone_url(space, repo) {
323 Some(url) => (url, false),
324 None => (String::new(), true),
340325 }
341 let url = if port == 22 {
342 format!("git@{host}:{space}/{repo}.git")
343 } else {
344 format!("ssh://git@{host}:{port}/{space}/{repo}.git")
345 };
346 (url, false)
347326}
348327
349328pub fn empty_state(dto: &RepoDto) -> bool {
Modified lib/git/src/service/repos.rs lib/git/src/service/repos.rs +79 −5 View lib/git/src/service/repos.rs
@@ -49,6 +49,8 @@
4949 pub primary_language: Option<String>,
5050 pub license_name: Option<String>,
5151 pub object_format: String,
52 pub http_clone_url: Option<String>,
53 pub ssh_clone_url: Option<String>,
5254 pub created_at: OffsetDateTime,
5355 pub updated_at: OffsetDateTime,
5456 pub last_pushed_at: Option<OffsetDateTime>,
@@ -133,6 +135,8 @@
133135 primary_language: m.primary_language,
134136 license_name: m.license_name,
135137 object_format: m.object_format,
138 http_clone_url: None,
139 ssh_clone_url: None,
136140 created_at: m.created_at,
137141 updated_at: m.updated_at,
138142 last_pushed_at: m.last_pushed_at,
@@ -176,6 +180,70 @@
176180}
177181
178182impl GitService {
183 pub fn http_clone_url(&self, space: &str, repo: &str) -> Option<String> {
184 let cfg = &self.state.config.git;
185 let base = if cfg.http_clone_base_url.is_empty() {
186 cfg.base_url.as_str()
187 } else {
188 cfg.http_clone_base_url.as_str()
189 };
190 if base.is_empty() {
191 return None;
192 }
193 Some(format!(
194 "{}/{space}/{repo}.git",
195 base.trim_end_matches('/')
196 ))
197 }
198
199 pub fn ssh_clone_url(&self, space: &str, repo: &str) -> Option<String> {
200 let cfg = &self.state.config.git;
201 let host = if cfg.ssh_clone_host.is_empty() {
202 cfg.base_url
203 .trim_start_matches("https://")
204 .trim_start_matches("http://")
205 .split('/')
206 .next()
207 .unwrap_or("")
208 .to_string()
209 } else {
210 cfg.ssh_clone_host.clone()
211 };
212 if host.is_empty() {
213 return None;
214 }
215 let port = if cfg.ssh_clone_port == 0 {
216 cfg.ssh.port
217 } else {
218 cfg.ssh_clone_port
219 };
220 let user = if cfg.ssh_clone_user.is_empty() {
221 "git"
222 } else {
223 cfg.ssh_clone_user.as_str()
224 };
225 Some(if port == 22 {
226 format!("{user}@{host}:{space}/{repo}.git")
227 } else {
228 format!("ssh://{user}@{host}:{port}/{space}/{repo}.git")
229 })
230 }
231
232 pub fn fill_clone_urls(&self, dto: &mut RepoDto, space_name: &str) {
233 let repo_name = dto.name.clone();
234 dto.http_clone_url = self.http_clone_url(space_name, &repo_name);
235 dto.ssh_clone_url = self.ssh_clone_url(space_name, &repo_name);
236 }
237
238 pub async fn repo_dto(&self, m: repos::Model) -> Result<RepoDto, GitError> {
239 let space = crate::share::access::space_for_dir(self.db_read(), m.dir_id)
240 .await?
241 .ok_or(GitError::NotFound)?;
242 let mut dto = RepoDto::from(m);
243 self.fill_clone_urls(&mut dto, &space.name);
244 Ok(dto)
245 }
246
179247 async fn lookup_space(&self, space_name: &str) -> Result<spaces::Model, GitError> {
180248 let lower = space_name.trim().to_lowercase();
181249 if lower.is_empty() {
@@ -372,7 +440,9 @@
372440 .one(self.db_read())
373441 .await?
374442 .ok_or(GitError::NotFound)?;
375 Ok(RepoDto::from(fresh))
443 let mut dto = RepoDto::from(fresh);
444 self.fill_clone_urls(&mut dto, &space.name);
445 Ok(dto)
376446 }
377447
378448 pub async fn get_repo(
@@ -381,7 +451,7 @@
381451 repo_name: String,
382452 ) -> Result<RepoDto, GitError> {
383453 let ctx = self.context(space_name, repo_name).await?;
384 Ok(RepoDto::from(ctx.model))
454 self.repo_dto(ctx.model).await
385455 }
386456
387457 pub async fn repo_viewer_state(
@@ -413,7 +483,7 @@
413483 &self,
414484 ctx: std::sync::Arc<crate::context::facade::GitContext>,
415485 ) -> Result<RepoDto, GitError> {
416 Ok(RepoDto::from(ctx.model.clone()))
486 self.repo_dto(ctx.model.clone()).await
417487 }
418488
419489 pub async fn list_repos(
@@ -528,7 +598,9 @@
528598 return Err(map_baby(e));
529599 }
530600 }
531 Ok(RepoDto::from(saved))
601 let mut dto = RepoDto::from(saved);
602 self.fill_clone_urls(&mut dto, &space.name);
603 Ok(dto)
532604 }
533605
534606 pub async fn set_repo_archived(
@@ -559,7 +631,9 @@
559631 ..Default::default()
560632 };
561633 let saved = am.update(self.db_write()).await?;
562 Ok(RepoDto::from(saved))
634 let mut dto = RepoDto::from(saved);
635 self.fill_clone_urls(&mut dto, &space.name);
636 Ok(dto)
563637 }
564638
565639 async fn repo_space(&self, ctx: &GitContext) -> Result<spaces::Model, GitError> {
Modified lib/models/schema_pre_sync.sql lib/models/schema_pre_sync.sql +26 −7 View lib/models/schema_pre_sync.sql
@@ -17,10 +17,29 @@
1717DROP INDEX IF EXISTS idx_chat_draft_unique;
1818
1919-- idempotent column adds for phase-2 model changes (sync layer cannot
20-- express NOT NULL DEFAULT additions on populated tables)
21ALTER TABLE room_members ADD COLUMN IF NOT EXISTS role SMALLINT NOT NULL DEFAULT 0;
22ALTER TABLE issue ADD COLUMN IF NOT EXISTS priority SMALLINT NOT NULL DEFAULT 0;
23ALTER TABLE issue ADD COLUMN IF NOT EXISTS due_at TIMESTAMP WITH TIME ZONE;
24ALTER TABLE user_access_key ADD COLUMN IF NOT EXISTS expires_at TIMESTAMP WITH TIME ZONE;
25ALTER TABLE user_access_key ADD COLUMN IF NOT EXISTS last_used_at TIMESTAMP WITH TIME ZONE;
26ALTER TABLE chat_message_reminder ADD COLUMN IF NOT EXISTS note TEXT;
20-- express NOT NULL DEFAULT additions on populated tables).
21-- Guarded with to_regclass: on a fresh database the tables do not exist yet
22-- (they are created by the registry sync right after, with these columns
23-- already included), so the ALTERs must be skipped instead of failing.
24DO $$ BEGIN
25 IF to_regclass('room_members') IS NOT NULL THEN
26 ALTER TABLE room_members ADD COLUMN IF NOT EXISTS role SMALLINT NOT NULL DEFAULT 0;
27 END IF;
28END $$;
29DO $$ BEGIN
30 IF to_regclass('issue') IS NOT NULL THEN
31 ALTER TABLE issue ADD COLUMN IF NOT EXISTS priority SMALLINT NOT NULL DEFAULT 0;
32 ALTER TABLE issue ADD COLUMN IF NOT EXISTS due_at TIMESTAMP WITH TIME ZONE;
33 END IF;
34END $$;
35DO $$ BEGIN
36 IF to_regclass('user_access_key') IS NOT NULL THEN
37 ALTER TABLE user_access_key ADD COLUMN IF NOT EXISTS expires_at TIMESTAMP WITH TIME ZONE;
38 ALTER TABLE user_access_key ADD COLUMN IF NOT EXISTS last_used_at TIMESTAMP WITH TIME ZONE;
39 END IF;
40END $$;
41DO $$ BEGIN
42 IF to_regclass('chat_message_reminder') IS NOT NULL THEN
43 ALTER TABLE chat_message_reminder ADD COLUMN IF NOT EXISTS note TEXT;
44 END IF;
45END $$;
Modified lib/user/src/handlers/get_user_avatar.rs lib/user/src/handlers/get_user_avatar.rs +11 −9 View lib/user/src/handlers/get_user_avatar.rs
@@ -1,5 +1,5 @@
11use axum::extract::{Path, State};
2use axum::http::header;
2use axum::http::{StatusCode, header};
33use axum::response::Response;
44use session::Session;
55use session::share::session::require_uid;
@@ -12,10 +12,10 @@
1212 get,
1313 path = "/users/{user_id}/avatar",
1414 tag = "users",
15 description = "按目标用户可见性规则以图片流返回头像。",
15 description = "按目标用户可见性规则重定向到 S3 预签名头像链接(7 天有效, Redis 缓存)。",
1616 params(("user_id" = Uuid, Path, description = "user id")),
1717 responses(
18 (status = 200, description = "avatar image bytes", content_type = "image/*"),
18 (status = 302, description = "redirect to presigned s3 avatar url"),
1919 (status = 404, description = "no avatar or user not visible", body = export::rest::AppResponseData<export::rest::NoData>),
2020 (status = 401, description = "authentication required", body = export::rest::AppResponseData<export::rest::NoData>)
2121 )
@@ -25,12 +25,14 @@
2525 session: Session,
2626 Path(user_id): Path<Uuid>,
2727) -> Result<Response, ApiError> {
28 let caller = require_uid(&session).map_err(|_| ApiError::Unauthorized(export::rest::AppResponseData::err(401, "unauthorized")))?;
29 let (content_type, body) = service.avatar_stream(caller, user_id).await?;
28 let caller = require_uid(&session).map_err(|_| {
29 ApiError::Unauthorized(export::rest::AppResponseData::err(401, "unauthorized"))
30 })?;
31 let url = service.avatar_redirect_url(caller, user_id).await?;
3032 Ok(Response::builder()
31 .status(200)
32 .header(header::CONTENT_TYPE, content_type)
33 .header(header::CACHE_CONTROL, "public, max-age=300")
34 .body(body)
33 .status(StatusCode::FOUND)
34 .header(header::LOCATION, url)
35 .header(header::CACHE_CONTROL, "private, max-age=300")
36 .body(axum::body::Body::empty())
3537 .unwrap())
3638}
Modified lib/user/src/handlers/mod.rs lib/user/src/handlers/mod.rs +2 −2 View lib/user/src/handlers/mod.rs
@@ -5,13 +5,13 @@
55pub mod delete_meme;
66pub mod delete_push_subscription;
77pub mod get_avatar;
8pub mod get_profile_detail;
9pub mod get_session;
810pub mod get_user_avatar;
9pub mod get_profile_detail;
1011pub mod list_emails;
1112pub mod list_memes;
1213pub mod list_push_subscriptions;
1314pub mod list_saved_messages;
14pub mod get_session;
1515pub mod list_sessions;
1616pub mod my_reports;
1717pub mod my_spaces;
Modified lib/user/src/services/avatar.rs lib/user/src/services/avatar.rs +74 −18 View lib/user/src/services/avatar.rs
@@ -1,5 +1,5 @@
1use models::users::user;
12use models::users::user::ProfileVisibility;
2use models::users::user;
33use sea_orm::{ActiveModelTrait, EntityTrait, Set};
44use serde::{Deserialize, Serialize};
55use time::OffsetDateTime;
@@ -11,7 +11,21 @@
1111
1212pub const MAX_AVATAR_BYTES: usize = 5 * 1024 * 1024;
1313pub const AVATAR_PREFIX: &str = "avatars";
14pub const AVATAR_URL_TTL_SECS: u64 = 7 * 24 * 60 * 60;
15pub const AVATAR_URL_CACHE_PREFIX: &str = "user:avatar:url";
16
17fn avatar_url_cache_key(uid: Uuid) -> String {
18 format!("{AVATAR_URL_CACHE_PREFIX}:{uid}")
19}
1420
21fn avatar_object_key(stored_url: &str) -> Option<String> {
22 let key = stored_url.strip_prefix("storage://")?;
23 if key.is_empty() {
24 return None;
25 }
26 Some(key.to_string())
27}
28
1529#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
1630#[serde(rename_all = "lowercase")]
1731pub enum AvatarFormat {
@@ -71,6 +85,45 @@
7185}
7286
7387impl UserService {
88 async fn presign_avatar_url(&self, uid: Uuid, stored_url: &str) -> Result<String, UserError> {
89 let cache_key = avatar_url_cache_key(uid);
90 match crate::share::redis::get_string(&self.state.redis, &cache_key).await {
91 Ok(Some(url)) => return Ok(url),
92 Ok(None) => {}
93 Err(error) => {
94 tracing::warn!(%error, "failed to read avatar url cache");
95 }
96 }
97 let key = avatar_object_key(stored_url).ok_or(UserError::NotFound)?;
98 let url = self
99 .storage()
100 .client()
101 .presign_get(&key, AVATAR_URL_TTL_SECS as u32)
102 .map_err(UserError::from)?;
103 if let Err(error) = crate::share::redis::set_string(
104 &self.state.redis,
105 &cache_key,
106 &url,
107 AVATAR_URL_TTL_SECS,
108 )
109 .await
110 {
111 tracing::warn!(%error, "failed to cache avatar presigned url");
112 }
113 Ok(url)
114 }
115
116 pub async fn avatar_presigned_url(&self, uid: Uuid) -> Result<Option<String>, UserError> {
117 let user = user::Entity::find_by_id(uid)
118 .one(self.db().read())
119 .await?
120 .ok_or(UserError::NotFound)?;
121 if user.avatar_url.is_empty() {
122 return Ok(None);
123 }
124 Ok(Some(self.presign_avatar_url(uid, &user.avatar_url).await?))
125 }
126
74127 pub async fn upload_avatar_stream(
75128 &self,
76129 uid: Uuid,
@@ -109,6 +162,12 @@
109162 active.updated_at = Set(now);
110163 active.update(self.db().write()).await?;
111164
165 if let Err(error) =
166 crate::share::redis::del(&self.state.redis, &avatar_url_cache_key(uid)).await
167 {
168 tracing::warn!(%error, "failed to invalidate avatar url cache");
169 }
170
112171 let meta_key = format!("{AVATAR_PREFIX}/{uid}.meta.json");
113172 let meta = serde_json::json!({
114173 "uid": uid,
@@ -132,9 +191,10 @@
132191 }
133192 }
134193
194 let url = self.presign_avatar_url(uid, &stored_url).await?;
135195 Ok(AvatarDto {
136196 user_id: uid,
137 url: stored_url,
197 url,
138198 format: fmt,
139199 bytes: bytes.len() as u64,
140200 updated_at: now,
@@ -153,6 +213,12 @@
153213 active.updated_at = Set(now);
154214 active.update(self.db().write()).await?;
155215
216 if let Err(error) =
217 crate::share::redis::del(&self.state.redis, &avatar_url_cache_key(uid)).await
218 {
219 tracing::warn!(%error, "failed to invalidate avatar url cache");
220 }
221
156222 if old_url.starts_with(&format!("storage://{AVATAR_PREFIX}/")) {
157223 let rel = old_url
158224 .trim_start_matches("storage://")
@@ -170,11 +236,11 @@
170236 Ok(())
171237 }
172238
173 pub async fn avatar_stream(
239 pub async fn avatar_redirect_url(
174240 &self,
175241 caller: Uuid,
176242 target_uid: Uuid,
177 ) -> Result<(String, axum::body::Body), UserError> {
243 ) -> Result<String, UserError> {
178244 let target = user::Entity::find_by_id(target_uid)
179245 .one(self.db().read())
180246 .await?
@@ -188,19 +254,8 @@
188254 if target.avatar_url.is_empty() {
189255 return Err(UserError::NotFound);
190256 }
191 let ext = target.avatar_url.rsplit('.').next().unwrap_or_default();
192 let fmt = AvatarFormat::from_extension(ext).unwrap_or(AvatarFormat::Jpg);
193 let rel = target
194 .avatar_url
195 .trim_start_matches("storage://")
196 .trim_start_matches(AVATAR_PREFIX)
197 .trim_start_matches('/');
198 let key = format!("{AVATAR_PREFIX}/{rel}");
199 let stream = self.storage().client().get_stream(&key).await.map_err(UserError::from)?;
200 let stream = futures::StreamExt::map(stream, |item| {
201 item.map_err(|e| std::io::Error::other(e.to_string()))
202 });
203 Ok((fmt.content_type().to_string(), axum::body::Body::from_stream(stream)))
257 self.presign_avatar_url(target_uid, &target.avatar_url)
258 .await
204259 }
205260
206261 pub async fn current_avatar(&self, uid: Uuid) -> Result<Option<AvatarDto>, UserError> {
@@ -213,9 +268,10 @@
213268 }
214269 let ext = user.avatar_url.rsplit('.').next().unwrap_or_default();
215270 let fmt = AvatarFormat::from_extension(ext).unwrap_or(AvatarFormat::Jpg);
271 let url = self.presign_avatar_url(uid, &user.avatar_url).await?;
216272 Ok(Some(AvatarDto {
217273 user_id: uid,
218 url: user.avatar_url,
274 url,
219275 format: fmt,
220276 bytes: 0,
221277 updated_at: user.updated_at,
Modified lib/user/src/services/notifications.rs lib/user/src/services/notifications.rs +5 −1 View lib/user/src/services/notifications.rs
@@ -100,7 +100,11 @@
100100 Ok(rows.into_iter().map(RecentMentionDto::from).collect())
101101 }
102102
103 pub async fn my_reports(&self, uid: Uuid, q: PageQuery) -> Result<Vec<UserReportDto>, UserError> {
103 pub async fn my_reports(
104 &self,
105 uid: Uuid,
106 q: PageQuery,
107 ) -> Result<Vec<UserReportDto>, UserError> {
104108 let rows = user_reports::Entity::find()
105109 .filter(user_reports::Column::ReporterUid.eq(Some(uid)))
106110 .order_by_desc(user_reports::Column::CreatedAt)
Modified lib/user/src/services/profile.rs lib/user/src/services/profile.rs +25 −5 View lib/user/src/services/profile.rs
@@ -359,7 +359,11 @@
359359 user.uid,
360360 &user.avatar_url,
361361 ),
362 visibility: if is_owner { Some(user.visibility) } else { None },
362 visibility: if is_owner {
363 Some(user.visibility)
364 } else {
365 None
366 },
363367 real_name: gate(p.show_real_name, p.real_name),
364368 bio: gate(emit_activity, p.bio),
365369 pronouns: gate(emit_activity, p.pronouns),
@@ -369,9 +373,21 @@
369373 banner_url: gate(emit_activity, p.banner_url),
370374 accent_color: gate(emit_activity, p.accent_color),
371375 show_email: if is_owner { Some(p.show_email) } else { None },
372 show_location: if is_owner { Some(p.show_location) } else { None },
373 show_real_name: if is_owner { Some(p.show_real_name) } else { None },
374 show_activity: if is_owner { Some(p.show_activity) } else { None },
376 show_location: if is_owner {
377 Some(p.show_location)
378 } else {
379 None
380 },
381 show_real_name: if is_owner {
382 Some(p.show_real_name)
383 } else {
384 None
385 },
386 show_activity: if is_owner {
387 Some(p.show_activity)
388 } else {
389 None
390 },
375391 updated_at: p.updated_at,
376392 })
377393 } else {
@@ -383,7 +399,11 @@
383399 user.uid,
384400 &user.avatar_url,
385401 ),
386 visibility: if is_owner { Some(user.visibility) } else { None },
402 visibility: if is_owner {
403 Some(user.visibility)
404 } else {
405 None
406 },
387407 real_name: None,
388408 bio: None,
389409 pronouns: None,
Modified lib/user/src/share/redis.rs lib/user/src/share/redis.rs +5 −0 View lib/user/src/share/redis.rs
@@ -52,6 +52,11 @@
5252 })
5353}
5454
55pub async fn del(redis: &AppRedis, key: &str) -> Result<(), RedisError> {
56 let mut conn = redis.connection_clone();
57 dispatch!(conn, |c| redis::cmd("DEL").arg(key).exec_async(c).await)
58}
59
5560pub async fn getdel_string(redis: &AppRedis, key: &str) -> Result<Option<String>, RedisError> {
5661 let mut conn = redis.connection_clone();
5762 dispatch!(conn, |c| redis::cmd("GETDEL").arg(key).query_async(c).await)
Modified lib/user/src/openapi.rs lib/user/src/openapi.rs +1 −1 View lib/user/src/openapi.rs
@@ -10,7 +10,7 @@
1010use crate::services::avatar::{AvatarDto, AvatarFormat};
1111use crate::services::emails::{AddEmailCmd, AddEmailResp, EmailDto, VerifyAddEmailCmd};
1212use crate::services::notifications::{
13 RecentMentionDto, UserReportDto, SaveMessageCmd, SavedMessageDto,
13 RecentMentionDto, SaveMessageCmd, SavedMessageDto, UserReportDto,
1414};
1515use crate::services::profile::{
1616 ListLimitQuery, MemeDto, MySpaceDto, ProfileDetailDto, SpaceDto, UpsertMemeCmd,
Modified lib/user/src/routes.rs lib/user/src/routes.rs +10 −4 View lib/user/src/routes.rs
@@ -8,13 +8,13 @@
88use crate::handlers::delete_meme::delete_meme;
99use crate::handlers::delete_push_subscription::delete_push_subscription;
1010use crate::handlers::get_avatar::get_avatar;
11use crate::handlers::get_profile_detail::get_profile_detail;
12use crate::handlers::get_session::get_session;
1113use crate::handlers::get_user_avatar::get_user_avatar;
12use crate::handlers::get_profile_detail::get_profile_detail;
1314use crate::handlers::list_emails::list_emails;
1415use crate::handlers::list_memes::list_memes;
1516use crate::handlers::list_push_subscriptions::list_push_subscriptions;
1617use crate::handlers::list_saved_messages::list_saved_messages;
17use crate::handlers::get_session::get_session;
1818use crate::handlers::list_sessions::list_sessions;
1919use crate::handlers::my_reports::my_reports;
2020use crate::handlers::my_spaces::my_spaces;
@@ -46,7 +46,10 @@
4646 )
4747 .route("/me/spaces", get(my_spaces))
4848 .route("/me/memes", get(list_memes).post(create_meme))
49 .route("/me/memes/{meme_id}", put(crate::handlers::update_meme::update_meme))
49 .route(
50 "/me/memes/{meme_id}",
51 put(crate::handlers::update_meme::update_meme),
52 )
5053 .route("/me/memes/{meme_id}", delete(delete_meme))
5154 .route("/me/emails", get(list_emails).post(add_email))
5255 .route("/me/emails/verify", post(verify_add_email))
@@ -56,8 +59,11 @@
5659 "/me/sessions",
5760 get(list_sessions).delete(revoke_all_sessions),
5861 )
59 .route("/me/sessions/{session_id}", get(get_session).delete(revoke_session))
6062 .route(
63 "/me/sessions/{session_id}",
64 get(get_session).delete(revoke_session),
65 )
66 .route(
6167 "/me/push-subscriptions",
6268 get(list_push_subscriptions).post(upsert_push_subscription),
6369 )
Modified openapi.json openapi.json View openapi.json
Diff too large to display.