Compare commits
36 Commits
3f110995a4
...
v0.9.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
eac8fa6edb
|
|||
|
43f918a074
|
|||
| e322867d79 | |||
|
4d6fa318b7
|
|||
| 4e8878a4b5 | |||
|
e65b890880
|
|||
|
f57edd4d3b
|
|||
|
1afd56fb80
|
|||
| 71669a4b96 | |||
|
c312e30c17
|
|||
| 51f4556ede | |||
| 4fa4ae6b54 | |||
| 869ff4691b | |||
|
822a2dc018
|
|||
|
5b7fc3707b
|
|||
| 0e2dc54dc6 | |||
| 87f09c94d0 | |||
|
b33b8104a8
|
|||
| 4a4a222973 | |||
| 8c524abcf5 | |||
|
a852ab75ae
|
|||
|
de1f234c15
|
|||
| 4581900427 | |||
|
56d91083e5
|
|||
|
ba7c3795f8
|
|||
|
bbf3fb91a0
|
|||
| 1754df73cb | |||
|
9a1f9abf84
|
|||
|
2753388e1e
|
|||
|
f3159d30f1
|
|||
|
ca238be6f4
|
|||
|
8747ce4eb0
|
|||
|
fcda3b9c8c
|
|||
|
67689dcce3
|
|||
|
22ffcd54db
|
|||
|
bd1b177993
|
78
.env.example
78
.env.example
@@ -1,14 +1,14 @@
|
||||
PRIMARY_DOMAIN=kosmos.org
|
||||
AKKOUNTS_DOMAIN=accounts.example.com
|
||||
# PRIMARY_DOMAIN=kosmos.org
|
||||
# AKKOUNTS_DOMAIN=accounts.example.com
|
||||
|
||||
SMTP_SERVER=smtp.example.com
|
||||
SMTP_PORT=587
|
||||
SMTP_LOGIN=accounts
|
||||
SMTP_PASSWORD=123abc
|
||||
SMTP_FROM_ADDRESS=accounts@example.com
|
||||
SMTP_DOMAIN=example.com
|
||||
SMTP_AUTH_METHOD=plain
|
||||
SMTP_ENABLE_STARTTLS=auto
|
||||
# SMTP_SERVER=smtp.example.com
|
||||
# SMTP_PORT=587
|
||||
# SMTP_LOGIN=accounts
|
||||
# SMTP_PASSWORD=123abc
|
||||
# SMTP_FROM_ADDRESS=accounts@example.com
|
||||
# SMTP_DOMAIN=example.com
|
||||
# SMTP_AUTH_METHOD=plain
|
||||
# SMTP_ENABLE_STARTTLS=auto
|
||||
|
||||
# S3_ENABLED=true
|
||||
# S3_ENDPOINT=https://s3.kosmos.org
|
||||
@@ -18,47 +18,47 @@ SMTP_ENABLE_STARTTLS=auto
|
||||
# S3_ACCESS_KEY=123456abcdefg
|
||||
# S3_SECRET_KEY=123456789123456789123456789
|
||||
|
||||
LDAP_HOST=localhost
|
||||
LDAP_PORT=389
|
||||
LDAP_ADMIN_PASSWORD=passthebutter
|
||||
LDAP_SUFFIX='dc=kosmos,dc=org'
|
||||
# LDAP_HOST=localhost
|
||||
# LDAP_PORT=389
|
||||
# LDAP_ADMIN_PASSWORD=passthebutter
|
||||
# LDAP_SUFFIX='dc=kosmos,dc=org'
|
||||
|
||||
REDIS_URL='redis://localhost:6379/1'
|
||||
# REDIS_URL='redis://localhost:6379/1'
|
||||
|
||||
WEBHOOKS_ALLOWED_IPS='10.1.1.163'
|
||||
# WEBHOOKS_ALLOWED_IPS='10.1.1.163'
|
||||
|
||||
#
|
||||
# Service Integrations
|
||||
#
|
||||
|
||||
BTCPAY_API_URL='http://localhost:23001/api/v1'
|
||||
BTCPAY_STORE_ID=''
|
||||
BTCPAY_AUTH_TOKEN=''
|
||||
# BTCPAY_API_URL='http://localhost:23001/api/v1'
|
||||
# BTCPAY_STORE_ID=''
|
||||
# BTCPAY_AUTH_TOKEN=''
|
||||
|
||||
DISCOURSE_PUBLIC_URL='https://community.kosmos.org'
|
||||
DISCOURSE_CONNECT_SECRET='discourse_connect_ftw'
|
||||
# DISCOURSE_PUBLIC_URL='https://community.kosmos.org'
|
||||
# DISCOURSE_CONNECT_SECRET='discourse_connect_ftw'
|
||||
|
||||
DRONECI_PUBLIC_URL='https://drone.kosmos.org'
|
||||
# DRONECI_PUBLIC_URL='https://drone.kosmos.org'
|
||||
|
||||
EJABBERD_ADMIN_URL='https://xmpp.kosmos.org/admin'
|
||||
EJABBERD_API_URL='https://xmpp.kosmos.org/api'
|
||||
# EJABBERD_ADMIN_URL='https://xmpp.kosmos.org/admin'
|
||||
# EJABBERD_API_URL='https://xmpp.kosmos.org/api'
|
||||
|
||||
GITEA_PUBLIC_URL='https://gitea.kosmos.org'
|
||||
# GITEA_PUBLIC_URL='https://gitea.kosmos.org'
|
||||
|
||||
LNDHUB_API_URL='http://localhost:3023'
|
||||
LNDHUB_PUBLIC_URL='https://lndhub.kosmos.org'
|
||||
LNDHUB_PUBLIC_KEY='0123d3be18617f39cf645851e3ba63f51fc13f0bb09e3bb25e6fd4de556486d946'
|
||||
LNDHUB_ADMIN_UI=true
|
||||
LNDHUB_ADMIN_TOKEN=123456789
|
||||
LNDHUB_PG_HOST=localhost
|
||||
LNDHUB_PG_PORT=5432
|
||||
LNDHUB_PG_DATABASE=lndhub
|
||||
LNDHUB_PG_USERNAME=lndhub
|
||||
LNDHUB_PG_PASSWORD=''
|
||||
# LNDHUB_API_URL='http://localhost:3023'
|
||||
# LNDHUB_PUBLIC_URL='https://lndhub.kosmos.org'
|
||||
# LNDHUB_PUBLIC_KEY='0123d3be18617f39cf645851e3ba63f51fc13f0bb09e3bb25e6fd4de556486d946'
|
||||
# LNDHUB_ADMIN_UI=true
|
||||
# LNDHUB_ADMIN_TOKEN=123456789
|
||||
# LNDHUB_PG_HOST=localhost
|
||||
# LNDHUB_PG_PORT=5432
|
||||
# LNDHUB_PG_DATABASE=lndhub
|
||||
# LNDHUB_PG_USERNAME=lndhub
|
||||
# LNDHUB_PG_PASSWORD=''
|
||||
|
||||
MASTODON_PUBLIC_URL='https://kosmos.social'
|
||||
# MASTODON_PUBLIC_URL='https://kosmos.social'
|
||||
|
||||
MEDIAWIKI_PUBLIC_URL='https://wiki.kosmos.org'
|
||||
# MEDIAWIKI_PUBLIC_URL='https://wiki.kosmos.org'
|
||||
|
||||
RS_STORAGE_URL='https://storage.kosmos.org'
|
||||
RS_REDIS_URL='redis://localhost:6379/2'
|
||||
# RS_STORAGE_URL='https://storage.kosmos.org'
|
||||
# RS_REDIS_URL='redis://localhost:6379/2'
|
||||
|
||||
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
|
||||
|
||||
|
||||
46
README.md
46
README.md
@@ -14,8 +14,10 @@ so:
|
||||
|
||||
1. Make sure [Docker Compose is installed][1] and Docker is running (included in
|
||||
Docker Desktop)
|
||||
3. Run `docker compose up` and wait until 389ds announces its successful start
|
||||
in the log output
|
||||
3. Run `docker compose up --build` and wait until all services have started
|
||||
(389ds might take an extra minute to be ready). This will take a while when
|
||||
running for the first time, so you might want to do something else in the
|
||||
meantime.
|
||||
4. `docker-compose exec ldap dsconf localhost backend create --suffix="dc=kosmos,dc=org" --be-name="dev"`
|
||||
5. `docker compose run web rails ldap:setup`
|
||||
6. `docker compose run web rails db:setup`
|
||||
@@ -28,38 +30,44 @@ have the password "user is user".
|
||||
|
||||
### Rails app
|
||||
|
||||
_Note: when using Docker Compose, prefix the following commands with `docker-compose
|
||||
run web`._
|
||||
|
||||
Installing dependencies:
|
||||
|
||||
bundle install
|
||||
yarn install
|
||||
|
||||
Setting up local database (SQLite):
|
||||
Migrating the local database (after schema changes):
|
||||
|
||||
bundle exec rails db:create
|
||||
bundle exec rails db:migrate
|
||||
|
||||
Running the dev server and auto-building CSS files on change:
|
||||
Running the dev server, and auto-building CSS files on change _(automatic with Docker Compose)_:
|
||||
|
||||
bin/dev
|
||||
|
||||
Running the background workers (requires Redis):
|
||||
Running the background workers (requires Redis) _(automatic with Docker Compose)_:
|
||||
|
||||
bundle exec sidekiq -C config/sidekiq.yml
|
||||
|
||||
Running all specs:
|
||||
Running the test suite:
|
||||
|
||||
bundle exec rspec
|
||||
|
||||
### Docker (Compose)
|
||||
Running the test suite with Docker Compose requires overriding the Rails
|
||||
environment:
|
||||
|
||||
There is a working Docker Compose config file, which define a number of services including
|
||||
an app server for Rails as well as a local 389ds (LDAP) server.
|
||||
docker-compose run -e "RAILS_ENV=test" web rspec
|
||||
|
||||
For Rails developers, you probably just want to start the LDAP server: `docker-compose up ldap`,
|
||||
listening on port 389 on your machine.
|
||||
### Docker Compose
|
||||
|
||||
You can pick and choose your services adding them by name (listed in `docker-compose.yml`) at
|
||||
the end of the docker compose command. eg. `docker compose up ldap redis`
|
||||
Services/containers are configured in `docker-compose.yml`.
|
||||
|
||||
You can run services selectively, for example if you want to run the Rails app
|
||||
and test suite on the host machine. Just add the service names of the
|
||||
containers you want to run to the `up` command, like so:
|
||||
|
||||
docker-compose up ldap redis
|
||||
|
||||
#### LDAP server
|
||||
|
||||
@@ -76,13 +84,15 @@ Now you can seed the back-end with data using this Rails task:
|
||||
The setup task will first delete any existing entries in the directory tree
|
||||
("dc=kosmos,dc=org"), and then create our development entries.
|
||||
|
||||
Note that all 389ds data is stored in `tmp/389ds`. So if you want to start over
|
||||
with a fresh installation, delete both that directory as well as the container.
|
||||
Note that all 389ds data is stored in the `389ds-data` volume. So if you want
|
||||
to start over with a fresh installation, delete both that volume as well as the
|
||||
container.
|
||||
|
||||
#### Minio / RS
|
||||
#### Minio / remoteStorage
|
||||
|
||||
If you want to run remoteStorage accounts locally, you will have to create the
|
||||
respective bucket first:
|
||||
respective bucket first. With the `minio` container running (run by default
|
||||
when using Docker Compose), follow these steps:
|
||||
|
||||
* `docker compose up web redis minio liquor-cabinet`
|
||||
* Head to http://localhost:9001 and log in with user `minioadmin`, password
|
||||
|
||||
@@ -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,8 +1,8 @@
|
||||
class Admin::Settings::RegistrationsController < Admin::SettingsController
|
||||
def index
|
||||
def show
|
||||
end
|
||||
|
||||
def create
|
||||
def update
|
||||
update_settings
|
||||
|
||||
redirect_to admin_settings_registrations_path, flash: {
|
||||
|
||||
@@ -1,19 +1,32 @@
|
||||
class Admin::Settings::ServicesController < Admin::SettingsController
|
||||
def index
|
||||
@service = params[:s]
|
||||
before_action :set_service, only: [:show, :update]
|
||||
|
||||
if @service.blank?
|
||||
redirect_to admin_settings_services_path(params: { s: "btcpay" })
|
||||
end
|
||||
def index
|
||||
redirect_to admin_settings_service_path("btcpay")
|
||||
end
|
||||
|
||||
def create
|
||||
service = params.require(:service)
|
||||
def show
|
||||
end
|
||||
|
||||
def update
|
||||
update_settings
|
||||
|
||||
redirect_to admin_settings_services_path(params: { s: service }), flash: {
|
||||
redirect_to admin_settings_service_path(@service), flash: {
|
||||
success: "Settings saved"
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_subsection
|
||||
@subsection = "services"
|
||||
end
|
||||
|
||||
def set_service
|
||||
@service = params[:service]
|
||||
|
||||
if @service.blank?
|
||||
redirect_to admin_settings_services_path and return
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -20,7 +20,7 @@ class Admin::SettingsController < Admin::BaseController
|
||||
end
|
||||
|
||||
if @errors.any?
|
||||
render :index and return
|
||||
render :show and return
|
||||
end
|
||||
|
||||
changed_keys.each do |key|
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -20,6 +20,8 @@ module AppCatalogManager
|
||||
|
||||
@app.save!
|
||||
|
||||
# TODO move icon downloads to separate, async job
|
||||
|
||||
if icon = metadata.select_icon(sizes: "256x256") ||
|
||||
icon = metadata.select_icon(sizes: "192x192")
|
||||
attach_remote_image(:icon, icon)
|
||||
@@ -49,7 +51,12 @@ module AppCatalogManager
|
||||
tempfile = Down.download(download_url)
|
||||
@app.send(attachment_name).attach(key: key, io: tempfile, filename: filename)
|
||||
rescue Down::NotFound
|
||||
Rails.logger.warn "Icon download failed: NotFound error for #{download_url}"
|
||||
msg = "Download of \"#{attachment_name}\" failed: NotFound error for #{download_url}"
|
||||
Rails.logger.warn(msg)
|
||||
Sentry.capture_message(msg)
|
||||
rescue => e
|
||||
Rails.logger.warn "Saving attachment \"#{attachment_name}\" failed: \"#{e.message}\""
|
||||
Sentry.capture_exception(e) if Setting.sentry_enabled?
|
||||
end
|
||||
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
|
||||
@@ -1,7 +1,7 @@
|
||||
<%= render HeaderComponent.new(title: "Settings") %>
|
||||
|
||||
<%= render MainWithSidenavComponent.new(sidenav_partial: 'shared/admin_sidenav_settings') do %>
|
||||
<%= form_for(Setting.new, url: admin_settings_registrations_path) do |f| %>
|
||||
<%= form_for(Setting.new, url: admin_settings_registrations_path, method: :put) do |f| %>
|
||||
<section>
|
||||
<h3>Registrations</h3>
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
<%= render HeaderComponent.new(title: "Settings") %>
|
||||
|
||||
<%= render MainWithSidenavComponent.new(sidenav_partial: 'shared/admin_sidenav_settings') do %>
|
||||
<%= form_for(Setting.new, url: admin_settings_services_path) do |f| %>
|
||||
<%= hidden_field_tag :service, @service %>
|
||||
|
||||
<%= form_for(Setting.new, url: admin_settings_service_path(@service), method: :put) do |f| %>
|
||||
<% if @errors && @errors.any? %>
|
||||
<section>
|
||||
<%= render partial: "admin/settings/errors", locals: { errors: @errors } %>
|
||||
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,6 +1,7 @@
|
||||
<%
|
||||
# TODO remove when https://github.com/hotwired/turbo/issues/203 is fixed
|
||||
enable_turbo = !session[:user_return_to] || !session[:user_return_to].match?('/discourse/connect')
|
||||
enable_turbo = session[:user_return_to].blank? ||
|
||||
['/discourse/connect', '/rs/oauth'].none? { |s| session[:user_return_to].match(s) }
|
||||
%>
|
||||
|
||||
<%= render HeaderCompactComponent.new(title: "Log in") %>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
@@ -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.
|
||||
@@ -31,6 +31,7 @@
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<% if Flipper.enabled?(:avatar_upload, current_user) %>
|
||||
<label class="block">
|
||||
<p class="font-bold mb-1">
|
||||
Avatar
|
||||
@@ -56,6 +57,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
<% end %>
|
||||
|
||||
<p class="mt-8 pt-6 border-t border-gray-200 text-right">
|
||||
<%= f.submit 'Save', class: "btn-md btn-blue w-full md:w-auto" %>
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
) %>
|
||||
<%= render SidenavLinkComponent.new(
|
||||
name: "Services", path: admin_settings_services_path, icon: "grid",
|
||||
active: current_page?(admin_settings_services_path)
|
||||
active: controller_name == "services"
|
||||
) %>
|
||||
<% if current_page?(admin_settings_services_path) %>
|
||||
<% if controller_name == "services" %>
|
||||
<%= render partial: "shared/admin_sidenav_settings_services" %>
|
||||
<% end %>
|
||||
<%= render SidenavLinkComponent.new(
|
||||
|
||||
@@ -1,77 +1,77 @@
|
||||
<%= render SidenavLinkComponent.new(
|
||||
level: 2,
|
||||
name: "BTCPay",
|
||||
path: admin_settings_services_path(params: { s: "btcpay" }),
|
||||
path: admin_settings_service_path("btcpay"),
|
||||
text_icon: Setting.btcpay_enabled? ? "◉" : "○",
|
||||
active: current_page?(admin_settings_services_path(params: { s: "btcpay" })),
|
||||
active: current_page?(admin_settings_service_path("btcpay")),
|
||||
) %>
|
||||
<%= render SidenavLinkComponent.new(
|
||||
level: 2,
|
||||
name: "Discourse",
|
||||
path: admin_settings_services_path(params: { s: "discourse" }),
|
||||
path: admin_settings_service_path("discourse"),
|
||||
text_icon: Setting.discourse_enabled? ? "◉" : "○",
|
||||
active: current_page?(admin_settings_services_path(params: { s: "discourse" })),
|
||||
active: current_page?(admin_settings_service_path("discourse")),
|
||||
) %>
|
||||
<%= render SidenavLinkComponent.new(
|
||||
level: 2,
|
||||
name: "Drone CI",
|
||||
path: admin_settings_services_path(params: { s: "droneci" }),
|
||||
path: admin_settings_service_path("droneci"),
|
||||
text_icon: Setting.droneci_enabled? ? "◉" : "○",
|
||||
active: current_page?(admin_settings_services_path(params: { s: "droneci" })),
|
||||
active: current_page?(admin_settings_service_path("droneci")),
|
||||
) %>
|
||||
<%= render SidenavLinkComponent.new(
|
||||
level: 2,
|
||||
name: "E-Mail",
|
||||
path: admin_settings_services_path(params: { s: "email" }),
|
||||
path: admin_settings_service_path("email"),
|
||||
text_icon: Setting.email_enabled? ? "◉" : "○",
|
||||
active: current_page?(admin_settings_services_path(params: { s: "email" })),
|
||||
) %>
|
||||
<%= render SidenavLinkComponent.new(
|
||||
level: 2,
|
||||
name: "ejabberd",
|
||||
path: admin_settings_services_path(params: { s: "ejabberd" }),
|
||||
path: admin_settings_service_path("ejabberd"),
|
||||
text_icon: Setting.ejabberd_enabled? ? "◉" : "○",
|
||||
active: current_page?(admin_settings_services_path(params: { s: "ejabberd" })),
|
||||
active: current_page?(admin_settings_service_path("ejabberd")),
|
||||
) %>
|
||||
<%= render SidenavLinkComponent.new(
|
||||
level: 2,
|
||||
name: "Gitea",
|
||||
path: admin_settings_services_path(params: { s: "gitea" }),
|
||||
path: admin_settings_service_path("gitea"),
|
||||
text_icon: Setting.gitea_enabled? ? "◉" : "○",
|
||||
active: current_page?(admin_settings_services_path(params: { s: "gitea" })),
|
||||
active: current_page?(admin_settings_service_path("gitea")),
|
||||
) %>
|
||||
<%= render SidenavLinkComponent.new(
|
||||
level: 2,
|
||||
name: "LNDHub",
|
||||
path: admin_settings_services_path(params: { s: "lndhub" }),
|
||||
path: admin_settings_service_path("lndhub"),
|
||||
text_icon: Setting.lndhub_enabled? ? "◉" : "○",
|
||||
active: current_page?(admin_settings_services_path(params: { s: "lndhub" })),
|
||||
active: current_page?(admin_settings_service_path("lndhub")),
|
||||
) %>
|
||||
<%= render SidenavLinkComponent.new(
|
||||
level: 2,
|
||||
name: "Mastodon",
|
||||
path: admin_settings_services_path(params: { s: "mastodon" }),
|
||||
path: admin_settings_service_path("mastodon"),
|
||||
text_icon: Setting.mastodon_enabled? ? "◉" : "○",
|
||||
active: current_page?(admin_settings_services_path(params: { s: "mastodon" })),
|
||||
active: current_page?(admin_settings_service_path("mastodon")),
|
||||
) %>
|
||||
<%= render SidenavLinkComponent.new(
|
||||
level: 2,
|
||||
name: "MediaWiki",
|
||||
path: admin_settings_services_path(params: { s: "mediawiki" }),
|
||||
path: admin_settings_service_path("mediawiki"),
|
||||
text_icon: Setting.mediawiki_enabled? ? "◉" : "○",
|
||||
active: current_page?(admin_settings_services_path(params: { s: "mediawiki" })),
|
||||
active: current_page?(admin_settings_service_path("mediawiki")),
|
||||
) %>
|
||||
<%= render SidenavLinkComponent.new(
|
||||
level: 2,
|
||||
name: "Nostr",
|
||||
path: admin_settings_services_path(params: { s: "nostr" }),
|
||||
path: admin_settings_service_path("nostr"),
|
||||
text_icon: Setting.nostr_enabled? ? "◉" : "○",
|
||||
active: current_page?(admin_settings_services_path(params: { s: "nostr" })),
|
||||
active: current_page?(admin_settings_service_path("nostr")),
|
||||
) %>
|
||||
<%= render SidenavLinkComponent.new(
|
||||
level: 2,
|
||||
name: "RemoteStorage",
|
||||
path: admin_settings_services_path(params: { s: "remotestorage" }),
|
||||
path: admin_settings_service_path("remotestorage"),
|
||||
text_icon: Setting.remotestorage_enabled? ? "◉" : "○",
|
||||
active: current_page?(admin_settings_services_path(params: { s: "remotestorage" })),
|
||||
active: current_page?(admin_settings_service_path("remotestorage")),
|
||||
) %>
|
||||
|
||||
@@ -52,10 +52,9 @@ Rails.application.configure do
|
||||
|
||||
config.active_job.queue_adapter = :test
|
||||
|
||||
if ENV["S3_ENABLED"]
|
||||
if ENV["S3_ENABLED"] && ENV["S3_ENABLED"].to_s != "false"
|
||||
config.active_storage.service = :s3
|
||||
else
|
||||
# Store attachments on the local disk (in ./tmp)
|
||||
config.active_storage.service = :test
|
||||
config.active_storage.service = :local
|
||||
end
|
||||
end
|
||||
|
||||
@@ -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
|
||||
@@ -83,8 +93,8 @@ Rails.application.routes.draw do
|
||||
end
|
||||
|
||||
namespace :settings do
|
||||
resources 'registrations', only: ['index', 'create']
|
||||
resources 'services', only: ['index', 'create']
|
||||
resource 'registrations', only: ['show', 'update']
|
||||
resources 'services', param: 'service', only: ['index', 'show', 'update']
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
11
db/schema.rb
11
db/schema.rb
@@ -10,7 +10,7 @@
|
||||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema[7.1].define(version: 2024_02_07_080515) do
|
||||
ActiveRecord::Schema[7.1].define(version: 2024_02_16_124640) do
|
||||
create_table "active_storage_attachments", force: :cascade do |t|
|
||||
t.string "name", null: false
|
||||
t.string "record_type", null: false
|
||||
@@ -50,12 +50,17 @@ ActiveRecord::Schema[7.1].define(version: 2024_02_07_080515) do
|
||||
create_table "donations", force: :cascade do |t|
|
||||
t.integer "user_id"
|
||||
t.integer "amount_sats"
|
||||
t.integer "amount_eur"
|
||||
t.integer "amount_usd"
|
||||
t.integer "fiat_amount"
|
||||
t.string "public_name"
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.datetime "paid_at", precision: nil
|
||||
t.string "fiat_currency", default: "USD"
|
||||
t.string "donation_method"
|
||||
t.string "payment_method"
|
||||
t.string "btcpay_invoice_id"
|
||||
t.string "payment_status"
|
||||
t.index ["payment_status"], name: "index_donations_on_payment_status"
|
||||
t.index ["user_id"], name: "index_donations_on_user_id"
|
||||
end
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -2,7 +2,7 @@ services:
|
||||
ldap:
|
||||
image: 4teamwork/389ds:latest
|
||||
volumes:
|
||||
- ./tmp/389ds:/data
|
||||
- 389ds-data:/data
|
||||
networks:
|
||||
- external_network
|
||||
- internal_network
|
||||
@@ -16,11 +16,12 @@ services:
|
||||
restart: always
|
||||
image: redis:7-alpine
|
||||
networks:
|
||||
- external_network
|
||||
- internal_network
|
||||
healthcheck:
|
||||
test: ['CMD', 'redis-cli', 'ping']
|
||||
volumes:
|
||||
- ./tmp/redis:/data
|
||||
- redis-data:/data
|
||||
|
||||
web:
|
||||
build: .
|
||||
@@ -84,10 +85,10 @@ services:
|
||||
- "9000:9000"
|
||||
- "9001:9001"
|
||||
volumes:
|
||||
- ./tmp/minio:/data
|
||||
- minio-data:/data
|
||||
|
||||
liquor-cabinet:
|
||||
image: gitea.kosmos.org/5apps/liquor-cabinet:2.0.0-beta.2
|
||||
image: gitea.kosmos.org/5apps/liquor-cabinet:2.0.0-rc.1
|
||||
networks:
|
||||
- external_network
|
||||
- internal_network
|
||||
@@ -119,3 +120,11 @@ networks:
|
||||
external_network:
|
||||
internal_network:
|
||||
internal: true
|
||||
|
||||
volumes:
|
||||
389ds-data:
|
||||
driver: local
|
||||
minio-data:
|
||||
driver: local
|
||||
redis-data:
|
||||
driver: local
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"postcss-preset-env": "^7.8.3",
|
||||
"tailwindcss": "^3.2.4"
|
||||
},
|
||||
"version": "0.8.1",
|
||||
"version": "0.9.0",
|
||||
"scripts": {
|
||||
"build:css:tailwind": "tailwindcss --postcss -i ./app/assets/stylesheets/application.tailwind.css -o ./app/assets/builds/application.css",
|
||||
"build:css": "yarn run build:css:tailwind"
|
||||
|
||||
@@ -23,35 +23,35 @@ RSpec.describe 'Admin/global settings', type: :feature do
|
||||
scenario "Opening service settings shows page for first service" do
|
||||
visit admin_settings_services_path
|
||||
|
||||
expect(current_url).to eq(admin_settings_services_url(params: { s: "btcpay" }))
|
||||
expect(current_url).to eq(admin_settings_service_url("btcpay"))
|
||||
end
|
||||
|
||||
scenario "View service settings" do
|
||||
visit admin_settings_services_path(params: { s: "ejabberd" })
|
||||
visit admin_settings_service_path("ejabberd")
|
||||
|
||||
expect(page).to have_content("Enable ejabberd integration")
|
||||
expect(page).to have_field("API URL", with: "http://xmpp.example.com/api")
|
||||
end
|
||||
|
||||
scenario "Disable a service integration" do
|
||||
visit admin_settings_services_path(params: { s: "ejabberd" })
|
||||
visit admin_settings_service_path("ejabberd")
|
||||
expect(page).to have_checked_field("setting[ejabberd_enabled]")
|
||||
|
||||
uncheck "setting[ejabberd_enabled]"
|
||||
click_button "Save"
|
||||
|
||||
expect(current_url).to eq(admin_settings_services_url(params: { s: "ejabberd" }))
|
||||
expect(current_url).to eq(admin_settings_service_url("ejabberd"))
|
||||
expect(page).to_not have_checked_field("setting[ejabberd_enabled]")
|
||||
expect(page).to_not have_field("API URL", disabled: true)
|
||||
end
|
||||
|
||||
scenario "Resettable fields" do
|
||||
visit admin_settings_services_path(params: { s: "ejabberd" })
|
||||
visit admin_settings_service_path("ejabberd")
|
||||
expect(page).to have_field("API URL", with: "http://xmpp.example.com/api")
|
||||
expect(page).to_not have_css('input#setting_ejabberd_api_url+button')
|
||||
|
||||
Setting.ejabberd_api_url = "http://example.com/foo"
|
||||
visit admin_settings_services_path(params: { s: "ejabberd" })
|
||||
visit admin_settings_service_path("ejabberd")
|
||||
expect(page).to have_field("API URL", with: "http://example.com/foo")
|
||||
expect(page).to have_css('input#setting_ejabberd_api_url+button')
|
||||
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
|
||||
@@ -12,6 +12,8 @@ RSpec.describe 'Profile settings', type: :feature do
|
||||
uid: user.cn, ou: user.ou, display_name: "Mark"
|
||||
})
|
||||
allow_any_instance_of(User).to receive(:avatar).and_return(avatar_base64)
|
||||
|
||||
Flipper.enable "avatar_upload"
|
||||
end
|
||||
|
||||
feature "Update display name" do
|
||||
|
||||
@@ -15,7 +15,7 @@ RSpec.describe "WebFinger", type: :request do
|
||||
res = JSON.parse(response.body)
|
||||
rs_link = res["links"].find {|l| l["rel"] == "http://tools.ietf.org/id/draft-dejong-remotestorage"}
|
||||
|
||||
expect(rs_link["href"]).to eql("https://storage.kosmos.org/tony")
|
||||
expect(rs_link["href"]).to eql("#{Setting.rs_storage_url}/tony")
|
||||
|
||||
oauth_url = rs_link["properties"]["http://tools.ietf.org/html/rfc6749#section-4.2"]
|
||||
expect(oauth_url).to eql("http://www.example.com/rs/oauth/tony")
|
||||
|
||||
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
|
||||
Reference in New Issue
Block a user