1 Commits

Author SHA1 Message Date
Râu Cao
462dd24da3 WIP contribution nav
All checks were successful
continuous-integration/drone/push Build is passing
2023-06-12 14:32:59 +02:00
160 changed files with 778 additions and 5064 deletions

View File

@@ -12,16 +12,14 @@ steps:
settings: settings:
restore: true restore: true
mount: mount:
- ./vendor/cache - ./vendor
when: when:
branch: branch:
- master - master
- name: rspec - name: rspec
image: gitea.kosmos.org/kosmos/akkounts-ci:0.1.0 image: guildeducation/rails:2.7.2-14.20.0
environment: environment:
RAILS_ENV: test RAILS_ENV: test
REDIS_URL: redis://redis:6379/0
RS_REDIS_URL: redis://redis:6379/1
commands: commands:
- bundle config unset deployment - bundle config unset deployment
- bundle config set cache_all 'true' - bundle config set cache_all 'true'
@@ -39,15 +37,11 @@ steps:
settings: settings:
rebuild: true rebuild: true
mount: mount:
- ./vendor/cache - ./vendor
when: when:
branch: branch:
- master - master
services:
- name: redis
image: redis
volumes: volumes:
- name: cache - name: cache
host: host:

View File

@@ -1,4 +1,3 @@
PRIMARY_DOMAIN=kosmos.org
AKKOUNTS_DOMAIN=accounts.example.com AKKOUNTS_DOMAIN=accounts.example.com
SMTP_SERVER=smtp.example.com SMTP_SERVER=smtp.example.com
@@ -10,32 +9,27 @@ SMTP_DOMAIN=example.com
SMTP_AUTH_METHOD=plain SMTP_AUTH_METHOD=plain
SMTP_ENABLE_STARTTLS=auto SMTP_ENABLE_STARTTLS=auto
REDIS_URL='redis://localhost:6379/1'
LDAP_HOST=localhost LDAP_HOST=localhost
LDAP_PORT=389 LDAP_PORT=389
LDAP_ADMIN_PASSWORD=passthebutter LDAP_ADMIN_PASSWORD=passthebutter
LDAP_SUFFIX='dc=kosmos,dc=org' LDAP_SUFFIX='dc=kosmos,dc=org'
REDIS_URL='redis://localhost:6379/1'
WEBHOOKS_ALLOWED_IPS='10.1.1.163' WEBHOOKS_ALLOWED_IPS='10.1.1.163'
DISCOURSE_PUBLIC_URL='https://community.kosmos.org' DISCOURSE_PUBLIC_URL='https://community.kosmos.org'
DISCOURSE_CONNECT_SECRET='discourse_connect_ftw' DISCOURSE_CONNECT_SECRET='discourse_connect_ftw'
DRONECI_PUBLIC_URL='https://drone.kosmos.org'
GITEA_PUBLIC_URL='https://gitea.kosmos.org' GITEA_PUBLIC_URL='https://gitea.kosmos.org'
MASTODON_PUBLIC_URL='https://kosmos.social' MASTODON_PUBLIC_URL='https://kosmos.social'
MEDIAWIKI_PUBLIC_URL='https://wiki.kosmos.org' MEDIAWIKI_PUBLIC_URL='https://wiki.kosmos.org'
RS_STORAGE_URL='https://storage.kosmos.org' RS_STORAGE_URL='https://storage.kosmos.org'
RS_REDIS_URL='redis://localhost:6379/2'
EJABBERD_ADMIN_URL='https://xmpp.kosmos.org/admin' EJABBERD_ADMIN_URL='https://xmpp.kosmos.org/admin'
EJABBERD_API_URL='https://xmpp.kosmos.org/api' EJABBERD_API_URL='https://xmpp.kosmos.org/api'
BTCPAY_API_URL='http://localhost:23001/api/v1' BTCPAY_API_URL='http://localhost:23001/api/v1'
BTCPAY_STORE_ID=''
BTCPAY_AUTH_TOKEN=''
LNDHUB_API_URL='http://localhost:3023' LNDHUB_API_URL='http://localhost:3023'
LNDHUB_PUBLIC_URL='https://lndhub.kosmos.org' LNDHUB_PUBLIC_URL='https://lndhub.kosmos.org'

View File

@@ -1,20 +1,14 @@
PRIMARY_DOMAIN=kosmos.org
REDIS_URL='redis://localhost:6379/0'
BTCPAY_API_URL='http://btcpay.example.com/api/v1'
BTCPAY_STORE_ID='123456'
DISCOURSE_PUBLIC_URL='http://discourse.example.com' DISCOURSE_PUBLIC_URL='http://discourse.example.com'
DISCOURSE_CONNECT_SECRET='discourse_connect_ftw' DISCOURSE_CONNECT_SECRET='discourse_connect_ftw'
EJABBERD_API_URL='http://xmpp.example.com/api' EJABBERD_API_URL='http://xmpp.example.com/api'
BTCPAY_API_URL='http://btcpay.example.com/api/v1'
LNDHUB_API_URL='http://localhost:3026' LNDHUB_API_URL='http://localhost:3026'
LNDHUB_PUBLIC_URL='https://lndhub.kosmos.org' LNDHUB_PUBLIC_URL='https://lndhub.kosmos.org'
LNDHUB_PUBLIC_KEY='024cd3be18617f39cf645851e3ba63f51fc13f0bb09e3bb25e6fd4de556486d946' LNDHUB_PUBLIC_KEY='024cd3be18617f39cf645851e3ba63f51fc13f0bb09e3bb25e6fd4de556486d946'
RS_STORAGE_URL='https://storage.kosmos.org' RS_STORAGE_URL='https://storage.kosmos.org'
RS_REDIS_URL='redis://localhost:6379/1'
WEBHOOKS_ALLOWED_IPS='10.1.1.23' WEBHOOKS_ALLOWED_IPS='10.1.1.23'

View File

@@ -7,7 +7,6 @@ version-resolver:
minor: minor:
labels: labels:
- 'release/minor' - 'release/minor'
- 'feature'
patch: patch:
labels: labels:
- 'release/patch' - 'release/patch'

2
.gitignore vendored
View File

@@ -23,7 +23,6 @@
!/tmp/pids/ !/tmp/pids/
!/tmp/pids/.keep !/tmp/pids/.keep
/storage
/public/assets /public/assets
.byebug_history .byebug_history
@@ -40,7 +39,6 @@ yarn-debug.log*
# Ignore local dotenv config file # Ignore local dotenv config file
.env .env
.env.development
# Ignore redis dumps from sidekiq # Ignore redis dumps from sidekiq
dump.rdb dump.rdb

View File

@@ -4,14 +4,14 @@ FROM ruby:2.7.6
SHELL ["/bin/bash", "-o", "pipefail", "-c"] SHELL ["/bin/bash", "-o", "pipefail", "-c"]
RUN apt-get update -qq && apt-get install -y --no-install-recommends curl \ RUN apt-get update -qq && apt-get install -y --no-install-recommends curl \
ldap-utils tini libvips ldap-utils tini
RUN curl -fsSL https://deb.nodesource.com/setup_lts.x | bash - RUN curl -fsSL https://deb.nodesource.com/setup_lts.x | bash -
RUN apt-get update && apt-get install -y nodejs RUN apt-get update && apt-get install -y nodejs
WORKDIR /akkounts WORKDIR /akkounts
COPY Gemfile /akkounts/Gemfile
COPY ["Gemfile", "Gemfile.lock", "package.json", "./"] COPY Gemfile.lock /akkounts/Gemfile.lock
COPY package.json /akkounts/package.json
RUN bundle install RUN bundle install
RUN gem install foreman RUN gem install foreman
RUN npm install -g yarn RUN npm install -g yarn

10
Gemfile
View File

@@ -37,7 +37,6 @@ gem 'devise_ldap_authenticatable'
gem 'net-ldap' gem 'net-ldap'
# Utilities # Utilities
gem "image_processing", "~> 1.12.2"
gem "rqrcode", "~> 2.0" gem "rqrcode", "~> 2.0"
gem 'rails-settings-cached', '~> 2.8.3' gem 'rails-settings-cached', '~> 2.8.3'
gem 'pagy', '~> 6.0', '>= 6.0.2' gem 'pagy', '~> 6.0', '>= 6.0.2'
@@ -52,20 +51,17 @@ gem 'faraday'
gem 'sidekiq', '< 7' gem 'sidekiq', '< 7'
gem 'sidekiq-scheduler' gem 'sidekiq-scheduler'
# Service integrations
gem 'discourse_api'
# Monitoring # Monitoring
gem "sentry-ruby" gem "sentry-ruby"
gem "sentry-rails" gem "sentry-rails"
# Services
gem 'discourse_api'
gem "lnurl"
gem 'nostr', git: 'https://gitea.kosmos.org/kosmos/nostr-gem.git', branch: 'feature/ruby_2.7_compat'
group :development, :test do group :development, :test do
# Use sqlite3 as the database for Active Record # Use sqlite3 as the database for Active Record
gem 'sqlite3', '~> 1.4' gem 'sqlite3', '~> 1.4'
gem 'rspec-rails' gem 'rspec-rails'
gem 'rails-controller-testing'
gem "byebug", "~> 11.1" gem "byebug", "~> 11.1"
end end

View File

