5 Commits

Author SHA1 Message Date
f57fff0087 Send email confirmation when BTC payment is confirmed
All checks were successful
continuous-integration/drone/push Build is passing
2024-02-22 15:20:17 +01:00
18ff3d3f0d Implement bitcoin donations via BTCPay 2024-02-22 15:19:27 +01:00
1b3ac90ddd Allow other controllers to access lndhub user balance 2024-02-22 15:19:27 +01:00
5db0ee6658 DRY up btcpay and lndhub services
Removing initialize methods from the main/manager class also allows for
different iniitalizers in specific task services
2024-02-22 15:19:27 +01:00
da31a027c5 Move past donations to partial 2024-02-22 15:19:27 +01:00
61 changed files with 230 additions and 568 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,49 +18,48 @@
# 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_PUBLIC_URL='https://btcpay.example.com' BTCPAY_PUBLIC_URL='https://btcpay.example.com'
# 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'
# MASTODON_ADDRESS_DOMAIN='https://kosmos.org'
# 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

@@ -14,10 +14,8 @@ 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 --build` and wait until all services have started 3. Run `docker compose up` and wait until 389ds announces its successful start
(389ds might take an extra minute to be ready). This will take a while when in the log output
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`
@@ -30,44 +28,38 @@ 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
Migrating the local database (after schema changes): Setting up local database (SQLite):
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 _(automatic with Docker Compose)_: Running the dev server and auto-building CSS files on change:
bin/dev bin/dev
Running the background workers (requires Redis) _(automatic with Docker Compose)_: Running the background workers (requires Redis):
bundle exec sidekiq -C config/sidekiq.yml bundle exec sidekiq -C config/sidekiq.yml
Running the test suite: Running all specs:
bundle exec rspec bundle exec rspec
Running the test suite with Docker Compose requires overriding the Rails ### Docker (Compose)
environment:
docker-compose run -e "RAILS_ENV=test" web rspec 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 For Rails developers, you probably just want to start the LDAP server: `docker-compose up ldap`,
listening on port 389 on your machine.
Services/containers are configured in `docker-compose.yml`. 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`
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
@@ -84,15 +76,13 @@ 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 the `389ds-data` volume. So if you want Note that all 389ds data is stored in `tmp/389ds`. So if you want to start over
to start over with a fresh installation, delete both that volume as well as the with a fresh installation, delete both that directory as well as the container.
container.
#### Minio / remoteStorage #### Minio / RS
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. With the `minio` container running (run by default respective bucket first:
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

@@ -1,5 +0,0 @@
<% if @image_url %>
<%= image_tag @image_url, class: "h-full w-full" %>
<% else %>
<%= render partial: "icons/remotestorage", locals: { custom_class: "h-full w-full p-0.5 text-gray-200" } %>
<% end %>

View File

@@ -1,21 +0,0 @@
# frozen_string_literal: true
module AppCatalog
class WebAppIconComponent < ViewComponent::Base
def initialize(web_app:)
if web_app&.icon&.attached?
@image_url = image_url_for(web_app.icon)
elsif web_app&.apple_touch_icon&.attached?
@image_url = image_url_for(web_app.apple_touch_icon)
end
end
def image_url_for(attachment)
if Setting.s3_enabled?
s3_image_url(attachment)
else
Rails.application.routes.url_helpers.rails_blob_path(attachment, only_path: true)
end
end
end
end

View File

@@ -1,10 +1,16 @@
<div class="flex items-center gap-4"> <div class="flex items-center gap-4">
<div class="h-16 w-16 flex-none"> <div class="h-16 w-16 flex-none">
<%= render AppCatalog::WebAppIconComponent.new(web_app: @web_app) %> <% if @web_app.icon.attached? %>
<%= image_tag s3_image_url(@web_app.icon), class: "h-full w-full" %>
<% elsif @web_app.apple_touch_icon.attached? %>
<%= image_tag s3_image_url(@web_app.apple_touch_icon), class: "h-full w-full" %>
<% else %>
<%= render partial: "icons/remotestorage", locals: { custom_class: "h-full w-full p-0.5 text-gray-200" } %>
<% end %>
</div> </div>
<div class="flex-grow"> <div class="flex-grow">
<h4 class="mb-1 text-lg font-bold"> <h4 class="mb-1 text-lg font-bold">
<%= @web_app&.name || @auth.app_name %> <%= @web_app.name %>
</h4> </h4>
<p class="text-sm text-gray-500"> <p class="text-sm text-gray-500">
<%= @auth.client_id %> <%= @auth.client_id %>

View File

@@ -8,7 +8,7 @@ class Admin::DonationsController < Admin::BaseController
@stats = { @stats = {
overall_sats: @donations.sum("amount_sats"), overall_sats: @donations.sum("amount_sats"),
donor_count: Donation.completed.count(:user_id) donor_count: @donations.distinct.count(:user_id)
} }
end end

View File

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

View File

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

View File

@@ -28,7 +28,6 @@ class Contributions::DonationsController < ApplicationController
if params[:currency] == "sats" if params[:currency] == "sats"
fiat_amount = nil fiat_amount = nil
fiat_currency = nil fiat_currency = nil
amount_sats = params[:amount]
else else
fiat_amount = params[:amount].to_i fiat_amount = params[:amount].to_i
fiat_currency = params[:currency] fiat_currency = params[:currency]

View File

@@ -12,7 +12,7 @@ class SettingsController < ApplicationController
end end
def show def show
if @settings_section == "nostr" if @settings_section == "experiments"
session[:shared_secret] ||= SecureRandom.base64(12) session[:shared_secret] ||= SecureRandom.base64(12)
end end
end end
@@ -88,7 +88,6 @@ class SettingsController < ApplicationController
def set_nostr_pubkey def set_nostr_pubkey
signed_event = nostr_event_params[:signed_event].to_h.symbolize_keys signed_event = nostr_event_params[:signed_event].to_h.symbolize_keys
is_valid_id = NostrManager::ValidateId.call(event: signed_event) is_valid_id = NostrManager::ValidateId.call(event: signed_event)
is_valid_sig = NostrManager::VerifySignature.call(event: signed_event) is_valid_sig = NostrManager::VerifySignature.call(event: signed_event)
is_correct_content = signed_event[:content] == "Connect my public key to #{current_user.address} (confirmation #{session[:shared_secret]})" is_correct_content = signed_event[:content] == "Connect my public key to #{current_user.address} (confirmation #{session[:shared_secret]})"
@@ -98,26 +97,30 @@ class SettingsController < ApplicationController
http_status :unprocessable_entity and return http_status :unprocessable_entity and return
end end
user_with_pubkey = LdapManager::FetchUserByNostrKey.call(pubkey: signed_event[:pubkey]) pubkey_taken = User.all_except(current_user).where(
ou: current_user.ou, nostr_pubkey: signed_event[:pubkey]
).any?
if user_with_pubkey.present? && (user_with_pubkey != current_user) if pubkey_taken
flash[:alert] = "Public key already in use for a different account" flash[:alert] = "Public key already in use for a different account"
http_status :unprocessable_entity and return http_status :unprocessable_entity and return
end end
LdapManager::UpdateNostrKey.call(dn: current_user.dn, pubkey: signed_event[:pubkey]) current_user.update! nostr_pubkey: signed_event[:pubkey]
session[:shared_secret] = nil session[:shared_secret] = nil
flash[:success] = "Public key verification successful" flash[:success] = "Public key verification successful"
http_status :ok http_status :ok
rescue
flash[:alert] = "Public key could not be verified"
http_status :unprocessable_entity and return
end end
# DELETE /settings/nostr_pubkey # DELETE /settings/nostr_pubkey
def remove_nostr_pubkey def remove_nostr_pubkey
# TODO require current pubkey or password to delete current_user.update! nostr_pubkey: nil
LdapManager::UpdateNostrKey.call(dn: current_user.dn, pubkey: nil)
redirect_to setting_path(:nostr), flash: { redirect_to setting_path(:experiments), flash: {
success: 'Public key removed from account' success: 'Public key removed from account'
} }
end end
@@ -131,8 +134,8 @@ class SettingsController < ApplicationController
def set_settings_section def set_settings_section
@settings_section = params[:section] @settings_section = params[:section]
allowed_sections = [ allowed_sections = [
:profile, :account, :xmpp, :email, :profile, :account, :xmpp, :email, :lightning, :remotestorage,
:lightning, :remotestorage, :nostr :experiments
] ]
unless allowed_sections.include?(@settings_section.to_sym) unless allowed_sections.include?(@settings_section.to_sym)
@@ -162,7 +165,7 @@ class SettingsController < ApplicationController
def nostr_event_params def nostr_event_params
params.permit(signed_event: [ params.permit(signed_event: [
:id, :pubkey, :created_at, :kind, :content, :sig, tags: [] :id, :pubkey, :created_at, :kind, :tags, :content, :sig
]) ])
end end

View File

@@ -1,7 +1,7 @@
class AppCatalog::WebApp < ApplicationRecord class AppCatalog::WebApp < ApplicationRecord
store :metadata, coder: JSON store :metadata, coder: JSON
has_many :remote_storage_authorizations, dependent: :destroy has_many :remote_storage_authorizations
has_one_attached :icon has_one_attached :icon
has_one_attached :apple_touch_icon has_one_attached :apple_touch_icon

View File

@@ -15,9 +15,6 @@ class Setting < RailsSettings::Base
field :redis_url, type: :string, field :redis_url, type: :string,
default: ENV["REDIS_URL"] || "redis://localhost:6379/0" default: ENV["REDIS_URL"] || "redis://localhost:6379/0"
field :s3_enabled, type: :boolean,
default: ENV["S3_ENABLED"] && ENV["S3_ENABLED"].to_s != "false"
# #
# Registrations # Registrations
# #

View File

@@ -50,6 +50,8 @@ class User < ApplicationRecord
validates_length_of :display_name, minimum: 3, maximum: 35, allow_blank: true, validates_length_of :display_name, minimum: 3, maximum: 35, allow_blank: true,
if: -> { defined?(@display_name) } if: -> { defined?(@display_name) }
validates_uniqueness_of :nostr_pubkey, allow_blank: true
validate :acceptable_avatar validate :acceptable_avatar
# #
@@ -161,41 +163,37 @@ class User < ApplicationRecord
@display_name ||= ldap_entry[:display_name] @display_name ||= ldap_entry[:display_name]
end end
def nostr_pubkey
@nostr_pubkey ||= ldap_entry[:nostr_key]
end
def nostr_pubkey_bech32
return nil unless nostr_pubkey.present?
Nostr::PublicKey.new(nostr_pubkey).to_bech32
end
def avatar def avatar
@avatar_base64 ||= LdapManager::FetchAvatar.call(cn: cn) @avatar_base64 ||= LdapManager::FetchAvatar.call(cn: cn)
end end
def services_enabled def services_enabled
ldap_entry[:services_enabled] || [] ldap_entry[:service] || []
end end
def enable_service(service) def enable_service(service)
current_services = services_enabled current_services = services_enabled
new_services = Array(service).map(&:to_s) new_services = Array(service).map(&:to_s)
services = (current_services + new_services).uniq services = (current_services + new_services).uniq
ldap.replace_attribute(dn, :serviceEnabled, services) ldap.replace_attribute(dn, :service, services)
end end
def disable_service(service) def disable_service(service)
current_services = services_enabled current_services = services_enabled
disabled_services = Array(service).map(&:to_s) disabled_services = Array(service).map(&:to_s)
services = (current_services - disabled_services).uniq services = (current_services - disabled_services).uniq
ldap.replace_attribute(dn, :serviceEnabled, services) ldap.replace_attribute(dn, :service, services)
end end
def disable_all_services def disable_all_services
ldap.delete_attribute(dn,:service) ldap.delete_attribute(dn,:service)
end end
def nostr_pubkey_bech32
return nil unless nostr_pubkey.present?
Nostr::PublicKey.new(nostr_pubkey).to_bech32
end
private private
def ldap def ldap

View File

@@ -18,10 +18,6 @@ module AppCatalogManager
@app.metadata[prop] = metadata.send(prop) if prop @app.metadata[prop] = metadata.send(prop) if prop
end end
@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)
@@ -31,6 +27,8 @@ module AppCatalogManager
if apple_touch_icon = metadata.select_icon(purpose: "apple-touch-icon") if apple_touch_icon = metadata.select_icon(purpose: "apple-touch-icon")
attach_remote_image(:apple_touch_icon, apple_touch_icon) attach_remote_image(:apple_touch_icon, apple_touch_icon)
end end
@app.save!
rescue Manifique::Error => e rescue Manifique::Error => e
msg = "Fetching web app manifest failed for #{e.url}: #{e.type}" msg = "Fetching web app manifest failed for #{e.url}: #{e.type}"
Rails.logger.warn(msg) Rails.logger.warn(msg)
@@ -44,19 +42,14 @@ module AppCatalogManager
else else
download_url = "#{@app.url}/#{icon["src"].gsub(/^\//,'')}" download_url = "#{@app.url}/#{icon["src"].gsub(/^\//,'')}"
end end
filename = "#{attachment_name}-#{Time.now.to_i}.png" filename = "#{attachment_name}.png"
key = "web_apps/#{@app.id}/icons/#{filename}" key = "web_apps/#{@app.id}/icons/#{attachment_name}.png"
begin begin
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
msg = "Download of \"#{attachment_name}\" failed: NotFound error for #{download_url}" Rails.logger.warn "Icon download 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

@@ -9,7 +9,7 @@ module LdapManager
attributes = %w{ jpegPhoto } attributes = %w{ jpegPhoto }
filter = Net::LDAP::Filter.eq("cn", @cn) filter = Net::LDAP::Filter.eq("cn", @cn)
entry = client.search(base: treebase, filter: filter, attributes: attributes).first entry = ldap_client.search(base: treebase, filter: filter, attributes: attributes).first
entry.try(:jpegPhoto) ? entry.jpegPhoto.first : nil entry.try(:jpegPhoto) ? entry.jpegPhoto.first : nil
end end
end end

View File

@@ -1,18 +0,0 @@
module LdapManager
class FetchUserByNostrKey < LdapManagerService
def initialize(pubkey:)
@ou = Setting.primary_domain
@pubkey = pubkey
end
def call
treebase = "ou=#{@ou},cn=users,#{ldap_suffix}"
attributes = %w{ cn }
filter = Net::LDAP::Filter.eq("nostrKey", @pubkey)
entry = client.search(base: treebase, filter: filter, attributes: attributes).first
User.find_by cn: entry.cn, ou: @ou unless entry.nil?
end
end
end

View File

@@ -1,16 +0,0 @@
module LdapManager
class UpdateNostrKey < LdapManagerService
def initialize(dn:, pubkey:)
@dn = dn
@pubkey = pubkey
end
def call
if @pubkey.present?
replace_attribute @dn, :nostrKey, @pubkey
else
delete_attribute @dn, :nostrKey
end
end
end
end

View File

@@ -1,2 +1,5 @@
class LdapManagerService < LdapService class LdapManagerService < LdapService
def suffix
@suffix ||= ENV["LDAP_SUFFIX"] || "dc=kosmos,dc=org"
end
end end

View File

@@ -1,47 +1,41 @@
class LdapService < ApplicationService class LdapService < ApplicationService
def modify(dn, operations=[]) def initialize
client.modify dn: dn, operations: operations @suffix = ENV["LDAP_SUFFIX"] || "dc=kosmos,dc=org"
client.get_operation_result.code
end end
def add_attribute(dn, attr, values) def add_attribute(dn, attr, values)
client.add_attribute dn, attr, values ldap_client.add_attribute dn, attr, values
client.get_operation_result.code
end end
def replace_attribute(dn, attr, values) def replace_attribute(dn, attr, values)
client.replace_attribute dn, attr, values ldap_client.replace_attribute dn, attr, values
client.get_operation_result.code
end end
def delete_attribute(dn, attr) def delete_attribute(dn, attr)
client.delete_attribute dn, attr ldap_client.delete_attribute dn, attr
client.get_operation_result.code
end end
def add_entry(dn, attrs, interactive=false) def add_entry(dn, attrs, interactive=false)
puts "Add entry: #{dn}" if interactive puts "Adding entry: #{dn}" if interactive
client.add dn: dn, attributes: attrs res = ldap_client.add dn: dn, attributes: attrs
client.get_operation_result.code puts res.inspect if interactive && !res
res
end end
def delete_entry(dn, interactive=false) def delete_entry(dn, interactive=false)
puts "Delete entry: #{dn}" if interactive puts "Deleting entry: #{dn}" if interactive
client.delete dn: dn res = ldap_client.delete dn: dn
client.get_operation_result.code puts res.inspect if interactive && !res
res
end end
def delete_all_users! def delete_all_entries!
delete_all_entries!(objectclass: "person")
end
def delete_all_entries!(objectclass: "*")
if Rails.env.production? if Rails.env.production?
raise "Mass deletion of entries not allowed in production" raise "Mass deletion of entries not allowed in production"
end end
filter = Net::LDAP::Filter.eq("objectClass", objectclass) filter = Net::LDAP::Filter.eq("objectClass", "*")
entries = client.search(base: ldap_suffix, filter: filter, attributes: %w{dn}) entries = ldap_client.search(base: @suffix, filter: filter, attributes: %w{dn})
entries.sort_by!{ |e| e.dn.length }.reverse! entries.sort_by!{ |e| e.dn.length }.reverse!
entries.each do |e| entries.each do |e|
@@ -51,18 +45,18 @@ class LdapService < ApplicationService
def fetch_users(args={}) def fetch_users(args={})
if args[:ou] if args[:ou]
treebase = "ou=#{args[:ou]},cn=users,#{ldap_suffix}" treebase = "ou=#{args[:ou]},cn=users,#{@suffix}"
else else
treebase = ldap_config["base"] treebase = ldap_config["base"]
end end
attributes = %w[ attributes = %w[
dn cn uid mail displayName admin serviceEnabled dn cn uid mail displayName admin service
mailRoutingAddress mailpassword nostrKey mailRoutingAddress mailpassword
] ]
filter = Net::LDAP::Filter.eq("uid", args[:uid] || "*") filter = Net::LDAP::Filter.eq("uid", args[:uid] || "*")
entries = client.search(base: treebase, filter: filter, attributes: attributes) entries = ldap_client.search(base: treebase, filter: filter, attributes: attributes)
entries.sort_by! { |e| e.cn[0] } entries.sort_by! { |e| e.cn[0] }
entries = entries.collect do |e| entries = entries.collect do |e|
{ {
@@ -70,10 +64,9 @@ class LdapService < ApplicationService
mail: e.try(:mail) ? e.mail.first : nil, mail: e.try(:mail) ? e.mail.first : nil,
display_name: e.try(:displayName) ? e.displayName.first : nil, display_name: e.try(:displayName) ? e.displayName.first : nil,
admin: e.try(:admin) ? 'admin' : nil, admin: e.try(:admin) ? 'admin' : nil,
services_enabled: e.try(:serviceEnabled), service: e.try(:service),
email_maildrop: e.try(:mailRoutingAddress), email_maildrop: e.try(:mailRoutingAddress),
email_password: e.try(:mailpassword), email_password: e.try(:mailpassword)
nostr_key: e.try(:nostrKey) ? e.nostrKey.first : nil
} }
end end
end end
@@ -82,9 +75,9 @@ class LdapService < ApplicationService
attributes = %w{dn ou description} attributes = %w{dn ou description}
filter = Net::LDAP::Filter.eq("objectClass", "organizationalUnit") filter = Net::LDAP::Filter.eq("objectClass", "organizationalUnit")
# filter = Net::LDAP::Filter.eq("objectClass", "*") # filter = Net::LDAP::Filter.eq("objectClass", "*")
treebase = "cn=users,#{ldap_suffix}" treebase = "cn=users,#{@suffix}"
entries = client.search(base: treebase, filter: filter, attributes: attributes) entries = ldap_client.search(base: treebase, filter: filter, attributes: attributes)
entries.sort_by! { |e| e.ou[0] } entries.sort_by! { |e| e.ou[0] }
@@ -98,10 +91,10 @@ class LdapService < ApplicationService
end end
def add_organization(ou, description, interactive=false) def add_organization(ou, description, interactive=false)
dn = "ou=#{ou},cn=users,#{ldap_suffix}" dn = "ou=#{ou},cn=users,#{@suffix}"
aci = <<-EOS aci = <<-EOS
(target="ldap:///cn=*,ou=#{ou},cn=users,#{ldap_suffix}")(targetattr="cn || sn || uid || mail || userPassword || nsRole || objectClass") (version 3.0; acl "service-#{ou.gsub(".", "-")}-read-search"; allow (read,search) userdn="ldap:///uid=service,ou=#{ou},cn=applications,#{ldap_suffix}";) (target="ldap:///cn=*,ou=#{ou},cn=users,#{@suffix}")(targetattr="cn || sn || uid || mail || userPassword || nsRole || objectClass") (version 3.0; acl "service-#{ou.gsub(".", "-")}-read-search"; allow (read,search) userdn="ldap:///uid=service,ou=#{ou},cn=applications,#{@suffix}";)
EOS EOS
attrs = { attrs = {
@@ -122,22 +115,22 @@ class LdapService < ApplicationService
delete_all_entries! delete_all_entries!
user_read_aci = <<-EOS user_read_aci = <<-EOS
(target="ldap:///#{ldap_suffix}")(targetattr="*") (version 3.0; acl "user-read-search-own-attributes"; allow (read,search) userdn="ldap:///self";) (target="ldap:///#{@suffix}")(targetattr="*") (version 3.0; acl "user-read-search-own-attributes"; allow (read,search) userdn="ldap:///self";)
EOS EOS
add_entry ldap_suffix, { add_entry @suffix, {
dc: "kosmos", objectClass: ["top", "domain"], aci: user_read_aci dc: "kosmos", objectClass: ["top", "domain"], aci: user_read_aci
}, true }, true
add_entry "cn=users,#{ldap_suffix}", { add_entry "cn=users,#{@suffix}", {
cn: "users", objectClass: ["top", "organizationalRole"] cn: "users", objectClass: ["top", "organizationalRole"]
}, true }, true
end end
private private
def client def ldap_client
client ||= Net::LDAP.new host: ldap_config['host'], ldap_client ||= Net::LDAP.new host: ldap_config['host'],
port: ldap_config['port'], port: ldap_config['port'],
# TODO has to be :simple_tls if TLS is enabled # TODO has to be :simple_tls if TLS is enabled
# encryption: ldap_config['ssl'], # encryption: ldap_config['ssl'],
@@ -151,8 +144,4 @@ class LdapService < ApplicationService
def ldap_config def ldap_config
ldap_config ||= YAML.load(ERB.new(File.read("#{Rails.root}/config/ldap.yml")).result)[Rails.env] ldap_config ||= YAML.load(ERB.new(File.read("#{Rails.root}/config/ldap.yml")).result)[Rails.env]
end end
def ldap_suffix
@ldap_suffix ||= ENV["LDAP_SUFFIX"] || "dc=kosmos,dc=org"
end
end end

View File

@@ -35,7 +35,7 @@
<tbody> <tbody>
<% @donations.each do |donation| %> <% @donations.each do |donation| %>
<tr> <tr>
<td><%= link_to donation.user.cn, admin_user_path(donation.user.cn), class: 'ks-text-link' %></td> <td><%= link_to donation.user.address, admin_user_path(donation.user.address), class: 'ks-text-link' %></td>
<td class="text-right"><% if donation.amount_sats.present? %><%= number_with_delimiter donation.amount_sats %><% end %></td> <td class="text-right"><% if donation.amount_sats.present? %><%= number_with_delimiter donation.amount_sats %><% end %></td>
<td class="text-right"><% if donation.fiat_amount.present? %><%= number_to_currency donation.fiat_amount.to_f / 100, unit: "" %> <%= donation.fiat_currency %><% end %></td> <td class="text-right"><% if donation.fiat_amount.present? %><%= number_to_currency donation.fiat_amount.to_f / 100, unit: "" %> <%= donation.fiat_currency %><% end %></td>
<td class="pl-2"><%= donation.public_name %></td> <td class="pl-2"><%= donation.public_name %></td>

View File

@@ -6,7 +6,7 @@
<tbody> <tbody>
<tr> <tr>
<th>User</th> <th>User</th>
<td><%= link_to @donation.user.cn, admin_user_path(@donation.user.cn), class: 'ks-text-link' %></td> <td><%= link_to @donation.user.address, admin_user_path(@donation.user.address), class: 'ks-text-link' %></td>
</tr> </tr>
<tr> <tr>
<th>Donation Method</th> <th>Donation Method</th>

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, method: :put) do |f| %> <%= form_for(Setting.new, url: admin_settings_registrations_path) do |f| %>
<section> <section>
<h3>Registrations</h3> <h3>Registrations</h3>

View File

@@ -1,7 +1,9 @@
<%= 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_service_path(@service), method: :put) do |f| %> <%= form_for(Setting.new, url: admin_settings_services_path) 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

@@ -1,7 +1,6 @@
<% <%
# 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].blank? || enable_turbo = !session[:user_return_to] || !session[:user_return_to].match?('/discourse/connect')
['/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,6 +0,0 @@
<svg width="24" height="24" class="icon-nostrich-head <%= custom_class %>" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3.03377 4.84648C2.38935 5.60878 1.88639 6.49681 1.5799 7.4713C3.32454 7.07836 5.64286 6.98406 6.95527 6.88189C7.36392 5.20013 8.52701 3.91915 10.476 4.0056C11.3169 4.04489 12.0556 4.58714 12.5664 5.42017C12.9436 5.01937 13.4466 4.75218 14.1146 4.65787C14.1617 4.65787 14.2639 4.65001 14.3425 4.65001C12.9593 3.14114 10.9868 2.18237 8.77849 2.18237C8.3777 2.18237 7.98476 2.22167 7.59183 2.28454C7.51324 2.28454 7.41108 2.30026 7.27748 2.33169C7.26962 2.33169 7.2539 2.33169 7.24604 2.33169C7.23818 2.33169 7.23032 2.33169 7.21461 2.33169C5.69001 2.70105 4.54264 2.40242 3.89037 1.51438C3.81964 1.42008 3.54458 1.00357 3.45814 0.272705C2.97876 0.767805 2.66441 1.58511 2.9316 2.45743C3.14379 3.149 3.54458 3.51836 3.97681 3.73054C3.31668 3.76984 2.76657 3.6441 2.21646 3.22759C1.89425 2.98396 1.68992 2.71677 1.352 2.01734C1.03765 2.51244 1.06909 3.06255 1.13195 3.34547C1.21054 3.72268 1.40701 4.14706 1.65849 4.39068C2.04357 4.76789 2.59368 4.85434 3.04162 4.84648H3.03377Z" fill="currentColor"/>
<path d="M10.4837 11.3458C11.4602 11.3458 12.2519 9.99116 12.2519 8.32016C12.2519 6.64917 11.4602 5.29456 10.4837 5.29456C9.50711 5.29456 8.71545 6.64917 8.71545 8.32016C8.71545 9.99116 9.50711 11.3458 10.4837 11.3458Z" fill="currentColor"/>
<path d="M14.3737 10.615C15.1376 10.615 15.7569 9.53831 15.7569 8.21019C15.7569 6.88207 15.1376 5.80542 14.3737 5.80542C13.6099 5.80542 12.9906 6.88207 12.9906 8.21019C12.9906 9.53831 13.6099 10.615 14.3737 10.615Z" fill="currentColor"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.52542 23.9833C7.53337 23.6314 7.66454 22.5232 8.7864 20.3047C9.2815 19.3381 10.4053 18.0021 11.2462 17.2791C11.6941 16.8862 12.1421 16.5561 12.5822 16.2496C12.8101 16.116 13.0222 15.9745 13.2266 15.8252C16.9076 13.5684 20.157 14.0396 22.8528 14.4306L22.9321 14.4421C22.9321 14.4421 23.5765 12.5246 20.9203 11.5344C19.4743 11 17.7689 10.5677 16.3465 10.2691C16.1422 10.6385 15.8828 10.9528 15.5763 11.1886C15.5721 11.1917 15.5678 11.195 15.5634 11.1983C15.3354 11.3696 14.795 11.7757 13.816 11.6601C13.313 11.5972 12.9279 11.3929 12.6215 11.0943C12.1028 11.9509 11.3562 12.5088 10.4917 12.5874C8.09483 12.7918 6.88458 10.7799 6.806 8.55591C5.00635 8.7288 2.55443 9.83688 1.24988 10.4813L1.25662 22.0396C2.92115 22.6846 5.41819 23.4807 7.52542 23.9833Z" fill="currentColor"/>
</svg>

Before

Width:  |  Height:  |  Size: 2.4 KiB

View File

@@ -1,3 +0,0 @@
<svg width="24" height="24" class="icon-nostrich-n <%= custom_class %>" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M24 10.4604V23.135C24 23.6117 23.6161 23.9985 23.1429 23.9985H12.8578C12.3847 23.9985 12.0008 23.6117 12.0008 23.135V20.7746C12.0476 17.8812 12.3515 15.1096 12.9894 13.8487C13.3718 13.0904 14.0021 12.6777 14.7262 12.4569C16.0942 12.0426 18.4947 12.3259 19.5135 12.2772C19.5135 12.2772 22.5912 12.4005 22.5912 10.6447C22.5912 9.23147 21.2156 9.34264 21.2156 9.34264C19.6994 9.38223 18.5446 9.27868 17.7963 8.98173C16.5432 8.48528 16.5009 7.57462 16.4963 7.27005C16.4343 3.75228 11.2858 3.33046 6.74939 4.20305C1.78976 5.1533 6.80381 12.3152 6.80381 21.8756V23.1518C6.79474 23.6208 6.41834 24 5.94974 24H0.857089C0.383951 24 0 23.6132 0 23.1365V1.21523C0 0.738579 0.383951 0.351777 0.857089 0.351777H5.64439C6.11753 0.351777 6.50148 0.738579 6.50148 1.21523C6.50148 1.92335 7.29206 2.31777 7.86345 1.90508C9.58519 0.662437 11.7952 0 14.2682 0C19.8083 0 23.997 3.25279 23.997 10.4604H24ZM14.8033 7.88832C14.8033 6.86802 13.9825 6.04112 12.9697 6.04112C11.9569 6.04112 11.1361 6.86802 11.1361 7.88832C11.1361 8.90863 11.9569 9.73553 12.9697 9.73553C13.9825 9.73553 14.8033 8.90863 14.8033 7.88832Z" fill="currentColor"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -1,3 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" class="icon-nostrich <%= custom_class %>" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M17.7084 10.1607C18.1683 13.3466 14.8705 14.0207 12.9733 13.9618C12.8515 13.958 12.7366 14.0173 12.6647 14.1157C12.4684 14.384 12.1547 14.7309 11.9125 14.7309C11.6405 14.7309 11.3957 15.254 11.284 15.5795C11.2723 15.6137 11.3059 15.6452 11.3403 15.634C14.345 14.6584 15.5241 14.3238 16.032 14.4178C16.4421 14.4937 17.209 15.8665 17.5413 16.5434C16.7155 16.5909 16.4402 15.8507 16.2503 15.7178C16.0985 15.6116 16.0415 16.0974 16.032 16.3536C15.8517 16.2587 15.6239 16.1259 15.6049 15.7178C15.5859 15.3098 15.3771 15.4142 15.2157 15.4332C15.0544 15.4521 12.5769 16.2493 12.2067 16.3536C11.8366 16.458 11.4094 16.6004 11.0582 16.8471C10.4697 17.1318 10.09 16.9325 9.98561 16.4485C9.90208 16.0614 10.4444 14.8701 10.726 14.3229C10.3779 14.4526 9.65529 14.7158 9.54898 14.7309C9.44588 14.7457 8.13815 15.7552 7.43879 16.3038C7.398 16.3358 7.37174 16.3827 7.36236 16.4336C7.25047 17.0416 6.89335 17.2118 6.27423 17.5303C5.77602 17.7867 4.036 20.4606 3.14127 21.9041C3.0794 22.0039 2.9886 22.0806 2.8911 22.1461C2.32279 22.5276 1.74399 23.4985 1.50923 23.9737C1.17511 23.0095 1.61048 22.1802 1.86993 21.886C1.75602 21.7873 1.49341 21.8449 1.37634 21.886C1.69907 20.7757 2.82862 20.7757 2.79066 20.7757C2.99948 20.5954 5.44842 17.0938 5.50538 16.9325C5.56187 16.7725 5.46892 16.0242 6.69975 15.6139C6.7193 15.6073 6.73868 15.5984 6.75601 15.5873C7.71493 14.971 8.43427 13.9774 8.67571 13.5542C7.39547 13.4662 5.92943 12.7525 5.16289 12.294C4.99765 12.1952 4.8224 12.1092 4.63108 12.0875C3.58154 11.9687 2.53067 12.6401 2.10723 13.0228C1.93258 12.7799 2.12938 12.0739 2.24961 11.7513C1.82437 11.6905 1.19916 12.308 0.939711 12.6243C0.658747 12.184 0.904907 11.397 1.06311 11.0585C0.501179 11.0737 0.120232 11.3306 0 11.4571C0.465109 7.99343 4.02275 9.00076 4.06259 9.04675C3.87275 8.84937 3.88857 8.59126 3.92021 8.48688C6.0749 8.54381 7.08105 8.18321 7.71702 7.81313C12.7288 5.01374 14.8882 6.73133 15.6856 7.1631C16.4829 7.59487 17.9304 7.77042 18.9318 7.37187C20.1278 6.83097 19.9478 5.43673 19.7054 4.90461C19.4397 4.32101 17.9399 3.51438 17.4084 2.49428C16.8768 1.47418 17.34 0.233672 17.9558 0.0607684C18.5425 -0.103972 18.9615 0.0876835 19.2831 0.378128C19.4974 0.571763 20.0994 0.710259 20.3509 0.800409C20.6024 0.890558 21.0201 1.00918 20.9964 1.08035C20.9726 1.15152 20.5699 1.14202 20.5075 1.14202C20.3794 1.14202 20.2275 1.161 20.3794 1.23217C20.5575 1.30439 20.8263 1.40936 20.955 1.47846C20.9717 1.48744 20.9683 1.51084 20.95 1.51577C20.0765 1.75085 19.2966 1.26578 18.7183 1.82526C18.1298 2.39463 19.3827 2.83114 20.0282 3.51438C20.6736 4.19762 21.3381 5.01372 20.8065 6.87365C20.395 8.31355 18.6703 9.53781 17.7795 10.0167C17.7282 10.0442 17.7001 10.1031 17.7084 10.1607Z" fill="currentColor"/>
</svg>

Before

Width:  |  Height:  |  Size: 2.8 KiB

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<svg width="24" height="24" class="icon-remotestorage <%= custom_class %>" clip-rule="evenodd" fill-rule="evenodd" image-rendering="optimizeQuality" shape-rendering="geometricPrecision" text-rendering="geometricPrecision" version="1.1" viewBox="0 0 250 249.9" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"> <svg width="24" height="24" class="<%= custom_class %>" clip-rule="evenodd" fill-rule="evenodd" image-rendering="optimizeQuality" shape-rendering="geometricPrecision" text-rendering="geometricPrecision" version="1.1" viewBox="0 0 250 249.9" xml:space="preserve" xmlns="http://www.w3.org/2000/svg">
<g transform="translate(-66.822 -.16484)"> <g transform="translate(-66.822 -.16484)">
<polygon id="polygon1" fill="currentColor" transform="matrix(.29308 0 0 .29308 83.528 -.028385)" points="228 181 370 100 511 181 652 263 370 425 87 263 87 263 0 213 0 213 0 311 0 378 0 427 0 476 86 525 185 582 370 689 554 582 653 525 653 590 653 592 370 754 0 542 0 640 185 747 370 853 554 747 739 640 739 525 739 476 739 427 739 378 653 427 370 589 86 427 86 361 185 418 370 524 554 418 653 361 739 311 739 213 554 107 370 0 185 107 58 180 144 230"/> <polygon id="polygon1" fill="currentColor" transform="matrix(.29308 0 0 .29308 83.528 -.028385)" points="228 181 370 100 511 181 652 263 370 425 87 263 87 263 0 213 0 213 0 311 0 378 0 427 0 476 86 525 185 582 370 689 554 582 653 525 653 590 653 592 370 754 0 542 0 640 185 747 370 853 554 747 739 640 739 525 739 476 739 427 739 378 653 427 370 589 86 427 86 361 185 418 370 524 554 418 653 361 739 311 739 213 554 107 370 0 185 107 58 180 144 230"/>
</g> </g>

Before

Width:  |  Height:  |  Size: 867 B

After

Width:  |  Height:  |  Size: 848 B

View File

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

View File

@@ -34,7 +34,7 @@
<% end %> <% end %>
<% if Setting.nostr_enabled %> <% if Setting.nostr_enabled %>
<%= render SidenavLinkComponent.new( <%= render SidenavLinkComponent.new(
name: "Nostr", path: setting_path(:nostr), icon: "nostrich-head", name: "Experiments", path: setting_path(:experiments), icon: "science",
active: @settings_section.to_s == "nostr" active: @settings_section.to_s == "experiments"
) %> ) %>
<% end %> <% end %>

View File

@@ -71,7 +71,7 @@ Rails.application.configure do
# Allow requests from any IP # Allow requests from any IP
config.web_console.permissions = '0.0.0.0/0' config.web_console.permissions = '0.0.0.0/0'
if ENV["S3_ENABLED"] && ENV["S3_ENABLED"].to_s != "false" if ENV["S3_ENABLED"]
config.active_storage.service = :s3 config.active_storage.service = :s3
else else
config.active_storage.service = :local config.active_storage.service = :local

View File

@@ -110,7 +110,7 @@ Rails.application.configure do
# Set this to true and configure the email server for immediate delivery to raise delivery errors. # Set this to true and configure the email server for immediate delivery to raise delivery errors.
config.action_mailer.raise_delivery_errors = true config.action_mailer.raise_delivery_errors = true
if ENV["S3_ENABLED"] && ENV["S3_ENABLED"].to_s != "false" if ENV["S3_ENABLED"]
config.active_storage.service = :s3 config.active_storage.service = :s3
else else
config.active_storage.service = :local config.active_storage.service = :local

View File

@@ -52,9 +52,10 @@ Rails.application.configure do
config.active_job.queue_adapter = :test config.active_job.queue_adapter = :test
if ENV["S3_ENABLED"] && ENV["S3_ENABLED"].to_s != "false" if ENV["S3_ENABLED"]
config.active_storage.service = :s3 config.active_storage.service = :s3
else else
config.active_storage.service = :local # Store attachments on the local disk (in ./tmp)
config.active_storage.service = :test
end end
end end

View File

@@ -27,6 +27,7 @@ Devise.setup do |config|
config.ldap_auth_password_builder = Proc.new() { |new_password| config.ldap_auth_password_builder = Proc.new() { |new_password|
salt = SecureRandom.hex(32) salt = SecureRandom.hex(32)
hashed_pw = Base64.strict_encode64(Digest::SHA512.digest(new_password + salt) + salt) hashed_pw = Base64.strict_encode64(Digest::SHA512.digest(new_password + salt) + salt)
puts '{SSHA512}' + hashed_pw
'{SSHA512}' + hashed_pw '{SSHA512}' + hashed_pw
} }

View File

@@ -97,8 +97,8 @@ Rails.application.routes.draw do
end end
namespace :settings do namespace :settings do
resource 'registrations', only: ['show', 'update'] resources 'registrations', only: ['index', 'create']
resources 'services', param: 'service', only: ['index', 'show', 'update'] resources 'services', only: ['index', 'create']
end end
end end

View File

@@ -1,12 +1,12 @@
local: local:
service: Disk service: Disk
root: <%= ENV["ACTIVE_STORAGE_PATH"] || Rails.root.join("storage") %> root: <%= Rails.root.join("storage") %>
test: test:
service: Disk service: Disk
root: <%= Rails.root.join("tmp/storage") %> root: <%= Rails.root.join("tmp/storage") %>
<% if ENV["S3_ENABLED"] && ENV["S3_ENABLED"].to_s != "false" %> <% if ENV["S3_ENABLED"] %>
s3: s3:
service: S3 service: S3
endpoint: <%= ENV["S3_ENDPOINT"] %> endpoint: <%= ENV["S3_ENDPOINT"] %>

View File

@@ -1,5 +0,0 @@
class RemoveNostrPubkeyFromUsers < ActiveRecord::Migration[7.1]
def change
remove_column :users, :nostr_pubkey, :string
end
end

View File

@@ -10,12 +10,12 @@
# #
# 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_03_16_153558) 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
t.bigint "record_id", null: false t.integer "record_id", null: false
t.bigint "blob_id", null: false t.integer "blob_id", null: false
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.index ["blob_id"], name: "index_active_storage_attachments_on_blob_id" t.index ["blob_id"], name: "index_active_storage_attachments_on_blob_id"
t.index ["record_type", "record_id", "name", "blob_id"], name: "index_active_storage_attachments_uniqueness", unique: true t.index ["record_type", "record_id", "name", "blob_id"], name: "index_active_storage_attachments_uniqueness", unique: true
@@ -34,7 +34,7 @@ ActiveRecord::Schema[7.1].define(version: 2024_03_16_153558) do
end end
create_table "active_storage_variant_records", force: :cascade do |t| create_table "active_storage_variant_records", force: :cascade do |t|
t.bigint "blob_id", null: false t.integer "blob_id", null: false
t.string "variation_digest", null: false t.string "variation_digest", null: false
t.index ["blob_id", "variation_digest"], name: "index_active_storage_variant_records_uniqueness", unique: true t.index ["blob_id", "variation_digest"], name: "index_active_storage_variant_records_uniqueness", unique: true
end end
@@ -60,7 +60,6 @@ ActiveRecord::Schema[7.1].define(version: 2024_03_16_153558) do
t.string "payment_method" t.string "payment_method"
t.string "btcpay_invoice_id" t.string "btcpay_invoice_id"
t.string "payment_status" 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
@@ -129,6 +128,7 @@ ActiveRecord::Schema[7.1].define(version: 2024_03_16_153558) do
t.string "unconfirmed_email" t.string "unconfirmed_email"
t.text "ln_password_ciphertext" t.text "ln_password_ciphertext"
t.string "ln_account" t.string "ln_account"
t.string "nostr_pubkey"
t.datetime "remember_created_at" t.datetime "remember_created_at"
t.string "remember_token" t.string "remember_token"
t.text "preferences" t.text "preferences"

View File

@@ -3,10 +3,6 @@ require 'sidekiq/testing'
ldap = LdapService.new ldap = LdapService.new
Sidekiq::Testing.inline! do Sidekiq::Testing.inline! do
ldap.delete_all_users!
puts "Create user: admin"
CreateAccount.call(account: { 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
@@ -14,7 +10,6 @@ Sidekiq::Testing.inline! do
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"
puts "Create 35 random users"
35.times do |n| 35.times do |n|
username = Faker::Name.unique.first_name.downcase username = Faker::Name.unique.first_name.downcase
email = Faker::Internet.unique.email email = Faker::Internet.unique.email

View File

@@ -2,7 +2,7 @@ services:
ldap: ldap:
image: 4teamwork/389ds:latest image: 4teamwork/389ds:latest
volumes: volumes:
- 389ds-data:/data - ./tmp/389ds:/data
networks: networks:
- external_network - external_network
- internal_network - internal_network
@@ -16,12 +16,11 @@ 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:
- redis-data:/data - ./tmp/redis:/data
web: web:
build: . build: .
@@ -43,10 +42,8 @@ services:
LDAP_ADMIN_PASSWORD: passthebutter LDAP_ADMIN_PASSWORD: passthebutter
LDAP_USE_TLS: "false" LDAP_USE_TLS: "false"
REDIS_URL: redis://redis:6379/0 REDIS_URL: redis://redis:6379/0
ACTIVE_STORAGE_PATH: "/akkounts/tmp/attachments"
RS_REDIS_URL: redis://redis:6379/1 RS_REDIS_URL: redis://redis:6379/1
RS_STORAGE_URL: "http://localhost:4567" RS_STORAGE_URL: "http://localhost:4567"
S3_ENABLED: false
depends_on: depends_on:
- ldap - ldap
- redis - redis
@@ -70,7 +67,6 @@ services:
REDIS_URL: redis://redis:6379/0 REDIS_URL: redis://redis:6379/0
RS_REDIS_URL: redis://redis:6379/1 RS_REDIS_URL: redis://redis:6379/1
RS_STORAGE_URL: "http://localhost:4567" RS_STORAGE_URL: "http://localhost:4567"
S3_ENABLED: false
depends_on: depends_on:
- ldap - ldap
- redis - redis
@@ -85,10 +81,10 @@ services:
- "9000:9000" - "9000:9000"
- "9001:9001" - "9001:9001"
volumes: volumes:
- minio-data:/data - ./tmp/minio:/data
liquor-cabinet: liquor-cabinet:
image: gitea.kosmos.org/5apps/liquor-cabinet:2.0.0-rc.1 image: gitea.kosmos.org/5apps/liquor-cabinet:2.0.0-beta.2
networks: networks:
- external_network - external_network
- internal_network - internal_network
@@ -120,11 +116,3 @@ 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

@@ -1,6 +1,6 @@
namespace :ldap do namespace :ldap do
desc "Reset the LDAP directory and set up base entries and default org" desc "Reset the LDAP directory and set up base entries and default org"
task setup: [:environment, :add_custom_attributes] do |t, args| task setup: :environment do |t, args|
ldap = LdapService.new ldap = LdapService.new
ldap.delete_entry "cn=admin_role,ou=kosmos.org,cn=users,dc=kosmos,dc=org", true ldap.delete_entry "cn=admin_role,ou=kosmos.org,cn=users,dc=kosmos,dc=org", true
@@ -19,54 +19,6 @@ namespace :ldap do
}, true }, true
end end
# TODO
desc "Add application account to directory"
task add_application_account: :environment do |t, args|
# Add uid=service,ou=kosmos.org,cn=applications,dc=kosmos,dc=org with userPassword
end
# TODO
desc "Add application ACI/permissions for OU, i.e. read/search users"
task add_application_account: :environment do |t, args|
# (target="ldap:///cn=*,ou=#{ou},cn=users,#{ldap_suffix}")(targetattr="cn || sn || uid || mail || userPassword || nsRole || objectClass") (version 3.0; acl "service-#{ou.gsub(".", "-")}-read-search"; allow (read,search) userdn="ldap:///uid=service,ou=#{ou},cn=applications,#{ldap_suffix}";)
end
desc "Add custom attributes to schema"
task add_custom_attributes: :environment do |t, args|
%w[ admin service_enabled nostr_key ].each do |name|
Rake::Task["ldap:modify_ldap_schema"].invoke(name, "add")
Rake::Task['ldap:modify_ldap_schema'].reenable
end
end
desc "Delete custom attributes from schema"
task delete_custom_attributes: :environment do |t, args|
%w[ admin service_enabled nostr_key ].each do |name|
Rake::Task["ldap:modify_ldap_schema"].invoke(name, "delete")
Rake::Task['ldap:modify_ldap_schema'].reenable
end
end
desc "Modify LDAP schema"
task :modify_ldap_schema, [:name, :operation] => [:environment] do |t, args|
puts "Modify schema: #{args[:operation]} #{args[:name]}"
filename = "#{Rails.root}/schemas/ldap/#{args[:name]}.ldif"
ldif = YAML.safe_load(File.read(filename))
dn = ldif["dn"]
attribute = ldif["add"]
value = ldif[attribute]
operation = [ args[:operation].to_sym, attribute.to_sym, value ]
ldap = LdapService.new
res = ldap.modify dn, [ operation ]
if res != 0
puts "Result code: #{res}"
exit 1
end
end
desc "List user domains/organizations" desc "List user domains/organizations"
task list_organizations: :environment do |t, args| task list_organizations: :environment do |t, args|
ldap = LdapService.new ldap = LdapService.new

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.9.0", "version": "0.8.1",
"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

@@ -1,4 +0,0 @@
dn: ou=kosmos.org,cn=users,dc=kosmos,dc=org
changetype: modify
add: aci
aci: (target="ldap:///cn=*,ou=kosmos.org,cn=users,dc=kosmos,dc=org")(targetattr="cn || sn || uid || mail || userPassword || serviceEnabled || displayName || jpegPhoto || nsRole || objectClass") (version 3.0; acl "service-kosmos-read-search"; allow (read,search) userdn="ldap:///uid=service,ou=kosmos.org,cn=applications,dc=kosmos,dc=org";)

View File

@@ -1,9 +0,0 @@
dn: cn=schema
changetype: modify
add: attributeTypes
attributeTypes: ( 1.3.6.1.4.1.61554.1.1.2.1.1
NAME 'admin'
DESC 'Admin flag'
EQUALITY booleanMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.7
SINGLE-VALUE )

View File

@@ -1,4 +0,0 @@
dn: ou=kosmos.org,cn=users,dc=kosmos,dc=org
changetype: modify
delete: aci
aci: (target="ldap:///cn=*,ou=kosmos.org,cn=users,dc=kosmos,dc=org")(targetattr="cn || sn || uid || mail || userPassword || nsRole || objectClass") (version 3.0; acl "service-kosmos-read-search"; allow (read,search) userdn="ldap:///uid=service,ou=kosmos.org,cn=applications,dc=kosmos,dc=org";)

View File

@@ -1,9 +0,0 @@
dn: cn=schema
changetype: modify
add: attributeTypes
attributeTypes: ( 1.3.6.1.4.1.61554.1.1.2.1.21
NAME 'nostrKey'
DESC 'Nostr public key'
EQUALITY caseIgnoreMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.15
SINGLE-VALUE )

View File

@@ -1,8 +0,0 @@
dn: cn=schema
changetype: modify
add: attributeTypes
attributeTypes: ( 1.3.6.1.4.1.61554.1.1.2.1.2
NAME 'serviceEnabled'
DESC 'Services enabled for account'
EQUALITY caseExactMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )

View File

@@ -1,11 +0,0 @@
require "rails_helper"
RSpec.describe AppCatalog::WebAppIconComponent, type: :component do
describe "No web app given" do
it "renders the default icon" do
expect(
render_inline(described_class.new(web_app: nil)) {}.to_html
).to include("icon-remotestorage")
end
end
end

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_service_url("btcpay")) expect(current_url).to eq(admin_settings_services_url(params: { s: "btcpay" }))
end end
scenario "View service settings" do scenario "View service settings" do
visit admin_settings_service_path("ejabberd") visit admin_settings_services_path(params: { s: "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_service_path("ejabberd") visit admin_settings_services_path(params: { s: "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_service_url("ejabberd")) expect(current_url).to eq(admin_settings_services_url(params: { s: "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_service_path("ejabberd") visit admin_settings_services_path(params: { s: "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_service_path("ejabberd") visit admin_settings_services_path(params: { s: "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

@@ -1,19 +1,15 @@
require 'rails_helper' require 'rails_helper'
RSpec.describe 'Nostr Settings', type: :feature do RSpec.describe 'Experimental Settings', type: :feature do
let(:user) { create :user, cn: 'jimmy', ou: 'kosmos.org' } let(:user) { create :user, cn: 'jimmy', ou: 'kosmos.org' }
before do before do
login_as user, scope: :user login_as user, scope: :user
allow_any_instance_of(User).to receive(:dn)
.and_return("cn=#{user.cn},ou=kosmos.org,cn=users,dc=kosmos,dc=org")
allow_any_instance_of(User).to receive(:nostr_pubkey).and_return(nil)
end end
describe 'Adding a nostr pubkey' do describe 'Adding a nostr pubkey' do
scenario 'Without nostr browser extension available' do scenario 'Without nostr browser extension available' do
visit setting_path(:nostr) visit setting_path(:experiments)
expect(page).to have_content("No browser extension found") expect(page).to have_content("No browser extension found")
expect(page).to have_css('button[data-settings--nostr-pubkey-target=setPubkey]:disabled') expect(page).to have_css('button[data-settings--nostr-pubkey-target=setPubkey]:disabled')
end end
@@ -26,22 +22,19 @@ RSpec.describe 'Nostr Settings', type: :feature do
context "With pubkey configured" do context "With pubkey configured" do
before do before do
allow_any_instance_of(User).to receive(:nostr_pubkey) user.update! nostr_pubkey: "07e188a1ff87ce171d517b8ed2bb7a31b1d3453a0db3b15379ec07b724d232f3"
.and_return("ce273cbfb0d4e3e06930773a337c1459e4849efd4cb4c751b906a561c98a6d09")
end end
scenario 'Remove nostr pubkey from account' do scenario 'Remove nostr pubkey from account' do
visit setting_path(:nostr) visit setting_path(:experiments)
expect(page).to have_field("nostr_public_key", expect(page).to have_field("nostr_public_key",
with: "npub1ecnne0as6n37q6fswuarxlq5t8jgf8hafj6vw5deq6jkrjv2d5ysnehu73", with: "npub1qlsc3g0lsl8pw8230w8d9wm6xxcax3f6pkemz5measrmwfxjxteslf2hac",
disabled: true) disabled: true)
expect(LdapManager::UpdateNostrKey).to receive(:call).with(
dn: "cn=jimmy,ou=kosmos.org,cn=users,dc=kosmos,dc=org",
pubkey: nil
)
click_link "Remove" click_link "Remove"
expect(page).to_not have_field("nostr_public_key")
expect(page).to have_content("verify your public key")
expect(user.reload.nostr_pubkey).to be_nil
end end
end end
end end

View File

@@ -12,8 +12,6 @@ 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

@@ -66,7 +66,7 @@ RSpec.describe User, type: :model do
it "returns the entries from the LDAP service attribute" do it "returns the entries from the LDAP service attribute" do
expect(user).to receive(:ldap_entry).and_return({ expect(user).to receive(:ldap_entry).and_return({
uid: user.cn, ou: user.ou, mail: user.email, admin: nil, uid: user.cn, ou: user.ou, mail: user.email, admin: nil,
services_enabled: ["discourse", "email", "gitea", "wiki", "xmpp"] service: ["discourse", "email", "gitea", "wiki", "xmpp"]
}) })
expect(user.services_enabled).to eq(["discourse", "email", "gitea", "wiki", "xmpp"]) expect(user.services_enabled).to eq(["discourse", "email", "gitea", "wiki", "xmpp"])
end end
@@ -76,21 +76,21 @@ RSpec.describe User, type: :model do
before do before do
allow(user).to receive(:ldap_entry).and_return({ allow(user).to receive(:ldap_entry).and_return({
uid: user.cn, ou: user.ou, mail: user.email, admin: nil, uid: user.cn, ou: user.ou, mail: user.email, admin: nil,
services_enabled: ["discourse", "gitea"] service: ["discourse", "gitea"]
}) })
allow(user).to receive(:dn).and_return(dn) allow(user).to receive(:dn).and_return(dn)
end end
it "adds the service to the LDAP entry" do it "adds the service to the LDAP entry" do
expect_any_instance_of(LdapService).to receive(:replace_attribute) expect_any_instance_of(LdapService).to receive(:replace_attribute)
.with(dn, :serviceEnabled, ["discourse", "gitea", "wiki"]).and_return(true) .with(dn, :service, ["discourse", "gitea", "wiki"]).and_return(true)
user.enable_service(:wiki) user.enable_service(:wiki)
end end
it "adds multiple service to the LDAP entry" do it "adds multiple service to the LDAP entry" do
expect_any_instance_of(LdapService).to receive(:replace_attribute) expect_any_instance_of(LdapService).to receive(:replace_attribute)
.with(dn, :serviceEnabled, ["discourse", "gitea", "wiki", "xmpp"]).and_return(true) .with(dn, :service, ["discourse", "gitea", "wiki", "xmpp"]).and_return(true)
user.enable_service([:wiki, :xmpp]) user.enable_service([:wiki, :xmpp])
end end
@@ -100,21 +100,21 @@ RSpec.describe User, type: :model do
before do before do
allow(user).to receive(:ldap_entry).and_return({ allow(user).to receive(:ldap_entry).and_return({
uid: user.cn, ou: user.ou, mail: user.email, admin: nil, uid: user.cn, ou: user.ou, mail: user.email, admin: nil,
services_enabled: ["discourse", "gitea", "xmpp"] service: ["discourse", "gitea", "xmpp"]
}) })
allow(user).to receive(:dn).and_return(dn) allow(user).to receive(:dn).and_return(dn)
end end
it "removes the service from the LDAP entry" do it "removes the service from the LDAP entry" do
expect_any_instance_of(LdapService).to receive(:replace_attribute) expect_any_instance_of(LdapService).to receive(:replace_attribute)
.with(dn, :serviceEnabled, ["discourse", "gitea"]).and_return(true) .with(dn, :service, ["discourse", "gitea"]).and_return(true)
user.disable_service(:xmpp) user.disable_service(:xmpp)
end end
it "removes multiple services from the LDAP entry" do it "removes multiple services from the LDAP entry" do
expect_any_instance_of(LdapService).to receive(:replace_attribute) expect_any_instance_of(LdapService).to receive(:replace_attribute)
.with(dn, :serviceEnabled, ["discourse"]).and_return(true) .with(dn, :service, ["discourse"]).and_return(true)
user.disable_service([:xmpp, "gitea"]) user.disable_service([:xmpp, "gitea"])
end end
@@ -206,21 +206,9 @@ RSpec.describe User, type: :model do
end end
end end
describe "#nostr_pubkey" do
before do
allow_any_instance_of(User).to receive(:ldap_entry)
.and_return({ nostr_key: "07e188a1ff87ce171d517b8ed2bb7a31b1d3453a0db3b15379ec07b724d232f3" })
end
it "returns the raw pubkey from LDAP" do
expect(user.nostr_pubkey).to eq("07e188a1ff87ce171d517b8ed2bb7a31b1d3453a0db3b15379ec07b724d232f3")
end
end
describe "#nostr_pubkey_bech32" do describe "#nostr_pubkey_bech32" do
before do before do
allow_any_instance_of(User).to receive(:ldap_entry) user.update! nostr_pubkey: "07e188a1ff87ce171d517b8ed2bb7a31b1d3453a0db3b15379ec07b724d232f3"
.and_return({ nostr_key: "07e188a1ff87ce171d517b8ed2bb7a31b1d3453a0db3b15379ec07b724d232f3" })
end end
it "encodes the hexadecimal pubkey to bech32" do it "encodes the hexadecimal pubkey to bech32" do

View File

@@ -87,43 +87,6 @@ RSpec.describe "Donations", type: :request do
expect(response).to redirect_to("https://btcpay.example.com/i/Q9GBe143HJIkdpZeH4Ftx5") expect(response).to redirect_to("https://btcpay.example.com/i/Q9GBe143HJIkdpZeH4Ftx5")
end end
end end
describe "amount in sats" do
before do
expect(BtcpayManager::CreateInvoice).to receive(:call)
.with(amount: 0.0001, currency: "BTC", redirect_url: "http://www.example.com/contributions/donations/1/confirm_btcpay")
.and_return({
"id" => "Q9GBe143HJIkdpZeH4Ftx5",
"amount" => "0.0001",
"currency" => "BTC",
"checkoutLink" => "#{Setting.btcpay_api_url}/i/Q9GBe143HJIkdpZeH4Ftx5",
"expirationTime" => 1707908626,
"checkout" => { "redirectURL" => "http://www.example.com/contributions/donations/1/confirm_btcpay" }
})
post "/contributions/donations", params: {
donation_method: "btcpay", amount: "10000", currency: "sats",
public_name: "Garret Holmes"
}
end
it "creates a new donation record" do
expect(user.donations.count).to eq(1)
donation = user.donations.first
expect(donation.donation_method).to eq("btcpay")
expect(donation.payment_method).to be_nil
expect(donation.paid_at).to be_nil
expect(donation.public_name).to eq("Garret Holmes")
expect(donation.amount_sats).to eq(10000)
expect(donation.fiat_amount).to be_nil
expect(donation.fiat_currency).to be_nil
expect(donation.btcpay_invoice_id).to eq("Q9GBe143HJIkdpZeH4Ftx5")
end
it "redirects to the BTCPay checkout page" do
expect(response).to redirect_to("https://btcpay.example.com/i/Q9GBe143HJIkdpZeH4Ftx5")
end
end
end end
end end

View File

@@ -2,23 +2,14 @@ require 'rails_helper'
RSpec.describe "Settings", type: :request do RSpec.describe "Settings", type: :request do
let(:user) { create :user, cn: 'mark', ou: 'kosmos.org' } let(:user) { create :user, cn: 'mark', ou: 'kosmos.org' }
let(:other_user) { create :user, id: 2, cn: 'markymark', ou: 'kosmos.org', email: 'markymark@interscope.com' }
before do before do
login_as user, :scope => :user login_as user, :scope => :user
allow_any_instance_of(User).to receive(:dn)
.and_return("cn=#{user.cn},ou=kosmos.org,cn=users,dc=kosmos,dc=org")
allow_any_instance_of(User).to receive(:nostr_pubkey).and_return(nil)
allow(LdapManager::FetchUserByNostrKey).to receive(:call).with(
pubkey: "07e188a1ff87ce171d517b8ed2bb7a31b1d3453a0db3b15379ec07b724d232f3"
).and_return(nil)
end end
describe "GET /settings/nostr" do describe "GET /settings/experiments" do
it "works" do it "works" do
get setting_path(:nostr) get setting_path(:experiments)
expect(response).to have_http_status(200) expect(response).to have_http_status(200)
end end
end end
@@ -31,11 +22,6 @@ RSpec.describe "Settings", type: :request do
context "With valid data" do context "With valid data" do
before do before do
expect(LdapManager::UpdateNostrKey).to receive(:call).with(
dn: "cn=mark,ou=kosmos.org,cn=users,dc=kosmos,dc=org",
pubkey: "07e188a1ff87ce171d517b8ed2bb7a31b1d3453a0db3b15379ec07b724d232f3"
).and_return(0)
post set_nostr_pubkey_settings_path, params: { post set_nostr_pubkey_settings_path, params: {
signed_event: { signed_event: {
id: "84f266bbd784551aaa9e35cb0aceb4ee59182a1dab9ab279d9e40dd56ecbbdd3", id: "84f266bbd784551aaa9e35cb0aceb4ee59182a1dab9ab279d9e40dd56ecbbdd3",
@@ -55,46 +41,13 @@ RSpec.describe "Settings", type: :request do
expect(response).to have_http_status(200) expect(response).to have_http_status(200)
end end
it "informs the user about the success" do it "saves the pubkey" do
expect(flash[:success]).to eq("Public key verification successful") expect(user.nostr_pubkey).to eq("07e188a1ff87ce171d517b8ed2bb7a31b1d3453a0db3b15379ec07b724d232f3")
end
end
context "With key already in use by someone else" do
before do
expect(LdapManager::FetchUserByNostrKey).to receive(:call).with(
pubkey: "07e188a1ff87ce171d517b8ed2bb7a31b1d3453a0db3b15379ec07b724d232f3"
).and_return(other_user)
expect(LdapManager::UpdateNostrKey).not_to receive(:call)
post set_nostr_pubkey_settings_path, params: {
signed_event: {
id: "84f266bbd784551aaa9e35cb0aceb4ee59182a1dab9ab279d9e40dd56ecbbdd3",
pubkey: "07e188a1ff87ce171d517b8ed2bb7a31b1d3453a0db3b15379ec07b724d232f3",
created_at: 1678254161,
kind: 1,
content: "Connect my public key to mark@kosmos.org (confirmation rMjWEmvcvtTlQkMd)",
sig: "96796d420547d6e2c7be5de82a2ce7a48be99aac6415464a6081859ac1a9017305accc0228c630466a57d45ec1c3b456376eb538b76dfdaa2397e3258be02fdd"
}
}.to_json, headers: {
"CONTENT_TYPE" => "application/json",
"HTTP_ACCEPT" => "application/json"
}
end
it "returns a 422 status" do
expect(response).to have_http_status(422)
end
it "informs the user about the failure" do
expect(flash[:alert]).to eq("Public key already in use for a different account")
end end
end end
context "With wrong username" do context "With wrong username" do
before do before do
expect(LdapManager::UpdateNostrKey).not_to receive(:call)
post set_nostr_pubkey_settings_path, params: { post set_nostr_pubkey_settings_path, params: {
signed_event: { signed_event: {
id: "2e1e20ee762d6a5b5b30835eda9ca03146e4baf82490e53fd75794c08de08ac0", id: "2e1e20ee762d6a5b5b30835eda9ca03146e4baf82490e53fd75794c08de08ac0",
@@ -114,8 +67,8 @@ RSpec.describe "Settings", type: :request do
expect(response).to have_http_status(422) expect(response).to have_http_status(422)
end end
it "informs the user about the failure" do it "does not save the pubkey" do
expect(flash[:alert]).to eq("Public key could not be verified") expect(user.nostr_pubkey).to be_nil
end end
end end
@@ -124,8 +77,6 @@ RSpec.describe "Settings", type: :request do
session_stub = { shared_secret: "ho-chi-minh" } session_stub = { shared_secret: "ho-chi-minh" }
allow_any_instance_of(SettingsController).to receive(:session).and_return(session_stub) allow_any_instance_of(SettingsController).to receive(:session).and_return(session_stub)
expect(LdapManager::UpdateNostrKey).not_to receive(:call)
post set_nostr_pubkey_settings_path, params: { post set_nostr_pubkey_settings_path, params: {
signed_event: { signed_event: {
id: "84f266bbd784551aaa9e35cb0aceb4ee59182a1dab9ab279d9e40dd56ecbbdd3", id: "84f266bbd784551aaa9e35cb0aceb4ee59182a1dab9ab279d9e40dd56ecbbdd3",
@@ -145,8 +96,8 @@ RSpec.describe "Settings", type: :request do
expect(response).to have_http_status(422) expect(response).to have_http_status(422)
end end
it "informs the user about the failure" do it "does not save the pubkey" do
expect(flash[:alert]).to eq("Public key could not be verified") expect(user.nostr_pubkey).to be_nil
end end
end end
end end

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("#{Setting.rs_storage_url}/tony") expect(rs_link["href"]).to eql("https://storage.kosmos.org/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

@@ -19,11 +19,6 @@ RSpec.describe "Well-known URLs", type: :request do
context "user does not have a nostr pubkey configured" do context "user does not have a nostr pubkey configured" do
let(:user) { create :user, cn: 'spongebob', ou: 'kosmos.org' } let(:user) { create :user, cn: 'spongebob', ou: 'kosmos.org' }
before do
allow_any_instance_of(User).to receive(:ldap_entry)
.and_return({ nostr_key: nil })
end
it "returns a 404 status" do it "returns a 404 status" do
get "/.well-known/nostr.json?name=spongebob" get "/.well-known/nostr.json?name=spongebob"
expect(response).to have_http_status(:not_found) expect(response).to have_http_status(:not_found)
@@ -31,12 +26,8 @@ RSpec.describe "Well-known URLs", type: :request do
end end
context "user with nostr pubkey" do context "user with nostr pubkey" do
let(:user) { create :user, cn: 'bobdylan', ou: 'kosmos.org' } let(:user) { create :user, cn: 'bobdylan', ou: 'kosmos.org', nostr_pubkey: '438d35a6750d0dd6b75d032af8a768aad76b62f0c70ecb45f9c4d9e63540f7f4' }
before do before { user.save! }
user.save!
allow_any_instance_of(User).to receive(:nostr_pubkey)
.and_return('438d35a6750d0dd6b75d032af8a768aad76b62f0c70ecb45f9c4d9e63540f7f4')
end
it "returns a NIP-05 response" do it "returns a NIP-05 response" do
get "/.well-known/nostr.json?name=bobdylan" get "/.well-known/nostr.json?name=bobdylan"