collavre 0.5.0 → 0.7.0
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/assets/stylesheets/collavre/comment_versions.css +76 -0
- data/app/assets/stylesheets/collavre/comments_popup.css +347 -37
- data/app/assets/stylesheets/collavre/creatives.css +73 -1
- data/app/assets/stylesheets/collavre/org_chart.css +319 -0
- data/app/assets/stylesheets/collavre/popup.css +68 -1
- data/app/controllers/collavre/application_controller.rb +13 -0
- data/app/controllers/collavre/comments/versions_controller.rb +82 -0
- data/app/controllers/collavre/comments_controller.rb +14 -153
- data/app/controllers/collavre/concerns/exportable.rb +30 -0
- data/app/controllers/collavre/concerns/shareable.rb +28 -0
- data/app/controllers/collavre/concerns/slide_viewable.rb +37 -0
- data/app/controllers/collavre/concerns/tree_manageable.rb +141 -0
- data/app/controllers/collavre/creative_imports_controller.rb +6 -0
- data/app/controllers/collavre/creative_invitations_controller.rb +46 -0
- data/app/controllers/collavre/creative_plans_controller.rb +1 -1
- data/app/controllers/collavre/creative_shares_controller.rb +84 -14
- data/app/controllers/collavre/creatives_controller.rb +70 -194
- data/app/controllers/collavre/google_auth_controller.rb +3 -0
- data/app/controllers/collavre/invites_controller.rb +2 -1
- data/app/controllers/collavre/sessions_controller.rb +3 -0
- data/app/controllers/collavre/topics_controller.rb +39 -2
- data/app/controllers/collavre/users_controller.rb +5 -404
- data/app/controllers/concerns/collavre/comments/approval_actions.rb +108 -0
- data/app/controllers/concerns/collavre/comments/batch_operations.rb +55 -0
- data/app/controllers/concerns/collavre/comments/conversion.rb +46 -0
- data/app/controllers/concerns/collavre/users_controller/admin_operations.rb +74 -0
- data/app/controllers/concerns/collavre/users_controller/ai_user_management.rb +119 -0
- data/app/controllers/concerns/collavre/users_controller/contact_management.rb +166 -0
- data/app/controllers/concerns/collavre/users_controller/profile_and_settings.rb +102 -0
- data/app/controllers/concerns/collavre/users_controller/registration.rb +63 -0
- data/app/helpers/collavre/application_helper.rb +1 -0
- data/app/helpers/collavre/creatives_helper.rb +12 -9
- data/app/helpers/collavre/navigation_helper.rb +1 -1
- data/app/javascript/collavre.js +0 -1
- data/app/javascript/controllers/comment_controller.js +33 -70
- data/app/javascript/controllers/comment_version_controller.js +164 -0
- data/app/javascript/controllers/comments/__tests__/form_controller_review.test.js +305 -0
- data/app/javascript/controllers/comments/__tests__/list_controller_selection.test.js +103 -0
- data/app/javascript/controllers/comments/__tests__/review_quotes_store.test.js +113 -0
- data/app/javascript/controllers/comments/contexts_controller.js +363 -0
- data/app/javascript/controllers/comments/form_controller.js +304 -13
- data/app/javascript/controllers/comments/list_controller.js +151 -62
- data/app/javascript/controllers/comments/popup_controller.js +66 -38
- data/app/javascript/controllers/comments/presence_controller.js +2 -10
- data/app/javascript/controllers/comments/review_quotes_store.js +189 -0
- data/app/javascript/controllers/comments/topics_controller.js +34 -10
- data/app/javascript/controllers/index.js +15 -1
- data/app/javascript/controllers/org_chart_controller.js +46 -0
- data/app/javascript/controllers/share_modal_controller.js +369 -0
- data/app/javascript/controllers/topic_search_controller.js +103 -0
- data/app/javascript/creatives/drag_drop/event_handlers.js +42 -1
- data/app/javascript/lib/api/creatives.js +12 -0
- data/app/javascript/lib/api/csrf_fetch.js +35 -0
- data/app/javascript/lib/api/drag_drop.js +17 -0
- data/app/javascript/modules/command_menu.js +40 -0
- data/app/javascript/modules/creative_row_editor.js +88 -0
- data/app/javascript/modules/slide_view.js +2 -1
- data/app/jobs/collavre/ai_agent_job.rb +42 -30
- data/app/jobs/collavre/compress_job.rb +92 -0
- data/app/models/collavre/comment.rb +36 -1
- data/app/models/collavre/comment_version.rb +15 -0
- data/app/models/collavre/creative/describable.rb +1 -1
- data/app/models/collavre/creative.rb +51 -0
- data/app/models/collavre/task.rb +30 -2
- data/app/models/collavre/user.rb +20 -3
- data/app/services/collavre/ai_agent/a2a_dispatcher.rb +68 -0
- data/app/services/collavre/ai_agent/agent_lifecycle_manager.rb +89 -0
- data/app/services/collavre/ai_agent/message_builder.rb +85 -6
- data/app/services/collavre/ai_agent/response_finalizer.rb +97 -0
- data/app/services/collavre/ai_agent/response_streamer.rb +56 -0
- data/app/services/collavre/ai_agent/review_handler.rb +18 -1
- data/app/services/collavre/ai_agent_service.rb +130 -183
- data/app/services/collavre/ai_client.rb +6 -0
- data/app/services/collavre/auto_theme_generator.rb +1 -1
- data/app/services/collavre/command_menu_service.rb +19 -0
- data/app/services/collavre/comments/command_processor.rb +3 -1
- data/app/services/collavre/comments/compress_command.rb +75 -0
- data/app/services/collavre/comments/concerns/workflow_support.rb +115 -0
- data/app/services/collavre/comments/work_command.rb +161 -0
- data/app/services/collavre/comments/workflow_executor.rb +276 -0
- data/app/services/collavre/creatives/plan_tagger.rb +14 -3
- data/app/services/collavre/creatives/tree_formatter.rb +53 -13
- data/app/services/collavre/gemini_parent_recommender.rb +4 -4
- data/app/services/collavre/orchestration/agent_context_builder.rb +1 -3
- data/app/services/collavre/orchestration/agent_orchestrator.rb +15 -4
- data/app/services/collavre/orchestration/policy_resolver.rb +0 -19
- data/app/services/collavre/orchestration/scheduler.rb +3 -2
- data/app/services/collavre/orchestration/stuck_detector.rb +1 -1
- data/app/services/collavre/system_events/dispatcher.rb +9 -0
- data/app/services/collavre/tools/creative_create_service.rb +1 -8
- data/app/services/collavre/tools/creative_import_service.rb +46 -0
- data/app/services/collavre/tools/creative_retrieval_service.rb +157 -96
- data/app/services/collavre/tools/creative_update_service.rb +1 -8
- data/app/services/collavre/tools/cron_list_service.rb +1 -1
- data/app/services/collavre/tools/description_normalizable.rb +16 -0
- data/app/views/collavre/comments/_comment.html.erb +25 -8
- data/app/views/collavre/comments/_comments_popup.html.erb +32 -5
- data/app/views/collavre/creatives/_inline_edit_form.html.erb +13 -0
- data/app/views/collavre/creatives/_share_button.html.erb +4 -1
- data/app/views/collavre/creatives/_share_modal.html.erb +31 -1
- data/app/views/collavre/creatives/index.html.erb +5 -5
- data/app/views/collavre/creatives/slide_view.html.erb +1 -1
- data/app/views/collavre/users/{_contact_management.html.erb → _contact_list.html.erb} +4 -8
- data/app/views/collavre/users/_org_chart.html.erb +68 -0
- data/app/views/collavre/users/_org_chart_node.html.erb +169 -0
- data/app/views/collavre/users/new_ai.html.erb +9 -0
- data/app/views/collavre/users/show.html.erb +32 -8
- data/config/locales/comments.en.yml +57 -2
- data/config/locales/comments.ko.yml +57 -2
- data/config/locales/contacts.en.yml +31 -0
- data/config/locales/contacts.ko.yml +31 -0
- data/config/locales/contexts.en.yml +8 -0
- data/config/locales/contexts.ko.yml +8 -0
- data/config/locales/creatives.en.yml +6 -0
- data/config/locales/creatives.ko.yml +6 -0
- data/config/locales/users.en.yml +1 -0
- data/config/locales/users.ko.yml +1 -0
- data/config/routes.rb +14 -1
- data/db/migrate/20260220072200_add_workflow_fields_to_tasks.rb +12 -0
- data/db/migrate/20260223173533_add_review_type_to_comments.rb +5 -0
- data/db/migrate/20260225065200_create_comment_versions.rb +14 -0
- data/db/migrate/20260225074416_add_selected_version_id_to_comments.rb +7 -0
- data/lib/collavre/version.rb +1 -1
- metadata +47 -10
- data/app/javascript/lib/lexical/__tests__/action_text_attachment_node.test.jsx +0 -91
- data/app/javascript/lib/lexical/action_text_attachment_node.js +0 -459
- data/app/javascript/lib/lexical/dom_attachment_utils.js +0 -66
- data/app/javascript/modules/share_modal.js +0 -76
- data/app/javascript/modules/share_user_popup.js +0 -77
- data/app/services/collavre/orchestration/self_reflection_evaluator.rb +0 -231
- data/app/views/collavre/comments/_presence_avatars.html.erb +0 -8
- data/app/views/collavre/creatives/_delete_button.html.erb +0 -12
|
@@ -1,408 +1,9 @@
|
|
|
1
1
|
module Collavre
|
|
2
2
|
class UsersController < ApplicationController
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
@user = Collavre::User.new
|
|
9
|
-
if params[:invite_token].present?
|
|
10
|
-
@invitation = Collavre::Invitation.find_by_token_for(:invite, params[:invite_token])
|
|
11
|
-
@user.email = @invitation&.email
|
|
12
|
-
end
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
def create
|
|
16
|
-
@user = Collavre::User.new(user_params)
|
|
17
|
-
Collavre::Invitation.transaction do
|
|
18
|
-
if params[:invite_token].present?
|
|
19
|
-
invitation = Collavre::Invitation.find_by_token_for(:invite, params[:invite_token])
|
|
20
|
-
if invitation
|
|
21
|
-
@invitation = invitation
|
|
22
|
-
@user.email = invitation.email
|
|
23
|
-
end
|
|
24
|
-
end
|
|
25
|
-
if @user.save
|
|
26
|
-
if invitation
|
|
27
|
-
invitation.update(accepted_at: Time.current)
|
|
28
|
-
Collavre::CreativeShare.create!(
|
|
29
|
-
creative: invitation.creative,
|
|
30
|
-
user: @user,
|
|
31
|
-
permission: invitation.permission,
|
|
32
|
-
shared_by: invitation.inviter
|
|
33
|
-
)
|
|
34
|
-
Collavre::Contact.ensure(user: invitation.inviter, contact_user: @user)
|
|
35
|
-
invitation.creative.create_linked_creative_for_user(@user)
|
|
36
|
-
end
|
|
37
|
-
Collavre::EmailVerificationMailer.verify(@user).deliver_later
|
|
38
|
-
session.delete(:return_to_after_authenticating)
|
|
39
|
-
redirect_to new_session_path, notice: I18n.t("collavre.users.new.success_sign_up")
|
|
40
|
-
else
|
|
41
|
-
render :new, status: :unprocessable_entity
|
|
42
|
-
end
|
|
43
|
-
rescue ActiveSupport::MessageVerifier::InvalidSignature
|
|
44
|
-
flash.now[:alert] = I18n.t("collavre.invites.invalid")
|
|
45
|
-
render :new, status: :unprocessable_entity
|
|
46
|
-
end
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
def exists
|
|
50
|
-
user = Collavre::User.find_by(email: params[:email])
|
|
51
|
-
render json: { exists: user.present? }
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
def new_ai
|
|
55
|
-
@available_tools = load_available_tools
|
|
56
|
-
|
|
57
|
-
if params[:copy_from].present?
|
|
58
|
-
source = Collavre::User.find_by(id: params[:copy_from])
|
|
59
|
-
if source&.ai_user? && source.created_by_id == Current.user.id
|
|
60
|
-
@copy_source = source
|
|
61
|
-
@copy_name = "#{source.name} (copy)"
|
|
62
|
-
end
|
|
63
|
-
end
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
def create_ai
|
|
67
|
-
ai_id = params[:ai_id].to_s.strip.downcase
|
|
68
|
-
email = "#{ai_id}@ai.local"
|
|
69
|
-
searchable = ActiveModel::Type::Boolean.new.cast(params.fetch(:searchable, false))
|
|
70
|
-
|
|
71
|
-
@user = Collavre::User.new(
|
|
72
|
-
name: params[:name],
|
|
73
|
-
email: email,
|
|
74
|
-
password: SecureRandom.hex(36),
|
|
75
|
-
system_prompt: params[:system_prompt],
|
|
76
|
-
llm_vendor: params[:llm_vendor].presence || "google",
|
|
77
|
-
llm_model: params[:llm_model],
|
|
78
|
-
llm_api_key: params[:llm_api_key],
|
|
79
|
-
gateway_url: params[:gateway_url],
|
|
80
|
-
tools: params[:tools] || [],
|
|
81
|
-
searchable: searchable,
|
|
82
|
-
email_verified_at: Time.current,
|
|
83
|
-
created_by_id: Current.user.id,
|
|
84
|
-
routing_expression: params[:routing_expression]
|
|
85
|
-
)
|
|
86
|
-
@user.agent_conf = params[:agent_conf] if @user.respond_to?(:agent_conf=) && params[:agent_conf].present?
|
|
87
|
-
|
|
88
|
-
if @user.save
|
|
89
|
-
Collavre::Contact.ensure(user: Current.user, contact_user: @user)
|
|
90
|
-
redirect_to user_path(Current.user, tab: "contacts"), notice: I18n.t("collavre.users.create_ai.success")
|
|
91
|
-
else
|
|
92
|
-
flash.now[:alert] = @user.errors.full_messages.to_sentence
|
|
93
|
-
@available_tools = load_available_tools
|
|
94
|
-
render :new_ai, status: :unprocessable_entity
|
|
95
|
-
end
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
def edit_ai
|
|
99
|
-
@user = Collavre::User.find(params[:id])
|
|
100
|
-
unless @user.ai_user?
|
|
101
|
-
redirect_to user_path(@user), alert: I18n.t("collavre.users.edit_ai.not_an_ai")
|
|
102
|
-
return
|
|
103
|
-
end
|
|
104
|
-
|
|
105
|
-
@available_tools = load_available_tools
|
|
106
|
-
end
|
|
107
|
-
|
|
108
|
-
def update_ai
|
|
109
|
-
@user = Collavre::User.find(params[:id])
|
|
110
|
-
unless @user.ai_user?
|
|
111
|
-
redirect_to user_path(@user), alert: I18n.t("collavre.users.edit_ai.not_an_ai")
|
|
112
|
-
return
|
|
113
|
-
end
|
|
114
|
-
|
|
115
|
-
allowed = Current.user.system_admin? ||
|
|
116
|
-
(@user.ai_user? && @user.created_by_id == Current.user.id)
|
|
117
|
-
|
|
118
|
-
unless allowed
|
|
119
|
-
fallback = user_path(Current.user, tab: "contacts")
|
|
120
|
-
redirect_back fallback_location: fallback, alert: I18n.t("collavre.users.destroy.not_authorized")
|
|
121
|
-
return
|
|
122
|
-
end
|
|
123
|
-
|
|
124
|
-
ai_params = params.require(:user).permit(:name, :system_prompt, :llm_vendor, :llm_model, :llm_api_key, :gateway_url, :searchable, :routing_expression, :agent_conf, tools: [])
|
|
125
|
-
|
|
126
|
-
if @user.update(ai_params)
|
|
127
|
-
redirect_to edit_ai_user_path(@user), notice: I18n.t("collavre.users.update_ai.success")
|
|
128
|
-
else
|
|
129
|
-
@available_tools = load_available_tools
|
|
130
|
-
flash.now[:alert] = @user.errors.full_messages.to_sentence
|
|
131
|
-
render :edit_ai, status: :unprocessable_entity
|
|
132
|
-
end
|
|
133
|
-
end
|
|
134
|
-
|
|
135
|
-
def search
|
|
136
|
-
term = params[:q].to_s.strip.downcase
|
|
137
|
-
|
|
138
|
-
if term.blank? && params[:scope] != "contacts"
|
|
139
|
-
return render json: []
|
|
140
|
-
end
|
|
141
|
-
|
|
142
|
-
creative = Collavre::Creative.find_by(id: params[:creative_id])
|
|
143
|
-
|
|
144
|
-
if creative.present? && !creative.has_permission?(Current.user, :read)
|
|
145
|
-
head :forbidden and return
|
|
146
|
-
end
|
|
147
|
-
|
|
148
|
-
scope = if params[:scope] == "contacts" && Current.user
|
|
149
|
-
# Include both contacts and searchable users (e.g., AI agents with searchable=true)
|
|
150
|
-
contact_ids = Current.user.contact_users.select(:id)
|
|
151
|
-
searchable_ids = Collavre::User.where(searchable: true).select(:id)
|
|
152
|
-
Collavre::User.where(id: contact_ids).or(Collavre::User.where(id: searchable_ids))
|
|
153
|
-
else
|
|
154
|
-
Collavre::User.mentionable_for(creative)
|
|
155
|
-
end
|
|
156
|
-
|
|
157
|
-
users = scope
|
|
158
|
-
if term.present?
|
|
159
|
-
users = users.where("LOWER(users.email) LIKE :term OR LOWER(users.name) LIKE :term", term: "#{term}%")
|
|
160
|
-
end
|
|
161
|
-
|
|
162
|
-
limit = params[:limit].to_i
|
|
163
|
-
limit = 20 if limit <= 0
|
|
164
|
-
limit = 50 if limit > 50
|
|
165
|
-
|
|
166
|
-
user_ids = users.select(:id).distinct.limit(limit).pluck(:id)
|
|
167
|
-
users = Collavre::User.where(id: user_ids)
|
|
168
|
-
render json: users.map { |u| { id: u.id, name: u.display_name, email: u.email, avatar_url: view_context.user_avatar_url(u, size: 20) } }
|
|
169
|
-
end
|
|
170
|
-
|
|
171
|
-
def index
|
|
172
|
-
@users = Collavre::User.includes(:sessions, :devices)
|
|
173
|
-
end
|
|
174
|
-
|
|
175
|
-
def show
|
|
176
|
-
@user = Collavre::User.find(params[:id])
|
|
177
|
-
@active_tab = params[:tab].presence || "profile"
|
|
178
|
-
if Current.user
|
|
179
|
-
prepare_contacts
|
|
180
|
-
else
|
|
181
|
-
@contacts = Collavre::Contact.none
|
|
182
|
-
@contact_page = 1
|
|
183
|
-
@total_contact_pages = 1
|
|
184
|
-
@last_login_map = {}
|
|
185
|
-
@shared_by_me = {}
|
|
186
|
-
@shared_with_me = {}
|
|
187
|
-
end
|
|
188
|
-
end
|
|
189
|
-
|
|
190
|
-
def destroy
|
|
191
|
-
@user = Collavre::User.find(params[:id])
|
|
192
|
-
|
|
193
|
-
if @user == Current.user
|
|
194
|
-
redirect_to users_path, alert: I18n.t("collavre.users.destroy.cannot_delete_self")
|
|
195
|
-
return
|
|
196
|
-
end
|
|
197
|
-
|
|
198
|
-
allowed = Current.user.system_admin? ||
|
|
199
|
-
(@user.ai_user? && @user.created_by_id == Current.user.id)
|
|
200
|
-
|
|
201
|
-
unless allowed
|
|
202
|
-
fallback = user_path(Current.user, tab: "contacts")
|
|
203
|
-
redirect_back fallback_location: fallback, alert: I18n.t("collavre.users.destroy.not_authorized")
|
|
204
|
-
return
|
|
205
|
-
end
|
|
206
|
-
|
|
207
|
-
if @user.destroy
|
|
208
|
-
fallback = Current.user.system_admin? ? users_path : user_path(Current.user, tab: "contacts")
|
|
209
|
-
redirect_back fallback_location: fallback, notice: I18n.t("collavre.users.destroy.success")
|
|
210
|
-
else
|
|
211
|
-
redirect_to users_path, alert: I18n.t("collavre.users.destroy.failure")
|
|
212
|
-
end
|
|
213
|
-
end
|
|
214
|
-
|
|
215
|
-
def grant_system_admin
|
|
216
|
-
@user = Collavre::User.find(params[:id])
|
|
217
|
-
|
|
218
|
-
if @user.update(system_admin: true)
|
|
219
|
-
redirect_to users_path, notice: I18n.t("collavre.users.system_admin.granted")
|
|
220
|
-
else
|
|
221
|
-
redirect_to users_path, alert: I18n.t("collavre.users.system_admin.failed")
|
|
222
|
-
end
|
|
223
|
-
end
|
|
224
|
-
|
|
225
|
-
def revoke_system_admin
|
|
226
|
-
@user = Collavre::User.find(params[:id])
|
|
227
|
-
|
|
228
|
-
if @user.update(system_admin: false)
|
|
229
|
-
redirect_to users_path, notice: I18n.t("collavre.users.system_admin.revoked")
|
|
230
|
-
else
|
|
231
|
-
redirect_to users_path, alert: I18n.t("collavre.users.system_admin.failed")
|
|
232
|
-
end
|
|
233
|
-
end
|
|
234
|
-
|
|
235
|
-
def unlock
|
|
236
|
-
@user = Collavre::User.find(params[:id])
|
|
237
|
-
@user.unlock_account!
|
|
238
|
-
redirect_to users_path, notice: I18n.t("collavre.users.unlock.success", name: @user.display_name)
|
|
239
|
-
end
|
|
240
|
-
|
|
241
|
-
def lock
|
|
242
|
-
@user = Collavre::User.find(params[:id])
|
|
243
|
-
if @user == Current.user
|
|
244
|
-
redirect_to users_path, alert: I18n.t("collavre.users.lock.cannot_lock_self")
|
|
245
|
-
return
|
|
246
|
-
end
|
|
247
|
-
@user.lock_account!
|
|
248
|
-
redirect_to users_path, notice: I18n.t("collavre.users.lock.success", name: @user.display_name)
|
|
249
|
-
end
|
|
250
|
-
|
|
251
|
-
def update
|
|
252
|
-
@user = Collavre::User.find(params[:id])
|
|
253
|
-
if @user.update(profile_params)
|
|
254
|
-
redirect_to user_path(@user), notice: I18n.t("collavre.users.profile_updated")
|
|
255
|
-
else
|
|
256
|
-
render :show, status: :unprocessable_entity
|
|
257
|
-
end
|
|
258
|
-
end
|
|
259
|
-
|
|
260
|
-
def notification_settings
|
|
261
|
-
if Current.user.update(notification_settings_params)
|
|
262
|
-
head :no_content
|
|
263
|
-
else
|
|
264
|
-
render json: { errors: Current.user.errors.full_messages }, status: :unprocessable_entity
|
|
265
|
-
end
|
|
266
|
-
end
|
|
267
|
-
|
|
268
|
-
def edit_password
|
|
269
|
-
@user = Collavre::User.find(params[:id])
|
|
270
|
-
end
|
|
271
|
-
|
|
272
|
-
def passkeys
|
|
273
|
-
@user = Collavre::User.find(params[:id])
|
|
274
|
-
|
|
275
|
-
unless @user == Current.user || Current.user.system_admin?
|
|
276
|
-
redirect_to user_path(Current.user), alert: I18n.t("collavre.users.destroy.not_authorized")
|
|
277
|
-
end
|
|
278
|
-
end
|
|
279
|
-
|
|
280
|
-
def update_password
|
|
281
|
-
@user = Collavre::User.find(params[:id])
|
|
282
|
-
if @user.authenticate(params[:user][:current_password])
|
|
283
|
-
if @user.update(user_params)
|
|
284
|
-
redirect_to user_path(@user), notice: I18n.t("collavre.users.password_updated")
|
|
285
|
-
else
|
|
286
|
-
flash.now[:alert] = I18n.t("collavre.users.password_update_failed")
|
|
287
|
-
render :edit_password, status: :unprocessable_entity
|
|
288
|
-
end
|
|
289
|
-
else
|
|
290
|
-
@user.errors.add(:current_password, I18n.t("collavre.users.current_password_incorrect"))
|
|
291
|
-
flash.now[:alert] = I18n.t("collavre.users.password_update_failed")
|
|
292
|
-
render :edit_password, status: :unprocessable_entity
|
|
293
|
-
end
|
|
294
|
-
end
|
|
295
|
-
|
|
296
|
-
private
|
|
297
|
-
|
|
298
|
-
def load_available_tools
|
|
299
|
-
Collavre::McpService.available_tools(Current.user).map do |tool|
|
|
300
|
-
{
|
|
301
|
-
name: tool[:name],
|
|
302
|
-
description: tool[:description],
|
|
303
|
-
parameters: tool[:params]
|
|
304
|
-
}
|
|
305
|
-
end
|
|
306
|
-
end
|
|
307
|
-
|
|
308
|
-
def prepare_contacts
|
|
309
|
-
per_page = 10
|
|
310
|
-
@contact_page = [ params[:contact_page].to_i, 1 ].max
|
|
311
|
-
|
|
312
|
-
creative_shares = Collavre::CreativeShare.arel_table
|
|
313
|
-
creatives = Collavre::Creative.arel_table
|
|
314
|
-
|
|
315
|
-
user_creative_origins_sql = Collavre::Creative
|
|
316
|
-
.where(user_id: Current.user.id)
|
|
317
|
-
.select("COALESCE(origin_id, id) AS origin_id")
|
|
318
|
-
.to_sql
|
|
319
|
-
|
|
320
|
-
shared_by_me_scope = Collavre::CreativeShare
|
|
321
|
-
.joins(:creative)
|
|
322
|
-
.where.not(permission: Collavre::CreativeShare.permissions[:no_access])
|
|
323
|
-
.where(
|
|
324
|
-
creative_shares[:shared_by_id].eq(Current.user.id)
|
|
325
|
-
.or(
|
|
326
|
-
creative_shares[:shared_by_id].eq(nil).and(creatives[:user_id].eq(Current.user.id))
|
|
327
|
-
)
|
|
328
|
-
.or(
|
|
329
|
-
creatives[:id].in(Arel.sql("(#{user_creative_origins_sql})"))
|
|
330
|
-
)
|
|
331
|
-
)
|
|
332
|
-
|
|
333
|
-
shared_with_me_scope = Collavre::CreativeShare
|
|
334
|
-
.joins(:creative)
|
|
335
|
-
.where(user_id: Current.user.id)
|
|
336
|
-
.where.not(permission: Collavre::CreativeShare.permissions[:no_access])
|
|
337
|
-
|
|
338
|
-
contact_ids_sql = [
|
|
339
|
-
Current.user.contacts.select("contact_user_id AS user_id").to_sql,
|
|
340
|
-
shared_by_me_scope.select("creative_shares.user_id AS user_id").to_sql,
|
|
341
|
-
shared_with_me_scope.select("COALESCE(creative_shares.shared_by_id, creatives.user_id) AS user_id").to_sql
|
|
342
|
-
].join(" UNION ")
|
|
343
|
-
|
|
344
|
-
contact_users_relation = Collavre::User.where(
|
|
345
|
-
id: Collavre::User.from("(#{contact_ids_sql}) AS contact_ids").select(:user_id)
|
|
346
|
-
)
|
|
347
|
-
|
|
348
|
-
@total_contacts = contact_users_relation.count
|
|
349
|
-
@total_contact_pages = [ (@total_contacts.to_f / per_page).ceil, 1 ].max
|
|
350
|
-
paged_users = contact_users_relation
|
|
351
|
-
.includes(avatar_attachment: :blob)
|
|
352
|
-
.order(:name, :id)
|
|
353
|
-
.offset((@contact_page - 1) * per_page)
|
|
354
|
-
.limit(per_page)
|
|
355
|
-
|
|
356
|
-
existing_contacts = Current.user.contacts.includes(contact_user: [ avatar_attachment: :blob ]).index_by(&:contact_user_id)
|
|
357
|
-
@contacts = paged_users.map do |user|
|
|
358
|
-
existing_contacts[user.id] || Collavre::Contact.new(user: Current.user, contact_user: user)
|
|
359
|
-
end
|
|
360
|
-
|
|
361
|
-
@last_login_map = Collavre::Session.where(user_id: paged_users.map(&:id)).group(:user_id).maximum(:updated_at)
|
|
362
|
-
|
|
363
|
-
shares_from_me = shared_by_me_scope
|
|
364
|
-
.where(user_id: paged_users.map(&:id))
|
|
365
|
-
.includes(:creative)
|
|
366
|
-
|
|
367
|
-
@shared_by_me = shares_from_me.group_by(&:user_id).transform_values { |shares| shares.map(&:creative) }
|
|
368
|
-
|
|
369
|
-
shares_to_me = Collavre::CreativeShare
|
|
370
|
-
.joins(:creative)
|
|
371
|
-
.where(user_id: Current.user.id)
|
|
372
|
-
.where.not(permission: Collavre::CreativeShare.permissions[:no_access])
|
|
373
|
-
.where(
|
|
374
|
-
creative_shares[:shared_by_id].in(paged_users.map(&:id))
|
|
375
|
-
.or(creative_shares[:shared_by_id].eq(nil).and(creatives[:user_id].in(paged_users.map(&:id))))
|
|
376
|
-
)
|
|
377
|
-
.includes(:creative)
|
|
378
|
-
|
|
379
|
-
@shared_with_me = shares_to_me.group_by(&:sharer_id)
|
|
380
|
-
.transform_values { |shares| shares.map(&:creative) }
|
|
381
|
-
end
|
|
382
|
-
|
|
383
|
-
def user_params
|
|
384
|
-
params.require(:user).permit(:email, :password, :password_confirmation, :name)
|
|
385
|
-
end
|
|
386
|
-
|
|
387
|
-
def profile_params
|
|
388
|
-
params.require(:user).permit(
|
|
389
|
-
:avatar,
|
|
390
|
-
:avatar_url,
|
|
391
|
-
:display_level,
|
|
392
|
-
:completion_mark,
|
|
393
|
-
:theme,
|
|
394
|
-
:name,
|
|
395
|
-
:notifications_enabled,
|
|
396
|
-
:calendar_id,
|
|
397
|
-
:timezone,
|
|
398
|
-
:locale
|
|
399
|
-
).tap do |p|
|
|
400
|
-
p[:locale] = normalize_supported_locale(p[:locale]) if p.key?(:locale)
|
|
401
|
-
end
|
|
402
|
-
end
|
|
403
|
-
|
|
404
|
-
def notification_settings_params
|
|
405
|
-
params.require(:user).permit(:notifications_enabled)
|
|
406
|
-
end
|
|
3
|
+
include UsersController::Registration
|
|
4
|
+
include UsersController::AiUserManagement
|
|
5
|
+
include UsersController::ContactManagement
|
|
6
|
+
include UsersController::AdminOperations
|
|
7
|
+
include UsersController::ProfileAndSettings
|
|
407
8
|
end
|
|
408
9
|
end
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
module Collavre
|
|
2
|
+
module Comments
|
|
3
|
+
module ApprovalActions
|
|
4
|
+
extend ActiveSupport::Concern
|
|
5
|
+
|
|
6
|
+
def approve
|
|
7
|
+
status = @comment.approval_status(Current.user)
|
|
8
|
+
if status != :ok
|
|
9
|
+
error_key = case status
|
|
10
|
+
when :invalid_action_format then "collavre.comments.approve_invalid_format"
|
|
11
|
+
when :missing_action then "collavre.comments.approve_missing_action"
|
|
12
|
+
when :missing_approver then "collavre.comments.approve_missing_approver"
|
|
13
|
+
when :admin_required then "collavre.comments.approve_admin_required"
|
|
14
|
+
else "collavre.comments.approve_not_allowed"
|
|
15
|
+
end
|
|
16
|
+
http_status = case status
|
|
17
|
+
when :invalid_action_format, :missing_action, :missing_approver
|
|
18
|
+
:unprocessable_entity
|
|
19
|
+
else
|
|
20
|
+
:forbidden
|
|
21
|
+
end
|
|
22
|
+
render json: { error: I18n.t(error_key) }, status: http_status and return
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
begin
|
|
26
|
+
::Comments::ActionExecutor.new(comment: @comment, executor: Current.user).call
|
|
27
|
+
@comment = Comment.with_attached_images.includes(:comment_reactions, :comment_versions, :selected_version).find(@comment.id)
|
|
28
|
+
render partial: "collavre/comments/comment", locals: { comment: @comment, current_topic_id: current_topic_context }
|
|
29
|
+
rescue ::Comments::ActionExecutor::ExecutionError => e
|
|
30
|
+
render json: { error: e.message }, status: :unprocessable_entity
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def update_action
|
|
35
|
+
# Initial checks outside the lock
|
|
36
|
+
executed_error = false
|
|
37
|
+
update_success = false
|
|
38
|
+
approver_mismatch_error = false
|
|
39
|
+
status_error_key = nil
|
|
40
|
+
status_http_status = nil
|
|
41
|
+
validation_error_message = nil
|
|
42
|
+
|
|
43
|
+
@comment.with_lock do
|
|
44
|
+
@comment.reload
|
|
45
|
+
|
|
46
|
+
status_in_lock = @comment.approval_status(Current.user)
|
|
47
|
+
# Allow repairing invalid format if user is approver
|
|
48
|
+
if status_in_lock == :invalid_action_format
|
|
49
|
+
if @comment.approver_id == Current.user&.id
|
|
50
|
+
status_in_lock = :ok
|
|
51
|
+
else
|
|
52
|
+
status_in_lock = :not_allowed
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
if status_in_lock != :ok
|
|
57
|
+
approver_mismatch_error = true
|
|
58
|
+
status_error_key = case status_in_lock
|
|
59
|
+
when :invalid_action_format then "collavre.comments.approve_invalid_format"
|
|
60
|
+
when :missing_action then "collavre.comments.approve_missing_action"
|
|
61
|
+
when :missing_approver then "collavre.comments.approve_missing_approver"
|
|
62
|
+
when :admin_required then "collavre.comments.approve_admin_required"
|
|
63
|
+
else "collavre.comments.approve_not_allowed"
|
|
64
|
+
end
|
|
65
|
+
status_http_status = case status_in_lock
|
|
66
|
+
when :invalid_action_format, :missing_action, :missing_approver
|
|
67
|
+
:unprocessable_entity
|
|
68
|
+
else
|
|
69
|
+
:forbidden
|
|
70
|
+
end
|
|
71
|
+
elsif @comment.action_executed_at.present?
|
|
72
|
+
executed_error = true
|
|
73
|
+
else
|
|
74
|
+
action_payload = params.dig(:comment, :action)
|
|
75
|
+
if action_payload.blank?
|
|
76
|
+
validation_error_message = I18n.t("collavre.comments.approve_missing_action")
|
|
77
|
+
else
|
|
78
|
+
begin
|
|
79
|
+
validator = ::Comments::ActionValidator.new(comment: @comment)
|
|
80
|
+
parsed_payload = validator.validate!(action_payload)
|
|
81
|
+
normalized_action = JSON.pretty_generate(parsed_payload)
|
|
82
|
+
update_success = @comment.update(action: normalized_action)
|
|
83
|
+
rescue ::Comments::ActionValidator::ValidationError => e
|
|
84
|
+
validation_error_message = e.message
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
if approver_mismatch_error
|
|
91
|
+
render json: { error: I18n.t(status_error_key) }, status: status_http_status
|
|
92
|
+
elsif validation_error_message
|
|
93
|
+
render json: { error: validation_error_message }, status: :unprocessable_entity
|
|
94
|
+
elsif executed_error
|
|
95
|
+
render json: { error: I18n.t("collavre.comments.approve_already_executed") }, status: :unprocessable_entity
|
|
96
|
+
elsif update_success
|
|
97
|
+
@comment = Comment.with_attached_images.includes(:comment_reactions, :comment_versions, :selected_version).find(@comment.id)
|
|
98
|
+
render partial: "collavre/comments/comment", locals: { comment: @comment, current_topic_id: current_topic_context }
|
|
99
|
+
else
|
|
100
|
+
error_message = @comment.errors.full_messages.to_sentence.presence || I18n.t("collavre.comments.action_update_error")
|
|
101
|
+
render json: { error: error_message }, status: :unprocessable_entity
|
|
102
|
+
end
|
|
103
|
+
rescue ::Comments::ActionValidator::ValidationError => e
|
|
104
|
+
render json: { error: e.message }, status: :unprocessable_entity
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
module Collavre
|
|
2
|
+
module Comments
|
|
3
|
+
module BatchOperations
|
|
4
|
+
extend ActiveSupport::Concern
|
|
5
|
+
|
|
6
|
+
MAX_BATCH_DELETE = 100
|
|
7
|
+
|
|
8
|
+
def batch_destroy
|
|
9
|
+
comment_ids = Array(params[:comment_ids]).map(&:to_i).uniq.first(MAX_BATCH_DELETE)
|
|
10
|
+
if comment_ids.empty?
|
|
11
|
+
render json: { error: I18n.t("collavre.comments.batch_delete_no_selection") }, status: :unprocessable_entity and return
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
is_admin = @creative.has_permission?(Current.user, :admin)
|
|
15
|
+
is_creative_owner = @creative.user == Current.user
|
|
16
|
+
|
|
17
|
+
visible_scope = @creative.comments.where(
|
|
18
|
+
"comments.private = ? OR comments.user_id = ? OR comments.approver_id = ?",
|
|
19
|
+
false, Current.user.id, Current.user.id
|
|
20
|
+
)
|
|
21
|
+
comments = visible_scope.where(id: comment_ids).to_a
|
|
22
|
+
|
|
23
|
+
if comments.length != comment_ids.length
|
|
24
|
+
render json: { error: I18n.t("collavre.comments.batch_delete_not_found") }, status: :not_found and return
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Check permissions: user must own all comments, or be admin/creative owner
|
|
28
|
+
unless is_admin || is_creative_owner
|
|
29
|
+
unauthorized = comments.reject { |c| c.user == Current.user }
|
|
30
|
+
if unauthorized.any?
|
|
31
|
+
render json: { error: I18n.t("collavre.comments.not_owner") }, status: :forbidden and return
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
Comment.where(id: comments.map(&:id)).destroy_all
|
|
36
|
+
|
|
37
|
+
head :no_content
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def move
|
|
41
|
+
result = CommentMoveService.new(creative: @creative, user: Current.user).call(
|
|
42
|
+
comment_ids: params[:comment_ids],
|
|
43
|
+
target_creative_id: params[:target_creative_id],
|
|
44
|
+
target_topic_id: params[:target_topic_id]
|
|
45
|
+
)
|
|
46
|
+
render json: result
|
|
47
|
+
rescue CommentMoveService::MoveError => e
|
|
48
|
+
status = e.message == I18n.t("collavre.comments.move_not_allowed") ? :forbidden : :unprocessable_entity
|
|
49
|
+
render json: { error: e.message }, status: status
|
|
50
|
+
rescue ActiveRecord::RecordInvalid => e
|
|
51
|
+
render json: { error: e.record.errors.full_messages.to_sentence.presence || I18n.t("collavre.comments.move_error") }, status: :unprocessable_entity
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
module Collavre
|
|
2
|
+
module Comments
|
|
3
|
+
module Conversion
|
|
4
|
+
extend ActiveSupport::Concern
|
|
5
|
+
|
|
6
|
+
def convert
|
|
7
|
+
unless can_convert_comment?
|
|
8
|
+
render json: { error: I18n.t("collavre.comments.convert_not_allowed") }, status: :forbidden and return
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
created_creatives = ::MarkdownImporter.import(
|
|
12
|
+
@comment.content,
|
|
13
|
+
parent: @creative,
|
|
14
|
+
user: @creative.user,
|
|
15
|
+
create_root: true
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
primary_creative = created_creatives.first
|
|
19
|
+
system_message = build_convert_system_message(primary_creative) if primary_creative
|
|
20
|
+
|
|
21
|
+
@comment.destroy
|
|
22
|
+
|
|
23
|
+
if system_message.present?
|
|
24
|
+
Current.set(session: nil) do
|
|
25
|
+
@creative.comments.create!(content: system_message, user: nil)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
head :no_content
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def can_convert_comment?
|
|
35
|
+
@comment.user == Current.user || @creative.has_permission?(Current.user, :admin)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def build_convert_system_message(creative)
|
|
39
|
+
title = helpers.strip_tags(creative.description).to_s.strip
|
|
40
|
+
title = I18n.t("collavre.comments.convert_system_message_default_title") if title.blank?
|
|
41
|
+
url = creative_path(creative)
|
|
42
|
+
I18n.t("collavre.comments.convert_system_message", title: title, url: url)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
module Collavre
|
|
2
|
+
module UsersController::AdminOperations
|
|
3
|
+
extend ActiveSupport::Concern
|
|
4
|
+
|
|
5
|
+
included do
|
|
6
|
+
before_action :require_system_admin!, only: [ :index, :grant_system_admin, :revoke_system_admin, :unlock, :lock ]
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def index
|
|
10
|
+
@users = Collavre::User.includes(:sessions, :devices)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def destroy
|
|
14
|
+
@user = Collavre::User.find(params[:id])
|
|
15
|
+
|
|
16
|
+
if @user == Current.user
|
|
17
|
+
redirect_to users_path, alert: I18n.t("collavre.users.destroy.cannot_delete_self")
|
|
18
|
+
return
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
allowed = Current.user.system_admin? ||
|
|
22
|
+
(@user.ai_user? && @user.created_by_id == Current.user.id)
|
|
23
|
+
|
|
24
|
+
unless allowed
|
|
25
|
+
fallback = user_path(Current.user, tab: "contacts")
|
|
26
|
+
redirect_back fallback_location: fallback, alert: I18n.t("collavre.users.destroy.not_authorized")
|
|
27
|
+
return
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
if @user.destroy
|
|
31
|
+
fallback = Current.user.system_admin? ? users_path : user_path(Current.user, tab: "contacts")
|
|
32
|
+
redirect_back fallback_location: fallback, notice: I18n.t("collavre.users.destroy.success")
|
|
33
|
+
else
|
|
34
|
+
redirect_to users_path, alert: I18n.t("collavre.users.destroy.failure")
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def grant_system_admin
|
|
39
|
+
@user = Collavre::User.find(params[:id])
|
|
40
|
+
|
|
41
|
+
if @user.update(system_admin: true)
|
|
42
|
+
redirect_to users_path, notice: I18n.t("collavre.users.system_admin.granted")
|
|
43
|
+
else
|
|
44
|
+
redirect_to users_path, alert: I18n.t("collavre.users.system_admin.failed")
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def revoke_system_admin
|
|
49
|
+
@user = Collavre::User.find(params[:id])
|
|
50
|
+
|
|
51
|
+
if @user.update(system_admin: false)
|
|
52
|
+
redirect_to users_path, notice: I18n.t("collavre.users.system_admin.revoked")
|
|
53
|
+
else
|
|
54
|
+
redirect_to users_path, alert: I18n.t("collavre.users.system_admin.failed")
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def unlock
|
|
59
|
+
@user = Collavre::User.find(params[:id])
|
|
60
|
+
@user.unlock_account!
|
|
61
|
+
redirect_to users_path, notice: I18n.t("collavre.users.unlock.success", name: @user.display_name)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def lock
|
|
65
|
+
@user = Collavre::User.find(params[:id])
|
|
66
|
+
if @user == Current.user
|
|
67
|
+
redirect_to users_path, alert: I18n.t("collavre.users.lock.cannot_lock_self")
|
|
68
|
+
return
|
|
69
|
+
end
|
|
70
|
+
@user.lock_account!
|
|
71
|
+
redirect_to users_path, notice: I18n.t("collavre.users.lock.success", name: @user.display_name)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|