@@ -1,98 +1,81 @@
GIT
remote: https://gitea.kosmos.org/kosmos/nostr-gem.git
revision: 596529d9eb50d13b3f385245636698fccf37b442
branch: feature/ruby_2.7_compat
specs:
nostr (0.4.0)
bech32 (~> 1.3)
bip-schnorr (~> 0.4)
ecdsa (~> 1.2)
event_emitter (~> 0.2)
faye-websocket (~> 0.11)
json (~> 2.6)
GEM GEM
remote: https://rubygems.org/ remote: https://rubygems.org/
specs: specs:
actioncable (7.0.5) actioncable (7.0.4)
actionpack (= 7.0.5) actionpack (= 7.0.4)
activesupport (= 7.0.5) activesupport (= 7.0.4)
nio4r (~> 2.0) nio4r (~> 2.0)
websocket-driver (>= 0.6.1) websocket-driver (>= 0.6.1)
actionmailbox (7.0.5) actionmailbox (7.0.4)
actionpack (= 7.0.5) actionpack (= 7.0.4)
activejob (= 7.0.5) activejob (= 7.0.4)
activerecord (= 7.0.5) activerecord (= 7.0.4)
activestorage (= 7.0.5) activestorage (= 7.0.4)
activesupport (= 7.0.5) activesupport (= 7.0.4)
mail (>= 2.7.1) mail (>= 2.7.1)
net-imap net-imap
net-pop net-pop
net-smtp net-smtp
actionmailer (7.0.5) actionmailer (7.0.4)
actionpack (= 7.0.5) actionpack (= 7.0.4)
actionview (= 7.0.5) actionview (= 7.0.4)
activejob (= 7.0.5) activejob (= 7.0.4)
activesupport (= 7.0.5) activesupport (= 7.0.4)
mail (~> 2.5, >= 2.5.4) mail (~> 2.5, >= 2.5.4)
net-imap net-imap
net-pop net-pop
net-smtp net-smtp
rails-dom-testing (~> 2.0) rails-dom-testing (~> 2.0)
actionpack (7.0.5) actionpack (7.0.4)
actionview (= 7.0.5) actionview (= 7.0.4)
activesupport (= 7.0.5) activesupport (= 7.0.4)
rack (~> 2.0, >= 2.2.4) rack (~> 2.0, >= 2.2.0)
rack-test (>= 0.6.3) rack-test (>= 0.6.3)
rails-dom-testing (~> 2.0) rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.2.0) rails-html-sanitizer (~> 1.0, >= 1.2.0)
actiontext (7.0.5) actiontext (7.0.4)
actionpack (= 7.0.5) actionpack (= 7.0.4)
activerecord (= 7.0.5) activerecord (= 7.0.4)
activestorage (= 7.0.5) activestorage (= 7.0.4)
activesupport (= 7.0.5) activesupport (= 7.0.4)
globalid (>= 0.6.0) globalid (>= 0.6.0)
nokogiri (>= 1.8.5) nokogiri (>= 1.8.5)
actionview (7.0.5) actionview (7.0.4)
activesupport (= 7.0.5) activesupport (= 7.0.4)
builder (~> 3.1) builder (~> 3.1)
erubi (~> 1.4) erubi (~> 1.4)
rails-dom-testing (~> 2.0) rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.1, >= 1.2.0) rails-html-sanitizer (~> 1.1, >= 1.2.0)
activejob (7.0.5) activejob (7.0.4)
activesupport (= 7.0.5) activesupport (= 7.0.4)
globalid (>= 0.3.6) globalid (>= 0.3.6)
activemodel (7.0.5) activemodel (7.0.4)
activesupport (= 7.0.5) activesupport (= 7.0.4)
activerecord (7.0.5) activerecord (7.0.4)
activemodel (= 7.0.5) activemodel (= 7.0.4)
activesupport (= 7.0.5) activesupport (= 7.0.4)
activestorage (7.0.5) activestorage (7.0.4)
actionpack (= 7.0.5) actionpack (= 7.0.4)
activejob (= 7.0.5) activejob (= 7.0.4)
activerecord (= 7.0.5) activerecord (= 7.0.4)
activesupport (= 7.0.5) activesupport (= 7.0.4)
marcel (~> 1.0) marcel (~> 1.0)
mini_mime (>= 1.1.0) mini_mime (>= 1.1.0)
activesupport (7.0.5) activesupport (7.0.4)
concurrent-ruby (~> 1.0, >= 1.0.2) concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 1.6, < 2) i18n (>= 1.6, < 2)
minitest (>= 5.1) minitest (>= 5.1)
tzinfo (~> 2.0) tzinfo (~> 2.0)
addressable (2.8.4) addressable (2.8.1)
public_suffix (>= 2.0.2, < 6.0) public_suffix (>= 2.0.2, < 6.0)
ast (2.4.2) ast (2.4.2)
backport (1.2.0) backport (1.2.0)
bcrypt (3.1.18) bcrypt (3.1.18)
bech32 (1.3.0)
thor (>= 1.1.0)
benchmark (0.2.1) benchmark (0.2.1)
bindex (0.8.1) bindex (0.8.1)
bip-schnorr (0.6.0)
ecdsa_ext (~> 0.5.0)
builder (3.2.4) builder (3.2.4)
byebug (11.1.3) byebug (11.1.3)
capybara (3.39.2) capybara (3.38.0)
addressable addressable
matrix matrix
mini_mime (>= 0.1.3) mini_mime (>= 0.1.3)
@@ -102,21 +85,20 @@ GEM
regexp_parser (>= 1.5, < 3.0) regexp_parser (>= 1.5, < 3.0)
xpath (~> 3.2) xpath (~> 3.2)
chunky_png (1.4.0) chunky_png (1.4.0)
concurrent-ruby (1.2.2) concurrent-ruby (1.1.10)
connection_pool (2.4.1) connection_pool (2.3.0)
crack (0.4.5) crack (0.4.5)
rexml rexml
crass (1.0.6) crass (1.0.6)
cssbundling-rails (1.1.2) cssbundling-rails (1.1.1)
railties (>= 6.0.0) railties (>= 6.0.0)
database_cleaner (2.0.2) database_cleaner (2.0.1)
database_cleaner-active_record (>= 2, < 3) database_cleaner-active_record (~> 2.0.0)
database_cleaner-active_record (2.1.0) database_cleaner-active_record (2.0.1)
activerecord (>= 5.a) activerecord (>= 5.a)
database_cleaner-core (~> 2.0.0) database_cleaner-core (~> 2.0.0)
database_cleaner-core (2.0.1) database_cleaner-core (2.0.1)
date (3.3.3) devise (4.9.0)
devise (4.9.2)
bcrypt (~> 3.0) bcrypt (~> 3.0)
orm_adapter (~> 0.1) orm_adapter (~> 0.1)
railties (>= 4.1.0) railties (>= 4.1.0)
@@ -126,7 +108,7 @@ GEM
devise (>= 3.4.1) devise (>= 3.4.1)
net-ldap (>= 0.16.0) net-ldap (>= 0.16.0)
diff-lcs (1.5.0) diff-lcs (1.5.0)
discourse_api (2.0.1) discourse_api (2.0.0)
faraday (~> 2.7) faraday (~> 2.7)
faraday-follow_redirects faraday-follow_redirects
faraday-multipart faraday-multipart
@@ -136,22 +118,17 @@ GEM
dotenv (= 2.8.1) dotenv (= 2.8.1)
railties (>= 3.2) railties (>= 3.2)
e2mmap (0.1.0) e2mmap (0.1.0)
ecdsa (1.2.0) erubi (1.11.0)
ecdsa_ext (0.5.0)
ecdsa (~> 1.2.0)
erubi (1.12.0)
et-orbi (1.2.7) et-orbi (1.2.7)
tzinfo tzinfo
event_emitter (0.2.6)
eventmachine (1.2.7)
factory_bot (6.2.1) factory_bot (6.2.1)
activesupport (>= 5.0.0) activesupport (>= 5.0.0)
factory_bot_rails (6.2.0) factory_bot_rails (6.2.0)
factory_bot (~> 6.2.0) factory_bot (~> 6.2.0)
railties (>= 5.0.0) railties (>= 5.0.0)
faker (3.2.0) faker (3.0.0)
i18n (>= 1.8.11, < 2) i18n (>= 1.8.11, < 2)
faraday (2.7.6) faraday (2.7.1)
faraday-net_http (>= 2.0, < 3.1) faraday-net_http (>= 2.0, < 3.1)
ruby2_keywords (>= 0.0.4) ruby2_keywords (>= 0.0.4)
faraday-follow_redirects (0.3.0) faraday-follow_redirects (0.3.0)
@@ -159,9 +136,6 @@ GEM
faraday-multipart (1.0.4) faraday-multipart (1.0.4)
multipart-post (~> 2) multipart-post (~> 2)
faraday-net_http (3.0.2) faraday-net_http (3.0.2)
faye-websocket (0.11.2)
eventmachine (>= 0.12.0)
websocket-driver (>= 0.5.1)
ffi (1.15.5) ffi (1.15.5)
flipper (0.28.0) flipper (0.28.0)
concurrent-ruby (< 2) concurrent-ruby (< 2)
@@ -174,21 +148,18 @@ GEM
rack (>= 1.4, < 3) rack (>= 1.4, < 3)
rack-protection (>= 1.5.3, <= 4.0.0) rack-protection (>= 1.5.3, <= 4.0.0)
sanitize (< 7) sanitize (< 7)
fugit (1.8.1) fugit (1.7.2)
et-orbi (~> 1, >= 1.2.7) et-orbi (~> 1, >= 1.2.7)
raabro (~> 1.4) raabro (~> 1.4)
globalid (1.1.0) globalid (1.0.0)
activesupport (>= 5.0) activesupport (>= 5.0)
hashdiff (1.0.1) hashdiff (1.0.1)
i18n (1.14.1) i18n (1.12.0)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
image_processing (1.12.2) importmap-rails (1.1.5)
mini_magick (>= 4.9.5, < 5)
ruby-vips (>= 2.0.17, < 3)
importmap-rails (1.1.6)
actionpack (>= 6.0.0) actionpack (>= 6.0.0)
railties (>= 6.0.0) railties (>= 6.0.0)
jaro_winkler (1.5.6) jaro_winkler (1.5.4)
jbuilder (2.11.5) jbuilder (2.11.5)
actionview (>= 5.0.0) actionview (>= 5.0.0)
activesupport (>= 5.0.0) activesupport (>= 5.0.0)
@@ -197,8 +168,8 @@ GEM
rexml rexml
kramdown-parser-gfm (1.1.0) kramdown-parser-gfm (1.1.0)
kramdown (~> 2.0) kramdown (~> 2.0)
launchy (2.5.2) launchy (2.5.0)
addressable (~> 2.8) addressable (~> 2.7)
letter_opener (1.8.1) letter_opener (1.8.1)
launchy (>= 2.2, < 3) launchy (>= 2.2, < 3)
letter_opener_web (2.0.0) letter_opener_web (2.0.0)
@@ -206,89 +177,78 @@ GEM
letter_opener (~> 1.7) letter_opener (~> 1.7)
railties (>= 5.2) railties (>= 5.2)
rexml rexml
listen (3.8.0) listen (3.7.1)
rb-fsevent (~> 0.10, >= 0.10.3) rb-fsevent (~> 0.10, >= 0.10.3)
rb-inotify (~> 0.9, >= 0.9.10) rb-inotify (~> 0.9, >= 0.9.10)
lnurl (1.0.1) lockbox (1.1.0)
bech32 (~> 1.1) loofah (2.19.0)
lockbox (1.2.0)
loofah (2.21.3)
crass (~> 1.0.2) crass (~> 1.0.2)
nokogiri (>= 1.12.0) nokogiri (>= 1.5.9)
mail (2.8.1) mail (2.7.1)
mini_mime (>= 0.1.1) mini_mime (>= 0.1.1)
net-imap
net-pop
net-smtp
marcel (1.0.2) marcel (1.0.2)
matrix (0.4.2) matrix (0.4.2)
method_source (1.0.0) method_source (1.0.0)
mini_magick (4.12.0)
mini_mime (1.1.2) mini_mime (1.1.2)
minitest (5.18.0) mini_portile2 (2.8.0)
minitest (5.16.3)
multipart-post (2.3.0) multipart-post (2.3.0)
net-imap (0.3.6) net-imap (0.3.1)
date
net-protocol net-protocol
net-ldap (0.18.0) net-ldap (0.17.1)
net-pop (0.1.2) net-pop (0.1.2)
net-protocol net-protocol
net-protocol (0.2.1) net-protocol (0.1.3)
timeout timeout
net-smtp (0.3.3) net-smtp (0.3.3)
net-protocol net-protocol
nio4r (2.5.9) nio4r (2.5.8)
nokogiri (1.15.2-arm64-darwin) nokogiri (1.13.9)
mini_portile2 (~> 2.8.0)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.15.2-x86_64-linux) nokogiri (1.13.9-x86_64-linux)
racc (~> 1.4) racc (~> 1.4)
orm_adapter (0.5.0) orm_adapter (0.5.0)
pagy (6.0.4) pagy (6.0.2)
parallel (1.23.0) parallel (1.22.1)
parser (3.2.2.3) parser (3.2.1.1)
ast (~> 2.4.1) ast (~> 2.4.1)
racc
pg (1.2.3) pg (1.2.3)
public_suffix (5.0.1) public_suffix (5.0.0)
puma (4.3.12) puma (4.3.12)
nio4r (~> 2.0) nio4r (~> 2.0)
raabro (1.4.0) raabro (1.4.0)
racc (1.7.1) racc (1.6.0)
rack (2.2.7) rack (2.2.4)
rack-protection (3.0.6) rack-protection (3.0.6)
rack rack
rack-test (2.1.0) rack-test (2.0.2)
rack (>= 1.3) rack (>= 1.3)
rails (7.0.5) rails (7.0.4)
actioncable (= 7.0.5) actioncable (= 7.0.4)
actionmailbox (= 7.0.5) actionmailbox (= 7.0.4)
actionmailer (= 7.0.5) actionmailer (= 7.0.4)
actionpack (= 7.0.5) actionpack (= 7.0.4)
actiontext (= 7.0.5) actiontext (= 7.0.4)
actionview (= 7.0.5) actionview (= 7.0.4)
activejob (= 7.0.5) activejob (= 7.0.4)
activemodel (= 7.0.5) activemodel (= 7.0.4)
activerecord (= 7.0.5) activerecord (= 7.0.4)
activestorage (= 7.0.5) activestorage (= 7.0.4)
activesupport (= 7.0.5) activesupport (= 7.0.4)
bundler (>= 1.15.0) bundler (>= 1.15.0)
railties (= 7.0.5) railties (= 7.0.4)
rails-controller-testing (1.0.5)
actionpack (>= 5.0.1.rc1)
actionview (>= 5.0.1.rc1)
activesupport (>= 5.0.1.rc1)
rails-dom-testing (2.0.3) rails-dom-testing (2.0.3)
activesupport (>= 4.2.0) activesupport (>= 4.2.0)
nokogiri (>= 1.6) nokogiri (>= 1.6)
rails-html-sanitizer (1.6.0) rails-html-sanitizer (1.4.3)
loofah (~> 2.21) loofah (~> 2.3)
nokogiri (~> 1.14)
rails-settings-cached (2.8.3) rails-settings-cached (2.8.3)
activerecord (>= 5.0.0) activerecord (>= 5.0.0)
railties (>= 5.0.0) railties (>= 5.0.0)
railties (7.0.5) railties (7.0.4)
actionpack (= 7.0.5) actionpack (= 7.0.4)
activesupport (= 7.0.5) activesupport (= 7.0.4)
method_source method_source
rake (>= 12.2) rake (>= 12.2)
thor (~> 1.0) thor (~> 1.0)
@@ -298,109 +258,110 @@ GEM
rb-fsevent (0.11.2) rb-fsevent (0.11.2)
rb-inotify (0.10.1) rb-inotify (0.10.1)
ffi (~> 1.0) ffi (~> 1.0)
rbs (2.8.4) redis (5.0.5)
redis (4.8.1) redis-client (>= 0.9.0)
regexp_parser (2.8.1) redis-client (0.11.2)
connection_pool
regexp_parser (2.6.1)
responders (3.1.0) responders (3.1.0)
actionpack (>= 5.2) actionpack (>= 5.2)
railties (>= 5.2) railties (>= 5.2)
reverse_markdown (2.1.1) reverse_markdown (2.1.1)
nokogiri nokogiri
rexml (3.2.5) rexml (3.2.5)
rqrcode (2.2.0) rqrcode (2.1.2)
chunky_png (~> 1.0) chunky_png (~> 1.0)
rqrcode_core (~> 1.0) rqrcode_core (~> 1.0)
rqrcode_core (1.2.0) rqrcode_core (1.2.0)
rspec-core (3.12.2) rspec-core (3.12.0)
rspec-support (~> 3.12.0) rspec-support (~> 3.12.0)
rspec-expectations (3.12.3) rspec-expectations (3.12.0)
diff-lcs (>= 1.2.0, < 2.0) diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.12.0) rspec-support (~> 3.12.0)
rspec-mocks (3.12.5) rspec-mocks (3.12.0)
diff-lcs (>= 1.2.0, < 2.0) diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.12.0) rspec-support (~> 3.12.0)
rspec-rails (6.0.3) rspec-rails (6.0.1)
actionpack (>= 6.1) actionpack (>= 6.1)
activesupport (>= 6.1) activesupport (>= 6.1)
railties (>= 6.1) railties (>= 6.1)
rspec-core (~> 3.12) rspec-core (~> 3.11)
rspec-expectations (~> 3.12) rspec-expectations (~> 3.11)
rspec-mocks (~> 3.12) rspec-mocks (~> 3.11)
rspec-support (~> 3.12) rspec-support (~> 3.11)
rspec-support (3.12.0) rspec-support (3.12.0)
rubocop (1.52.1) rubocop (1.48.1)
json (~> 2.3) json (~> 2.3)
parallel (~> 1.10) parallel (~> 1.10)
parser (>= 3.2.2.3) parser (>= 3.2.0.0)
rainbow (>= 2.2.2, < 4.0) rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 1.8, < 3.0) regexp_parser (>= 1.8, < 3.0)
rexml (>= 3.2.5, < 4.0) rexml (>= 3.2.5, < 4.0)
rubocop-ast (>= 1.28.0, < 2.0) rubocop-ast (>= 1.26.0, < 2.0)
ruby-progressbar (~> 1.7) ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 3.0) unicode-display_width (>= 2.4.0, < 3.0)
rubocop-ast (1.29.0) rubocop-ast (1.28.0)
parser (>= 3.2.1.0) parser (>= 3.2.1.0)
ruby-progressbar (1.13.0) ruby-progressbar (1.13.0)
ruby-vips (2.1.4)
ffi (~> 1.12)
ruby2_keywords (0.0.5) ruby2_keywords (0.0.5)
rufus-scheduler (3.9.1) rufus-scheduler (3.8.2)
fugit (~> 1.1, >= 1.1.6) fugit (~> 1.1, >= 1.1.6)
sanitize (6.0.1) sanitize (6.0.1)
crass (~> 1.0.2) crass (~> 1.0.2)
nokogiri (>= 1.12.0) nokogiri (>= 1.12.0)
sentry-rails (5.9.0) sentry-rails (5.8.0)
railties (>= 5.0) railties (>= 5.0)
sentry-ruby (~> 5.9.0) sentry-ruby (~> 5.8.0)
sentry-ruby (5.9.0) sentry-ruby (5.8.0)
concurrent-ruby (~> 1.0, >= 1.0.2) concurrent-ruby (~> 1.0, >= 1.0.2)
sidekiq (6.5.9) sidekiq (6.5.5)
connection_pool (>= 2.2.5, < 3) connection_pool (>= 2.2.2)
rack (~> 2.0) rack (~> 2.0)
redis (>= 4.5.0, < 5) redis (>= 4.5.0)
sidekiq-scheduler (5.0.3) sidekiq-scheduler (4.0.3)
redis (>= 4.2.0)
rufus-scheduler (~> 3.2) rufus-scheduler (~> 3.2)
sidekiq (>= 6, < 8) sidekiq (>= 4, < 7)
tilt (>= 1.4.0) tilt (>= 1.4.0)
solargraph (0.49.0) solargraph (0.48.0)
backport (~> 1.2) backport (~> 1.2)
benchmark benchmark
bundler (~> 2.0) bundler (>= 1.17.2)
diff-lcs (~> 1.4) diff-lcs (~> 1.4)
e2mmap e2mmap
jaro_winkler (~> 1.5) jaro_winkler (~> 1.5)
kramdown (~> 2.3) kramdown (~> 2.3)
kramdown-parser-gfm (~> 1.1) kramdown-parser-gfm (~> 1.1)
parser (~> 3.0) parser (~> 3.0)
rbs (~> 2.0) reverse_markdown (>= 1.0.5, < 3)
reverse_markdown (~> 2.0) rubocop (>= 0.52)
rubocop (~> 1.38)
thor (~> 1.0) thor (~> 1.0)
tilt (~> 2.0) tilt (~> 2.0)
yard (~> 0.9, >= 0.9.24) yard (~> 0.9, >= 0.9.24)
sprockets (4.2.0) sprockets (4.1.1)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
rack (>= 2.2.4, < 4) rack (> 1, < 3)
sprockets-rails (3.4.2) sprockets-rails (3.4.2)
actionpack (>= 5.2) actionpack (>= 5.2)
activesupport (>= 5.2) activesupport (>= 5.2)
sprockets (>= 3.0.0) sprockets (>= 3.0.0)
sqlite3 (1.6.3-arm64-darwin) sqlite3 (1.5.4)
sqlite3 (1.6.3-x86_64-linux) mini_portile2 (~> 2.8.0)
sqlite3 (1.5.4-x86_64-linux)
stimulus-rails (1.2.1) stimulus-rails (1.2.1)
railties (>= 6.0.0) railties (>= 6.0.0)
thor (1.2.2) thor (1.2.1)
tilt (2.2.0) tilt (2.0.11)
timeout (0.3.2) timeout (0.3.0)
turbo-rails (1.4.0) turbo-rails (1.3.2)
actionpack (>= 6.0.0) actionpack (>= 6.0.0)
activejob (>= 6.0.0) activejob (>= 6.0.0)
railties (>= 6.0.0) railties (>= 6.0.0)
tzinfo (2.0.6) tzinfo (2.0.5)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
unicode-display_width (2.4.2) unicode-display_width (2.4.2)
view_component (3.2.0) view_component (2.78.0)
activesupport (>= 5.2.0, < 8.0) activesupport (>= 5.0.0, < 8.0)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
method_source (~> 1.0) method_source (~> 1.0)
warden (1.2.9) warden (1.2.9)
@@ -414,16 +375,18 @@ GEM
addressable (>= 2.8.0) addressable (>= 2.8.0)
crack (>= 0.3.2) crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0) hashdiff (>= 0.4.0, < 2.0.0)
webrick (1.7.0)
websocket-driver (0.7.5) websocket-driver (0.7.5)
websocket-extensions (>= 0.1.0) websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5) websocket-extensions (0.1.5)
xpath (3.2.0) xpath (3.2.0)
nokogiri (~> 1.8) nokogiri (~> 1.8)
yard (0.9.34) yard (0.9.28)
zeitwerk (2.6.8) webrick (~> 1.7.0)
zeitwerk (2.6.6)
PLATFORMS PLATFORMS
arm64-darwin-22 ruby
x86_64-linux x86_64-linux
DEPENDENCIES DEPENDENCIES
@@ -441,21 +404,17 @@ DEPENDENCIES
flipper flipper
flipper-active_record flipper-active_record
flipper-ui flipper-ui
image_processing (~> 1.12.2)
importmap-rails importmap-rails
jbuilder (~> 2.7) jbuilder (~> 2.7)
letter_opener letter_opener
letter_opener_web letter_opener_web
listen (~> 3.2) listen (~> 3.2)
lnurl
lockbox lockbox
net-ldap net-ldap
nostr!
pagy (~> 6.0, >= 6.0.2) pagy (~> 6.0, >= 6.0.2)
pg (~> 1.2.3) pg (~> 1.2.3)
puma (~> 4.1) puma (~> 4.1)
rails (~> 7.0.2) rails (~> 7.0.2)
rails-controller-testing
rails-settings-cached (~> 2.8.3) rails-settings-cached (~> 2.8.3)
rqrcode (~> 2.0) rqrcode (~> 2.0)
rspec-rails rspec-rails

View File

@@ -14,6 +14,7 @@ so:
1. Make sure [Docker Compose is installed][1] and Docker is running (included in 1. Make sure [Docker Compose is installed][1] and Docker is running (included in
Docker Desktop) Docker Desktop)
2. Uncomment the `redis`, `web`, and `sidekiq` sections in `docker-compose.yml`
3. Run `docker compose up` and wait until 389ds announces its successful start 3. Run `docker compose up` and wait until 389ds announces its successful start
in the log output in the log output
4. `docker-compose exec ldap dsconf localhost backend create --suffix="dc=kosmos,dc=org" --be-name="dev"` 4. `docker-compose exec ldap dsconf localhost backend create --suffix="dc=kosmos,dc=org" --be-name="dev"`
@@ -52,14 +53,12 @@ Running all specs:
### Docker (Compose) ### Docker (Compose)
There is a working Docker Compose config file, which define a number of services including There is a working Docker Compose config file, which allows you to spin up both
an app server for Rails as well as a local 389ds (LDAP) server. an app server for Rails as well as a local 389ds (LDAP) server.
For Rails developers, you probably just want to start the LDAP server: `docker-compose up ldap`, By default, `docker-compose up` will only start the LDAP server, listening on
listening on port 389 on your machine. port 389 on your machine. Uncomment other services in `docker-compose.yml` if
you want to use them.
You can pick and choose your services adding them by name (listed in `docker-compose.yml`) at
the end of the docker compose command. eg. `docker compose up ldap redis`
#### LDAP server #### LDAP server
@@ -79,13 +78,6 @@ The setup task will first delete any existing entries in the directory tree
Note that all 389ds data is stored in `tmp/389ds`. So if you want to start over Note that all 389ds data is stored in `tmp/389ds`. So if you want to start over
with a fresh installation, delete both that directory as well as the container. with a fresh installation, delete both that directory as well as the container.
### Adding npm modules to use with Stimulus controllers
The following command downloads the specified npm module to `vendor/javascript`
and adds an entry for it to `config/importmap.rb`.
bin/importmap pin bech32 --download
### Solargraph ### Solargraph
[Solargraph](https://solargraph.org/) is a Ruby language server, which you may [Solargraph](https://solargraph.org/) is a Ruby language server, which you may
@@ -106,8 +98,6 @@ command:
* [Tailwind CSS](https://tailwindcss.com/) * [Tailwind CSS](https://tailwindcss.com/)
* [Sass](https://sass-lang.com/documentation) * [Sass](https://sass-lang.com/documentation)
* [Stimulus](https://stimulus.hotwired.dev/handbook/)
* [Tailwind Stimulus Components](https://github.com/excid3/tailwindcss-stimulus-components)
### Testing ### Testing

View File

@@ -1,4 +1,3 @@
//= link_tree ../images //= link_tree ../images
//= link_tree ../../javascript .js //= link_tree ../../javascript .js
//= link_tree ../builds //= link_tree ../builds
//= link_tree ../../../vendor/javascript .js

View File

@@ -2,7 +2,6 @@
@import "tailwindcss/components"; @import "tailwindcss/components";
@import "tailwindcss/utilities"; @import "tailwindcss/utilities";
@import "components/animations";
@import "components/base"; @import "components/base";
@import "components/buttons"; @import "components/buttons";
@import "components/dashboard_services"; @import "components/dashboard_services";

View File

@@ -1,16 +0,0 @@
@keyframes scaleIn {
from {
transform: scale(0.5);
opacity: 0;
}
to {
transform: scale(1);
opacity: 1;
}
}
.animate-scale-in {
animation-name: scaleIn;
animation-duration: 0.15s;
animation-timing-function: cubic-bezier(0.2, 0, 0.13, 1);
}

View File

@@ -24,10 +24,6 @@
@apply text-xl mb-6; @apply text-xl mb-6;
} }
h4 {
@apply font-bold mb-4 leading-6;
}
main section { main section {
@apply pt-8 sm:pt-12; @apply pt-8 sm:pt-12;
} }

View File

@@ -15,11 +15,7 @@
} }
.btn-icon { .btn-icon {
@apply py-2 px-3; @apply px-3;
}
.btn-outline {
@apply py-2 border-2 border-gray-100 hover:bg-gray-100;
} }
.btn-gray { .btn-gray {
@@ -36,9 +32,4 @@
@apply bg-red-600 hover:bg-red-700 text-white @apply bg-red-600 hover:bg-red-700 text-white
focus:ring-red-500 focus:ring-opacity-75; focus:ring-red-500 focus:ring-opacity-75;
} }
.btn:disabled {
@apply bg-gray-100 hover:bg-gray-200 text-gray-400
focus:ring-gray-300 focus:ring-opacity-75;
}
} }

View File

@@ -15,10 +15,6 @@
@apply border-b-red-600; @apply border-b-red-600;
} }
.field_with_errors {
@apply inline-block;
}
.error-msg { .error-msg {
@apply text-red-700; @apply text-red-700;
} }

View File

