1 Commits

Author SHA1 Message Date
14c5dd22d6 Add strfry doc draft
All checks were successful
continuous-integration/drone/push Build is passing
2024-08-19 14:37:54 +02:00
99 changed files with 641 additions and 1779 deletions

View File

@@ -1,11 +1,18 @@
# syntax=docker/dockerfile:1 # syntax=docker/dockerfile:1
FROM ruby:3.3.4 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

@@ -44,8 +44,6 @@ gem 'pagy', '~> 6.0', '>= 6.0.2'
gem 'flipper' gem 'flipper'
gem 'flipper-active_record' gem 'flipper-active_record'
gem 'flipper-ui' gem 'flipper-ui'
gem 'gpgme', '~> 2.0.24'
gem 'zbase32', '~> 0.1.1'
# HTTP requests # HTTP requests
gem 'faraday' gem 'faraday'

View File

@@ -197,8 +197,6 @@ GEM
raabro (~> 1.4) raabro (~> 1.4)
globalid (1.2.1) globalid (1.2.1)
activesupport (>= 6.1) activesupport (>= 6.1)
gpgme (2.0.24)
mini_portile2 (~> 2.7)
hashdiff (1.1.0) hashdiff (1.1.0)
i18n (1.14.1) i18n (1.14.1)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
@@ -485,7 +483,6 @@ GEM
xpath (3.2.0) xpath (3.2.0)
nokogiri (~> 1.8) nokogiri (~> 1.8)
yard (0.9.34) yard (0.9.34)
zbase32 (0.1.1)
zeitwerk (2.6.12) zeitwerk (2.6.12)
PLATFORMS PLATFORMS
@@ -510,7 +507,6 @@ DEPENDENCIES
flipper flipper
flipper-active_record flipper-active_record
flipper-ui flipper-ui
gpgme (~> 2.0.24)
image_processing (~> 1.12.2) image_processing (~> 1.12.2)
importmap-rails importmap-rails
jbuilder (~> 2.7) jbuilder (~> 2.7)
@@ -544,7 +540,6 @@ DEPENDENCIES
warden warden
web-console (~> 4.2) web-console (~> 4.2)
webmock webmock
zbase32 (~> 0.1.1)
BUNDLED WITH BUNDLED WITH
2.5.5 2.5.5

View File

@@ -9,12 +9,4 @@ class Admin::Settings::RegistrationsController < Admin::SettingsController
success: "Settings saved" success: "Settings saved"
} }
end end
private
def setting_params
params.require(:setting).permit([
:reserved_usernames, default_services: []
])
end
end end

View File

@@ -9,12 +9,11 @@ class Admin::SettingsController < Admin::BaseController
changed_keys = [] changed_keys = []
setting_params.keys.each do |key| setting_params.keys.each do |key|
next if clean_param(key).nil? || next if setting_params[key].nil? ||
(Setting.send(key).to_s == clean_param(key)) (Setting.send(key).to_s == setting_params[key].strip)
changed_keys.push(key) changed_keys.push(key)
setting = Setting.new(var: key) setting = Setting.new(var: key)
setting.value = clean_param(key) setting.value = setting_params[key].strip
unless setting.valid? unless setting.valid?
@errors.merge!(setting.errors) @errors.merge!(setting.errors)
end end
@@ -25,7 +24,7 @@ class Admin::SettingsController < Admin::BaseController
end end
changed_keys.each do |key| changed_keys.each do |key|
Setting.send("#{key}=", clean_param(key)) Setting.send("#{key}=", setting_params[key].strip)
end end
end end
@@ -38,12 +37,4 @@ class Admin::SettingsController < Admin::BaseController
def setting_params def setting_params
params.require(:setting).permit(Setting.editable_keys.map(&:to_sym)) params.require(:setting).permit(Setting.editable_keys.map(&:to_sym))
end end
def clean_param(key)
if Setting.get_field(key)[:type] == :string
setting_params[key].strip
else
setting_params[key]
end
end
end end

View File

