Page MenuHomeGRNET

No OneTemporary

File Metadata

Created
Sun, May 18, 1:35 AM
diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css
index 3efd037..7f63099 100644
--- a/app/assets/stylesheets/application.css
+++ b/app/assets/stylesheets/application.css
@@ -1,51 +1,55 @@
/*
* This is a manifest file that'll be compiled into application.css, which will include all the files
* listed below.
*
* Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
* or vendor/assets/stylesheets of plugins, if any, can be referenced here using a relative path.
*
* You're free to add application-wide styles to this file and they'll appear at the bottom of the
* compiled file so the styles you add here take precedence over styles defined in any styles
* defined in the other CSS/SCSS files in this directory. It is generally better to create a new
* file per style scope.
*
*= require bootstrap.min
*= require dataTables.bootstrap.min
*= require bootstrap-editable
*= require_tree .
*= require_self
*/
/* Make sure navbar does not overlay body */
body {
padding-top: 70px;
}
.highlight , .highlight>td {
background-color: #b8e0b8 !important;
}
/* DataTable resets bootstrap's margin-bottom */
.datatable-wrapper {
margin-bottom: 20px;
}
/* Reset bootstrap's help cursor on control links */
table a abbr[title] {
cursor: pointer;
}
.tab-pane table {
margin-top: 20px;
}
#inline-record-form #record_ttl {
width: 80px;
}
#inline-record-form #record_prio {
width: 80px;
}
#inline-record-form #record_content {
width: 300px;
}
+
+#domains span.glyphicon-volume-up {
+ color: red;
+}
diff --git a/app/controllers/domains_controller.rb b/app/controllers/domains_controller.rb
index 5c9903d..35f001f 100644
--- a/app/controllers/domains_controller.rb
+++ b/app/controllers/domains_controller.rb
@@ -1,104 +1,107 @@
+require 'set'
+
class DomainsController < ApplicationController
before_action :authenticate_user!
before_action :domain, only: [:show, :edit, :edit_dnssec, :update, :destroy, :full_destroy]
before_action :group, only: [:show, :edit, :edit_dnssec, :update, :destroy, :full_destroy]
helper_method :edit_group_scope
# GET /domains
def index
@domains = show_domain_scope.includes(:group, :soa).all
+ @optouts = Set.new current_user.subscriptions.pluck(:domain_id)
end
# GET /domains/1
def show
@record = Record.new(domain_id: @domain.id)
end
# GET /domains/new
def new
@domain = Domain.new(new_domain_params)
end
# GET /domains/1/edit
def edit
end
# GET /domains/1/edit_dnssec
def edit_dnssec
end
# POST /domains
def create
@domain = Domain.new(domain_params)
if @domain.save
notify_domain(@domain, :create)
redirect_to @domain, notice: "#{@domain.name} was successfully created."
else
render :new
end
end
# PATCH/PUT /domains/1
def update
if @domain.update(domain_params)
notify_domain(@domain, :update)
redirect_to @domain, notice: "#{@domain.name} was successfully updated."
else
if domain_params[:dnssec] # DNSSEC form
render :edit_dnssec
else
render :edit
end
end
end
# DELETE /domains/1
def destroy
if @domain.remove
notify_domain(@domain, :destroy)
redirect_to domains_url, notice: "#{@domain.name} is scheduled for removal."
else
redirect_to domains_url, alert: "#{@domain.name} cannot be deleted! (state '#{@domain.state}')"
end
end
# DELETE /domains/1/full_destroy
def full_destroy
if @domain.full_remove
notify_domain(@domain, :destroy)
redirect_to domains_url,
notice: "#{@domain.name} is scheduled for full removal. DS records will be dropped from the parent zone before proceeding"
else
redirect_to domains_url, alert: "#{@domain.name} cannot be deleted! (state '#{@domain.state}')"
end
end
private
def group
domain.group
end
def new_domain_params
params.permit(:group_id)
end
def domain_params
params.require(:domain).tap { |d|
# Make sure group id is permitted (belongs to edit_group_scope)
d[:group_id] = edit_group_scope.find_by_id(d[:group_id]).try(:id)
# Sometimes domain name might contain whitespace, make sure we remove
# them. Note that we use a regex to handle unicode whitespace characters as well.
d[:name] = d[:name].gsub(/\p{Space}/, '') if d[:name]
}.permit(:name, :type, :master, :group_id,
:dnssec, :dnssec_parent, :dnssec_parent_authority, :dnssec_policy_id)
end
def notify_domain(*args)
notification.notify_domain(current_user, *args) if WebDNS.settings[:notifications]
end
end
diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb
index 0d0c3eb..d22f844 100644
--- a/app/controllers/groups_controller.rb
+++ b/app/controllers/groups_controller.rb
@@ -1,53 +1,54 @@
class GroupsController < ApplicationController
before_action :authenticate_user!
before_action :group, only: [:show, :create_member, :destroy_member, :search_member]
before_action :user, only: [:destroy_member]
# GET /groups/1
def show
@domains = @group.domains
+ @optouts = Set.new current_user.subscriptions.where(domain_id: @domains).pluck(:domain_id)
end
# POST /groups/1/members/
def create_member
@user = User.find_by_email(params[:email])
if !@user
redirect_to group_path(@group, anchor: 'tab-members'), alert: "User '#{params[:email]}' not found!"
return
end
membership = @group.memberships.find_or_create_by!(user_id: @user.id)
redirect_to group_path(@group, anchor: 'tab-members'), notice: "#{membership.user.email} is now a member of #{@group.name}"
end
# DELETE /groups/1/member/1
def destroy_member
membership = @group.memberships.find_by!(user_id: user.id)
membership.destroy!
redirect_to @group, notice: "#{membership.user.email} was successfully removed from #{@group.name}"
end
def search_member
results = []
if params[:q].present?
uids = group.users.pluck(:id)
results = User
.where('email like ?', "#{params[:q]}%")
.where.not(id: uids) # Exclude group members
.limit(10)
end
render json: results.map { |r| Hash[:id, r.id, :email, r.email] }
end
private
def user
@user ||= User.find(params[:user_id])
end
end
diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb
index ef79c33..01aae98 100644
--- a/app/controllers/users_controller.rb
+++ b/app/controllers/users_controller.rb
@@ -1,31 +1,48 @@
class UsersController < ApplicationController
before_action :authenticate_user!
- before_action :user, only: [:token, :generate_token]
+ before_action :user, only: [:mute, :unmute, :token, :generate_token]
# GET /users/1/token
def token
end
# POST /users/1/generate_token
def generate_token
@user.token = SecureRandom.hex(10)
@user.save!
redirect_to token_user_path(@user)
end
+ # PUT /users/1/unsubscribe/2
+ def mute
+ domain = show_domain_scope.find(params[:domain_id])
+ @user.subscriptions.find_or_create_by!(domain: domain)
+
+ redirect_to domains_url, notice: "Successfully unsubscribed from #{domain.name} notifications!"
+ end
+
+ # PUT /users/1/subscribe/2
+ def unmute
+ domain = show_domain_scope.find(params[:domain_id])
+ # Drop all opt-outs
+ @user.subscriptions.where(domain: domain).delete_all
+
+ redirect_to domains_url, notice: "Successfully subscribed to #{domain.name} notifications!"
+ end
+
private
def user
- @user ||= User.find(params[:id])
+ @user ||= User.find(params[:user_id] || params[:id])
# Guard access to other user tokens
if current_user.id != @user.id && !admin?
redirect_to(root_path, alert: 'You need admin rights for that!')
end
@user
end
end
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index b48b410..7a7b4d0 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -1,57 +1,65 @@
module ApplicationHelper
TIME_PERIODS = {
1.second => 'second',
1.minute => 'minute',
1.hour => 'hour',
1.day => 'day',
1.week => 'week',
1.month => 'month',
1.year.to_i => 'year',
}
def can_edit?(object)
return true unless object.respond_to?(:editable?)
by = admin? ? :admin : :user
object.editable?(by)
end
def seconds_to_human(seconds)
acc = {}
remaining = seconds
TIME_PERIODS.to_a.reverse_each do |p, human|
period_count, remaining = remaining.divmod(p)
acc[human] = period_count if not period_count.zero?
end
acc.map { |singular, count|
human = count < 2 ? singular : "#{singular}s"
"#{count} #{human}"
}.join(', ')
end
def link_to_edit(*args, &block)
link_to(abbr_glyph(:pencil, 'Edit'), *args, &block)
end
def link_to_destroy(*args, &block)
link_to(abbr_glyph(:remove, 'Remove'), *args, &block)
end
def link_to_enable(*args, &block)
link_to(abbr_glyph(:'eye-close', 'Enable'), *args, &block)
end
def link_to_disable(*args, &block)
link_to(abbr_glyph(:'eye-open', 'Disable'), *args, &block)
end
+ def link_to_mute(*args, &block)
+ link_to(abbr_glyph(:'volume-off', 'Disable notifications'), *args, &block)
+ end
+
+ def link_to_unmute(*args, &block)
+ link_to(abbr_glyph(:'volume-up', 'Renable notifications'), *args, &block)
+ end
+
def glyph(icon)
content_tag(:span, '', class: "glyphicon glyphicon-#{icon}")
end
def abbr_glyph(icon, title)
content_tag(:abbr, glyph(icon), title: title)
end
end
diff --git a/app/views/domains/index.html.erb b/app/views/domains/index.html.erb
index a95799f..22beec0 100644
--- a/app/views/domains/index.html.erb
+++ b/app/views/domains/index.html.erb
@@ -1,56 +1,61 @@
<% if current_user.memberships.empty? %>
<div class="jumbotron">
<h2>Wellcome to WebDNS!</h2>
<p>
In order to manage domains you have to be a member of a group.
</p>
<p>
You can either contact an admin to create a new group for you, or ask another user for an invite to an existing group.
</p>
</div>
<% end %>
<div class="datatable-wrapper">
<table id="domains" class="table table-striped">
<thead>
<tr>
<th>Domain</th>
<th>Serial</th>
<th>Group</th>
<th>State</th>
<th>Slave</th>
<th>DNSSEC</th>
<th class="no-order-and-search">Controls</th>
</tr>
</thead>
<tbody>
<% @domains.group_by(&:group).each do |group, domains| %>
<% domains.each do |domain| %>
<tr class="group-<%= group.id =%>">
<td><%= link_to domain.name, domain %></td>
<td><%= domain.serial %></td>
<td><%= link_to group.name, group_path(group) %></td>
<td><%= human_state(domain.state) %></td>
<td><%= domain.slave? ? domain.master : '-' %></td>
<td><%= domain.dnssec? ? 'secure' : '-' %></td>
<td>
<%= link_to_edit edit_domain_path(domain) %>
+ <% if @optouts.include? domain.id %>
+ <%= link_to_unmute user_domain_unmute_path(current_user, domain), method: :put %>
+ <% else %>
+ <%= link_to_mute user_domain_mute_path(current_user, domain), method: :put %>
+ <% end %>
<%= link_to_destroy domain, method: :delete, data: { confirm: 'Are you sure?' } if domain.can_remove? %>
<%= link_to_full_destroy full_destroy_domain_path(domain),
method: :delete, data: { confirm: 'Are you sure?' } if domain.can_remove? && domain.dnssec? %>
</td>
</tr>
<% end %>
<% end %>
</tbody>
</table>
</div>
<p>
<% if current_user.memberships.any? %>
<%= link_to 'Add Domain', new_domain_path, class: 'btn btn-primary' %>
<% else %>
<%= link_to 'Add Domain', new_domain_path, class: 'btn btn-primary disabled' %>
<% end %>
</p>
diff --git a/app/views/groups/show.html.erb b/app/views/groups/show.html.erb
index 7874355..d81de70 100644
--- a/app/views/groups/show.html.erb
+++ b/app/views/groups/show.html.erb
@@ -1,87 +1,92 @@
<% content_for :more_breadcrumbs do %>
<li>
<%= link_to_edit edit_admin_group_path(@group) %>
<%= link_to_destroy admin_group_path(@group), method: :delete, data: { confirm: 'Are you sure?' } %>
</li>
<% end if admin? %>
<ul class="nav nav-tabs">
<li role="presentation" class="active"><a id="tab-link-domains" data-toggle="tab" href="#domains_tab">Domains</a></li>
<li role="presentation"><a id="tab-link-members" data-toggle="tab" href="#members_tab">Members</a></li>
</ul>
<div class="tab-content">
<div role="tabpanel" class="tab-pane active" id="domains_tab">
<table id="domains" class="table table-striped">
<thead>
<tr>
<th>Domain</th>
<th>Serial</th>
<th>Group</th>
<th>State</th>
<th>Slave</th>
<th>DNSSEC</th>
<th class="no-order-and-search">Controls</th>
</tr>
</thead>
<tbody>
<% @domains.group_by(&:group).each do |group, domains| %>
<% domains.each do |domain| %>
<tr class="group-<%= group.id =%>">
<td><%= link_to domain.name, domain %></td>
<td><%= domain.serial %></td>
<td><%= link_to group.name, group_path(group) %></td>
<td><%= human_state(domain.state) %></td>
<td><%= domain.slave? ? domain.master : '-' %></td>
<td><%= domain.dnssec? ? 'secure' : '-' %></td>
<td>
<%= link_to_edit edit_domain_path(domain) %>
+ <% if @optouts.include? domain.id %>
+ <%= link_to_unmute user_domain_unmute_path(current_user, domain), method: :put %>
+ <% else %>
+ <%= link_to_mute user_domain_mute_path(current_user, domain), method: :put %>
+ <% end %>
<%= link_to_destroy domain, method: :delete, data: { confirm: 'Are you sure?' } if domain.can_remove? %>
<%= link_to_full_destroy full_destroy_domain_path(domain),
method: :delete, data: { confirm: 'Are you sure?' } if domain.can_remove? && domain.dnssec? %>
</td>
</tr>
<% end %>
<% end %>
</tbody>
</table>
<p>
<% if current_user.memberships.any? %>
<%= link_to 'Add Domain', new_domain_path(group_id: @group.id), class: 'btn btn-primary' %>
<% else %>
<%= link_to 'Add Domain', new_domain_path(group_id: @group.id), class: 'btn btn-primary disabled' %>
<% end %>
</p>
</div>
<div role="tabpanel" class="tab-pane" id="members_tab">
<table class="table table-striped table-hover">
<thead>
<tr>
<th>Member</th>
<th>Controls</th>
</tr>
</thead>
<tbody>
<% @group.memberships.includes(:user).each do |membership| %>
<tr>
<td><%= membership.user.email %><%= " (you)" if current_user == membership.user %></td>
<td>
<%= link_to_destroy destroy_member_group_path(@group, membership.user_id), method: :delete %>
</td>
</tr>
<% end %>
</tbody>
</table>
<p>
<%= bootstrap_form_tag(url: create_member_group_path(@group), layout: :inline) do |f| %>
<%= f.text_field :email, prepend: 'Add Member', hide_label: true, id: 'js-search-member', data: { group: @group.id } %>
<%= f.submit 'Add', class: 'btn btn-primary' %>
<% end %>
</p>
</div>
</div>
diff --git a/config/routes.rb b/config/routes.rb
index cb736ad..6c277dc 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -1,82 +1,86 @@
Rails.application.routes.draw do
# Override devise user removal
devise_scope :users do
delete :users, to: redirect('/')
end
devise_for :users
get '/auth/saml', to: 'auth#saml'
root to: redirect('/domains')
resources :users, only: [] do
get :token, to: 'users#token', on: :member
post :generate_token, to: 'users#generate_token', on: :member
+ resources :domains, only: [] do
+ put :mute, to: 'users#mute'
+ put :unmute, to: 'users#unmute'
+ end
end
resources :groups, only: [:show] do
get :search_member,
to: 'groups#search_member', on: :member
post :members,
to: 'groups#create_member', as: :create_member, on: :member
delete 'member/:user_id',
to: 'groups#destroy_member', as: :destroy_member, on: :member
end
resources :domains do
get :edit_dnssec, to: 'domains#edit_dnssec', on: :member
delete :full_destroy, to: 'domains#full_destroy', on: :member
resources :records, except: [:index, :show] do
# Reuse records#update instead of introducing new controller actions
#
# rubocop:disable Style/AlignHash
put :disable, to: 'records#update', on: :member,
defaults: { record: { disabled: true } }
put :enable, to: 'records#update', on: :member,
defaults: { record: { disabled: false } }
put :editable, to: 'records#editable', on: :collection
post :valid, to: 'records#valid', on: :collection
post :bulk, to: 'records#bulk', on: :collection
# rubocop:enable Style/AlignHash
end
end
get '/records/search', to: 'records#search'
# Admin
namespace :admin do
root to: redirect('/admin/groups')
resources :groups, except: [:show]
resources :jobs, only: [:index, :destroy] do
put :done, to: 'jobs#update', on: :member,
defaults: { job: { status: 1 } }
put :pending, to: 'jobs#update', on: :member,
defaults: { job: { status: 0 } }
get '/type/:category', to: 'jobs#index', on: :collection,
constraints: proc { |req| ['completed', 'pending'].include?(req.params[:category]) }
end
resources :users, only: [:destroy] do
get :orphans, to: 'users#orphans', on: :collection
put :update_groups, to: 'users#update_groups', on: :collection
end
end
# API
scope '/api' do
get :ping, to: 'api#ping'
get :whoami, to: 'api#whoami'
get '/domain/:domain/list', to: 'api#list', constraints: { domain: /[^\/]+/}
post '/domain/:domain/bulk', to: 'api#bulk', constraints: { domain: /[^\/]+/}
get :domains, to: 'api#domains'
end if WebDNS.settings[:api]
# Private
put 'private/replace_ds', to: 'private#replace_ds'
put 'private/trigger_event', to: 'private#trigger_event'
get 'private/zones', to: 'private#zones'
get 'help/api', to: 'help#api'
end

Event Timeline