@@ -1,15 +0,0 @@
<div class="flex">
<div class="<%= @icon_container_class %>">
<%= image_tag(@icon_path, class: 'h-full w-full') %>
</div>
<div class="flex-1 px-4">
<h4 class="sm:pt-2 mb-2 text-lg font-bold"><%= @name %></h4>
<p class="leading-snug"><%= @description %></p>
<p class="leading-snug flex flex-wrap gap-3">
<% @links.each do |link| %>
<a href="<%= link[1] %>" target="_blank"
class="flex-0 btn-sm btn-gray"><%= link[0] %></a>
<% end %>
</p>
</div>
</div>

View File

@@ -1,19 +0,0 @@
# frozen_string_literal: true
class AppInfoComponent < ViewComponent::Base
def initialize(name:, description:, icon_path: , icon_fill_box: false, links: [])
@name = name
@description = description
@icon_path = icon_path
@icon_container_class = icon_container_class(icon_fill_box)
@links = links
end
def icon_container_class(icon_fill_box)
str = "flex-0 h-16 w-16 sm:h-28 sm:w-28 bg-white rounded-3xl overflow-hidden"
unless icon_fill_box
str += " p-2 border border-gray-200"
end
str
end
end

View File

@@ -1,6 +1,4 @@
<%= tag.public_send(@tag, class: "mb-6 last:mb-0", data: { <%= tag.public_send(@tag, class: "mb-6 last:mb-0") do %>
:'field-name' => @field_name
}) do %>
<% if @positioning == :vertical %> <% if @positioning == :vertical %>
<label class="block"> <label class="block">
<p class="font-bold <%= @descripton.present? ? "mb-1" : "mb-2" %>"> <p class="font-bold <%= @descripton.present? ? "mb-1" : "mb-2" %>">
@@ -11,21 +9,7 @@
<%= @descripton %> <%= @descripton %>
</p> </p>
<% end %> <% end %>
<%= content %>
<%= tag.p class: "flex gap-x-1", data: {
controller: @resettable ? "settings--resettable-field" : nil,
} do %>
<%= content %>
<% if @resettable %>
<button type="button"
class="relative grow-0 shrink-0 btn-md btn-outline text-red-700"
title="Reset to default value"
data-settings--resettable-field-target="resetButton"
data-action="settings--resettable-field#resetField">
Reset
</button>
<% end %>
<% end %>
</label> </label>
<% elsif @positioning == :horizontal %> <% elsif @positioning == :horizontal %>
<label class="block flex items-center justify-between"> <label class="block flex items-center justify-between">

View File

@@ -2,15 +2,11 @@
module FormElements module FormElements
class FieldsetComponent < ViewComponent::Base class FieldsetComponent < ViewComponent::Base
def initialize(tag: "li", positioning: :vertical, def initialize(tag: "li", positioning: :vertical, title:, description: nil)
title:, description: nil,
field_name: nil, resettable: false)
@tag = tag @tag = tag
@positioning = positioning @positioning = positioning
@title = title @title = title
@descripton = description @descripton = description
@field_name = field_name
@resettable = resettable
end end
end end
end end

View File

@@ -1,13 +0,0 @@
<%= render FormElements::FieldsetComponent.new(
title: @title,
description: @description,
field_name: "setting_#{@key.to_s}",
resettable: @resettable
) do %>
<%= method("#{@type}_field").call :setting, @key,
value: Setting.public_send(@key),
data: {
:'default-value' => Setting.get_field(@key)[:default]
},
class: "w-full" %>
<% end %>

View File

@@ -1,20 +0,0 @@
# frozen_string_literal: true
module FormElements
class FieldsetResettableSettingComponent < ViewComponent::Base
def initialize(tag: "li", key:, type: :text, title:, description: nil)
@tag = tag
@positioning = :vertical
@title = title
@description = description
@key = key.to_sym
@type = type
@resettable = is_resettable?(@key)
end
def is_resettable?(key)
default_value = Setting.get_field(key)[:default]
default_value.present? && (default_value != Setting.send(key))
end
end
end

View File

@@ -0,0 +1,3 @@
<%= link_to @path, class: @link_class do %>
<%= @name %>
<% end %>

View File

@@ -0,0 +1,20 @@
# frozen_string_literal: true
class HeaderTabLinkComponent < ViewComponent::Base
def initialize(name:, path:, active: false, disabled: false)
@name = name
@path = path
@active = active
@disabled = disabled
@link_class = class_names_link(path)
end
def class_names_link(path)
common = "block md:inline-block px-5 py-2 rounded-md font-medium text-base md:text-xl"
if @active
"#{common} bg-gray-900/50 text-white"
else
"#{common} text-gray-300 hover:bg-gray-900/30 hover:text-white active:bg-gray-900/30 active:text-white"
end
end
end

View File

@@ -0,0 +1,12 @@
<header class="py-10">
<div class="max-w-6xl md:flex md:gap-x-10 mx-auto px-4 sm:px-6 lg:px-8">
<% if @title.present? %>
<h1 class="text-3xl font-bold text-white">
<%= @title %>
</h1>
<% end %>
<nav class="md:grow flex gap-x-4 <%= @title.present? ? "justify-end" : "justify-start" %>" aria-label="Tabs">
<%= render partial: @tabnav_partial %>
</nav>
</div>
</header>

View File

@@ -0,0 +1,8 @@
# frozen_string_literal: true
class HeaderWithTabsComponent < ViewComponent::Base
def initialize(title: nil, tabnav_partial:)
@title = title
@tabnav_partial = tabnav_partial
end
end

View File

@@ -1,28 +0,0 @@
<div tabindex="-1" class="relative z-10">
<!-- Modal Background -->
<div class="hidden fixed inset-0 bg-black bg-opacity-80 overflow-y-auto flex items-center justify-center"
data-modal-target="background"
data-action="click->modal#closeBackground"
data-transition-enter="transition-all ease-in-out duration-100"
data-transition-enter-from="bg-opacity-0"
data-transition-enter-to="bg-opacity-80"
data-transition-leave="transition-all ease-in-out duration-100"
data-transition-leave-from="bg-opacity-80"
data-transition-leave-to="bg-opacity-0">
<!-- Modal Container -->
<div data-modal-target="container"
class="max-h-screen w-auto max-w-lg relative
hidden animate-scale-in fixed inset-0 overflow-y-auto flex items-center justify-center">
<!-- Modal Card -->
<div class="m-1 bg-white rounded shadow">
<div class="p-8">
<%= content %>
<div class="flex justify-end items-center flex-wrap mt-6">
<button class="btn-md btn-blue" data-action="click->modal#close:prevent">Close</button>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@@ -1,2 +0,0 @@
class ModalComponent < ViewComponent::Base
end

View File

@@ -1,6 +0,0 @@
<%= render ModalComponent.new do %>
<% if @descripton.present? %>
<p class="mb-6"><%= @description %></p>
<% end %>
<p><%= raw @qr_code_svg %></p>
<% end %>

View File

@@ -1,24 +0,0 @@
require "rqrcode"
class QrCodeModalComponent < ViewComponent::Base
def initialize(qr_content:, description: nil)
@description = description
@qr_code_svg = qr_code_svg(qr_content)
end
private
def qr_code_svg(content)
qr_code = RQRCode::QRCode.new(content)
qr_code.as_svg(
color: "000",
shape_rendering: "crispEdges",
module_size: 6,
standalone: true,
use_path: true,
svg_attributes: {
class: 'inline-block'
}
)
end
end

View File

@@ -1,8 +1,4 @@
<%= link_to @path, class: @link_class, title: (@disabled ? "Coming soon" : nil) do %> <%= link_to @path, class: @link_class, title: (@disabled ? "Coming soon" : nil) do %>
<% if @icon.present? %>
<%= render partial: "icons/#{@icon}", locals: { custom_class: @icon_class } %> <%= render partial: "icons/#{@icon}", locals: { custom_class: @icon_class } %>
<% elsif @text_icon.present? %>
<span class="mr-3"><%= @text_icon %></span>
<% end %>
<span class="truncate"><%= @name %></span> <span class="truncate"><%= @name %></span>
<% end %> <% end %>

View File

@@ -1,13 +1,11 @@
# frozen_string_literal: true # frozen_string_literal: true
class SidenavLinkComponent < ViewComponent::Base class SidenavLinkComponent < ViewComponent::Base
def initialize(name:, level: 1, path:, icon: nil, text_icon: nil, def initialize(name:, level: 1, path:, icon:, active: false, disabled: false)
active: false, disabled: false)
@name = name @name = name
@level = level @level = level
@path = path @path = path
@icon = icon @icon = icon
@text_icon = text_icon
@active = active @active = active
@disabled = disabled @disabled = disabled
@link_class = class_names_link(path) @link_class = class_names_link(path)

View File

@@ -3,7 +3,7 @@ class Admin::Settings::ServicesController < Admin::SettingsController
@service = params[:s] @service = params[:s]
if @service.blank? if @service.blank?
redirect_to admin_settings_services_path(params: { s: "btcpay" }) redirect_to admin_settings_services_path(params: { s: "discourse" })
end end
end end

View File

@@ -4,7 +4,7 @@ class Admin::UsersController < Admin::BaseController
def index def index
ldap = LdapService.new ldap = LdapService.new
@ou = params[:ou] || Setting.primary_domain @ou = params[:ou] || "kosmos.org"
@orgs = ldap.fetch_organizations @orgs = ldap.fetch_organizations
@pagy, @users = pagy(User.where(ou: @ou).order(cn: :asc)) @pagy, @users = pagy(User.where(ou: @ou).order(cn: :asc))
@@ -20,8 +20,6 @@ class Admin::UsersController < Admin::BaseController
end end
@services_enabled = @user.services_enabled @services_enabled = @user.services_enabled
@avatar = LdapManager::FetchAvatar.call(cn: @user.cn, ou: @user.ou)
end end
private private

View File

@@ -1,29 +0,0 @@
class Api::BtcpayController < Api::BaseController
before_action :require_feature_enabled
def onchain_btc_balance
balance = BtcpayManager::FetchOnchainWalletBalance.call
render json: balance
rescue => error
Rails.logger.warn "Failed to fetch BTC wallet balance: #{error.message}"
render json: { error: 'Failed to fetch wallet balance' },
status: 500
end
def lightning_btc_balance
balance = BtcpayManager::FetchLightningWalletBalance.call
render json: balance
rescue => error
Rails.logger.warn "Failed to fetch BTC lightning balance: #{error.message}"
render json: { error: 'Failed to fetch wallet balance' },
status: 500
end
private
def require_feature_enabled
unless Setting.btcpay_publish_wallet_balances
http_status :not_found and return
end
end
end

View File

@@ -0,0 +1,13 @@
class Api::KreditsController < Api::BaseController
def onchain_btc_balance
btcpay = BtcPay.new
balance = btcpay.onchain_wallet_balance
render json: balance
rescue => error
Rails.logger.warn "Failed to fetch kredits BTC wallet balance: #{error.message}"
render json: { error: 'Failed to fetch wallet balance' },
status: 500
end
end

View File

@@ -37,8 +37,4 @@ class ApplicationController < ActionController::Base
format.any { head status } format.any { head status }
end end
end end
def after_sign_in_path_for(user)
session[:user_return_to] || root_path
end
end end

View File

