diff --git a/README.md b/README.md index fbefe81c..27c4f7c7 100644 --- a/README.md +++ b/README.md @@ -21,9 +21,27 @@ Support companies that are providing open-source solutions! | Automated reminders | Done | Configure reminder intervals per-account. Pending signers receive scheduled follow-up emails. | | Template creation via API | Done | `POST /api/templates/pdf` and `PUT /api/templates/:id/documents` — create and manage templates programmatically with field coordinates or embedded text tags. | | Professional email design | Done | Table-based responsive email layout with company branding, styled CTA buttons, and proper footer. | +| Teams & user roles | Done | Multi-team support with admin/editor roles. Editors see only their team's documents. Admins can move folders between teams. | See [`docs/API.md`](docs/API.md) for full API reference on the new endpoints. +## Teams & Roles + +This fork implements team-based access control with two roles: + +| Role | Access | +|------|--------| +| **Admin** | Full access to all teams, users, settings, and resources in the account | +| **Editor** | Full access to templates, submissions, and documents within their team only. Can manage personalization, API keys, and webhooks. Cannot manage users, teams, or account settings. | + +**Key features:** +- Create multiple teams per account (Settings > Teams) +- Assign users to teams with role selection +- Editors are scoped to their team — they only see templates, submissions, and folders belonging to their team +- Admins can move entire folders (with all templates and submissions) to another team via the folder edit modal +- API tokens respect the user's role and team membership +- Migrations handle both greenfield installs and existing deployments (auto-creates a "Default" team and backfills) + ## What's NOT included These Pro features remain unavailable in this fork (they require significant UI/infrastructure work): diff --git a/app/controllers/api/templates_pdf_controller.rb b/app/controllers/api/templates_pdf_controller.rb index 27c9dad2..c36c1688 100644 --- a/app/controllers/api/templates_pdf_controller.rb +++ b/app/controllers/api/templates_pdf_controller.rb @@ -15,7 +15,8 @@ module Api author: current_user, source: :api, name: params[:name].presence || extract_default_name, - external_id: params[:external_id] + external_id: params[:external_id], + team_id: @template.team_id || current_user.team_id ) if params[:folder_name].present? diff --git a/app/controllers/profile_controller.rb b/app/controllers/profile_controller.rb index 1bbdb14a..8dd93679 100644 --- a/app/controllers/profile_controller.rb +++ b/app/controllers/profile_controller.rb @@ -2,7 +2,7 @@ class ProfileController < ApplicationController before_action do - authorize!(:manage, current_user) + authorize!(:update, current_user) end def index; end diff --git a/app/controllers/setup_controller.rb b/app/controllers/setup_controller.rb index 734edbb5..f1147e37 100644 --- a/app/controllers/setup_controller.rb +++ b/app/controllers/setup_controller.rb @@ -28,6 +28,8 @@ class SetupController < ApplicationController return render :index, status: :unprocessable_content unless @account.valid? + @user.team = @account.teams.new(name: 'Default') + if @user.save encrypted_configs = [ { key: EncryptedConfig::APP_URL_KEY, value: encrypted_config_params[:value] }, diff --git a/app/controllers/teams_controller.rb b/app/controllers/teams_controller.rb new file mode 100644 index 00000000..9a215c51 --- /dev/null +++ b/app/controllers/teams_controller.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +class TeamsController < ApplicationController + load_and_authorize_resource :team + + before_action :set_teams, only: :index + + def index; end + + def new; end + + def edit; end + + def create + @team.account = current_account + + if @team.save + redirect_back fallback_location: settings_teams_path, notice: I18n.t('team_has_been_created') + else + render turbo_stream: turbo_stream.replace(:modal, template: 'teams/new'), status: :unprocessable_content + end + end + + def update + if @team.update(team_params) + redirect_back fallback_location: settings_teams_path, notice: I18n.t('team_has_been_updated') + else + render turbo_stream: turbo_stream.replace(:modal, template: 'teams/edit'), status: :unprocessable_content + end + end + + def destroy + @team.update!(archived_at: Time.current) + + redirect_back fallback_location: settings_teams_path, notice: I18n.t('team_has_been_archived') + end + + private + + def set_teams + @teams = current_account.teams.active + .left_joins(:users) + .where(users: { archived_at: nil }) + .or(current_account.teams.active.left_joins(:users).where(users: { id: nil })) + .select('teams.*, COUNT(users.id) AS active_users_count') + .group('teams.id') + .order(:name) + end + + def team_params + params.require(:team).permit(:name) + end +end diff --git a/app/controllers/template_folders_controller.rb b/app/controllers/template_folders_controller.rb index 25e489ce..27a09cfe 100644 --- a/app/controllers/template_folders_controller.rb +++ b/app/controllers/template_folders_controller.rb @@ -43,8 +43,20 @@ class TemplateFoldersController < ApplicationController def edit; end def update - if @template_folder != current_account.default_template_folder && - @template_folder.update(template_folder_params) + if @template_folder == current_account.default_template_folder + redirect_to folder_path(@template_folder), alert: I18n.t('unable_to_rename_folder') + return + end + + new_team_id = template_folder_params[:team_id] + team_changed = new_team_id.present? && new_team_id.to_i != @template_folder.team_id + + if team_changed + authorize! :manage, Team.new(account: current_account) + target_team = current_account.teams.active.find(new_team_id) + move_folder_to_team(@template_folder, target_team) + redirect_to folder_path(@template_folder), notice: I18n.t('folder_has_been_moved') + elsif @template_folder.update(template_folder_params.except(:team_id)) redirect_to folder_path(@template_folder), notice: I18n.t('folder_name_has_been_updated') else redirect_to folder_path(@template_folder), alert: I18n.t('unable_to_rename_folder') @@ -64,7 +76,19 @@ class TemplateFoldersController < ApplicationController end def template_folder_params - params.require(:template_folder).permit(:name) + params.require(:template_folder).permit(:name, :team_id) + end + + def move_folder_to_team(folder, target_team) + template_ids = Template.where(folder_id: folder.id).pluck(:id) + submission_ids = Submission.where(template_id: template_ids).pluck(:id) + + ActiveRecord::Base.transaction do + folder.update!(team_id: target_team.id) + Template.where(id: template_ids).update_all(team_id: target_team.id) if template_ids.any? + Submission.where(id: submission_ids).update_all(team_id: target_team.id) if submission_ids.any? + Submitter.where(submission_id: submission_ids).update_all(team_id: target_team.id) if submission_ids.any? + end end def load_related_submissions diff --git a/app/controllers/templates_clone_controller.rb b/app/controllers/templates_clone_controller.rb index d001087f..2791f66e 100644 --- a/app/controllers/templates_clone_controller.rb +++ b/app/controllers/templates_clone_controller.rb @@ -29,6 +29,7 @@ class TemplatesCloneController < ApplicationController @template.account = current_account end + @template.team_id ||= current_user.team_id Templates.maybe_assign_access(@template) if @template.save diff --git a/app/controllers/templates_controller.rb b/app/controllers/templates_controller.rb index f32e1e1e..a6a5cbe8 100644 --- a/app/controllers/templates_controller.rb +++ b/app/controllers/templates_controller.rb @@ -52,6 +52,7 @@ class TemplatesController < ApplicationController @template.author = current_user @template.folder = TemplateFolders.find_or_create_by_name(current_user, params[:folder_name]) @template.account = current_account + @template.team_id ||= current_user.team_id Templates.maybe_assign_access(@template) diff --git a/app/controllers/templates_uploads_controller.rb b/app/controllers/templates_uploads_controller.rb index 7d8b1bae..88e012d9 100644 --- a/app/controllers/templates_uploads_controller.rb +++ b/app/controllers/templates_uploads_controller.rb @@ -43,6 +43,7 @@ class TemplatesUploadsController < ApplicationController def save_template!(template, url_params) template.account = current_account template.author = current_user + template.team_id ||= current_user.team_id template.folder = TemplateFolders.find_or_create_by_name(current_user, params[:folder_name]) template.name = File.basename((url_params || params)[:files].first.original_filename, '.*') diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 2d8f818f..262871fc 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -53,6 +53,7 @@ class UsersController < ApplicationController @user.password = SecureRandom.hex if @user.password.blank? @user.role = User::ADMIN_ROLE unless role_valid?(@user.role) + @user.team_id = current_user.team_id unless current_account.teams.exists?(id: @user.team_id) if @user.save UserMailer.invitation_email(@user).deliver_later! @@ -113,7 +114,7 @@ class UsersController < ApplicationController def user_params if params.key?(:user) - permitted_params = %i[email first_name last_name password archived_at otp_required_for_login] + permitted_params = %i[email first_name last_name password archived_at otp_required_for_login team_id] permitted_params << :role if role_valid?(params.dig(:user, :role)) diff --git a/app/models/account.rb b/app/models/account.rb index 6ca102ef..f987eced 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -22,6 +22,7 @@ class Account < ApplicationRecord has_one_attached :logo + has_many :teams, dependent: :destroy has_many :users, dependent: :destroy has_many :encrypted_configs, dependent: :destroy has_many :account_configs, dependent: :destroy @@ -71,4 +72,8 @@ class Account < ApplicationRecord super || build_default_template_folder(name: TemplateFolder::DEFAULT_NAME, author_id: users.minimum(:id)).tap(&:save!) end + + def default_team + teams.active.order(:id).first || teams.create!(name: 'Default') + end end diff --git a/app/models/submission.rb b/app/models/submission.rb index 92101b02..08748521 100644 --- a/app/models/submission.rb +++ b/app/models/submission.rb @@ -40,6 +40,7 @@ class Submission < ApplicationRecord belongs_to :template, optional: true belongs_to :account + belongs_to :team, optional: true belongs_to :created_by_user, class_name: 'User', optional: true has_one :search_entry, as: :record, inverse_of: :record, dependent: :destroy if SearchEntry.table_exists? diff --git a/app/models/submitter.rb b/app/models/submitter.rb index 0e4bc6c2..cb1b0abd 100644 --- a/app/models/submitter.rb +++ b/app/models/submitter.rb @@ -42,6 +42,7 @@ class Submitter < ApplicationRecord belongs_to :submission belongs_to :account + belongs_to :team, optional: true has_one :template, through: :submission has_one :search_entry, as: :record, inverse_of: :record, dependent: :destroy if SearchEntry.table_exists? has_many :submitter_versions, dependent: :destroy diff --git a/app/models/team.rb b/app/models/team.rb new file mode 100644 index 00000000..e2d95faa --- /dev/null +++ b/app/models/team.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +class Team < ApplicationRecord + belongs_to :account + + has_many :users, dependent: :nullify + has_many :templates, dependent: :nullify + has_many :submissions, dependent: :nullify + has_many :submitters, dependent: :nullify + has_many :template_folders, dependent: :nullify + + attribute :uuid, :string, default: -> { SecureRandom.uuid } + + scope :active, -> { where(archived_at: nil) } + scope :archived, -> { where.not(archived_at: nil) } + + validates :name, presence: true +end diff --git a/app/models/template.rb b/app/models/template.rb index 3c6f2e93..fe98d068 100644 --- a/app/models/template.rb +++ b/app/models/template.rb @@ -44,6 +44,7 @@ class Template < ApplicationRecord belongs_to :author, class_name: 'User' belongs_to :account belongs_to :folder, class_name: 'TemplateFolder' + belongs_to :team, optional: true has_one :search_entry, as: :record, inverse_of: :record, dependent: :destroy if SearchEntry.table_exists? diff --git a/app/models/template_folder.rb b/app/models/template_folder.rb index 58539a2e..3b827702 100644 --- a/app/models/template_folder.rb +++ b/app/models/template_folder.rb @@ -30,6 +30,7 @@ class TemplateFolder < ApplicationRecord belongs_to :author, class_name: 'User' belongs_to :account + belongs_to :team, optional: true belongs_to :parent_folder, class_name: 'TemplateFolder', optional: true has_many :templates, dependent: :destroy, foreign_key: :folder_id, inverse_of: :folder diff --git a/app/models/user.rb b/app/models/user.rb index b80ae769..ab6cd3d9 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -48,7 +48,8 @@ # class User < ApplicationRecord ROLES = [ - ADMIN_ROLE = 'admin' + ADMIN_ROLE = 'admin', + EDITOR_ROLE = 'editor' ].freeze EMAIL_REGEXP = /[^@;,<>\s]+@[^@;,<>\s]+/ @@ -60,6 +61,8 @@ class User < ApplicationRecord has_one_attached :initials belongs_to :account + belongs_to :team + has_one :access_token, dependent: :destroy has_many :access_tokens, dependent: :destroy has_many :mcp_tokens, dependent: :destroy @@ -77,6 +80,7 @@ class User < ApplicationRecord scope :active, -> { where(archived_at: nil) } scope :archived, -> { where.not(archived_at: nil) } scope :admins, -> { where(role: ADMIN_ROLE) } + scope :editors, -> { where(role: EDITOR_ROLE) } validates :email, format: { with: /\A[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\z/ } diff --git a/app/views/shared/_settings_nav.html.erb b/app/views/shared/_settings_nav.html.erb index 147b1c31..0450999f 100644 --- a/app/views/shared/_settings_nav.html.erb +++ b/app/views/shared/_settings_nav.html.erb @@ -9,9 +9,11 @@
  • <%= link_to t('profile'), settings_profile_index_path, class: 'text-base hover:bg-base-300' %>
  • -
  • - <%= link_to t('account'), settings_account_path, class: 'text-base hover:bg-base-300' %> -
  • + <% if can?(:manage, current_account) %> +
  • + <%= link_to t('account'), settings_account_path, class: 'text-base hover:bg-base-300' %> +
  • + <% end %> <% unless Docuseal.multitenant? %> <% if can?(:read, EncryptedConfig.new(key: EncryptedConfig::EMAIL_SMTP_KEY, account: current_account)) && ENV['SMTP_ADDRESS'].blank? && true_user == current_user %>
  • @@ -44,11 +46,16 @@ <%= link_to t('personalization'), settings_personalization_path, class: 'text-base hover:bg-base-300' %>
  • <% end %> - <% if can?(:read, User) %> + <% if can?(:read, User.new(account: current_account)) %>
  • <%= link_to t('users'), settings_users_path, class: 'text-base hover:bg-base-300' %>
  • <% end %> + <% if can?(:read, Team.new(account: current_account)) %> +
  • + <%= link_to t('teams'), settings_teams_path, class: 'text-base hover:bg-base-300' %> +
  • + <% end %> <%= render 'shared/settings_nav_extra' %> <% if Docuseal.demo? || !Docuseal.multitenant? %> <% if can?(:read, AccessToken) %> diff --git a/app/views/teams/_form.html.erb b/app/views/teams/_form.html.erb new file mode 100644 index 00000000..10a25ad0 --- /dev/null +++ b/app/views/teams/_form.html.erb @@ -0,0 +1,11 @@ +<%= form_for team, url: team.persisted? ? settings_team_path(team) : settings_teams_path, html: { class: 'space-y-4' }, data: { turbo_frame: :_top } do |f| %> +
    +
    + <%= f.label :name, t('name'), class: 'label' %> + <%= f.text_field :name, required: true, class: 'base-input w-full', dir: 'auto' %> +
    +
    +
    + <%= f.button team.persisted? ? t('save') : t('create'), class: 'base-button' %> +
    +<% end %> diff --git a/app/views/teams/edit.html.erb b/app/views/teams/edit.html.erb new file mode 100644 index 00000000..f12f6dee --- /dev/null +++ b/app/views/teams/edit.html.erb @@ -0,0 +1,3 @@ +<%= render 'shared/turbo_modal', title: t('edit_team') do %> + <%= render 'form', team: @team %> +<% end %> diff --git a/app/views/teams/index.html.erb b/app/views/teams/index.html.erb new file mode 100644 index 00000000..c15ec75e --- /dev/null +++ b/app/views/teams/index.html.erb @@ -0,0 +1,49 @@ +
    + <%= render 'shared/settings_nav' %> +
    +
    +

    + <%= t('teams') %> +

    +
    + <% if can?(:create, Team.new(account: current_account)) %> + <%= link_to new_settings_team_path, class: 'btn btn-primary btn-md gap-2 w-full md:w-fit', data: { turbo_frame: 'modal' } do %> + <%= svg_icon('plus', class: 'w-6 h-6') %> + <%= t('new_team') %> + <% end %> + <% end %> +
    +
    +
    + + + + + + + + + + <% @teams.each do |team| %> + + + + + + <% end %> + +
    <%= t('name') %><%= t('members') %>
    <%= team.name %><%= team.active_users_count %> + <% if can?(:update, team) %> + <%= link_to edit_settings_team_path(team), class: 'btn btn-outline btn-xs', title: t('edit'), data: { turbo_frame: 'modal' } do %> + <%= t('edit') %> + <% end %> + <% end %> + <% if can?(:destroy, team) && team.active_users_count == 0 %> + <%= button_to settings_team_path(team), method: :delete, class: 'btn btn-outline btn-error btn-xs', title: t('archive'), data: { turbo_confirm: t('are_you_sure_') } do %> + <%= t('archive') %> + <% end %> + <% end %> +
    +
    +
    +
    diff --git a/app/views/teams/new.html.erb b/app/views/teams/new.html.erb new file mode 100644 index 00000000..619747ca --- /dev/null +++ b/app/views/teams/new.html.erb @@ -0,0 +1,3 @@ +<%= render 'shared/turbo_modal', title: t('new_team') do %> + <%= render 'form', team: @team %> +<% end %> diff --git a/app/views/template_folders/edit.html.erb b/app/views/template_folders/edit.html.erb index 5ecb3a0d..3343c485 100644 --- a/app/views/template_folders/edit.html.erb +++ b/app/views/template_folders/edit.html.erb @@ -3,8 +3,16 @@
    <%= f.text_field :name, required: true, placeholder: "#{t('folder_name')}...", class: 'base-input w-full', autofocus: true, dir: 'auto' %>
    + <% if can?(:manage, Team.new(account: current_account)) %> +
    + + <%= f.select :team_id, current_account.teams.active.order(:name).map { |t| [t.name, t.id] }, { selected: @template_folder.team_id }, class: 'base-select w-full' %> +
    + <% end %>
    - <%= f.button button_title(title: t('rename'), disabled_with: t('saving')), class: 'base-button' %> + <%= f.button button_title(title: t('save'), disabled_with: t('saving')), class: 'base-button' %>
    <% end %> <% end %> diff --git a/app/views/users/_form.html.erb b/app/views/users/_form.html.erb index c7652916..550885d9 100644 --- a/app/views/users/_form.html.erb +++ b/app/views/users/_form.html.erb @@ -38,6 +38,12 @@ <% end %> <%= render 'role_select', f: %> + <% if can?(:manage, Team.new(account: current_account)) %> +
    + <%= f.label :team_id, t('team'), class: 'label' %> + <%= f.select :team_id, current_account.teams.active.order(:name).map { |t| [t.name, t.id] }, { selected: f.object.team_id }, class: 'base-select' %> +
    + <% end %> <% end %> <% if local_assigns[:extra_fields_html].present? %> <%= local_assigns[:extra_fields_html] %> diff --git a/app/views/users/_role_select.html.erb b/app/views/users/_role_select.html.erb index d14b2778..9fb56f69 100644 --- a/app/views/users/_role_select.html.erb +++ b/app/views/users/_role_select.html.erb @@ -1,20 +1,4 @@
    <%= f.label :role, class: 'label' %> - <%= f.select :role, nil, {}, class: 'base-select' do %> - - - - <% end %> - <% if Docuseal.multitenant? %> - - <% end %> - "> - <%= svg_icon('info_circle', class: 'w-4 h-4 inline align-text-bottom') %> - <%= t('unlock_more_user_roles_with_docuseal_pro') %> - <%= t('learn_more') %> - + <%= f.select :role, [['Admin', 'admin'], ['Editor', 'editor']], { selected: f.object.role }, class: 'base-select' %>
    diff --git a/config/locales/i18n.yml b/config/locales/i18n.yml index 02a462cc..eacf6304 100644 --- a/config/locales/i18n.yml +++ b/config/locales/i18n.yml @@ -570,6 +570,7 @@ en: &en form_has_been_declined: Form has been declined. file_is_missing: File is missing folder_name_has_been_updated: Folder name has been updated. + folder_has_been_moved: Folder has been moved to the selected team. unable_to_rename_folder: Unable to rename folder. template_has_been_removed: Template has been removed. template_has_been_archived: Template has been archived. diff --git a/config/routes.rb b/config/routes.rb index 0f482850..1556c0fa 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -190,6 +190,7 @@ Rails.application.routes.draw do resources :sso, only: %i[index], controller: 'sso_settings' resources :notifications, only: %i[index create], controller: 'notifications_settings' resource :esign, only: %i[show create new update destroy], controller: 'esign_settings' + resources :teams, only: %i[index new create edit update destroy] resources :users, only: %i[index] resources :archived_users, only: %i[index], path: 'users/:status', controller: 'users', defaults: { status: :archived } diff --git a/db/migrate/20260508100000_create_teams.rb b/db/migrate/20260508100000_create_teams.rb new file mode 100644 index 00000000..008f309d --- /dev/null +++ b/db/migrate/20260508100000_create_teams.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class CreateTeams < ActiveRecord::Migration[8.0] + def change + create_table :teams do |t| + t.string :name, null: false + t.references :account, null: false, foreign_key: true + t.string :uuid, null: false + t.datetime :archived_at + + t.timestamps + end + + add_index :teams, :uuid, unique: true + add_index :teams, %i[account_id name], unique: true, where: "archived_at IS NULL" + end +end diff --git a/db/migrate/20260508100001_add_team_id_to_resources.rb b/db/migrate/20260508100001_add_team_id_to_resources.rb new file mode 100644 index 00000000..f3ffd792 --- /dev/null +++ b/db/migrate/20260508100001_add_team_id_to_resources.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class AddTeamIdToResources < ActiveRecord::Migration[8.0] + def change + add_reference :users, :team, foreign_key: true + add_reference :templates, :team, foreign_key: true + add_reference :submissions, :team, foreign_key: true + add_reference :submitters, :team, foreign_key: true + add_reference :template_folders, :team, foreign_key: true + end +end diff --git a/db/migrate/20260508100002_backfill_teams.rb b/db/migrate/20260508100002_backfill_teams.rb new file mode 100644 index 00000000..0c8da303 --- /dev/null +++ b/db/migrate/20260508100002_backfill_teams.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +class BackfillTeams < ActiveRecord::Migration[8.0] + def up + Account.find_each do |account| + ActiveRecord::Base.transaction do + team = Team.create!( + name: 'Default', + account: account, + uuid: SecureRandom.uuid + ) + + User.where(account_id: account.id, team_id: nil).update_all(team_id: team.id) + Template.where(account_id: account.id, team_id: nil).update_all(team_id: team.id) + Submission.where(account_id: account.id, team_id: nil).update_all(team_id: team.id) + Submitter.where(account_id: account.id, team_id: nil).update_all(team_id: team.id) + TemplateFolder.where(account_id: account.id, team_id: nil).update_all(team_id: team.id) + end + end + + change_column_null :users, :team_id, false + end + + def down + change_column_null :users, :team_id, true + end +end diff --git a/lib/ability.rb b/lib/ability.rb index da472f58..35161c9b 100644 --- a/lib/ability.rb +++ b/lib/ability.rb @@ -4,6 +4,23 @@ class Ability include CanCan::Ability def initialize(user) + return unless user + + can :manage, EncryptedUserConfig, user_id: user.id + can :manage, UserConfig, user_id: user.id + can :manage, AccessToken, user_id: user.id + can :manage, McpToken, user_id: user.id + + if user.role == User::ADMIN_ROLE + admin_abilities(user) + elsif user.role == User::EDITOR_ROLE + editor_abilities(user) + end + end + + private + + def admin_abilities(user) can %i[read create update], Template, Abilities::TemplateConditions.collection(user) do |template| Abilities::TemplateConditions.entity(template, user:, ability: 'manage') end @@ -15,14 +32,25 @@ class Ability can :manage, Submitter, account_id: user.account_id can :manage, User, account_id: user.account_id can :manage, EncryptedConfig, account_id: user.account_id - can :manage, EncryptedUserConfig, user_id: user.id can :manage, AccountConfig, account_id: user.account_id - can :manage, UserConfig, user_id: user.id can :manage, Account, id: user.account_id - can :manage, AccessToken, user_id: user.id - can :manage, McpToken, user_id: user.id can :manage, WebhookUrl, account_id: user.account_id - + can :manage, Team, account_id: user.account_id can :manage, :mcp end + + def editor_abilities(user) + can %i[read create update], Template, team_id: user.team_id, account_id: user.account_id + can :destroy, Template, team_id: user.team_id, account_id: user.account_id + can :manage, TemplateFolder, team_id: user.team_id, account_id: user.account_id + can :read, TemplateSharing, template: { team_id: user.team_id, account_id: user.account_id } + can :manage, Submission, team_id: user.team_id, account_id: user.account_id + can :manage, Submitter, team_id: user.team_id, account_id: user.account_id + can :manage, AccountConfig, account_id: user.account_id + can :manage, WebhookUrl, account_id: user.account_id + + can :read, User, id: user.id + can :update, User, id: user.id + can :read, Team, id: user.team_id + end end diff --git a/lib/submissions.rb b/lib/submissions.rb index 33081926..f1cc4e6c 100644 --- a/lib/submissions.rb +++ b/lib/submissions.rb @@ -93,6 +93,7 @@ module Submissions parse_emails(emails, user).uniq.map do |email| submission = template.submissions.new(created_by_user: user, account_id: user.account_id, + team_id: template.team_id, source:, expire_at:, template_submitters: template.submitters) @@ -100,6 +101,7 @@ module Submissions submission.submitters.new(email: normalize_email(email), uuid: template.submitters.first['uuid'], account_id: user.account_id, + team_id: template.team_id, preferences:, sent_at: mark_as_sent ? Time.current : nil) diff --git a/lib/submissions/create_from_submitters.rb b/lib/submissions/create_from_submitters.rb index 93d5a0e3..6ba70ab2 100644 --- a/lib/submissions/create_from_submitters.rb +++ b/lib/submissions/create_from_submitters.rb @@ -22,6 +22,7 @@ module Submissions submission = template.submissions.new( created_by_user: user, source:, account_id: user.account_id, + team_id: template.team_id, preferences: set_submission_preferences, name: with_template ? attrs[:name] : (attrs[:name].presence || template.name), variables: attrs[:variables] || {}, @@ -375,6 +376,7 @@ module Submissions phone: (attrs[:phone] || values[phone_field_uuid]).to_s.gsub(/[^0-9+]/, ''), name: attrs[:name], account_id: user.account_id, + team_id: submission.team_id, external_id: attrs[:external_id].presence || attrs[:application_key], completed_at: attrs[:completed].present? ? Time.current : nil, values: values.except(phone_field_uuid), diff --git a/lib/template_folders.rb b/lib/template_folders.rb index d4a3af9e..29558671 100644 --- a/lib/template_folders.rb +++ b/lib/template_folders.rb @@ -82,12 +82,12 @@ module TemplateFolders parent_name, name = name.to_s.split(' / ', 2).map(&:squish) if name.present? - parent_folder = author.account.template_folders.create_with(author:) + parent_folder = author.account.template_folders.create_with(author:, team_id: author.team_id) .find_or_create_by(name: parent_name, parent_folder_id: nil) else name = parent_name end - author.account.template_folders.create_with(author:).find_or_create_by(name:, parent_folder:) + author.account.template_folders.create_with(author:, team_id: author.team_id).find_or_create_by(name:, parent_folder:) end end