36 Commits

Author SHA1 Message Date
eac8fa6edb 0.9.0
All checks were successful
continuous-integration/drone/push Build is passing
2024-03-07 14:48:27 +01:00
43f918a074 Update liquor-cabinet image, fix LC/redis networking issue on Linux
All checks were successful
continuous-integration/drone/push Build is passing
2024-03-06 22:07:35 +01:00
e322867d79 Merge pull request 'Fix login redirect for existing RS auth' (#180) from bugfix/178-rs_login_redirect into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #180
2024-03-06 21:06:27 +00:00
4d6fa318b7 Fix login redirect for existing RS auth
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Release Drafter / Update release notes draft (pull_request) Successful in 3s
fixes #178
2024-03-06 22:00:15 +01:00
4e8878a4b5 Merge pull request 'Allow running specs in Docker container, update README' (#177) from dev/docker_rspec into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #177
Reviewed-by: galfert <garret.alfert@gmail.com>
2024-03-03 11:47:53 +00:00
e65b890880 Update db schema
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Release Drafter / Update release notes draft (pull_request) Successful in 3s
2024-03-02 17:31:44 +01:00
f57edd4d3b Update README to account for Docker Compose everywhere
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-03-02 16:57:07 +01:00
1afd56fb80 Allow running specs in Docker (Web) container 2024-03-02 16:56:07 +01:00
71669a4b96 Merge pull request 'Refactor admin settings routes' (#156) from feature/content_settings into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #156
2024-03-02 14:30:21 +00:00
c312e30c17 Fix link in admin settings/services sidenav
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Release Drafter / Update release notes draft (pull_request) Successful in 2s
2024-03-02 15:26:12 +01:00
51f4556ede Refactor admin settings routes
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
This is much cleaner, and semantically more correct.
2024-03-02 14:22:08 +00:00
4fa4ae6b54 Merge pull request 'Comment out settings in .env.example' (#175) from task/env-example into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #175
Reviewed-by: Râu Cao <raucao@kosmos.org>
2024-03-02 13:30:18 +00:00
869ff4691b Comment out settings in .env.example
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Release Drafter / Update release notes draft (pull_request) Successful in 2s
2024-03-02 12:43:59 +01:00
822a2dc018 Fix specs
All checks were successful
continuous-integration/drone/push Build is passing
2024-03-01 17:15:02 +01:00
5b7fc3707b Hide avatar settings behind feature flag
Some checks failed
continuous-integration/drone/push Build is failing
In favor of #157
2024-03-01 11:13:49 +01:00
0e2dc54dc6 Merge pull request 'Upgrade Rails to 7.1, update dependencies, require Ruby 3.x' (#160) from chore/update_dependencies into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #160
Reviewed-by: slvrbckt <slvrbckt@noreply.kosmos.org>
2024-02-27 18:56:59 +00:00
87f09c94d0 Merge pull request 'Fix/improve local ActiveStorage backend usage and handling of WebApp icons' (#162) from bugfix/local_web_app_icons into chore/update_dependencies
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Release Drafter / Update release notes draft (pull_request) Successful in 2s
Reviewed-on: #162
Reviewed-by: greg <greg@noreply.kosmos.org>
2024-02-27 16:07:55 +00:00
b33b8104a8 Fix typo
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
Release Drafter / Update release notes draft (pull_request) Successful in 3s
2024-02-27 14:33:37 +01:00
4a4a222973 Merge branch 'chore/update_dependencies' into bugfix/local_web_app_icons
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-02-23 18:25:23 +00:00
8c524abcf5 Merge pull request 'Fix Docker volume permissions on some host platforms' (#171) from bugfix/macos_docker_volumes into chore/update_dependencies
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Reviewed-on: #171
Reviewed-by: galfert <garret.alfert@gmail.com>
2024-02-23 18:24:10 +00:00
a852ab75ae Fix Docker volume permissions on some host platforms
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Release Drafter / Update release notes draft (pull_request) Successful in 2s
Use named volumes instead of bind mounts.
2024-02-23 16:43:56 +01:00
de1f234c15 Merge branch 'chore/update_dependencies' into bugfix/local_web_app_icons
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-02-22 15:13:18 +01:00
4581900427 Merge pull request 'Fix Ruby in Docker container on Apple silicon' (#168) from chore/fix_docker_ruby_on_apple_silicon into chore/update_dependencies
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Reviewed-on: #168
Reviewed-by: slvrbckt <slvrbckt@noreply.kosmos.org>
2024-02-22 14:12:05 +00:00
56d91083e5 Fix seeds for new keyword argument
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
Release Drafter / Update release notes draft (pull_request) Successful in 3s
2024-02-22 13:24:41 +01:00
ba7c3795f8 Add pkg-config
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-02-22 11:29:56 +01:00
bbf3fb91a0 Fix Ruby in Docker container on Apple silicon
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-02-22 10:47:21 +01:00
1754df73cb Merge pull request 'Allow admins to add and remove invitations per account' (#167) from feature/164-invites into chore/update_dependencies
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Reviewed-on: #167
Reviewed-by: galfert <garret.alfert@gmail.com>
2024-02-17 10:17:47 +00:00
9a1f9abf84 Formatting
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Release Drafter / Update release notes draft (pull_request) Successful in 3s
2024-02-10 12:53:26 +01:00
2753388e1e Add specs for admin user management 2024-02-10 12:53:11 +01:00
f3159d30f1 Allow admins to add and remove invitations per account
All checks were successful
continuous-integration/drone/push Build is passing
2024-02-10 11:21:45 +01:00
ca238be6f4 Add option for hiding close button in modal windows 2024-02-10 10:24:09 +01:00
8747ce4eb0 Remove multi-domain support on admin user pages
All checks were successful
continuous-integration/drone/push Build is passing
refs #166
2024-02-10 08:55:15 +01:00
fcda3b9c8c WIP Make dropdowns more configurable, add invitations menu to admin page 2024-02-09 18:57:07 +01:00
67689dcce3 Add service for creating invites
All checks were successful
continuous-integration/drone/push Build is passing
2024-02-09 17:59:07 +01:00
22ffcd54db Patch away a deprecation warning caused by Devise
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-02-09 17:58:28 +01:00
bd1b177993 Rescue all icon download/upload errors, send to Sentry
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-02-08 13:36:17 +01:00
41 changed files with 432 additions and 156 deletions

View File

@@ -1,14 +1,14 @@
PRIMARY_DOMAIN=kosmos.org # PRIMARY_DOMAIN=kosmos.org
AKKOUNTS_DOMAIN=accounts.example.com # AKKOUNTS_DOMAIN=accounts.example.com
SMTP_SERVER=smtp.example.com # SMTP_SERVER=smtp.example.com
SMTP_PORT=587 # SMTP_PORT=587
SMTP_LOGIN=accounts # SMTP_LOGIN=accounts
SMTP_PASSWORD=123abc # SMTP_PASSWORD=123abc
SMTP_FROM_ADDRESS=accounts@example.com # SMTP_FROM_ADDRESS=accounts@example.com
SMTP_DOMAIN=example.com # SMTP_DOMAIN=example.com
SMTP_AUTH_METHOD=plain # SMTP_AUTH_METHOD=plain
SMTP_ENABLE_STARTTLS=auto # SMTP_ENABLE_STARTTLS=auto
# S3_ENABLED=true # S3_ENABLED=true
# S3_ENDPOINT=https://s3.kosmos.org # S3_ENDPOINT=https://s3.kosmos.org
@@ -18,47 +18,47 @@ SMTP_ENABLE_STARTTLS=auto
# S3_ACCESS_KEY=123456abcdefg # S3_ACCESS_KEY=123456abcdefg
# S3_SECRET_KEY=123456789123456789123456789 # S3_SECRET_KEY=123456789123456789123456789
LDAP_HOST=localhost # LDAP_HOST=localhost
LDAP_PORT=389 # LDAP_PORT=389
LDAP_ADMIN_PASSWORD=passthebutter # LDAP_ADMIN_PASSWORD=passthebutter
LDAP_SUFFIX='dc=kosmos,dc=org' # 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 # Service Integrations
# #
BTCPAY_API_URL='http://localhost:23001/api/v1' # BTCPAY_API_URL='http://localhost:23001/api/v1'
BTCPAY_STORE_ID='' # BTCPAY_STORE_ID=''
BTCPAY_AUTH_TOKEN='' # BTCPAY_AUTH_TOKEN=''
DISCOURSE_PUBLIC_URL='https://community.kosmos.org' # DISCOURSE_PUBLIC_URL='https://community.kosmos.org'
DISCOURSE_CONNECT_SECRET='discourse_connect_ftw' # 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_ADMIN_URL='https://xmpp.kosmos.org/admin'
EJABBERD_API_URL='https://xmpp.kosmos.org/api' # 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_API_URL='http://localhost:3023'
LNDHUB_PUBLIC_URL='https://lndhub.kosmos.org' # LNDHUB_PUBLIC_URL='https://lndhub.kosmos.org'
LNDHUB_PUBLIC_KEY='0123d3be18617f39cf645851e3ba63f51fc13f0bb09e3bb25e6fd4de556486d946' # LNDHUB_PUBLIC_KEY='0123d3be18617f39cf645851e3ba63f51fc13f0bb09e3bb25e6fd4de556486d946'
LNDHUB_ADMIN_UI=true # LNDHUB_ADMIN_UI=true
LNDHUB_ADMIN_TOKEN=123456789 # LNDHUB_ADMIN_TOKEN=123456789
LNDHUB_PG_HOST=localhost # LNDHUB_PG_HOST=localhost
LNDHUB_PG_PORT=5432 # LNDHUB_PG_PORT=5432
LNDHUB_PG_DATABASE=lndhub # LNDHUB_PG_DATABASE=lndhub
LNDHUB_PG_USERNAME=lndhub # LNDHUB_PG_USERNAME=lndhub
LNDHUB_PG_PASSWORD='' # 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_STORAGE_URL='https://storage.kosmos.org'
RS_REDIS_URL='redis://localhost:6379/2' # RS_REDIS_URL='redis://localhost:6379/2'

View File

@@ -1,10 +1,18 @@
# syntax=docker/dockerfile:1 # syntax=docker/dockerfile:1
FROM ruby:3.3.0 FROM debian:bullseye-slim as base
SHELL ["/bin/bash", "-o", "pipefail", "-c"] SHELL ["/bin/bash", "-o", "pipefail", "-c"]
RUN apt-get update -qq && apt-get install -y --no-install-recommends curl \ # TODO Remove when upstream Ruby works properly on Apple silicon
ldap-utils tini libvips 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 curl -fsSL https://deb.nodesource.com/setup_lts.x | bash -
RUN apt-get update && apt-get install -y nodejs RUN apt-get update && apt-get install -y nodejs

View File

@@ -14,8 +14,10 @@ so:
1. Make sure [Docker Compose is installed][1] and Docker is running (included in 1. Make sure [Docker Compose is installed][1] and Docker is running (included in
Docker Desktop) Docker Desktop)
3. Run `docker compose up` and wait until 389ds announces its successful start 3. Run `docker compose up --build` and wait until all services have started
in the log output (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"` 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` 5. `docker compose run web rails ldap:setup`
6. `docker compose run web rails db:setup` 6. `docker compose run web rails db:setup`
@@ -28,38 +30,44 @@ have the password "user is user".
### Rails app ### Rails app
_Note: when using Docker Compose, prefix the following commands with `docker-compose
run web`._
Installing dependencies: Installing dependencies:
bundle install bundle install
yarn 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 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 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 bundle exec sidekiq -C config/sidekiq.yml
Running all specs: Running the test suite:
bundle exec rspec 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 docker-compose run -e "RAILS_ENV=test" web rspec
an app server for Rails as well as a local 389ds (LDAP) server.
For Rails developers, you probably just want to start the LDAP server: `docker-compose up ldap`, ### Docker Compose
listening on port 389 on your machine.
You can pick and choose your services adding them by name (listed in `docker-compose.yml`) at Services/containers are configured in `docker-compose.yml`.
the end of the docker compose command. eg. `docker compose up ldap redis`
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 #### 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 The setup task will first delete any existing entries in the directory tree
("dc=kosmos,dc=org"), and then create our development entries. ("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 Note that all 389ds data is stored in the `389ds-data` volume. So if you want
with a fresh installation, delete both that directory as well as the container. 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 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` * `docker compose up web redis minio liquor-cabinet`
* Head to http://localhost:9001 and log in with user `minioadmin`, password * Head to http://localhost:9001 and log in with user `minioadmin`, password

View File

@@ -2,13 +2,21 @@
<div class="relative inline-block"> <div class="relative inline-block">
<div role="button" tabindex="0" data-dropdown-target="button" <div role="button" tabindex="0" data-dropdown-target="button"
class="inline-block select-none"> class="inline-block select-none">
<% if @size == :large %>
<span class="appearance-none flex items-center inline-block"> <span class="appearance-none flex items-center inline-block">
<span class="p-2 bg-gray-50 hover:bg-gray-100 rounded-full"> <span class="p-2 bg-gray-50 hover:bg-gray-100 rounded-full">
<%= render partial: "icons/kebab-menu", locals: { <%= render partial: "icons/#{@icon_name}",
custom_class: "inline text-gray-500 h-6 w-6" locals: { custom_class: "inline text-gray-500 h-6 w-6" } %>
} %>
</span> </span>
</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>
<div data-dropdown-target="menu" <div data-dropdown-target="menu"
data-transition-enter="transition ease-out duration-200" data-transition-enter="transition ease-out duration-200"

View File

@@ -1,5 +1,8 @@
# frozen_string_literal: true # frozen_string_literal: true
class DropdownComponent < ViewComponent::Base class DropdownComponent < ViewComponent::Base
def initialize(size: :large, icon_name: "kebap-menu")
@size = size.to_sym
@icon_name = icon_name
end
end end

View File

@@ -5,7 +5,9 @@
} : nil do %> } : nil do %>
<div class="flex flex-col"> <div class="flex flex-col">
<label class="font-bold mb-1"><%= @title %></label> <label class="font-bold mb-1"><%= @title %></label>
<% if @description.present? %>
<p class="text-gray-500"><%= @descripton %></p> <p class="text-gray-500"><%= @descripton %></p>
<% end %>
</div> </div>
<div class="relative ml-4 inline-flex flex-shrink-0"> <div class="relative ml-4 inline-flex flex-shrink-0">
<%= render FormElements::ToggleComponent.new( <%= render FormElements::ToggleComponent.new(

View File

@@ -3,7 +3,7 @@
module FormElements module FormElements
class FieldsetToggleComponent < ViewComponent::Base class FieldsetToggleComponent < ViewComponent::Base
def initialize(tag: "li", form: nil, attribute: nil, field_name: nil, 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 @tag = tag
@form = form @form = form
@attribute = attribute @attribute = attribute

View File

@@ -18,9 +18,11 @@
<div class="m-1 bg-white rounded shadow"> <div class="m-1 bg-white rounded shadow">
<div class="p-8"> <div class="p-8">
<%= content %> <%= content %>
<% if @show_close_button %>
<div class="flex justify-end items-center flex-wrap mt-6"> <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> <button class="btn-md btn-blue" data-action="click->modal#close:prevent">Close</button>
</div> </div>
<% end %>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1,2 +1,5 @@
class ModalComponent < ViewComponent::Base class ModalComponent < ViewComponent::Base
def initialize(show_close_button: true)
@show_close_button = show_close_button
end
end end

View File

@@ -1,8 +1,8 @@
class Admin::Settings::RegistrationsController < Admin::SettingsController class Admin::Settings::RegistrationsController < Admin::SettingsController
def index def show
end end
def create def update
update_settings update_settings
redirect_to admin_settings_registrations_path, flash: { redirect_to admin_settings_registrations_path, flash: {

View File

@@ -1,19 +1,32 @@
class Admin::Settings::ServicesController < Admin::SettingsController class Admin::Settings::ServicesController < Admin::SettingsController
def index before_action :set_service, only: [:show, :update]
@service = params[:s]
if @service.blank? def index
redirect_to admin_settings_services_path(params: { s: "btcpay" }) redirect_to admin_settings_service_path("btcpay")
end
end end
def create def show
service = params.require(:service) end
def update
update_settings update_settings
redirect_to admin_settings_services_path(params: { s: service }), flash: { redirect_to admin_settings_service_path(@service), flash: {
success: "Settings saved" success: "Settings saved"
} }
end 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 end

View File

@@ -20,7 +20,7 @@ class Admin::SettingsController < Admin::BaseController
end end
if @errors.any? if @errors.any?
render :index and return render :show and return
end end
changed_keys.each do |key| changed_keys.each do |key|

View File

@@ -1,11 +1,11 @@
class Admin::UsersController < Admin::BaseController class Admin::UsersController < Admin::BaseController
before_action :set_user, only: [:show] before_action :set_user, except: [:index]
before_action :set_current_section before_action :set_current_section
# GET /admin/users
def index def index
ldap = LdapService.new ldap = LdapService.new
@ou = params[:ou] || Setting.primary_domain @ou = Setting.primary_domain
@orgs = ldap.fetch_organizations
@pagy, @users = pagy(User.where(ou: @ou).order(cn: :asc)) @pagy, @users = pagy(User.where(ou: @ou).order(cn: :asc))
@stats = { @stats = {
@@ -14,6 +14,7 @@ class Admin::UsersController < Admin::BaseController
} }
end end
# GET /admin/users/:username
def show def show
if Setting.lndhub_admin_enabled? if Setting.lndhub_admin_enabled?
@lndhub_user = @user.lndhub_user @lndhub_user = @user.lndhub_user
@@ -24,11 +25,35 @@ class Admin::UsersController < Admin::BaseController
@avatar = LdapManager::FetchAvatar.call(cn: @user.cn) @avatar = LdapManager::FetchAvatar.call(cn: @user.cn)
end 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 private
def set_user def set_user
address = params[:address].split("@") @user = User.find_by(cn: params[:username], ou: Setting.primary_domain)
@user = User.where(cn: address.first, ou: address.last).first http_status :not_found unless @user
end end
def set_current_section def set_current_section

View File

@@ -17,4 +17,10 @@ class NotificationMailer < ApplicationMailer
@subject = "New app connected to your storage" @subject = "New app connected to your storage"
mail to: @user.email, subject: @subject mail to: @user.email, subject: @subject
end end
def new_invitations_available
@user = params[:user]
@subject = "New invitations added to your account"
mail to: @user.email, subject: @subject
end
end end

View File

@@ -20,6 +20,8 @@ module AppCatalogManager
@app.save! @app.save!
# TODO move icon downloads to separate, async job
if icon = metadata.select_icon(sizes: "256x256") || if icon = metadata.select_icon(sizes: "256x256") ||
icon = metadata.select_icon(sizes: "192x192") icon = metadata.select_icon(sizes: "192x192")
attach_remote_image(:icon, icon) attach_remote_image(:icon, icon)
@@ -49,7 +51,12 @@ module AppCatalogManager
tempfile = Down.download(download_url) tempfile = Down.download(download_url)
@app.send(attachment_name).attach(key: key, io: tempfile, filename: filename) @app.send(attachment_name).attach(key: key, io: tempfile, filename: filename)
rescue Down::NotFound 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 end
end end

View 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

View File

@@ -1,7 +1,7 @@
<%= render HeaderComponent.new(title: "Settings") %> <%= render HeaderComponent.new(title: "Settings") %>
<%= render MainWithSidenavComponent.new(sidenav_partial: 'shared/admin_sidenav_settings') do %> <%= 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> <section>
<h3>Registrations</h3> <h3>Registrations</h3>

View File

@@ -1,9 +1,7 @@
<%= render HeaderComponent.new(title: "Settings") %> <%= render HeaderComponent.new(title: "Settings") %>
<%= render MainWithSidenavComponent.new(sidenav_partial: 'shared/admin_sidenav_settings') do %> <%= render MainWithSidenavComponent.new(sidenav_partial: 'shared/admin_sidenav_settings') do %>
<%= form_for(Setting.new, url: admin_settings_services_path) do |f| %> <%= form_for(Setting.new, url: admin_settings_service_path(@service), method: :put) do |f| %>
<%= hidden_field_tag :service, @service %>
<% if @errors && @errors.any? %> <% if @errors && @errors.any? %>
<section> <section>
<%= render partial: "admin/settings/errors", locals: { errors: @errors } %> <%= render partial: "admin/settings/errors", locals: { errors: @errors } %>

View 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 %>

View File

@@ -1,4 +1,4 @@
<%= render HeaderComponent.new(title: "Users: #{@ou}") %> <%= render HeaderComponent.new(title: "Users") %>
<%= render MainSimpleComponent.new do %> <%= render MainSimpleComponent.new do %>
<section> <section>
@@ -16,19 +16,6 @@
<% end %> <% end %>
</section> </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> <section>
<table class="divided mb-8"> <table class="divided mb-8">
<thead> <thead>
@@ -36,13 +23,12 @@
<th>UID</th> <th>UID</th>
<th>Status</th> <th>Status</th>
<th>Roles</th> <th>Roles</th>
<!-- <th>Password</th> -->
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<% @users.each do |user| %> <% @users.each do |user| %>
<tr> <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.confirmed_at.nil? ? badge("pending", :yellow) : "" %></td>
<td><%= user.is_admin? ? badge("admin", :red) : "" %></td> <td><%= user.is_admin? ? badge("admin", :red) : "" %></td>
</tr> </tr>

View File

@@ -1,4 +1,4 @@
<%= render HeaderComponent.new(title: "User: #{@user.address}") %> <%= render HeaderComponent.new(title: "User: #{@user.cn}") %>
<%= render MainSimpleComponent.new do %> <%= render MainSimpleComponent.new do %>
<div class="mb-12 sm:flex sm:flex-row sm:gap-x-8"> <div class="mb-12 sm:flex sm:flex-row sm:gap-x-8">
@@ -42,8 +42,34 @@
</tr> </tr>
<tr> <tr>
<th>Invitations available</th> <th>Invitations available</th>
<td> <td data-controller="modal" data-action="keydown.esc->modal#close">
<%= @user.invitations.count %> <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> </td>
</tr> </tr>
<tr> <tr>

View File

@@ -1,6 +1,7 @@
<% <%
# TODO remove when https://github.com/hotwired/turbo/issues/203 is fixed # 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") %> <%= render HeaderCompactComponent.new(title: "Log in") %>

View File

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -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

View File

@@ -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

View File

@@ -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.

View File

@@ -31,6 +31,7 @@
<% end %> <% end %>
<% end %> <% end %>
<% if Flipper.enabled?(:avatar_upload, current_user) %>
<label class="block"> <label class="block">
<p class="font-bold mb-1"> <p class="font-bold mb-1">
Avatar Avatar
@@ -56,6 +57,7 @@
</div> </div>
</div> </div>
</label> </label>
<% end %>
<p class="mt-8 pt-6 border-t border-gray-200 text-right"> <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" %> <%= f.submit 'Save', class: "btn-md btn-blue w-full md:w-auto" %>

View File

@@ -4,9 +4,9 @@
) %> ) %>
<%= render SidenavLinkComponent.new( <%= render SidenavLinkComponent.new(
name: "Services", path: admin_settings_services_path, icon: "grid", 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" %> <%= render partial: "shared/admin_sidenav_settings_services" %>
<% end %> <% end %>
<%= render SidenavLinkComponent.new( <%= render SidenavLinkComponent.new(

View File

@@ -1,77 +1,77 @@
<%= render SidenavLinkComponent.new( <%= render SidenavLinkComponent.new(
level: 2, level: 2,
name: "BTCPay", name: "BTCPay",
path: admin_settings_services_path(params: { s: "btcpay" }), path: admin_settings_service_path("btcpay"),
text_icon: Setting.btcpay_enabled? ? "◉" : "○", 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( <%= render SidenavLinkComponent.new(
level: 2, level: 2,
name: "Discourse", name: "Discourse",
path: admin_settings_services_path(params: { s: "discourse" }), path: admin_settings_service_path("discourse"),
text_icon: Setting.discourse_enabled? ? "◉" : "○", 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( <%= render SidenavLinkComponent.new(
level: 2, level: 2,
name: "Drone CI", name: "Drone CI",
path: admin_settings_services_path(params: { s: "droneci" }), path: admin_settings_service_path("droneci"),
text_icon: Setting.droneci_enabled? ? "◉" : "○", 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( <%= render SidenavLinkComponent.new(
level: 2, level: 2,
name: "E-Mail", name: "E-Mail",
path: admin_settings_services_path(params: { s: "email" }), path: admin_settings_service_path("email"),
text_icon: Setting.email_enabled? ? "◉" : "○", text_icon: Setting.email_enabled? ? "◉" : "○",
active: current_page?(admin_settings_services_path(params: { s: "email" })), active: current_page?(admin_settings_services_path(params: { s: "email" })),
) %> ) %>
<%= render SidenavLinkComponent.new( <%= render SidenavLinkComponent.new(
level: 2, level: 2,
name: "ejabberd", name: "ejabberd",
path: admin_settings_services_path(params: { s: "ejabberd" }), path: admin_settings_service_path("ejabberd"),
text_icon: Setting.ejabberd_enabled? ? "◉" : "○", 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( <%= render SidenavLinkComponent.new(
level: 2, level: 2,
name: "Gitea", name: "Gitea",
path: admin_settings_services_path(params: { s: "gitea" }), path: admin_settings_service_path("gitea"),
text_icon: Setting.gitea_enabled? ? "◉" : "○", 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( <%= render SidenavLinkComponent.new(
level: 2, level: 2,
name: "LNDHub", name: "LNDHub",
path: admin_settings_services_path(params: { s: "lndhub" }), path: admin_settings_service_path("lndhub"),
text_icon: Setting.lndhub_enabled? ? "◉" : "○", 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( <%= render SidenavLinkComponent.new(
level: 2, level: 2,
name: "Mastodon", name: "Mastodon",
path: admin_settings_services_path(params: { s: "mastodon" }), path: admin_settings_service_path("mastodon"),
text_icon: Setting.mastodon_enabled? ? "◉" : "○", 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( <%= render SidenavLinkComponent.new(
level: 2, level: 2,
name: "MediaWiki", name: "MediaWiki",
path: admin_settings_services_path(params: { s: "mediawiki" }), path: admin_settings_service_path("mediawiki"),
text_icon: Setting.mediawiki_enabled? ? "◉" : "○", 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( <%= render SidenavLinkComponent.new(
level: 2, level: 2,
name: "Nostr", name: "Nostr",
path: admin_settings_services_path(params: { s: "nostr" }), path: admin_settings_service_path("nostr"),
text_icon: Setting.nostr_enabled? ? "◉" : "○", 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( <%= render SidenavLinkComponent.new(
level: 2, level: 2,
name: "RemoteStorage", name: "RemoteStorage",
path: admin_settings_services_path(params: { s: "remotestorage" }), path: admin_settings_service_path("remotestorage"),
text_icon: Setting.remotestorage_enabled? ? "◉" : "○", text_icon: Setting.remotestorage_enabled? ? "◉" : "○",
active: current_page?(admin_settings_services_path(params: { s: "remotestorage" })), active: current_page?(admin_settings_service_path("remotestorage")),
) %> ) %>

View File

@@ -52,10 +52,9 @@ Rails.application.configure do
config.active_job.queue_adapter = :test 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 config.active_storage.service = :s3
else else
# Store attachments on the local disk (in ./tmp) config.active_storage.service = :local
config.active_storage.service = :test
end end
end end

View File

@@ -325,3 +325,10 @@ Devise.setup do |config|
# changed. Defaults to true, so a user is signed in automatically after changing a password. # changed. Defaults to true, so a user is signed in automatically after changing a password.
# config.sign_in_after_change_password = true # config.sign_in_after_change_password = true
end end
# https://github.com/heartcombo/devise/issues/5644
class Devise::SecretKeyFinder
def find
@application.secret_key_base
end
end

View File

@@ -73,9 +73,19 @@ Rails.application.routes.draw do
namespace :admin do namespace :admin do
root to: 'dashboard#index' 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' get 'invitations', to: 'invitations#index'
resources :donations resources :donations
get 'lightning', to: 'lightning#index' get 'lightning', to: 'lightning#index'
namespace :app_catalog do namespace :app_catalog do
@@ -83,8 +93,8 @@ Rails.application.routes.draw do
end end
namespace :settings do namespace :settings do
resources 'registrations', only: ['index', 'create'] resource 'registrations', only: ['show', 'update']
resources 'services', only: ['index', 'create'] resources 'services', param: 'service', only: ['index', 'show', 'update']
end end
end end

View File

@@ -10,7 +10,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # 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| create_table "active_storage_attachments", force: :cascade do |t|
t.string "name", null: false t.string "name", null: false
t.string "record_type", 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| create_table "donations", force: :cascade do |t|
t.integer "user_id" t.integer "user_id"
t.integer "amount_sats" t.integer "amount_sats"
t.integer "amount_eur" t.integer "fiat_amount"
t.integer "amount_usd"
t.string "public_name" t.string "public_name"
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
t.datetime "paid_at", precision: nil 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" t.index ["user_id"], name: "index_donations_on_user_id"
end end

View File

@@ -3,10 +3,10 @@ require 'sidekiq/testing'
ldap = LdapService.new ldap = LdapService.new
Sidekiq::Testing.inline! do Sidekiq::Testing.inline! do
CreateAccount.call( CreateAccount.call(account: {
username: "admin", domain: "kosmos.org", email: "admin@example.com", username: "admin", domain: "kosmos.org", email: "admin@example.com",
password: "admin is admin", confirmed: true password: "admin is admin", confirmed: true
) })
ldap.add_attribute "cn=admin,ou=kosmos.org,cn=users,dc=kosmos,dc=org", :admin, "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 email = Faker::Internet.unique.email
next if username.length < 3 next if username.length < 3
CreateAccount.call( CreateAccount.call(account: {
username: username, domain: "kosmos.org", email: email, username: username, domain: "kosmos.org", email: email,
password: "user is user", confirmed: true password: "user is user", confirmed: true
) })
end end
end end

View File

@@ -2,7 +2,7 @@ services:
ldap: ldap:
image: 4teamwork/389ds:latest image: 4teamwork/389ds:latest
volumes: volumes:
- ./tmp/389ds:/data - 389ds-data:/data
networks: networks:
- external_network - external_network
- internal_network - internal_network
@@ -16,11 +16,12 @@ services:
restart: always restart: always
image: redis:7-alpine image: redis:7-alpine
networks: networks:
- external_network
- internal_network - internal_network
healthcheck: healthcheck:
test: ['CMD', 'redis-cli', 'ping'] test: ['CMD', 'redis-cli', 'ping']
volumes: volumes:
- ./tmp/redis:/data - redis-data:/data
web: web:
build: . build: .
@@ -84,10 +85,10 @@ services:
- "9000:9000" - "9000:9000"
- "9001:9001" - "9001:9001"
volumes: volumes:
- ./tmp/minio:/data - minio-data:/data
liquor-cabinet: 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: networks:
- external_network - external_network
- internal_network - internal_network
@@ -119,3 +120,11 @@ networks:
external_network: external_network:
internal_network: internal_network:
internal: true internal: true
volumes:
389ds-data:
driver: local
minio-data:
driver: local
redis-data:
driver: local

View File

@@ -11,7 +11,7 @@
"postcss-preset-env": "^7.8.3", "postcss-preset-env": "^7.8.3",
"tailwindcss": "^3.2.4" "tailwindcss": "^3.2.4"
}, },
"version": "0.8.1", "version": "0.9.0",
"scripts": { "scripts": {
"build:css:tailwind": "tailwindcss --postcss -i ./app/assets/stylesheets/application.tailwind.css -o ./app/assets/builds/application.css", "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" "build:css": "yarn run build:css:tailwind"

View File

@@ -23,35 +23,35 @@ RSpec.describe 'Admin/global settings', type: :feature do
scenario "Opening service settings shows page for first service" do scenario "Opening service settings shows page for first service" do
visit admin_settings_services_path 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 end
scenario "View service settings" do 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_content("Enable ejabberd integration")
expect(page).to have_field("API URL", with: "http://xmpp.example.com/api") expect(page).to have_field("API URL", with: "http://xmpp.example.com/api")
end end
scenario "Disable a service integration" do 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]") expect(page).to have_checked_field("setting[ejabberd_enabled]")
uncheck "setting[ejabberd_enabled]" uncheck "setting[ejabberd_enabled]"
click_button "Save" 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_checked_field("setting[ejabberd_enabled]")
expect(page).to_not have_field("API URL", disabled: true) expect(page).to_not have_field("API URL", disabled: true)
end end
scenario "Resettable fields" do 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 have_field("API URL", with: "http://xmpp.example.com/api")
expect(page).to_not have_css('input#setting_ejabberd_api_url+button') expect(page).to_not have_css('input#setting_ejabberd_api_url+button')
Setting.ejabberd_api_url = "http://example.com/foo" 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_field("API URL", with: "http://example.com/foo")
expect(page).to have_css('input#setting_ejabberd_api_url+button') expect(page).to have_css('input#setting_ejabberd_api_url+button')
end end

View 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

View File

@@ -12,6 +12,8 @@ RSpec.describe 'Profile settings', type: :feature do
uid: user.cn, ou: user.ou, display_name: "Mark" uid: user.cn, ou: user.ou, display_name: "Mark"
}) })
allow_any_instance_of(User).to receive(:avatar).and_return(avatar_base64) allow_any_instance_of(User).to receive(:avatar).and_return(avatar_base64)
Flipper.enable "avatar_upload"
end end
feature "Update display name" do feature "Update display name" do

View File

@@ -15,7 +15,7 @@ RSpec.describe "WebFinger", type: :request do
res = JSON.parse(response.body) res = JSON.parse(response.body)
rs_link = res["links"].find {|l| l["rel"] == "http://tools.ietf.org/id/draft-dejong-remotestorage"} 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"] 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") expect(oauth_url).to eql("http://www.example.com/rs/oauth/tony")

View 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