@@ -1,146 +0,0 @@
class Rs::OauthController < ApplicationController
before_action :require_signed_in_with_username, only: :new
before_action :authenticate_user!, only: :create
def new
username, org = params[:useraddress].split("@")
@user = User.where(cn: username.downcase, ou: org).first
@scopes = parse_scopes params[:scope]
@redirect_uri = params[:redirect_uri]
@client_id = params[:client_id]
@state = params[:state]
@root_access_requested = (@scopes & [":r",":rw"]).any?
@denial_url = url_with_state("#{@redirect_uri}#error=access_denied", @state)
@expire_at_dates = [["Never", nil],
["In 1 month", 1.month.from_now],
["In 1 day", 1.day.from_now]]
http_status :bad_request and return unless @redirect_uri.present?
unless current_user == @user
sign_out :user
redirect_to new_rs_oauth_url(@user.address,
scope: params[:scope],
redirect_uri: params[:redirect_uri],
client_id: params[:client_id],
state: params[:state])
return
end
unless @client_id.present?
redirect_to(url_with_state("#{@redirect_uri}#error=invalid_request", @state),
allow_other_host: true) and return
end
if @scopes.empty?
redirect_to(url_with_state("#{@redirect_uri}#error=invalid_scope", @state),
allow_other_host: true) and return
end
unless hostname_of(@client_id) == hostname_of(@redirect_uri)
redirect_to(url_with_state("#{@redirect_uri}#error=invalid_client", @state),
allow_other_host: true) and return
end
@client_id.gsub!(/http(s)?:\/\//, "")
if auth = current_user.remote_storage_authorizations.valid.where(permissions: @scopes, client_id: @client_id).first
redirect_to(url_with_state("#{@redirect_uri}#access_token=#{auth.token}", @state),
allow_other_host: true) and return
end
end
def create
unless current_user.id.to_s == params[:user_id]
Rails.logger.info("NO MATCH: #{params[:user_id]}, #{current_user.id}")
http_status :forbidden and return
end
permissions = parse_scopes params[:scope]
redirect_uri = params[:redirect_uri].presence
client_id = params[:client_id].presence
state = params[:state].presence
expire_at = params[:expire_at].presence
http_status :bad_request and return unless redirect_uri.present?
if permissions.empty?
redirect_to(url_with_state("#{redirect_uri}#error=invalid_scope", state),
allow_other_host: true) and return
end
unless client_id.present?
redirect_to(url_with_state("#{redirect_uri}#error=invalid_request", state),
allow_other_host: true) and return
end
unless hostname_of(client_id) == hostname_of(redirect_uri)
redirect_to(url_with_state("#{redirect_uri}#error=invalid_client", state),
allow_other_host: true) and return
end
client_id.gsub!(/http(s)?:\/\//, "")
auth = current_user.remote_storage_authorizations.create!(
permissions: permissions,
client_id: client_id,
redirect_uri: redirect_uri,
app_name: client_id, #TODO use user-defined name
expire_at: expire_at
)
redirect_to url_with_state("#{redirect_uri}#access_token=#{auth.token}", state),
allow_other_host: true
end
# GET /rs/oauth/token/:id/launch_app
def launch_app
auth = current_user.remote_storage_authorizations.find(params[:id])
redirect_to app_auth_url(auth), allow_other_host: true
end
private
def require_signed_in_with_username
unless user_signed_in?
username, org = params[:useraddress].split("@")
session[:user_return_to] = request.url
redirect_to new_user_session_path(cn: username, ou: org)
end
end
def app_auth_url(auth)
url = "#{auth.url}#remotestorage=#{current_user.address}"
url += "&access_token=#{auth.token}"
url
end
def hostname_of(uri)
uri.gsub(/http(s)?:\/\//, "").split(":")[0].split("/")[0]
end
def parse_scopes(scope_string)
return [] if scope_string.blank?
scopes = scope_string.
gsub(/\[|\]/, "").
gsub(/\,/, " ").
gsub(/\/:/, ":").
split(/\s/).map(&:strip).
reject(&:empty?)
scopes = [":r"] if scopes.include?("*:r")
scopes = [":rw"] if scopes.include?("*:rw")
scopes
end
def url_with_state(url, state)
state ? "#{url}&state=#{CGI.escape(state)}" : url
end
end

View File

@@ -1,9 +0,0 @@
class Services::BaseController < ApplicationController
before_action :set_current_section
private
def set_current_section
@current_section = :services
end
end

View File

@@ -1,14 +0,0 @@
class Services::ChatController < Services::BaseController
before_action :authenticate_user!
before_action :require_service_available
def show
@service_enabled = current_user.services_enabled.include?(:xmpp)
end
private
def require_service_available
http_status :not_found unless Setting.ejabberd_enabled?
end
end

View File

@@ -1,5 +1,4 @@
require "rqrcode" require "rqrcode"
require "lnurl"
class Services::LightningController < ApplicationController class Services::LightningController < ApplicationController
before_action :authenticate_user! before_action :authenticate_user!
@@ -8,51 +7,25 @@ class Services::LightningController < ApplicationController
before_action :fetch_balance before_action :fetch_balance
def index def index
@wallet_setup_url = "lndhub://#{current_user.ln_account}:#{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(
color: "000",
shape_rendering: "crispEdges",
module_size: 6,
standalone: true,
use_path: true,
svg_attributes: {
class: 'inline-block'
}
)
end end
def transactions def transactions
@transactions = fetch_transactions @transactions = fetch_transactions
end end
def qr_lnurlp
lnurlp_url = "https://kosmos.org/.well-known/lnurlp/#{current_user.cn}"
lnurlp_bech32 = Lnurl.new(lnurlp_url).to_bech32
qr_code = RQRCode::QRCode.new("lightning:" + lnurlp_bech32)
respond_to do |format|
format.svg do
qr_svg = qr_code.as_svg(
color: "000",
shape_rendering: "crispEdges",
module_size: 6,
standalone: true,
use_path: true,
svg_attributes: {
class: 'inline-block'
}
)
send_data(
qr_svg,
filename: "bitcoin-lightning-#{current_user.address}.svg",
type: "image/svg+xml"
)
end
format.png do
qr_png = qr_code.as_png(
fill: "white",
color: "black",
size: 1024,
)
send_data(
qr_png,
filename: "bitcoin-lightning-#{current_user.address}.png",
type: "image/png"
)
end
end
end
private private
def authenticate_with_lndhub(options={}) def authenticate_with_lndhub(options={})

View File

@@ -1,14 +0,0 @@
class Services::MastodonController < Services::BaseController
before_action :authenticate_user!
before_action :require_service_available
def show
@service_enabled = current_user.services_enabled.include?(:mastodon)
end
private
def require_service_available
http_status :not_found unless Setting.mastodon_enabled?
end
end

View File

@@ -1,7 +1,8 @@
class Services::RemotestorageController < Services::BaseController class Services::RemotestorageController < ApplicationController
before_action :authenticate_user! before_action :require_user_signed_in
before_action :require_service_enabled
before_action :require_feature_enabled before_action :require_feature_enabled
before_action :require_service_available before_action :set_current_section
def dashboard def dashboard
# unless current_user.services_enabled.include?(:remotestorage) # unless current_user.services_enabled.include?(:remotestorage)
@@ -17,7 +18,13 @@ class Services::RemotestorageController < Services::BaseController
end end
end end
def require_service_available def require_service_enabled
http_status :not_found unless Setting.remotestorage_enabled? unless Setting.remotestorage_enabled?
http_status :not_found
end
end
def set_current_section
@current_section = :services
end end
end end

View File

@@ -1,5 +1,3 @@
require 'securerandom'
class SettingsController < ApplicationController class SettingsController < ApplicationController
before_action :authenticate_user! before_action :authenticate_user!
before_action :set_main_nav_section before_action :set_main_nav_section
@@ -11,23 +9,15 @@ class SettingsController < ApplicationController
end end
def show def show
if @settings_section == "experiments"
session[:shared_secret] ||= SecureRandom.base64(12)
end
end end
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]
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])
LdapManager::UpdateDisplayName.call(@user.dn, @user.display_name) LdapManager::UpdateDisplayName.call(@user.dn, user_params[:display_name])
end
if @user.avatar_new.present?
LdapManager::UpdateAvatar.call(@user.dn, @user.avatar_new)
end end
redirect_to setting_path(@settings_section), flash: { redirect_to setting_path(@settings_section), flash: {
@@ -63,45 +53,6 @@ class SettingsController < ApplicationController
redirect_to check_your_email_path, notice: msg redirect_to check_your_email_path, notice: msg
end end
def set_nostr_pubkey
signed_event = nostr_event_params[:signed_event].to_h.symbolize_keys
is_valid_id = NostrManager::ValidateId.call(signed_event)
is_valid_sig = NostrManager::VerifySignature.call(signed_event)
is_correct_content = signed_event[:content] == "Connect my public key to #{current_user.address} (confirmation #{session[:shared_secret]})"
unless is_valid_id && is_valid_sig && is_correct_content
flash[:alert] = "Public key could not be verified"
http_status :unprocessable_entity and return
end
pubkey_taken = User.all_except(current_user).where(
ou: current_user.ou, nostr_pubkey: signed_event[:pubkey]
).any?
if pubkey_taken
flash[:alert] = "Public key already in use for a different account"
http_status :unprocessable_entity and return
end
current_user.update! nostr_pubkey: signed_event[:pubkey]
session[:shared_secret] = nil
flash[:success] = "Public key verification successful"
http_status :ok
rescue
flash[:alert] = "Public key could not be verified"
http_status :unprocessable_entity and return
end
# DELETE /settings/nostr_pubkey
def remove_nostr_pubkey
current_user.update! nostr_pubkey: nil
redirect_to setting_path(:experiments), flash: {
success: 'Public key removed from account'
}
end
private private
def set_main_nav_section def set_main_nav_section
@@ -110,7 +61,7 @@ class SettingsController < ApplicationController
def set_settings_section def set_settings_section
@settings_section = params[:section] @settings_section = params[:section]
allowed_sections = [:profile, :account, :lightning, :xmpp, :experiments] allowed_sections = [:profile, :account, :lightning, :xmpp]
unless allowed_sections.include?(@settings_section.to_sym) unless allowed_sections.include?(@settings_section.to_sym)
redirect_to setting_path(:profile) redirect_to setting_path(:profile)
@@ -122,7 +73,7 @@ class SettingsController < ApplicationController
end end
def user_params def user_params
params.require(:user).permit(:display_name, :avatar, preferences: [ params.require(:user).permit(:display_name, preferences: [
:lightning_notify_sats_received, :lightning_notify_sats_received,
:xmpp_exchange_contacts_with_invitees :xmpp_exchange_contacts_with_invitees
]) ])
@@ -131,10 +82,4 @@ class SettingsController < ApplicationController
def email_params def email_params
params.require(:user).permit(:email, :current_password) params.require(:user).permit(:email, :current_password)
end end
def nostr_event_params
params.permit(signed_event: [
:id, :pubkey, :created_at, :kind, :tags, :content, :sig
])
end
end end

View File

@@ -88,7 +88,7 @@ class SignupController < ApplicationController
if session[:new_user].present? if session[:new_user].present?
@user = User.new(session[:new_user]) @user = User.new(session[:new_user])
else else
@user = User.new(ou: Setting.primary_domain) @user = User.new(ou: "kosmos.org")
end end
end end
@@ -98,7 +98,7 @@ class SignupController < ApplicationController
CreateAccount.call( CreateAccount.call(
username: @user.cn, username: @user.cn,
domain: Setting.primary_domain, domain: "kosmos.org",
email: @user.email, email: @user.email,
password: @user.password, password: @user.password,
invitation: @invitation invitation: @invitation

View File

@@ -30,7 +30,7 @@ class WebhooksController < ApplicationController
def notify_xmpp(address, amt_sats, memo) def notify_xmpp(address, amt_sats, memo)
payload = { payload = {
type: "normal", type: "normal",
from: Setting.xmpp_notifications_from_address, from: "kosmos.org", # TODO domain config
to: address, to: address,
subject: "Sats received!", subject: "Sats received!",
body: "#{helpers.number_with_delimiter amt_sats} sats received in your Lightning wallet:\n> #{memo}" body: "#{helpers.number_with_delimiter amt_sats} sats received in your Lightning wallet:\n> #{memo}"

View File

@@ -1,16 +0,0 @@
class WellKnownController < ApplicationController
def nostr
http_status :unprocessable_entity and return if params[:name].blank?
domain = request.headers["X-Forwarded-Host"].presence || Setting.primary_domain
@user = User.where(cn: params[:name], ou: domain).first
http_status :not_found and return if @user.nil? || @user.nostr_pubkey.blank?
respond_to do |format|
format.json do
render json: {
names: { "#{@user.cn}": @user.nostr_pubkey }
}.to_json
end
end
end
end

View File

@@ -1,11 +0,0 @@
module OauthHelper
def scope_name(scope)
scope.gsub(/(\:.+)/, '')
end
def scope_permissions(scope)
scope.match(/\:r$/) ? "r" : "rw"
end
end

View File

@@ -1,11 +1,7 @@
import { Application } from "@hotwired/stimulus" import { Application } from "@hotwired/stimulus"
import { Modal, Tabs } from "tailwindcss-stimulus-components"
const application = Application.start() const application = Application.start()
application.register('modal', Modal)
application.register('tabs', Tabs)
// Configure Stimulus development experience // Configure Stimulus development experience
application.debug = false application.debug = false
window.Stimulus = application window.Stimulus = application

View File

@@ -1,65 +0,0 @@
import { Controller } from "@hotwired/stimulus"
import { bech32 } from "bech32"
function hexToBytes (hex) {
let bytes = []
for (let c = 0; c < hex.length; c += 2) {
bytes.push(parseInt(hex.substr(c, 2), 16))
}
return bytes
}
// Connects to data-controller="settings--nostr-pubkey"
export default class extends Controller {
static targets = [ "noExtension", "setPubkey", "pubkeyBech32Input" ]
static values = { userAddress: String, pubkeyHex: String, sharedSecret: String }
connect () {
if (this.hasPubkeyHexValue && this.pubkeyHexValue.length > 0) {
this.pubkeyBech32InputTarget.value = this.pubkeyBech32
}
if (window.nostr) {
if (this.hasSetPubkeyTarget) {
this.setPubkeyTarget.disabled = false
}
} else {
this.noExtensionTarget.classList.remove("hidden")
}
}
async setPubkey () {
this.setPubkeyTarget.disabled = true
try {
const signedEvent = await window.nostr.signEvent({
created_at: Math.floor(Date.now() / 1000),
kind: 1,
tags: [],
content: `Connect my public key to ${this.userAddressValue} (confirmation ${this.sharedSecretValue})`
})
const res = await fetch("/settings/set_nostr_pubkey", {
method: "POST", credentials: "include", headers: {
"Accept": "application/json", 'Content-Type': 'application/json',
"X-CSRF-Token": this.csrfToken
}, body: JSON.stringify({ signed_event: signedEvent })
});
window.location.reload()
} catch (error) {
console.warn('Unable to verify pubkey:', error.message)
this.setPubkeyTarget.disabled = false
}
}
get pubkeyBech32 () {
const words = bech32.toWords(hexToBytes(this.pubkeyHexValue))
return bech32.encode('npub', words)
}
get csrfToken () {
const element = document.head.querySelector('meta[name="csrf-token"]')
return element.getAttribute("content")
}
}

View File

@@ -1,10 +0,0 @@
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = [ "resetButton" ]
resetField () {
const inputEl = this.element.querySelector('input')
inputEl.value = inputEl.dataset.defaultValue
}
}

View File

@@ -1,10 +0,0 @@
class RemoteStorageExpireAuthorizationJob < ApplicationJob
queue_as :remotestorage
def perform(rs_auth_id)
rs_auth = RemoteStorageAuthorization.find rs_auth_id
return unless rs_auth.expire_at.nil? || rs_auth.expire_at <= DateTime.now
rs_auth.destroy!
end
end

View File

@@ -1,63 +0,0 @@
class RemoteStorageAuthorization < ApplicationRecord
belongs_to :user
serialize :permissions
validates_presence_of :permissions
validates_presence_of :client_id
scope :valid, -> { where(expire_at: nil).or(where(expire_at: (DateTime.now)..)) }
scope :expired, -> { where(expire_at: ..(DateTime.now)) }
after_initialize do |a|
a.permissions = [] if a.permissions == nil
end
before_create :generate_token
before_create :store_token_in_redis
after_create :schedule_token_expiry
before_destroy :delete_token_from_redis
after_destroy :remove_token_expiry_job
def url
if self.redirect_uri
uri = URI.parse self.redirect_uri
"#{uri.scheme}://#{client_id}"
else
"http://#{client_id}"
end
end
def delete_token_from_redis
key = "rs:authorizations:#{user.address}:#{token}"
redis.srem? key, redis.smembers(key)
end
private
def redis
@redis ||= Redis.new(url: Setting.rs_redis_url)
end
def generate_token(length=16)
self.token = SecureRandom.hex(length) if self.token.blank?
end
def store_token_in_redis
redis.sadd "rs:authorizations:#{user.address}:#{token}", permissions
end
def schedule_token_expiry
return unless expire_at.present?
RemoteStorageExpireAuthorizationJob.set(wait_until: expire_at)
.perform_later(id)
end
def remove_token_expiry_job
queue = Sidekiq::Queue.new(RemoteStorageExpireAuthorizationJob.queue_name)
queue.each do |job|
next unless job.display_class == "RemoteStorageExpireAuthorizationJob"
job.delete if job.display_args == [id]
end
end
end

View File

@@ -2,9 +2,6 @@
class Setting < RailsSettings::Base class Setting < RailsSettings::Base
cache_prefix { "v1" } cache_prefix { "v1" }
field :primary_domain, type: :string,
default: ENV["PRIMARY_DOMAIN"].presence
field :accounts_domain, type: :string, field :accounts_domain, type: :string,
default: ENV["AKKOUNTS_DOMAIN"].presence default: ENV["AKKOUNTS_DOMAIN"].presence
@@ -12,7 +9,7 @@ class Setting < RailsSettings::Base
# Internal services # Internal services
# #
field :redis_url, type: :string, field :redis_url, type: :string, readonly: true,
default: ENV["REDIS_URL"] || "redis://localhost:6379/0" default: ENV["REDIS_URL"] || "redis://localhost:6379/0"
# #
@@ -29,67 +26,38 @@ class Setting < RailsSettings::Base
field :xmpp_default_rooms, type: :array, default: [] field :xmpp_default_rooms, type: :array, default: []
field :xmpp_autojoin_default_rooms, type: :boolean, default: false field :xmpp_autojoin_default_rooms, type: :boolean, default: false
field :xmpp_notifications_from_address, type: :string, default: primary_domain
# #
# Sentry # Sentry
# #
field :sentry_enabled, type: :boolean, readonly: true, field :sentry_enabled, type: :boolean, readonly: true,
default: ENV["SENTRY_DSN"].present? default: (ENV["SENTRY_DSN"].present?.to_s || false)
#
# BTCPay Server
#
field :btcpay_api_url, type: :string,
default: ENV["BTCPAY_API_URL"].presence
field :btcpay_enabled, type: :boolean,
default: ENV["BTCPAY_API_URL"].present?
field :btcpay_store_id, type: :string,
default: ENV["BTCPAY_STORE_ID"].presence
field :btcpay_auth_token, type: :string,
default: ENV["BTCPAY_AUTH_TOKEN"].presence
field :btcpay_publish_wallet_balances, type: :boolean, default: true
# #
# Discourse # Discourse
# #
field :discourse_public_url, type: :string, field :discourse_public_url, type: :string, readonly: true,
default: ENV["DISCOURSE_PUBLIC_URL"].presence default: ENV["DISCOURSE_PUBLIC_URL"].presence
field :discourse_enabled, type: :boolean, field :discourse_enabled, type: :boolean,
default: ENV["DISCOURSE_PUBLIC_URL"].present? default: (ENV["DISCOURSE_PUBLIC_URL"].present?.to_s || false)
field :discourse_connect_secret, type: :string, field :discourse_connect_secret, type: :string, readonly: true,
default: ENV["DISCOURSE_CONNECT_SECRET"].presence default: ENV["DISCOURSE_CONNECT_SECRET"].presence
#
# Drone CI
#
field :droneci_public_url, type: :string,
default: ENV["DRONECI_PUBLIC_URL"].presence
field :droneci_enabled, type: :boolean,
default: ENV["DRONECI_PUBLIC_URL"].present?
# #
# ejabberd # ejabberd
# #
field :ejabberd_enabled, type: :boolean, field :ejabberd_enabled, type: :boolean,
default: ENV["EJABBERD_API_URL"].present? default: (ENV["EJABBERD_API_URL"].present?.to_s || false)
field :ejabberd_api_url, type: :string, field :ejabberd_api_url, type: :string, readonly: true,
default: ENV["EJABBERD_API_URL"].presence default: ENV["EJABBERD_API_URL"].presence
field :ejabberd_admin_url, type: :string, field :ejabberd_admin_url, type: :string, readonly: true,
default: ENV["EJABBERD_ADMIN_URL"].presence default: ENV["EJABBERD_ADMIN_URL"].presence
field :ejabberd_buddy_roster, type: :string, field :ejabberd_buddy_roster, type: :string,
@@ -99,56 +67,50 @@ class Setting < RailsSettings::Base
# Gitea # Gitea
# #
field :gitea_public_url, type: :string, field :gitea_public_url, type: :string, readonly: true,
default: ENV["GITEA_PUBLIC_URL"].presence default: ENV["GITEA_PUBLIC_URL"].presence
field :gitea_enabled, type: :boolean, field :gitea_enabled, type: :boolean,
default: ENV["GITEA_PUBLIC_URL"].present? default: (ENV["GITEA_PUBLIC_URL"].present?.to_s || false)
# #
# Lightning Network # Lightning Network
# #
field :lndhub_api_url, type: :string, field :lndhub_api_url, type: :string, readonly: true,
default: ENV["LNDHUB_API_URL"].presence default: ENV["LNDHUB_API_URL"].presence
field :lndhub_enabled, type: :boolean, field :lndhub_enabled, type: :boolean,
default: ENV["LNDHUB_API_URL"].present? default: (ENV["LNDHUB_API_URL"].present?.to_s || false)
field :lndhub_admin_token, type: :string,
default: ENV["LNDHUB_ADMIN_TOKEN"].presence
field :lndhub_admin_enabled, type: :boolean, field :lndhub_admin_enabled, type: :boolean,
default: ENV["LNDHUB_ADMIN_UI"] || false default: (ENV["LNDHUB_ADMIN_UI"] || false)
field :lndhub_public_key, type: :string, field :lndhub_public_key, type: :string, readonly: true,
default: (ENV["LNDHUB_PUBLIC_KEY"] || "") default: (ENV["LNDHUB_PUBLIC_KEY"] || "")
field :lndhub_keysend_enabled, type: :boolean, field :lndhub_keysend_enabled, type: :boolean,
default: -> { self.lndhub_public_key.present? } default: -> { self.lndhub_public_key.present?.to_s || false }
# #
# Mastodon # Mastodon
# #
field :mastodon_public_url, type: :string, field :mastodon_public_url, type: :string, readonly: true,
default: ENV["MASTODON_PUBLIC_URL"].presence default: ENV["MASTODON_PUBLIC_URL"].presence
field :mastodon_enabled, type: :boolean, field :mastodon_enabled, type: :boolean,
default: ENV["MASTODON_PUBLIC_URL"].present? default: (ENV["MASTODON_PUBLIC_URL"].present?.to_s || false)
field :mastodon_address_domain, type: :string,
default: ENV["MASTODON_ADDRESS_DOMAIN"].presence || self.primary_domain
# #
# MediaWiki # MediaWiki
# #
field :mediawiki_public_url, type: :string, field :mediawiki_public_url, type: :string, readonly: true,
default: ENV["MEDIAWIKI_PUBLIC_URL"].presence default: ENV["MEDIAWIKI_PUBLIC_URL"].presence
field :mediawiki_enabled, type: :boolean, field :mediawiki_enabled, type: :boolean,
default: ENV["MEDIAWIKI_PUBLIC_URL"].present? default: (ENV["MEDIAWIKI_PUBLIC_URL"].present?.to_s || false)
# #
# Nostr # Nostr
@@ -161,11 +123,8 @@ class Setting < RailsSettings::Base
# #
field :remotestorage_enabled, type: :boolean, field :remotestorage_enabled, type: :boolean,
default: ENV["RS_STORAGE_URL"].present? default: (ENV["RS_STORAGE_URL"].present?.to_s || false)
field :rs_storage_url, type: :string, field :rs_storage_url, type: :string,
default: ENV["RS_STORAGE_URL"].presence default: ENV["RS_STORAGE_URL"].presence
field :rs_redis_url, type: :string,
default: ENV["RS_REDIS_URL"] || "redis://localhost:6379/1"
end end

View File

@@ -2,14 +2,10 @@ class User < ApplicationRecord
include EmailValidatable include EmailValidatable
attr_accessor :display_name attr_accessor :display_name
attr_accessor :avatar_new
serialize :preferences, UserPreferences serialize :preferences, UserPreferences
#
# Relations # Relations
#
has_many :invitations, dependent: :destroy has_many :invitations, dependent: :destroy
has_one :invitation, inverse_of: :invitee, foreign_key: 'invited_user_id' has_one :invitation, inverse_of: :invitee, foreign_key: 'invited_user_id'
has_one :inviter, through: :invitation, source: :user has_one :inviter, through: :invitation, source: :user
@@ -22,13 +18,7 @@ class User < ApplicationRecord
has_many :accounts, through: :lndhub_user has_many :accounts, through: :lndhub_user
has_many :remote_storage_authorizations validates_uniqueness_of :cn
#
# Validations
#
validates_uniqueness_of :cn, scope: :ou
validates_length_of :cn, minimum: 3 validates_length_of :cn, minimum: 3
validates_format_of :cn, with: /\A([a-z0-9\-])*\z/, validates_format_of :cn, with: /\A([a-z0-9\-])*\z/,
if: Proc.new{ |u| u.cn.present? }, if: Proc.new{ |u| u.cn.present? },
@@ -46,21 +36,8 @@ class User < ApplicationRecord
validates_length_of :display_name, minimum: 3, maximum: 35, allow_blank: true, validates_length_of :display_name, minimum: 3, maximum: 35, allow_blank: true,
if: -> { defined?(@display_name) } if: -> { defined?(@display_name) }
validates_uniqueness_of :nostr_pubkey, allow_blank: true scope :confirmed, -> { where.not(confirmed_at: nil) }
scope :pending, -> { where(confirmed_at: nil) }
validate :acceptable_avatar
#
# Scopes
#
scope :confirmed, -> { where.not(confirmed_at: nil) }
scope :pending, -> { where(confirmed_at: nil) }
scope :all_except, -> (user) { where.not(id: user) }
#
# Encrypted database columns
#
has_encrypted :ln_login, :ln_password has_encrypted :ln_login, :ln_password
@@ -90,7 +67,6 @@ class User < ApplicationRecord
# E-Mail update confirmed # E-Mail update confirmed
LdapManager::UpdateEmail.call(self.dn, self.email) LdapManager::UpdateEmail.call(self.dn, self.email)
else else
# TODO Make configurable
# E-Mail from signup confirmed (i.e. account activation) # E-Mail from signup confirmed (i.e. account activation)
enable_service %w[ discourse gitea mediawiki xmpp ] enable_service %w[ discourse gitea mediawiki xmpp ]
@@ -128,11 +104,6 @@ class User < ApplicationRecord
"#{self.cn}@#{self.ou}" "#{self.cn}@#{self.ou}"
end end
def mastodon_address
return nil unless Setting.mastodon_enabled?
"#{self.cn}@#{Setting.mastodon_address_domain}"
end
def valid_attribute?(attribute_name) def valid_attribute?(attribute_name)
self.valid? self.valid?
self.errors[attribute_name].blank? self.errors[attribute_name].blank?
@@ -158,10 +129,6 @@ class User < ApplicationRecord
@display_name ||= ldap_entry[:display_name] @display_name ||= ldap_entry[:display_name]
end end
def avatar
@avatar_base64 ||= LdapManager::FetchAvatar.call(cn: cn, ou: ou)
end
def services_enabled def services_enabled
ldap_entry[:service] || [] ldap_entry[:service] || []
end end
@@ -190,17 +157,4 @@ class User < ApplicationRecord
return @ldap_service if defined?(@ldap_service) return @ldap_service if defined?(@ldap_service)
@ldap_service = LdapService.new @ldap_service = LdapService.new
end end
def acceptable_avatar
return unless avatar_new.present?
if avatar_new.size > 1.megabyte
errors.add(:avatar, "file size is too large")
end
acceptable_types = ["image/jpeg", "image/png"]
unless acceptable_types.include?(avatar_new.content_type)
errors.add(:avatar, "must be a JPEG or PNG file")
end
end
end end

32
app/services/btc_pay.rb Normal file
View File

@@ -0,0 +1,32 @@
#
# API Docs: https://docs.btcpayserver.org/API/Greenfield/v1/
#
class BtcPay
def initialize
@base_url = ENV["BTCPAY_API_URL"]
@store_id = Rails.application.credentials.btcpay[:store_id]
@auth_token = Rails.application.credentials.btcpay[:auth_token]
end
def onchain_wallet_balance
res = get "stores/#{@store_id}/payment-methods/onchain/BTC/wallet"
{
balance: res["balance"].to_f,
unconfirmed_balance: res["unconfirmedBalance"].to_f,
confirmed_balance: res["confirmedBalance"].to_f
}
end
private
def get(endpoint)
res = Faraday.get("#{@base_url}/#{endpoint}", {}, {
"Content-Type" => "application/json",
"Accept" => "application/json",
"Authorization" => "token #{@auth_token}"
})
JSON.parse(res.body)
end
end

View File

@@ -1,11 +0,0 @@
module BtcpayManager
class FetchLightningWalletBalance < BtcpayManagerService
def call
res = get "stores/#{store_id}/lightning/BTC/balance"
{
balance: res["offchain"]["local"].to_i / 1000 # msats to sats
}
end
end
end

View File

@@ -1,13 +0,0 @@
module BtcpayManager
class FetchOnchainWalletBalance < BtcpayManagerService
def call
res = get "stores/#{store_id}/payment-methods/onchain/BTC/wallet"
{
balance: (res["balance"].to_f * 100000000).to_i, # BTC to sats
unconfirmed_balance: (res["unconfirmedBalance"].to_f * 100000000).to_i,
confirmed_balance: (res["confirmedBalance"].to_f * 100000000).to_i
}
end
end
end

View File

@@ -1,24 +0,0 @@
#
# API Docs: https://docs.btcpayserver.org/API/Greenfield/v1/
#
class BtcpayManagerService < ApplicationService
attr_reader :base_url, :store_id, :auth_token
def initialize
@base_url = Setting.btcpay_api_url
@store_id = Setting.btcpay_store_id
@auth_token = Setting.btcpay_auth_token
end
private
def get(endpoint)
res = Faraday.get("#{base_url}/#{endpoint}", {}, {
"Content-Type" => "application/json",
"Accept" => "application/json",
"Authorization" => "token #{auth_token}"
})
JSON.parse(res.body)
end
end

View File

@@ -1,7 +1,7 @@
class CreateAccount < ApplicationService class CreateAccount < ApplicationService
def initialize(args) def initialize(args)
@username = args[:username] @username = args[:username]
@domain = args[:ou] || Setting.primary_domain @domain = args[:ou] || "kosmos.org"
@email = args[:email] @email = args[:email]
@password = args[:password] @password = args[:password]
@invitation = args[:invitation] @invitation = args[:invitation]

View File

@@ -1,17 +0,0 @@
module LdapManager
class FetchAvatar < LdapManagerService
def initialize(cn:, ou: nil)
@cn = cn
@ou = ou
end
def call
treebase = @ou ? "ou=#{@ou},cn=users,#{suffix}" : ldap_config["base"]
attributes = %w{ jpegPhoto }
filter = Net::LDAP::Filter.eq("cn", @cn)
entry = ldap_client.search(base: treebase, filter: filter, attributes: attributes).first
entry.try(:jpegPhoto) ? entry.jpegPhoto.first : nil
end
end
end

View File

@@ -1,27 +0,0 @@
require "image_processing/vips"
module LdapManager
class UpdateAvatar < LdapManagerService
def initialize(dn, file)
@dn = dn
@img_data = process(file)
end
def call
replace_attribute @dn, :jpegPhoto, @img_data
end
private
def process(file)
processed = ImageProcessing::Vips
.resize_to_fill(512, 512)
.source(file)
.convert("jpeg")
.saver(strip: true)
.call
Base64.strict_encode64 processed.read
end
end
end

View File

@@ -1,5 +1,2 @@
class LdapManagerService < LdapService class LdapManagerService < LdapService
def suffix
@suffix ||= ENV["LDAP_SUFFIX"] || "dc=kosmos,dc=org"
end
end end

View File

@@ -14,7 +14,7 @@ class LndhubV2 < Lndhub
end end
def create_account(payload={}) def create_account(payload={})
post "v2/users", payload, admin_token: Setting.lndhub_admin_token post "v2/users", payload, admin_token: Rails.application.credentials.lndhub[:admin_token]
end end
def create_invoice(payload) def create_invoice(payload)

View File

@@ -1,11 +0,0 @@
module NostrManager
class ValidateId < NostrManagerService
def initialize(event)
@event = Nostr::Event.new(**event)
end
def call
@event.id == Digest::SHA256.hexdigest(JSON.generate(@event.serialize))
end
end
end

View File

@@ -1,17 +0,0 @@
module NostrManager
class VerifySignature < NostrManagerService
def initialize(event)
@event = Nostr::Event.new(**event)
end
def call
Schnorr.check_sig!(
[@event.id].pack('H*'),
[@event.pubkey].pack('H*'),
[@event.sig].pack('H*')
)
rescue Schnorr::InvalidSignatureError
false
end
end
end

View File

@@ -1,4 +0,0 @@
require "nostr"
class NostrManagerService < ApplicationService
end

View File

@@ -12,7 +12,7 @@
<section class="sm:w-1/2 grid grid-cols-2 items-center gap-y-2"> <section class="sm:w-1/2 grid grid-cols-2 items-center gap-y-2">
<%= form.label :user_id %> <%= form.label :user_id %>
<%= form.collection_select :user_id, User.where(ou: Setting.primary_domain).order(:cn), :id, :cn, {} %> <%= form.collection_select :user_id, User.where(ou: "kosmos.org").order(:cn), :id, :cn, {} %>
<%= form.label :amount_sats, "Amount BTC (sats)" %> <%= form.label :amount_sats, "Amount BTC (sats)" %>
<%= form.number_field :amount_sats %> <%= form.number_field :amount_sats %>

View File

@@ -1,37 +0,0 @@
<h3>BTCPay Server</h3>
<ul role="list">
<%= render FormElements::FieldsetToggleComponent.new(
form: f,
attribute: :btcpay_enabled,
enabled: Setting.btcpay_enabled?,
title: "Enable BTCPay integration",
description: "BTCPay configuration present and features enabled"
) %>
<% if Setting.btcpay_enabled? %>
<%= render FormElements::FieldsetResettableSettingComponent.new(
key: :btcpay_api_url,
title: "API URL"
) %>
<%= render FormElements::FieldsetResettableSettingComponent.new(
key: :btcpay_store_id,
title: "Store ID"
) %>
<%= render FormElements::FieldsetResettableSettingComponent.new(
key: :btcpay_auth_token,
type: :password,
title: "Auth Token"
) %>
</ul>
</section>
<section>
<h3>REST API</h3>
<ul role="list">
<%= render FormElements::FieldsetToggleComponent.new(
form: f,
attribute: :btcpay_publish_wallet_balances,
enabled: Setting.btcpay_publish_wallet_balances?,
title: "Publish wallet balances",
description: "Publish the store's on-chain and Lightning wallet balances"
) %>
<% end %>
</ul>

View File

@@ -8,15 +8,16 @@
description: "Discourse configuration present and features enabled" description: "Discourse configuration present and features enabled"
) %> ) %>
<% if Setting.discourse_enabled? %> <% if Setting.discourse_enabled? %>
<%= render FormElements::FieldsetResettableSettingComponent.new( <%= render FormElements::FieldsetComponent.new(title: "Public URL") do %>
key: :discourse_public_url, <%= f.text_field :discourse_public_url,
title: "Public URL" value: Setting.discourse_public_url,
) %> class: "w-full", disabled: true %>
<%= render FormElements::FieldsetResettableSettingComponent.new( <% end %>
key: :discourse_connect_secret, <%= render FormElements::FieldsetComponent.new(title: "Connect secret") do %>
type: :password, <%= f.password_field :discourse_connect_secret,
title: "Connect secret" value: Setting.discourse_connect_secret,
) %> class: "w-full", disabled: true %>
<% end %>
<% end %> <% end %>
</ul> </ul>
<% if Setting.discourse_enabled? %> <% if Setting.discourse_enabled? %>
@@ -30,14 +31,14 @@
<input type="text" class="grow" disabled="disabled" <input type="text" class="grow" disabled="disabled"
value="https://<%= Setting.accounts_domain %>/discourse/connect" value="https://<%= Setting.accounts_domain %>/discourse/connect"
data-clipboard-target="source" /> data-clipboard-target="source" />
<button class="btn-md btn-icon btn-outline shrink-0" <button class="btn-md btn-icon btn-blue shrink-0"
data-clipboard-target="trigger" data-action="clipboard#copy" data-clipboard-target="trigger" data-action="clipboard#copy"
title="Copy to clipboard"> title="Copy to clipboard">
<span class="content-initial"> <span class="content-initial">
<%= render partial: "icons/copy", locals: { custom_class: "text-blue-600 h-4 w-4 inline" } %> <%= render partial: "icons/copy", locals: { custom_class: "text-white h-4 w-4 inline" } %>
</span> </span>
<span class="content-active hidden"> <span class="content-active hidden">
<%= render partial: "icons/check", locals: { custom_class: "text-blue-600 h-4 w-4 inline" } %> <%= render partial: "icons/check", locals: { custom_class: "text-white h-4 w-4 inline" } %>
</span> </span>
</button> </button>
</li> </li>

View File

@@ -1,16 +0,0 @@
<h3>Drone CI</h3>
<ul role="list">
<%= render FormElements::FieldsetToggleComponent.new(
form: f,
attribute: :droneci_enabled,
enabled: Setting.droneci_enabled?,
title: "Enable Drone CI integration",
description: "Drone CI configuration present and features enabled"
) %>
<% if Setting.droneci_enabled? %>
<%= render FormElements::FieldsetResettableSettingComponent.new(
key: :droneci_public_url,
title: "Public URL"
) %>
<% end %>
</ul>

View File

@@ -8,14 +8,16 @@
description: "ejabberd configuration present and features enabled" description: "ejabberd configuration present and features enabled"
) %> ) %>
<% if Setting.ejabberd_enabled? %> <% if Setting.ejabberd_enabled? %>
<%= render FormElements::FieldsetResettableSettingComponent.new( <%= render FormElements::FieldsetComponent.new(title: "API URL") do %>
key: :ejabberd_api_url, <%= f.text_field :ejabberd_api_url,
title: "API URL" value: Setting.ejabberd_api_url,
) %> class: "w-full", disabled: true %>
<%= render FormElements::FieldsetResettableSettingComponent.new( <% end %>
key: :ejabberd_admin_url, <%= render FormElements::FieldsetComponent.new(title: "Admin URL") do %>
title: "Admin URL" <%= f.text_field :ejabberd_admin_url,
) %> value: Setting.ejabberd_admin_url,
class: "w-full", disabled: true %>
<% end %>
</ul> </ul>
<h3 class="mt-10">User default settings</h3> <h3 class="mt-10">User default settings</h3>
<ul role="list"> <ul role="list">
@@ -35,24 +37,12 @@
title: "Auto-join default rooms", title: "Auto-join default rooms",
description: "Automatically join above default rooms in chat clients" description: "Automatically join above default rooms in chat clients"
) %> ) %>
<%= render FormElements::FieldsetResettableSettingComponent.new( <%= render FormElements::FieldsetComponent.new(
key: :ejabberd_buddy_roster,
title: "Contact roster name", title: "Contact roster name",
description: "Used when exchanging contacts after signup from invitation" description: "Used when exchanging contacts after signup from invitation"
) %>
</ul>
<h3 class="mt-10">Notifications</h3>
<ul role="list">
<%= render FormElements::FieldsetComponent.new(
title: "From address",
description: "Address (JID) of the account notifications are sent from",
resettable: Setting.get_field(:xmpp_notifications_from_address)[:default] != Setting.xmpp_notifications_from_address
) do %> ) do %>
<%= f.text_field :xmpp_notifications_from_address, <%= f.text_field :ejabberd_buddy_roster,
value: Setting.xmpp_notifications_from_address, value: Setting.ejabberd_buddy_roster,
data: {
:'default-value' => Setting.get_field(:xmpp_notifications_from_address)[:default]
},
class: "w-full" %> class: "w-full" %>
<% end %> <% end %>
<% end %> <% end %>