@@ -30,7 +30,7 @@ class Admin::UsersController < Admin::BaseController
amount = params[:amount].to_i amount = params[:amount].to_i
notify_user = ActiveRecord::Type::Boolean.new.cast(params[:notify_user]) notify_user = ActiveRecord::Type::Boolean.new.cast(params[:notify_user])
UserManager::CreateInvitations.call(user: @user, amount: amount, notify: notify_user) CreateInvitations.call(user: @user, amount: amount, notify: notify_user)
redirect_to admin_user_path(@user.cn), flash: { redirect_to admin_user_path(@user.cn), flash: {
success: "Added #{amount} invitations to #{@user.cn}'s account" success: "Added #{amount} invitations to #{@user.cn}'s account"

View File

@@ -1,7 +1,7 @@
class LnurlpayController < ApplicationController class LnurlpayController < ApplicationController
before_action :check_service_available before_action :check_service_available
before_action :find_user before_action :find_user
before_action :set_cors_access_control_headers before_action :set_cors_access_control_headers, only: [:invoice]
MIN_SATS = 10 MIN_SATS = 10
MAX_SATS = 1_000_000 MAX_SATS = 1_000_000

View File

@@ -3,7 +3,7 @@ class Services::ChatController < Services::BaseController
before_action :require_service_available before_action :require_service_available
def show def show
@service_enabled = current_user.service_enabled?(:ejabberd) @service_enabled = current_user.service_enabled?(:xmpp)
end end
private private

View File

@@ -21,12 +21,10 @@ class SettingsController < ApplicationController
end end
end end
# PUT /settings/:section
def update def update
@user.preferences.merge!(user_params[:preferences] || {}) @user.preferences.merge!(user_params[:preferences] || {})
@user.display_name = user_params[:display_name] @user.display_name = user_params[:display_name]
@user.avatar_new = user_params[:avatar] @user.avatar_new = user_params[:avatar]
@user.pgp_pubkey = user_params[:pgp_pubkey]
if @user.save if @user.save
if @user.display_name && (@user.display_name != @user.ldap_entry[:display_name]) if @user.display_name && (@user.display_name != @user.ldap_entry[:display_name])
@@ -37,10 +35,6 @@ class SettingsController < ApplicationController
LdapManager::UpdateAvatar.call(dn: @user.dn, file: @user.avatar_new) LdapManager::UpdateAvatar.call(dn: @user.dn, file: @user.avatar_new)
end end
if @user.pgp_pubkey && (@user.pgp_pubkey != @user.ldap_entry[:pgp_key])
UserManager::UpdatePgpKey.call(user: @user)
end
redirect_to setting_path(@settings_section), flash: { redirect_to setting_path(@settings_section), flash: {
success: 'Settings saved.' success: 'Settings saved.'
} }
@@ -50,7 +44,6 @@ class SettingsController < ApplicationController
end end
end end
# POST /settings/update_email
def update_email def update_email
if @user.valid_ldap_authentication?(security_params[:current_password]) if @user.valid_ldap_authentication?(security_params[:current_password])
if @user.update email: email_params[:email] if @user.update email: email_params[:email]
@@ -68,7 +61,6 @@ class SettingsController < ApplicationController
end end
end end
# POST /settings/reset_email_password
def reset_email_password def reset_email_password
@user.current_password = security_params[:current_password] @user.current_password = security_params[:current_password]
@@ -91,7 +83,6 @@ class SettingsController < ApplicationController
end end
end end
# POST /settings/reset_password
def reset_password def reset_password
current_user.send_reset_password_instructions current_user.send_reset_password_instructions
sign_out current_user sign_out current_user
@@ -99,7 +90,6 @@ class SettingsController < ApplicationController
redirect_to check_your_email_path, notice: msg redirect_to check_your_email_path, notice: msg
end end
# POST /settings/set_nostr_pubkey
def set_nostr_pubkey def set_nostr_pubkey
signed_event = Nostr::Event.new(**nostr_event_from_params) signed_event = Nostr::Event.new(**nostr_event_from_params)
@@ -162,8 +152,7 @@ class SettingsController < ApplicationController
def user_params def user_params
params.require(:user).permit( params.require(:user).permit(
:display_name, :avatar, :pgp_pubkey, :display_name, :avatar, preferences: UserPreferences.pref_keys
preferences: UserPreferences.pref_keys
) )
end end

View File

@@ -96,7 +96,7 @@ class SignupController < ApplicationController
session[:new_user] = nil session[:new_user] = nil
session[:validation_error] = nil session[:validation_error] = nil
UserManager::CreateAccount.call(account: { CreateAccount.call(account: {
username: @user.cn, username: @user.cn,
domain: Setting.primary_domain, domain: Setting.primary_domain,
email: @user.email, email: @user.email,

View File

@@ -1,35 +0,0 @@
class WebKeyDirectoryController < WellKnownController
before_action :allow_cross_origin_requests
# /.well-known/openpgpkey/hu/:hashed_username(.txt)
def show
@user = User.find_by(cn: params[:l].downcase)
if @user.nil? ||
@user.pgp_pubkey.blank? ||
!@user.pgp_pubkey_contains_user_address?
http_status :not_found and return
end
if params[:hashed_username] != @user.wkd_hash
http_status :unprocessable_entity and return
end
respond_to do |format|
format.text do
response.headers['Content-Type'] = 'text/plain'
render plain: @user.pgp_pubkey
end
format.any do
key = @user.gnupg_key.export
send_data key, filename: "#{@user.wkd_hash}.pem",
type: "application/octet-stream"
end
end
end
def policy
head :ok
end
end

View File

@@ -1,6 +1,8 @@
class WebfingerController < WellKnownController class WebfingerController < ApplicationController
before_action :allow_cross_origin_requests, only: [:show] before_action :allow_cross_origin_requests, only: [:show]
layout false
def show def show
resource = params[:resource] resource = params[:resource]
@@ -74,7 +76,7 @@ class WebfingerController < WellKnownController
end end
def remotestorage_link def remotestorage_link
auth_url = new_rs_oauth_url(@username, host: Setting.accounts_domain) auth_url = new_rs_oauth_url(@username)
storage_url = "#{Setting.rs_storage_url}/#{@username}" storage_url = "#{Setting.rs_storage_url}/#{@username}"
{ {
@@ -89,4 +91,10 @@ class WebfingerController < WellKnownController
} }
} }
end end
def allow_cross_origin_requests
return unless Rails.env.development?
headers['Access-Control-Allow-Origin'] = "*"
headers['Access-Control-Allow-Methods'] = "GET"
end
end end

View File

@@ -1,8 +1,5 @@
class WellKnownController < ApplicationController class WellKnownController < ApplicationController
before_action :require_nostr_enabled, only: [ :nostr ] before_action :require_nostr_enabled, only: [ :nostr ]
before_action :allow_cross_origin_requests, only: [ :nostr ]
layout false
def nostr def nostr
http_status :unprocessable_entity and return if params[:name].blank? http_status :unprocessable_entity and return if params[:name].blank?
@@ -10,14 +7,8 @@ class WellKnownController < ApplicationController
relay_url = Setting.nostr_relay_url.presence relay_url = Setting.nostr_relay_url.presence
if params[:name] == "_" if params[:name] == "_"
if domain == Setting.primary_domain # pubkey for the primary domain without a username (e.g. kosmos.org)
# pubkey for the primary domain without a username (e.g. kosmos.org) res = { names: { "_": Setting.nostr_public_key } }
res = { names: { "_": Setting.nostr_public_key_primary_domain.presence || Setting.nostr_public_key } }
else
# pubkey for the akkounts domain without a username (e.g. accounts.kosmos.org)
res = { names: { "_": Setting.nostr_public_key } }
end
res[:relays] = { "_" => [ relay_url ] } if relay_url res[:relays] = { "_" => [ relay_url ] } if relay_url
else else
@user = User.where(cn: params[:name], ou: domain).first @user = User.where(cn: params[:name], ou: domain).first
@@ -39,9 +30,4 @@ class WellKnownController < ApplicationController
def require_nostr_enabled def require_nostr_enabled
http_status :not_found unless Setting.nostr_enabled? http_status :not_found unless Setting.nostr_enabled?
end end
def allow_cross_origin_requests
headers['Access-Control-Allow-Origin'] = "*"
headers['Access-Control-Allow-Methods'] = "GET"
end
end end

View File

@@ -0,0 +1,2 @@
module DashboardHelper
end

View File

@@ -0,0 +1,2 @@
module DonationsHelper
end

View File

@@ -0,0 +1,2 @@
module InvitationsHelper
end

View File

@@ -0,0 +1,2 @@
module LnurlpayHelper
end

View File

@@ -1,12 +0,0 @@
module ServicesHelper
def service_human_name(key, category = :external)
SERVICES[category][key][:name] || key.to_s
end
def service_display_name(key, category = :external)
SERVICES[category][key][:display_name] ||
service_human_name(key, category)
end
end

View File

@@ -0,0 +1,2 @@
module SettingsHelper
end

View File

@@ -0,0 +1,2 @@
module SignupHelper
end

View File

@@ -0,0 +1,2 @@
module UsersHelper
end

View File

@@ -0,0 +1,2 @@
module WalletHelper
end

View File

@@ -0,0 +1,2 @@
module WelcomeHelper
end

View File

@@ -2,8 +2,8 @@ class XmppExchangeContactsJob < ApplicationJob
queue_as :default queue_as :default
def perform(inviter, invitee) def perform(inviter, invitee)
return unless inviter.service_enabled?(:ejabberd) && return unless inviter.service_enabled?(:xmpp) &&
invitee.service_enabled?(:ejabberd) && invitee.service_enabled?(:xmpp) &&
inviter.preferences[:xmpp_exchange_contacts_with_invitees] inviter.preferences[:xmpp_exchange_contacts_with_invitees]
ejabberd = EjabberdApiClient.new ejabberd = EjabberdApiClient.new

View File

@@ -1,90 +1,3 @@
class ApplicationMailer < ActionMailer::Base class ApplicationMailer < ActionMailer::Base
default Rails.application.config.action_mailer.default_options
layout 'mailer' layout 'mailer'
private
def send_mail
@template ||= "#{self.class.name.underscore}/#{caller[0][/`([^']*)'/, 1]}"
headers['Message-ID'] = message_id
if @user.pgp_pubkey.present?
mail(to: @user.email, subject: "...", content_type: pgp_content_type) do |format|
format.text { render plain: pgp_content }
end
else
mail(to: @user.email, subject: @subject) do |format|
format.text { render @template }
end
end
end
def from_address
self.class.default[:from]
end
def from_domain
Mail::Address.new(from_address).domain
end
def message_id
@message_id ||= "#{SecureRandom.uuid}@#{from_domain}"
end
def boundary
@boundary ||= SecureRandom.hex(8)
end
def pgp_content_type
"multipart/encrypted; protocol=\"application/pgp-encrypted\"; boundary=\"------------#{boundary}\""
end
def pgp_nested_content
message_content = render_to_string(template: @template)
message_content_base64 = Base64.encode64(message_content)
nested_boundary = SecureRandom.hex(8)
<<~NESTED_CONTENT
Content-Type: multipart/mixed; boundary="------------#{nested_boundary}"; protected-headers="v1"
Subject: #{@subject}
From: <#{from_address}>
To: #{@user.display_name || @user.cn} <#{@user.email}>
Message-ID: <#{message_id}>
--------------#{nested_boundary}
Content-Type: text/plain; charset=UTF-8; format=flowed
Content-Transfer-Encoding: base64
#{message_content_base64}
--------------#{nested_boundary}--
NESTED_CONTENT
end
def pgp_content
encrypted_content = UserManager::PgpEncrypt.call(user: @user, text: pgp_nested_content)
encrypted_base64 = Base64.encode64(encrypted_content.to_s)
<<~EMAIL_CONTENT
This is an OpenPGP/MIME encrypted message (RFC 4880 and 3156)
--------------#{boundary}
Content-Type: application/pgp-encrypted
Content-Description: PGP/MIME version identification
Version: 1
--------------#{boundary}
Content-Type: application/octet-stream; name="encrypted.asc"
Content-Description: OpenPGP encrypted message
Content-Disposition: inline; filename="encrypted.asc"
-----BEGIN PGP MESSAGE-----
#{encrypted_base64}
-----END PGP MESSAGE-----
--------------#{boundary}--
EMAIL_CONTENT
end
end end

View File

@@ -18,6 +18,6 @@ class CustomMailer < ApplicationMailer
@user = params[:user] @user = params[:user]
@subject = params[:subject] @subject = params[:subject]
@body = params[:body] @body = params[:body]
send_mail mail(to: @user.email, subject: @subject)
end end
end end

View File

@@ -3,7 +3,7 @@ class NotificationMailer < ApplicationMailer
@user = params[:user] @user = params[:user]
@amount_sats = params[:amount_sats] @amount_sats = params[:amount_sats]
@subject = "Sats received" @subject = "Sats received"
send_mail mail to: @user.email, subject: @subject
end end
def remotestorage_auth_created def remotestorage_auth_created
@@ -15,19 +15,19 @@ class NotificationMailer < ApplicationMailer
"#{access} #{directory}" "#{access} #{directory}"
end end
@subject = "New app connected to your storage" @subject = "New app connected to your storage"
send_mail mail to: @user.email, subject: @subject
end end
def new_invitations_available def new_invitations_available
@user = params[:user] @user = params[:user]
@subject = "New invitations added to your account" @subject = "New invitations added to your account"
send_mail mail to: @user.email, subject: @subject
end end
def bitcoin_donation_confirmed def bitcoin_donation_confirmed
@user = params[:user] @user = params[:user]
@donation = params[:donation] @donation = params[:donation]
@subject = "Donation confirmed" @subject = "Donation confirmed"
send_mail mail to: @user.email, subject: @subject
end end
end end

View File

@@ -1,24 +0,0 @@
module Settings
module BtcpaySettings
extend ActiveSupport::Concern
included do
field :btcpay_api_url, type: :string,
default: ENV["BTCPAY_API_URL"].presence
field :btcpay_enabled, type: :boolean,
default: ENV["BTCPAY_API_URL"].present?
field :btcpay_public_url, type: :string,
default: ENV["BTCPAY_PUBLIC_URL"].presence
field :btcpay_store_id, type: :string,
default: ENV["BTCPAY_STORE_ID"].presence
field :btcpay_auth_token, type: :string,
default: ENV["BTCPAY_AUTH_TOKEN"].presence
field :btcpay_publish_wallet_balances, type: :boolean, default: true
end
end
end

View File

@@ -1,16 +0,0 @@
module Settings
module DiscourseSettings
extend ActiveSupport::Concern
included do
field :discourse_public_url, type: :string,
default: ENV["DISCOURSE_PUBLIC_URL"].presence
field :discourse_enabled, type: :boolean,
default: ENV["DISCOURSE_PUBLIC_URL"].present?
field :discourse_connect_secret, type: :string,
default: ENV["DISCOURSE_CONNECT_SECRET"].presence
end
end
end

View File

@@ -1,13 +0,0 @@
module Settings
module DroneCiSettings
extend ActiveSupport::Concern
included do
field :droneci_public_url, type: :string,
default: ENV["DRONECI_PUBLIC_URL"].presence
field :droneci_enabled, type: :boolean,
default: ENV["DRONECI_PUBLIC_URL"].present?
end
end
end

View File

@@ -1,19 +0,0 @@
module Settings
module EjabberdSettings
extend ActiveSupport::Concern
included do
field :ejabberd_enabled, type: :boolean,
default: ENV["EJABBERD_API_URL"].present?
field :ejabberd_api_url, type: :string,
default: ENV["EJABBERD_API_URL"].presence
field :ejabberd_admin_url, type: :string,
default: ENV["EJABBERD_ADMIN_URL"].presence
field :ejabberd_buddy_roster, type: :string,
default: "Buddies"
end
end
end

View File

@@ -1,28 +0,0 @@
module Settings
module EmailSettings
extend ActiveSupport::Concern
included do
field :email_enabled, type: :boolean,
default: ENV["EMAIL_SMTP_HOST"].present?
# field :email_smtp_host, type: :string,
# default: ENV["EMAIL_SMTP_HOST"].presence
#
# field :email_smtp_port, type: :string,
# default: ENV["EMAIL_SMTP_PORT"].presence || 587
#
# field :email_smtp_enable_starttls, type: :string,
# default: ENV["EMAIL_SMTP_PORT"].presence || true
#
# field :email_auth_method, type: :string,
# default: ENV["EMAIL_AUTH_METHOD"].presence || "plain"
#
# field :email_imap_host, type: :string,
# default: ENV["EMAIL_IMAP_HOST"].presence
#
# field :email_imap_port, type: :string,
# default: ENV["EMAIL_IMAP_PORT"].presence || 993
end
end
end

View File

@@ -1,34 +0,0 @@
module Settings
module GeneralSettings
extend ActiveSupport::Concern
included do
field :primary_domain, type: :string,
default: ENV["PRIMARY_DOMAIN"].presence
field :accounts_domain, type: :string,
default: ENV["AKKOUNTS_DOMAIN"].presence
#
# Internal services
#
field :redis_url, type: :string,
default: ENV["REDIS_URL"] || "redis://localhost:6379/0"
field :s3_enabled, type: :boolean,
default: ENV["S3_ENABLED"] && ENV["S3_ENABLED"].to_s != "false"
field :sentry_enabled, type: :boolean, readonly: true,
default: ENV["SENTRY_DSN"].present?
#
# Registrations
#
field :reserved_usernames, type: :array, default: %w[
account accounts donations mail webmaster support
]
end
end
end

View File

@@ -1,13 +0,0 @@
module Settings
module GiteaSettings
extend ActiveSupport::Concern
included do
field :gitea_public_url, type: :string,
default: ENV["GITEA_PUBLIC_URL"].presence
field :gitea_enabled, type: :boolean,
default: ENV["GITEA_PUBLIC_URL"].present?
end
end
end

View File

@@ -1,25 +0,0 @@
module Settings
module LightningNetworkSettings
extend ActiveSupport::Concern
included do
field :lndhub_api_url, type: :string,
default: ENV["LNDHUB_API_URL"].presence
field :lndhub_enabled, type: :boolean,
default: ENV["LNDHUB_API_URL"].present?
field :lndhub_admin_token, type: :string,
default: ENV["LNDHUB_ADMIN_TOKEN"].presence
field :lndhub_admin_enabled, type: :boolean,
default: ENV["LNDHUB_ADMIN_UI"] || false
field :lndhub_public_key, type: :string,
default: (ENV["LNDHUB_PUBLIC_KEY"] || "")
field :lndhub_keysend_enabled, type: :boolean,
default: -> { self.lndhub_public_key.present? }
end
end
end

View File

@@ -1,16 +0,0 @@
module Settings
module MastodonSettings
extend ActiveSupport::Concern
included do
field :mastodon_public_url, type: :string,
default: ENV["MASTODON_PUBLIC_URL"].presence
field :mastodon_enabled, type: :boolean,
default: ENV["MASTODON_PUBLIC_URL"].present?
field :mastodon_address_domain, type: :string,
default: ENV["MASTODON_ADDRESS_DOMAIN"].presence || self.primary_domain
end
end
end

View File

@@ -1,13 +0,0 @@
module Settings
module MediaWikiSettings
extend ActiveSupport::Concern
included do
field :mediawiki_public_url, type: :string,
default: ENV["MEDIAWIKI_PUBLIC_URL"].presence
field :mediawiki_enabled, type: :boolean,
default: ENV["MEDIAWIKI_PUBLIC_URL"].present?
end
end
end

View File

@@ -1,25 +0,0 @@
module Settings
module NostrSettings
extend ActiveSupport::Concern
included do
field :nostr_enabled, type: :boolean,
default: ENV["NOSTR_PRIVATE_KEY"].present?
field :nostr_private_key, type: :string,
default: ENV["NOSTR_PRIVATE_KEY"].presence
field :nostr_public_key, type: :string,
default: ENV["NOSTR_PUBLIC_KEY"].presence
field :nostr_public_key_primary_domain, type: :string,
default: ENV["NOSTR_PUBLIC_KEY_PRIMARY_DOMAIN"].presence
field :nostr_relay_url, type: :string,
default: ENV["NOSTR_RELAY_URL"].presence
field :nostr_zaps_relay_limit, type: :integer,
default: 12
end
end
end

View File

@@ -1,9 +0,0 @@
module Settings
module OpenCollectiveSettings
extend ActiveSupport::Concern
included do
field :opencollective_enabled, type: :boolean, default: true
end
end
end

View File

@@ -1,16 +0,0 @@
module Settings
module RemoteStorageSettings
extend ActiveSupport::Concern
included do
field :remotestorage_enabled, type: :boolean,
default: ENV["RS_STORAGE_URL"].present?
field :rs_storage_url, type: :string,
default: ENV["RS_STORAGE_URL"].presence
field :rs_redis_url, type: :string,
default: ENV["RS_REDIS_URL"] || "redis://localhost:6379/1"
end
end
end

View File

@@ -1,11 +0,0 @@
module Settings
module XmppSettings
extend ActiveSupport::Concern
included do
field :xmpp_default_rooms, type: :array, default: []
field :xmpp_autojoin_default_rooms, type: :boolean, default: false
field :xmpp_notifications_from_address, type: :string, default: primary_domain
end
end
end

View File

@@ -2,30 +2,226 @@
class Setting < RailsSettings::Base class Setting < RailsSettings::Base
cache_prefix { "v1" } cache_prefix { "v1" }
Dir[Rails.root.join('app', 'models', 'concerns', 'settings', '*.rb')].each do |file| field :primary_domain, type: :string,
require file default: ENV["PRIMARY_DOMAIN"].presence
field :accounts_domain, type: :string,
default: ENV["AKKOUNTS_DOMAIN"].presence
#
# Internal services
#
field :redis_url, type: :string,
default: ENV["REDIS_URL"] || "redis://localhost:6379/0"
field :s3_enabled, type: :boolean,
default: ENV["S3_ENABLED"] && ENV["S3_ENABLED"].to_s != "false"
#
# Registrations
#
field :reserved_usernames, type: :array, default: %w[
account accounts donations mail webmaster support
]
#
# XMPP
#
field :xmpp_default_rooms, type: :array, default: []
field :xmpp_autojoin_default_rooms, type: :boolean, default: false
field :xmpp_notifications_from_address, type: :string, default: primary_domain
#
# Sentry
#
field :sentry_enabled, type: :boolean, readonly: true,
default: ENV["SENTRY_DSN"].present?
#
# BTCPay Server
#
field :btcpay_api_url, type: :string,
default: ENV["BTCPAY_API_URL"].presence
field :btcpay_enabled, type: :boolean,
default: ENV["BTCPAY_API_URL"].present?
field :btcpay_public_url, type: :string,
default: ENV["BTCPAY_PUBLIC_URL"].presence
field :btcpay_store_id, type: :string,
default: ENV["BTCPAY_STORE_ID"].presence
field :btcpay_auth_token, type: :string,
default: ENV["BTCPAY_AUTH_TOKEN"].presence
field :btcpay_publish_wallet_balances, type: :boolean, default: true
#
# Discourse
#
field :discourse_public_url, type: :string,
default: ENV["DISCOURSE_PUBLIC_URL"].presence
field :discourse_enabled, type: :boolean,
default: ENV["DISCOURSE_PUBLIC_URL"].present?
field :discourse_connect_secret, type: :string,
default: ENV["DISCOURSE_CONNECT_SECRET"].presence
#
# Drone CI
#
field :droneci_public_url, type: :string,
default: ENV["DRONECI_PUBLIC_URL"].presence
field :droneci_enabled, type: :boolean,
default: ENV["DRONECI_PUBLIC_URL"].present?
#
# ejabberd
#
field :ejabberd_enabled, type: :boolean,
default: ENV["EJABBERD_API_URL"].present?
field :ejabberd_api_url, type: :string,
default: ENV["EJABBERD_API_URL"].presence
field :ejabberd_admin_url, type: :string,
default: ENV["EJABBERD_ADMIN_URL"].presence
field :ejabberd_buddy_roster, type: :string,
default: "Buddies"
#
# Gitea
#
field :gitea_public_url, type: :string,
default: ENV["GITEA_PUBLIC_URL"].presence
field :gitea_enabled, type: :boolean,
default: ENV["GITEA_PUBLIC_URL"].present?
#
# Lightning Network
#
field :lndhub_api_url, type: :string,
default: ENV["LNDHUB_API_URL"].presence
field :lndhub_enabled, type: :boolean,
default: ENV["LNDHUB_API_URL"].present?
field :lndhub_admin_token, type: :string,
default: ENV["LNDHUB_ADMIN_TOKEN"].presence
field :lndhub_admin_enabled, type: :boolean,
default: ENV["LNDHUB_ADMIN_UI"] || false
field :lndhub_public_key, type: :string,
default: (ENV["LNDHUB_PUBLIC_KEY"] || "")
field :lndhub_keysend_enabled, type: :boolean,
default: -> { self.lndhub_public_key.present? }
#
# Mastodon
#
field :mastodon_public_url, type: :string,
default: ENV["MASTODON_PUBLIC_URL"].presence
field :mastodon_enabled, type: :boolean,
default: ENV["MASTODON_PUBLIC_URL"].present?
field :mastodon_address_domain, type: :string,
default: ENV["MASTODON_ADDRESS_DOMAIN"].presence || self.primary_domain
#
# MediaWiki
#
field :mediawiki_public_url, type: :string,
default: ENV["MEDIAWIKI_PUBLIC_URL"].presence
field :mediawiki_enabled, type: :boolean,
default: ENV["MEDIAWIKI_PUBLIC_URL"].present?
#
# Nostr
#
field :nostr_enabled, type: :boolean,
default: ENV["NOSTR_PRIVATE_KEY"].present?
field :nostr_private_key, type: :string,
default: ENV["NOSTR_PRIVATE_KEY"].presence
field :nostr_public_key, type: :string,
default: ENV["NOSTR_PUBLIC_KEY"].presence
field :nostr_relay_url, type: :string,
default: ENV["NOSTR_RELAY_URL"].presence
field :nostr_zaps_relay_limit, type: :integer,
default: 12
#
# OpenCollective
#
field :opencollective_enabled, type: :boolean, default: true
#
# RemoteStorage
#
field :remotestorage_enabled, type: :boolean,
default: ENV["RS_STORAGE_URL"].present?
field :rs_storage_url, type: :string,
default: ENV["RS_STORAGE_URL"].presence
field :rs_redis_url, type: :string,
default: ENV["RS_REDIS_URL"] || "redis://localhost:6379/1"
#
# E-Mail Service
#
field :email_enabled, type: :boolean,
default: ENV["EMAIL_SMTP_HOST"].present?
# field :email_smtp_host, type: :string,
# default: ENV["EMAIL_SMTP_HOST"].presence
#
# field :email_smtp_port, type: :string,
# default: ENV["EMAIL_SMTP_PORT"].presence || 587
#
# field :email_smtp_enable_starttls, type: :string,
# default: ENV["EMAIL_SMTP_PORT"].presence || true
#
# field :email_auth_method, type: :string,
# default: ENV["EMAIL_AUTH_METHOD"].presence || "plain"
#
# field :email_imap_host, type: :string,
# default: ENV["EMAIL_IMAP_HOST"].presence
#
# field :email_imap_port, type: :string,
# default: ENV["EMAIL_IMAP_PORT"].presence || 993
def self.default_services
# TODO Make configurable from respective service settings page
%w[ discourse gitea mastodon mediawiki xmpp ]
end end
include Settings::GeneralSettings
include Settings::BtcpaySettings
include Settings::DiscourseSettings
include Settings::DroneCiSettings
include Settings::EjabberdSettings
include Settings::EmailSettings
include Settings::GiteaSettings
include Settings::LightningNetworkSettings
include Settings::MastodonSettings
include Settings::MediaWikiSettings
include Settings::NostrSettings
include Settings::OpenCollectiveSettings
include Settings::RemoteStorageSettings
include Settings::XmppSettings
def self.available_services
known_services = SERVICES[:external].keys
known_services.select {|s| Setting.send "#{s}_enabled?" }
end
field :default_services, type: :array,
default: self.available_services
end end

View File

@@ -3,10 +3,9 @@ require 'nostr'
class User < ApplicationRecord class User < ApplicationRecord
include EmailValidatable include EmailValidatable
attr_accessor :current_password
attr_accessor :avatar_new
attr_accessor :display_name attr_accessor :display_name
attr_accessor :pgp_pubkey attr_accessor :avatar_new
attr_accessor :current_password
serialize :preferences, coder: UserPreferences serialize :preferences, coder: UserPreferences
@@ -52,8 +51,6 @@ class User < ApplicationRecord
validate :acceptable_avatar validate :acceptable_avatar
validate :acceptable_pgp_key_format, if: -> { defined?(@pgp_pubkey) && @pgp_pubkey.present? }
# #
# Scopes # Scopes
# #
@@ -168,24 +165,6 @@ class User < ApplicationRecord
Nostr::PublicKey.new(nostr_pubkey).to_bech32 Nostr::PublicKey.new(nostr_pubkey).to_bech32
end end
def pgp_pubkey
@pgp_pubkey ||= ldap_entry[:pgp_key]
end
def gnupg_key
return nil unless pgp_pubkey.present?
GPGME::Key.import(pgp_pubkey)
GPGME::Key.get(pgp_fpr)
end
def pgp_pubkey_contains_user_address?
gnupg_key.uids.map(&:email).include?(address)
end
def wkd_hash
ZBase32.encode(Digest::SHA1.digest(cn))
end
def avatar def avatar
@avatar_base64 ||= LdapManager::FetchAvatar.call(cn: cn) @avatar_base64 ||= LdapManager::FetchAvatar.call(cn: cn)
end end
@@ -201,14 +180,14 @@ class User < ApplicationRecord
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.sort services = (current_services + new_services).uniq
ldap.replace_attribute(dn, :serviceEnabled, services) ldap.replace_attribute(dn, :serviceEnabled, 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.sort services = (current_services - disabled_services).uniq
ldap.replace_attribute(dn, :serviceEnabled, services) ldap.replace_attribute(dn, :serviceEnabled, services)
end end
@@ -235,10 +214,4 @@ class User < ApplicationRecord
errors.add(:avatar, "must be a JPEG or PNG file") errors.add(:avatar, "must be a JPEG or PNG file")
end end
end end
def acceptable_pgp_key_format
unless GPGME::Key.valid?(pgp_pubkey)
errors.add(:pgp_pubkey, 'is not a valid armored PGP public key block')
end
end
end end

View File

@@ -0,0 +1,54 @@
class CreateAccount < ApplicationService
def initialize(account:)
@username = account[:username]
@domain = account[:ou] || Setting.primary_domain
@email = account[:email]
@password = account[:password]
@invitation = account[:invitation]
@confirmed = account[:confirmed]
end
def call
user = create_user_in_database
add_ldap_document
create_lndhub_account(user) if Setting.lndhub_enabled
if @invitation.present?
update_invitation(user.id)
end
end
private
def create_user_in_database
User.create!(
cn: @username,
ou: @domain,
email: @email,
password: @password,
password_confirmation: @password,
confirmed_at: @confirmed ? DateTime.now : nil
)
end
def update_invitation(user_id)
@invitation.update! invited_user_id: user_id, used_at: DateTime.now
end
def add_ldap_document
hashed_pw = Devise.ldap_auth_password_builder.call(@password)
CreateLdapUserJob.perform_later(
username: @username,
domain: @domain,
email: @email,
hashed_pw: hashed_pw,
confirmed: @confirmed
)
end
def create_lndhub_account(user)
#TODO enable in development when we have a local lndhub (mock?) API
return if Rails.env.development?
CreateLndhubAccountJob.perform_later(user)
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,16 +0,0 @@
module LdapManager
class UpdatePgpKey < LdapManagerService
def initialize(dn:, pubkey:)
@dn = dn
@pubkey = pubkey
end
def call
if @pubkey.present?
replace_attribute @dn, :pgpKey, @pubkey
else
delete_attribute @dn, :pgpKey
end
end
end
end

View File

@@ -58,7 +58,7 @@ class LdapService < ApplicationService
attributes = %w[ attributes = %w[
dn cn uid mail displayName admin serviceEnabled dn cn uid mail displayName admin serviceEnabled
mailRoutingAddress mailpassword nostrKey pgpKey mailRoutingAddress mailpassword nostrKey
] ]
filter = Net::LDAP::Filter.eq("uid", args[:uid] || "*") filter = Net::LDAP::Filter.eq("uid", args[:uid] || "*")
@@ -73,8 +73,7 @@ class LdapService < ApplicationService
services_enabled: e.try(:serviceEnabled), services_enabled: e.try(:serviceEnabled),
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, nostr_key: e.try(:nostrKey) ? e.nostrKey.first : nil
pgp_key: e.try(:pgpKey) ? e.pgpKey.first : nil
} }
end end
end end
@@ -102,7 +101,7 @@ class LdapService < ApplicationService
dn = "ou=#{ou},cn=users,#{ldap_suffix}" dn = "ou=#{ou},cn=users,#{ldap_suffix}"
aci = <<-EOS aci = <<-EOS
(target="ldap:///cn=*,ou=#{ou},cn=users,#{ldap_suffix}")(targetattr="cn || sn || uid || userPassword || mail || mailRoutingAddress || serviceEnabled || nostrKey || pgpKey || 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,#{ldap_suffix}")(targetattr="cn || sn || uid || userPassword || mail || mailRoutingAddress || serviceEnabled || nostrKey || nsRole || objectClass") (version 3.0; acl "service-#{ou.gsub(".", "-")}-read-search"; allow (read,search) userdn="ldap:///uid=service,ou=#{ou},cn=applications,#{ldap_suffix}";)
EOS EOS
attrs = { attrs = {

View File

@@ -1,56 +0,0 @@
module UserManager
class CreateAccount < UserManagerService
def initialize(account:)
@username = account[:username]
@domain = account[:ou] || Setting.primary_domain
@email = account[:email]
@password = account[:password]
@invitation = account[:invitation]
@confirmed = account[:confirmed]
end
def call
user = create_user_in_database
add_ldap_document
create_lndhub_account(user) if Setting.lndhub_enabled
if @invitation.present?
update_invitation(user.id)
end
end
private
def create_user_in_database
User.create!(
cn: @username,
ou: @domain,
email: @email,
password: @password,
password_confirmation: @password,
confirmed_at: @confirmed ? DateTime.now : nil
)
end
def update_invitation(user_id)
@invitation.update! invited_user_id: user_id, used_at: DateTime.now
end
def add_ldap_document
hashed_pw = Devise.ldap_auth_password_builder.call(@password)
CreateLdapUserJob.perform_later(
username: @username,
domain: @domain,
email: @email,
hashed_pw: hashed_pw,
confirmed: @confirmed
)
end
def create_lndhub_account(user)
#TODO enable in development when we have a local lndhub (mock?) API
return if Rails.env.development?
CreateLndhubAccountJob.perform_later(user)
end
end
end

View File

@@ -1,19 +0,0 @@
module UserManager
class CreateInvitations < UserManagerService
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
end

View File

@@ -1,19 +0,0 @@
require 'gpgme'
module UserManager
class PgpEncrypt < UserManagerService
def initialize(user:, text:)
@user = user
@text = text
end
def call
crypto = GPGME::Crypto.new
crypto.encrypt(
@text,
recipients: @user.gnupg_key,
always_trust: true
)
end
end
end

View File

@@ -1,24 +0,0 @@
module UserManager
class UpdatePgpKey < UserManagerService
def initialize(user:)
@user = user
end
def call
if @user.pgp_pubkey.blank?
@user.update! pgp_fpr: nil
else
result = GPGME::Key.import(@user.pgp_pubkey)
if result.imports.present?
@user.update! pgp_fpr: result.imports.first.fpr
else
# TODO notify Sentry, user
raise "Failed to import OpenPGP pubkey"
end
end
LdapManager::UpdatePgpKey.call(dn: @user.dn, pubkey: @user.pgp_pubkey)
end
end
end

View File

@@ -1,2 +0,0 @@
class UserManagerService < ApplicationService
end

View File

@@ -9,36 +9,18 @@
<%= render partial: "admin/settings/errors", locals: { errors: @errors } %> <%= render partial: "admin/settings/errors", locals: { errors: @errors } %>
<% end %> <% end %>
<ul role="list"> <label class="block">
<%= render FormElements::FieldsetComponent.new( <p class="font-bold mb-1">Reserved usernames</p>
title: "Reserved usernames", <p class="text-gray-500">
description: "These usernames cannot be registered as accounts." These usernames cannot be registered as accounts:
) do %> </p>
<%= f.text_area :reserved_usernames, <%= f.text_area :reserved_usernames,
value: Setting.reserved_usernames.join("\n"), value: Setting.reserved_usernames.join("\n"),
class: "h-44 w-60" %> class: "h-44 mb-2" %>
<p class="text-sm text-gray-500"> <p class="text-sm text-gray-500">
One username per line One username per line
</p> </p>
<% end %> </label>
<li>
<p class="font-bold mb-1">Default services</p>
<p class="text-gray-500">
These services are enabled for new users by default after signup.
</p>
<div class="flex flex-wrap gap-x-6 gap-y-2">
<% Setting.available_services.each do |option| %>
<div class="md:inline-block">
<%= f.check_box :default_services,
{ multiple: true, checked: Setting.default_services.include?(option),
class: "h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-600 mr-0.5" },
option, nil %>
<%= f.label "default_services_#{option.parameterize}", service_human_name(option) %>
</div>
<% end %>
</div>
</li>
</ul>
</section> </section>
<section> <section>

View File

@@ -19,11 +19,6 @@
title: "Public key", title: "Public key",
description: "The corresponding public key of the accounts service" description: "The corresponding public key of the accounts service"
) %> ) %>
<%= render FormElements::FieldsetResettableSettingComponent.new(
key: :nostr_public_key_primary_domain,
title: "Public key for primary domain (NIP-05)",
description: "(optional) A different pubkey to announce for the _@#{Setting.primary_domain} Nostr address"
) %>
<%= render FormElements::FieldsetResettableSettingComponent.new( <%= render FormElements::FieldsetResettableSettingComponent.new(
key: :nostr_relay_url, key: :nostr_relay_url,
title: "Relay URL", title: "Relay URL",

View File

@@ -1,4 +1,5 @@
<h3>RemoteStorage</h3> <h3>RemoteStorage</h3>
<p class="text-red-600 mb-8">Feature currently in development.</p>
<ul role="list"> <ul role="list">
<%= render FormElements::FieldsetToggleComponent.new( <%= render FormElements::FieldsetToggleComponent.new(
form: f, form: f,

View File

@@ -89,47 +89,13 @@
</section> </section>
<section class="sm:flex-1 sm:pt-0"> <section class="sm:flex-1 sm:pt-0">
<h3>LDAP</h3> <% if @avatar.present? %>
<table class="divided"> <h3>LDAP<h3>
<tbody> <p>
<tr> <img src="data:image/jpeg;base64,<%= @avatar %>" class="h-48 w-48" />
<th>Avatar</th> </p>
<td> <% end %>
<% if @avatar.present? %> <!-- <h3>Actions</h3> -->
<img src="data:image/jpeg;base64,<%= @avatar %>" class="h-48 w-48" />
<% else %>
&mdash;
<% end %>
</td>
</tr>
<tr>
<th>Display name</th>
<td><%= @user.display_name || "—" %></td>
</tr>
<tr>
<th class="align-top">PGP key</th>
<td class="align-top leading-5">
<% if @user.pgp_pubkey.present? %>
<span class="font-mono" title="<%= @user.pgp_fpr %>">
<% if @user.pgp_pubkey_contains_user_address? %>
<%= link_to wkd_key_url(hashed_username: @user.wkd_hash, l: @user.cn, format: :txt),
class: "ks-text-link", target: "_blank" do %>
<%= "#{@user.pgp_fpr[0, 8]}…#{@user.pgp_fpr[-8..-1]}" %>
<% end %>
<% else %>
<%= "#{@user.pgp_fpr[0, 8]}…#{@user.pgp_fpr[-8..-1]}" %>
<% end %>
</span><br />
<% @user.gnupg_key.uids.each do |uid| %>
<%= uid.uid %><br />
<% end %>
<% else %>
&mdash;
<% end %>
</td>
</tr>
</tbody>
</table>
</section> </section>
</div> </div>
@@ -218,7 +184,7 @@
<td>XMPP (ejabberd)</td> <td>XMPP (ejabberd)</td>
<td> <td>
<%= render FormElements::ToggleComponent.new( <%= render FormElements::ToggleComponent.new(
enabled: @services_enabled.include?("ejabberd"), enabled: @services_enabled.include?("xmpp"),
input_enabled: false input_enabled: false
) %> ) %>
</td> </td>
@@ -239,9 +205,7 @@
) %> ) %>
</td> </td>
<td class="text-right"> <td class="text-right">
<% if @user.nostr_pubkey.present? %>
<%= link_to "Open profile", "https://njump.me/#{@user.nostr_pubkey_bech32}", class: "btn-sm btn-gray" %> <%= link_to "Open profile", "https://njump.me/#{@user.nostr_pubkey_bech32}", class: "btn-sm btn-gray" %>
<% end %>
</td> </td>
</tr> </tr>
<% end %> <% end %>

View File

@@ -14,8 +14,7 @@
<p class="mb-6"> <p class="mb-6">
In order to connect an app to your storage account, give it your address: In order to connect an app to your storage account, give it your address:
</p> </p>
<p data-controller="clipboard" class="flex items-center gap-1 sm:w-2/5"> <p data-controller="clipboard" class="flex gap-1 sm:w-2/5">
<img src="/img/logos/icon_remotestorage.svg" class="inline-block h-6 w-6 mr-1">
<input type="text" id="user_address" class="grow" <input type="text" id="user_address" class="grow"
value=<%= current_user.address %> disabled="disabled" value=<%= current_user.address %> disabled="disabled"
data-clipboard-target="source" /> data-clipboard-target="source" />
@@ -32,24 +31,6 @@
</p> </p>
</section> </section>
<section>
<h3>Compatible Apps</h3>
<p>
Your Storage account is based on a new open standard called
<a href="https://remotestorage.io" target="_blank">
<img src="/img/logos/icon_remotestorage.svg" class="h-4 w-4 inline">
<strong>remoteStorage</strong>
</a>, which is not yet widely supported. Look
for the remoteStorage icon, or check the Sync settings in apps.
</p>
<p>
If you want your favorite apps to support syncing data with your own
Storage account, let the developers know! All relevant information is
available on the <a href="https://remotestorage.io"
target="_blank" class="ks-text-link">remoteStorage website</a>.
</p>
</section>
<section> <section>
<h3>Recommended Apps</h3> <h3>Recommended Apps</h3>
<div data-controller="tabs" <div data-controller="tabs"

View File

@@ -1,6 +1,6 @@
<%= tag.section data: { <%= tag.section data: {
controller: "settings--account--email", controller: "settings--account--email",
"settings--account--email-validation-failed-value": @validation_errors&.[](:email)&.present? "settings--account--email-validation-failed-value": @validation_errors.present?
} do %> } do %>
<h3>E-Mail</h3> <h3>E-Mail</h3>
<%= form_for(@user, url: update_email_settings_path, method: "post") do |f| %> <%= form_for(@user, url: update_email_settings_path, method: "post") do |f| %>
@@ -23,7 +23,7 @@
</span> </span>
</button> </button>
</p> </p>
<% if @validation_errors&.[](:email)&.present? %> <% if @validation_errors.present? && @validation_errors[:email].present? %>
<p class="error-msg"><%= @validation_errors[:email].first %></p> <p class="error-msg"><%= @validation_errors[:email].first %></p>
<% end %> <% end %>
<div class="initial-hidden"> <div class="initial-hidden">
@@ -41,33 +41,10 @@
<% end %> <% end %>
<section> <section>
<h3>Password</h3> <h3>Password</h3>
<p class="mb-6">Use the following button to request an email with a password reset link:</p> <p class="mb-8">Use the following button to request an email with a password reset link:</p>
<%= form_with(url: reset_password_settings_path, method: :post) do %> <%= form_with(url: reset_password_settings_path, method: :post) do %>
<p> <p>
<%= submit_tag("Send me a password reset link", class: 'btn-md btn-gray w-full sm:w-auto') %> <%= submit_tag("Send me a password reset link", class: 'btn-md btn-gray w-full sm:w-auto') %>
</p> </p>
<% end %> <% end %>
</section> </section>
<%= form_for(@user, url: setting_path(:account), html: { :method => :put }) do |f| %>
<section class="!pt-8 sm:!pt-12">
<h3>OpenPGP</h3>
<ul role="list">
<%= render FormElements::FieldsetComponent.new(
title: "Public key",
description: "Your OpenPGP public key in ASCII Armor format"
) do %>
<%= f.text_area :pgp_pubkey,
value: @user.pgp_pubkey,
class: "h-24 w-full" %>
<% if @validation_errors&.[](:pgp_pubkey)&.present? %>
<p class="error-msg">This <%= @validation_errors[:pgp_pubkey].first %></p>
<% end %>
<% end %>
</ul>
</section>
<section>
<p class="pt-6 border-t border-gray-200 text-right">
<%= f.submit 'Save', class: "btn-md btn-blue w-full md:w-auto" %>
</p>
</section>
<% end %>

View File

@@ -5,7 +5,7 @@
<h3>E-Mail Password</h3> <h3>E-Mail Password</h3>
<%= form_for(@user, url: reset_email_password_settings_path, method: "post") do |f| %> <%= form_for(@user, url: reset_email_password_settings_path, method: "post") do |f| %>
<%= hidden_field_tag :section, "email" %> <%= hidden_field_tag :section, "email" %>
<p class="mb-6"> <p class="mb-8">
Use the following button to generate a new email password: Use the following button to generate a new email password:
</p> </p>
<p class="hidden initial-visible"> <p class="hidden initial-visible">

View File

@@ -57,22 +57,16 @@ Rails.application.configure do
# routes, locales, etc. This feature depends on the listen gem. # routes, locales, etc. This feature depends on the listen gem.
config.file_watcher = ActiveSupport::EventedFileUpdateChecker config.file_watcher = ActiveSupport::EventedFileUpdateChecker
config.action_mailer.default_options = {
from: "accounts@localhost"
}
# Don't actually send emails, cache them for viewing via letter opener # Don't actually send emails, cache them for viewing via letter opener
config.action_mailer.delivery_method = :letter_opener config.action_mailer.delivery_method = :letter_opener
# Don't care if the mailer can't send # Don't care if the mailer can't send
config.action_mailer.raise_delivery_errors = true config.action_mailer.raise_delivery_errors = true
# Base URL to be used by email template link helpers # Base URL to be used by email template link helpers
config.action_mailer.default_url_options = { config.action_mailer.default_url_options = { host: "localhost:3000", protocol: "http" }
host: "localhost:3000",
protocol: "http"
}
config.action_mailer.default_options = {
from: "accounts@localhost",
message_id: -> { "<#{Mail.random_tag}@localhost>" },
}
# 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'

View File

@@ -63,7 +63,7 @@ Rails.application.configure do
outgoing_email_domain = Mail::Address.new(outgoing_email_address).domain outgoing_email_domain = Mail::Address.new(outgoing_email_address).domain
config.action_mailer.default_url_options = { config.action_mailer.default_url_options = {
host: ENV.fetch('AKKOUNTS_DOMAIN'), host: ENV['AKKOUNTS_DOMAIN'],
protocol: "https", protocol: "https",
} }

View File

@@ -46,12 +46,8 @@ Rails.application.configure do
config.action_mailer.default_url_options = { config.action_mailer.default_url_options = {
host: "accounts.kosmos.org", host: "accounts.kosmos.org",
protocol: "https" protocol: "https",
} from: "accounts@kosmos.org"
config.action_mailer.default_options = {
from: "accounts@kosmos.org",
message_id: -> { "<#{Mail.random_tag}@kosmos.org>" },
} }
config.active_job.queue_adapter = :test config.active_job.queue_adapter = :test

View File

@@ -1,2 +0,0 @@
config_path = Rails.root.join('config', 'services.yml')
SERVICES = YAML.load_file(config_path).deep_symbolize_keys.with_indifferent_access

View File

@@ -70,12 +70,10 @@ Rails.application.routes.draw do
get '.well-known/webfinger', to: 'webfinger#show' get '.well-known/webfinger', to: 'webfinger#show'
get '.well-known/nostr', to: 'well_known#nostr' get '.well-known/nostr', to: 'well_known#nostr'
get '.well-known/lnurlp/:username', to: 'lnurlpay#index', as: :lightning_address get '.well-known/lnurlp/:username', to: 'lnurlpay#index', as: 'lightning_address'
get '.well-known/keysend/:username', to: 'lnurlpay#keysend', as: :lightning_address_keysend get '.well-known/keysend/:username', to: 'lnurlpay#keysend', as: 'lightning_address_keysend'
get '.well-known/openpgpkey/hu/:hashed_username(.:format)', to: 'web_key_directory#show', as: :wkd_key
get '.well-known/openpgpkey/policy', to: 'web_key_directory#policy'
get 'lnurlpay/:username/invoice', to: 'lnurlpay#invoice', as: :lnurlpay_invoice get 'lnurlpay/:username/invoice', to: 'lnurlpay#invoice', as: 'lnurlpay_invoice'
post 'webhooks/lndhub', to: 'webhooks#lndhub' post 'webhooks/lndhub', to: 'webhooks#lndhub'

View File

@@ -1,30 +0,0 @@
internal:
btcpay:
name: BTCPay Server
postgres:
name: PostgreSQL
sentry:
name: Sentry
external:
discourse:
name: Discourse
droneci:
name: Drone CI
ejabberd:
display_name: Chat
email:
name: E-Mail
gitea:
name: Gitea
lndhub:
name: LNDHub
display_name: Lightning Network
mastodon:
name: Mastodon
mediawiki:
name: MediaWiki
nostr:
name: Nostr
remotestorage:
name: remoteStorage
display_name: Storage

View File

@@ -1,6 +1,4 @@
:concurrency: 2 :concurrency: 2
production:
:concurrency: 10
:queues: :queues:
- default - default
- mailers - mailers

View File

@@ -1,5 +0,0 @@
class AddPgpFprToUsers < ActiveRecord::Migration[7.1]
def change
add_column :users, :pgp_fpr, :string
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_09_22_205634) do ActiveRecord::Schema[7.1].define(version: 2024_06_07_123654) 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
@@ -132,7 +132,6 @@ ActiveRecord::Schema[7.1].define(version: 2024_09_22_205634) do
t.datetime "remember_created_at" t.datetime "remember_created_at"
t.string "remember_token" t.string "remember_token"
t.text "preferences" t.text "preferences"
t.string "pgp_fpr"
t.index ["email"], name: "index_users_on_email", unique: true t.index ["email"], name: "index_users_on_email", unique: true
t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
end end

View File

@@ -7,7 +7,7 @@ Sidekiq::Testing.inline! do
puts "Create user: admin" puts "Create user: admin"
UserManager::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
}) })
@@ -20,7 +20,7 @@ Sidekiq::Testing.inline! do
email = Faker::Internet.unique.email email = Faker::Internet.unique.email
next if username.length < 3 next if username.length < 3
UserManager::CreateAccount.call(account: { 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
}) })

