Merge branch 'master' into feature/ln_address_keysend
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing

This commit is contained in:
Greg 2023-03-02 15:49:13 +00:00
commit cca44d7542
57 changed files with 1042 additions and 214 deletions

View File

@ -2,7 +2,6 @@ EJABBERD_API_URL='http://xmpp.example.com/api'
BTCPAY_API_URL='http://btcpay.example.com/api/v1'
LNDHUB_LEGACY_API_URL='http://localhost:3023'
LNDHUB_API_URL='http://localhost:3026'
LNDHUB_PUBLIC_URL='https://lndhub.kosmos.org'
LNDHUB_PUBLIC_KEY='024cd3be18617f39cf645851e3ba63f51fc13f0bb09e3bb25e6fd4de556486d946'

View File

@ -39,6 +39,7 @@ gem 'net-ldap'
# Utilities
gem "rqrcode", "~> 2.0"
gem 'rails-settings-cached', '~> 2.8.3'
gem 'pagy', '~> 6.0', '>= 6.0.2'
# HTTP requests
gem 'faraday'

View File

@ -178,6 +178,7 @@ GEM
nokogiri (1.13.9-x86_64-linux)
racc (~> 1.4)
orm_adapter (0.5.0)
pagy (6.0.2)
pg (1.2.3)
public_suffix (5.0.0)
puma (4.3.12)
@ -327,6 +328,7 @@ DEPENDENCIES
listen (~> 3.2)
lockbox
net-ldap
pagy (~> 6.0, >= 6.0.2)
pg (~> 1.2.3)
puma (~> 4.1)
rails (~> 7.0.2)

View File