View File

@@ -8,9 +8,10 @@
description: "Gitea configuration present and features enabled" description: "Gitea configuration present and features enabled"
) %> ) %>
<% if Setting.gitea_enabled? %> <% if Setting.gitea_enabled? %>
<%= render FormElements::FieldsetResettableSettingComponent.new( <%= render FormElements::FieldsetComponent.new(title: "Public URL") do %>
key: :gitea_public_url, <%= f.text_field :gitea_public_url,
title: "Public URL" value: Setting.gitea_public_url,
) %> class: "w-full", disabled: true %>
<% end %>
<% end %> <% end %>
</ul> </ul>

View File

@@ -8,36 +8,31 @@
description: "LNDHub configuration present and wallet features enabled" description: "LNDHub configuration present and wallet features enabled"
) %> ) %>
<% if Setting.lndhub_enabled? %> <% if Setting.lndhub_enabled? %>
<%= render FormElements::FieldsetResettableSettingComponent.new( <%= render FormElements::FieldsetComponent.new(title: "API URL") do %>
key: :lndhub_api_url, <%= f.text_field :lndhub_api_url,
title: "API URL" value: Setting.lndhub_api_url,
) %> class: "w-full", disabled: true %>
<%= render FormElements::FieldsetResettableSettingComponent.new( <% end %>
key: :lndhub_admin_token, <% end %>
type: :password, <%= render FormElements::FieldsetToggleComponent.new(
title: "Admin token", form: f,
description: "Auth token for creating new lndhub accounts" attribute: :lndhub_admin_enabled,
) %> enabled: Setting.lndhub_admin_enabled?,
<%= render FormElements::FieldsetToggleComponent.new( title: "Enable LNDHub admin panel",
form: f, description: "LNDHub database configuration present and admin panel enabled"
attribute: :lndhub_admin_enabled, ) %>
enabled: Setting.lndhub_admin_enabled?, <%= render FormElements::FieldsetToggleComponent.new(
title: "Enable LNDHub admin panel", form: f,
description: "LNDHub database configuration present and admin panel enabled" attribute: :lndhub_keysend_enabled,
) %> enabled: Setting.lndhub_keysend_enabled?,
<%= render FormElements::FieldsetToggleComponent.new( title: "Enable keysend payments",
form: f, description: "Allow users to receive invoice-less payments to their Lightning Address"
attribute: :lndhub_keysend_enabled, ) %>
enabled: Setting.lndhub_keysend_enabled?, <% if Setting.lndhub_keysend_enabled? %>
title: "Enable keysend payments", <%= render FormElements::FieldsetComponent.new(title: "Public key", description: "The public key of the Lightning node used by LNDHub") do %>
description: "Allow users to receive invoice-less payments to their Lightning Address" <%= f.text_field :lndhub_public_key,
) %> value: Setting.lndhub_public_key,
<% if Setting.lndhub_keysend_enabled? %> class: "w-full", disabled: true %>
<%= render FormElements::FieldsetResettableSettingComponent.new(
key: :lndhub_public_key,
title: "Public key",
description: "The public key of the Lightning node used by LNDHub"
) %>
<% end %> <% end %>
<% end %> <% end %>
</ul> </ul>

View File

@@ -8,13 +8,10 @@
description: "Mastodon configuration present and features enabled" description: "Mastodon configuration present and features enabled"
) %> ) %>
<% if Setting.mastodon_enabled? %> <% if Setting.mastodon_enabled? %>
<%= render FormElements::FieldsetResettableSettingComponent.new( <%= render FormElements::FieldsetComponent.new(title: "Public URL") do %>
key: :mastodon_public_url, <%= f.text_field :mastodon_public_url,
title: "Public URL" value: Setting.mastodon_public_url,
) %> class: "w-full", disabled: true %>
<%= render FormElements::FieldsetResettableSettingComponent.new( <% end %>
key: :mastodon_address_domain,
title: "User address domain"
) %>
<% end %> <% end %>
</ul> </ul>

View File

@@ -8,9 +8,10 @@
description: "MediaWiki configuration present and features enabled" description: "MediaWiki configuration present and features enabled"
) %> ) %>
<% if Setting.mediawiki_enabled? %> <% if Setting.mediawiki_enabled? %>
<%= render FormElements::FieldsetResettableSettingComponent.new( <%= render FormElements::FieldsetComponent.new(title: "Public URL") do %>
key: :mediawiki_public_url, <%= f.text_field :mediawiki_public_url,
title: "Public URL" value: Setting.mediawiki_public_url,
) %> class: "w-full", disabled: true %>
<% end %>
<% end %> <% end %>
</ul> </ul>

View File

@@ -1,5 +1,4 @@
<h3>RemoteStorage</h3> <h3>RemoteStorage</h3>
<p class="text-red-600 mb-8">Feature currently in development.</p>
<ul role="list"> <ul role="list">
<%= render FormElements::FieldsetToggleComponent.new( <%= render FormElements::FieldsetToggleComponent.new(
form: f, form: f,
@@ -9,13 +8,10 @@
description: "RemoteStorage configuration present and features enabled" description: "RemoteStorage configuration present and features enabled"
) %> ) %>
<% if Setting.remotestorage_enabled? %> <% if Setting.remotestorage_enabled? %>
<%= render FormElements::FieldsetResettableSettingComponent.new( <%= render FormElements::FieldsetComponent.new(title: "Storage URL") do %>
key: :rs_storage_url, <%= f.text_field :rs_storage_url,
title: "Storage Base URL" value: Setting.rs_storage_url,
) %> class: "w-full", disabled: true %>
<%= render FormElements::FieldsetResettableSettingComponent.new( <% end %>
key: :rs_redis_url,
title: "Redis URL"
) %>
<% end %> <% end %>
</ul> </ul>

View File

@@ -63,10 +63,6 @@
</section> </section>
<section class="sm:flex-1 sm:pt-0"> <section class="sm:flex-1 sm:pt-0">
<h3>LDAP<h3>
<p>
<img src="data:image/jpeg;base64,<%= @avatar %>" class="h-48 w-48" />
</p>
<!-- <h3>Actions</h3> --> <!-- <h3>Actions</h3> -->
</section> </section>
</div> </div>