View File

@@ -1,13 +0,0 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
mDMEZvGiUxYJKwYBBAHaRw8BAQdARPZXLqyB3nylJuzuARlOJxqc9mchMKHI4Cy+
hPWlzja0GEFkbWluIDxhZG1pbkBrb3Ntb3Mub3JnPoiZBBMWCgBBFiEE0pie1+fG
ImdZwzGnwgEYSg8AulYFAmbxolMCGwMFCQWjmoAFCwkIBwICIgIGFQoJCAsCBBYC
AwECHgcCF4AACgkQwgEYSg8AulaldAEA7yzh7XRCdIJDHgLUvKHsy2NnyLaDD1Tl
hyZWbl5og0IBAJAQ2Dm82YXMdUK3X1OGlK8KH5O4E5lSFY4+8/xx0UEJuDgEZvGi
UxIKKwYBBAGXVQEFAQEHQJc8pzzeIF7Hm5z1eseRAqGvFa+V1BIDf+1XQzuJhhxi
AwEIB4h+BBgWCgAmFiEE0pie1+fGImdZwzGnwgEYSg8AulYFAmbxolMCGwwFCQWj
moAACgkQwgEYSg8AulbLtgEApZvuDqSP77lrl1jmtCAJEEZk/ofsRFkf1g3U3Zhm
9PcA/1+AbcyqjLTcqIPjHmZyGEPiaAvEsBzbPKEPiL3JYhkG
=45sx
-----END PGP PUBLIC KEY BLOCK-----

