Initial commit: Redmine Role Groups Plugin
🤖 Generated with Claude Code
This commit is contained in:
commit
16263fdccb
225
app/controllers/role_groups_controller.rb
Normal file
225
app/controllers/role_groups_controller.rb
Normal file
@ -0,0 +1,225 @@
|
|||||||
|
class RoleGroupsController < ApplicationController
|
||||||
|
layout 'admin'
|
||||||
|
before_action :require_admin
|
||||||
|
before_action :find_role_group, only: [:show, :edit, :update, :destroy, :add_member, :remove_member]
|
||||||
|
|
||||||
|
def index
|
||||||
|
@role_groups = RoleGroup.sorted.includes(:users)
|
||||||
|
end
|
||||||
|
|
||||||
|
def show
|
||||||
|
@members = @role_group.role_group_members.includes(:user).order('users.lastname, users.firstname')
|
||||||
|
end
|
||||||
|
|
||||||
|
def new
|
||||||
|
@role_group = RoleGroup.new
|
||||||
|
end
|
||||||
|
|
||||||
|
def create
|
||||||
|
@role_group = RoleGroup.new(role_group_params)
|
||||||
|
if @role_group.save
|
||||||
|
flash[:notice] = l(:notice_successful_create)
|
||||||
|
redirect_to '/role_groups'
|
||||||
|
else
|
||||||
|
render :new
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def edit
|
||||||
|
end
|
||||||
|
|
||||||
|
def update
|
||||||
|
if @role_group.update(role_group_params)
|
||||||
|
flash[:notice] = l(:notice_successful_update)
|
||||||
|
redirect_to '/role_groups'
|
||||||
|
else
|
||||||
|
render :edit
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
@role_group.destroy
|
||||||
|
flash[:notice] = l(:notice_successful_delete)
|
||||||
|
redirect_to '/role_groups'
|
||||||
|
end
|
||||||
|
|
||||||
|
def add_member
|
||||||
|
user = find_or_create_user_from_ldap(params[:user_id], params[:ldap_uid], params[:ldap_id])
|
||||||
|
|
||||||
|
if user
|
||||||
|
member = @role_group.role_group_members.find_or_initialize_by(user: user)
|
||||||
|
member.note = params[:note]
|
||||||
|
if member.save
|
||||||
|
flash[:notice] = "#{user.name} 추가됨"
|
||||||
|
else
|
||||||
|
flash[:error] = member.errors.full_messages.join(', ')
|
||||||
|
end
|
||||||
|
else
|
||||||
|
flash[:error] = '사용자를 찾을 수 없습니다'
|
||||||
|
end
|
||||||
|
redirect_to role_group_path(@role_group)
|
||||||
|
end
|
||||||
|
|
||||||
|
def remove_member
|
||||||
|
member = @role_group.role_group_members.find_by(user_id: params[:user_id])
|
||||||
|
if member
|
||||||
|
member.destroy
|
||||||
|
flash[:notice] = '멤버 제거됨'
|
||||||
|
end
|
||||||
|
redirect_to role_group_path(@role_group)
|
||||||
|
end
|
||||||
|
|
||||||
|
def search_ldap
|
||||||
|
query = params[:q].to_s.strip
|
||||||
|
if query.length < 2
|
||||||
|
render json: []
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
results = []
|
||||||
|
|
||||||
|
AuthSourceLdap.all.each do |ldap|
|
||||||
|
begin
|
||||||
|
ldap_users = search_ldap_users(ldap, query)
|
||||||
|
ldap_users.each do |entry|
|
||||||
|
uid = entry[ldap.attr_login]&.first
|
||||||
|
next unless uid
|
||||||
|
|
||||||
|
existing_user = User.find_by(login: uid)
|
||||||
|
|
||||||
|
results << {
|
||||||
|
uid: uid,
|
||||||
|
name: "#{entry[ldap.attr_firstname]&.first} #{entry[ldap.attr_lastname]&.first}".strip,
|
||||||
|
email: entry[ldap.attr_mail]&.first,
|
||||||
|
ldap_id: ldap.id,
|
||||||
|
exists: existing_user.present?,
|
||||||
|
user_id: existing_user&.id
|
||||||
|
}
|
||||||
|
end
|
||||||
|
rescue => e
|
||||||
|
Rails.logger.error "LDAP search error: #{e.message}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
render json: results.uniq { |r| r[:uid] }.first(20)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def find_role_group
|
||||||
|
@role_group = RoleGroup.find(params[:id])
|
||||||
|
rescue ActiveRecord::RecordNotFound
|
||||||
|
render_404
|
||||||
|
end
|
||||||
|
|
||||||
|
def role_group_params
|
||||||
|
params.require(:role_group).permit(:name, :description, :color, :position)
|
||||||
|
end
|
||||||
|
|
||||||
|
def search_ldap_users(ldap, query)
|
||||||
|
options = {
|
||||||
|
host: ldap.host,
|
||||||
|
port: ldap.port,
|
||||||
|
auth: {
|
||||||
|
method: :simple,
|
||||||
|
username: ldap.account,
|
||||||
|
password: ldap.account_password
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ldap.tls
|
||||||
|
if ldap.verify_peer
|
||||||
|
options[:encryption] = { method: :simple_tls }
|
||||||
|
else
|
||||||
|
options[:encryption] = { method: :simple_tls, tls_options: { verify_mode: OpenSSL::SSL::VERIFY_NONE } }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
conn = Net::LDAP.new(options)
|
||||||
|
|
||||||
|
filter = Net::LDAP::Filter.begins(ldap.attr_login, query) |
|
||||||
|
Net::LDAP::Filter.begins(ldap.attr_firstname, query) |
|
||||||
|
Net::LDAP::Filter.begins(ldap.attr_lastname, query)
|
||||||
|
|
||||||
|
if ldap.filter.present?
|
||||||
|
base_filter = Net::LDAP::Filter.construct(ldap.filter)
|
||||||
|
filter = base_filter & filter
|
||||||
|
end
|
||||||
|
|
||||||
|
conn.search(
|
||||||
|
base: ldap.base_dn,
|
||||||
|
filter: filter,
|
||||||
|
attributes: [ldap.attr_login, ldap.attr_firstname, ldap.attr_lastname, ldap.attr_mail],
|
||||||
|
size: 20
|
||||||
|
) || []
|
||||||
|
end
|
||||||
|
|
||||||
|
def find_or_create_user_from_ldap(user_id, ldap_uid, ldap_id)
|
||||||
|
return User.find(user_id) if user_id.present?
|
||||||
|
|
||||||
|
return nil if ldap_uid.blank?
|
||||||
|
|
||||||
|
user = User.find_by(login: ldap_uid)
|
||||||
|
return user if user
|
||||||
|
|
||||||
|
ldap = AuthSourceLdap.find_by(id: ldap_id)
|
||||||
|
return nil unless ldap
|
||||||
|
|
||||||
|
user_info = get_ldap_user_info(ldap, ldap_uid)
|
||||||
|
return nil unless user_info
|
||||||
|
|
||||||
|
user = User.new(
|
||||||
|
login: ldap_uid,
|
||||||
|
firstname: user_info[:firstname] || ldap_uid,
|
||||||
|
lastname: user_info[:lastname] || '-',
|
||||||
|
mail: user_info[:mail] || "#{ldap_uid}@example.com",
|
||||||
|
auth_source_id: ldap.id,
|
||||||
|
status: User::STATUS_ACTIVE
|
||||||
|
)
|
||||||
|
user.random_password
|
||||||
|
user.save ? user : nil
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_ldap_user_info(ldap, uid)
|
||||||
|
options = {
|
||||||
|
host: ldap.host,
|
||||||
|
port: ldap.port,
|
||||||
|
auth: {
|
||||||
|
method: :simple,
|
||||||
|
username: ldap.account,
|
||||||
|
password: ldap.account_password
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ldap.tls
|
||||||
|
if ldap.verify_peer
|
||||||
|
options[:encryption] = { method: :simple_tls }
|
||||||
|
else
|
||||||
|
options[:encryption] = { method: :simple_tls, tls_options: { verify_mode: OpenSSL::SSL::VERIFY_NONE } }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
conn = Net::LDAP.new(options)
|
||||||
|
|
||||||
|
filter = Net::LDAP::Filter.eq(ldap.attr_login, uid)
|
||||||
|
if ldap.filter.present?
|
||||||
|
base_filter = Net::LDAP::Filter.construct(ldap.filter)
|
||||||
|
filter = base_filter & filter
|
||||||
|
end
|
||||||
|
|
||||||
|
result = conn.search(
|
||||||
|
base: ldap.base_dn,
|
||||||
|
filter: filter,
|
||||||
|
attributes: [ldap.attr_login, ldap.attr_firstname, ldap.attr_lastname, ldap.attr_mail],
|
||||||
|
size: 1
|
||||||
|
)&.first
|
||||||
|
|
||||||
|
return nil unless result
|
||||||
|
|
||||||
|
{
|
||||||
|
firstname: result[ldap.attr_firstname]&.first,
|
||||||
|
lastname: result[ldap.attr_lastname]&.first,
|
||||||
|
mail: result[ldap.attr_mail]&.first
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
12
app/models/role_group.rb
Normal file
12
app/models/role_group.rb
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
class RoleGroup < ActiveRecord::Base
|
||||||
|
has_many :role_group_members, dependent: :destroy
|
||||||
|
has_many :users, through: :role_group_members
|
||||||
|
|
||||||
|
validates :name, presence: true, uniqueness: true
|
||||||
|
|
||||||
|
scope :sorted, -> { order(:position, :name) }
|
||||||
|
|
||||||
|
def member_count
|
||||||
|
users.count
|
||||||
|
end
|
||||||
|
end
|
||||||
7
app/models/role_group_member.rb
Normal file
7
app/models/role_group_member.rb
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
class RoleGroupMember < ActiveRecord::Base
|
||||||
|
belongs_to :role_group
|
||||||
|
belongs_to :user
|
||||||
|
|
||||||
|
validates :role_group_id, presence: true
|
||||||
|
validates :user_id, presence: true, uniqueness: { scope: :role_group_id }
|
||||||
|
end
|
||||||
23
app/views/role_groups/_form.html.erb
Normal file
23
app/views/role_groups/_form.html.erb
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<fieldset class="box tabular">
|
||||||
|
<legend>역할 그룹 정보</legend>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<%= f.label :name, '그룹명' %> <span class="required">*</span>
|
||||||
|
<%= f.text_field :name, size: 40, required: true, placeholder: 'DBA, 시스템, 방화벽 등' %>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<%= f.label :description, '설명' %>
|
||||||
|
<%= f.text_area :description, rows: 3, cols: 60, placeholder: '이 역할 그룹에 대한 설명' %>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<%= f.label :color, '색상' %>
|
||||||
|
<%= f.color_field :color, value: (@role_group.color || '#007bff') %>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<%= f.label :position, '순서' %>
|
||||||
|
<%= f.number_field :position, value: (@role_group.position || 0), min: 0 %>
|
||||||
|
</p>
|
||||||
|
</fieldset>
|
||||||
9
app/views/role_groups/edit.html.erb
Normal file
9
app/views/role_groups/edit.html.erb
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<h2>역할 그룹 수정: <%= @role_group.name %></h2>
|
||||||
|
|
||||||
|
<%= form_for @role_group, url: update_role_group_path(@role_group), method: :patch, html: { class: 'tabular' } do |f| %>
|
||||||
|
<%= render partial: 'form', locals: { f: f } %>
|
||||||
|
<p>
|
||||||
|
<%= f.submit '저장', class: 'button-positive' %>
|
||||||
|
<%= link_to '취소', '/role_groups', class: 'button' %>
|
||||||
|
</p>
|
||||||
|
<% end %>
|
||||||
38
app/views/role_groups/index.html.erb
Normal file
38
app/views/role_groups/index.html.erb
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
<h2>역할 그룹 관리</h2>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<%= link_to '새 역할 그룹', new_role_group_path, class: 'icon icon-add' %>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<% if @role_groups.any? %>
|
||||||
|
<table class="list">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>역할 그룹</th>
|
||||||
|
<th>설명</th>
|
||||||
|
<th>멤버 수</th>
|
||||||
|
<th>작업</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<% @role_groups.each do |group| %>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<span style="display: inline-block; width: 12px; height: 12px; background: <%= group.color %>; border-radius: 2px; margin-right: 8px;"></span>
|
||||||
|
<strong><%= link_to group.name, role_group_path(group) %></strong>
|
||||||
|
</td>
|
||||||
|
<td><%= truncate(group.description, length: 50) %></td>
|
||||||
|
<td><%= group.member_count %>명</td>
|
||||||
|
<td>
|
||||||
|
<%= link_to '관리', role_group_path(group), class: 'icon icon-user' %>
|
||||||
|
<%= link_to '수정', edit_role_group_path(group), class: 'icon icon-edit' %>
|
||||||
|
<%= link_to '삭제', destroy_role_group_path(group), method: :delete,
|
||||||
|
data: { confirm: '정말 삭제하시겠습니까?' }, class: 'icon icon-del' %>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<% end %>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<% else %>
|
||||||
|
<p class="nodata">등록된 역할 그룹이 없습니다.</p>
|
||||||
|
<% end %>
|
||||||
9
app/views/role_groups/new.html.erb
Normal file
9
app/views/role_groups/new.html.erb
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<h2>새 역할 그룹</h2>
|
||||||
|
|
||||||
|
<%= form_for @role_group, url: '/role_groups/create', html: { class: 'tabular' } do |f| %>
|
||||||
|
<%= render partial: 'form', locals: { f: f } %>
|
||||||
|
<p>
|
||||||
|
<%= f.submit '생성', class: 'button-positive' %>
|
||||||
|
<%= link_to '취소', '/role_groups', class: 'button' %>
|
||||||
|
</p>
|
||||||
|
<% end %>
|
||||||
125
app/views/role_groups/show.html.erb
Normal file
125
app/views/role_groups/show.html.erb
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
<h2>
|
||||||
|
<span style="display: inline-block; width: 16px; height: 16px; background: <%= @role_group.color %>; border-radius: 3px; margin-right: 10px; vertical-align: middle;"></span>
|
||||||
|
<%= @role_group.name %>
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<% if @role_group.description.present? %>
|
||||||
|
<p><%= @role_group.description %></p>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<h3>멤버 (<%= @members.count %>명)</h3>
|
||||||
|
|
||||||
|
<% if @members.any? %>
|
||||||
|
<table class="list">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>이름</th>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>이메일</th>
|
||||||
|
<th>비고</th>
|
||||||
|
<th>작업</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<% @members.each do |member| %>
|
||||||
|
<tr>
|
||||||
|
<td><%= link_to member.user.name, user_path(member.user) %></td>
|
||||||
|
<td><%= member.user.login %></td>
|
||||||
|
<td><%= member.user.mail %></td>
|
||||||
|
<td><%= member.note %></td>
|
||||||
|
<td>
|
||||||
|
<%= link_to '제거', remove_member_role_group_path(@role_group, user_id: member.user_id),
|
||||||
|
method: :delete, data: { confirm: '이 멤버를 제거하시겠습니까?' }, class: 'icon icon-del' %>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<% end %>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<% else %>
|
||||||
|
<p class="nodata">멤버가 없습니다.</p>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<h3>멤버 추가 (LDAP 검색)</h3>
|
||||||
|
|
||||||
|
<div class="box">
|
||||||
|
<p>
|
||||||
|
<label for="ldap_search">사용자 검색:</label>
|
||||||
|
<input type="text" id="ldap_search" size="30" placeholder="이름 또는 ID로 검색..." autocomplete="off" />
|
||||||
|
<span id="search_status" style="margin-left: 10px; color: #666;"></span>
|
||||||
|
</p>
|
||||||
|
<div id="search_results" style="margin-top: 10px;"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<%= link_to '목록으로', '/role_groups', class: 'icon icon-back' %>
|
||||||
|
<%= link_to '수정', edit_role_group_path(@role_group), class: 'icon icon-edit' %>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<%= javascript_tag do %>
|
||||||
|
var searchTimeout = null;
|
||||||
|
|
||||||
|
$('#ldap_search').on('keyup', function() {
|
||||||
|
var query = $(this).val();
|
||||||
|
|
||||||
|
if (searchTimeout) clearTimeout(searchTimeout);
|
||||||
|
|
||||||
|
if (query.length < 2) {
|
||||||
|
$('#search_results').html('');
|
||||||
|
$('#search_status').text('');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$('#search_status').text('검색 중...');
|
||||||
|
|
||||||
|
searchTimeout = setTimeout(function() {
|
||||||
|
$.ajax({
|
||||||
|
url: '<%= search_ldap_role_groups_path %>',
|
||||||
|
data: { q: query },
|
||||||
|
dataType: 'json',
|
||||||
|
success: function(data) {
|
||||||
|
$('#search_status').text(data.length + '명 발견');
|
||||||
|
var html = '';
|
||||||
|
|
||||||
|
if (data.length === 0) {
|
||||||
|
html = '<p style="color: #999;">검색 결과가 없습니다.</p>';
|
||||||
|
} else {
|
||||||
|
html = '<table class="list" style="width: auto;"><thead><tr><th>ID</th><th>이름</th><th>이메일</th><th>상태</th><th></th></tr></thead><tbody>';
|
||||||
|
data.forEach(function(user) {
|
||||||
|
html += '<tr>';
|
||||||
|
html += '<td>' + user.uid + '</td>';
|
||||||
|
html += '<td>' + user.name + '</td>';
|
||||||
|
html += '<td>' + (user.email || '-') + '</td>';
|
||||||
|
html += '<td>' + (user.exists ? '<span style="color: green;">등록됨</span>' : '<span style="color: blue;">LDAP</span>') + '</td>';
|
||||||
|
html += '<td>';
|
||||||
|
html += '<form action="<%= add_member_role_group_path(@role_group) %>" method="post" style="display:inline;">';
|
||||||
|
html += '<input type="hidden" name="authenticity_token" value="' + $('meta[name="csrf-token"]').attr('content') + '">';
|
||||||
|
if (user.exists) {
|
||||||
|
html += '<input type="hidden" name="user_id" value="' + user.user_id + '">';
|
||||||
|
} else {
|
||||||
|
html += '<input type="hidden" name="ldap_uid" value="' + user.uid + '">';
|
||||||
|
html += '<input type="hidden" name="ldap_id" value="' + user.ldap_id + '">';
|
||||||
|
}
|
||||||
|
html += '<input type="submit" value="추가" class="button-positive" style="padding: 2px 10px;">';
|
||||||
|
html += '</form>';
|
||||||
|
html += '</td>';
|
||||||
|
html += '</tr>';
|
||||||
|
});
|
||||||
|
html += '</tbody></table>';
|
||||||
|
}
|
||||||
|
|
||||||
|
$('#search_results').html(html);
|
||||||
|
},
|
||||||
|
error: function() {
|
||||||
|
$('#search_status').text('검색 오류');
|
||||||
|
$('#search_results').html('<p style="color: red;">검색 중 오류가 발생했습니다.</p>');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, 300);
|
||||||
|
});
|
||||||
|
<% end %>
|
||||||
12
config/routes.rb
Normal file
12
config/routes.rb
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
Rails.application.routes.draw do
|
||||||
|
get 'role_groups', to: 'role_groups#index'
|
||||||
|
get 'role_groups/new', to: 'role_groups#new', as: 'new_role_group'
|
||||||
|
get 'role_groups/search_ldap', to: 'role_groups#search_ldap', as: 'search_ldap_role_groups'
|
||||||
|
post 'role_groups/create', to: 'role_groups#create'
|
||||||
|
get 'role_groups/:id', to: 'role_groups#show', as: 'role_group'
|
||||||
|
get 'role_groups/:id/edit', to: 'role_groups#edit', as: 'edit_role_group'
|
||||||
|
patch 'role_groups/:id', to: 'role_groups#update', as: 'update_role_group'
|
||||||
|
delete 'role_groups/:id', to: 'role_groups#destroy', as: 'destroy_role_group'
|
||||||
|
post 'role_groups/:id/add_member', to: 'role_groups#add_member', as: 'add_member_role_group'
|
||||||
|
delete 'role_groups/:id/remove_member/:user_id', to: 'role_groups#remove_member', as: 'remove_member_role_group'
|
||||||
|
end
|
||||||
13
db/migrate/001_create_role_groups.rb
Normal file
13
db/migrate/001_create_role_groups.rb
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
class CreateRoleGroups < ActiveRecord::Migration[6.1]
|
||||||
|
def change
|
||||||
|
create_table :role_groups do |t|
|
||||||
|
t.string :name, null: false
|
||||||
|
t.text :description
|
||||||
|
t.string :color, default: '#007bff'
|
||||||
|
t.integer :position, default: 0
|
||||||
|
t.timestamps
|
||||||
|
end
|
||||||
|
|
||||||
|
add_index :role_groups, :name, unique: true
|
||||||
|
end
|
||||||
|
end
|
||||||
16
db/migrate/002_create_role_group_members.rb
Normal file
16
db/migrate/002_create_role_group_members.rb
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
class CreateRoleGroupMembers < ActiveRecord::Migration[6.1]
|
||||||
|
def change
|
||||||
|
create_table :role_group_members do |t|
|
||||||
|
t.bigint :role_group_id, null: false
|
||||||
|
t.integer :user_id, null: false
|
||||||
|
t.text :note
|
||||||
|
t.timestamps
|
||||||
|
end
|
||||||
|
|
||||||
|
add_index :role_group_members, :role_group_id
|
||||||
|
add_index :role_group_members, :user_id
|
||||||
|
add_index :role_group_members, [:role_group_id, :user_id], unique: true
|
||||||
|
add_foreign_key :role_group_members, :role_groups
|
||||||
|
add_foreign_key :role_group_members, :users
|
||||||
|
end
|
||||||
|
end
|
||||||
9
init.rb
Normal file
9
init.rb
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
Redmine::Plugin.register :role_groups do
|
||||||
|
name '역할 그룹 관리'
|
||||||
|
author 'Admin'
|
||||||
|
description 'Role-based grouping for users (DBA, System, Firewall, etc.)'
|
||||||
|
version '1.0.0'
|
||||||
|
|
||||||
|
menu :admin_menu, :role_groups, { controller: 'role_groups', action: 'index' },
|
||||||
|
caption: '역할 그룹', html: { class: 'icon icon-group' }
|
||||||
|
end
|
||||||
Loading…
Reference in New Issue
Block a user