View File

@@ -1,6 +1,10 @@
<%= render HeaderComponent.new(title: "Contributions") %> <%# <%= render HeaderComponent.new(title: "Contributions") %>
<%= render HeaderWithTabsComponent.new(
# title: "Contributions",
tabnav_partial: "shared/tabnav_contributions"
) %>
<%= render MainWithTabnavComponent.new(tabnav_partial: "shared/tabnav_contributions") do %> <%= render MainSimpleComponent.new do %>
<section> <section>
<% if @donations.any? %> <% if @donations.any? %>
<p class="mb-12"> <p class="mb-12">

View File

@@ -1,6 +1,9 @@
<%= render HeaderComponent.new(title: "Contributions") %> <%= render HeaderWithTabsComponent.new(
# title: "Contributions",
tabnav_partial: "shared/tabnav_contributions"
) %>
<%= render MainWithTabnavComponent.new(tabnav_partial: "shared/tabnav_contributions") do %> <%= render MainSimpleComponent.new do %>
<section> <section>
<p class="mb-8"> <p class="mb-8">
Project contributions are how we develop and run all Kosmos software and Project contributions are how we develop and run all Kosmos software and

View File

@@ -7,85 +7,73 @@
services: services:
</p> </p>
<div class="services grid grid-cols-1 sm:grid-cols-2 gap-4 sm:gap-6"> <div class="services grid grid-cols-1 sm:grid-cols-2 gap-4 sm:gap-6">
<% if Setting.ejabberd_enabled? %> <div class="border border-gray-300 rounded-md hover:border-gray-400
<div class="border border-gray-300 rounded-md hover:border-gray-400 bg-cover bg-[center_top_-50px] bg-no-repeat
bg-cover bg-[center_top_-50px] bg-no-repeat bg-[url(/img/logos/icon_xmpp.svg)]">
bg-[url(/img/logos/icon_xmpp.svg)]"> <%= link_to "https://wiki.kosmos.org/Services:Chat",
<%= link_to services_chat_path, class: "block h-full px-6 py-6 rounded-md" do %>
class: "block h-full px-6 py-6 rounded-md" do %> <h3 class="mb-3.5">Chat</h3>
<h3 class="mb-3.5">Chat</h3> <p class="text-gray-600">
<p class="text-gray-600"> Federated chat rooms and instant messaging
Federated chat rooms and instant messaging </p>
</p> <% end %>
<% end %> </div>
</div> <div class="border border-gray-300 rounded-md hover:border-gray-400
<% end %> bg-[length:95%] bg-center bg-no-repeat
<% if Setting.mastodon_enabled? %> bg-[url(/img/logos/icon_discourse.svg)]">
<div class="border border-gray-300 rounded-md hover:border-gray-400 <%= link_to "#{Setting.discourse_public_url}/session/sso?return_path=/",
bg-[length:80%] bg-[right_top_-30px] bg-no-repeat class: "block h-full px-6 py-6 rounded-md" do %>
bg-[url(/img/logos/icon_mastodon.svg)]"> <h3 class="mb-3.5">Discourse</h3>
<%= link_to services_mastodon_path, class: "block h-full px-6 py-6 rounded-md" do %> <p class="text-gray-600">
<h3 class="mb-3.5">Mastodon</h3> Kosmos community forums and user support/help site
<p class="text-gray-600"> </p>
Your account on the Open Social Web <% end %>
</p> </div>
<% end %> <div class="border border-gray-300 rounded-md hover:border-gray-400
</div> bg-cover bg-[center_top_-20px] bg-no-repeat
<% end %> bg-[url(/img/logos/icon_mediawiki.svg)]">
<% if Setting.discourse_enabled? %> <%= link_to "https://wiki.kosmos.org",
<div class="border border-gray-300 rounded-md hover:border-gray-400 class: "block h-full px-6 py-6 rounded-md" do %>
bg-[length:95%] bg-center bg-no-repeat <h3 class="mb-3.5">Wiki</h3>
bg-[url(/img/logos/icon_discourse.svg)]"> <p class="text-gray-600">
<%= link_to "#{Setting.discourse_public_url}/session/sso?return_path=/", Kosmos documentation and knowledge base
class: "block h-full px-6 py-6 rounded-md" do %> </p>
<h3 class="mb-3.5">Discourse</h3> <% end %>
<p class="text-gray-600"> </div>
Kosmos community forums and user support/help site <div class="border border-gray-300 rounded-md hover:border-gray-400
</p> bg-cover bg-center sm:bg-[center_top_-140px] bg-no-repeat
<% end %> bg-[url(/img/logos/icon_lightning.svg)]">
</div> <%= link_to services_lightning_index_path,
<% end %> class: "block h-full px-6 py-6 rounded-md" do %>
<% if Setting.lndhub_enabled? %> <h3 class="mb-3.5">Lightning Network</h3>
<div class="border border-gray-300 rounded-md hover:border-gray-400 <p class="text-gray-600">
bg-cover bg-center sm:bg-[center_top_-140px] bg-no-repeat Send and receive sats over the Bitcoin Lightning Network
bg-[url(/img/logos/icon_lightning.svg)]"> </p>
<%= link_to services_lightning_index_path, <% end %>
class: "block h-full px-6 py-6 rounded-md" do %> </div>
<h3 class="mb-3.5">Lightning Network</h3> <div class="border border-gray-300 rounded-md hover:border-gray-400
<p class="text-gray-600"> bg-cover bg-center bg-no-repeat
Send and receive sats over the Bitcoin Lightning Network bg-[url(/img/logos/icon_gitea.png)]">
</p> <%= link_to "https://gitea.kosmos.org",
<% end %> class: "block h-full px-6 py-6 rounded-md" do %>
</div> <h3 class="mb-3.5">Gitea</h3>
<% end %> <p class="text-gray-600">
<% if Setting.gitea_enabled? %> Code hosting and collaboration for software projects
<div class="border border-gray-300 rounded-md hover:border-gray-400 </p>
bg-cover bg-center bg-no-repeat <% end %>
bg-[url(/img/logos/icon_gitea.png)]"> </div>
<%= link_to Setting.gitea_public_url, <div class="border border-gray-300 rounded-md hover:border-gray-400
class: "block h-full px-6 py-6 rounded-md" do %> bg-cover bg-[center_top_-70px] bg-no-repeat
<h3 class="mb-3.5">Gitea</h3> bg-[url(/img/logos/icon_droneci.svg)]">
<p class="text-gray-600"> <%= link_to "https://drone.kosmos.org",
Code hosting and collaboration for software projects class: "block h-full px-6 py-6 rounded-md" do %>
</p> <h3 class="mb-3.5">Drone CI</h3>
<% end %> <p class="text-gray-600">
</div> Continuous integration for software projects on Gitea
<% end %> </p>
<% if Setting.droneci_enabled? %> <% end %>
<div class="border border-gray-300 rounded-md hover:border-gray-400 </div>
bg-cover bg-[center_top_-70px] bg-no-repeat <% if Setting.remotestorage_enabled? && Flipper.enabled?(:remotestorage, current_user) %>
bg-[url(/img/logos/icon_droneci.svg)]">
<%= link_to Setting.droneci_public_url,
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>
<% end %>
<% if Setting.remotestorage_enabled? &&
Flipper.enabled?(:remotestorage, current_user) %>
<div class="border border-gray-300 rounded-md hover:border-gray-400"> <div class="border border-gray-300 rounded-md hover:border-gray-400">
<%= link_to services_storage_path, <%= link_to services_storage_path,
class: "block h-full px-6 py-6 rounded-md" do %> class: "block h-full px-6 py-6 rounded-md" do %>
@@ -96,19 +84,16 @@
<% end %> <% end %>
</div> </div>
<% end %> <% end %>
<% if Setting.mediawiki_enabled? %> <!-- <div class="border border&#45;gray&#45;300 rounded&#45;md hover:border&#45;gray&#45;400 -->
<div class="border border-gray-300 rounded-md hover:border-gray-400 <!-- bg&#45;[length:80%] bg&#45;[right_top_&#45;30px] bg&#45;no&#45;repeat -->
bg-cover bg-[center_top_-20px] bg-no-repeat <!-- bg&#45;[url(/img/logos/icon_mastodon.svg)]"> -->
bg-[url(/img/logos/icon_mediawiki.svg)]"> <!-- <%= link_to "https://kosmos.social", class: "block h&#45;full px&#45;6 py&#45;6 rounded&#45;md" do %> -->
<%= link_to Setting.mediawiki_public_url, <!-- <h3 class="mb&#45;3.5">Mastodon</h3> -->
class: "block h-full px-6 py-6 rounded-md" do %> <!-- <p class="text&#45;gray&#45;400"> -->
<h3 class="mb-3.5">Wiki</h3> <!-- Your account on the Open Social Web -->
<p class="text-gray-600"> <!-- </p> -->
Kosmos documentation and knowledge base <!-- <% end %> -->
</p> <!-- </div> -->
<% end %>
</div>
<% end %>
</div> </div>
</section> </section>
<% end %> <% end %>

View File

@@ -10,7 +10,7 @@
<p class="flex gap-2 items-center"> <p class="flex gap-2 items-center">
<%= f.text_field :cn, autofocus: true, autocomplete: "username", <%= f.text_field :cn, autofocus: true, autocomplete: "username",
required: true, class: "relative grow"%> required: true, class: "relative grow"%>
<span class="relative shrink-0 text-gray-500">@ <%= Setting.primary_domain %></span> <span class="relative shrink-0 text-gray-500">@ kosmos.org</span>
</p> </p>
</div> </div>
<p> <p>

View File

