diff --git a/ROADMAP.md b/ROADMAP.md index 1beced0a..e808d7da 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -230,6 +230,7 @@ Order = user-visible value first; each ships with backend + UI + tests. - [x] SMS verification-code sign-in + phone binding — generic HTTP SMS gateway (`sms_*` settings: endpoint/method/headers/body template with `{phone}`/`{code}` placeholders, SSRF-guarded outbound call); `users.phone` unique optional column; KV-stored 6-digit codes (5-min TTL, single-use, 60s resend throttle) across `login`/`bind`/`reset` scenes; `POST /session/sms/send` (IP rate-limit + login-CAPTCHA gate) / `POST /session/sms/login` (auto-provisions synthetic `sms_*@sms.local` accounts when enabled, 2FA continuation preserved) / `POST /user/reset_sms` / `PUT|DELETE /user/setting/phone`; masked phone in user settings response; login page gains an SMS phase + reset-via-SMS mode in forgot password, security settings gain a phone-binding section, admin UserSession gains an SMS gateway accordion; en+zh locales - [x] Direct-link traffic packs (upstream #2178 item 11) — `users.dl_traffic` (bytes, `-1` = unlimited default preserving legacy behavior); `RedirectDirectLink` atomically charges the owner's balance by file size before issuing the signed entity URL (`CodeInsufficientTraffic` = 40094 on exhaustion, balance never goes negative); new `traffic` SKU + gift-code type top up the balance permanently (unlimited users stay unlimited); admin VAS gains a Traffic product section + traffic gift-code type, Shop gains a Traffic packs tab, Finance shows the remaining allowance; en+zh locales - [x] CLI OAuth + consent denial (ported from upstream PR #3588) — built-in `Cloudreve CLI` public client (`http://127.0.0.1/callback`, desktop scope set, empty secret + mandatory PKCE per our public-client convention); `redirectURIMatches` implements RFC 8252 loopback matching (any port on `127.0.0.1`/`[::1]` literal only — no `localhost`, no userinfo/fragments/encoded paths, strict query equality); `POST /session/oauth/consent/deny` returns `access_denied` after full client+redirect validation (`Deny` is internal, `json:"-"`); token exchange rejects PKCE downgrade (verifier without a registered challenge); migration preserves admin edits; tests cover redirect matrix, denial, downgrade, and migration idempotence +- [x] Slave delegation of master functions (upstream #2178 主从) — audited the full surface: entity downloads/uploads/thumbnails/media-meta already resolve to signed node URLs or slave RPCs, and archive/extract/remote-download tasks already dispatch via the node capability pool; the remaining master relay was `WopiService.GetFile`, which now 302-redirects the WOPI client to the node's signed content URL for unencrypted remote entities (encrypted/`InternalProxy` entities still proxy through master, preserving inline decryption and admin intent). PutFile/lock orchestration stays master-side by design; transcoding is N/A (no transcoding exists in CE to delegate) - [x] Storage-policy content audit (upstream #2178 item 9, 鉴黄) — `PolicySetting.AuditEndpoint`/`AuditMaxSize`: share creation POSTs each image entity (size-capped, deduped, version entities only) to the configured endpoint via `EntitySource` streaming — bytes never route through the charged direct-link path; `{"flagged":true}` aborts the share (`CodeContentAuditRejected` = 40098), endpoint/parse failures fail closed (`CodeContentAuditFailed` = 40097), and blocked attempts record `content_audit_blocked` (event 62, admin-toggleable under Share events); admin policy editor gains endpoint + size-limit fields; en+zh locales - [x] Multiple user groups per user (upstream #2494 / fork #19) — new `group_memberships` join table (unique `(user_id, group_id)`, nullable `expires`) layered on the unchanged `group_users` primary column; `GroupsOf`/`GroupIDsOf`/`EffectiveGroup` compute the effective set with union semantics: permission bits OR'ed, quotas/limits most-permissive (0 = unlimited where the field uses that convention; `SourceBatchSize`/`MaxWalkedFiles`/`TrashRetention` treated as disabled-or-duration so plain max wins), allowed-policy lists unioned while the primary group's default policy is kept; memberships ride `LoadUserGroup` eager loading so every existing read site sees them automatically; ~100 permission/settings/quota reads across explorer/share/webdav/filemanager/middleware now resolve through `EffectiveGroup`; group-subject ACL entries match any effective membership; `StoragePolicyClient.ListByGroups` unions allowed sets (synthetic merged group never hits ID-keyed queries); group purchases and group gift codes upsert an expiring membership instead of switching `group_users` (UserGrant row kept for history, revert path becomes a no-op); `grant_expire` cron sweeps expired memberships and emits `membership_unsubscribe`; admin user editor gains an "Additional groups" multi-select (`memberships` field on upsert, delegated admins still barred from admin-capable groups); `CountUsers` and the admin user-list group filter count membership holders; user-facing profile group payload exposes the merged union so feature gating matches actual rights; en+zh locales - [x] Group folders via ACL discovery (upstream #2494 / fork #19 remainder) — `AclClient.SharedFileIDs` unions user/group-subject grants (effective memberships included, everyone/anonymous excluded, read bit required) into a per-file permission map; `shared_with_me` flat listing unions those entries next to saved share shortcuts (`childFileQuery` `Or` clause + `AclSharedIDs` threading, search skips the owner pin); `sharedWithMeNavigator.To` resolves `cloudreve://shared_with_me/` — own symbolic shortcuts resolve for WebDAV share-URI translation, ACL-granted files become subtree roots with owner parent chain + `aclPermsToCapabilities` caps stamped through `newFile` inheritance; uploads/creates inside charge the owner's quota and the folder's storage policy; vault wrapper fail-closes on owner-vaulted entries; per-subtree navigator IDs keep ACL caps from going stale across folders; empty-state copy updated (en+zh) diff --git a/service/explorer/viewer.go b/service/explorer/viewer.go index 18808b63..e64ecf41 100644 --- a/service/explorer/viewer.go +++ b/service/explorer/viewer.go @@ -315,6 +315,18 @@ func (service *WopiService) GetFile(c *gin.Context) error { defer entitySource.Close() + // Delegate content serving to the storage node: for entities stored + // off-master with no internal-proxy requirement, the WOPI client fetches + // bytes directly from the node's signed URL instead of relaying through + // master's reverse proxy. + if !entitySource.IsLocal() && !entitySource.ShouldInternalProxy() { + expire := time.Now().Add(time.Hour) + if u, err := entitySource.Url(c, entitysource.WithExpire(&expire)); err == nil { + c.Redirect(http.StatusFound, u.Url) + return nil + } + } + entitySource.Serve(c.Writer, c.Request, entitysource.WithContext(c), )