Compare commits
7 Commits
feature/no
...
37b106e73c
| Author | SHA1 | Date | |
|---|---|---|---|
|
37b106e73c
|
|||
|
c3f1f97e1a
|
|||
|
4a677178e8
|
|||
|
3042a02a17
|
|||
|
118fddb497
|
|||
|
ba683a7b95
|
|||
|
90a8a70c15
|
2
Gemfile
2
Gemfile
@@ -44,6 +44,8 @@ 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'
|
||||||
|
|||||||
@@ -197,6 +197,8 @@ 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)
|
||||||
@@ -483,6 +485,7 @@ 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
|
||||||
@@ -507,6 +510,7 @@ 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)
|
||||||
@@ -540,6 +544,7 @@ 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
|
||||||
|
|||||||
@@ -1,44 +0,0 @@
|
|||||||
<div class="w-[72vw] md:w-[500px]">
|
|
||||||
<header class="absolute z-10 h-36 sm:h-44 inset-x-1 top-1 rounded-t
|
|
||||||
bg-cover bg-center bg-gray-50"
|
|
||||||
style="background-image: url('<%= @profile["banner"]%>');">
|
|
||||||
<div class="inline-block z-20 size-28 sm:size-32 ml-4 mt-16 sm:mt-20">
|
|
||||||
<% if @profile["picture"].present? %>
|
|
||||||
<img src="<%= @profile["picture"] %>"
|
|
||||||
class="inline-block size:28 sm:size-32 rounded-full border-2 border-white" />
|
|
||||||
<% else %>
|
|
||||||
<span class="inline-block size:28 sm:size-32 overflow-hidden rounded-full border-2 border-white bg-gray-100">
|
|
||||||
<svg class="size-full text-gray-300" fill="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path d="M24 20.993V24H0v-2.996A14.977 14.977 0 0112.004 15c4.904 0 9.26 2.354 11.996 5.993zM16.002 8.999a4 4 0 11-8 0 4 4 0 018 0z" />
|
|
||||||
</svg>
|
|
||||||
</span>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
<main class="mt-44 sm:mt-52">
|
|
||||||
<%= form_for(@user, url: setting_path(:nostr), html: { :method => :put }) do |f| %>
|
|
||||||
<%= render FormElements::FieldsetComponent.new(tag: "div", title: "Display name") do %>
|
|
||||||
<%= f.text_field :display_name, value: @display_name, class: "w-full sm:w-3/5" %>
|
|
||||||
<% if @validation_errors.present? && @validation_errors[:display_name].present? %>
|
|
||||||
<p class="error-msg mt-2"><%= @validation_errors[:display_name].first %></p>
|
|
||||||
<% end %>
|
|
||||||
<% end %>
|
|
||||||
<%= render FormElements::FieldsetComponent.new(tag: "div", title: "Nostr address (NIP-05)") do %>
|
|
||||||
<%= f.text_field :nip05_address, value: @profile["nip05"], class: "w-full sm:w-3/5" %>
|
|
||||||
<% if @validation_errors.present? && @validation_errors[:nip05_address].present? %>
|
|
||||||
<p class="error-msg mt-2"><%= @validation_errors[:nip05_address].first %></p>
|
|
||||||
<% end %>
|
|
||||||
<% end %>
|
|
||||||
<%= render FormElements::FieldsetComponent.new(tag: "div", title: "Ligtning address for Zaps") do %>
|
|
||||||
<%= f.text_field :lud16_address, value: @profile["lud16"], class: "w-full sm:w-3/5" %>
|
|
||||||
<% if @validation_errors.present? && @validation_errors[:lud16_address].present? %>
|
|
||||||
<p class="error-msg mt-2"><%= @validation_errors[:lud16_address].first %></p>
|
|
||||||
<% end %>
|
|
||||||
<% end %>
|
|
||||||
<% end %>
|
|
||||||
</main>
|
|
||||||
<footer>
|
|
||||||
<%# <%= @profile.inspect %>
|
|
||||||
<%# <%= @profile_event.inspect %>
|
|
||||||
</footer>
|
|
||||||
</div>
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
module Settings
|
|
||||||
class NostrEditProfileComponent < ViewComponent::Base
|
|
||||||
def initialize(user:, profile_event:)
|
|
||||||
if profile_event.present?
|
|
||||||
@user = user
|
|
||||||
@profile_event = profile_event
|
|
||||||
@profile = JSON.parse(profile_event["content"])
|
|
||||||
@display_name = @profile["display_name"] || @profile["displayName"]
|
|
||||||
|
|
||||||
if @profile["nip05"].present? && @profile["nip05"] == @user.address
|
|
||||||
# "Your profile's Nostr address is set to <strong>#{ user_address }</strong>"
|
|
||||||
else
|
|
||||||
# "Your profile's Nostr address is not set to <strong>#{ user_address }</strong> yet"
|
|
||||||
end
|
|
||||||
|
|
||||||
if @profile["lud16"].present? && @profile["lud16"] == @user.address
|
|
||||||
# "Your profile's Lightning address is set to <strong>#{ user_address }</strong>"
|
|
||||||
else
|
|
||||||
# "Your profile's Lightning address is not set to <strong>#{ user_address }</strong> yet"
|
|
||||||
end
|
|
||||||
else
|
|
||||||
# "We could not find a profile for your public key"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
<% @statuses.each do |status| %>
|
|
||||||
<%= render StatusTextComponent.new(
|
|
||||||
text: status[:text],
|
|
||||||
icon_name: status[:icon_name],
|
|
||||||
icon_color: status[:icon_color]
|
|
||||||
) %>
|
|
||||||
<% end %>
|
|
||||||
|
|
||||||
<% if @status == 1 %>
|
|
||||||
<p class="mt-8">
|
|
||||||
<button class="btn-md btn-blue">
|
|
||||||
Edit my profile
|
|
||||||
</button>
|
|
||||||
</p>
|
|
||||||
<% elsif @status == 2 %>
|
|
||||||
<p class="mt-8">
|
|
||||||
<button class="btn-md btn-blue">
|
|
||||||
Create my profile
|
|
||||||
</button>
|
|
||||||
</p>
|
|
||||||
<% end %>
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
module Settings
|
|
||||||
class NostrProfileStatusComponent < ViewComponent::Base
|
|
||||||
def initialize(profile_event:, user_address:)
|
|
||||||
@statuses = []
|
|
||||||
|
|
||||||
if profile_event.present?
|
|
||||||
profile = JSON.parse(profile_event["content"])
|
|
||||||
|
|
||||||
@statuses.push({
|
|
||||||
text: "You have a public Nostr profile",
|
|
||||||
icon_name: "check-circle",
|
|
||||||
icon_color: "emerald-500"
|
|
||||||
})
|
|
||||||
|
|
||||||
if profile["nip05"].present? && profile["nip05"] == user_address
|
|
||||||
@statuses.push({
|
|
||||||
text: "Your profile's Nostr address is set to <strong>#{ user_address }</strong>",
|
|
||||||
icon_name: "check-circle",
|
|
||||||
icon_color: "emerald-500"
|
|
||||||
})
|
|
||||||
else
|
|
||||||
@statuses.push({
|
|
||||||
text: "Your profile's Nostr address is not set to <strong>#{ user_address }</strong> yet",
|
|
||||||
icon_name: "alert-octagon",
|
|
||||||
icon_color: "amber-500"
|
|
||||||
})
|
|
||||||
end
|
|
||||||
|
|
||||||
if profile["lud16"].present? && profile["lud16"] == user_address
|
|
||||||
@statuses.push({
|
|
||||||
text: "Your profile's Lightning address is set to <strong>#{ user_address }</strong>",
|
|
||||||
icon_name: "check-circle",
|
|
||||||
icon_color: "emerald-500"
|
|
||||||
})
|
|
||||||
else
|
|
||||||
@statuses.push({
|
|
||||||
text: "Your profile's Lightning address is not set to <strong>#{ user_address }</strong> yet",
|
|
||||||
icon_name: "alert-octagon",
|
|
||||||
icon_color: "amber-500"
|
|
||||||
})
|
|
||||||
end
|
|
||||||
else
|
|
||||||
@statuses.push({
|
|
||||||
text: "We could not find a profile for your public key",
|
|
||||||
icon_name: "alert-octagon",
|
|
||||||
icon_color: "amber-500"
|
|
||||||
})
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
<%= render StatusTextComponent.new(
|
|
||||||
text: @text,
|
|
||||||
icon_name: @icon_name,
|
|
||||||
icon_color: @icon_color) %>
|
|
||||||
|
|
||||||
<% if @status == 1 %>
|
|
||||||
<p class="mt-8">
|
|
||||||
<button class="btn-md btn-blue">
|
|
||||||
Add the relay to my list
|
|
||||||
</button>
|
|
||||||
</p>
|
|
||||||
<% elsif @status == 2 %>
|
|
||||||
<p class="mt-8">
|
|
||||||
<button class="btn-md btn-blue">
|
|
||||||
Set up default relays
|
|
||||||
</button>
|
|
||||||
</p>
|
|
||||||
<% end %>
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
module Settings
|
|
||||||
class NostrRelayStatusComponent < ViewComponent::Base
|
|
||||||
def initialize(nip65_event:)
|
|
||||||
if nip65_event.present?
|
|
||||||
if relay_urls(nip65_event).any? { |r| r.include?("wss://nostr.kosmos.org") }
|
|
||||||
@text = "You have a relay list, and the Kosmos relay is part of it"
|
|
||||||
@icon_name = "check-circle"
|
|
||||||
@icon_color = "emerald-500"
|
|
||||||
@status = 0
|
|
||||||
else
|
|
||||||
@text = "The Kosmos relay is missing from your relay list"
|
|
||||||
@icon_name = "alert-octagon"
|
|
||||||
@icon_color = "amber-500"
|
|
||||||
@status = 1
|
|
||||||
end
|
|
||||||
else
|
|
||||||
@text = "We could not find a relay list for your public key"
|
|
||||||
@icon_name = "alert-octagon"
|
|
||||||
@icon_color = "amber-500"
|
|
||||||
@status = 2
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def relay_urls(nip65_event)
|
|
||||||
nip65_event["tags"].select{ |t| t[0] == "r" }.map{ |t| t[1] }
|
|
||||||
# @inbox_relay_urls = relay_tags&.select{ |t| t[2] == "read" }&.map{ |t| t[1] }
|
|
||||||
# @outbox_relay_urls = relay_tags&.select{ |t| t[2] != "read" }&.map{ |t| t[1] }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
<p class="flex gap-x-4 items-center">
|
|
||||||
<span class="inline-block h-6 w-6 grow-0 text-<%= @icon_color %>">
|
|
||||||
<%= render "icons/#{@icon_name}" %>
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
<%= raw @text %>
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
class StatusTextComponent < ViewComponent::Base
|
|
||||||
def initialize(text:, icon_name:, icon_color:)
|
|
||||||
@text = text
|
|
||||||
@icon_name = icon_name
|
|
||||||
@icon_color = icon_color
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -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])
|
||||||
|
|
||||||
CreateInvitations.call(user: @user, amount: amount, notify: notify_user)
|
UserManager::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"
|
||||||
|
|||||||
@@ -4,13 +4,8 @@ require "bcrypt"
|
|||||||
class SettingsController < ApplicationController
|
class SettingsController < ApplicationController
|
||||||
before_action :authenticate_user!
|
before_action :authenticate_user!
|
||||||
before_action :set_main_nav_section
|
before_action :set_main_nav_section
|
||||||
before_action :set_settings_section, only: [
|
before_action :set_settings_section, only: [:show, :update, :update_email, :reset_email_password]
|
||||||
:show, :update, :update_email, :reset_email_password
|
before_action :set_user, only: [:show, :update, :update_email, :reset_email_password]
|
||||||
]
|
|
||||||
before_action :set_user, only: [
|
|
||||||
:show, :update, :update_email, :reset_email_password,
|
|
||||||
:fetch_nostr_user_metadata
|
|
||||||
]
|
|
||||||
|
|
||||||
def index
|
def index
|
||||||
redirect_to setting_path(:profile)
|
redirect_to setting_path(:profile)
|
||||||
@@ -26,10 +21,12 @@ 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])
|
||||||
@@ -40,6 +37,10 @@ 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.'
|
||||||
}
|
}
|
||||||
@@ -49,6 +50,7 @@ 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]
|
||||||
@@ -66,6 +68,7 @@ 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]
|
||||||
|
|
||||||
@@ -88,6 +91,7 @@ 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
|
||||||
@@ -95,6 +99,7 @@ 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)
|
||||||
|
|
||||||
@@ -133,28 +138,6 @@ class SettingsController < ApplicationController
|
|||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
def fetch_nostr_user_metadata
|
|
||||||
if @user.nostr_pubkey.present?
|
|
||||||
outbox_relay_urls = nil
|
|
||||||
|
|
||||||
# if @nip65_event = NostrManager::DiscoverUserRelays.call(pubkey: @user.nostr_pubkey)
|
|
||||||
# relay_tags = @nip65_event["tags"].select{ |t| t[0] == "r" }
|
|
||||||
# outbox_relay_urls = relay_tags&.select{ |t| t[2] != "read" }&.map{ |t| t[1] }
|
|
||||||
# end
|
|
||||||
|
|
||||||
# @profile = NostrManager::DiscoverUserProfile.call(
|
|
||||||
# pubkey: @user.nostr_pubkey,
|
|
||||||
# relays: outbox_relay_urls
|
|
||||||
# )
|
|
||||||
@profile = {"content"=>"{\"name\":\"jimmy\",\"picture\":\"https://storage.kosmos.org/jimmy/public/shares/241028-1117-tony.jpg\",\"banner\":\"https://storage.kosmos.org/raucao/public/shares/240604-1517-1500x500.jpg\",\"nip05\":\"jimmy@kosmos.org\",\"lud16\":\"jimmy@kosmos.org\",\"pubkey\":\"07e188a1ff87ce171d517b8ed2bb7a31b1d3453a0db3b15379ec07b724d232f3\",\"display_name\":\"Jimmy\",\"displayName\":\"Jimmy\",\"about\":\"I don't exist. Follow at your own peril.\"}", "created_at"=>1730114246, "id"=>"6b15b1308a61ee837bd3b50319978314650e435891c259f4ea499f819f35a4f6", "kind"=>0, "pubkey"=>"07e188a1ff87ce171d517b8ed2bb7a31b1d3453a0db3b15379ec07b724d232f3", "sig"=>"4f681f4b95646bbf88a6eae9ca92c0f2ce5effecfa017556a23490f91a99243aedf81d956ee2466ed64fecb9a03b6b89cd80ff116df0178830977e203867d7ae", "tags"=>[]}
|
|
||||||
# @profile = {"content"=>"{\"name\":\"jimmy\",\"nip05\":\"jimmy@kosmos.org\",\"lud16\":\"jimmy@kosmos.org\",\"pubkey\":\"07e188a1ff87ce171d517b8ed2bb7a31b1d3453a0db3b15379ec07b724d232f3\",\"display_name\":\"Jimmy\",\"displayName\":\"Jimmy\",\"about\":\"I don't exist. Follow at your own peril.\"}", "created_at"=>1730114246, "id"=>"6b15b1308a61ee837bd3b50319978314650e435891c259f4ea499f819f35a4f6", "kind"=>0, "pubkey"=>"07e188a1ff87ce171d517b8ed2bb7a31b1d3453a0db3b15379ec07b724d232f3", "sig"=>"4f681f4b95646bbf88a6eae9ca92c0f2ce5effecfa017556a23490f91a99243aedf81d956ee2466ed64fecb9a03b6b89cd80ff116df0178830977e203867d7ae", "tags"=>[]}
|
|
||||||
else
|
|
||||||
@relays, @profile = [nil, nil]
|
|
||||||
end
|
|
||||||
|
|
||||||
render partial: 'nostr_user_metadata'
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def set_main_nav_section
|
def set_main_nav_section
|
||||||
@@ -179,7 +162,8 @@ class SettingsController < ApplicationController
|
|||||||
|
|
||||||
def user_params
|
def user_params
|
||||||
params.require(:user).permit(
|
params.require(:user).permit(
|
||||||
:display_name, :avatar, preferences: UserPreferences.pref_keys
|
:display_name, :avatar, :pgp_pubkey,
|
||||||
|
preferences: UserPreferences.pref_keys
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ class SignupController < ApplicationController
|
|||||||
session[:new_user] = nil
|
session[:new_user] = nil
|
||||||
session[:validation_error] = nil
|
session[:validation_error] = nil
|
||||||
|
|
||||||
CreateAccount.call(account: {
|
UserManager::CreateAccount.call(account: {
|
||||||
username: @user.cn,
|
username: @user.cn,
|
||||||
domain: Setting.primary_domain,
|
domain: Setting.primary_domain,
|
||||||
email: @user.email,
|
email: @user.email,
|
||||||
|
|||||||
34
app/controllers/web_key_directory_controller.rb
Normal file
34
app/controllers/web_key_directory_controller.rb
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
class WebKeyDirectoryController < WellKnownController
|
||||||
|
before_action :allow_cross_origin_requests, only: [ :show ]
|
||||||
|
|
||||||
|
# /.well-known/openpgpkey/hu/:hashed_username(.txt)
|
||||||
|
def show
|
||||||
|
@user = User.find_by(cn: params[:l])
|
||||||
|
|
||||||
|
if @user.nil? ||
|
||||||
|
@user.pgp_pubkey.empty? ||
|
||||||
|
!@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
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
end
|
||||||
@@ -20,19 +20,6 @@ module Settings
|
|||||||
|
|
||||||
field :nostr_zaps_relay_limit, type: :integer,
|
field :nostr_zaps_relay_limit, type: :integer,
|
||||||
default: 12
|
default: 12
|
||||||
|
|
||||||
field :nostr_discovery_relays, type: :array, default: %w[
|
|
||||||
wss://nostr.kosmos.org
|
|
||||||
wss://purplepag.es
|
|
||||||
wss://relay.nostr.band
|
|
||||||
wss://njump.me
|
|
||||||
wss://relay.damus.io
|
|
||||||
]
|
|
||||||
|
|
||||||
def self.nostr_relay_url_http
|
|
||||||
self.nostr_relay_url.gsub(/^ws:/, "http:")
|
|
||||||
.gsub(/^wss:/, "https:")
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -3,9 +3,10 @@ require 'nostr'
|
|||||||
class User < ApplicationRecord
|
class User < ApplicationRecord
|
||||||
include EmailValidatable
|
include EmailValidatable
|
||||||
|
|
||||||
attr_accessor :display_name
|
|
||||||
attr_accessor :avatar_new
|
|
||||||
attr_accessor :current_password
|
attr_accessor :current_password
|
||||||
|
attr_accessor :avatar_new
|
||||||
|
attr_accessor :display_name
|
||||||
|
attr_accessor :pgp_pubkey
|
||||||
|
|
||||||
serialize :preferences, coder: UserPreferences
|
serialize :preferences, coder: UserPreferences
|
||||||
|
|
||||||
@@ -51,6 +52,8 @@ class User < ApplicationRecord
|
|||||||
|
|
||||||
validate :acceptable_avatar
|
validate :acceptable_avatar
|
||||||
|
|
||||||
|
validate :acceptable_pgp_key_format, if: -> { defined?(@pgp_pubkey) && @pgp_pubkey.present? }
|
||||||
|
|
||||||
#
|
#
|
||||||
# Scopes
|
# Scopes
|
||||||
#
|
#
|
||||||
@@ -165,6 +168,23 @@ 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?
|
||||||
|
@gnupg_key ||= 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
|
||||||
@@ -214,4 +234,10 @@ 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
|
||||||
|
|||||||
@@ -1,54 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
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
|
|
||||||
16
app/services/ldap_manager/update_pgp_key.rb
Normal file
16
app/services/ldap_manager/update_pgp_key.rb
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
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
|
||||||
@@ -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
|
mailRoutingAddress mailpassword nostrKey pgpKey
|
||||||
]
|
]
|
||||||
filter = Net::LDAP::Filter.eq("uid", args[:uid] || "*")
|
filter = Net::LDAP::Filter.eq("uid", args[:uid] || "*")
|
||||||
|
|
||||||
@@ -73,7 +73,8 @@ 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
|
||||||
@@ -101,7 +102,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 || 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 || 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}";)
|
||||||
EOS
|
EOS
|
||||||
|
|
||||||
attrs = {
|
attrs = {
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
module NostrManager
|
|
||||||
class DiscoverUserProfile < NostrManagerService
|
|
||||||
def initialize(pubkey:, relays: nil)
|
|
||||||
@pubkey = pubkey
|
|
||||||
@relays = relays.present? ? relays : Setting.nostr_discovery_relays
|
|
||||||
end
|
|
||||||
|
|
||||||
def call
|
|
||||||
filter = Nostr::Filter.new(
|
|
||||||
authors: [@pubkey],
|
|
||||||
kinds: [0],
|
|
||||||
limit: 1,
|
|
||||||
)
|
|
||||||
|
|
||||||
NostrManager::FetchLatestEvent.call(
|
|
||||||
relays: @relays,
|
|
||||||
filter: filter
|
|
||||||
)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
module NostrManager
|
|
||||||
class DiscoverUserRelays < NostrManagerService
|
|
||||||
def initialize(pubkey:)
|
|
||||||
@pubkey = pubkey
|
|
||||||
@relays = Setting.nostr_discovery_relays
|
|
||||||
end
|
|
||||||
|
|
||||||
def call
|
|
||||||
filter = Nostr::Filter.new(
|
|
||||||
authors: [@pubkey],
|
|
||||||
kinds: [10002],
|
|
||||||
limit: 1,
|
|
||||||
)
|
|
||||||
|
|
||||||
NostrManager::FetchLatestEvent.call(
|
|
||||||
relays: @relays,
|
|
||||||
filter: filter
|
|
||||||
)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
module NostrManager
|
|
||||||
class FetchEvent < NostrManagerService
|
|
||||||
TIMEOUT = 10
|
|
||||||
|
|
||||||
def initialize(filter:, relay_url:)
|
|
||||||
@filter = filter
|
|
||||||
@relay = new_relay(relay_url)
|
|
||||||
@client = Nostr::Client.new
|
|
||||||
end
|
|
||||||
|
|
||||||
def call
|
|
||||||
filter, client, relay = @filter, @client, @relay
|
|
||||||
event = nil
|
|
||||||
mutex = Mutex.new
|
|
||||||
received_event = ConditionVariable.new
|
|
||||||
log_prefix = "[nostr][#{@relay.name}]"
|
|
||||||
|
|
||||||
thread = Thread.new do
|
|
||||||
client.on :connect do
|
|
||||||
client.subscribe(filter: filter)
|
|
||||||
end
|
|
||||||
|
|
||||||
client.on :error do |e|
|
|
||||||
Rails.logger.info "#{log_prefix} Error: #{e}"
|
|
||||||
Thread.current.exit
|
|
||||||
end
|
|
||||||
|
|
||||||
client.on :message do |m|
|
|
||||||
msg = JSON.parse(m) rescue nil
|
|
||||||
if msg && msg[0] == "EVENT" && msg[2]
|
|
||||||
Rails.logger.debug "#{log_prefix} Event received: #{msg[2]["id"]}"
|
|
||||||
mutex.synchronize do
|
|
||||||
event = msg[2]
|
|
||||||
received_event.signal
|
|
||||||
end
|
|
||||||
elsif msg && msg[0] == "EOSE"
|
|
||||||
Thread.current.exit
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
client.connect relay
|
|
||||||
end
|
|
||||||
|
|
||||||
begin
|
|
||||||
Timeout.timeout(TIMEOUT) do
|
|
||||||
mutex.synchronize do
|
|
||||||
received_event.wait(mutex) if event.nil?
|
|
||||||
end
|
|
||||||
end
|
|
||||||
rescue Timeout::Error
|
|
||||||
Rails.logger.debug "#{log_prefix} Timeout: No event received within #{TIMEOUT} seconds"
|
|
||||||
ensure
|
|
||||||
thread.exit
|
|
||||||
end
|
|
||||||
|
|
||||||
event
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
module NostrManager
|
|
||||||
class FetchLatestEvent < NostrManagerService
|
|
||||||
TIMEOUT = 20
|
|
||||||
|
|
||||||
def initialize(relays:, filter:, max_events: 2)
|
|
||||||
@relays = relays
|
|
||||||
@filter = filter
|
|
||||||
@max_events = max_events
|
|
||||||
end
|
|
||||||
|
|
||||||
def call
|
|
||||||
received_events = 0
|
|
||||||
events = []
|
|
||||||
|
|
||||||
begin
|
|
||||||
Timeout.timeout(TIMEOUT) do
|
|
||||||
@relays.each do |url|
|
|
||||||
event = NostrManager::FetchEvent.call(filter: @filter, relay_url: url)
|
|
||||||
|
|
||||||
if event.present?
|
|
||||||
events << event if events.none? { |e| e["id"] == event["id"] }
|
|
||||||
received_events += 1
|
|
||||||
end
|
|
||||||
|
|
||||||
if received_events >= @max_events
|
|
||||||
Rails.logger.debug "Found #{@max_events} events, ending the search"
|
|
||||||
break
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
events.min_by { |e| e["created_at"] }
|
|
||||||
end
|
|
||||||
rescue Timeout::Error
|
|
||||||
if events.size == 1
|
|
||||||
Rails.logger.debug "[nostr] Timeout: only found 1 event within #{TIMEOUT} seconds for filter: #{@filter.inspect}"
|
|
||||||
events.first
|
|
||||||
else
|
|
||||||
Rails.logger.debug "[nostr] Timeout: no events found within #{TIMEOUT} seconds for filter: #{@filter.inspect}"
|
|
||||||
nil
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -19,28 +19,28 @@ module NostrManager
|
|||||||
|
|
||||||
thread = Thread.new do
|
thread = Thread.new do
|
||||||
client.on :connect do
|
client.on :connect do
|
||||||
Rails.logger.debug "#{log_prefix} Publishing #{event.id}..."
|
puts "#{log_prefix} Publishing #{event.id}..."
|
||||||
client.publish event
|
client.publish event
|
||||||
end
|
end
|
||||||
|
|
||||||
client.on :error do |e|
|
client.on :error do |e|
|
||||||
Rails.logger.debug "#{log_prefix} Error: #{e}"
|
puts "#{log_prefix} Error: #{e}"
|
||||||
Rails.logger.debug "#{log_prefix} Closing thread..."
|
puts "#{log_prefix} Closing thread..."
|
||||||
thread.exit
|
thread.exit
|
||||||
end
|
end
|
||||||
|
|
||||||
client.on :message do |m|
|
client.on :message do |m|
|
||||||
Rails.logger.debug "#{log_prefix} Message: #{m}"
|
puts "#{log_prefix} Message: #{m}"
|
||||||
msg = JSON.parse(m) rescue []
|
msg = JSON.parse(m) rescue []
|
||||||
if msg[0] == "OK" && msg[1] == event.id && msg[2]
|
if msg[0] == "OK" && msg[1] == event.id && msg[2]
|
||||||
Rails.logger.debug "#{log_prefix} Event published. Closing thread..."
|
puts "#{log_prefix} Event published. Closing thread..."
|
||||||
else
|
else
|
||||||
Rails.logger.debug "#{log_prefix} Unexpected message from relay. Closing thread..."
|
puts "#{log_prefix} Unexpected message from relay. Closing thread..."
|
||||||
end
|
end
|
||||||
thread.exit
|
thread.exit
|
||||||
end
|
end
|
||||||
|
|
||||||
Rails.logger.debug "#{log_prefix} Connecting to #{relay.url}..."
|
puts "#{log_prefix} Connecting to #{relay.url}..."
|
||||||
client.connect relay
|
client.connect relay
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ require "nostr"
|
|||||||
class NostrManagerService < ApplicationService
|
class NostrManagerService < ApplicationService
|
||||||
def parse_tags(tags)
|
def parse_tags(tags)
|
||||||
out = {}
|
out = {}
|
||||||
# TODO support more than 1 item for each tag type
|
|
||||||
tags.each do |tag|
|
tags.each do |tag|
|
||||||
out[tag[0].to_sym] = tag[1, tag.length]
|
out[tag[0].to_sym] = tag[1, tag.length]
|
||||||
end
|
end
|
||||||
@@ -20,8 +19,4 @@ class NostrManagerService < ApplicationService
|
|||||||
def site_user
|
def site_user
|
||||||
Nostr::User.new(keypair: site_keypair)
|
Nostr::User.new(keypair: site_keypair)
|
||||||
end
|
end
|
||||||
|
|
||||||
def new_relay(url)
|
|
||||||
Nostr::Relay.new(url: url, name: URI.parse(url).host)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|||||||
56
app/services/user_manager/create_account.rb
Normal file
56
app/services/user_manager/create_account.rb
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
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
|
||||||
19
app/services/user_manager/create_invitations.rb
Normal file
19
app/services/user_manager/create_invitations.rb
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
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
|
||||||
24
app/services/user_manager/update_pgp_key.rb
Normal file
24
app/services/user_manager/update_pgp_key.rb
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
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
|
||||||
2
app/services/user_manager_service.rb
Normal file
2
app/services/user_manager_service.rb
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
class UserManagerService < ApplicationService
|
||||||
|
end
|
||||||
@@ -31,28 +31,13 @@
|
|||||||
) %>
|
) %>
|
||||||
</ul>
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<h3>Zaps</h3>
|
<h3>Zaps</h3>
|
||||||
<ul role="list">
|
<ul role="list">
|
||||||
<%= render FormElements::FieldsetResettableSettingComponent.new(
|
<%= render FormElements::FieldsetResettableSettingComponent.new(
|
||||||
key: :nostr_zaps_relay_limit,
|
key: :nostr_zaps_relay_limit,
|
||||||
title: "Relay limit",
|
title: "Relay limit",
|
||||||
description: "The maximum number of sender-defined relays to try to publish zap receipts to"
|
description: "The maximum number of relays to publish zap receipts to"
|
||||||
) %>
|
) %>
|
||||||
</ul>
|
</ul>
|
||||||
</section>
|
|
||||||
|
|
||||||
<section>
|
|
||||||
<h3>Onboarding</h3>
|
|
||||||
<ul role="list">
|
|
||||||
<%= render FormElements::FieldsetComponent.new(
|
|
||||||
title: "Discovery relays",
|
|
||||||
description: "Used to discover a user's published relay list and/or profile"
|
|
||||||
) do %>
|
|
||||||
<%= f.text_area :nostr_discovery_relays,
|
|
||||||
value: Setting.nostr_discovery_relays.join("\n"),
|
|
||||||
class: "h-44 w-80" %>
|
|
||||||
<% end %>
|
|
||||||
</ul>
|
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|||||||
@@ -89,13 +89,47 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="sm:flex-1 sm:pt-0">
|
<section class="sm:flex-1 sm:pt-0">
|
||||||
<% if @avatar.present? %>
|
<h3>LDAP</h3>
|
||||||
<h3>LDAP<h3>
|
<table class="divided">
|
||||||
<p>
|
<tbody>
|
||||||
<img src="data:image/jpeg;base64,<%= @avatar %>" class="h-48 w-48" />
|
<tr>
|
||||||
</p>
|
<th>Avatar</th>
|
||||||
<% end %>
|
<td>
|
||||||
<!-- <h3>Actions</h3> -->
|
<% if @avatar.present? %>
|
||||||
|
<img src="data:image/jpeg;base64,<%= @avatar %>" class="h-48 w-48" />
|
||||||
|
<% else %>
|
||||||
|
—
|
||||||
|
<% 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 %>
|
||||||
|
—
|
||||||
|
<% end %>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -184,7 +218,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>
|
||||||
|
|||||||
@@ -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.present?
|
"settings--account--email-validation-failed-value": @validation_errors&.[](:email)&.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.present? && @validation_errors[:email].present? %>
|
<% if @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,10 +41,33 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
<section>
|
<section>
|
||||||
<h3>Password</h3>
|
<h3>Password</h3>
|
||||||
<p class="mb-8">Use the following button to request an email with a password reset link:</p>
|
<p class="mb-6">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 ([example])"
|
||||||
|
) 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 %>
|
||||||
|
|||||||
@@ -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-8">
|
<p class="mb-6">
|
||||||
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">
|
||||||
|
|||||||
@@ -1,43 +1,47 @@
|
|||||||
<div data-controller="settings--nostr-pubkey"
|
<section>
|
||||||
data-settings--nostr-pubkey-user-address-value="<%= current_user.address %>"
|
<h3>Nostr</h3>
|
||||||
data-settings--nostr-pubkey-site-value="<%= Setting.accounts_domain %>"
|
<h4 class="mb-0">Public Key</h4>
|
||||||
data-settings--nostr-pubkey-shared-secret-value="<%= session[:shared_secret] %>"
|
<div data-controller="settings--nostr-pubkey"
|
||||||
data-settings--nostr-pubkey-pubkey-hex-value="<%= current_user.nostr_pubkey %>">
|
data-settings--nostr-pubkey-user-address-value="<%= current_user.address %>"
|
||||||
<section class="mb-8 sm:mb-12">
|
data-settings--nostr-pubkey-site-value="<%= Setting.accounts_domain %>"
|
||||||
<h3>Nostr</h3>
|
data-settings--nostr-pubkey-shared-secret-value="<%= session[:shared_secret] %>"
|
||||||
<h4 class="mb-0">
|
data-settings--nostr-pubkey-pubkey-hex-value="<%= current_user.nostr_pubkey %>">
|
||||||
Public Key
|
|
||||||
</h4>
|
<p class="<%= current_user.nostr_pubkey.present? ? '' : 'hidden' %> mt-2 flex gap-1">
|
||||||
<p class="<%= current_user.nostr_pubkey.present? ? '' : 'hidden' %> mt-2 flex gap-x-1">
|
|
||||||
<input type="text" value="<%= current_user.nostr_pubkey_bech32 %>" disabled
|
<input type="text" value="<%= current_user.nostr_pubkey_bech32 %>" disabled
|
||||||
data-settings--nostr-pubkey-target="pubkeyBech32Input"
|
data-settings--nostr-pubkey-target="pubkeyBech32Input"
|
||||||
name="nostr_public_key" class="w-full" />
|
name="nostr_public_key" class="relative grow" />
|
||||||
<%= link_to nostr_pubkey_settings_path,
|
<%= link_to nostr_pubkey_settings_path,
|
||||||
class: 'btn-md btn-outline relative grow-0 shrink-0 text-red-700',
|
class: 'btn-md btn-outline text-red-700 relative shrink-0',
|
||||||
data: { turbo_method: :delete, turbo_confirm: 'Are you sure?' } do %>
|
data: { turbo_method: :delete, turbo_confirm: 'Are you sure?' } do %>
|
||||||
Remove
|
Remove
|
||||||
<% end %>
|
<% end %>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<% if current_user.nostr_pubkey.present? %>
|
<% if current_user.nostr_pubkey.present? %>
|
||||||
<!-- <div> -->
|
<div class="rounded-md bg-blue-50 p-4">
|
||||||
<!-- Pubkey present -->
|
<div class="flex">
|
||||||
<!-- </div> -->
|
<div class="flex-shrink-0">
|
||||||
|
<svg class="h-5 w-5 text-blue-400" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||||||
|
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a.75.75 0 000 1.5h.253a.25.25 0 01.244.304l-.459 2.066A1.75 1.75 0 0010.747 15H11a.75.75 0 000-1.5h-.253a.25.25 0 01-.244-.304l.459-2.066A1.75 1.75 0 009.253 9H9z" clip-rule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="ml-3 flex-1">
|
||||||
|
<p class="text-sm text-blue-800">
|
||||||
|
Your user address <strong><%= current_user.address %></strong> is
|
||||||
|
also a Nostr address now. Use your favorite Nostr app, or for
|
||||||
|
example <a href="http://metadata.nostr.com" target="_blank"
|
||||||
|
class="underline">metadata.nostr.com</a>, to add this
|
||||||
|
<strong>NIP-05</strong> address to your public profile.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<% else %>
|
<% else %>
|
||||||
<p class="my-4">
|
<p class="my-4">
|
||||||
Verify your Nostr public key with us in order to enable Nostr-specific
|
If you use any apps on the Nostr network, you can verify your public key
|
||||||
features for your account:
|
with us in order to enable Nostr-specific features for your account.
|
||||||
</p>
|
</p>
|
||||||
<ul class="list-disc list-inside">
|
|
||||||
<li>Log in with Nostr (no password needed)</li>
|
|
||||||
<li>Verified Nostr address</li>
|
|
||||||
<% if Setting.lndhub_enabled? %>
|
|
||||||
<li>Receive zaps in your Lightning account</li>
|
|
||||||
<% end %>
|
|
||||||
<% if Setting.nostr_relay_url.present? %>
|
|
||||||
<li>Publish notes on <%= link_to "our relay", Setting.nostr_relay_url_http, class: "ks-text-link", target: "_blank" %></li>
|
|
||||||
<% end %>
|
|
||||||
</ul>
|
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<div data-settings--nostr-pubkey-target="noExtension"
|
<div data-settings--nostr-pubkey-target="noExtension"
|
||||||
@@ -54,8 +58,8 @@
|
|||||||
</h3>
|
</h3>
|
||||||
<div class="mt-2 mb-0 text-sm text-blue-800">
|
<div class="mt-2 mb-0 text-sm text-blue-800">
|
||||||
<p>
|
<p>
|
||||||
We recommend Alby, which you can also use a wallet for your
|
We recommend Alby, which you can also use for your Lightning
|
||||||
Lightning account.
|
Wallet.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-4">
|
<div class="mt-4">
|
||||||
@@ -82,11 +86,5 @@
|
|||||||
</button>
|
</button>
|
||||||
</p>
|
</p>
|
||||||
<% end %>
|
<% end %>
|
||||||
</section>
|
</div>
|
||||||
|
</section>
|
||||||
<% if current_user.nostr_pubkey.present? %>
|
|
||||||
<%= turbo_frame_tag "nostr_user_metadata", src: nostr_user_metadata_settings_path do %>
|
|
||||||
<p>Loading...</p>
|
|
||||||
<% end %>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
<%= turbo_frame_tag "nostr_user_metadata" do %>
|
|
||||||
<section>
|
|
||||||
<h3>Relays</h3>
|
|
||||||
<%= render Settings::NostrRelayStatusComponent.new(
|
|
||||||
nip65_event: @nip65_event
|
|
||||||
) %>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section>
|
|
||||||
<h3>Profile</h3>
|
|
||||||
<%= render Settings::NostrProfileStatusComponent.new(
|
|
||||||
profile_event: @profile,
|
|
||||||
user_address: current_user.address
|
|
||||||
) %>
|
|
||||||
<div class="mt-8" data-controller="modal" data-action="keydown.esc->modal#close">
|
|
||||||
<button data-action="click->modal#open" class="btn-md btn-blue w-full sm:w-auto">
|
|
||||||
Edit profile
|
|
||||||
</button>
|
|
||||||
<%= render ModalComponent.new(show_close_button: false) do %>
|
|
||||||
<%= render Settings::NostrEditProfileComponent.new(
|
|
||||||
user: current_user,
|
|
||||||
profile_event: @profile
|
|
||||||
) %>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<% end %>
|
|
||||||
@@ -19,6 +19,12 @@
|
|||||||
active: @settings_section.to_s == "email"
|
active: @settings_section.to_s == "email"
|
||||||
) %>
|
) %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
<% if Setting.lndhub_enabled %>
|
||||||
|
<%= render SidenavLinkComponent.new(
|
||||||
|
name: "Lightning", path: setting_path(:lightning), icon: "zap",
|
||||||
|
active: @settings_section.to_s == "lightning"
|
||||||
|
) %>
|
||||||
|
<% end %>
|
||||||
<% if Setting.remotestorage_enabled? &&
|
<% if Setting.remotestorage_enabled? &&
|
||||||
Flipper.enabled?(:remotestorage, current_user) %>
|
Flipper.enabled?(:remotestorage, current_user) %>
|
||||||
<%= render SidenavLinkComponent.new(
|
<%= render SidenavLinkComponent.new(
|
||||||
@@ -26,12 +32,6 @@
|
|||||||
active: @settings_section.to_s == "remotestorage"
|
active: @settings_section.to_s == "remotestorage"
|
||||||
) %>
|
) %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% if Setting.lndhub_enabled %>
|
|
||||||
<%= render SidenavLinkComponent.new(
|
|
||||||
name: "Lightning", path: setting_path(:lightning), icon: "zap",
|
|
||||||
active: @settings_section.to_s == "lightning"
|
|
||||||
) %>
|
|
||||||
<% end %>
|
|
||||||
<% if Setting.nostr_enabled %>
|
<% if Setting.nostr_enabled %>
|
||||||
<%= render SidenavLinkComponent.new(
|
<%= render SidenavLinkComponent.new(
|
||||||
name: "Nostr", path: setting_path(:nostr), icon: "nostrich-head",
|
name: "Nostr", path: setting_path(:nostr), icon: "nostrich-head",
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
<div>
|
|
||||||
<%= profile.inspect %>
|
|
||||||
</div>
|
|
||||||
@@ -65,16 +65,16 @@ Rails.application.routes.draw do
|
|||||||
post 'reset_email_password'
|
post 'reset_email_password'
|
||||||
post 'set_nostr_pubkey'
|
post 'set_nostr_pubkey'
|
||||||
delete 'nostr_pubkey', to: 'settings#remove_nostr_pubkey'
|
delete 'nostr_pubkey', to: 'settings#remove_nostr_pubkey'
|
||||||
get 'fetch_nostr_user_metadata', as: 'nostr_user_metadata'
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
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 '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'
|
||||||
|
|
||||||
|
|||||||
5
db/migrate/20240922205634_add_pgp_fpr_to_users.rb
Normal file
5
db/migrate/20240922205634_add_pgp_fpr_to_users.rb
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
class AddPgpFprToUsers < ActiveRecord::Migration[7.1]
|
||||||
|
def change
|
||||||
|
add_column :users, :pgp_fpr, :string
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -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_06_07_123654) do
|
ActiveRecord::Schema[7.1].define(version: 2024_09_22_205634) 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,6 +132,7 @@ ActiveRecord::Schema[7.1].define(version: 2024_06_07_123654) 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
|
||||||
|
|||||||
13
db/seeds/admin.asc
Normal file
13
db/seeds/admin.asc
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
-----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-----
|
||||||
@@ -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 ].each do |name|
|
%w[ admin service_enabled nostr_key pgp_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 ].each do |name|
|
%w[ admin service_enabled nostr_key pgp_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
|
||||||
|
|||||||
8
schemas/ldap/pgp_key.ldif
Normal file
8
schemas/ldap/pgp_key.ldif
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
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 )
|
||||||
@@ -14,6 +14,7 @@ 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
|
||||||
@@ -55,4 +56,44 @@ 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
|
||||||
|
|||||||
@@ -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"
|
uid: user.cn, ou: user.ou, display_name: "Mark", pgp_key: nil
|
||||||
})
|
})
|
||||||
allow_any_instance_of(User).to receive(:avatar).and_return(avatar_base64)
|
allow_any_instance_of(User).to receive(:avatar).and_return(avatar_base64)
|
||||||
|
|
||||||
|
|||||||
@@ -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(CreateAccount).to receive(:call)
|
expect(UserManager::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(CreateAccount).to receive(:call)
|
expect(UserManager::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",
|
||||||
|
|||||||
11
spec/fixtures/files/pgp_key_invalid.asc
vendored
Normal file
11
spec/fixtures/files/pgp_key_invalid.asc
vendored
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
-----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-----
|
||||||
16
spec/fixtures/files/pgp_key_valid_alice.asc
vendored
Normal file
16
spec/fixtures/files/pgp_key_valid_alice.asc
vendored
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
-----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-----
|
||||||
13
spec/fixtures/files/pgp_key_valid_jimmy.asc
vendored
Normal file
13
spec/fixtures/files/pgp_key_valid_jimmy.asc
vendored
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
-----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-----
|
||||||
BIN
spec/fixtures/files/pgp_key_valid_jimmy.pem
vendored
Normal file
BIN
spec/fixtures/files/pgp_key_valid_jimmy.pem
vendored
Normal file
Binary file not shown.
@@ -1,20 +1,16 @@
|
|||||||
require 'rails_helper'
|
require 'rails_helper'
|
||||||
|
|
||||||
RSpec.describe User, type: :model do
|
RSpec.describe User, type: :model do
|
||||||
let(:user) { create :user, cn: "philipp" }
|
let(:user) { create :user, cn: "philipp", ou: "kosmos.org", email: "philipp@example.com" }
|
||||||
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("jimmy@kosmos.org")
|
expect(user.address).to eq("philipp@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
|
||||||
@@ -32,7 +28,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("jimmy@kosmos.org")
|
expect(user.mastodon_address).to eq("philipp@kosmos.org")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -42,7 +38,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("jimmy@kosmos.social")
|
expect(user.mastodon_address).to eq("philipp@kosmos.social")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -239,7 +235,7 @@ RSpec.describe User, type: :model do
|
|||||||
|
|
||||||
describe "#nostr_pubkey" do
|
describe "#nostr_pubkey" do
|
||||||
before do
|
before do
|
||||||
allow_any_instance_of(User).to receive(:ldap_entry)
|
allow(user).to receive(:ldap_entry)
|
||||||
.and_return({ nostr_key: "07e188a1ff87ce171d517b8ed2bb7a31b1d3453a0db3b15379ec07b724d232f3" })
|
.and_return({ nostr_key: "07e188a1ff87ce171d517b8ed2bb7a31b1d3453a0db3b15379ec07b724d232f3" })
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -250,7 +246,7 @@ RSpec.describe User, type: :model do
|
|||||||
|
|
||||||
describe "#nostr_pubkey_bech32" do
|
describe "#nostr_pubkey_bech32" do
|
||||||
before do
|
before do
|
||||||
allow_any_instance_of(User).to receive(:ldap_entry)
|
allow(user).to receive(:ldap_entry)
|
||||||
.and_return({ nostr_key: "07e188a1ff87ce171d517b8ed2bb7a31b1d3453a0db3b15379ec07b724d232f3" })
|
.and_return({ nostr_key: "07e188a1ff87ce171d517b8ed2bb7a31b1d3453a0db3b15379ec07b724d232f3" })
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -258,4 +254,73 @@ 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
|
||||||
|
|||||||
84
spec/requests/web_key_directory_spec.rb
Normal file
84
spec/requests/web_key_directory_spec.rb
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe "OpenPGP Web Key Directory", type: :request do
|
||||||
|
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 .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
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
require 'rails_helper'
|
require 'rails_helper'
|
||||||
|
|
||||||
RSpec.describe CreateAccount, type: :model do
|
RSpec.describe UserManager::CreateAccount, type: :model do
|
||||||
describe "#create_user_in_database" do
|
describe "#create_user_in_database" do
|
||||||
let(:service) { CreateAccount.new(account: {
|
let(:service) { described_class.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 CreateAccount, type: :model do
|
|||||||
|
|
||||||
describe "#update_invitation" do
|
describe "#update_invitation" do
|
||||||
let(:invitation) { create :invitation }
|
let(:invitation) { create :invitation }
|
||||||
let(:service) { CreateAccount.new(account: {
|
let(:service) { described_class.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 CreateAccount, type: :model do
|
|||||||
describe "#add_ldap_document" do
|
describe "#add_ldap_document" do
|
||||||
include ActiveJob::TestHelper
|
include ActiveJob::TestHelper
|
||||||
|
|
||||||
let(:service) { CreateAccount.new(account: {
|
let(:service) { described_class.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 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) { CreateAccount.new(account: {
|
let(:service) { described_class.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 CreateAccount, type: :model do
|
|||||||
describe "#create_lndhub_account" do
|
describe "#create_lndhub_account" do
|
||||||
include ActiveJob::TestHelper
|
include ActiveJob::TestHelper
|
||||||
|
|
||||||
let(:service) { CreateAccount.new(account: {
|
let(:service) { described_class.new(account: {
|
||||||
username: 'halfinney', email: 'halfinney@example.com',
|
username: 'halfinney', email: 'halfinney@example.com',
|
||||||
password: 'bright-ideas-in-winter'
|
password: 'bright-ideas-in-winter'
|
||||||
})}
|
})}
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
require 'rails_helper'
|
require 'rails_helper'
|
||||||
|
|
||||||
RSpec.describe CreateInvitations, type: :model do
|
RSpec.describe UserManager::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
|
||||||
CreateInvitations.call(user: user, amount: 5)
|
described_class.call(user: user, amount: 5)
|
||||||
end
|
end
|
||||||
|
|
||||||
after(:each) { clear_enqueued_jobs }
|
after(:each) { clear_enqueued_jobs }
|
||||||
@@ -28,7 +28,7 @@ RSpec.describe CreateInvitations, type: :model do
|
|||||||
|
|
||||||
describe "#call with notification disabled" do
|
describe "#call with notification disabled" do
|
||||||
before do
|
before do
|
||||||
CreateInvitations.call(user: user, amount: 3, notify: false)
|
described_class.call(user: user, amount: 3, notify: false)
|
||||||
end
|
end
|
||||||
|
|
||||||
after(:each) { clear_enqueued_jobs }
|
after(:each) { clear_enqueued_jobs }
|
||||||
74
spec/services/user_manager/update_pgp_key_spec.rb
Normal file
74
spec/services/user_manager/update_pgp_key_spec.rb
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
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
|
||||||
Reference in New Issue
Block a user