@@ -12,17 +12,15 @@
<div class="mb-6"> <div class="mb-6">
<%= f.label :cn, 'User', class: 'block mb-2 font-bold' %> <%= f.label :cn, 'User', class: 'block mb-2 font-bold' %>
<p class="flex gap-2 items-center"> <p class="flex gap-2 items-center">
<%= f.text_field :cn, value: h(params[:cn]), <%= f.text_field :cn, autofocus: true, autocomplete: "username",
autofocus: params[:cn].blank?, autocomplete: "username",
required: true, class: "relative grow", tabindex: "1" %> required: true, class: "relative grow", tabindex: "1" %>
<span class="relative shrink-0 text-gray-500">@ <%= Setting.primary_domain %></span> <span class="relative shrink-0 text-gray-500">@ kosmos.org</span>
</p> </p>
</div> </div>
<p class="mb-8"> <p class="mb-8">
<%= f.label :password, class: 'block mb-2 font-bold' %> <%= f.label :password, class: 'block mb-2 font-bold' %>
<%= f.password_field :password, autocomplete: "current-password", <%= f.password_field :password, autocomplete: "current-password",
autofocus: params[:cn].present?, required: true, required: true, class: "w-full", tabindex: "2" %>
class: "w-full", tabindex: "2" %>
</p> </p>
<%= tag.div class: "flex items-center mb-8 gap-x-3", data: { <%= tag.div class: "flex items-center mb-8 gap-x-3", data: {

View File

@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-alert-triangle <%= custom_class %>"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path><line x1="12" y1="9" x2="12" y2="13"></line><line x1="12" y1="17" x2="12.01" y2="17"></line></svg> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-alert-triangle"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path><line x1="12" y1="9" x2="12" y2="13"></line><line x1="12" y1="17" x2="12.01" y2="17"></line></svg>

Before

Width:  |  Height:  |  Size: 445 B

After

Width:  |  Height:  |  Size: 424 B

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 512 512" fill="currentColor" stroke="currentColor" stroke-width="2" class="<%= custom_class %>"><path d="M475.31 364.144L288 256l187.31-108.144c5.74-3.314 7.706-10.653 4.392-16.392l-4-6.928c-3.314-5.74-10.653-7.706-16.392-4.392L272 228.287V12c0-6.627-5.373-12-12-12h-8c-6.627 0-12 5.373-12 12v216.287L52.69 120.144c-5.74-3.314-13.079-1.347-16.392 4.392l-4 6.928c-3.314 5.74-1.347 13.079 4.392 16.392L224 256 36.69 364.144c-5.74 3.314-7.706 10.653-4.392 16.392l4 6.928c3.314 5.74 10.653 7.706 16.392 4.392L240 283.713V500c0 6.627 5.373 12 12 12h8c6.627 0 12-5.373 12-12V283.713l187.31 108.143c5.74 3.314 13.079 1.347 16.392-4.392l4-6.928c3.314-5.74 1.347-13.079-4.392-16.392z"/></svg>

Before

Width:  |  Height:  |  Size: 760 B

View File

@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-folder <%= custom_class %>"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path></svg> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-folder"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path></svg>

Before

Width:  |  Height:  |  Size: 331 B

After

Width:  |  Height:  |  Size: 311 B

View File

@@ -1,11 +0,0 @@
<svg class="icon-qr-code <%= custom_class %>" fill="currentColor" width="90" height="90" version="1.1" viewBox="0 0 90 90" xmlns="http://www.w3.org/2000/svg">
<path id="path2" d="m22.014 22.612c0-2.5389 2.0586-4.5976 4.5976-4.5976h9.1937c2.539 0 4.5976 2.0587 4.5976 4.5976v6.1937c0 2.539-2.0586 4.5976-4.5976 4.5976h-9.1937c-2.539 0-4.5976-2.0586-4.5976-4.5976z"/>
<path id="path4" d="m22.014 61.598c0-2.539 2.0586-4.5976 4.5976-4.5976h9.1937c2.539 0 4.5976 2.0586 4.5976 4.5976v6.1937c0 2.539-2.0586 4.5976-4.5976 4.5976h-9.1937c-2.539 0-4.5976-2.0586-4.5976-4.5976z"/>
<path id="path6" d="m50 22.612c0-2.5389 2.0586-4.5976 4.5976-4.5976h9.1937c2.539 0 4.5976 2.0587 4.5976 4.5976v6.1937c0 2.539-2.0586 4.5976-4.5976 4.5976h-9.1937c-2.539 0-4.5976-2.0586-4.5976-4.5976z"/>
<path id="path8" d="m50 61.598c0-2.539 2.0586-4.5976 4.5976-4.5976h9.1937c2.539 0 4.5976 2.0586 4.5976 4.5976v6.1937c0 2.539-2.0586 4.5976-4.5976 4.5976h-9.1937c-2.539 0-4.5976-2.0586-4.5976-4.5976z"/>
<path id="path10" d="m8.85 45c0-1.7397 1.4103-3.15 3.15-3.15h66.5c1.7397 0 3.15 1.4103 3.15 3.15s-1.4103 3.15-3.15 3.15h-66.5c-1.7397 0-3.15-1.4103-3.15-3.15z" clip-rule="evenodd" fill-rule="evenodd"/>
<path id="path12" d="m11.566 0c-6.3876 0-11.566 5.1782-11.566 11.566v14.627c0 1.7713 1.4359 3.2073 3.2072 3.2073s3.2072-1.436 3.2072-3.2073v-14.627c0-2.845 2.3064-5.1514 5.1514-5.1514h14.627c1.7713 0 3.2073-1.4359 3.2073-3.2072s-1.436-3.2072-3.2073-3.2072z" clip-rule="evenodd" fill-rule="evenodd"/>
<path id="path14" d="m11.566 90c-6.3876 0-11.566-5.1782-11.566-11.566v-14.628c0-1.7713 1.4359-3.2072 3.2072-3.2072s3.2072 1.4359 3.2072 3.2072v14.628c0 2.845 2.3064 5.1513 5.1514 5.1513h14.627c1.7713 0 3.2073 1.436 3.2073 3.2073 0 1.7712-1.436 3.2072-3.2073 3.2072z" clip-rule="evenodd" fill-rule="evenodd"/>
<path id="path16" d="m78.434 0c6.3876 0 11.566 5.1782 11.566 11.566v14.627c0 1.7713-1.4359 3.2073-3.2072 3.2073s-3.2072-1.436-3.2072-3.2073v-14.627c0-2.845-2.3064-5.1514-5.1514-5.1514h-14.627c-1.7713 0-3.2073-1.4359-3.2073-3.2072s1.436-3.2072 3.2073-3.2072z" clip-rule="evenodd" fill-rule="evenodd"/>
<path id="path18" d="m78.434 90c6.3876 0 11.566-5.1782 11.566-11.566v-14.628c0-1.7713-1.4359-3.2072-3.2072-3.2072s-3.2072 1.4359-3.2072 3.2072v14.628c0 2.845-2.3064 5.1513-5.1514 5.1513h-14.627c-1.7713 0-3.2073 1.436-3.2073 3.2073 0 1.7712 1.436 3.2072 3.2073 3.2072z" clip-rule="evenodd" fill-rule="evenodd"/>
</svg>

Before

Width:  |  Height:  |  Size: 2.4 KiB

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48" viewBox="0 96 960 960" width="48" class="material-science <%= custom_class %>" fill="currentColor"><path d="M172 936q-41.777 0-59.388-39Q95 858 124 826l248-280V276h-52q-12.75 0-21.375-8.675-8.625-8.676-8.625-21.5 0-12.825 8.625-21.325T320 216h320q12.75 0 21.375 8.675 8.625 8.676 8.625 21.5 0 12.825-8.625 21.325T640 276h-52v270l248 280q29 32 11.388 71-17.611 39-59.388 39H172Zm-12-60h640L528 568V276h-96v292L160 876Zm318-300Z"/></svg>

Before

Width:  |  Height:  |  Size: 488 B

View File

@@ -8,27 +8,20 @@
</p> </p>
<ul class="md:w-3/4"> <ul class="md:w-3/4">
<% @invitations_unused.each do |invitation| %> <% @invitations_unused.each do |invitation| %>
<li class="mb-3 flex gap-1" <li class="font-mono mb-2 flex gap-1" data-controller="clipboard">
data-controller="clipboard modal" <input type="text" disabled class="relative grow"
data-action="keydown.esc->modal#close">
<input type="text" disabled class="relative grow font-mono"
value="<%= invitation_url(invitation.token) %>" value="<%= invitation_url(invitation.token) %>"
data-clipboard-target="source" /> data-clipboard-target="source" />
<button class="btn-md btn-icon btn-outline shrink-0 w-auto" <button id="copy-user-address" class="btn-md btn-icon btn-blue shrink-0 w-auto"
data-clipboard-target="trigger" data-action="clipboard#copy" data-clipboard-target="trigger" data-action="clipboard#copy"
title="Copy to clipboard"> title="Copy to clipboard">
<span class="content-initial"> <span class="content-initial">
<%= render partial: "icons/copy", locals: { custom_class: "text-blue-600 h-4 w-4 inline" } %> <%= render partial: "icons/copy", locals: { custom_class: "text-white h-4 w-4 inline" } %>
</span> </span>
<span class="content-active hidden"> <span class="content-active hidden">
<%= render partial: "icons/check", locals: { custom_class: "text-blue-600 h-4 w-4 inline" } %> <%= render partial: "icons/check", locals: { custom_class: "text-white h-4 w-4 inline" } %>
</span> </span>
</button> </button>
<button class="btn-md btn-icon btn-outline shrink-0 w-auto"
data-action="click->modal#open" title="Show QR code">
<%= render partial: "icons/qr_code", locals: { custom_class: "text-blue-600 h-4 w-4 inline" } %>
</button>
<%= render QrCodeModalComponent.new(qr_content: invitation_url(invitation.token)) %>
</li> </li>
<% end %> <% end %>
</ul> </ul>

View File

@@ -1,58 +0,0 @@
<%= render HeaderCompactComponent.new(title: "Storage") %>
<%= render MainCompactComponent.new do %>
<section class="permissions">
<p class="mb-8">
The app on
<%= link_to @client_id, "https://#{@client_id}", class: "ks-text-link" %>
is asking for access to these folders:
</p>
<% if @root_access_requested %>
<p class="scope text-lg">
<span class="text-red-700">
<%= render partial: "icons/alert-triangle",
locals: { custom_class: "inline-block align-bottom mr-1.5" } %>
All files and directories
</span>
<% if (@scopes & [":r"]).any? %>
<span class="text-sm text-gray-500">(read only)</span>
<% end %>
</p>
<% else %>
<% @scopes.each do |scope| %>
<p class="scope text-gray-600">
<span class="text-lg">
<%= render partial: "icons/folder",
locals: { custom_class: "inline-block align-bottom mr-1.5" } %>
<%= scope_name(scope) %>
</span>
<% if scope_permissions(scope) == "r" %>
<span>(read only)</span>
<% end %>
</p>
<% end %>
<% end %>
<%= form_with(url: rs_oauth_path, method: :post, data: { turbo: false }) do |f| %>
<%= f.hidden_field :redirect_uri, value: @redirect_uri %>
<%= f.hidden_field :scope, value: @scopes.join(" ") %>
<%= f.hidden_field :user_id, value: @user.id %>
<%= f.hidden_field :client_id, value: @client_id %>
<%= f.hidden_field :state, value: @state %>
<p class="mt-8 mb-6">
<%= f.label :expire_at, "Permission expires:", class: "mr-1.5" %>
<%= f.select :expire_at, options_for_select(@expire_at_dates) %>
</p>
<p class="text-sm text-gray-500">
You can revoke access for this app at any time on your storage dashboard.
</p>
<p class="mt-8 flex flex-col sm:flex-row gap-3 sm:gap-2 sm:justify-items-stretch">
<%= f.submit "Allow",
class: "btn-md btn-blue w-full sm:order-last sm:grow",
data: { disable_with: "Saving..." } %>
<%= link_to "Deny", @denial_url, class: "btn-md btn-gray text-red-700 w-full sm:grow" %>
</div>
<% end %>
</section>
<% end %>

View File

@@ -1,199 +0,0 @@
<%= render HeaderComponent.new(title: "Chat") %>
<%= render MainSimpleComponent.new do %>
<section>
<p class="mb-6">
Chat with anyone on the open Jabber (XMPP) network. Message people directly, or
join public channels or private rooms.
</p>
</section>
<section data-controller="modal" data-action="keydown.esc->modal#close">
<h3>Your Chat Address</h3>
<p class="mb-6">
When you exchange contacts with people, give them your
address, or add them using their address:
</p>
<p data-controller="clipboard" class="flex gap-1 sm:w-2/5">
<input type="text" id="user_address" class="grow"
value=<%= current_user.address %> disabled="disabled"
data-clipboard-target="source" />
<button id="copy-user-address" class="btn-md btn-icon btn-outline shrink-0"
data-clipboard-target="trigger" data-action="clipboard#copy"
title="Copy to clipboard">
<span class="content-initial">
<%= render partial: "icons/copy", locals: { custom_class: "text-blue-600 h-4 w-4 inline" } %>
</span>
<span class="content-active hidden">
<%= render partial: "icons/check", locals: { custom_class: "text-blue-600 h-4 w-4 inline" } %>
</span>
</button>
<button class="btn-md btn-icon btn-outline shrink-0 w-auto"
data-action="click->modal#open" title="Show QR code">
<%= render partial: "icons/qr_code", locals: { custom_class: "text-blue-600 h-4 w-4 inline" } %>
</button>
</p>
<%= render QrCodeModalComponent.new(qr_content: "xmpp:"+current_user.address) %>
</section>
<section>
<h3>Chat Apps</h3>
<p>
Use your account with many different apps, and on any devices you wish!
When opening an app for the first time, just enter your user address and
password to log in.
</p>
</section>
<section>
<h3>Recommended Apps</h3>
<div data-controller="tabs"
data-tabs-active-tab-class="-mb-px border-gray-200 border-l border-t border-r rounded-t text-indigo-600 hover:text-indigo-600"
data-tabs-inactive-tab-class="text-gray-500 hover:text-gray-700"
class="mb-12">
<select data-action="tabs#change" data-tabs-target="select"
class="block w-full mb-8 sm:hidden">
<optgroup label="Mobile">
<option>Android</option>
<option>iOS</option>
</optgroup>
<optgroup label="Desktop">
<option>Linux</option>
<option>Windows</option>
<option>macOS</option>
</optgroup>
</select>
<ul class="hidden sm:flex list-reset mb-8 border-gray-200 border-b">
<li class="mr-2" data-tabs-target="tab" data-action="click->tabs#change:prevent">
<a href="#" class="bg-white inline-block py-2 px-4 font-semibold no-underline">
Android
</a>
</li>
<li class="mr-2" data-tabs-target="tab" data-action="click->tabs#change:prevent">
<a href="#" class="bg-white inline-block py-2 px-4 font-semibold no-underline">
iOS
</a>
</li>
<li class="mr-2" data-tabs-target="tab" data-action="click->tabs#change:prevent">
<a href="#" class="bg-white inline-block py-2 px-4 font-semibold no-underline">
Linux
</a>
</li>
<li class="mr-2" data-tabs-target="tab" data-action="click->tabs#change:prevent">
<a href="#" class="bg-white inline-block py-2 px-4 font-semibold no-underline">
Windows
</a>
</li>
<li class="mr-2" data-tabs-target="tab" data-action="click->tabs#change:prevent">
<a href="#" class="bg-white inline-block py-2 px-4 font-semibold no-underline">
macOS
</a>
</li>
<!-- <li class="mr&#45;2" data&#45;tabs&#45;target="tab" data&#45;action="click&#45;>tabs#change"> -->
<!-- <a href="#" class="bg&#45;white inline&#45;block py&#45;2 px&#45;4 font&#45;semibold no&#45;underline"> -->
<!-- Web -->
<!-- </a> -->
<!-- </li> -->
</ul>
<div id="apps-android" class="hidden grid grid-cols-1 gap-6"
data-tabs-target="panel">
<%= render AppInfoComponent.new(
name: "Conversations",
description: "The gold standard for Jabber on mobile devices",
icon_path: "/img/logos/icon_conversations.png",
links: [
["Website", "https://conversations.im"],
["Google Play", "https://play.google.com/store/apps/details?id=eu.siacs.conversations"],
["F-Droid", "https://f-droid.org/en/packages/eu.siacs.conversations/"],
]
) %>
</div>
<div id="apps-ios" class="hidden grid grid-cols-1 gap-6"
data-tabs-target="panel">
<%= render AppInfoComponent.new(
name: "Siskin IM",
description: "Lightweight and powerful chat app for iPhone and iPad",
icon_path: "/img/logos/logo_siskin.png",
links: [
["Website", "https://siskin.im"],
["App Store", "https://apps.apple.com/us/app/tigase-messenger/id1153516838"]
]
) %>
<%= render AppInfoComponent.new(
name: "Monal",
description: "A chat app for iOS, iPadOS, and macOS",
icon_path: "/img/logos/icon_monal.svg",
icon_fill_box: true,
links: [
["Website", "https://monal-im.org"],
["App Store", "https://apps.apple.com/app/id317711500"]
]
) %>
</div>
<div id="apps-linux" class="hidden grid grid-cols-1 gap-6"
data-tabs-target="panel">
<%= render AppInfoComponent.new(
name: "Dino",
description: "A modern and simple chat app for Linux (good for GNOME)",
icon_path: "/img/logos/icon_dino.svg",
links: [
["Website", "https://dino.im"],
["Install from package", "https://github.com/dino/dino/wiki/Distribution-Packages"]
]
) %>
<%= render AppInfoComponent.new(
name: "Kaidan",
description: "A fairly new, user-friendly chat app for all devices (good for KDE)",
icon_path: "/img/logos/icon_kaidan.svg",
links: [
["Website", "https://kaidan.im"],
]
) %>
<%= render AppInfoComponent.new(
name: "Gajim",
description: "A fully-featured chat app for Linux and Windows",
icon_path: "/img/logos/icon_gajim.png",
links: [
["Website", "https://gajim.org/"]
]
) %>
</div>
<div id="apps-windows" class="hidden grid grid-cols-1 gap-6"
data-tabs-target="panel">
<%= render AppInfoComponent.new(
name: "Gajim",
description: "A fully-featured chat app for Linux and Windows",
icon_path: "/img/logos/icon_gajim.png",
links: [
["Website", "https://gajim.org/"],
["Microsoft Store", "https://apps.microsoft.com/store/detail/9PGGF6HD43F9?launch=true&mode=mini"],
["Download options", "https://gajim.org/download/"]
]
) %>
</div>
<div id="apps-mac" class="hidden grid grid-cols-1 gap-6"
data-tabs-target="panel">
<%= render AppInfoComponent.new(
name: "Beagle IM",
description: "Lightweight and powerful chat app for macOS",
icon_path: "/img/logos/logo_beagle.png",
links: [
["Website", "https://beagle.im"],
["App Store", "https://apps.apple.com/us/app/beagleim-by-tigase-inc/id1445349494"]
]
) %>
<%= render AppInfoComponent.new(
name: "Monal",
description: "A chat app for iOS, iPadOS, and macOS",
icon_path: "/img/logos/icon_monal.svg",
icon_fill_box: true,
links: [
["Website", "https://monal-im.org"],
["App Store", "https://apps.apple.com/app/id1637078500"]
]
) %>
</div>
<!-- <div class="hidden grid grid&#45;cols&#45;1 gap&#45;4 sm:gap&#45;6" data&#45;tabs&#45;target="panel"> -->
<!-- Web -->
<!-- </div> -->
</div>
</section>
<% end %>

View File

@@ -7,29 +7,17 @@
<section> <section>
<h3>Lightning Address</h3> <h3>Lightning Address</h3>
<p class="mb-6"> <p>
Your Kosmos user address is also a Your Kosmos user address is also a
<a class="ks-text-link" href="https://lightningaddress.com/" target="_blank">Lightning Address</a>! <a class="ks-text-link" href="https://lightningaddress.com/" target="_blank">Lightning Address</a>!
The easiest way to receive sats is by just giving out your address: The easiest way to receive sats is by just giving out your address:
</p> </p>
<p data-controller="clipboard" class="flex gap-1 sm:w-2/5"> <p>
<input type="text" id="user_address" class="grow" <strong><%= current_user.address %></strong>
value=<%= current_user.address %> disabled="disabled"
data-clipboard-target="source" />
<button id="copy-user-address" class="btn-md btn-icon btn-outline shrink-0"
data-clipboard-target="trigger" data-action="clipboard#copy"
title="Copy to clipboard">
<span class="content-initial">
<%= render partial: "icons/copy", locals: { custom_class: "text-blue-600 h-4 w-4 inline" } %>
</span>
<span class="content-active hidden">
<%= render partial: "icons/check", locals: { custom_class: "text-blue-600 h-4 w-4 inline" } %>
</span>
</button>
</p> </p>
</section> </section>
<section data-controller="modal" data-action="keydown.esc->modal#close"> <section>
<h3>Wallet Apps</h3> <h3>Wallet Apps</h3>
<p> <p>
You can connect various wallet apps to your Kosmos account. This allows You can connect various wallet apps to your Kosmos account. This allows
@@ -40,16 +28,19 @@
</p> </p>
<p data-controller="clipboard" class="my-6 text-center md:text-left"> <p data-controller="clipboard" class="my-6 text-center md:text-left">
<input type="text" disabled class="hidden" aria-hidden=true <input type="text" disabled class="hidden" aria-hidden=true
value="<%= @wallet_setup_url %>" data-clipboard-target="source" /> value="<%= @wallet_url%>" data-clipboard-target="source" />
<button id="copy-setup-code" class="btn-md btn-blue w-full sm:w-auto" <button id="copy-setup-code" class="btn-md btn-blue w-full sm:w-auto"
data-action="clipboard#copy" data-clipboard-target="trigger"> data-action="clipboard#copy" data-clipboard-target="trigger">
<span class="content-initial">Copy setup code/URL</span> <span class="content-initial">Copy setup code/URL</span>
<span class="content-active hidden">Copied ✔</span> <span class="content-active hidden">Copied ✔</span>
</button> </button>
<span class="mx-2 my-2 md:my-0 block md:inline">or</span> <span class="mx-2 my-2 md:my-0 block md:inline">or</span>
<button data-action="click->modal#open" class="btn-md btn-blue w-full sm:w-auto">Show setup QR code</button> <button id="show-setup-code" class="btn-md btn-blue w-full sm:w-auto">Show setup QR code</button>
<button id="hide-setup-code" class="hidden btn-md btn-blue w-full sm:w-auto">Hide setup QR code</button>
</p>
<p id="setup-code" class="hidden my-10 w-full text-center">
<%= raw @svg %>
</p> </p>
<%= render QrCodeModalComponent.new(qr_content: @wallet_setup_url) %>
</section> </section>
<section> <section>
@@ -97,22 +88,28 @@
</p> </p>
</div> </div>
</section> </section>
<section class="mb-12">
<h3>QR Code for Donations/Tips</h3>
<p>
You can print out or publish a QR code for people to scan with their
wallet apps, so they can send you sats without a direct personal
interaction (for example at a concert, or on your website).
</p>
<p class="my-6 text-center md:text-left">
<%= link_to "Download SVG file",
qr_lnurlp_services_lightning_index_path(format: "svg"),
class: "btn-md btn-blue w-full sm:w-auto"%>
<span class="mx-2 my-2 md:my-0 block md:inline">or</span>
<%= link_to "Download PNG file",
qr_lnurlp_services_lightning_index_path(format: "png"),
class: "btn-md btn-blue w-full sm:w-auto"%>
</p>
</section>
<% end %> <% end %>
<script type="text/javascript">
(function () {
const buttonShow = document.querySelector('#show-setup-code');
const buttonHide = document.querySelector('#hide-setup-code');
const setupCode = document.querySelector('#setup-code');
buttonShow.addEventListener('click', function(ev) {
ev.preventDefault();
setupCode.classList.remove('hidden');
buttonHide.classList.remove('hidden');
buttonShow.classList.add('hidden');
setupCode.scrollIntoView({behavior: "smooth", block: "nearest"});
});
buttonHide.addEventListener('click', function(ev) {
ev.preventDefault();
const el = document.querySelector('#setup-code');
setupCode.classList.add('hidden');
buttonHide.classList.add('hidden');
buttonShow.classList.remove('hidden');
});
})();
</script>

View File

@@ -1,219 +0,0 @@
<%= render HeaderComponent.new(title: "Social") %>
<%= render MainSimpleComponent.new do %>
<section>
<p class="mb-6">
Follow and interact with anyone on the open social web, from your Kosmos Mastodon account.
</p>
</section>
<section data-controller="modal" data-action="keydown.esc->modal#close">
<h3>Your User Address</h3>
<p class="mb-6">
Others can follow you under this address:
</p>
<p data-controller="clipboard" class="flex gap-1 sm:w-2/5">
<input type="text" id="user_address" class="grow"
value=<%= current_user.mastodon_address %> disabled="disabled"
data-clipboard-target="source" />
<button id="copy-user-address" class="btn-md btn-icon btn-outline shrink-0"
data-clipboard-target="trigger" data-action="clipboard#copy"
title="Copy to clipboard">
<span class="content-initial">
<%= render partial: "icons/copy", locals: { custom_class: "text-blue-600 h-4 w-4 inline" } %>
</span>
<span class="content-active hidden">
<%= render partial: "icons/check", locals: { custom_class: "text-blue-600 h-4 w-4 inline" } %>
</span>
</button>
<button class="btn-md btn-icon btn-outline shrink-0 w-auto"
data-action="click->modal#open" title="Show QR code">
<%= render partial: "icons/qr_code", locals: { custom_class: "text-blue-600 h-4 w-4 inline" } %>
</button>
</p>
<%= render QrCodeModalComponent.new(qr_content: current_user.address) %>
</section>
<section>
<h3>Social Apps</h3>
<p>
Use your Mastodon account with many different apps, and on any devices
you wish! When adding your account to an app, you will log in via
<a href="https://kosmos.social" target="_blank" class="ks-text-link">kosmos.social</a>.
</p>
</section>
<section>
<h3>Recommended Apps</h3>
<div data-controller="tabs"
data-tabs-active-tab-class="-mb-px border-gray-200 border-l border-t border-r rounded-t text-indigo-600 hover:text-indigo-600"
data-tabs-inactive-tab-class="text-gray-500 hover:text-gray-700"
class="mb-12">
<select data-action="tabs#change" data-tabs-target="select"
class="block w-full mb-8 sm:hidden">
<option>Web</option>
<optgroup label="Mobile">
<option>Android</option>
<option>iOS</option>
</optgroup>
<optgroup label="Desktop">
<option>Linux</option>
<option>Windows</option>
<option>macOS</option>
</optgroup>
</select>
<ul class="hidden sm:flex list-reset mb-8 border-gray-200 border-b">
<li class="mr-2" data-tabs-target="tab" data-action="click->tabs#change:prevent">
<a href="#" class="bg-white inline-block py-2 px-4 font-semibold no-underline">
Web
</a>
</li>
<li class="mr-2" data-tabs-target="tab" data-action="click->tabs#change:prevent">
<a href="#" class="bg-white inline-block py-2 px-5 font-semibold no-underline">
Android
</a>
</li>
<li class="mr-2" data-tabs-target="tab" data-action="click->tabs#change:prevent">
<a href="#" class="bg-white inline-block py-2 px-4 font-semibold no-underline">
iOS
</a>
</li>
<li class="mr-2" data-tabs-target="tab" data-action="click->tabs#change:prevent">
<a href="#" class="bg-white inline-block py-2 px-4 font-semibold no-underline">
Linux
</a>
</li>
<li class="mr-2" data-tabs-target="tab" data-action="click->tabs#change:prevent">
<a href="#" class="bg-white inline-block py-2 px-4 font-semibold no-underline">
Windows
</a>
</li>
<li class="mr-2" data-tabs-target="tab" data-action="click->tabs#change:prevent">
<a href="#" class="bg-white inline-block py-2 px-4 font-semibold no-underline">
macOS
</a>
</li>
</ul>
<div class="hidden grid grid-cols-1 gap-6" data-tabs-target="panel">
<%= render AppInfoComponent.new(
name: "kosmos.social",
description: "The official Web app",
icon_path: "/img/logos/icon_mastodon-2.svg",
links: [
["Launch", "https://kosmos.social"]
]
) %>
<%= render AppInfoComponent.new(
name: "Elk",
description: " A nimble Mastodon web client",
icon_path: "/img/logos/icon_elk.svg",
links: [
["Launch", "https://elk.zone"],
["GitHub", "https://github.com/elk-zone/elk"]
]
) %>
<%= render AppInfoComponent.new(
name: "Sengi",
description: "A cross-platform app, inspired by TweetDeck",
icon_path: "/img/logos/icon_sengi.png",
links: [
["Website", "https://nicolasconstant.github.io/sengi/"],
["GitHub", "https://github.com/NicolasConstant/sengi"]
]
) %>
</div>
<div class="hidden grid grid-cols-1 gap-6" data-tabs-target="panel">
<%= render AppInfoComponent.new(
name: "Mastodon for Android",
description: "Android client by the Mastodon core team",
icon_path: "/img/logos/icon_mastodon-2.svg",
links: [
["Website", "https://joinmastodon.org/apps"],
["Google Play", "https://play.google.com/store/apps/details?id=org.joinmastodon.android"]
]
) %>
<%= render AppInfoComponent.new(
name: "Fedilab",
description: "Android client with many features",
icon_path: "/img/logos/icon_fedilab.png",
links: [
["Website", "https://fedilab.app"],
["Google Play", "https://play.google.com/store/apps/details?id=app.fedilab.android"],
["F-Droid", "https://f-droid.org/packages/fr.gouv.etalab.mastodon"],
]
) %>
<%= render AppInfoComponent.new(
name: "Megalodon",
description: "A popular fork of the official Android app",
icon_path: "/img/logos/icon_megalodon.png",
icon_fill_box: true,
links: [
["Website", "https://sk22.github.io/megalodon/"],
["Google Play", "https://play.google.com/store/apps/details?id=org.joinmastodon.android.sk"]
]
) %>
</div>
<div class="hidden grid grid-cols-1 gap-6" data-tabs-target="panel">
<%= render AppInfoComponent.new(
name: "Mastodon for iOS",
description: "iOS client by the Mastodon core team",
icon_path: "/img/logos/icon_mastodon-2.svg",
links: [
["Website", "https://joinmastodon.org/apps"],
["App Store", "https://apps.apple.com/us/app/mastodon-for-iphone/id1571998974"]
]
) %>
<%= render AppInfoComponent.new(
name: "Ice Cubes",
description: "Slick, fast, open source, and with customizable UI",
icon_path: "/img/logos/icon_icecubes.png",
icon_fill_box: true,
links: [
["App Store", "https://apps.apple.com/us/app/ice-cubes-for-mastodon/id6444915884"],
["GitHub", "https://github.com/Dimillian/IceCubesApp"]
]
) %>
<%= render AppInfoComponent.new(
name: "Mammoth",
description: " Powerful, fast, feature-rich",
icon_path: "/img/logos/icon_mammoth.png",
links: [
["Website", "https://getmammoth.app/"],
["App Store", "https://apps.apple.com/app/mammoth-for-mastodon/id1667573899"]
]
) %>
</div>
<div class="hidden grid grid-cols-1 gap-6" data-tabs-target="panel">
<%= render AppInfoComponent.new(
name: "Tuba",
description: "A simple, fast Mastodon app for Linux (good on GNOME)",
icon_path: "/img/logos/icon_tuba.svg",
links: [
["Website", "https://tuba.geopjr.dev"],
["Flathub", "https://flathub.org/apps/dev.geopjr.Tuba"],
]
) %>
</div>
<div class="hidden grid grid-cols-1 gap-6" data-tabs-target="panel">
<%= render AppInfoComponent.new(
name: "Sengi",
description: "A cross-platform app, inspired by TweetDeck",
icon_path: "/img/logos/icon_sengi.png",
links: [
["Website", "https://nicolasconstant.github.io/sengi/"],
["GitHub", "https://github.com/NicolasConstant/sengi"]
]
) %>
</div>
<div class="hidden grid grid-cols-1 gap-6" data-tabs-target="panel">
<%= render AppInfoComponent.new(
name: "Mastonaut",
description: "Simple, elegant, and native Mastodon client for Mac",
icon_path: "/img/logos/icon_mastonaut.png",
links: [
["Launch", "https://www.mastonaut.app"],
["Mac App Store", "https://apps.apple.com/app/mastonaut/id1450757574"]
]
) %>
</div>
</div>
</section>
<% end %>

View File

@@ -13,13 +13,13 @@
'settings--account--email-target': 'emailField' 'settings--account--email-target': 'emailField'
}, required: true %> }, required: true %>
<button type="button" id="edit-email" <button type="button" id="edit-email"
class="btn-md btn-icon btn-outline shrink-0 hidden initial-visible" class="btn-md btn-icon btn-blue shrink-0 hidden initial-visible"
data-settings--account--email-target="editEmailButton" data-settings--account--email-target="editEmailButton"
data-action="settings--account--email#editEmail" data-action="settings--account--email#editEmail"
title="Edit email address"> title="Edit email address">
<span class=""> <span class="">
<%= render partial: "icons/edit-3", locals: { <%= render partial: "icons/edit-3", locals: {
custom_class: "text-blue-600 h-4 w-4 inline" } %> custom_class: "text-white h-4 w-4 inline" } %>
</span> </span>
</button> </button>
</p> </p>

