Compare commits
3 Commits
9f6fa6deba
...
feature/no
| Author | SHA1 | Date | |
|---|---|---|---|
|
011386fb8d
|
|||
|
4d77f5d38c
|
|||
|
64de4deddd
|
2
Gemfile
2
Gemfile
@@ -44,8 +44,6 @@ gem 'pagy', '~> 6.0', '>= 6.0.2'
|
|||||||
gem 'flipper'
|
gem 'flipper'
|
||||||
gem 'flipper-active_record'
|
gem 'flipper-active_record'
|
||||||
gem 'flipper-ui'
|
gem 'flipper-ui'
|
||||||
gem 'gpgme', '~> 2.0.24'
|
|
||||||
gem 'zbase32', '~> 0.1.1'
|
|
||||||
|
|
||||||
# HTTP requests
|
# HTTP requests
|
||||||
gem 'faraday'
|
gem 'faraday'
|
||||||
|
|||||||
@@ -197,8 +197,6 @@ GEM
|
|||||||
raabro (~> 1.4)
|
raabro (~> 1.4)
|
||||||
globalid (1.2.1)
|
globalid (1.2.1)
|
||||||
activesupport (>= 6.1)
|
activesupport (>= 6.1)
|
||||||
gpgme (2.0.24)
|
|
||||||
mini_portile2 (~> 2.7)
|
|
||||||
hashdiff (1.1.0)
|
hashdiff (1.1.0)
|
||||||
i18n (1.14.1)
|
i18n (1.14.1)
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
@@ -485,7 +483,6 @@ GEM
|
|||||||
xpath (3.2.0)
|
xpath (3.2.0)
|
||||||
nokogiri (~> 1.8)
|
nokogiri (~> 1.8)
|
||||||
yard (0.9.34)
|
yard (0.9.34)
|
||||||
zbase32 (0.1.1)
|
|
||||||
zeitwerk (2.6.12)
|
zeitwerk (2.6.12)
|
||||||
|
|
||||||
PLATFORMS
|
PLATFORMS
|
||||||
@@ -510,7 +507,6 @@ DEPENDENCIES
|
|||||||
flipper
|
flipper
|
||||||
flipper-active_record
|
flipper-active_record
|
||||||
flipper-ui
|
flipper-ui
|
||||||
gpgme (~> 2.0.24)
|
|
||||||
image_processing (~> 1.12.2)
|
image_processing (~> 1.12.2)
|
||||||
importmap-rails
|
importmap-rails
|
||||||
jbuilder (~> 2.7)
|
jbuilder (~> 2.7)
|
||||||
@@ -544,7 +540,6 @@ DEPENDENCIES
|
|||||||
warden
|
warden
|
||||||
web-console (~> 4.2)
|
web-console (~> 4.2)
|
||||||
webmock
|
webmock
|
||||||
zbase32 (~> 0.1.1)
|
|
||||||
|
|
||||||
BUNDLED WITH
|
BUNDLED WITH
|
||||||
2.5.5
|
2.5.5
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ class Admin::UsersController < Admin::BaseController
|
|||||||
amount = params[:amount].to_i
|
amount = params[:amount].to_i
|
||||||
notify_user = ActiveRecord::Type::Boolean.new.cast(params[:notify_user])
|
notify_user = ActiveRecord::Type::Boolean.new.cast(params[:notify_user])
|
||||||
|
|
||||||
UserManager::CreateInvitations.call(user: @user, amount: amount, notify: notify_user)
|
CreateInvitations.call(user: @user, amount: amount, notify: notify_user)
|
||||||
|
|
||||||
redirect_to admin_user_path(@user.cn), flash: {
|
redirect_to admin_user_path(@user.cn), flash: {
|
||||||
success: "Added #{amount} invitations to #{@user.cn}'s account"
|
success: "Added #{amount} invitations to #{@user.cn}'s account"
|
||||||
|
|||||||
@@ -21,12 +21,10 @@ class SettingsController < ApplicationController
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# PUT /settings/:section
|
|
||||||
def update
|
def update
|
||||||
@user.preferences.merge!(user_params[:preferences] || {})
|
@user.preferences.merge!(user_params[:preferences] || {})
|
||||||
@user.display_name = user_params[:display_name]
|
@user.display_name = user_params[:display_name]
|
||||||
@user.avatar_new = user_params[:avatar]
|
@user.avatar_new = user_params[:avatar]
|
||||||
@user.pgp_pubkey = user_params[:pgp_pubkey]
|
|
||||||
|
|
||||||
if @user.save
|
if @user.save
|
||||||
if @user.display_name && (@user.display_name != @user.ldap_entry[:display_name])
|
if @user.display_name && (@user.display_name != @user.ldap_entry[:display_name])
|
||||||
@@ -37,10 +35,6 @@ class SettingsController < ApplicationController
|
|||||||
LdapManager::UpdateAvatar.call(dn: @user.dn, file: @user.avatar_new)
|
LdapManager::UpdateAvatar.call(dn: @user.dn, file: @user.avatar_new)
|
||||||
end
|
end
|
||||||
|
|
||||||
if @user.pgp_pubkey && (@user.pgp_pubkey != @user.ldap_entry[:pgp_key])
|
|
||||||
UserManager::UpdatePgpKey.call(user: @user)
|
|
||||||
end
|
|
||||||
|
|
||||||
redirect_to setting_path(@settings_section), flash: {
|
redirect_to setting_path(@settings_section), flash: {
|
||||||
success: 'Settings saved.'
|
success: 'Settings saved.'
|
||||||
}
|
}
|
||||||
@@ -50,7 +44,6 @@ class SettingsController < ApplicationController
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# POST /settings/update_email
|
|
||||||
def update_email
|
def update_email
|
||||||
if @user.valid_ldap_authentication?(security_params[:current_password])
|
if @user.valid_ldap_authentication?(security_params[:current_password])
|
||||||
if @user.update email: email_params[:email]
|
if @user.update email: email_params[:email]
|
||||||
@@ -68,7 +61,6 @@ class SettingsController < ApplicationController
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# POST /settings/reset_email_password
|
|
||||||
def reset_email_password
|
def reset_email_password
|
||||||
@user.current_password = security_params[:current_password]
|
@user.current_password = security_params[:current_password]
|
||||||
|
|
||||||
@@ -91,7 +83,6 @@ class SettingsController < ApplicationController
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# POST /settings/reset_password
|
|
||||||
def reset_password
|
def reset_password
|
||||||
current_user.send_reset_password_instructions
|
current_user.send_reset_password_instructions
|
||||||
sign_out current_user
|
sign_out current_user
|
||||||
@@ -99,7 +90,6 @@ class SettingsController < ApplicationController
|
|||||||
redirect_to check_your_email_path, notice: msg
|
redirect_to check_your_email_path, notice: msg
|
||||||
end
|
end
|
||||||
|
|
||||||
# POST /settings/set_nostr_pubkey
|
|
||||||
def set_nostr_pubkey
|
def set_nostr_pubkey
|
||||||
signed_event = Nostr::Event.new(**nostr_event_from_params)
|
signed_event = Nostr::Event.new(**nostr_event_from_params)
|
||||||
|
|
||||||
@@ -162,8 +152,7 @@ class SettingsController < ApplicationController
|
|||||||
|
|
||||||
def user_params
|
def user_params
|
||||||
params.require(:user).permit(
|
params.require(:user).permit(
|
||||||
:display_name, :avatar, :pgp_pubkey,
|
:display_name, :avatar, preferences: UserPreferences.pref_keys
|
||||||
preferences: UserPreferences.pref_keys
|
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ class SignupController < ApplicationController
|
|||||||
session[:new_user] = nil
|
session[:new_user] = nil
|
||||||
session[:validation_error] = nil
|
session[:validation_error] = nil
|
||||||
|
|
||||||
UserManager::CreateAccount.call(account: {
|
CreateAccount.call(account: {
|
||||||
username: @user.cn,
|
username: @user.cn,
|
||||||
domain: Setting.primary_domain,
|
domain: Setting.primary_domain,
|
||||||
email: @user.email,
|
email: @user.email,
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
class WebKeyDirectoryController < WellKnownController
|
|
||||||
before_action :allow_cross_origin_requests, only: [ :show ]
|
|
||||||
|
|
||||||
# /.well-known/openpgpkey/hu/:hashed_username(.txt)
|
|
||||||
def show
|
|
||||||
@user = User.find_by(cn: params[:l])
|
|
||||||
|
|
||||||
if @user.nil? ||
|
|
||||||
@user.pgp_pubkey.empty? ||
|
|
||||||
!@user.pgp_pubkey_contains_user_address?
|
|
||||||
http_status :not_found and return
|
|
||||||
end
|
|
||||||
|
|
||||||
if params[:hashed_username] != @user.wkd_hash
|
|
||||||
http_status :unprocessable_entity and return
|
|
||||||
end
|
|
||||||
|
|
||||||
respond_to do |format|
|
|
||||||
format.text do
|
|
||||||
response.headers['Content-Type'] = 'text/plain'
|
|
||||||
render plain: @user.pgp_pubkey
|
|
||||||
end
|
|
||||||
|
|
||||||
format.any do
|
|
||||||
key = @user.gnupg_key.export
|
|
||||||
send_data key, filename: "#{@user.wkd_hash}.pem",
|
|
||||||
type: "application/octet-stream"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
end
|
|
||||||
@@ -1,8 +1,14 @@
|
|||||||
import { Controller } from "@hotwired/stimulus"
|
import { Controller } from "@hotwired/stimulus"
|
||||||
|
import { Nostrify } from "nostrify"
|
||||||
|
|
||||||
// Connects to data-controller="settings--nostr-pubkey"
|
// Connects to data-controller="settings--nostr-pubkey"
|
||||||
export default class extends Controller {
|
export default class extends Controller {
|
||||||
static targets = [ "noExtension", "setPubkey", "pubkeyBech32Input" ]
|
static targets = [
|
||||||
|
"noExtension",
|
||||||
|
"setPubkey", "pubkeyBech32Input",
|
||||||
|
"relayList", "relayListStatus",
|
||||||
|
"profileStatusNip05", "profileStatusLud16"
|
||||||
|
]
|
||||||
static values = {
|
static values = {
|
||||||
userAddress: String,
|
userAddress: String,
|
||||||
pubkeyHex: String,
|
pubkeyHex: String,
|
||||||
@@ -15,6 +21,14 @@ export default class extends Controller {
|
|||||||
if (this.hasSetPubkeyTarget) {
|
if (this.hasSetPubkeyTarget) {
|
||||||
this.setPubkeyTarget.disabled = false
|
this.setPubkeyTarget.disabled = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.pubkeyHexValue) {
|
||||||
|
this.discoverUserOnNostr().then(() => {
|
||||||
|
this.renderRelayStatus()
|
||||||
|
this.renderProfileNip05Status()
|
||||||
|
this.renderProfileLud16Status()
|
||||||
|
})
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
this.noExtensionTarget.classList.remove("hidden")
|
this.noExtensionTarget.classList.remove("hidden")
|
||||||
}
|
}
|
||||||
@@ -49,8 +63,172 @@ export default class extends Controller {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async discoverUserOnNostr () {
|
||||||
|
this.nip65Relays = await this.findUserRelays()
|
||||||
|
this.profile = await this.findUserProfile()
|
||||||
|
}
|
||||||
|
|
||||||
|
async findUserRelays () {
|
||||||
|
const controller = new AbortController();
|
||||||
|
const signal = controller.signal;
|
||||||
|
const filters = [{ kinds: [10002], authors: [this.pubkeyHexValue], limit: 1 }]
|
||||||
|
const messages = []
|
||||||
|
|
||||||
|
for await (const msg of this.discoveryPool.req(filters, { signal })) {
|
||||||
|
if (msg[0] === 'EVENT') {
|
||||||
|
if (!messages.find(m => m.id === msg[2].id)) {
|
||||||
|
messages.push(msg[2])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (msg[0] === 'EOSE') { break }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close the relay subscription
|
||||||
|
controller.abort()
|
||||||
|
if (messages.length === 0) { return messages }
|
||||||
|
|
||||||
|
const sortedMessages = messages.sort((a, b) => a.createdAt - b.createdAt)
|
||||||
|
const newestMessage = messages[messages.length - 1]
|
||||||
|
|
||||||
|
return newestMessage.tags.filter(t => t[0] === 'r')
|
||||||
|
.map(t => { return { url: t[1], marker: t[2] } })
|
||||||
|
}
|
||||||
|
|
||||||
|
async findUserProfile () {
|
||||||
|
const controller = new AbortController();
|
||||||
|
const signal = controller.signal;
|
||||||
|
const filters = [{ kinds: [0], authors: [this.pubkeyHexValue], limit: 1 }]
|
||||||
|
const messages = []
|
||||||
|
|
||||||
|
for await (const msg of this.discoveryPool.req(filters, { signal })) {
|
||||||
|
if (msg[0] === 'EVENT') {
|
||||||
|
if (!messages.find(m => m.id === msg[2].id)) {
|
||||||
|
messages.push(msg[2])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (msg[0] === 'EOSE') { break }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close the relay subscription
|
||||||
|
controller.abort()
|
||||||
|
if (messages.length === 0) { return null }
|
||||||
|
|
||||||
|
const sortedMessages = messages.sort((a, b) => a.createdAt - b.createdAt)
|
||||||
|
const newestMessage = messages[messages.length - 1]
|
||||||
|
|
||||||
|
return JSON.parse(newestMessage.content)
|
||||||
|
}
|
||||||
|
|
||||||
|
renderRelayStatus () {
|
||||||
|
let showStatus
|
||||||
|
|
||||||
|
if (this.nip65Relays.length > 0) {
|
||||||
|
if (this.relaysContainAccountsRelay) {
|
||||||
|
showStatus = 'green'
|
||||||
|
} else {
|
||||||
|
showStatus = 'orange'
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
showStatus = 'red'
|
||||||
|
}
|
||||||
|
// showStatus = 'red'
|
||||||
|
|
||||||
|
this.relayListStatusTarget
|
||||||
|
.querySelector(`.status-${showStatus}`)
|
||||||
|
.classList.remove("hidden")
|
||||||
|
}
|
||||||
|
|
||||||
|
renderProfileNip05Status () {
|
||||||
|
let showStatus
|
||||||
|
|
||||||
|
if (this.profile?.nip05) {
|
||||||
|
if (this.profile.nip05 === this.userAddressValue) {
|
||||||
|
showStatus = 'green'
|
||||||
|
} else {
|
||||||
|
showStatus = 'red'
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
showStatus = 'orange'
|
||||||
|
}
|
||||||
|
|
||||||
|
this.profileStatusNip05Target
|
||||||
|
.querySelector(`.status-${showStatus}`)
|
||||||
|
.classList.remove("hidden")
|
||||||
|
}
|
||||||
|
|
||||||
|
renderProfileLud16Status () {
|
||||||
|
let showStatus
|
||||||
|
|
||||||
|
if (this.profile?.lud16) {
|
||||||
|
if (this.profile.lud16 === this.userAddressValue) {
|
||||||
|
showStatus = 'green'
|
||||||
|
} else {
|
||||||
|
showStatus = 'red'
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
showStatus = 'orange'
|
||||||
|
}
|
||||||
|
|
||||||
|
this.profileStatusLud16Target
|
||||||
|
.querySelector(`.status-${showStatus}`)
|
||||||
|
.classList.remove("hidden")
|
||||||
|
}
|
||||||
|
|
||||||
|
// renderRelayList (relays) {
|
||||||
|
// const html = relays.map(relay => `
|
||||||
|
// <li class="flex items-center justify-between p-2 border-b">
|
||||||
|
// <span>${relay.url}</span>
|
||||||
|
// <button
|
||||||
|
// data-action="click->list#handleItemClick"
|
||||||
|
// data-item="${relay.url}"
|
||||||
|
// class="bg-blue-500 text-white px-3 py-1 rounded">
|
||||||
|
// Action
|
||||||
|
// </button>
|
||||||
|
// </li>
|
||||||
|
// `).join("")
|
||||||
|
//
|
||||||
|
// this.relayListTarget.innerHTML = html
|
||||||
|
// }
|
||||||
|
|
||||||
get csrfToken () {
|
get csrfToken () {
|
||||||
const element = document.head.querySelector('meta[name="csrf-token"]')
|
const element = document.head.querySelector('meta[name="csrf-token"]')
|
||||||
return element.getAttribute("content")
|
return element.getAttribute("content")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Used to find a user's profile and relays
|
||||||
|
get discoveryRelays () {
|
||||||
|
return [
|
||||||
|
'ws://localhost:4777',
|
||||||
|
'wss://nostr.kosmos.org',
|
||||||
|
'wss://purplepag.es',
|
||||||
|
// 'wss://relay.nostr.band',
|
||||||
|
// 'wss://njump.me',
|
||||||
|
// 'wss://relay.damus.io',
|
||||||
|
// 'wss://nos.lol',
|
||||||
|
// 'wss://eden.nostr.land',
|
||||||
|
// 'wss://relay.snort.social',
|
||||||
|
// 'wss://nostr.wine',
|
||||||
|
// 'wss://relay.primal.net',
|
||||||
|
// 'wss://nostr.bitcoiner.social',
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
get discoveryPool () {
|
||||||
|
if (!this._discoveryPool) {
|
||||||
|
this._discoveryPool = new Nostrify.NPool({
|
||||||
|
open: (url) => new Nostrify.NRelay1(url),
|
||||||
|
reqRouter: async (filters) => new Map(
|
||||||
|
this.discoveryRelays.map(relayUrl => [ relayUrl, filters ])
|
||||||
|
),
|
||||||
|
eventRouter: async (event) => [],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return this._discoveryPool
|
||||||
|
}
|
||||||
|
|
||||||
|
get relaysContainAccountsRelay () {
|
||||||
|
// TODO use URL from view/settings
|
||||||
|
return !!this.nip65Relays.find(r => r.url.match('wss://nostr.kosmos.org'))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,10 +3,9 @@ require 'nostr'
|
|||||||
class User < ApplicationRecord
|
class User < ApplicationRecord
|
||||||
include EmailValidatable
|
include EmailValidatable
|
||||||
|
|
||||||
attr_accessor :current_password
|
|
||||||
attr_accessor :avatar_new
|
|
||||||
attr_accessor :display_name
|
attr_accessor :display_name
|
||||||
attr_accessor :pgp_pubkey
|
attr_accessor :avatar_new
|
||||||
|
attr_accessor :current_password
|
||||||
|
|
||||||
serialize :preferences, coder: UserPreferences
|
serialize :preferences, coder: UserPreferences
|
||||||
|
|
||||||
@@ -52,8 +51,6 @@ class User < ApplicationRecord
|
|||||||
|
|
||||||
validate :acceptable_avatar
|
validate :acceptable_avatar
|
||||||
|
|
||||||
validate :acceptable_pgp_key_format, if: -> { defined?(@pgp_pubkey) && @pgp_pubkey.present? }
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Scopes
|
# Scopes
|
||||||
#
|
#
|
||||||
@@ -168,23 +165,6 @@ class User < ApplicationRecord
|
|||||||
Nostr::PublicKey.new(nostr_pubkey).to_bech32
|
Nostr::PublicKey.new(nostr_pubkey).to_bech32
|
||||||
end
|
end
|
||||||
|
|
||||||
def pgp_pubkey
|
|
||||||
@pgp_pubkey ||= ldap_entry[:pgp_key]
|
|
||||||
end
|
|
||||||
|
|
||||||
def gnupg_key
|
|
||||||
return nil unless pgp_pubkey.present?
|
|
||||||
@gnupg_key ||= GPGME::Key.get(pgp_fpr)
|
|
||||||
end
|
|
||||||
|
|
||||||
def pgp_pubkey_contains_user_address?
|
|
||||||
gnupg_key.uids.map(&:email).include?(address)
|
|
||||||
end
|
|
||||||
|
|
||||||
def wkd_hash
|
|
||||||
ZBase32.encode(Digest::SHA1.digest(cn))
|
|
||||||
end
|
|
||||||
|
|
||||||
def avatar
|
def avatar
|
||||||
@avatar_base64 ||= LdapManager::FetchAvatar.call(cn: cn)
|
@avatar_base64 ||= LdapManager::FetchAvatar.call(cn: cn)
|
||||||
end
|
end
|
||||||
@@ -234,10 +214,4 @@ class User < ApplicationRecord
|
|||||||
errors.add(:avatar, "must be a JPEG or PNG file")
|
errors.add(:avatar, "must be a JPEG or PNG file")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def acceptable_pgp_key_format
|
|
||||||
unless GPGME::Key.valid?(pgp_pubkey)
|
|
||||||
errors.add(:pgp_pubkey, 'is not a valid armored PGP public key block')
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|||||||
54
app/services/create_account.rb
Normal file
54
app/services/create_account.rb
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
class CreateAccount < ApplicationService
|
||||||
|
def initialize(account:)
|
||||||
|
@username = account[:username]
|
||||||
|
@domain = account[:ou] || Setting.primary_domain
|
||||||
|
@email = account[:email]
|
||||||
|
@password = account[:password]
|
||||||
|
@invitation = account[:invitation]
|
||||||
|
@confirmed = account[:confirmed]
|
||||||
|
end
|
||||||
|
|
||||||
|
def call
|
||||||
|
user = create_user_in_database
|
||||||
|
add_ldap_document
|
||||||
|
create_lndhub_account(user) if Setting.lndhub_enabled
|
||||||
|
|
||||||
|
if @invitation.present?
|
||||||
|
update_invitation(user.id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def create_user_in_database
|
||||||
|
User.create!(
|
||||||
|
cn: @username,
|
||||||
|
ou: @domain,
|
||||||
|
email: @email,
|
||||||
|
password: @password,
|
||||||
|
password_confirmation: @password,
|
||||||
|
confirmed_at: @confirmed ? DateTime.now : nil
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def update_invitation(user_id)
|
||||||
|
@invitation.update! invited_user_id: user_id, used_at: DateTime.now
|
||||||
|
end
|
||||||
|
|
||||||
|
def add_ldap_document
|
||||||
|
hashed_pw = Devise.ldap_auth_password_builder.call(@password)
|
||||||
|
CreateLdapUserJob.perform_later(
|
||||||
|
username: @username,
|
||||||
|
domain: @domain,
|
||||||
|
email: @email,
|
||||||
|
hashed_pw: hashed_pw,
|
||||||
|
confirmed: @confirmed
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_lndhub_account(user)
|
||||||
|
#TODO enable in development when we have a local lndhub (mock?) API
|
||||||
|
return if Rails.env.development?
|
||||||
|
CreateLndhubAccountJob.perform_later(user)
|
||||||
|
end
|
||||||
|
end
|
||||||
17
app/services/create_invitations.rb
Normal file
17
app/services/create_invitations.rb
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
class CreateInvitations < ApplicationService
|
||||||
|
def initialize(user:, amount:, notify: true)
|
||||||
|
@user = user
|
||||||
|
@amount = amount
|
||||||
|
@notify = notify
|
||||||
|
end
|
||||||
|
|
||||||
|
def call
|
||||||
|
@amount.times do
|
||||||
|
Invitation.create(user: @user)
|
||||||
|
end
|
||||||
|
|
||||||
|
if @notify
|
||||||
|
NotificationMailer.with(user: @user).new_invitations_available.deliver_later
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
module LdapManager
|
|
||||||
class UpdatePgpKey < LdapManagerService
|
|
||||||
def initialize(dn:, pubkey:)
|
|
||||||
@dn = dn
|
|
||||||
@pubkey = pubkey
|
|
||||||
end
|
|
||||||
|
|
||||||
def call
|
|
||||||
if @pubkey.present?
|
|
||||||
replace_attribute @dn, :pgpKey, @pubkey
|
|
||||||
else
|
|
||||||
delete_attribute @dn, :pgpKey
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -58,7 +58,7 @@ class LdapService < ApplicationService
|
|||||||
|
|
||||||
attributes = %w[
|
attributes = %w[
|
||||||
dn cn uid mail displayName admin serviceEnabled
|
dn cn uid mail displayName admin serviceEnabled
|
||||||
mailRoutingAddress mailpassword nostrKey pgpKey
|
mailRoutingAddress mailpassword nostrKey
|
||||||
]
|
]
|
||||||
filter = Net::LDAP::Filter.eq("uid", args[:uid] || "*")
|
filter = Net::LDAP::Filter.eq("uid", args[:uid] || "*")
|
||||||
|
|
||||||
@@ -73,8 +73,7 @@ class LdapService < ApplicationService
|
|||||||
services_enabled: e.try(:serviceEnabled),
|
services_enabled: e.try(:serviceEnabled),
|
||||||
email_maildrop: e.try(:mailRoutingAddress),
|
email_maildrop: e.try(:mailRoutingAddress),
|
||||||
email_password: e.try(:mailpassword),
|
email_password: e.try(:mailpassword),
|
||||||
nostr_key: e.try(:nostrKey) ? e.nostrKey.first : nil,
|
nostr_key: e.try(:nostrKey) ? e.nostrKey.first : nil
|
||||||
pgp_key: e.try(:pgpKey) ? e.pgpKey.first : nil
|
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -102,7 +101,7 @@ class LdapService < ApplicationService
|
|||||||
dn = "ou=#{ou},cn=users,#{ldap_suffix}"
|
dn = "ou=#{ou},cn=users,#{ldap_suffix}"
|
||||||
|
|
||||||
aci = <<-EOS
|
aci = <<-EOS
|
||||||
(target="ldap:///cn=*,ou=#{ou},cn=users,#{ldap_suffix}")(targetattr="cn || sn || uid || userPassword || mail || mailRoutingAddress || serviceEnabled || nostrKey || pgpKey || nsRole || objectClass") (version 3.0; acl "service-#{ou.gsub(".", "-")}-read-search"; allow (read,search) userdn="ldap:///uid=service,ou=#{ou},cn=applications,#{ldap_suffix}";)
|
(target="ldap:///cn=*,ou=#{ou},cn=users,#{ldap_suffix}")(targetattr="cn || sn || uid || userPassword || mail || mailRoutingAddress || serviceEnabled || nostrKey || nsRole || objectClass") (version 3.0; acl "service-#{ou.gsub(".", "-")}-read-search"; allow (read,search) userdn="ldap:///uid=service,ou=#{ou},cn=applications,#{ldap_suffix}";)
|
||||||
EOS
|
EOS
|
||||||
|
|
||||||
attrs = {
|
attrs = {
|
||||||
|
|||||||
@@ -1,56 +0,0 @@
|
|||||||
module UserManager
|
|
||||||
class CreateAccount < UserManagerService
|
|
||||||
def initialize(account:)
|
|
||||||
@username = account[:username]
|
|
||||||
@domain = account[:ou] || Setting.primary_domain
|
|
||||||
@email = account[:email]
|
|
||||||
@password = account[:password]
|
|
||||||
@invitation = account[:invitation]
|
|
||||||
@confirmed = account[:confirmed]
|
|
||||||
end
|
|
||||||
|
|
||||||
def call
|
|
||||||
user = create_user_in_database
|
|
||||||
add_ldap_document
|
|
||||||
create_lndhub_account(user) if Setting.lndhub_enabled
|
|
||||||
|
|
||||||
if @invitation.present?
|
|
||||||
update_invitation(user.id)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def create_user_in_database
|
|
||||||
User.create!(
|
|
||||||
cn: @username,
|
|
||||||
ou: @domain,
|
|
||||||
email: @email,
|
|
||||||
password: @password,
|
|
||||||
password_confirmation: @password,
|
|
||||||
confirmed_at: @confirmed ? DateTime.now : nil
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
def update_invitation(user_id)
|
|
||||||
@invitation.update! invited_user_id: user_id, used_at: DateTime.now
|
|
||||||
end
|
|
||||||
|
|
||||||
def add_ldap_document
|
|
||||||
hashed_pw = Devise.ldap_auth_password_builder.call(@password)
|
|
||||||
CreateLdapUserJob.perform_later(
|
|
||||||
username: @username,
|
|
||||||
domain: @domain,
|
|
||||||
email: @email,
|
|
||||||
hashed_pw: hashed_pw,
|
|
||||||
confirmed: @confirmed
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
def create_lndhub_account(user)
|
|
||||||
#TODO enable in development when we have a local lndhub (mock?) API
|
|
||||||
return if Rails.env.development?
|
|
||||||
CreateLndhubAccountJob.perform_later(user)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
module UserManager
|
|
||||||
class CreateInvitations < UserManagerService
|
|
||||||
def initialize(user:, amount:, notify: true)
|
|
||||||
@user = user
|
|
||||||
@amount = amount
|
|
||||||
@notify = notify
|
|
||||||
end
|
|
||||||
|
|
||||||
def call
|
|
||||||
@amount.times do
|
|
||||||
Invitation.create(user: @user)
|
|
||||||
end
|
|
||||||
|
|
||||||
if @notify
|
|
||||||
NotificationMailer.with(user: @user).new_invitations_available.deliver_later
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
module UserManager
|
|
||||||
class UpdatePgpKey < UserManagerService
|
|
||||||
def initialize(user:)
|
|
||||||
@user = user
|
|
||||||
end
|
|
||||||
|
|
||||||
def call
|
|
||||||
if @user.pgp_pubkey.blank?
|
|
||||||
@user.update! pgp_fpr: nil
|
|
||||||
else
|
|
||||||
result = GPGME::Key.import(@user.pgp_pubkey)
|
|
||||||
|
|
||||||
if result.imports.present?
|
|
||||||
@user.update! pgp_fpr: result.imports.first.fpr
|
|
||||||
else
|
|
||||||
# TODO notify Sentry, user
|
|
||||||
raise "Failed to import OpenPGP pubkey"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
LdapManager::UpdatePgpKey.call(dn: @user.dn, pubkey: @user.pgp_pubkey)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
class UserManagerService < ApplicationService
|
|
||||||
end
|
|
||||||
@@ -89,47 +89,13 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="sm:flex-1 sm:pt-0">
|
<section class="sm:flex-1 sm:pt-0">
|
||||||
<h3>LDAP</h3>
|
<% if @avatar.present? %>
|
||||||
<table class="divided">
|
<h3>LDAP<h3>
|
||||||
<tbody>
|
<p>
|
||||||
<tr>
|
<img src="data:image/jpeg;base64,<%= @avatar %>" class="h-48 w-48" />
|
||||||
<th>Avatar</th>
|
</p>
|
||||||
<td>
|
<% end %>
|
||||||
<% if @avatar.present? %>
|
<!-- <h3>Actions</h3> -->
|
||||||
<img src="data:image/jpeg;base64,<%= @avatar %>" class="h-48 w-48" />
|
|
||||||
<% else %>
|
|
||||||
—
|
|
||||||
<% end %>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th>Display name</th>
|
|
||||||
<td><%= @user.display_name || "—" %></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th class="align-top">PGP key</th>
|
|
||||||
<td class="align-top leading-5">
|
|
||||||
<% if @user.pgp_pubkey.present? %>
|
|
||||||
<span class="font-mono" title="<%= @user.pgp_fpr %>">
|
|
||||||
<% if @user.pgp_pubkey_contains_user_address? %>
|
|
||||||
<%= link_to wkd_key_url(hashed_username: @user.wkd_hash, l: @user.cn, format: :txt),
|
|
||||||
class: "ks-text-link", target: "_blank" do %>
|
|
||||||
<%= "#{@user.pgp_fpr[0, 8]}…#{@user.pgp_fpr[-8..-1]}" %>
|
|
||||||
<% end %>
|
|
||||||
<% else %>
|
|
||||||
<%= "#{@user.pgp_fpr[0, 8]}…#{@user.pgp_fpr[-8..-1]}" %>
|
|
||||||
<% end %>
|
|
||||||
</span><br />
|
|
||||||
<% @user.gnupg_key.uids.each do |uid| %>
|
|
||||||
<%= uid.uid %><br />
|
|
||||||
<% end %>
|
|
||||||
<% else %>
|
|
||||||
—
|
|
||||||
<% end %>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -218,7 +184,7 @@
|
|||||||
<td>XMPP (ejabberd)</td>
|
<td>XMPP (ejabberd)</td>
|
||||||
<td>
|
<td>
|
||||||
<%= render FormElements::ToggleComponent.new(
|
<%= render FormElements::ToggleComponent.new(
|
||||||
enabled: @services_enabled.include?("xmpp"),
|
enabled: @services_enabled.include?("ejabberd"),
|
||||||
input_enabled: false
|
input_enabled: false
|
||||||
) %>
|
) %>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<%= tag.section data: {
|
<%= tag.section data: {
|
||||||
controller: "settings--account--email",
|
controller: "settings--account--email",
|
||||||
"settings--account--email-validation-failed-value": @validation_errors&.[](:email)&.present?
|
"settings--account--email-validation-failed-value": @validation_errors.present?
|
||||||
} do %>
|
} do %>
|
||||||
<h3>E-Mail</h3>
|
<h3>E-Mail</h3>
|
||||||
<%= form_for(@user, url: update_email_settings_path, method: "post") do |f| %>
|
<%= form_for(@user, url: update_email_settings_path, method: "post") do |f| %>
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</p>
|
</p>
|
||||||
<% if @validation_errors&.[](:email)&.present? %>
|
<% if @validation_errors.present? && @validation_errors[:email].present? %>
|
||||||
<p class="error-msg"><%= @validation_errors[:email].first %></p>
|
<p class="error-msg"><%= @validation_errors[:email].first %></p>
|
||||||
<% end %>
|
<% end %>
|
||||||
<div class="initial-hidden">
|
<div class="initial-hidden">
|
||||||
@@ -41,33 +41,10 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
<section>
|
<section>
|
||||||
<h3>Password</h3>
|
<h3>Password</h3>
|
||||||
<p class="mb-6">Use the following button to request an email with a password reset link:</p>
|
<p class="mb-8">Use the following button to request an email with a password reset link:</p>
|
||||||
<%= form_with(url: reset_password_settings_path, method: :post) do %>
|
<%= form_with(url: reset_password_settings_path, method: :post) do %>
|
||||||
<p>
|
<p>
|
||||||
<%= submit_tag("Send me a password reset link", class: 'btn-md btn-gray w-full sm:w-auto') %>
|
<%= submit_tag("Send me a password reset link", class: 'btn-md btn-gray w-full sm:w-auto') %>
|
||||||
</p>
|
</p>
|
||||||
<% end %>
|
<% end %>
|
||||||
</section>
|
</section>
|
||||||
<%= form_for(@user, url: setting_path(:account), html: { :method => :put }) do |f| %>
|
|
||||||
<section class="!pt-8 sm:!pt-12">
|
|
||||||
<h3>OpenPGP</h3>
|
|
||||||
<ul role="list">
|
|
||||||
<%= render FormElements::FieldsetComponent.new(
|
|
||||||
title: "Public key",
|
|
||||||
description: "Your OpenPGP public key in ASCII Armor format"
|
|
||||||
) do %>
|
|
||||||
<%= f.text_area :pgp_pubkey,
|
|
||||||
value: @user.pgp_pubkey,
|
|
||||||
class: "h-24 w-full" %>
|
|
||||||
<% if @validation_errors&.[](:pgp_pubkey)&.present? %>
|
|
||||||
<p class="error-msg">This <%= @validation_errors[:pgp_pubkey].first %></p>
|
|
||||||
<% end %>
|
|
||||||
<% end %>
|
|
||||||
</ul>
|
|
||||||
</section>
|
|
||||||
<section>
|
|
||||||
<p class="pt-6 border-t border-gray-200 text-right">
|
|
||||||
<%= f.submit 'Save', class: "btn-md btn-blue w-full md:w-auto" %>
|
|
||||||
</p>
|
|
||||||
</section>
|
|
||||||
<% end %>
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<h3>E-Mail Password</h3>
|
<h3>E-Mail Password</h3>
|
||||||
<%= form_for(@user, url: reset_email_password_settings_path, method: "post") do |f| %>
|
<%= form_for(@user, url: reset_email_password_settings_path, method: "post") do |f| %>
|
||||||
<%= hidden_field_tag :section, "email" %>
|
<%= hidden_field_tag :section, "email" %>
|
||||||
<p class="mb-6">
|
<p class="mb-8">
|
||||||
Use the following button to generate a new email password:
|
Use the following button to generate a new email password:
|
||||||
</p>
|
</p>
|
||||||
<p class="hidden initial-visible">
|
<p class="hidden initial-visible">
|
||||||
|
|||||||
@@ -1,46 +1,32 @@
|
|||||||
<section>
|
<div data-controller="settings--nostr-pubkey"
|
||||||
<h3>Nostr</h3>
|
data-settings--nostr-pubkey-user-address-value="<%= current_user.address %>"
|
||||||
<h4 class="mb-0">Public Key</h4>
|
data-settings--nostr-pubkey-site-value="<%= Setting.accounts_domain %>"
|
||||||
<div data-controller="settings--nostr-pubkey"
|
data-settings--nostr-pubkey-shared-secret-value="<%= session[:shared_secret] %>"
|
||||||
data-settings--nostr-pubkey-user-address-value="<%= current_user.address %>"
|
data-settings--nostr-pubkey-pubkey-hex-value="<%= current_user.nostr_pubkey %>">
|
||||||
data-settings--nostr-pubkey-site-value="<%= Setting.accounts_domain %>"
|
<section>
|
||||||
data-settings--nostr-pubkey-shared-secret-value="<%= session[:shared_secret] %>"
|
<h3>Nostr</h3>
|
||||||
data-settings--nostr-pubkey-pubkey-hex-value="<%= current_user.nostr_pubkey %>">
|
<h4 class="mb-0">
|
||||||
|
Public Key
|
||||||
<p class="<%= current_user.nostr_pubkey.present? ? '' : 'hidden' %> mt-2 flex gap-1">
|
</h4>
|
||||||
|
<p class="<%= current_user.nostr_pubkey.present? ? '' : 'hidden' %> mt-2 flex gap-x-1">
|
||||||
<input type="text" value="<%= current_user.nostr_pubkey_bech32 %>" disabled
|
<input type="text" value="<%= current_user.nostr_pubkey_bech32 %>" disabled
|
||||||
data-settings--nostr-pubkey-target="pubkeyBech32Input"
|
data-settings--nostr-pubkey-target="pubkeyBech32Input"
|
||||||
name="nostr_public_key" class="relative grow" />
|
name="nostr_public_key" class="w-full" />
|
||||||
<%= link_to nostr_pubkey_settings_path,
|
<%= link_to nostr_pubkey_settings_path,
|
||||||
class: 'btn-md btn-outline text-red-700 relative shrink-0',
|
class: 'btn-md btn-outline relative grow-0 shrink-0 text-red-700',
|
||||||
data: { turbo_method: :delete, turbo_confirm: 'Are you sure?' } do %>
|
data: { turbo_method: :delete, turbo_confirm: 'Are you sure?' } do %>
|
||||||
Remove
|
Remove
|
||||||
<% end %>
|
<% end %>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<% if current_user.nostr_pubkey.present? %>
|
<% if current_user.nostr_pubkey.present? %>
|
||||||
<div class="rounded-md bg-blue-50 p-4">
|
<!-- <div> -->
|
||||||
<div class="flex">
|
<!-- Pubkey present -->
|
||||||
<div class="flex-shrink-0">
|
<!-- </div> -->
|
||||||
<svg class="h-5 w-5 text-blue-400" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
|
||||||
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a.75.75 0 000 1.5h.253a.25.25 0 01.244.304l-.459 2.066A1.75 1.75 0 0010.747 15H11a.75.75 0 000-1.5h-.253a.25.25 0 01-.244-.304l.459-2.066A1.75 1.75 0 009.253 9H9z" clip-rule="evenodd" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div class="ml-3 flex-1">
|
|
||||||
<p class="text-sm text-blue-800">
|
|
||||||
Your user address <strong><%= current_user.address %></strong> is
|
|
||||||
also a Nostr address now. Use your favorite Nostr app, or for
|
|
||||||
example <a href="http://metadata.nostr.com" target="_blank"
|
|
||||||
class="underline">metadata.nostr.com</a>, to add this
|
|
||||||
<strong>NIP-05</strong> address to your public profile.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<% else %>
|
<% else %>
|
||||||
<p class="my-4">
|
<p class="my-4">
|
||||||
If you use any apps on the Nostr network, you can verify your public key
|
Verify your Nostr public key with us in order to enable Nostr-specific
|
||||||
with us in order to enable Nostr-specific features for your account.
|
features for your account.
|
||||||
</p>
|
</p>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
@@ -58,8 +44,8 @@
|
|||||||
</h3>
|
</h3>
|
||||||
<div class="mt-2 mb-0 text-sm text-blue-800">
|
<div class="mt-2 mb-0 text-sm text-blue-800">
|
||||||
<p>
|
<p>
|
||||||
We recommend Alby, which you can also use for your Lightning
|
We recommend Alby, which you can also use a wallet for your
|
||||||
Wallet.
|
Lightning account.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-4">
|
<div class="mt-4">
|
||||||
@@ -86,5 +72,113 @@
|
|||||||
</button>
|
</button>
|
||||||
</p>
|
</p>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</section>
|
||||||
</section>
|
|
||||||
|
<% if current_user.nostr_pubkey.present? %>
|
||||||
|
<section>
|
||||||
|
<h3>Profile</h3>
|
||||||
|
<div data-settings--nostr-pubkey-target="profileStatus" class="mb-4">
|
||||||
|
<p class="status-green hidden flex gap-x-4 items-center">
|
||||||
|
<span class="inline-block h-6 w-6 grow-0 text-emerald-500 %>">
|
||||||
|
<%= render "icons/check-circle" %>
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
You already have a profile for your public key
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
<p class="status-orange hidden flex gap-x-4 items-center">
|
||||||
|
<span class="inline-block h-6 w-6 grow-0 text-amber-500 %>">
|
||||||
|
<%= render "icons/alert-octagon" %>
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
<strong><%= current_user.address %></strong> is not set as your Nostr address
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div data-settings--nostr-pubkey-target="profileStatusNip05" class="mb-4">
|
||||||
|
<p class="status-green hidden flex gap-x-4 items-center">
|
||||||
|
<span class="inline-block h-6 w-6 grow-0 text-emerald-500 %>">
|
||||||
|
<%= render "icons/check-circle" %>
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
<strong><%= current_user.address %></strong> is set as your Nostr address
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
<p class="status-orange hidden flex gap-x-4 items-center">
|
||||||
|
<span class="inline-block h-6 w-6 grow-0 text-amber-500 %>">
|
||||||
|
<%= render "icons/alert-octagon" %>
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
<strong><%= current_user.address %></strong> is not set as your Nostr address
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
<p class="status-red hidden flex gap-x-4 items-center">
|
||||||
|
<span class="inline-block h-6 w-6 grow-0 text-amber-500 %>">
|
||||||
|
<%= render "icons/alert-octagon" %>
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
Your profile's Nostr address is not set to <strong><%= current_user.address %></strong> yet
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div data-settings--nostr-pubkey-target="profileStatusLud16">
|
||||||
|
<p class="status-green hidden flex gap-x-4 items-center">
|
||||||
|
<span class="inline-block h-6 w-6 grow-0 text-emerald-500 %>">
|
||||||
|
<%= render "icons/check-circle" %>
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
<strong><%= current_user.address %></strong> is set as your Lightning address
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
<p class="status-orange hidden flex gap-x-4 items-center">
|
||||||
|
<span class="inline-block h-6 w-6 grow-0 text-amber-500 %>">
|
||||||
|
<%= render "icons/alert-octagon" %>
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
<strong><%= current_user.address %></strong> is not set as your Lightning address yet
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
<p class="status-red hidden flex gap-x-4 items-center">
|
||||||
|
<span class="inline-block h-6 w-6 grow-0 text-amber-500 %>">
|
||||||
|
<%= render "icons/alert-octagon" %>
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
Your profile's Lightning address is not set to <strong><%= current_user.address %></strong> yet
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h3>Relays</h3>
|
||||||
|
<div data-settings--nostr-pubkey-target="relayListStatus">
|
||||||
|
<p class="status-green hidden flex gap-x-4 items-center">
|
||||||
|
<span class="inline-block h-6 w-6 grow-0 text-emerald-500 %>">
|
||||||
|
<%= render "icons/check-circle" %>
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
You have a relay list, and the Kosmos relay is part of it
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
<p class="status-orange hidden flex gap-x-4 items-center">
|
||||||
|
<span class="inline-block h-6 w-6 grow-0 text-amber-500 %>">
|
||||||
|
<%= render "icons/alert-octagon" %>
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
The Kosmos relay is missing from your relay list
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
<p class="status-red hidden flex gap-x-4 items-center">
|
||||||
|
<span class="inline-block h-6 w-6 grow-0 text-amber-500 %>">
|
||||||
|
<%= render "icons/alert-octagon" %>
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
We could not find a relay list for your public key
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<ul data-settings--nostr-pubkey-target="relayList">
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
|||||||
@@ -6,3 +6,4 @@ pin "@hotwired/stimulus", to: "stimulus.min.js", preload: true
|
|||||||
pin "@hotwired/stimulus-loading", to: "stimulus-loading.js", preload: true
|
pin "@hotwired/stimulus-loading", to: "stimulus-loading.js", preload: true
|
||||||
pin_all_from "app/javascript/controllers", under: "controllers"
|
pin_all_from "app/javascript/controllers", under: "controllers"
|
||||||
pin "tailwindcss-stimulus-components" # @4.0.3
|
pin "tailwindcss-stimulus-components" # @4.0.3
|
||||||
|
pin "nostrify"
|
||||||
|
|||||||
@@ -70,11 +70,10 @@ Rails.application.routes.draw do
|
|||||||
|
|
||||||
get '.well-known/webfinger', to: 'webfinger#show'
|
get '.well-known/webfinger', to: 'webfinger#show'
|
||||||
get '.well-known/nostr', to: 'well_known#nostr'
|
get '.well-known/nostr', to: 'well_known#nostr'
|
||||||
get '.well-known/lnurlp/:username', to: 'lnurlpay#index', as: :lightning_address
|
get '.well-known/lnurlp/:username', to: 'lnurlpay#index', as: 'lightning_address'
|
||||||
get '.well-known/keysend/:username', to: 'lnurlpay#keysend', as: :lightning_address_keysend
|
get '.well-known/keysend/:username', to: 'lnurlpay#keysend', as: 'lightning_address_keysend'
|
||||||
get '.well-known/openpgpkey/hu/:hashed_username(.:format)', to: 'web_key_directory#show', as: :wkd_key
|
|
||||||
|
|
||||||
get 'lnurlpay/:username/invoice', to: 'lnurlpay#invoice', as: :lnurlpay_invoice
|
get 'lnurlpay/:username/invoice', to: 'lnurlpay#invoice', as: 'lnurlpay_invoice'
|
||||||
|
|
||||||
post 'webhooks/lndhub', to: 'webhooks#lndhub'
|
post 'webhooks/lndhub', to: 'webhooks#lndhub'
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
class AddPgpFprToUsers < ActiveRecord::Migration[7.1]
|
|
||||||
def change
|
|
||||||
add_column :users, :pgp_fpr, :string
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema[7.1].define(version: 2024_09_22_205634) do
|
ActiveRecord::Schema[7.1].define(version: 2024_06_07_123654) do
|
||||||
create_table "active_storage_attachments", force: :cascade do |t|
|
create_table "active_storage_attachments", force: :cascade do |t|
|
||||||
t.string "name", null: false
|
t.string "name", null: false
|
||||||
t.string "record_type", null: false
|
t.string "record_type", null: false
|
||||||
@@ -132,7 +132,6 @@ ActiveRecord::Schema[7.1].define(version: 2024_09_22_205634) do
|
|||||||
t.datetime "remember_created_at"
|
t.datetime "remember_created_at"
|
||||||
t.string "remember_token"
|
t.string "remember_token"
|
||||||
t.text "preferences"
|
t.text "preferences"
|
||||||
t.string "pgp_fpr"
|
|
||||||
t.index ["email"], name: "index_users_on_email", unique: true
|
t.index ["email"], name: "index_users_on_email", unique: true
|
||||||
t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
|
t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
|
||||||
|
|
||||||
mDMEZvGiUxYJKwYBBAHaRw8BAQdARPZXLqyB3nylJuzuARlOJxqc9mchMKHI4Cy+
|
|
||||||
hPWlzja0GEFkbWluIDxhZG1pbkBrb3Ntb3Mub3JnPoiZBBMWCgBBFiEE0pie1+fG
|
|
||||||
ImdZwzGnwgEYSg8AulYFAmbxolMCGwMFCQWjmoAFCwkIBwICIgIGFQoJCAsCBBYC
|
|
||||||
AwECHgcCF4AACgkQwgEYSg8AulaldAEA7yzh7XRCdIJDHgLUvKHsy2NnyLaDD1Tl
|
|
||||||
hyZWbl5og0IBAJAQ2Dm82YXMdUK3X1OGlK8KH5O4E5lSFY4+8/xx0UEJuDgEZvGi
|
|
||||||
UxIKKwYBBAGXVQEFAQEHQJc8pzzeIF7Hm5z1eseRAqGvFa+V1BIDf+1XQzuJhhxi
|
|
||||||
AwEIB4h+BBgWCgAmFiEE0pie1+fGImdZwzGnwgEYSg8AulYFAmbxolMCGwwFCQWj
|
|
||||||
moAACgkQwgEYSg8AulbLtgEApZvuDqSP77lrl1jmtCAJEEZk/ofsRFkf1g3U3Zhm
|
|
||||||
9PcA/1+AbcyqjLTcqIPjHmZyGEPiaAvEsBzbPKEPiL3JYhkG
|
|
||||||
=45sx
|
|
||||||
-----END PGP PUBLIC KEY BLOCK-----
|
|
||||||
@@ -21,7 +21,7 @@ namespace :ldap do
|
|||||||
|
|
||||||
desc "Add custom attributes to schema"
|
desc "Add custom attributes to schema"
|
||||||
task add_custom_attributes: :environment do |t, args|
|
task add_custom_attributes: :environment do |t, args|
|
||||||
%w[ admin service_enabled nostr_key pgp_key ].each do |name|
|
%w[ admin service_enabled nostr_key ].each do |name|
|
||||||
Rake::Task["ldap:modify_ldap_schema"].invoke(name, "add")
|
Rake::Task["ldap:modify_ldap_schema"].invoke(name, "add")
|
||||||
Rake::Task['ldap:modify_ldap_schema'].reenable
|
Rake::Task['ldap:modify_ldap_schema'].reenable
|
||||||
end
|
end
|
||||||
@@ -29,7 +29,7 @@ namespace :ldap do
|
|||||||
|
|
||||||
desc "Delete custom attributes from schema"
|
desc "Delete custom attributes from schema"
|
||||||
task delete_custom_attributes: :environment do |t, args|
|
task delete_custom_attributes: :environment do |t, args|
|
||||||
%w[ admin service_enabled nostr_key pgp_key ].each do |name|
|
%w[ admin service_enabled nostr_key ].each do |name|
|
||||||
Rake::Task["ldap:modify_ldap_schema"].invoke(name, "delete")
|
Rake::Task["ldap:modify_ldap_schema"].invoke(name, "delete")
|
||||||
Rake::Task['ldap:modify_ldap_schema'].reenable
|
Rake::Task['ldap:modify_ldap_schema'].reenable
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
dn: cn=schema
|
|
||||||
changetype: modify
|
|
||||||
add: attributeTypes
|
|
||||||
attributeTypes: ( 1.3.6.1.4.1.3401.8.2.11
|
|
||||||
NAME 'pgpKey'
|
|
||||||
DESC 'OpenPGP public key block'
|
|
||||||
SYNTAX 1.3.6.1.4.1.1466.115.121.1.26
|
|
||||||
SINGLE-VALUE )
|
|
||||||
@@ -14,7 +14,6 @@ RSpec.describe 'Account settings', type: :feature do
|
|||||||
.with("invalid password").and_return(false)
|
.with("invalid password").and_return(false)
|
||||||
allow_any_instance_of(User).to receive(:valid_ldap_authentication?)
|
allow_any_instance_of(User).to receive(:valid_ldap_authentication?)
|
||||||
.with("valid password").and_return(true)
|
.with("valid password").and_return(true)
|
||||||
allow_any_instance_of(User).to receive(:pgp_pubkey).and_return(nil)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
scenario 'fails with invalid password' do
|
scenario 'fails with invalid password' do
|
||||||
@@ -56,44 +55,4 @@ RSpec.describe 'Account settings', type: :feature do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
feature "Update OpenPGP key" do
|
|
||||||
let(:invalid_key) { File.read("#{Rails.root}/spec/fixtures/files/pgp_key_invalid.asc") }
|
|
||||||
let(:valid_key_alice) { File.read("#{Rails.root}/spec/fixtures/files/pgp_key_valid_alice.asc") }
|
|
||||||
let(:fingerprint_alice) { "EB85BB5FA33A75E15E944E63F231550C4F47E38E" }
|
|
||||||
|
|
||||||
before do
|
|
||||||
login_as user, :scope => :user
|
|
||||||
allow_any_instance_of(User).to receive(:ldap_entry).and_return({
|
|
||||||
uid: user.cn, ou: user.ou, display_name: nil, pgp_key: nil
|
|
||||||
})
|
|
||||||
end
|
|
||||||
|
|
||||||
scenario 'rejects an invalid key' do
|
|
||||||
expect(UserManager::UpdatePgpKey).not_to receive(:call)
|
|
||||||
|
|
||||||
visit setting_path(:account)
|
|
||||||
fill_in 'Public key', with: invalid_key
|
|
||||||
click_button "Save"
|
|
||||||
|
|
||||||
expect(current_url).to eq(setting_url(:account))
|
|
||||||
within ".error-msg" do
|
|
||||||
expect(page).to have_content("This is not a valid armored PGP public key block")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
scenario 'stores a valid key' do
|
|
||||||
expect(UserManager::UpdatePgpKey).to receive(:call)
|
|
||||||
.with(user: user).and_return(true)
|
|
||||||
|
|
||||||
visit setting_path(:account)
|
|
||||||
fill_in 'Public key', with: valid_key_alice
|
|
||||||
click_button "Save"
|
|
||||||
|
|
||||||
expect(current_url).to eq(setting_url(:account))
|
|
||||||
within ".flash-msg" do
|
|
||||||
expect(page).to have_content("Settings saved")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ RSpec.describe 'Profile settings', type: :feature do
|
|||||||
allow(user).to receive(:display_name).and_return("Mark")
|
allow(user).to receive(:display_name).and_return("Mark")
|
||||||
allow_any_instance_of(User).to receive(:dn).and_return("cn=mwahlberg,ou=kosmos.org,cn=users,dc=kosmos,dc=org")
|
allow_any_instance_of(User).to receive(:dn).and_return("cn=mwahlberg,ou=kosmos.org,cn=users,dc=kosmos,dc=org")
|
||||||
allow_any_instance_of(User).to receive(:ldap_entry).and_return({
|
allow_any_instance_of(User).to receive(:ldap_entry).and_return({
|
||||||
uid: user.cn, ou: user.ou, display_name: "Mark", pgp_key: nil
|
uid: user.cn, ou: user.ou, display_name: "Mark"
|
||||||
})
|
})
|
||||||
allow_any_instance_of(User).to receive(:avatar).and_return(avatar_base64)
|
allow_any_instance_of(User).to receive(:avatar).and_return(avatar_base64)
|
||||||
|
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ RSpec.describe "Signup", type: :feature do
|
|||||||
click_button "Continue"
|
click_button "Continue"
|
||||||
expect(page).to have_content("Choose a password")
|
expect(page).to have_content("Choose a password")
|
||||||
|
|
||||||
expect(UserManager::CreateAccount).to receive(:call)
|
expect(CreateAccount).to receive(:call)
|
||||||
.with(account: {
|
.with(account: {
|
||||||
username: "tony", domain: "kosmos.org",
|
username: "tony", domain: "kosmos.org",
|
||||||
email: "tony@example.com", password: "a-valid-password",
|
email: "tony@example.com", password: "a-valid-password",
|
||||||
@@ -96,7 +96,7 @@ RSpec.describe "Signup", type: :feature do
|
|||||||
click_button "Create account"
|
click_button "Create account"
|
||||||
expect(page).to have_content("Password is too short")
|
expect(page).to have_content("Password is too short")
|
||||||
|
|
||||||
expect(UserManager::CreateAccount).to receive(:call)
|
expect(CreateAccount).to receive(:call)
|
||||||
.with(account: {
|
.with(account: {
|
||||||
username: "tony", domain: "kosmos.org",
|
username: "tony", domain: "kosmos.org",
|
||||||
email: "tony@example.com", password: "a-valid-password",
|
email: "tony@example.com", password: "a-valid-password",
|
||||||
|
|||||||
11
spec/fixtures/files/pgp_key_invalid.asc
vendored
11
spec/fixtures/files/pgp_key_invalid.asc
vendored
@@ -1,11 +0,0 @@
|
|||||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
|
||||||
b7O1u120JkFsaWNlIExvdmVsYWNlIDxhbGljZUBvcGVucGdwLmV4YW1wbGU+iJAE
|
|
||||||
ExYIADgCGwMFCwkIBwIGFQoJCAsCBBYCAwECHgECF4AWIQTrhbtfozp14V6UTmPy
|
|
||||||
MVUMT0fjjgUCXaWfOgAKCRDyMVUMT0fjjukrAPoDnHBSogOmsHOsd9qGsiZpgRnO
|
|
||||||
dypvbm+QtXZqth9rvwD9HcDC0tC+PHAsO7OTh1S1TC9RiJsvawAfCPaQZoed8gK4
|
|
||||||
OARcRwTpEgorBgEEAZdVAQUBAQdAQv8GIa2rSTzgqbXCpDDYMiKRVitCsy203x3s
|
|
||||||
E9+eviIDAQgHiHgEGBYIACAWIQTrhbtfozp14V6UTmPyMVUMT0fjjgUCXEcE6QIb
|
|
||||||
DAAKCRDyMVUMT0fjjlnQAQDFHUs6TIcxrNTtEZFjUFm1M0PJ1Dng/cDW4xN80fsn
|
|
||||||
0QEA22Kr7VkCjeAEC08VSTeV+QFsmz55/lntWkwYWhmvOgE=
|
|
||||||
=iIGO
|
|
||||||
-----END PGP PUBLIC KEY BLOCK-----
|
|
||||||
16
spec/fixtures/files/pgp_key_valid_alice.asc
vendored
16
spec/fixtures/files/pgp_key_valid_alice.asc
vendored
@@ -1,16 +0,0 @@
|
|||||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
|
||||||
Comment: Alice's OpenPGP certificate
|
|
||||||
Comment: https://www.ietf.org/id/draft-bre-openpgp-samples-01.html
|
|
||||||
|
|
||||||
mDMEXEcE6RYJKwYBBAHaRw8BAQdArjWwk3FAqyiFbFBKT4TzXcVBqPTB3gmzlC/U
|
|
||||||
b7O1u120JkFsaWNlIExvdmVsYWNlIDxhbGljZUBvcGVucGdwLmV4YW1wbGU+iJAE
|
|
||||||
ExYIADgCGwMFCwkIBwIGFQoJCAsCBBYCAwECHgECF4AWIQTrhbtfozp14V6UTmPy
|
|
||||||
MVUMT0fjjgUCXaWfOgAKCRDyMVUMT0fjjukrAPoDnHBSogOmsHOsd9qGsiZpgRnO
|
|
||||||
dypvbm+QtXZqth9rvwD9HcDC0tC+PHAsO7OTh1S1TC9RiJsvawAfCPaQZoed8gK4
|
|
||||||
OARcRwTpEgorBgEEAZdVAQUBAQdAQv8GIa2rSTzgqbXCpDDYMiKRVitCsy203x3s
|
|
||||||
E9+eviIDAQgHiHgEGBYIACAWIQTrhbtfozp14V6UTmPyMVUMT0fjjgUCXEcE6QIb
|
|
||||||
DAAKCRDyMVUMT0fjjlnQAQDFHUs6TIcxrNTtEZFjUFm1M0PJ1Dng/cDW4xN80fsn
|
|
||||||
0QEA22Kr7VkCjeAEC08VSTeV+QFsmz55/lntWkwYWhmvOgE=
|
|
||||||
=iIGO
|
|
||||||
|
|
||||||
-----END PGP PUBLIC KEY BLOCK-----
|
|
||||||
13
spec/fixtures/files/pgp_key_valid_jimmy.asc
vendored
13
spec/fixtures/files/pgp_key_valid_jimmy.asc
vendored
@@ -1,13 +0,0 @@
|
|||||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
|
||||||
|
|
||||||
mDMEZvFjRhYJKwYBBAHaRw8BAQdACUxVX9bGlbuNR0MNYUyHHxTcOgm4qjwq8Bjg
|
|
||||||
7P41OFK0GEppbW15IDxqaW1teUBrb3Ntb3Mub3JnPoiZBBMWCgBBFiEEMWv1FiNt
|
|
||||||
r3cjaxX2BX2Tly+4YsMFAmbxY0YCGwMFCQWjmoAFCwkIBwICIgIGFQoJCAsCBBYC
|
|
||||||
AwECHgcCF4AACgkQBX2Tly+4YsMjHgEAoOOLrv9pWbi8hhrSMkqJ7FJvsBTQF//U
|
|
||||||
aJUQRa8CTgoBAI3kyGKZ8gOC8UOOKsUC0LiNCVXPyX45h8T4QFRdEVYKuDgEZvFj
|
|
||||||
RhIKKwYBBAGXVQEFAQEHQIomqcQ59UjtQex54pz8qGqyxCj2DPJYUat9pXinDgN8
|
|
||||||
AwEIB4h+BBgWCgAmFiEEMWv1FiNtr3cjaxX2BX2Tly+4YsMFAmbxY0YCGwwFCQWj
|
|
||||||
moAACgkQBX2Tly+4YsPoVgEA/9Q5Gs1klP4u/nw343V57e9s4RKmEiRSkErnC9wW
|
|
||||||
Iu0A/jp6Elz2pDQPB2XLwcb+n7JlgA05HI0zWj1+EoM7TC4J
|
|
||||||
=KQbn
|
|
||||||
-----END PGP PUBLIC KEY BLOCK-----
|
|
||||||
BIN
spec/fixtures/files/pgp_key_valid_jimmy.pem
vendored
BIN
spec/fixtures/files/pgp_key_valid_jimmy.pem
vendored
Binary file not shown.
@@ -1,16 +1,20 @@
|
|||||||
require 'rails_helper'
|
require 'rails_helper'
|
||||||
|
|
||||||
RSpec.describe User, type: :model do
|
RSpec.describe User, type: :model do
|
||||||
let(:user) { create :user, cn: "philipp", ou: "kosmos.org", email: "philipp@example.com" }
|
let(:user) { create :user, cn: "philipp" }
|
||||||
let(:dn) { "cn=philipp,ou=kosmos.org,cn=users,dc=kosmos,dc=org" }
|
let(:dn) { "cn=philipp,ou=kosmos.org,cn=users,dc=kosmos,dc=org" }
|
||||||
|
|
||||||
describe "#address" do
|
describe "#address" do
|
||||||
|
let(:user) { build :user, cn: "jimmy", ou: "kosmos.org" }
|
||||||
|
|
||||||
it "returns the user address" do
|
it "returns the user address" do
|
||||||
expect(user.address).to eq("philipp@kosmos.org")
|
expect(user.address).to eq("jimmy@kosmos.org")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "#mastodon_address" do
|
describe "#mastodon_address" do
|
||||||
|
let(:user) { build :user, cn: "jimmy", ou: "kosmos.org" }
|
||||||
|
|
||||||
context "Mastodon service not configured" do
|
context "Mastodon service not configured" do
|
||||||
before do
|
before do
|
||||||
Setting.mastodon_enabled = false
|
Setting.mastodon_enabled = false
|
||||||
@@ -28,7 +32,7 @@ RSpec.describe User, type: :model do
|
|||||||
|
|
||||||
describe "domain is the same as primary domain" do
|
describe "domain is the same as primary domain" do
|
||||||
it "returns the user address" do
|
it "returns the user address" do
|
||||||
expect(user.mastodon_address).to eq("philipp@kosmos.org")
|
expect(user.mastodon_address).to eq("jimmy@kosmos.org")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -38,7 +42,7 @@ RSpec.describe User, type: :model do
|
|||||||
end
|
end
|
||||||
|
|
||||||
it "returns the user address" do
|
it "returns the user address" do
|
||||||
expect(user.mastodon_address).to eq("philipp@kosmos.social")
|
expect(user.mastodon_address).to eq("jimmy@kosmos.social")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -235,7 +239,7 @@ RSpec.describe User, type: :model do
|
|||||||
|
|
||||||
describe "#nostr_pubkey" do
|
describe "#nostr_pubkey" do
|
||||||
before do
|
before do
|
||||||
allow(user).to receive(:ldap_entry)
|
allow_any_instance_of(User).to receive(:ldap_entry)
|
||||||
.and_return({ nostr_key: "07e188a1ff87ce171d517b8ed2bb7a31b1d3453a0db3b15379ec07b724d232f3" })
|
.and_return({ nostr_key: "07e188a1ff87ce171d517b8ed2bb7a31b1d3453a0db3b15379ec07b724d232f3" })
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -246,7 +250,7 @@ RSpec.describe User, type: :model do
|
|||||||
|
|
||||||
describe "#nostr_pubkey_bech32" do
|
describe "#nostr_pubkey_bech32" do
|
||||||
before do
|
before do
|
||||||
allow(user).to receive(:ldap_entry)
|
allow_any_instance_of(User).to receive(:ldap_entry)
|
||||||
.and_return({ nostr_key: "07e188a1ff87ce171d517b8ed2bb7a31b1d3453a0db3b15379ec07b724d232f3" })
|
.and_return({ nostr_key: "07e188a1ff87ce171d517b8ed2bb7a31b1d3453a0db3b15379ec07b724d232f3" })
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -254,73 +258,4 @@ RSpec.describe User, type: :model do
|
|||||||
expect(user.nostr_pubkey_bech32).to eq("npub1qlsc3g0lsl8pw8230w8d9wm6xxcax3f6pkemz5measrmwfxjxteslf2hac")
|
expect(user.nostr_pubkey_bech32).to eq("npub1qlsc3g0lsl8pw8230w8d9wm6xxcax3f6pkemz5measrmwfxjxteslf2hac")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "OpenPGP key" do
|
|
||||||
let(:alice) { create :user, id: 2, cn: "alice", email: "alice@example.com" }
|
|
||||||
let(:jimmy) { create :user, id: 3, cn: "jimmy", email: "jimmy@example.com" }
|
|
||||||
let(:valid_key_alice) { File.read("#{Rails.root}/spec/fixtures/files/pgp_key_valid_alice.asc") }
|
|
||||||
let(:valid_key_jimmy) { File.read("#{Rails.root}/spec/fixtures/files/pgp_key_valid_jimmy.asc") }
|
|
||||||
let(:fingerprint_alice) { "EB85BB5FA33A75E15E944E63F231550C4F47E38E" }
|
|
||||||
let(:fingerprint_jimmy) { "316BF516236DAF77236B15F6057D93972FB862C3" }
|
|
||||||
let(:invalid_key) { File.read("#{Rails.root}/spec/fixtures/files/pgp_key_invalid.asc") }
|
|
||||||
|
|
||||||
before do
|
|
||||||
GPGME::Key.import(valid_key_alice)
|
|
||||||
GPGME::Key.import(valid_key_jimmy)
|
|
||||||
alice.update pgp_fpr: fingerprint_alice
|
|
||||||
jimmy.update pgp_fpr: fingerprint_jimmy
|
|
||||||
allow(alice).to receive(:ldap_entry).and_return({ pgp_key: valid_key_alice })
|
|
||||||
allow(jimmy).to receive(:ldap_entry).and_return({ pgp_key: valid_key_jimmy })
|
|
||||||
end
|
|
||||||
|
|
||||||
after do
|
|
||||||
alice.gnupg_key.delete!
|
|
||||||
jimmy.gnupg_key.delete!
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "#acceptable_pgp_key_format" do
|
|
||||||
it "validates the record when the key is valid" do
|
|
||||||
alice.pgp_pubkey = valid_key_alice
|
|
||||||
expect(alice).to be_valid
|
|
||||||
end
|
|
||||||
|
|
||||||
it "adds a validation error when the key is not valid" do
|
|
||||||
user.pgp_pubkey = invalid_key
|
|
||||||
expect(user).to_not be_valid
|
|
||||||
expect(user.errors[:pgp_pubkey]).to be_present
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "#pgp_pubkey" do
|
|
||||||
it "returns the raw pubkey from LDAP" do
|
|
||||||
expect(alice.pgp_pubkey).to eq(valid_key_alice)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "#gnupg_key" do
|
|
||||||
subject { alice.gnupg_key }
|
|
||||||
|
|
||||||
it "returns a GPGME::Key object from the system's GPG keyring" do
|
|
||||||
expect(subject).to be_a(GPGME::Key)
|
|
||||||
expect(subject.fingerprint).to eq(fingerprint_alice)
|
|
||||||
expect(subject.email).to eq("alice@openpgp.example")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "#pgp_pubkey_contains_user_address?" do
|
|
||||||
it "returns false when the user address is one of the UIDs of the key" do
|
|
||||||
expect(alice.pgp_pubkey_contains_user_address?).to eq(false)
|
|
||||||
end
|
|
||||||
|
|
||||||
it "returns true when the user address is missing from the UIDs of the key" do
|
|
||||||
expect(jimmy.pgp_pubkey_contains_user_address?).to eq(true)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "wkd_hash" do
|
|
||||||
it "returns a z-base32 encoded SHA-1 digest of the username" do
|
|
||||||
expect(alice.wkd_hash).to eq("kei1q4tipxxu1yj79k9kfukdhfy631xe")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,84 +0,0 @@
|
|||||||
require 'rails_helper'
|
|
||||||
|
|
||||||
RSpec.describe "OpenPGP Web Key Directory", type: :request do
|
|
||||||
describe "non-existent user" do
|
|
||||||
it "returns a 404 status" do
|
|
||||||
get "/.well-known/openpgpkey/hu/fmb8gw3n4zdj4xpwaziki4mwcxr1368i?l=aristotle"
|
|
||||||
expect(response).to have_http_status(:not_found)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "user without pubkey" do
|
|
||||||
let(:user) { create :user, cn: 'bernd', ou: 'kosmos.org' }
|
|
||||||
|
|
||||||
it "returns a 404 status" do
|
|
||||||
get "/.well-known/openpgpkey/hu/kp95h369c89sx8ia1hn447i868nqyz4t?l=bernd"
|
|
||||||
expect(response).to have_http_status(:not_found)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "user with pubkey" do
|
|
||||||
let(:alice) { create :user, id: 2, cn: "alice", email: "alice@example.com" }
|
|
||||||
let(:jimmy) { create :user, id: 3, cn: "jimmy", email: "jimmy@example.com" }
|
|
||||||
let(:valid_key_alice) { File.read("#{Rails.root}/spec/fixtures/files/pgp_key_valid_alice.asc") }
|
|
||||||
let(:valid_key_jimmy) { File.read("#{Rails.root}/spec/fixtures/files/pgp_key_valid_jimmy.asc") }
|
|
||||||
let(:fingerprint_alice) { "EB85BB5FA33A75E15E944E63F231550C4F47E38E" }
|
|
||||||
let(:fingerprint_jimmy) { "316BF516236DAF77236B15F6057D93972FB862C3" }
|
|
||||||
let(:invalid_key) { File.read("#{Rails.root}/spec/fixtures/files/pgp_key_invalid.asc") }
|
|
||||||
|
|
||||||
before do
|
|
||||||
GPGME::Key.import(valid_key_alice)
|
|
||||||
GPGME::Key.import(valid_key_jimmy)
|
|
||||||
alice.update pgp_fpr: fingerprint_alice
|
|
||||||
jimmy.update pgp_fpr: fingerprint_jimmy
|
|
||||||
end
|
|
||||||
|
|
||||||
after do
|
|
||||||
alice.gnupg_key.delete!
|
|
||||||
jimmy.gnupg_key.delete!
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "pubkey does not contain user address" do
|
|
||||||
before do
|
|
||||||
allow_any_instance_of(User).to receive(:ldap_entry)
|
|
||||||
.and_return({ pgp_key: valid_key_alice })
|
|
||||||
end
|
|
||||||
|
|
||||||
it "returns a 404 status" do
|
|
||||||
get "/.well-known/openpgpkey/hu/kei1q4tipxxu1yj79k9kfukdhfy631xe?l=alice"
|
|
||||||
expect(response).to have_http_status(:not_found)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "pubkey contains user address" do
|
|
||||||
before do
|
|
||||||
allow_any_instance_of(User).to receive(:ldap_entry)
|
|
||||||
.and_return({ pgp_key: valid_key_jimmy })
|
|
||||||
end
|
|
||||||
|
|
||||||
it "returns the pubkey in binary format" do
|
|
||||||
get "/.well-known/openpgpkey/hu/yuca4ky39mhwkjo78qb8zjgbfj1hg3yf?l=jimmy"
|
|
||||||
expect(response).to have_http_status(:ok)
|
|
||||||
expect(response.headers['Content-Type']).to eq("application/octet-stream")
|
|
||||||
expected_binary_data = File.binread("#{Rails.root}/spec/fixtures/files/pgp_key_valid_jimmy.pem")
|
|
||||||
expect(response.body).to eq(expected_binary_data)
|
|
||||||
end
|
|
||||||
|
|
||||||
context "with .txt extension" do
|
|
||||||
it "returns the pubkey as ASCII Armor plain text" do
|
|
||||||
get "/.well-known/openpgpkey/hu/yuca4ky39mhwkjo78qb8zjgbfj1hg3yf.txt?l=jimmy"
|
|
||||||
expect(response).to have_http_status(:ok)
|
|
||||||
expect(response.body).to eq(valid_key_jimmy)
|
|
||||||
expect(response.headers['Content-Type']).to eq("text/plain")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context "invalid URL" do
|
|
||||||
it "returns a 422 status" do
|
|
||||||
get "/.well-known/openpgpkey/hu/123456abcdef?l=alice"
|
|
||||||
expect(response).to have_http_status(:not_found)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
require 'rails_helper'
|
require 'rails_helper'
|
||||||
|
|
||||||
RSpec.describe UserManager::CreateAccount, type: :model do
|
RSpec.describe CreateAccount, type: :model do
|
||||||
describe "#create_user_in_database" do
|
describe "#create_user_in_database" do
|
||||||
let(:service) { described_class.new(account: {
|
let(:service) { CreateAccount.new(account: {
|
||||||
username: 'isaacnewton',
|
username: 'isaacnewton',
|
||||||
email: 'isaacnewton@example.com',
|
email: 'isaacnewton@example.com',
|
||||||
password: 'bright-ideas-in-autumn'
|
password: 'bright-ideas-in-autumn'
|
||||||
@@ -19,7 +19,7 @@ RSpec.describe UserManager::CreateAccount, type: :model do
|
|||||||
|
|
||||||
describe "#update_invitation" do
|
describe "#update_invitation" do
|
||||||
let(:invitation) { create :invitation }
|
let(:invitation) { create :invitation }
|
||||||
let(:service) { described_class.new(account: {
|
let(:service) { CreateAccount.new(account: {
|
||||||
username: 'isaacnewton',
|
username: 'isaacnewton',
|
||||||
email: 'isaacnewton@example.com',
|
email: 'isaacnewton@example.com',
|
||||||
password: 'bright-ideas-in-autumn',
|
password: 'bright-ideas-in-autumn',
|
||||||
@@ -42,7 +42,7 @@ RSpec.describe UserManager::CreateAccount, type: :model do
|
|||||||
describe "#add_ldap_document" do
|
describe "#add_ldap_document" do
|
||||||
include ActiveJob::TestHelper
|
include ActiveJob::TestHelper
|
||||||
|
|
||||||
let(:service) { described_class.new(account: {
|
let(:service) { CreateAccount.new(account: {
|
||||||
username: 'halfinney',
|
username: 'halfinney',
|
||||||
email: 'halfinney@example.com',
|
email: 'halfinney@example.com',
|
||||||
password: 'remember-remember-the-5th-of-november'
|
password: 'remember-remember-the-5th-of-november'
|
||||||
@@ -68,7 +68,7 @@ RSpec.describe UserManager::CreateAccount, type: :model do
|
|||||||
describe "#add_ldap_document for pre-confirmed account" do
|
describe "#add_ldap_document for pre-confirmed account" do
|
||||||
include ActiveJob::TestHelper
|
include ActiveJob::TestHelper
|
||||||
|
|
||||||
let(:service) { described_class.new(account: {
|
let(:service) { CreateAccount.new(account: {
|
||||||
username: 'halfinney',
|
username: 'halfinney',
|
||||||
email: 'halfinney@example.com',
|
email: 'halfinney@example.com',
|
||||||
password: 'remember-remember-the-5th-of-november',
|
password: 'remember-remember-the-5th-of-november',
|
||||||
@@ -89,7 +89,7 @@ RSpec.describe UserManager::CreateAccount, type: :model do
|
|||||||
describe "#create_lndhub_account" do
|
describe "#create_lndhub_account" do
|
||||||
include ActiveJob::TestHelper
|
include ActiveJob::TestHelper
|
||||||
|
|
||||||
let(:service) { described_class.new(account: {
|
let(:service) { CreateAccount.new(account: {
|
||||||
username: 'halfinney', email: 'halfinney@example.com',
|
username: 'halfinney', email: 'halfinney@example.com',
|
||||||
password: 'bright-ideas-in-winter'
|
password: 'bright-ideas-in-winter'
|
||||||
})}
|
})}
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
require 'rails_helper'
|
require 'rails_helper'
|
||||||
|
|
||||||
RSpec.describe UserManager::CreateInvitations, type: :model do
|
RSpec.describe CreateInvitations, type: :model do
|
||||||
include ActiveJob::TestHelper
|
include ActiveJob::TestHelper
|
||||||
|
|
||||||
let(:user) { create :user }
|
let(:user) { create :user }
|
||||||
|
|
||||||
describe "#call" do
|
describe "#call" do
|
||||||
before do
|
before do
|
||||||
described_class.call(user: user, amount: 5)
|
CreateInvitations.call(user: user, amount: 5)
|
||||||
end
|
end
|
||||||
|
|
||||||
after(:each) { clear_enqueued_jobs }
|
after(:each) { clear_enqueued_jobs }
|
||||||
@@ -28,7 +28,7 @@ RSpec.describe UserManager::CreateInvitations, type: :model do
|
|||||||
|
|
||||||
describe "#call with notification disabled" do
|
describe "#call with notification disabled" do
|
||||||
before do
|
before do
|
||||||
described_class.call(user: user, amount: 3, notify: false)
|
CreateInvitations.call(user: user, amount: 3, notify: false)
|
||||||
end
|
end
|
||||||
|
|
||||||
after(:each) { clear_enqueued_jobs }
|
after(:each) { clear_enqueued_jobs }
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
require 'rails_helper'
|
|
||||||
|
|
||||||
RSpec.describe UserManager::UpdatePgpKey, type: :model do
|
|
||||||
include ActiveJob::TestHelper
|
|
||||||
|
|
||||||
let(:alice) { create :user, cn: "alice" }
|
|
||||||
let(:dn) { "cn=alice,ou=kosmos.org,cn=users,dc=kosmos,dc=org" }
|
|
||||||
let(:pubkey_asc) { File.read("#{Rails.root}/spec/fixtures/files/pgp_key_valid_alice.asc") }
|
|
||||||
let(:fingerprint) { "EB85BB5FA33A75E15E944E63F231550C4F47E38E" }
|
|
||||||
|
|
||||||
before do
|
|
||||||
allow(alice).to receive(:dn).and_return(dn)
|
|
||||||
allow(alice).to receive(:ldap_entry).and_return({
|
|
||||||
uid: alice.cn, ou: alice.ou, pgp_key: nil
|
|
||||||
})
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "#call" do
|
|
||||||
context "with valid key" do
|
|
||||||
before do
|
|
||||||
alice.pgp_pubkey = pubkey_asc
|
|
||||||
|
|
||||||
allow(LdapManager::UpdatePgpKey).to receive(:call)
|
|
||||||
.with(dn: alice.dn, pubkey: pubkey_asc)
|
|
||||||
end
|
|
||||||
|
|
||||||
after do
|
|
||||||
alice.gnupg_key.delete!
|
|
||||||
end
|
|
||||||
|
|
||||||
it "imports the key into the GnuPG keychain" do
|
|
||||||
described_class.call(user: alice)
|
|
||||||
expect(alice.gnupg_key).to be_present
|
|
||||||
end
|
|
||||||
|
|
||||||
it "stores the key's fingerprint on the user record" do
|
|
||||||
described_class.call(user: alice)
|
|
||||||
expect(alice.pgp_fpr).to eq(fingerprint)
|
|
||||||
end
|
|
||||||
|
|
||||||
it "updates the user's LDAP entry with the new key" do
|
|
||||||
expect(LdapManager::UpdatePgpKey).to receive(:call)
|
|
||||||
.with(dn: alice.dn, pubkey: pubkey_asc)
|
|
||||||
described_class.call(user: alice)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context "with empty key" do
|
|
||||||
before do
|
|
||||||
alice.update pgp_fpr: fingerprint
|
|
||||||
alice.pgp_pubkey = ""
|
|
||||||
|
|
||||||
allow(LdapManager::UpdatePgpKey).to receive(:call)
|
|
||||||
.with(dn: alice.dn, pubkey: "")
|
|
||||||
end
|
|
||||||
|
|
||||||
it "does not attempt to import the key" do
|
|
||||||
expect(GPGME::Key).not_to receive(:import)
|
|
||||||
described_class.call(user: alice)
|
|
||||||
end
|
|
||||||
|
|
||||||
it "removes the key's fingerprint from the user record" do
|
|
||||||
described_class.call(user: alice)
|
|
||||||
expect(alice.pgp_fpr).to be_nil
|
|
||||||
end
|
|
||||||
|
|
||||||
it "removes the key from the user's LDAP entry" do
|
|
||||||
expect(LdapManager::UpdatePgpKey).to receive(:call)
|
|
||||||
.with(dn: alice.dn, pubkey: "")
|
|
||||||
described_class.call(user: alice)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
11690
vendor/javascript/nostrify.js
vendored
Normal file
11690
vendor/javascript/nostrify.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
vendor/javascript/nostrify.js.map
vendored
Normal file
1
vendor/javascript/nostrify.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user