collavre 0.20.1 → 0.20.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/app/controllers/collavre/concerns/slide_viewable.rb +154 -9
- data/app/controllers/collavre/creative_invitations_controller.rb +8 -11
- data/app/controllers/collavre/creative_shares_controller.rb +12 -16
- data/app/controllers/collavre/passwords_controller.rb +1 -0
- data/lib/collavre/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9faf02fa6fe0e0512402d22782e9341e8d78b906c0220961f19758f55cb18251
|
|
4
|
+
data.tar.gz: a87e2eea061fa0a5b527084196a34713d837b793d5e7fab4c87333be804cafb8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fdcf64acf4b287d3fef53472b172601aaea44f0dccdb137d9db271c91402a52adf0a518fd654f8fffd3398da331d3eedd9efd421f6b60138ed0bd6c538abdf60
|
|
7
|
+
data.tar.gz: a1c6330363dea5703eeee7f23039c9a4d58f8300a29d586d709b14f50e37bf46ac34a0a20546dff4009c2ebf1ea84a3172d6d48f528407609a5354aeb896b02d
|
|
@@ -21,16 +21,161 @@ module Collavre
|
|
|
21
21
|
|
|
22
22
|
private
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
24
|
+
# Builds @slide_ids by performing an in-order DFS traversal of the
|
|
25
|
+
# creative tree rooted at +root+, including any "linked children"
|
|
26
|
+
# (children of the node's origin when the node is a linked creative).
|
|
27
|
+
#
|
|
28
|
+
# Performance: a naive recursion issues one query per node for both
|
|
29
|
+
# +children+ and +linked_children+ (O(N) queries for an N-node tree).
|
|
30
|
+
# This implementation walks the tree level-by-level via batched queries,
|
|
31
|
+
# so total DB round-trips scale with tree depth rather than node count.
|
|
32
|
+
def build_slide_ids(root)
|
|
33
|
+
user = Current.user
|
|
34
|
+
return unless root.has_permission?(user, :read)
|
|
35
|
+
|
|
36
|
+
children_by_parent = {}
|
|
37
|
+
permission_cache = { root.id => true }
|
|
38
|
+
|
|
39
|
+
# BFS over the creative tree, batching DB lookups one level at a time.
|
|
40
|
+
# Only nodes the user may read are expanded, matching the original
|
|
41
|
+
# recursion which short-circuits on permission denial.
|
|
42
|
+
frontier = [ root ]
|
|
43
|
+
until frontier.empty?
|
|
44
|
+
# 1) Load own children for every node in the current level in one query.
|
|
45
|
+
parent_ids = frontier.map(&:id)
|
|
46
|
+
own_children_by_parent = Creative
|
|
47
|
+
.where(parent_id: parent_ids)
|
|
48
|
+
.order(:sequence)
|
|
49
|
+
.group_by(&:parent_id)
|
|
50
|
+
|
|
51
|
+
# 2) For nodes with an origin, load linked children (origin's children
|
|
52
|
+
# visible to the user) in a single batched query.
|
|
53
|
+
linked_nodes = frontier.select { |n| n.origin_id.present? }
|
|
54
|
+
linked_children_by_node = preload_linked_children(linked_nodes, user)
|
|
55
|
+
|
|
56
|
+
next_frontier = []
|
|
57
|
+
frontier.each do |node|
|
|
58
|
+
own = own_children_by_parent[node.id] || []
|
|
59
|
+
merged = if node.origin_id.present?
|
|
60
|
+
linked = linked_children_by_node[node.id] || []
|
|
61
|
+
(own + linked).uniq.sort_by(&:sequence)
|
|
62
|
+
else
|
|
63
|
+
own
|
|
64
|
+
end
|
|
65
|
+
children_by_parent[node.id] = merged
|
|
66
|
+
next_frontier.concat(merged)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# 3) Bulk permission check for the next level (uniqued by id).
|
|
70
|
+
next_unique = next_frontier.uniq(&:id)
|
|
71
|
+
uncached = next_unique.reject { |c| permission_cache.key?(c.id) }
|
|
72
|
+
uncached.each do |child|
|
|
73
|
+
permission_cache[child.id] = child.has_permission?(user, :read)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Only descend into permitted nodes — matches the recursive version's
|
|
77
|
+
# short-circuit. Iterating in node order keeps duplicates collapsed
|
|
78
|
+
# while preserving discovery order.
|
|
79
|
+
frontier = next_unique.select { |c| permission_cache[c.id] }
|
|
32
80
|
end
|
|
33
|
-
|
|
81
|
+
|
|
82
|
+
# 4) DFS in memory using the prebuilt maps to populate @slide_ids in
|
|
83
|
+
# the same order the recursive version would have produced.
|
|
84
|
+
stack = [ root ]
|
|
85
|
+
until stack.empty?
|
|
86
|
+
node = stack.pop
|
|
87
|
+
next unless permission_cache[node.id]
|
|
88
|
+
|
|
89
|
+
@slide_ids << node.id
|
|
90
|
+
children = children_by_parent[node.id] || []
|
|
91
|
+
# Push in reverse so DFS visits in ascending sequence order.
|
|
92
|
+
children.reverse_each { |child| stack.push(child) }
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Returns a hash of +node.id => Array(linked_children)+ for the given
|
|
97
|
+
# nodes (each of which must have +origin_id+ present). Equivalent in
|
|
98
|
+
# result to calling +node.linked_children+ on each node, but executes a
|
|
99
|
+
# bounded number of queries regardless of how many nodes are passed in.
|
|
100
|
+
def preload_linked_children(nodes, user)
|
|
101
|
+
return {} if nodes.empty?
|
|
102
|
+
|
|
103
|
+
# Resolve each node's effective origin (follows linked chains).
|
|
104
|
+
# effective_origin walks the chain by reading the origin association;
|
|
105
|
+
# repeated calls within a request hit ActiveRecord's identity per
|
|
106
|
+
# instance, so this is bounded by the linked-chain depth.
|
|
107
|
+
origin_for_node = {}
|
|
108
|
+
nodes.each do |node|
|
|
109
|
+
origin = node.effective_origin(Set.new)
|
|
110
|
+
origin_for_node[node.id] = origin if origin
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
unique_origin_ids = origin_for_node.values.map(&:id).uniq
|
|
114
|
+
return {} if unique_origin_ids.empty?
|
|
115
|
+
|
|
116
|
+
# Single batched query for all candidate children across all origins.
|
|
117
|
+
candidate_children = Creative
|
|
118
|
+
.where(parent_id: unique_origin_ids)
|
|
119
|
+
.order(:sequence)
|
|
120
|
+
.to_a
|
|
121
|
+
candidates_by_origin = candidate_children.group_by(&:parent_id)
|
|
122
|
+
candidate_ids = candidate_children.map(&:id)
|
|
123
|
+
|
|
124
|
+
accessible_ids = accessible_child_ids(candidate_ids, candidate_children, user)
|
|
125
|
+
|
|
126
|
+
result = {}
|
|
127
|
+
nodes.each do |node|
|
|
128
|
+
origin = origin_for_node[node.id]
|
|
129
|
+
next unless origin
|
|
130
|
+
|
|
131
|
+
children = candidates_by_origin[origin.id] || []
|
|
132
|
+
result[node.id] = children.select { |c| accessible_ids.include?(c.id) }
|
|
133
|
+
end
|
|
134
|
+
result
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Mirrors the access check in Creative#children_with_permission, but
|
|
138
|
+
# operates on a pre-fetched set of candidate children so the work
|
|
139
|
+
# collapses to a fixed number of queries instead of one per parent.
|
|
140
|
+
def accessible_child_ids(candidate_ids, candidate_children, user)
|
|
141
|
+
return Set.new if candidate_ids.empty?
|
|
142
|
+
|
|
143
|
+
min_rank = CreativeShare.permissions["read"]
|
|
144
|
+
accessible = Set.new
|
|
145
|
+
|
|
146
|
+
if user
|
|
147
|
+
user_entries = CreativeSharesCache
|
|
148
|
+
.where(creative_id: candidate_ids, user_id: user.id)
|
|
149
|
+
.pluck(:creative_id, :permission)
|
|
150
|
+
|
|
151
|
+
user_has_entry = Set.new
|
|
152
|
+
user_entries.each do |cid, perm|
|
|
153
|
+
user_has_entry << cid
|
|
154
|
+
perm_rank = CreativeSharesCache.permissions[perm]
|
|
155
|
+
if perm_rank && perm_rank >= min_rank && perm_rank != CreativeSharesCache.permissions[:no_access]
|
|
156
|
+
accessible << cid
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
public_accessible = CreativeSharesCache
|
|
161
|
+
.where(creative_id: candidate_ids, user_id: nil)
|
|
162
|
+
.where("permission >= ?", min_rank)
|
|
163
|
+
.where.not(permission: :no_access)
|
|
164
|
+
.pluck(:creative_id)
|
|
165
|
+
accessible.merge(public_accessible.reject { |cid| user_has_entry.include?(cid) })
|
|
166
|
+
|
|
167
|
+
owned_ids = candidate_children.select { |c| c.user_id == user.id }.map(&:id)
|
|
168
|
+
accessible.merge(owned_ids)
|
|
169
|
+
else
|
|
170
|
+
public_accessible = CreativeSharesCache
|
|
171
|
+
.where(creative_id: candidate_ids, user_id: nil)
|
|
172
|
+
.where("permission >= ?", min_rank)
|
|
173
|
+
.where.not(permission: :no_access)
|
|
174
|
+
.pluck(:creative_id)
|
|
175
|
+
accessible.merge(public_accessible)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
accessible
|
|
34
179
|
end
|
|
35
180
|
end
|
|
36
181
|
end
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
module Collavre
|
|
4
4
|
class CreativeInvitationsController < ApplicationController
|
|
5
5
|
before_action :set_invitation
|
|
6
|
-
before_action :authorize_admin!
|
|
7
6
|
|
|
8
7
|
def update
|
|
9
8
|
if @invitation.update(permission: params[:permission])
|
|
@@ -29,18 +28,16 @@ module Collavre
|
|
|
29
28
|
|
|
30
29
|
private
|
|
31
30
|
|
|
31
|
+
# Scoped lookup: only returns the invitation if Current.user has admin
|
|
32
|
+
# permission on its creative. Raises ActiveRecord::RecordNotFound otherwise
|
|
33
|
+
# so that record existence is not leaked to unauthorized users.
|
|
32
34
|
def set_invitation
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
def authorize_admin!
|
|
38
|
-
return if @creative.has_permission?(Current.user, :admin)
|
|
35
|
+
invitation = Invitation.find_by(id: params[:id])
|
|
36
|
+
creative = invitation&.creative
|
|
37
|
+
raise ActiveRecord::RecordNotFound unless creative&.has_permission?(Current.user, :admin)
|
|
39
38
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
format.json { render json: { error: t("collavre.creatives.errors.no_permission") }, status: :forbidden }
|
|
43
|
-
end
|
|
39
|
+
@invitation = invitation
|
|
40
|
+
@creative = creative
|
|
44
41
|
end
|
|
45
42
|
end
|
|
46
43
|
end
|
|
@@ -107,14 +107,7 @@ module Collavre
|
|
|
107
107
|
end
|
|
108
108
|
|
|
109
109
|
def update
|
|
110
|
-
@creative_share =
|
|
111
|
-
unless @creative_share.creative.has_permission?(Current.user, :admin)
|
|
112
|
-
respond_to do |format|
|
|
113
|
-
format.html { redirect_back fallback_location: main_app.root_path, alert: t("collavre.creatives.errors.no_permission") }
|
|
114
|
-
format.json { render json: { error: t("collavre.creatives.errors.no_permission") }, status: :forbidden }
|
|
115
|
-
end
|
|
116
|
-
return
|
|
117
|
-
end
|
|
110
|
+
@creative_share = find_admin_creative_share(params[:id])
|
|
118
111
|
|
|
119
112
|
if @creative_share.update(permission: params[:permission])
|
|
120
113
|
respond_to do |format|
|
|
@@ -130,14 +123,7 @@ module Collavre
|
|
|
130
123
|
end
|
|
131
124
|
|
|
132
125
|
def destroy
|
|
133
|
-
@creative_share =
|
|
134
|
-
unless @creative_share.creative.has_permission?(Current.user, :admin)
|
|
135
|
-
respond_to do |format|
|
|
136
|
-
format.html { redirect_back fallback_location: main_app.root_path, alert: t("collavre.creatives.errors.no_permission") }
|
|
137
|
-
format.json { render json: { error: t("collavre.creatives.errors.no_permission") }, status: :forbidden }
|
|
138
|
-
end
|
|
139
|
-
return
|
|
140
|
-
end
|
|
126
|
+
@creative_share = find_admin_creative_share(params[:id])
|
|
141
127
|
|
|
142
128
|
@creative_share.destroy
|
|
143
129
|
# remove linked creative if it exists
|
|
@@ -151,6 +137,16 @@ module Collavre
|
|
|
151
137
|
|
|
152
138
|
private
|
|
153
139
|
|
|
140
|
+
# Scoped lookup: only returns the share if Current.user has admin permission
|
|
141
|
+
# on its creative. Raises ActiveRecord::RecordNotFound otherwise so that
|
|
142
|
+
# record existence is not leaked via 403 vs 404 distinction.
|
|
143
|
+
def find_admin_creative_share(id)
|
|
144
|
+
share = CreativeShare.find_by(id: id)
|
|
145
|
+
raise ActiveRecord::RecordNotFound unless share&.creative&.has_permission?(Current.user, :admin)
|
|
146
|
+
|
|
147
|
+
share
|
|
148
|
+
end
|
|
149
|
+
|
|
154
150
|
def all_descendants(creative)
|
|
155
151
|
creative.children.flat_map { |child| [ child ] + all_descendants(child) }
|
|
156
152
|
end
|
|
@@ -20,6 +20,7 @@ module Collavre
|
|
|
20
20
|
|
|
21
21
|
def update
|
|
22
22
|
if @user.update(params.permit(:password, :password_confirmation))
|
|
23
|
+
@user.update_column(:email_verified_at, Time.current) unless @user.email_verified?
|
|
23
24
|
redirect_to new_session_path, notice: "Password has been reset."
|
|
24
25
|
else
|
|
25
26
|
redirect_to edit_password_path(params[:token]), alert: "Passwords did not match."
|
data/lib/collavre/version.rb
CHANGED