View File

@@ -1,89 +0,0 @@
<section>
<h3>Nostr</h3>
<h4 class="mb-0">Public Key</h4>
<div data-controller="settings--nostr-pubkey"
data-settings--nostr-pubkey-user-address-value="<%= current_user.address %>"
data-settings--nostr-pubkey-shared-secret-value="<%= session[:shared_secret] %>"
data-settings--nostr-pubkey-pubkey-hex-value="<%= current_user.nostr_pubkey %>">
<p class="<%= current_user.nostr_pubkey.present? ? '' : 'hidden' %> mt-2 flex gap-1">
<input type="text" value="<%= current_user.nostr_pubkey %>" disabled
data-settings--nostr-pubkey-target="pubkeyBech32Input"
name="nostr_public_key" class="relative grow" />
<%= link_to nostr_pubkey_settings_path,
class: 'btn-md btn-outline text-red-700 relative shrink-0',
data: { turbo_method: :delete, turbo_confirm: 'Are you sure?' } do %>
Remove
<% end %>
</p>
<% if current_user.nostr_pubkey.present? %>
<div class="rounded-md bg-blue-50 p-4">
<div class="flex">
<div class="flex-shrink-0">
<svg class="h-5 w-5 text-blue-400" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a.75.75 0 000 1.5h.253a.25.25 0 01.244.304l-.459 2.066A1.75 1.75 0 0010.747 15H11a.75.75 0 000-1.5h-.253a.25.25 0 01-.244-.304l.459-2.066A1.75 1.75 0 009.253 9H9z" clip-rule="evenodd" />
</svg>
</div>
<div class="ml-3 flex-1">
<p class="text-sm text-blue-800">
Your user address <strong><%= current_user.address %></strong> is
also a Nostr address now. Use your favorite Nostr app, or for
example <a href="http://metadata.nostr.com" target="_blank"
class="underline">metadata.nostr.com</a>, to add this
<strong>NIP-05</strong> address to your public profile.
</p>
</div>
</div>
</div>
<% else %>
<p class="my-4">
If you use any apps on the Nostr network, you can verify your public key
with us in order to enable Nostr-specific features for your account.
</p>
<% end %>
<div data-settings--nostr-pubkey-target="noExtension"
class="hidden rounded-md bg-blue-50 p-4">
<div class="flex">
<div class="flex-shrink-0">
<svg class="h-5 w-5 text-blue-400" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a.75.75 0 000 1.5h.253a.25.25 0 01.244.304l-.459 2.066A1.75 1.75 0 0010.747 15H11a.75.75 0 000-1.5h-.253a.25.25 0 01-.244-.304l.459-2.066A1.75 1.75 0 009.253 9H9z" clip-rule="evenodd" />
</svg>
</div>
<div class="ml-3">
<h3 class="mb-0 text-sm font-bold text-blue-800">
No browser extension found
</h3>
<div class="mt-2 mb-0 text-sm text-blue-800">
<p>
We recommend Alby, which you can also use for your Lightning
Wallet.
</p>
</div>
<div class="mt-4">
<div class="-mx-2 -my-1.5 flex">
<a href="https://getalby.com" target="_blank"
class="rounded-md bg-blue-50 px-2 py-1.5 text-sm
font-bold text-blue-800 hover:bg-blue-100
focus:outline-none focus:ring-2 focus:ring-blue-600
focus:ring-offset-2 focus:ring-offset-blue-50">
Get Alby
</a>
</div>
</div>
</div>
</div>
</div>
<% unless current_user.nostr_pubkey.present? %>
<p class="mt-8">
<button class="btn-md btn-gray w-full sm:w-auto" disabled
data-settings--nostr-pubkey-target="setPubkey"
data-action="settings--nostr-pubkey#setPubkey">
Get public key from browser extension
</button>
</p>
<% end %>
</div>
</section>

View File

@@ -1,62 +1,33 @@
<section> <section>
<h3>Profile</h3> <h3>Profile</h3>
<div class="mb-6"> <p class="mb-2">
<p class="mb-2"> <%= label :user_address, 'User address', class: 'font-bold' %>
<%= label :user_address, 'User address', class: 'font-bold' %> </p>
</p> <p data-controller="clipboard" class="flex gap-1 mb-2 sm:w-3/5">
<p data-controller="clipboard" class="flex gap-1 mb-2 sm:w-3/5"> <input type="text" id="user_address" class="grow"
<input type="text" id="user_address" class="grow" value=<%= @user.address %> disabled="disabled"
value=<%= @user.address %> disabled="disabled" data-clipboard-target="source" />
data-clipboard-target="source" /> <button id="copy-user-address" class="btn-md btn-icon btn-blue shrink-0"
<button id="copy-user-address" class="btn-md btn-icon btn-outline shrink-0" data-clipboard-target="trigger" data-action="clipboard#copy"
data-clipboard-target="trigger" data-action="clipboard#copy" title="Copy to clipboard">
title="Copy to clipboard"> <span class="content-initial">
<span class="content-initial"> <%= render partial: "icons/copy", locals: { custom_class: "text-white h-4 w-4 inline" } %>
<%= render partial: "icons/copy", locals: { custom_class: "text-blue-600 h-4 w-4 inline" } %> </span>
</span> <span class="content-active hidden">
<span class="content-active hidden"> <%= render partial: "icons/check", locals: { custom_class: "text-white h-4 w-4 inline" } %>
<%= render partial: "icons/check", locals: { custom_class: "text-blue-600 h-4 w-4 inline" } %> </span>
</span> </button>
</button> </p>
</p> <p class="text-sm text-gray-500">
<p class="text-sm text-gray-500"> Your user address for Chat and Lightning Network.
Your user address for Chat and Lightning Network. </p>
</p>
</div>
<%= form_for(@user, url: setting_path(:profile), html: { :method => :put }) do |f| %> <%= form_for(@user, url: setting_path(:profile), html: { :method => :put }) do |f| %>
<%= render FormElements::FieldsetComponent.new(tag: "div", title: "Display name") do %> <%= render FormElements::FieldsetComponent.new(tag: "div", title: "Display name") do %>
<%= f.text_field :display_name, class: "w-full sm:w-3/5" %> <%= f.text_field :display_name, class: "w-full sm:w-3/5 mb-2" %>
<% if @validation_errors.present? && @validation_errors[:display_name].present? %> <% if @validation_errors.present? && @validation_errors[:display_name].present? %>
<p class="error-msg mt-2"><%= @validation_errors[:display_name].first %></p> <p class="error-msg"><%= @validation_errors[:display_name].first %></p>
<% end %> <% end %>
<% end %> <% end %>
<label class="block">
<p class="font-bold mb-1">
Avatar
</p>
<p class="text-gray-500">
Default profile picture
</p>
<div class="flex items-center gap-6">
<% if current_user.avatar.present? %>
<p class="flex-none">
<%= image_tag "data:image/jpeg;base64,#{current_user.avatar}", class: "h-24 w-24 rounded-lg" %>
</p>
<% end %>
<div class="grow">
<p class="mb-2">
<%= f.file_field :avatar, class: "" %>
<p class="text-sm text-gray-500">
JPEG or PNG image, not larger than 1 megabyte
</p>
<% if @validation_errors.present? && @validation_errors[:avatar].present? %>
<p class="error-msg mb-2"><%= @validation_errors[:avatar].first %></p>
<% end %>
</div>
</div>
</label>
<p class="mt-8 pt-6 border-t border-gray-200 text-right"> <p class="mt-8 pt-6 border-t border-gray-200 text-right">
<%= f.submit 'Save', class: "btn-md btn-blue w-full md:w-auto" %> <%= f.submit 'Save', class: "btn-md btn-blue w-full md:w-auto" %>
</p> </p>

View File

@@ -1,70 +1,56 @@
<%= render SidenavLinkComponent.new(
level: 2,
name: "BTCPay",
path: admin_settings_services_path(params: { s: "btcpay" }),
text_icon: Setting.btcpay_enabled? ? "◉" : "○",
active: current_page?(admin_settings_services_path(params: { s: "btcpay" })),
) %>
<%= render SidenavLinkComponent.new( <%= render SidenavLinkComponent.new(
level: 2, level: 2,
name: "Discourse", name: "Discourse",
path: admin_settings_services_path(params: { s: "discourse" }), path: admin_settings_services_path(params: { s: "discourse" }),
text_icon: Setting.discourse_enabled? ? "" : "", icon: Setting.discourse_enabled? ? "check" : "x",
active: current_page?(admin_settings_services_path(params: { s: "discourse" })), active: current_page?(admin_settings_services_path(params: { s: "discourse" })),
) %> ) %>
<%= render SidenavLinkComponent.new(
level: 2,
name: "Drone CI",
path: admin_settings_services_path(params: { s: "droneci" }),
text_icon: Setting.droneci_enabled? ? "◉" : "○",
active: current_page?(admin_settings_services_path(params: { s: "droneci" })),
) %>
<%= render SidenavLinkComponent.new( <%= render SidenavLinkComponent.new(
level: 2, level: 2,
name: "ejabberd", name: "ejabberd",
path: admin_settings_services_path(params: { s: "ejabberd" }), path: admin_settings_services_path(params: { s: "ejabberd" }),
text_icon: Setting.ejabberd_enabled? ? "" : "", icon: Setting.ejabberd_enabled? ? "check" : "x",
active: current_page?(admin_settings_services_path(params: { s: "ejabberd" })), active: current_page?(admin_settings_services_path(params: { s: "ejabberd" })),
) %> ) %>
<%= render SidenavLinkComponent.new( <%= render SidenavLinkComponent.new(
level: 2, level: 2,
name: "Gitea", name: "Gitea",
path: admin_settings_services_path(params: { s: "gitea" }), path: admin_settings_services_path(params: { s: "gitea" }),
text_icon: Setting.gitea_enabled? ? "" : "", icon: Setting.gitea_enabled? ? "check" : "x",
active: current_page?(admin_settings_services_path(params: { s: "gitea" })), active: current_page?(admin_settings_services_path(params: { s: "gitea" })),
) %> ) %>
<%= render SidenavLinkComponent.new( <%= render SidenavLinkComponent.new(
level: 2, level: 2,
name: "LNDHub", name: "LNDHub",
path: admin_settings_services_path(params: { s: "lndhub" }), path: admin_settings_services_path(params: { s: "lndhub" }),
text_icon: Setting.lndhub_enabled? ? "" : "", icon: Setting.lndhub_enabled? ? "check" : "x",
active: current_page?(admin_settings_services_path(params: { s: "lndhub" })), active: current_page?(admin_settings_services_path(params: { s: "lndhub" })),
) %> ) %>
<%= render SidenavLinkComponent.new( <%= render SidenavLinkComponent.new(
level: 2, level: 2,
name: "Mastodon", name: "Mastodon",
path: admin_settings_services_path(params: { s: "mastodon" }), path: admin_settings_services_path(params: { s: "mastodon" }),
text_icon: Setting.mastodon_enabled? ? "" : "", icon: Setting.mastodon_enabled? ? "check" : "x",
active: current_page?(admin_settings_services_path(params: { s: "mastodon" })), active: current_page?(admin_settings_services_path(params: { s: "mastodon" })),
) %> ) %>
<%= render SidenavLinkComponent.new( <%= render SidenavLinkComponent.new(
level: 2, level: 2,
name: "MediaWiki", name: "MediaWiki",
path: admin_settings_services_path(params: { s: "mediawiki" }), path: admin_settings_services_path(params: { s: "mediawiki" }),
text_icon: Setting.mediawiki_enabled? ? "" : "", icon: Setting.mediawiki_enabled? ? "check" : "x",
active: current_page?(admin_settings_services_path(params: { s: "mediawiki" })), active: current_page?(admin_settings_services_path(params: { s: "mediawiki" })),
) %> ) %>
<%= render SidenavLinkComponent.new( <%= render SidenavLinkComponent.new(
level: 2, level: 2,
name: "Nostr", name: "Nostr",
path: admin_settings_services_path(params: { s: "nostr" }), path: admin_settings_services_path(params: { s: "nostr" }),
text_icon: Setting.nostr_enabled? ? "" : "", icon: Setting.nostr_enabled? ? "check" : "x",
active: current_page?(admin_settings_services_path(params: { s: "nostr" })), active: current_page?(admin_settings_services_path(params: { s: "nostr" })),
) %> ) %>
<%= render SidenavLinkComponent.new( <%= render SidenavLinkComponent.new(
level: 2, level: 2,
name: "RemoteStorage", name: "RemoteStorage",
path: admin_settings_services_path(params: { s: "remotestorage" }), path: admin_settings_services_path(params: { s: "remotestorage" }),
text_icon: Setting.remotestorage_enabled? ? "" : "", icon: Setting.remotestorage_enabled? ? "check" : "x",
active: current_page?(admin_settings_services_path(params: { s: "remotestorage" })), active: current_page?(admin_settings_services_path(params: { s: "remotestorage" })),
) %> ) %>

View File

@@ -18,9 +18,3 @@
active: @settings_section.to_s == "lightning" active: @settings_section.to_s == "lightning"
) %> ) %>
<% end %> <% end %>
<% if Setting.nostr_enabled %>
<%= render SidenavLinkComponent.new(
name: "Experiments", path: setting_path(:experiments), icon: "science",
active: @settings_section.to_s == "experiments"
) %>
<% end %>

View File

@@ -1,12 +1,8 @@
<div class="border-b border-gray-200"> <%= render HeaderTabLinkComponent.new(
<nav class="-mb-px flex" aria-label="Tabs"> name: "Donations", path: contributions_donations_path,
<%= render TabnavLinkComponent.new( active: current_page?(contributions_donations_path)
name: "Donations", path: contributions_donations_path, ) %>
active: current_page?(contributions_donations_path) <%= render HeaderTabLinkComponent.new(
) %> name: "Projects", path: contributions_projects_path,
<%= render TabnavLinkComponent.new( active: current_page?(contributions_projects_path)
name: "Projects", path: contributions_projects_path, ) %>
active: current_page?(contributions_projects_path)
) %>
</nav>
</div>

View File

@@ -1,6 +0,0 @@
<%= render HeaderCompactComponent.new(title: "404") %>
<%= render MainCompactComponent.new do %>
<h2>Bad request</h2>
<p>Please go back and try again.</p>
<% end %>

View File

@@ -10,7 +10,7 @@
<%= f.text_field :cn, autofocus: true, autocomplete: "username", <%= f.text_field :cn, autofocus: true, autocomplete: "username",
required: true, class: "relative grow text-xl"%> required: true, class: "relative grow text-xl"%>
<span class="relative shrink-0 text-gray-500 md:text-xl"> <span class="relative shrink-0 text-gray-500 md:text-xl">
@ <%= Setting.primary_domain %> @ kosmos.org
</span> </span>
</p> </p>
</div> </div>

Some files were not shown because too many files have changed in this diff Show More