View File

@@ -0,0 +1,29 @@
# strfry (nostr relay)
## LDAP policy
...
## Useful scripts
### Syncing events for all local nostr users from a remote relay
You can sync all events of all local users with a pubkey stored in LDAP from a
specified remote relay to the local relay with the `strfry-sync.ts` script:
deno run -A /opt/strfry-sync.ts wss://relay.example.com
Doing the same with Docker Compose (great for seeding data to your local relay
in development):
docker compose run strfry deno run -A /opt/strfry-sync.ts wss://relay.example.com
## Docker image
In order to use the LDAP policy with Docker, you will need
[Deno](https://deno.com/) installed in your strfry container. We provide a
custom Docker image for strfry with Deno included (which we use in
development):
* Registry: https://gitea.kosmos.org/kosmos/-/packages/container/strfry-deno/1.1.1
* Source: https://github.com/raucao/strfry/blob/docker_deno/ubuntu.Dockerfile

View File

@@ -111,7 +111,7 @@ services:
- redis - redis
strfry: strfry:
image: gitea.kosmos.org/kosmos/strfry-deno:2.0.0 image: gitea.kosmos.org/kosmos/strfry-deno:1.1.1
volumes: volumes:
- ./docker/strfry/strfry.conf:/etc/strfry.conf - ./docker/strfry/strfry.conf:/etc/strfry.conf
- ./extras/strfry:/opt/strfry - ./extras/strfry:/opt/strfry

View File

@@ -1,57 +0,0 @@
# Nostr
## strfry
The `extras/strfry` directory contains code to integrate [strfry][1] with
akkounts, so that notes published to the relay have to be authored by (or in
some cases just related to) local users who have verified their Nostr public
key.
### Requirements
[Deno](https://deno.com/) needs to be installed on the machine that you run
strfry on.
We provide a Docker image with recent strfry and Deno builds:
https://gitea.kosmos.org/kosmos/-/packages/container/strfry-deno/
### Configuration
You can use either environment variables (see e.g. the `strfry` service in
`docker-compose-yml`) or a local `.env` file in the same working directory
that you place the extra files in (e.g. `/opt/strfry`).
In your `strfry.conf`, configure `strfry-policy.ts` as the write policy, like so:
```
writePolicy {
plugin = "/opt/strfry/strfry-policy.ts"
}
```
All dependencies will be downloaded and cached automatically when the plugin is
called for the first time.
### Manual tasks
You can sync all notes authored by local users (any account that has verified
their Nostr pubkey with akkounts) from a remote [strfry][1] relay via negentropy
sync:
deno run -A /opt/strfry/strfry-sync.ts wss://nostr.kosmos.org
Or, in the running container when using Docker Compose:
docker compose exec strfry deno run -A /opt/strfry/strfry-sync.ts wss://nostr.kosmos.org
The `strfry` service container also exposes the local relay on your local host
on port 4777.
[nak](https://github.com/fiatjaf/nak) is a helpful tool for manual Nostr tasks.
Here's how you can grab a note by its event ID from a remote relay and publish
it to your local strfry for example:
nak req -i 0fb010192685b86b0810b3de3706fbbf3b8c1db30b14533094a2b9700c820cdc nostr.kosmos.org | nak event ws://localhost:4777
[1]: https://github.com/hoytech/strfry

320
extras/strfry/deno.lock generated
View File

@@ -1,231 +1,101 @@
{ {
"version": "4", "version": "3",
"specifiers": { "packages": {
"jsr:@nostr/tools@*": "2.3.1", "specifiers": {
"jsr:@nostr/tools@^2.3.1": "2.3.1", "jsr:@nostr/tools@^2.3.1": "jsr:@nostr/tools@2.3.1",
"jsr:@nostrify/nostrify@0.36": "0.36.2", "npm:@noble/ciphers@^0.5.1": "npm:@noble/ciphers@0.5.3",
"jsr:@nostrify/policies@*": "0.36.1", "npm:@noble/curves@1.2.0": "npm:@noble/curves@1.2.0",
"jsr:@nostrify/strfry@*": "0.2.1", "npm:@noble/hashes@1.3.1": "npm:@noble/hashes@1.3.1",
"jsr:@nostrify/types@0.35": "0.35.0", "npm:@scure/base@1.1.1": "npm:@scure/base@1.1.1",
"jsr:@nostrify/types@0.36": "0.36.0", "npm:ldapts": "npm:ldapts@7.0.12"
"jsr:@std/bytes@^1.0.5": "1.0.5",
"jsr:@std/encoding@~0.224.1": "0.224.3",
"jsr:@std/json@^1.0.1": "1.0.1",
"jsr:@std/streams@^1.0.7": "1.0.9",
"jsr:@std/streams@^1.0.8": "1.0.9",
"npm:@noble/ciphers@~0.5.1": "0.5.3",
"npm:@noble/curves@1.2.0": "1.2.0",
"npm:@noble/hashes@1.3.1": "1.3.1",
"npm:@scure/base@1.1.1": "1.1.1",
"npm:@scure/bip32@^1.4.0": "1.6.2",
"npm:@scure/bip39@^1.3.0": "1.5.4",
"npm:ldapts@*": "7.0.12",
"npm:lru-cache@^10.2.0": "10.4.3",
"npm:nostr-tools@^2.7.0": "2.12.0",
"npm:websocket-ts@^2.1.5": "2.2.1",
"npm:zod@^3.23.8": "3.24.2"
},
"jsr": {
"@nostr/tools@2.3.1": {
"integrity": "af01dc45cb28784c584d7a0699707196f397bcc53946efa582a01b11ddde4d61",
"dependencies": [
"npm:@noble/ciphers",
"npm:@noble/curves",
"npm:@noble/hashes",
"npm:@scure/base"
]
}, },
"@nostrify/nostrify@0.36.2": { "jsr": {
"integrity": "cc4787ca170b623a2e5dfed1baa4426077daa6143af728ea7dd325d58f4d04d6", "@nostr/tools@2.3.1": {
"dependencies": [ "integrity": "af01dc45cb28784c584d7a0699707196f397bcc53946efa582a01b11ddde4d61",
"jsr:@nostrify/types@0.35", "dependencies": [
"jsr:@std/encoding", "npm:@noble/ciphers@^0.5.1",
"npm:@scure/bip32", "npm:@noble/curves@1.2.0",
"npm:@scure/bip39", "npm:@noble/hashes@1.3.1",
"npm:lru-cache", "npm:@scure/base@1.1.1"
"npm:nostr-tools", ]
"npm:websocket-ts", }
"npm:zod"
]
}, },
"@nostrify/policies@0.36.1": { "npm": {
"integrity": "6d59af115a687fcd18b6caebab0e4f50ee6cdb0aafa2aacd0aec2065021275b4", "@noble/ciphers@0.5.3": {
"dependencies": [ "integrity": "sha512-B0+6IIHiqEs3BPMT0hcRmHvEj2QHOLu+uwt+tqDDeVd0oyVzh7BPrDcPjRnV1PV/5LaknXJJQvOuRGR0zQJz+w==",
"jsr:@nostrify/nostrify", "dependencies": {}
"jsr:@nostrify/types@0.35", },
"npm:nostr-tools" "@noble/curves@1.2.0": {
] "integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==",
}, "dependencies": {
"@nostrify/strfry@0.2.1": { "@noble/hashes": "@noble/hashes@1.3.2"
"integrity": "be437b13f49e6564e557da23072bf642723a603568f672543a64d9fda6663432", }
"dependencies": [ },
"jsr:@nostrify/types@0.36", "@noble/hashes@1.3.1": {
"jsr:@std/json", "integrity": "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==",
"jsr:@std/streams@^1.0.8" "dependencies": {}
] },
}, "@noble/hashes@1.3.2": {
"@nostrify/types@0.35.0": { "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==",
"integrity": "b8d515563d467072694557d5626fa1600f74e83197eef45dd86a9a99c64f7fe6" "dependencies": {}
}, },
"@nostrify/types@0.36.0": { "@scure/base@1.1.1": {
"integrity": "b3413467debcbd298d217483df4e2aae6c335a34765c90ac7811cf7c637600e7" "integrity": "sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA==",
}, "dependencies": {}
"@std/bytes@1.0.5": { },
"integrity": "4465dd739d7963d964c809202ebea6d5c6b8e3829ef25c6a224290fbb8a1021e" "@types/asn1@0.2.4": {
}, "integrity": "sha512-V91DSJ2l0h0gRhVP4oBfBzRBN9lAbPUkGDMCnwedqPKX2d84aAMc9CulOvxdw1f7DfEYx99afab+Rsm3e52jhA==",
"@std/encoding@0.224.3": { "dependencies": {
"integrity": "5e861b6d81be5359fad4155e591acf17c0207b595112d1840998bb9f476dbdaf" "@types/node": "@types/node@18.16.19"
}, }
"@std/json@1.0.1": { },
"integrity": "1f0f70737e8827f9acca086282e903677bc1bb0c8ffcd1f21bca60039563049f", "@types/node@18.16.19": {
"dependencies": [ "integrity": "sha512-IXl7o+R9iti9eBW4Wg2hx1xQDig183jj7YLn8F7udNceyfkbn1ZxmzZXuak20gR40D7pIkIY1kYGx5VIGbaHKA==",
"jsr:@std/streams@^1.0.7" "dependencies": {}
] },
}, "@types/uuid@9.0.8": {
"@std/streams@1.0.9": { "integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==",
"integrity": "a9d26b1988cdd7aa7b1f4b51e1c36c1557f3f252880fa6cc5b9f37078b1a5035", "dependencies": {}
"dependencies": [ },
"jsr:@std/bytes" "asn1@0.2.6": {
] "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==",
} "dependencies": {
}, "safer-buffer": "safer-buffer@2.1.2"
"npm": { }
"@noble/ciphers@0.5.3": { },
"integrity": "sha512-B0+6IIHiqEs3BPMT0hcRmHvEj2QHOLu+uwt+tqDDeVd0oyVzh7BPrDcPjRnV1PV/5LaknXJJQvOuRGR0zQJz+w==" "debug@4.3.5": {
}, "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==",
"@noble/curves@1.1.0": { "dependencies": {
"integrity": "sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA==", "ms": "ms@2.1.2"
"dependencies": [ }
"@noble/hashes@1.3.1" },
] "ldapts@7.0.12": {
}, "integrity": "sha512-orwgIejUi/ZyGah9y8jWZmFUg8Ci5M8WAv0oZjSf3MVuk1sRBdor9Qy1ttGHbYpWj96HXKFunQ8AYZ8WWGp17g==",
"@noble/curves@1.2.0": { "dependencies": {
"integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==", "@types/asn1": "@types/asn1@0.2.4",
"dependencies": [ "@types/uuid": "@types/uuid@9.0.8",
"@noble/hashes@1.3.2" "asn1": "asn1@0.2.6",
] "debug": "debug@4.3.5",
}, "strict-event-emitter-types": "strict-event-emitter-types@2.0.0",
"@noble/curves@1.8.2": { "uuid": "uuid@9.0.1"
"integrity": "sha512-vnI7V6lFNe0tLAuJMu+2sX+FcL14TaCWy1qiczg1VwRmPrpQCdq5ESXQMqUc2tluRNf6irBXrWbl1mGN8uaU/g==", }
"dependencies": [ },
"@noble/hashes@1.7.2" "ms@2.1.2": {
] "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
}, "dependencies": {}
"@noble/hashes@1.3.1": { },
"integrity": "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==" "safer-buffer@2.1.2": {
}, "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"@noble/hashes@1.3.2": { "dependencies": {}
"integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==" },
}, "strict-event-emitter-types@2.0.0": {
"@noble/hashes@1.7.2": { "integrity": "sha512-Nk/brWYpD85WlOgzw5h173aci0Teyv8YdIAEtV+N88nDB0dLlazZyJMIsN6eo1/AR61l+p6CJTG1JIyFaoNEEA==",
"integrity": "sha512-biZ0NUSxyjLLqo6KxEJ1b+C2NAx0wtDoFvCaXHGgUkeHzf3Xc1xKumFKREuT7f7DARNZ/slvYUwFG6B0f2b6hQ==" "dependencies": {}
}, },
"@scure/base@1.1.1": { "uuid@9.0.1": {
"integrity": "sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA==" "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
}, "dependencies": {}
"@scure/base@1.2.4": { }
"integrity": "sha512-5Yy9czTO47mqz+/J8GM6GIId4umdCk1wc1q8rKERQulIoc8VP9pzDcghv10Tl2E7R96ZUx/PhND3ESYUQX8NuQ=="
},
"@scure/bip32@1.3.1": {
"integrity": "sha512-osvveYtyzdEVbt3OfwwXFr4P2iVBL5u1Q3q4ONBfDY/UpOuXmOlbgwc1xECEboY8wIays8Yt6onaWMUdUbfl0A==",
"dependencies": [
"@noble/curves@1.1.0",
"@noble/hashes@1.3.2",
"@scure/base@1.1.1"
]
},
"@scure/bip32@1.6.2": {
"integrity": "sha512-t96EPDMbtGgtb7onKKqxRLfE5g05k7uHnHRM2xdE6BP/ZmxaLtPek4J4KfVn/90IQNrU1IOAqMgiDtUdtbe3nw==",
"dependencies": [
"@noble/curves@1.8.2",
"@noble/hashes@1.7.2",
"@scure/base@1.2.4"
]
},
"@scure/bip39@1.2.1": {
"integrity": "sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg==",
"dependencies": [
"@noble/hashes@1.3.2",
"@scure/base@1.1.1"
]
},
"@scure/bip39@1.5.4": {
"integrity": "sha512-TFM4ni0vKvCfBpohoh+/lY05i9gRbSwXWngAsF4CABQxoaOHijxuaZ2R6cStDQ5CHtHO9aGJTr4ksVJASRRyMA==",
"dependencies": [
"@noble/hashes@1.7.2",
"@scure/base@1.2.4"
]
},
"@types/asn1@0.2.4": {
"integrity": "sha512-V91DSJ2l0h0gRhVP4oBfBzRBN9lAbPUkGDMCnwedqPKX2d84aAMc9CulOvxdw1f7DfEYx99afab+Rsm3e52jhA==",
"dependencies": [
"@types/node"
]
},
"@types/node@18.16.19": {
"integrity": "sha512-IXl7o+R9iti9eBW4Wg2hx1xQDig183jj7YLn8F7udNceyfkbn1ZxmzZXuak20gR40D7pIkIY1kYGx5VIGbaHKA=="
},
"@types/uuid@9.0.8": {
"integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA=="
},
"asn1@0.2.6": {
"integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==",
"dependencies": [
"safer-buffer"
]
},
"debug@4.3.5": {
"integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==",
"dependencies": [
"ms"
]
},
"ldapts@7.0.12": {
"integrity": "sha512-orwgIejUi/ZyGah9y8jWZmFUg8Ci5M8WAv0oZjSf3MVuk1sRBdor9Qy1ttGHbYpWj96HXKFunQ8AYZ8WWGp17g==",
"dependencies": [
"@types/asn1",
"@types/uuid",
"asn1",
"debug",
"strict-event-emitter-types",
"uuid"
]
},
"lru-cache@10.4.3": {
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="
},
"ms@2.1.2": {
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"nostr-tools@2.12.0": {
"integrity": "sha512-pUWEb020gTvt1XZvTa8AKNIHWFapjsv2NKyk43Ez2nnvz6WSXsrTFE0XtkNLSRBjPn6EpxumKeNiVzLz74jNSA==",
"dependencies": [
"@noble/ciphers",
"@noble/curves@1.2.0",
"@noble/hashes@1.3.1",
"@scure/base@1.1.1",
"@scure/bip32@1.3.1",
"@scure/bip39@1.2.1",
"nostr-wasm"
]
},
"nostr-wasm@0.1.0": {
"integrity": "sha512-78BTryCLcLYv96ONU8Ws3Q1JzjlAt+43pWQhIl86xZmWeegYCNLPml7yQ+gG3vR6V5h4XGj+TxO+SS5dsThQIA=="
},
"safer-buffer@2.1.2": {
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
},
"strict-event-emitter-types@2.0.0": {
"integrity": "sha512-Nk/brWYpD85WlOgzw5h173aci0Teyv8YdIAEtV+N88nDB0dLlazZyJMIsN6eo1/AR61l+p6CJTG1JIyFaoNEEA=="
},
"uuid@9.0.1": {
"integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="
},
"websocket-ts@2.2.1": {
"integrity": "sha512-YKPDfxlK5qOheLZ2bTIiktZO1bpfGdNCPJmTEaPW7G9UXI1GKjDdeacOrsULUS000OPNxDVOyAuKLuIWPqWM0Q=="
},
"zod@3.24.2": {
"integrity": "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ=="
} }
}, },
"remote": { "remote": {

View File

@@ -1,8 +1,8 @@
import { NostrEvent, NostrRelayInfo, NostrRelayOK, NPolicy } from 'jsr:@nostrify/types@^0.35.0'; import type { IterablePubkeys, Policy } from 'https://gitlab.com/soapbox-pub/strfry-policies/-/raw/develop/mod.ts';
import { nip57 } from 'jsr:@nostr/tools';
import { Client } from 'npm:ldapts'; import { Client } from 'npm:ldapts';
import { nip57 } from '@nostr/tools';
export interface LdapConfig { interface LdapConfig {
url: string; url: string;
bindDN: string; bindDN: string;
password: string; password: string;
@@ -10,73 +10,68 @@ export interface LdapConfig {
whitelistPubkeys?: IterablePubkeys; whitelistPubkeys?: IterablePubkeys;
} }
export class LdapPolicy implements NPolicy { const ldapPolicy: Policy<LdapConfig> = async (msg, opts) => {
constructor(private opts: LdapConfig) {} const client = new Client({ url: opts.url });
const { kind, tags } = msg.event;
let { pubkey } = msg.event;
let out = { id: msg.event.id }
// deno-lint-ignore require-await if (opts.whitelistPubkeys.includes(pubkey)) {
async call(event: NostrEvent): Promise<NostrRelayOK> { out['action'] = 'accept';
const client = new Client({ url: this.opts.url }); out['msg'] = '';
const { id, kind, tags } = event; return out;
let { pubkey } = event; }
if (this.opts.whitelistPubkeys.includes(pubkey)) { // Zap receipt
return ['OK', id, true, '']; if (kind === 9735) {
const descriptionTag = tags.find(([t, v]) => t === 'description' && v);
const invalidZapRequestMsg = 'Zap receipts must contain a valid zap request from a relay member';
if (typeof descriptionTag === 'undefined') {
out['action'] = 'reject';
out['msg'] = invalidZapRequestMsg;
return out;
} }
// Zap receipt const zapRequestJSON = descriptionTag[1];
if (kind === 9735) { const validationResult = nip57.validateZapRequest(zapRequestJSON);
const descriptionTag = tags.find(([t, v]) => t === 'description' && v);
const invalidZapRequestMsg = 'Zap receipts must contain a valid zap request from a relay member';
if (typeof descriptionTag === 'undefined') { // TODO
return ['OK', id, false, invalidZapRequestMsg]; // The zap receipt event's pubkey MUST be the same as the recipient's lnurl provider's nostrPubkey (retrieved in step 1 of the protocol flow).
} // The invoiceAmount contained in the bolt11 tag of the zap receipt MUST equal the amount tag of the zap request (if present).
const zapRequestJSON = descriptionTag[1]; if (validationResult === null) {
const validationResult = nip57.validateZapRequest(zapRequestJSON); pubkey = JSON.parse(zapRequestJSON).pubkey;
} else {
// TODO out['action'] = 'reject';
// The zap receipt event's pubkey MUST be the same as the recipient's lnurl provider's nostrPubkey (retrieved in step 1 of the protocol flow). out['msg'] = invalidZapRequestMsg;
// The invoiceAmount contained in the bolt11 tag of the zap receipt MUST equal the amount tag of the zap request (if present). return out;
if (validationResult === null) {
pubkey = JSON.parse(zapRequestJSON).pubkey;
} else {
return ['OK', id, false, invalidZapRequestMsg];
}
}
const out = { accept: true, msg: ''};
try {
await client.bind(this.opts.bindDN, this.opts.password);
const { searchEntries } = await client.search(this.opts.searchDN, {
filter: `(nostrKey=${pubkey})`,
attributes: ['nostrKey']
});
const memberKey = searchEntries[0]?.nostrKey;
if (memberKey === pubkey) {
out['accept'] = true;
} else {
out['accept'] = false;
out['msg'] = 'Only members can publish notes on this relay';
}
} catch (ex) {
out['accept'] = false;
out['msg'] = 'Auth service temporarily unavailable';
} finally {
await client.unbind();
return ['OK', id, out['accept'], out['msg']];
} }
} }
get info(): NostrRelayInfo { try {
return { await client.bind(opts.bindDN, opts.password);
limitation: {
restricted_writes: true, const { searchEntries } = await client.search(opts.searchDN, {
}, filter: `(nostrKey=${pubkey})`,
}; attributes: ['nostrKey']
});
const memberKey = searchEntries[0]?.nostrKey;
if (memberKey === pubkey) {
out['action'] = 'accept';
out['msg'] = '';
} else {
out['action'] = 'reject';
out['msg'] = 'Only members can publish notes on this relay';
}
} catch (ex) {
out['action'] = 'reject';
out['msg'] = 'Auth service temporarily unavailable';
} finally {
await client.unbind();
return out;
} }
} };
export default ldapPolicy;

View File

@@ -1,20 +1,20 @@
#!/bin/sh #!/bin/sh
//bin/true; exec deno run --unstable-kv -A "$0" "$@" //bin/true; exec deno run -A "$0" "$@"
import { import {
AntiDuplicationPolicy, antiDuplicationPolicy,
HellthreadPolicy, hellthreadPolicy,
PipePolicy, pipeline,
rateLimitPolicy,
readStdin, readStdin,
writeStdout, writeStdout,
} from 'jsr:@nostrify/policies'; } from 'https://gitlab.com/soapbox-pub/strfry-policies/-/raw/develop/mod.ts';
import { strfry } from 'jsr:@nostrify/strfry'; import ldapPolicy from './ldap-policy.ts';
import { LdapConfig, LdapPolicy } from './ldap-policy.ts';
import { load } from "https://deno.land/std@0.224.0/dotenv/mod.ts"; import { load } from "https://deno.land/std@0.224.0/dotenv/mod.ts";
const dirname = new URL('.', import.meta.url).pathname; const dirname = new URL('.', import.meta.url).pathname;
await load({ envPath: `${dirname}/.env`, export: true }); await load({ envPath: `${dirname}/.env`, export: true });
const ldapConfig: LdapConfig = { const ldapConfig = {
url: Deno.env.get("LDAP_URL"), url: Deno.env.get("LDAP_URL"),
bindDN: Deno.env.get("LDAP_BIND_DN"), bindDN: Deno.env.get("LDAP_BIND_DN"),
password: Deno.env.get("LDAP_PASSWORD"), password: Deno.env.get("LDAP_PASSWORD"),
@@ -22,10 +22,13 @@ const ldapConfig: LdapConfig = {
whitelistPubkeys: Deno.env.get("WHITELIST_PUBKEYS")?.split(',') whitelistPubkeys: Deno.env.get("WHITELIST_PUBKEYS")?.split(',')
} }
const policy = new PipePolicy([ for await (const msg of readStdin()) {
new HellthreadPolicy({ limit: 10 }), const result = await pipeline(msg, [
new AntiDuplicationPolicy({ kv: await Deno.openKv(), expireIn: 60000, minLength: 50 }), [hellthreadPolicy, { limit: 10 }],
new LdapPolicy(ldapConfig) [antiDuplicationPolicy, { ttl: 60000, minLength: 50 }],
]); [rateLimitPolicy, { whitelist: ['127.0.0.1'] }],
[ldapPolicy, ldapConfig],
]);
await strfry(policy); writeStdout(result);
}

View File

@@ -21,7 +21,7 @@ namespace :ldap do
desc "Add custom attributes to schema" desc "Add custom attributes to schema"
task add_custom_attributes: :environment do |t, args| task add_custom_attributes: :environment do |t, args|
%w[ admin service_enabled nostr_key pgp_key ].each do |name| %w[ admin service_enabled nostr_key ].each do |name|
Rake::Task["ldap:modify_ldap_schema"].invoke(name, "add") Rake::Task["ldap:modify_ldap_schema"].invoke(name, "add")
Rake::Task['ldap:modify_ldap_schema'].reenable Rake::Task['ldap:modify_ldap_schema'].reenable
end end
@@ -29,7 +29,7 @@ namespace :ldap do
desc "Delete custom attributes from schema" desc "Delete custom attributes from schema"
task delete_custom_attributes: :environment do |t, args| task delete_custom_attributes: :environment do |t, args|
%w[ admin service_enabled nostr_key pgp_key ].each do |name| %w[ admin service_enabled nostr_key ].each do |name|
Rake::Task["ldap:modify_ldap_schema"].invoke(name, "delete") Rake::Task["ldap:modify_ldap_schema"].invoke(name, "delete")
Rake::Task['ldap:modify_ldap_schema'].reenable Rake::Task['ldap:modify_ldap_schema'].reenable
end end

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.10.0", "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

@@ -5,5 +5,5 @@ attributeTypes: ( 1.3.6.1.4.1.61554.1.1.2.1.21
NAME 'nostrKey' NAME 'nostrKey'
DESC 'Nostr public key' DESC 'Nostr public key'
EQUALITY caseIgnoreMatch EQUALITY caseIgnoreMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 SYNTAX 1.3.6.1.4.1.1466.115.121.1.15
SINGLE-VALUE ) SINGLE-VALUE )

View File

@@ -1,8 +0,0 @@
dn: cn=schema
changetype: modify
add: attributeTypes
attributeTypes: ( 1.3.6.1.4.1.3401.8.2.11
NAME 'pgpKey'
DESC 'OpenPGP public key block'
SYNTAX 1.3.6.1.4.1.1466.115.121.1.26
SINGLE-VALUE )

View File

@@ -14,7 +14,6 @@ RSpec.describe 'Account settings', type: :feature do
.with("invalid password").and_return(false) .with("invalid password").and_return(false)
allow_any_instance_of(User).to receive(:valid_ldap_authentication?) allow_any_instance_of(User).to receive(:valid_ldap_authentication?)
.with("valid password").and_return(true) .with("valid password").and_return(true)
allow_any_instance_of(User).to receive(:pgp_pubkey).and_return(nil)
end end
scenario 'fails with invalid password' do scenario 'fails with invalid password' do
@@ -56,44 +55,4 @@ RSpec.describe 'Account settings', type: :feature do
end end
end end
end end
feature "Update OpenPGP key" do
let(:invalid_key) { File.read("#{Rails.root}/spec/fixtures/files/pgp_key_invalid.asc") }
let(:valid_key_alice) { File.read("#{Rails.root}/spec/fixtures/files/pgp_key_valid_alice.asc") }
let(:fingerprint_alice) { "EB85BB5FA33A75E15E944E63F231550C4F47E38E" }
before do
login_as user, :scope => :user
allow_any_instance_of(User).to receive(:ldap_entry).and_return({
uid: user.cn, ou: user.ou, display_name: nil, pgp_key: nil
})
end
scenario 'rejects an invalid key' do
expect(UserManager::UpdatePgpKey).not_to receive(:call)
visit setting_path(:account)
fill_in 'Public key', with: invalid_key
click_button "Save"
expect(current_url).to eq(setting_url(:account))
within ".error-msg" do
expect(page).to have_content("This is not a valid armored PGP public key block")
end
end
scenario 'stores a valid key' do
expect(UserManager::UpdatePgpKey).to receive(:call)
.with(user: user).and_return(true)
visit setting_path(:account)
fill_in 'Public key', with: valid_key_alice
click_button "Save"
expect(current_url).to eq(setting_url(:account))
within ".flash-msg" do
expect(page).to have_content("Settings saved")
end
end
end
end end

View File

@@ -9,7 +9,7 @@ RSpec.describe 'Profile settings', type: :feature do
allow(user).to receive(:display_name).and_return("Mark") allow(user).to receive(:display_name).and_return("Mark")
allow_any_instance_of(User).to receive(:dn).and_return("cn=mwahlberg,ou=kosmos.org,cn=users,dc=kosmos,dc=org") allow_any_instance_of(User).to receive(:dn).and_return("cn=mwahlberg,ou=kosmos.org,cn=users,dc=kosmos,dc=org")
allow_any_instance_of(User).to receive(:ldap_entry).and_return({ allow_any_instance_of(User).to receive(:ldap_entry).and_return({
uid: user.cn, ou: user.ou, display_name: "Mark", pgp_key: nil 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)

View File

@@ -52,7 +52,7 @@ RSpec.describe "Signup", type: :feature do
click_button "Continue" click_button "Continue"
expect(page).to have_content("Choose a password") expect(page).to have_content("Choose a password")
expect(UserManager::CreateAccount).to receive(:call) expect(CreateAccount).to receive(:call)
.with(account: { .with(account: {
username: "tony", domain: "kosmos.org", username: "tony", domain: "kosmos.org",
email: "tony@example.com", password: "a-valid-password", email: "tony@example.com", password: "a-valid-password",
@@ -96,7 +96,7 @@ RSpec.describe "Signup", type: :feature do
click_button "Create account" click_button "Create account"
expect(page).to have_content("Password is too short") expect(page).to have_content("Password is too short")
expect(UserManager::CreateAccount).to receive(:call) expect(CreateAccount).to receive(:call)
.with(account: { .with(account: {
username: "tony", domain: "kosmos.org", username: "tony", domain: "kosmos.org",
email: "tony@example.com", password: "a-valid-password", email: "tony@example.com", password: "a-valid-password",

View File

@@ -1,11 +0,0 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
b7O1u120JkFsaWNlIExvdmVsYWNlIDxhbGljZUBvcGVucGdwLmV4YW1wbGU+iJAE
ExYIADgCGwMFCwkIBwIGFQoJCAsCBBYCAwECHgECF4AWIQTrhbtfozp14V6UTmPy
MVUMT0fjjgUCXaWfOgAKCRDyMVUMT0fjjukrAPoDnHBSogOmsHOsd9qGsiZpgRnO
dypvbm+QtXZqth9rvwD9HcDC0tC+PHAsO7OTh1S1TC9RiJsvawAfCPaQZoed8gK4
OARcRwTpEgorBgEEAZdVAQUBAQdAQv8GIa2rSTzgqbXCpDDYMiKRVitCsy203x3s
E9+eviIDAQgHiHgEGBYIACAWIQTrhbtfozp14V6UTmPyMVUMT0fjjgUCXEcE6QIb
DAAKCRDyMVUMT0fjjlnQAQDFHUs6TIcxrNTtEZFjUFm1M0PJ1Dng/cDW4xN80fsn
0QEA22Kr7VkCjeAEC08VSTeV+QFsmz55/lntWkwYWhmvOgE=
=iIGO
-----END PGP PUBLIC KEY BLOCK-----

View File

@@ -1,16 +0,0 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
Comment: Alice's OpenPGP certificate
Comment: https://www.ietf.org/id/draft-bre-openpgp-samples-01.html
mDMEXEcE6RYJKwYBBAHaRw8BAQdArjWwk3FAqyiFbFBKT4TzXcVBqPTB3gmzlC/U
b7O1u120JkFsaWNlIExvdmVsYWNlIDxhbGljZUBvcGVucGdwLmV4YW1wbGU+iJAE
ExYIADgCGwMFCwkIBwIGFQoJCAsCBBYCAwECHgECF4AWIQTrhbtfozp14V6UTmPy
MVUMT0fjjgUCXaWfOgAKCRDyMVUMT0fjjukrAPoDnHBSogOmsHOsd9qGsiZpgRnO
dypvbm+QtXZqth9rvwD9HcDC0tC+PHAsO7OTh1S1TC9RiJsvawAfCPaQZoed8gK4
OARcRwTpEgorBgEEAZdVAQUBAQdAQv8GIa2rSTzgqbXCpDDYMiKRVitCsy203x3s
E9+eviIDAQgHiHgEGBYIACAWIQTrhbtfozp14V6UTmPyMVUMT0fjjgUCXEcE6QIb
DAAKCRDyMVUMT0fjjlnQAQDFHUs6TIcxrNTtEZFjUFm1M0PJ1Dng/cDW4xN80fsn
0QEA22Kr7VkCjeAEC08VSTeV+QFsmz55/lntWkwYWhmvOgE=
=iIGO
-----END PGP PUBLIC KEY BLOCK-----

View File

@@ -1,13 +0,0 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
mDMEZvFjRhYJKwYBBAHaRw8BAQdACUxVX9bGlbuNR0MNYUyHHxTcOgm4qjwq8Bjg
7P41OFK0GEppbW15IDxqaW1teUBrb3Ntb3Mub3JnPoiZBBMWCgBBFiEEMWv1FiNt
r3cjaxX2BX2Tly+4YsMFAmbxY0YCGwMFCQWjmoAFCwkIBwICIgIGFQoJCAsCBBYC
AwECHgcCF4AACgkQBX2Tly+4YsMjHgEAoOOLrv9pWbi8hhrSMkqJ7FJvsBTQF//U
aJUQRa8CTgoBAI3kyGKZ8gOC8UOOKsUC0LiNCVXPyX45h8T4QFRdEVYKuDgEZvFj
RhIKKwYBBAGXVQEFAQEHQIomqcQ59UjtQex54pz8qGqyxCj2DPJYUat9pXinDgN8
AwEIB4h+BBgWCgAmFiEEMWv1FiNtr3cjaxX2BX2Tly+4YsMFAmbxY0YCGwwFCQWj
moAACgkQBX2Tly+4YsPoVgEA/9Q5Gs1klP4u/nw343V57e9s4RKmEiRSkErnC9wW
Iu0A/jp6Elz2pDQPB2XLwcb+n7JlgA05HI0zWj1+EoM7TC4J
=KQbn
-----END PGP PUBLIC KEY BLOCK-----

Binary file not shown.

View File

@@ -1,25 +0,0 @@
require 'rails_helper'
describe ServicesHelper do
describe "#service_human_name" do
it "returns the human name when it's configured" do
expect(service_human_name("mastodon")).to eq("Mastodon")
end
it "returns the key when there is no human name" do
expect(service_human_name("ejabberd")).to eq("ejabberd")
end
end
describe "#service_display_name" do
it "returns the display name when it's configured" do
expect(service_display_name("lndhub")).to eq("Lightning Network")
end
it "returns the human name when there is no display name" do
expect(service_display_name("mastodon")).to eq("Mastodon")
end
end
end

View File

@@ -44,7 +44,7 @@ RSpec.describe CreateLdapUserJob, type: :job do
it "adds default services for pre-confirmed accounts" do it "adds default services for pre-confirmed accounts" do
allow(ldap_client_mock).to receive(:add) # spy on mock allow(ldap_client_mock).to receive(:add) # spy on mock
Setting.default_services = ["ejabberd", "discourse"] allow(Setting).to receive(:default_services).and_return(["xmpp", "discourse"])
perform_enqueued_jobs { job_for_preconfirmed_account } perform_enqueued_jobs { job_for_preconfirmed_account }
@@ -56,7 +56,7 @@ RSpec.describe CreateLdapUserJob, type: :job do
sn: "halfinney", sn: "halfinney",
uid: "halfinney", uid: "halfinney",
mail: "halfinney@example.com", mail: "halfinney@example.com",
serviceEnabled: ["ejabberd", "discourse"], serviceEnabled: ["xmpp", "discourse"],
userPassword: "remember-remember-the-5th-of-november" userPassword: "remember-remember-the-5th-of-november"
} }
) )

View File

@@ -13,7 +13,7 @@ RSpec.describe XmppExchangeContactsJob, type: :job do
before do before do
stub_request(:post, "http://xmpp.example.com/api/add_rosteritem") stub_request(:post, "http://xmpp.example.com/api/add_rosteritem")
.to_return(status: 200, body: "", headers: {}) .to_return(status: 200, body: "", headers: {})
allow_any_instance_of(User).to receive(:services_enabled).and_return(["ejabberd"]) allow_any_instance_of(User).to receive(:services_enabled).and_return(["xmpp"])
end end
it "posts add_rosteritem commands to the ejabberd API" do it "posts add_rosteritem commands to the ejabberd API" do

View File

@@ -1,87 +0,0 @@
# spec/mailers/welcome_mailer_spec.rb
require 'rails_helper'
RSpec.describe NotificationMailer, type: :mailer do
describe '#lightning_sats_received' do
context "without PGP key" do
let(:user) { create(:user, cn: "phil", email: 'phil@example.com') }
before do
allow_any_instance_of(User).to receive(:ldap_entry).and_return({
uid: user.cn, ou: user.ou, display_name: nil, pgp_key: nil
})
end
describe "unencrypted email" do
let(:mail) { described_class.with(user: user, amount_sats: 21000).lightning_sats_received }
it 'renders the correct to/from headers' do
expect(mail.to).to eq([user.email])
expect(mail.from).to eq(['accounts@kosmos.org'])
end
it 'renders the correct subject' do
expect(mail.subject).to eq('Sats received')
end
it 'uses the correct content type' do
expect(mail.header['content-type'].to_s).to include('text/plain')
end
it 'renders the body with correct content' do
expect(mail.body.encoded).to match(/You just received 21,000 sats/)
expect(mail.body.encoded).to include(user.address)
end
it 'includes a link to the lightning service page' do
expect(mail.body.encoded).to include("https://accounts.kosmos.org/services/lightning")
end
end
end
context "with PGP key" do
let(:pgp_pubkey) { File.read("#{Rails.root}/spec/fixtures/files/pgp_key_valid_alice.asc") }
let(:pgp_fingerprint) { "EB85BB5FA33A75E15E944E63F231550C4F47E38E" }
let(:user) { create(:user, id: 2, cn: "alice", email: 'alice@example.com', pgp_fpr: pgp_fingerprint) }
before do
allow_any_instance_of(User).to receive(:ldap_entry).and_return({
uid: user.cn, ou: user.ou, display_name: nil, pgp_key: pgp_pubkey
})
end
describe "encrypted email" do
let(:mail) { described_class.with(user: user, amount_sats: 21000).lightning_sats_received }
it 'renders the correct to/from headers' do
expect(mail.to).to eq([user.email])
expect(mail.from).to eq(['accounts@kosmos.org'])
end
it 'encrypts the subject line' do
expect(mail.subject).to eq('...')
end
it 'uses the correct content type' do
expect(mail.header['content-type'].to_s).to include('multipart/encrypted')
expect(mail.header['content-type'].to_s).to include('protocol="application/pgp-encrypted"')
end
it 'renders the PGP version part' do
expect(mail.body.encoded).to include("Content-Type: application/pgp-encrypted")
expect(mail.body.encoded).to include("Content-Description: PGP/MIME version identification")
expect(mail.body.encoded).to include("Version: 1")
end
it 'renders the encrypted PGP part' do
expect(mail.body.encoded).to include('Content-Type: application/octet-stream; name="encrypted.asc"')
expect(mail.body.encoded).to include('Content-Description: OpenPGP encrypted message')
expect(mail.body.encoded).to include('Content-Disposition: inline; filename="encrypted.asc"')
expect(mail.body.encoded).to include('-----BEGIN PGP MESSAGE-----')
expect(mail.body.encoded).to include('hF4DR')
end
end
end
end
end

View File

@@ -1,25 +0,0 @@
require 'rails_helper'
RSpec.describe Setting, type: :model do
describe ".available_services" do
before do
Setting.discourse_enabled = true
Setting.ejabberd_enabled = true
Setting.email_enabled = false
Setting.gitea_enabled = false
Setting.lndhub_enabled = true
Setting.mastodon_enabled = true
Setting.mediawiki_enabled = false
Setting.nostr_enabled = false
Setting.remotestorage_enabled = true
end
it "contains all enabled services" do
expect(Setting.available_services).to eq(%w[
discourse ejabberd lndhub mastodon remotestorage
])
end
end
end

View File

@@ -1,16 +1,20 @@
require 'rails_helper' require 'rails_helper'
RSpec.describe User, type: :model do RSpec.describe User, type: :model do
let(:user) { create :user, cn: "philipp", ou: "kosmos.org", email: "philipp@example.com" } let(:user) { create :user, cn: "philipp" }
let(:dn) { "cn=philipp,ou=kosmos.org,cn=users,dc=kosmos,dc=org" } let(:dn) { "cn=philipp,ou=kosmos.org,cn=users,dc=kosmos,dc=org" }
describe "#address" do describe "#address" do
let(:user) { build :user, cn: "jimmy", ou: "kosmos.org" }
it "returns the user address" do it "returns the user address" do
expect(user.address).to eq("philipp@kosmos.org") expect(user.address).to eq("jimmy@kosmos.org")
end end
end end
describe "#mastodon_address" do describe "#mastodon_address" do
let(:user) { build :user, cn: "jimmy", ou: "kosmos.org" }
context "Mastodon service not configured" do context "Mastodon service not configured" do
before do before do
Setting.mastodon_enabled = false Setting.mastodon_enabled = false
@@ -28,7 +32,7 @@ RSpec.describe User, type: :model do
describe "domain is the same as primary domain" do describe "domain is the same as primary domain" do
it "returns the user address" do it "returns the user address" do
expect(user.mastodon_address).to eq("philipp@kosmos.org") expect(user.mastodon_address).to eq("jimmy@kosmos.org")
end end
end end
@@ -38,7 +42,7 @@ RSpec.describe User, type: :model do
end end
it "returns the user address" do it "returns the user address" do
expect(user.mastodon_address).to eq("philipp@kosmos.social") expect(user.mastodon_address).to eq("jimmy@kosmos.social")
end end
end end
@@ -74,9 +78,9 @@ 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", "ejabberd", "email", "gitea", "wiki"] services_enabled: ["discourse", "email", "gitea", "wiki", "xmpp"]
}) })
expect(user.services_enabled).to eq(["discourse", "ejabberd", "email", "gitea", "wiki"]) expect(user.services_enabled).to eq(["discourse", "email", "gitea", "wiki", "xmpp"])
end end
end end
@@ -84,7 +88,7 @@ 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: ["ejabberd", "gitea"] services_enabled: ["gitea", "xmpp"]
}) })
end end
@@ -117,9 +121,9 @@ RSpec.describe User, type: :model do
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", "ejabberd", "gitea", "wiki"]).and_return(true) .with(dn, :serviceEnabled, ["discourse", "gitea", "wiki", "xmpp"]).and_return(true)
user.enable_service([:ejabberd, :wiki]) user.enable_service([:wiki, :xmpp])
end end
end end
@@ -127,7 +131,7 @@ 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", "ejabberd", "gitea"] services_enabled: ["discourse", "gitea", "xmpp"]
}) })
allow(user).to receive(:dn).and_return(dn) allow(user).to receive(:dn).and_return(dn)
end end
@@ -136,14 +140,14 @@ RSpec.describe User, type: :model 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, :serviceEnabled, ["discourse", "gitea"]).and_return(true)
user.disable_service(:ejabberd) 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, :serviceEnabled, ["discourse"]).and_return(true)
user.disable_service([:ejabberd, "gitea"]) user.disable_service([:xmpp, "gitea"])
end end
end end
@@ -174,7 +178,7 @@ RSpec.describe User, type: :model do
after { clear_enqueued_jobs } after { clear_enqueued_jobs }
it "enables default services" do it "enables default services" do
expect(user).to receive(:enable_service).with(Setting.default_services) expect(user).to receive(:enable_service).with(%w[ discourse gitea mastodon mediawiki xmpp ])
user.send :devise_after_confirmation user.send :devise_after_confirmation
end end
@@ -235,7 +239,7 @@ RSpec.describe User, type: :model do
describe "#nostr_pubkey" do describe "#nostr_pubkey" do
before do before do
allow(user).to receive(:ldap_entry) allow_any_instance_of(User).to receive(:ldap_entry)
.and_return({ nostr_key: "07e188a1ff87ce171d517b8ed2bb7a31b1d3453a0db3b15379ec07b724d232f3" }) .and_return({ nostr_key: "07e188a1ff87ce171d517b8ed2bb7a31b1d3453a0db3b15379ec07b724d232f3" })
end end
@@ -246,7 +250,7 @@ RSpec.describe User, type: :model do
describe "#nostr_pubkey_bech32" do describe "#nostr_pubkey_bech32" do
before do before do
allow(user).to receive(:ldap_entry) allow_any_instance_of(User).to receive(:ldap_entry)
.and_return({ nostr_key: "07e188a1ff87ce171d517b8ed2bb7a31b1d3453a0db3b15379ec07b724d232f3" }) .and_return({ nostr_key: "07e188a1ff87ce171d517b8ed2bb7a31b1d3453a0db3b15379ec07b724d232f3" })
end end
@@ -254,73 +258,4 @@ RSpec.describe User, type: :model do
expect(user.nostr_pubkey_bech32).to eq("npub1qlsc3g0lsl8pw8230w8d9wm6xxcax3f6pkemz5measrmwfxjxteslf2hac") expect(user.nostr_pubkey_bech32).to eq("npub1qlsc3g0lsl8pw8230w8d9wm6xxcax3f6pkemz5measrmwfxjxteslf2hac")
end end
end end
describe "OpenPGP key" do
let(:alice) { create :user, id: 2, cn: "alice", email: "alice@example.com" }
let(:jimmy) { create :user, id: 3, cn: "jimmy", email: "jimmy@example.com" }
let(:valid_key_alice) { File.read("#{Rails.root}/spec/fixtures/files/pgp_key_valid_alice.asc") }
let(:valid_key_jimmy) { File.read("#{Rails.root}/spec/fixtures/files/pgp_key_valid_jimmy.asc") }
let(:fingerprint_alice) { "EB85BB5FA33A75E15E944E63F231550C4F47E38E" }
let(:fingerprint_jimmy) { "316BF516236DAF77236B15F6057D93972FB862C3" }
let(:invalid_key) { File.read("#{Rails.root}/spec/fixtures/files/pgp_key_invalid.asc") }
before do
GPGME::Key.import(valid_key_alice)
GPGME::Key.import(valid_key_jimmy)
alice.update pgp_fpr: fingerprint_alice
jimmy.update pgp_fpr: fingerprint_jimmy
allow(alice).to receive(:ldap_entry).and_return({ pgp_key: valid_key_alice })
allow(jimmy).to receive(:ldap_entry).and_return({ pgp_key: valid_key_jimmy })
end
after do
alice.gnupg_key.delete!
jimmy.gnupg_key.delete!
end
describe "#acceptable_pgp_key_format" do
it "validates the record when the key is valid" do
alice.pgp_pubkey = valid_key_alice
expect(alice).to be_valid
end
it "adds a validation error when the key is not valid" do
user.pgp_pubkey = invalid_key
expect(user).to_not be_valid
expect(user.errors[:pgp_pubkey]).to be_present
end
end
describe "#pgp_pubkey" do
it "returns the raw pubkey from LDAP" do
expect(alice.pgp_pubkey).to eq(valid_key_alice)
end
end
describe "#gnupg_key" do
subject { alice.gnupg_key }
it "returns a GPGME::Key object from the system's GPG keyring" do
expect(subject).to be_a(GPGME::Key)
expect(subject.fingerprint).to eq(fingerprint_alice)
expect(subject.email).to eq("alice@openpgp.example")
end
end
describe "#pgp_pubkey_contains_user_address?" do
it "returns false when the user address is one of the UIDs of the key" do
expect(alice.pgp_pubkey_contains_user_address?).to eq(false)
end
it "returns true when the user address is missing from the UIDs of the key" do
expect(jimmy.pgp_pubkey_contains_user_address?).to eq(true)
end
end
describe "wkd_hash" do
it "returns a z-base32 encoded SHA-1 digest of the username" do
expect(alice.wkd_hash).to eq("kei1q4tipxxu1yj79k9kfukdhfy631xe")
end
end
end
end end

