Compare commits
27 Commits
193a4c2edd
...
feature/co
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
462dd24da3
|
||
|
|
8eb5f093a4
|
||
| de45d070aa | |||
| c0b1112e49 | |||
|
|
2f90393eb6
|
||
|
|
8b87072485
|
||
|
|
82019f47be
|
||
|
|
259e72167b
|
||
|
|
7000908891
|
||
|
|
df0c13b400
|
||
|
|
387a2fa2e6
|
||
| 68eba80fd7 | |||
|
|
7e05530ab7
|
||
|
|
745a319b3d
|
||
|
|
f829bb3379
|
||
|
|
19bafe081f
|
||
| d130f2f68b | |||
|
|
e284996c1c
|
||
|
|
51489a83ab
|
||
|
|
05426e4ced
|
||
|
|
445cdfa024
|
||
|
|
f74227fedb
|
||
|
|
32d1992632
|
||
| 48be35f1b1 | |||
| 87720ef285 | |||
|
|
c58358c66e
|
||
|
|
287adbd365
|
@@ -19,6 +19,8 @@ LDAP_SUFFIX='dc=kosmos,dc=org'
|
|||||||
WEBHOOKS_ALLOWED_IPS='10.1.1.163'
|
WEBHOOKS_ALLOWED_IPS='10.1.1.163'
|
||||||
|
|
||||||
DISCOURSE_PUBLIC_URL='https://community.kosmos.org'
|
DISCOURSE_PUBLIC_URL='https://community.kosmos.org'
|
||||||
|
DISCOURSE_CONNECT_SECRET='discourse_connect_ftw'
|
||||||
|
|
||||||
GITEA_PUBLIC_URL='https://gitea.kosmos.org'
|
GITEA_PUBLIC_URL='https://gitea.kosmos.org'
|
||||||
MASTODON_PUBLIC_URL='https://kosmos.social'
|
MASTODON_PUBLIC_URL='https://kosmos.social'
|
||||||
MEDIAWIKI_PUBLIC_URL='https://wiki.kosmos.org'
|
MEDIAWIKI_PUBLIC_URL='https://wiki.kosmos.org'
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
DISCOURSE_PUBLIC_URL='http://discourse.example.com'
|
||||||
|
DISCOURSE_CONNECT_SECRET='discourse_connect_ftw'
|
||||||
|
|
||||||
EJABBERD_API_URL='http://xmpp.example.com/api'
|
EJABBERD_API_URL='http://xmpp.example.com/api'
|
||||||
|
|
||||||
BTCPAY_API_URL='http://btcpay.example.com/api/v1'
|
BTCPAY_API_URL='http://btcpay.example.com/api/v1'
|
||||||
|
|||||||
6
Gemfile
6
Gemfile
@@ -40,6 +40,9 @@ gem 'net-ldap'
|
|||||||
gem "rqrcode", "~> 2.0"
|
gem "rqrcode", "~> 2.0"
|
||||||
gem 'rails-settings-cached', '~> 2.8.3'
|
gem 'rails-settings-cached', '~> 2.8.3'
|
||||||
gem 'pagy', '~> 6.0', '>= 6.0.2'
|
gem 'pagy', '~> 6.0', '>= 6.0.2'
|
||||||
|
gem 'flipper'
|
||||||
|
gem 'flipper-active_record'
|
||||||
|
gem 'flipper-ui'
|
||||||
|
|
||||||
# HTTP requests
|
# HTTP requests
|
||||||
gem 'faraday'
|
gem 'faraday'
|
||||||
@@ -48,6 +51,9 @@ gem 'faraday'
|
|||||||
gem 'sidekiq', '< 7'
|
gem 'sidekiq', '< 7'
|
||||||
gem 'sidekiq-scheduler'
|
gem 'sidekiq-scheduler'
|
||||||
|
|
||||||
|
# Service integrations
|
||||||
|
gem 'discourse_api'
|
||||||
|
|
||||||
# Monitoring
|
# Monitoring
|
||||||
gem "sentry-ruby"
|
gem "sentry-ruby"
|
||||||
gem "sentry-rails"
|
gem "sentry-rails"
|
||||||
|
|||||||
30
Gemfile.lock
30
Gemfile.lock
@@ -108,6 +108,11 @@ GEM
|
|||||||
devise (>= 3.4.1)
|
devise (>= 3.4.1)
|
||||||
net-ldap (>= 0.16.0)
|
net-ldap (>= 0.16.0)
|
||||||
diff-lcs (1.5.0)
|
diff-lcs (1.5.0)
|
||||||
|
discourse_api (2.0.0)
|
||||||
|
faraday (~> 2.7)
|
||||||
|
faraday-follow_redirects
|
||||||
|
faraday-multipart
|
||||||
|
rack (>= 1.6)
|
||||||
dotenv (2.8.1)
|
dotenv (2.8.1)
|
||||||
dotenv-rails (2.8.1)
|
dotenv-rails (2.8.1)
|
||||||
dotenv (= 2.8.1)
|
dotenv (= 2.8.1)
|
||||||
@@ -126,8 +131,23 @@ GEM
|
|||||||
faraday (2.7.1)
|
faraday (2.7.1)
|
||||||
faraday-net_http (>= 2.0, < 3.1)
|
faraday-net_http (>= 2.0, < 3.1)
|
||||||
ruby2_keywords (>= 0.0.4)
|
ruby2_keywords (>= 0.0.4)
|
||||||
|
faraday-follow_redirects (0.3.0)
|
||||||
|
faraday (>= 1, < 3)
|
||||||
|
faraday-multipart (1.0.4)
|
||||||
|
multipart-post (~> 2)
|
||||||
faraday-net_http (3.0.2)
|
faraday-net_http (3.0.2)
|
||||||
ffi (1.15.5)
|
ffi (1.15.5)
|
||||||
|
flipper (0.28.0)
|
||||||
|
concurrent-ruby (< 2)
|
||||||
|
flipper-active_record (0.28.0)
|
||||||
|
activerecord (>= 4.2, < 8)
|
||||||
|
flipper (~> 0.28.0)
|
||||||
|
flipper-ui (0.28.0)
|
||||||
|
erubi (>= 1.0.0, < 2.0.0)
|
||||||
|
flipper (~> 0.28.0)
|
||||||
|
rack (>= 1.4, < 3)
|
||||||
|
rack-protection (>= 1.5.3, <= 4.0.0)
|
||||||
|
sanitize (< 7)
|
||||||
fugit (1.7.2)
|
fugit (1.7.2)
|
||||||
et-orbi (~> 1, >= 1.2.7)
|
et-orbi (~> 1, >= 1.2.7)
|
||||||
raabro (~> 1.4)
|
raabro (~> 1.4)
|
||||||
@@ -172,6 +192,7 @@ GEM
|
|||||||
mini_mime (1.1.2)
|
mini_mime (1.1.2)
|
||||||
mini_portile2 (2.8.0)
|
mini_portile2 (2.8.0)
|
||||||
minitest (5.16.3)
|
minitest (5.16.3)
|
||||||
|
multipart-post (2.3.0)
|
||||||
net-imap (0.3.1)
|
net-imap (0.3.1)
|
||||||
net-protocol
|
net-protocol
|
||||||
net-ldap (0.17.1)
|
net-ldap (0.17.1)
|
||||||
@@ -199,6 +220,8 @@ GEM
|
|||||||
raabro (1.4.0)
|
raabro (1.4.0)
|
||||||
racc (1.6.0)
|
racc (1.6.0)
|
||||||
rack (2.2.4)
|
rack (2.2.4)
|
||||||
|
rack-protection (3.0.6)
|
||||||
|
rack
|
||||||
rack-test (2.0.2)
|
rack-test (2.0.2)
|
||||||
rack (>= 1.3)
|
rack (>= 1.3)
|
||||||
rails (7.0.4)
|
rails (7.0.4)
|
||||||
@@ -283,6 +306,9 @@ GEM
|
|||||||
ruby2_keywords (0.0.5)
|
ruby2_keywords (0.0.5)
|
||||||
rufus-scheduler (3.8.2)
|
rufus-scheduler (3.8.2)
|
||||||
fugit (~> 1.1, >= 1.1.6)
|
fugit (~> 1.1, >= 1.1.6)
|
||||||
|
sanitize (6.0.1)
|
||||||
|
crass (~> 1.0.2)
|
||||||
|
nokogiri (>= 1.12.0)
|
||||||
sentry-rails (5.8.0)
|
sentry-rails (5.8.0)
|
||||||
railties (>= 5.0)
|
railties (>= 5.0)
|
||||||
sentry-ruby (~> 5.8.0)
|
sentry-ruby (~> 5.8.0)
|
||||||
@@ -370,10 +396,14 @@ DEPENDENCIES
|
|||||||
database_cleaner
|
database_cleaner
|
||||||
devise (~> 4.9.0)
|
devise (~> 4.9.0)
|
||||||
devise_ldap_authenticatable
|
devise_ldap_authenticatable
|
||||||
|
discourse_api
|
||||||
dotenv-rails
|
dotenv-rails
|
||||||
factory_bot_rails
|
factory_bot_rails
|
||||||
faker
|
faker
|
||||||
faraday
|
faraday
|
||||||
|
flipper
|
||||||
|
flipper-active_record
|
||||||
|
flipper-ui
|
||||||
importmap-rails
|
importmap-rails
|
||||||
jbuilder (~> 2.7)
|
jbuilder (~> 2.7)
|
||||||
letter_opener
|
letter_opener
|
||||||
|
|||||||
@@ -114,6 +114,10 @@ command:
|
|||||||
* [Sidekiq](https://github.com/mperham/sidekiq/wiki/)
|
* [Sidekiq](https://github.com/mperham/sidekiq/wiki/)
|
||||||
* [ActiveJob](https://github.com/mperham/sidekiq/wiki/Active-Job)
|
* [ActiveJob](https://github.com/mperham/sidekiq/wiki/Active-Job)
|
||||||
|
|
||||||
|
### Feature Flags
|
||||||
|
|
||||||
|
* [Flipper](https://www.flippercloud.io/docs/get-started/self-hosted)
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
[GNU Affero General Public License v3.0](https://choosealicense.com/licenses/agpl-3.0/)
|
[GNU Affero General Public License v3.0](https://choosealicense.com/licenses/agpl-3.0/)
|
||||||
|
|||||||
3
app/components/header_tab_link_component.html.erb
Normal file
3
app/components/header_tab_link_component.html.erb
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<%= link_to @path, class: @link_class do %>
|
||||||
|
<%= @name %>
|
||||||
|
<% end %>
|
||||||
20
app/components/header_tab_link_component.rb
Normal file
20
app/components/header_tab_link_component.rb
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class HeaderTabLinkComponent < ViewComponent::Base
|
||||||
|
def initialize(name:, path:, active: false, disabled: false)
|
||||||
|
@name = name
|
||||||
|
@path = path
|
||||||
|
@active = active
|
||||||
|
@disabled = disabled
|
||||||
|
@link_class = class_names_link(path)
|
||||||
|
end
|
||||||
|
|
||||||
|
def class_names_link(path)
|
||||||
|
common = "block md:inline-block px-5 py-2 rounded-md font-medium text-base md:text-xl"
|
||||||
|
if @active
|
||||||
|
"#{common} bg-gray-900/50 text-white"
|
||||||
|
else
|
||||||
|
"#{common} text-gray-300 hover:bg-gray-900/30 hover:text-white active:bg-gray-900/30 active:text-white"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
12
app/components/header_with_tabs_component.html.erb
Normal file
12
app/components/header_with_tabs_component.html.erb
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<header class="py-10">
|
||||||
|
<div class="max-w-6xl md:flex md:gap-x-10 mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
<% if @title.present? %>
|
||||||
|
<h1 class="text-3xl font-bold text-white">
|
||||||
|
<%= @title %>
|
||||||
|
</h1>
|
||||||
|
<% end %>
|
||||||
|
<nav class="md:grow flex gap-x-4 <%= @title.present? ? "justify-end" : "justify-start" %>" aria-label="Tabs">
|
||||||
|
<%= render partial: @tabnav_partial %>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
8
app/components/header_with_tabs_component.rb
Normal file
8
app/components/header_with_tabs_component.rb
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class HeaderWithTabsComponent < ViewComponent::Base
|
||||||
|
def initialize(title: nil, tabnav_partial:)
|
||||||
|
@title = title
|
||||||
|
@tabnav_partial = tabnav_partial
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
class AccountController < ApplicationController
|
class AccountController < ApplicationController
|
||||||
before_action :require_user_signed_in
|
before_action :authenticate_user!
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@current_section = :account
|
@current_section = :account
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
class Contributions::DonationsController < ApplicationController
|
class Contributions::DonationsController < ApplicationController
|
||||||
before_action :require_user_signed_in
|
before_action :authenticate_user!
|
||||||
|
|
||||||
# GET /donations
|
# GET /donations
|
||||||
# GET /donations.json
|
# GET /donations.json
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
class Contributions::ProjectsController < ApplicationController
|
class Contributions::ProjectsController < ApplicationController
|
||||||
before_action :require_user_signed_in
|
before_action :authenticate_user!
|
||||||
|
|
||||||
# GET /contributions
|
# GET /contributions
|
||||||
def index
|
def index
|
||||||
|
|||||||
17
app/controllers/discourse/sso_controller.rb
Normal file
17
app/controllers/discourse/sso_controller.rb
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
class Discourse::SsoController < ApplicationController
|
||||||
|
before_action :authenticate_user!
|
||||||
|
|
||||||
|
def connect
|
||||||
|
secret = Setting.discourse_connect_secret
|
||||||
|
sso = DiscourseApi::SingleSignOn.parse(request.query_string, secret)
|
||||||
|
sso.external_id = current_user.id
|
||||||
|
sso.email = current_user.email
|
||||||
|
sso.username = current_user.cn
|
||||||
|
sso.name = current_user.display_name
|
||||||
|
sso.admin = current_user.is_admin?
|
||||||
|
sso.sso_secret = secret
|
||||||
|
|
||||||
|
redirect_to sso.to_url("#{Setting.discourse_public_url}/session/sso_login"),
|
||||||
|
allow_other_host: true
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
class InvitationsController < ApplicationController
|
class InvitationsController < ApplicationController
|
||||||
before_action :require_user_signed_in, except: ["show"]
|
before_action :authenticate_user!, except: ["show"]
|
||||||
before_action :require_user_signed_out, only: ["show"]
|
before_action :require_user_signed_out, only: ["show"]
|
||||||
|
|
||||||
# GET /invitations
|
# GET /invitations
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
require "rqrcode"
|
require "rqrcode"
|
||||||
|
|
||||||
class Services::LightningController < ApplicationController
|
class Services::LightningController < ApplicationController
|
||||||
before_action :require_user_signed_in
|
before_action :authenticate_user!
|
||||||
before_action :authenticate_with_lndhub
|
before_action :authenticate_with_lndhub
|
||||||
before_action :set_current_section
|
before_action :set_current_section
|
||||||
before_action :fetch_balance
|
before_action :fetch_balance
|
||||||
@@ -37,8 +37,8 @@ class Services::LightningController < ApplicationController
|
|||||||
session[:ln_auth_token] = auth_token
|
session[:ln_auth_token] = auth_token
|
||||||
@ln_auth_token = auth_token
|
@ln_auth_token = auth_token
|
||||||
end
|
end
|
||||||
rescue
|
rescue => e
|
||||||
# TODO add exception tracking
|
Sentry.capture_exception(e) if Setting.sentry_enabled?
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_current_section
|
def set_current_section
|
||||||
@@ -49,9 +49,9 @@ class Services::LightningController < ApplicationController
|
|||||||
lndhub = Lndhub.new
|
lndhub = Lndhub.new
|
||||||
data = lndhub.balance @ln_auth_token
|
data = lndhub.balance @ln_auth_token
|
||||||
@balance = data["BTC"]["AvailableBalance"] rescue nil
|
@balance = data["BTC"]["AvailableBalance"] rescue nil
|
||||||
rescue
|
rescue AuthError
|
||||||
authenticate_with_lndhub(force_reauth: true)
|
authenticate_with_lndhub(force_reauth: true)
|
||||||
return nil if @fetch_balance_retried
|
raise if @fetch_balance_retried
|
||||||
@fetch_balance_retried = true
|
@fetch_balance_retried = true
|
||||||
fetch_balance
|
fetch_balance
|
||||||
end
|
end
|
||||||
@@ -61,9 +61,9 @@ class Services::LightningController < ApplicationController
|
|||||||
txs = lndhub.gettxs @ln_auth_token
|
txs = lndhub.gettxs @ln_auth_token
|
||||||
invoices = lndhub.getuserinvoices(@ln_auth_token).select{|i| i["ispaid"]}
|
invoices = lndhub.getuserinvoices(@ln_auth_token).select{|i| i["ispaid"]}
|
||||||
process_transactions(txs + invoices)
|
process_transactions(txs + invoices)
|
||||||
rescue
|
rescue AuthError
|
||||||
authenticate_with_lndhub(force_reauth: true)
|
authenticate_with_lndhub(force_reauth: true)
|
||||||
return [] if @fetch_transactions_retried
|
raise if @fetch_transactions_retried
|
||||||
@fetch_transactions_retried = true
|
@fetch_transactions_retried = true
|
||||||
fetch_transactions
|
fetch_transactions
|
||||||
end
|
end
|
||||||
@@ -86,6 +86,10 @@ class Services::LightningController < ApplicationController
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Handle an edge case where lndhub.go includes a failed payment in the
|
||||||
|
# list, which wasn't actually booked
|
||||||
|
txs.reject!{ |tx| tx["type"] == "paid_invoice" && tx["payment_preimage"].blank? }
|
||||||
|
|
||||||
txs.sort{ |a,b| b["datetime"] <=> a["datetime"] }
|
txs.sort{ |a,b| b["datetime"] <=> a["datetime"] }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
30
app/controllers/services/remotestorage_controller.rb
Normal file
30
app/controllers/services/remotestorage_controller.rb
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
class Services::RemotestorageController < ApplicationController
|
||||||
|
before_action :require_user_signed_in
|
||||||
|
before_action :require_service_enabled
|
||||||
|
before_action :require_feature_enabled
|
||||||
|
before_action :set_current_section
|
||||||
|
|
||||||
|
def dashboard
|
||||||
|
# unless current_user.services_enabled.include?(:remotestorage)
|
||||||
|
# redirect_to service_remotestorage_info_path
|
||||||
|
# end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def require_feature_enabled
|
||||||
|
unless Flipper.enabled?(:remotestorage, current_user)
|
||||||
|
http_status :forbidden
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def require_service_enabled
|
||||||
|
unless Setting.remotestorage_enabled?
|
||||||
|
http_status :not_found
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_current_section
|
||||||
|
@current_section = :services
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -2,35 +2,41 @@ class SettingsController < ApplicationController
|
|||||||
before_action :authenticate_user!
|
before_action :authenticate_user!
|
||||||
before_action :set_main_nav_section
|
before_action :set_main_nav_section
|
||||||
before_action :set_settings_section, only: [:show, :update, :update_email]
|
before_action :set_settings_section, only: [:show, :update, :update_email]
|
||||||
|
before_action :set_user, only: [:show, :update, :update_email]
|
||||||
|
|
||||||
def index
|
def index
|
||||||
redirect_to setting_path(:profile)
|
redirect_to setting_path(:profile)
|
||||||
end
|
end
|
||||||
|
|
||||||
def show
|
def show
|
||||||
@user = current_user
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
@user = current_user
|
@user.preferences.merge!(user_params[:preferences] || {})
|
||||||
@user.preferences.merge! user_params[:preferences]
|
@user.display_name = user_params[:display_name]
|
||||||
@user.save!
|
|
||||||
|
|
||||||
redirect_to setting_path(@settings_section), flash: {
|
if @user.save
|
||||||
success: 'Settings saved.'
|
if @user.display_name && (@user.display_name != @user.ldap_entry[:display_name])
|
||||||
}
|
LdapManager::UpdateDisplayName.call(@user.dn, user_params[:display_name])
|
||||||
|
end
|
||||||
|
|
||||||
|
redirect_to setting_path(@settings_section), flash: {
|
||||||
|
success: 'Settings saved.'
|
||||||
|
}
|
||||||
|
else
|
||||||
|
@validation_errors = @user.errors
|
||||||
|
render :show, status: :unprocessable_entity
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def update_email
|
def update_email
|
||||||
if current_user.valid_ldap_authentication?(email_params[:current_password])
|
if @user.valid_ldap_authentication?(email_params[:current_password])
|
||||||
current_user.email = email_params[:email]
|
if @user.update email: email_params[:email]
|
||||||
|
|
||||||
if current_user.update email: email_params[:email]
|
|
||||||
redirect_to setting_path(:account), flash: {
|
redirect_to setting_path(:account), flash: {
|
||||||
notice: 'Please confirm your new address using the confirmation link we just sent you.'
|
notice: 'Please confirm your new address using the confirmation link we just sent you.'
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@validation_errors = current_user.errors
|
@validation_errors = @user.errors
|
||||||
render :show, status: :unprocessable_entity
|
render :show, status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
@@ -49,27 +55,31 @@ class SettingsController < ApplicationController
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def set_main_nav_section
|
def set_main_nav_section
|
||||||
@current_section = :settings
|
@current_section = :settings
|
||||||
end
|
|
||||||
|
|
||||||
def set_settings_section
|
|
||||||
@settings_section = params[:section]
|
|
||||||
allowed_sections = [:profile, :account, :lightning, :xmpp]
|
|
||||||
|
|
||||||
unless allowed_sections.include?(@settings_section.to_sym)
|
|
||||||
redirect_to setting_path(:profile)
|
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
def user_params
|
def set_settings_section
|
||||||
params.require(:user).permit(preferences: [
|
@settings_section = params[:section]
|
||||||
:lightning_notify_sats_received,
|
allowed_sections = [:profile, :account, :lightning, :xmpp]
|
||||||
:xmpp_exchange_contacts_with_invitees
|
|
||||||
])
|
|
||||||
end
|
|
||||||
|
|
||||||
def email_params
|
unless allowed_sections.include?(@settings_section.to_sym)
|
||||||
params.require(:user).permit(:email, :current_password)
|
redirect_to setting_path(:profile)
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_user
|
||||||
|
@user = current_user
|
||||||
|
end
|
||||||
|
|
||||||
|
def user_params
|
||||||
|
params.require(:user).permit(:display_name, preferences: [
|
||||||
|
:lightning_notify_sats_received,
|
||||||
|
:xmpp_exchange_contacts_with_invitees
|
||||||
|
])
|
||||||
|
end
|
||||||
|
|
||||||
|
def email_params
|
||||||
|
params.require(:user).permit(:email, :current_password)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
1
app/errors/auth_error.rb
Normal file
1
app/errors/auth_error.rb
Normal file
@@ -0,0 +1 @@
|
|||||||
|
class AuthError < StandardError; end
|
||||||
@@ -2,6 +2,9 @@
|
|||||||
class Setting < RailsSettings::Base
|
class Setting < RailsSettings::Base
|
||||||
cache_prefix { "v1" }
|
cache_prefix { "v1" }
|
||||||
|
|
||||||
|
field :accounts_domain, type: :string,
|
||||||
|
default: ENV["AKKOUNTS_DOMAIN"].presence
|
||||||
|
|
||||||
#
|
#
|
||||||
# Internal services
|
# Internal services
|
||||||
#
|
#
|
||||||
@@ -41,6 +44,9 @@ class Setting < RailsSettings::Base
|
|||||||
field :discourse_enabled, type: :boolean,
|
field :discourse_enabled, type: :boolean,
|
||||||
default: (ENV["DISCOURSE_PUBLIC_URL"].present?.to_s || false)
|
default: (ENV["DISCOURSE_PUBLIC_URL"].present?.to_s || false)
|
||||||
|
|
||||||
|
field :discourse_connect_secret, type: :string, readonly: true,
|
||||||
|
default: ENV["DISCOURSE_CONNECT_SECRET"].presence
|
||||||
|
|
||||||
#
|
#
|
||||||
# ejabberd
|
# ejabberd
|
||||||
#
|
#
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
class User < ApplicationRecord
|
class User < ApplicationRecord
|
||||||
include EmailValidatable
|
include EmailValidatable
|
||||||
|
|
||||||
|
attr_accessor :display_name
|
||||||
|
|
||||||
serialize :preferences, UserPreferences
|
serialize :preferences, UserPreferences
|
||||||
|
|
||||||
# Relations
|
# Relations
|
||||||
@@ -17,7 +19,7 @@ class User < ApplicationRecord
|
|||||||
has_many :accounts, through: :lndhub_user
|
has_many :accounts, through: :lndhub_user
|
||||||
|
|
||||||
validates_uniqueness_of :cn
|
validates_uniqueness_of :cn
|
||||||
validates_length_of :cn, :minimum => 3
|
validates_length_of :cn, minimum: 3
|
||||||
validates_format_of :cn, with: /\A([a-z0-9\-])*\z/,
|
validates_format_of :cn, with: /\A([a-z0-9\-])*\z/,
|
||||||
if: Proc.new{ |u| u.cn.present? },
|
if: Proc.new{ |u| u.cn.present? },
|
||||||
message: "is invalid. Please use only letters, numbers and -"
|
message: "is invalid. Please use only letters, numbers and -"
|
||||||
@@ -31,6 +33,9 @@ class User < ApplicationRecord
|
|||||||
validates_uniqueness_of :email
|
validates_uniqueness_of :email
|
||||||
validates :email, email: true
|
validates :email, email: true
|
||||||
|
|
||||||
|
validates_length_of :display_name, minimum: 3, maximum: 35, allow_blank: true,
|
||||||
|
if: -> { defined?(@display_name) }
|
||||||
|
|
||||||
scope :confirmed, -> { where.not(confirmed_at: nil) }
|
scope :confirmed, -> { where.not(confirmed_at: nil) }
|
||||||
scope :pending, -> { where(confirmed_at: nil) }
|
scope :pending, -> { where(confirmed_at: nil) }
|
||||||
|
|
||||||
@@ -115,8 +120,13 @@ class User < ApplicationRecord
|
|||||||
@dn = Devise::LDAP::Adapter.get_dn(self.cn)
|
@dn = Devise::LDAP::Adapter.get_dn(self.cn)
|
||||||
end
|
end
|
||||||
|
|
||||||
def ldap_entry
|
def ldap_entry(reload: false)
|
||||||
ldap.fetch_users(uid: self.cn, ou: self.ou).first
|
return @ldap_entry if defined?(@ldap_entry) && !reload
|
||||||
|
@ldap_entry = ldap.fetch_users(uid: self.cn, ou: self.ou).first
|
||||||
|
end
|
||||||
|
|
||||||
|
def display_name
|
||||||
|
@display_name ||= ldap_entry[:display_name]
|
||||||
end
|
end
|
||||||
|
|
||||||
def services_enabled
|
def services_enabled
|
||||||
|
|||||||
12
app/services/ldap_manager/update_display_name.rb
Normal file
12
app/services/ldap_manager/update_display_name.rb
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
module LdapManager
|
||||||
|
class UpdateDisplayName < LdapManagerService
|
||||||
|
def initialize(dn, display_name)
|
||||||
|
@dn = dn
|
||||||
|
@display_name = display_name
|
||||||
|
end
|
||||||
|
|
||||||
|
def call
|
||||||
|
replace_attribute @dn, :displayName, @display_name
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -6,7 +6,7 @@ module LdapManager
|
|||||||
end
|
end
|
||||||
|
|
||||||
def call
|
def call
|
||||||
replace_attribute @dn, :mail, [ @address ]
|
replace_attribute @dn, :mail, @address
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ class LdapService < ApplicationService
|
|||||||
treebase = ldap_config["base"]
|
treebase = ldap_config["base"]
|
||||||
end
|
end
|
||||||
|
|
||||||
attributes = %w{dn cn uid mail admin service}
|
attributes = %w{dn cn uid mail displayName admin service}
|
||||||
filter = Net::LDAP::Filter.eq("uid", args[:uid] || "*")
|
filter = Net::LDAP::Filter.eq("uid", args[:uid] || "*")
|
||||||
|
|
||||||
entries = ldap_client.search(base: treebase, filter: filter, attributes: attributes)
|
entries = ldap_client.search(base: treebase, filter: filter, attributes: attributes)
|
||||||
@@ -59,6 +59,7 @@ class LdapService < ApplicationService
|
|||||||
{
|
{
|
||||||
uid: e.uid.first,
|
uid: e.uid.first,
|
||||||
mail: e.try(:mail) ? e.mail.first : nil,
|
mail: e.try(:mail) ? e.mail.first : nil,
|
||||||
|
display_name: e.try(:displayName) ? e.displayName.first : nil,
|
||||||
admin: e.try(:admin) ? 'admin' : nil,
|
admin: e.try(:admin) ? 'admin' : nil,
|
||||||
service: e.try(:service)
|
service: e.try(:service)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,12 +12,7 @@ class Lndhub
|
|||||||
end
|
end
|
||||||
|
|
||||||
res = Faraday.post "#{@base_url}/#{endpoint}", payload.to_json, headers
|
res = Faraday.post "#{@base_url}/#{endpoint}", payload.to_json, headers
|
||||||
|
log_error(res) if res.status != 200
|
||||||
if res.status != 200
|
|
||||||
Rails.logger.error "[lndhub] API request failed:"
|
|
||||||
Rails.logger.error res.body
|
|
||||||
#TODO add some kind of exception tracking/notifications
|
|
||||||
end
|
|
||||||
|
|
||||||
JSON.parse(res.body)
|
JSON.parse(res.body)
|
||||||
end
|
end
|
||||||
@@ -31,7 +26,7 @@ class Lndhub
|
|||||||
data = JSON.parse(res.body)
|
data = JSON.parse(res.body)
|
||||||
|
|
||||||
if data.is_a?(Hash) && data["error"] && data["message"] == "bad auth"
|
if data.is_a?(Hash) && data["error"] && data["message"] == "bad auth"
|
||||||
raise "BAD_AUTH"
|
raise AuthError
|
||||||
else
|
else
|
||||||
data
|
data
|
||||||
end
|
end
|
||||||
@@ -68,4 +63,13 @@ class Lndhub
|
|||||||
|
|
||||||
invoice["payment_request"]
|
invoice["payment_request"]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def log_error(res)
|
||||||
|
Rails.logger.error "[lndhub] API request failed:"
|
||||||
|
Rails.logger.error res.body
|
||||||
|
|
||||||
|
if Setting.sentry_enabled?
|
||||||
|
Sentry.capture_message("Lndhub API request failed: #{res.body}")
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,9 +1,4 @@
|
|||||||
class LndhubV2
|
class LndhubV2 < Lndhub
|
||||||
attr_accessor :auth_token
|
|
||||||
|
|
||||||
def initialize
|
|
||||||
@base_url = ENV["LNDHUB_API_URL"]
|
|
||||||
end
|
|
||||||
|
|
||||||
def post(endpoint, payload, options={})
|
def post(endpoint, payload, options={})
|
||||||
headers = { "Content-Type" => "application/json" }
|
headers = { "Content-Type" => "application/json" }
|
||||||
@@ -12,64 +7,12 @@ class LndhubV2
|
|||||||
elsif options[:admin_token]
|
elsif options[:admin_token]
|
||||||
headers.merge!({ "Authorization" => "Bearer #{options[:admin_token]}" })
|
headers.merge!({ "Authorization" => "Bearer #{options[:admin_token]}" })
|
||||||
end
|
end
|
||||||
|
|
||||||
res = Faraday.post "#{@base_url}/#{endpoint}", payload.to_json, headers
|
res = Faraday.post "#{@base_url}/#{endpoint}", payload.to_json, headers
|
||||||
|
log_error(res) if res.status != 200
|
||||||
if res.status != 200
|
|
||||||
Rails.logger.error "[lndhub] API request failed:"
|
|
||||||
Rails.logger.error res.body
|
|
||||||
#TODO add some kind of exception tracking/notifications
|
|
||||||
end
|
|
||||||
|
|
||||||
JSON.parse(res.body)
|
JSON.parse(res.body)
|
||||||
end
|
end
|
||||||
|
|
||||||
def get(endpoint, auth_token)
|
|
||||||
res = Faraday.get("#{@base_url}/#{endpoint}", {}, {
|
|
||||||
"Content-Type" => "application/json",
|
|
||||||
"Accept" => "application/json",
|
|
||||||
"Authorization" => "Bearer #{auth_token}"
|
|
||||||
})
|
|
||||||
|
|
||||||
JSON.parse(res.body)
|
|
||||||
end
|
|
||||||
|
|
||||||
def create(payload)
|
|
||||||
post "create", payload
|
|
||||||
end
|
|
||||||
|
|
||||||
def authenticate(user)
|
|
||||||
credentials = post "auth?type=auth", { login: user.ln_account, password: user.ln_password }
|
|
||||||
self.auth_token = credentials["access_token"]
|
|
||||||
self.auth_token
|
|
||||||
end
|
|
||||||
|
|
||||||
def balance(user_token=nil)
|
|
||||||
get "balance", user_token || auth_token
|
|
||||||
end
|
|
||||||
|
|
||||||
def gettxs(user_token)
|
|
||||||
get "gettxs", user_token || auth_token
|
|
||||||
end
|
|
||||||
|
|
||||||
def getuserinvoices(user_token)
|
|
||||||
get "getuserinvoices", user_token || auth_token
|
|
||||||
end
|
|
||||||
|
|
||||||
def addinvoice(payload)
|
|
||||||
invoice = post "addinvoice", {
|
|
||||||
amt: payload[:amount],
|
|
||||||
memo: payload[:memo],
|
|
||||||
description_hash: payload[:description_hash]
|
|
||||||
}
|
|
||||||
|
|
||||||
invoice["payment_request"]
|
|
||||||
end
|
|
||||||
|
|
||||||
#
|
|
||||||
# V2
|
|
||||||
#
|
|
||||||
|
|
||||||
def create_account(payload={})
|
def create_account(payload={})
|
||||||
post "v2/users", payload, admin_token: Rails.application.credentials.lndhub[:admin_token]
|
post "v2/users", payload, admin_token: Rails.application.credentials.lndhub[:admin_token]
|
||||||
end
|
end
|
||||||
@@ -78,4 +21,5 @@ class LndhubV2
|
|||||||
# Payload: { amount: 1000, description: "", description_hash: "" }
|
# Payload: { amount: 1000, description: "", description_hash: "" }
|
||||||
post "v2/invoices", payload
|
post "v2/invoices", payload
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -7,11 +7,46 @@
|
|||||||
title: "Enable Discourse integration",
|
title: "Enable Discourse integration",
|
||||||
description: "Discourse configuration present and features enabled"
|
description: "Discourse configuration present and features enabled"
|
||||||
) %>
|
) %>
|
||||||
<% if Setting.discourse_enabled? %>
|
<% if Setting.discourse_enabled? %>
|
||||||
<%= render FormElements::FieldsetComponent.new(title: "Public URL") do %>
|
<%= render FormElements::FieldsetComponent.new(title: "Public URL") do %>
|
||||||
<%= f.text_field :discourse_public_url,
|
<%= f.text_field :discourse_public_url,
|
||||||
value: Setting.discourse_public_url,
|
value: Setting.discourse_public_url,
|
||||||
class: "w-full", disabled: true %>
|
class: "w-full", disabled: true %>
|
||||||
<% end %>
|
|
||||||
<% end %>
|
<% end %>
|
||||||
|
<%= render FormElements::FieldsetComponent.new(title: "Connect secret") do %>
|
||||||
|
<%= f.password_field :discourse_connect_secret,
|
||||||
|
value: Setting.discourse_connect_secret,
|
||||||
|
class: "w-full", disabled: true %>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
</ul>
|
</ul>
|
||||||
|
<% if Setting.discourse_enabled? %>
|
||||||
|
<% content_for :documentation do %>
|
||||||
|
<h3 class="mt-8">How to configure Discourse</h3>
|
||||||
|
<ol class="list-decimal list-inside">
|
||||||
|
<li class="mb-6">
|
||||||
|
Set the <strong>Discourse Connect URL</strong> to the following URL:
|
||||||
|
</li>
|
||||||
|
<li data-controller="clipboard" class="mb-6 flex gap-1">
|
||||||
|
<input type="text" class="grow" disabled="disabled"
|
||||||
|
value="https://<%= Setting.accounts_domain %>/discourse/connect"
|
||||||
|
data-clipboard-target="source" />
|
||||||
|
<button class="btn-md btn-icon btn-blue shrink-0"
|
||||||
|
data-clipboard-target="trigger" data-action="clipboard#copy"
|
||||||
|
title="Copy to clipboard">
|
||||||
|
<span class="content-initial">
|
||||||
|
<%= render partial: "icons/copy", locals: { custom_class: "text-white h-4 w-4 inline" } %>
|
||||||
|
</span>
|
||||||
|
<span class="content-active hidden">
|
||||||
|
<%= render partial: "icons/check", locals: { custom_class: "text-white h-4 w-4 inline" } %>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li class="mb-6">
|
||||||
|
Set the <strong>Discourse Connect Secret</strong> to the value above.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Enable Discourse Connect.
|
||||||
|
</li>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
class: "w-full", disabled: true %>
|
class: "w-full", disabled: true %>
|
||||||
<% end %>
|
<% end %>
|
||||||
</ul>
|
</ul>
|
||||||
<h3 class="mt-8">User default settings</h3>
|
<h3 class="mt-10">User default settings</h3>
|
||||||
<ul role="list">
|
<ul role="list">
|
||||||
<%= render FormElements::FieldsetComponent.new(
|
<%= render FormElements::FieldsetComponent.new(
|
||||||
title: "Default rooms",
|
title: "Default rooms",
|
||||||
|
|||||||
@@ -20,4 +20,10 @@
|
|||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
|
<% if content_for?(:documentation) %>
|
||||||
|
<section>
|
||||||
|
<%= yield :documentation %>
|
||||||
|
</section>
|
||||||
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|||||||
@@ -6,6 +6,10 @@
|
|||||||
<h3>Account</h3>
|
<h3>Account</h3>
|
||||||
<table class="divided">
|
<table class="divided">
|
||||||
<tbody>
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
|
<td><%= @user.id %></td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Created at</th>
|
<th>Created at</th>
|
||||||
<td><%= @user.created_at.strftime("%Y-%m-%d (%H:%M UTC)") %></td>
|
<td><%= @user.created_at.strftime("%Y-%m-%d (%H:%M UTC)") %></td>
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
<%= render HeaderComponent.new(title: "Contributions") %>
|
<%# <%= render HeaderComponent.new(title: "Contributions") %>
|
||||||
|
<%= render HeaderWithTabsComponent.new(
|
||||||
|
# title: "Contributions",
|
||||||
|
tabnav_partial: "shared/tabnav_contributions"
|
||||||
|
) %>
|
||||||
|
|
||||||
<%= render MainWithTabnavComponent.new(tabnav_partial: "shared/tabnav_contributions") do %>
|
<%= render MainSimpleComponent.new do %>
|
||||||
<section>
|
<section>
|
||||||
<% if @donations.any? %>
|
<% if @donations.any? %>
|
||||||
<p class="mb-12">
|
<p class="mb-12">
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
<%= render HeaderComponent.new(title: "Contributions") %>
|
<%= render HeaderWithTabsComponent.new(
|
||||||
|
# title: "Contributions",
|
||||||
|
tabnav_partial: "shared/tabnav_contributions"
|
||||||
|
) %>
|
||||||
|
|
||||||
<%= render MainWithTabnavComponent.new(tabnav_partial: "shared/tabnav_contributions") do %>
|
<%= render MainSimpleComponent.new do %>
|
||||||
<section>
|
<section>
|
||||||
<p class="mb-8">
|
<p class="mb-8">
|
||||||
Project contributions are how we develop and run all Kosmos software and
|
Project contributions are how we develop and run all Kosmos software and
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
<div class="border border-gray-300 rounded-md hover:border-gray-400
|
<div class="border border-gray-300 rounded-md hover:border-gray-400
|
||||||
bg-[length:95%] bg-center bg-no-repeat
|
bg-[length:95%] bg-center bg-no-repeat
|
||||||
bg-[url(/img/logos/icon_discourse.svg)]">
|
bg-[url(/img/logos/icon_discourse.svg)]">
|
||||||
<%= link_to "https://community.kosmos.org",
|
<%= link_to "#{Setting.discourse_public_url}/session/sso?return_path=/",
|
||||||
class: "block h-full px-6 py-6 rounded-md" do %>
|
class: "block h-full px-6 py-6 rounded-md" do %>
|
||||||
<h3 class="mb-3.5">Discourse</h3>
|
<h3 class="mb-3.5">Discourse</h3>
|
||||||
<p class="text-gray-600">
|
<p class="text-gray-600">
|
||||||
@@ -73,6 +73,17 @@
|
|||||||
</p>
|
</p>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
|
<% if Setting.remotestorage_enabled? && Flipper.enabled?(:remotestorage, current_user) %>
|
||||||
|
<div class="border border-gray-300 rounded-md hover:border-gray-400">
|
||||||
|
<%= link_to services_storage_path,
|
||||||
|
class: "block h-full px-6 py-6 rounded-md" do %>
|
||||||
|
<h3 class="mb-3.5">Storage</h3>
|
||||||
|
<p class="text-gray-600">
|
||||||
|
Sync your data between apps and devices
|
||||||
|
</p>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
<!-- <div class="border border-gray-300 rounded-md hover:border-gray-400 -->
|
<!-- <div class="border border-gray-300 rounded-md hover:border-gray-400 -->
|
||||||
<!-- bg-[length:80%] bg-[right_top_-30px] bg-no-repeat -->
|
<!-- bg-[length:80%] bg-[right_top_-30px] bg-no-repeat -->
|
||||||
<!-- bg-[url(/img/logos/icon_mastodon.svg)]"> -->
|
<!-- bg-[url(/img/logos/icon_mastodon.svg)]"> -->
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
|
<%
|
||||||
|
# TODO remove when https://github.com/hotwired/turbo/issues/203 is fixed
|
||||||
|
enable_turbo = !session[:user_return_to] || !session[:user_return_to].match?('/discourse/connect')
|
||||||
|
%>
|
||||||
|
|
||||||
<%= render HeaderCompactComponent.new(title: "Log in") %>
|
<%= render HeaderCompactComponent.new(title: "Log in") %>
|
||||||
|
|
||||||
<%= render MainCompactComponent.new do %>
|
<%= render MainCompactComponent.new do %>
|
||||||
<%= form_for(resource, as: resource_name, url: session_path(resource_name)) do |f| %>
|
<%= form_for(resource, as: resource_name, url: session_path(resource_name),
|
||||||
|
data: { turbo: enable_turbo.to_s }) do |f| %>
|
||||||
<%= render "devise/shared/error_messages", resource: resource %>
|
<%= render "devise/shared/error_messages", resource: resource %>
|
||||||
<div class="mb-6">
|
<div class="mb-6">
|
||||||
<%= f.label :cn, 'User', class: 'block mb-2 font-bold' %>
|
<%= f.label :cn, 'User', class: 'block mb-2 font-bold' %>
|
||||||
|
|||||||
7
app/views/services/remotestorage/dashboard.html.erb
Normal file
7
app/views/services/remotestorage/dashboard.html.erb
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<%= render HeaderComponent.new(title: "Storage") %>
|
||||||
|
|
||||||
|
<%= render MainSimpleComponent.new do %>
|
||||||
|
<section>
|
||||||
|
<h3>Feature enabled</h3>
|
||||||
|
</section>
|
||||||
|
<% end %>
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
"settings--account--email-validation-failed-value": @validation_errors.present?
|
"settings--account--email-validation-failed-value": @validation_errors.present?
|
||||||
} do %>
|
} do %>
|
||||||
<h3>E-Mail</h3>
|
<h3>E-Mail</h3>
|
||||||
<%= form_for(current_user, url: update_email_settings_path, method: "post") do |f| %>
|
<%= form_for(@user, url: update_email_settings_path, method: "post") do |f| %>
|
||||||
<%= hidden_field_tag :section, "account" %>
|
<%= hidden_field_tag :section, "account" %>
|
||||||
<p class="mb-2">
|
<p class="mb-2">
|
||||||
<%= f.label :email, 'Address', class: 'font-bold' %>
|
<%= f.label :email, 'Address', class: 'font-bold' %>
|
||||||
@@ -34,7 +34,7 @@
|
|||||||
<%= f.password_field :current_password, class: "w-full", required: true %>
|
<%= f.password_field :current_password, class: "w-full", required: true %>
|
||||||
</p>
|
</p>
|
||||||
<p class="mt-6">
|
<p class="mt-6">
|
||||||
<%= f.submit "Update", class: "btn-md btn-blue" %>
|
<%= f.submit "Update", class: "btn-md btn-blue w-full md:w-auto" %>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|||||||
@@ -21,10 +21,15 @@
|
|||||||
<p class="text-sm text-gray-500">
|
<p class="text-sm text-gray-500">
|
||||||
Your user address for Chat and Lightning Network.
|
Your user address for Chat and Lightning Network.
|
||||||
</p>
|
</p>
|
||||||
|
<%= form_for(@user, url: setting_path(:profile), html: { :method => :put }) do |f| %>
|
||||||
<%# <%= form_for(@user, as: "profile", url: settings_profile_path) do |f| %>
|
<%= render FormElements::FieldsetComponent.new(tag: "div", title: "Display name") do %>
|
||||||
<%# <p class="mt-8">
|
<%= f.text_field :display_name, class: "w-full sm:w-3/5 mb-2" %>
|
||||||
<%# <%= f.submit "Save changes", class: 'btn-md btn-blue w-full sm:w-auto' %>
|
<% if @validation_errors.present? && @validation_errors[:display_name].present? %>
|
||||||
<%# </p>
|
<p class="error-msg"><%= @validation_errors[:display_name].first %></p>
|
||||||
<%# <% end %>
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
<p class="mt-8 pt-6 border-t border-gray-200 text-right">
|
||||||
|
<%= f.submit 'Save', class: "btn-md btn-blue w-full md:w-auto" %>
|
||||||
|
</p>
|
||||||
|
<% end %>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
<%= render SidenavLinkComponent.new(
|
<%= render SidenavLinkComponent.new(
|
||||||
name: "Profile", path: setting_path(:profile), icon: "user",
|
name: "Profile", path: setting_path(:profile), icon: "user",
|
||||||
active: current_page?(setting_path(:profile))
|
active: @settings_section.to_s == "profile"
|
||||||
) %>
|
) %>
|
||||||
<%= render SidenavLinkComponent.new(
|
<%= render SidenavLinkComponent.new(
|
||||||
name: "Account", path: setting_path(:account), icon: "key",
|
name: "Account", path: setting_path(:account), icon: "key",
|
||||||
active: current_page?(setting_path(:account))
|
active: @settings_section.to_s == "account"
|
||||||
) %>
|
) %>
|
||||||
<% if Setting.ejabberd_enabled %>
|
<% if Setting.ejabberd_enabled %>
|
||||||
<%= render SidenavLinkComponent.new(
|
<%= render SidenavLinkComponent.new(
|
||||||
name: "Chat", path: setting_path(:xmpp), icon: "message-circle",
|
name: "Chat", path: setting_path(:xmpp), icon: "message-circle",
|
||||||
active: current_page?(setting_path(:xmpp))
|
active: @settings_section.to_s == "xmpp"
|
||||||
) %>
|
) %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% if Setting.lndhub_enabled %>
|
<% if Setting.lndhub_enabled %>
|
||||||
<%= render SidenavLinkComponent.new(
|
<%= render SidenavLinkComponent.new(
|
||||||
name: "Lightning", path: setting_path(:lightning), icon: "zap",
|
name: "Lightning", path: setting_path(:lightning), icon: "zap",
|
||||||
active: current_page?(setting_path(:lightning))
|
active: @settings_section.to_s == "lightning"
|
||||||
) %>
|
) %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|||||||
@@ -1,12 +1,8 @@
|
|||||||
<div class="border-b border-gray-200">
|
<%= render HeaderTabLinkComponent.new(
|
||||||
<nav class="-mb-px flex" aria-label="Tabs">
|
name: "Donations", path: contributions_donations_path,
|
||||||
<%= render TabnavLinkComponent.new(
|
active: current_page?(contributions_donations_path)
|
||||||
name: "Donations", path: contributions_donations_path,
|
) %>
|
||||||
active: current_page?(contributions_donations_path)
|
<%= render HeaderTabLinkComponent.new(
|
||||||
) %>
|
name: "Projects", path: contributions_projects_path,
|
||||||
<%= render TabnavLinkComponent.new(
|
active: current_page?(contributions_projects_path)
|
||||||
name: "Projects", path: contributions_projects_path,
|
) %>
|
||||||
active: current_page?(contributions_projects_path)
|
|
||||||
) %>
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
require 'sidekiq/web'
|
require 'sidekiq/web'
|
||||||
|
|
||||||
Rails.application.routes.draw do
|
Rails.application.routes.draw do
|
||||||
devise_for :users, controllers: { confirmations: "users/confirmations" }
|
devise_for :users, controllers: { confirmations: 'users/confirmations' }
|
||||||
|
|
||||||
get 'welcome', to: 'welcome#index'
|
get 'welcome', to: 'welcome#index'
|
||||||
get 'check_your_email', to: 'welcome#check_your_email'
|
get 'check_your_email', to: 'welcome#check_your_email'
|
||||||
@@ -19,6 +19,8 @@ Rails.application.routes.draw do
|
|||||||
resources :invitations, only: ['index', 'show', 'create', 'destroy']
|
resources :invitations, only: ['index', 'show', 'create', 'destroy']
|
||||||
|
|
||||||
namespace :services do
|
namespace :services do
|
||||||
|
get 'storage', to: 'remotestorage#dashboard'
|
||||||
|
|
||||||
resources :lightning, only: [:index] do
|
resources :lightning, only: [:index] do
|
||||||
collection do
|
collection do
|
||||||
get 'transactions'
|
get 'transactions'
|
||||||
@@ -59,15 +61,20 @@ Rails.application.routes.draw do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
get ".well-known/webfinger" => "webfinger#show"
|
get ".well-known/webfinger", to: 'webfinger#show'
|
||||||
|
|
||||||
|
namespace :discourse do
|
||||||
|
get "connect", to: 'sso#connect'
|
||||||
|
end
|
||||||
|
|
||||||
authenticate :user, ->(user) { user.is_admin? } do
|
authenticate :user, ->(user) { user.is_admin? } do
|
||||||
mount Sidekiq::Web => '/sidekiq'
|
mount Sidekiq::Web, at: '/sidekiq'
|
||||||
|
mount Flipper::UI.app(Flipper), at: '/flipper'
|
||||||
end
|
end
|
||||||
|
|
||||||
# Letter Opener (open "sent" emails in dev and staging)
|
# Letter Opener (open "sent" emails in dev and staging)
|
||||||
if Rails.env.match(/staging|development/)
|
if Rails.env.match(/staging|development/)
|
||||||
mount LetterOpenerWeb::Engine, at: "letter_opener"
|
mount LetterOpenerWeb::Engine, at: '/letter_opener'
|
||||||
end
|
end
|
||||||
|
|
||||||
root to: 'dashboard#index'
|
root to: 'dashboard#index'
|
||||||
|
|||||||
22
db/migrate/20230523120753_create_flipper_tables.rb
Normal file
22
db/migrate/20230523120753_create_flipper_tables.rb
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
class CreateFlipperTables < ActiveRecord::Migration[7.0]
|
||||||
|
def self.up
|
||||||
|
create_table :flipper_features do |t|
|
||||||
|
t.string :key, null: false
|
||||||
|
t.timestamps null: false
|
||||||
|
end
|
||||||
|
add_index :flipper_features, :key, unique: true
|
||||||
|
|
||||||
|
create_table :flipper_gates do |t|
|
||||||
|
t.string :feature_key, null: false
|
||||||
|
t.string :key, null: false
|
||||||
|
t.string :value
|
||||||
|
t.timestamps null: false
|
||||||
|
end
|
||||||
|
add_index :flipper_gates, [:feature_key, :key, :value], unique: true
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.down
|
||||||
|
drop_table :flipper_gates
|
||||||
|
drop_table :flipper_features
|
||||||
|
end
|
||||||
|
end
|
||||||
18
db/schema.rb
18
db/schema.rb
@@ -10,7 +10,7 @@
|
|||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema[7.0].define(version: 2023_04_03_135149) do
|
ActiveRecord::Schema[7.0].define(version: 2023_05_23_120753) do
|
||||||
create_table "donations", force: :cascade do |t|
|
create_table "donations", force: :cascade do |t|
|
||||||
t.integer "user_id"
|
t.integer "user_id"
|
||||||
t.integer "amount_sats"
|
t.integer "amount_sats"
|
||||||
@@ -23,6 +23,22 @@ ActiveRecord::Schema[7.0].define(version: 2023_04_03_135149) do
|
|||||||
t.index ["user_id"], name: "index_donations_on_user_id"
|
t.index ["user_id"], name: "index_donations_on_user_id"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
create_table "flipper_features", force: :cascade do |t|
|
||||||
|
t.string "key", null: false
|
||||||
|
t.datetime "created_at", null: false
|
||||||
|
t.datetime "updated_at", null: false
|
||||||
|
t.index ["key"], name: "index_flipper_features_on_key", unique: true
|
||||||
|
end
|
||||||
|
|
||||||
|
create_table "flipper_gates", force: :cascade do |t|
|
||||||
|
t.string "feature_key", null: false
|
||||||
|
t.string "key", null: false
|
||||||
|
t.string "value"
|
||||||
|
t.datetime "created_at", null: false
|
||||||
|
t.datetime "updated_at", null: false
|
||||||
|
t.index ["feature_key", "key", "value"], name: "index_flipper_gates_on_feature_key_and_key_and_value", unique: true
|
||||||
|
end
|
||||||
|
|
||||||
create_table "invitations", force: :cascade do |t|
|
create_table "invitations", force: :cascade do |t|
|
||||||
t.string "token"
|
t.string "token"
|
||||||
t.integer "user_id"
|
t.integer "user_id"
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
"postcss-preset-env": "^7.8.3",
|
"postcss-preset-env": "^7.8.3",
|
||||||
"tailwindcss": "^3.2.4"
|
"tailwindcss": "^3.2.4"
|
||||||
},
|
},
|
||||||
"version": "0.5.0",
|
"version": "0.6.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build:css:tailwind": "tailwindcss --postcss -i ./app/assets/stylesheets/application.tailwind.css -o ./app/assets/builds/application.css",
|
"build:css:tailwind": "tailwindcss --postcss -i ./app/assets/stylesheets/application.tailwind.css -o ./app/assets/builds/application.css",
|
||||||
"build:css": "yarn run build:css:tailwind"
|
"build:css": "yarn run build:css:tailwind"
|
||||||
|
|||||||
@@ -2,54 +2,57 @@ require 'rails_helper'
|
|||||||
|
|
||||||
RSpec.describe 'Account settings', type: :feature do
|
RSpec.describe 'Account settings', type: :feature do
|
||||||
let(:user) { create :user }
|
let(:user) { create :user }
|
||||||
let(:geraint) { create :user, id: 2, cn: 'geraint', email: "lamagliarosa@example.com" }
|
|
||||||
|
|
||||||
before do
|
feature "Update email address" do
|
||||||
login_as user, :scope => :user
|
let(:geraint) { create :user, id: 2, cn: 'geraint', email: "lamagliarosa@example.com" }
|
||||||
geraint.save!
|
|
||||||
|
|
||||||
allow_any_instance_of(User).to receive(:valid_ldap_authentication?)
|
before do
|
||||||
.with("invalid password").and_return(false)
|
login_as user, :scope => :user
|
||||||
allow_any_instance_of(User).to receive(:valid_ldap_authentication?)
|
geraint.save!
|
||||||
.with("valid password").and_return(true)
|
|
||||||
end
|
|
||||||
|
|
||||||
scenario 'Update email address fails with invalid password' do
|
allow_any_instance_of(User).to receive(:valid_ldap_authentication?)
|
||||||
visit setting_path(:account)
|
.with("invalid password").and_return(false)
|
||||||
fill_in 'Address', with: "lamagliarosa@example.com"
|
allow_any_instance_of(User).to receive(:valid_ldap_authentication?)
|
||||||
fill_in 'Current password', with: "invalid password"
|
.with("valid password").and_return(true)
|
||||||
click_button "Update"
|
|
||||||
|
|
||||||
expect(current_url).to eq(setting_url(:account))
|
|
||||||
expect(user.reload.unconfirmed_email).to be_nil
|
|
||||||
within ".flash-msg" do
|
|
||||||
expect(page).to have_content("did not match your current password")
|
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
scenario 'Update email address fails when new address already taken' do
|
scenario 'fails with invalid password' do
|
||||||
visit setting_path(:account)
|
visit setting_path(:account)
|
||||||
fill_in 'Address', with: "lamagliarosa@example.com"
|
fill_in 'Address', with: "lamagliarosa@example.com"
|
||||||
fill_in 'Current password', with: "valid password"
|
fill_in 'Current password', with: "invalid password"
|
||||||
click_button "Update"
|
click_button "Update"
|
||||||
|
|
||||||
expect(current_url).to eq(setting_url(:update_email))
|
expect(current_url).to eq(setting_url(:account))
|
||||||
expect(user.reload.unconfirmed_email).to be_nil
|
expect(user.reload.unconfirmed_email).to be_nil
|
||||||
within ".error-msg" do
|
within ".flash-msg" do
|
||||||
expect(page).to have_content("has already been taken")
|
expect(page).to have_content("did not match your current password")
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
scenario 'Update email address works' do
|
scenario 'fails when new address already taken' do
|
||||||
visit setting_path(:account)
|
visit setting_path(:account)
|
||||||
fill_in 'Address', with: "lamagliabianca@example.com"
|
fill_in 'Address', with: "lamagliarosa@example.com"
|
||||||
fill_in 'Current password', with: "valid password"
|
fill_in 'Current password', with: "valid password"
|
||||||
click_button "Update"
|
click_button "Update"
|
||||||
|
|
||||||
expect(current_url).to eq(setting_url(:account))
|
expect(current_url).to eq(setting_url(:update_email))
|
||||||
expect(user.reload.unconfirmed_email).to eq("lamagliabianca@example.com")
|
expect(user.reload.unconfirmed_email).to be_nil
|
||||||
within ".flash-msg" do
|
within ".error-msg" do
|
||||||
expect(page).to have_content("Please confirm your new address")
|
expect(page).to have_content("has already been taken")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
scenario 'works with valid password and address' do
|
||||||
|
visit setting_path(:account)
|
||||||
|
fill_in 'Address', with: "lamagliabianca@example.com"
|
||||||
|
fill_in 'Current password', with: "valid password"
|
||||||
|
click_button "Update"
|
||||||
|
|
||||||
|
expect(current_url).to eq(setting_url(:account))
|
||||||
|
expect(user.reload.unconfirmed_email).to eq("lamagliabianca@example.com")
|
||||||
|
within ".flash-msg" do
|
||||||
|
expect(page).to have_content("Please confirm your new address")
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
45
spec/features/settings/profile_spec.rb
Normal file
45
spec/features/settings/profile_spec.rb
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe 'Profile settings', type: :feature do
|
||||||
|
let(:user) { create :user, cn: "mwahlberg" }
|
||||||
|
|
||||||
|
before do
|
||||||
|
login_as user, :scope => :user
|
||||||
|
end
|
||||||
|
|
||||||
|
feature "Update display name" do
|
||||||
|
before do
|
||||||
|
allow(user).to receive(:display_name).and_return("Mark")
|
||||||
|
allow_any_instance_of(User).to receive(:dn).and_return("cn=mwahlberg,ou=kosmos.org,cn=users,dc=kosmos,dc=org")
|
||||||
|
allow_any_instance_of(User).to receive(:ldap_entry).and_return({
|
||||||
|
uid: user.cn, ou: user.ou, display_name: "Mark"
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
scenario 'fails with validation error' do
|
||||||
|
visit setting_path(:profile)
|
||||||
|
fill_in 'Display name', with: "M"
|
||||||
|
click_button "Save"
|
||||||
|
|
||||||
|
expect(current_url).to eq(setting_url(:profile))
|
||||||
|
expect(page).to have_field('Display name', with: 'M')
|
||||||
|
within ".error-msg" do
|
||||||
|
expect(page).to have_content("is too short")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
scenario 'works with valid input' do
|
||||||
|
expect(LdapManager::UpdateDisplayName).to receive(:call)
|
||||||
|
.with(user.dn, "Marky Mark").and_return(true)
|
||||||
|
|
||||||
|
visit setting_path(:profile)
|
||||||
|
fill_in 'Display name', with: "Marky Mark"
|
||||||
|
click_button "Save"
|
||||||
|
|
||||||
|
expect(current_url).to eq(setting_url(:profile))
|
||||||
|
within ".flash-msg" do
|
||||||
|
expect(page).to have_content("Settings saved")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
41
spec/requests/discourse/sso_spec.rb
Normal file
41
spec/requests/discourse/sso_spec.rb
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
require 'rails_helper'
|
||||||
|
require 'webmock/rspec'
|
||||||
|
|
||||||
|
RSpec.describe "Discourse SSO", type: :request do
|
||||||
|
|
||||||
|
describe "GET /discourse/connect" do
|
||||||
|
let(:user) { create :user, cn: 'jimmy', ou: 'kosmos.org' }
|
||||||
|
|
||||||
|
before do
|
||||||
|
Warden.test_mode!
|
||||||
|
login_as user, scope: :user
|
||||||
|
allow(user).to receive(:display_name).and_return('Jimbo')
|
||||||
|
allow(user).to receive(:is_admin?).and_return(false)
|
||||||
|
end
|
||||||
|
|
||||||
|
after do
|
||||||
|
Warden.test_reset!
|
||||||
|
end
|
||||||
|
|
||||||
|
context "with invalid SSO credentials" do
|
||||||
|
it "results in a failed signature check" do
|
||||||
|
expect {
|
||||||
|
get discourse_connect_path(
|
||||||
|
sso: "bm9uY2U9ODk2N2NiMmFlZTdlMjdjNzZiZTNkZWQ5ODIwYzMzN2QmcmV0dXJuX3Nzb191cmw9aHR0cCUzQSUyRiUyRmxvY2FsaG9zdCUzQTMwMDAlMkZzZXNzaW9uJTJGc3NvX2xvZ2lu",
|
||||||
|
sig: "01fc008ff7b51855217e879b6f14aaddefbbd4df2d128951f7bb70cfde834c2a"
|
||||||
|
)
|
||||||
|
}.to raise_error(DiscourseApi::SingleSignOn::ParseError)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "valid SSO credentials" do
|
||||||
|
it "redirects to the Discourse SSO endpoint" do
|
||||||
|
get discourse_connect_path(
|
||||||
|
sso: "bm9uY2U9YjQwYWZmYzg0YWQ2NWE1ZTk5MjdlZWU1NWEzMjdhMTQmcmV0dXJuX3Nzb191cmw9aHR0cCUzQSUyRiUyRmxvY2FsaG9zdCUzQTMwMDAlMkZzZXNzaW9uJTJGc3NvX2xvZ2lu",
|
||||||
|
sig: "b7905c5db612391293249ad5272dac493681efcd255133f6c2aff91ba654a319"
|
||||||
|
)
|
||||||
|
expect(response).to redirect_to('http://discourse.example.com/session/sso_login?sso=YWRtaW49ZmFsc2UmZW1haWw9amltbXklNDBleGFtcGxlLmNvbSZleHRlcm5hbF9pZD0xJm5hbWU9SmltYm8mbm9uY2U9YjQwYWZmYzg0YWQ2NWE1ZTk5MjdlZWU1NWEzMjdhMTQmcmV0dXJuX3Nzb191cmw9aHR0cCUzQSUyRiUyRmxvY2FsaG9zdCUzQTMwMDAlMkZzZXNzaW9uJTJGc3NvX2xvZ2luJnVzZXJuYW1lPWppbW15&sig=d5f8b1d6db66569bef789fda4a3216119c2d42b84725d043c9a57dde1e528842')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
Reference in New Issue
Block a user