Compare commits
160 Commits
v0.9.0
...
feature/no
| Author | SHA1 | Date | |
|---|---|---|---|
|
9866cd0404
|
|||
|
10d29b6fab
|
|||
|
6f8f60a9e2
|
|||
|
c1b4665706
|
|||
|
5447150d4d
|
|||
|
bf26703b2d
|
|||
|
21c6264ea9
|
|||
|
79ef9fa6d5
|
|||
|
04a9061663
|
|||
|
5283f6fce7
|
|||
|
a08a4746f7
|
|||
|
9e3652479b
|
|||
|
011386fb8d
|
|||
|
4d77f5d38c
|
|||
|
64de4deddd
|
|||
|
8f7994d82e
|
|||
|
a7d0e71ab6
|
|||
|
27d9f73c61
|
|||
|
ed3de8b16f
|
|||
|
d7b4c67953
|
|||
| 7489d4a32f | |||
|
ac77e5b7c1
|
|||
|
e544c28105
|
|||
|
4909dac5c2
|
|||
| 3cf4348695 | |||
|
af3da0a26c
|
|||
|
2d32320c7d
|
|||
|
fc2bec6246
|
|||
|
5addd25186
|
|||
|
215d178e69
|
|||
|
5474bf66e7
|
|||
|
ef2a37e2bf
|
|||
|
0e3180602c
|
|||
|
15e2f9b962
|
|||
|
4ae10c9b53
|
|||
| 45137e0cfe | |||
|
717fe93104
|
|||
|
fdac789ccb
|
|||
|
9355dab6b6
|
|||
|
f3676949d2
|
|||
|
79952b73c5
|
|||
| 17c419403e | |||
|
6d06312a5c
|
|||
|
acb399b0b7
|
|||
|
bf20b6467e
|
|||
|
b91d90d75c
|
|||
|
3284bbf6ca
|
|||
|
171b84ee81
|
|||
|
54b01dd282
|
|||
|
e08ea64f47
|
|||
|
8cc2c9554f
|
|||
| 32dff9c67f | |||
|
126b8b20e0
|
|||
|
5abf69f356
|
|||
|
210a69bd9b
|
|||
|
bbed3cd367
|
|||
|
7943da0f17
|
|||
| 620167eedf | |||
|
e077debfc2
|
|||
|
531b2c3002
|
|||
|
6d2bc729b8
|
|||
|
2630ec2af4
|
|||
| daed5c1eea | |||
| 2e9429bb32 | |||
|
37c15c7a62
|
|||
|
01ecea74ff
|
|||
|
f401a03590
|
|||
|
fff6dea100
|
|||
|
48ab96dda9
|
|||
|
7ac3130c18
|
|||
|
cbfa148051
|
|||
|
87d900b627
|
|||
|
926dc06294
|
|||
|
00b73b06d7
|
|||
|
0daac33915
|
|||
|
0e472bc311
|
|||
| 40b34d0935 | |||
|
61cb8f4941
|
|||
|
433ac4dc8e
|
|||
|
62fe0d8fac
|
|||
|
2a675fd135
|
|||
|
c2c3ebc2e1
|
|||
|
5a5c316c14
|
|||
| f0d5457ec1 | |||
|
5588e3b3e8
|
|||
|
8949d76d26
|
|||
| 8bc9bbdc33 | |||
| d6d09b57b8 | |||
|
1685d6ecf8
|
|||
|
5348a229a6
|
|||
|
bad3b7a2be
|
|||
|
b541e95bb7
|
|||
|
3f43fe8101
|
|||
|
231dfc8404
|
|||
|
eeb9b0a331
|
|||
|
08e783d185
|
|||
|
fa5dc8ca46
|
|||
|
bc34e9c5e0
|
|||
| f388bd0237 | |||
|
48041630ca
|
|||
|
2d1ff29eca
|
|||
| 46fa42e387 | |||
|
c6c5d80fb4
|
|||
|
c0f4e7925e
|
|||
|
49d24990b4
|
|||
|
619bd954b7
|
|||
|
e27c64b5f1
|
|||
|
b36baf26eb
|
|||
|
adedaa5f7b
|
|||
|
596ed7fccc
|
|||
|
5685e1b7bc
|
|||
|
c3b82fc2a9
|
|||
|
77e2fe5792
|
|||
|
bc43082839
|
|||
|
b09225543b
|
|||
|
f2507409a3
|
|||
|
46b4723999
|
|||
|
3f90a011c4
|
|||
|
3ba333e802
|
|||
| d9dff3e872 | |||
| 6ddeacb779 | |||
|
78aff3d796
|
|||
|
8f600f44bd
|
|||
|
819ecf6ad8
|
|||
|
945eaba5e1
|
|||
|
22d362e1a0
|
|||
|
d4e67a830c
|
|||
|
670b2da1ef
|
|||
|
ed5c5b3081
|
|||
| 4ee6bfddfa | |||
|
8b60890061
|
|||
|
0367450c4b
|
|||
|
e6f5623c7f
|
|||
| 367f566ccb | |||
|
80e69df75c
|
|||
|
02af69b055
|
|||
|
5d459e7e7d
|
|||
| 51a3cb60ec | |||
| 43c57c128f | |||
|
5a3adba603
|
|||
|
3715cb518b
|
|||
|
2c9ecc1fef
|
|||
|
095747e89b
|
|||
|
2130369604
|
|||
|
c996351930
|
|||
| 8b897168cc | |||
|
4217ba52e0
|
|||
|
de20931d30
|
|||
|
8de0a2e26e
|
|||
|
06521d1c34
|
|||
|
38b3d68fd5
|
|||
|
7f2df3b025
|
|||
|
da22a9d448
|
|||
|
e3b96d5cff
|
|||
|
c36cf5eee6
|
|||
|
54220019bb
|
|||
|
079ee8833c
|
|||
|
26d613bdca
|
|||
|
69b3afb8f7
|
|||
|
fee951c05c
|
@@ -29,8 +29,10 @@
|
||||
|
||||
#
|
||||
# Service Integrations
|
||||
# (sorted alphabetically by service name)
|
||||
#
|
||||
|
||||
# BTCPAY_PUBLIC_URL='https://btcpay.example.com'
|
||||
# BTCPAY_API_URL='http://localhost:23001/api/v1'
|
||||
# BTCPAY_STORE_ID=''
|
||||
# BTCPAY_AUTH_TOKEN=''
|
||||
@@ -57,8 +59,13 @@
|
||||
# LNDHUB_PG_PASSWORD=''
|
||||
|
||||
# MASTODON_PUBLIC_URL='https://kosmos.social'
|
||||
# MASTODON_ADDRESS_DOMAIN='https://kosmos.org'
|
||||
|
||||
# MEDIAWIKI_PUBLIC_URL='https://wiki.kosmos.org'
|
||||
|
||||
# NOSTR_PRIVATE_KEY='123456abcdef...'
|
||||
# NOSTR_PUBLIC_KEY='123456abcdef...'
|
||||
# NOSTR_RELAY_URL='wss://nostr.kosmos.org'
|
||||
|
||||
# RS_STORAGE_URL='https://storage.kosmos.org'
|
||||
# RS_REDIS_URL='redis://localhost:6379/2'
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
PRIMARY_DOMAIN=kosmos.org
|
||||
AKKOUNTS_DOMAIN=accounts.kosmos.org
|
||||
|
||||
REDIS_URL='redis://localhost:6379/0'
|
||||
|
||||
BTCPAY_PUBLIC_URL='https://btcpay.example.com'
|
||||
BTCPAY_API_URL='http://btcpay.example.com/api/v1'
|
||||
BTCPAY_STORE_ID='123456'
|
||||
|
||||
@@ -10,10 +12,15 @@ DISCOURSE_CONNECT_SECRET='discourse_connect_ftw'
|
||||
|
||||
EJABBERD_API_URL='http://xmpp.example.com/api'
|
||||
|
||||
MASTODON_PUBLIC_URL='http://example.social'
|
||||
|
||||
LNDHUB_API_URL='http://localhost:3026'
|
||||
LNDHUB_PUBLIC_URL='https://lndhub.kosmos.org'
|
||||
LNDHUB_PUBLIC_KEY='024cd3be18617f39cf645851e3ba63f51fc13f0bb09e3bb25e6fd4de556486d946'
|
||||
|
||||
NOSTR_PRIVATE_KEY='7c3ef7e448505f0615137af38569d01807d3b05b5005d5ecf8aaafcd40323cea'
|
||||
NOSTR_PUBLIC_KEY='bdd76ce2934b2f591f9fad2ebe9da18f20d2921de527494ba00eeaa0a0efadcf'
|
||||
|
||||
RS_STORAGE_URL='https://storage.kosmos.org'
|
||||
RS_REDIS_URL='redis://localhost:6379/1'
|
||||
|
||||
|
||||
13
Dockerfile
13
Dockerfile
@@ -1,18 +1,11 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
FROM debian:bullseye-slim as base
|
||||
FROM ruby:3.3.4
|
||||
|
||||
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
||||
|
||||
# TODO Remove when upstream Ruby works properly on Apple silicon
|
||||
RUN apt update && apt install -y build-essential wget autoconf libpq-dev pkg-config
|
||||
RUN wget https://github.com/postmodern/ruby-install/releases/download/v0.9.3/ruby-install-0.9.3.tar.gz \
|
||||
&& tar -xzvf ruby-install-0.9.3.tar.gz \
|
||||
&& cd ruby-install-0.9.3/ \
|
||||
&& make install
|
||||
RUN ruby-install -p https://github.com/ruby/ruby/pull/9371.diff ruby 3.3.0
|
||||
ENV PATH="/opt/rubies/ruby-3.3.0/bin:${PATH}"
|
||||
RUN apt-get update -qq && apt-get install -y --no-install-recommends curl \
|
||||
ldap-utils tini libvips
|
||||
|
||||
RUN apt-get install -y --no-install-recommends curl ldap-utils tini libvips
|
||||
RUN curl -fsSL https://deb.nodesource.com/setup_lts.x | bash -
|
||||
RUN apt-get update && apt-get install -y nodejs
|
||||
|
||||
|
||||
4
Gemfile
4
Gemfile
@@ -61,8 +61,8 @@ gem "sentry-rails"
|
||||
# Services
|
||||
gem 'discourse_api'
|
||||
gem "lnurl"
|
||||
gem 'manifique'
|
||||
gem 'nostr'
|
||||
gem 'manifique', '~> 1.1.0'
|
||||
gem 'nostr', '~> 0.6.0'
|
||||
|
||||
group :development, :test do
|
||||
# Use sqlite3 as the database for Active Record
|
||||
|
||||
12
Gemfile.lock
12
Gemfile.lock
@@ -155,7 +155,7 @@ GEM
|
||||
ruby2_keywords
|
||||
e2mmap (0.1.0)
|
||||
ecdsa (1.2.0)
|
||||
ecdsa_ext (0.5.0)
|
||||
ecdsa_ext (0.5.1)
|
||||
ecdsa (~> 1.2.0)
|
||||
erubi (1.12.0)
|
||||
et-orbi (1.2.7)
|
||||
@@ -245,7 +245,7 @@ GEM
|
||||
net-imap
|
||||
net-pop
|
||||
net-smtp
|
||||
manifique (1.0.1)
|
||||
manifique (1.1.0)
|
||||
faraday (~> 2.9.0)
|
||||
faraday-follow_redirects (= 0.3.0)
|
||||
nokogiri (~> 1.16.0)
|
||||
@@ -278,9 +278,9 @@ GEM
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.16.0-x86_64-linux)
|
||||
racc (~> 1.4)
|
||||
nostr (0.5.0)
|
||||
nostr (0.6.0)
|
||||
bech32 (~> 1.4)
|
||||
bip-schnorr (~> 0.6)
|
||||
bip-schnorr (~> 0.7)
|
||||
ecdsa (~> 1.2)
|
||||
event_emitter (~> 0.2)
|
||||
faye-websocket (~> 0.11)
|
||||
@@ -515,9 +515,9 @@ DEPENDENCIES
|
||||
listen (~> 3.2)
|
||||
lnurl
|
||||
lockbox
|
||||
manifique
|
||||
manifique (~> 1.1.0)
|
||||
net-ldap
|
||||
nostr
|
||||
nostr (~> 0.6.0)
|
||||
pagy (~> 6.0, >= 6.0.2)
|
||||
pg (~> 1.5)
|
||||
puma (~> 4.1)
|
||||
|
||||
@@ -32,11 +32,21 @@
|
||||
focus:ring-blue-400 focus:ring-opacity-75;
|
||||
}
|
||||
|
||||
.btn-emerald {
|
||||
@apply bg-emerald-500 hover:bg-emerald-600 text-white
|
||||
focus:ring-emerald-400 focus:ring-opacity-75;
|
||||
}
|
||||
|
||||
.btn-red {
|
||||
@apply bg-red-600 hover:bg-red-700 text-white
|
||||
focus:ring-red-500 focus:ring-opacity-75;
|
||||
}
|
||||
|
||||
.btn-outline-purple {
|
||||
@apply border-2 border-purple-500 hover:bg-purple-100
|
||||
focus:ring-purple-400 focus:ring-opacity-75;
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
@apply bg-gray-100 hover:bg-gray-200 text-gray-400
|
||||
focus:ring-gray-300 focus:ring-opacity-75;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
@layer components {
|
||||
.services > div > a {
|
||||
background-image: linear-gradient(110deg, rgba(255,255,255,0.99) 0, rgba(255,255,255,0.88) 100%);
|
||||
background-image: linear-gradient(110deg, rgba(255,255,255,0.99) 20%, rgba(255,255,255,0.88) 100%);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
) do %>
|
||||
<%= method("#{@type}_field").call :setting, @key,
|
||||
value: Setting.public_send(@key),
|
||||
placeholder: @placeholder,
|
||||
data: {
|
||||
:'default-value' => Setting.get_field(@key)[:default]
|
||||
},
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
module FormElements
|
||||
class FieldsetResettableSettingComponent < ViewComponent::Base
|
||||
def initialize(tag: "li", key:, type: :text, title:, description: nil)
|
||||
def initialize(tag: "li", key:, type: :text, title:, description: nil, placeholder: nil)
|
||||
@tag = tag
|
||||
@positioning = :vertical
|
||||
@title = title
|
||||
@@ -10,6 +10,7 @@ module FormElements
|
||||
@key = key.to_sym
|
||||
@type = type
|
||||
@resettable = is_resettable?(@key)
|
||||
@placeholder = placeholder
|
||||
end
|
||||
|
||||
def is_resettable?(key)
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<div class="flex flex-col">
|
||||
<label class="font-bold mb-1"><%= @title %></label>
|
||||
<% if @description.present? %>
|
||||
<p class="text-gray-500"><%= @descripton %></p>
|
||||
<p class="text-gray-500"><%= @description %></p>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="relative ml-4 inline-flex flex-shrink-0">
|
||||
|
||||
@@ -12,7 +12,7 @@ module FormElements
|
||||
@enabled = enabled
|
||||
@input_enabled = input_enabled
|
||||
@title = title
|
||||
@descripton = description
|
||||
@description = description
|
||||
@button_text = @enabled ? "Switch off" : "Switch on"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<main class="w-full max-w-6xl mx-auto pb-12 px-4 md:px-6 lg:px-8">
|
||||
<div class="bg-white rounded-lg shadow">
|
||||
<div class="md:min-h-[50vh] bg-white rounded-lg shadow">
|
||||
<div class="px-6 sm:px-12 pt-2 sm:pt-4">
|
||||
<%= render partial: @tabnav_partial %>
|
||||
</div>
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
|
||||
<!-- Modal Container -->
|
||||
<div data-modal-target="container"
|
||||
class="max-h-screen w-auto max-w-lg relative
|
||||
class="relative m-4 max-h-screen w-auto max-w-full
|
||||
hidden animate-scale-in fixed inset-0 overflow-y-auto flex items-center justify-center">
|
||||
<!-- Modal Card -->
|
||||
<div class="m-1 bg-white rounded shadow">
|
||||
|
||||
@@ -34,6 +34,8 @@ class NotificationComponent < ViewComponent::Base
|
||||
'alert-octagon'
|
||||
when 'alert'
|
||||
'alert-octagon'
|
||||
when 'warning'
|
||||
'alert-octagon'
|
||||
else
|
||||
'info'
|
||||
end
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
<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>
|
||||
28
app/components/settings/nostr_edit_profile_component.rb
Normal file
28
app/components/settings/nostr_edit_profile_component.rb
Normal file
@@ -0,0 +1,28 @@
|
||||
# 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
|
||||
@@ -0,0 +1,21 @@
|
||||
<% @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 %>
|
||||
53
app/components/settings/nostr_profile_status_component.rb
Normal file
53
app/components/settings/nostr_profile_status_component.rb
Normal file
@@ -0,0 +1,53 @@
|
||||
# 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
|
||||
@@ -0,0 +1,18 @@
|
||||
<%= 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 %>
|
||||
34
app/components/settings/nostr_relay_status_component.rb
Normal file
34
app/components/settings/nostr_relay_status_component.rb
Normal file
@@ -0,0 +1,34 @@
|
||||
# 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
|
||||
8
app/components/status_text_component.html.erb
Normal file
8
app/components/status_text_component.html.erb
Normal file
@@ -0,0 +1,8 @@
|
||||
<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>
|
||||
7
app/components/status_text_component.rb
Normal file
7
app/components/status_text_component.rb
Normal file
@@ -0,0 +1,7 @@
|
||||
class StatusTextComponent < ViewComponent::Base
|
||||
def initialize(text:, icon_name:, icon_color:)
|
||||
@text = text
|
||||
@icon_name = icon_name
|
||||
@icon_color = icon_color
|
||||
end
|
||||
end
|
||||
@@ -3,18 +3,16 @@ class Admin::DonationsController < Admin::BaseController
|
||||
before_action :set_current_section, only: [:index, :show, :new, :edit]
|
||||
|
||||
# GET /donations
|
||||
# GET /donations.json
|
||||
def index
|
||||
@pagy, @donations = pagy(Donation.all.order('created_at desc'))
|
||||
@pagy, @donations = pagy(Donation.completed.order('paid_at desc'))
|
||||
|
||||
@stats = {
|
||||
overall_sats: @donations.all.sum("amount_sats"),
|
||||
donor_count: Donation.distinct.count(:user_id)
|
||||
overall_sats: @donations.sum("amount_sats"),
|
||||
donor_count: Donation.completed.count(:user_id)
|
||||
}
|
||||
end
|
||||
|
||||
# GET /donations/1
|
||||
# GET /donations/1.json
|
||||
def show
|
||||
end
|
||||
|
||||
@@ -28,54 +26,41 @@ class Admin::DonationsController < Admin::BaseController
|
||||
end
|
||||
|
||||
# POST /donations
|
||||
# POST /donations.json
|
||||
def create
|
||||
@donation = Donation.new(donation_params)
|
||||
|
||||
respond_to do |format|
|
||||
if @donation.save
|
||||
format.html do
|
||||
redirect_to admin_donation_url(@donation), flash: {
|
||||
success: 'Donation was successfully created.'
|
||||
}
|
||||
end
|
||||
format.json { render :show, status: :created, location: @donation }
|
||||
else
|
||||
format.html { render :new, status: :unprocessable_entity }
|
||||
format.json { render json: @donation.errors, status: :unprocessable_entity }
|
||||
end
|
||||
if @donation.paid_at == nil
|
||||
@donation.errors.add(:paid_at, message: "is required")
|
||||
render :new, status: :unprocessable_entity and return
|
||||
end
|
||||
|
||||
if @donation.save
|
||||
redirect_to admin_donation_url(@donation), flash: {
|
||||
success: 'Donation was successfully created.'
|
||||
}
|
||||
else
|
||||
render :new, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
# PATCH/PUT /donations/1
|
||||
# PATCH/PUT /donations/1.json
|
||||
# PUT /donations/1
|
||||
def update
|
||||
respond_to do |format|
|
||||
if @donation.update(donation_params)
|
||||
format.html do
|
||||
redirect_to admin_donation_url(@donation), flash: {
|
||||
success: 'Donation was successfully updated.'
|
||||
}
|
||||
end
|
||||
format.json { render :show, status: :ok, location: @donation }
|
||||
else
|
||||
format.html { render :edit, status: :unprocessable_entity }
|
||||
format.json { render json: @donation.errors, status: :unprocessable_entity }
|
||||
end
|
||||
if @donation.update(donation_params)
|
||||
redirect_to admin_donation_url(@donation), flash: {
|
||||
success: 'Donation was successfully updated.'
|
||||
}
|
||||
else
|
||||
render :edit, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
# DELETE /donations/1
|
||||
# DELETE /donations/1.json
|
||||
def destroy
|
||||
@donation.destroy
|
||||
respond_to do |format|
|
||||
format.html do redirect_to admin_donations_url, flash: {
|
||||
success: 'Donation was successfully destroyed.'
|
||||
}
|
||||
end
|
||||
format.json { head :no_content }
|
||||
end
|
||||
|
||||
redirect_to admin_donations_url, flash: {
|
||||
success: 'Donation was successfully destroyed.'
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
@@ -86,7 +71,10 @@ class Admin::DonationsController < Admin::BaseController
|
||||
|
||||
# Only allow a list of trusted parameters through.
|
||||
def donation_params
|
||||
params.require(:donation).permit(:user_id, :amount_sats, :amount_eur, :amount_usd, :public_name, :paid_at)
|
||||
params.require(:donation).permit(
|
||||
:user_id, :donation_method,
|
||||
:amount_sats, :fiat_amount, :fiat_currency,
|
||||
:public_name, :paid_at)
|
||||
end
|
||||
|
||||
def set_current_section
|
||||
|
||||
@@ -9,4 +9,12 @@ class Admin::Settings::RegistrationsController < Admin::SettingsController
|
||||
success: "Settings saved"
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def setting_params
|
||||
params.require(:setting).permit([
|
||||
:reserved_usernames, default_services: []
|
||||
])
|
||||
end
|
||||
end
|
||||
|
||||
@@ -9,11 +9,12 @@ class Admin::SettingsController < Admin::BaseController
|
||||
changed_keys = []
|
||||
|
||||
setting_params.keys.each do |key|
|
||||
next if setting_params[key].nil? ||
|
||||
(Setting.send(key).to_s == setting_params[key].strip)
|
||||
next if clean_param(key).nil? ||
|
||||
(Setting.send(key).to_s == clean_param(key))
|
||||
|
||||
changed_keys.push(key)
|
||||
setting = Setting.new(var: key)
|
||||
setting.value = setting_params[key].strip
|
||||
setting.value = clean_param(key)
|
||||
unless setting.valid?
|
||||
@errors.merge!(setting.errors)
|
||||
end
|
||||
@@ -24,7 +25,7 @@ class Admin::SettingsController < Admin::BaseController
|
||||
end
|
||||
|
||||
changed_keys.each do |key|
|
||||
Setting.send("#{key}=", setting_params[key].strip)
|
||||
Setting.send("#{key}=", clean_param(key))
|
||||
end
|
||||
end
|
||||
|
||||
@@ -37,4 +38,12 @@ class Admin::SettingsController < Admin::BaseController
|
||||
def setting_params
|
||||
params.require(:setting).permit(Setting.editable_keys.map(&:to_sym))
|
||||
end
|
||||
|
||||
def clean_param(key)
|
||||
if Setting.get_field(key)[:type] == :string
|
||||
setting_params[key].strip
|
||||
else
|
||||
setting_params[key]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -41,4 +41,31 @@ class ApplicationController < ActionController::Base
|
||||
def after_sign_in_path_for(user)
|
||||
session[:user_return_to] || root_path
|
||||
end
|
||||
|
||||
def lndhub_authenticate(options={})
|
||||
if session[:ln_auth_token].present? && !options[:force_reauth]
|
||||
@ln_auth_token = session[:ln_auth_token]
|
||||
else
|
||||
lndhub = Lndhub.new
|
||||
auth_token = lndhub.authenticate(current_user)
|
||||
session[:ln_auth_token] = auth_token
|
||||
@ln_auth_token = auth_token
|
||||
end
|
||||
rescue => e
|
||||
Sentry.capture_exception(e) if Setting.sentry_enabled?
|
||||
end
|
||||
|
||||
def lndhub_fetch_balance
|
||||
@balance = LndhubManager::FetchUserBalance.call(auth_token: @ln_auth_token)
|
||||
rescue AuthError
|
||||
lndhub_authenticate(force_reauth: true)
|
||||
raise if @fetch_balance_retried
|
||||
@fetch_balance_retried = true
|
||||
lndhub_fetch_balance
|
||||
end
|
||||
|
||||
def nostr_event_from_params
|
||||
params.permit!
|
||||
params[:signed_event].to_h.symbolize_keys
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,10 +1,129 @@
|
||||
class Contributions::DonationsController < ApplicationController
|
||||
before_action :authenticate_user!
|
||||
include BtcpayHelper
|
||||
|
||||
# GET /donations
|
||||
# GET /donations.json
|
||||
before_action :authenticate_user!
|
||||
before_action :set_donation_methods, only: [:index, :create]
|
||||
before_action :require_donation_method_enabled, only: [:create]
|
||||
before_action :validate_donation_params, only: [:create]
|
||||
before_action :set_donation, only: [:confirm_btcpay]
|
||||
|
||||
# GET /contributions/donations
|
||||
def index
|
||||
@donations = current_user.donations.completed
|
||||
@current_section = :contributions
|
||||
@donations_completed = current_user.donations.completed.order('paid_at desc')
|
||||
@donations_pending = current_user.donations.processing.order('created_at desc')
|
||||
|
||||
if Setting.lndhub_enabled?
|
||||
begin
|
||||
lndhub_authenticate
|
||||
lndhub_fetch_balance
|
||||
rescue
|
||||
@balance = 0
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# POST /contributions/donations
|
||||
def create
|
||||
if params[:currency] == "sats"
|
||||
fiat_amount = nil
|
||||
fiat_currency = nil
|
||||
amount_sats = params[:amount]
|
||||
else
|
||||
fiat_amount = params[:amount].to_i
|
||||
fiat_currency = params[:currency]
|
||||
amount_sats = nil
|
||||
end
|
||||
|
||||
@donation = current_user.donations.create!(
|
||||
donation_method: params[:donation_method],
|
||||
payment_method: nil,
|
||||
paid_at: nil,
|
||||
amount_sats: amount_sats,
|
||||
fiat_amount: (fiat_amount.nil? ? nil : fiat_amount * 100), # store in cents
|
||||
fiat_currency: fiat_currency,
|
||||
public_name: params[:public_name]
|
||||
)
|
||||
|
||||
case params[:donation_method]
|
||||
when "btcpay"
|
||||
res = BtcpayManager::CreateInvoice.call(
|
||||
amount: fiat_amount || (amount_sats.to_f / 100000000),
|
||||
currency: fiat_currency || "BTC",
|
||||
redirect_url: confirm_btcpay_contributions_donation_url(@donation)
|
||||
)
|
||||
|
||||
@donation.update! btcpay_invoice_id: res["id"]
|
||||
|
||||
redirect_to btcpay_checkout_url(res["id"]), allow_other_host: true
|
||||
else
|
||||
redirect_to contributions_donations_url, flash: {
|
||||
error: "Donation method currently not available"
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
def confirm_btcpay
|
||||
redirect_to contributions_donations_url and return if @donation.completed?
|
||||
|
||||
invoice = BtcpayManager::FetchInvoice.call(invoice_id: @donation.btcpay_invoice_id)
|
||||
|
||||
if @donation.amount_sats.present?
|
||||
# TODO make default fiat currency configurable and/or determine from user's
|
||||
# i18n browser settings
|
||||
@donation.fiat_currency = "EUR"
|
||||
exchange_rate = BtcpayManager::FetchExchangeRate.call(fiat_currency: @donation.fiat_currency)
|
||||
@donation.fiat_amount = (((@donation.amount_sats.to_f / 100000000) * exchange_rate) * 100).to_i
|
||||
else
|
||||
amt_str = invoice["paymentMethods"].first["amount"]
|
||||
@donation.amount_sats = amt_str.tr(".","").sub(/0*$/, "").to_i
|
||||
end
|
||||
|
||||
case invoice["status"]
|
||||
when "Settled"
|
||||
@donation.paid_at = DateTime.now
|
||||
@donation.payment_status = "settled"
|
||||
@donation.save!
|
||||
flash_message = { success: "Thank you!" }
|
||||
when "Processing"
|
||||
unless @donation.processing?
|
||||
@donation.payment_status = "processing"
|
||||
@donation.save!
|
||||
flash_message = { success: "Thank you! We will send you an email when the payment is confirmed." }
|
||||
BtcpayCheckDonationJob.set(wait: 20.seconds).perform_later(@donation)
|
||||
end
|
||||
when "Expired"
|
||||
flash_message = { warning: "The payment request for this donation has expired" }
|
||||
else
|
||||
flash_message = { warning: "Could not determine status of payment" }
|
||||
end
|
||||
|
||||
redirect_to contributions_donations_url, flash: flash_message
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_donation
|
||||
@donation = current_user.donations.find_by(id: params[:id])
|
||||
http_status :not_found unless @donation.present?
|
||||
end
|
||||
|
||||
def set_donation_methods
|
||||
@donation_methods = []
|
||||
@donation_methods.push :btcpay if Setting.btcpay_enabled?
|
||||
@donation_methods.push :lndhub if Setting.lndhub_enabled?
|
||||
@donation_methods.push :opencollective if Setting.opencollective_enabled?
|
||||
end
|
||||
|
||||
def require_donation_method_enabled
|
||||
http_status :forbidden unless @donation_methods.include?(
|
||||
params[:donation_method].to_sym
|
||||
)
|
||||
end
|
||||
|
||||
def validate_donation_params
|
||||
if !%w[EUR USD sats].include?(params[:currency]) || (params[:amount].to_i <= 0)
|
||||
http_status :unprocessable_entity
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
class LnurlpayController < ApplicationController
|
||||
before_action :check_service_available
|
||||
before_action :find_user
|
||||
before_action :set_cors_access_control_headers
|
||||
|
||||
MIN_SATS = 10
|
||||
MAX_SATS = 1_000_000
|
||||
MAX_COMMENT_CHARS = 100
|
||||
|
||||
# GET /.well-known/lnurlp/:username
|
||||
def index
|
||||
render json: {
|
||||
res = {
|
||||
status: "OK",
|
||||
callback: "https://#{Setting.accounts_domain}/lnurlpay/#{@user.cn}/invoice",
|
||||
tag: "payRequest",
|
||||
@@ -16,8 +18,16 @@ class LnurlpayController < ApplicationController
|
||||
metadata: metadata(@user.address),
|
||||
commentAllowed: MAX_COMMENT_CHARS
|
||||
}
|
||||
|
||||
if Setting.nostr_enabled?
|
||||
res[:allowsNostr] = true
|
||||
res[:nostrPubkey] = Setting.nostr_public_key
|
||||
end
|
||||
|
||||
render json: res
|
||||
end
|
||||
|
||||
# GET /.well-known/keysend/:username
|
||||
def keysend
|
||||
http_status :not_found and return unless Setting.lndhub_keysend_enabled?
|
||||
|
||||
@@ -32,8 +42,9 @@ class LnurlpayController < ApplicationController
|
||||
}
|
||||
end
|
||||
|
||||
# GET /lnurlpay/:username/invoice
|
||||
def invoice
|
||||
amount = params[:amount].to_i / 1000 # msats
|
||||
amount = params[:amount].to_i / 1000 # msats to sats
|
||||
comment = params[:comment] || ""
|
||||
address = @user.address
|
||||
|
||||
@@ -42,53 +53,109 @@ class LnurlpayController < ApplicationController
|
||||
return
|
||||
end
|
||||
|
||||
if !valid_comment?(comment)
|
||||
render json: { status: "ERROR", reason: "Comment too long" }
|
||||
return
|
||||
if params[:nostr].present? && Setting.nostr_enabled?
|
||||
handle_zap_request amount, params[:nostr], params[:lnurl]
|
||||
else
|
||||
handle_pay_request address, amount, comment
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_cors_access_control_headers
|
||||
headers['Access-Control-Allow-Origin'] = "*"
|
||||
headers['Access-Control-Allow-Headers'] = "*"
|
||||
headers['Access-Control-Allow-Methods'] = "GET"
|
||||
end
|
||||
|
||||
memo = "To #{address}"
|
||||
memo = "#{memo}: \"#{comment}\"" if comment.present?
|
||||
def check_service_available
|
||||
http_status :not_found unless Setting.lndhub_enabled?
|
||||
end
|
||||
|
||||
payment_request = @user.ln_create_invoice({
|
||||
amount: amount, # we create invoices in sats
|
||||
memo: memo,
|
||||
description_hash: Digest::SHA2.hexdigest(metadata(address)),
|
||||
})
|
||||
def find_user
|
||||
@user = User.where(cn: params[:username], ou: Setting.primary_domain).first
|
||||
http_status :not_found if @user.nil?
|
||||
end
|
||||
|
||||
render json: {
|
||||
status: "OK",
|
||||
successAction: {
|
||||
tag: "message",
|
||||
message: "Sats received. Thank you!"
|
||||
},
|
||||
routes: [],
|
||||
pr: payment_request
|
||||
}
|
||||
end
|
||||
def metadata(address)
|
||||
"[[\"text/identifier\",\"#{address}\"],[\"text/plain\",\"Sats for #{address}\"]]"
|
||||
end
|
||||
|
||||
private
|
||||
def valid_amount?(amount_in_sats)
|
||||
amount_in_sats <= MAX_SATS && amount_in_sats >= MIN_SATS
|
||||
end
|
||||
|
||||
def find_user
|
||||
@user = User.where(cn: params[:username], ou: Setting.primary_domain).first
|
||||
http_status :not_found if @user.nil?
|
||||
end
|
||||
def valid_comment?(comment)
|
||||
comment.length <= MAX_COMMENT_CHARS
|
||||
end
|
||||
|
||||
def metadata(address)
|
||||
"[[\"text/identifier\", \"#{address}\"], [\"text/plain\", \"Send sats, receive thanks.\"]]"
|
||||
end
|
||||
def handle_pay_request(address, amount, comment)
|
||||
if !valid_comment?(comment)
|
||||
render json: { status: "ERROR", reason: "Comment too long" }
|
||||
return
|
||||
end
|
||||
|
||||
def valid_amount?(amount_in_sats)
|
||||
amount_in_sats <= MAX_SATS && amount_in_sats >= MIN_SATS
|
||||
end
|
||||
desc = "To #{address}"
|
||||
desc = "#{desc}: \"#{comment}\"" if comment.present?
|
||||
|
||||
def valid_comment?(comment)
|
||||
comment.length <= MAX_COMMENT_CHARS
|
||||
end
|
||||
invoice = LndhubManager::CreateUserInvoice.call(
|
||||
user: @user, payload: {
|
||||
amount: amount, # sats
|
||||
description: desc,
|
||||
description_hash: Digest::SHA256.hexdigest(metadata(address)),
|
||||
}
|
||||
)
|
||||
|
||||
private
|
||||
render json: {
|
||||
status: "OK",
|
||||
successAction: {
|
||||
tag: "message",
|
||||
message: "Sats received. Thank you!"
|
||||
},
|
||||
routes: [],
|
||||
pr: invoice["payment_request"]
|
||||
}
|
||||
end
|
||||
|
||||
def check_service_available
|
||||
http_status :not_found unless Setting.lndhub_enabled?
|
||||
end
|
||||
def nostr_event_from_payload(nostr_param)
|
||||
event_obj = JSON.parse(nostr_param).transform_keys(&:to_sym)
|
||||
Nostr::Event.new(**event_obj)
|
||||
rescue => e
|
||||
return nil
|
||||
end
|
||||
|
||||
def valid_zap_request?(amount, event, lnurl)
|
||||
NostrManager::VerifyZapRequest.call(
|
||||
amount: amount, event: event, lnurl: lnurl
|
||||
)
|
||||
end
|
||||
|
||||
def handle_zap_request(amount, nostr_param, lnurl_param)
|
||||
event = nostr_event_from_payload(nostr_param)
|
||||
|
||||
unless event.present? && valid_zap_request?(amount*1000, event, lnurl_param)
|
||||
render json: { status: "ERROR", reason: "Invalid zap request" }
|
||||
return
|
||||
end
|
||||
|
||||
# TODO might want to use the existing invoice and zap record if there are
|
||||
# multiple calls with the same zap request
|
||||
|
||||
desc = "Zap for #{@user.address}"
|
||||
desc = "#{desc}: \"#{event.content}\"" if event.content.present?
|
||||
|
||||
invoice = LndhubManager::CreateUserInvoice.call(
|
||||
user: @user, payload: {
|
||||
amount: amount, # sats
|
||||
description: desc,
|
||||
description_hash: Digest::SHA256.hexdigest(event.to_json),
|
||||
}
|
||||
)
|
||||
|
||||
@user.zaps.create! request: event,
|
||||
payment_request: invoice["payment_request"],
|
||||
amount: amount
|
||||
|
||||
render json: { status: "OK", pr: invoice["payment_request"] }
|
||||
end
|
||||
end
|
||||
|
||||
@@ -3,7 +3,7 @@ class Services::ChatController < Services::BaseController
|
||||
before_action :require_service_available
|
||||
|
||||
def show
|
||||
@service_enabled = current_user.services_enabled.include?(:xmpp)
|
||||
@service_enabled = current_user.service_enabled?(:ejabberd)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -2,10 +2,11 @@ require "rqrcode"
|
||||
require "lnurl"
|
||||
|
||||
class Services::LightningController < ApplicationController
|
||||
before_action :authenticate_user!
|
||||
before_action :authenticate_with_lndhub
|
||||
before_action :set_current_section
|
||||
before_action :fetch_balance
|
||||
before_action :require_service_available
|
||||
before_action :authenticate_user!
|
||||
before_action :lndhub_authenticate
|
||||
before_action :lndhub_fetch_balance
|
||||
|
||||
def index
|
||||
@wallet_setup_url = "lndhub://#{current_user.ln_account}:#{current_user.ln_password}@#{ENV['LNDHUB_PUBLIC_URL']}"
|
||||
@@ -55,32 +56,12 @@ class Services::LightningController < ApplicationController
|
||||
|
||||
private
|
||||
|
||||
def authenticate_with_lndhub(options={})
|
||||
if session[:ln_auth_token].present? && !options[:force_reauth]
|
||||
@ln_auth_token = session[:ln_auth_token]
|
||||
else
|
||||
lndhub = Lndhub.new
|
||||
auth_token = lndhub.authenticate(current_user)
|
||||
session[:ln_auth_token] = auth_token
|
||||
@ln_auth_token = auth_token
|
||||
end
|
||||
rescue => e
|
||||
Sentry.capture_exception(e) if Setting.sentry_enabled?
|
||||
end
|
||||
|
||||
def set_current_section
|
||||
@current_section = :services
|
||||
end
|
||||
|
||||
def fetch_balance
|
||||
lndhub = Lndhub.new
|
||||
data = lndhub.balance @ln_auth_token
|
||||
@balance = data["BTC"]["AvailableBalance"] rescue nil
|
||||
rescue AuthError
|
||||
authenticate_with_lndhub(force_reauth: true)
|
||||
raise if @fetch_balance_retried
|
||||
@fetch_balance_retried = true
|
||||
fetch_balance
|
||||
def require_service_available
|
||||
http_status :not_found unless Setting.lndhub_enabled?
|
||||
end
|
||||
|
||||
def fetch_transactions
|
||||
|
||||
@@ -3,7 +3,7 @@ class Services::MastodonController < Services::BaseController
|
||||
before_action :require_service_available
|
||||
|
||||
def show
|
||||
@service_enabled = current_user.services_enabled.include?(:mastodon)
|
||||
@service_enabled = current_user.service_enabled?(:mastodon)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -5,11 +5,10 @@ class Services::RemotestorageController < Services::BaseController
|
||||
|
||||
# Dashboard
|
||||
def show
|
||||
# unless current_user.services_enabled.include?(:remotestorage)
|
||||
# unless current_user.service_enabled?(:remotestorage)
|
||||
# redirect_to service_remotestorage_info_path
|
||||
# end
|
||||
@rs_auths = current_user.remote_storage_authorizations
|
||||
# TODO sort by app name
|
||||
# @rs_apps_connected = current_user.remote_storage_authorizations.any?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -3,13 +3,18 @@ class Services::RsAuthsController < Services::BaseController
|
||||
before_action :require_feature_enabled
|
||||
before_action :require_service_available
|
||||
# before_action :require_service_enabled
|
||||
before_action :find_rs_auth
|
||||
before_action :find_rs_auth, only: [:destroy, :launch_app]
|
||||
|
||||
def index
|
||||
@rs_auths = current_user.remote_storage_authorizations
|
||||
# TODO sort by app name?
|
||||
end
|
||||
|
||||
def destroy
|
||||
@auth.destroy!
|
||||
|
||||
respond_to do |format|
|
||||
format.html do redirect_to services_storage_url, flash: {
|
||||
format.html do redirect_to apps_services_storage_url, flash: {
|
||||
success: 'App authorization revoked'
|
||||
}
|
||||
end
|
||||
|
||||
@@ -4,15 +4,24 @@ require "bcrypt"
|
||||
class SettingsController < ApplicationController
|
||||
before_action :authenticate_user!
|
||||
before_action :set_main_nav_section
|
||||
before_action :set_settings_section, only: [:show, :update, :update_email, :reset_email_password]
|
||||
before_action :set_user, only: [:show, :update, :update_email, :reset_email_password]
|
||||
before_action :set_settings_section, 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
|
||||
redirect_to setting_path(:profile)
|
||||
end
|
||||
|
||||
def show
|
||||
if @settings_section == "experiments"
|
||||
case @settings_section
|
||||
when "lightning"
|
||||
@notifications_enabled = @user.preferences[:lightning_notify_sats_received] != "disabled" ||
|
||||
@user.preferences[:lightning_notify_zap_received] != "disabled"
|
||||
when "nostr"
|
||||
session[:shared_secret] ||= SecureRandom.base64(12)
|
||||
end
|
||||
end
|
||||
@@ -87,44 +96,65 @@ class SettingsController < ApplicationController
|
||||
end
|
||||
|
||||
def set_nostr_pubkey
|
||||
signed_event = nostr_event_params[:signed_event].to_h.symbolize_keys
|
||||
is_valid_id = NostrManager::ValidateId.call(event: signed_event)
|
||||
is_valid_sig = NostrManager::VerifySignature.call(event: signed_event)
|
||||
is_correct_content = signed_event[:content] == "Connect my public key to #{current_user.address} (confirmation #{session[:shared_secret]})"
|
||||
signed_event = Nostr::Event.new(**nostr_event_from_params)
|
||||
|
||||
unless is_valid_id && is_valid_sig && is_correct_content
|
||||
is_valid_sig = signed_event.verify_signature
|
||||
is_valid_auth = NostrManager::VerifyAuth.call(
|
||||
event: signed_event,
|
||||
challenge: session[:shared_secret]
|
||||
)
|
||||
|
||||
unless is_valid_sig && is_valid_auth
|
||||
flash[:alert] = "Public key could not be verified"
|
||||
http_status :unprocessable_entity and return
|
||||
end
|
||||
|
||||
pubkey_taken = User.all_except(current_user).where(
|
||||
ou: current_user.ou, nostr_pubkey: signed_event[:pubkey]
|
||||
).any?
|
||||
user_with_pubkey = LdapManager::FetchUserByNostrKey.call(pubkey: signed_event.pubkey)
|
||||
|
||||
if pubkey_taken
|
||||
if user_with_pubkey.present? && (user_with_pubkey != current_user)
|
||||
flash[:alert] = "Public key already in use for a different account"
|
||||
http_status :unprocessable_entity and return
|
||||
end
|
||||
|
||||
current_user.update! nostr_pubkey: signed_event[:pubkey]
|
||||
LdapManager::UpdateNostrKey.call(dn: current_user.dn, pubkey: signed_event.pubkey)
|
||||
session[:shared_secret] = nil
|
||||
|
||||
flash[:success] = "Public key verification successful"
|
||||
http_status :ok
|
||||
rescue
|
||||
flash[:alert] = "Public key could not be verified"
|
||||
http_status :unprocessable_entity and return
|
||||
end
|
||||
|
||||
# DELETE /settings/nostr_pubkey
|
||||
def remove_nostr_pubkey
|
||||
current_user.update! nostr_pubkey: nil
|
||||
# TODO require current pubkey or password to delete
|
||||
LdapManager::UpdateNostrKey.call(dn: current_user.dn, pubkey: nil)
|
||||
|
||||
redirect_to setting_path(:experiments), flash: {
|
||||
redirect_to setting_path(:nostr), flash: {
|
||||
success: 'Public key removed from account'
|
||||
}
|
||||
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
|
||||
|
||||
def set_main_nav_section
|
||||
@@ -134,8 +164,8 @@ class SettingsController < ApplicationController
|
||||
def set_settings_section
|
||||
@settings_section = params[:section]
|
||||
allowed_sections = [
|
||||
:profile, :account, :xmpp, :email, :lightning, :remotestorage,
|
||||
:experiments
|
||||
:profile, :account, :xmpp, :email,
|
||||
:lightning, :remotestorage, :nostr
|
||||
]
|
||||
|
||||
unless allowed_sections.include?(@settings_section.to_sym)
|
||||
@@ -148,11 +178,9 @@ class SettingsController < ApplicationController
|
||||
end
|
||||
|
||||
def user_params
|
||||
params.require(:user).permit(:display_name, :avatar, preferences: [
|
||||
:lightning_notify_sats_received,
|
||||
:remotestorage_notify_auth_created,
|
||||
:xmpp_exchange_contacts_with_invitees
|
||||
])
|
||||
params.require(:user).permit(
|
||||
:display_name, :avatar, preferences: UserPreferences.pref_keys
|
||||
)
|
||||
end
|
||||
|
||||
def email_params
|
||||
@@ -163,12 +191,6 @@ class SettingsController < ApplicationController
|
||||
params.require(:user).permit(:current_password)
|
||||
end
|
||||
|
||||
def nostr_event_params
|
||||
params.permit(signed_event: [
|
||||
:id, :pubkey, :created_at, :kind, :tags, :content, :sig
|
||||
])
|
||||
end
|
||||
|
||||
def generate_email_password
|
||||
characters = [('a'..'z'), ('A'..'Z'), (0..9)].map(&:to_a).flatten
|
||||
SecureRandom.random_bytes(16).each_byte.map { |b| characters[b % characters.length] }.join
|
||||
|
||||
62
app/controllers/users/sessions_controller.rb
Normal file
62
app/controllers/users/sessions_controller.rb
Normal file
@@ -0,0 +1,62 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Users::SessionsController < Devise::SessionsController
|
||||
# before_action :configure_sign_in_params, only: [:create]
|
||||
|
||||
# GET /resource/sign_in
|
||||
def new
|
||||
session[:shared_secret] = SecureRandom.base64(12)
|
||||
super
|
||||
end
|
||||
|
||||
# POST /resource/sign_in
|
||||
# def create
|
||||
# super
|
||||
# end
|
||||
|
||||
# DELETE /resource/sign_out
|
||||
# def destroy
|
||||
# super
|
||||
# end
|
||||
|
||||
# POST /users/nostr_login
|
||||
def nostr_login
|
||||
signed_event = Nostr::Event.new(**nostr_event_from_params)
|
||||
|
||||
is_valid_sig = signed_event.verify_signature
|
||||
is_valid_auth = NostrManager::VerifyAuth.call(
|
||||
event: signed_event,
|
||||
challenge: session[:shared_secret]
|
||||
)
|
||||
|
||||
session[:shared_secret] = nil
|
||||
|
||||
unless is_valid_sig && is_valid_auth
|
||||
flash[:alert] = "Login verification failed"
|
||||
http_status :unauthorized and return
|
||||
end
|
||||
|
||||
user = LdapManager::FetchUserByNostrKey.call(pubkey: signed_event.pubkey)
|
||||
|
||||
if user.present?
|
||||
set_flash_message!(:notice, :signed_in)
|
||||
sign_in("user", user)
|
||||
render json: { redirect_url: after_sign_in_path_for(user) }, status: :ok
|
||||
else
|
||||
flash[:alert] = "Failed to find your account. Nostr login may be disabled."
|
||||
http_status :unauthorized
|
||||
end
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def set_flash_message(key, kind, options = {})
|
||||
# Hide flash message after redirecting from a signin route while logged in
|
||||
super unless key == :alert && kind == "already_authenticated"
|
||||
end
|
||||
|
||||
# If you have extra params to permit, append them to the sanitizer.
|
||||
# def configure_sign_in_params
|
||||
# devise_parameter_sanitizer.permit(:sign_in, keys: [:attribute])
|
||||
# end
|
||||
end
|
||||
@@ -1,20 +1,19 @@
|
||||
class WebfingerController < ApplicationController
|
||||
class WebfingerController < WellKnownController
|
||||
before_action :allow_cross_origin_requests, only: [:show]
|
||||
|
||||
layout false
|
||||
|
||||
def show
|
||||
resource = params[:resource]
|
||||
|
||||
if resource && @useraddress = resource.match(/acct:(.+)/)&.[](1)
|
||||
@username, @org = @useraddress.split("@")
|
||||
@username, @domain = @useraddress.split("@")
|
||||
|
||||
unless Rails.env.development?
|
||||
# Allow different domains (e.g. localhost:3000) in development only
|
||||
head 404 and return unless @org == Setting.primary_domain
|
||||
head 404 and return unless @domain == Setting.primary_domain
|
||||
end
|
||||
|
||||
unless User.where(cn: @username.downcase, ou: Setting.primary_domain).any?
|
||||
unless @user = User.where(ou: Setting.primary_domain)
|
||||
.find_by(cn: @username.downcase)
|
||||
head 404 and return
|
||||
end
|
||||
|
||||
@@ -28,22 +27,60 @@ class WebfingerController < ApplicationController
|
||||
private
|
||||
|
||||
def webfinger
|
||||
links = [];
|
||||
jrd = {
|
||||
subject: "acct:#{@user.address}",
|
||||
aliases: [],
|
||||
links: []
|
||||
}
|
||||
|
||||
# TODO check if storage service is enabled for user, not just globally
|
||||
links << remotestorage_link if Setting.remotestorage_enabled
|
||||
if Setting.mastodon_enabled && @user.service_enabled?(:mastodon)
|
||||
# https://docs.joinmastodon.org/spec/webfinger/
|
||||
jrd[:aliases] += mastodon_aliases
|
||||
jrd[:links] += mastodon_links
|
||||
end
|
||||
|
||||
{ "links" => links }
|
||||
if Setting.remotestorage_enabled && @user.service_enabled?(:remotestorage)
|
||||
# https://datatracker.ietf.org/doc/draft-dejong-remotestorage/
|
||||
jrd[:links] << remotestorage_link
|
||||
end
|
||||
|
||||
jrd
|
||||
end
|
||||
|
||||
def mastodon_aliases
|
||||
[
|
||||
"#{Setting.mastodon_public_url}/@#{@user.cn}",
|
||||
"#{Setting.mastodon_public_url}/users/#{@user.cn}"
|
||||
]
|
||||
end
|
||||
|
||||
def mastodon_links
|
||||
[
|
||||
{
|
||||
rel: "http://webfinger.net/rel/profile-page",
|
||||
type: "text/html",
|
||||
href: "#{Setting.mastodon_public_url}/@#{@user.cn}"
|
||||
},
|
||||
{
|
||||
rel: "self",
|
||||
type: "application/activity+json",
|
||||
href: "#{Setting.mastodon_public_url}/users/#{@user.cn}"
|
||||
},
|
||||
{
|
||||
rel: "http://ostatus.org/schema/1.0/subscribe",
|
||||
template: "#{Setting.mastodon_public_url}/authorize_interaction?uri={uri}"
|
||||
}
|
||||
]
|
||||
end
|
||||
|
||||
def remotestorage_link
|
||||
auth_url = new_rs_oauth_url(@username)
|
||||
auth_url = new_rs_oauth_url(@username, host: Setting.accounts_domain)
|
||||
storage_url = "#{Setting.rs_storage_url}/#{@username}"
|
||||
|
||||
{
|
||||
"rel" => "http://tools.ietf.org/id/draft-dejong-remotestorage",
|
||||
"href" => storage_url,
|
||||
"properties" => {
|
||||
rel: "http://tools.ietf.org/id/draft-dejong-remotestorage",
|
||||
href: storage_url,
|
||||
properties: {
|
||||
"http://remotestorage.io/spec/version" => "draft-dejong-remotestorage-13",
|
||||
"http://tools.ietf.org/html/rfc6749#section-4.2" => auth_url,
|
||||
"http://tools.ietf.org/html/rfc6750#section-2.3" => nil, # access token via a HTTP query parameter
|
||||
@@ -52,10 +89,4 @@ class WebfingerController < ApplicationController
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def allow_cross_origin_requests
|
||||
return unless Rails.env.development?
|
||||
headers['Access-Control-Allow-Origin'] = "*"
|
||||
headers['Access-Control-Allow-Methods'] = "GET"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -2,45 +2,76 @@ class WebhooksController < ApplicationController
|
||||
skip_forgery_protection
|
||||
|
||||
before_action :authorize_request
|
||||
before_action :process_payload
|
||||
|
||||
def lndhub
|
||||
begin
|
||||
payload = JSON.parse(request.body.read, symbolize_names: true)
|
||||
head :no_content and return unless payload[:type] == "incoming"
|
||||
rescue
|
||||
head :unprocessable_entity and return
|
||||
@user = User.find_by!(ln_account: @payload[:user_login])
|
||||
|
||||
if @zap = @user.zaps.find_by(payment_request: @payload[:payment_request])
|
||||
settled_at = Time.parse(@payload[:settled_at])
|
||||
zap_receipt = NostrManager::CreateZapReceipt.call(
|
||||
zap: @zap,
|
||||
paid_at: settled_at.to_i,
|
||||
preimage: @payload[:preimage]
|
||||
)
|
||||
@zap.update! settled_at: settled_at, receipt: zap_receipt.to_h
|
||||
NostrManager::PublishZapReceipt.call(zap: @zap)
|
||||
end
|
||||
|
||||
user = User.find_by!(ln_account: payload[:user_login])
|
||||
notify = user.preferences[:lightning_notify_sats_received]
|
||||
case notify
|
||||
when "xmpp"
|
||||
notify_xmpp(user.address, payload[:amount], payload[:memo])
|
||||
when "email"
|
||||
NotificationMailer.with(user: user, amount_sats: payload[:amount])
|
||||
.lightning_sats_received.deliver_later
|
||||
end
|
||||
send_notifications
|
||||
|
||||
head :ok
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# TODO refactor into mailer-like generic class/service
|
||||
def notify_xmpp(address, amt_sats, memo)
|
||||
payload = {
|
||||
type: "normal",
|
||||
from: Setting.xmpp_notifications_from_address,
|
||||
to: address,
|
||||
subject: "Sats received!",
|
||||
body: "#{helpers.number_with_delimiter amt_sats} sats received in your Lightning wallet:\n> #{memo}"
|
||||
}
|
||||
XmppSendMessageJob.perform_later(payload)
|
||||
end
|
||||
|
||||
def authorize_request
|
||||
if !ENV['WEBHOOKS_ALLOWED_IPS'].split(',').include?(request.remote_ip)
|
||||
head :forbidden and return
|
||||
end
|
||||
end
|
||||
|
||||
def process_payload
|
||||
@payload = JSON.parse(request.body.read, symbolize_names: true)
|
||||
unless @payload[:type] == "incoming" &&
|
||||
@payload[:state] == "settled"
|
||||
head :no_content and return
|
||||
end
|
||||
rescue
|
||||
head :unprocessable_entity and return
|
||||
end
|
||||
|
||||
def send_notifications
|
||||
return if @payload[:amount] < @user.preferences[:lightning_notify_min_sats]
|
||||
|
||||
if @user.preferences[:lightning_notify_only_with_message]
|
||||
return if @payload[:memo].blank?
|
||||
end
|
||||
|
||||
target = @zap.present? ? @user.preferences[:lightning_notify_zap_received] :
|
||||
@user.preferences[:lightning_notify_sats_received]
|
||||
|
||||
case target
|
||||
when "xmpp"
|
||||
notify_xmpp
|
||||
when "email"
|
||||
notify_email
|
||||
end
|
||||
end
|
||||
|
||||
# TODO refactor into mailer-like generic class/service
|
||||
def notify_xmpp
|
||||
XmppSendMessageJob.perform_later({
|
||||
type: "normal",
|
||||
from: Setting.xmpp_notifications_from_address,
|
||||
to: @user.address,
|
||||
subject: "Sats received!",
|
||||
body: "#{helpers.number_with_delimiter @payload[:amount]} sats received in your Lightning wallet:\n> #{@payload[:memo]}"
|
||||
})
|
||||
end
|
||||
|
||||
def notify_email
|
||||
NotificationMailer.with(user: @user, amount_sats: @payload[:amount])
|
||||
.lightning_sats_received.deliver_later
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,16 +1,47 @@
|
||||
class WellKnownController < ApplicationController
|
||||
before_action :require_nostr_enabled, only: [ :nostr ]
|
||||
before_action :allow_cross_origin_requests, only: [ :nostr ]
|
||||
|
||||
layout false
|
||||
|
||||
def nostr
|
||||
http_status :unprocessable_entity and return if params[:name].blank?
|
||||
domain = request.headers["X-Forwarded-Host"].presence || Setting.primary_domain
|
||||
@user = User.where(cn: params[:name], ou: domain).first
|
||||
http_status :not_found and return if @user.nil? || @user.nostr_pubkey.blank?
|
||||
relay_url = Setting.nostr_relay_url.presence
|
||||
|
||||
if params[:name] == "_"
|
||||
if domain == Setting.primary_domain
|
||||
# pubkey for the primary domain without a username (e.g. kosmos.org)
|
||||
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
|
||||
else
|
||||
@user = User.where(cn: params[:name], ou: domain).first
|
||||
http_status :not_found and return if @user.nil? || @user.nostr_pubkey.blank?
|
||||
|
||||
res = { names: { @user.cn => @user.nostr_pubkey } }
|
||||
res[:relays] = { @user.nostr_pubkey => [ relay_url ] } if relay_url
|
||||
end
|
||||
|
||||
respond_to do |format|
|
||||
format.json do
|
||||
render json: {
|
||||
names: { "#{@user.cn}": @user.nostr_pubkey }
|
||||
}.to_json
|
||||
render json: res.to_json
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def require_nostr_enabled
|
||||
http_status :not_found unless Setting.nostr_enabled?
|
||||
end
|
||||
|
||||
def allow_cross_origin_requests
|
||||
headers['Access-Control-Allow-Origin'] = "*"
|
||||
headers['Access-Control-Allow-Methods'] = "GET"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
module ApplicationHelper
|
||||
include Pagy::Frontend
|
||||
|
||||
def sats_to_btc(sats)
|
||||
sats.to_f / 100000000
|
||||
end
|
||||
|
||||
def main_nav_class(current_section, link_to_section)
|
||||
if current_section == link_to_section
|
||||
"bg-gray-900/50 text-white px-3 py-2 rounded-md font-medium text-base md:text-sm block md:inline-block"
|
||||
|
||||
7
app/helpers/btcpay_helper.rb
Normal file
7
app/helpers/btcpay_helper.rb
Normal file
@@ -0,0 +1,7 @@
|
||||
module BtcpayHelper
|
||||
|
||||
def btcpay_checkout_url(invoice_id)
|
||||
"#{Setting.btcpay_public_url}/i/#{invoice_id}"
|
||||
end
|
||||
|
||||
end
|
||||
@@ -1,2 +0,0 @@
|
||||
module DashboardHelper
|
||||
end
|
||||
@@ -1,2 +0,0 @@
|
||||
module DonationsHelper
|
||||
end
|
||||
@@ -1,2 +0,0 @@
|
||||
module InvitationsHelper
|
||||
end
|
||||
@@ -1,2 +0,0 @@
|
||||
module LnurlpayHelper
|
||||
end
|
||||
12
app/helpers/services_helper.rb
Normal file
12
app/helpers/services_helper.rb
Normal file
@@ -0,0 +1,12 @@
|
||||
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
|
||||
@@ -1,2 +0,0 @@
|
||||
module SettingsHelper
|
||||
end
|
||||
@@ -1,2 +0,0 @@
|
||||
module SignupHelper
|
||||
end
|
||||
@@ -1,2 +0,0 @@
|
||||
module UsersHelper
|
||||
end
|
||||
@@ -1,2 +0,0 @@
|
||||
module WalletHelper
|
||||
end
|
||||
@@ -1,2 +0,0 @@
|
||||
module WelcomeHelper
|
||||
end
|
||||
53
app/javascript/controllers/nostr_login_controller.js
Normal file
53
app/javascript/controllers/nostr_login_controller.js
Normal file
@@ -0,0 +1,53 @@
|
||||
import { Controller } from "@hotwired/stimulus"
|
||||
|
||||
// Connects to data-controller="nostr-login"
|
||||
export default class extends Controller {
|
||||
static targets = [ "loginForm", "loginButton" ]
|
||||
static values = { site: String, sharedSecret: String }
|
||||
|
||||
connect() {
|
||||
if (window.nostr) {
|
||||
this.loginButtonTarget.disabled = false
|
||||
this.loginFormTarget.classList.remove("hidden")
|
||||
}
|
||||
}
|
||||
|
||||
async login () {
|
||||
this.loginButtonTarget.disabled = true
|
||||
|
||||
try {
|
||||
// Auth based on NIP-42
|
||||
const signedEvent = await window.nostr.signEvent({
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
kind: 22242,
|
||||
tags: [
|
||||
["site", this.siteValue],
|
||||
["challenge", this.sharedSecretValue]
|
||||
],
|
||||
content: ""
|
||||
})
|
||||
|
||||
const res = await fetch("/users/nostr_login", {
|
||||
method: "POST", credentials: "include", headers: {
|
||||
"Accept": "application/json", 'Content-Type': 'application/json',
|
||||
"X-CSRF-Token": this.csrfToken
|
||||
}, body: JSON.stringify({ signed_event: signedEvent })
|
||||
})
|
||||
|
||||
if (res.status === 200) {
|
||||
res.json().then(r => { window.location.href = r.redirect_url })
|
||||
} else {
|
||||
window.location.reload()
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Unable to authenticate:', error.message)
|
||||
} finally {
|
||||
this.loginButtonTarget.disabled = false
|
||||
}
|
||||
}
|
||||
|
||||
get csrfToken () {
|
||||
const element = document.head.querySelector('meta[name="csrf-token"]')
|
||||
return element.getAttribute("content")
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,12 @@ import { Controller } from "@hotwired/stimulus"
|
||||
// Connects to data-controller="settings--nostr-pubkey"
|
||||
export default class extends Controller {
|
||||
static targets = [ "noExtension", "setPubkey", "pubkeyBech32Input" ]
|
||||
static values = { userAddress: String, pubkeyHex: String, sharedSecret: String }
|
||||
static values = {
|
||||
userAddress: String,
|
||||
pubkeyHex: String,
|
||||
site: String,
|
||||
sharedSecret: String
|
||||
}
|
||||
|
||||
connect () {
|
||||
if (window.nostr) {
|
||||
@@ -19,11 +24,15 @@ export default class extends Controller {
|
||||
this.setPubkeyTarget.disabled = true
|
||||
|
||||
try {
|
||||
// Auth based on NIP-42
|
||||
const signedEvent = await window.nostr.signEvent({
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
kind: 1,
|
||||
tags: [],
|
||||
content: `Connect my public key to ${this.userAddressValue} (confirmation ${this.sharedSecretValue})`
|
||||
kind: 22242,
|
||||
tags: [
|
||||
["site", this.siteValue],
|
||||
["challenge", this.sharedSecretValue]
|
||||
],
|
||||
content: ""
|
||||
})
|
||||
|
||||
const res = await fetch("/settings/set_nostr_pubkey", {
|
||||
|
||||
28
app/jobs/btcpay_check_donation_job.rb
Normal file
28
app/jobs/btcpay_check_donation_job.rb
Normal file
@@ -0,0 +1,28 @@
|
||||
class BtcpayCheckDonationJob < ApplicationJob
|
||||
queue_as :default
|
||||
|
||||
def perform(donation)
|
||||
return if donation.completed?
|
||||
|
||||
invoice = BtcpayManager::FetchInvoice.call(
|
||||
invoice_id: donation.btcpay_invoice_id
|
||||
)
|
||||
|
||||
case invoice["status"]
|
||||
when "Settled"
|
||||
donation.paid_at = DateTime.now
|
||||
donation.payment_status = "settled"
|
||||
donation.save!
|
||||
|
||||
NotificationMailer.with(user: donation.user)
|
||||
.bitcoin_donation_confirmed
|
||||
.deliver_later
|
||||
when "Processing"
|
||||
re_enqueue_job(donation)
|
||||
end
|
||||
end
|
||||
|
||||
def re_enqueue_job(donation)
|
||||
self.class.set(wait: 20.seconds).perform_later(donation)
|
||||
end
|
||||
end
|
||||
@@ -1,7 +1,7 @@
|
||||
class CreateLdapUserJob < ApplicationJob
|
||||
queue_as :default
|
||||
|
||||
def perform(username, domain, email, hashed_pw)
|
||||
def perform(username:, domain:, email:, hashed_pw:, confirmed: false)
|
||||
dn = "cn=#{username},ou=#{domain},cn=users,dc=kosmos,dc=org"
|
||||
attr = {
|
||||
objectclass: ["top", "account", "person", "extensibleObject"],
|
||||
@@ -12,6 +12,10 @@ class CreateLdapUserJob < ApplicationJob
|
||||
userPassword: hashed_pw
|
||||
}
|
||||
|
||||
if confirmed
|
||||
attr[:serviceEnabled] = Setting.default_services
|
||||
end
|
||||
|
||||
ldap_client.add(dn: dn, attributes: attr)
|
||||
end
|
||||
|
||||
|
||||
7
app/jobs/nostr_publish_event_job.rb
Normal file
7
app/jobs/nostr_publish_event_job.rb
Normal file
@@ -0,0 +1,7 @@
|
||||
class NostrPublishEventJob < ApplicationJob
|
||||
queue_as :nostr
|
||||
|
||||
def perform(event:, relay_url:)
|
||||
NostrManager::PublishEvent.call(event: event, relay_url: relay_url)
|
||||
end
|
||||
end
|
||||
@@ -2,8 +2,8 @@ class XmppExchangeContactsJob < ApplicationJob
|
||||
queue_as :default
|
||||
|
||||
def perform(inviter, invitee)
|
||||
return unless inviter.services_enabled.include?("xmpp") &&
|
||||
invitee.services_enabled.include?("xmpp") &&
|
||||
return unless inviter.service_enabled?(:ejabberd) &&
|
||||
invitee.service_enabled?(:ejabberd) &&
|
||||
inviter.preferences[:xmpp_exchange_contacts_with_invitees]
|
||||
|
||||
ejabberd = EjabberdApiClient.new
|
||||
|
||||
@@ -23,4 +23,11 @@ class NotificationMailer < ApplicationMailer
|
||||
@subject = "New invitations added to your account"
|
||||
mail to: @user.email, subject: @subject
|
||||
end
|
||||
|
||||
def bitcoin_donation_confirmed
|
||||
@user = params[:user]
|
||||
@donation = params[:donation]
|
||||
@subject = "Donation confirmed"
|
||||
mail to: @user.email, subject: @subject
|
||||
end
|
||||
end
|
||||
|
||||
24
app/models/concerns/settings/btcpay_settings.rb
Normal file
24
app/models/concerns/settings/btcpay_settings.rb
Normal file
@@ -0,0 +1,24 @@
|
||||
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
|
||||
16
app/models/concerns/settings/discourse_settings.rb
Normal file
16
app/models/concerns/settings/discourse_settings.rb
Normal file
@@ -0,0 +1,16 @@
|
||||
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
|
||||
13
app/models/concerns/settings/drone_ci_settings.rb
Normal file
13
app/models/concerns/settings/drone_ci_settings.rb
Normal file
@@ -0,0 +1,13 @@
|
||||
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
|
||||
19
app/models/concerns/settings/ejabberd_settings.rb
Normal file
19
app/models/concerns/settings/ejabberd_settings.rb
Normal file
@@ -0,0 +1,19 @@
|
||||
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
|
||||
28
app/models/concerns/settings/email_settings.rb
Normal file
28
app/models/concerns/settings/email_settings.rb
Normal file
@@ -0,0 +1,28 @@
|
||||
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
|
||||
34
app/models/concerns/settings/general_settings.rb
Normal file
34
app/models/concerns/settings/general_settings.rb
Normal file
@@ -0,0 +1,34 @@
|
||||
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
|
||||
13
app/models/concerns/settings/gitea_settings.rb
Normal file
13
app/models/concerns/settings/gitea_settings.rb
Normal file
@@ -0,0 +1,13 @@
|
||||
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
|
||||
25
app/models/concerns/settings/lightning_network_settings.rb
Normal file
25
app/models/concerns/settings/lightning_network_settings.rb
Normal file
@@ -0,0 +1,25 @@
|
||||
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
|
||||
16
app/models/concerns/settings/mastodon_settings.rb
Normal file
16
app/models/concerns/settings/mastodon_settings.rb
Normal file
@@ -0,0 +1,16 @@
|
||||
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
|
||||
13
app/models/concerns/settings/media_wiki_settings.rb
Normal file
13
app/models/concerns/settings/media_wiki_settings.rb
Normal file
@@ -0,0 +1,13 @@
|
||||
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
|
||||
38
app/models/concerns/settings/nostr_settings.rb
Normal file
38
app/models/concerns/settings/nostr_settings.rb
Normal file
@@ -0,0 +1,38 @@
|
||||
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
|
||||
|
||||
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
|
||||
9
app/models/concerns/settings/open_collective_settings.rb
Normal file
9
app/models/concerns/settings/open_collective_settings.rb
Normal file
@@ -0,0 +1,9 @@
|
||||
module Settings
|
||||
module OpenCollectiveSettings
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
field :opencollective_enabled, type: :boolean, default: true
|
||||
end
|
||||
end
|
||||
end
|
||||
16
app/models/concerns/settings/remote_storage_settings.rb
Normal file
16
app/models/concerns/settings/remote_storage_settings.rb
Normal file
@@ -0,0 +1,16 @@
|
||||
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
|
||||
11
app/models/concerns/settings/xmpp_settings.rb
Normal file
11
app/models/concerns/settings/xmpp_settings.rb
Normal file
@@ -0,0 +1,11 @@
|
||||
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
|
||||
@@ -4,12 +4,25 @@ class Donation < ApplicationRecord
|
||||
|
||||
# Validations
|
||||
validates_presence_of :user
|
||||
validates_presence_of :amount_sats
|
||||
validates_presence_of :paid_at
|
||||
|
||||
# Hooks
|
||||
# TODO before_create :store_fiat_value
|
||||
validates_presence_of :donation_method,
|
||||
inclusion: { in: %w[ custom btcpay lndhub ] }
|
||||
validates_presence_of :payment_status, allow_nil: true,
|
||||
inclusion: { in: %w[ processing settled ] }
|
||||
validates_presence_of :paid_at, allow_nil: true
|
||||
validates_presence_of :amount_sats, allow_nil: true
|
||||
validates_presence_of :fiat_amount, allow_nil: true
|
||||
validates_presence_of :fiat_currency, allow_nil: true,
|
||||
inclusion: { in: %w[ EUR USD ] }
|
||||
|
||||
#Scopes
|
||||
scope :completed, -> { where.not(paid_at: nil) }
|
||||
scope :processing, -> { where(payment_status: "processing") }
|
||||
scope :completed, -> { where(payment_status: "settled") }
|
||||
|
||||
def processing?
|
||||
payment_status == "processing"
|
||||
end
|
||||
|
||||
def completed?
|
||||
payment_status == "settled"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -2,199 +2,30 @@
|
||||
class Setting < RailsSettings::Base
|
||||
cache_prefix { "v1" }
|
||||
|
||||
field :primary_domain, type: :string,
|
||||
default: ENV["PRIMARY_DOMAIN"].presence
|
||||
Dir[Rails.root.join('app', 'models', 'concerns', 'settings', '*.rb')].each do |file|
|
||||
require file
|
||||
end
|
||||
|
||||
field :accounts_domain, type: :string,
|
||||
default: ENV["AKKOUNTS_DOMAIN"].presence
|
||||
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
|
||||
|
||||
#
|
||||
# Internal services
|
||||
#
|
||||
def self.available_services
|
||||
known_services = SERVICES[:external].keys
|
||||
known_services.select {|s| Setting.send "#{s}_enabled?" }
|
||||
end
|
||||
|
||||
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_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: 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
|
||||
field :default_services, type: :array,
|
||||
default: self.available_services
|
||||
end
|
||||
|
||||
@@ -17,16 +17,15 @@ class User < ApplicationRecord
|
||||
has_one :invitation, inverse_of: :invitee, foreign_key: 'invited_user_id'
|
||||
has_one :inviter, through: :invitation, source: :user
|
||||
has_many :invitees, through: :invitations
|
||||
|
||||
has_many :donations, dependent: :nullify
|
||||
has_many :remote_storage_authorizations
|
||||
has_many :zaps
|
||||
|
||||
has_one :lndhub_user, class_name: "LndhubUser", inverse_of: "user",
|
||||
primary_key: "ln_account", foreign_key: "login"
|
||||
|
||||
has_many :accounts, through: :lndhub_user
|
||||
|
||||
has_many :remote_storage_authorizations
|
||||
|
||||
#
|
||||
# Validations
|
||||
#
|
||||
@@ -50,8 +49,6 @@ class User < ApplicationRecord
|
||||
validates_length_of :display_name, minimum: 3, maximum: 35, allow_blank: true,
|
||||
if: -> { defined?(@display_name) }
|
||||
|
||||
validates_uniqueness_of :nostr_pubkey, allow_blank: true
|
||||
|
||||
validate :acceptable_avatar
|
||||
|
||||
#
|
||||
@@ -95,9 +92,7 @@ class User < ApplicationRecord
|
||||
LdapManager::UpdateEmail.call(dn: self.dn, address: self.email)
|
||||
else
|
||||
# E-Mail from signup confirmed (i.e. account activation)
|
||||
|
||||
# TODO Make configurable, only activate globally enabled services
|
||||
enable_service %w[ discourse gitea mediawiki xmpp ]
|
||||
enable_default_services
|
||||
|
||||
# TODO enable in development when we have easy setup of ejabberd etc.
|
||||
return if Rails.env.development? || !Setting.ejabberd_enabled?
|
||||
@@ -135,7 +130,7 @@ class User < ApplicationRecord
|
||||
|
||||
def mastodon_address
|
||||
return nil unless Setting.mastodon_enabled?
|
||||
"#{self.cn}@#{Setting.mastodon_address_domain}"
|
||||
"#{self.cn.gsub("-", "_")}@#{Setting.mastodon_address_domain}"
|
||||
end
|
||||
|
||||
def valid_attribute?(attribute_name)
|
||||
@@ -143,10 +138,8 @@ class User < ApplicationRecord
|
||||
self.errors[attribute_name].blank?
|
||||
end
|
||||
|
||||
def ln_create_invoice(payload)
|
||||
lndhub = Lndhub.new
|
||||
lndhub.authenticate self
|
||||
lndhub.addinvoice payload
|
||||
def enable_default_services
|
||||
enable_service Setting.default_services
|
||||
end
|
||||
|
||||
def dn
|
||||
@@ -163,30 +156,8 @@ class User < ApplicationRecord
|
||||
@display_name ||= ldap_entry[:display_name]
|
||||
end
|
||||
|
||||
def avatar
|
||||
@avatar_base64 ||= LdapManager::FetchAvatar.call(cn: cn)
|
||||
end
|
||||
|
||||
def services_enabled
|
||||
ldap_entry[:service] || []
|
||||
end
|
||||
|
||||
def enable_service(service)
|
||||
current_services = services_enabled
|
||||
new_services = Array(service).map(&:to_s)
|
||||
services = (current_services + new_services).uniq
|
||||
ldap.replace_attribute(dn, :service, services)
|
||||
end
|
||||
|
||||
def disable_service(service)
|
||||
current_services = services_enabled
|
||||
disabled_services = Array(service).map(&:to_s)
|
||||
services = (current_services - disabled_services).uniq
|
||||
ldap.replace_attribute(dn, :service, services)
|
||||
end
|
||||
|
||||
def disable_all_services
|
||||
ldap.delete_attribute(dn,:service)
|
||||
def nostr_pubkey
|
||||
@nostr_pubkey ||= ldap_entry[:nostr_key]
|
||||
end
|
||||
|
||||
def nostr_pubkey_bech32
|
||||
@@ -194,6 +165,36 @@ class User < ApplicationRecord
|
||||
Nostr::PublicKey.new(nostr_pubkey).to_bech32
|
||||
end
|
||||
|
||||
def avatar
|
||||
@avatar_base64 ||= LdapManager::FetchAvatar.call(cn: cn)
|
||||
end
|
||||
|
||||
def services_enabled
|
||||
ldap_entry[:services_enabled] || []
|
||||
end
|
||||
|
||||
def service_enabled?(name)
|
||||
services_enabled.map(&:to_sym).include?(name.to_sym)
|
||||
end
|
||||
|
||||
def enable_service(service)
|
||||
current_services = services_enabled
|
||||
new_services = Array(service).map(&:to_s)
|
||||
services = (current_services + new_services).uniq.sort
|
||||
ldap.replace_attribute(dn, :serviceEnabled, services)
|
||||
end
|
||||
|
||||
def disable_service(service)
|
||||
current_services = services_enabled
|
||||
disabled_services = Array(service).map(&:to_s)
|
||||
services = (current_services - disabled_services).uniq.sort
|
||||
ldap.replace_attribute(dn, :serviceEnabled, services)
|
||||
end
|
||||
|
||||
def disable_all_services
|
||||
ldap.delete_attribute(dn,:service)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def ldap
|
||||
|
||||
@@ -26,4 +26,8 @@ class UserPreferences
|
||||
end
|
||||
hash.stringify_keys!.to_h
|
||||
end
|
||||
|
||||
def self.pref_keys
|
||||
DEFAULT_PREFS.keys.map(&:to_sym)
|
||||
end
|
||||
end
|
||||
|
||||
20
app/models/zap.rb
Normal file
20
app/models/zap.rb
Normal file
@@ -0,0 +1,20 @@
|
||||
class Zap < ApplicationRecord
|
||||
belongs_to :user
|
||||
|
||||
scope :settled, -> { where.not(settled_at: nil) }
|
||||
scope :unpaid, -> { where(settled_at: nil) }
|
||||
|
||||
def request_event
|
||||
nostr_event_from_hash(request)
|
||||
end
|
||||
|
||||
def receipt_event
|
||||
nostr_event_from_hash(receipt)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def nostr_event_from_hash(hash)
|
||||
Nostr::Event.new(**hash.symbolize_keys)
|
||||
end
|
||||
end
|
||||
21
app/services/btcpay_manager/create_invoice.rb
Normal file
21
app/services/btcpay_manager/create_invoice.rb
Normal file
@@ -0,0 +1,21 @@
|
||||
module BtcpayManager
|
||||
class CreateInvoice < BtcpayManagerService
|
||||
def initialize(amount:, currency:, redirect_url:)
|
||||
@amount = amount
|
||||
@currency = currency
|
||||
@redirect_url = redirect_url
|
||||
end
|
||||
|
||||
def call
|
||||
post "/invoices", {
|
||||
amount: @amount.to_s,
|
||||
currency: @currency,
|
||||
checkout: {
|
||||
redirectURL: @redirect_url,
|
||||
redirectAutomatically: true,
|
||||
requiresRefundEmail: false
|
||||
}
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
14
app/services/btcpay_manager/fetch_exchange_rate.rb
Normal file
14
app/services/btcpay_manager/fetch_exchange_rate.rb
Normal file
@@ -0,0 +1,14 @@
|
||||
module BtcpayManager
|
||||
class FetchExchangeRate < BtcpayManagerService
|
||||
def initialize(fiat_currency:)
|
||||
@fiat_currency = fiat_currency
|
||||
end
|
||||
|
||||
def call
|
||||
pair_str = "BTC_#{@fiat_currency}"
|
||||
res = get "rates", { currencyPair: pair_str }
|
||||
pair = res.find{|p| p["currencyPair"] == pair_str }
|
||||
rate = pair["rate"].to_f
|
||||
end
|
||||
end
|
||||
end
|
||||
14
app/services/btcpay_manager/fetch_invoice.rb
Normal file
14
app/services/btcpay_manager/fetch_invoice.rb
Normal file
@@ -0,0 +1,14 @@
|
||||
module BtcpayManager
|
||||
class FetchInvoice < BtcpayManagerService
|
||||
def initialize(invoice_id:)
|
||||
@invoice_id = invoice_id
|
||||
end
|
||||
|
||||
def call
|
||||
invoice = get "/invoices/#{@invoice_id}"
|
||||
payment_methods = get "/invoices/#{@invoice_id}/payment-methods"
|
||||
invoice["paymentMethods"] = payment_methods
|
||||
invoice
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,7 +1,7 @@
|
||||
module BtcpayManager
|
||||
class FetchLightningWalletBalance < BtcpayManagerService
|
||||
def call
|
||||
res = get "stores/#{store_id}/lightning/BTC/balance"
|
||||
res = get "/lightning/BTC/balance"
|
||||
|
||||
{
|
||||
confirmed_balance: res["offchain"]["local"].to_i / 1000 # msats to sats
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
module BtcpayManager
|
||||
class FetchOnchainWalletBalance < BtcpayManagerService
|
||||
def call
|
||||
res = get "stores/#{store_id}/payment-methods/onchain/BTC/wallet"
|
||||
res = get "/payment-methods/onchain/BTC/wallet"
|
||||
|
||||
{
|
||||
balance: (res["balance"].to_f * 100000000).to_i, # BTC to sats
|
||||
|
||||
@@ -2,23 +2,35 @@
|
||||
# API Docs: https://docs.btcpayserver.org/API/Greenfield/v1/
|
||||
#
|
||||
class BtcpayManagerService < ApplicationService
|
||||
attr_reader :base_url, :store_id, :auth_token
|
||||
|
||||
def initialize
|
||||
@base_url = Setting.btcpay_api_url
|
||||
@store_id = Setting.btcpay_store_id
|
||||
@auth_token = Setting.btcpay_auth_token
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def get(endpoint)
|
||||
res = Faraday.get("#{base_url}/#{endpoint}", {}, {
|
||||
def base_url
|
||||
@base_url ||= "#{Setting.btcpay_api_url}/stores/#{Setting.btcpay_store_id}"
|
||||
end
|
||||
|
||||
def auth_token
|
||||
@auth_token ||= Setting.btcpay_auth_token
|
||||
end
|
||||
|
||||
def headers
|
||||
{
|
||||
"Content-Type" => "application/json",
|
||||
"Accept" => "application/json",
|
||||
"Authorization" => "token #{auth_token}"
|
||||
})
|
||||
}
|
||||
end
|
||||
|
||||
def endpoint_url(path)
|
||||
"#{base_url}/#{path.gsub(/^\//, '')}"
|
||||
end
|
||||
|
||||
def get(path, params = {})
|
||||
res = Faraday.get endpoint_url(path), params, headers
|
||||
JSON.parse(res.body)
|
||||
end
|
||||
|
||||
def post(path, payload)
|
||||
res = Faraday.post endpoint_url(path), payload.to_json, headers
|
||||
JSON.parse(res.body)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -35,11 +35,15 @@ class CreateAccount < ApplicationService
|
||||
@invitation.update! invited_user_id: user_id, used_at: DateTime.now
|
||||
end
|
||||
|
||||
# TODO move to confirmation
|
||||
# (and/or add email_confirmed to entry and use in login filter)
|
||||
def add_ldap_document
|
||||
hashed_pw = Devise.ldap_auth_password_builder.call(@password)
|
||||
CreateLdapUserJob.perform_later(@username, @domain, @email, hashed_pw)
|
||||
CreateLdapUserJob.perform_later(
|
||||
username: @username,
|
||||
domain: @domain,
|
||||
email: @email,
|
||||
hashed_pw: hashed_pw,
|
||||
confirmed: @confirmed
|
||||
)
|
||||
end
|
||||
|
||||
def create_lndhub_account(user)
|
||||
|
||||
@@ -9,7 +9,7 @@ module LdapManager
|
||||
attributes = %w{ jpegPhoto }
|
||||
filter = Net::LDAP::Filter.eq("cn", @cn)
|
||||
|
||||
entry = ldap_client.search(base: treebase, filter: filter, attributes: attributes).first
|
||||
entry = client.search(base: treebase, filter: filter, attributes: attributes).first
|
||||
entry.try(:jpegPhoto) ? entry.jpegPhoto.first : nil
|
||||
end
|
||||
end
|
||||
|
||||
18
app/services/ldap_manager/fetch_user_by_nostr_key.rb
Normal file
18
app/services/ldap_manager/fetch_user_by_nostr_key.rb
Normal file
@@ -0,0 +1,18 @@
|
||||
module LdapManager
|
||||
class FetchUserByNostrKey < LdapManagerService
|
||||
def initialize(pubkey:)
|
||||
@ou = Setting.primary_domain
|
||||
@pubkey = pubkey
|
||||
end
|
||||
|
||||
def call
|
||||
treebase = "ou=#{@ou},cn=users,#{ldap_suffix}"
|
||||
attributes = %w{ cn }
|
||||
filter = Net::LDAP::Filter.eq("nostrKey", @pubkey)
|
||||
|
||||
entry = client.search(base: treebase, filter: filter, attributes: attributes).first
|
||||
|
||||
User.find_by cn: entry.cn, ou: @ou unless entry.nil?
|
||||
end
|
||||
end
|
||||
end
|
||||
16
app/services/ldap_manager/update_nostr_key.rb
Normal file
16
app/services/ldap_manager/update_nostr_key.rb
Normal file
@@ -0,0 +1,16 @@
|
||||
module LdapManager
|
||||
class UpdateNostrKey < LdapManagerService
|
||||
def initialize(dn:, pubkey:)
|
||||
@dn = dn
|
||||
@pubkey = pubkey
|
||||
end
|
||||
|
||||
def call
|
||||
if @pubkey.present?
|
||||
replace_attribute @dn, :nostrKey, @pubkey
|
||||
else
|
||||
delete_attribute @dn, :nostrKey
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,5 +1,2 @@
|
||||
class LdapManagerService < LdapService
|
||||
def suffix
|
||||
@suffix ||= ENV["LDAP_SUFFIX"] || "dc=kosmos,dc=org"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,41 +1,47 @@
|
||||
class LdapService < ApplicationService
|
||||
def initialize
|
||||
@suffix = ENV["LDAP_SUFFIX"] || "dc=kosmos,dc=org"
|
||||
def modify(dn, operations=[])
|
||||
client.modify dn: dn, operations: operations
|
||||
client.get_operation_result.code
|
||||
end
|
||||
|
||||
def add_attribute(dn, attr, values)
|
||||
ldap_client.add_attribute dn, attr, values
|
||||
client.add_attribute dn, attr, values
|
||||
client.get_operation_result.code
|
||||
end
|
||||
|
||||
def replace_attribute(dn, attr, values)
|
||||
ldap_client.replace_attribute dn, attr, values
|
||||
client.replace_attribute dn, attr, values
|
||||
client.get_operation_result.code
|
||||
end
|
||||
|
||||
def delete_attribute(dn, attr)
|
||||
ldap_client.delete_attribute dn, attr
|
||||
client.delete_attribute dn, attr
|
||||
client.get_operation_result.code
|
||||
end
|
||||
|
||||
def add_entry(dn, attrs, interactive=false)
|
||||
puts "Adding entry: #{dn}" if interactive
|
||||
res = ldap_client.add dn: dn, attributes: attrs
|
||||
puts res.inspect if interactive && !res
|
||||
res
|
||||
puts "Add entry: #{dn}" if interactive
|
||||
client.add dn: dn, attributes: attrs
|
||||
client.get_operation_result.code
|
||||
end
|
||||
|
||||
def delete_entry(dn, interactive=false)
|
||||
puts "Deleting entry: #{dn}" if interactive
|
||||
res = ldap_client.delete dn: dn
|
||||
puts res.inspect if interactive && !res
|
||||
res
|
||||
puts "Delete entry: #{dn}" if interactive
|
||||
client.delete dn: dn
|
||||
client.get_operation_result.code
|
||||
end
|
||||
|
||||
def delete_all_entries!
|
||||
def delete_all_users!
|
||||
delete_all_entries!(objectclass: "person")
|
||||
end
|
||||
|
||||
def delete_all_entries!(objectclass: "*")
|
||||
if Rails.env.production?
|
||||
raise "Mass deletion of entries not allowed in production"
|
||||
end
|
||||
|
||||
filter = Net::LDAP::Filter.eq("objectClass", "*")
|
||||
entries = ldap_client.search(base: @suffix, filter: filter, attributes: %w{dn})
|
||||
filter = Net::LDAP::Filter.eq("objectClass", objectclass)
|
||||
entries = client.search(base: ldap_suffix, filter: filter, attributes: %w{dn})
|
||||
entries.sort_by!{ |e| e.dn.length }.reverse!
|
||||
|
||||
entries.each do |e|
|
||||
@@ -45,18 +51,18 @@ class LdapService < ApplicationService
|
||||
|
||||
def fetch_users(args={})
|
||||
if args[:ou]
|
||||
treebase = "ou=#{args[:ou]},cn=users,#{@suffix}"
|
||||
treebase = "ou=#{args[:ou]},cn=users,#{ldap_suffix}"
|
||||
else
|
||||
treebase = ldap_config["base"]
|
||||
end
|
||||
|
||||
attributes = %w[
|
||||
dn cn uid mail displayName admin service
|
||||
mailRoutingAddress mailpassword
|
||||
dn cn uid mail displayName admin serviceEnabled
|
||||
mailRoutingAddress mailpassword nostrKey
|
||||
]
|
||||
filter = Net::LDAP::Filter.eq("uid", args[:uid] || "*")
|
||||
|
||||
entries = ldap_client.search(base: treebase, filter: filter, attributes: attributes)
|
||||
entries = client.search(base: treebase, filter: filter, attributes: attributes)
|
||||
entries.sort_by! { |e| e.cn[0] }
|
||||
entries = entries.collect do |e|
|
||||
{
|
||||
@@ -64,9 +70,10 @@ class LdapService < ApplicationService
|
||||
mail: e.try(:mail) ? e.mail.first : nil,
|
||||
display_name: e.try(:displayName) ? e.displayName.first : nil,
|
||||
admin: e.try(:admin) ? 'admin' : nil,
|
||||
service: e.try(:service),
|
||||
services_enabled: e.try(:serviceEnabled),
|
||||
email_maildrop: e.try(:mailRoutingAddress),
|
||||
email_password: e.try(:mailpassword)
|
||||
email_password: e.try(:mailpassword),
|
||||
nostr_key: e.try(:nostrKey) ? e.nostrKey.first : nil
|
||||
}
|
||||
end
|
||||
end
|
||||
@@ -75,9 +82,9 @@ class LdapService < ApplicationService
|
||||
attributes = %w{dn ou description}
|
||||
filter = Net::LDAP::Filter.eq("objectClass", "organizationalUnit")
|
||||
# filter = Net::LDAP::Filter.eq("objectClass", "*")
|
||||
treebase = "cn=users,#{@suffix}"
|
||||
treebase = "cn=users,#{ldap_suffix}"
|
||||
|
||||
entries = ldap_client.search(base: treebase, filter: filter, attributes: attributes)
|
||||
entries = client.search(base: treebase, filter: filter, attributes: attributes)
|
||||
|
||||
entries.sort_by! { |e| e.ou[0] }
|
||||
|
||||
@@ -91,10 +98,10 @@ class LdapService < ApplicationService
|
||||
end
|
||||
|
||||
def add_organization(ou, description, interactive=false)
|
||||
dn = "ou=#{ou},cn=users,#{@suffix}"
|
||||
dn = "ou=#{ou},cn=users,#{ldap_suffix}"
|
||||
|
||||
aci = <<-EOS
|
||||
(target="ldap:///cn=*,ou=#{ou},cn=users,#{@suffix}")(targetattr="cn || sn || uid || mail || userPassword || nsRole || objectClass") (version 3.0; acl "service-#{ou.gsub(".", "-")}-read-search"; allow (read,search) userdn="ldap:///uid=service,ou=#{ou},cn=applications,#{@suffix}";)
|
||||
(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
|
||||
|
||||
attrs = {
|
||||
@@ -115,22 +122,22 @@ class LdapService < ApplicationService
|
||||
delete_all_entries!
|
||||
|
||||
user_read_aci = <<-EOS
|
||||
(target="ldap:///#{@suffix}")(targetattr="*") (version 3.0; acl "user-read-search-own-attributes"; allow (read,search) userdn="ldap:///self";)
|
||||
(target="ldap:///#{ldap_suffix}")(targetattr="*") (version 3.0; acl "user-read-search-own-attributes"; allow (read,search) userdn="ldap:///self";)
|
||||
EOS
|
||||
|
||||
add_entry @suffix, {
|
||||
add_entry ldap_suffix, {
|
||||
dc: "kosmos", objectClass: ["top", "domain"], aci: user_read_aci
|
||||
}, true
|
||||
|
||||
add_entry "cn=users,#{@suffix}", {
|
||||
add_entry "cn=users,#{ldap_suffix}", {
|
||||
cn: "users", objectClass: ["top", "organizationalRole"]
|
||||
}, true
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def ldap_client
|
||||
ldap_client ||= Net::LDAP.new host: ldap_config['host'],
|
||||
def client
|
||||
client ||= Net::LDAP.new host: ldap_config['host'],
|
||||
port: ldap_config['port'],
|
||||
# TODO has to be :simple_tls if TLS is enabled
|
||||
# encryption: ldap_config['ssl'],
|
||||
@@ -144,4 +151,8 @@ class LdapService < ApplicationService
|
||||
def ldap_config
|
||||
ldap_config ||= YAML.load(ERB.new(File.read("#{Rails.root}/config/ldap.yml")).result)[Rails.env]
|
||||
end
|
||||
|
||||
def ldap_suffix
|
||||
@ldap_suffix ||= ENV["LDAP_SUFFIX"] || "dc=kosmos,dc=org"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,24 +1,20 @@
|
||||
class Lndhub
|
||||
class Lndhub < ApplicationService
|
||||
attr_accessor :auth_token
|
||||
|
||||
def initialize
|
||||
@base_url = ENV["LNDHUB_API_URL"]
|
||||
end
|
||||
|
||||
def post(endpoint, payload)
|
||||
def post(path, payload)
|
||||
headers = { "Content-Type" => "application/json" }
|
||||
if auth_token
|
||||
headers.merge!({ "Authorization" => "Bearer #{auth_token}" })
|
||||
end
|
||||
|
||||
res = Faraday.post "#{@base_url}/#{endpoint}", payload.to_json, headers
|
||||
res = Faraday.post endpoint_url(path), payload.to_json, headers
|
||||
log_error(res) if res.status != 200
|
||||
|
||||
JSON.parse(res.body)
|
||||
end
|
||||
|
||||
def get(endpoint, auth_token)
|
||||
res = Faraday.get("#{@base_url}/#{endpoint}", {}, {
|
||||
def get(path, auth_token)
|
||||
res = Faraday.get(endpoint_url(path), {}, {
|
||||
"Content-Type" => "application/json",
|
||||
"Accept" => "application/json",
|
||||
"Authorization" => "Bearer #{auth_token}"
|
||||
@@ -42,7 +38,7 @@ class Lndhub
|
||||
self.auth_token
|
||||
end
|
||||
|
||||
def balance(user_token=nil)
|
||||
def fetch_balance(user_token=nil)
|
||||
get "balance", user_token || auth_token
|
||||
end
|
||||
|
||||
@@ -72,4 +68,14 @@ class Lndhub
|
||||
Sentry.capture_message("Lndhub API request failed: #{res.body}")
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def base_url
|
||||
@base_url ||= Setting.lndhub_api_url
|
||||
end
|
||||
|
||||
def endpoint_url(path)
|
||||
"#{base_url}/#{path.gsub(/^\//, '')}"
|
||||
end
|
||||
end
|
||||
|
||||
13
app/services/lndhub_manager/create_user_invoice.rb
Normal file
13
app/services/lndhub_manager/create_user_invoice.rb
Normal file
@@ -0,0 +1,13 @@
|
||||
module LndhubManager
|
||||
class CreateUserInvoice < LndhubV2
|
||||
def initialize(user:, payload:)
|
||||
@user = user
|
||||
@payload = payload
|
||||
end
|
||||
|
||||
def call
|
||||
authenticate @user
|
||||
create_invoice @payload
|
||||
end
|
||||
end
|
||||
end
|
||||
12
app/services/lndhub_manager/fetch_user_balance.rb
Normal file
12
app/services/lndhub_manager/fetch_user_balance.rb
Normal file
@@ -0,0 +1,12 @@
|
||||
module LndhubManager
|
||||
class FetchUserBalance < Lndhub
|
||||
def initialize(auth_token:)
|
||||
@auth_token = auth_token
|
||||
end
|
||||
|
||||
def call
|
||||
data = fetch_balance(auth_token)
|
||||
data["BTC"]["AvailableBalance"] rescue nil
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,13 +1,13 @@
|
||||
class LndhubV2 < Lndhub
|
||||
|
||||
def post(endpoint, payload, options={})
|
||||
def post(path, payload, options={})
|
||||
headers = { "Content-Type" => "application/json" }
|
||||
if auth_token
|
||||
headers.merge!({ "Authorization" => "Bearer #{auth_token}" })
|
||||
elsif options[:admin_token]
|
||||
headers.merge!({ "Authorization" => "Bearer #{options[:admin_token]}" })
|
||||
end
|
||||
res = Faraday.post "#{@base_url}/#{endpoint}", payload.to_json, headers
|
||||
res = Faraday.post endpoint_url(path), payload.to_json, headers
|
||||
log_error(res) if res.status != 200
|
||||
|
||||
JSON.parse(res.body)
|
||||
|
||||
25
app/services/nostr_manager/create_zap_receipt.rb
Normal file
25
app/services/nostr_manager/create_zap_receipt.rb
Normal file
@@ -0,0 +1,25 @@
|
||||
module NostrManager
|
||||
class CreateZapReceipt < NostrManagerService
|
||||
def initialize(zap:, paid_at:, preimage:)
|
||||
@zap, @paid_at, @preimage = zap, paid_at, preimage
|
||||
end
|
||||
|
||||
def call
|
||||
request_tags = parse_tags(@zap.request_event.tags)
|
||||
|
||||
site_user.create_event(
|
||||
kind: 9735,
|
||||
created_at: @paid_at,
|
||||
content: "",
|
||||
tags: [
|
||||
["p", request_tags[:p].first],
|
||||
["e", request_tags[:e]&.first],
|
||||
["a", request_tags[:a]&.first],
|
||||
["bolt11", @zap.payment_request],
|
||||
["preimage", @preimage],
|
||||
["description", @zap.request_event.to_json]
|
||||
].reject { |t| t[1].nil? }
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
21
app/services/nostr_manager/discover_user_profile.rb
Normal file
21
app/services/nostr_manager/discover_user_profile.rb
Normal file
@@ -0,0 +1,21 @@
|
||||
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
|
||||
21
app/services/nostr_manager/discover_user_relays.rb
Normal file
21
app/services/nostr_manager/discover_user_relays.rb
Normal file
@@ -0,0 +1,21 @@
|
||||
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
|
||||
59
app/services/nostr_manager/fetch_event.rb
Normal file
59
app/services/nostr_manager/fetch_event.rb
Normal file
@@ -0,0 +1,59 @@
|
||||
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
|
||||
44
app/services/nostr_manager/fetch_latest_event.rb
Normal file
44
app/services/nostr_manager/fetch_latest_event.rb
Normal file
@@ -0,0 +1,44 @@
|
||||
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
|
||||
50
app/services/nostr_manager/publish_event.rb
Normal file
50
app/services/nostr_manager/publish_event.rb
Normal file
@@ -0,0 +1,50 @@
|
||||
module NostrManager
|
||||
class PublishEvent < NostrManagerService
|
||||
def initialize(event:, relay_url:)
|
||||
relay_name = URI.parse(relay_url).host
|
||||
@relay = Nostr::Relay.new(url: relay_url, name: relay_name)
|
||||
|
||||
if event.is_a?(Nostr::Event)
|
||||
@event = event
|
||||
else
|
||||
@event = Nostr::Event.new(**event.symbolize_keys)
|
||||
end
|
||||
|
||||
@client = Nostr::Client.new
|
||||
end
|
||||
|
||||
def call
|
||||
client, relay, event = @client, @relay, @event
|
||||
log_prefix = "[nostr][#{relay.name}]"
|
||||
|
||||
thread = Thread.new do
|
||||
client.on :connect do
|
||||
Rails.logger.debug "#{log_prefix} Publishing #{event.id}..."
|
||||
client.publish event
|
||||
end
|
||||
|
||||
client.on :error do |e|
|
||||
Rails.logger.debug "#{log_prefix} Error: #{e}"
|
||||
Rails.logger.debug "#{log_prefix} Closing thread..."
|
||||
thread.exit
|
||||
end
|
||||
|
||||
client.on :message do |m|
|
||||
Rails.logger.debug "#{log_prefix} Message: #{m}"
|
||||
msg = JSON.parse(m) rescue []
|
||||
if msg[0] == "OK" && msg[1] == event.id && msg[2]
|
||||
Rails.logger.debug "#{log_prefix} Event published. Closing thread..."
|
||||
else
|
||||
Rails.logger.debug "#{log_prefix} Unexpected message from relay. Closing thread..."
|
||||
end
|
||||
thread.exit
|
||||
end
|
||||
|
||||
Rails.logger.debug "#{log_prefix} Connecting to #{relay.url}..."
|
||||
client.connect relay
|
||||
end
|
||||
|
||||
thread.join
|
||||
end
|
||||
end
|
||||
end
|
||||
24
app/services/nostr_manager/publish_zap_receipt.rb
Normal file
24
app/services/nostr_manager/publish_zap_receipt.rb
Normal file
@@ -0,0 +1,24 @@
|
||||
module NostrManager
|
||||
class PublishZapReceipt < NostrManagerService
|
||||
def initialize(zap:, delayed: true)
|
||||
@zap, @delayed = zap, delayed
|
||||
end
|
||||
|
||||
def call
|
||||
tags = parse_tags(@zap.request_event.tags)
|
||||
relays = tags[:relays].take(Setting.nostr_zaps_relay_limit)
|
||||
|
||||
if Setting.nostr_relay_url.present?
|
||||
relays << Setting.nostr_relay_url
|
||||
end
|
||||
|
||||
relays.uniq.each do |relay_url|
|
||||
if @delayed
|
||||
NostrPublishEventJob.perform_later(event: @zap.receipt, relay_url: relay_url)
|
||||
else
|
||||
NostrManager::PublishEvent.call(event: @zap.receipt_event, relay_url: relay_url)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,11 +0,0 @@
|
||||
module NostrManager
|
||||
class ValidateId < NostrManagerService
|
||||
def initialize(event:)
|
||||
@event = Nostr::Event.new(**event)
|
||||
end
|
||||
|
||||
def call
|
||||
@event.id == Digest::SHA256.hexdigest(JSON.generate(@event.serialize))
|
||||
end
|
||||
end
|
||||
end
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user