View File

@@ -1,101 +0,0 @@
require 'rails_helper'
RSpec.describe "OpenPGP Web Key Directory", type: :request do
describe "policy" do
it "returns an empty 200 response" do
get "/.well-known/openpgpkey/policy"
expect(response).to have_http_status(:ok)
expect(response.body).to be_empty
end
end
describe "non-existent user" do
it "returns a 404 status" do
get "/.well-known/openpgpkey/hu/fmb8gw3n4zdj4xpwaziki4mwcxr1368i?l=aristotle"
expect(response).to have_http_status(:not_found)
end
end
describe "user without pubkey" do
let(:user) { create :user, cn: 'bernd', ou: 'kosmos.org' }
it "returns a 404 status" do
get "/.well-known/openpgpkey/hu/kp95h369c89sx8ia1hn447i868nqyz4t?l=bernd"
expect(response).to have_http_status(:not_found)
end
end
describe "user with pubkey" do
let(:alice) { create :user, id: 2, cn: "alice", email: "alice@example.com" }
let(:jimmy) { create :user, id: 3, cn: "jimmy", email: "jimmy@example.com" }
let(:valid_key_alice) { File.read("#{Rails.root}/spec/fixtures/files/pgp_key_valid_alice.asc") }
let(:valid_key_jimmy) { File.read("#{Rails.root}/spec/fixtures/files/pgp_key_valid_jimmy.asc") }
let(:fingerprint_alice) { "EB85BB5FA33A75E15E944E63F231550C4F47E38E" }
let(:fingerprint_jimmy) { "316BF516236DAF77236B15F6057D93972FB862C3" }
let(:invalid_key) { File.read("#{Rails.root}/spec/fixtures/files/pgp_key_invalid.asc") }
before do
GPGME::Key.import(valid_key_alice)
GPGME::Key.import(valid_key_jimmy)
alice.update pgp_fpr: fingerprint_alice
jimmy.update pgp_fpr: fingerprint_jimmy
end
after do
alice.gnupg_key.delete!
jimmy.gnupg_key.delete!
end
describe "pubkey does not contain user address" do
before do
allow_any_instance_of(User).to receive(:ldap_entry)
.and_return({ pgp_key: valid_key_alice })
end
it "returns a 404 status" do
get "/.well-known/openpgpkey/hu/kei1q4tipxxu1yj79k9kfukdhfy631xe?l=alice"
expect(response).to have_http_status(:not_found)
end
end
describe "pubkey contains user address" do
before do
allow_any_instance_of(User).to receive(:ldap_entry)
.and_return({ pgp_key: valid_key_jimmy })
end
it "returns the pubkey in binary format" do
get "/.well-known/openpgpkey/hu/yuca4ky39mhwkjo78qb8zjgbfj1hg3yf?l=jimmy"
expect(response).to have_http_status(:ok)
expect(response.headers['Content-Type']).to eq("application/octet-stream")
expected_binary_data = File.binread("#{Rails.root}/spec/fixtures/files/pgp_key_valid_jimmy.pem")
expect(response.body).to eq(expected_binary_data)
end
context "with wrong capitalization of username" do
it "returns the pubkey as ASCII Armor plain text" do
get "/.well-known/openpgpkey/hu/yuca4ky39mhwkjo78qb8zjgbfj1hg3yf?l=JimmY"
expect(response).to have_http_status(:ok)
expected_binary_data = File.binread("#{Rails.root}/spec/fixtures/files/pgp_key_valid_jimmy.pem")
expect(response.body).to eq(expected_binary_data)
end
end
context "with .txt extension" do
it "returns the pubkey as ASCII Armor plain text" do
get "/.well-known/openpgpkey/hu/yuca4ky39mhwkjo78qb8zjgbfj1hg3yf.txt?l=jimmy"
expect(response).to have_http_status(:ok)
expect(response.body).to eq(valid_key_jimmy)
expect(response.headers['Content-Type']).to eq("text/plain")
end
end
context "invalid URL" do
it "returns a 422 status" do
get "/.well-known/openpgpkey/hu/123456abcdef?l=alice"
expect(response).to have_http_status(:not_found)
end
end
end
end
end