@ -19,7 +19,7 @@ so:
in the log output
4. `docker-compose exec ldap dsconf localhost backend create --suffix="dc=kosmos,dc=org" --be-name="dev"`
5. `docker compose run web rails ldap:setup`
5. `docker compose run web rails db:setup`
6. `docker compose run web rails db:setup`
After these steps, you should have a working Rails app with a handful of test
users running on [http://localhost:3000](http://localhost:3000).
@ -81,12 +81,15 @@ with a fresh installation, delete both that directory as well as the container.
## Documentation
### Rails
* [Ruby on Rails](https://guides.rubyonrails.org/)
* [Sass](https://sass-lang.com/documentation)
* [Pagination](https://ddnexus.github.io/pagy/)
### Front-end
* [Tailwind CSS](https://tailwindcss.com/)
* [Sass](https://sass-lang.com/documentation)
### Testing

View File

@ -4,7 +4,9 @@
@import "components/base";
@import "components/buttons";
@import "components/dashboard_services";
@import "components/forms";
@import "components/links";
@import "components/notifications";
@import "components/pagination";
@import "components/tables";

View File

@ -36,10 +36,18 @@
@apply mb-4 leading-6;
}
main p:last-child {
@apply mb-0;
}
main ul {
@apply mb-6;
}
main ul:last-child {
@apply mb-0;
}
main ul li {
@apply leading-6;
}

View File

@ -1,6 +1,6 @@
@layer components {
.btn {
@apply font-semibold rounded-md leading-none cursor-pointer text-center
@apply inline-block font-semibold rounded-md leading-none cursor-pointer text-center
transition-colors duration-75 focus:outline-none focus:ring-4;
}

View File

@ -0,0 +1,5 @@
@layer components {
.services > div > a {
background-image: linear-gradient(110deg, rgba(255,255,255,0.99) 0, rgba(255,255,255,0.88) 100%);
}
}

View File

@ -0,0 +1,45 @@
@layer components {
.pagy-nav.pagination {
@apply isolate inline-flex -space-x-px rounded-md shadow-sm;
}
.pagy-nav .page:not(.prev):not(.next) {
@apply hidden sm:inline-block;
}
.pagy-nav .page.next a {
@apply relative inline-flex items-center rounded-r-md border
border-gray-300 bg-white px-3 py-2 text-sm font-medium
text-gray-500 hover:bg-gray-100 focus:z-20;
}
.pagy-nav .page.prev a {
@apply relative inline-flex items-center rounded-l-md border
border-gray-300 bg-white px-3 py-2 text-sm font-medium
text-gray-500 hover:bg-gray-100 focus:z-20;
}
.pagy-nav .page.next.disabled {
@apply relative inline-flex items-center rounded-r-md border
border-gray-300 bg-gray-100 px-3 py-2 text-sm font-medium
text-gray-400 focus:z-20;
}
.pagy-nav .page.prev.disabled {
@apply relative inline-flex items-center rounded-l-md border
border-gray-300 bg-gray-100 px-3 py-2 text-sm font-medium
text-gray-400 focus:z-20;
}
.pagy-nav .page a, .page.gap {
@apply bg-white border-gray-300 text-gray-500 hover:bg-gray-100 relative
inline-flex items-center border px-4 py-2 text-sm font-medium
focus:z-20;
}
.pagy-nav .page.active {
@apply z-10 border-indigo-500 bg-indigo-50 text-indigo-600 relative
inline-flex items-center border px-4 py-2 text-sm font-medium
focus:z-20;
}
}

View File

@ -7,16 +7,30 @@
@apply text-left;
}
table th {
table thead th {
@apply pb-3.5 text-sm font-normal uppercase text-gray-500;
}
table tbody th {
@apply text-left font-normal text-gray-500;
}
table th:not(:last-of-type),
table td:not(:last-of-type) {
@apply pr-2;
}
table td {
table td, tbody th {
@apply py-2;
}
table.divided {
@apply divide-y divide-gray-300;
}
table.divided tbody {
@apply divide-y divide-gray-200;
}
table.divided td, table.divided tbody th {
@apply py-3;
}
}

View File

@ -1,4 +1,5 @@
class Admin::BaseController < ApplicationController
include Pagy::Backend
before_action :authenticate_user!
before_action :authorize_admin
@ -7,5 +8,4 @@ class Admin::BaseController < ApplicationController
def set_context
@context = :admin
end
end

View File

@ -5,7 +5,8 @@ class Admin::DonationsController < Admin::BaseController
# GET /donations
# GET /donations.json
def index
@donations = Donation.all.order('created_at desc')
@pagy, @donations = pagy(Donation.all.order('created_at desc'))
@stats = {
overall_sats: @donations.all.sum("amount_sats"),
donor_count: Donation.distinct.count(:user_id)

View File

@ -1,7 +1,8 @@
class Admin::InvitationsController < Admin::BaseController
def index
@current_section = :invitations
@invitations_used = Invitation.used.order('used_at desc')
@pagy, @invitations_used = pagy(Invitation.used.order('used_at desc'))
@stats = {
available: Invitation.unused.count,
accepted: @invitations_used.length,

View File

@ -1,20 +0,0 @@
class Admin::LdapUsersController < Admin::BaseController
before_action :set_current_section
def index
ldap = LdapService.new
@ou = params[:ou] || "kosmos.org"
@orgs = ldap.fetch_organizations
@entries = ldap.fetch_users(ou: @ou)
@stats = {
users_confirmed: User.where(ou: @ou).confirmed.count,
users_pending: User.where(ou: @ou).pending.count
}
end
private
def set_current_section
@current_section = :ldap_users
end
end

View File

@ -0,0 +1,35 @@
class Admin::UsersController < Admin::BaseController
before_action :set_user, only: [:show]
before_action :set_current_section
def index
ldap = LdapService.new
@ou = params[:ou] || "kosmos.org"
@orgs = ldap.fetch_organizations
@pagy, @users = pagy(User.where(ou: @ou).order(cn: :asc))
@stats = {
users_confirmed: User.where(ou: @ou).confirmed.count,
users_pending: User.where(ou: @ou).pending.count
}
end
def show
if Setting.lndhub_admin_enabled?
@lndhub_user = @user.lndhub_user
end
@services_enabled = @user.services_enabled
end
private
def set_user
address = params[:address].split("@")
@user = User.where(cn: address.first, ou: address.last).first
end
def set_current_section
@current_section = :users
end
end

View File

@ -0,0 +1,17 @@
# frozen_string_literal: true
class Users::ConfirmationsController < Devise::ConfirmationsController
# GET /resource/confirmation?confirmation_token=abcdef
def show
self.resource = resource_class.confirm_by_token(params[:confirmation_token])
yield resource if block_given?
if resource.errors.empty?
set_flash_message!(:success, :confirmed)
resource.devise_after_confirmation
respond_with_navigational(resource){ redirect_to after_confirmation_path_for(resource_name, resource) }
else
respond_with_navigational(resource.errors, status: :unprocessable_entity){ render :new }
end
end
end

View File

@ -7,7 +7,7 @@ class WalletController < ApplicationController
before_action :fetch_balance
def index
@wallet_url = "lndhub://#{current_user.ln_login}:#{current_user.ln_password}@#{ENV['LNDHUB_PUBLIC_URL']}"
@wallet_url = "lndhub://#{current_user.ln_account}:#{current_user.ln_password}@#{ENV['LNDHUB_PUBLIC_URL']}"
qrcode = RQRCode::QRCode.new(@wallet_url)
@svg = qrcode.as_svg(

View File

@ -1,4 +1,6 @@
module ApplicationHelper
include Pagy::Frontend
def sats_to_btc(sats)
sats.to_f / 100000000
end
@ -10,5 +12,10 @@ module ApplicationHelper
"text-gray-300 hover:bg-gray-900/30 hover:text-white active:bg-gray-900/30 active:text-white px-3 py-2 rounded-md font-medium text-base md:text-sm block md:inline-block"
end
end
end
# Colors available: gray, red, yellow, green, blue, purple, pink
# (Add more colors by adding classes to the safelist in tailwind.config.js)
def badge(text, color)
tag.span text, class: "inline-flex items-center rounded-full bg-#{color}-100 px-2.5 py-0.5 text-xs font-medium text-#{color}-800"
end
end

View File

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

View File

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

View File

@ -2,13 +2,12 @@ class CreateLndhubAccountJob < ApplicationJob
queue_as :default
def perform(user)
return if user.ln_login.present? && user.ln_password.present?
return if user.ln_account.present? && user.ln_password.present?
lndhub = LndhubV2.new
credentials = lndhub.create_account
user.update! ln_account: credentials["login"],
ln_login: credentials["login"], # TODO remove when production is migrated
ln_password: credentials["password"]
end
end

View File

@ -1,6 +1,7 @@
class Invitation < ApplicationRecord
# Relations
belongs_to :user
belongs_to :invitee, class_name: "User", foreign_key: 'invited_user_id', optional: true
# Validations
validates_presence_of :user

View File

@ -10,6 +10,18 @@ class LndhubUser < LndhubBase
foreign_key: "login"
def balance
accounts.current.first.ledgers.sum("account_ledgers.amount")
accounts.current.first.ledgers.sum("account_ledgers.amount").to_i.abs
end
def sum_outgoing
accounts.outgoing.first.ledgers.sum("account_ledgers.amount").to_i.abs
end
def sum_incoming
accounts.incoming.first.ledgers.sum("account_ledgers.amount").to_i.abs
end
def sum_fees
accounts.fees.first.ledgers.sum("account_ledgers.amount").to_i.abs
end
end

View File

@ -7,7 +7,7 @@ class Setting < RailsSettings::Base
#
field :reserved_usernames, type: :array, default: %w[
account accounts admin donations mail webmaster support
account accounts donations mail webmaster support
]
#

View File

@ -3,6 +3,10 @@ class User < ApplicationRecord
# Relations
has_many :invitations, dependent: :destroy
has_one :invitation, inverse_of: :invitee, foreign_key: 'invited_user_id'
has_one :inviter, through: :invitation, source: :user
has_many :invitees, through: :invitations
has_many :donations, dependent: :nullify
has_one :lndhub_user, class_name: "LndhubUser", inverse_of: "user",
@ -27,8 +31,7 @@ class User < ApplicationRecord
scope :confirmed, -> { where.not(confirmed_at: nil) }
scope :pending, -> { where(confirmed_at: nil) }
lockbox_encrypts :ln_login
lockbox_encrypts :ln_password
has_encrypted :ln_login, :ln_password
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
@ -39,9 +42,9 @@ class User < ApplicationRecord
def ldap_before_save
self.email = Devise::LDAP::Adapter.get_ldap_param(self.cn, "mail").first
dn = Devise::LDAP::Adapter.get_ldap_param(self.cn, "dn")
self.ou = dn.split(',').select{|e| e[0..1] == "ou"}.first.delete_prefix("ou=")
self.ou = dn.split(',')
.select{|e| e[0..1] == "ou"}.first
.delete_prefix("ou=")
if self.confirmed_at.blank? && self.confirmation_token.blank?
# User had an account with a trusted email address before akkounts was a thing
@ -49,6 +52,10 @@ class User < ApplicationRecord
end
end
def devise_after_confirmation
enable_service %w[discourse gitea wiki xmpp]
end
def reset_password(new_password, new_password_confirmation)
self.password = new_password
self.password_confirmation = new_password_confirmation
@ -81,4 +88,42 @@ class User < ApplicationRecord
lndhub.authenticate self
lndhub.addinvoice payload
end
def dn
return @dn if defined?(@dn)
@dn = Devise::LDAP::Adapter.get_dn(self.cn)
end
def ldap_entry
ldap.fetch_users(uid: self.cn, ou: self.ou).first
end
def services_enabled
ldap_entry[:service] || []
end
def enable_service(service)
current_services = services_enabled
new_services = Array(service).map(&:to_s)
services = (current_services + new_services).uniq
ldap.replace_attribute(dn, :service, services)
end
def disable_service(service)
current_services = services_enabled
disabled_services = Array(service).map(&:to_s)
services = (current_services - disabled_services).uniq
ldap.replace_attribute(dn, :service, services)
end
def disable_all_services
ldap.delete_attribute(dn,:service)
end
private
def ldap
return @ldap_service if defined?(@ldap_service)
@ldap_service = LdapService.new
end
end

View File

@ -3,6 +3,18 @@ class LdapService < ApplicationService
@suffix = ENV["LDAP_SUFFIX"] || "dc=kosmos,dc=org"
end
def add_attribute(dn, attr, values)
ldap_client.add_attribute dn, attr, values
end
def replace_attribute(dn, attr, values)
ldap_client.replace_attribute dn, attr, values
end
def delete_attribute(dn, attr)
ldap_client.delete_attribute dn, attr
end
def add_entry(dn, attrs, interactive=false)
puts "Adding entry: #{dn}" if interactive
res = ldap_client.add dn: dn, attributes: attrs
@ -10,10 +22,6 @@ class LdapService < ApplicationService
res
end
def add_attribute(dn, attr, value)
ldap_client.add_attribute dn, attr, value
end
def delete_entry(dn, interactive=false)
puts "Deleting entry: #{dn}" if interactive
res = ldap_client.delete dn: dn
@ -42,18 +50,17 @@ class LdapService < ApplicationService
treebase = ldap_config["base"]
end
attributes = %w{dn cn uid mail admin}
filter = Net::LDAP::Filter.eq("uid", "*")
attributes = %w{dn cn uid mail admin service}
filter = Net::LDAP::Filter.eq("uid", args[:uid] || "*")
entries = ldap_client.search(base: treebase, filter: filter, attributes: attributes)
entries.sort_by! { |e| e.cn[0] }
entries = entries.collect do |e|
{
uid: e.uid.first,
mail: e.try(:mail) ? e.mail.first : nil,
admin: e.try(:admin) ? 'admin' : nil
# password: e.userpassword.first
admin: e.try(:admin) ? 'admin' : nil,
service: e.try(:service)
}
end
end
@ -131,5 +138,4 @@ class LdapService < ApplicationService
def ldap_config
ldap_config ||= YAML.load(ERB.new(File.read("#{Rails.root}/config/ldap.yml")).result)[Rails.env]
end
end

View File

@ -2,7 +2,7 @@ class Lndhub
attr_accessor :auth_token
def initialize
@base_url = ENV["LNDHUB_LEGACY_API_URL"]
@base_url = ENV["LNDHUB_API_URL"]
end
def post(endpoint, payload)
@ -42,7 +42,7 @@ class Lndhub
end
def authenticate(user)
credentials = post "auth?type=auth", { login: user.ln_login, password: user.ln_password }
credentials = post "auth?type=auth", { login: user.ln_account, password: user.ln_password }
self.auth_token = credentials["access_token"]
self.auth_token
end

View File

@ -39,7 +39,7 @@ class LndhubV2
end
def authenticate(user)
credentials = post "auth?type=auth", { login: user.ln_login, password: user.ln_password }
credentials = post "auth?type=auth", { login: user.ln_account, password: user.ln_password }
self.auth_token = credentials["access_token"]
self.auth_token
end

View File

@ -10,46 +10,24 @@
</div>
<% end %>
<div class="field">
<p>
<div class="sm:w-1/2 grid grid-cols-2 items-center gap-y-2">
<%= form.label :user_id %>
<%= form.collection_select :user_id, User.where(ou: "kosmos.org").order(:cn), :id, :cn %>
</p>
</div>
<%= form.collection_select :user_id, User.where(ou: "kosmos.org").order(:cn), :id, :cn, {} %>
<div class="field">
<p>
<%= form.label :amount_sats, "Amount BTC (sats)" %>
<%= form.number_field :amount_sats %>
</p>
</div>
<div class="field">
<p>
<%= form.label :amount_eur, "Amount EUR (cents)" %>
<%= form.number_field :amount_eur %>
</p>
</div>
<div class="field">
<p>
<%= form.label :amount_usd, "Amount USD (cents)"%>
<%= form.number_field :amount_usd %>
</p>
</div>
<div class="field">
<p>
<%= form.label :public_name %>
<%= form.text_field :public_name %>
</p>
</div>
<div class="field">
<p>
<%= form.label :paid_at %>
<%= form.text_field :paid_at %>
</p>
</div>
<p class="mt-8">

View File

@ -1,12 +1,9 @@
<%= render HeaderComponent.new(title: "Donations") %>
<%= render HeaderComponent.new(title: "Donation ##{@donation.id}") %>
<%= render MainSimpleComponent.new do %>
<h2>Editing Donation</h2>
<%= render 'form', donation: @donation, url: admin_donation_path(@donation) %>
<p class="mt-8">
<%= link_to 'Show', admin_donation_path(@donation), class: 'ks-text-link' %> |
<%= link_to 'Back', admin_donations_path, class: 'ks-text-link' %>
<%= link_to 'Cancel', admin_donation_path(@donation), class: 'btn-sm btn-gray' %>
<p>
<% end %>

View File

@ -21,7 +21,7 @@
<section>
<% if @donations.any? %>
<h3>Recent Donations</h3>
<table>
<table class="divided mb-8">
<thead>
<tr>
<th>User</th>
@ -33,11 +33,10 @@
<th></th>
</tr>
</thead>
<tbody>
<% @donations.each do |donation| %>
<tr>
<td><%= donation.user.address %></td>
<td><%= link_to donation.user.address, admin_user_path(donation.user.address), class: 'ks-text-link' %></td>
<td class="text-right"><%= sats_to_btc donation.amount_sats %></td>
<td class="text-right"><% if donation.amount_eur.present? %><%= number_to_currency donation.amount_eur / 100, unit: "" %><% end %></td>
<td class="text-right"><% if donation.amount_usd.present? %><%= number_to_currency donation.amount_usd / 100, unit: "" %><% end %></td>
@ -53,6 +52,7 @@
<% end %>
</tbody>
</table>
<%== pagy_nav @pagy %>
<% else %>
<p>
No donations yet.

View File

@ -1,8 +1,6 @@
<%= render HeaderComponent.new(title: "Donations") %>
<%= render HeaderComponent.new(title: "Add Donation") %>
<%= render MainSimpleComponent.new do %>
<h2>New Donation</h2>
<%= render 'form', donation: @donation, url: admin_donations_path %>
<p class="mt-8">

View File

@ -1,38 +1,41 @@
<%= render HeaderComponent.new(title: "Donations") %>
<%= render HeaderComponent.new(title: "Donation ##{@donation.id}") %>
<%= render MainSimpleComponent.new do %>
<p>
<strong>User:</strong>
<%= @donation.user.address %>
</p>
<section>
<table class="w-1/2 divided">
<tbody>
<tr>
<th>User</th>
<td><%= link_to @donation.user.address, admin_user_path(@donation.user.address), class: 'ks-text-link' %></td>
</tr>
<tr>
<th>Amount sats</th>
<td><%= @donation.amount_sats %></td>
</tr>
<tr>
<th>Amount EUR</th>
<td><%= @donation.amount_eur %></td>
</tr>
<tr>
<th>Amount USD</th>
<td><%= @donation.amount_usd %></td>
</tr>
<tr>
<th>Public name</th>
<td><%= @donation.public_name %></td>
</tr>
<tr>
<th>Date</th>
<td><%= @donation.paid_at.strftime("%Y-%m-%d (%H:%M UTC)") %></td>
</tr>
</tbody>
</table>
</section>
<section>
<p>
<strong>Amount sats:</strong>
<%= @donation.amount_sats %>
</p>
<p>
<strong>Amount eur:</strong>
<%= @donation.amount_eur %>
</p>
<p>
<strong>Amount usd:</strong>
<%= @donation.amount_usd %>
</p>
<p>
<strong>Public name:</strong>
<%= @donation.public_name %>
</p>
<p>
<strong>Date:</strong>
<%= @donation.paid_at %>
</p>
<p class="mt-8">
<%= link_to 'Edit', edit_admin_donation_path(@donation), class: 'ks-text-link' %> |
<%= link_to 'Back', admin_donations_path, class: 'ks-text-link' %>
<%= link_to 'Edit', edit_admin_donation_path(@donation), class: 'btn-md btn-blue mr-1' %>
<%= link_to 'Back', admin_donations_path, class: 'btn-md btn-gray' %>
</p>
</section>
<% end %>

View File

@ -24,7 +24,7 @@
<% if @invitations_used.any? %>
<section>
<h3>Recently Accepted</h3>
<table>
<table class="divided mb-8">
<thead>
<tr>
<th>Token</th>
@ -38,12 +38,13 @@
<tr>
<td class="overflow-ellipsis font-mono"><%= invitation.token %></td>
<td><%= invitation.used_at.strftime("%Y-%m-%d (%H:%M UTC)") %></td>
<td><%= invitation.user.address %></td>
<td><%= User.find(invitation.invited_user_id).address %></td>
<td><%= link_to invitation.user.address, admin_user_path(invitation.user.address), class: "ks-text-link" %></td>
<td><%= link_to invitation.invitee.address, admin_user_path(invitation.invitee.address), class: "ks-text-link" %></td>
</tr>
<% end %>
</tbody>
</table>
<%== pagy_nav @pagy %>
</section>
<% end %>
<% end %>

View File

@ -20,7 +20,7 @@
<section>
<h3>Accounts</h3>
<table>
<table class="divided">
<thead>
<tr>
<th>LN Account</th>
@ -36,7 +36,7 @@
</td>
<td>
<% if user = @users.find{ |u| u[2] == account.login } %>
<%= "#{user[0]}@#{user[1]}" %>
<%= link_to "#{user[0]}@#{user[1]}", admin_user_path("#{user[0]}@#{user[1]}"), class: "ks-text-link" %>
<% end %>
</td>
<td><%= number_with_delimiter account.balance.to_i.to_s %></td>

View File

@ -22,14 +22,14 @@
<%= f.text_area :reserved_usernames,
value: Setting.reserved_usernames.join("\n"),
class: "h-44 mb-2" %>
<p class="mb-0 text-sm text-gray-500">
<p class="text-sm text-gray-500">
One username per line
</p>
</label>
</section>
<section>
<p class="mb-0 pt-6 border-t border-gray-200">
<p class="pt-6 border-t border-gray-200">
<%= f.submit 'Save', class: "btn-md btn-blue w-full md:w-auto" %>
</p>
</section>

View File

@ -18,7 +18,7 @@
<li class="flex items-center justify-between py-6">
<div class="flex flex-col">
<label class="font-bold mb-1">Enable LNDHub integration</label>
<p class="text-gray-500 mb-0">LNDHub configuration present and wallet features enabled</p>
<p class="text-gray-500">LNDHub configuration present and wallet features enabled</p>
</div>
<%= f.check_box :lndhub_enabled, checked: Setting.lndhub_enabled?,
disabled: true,
@ -27,7 +27,7 @@
<li class="flex items-center justify-between py-6">
<div class="flex flex-col">
<label class="font-bold mb-1">Enable LNDHub admin panel</label>
<p class="text-gray-500 mb-0">LNDHub database configuration present and admin panel enabled</p>
<p class="text-gray-500">LNDHub database configuration present and admin panel enabled</p>
</div>
<%= f.check_box :lndhub_admin_enabled, checked: Setting.lndhub_admin_enabled?,
disabled: true,

View File

@ -1,4 +1,4 @@
<%= render HeaderComponent.new(title: "LDAP Users: #{@ou}") %>
<%= render HeaderComponent.new(title: "Users: #{@ou}") %>
<%= render MainSimpleComponent.new do %>
<section>
@ -22,7 +22,7 @@
<ul>
<% @orgs.each do |org| %>
<li class="inline-block">
<%= link_to org[:ou], admin_ldap_users_path(ou: org[:ou]), class: "ks-text-link" %>
<%= link_to org[:ou], admin_users_path(ou: org[:ou]), class: "ks-text-link" %>
</li>
<% end %>
</ul>
@ -30,25 +30,25 @@
<% end %>
<section>
<table>
<table class="divided mb-8">
<thead>
<tr>
<th>UID</th>
<th>E-Mail</th>
<th>Admin</th>
<th>Status</th>
<th>Roles</th>
<!-- <th>Password</th> -->
</tr>
</thead>
<tbody>
<% @entries.each do |entry| %>
<% @users.each do |user| %>
<tr>
<td><%= entry[:uid] %></td>
<td><%= entry[:mail] %></td>
<td><%= entry[:admin] %></td>
<!-- <td><%= entry[:password] %></td> -->
<td><%= link_to(user.cn, admin_user_path(user.address), class: 'ks-text-link') %></td>
<td><%= user.confirmed_at.nil? ? badge("pending", :yellow) : "" %></td>
<td><%= user.is_admin? ? badge("admin", :red) : "" %></td>
</tr>
<% end %>
</tbody>
</table>
<%== pagy_nav @pagy %>
</section>
<% end %>

View File

@ -0,0 +1,123 @@
<%= render HeaderComponent.new(title: "User: #{@user.address}") %>
<%= render MainSimpleComponent.new do %>
<div class="mb-12 sm:flex sm:flex-row sm:gap-x-8">
<section class="sm:flex-1">
<h3>Account</h3>
<table class="divided">
<tbody>
<tr>
<th>Created at</th>
<td><%= @user.created_at.strftime("%Y-%m-%d (%H:%M UTC)") %></td>
</tr>
<tr>
<th>Confirmed at</th>
<td>
<% if @user.confirmed_at %>
<%= @user.confirmed_at.strftime("%Y-%m-%d (%H:%M UTC)") %>
<% else %>
<%= badge "pending", :yellow %>
<% end %>
</td>
</tr>
<tr>
<th>Email</th>
<td><%= @user.email %></td>
</tr>
<tr>
<th>Roles</th>
<td><%= @user.is_admin? ? badge("admin", :red) : "—" %></td>
</tr>
<tr>
<th>Invited by</th>
<td>
<% if @user.inviter %>
<%= link_to @user.inviter.address, admin_user_path(@user.inviter.address), class: 'ks-text-link' %>
<% else %>&mdash;<% end %>
</td>
</tr>
<tr>
<th>Invitations available</th>
<td>
<%= @user.invitations.count %>
</td>
</tr>
<tr>
<th class="align-top">Invited users</th>
<td class="align-top">
<% if @user.invitees.length > 0 %>
<ul class="mb-0">
<% @user.invitees.order(cn: :asc).each do |invitee| %>
<li class="leading-none mb-2 last:mb-0"><%= link_to invitee.address, admin_user_path(invitee.address), class: 'ks-text-link' %></li>
<% end %>
</ul>
<% else %>&mdash;<% end %>
</td>
</tr>
</tbody>
</table>
</section>
<section class="sm:flex-1 sm:pt-0">
<!-- <h3>Actions</h3> -->
</section>
</div>
<section>
<h3>Services</h3>
<table class="sm:w-1/4">
<tbody>
<tr>
<td>Discourse</td>
<td><%= check_box_tag 'service_discourse', 'enabled', @services_enabled.include?("discourse"), disabled: true %></td>
</tr>
<tr>
<td>Gitea</td>
<td><%= check_box_tag 'service_gitea', 'enabled', @services_enabled.include?("gitea"), disabled: true %></td>
</tr>
<tr>
<td>Mastodon</td>
<td><%= check_box_tag 'service_mastodon', 'enabled', @services_enabled.include?("mastodon"), disabled: true %></td>
</tr>
<tr>
<td>Wiki</td>
<td><%= check_box_tag 'service_wiki', 'enabled', @services_enabled.include?("wiki"), disabled: true %></td>
</tr>
<tr>
<td>XMPP</td>
<td><%= check_box_tag 'service_xmpp', 'enabled', @services_enabled.include?("xmpp"), disabled: true %></td>
</tr>
</tbody>
</table>
</section>
<% if Setting.lndhub_admin_enabled? && @user.confirmed? %>
<section>
<h3>LndHub</h3>
<% if @lndhub_user %>
<table>
<thead>
<tr>
<th>Account</th>
<th>Balance</th>
<th>Incoming</th>
<th>Outgoing</th>
<th>Fees</th>
</tr>
</thead>
<tbody>
<tr>
<td><%= @user.ln_account %></td>
<td><%= number_with_delimiter @lndhub_user.balance %> sats</td>
<td><%= number_with_delimiter @lndhub_user.sum_incoming %> sats</td>
<td><%= number_with_delimiter @lndhub_user.sum_outgoing %> sats</td>
<td><%= number_with_delimiter @lndhub_user.sum_fees %> sats</td>
</tr>
</tbody>
</table>
<% else %>
<p>No LndHub user found for account <strong class="font-mono"><%= @user.ln_account %></strong>.
<% end %>
</section>
<% end %>
<% end %>

View File

@ -2,60 +2,87 @@
<%= render MainSimpleComponent.new do %>
<section>
<p>
<p class="mb-8">
Your Kosmos account and password currently give you access to these
services:
</p>
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-6 services mt-12">
<div>
<h3 class="mb-3.5">
<%= link_to "Chat", "https://wiki.kosmos.org/Services:Chat", class: "ks-text-link" %>
</h3>
<p class="text-gray-500">
Chat rooms and instant messaging (XMPP/Jabber)
<div class="services grid grid-cols-1 sm:grid-cols-2 gap-4 sm:gap-6">
<div class="border border-gray-300 rounded-md hover:border-gray-400
bg-cover bg-[center_top_-50px] bg-no-repeat
bg-[url(/img/logos/icon_xmpp.svg)]">
<%= link_to "https://wiki.kosmos.org/Services:Chat",
class: "block h-full px-6 py-6 rounded-md" do %>
<h3 class="mb-3.5">Chat</h3>
<p class="text-gray-600">
Federated chat rooms and instant messaging
</p>
<% end %>
</div>
<div>
<h3 class="mb-3.5">
<%= link_to "Discourse", "https://community.kosmos.org", class: "ks-text-link" %>
</h3>
<p class="text-gray-500">
<div class="border border-gray-300 rounded-md hover:border-gray-400
bg-[length:95%] bg-center bg-no-repeat
bg-[url(/img/logos/icon_discourse.svg)]">
<%= link_to "https://community.kosmos.org",
class: "block h-full px-6 py-6 rounded-md" do %>
<h3 class="mb-3.5">Discourse</h3>
<p class="text-gray-600">
Kosmos community forums and user support/help site
</p>
<% end %>
</div>
<div>
<h3 class="mb-3.5">
<%= render partial: "icons/zap", locals: { custom_class: "text-amber-500 h-4 w-4 inline" } %>
<%= link_to "Lightning Wallet", wallet_path, class: "ks-text-link" %>
</h3>
<p class="text-gray-500">
Send and receive sats over the Bitcoin Lightning Network
</p>
</div>
<div>
<h3 class="mb-3.5">
<%= link_to "Wiki", "https://wiki.kosmos.org", class: "ks-text-link" %>
</h3>
<p class="text-gray-500">
<div class="border border-gray-300 rounded-md hover:border-gray-400
bg-cover bg-[center_top_-20px] bg-no-repeat
bg-[url(/img/logos/icon_mediawiki.svg)]">
<%= link_to "https://wiki.kosmos.org",
class: "block h-full px-6 py-6 rounded-md" do %>
<h3 class="mb-3.5">Wiki</h3>
<p class="text-gray-600">
Kosmos documentation and knowledge base
</p>
<% end %>
</div>
<div>
<h3 class="mb-3.5">
<%= link_to "Gitea", "https://gitea.kosmos.org", class: "ks-text-link" %>
</h3>
<p class="text-gray-500">
<div class="border border-gray-300 rounded-md hover:border-gray-400
bg-cover bg-center sm:bg-[center_top_-140px] bg-no-repeat
bg-[url(/img/logos/icon_lightning.svg)]">
<%= link_to wallet_path,
class: "block h-full px-6 py-6 rounded-md" do %>
<h3 class="mb-3.5">Wallet</h3>
<p class="text-gray-600">
Send and receive sats over the Bitcoin Lightning Network
</p>
<% end %>
</div>
<div class="border border-gray-300 rounded-md hover:border-gray-400
bg-cover bg-center bg-no-repeat
bg-[url(/img/logos/icon_gitea.png)]">
<%= link_to "https://gitea.kosmos.org",
class: "block h-full px-6 py-6 rounded-md" do %>
<h3 class="mb-3.5">Gitea</h3>
<p class="text-gray-600">
Code hosting and collaboration for software projects
</p>
<% end %>
</div>
<div>
<h3 class="mb-3.5">
<%= link_to "Drone CI", "https://drone.kosmos.org", class: "ks-text-link" %>
</h3>
<p class="text-gray-500">
<div class="border border-gray-300 rounded-md hover:border-gray-400
bg-cover bg-[center_top_-70px] bg-no-repeat
bg-[url(/img/logos/icon_droneci.svg)]">
<%= link_to "https://drone.kosmos.org",
class: "block h-full px-6 py-6 rounded-md" do %>
<h3 class="mb-3.5">Drone CI</h3>
<p class="text-gray-600">
Continuous integration for software projects on Gitea
</p>
<% end %>
</div>
<!-- <div class="border border&#45;gray&#45;300 rounded&#45;md hover:border&#45;gray&#45;400 -->
<!-- bg&#45;[length:80%] bg&#45;[right_top_&#45;30px] bg&#45;no&#45;repeat -->
<!-- bg&#45;[url(/img/logos/icon_mastodon.svg)]"> -->
<!-- <%= link_to "https://kosmos.social", class: "block h&#45;full px&#45;6 py&#45;6 rounded&#45;md" do %> -->
<!-- <h3 class="mb&#45;3.5">Mastodon</h3> -->
<!-- <p class="text&#45;gray&#45;400"> -->
<!-- Your account on the Open Social Web -->
<!-- </p> -->
<!-- <% end %> -->
<!-- </div> -->
</div>
</section>
<% end %>

View File

@ -1,7 +1,7 @@
<%= link_to "Dashboard", admin_root_path,
class: main_nav_class(@current_section, :dashboard) %>
<%= link_to "Users", admin_ldap_users_path,
class: main_nav_class(@current_section, :ldap_users) %>
<%= link_to "Users", admin_users_path,
class: main_nav_class(@current_section, :users) %>
<%= link_to "Invitations", admin_invitations_path,
class: main_nav_class(@current_section, :invitations) %>
<%= link_to "Donations", admin_donations_path,

View File

@ -22,7 +22,7 @@ Bundler.require(*Rails.groups)
module Akkounts
class Application < Rails::Application
# Initialize configuration defaults for originally generated Rails version.
config.load_defaults 6.0
config.load_defaults 7.0
# Settings in config/environments/* take precedence over those specified here.
# Application configuration can go into files in config/initializers

250
config/initializers/pagy.rb Normal file
View File

@ -0,0 +1,250 @@
# frozen_string_literal: true
# Pagy initializer file (6.0.2)
# Customize only what you really need and notice that the core Pagy works also without any of the following lines.
# Should you just cherry pick part of this file, please maintain the require-order of the extras
# Pagy DEFAULT Variables
# See https://ddnexus.github.io/pagy/docs/api/pagy#variables
# All the Pagy::DEFAULT are set for all the Pagy instances but can be overridden per instance by just passing them to
# Pagy.new|Pagy::Countless.new|Pagy::Calendar::*.new or any of the #pagy* controller methods
# Instance variables
# See https://ddnexus.github.io/pagy/docs/api/pagy#instance-variables
# Pagy::DEFAULT[:page] = 1 # default
# Pagy::DEFAULT[:items] = 20 # default
# Pagy::DEFAULT[:outset] = 0 # default
# Other Variables
# See https://ddnexus.github.io/pagy/docs/api/pagy#other-variables
# Pagy::DEFAULT[:size] = [1,4,4,1] # default
# Pagy::DEFAULT[:page_param] = :page # default
# The :params can be also set as a lambda e.g ->(params){ params.exclude('useless').merge!('custom' => 'useful') }
# Pagy::DEFAULT[:params] = {} # default
# Pagy::DEFAULT[:fragment] = '#fragment' # example
# Pagy::DEFAULT[:link_extra] = 'data-remote="true"' # example
# Pagy::DEFAULT[:i18n_key] = 'pagy.item_name' # default
# Pagy::DEFAULT[:cycle] = true # example
# Pagy::DEFAULT[:request_path] = "/foo" # example
# Extras
# See https://ddnexus.github.io/pagy/categories/extra
# Backend Extras
# Arel extra: For better performance utilizing grouped ActiveRecord collections:
# See: https://ddnexus.github.io/pagy/docs/extras/arel
# require 'pagy/extras/arel'
# Array extra: Paginate arrays efficiently, avoiding expensive array-wrapping and without overriding
# See https://ddnexus.github.io/pagy/docs/extras/array
# require 'pagy/extras/array'
# Calendar extra: Add pagination filtering by calendar time unit (year, quarter, month, week, day)
# See https://ddnexus.github.io/pagy/docs/extras/calendar
# require 'pagy/extras/calendar'
# Default for each unit
# Pagy::Calendar::Year::DEFAULT[:order] = :asc # Time direction of pagination
# Pagy::Calendar::Year::DEFAULT[:format] = '%Y' # strftime format
#
# Pagy::Calendar::Quarter::DEFAULT[:order] = :asc # Time direction of pagination
# Pagy::Calendar::Quarter::DEFAULT[:format] = '%Y-Q%q' # strftime format
#
# Pagy::Calendar::Month::DEFAULT[:order] = :asc # Time direction of pagination
# Pagy::Calendar::Month::DEFAULT[:format] = '%Y-%m' # strftime format
#
# Pagy::Calendar::Week::DEFAULT[:order] = :asc # Time direction of pagination
# Pagy::Calendar::Week::DEFAULT[:format] = '%Y-%W' # strftime format
#
# Pagy::Calendar::Day::DEFAULT[:order] = :asc # Time direction of pagination
# Pagy::Calendar::Day::DEFAULT[:format] = '%Y-%m-%d' # strftime format
#
# Uncomment the following lines, if you need calendar localization without using the I18n extra
# module LocalizePagyCalendar
# def localize(time, opts)
# ::I18n.l(time, **opts)
# end
# end
# Pagy::Calendar.prepend LocalizePagyCalendar
# Countless extra: Paginate without any count, saving one query per rendering
# See https://ddnexus.github.io/pagy/docs/extras/countless
# require 'pagy/extras/countless'
# Pagy::DEFAULT[:countless_minimal] = false # default (eager loading)
# Elasticsearch Rails extra: Paginate `ElasticsearchRails::Results` objects
# See https://ddnexus.github.io/pagy/docs/extras/elasticsearch_rails
# Default :pagy_search method: change only if you use also
# the searchkick or meilisearch extra that defines the same
# Pagy::DEFAULT[:elasticsearch_rails_pagy_search] = :pagy_search
# Default original :search method called internally to do the actual search
# Pagy::DEFAULT[:elasticsearch_rails_search] = :search
# require 'pagy/extras/elasticsearch_rails'
# Headers extra: http response headers (and other helpers) useful for API pagination
# See http://ddnexus.github.io/pagy/extras/headers
# require 'pagy/extras/headers'
# Pagy::DEFAULT[:headers] = { page: 'Current-Page',
# items: 'Page-Items',
# count: 'Total-Count',
# pages: 'Total-Pages' } # default
# Meilisearch extra: Paginate `Meilisearch` result objects
# See https://ddnexus.github.io/pagy/docs/extras/meilisearch
# Default :pagy_search method: change only if you use also
# the elasticsearch_rails or searchkick extra that define the same method
# Pagy::DEFAULT[:meilisearch_pagy_search] = :pagy_search
# Default original :search method called internally to do the actual search
# Pagy::DEFAULT[:meilisearch_search] = :ms_search
# require 'pagy/extras/meilisearch'
# Metadata extra: Provides the pagination metadata to Javascript frameworks like Vue.js, react.js, etc.
# See https://ddnexus.github.io/pagy/docs/extras/metadata
# you must require the frontend helpers internal extra (BEFORE the metadata extra) ONLY if you need also the :sequels
# require 'pagy/extras/frontend_helpers'
# require 'pagy/extras/metadata'
# For performance reasons, you should explicitly set ONLY the metadata you use in the frontend
# Pagy::DEFAULT[:metadata] = %i[scaffold_url page prev next last] # example
# Searchkick extra: Paginate `Searchkick::Results` objects
# See https://ddnexus.github.io/pagy/docs/extras/searchkick
# Default :pagy_search method: change only if you use also
# the elasticsearch_rails or meilisearch extra that defines the same
# DEFAULT[:searchkick_pagy_search] = :pagy_search
# Default original :search method called internally to do the actual search
# Pagy::DEFAULT[:searchkick_search] = :search
# require 'pagy/extras/searchkick'
# uncomment if you are going to use Searchkick.pagy_search
# Searchkick.extend Pagy::Searchkick
# Frontend Extras
# Bootstrap extra: Add nav, nav_js and combo_nav_js helpers and templates for Bootstrap pagination
# See https://ddnexus.github.io/pagy/docs/extras/bootstrap
# require 'pagy/extras/bootstrap'
# Bulma extra: Add nav, nav_js and combo_nav_js helpers and templates for Bulma pagination
# See https://ddnexus.github.io/pagy/docs/extras/bulma
# require 'pagy/extras/bulma'
# Foundation extra: Add nav, nav_js and combo_nav_js helpers and templates for Foundation pagination
# See https://ddnexus.github.io/pagy/docs/extras/foundation
# require 'pagy/extras/foundation'
# Materialize extra: Add nav, nav_js and combo_nav_js helpers for Materialize pagination
# See https://ddnexus.github.io/pagy/docs/extras/materialize
# require 'pagy/extras/materialize'
# Navs extra: Add nav_js and combo_nav_js javascript helpers
# Notice: the other frontend extras add their own framework-styled versions,
# so require this extra only if you need the unstyled version
# See https://ddnexus.github.io/pagy/docs/extras/navs
# require 'pagy/extras/navs'
# Semantic extra: Add nav, nav_js and combo_nav_js helpers for Semantic UI pagination
# See https://ddnexus.github.io/pagy/docs/extras/semantic
# require 'pagy/extras/semantic'
# UIkit extra: Add nav helper and templates for UIkit pagination
# See https://ddnexus.github.io/pagy/docs/extras/uikit
# require 'pagy/extras/uikit'
# Multi size var used by the *_nav_js helpers
# See https://ddnexus.github.io/pagy/docs/extras/navs#steps
# Pagy::DEFAULT[:steps] = { 0 => [2,3,3,2], 540 => [3,5,5,3], 720 => [5,7,7,5] } # example
# Feature Extras
# Gearbox extra: Automatically change the number of items per page depending on the page number
# See https://ddnexus.github.io/pagy/docs/extras/gearbox
# require 'pagy/extras/gearbox'
# set to false only if you want to make :gearbox_extra an opt-in variable
# Pagy::DEFAULT[:gearbox_extra] = false # default true
# Pagy::DEFAULT[:gearbox_items] = [15, 30, 60, 100] # default
# Items extra: Allow the client to request a custom number of items per page with an optional selector UI
# See https://ddnexus.github.io/pagy/docs/extras/items
# require 'pagy/extras/items'
# set to false only if you want to make :items_extra an opt-in variable
# Pagy::DEFAULT[:items_extra] = false # default true
# Pagy::DEFAULT[:items_param] = :items # default
# Pagy::DEFAULT[:max_items] = 100 # default
# Overflow extra: Allow for easy handling of overflowing pages
# See https://ddnexus.github.io/pagy/docs/extras/overflow
# require 'pagy/extras/overflow'
# Pagy::DEFAULT[:overflow] = :empty_page # default (other options: :last_page and :exception)
# Support extra: Extra support for features like: incremental, infinite, auto-scroll pagination
# See https://ddnexus.github.io/pagy/docs/extras/support
# require 'pagy/extras/support'
# Trim extra: Remove the page=1 param from links
# See https://ddnexus.github.io/pagy/docs/extras/trim
# require 'pagy/extras/trim'
# set to false only if you want to make :trim_extra an opt-in variable
# Pagy::DEFAULT[:trim_extra] = false # default true
# Standalone extra: Use pagy in non Rack environment/gem
# See https://ddnexus.github.io/pagy/docs/extras/standalone
# require 'pagy/extras/standalone'
# Pagy::DEFAULT[:url] = 'http://www.example.com/subdir' # optional default
# Rails
# Enable the .js file required by the helpers that use javascript
# (pagy*_nav_js, pagy*_combo_nav_js, and pagy_items_selector_js)
# See https://ddnexus.github.io/pagy/docs/api/javascript
# With the asset pipeline
# Sprockets need to look into the pagy javascripts dir, so add it to the assets paths
# Rails.application.config.assets.paths << Pagy.root.join('javascripts')
# I18n
# Pagy internal I18n: ~18x faster using ~10x less memory than the i18n gem
# See https://ddnexus.github.io/pagy/docs/api/i18n
# Notice: No need to configure anything in this section if your app uses only "en"
# or if you use the i18n extra below
#
# Examples:
# load the "de" built-in locale:
# Pagy::I18n.load(locale: 'de')
#
# load the "de" locale defined in the custom file at :filepath:
# Pagy::I18n.load(locale: 'de', filepath: 'path/to/pagy-de.yml')
#
# load the "de", "en" and "es" built-in locales:
# (the first passed :locale will be used also as the default_locale)
# Pagy::I18n.load({ locale: 'de' },
# { locale: 'en' },
# { locale: 'es' })
#
# load the "en" built-in locale, a custom "es" locale,
# and a totally custom locale complete with a custom :pluralize proc:
# (the first passed :locale will be used also as the default_locale)
# Pagy::I18n.load({ locale: 'en' },
# { locale: 'es', filepath: 'path/to/pagy-es.yml' },
# { locale: 'xyz', # not built-in
# filepath: 'path/to/pagy-xyz.yml',
# pluralize: lambda{ |count| ... } )
# I18n extra: uses the standard i18n gem which is ~18x slower using ~10x more memory
# than the default pagy internal i18n (see above)
# See https://ddnexus.github.io/pagy/docs/extras/i18n
# require 'pagy/extras/i18n'
# Default i18n key
# Pagy::DEFAULT[:i18n_key] = 'pagy.item_name' # default
# When you are done setting your own default freeze it, so it will not get changed accidentally
Pagy::DEFAULT.freeze

View File

@ -1,7 +1,7 @@
require 'sidekiq/web'
Rails.application.routes.draw do
devise_for :users
devise_for :users, :controllers => { :confirmations => "users/confirmations" }
get 'welcome', to: 'welcome#index'
get 'check_your_email', to: 'welcome#check_your_email'
@ -43,7 +43,7 @@ Rails.application.routes.draw do
namespace :admin do
root to: 'dashboard#index'
get 'ldap_users', to: 'ldap_users#index'
resources 'users', param: 'address', only: ['index', 'show'], constraints: { address: /.*/ }
get 'invitations', to: 'invitations#index'
resources :donations
get 'lightning', to: 'lightning#index'

View File

@ -0,0 +1,5 @@
class RemoveLnLoginCiphertextFromUsers < ActiveRecord::Migration[7.0]
def change
remove_column :users, :ln_login_ciphertext
end
end

View File

@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[7.0].define(version: 2023_02_17_084310) do
ActiveRecord::Schema[7.0].define(version: 2023_02_23_115536) do
create_table "donations", force: :cascade do |t|
t.integer "user_id"
t.integer "amount_sats"

View File

@ -10,7 +10,7 @@ Sidekiq::Testing.inline! do
ldap.add_attribute "cn=admin,ou=kosmos.org,cn=users,dc=kosmos,dc=org", :admin, "true"
5.times do |n|
35.times do |n|
username = Faker::Name.unique.first_name.downcase
email = Faker::Internet.unique.email

View File

@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -1 104 106">
<path fill="#231f20" d="M51.87 0C23.71 0 0 22.83 0 51v52.81l51.86-.05c28.16 0 51-23.71 51-51.87S80 0 51.87 0Z"/>
<path fill="#fff9ae" d="M52.37 19.74a31.62 31.62 0 0 0-27.79 46.67l-5.72 18.4 20.54-4.64a31.61 31.61 0 1 0 13-60.43Z"/>
<path fill="#00aeef" d="M77.45 32.12a31.6 31.6 0 0 1-38.05 48l-20.54 4.7 20.91-2.47a31.6 31.6 0 0 0 37.68-50.23Z"/>
<path fill="#00a94f" d="M71.63 26.29A31.6 31.6 0 0 1 38.8 78l-19.94 6.82 20.54-4.65a31.6 31.6 0 0 0 32.23-53.88Z"/>
<path fill="#f15d22" d="M26.47 67.11a31.61 31.61 0 0 1 51-35 31.61 31.61 0 0 0-52.89 34.3l-5.72 18.4Z"/>
<path fill="#e31b23" d="M24.58 66.41a31.61 31.61 0 0 1 47.05-40.12 31.61 31.61 0 0 0-49 39.63l-3.76 18.9Z"/>
</svg>

After

Width:  |  Height:  |  Size: 761 B

View File

@ -0,0 +1,9 @@
<svg width="33" height="34" viewBox="0 0 33 34" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M32.8125 17.2932C32.8125 19.6071 32.3208 21.8488 31.4779 23.8735L25.6476 17.944C26.35 16.787 26.7012 15.4131 26.7012 13.9669C26.7715 9.84522 23.47 6.44662 19.3958 6.44662C17.9207 6.44662 16.586 6.88048 15.4621 7.60359L10.0533 2.10799C12.0904 1.16795 14.2679 0.661774 16.6562 0.661774C25.5773 0.661774 32.8125 8.10976 32.8125 17.2932ZM7.80543 3.40958L13.6357 9.41135C12.6523 10.7129 12.0904 12.3038 12.0904 14.0392C12.0904 18.2332 15.3918 21.6318 19.466 21.6318C21.1519 21.6318 22.6973 21.0534 23.9617 20.041L30.2134 26.4767C27.2632 30.9599 22.3461 33.9246 16.6562 33.9246C7.73519 33.9246 0.5 26.4767 0.5 17.2932C0.5 11.436 3.38003 6.37431 7.80543 3.40958ZM19.466 18.7394C22.2056 18.7394 24.3832 16.4978 24.3832 13.6777C24.3832 10.8576 22.2056 8.61594 19.466 8.61594C16.7265 8.61594 14.5489 10.8576 14.5489 13.6777C14.5489 16.4978 16.7265 18.7394 19.466 18.7394Z" fill="url(#paint0_linear)"/>
<defs>
<linearGradient id="paint0_linear" x1="6.88952" y1="1.73016" x2="27.8689" y2="33.2773" gradientUnits="userSpaceOnUse">
<stop stop-color="#73DFE7"/>
<stop offset="1" stop-color="#0095F7"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 24 24" fill="none" stroke="#fbbf24" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-zap"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"></polygon></svg>

After

Width:  |  Height:  |  Size: 279 B

View File

@ -0,0 +1,48 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="79"
height="79"
viewBox="0 0 79 75"
version="1.1"
id="svg7"
sodipodi:docname="icon_mastodon.svg"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs11" />
<sodipodi:namedview
id="namedview9"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="true"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
inkscape:zoom="10.329114"
inkscape:cx="34.465686"
inkscape:cy="39.548407"
inkscape:window-width="1920"
inkscape:window-height="1011"
inkscape:window-x="0"
inkscape:window-y="32"
inkscape:window-maximized="1"
inkscape:current-layer="svg7" />
<symbol
id="logo-symbol-icon">
<path
d="M74.7135 16.6043C73.6199 8.54587 66.5351 2.19527 58.1366 0.964691C56.7196 0.756754 51.351 0 38.9148 0H38.822C26.3824 0 23.7135 0.756754 22.2966 0.964691C14.1319 2.16118 6.67571 7.86752 4.86669 16.0214C3.99657 20.0369 3.90371 24.4888 4.06535 28.5726C4.29578 34.4289 4.34049 40.275 4.877 46.1075C5.24791 49.9817 5.89495 53.8251 6.81328 57.6088C8.53288 64.5968 15.4938 70.4122 22.3138 72.7848C29.6155 75.259 37.468 75.6697 44.9919 73.971C45.8196 73.7801 46.6381 73.5586 47.4475 73.3063C49.2737 72.7302 51.4164 72.086 52.9915 70.9542C53.0131 70.9384 53.0308 70.9178 53.0433 70.8942C53.0558 70.8706 53.0628 70.8445 53.0637 70.8179V65.1661C53.0634 65.1412 53.0574 65.1167 53.0462 65.0944C53.035 65.0721 53.0189 65.0525 52.9992 65.0371C52.9794 65.0218 52.9564 65.011 52.9318 65.0056C52.9073 65.0002 52.8819 65.0003 52.8574 65.0059C48.0369 66.1472 43.0971 66.7193 38.141 66.7103C29.6118 66.7103 27.3178 62.6981 26.6609 61.0278C26.1329 59.5842 25.7976 58.0784 25.6636 56.5486C25.6622 56.5229 25.667 56.4973 25.6775 56.4738C25.688 56.4502 25.7039 56.4295 25.724 56.4132C25.7441 56.397 25.7678 56.3856 25.7931 56.3801C25.8185 56.3746 25.8448 56.3751 25.8699 56.3816C30.6101 57.5151 35.4693 58.0873 40.3455 58.086C41.5183 58.086 42.6876 58.086 43.8604 58.0553C48.7647 57.919 53.9339 57.6701 58.7591 56.7361C58.8794 56.7123 58.9998 56.6918 59.103 56.6611C66.7139 55.2124 73.9569 50.665 74.6929 39.1501C74.7204 38.6967 74.7892 34.4016 74.7892 33.9312C74.7926 32.3325 75.3085 22.5901 74.7135 16.6043ZM62.9996 45.3371H54.9966V25.9069C54.9966 21.8163 53.277 19.7302 49.7793 19.7302C45.9343 19.7302 44.0083 22.1981 44.0083 27.0727V37.7082H36.0534V27.0727C36.0534 22.1981 34.124 19.7302 30.279 19.7302C26.8019 19.7302 25.0651 21.8163 25.0617 25.9069V45.3371H17.0656V25.3172C17.0656 21.2266 18.1191 17.9769 20.2262 15.568C22.3998 13.1648 25.2509 11.9308 28.7898 11.9308C32.8859 11.9308 35.9812 13.492 38.0447 16.6111L40.036 19.9245L42.0308 16.6111C44.0943 13.492 47.1896 11.9308 51.2788 11.9308C54.8143 11.9308 57.6654 13.1648 59.8459 15.568C61.9529 17.9746 63.0065 21.2243 63.0065 25.3172L62.9996 45.3371Z"
fill="currentColor"
id="path2"
style="fill:#563acc;fill-opacity:1" />
</symbol>
<use
xlink:href="#logo-symbol-icon"
style="color:#fff;fill:#563acc;fill-opacity:1"
id="use5" />
</svg>

After

Width:  |  Height:  |  Size: 3.4 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 10 KiB

View File

@ -0,0 +1,114 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 13.0.2, SVG Export Plug-In . SVG Version: 6.00 Build 14948) -->
<svg
version="1.1"
xml:space="preserve"
viewBox="0 0 174.98412 127.21901"
width="174.98412"
height="127.21901"
x="0px"
y="0px"
enable-background="new 0 0 200 200"
id="svg36"
sodipodi:docname="icon_xmpp.svg"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs
id="defs40" /><sodipodi:namedview
id="namedview38"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
inkscape:zoom="4.08"
inkscape:cx="86.519608"
inkscape:cy="84.681373"
inkscape:window-width="1920"
inkscape:window-height="1011"
inkscape:window-x="0"
inkscape:window-y="32"
inkscape:window-maximized="1"
inkscape:current-layer="svg36" />
<linearGradient
id="SVGID_right_"
y2="1.279e-13"
gradientUnits="userSpaceOnUse"
x2="-1073.2"
gradientTransform="translate(1183.188)"
y1="126.85"
x1="-1073.2">
<stop
stop-color="#1b3967"
offset=".011"
id="stop2" />
<stop
stop-color="#13b5ea"
offset=".467"
id="stop4" />
<stop
stop-color="#002b5c"
offset=".9945"
id="stop6" />
</linearGradient>
<linearGradient
id="SVGID_left_"
y2="1.279e-13"
gradientUnits="userSpaceOnUse"
x2="-1073.2"
gradientTransform="matrix(-1,0,0,1,-1008.204,-10e-4)"
y1="126.85"
x1="-1073.2">
<stop
stop-color="#1b3967"
offset=".011"
id="stop9" />
<stop
stop-color="#13b5ea"
offset=".467"
id="stop11" />
<stop
stop-color="#002b5c"
offset=".9945"
id="stop13" />
</linearGradient>
<path
d="m 138.38912,14.189001 c 0.077,1.313 -1.787,0.968 -1.787,2.293 0,38.551 -46.558,97.366009 -91.687985,108.730009 v 1.639 C 104.86713,121.33101 173.47412,59.051001 174.98412,0.001 l -36.599,14.189001 z"
style="fill:url(#SVGID_right_)"
id="path16" />
<path
d="m 120.25712,18.932001 c 0.076,1.313 0.12,2.63 0.12,3.957 0,38.551 -30.69898,90.497009 -75.826985,101.860009 v 1.639 c 59.044005,-2.79 105.809995,-63.024009 105.809995,-109.200009 0,-2.375 -0.128,-4.729 -0.371,-7.056 l -29.73,8.798 z"
style="fill:#e96d1f"
id="path18" />
<path
d="m 150.27512,9.583001 -7.61699,2.722 c 0.041,0.962 0.066,2.254 0.066,3.225 0,41.219 -37.271,98.204009 -87.271995,107.120009 -3.24501,1.088 -7.53801,2.077 -10.932,2.931 v 1.638 C 109.77413,121.65901 155.62013,55.353001 150.28012,9.579001 Z"
style="fill:#d9541e"
id="path20" />
<path
d="m 36.595,14.188001 c -0.077,1.313 1.787,0.968 1.787,2.293 0,38.551 46.558007,97.366009 91.68799,108.730009 v 1.639 C 70.117,121.33001 1.51,59.050001 0,0 l 36.599,14.189001 z"
style="fill:url(#SVGID_left_)"
id="path22" />
<path
d="m 54.727,18.931001 c -0.076,1.313 -0.12,2.63 -0.12,3.957 0,38.551 30.698995,90.497009 75.82699,101.860009 v 1.639 C 71.39,123.59701 24.624,63.363001 24.624,17.187001 c 0,-2.375 0.128,-4.729 0.371,-7.056 l 29.73,8.798 z"
style="fill:#a0ce67"
id="path24" />
<path
d="m 24.709,9.582001 7.617,2.722 c -0.041,0.962 -0.066,2.254 -0.066,3.225 0,41.219 37.271,98.204009 87.27199,107.120009 3.245,1.088 7.538,2.077 10.932,2.931 v 1.638 C 65.21,121.65801 19.364,55.352001 24.704,9.578001 Z"
style="fill:#439639"
id="path26" />
</svg>

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

@ -19,7 +19,7 @@ RSpec.describe CreateLndhubAccountJob, type: :job do
.with { |req| req.body == '{}' }
user.reload
expect(user.ln_login).to eq("abc123")
expect(user.ln_account).to eq("abc123")
expect(user.ln_password).to eq("def456")
end

View File

@ -1,7 +1,16 @@
require 'rails_helper'
RSpec.describe User, type: :model do
let(:user) { create :user }
let(:user) { create :user, cn: "philipp" }
let(:dn) { "cn=philipp,ou=kosmos.org,cn=users,dc=kosmos,dc=org" }
describe "#address" do
let(:user) { build :user, cn: "jimmy", ou: "kosmos.org" }
it "returns the user address" do
expect(user.address).to eq("jimmy@kosmos.org")
end
end
describe "#is_admin?" do
it "returns true when admin flag is set in LDAP" do
@ -21,11 +30,75 @@ RSpec.describe User, type: :model do
end
end
describe "#address" do
let(:user) { build :user, cn: "jimmy", ou: "kosmos.org" }
describe "#services_enabled" do
it "returns the entries from the LDAP service attribute" do
expect(user).to receive(:ldap_entry).and_return({
uid: user.cn, ou: user.ou, mail: user.email, admin: nil,
service: ["discourse", "gitea", "wiki", "xmpp"]
})
expect(user.services_enabled).to eq(["discourse", "gitea", "wiki", "xmpp"])
end
end
it "returns the user address" do
expect(user.address).to eq("jimmy@kosmos.org")
describe "#enable_service" do
before do
allow(user).to receive(:ldap_entry).and_return({
uid: user.cn, ou: user.ou, mail: user.email, admin: nil,
service: ["discourse", "gitea"]
})
allow(user).to receive(:dn).and_return(dn)
end
it "adds the service to the LDAP entry" do
expect_any_instance_of(LdapService).to receive(:replace_attribute)
.with(dn, :service, ["discourse", "gitea", "wiki"]).and_return(true)
user.enable_service(:wiki)
end
it "adds multiple service to the LDAP entry" do
expect_any_instance_of(LdapService).to receive(:replace_attribute)
.with(dn, :service, ["discourse", "gitea", "wiki", "xmpp"]).and_return(true)
user.enable_service([:wiki, :xmpp])
end
end
describe "#disable_service" do
before do
allow(user).to receive(:ldap_entry).and_return({
uid: user.cn, ou: user.ou, mail: user.email, admin: nil,
service: ["discourse", "gitea", "xmpp"]
})
allow(user).to receive(:dn).and_return(dn)
end
it "removes the service from the LDAP entry" do
expect_any_instance_of(LdapService).to receive(:replace_attribute)
.with(dn, :service, ["discourse", "gitea"]).and_return(true)
user.disable_service(:xmpp)
end
it "removes multiple services from the LDAP entry" do
expect_any_instance_of(LdapService).to receive(:replace_attribute)
.with(dn, :service, ["discourse"]).and_return(true)
user.disable_service([:xmpp, "gitea"])
end
end
describe "#disable_all_services" do
before do
allow(user).to receive(:dn).and_return(dn)
end
it "removes all services from the LDAP entry" do
expect_any_instance_of(LdapService).to receive(:delete_attribute)
.with(dn, :service).and_return(true)
user.disable_all_services
end
end
end

View File

@ -7,6 +7,15 @@ module.exports = {
'./app/helpers/**/*.rb',
'./app/javascript/**/*.js'
],
safelist: [
'bg-gray-100', 'text-gray-800',
'bg-red-100', 'text-red-800',
'bg-yellow-100', 'text-yellow-800',
'bg-green-100', 'text-green-800',
'bg-blue-100', 'text-blue-800',
'bg-purple-100', 'text-purple-800',
'bg-pink-100', 'text-pink-800',
],
theme: {
extend: {
fontFamily: {
@ -16,5 +25,5 @@ module.exports = {
},
plugins: [
require('@tailwindcss/forms')
],
]
}