Merge branch 'chore/update_dependencies' into bugfix/local_web_app_icons
This commit is contained in:
commit
de1f234c15
14
Dockerfile
14
Dockerfile
@ -1,10 +1,18 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
FROM ruby:3.3.0
|
||||
FROM debian:bullseye-slim as base
|
||||
|
||||
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
||||
|
||||
RUN apt-get update -qq && apt-get install -y --no-install-recommends curl \
|
||||
ldap-utils tini libvips
|
||||
# TODO Remove when upstream Ruby works properly on Apple silicon
|
||||
RUN apt update && apt install -y build-essential wget autoconf libpq-dev pkg-config
|
||||
RUN wget https://github.com/postmodern/ruby-install/releases/download/v0.9.3/ruby-install-0.9.3.tar.gz \
|
||||
&& tar -xzvf ruby-install-0.9.3.tar.gz \
|
||||
&& cd ruby-install-0.9.3/ \
|
||||
&& make install
|
||||
RUN ruby-install -p https://github.com/ruby/ruby/pull/9371.diff ruby 3.3.0
|
||||
ENV PATH="/opt/rubies/ruby-3.3.0/bin:${PATH}"
|
||||
|
||||
RUN apt-get install -y --no-install-recommends curl ldap-utils tini libvips
|
||||
RUN curl -fsSL https://deb.nodesource.com/setup_lts.x | bash -
|
||||
RUN apt-get update && apt-get install -y nodejs
|
||||
|
||||
|
@ -2,13 +2,21 @@
|
||||
<div class="relative inline-block">
|
||||
<div role="button" tabindex="0" data-dropdown-target="button"
|
||||
class="inline-block select-none">
|
||||
<% if @size == :large %>
|
||||
<span class="appearance-none flex items-center inline-block">
|
||||
<span class="p-2 bg-gray-50 hover:bg-gray-100 rounded-full">
|
||||
<%= render partial: "icons/kebab-menu", locals: {
|
||||
custom_class: "inline text-gray-500 h-6 w-6"
|
||||
} %>
|
||||
<%= render partial: "icons/#{@icon_name}",
|
||||
locals: { custom_class: "inline text-gray-500 h-6 w-6" } %>
|
||||
</span>
|
||||
</span>
|
||||
<% elsif @size == :small %>
|
||||
<span class="appearance-none flex items-center inline-block">
|
||||
<span class="text-gray-500 hover:text-blue-600">
|
||||
<%= render partial: "icons/#{@icon_name}",
|
||||
locals: { custom_class: "inline h-4 w-4" } %>
|
||||
</span>
|
||||
</span>
|
||||
<% end %>
|
||||
</div>
|
||||
<div data-dropdown-target="menu"
|
||||
data-transition-enter="transition ease-out duration-200"
|
||||
|
@ -1,5 +1,8 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class DropdownComponent < ViewComponent::Base
|
||||
|
||||
def initialize(size: :large, icon_name: "kebap-menu")
|
||||
@size = size.to_sym
|
||||
@icon_name = icon_name
|
||||
end
|
||||
end
|
||||
|
@ -5,7 +5,9 @@
|
||||
} : nil do %>
|
||||
<div class="flex flex-col">
|
||||
<label class="font-bold mb-1"><%= @title %></label>
|
||||
<% if @description.present? %>
|
||||
<p class="text-gray-500"><%= @descripton %></p>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="relative ml-4 inline-flex flex-shrink-0">
|
||||
<%= render FormElements::ToggleComponent.new(
|
||||
|
@ -3,7 +3,7 @@
|
||||
module FormElements
|
||||
class FieldsetToggleComponent < ViewComponent::Base
|
||||
def initialize(tag: "li", form: nil, attribute: nil, field_name: nil,
|
||||
enabled: false, input_enabled: true, title:, description:)
|
||||
enabled: false, input_enabled: true, title:, description: nil)
|
||||
@tag = tag
|
||||
@form = form
|
||||
@attribute = attribute
|
||||
|
@ -18,9 +18,11 @@
|
||||
<div class="m-1 bg-white rounded shadow">
|
||||
<div class="p-8">
|
||||
<%= content %>
|
||||
<% if @show_close_button %>
|
||||
<div class="flex justify-end items-center flex-wrap mt-6">
|
||||
<button class="btn-md btn-blue" data-action="click->modal#close:prevent">Close</button>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,2 +1,5 @@
|
||||
class ModalComponent < ViewComponent::Base
|
||||
def initialize(show_close_button: true)
|
||||
@show_close_button = show_close_button
|
||||
end
|
||||
end
|
||||
|
@ -1,11 +1,11 @@
|
||||
class Admin::UsersController < Admin::BaseController
|
||||
before_action :set_user, only: [:show]
|
||||
before_action :set_user, except: [:index]
|
||||
before_action :set_current_section
|
||||
|
||||
# GET /admin/users
|
||||
def index
|
||||
ldap = LdapService.new
|
||||
@ou = params[:ou] || Setting.primary_domain
|
||||
@orgs = ldap.fetch_organizations
|
||||
@ou = Setting.primary_domain
|
||||
@pagy, @users = pagy(User.where(ou: @ou).order(cn: :asc))
|
||||
|
||||
@stats = {
|
||||
@ -14,6 +14,7 @@ class Admin::UsersController < Admin::BaseController
|
||||
}
|
||||
end
|
||||
|
||||
# GET /admin/users/:username
|
||||
def show
|
||||
if Setting.lndhub_admin_enabled?
|
||||
@lndhub_user = @user.lndhub_user
|
||||
@ -24,11 +25,35 @@ class Admin::UsersController < Admin::BaseController
|
||||
@avatar = LdapManager::FetchAvatar.call(cn: @user.cn)
|
||||
end
|
||||
|
||||
# POST /admin/users/:username/invitations
|
||||
def create_invitations
|
||||
amount = params[:amount].to_i
|
||||
notify_user = ActiveRecord::Type::Boolean.new.cast(params[:notify_user])
|
||||
|
||||
CreateInvitations.call(user: @user, amount: amount, notify: notify_user)
|
||||
|
||||
redirect_to admin_user_path(@user.cn), flash: {
|
||||
success: "Added #{amount} invitations to #{@user.cn}'s account"
|
||||
}
|
||||
end
|
||||
|
||||
# DELETE /admin/users/:username/invitations
|
||||
def delete_invitations
|
||||
invitations = @user.invitations.unused
|
||||
amount = invitations.count
|
||||
|
||||
invitations.destroy_all
|
||||
|
||||
redirect_to admin_user_path(@user.cn), flash: {
|
||||
success: "Removed #{amount} invitations from #{@user.cn}'s account"
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_user
|
||||
address = params[:address].split("@")
|
||||
@user = User.where(cn: address.first, ou: address.last).first
|
||||
@user = User.find_by(cn: params[:username], ou: Setting.primary_domain)
|
||||
http_status :not_found unless @user
|
||||
end
|
||||
|
||||
def set_current_section
|
||||
|
@ -17,4 +17,10 @@ class NotificationMailer < ApplicationMailer
|
||||
@subject = "New app connected to your storage"
|
||||
mail to: @user.email, subject: @subject
|
||||
end
|
||||
|
||||
def new_invitations_available
|
||||
@user = params[:user]
|
||||
@subject = "New invitations added to your account"
|
||||
mail to: @user.email, subject: @subject
|
||||
end
|
||||
end
|
||||
|
17
app/services/create_invitations.rb
Normal file
17
app/services/create_invitations.rb
Normal file
@ -0,0 +1,17 @@
|
||||
class CreateInvitations < ApplicationService
|
||||
def initialize(user:, amount:, notify: true)
|
||||
@user = user
|
||||
@amount = amount
|
||||
@notify = notify
|
||||
end
|
||||
|
||||
def call
|
||||
@amount.times do
|
||||
Invitation.create(user: @user)
|
||||
end
|
||||
|
||||
if @notify
|
||||
NotificationMailer.with(user: @user).new_invitations_available.deliver_later
|
||||
end
|
||||
end
|
||||
end
|
21
app/views/admin/users/_create_invitations.html.erb
Normal file
21
app/views/admin/users/_create_invitations.html.erb
Normal file
@ -0,0 +1,21 @@
|
||||
<h3>Add new invitations to <%= @user.cn %>'s account</h3>
|
||||
<%= form_with(url: invitations_admin_user_path, method: :post) do |form| %>
|
||||
<ul role="list">
|
||||
<%= render FormElements::FieldsetComponent.new(
|
||||
positioning: :horizontal,
|
||||
title: "Amount"
|
||||
) do %>
|
||||
<%= form.select :amount, options_for_select([
|
||||
["3", "3"], ["5", "5"], ["10", "10"], ["20", "20"]
|
||||
]) %>
|
||||
<% end %>
|
||||
<%= render FormElements::FieldsetToggleComponent.new(
|
||||
field_name: "notify_user",
|
||||
enabled: true,
|
||||
title: "Notify user via email"
|
||||
) %>
|
||||
</ul>
|
||||
<p class="pt-6 border-t border-gray-200 text-right">
|
||||
<%= form.submit 'Add', class: "btn-md btn-blue w-full" %>
|
||||
</p>
|
||||
<% end %>
|
@ -1,4 +1,4 @@
|
||||
<%= render HeaderComponent.new(title: "Users: #{@ou}") %>
|
||||
<%= render HeaderComponent.new(title: "Users") %>
|
||||
|
||||
<%= render MainSimpleComponent.new do %>
|
||||
<section>
|
||||
@ -16,19 +16,6 @@
|
||||
<% end %>
|
||||
</section>
|
||||
|
||||
<% if @orgs.length > 1 %>
|
||||
<section>
|
||||
<h3 class="hidden">Domains</h3>
|
||||
<ul>
|
||||
<% @orgs.each do |org| %>
|
||||
<li class="inline-block">
|
||||
<%= link_to org[:ou], admin_users_path(ou: org[:ou]), class: "ks-text-link" %>
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
</section>
|
||||
<% end %>
|
||||
|
||||
<section>
|
||||
<table class="divided mb-8">
|
||||
<thead>
|
||||
@ -36,13 +23,12 @@
|
||||
<th>UID</th>
|
||||
<th>Status</th>
|
||||
<th>Roles</th>
|
||||
<!-- <th>Password</th> -->
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% @users.each do |user| %>
|
||||
<tr>
|
||||
<td><%= link_to(user.cn, admin_user_path(user.address), class: 'ks-text-link') %></td>
|
||||
<td><%= link_to(user.cn, admin_user_path(user.cn), class: 'ks-text-link') %></td>
|
||||
<td><%= user.confirmed_at.nil? ? badge("pending", :yellow) : "" %></td>
|
||||
<td><%= user.is_admin? ? badge("admin", :red) : "" %></td>
|
||||
</tr>
|
||||
|
@ -1,4 +1,4 @@
|
||||
<%= render HeaderComponent.new(title: "User: #{@user.address}") %>
|
||||
<%= render HeaderComponent.new(title: "User: #{@user.cn}") %>
|
||||
|
||||
<%= render MainSimpleComponent.new do %>
|
||||
<div class="mb-12 sm:flex sm:flex-row sm:gap-x-8">
|
||||
@ -42,8 +42,34 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Invitations available</th>
|
||||
<td>
|
||||
<%= @user.invitations.count %>
|
||||
<td data-controller="modal" data-action="keydown.esc->modal#close">
|
||||
<div class="flex justify-between">
|
||||
<span>
|
||||
<%= @user.invitations.count %>
|
||||
</span>
|
||||
<span>
|
||||
<button id="add-invitations" data-action="click->modal#open">
|
||||
<%= render partial: "icons/plus-circle", locals: {
|
||||
custom_class: "text-green-600 hover:text-green-500 -mt-2 -mb-1 h-6 w-6 inline-block"
|
||||
} %>
|
||||
</button>
|
||||
<% if @user.invitations.unused.count > 0 %>
|
||||
<%= link_to invitations_admin_user_path(@user.cn),
|
||||
id: "remove-invitations", data: {
|
||||
turbo_method: :delete,
|
||||
turbo_confirm: "Delete all of #{@user.cn}'s available invitations?"
|
||||
} do %>
|
||||
<%= render partial: "icons/x-circle", locals: {
|
||||
custom_class: "text-red-600 hover:text-red-500 -mt-2 -mb-1 h-6 w-6 inline-block"
|
||||
} %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</span>
|
||||
</div>
|
||||
<%= render ModalComponent.new(show_close_button: false) do %>
|
||||
<%= render partial: "admin/users/create_invitations",
|
||||
locals: { user: @user } %>
|
||||
<% end %>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
@ -1 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-plus-circle"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="16"></line><line x1="8" y1="12" x2="16" y2="12"></line></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-plus-circle <%= custom_class %>"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="16"></line><line x1="8" y1="12" x2="16" y2="12"></line></svg>
|
||||
|
Before Width: | Height: | Size: 351 B After Width: | Height: | Size: 372 B |
@ -1 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-x-circle"><circle cx="12" cy="12" r="10"></circle><line x1="15" y1="9" x2="9" y2="15"></line><line x1="9" y1="9" x2="15" y2="15"></line></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-x-circle <%= custom_class %>"><circle cx="12" cy="12" r="10"></circle><line x1="15" y1="9" x2="9" y2="15"></line><line x1="9" y1="9" x2="15" y2="15"></line></svg>
|
||||
|
Before Width: | Height: | Size: 346 B After Width: | Height: | Size: 367 B |
@ -0,0 +1,11 @@
|
||||
Hi <%= @user.display_name.presence || @user.cn %>,
|
||||
|
||||
New invitations have just been added to your Kosmos account, so you can invite more people to our cooperative services:
|
||||
|
||||
<%= invitations_url %>
|
||||
|
||||
Have a nice day!
|
||||
|
||||
---
|
||||
|
||||
Tip: if you want to invite someone you're meeting in person, log into your account panel on a mobile device and let people scan the invitation QR code from theirs.
|
@ -325,3 +325,10 @@ Devise.setup do |config|
|
||||
# changed. Defaults to true, so a user is signed in automatically after changing a password.
|
||||
# config.sign_in_after_change_password = true
|
||||
end
|
||||
|
||||
# https://github.com/heartcombo/devise/issues/5644
|
||||
class Devise::SecretKeyFinder
|
||||
def find
|
||||
@application.secret_key_base
|
||||
end
|
||||
end
|
||||
|
@ -73,9 +73,19 @@ Rails.application.routes.draw do
|
||||
namespace :admin do
|
||||
root to: 'dashboard#index'
|
||||
|
||||
resources 'users', param: 'address', only: ['index', 'show'], constraints: { address: /.*/ }
|
||||
resources 'users', param: 'username', only: ['index', 'show'] do
|
||||
member do
|
||||
post 'invitations', to: 'users#create_invitations'
|
||||
delete 'invitations', to: 'users#delete_invitations'
|
||||
end
|
||||
end
|
||||
|
||||
# post 'users/:username/invitations', to: 'users#create_invitations'
|
||||
|
||||
get 'invitations', to: 'invitations#index'
|
||||
|
||||
resources :donations
|
||||
|
||||
get 'lightning', to: 'lightning#index'
|
||||
|
||||
namespace :app_catalog do
|
||||
|
@ -3,10 +3,10 @@ require 'sidekiq/testing'
|
||||
ldap = LdapService.new
|
||||
|
||||
Sidekiq::Testing.inline! do
|
||||
CreateAccount.call(
|
||||
CreateAccount.call(account: {
|
||||
username: "admin", domain: "kosmos.org", email: "admin@example.com",
|
||||
password: "admin is admin", confirmed: true
|
||||
)
|
||||
})
|
||||
|
||||
ldap.add_attribute "cn=admin,ou=kosmos.org,cn=users,dc=kosmos,dc=org", :admin, "true"
|
||||
|
||||
@ -15,9 +15,9 @@ Sidekiq::Testing.inline! do
|
||||
email = Faker::Internet.unique.email
|
||||
next if username.length < 3
|
||||
|
||||
CreateAccount.call(
|
||||
CreateAccount.call(account: {
|
||||
username: username, domain: "kosmos.org", email: email,
|
||||
password: "user is user", confirmed: true
|
||||
)
|
||||
})
|
||||
end
|
||||
end
|
||||
|
55
spec/features/admin/users_spec.rb
Normal file
55
spec/features/admin/users_spec.rb
Normal file
@ -0,0 +1,55 @@
|
||||
require "rails_helper"
|
||||
|
||||
RSpec.describe "Admin: User management", type: :feature do
|
||||
let(:admin) { create :user }
|
||||
let(:user) { create :user, id: 2, cn: "alfred", email: "alfred@example.com" }
|
||||
|
||||
before do
|
||||
user.save!
|
||||
|
||||
allow(Devise::LDAP::Adapter).to receive(:get_ldap_param)
|
||||
.with(admin.cn, :admin).and_return(["true"])
|
||||
allow(Devise::LDAP::Adapter).to receive(:get_ldap_param)
|
||||
.with(user.cn, :admin).and_return(nil)
|
||||
allow_any_instance_of(User).to receive(:ldap_entry)
|
||||
.and_return({ uid: user.cn, mail: user.email, display_name: "Freddy" })
|
||||
allow_any_instance_of(LdapManager::FetchAvatar).to receive(:call)
|
||||
.and_return(nil)
|
||||
|
||||
login_as admin, :scope => :user
|
||||
end
|
||||
|
||||
describe "User details page" do
|
||||
before do
|
||||
visit admin_user_path("alfred")
|
||||
end
|
||||
|
||||
it "shows the user info" do
|
||||
within "h1" do
|
||||
expect(page).to have_content("User: alfred")
|
||||
end
|
||||
expect(page).to have_content("alfred@example.com")
|
||||
end
|
||||
end
|
||||
|
||||
scenario 'Add invitations to account' do
|
||||
visit admin_user_path("alfred")
|
||||
find("#add-invitations").click
|
||||
|
||||
select "5", :from => "amount"
|
||||
uncheck "notify_user"
|
||||
click_button "Add"
|
||||
|
||||
expect(user.invitations.count).to eq(5)
|
||||
end
|
||||
|
||||
scenario 'Remove invitations from account' do
|
||||
3.times { Invitation.create(user: user) }
|
||||
expect(user.invitations.count).to eq(3)
|
||||
|
||||
visit admin_user_path("alfred")
|
||||
find("#remove-invitations").click
|
||||
|
||||
expect(user.invitations.count).to eq(0)
|
||||
end
|
||||
end
|
40
spec/services/create_invitations_spec.rb
Normal file
40
spec/services/create_invitations_spec.rb
Normal file
@ -0,0 +1,40 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe CreateInvitations, type: :model do
|
||||
include ActiveJob::TestHelper
|
||||
|
||||
let(:user) { create :user }
|
||||
|
||||
describe "#call" do
|
||||
before do
|
||||
CreateInvitations.call(user: user, amount: 5)
|
||||
end
|
||||
|
||||
after(:each) { clear_enqueued_jobs }
|
||||
|
||||
it "creates the right amount of invitations for the given user" do
|
||||
expect(user.invitations.count).to eq(5)
|
||||
end
|
||||
|
||||
it "sends an email notification to the user" do
|
||||
expect(enqueued_jobs.size).to eq(1)
|
||||
expect(enqueued_jobs.first["job_class"]).to eq("ActionMailer::MailDeliveryJob")
|
||||
args = enqueued_jobs.first['arguments']
|
||||
expect(args[0]).to eq("NotificationMailer")
|
||||
expect(args[1]).to eq("new_invitations_available")
|
||||
expect(args[3]["params"]["user"]["_aj_globalid"]).to eq("gid://akkounts/User/1")
|
||||
end
|
||||
end
|
||||
|
||||
describe "#call with notification disabled" do
|
||||
before do
|
||||
CreateInvitations.call(user: user, amount: 3, notify: false)
|
||||
end
|
||||
|
||||
after(:each) { clear_enqueued_jobs }
|
||||
|
||||
it "does not send an email notification to the user" do
|
||||
expect(enqueued_jobs.size).to eq(0)
|
||||
end
|
||||
end
|
||||
end
|
Loading…
x
Reference in New Issue
Block a user