View File

@@ -44,7 +44,7 @@ RSpec.describe "WebFinger", type: :request do
before do before do
allow_any_instance_of(User).to receive(:ldap_entry).and_return({ allow_any_instance_of(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: ["ejabberd"] services_enabled: ["xmpp"]
}) })
end end
@@ -92,13 +92,7 @@ RSpec.describe "WebFinger", type: :request do
expect(rs_link["href"]).to eql("#{Setting.rs_storage_url}/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://accounts.kosmos.org/rs/oauth/tony") expect(oauth_url).to eql("http://www.example.com/rs/oauth/tony")
end
it "returns CORS headers" do
get "/.well-known/nostr.json?name=bobdylan"
expect(response.headers['Access-Control-Allow-Origin']).to eq("*")
expect(response.headers['Access-Control-Allow-Methods']).to eq('GET')
end end
end end
@@ -106,7 +100,7 @@ RSpec.describe "WebFinger", type: :request do
before do before do
allow_any_instance_of(User).to receive(:ldap_entry).and_return({ allow_any_instance_of(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: ["ejabberd"] services_enabled: ["xmpp"]
}) })
end end

View File

@@ -46,12 +46,6 @@ RSpec.describe "Well-known URLs", type: :request do
expect(res["names"]["bobdylan"]).to eq(user.nostr_pubkey) expect(res["names"]["bobdylan"]).to eq(user.nostr_pubkey)
end end
it "returns CORS headers" do
get "/.well-known/nostr.json?name=bobdylan"
expect(response.headers['Access-Control-Allow-Origin']).to eq("*")
expect(response.headers['Access-Control-Allow-Methods']).to eq('GET')
end
context "without relay configured" do context "without relay configured" do
before do before do
Setting.nostr_relay_url = "" Setting.nostr_relay_url = ""
@@ -79,36 +73,10 @@ RSpec.describe "Well-known URLs", type: :request do
end end
describe "placeholder username for domain's own pubkey" do describe "placeholder username for domain's own pubkey" do
describe "for primary domain" do it "returns the configured nostr pubkey" do
context "no different pubkey configured for primary domain" do get "/.well-known/nostr.json?name=_"
it "returns the akkounts nostr pubkey" do res = JSON.parse(response.body)
get "/.well-known/nostr.json?name=_" expect(res["names"]["_"]).to eq(Setting.nostr_public_key)
res = JSON.parse(response.body)
expect(res["names"]["_"]).to eq("bdd76ce2934b2f591f9fad2ebe9da18f20d2921de527494ba00eeaa0a0efadcf")
end
end
context "different pubkey configured for primary domain" do
before do
Setting.nostr_public_key_primary_domain = "b3e8f62fbe41217ffc0aa1e178d297339932d8ba4f46d9c7df3b61575e78fecc"
end
it "returns the primary domain's nostr pubkey" do
get "/.well-known/nostr.json?name=_"
res = JSON.parse(response.body)
expect(res["names"]["_"]).to eq("b3e8f62fbe41217ffc0aa1e178d297339932d8ba4f46d9c7df3b61575e78fecc")
end
end
end
describe "for akkounts domain" do
it "returns the configured nostr pubkey" do
headers = { "X-Forwarded-Host" => "accounts.kosmos.org" }
get "/.well-known/nostr.json?name=_"
res = JSON.parse(response.body)
expect(res["names"]["_"]).to eq("bdd76ce2934b2f591f9fad2ebe9da18f20d2921de527494ba00eeaa0a0efadcf")
end
end end
context "with relay configured" do context "with relay configured" do

View File

@@ -1,8 +1,8 @@
require 'rails_helper' require 'rails_helper'
RSpec.describe UserManager::CreateAccount, type: :model do RSpec.describe CreateAccount, type: :model do
describe "#create_user_in_database" do describe "#create_user_in_database" do
let(:service) { described_class.new(account: { let(:service) { CreateAccount.new(account: {
username: 'isaacnewton', username: 'isaacnewton',
email: 'isaacnewton@example.com', email: 'isaacnewton@example.com',
password: 'bright-ideas-in-autumn' password: 'bright-ideas-in-autumn'
@@ -19,7 +19,7 @@ RSpec.describe UserManager::CreateAccount, type: :model do
describe "#update_invitation" do describe "#update_invitation" do
let(:invitation) { create :invitation } let(:invitation) { create :invitation }
let(:service) { described_class.new(account: { let(:service) { CreateAccount.new(account: {
username: 'isaacnewton', username: 'isaacnewton',
email: 'isaacnewton@example.com', email: 'isaacnewton@example.com',
password: 'bright-ideas-in-autumn', password: 'bright-ideas-in-autumn',
@@ -42,7 +42,7 @@ RSpec.describe UserManager::CreateAccount, type: :model do
describe "#add_ldap_document" do describe "#add_ldap_document" do
include ActiveJob::TestHelper include ActiveJob::TestHelper
let(:service) { described_class.new(account: { let(:service) { CreateAccount.new(account: {
username: 'halfinney', username: 'halfinney',
email: 'halfinney@example.com', email: 'halfinney@example.com',
password: 'remember-remember-the-5th-of-november' password: 'remember-remember-the-5th-of-november'
@@ -68,7 +68,7 @@ RSpec.describe UserManager::CreateAccount, type: :model do
describe "#add_ldap_document for pre-confirmed account" do describe "#add_ldap_document for pre-confirmed account" do
include ActiveJob::TestHelper include ActiveJob::TestHelper
let(:service) { described_class.new(account: { let(:service) { CreateAccount.new(account: {
username: 'halfinney', username: 'halfinney',
email: 'halfinney@example.com', email: 'halfinney@example.com',
password: 'remember-remember-the-5th-of-november', password: 'remember-remember-the-5th-of-november',
@@ -89,7 +89,7 @@ RSpec.describe UserManager::CreateAccount, type: :model do
describe "#create_lndhub_account" do describe "#create_lndhub_account" do
include ActiveJob::TestHelper include ActiveJob::TestHelper
let(:service) { described_class.new(account: { let(:service) { CreateAccount.new(account: {
username: 'halfinney', email: 'halfinney@example.com', username: 'halfinney', email: 'halfinney@example.com',
password: 'bright-ideas-in-winter' password: 'bright-ideas-in-winter'
})} })}

View File

@@ -1,13 +1,13 @@
require 'rails_helper' require 'rails_helper'
RSpec.describe UserManager::CreateInvitations, type: :model do RSpec.describe CreateInvitations, type: :model do
include ActiveJob::TestHelper include ActiveJob::TestHelper
let(:user) { create :user } let(:user) { create :user }
describe "#call" do describe "#call" do
before do before do
described_class.call(user: user, amount: 5) CreateInvitations.call(user: user, amount: 5)
end end
after(:each) { clear_enqueued_jobs } after(:each) { clear_enqueued_jobs }
@@ -28,7 +28,7 @@ RSpec.describe UserManager::CreateInvitations, type: :model do
describe "#call with notification disabled" do describe "#call with notification disabled" do
before do before do
described_class.call(user: user, amount: 3, notify: false) CreateInvitations.call(user: user, amount: 3, notify: false)
end end
after(:each) { clear_enqueued_jobs } after(:each) { clear_enqueued_jobs }

View File

@@ -1,74 +0,0 @@
require 'rails_helper'
RSpec.describe UserManager::UpdatePgpKey, type: :model do
include ActiveJob::TestHelper
let(:alice) { create :user, cn: "alice" }
let(:dn) { "cn=alice,ou=kosmos.org,cn=users,dc=kosmos,dc=org" }
let(:pubkey_asc) { File.read("#{Rails.root}/spec/fixtures/files/pgp_key_valid_alice.asc") }
let(:fingerprint) { "EB85BB5FA33A75E15E944E63F231550C4F47E38E" }
before do
allow(alice).to receive(:dn).and_return(dn)
allow(alice).to receive(:ldap_entry).and_return({
uid: alice.cn, ou: alice.ou, pgp_key: nil
})
end
describe "#call" do
context "with valid key" do
before do
alice.pgp_pubkey = pubkey_asc
allow(LdapManager::UpdatePgpKey).to receive(:call)
.with(dn: alice.dn, pubkey: pubkey_asc)
end
after do
alice.gnupg_key.delete!
end
it "imports the key into the GnuPG keychain" do
described_class.call(user: alice)
expect(alice.gnupg_key).to be_present
end
it "stores the key's fingerprint on the user record" do
described_class.call(user: alice)
expect(alice.pgp_fpr).to eq(fingerprint)
end
it "updates the user's LDAP entry with the new key" do
expect(LdapManager::UpdatePgpKey).to receive(:call)
.with(dn: alice.dn, pubkey: pubkey_asc)
described_class.call(user: alice)
end
end
context "with empty key" do
before do
alice.update pgp_fpr: fingerprint
alice.pgp_pubkey = ""
allow(LdapManager::UpdatePgpKey).to receive(:call)
.with(dn: alice.dn, pubkey: "")
end
it "does not attempt to import the key" do
expect(GPGME::Key).not_to receive(:import)
described_class.call(user: alice)
end
it "removes the key's fingerprint from the user record" do
described_class.call(user: alice)
expect(alice.pgp_fpr).to be_nil
end
it "removes the key from the user's LDAP entry" do
expect(LdapManager::UpdatePgpKey).to receive(:call)
.with(dn: alice.dn, pubkey: "")
described_class.call(user: alice)
end
end
end
end